@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.
Files changed (99) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/index.browser.es.js +1704 -1762
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +1706 -1780
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +1704 -1762
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +61 -30
  9. package/dist/src/StreamSfuClient.d.ts +4 -5
  10. package/dist/src/devices/CameraManager.d.ts +5 -8
  11. package/dist/src/devices/InputMediaDeviceManager.d.ts +5 -5
  12. package/dist/src/devices/MicrophoneManager.d.ts +7 -2
  13. package/dist/src/devices/ScreenShareManager.d.ts +1 -2
  14. package/dist/src/gen/coordinator/index.d.ts +904 -515
  15. package/dist/src/gen/video/sfu/event/events.d.ts +38 -19
  16. package/dist/src/gen/video/sfu/models/models.d.ts +76 -9
  17. package/dist/src/helpers/array.d.ts +7 -0
  18. package/dist/src/permissions/PermissionsContext.d.ts +6 -0
  19. package/dist/src/rtc/BasePeerConnection.d.ts +90 -0
  20. package/dist/src/rtc/Dispatcher.d.ts +0 -1
  21. package/dist/src/rtc/IceTrickleBuffer.d.ts +3 -2
  22. package/dist/src/rtc/Publisher.d.ts +32 -86
  23. package/dist/src/rtc/Subscriber.d.ts +4 -56
  24. package/dist/src/rtc/TransceiverCache.d.ts +55 -0
  25. package/dist/src/rtc/codecs.d.ts +1 -15
  26. package/dist/src/rtc/helpers/sdp.d.ts +8 -0
  27. package/dist/src/rtc/helpers/tracks.d.ts +1 -0
  28. package/dist/src/rtc/index.d.ts +3 -0
  29. package/dist/src/rtc/videoLayers.d.ts +11 -25
  30. package/dist/src/stats/{stateStoreStatsReporter.d.ts → CallStateStatsReporter.d.ts} +5 -1
  31. package/dist/src/stats/SfuStatsReporter.d.ts +4 -2
  32. package/dist/src/stats/index.d.ts +1 -1
  33. package/dist/src/stats/types.d.ts +8 -0
  34. package/dist/src/store/CallState.d.ts +47 -5
  35. package/dist/src/store/rxUtils.d.ts +15 -1
  36. package/dist/src/types.d.ts +26 -22
  37. package/package.json +1 -1
  38. package/src/Call.ts +310 -271
  39. package/src/StreamSfuClient.ts +9 -14
  40. package/src/StreamVideoClient.ts +1 -1
  41. package/src/__tests__/Call.publishing.test.ts +306 -0
  42. package/src/devices/CameraManager.ts +33 -16
  43. package/src/devices/InputMediaDeviceManager.ts +36 -27
  44. package/src/devices/MicrophoneManager.ts +29 -8
  45. package/src/devices/ScreenShareManager.ts +6 -8
  46. package/src/devices/__tests__/CameraManager.test.ts +111 -14
  47. package/src/devices/__tests__/InputMediaDeviceManager.test.ts +4 -4
  48. package/src/devices/__tests__/MicrophoneManager.test.ts +59 -21
  49. package/src/devices/__tests__/ScreenShareManager.test.ts +5 -5
  50. package/src/devices/__tests__/mocks.ts +1 -0
  51. package/src/events/__tests__/internal.test.ts +132 -0
  52. package/src/events/__tests__/mutes.test.ts +0 -3
  53. package/src/events/__tests__/speaker.test.ts +92 -0
  54. package/src/events/participant.ts +3 -4
  55. package/src/gen/coordinator/index.ts +902 -514
  56. package/src/gen/video/sfu/event/events.ts +91 -30
  57. package/src/gen/video/sfu/models/models.ts +105 -13
  58. package/src/helpers/array.ts +14 -0
  59. package/src/permissions/PermissionsContext.ts +22 -0
  60. package/src/permissions/__tests__/PermissionsContext.test.ts +40 -0
  61. package/src/rpc/__tests__/createClient.test.ts +38 -0
  62. package/src/rpc/createClient.ts +11 -5
  63. package/src/rtc/BasePeerConnection.ts +240 -0
  64. package/src/rtc/Dispatcher.ts +0 -9
  65. package/src/rtc/IceTrickleBuffer.ts +24 -4
  66. package/src/rtc/Publisher.ts +210 -528
  67. package/src/rtc/Subscriber.ts +26 -200
  68. package/src/rtc/TransceiverCache.ts +120 -0
  69. package/src/rtc/__tests__/Publisher.test.ts +407 -210
  70. package/src/rtc/__tests__/Subscriber.test.ts +88 -36
  71. package/src/rtc/__tests__/mocks/webrtc.mocks.ts +22 -2
  72. package/src/rtc/__tests__/videoLayers.test.ts +161 -54
  73. package/src/rtc/codecs.ts +1 -131
  74. package/src/rtc/helpers/__tests__/rtcConfiguration.test.ts +34 -0
  75. package/src/rtc/helpers/__tests__/sdp.test.ts +59 -0
  76. package/src/rtc/helpers/sdp.ts +30 -0
  77. package/src/rtc/helpers/tracks.ts +3 -0
  78. package/src/rtc/index.ts +4 -0
  79. package/src/rtc/videoLayers.ts +68 -76
  80. package/src/stats/{stateStoreStatsReporter.ts → CallStateStatsReporter.ts} +58 -27
  81. package/src/stats/SfuStatsReporter.ts +31 -3
  82. package/src/stats/index.ts +1 -1
  83. package/src/stats/types.ts +12 -0
  84. package/src/store/CallState.ts +115 -5
  85. package/src/store/__tests__/CallState.test.ts +101 -0
  86. package/src/store/rxUtils.ts +23 -1
  87. package/src/types.ts +27 -22
  88. package/dist/src/helpers/sdp-munging.d.ts +0 -24
  89. package/dist/src/rtc/bitrateLookup.d.ts +0 -2
  90. package/dist/src/rtc/helpers/iceCandidate.d.ts +0 -2
  91. package/src/helpers/__tests__/hq-audio-sdp.ts +0 -332
  92. package/src/helpers/__tests__/sdp-munging.test.ts +0 -283
  93. package/src/helpers/sdp-munging.ts +0 -265
  94. package/src/rtc/__tests__/bitrateLookup.test.ts +0 -12
  95. package/src/rtc/__tests__/codecs.test.ts +0 -145
  96. package/src/rtc/bitrateLookup.ts +0 -61
  97. package/src/rtc/helpers/iceCandidate.ts +0 -16
  98. /package/dist/src/{compatibility.d.ts → helpers/compatibility.d.ts} +0 -0
  99. /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
+ };
@@ -57,3 +57,6 @@ export const toTrackType = (trackType: string): TrackType | undefined => {
57
57
  return undefined;
58
58
  }
59
59
  };
60
+
61
+ export const isAudioTrackType = (trackType: TrackType): boolean =>
62
+ trackType === TrackType.AUDIO || trackType === TrackType.SCREEN_SHARE_AUDIO;
package/src/rtc/index.ts CHANGED
@@ -5,3 +5,7 @@ export * from './Publisher';
5
5
  export * from './Subscriber';
6
6
  export * from './signal';
7
7
  export * from './videoLayers';
8
+
9
+ export * from './helpers/sdp';
10
+ export * from './helpers/tracks';
11
+ export * from './helpers/rtcConfiguration';
@@ -1,8 +1,11 @@
1
- import { PreferredCodec, PublishOptions } from '../types';
2
- import { TargetResolutionResponse } from '../gen/shims';
3
1
  import { isSvcCodec } from './codecs';
4
- import { getOptimalBitrate } from './bitrateLookup';
5
- import { VideoQuality } from '../gen/video/sfu/models/models';
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: DEFAULT_BITRATE,
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 = (layers: OptimalVideoLayer[] | undefined) => {
35
- // we take the `f` layer, and we rename it to `q`.
36
- return layers?.filter((l) => l.rid === 'f').map((l) => ({ ...l, rid: 'q' }));
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
- * Determines the most optimal video layers for simulcasting
52
- * for the given track.
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 targetResolution the expected target resolution.
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 findOptimalVideoLayers = (
81
+ export const computeVideoLayers = (
60
82
  videoTrack: MediaStreamTrack,
61
- targetResolution: TargetResolutionResponse = defaultTargetResolution,
62
- codecInUse?: PreferredCodec,
63
- publishOptions?: PublishOptions,
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
- scalabilityMode,
70
- bitrateDownscaleFactor = 2,
71
- maxSimulcastLayers = 3,
72
- } = publishOptions || {};
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
- targetResolution,
98
+ videoDimension,
75
99
  width,
76
100
  height,
77
- codecInUse,
78
- publishOptions,
101
+ bitrate,
79
102
  );
80
103
  let downscaleFactor = 1;
81
104
  let bitrateFactor = 1;
82
- const svcCodec = isSvcCodec(codecInUse);
83
- const totalLayers = svcCodec ? 3 : Math.min(3, maxSimulcastLayers);
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
- Math.round(maxBitrate / bitrateFactor) || defaultBitratePerRid[rid],
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 = scalabilityMode || 'L3T2_KEY';
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 *= bitrateDownscaleFactor;
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 codecInUse the codec in use.
128
- * @param publishOptions the publish options.
151
+ * @param bitrate the target bitrate.
129
152
  */
130
153
  export const getComputedMaxBitrate = (
131
- targetResolution: TargetResolutionResponse,
154
+ targetResolution: VideoDimension,
132
155
  currentWidth: number,
133
156
  currentHeight: number,
134
- codecInUse?: PreferredCodec,
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
- mediaStream: MediaStream,
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
- mediaStream: MediaStream,
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 (let track of mediaStream.getTracks()) {
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
- const sessionIds = new Set(sessionIdsToTrack);
125
- if (sessionIds.size > 0) {
126
- for (let participant of state.participants) {
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 kind = participant.isLocalParticipant
129
- ? 'publisher'
130
- : 'subscriber';
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 mergedStream = new MediaStream([
133
- ...(participant.videoStream?.getVideoTracks() || []),
134
- ...(participant.audioStream?.getAudioTracks() || []),
135
- ]);
136
- participantStats[participant.sessionId] = await getStatsForStream(
137
- kind,
138
- mergedStream,
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 { InputDevices } from '../gen/video/sfu/models/models';
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
- sendTelemetryData = async (telemetryData: Telemetry) => {
122
- return this.run(telemetryData);
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) => {
@@ -1,3 +1,3 @@
1
- export * from './stateStoreStatsReporter';
1
+ export * from './CallStateStatsReporter';
2
2
  export * from './SfuStatsReporter';
3
3
  export * from './types';
@@ -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
+ }