@stream-io/video-client 0.0.28 → 0.0.30

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 (67) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/index.browser.es.js +2512 -1754
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +2532 -1752
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +2512 -1754
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +2 -3
  9. package/dist/src/StreamSfuClient.d.ts +23 -10
  10. package/dist/src/StreamVideoClient.d.ts +1 -4
  11. package/dist/src/client-details.d.ts +2 -1
  12. package/dist/src/coordinator/connection/types.d.ts +2 -2
  13. package/dist/src/coordinator/connection/utils.d.ts +1 -0
  14. package/dist/src/events/internal.d.ts +4 -0
  15. package/dist/src/gen/coordinator/index.d.ts +6 -0
  16. package/dist/src/gen/google/protobuf/struct.d.ts +8 -15
  17. package/dist/src/gen/google/protobuf/timestamp.d.ts +2 -9
  18. package/dist/src/gen/video/sfu/event/events.d.ts +121 -1
  19. package/dist/src/gen/video/sfu/models/models.d.ts +38 -1
  20. package/dist/src/gen/video/sfu/signal_rpc/signal.client.d.ts +3 -14
  21. package/dist/src/gen/video/sfu/signal_rpc/signal.d.ts +4 -12
  22. package/dist/src/logger.d.ts +4 -2
  23. package/dist/src/rtc/Dispatcher.d.ts +1 -2
  24. package/dist/src/rtc/{publisher.d.ts → Publisher.d.ts} +49 -15
  25. package/dist/src/rtc/Subscriber.d.ts +58 -0
  26. package/dist/src/rtc/__tests__/Subscriber.test.d.ts +1 -0
  27. package/dist/src/rtc/flows/join.d.ts +8 -1
  28. package/dist/src/rtc/index.d.ts +2 -2
  29. package/dist/src/rtc/signal.d.ts +1 -0
  30. package/dist/src/stats/state-store-stats-reporter.d.ts +3 -4
  31. package/dist/src/store/CallState.d.ts +10 -0
  32. package/package.json +3 -1
  33. package/src/Call.ts +215 -209
  34. package/src/StreamSfuClient.ts +48 -21
  35. package/src/StreamVideoClient.ts +7 -24
  36. package/src/client-details.ts +33 -1
  37. package/src/coordinator/connection/client.ts +6 -8
  38. package/src/coordinator/connection/types.ts +2 -3
  39. package/src/coordinator/connection/utils.ts +1 -0
  40. package/src/events/call.ts +0 -1
  41. package/src/events/callEventHandlers.ts +2 -0
  42. package/src/events/internal.ts +20 -0
  43. package/src/events/sessions.ts +0 -1
  44. package/src/gen/coordinator/index.ts +6 -0
  45. package/src/gen/google/protobuf/struct.ts +541 -333
  46. package/src/gen/google/protobuf/timestamp.ts +214 -148
  47. package/src/gen/video/sfu/event/events.ts +353 -3
  48. package/src/gen/video/sfu/models/models.ts +37 -0
  49. package/src/gen/video/sfu/signal_rpc/signal.client.ts +160 -94
  50. package/src/gen/video/sfu/signal_rpc/signal.ts +1214 -731
  51. package/src/logger.ts +43 -30
  52. package/src/rtc/Dispatcher.ts +5 -9
  53. package/src/rtc/{publisher.ts → Publisher.ts} +245 -111
  54. package/src/rtc/Subscriber.ts +304 -0
  55. package/src/rtc/__tests__/{publisher.test.ts → Publisher.test.ts} +77 -9
  56. package/src/rtc/__tests__/Subscriber.test.ts +121 -0
  57. package/src/rtc/__tests__/mocks/webrtc.mocks.ts +20 -0
  58. package/src/rtc/flows/join.ts +42 -1
  59. package/src/rtc/index.ts +2 -2
  60. package/src/rtc/signal.ts +6 -5
  61. package/src/rtc/videoLayers.ts +1 -4
  62. package/src/stats/state-store-stats-reporter.ts +3 -5
  63. package/src/store/CallState.ts +20 -0
  64. package/src/types.ts +0 -1
  65. package/dist/src/rtc/subscriber.d.ts +0 -9
  66. package/src/rtc/subscriber.ts +0 -107
  67. /package/dist/src/rtc/__tests__/{publisher.test.d.ts → Publisher.test.d.ts} +0 -0
package/src/logger.ts CHANGED
@@ -1,45 +1,58 @@
1
- import { LogLevel, Logger } from './coordinator/connection/types';
1
+ import { Logger, LogLevel } from './coordinator/connection/types';
2
+
3
+ // log levels, sorted by verbosity
4
+ export const logLevels: Record<LogLevel, number> = Object.freeze({
5
+ trace: 0,
6
+ debug: 1,
7
+ info: 2,
8
+ warn: 3,
9
+ error: 4,
10
+ });
2
11
 
3
12
  let logger: Logger | undefined;
13
+ let level: LogLevel = 'info';
4
14
 
5
- export const logToConsole: Logger = (
6
- logLevel: LogLevel,
7
- message: string,
8
- extraData?: Record<string, unknown>,
9
- tags?: string[],
10
- ) => {
15
+ export const logToConsole: Logger = (logLevel, message, ...args) => {
11
16
  let logMethod;
12
- if (logLevel === 'error') {
13
- logMethod = console.error;
14
- } else if (logLevel === 'warn') {
15
- logMethod = console.warn;
16
- } else {
17
- logMethod = console.log;
17
+ switch (logLevel) {
18
+ case 'error':
19
+ logMethod = console.error;
20
+ break;
21
+ case 'warn':
22
+ logMethod = console.warn;
23
+ break;
24
+ case 'info':
25
+ logMethod = console.info;
26
+ break;
27
+ case 'trace':
28
+ logMethod = console.trace;
29
+ break;
30
+ default:
31
+ logMethod = console.log;
32
+ break;
18
33
  }
19
34
 
20
- logMethod(
21
- logLevel,
22
- `${tags?.join(':')} - ${message}`,
23
- extraData ? extraData : '',
24
- );
35
+ logMethod(message, ...args);
25
36
  };
26
37
 
27
- export const setLogger = (l: Logger) => {
38
+ export const setLogger = (l: Logger, lvl?: LogLevel) => {
28
39
  logger = l;
40
+ if (lvl) {
41
+ setLogLevel(lvl);
42
+ }
43
+ };
44
+
45
+ export const setLogLevel = (l: LogLevel) => {
46
+ level = l;
29
47
  };
30
48
 
31
49
  export const getLogger = (withTags?: string[]) => {
32
- const loggerMethod = logger || (() => {});
33
- const result: Logger = (
34
- logLevel: LogLevel,
35
- messeage: string,
36
- extraData?: Record<string, unknown>,
37
- tags?: string[],
38
- ) => {
39
- loggerMethod(logLevel, messeage, extraData, [
40
- ...(tags || []),
41
- ...(withTags || []),
42
- ]);
50
+ const loggerMethod = logger || logToConsole;
51
+ const tags = (withTags || []).join(':');
52
+ const result: Logger = (logLevel, message, ...args) => {
53
+ if (logLevels[logLevel] >= logLevels[level]) {
54
+ loggerMethod(logLevel, `[${tags}]: ${message}`, ...args);
55
+ }
43
56
  };
44
57
  return result;
45
58
  };
@@ -20,6 +20,7 @@ const sfuEventKinds: { [key in SfuEventKinds]: undefined } = {
20
20
  trackUnpublished: undefined,
21
21
  error: undefined,
22
22
  callGrantsUpdated: undefined,
23
+ goAway: undefined,
23
24
  };
24
25
 
25
26
  export const isSfuEvent = (
@@ -34,19 +35,14 @@ export class Dispatcher {
34
35
  private subscribers: {
35
36
  [eventName: string]: SfuEventListener[] | undefined;
36
37
  } = {};
37
- private logger?: Logger;
38
-
39
- constructor() {
40
- this.logger = getLogger(['sfu-client']);
41
- }
38
+ private readonly logger: Logger = getLogger(['sfu-client']);
42
39
 
43
40
  dispatch = (message: SfuEvent) => {
44
41
  const eventKind = message.eventPayload.oneofKind;
45
42
  if (eventKind) {
46
- this.logger?.('info', `Dispatching ${eventKind}`);
47
- this.logger?.(
43
+ this.logger(
48
44
  'debug',
49
- `Event payload`,
45
+ `Dispatching ${eventKind}`,
50
46
  (message.eventPayload as any)[eventKind],
51
47
  );
52
48
  const listeners = this.subscribers[eventKind];
@@ -54,7 +50,7 @@ export class Dispatcher {
54
50
  try {
55
51
  fn(message);
56
52
  } catch (e) {
57
- this.logger?.('warn', 'Listener failed with error', e);
53
+ this.logger('warn', 'Listener failed with error', e);
58
54
  }
59
55
  });
60
56
  }
@@ -1,3 +1,4 @@
1
+ import * as SDP from 'sdp-transform';
1
2
  import { StreamSfuClient } from '../StreamSfuClient';
2
3
  import {
3
4
  PeerType,
@@ -10,6 +11,7 @@ import { getIceCandidate } from './helpers/iceCandidate';
10
11
  import {
11
12
  findOptimalScreenSharingLayers,
12
13
  findOptimalVideoLayers,
14
+ OptimalVideoLayer,
13
15
  } from './videoLayers';
14
16
  import { getPreferredCodecs } from './codecs';
15
17
  import {
@@ -25,6 +27,7 @@ import {
25
27
  toggleDtx,
26
28
  } from '../helpers/sdp-munging';
27
29
  import { Logger } from '../coordinator/connection/types';
30
+ import { getLogger } from '../logger';
28
31
 
29
32
  export type PublisherOpts = {
30
33
  sfuClient: StreamSfuClient;
@@ -40,9 +43,9 @@ export type PublisherOpts = {
40
43
  * @internal
41
44
  */
42
45
  export class Publisher {
43
- private readonly publisher: RTCPeerConnection;
44
- private readonly sfuClient: StreamSfuClient;
46
+ private pc: RTCPeerConnection;
45
47
  private readonly state: CallState;
48
+
46
49
  private readonly transceiverRegistry: {
47
50
  [key in TrackType]: RTCRtpTransceiver | undefined;
48
51
  } = {
@@ -52,7 +55,8 @@ export class Publisher {
52
55
  [TrackType.SCREEN_SHARE_AUDIO]: undefined,
53
56
  [TrackType.UNSPECIFIED]: undefined,
54
57
  };
55
- private readonly trackKindRegistry: {
58
+
59
+ private readonly trackKindMapping: {
56
60
  [key in TrackType]: 'video' | 'audio' | undefined;
57
61
  } = {
58
62
  [TrackType.AUDIO]: 'audio',
@@ -61,11 +65,37 @@ export class Publisher {
61
65
  [TrackType.SCREEN_SHARE_AUDIO]: undefined,
62
66
  [TrackType.UNSPECIFIED]: undefined,
63
67
  };
64
- private isDtxEnabled: boolean;
65
- private isRedEnabled: boolean;
66
- private preferredVideoCodec?: string;
67
- private logger?: Logger;
68
68
 
69
+ private readonly trackLayersCache: {
70
+ [key in TrackType]: OptimalVideoLayer[] | undefined;
71
+ } = {
72
+ [TrackType.AUDIO]: undefined,
73
+ [TrackType.VIDEO]: undefined,
74
+ [TrackType.SCREEN_SHARE]: undefined,
75
+ [TrackType.SCREEN_SHARE_AUDIO]: undefined,
76
+ [TrackType.UNSPECIFIED]: undefined,
77
+ };
78
+
79
+ private readonly isDtxEnabled: boolean;
80
+ private readonly isRedEnabled: boolean;
81
+ private readonly preferredVideoCodec?: string;
82
+ private logger: Logger = getLogger(['Publisher']);
83
+
84
+ /**
85
+ * The SFU client instance to use for publishing and signaling.
86
+ */
87
+ sfuClient: StreamSfuClient;
88
+
89
+ /**
90
+ * Constructs a new `Publisher` instance.
91
+ *
92
+ * @param connectionConfig the connection configuration to use.
93
+ * @param sfuClient the SFU client to use.
94
+ * @param state the call state to use.
95
+ * @param isDtxEnabled whether DTX is enabled.
96
+ * @param isRedEnabled whether RED is enabled.
97
+ * @param preferredVideoCodec the preferred video codec.
98
+ */
69
99
  constructor({
70
100
  connectionConfig,
71
101
  sfuClient,
@@ -74,6 +104,15 @@ export class Publisher {
74
104
  isRedEnabled,
75
105
  preferredVideoCodec,
76
106
  }: PublisherOpts) {
107
+ this.pc = this.createPeerConnection(connectionConfig);
108
+ this.sfuClient = sfuClient;
109
+ this.state = state;
110
+ this.isDtxEnabled = isDtxEnabled;
111
+ this.isRedEnabled = isRedEnabled;
112
+ this.preferredVideoCodec = preferredVideoCodec;
113
+ }
114
+
115
+ private createPeerConnection = (connectionConfig?: RTCConfiguration) => {
77
116
  const pc = new RTCPeerConnection(connectionConfig);
78
117
  pc.addEventListener('icecandidate', this.onIceCandidate);
79
118
  pc.addEventListener('negotiationneeded', this.onNegotiationNeeded);
@@ -87,14 +126,28 @@ export class Publisher {
87
126
  'icegatheringstatechange',
88
127
  this.onIceGatheringStateChange,
89
128
  );
129
+ pc.addEventListener('signalingstatechange', this.onSignalingStateChange);
130
+ return pc;
131
+ };
90
132
 
91
- this.publisher = pc;
92
- this.sfuClient = sfuClient;
93
- this.state = state;
94
- this.isDtxEnabled = isDtxEnabled;
95
- this.isRedEnabled = isRedEnabled;
96
- this.preferredVideoCodec = preferredVideoCodec;
97
- }
133
+ /**
134
+ * Closes the publisher PeerConnection and cleans up the resources.
135
+ */
136
+ close = ({ stopTracks = true } = {}) => {
137
+ if (stopTracks) {
138
+ this.stopPublishing();
139
+ Object.keys(this.transceiverRegistry).forEach((trackType) => {
140
+ // @ts-ignore
141
+ this.transceiverRegistry[trackType] = undefined;
142
+ });
143
+ Object.keys(this.trackLayersCache).forEach((trackType) => {
144
+ // @ts-ignore
145
+ this.trackLayersCache[trackType] = undefined;
146
+ });
147
+ }
148
+
149
+ this.pc.close();
150
+ };
98
151
 
99
152
  /**
100
153
  * Starts publishing the given track of the given media stream.
@@ -112,13 +165,17 @@ export class Publisher {
112
165
  trackType: TrackType,
113
166
  opts: PublishOptions = {},
114
167
  ) => {
115
- let transceiver = this.publisher
168
+ if (track.readyState === 'ended') {
169
+ throw new Error(`Can't publish a track that has ended already.`);
170
+ }
171
+
172
+ let transceiver = this.pc
116
173
  .getTransceivers()
117
174
  .find(
118
175
  (t) =>
119
176
  t === this.transceiverRegistry[trackType] &&
120
177
  t.sender.track &&
121
- t.sender.track?.kind === this.trackKindRegistry[trackType],
178
+ t.sender.track?.kind === this.trackKindMapping[trackType],
122
179
  );
123
180
 
124
181
  /**
@@ -126,7 +183,7 @@ export class Publisher {
126
183
  * Once the track has ended, it will notify the SFU and update the state.
127
184
  */
128
185
  const handleTrackEnded = async () => {
129
- this.logger?.(
186
+ this.logger(
130
187
  'info',
131
188
  `Track ${TrackType[trackType]} has ended, notifying the SFU`,
132
189
  );
@@ -158,7 +215,7 @@ export class Publisher {
158
215
  // keep in mind that `track.stop()` doesn't trigger this event.
159
216
  track.addEventListener('ended', handleTrackEnded);
160
217
 
161
- transceiver = this.publisher.addTransceiver(track, {
218
+ transceiver = this.pc.addTransceiver(track, {
162
219
  direction: 'sendonly',
163
220
  streams:
164
221
  trackType === TrackType.VIDEO || trackType === TrackType.SCREEN_SHARE
@@ -170,7 +227,7 @@ export class Publisher {
170
227
  this.transceiverRegistry[trackType] = transceiver;
171
228
 
172
229
  if ('setCodecPreferences' in transceiver && codecPreferences) {
173
- this.logger?.(
230
+ this.logger(
174
231
  'info',
175
232
  `Setting ${TrackType[trackType]} codec preferences`,
176
233
  codecPreferences,
@@ -202,7 +259,7 @@ export class Publisher {
202
259
  * @param trackType the track type to unpublish.
203
260
  */
204
261
  unpublishStream = async (trackType: TrackType) => {
205
- const transceiver = this.publisher
262
+ const transceiver = this.pc
206
263
  .getTransceivers()
207
264
  .find((t) => t === this.transceiverRegistry[trackType] && t.sender.track);
208
265
  if (
@@ -266,39 +323,39 @@ export class Publisher {
266
323
 
267
324
  /**
268
325
  * Stops publishing all tracks and stop all tracks.
269
- *
270
- * @param options - Options
271
- * @param options.stopTracks - If `true` (default), all tracks will be stopped.
272
326
  */
273
- stopPublishing = (
274
- options: {
275
- stopTracks?: boolean;
276
- } = {},
277
- ) => {
278
- const { stopTracks = true } = options;
279
- if (stopTracks) {
280
- this.publisher.getSenders().forEach((s) => {
281
- s.track?.stop();
282
-
283
- if (this.publisher.signalingState !== 'closed') {
284
- this.publisher.removeTrack(s);
285
- }
286
- });
287
- }
288
- this.publisher.close();
327
+ stopPublishing = () => {
328
+ this.logger('debug', 'Stopping publishing all tracks');
329
+ this.pc.getSenders().forEach((s) => {
330
+ s.track?.stop();
331
+ if (this.pc.signalingState !== 'closed') {
332
+ this.pc.removeTrack(s);
333
+ }
334
+ });
289
335
  };
290
336
 
291
337
  updateVideoPublishQuality = async (enabledRids: string[]) => {
292
- this.logger?.(
338
+ this.logger(
293
339
  'info',
294
340
  'Update publish quality, requested rids by SFU:',
295
341
  enabledRids,
296
342
  );
297
343
 
298
344
  const videoSender = this.transceiverRegistry[TrackType.VIDEO]?.sender;
299
- if (!videoSender) return;
345
+ if (!videoSender) {
346
+ this.logger('warn', 'Update publish quality, no video sender found.');
347
+ return;
348
+ }
300
349
 
301
350
  const params = videoSender.getParameters();
351
+ if (params.encodings.length === 0) {
352
+ this.logger(
353
+ 'warn',
354
+ 'Update publish quality, No suitable video encoding quality found',
355
+ );
356
+ return;
357
+ }
358
+
302
359
  let changed = false;
303
360
  params.encodings.forEach((enc) => {
304
361
  // flip 'active' flag only when necessary
@@ -308,18 +365,19 @@ export class Publisher {
308
365
  changed = true;
309
366
  }
310
367
  });
368
+
369
+ const activeRids = params.encodings
370
+ .filter((e) => e.active)
371
+ .map((e) => e.rid)
372
+ .join(', ');
311
373
  if (changed) {
312
- if (params.encodings.length === 0) {
313
- this.logger?.('warn', 'No suitable video encoding quality found');
314
- }
315
374
  await videoSender.setParameters(params);
316
- this.logger?.(
375
+ this.logger(
317
376
  'info',
318
- `Update publish quality, enabled rids: ${params.encodings
319
- .filter((e) => e.active)
320
- .map((e) => e.rid)
321
- .join(', ')}`,
377
+ `Update publish quality, enabled rids: ${activeRids}`,
322
378
  );
379
+ } else {
380
+ this.logger('info', `Update publish quality, no change: ${activeRids}`);
323
381
  }
324
382
  };
325
383
 
@@ -328,9 +386,9 @@ export class Publisher {
328
386
  * @param selector
329
387
  * @returns
330
388
  */
331
- getStats(selector?: MediaStreamTrack | null | undefined) {
332
- return this.publisher.getStats(selector);
333
- }
389
+ getStats = (selector?: MediaStreamTrack | null | undefined) => {
390
+ return this.pc.getStats(selector);
391
+ };
334
392
 
335
393
  private getCodecPreferences = (
336
394
  trackType: TrackType,
@@ -353,7 +411,7 @@ export class Publisher {
353
411
  private onIceCandidate = async (e: RTCPeerConnectionIceEvent) => {
354
412
  const { candidate } = e;
355
413
  if (!candidate) {
356
- this.logger?.('warn', 'null ice candidate');
414
+ this.logger('warn', 'null ice candidate');
357
415
  return;
358
416
  }
359
417
  await this.sfuClient.iceTrickle({
@@ -362,10 +420,83 @@ export class Publisher {
362
420
  });
363
421
  };
364
422
 
423
+ /**
424
+ * Performs a migration of this publisher instance to a new SFU.
425
+ *
426
+ * Initiates a new `iceRestart` offer/answer exchange with the new SFU.
427
+ *
428
+ * @param sfuClient the new SFU client to migrate to.
429
+ * @param connectionConfig the new connection configuration to use.
430
+ */
431
+ migrateTo = async (
432
+ sfuClient: StreamSfuClient,
433
+ connectionConfig?: RTCConfiguration,
434
+ ) => {
435
+ this.sfuClient = sfuClient;
436
+ this.pc.setConfiguration(connectionConfig);
437
+
438
+ const shouldRestartIce = this.pc.iceConnectionState === 'connected';
439
+ if (shouldRestartIce) {
440
+ // negotiate only if there are tracks to publish
441
+ await this.negotiate({ iceRestart: true });
442
+ }
443
+ };
444
+
365
445
  private onNegotiationNeeded = async () => {
366
- this.logger?.('info', 'AAA onNegotiationNeeded');
367
- const offer = await this.publisher.createOffer();
368
- let sdp = offer.sdp;
446
+ await this.negotiate();
447
+ };
448
+
449
+ /**
450
+ * Initiates a new offer/answer exchange with the currently connected SFU.
451
+ *
452
+ * @param options the optional offer options to use.
453
+ */
454
+ private negotiate = async (options?: RTCOfferOptions) => {
455
+ const offer = await this.pc.createOffer(options);
456
+ offer.sdp = this.mungeCodecs(offer.sdp);
457
+
458
+ const trackInfos = this.getCurrentTrackInfos(offer.sdp);
459
+ if (trackInfos.length === 0) {
460
+ throw new Error(
461
+ `Can't initiate negotiation without announcing any tracks`,
462
+ );
463
+ }
464
+
465
+ await this.pc.setLocalDescription(offer);
466
+
467
+ const { response } = await this.sfuClient.setPublisher({
468
+ sdp: offer.sdp || '',
469
+ tracks: trackInfos,
470
+ });
471
+
472
+ try {
473
+ await this.pc.setRemoteDescription({
474
+ type: 'answer',
475
+ sdp: response.sdp,
476
+ });
477
+ } catch (e) {
478
+ this.logger('error', `setRemoteDescription error`, {
479
+ sdp: response.sdp,
480
+ error: e,
481
+ });
482
+ }
483
+
484
+ this.sfuClient.iceTrickleBuffer.publisherCandidates.subscribe(
485
+ async (candidate) => {
486
+ try {
487
+ const iceCandidate = JSON.parse(candidate.iceCandidate);
488
+ await this.pc.addIceCandidate(iceCandidate);
489
+ } catch (e) {
490
+ this.logger('error', `ICE candidate error`, {
491
+ error: e,
492
+ candidate,
493
+ });
494
+ }
495
+ },
496
+ );
497
+ };
498
+
499
+ private mungeCodecs = (sdp?: string) => {
369
500
  if (sdp) {
370
501
  sdp = toggleDtx(sdp, this.isDtxEnabled);
371
502
  if (isReactNative()) {
@@ -382,28 +513,63 @@ export class Publisher {
382
513
  }
383
514
  }
384
515
  }
385
- offer.sdp = sdp;
386
- await this.publisher.setLocalDescription(offer);
516
+ return sdp;
517
+ };
518
+
519
+ getCurrentTrackInfos = (sdp?: string) => {
520
+ sdp = sdp || this.pc.localDescription?.sdp;
521
+ const extractMid = (
522
+ defaultMid: string | null,
523
+ track: MediaStreamTrack,
524
+ ): string => {
525
+ if (defaultMid) return defaultMid;
526
+ if (!sdp) {
527
+ this.logger('warn', 'No SDP found. Returning empty mid');
528
+ return '';
529
+ }
530
+
531
+ this.logger('warn', 'No mid found for track. Trying to find it from SDP');
532
+
533
+ const parsedSdp = SDP.parse(sdp);
534
+ const media = parsedSdp.media.find((m) => m.type === track.kind);
535
+ if (typeof media?.mid === 'undefined') {
536
+ this.logger('warn', `No mid found in SDP for track type ${track.kind}`);
537
+ return '';
538
+ }
539
+ return String(media.mid);
540
+ };
387
541
 
388
542
  const metadata = this.state.metadata;
389
543
  const targetResolution = metadata?.settings.video.target_resolution;
390
- const trackInfos = this.publisher
544
+ return this.pc
391
545
  .getTransceivers()
392
- .filter((t) => t.direction === 'sendonly' && !!t.sender.track)
546
+ .filter((t) => t.direction === 'sendonly' && t.sender.track)
393
547
  .map<TrackInfo>((transceiver) => {
394
- const trackType = Number(
548
+ const trackType: TrackType = Number(
395
549
  Object.keys(this.transceiverRegistry).find(
396
550
  (key) =>
397
551
  this.transceiverRegistry[key as any as TrackType] === transceiver,
398
552
  ),
399
553
  );
400
554
  const track = transceiver.sender.track!;
401
- const optimalLayers =
402
- trackType === TrackType.VIDEO
403
- ? findOptimalVideoLayers(track, targetResolution)
404
- : trackType === TrackType.SCREEN_SHARE
405
- ? findOptimalScreenSharingLayers(track)
406
- : [];
555
+ let optimalLayers: OptimalVideoLayer[];
556
+ if (track.readyState === 'live') {
557
+ optimalLayers =
558
+ trackType === TrackType.VIDEO
559
+ ? findOptimalVideoLayers(track, targetResolution)
560
+ : trackType === TrackType.SCREEN_SHARE
561
+ ? findOptimalScreenSharingLayers(track)
562
+ : [];
563
+ this.trackLayersCache[trackType] = optimalLayers;
564
+ } else {
565
+ // we report the last known optimal layers for ended tracks
566
+ optimalLayers = this.trackLayersCache[trackType] || [];
567
+ this.logger(
568
+ 'debug',
569
+ `Track ${TrackType[trackType]} is ended. Announcing last known optimal layers`,
570
+ optimalLayers,
571
+ );
572
+ }
407
573
 
408
574
  const layers = optimalLayers.map<VideoLayer>((optimalLayer) => ({
409
575
  rid: optimalLayer.rid || '',
@@ -420,69 +586,37 @@ export class Publisher {
420
586
  trackId: track.id,
421
587
  layers: layers,
422
588
  trackType,
423
- mid: transceiver.mid || '',
589
+ mid: extractMid(transceiver.mid, track),
424
590
 
425
591
  // FIXME OL: adjust these values
426
592
  stereo: false,
427
- dtx: this.isDtxEnabled,
428
- red: this.isRedEnabled,
593
+ dtx: TrackType.AUDIO === trackType && this.isDtxEnabled,
594
+ red: TrackType.AUDIO === trackType && this.isRedEnabled,
429
595
  };
430
596
  });
431
-
432
- // TODO debounce for 250ms
433
- const { response } = await this.sfuClient.setPublisher({
434
- sdp: offer.sdp || '',
435
- tracks: trackInfos,
436
- });
437
-
438
- try {
439
- await this.publisher.setRemoteDescription({
440
- type: 'answer',
441
- sdp: response.sdp,
442
- });
443
- } catch (e) {
444
- this.logger?.('error', `Publisher: setRemoteDescription error`, {
445
- sdp: response.sdp,
446
- error: e,
447
- });
448
- }
449
-
450
- this.sfuClient.iceTrickleBuffer.publisherCandidates.subscribe(
451
- async (candidate) => {
452
- try {
453
- const iceCandidate = JSON.parse(candidate.iceCandidate);
454
- await this.publisher.addIceCandidate(iceCandidate);
455
- } catch (e) {
456
- this.logger?.('error', `Publisher: ICE candidate error`, {
457
- error: e,
458
- candidate,
459
- });
460
- }
461
- },
462
- );
463
597
  };
464
598
 
465
599
  private onIceCandidateError = (e: Event) => {
466
600
  const errorMessage =
467
601
  e instanceof RTCPeerConnectionIceErrorEvent &&
468
602
  `${e.errorCode}: ${e.errorText}`;
469
- this.logger?.('error', `Publisher: ICE Candidate error`, errorMessage);
603
+ this.logger('error', `ICE Candidate error`, errorMessage);
470
604
  };
471
605
 
472
606
  private onIceConnectionStateChange = () => {
473
- this.logger?.(
607
+ this.logger(
474
608
  'error',
475
- `Publisher: ICE Connection state changed`,
476
- this.publisher.iceConnectionState,
609
+ `ICE Connection state changed`,
610
+ this.pc.iceConnectionState,
477
611
  );
478
612
  };
479
613
 
480
614
  private onIceGatheringStateChange = () => {
481
- this.logger?.(
482
- 'error',
483
- `Publisher: ICE Gathering State`,
484
- this.publisher.iceGatheringState,
485
- );
615
+ this.logger('error', `ICE Gathering State`, this.pc.iceGatheringState);
616
+ };
617
+
618
+ private onSignalingStateChange = () => {
619
+ this.logger('debug', `Signaling state changed`, this.pc.signalingState);
486
620
  };
487
621
 
488
622
  private ridToVideoQuality = (rid: string): VideoQuality => {