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.
Files changed (41) hide show
  1. package/dist/api/SignalClient.d.ts +3 -2
  2. package/dist/api/SignalClient.js +11 -3
  3. package/dist/api/SignalClient.js.map +1 -1
  4. package/dist/proto/livekit_rtc.d.ts +15 -10
  5. package/dist/proto/livekit_rtc.js +36 -22
  6. package/dist/proto/livekit_rtc.js.map +1 -1
  7. package/dist/room/RTCEngine.d.ts +8 -2
  8. package/dist/room/RTCEngine.js +137 -42
  9. package/dist/room/RTCEngine.js.map +1 -1
  10. package/dist/room/Room.d.ts +7 -0
  11. package/dist/room/Room.js +66 -16
  12. package/dist/room/Room.js.map +1 -1
  13. package/dist/room/events.d.ts +5 -3
  14. package/dist/room/events.js +5 -3
  15. package/dist/room/events.js.map +1 -1
  16. package/dist/room/participant/LocalParticipant.d.ts +1 -2
  17. package/dist/room/participant/LocalParticipant.js +5 -5
  18. package/dist/room/participant/LocalParticipant.js.map +1 -1
  19. package/dist/room/participant/RemoteParticipant.js +3 -0
  20. package/dist/room/participant/RemoteParticipant.js.map +1 -1
  21. package/dist/room/track/LocalTrackPublication.d.ts +2 -0
  22. package/dist/room/track/LocalTrackPublication.js.map +1 -1
  23. package/dist/room/track/RemoteTrackPublication.d.ts +1 -1
  24. package/dist/room/track/RemoteTrackPublication.js +7 -1
  25. package/dist/room/track/RemoteTrackPublication.js.map +1 -1
  26. package/dist/room/track/Track.js +9 -5
  27. package/dist/room/track/Track.js.map +1 -1
  28. package/dist/version.d.ts +2 -2
  29. package/dist/version.js +2 -2
  30. package/package.json +2 -2
  31. package/src/api/SignalClient.ts +14 -4
  32. package/src/proto/livekit_rtc.ts +55 -41
  33. package/src/room/RTCEngine.ts +145 -47
  34. package/src/room/Room.ts +92 -37
  35. package/src/room/events.ts +5 -3
  36. package/src/room/participant/LocalParticipant.ts +5 -5
  37. package/src/room/participant/RemoteParticipant.ts +3 -0
  38. package/src/room/track/LocalTrackPublication.ts +3 -0
  39. package/src/room/track/RemoteTrackPublication.ts +8 -2
  40. package/src/room/track/Track.ts +11 -5
  41. package/src/version.ts +2 -2
@@ -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 = 5;
18
- export const maxICEConnectTimeout = 5 * 1000;
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 iceConnected: boolean = false;
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.oniceconnectionstatechange = () => {
178
- if (primaryPC.iceConnectionState === 'connected') {
179
- log.trace('ICE connected');
180
- if (!this.iceConnected) {
181
- this.iceConnected = true;
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.iceConnectionState === 'failed') {
199
+ } else if (primaryPC.connectionState === 'failed') {
188
200
  // on Safari, PeerConnection will switch to 'disconnected' during renegotiation
189
- log.trace('ICE disconnected');
190
- if (this.iceConnected) {
191
- this.iceConnected = false;
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
- this.emit(EngineEvent.Disconnected);
277
- this.close();
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 >= maxReconnectRetries) {
324
- log.info(
325
- 'could not connect to signal after',
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.reconnect()
337
- .then(() => {
338
- this.reconnectAttempts = 0;
339
- })
340
- .catch(this.handleDisconnect);
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 reconnect(): Promise<void> {
345
- if (this.isClosed) {
346
- return;
347
- }
393
+ private async restartConnection() {
348
394
  if (!this.url || !this.token) {
349
- throw new ConnectionError('could not reconnect, url or token not saved');
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.Reconnecting);
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.client.reconnect(this.url, this.token);
359
- this.emit(EngineEvent.SignalConnected);
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
- const startTime = (new Date()).getTime();
451
+ await this.waitForPCConnected();
452
+
453
+ // resume success
454
+ this.emit(EngineEvent.Resumed);
455
+ }
373
456
 
374
- while ((new Date()).getTime() - startTime < maxICEConnectTimeout * 2) {
375
- if (this.iceConnected) {
376
- // reconnect success
377
- this.emit(EngineEvent.Reconnected);
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 ICE connection');
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, SimulateScenario, StreamStateUpdate, SubscriptionPermissionUpdate,
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);
@@ -368,9 +368,11 @@ export enum ParticipantEvent {
368
368
  export enum EngineEvent {
369
369
  Connected = 'connected',
370
370
  Disconnected = 'disconnected',
371
- Reconnecting = 'reconnecting',
372
- Reconnected = 'reconnected',
373
- SignalConnected = 'singalConnected',
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 (this.roomOptions?.stopLocalTrackOnUnpublish ?? true) {
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
- protected emitTrackUpdate() {
180
+ /* @internal */
181
+ emitTrackUpdate() {
176
182
  const settings: UpdateTrackSettings = UpdateTrackSettings.fromPartial({
177
183
  trackSids: [this.trackSid],
178
184
  disabled: this.disabled,
@@ -76,13 +76,15 @@ export class Track extends EventEmitter {
76
76
  element.autoplay = true;
77
77
  }
78
78
 
79
- // already attached
80
- if (this.attachedElements.includes(element)) {
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
- // remove existing tracks of same type from stream
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.15.4';
2
- export const protocolVersion = 5;
1
+ export const version = '0.16.0';
2
+ export const protocolVersion = 6;