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.
Files changed (48) hide show
  1. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  2. package/dist/livekit-client.e2ee.worker.mjs +3 -3
  3. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  4. package/dist/livekit-client.esm.mjs +341 -135
  5. package/dist/livekit-client.esm.mjs.map +1 -1
  6. package/dist/livekit-client.umd.js +1 -1
  7. package/dist/livekit-client.umd.js.map +1 -1
  8. package/dist/src/api/SignalClient.d.ts.map +1 -1
  9. package/dist/src/api/utils.d.ts +3 -0
  10. package/dist/src/api/utils.d.ts.map +1 -0
  11. package/dist/src/e2ee/KeyProvider.d.ts.map +1 -1
  12. package/dist/src/room/RTCEngine.d.ts +1 -0
  13. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  14. package/dist/src/room/Room.d.ts.map +1 -1
  15. package/dist/src/room/StreamWriter.d.ts +1 -1
  16. package/dist/src/room/StreamWriter.d.ts.map +1 -1
  17. package/dist/src/room/events.d.ts +2 -1
  18. package/dist/src/room/events.d.ts.map +1 -1
  19. package/dist/src/room/participant/LocalParticipant.d.ts +10 -1
  20. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  21. package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
  22. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  23. package/dist/src/room/track/RemoteTrackPublication.d.ts.map +1 -1
  24. package/dist/src/room/track/create.d.ts.map +1 -1
  25. package/dist/src/room/track/options.d.ts +3 -2
  26. package/dist/src/room/track/options.d.ts.map +1 -1
  27. package/dist/ts4.2/src/api/utils.d.ts +3 -0
  28. package/dist/ts4.2/src/room/RTCEngine.d.ts +1 -0
  29. package/dist/ts4.2/src/room/StreamWriter.d.ts +1 -1
  30. package/dist/ts4.2/src/room/events.d.ts +2 -1
  31. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +10 -1
  32. package/dist/ts4.2/src/room/track/options.d.ts +3 -2
  33. package/package.json +13 -13
  34. package/src/api/SignalClient.ts +8 -17
  35. package/src/api/utils.test.ts +112 -0
  36. package/src/api/utils.ts +23 -0
  37. package/src/room/DeviceManager.ts +1 -1
  38. package/src/room/RTCEngine.ts +5 -0
  39. package/src/room/Room.ts +1 -1
  40. package/src/room/StreamWriter.ts +1 -1
  41. package/src/room/defaults.ts +2 -2
  42. package/src/room/events.ts +1 -0
  43. package/src/room/participant/LocalParticipant.ts +148 -49
  44. package/src/room/track/LocalTrack.ts +2 -2
  45. package/src/room/track/LocalVideoTrack.ts +15 -14
  46. package/src/room/track/create.ts +87 -47
  47. package/src/room/track/options.ts +5 -1
  48. 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
+ });
@@ -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();
@@ -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 = false) {
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;
@@ -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 BinaryStreamWriter extends BaseStreamWriter<Uint8Array, ByteStreamInfo> {}
32
+ export class ByteStreamWriter extends BaseStreamWriter<Uint8Array, ByteStreamInfo> {}
@@ -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
 
@@ -562,6 +562,7 @@ export enum EngineEvent {
562
562
  LocalTrackSubscribed = 'localTrackSubscribed',
563
563
  Offline = 'offline',
564
564
  SignalRequestResponse = 'signalRequestResponse',
565
+ SignalConnected = 'signalConnected',
565
566
  }
566
567
 
567
568
  export enum TrackEvent {
@@ -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 = this.publish(track, opts, isStereo);
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 totalLength = file.size;
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
- encryptionType: options?.encryptionType,
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: file.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
- function read(b: Blob): Promise<Uint8Array> {
1742
- return new Promise((resolve) => {
1743
- const fr = new FileReader();
1744
- fr.onload = () => {
1745
- resolve(new Uint8Array(fr.result as ArrayBuffer));
1746
- };
1747
- fr.readAsArrayBuffer(b);
1748
- });
1749
- }
1750
- const totalChunks = Math.ceil(totalLength / STREAM_CHUNK_SIZE);
1751
- for (let i = 0; i < totalChunks; i++) {
1752
- const chunkData = await read(
1753
- file.slice(i * STREAM_CHUNK_SIZE, Math.min((i + 1) * STREAM_CHUNK_SIZE, totalLength)),
1754
- );
1755
- await this.engine.waitForBufferStatusLow(DataPacket_Kind.RELIABLE);
1756
- const chunk = new DataStream_Chunk({
1757
- content: chunkData,
1758
- streamId,
1759
- chunkIndex: numberToBigInt(i),
1760
- });
1761
- const chunkPacket = new DataPacket({
1762
- destinationIdentities,
1763
- value: {
1764
- case: 'streamChunk',
1765
- value: chunk,
1766
- },
1767
- });
1768
- await this.engine.sendDataPacket(chunkPacket, DataPacket_Kind.RELIABLE);
1769
- options?.onProgress?.((i + 1) / totalChunks);
1770
- }
1771
- const trailer = new DataStream_Trailer({
1772
- streamId,
1773
- });
1774
- const trailerPacket = new DataPacket({
1775
- destinationIdentities,
1776
- value: {
1777
- case: 'streamTrailer',
1778
- value: trailer,
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
- await this.engine.sendDataPacket(trailerPacket, DataPacket_Kind.RELIABLE);
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
- /* disable closable spatial layer as it has video blur / frozen issue with current server / client
460
- 1. chrome 113: when switching to up layer with scalability Mode change, it will generate a
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
- // const mode = new ScalabilityMode(encoding.scalabilityMode);
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 /* || mode.spatial !== maxQuality + 1*/) {
480
+ } else if (!encoding.active || mode.spatial !== maxQuality + 1) {
483
481
  hasChanged = true;
484
482
  encoding.active = true;
485
- /*
486
- @ts-ignore
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