@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
@@ -1,22 +1,10 @@
1
- import { StreamSfuClient } from '../StreamSfuClient';
2
- import { getIceCandidate } from './helpers/iceCandidate';
1
+ import {
2
+ BasePeerConnection,
3
+ BasePeerConnectionOpts,
4
+ } from './BasePeerConnection';
3
5
  import { PeerType } from '../gen/video/sfu/models/models';
4
6
  import { SubscriberOffer } from '../gen/video/sfu/event/events';
5
- import { Dispatcher } from './Dispatcher';
6
- import { getLogger } from '../logger';
7
- import { CallingState, CallState } from '../store';
8
- import { withoutConcurrency } from '../helpers/concurrency';
9
7
  import { toTrackType, trackTypeToParticipantStreamKey } from './helpers/tracks';
10
- import { Logger } from '../coordinator/connection/types';
11
-
12
- export type SubscriberOpts = {
13
- sfuClient: StreamSfuClient;
14
- dispatcher: Dispatcher;
15
- state: CallState;
16
- connectionConfig?: RTCConfiguration;
17
- onUnrecoverableError?: () => void;
18
- logTag: string;
19
- };
20
8
 
21
9
  /**
22
10
  * A wrapper around the `RTCPeerConnection` that handles the incoming
@@ -24,138 +12,30 @@ export type SubscriberOpts = {
24
12
  *
25
13
  * @internal
26
14
  */
27
- export class Subscriber {
28
- private readonly logger: Logger;
29
- private pc: RTCPeerConnection;
30
- private sfuClient: StreamSfuClient;
31
- private state: CallState;
32
-
33
- private readonly unregisterOnSubscriberOffer: () => void;
34
- private readonly unregisterOnIceRestart: () => void;
35
- private readonly onUnrecoverableError?: () => void;
36
-
37
- private isIceRestarting = false;
38
-
15
+ export class Subscriber extends BasePeerConnection {
39
16
  /**
40
17
  * Constructs a new `Subscriber` instance.
41
- *
42
- * @param sfuClient the SFU client to use.
43
- * @param dispatcher the dispatcher to use.
44
- * @param state the state of the call.
45
- * @param connectionConfig the connection configuration to use.
46
- * @param iceRestartDelay the delay in milliseconds to wait before restarting ICE when connection goes to `disconnected` state.
47
- * @param onUnrecoverableError a callback to call when an unrecoverable error occurs.
48
- * @param logTag a tag to use for logging.
49
18
  */
50
- constructor({
51
- sfuClient,
52
- dispatcher,
53
- state,
54
- connectionConfig,
55
- onUnrecoverableError,
56
- logTag,
57
- }: SubscriberOpts) {
58
- this.logger = getLogger(['Subscriber', logTag]);
59
- this.sfuClient = sfuClient;
60
- this.state = state;
61
- this.onUnrecoverableError = onUnrecoverableError;
19
+ constructor(opts: BasePeerConnectionOpts) {
20
+ super(PeerType.SUBSCRIBER, opts);
21
+ this.pc.addEventListener('track', this.handleOnTrack);
62
22
 
63
- this.pc = this.createPeerConnection(connectionConfig);
64
-
65
- const subscriberOfferConcurrencyTag = Symbol('subscriberOffer');
66
- this.unregisterOnSubscriberOffer = dispatcher.on(
67
- 'subscriberOffer',
68
- (subscriberOffer) => {
69
- withoutConcurrency(subscriberOfferConcurrencyTag, () => {
70
- return this.negotiate(subscriberOffer);
71
- }).catch((err) => {
72
- this.logger('error', `Negotiation failed.`, err);
73
- });
74
- },
75
- );
76
-
77
- const iceRestartConcurrencyTag = Symbol('iceRestart');
78
- this.unregisterOnIceRestart = dispatcher.on('iceRestart', (iceRestart) => {
79
- withoutConcurrency(iceRestartConcurrencyTag, async () => {
80
- if (iceRestart.peerType !== PeerType.SUBSCRIBER) return;
81
- await this.restartIce();
82
- }).catch((err) => {
83
- this.logger('error', `ICERestart failed`, err);
84
- this.onUnrecoverableError?.();
23
+ this.on('subscriberOffer', async (subscriberOffer) => {
24
+ return this.negotiate(subscriberOffer).catch((err) => {
25
+ this.logger('error', `Negotiation failed.`, err);
85
26
  });
86
27
  });
87
28
  }
88
29
 
89
- /**
90
- * Creates a new `RTCPeerConnection` instance with the given configuration.
91
- *
92
- * @param connectionConfig the connection configuration to use.
93
- */
94
- private createPeerConnection = (connectionConfig?: RTCConfiguration) => {
95
- const pc = new RTCPeerConnection(connectionConfig);
96
- pc.addEventListener('icecandidate', this.onIceCandidate);
97
- pc.addEventListener('track', this.handleOnTrack);
98
-
99
- pc.addEventListener('icecandidateerror', this.onIceCandidateError);
100
- pc.addEventListener(
101
- 'iceconnectionstatechange',
102
- this.onIceConnectionStateChange,
103
- );
104
- pc.addEventListener(
105
- 'icegatheringstatechange',
106
- this.onIceGatheringStateChange,
107
- );
108
-
109
- return pc;
110
- };
111
-
112
- /**
113
- * Closes the `RTCPeerConnection` and unsubscribes from the dispatcher.
114
- */
115
- close = () => {
116
- this.detachEventHandlers();
117
- this.pc.close();
118
- };
119
-
120
30
  /**
121
31
  * Detaches the event handlers from the `RTCPeerConnection`.
122
32
  * This is useful when we want to replace the `RTCPeerConnection`
123
33
  * instance with a new one (in case of migration).
124
34
  */
125
- detachEventHandlers = () => {
126
- this.unregisterOnSubscriberOffer();
127
- this.unregisterOnIceRestart();
128
-
129
- this.pc.removeEventListener('icecandidate', this.onIceCandidate);
35
+ detachEventHandlers() {
36
+ super.detachEventHandlers();
130
37
  this.pc.removeEventListener('track', this.handleOnTrack);
131
- this.pc.removeEventListener('icecandidateerror', this.onIceCandidateError);
132
- this.pc.removeEventListener(
133
- 'iceconnectionstatechange',
134
- this.onIceConnectionStateChange,
135
- );
136
- this.pc.removeEventListener(
137
- 'icegatheringstatechange',
138
- this.onIceGatheringStateChange,
139
- );
140
- };
141
-
142
- /**
143
- * Returns the result of the `RTCPeerConnection.getStats()` method
144
- * @param selector
145
- * @returns
146
- */
147
- getStats = (selector?: MediaStreamTrack | null | undefined) => {
148
- return this.pc.getStats(selector);
149
- };
150
-
151
- /**
152
- * Sets the SFU client to use.
153
- *
154
- * @param sfuClient the SFU client to use.
155
- */
156
- setSfuClient = (sfuClient: StreamSfuClient) => {
157
- this.sfuClient = sfuClient;
158
- };
38
+ }
159
39
 
160
40
  /**
161
41
  * Restarts the ICE connection and renegotiates with the SFU.
@@ -239,7 +119,18 @@ export class Subscriber {
239
119
  this.logger('error', `Unknown track type: ${rawTrackType}`);
240
120
  return;
241
121
  }
122
+
123
+ // get the previous stream to dispose it later
124
+ // usually this happens during migration, when the stream is replaced
125
+ // with a new one but the old one is still in the state
242
126
  const previousStream = participantToUpdate[streamKindProp];
127
+
128
+ // replace the previous stream with the new one, prevents flickering
129
+ this.state.updateParticipant(participantToUpdate.sessionId, {
130
+ [streamKindProp]: primaryStream,
131
+ });
132
+
133
+ // now, dispose the previous stream if it exists
243
134
  if (previousStream) {
244
135
  this.logger(
245
136
  'info',
@@ -250,26 +141,6 @@ export class Subscriber {
250
141
  previousStream.removeTrack(t);
251
142
  });
252
143
  }
253
- this.state.updateParticipant(participantToUpdate.sessionId, {
254
- [streamKindProp]: primaryStream,
255
- });
256
- };
257
-
258
- private onIceCandidate = (e: RTCPeerConnectionIceEvent) => {
259
- const { candidate } = e;
260
- if (!candidate) {
261
- this.logger('debug', 'null ice candidate');
262
- return;
263
- }
264
-
265
- this.sfuClient
266
- .iceTrickle({
267
- iceCandidate: getIceCandidate(candidate),
268
- peerType: PeerType.SUBSCRIBER,
269
- })
270
- .catch((err) => {
271
- this.logger('warn', `ICETrickle failed`, err);
272
- });
273
144
  };
274
145
 
275
146
  private negotiate = async (subscriberOffer: SubscriberOffer) => {
@@ -280,16 +151,7 @@ export class Subscriber {
280
151
  sdp: subscriberOffer.sdp,
281
152
  });
282
153
 
283
- this.sfuClient.iceTrickleBuffer.subscriberCandidates.subscribe(
284
- async (candidate) => {
285
- try {
286
- const iceCandidate = JSON.parse(candidate.iceCandidate);
287
- await this.pc.addIceCandidate(iceCandidate);
288
- } catch (e) {
289
- this.logger('warn', `ICE candidate error`, [e, candidate]);
290
- }
291
- },
292
- );
154
+ this.addTrickledIceCandidates();
293
155
 
294
156
  const answer = await this.pc.createAnswer();
295
157
  await this.pc.setLocalDescription(answer);
@@ -301,40 +163,4 @@ export class Subscriber {
301
163
 
302
164
  this.isIceRestarting = false;
303
165
  };
304
-
305
- private onIceConnectionStateChange = () => {
306
- const state = this.pc.iceConnectionState;
307
- this.logger('debug', `ICE connection state changed`, state);
308
-
309
- if (this.state.callingState === CallingState.RECONNECTING) return;
310
-
311
- // do nothing when ICE is restarting
312
- if (this.isIceRestarting) return;
313
-
314
- if (state === 'failed' || state === 'disconnected') {
315
- this.logger('debug', `Attempting to restart ICE`);
316
- this.restartIce().catch((e) => {
317
- this.logger('error', `ICE restart failed`, e);
318
- this.onUnrecoverableError?.();
319
- });
320
- }
321
- };
322
-
323
- private onIceGatheringStateChange = () => {
324
- this.logger(
325
- 'debug',
326
- `ICE gathering state changed`,
327
- this.pc.iceGatheringState,
328
- );
329
- };
330
-
331
- private onIceCandidateError = (e: Event) => {
332
- const errorMessage =
333
- e instanceof RTCPeerConnectionIceErrorEvent &&
334
- `${e.errorCode}: ${e.errorText}`;
335
- const iceState = this.pc.iceConnectionState;
336
- const logLevel =
337
- iceState === 'connected' || iceState === 'checking' ? 'debug' : 'warn';
338
- this.logger(logLevel, `ICE Candidate error`, errorMessage);
339
- };
340
166
  }
@@ -0,0 +1,120 @@
1
+ import { PublishOption, TrackType } from '../gen/video/sfu/models/models';
2
+ import { OptimalVideoLayer } from './videoLayers';
3
+
4
+ type TransceiverId = {
5
+ publishOption: PublishOption;
6
+ transceiver: RTCRtpTransceiver;
7
+ };
8
+ type TrackLayersCache = {
9
+ publishOption: PublishOption;
10
+ layers: OptimalVideoLayer[];
11
+ };
12
+
13
+ export class TransceiverCache {
14
+ private readonly cache: TransceiverId[] = [];
15
+ private readonly layers: TrackLayersCache[] = [];
16
+
17
+ /**
18
+ * An array maintaining the order how transceivers were added to the peer connection.
19
+ * This is needed because some browsers (Firefox) don't reliably report
20
+ * trackId and `mid` parameters.
21
+ */
22
+ private readonly transceiverOrder: RTCRtpTransceiver[] = [];
23
+
24
+ /**
25
+ * Adds a transceiver to the cache.
26
+ */
27
+ add = (publishOption: PublishOption, transceiver: RTCRtpTransceiver) => {
28
+ this.cache.push({ publishOption, transceiver });
29
+ this.transceiverOrder.push(transceiver);
30
+ };
31
+
32
+ /**
33
+ * Gets the transceiver for the given publish option.
34
+ */
35
+ get = (publishOption: PublishOption): RTCRtpTransceiver | undefined => {
36
+ return this.findTransceiver(publishOption)?.transceiver;
37
+ };
38
+
39
+ /**
40
+ * Gets the last transceiver for the given track type and publish option id.
41
+ */
42
+ getWith = (trackType: TrackType, id: number) => {
43
+ return this.findTransceiver({ trackType, id })?.transceiver;
44
+ };
45
+
46
+ /**
47
+ * Checks if the cache has the given publish option.
48
+ */
49
+ has = (publishOption: PublishOption): boolean => {
50
+ return !!this.get(publishOption);
51
+ };
52
+
53
+ /**
54
+ * Finds the first transceiver that satisfies the given predicate.
55
+ */
56
+ find = (
57
+ predicate: (item: TransceiverId) => boolean,
58
+ ): TransceiverId | undefined => {
59
+ return this.cache.find(predicate);
60
+ };
61
+
62
+ /**
63
+ * Provides all the items in the cache.
64
+ */
65
+ items = (): TransceiverId[] => {
66
+ return this.cache;
67
+ };
68
+
69
+ /**
70
+ * Init index of the transceiver in the cache.
71
+ */
72
+ indexOf = (transceiver: RTCRtpTransceiver): number => {
73
+ return this.transceiverOrder.indexOf(transceiver);
74
+ };
75
+
76
+ /**
77
+ * Gets cached video layers for the given track.
78
+ */
79
+ getLayers = (
80
+ publishOption: PublishOption,
81
+ ): OptimalVideoLayer[] | undefined => {
82
+ const entry = this.layers.find(
83
+ (item) =>
84
+ item.publishOption.id === publishOption.id &&
85
+ item.publishOption.trackType === publishOption.trackType,
86
+ );
87
+ return entry?.layers;
88
+ };
89
+
90
+ /**
91
+ * Sets the video layers for the given track.
92
+ */
93
+ setLayers = (
94
+ publishOption: PublishOption,
95
+ layers: OptimalVideoLayer[] = [],
96
+ ) => {
97
+ const entry = this.findLayer(publishOption);
98
+ if (entry) {
99
+ entry.layers = layers;
100
+ } else {
101
+ this.layers.push({ publishOption, layers });
102
+ }
103
+ };
104
+
105
+ private findTransceiver = (publishOption: Partial<PublishOption>) => {
106
+ return this.cache.find(
107
+ (item) =>
108
+ item.publishOption.id === publishOption.id &&
109
+ item.publishOption.trackType === publishOption.trackType,
110
+ );
111
+ };
112
+
113
+ private findLayer = (publishOption: PublishOption) => {
114
+ return this.layers.find(
115
+ (item) =>
116
+ item.publishOption.id === publishOption.id &&
117
+ item.publishOption.trackType === publishOption.trackType,
118
+ );
119
+ };
120
+ }