@stream-io/video-client 1.48.0 → 1.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/dist/index.browser.es.js +1497 -677
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +1497 -677
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +1497 -677
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +77 -4
- package/dist/src/StreamSfuClient.d.ts +8 -1
- package/dist/src/coordinator/connection/client.d.ts +1 -1
- package/dist/src/coordinator/connection/connection.d.ts +31 -25
- package/dist/src/coordinator/connection/types.d.ts +14 -0
- package/dist/src/coordinator/connection/utils.d.ts +1 -0
- package/dist/src/devices/DeviceManager.d.ts +3 -0
- package/dist/src/devices/DeviceManagerState.d.ts +13 -1
- package/dist/src/gen/video/sfu/event/events.d.ts +5 -1
- package/dist/src/gen/video/sfu/models/models.d.ts +34 -0
- package/dist/src/helpers/AudioBindingsWatchdog.d.ts +3 -3
- package/dist/src/helpers/BlockedAudioTracker.d.ts +30 -0
- package/dist/src/helpers/DynascaleManager.d.ts +8 -86
- package/dist/src/helpers/MediaPlaybackWatchdog.d.ts +32 -0
- package/dist/src/helpers/SlidingWindowRateLimiter.d.ts +28 -0
- package/dist/src/helpers/TrackSubscriptionManager.d.ts +114 -0
- package/dist/src/helpers/ViewportTracker.d.ts +11 -17
- package/dist/src/helpers/browsers.d.ts +13 -0
- package/dist/src/helpers/concurrency.d.ts +6 -4
- package/dist/src/rtc/BasePeerConnection.d.ts +11 -2
- package/dist/src/rtc/Publisher.d.ts +17 -0
- package/dist/src/rtc/Subscriber.d.ts +1 -0
- package/dist/src/rtc/helpers/degradationPreference.d.ts +2 -0
- package/dist/src/rtc/index.d.ts +1 -0
- package/dist/src/rtc/types.d.ts +33 -1
- package/dist/src/stats/rtc/types.d.ts +1 -1
- package/dist/src/store/rxUtils.d.ts +9 -0
- package/dist/src/types.d.ts +18 -0
- package/package.json +2 -2
- package/src/Call.ts +268 -40
- package/src/StreamSfuClient.ts +75 -12
- package/src/__tests__/Call.lifecycle.test.ts +67 -0
- package/src/__tests__/Call.publishing.test.ts +103 -0
- package/src/__tests__/StreamSfuClient.test.ts +275 -0
- package/src/coordinator/connection/__tests__/connection.test.ts +482 -0
- package/src/coordinator/connection/client.ts +1 -1
- package/src/coordinator/connection/connection.ts +149 -96
- package/src/coordinator/connection/types.ts +15 -0
- package/src/coordinator/connection/utils.ts +15 -0
- package/src/devices/DeviceManager.ts +92 -32
- package/src/devices/DeviceManagerState.ts +20 -1
- package/src/devices/__tests__/DeviceManager.test.ts +283 -0
- package/src/devices/__tests__/ScreenShareManager.test.ts +0 -2
- package/src/devices/__tests__/mocks.ts +2 -0
- package/src/devices/devices.ts +2 -1
- package/src/gen/video/sfu/event/events.ts +15 -0
- package/src/gen/video/sfu/models/models.ts +44 -0
- package/src/helpers/AudioBindingsWatchdog.ts +10 -7
- package/src/helpers/BlockedAudioTracker.ts +74 -0
- package/src/helpers/DynascaleManager.ts +46 -337
- package/src/helpers/MediaPlaybackWatchdog.ts +121 -0
- package/src/helpers/SlidingWindowRateLimiter.ts +49 -0
- package/src/helpers/TrackSubscriptionManager.ts +243 -0
- package/src/helpers/ViewportTracker.ts +74 -19
- package/src/helpers/__tests__/BlockedAudioTracker.test.ts +114 -0
- package/src/helpers/__tests__/DynascaleManager.test.ts +175 -122
- package/src/helpers/__tests__/MediaPlaybackWatchdog.test.ts +180 -0
- package/src/helpers/__tests__/SlidingWindowRateLimiter.test.ts +43 -0
- package/src/helpers/__tests__/TrackSubscriptionManager.test.ts +310 -0
- package/src/helpers/__tests__/ViewportTracker.test.ts +83 -0
- package/src/helpers/__tests__/browsers.test.ts +85 -1
- package/src/helpers/browsers.ts +24 -0
- package/src/helpers/concurrency.ts +9 -10
- package/src/rpc/retryable.ts +0 -1
- package/src/rtc/BasePeerConnection.ts +96 -6
- package/src/rtc/Publisher.ts +49 -2
- package/src/rtc/Subscriber.ts +42 -14
- package/src/rtc/__tests__/Call.reconnect.test.ts +761 -0
- package/src/rtc/__tests__/Publisher.test.ts +332 -10
- package/src/rtc/__tests__/Subscriber.test.ts +202 -1
- package/src/rtc/__tests__/mocks/webrtc.mocks.ts +13 -2
- package/src/rtc/helpers/__tests__/degradationPreference.test.ts +23 -0
- package/src/rtc/helpers/degradationPreference.ts +22 -0
- package/src/rtc/index.ts +1 -0
- package/src/rtc/types.ts +38 -1
- package/src/stats/rtc/types.ts +1 -0
- package/src/store/__tests__/rxUtils.test.ts +276 -0
- package/src/store/rxUtils.ts +19 -0
- package/src/types.ts +19 -0
|
@@ -13,7 +13,12 @@ import { StreamSfuClient } from '../StreamSfuClient';
|
|
|
13
13
|
import { AllSfuEvents, Dispatcher } from './Dispatcher';
|
|
14
14
|
import { withoutConcurrency } from '../helpers/concurrency';
|
|
15
15
|
import { StatsTracer, Tracer, traceRTCPeerConnection } from '../stats';
|
|
16
|
-
import
|
|
16
|
+
import {
|
|
17
|
+
BasePeerConnectionOpts,
|
|
18
|
+
OnIceConnected,
|
|
19
|
+
OnReconnectionNeeded,
|
|
20
|
+
ReconnectReason,
|
|
21
|
+
} from './types';
|
|
17
22
|
import type { ClientPublishOptions } from '../types';
|
|
18
23
|
|
|
19
24
|
/**
|
|
@@ -31,8 +36,11 @@ export abstract class BasePeerConnection {
|
|
|
31
36
|
protected sfuClient: StreamSfuClient;
|
|
32
37
|
|
|
33
38
|
private onReconnectionNeeded?: OnReconnectionNeeded;
|
|
39
|
+
private onIceConnected?: OnIceConnected;
|
|
34
40
|
private readonly iceRestartDelay: number;
|
|
41
|
+
private iceHasEverConnected = false;
|
|
35
42
|
private iceRestartTimeout?: NodeJS.Timeout;
|
|
43
|
+
private preConnectStuckTimeout?: NodeJS.Timeout;
|
|
36
44
|
protected isIceRestarting = false;
|
|
37
45
|
private isDisposed = false;
|
|
38
46
|
|
|
@@ -56,6 +64,7 @@ export abstract class BasePeerConnection {
|
|
|
56
64
|
state,
|
|
57
65
|
dispatcher,
|
|
58
66
|
onReconnectionNeeded,
|
|
67
|
+
onIceConnected,
|
|
59
68
|
tag,
|
|
60
69
|
enableTracing,
|
|
61
70
|
clientPublishOptions,
|
|
@@ -70,6 +79,7 @@ export abstract class BasePeerConnection {
|
|
|
70
79
|
this.clientPublishOptions = clientPublishOptions;
|
|
71
80
|
this.tag = tag;
|
|
72
81
|
this.onReconnectionNeeded = onReconnectionNeeded;
|
|
82
|
+
this.onIceConnected = onIceConnected;
|
|
73
83
|
this.logger = videoLoggerSystem.getLogger(
|
|
74
84
|
peerType === PeerType.SUBSCRIBER ? 'Subscriber' : 'Publisher',
|
|
75
85
|
{ tags: [tag] },
|
|
@@ -108,7 +118,10 @@ export abstract class BasePeerConnection {
|
|
|
108
118
|
dispose() {
|
|
109
119
|
clearTimeout(this.iceRestartTimeout);
|
|
110
120
|
this.iceRestartTimeout = undefined;
|
|
121
|
+
clearTimeout(this.preConnectStuckTimeout);
|
|
122
|
+
this.preConnectStuckTimeout = undefined;
|
|
111
123
|
this.onReconnectionNeeded = undefined;
|
|
124
|
+
this.onIceConnected = undefined;
|
|
112
125
|
this.isDisposed = true;
|
|
113
126
|
this.detachEventHandlers();
|
|
114
127
|
this.pc.close();
|
|
@@ -145,14 +158,17 @@ export abstract class BasePeerConnection {
|
|
|
145
158
|
*/
|
|
146
159
|
protected tryRestartIce = () => {
|
|
147
160
|
this.restartIce().catch((e) => {
|
|
148
|
-
|
|
149
|
-
this.logger.error(reason, e);
|
|
161
|
+
this.logger.error('restartICE() failed, initiating reconnect', e);
|
|
150
162
|
const strategy =
|
|
151
163
|
e instanceof NegotiationError &&
|
|
152
164
|
e.error.code === ErrorCode.PARTICIPANT_SIGNAL_LOST
|
|
153
165
|
? WebsocketReconnectStrategy.FAST
|
|
154
166
|
: WebsocketReconnectStrategy.REJOIN;
|
|
155
|
-
this.onReconnectionNeeded?.(
|
|
167
|
+
this.onReconnectionNeeded?.(
|
|
168
|
+
strategy,
|
|
169
|
+
ReconnectReason.RESTART_ICE_FAILED,
|
|
170
|
+
this.peerType,
|
|
171
|
+
);
|
|
156
172
|
});
|
|
157
173
|
};
|
|
158
174
|
|
|
@@ -239,6 +255,20 @@ export abstract class BasePeerConnection {
|
|
|
239
255
|
return !failedStates.has(iceState) && !failedStates.has(connectionState);
|
|
240
256
|
};
|
|
241
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Returns true only when the peer connection is currently fully established
|
|
260
|
+
* (ICE `connected`/`completed` AND connection state `connected`).
|
|
261
|
+
* Transient states like `disconnected`, `checking`, or `new` return false.
|
|
262
|
+
*/
|
|
263
|
+
isStable = () => {
|
|
264
|
+
const iceState = this.pc.iceConnectionState;
|
|
265
|
+
const connectionState = this.pc.connectionState;
|
|
266
|
+
return (
|
|
267
|
+
(iceState === 'connected' || iceState === 'completed') &&
|
|
268
|
+
connectionState === 'connected'
|
|
269
|
+
);
|
|
270
|
+
};
|
|
271
|
+
|
|
242
272
|
/**
|
|
243
273
|
* Handles the ICECandidate event and
|
|
244
274
|
* Initiates an ICE Trickle process with the SFU.
|
|
@@ -292,7 +322,7 @@ export abstract class BasePeerConnection {
|
|
|
292
322
|
if (state === 'failed') {
|
|
293
323
|
this.onReconnectionNeeded?.(
|
|
294
324
|
WebsocketReconnectStrategy.REJOIN,
|
|
295
|
-
|
|
325
|
+
ReconnectReason.CONNECTION_FAILED,
|
|
296
326
|
this.peerType,
|
|
297
327
|
);
|
|
298
328
|
return;
|
|
@@ -320,6 +350,54 @@ export abstract class BasePeerConnection {
|
|
|
320
350
|
// do nothing when ICE is restarting
|
|
321
351
|
if (this.isIceRestarting) return;
|
|
322
352
|
|
|
353
|
+
// Pre-connect handling: ICE has never reached `connected`/`completed`.
|
|
354
|
+
// Restart is futile here (the data plane was never established), but
|
|
355
|
+
// these two terminal-ish states need different treatment:
|
|
356
|
+
// - `failed` is terminal, escalate to REJOIN so a new SFU/credentials
|
|
357
|
+
// /PC configuration gets a chance, and let `Call.reconnect` count
|
|
358
|
+
// this toward the unsupported-network budget.
|
|
359
|
+
// - `disconnected` is transient, the browser may yet move back to
|
|
360
|
+
// `checking`/`connected`. Don't restart, don't escalate; wait it
|
|
361
|
+
// out. If it ultimately fails, ICE will transition to `failed` and
|
|
362
|
+
// the branch above will take over.
|
|
363
|
+
if (!this.iceHasEverConnected) {
|
|
364
|
+
if (state === 'failed') {
|
|
365
|
+
this.logger.info('ICE failed before connected, escalating to REJOIN');
|
|
366
|
+
clearTimeout(this.preConnectStuckTimeout);
|
|
367
|
+
this.preConnectStuckTimeout = undefined;
|
|
368
|
+
this.onReconnectionNeeded?.(
|
|
369
|
+
WebsocketReconnectStrategy.REJOIN,
|
|
370
|
+
ReconnectReason.ICE_NEVER_CONNECTED,
|
|
371
|
+
this.peerType,
|
|
372
|
+
);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (state === 'disconnected') {
|
|
376
|
+
this.logger.info('ICE disconnected before connected, wait to recover');
|
|
377
|
+
// Watchdog: if the browser stays in `disconnected` without ever
|
|
378
|
+
// reaching `connected` or transitioning to `failed`, escalate to
|
|
379
|
+
// REJOIN ourselves so we don't wait silently forever. Rare but
|
|
380
|
+
// observed on flaky mobile networks.
|
|
381
|
+
clearTimeout(this.preConnectStuckTimeout);
|
|
382
|
+
this.preConnectStuckTimeout = setTimeout(() => {
|
|
383
|
+
if (
|
|
384
|
+
!this.iceHasEverConnected &&
|
|
385
|
+
this.pc.iceConnectionState === 'disconnected'
|
|
386
|
+
) {
|
|
387
|
+
this.logger.info(
|
|
388
|
+
'ICE stuck in pre-connect disconnected, escalating to REJOIN',
|
|
389
|
+
);
|
|
390
|
+
this.onReconnectionNeeded?.(
|
|
391
|
+
WebsocketReconnectStrategy.REJOIN,
|
|
392
|
+
ReconnectReason.ICE_NEVER_CONNECTED,
|
|
393
|
+
this.peerType,
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
}, this.iceRestartDelay * 2);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
323
401
|
switch (state) {
|
|
324
402
|
case 'failed':
|
|
325
403
|
// in the `failed` state, we try to restart ICE immediately
|
|
@@ -341,12 +419,24 @@ export abstract class BasePeerConnection {
|
|
|
341
419
|
break;
|
|
342
420
|
|
|
343
421
|
case 'connected':
|
|
344
|
-
|
|
422
|
+
case 'completed':
|
|
423
|
+
// Fire `onIceConnected` exactly once per peer-connection lifetime —
|
|
424
|
+
// the first time ICE reaches `connected`/`completed` end-to-end.
|
|
425
|
+
// Used by `Call` to reset the unsupported-network failure counter
|
|
426
|
+
// only after WebRTC has actually recovered, not merely on SFU join.
|
|
427
|
+
if (!this.iceHasEverConnected) {
|
|
428
|
+
this.iceHasEverConnected = true;
|
|
429
|
+
this.onIceConnected?.(this.peerType);
|
|
430
|
+
}
|
|
431
|
+
// clear any scheduled restartICE since the connection is healthy
|
|
345
432
|
if (this.iceRestartTimeout) {
|
|
346
433
|
this.logger.info('connected connection, canceling restartICE');
|
|
347
434
|
clearTimeout(this.iceRestartTimeout);
|
|
348
435
|
this.iceRestartTimeout = undefined;
|
|
349
436
|
}
|
|
437
|
+
// clear the pre-connect watchdog if it was armed
|
|
438
|
+
clearTimeout(this.preConnectStuckTimeout);
|
|
439
|
+
this.preConnectStuckTimeout = undefined;
|
|
350
440
|
break;
|
|
351
441
|
}
|
|
352
442
|
};
|
package/src/rtc/Publisher.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
toVideoLayers,
|
|
21
21
|
} from './layers';
|
|
22
22
|
import { isSvcCodec } from './codecs';
|
|
23
|
+
import { toRTCDegradationPreference } from './helpers/degradationPreference';
|
|
23
24
|
import { isAudioTrackType } from './helpers/tracks';
|
|
24
25
|
import { extractMid, removeCodecsExcept, setStartBitrate } from './helpers/sdp';
|
|
25
26
|
import { withoutConcurrency } from '../helpers/concurrency';
|
|
@@ -135,7 +136,9 @@ export class Publisher extends BasePeerConnection {
|
|
|
135
136
|
});
|
|
136
137
|
|
|
137
138
|
const params = transceiver.sender.getParameters();
|
|
138
|
-
params.degradationPreference =
|
|
139
|
+
params.degradationPreference =
|
|
140
|
+
toRTCDegradationPreference(publishOption.degradationPreference) ??
|
|
141
|
+
'maintain-framerate';
|
|
139
142
|
await transceiver.sender.setParameters(params);
|
|
140
143
|
|
|
141
144
|
const trackType = publishOption.trackType;
|
|
@@ -248,6 +251,38 @@ export class Publisher extends BasePeerConnection {
|
|
|
248
251
|
return false;
|
|
249
252
|
};
|
|
250
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Re-arms the encoder for the given track type by detaching and
|
|
256
|
+
* reattaching the currently published track on each matching sender.
|
|
257
|
+
*
|
|
258
|
+
* Workaround for a WebKit / iOS Safari quirk: after a system audio
|
|
259
|
+
* session interruption (Siri, PSTN call), the `RTCRtpSender` encoder
|
|
260
|
+
* can stop producing RTP packets even though the underlying
|
|
261
|
+
* `MediaStreamTrack` is `live` and `track.muted === false`.
|
|
262
|
+
* `replaceTrack(null)` followed by `replaceTrack(track)` resets the
|
|
263
|
+
* sender's encoder pipeline without renegotiation, restoring packet
|
|
264
|
+
* flow with the same SSRC.
|
|
265
|
+
*
|
|
266
|
+
* No-op when nothing is published for the given track type.
|
|
267
|
+
*
|
|
268
|
+
* @param trackType the track type to refresh.
|
|
269
|
+
*/
|
|
270
|
+
refreshTrack = async (trackType: TrackType) => {
|
|
271
|
+
for (const item of this.transceiverCache.items()) {
|
|
272
|
+
if (item.publishOption.trackType !== trackType) continue;
|
|
273
|
+
const { sender } = item.transceiver;
|
|
274
|
+
const track = sender.track;
|
|
275
|
+
if (!track || track.readyState !== 'live') continue;
|
|
276
|
+
try {
|
|
277
|
+
await sender.replaceTrack(null);
|
|
278
|
+
await sender.replaceTrack(track);
|
|
279
|
+
this.logger.debug(`Refreshed ${TrackType[trackType]} sender`);
|
|
280
|
+
} catch (err) {
|
|
281
|
+
this.logger.warn(`Failed to refresh ${TrackType[trackType]}`, err);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
|
|
251
286
|
/**
|
|
252
287
|
* Stops the cloned track that is being published to the SFU.
|
|
253
288
|
*/
|
|
@@ -344,6 +379,17 @@ export class Publisher extends BasePeerConnection {
|
|
|
344
379
|
}
|
|
345
380
|
}
|
|
346
381
|
|
|
382
|
+
const degradationPreference = toRTCDegradationPreference(
|
|
383
|
+
videoSender.degradationPreference,
|
|
384
|
+
);
|
|
385
|
+
if (
|
|
386
|
+
degradationPreference &&
|
|
387
|
+
params.degradationPreference !== degradationPreference
|
|
388
|
+
) {
|
|
389
|
+
params.degradationPreference = degradationPreference;
|
|
390
|
+
changed = true;
|
|
391
|
+
}
|
|
392
|
+
|
|
347
393
|
const activeEncoders = params.encodings.filter((e) => e.active);
|
|
348
394
|
if (!changed) {
|
|
349
395
|
return this.logger.info(`${tag} no change:`, activeEncoders);
|
|
@@ -460,7 +506,8 @@ export class Publisher extends BasePeerConnection {
|
|
|
460
506
|
const trackInfos: TrackInfo[] = [];
|
|
461
507
|
for (const publishOption of this.publishOptions) {
|
|
462
508
|
const bundle = this.transceiverCache.get(publishOption);
|
|
463
|
-
|
|
509
|
+
const track = bundle?.transceiver.sender.track;
|
|
510
|
+
if (!bundle || !track || track.readyState !== 'live') continue;
|
|
464
511
|
trackInfos.push(this.toTrackInfo(bundle, sdp));
|
|
465
512
|
}
|
|
466
513
|
return trackInfos;
|
package/src/rtc/Subscriber.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { BasePeerConnection } from './BasePeerConnection';
|
|
2
2
|
import { BasePeerConnectionOpts } from './types';
|
|
3
3
|
import { NegotiationError } from './NegotiationError';
|
|
4
|
-
import { PeerType } from '../gen/video/sfu/models/models';
|
|
4
|
+
import { PeerType, TrackType } from '../gen/video/sfu/models/models';
|
|
5
5
|
import { SubscriberOffer } from '../gen/video/sfu/event/events';
|
|
6
6
|
import { toTrackType, trackTypeToParticipantStreamKey } from './helpers/tracks';
|
|
7
|
+
import { pushToIfMissing, removeFromIfPresent } from '../helpers/array';
|
|
7
8
|
import { enableStereo, removeCodecsExcept } from './helpers/sdp';
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -67,7 +68,8 @@ export class Subscriber extends BasePeerConnection {
|
|
|
67
68
|
};
|
|
68
69
|
|
|
69
70
|
private handleOnTrack = (e: RTCTrackEvent) => {
|
|
70
|
-
const
|
|
71
|
+
const { streams, track } = e;
|
|
72
|
+
const [primaryStream] = streams;
|
|
71
73
|
// example: `e3f6aaf8-b03d-4911-be36-83f47d37a76a:TRACK_TYPE_VIDEO`
|
|
72
74
|
const [trackId, rawTrackType] = primaryStream.id.split(':');
|
|
73
75
|
const participantToUpdate = this.state.participants.find(
|
|
@@ -75,30 +77,35 @@ export class Subscriber extends BasePeerConnection {
|
|
|
75
77
|
);
|
|
76
78
|
this.logger.debug(
|
|
77
79
|
`[onTrack]: Got remote ${rawTrackType} track for userId: ${participantToUpdate?.userId}`,
|
|
78
|
-
|
|
79
|
-
|
|
80
|
+
track.id,
|
|
81
|
+
track,
|
|
80
82
|
);
|
|
81
83
|
|
|
84
|
+
const trackType = toTrackType(rawTrackType);
|
|
85
|
+
if (!trackType) {
|
|
86
|
+
return this.logger.error(`Unknown track type: ${rawTrackType}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
82
89
|
const trackDebugInfo = `${participantToUpdate?.userId} ${rawTrackType}:${trackId}`;
|
|
83
|
-
|
|
90
|
+
track.addEventListener('mute', () => {
|
|
84
91
|
this.logger.info(`[onTrack]: Track muted: ${trackDebugInfo}`);
|
|
92
|
+
this.setRemoteTrackInterrupted(trackId, trackType, true);
|
|
85
93
|
});
|
|
86
|
-
|
|
87
|
-
e.track.addEventListener('unmute', () => {
|
|
94
|
+
track.addEventListener('unmute', () => {
|
|
88
95
|
this.logger.info(`[onTrack]: Track unmuted: ${trackDebugInfo}`);
|
|
96
|
+
this.setRemoteTrackInterrupted(trackId, trackType, false);
|
|
89
97
|
});
|
|
90
|
-
|
|
91
|
-
e.track.addEventListener('ended', () => {
|
|
98
|
+
track.addEventListener('ended', () => {
|
|
92
99
|
this.logger.info(`[onTrack]: Track ended: ${trackDebugInfo}`);
|
|
100
|
+
this.setRemoteTrackInterrupted(trackId, trackType, false);
|
|
93
101
|
this.state.removeOrphanedTrack(primaryStream.id);
|
|
94
102
|
});
|
|
95
103
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return this.logger.error(`Unknown track type: ${rawTrackType}`);
|
|
104
|
+
if (track.muted) {
|
|
105
|
+
this.setRemoteTrackInterrupted(trackId, trackType, true);
|
|
99
106
|
}
|
|
100
107
|
|
|
101
|
-
this.trackIdToTrackType.set(
|
|
108
|
+
this.trackIdToTrackType.set(track.id, trackType);
|
|
102
109
|
|
|
103
110
|
if (!participantToUpdate) {
|
|
104
111
|
this.logger.warn(
|
|
@@ -133,7 +140,7 @@ export class Subscriber extends BasePeerConnection {
|
|
|
133
140
|
// now, dispose the previous stream if it exists
|
|
134
141
|
if (previousStream) {
|
|
135
142
|
this.logger.info(
|
|
136
|
-
`[onTrack]: Cleaning up previous remote ${
|
|
143
|
+
`[onTrack]: Cleaning up previous remote ${track.kind} tracks for userId: ${participantToUpdate.userId}`,
|
|
137
144
|
);
|
|
138
145
|
previousStream.getTracks().forEach((t) => {
|
|
139
146
|
t.stop();
|
|
@@ -142,6 +149,27 @@ export class Subscriber extends BasePeerConnection {
|
|
|
142
149
|
}
|
|
143
150
|
};
|
|
144
151
|
|
|
152
|
+
private setRemoteTrackInterrupted = (
|
|
153
|
+
trackId: string,
|
|
154
|
+
trackType: TrackType,
|
|
155
|
+
interrupted: boolean,
|
|
156
|
+
) => {
|
|
157
|
+
if (trackType !== TrackType.AUDIO) return;
|
|
158
|
+
const target = this.state.participants.find(
|
|
159
|
+
(p) => p.trackLookupPrefix === trackId,
|
|
160
|
+
);
|
|
161
|
+
if (!target) return;
|
|
162
|
+
this.state.updateParticipant(target.sessionId, (p) => {
|
|
163
|
+
const current = p.interruptedTracks ?? [];
|
|
164
|
+
const has = current.includes(trackType);
|
|
165
|
+
if (interrupted === has) return {};
|
|
166
|
+
const next = interrupted
|
|
167
|
+
? pushToIfMissing([...current], trackType)
|
|
168
|
+
: removeFromIfPresent([...current], trackType);
|
|
169
|
+
return { interruptedTracks: next };
|
|
170
|
+
});
|
|
171
|
+
};
|
|
172
|
+
|
|
145
173
|
private negotiate = async (subscriberOffer: SubscriberOffer) => {
|
|
146
174
|
await this.pc.setRemoteDescription({
|
|
147
175
|
type: 'offer',
|