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.
- 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