@stream-io/video-client 1.13.1 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/index.browser.es.js +1704 -1762
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +1706 -1780
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +1704 -1762
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +61 -30
- package/dist/src/StreamSfuClient.d.ts +4 -5
- package/dist/src/devices/CameraManager.d.ts +5 -8
- package/dist/src/devices/InputMediaDeviceManager.d.ts +5 -5
- package/dist/src/devices/MicrophoneManager.d.ts +7 -2
- package/dist/src/devices/ScreenShareManager.d.ts +1 -2
- package/dist/src/gen/coordinator/index.d.ts +904 -515
- package/dist/src/gen/video/sfu/event/events.d.ts +38 -19
- package/dist/src/gen/video/sfu/models/models.d.ts +76 -9
- package/dist/src/helpers/array.d.ts +7 -0
- package/dist/src/permissions/PermissionsContext.d.ts +6 -0
- package/dist/src/rtc/BasePeerConnection.d.ts +90 -0
- package/dist/src/rtc/Dispatcher.d.ts +0 -1
- package/dist/src/rtc/IceTrickleBuffer.d.ts +3 -2
- package/dist/src/rtc/Publisher.d.ts +32 -86
- package/dist/src/rtc/Subscriber.d.ts +4 -56
- package/dist/src/rtc/TransceiverCache.d.ts +55 -0
- package/dist/src/rtc/codecs.d.ts +1 -15
- package/dist/src/rtc/helpers/sdp.d.ts +8 -0
- package/dist/src/rtc/helpers/tracks.d.ts +1 -0
- package/dist/src/rtc/index.d.ts +3 -0
- package/dist/src/rtc/videoLayers.d.ts +11 -25
- package/dist/src/stats/{stateStoreStatsReporter.d.ts → CallStateStatsReporter.d.ts} +5 -1
- package/dist/src/stats/SfuStatsReporter.d.ts +4 -2
- package/dist/src/stats/index.d.ts +1 -1
- package/dist/src/stats/types.d.ts +8 -0
- package/dist/src/store/CallState.d.ts +47 -5
- package/dist/src/store/rxUtils.d.ts +15 -1
- package/dist/src/types.d.ts +26 -22
- package/package.json +1 -1
- package/src/Call.ts +310 -271
- package/src/StreamSfuClient.ts +9 -14
- package/src/StreamVideoClient.ts +1 -1
- package/src/__tests__/Call.publishing.test.ts +306 -0
- package/src/devices/CameraManager.ts +33 -16
- package/src/devices/InputMediaDeviceManager.ts +36 -27
- package/src/devices/MicrophoneManager.ts +29 -8
- package/src/devices/ScreenShareManager.ts +6 -8
- package/src/devices/__tests__/CameraManager.test.ts +111 -14
- package/src/devices/__tests__/InputMediaDeviceManager.test.ts +4 -4
- package/src/devices/__tests__/MicrophoneManager.test.ts +59 -21
- package/src/devices/__tests__/ScreenShareManager.test.ts +5 -5
- package/src/devices/__tests__/mocks.ts +1 -0
- package/src/events/__tests__/internal.test.ts +132 -0
- package/src/events/__tests__/mutes.test.ts +0 -3
- package/src/events/__tests__/speaker.test.ts +92 -0
- package/src/events/participant.ts +3 -4
- package/src/gen/coordinator/index.ts +902 -514
- package/src/gen/video/sfu/event/events.ts +91 -30
- package/src/gen/video/sfu/models/models.ts +105 -13
- package/src/helpers/array.ts +14 -0
- package/src/permissions/PermissionsContext.ts +22 -0
- package/src/permissions/__tests__/PermissionsContext.test.ts +40 -0
- package/src/rpc/__tests__/createClient.test.ts +38 -0
- package/src/rpc/createClient.ts +11 -5
- package/src/rtc/BasePeerConnection.ts +240 -0
- package/src/rtc/Dispatcher.ts +0 -9
- package/src/rtc/IceTrickleBuffer.ts +24 -4
- package/src/rtc/Publisher.ts +210 -528
- package/src/rtc/Subscriber.ts +26 -200
- package/src/rtc/TransceiverCache.ts +120 -0
- package/src/rtc/__tests__/Publisher.test.ts +407 -210
- package/src/rtc/__tests__/Subscriber.test.ts +88 -36
- package/src/rtc/__tests__/mocks/webrtc.mocks.ts +22 -2
- package/src/rtc/__tests__/videoLayers.test.ts +161 -54
- package/src/rtc/codecs.ts +1 -131
- package/src/rtc/helpers/__tests__/rtcConfiguration.test.ts +34 -0
- package/src/rtc/helpers/__tests__/sdp.test.ts +59 -0
- package/src/rtc/helpers/sdp.ts +30 -0
- package/src/rtc/helpers/tracks.ts +3 -0
- package/src/rtc/index.ts +4 -0
- package/src/rtc/videoLayers.ts +68 -76
- package/src/stats/{stateStoreStatsReporter.ts → CallStateStatsReporter.ts} +58 -27
- package/src/stats/SfuStatsReporter.ts +31 -3
- package/src/stats/index.ts +1 -1
- package/src/stats/types.ts +12 -0
- package/src/store/CallState.ts +115 -5
- package/src/store/__tests__/CallState.test.ts +101 -0
- package/src/store/rxUtils.ts +23 -1
- package/src/types.ts +27 -22
- package/dist/src/helpers/sdp-munging.d.ts +0 -24
- package/dist/src/rtc/bitrateLookup.d.ts +0 -2
- package/dist/src/rtc/helpers/iceCandidate.d.ts +0 -2
- package/src/helpers/__tests__/hq-audio-sdp.ts +0 -332
- package/src/helpers/__tests__/sdp-munging.test.ts +0 -283
- package/src/helpers/sdp-munging.ts +0 -265
- package/src/rtc/__tests__/bitrateLookup.test.ts +0 -12
- package/src/rtc/__tests__/codecs.test.ts +0 -145
- package/src/rtc/bitrateLookup.ts +0 -61
- package/src/rtc/helpers/iceCandidate.ts +0 -16
- /package/dist/src/{compatibility.d.ts → helpers/compatibility.d.ts} +0 -0
- /package/src/{compatibility.ts → helpers/compatibility.ts} +0 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ICEServer } from '../../../gen/coordinator';
|
|
3
|
+
import { toRtcConfiguration } from '../rtcConfiguration';
|
|
4
|
+
|
|
5
|
+
describe('rtcConfiguration', () => {
|
|
6
|
+
it('should map ICEServer configuration to RTCConfiguration', () => {
|
|
7
|
+
const config: ICEServer[] = [
|
|
8
|
+
{
|
|
9
|
+
urls: ['stun:stun.l.google.com:19302'],
|
|
10
|
+
username: 'user',
|
|
11
|
+
password: 'pass',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
urls: ['turn:turn.example.com'],
|
|
15
|
+
username: 'user',
|
|
16
|
+
password: 'pass',
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
expect(toRtcConfiguration(config)).toEqual({
|
|
20
|
+
iceServers: [
|
|
21
|
+
{
|
|
22
|
+
urls: ['stun:stun.l.google.com:19302'],
|
|
23
|
+
username: 'user',
|
|
24
|
+
credential: 'pass',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
urls: ['turn:turn.example.com'],
|
|
28
|
+
username: 'user',
|
|
29
|
+
credential: 'pass',
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import '../../__tests__/mocks/webrtc.mocks';
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { extractMid } from '../sdp';
|
|
5
|
+
|
|
6
|
+
const sdp = `v=0
|
|
7
|
+
o=- 8380609262679842857 2 IN IP4 127.0.0.1
|
|
8
|
+
s=-
|
|
9
|
+
t=0 0
|
|
10
|
+
a=group:BUNDLE 0 1
|
|
11
|
+
a=extmap-allow-mixed
|
|
12
|
+
a=msid-semantic: WMS
|
|
13
|
+
m=video 9 UDP/TLS/RTP/SAVPF 96
|
|
14
|
+
c=IN IP4 0.0.0.0
|
|
15
|
+
a=rtcp:9 IN IP4 0.0.0.0
|
|
16
|
+
a=mid:100
|
|
17
|
+
a=sendonly
|
|
18
|
+
a=msid:- 8d240fd6-26a1-40f6-a769-4d7d24cfd286
|
|
19
|
+
a=rtcp-mux
|
|
20
|
+
a=rtcp-rsize
|
|
21
|
+
a=rtpmap:96 VP8/90000
|
|
22
|
+
a=rtcp-fb:96 goog-remb
|
|
23
|
+
a=rtcp-fb:96 transport-cc
|
|
24
|
+
a=rtcp-fb:96 ccm fir
|
|
25
|
+
a=rtcp-fb:96 nack
|
|
26
|
+
a=rtcp-fb:96 nack pli
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
describe('sdp', () => {
|
|
30
|
+
it('should extract mid from transceiver', () => {
|
|
31
|
+
const transceiver = new RTCRtpTransceiver();
|
|
32
|
+
// @ts-ignore - mid is a readonly property
|
|
33
|
+
transceiver.mid = '10';
|
|
34
|
+
expect(extractMid(transceiver, -1, '')).toBe('10');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should use transceiverInitIndex (heuristic) when SDP is not present', () => {
|
|
38
|
+
expect(extractMid(new RTCRtpTransceiver(), 5, '')).toBe('5');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should extract mid from SDP', () => {
|
|
42
|
+
const track = new MediaStreamTrack();
|
|
43
|
+
// @ts-ignore - id is a readonly property
|
|
44
|
+
track.id = '8d240fd6-26a1-40f6-a769-4d7d24cfd286';
|
|
45
|
+
const transceiver = new RTCRtpTransceiver();
|
|
46
|
+
vi.spyOn(transceiver.sender, 'track', 'get').mockReturnValue(track);
|
|
47
|
+
|
|
48
|
+
expect(extractMid(transceiver, -1, sdp)).toBe('100');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should fallback to transceiverInitIndex when mid can not be found in SDP', () => {
|
|
52
|
+
const track = new MediaStreamTrack();
|
|
53
|
+
// @ts-ignore - id is a readonly property
|
|
54
|
+
track.id = 'not-known';
|
|
55
|
+
const transceiver = new RTCRtpTransceiver();
|
|
56
|
+
vi.spyOn(transceiver.sender, 'track', 'get').mockReturnValue(track);
|
|
57
|
+
expect(extractMid(transceiver, 3, sdp)).toBe('3');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { parse } from 'sdp-transform';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extracts the mid from the transceiver or the SDP.
|
|
5
|
+
*
|
|
6
|
+
* @param transceiver the transceiver.
|
|
7
|
+
* @param transceiverInitIndex the index of the transceiver in the transceiver's init array.
|
|
8
|
+
* @param sdp the SDP.
|
|
9
|
+
*/
|
|
10
|
+
export const extractMid = (
|
|
11
|
+
transceiver: RTCRtpTransceiver,
|
|
12
|
+
transceiverInitIndex: number,
|
|
13
|
+
sdp: string | undefined,
|
|
14
|
+
): string => {
|
|
15
|
+
if (transceiver.mid) return transceiver.mid;
|
|
16
|
+
if (!sdp) return String(transceiverInitIndex);
|
|
17
|
+
|
|
18
|
+
const track = transceiver.sender.track!;
|
|
19
|
+
const parsedSdp = parse(sdp);
|
|
20
|
+
const media = parsedSdp.media.find((m) => {
|
|
21
|
+
return (
|
|
22
|
+
m.type === track.kind &&
|
|
23
|
+
// if `msid` is not present, we assume that the track is the first one
|
|
24
|
+
(m.msid?.includes(track.id) ?? true)
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
if (typeof media?.mid !== 'undefined') return String(media.mid);
|
|
28
|
+
if (transceiverInitIndex < 0) return '';
|
|
29
|
+
return String(transceiverInitIndex);
|
|
30
|
+
};
|
package/src/rtc/index.ts
CHANGED
package/src/rtc/videoLayers.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { PreferredCodec, PublishOptions } from '../types';
|
|
2
|
-
import { TargetResolutionResponse } from '../gen/shims';
|
|
3
1
|
import { isSvcCodec } from './codecs';
|
|
4
|
-
import {
|
|
5
|
-
|
|
2
|
+
import {
|
|
3
|
+
PublishOption,
|
|
4
|
+
VideoDimension,
|
|
5
|
+
VideoLayer,
|
|
6
|
+
VideoQuality,
|
|
7
|
+
} from '../gen/video/sfu/models/models';
|
|
8
|
+
import { isAudioTrackType } from './helpers/tracks';
|
|
6
9
|
|
|
7
10
|
export type OptimalVideoLayer = RTCRtpEncodingParameters & {
|
|
8
11
|
width: number;
|
|
@@ -11,17 +14,10 @@ export type OptimalVideoLayer = RTCRtpEncodingParameters & {
|
|
|
11
14
|
scalabilityMode?: string;
|
|
12
15
|
};
|
|
13
16
|
|
|
14
|
-
const DEFAULT_BITRATE = 1250000;
|
|
15
|
-
const defaultTargetResolution: TargetResolutionResponse = {
|
|
16
|
-
bitrate: DEFAULT_BITRATE,
|
|
17
|
-
width: 1280,
|
|
18
|
-
height: 720,
|
|
19
|
-
};
|
|
20
|
-
|
|
21
17
|
const defaultBitratePerRid: Record<string, number> = {
|
|
22
18
|
q: 300000,
|
|
23
19
|
h: 750000,
|
|
24
|
-
f:
|
|
20
|
+
f: 1250000,
|
|
25
21
|
};
|
|
26
22
|
|
|
27
23
|
/**
|
|
@@ -31,9 +27,17 @@ const defaultBitratePerRid: Record<string, number> = {
|
|
|
31
27
|
*
|
|
32
28
|
* @param layers the layers to process.
|
|
33
29
|
*/
|
|
34
|
-
export const toSvcEncodings = (
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
export const toSvcEncodings = (
|
|
31
|
+
layers: OptimalVideoLayer[] | undefined,
|
|
32
|
+
): RTCRtpEncodingParameters[] | undefined => {
|
|
33
|
+
if (!layers) return;
|
|
34
|
+
// we take the highest quality layer, and we assign it to `q` encoder.
|
|
35
|
+
const withRid = (rid: string) => (l: OptimalVideoLayer) => l.rid === rid;
|
|
36
|
+
const highestLayer =
|
|
37
|
+
layers.find(withRid('f')) ||
|
|
38
|
+
layers.find(withRid('h')) ||
|
|
39
|
+
layers.find(withRid('q'));
|
|
40
|
+
return [{ ...highestLayer, rid: 'q' }];
|
|
37
41
|
};
|
|
38
42
|
|
|
39
43
|
/**
|
|
@@ -48,60 +52,80 @@ export const ridToVideoQuality = (rid: string): VideoQuality => {
|
|
|
48
52
|
};
|
|
49
53
|
|
|
50
54
|
/**
|
|
51
|
-
*
|
|
52
|
-
|
|
55
|
+
* Converts the given video layers to SFU video layers.
|
|
56
|
+
*/
|
|
57
|
+
export const toVideoLayers = (
|
|
58
|
+
layers: OptimalVideoLayer[] | undefined = [],
|
|
59
|
+
): VideoLayer[] => {
|
|
60
|
+
return layers.map<VideoLayer>((layer) => ({
|
|
61
|
+
rid: layer.rid || '',
|
|
62
|
+
bitrate: layer.maxBitrate || 0,
|
|
63
|
+
fps: layer.maxFramerate || 0,
|
|
64
|
+
quality: ridToVideoQuality(layer.rid || ''),
|
|
65
|
+
videoDimension: { width: layer.width, height: layer.height },
|
|
66
|
+
}));
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Converts the spatial and temporal layers to a scalability mode.
|
|
71
|
+
*/
|
|
72
|
+
const toScalabilityMode = (spatialLayers: number, temporalLayers: number) =>
|
|
73
|
+
`L${spatialLayers}T${temporalLayers}${spatialLayers > 1 ? '_KEY' : ''}`;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Determines the most optimal video layers for the given track.
|
|
53
77
|
*
|
|
54
78
|
* @param videoTrack the video track to find optimal layers for.
|
|
55
|
-
* @param
|
|
56
|
-
* @param codecInUse the codec in use.
|
|
57
|
-
* @param publishOptions the publish options for the track.
|
|
79
|
+
* @param publishOption the publish options for the track.
|
|
58
80
|
*/
|
|
59
|
-
export const
|
|
81
|
+
export const computeVideoLayers = (
|
|
60
82
|
videoTrack: MediaStreamTrack,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
) => {
|
|
83
|
+
publishOption: PublishOption,
|
|
84
|
+
): OptimalVideoLayer[] | undefined => {
|
|
85
|
+
if (isAudioTrackType(publishOption.trackType)) return;
|
|
65
86
|
const optimalVideoLayers: OptimalVideoLayer[] = [];
|
|
66
87
|
const settings = videoTrack.getSettings();
|
|
67
88
|
const { width = 0, height = 0 } = settings;
|
|
68
89
|
const {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
90
|
+
bitrate,
|
|
91
|
+
codec,
|
|
92
|
+
fps,
|
|
93
|
+
maxSpatialLayers = 3,
|
|
94
|
+
maxTemporalLayers = 3,
|
|
95
|
+
videoDimension = { width: 1280, height: 720 },
|
|
96
|
+
} = publishOption;
|
|
73
97
|
const maxBitrate = getComputedMaxBitrate(
|
|
74
|
-
|
|
98
|
+
videoDimension,
|
|
75
99
|
width,
|
|
76
100
|
height,
|
|
77
|
-
|
|
78
|
-
publishOptions,
|
|
101
|
+
bitrate,
|
|
79
102
|
);
|
|
80
103
|
let downscaleFactor = 1;
|
|
81
104
|
let bitrateFactor = 1;
|
|
82
|
-
const svcCodec = isSvcCodec(
|
|
83
|
-
const
|
|
84
|
-
for (const rid of ['f', 'h', 'q'].slice(0, totalLayers)) {
|
|
105
|
+
const svcCodec = isSvcCodec(codec?.name);
|
|
106
|
+
for (const rid of ['f', 'h', 'q'].slice(0, maxSpatialLayers)) {
|
|
85
107
|
const layer: OptimalVideoLayer = {
|
|
86
108
|
active: true,
|
|
87
109
|
rid,
|
|
88
110
|
width: Math.round(width / downscaleFactor),
|
|
89
111
|
height: Math.round(height / downscaleFactor),
|
|
90
|
-
maxBitrate:
|
|
91
|
-
|
|
92
|
-
maxFramerate: 30,
|
|
112
|
+
maxBitrate: maxBitrate / bitrateFactor || defaultBitratePerRid[rid],
|
|
113
|
+
maxFramerate: fps,
|
|
93
114
|
};
|
|
94
115
|
if (svcCodec) {
|
|
95
116
|
// for SVC codecs, we need to set the scalability mode, and the
|
|
96
117
|
// codec will handle the rest (layers, temporal layers, etc.)
|
|
97
|
-
layer.scalabilityMode =
|
|
118
|
+
layer.scalabilityMode = toScalabilityMode(
|
|
119
|
+
maxSpatialLayers,
|
|
120
|
+
maxTemporalLayers,
|
|
121
|
+
);
|
|
98
122
|
} else {
|
|
99
123
|
// for non-SVC codecs, we need to downscale proportionally (simulcast)
|
|
100
124
|
layer.scaleResolutionDownBy = downscaleFactor;
|
|
101
125
|
}
|
|
102
126
|
|
|
103
127
|
downscaleFactor *= 2;
|
|
104
|
-
bitrateFactor *=
|
|
128
|
+
bitrateFactor *= 2;
|
|
105
129
|
|
|
106
130
|
// Reversing the order [f, h, q] to [q, h, f] as Chrome uses encoding index
|
|
107
131
|
// when deciding which layer to disable when CPU or bandwidth is constrained.
|
|
@@ -124,29 +148,17 @@ export const findOptimalVideoLayers = (
|
|
|
124
148
|
* @param targetResolution the target resolution.
|
|
125
149
|
* @param currentWidth the current width of the track.
|
|
126
150
|
* @param currentHeight the current height of the track.
|
|
127
|
-
* @param
|
|
128
|
-
* @param publishOptions the publish options.
|
|
151
|
+
* @param bitrate the target bitrate.
|
|
129
152
|
*/
|
|
130
153
|
export const getComputedMaxBitrate = (
|
|
131
|
-
targetResolution:
|
|
154
|
+
targetResolution: VideoDimension,
|
|
132
155
|
currentWidth: number,
|
|
133
156
|
currentHeight: number,
|
|
134
|
-
|
|
135
|
-
publishOptions?: PublishOptions,
|
|
157
|
+
bitrate: number,
|
|
136
158
|
): number => {
|
|
137
159
|
// if the current resolution is lower than the target resolution,
|
|
138
160
|
// we want to proportionally reduce the target bitrate
|
|
139
|
-
const {
|
|
140
|
-
width: targetWidth,
|
|
141
|
-
height: targetHeight,
|
|
142
|
-
bitrate: targetBitrate,
|
|
143
|
-
} = targetResolution;
|
|
144
|
-
const { preferredBitrate } = publishOptions || {};
|
|
145
|
-
const frameHeight =
|
|
146
|
-
currentWidth > currentHeight ? currentHeight : currentWidth;
|
|
147
|
-
const bitrate =
|
|
148
|
-
preferredBitrate ||
|
|
149
|
-
(codecInUse ? getOptimalBitrate(codecInUse, frameHeight) : targetBitrate);
|
|
161
|
+
const { width: targetWidth, height: targetHeight } = targetResolution;
|
|
150
162
|
if (currentWidth < targetWidth || currentHeight < targetHeight) {
|
|
151
163
|
const currentPixels = currentWidth * currentHeight;
|
|
152
164
|
const targetPixels = targetWidth * targetHeight;
|
|
@@ -188,23 +200,3 @@ const withSimulcastConstraints = (
|
|
|
188
200
|
rid: ridMapping[index], // reassign rid
|
|
189
201
|
}));
|
|
190
202
|
};
|
|
191
|
-
|
|
192
|
-
export const findOptimalScreenSharingLayers = (
|
|
193
|
-
videoTrack: MediaStreamTrack,
|
|
194
|
-
publishOptions?: PublishOptions,
|
|
195
|
-
defaultMaxBitrate = 3000000,
|
|
196
|
-
): OptimalVideoLayer[] => {
|
|
197
|
-
const { screenShareSettings: preferences } = publishOptions || {};
|
|
198
|
-
const settings = videoTrack.getSettings();
|
|
199
|
-
return [
|
|
200
|
-
{
|
|
201
|
-
active: true,
|
|
202
|
-
rid: 'q', // single track, start from 'q'
|
|
203
|
-
width: settings.width || 0,
|
|
204
|
-
height: settings.height || 0,
|
|
205
|
-
scaleResolutionDownBy: 1,
|
|
206
|
-
maxBitrate: preferences?.maxBitrate ?? defaultMaxBitrate,
|
|
207
|
-
maxFramerate: preferences?.maxFramerate ?? 30,
|
|
208
|
-
},
|
|
209
|
-
];
|
|
210
|
-
};
|
|
@@ -2,12 +2,15 @@ import type {
|
|
|
2
2
|
AggregatedStatsReport,
|
|
3
3
|
BaseStats,
|
|
4
4
|
ParticipantsStatsReport,
|
|
5
|
+
RTCMediaSourceStats,
|
|
5
6
|
StatsReport,
|
|
6
7
|
} from './types';
|
|
7
8
|
import { CallState } from '../store';
|
|
8
9
|
import { Publisher, Subscriber } from '../rtc';
|
|
9
10
|
import { getLogger } from '../logger';
|
|
10
11
|
import { flatten } from './utils';
|
|
12
|
+
import { TrackType } from '../gen/video/sfu/models/models';
|
|
13
|
+
import { isFirefox } from '../helpers/browsers';
|
|
11
14
|
|
|
12
15
|
export type StatsReporterOpts = {
|
|
13
16
|
subscriber: Subscriber;
|
|
@@ -41,7 +44,7 @@ export type StatsReporter = {
|
|
|
41
44
|
*/
|
|
42
45
|
getStatsForStream: (
|
|
43
46
|
kind: 'subscriber' | 'publisher',
|
|
44
|
-
|
|
47
|
+
tracks: MediaStreamTrack[],
|
|
45
48
|
) => Promise<StatsReport[]>;
|
|
46
49
|
|
|
47
50
|
/**
|
|
@@ -87,12 +90,12 @@ export const createStatsReporter = ({
|
|
|
87
90
|
|
|
88
91
|
const getStatsForStream = async (
|
|
89
92
|
kind: 'subscriber' | 'publisher',
|
|
90
|
-
|
|
93
|
+
tracks: MediaStreamTrack[],
|
|
91
94
|
) => {
|
|
92
95
|
const pc = kind === 'subscriber' ? subscriber : publisher;
|
|
93
96
|
if (!pc) return [];
|
|
94
97
|
const statsForStream: StatsReport[] = [];
|
|
95
|
-
for (
|
|
98
|
+
for (const track of tracks) {
|
|
96
99
|
const report = await pc.getStats(track);
|
|
97
100
|
const stats = transform(report, {
|
|
98
101
|
// @ts-ignore
|
|
@@ -121,31 +124,28 @@ export const createStatsReporter = ({
|
|
|
121
124
|
*/
|
|
122
125
|
const run = async () => {
|
|
123
126
|
const participantStats: ParticipantsStatsReport = {};
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
for (
|
|
127
|
+
if (sessionIdsToTrack.size > 0) {
|
|
128
|
+
const sessionIds = new Set(sessionIdsToTrack);
|
|
129
|
+
for (const participant of state.participants) {
|
|
127
130
|
if (!sessionIds.has(participant.sessionId)) continue;
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
+
const {
|
|
132
|
+
audioStream,
|
|
133
|
+
isLocalParticipant,
|
|
134
|
+
sessionId,
|
|
135
|
+
userId,
|
|
136
|
+
videoStream,
|
|
137
|
+
} = participant;
|
|
138
|
+
const kind = isLocalParticipant ? 'publisher' : 'subscriber';
|
|
131
139
|
try {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
);
|
|
140
|
-
mergedStream.getTracks().forEach((t) => {
|
|
141
|
-
mergedStream.removeTrack(t);
|
|
142
|
-
});
|
|
140
|
+
const tracks = isLocalParticipant
|
|
141
|
+
? publisher?.getPublishedTracks() || []
|
|
142
|
+
: [
|
|
143
|
+
...(videoStream?.getVideoTracks() || []),
|
|
144
|
+
...(audioStream?.getAudioTracks() || []),
|
|
145
|
+
];
|
|
146
|
+
participantStats[sessionId] = await getStatsForStream(kind, tracks);
|
|
143
147
|
} catch (e) {
|
|
144
|
-
logger(
|
|
145
|
-
'error',
|
|
146
|
-
`Failed to collect stats for ${kind} of ${participant.userId}`,
|
|
147
|
-
e,
|
|
148
|
-
);
|
|
148
|
+
logger('warn', `Failed to collect ${kind} stats for ${userId}`, e);
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
}
|
|
@@ -157,6 +157,7 @@ export const createStatsReporter = ({
|
|
|
157
157
|
transform(report, {
|
|
158
158
|
kind: 'subscriber',
|
|
159
159
|
trackKind: 'video',
|
|
160
|
+
publisher,
|
|
160
161
|
}),
|
|
161
162
|
)
|
|
162
163
|
.then(aggregate),
|
|
@@ -167,6 +168,7 @@ export const createStatsReporter = ({
|
|
|
167
168
|
transform(report, {
|
|
168
169
|
kind: 'publisher',
|
|
169
170
|
trackKind: 'video',
|
|
171
|
+
publisher,
|
|
170
172
|
}),
|
|
171
173
|
)
|
|
172
174
|
.then(aggregate)
|
|
@@ -220,11 +222,14 @@ export type StatsTransformOpts = {
|
|
|
220
222
|
* The kind of track we are transforming stats for.
|
|
221
223
|
*/
|
|
222
224
|
trackKind: 'audio' | 'video';
|
|
223
|
-
|
|
224
225
|
/**
|
|
225
226
|
* The kind of peer connection we are transforming stats for.
|
|
226
227
|
*/
|
|
227
228
|
kind: 'subscriber' | 'publisher';
|
|
229
|
+
/**
|
|
230
|
+
* The publisher instance.
|
|
231
|
+
*/
|
|
232
|
+
publisher: Publisher | undefined;
|
|
228
233
|
};
|
|
229
234
|
|
|
230
235
|
/**
|
|
@@ -237,7 +242,7 @@ const transform = (
|
|
|
237
242
|
report: RTCStatsReport,
|
|
238
243
|
opts: StatsTransformOpts,
|
|
239
244
|
): StatsReport => {
|
|
240
|
-
const { trackKind, kind } = opts;
|
|
245
|
+
const { trackKind, kind, publisher } = opts;
|
|
241
246
|
const direction = kind === 'subscriber' ? 'inbound-rtp' : 'outbound-rtp';
|
|
242
247
|
const stats = flatten(report);
|
|
243
248
|
const streams = stats
|
|
@@ -268,6 +273,20 @@ const transform = (
|
|
|
268
273
|
roundTripTime = candidatePair?.currentRoundTripTime;
|
|
269
274
|
}
|
|
270
275
|
|
|
276
|
+
let trackType: TrackType | undefined;
|
|
277
|
+
if (kind === 'publisher' && publisher) {
|
|
278
|
+
const firefox = isFirefox();
|
|
279
|
+
const mediaSource = stats.find(
|
|
280
|
+
(s) =>
|
|
281
|
+
s.type === 'media-source' &&
|
|
282
|
+
// Firefox doesn't have mediaSourceId, so we need to guess the media source
|
|
283
|
+
(firefox ? true : s.id === rtcStreamStats.mediaSourceId),
|
|
284
|
+
) as RTCMediaSourceStats | undefined;
|
|
285
|
+
if (mediaSource) {
|
|
286
|
+
trackType = publisher.getTrackType(mediaSource.trackIdentifier);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
271
290
|
return {
|
|
272
291
|
bytesSent: rtcStreamStats.bytesSent,
|
|
273
292
|
bytesReceived: rtcStreamStats.bytesReceived,
|
|
@@ -278,10 +297,12 @@ const transform = (
|
|
|
278
297
|
framesPerSecond: rtcStreamStats.framesPerSecond,
|
|
279
298
|
jitter: rtcStreamStats.jitter,
|
|
280
299
|
kind: rtcStreamStats.kind,
|
|
300
|
+
mediaSourceId: rtcStreamStats.mediaSourceId,
|
|
281
301
|
// @ts-ignore: available in Chrome only, TS doesn't recognize this
|
|
282
302
|
qualityLimitationReason: rtcStreamStats.qualityLimitationReason,
|
|
283
303
|
rid: rtcStreamStats.rid,
|
|
284
304
|
ssrc: rtcStreamStats.ssrc,
|
|
305
|
+
trackType,
|
|
285
306
|
};
|
|
286
307
|
});
|
|
287
308
|
|
|
@@ -304,6 +325,7 @@ const getEmptyStats = (stats?: StatsReport): AggregatedStatsReport => {
|
|
|
304
325
|
highestFrameHeight: 0,
|
|
305
326
|
highestFramesPerSecond: 0,
|
|
306
327
|
codec: '',
|
|
328
|
+
codecPerTrackType: {},
|
|
307
329
|
timestamp: Date.now(),
|
|
308
330
|
};
|
|
309
331
|
};
|
|
@@ -349,6 +371,15 @@ const aggregate = (stats: StatsReport): AggregatedStatsReport => {
|
|
|
349
371
|
);
|
|
350
372
|
// we take the first codec we find, as it should be the same for all streams
|
|
351
373
|
report.codec = streams[0].codec || '';
|
|
374
|
+
report.codecPerTrackType = streams.reduce(
|
|
375
|
+
(acc, stream) => {
|
|
376
|
+
if (stream.trackType) {
|
|
377
|
+
acc[stream.trackType] = stream.codec || '';
|
|
378
|
+
}
|
|
379
|
+
return acc;
|
|
380
|
+
},
|
|
381
|
+
{} as Record<TrackType, string>,
|
|
382
|
+
);
|
|
352
383
|
}
|
|
353
384
|
|
|
354
385
|
const qualityLimitationReason = [
|
|
@@ -9,7 +9,10 @@ import {
|
|
|
9
9
|
getWebRTCInfo,
|
|
10
10
|
LocalClientDetailsType,
|
|
11
11
|
} from '../client-details';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
InputDevices,
|
|
14
|
+
WebsocketReconnectStrategy,
|
|
15
|
+
} from '../gen/video/sfu/models/models';
|
|
13
16
|
import { CameraManager, MicrophoneManager } from '../devices';
|
|
14
17
|
import { createSubscription } from '../store/rxUtils';
|
|
15
18
|
import { CallState } from '../store';
|
|
@@ -118,8 +121,33 @@ export class SfuStatsReporter {
|
|
|
118
121
|
);
|
|
119
122
|
};
|
|
120
123
|
|
|
121
|
-
|
|
122
|
-
|
|
124
|
+
sendConnectionTime = (connectionTimeSeconds: number) => {
|
|
125
|
+
this.sendTelemetryData({
|
|
126
|
+
data: {
|
|
127
|
+
oneofKind: 'connectionTimeSeconds',
|
|
128
|
+
connectionTimeSeconds,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
sendReconnectionTime = (
|
|
134
|
+
strategy: WebsocketReconnectStrategy,
|
|
135
|
+
timeSeconds: number,
|
|
136
|
+
) => {
|
|
137
|
+
this.sendTelemetryData({
|
|
138
|
+
data: {
|
|
139
|
+
oneofKind: 'reconnection',
|
|
140
|
+
reconnection: { strategy, timeSeconds },
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
private sendTelemetryData = (telemetryData: Telemetry) => {
|
|
146
|
+
// intentionally not awaiting the promise here
|
|
147
|
+
// to avoid impeding with the ongoing actions.
|
|
148
|
+
this.run(telemetryData).catch((err) => {
|
|
149
|
+
this.logger('warn', 'Failed to send telemetry data', err);
|
|
150
|
+
});
|
|
123
151
|
};
|
|
124
152
|
|
|
125
153
|
private run = async (telemetryData?: Telemetry) => {
|
package/src/stats/index.ts
CHANGED
package/src/stats/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { TrackType } from '../gen/video/sfu/models/models';
|
|
2
|
+
|
|
1
3
|
export type BaseStats = {
|
|
2
4
|
audioLevel?: number;
|
|
3
5
|
bytesSent?: number;
|
|
@@ -9,9 +11,11 @@ export type BaseStats = {
|
|
|
9
11
|
framesPerSecond?: number;
|
|
10
12
|
jitter?: number;
|
|
11
13
|
kind?: string;
|
|
14
|
+
mediaSourceId?: string;
|
|
12
15
|
qualityLimitationReason?: string;
|
|
13
16
|
rid?: string;
|
|
14
17
|
ssrc?: number;
|
|
18
|
+
trackType?: TrackType;
|
|
15
19
|
};
|
|
16
20
|
|
|
17
21
|
export type StatsReport = {
|
|
@@ -30,6 +34,7 @@ export type AggregatedStatsReport = {
|
|
|
30
34
|
highestFrameHeight: number;
|
|
31
35
|
highestFramesPerSecond: number;
|
|
32
36
|
codec: string;
|
|
37
|
+
codecPerTrackType: Partial<Record<TrackType, string>>;
|
|
33
38
|
timestamp: number;
|
|
34
39
|
rawReport: StatsReport;
|
|
35
40
|
};
|
|
@@ -48,3 +53,10 @@ export type CallStatsReport = {
|
|
|
48
53
|
participants: ParticipantsStatsReport;
|
|
49
54
|
timestamp: number;
|
|
50
55
|
};
|
|
56
|
+
|
|
57
|
+
// shim for RTCMediaSourceStats, not yet available in the standard types
|
|
58
|
+
// https://www.w3.org/TR/webrtc-stats/#mediasourcestats-dict*
|
|
59
|
+
export interface RTCMediaSourceStats {
|
|
60
|
+
kind: string;
|
|
61
|
+
trackIdentifier: string;
|
|
62
|
+
}
|