Skip to content

Commit e6ed8ee

Browse files
authored
Expose track-level upstream & downstream stats (#103)
Sample app is updated to surface bandwidth stats and time to first render. Also fixed dynacast for FireFox.
1 parent 1065124 commit e6ed8ee

File tree

13 files changed

+367
-83
lines changed

13 files changed

+367
-83
lines changed

example/sample.ts

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import {
22
DataPacket_Kind, LocalParticipant,
3+
LocalTrack,
34
MediaDeviceFailure,
45
Participant,
56
ParticipantEvent,
6-
RemoteParticipant, Room, RoomConnectOptions, RoomEvent,
7+
RemoteAudioTrack,
8+
RemoteParticipant, RemoteVideoTrack, Room, RoomConnectOptions, RoomEvent,
79
RoomOptions, RoomState, setLogLevel, Track, TrackPublication,
810
VideoCaptureOptions, VideoPresets,
911
} from '../src/index';
@@ -16,8 +18,8 @@ const state = {
1618
encoder: new TextEncoder(),
1719
decoder: new TextDecoder(),
1820
defaultDevices: new Map<MediaDeviceKind, string>(),
21+
bitrateInterval: undefined as any,
1922
};
20-
2123
let currentRoom: Room | undefined;
2224

2325
// handles actions from the HTML
@@ -56,6 +58,8 @@ const appActions = {
5658
await room.localParticipant.enableCameraAndMicrophone();
5759
updateButtonsForPublishState();
5860
}
61+
62+
state.bitrateInterval = setInterval(renderBitrate, 1000);
5963
},
6064

6165
connectToRoom: async (
@@ -202,6 +206,9 @@ const appActions = {
202206
if (currentRoom) {
203207
currentRoom.disconnect();
204208
}
209+
if (state.bitrateInterval) {
210+
clearInterval(state.bitrateInterval);
211+
}
205212
},
206213

207214
disconnectSignal: () => {
@@ -342,7 +349,11 @@ function renderParticipant(participant: Participant, remove: boolean = false) {
342349
<div class="info-bar">
343350
<div id="name-${participant.sid}" class="name">
344351
</div>
345-
<div id="size-${participant.sid}" class="size">
352+
<div style="text-align: center;">
353+
<span id="size-${participant.sid}" class="size">
354+
</span>
355+
<span id="bitrate-${participant.sid}" class="bitrate">
356+
</span>
346357
</div>
347358
<div class="right">
348359
<span id="signal-${participant.sid}"></span>
@@ -387,11 +398,18 @@ function renderParticipant(participant: Participant, remove: boolean = false) {
387398

388399
const cameraEnabled = cameraPub && cameraPub.isSubscribed && !cameraPub.isMuted;
389400
if (cameraEnabled) {
390-
cameraPub?.videoTrack?.attach(videoElm);
391401
if (participant instanceof LocalParticipant) {
392402
// flip
393403
videoElm.style.transform = 'scale(-1, 1)';
404+
} else if (!cameraPub?.videoTrack?.attachedElements.includes(videoElm)) {
405+
const startTime = Date.now();
406+
// measure time to render
407+
videoElm.addEventListener('loadeddata', () => {
408+
const elapsed = Date.now() - startTime;
409+
appendLog(`RemoteVideoTrack ${cameraPub?.trackSid} rendered in ${elapsed}ms`);
410+
});
394411
}
412+
cameraPub?.videoTrack?.attach(videoElm);
395413
} else if (cameraPub?.videoTrack) {
396414
// detach manually whenever possible
397415
cameraPub.videoTrack?.detach(videoElm);
@@ -465,6 +483,32 @@ function renderScreenShare() {
465483
}
466484
}
467485

486+
function renderBitrate() {
487+
if (!currentRoom || currentRoom.state !== RoomState.Connected) {
488+
return;
489+
}
490+
const participants: Participant[] = [...currentRoom.participants.values()];
491+
participants.push(currentRoom.localParticipant);
492+
493+
for (const p of participants) {
494+
const elm = $(`bitrate-${p.sid}`);
495+
let totalBitrate = 0;
496+
for (const t of p.tracks.values()) {
497+
if (t.track instanceof RemoteAudioTrack || t.track instanceof RemoteVideoTrack
498+
|| t.track instanceof LocalTrack) {
499+
totalBitrate += t.track.currentBitrate;
500+
}
501+
}
502+
let displayText = '';
503+
if (totalBitrate > 0) {
504+
displayText = `${Math.round(totalBitrate / 1024).toLocaleString()} kbps`;
505+
}
506+
if (elm) {
507+
elm.innerHTML = displayText;
508+
}
509+
}
510+
}
511+
468512
function updateVideoSize(element: HTMLVideoElement, target: HTMLElement) {
469513
target.innerHTML = `(${element.videoWidth}x${element.videoHeight})`;
470514
}

src/room/participant/LocalParticipant.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,8 @@ export default class LocalParticipant extends Participant {
416416
const disableLayerPause = this.roomOptions?.expDisableLayerPause ?? false;
417417
if (track instanceof LocalVideoTrack && !disableLayerPause) {
418418
track.startMonitor(this.engine.client);
419+
} else if (track instanceof LocalAudioTrack) {
420+
track.startMonitor();
419421
}
420422

421423
if (opts.videoCodec) {

src/room/participant/RemoteParticipant.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ export default class RemoteParticipant extends Participant {
127127
track.source = publication.source;
128128
// keep publication's muted status
129129
track.isMuted = publication.isMuted;
130+
track.receiver = receiver;
131+
track.startMonitor();
130132

131133
// when media track is ended, fire the event
132134
mediaTrack.onended = () => {

src/room/participant/publishUtils.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ describe('computeVideoEncodings', () => {
4141
const encodings = computeVideoEncodings(false, 640, 480, {
4242
simulcast: false,
4343
});
44-
expect(encodings).toBeUndefined();
44+
expect(encodings).toEqual([{}]);
4545
});
4646

4747
it('respects client defined bitrate', () => {

src/room/participant/publishUtils.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,17 +60,17 @@ export function computeVideoEncodings(
6060
width?: number,
6161
height?: number,
6262
options?: TrackPublishOptions,
63-
): RTCRtpEncodingParameters[] | undefined {
63+
): RTCRtpEncodingParameters[] {
6464
let videoEncoding: VideoEncoding | undefined = options?.videoEncoding;
6565
if (isScreenShare) {
6666
videoEncoding = options?.screenShareEncoding;
6767
}
6868
const useSimulcast = !isScreenShare && options?.simulcast;
6969

7070
if ((!videoEncoding && !useSimulcast) || !width || !height) {
71-
// don't set encoding when we are not simulcasting and user isn't restricting
72-
// encoding parameters
73-
return;
71+
// when we aren't simulcasting, will need to return a single encoding without
72+
// capping bandwidth. we always require a encoding for dynacast
73+
return [{}];
7474
}
7575

7676
if (!videoEncoding) {

src/room/stats.ts

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ interface SenderStats {
55
/** number of packets sent */
66
packetsSent?: number;
77

8+
/** number of bytes sent */
9+
bytesSent?: number;
10+
811
/** jitter as perceived by remote */
912
jitter?: number;
1013

@@ -16,6 +19,8 @@ interface SenderStats {
1619

1720
/** ID of the outbound stream */
1821
streamId?: string;
22+
23+
timestamp: number;
1924
}
2025

2126
export interface AudioSenderStats extends SenderStats {
@@ -45,8 +50,6 @@ export interface VideoSenderStats extends SenderStats {
4550
qualityLimitationResolutionChanges: number;
4651

4752
retransmittedPacketsSent: number;
48-
49-
timestamp: number;
5053
}
5154

5255
interface ReceiverStats {
@@ -58,7 +61,29 @@ interface ReceiverStats {
5861
/** number of packets sent */
5962
packetsReceived?: number;
6063

64+
bytesReceived?: number;
65+
6166
streamId?: string;
67+
68+
jitter?: number;
69+
70+
timestamp: number;
71+
}
72+
73+
export interface AudioReceiverStats extends ReceiverStats {
74+
type: 'audio';
75+
76+
concealedSamples?: number;
77+
78+
concealmentEvents?: number;
79+
80+
silentConcealedSamples?: number;
81+
82+
silentConcealmentEvents?: number;
83+
84+
totalAudioEnergy?: number;
85+
86+
totalSamplesDuration?: number;
6287
}
6388

6489
export interface VideoReceiverStats extends ReceiverStats {
@@ -70,13 +95,36 @@ export interface VideoReceiverStats extends ReceiverStats {
7095

7196
framesReceived: number;
7297

73-
frameWidth: number;
98+
frameWidth?: number;
7499

75-
frameHeight: number;
100+
frameHeight?: number;
76101

77-
firCount: number;
102+
firCount?: number;
78103

79-
pliCount: number;
104+
pliCount?: number;
80105

81-
nackCount: number;
106+
nackCount?: number;
107+
}
108+
109+
export function computeBitrate<T extends ReceiverStats | SenderStats>(
110+
currentStats: T,
111+
prevStats?: T,
112+
): number {
113+
if (!prevStats) {
114+
return 0;
115+
}
116+
let bytesNow: number | undefined;
117+
let bytesPrev: number | undefined;
118+
if ('bytesReceived' in currentStats) {
119+
bytesNow = (currentStats as ReceiverStats).bytesReceived;
120+
bytesPrev = (prevStats as ReceiverStats).bytesReceived;
121+
} else if ('bytesSent' in currentStats) {
122+
bytesNow = (currentStats as SenderStats).bytesSent;
123+
bytesPrev = (prevStats as SenderStats).bytesSent;
124+
}
125+
if (bytesNow === undefined || bytesPrev === undefined
126+
|| currentStats.timestamp === undefined || prevStats.timestamp === undefined) {
127+
return 0;
128+
}
129+
return ((bytesNow - bytesPrev) * 8 * 1000) / (currentStats.timestamp - prevStats.timestamp);
82130
}

src/room/track/LocalAudioTrack.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import log from '../../logger';
2+
import { AudioSenderStats, computeBitrate, monitorFrequency } from '../stats';
23
import LocalTrack from './LocalTrack';
34
import { AudioCaptureOptions } from './options';
45
import { Track } from './Track';
@@ -10,7 +11,7 @@ export default class LocalAudioTrack extends LocalTrack {
1011
/** @internal */
1112
stopOnMute: boolean = false;
1213

13-
/** @internal */
14+
private prevStats?: AudioSenderStats;
1415

1516
constructor(
1617
mediaTrack: MediaStreamTrack,
@@ -59,4 +60,53 @@ export default class LocalAudioTrack extends LocalTrack {
5960
}
6061
await this.restart(constraints);
6162
}
63+
64+
/* @internal */
65+
startMonitor() {
66+
setTimeout(() => {
67+
this.monitorSender();
68+
}, monitorFrequency);
69+
}
70+
71+
private monitorSender = async () => {
72+
if (!this.sender) {
73+
this._currentBitrate = 0;
74+
return;
75+
}
76+
const stats = await this.getSenderStats();
77+
78+
if (stats && this.prevStats) {
79+
this._currentBitrate = computeBitrate(stats, this.prevStats);
80+
}
81+
82+
this.prevStats = stats;
83+
setTimeout(() => {
84+
this.monitorSender();
85+
}, monitorFrequency);
86+
};
87+
88+
async getSenderStats(): Promise<AudioSenderStats | undefined> {
89+
if (!this.sender) {
90+
return undefined;
91+
}
92+
93+
const stats = await this.sender.getStats();
94+
let audioStats: AudioSenderStats | undefined;
95+
stats.forEach((v) => {
96+
if (v.type === 'outbound-rtp') {
97+
audioStats = {
98+
type: 'audio',
99+
streamId: v.id,
100+
packetsSent: v.packetsSent,
101+
packetsLost: v.packetsLost,
102+
bytesSent: v.bytesSent,
103+
timestamp: v.timestamp,
104+
roundTripTime: v.roundTripTime,
105+
jitter: v.jitter,
106+
};
107+
}
108+
});
109+
110+
return audioStats;
111+
}
62112
}

src/room/track/LocalTrack.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export default class LocalTrack extends Track {
1010

1111
protected constraints: MediaTrackConstraints;
1212

13+
protected _currentBitrate: number = 0;
14+
1315
protected constructor(
1416
mediaTrack: MediaStreamTrack, kind: Track.Kind, constraints?: MediaTrackConstraints,
1517
) {
@@ -37,6 +39,11 @@ export default class LocalTrack extends Track {
3739
return undefined;
3840
}
3941

42+
/** current send bits per second */
43+
get currentBitrate(): number {
44+
return this._currentBitrate;
45+
}
46+
4047
/**
4148
* @returns DeviceID of the device that is currently being used for this track
4249
*/

0 commit comments

Comments
 (0)