livekit-client 2.17.1 → 2.17.3

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 (145) hide show
  1. package/README.md +7 -5
  2. package/dist/livekit-client.e2ee.worker.js +1 -1
  3. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  4. package/dist/livekit-client.e2ee.worker.mjs +21 -14
  5. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  6. package/dist/livekit-client.esm.mjs +2087 -1920
  7. package/dist/livekit-client.esm.mjs.map +1 -1
  8. package/dist/livekit-client.umd.js +1 -1
  9. package/dist/livekit-client.umd.js.map +1 -1
  10. package/dist/src/e2ee/E2eeManager.d.ts +2 -0
  11. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  12. package/dist/src/e2ee/KeyProvider.d.ts +2 -0
  13. package/dist/src/e2ee/KeyProvider.d.ts.map +1 -1
  14. package/dist/src/e2ee/events.d.ts +1 -1
  15. package/dist/src/e2ee/events.d.ts.map +1 -1
  16. package/dist/src/e2ee/types.d.ts +1 -0
  17. package/dist/src/e2ee/types.d.ts.map +1 -1
  18. package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts +2 -2
  19. package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts.map +1 -1
  20. package/dist/src/index.d.ts +7 -6
  21. package/dist/src/index.d.ts.map +1 -1
  22. package/dist/src/logger.d.ts +2 -1
  23. package/dist/src/logger.d.ts.map +1 -1
  24. package/dist/src/room/PCTransport.d.ts +1 -4
  25. package/dist/src/room/PCTransport.d.ts.map +1 -1
  26. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  27. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  28. package/dist/src/room/Room.d.ts.map +1 -1
  29. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -1
  30. package/dist/src/room/data-stream/incoming/StreamReader.d.ts +2 -4
  31. package/dist/src/room/data-stream/incoming/StreamReader.d.ts.map +1 -1
  32. package/dist/src/room/data-track/depacketizer.d.ts +51 -0
  33. package/dist/src/room/data-track/depacketizer.d.ts.map +1 -0
  34. package/dist/src/room/data-track/e2ee.d.ts +12 -0
  35. package/dist/src/room/data-track/e2ee.d.ts.map +1 -0
  36. package/dist/src/room/data-track/frame.d.ts +7 -0
  37. package/dist/src/room/data-track/frame.d.ts.map +1 -0
  38. package/dist/src/room/data-track/handle.d.ts +6 -7
  39. package/dist/src/room/data-track/handle.d.ts.map +1 -1
  40. package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +76 -0
  41. package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts.map +1 -0
  42. package/dist/src/room/data-track/outgoing/errors.d.ts +64 -0
  43. package/dist/src/room/data-track/outgoing/errors.d.ts.map +1 -0
  44. package/dist/src/room/data-track/outgoing/pipeline.d.ts +22 -0
  45. package/dist/src/room/data-track/outgoing/pipeline.d.ts.map +1 -0
  46. package/dist/src/room/data-track/outgoing/types.d.ts +31 -0
  47. package/dist/src/room/data-track/outgoing/types.d.ts.map +1 -0
  48. package/dist/src/room/data-track/packet/index.d.ts +3 -3
  49. package/dist/src/room/data-track/packet/index.d.ts.map +1 -1
  50. package/dist/src/room/data-track/packetizer.d.ts +43 -0
  51. package/dist/src/room/data-track/packetizer.d.ts.map +1 -0
  52. package/dist/src/room/data-track/track.d.ts +30 -0
  53. package/dist/src/room/data-track/track.d.ts.map +1 -0
  54. package/dist/src/room/data-track/utils.d.ts +34 -2
  55. package/dist/src/room/data-track/utils.d.ts.map +1 -1
  56. package/dist/src/room/debounce.d.ts +11 -0
  57. package/dist/src/room/debounce.d.ts.map +1 -0
  58. package/dist/src/room/events.d.ts +1 -1
  59. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  60. package/dist/src/room/track/LocalAudioTrack.d.ts +1 -1
  61. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  62. package/dist/src/room/track/LocalTrack.d.ts +2 -1
  63. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  64. package/dist/src/room/types.d.ts +0 -2
  65. package/dist/src/room/types.d.ts.map +1 -1
  66. package/dist/src/room/utils.d.ts +6 -1
  67. package/dist/src/room/utils.d.ts.map +1 -1
  68. package/dist/src/utils/subscribeToEvents.d.ts +12 -0
  69. package/dist/src/utils/subscribeToEvents.d.ts.map +1 -0
  70. package/dist/src/utils/throws.d.ts +4 -2
  71. package/dist/src/utils/throws.d.ts.map +1 -1
  72. package/dist/ts4.2/e2ee/E2eeManager.d.ts +2 -0
  73. package/dist/ts4.2/e2ee/KeyProvider.d.ts +2 -0
  74. package/dist/ts4.2/e2ee/events.d.ts +1 -1
  75. package/dist/ts4.2/e2ee/types.d.ts +1 -0
  76. package/dist/ts4.2/e2ee/worker/ParticipantKeyHandler.d.ts +2 -2
  77. package/dist/ts4.2/index.d.ts +7 -3
  78. package/dist/ts4.2/logger.d.ts +2 -1
  79. package/dist/ts4.2/room/PCTransport.d.ts +1 -6
  80. package/dist/ts4.2/room/data-stream/incoming/StreamReader.d.ts +2 -4
  81. package/dist/ts4.2/room/data-track/depacketizer.d.ts +51 -0
  82. package/dist/ts4.2/room/data-track/e2ee.d.ts +12 -0
  83. package/dist/ts4.2/room/data-track/frame.d.ts +7 -0
  84. package/dist/ts4.2/room/data-track/handle.d.ts +6 -7
  85. package/dist/ts4.2/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +77 -0
  86. package/dist/ts4.2/room/data-track/outgoing/errors.d.ts +64 -0
  87. package/dist/ts4.2/room/data-track/outgoing/pipeline.d.ts +22 -0
  88. package/dist/ts4.2/room/data-track/outgoing/types.d.ts +31 -0
  89. package/dist/ts4.2/room/data-track/packet/index.d.ts +3 -3
  90. package/dist/ts4.2/room/data-track/packetizer.d.ts +43 -0
  91. package/dist/ts4.2/room/data-track/track.d.ts +30 -0
  92. package/dist/ts4.2/room/data-track/utils.d.ts +34 -2
  93. package/dist/ts4.2/room/debounce.d.ts +11 -0
  94. package/dist/ts4.2/room/events.d.ts +1 -1
  95. package/dist/ts4.2/room/track/LocalAudioTrack.d.ts +1 -1
  96. package/dist/ts4.2/room/track/LocalTrack.d.ts +2 -1
  97. package/dist/ts4.2/room/types.d.ts +0 -2
  98. package/dist/ts4.2/room/utils.d.ts +6 -1
  99. package/dist/ts4.2/utils/subscribeToEvents.d.ts +12 -0
  100. package/dist/ts4.2/utils/throws.d.ts +4 -2
  101. package/package.json +4 -5
  102. package/src/e2ee/E2eeManager.ts +9 -5
  103. package/src/e2ee/KeyProvider.ts +10 -1
  104. package/src/e2ee/events.ts +1 -1
  105. package/src/e2ee/types.ts +1 -0
  106. package/src/e2ee/worker/ParticipantKeyHandler.ts +7 -4
  107. package/src/e2ee/worker/e2ee.worker.ts +20 -10
  108. package/src/index.ts +15 -5
  109. package/src/logger.ts +1 -0
  110. package/src/room/PCTransport.ts +2 -1
  111. package/src/room/PCTransportManager.ts +27 -9
  112. package/src/room/RTCEngine.ts +13 -2
  113. package/src/room/Room.ts +11 -5
  114. package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +5 -25
  115. package/src/room/data-stream/incoming/StreamReader.ts +56 -73
  116. package/src/room/data-track/depacketizer.test.ts +442 -0
  117. package/src/room/data-track/depacketizer.ts +298 -0
  118. package/src/room/data-track/e2ee.ts +14 -0
  119. package/src/room/data-track/frame.ts +8 -0
  120. package/src/room/data-track/handle.test.ts +1 -1
  121. package/src/room/data-track/handle.ts +9 -14
  122. package/src/room/data-track/outgoing/OutgoingDataTrackManager.test.ts +392 -0
  123. package/src/room/data-track/outgoing/OutgoingDataTrackManager.ts +302 -0
  124. package/src/room/data-track/outgoing/errors.ts +157 -0
  125. package/src/room/data-track/outgoing/pipeline.ts +76 -0
  126. package/src/room/data-track/outgoing/types.ts +37 -0
  127. package/src/room/data-track/packet/index.test.ts +9 -9
  128. package/src/room/data-track/packet/index.ts +11 -9
  129. package/src/room/data-track/packet/serializable.ts +1 -1
  130. package/src/room/data-track/packetizer.test.ts +131 -0
  131. package/src/room/data-track/packetizer.ts +132 -0
  132. package/src/room/data-track/track.ts +50 -0
  133. package/src/room/data-track/utils.test.ts +27 -1
  134. package/src/room/data-track/utils.ts +125 -5
  135. package/src/room/debounce.ts +115 -0
  136. package/src/room/events.ts +1 -1
  137. package/src/room/participant/LocalParticipant.ts +2 -0
  138. package/src/room/track/LocalAudioTrack.ts +10 -10
  139. package/src/room/track/LocalTrack.ts +14 -5
  140. package/src/room/track/LocalVideoTrack.ts +1 -1
  141. package/src/room/track/RemoteVideoTrack.ts +1 -1
  142. package/src/room/types.ts +0 -2
  143. package/src/room/utils.ts +7 -2
  144. package/src/utils/subscribeToEvents.ts +63 -0
  145. package/src/utils/throws.ts +3 -1
@@ -0,0 +1,50 @@
1
+ import type { DataTrackFrame } from './frame';
2
+ import { type DataTrackHandle } from './handle';
3
+ import type OutgoingDataTrackManager from './outgoing/OutgoingDataTrackManager';
4
+
5
+ export type DataTrackSid = string;
6
+
7
+ /** Information about a published data track. */
8
+ export type DataTrackInfo = {
9
+ sid: DataTrackSid;
10
+ pubHandle: DataTrackHandle;
11
+ name: String;
12
+ usesE2ee: boolean;
13
+ };
14
+
15
+ export class LocalDataTrack {
16
+ info: DataTrackInfo;
17
+
18
+ protected manager: OutgoingDataTrackManager;
19
+
20
+ constructor(info: DataTrackInfo, manager: OutgoingDataTrackManager) {
21
+ this.info = info;
22
+ this.manager = manager;
23
+ }
24
+
25
+ /** The raw descriptor from the manager containing the internal state for this local track. */
26
+ protected get descriptor() {
27
+ return this.manager.getDescriptor(this.info.pubHandle);
28
+ }
29
+
30
+ isPublished() {
31
+ return this.descriptor?.type === 'active';
32
+ }
33
+
34
+ /** Try pushing a frame to subscribers of the track.
35
+ *
36
+ * Pushing a frame can fail for several reasons:
37
+ *
38
+ * - The track has been unpublished by the local participant or SFU
39
+ * - The room is no longer connected
40
+ */
41
+ tryPush(payload: DataTrackFrame['payload']) {
42
+ try {
43
+ return this.manager.tryProcessAndSend(this.info.pubHandle, payload);
44
+ } catch (err) {
45
+ // NOTE: wrapping in the bare try/catch like this means that the Throws<...> type doesn't
46
+ // propegate upwards into the public interface.
47
+ throw err;
48
+ }
49
+ }
50
+ }
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable @typescript-eslint/no-unused-vars */
2
2
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
- import { WrapAroundUnsignedInt } from './utils';
3
+ import { U16_MAX_SIZE, WrapAroundUnsignedInt } from './utils';
4
4
 
5
5
  describe('WrapAroundUnsignedInt', () => {
6
6
  it('should test initialization + edge cases', () => {
@@ -25,4 +25,30 @@ describe('WrapAroundUnsignedInt', () => {
25
25
  n.update((v) => v - 1);
26
26
  expect(n.value).toBe(65535);
27
27
  });
28
+
29
+ it.each([
30
+ // Happy path
31
+ [5, 10, true],
32
+ [10, 5, false],
33
+
34
+ // Equality cases
35
+ [0, 0, false],
36
+ [7, 7, false],
37
+ [U16_MAX_SIZE + 1, U16_MAX_SIZE + 1, false],
38
+
39
+ // Boundary cases
40
+ [0, 2, true],
41
+ [U16_MAX_SIZE - 1, U16_MAX_SIZE, true],
42
+
43
+ // Wraparound cases
44
+ [1, U16_MAX_SIZE + 1 /* wraps around to 0 */, false],
45
+ [U16_MAX_SIZE + 1 /* wraps around to 0 */, 5, true],
46
+ [2, (U16_MAX_SIZE + 1) * 5 + 3 /* wraps around to 3 */, true],
47
+ [(U16_MAX_SIZE + 1) * 5 + 3 /* wraps around to 3 */, 5, true],
48
+ ])('should ensure isBefore works', (first, second, result) => {
49
+ expect(
50
+ WrapAroundUnsignedInt.u16(first).isBefore(WrapAroundUnsignedInt.u16(second)),
51
+ `${first} isBefore ${second} != ${result}`,
52
+ ).toStrictEqual(result);
53
+ });
28
54
  });
@@ -1,4 +1,5 @@
1
1
  export const U16_MAX_SIZE = 0xffff;
2
+ export const U32_MAX_SIZE = 0xffffffff;
2
3
 
3
4
  /**
4
5
  * A number of fields withing the data tracks packet specification assume wrap around behavior when
@@ -15,6 +16,10 @@ export class WrapAroundUnsignedInt<MaxSize extends number> {
15
16
  return new WrapAroundUnsignedInt(raw, U16_MAX_SIZE);
16
17
  }
17
18
 
19
+ static u32(raw: number) {
20
+ return new WrapAroundUnsignedInt(raw, U32_MAX_SIZE);
21
+ }
22
+
18
23
  constructor(raw: number, maxSize: MaxSize) {
19
24
  this.value = raw;
20
25
  if (raw < 0) {
@@ -42,31 +47,146 @@ export class WrapAroundUnsignedInt<MaxSize extends number> {
42
47
  }
43
48
  }
44
49
 
50
+ clone() {
51
+ return new WrapAroundUnsignedInt(this.value, this.maxSize);
52
+ }
53
+
45
54
  /** When called, maps the containing value to a new containing value. After mapping, the wrap
46
- * around external max size bounds are applied. */
55
+ * around external max size bounds are applied. Note that this is a mutative operation. */
47
56
  update(updateFn: (value: number) => number) {
48
57
  this.value = updateFn(this.value);
49
58
  this.clamp();
50
59
  }
60
+
61
+ /** Increments the given `n` to the inner value. Note that this is a mutative operation. */
62
+ increment(n = 1) {
63
+ this.update((value) => value + n);
64
+ }
65
+
66
+ /** Decrements the given `n` from the inner value. Note that this is a mutative operation. */
67
+ decrement(n = 1) {
68
+ this.update((value) => value - n);
69
+ }
70
+
71
+ getThenIncrement() {
72
+ const previousValue = this.value;
73
+ this.increment();
74
+ return new WrapAroundUnsignedInt(previousValue, this.maxSize);
75
+ }
76
+
77
+ /** Returns true if {@link this} is before the passed other {@link WrapAroundUnsignedInt}. */
78
+ isBefore(other: WrapAroundUnsignedInt<MaxSize>) {
79
+ const a = this.value >>> 0;
80
+ const b = other.value >>> 0;
81
+ const diff = (b - a) >>> 0;
82
+ return diff !== 0 && diff < this.maxSize + 1;
83
+ }
51
84
  }
52
85
 
53
86
  export class DataTrackTimestamp<RateInHz extends number> {
54
87
  rateInHz: RateInHz;
55
88
 
56
- timestamp: number;
89
+ timestamp: WrapAroundUnsignedInt<typeof U32_MAX_SIZE>;
57
90
 
58
91
  static fromRtpTicks(rtpTicks: number) {
59
92
  return new DataTrackTimestamp(rtpTicks, 90_000);
60
93
  }
61
94
 
62
- asTicks() {
63
- return this.timestamp;
95
+ /** Generates a timestamp initialized to a non cryptographically secure random value, so that
96
+ * different streams are more difficult to correlate in packet capture. */
97
+ static rtpRandom() {
98
+ const randomValue = Math.round(Math.random() * U32_MAX_SIZE);
99
+ return DataTrackTimestamp.fromRtpTicks(randomValue);
64
100
  }
65
101
 
66
102
  private constructor(raw: number, rateInHz: RateInHz) {
67
- this.timestamp = raw;
103
+ this.timestamp = WrapAroundUnsignedInt.u32(raw);
104
+ this.rateInHz = rateInHz;
105
+ }
106
+
107
+ asTicks() {
108
+ return this.timestamp.value;
109
+ }
110
+
111
+ clone() {
112
+ return new DataTrackTimestamp(this.timestamp.value, this.rateInHz);
113
+ }
114
+
115
+ wrappingAdd(n: number) {
116
+ this.timestamp.increment(n);
117
+ }
118
+
119
+ /** Returns true if {@link this} is before the passed other {@link DataTrackTimestamp}. */
120
+ isBefore(other: DataTrackTimestamp<RateInHz>) {
121
+ return this.timestamp.isBefore(other.timestamp);
122
+ }
123
+ }
124
+
125
+ export class DataTrackClock<RateInHz extends number> {
126
+ epoch: Date;
127
+
128
+ base: DataTrackTimestamp<RateInHz>;
129
+
130
+ previous: DataTrackTimestamp<RateInHz>;
131
+
132
+ rateInHz: RateInHz;
133
+
134
+ private constructor(rateInHz: RateInHz, epoch: Date, base: DataTrackTimestamp<RateInHz>) {
135
+ this.epoch = epoch;
136
+ this.base = base;
137
+ this.previous = base.clone();
68
138
  this.rateInHz = rateInHz;
69
139
  }
140
+
141
+ static startingNow<RateInHz extends number>(
142
+ base: DataTrackTimestamp<RateInHz>,
143
+ rateInHz: RateInHz,
144
+ ) {
145
+ return new DataTrackClock(rateInHz, new Date(), base);
146
+ }
147
+
148
+ static startingAtTime<RateInHz extends number>(
149
+ epoch: Date,
150
+ base: DataTrackTimestamp<RateInHz>,
151
+ rateInHz: RateInHz,
152
+ ) {
153
+ return new DataTrackClock(rateInHz, epoch, base);
154
+ }
155
+
156
+ static rtpStartingNow(base: DataTrackTimestamp<90_000>) {
157
+ return DataTrackClock.startingNow(base, 90_000);
158
+ }
159
+
160
+ static rtpStartingAtTime(epoch: Date, base: DataTrackTimestamp<90_000>) {
161
+ return DataTrackClock.startingAtTime(epoch, base, 90_000);
162
+ }
163
+
164
+ now(): DataTrackTimestamp<RateInHz> {
165
+ return this.at(new Date());
166
+ }
167
+
168
+ at(timestamp: Date) {
169
+ let elapsedMs = timestamp.getTime() - this.epoch.getTime();
170
+ let durationTicks = DataTrackClock.durationInMsToTicks(elapsedMs, this.rateInHz);
171
+
172
+ let result = this.base.clone();
173
+ result.wrappingAdd(durationTicks);
174
+
175
+ // Enforce monotonicity in RTP wraparound space
176
+ if (result.isBefore(this.previous)) {
177
+ result = this.previous;
178
+ }
179
+ this.previous = result.clone();
180
+ return result.clone();
181
+ }
182
+
183
+ /** Convert a duration since the epoch into clock ticks. */
184
+ static durationInMsToTicks(durationMilliseconds: number, rateInHz: number) {
185
+ // round(nanos * rate_hz / 1e9)
186
+ let durationNanoseconds = durationMilliseconds * 1e6;
187
+ let ticks = (durationNanoseconds * rateInHz + 500_000_000) / 1_000_000_000;
188
+ return Math.round(ticks);
189
+ }
70
190
  }
71
191
 
72
192
  export function coerceToDataView<Input extends DataView | ArrayBuffer | Uint8Array>(
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Originally from ts-debounce (https://github.com/chodorowicz/ts-debounce)
3
+ * with the following license:
4
+ *
5
+ * MIT License
6
+ *
7
+ * Copyright (c) 2017 Jakub Chodorowicz
8
+ *
9
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ * of this software and associated documentation files (the "Software"), to deal
11
+ * in the Software without restriction, including without limitation the rights
12
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ * copies of the Software, and to permit persons to whom the Software is
14
+ * furnished to do so, subject to the following conditions:
15
+ *
16
+ * The above copyright notice and this permission notice shall be included in all
17
+ * copies or substantial portions of the Software.
18
+ *
19
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ * SOFTWARE.
26
+ *
27
+ * Modified to use CriticalTimers for reliable timer execution.
28
+ */
29
+ /* eslint-disable @typescript-eslint/no-this-alias, @typescript-eslint/no-unused-expressions, @typescript-eslint/no-shadow */
30
+ import CriticalTimers from './timers';
31
+
32
+ export type Options<Result> = {
33
+ isImmediate?: boolean;
34
+ maxWait?: number;
35
+ callback?: (data: Result) => void;
36
+ };
37
+
38
+ export interface DebouncedFunction<Args extends any[], F extends (...args: Args) => any> {
39
+ (this: ThisParameterType<F>, ...args: Args & Parameters<F>): Promise<ReturnType<F>>;
40
+ cancel: (reason?: any) => void;
41
+ }
42
+
43
+ interface DebouncedPromise<FunctionReturn> {
44
+ resolve: (result: FunctionReturn) => void;
45
+ reject: (reason?: any) => void;
46
+ }
47
+
48
+ export function debounce<Args extends any[], F extends (...args: Args) => any>(
49
+ func: F,
50
+ waitMilliseconds = 50,
51
+ options: Options<ReturnType<F>> = {},
52
+ ): DebouncedFunction<Args, F> {
53
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
54
+ const isImmediate = options.isImmediate ?? false;
55
+ const callback = options.callback ?? false;
56
+ const maxWait = options.maxWait;
57
+ let lastInvokeTime = Date.now();
58
+
59
+ let promises: DebouncedPromise<ReturnType<F>>[] = [];
60
+
61
+ function nextInvokeTimeout() {
62
+ if (maxWait !== undefined) {
63
+ const timeSinceLastInvocation = Date.now() - lastInvokeTime;
64
+
65
+ if (timeSinceLastInvocation + waitMilliseconds >= maxWait) {
66
+ return maxWait - timeSinceLastInvocation;
67
+ }
68
+ }
69
+
70
+ return waitMilliseconds;
71
+ }
72
+
73
+ const debouncedFunction = function (this: ThisParameterType<F>, ...args: Parameters<F>) {
74
+ const context = this;
75
+ return new Promise<ReturnType<F>>((resolve, reject) => {
76
+ const invokeFunction = function () {
77
+ timeoutId = undefined;
78
+ lastInvokeTime = Date.now();
79
+ if (!isImmediate) {
80
+ const result = func.apply(context, args);
81
+ callback && callback(result);
82
+ // biome-ignore lint/suspicious/useIterableCallbackReturn: vendored code
83
+ promises.forEach(({ resolve }) => resolve(result));
84
+ promises = [];
85
+ }
86
+ };
87
+
88
+ const shouldCallNow = isImmediate && timeoutId === undefined;
89
+
90
+ if (timeoutId !== undefined) {
91
+ CriticalTimers.clearTimeout(timeoutId);
92
+ }
93
+
94
+ timeoutId = CriticalTimers.setTimeout(invokeFunction, nextInvokeTimeout());
95
+
96
+ if (shouldCallNow) {
97
+ const result = func.apply(context, args);
98
+ callback && callback(result);
99
+ return resolve(result);
100
+ }
101
+ promises.push({ resolve, reject });
102
+ });
103
+ };
104
+
105
+ debouncedFunction.cancel = function (reason?: any) {
106
+ if (timeoutId !== undefined) {
107
+ CriticalTimers.clearTimeout(timeoutId);
108
+ }
109
+ // biome-ignore lint/suspicious/useIterableCallbackReturn: vendored code
110
+ promises.forEach(({ reject }) => reject(reason));
111
+ promises = [];
112
+ };
113
+
114
+ return debouncedFunction;
115
+ }
@@ -34,7 +34,7 @@ export enum RoomEvent {
34
34
 
35
35
  /**
36
36
  * When disconnected from room. This fires when room.disconnect() is called or
37
- * when an unrecoverable connection issue had occured.
37
+ * when an unrecoverable connection issue had occurred.
38
38
  *
39
39
  * DisconnectReason can be used to determine why the participant was disconnected. Notable reasons are
40
40
  * - DUPLICATE_IDENTITY: another client with the same identity has joined the room
@@ -275,6 +275,8 @@ export default class LocalParticipant extends Participant {
275
275
 
276
276
  private handleClosing = () => {
277
277
  if (this.reconnectFuture) {
278
+ // @throws-transformer ignore - introduced due to adding Throws into Future, investigate this
279
+ // further
278
280
  this.reconnectFuture.promise.catch((e) => this.log.warn(e.message, this.logContext));
279
281
  this.reconnectFuture?.reject?.(new Error('Got disconnected during reconnection attempt'));
280
282
  this.reconnectFuture = undefined;
@@ -3,7 +3,7 @@ import { TrackEvent } from '../events';
3
3
  import { computeBitrate, monitorFrequency } from '../stats';
4
4
  import type { AudioSenderStats } from '../stats';
5
5
  import type { LoggerOptions } from '../types';
6
- import { isReactNative, isWeb, unwrapConstraint } from '../utils';
6
+ import { isReactNative, isWeb } from '../utils';
7
7
  import LocalTrack from './LocalTrack';
8
8
  import { Track } from './Track';
9
9
  import type { AudioCaptureOptions } from './options';
@@ -74,18 +74,15 @@ export default class LocalAudioTrack extends LocalTrack<Track.Kind.Audio> {
74
74
  return this;
75
75
  }
76
76
 
77
- const deviceHasChanged =
78
- this._constraints.deviceId &&
79
- this._mediaStreamTrack.getSettings().deviceId !==
80
- unwrapConstraint(this._constraints.deviceId);
81
-
82
77
  if (
83
78
  this.source === Track.Source.Microphone &&
84
- (this.stopOnMute || this._mediaStreamTrack.readyState === 'ended' || deviceHasChanged) &&
79
+ (this.stopOnMute ||
80
+ this._mediaStreamTrack.readyState === 'ended' ||
81
+ this.pendingDeviceChange) &&
85
82
  !this.isUserProvided
86
83
  ) {
87
84
  this.log.debug('reacquiring mic track', this.logContext);
88
- await this.restartTrack();
85
+ await this.restart(undefined, true);
89
86
  }
90
87
  await super.unmute();
91
88
 
@@ -106,8 +103,11 @@ export default class LocalAudioTrack extends LocalTrack<Track.Kind.Audio> {
106
103
  await this.restart(constraints);
107
104
  }
108
105
 
109
- protected async restart(constraints?: MediaTrackConstraints): Promise<typeof this> {
110
- const track = await super.restart(constraints);
106
+ protected async restart(
107
+ constraints?: MediaTrackConstraints,
108
+ isUnmuting?: boolean,
109
+ ): Promise<typeof this> {
110
+ const track = await super.restart(constraints, isUnmuting);
111
111
  this.checkForSilence();
112
112
  return track;
113
113
  }
@@ -1,7 +1,7 @@
1
1
  import { Mutex } from '@livekit/mutex';
2
- import { debounce } from 'ts-debounce';
3
2
  import { getBrowser } from '../../utils/browserParser';
4
3
  import DeviceManager from '../DeviceManager';
4
+ import { debounce } from '../debounce';
5
5
  import { DeviceUnsupportedError, TrackInvalidError } from '../errors';
6
6
  import { TrackEvent } from '../events';
7
7
  import type { LoggerOptions } from '../types';
@@ -65,6 +65,8 @@ export default abstract class LocalTrack<
65
65
 
66
66
  protected trackChangeLock: Mutex;
67
67
 
68
+ protected pendingDeviceChange: boolean = false;
69
+
68
70
  /**
69
71
  *
70
72
  * @param mediaTrack
@@ -145,7 +147,11 @@ export default abstract class LocalTrack<
145
147
  return this._mediaStreamTrack.getSettings();
146
148
  }
147
149
 
148
- private async setMediaStreamTrack(newTrack: MediaStreamTrack, force?: boolean) {
150
+ private async setMediaStreamTrack(
151
+ newTrack: MediaStreamTrack,
152
+ force?: boolean,
153
+ isUnmuting?: boolean,
154
+ ) {
149
155
  if (newTrack === this._mediaStreamTrack && !force) {
150
156
  return;
151
157
  }
@@ -202,7 +208,8 @@ export default abstract class LocalTrack<
202
208
  this._mediaStreamTrack = newTrack;
203
209
  if (newTrack) {
204
210
  // sync muted state with the enabled state of the newly provided track
205
- this._mediaStreamTrack.enabled = !this.isMuted;
211
+ // if restarting as part of an unmute, set enabled to true directly to avoid mute cycling
212
+ this._mediaStreamTrack.enabled = isUnmuting ? true : !this.isMuted;
206
213
  // when a valid track is replace, we'd want to start producing
207
214
  await this.resumeUpstream();
208
215
  this.attachedElements.forEach((el) => {
@@ -246,6 +253,7 @@ export default abstract class LocalTrack<
246
253
  // when track is muted, underlying media stream track is stopped and
247
254
  // will be restarted later
248
255
  if (this.isMuted) {
256
+ this.pendingDeviceChange = true;
249
257
  return true;
250
258
  }
251
259
 
@@ -320,7 +328,7 @@ export default abstract class LocalTrack<
320
328
  }
321
329
  }
322
330
 
323
- protected async restart(constraints?: MediaTrackConstraints) {
331
+ protected async restart(constraints?: MediaTrackConstraints, isUnmuting?: boolean) {
324
332
  this.manuallyStopped = false;
325
333
  const unlock = await this.trackChangeLock.lock();
326
334
 
@@ -363,8 +371,9 @@ export default abstract class LocalTrack<
363
371
  newTrack.addEventListener('ended', this.handleEnded);
364
372
  this.log.debug('re-acquired MediaStreamTrack', this.logContext);
365
373
 
366
- await this.setMediaStreamTrack(newTrack);
374
+ await this.setMediaStreamTrack(newTrack, false, isUnmuting);
367
375
  this._constraints = constraints;
376
+ this.pendingDeviceChange = false;
368
377
  this.emit(TrackEvent.Restarted, this);
369
378
  if (this.manuallyStopped) {
370
379
  this.log.warn(
@@ -168,7 +168,7 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
168
168
 
169
169
  if (this.source === Track.Source.Camera && !this.isUserProvided) {
170
170
  this.log.debug('reacquiring camera track', this.logContext);
171
- await this.restartTrack();
171
+ await this.restart(undefined, true);
172
172
  }
173
173
  await super.unmute();
174
174
  return this;
@@ -1,4 +1,4 @@
1
- import { debounce } from 'ts-debounce';
1
+ import { debounce } from '../debounce';
2
2
  import { TrackEvent } from '../events';
3
3
  import type { VideoReceiverStats } from '../stats';
4
4
  import { computeBitrate } from '../stats';
package/src/room/types.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import type { DataStream_Chunk, Encryption_Type } from '@livekit/protocol';
2
- import type { Future } from './utils';
3
2
 
4
3
  export type SimulationOptions = {
5
4
  publish?: {
@@ -125,7 +124,6 @@ export interface StreamController<T extends DataStream_Chunk> {
125
124
  startTime: number;
126
125
  endTime?: number;
127
126
  sendingParticipantIdentity: string;
128
- outOfBandFailureRejectingFuture: Future<never, Error>;
129
127
  }
130
128
 
131
129
  export interface BaseStreamInfo {
package/src/room/utils.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  import TypedPromise from '../utils/TypedPromise';
9
9
  import { getBrowser } from '../utils/browserParser';
10
10
  import type { BrowserDetails } from '../utils/browserParser';
11
+ import { type Throws } from '../utils/throws';
11
12
  import { protocolVersion, version } from '../version';
12
13
  import { type ConnectionError, ConnectionErrorReason } from './errors';
13
14
  import type LocalParticipant from './participant/LocalParticipant';
@@ -457,8 +458,12 @@ export function getStereoAudioStreamTrack() {
457
458
  return stereoTrack;
458
459
  }
459
460
 
461
+ /** An object that represents a serialized version of a `new Promise((resolve, reject) => {})`
462
+ * constructor. Wait for a promise resolution with `await future.promise` and explicitly resolve or
463
+ * reject the inner promise with `future.resolve(...)` or `future.reject(...)`.
464
+ */
460
465
  export class Future<T, E extends Error> {
461
- promise: Promise<T>;
466
+ promise: Promise<Throws<T, E>>;
462
467
 
463
468
  resolve?: (arg: T) => void;
464
469
 
@@ -486,7 +491,7 @@ export class Future<T, E extends Error> {
486
491
  }).finally(() => {
487
492
  this._isResolved = true;
488
493
  this.onFinally?.();
489
- });
494
+ }) as Promise<Throws<T, E>>;
490
495
  }
491
496
  }
492
497
 
@@ -0,0 +1,63 @@
1
+ import { type EventMap } from 'typed-emitter';
2
+ import type TypedEventEmitter from 'typed-emitter';
3
+ import { Future } from '../room/utils';
4
+
5
+ /** A test helper to listen to events received by an event emitter and allow them to be imperatively
6
+ * queried after the fact. */
7
+ export function subscribeToEvents<
8
+ Callbacks extends EventMap,
9
+ EventNames extends keyof Callbacks = keyof Callbacks,
10
+ >(eventEmitter: TypedEventEmitter<Callbacks>, eventNames: Array<EventNames>) {
11
+ const nextEventListeners = new Map<EventNames, Array<Future<unknown, never>>>(
12
+ eventNames.map((eventName) => [eventName, []]),
13
+ );
14
+ const buffers = new Map<EventNames, Array<unknown>>(
15
+ eventNames.map((eventName) => [eventName, []]),
16
+ );
17
+
18
+ const eventHandlers = eventNames.map((eventName) => {
19
+ const onEvent = ((event: unknown) => {
20
+ const listeners = nextEventListeners.get(eventName)!;
21
+ if (listeners.length > 0) {
22
+ for (const listener of listeners) {
23
+ listener.resolve?.(event);
24
+ }
25
+ nextEventListeners.set(eventName, []);
26
+ } else {
27
+ buffers.get(eventName)!.push(event);
28
+ }
29
+ }) as Callbacks[keyof Callbacks];
30
+ return [eventName, onEvent] as [keyof Callbacks, Callbacks[keyof Callbacks]];
31
+ });
32
+ for (const [eventName, onEvent] of eventHandlers) {
33
+ eventEmitter.on(eventName, onEvent);
34
+ }
35
+
36
+ return {
37
+ /** Listen for the next occurrance of an event to be emitted, or return the last event that was
38
+ * buffered (but hasn't been processed yet). */
39
+ async waitFor<
40
+ EventPayload extends Parameters<Callbacks[EventName]>[0],
41
+ EventName extends EventNames = EventNames,
42
+ >(eventName: EventName): Promise<EventPayload> {
43
+ // If an event is already buffered which hasn't been processed yet, pull that off the buffer
44
+ // and use it.
45
+ const earliestBufferedEvent = buffers.get(eventName)!.shift();
46
+ if (earliestBufferedEvent) {
47
+ return earliestBufferedEvent as EventPayload;
48
+ }
49
+
50
+ // Otherwise wait for the next event to come in.
51
+ const future = new Future<unknown, never>();
52
+ nextEventListeners.get(eventName)!.push(future);
53
+ const nextEvent = await future.promise;
54
+ return nextEvent as EventPayload;
55
+ },
56
+ /** Cleanup any lingering subscriptions. */
57
+ unsubscribe: () => {
58
+ for (const [eventName, onEvent] of eventHandlers) {
59
+ eventEmitter.off(eventName, onEvent);
60
+ }
61
+ },
62
+ };
63
+ }
@@ -1,3 +1,5 @@
1
+ type Primitives = null | undefined | string | number | bigint | boolean | symbol;
2
+
1
3
  /**
2
4
  * Branded type that encodes possible thrown errors in the return type.
3
5
  *
@@ -11,7 +13,7 @@
11
13
  *
12
14
  * For more info about how this is checked, see ./throws-transformer at the root of this repo.
13
15
  */
14
- export type Throws<T, E extends Error> = T & { readonly __throws?: E };
16
+ export type Throws<T, E extends Error> = (T & { readonly __throws?: E }) | Extract<T, Primitives>;
15
17
 
16
18
  /**
17
19
  * Extract the error types from a Throws type.