livekit-client 1.2.0 → 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 +1007 -107
- 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 +10 -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 +3 -3
- 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 +113 -0
- 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 +56 -20
- package/src/room/participant/RemoteParticipant.ts +13 -0
- package/src/room/participant/publishUtils.ts +1 -1
- 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
|
*/
|
@@ -125,8 +125,9 @@ export default class LocalParticipant extends Participant {
|
|
125
125
|
setCameraEnabled(
|
126
126
|
enabled: boolean,
|
127
127
|
options?: VideoCaptureOptions,
|
128
|
+
publishOptions?: TrackPublishOptions,
|
128
129
|
): Promise<LocalTrackPublication | undefined> {
|
129
|
-
return this.setTrackEnabled(Track.Source.Camera, enabled, options);
|
130
|
+
return this.setTrackEnabled(Track.Source.Camera, enabled, options, publishOptions);
|
130
131
|
}
|
131
132
|
|
132
133
|
/**
|
@@ -138,8 +139,9 @@ export default class LocalParticipant extends Participant {
|
|
138
139
|
setMicrophoneEnabled(
|
139
140
|
enabled: boolean,
|
140
141
|
options?: AudioCaptureOptions,
|
142
|
+
publishOptions?: TrackPublishOptions,
|
141
143
|
): Promise<LocalTrackPublication | undefined> {
|
142
|
-
return this.setTrackEnabled(Track.Source.Microphone, enabled, options);
|
144
|
+
return this.setTrackEnabled(Track.Source.Microphone, enabled, options, publishOptions);
|
143
145
|
}
|
144
146
|
|
145
147
|
/**
|
@@ -149,8 +151,9 @@ export default class LocalParticipant extends Participant {
|
|
149
151
|
setScreenShareEnabled(
|
150
152
|
enabled: boolean,
|
151
153
|
options?: ScreenShareCaptureOptions,
|
154
|
+
publishOptions?: TrackPublishOptions,
|
152
155
|
): Promise<LocalTrackPublication | undefined> {
|
153
|
-
return this.setTrackEnabled(Track.Source.ScreenShare, enabled, options);
|
156
|
+
return this.setTrackEnabled(Track.Source.ScreenShare, enabled, options, publishOptions);
|
154
157
|
}
|
155
158
|
|
156
159
|
/** @internal */
|
@@ -172,21 +175,25 @@ export default class LocalParticipant extends Participant {
|
|
172
175
|
source: Extract<Track.Source, Track.Source.Camera>,
|
173
176
|
enabled: boolean,
|
174
177
|
options?: VideoCaptureOptions,
|
178
|
+
publishOptions?: TrackPublishOptions,
|
175
179
|
): Promise<LocalTrackPublication | undefined>;
|
176
180
|
private async setTrackEnabled(
|
177
181
|
source: Extract<Track.Source, Track.Source.Microphone>,
|
178
182
|
enabled: boolean,
|
179
183
|
options?: AudioCaptureOptions,
|
184
|
+
publishOptions?: TrackPublishOptions,
|
180
185
|
): Promise<LocalTrackPublication | undefined>;
|
181
186
|
private async setTrackEnabled(
|
182
187
|
source: Extract<Track.Source, Track.Source.ScreenShare>,
|
183
188
|
enabled: boolean,
|
184
189
|
options?: ScreenShareCaptureOptions,
|
190
|
+
publishOptions?: TrackPublishOptions,
|
185
191
|
): Promise<LocalTrackPublication | undefined>;
|
186
192
|
private async setTrackEnabled(
|
187
193
|
source: Track.Source,
|
188
194
|
enabled: true,
|
189
195
|
options?: VideoCaptureOptions | AudioCaptureOptions | ScreenShareCaptureOptions,
|
196
|
+
publishOptions?: TrackPublishOptions,
|
190
197
|
) {
|
191
198
|
log.debug('setTrackEnabled', { source, enabled });
|
192
199
|
let track = this.getTrack(source);
|
@@ -224,7 +231,7 @@ export default class LocalParticipant extends Participant {
|
|
224
231
|
}
|
225
232
|
const publishPromises: Array<Promise<LocalTrackPublication>> = [];
|
226
233
|
for (const localTrack of localTracks) {
|
227
|
-
publishPromises.push(this.publishTrack(localTrack));
|
234
|
+
publishPromises.push(this.publishTrack(localTrack, publishOptions));
|
228
235
|
}
|
229
236
|
const publishedTracks = await Promise.all(publishPromises);
|
230
237
|
// for screen share publications including audio, this will only return the screen share publication, not the screen share audio one
|
@@ -427,6 +434,23 @@ export default class LocalParticipant extends Participant {
|
|
427
434
|
if (opts.source) {
|
428
435
|
track.source = opts.source;
|
429
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
|
+
}
|
430
454
|
if (opts.stopMicTrackOnMute && track instanceof LocalAudioTrack) {
|
431
455
|
track.stopOnMute = true;
|
432
456
|
}
|
@@ -544,6 +568,14 @@ export default class LocalParticipant extends Participant {
|
|
544
568
|
track.codec = opts.videoCodec;
|
545
569
|
}
|
546
570
|
|
571
|
+
if (track.codec === 'av1' && encodings && encodings[0]?.maxBitrate) {
|
572
|
+
this.engine.publisher.setTrackCodecBitrate(
|
573
|
+
req.cid,
|
574
|
+
track.codec,
|
575
|
+
encodings[0].maxBitrate / 1000,
|
576
|
+
);
|
577
|
+
}
|
578
|
+
|
547
579
|
this.engine.negotiate();
|
548
580
|
|
549
581
|
// store RTPSender
|
@@ -642,6 +674,13 @@ export default class LocalParticipant extends Participant {
|
|
642
674
|
this.setPreferredCodec(transceiver, track.kind, opts.videoCodec);
|
643
675
|
track.setSimulcastTrackSender(opts.videoCodec, transceiver.sender);
|
644
676
|
|
677
|
+
if (videoCodec === 'av1' && encodings[0]?.maxBitrate) {
|
678
|
+
this.engine.publisher.setTrackCodecBitrate(
|
679
|
+
req.cid,
|
680
|
+
videoCodec,
|
681
|
+
encodings[0].maxBitrate / 1000,
|
682
|
+
);
|
683
|
+
}
|
645
684
|
this.engine.negotiate();
|
646
685
|
log.debug(`published ${opts.videoCodec} for track ${track.sid}`, { encodings, trackInfo: ti });
|
647
686
|
}
|
@@ -664,8 +703,6 @@ export default class LocalParticipant extends Participant {
|
|
664
703
|
}
|
665
704
|
|
666
705
|
track = publication.track;
|
667
|
-
|
668
|
-
track.sender = undefined;
|
669
706
|
track.off(TrackEvent.Muted, this.onTrackMuted);
|
670
707
|
track.off(TrackEvent.Unmuted, this.onTrackUnmuted);
|
671
708
|
track.off(TrackEvent.Ended, this.handleTrackEnded);
|
@@ -679,22 +716,21 @@ export default class LocalParticipant extends Participant {
|
|
679
716
|
track.stop();
|
680
717
|
}
|
681
718
|
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
}
|
694
|
-
}
|
695
|
-
});
|
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
|
+
}
|
696
730
|
}
|
697
731
|
|
732
|
+
track.sender = undefined;
|
733
|
+
|
698
734
|
// remove from our maps
|
699
735
|
this.tracks.delete(publication.trackSid);
|
700
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
|
@@ -106,7 +106,7 @@ export function computeVideoEncodings(
|
|
106
106
|
encodings.push({
|
107
107
|
rid: videoRids[2 - i],
|
108
108
|
scaleResolutionDownBy: 2 ** i,
|
109
|
-
maxBitrate: videoEncoding ? videoEncoding.maxBitrate /
|
109
|
+
maxBitrate: videoEncoding ? videoEncoding.maxBitrate / 3 ** i : 0,
|
110
110
|
/* @ts-ignore */
|
111
111
|
maxFramerate: original.encoding.maxFramerate,
|
112
112
|
/* @ts-ignore */
|