livekit-client 1.2.2 → 1.2.3
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/dist/livekit-client.esm.mjs +972 -109
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/options.d.ts +5 -0
- package/dist/src/options.d.ts.map +1 -1
- package/dist/src/room/DefaultReconnectPolicy.d.ts +8 -0
- package/dist/src/room/DefaultReconnectPolicy.d.ts.map +1 -0
- package/dist/src/room/PCTransport.d.ts +1 -0
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +6 -1
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/ReconnectPolicy.d.ts +23 -0
- package/dist/src/room/ReconnectPolicy.d.ts.map +1 -0
- package/dist/src/room/Room.d.ts +8 -0
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +2 -2
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
- package/package.json +3 -1
- package/src/index.ts +2 -0
- package/src/options.ts +6 -0
- package/src/room/DefaultReconnectPolicy.ts +35 -0
- package/src/room/PCTransport.ts +91 -17
- package/src/room/RTCEngine.ts +79 -31
- package/src/room/ReconnectPolicy.ts +25 -0
- package/src/room/Room.ts +55 -30
- package/src/room/events.ts +2 -2
- package/src/room/participant/LocalParticipant.ts +30 -16
- package/src/room/participant/RemoteParticipant.ts +13 -0
- package/src/room/track/LocalVideoTrack.ts +0 -1
package/src/room/RTCEngine.ts
CHANGED
@@ -2,6 +2,7 @@ import { EventEmitter } from 'events';
|
|
2
2
|
import type TypedEventEmitter from 'typed-emitter';
|
3
3
|
import { SignalClient, SignalOptions } from '../api/SignalClient';
|
4
4
|
import log from '../logger';
|
5
|
+
import { RoomOptions } from '../options';
|
5
6
|
import {
|
6
7
|
ClientConfigSetting,
|
7
8
|
ClientConfiguration,
|
@@ -19,16 +20,16 @@ import {
|
|
19
20
|
SignalTarget,
|
20
21
|
TrackPublishedResponse,
|
21
22
|
} from '../proto/livekit_rtc';
|
23
|
+
import DefaultReconnectPolicy from './DefaultReconnectPolicy';
|
22
24
|
import { ConnectionError, TrackInvalidError, UnexpectedConnectionState } from './errors';
|
23
25
|
import { EngineEvent } from './events';
|
24
26
|
import PCTransport from './PCTransport';
|
27
|
+
import { ReconnectContext, ReconnectPolicy } from './ReconnectPolicy';
|
25
28
|
import { isFireFox, isWeb, sleep } from './utils';
|
26
29
|
|
27
30
|
const lossyDataChannel = '_lossy';
|
28
31
|
const reliableDataChannel = '_reliable';
|
29
|
-
const maxReconnectRetries = 10;
|
30
32
|
const minReconnectWait = 2 * 1000;
|
31
|
-
const maxReconnectDuration = 60 * 1000;
|
32
33
|
export const maxICEConnectTimeout = 15 * 1000;
|
33
34
|
|
34
35
|
enum PCState {
|
@@ -96,9 +97,15 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
96
97
|
|
97
98
|
private attemptingReconnect: boolean = false;
|
98
99
|
|
99
|
-
|
100
|
+
private reconnectPolicy: ReconnectPolicy;
|
101
|
+
|
102
|
+
private reconnectTimeout?: ReturnType<typeof setTimeout>;
|
103
|
+
|
104
|
+
constructor(private options: RoomOptions) {
|
100
105
|
super();
|
101
106
|
this.client = new SignalClient();
|
107
|
+
this.client.signalLatency = this.options.expSignalLatency;
|
108
|
+
this.reconnectPolicy = this.options.reconnectPolicy ?? new DefaultReconnectPolicy();
|
102
109
|
}
|
103
110
|
|
104
111
|
async join(
|
@@ -157,8 +164,16 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
157
164
|
if (this.pendingTrackResolvers[req.cid]) {
|
158
165
|
throw new TrackInvalidError('a track with the same ID has already been published');
|
159
166
|
}
|
160
|
-
return new Promise<TrackInfo>((resolve) => {
|
161
|
-
|
167
|
+
return new Promise<TrackInfo>((resolve, reject) => {
|
168
|
+
const publicationTimeout = setTimeout(() => {
|
169
|
+
reject(
|
170
|
+
new ConnectionError('publication of local track timed out, no response from server'),
|
171
|
+
);
|
172
|
+
}, 15_000);
|
173
|
+
this.pendingTrackResolvers[req.cid] = (info: TrackInfo) => {
|
174
|
+
clearTimeout(publicationTimeout);
|
175
|
+
resolve(info);
|
176
|
+
};
|
162
177
|
this.client.sendAddTrack(req);
|
163
178
|
});
|
164
179
|
}
|
@@ -317,8 +332,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
317
332
|
await this.subscriber.setRemoteDescription(sd);
|
318
333
|
|
319
334
|
// answer the offer
|
320
|
-
const answer = await this.subscriber.
|
321
|
-
await this.subscriber.pc.setLocalDescription(answer);
|
335
|
+
const answer = await this.subscriber.createAndSetAnswer();
|
322
336
|
this.client.sendAnswer(answer);
|
323
337
|
};
|
324
338
|
|
@@ -437,18 +451,42 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
437
451
|
// websocket reconnect behavior. if websocket is interrupted, and the PeerConnection
|
438
452
|
// continues to work, we can reconnect to websocket to continue the session
|
439
453
|
// after a number of retries, we'll close and give up permanently
|
440
|
-
private handleDisconnect = (connection: string) => {
|
454
|
+
private handleDisconnect = (connection: string, signalEvents: boolean = false) => {
|
441
455
|
if (this._isClosed) {
|
442
456
|
return;
|
443
457
|
}
|
458
|
+
|
444
459
|
log.debug(`${connection} disconnected`);
|
445
460
|
if (this.reconnectAttempts === 0) {
|
446
461
|
// only reset start time on the first try
|
447
462
|
this.reconnectStart = Date.now();
|
448
463
|
}
|
449
464
|
|
450
|
-
const
|
451
|
-
|
465
|
+
const disconnect = (duration: number) => {
|
466
|
+
log.info(
|
467
|
+
`could not recover connection after ${this.reconnectAttempts} attempts, ${duration}ms. giving up`,
|
468
|
+
);
|
469
|
+
this.emit(EngineEvent.Disconnected);
|
470
|
+
this.close();
|
471
|
+
};
|
472
|
+
|
473
|
+
const duration = Date.now() - this.reconnectStart;
|
474
|
+
const delay = this.getNextRetryDelay({
|
475
|
+
elapsedMs: duration,
|
476
|
+
retryCount: this.reconnectAttempts,
|
477
|
+
});
|
478
|
+
|
479
|
+
if (delay === null) {
|
480
|
+
disconnect(duration);
|
481
|
+
return;
|
482
|
+
}
|
483
|
+
|
484
|
+
log.debug(`reconnecting in ${delay}ms`);
|
485
|
+
|
486
|
+
if (this.reconnectTimeout) {
|
487
|
+
clearTimeout(this.reconnectTimeout);
|
488
|
+
}
|
489
|
+
this.reconnectTimeout = setTimeout(async () => {
|
452
490
|
if (this._isClosed) {
|
453
491
|
return;
|
454
492
|
}
|
@@ -469,16 +507,20 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
469
507
|
try {
|
470
508
|
this.attemptingReconnect = true;
|
471
509
|
if (this.fullReconnectOnNext) {
|
472
|
-
await this.restartConnection();
|
510
|
+
await this.restartConnection(signalEvents);
|
473
511
|
} else {
|
474
|
-
await this.resumeConnection();
|
512
|
+
await this.resumeConnection(signalEvents);
|
475
513
|
}
|
476
514
|
this.reconnectAttempts = 0;
|
477
515
|
this.fullReconnectOnNext = false;
|
516
|
+
if (this.reconnectTimeout) {
|
517
|
+
clearTimeout(this.reconnectTimeout);
|
518
|
+
}
|
478
519
|
} catch (e) {
|
479
520
|
this.reconnectAttempts += 1;
|
480
521
|
let reconnectRequired = false;
|
481
522
|
let recoverable = true;
|
523
|
+
let requireSignalEvents = false;
|
482
524
|
if (e instanceof UnexpectedConnectionState) {
|
483
525
|
log.debug('received unrecoverable error', { error: e });
|
484
526
|
// unrecoverable
|
@@ -488,26 +530,17 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
488
530
|
reconnectRequired = true;
|
489
531
|
}
|
490
532
|
|
491
|
-
// when we flip from resume to reconnect
|
492
|
-
//
|
533
|
+
// when we flip from resume to reconnect
|
534
|
+
// we need to fire the right reconnecting events
|
493
535
|
if (reconnectRequired && !this.fullReconnectOnNext) {
|
494
536
|
this.fullReconnectOnNext = true;
|
495
|
-
|
496
|
-
}
|
497
|
-
|
498
|
-
const duration = Date.now() - this.reconnectStart;
|
499
|
-
if (this.reconnectAttempts >= maxReconnectRetries || duration > maxReconnectDuration) {
|
500
|
-
recoverable = false;
|
537
|
+
requireSignalEvents = true;
|
501
538
|
}
|
502
539
|
|
503
540
|
if (recoverable) {
|
504
|
-
this.handleDisconnect('reconnect');
|
541
|
+
this.handleDisconnect('reconnect', requireSignalEvents);
|
505
542
|
} else {
|
506
|
-
|
507
|
-
`could not recover connection after ${maxReconnectRetries} attempts, ${duration}ms. giving up`,
|
508
|
-
);
|
509
|
-
this.emit(EngineEvent.Disconnected);
|
510
|
-
this.close();
|
543
|
+
disconnect(Date.now() - this.reconnectStart);
|
511
544
|
}
|
512
545
|
} finally {
|
513
546
|
this.attemptingReconnect = false;
|
@@ -515,14 +548,25 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
515
548
|
}, delay);
|
516
549
|
};
|
517
550
|
|
518
|
-
private
|
551
|
+
private getNextRetryDelay(context: ReconnectContext) {
|
552
|
+
try {
|
553
|
+
return this.reconnectPolicy.nextRetryDelayInMs(context);
|
554
|
+
} catch (e) {
|
555
|
+
log.warn('encountered error in reconnect policy', { error: e });
|
556
|
+
}
|
557
|
+
|
558
|
+
// error in user code with provided reconnect policy, stop reconnecting
|
559
|
+
return null;
|
560
|
+
}
|
561
|
+
|
562
|
+
private async restartConnection(emitRestarting: boolean = false) {
|
519
563
|
if (!this.url || !this.token) {
|
520
564
|
// permanent failure, don't attempt reconnection
|
521
565
|
throw new UnexpectedConnectionState('could not reconnect, url or token not saved');
|
522
566
|
}
|
523
567
|
|
524
568
|
log.info(`reconnecting, attempt: ${this.reconnectAttempts}`);
|
525
|
-
if (this.reconnectAttempts === 0) {
|
569
|
+
if (emitRestarting || this.reconnectAttempts === 0) {
|
526
570
|
this.emit(EngineEvent.Restarting);
|
527
571
|
}
|
528
572
|
|
@@ -550,7 +594,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
550
594
|
this.emit(EngineEvent.Restarted, joinResponse);
|
551
595
|
}
|
552
596
|
|
553
|
-
private async resumeConnection(): Promise<void> {
|
597
|
+
private async resumeConnection(emitResuming: boolean = false): Promise<void> {
|
554
598
|
if (!this.url || !this.token) {
|
555
599
|
// permanent failure, don't attempt reconnection
|
556
600
|
throw new UnexpectedConnectionState('could not reconnect, url or token not saved');
|
@@ -561,14 +605,18 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
561
605
|
}
|
562
606
|
|
563
607
|
log.info(`resuming signal connection, attempt ${this.reconnectAttempts}`);
|
564
|
-
if (this.reconnectAttempts === 0) {
|
608
|
+
if (emitResuming || this.reconnectAttempts === 0) {
|
565
609
|
this.emit(EngineEvent.Resuming);
|
566
610
|
}
|
567
611
|
|
568
612
|
try {
|
569
613
|
await this.client.reconnect(this.url, this.token);
|
570
614
|
} catch (e) {
|
571
|
-
|
615
|
+
let message = '';
|
616
|
+
if (e instanceof Error) {
|
617
|
+
message = e.message;
|
618
|
+
}
|
619
|
+
throw new SignalReconnectError(message);
|
572
620
|
}
|
573
621
|
this.emit(EngineEvent.SignalResumed);
|
574
622
|
|
@@ -0,0 +1,25 @@
|
|
1
|
+
/** Controls reconnecting of the client */
|
2
|
+
export interface ReconnectPolicy {
|
3
|
+
/** Called after disconnect was detected
|
4
|
+
*
|
5
|
+
* @returns {number | null} Amount of time in milliseconds to delay the next reconnect attempt, `null` signals to stop retrying.
|
6
|
+
*/
|
7
|
+
nextRetryDelayInMs(context: ReconnectContext): number | null;
|
8
|
+
}
|
9
|
+
|
10
|
+
export interface ReconnectContext {
|
11
|
+
/**
|
12
|
+
* Number of failed reconnect attempts
|
13
|
+
*/
|
14
|
+
readonly retryCount: number;
|
15
|
+
|
16
|
+
/**
|
17
|
+
* Elapsed amount of time in milliseconds since the disconnect.
|
18
|
+
*/
|
19
|
+
readonly elapsedMs: number;
|
20
|
+
|
21
|
+
/**
|
22
|
+
* Reason for retrying
|
23
|
+
*/
|
24
|
+
readonly retryReason?: Error;
|
25
|
+
}
|
package/src/room/Room.ts
CHANGED
@@ -134,9 +134,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
134
134
|
return;
|
135
135
|
}
|
136
136
|
|
137
|
-
this.engine = new RTCEngine();
|
137
|
+
this.engine = new RTCEngine(this.options);
|
138
138
|
|
139
|
-
this.engine.client.signalLatency = this.options.expSignalLatency;
|
140
139
|
this.engine.client.onParticipantUpdate = this.handleParticipantUpdates;
|
141
140
|
this.engine.client.onRoomUpdate = this.handleRoomUpdate;
|
142
141
|
this.engine.client.onSpeakersChanged = this.handleSpeakersChanged;
|
@@ -255,36 +254,16 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
255
254
|
this.localParticipant.updateInfo(pi);
|
256
255
|
// forward metadata changed for the local participant
|
257
256
|
this.localParticipant
|
258
|
-
.on(ParticipantEvent.ParticipantMetadataChanged,
|
259
|
-
|
260
|
-
|
261
|
-
.on(ParticipantEvent.
|
262
|
-
|
263
|
-
|
264
|
-
.on(ParticipantEvent.
|
265
|
-
this.emit(RoomEvent.TrackUnmuted, pub, this.localParticipant);
|
266
|
-
})
|
267
|
-
.on(ParticipantEvent.LocalTrackPublished, (pub: LocalTrackPublication) => {
|
268
|
-
this.emit(RoomEvent.LocalTrackPublished, pub, this.localParticipant);
|
269
|
-
})
|
270
|
-
.on(ParticipantEvent.LocalTrackUnpublished, (pub: LocalTrackPublication) => {
|
271
|
-
this.emit(RoomEvent.LocalTrackUnpublished, pub, this.localParticipant);
|
272
|
-
})
|
273
|
-
.on(ParticipantEvent.ConnectionQualityChanged, (quality: ConnectionQuality) => {
|
274
|
-
this.emit(RoomEvent.ConnectionQualityChanged, quality, this.localParticipant);
|
275
|
-
})
|
276
|
-
.on(ParticipantEvent.MediaDevicesError, (e: Error) => {
|
277
|
-
this.emit(RoomEvent.MediaDevicesError, e);
|
278
|
-
})
|
257
|
+
.on(ParticipantEvent.ParticipantMetadataChanged, this.onLocalParticipantMetadataChanged)
|
258
|
+
.on(ParticipantEvent.TrackMuted, this.onLocalTrackMuted)
|
259
|
+
.on(ParticipantEvent.TrackUnmuted, this.onLocalTrackUnmuted)
|
260
|
+
.on(ParticipantEvent.LocalTrackPublished, this.onLocalTrackPublished)
|
261
|
+
.on(ParticipantEvent.LocalTrackUnpublished, this.onLocalTrackUnpublished)
|
262
|
+
.on(ParticipantEvent.ConnectionQualityChanged, this.onLocalConnectionQualityChanged)
|
263
|
+
.on(ParticipantEvent.MediaDevicesError, this.onMediaDevicesError)
|
279
264
|
.on(
|
280
265
|
ParticipantEvent.ParticipantPermissionsChanged,
|
281
|
-
|
282
|
-
this.emit(
|
283
|
-
RoomEvent.ParticipantPermissionsChanged,
|
284
|
-
prevPermissions,
|
285
|
-
this.localParticipant,
|
286
|
-
);
|
287
|
-
},
|
266
|
+
this.onLocalParticipantPermissionsChanged,
|
288
267
|
);
|
289
268
|
|
290
269
|
// populate remote participants, these should not trigger new events
|
@@ -657,6 +636,19 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
657
636
|
});
|
658
637
|
});
|
659
638
|
|
639
|
+
this.localParticipant
|
640
|
+
.off(ParticipantEvent.ParticipantMetadataChanged, this.onLocalParticipantMetadataChanged)
|
641
|
+
.off(ParticipantEvent.TrackMuted, this.onLocalTrackMuted)
|
642
|
+
.off(ParticipantEvent.TrackUnmuted, this.onLocalTrackUnmuted)
|
643
|
+
.off(ParticipantEvent.LocalTrackPublished, this.onLocalTrackPublished)
|
644
|
+
.off(ParticipantEvent.LocalTrackUnpublished, this.onLocalTrackUnpublished)
|
645
|
+
.off(ParticipantEvent.ConnectionQualityChanged, this.onLocalConnectionQualityChanged)
|
646
|
+
.off(ParticipantEvent.MediaDevicesError, this.onMediaDevicesError)
|
647
|
+
.off(
|
648
|
+
ParticipantEvent.ParticipantPermissionsChanged,
|
649
|
+
this.onLocalParticipantPermissionsChanged,
|
650
|
+
);
|
651
|
+
|
660
652
|
this.localParticipant.tracks.forEach((pub) => {
|
661
653
|
if (pub.track) {
|
662
654
|
this.localParticipant.unpublishTrack(pub.track, shouldStopTracks);
|
@@ -666,6 +658,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
666
658
|
pub.track?.stop();
|
667
659
|
}
|
668
660
|
});
|
661
|
+
|
669
662
|
this.localParticipant.tracks.clear();
|
670
663
|
this.localParticipant.videoTracks.clear();
|
671
664
|
this.localParticipant.audioTracks.clear();
|
@@ -1081,6 +1074,38 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1081
1074
|
return false;
|
1082
1075
|
}
|
1083
1076
|
|
1077
|
+
private onLocalParticipantMetadataChanged = (metadata: string | undefined) => {
|
1078
|
+
this.emit(RoomEvent.ParticipantMetadataChanged, metadata, this.localParticipant);
|
1079
|
+
};
|
1080
|
+
|
1081
|
+
private onLocalTrackMuted = (pub: TrackPublication) => {
|
1082
|
+
this.emit(RoomEvent.TrackMuted, pub, this.localParticipant);
|
1083
|
+
};
|
1084
|
+
|
1085
|
+
private onLocalTrackUnmuted = (pub: TrackPublication) => {
|
1086
|
+
this.emit(RoomEvent.TrackUnmuted, pub, this.localParticipant);
|
1087
|
+
};
|
1088
|
+
|
1089
|
+
private onLocalTrackPublished = (pub: LocalTrackPublication) => {
|
1090
|
+
this.emit(RoomEvent.LocalTrackPublished, pub, this.localParticipant);
|
1091
|
+
};
|
1092
|
+
|
1093
|
+
private onLocalTrackUnpublished = (pub: LocalTrackPublication) => {
|
1094
|
+
this.emit(RoomEvent.LocalTrackUnpublished, pub, this.localParticipant);
|
1095
|
+
};
|
1096
|
+
|
1097
|
+
private onLocalConnectionQualityChanged = (quality: ConnectionQuality) => {
|
1098
|
+
this.emit(RoomEvent.ConnectionQualityChanged, quality, this.localParticipant);
|
1099
|
+
};
|
1100
|
+
|
1101
|
+
private onMediaDevicesError = (e: Error) => {
|
1102
|
+
this.emit(RoomEvent.MediaDevicesError, e);
|
1103
|
+
};
|
1104
|
+
|
1105
|
+
private onLocalParticipantPermissionsChanged = (prevPermissions: ParticipantPermission) => {
|
1106
|
+
this.emit(RoomEvent.ParticipantPermissionsChanged, prevPermissions, this.localParticipant);
|
1107
|
+
};
|
1108
|
+
|
1084
1109
|
// /** @internal */
|
1085
1110
|
emit<E extends keyof RoomEventCallbacks>(
|
1086
1111
|
event: E,
|
package/src/room/events.ts
CHANGED
@@ -218,8 +218,8 @@ export enum RoomEvent {
|
|
218
218
|
* When we have encountered an error while attempting to create a track.
|
219
219
|
* The errors take place in getUserMedia().
|
220
220
|
* Use MediaDeviceFailure.getFailure(error) to get the reason of failure.
|
221
|
-
* [[
|
222
|
-
* an error while creating the audio or video track respectively.
|
221
|
+
* [[LocalParticipant.lastCameraError]] and [[LocalParticipant.lastMicrophoneError]]
|
222
|
+
* will indicate if it had an error while creating the audio or video track respectively.
|
223
223
|
*
|
224
224
|
* args: (error: Error)
|
225
225
|
*/
|
@@ -434,6 +434,23 @@ export default class LocalParticipant extends Participant {
|
|
434
434
|
if (opts.source) {
|
435
435
|
track.source = opts.source;
|
436
436
|
}
|
437
|
+
const existingTrackOfSource = Array.from(this.tracks.values()).find(
|
438
|
+
(publishedTrack) => track instanceof LocalTrack && publishedTrack.source === track.source,
|
439
|
+
);
|
440
|
+
if (existingTrackOfSource) {
|
441
|
+
try {
|
442
|
+
// throw an Error in order to capture the stack trace
|
443
|
+
throw Error(`publishing a second track with the same source: ${track.source}`);
|
444
|
+
} catch (e: unknown) {
|
445
|
+
if (e instanceof Error) {
|
446
|
+
log.warn(e.message, {
|
447
|
+
oldTrack: existingTrackOfSource,
|
448
|
+
newTrack: track,
|
449
|
+
trace: e.stack,
|
450
|
+
});
|
451
|
+
}
|
452
|
+
}
|
453
|
+
}
|
437
454
|
if (opts.stopMicTrackOnMute && track instanceof LocalAudioTrack) {
|
438
455
|
track.stopOnMute = true;
|
439
456
|
}
|
@@ -686,8 +703,6 @@ export default class LocalParticipant extends Participant {
|
|
686
703
|
}
|
687
704
|
|
688
705
|
track = publication.track;
|
689
|
-
|
690
|
-
track.sender = undefined;
|
691
706
|
track.off(TrackEvent.Muted, this.onTrackMuted);
|
692
707
|
track.off(TrackEvent.Unmuted, this.onTrackUnmuted);
|
693
708
|
track.off(TrackEvent.Ended, this.handleTrackEnded);
|
@@ -701,22 +716,21 @@ export default class LocalParticipant extends Participant {
|
|
701
716
|
track.stop();
|
702
717
|
}
|
703
718
|
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
}
|
716
|
-
}
|
717
|
-
});
|
719
|
+
if (
|
720
|
+
this.engine.publisher &&
|
721
|
+
this.engine.publisher.pc.connectionState !== 'closed' &&
|
722
|
+
track.sender
|
723
|
+
) {
|
724
|
+
try {
|
725
|
+
this.engine.publisher.pc.removeTrack(track.sender);
|
726
|
+
this.engine.negotiate();
|
727
|
+
} catch (e) {
|
728
|
+
log.warn('failed to remove track', { error: e, method: 'unpublishTrack' });
|
729
|
+
}
|
718
730
|
}
|
719
731
|
|
732
|
+
track.sender = undefined;
|
733
|
+
|
720
734
|
// remove from our maps
|
721
735
|
this.tracks.delete(publication.trackSid);
|
722
736
|
switch (publication.kind) {
|
@@ -220,6 +220,19 @@ export default class RemoteParticipant extends Participant {
|
|
220
220
|
// always emit events for new publications, Room will not forward them unless it's ready
|
221
221
|
newTracks.forEach((publication) => {
|
222
222
|
this.emit(ParticipantEvent.TrackPublished, publication);
|
223
|
+
const existingTrackOfSource = Array.from(this.tracks.values()).find(
|
224
|
+
(publishedTrack) => publishedTrack.source === publication.source,
|
225
|
+
);
|
226
|
+
if (existingTrackOfSource) {
|
227
|
+
log.warn(
|
228
|
+
`received a second track publication for ${this.identity} with the same source: ${publication.source}`,
|
229
|
+
{
|
230
|
+
oldTrack: existingTrackOfSource,
|
231
|
+
newTrack: publication,
|
232
|
+
participant: this,
|
233
|
+
},
|
234
|
+
);
|
235
|
+
}
|
223
236
|
});
|
224
237
|
|
225
238
|
// detect removed tracks
|