livekit-client 1.15.3 → 1.15.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. package/dist/livekit-client.esm.mjs +91 -50
  2. package/dist/livekit-client.esm.mjs.map +1 -1
  3. package/dist/livekit-client.umd.js +1 -1
  4. package/dist/livekit-client.umd.js.map +1 -1
  5. package/dist/src/proto/livekit_models_pb.d.ts +11 -1
  6. package/dist/src/proto/livekit_models_pb.d.ts.map +1 -1
  7. package/dist/src/room/RTCEngine.d.ts +4 -1
  8. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  9. package/dist/src/room/Room.d.ts.map +1 -1
  10. package/dist/src/room/events.d.ts +4 -1
  11. package/dist/src/room/events.d.ts.map +1 -1
  12. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  13. package/dist/src/room/participant/Participant.d.ts +6 -0
  14. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  15. package/dist/src/room/stats.d.ts +1 -0
  16. package/dist/src/room/stats.d.ts.map +1 -1
  17. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  18. package/dist/src/room/track/Track.d.ts +0 -3
  19. package/dist/src/room/track/Track.d.ts.map +1 -1
  20. package/dist/src/version.d.ts +1 -1
  21. package/dist/ts4.2/src/proto/livekit_models_pb.d.ts +11 -1
  22. package/dist/ts4.2/src/room/RTCEngine.d.ts +4 -0
  23. package/dist/ts4.2/src/room/events.d.ts +4 -1
  24. package/dist/ts4.2/src/room/participant/Participant.d.ts +6 -0
  25. package/dist/ts4.2/src/room/stats.d.ts +1 -0
  26. package/dist/ts4.2/src/room/track/Track.d.ts +0 -3
  27. package/dist/ts4.2/src/version.d.ts +1 -1
  28. package/package.json +2 -2
  29. package/src/proto/livekit_models_pb.ts +15 -1
  30. package/src/proto/livekit_rtc_pb.ts +1 -1
  31. package/src/room/RTCEngine.ts +13 -0
  32. package/src/room/Room.ts +26 -8
  33. package/src/room/events.ts +3 -0
  34. package/src/room/participant/LocalParticipant.ts +4 -6
  35. package/src/room/participant/Participant.ts +11 -0
  36. package/src/room/stats.ts +2 -0
  37. package/src/room/track/RemoteVideoTrack.ts +8 -0
  38. package/src/room/track/Track.ts +34 -48
  39. package/src/version.ts +1 -1
package/src/room/Room.ts CHANGED
@@ -195,7 +195,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
195
195
  );
196
196
  }
197
197
  if (this.options.audioOutput?.deviceId) {
198
- this.switchActiveDevice('audiooutput', unwrapConstraint(this.options.audioOutput.deviceId));
198
+ this.switchActiveDevice(
199
+ 'audiooutput',
200
+ unwrapConstraint(this.options.audioOutput.deviceId),
201
+ ).catch((e) => log.warn(`Could not set audio output: ${e.message}`));
199
202
  }
200
203
 
201
204
  if (this.options.e2ee) {
@@ -865,19 +868,29 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
865
868
  };
866
869
 
867
870
  startVideo = async () => {
871
+ const elements: HTMLMediaElement[] = [];
868
872
  for (const p of this.participants.values()) {
869
873
  p.videoTracks.forEach((tr) => {
870
874
  tr.track?.attachedElements.forEach((el) => {
871
- el.play().catch((e) => {
872
- if (e.name === 'NotAllowedError') {
873
- log.warn(
874
- 'Resuming video playback failed, make sure you call `startVideo` directly in a user gesture handler',
875
- );
876
- }
877
- });
875
+ if (!elements.includes(el)) {
876
+ elements.push(el);
877
+ }
878
878
  });
879
879
  });
880
880
  }
881
+ await Promise.all(elements.map((el) => el.play()))
882
+ .then(() => {
883
+ this.handleVideoPlaybackStarted();
884
+ })
885
+ .catch((e) => {
886
+ if (e.name === 'NotAllowedError') {
887
+ this.handleVideoPlaybackFailed();
888
+ } else {
889
+ log.warn(
890
+ 'Resuming video playback failed, make sure you call `startVideo` directly in a user gesture handler',
891
+ );
892
+ }
893
+ });
881
894
  };
882
895
 
883
896
  /**
@@ -1494,6 +1507,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1494
1507
  if (this.options.expWebAudioMix) {
1495
1508
  participant.setAudioContext(this.audioContext);
1496
1509
  }
1510
+ if (this.options.audioOutput?.deviceId) {
1511
+ participant
1512
+ .setAudioOutput(this.options.audioOutput)
1513
+ .catch((e) => log.warn(`Could not set audio output: ${e.message}`));
1514
+ }
1497
1515
  return participant;
1498
1516
  }
1499
1517
 
@@ -486,6 +486,9 @@ export enum EngineEvent {
486
486
  ConnectionQualityUpdate = 'connectionQualityUpdate',
487
487
  SubscriptionError = 'subscriptionError',
488
488
  SubscriptionPermissionUpdate = 'subscriptionPermissionUpdate',
489
+ RemoteMute = 'remoteMute',
490
+ SubscribedQualityUpdate = 'subscribedQualityUpdate',
491
+ LocalTrackUnpublished = 'localTrackUnpublished',
489
492
  }
490
493
 
491
494
  export enum TrackEvent {
@@ -127,7 +127,7 @@ export default class LocalParticipant extends Participant {
127
127
  */
128
128
  setupEngine(engine: RTCEngine) {
129
129
  this.engine = engine;
130
- this.engine.client.onRemoteMuteChanged = (trackSid: string, muted: boolean) => {
130
+ this.engine.on(EngineEvent.RemoteMute, (trackSid: string, muted: boolean) => {
131
131
  const pub = this.tracks.get(trackSid);
132
132
  if (!pub || !pub.track) {
133
133
  return;
@@ -137,11 +137,7 @@ export default class LocalParticipant extends Participant {
137
137
  } else {
138
138
  pub.unmute();
139
139
  }
140
- };
141
-
142
- this.engine.client.onSubscribedQualityUpdate = this.handleSubscribedQualityUpdate;
143
-
144
- this.engine.client.onLocalTrackUnpublished = this.handleLocalTrackUnpublished;
140
+ });
145
141
 
146
142
  this.engine
147
143
  .on(EngineEvent.Connected, this.handleReconnected)
@@ -149,6 +145,8 @@ export default class LocalParticipant extends Participant {
149
145
  .on(EngineEvent.SignalResumed, this.handleReconnected)
150
146
  .on(EngineEvent.Restarting, this.handleReconnecting)
151
147
  .on(EngineEvent.Resuming, this.handleReconnecting)
148
+ .on(EngineEvent.LocalTrackUnpublished, this.handleLocalTrackUnpublished)
149
+ .on(EngineEvent.SubscribedQualityUpdate, this.handleSubscribedQualityUpdate)
152
150
  .on(EngineEvent.Disconnected, this.handleDisconnected);
153
151
  }
154
152
 
@@ -21,6 +21,11 @@ export enum ConnectionQuality {
21
21
  Excellent = 'excellent',
22
22
  Good = 'good',
23
23
  Poor = 'poor',
24
+ /**
25
+ * Indicates that a participant has temporarily (or permanently) lost connection to LiveKit.
26
+ * For permanent disconnection a `ParticipantDisconnected` event will be emitted after a timeout
27
+ */
28
+ Lost = 'lost',
24
29
  Unknown = 'unknown',
25
30
  }
26
31
 
@@ -32,6 +37,8 @@ function qualityFromProto(q: ProtoQuality): ConnectionQuality {
32
37
  return ConnectionQuality.Good;
33
38
  case ProtoQuality.POOR:
34
39
  return ConnectionQuality.Poor;
40
+ case ProtoQuality.LOST:
41
+ return ConnectionQuality.Lost;
35
42
  default:
36
43
  return ConnectionQuality.Unknown;
37
44
  }
@@ -77,6 +84,10 @@ export default class Participant extends (EventEmitter as new () => TypedEmitter
77
84
  return this.tracks.size > 0 && Array.from(this.tracks.values()).every((tr) => tr.isEncrypted);
78
85
  }
79
86
 
87
+ get isAgent() {
88
+ return this.permissions?.agent ?? false;
89
+ }
90
+
80
91
  /** @internal */
81
92
  constructor(sid: string, identity: string, name?: string, metadata?: string) {
82
93
  super();
package/src/room/stats.ts CHANGED
@@ -106,6 +106,8 @@ export interface VideoReceiverStats extends ReceiverStats {
106
106
  nackCount?: number;
107
107
 
108
108
  decoderImplementation?: string;
109
+
110
+ mimeType?: string;
109
111
  }
110
112
 
111
113
  export function computeBitrate<T extends ReceiverStats | SenderStats>(
@@ -169,8 +169,11 @@ export default class RemoteVideoTrack extends RemoteTrack {
169
169
 
170
170
  const stats = await this.receiver.getStats();
171
171
  let receiverStats: VideoReceiverStats | undefined;
172
+ let codecID = '';
173
+ let codecs = new Map<string, any>();
172
174
  stats.forEach((v) => {
173
175
  if (v.type === 'inbound-rtp') {
176
+ codecID = v.codecId;
174
177
  receiverStats = {
175
178
  type: 'video',
176
179
  framesDecoded: v.framesDecoded,
@@ -188,8 +191,13 @@ export default class RemoteVideoTrack extends RemoteTrack {
188
191
  bytesReceived: v.bytesReceived,
189
192
  decoderImplementation: v.decoderImplementation,
190
193
  };
194
+ } else if (v.type === 'codec') {
195
+ codecs.set(v.id, v);
191
196
  }
192
197
  });
198
+ if (receiverStats && codecID !== '' && codecs.get(codecID)) {
199
+ receiverStats.mimeType = codecs.get(codecID).mimeType;
200
+ }
193
201
  return receiverStats;
194
202
  }
195
203
 
@@ -1,7 +1,7 @@
1
1
  import { EventEmitter } from 'events';
2
- import { debounce } from 'ts-debounce';
3
2
  import type TypedEventEmitter from 'typed-emitter';
4
3
  import type { SignalClient } from '../../api/SignalClient';
4
+ import log from '../../logger';
5
5
  import { TrackSource, TrackType } from '../../proto/livekit_models_pb';
6
6
  import { StreamState as ProtoStreamState } from '../../proto/livekit_rtc_pb';
7
7
  import { TrackEvent } from '../events';
@@ -113,9 +113,6 @@ export abstract class Track extends (EventEmitter as new () => TypedEventEmitter
113
113
 
114
114
  if (!this.attachedElements.includes(element)) {
115
115
  this.attachedElements.push(element);
116
- // listen to suspend events in order to detect auto playback issues
117
- element.addEventListener('suspend', this.handleElementSuspended);
118
- element.addEventListener('playing', this.handleElementPlay);
119
116
  }
120
117
 
121
118
  // even if we believe it's already attached to the element, it's possible
@@ -125,27 +122,38 @@ export abstract class Track extends (EventEmitter as new () => TypedEventEmitter
125
122
 
126
123
  // handle auto playback failures
127
124
  const allMediaStreamTracks = (element.srcObject as MediaStream).getTracks();
128
- if (allMediaStreamTracks.some((tr) => tr.kind === 'audio')) {
129
- // manually play audio to detect audio playback status
130
- element
131
- .play()
132
- .then(() => {
133
- this.emit(TrackEvent.AudioPlaybackStarted);
134
- })
135
- .catch((e) => {
136
- // If audio playback isn't allowed make sure we still play back the video
137
- if (
138
- element &&
139
- allMediaStreamTracks.some((tr) => tr.kind === 'video') &&
140
- e.name === 'NotAllowedError'
141
- ) {
142
- element.muted = true;
143
- element.play().catch(() => {
144
- // catch for Safari, exceeded options at this point to automatically play the media element
145
- });
146
- }
147
- });
148
- }
125
+ const hasAudio = allMediaStreamTracks.some((tr) => tr.kind === 'audio');
126
+
127
+ // manually play media to detect auto playback status
128
+ element
129
+ .play()
130
+ .then(() => {
131
+ this.emit(hasAudio ? TrackEvent.AudioPlaybackStarted : TrackEvent.VideoPlaybackStarted);
132
+ })
133
+ .catch((e) => {
134
+ if (e.name === 'NotAllowedError') {
135
+ this.emit(hasAudio ? TrackEvent.AudioPlaybackFailed : TrackEvent.VideoPlaybackFailed, e);
136
+ } else if (e.name === 'AbortError') {
137
+ // commonly triggered by another `play` request, only log for debugging purposes
138
+ log.debug(
139
+ `${hasAudio ? 'audio' : 'video'} playback aborted, likely due to new play request`,
140
+ );
141
+ } else {
142
+ log.warn(`could not playback ${hasAudio ? 'audio' : 'video'}`, e);
143
+ }
144
+ // If audio playback isn't allowed make sure we still play back the video
145
+ if (
146
+ hasAudio &&
147
+ element &&
148
+ allMediaStreamTracks.some((tr) => tr.kind === 'video') &&
149
+ e.name === 'NotAllowedError'
150
+ ) {
151
+ element.muted = true;
152
+ element.play().catch(() => {
153
+ // catch for Safari, exceeded options at this point to automatically play the media element
154
+ });
155
+ }
156
+ });
149
157
 
150
158
  this.emit(TrackEvent.ElementAttached, element);
151
159
  return element;
@@ -170,8 +178,6 @@ export abstract class Track extends (EventEmitter as new () => TypedEventEmitter
170
178
  if (idx >= 0) {
171
179
  this.attachedElements.splice(idx, 1);
172
180
  this.recycleElement(element);
173
- element.removeEventListener('suspend', this.handleElementSuspended);
174
- element.removeEventListener('playing', this.handleElementPlay);
175
181
  this.emit(TrackEvent.ElementDetached, element);
176
182
  }
177
183
  return element;
@@ -182,8 +188,6 @@ export abstract class Track extends (EventEmitter as new () => TypedEventEmitter
182
188
  detachTrack(this.mediaStreamTrack, elm);
183
189
  detached.push(elm);
184
190
  this.recycleElement(elm);
185
- elm.removeEventListener('suspend', this.handleElementSuspended);
186
- elm.removeEventListener('playing', this.handleElementPlay);
187
191
  this.emit(TrackEvent.ElementDetached, elm);
188
192
  });
189
193
 
@@ -270,24 +274,6 @@ export abstract class Track extends (EventEmitter as new () => TypedEventEmitter
270
274
  document.removeEventListener('visibilitychange', this.appVisibilityChangedListener);
271
275
  }
272
276
  }
273
-
274
- private handleElementSuspended = () => {
275
- this.debouncedPlaybackStateChange(false);
276
- };
277
-
278
- private handleElementPlay = () => {
279
- this.debouncedPlaybackStateChange(true);
280
- };
281
-
282
- private debouncedPlaybackStateChange = debounce((allowed: boolean) => {
283
- // we debounce this as Safari triggers both `playing` and `suspend` shortly after one another
284
- // in order not to raise the wrong event, we debounce the call to make sure we only emit the correct status
285
- if (this.kind === Track.Kind.Audio) {
286
- this.emit(allowed ? TrackEvent.AudioPlaybackStarted : TrackEvent.AudioPlaybackFailed);
287
- } else if (this.kind === Track.Kind.Video) {
288
- this.emit(allowed ? TrackEvent.VideoPlaybackStarted : TrackEvent.VideoPlaybackFailed);
289
- }
290
- }, 300);
291
277
  }
292
278
 
293
279
  export function attachToElement(track: MediaStreamTrack, element: HTMLMediaElement) {
@@ -340,7 +326,7 @@ export function attachToElement(track: MediaStreamTrack, element: HTMLMediaEleme
340
326
  // when the window is backgrounded before the first frame is drawn
341
327
  // manually calling play here seems to fix that
342
328
  element.play().catch(() => {
343
- /** do nothing, we watch the `suspended` event do deal with these failures */
329
+ /** do nothing */
344
330
  });
345
331
  }, 0);
346
332
  }
package/src/version.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import { version as v } from '../package.json';
2
2
 
3
3
  export const version = v;
4
- export const protocolVersion = 10;
4
+ export const protocolVersion = 11;