livekit-client 0.15.4 → 0.16.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/dist/api/SignalClient.d.ts +3 -2
- package/dist/api/SignalClient.js +11 -3
- package/dist/api/SignalClient.js.map +1 -1
- package/dist/proto/livekit_rtc.d.ts +15 -10
- package/dist/proto/livekit_rtc.js +36 -22
- package/dist/proto/livekit_rtc.js.map +1 -1
- package/dist/room/RTCEngine.d.ts +8 -2
- package/dist/room/RTCEngine.js +137 -42
- package/dist/room/RTCEngine.js.map +1 -1
- package/dist/room/Room.d.ts +7 -0
- package/dist/room/Room.js +66 -16
- package/dist/room/Room.js.map +1 -1
- package/dist/room/events.d.ts +5 -3
- package/dist/room/events.js +5 -3
- package/dist/room/events.js.map +1 -1
- package/dist/room/participant/LocalParticipant.d.ts +1 -2
- package/dist/room/participant/LocalParticipant.js +5 -5
- package/dist/room/participant/LocalParticipant.js.map +1 -1
- package/dist/room/participant/RemoteParticipant.js +3 -0
- package/dist/room/participant/RemoteParticipant.js.map +1 -1
- package/dist/room/track/LocalTrackPublication.d.ts +2 -0
- package/dist/room/track/LocalTrackPublication.js.map +1 -1
- package/dist/room/track/RemoteTrackPublication.d.ts +1 -1
- package/dist/room/track/RemoteTrackPublication.js +7 -1
- package/dist/room/track/RemoteTrackPublication.js.map +1 -1
- package/dist/room/track/Track.js +9 -5
- package/dist/room/track/Track.js.map +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/package.json +2 -2
- package/src/api/SignalClient.ts +14 -4
- package/src/proto/livekit_rtc.ts +55 -41
- package/src/room/RTCEngine.ts +145 -47
- package/src/room/Room.ts +92 -37
- package/src/room/events.ts +5 -3
- package/src/room/participant/LocalParticipant.ts +5 -5
- package/src/room/participant/RemoteParticipant.ts +3 -0
- package/src/room/track/LocalTrackPublication.ts +3 -0
- package/src/room/track/RemoteTrackPublication.ts +8 -2
- package/src/room/track/Track.ts +11 -5
- package/src/version.ts +2 -2
package/src/room/RTCEngine.ts
CHANGED
@@ -4,6 +4,7 @@ import log from '../logger';
|
|
4
4
|
import { DataPacket, DataPacket_Kind, TrackInfo } from '../proto/livekit_models';
|
5
5
|
import {
|
6
6
|
AddTrackRequest, JoinResponse,
|
7
|
+
LeaveRequest,
|
7
8
|
SignalTarget,
|
8
9
|
TrackPublishedResponse,
|
9
10
|
} from '../proto/livekit_rtc';
|
@@ -14,8 +15,10 @@ import { sleep } from './utils';
|
|
14
15
|
|
15
16
|
const lossyDataChannel = '_lossy';
|
16
17
|
const reliableDataChannel = '_reliable';
|
17
|
-
const maxReconnectRetries =
|
18
|
-
|
18
|
+
const maxReconnectRetries = 10;
|
19
|
+
const minReconnectWait = 1 * 1000;
|
20
|
+
const maxReconnectDuration = 60 * 1000;
|
21
|
+
export const maxICEConnectTimeout = 15 * 1000;
|
19
22
|
|
20
23
|
/** @internal */
|
21
24
|
export default class RTCEngine extends EventEmitter {
|
@@ -39,7 +42,9 @@ export default class RTCEngine extends EventEmitter {
|
|
39
42
|
|
40
43
|
private subscriberPrimary: boolean = false;
|
41
44
|
|
42
|
-
private
|
45
|
+
private primaryPC?: RTCPeerConnection;
|
46
|
+
|
47
|
+
private pcConnected: boolean = false;
|
43
48
|
|
44
49
|
private isClosed: boolean = true;
|
45
50
|
|
@@ -54,8 +59,14 @@ export default class RTCEngine extends EventEmitter {
|
|
54
59
|
|
55
60
|
private token?: string;
|
56
61
|
|
62
|
+
private signalOpts?: SignalOptions;
|
63
|
+
|
57
64
|
private reconnectAttempts: number = 0;
|
58
65
|
|
66
|
+
private reconnectStart: number = 0;
|
67
|
+
|
68
|
+
private fullReconnect: boolean = false;
|
69
|
+
|
59
70
|
private connectedServerAddr?: string;
|
60
71
|
|
61
72
|
constructor() {
|
@@ -66,9 +77,9 @@ export default class RTCEngine extends EventEmitter {
|
|
66
77
|
async join(url: string, token: string, opts?: SignalOptions): Promise<JoinResponse> {
|
67
78
|
this.url = url;
|
68
79
|
this.token = token;
|
80
|
+
this.signalOpts = opts;
|
69
81
|
|
70
82
|
const joinResponse = await this.client.join(url, token, opts);
|
71
|
-
this.emit(EngineEvent.SignalConnected);
|
72
83
|
this.isClosed = false;
|
73
84
|
|
74
85
|
this.subscriberPrimary = joinResponse.subscriberPrimary;
|
@@ -174,21 +185,22 @@ export default class RTCEngine extends EventEmitter {
|
|
174
185
|
// in subscriber primary mode, server side opens sub data channels.
|
175
186
|
this.subscriber.pc.ondatachannel = this.handleDataChannel;
|
176
187
|
}
|
177
|
-
primaryPC
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
188
|
+
this.primaryPC = primaryPC;
|
189
|
+
primaryPC.onconnectionstatechange = () => {
|
190
|
+
if (primaryPC.connectionState === 'connected') {
|
191
|
+
log.trace('pc connected');
|
192
|
+
if (!this.pcConnected) {
|
193
|
+
this.pcConnected = true;
|
182
194
|
this.emit(EngineEvent.Connected);
|
183
195
|
}
|
184
196
|
getConnectedAddress(primaryPC).then((v) => {
|
185
197
|
this.connectedServerAddr = v;
|
186
198
|
});
|
187
|
-
} else if (primaryPC.
|
199
|
+
} else if (primaryPC.connectionState === 'failed') {
|
188
200
|
// on Safari, PeerConnection will switch to 'disconnected' during renegotiation
|
189
|
-
log.trace('
|
190
|
-
if (this.
|
191
|
-
this.
|
201
|
+
log.trace('pc disconnected');
|
202
|
+
if (this.pcConnected) {
|
203
|
+
this.pcConnected = false;
|
192
204
|
|
193
205
|
this.handleDisconnect('peerconnection');
|
194
206
|
}
|
@@ -268,13 +280,22 @@ export default class RTCEngine extends EventEmitter {
|
|
268
280
|
resolve(res.track!);
|
269
281
|
};
|
270
282
|
|
283
|
+
this.client.onTokenRefresh = (token: string) => {
|
284
|
+
this.token = token;
|
285
|
+
};
|
286
|
+
|
271
287
|
this.client.onClose = () => {
|
272
288
|
this.handleDisconnect('signal');
|
273
289
|
};
|
274
290
|
|
275
|
-
this.client.onLeave = () => {
|
276
|
-
|
277
|
-
|
291
|
+
this.client.onLeave = (leave?: LeaveRequest) => {
|
292
|
+
if (leave?.canReconnect) {
|
293
|
+
this.fullReconnect = true;
|
294
|
+
this.primaryPC = undefined;
|
295
|
+
} else {
|
296
|
+
this.emit(EngineEvent.Disconnected);
|
297
|
+
this.close();
|
298
|
+
}
|
278
299
|
};
|
279
300
|
}
|
280
301
|
|
@@ -320,48 +341,106 @@ export default class RTCEngine extends EventEmitter {
|
|
320
341
|
return;
|
321
342
|
}
|
322
343
|
log.debug(`${connection} disconnected`);
|
323
|
-
if (this.reconnectAttempts
|
324
|
-
|
325
|
-
|
326
|
-
maxReconnectRetries,
|
327
|
-
'attempts. giving up',
|
328
|
-
);
|
329
|
-
this.emit(EngineEvent.Disconnected);
|
330
|
-
this.close();
|
331
|
-
return;
|
344
|
+
if (this.reconnectAttempts === 0) {
|
345
|
+
// only reset start time on the first try
|
346
|
+
this.reconnectStart = Date.now();
|
332
347
|
}
|
333
348
|
|
334
349
|
const delay = (this.reconnectAttempts * this.reconnectAttempts) * 300;
|
335
|
-
setTimeout(() => {
|
336
|
-
this.
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
350
|
+
setTimeout(async () => {
|
351
|
+
if (this.isClosed) {
|
352
|
+
return;
|
353
|
+
}
|
354
|
+
|
355
|
+
try {
|
356
|
+
if (this.fullReconnect) {
|
357
|
+
await this.restartConnection();
|
358
|
+
} else {
|
359
|
+
await this.resumeConnection();
|
360
|
+
}
|
361
|
+
this.reconnectAttempts = 0;
|
362
|
+
this.fullReconnect = false;
|
363
|
+
} catch (e) {
|
364
|
+
this.reconnectAttempts += 1;
|
365
|
+
let recoverable = true;
|
366
|
+
if (e instanceof UnexpectedConnectionState) {
|
367
|
+
log.debug('received unrecoverable error', e.message);
|
368
|
+
// unrecoverable
|
369
|
+
recoverable = false;
|
370
|
+
} else if (!(e instanceof SignalReconnectError)) {
|
371
|
+
// cannot resume
|
372
|
+
this.fullReconnect = true;
|
373
|
+
}
|
374
|
+
|
375
|
+
const duration = Date.now() - this.reconnectStart;
|
376
|
+
if (this.reconnectAttempts >= maxReconnectRetries || duration > maxReconnectDuration) {
|
377
|
+
recoverable = false;
|
378
|
+
}
|
379
|
+
|
380
|
+
if (recoverable) {
|
381
|
+
this.handleDisconnect('reconnect');
|
382
|
+
} else {
|
383
|
+
log.info(
|
384
|
+
`could not recover connection after ${maxReconnectRetries} attempts, ${duration}ms. giving up`,
|
385
|
+
);
|
386
|
+
this.emit(EngineEvent.Disconnected);
|
387
|
+
this.close();
|
388
|
+
}
|
389
|
+
}
|
341
390
|
}, delay);
|
342
391
|
};
|
343
392
|
|
344
|
-
private async
|
345
|
-
if (this.isClosed) {
|
346
|
-
return;
|
347
|
-
}
|
393
|
+
private async restartConnection() {
|
348
394
|
if (!this.url || !this.token) {
|
349
|
-
|
395
|
+
// permanent failure, don't attempt reconnection
|
396
|
+
throw new UnexpectedConnectionState('could not reconnect, url or token not saved');
|
350
397
|
}
|
351
|
-
log.info('reconnecting to signal connection, attempt', this.reconnectAttempts);
|
352
398
|
|
399
|
+
log.info('reconnecting, attempt', this.reconnectAttempts);
|
353
400
|
if (this.reconnectAttempts === 0) {
|
354
|
-
this.emit(EngineEvent.
|
401
|
+
this.emit(EngineEvent.Restarting);
|
402
|
+
}
|
403
|
+
|
404
|
+
this.primaryPC = undefined;
|
405
|
+
this.publisher?.close();
|
406
|
+
this.publisher = undefined;
|
407
|
+
this.subscriber?.close();
|
408
|
+
this.subscriber = undefined;
|
409
|
+
|
410
|
+
let joinResponse: JoinResponse;
|
411
|
+
try {
|
412
|
+
joinResponse = await this.join(this.url, this.token, this.signalOpts);
|
413
|
+
} catch (e) {
|
414
|
+
throw new SignalReconnectError();
|
355
415
|
}
|
356
|
-
this.reconnectAttempts += 1;
|
357
416
|
|
358
|
-
await this.
|
359
|
-
|
417
|
+
await this.waitForPCConnected();
|
418
|
+
|
419
|
+
// reconnect success
|
420
|
+
this.emit(EngineEvent.Restarted, joinResponse);
|
421
|
+
}
|
360
422
|
|
423
|
+
private async resumeConnection(): Promise<void> {
|
424
|
+
if (!this.url || !this.token) {
|
425
|
+
// permanent failure, don't attempt reconnection
|
426
|
+
throw new UnexpectedConnectionState('could not reconnect, url or token not saved');
|
427
|
+
}
|
361
428
|
// trigger publisher reconnect
|
362
429
|
if (!this.publisher || !this.subscriber) {
|
363
430
|
throw new UnexpectedConnectionState('publisher and subscriber connections unset');
|
364
431
|
}
|
432
|
+
log.info('resuming signal connection, attempt', this.reconnectAttempts);
|
433
|
+
if (this.reconnectAttempts === 0) {
|
434
|
+
this.emit(EngineEvent.Resuming);
|
435
|
+
}
|
436
|
+
|
437
|
+
try {
|
438
|
+
await this.client.reconnect(this.url, this.token);
|
439
|
+
} catch (e) {
|
440
|
+
throw new SignalReconnectError();
|
441
|
+
}
|
442
|
+
this.emit(EngineEvent.SignalResumed);
|
443
|
+
|
365
444
|
this.subscriber.restartingIce = true;
|
366
445
|
|
367
446
|
// only restart publisher if it's needed
|
@@ -369,19 +448,35 @@ export default class RTCEngine extends EventEmitter {
|
|
369
448
|
await this.publisher.createAndSendOffer({ iceRestart: true });
|
370
449
|
}
|
371
450
|
|
372
|
-
|
451
|
+
await this.waitForPCConnected();
|
452
|
+
|
453
|
+
// resume success
|
454
|
+
this.emit(EngineEvent.Resumed);
|
455
|
+
}
|
373
456
|
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
457
|
+
async waitForPCConnected() {
|
458
|
+
const startTime = (new Date()).getTime();
|
459
|
+
let now = startTime;
|
460
|
+
this.pcConnected = false;
|
461
|
+
|
462
|
+
while (now - startTime < maxICEConnectTimeout) {
|
463
|
+
// if there is no connectionstatechange callback fired
|
464
|
+
// check connectionstate after minReconnectWait
|
465
|
+
if (this.primaryPC === undefined) {
|
466
|
+
// we can abort early, connection is hosed
|
467
|
+
break;
|
468
|
+
} else if (now - startTime > minReconnectWait && this.primaryPC?.connectionState === 'connected') {
|
469
|
+
this.pcConnected = true;
|
470
|
+
}
|
471
|
+
if (this.pcConnected) {
|
378
472
|
return;
|
379
473
|
}
|
380
474
|
await sleep(100);
|
475
|
+
now = (new Date()).getTime();
|
381
476
|
}
|
382
477
|
|
383
478
|
// have not reconnected, throw
|
384
|
-
throw new ConnectionError('could not establish
|
479
|
+
throw new ConnectionError('could not establish PC connection');
|
385
480
|
}
|
386
481
|
|
387
482
|
/* @internal */
|
@@ -482,3 +577,6 @@ async function getConnectedAddress(pc: RTCPeerConnection): Promise<string | unde
|
|
482
577
|
}
|
483
578
|
return candidates.get(selectedID);
|
484
579
|
}
|
580
|
+
|
581
|
+
class SignalReconnectError extends Error {
|
582
|
+
}
|
package/src/room/Room.ts
CHANGED
@@ -7,7 +7,11 @@ import {
|
|
7
7
|
ParticipantInfo_State, Room as RoomModel, SpeakerInfo, UserPacket,
|
8
8
|
} from '../proto/livekit_models';
|
9
9
|
import {
|
10
|
-
ConnectionQualityUpdate,
|
10
|
+
ConnectionQualityUpdate,
|
11
|
+
JoinResponse,
|
12
|
+
SimulateScenario,
|
13
|
+
StreamStateUpdate,
|
14
|
+
SubscriptionPermissionUpdate,
|
11
15
|
} from '../proto/livekit_rtc';
|
12
16
|
import DeviceManager from './DeviceManager';
|
13
17
|
import { ConnectionError, UnsupportedServer } from './errors';
|
@@ -113,48 +117,47 @@ class Room extends EventEmitter {
|
|
113
117
|
}
|
114
118
|
|
115
119
|
this.engine = new RTCEngine();
|
116
|
-
this.engine.client.signalLatency = this.options.expSignalLatency;
|
117
|
-
|
118
|
-
this.engine.on(
|
119
|
-
EngineEvent.MediaTrackAdded,
|
120
|
-
(
|
121
|
-
mediaTrack: MediaStreamTrack,
|
122
|
-
stream: MediaStream,
|
123
|
-
receiver?: RTCRtpReceiver,
|
124
|
-
) => {
|
125
|
-
this.onTrackAdded(mediaTrack, stream, receiver);
|
126
|
-
},
|
127
|
-
);
|
128
|
-
|
129
|
-
this.engine.on(EngineEvent.Disconnected, () => {
|
130
|
-
this.handleDisconnect();
|
131
|
-
});
|
132
120
|
|
121
|
+
this.engine.client.signalLatency = this.options.expSignalLatency;
|
133
122
|
this.engine.client.onParticipantUpdate = this.handleParticipantUpdates;
|
134
123
|
this.engine.client.onRoomUpdate = this.handleRoomUpdate;
|
135
124
|
this.engine.client.onSpeakersChanged = this.handleSpeakersChanged;
|
136
125
|
this.engine.client.onStreamStateUpdate = this.handleStreamStateUpdate;
|
137
126
|
this.engine.client.onSubscriptionPermissionUpdate = this.handleSubscriptionPermissionUpdate;
|
138
|
-
this.engine.on(EngineEvent.ActiveSpeakersUpdate, this.handleActiveSpeakersUpdate);
|
139
|
-
this.engine.on(EngineEvent.DataPacketReceived, this.handleDataPacket);
|
140
|
-
|
141
|
-
this.engine.on(EngineEvent.Reconnecting, () => {
|
142
|
-
this.state = RoomState.Reconnecting;
|
143
|
-
this.emit(RoomEvent.Reconnecting);
|
144
|
-
});
|
145
|
-
|
146
|
-
this.engine.on(EngineEvent.Reconnected, () => {
|
147
|
-
this.state = RoomState.Connected;
|
148
|
-
this.emit(RoomEvent.Reconnected);
|
149
|
-
});
|
150
|
-
|
151
|
-
this.engine.on(EngineEvent.SignalConnected, () => {
|
152
|
-
if (this.state === RoomState.Reconnecting) {
|
153
|
-
this.sendSyncState();
|
154
|
-
}
|
155
|
-
});
|
156
|
-
|
157
127
|
this.engine.client.onConnectionQuality = this.handleConnectionQualityUpdate;
|
128
|
+
|
129
|
+
this.engine
|
130
|
+
.on(
|
131
|
+
EngineEvent.MediaTrackAdded,
|
132
|
+
(
|
133
|
+
mediaTrack: MediaStreamTrack,
|
134
|
+
stream: MediaStream,
|
135
|
+
receiver?: RTCRtpReceiver,
|
136
|
+
) => {
|
137
|
+
this.onTrackAdded(mediaTrack, stream, receiver);
|
138
|
+
},
|
139
|
+
)
|
140
|
+
.on(EngineEvent.Disconnected, () => {
|
141
|
+
this.handleDisconnect();
|
142
|
+
})
|
143
|
+
.on(EngineEvent.ActiveSpeakersUpdate, this.handleActiveSpeakersUpdate)
|
144
|
+
.on(EngineEvent.DataPacketReceived, this.handleDataPacket)
|
145
|
+
.on(EngineEvent.Resuming, () => {
|
146
|
+
this.state = RoomState.Reconnecting;
|
147
|
+
this.emit(RoomEvent.Reconnecting);
|
148
|
+
})
|
149
|
+
.on(EngineEvent.Resumed, () => {
|
150
|
+
this.state = RoomState.Connected;
|
151
|
+
this.emit(RoomEvent.Reconnected);
|
152
|
+
this.updateSubscriptions();
|
153
|
+
})
|
154
|
+
.on(EngineEvent.SignalResumed, () => {
|
155
|
+
if (this.state === RoomState.Reconnecting) {
|
156
|
+
this.sendSyncState();
|
157
|
+
}
|
158
|
+
})
|
159
|
+
.on(EngineEvent.Restarting, this.handleRestarting)
|
160
|
+
.on(EngineEvent.Restarted, this.handleRestarted);
|
158
161
|
}
|
159
162
|
|
160
163
|
/**
|
@@ -439,6 +442,43 @@ class Room extends EventEmitter {
|
|
439
442
|
);
|
440
443
|
}
|
441
444
|
|
445
|
+
private handleRestarting = () => {
|
446
|
+
this.state = RoomState.Reconnecting;
|
447
|
+
this.emit(RoomEvent.Reconnecting);
|
448
|
+
|
449
|
+
// also unwind existing participants & existing subscriptions
|
450
|
+
for (const p of this.participants.values()) {
|
451
|
+
this.handleParticipantDisconnected(p.sid, p);
|
452
|
+
}
|
453
|
+
};
|
454
|
+
|
455
|
+
private handleRestarted = async (joinResponse: JoinResponse) => {
|
456
|
+
this.state = RoomState.Connected;
|
457
|
+
this.emit(RoomEvent.Reconnected);
|
458
|
+
|
459
|
+
// rehydrate participants
|
460
|
+
if (joinResponse.participant) {
|
461
|
+
// with a restart, the sid will have changed, we'll map our understanding to it
|
462
|
+
this.localParticipant.sid = joinResponse.participant.sid;
|
463
|
+
this.handleParticipantUpdates([joinResponse.participant]);
|
464
|
+
}
|
465
|
+
this.handleParticipantUpdates(joinResponse.otherParticipants);
|
466
|
+
|
467
|
+
// unpublish & republish tracks
|
468
|
+
const localPubs: LocalTrackPublication[] = [];
|
469
|
+
this.localParticipant.tracks.forEach((pub) => {
|
470
|
+
if (pub.track) {
|
471
|
+
localPubs.push(pub);
|
472
|
+
}
|
473
|
+
});
|
474
|
+
|
475
|
+
await Promise.all(localPubs.map(async (pub) => {
|
476
|
+
const track = pub.track!;
|
477
|
+
this.localParticipant.unpublishTrack(track, false);
|
478
|
+
this.localParticipant.publishTrack(track, pub.options);
|
479
|
+
}));
|
480
|
+
};
|
481
|
+
|
442
482
|
private handleDisconnect(shouldStopTracks = true) {
|
443
483
|
if (this.state === RoomState.Disconnected) {
|
444
484
|
return;
|
@@ -474,7 +514,8 @@ class Room extends EventEmitter {
|
|
474
514
|
private handleParticipantUpdates = (participantInfos: ParticipantInfo[]) => {
|
475
515
|
// handle changes to participant state, and send events
|
476
516
|
participantInfos.forEach((info) => {
|
477
|
-
if (info.sid === this.localParticipant.sid
|
517
|
+
if (info.sid === this.localParticipant.sid
|
518
|
+
|| info.identity === this.localParticipant.identity) {
|
478
519
|
this.localParticipant.updateInfo(info);
|
479
520
|
return;
|
480
521
|
}
|
@@ -775,6 +816,20 @@ class Room extends EventEmitter {
|
|
775
816
|
});
|
776
817
|
}
|
777
818
|
|
819
|
+
/**
|
820
|
+
* After resuming, we'll need to notify the server of the current
|
821
|
+
* subscription settings.
|
822
|
+
*/
|
823
|
+
private updateSubscriptions() {
|
824
|
+
for (const p of this.participants.values()) {
|
825
|
+
for (const pub of p.videoTracks.values()) {
|
826
|
+
if (pub.isSubscribed && pub instanceof RemoteTrackPublication) {
|
827
|
+
pub.emitTrackUpdate();
|
828
|
+
}
|
829
|
+
}
|
830
|
+
}
|
831
|
+
}
|
832
|
+
|
778
833
|
/** @internal */
|
779
834
|
emit(event: string | symbol, ...args: any[]): boolean {
|
780
835
|
log.debug('room event', event, ...args);
|
package/src/room/events.ts
CHANGED
@@ -368,9 +368,11 @@ export enum ParticipantEvent {
|
|
368
368
|
export enum EngineEvent {
|
369
369
|
Connected = 'connected',
|
370
370
|
Disconnected = 'disconnected',
|
371
|
-
|
372
|
-
|
373
|
-
|
371
|
+
Resuming = 'resuming',
|
372
|
+
Resumed = 'resumed',
|
373
|
+
Restarting = 'restarting',
|
374
|
+
Restarted = 'restarted',
|
375
|
+
SignalResumed = 'signalResumed',
|
374
376
|
MediaTrackAdded = 'mediaTrackAdded',
|
375
377
|
ActiveSpeakersUpdate = 'activeSpeakersUpdate',
|
376
378
|
DataPacketReceived = 'dataPacketReceived',
|
@@ -434,6 +434,7 @@ export default class LocalParticipant extends Participant {
|
|
434
434
|
|
435
435
|
unpublishTrack(
|
436
436
|
track: LocalTrack | MediaStreamTrack,
|
437
|
+
stopOnUnpublish?: boolean,
|
437
438
|
): LocalTrackPublication | null {
|
438
439
|
// look through all published tracks to find the right ones
|
439
440
|
const publication = this.getPublicationForTrack(track);
|
@@ -455,7 +456,10 @@ export default class LocalParticipant extends Participant {
|
|
455
456
|
track.off(TrackEvent.Unmuted, this.onTrackUnmuted);
|
456
457
|
track.off(TrackEvent.Ended, this.onTrackUnpublish);
|
457
458
|
|
458
|
-
if (
|
459
|
+
if (stopOnUnpublish === undefined) {
|
460
|
+
stopOnUnpublish = this.roomOptions?.stopLocalTrackOnUnpublish ?? true;
|
461
|
+
}
|
462
|
+
if (stopOnUnpublish) {
|
459
463
|
track.stop();
|
460
464
|
}
|
461
465
|
|
@@ -507,10 +511,6 @@ export default class LocalParticipant extends Participant {
|
|
507
511
|
return publications;
|
508
512
|
}
|
509
513
|
|
510
|
-
get publisherMetrics(): any {
|
511
|
-
return null;
|
512
|
-
}
|
513
|
-
|
514
514
|
/**
|
515
515
|
* Publish a new data payload to the room. Data will be forwarded to each
|
516
516
|
* participant in the room if the destination argument is empty
|
@@ -53,6 +53,9 @@ export default class RemoteParticipant extends Participant {
|
|
53
53
|
},
|
54
54
|
);
|
55
55
|
publication.on(TrackEvent.UpdateSubscription, (sub: UpdateSubscription) => {
|
56
|
+
sub.participantTracks.forEach((pt) => {
|
57
|
+
pt.participantSid = this.sid;
|
58
|
+
});
|
56
59
|
this.signalClient.sendUpdateSubscription(sub);
|
57
60
|
});
|
58
61
|
publication.on(TrackEvent.Ended, (track: RemoteTrack) => {
|
@@ -3,12 +3,15 @@ import { TrackEvent } from '../events';
|
|
3
3
|
import LocalAudioTrack from './LocalAudioTrack';
|
4
4
|
import LocalTrack from './LocalTrack';
|
5
5
|
import LocalVideoTrack from './LocalVideoTrack';
|
6
|
+
import { TrackPublishOptions } from './options';
|
6
7
|
import { Track } from './Track';
|
7
8
|
import { TrackPublication } from './TrackPublication';
|
8
9
|
|
9
10
|
export default class LocalTrackPublication extends TrackPublication {
|
10
11
|
track?: LocalTrack;
|
11
12
|
|
13
|
+
options?: TrackPublishOptions;
|
14
|
+
|
12
15
|
constructor(kind: Track.Kind, ti: TrackInfo, track?: LocalTrack) {
|
13
16
|
super(kind, ti.sid, ti.name);
|
14
17
|
|
@@ -35,7 +35,12 @@ export default class RemoteTrackPublication extends TrackPublication {
|
|
35
35
|
const sub: UpdateSubscription = {
|
36
36
|
trackSids: [this.trackSid],
|
37
37
|
subscribe: this.subscribed,
|
38
|
-
participantTracks: [
|
38
|
+
participantTracks: [{
|
39
|
+
// sending an empty participant id since TrackPublication doesn't keep it
|
40
|
+
// this is filled in by the participant that receives this message
|
41
|
+
participantSid: '',
|
42
|
+
trackSids: [this.trackSid],
|
43
|
+
}],
|
39
44
|
};
|
40
45
|
this.emit(TrackEvent.UpdateSubscription, sub);
|
41
46
|
}
|
@@ -172,7 +177,8 @@ export default class RemoteTrackPublication extends TrackPublication {
|
|
172
177
|
this.emitTrackUpdate();
|
173
178
|
};
|
174
179
|
|
175
|
-
|
180
|
+
/* @internal */
|
181
|
+
emitTrackUpdate() {
|
176
182
|
const settings: UpdateTrackSettings = UpdateTrackSettings.fromPartial({
|
177
183
|
trackSids: [this.trackSid],
|
178
184
|
disabled: this.disabled,
|
package/src/room/track/Track.ts
CHANGED
@@ -76,13 +76,15 @@ export class Track extends EventEmitter {
|
|
76
76
|
element.autoplay = true;
|
77
77
|
}
|
78
78
|
|
79
|
-
|
80
|
-
|
81
|
-
return element;
|
79
|
+
if (!this.attachedElements.includes(element)) {
|
80
|
+
this.attachedElements.push(element);
|
82
81
|
}
|
83
82
|
|
83
|
+
// even if we believe it's already attached to the element, it's possible
|
84
|
+
// the element's srcObject was set to something else out of band.
|
85
|
+
// we'll want to re-attach it in that case
|
86
|
+
|
84
87
|
attachToElement(this.mediaStreamTrack, element);
|
85
|
-
this.attachedElements.push(element);
|
86
88
|
|
87
89
|
if (element instanceof HTMLAudioElement) {
|
88
90
|
// manually play audio to detect audio playback status
|
@@ -171,7 +173,7 @@ export function attachToElement(track: MediaStreamTrack, element: HTMLMediaEleme
|
|
171
173
|
element.srcObject = mediaStream;
|
172
174
|
}
|
173
175
|
|
174
|
-
//
|
176
|
+
// check if track matches existing track
|
175
177
|
let existingTracks: MediaStreamTrack[];
|
176
178
|
if (track.kind === 'audio') {
|
177
179
|
existingTracks = mediaStream.getAudioTracks();
|
@@ -179,6 +181,10 @@ export function attachToElement(track: MediaStreamTrack, element: HTMLMediaEleme
|
|
179
181
|
existingTracks = mediaStream.getVideoTracks();
|
180
182
|
}
|
181
183
|
|
184
|
+
if (existingTracks.includes(track)) {
|
185
|
+
return;
|
186
|
+
}
|
187
|
+
|
182
188
|
existingTracks.forEach((et) => {
|
183
189
|
mediaStream.removeTrack(et);
|
184
190
|
});
|
package/src/version.ts
CHANGED
@@ -1,2 +1,2 @@
|
|
1
|
-
export const version = '0.
|
2
|
-
export const protocolVersion =
|
1
|
+
export const version = '0.16.0';
|
2
|
+
export const protocolVersion = 6;
|