livekit-client 2.19.0 → 2.19.2
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 +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +181 -122
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +517 -426
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.pt.worker.js +1 -1
- package/dist/livekit-client.pt.worker.js.map +1 -1
- package/dist/livekit-client.pt.worker.mjs +129 -95
- package/dist/livekit-client.pt.worker.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/e2ee/worker/FrameCryptor.d.ts +12 -2
- package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +8 -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/rpc/client/RpcClientManager.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +1 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +11 -1
- package/dist/ts4.2/room/RTCEngine.d.ts +8 -0
- package/dist/ts4.2/room/utils.d.ts +1 -0
- package/package.json +10 -8
- package/src/api/SignalClient.ts +1 -0
- package/src/e2ee/worker/FrameCryptor.ts +42 -7
- package/src/room/RTCEngine.ts +48 -20
- package/src/room/Room.test.ts +99 -1
- package/src/room/Room.ts +24 -7
- package/src/room/rpc/client/RpcClientManager.ts +5 -2
- package/src/room/utils.ts +5 -0
package/src/room/Room.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ClientInfo_Capability, JoinResponse } from '@livekit/protocol';
|
|
2
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
3
|
import Room from './Room';
|
|
4
4
|
import { roomConnectOptionDefaults, roomOptionDefaults } from './defaults';
|
|
5
5
|
import { RoomEvent } from './events';
|
|
@@ -89,3 +89,101 @@ describe('Room signaling options', () => {
|
|
|
89
89
|
);
|
|
90
90
|
});
|
|
91
91
|
});
|
|
92
|
+
|
|
93
|
+
describe('Room lifecycle', () => {
|
|
94
|
+
afterEach(() => {
|
|
95
|
+
vi.restoreAllMocks();
|
|
96
|
+
// Tear down the mocked mediaDevices so other tests see the env they
|
|
97
|
+
// expected (happy-dom does not provide navigator.mediaDevices by default).
|
|
98
|
+
if ((navigator as { mediaDevices?: unknown }).mediaDevices) {
|
|
99
|
+
Object.defineProperty(navigator, 'mediaDevices', {
|
|
100
|
+
configurable: true,
|
|
101
|
+
value: undefined,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('wraps the constructor-registered devicechange listener in a WeakRef so the Room is GC-eligible (#1940)', async () => {
|
|
107
|
+
// happy-dom does not provide navigator.mediaDevices. Install a minimal
|
|
108
|
+
// EventTarget stand-in so the constructor takes the listener-registration
|
|
109
|
+
// branch and we can observe the registered listener.
|
|
110
|
+
const mediaDevices = new EventTarget() as EventTarget & {
|
|
111
|
+
addEventListener: EventTarget['addEventListener'];
|
|
112
|
+
removeEventListener: EventTarget['removeEventListener'];
|
|
113
|
+
};
|
|
114
|
+
Object.defineProperty(navigator, 'mediaDevices', {
|
|
115
|
+
configurable: true,
|
|
116
|
+
value: mediaDevices,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const addSpy = vi.spyOn(mediaDevices, 'addEventListener');
|
|
120
|
+
const derefSpy = vi.spyOn(WeakRef.prototype, 'deref');
|
|
121
|
+
const cleanupRegistrySpy = Room.cleanupRegistry
|
|
122
|
+
? vi.spyOn(Room.cleanupRegistry, 'register')
|
|
123
|
+
: undefined;
|
|
124
|
+
|
|
125
|
+
const room = new Room();
|
|
126
|
+
const handleDeviceChangeSpy = vi.spyOn(
|
|
127
|
+
room as unknown as { handleDeviceChange: (ev: Event) => void },
|
|
128
|
+
'handleDeviceChange',
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// Constructor must register exactly one devicechange listener with AbortSignal teardown.
|
|
132
|
+
const deviceChangeAdds = addSpy.mock.calls.filter(([type]) => type === 'devicechange');
|
|
133
|
+
expect(deviceChangeAdds).toHaveLength(1);
|
|
134
|
+
const listener = deviceChangeAdds[0][1] as EventListener;
|
|
135
|
+
const addOptions = deviceChangeAdds[0][2] as AddEventListenerOptions | undefined;
|
|
136
|
+
expect(addOptions?.signal).toBeInstanceOf(AbortSignal);
|
|
137
|
+
|
|
138
|
+
// FinalizationRegistry must be registered with the Room as the target so the
|
|
139
|
+
// cleanup callback fires when the user drops their Room reference.
|
|
140
|
+
if (Room.cleanupRegistry) {
|
|
141
|
+
expect(cleanupRegistrySpy).toHaveBeenCalledWith(room, expect.any(Function));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// While the WeakRef still derefs to the Room, the listener forwards to handleDeviceChange.
|
|
145
|
+
listener.call(null, new Event('devicechange'));
|
|
146
|
+
expect(handleDeviceChangeSpy).toHaveBeenCalledTimes(1);
|
|
147
|
+
|
|
148
|
+
// Simulate the Room being GC'd by forcing deref to return undefined; the
|
|
149
|
+
// listener must short-circuit instead of calling handleDeviceChange.
|
|
150
|
+
derefSpy.mockReturnValue(undefined);
|
|
151
|
+
listener.call(null, new Event('devicechange'));
|
|
152
|
+
expect(handleDeviceChangeSpy).toHaveBeenCalledTimes(1);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('falls back to a direct devicechange listener when WeakRef/FinalizationRegistry are unavailable (#1944)', async () => {
|
|
156
|
+
const mediaDevices = new EventTarget() as EventTarget & {
|
|
157
|
+
addEventListener: EventTarget['addEventListener'];
|
|
158
|
+
removeEventListener: EventTarget['removeEventListener'];
|
|
159
|
+
};
|
|
160
|
+
Object.defineProperty(navigator, 'mediaDevices', {
|
|
161
|
+
configurable: true,
|
|
162
|
+
value: mediaDevices,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Simulate a legacy browser by stubbing out cleanupRegistry.
|
|
166
|
+
const originalRegistry = Room.cleanupRegistry;
|
|
167
|
+
Object.defineProperty(Room, 'cleanupRegistry', {
|
|
168
|
+
configurable: true,
|
|
169
|
+
value: false,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const addSpy = vi.spyOn(mediaDevices, 'addEventListener');
|
|
174
|
+
const room = new Room();
|
|
175
|
+
const handleDeviceChange = (room as unknown as { handleDeviceChange: () => void })
|
|
176
|
+
.handleDeviceChange;
|
|
177
|
+
|
|
178
|
+
const deviceChangeAdds = addSpy.mock.calls.filter(([type]) => type === 'devicechange');
|
|
179
|
+
expect(deviceChangeAdds).toHaveLength(1);
|
|
180
|
+
// The registered listener is the bare handleDeviceChange method (no WeakRef closure).
|
|
181
|
+
expect(deviceChangeAdds[0][1]).toBe(handleDeviceChange);
|
|
182
|
+
} finally {
|
|
183
|
+
Object.defineProperty(Room, 'cleanupRegistry', {
|
|
184
|
+
configurable: true,
|
|
185
|
+
value: originalRegistry,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
});
|
package/src/room/Room.ts
CHANGED
|
@@ -373,18 +373,34 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
373
373
|
}
|
|
374
374
|
|
|
375
375
|
if (isWeb()) {
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
// in order to catch device changes prior to room connection we need to register the event in the constructor
|
|
379
|
-
navigator.mediaDevices?.addEventListener?.('devicechange', this.handleDeviceChange, {
|
|
380
|
-
signal: abortController.signal,
|
|
381
|
-
});
|
|
376
|
+
const cleanupController = new AbortController();
|
|
377
|
+
let onDeviceChange: () => void;
|
|
382
378
|
|
|
383
379
|
if (Room.cleanupRegistry) {
|
|
380
|
+
// Wrap the listener in a WeakRef closure so navigator.mediaDevices does not
|
|
381
|
+
// strongly retain the Room. When the user drops their Room ref, the
|
|
382
|
+
// FinalizationRegistry callback aborts the controller and removes the listener.
|
|
383
|
+
const roomRef = new WeakRef(this);
|
|
384
|
+
onDeviceChange = () => {
|
|
385
|
+
const self = roomRef.deref();
|
|
386
|
+
if (!self) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
self.handleDeviceChange();
|
|
390
|
+
};
|
|
384
391
|
Room.cleanupRegistry.register(this, () => {
|
|
385
|
-
|
|
392
|
+
cleanupController.abort();
|
|
386
393
|
});
|
|
394
|
+
} else {
|
|
395
|
+
// Legacy browsers without WeakRef/FinalizationRegistry: fall back to a
|
|
396
|
+
// direct listener (matches pre-#1944 behavior).
|
|
397
|
+
onDeviceChange = this.handleDeviceChange;
|
|
387
398
|
}
|
|
399
|
+
|
|
400
|
+
// in order to catch device changes prior to room connection we need to register the event in the constructor
|
|
401
|
+
navigator.mediaDevices?.addEventListener?.('devicechange', onDeviceChange, {
|
|
402
|
+
signal: cleanupController.signal,
|
|
403
|
+
});
|
|
388
404
|
}
|
|
389
405
|
}
|
|
390
406
|
|
|
@@ -755,6 +771,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
755
771
|
|
|
756
772
|
static cleanupRegistry =
|
|
757
773
|
typeof FinalizationRegistry !== 'undefined' &&
|
|
774
|
+
typeof WeakRef !== 'undefined' &&
|
|
758
775
|
new FinalizationRegistry((cleanup: () => void) => {
|
|
759
776
|
cleanup();
|
|
760
777
|
});
|
|
@@ -86,11 +86,14 @@ export default class RpcClientManager extends (EventEmitter as new () => TypedEm
|
|
|
86
86
|
|
|
87
87
|
const completionFuture = new Future<string, RpcError>();
|
|
88
88
|
|
|
89
|
+
let responseTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
89
90
|
const ackTimeoutId = setTimeout(() => {
|
|
90
91
|
this.pendingAcks.delete(id);
|
|
91
92
|
completionFuture.reject?.(RpcError.builtIn('CONNECTION_TIMEOUT'));
|
|
92
93
|
this.pendingResponses.delete(id);
|
|
93
|
-
|
|
94
|
+
if (responseTimeoutId !== null) {
|
|
95
|
+
clearTimeout(responseTimeoutId);
|
|
96
|
+
}
|
|
94
97
|
}, maxRoundTripLatencyMs);
|
|
95
98
|
|
|
96
99
|
this.pendingAcks.set(id, {
|
|
@@ -114,7 +117,7 @@ export default class RpcClientManager extends (EventEmitter as new () => TypedEm
|
|
|
114
117
|
remoteClientProtocol,
|
|
115
118
|
);
|
|
116
119
|
|
|
117
|
-
|
|
120
|
+
responseTimeoutId = setTimeout(() => {
|
|
118
121
|
this.pendingResponses.delete(id);
|
|
119
122
|
completionFuture.reject?.(RpcError.builtIn('RESPONSE_TIMEOUT'));
|
|
120
123
|
}, responseTimeoutMs);
|
package/src/room/utils.ts
CHANGED
|
@@ -794,3 +794,8 @@ export function extractMaxAgeFromRequestHeaders(headers: Headers): number | unde
|
|
|
794
794
|
export function isCompressionStreamSupported() {
|
|
795
795
|
return typeof CompressionStream !== 'undefined';
|
|
796
796
|
}
|
|
797
|
+
|
|
798
|
+
export function isPublisherOfferWithJoinSupported() {
|
|
799
|
+
// we have connectivity issue about publisher offer with join on firefox #1919
|
|
800
|
+
return isCompressionStreamSupported() && !isFireFox();
|
|
801
|
+
}
|