Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add subscriber video stats to analytics #2581

Merged
merged 6 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,11 @@ export abstract class BaseStatsAnalytics {
protected abstract handleStatsUpdate(hmsStats: HMSWebrtcStats): void;
}

type TempPublishStats = HMSTrackStats & { availableOutgoingBitrate?: number };
type TempPublishStats = HMSTrackStats & {
availableOutgoingBitrate?: number;
calculatedJitterBufferDelay?: number;
avSync?: number;
};

export abstract class RunningTrackAnalytics {
readonly sampleWindowSize: number;
Expand Down Expand Up @@ -158,6 +162,11 @@ export abstract class RunningTrackAnalytics {
return latestValue - firstValue;
}

protected calculateDifferenceAverage(key: keyof TempPublishStats, round = true) {
const avg = this.calculateDifferenceForSample(key) / this.tempStats.length;
return round ? Math.round(avg) : avg;
}

protected calculateInstancesOfHigh(key: keyof TempPublishStats, threshold: number) {
const checkStat = this.getLatestStat()[key];
if (typeof checkStat !== 'number') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ import {
RemoteVideoTrackAnalytics,
SubscribeAnalyticPayload,
} from './interfaces';
import { HMSTrackStats } from '../../interfaces';
import { HMSWebrtcStats } from '../../rtc-stats';
import { SUBSCRIBE_STATS_SAMPLE_WINDOW } from '../../utils/constants';
import { JAVA_INTEGER_MAX_VALUE, SUBSCRIBE_STATS_SAMPLE_WINDOW } from '../../utils/constants';
import AnalyticsEventFactory from '../AnalyticsEventFactory';

export class SubscribeStatsAnalytics extends BaseStatsAnalytics {
Expand Down Expand Up @@ -49,8 +50,14 @@ export class SubscribeStatsAnalytics extends BaseStatsAnalytics {
Object.keys(remoteTracksStats).forEach(trackID => {
const trackStats = remoteTracksStats[trackID];
const track = this.store.getTrackById(trackID);
const calculatedJitterBufferDelay =
trackStats.jitterBufferDelay &&
trackStats.jitterBufferEmittedCount &&
trackStats.jitterBufferDelay / trackStats.jitterBufferEmittedCount;

const avSync = this.calculateAvSyncForStat(trackStats, hmsStats);
if (this.trackAnalytics.has(trackID)) {
this.trackAnalytics.get(trackID)?.pushTempStat({ ...trackStats });
this.trackAnalytics.get(trackID)?.pushTempStat({ ...trackStats, calculatedJitterBufferDelay, avSync });
} else {
if (track) {
const trackAnalytics = new RunningRemoteTrackAnalytics({
Expand All @@ -59,7 +66,7 @@ export class SubscribeStatsAnalytics extends BaseStatsAnalytics {
ssrc: trackStats.ssrc.toString(),
kind: trackStats.kind,
});
trackAnalytics.pushTempStat({ ...trackStats });
trackAnalytics.pushTempStat({ ...trackStats, calculatedJitterBufferDelay, avSync });
this.trackAnalytics.set(trackID, trackAnalytics);
}
}
Expand All @@ -75,6 +82,25 @@ export class SubscribeStatsAnalytics extends BaseStatsAnalytics {
});
}
}

// eslint-disable-next-line complexity
private calculateAvSyncForStat(trackStats: HMSTrackStats, hmsStats: HMSWebrtcStats) {
if (!trackStats.peerID || !(trackStats.kind === 'video')) {
return;
}
const peer = this.store.getPeerById(trackStats.peerID);
const audioTrack = peer?.audioTrack;
const videoTrack = peer?.videoTrack;
const areBothTracksEnabled = audioTrack && videoTrack && audioTrack.enabled && videoTrack.enabled;
if (!areBothTracksEnabled) {
return JAVA_INTEGER_MAX_VALUE;
raviteja83 marked this conversation as resolved.
Show resolved Hide resolved
}
const audioStats = hmsStats.getRemoteTrackStats(audioTrack.trackId);
if (!audioStats) {
return JAVA_INTEGER_MAX_VALUE;
}
return audioStats.timestamp - trackStats.timestamp;
}
}

class RunningRemoteTrackAnalytics extends RunningTrackAnalytics {
Expand All @@ -96,7 +122,21 @@ class RunningRemoteTrackAnalytics extends RunningTrackAnalytics {
};

if (latestStat.kind === 'video') {
return removeUndefinedFromObject<RemoteAudioSample | RemoteVideoSample>(baseSample);
return removeUndefinedFromObject<RemoteAudioSample | RemoteVideoSample>(
Object.assign(baseSample, {
avg_av_sync_ms: this.calculateAvgAvSyncForSample(),
avg_frames_received_per_sec: this.calculateDifferenceAverage('framesReceived'),
avg_frames_dropped_per_sec: this.calculateDifferenceAverage('framesDropped'),
avg_frames_decoded_per_sec: this.calculateDifferenceAverage('framesDecoded'),
frame_width: this.calculateAverage('frameWidth'),
frame_height: this.calculateAverage('frameHeight'),
pause_count: this.calculateDifferenceForSample('pauseCount'),
pause_duration_seconds: this.calculateDifferenceForSample('totalPausesDuration'),
freeze_count: this.calculateDifferenceForSample('freezeCount'),
freeze_duration_seconds: this.calculateDifferenceForSample('totalFreezesDuration'),
avg_jitter_buffer_delay: this.calculateAverage('calculatedJitterBufferDelay'),
}),
);
} else {
const audio_concealed_samples =
(latestStat.concealedSamples || 0) -
Expand Down Expand Up @@ -135,4 +175,15 @@ class RunningRemoteTrackAnalytics extends RunningTrackAnalytics {
samples: this.samples,
};
};

private calculateAvgAvSyncForSample() {
const avSyncValues = this.tempStats.map(stat => stat.avSync);
const validAvSyncValues: number[] = avSyncValues.filter(
(value): value is number => value !== undefined && value !== JAVA_INTEGER_MAX_VALUE,
);
if (validAvSyncValues.length === 0) {
return JAVA_INTEGER_MAX_VALUE;
}
return validAvSyncValues.reduce((a, b) => a + b, 0) / validAvSyncValues.length;
}
}
7 changes: 7 additions & 0 deletions packages/hms-video-store/src/analytics/stats/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,11 @@ export interface RemoteVideoSample extends RemoteBaseSample {
avg_frames_decoded_per_sec?: number;
total_pli_count?: number;
total_nack_count?: number;
frame_width?: number;
frame_height?: number;
pause_count?: number;
pause_duration_seconds?: number;
freeze_count?: number;
freeze_duration_seconds?: number;
avg_jitter_buffer_delay?: number;
}
7 changes: 7 additions & 0 deletions packages/hms-video-store/src/interfaces/webrtc-stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ interface MissingOutboundStats extends RTCOutboundRtpStreamStats, MissingCommonS

export interface MissingInboundStats extends RTCInboundRtpStreamStats, MissingCommonStats {
bytesReceived?: number;
framesReceived?: number;
framesDropped?: number;
jitter?: number;
packetsLost?: number;
Expand All @@ -50,6 +51,12 @@ export interface MissingInboundStats extends RTCInboundRtpStreamStats, MissingCo
fecPacketsDiscarded?: number;
fecPacketsReceived?: number;
totalSamplesDuration?: number;
pauseCount?: number;
totalPausesDuration?: number;
freezeCount?: number;
totalFreezesDuration?: number;
jitterBufferDelay?: number;
jitterBufferEmittedCount?: number;
}

export type PeerConnectionType = 'publish' | 'subscribe';
Expand Down
8 changes: 4 additions & 4 deletions packages/hms-video-store/src/rtc-stats/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,15 +116,15 @@ export const getTrackStats = async (
}

return (
trackStats &&
Object.assign(trackStats, {
trackStats && {
...trackStats,
bitrate,
packetsLostRate,
peerId: track.peerId,
peerID: track.peerId,
enabled: track.enabled,
peerName,
codec: trackStats.codec,
})
}
);
};

Expand Down
2 changes: 2 additions & 0 deletions packages/hms-video-store/src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export const PUBLISH_STATS_PUSH_INTERVAL = 300;
export const SUBSCRIBE_STATS_SAMPLE_WINDOW = 10;
export const SUBSCRIBE_STATS_PUSH_INTERVAL = 60;

export const JAVA_INTEGER_MAX_VALUE = 2147483647;
raviteja83 marked this conversation as resolved.
Show resolved Hide resolved

export const HMSEvents = {
DEVICE_CHANGE: 'device-change',
LOCAL_AUDIO_ENABLED: 'local-audio-enabled',
Expand Down
Loading