livekit-client 2.15.4 → 2.15.6

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 (84) hide show
  1. package/dist/livekit-client.e2ee.worker.js +1 -1
  2. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  3. package/dist/livekit-client.e2ee.worker.mjs +373 -164
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +982 -643
  6. package/dist/livekit-client.esm.mjs.map +1 -1
  7. package/dist/livekit-client.umd.js +1 -1
  8. package/dist/livekit-client.umd.js.map +1 -1
  9. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  10. package/dist/src/e2ee/worker/FrameCryptor.d.ts +0 -47
  11. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  12. package/dist/src/e2ee/worker/naluUtils.d.ts +27 -0
  13. package/dist/src/e2ee/worker/naluUtils.d.ts.map +1 -0
  14. package/dist/src/e2ee/worker/sifPayload.d.ts +22 -0
  15. package/dist/src/e2ee/worker/sifPayload.d.ts.map +1 -0
  16. package/dist/src/index.d.ts +2 -2
  17. package/dist/src/index.d.ts.map +1 -1
  18. package/dist/src/room/Room.d.ts +6 -10
  19. package/dist/src/room/Room.d.ts.map +1 -1
  20. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts +20 -0
  21. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -0
  22. package/dist/{ts4.2/src/room → src/room/data-stream/incoming}/StreamReader.d.ts +82 -56
  23. package/dist/src/room/data-stream/incoming/StreamReader.d.ts.map +1 -0
  24. package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +27 -0
  25. package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts.map +1 -0
  26. package/dist/src/room/{StreamWriter.d.ts → data-stream/outgoing/StreamWriter.d.ts} +1 -1
  27. package/dist/src/room/data-stream/outgoing/StreamWriter.d.ts.map +1 -0
  28. package/dist/src/room/errors.d.ts +13 -0
  29. package/dist/src/room/errors.d.ts.map +1 -1
  30. package/dist/src/room/participant/LocalParticipant.d.ts +32 -19
  31. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  32. package/dist/src/room/track/LocalTrack.d.ts +7 -2
  33. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  34. package/dist/src/room/track/RemoteVideoTrack.d.ts +1 -0
  35. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  36. package/dist/src/room/track/Track.d.ts +4 -1
  37. package/dist/src/room/track/Track.d.ts.map +1 -1
  38. package/dist/src/room/types.d.ts +17 -1
  39. package/dist/src/room/types.d.ts.map +1 -1
  40. package/dist/src/room/utils.d.ts +8 -0
  41. package/dist/src/room/utils.d.ts.map +1 -1
  42. package/dist/ts4.2/src/e2ee/worker/FrameCryptor.d.ts +0 -47
  43. package/dist/ts4.2/src/e2ee/worker/naluUtils.d.ts +27 -0
  44. package/dist/ts4.2/src/e2ee/worker/sifPayload.d.ts +22 -0
  45. package/dist/ts4.2/src/index.d.ts +2 -2
  46. package/dist/ts4.2/src/room/Room.d.ts +6 -10
  47. package/dist/ts4.2/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts +20 -0
  48. package/dist/{src/room → ts4.2/src/room/data-stream/incoming}/StreamReader.d.ts +82 -56
  49. package/dist/ts4.2/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +27 -0
  50. package/dist/ts4.2/src/room/{StreamWriter.d.ts → data-stream/outgoing/StreamWriter.d.ts} +1 -1
  51. package/dist/ts4.2/src/room/errors.d.ts +13 -0
  52. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +32 -19
  53. package/dist/ts4.2/src/room/track/LocalTrack.d.ts +7 -2
  54. package/dist/ts4.2/src/room/track/RemoteVideoTrack.d.ts +1 -0
  55. package/dist/ts4.2/src/room/track/Track.d.ts +4 -1
  56. package/dist/ts4.2/src/room/types.d.ts +17 -1
  57. package/dist/ts4.2/src/room/utils.d.ts +8 -0
  58. package/package.json +7 -7
  59. package/src/e2ee/E2eeManager.ts +18 -1
  60. package/src/e2ee/worker/FrameCryptor.ts +56 -157
  61. package/src/e2ee/worker/e2ee.worker.ts +6 -1
  62. package/src/e2ee/worker/naluUtils.ts +328 -0
  63. package/src/e2ee/worker/sifPayload.ts +75 -0
  64. package/src/index.ts +2 -2
  65. package/src/room/Room.ts +104 -208
  66. package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +247 -0
  67. package/src/room/data-stream/incoming/StreamReader.ts +317 -0
  68. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +316 -0
  69. package/src/room/{StreamWriter.ts → data-stream/outgoing/StreamWriter.ts} +1 -1
  70. package/src/room/errors.ts +34 -0
  71. package/src/room/participant/LocalParticipant.ts +39 -295
  72. package/src/room/track/LocalAudioTrack.ts +2 -2
  73. package/src/room/track/LocalTrack.ts +70 -50
  74. package/src/room/track/RemoteVideoTrack.ts +12 -2
  75. package/src/room/track/Track.ts +10 -1
  76. package/src/room/types.ts +22 -1
  77. package/src/room/utils.ts +14 -5
  78. package/dist/src/e2ee/worker/SifGuard.d.ts +0 -11
  79. package/dist/src/e2ee/worker/SifGuard.d.ts.map +0 -1
  80. package/dist/src/room/StreamReader.d.ts.map +0 -1
  81. package/dist/src/room/StreamWriter.d.ts.map +0 -1
  82. package/dist/ts4.2/src/e2ee/worker/SifGuard.d.ts +0 -11
  83. package/src/e2ee/worker/SifGuard.ts +0 -47
  84. package/src/room/StreamReader.ts +0 -170
@@ -1,4 +1,3 @@
1
- import { Mutex } from '@livekit/mutex';
2
1
  import {
3
2
  AddTrackRequest,
4
3
  AudioTrackFeature,
@@ -7,12 +6,6 @@ import {
7
6
  Codec,
8
7
  DataPacket,
9
8
  DataPacket_Kind,
10
- DataStream_ByteHeader,
11
- DataStream_Chunk,
12
- DataStream_Header,
13
- DataStream_OperationType,
14
- DataStream_TextHeader,
15
- DataStream_Trailer,
16
9
  Encryption_Type,
17
10
  JoinResponse,
18
11
  ParticipantInfo,
@@ -34,7 +27,8 @@ import { SignalConnectionState } from '../../api/SignalClient';
34
27
  import type { InternalRoomOptions } from '../../options';
35
28
  import { PCTransportState } from '../PCTransportManager';
36
29
  import type RTCEngine from '../RTCEngine';
37
- import { ByteStreamWriter, TextStreamWriter } from '../StreamWriter';
30
+ import type OutgoingDataStreamManager from '../data-stream/outgoing/OutgoingDataStreamManager';
31
+ import type { TextStreamWriter } from '../data-stream/outgoing/StreamWriter';
38
32
  import { defaultVideoCodec } from '../defaults';
39
33
  import {
40
34
  DeviceUnsupportedError,
@@ -76,10 +70,11 @@ import {
76
70
  sourceToKind,
77
71
  } from '../track/utils';
78
72
  import {
79
- type ByteStreamInfo,
80
73
  type ChatMessage,
81
74
  type DataPublishOptions,
75
+ type SendFileOptions,
82
76
  type SendTextOptions,
77
+ type StreamBytesOptions,
83
78
  type StreamTextOptions,
84
79
  type TextStreamInfo,
85
80
  } from '../types';
@@ -96,9 +91,7 @@ import {
96
91
  isSafari17Based,
97
92
  isVideoTrack,
98
93
  isWeb,
99
- numberToBigInt,
100
94
  sleep,
101
- splitUtf8,
102
95
  supportsAV1,
103
96
  supportsVP9,
104
97
  } from '../utils';
@@ -112,8 +105,6 @@ import {
112
105
  getDefaultDegradationPreference,
113
106
  } from './publishUtils';
114
107
 
115
- const STREAM_CHUNK_SIZE = 15_000;
116
-
117
108
  export default class LocalParticipant extends Participant {
118
109
  audioTrackPublications: Map<string, LocalTrackPublication>;
119
110
 
@@ -157,6 +148,8 @@ export default class LocalParticipant extends Participant {
157
148
 
158
149
  private rpcHandlers: Map<string, (data: RpcInvocationData) => Promise<string>>;
159
150
 
151
+ private roomOutgoingDataStreamManager: OutgoingDataStreamManager;
152
+
160
153
  private pendingSignalRequests: Map<
161
154
  number,
162
155
  {
@@ -185,6 +178,7 @@ export default class LocalParticipant extends Participant {
185
178
  engine: RTCEngine,
186
179
  options: InternalRoomOptions,
187
180
  roomRpcHandlers: Map<string, (data: RpcInvocationData) => Promise<string>>,
181
+ roomOutgoingDataStreamManager: OutgoingDataStreamManager,
188
182
  ) {
189
183
  super(sid, identity, undefined, undefined, undefined, {
190
184
  loggerName: options.loggerName,
@@ -203,6 +197,7 @@ export default class LocalParticipant extends Participant {
203
197
  ]);
204
198
  this.pendingSignalRequests = new Map();
205
199
  this.rpcHandlers = roomRpcHandlers;
200
+ this.roomOutgoingDataStreamManager = roomOutgoingDataStreamManager;
206
201
  }
207
202
 
208
203
  get lastCameraError(): Error | undefined {
@@ -1690,6 +1685,7 @@ export default class LocalParticipant extends Participant {
1690
1685
  await this.engine.sendDataPacket(packet, DataPacket_Kind.RELIABLE);
1691
1686
  }
1692
1687
 
1688
+ /** @deprecated Consider migrating to {@link sendText} */
1693
1689
  async sendChatMessage(text: string, options?: SendTextOptions): Promise<ChatMessage> {
1694
1690
  const msg = {
1695
1691
  id: crypto.randomUUID(),
@@ -1712,6 +1708,7 @@ export default class LocalParticipant extends Participant {
1712
1708
  return msg;
1713
1709
  }
1714
1710
 
1711
+ /** @deprecated Consider migrating to {@link sendText} */
1715
1712
  async editChatMessage(editText: string, originalMessage: ChatMessage) {
1716
1713
  const msg = {
1717
1714
  ...originalMessage,
@@ -1733,300 +1730,47 @@ export default class LocalParticipant extends Participant {
1733
1730
  return msg;
1734
1731
  }
1735
1732
 
1733
+ /**
1734
+ * Sends the given string to participants in the room via the data channel.
1735
+ * For longer messages, consider using {@link streamText} instead.
1736
+ *
1737
+ * @param text The text payload
1738
+ * @param options.topic Topic identifier used to route the stream to appropriate handlers.
1739
+ */
1736
1740
  async sendText(text: string, options?: SendTextOptions): Promise<TextStreamInfo> {
1737
- const streamId = crypto.randomUUID();
1738
- const textInBytes = new TextEncoder().encode(text);
1739
- const totalTextLength = textInBytes.byteLength;
1740
-
1741
- const fileIds = options?.attachments?.map(() => crypto.randomUUID());
1742
-
1743
- const progresses = new Array<number>(fileIds ? fileIds.length + 1 : 1).fill(0);
1744
-
1745
- const handleProgress = (progress: number, idx: number) => {
1746
- progresses[idx] = progress;
1747
- const totalProgress = progresses.reduce((acc, val) => acc + val, 0);
1748
- options?.onProgress?.(totalProgress);
1749
- };
1750
-
1751
- const writer = await this.streamText({
1752
- streamId,
1753
- totalSize: totalTextLength,
1754
- destinationIdentities: options?.destinationIdentities,
1755
- topic: options?.topic,
1756
- attachedStreamIds: fileIds,
1757
- attributes: options?.attributes,
1758
- });
1759
-
1760
- await writer.write(text);
1761
- // set text part of progress to 1
1762
- handleProgress(1, 0);
1763
-
1764
- await writer.close();
1765
-
1766
- if (options?.attachments && fileIds) {
1767
- await Promise.all(
1768
- options.attachments.map(async (file, idx) =>
1769
- this._sendFile(fileIds[idx], file, {
1770
- topic: options.topic,
1771
- mimeType: file.type,
1772
- onProgress: (progress) => {
1773
- handleProgress(progress, idx + 1);
1774
- },
1775
- }),
1776
- ),
1777
- );
1778
- }
1779
- return writer.info;
1741
+ return this.roomOutgoingDataStreamManager.sendText(text, options);
1780
1742
  }
1781
1743
 
1782
1744
  /**
1745
+ * Creates a new TextStreamWriter which can be used to stream text incrementally
1746
+ * to participants in the room via the data channel.
1747
+ *
1748
+ * @param options.topic Topic identifier used to route the stream to appropriate handlers.
1749
+ *
1783
1750
  * @internal
1784
1751
  * @experimental CAUTION, might get removed in a minor release
1785
1752
  */
1786
1753
  async streamText(options?: StreamTextOptions): Promise<TextStreamWriter> {
1787
- const streamId = options?.streamId ?? crypto.randomUUID();
1788
-
1789
- const info: TextStreamInfo = {
1790
- id: streamId,
1791
- mimeType: 'text/plain',
1792
- timestamp: Date.now(),
1793
- topic: options?.topic ?? '',
1794
- size: options?.totalSize,
1795
- attributes: options?.attributes,
1796
- };
1797
- const header = new DataStream_Header({
1798
- streamId,
1799
- mimeType: info.mimeType,
1800
- topic: info.topic,
1801
- timestamp: numberToBigInt(info.timestamp),
1802
- totalLength: numberToBigInt(options?.totalSize),
1803
- attributes: info.attributes,
1804
- contentHeader: {
1805
- case: 'textHeader',
1806
- value: new DataStream_TextHeader({
1807
- version: options?.version,
1808
- attachedStreamIds: options?.attachedStreamIds,
1809
- replyToStreamId: options?.replyToStreamId,
1810
- operationType:
1811
- options?.type === 'update'
1812
- ? DataStream_OperationType.UPDATE
1813
- : DataStream_OperationType.CREATE,
1814
- }),
1815
- },
1816
- });
1817
- const destinationIdentities = options?.destinationIdentities;
1818
- const packet = new DataPacket({
1819
- destinationIdentities,
1820
- value: {
1821
- case: 'streamHeader',
1822
- value: header,
1823
- },
1824
- });
1825
- await this.engine.sendDataPacket(packet, DataPacket_Kind.RELIABLE);
1826
-
1827
- let chunkId = 0;
1828
- const localP = this;
1829
-
1830
- const writableStream = new WritableStream<string>({
1831
- // Implement the sink
1832
- async write(text) {
1833
- for (const textByteChunk of splitUtf8(text, STREAM_CHUNK_SIZE)) {
1834
- await localP.engine.waitForBufferStatusLow(DataPacket_Kind.RELIABLE);
1835
- const chunk = new DataStream_Chunk({
1836
- content: textByteChunk,
1837
- streamId,
1838
- chunkIndex: numberToBigInt(chunkId),
1839
- });
1840
- const chunkPacket = new DataPacket({
1841
- destinationIdentities,
1842
- value: {
1843
- case: 'streamChunk',
1844
- value: chunk,
1845
- },
1846
- });
1847
- await localP.engine.sendDataPacket(chunkPacket, DataPacket_Kind.RELIABLE);
1848
-
1849
- chunkId += 1;
1850
- }
1851
- },
1852
- async close() {
1853
- const trailer = new DataStream_Trailer({
1854
- streamId,
1855
- });
1856
- const trailerPacket = new DataPacket({
1857
- destinationIdentities,
1858
- value: {
1859
- case: 'streamTrailer',
1860
- value: trailer,
1861
- },
1862
- });
1863
- await localP.engine.sendDataPacket(trailerPacket, DataPacket_Kind.RELIABLE);
1864
- },
1865
- abort(err) {
1866
- console.log('Sink error:', err);
1867
- // TODO handle aborts to signal something to receiver side
1868
- },
1869
- });
1870
-
1871
- let onEngineClose = async () => {
1872
- await writer.close();
1873
- };
1874
-
1875
- localP.engine.once(EngineEvent.Closing, onEngineClose);
1876
-
1877
- const writer = new TextStreamWriter(writableStream, info, () =>
1878
- this.engine.off(EngineEvent.Closing, onEngineClose),
1879
- );
1880
-
1881
- return writer;
1754
+ return this.roomOutgoingDataStreamManager.streamText(options);
1882
1755
  }
1883
1756
 
1884
- async sendFile(
1885
- file: File,
1886
- options?: {
1887
- mimeType?: string;
1888
- topic?: string;
1889
- destinationIdentities?: Array<string>;
1890
- onProgress?: (progress: number) => void;
1891
- },
1892
- ): Promise<{ id: string }> {
1893
- const streamId = crypto.randomUUID();
1894
- await this._sendFile(streamId, file, options);
1895
- return { id: streamId };
1896
- }
1897
-
1898
- private async _sendFile(
1899
- streamId: string,
1900
- file: File,
1901
- options?: {
1902
- mimeType?: string;
1903
- topic?: string;
1904
- encryptionType?: Encryption_Type.NONE;
1905
- destinationIdentities?: Array<string>;
1906
- onProgress?: (progress: number) => void;
1907
- },
1908
- ) {
1909
- const writer = await this.streamBytes({
1910
- streamId,
1911
- totalSize: file.size,
1912
- name: file.name,
1913
- mimeType: options?.mimeType ?? file.type,
1914
- topic: options?.topic,
1915
- destinationIdentities: options?.destinationIdentities,
1916
- });
1917
- const reader = file.stream().getReader();
1918
- while (true) {
1919
- const { done, value } = await reader.read();
1920
- if (done) {
1921
- break;
1922
- }
1923
- await writer.write(value);
1924
- }
1925
- await writer.close();
1926
- return writer.info;
1757
+ /** Send a File to all participants in the room via the data channel.
1758
+ * @param file The File object payload
1759
+ * @param options.topic Topic identifier used to route the stream to appropriate handlers.
1760
+ * @param options.onProgress A callback function used to monitor the upload progress percentage.
1761
+ */
1762
+ async sendFile(file: File, options?: SendFileOptions): Promise<{ id: string }> {
1763
+ return this.roomOutgoingDataStreamManager.sendFile(file, options);
1927
1764
  }
1928
1765
 
1929
- async streamBytes(options?: {
1930
- name?: string;
1931
- topic?: string;
1932
- attributes?: Record<string, string>;
1933
- destinationIdentities?: Array<string>;
1934
- streamId?: string;
1935
- mimeType?: string;
1936
- totalSize?: number;
1937
- }) {
1938
- const streamId = options?.streamId ?? crypto.randomUUID();
1939
- const destinationIdentities = options?.destinationIdentities;
1940
-
1941
- const info: ByteStreamInfo = {
1942
- id: streamId,
1943
- mimeType: options?.mimeType ?? 'application/octet-stream',
1944
- topic: options?.topic ?? '',
1945
- timestamp: Date.now(),
1946
- attributes: options?.attributes,
1947
- size: options?.totalSize,
1948
- name: options?.name ?? 'unknown',
1949
- };
1950
-
1951
- const header = new DataStream_Header({
1952
- totalLength: numberToBigInt(info.size ?? 0),
1953
- mimeType: info.mimeType,
1954
- streamId,
1955
- topic: info.topic,
1956
- timestamp: numberToBigInt(Date.now()),
1957
- attributes: info.attributes,
1958
- contentHeader: {
1959
- case: 'byteHeader',
1960
- value: new DataStream_ByteHeader({
1961
- name: info.name,
1962
- }),
1963
- },
1964
- });
1965
-
1966
- const packet = new DataPacket({
1967
- destinationIdentities,
1968
- value: {
1969
- case: 'streamHeader',
1970
- value: header,
1971
- },
1972
- });
1973
-
1974
- await this.engine.sendDataPacket(packet, DataPacket_Kind.RELIABLE);
1975
-
1976
- let chunkId = 0;
1977
- const writeMutex = new Mutex();
1978
- const engine = this.engine;
1979
- const log = this.log;
1980
-
1981
- const writableStream = new WritableStream<Uint8Array>({
1982
- async write(chunk) {
1983
- const unlock = await writeMutex.lock();
1984
-
1985
- let byteOffset = 0;
1986
- try {
1987
- while (byteOffset < chunk.byteLength) {
1988
- const subChunk = chunk.slice(byteOffset, byteOffset + STREAM_CHUNK_SIZE);
1989
- await engine.waitForBufferStatusLow(DataPacket_Kind.RELIABLE);
1990
- const chunkPacket = new DataPacket({
1991
- destinationIdentities,
1992
- value: {
1993
- case: 'streamChunk',
1994
- value: new DataStream_Chunk({
1995
- content: subChunk,
1996
- streamId,
1997
- chunkIndex: numberToBigInt(chunkId),
1998
- }),
1999
- },
2000
- });
2001
- await engine.sendDataPacket(chunkPacket, DataPacket_Kind.RELIABLE);
2002
- chunkId += 1;
2003
- byteOffset += subChunk.byteLength;
2004
- }
2005
- } finally {
2006
- unlock();
2007
- }
2008
- },
2009
- async close() {
2010
- const trailer = new DataStream_Trailer({
2011
- streamId,
2012
- });
2013
- const trailerPacket = new DataPacket({
2014
- destinationIdentities,
2015
- value: {
2016
- case: 'streamTrailer',
2017
- value: trailer,
2018
- },
2019
- });
2020
- await engine.sendDataPacket(trailerPacket, DataPacket_Kind.RELIABLE);
2021
- },
2022
- abort(err) {
2023
- log.error('Sink error:', err);
2024
- },
2025
- });
2026
-
2027
- const byteWriter = new ByteStreamWriter(writableStream, info);
2028
-
2029
- return byteWriter;
1766
+ /**
1767
+ * Stream bytes incrementally to participants in the room via the data channel.
1768
+ * For sending files, consider using {@link sendFile} instead.
1769
+ *
1770
+ * @param options.topic Topic identifier used to route the stream to appropriate handlers.
1771
+ */
1772
+ async streamBytes(options?: StreamBytesOptions) {
1773
+ return this.roomOutgoingDataStreamManager.streamBytes(options);
2030
1774
  }
2031
1775
 
2032
1776
  /**
@@ -169,7 +169,7 @@ export default class LocalAudioTrack extends LocalTrack<Track.Kind.Audio> {
169
169
  };
170
170
 
171
171
  async setProcessor(processor: TrackProcessor<Track.Kind.Audio, AudioProcessorOptions>) {
172
- const unlock = await this.processorLock.lock();
172
+ const unlock = await this.trackChangeLock.lock();
173
173
  try {
174
174
  if (!isReactNative() && !this.audioContext) {
175
175
  throw Error(
@@ -177,7 +177,7 @@ export default class LocalAudioTrack extends LocalTrack<Track.Kind.Audio> {
177
177
  );
178
178
  }
179
179
  if (this.processor) {
180
- await this.stopProcessor();
180
+ await this.internalStopProcessor();
181
181
  }
182
182
 
183
183
  const processorOptions = {
@@ -57,15 +57,13 @@ export default abstract class LocalTrack<
57
57
 
58
58
  protected processor?: TrackProcessor<TrackKind, any>;
59
59
 
60
- protected processorLock: Mutex;
61
-
62
60
  protected audioContext?: AudioContext;
63
61
 
64
62
  protected manuallyStopped: boolean = false;
65
63
 
66
64
  protected localTrackRecorder: LocalTrackRecorder<typeof this> | undefined;
67
65
 
68
- private restartLock: Mutex;
66
+ protected trackChangeLock: Mutex;
69
67
 
70
68
  /**
71
69
  *
@@ -86,9 +84,14 @@ export default abstract class LocalTrack<
86
84
  this.providedByUser = userProvidedTrack;
87
85
  this.muteLock = new Mutex();
88
86
  this.pauseUpstreamLock = new Mutex();
89
- this.processorLock = new Mutex();
90
- this.restartLock = new Mutex();
91
- this.setMediaStreamTrack(mediaTrack, true);
87
+ this.trackChangeLock = new Mutex();
88
+ this.trackChangeLock.lock().then(async (unlock) => {
89
+ try {
90
+ await this.setMediaStreamTrack(mediaTrack, true);
91
+ } finally {
92
+ unlock();
93
+ }
94
+ });
92
95
 
93
96
  // added to satisfy TS compiler, constraints are synced with MediaStreamTrack
94
97
  this._constraints = mediaTrack.getConstraints();
@@ -171,27 +174,22 @@ export default abstract class LocalTrack<
171
174
  }
172
175
  let processedTrack: MediaStreamTrack | undefined;
173
176
  if (this.processor && newTrack) {
174
- const unlock = await this.processorLock.lock();
175
- try {
176
- this.log.debug('restarting processor', this.logContext);
177
- if (this.kind === 'unknown') {
178
- throw TypeError('cannot set processor on track of unknown kind');
179
- }
177
+ this.log.debug('restarting processor', this.logContext);
178
+ if (this.kind === 'unknown') {
179
+ throw TypeError('cannot set processor on track of unknown kind');
180
+ }
180
181
 
181
- if (this.processorElement) {
182
- attachToElement(newTrack, this.processorElement);
183
- // ensure the processorElement itself stays muted
184
- this.processorElement.muted = true;
185
- }
186
- await this.processor.restart({
187
- track: newTrack,
188
- kind: this.kind,
189
- element: this.processorElement,
190
- });
191
- processedTrack = this.processor.processedTrack;
192
- } finally {
193
- unlock();
182
+ if (this.processorElement) {
183
+ attachToElement(newTrack, this.processorElement);
184
+ // ensure the processorElement itself stays muted
185
+ this.processorElement.muted = true;
194
186
  }
187
+ await this.processor.restart({
188
+ track: newTrack,
189
+ kind: this.kind,
190
+ element: this.processorElement,
191
+ });
192
+ processedTrack = this.processor.processedTrack;
195
193
  }
196
194
  if (this.sender && this.sender.transport?.state !== 'closed') {
197
195
  await this.sender.replaceTrack(processedTrack ?? newTrack);
@@ -290,36 +288,42 @@ export default abstract class LocalTrack<
290
288
  track: MediaStreamTrack,
291
289
  userProvidedOrOptions: boolean | ReplaceTrackOptions | undefined,
292
290
  ) {
293
- if (!this.sender) {
294
- throw new TrackInvalidError('unable to replace an unpublished track');
295
- }
291
+ const unlock = await this.trackChangeLock.lock();
292
+ try {
293
+ if (!this.sender) {
294
+ throw new TrackInvalidError('unable to replace an unpublished track');
295
+ }
296
296
 
297
- let userProvidedTrack: boolean | undefined;
298
- let stopProcessor: boolean | undefined;
297
+ let userProvidedTrack: boolean | undefined;
298
+ let stopProcessor: boolean | undefined;
299
299
 
300
- if (typeof userProvidedOrOptions === 'boolean') {
301
- userProvidedTrack = userProvidedOrOptions;
302
- } else if (userProvidedOrOptions !== undefined) {
303
- userProvidedTrack = userProvidedOrOptions.userProvidedTrack;
304
- stopProcessor = userProvidedOrOptions.stopProcessor;
305
- }
300
+ if (typeof userProvidedOrOptions === 'boolean') {
301
+ userProvidedTrack = userProvidedOrOptions;
302
+ } else if (userProvidedOrOptions !== undefined) {
303
+ userProvidedTrack = userProvidedOrOptions.userProvidedTrack;
304
+ stopProcessor = userProvidedOrOptions.stopProcessor;
305
+ }
306
306
 
307
- this.providedByUser = userProvidedTrack ?? true;
307
+ this.providedByUser = userProvidedTrack ?? true;
308
308
 
309
- this.log.debug('replace MediaStreamTrack', this.logContext);
310
- await this.setMediaStreamTrack(track);
311
- // this must be synced *after* setting mediaStreamTrack above, since it relies
312
- // on the previous state in order to cleanup
309
+ this.log.debug('replace MediaStreamTrack', this.logContext);
310
+ await this.setMediaStreamTrack(track);
311
+ // this must be synced *after* setting mediaStreamTrack above, since it relies
312
+ // on the previous state in order to cleanup
313
313
 
314
- if (stopProcessor && this.processor) {
315
- await this.stopProcessor();
314
+ if (stopProcessor && this.processor) {
315
+ await this.internalStopProcessor();
316
+ }
317
+ return this;
318
+ } finally {
319
+ unlock();
316
320
  }
317
- return this;
318
321
  }
319
322
 
320
323
  protected async restart(constraints?: MediaTrackConstraints) {
321
324
  this.manuallyStopped = false;
322
- const unlock = await this.restartLock.lock();
325
+ const unlock = await this.trackChangeLock.lock();
326
+
323
327
  try {
324
328
  if (!constraints) {
325
329
  constraints = this._constraints;
@@ -335,7 +339,7 @@ export default abstract class LocalTrack<
335
339
  if (this.kind === Track.Kind.Video) {
336
340
  streamConstraints.video = deviceId || facingMode ? { deviceId, facingMode } : true;
337
341
  } else {
338
- streamConstraints.audio = deviceId ? { deviceId } : true;
342
+ streamConstraints.audio = deviceId ? { deviceId, ...otherConstraints } : true;
339
343
  }
340
344
 
341
345
  // these steps are duplicated from setMediaStreamTrack because we must stop
@@ -352,7 +356,10 @@ export default abstract class LocalTrack<
352
356
  // create new track and attach
353
357
  const mediaStream = await navigator.mediaDevices.getUserMedia(streamConstraints);
354
358
  const newTrack = mediaStream.getTracks()[0];
355
- await newTrack.applyConstraints(otherConstraints);
359
+ if (this.kind === Track.Kind.Video) {
360
+ // we already captured the audio track with the constraints, so we only need to apply the video constraints
361
+ await newTrack.applyConstraints(otherConstraints);
362
+ }
356
363
  newTrack.addEventListener('ended', this.handleEnded);
357
364
  this.log.debug('re-acquired MediaStreamTrack', this.logContext);
358
365
 
@@ -518,7 +525,7 @@ export default abstract class LocalTrack<
518
525
  * @returns
519
526
  */
520
527
  async setProcessor(processor: TrackProcessor<TrackKind>, showProcessedStreamLocally = true) {
521
- const unlock = await this.processorLock.lock();
528
+ const unlock = await this.trackChangeLock.lock();
522
529
  try {
523
530
  this.log.debug('setting up processor', this.logContext);
524
531
 
@@ -534,7 +541,7 @@ export default abstract class LocalTrack<
534
541
  this.log.debug('processor initialized', this.logContext);
535
542
 
536
543
  if (this.processor) {
537
- await this.stopProcessor();
544
+ await this.internalStopProcessor();
538
545
  }
539
546
  if (this.kind === 'unknown') {
540
547
  throw TypeError('cannot set processor on track of unknown kind');
@@ -589,8 +596,21 @@ export default abstract class LocalTrack<
589
596
  * @returns
590
597
  */
591
598
  async stopProcessor(keepElement = true) {
592
- if (!this.processor) return;
599
+ const unlock = await this.trackChangeLock.lock();
600
+ try {
601
+ await this.internalStopProcessor(keepElement);
602
+ } finally {
603
+ unlock();
604
+ }
605
+ }
593
606
 
607
+ /**
608
+ * @internal
609
+ * This method assumes the caller has acquired a trackChangeLock already.
610
+ * The public facing method for stopping the processor is `stopProcessor` and it wraps this method in the trackChangeLock.
611
+ */
612
+ protected async internalStopProcessor(keepElement = true) {
613
+ if (!this.processor) return;
594
614
  this.log.debug('stopping processor', this.logContext);
595
615
  this.processor.processedTrack?.stop();
596
616
  await this.processor.destroy();
@@ -38,6 +38,16 @@ export default class RemoteVideoTrack extends RemoteTrack<Track.Kind.Video> {
38
38
  return this.adaptiveStreamSettings !== undefined;
39
39
  }
40
40
 
41
+ override setStreamState(value: Track.StreamState) {
42
+ super.setStreamState(value);
43
+ console.log('setStreamState', value);
44
+ if (value === Track.StreamState.Active) {
45
+ // update visibility for adaptive stream tracks when stream state received from server is active
46
+ // this is needed to ensure the track is stopped when there's no element attached to it at all
47
+ this.updateVisibility();
48
+ }
49
+ }
50
+
41
51
  /**
42
52
  * Note: When using adaptiveStream, you need to use remoteVideoTrack.attach() to add the track to a HTMLVideoElement, otherwise your video tracks might never start
43
53
  */
@@ -220,7 +230,7 @@ export default class RemoteVideoTrack extends RemoteTrack<Track.Kind.Video> {
220
230
  this.updateDimensions();
221
231
  }, REACTION_DELAY);
222
232
 
223
- private updateVisibility() {
233
+ private updateVisibility(forceEmit?: boolean) {
224
234
  const lastVisibilityChange = this.elementInfos.reduce(
225
235
  (prev, info) => Math.max(prev, info.visibilityChangedAt || 0),
226
236
  0,
@@ -234,7 +244,7 @@ export default class RemoteVideoTrack extends RemoteTrack<Track.Kind.Video> {
234
244
  const isVisible =
235
245
  (this.elementInfos.some((info) => info.visible) && !backgroundPause) || isPiPMode;
236
246
 
237
- if (this.lastVisible === isVisible) {
247
+ if (this.lastVisible === isVisible && !forceEmit) {
238
248
  return;
239
249
  }
240
250