@stream-io/video-client 1.8.4 → 1.9.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 (36) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/index.browser.es.js +409 -441
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +409 -441
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +409 -441
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +10 -12
  9. package/dist/src/devices/CameraManager.d.ts +2 -22
  10. package/dist/src/events/internal.d.ts +0 -4
  11. package/dist/src/gen/video/sfu/event/events.d.ts +2 -71
  12. package/dist/src/helpers/sdp-munging.d.ts +8 -0
  13. package/dist/src/rtc/Publisher.d.ts +18 -23
  14. package/dist/src/rtc/bitrateLookup.d.ts +2 -0
  15. package/dist/src/rtc/codecs.d.ts +9 -2
  16. package/dist/src/rtc/videoLayers.d.ts +31 -4
  17. package/dist/src/types.d.ts +30 -2
  18. package/package.json +1 -1
  19. package/src/Call.ts +21 -38
  20. package/src/devices/CameraManager.ts +8 -42
  21. package/src/devices/ScreenShareManager.ts +1 -3
  22. package/src/devices/__tests__/CameraManager.test.ts +0 -15
  23. package/src/devices/__tests__/ScreenShareManager.test.ts +0 -14
  24. package/src/events/callEventHandlers.ts +0 -2
  25. package/src/events/internal.ts +0 -16
  26. package/src/gen/video/sfu/event/events.ts +8 -120
  27. package/src/helpers/sdp-munging.ts +38 -15
  28. package/src/rtc/Publisher.ts +211 -317
  29. package/src/rtc/__tests__/Publisher.test.ts +196 -7
  30. package/src/rtc/__tests__/bitrateLookup.test.ts +12 -0
  31. package/src/rtc/__tests__/mocks/webrtc.mocks.ts +2 -0
  32. package/src/rtc/__tests__/videoLayers.test.ts +51 -36
  33. package/src/rtc/bitrateLookup.ts +61 -0
  34. package/src/rtc/codecs.ts +56 -9
  35. package/src/rtc/videoLayers.ts +68 -19
  36. package/src/types.ts +30 -2
@@ -1,29 +1,33 @@
1
- import * as SDP from 'sdp-transform';
2
1
  import { StreamSfuClient } from '../StreamSfuClient';
3
2
  import {
4
3
  PeerType,
5
4
  TrackInfo,
6
5
  TrackType,
7
6
  VideoLayer,
8
- VideoQuality,
9
7
  } from '../gen/video/sfu/models/models';
10
8
  import { getIceCandidate } from './helpers/iceCandidate';
11
9
  import {
12
10
  findOptimalScreenSharingLayers,
13
11
  findOptimalVideoLayers,
14
12
  OptimalVideoLayer,
13
+ ridToVideoQuality,
14
+ toSvcEncodings,
15
15
  } from './videoLayers';
16
- import { getPreferredCodecs, getRNOptimalCodec } from './codecs';
16
+ import { getOptimalVideoCodec, getPreferredCodecs, isSvcCodec } from './codecs';
17
17
  import { trackTypeToParticipantStreamKey } from './helpers/tracks';
18
18
  import { CallingState, CallState } from '../store';
19
19
  import { PublishOptions } from '../types';
20
- import { isReactNative } from '../helpers/platforms';
21
- import { enableHighQualityAudio, toggleDtx } from '../helpers/sdp-munging';
20
+ import {
21
+ enableHighQualityAudio,
22
+ extractMid,
23
+ toggleDtx,
24
+ } from '../helpers/sdp-munging';
22
25
  import { Logger } from '../coordinator/connection/types';
23
26
  import { getLogger } from '../logger';
24
27
  import { Dispatcher } from './Dispatcher';
25
28
  import { VideoLayerSetting } from '../gen/video/sfu/event/events';
26
29
  import { TargetResolutionResponse } from '../gen/shims';
30
+ import { withoutConcurrency } from '../helpers/concurrency';
27
31
 
28
32
  export type PublisherConstructorOpts = {
29
33
  sfuClient: StreamSfuClient;
@@ -45,21 +49,9 @@ export class Publisher {
45
49
  private readonly logger: Logger;
46
50
  private pc: RTCPeerConnection;
47
51
  private readonly state: CallState;
48
-
49
- private readonly transceiverRegistry: {
50
- [key in TrackType]: RTCRtpTransceiver | undefined;
51
- } = {
52
- [TrackType.AUDIO]: undefined,
53
- [TrackType.VIDEO]: undefined,
54
- [TrackType.SCREEN_SHARE]: undefined,
55
- [TrackType.SCREEN_SHARE_AUDIO]: undefined,
56
- [TrackType.UNSPECIFIED]: undefined,
57
- };
58
-
59
- private readonly publishOptionsPerTrackType = new Map<
60
- TrackType,
61
- PublishOptions
62
- >();
52
+ private readonly transceiverCache = new Map<TrackType, RTCRtpTransceiver>();
53
+ private readonly trackLayersCache = new Map<TrackType, OptimalVideoLayer[]>();
54
+ private readonly publishOptsForTrack = new Map<TrackType, PublishOptions>();
63
55
 
64
56
  /**
65
57
  * An array maintaining the order how transceivers were added to the peer connection.
@@ -69,52 +61,18 @@ export class Publisher {
69
61
  * @internal
70
62
  */
71
63
  private readonly transceiverInitOrder: TrackType[] = [];
72
-
73
- private readonly trackKindMapping: {
74
- [key in TrackType]: 'video' | 'audio' | undefined;
75
- } = {
76
- [TrackType.AUDIO]: 'audio',
77
- [TrackType.VIDEO]: 'video',
78
- [TrackType.SCREEN_SHARE]: 'video',
79
- [TrackType.SCREEN_SHARE_AUDIO]: 'audio',
80
- [TrackType.UNSPECIFIED]: undefined,
81
- };
82
-
83
- private readonly trackLayersCache: {
84
- [key in TrackType]: OptimalVideoLayer[] | undefined;
85
- } = {
86
- [TrackType.AUDIO]: undefined,
87
- [TrackType.VIDEO]: undefined,
88
- [TrackType.SCREEN_SHARE]: undefined,
89
- [TrackType.SCREEN_SHARE_AUDIO]: undefined,
90
- [TrackType.UNSPECIFIED]: undefined,
91
- };
92
-
93
64
  private readonly isDtxEnabled: boolean;
94
65
  private readonly isRedEnabled: boolean;
95
66
 
96
67
  private readonly unsubscribeOnIceRestart: () => void;
68
+ private readonly unsubscribeChangePublishQuality: () => void;
97
69
  private readonly onUnrecoverableError?: () => void;
98
70
 
99
71
  private isIceRestarting = false;
100
-
101
- /**
102
- * The SFU client instance to use for publishing and signaling.
103
- */
104
- sfuClient: StreamSfuClient;
72
+ private sfuClient: StreamSfuClient;
105
73
 
106
74
  /**
107
75
  * Constructs a new `Publisher` instance.
108
- *
109
- * @param connectionConfig the connection configuration to use.
110
- * @param sfuClient the SFU client to use.
111
- * @param state the call state to use.
112
- * @param dispatcher the dispatcher to use.
113
- * @param isDtxEnabled whether DTX is enabled.
114
- * @param isRedEnabled whether RED is enabled.
115
- * @param iceRestartDelay the delay in milliseconds to wait before restarting ICE once connection goes to `disconnected` state.
116
- * @param onUnrecoverableError a callback to call when an unrecoverable error occurs.
117
- * @param logTag the log tag to use.
118
76
  */
119
77
  constructor({
120
78
  connectionConfig,
@@ -141,6 +99,21 @@ export class Publisher {
141
99
  this.onUnrecoverableError?.();
142
100
  });
143
101
  });
102
+
103
+ this.unsubscribeChangePublishQuality = dispatcher.on(
104
+ 'changePublishQuality',
105
+ ({ videoSenders }) => {
106
+ withoutConcurrency('publisher.changePublishQuality', async () => {
107
+ for (const videoSender of videoSenders) {
108
+ const { layers } = videoSender;
109
+ const enabledLayers = layers.filter((l) => l.active);
110
+ await this.changePublishQuality(enabledLayers);
111
+ }
112
+ }).catch((err) => {
113
+ this.logger('warn', 'Failed to change publish quality', err);
114
+ });
115
+ },
116
+ );
144
117
  }
145
118
 
146
119
  private createPeerConnection = (connectionConfig?: RTCConfiguration) => {
@@ -167,14 +140,8 @@ export class Publisher {
167
140
  close = ({ stopTracks }: { stopTracks: boolean }) => {
168
141
  if (stopTracks) {
169
142
  this.stopPublishing();
170
- Object.keys(this.transceiverRegistry).forEach((trackType) => {
171
- // @ts-ignore
172
- this.transceiverRegistry[trackType] = undefined;
173
- });
174
- Object.keys(this.trackLayersCache).forEach((trackType) => {
175
- // @ts-ignore
176
- this.trackLayersCache[trackType] = undefined;
177
- });
143
+ this.transceiverCache.clear();
144
+ this.trackLayersCache.clear();
178
145
  }
179
146
 
180
147
  this.detachEventHandlers();
@@ -188,6 +155,7 @@ export class Publisher {
188
155
  */
189
156
  detachEventHandlers = () => {
190
157
  this.unsubscribeOnIceRestart();
158
+ this.unsubscribeChangePublishQuality();
191
159
 
192
160
  this.pc.removeEventListener('icecandidate', this.onIceCandidate);
193
161
  this.pc.removeEventListener('negotiationneeded', this.onNegotiationNeeded);
@@ -227,106 +195,97 @@ export class Publisher {
227
195
  throw new Error(`Can't publish a track that has ended already.`);
228
196
  }
229
197
 
230
- let transceiver = this.pc
231
- .getTransceivers()
232
- .find(
233
- (t) =>
234
- t === this.transceiverRegistry[trackType] &&
235
- t.sender.track &&
236
- t.sender.track?.kind === this.trackKindMapping[trackType],
237
- );
238
-
239
- /**
240
- * An event handler which listens for the 'ended' event on the track.
241
- * Once the track has ended, it will notify the SFU and update the state.
242
- */
243
- const handleTrackEnded = () => {
244
- this.logger(
245
- 'info',
246
- `Track ${TrackType[trackType]} has ended abruptly, notifying the SFU`,
247
- );
248
- // cleanup, this event listener needs to run only once.
249
- track.removeEventListener('ended', handleTrackEnded);
250
- this.notifyTrackMuteStateChanged(mediaStream, trackType, true).catch(
251
- (err) => this.logger('warn', `Couldn't notify track mute state`, err),
252
- );
253
- };
254
-
255
- if (!transceiver) {
256
- const { settings } = this.state;
257
- const targetResolution = settings?.video
258
- .target_resolution as TargetResolutionResponse;
259
- const screenShareBitrate =
260
- settings?.screensharing.target_resolution?.bitrate;
261
-
262
- const videoEncodings =
263
- trackType === TrackType.VIDEO
264
- ? findOptimalVideoLayers(track, targetResolution, opts)
265
- : trackType === TrackType.SCREEN_SHARE
266
- ? findOptimalScreenSharingLayers(track, opts, screenShareBitrate)
267
- : undefined;
198
+ // enable the track if it is disabled
199
+ if (!track.enabled) track.enabled = true;
268
200
 
201
+ const transceiver = this.transceiverCache.get(trackType);
202
+ if (!transceiver || !transceiver.sender.track) {
269
203
  // listen for 'ended' event on the track as it might be ended abruptly
270
- // by an external factor as permission revokes, device disconnected, etc.
204
+ // by an external factors such as permission revokes, a disconnected device, etc.
271
205
  // keep in mind that `track.stop()` doesn't trigger this event.
272
- track.addEventListener('ended', handleTrackEnded);
273
- if (!track.enabled) {
274
- track.enabled = true;
275
- }
276
-
277
- transceiver = this.pc.addTransceiver(track, {
278
- direction: 'sendonly',
279
- streams:
280
- trackType === TrackType.VIDEO || trackType === TrackType.SCREEN_SHARE
281
- ? [mediaStream]
282
- : undefined,
283
- sendEncodings: videoEncodings,
284
- });
285
-
286
- this.logger('debug', `Added ${TrackType[trackType]} transceiver`);
287
- this.transceiverInitOrder.push(trackType);
288
- this.transceiverRegistry[trackType] = transceiver;
289
- this.publishOptionsPerTrackType.set(trackType, opts);
290
-
291
- const { preferredCodec } = opts;
292
- const codec =
293
- isReactNative() && trackType === TrackType.VIDEO && !preferredCodec
294
- ? getRNOptimalCodec()
295
- : preferredCodec;
296
-
297
- const codecPreferences =
298
- 'setCodecPreferences' in transceiver
299
- ? this.getCodecPreferences(trackType, codec)
300
- : undefined;
301
- if (codecPreferences) {
302
- this.logger(
303
- 'info',
304
- `Setting ${TrackType[trackType]} codec preferences`,
305
- codecPreferences,
206
+ const handleTrackEnded = () => {
207
+ this.logger('info', `Track ${TrackType[trackType]} has ended abruptly`);
208
+ track.removeEventListener('ended', handleTrackEnded);
209
+ this.notifyTrackMuteStateChanged(mediaStream, trackType, true).catch(
210
+ (err) => this.logger('warn', `Couldn't notify track mute state`, err),
306
211
  );
307
- try {
308
- transceiver.setCodecPreferences(codecPreferences);
309
- } catch (err) {
310
- this.logger('warn', `Couldn't set codec preferences`, err);
311
- }
312
- }
212
+ };
213
+ track.addEventListener('ended', handleTrackEnded);
214
+ this.addTransceiver(trackType, track, opts, mediaStream);
313
215
  } else {
314
- const previousTrack = transceiver.sender.track;
315
- // don't stop the track if we are re-publishing the same track
316
- if (previousTrack && previousTrack !== track) {
317
- previousTrack.stop();
318
- previousTrack.removeEventListener('ended', handleTrackEnded);
319
- track.addEventListener('ended', handleTrackEnded);
320
- }
321
- if (!track.enabled) {
322
- track.enabled = true;
323
- }
324
- await transceiver.sender.replaceTrack(track);
216
+ await this.updateTransceiver(transceiver, track);
325
217
  }
326
218
 
327
219
  await this.notifyTrackMuteStateChanged(mediaStream, trackType, false);
328
220
  };
329
221
 
222
+ /**
223
+ * Adds a new transceiver to the peer connection.
224
+ * This needs to be called when a new track kind is added to the peer connection.
225
+ * In other cases, use `updateTransceiver` method.
226
+ */
227
+ private addTransceiver = (
228
+ trackType: TrackType,
229
+ track: MediaStreamTrack,
230
+ opts: PublishOptions,
231
+ mediaStream: MediaStream,
232
+ ) => {
233
+ const { forceCodec, preferredCodec } = opts;
234
+ const codecInUse = forceCodec || getOptimalVideoCodec(preferredCodec);
235
+ const videoEncodings = this.computeLayers(trackType, track, opts);
236
+ const transceiver = this.pc.addTransceiver(track, {
237
+ direction: 'sendonly',
238
+ streams:
239
+ trackType === TrackType.VIDEO || trackType === TrackType.SCREEN_SHARE
240
+ ? [mediaStream]
241
+ : undefined,
242
+ sendEncodings: isSvcCodec(codecInUse)
243
+ ? toSvcEncodings(videoEncodings)
244
+ : videoEncodings,
245
+ });
246
+
247
+ this.logger('debug', `Added ${TrackType[trackType]} transceiver`);
248
+ this.transceiverInitOrder.push(trackType);
249
+ this.transceiverCache.set(trackType, transceiver);
250
+ this.publishOptsForTrack.set(trackType, opts);
251
+
252
+ // handle codec preferences
253
+ if (!('setCodecPreferences' in transceiver)) return;
254
+
255
+ const codecPreferences = this.getCodecPreferences(
256
+ trackType,
257
+ trackType === TrackType.VIDEO ? codecInUse : undefined,
258
+ );
259
+ if (!codecPreferences) return;
260
+
261
+ try {
262
+ this.logger(
263
+ 'info',
264
+ `Setting ${TrackType[trackType]} codec preferences`,
265
+ codecPreferences,
266
+ );
267
+ transceiver.setCodecPreferences(codecPreferences);
268
+ } catch (err) {
269
+ this.logger('warn', `Couldn't set codec preferences`, err);
270
+ }
271
+ };
272
+
273
+ /**
274
+ * Updates the given transceiver with the new track.
275
+ * Stops the previous track and replaces it with the new one.
276
+ */
277
+ private updateTransceiver = async (
278
+ transceiver: RTCRtpTransceiver,
279
+ track: MediaStreamTrack,
280
+ ) => {
281
+ const previousTrack = transceiver.sender.track;
282
+ // don't stop the track if we are re-publishing the same track
283
+ if (previousTrack && previousTrack !== track) {
284
+ previousTrack.stop();
285
+ }
286
+ await transceiver.sender.replaceTrack(track);
287
+ };
288
+
330
289
  /**
331
290
  * Stops publishing the given track type to the SFU, if it is currently being published.
332
291
  * Underlying track will be stopped and removed from the publisher.
@@ -334,9 +293,7 @@ export class Publisher {
334
293
  * @param stopTrack specifies whether track should be stopped or just disabled
335
294
  */
336
295
  unpublishStream = async (trackType: TrackType, stopTrack: boolean) => {
337
- const transceiver = this.pc
338
- .getTransceivers()
339
- .find((t) => t === this.transceiverRegistry[trackType] && t.sender.track);
296
+ const transceiver = this.transceiverCache.get(trackType);
340
297
  if (
341
298
  transceiver &&
342
299
  transceiver.sender.track &&
@@ -360,7 +317,7 @@ export class Publisher {
360
317
  * @param trackType the track type to check.
361
318
  */
362
319
  isPublishing = (trackType: TrackType): boolean => {
363
- const transceiver = this.transceiverRegistry[trackType];
320
+ const transceiver = this.transceiverCache.get(trackType);
364
321
  if (!transceiver || !transceiver.sender) return false;
365
322
  const track = transceiver.sender.track;
366
323
  return !!track && track.readyState === 'live' && track.enabled;
@@ -406,14 +363,14 @@ export class Publisher {
406
363
  });
407
364
  };
408
365
 
409
- updateVideoPublishQuality = async (enabledLayers: VideoLayerSetting[]) => {
366
+ private changePublishQuality = async (enabledLayers: VideoLayerSetting[]) => {
410
367
  this.logger(
411
368
  'info',
412
369
  'Update publish quality, requested layers by SFU:',
413
370
  enabledLayers,
414
371
  );
415
372
 
416
- const videoSender = this.transceiverRegistry[TrackType.VIDEO]?.sender;
373
+ const videoSender = this.transceiverCache.get(TrackType.VIDEO)?.sender;
417
374
  if (!videoSender) {
418
375
  this.logger('warn', 'Update publish quality, no video sender found.');
419
376
  return;
@@ -428,79 +385,64 @@ export class Publisher {
428
385
  return;
429
386
  }
430
387
 
388
+ const [codecInUse] = params.codecs;
389
+ const usesSvcCodec = codecInUse && isSvcCodec(codecInUse.mimeType);
390
+
431
391
  let changed = false;
432
- let enabledRids = enabledLayers
433
- .filter((ly) => ly.active)
434
- .map((ly) => ly.name);
435
- params.encodings.forEach((enc) => {
392
+ for (const encoder of params.encodings) {
393
+ const layer = usesSvcCodec
394
+ ? // for SVC, we only have one layer (q) and often rid is omitted
395
+ enabledLayers[0]
396
+ : // for non-SVC, we need to find the layer by rid (simulcast)
397
+ enabledLayers.find((l) => l.name === encoder.rid);
398
+
436
399
  // flip 'active' flag only when necessary
437
- const shouldEnable = enabledRids.includes(enc.rid!);
438
- if (shouldEnable !== enc.active) {
439
- enc.active = shouldEnable;
400
+ const shouldActivate = !!layer?.active;
401
+ if (shouldActivate !== encoder.active) {
402
+ encoder.active = shouldActivate;
440
403
  changed = true;
441
404
  }
442
- if (shouldEnable) {
443
- let layer = enabledLayers.find((vls) => vls.name === enc.rid);
444
- if (layer !== undefined) {
445
- if (
446
- layer.scaleResolutionDownBy >= 1 &&
447
- layer.scaleResolutionDownBy !== enc.scaleResolutionDownBy
448
- ) {
449
- this.logger(
450
- 'debug',
451
- '[dynascale]: setting scaleResolutionDownBy from server',
452
- 'layer',
453
- layer.name,
454
- 'scale-resolution-down-by',
455
- layer.scaleResolutionDownBy,
456
- );
457
- enc.scaleResolutionDownBy = layer.scaleResolutionDownBy;
458
- changed = true;
459
- }
460
405
 
461
- if (layer.maxBitrate > 0 && layer.maxBitrate !== enc.maxBitrate) {
462
- this.logger(
463
- 'debug',
464
- '[dynascale] setting max-bitrate from the server',
465
- 'layer',
466
- layer.name,
467
- 'max-bitrate',
468
- layer.maxBitrate,
469
- );
470
- enc.maxBitrate = layer.maxBitrate;
471
- changed = true;
472
- }
473
-
474
- if (
475
- layer.maxFramerate > 0 &&
476
- layer.maxFramerate !== enc.maxFramerate
477
- ) {
478
- this.logger(
479
- 'debug',
480
- '[dynascale]: setting maxFramerate from server',
481
- 'layer',
482
- layer.name,
483
- 'max-framerate',
484
- layer.maxFramerate,
485
- );
486
- enc.maxFramerate = layer.maxFramerate;
487
- changed = true;
488
- }
489
- }
406
+ // skip the rest of the settings if the layer is disabled or not found
407
+ if (!layer) continue;
408
+
409
+ const {
410
+ maxFramerate,
411
+ scaleResolutionDownBy,
412
+ maxBitrate,
413
+ scalabilityMode,
414
+ } = layer;
415
+ if (
416
+ scaleResolutionDownBy >= 1 &&
417
+ scaleResolutionDownBy !== encoder.scaleResolutionDownBy
418
+ ) {
419
+ encoder.scaleResolutionDownBy = scaleResolutionDownBy;
420
+ changed = true;
490
421
  }
491
- });
422
+ if (maxBitrate > 0 && maxBitrate !== encoder.maxBitrate) {
423
+ encoder.maxBitrate = maxBitrate;
424
+ changed = true;
425
+ }
426
+ if (maxFramerate > 0 && maxFramerate !== encoder.maxFramerate) {
427
+ encoder.maxFramerate = maxFramerate;
428
+ changed = true;
429
+ }
430
+ // @ts-expect-error scalabilityMode is not in the typedefs yet
431
+ if (scalabilityMode && scalabilityMode !== encoder.scalabilityMode) {
432
+ // @ts-expect-error scalabilityMode is not in the typedefs yet
433
+ encoder.scalabilityMode = scalabilityMode;
434
+ changed = true;
435
+ }
436
+ }
492
437
 
493
438
  const activeLayers = params.encodings.filter((e) => e.active);
494
- if (changed) {
495
- await videoSender.setParameters(params);
496
- this.logger(
497
- 'info',
498
- `Update publish quality, enabled rids: `,
499
- activeLayers,
500
- );
501
- } else {
502
- this.logger('info', `Update publish quality, no change: `, activeLayers);
439
+ if (!changed) {
440
+ this.logger('info', `Update publish quality, no change:`, activeLayers);
441
+ return;
503
442
  }
443
+
444
+ await videoSender.setParameters(params);
445
+ this.logger('info', `Update publish quality, enabled rids:`, activeLayers);
504
446
  };
505
447
 
506
448
  /**
@@ -514,7 +456,7 @@ export class Publisher {
514
456
 
515
457
  private getCodecPreferences = (
516
458
  trackType: TrackType,
517
- preferredCodec?: string | null,
459
+ preferredCodec?: string,
518
460
  ) => {
519
461
  if (trackType === TrackType.VIDEO) {
520
462
  return getPreferredCodecs('video', preferredCodec || 'vp8');
@@ -582,23 +524,22 @@ export class Publisher {
582
524
  */
583
525
  private negotiate = async (options?: RTCOfferOptions) => {
584
526
  const offer = await this.pc.createOffer(options);
585
- let sdp = this.mungeCodecs(offer.sdp);
586
- if (sdp && this.isPublishing(TrackType.SCREEN_SHARE_AUDIO)) {
587
- sdp = this.enableHighQualityAudio(sdp);
527
+ if (offer.sdp) {
528
+ offer.sdp = toggleDtx(offer.sdp, this.isDtxEnabled);
529
+ if (this.isPublishing(TrackType.SCREEN_SHARE_AUDIO)) {
530
+ offer.sdp = this.enableHighQualityAudio(offer.sdp);
531
+ }
588
532
  }
589
533
 
590
- // set the munged SDP back to the offer
591
- offer.sdp = sdp;
592
-
593
534
  const trackInfos = this.getAnnouncedTracks(offer.sdp);
594
535
  if (trackInfos.length === 0) {
595
536
  throw new Error(`Can't negotiate without announcing any tracks`);
596
537
  }
597
538
 
598
- this.isIceRestarting = options?.iceRestart ?? false;
599
- await this.pc.setLocalDescription(offer);
600
-
601
539
  try {
540
+ this.isIceRestarting = options?.iceRestart ?? false;
541
+ await this.pc.setLocalDescription(offer);
542
+
602
543
  const { response } = await this.sfuClient.setPublisher({
603
544
  sdp: offer.sdp || '',
604
545
  tracks: trackInfos,
@@ -623,61 +564,14 @@ export class Publisher {
623
564
  };
624
565
 
625
566
  private enableHighQualityAudio = (sdp: string) => {
626
- const transceiver = this.transceiverRegistry[TrackType.SCREEN_SHARE_AUDIO];
567
+ const transceiver = this.transceiverCache.get(TrackType.SCREEN_SHARE_AUDIO);
627
568
  if (!transceiver) return sdp;
628
569
 
629
- const mid = this.extractMid(transceiver, sdp, TrackType.SCREEN_SHARE_AUDIO);
630
- return enableHighQualityAudio(sdp, mid);
631
- };
632
-
633
- private mungeCodecs = (sdp?: string) => {
634
- if (sdp) {
635
- sdp = toggleDtx(sdp, this.isDtxEnabled);
636
- }
637
- return sdp;
638
- };
639
-
640
- private extractMid = (
641
- transceiver: RTCRtpTransceiver,
642
- sdp: string | undefined,
643
- trackType: TrackType,
644
- ): string => {
645
- if (transceiver.mid) return transceiver.mid;
646
-
647
- if (!sdp) {
648
- this.logger('warn', 'No SDP found. Returning empty mid');
649
- return '';
650
- }
651
-
652
- this.logger(
653
- 'debug',
654
- `No 'mid' found for track. Trying to find it from the Offer SDP`,
570
+ const transceiverInitIndex = this.transceiverInitOrder.indexOf(
571
+ TrackType.SCREEN_SHARE_AUDIO,
655
572
  );
656
-
657
- const track = transceiver.sender.track!;
658
- const parsedSdp = SDP.parse(sdp);
659
- const media = parsedSdp.media.find((m) => {
660
- return (
661
- m.type === track.kind &&
662
- // if `msid` is not present, we assume that the track is the first one
663
- (m.msid?.includes(track.id) ?? true)
664
- );
665
- });
666
- if (typeof media?.mid === 'undefined') {
667
- this.logger(
668
- 'debug',
669
- `No mid found in SDP for track type ${track.kind} and id ${track.id}. Attempting to find it heuristically`,
670
- );
671
-
672
- const heuristicMid = this.transceiverInitOrder.indexOf(trackType);
673
- if (heuristicMid !== -1) {
674
- return String(heuristicMid);
675
- }
676
-
677
- this.logger('debug', 'No heuristic mid found. Returning empty mid');
678
- return '';
679
- }
680
- return String(media.mid);
573
+ const mid = extractMid(transceiver, transceiverInitIndex, sdp);
574
+ return enableHighQualityAudio(sdp, mid);
681
575
  };
682
576
 
683
577
  /**
@@ -688,35 +582,23 @@ export class Publisher {
688
582
  */
689
583
  getAnnouncedTracks = (sdp?: string): TrackInfo[] => {
690
584
  sdp = sdp || this.pc.localDescription?.sdp;
691
-
692
- const { settings } = this.state;
693
- const targetResolution = settings?.video
694
- .target_resolution as TargetResolutionResponse;
695
585
  return this.pc
696
586
  .getTransceivers()
697
587
  .filter((t) => t.direction === 'sendonly' && t.sender.track)
698
588
  .map<TrackInfo>((transceiver) => {
699
- const trackType: TrackType = Number(
700
- Object.keys(this.transceiverRegistry).find(
701
- (key) =>
702
- this.transceiverRegistry[key as any as TrackType] === transceiver,
703
- ),
704
- );
589
+ let trackType!: TrackType;
590
+ this.transceiverCache.forEach((value, key) => {
591
+ if (value === transceiver) trackType = key;
592
+ });
705
593
  const track = transceiver.sender.track!;
706
594
  let optimalLayers: OptimalVideoLayer[];
707
595
  const isTrackLive = track.readyState === 'live';
708
596
  if (isTrackLive) {
709
- const publishOpts = this.publishOptionsPerTrackType.get(trackType);
710
- optimalLayers =
711
- trackType === TrackType.VIDEO
712
- ? findOptimalVideoLayers(track, targetResolution, publishOpts)
713
- : trackType === TrackType.SCREEN_SHARE
714
- ? findOptimalScreenSharingLayers(track, publishOpts)
715
- : [];
716
- this.trackLayersCache[trackType] = optimalLayers;
597
+ optimalLayers = this.computeLayers(trackType, track) || [];
598
+ this.trackLayersCache.set(trackType, optimalLayers);
717
599
  } else {
718
600
  // we report the last known optimal layers for ended tracks
719
- optimalLayers = this.trackLayersCache[trackType] || [];
601
+ optimalLayers = this.trackLayersCache.get(trackType) || [];
720
602
  this.logger(
721
603
  'debug',
722
604
  `Track ${TrackType[trackType]} is ended. Announcing last known optimal layers`,
@@ -728,7 +610,7 @@ export class Publisher {
728
610
  rid: optimalLayer.rid || '',
729
611
  bitrate: optimalLayer.maxBitrate || 0,
730
612
  fps: optimalLayer.maxFramerate || 0,
731
- quality: this.ridToVideoQuality(optimalLayer.rid || ''),
613
+ quality: ridToVideoQuality(optimalLayer.rid || ''),
732
614
  videoDimension: {
733
615
  width: optimalLayer.width,
734
616
  height: optimalLayer.height,
@@ -742,13 +624,13 @@ export class Publisher {
742
624
 
743
625
  const trackSettings = track.getSettings();
744
626
  const isStereo = isAudioTrack && trackSettings.channelCount === 2;
745
-
627
+ const transceiverInitIndex =
628
+ this.transceiverInitOrder.indexOf(trackType);
746
629
  return {
747
630
  trackId: track.id,
748
631
  layers: layers,
749
632
  trackType,
750
- mid: this.extractMid(transceiver, sdp, trackType),
751
-
633
+ mid: extractMid(transceiver, transceiverInitIndex, sdp),
752
634
  stereo: isStereo,
753
635
  dtx: isAudioTrack && this.isDtxEnabled,
754
636
  red: isAudioTrack && this.isRedEnabled,
@@ -757,6 +639,26 @@ export class Publisher {
757
639
  });
758
640
  };
759
641
 
642
+ private computeLayers = (
643
+ trackType: TrackType,
644
+ track: MediaStreamTrack,
645
+ opts?: PublishOptions,
646
+ ): OptimalVideoLayer[] | undefined => {
647
+ const { settings } = this.state;
648
+ const targetResolution = settings?.video
649
+ .target_resolution as TargetResolutionResponse;
650
+ const screenShareBitrate =
651
+ settings?.screensharing.target_resolution?.bitrate;
652
+
653
+ const publishOpts = opts || this.publishOptsForTrack.get(trackType);
654
+ const codecInUse = getOptimalVideoCodec(publishOpts?.preferredCodec);
655
+ return trackType === TrackType.VIDEO
656
+ ? findOptimalVideoLayers(track, targetResolution, codecInUse, publishOpts)
657
+ : trackType === TrackType.SCREEN_SHARE
658
+ ? findOptimalScreenSharingLayers(track, publishOpts, screenShareBitrate)
659
+ : undefined;
660
+ };
661
+
760
662
  private onIceCandidateError = (e: Event) => {
761
663
  const errorMessage =
762
664
  e instanceof RTCPeerConnectionIceErrorEvent &&
@@ -789,12 +691,4 @@ export class Publisher {
789
691
  private onSignalingStateChange = () => {
790
692
  this.logger('debug', `Signaling state changed`, this.pc.signalingState);
791
693
  };
792
-
793
- private ridToVideoQuality = (rid: string): VideoQuality => {
794
- return rid === 'q'
795
- ? VideoQuality.LOW_UNSPECIFIED
796
- : rid === 'h'
797
- ? VideoQuality.MID
798
- : VideoQuality.HIGH; // default to HIGH
799
- };
800
694
  }