@stream-io/video-client 1.14.0 → 1.15.1

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 (92) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/index.browser.es.js +1533 -1783
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +1514 -1783
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +1533 -1783
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +43 -28
  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/video/sfu/event/events.d.ts +38 -19
  15. package/dist/src/gen/video/sfu/models/models.d.ts +76 -9
  16. package/dist/src/helpers/array.d.ts +7 -0
  17. package/dist/src/permissions/PermissionsContext.d.ts +6 -0
  18. package/dist/src/rtc/BasePeerConnection.d.ts +90 -0
  19. package/dist/src/rtc/Dispatcher.d.ts +0 -1
  20. package/dist/src/rtc/IceTrickleBuffer.d.ts +3 -2
  21. package/dist/src/rtc/Publisher.d.ts +32 -86
  22. package/dist/src/rtc/Subscriber.d.ts +4 -56
  23. package/dist/src/rtc/TransceiverCache.d.ts +55 -0
  24. package/dist/src/rtc/codecs.d.ts +1 -15
  25. package/dist/src/rtc/helpers/sdp.d.ts +8 -0
  26. package/dist/src/rtc/helpers/tracks.d.ts +1 -0
  27. package/dist/src/rtc/index.d.ts +3 -0
  28. package/dist/src/rtc/videoLayers.d.ts +11 -25
  29. package/dist/src/stats/{stateStoreStatsReporter.d.ts → CallStateStatsReporter.d.ts} +5 -1
  30. package/dist/src/stats/SfuStatsReporter.d.ts +4 -2
  31. package/dist/src/stats/index.d.ts +1 -1
  32. package/dist/src/stats/types.d.ts +8 -0
  33. package/dist/src/types.d.ts +12 -22
  34. package/package.json +1 -1
  35. package/src/Call.ts +254 -268
  36. package/src/StreamSfuClient.ts +9 -14
  37. package/src/StreamVideoClient.ts +1 -1
  38. package/src/__tests__/Call.publishing.test.ts +306 -0
  39. package/src/devices/CameraManager.ts +33 -16
  40. package/src/devices/InputMediaDeviceManager.ts +38 -27
  41. package/src/devices/MicrophoneManager.ts +29 -8
  42. package/src/devices/ScreenShareManager.ts +6 -8
  43. package/src/devices/__tests__/CameraManager.test.ts +111 -14
  44. package/src/devices/__tests__/InputMediaDeviceManager.test.ts +4 -4
  45. package/src/devices/__tests__/MicrophoneManager.test.ts +59 -21
  46. package/src/devices/__tests__/ScreenShareManager.test.ts +5 -5
  47. package/src/devices/__tests__/mocks.ts +1 -0
  48. package/src/events/__tests__/internal.test.ts +132 -0
  49. package/src/events/__tests__/mutes.test.ts +0 -3
  50. package/src/events/__tests__/speaker.test.ts +92 -0
  51. package/src/events/participant.ts +3 -4
  52. package/src/gen/video/sfu/event/events.ts +91 -30
  53. package/src/gen/video/sfu/models/models.ts +105 -13
  54. package/src/helpers/array.ts +14 -0
  55. package/src/permissions/PermissionsContext.ts +22 -0
  56. package/src/permissions/__tests__/PermissionsContext.test.ts +40 -0
  57. package/src/rpc/__tests__/createClient.test.ts +38 -0
  58. package/src/rpc/createClient.ts +11 -5
  59. package/src/rtc/BasePeerConnection.ts +240 -0
  60. package/src/rtc/Dispatcher.ts +0 -9
  61. package/src/rtc/IceTrickleBuffer.ts +24 -4
  62. package/src/rtc/Publisher.ts +210 -528
  63. package/src/rtc/Subscriber.ts +26 -200
  64. package/src/rtc/TransceiverCache.ts +120 -0
  65. package/src/rtc/__tests__/Publisher.test.ts +407 -210
  66. package/src/rtc/__tests__/Subscriber.test.ts +88 -36
  67. package/src/rtc/__tests__/mocks/webrtc.mocks.ts +22 -2
  68. package/src/rtc/__tests__/videoLayers.test.ts +161 -54
  69. package/src/rtc/codecs.ts +1 -131
  70. package/src/rtc/helpers/__tests__/rtcConfiguration.test.ts +34 -0
  71. package/src/rtc/helpers/__tests__/sdp.test.ts +59 -0
  72. package/src/rtc/helpers/sdp.ts +30 -0
  73. package/src/rtc/helpers/tracks.ts +3 -0
  74. package/src/rtc/index.ts +4 -0
  75. package/src/rtc/videoLayers.ts +68 -76
  76. package/src/stats/{stateStoreStatsReporter.ts → CallStateStatsReporter.ts} +58 -27
  77. package/src/stats/SfuStatsReporter.ts +31 -3
  78. package/src/stats/index.ts +1 -1
  79. package/src/stats/types.ts +12 -0
  80. package/src/types.ts +12 -22
  81. package/dist/src/helpers/sdp-munging.d.ts +0 -24
  82. package/dist/src/rtc/bitrateLookup.d.ts +0 -2
  83. package/dist/src/rtc/helpers/iceCandidate.d.ts +0 -2
  84. package/src/helpers/__tests__/hq-audio-sdp.ts +0 -332
  85. package/src/helpers/__tests__/sdp-munging.test.ts +0 -283
  86. package/src/helpers/sdp-munging.ts +0 -265
  87. package/src/rtc/__tests__/bitrateLookup.test.ts +0 -12
  88. package/src/rtc/__tests__/codecs.test.ts +0 -145
  89. package/src/rtc/bitrateLookup.ts +0 -61
  90. package/src/rtc/helpers/iceCandidate.ts +0 -16
  91. /package/dist/src/{compatibility.d.ts → helpers/compatibility.d.ts} +0 -0
  92. /package/src/{compatibility.ts → helpers/compatibility.ts} +0 -0
@@ -1,46 +1,27 @@
1
- import { StreamSfuClient } from '../StreamSfuClient';
1
+ import {
2
+ BasePeerConnection,
3
+ BasePeerConnectionOpts,
4
+ } from './BasePeerConnection';
5
+ import { TransceiverCache } from './TransceiverCache';
2
6
  import {
3
7
  PeerType,
8
+ PublishOption,
4
9
  TrackInfo,
5
10
  TrackType,
6
- VideoLayer,
7
11
  } from '../gen/video/sfu/models/models';
8
- import { getIceCandidate } from './helpers/iceCandidate';
12
+ import { VideoSender } from '../gen/video/sfu/event/events';
9
13
  import {
10
- findOptimalScreenSharingLayers,
11
- findOptimalVideoLayers,
12
- OptimalVideoLayer,
13
- ridToVideoQuality,
14
+ computeVideoLayers,
14
15
  toSvcEncodings,
16
+ toVideoLayers,
15
17
  } from './videoLayers';
16
- import { getOptimalVideoCodec, getPreferredCodecs, isSvcCodec } from './codecs';
17
- import { trackTypeToParticipantStreamKey } from './helpers/tracks';
18
- import { CallingState, CallState } from '../store';
19
- import { PublishOptions } from '../types';
20
- import {
21
- enableHighQualityAudio,
22
- extractMid,
23
- preserveCodec,
24
- toggleDtx,
25
- } from '../helpers/sdp-munging';
26
- import { Logger } from '../coordinator/connection/types';
27
- import { getLogger } from '../logger';
28
- import { Dispatcher } from './Dispatcher';
29
- import { VideoLayerSetting } from '../gen/video/sfu/event/events';
30
- import { TargetResolutionResponse } from '../gen/shims';
18
+ import { isSvcCodec } from './codecs';
19
+ import { isAudioTrackType } from './helpers/tracks';
20
+ import { extractMid } from './helpers/sdp';
31
21
  import { withoutConcurrency } from '../helpers/concurrency';
32
- import { isReactNative } from '../helpers/platforms';
33
- import { isFirefox } from '../helpers/browsers';
34
-
35
- export type PublisherConstructorOpts = {
36
- sfuClient: StreamSfuClient;
37
- state: CallState;
38
- dispatcher: Dispatcher;
39
- connectionConfig?: RTCConfiguration;
40
- isDtxEnabled: boolean;
41
- isRedEnabled: boolean;
42
- onUnrecoverableError?: () => void;
43
- logTag: string;
22
+
23
+ export type PublisherConstructorOpts = BasePeerConnectionOpts & {
24
+ publishOptions: PublishOption[];
44
25
  };
45
26
 
46
27
  /**
@@ -48,54 +29,19 @@ export type PublisherConstructorOpts = {
48
29
  *
49
30
  * @internal
50
31
  */
51
- export class Publisher {
52
- private readonly logger: Logger;
53
- private pc: RTCPeerConnection;
54
- private readonly state: CallState;
55
- private readonly transceiverCache = new Map<TrackType, RTCRtpTransceiver>();
56
- private readonly trackLayersCache = new Map<TrackType, OptimalVideoLayer[]>();
57
- private readonly publishOptsForTrack = new Map<TrackType, PublishOptions>();
58
-
59
- /**
60
- * An array maintaining the order how transceivers were added to the peer connection.
61
- * This is needed because some browsers (Firefox) don't reliably report
62
- * trackId and `mid` parameters.
63
- *
64
- * @internal
65
- */
66
- private readonly transceiverInitOrder: TrackType[] = [];
67
- private readonly isDtxEnabled: boolean;
68
- private readonly isRedEnabled: boolean;
69
-
70
- private readonly unsubscribeOnIceRestart: () => void;
71
- private readonly unsubscribeChangePublishQuality: () => void;
72
- private readonly onUnrecoverableError?: () => void;
73
-
74
- private isIceRestarting = false;
75
- private sfuClient: StreamSfuClient;
32
+ export class Publisher extends BasePeerConnection {
33
+ private readonly transceiverCache = new TransceiverCache();
34
+ private publishOptions: PublishOption[];
76
35
 
77
36
  /**
78
37
  * Constructs a new `Publisher` instance.
79
38
  */
80
- constructor({
81
- connectionConfig,
82
- sfuClient,
83
- dispatcher,
84
- state,
85
- isDtxEnabled,
86
- isRedEnabled,
87
- onUnrecoverableError,
88
- logTag,
89
- }: PublisherConstructorOpts) {
90
- this.logger = getLogger(['Publisher', logTag]);
91
- this.pc = this.createPeerConnection(connectionConfig);
92
- this.sfuClient = sfuClient;
93
- this.state = state;
94
- this.isDtxEnabled = isDtxEnabled;
95
- this.isRedEnabled = isRedEnabled;
96
- this.onUnrecoverableError = onUnrecoverableError;
97
-
98
- this.unsubscribeOnIceRestart = dispatcher.on('iceRestart', (iceRestart) => {
39
+ constructor({ publishOptions, ...baseOptions }: PublisherConstructorOpts) {
40
+ super(PeerType.PUBLISHER_UNSPECIFIED, baseOptions);
41
+ this.publishOptions = publishOptions;
42
+ this.pc.addEventListener('negotiationneeded', this.onNegotiationNeeded);
43
+
44
+ this.on('iceRestart', (iceRestart) => {
99
45
  if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED) return;
100
46
  this.restartIce().catch((err) => {
101
47
  this.logger('warn', `ICERestart failed`, err);
@@ -103,79 +49,27 @@ export class Publisher {
103
49
  });
104
50
  });
105
51
 
106
- this.unsubscribeChangePublishQuality = dispatcher.on(
107
- 'changePublishQuality',
108
- ({ videoSenders }) => {
109
- withoutConcurrency('publisher.changePublishQuality', async () => {
110
- for (const videoSender of videoSenders) {
111
- const { layers } = videoSender;
112
- const enabledLayers = layers.filter((l) => l.active);
113
- await this.changePublishQuality(enabledLayers);
114
- }
115
- }).catch((err) => {
116
- this.logger('warn', 'Failed to change publish quality', err);
117
- });
118
- },
119
- );
120
- }
121
-
122
- private createPeerConnection = (connectionConfig?: RTCConfiguration) => {
123
- const pc = new RTCPeerConnection(connectionConfig);
124
- pc.addEventListener('icecandidate', this.onIceCandidate);
125
- pc.addEventListener('negotiationneeded', this.onNegotiationNeeded);
126
-
127
- pc.addEventListener('icecandidateerror', this.onIceCandidateError);
128
- pc.addEventListener(
129
- 'iceconnectionstatechange',
130
- this.onIceConnectionStateChange,
131
- );
132
- pc.addEventListener(
133
- 'icegatheringstatechange',
134
- this.onIceGatheringStateChange,
135
- );
136
- pc.addEventListener('signalingstatechange', this.onSignalingStateChange);
137
- return pc;
138
- };
139
-
140
- /**
141
- * Closes the publisher PeerConnection and cleans up the resources.
142
- */
143
- close = ({ stopTracks }: { stopTracks: boolean }) => {
144
- if (stopTracks) {
145
- this.stopPublishing();
146
- this.transceiverCache.clear();
147
- this.trackLayersCache.clear();
148
- }
52
+ this.on('changePublishQuality', async (event) => {
53
+ for (const videoSender of event.videoSenders) {
54
+ await this.changePublishQuality(videoSender);
55
+ }
56
+ });
149
57
 
150
- this.detachEventHandlers();
151
- this.pc.close();
152
- };
58
+ this.on('changePublishOptions', (event) => {
59
+ this.publishOptions = event.publishOptions;
60
+ return this.syncPublishOptions();
61
+ });
62
+ }
153
63
 
154
64
  /**
155
65
  * Detaches the event handlers from the `RTCPeerConnection`.
156
66
  * This is useful when we want to replace the `RTCPeerConnection`
157
67
  * instance with a new one (in case of migration).
158
68
  */
159
- detachEventHandlers = () => {
160
- this.unsubscribeOnIceRestart();
161
- this.unsubscribeChangePublishQuality();
162
-
163
- this.pc.removeEventListener('icecandidate', this.onIceCandidate);
69
+ detachEventHandlers() {
70
+ super.detachEventHandlers();
164
71
  this.pc.removeEventListener('negotiationneeded', this.onNegotiationNeeded);
165
- this.pc.removeEventListener('icecandidateerror', this.onIceCandidateError);
166
- this.pc.removeEventListener(
167
- 'iceconnectionstatechange',
168
- this.onIceConnectionStateChange,
169
- );
170
- this.pc.removeEventListener(
171
- 'icegatheringstatechange',
172
- this.onIceGatheringStateChange,
173
- );
174
- this.pc.removeEventListener(
175
- 'signalingstatechange',
176
- this.onSignalingStateChange,
177
- );
178
- };
72
+ }
179
73
 
180
74
  /**
181
75
  * Starts publishing the given track of the given media stream.
@@ -183,135 +77,86 @@ export class Publisher {
183
77
  * Consecutive calls to this method will replace the stream.
184
78
  * The previous stream will be stopped.
185
79
  *
186
- * @param mediaStream the media stream to publish.
187
80
  * @param track the track to publish.
188
81
  * @param trackType the track type to publish.
189
- * @param opts the optional publish options to use.
190
82
  */
191
- publishStream = async (
192
- mediaStream: MediaStream,
193
- track: MediaStreamTrack,
194
- trackType: TrackType,
195
- opts: PublishOptions = {},
196
- ) => {
197
- if (track.readyState === 'ended') {
198
- throw new Error(`Can't publish a track that has ended already.`);
83
+ publish = async (track: MediaStreamTrack, trackType: TrackType) => {
84
+ if (!this.publishOptions.some((o) => o.trackType === trackType)) {
85
+ throw new Error(`No publish options found for ${TrackType[trackType]}`);
199
86
  }
200
87
 
201
- // enable the track if it is disabled
202
- if (!track.enabled) track.enabled = true;
203
-
204
- const transceiver = this.transceiverCache.get(trackType);
205
- if (!transceiver || !transceiver.sender.track) {
206
- // listen for 'ended' event on the track as it might be ended abruptly
207
- // by an external factors such as permission revokes, a disconnected device, etc.
208
- // keep in mind that `track.stop()` doesn't trigger this event.
209
- const handleTrackEnded = () => {
210
- this.logger('info', `Track ${TrackType[trackType]} has ended abruptly`);
211
- track.removeEventListener('ended', handleTrackEnded);
212
- this.notifyTrackMuteStateChanged(mediaStream, trackType, true).catch(
213
- (err) => this.logger('warn', `Couldn't notify track mute state`, err),
214
- );
215
- };
216
- track.addEventListener('ended', handleTrackEnded);
217
- this.addTransceiver(trackType, track, opts, mediaStream);
218
- } else {
219
- await this.updateTransceiver(transceiver, track);
220
- }
88
+ for (const publishOption of this.publishOptions) {
89
+ if (publishOption.trackType !== trackType) continue;
90
+
91
+ // create a clone of the track as otherwise the same trackId will
92
+ // appear in the SDP in multiple transceivers
93
+ const trackToPublish = track.clone();
221
94
 
222
- await this.notifyTrackMuteStateChanged(mediaStream, trackType, false);
95
+ const transceiver = this.transceiverCache.get(publishOption);
96
+ if (!transceiver) {
97
+ this.addTransceiver(trackToPublish, publishOption);
98
+ } else {
99
+ await transceiver.sender.replaceTrack(trackToPublish);
100
+ }
101
+ }
223
102
  };
224
103
 
225
104
  /**
226
- * Adds a new transceiver to the peer connection.
227
- * This needs to be called when a new track kind is added to the peer connection.
228
- * In other cases, use `updateTransceiver` method.
105
+ * Adds a new transceiver carrying the given track to the peer connection.
229
106
  */
230
107
  private addTransceiver = (
231
- trackType: TrackType,
232
108
  track: MediaStreamTrack,
233
- opts: PublishOptions,
234
- mediaStream: MediaStream,
109
+ publishOption: PublishOption,
235
110
  ) => {
236
- const { forceCodec, preferredCodec } = opts;
237
- const codecInUse = forceCodec || getOptimalVideoCodec(preferredCodec);
238
- const videoEncodings = this.computeLayers(trackType, track, opts);
111
+ const videoEncodings = computeVideoLayers(track, publishOption);
112
+ const sendEncodings = isSvcCodec(publishOption.codec?.name)
113
+ ? toSvcEncodings(videoEncodings)
114
+ : videoEncodings;
239
115
  const transceiver = this.pc.addTransceiver(track, {
240
116
  direction: 'sendonly',
241
- streams:
242
- trackType === TrackType.VIDEO || trackType === TrackType.SCREEN_SHARE
243
- ? [mediaStream]
244
- : undefined,
245
- sendEncodings: isSvcCodec(codecInUse)
246
- ? toSvcEncodings(videoEncodings)
247
- : videoEncodings,
117
+ sendEncodings,
248
118
  });
249
119
 
120
+ const trackType = publishOption.trackType;
250
121
  this.logger('debug', `Added ${TrackType[trackType]} transceiver`);
251
- this.transceiverInitOrder.push(trackType);
252
- this.transceiverCache.set(trackType, transceiver);
253
- this.publishOptsForTrack.set(trackType, opts);
254
-
255
- // handle codec preferences
256
- if (!('setCodecPreferences' in transceiver)) return;
257
-
258
- const codecPreferences = this.getCodecPreferences(
259
- trackType,
260
- trackType === TrackType.VIDEO ? codecInUse : undefined,
261
- 'receiver',
262
- );
263
- if (!codecPreferences) return;
264
-
265
- try {
266
- this.logger(
267
- 'info',
268
- `Setting ${TrackType[trackType]} codec preferences`,
269
- codecPreferences,
270
- );
271
- transceiver.setCodecPreferences(codecPreferences);
272
- } catch (err) {
273
- this.logger('warn', `Couldn't set codec preferences`, err);
274
- }
122
+ this.transceiverCache.add(publishOption, transceiver);
275
123
  };
276
124
 
277
125
  /**
278
- * Updates the given transceiver with the new track.
279
- * Stops the previous track and replaces it with the new one.
126
+ * Synchronizes the current Publisher state with the provided publish options.
280
127
  */
281
- private updateTransceiver = async (
282
- transceiver: RTCRtpTransceiver,
283
- track: MediaStreamTrack,
284
- ) => {
285
- const previousTrack = transceiver.sender.track;
286
- // don't stop the track if we are re-publishing the same track
287
- if (previousTrack && previousTrack !== track) {
288
- previousTrack.stop();
128
+ private syncPublishOptions = async () => {
129
+ // enable publishing with new options -> [av1, vp9]
130
+ for (const publishOption of this.publishOptions) {
131
+ const { trackType } = publishOption;
132
+ if (!this.isPublishing(trackType)) continue;
133
+ if (this.transceiverCache.has(publishOption)) continue;
134
+
135
+ const item = this.transceiverCache.find(
136
+ (i) =>
137
+ !!i.transceiver.sender.track &&
138
+ i.publishOption.trackType === trackType,
139
+ );
140
+ if (!item || !item.transceiver) continue;
141
+
142
+ // take the track from the existing transceiver for the same track type,
143
+ // clone it and publish it with the new publish options
144
+ const track = item.transceiver.sender.track!.clone();
145
+ this.addTransceiver(track, publishOption);
289
146
  }
290
- await transceiver.sender.replaceTrack(track);
291
- };
292
147
 
293
- /**
294
- * Stops publishing the given track type to the SFU, if it is currently being published.
295
- * Underlying track will be stopped and removed from the publisher.
296
- * @param trackType the track type to unpublish.
297
- * @param stopTrack specifies whether track should be stopped or just disabled
298
- */
299
- unpublishStream = async (trackType: TrackType, stopTrack: boolean) => {
300
- const transceiver = this.transceiverCache.get(trackType);
301
- if (
302
- transceiver &&
303
- transceiver.sender.track &&
304
- (stopTrack
305
- ? transceiver.sender.track.readyState === 'live'
306
- : transceiver.sender.track.enabled)
307
- ) {
308
- stopTrack
309
- ? transceiver.sender.track.stop()
310
- : (transceiver.sender.track.enabled = false);
311
- // We don't need to notify SFU if unpublishing in response to remote soft mute
312
- if (this.state.localParticipant?.publishedTracks.includes(trackType)) {
313
- await this.notifyTrackMuteStateChanged(undefined, trackType, true);
314
- }
148
+ // stop publishing with options not required anymore -> [vp9]
149
+ for (const item of this.transceiverCache.items()) {
150
+ const { publishOption, transceiver } = item;
151
+ const hasPublishOption = this.publishOptions.some(
152
+ (option) =>
153
+ option.id === publishOption.id &&
154
+ option.trackType === publishOption.trackType,
155
+ );
156
+ if (hasPublishOption) continue;
157
+ // it is safe to stop the track here, it is a clone
158
+ transceiver.sender.track?.stop();
159
+ await transceiver.sender.replaceTrack(null);
315
160
  }
316
161
  };
317
162
 
@@ -321,72 +166,59 @@ export class Publisher {
321
166
  * @param trackType the track type to check.
322
167
  */
323
168
  isPublishing = (trackType: TrackType): boolean => {
324
- const transceiver = this.transceiverCache.get(trackType);
325
- if (!transceiver || !transceiver.sender) return false;
326
- const track = transceiver.sender.track;
327
- return !!track && track.readyState === 'live' && track.enabled;
328
- };
169
+ for (const item of this.transceiverCache.items()) {
170
+ if (item.publishOption.trackType !== trackType) continue;
329
171
 
330
- private notifyTrackMuteStateChanged = async (
331
- mediaStream: MediaStream | undefined,
332
- trackType: TrackType,
333
- isMuted: boolean,
334
- ) => {
335
- await this.sfuClient.updateMuteState(trackType, isMuted);
336
-
337
- const audioOrVideoOrScreenShareStream =
338
- trackTypeToParticipantStreamKey(trackType);
339
- if (!audioOrVideoOrScreenShareStream) return;
340
- if (isMuted) {
341
- this.state.updateParticipant(this.sfuClient.sessionId, (p) => ({
342
- publishedTracks: p.publishedTracks.filter((t) => t !== trackType),
343
- [audioOrVideoOrScreenShareStream]: undefined,
344
- }));
345
- } else {
346
- this.state.updateParticipant(this.sfuClient.sessionId, (p) => {
347
- return {
348
- publishedTracks: p.publishedTracks.includes(trackType)
349
- ? p.publishedTracks
350
- : [...p.publishedTracks, trackType],
351
- [audioOrVideoOrScreenShareStream]: mediaStream,
352
- };
353
- });
172
+ const track = item.transceiver.sender.track;
173
+ if (!track) continue;
174
+
175
+ if (track.readyState === 'live' && track.enabled) return true;
354
176
  }
177
+ return false;
355
178
  };
356
179
 
357
180
  /**
358
- * Stops publishing all tracks and stop all tracks.
181
+ * Maps the given track ID to the corresponding track type.
359
182
  */
360
- private stopPublishing = () => {
361
- this.logger('debug', 'Stopping publishing all tracks');
362
- this.pc.getSenders().forEach((s) => {
363
- s.track?.stop();
364
- if (this.pc.signalingState !== 'closed') {
365
- this.pc.removeTrack(s);
183
+ getTrackType = (trackId: string): TrackType | undefined => {
184
+ for (const transceiverId of this.transceiverCache.items()) {
185
+ const { publishOption, transceiver } = transceiverId;
186
+ if (transceiver.sender.track?.id === trackId) {
187
+ return publishOption.trackType;
366
188
  }
367
- });
189
+ }
190
+ return undefined;
368
191
  };
369
192
 
370
- private changePublishQuality = async (enabledLayers: VideoLayerSetting[]) => {
371
- this.logger(
372
- 'info',
373
- 'Update publish quality, requested layers by SFU:',
374
- enabledLayers,
375
- );
193
+ /**
194
+ * Stops the cloned track that is being published to the SFU.
195
+ */
196
+ stopTracks = (...trackTypes: TrackType[]) => {
197
+ for (const item of this.transceiverCache.items()) {
198
+ const { publishOption, transceiver } = item;
199
+ if (!trackTypes.includes(publishOption.trackType)) continue;
200
+ transceiver.sender.track?.stop();
201
+ }
202
+ };
376
203
 
377
- const videoSender = this.transceiverCache.get(TrackType.VIDEO)?.sender;
378
- if (!videoSender) {
379
- this.logger('warn', 'Update publish quality, no video sender found.');
380
- return;
204
+ private changePublishQuality = async (videoSender: VideoSender) => {
205
+ const { trackType, layers, publishOptionId } = videoSender;
206
+ const enabledLayers = layers.filter((l) => l.active);
207
+
208
+ const tag = 'Update publish quality:';
209
+ this.logger('info', `${tag} requested layers by SFU:`, enabledLayers);
210
+
211
+ const sender = this.transceiverCache.getWith(
212
+ trackType,
213
+ publishOptionId,
214
+ )?.sender;
215
+ if (!sender) {
216
+ return this.logger('warn', `${tag} no video sender found.`);
381
217
  }
382
218
 
383
- const params = videoSender.getParameters();
219
+ const params = sender.getParameters();
384
220
  if (params.encodings.length === 0) {
385
- this.logger(
386
- 'warn',
387
- 'Update publish quality, No suitable video encoding quality found',
388
- );
389
- return;
221
+ return this.logger('warn', `${tag} there are no encodings set.`);
390
222
  }
391
223
 
392
224
  const [codecInUse] = params.codecs;
@@ -440,73 +272,13 @@ export class Publisher {
440
272
  }
441
273
  }
442
274
 
443
- const activeLayers = params.encodings.filter((e) => e.active);
275
+ const activeEncoders = params.encodings.filter((e) => e.active);
444
276
  if (!changed) {
445
- this.logger('info', `Update publish quality, no change:`, activeLayers);
446
- return;
447
- }
448
-
449
- await videoSender.setParameters(params);
450
- this.logger('info', `Update publish quality, enabled rids:`, activeLayers);
451
- };
452
-
453
- /**
454
- * Returns the result of the `RTCPeerConnection.getStats()` method
455
- * @param selector
456
- * @returns
457
- */
458
- getStats = (selector?: MediaStreamTrack | null | undefined) => {
459
- return this.pc.getStats(selector);
460
- };
461
-
462
- private getCodecPreferences = (
463
- trackType: TrackType,
464
- preferredCodec: string | undefined,
465
- codecPreferencesSource: 'sender' | 'receiver',
466
- ) => {
467
- if (trackType === TrackType.VIDEO) {
468
- return getPreferredCodecs(
469
- 'video',
470
- preferredCodec || 'vp8',
471
- undefined,
472
- codecPreferencesSource,
473
- );
474
- }
475
- if (trackType === TrackType.AUDIO) {
476
- const defaultAudioCodec = this.isRedEnabled ? 'red' : 'opus';
477
- const codecToRemove = !this.isRedEnabled ? 'red' : undefined;
478
- return getPreferredCodecs(
479
- 'audio',
480
- preferredCodec ?? defaultAudioCodec,
481
- codecToRemove,
482
- codecPreferencesSource,
483
- );
277
+ return this.logger('info', `${tag} no change:`, activeEncoders);
484
278
  }
485
- };
486
279
 
487
- private onIceCandidate = (e: RTCPeerConnectionIceEvent) => {
488
- const { candidate } = e;
489
- if (!candidate) {
490
- this.logger('debug', 'null ice candidate');
491
- return;
492
- }
493
- this.sfuClient
494
- .iceTrickle({
495
- iceCandidate: getIceCandidate(candidate),
496
- peerType: PeerType.PUBLISHER_UNSPECIFIED,
497
- })
498
- .catch((err) => {
499
- this.logger('warn', `ICETrickle failed`, err);
500
- });
501
- };
502
-
503
- /**
504
- * Sets the SFU client to use.
505
- *
506
- * @param sfuClient the SFU client to use.
507
- */
508
- setSfuClient = (sfuClient: StreamSfuClient) => {
509
- this.sfuClient = sfuClient;
280
+ await sender.setParameters(params);
281
+ this.logger('info', `${tag} enabled rids:`, activeEncoders);
510
282
  };
511
283
 
512
284
  /**
@@ -523,10 +295,12 @@ export class Publisher {
523
295
  };
524
296
 
525
297
  private onNegotiationNeeded = () => {
526
- this.negotiate().catch((err) => {
527
- this.logger('error', `Negotiation failed.`, err);
528
- this.onUnrecoverableError?.();
529
- });
298
+ withoutConcurrency('publisher.negotiate', () => this.negotiate()).catch(
299
+ (err) => {
300
+ this.logger('error', `Negotiation failed.`, err);
301
+ this.onUnrecoverableError?.();
302
+ },
303
+ );
530
304
  };
531
305
 
532
306
  /**
@@ -536,19 +310,6 @@ export class Publisher {
536
310
  */
537
311
  private negotiate = async (options?: RTCOfferOptions) => {
538
312
  const offer = await this.pc.createOffer(options);
539
- if (offer.sdp) {
540
- offer.sdp = toggleDtx(offer.sdp, this.isDtxEnabled);
541
- if (this.isPublishing(TrackType.SCREEN_SHARE_AUDIO)) {
542
- offer.sdp = this.enableHighQualityAudio(offer.sdp);
543
- }
544
- if (this.isPublishing(TrackType.VIDEO)) {
545
- // Hotfix for platforms that don't respect the ordered codec list
546
- // (Firefox, Android, Linux, etc...).
547
- // We remove all the codecs from the SDP except the one we want to use.
548
- offer.sdp = this.removeUnpreferredCodecs(offer.sdp, TrackType.VIDEO);
549
- }
550
- }
551
-
552
313
  const trackInfos = this.getAnnouncedTracks(offer.sdp);
553
314
  if (trackInfos.length === 0) {
554
315
  throw new Error(`Can't negotiate without announcing any tracks`);
@@ -569,164 +330,85 @@ export class Publisher {
569
330
  this.isIceRestarting = false;
570
331
  }
571
332
 
572
- this.sfuClient.iceTrickleBuffer.publisherCandidates.subscribe(
573
- async (candidate) => {
574
- try {
575
- const iceCandidate = JSON.parse(candidate.iceCandidate);
576
- await this.pc.addIceCandidate(iceCandidate);
577
- } catch (e) {
578
- this.logger('warn', `ICE candidate error`, e, candidate);
579
- }
580
- },
581
- );
333
+ this.addTrickledIceCandidates();
582
334
  };
583
335
 
584
- private removeUnpreferredCodecs(sdp: string, trackType: TrackType): string {
585
- const opts = this.publishOptsForTrack.get(trackType);
586
- const forceSingleCodec =
587
- !!opts?.forceSingleCodec || isReactNative() || isFirefox();
588
- if (!opts || !forceSingleCodec) return sdp;
589
-
590
- const codec = opts.forceCodec || getOptimalVideoCodec(opts.preferredCodec);
591
- const orderedCodecs = this.getCodecPreferences(trackType, codec, 'sender');
592
- if (!orderedCodecs || orderedCodecs.length === 0) return sdp;
593
-
594
- const transceiver = this.transceiverCache.get(trackType);
595
- if (!transceiver) return sdp;
596
-
597
- const index = this.transceiverInitOrder.indexOf(trackType);
598
- const mid = extractMid(transceiver, index, sdp);
599
- const [codecToPreserve] = orderedCodecs;
600
- return preserveCodec(sdp, mid, codecToPreserve);
601
- }
602
-
603
- private enableHighQualityAudio = (sdp: string) => {
604
- const transceiver = this.transceiverCache.get(TrackType.SCREEN_SHARE_AUDIO);
605
- if (!transceiver) return sdp;
606
-
607
- const transceiverInitIndex = this.transceiverInitOrder.indexOf(
608
- TrackType.SCREEN_SHARE_AUDIO,
609
- );
610
- const mid = extractMid(transceiver, transceiverInitIndex, sdp);
611
- return enableHighQualityAudio(sdp, mid);
336
+ /**
337
+ * Returns a list of tracks that are currently being published.
338
+ */
339
+ getPublishedTracks = (): MediaStreamTrack[] => {
340
+ const tracks: MediaStreamTrack[] = [];
341
+ for (const { transceiver } of this.transceiverCache.items()) {
342
+ const track = transceiver.sender.track;
343
+ if (track && track.readyState === 'live') tracks.push(track);
344
+ }
345
+ return tracks;
612
346
  };
613
347
 
614
348
  /**
615
349
  * Returns a list of tracks that are currently being published.
616
- *
617
- * @internal
618
350
  * @param sdp an optional SDP to extract the `mid` from.
619
351
  */
620
- getAnnouncedTracks = (sdp?: string): TrackInfo[] => {
621
- sdp = sdp || this.pc.localDescription?.sdp;
622
- return this.pc
623
- .getTransceivers()
624
- .filter((t) => t.direction === 'sendonly' && t.sender.track)
625
- .map<TrackInfo>((transceiver) => {
626
- let trackType!: TrackType;
627
- this.transceiverCache.forEach((value, key) => {
628
- if (value === transceiver) trackType = key;
629
- });
630
- const track = transceiver.sender.track!;
631
- let optimalLayers: OptimalVideoLayer[];
632
- const isTrackLive = track.readyState === 'live';
633
- if (isTrackLive) {
634
- optimalLayers = this.computeLayers(trackType, track) || [];
635
- this.trackLayersCache.set(trackType, optimalLayers);
636
- } else {
637
- // we report the last known optimal layers for ended tracks
638
- optimalLayers = this.trackLayersCache.get(trackType) || [];
639
- this.logger(
640
- 'debug',
641
- `Track ${TrackType[trackType]} is ended. Announcing last known optimal layers`,
642
- optimalLayers,
643
- );
644
- }
645
-
646
- const layers = optimalLayers.map<VideoLayer>((optimalLayer) => ({
647
- rid: optimalLayer.rid || '',
648
- bitrate: optimalLayer.maxBitrate || 0,
649
- fps: optimalLayer.maxFramerate || 0,
650
- quality: ridToVideoQuality(optimalLayer.rid || ''),
651
- videoDimension: {
652
- width: optimalLayer.width,
653
- height: optimalLayer.height,
654
- },
655
- }));
656
-
657
- const isAudioTrack = [
658
- TrackType.AUDIO,
659
- TrackType.SCREEN_SHARE_AUDIO,
660
- ].includes(trackType);
661
-
662
- const trackSettings = track.getSettings();
663
- const isStereo = isAudioTrack && trackSettings.channelCount === 2;
664
- const transceiverInitIndex =
665
- this.transceiverInitOrder.indexOf(trackType);
666
- return {
667
- trackId: track.id,
668
- layers: layers,
669
- trackType,
670
- mid: extractMid(transceiver, transceiverInitIndex, sdp),
671
- stereo: isStereo,
672
- dtx: isAudioTrack && this.isDtxEnabled,
673
- red: isAudioTrack && this.isRedEnabled,
674
- muted: !isTrackLive,
675
- };
676
- });
677
- };
678
-
679
- private computeLayers = (
680
- trackType: TrackType,
681
- track: MediaStreamTrack,
682
- opts?: PublishOptions,
683
- ): OptimalVideoLayer[] | undefined => {
684
- const { settings } = this.state;
685
- const targetResolution = settings?.video
686
- .target_resolution as TargetResolutionResponse;
687
- const screenShareBitrate =
688
- settings?.screensharing.target_resolution?.bitrate;
689
-
690
- const publishOpts = opts || this.publishOptsForTrack.get(trackType);
691
- const codecInUse =
692
- opts?.forceCodec || getOptimalVideoCodec(opts?.preferredCodec);
693
- return trackType === TrackType.VIDEO
694
- ? findOptimalVideoLayers(track, targetResolution, codecInUse, publishOpts)
695
- : trackType === TrackType.SCREEN_SHARE
696
- ? findOptimalScreenSharingLayers(track, publishOpts, screenShareBitrate)
697
- : undefined;
698
- };
699
-
700
- private onIceCandidateError = (e: Event) => {
701
- const errorMessage =
702
- e instanceof RTCPeerConnectionIceErrorEvent &&
703
- `${e.errorCode}: ${e.errorText}`;
704
- const iceState = this.pc.iceConnectionState;
705
- const logLevel =
706
- iceState === 'connected' || iceState === 'checking' ? 'debug' : 'warn';
707
- this.logger(logLevel, `ICE Candidate error`, errorMessage);
708
- };
709
-
710
- private onIceConnectionStateChange = () => {
711
- const state = this.pc.iceConnectionState;
712
- this.logger('debug', `ICE Connection state changed to`, state);
713
-
714
- if (this.state.callingState === CallingState.RECONNECTING) return;
715
-
716
- if (state === 'failed' || state === 'disconnected') {
717
- this.logger('debug', `Attempting to restart ICE`);
718
- this.restartIce().catch((e) => {
719
- this.logger('error', `ICE restart error`, e);
720
- this.onUnrecoverableError?.();
721
- });
352
+ getAnnouncedTracks = (sdp: string | undefined): TrackInfo[] => {
353
+ const trackInfos: TrackInfo[] = [];
354
+ for (const bundle of this.transceiverCache.items()) {
355
+ const { transceiver, publishOption } = bundle;
356
+ const track = transceiver.sender.track;
357
+ if (!track) continue;
358
+
359
+ trackInfos.push(this.toTrackInfo(transceiver, publishOption, sdp));
722
360
  }
361
+ return trackInfos;
723
362
  };
724
363
 
725
- private onIceGatheringStateChange = () => {
726
- this.logger('debug', `ICE Gathering State`, this.pc.iceGatheringState);
364
+ /**
365
+ * Returns a list of tracks that are currently being published.
366
+ * This method shall be used for the reconnection flow.
367
+ * There we shouldn't announce the tracks that have been stopped due to a codec switch.
368
+ */
369
+ getAnnouncedTracksForReconnect = (): TrackInfo[] => {
370
+ const sdp = this.pc.localDescription?.sdp;
371
+ const trackInfos: TrackInfo[] = [];
372
+ for (const publishOption of this.publishOptions) {
373
+ const transceiver = this.transceiverCache.get(publishOption);
374
+ if (!transceiver || !transceiver.sender.track) continue;
375
+
376
+ trackInfos.push(this.toTrackInfo(transceiver, publishOption, sdp));
377
+ }
378
+ return trackInfos;
727
379
  };
728
380
 
729
- private onSignalingStateChange = () => {
730
- this.logger('debug', `Signaling state changed`, this.pc.signalingState);
381
+ /**
382
+ * Converts the given transceiver to a `TrackInfo` object.
383
+ */
384
+ private toTrackInfo = (
385
+ transceiver: RTCRtpTransceiver,
386
+ publishOption: PublishOption,
387
+ sdp: string | undefined,
388
+ ): TrackInfo => {
389
+ const track = transceiver.sender.track!;
390
+ const isTrackLive = track.readyState === 'live';
391
+ const layers = isTrackLive
392
+ ? computeVideoLayers(track, publishOption)
393
+ : this.transceiverCache.getLayers(publishOption);
394
+ this.transceiverCache.setLayers(publishOption, layers);
395
+
396
+ const isAudioTrack = isAudioTrackType(publishOption.trackType);
397
+ const isStereo = isAudioTrack && track.getSettings().channelCount === 2;
398
+ const transceiverIndex = this.transceiverCache.indexOf(transceiver);
399
+ const audioSettings = this.state.settings?.audio;
400
+
401
+ return {
402
+ trackId: track.id,
403
+ layers: toVideoLayers(layers),
404
+ trackType: publishOption.trackType,
405
+ mid: extractMid(transceiver, transceiverIndex, sdp),
406
+ stereo: isStereo,
407
+ dtx: isAudioTrack && !!audioSettings?.opus_dtx_enabled,
408
+ red: isAudioTrack && !!audioSettings?.redundant_coding_enabled,
409
+ muted: !isTrackLive,
410
+ codec: publishOption.codec,
411
+ publishOptionId: publishOption.id,
412
+ };
731
413
  };
732
414
  }