livekit-client 1.15.3 → 1.15.5
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 +91 -50
- 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/proto/livekit_models_pb.d.ts +11 -1
- package/dist/src/proto/livekit_models_pb.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +4 -1
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +4 -1
- package/dist/src/room/events.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts +6 -0
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/room/stats.d.ts +1 -0
- package/dist/src/room/stats.d.ts.map +1 -1
- package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts +0 -3
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/ts4.2/src/proto/livekit_models_pb.d.ts +11 -1
- package/dist/ts4.2/src/room/RTCEngine.d.ts +4 -0
- package/dist/ts4.2/src/room/events.d.ts +4 -1
- package/dist/ts4.2/src/room/participant/Participant.d.ts +6 -0
- package/dist/ts4.2/src/room/stats.d.ts +1 -0
- package/dist/ts4.2/src/room/track/Track.d.ts +0 -3
- package/dist/ts4.2/src/version.d.ts +1 -1
- package/package.json +2 -2
- package/src/proto/livekit_models_pb.ts +15 -1
- package/src/proto/livekit_rtc_pb.ts +1 -1
- package/src/room/RTCEngine.ts +13 -0
- package/src/room/Room.ts +26 -8
- package/src/room/events.ts +3 -0
- package/src/room/participant/LocalParticipant.ts +4 -6
- package/src/room/participant/Participant.ts +11 -0
- package/src/room/stats.ts +2 -0
- package/src/room/track/RemoteVideoTrack.ts +8 -0
- package/src/room/track/Track.ts +34 -48
- 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(
|
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
|
-
|
872
|
-
|
873
|
-
|
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
|
|
package/src/room/events.ts
CHANGED
@@ -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.
|
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
@@ -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
|
|
package/src/room/track/Track.ts
CHANGED
@@ -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
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
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
|
329
|
+
/** do nothing */
|
344
330
|
});
|
345
331
|
}, 0);
|
346
332
|
}
|
package/src/version.ts
CHANGED