livekit-client 1.14.4 → 1.15.1
Sign up to get free protection for your applications and to get access to all the features.
- package/README.md +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +5488 -5230
- 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/api/SignalClient.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +10 -4
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/PCTransportManager.d.ts +51 -0
- package/dist/src/room/PCTransportManager.d.ts.map +1 -0
- package/dist/src/room/RTCEngine.d.ts +8 -5
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +9 -0
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +10 -0
- package/dist/src/room/events.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +0 -5
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts +6 -2
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/track/options.d.ts +2 -0
- package/dist/src/room/track/options.d.ts.map +1 -1
- package/dist/src/room/track/utils.d.ts +3 -0
- package/dist/src/room/track/utils.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/test/mocks.d.ts +1 -1
- package/dist/src/test/mocks.d.ts.map +1 -1
- package/dist/ts4.2/src/room/PCTransport.d.ts +10 -4
- package/dist/ts4.2/src/room/PCTransportManager.d.ts +51 -0
- package/dist/ts4.2/src/room/RTCEngine.d.ts +8 -5
- package/dist/ts4.2/src/room/Room.d.ts +9 -0
- package/dist/ts4.2/src/room/events.d.ts +10 -0
- package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +0 -5
- package/dist/ts4.2/src/room/track/Track.d.ts +6 -2
- package/dist/ts4.2/src/room/track/options.d.ts +2 -0
- package/dist/ts4.2/src/room/track/utils.d.ts +3 -0
- package/dist/ts4.2/src/test/mocks.d.ts +1 -1
- package/package.json +20 -19
- package/src/api/SignalClient.ts +7 -1
- package/src/connectionHelper/checks/webrtc.ts +2 -2
- package/src/room/PCTransport.ts +66 -29
- package/src/room/PCTransportManager.ts +336 -0
- package/src/room/RTCEngine.ts +178 -246
- package/src/room/Room.ts +49 -46
- package/src/room/defaults.ts +1 -1
- package/src/room/events.ts +11 -0
- package/src/room/participant/LocalParticipant.ts +9 -51
- package/src/room/track/LocalTrack.ts +2 -0
- package/src/room/track/Track.ts +30 -9
- package/src/room/track/options.ts +2 -0
- package/src/room/track/utils.ts +19 -0
- package/src/room/utils.ts +2 -1
- package/src/test/mocks.ts +5 -1
package/src/room/Room.ts
CHANGED
@@ -2,7 +2,6 @@ import { protoInt64 } from '@bufbuild/protobuf';
|
|
2
2
|
import { EventEmitter } from 'events';
|
3
3
|
import type TypedEmitter from 'typed-emitter';
|
4
4
|
import 'webrtc-adapter';
|
5
|
-
import { toProtoSessionDescription } from '../api/SignalClient';
|
6
5
|
import { EncryptionEvent } from '../e2ee';
|
7
6
|
import { E2EEManager } from '../e2ee/E2eeManager';
|
8
7
|
import log from '../logger';
|
@@ -35,8 +34,6 @@ import {
|
|
35
34
|
StreamStateUpdate,
|
36
35
|
SubscriptionPermissionUpdate,
|
37
36
|
SubscriptionResponse,
|
38
|
-
SyncState,
|
39
|
-
UpdateSubscription,
|
40
37
|
} from '../proto/livekit_rtc_pb';
|
41
38
|
import { getBrowser } from '../utils/browserParser';
|
42
39
|
import DeviceManager from './DeviceManager';
|
@@ -152,6 +149,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
152
149
|
|
153
150
|
private regionUrl?: string;
|
154
151
|
|
152
|
+
private isVideoPlaybackBlocked: boolean = false;
|
153
|
+
|
155
154
|
/**
|
156
155
|
* Creates a new Room, the primary construct for a LiveKit session.
|
157
156
|
* @param options
|
@@ -865,6 +864,22 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
865
864
|
}
|
866
865
|
};
|
867
866
|
|
867
|
+
startVideo = async () => {
|
868
|
+
for (const p of this.participants.values()) {
|
869
|
+
p.videoTracks.forEach((tr) => {
|
870
|
+
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
|
+
});
|
878
|
+
});
|
879
|
+
});
|
880
|
+
}
|
881
|
+
};
|
882
|
+
|
868
883
|
/**
|
869
884
|
* Returns true if audio playback is enabled
|
870
885
|
*/
|
@@ -872,6 +887,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
872
887
|
return this.audioEnabled;
|
873
888
|
}
|
874
889
|
|
890
|
+
/**
|
891
|
+
* Returns true if video playback is enabled
|
892
|
+
*/
|
893
|
+
get canPlaybackVideo(): boolean {
|
894
|
+
return !this.isVideoPlaybackBlocked;
|
895
|
+
}
|
896
|
+
|
875
897
|
/**
|
876
898
|
* Returns the active audio output device used in this room.
|
877
899
|
* @return the previously successfully set audio output device ID or an empty string if the default device is used.
|
@@ -1384,6 +1406,20 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1384
1406
|
this.emit(RoomEvent.AudioPlaybackStatusChanged, false);
|
1385
1407
|
};
|
1386
1408
|
|
1409
|
+
private handleVideoPlaybackStarted = () => {
|
1410
|
+
if (this.isVideoPlaybackBlocked) {
|
1411
|
+
this.isVideoPlaybackBlocked = false;
|
1412
|
+
this.emit(RoomEvent.VideoPlaybackStatusChanged, true);
|
1413
|
+
}
|
1414
|
+
};
|
1415
|
+
|
1416
|
+
private handleVideoPlaybackFailed = () => {
|
1417
|
+
if (!this.isVideoPlaybackBlocked) {
|
1418
|
+
this.isVideoPlaybackBlocked = true;
|
1419
|
+
this.emit(RoomEvent.VideoPlaybackStatusChanged, false);
|
1420
|
+
}
|
1421
|
+
};
|
1422
|
+
|
1387
1423
|
private handleDeviceChange = async () => {
|
1388
1424
|
this.emit(RoomEvent.MediaDevicesChanged);
|
1389
1425
|
};
|
@@ -1487,6 +1523,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1487
1523
|
if (track.kind === Track.Kind.Audio) {
|
1488
1524
|
track.on(TrackEvent.AudioPlaybackStarted, this.handleAudioPlaybackStarted);
|
1489
1525
|
track.on(TrackEvent.AudioPlaybackFailed, this.handleAudioPlaybackFailed);
|
1526
|
+
} else if (track.kind === Track.Kind.Video) {
|
1527
|
+
track.on(TrackEvent.VideoPlaybackFailed, this.handleVideoPlaybackFailed);
|
1528
|
+
track.on(TrackEvent.VideoPlaybackStarted, this.handleVideoPlaybackStarted);
|
1490
1529
|
}
|
1491
1530
|
this.emit(RoomEvent.TrackSubscribed, track, publication, participant);
|
1492
1531
|
},
|
@@ -1551,49 +1590,12 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1551
1590
|
}
|
1552
1591
|
|
1553
1592
|
private sendSyncState() {
|
1554
|
-
const
|
1555
|
-
|
1556
|
-
|
1557
|
-
|
1558
|
-
|
1559
|
-
|
1560
|
-
|
1561
|
-
/* 1. autosubscribe on, so subscribed tracks = all tracks - unsub tracks,
|
1562
|
-
in this case, we send unsub tracks, so server add all tracks to this
|
1563
|
-
subscribe pc and unsub special tracks from it.
|
1564
|
-
2. autosubscribe off, we send subscribed tracks.
|
1565
|
-
*/
|
1566
|
-
const autoSubscribe = this.connOptions?.autoSubscribe ?? true;
|
1567
|
-
const trackSids = new Array<string>();
|
1568
|
-
this.participants.forEach((participant) => {
|
1569
|
-
participant.tracks.forEach((track) => {
|
1570
|
-
if (track.isDesired !== autoSubscribe) {
|
1571
|
-
trackSids.push(track.trackSid);
|
1572
|
-
}
|
1573
|
-
});
|
1574
|
-
});
|
1575
|
-
|
1576
|
-
this.engine.client.sendSyncState(
|
1577
|
-
new SyncState({
|
1578
|
-
answer: toProtoSessionDescription({
|
1579
|
-
sdp: previousAnswer.sdp,
|
1580
|
-
type: previousAnswer.type,
|
1581
|
-
}),
|
1582
|
-
offer: previousOffer
|
1583
|
-
? toProtoSessionDescription({
|
1584
|
-
sdp: previousOffer.sdp,
|
1585
|
-
type: previousOffer.type,
|
1586
|
-
})
|
1587
|
-
: undefined,
|
1588
|
-
subscription: new UpdateSubscription({
|
1589
|
-
trackSids,
|
1590
|
-
subscribe: !autoSubscribe,
|
1591
|
-
participantTracks: [],
|
1592
|
-
}),
|
1593
|
-
publishTracks: this.localParticipant.publishedTracksInfo(),
|
1594
|
-
dataChannels: this.localParticipant.dataChannelsInfo(),
|
1595
|
-
}),
|
1596
|
-
);
|
1593
|
+
const remoteTracks = Array.from(this.participants.values()).reduce((acc, participant) => {
|
1594
|
+
acc.push(...(participant.getTracks() as RemoteTrackPublication[])); // FIXME would be nice to have this return RemoteTrackPublications directly instead of the type cast
|
1595
|
+
return acc;
|
1596
|
+
}, [] as RemoteTrackPublication[]);
|
1597
|
+
const localTracks = this.localParticipant.getTracks() as LocalTrackPublication[]; // FIXME would be nice to have this return LocalTrackPublications directly instead of the type cast
|
1598
|
+
this.engine.sendSyncState(remoteTracks, localTracks);
|
1597
1599
|
}
|
1598
1600
|
|
1599
1601
|
/**
|
@@ -1929,6 +1931,7 @@ export type RoomEventCallbacks = {
|
|
1929
1931
|
participant: RemoteParticipant,
|
1930
1932
|
) => void;
|
1931
1933
|
audioPlaybackChanged: (playing: boolean) => void;
|
1934
|
+
videoPlaybackChanged: (playing: boolean) => void;
|
1932
1935
|
signalConnected: () => void;
|
1933
1936
|
recordingStatusChanged: (recording: boolean) => void;
|
1934
1937
|
participantEncryptionStatusChanged: (encrypted: boolean, participant?: Participant) => void;
|
package/src/room/defaults.ts
CHANGED
@@ -22,7 +22,7 @@ export const publishDefaults: TrackPublishDefaults = {
|
|
22
22
|
screenShareEncoding: ScreenSharePresets.h1080fps15.encoding,
|
23
23
|
stopMicTrackOnMute: false,
|
24
24
|
videoCodec: defaultVideoCodec,
|
25
|
-
backupCodec:
|
25
|
+
backupCodec: true,
|
26
26
|
} as const;
|
27
27
|
|
28
28
|
export const audioDefaults: AudioCaptureOptions = {
|
package/src/room/events.ts
CHANGED
@@ -245,6 +245,13 @@ export enum RoomEvent {
|
|
245
245
|
*/
|
246
246
|
AudioPlaybackStatusChanged = 'audioPlaybackChanged',
|
247
247
|
|
248
|
+
/**
|
249
|
+
* LiveKit will attempt to autoplay all video tracks when you attach them to
|
250
|
+
* a video element. However, if that fails, we'll notify you via VideoPlaybackStatusChanged.
|
251
|
+
* Calling `room.startVideo()` in a user gesture event handler will resume the video playback.
|
252
|
+
*/
|
253
|
+
VideoPlaybackStatusChanged = 'videoPlaybackChanged',
|
254
|
+
|
248
255
|
/**
|
249
256
|
* When we have encountered an error while attempting to create a track.
|
250
257
|
* The errors take place in getUserMedia().
|
@@ -510,6 +517,10 @@ export enum TrackEvent {
|
|
510
517
|
/** @internal */
|
511
518
|
VideoDimensionsChanged = 'videoDimensionsChanged',
|
512
519
|
/** @internal */
|
520
|
+
VideoPlaybackStarted = 'videoPlaybackStarted',
|
521
|
+
/** @internal */
|
522
|
+
VideoPlaybackFailed = 'videoPlaybackFailed',
|
523
|
+
/** @internal */
|
513
524
|
ElementAttached = 'elementAttached',
|
514
525
|
/** @internal */
|
515
526
|
ElementDetached = 'elementDetached',
|
@@ -10,13 +10,11 @@ import {
|
|
10
10
|
} from '../../proto/livekit_models_pb';
|
11
11
|
import {
|
12
12
|
AddTrackRequest,
|
13
|
-
DataChannelInfo,
|
14
|
-
SignalTarget,
|
15
13
|
SimulcastCodec,
|
16
14
|
SubscribedQualityUpdate,
|
17
|
-
TrackPublishedResponse,
|
18
15
|
TrackUnpublishedResponse,
|
19
16
|
} from '../../proto/livekit_rtc_pb';
|
17
|
+
import { PCTransportState } from '../PCTransportManager';
|
20
18
|
import type RTCEngine from '../RTCEngine';
|
21
19
|
import { defaultVideoCodec } from '../defaults';
|
22
20
|
import { DeviceUnsupportedError, TrackInvalidError, UnexpectedConnectionState } from '../errors';
|
@@ -766,8 +764,8 @@ export default class LocalParticipant extends Participant {
|
|
766
764
|
publication.options = opts;
|
767
765
|
track.sid = ti.sid;
|
768
766
|
|
769
|
-
if (!this.engine.
|
770
|
-
throw new UnexpectedConnectionState('
|
767
|
+
if (!this.engine.pcManager) {
|
768
|
+
throw new UnexpectedConnectionState('pcManager is not ready');
|
771
769
|
}
|
772
770
|
log.debug(`publishing ${track.kind} with encodings`, { encodings, trackInfo: ti });
|
773
771
|
|
@@ -783,21 +781,21 @@ export default class LocalParticipant extends Participant {
|
|
783
781
|
fix the issue.
|
784
782
|
*/
|
785
783
|
let trackTransceiver: RTCRtpTransceiver | undefined = undefined;
|
786
|
-
for (const transceiver of this.engine.publisher.getTransceivers()) {
|
784
|
+
for (const transceiver of this.engine.pcManager.publisher.getTransceivers()) {
|
787
785
|
if (transceiver.sender === track.sender) {
|
788
786
|
trackTransceiver = transceiver;
|
789
787
|
break;
|
790
788
|
}
|
791
789
|
}
|
792
790
|
if (trackTransceiver) {
|
793
|
-
this.engine.publisher.setTrackCodecBitrate({
|
791
|
+
this.engine.pcManager.publisher.setTrackCodecBitrate({
|
794
792
|
transceiver: trackTransceiver,
|
795
793
|
codec: 'opus',
|
796
794
|
maxbr: encodings[0]?.maxBitrate ? encodings[0].maxBitrate / 1000 : 0,
|
797
795
|
});
|
798
796
|
}
|
799
797
|
} else if (track.codec && isSVCCodec(track.codec) && encodings[0]?.maxBitrate) {
|
800
|
-
this.engine.publisher.setTrackCodecBitrate({
|
798
|
+
this.engine.pcManager.publisher.setTrackCodecBitrate({
|
801
799
|
cid: req.cid,
|
802
800
|
codec: track.codec,
|
803
801
|
maxbr: encodings[0].maxBitrate / 1000,
|
@@ -929,12 +927,12 @@ export default class LocalParticipant extends Participant {
|
|
929
927
|
const trackSender = track.sender;
|
930
928
|
track.sender = undefined;
|
931
929
|
if (
|
932
|
-
this.engine.
|
933
|
-
this.engine.
|
930
|
+
this.engine.pcManager &&
|
931
|
+
this.engine.pcManager.currentState < PCTransportState.FAILED &&
|
934
932
|
trackSender
|
935
933
|
) {
|
936
934
|
try {
|
937
|
-
for (const transceiver of this.engine.publisher.getTransceivers()) {
|
935
|
+
for (const transceiver of this.engine.pcManager.publisher.getTransceivers()) {
|
938
936
|
// if sender is not currently sending (after replaceTrack(null))
|
939
937
|
// removeTrack would have no effect.
|
940
938
|
// to ensure we end up successfully removing the track, manually set
|
@@ -1310,44 +1308,4 @@ export default class LocalParticipant extends Participant {
|
|
1310
1308
|
});
|
1311
1309
|
return publication;
|
1312
1310
|
}
|
1313
|
-
|
1314
|
-
/** @internal */
|
1315
|
-
publishedTracksInfo(): TrackPublishedResponse[] {
|
1316
|
-
const infos: TrackPublishedResponse[] = [];
|
1317
|
-
this.tracks.forEach((track: LocalTrackPublication) => {
|
1318
|
-
if (track.track !== undefined) {
|
1319
|
-
infos.push(
|
1320
|
-
new TrackPublishedResponse({
|
1321
|
-
cid: track.track.mediaStreamID,
|
1322
|
-
track: track.trackInfo,
|
1323
|
-
}),
|
1324
|
-
);
|
1325
|
-
}
|
1326
|
-
});
|
1327
|
-
return infos;
|
1328
|
-
}
|
1329
|
-
|
1330
|
-
/** @internal */
|
1331
|
-
dataChannelsInfo(): DataChannelInfo[] {
|
1332
|
-
const infos: DataChannelInfo[] = [];
|
1333
|
-
const getInfo = (dc: RTCDataChannel | undefined, target: SignalTarget) => {
|
1334
|
-
if (dc?.id !== undefined && dc.id !== null) {
|
1335
|
-
infos.push(
|
1336
|
-
new DataChannelInfo({
|
1337
|
-
label: dc.label,
|
1338
|
-
id: dc.id,
|
1339
|
-
target,
|
1340
|
-
}),
|
1341
|
-
);
|
1342
|
-
}
|
1343
|
-
};
|
1344
|
-
getInfo(this.engine.dataChannelForKind(DataPacket_Kind.LOSSY), SignalTarget.PUBLISHER);
|
1345
|
-
getInfo(this.engine.dataChannelForKind(DataPacket_Kind.RELIABLE), SignalTarget.PUBLISHER);
|
1346
|
-
getInfo(this.engine.dataChannelForKind(DataPacket_Kind.LOSSY, true), SignalTarget.SUBSCRIBER);
|
1347
|
-
getInfo(
|
1348
|
-
this.engine.dataChannelForKind(DataPacket_Kind.RELIABLE, true),
|
1349
|
-
SignalTarget.SUBSCRIBER,
|
1350
|
-
);
|
1351
|
-
return infos;
|
1352
|
-
}
|
1353
1311
|
}
|
@@ -108,9 +108,11 @@ export default abstract class LocalTrack extends Track {
|
|
108
108
|
this.attachedElements.forEach((el) => {
|
109
109
|
detachTrack(this._mediaStreamTrack, el);
|
110
110
|
});
|
111
|
+
this.debouncedTrackMuteHandler.cancel('new-track');
|
111
112
|
this._mediaStreamTrack.removeEventListener('ended', this.handleEnded);
|
112
113
|
this._mediaStreamTrack.removeEventListener('mute', this.handleTrackMuteEvent);
|
113
114
|
this._mediaStreamTrack.removeEventListener('unmute', this.handleTrackUnmuteEvent);
|
115
|
+
|
114
116
|
if (!this.providedByUser && this._mediaStreamTrack !== newTrack) {
|
115
117
|
this._mediaStreamTrack.stop();
|
116
118
|
}
|
package/src/room/track/Track.ts
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
import { EventEmitter } from 'events';
|
2
|
+
import { debounce } from 'ts-debounce';
|
2
3
|
import type TypedEventEmitter from 'typed-emitter';
|
3
4
|
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,6 +113,9 @@ 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);
|
116
119
|
}
|
117
120
|
|
118
121
|
// even if we believe it's already attached to the element, it's possible
|
@@ -130,11 +133,6 @@ export abstract class Track extends (EventEmitter as new () => TypedEventEmitter
|
|
130
133
|
this.emit(TrackEvent.AudioPlaybackStarted);
|
131
134
|
})
|
132
135
|
.catch((e) => {
|
133
|
-
if (e.name === 'NotAllowedError') {
|
134
|
-
this.emit(TrackEvent.AudioPlaybackFailed, e);
|
135
|
-
} else {
|
136
|
-
log.warn('could not playback audio', e);
|
137
|
-
}
|
138
136
|
// If audio playback isn't allowed make sure we still play back the video
|
139
137
|
if (
|
140
138
|
element &&
|
@@ -172,6 +170,8 @@ export abstract class Track extends (EventEmitter as new () => TypedEventEmitter
|
|
172
170
|
if (idx >= 0) {
|
173
171
|
this.attachedElements.splice(idx, 1);
|
174
172
|
this.recycleElement(element);
|
173
|
+
element.removeEventListener('suspend', this.handleElementSuspended);
|
174
|
+
element.removeEventListener('playing', this.handleElementPlay);
|
175
175
|
this.emit(TrackEvent.ElementDetached, element);
|
176
176
|
}
|
177
177
|
return element;
|
@@ -182,6 +182,8 @@ export abstract class Track extends (EventEmitter as new () => TypedEventEmitter
|
|
182
182
|
detachTrack(this.mediaStreamTrack, elm);
|
183
183
|
detached.push(elm);
|
184
184
|
this.recycleElement(elm);
|
185
|
+
elm.removeEventListener('suspend', this.handleElementSuspended);
|
186
|
+
elm.removeEventListener('playing', this.handleElementPlay);
|
185
187
|
this.emit(TrackEvent.ElementDetached, elm);
|
186
188
|
});
|
187
189
|
|
@@ -268,9 +270,26 @@ export abstract class Track extends (EventEmitter as new () => TypedEventEmitter
|
|
268
270
|
document.removeEventListener('visibilitychange', this.appVisibilityChangedListener);
|
269
271
|
}
|
270
272
|
}
|
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);
|
271
291
|
}
|
272
292
|
|
273
|
-
/** @internal */
|
274
293
|
export function attachToElement(track: MediaStreamTrack, element: HTMLMediaElement) {
|
275
294
|
let mediaStream: MediaStream;
|
276
295
|
if (element.srcObject instanceof MediaStream) {
|
@@ -321,7 +340,7 @@ export function attachToElement(track: MediaStreamTrack, element: HTMLMediaEleme
|
|
321
340
|
// when the window is backgrounded before the first frame is drawn
|
322
341
|
// manually calling play here seems to fix that
|
323
342
|
element.play().catch(() => {
|
324
|
-
|
343
|
+
/** do nothing, we watch the `suspended` event do deal with these failures */
|
325
344
|
});
|
326
345
|
}, 0);
|
327
346
|
}
|
@@ -446,10 +465,12 @@ export type TrackEventCallbacks = {
|
|
446
465
|
updateSettings: () => void;
|
447
466
|
updateSubscription: () => void;
|
448
467
|
audioPlaybackStarted: () => void;
|
449
|
-
audioPlaybackFailed: (error
|
468
|
+
audioPlaybackFailed: (error?: Error) => void;
|
450
469
|
audioSilenceDetected: () => void;
|
451
470
|
visibilityChanged: (visible: boolean, track?: any) => void;
|
452
471
|
videoDimensionsChanged: (dimensions: Track.Dimensions, track?: any) => void;
|
472
|
+
videoPlaybackStarted: () => void;
|
473
|
+
videoPlaybackFailed: (error?: Error) => void;
|
453
474
|
elementAttached: (element: HTMLMediaElement) => void;
|
454
475
|
elementDetached: (element: HTMLMediaElement) => void;
|
455
476
|
upstreamPaused: (track: any) => void;
|
@@ -14,6 +14,8 @@ export interface TrackPublishDefaults {
|
|
14
14
|
*
|
15
15
|
* You could customize specific encoding parameters of the backup track by
|
16
16
|
* explicitly setting codec and encoding fields.
|
17
|
+
*
|
18
|
+
* Defaults to `true`
|
17
19
|
*/
|
18
20
|
backupCodec?: true | false | { codec: BackupVideoCodec; encoding?: VideoEncoding };
|
19
21
|
|
package/src/room/track/utils.ts
CHANGED
@@ -1,6 +1,8 @@
|
|
1
|
+
import { TrackPublishedResponse } from '../../proto/livekit_rtc_pb';
|
1
2
|
import { cloneDeep } from '../../utils/cloneDeep';
|
2
3
|
import { isSafari, sleep } from '../utils';
|
3
4
|
import { Track } from './Track';
|
5
|
+
import type { TrackPublication } from './TrackPublication';
|
4
6
|
import {
|
5
7
|
type AudioCaptureOptions,
|
6
8
|
type CreateLocalTracksOptions,
|
@@ -190,3 +192,20 @@ export function mimeTypeToVideoCodecString(mimeType: string) {
|
|
190
192
|
}
|
191
193
|
return codec;
|
192
194
|
}
|
195
|
+
|
196
|
+
export function getTrackPublicationInfo<T extends TrackPublication>(
|
197
|
+
tracks: T[],
|
198
|
+
): TrackPublishedResponse[] {
|
199
|
+
const infos: TrackPublishedResponse[] = [];
|
200
|
+
tracks.forEach((track: TrackPublication) => {
|
201
|
+
if (track.track !== undefined) {
|
202
|
+
infos.push(
|
203
|
+
new TrackPublishedResponse({
|
204
|
+
cid: track.track.mediaStreamID,
|
205
|
+
track: track.trackInfo,
|
206
|
+
}),
|
207
|
+
);
|
208
|
+
}
|
209
|
+
});
|
210
|
+
return infos;
|
211
|
+
}
|
package/src/room/utils.ts
CHANGED
@@ -2,6 +2,7 @@ import { ClientInfo, ClientInfo_SDK } from '../proto/livekit_models_pb';
|
|
2
2
|
import type { DetectableBrowser } from '../utils/browserParser';
|
3
3
|
import { getBrowser } from '../utils/browserParser';
|
4
4
|
import { protocolVersion, version } from '../version';
|
5
|
+
import CriticalTimers from './timers';
|
5
6
|
import type LocalAudioTrack from './track/LocalAudioTrack';
|
6
7
|
import type RemoteAudioTrack from './track/RemoteAudioTrack';
|
7
8
|
import { VideoCodec, videoCodecs } from './track/options';
|
@@ -21,7 +22,7 @@ export function unpackStreamId(packed: string): string[] {
|
|
21
22
|
}
|
22
23
|
|
23
24
|
export async function sleep(duration: number): Promise<void> {
|
24
|
-
return new Promise((resolve) => setTimeout(resolve, duration));
|
25
|
+
return new Promise((resolve) => CriticalTimers.setTimeout(resolve, duration));
|
25
26
|
}
|
26
27
|
|
27
28
|
/** @internal */
|
package/src/test/mocks.ts
CHANGED
@@ -8,7 +8,11 @@ vi.mock('../room/RTCEngine');
|
|
8
8
|
|
9
9
|
// mock helpers for testing
|
10
10
|
|
11
|
-
const mocks
|
11
|
+
const mocks: {
|
12
|
+
SignalClient: MockedClass<typeof SignalClient>;
|
13
|
+
RTCEngine: MockedClass<typeof RTCEngine>;
|
14
|
+
MockLocalVideoTrack: { stop: () => void };
|
15
|
+
} = {
|
12
16
|
SignalClient: SignalClient as MockedClass<typeof SignalClient>,
|
13
17
|
RTCEngine: RTCEngine as MockedClass<typeof RTCEngine>,
|
14
18
|
MockLocalVideoTrack: {
|