livekit-client 2.9.9 → 2.11.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.
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +3 -3
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +341 -135
- 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/api/utils.d.ts +3 -0
- package/dist/src/api/utils.d.ts.map +1 -0
- package/dist/src/e2ee/KeyProvider.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +1 -0
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/StreamWriter.d.ts +1 -1
- package/dist/src/room/StreamWriter.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +2 -1
- package/dist/src/room/events.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +10 -1
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/RemoteTrackPublication.d.ts.map +1 -1
- package/dist/src/room/track/create.d.ts.map +1 -1
- package/dist/src/room/track/options.d.ts +3 -2
- package/dist/src/room/track/options.d.ts.map +1 -1
- package/dist/ts4.2/src/api/utils.d.ts +3 -0
- package/dist/ts4.2/src/room/RTCEngine.d.ts +1 -0
- package/dist/ts4.2/src/room/StreamWriter.d.ts +1 -1
- package/dist/ts4.2/src/room/events.d.ts +2 -1
- package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +10 -1
- package/dist/ts4.2/src/room/track/options.d.ts +3 -2
- package/package.json +13 -13
- package/src/api/SignalClient.ts +8 -17
- package/src/api/utils.test.ts +112 -0
- package/src/api/utils.ts +23 -0
- package/src/room/DeviceManager.ts +1 -1
- package/src/room/RTCEngine.ts +5 -0
- package/src/room/Room.ts +1 -1
- package/src/room/StreamWriter.ts +1 -1
- package/src/room/defaults.ts +2 -2
- package/src/room/events.ts +1 -0
- package/src/room/participant/LocalParticipant.ts +148 -49
- package/src/room/track/LocalTrack.ts +2 -2
- package/src/room/track/LocalVideoTrack.ts +15 -14
- package/src/room/track/create.ts +87 -47
- package/src/room/track/options.ts +5 -1
- package/src/room/track/utils.ts +6 -6
@@ -0,0 +1,112 @@
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
2
|
+
import { createRtcUrl, createValidateUrl } from './utils';
|
3
|
+
|
4
|
+
describe('createRtcUrl', () => {
|
5
|
+
it('should create a basic RTC URL', () => {
|
6
|
+
const url = 'wss://example.com';
|
7
|
+
const searchParams = new URLSearchParams();
|
8
|
+
const result = createRtcUrl(url, searchParams);
|
9
|
+
expect(result.toString()).toBe('wss://example.com/rtc');
|
10
|
+
});
|
11
|
+
|
12
|
+
it('should handle search parameters', () => {
|
13
|
+
const url = 'wss://example.com';
|
14
|
+
const searchParams = new URLSearchParams({
|
15
|
+
token: 'test-token',
|
16
|
+
room: 'test-room',
|
17
|
+
});
|
18
|
+
const result = createRtcUrl(url, searchParams);
|
19
|
+
|
20
|
+
const parsedResult = new URL(result);
|
21
|
+
expect(parsedResult.pathname).toBe('/rtc');
|
22
|
+
expect(parsedResult.searchParams.get('token')).toBe('test-token');
|
23
|
+
expect(parsedResult.searchParams.get('room')).toBe('test-room');
|
24
|
+
});
|
25
|
+
|
26
|
+
it('should handle ws protocol', () => {
|
27
|
+
const url = 'ws://example.com';
|
28
|
+
const searchParams = new URLSearchParams();
|
29
|
+
const result = createRtcUrl(url, searchParams);
|
30
|
+
|
31
|
+
const parsedResult = new URL(result);
|
32
|
+
expect(parsedResult.pathname).toBe('/rtc');
|
33
|
+
});
|
34
|
+
|
35
|
+
it('should handle sub paths', () => {
|
36
|
+
const url = 'wss://example.com/sub/path';
|
37
|
+
const searchParams = new URLSearchParams();
|
38
|
+
const result = createRtcUrl(url, searchParams);
|
39
|
+
|
40
|
+
const parsedResult = new URL(result);
|
41
|
+
expect(parsedResult.pathname).toBe('/sub/path/rtc');
|
42
|
+
});
|
43
|
+
|
44
|
+
it('should handle sub paths with trailing slashes', () => {
|
45
|
+
const url = 'wss://example.com/sub/path/';
|
46
|
+
const searchParams = new URLSearchParams();
|
47
|
+
const result = createRtcUrl(url, searchParams);
|
48
|
+
|
49
|
+
const parsedResult = new URL(result);
|
50
|
+
expect(parsedResult.pathname).toBe('/sub/path/rtc');
|
51
|
+
});
|
52
|
+
|
53
|
+
it('should handle sub paths with url params', () => {
|
54
|
+
const url = 'wss://example.com/sub/path?param=value';
|
55
|
+
const searchParams = new URLSearchParams();
|
56
|
+
searchParams.set('token', 'test-token');
|
57
|
+
const result = createRtcUrl(url, searchParams);
|
58
|
+
|
59
|
+
const parsedResult = new URL(result);
|
60
|
+
expect(parsedResult.pathname).toBe('/sub/path/rtc');
|
61
|
+
expect(parsedResult.searchParams.get('param')).toBe('value');
|
62
|
+
expect(parsedResult.searchParams.get('token')).toBe('test-token');
|
63
|
+
});
|
64
|
+
});
|
65
|
+
|
66
|
+
describe('createValidateUrl', () => {
|
67
|
+
it('should create a basic validate URL', () => {
|
68
|
+
const rtcUrl = createRtcUrl('wss://example.com', new URLSearchParams());
|
69
|
+
const result = createValidateUrl(rtcUrl);
|
70
|
+
expect(result.toString()).toBe('https://example.com/rtc/validate');
|
71
|
+
});
|
72
|
+
|
73
|
+
it('should handle search parameters', () => {
|
74
|
+
const rtcUrl = createRtcUrl(
|
75
|
+
'wss://example.com',
|
76
|
+
new URLSearchParams({
|
77
|
+
token: 'test-token',
|
78
|
+
room: 'test-room',
|
79
|
+
}),
|
80
|
+
);
|
81
|
+
const result = createValidateUrl(rtcUrl);
|
82
|
+
|
83
|
+
const parsedResult = new URL(result);
|
84
|
+
expect(parsedResult.pathname).toBe('/rtc/validate');
|
85
|
+
expect(parsedResult.searchParams.get('token')).toBe('test-token');
|
86
|
+
expect(parsedResult.searchParams.get('room')).toBe('test-room');
|
87
|
+
});
|
88
|
+
|
89
|
+
it('should handle ws protocol', () => {
|
90
|
+
const rtcUrl = createRtcUrl('ws://example.com', new URLSearchParams());
|
91
|
+
const result = createValidateUrl(rtcUrl);
|
92
|
+
|
93
|
+
const parsedResult = new URL(result);
|
94
|
+
expect(parsedResult.pathname).toBe('/rtc/validate');
|
95
|
+
});
|
96
|
+
|
97
|
+
it('should preserve the original path', () => {
|
98
|
+
const rtcUrl = createRtcUrl('wss://example.com/some/path', new URLSearchParams());
|
99
|
+
const result = createValidateUrl(rtcUrl);
|
100
|
+
|
101
|
+
const parsedResult = new URL(result);
|
102
|
+
expect(parsedResult.pathname).toBe('/some/path/rtc/validate');
|
103
|
+
});
|
104
|
+
|
105
|
+
it('should handle sub paths with trailing slashes', () => {
|
106
|
+
const rtcUrl = createRtcUrl('wss://example.com/sub/path/', new URLSearchParams());
|
107
|
+
const result = createValidateUrl(rtcUrl);
|
108
|
+
|
109
|
+
const parsedResult = new URL(result);
|
110
|
+
expect(parsedResult.pathname).toBe('/sub/path/rtc/validate');
|
111
|
+
});
|
112
|
+
});
|
package/src/api/utils.ts
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
import { toHttpUrl } from '../room/utils';
|
2
|
+
|
3
|
+
export function createRtcUrl(url: string, searchParams: URLSearchParams) {
|
4
|
+
const urlObj = new URL(url);
|
5
|
+
searchParams.forEach((value, key) => {
|
6
|
+
urlObj.searchParams.set(key, value);
|
7
|
+
});
|
8
|
+
return appendUrlPath(urlObj, 'rtc');
|
9
|
+
}
|
10
|
+
|
11
|
+
export function createValidateUrl(rtcWsUrl: string) {
|
12
|
+
const urlObj = new URL(toHttpUrl(rtcWsUrl));
|
13
|
+
return appendUrlPath(urlObj, 'validate');
|
14
|
+
}
|
15
|
+
|
16
|
+
function ensureTrailingSlash(path: string) {
|
17
|
+
return path.endsWith('/') ? path : `${path}/`;
|
18
|
+
}
|
19
|
+
|
20
|
+
function appendUrlPath(urlObj: URL, path: string) {
|
21
|
+
urlObj.pathname = `${ensureTrailingSlash(urlObj.pathname)}${path}`;
|
22
|
+
return urlObj.toString();
|
23
|
+
}
|
@@ -57,7 +57,7 @@ export default class DeviceManager {
|
|
57
57
|
if (isDummyDeviceOrEmpty) {
|
58
58
|
const permissionsToAcquire = {
|
59
59
|
video: kind !== 'audioinput' && kind !== 'audiooutput',
|
60
|
-
audio: kind !== 'videoinput' && { deviceId: 'default' },
|
60
|
+
audio: kind !== 'videoinput' && { deviceId: { ideal: 'default' } },
|
61
61
|
};
|
62
62
|
const stream = await navigator.mediaDevices.getUserMedia(permissionsToAcquire);
|
63
63
|
devices = await navigator.mediaDevices.enumerateDevices();
|
package/src/room/RTCEngine.ts
CHANGED
@@ -248,6 +248,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
248
248
|
}
|
249
249
|
|
250
250
|
this.clientConfiguration = joinResponse.clientConfiguration;
|
251
|
+
// emit signal connected event after a short delay to allow for join response to be processed on room
|
252
|
+
setTimeout(() => {
|
253
|
+
this.emit(EngineEvent.SignalConnected);
|
254
|
+
}, 10);
|
251
255
|
return joinResponse;
|
252
256
|
} catch (e) {
|
253
257
|
if (e instanceof ConnectionError) {
|
@@ -1500,6 +1504,7 @@ export type EngineEventCallbacks = {
|
|
1500
1504
|
remoteMute: (trackSid: string, muted: boolean) => void;
|
1501
1505
|
offline: () => void;
|
1502
1506
|
signalRequestResponse: (response: RequestResponse) => void;
|
1507
|
+
signalConnected: () => void;
|
1503
1508
|
};
|
1504
1509
|
|
1505
1510
|
function supportOptionalDatachannel(protocol: number | undefined): boolean {
|
package/src/room/Room.ts
CHANGED
@@ -1280,7 +1280,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1280
1280
|
* `audiooutput` to set speaker for all incoming audio tracks
|
1281
1281
|
* @param deviceId
|
1282
1282
|
*/
|
1283
|
-
async switchActiveDevice(kind: MediaDeviceKind, deviceId: string, exact: boolean =
|
1283
|
+
async switchActiveDevice(kind: MediaDeviceKind, deviceId: string, exact: boolean = true) {
|
1284
1284
|
let success = true;
|
1285
1285
|
let needsUpdateWithoutTracks = false;
|
1286
1286
|
const deviceConstraint = exact ? { exact: deviceId } : deviceId;
|
package/src/room/StreamWriter.ts
CHANGED
@@ -29,4 +29,4 @@ class BaseStreamWriter<T, InfoType extends BaseStreamInfo> {
|
|
29
29
|
|
30
30
|
export class TextStreamWriter extends BaseStreamWriter<string, TextStreamInfo> {}
|
31
31
|
|
32
|
-
export class
|
32
|
+
export class ByteStreamWriter extends BaseStreamWriter<Uint8Array, ByteStreamInfo> {}
|
package/src/room/defaults.ts
CHANGED
@@ -22,7 +22,7 @@ export const publishDefaults: TrackPublishDefaults = {
|
|
22
22
|
} as const;
|
23
23
|
|
24
24
|
export const audioDefaults: AudioCaptureOptions = {
|
25
|
-
deviceId: 'default',
|
25
|
+
deviceId: { ideal: 'default' },
|
26
26
|
autoGainControl: true,
|
27
27
|
echoCancellation: true,
|
28
28
|
noiseSuppression: true,
|
@@ -30,7 +30,7 @@ export const audioDefaults: AudioCaptureOptions = {
|
|
30
30
|
};
|
31
31
|
|
32
32
|
export const videoDefaults: VideoCaptureOptions = {
|
33
|
-
deviceId: 'default',
|
33
|
+
deviceId: { ideal: 'default' },
|
34
34
|
resolution: VideoPresets.h720.resolution,
|
35
35
|
};
|
36
36
|
|
package/src/room/events.ts
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
+
import { Mutex } from '@livekit/mutex';
|
1
2
|
import {
|
2
3
|
AddTrackRequest,
|
4
|
+
BackupCodecPolicy,
|
3
5
|
ChatMessage as ChatMessageModel,
|
4
6
|
Codec,
|
5
7
|
DataPacket,
|
@@ -26,10 +28,11 @@ import {
|
|
26
28
|
UserPacket,
|
27
29
|
protoInt64,
|
28
30
|
} from '@livekit/protocol';
|
31
|
+
import { SignalConnectionState } from '../../api/SignalClient';
|
29
32
|
import type { InternalRoomOptions } from '../../options';
|
30
33
|
import { PCTransportState } from '../PCTransportManager';
|
31
34
|
import type RTCEngine from '../RTCEngine';
|
32
|
-
import { TextStreamWriter } from '../StreamWriter';
|
35
|
+
import { ByteStreamWriter, TextStreamWriter } from '../StreamWriter';
|
33
36
|
import { defaultVideoCodec } from '../defaults';
|
34
37
|
import {
|
35
38
|
DeviceUnsupportedError,
|
@@ -71,6 +74,7 @@ import {
|
|
71
74
|
screenCaptureToDisplayMediaStreamOptions,
|
72
75
|
} from '../track/utils';
|
73
76
|
import {
|
77
|
+
type ByteStreamInfo,
|
74
78
|
type ChatMessage,
|
75
79
|
type DataPublishOptions,
|
76
80
|
type SendTextOptions,
|
@@ -527,9 +531,11 @@ export default class LocalParticipant extends Participant {
|
|
527
531
|
...this.logContext,
|
528
532
|
...getLogContextFromTrack(localTrack),
|
529
533
|
});
|
534
|
+
|
530
535
|
publishPromises.push(this.publishTrack(localTrack, publishOptions));
|
531
536
|
}
|
532
537
|
const publishedTracks = await Promise.all(publishPromises);
|
538
|
+
|
533
539
|
// for screen share publications including audio, this will only return the screen share publication, not the screen share audio one
|
534
540
|
// revisit if we want to return an array of tracks instead for v2
|
535
541
|
[track] = publishedTracks;
|
@@ -861,7 +867,47 @@ export default class LocalParticipant extends Participant {
|
|
861
867
|
if (opts.source) {
|
862
868
|
track.source = opts.source;
|
863
869
|
}
|
864
|
-
const publishPromise =
|
870
|
+
const publishPromise = new Promise<LocalTrackPublication>(async (resolve, reject) => {
|
871
|
+
try {
|
872
|
+
if (this.engine.client.currentState !== SignalConnectionState.CONNECTED) {
|
873
|
+
this.log.debug('deferring track publication until signal is connected', {
|
874
|
+
...this.logContext,
|
875
|
+
track: getLogContextFromTrack(track),
|
876
|
+
});
|
877
|
+
const onSignalConnected = async () => {
|
878
|
+
try {
|
879
|
+
const publication = await this.publish(track, opts, isStereo);
|
880
|
+
resolve(publication);
|
881
|
+
} catch (e) {
|
882
|
+
reject(e);
|
883
|
+
}
|
884
|
+
};
|
885
|
+
setTimeout(() => {
|
886
|
+
this.engine.off(EngineEvent.SignalConnected, onSignalConnected);
|
887
|
+
reject(
|
888
|
+
new PublishTrackError(
|
889
|
+
'publishing rejected as engine not connected within timeout',
|
890
|
+
408,
|
891
|
+
),
|
892
|
+
);
|
893
|
+
}, 15_000);
|
894
|
+
this.engine.once(EngineEvent.SignalConnected, onSignalConnected);
|
895
|
+
this.engine.on(EngineEvent.Closing, () => {
|
896
|
+
this.engine.off(EngineEvent.SignalConnected, onSignalConnected);
|
897
|
+
reject(new PublishTrackError('publishing rejected as engine closed', 499));
|
898
|
+
});
|
899
|
+
} else {
|
900
|
+
try {
|
901
|
+
const publication = await this.publish(track, opts, isStereo);
|
902
|
+
resolve(publication);
|
903
|
+
} catch (e) {
|
904
|
+
reject(e);
|
905
|
+
}
|
906
|
+
}
|
907
|
+
} catch (e) {
|
908
|
+
reject(e);
|
909
|
+
}
|
910
|
+
});
|
865
911
|
this.pendingPublishPromises.set(track, publishPromise);
|
866
912
|
try {
|
867
913
|
const publication = await publishPromise;
|
@@ -963,7 +1009,7 @@ export default class LocalParticipant extends Participant {
|
|
963
1009
|
stereo: isStereo,
|
964
1010
|
disableRed: this.isE2EEEnabled || !(opts.red ?? true),
|
965
1011
|
stream: opts?.stream,
|
966
|
-
backupCodecPolicy: opts?.backupCodecPolicy,
|
1012
|
+
backupCodecPolicy: opts?.backupCodecPolicy as BackupCodecPolicy,
|
967
1013
|
});
|
968
1014
|
|
969
1015
|
// compute encodings and layers for video
|
@@ -1712,23 +1758,62 @@ export default class LocalParticipant extends Participant {
|
|
1712
1758
|
onProgress?: (progress: number) => void;
|
1713
1759
|
},
|
1714
1760
|
) {
|
1715
|
-
const
|
1716
|
-
const header = new DataStream_Header({
|
1717
|
-
totalLength: numberToBigInt(totalLength),
|
1718
|
-
mimeType: options?.mimeType ?? file.type,
|
1761
|
+
const writer = await this.streamBytes({
|
1719
1762
|
streamId,
|
1763
|
+
totalSize: file.size,
|
1764
|
+
name: file.name,
|
1765
|
+
mimeType: options?.mimeType ?? file.type,
|
1720
1766
|
topic: options?.topic,
|
1721
|
-
|
1767
|
+
destinationIdentities: options?.destinationIdentities,
|
1768
|
+
});
|
1769
|
+
const reader = file.stream().getReader();
|
1770
|
+
while (true) {
|
1771
|
+
const { done, value } = await reader.read();
|
1772
|
+
if (done) {
|
1773
|
+
break;
|
1774
|
+
}
|
1775
|
+
await writer.write(value);
|
1776
|
+
}
|
1777
|
+
await writer.close();
|
1778
|
+
return writer.info;
|
1779
|
+
}
|
1780
|
+
|
1781
|
+
async streamBytes(options?: {
|
1782
|
+
name?: string;
|
1783
|
+
topic?: string;
|
1784
|
+
attributes?: Record<string, string>;
|
1785
|
+
destinationIdentities?: Array<string>;
|
1786
|
+
streamId?: string;
|
1787
|
+
mimeType?: string;
|
1788
|
+
totalSize?: number;
|
1789
|
+
}) {
|
1790
|
+
const streamId = options?.streamId ?? crypto.randomUUID();
|
1791
|
+
const destinationIdentities = options?.destinationIdentities;
|
1792
|
+
|
1793
|
+
const info: ByteStreamInfo = {
|
1794
|
+
id: streamId,
|
1795
|
+
mimeType: options?.mimeType ?? 'application/octet-stream',
|
1796
|
+
topic: options?.topic ?? '',
|
1797
|
+
timestamp: Date.now(),
|
1798
|
+
attributes: options?.attributes,
|
1799
|
+
size: options?.totalSize,
|
1800
|
+
name: options?.name ?? 'unknown',
|
1801
|
+
};
|
1802
|
+
|
1803
|
+
const header = new DataStream_Header({
|
1804
|
+
totalLength: numberToBigInt(info.size ?? 0),
|
1805
|
+
mimeType: info.mimeType,
|
1806
|
+
streamId,
|
1807
|
+
topic: info.topic,
|
1722
1808
|
timestamp: numberToBigInt(Date.now()),
|
1723
1809
|
contentHeader: {
|
1724
1810
|
case: 'byteHeader',
|
1725
1811
|
value: new DataStream_ByteHeader({
|
1726
|
-
name:
|
1812
|
+
name: info.name,
|
1727
1813
|
}),
|
1728
1814
|
},
|
1729
1815
|
});
|
1730
1816
|
|
1731
|
-
const destinationIdentities = options?.destinationIdentities;
|
1732
1817
|
const packet = new DataPacket({
|
1733
1818
|
destinationIdentities,
|
1734
1819
|
value: {
|
@@ -1738,47 +1823,61 @@ export default class LocalParticipant extends Participant {
|
|
1738
1823
|
});
|
1739
1824
|
|
1740
1825
|
await this.engine.sendDataPacket(packet, DataPacket_Kind.RELIABLE);
|
1741
|
-
|
1742
|
-
|
1743
|
-
|
1744
|
-
|
1745
|
-
|
1746
|
-
|
1747
|
-
|
1748
|
-
|
1749
|
-
|
1750
|
-
|
1751
|
-
|
1752
|
-
|
1753
|
-
|
1754
|
-
|
1755
|
-
|
1756
|
-
|
1757
|
-
|
1758
|
-
|
1759
|
-
|
1760
|
-
|
1761
|
-
|
1762
|
-
|
1763
|
-
|
1764
|
-
|
1765
|
-
|
1766
|
-
|
1767
|
-
|
1768
|
-
|
1769
|
-
|
1770
|
-
|
1771
|
-
|
1772
|
-
|
1773
|
-
|
1774
|
-
|
1775
|
-
|
1776
|
-
|
1777
|
-
|
1778
|
-
|
1826
|
+
|
1827
|
+
let chunkId = 0;
|
1828
|
+
const writeMutex = new Mutex();
|
1829
|
+
const engine = this.engine;
|
1830
|
+
const log = this.log;
|
1831
|
+
|
1832
|
+
const writableStream = new WritableStream<Uint8Array>({
|
1833
|
+
async write(chunk) {
|
1834
|
+
const unlock = await writeMutex.lock();
|
1835
|
+
|
1836
|
+
let byteOffset = 0;
|
1837
|
+
try {
|
1838
|
+
while (byteOffset < chunk.byteLength) {
|
1839
|
+
const subChunk = chunk.slice(byteOffset, byteOffset + STREAM_CHUNK_SIZE);
|
1840
|
+
await engine.waitForBufferStatusLow(DataPacket_Kind.RELIABLE);
|
1841
|
+
const chunkPacket = new DataPacket({
|
1842
|
+
destinationIdentities,
|
1843
|
+
value: {
|
1844
|
+
case: 'streamChunk',
|
1845
|
+
value: new DataStream_Chunk({
|
1846
|
+
content: subChunk,
|
1847
|
+
streamId,
|
1848
|
+
chunkIndex: numberToBigInt(chunkId),
|
1849
|
+
}),
|
1850
|
+
},
|
1851
|
+
});
|
1852
|
+
await engine.sendDataPacket(chunkPacket, DataPacket_Kind.RELIABLE);
|
1853
|
+
chunkId += 1;
|
1854
|
+
byteOffset += subChunk.byteLength;
|
1855
|
+
}
|
1856
|
+
} finally {
|
1857
|
+
unlock();
|
1858
|
+
}
|
1859
|
+
},
|
1860
|
+
async close() {
|
1861
|
+
const trailer = new DataStream_Trailer({
|
1862
|
+
streamId,
|
1863
|
+
});
|
1864
|
+
const trailerPacket = new DataPacket({
|
1865
|
+
destinationIdentities,
|
1866
|
+
value: {
|
1867
|
+
case: 'streamTrailer',
|
1868
|
+
value: trailer,
|
1869
|
+
},
|
1870
|
+
});
|
1871
|
+
await engine.sendDataPacket(trailerPacket, DataPacket_Kind.RELIABLE);
|
1872
|
+
},
|
1873
|
+
abort(err) {
|
1874
|
+
log.error('Sink error:', err);
|
1779
1875
|
},
|
1780
1876
|
});
|
1781
|
-
|
1877
|
+
|
1878
|
+
const byteWriter = new ByteStreamWriter(writableStream, info);
|
1879
|
+
|
1880
|
+
return byteWriter;
|
1782
1881
|
}
|
1783
1882
|
|
1784
1883
|
/**
|
@@ -314,7 +314,7 @@ export default abstract class LocalTrack<
|
|
314
314
|
if (!constraints) {
|
315
315
|
constraints = this._constraints;
|
316
316
|
}
|
317
|
-
const { deviceId, ...otherConstraints } = constraints;
|
317
|
+
const { deviceId, facingMode, ...otherConstraints } = constraints;
|
318
318
|
this.log.debug('restarting track with constraints', { ...this.logContext, constraints });
|
319
319
|
|
320
320
|
const streamConstraints: MediaStreamConstraints = {
|
@@ -323,7 +323,7 @@ export default abstract class LocalTrack<
|
|
323
323
|
};
|
324
324
|
|
325
325
|
if (this.kind === Track.Kind.Video) {
|
326
|
-
streamConstraints.video = deviceId ? { deviceId } : true;
|
326
|
+
streamConstraints.video = deviceId || facingMode ? { deviceId, facingMode } : true;
|
327
327
|
} else {
|
328
328
|
streamConstraints.audio = deviceId ? { deviceId } : true;
|
329
329
|
}
|
@@ -7,11 +7,12 @@ import {
|
|
7
7
|
} from '@livekit/protocol';
|
8
8
|
import type { SignalClient } from '../../api/SignalClient';
|
9
9
|
import type { StructuredLogger } from '../../logger';
|
10
|
+
import { getBrowser } from '../../utils/browserParser';
|
10
11
|
import { ScalabilityMode } from '../participant/publishUtils';
|
11
12
|
import type { VideoSenderStats } from '../stats';
|
12
13
|
import { computeBitrate, monitorFrequency } from '../stats';
|
13
14
|
import type { LoggerOptions } from '../types';
|
14
|
-
import { isFireFox, isMobile, isWeb } from '../utils';
|
15
|
+
import { compareVersions, isFireFox, isMobile, isWeb } from '../utils';
|
15
16
|
import LocalTrack from './LocalTrack';
|
16
17
|
import { Track, VideoQuality } from './Track';
|
17
18
|
import type { VideoCaptureOptions, VideoCodec } from './options';
|
@@ -455,18 +456,15 @@ async function setPublishingLayersForSender(
|
|
455
456
|
}
|
456
457
|
|
457
458
|
let hasChanged = false;
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
low resolution frame and recover very quickly, but noticable
|
462
|
-
2. livekit sfu: additional pli request cause video frozen for a few frames, also noticable */
|
463
|
-
const closableSpatial = false;
|
459
|
+
const browser = getBrowser();
|
460
|
+
const closableSpatial =
|
461
|
+
browser?.name === 'Chrome' && compareVersions(browser?.version, '133') > 0;
|
464
462
|
/* @ts-ignore */
|
465
463
|
if (closableSpatial && encodings[0].scalabilityMode) {
|
466
464
|
// svc dynacast encodings
|
467
465
|
const encoding = encodings[0];
|
468
466
|
/* @ts-ignore */
|
469
|
-
|
467
|
+
const mode = new ScalabilityMode(encoding.scalabilityMode);
|
470
468
|
let maxQuality = ProtoVideoQuality.OFF;
|
471
469
|
qualities.forEach((q) => {
|
472
470
|
if (q.enabled && (maxQuality === ProtoVideoQuality.OFF || q.quality > maxQuality)) {
|
@@ -479,22 +477,25 @@ async function setPublishingLayersForSender(
|
|
479
477
|
encoding.active = false;
|
480
478
|
hasChanged = true;
|
481
479
|
}
|
482
|
-
} else if (!encoding.active
|
480
|
+
} else if (!encoding.active || mode.spatial !== maxQuality + 1) {
|
483
481
|
hasChanged = true;
|
484
482
|
encoding.active = true;
|
485
|
-
/*
|
486
|
-
|
487
|
-
const originalMode = new ScalabilityMode(senderEncodings[0].scalabilityMode)
|
483
|
+
/* @ts-ignore */
|
484
|
+
const originalMode = new ScalabilityMode(senderEncodings[0].scalabilityMode);
|
488
485
|
mode.spatial = maxQuality + 1;
|
489
486
|
mode.suffix = originalMode.suffix;
|
490
487
|
if (mode.spatial === 1) {
|
491
488
|
// no suffix for L1Tx
|
492
489
|
mode.suffix = undefined;
|
493
490
|
}
|
494
|
-
@ts-ignore
|
491
|
+
/* @ts-ignore */
|
495
492
|
encoding.scalabilityMode = mode.toString();
|
496
493
|
encoding.scaleResolutionDownBy = 2 ** (2 - maxQuality);
|
497
|
-
|
494
|
+
if (senderEncodings[0].maxBitrate) {
|
495
|
+
encoding.maxBitrate =
|
496
|
+
senderEncodings[0].maxBitrate /
|
497
|
+
(encoding.scaleResolutionDownBy * encoding.scaleResolutionDownBy);
|
498
|
+
}
|
498
499
|
}
|
499
500
|
} else {
|
500
501
|
// simulcast dynacast encodings
|