livekit-client 2.20.1 → 2.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/livekit-client.e2ee.worker.js +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +37 -7
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +2279 -651
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.fm.worker.js +1 -1
- package/dist/livekit-client.fm.worker.js.map +1 -1
- package/dist/livekit-client.fm.worker.mjs +37 -7
- package/dist/livekit-client.fm.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/api/SignalClient.d.ts.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/logger.d.ts +3 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +2 -0
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/PCTransportManager.d.ts +1 -0
- package/dist/src/room/PCTransportManager.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +31 -26
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/RegionUrlProvider.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +4 -0
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/data-channel/DataChannelManager.d.ts +62 -0
- package/dist/src/room/data-channel/DataChannelManager.d.ts.map +1 -0
- package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
- package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts.map +1 -0
- package/dist/src/room/data-channel/LossyDataChannel.d.ts +40 -0
- package/dist/src/room/data-channel/LossyDataChannel.d.ts.map +1 -0
- package/dist/src/room/data-channel/ReliableDataChannel.d.ts +56 -0
- package/dist/src/room/data-channel/ReliableDataChannel.d.ts.map +1 -0
- package/dist/src/room/data-channel/types.d.ts +12 -0
- package/dist/src/room/data-channel/types.d.ts.map +1 -0
- package/dist/src/room/data-stream/compression.d.ts +49 -0
- package/dist/src/room/data-stream/compression.d.ts.map +1 -0
- package/dist/src/room/data-stream/constants.d.ts +18 -0
- package/dist/src/room/data-stream/constants.d.ts.map +1 -0
- package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
- package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -1
- package/dist/src/room/data-stream/incoming/StreamReader.d.ts.map +1 -1
- package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
- package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts.map +1 -1
- package/dist/src/room/data-stream/outgoing/header-utils.d.ts +18 -0
- package/dist/src/room/data-stream/outgoing/header-utils.d.ts.map +1 -0
- package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts +7 -0
- package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts.map +1 -1
- package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts.map +1 -1
- package/dist/src/room/errors.d.ts +3 -1
- package/dist/src/room/errors.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +9 -1
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/room/participant/RemoteParticipant.d.ts +11 -2
- package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/publishUtils.d.ts +7 -0
- package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
- package/dist/src/room/rpc/client/RpcClientManager.d.ts.map +1 -1
- package/dist/src/room/rpc/server/RpcServerManager.d.ts.map +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts +1 -0
- package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/track/options.d.ts +11 -1
- package/dist/src/room/track/options.d.ts.map +1 -1
- package/dist/src/room/types.d.ts +17 -0
- package/dist/src/room/types.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +13 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/test/signalToken.d.ts +26 -0
- package/dist/src/test/signalToken.d.ts.map +1 -0
- package/dist/src/utils/dataPacketBuffer.d.ts +18 -0
- package/dist/src/utils/dataPacketBuffer.d.ts.map +1 -1
- package/dist/src/version.d.ts +5 -1
- package/dist/src/version.d.ts.map +1 -1
- package/dist/ts4.2/index.d.ts +1 -1
- package/dist/ts4.2/logger.d.ts +3 -1
- package/dist/ts4.2/room/PCTransport.d.ts +2 -0
- package/dist/ts4.2/room/PCTransportManager.d.ts +1 -0
- package/dist/ts4.2/room/RTCEngine.d.ts +31 -26
- package/dist/ts4.2/room/Room.d.ts +4 -0
- package/dist/ts4.2/room/data-channel/DataChannelManager.d.ts +62 -0
- package/dist/ts4.2/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
- package/dist/ts4.2/room/data-channel/LossyDataChannel.d.ts +41 -0
- package/dist/ts4.2/room/data-channel/ReliableDataChannel.d.ts +57 -0
- package/dist/ts4.2/room/data-channel/types.d.ts +12 -0
- package/dist/ts4.2/room/data-stream/compression.d.ts +49 -0
- package/dist/ts4.2/room/data-stream/constants.d.ts +18 -0
- package/dist/ts4.2/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
- package/dist/ts4.2/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
- package/dist/ts4.2/room/data-stream/outgoing/header-utils.d.ts +18 -0
- package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +7 -0
- package/dist/ts4.2/room/errors.d.ts +3 -1
- package/dist/ts4.2/room/participant/LocalParticipant.d.ts +9 -1
- package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +11 -1
- package/dist/ts4.2/room/participant/publishUtils.d.ts +7 -0
- package/dist/ts4.2/room/track/LocalAudioTrack.d.ts +1 -0
- package/dist/ts4.2/room/track/options.d.ts +11 -1
- package/dist/ts4.2/room/types.d.ts +17 -0
- package/dist/ts4.2/room/utils.d.ts +13 -0
- package/dist/ts4.2/test/signalToken.d.ts +26 -0
- package/dist/ts4.2/utils/dataPacketBuffer.d.ts +18 -0
- package/dist/ts4.2/version.d.ts +5 -1
- package/package.json +8 -4
- package/src/api/SignalClient.e2e.test.ts +264 -0
- package/src/api/SignalClient.ts +34 -2
- package/src/index.ts +2 -0
- package/src/logger.ts +2 -0
- package/src/room/PCTransport.ts +66 -31
- package/src/room/PCTransportManager.ts +10 -6
- package/src/room/RTCEngine.test.ts +238 -6
- package/src/room/RTCEngine.ts +101 -338
- package/src/room/RegionUrlProvider.ts +4 -2
- package/src/room/Room.test.ts +4 -1
- package/src/room/Room.ts +47 -9
- package/src/room/data-channel/DataChannelManager.test.ts +152 -0
- package/src/room/data-channel/DataChannelManager.ts +237 -0
- package/src/room/data-channel/FlowControlledDataChannel.test.ts +154 -0
- package/src/room/data-channel/FlowControlledDataChannel.ts +216 -0
- package/src/room/data-channel/LossyDataChannel.test.ts +118 -0
- package/src/room/data-channel/LossyDataChannel.ts +125 -0
- package/src/room/data-channel/ReliableDataChannel.test.ts +158 -0
- package/src/room/data-channel/ReliableDataChannel.ts +153 -0
- package/src/room/data-channel/types.ts +30 -0
- package/src/room/data-stream/compression.ts +117 -0
- package/src/room/data-stream/constants.ts +18 -0
- package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +1601 -0
- package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +438 -86
- package/src/room/data-stream/incoming/StreamReader.ts +8 -2
- package/src/room/data-stream/outgoing/OutgoingDataStreamManager.test.ts +1292 -0
- package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +489 -116
- package/src/room/data-stream/outgoing/header-utils.ts +87 -0
- package/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +303 -0
- package/src/room/data-track/incoming/IncomingDataTrackManager.ts +70 -8
- package/src/room/data-track/outgoing/OutgoingDataTrackManager.ts +2 -0
- package/src/room/errors.ts +10 -4
- package/src/room/participant/LocalParticipant.ts +29 -6
- package/src/room/participant/Participant.ts +5 -0
- package/src/room/participant/RemoteParticipant.ts +19 -5
- package/src/room/participant/publishUtils.ts +14 -11
- package/src/room/rpc/client/RpcClientManager.test.ts +20 -27
- package/src/room/rpc/client/RpcClientManager.ts +1 -4
- package/src/room/rpc/server/RpcServerManager.test.ts +24 -23
- package/src/room/rpc/server/RpcServerManager.ts +1 -3
- package/src/room/track/LocalAudioTrack.ts +16 -0
- package/src/room/track/Track.ts +10 -6
- package/src/room/track/options.ts +11 -1
- package/src/room/types.ts +18 -0
- package/src/room/utils.ts +67 -0
- package/src/test/signalServerSetup.ts +151 -0
- package/src/test/signalToken.ts +59 -0
- package/src/test/vitest-context.d.ts +10 -0
- package/src/utils/dataPacketBuffer.test.ts +100 -0
- package/src/utils/dataPacketBuffer.ts +46 -1
- package/src/version.ts +5 -1
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { type LeaveRequest, LeaveRequest_Action } from '@livekit/protocol';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, inject, it, vi } from 'vitest';
|
|
3
|
+
import { ConnectionErrorReason } from '../room/errors';
|
|
4
|
+
import { createInvalidToken, createToken } from '../test/signalToken';
|
|
5
|
+
import { SignalClient, SignalConnectionState, type SignalOptions } from './SignalClient';
|
|
6
|
+
|
|
7
|
+
// Provided by src/test/signalServerSetup.ts (spawns the livekit-server test-server).
|
|
8
|
+
const serverUrl = inject('serverUrl');
|
|
9
|
+
const unavailable = inject('e2eUnavailable');
|
|
10
|
+
|
|
11
|
+
const defaultOpts = (): SignalOptions => ({
|
|
12
|
+
autoSubscribe: true,
|
|
13
|
+
maxRetries: 0,
|
|
14
|
+
e2eeEnabled: false,
|
|
15
|
+
websocketTimeout: 5_000,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
19
|
+
|
|
20
|
+
function withTimeout<T>(p: Promise<T>, ms: number, label: string): Promise<T> {
|
|
21
|
+
return Promise.race([
|
|
22
|
+
p,
|
|
23
|
+
new Promise<T>((_, reject) =>
|
|
24
|
+
setTimeout(() => reject(new Error(`timed out waiting for ${label}`)), ms),
|
|
25
|
+
),
|
|
26
|
+
]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Resolves with the reason passed to onClose (register before the event can fire). */
|
|
30
|
+
function captureClose(client: SignalClient): Promise<string> {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
client.onClose = (reason) => resolve(reason);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Resolves with the LeaveRequest passed to onLeave. */
|
|
37
|
+
function captureLeave(client: SignalClient): Promise<LeaveRequest> {
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
client.onLeave = (leave) => resolve(leave);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe.skipIf(!!unavailable)('SignalClient e2e', () => {
|
|
44
|
+
// Behaviors that depend on the signal path (v0 legacy vs v1).
|
|
45
|
+
describe.each([
|
|
46
|
+
{ label: 'v0', useV0: true },
|
|
47
|
+
{ label: 'v1', useV0: false },
|
|
48
|
+
])('$label path', ({ useV0 }) => {
|
|
49
|
+
let client: SignalClient;
|
|
50
|
+
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
client = new SignalClient(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterEach(async () => {
|
|
56
|
+
await client.close().catch(() => {});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/** Mint a token whose metadata selects the server behavior mode, then join. */
|
|
60
|
+
const join = (mode: string, opts: SignalOptions = defaultOpts(), abortSignal?: AbortSignal) =>
|
|
61
|
+
createToken({ signal: mode }).then((token) =>
|
|
62
|
+
client.join(serverUrl, token, opts, abortSignal, useV0),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
it('connects and reports CONNECTED with a join response', async () => {
|
|
66
|
+
const res = await join('happy');
|
|
67
|
+
expect(res).toBeTruthy();
|
|
68
|
+
expect(res.pingInterval).toBeGreaterThan(0);
|
|
69
|
+
expect(client.currentState).toBe(SignalConnectionState.CONNECTED);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('stays connected past the ping timeout while the server pongs', async () => {
|
|
73
|
+
const onClose = vi.fn();
|
|
74
|
+
await join('happy');
|
|
75
|
+
client.onClose = onClose;
|
|
76
|
+
// Server ping timeout is 3s; a healthy pong loop must keep us alive.
|
|
77
|
+
await sleep(4_000);
|
|
78
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
79
|
+
expect(client.currentState).toBe(SignalConnectionState.CONNECTED);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('closes with a ping timeout when the server stops ponging', async () => {
|
|
83
|
+
const closed = captureClose(client);
|
|
84
|
+
await join('no_pong');
|
|
85
|
+
expect(client.currentState).toBe(SignalConnectionState.CONNECTED);
|
|
86
|
+
const reason = await withTimeout(closed, 8_000, 'onClose (ping timeout)');
|
|
87
|
+
expect(reason).toContain('ping timeout');
|
|
88
|
+
expect(client.currentState).toBe(SignalConnectionState.DISCONNECTED);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('surfaces an unexpected transport close while connected', async () => {
|
|
92
|
+
const closed = captureClose(client);
|
|
93
|
+
await join('close_when_connected');
|
|
94
|
+
const reason = await withTimeout(closed, 5_000, 'onClose (transport closed)');
|
|
95
|
+
expect(reason).toBeTruthy();
|
|
96
|
+
expect(client.currentState).toBe(SignalConnectionState.DISCONNECTED);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('surfaces an abnormal socket drop (1006) while connected', async () => {
|
|
100
|
+
// Server sends the join, then drops the TCP connection with no close
|
|
101
|
+
// handshake -> the browser reports an abnormal 1006 closure, exercising
|
|
102
|
+
// WebSocketStream's error->close path (distinct from the clean 1011 above).
|
|
103
|
+
const closed = captureClose(client);
|
|
104
|
+
await join('drop_when_connected');
|
|
105
|
+
expect(client.currentState).toBe(SignalConnectionState.CONNECTED);
|
|
106
|
+
const reason = await withTimeout(closed, 5_000, 'onClose (abnormal drop)');
|
|
107
|
+
|
|
108
|
+
expect(reason).toBe('Unexpected WS error');
|
|
109
|
+
expect(client.currentState).toBe(SignalConnectionState.DISCONNECTED);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('rejects the join when the socket closes before the first message', async () => {
|
|
113
|
+
// Upgrade succeeds, then the server closes the WS before sending a join.
|
|
114
|
+
// The client is still establishing, so it rejects rather than hanging.
|
|
115
|
+
const err = await join('close_before_join').then(
|
|
116
|
+
() => undefined,
|
|
117
|
+
(e) => e as Error,
|
|
118
|
+
);
|
|
119
|
+
expect(err).toBeInstanceOf(Error);
|
|
120
|
+
expect((err as { reason?: ConnectionErrorReason }).reason).toBe(
|
|
121
|
+
ConnectionErrorReason.InternalError,
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('delivers a server-initiated leave while connected', async () => {
|
|
126
|
+
const left = captureLeave(client);
|
|
127
|
+
await join('leave_when_connected');
|
|
128
|
+
const leave = await withTimeout(left, 5_000, 'onLeave');
|
|
129
|
+
expect(leave).toBeTruthy();
|
|
130
|
+
expect(leave.action).toBe(LeaveRequest_Action.DISCONNECT);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('forwards a non-DISCONNECT leave action verbatim', async () => {
|
|
134
|
+
// SignalClient does not branch on the leave action (RTCEngine does); it
|
|
135
|
+
// must pass it through unchanged. Guards the FSM swap's leave_received
|
|
136
|
+
// effect, which carries leaveAction. Uses RECONNECT to prove it's not
|
|
137
|
+
// defaulted to DISCONNECT.
|
|
138
|
+
const left = captureLeave(client);
|
|
139
|
+
const token = await createToken({
|
|
140
|
+
signal: 'leave_when_connected',
|
|
141
|
+
leaveAction: LeaveRequest_Action.RECONNECT,
|
|
142
|
+
});
|
|
143
|
+
await client.join(serverUrl, token, defaultOpts(), undefined, useV0);
|
|
144
|
+
const leave = await withTimeout(left, 5_000, 'onLeave');
|
|
145
|
+
expect(leave.action).toBe(LeaveRequest_Action.RECONNECT);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('rejects the join when a leave arrives as the first message', async () => {
|
|
149
|
+
const err = await join('leave_first_message').then(
|
|
150
|
+
() => undefined,
|
|
151
|
+
(e) => e as Error,
|
|
152
|
+
);
|
|
153
|
+
expect(err).toBeInstanceOf(Error);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('closes gracefully via close() without firing onClose', async () => {
|
|
157
|
+
const onClose = vi.fn();
|
|
158
|
+
await join('happy');
|
|
159
|
+
client.onClose = onClose;
|
|
160
|
+
await client.close();
|
|
161
|
+
expect(client.isDisconnected).toBe(true);
|
|
162
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('reconnects (resume) back to CONNECTED', async () => {
|
|
166
|
+
await join('happy');
|
|
167
|
+
const token = await createToken({ signal: 'happy' });
|
|
168
|
+
await client.reconnect(serverUrl, token, 'RM_session');
|
|
169
|
+
expect(client.currentState).toBe(SignalConnectionState.CONNECTED);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('rejects a reconnect when a leave arrives as the first message', async () => {
|
|
173
|
+
// Reconnecting into a room that sends leave-first exercises the client's
|
|
174
|
+
// first-message validation while in RECONNECTING (path-independent: it
|
|
175
|
+
// doesn't rely on the mock detecting reconnect, which v1 hides inside the
|
|
176
|
+
// gzipped join_request the mock ignores).
|
|
177
|
+
await join('happy');
|
|
178
|
+
const token = await createToken({ signal: 'leave_first_message' });
|
|
179
|
+
const err = await client.reconnect(serverUrl, token, 'RM_session').then(
|
|
180
|
+
() => undefined,
|
|
181
|
+
(e) => e as Error,
|
|
182
|
+
);
|
|
183
|
+
expect(err).toBeInstanceOf(Error);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// --- validate-endpoint classification -------------------------------
|
|
187
|
+
it('classifies an invalid token as NotAllowed', async () => {
|
|
188
|
+
const token = await createInvalidToken();
|
|
189
|
+
const err = await client
|
|
190
|
+
.join(serverUrl, token, defaultOpts(), undefined, useV0)
|
|
191
|
+
.catch((e) => e);
|
|
192
|
+
expect(err.reason).toBe(ConnectionErrorReason.NotAllowed);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('classifies room-not-found as NotAllowed', async () => {
|
|
196
|
+
const err = await join('room_not_found').catch((e) => e);
|
|
197
|
+
expect(err.reason).toBe(ConnectionErrorReason.NotAllowed);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('classifies a wrong-path 404 as ServiceNotFound', async () => {
|
|
201
|
+
const err = await join('validate_service_not_found').catch((e) => e);
|
|
202
|
+
expect(err.reason).toBe(ConnectionErrorReason.ServiceNotFound);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('surfaces a 5xx validate as a WebSocket error (ws error shadows 5xx→internal)', async () => {
|
|
206
|
+
// Current behavior: handleConnectionError only maps a 5xx to InternalError
|
|
207
|
+
// when the ws rejection is NOT a ConnectionError. WebSocketStream always
|
|
208
|
+
// rejects with one, so for a refused upgrade the WS error wins. Only
|
|
209
|
+
// 401/403/404 (handled before that check) override it.
|
|
210
|
+
const err = await join('validate_500').catch((e) => e);
|
|
211
|
+
expect(err.reason).toBe(ConnectionErrorReason.WebSocket);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Path-independent transport-level failures (run once, via v0).
|
|
216
|
+
describe('transport failures', () => {
|
|
217
|
+
let client: SignalClient;
|
|
218
|
+
|
|
219
|
+
beforeEach(() => {
|
|
220
|
+
client = new SignalClient(false);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
afterEach(async () => {
|
|
224
|
+
await client.close().catch(() => {});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('classifies an unreachable server as ServerUnreachable', async () => {
|
|
228
|
+
const token = await createToken({ signal: 'happy' });
|
|
229
|
+
// Nothing listening on this port -> ws fails, validate fetch fails.
|
|
230
|
+
const err = await client
|
|
231
|
+
.join('ws://127.0.0.1:59999', token, defaultOpts(), undefined, true)
|
|
232
|
+
.catch((e) => e);
|
|
233
|
+
expect(err.reason).toBe(ConnectionErrorReason.ServerUnreachable);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('cancels the connect when the AbortSignal fires mid-connection', async () => {
|
|
237
|
+
const controller = new AbortController();
|
|
238
|
+
const token = await createToken({ signal: 'no_first_message' });
|
|
239
|
+
const p = client.join(serverUrl, token, defaultOpts(), controller.signal, true);
|
|
240
|
+
setTimeout(() => controller.abort('user requested abort'), 300);
|
|
241
|
+
const err = await p.catch((e) => e);
|
|
242
|
+
expect(err.reason).toBe(ConnectionErrorReason.Cancelled);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('times out when the socket opens but no first message arrives', async () => {
|
|
246
|
+
// wsTimeout only guards the WebSocket upgrade and is cleared once ws.opened
|
|
247
|
+
// resolves. If the server accepts the socket then stays silent, the wait for
|
|
248
|
+
// the first message is guarded by its own JOIN_RESPONSE_TIMEOUT (5s), so
|
|
249
|
+
// join() rejects with Timeout instead of hanging.
|
|
250
|
+
const token = await createToken({ signal: 'no_first_message' });
|
|
251
|
+
const joinPromise = client.join(serverUrl, token, defaultOpts(), undefined, true);
|
|
252
|
+
|
|
253
|
+
const err = await withTimeout(
|
|
254
|
+
joinPromise.then(
|
|
255
|
+
() => undefined,
|
|
256
|
+
(e) => e,
|
|
257
|
+
),
|
|
258
|
+
8_000,
|
|
259
|
+
'join to time out',
|
|
260
|
+
);
|
|
261
|
+
expect(err.reason).toBe(ConnectionErrorReason.Timeout);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
});
|
package/src/api/SignalClient.ts
CHANGED
|
@@ -121,6 +121,11 @@ export enum SignalConnectionState {
|
|
|
121
121
|
/** specifies how much time (in ms) we allow for the ws to close its connection gracefully before continuing */
|
|
122
122
|
const MAX_WS_CLOSE_TIME = 250;
|
|
123
123
|
|
|
124
|
+
/**
|
|
125
|
+
* How long (in ms) to wait for the first message after the WebSocket upgrade.
|
|
126
|
+
*/
|
|
127
|
+
const JOIN_RESPONSE_TIMEOUT = 5_000;
|
|
128
|
+
|
|
124
129
|
/** @internal */
|
|
125
130
|
export class SignalClient {
|
|
126
131
|
requestQueue: AsyncQueue;
|
|
@@ -408,7 +413,7 @@ export class SignalClient {
|
|
|
408
413
|
state: this.state,
|
|
409
414
|
});
|
|
410
415
|
if (this.state === SignalConnectionState.CONNECTED) {
|
|
411
|
-
this.handleOnClose(closeInfo.reason
|
|
416
|
+
this.handleOnClose(closeInfo.reason || 'Unexpected WS error');
|
|
412
417
|
}
|
|
413
418
|
}
|
|
414
419
|
return;
|
|
@@ -441,7 +446,34 @@ export class SignalClient {
|
|
|
441
446
|
}
|
|
442
447
|
const signalReader = connection.readable.getReader();
|
|
443
448
|
this.streamWriter = connection.writable.getWriter();
|
|
444
|
-
|
|
449
|
+
|
|
450
|
+
// wsTimeout only guarded the upgrade; guard the first-message read with
|
|
451
|
+
// its own timeout so a silent server can't hang join() forever.
|
|
452
|
+
let firstMessage: ReadableStreamReadResult<string | ArrayBuffer>;
|
|
453
|
+
let firstMessageTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
454
|
+
try {
|
|
455
|
+
firstMessage = await Promise.race([
|
|
456
|
+
signalReader.read(),
|
|
457
|
+
new Promise<never>((_, rejectRead) => {
|
|
458
|
+
firstMessageTimeout = setTimeout(() => {
|
|
459
|
+
rejectRead(
|
|
460
|
+
ConnectionError.timeout(
|
|
461
|
+
'signal connection timed out while waiting for the first message',
|
|
462
|
+
),
|
|
463
|
+
);
|
|
464
|
+
}, JOIN_RESPONSE_TIMEOUT);
|
|
465
|
+
}),
|
|
466
|
+
]);
|
|
467
|
+
} catch (e) {
|
|
468
|
+
// No first message in time: release the reader and tear down the ws
|
|
469
|
+
// so we surface the timeout instead of leaking an open connection.
|
|
470
|
+
signalReader.releaseLock();
|
|
471
|
+
reject(e);
|
|
472
|
+
this.close();
|
|
473
|
+
return;
|
|
474
|
+
} finally {
|
|
475
|
+
clearTimeout(firstMessageTimeout);
|
|
476
|
+
}
|
|
445
477
|
signalReader.releaseLock();
|
|
446
478
|
if (!firstMessage.value) {
|
|
447
479
|
throw ConnectionError.internal('no message received as first message');
|
package/src/index.ts
CHANGED
package/src/logger.ts
CHANGED
package/src/room/PCTransport.ts
CHANGED
|
@@ -15,15 +15,24 @@ interface TrackBitrateInfo {
|
|
|
15
15
|
transceiver?: RTCRtpTransceiver;
|
|
16
16
|
codec: string;
|
|
17
17
|
maxbr: number;
|
|
18
|
+
isScreenShare?: boolean;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
/*
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
/*
|
|
22
|
+
* Video codecs use a very low bitrate at the beginning and increase slowly by
|
|
23
|
+
* the bandwidth estimator until they reach the target bitrate. The process commonly
|
|
24
|
+
* costs more than 10 seconds causing subscribers to get blurry video at the first
|
|
25
|
+
* few seconds. We use x-google-start-bitrate to hint the BWE to start higher.
|
|
26
|
+
*
|
|
27
|
+
* Why 90%: Gives ~10% headroom for bandwidth estimation while starting close to target.
|
|
28
|
+
* Why same for all codecs: Target bitrate already accounts for codec efficiency
|
|
29
|
+
* (e.g., users set lower targets for VP9/AV1 knowing they're more efficient).
|
|
30
|
+
* Why cap at 1 Mbps: Prevents BWE from starting too aggressively on high bitrate tracks.
|
|
31
|
+
*/
|
|
32
|
+
const startBitrateMultiplier = 0.9;
|
|
33
|
+
|
|
34
|
+
/** Maximum x-google-start-bitrate in kbps. 1 Mbps prevents BWE from starting too aggressively. */
|
|
35
|
+
const maxStartBitrateKbps = 1000;
|
|
27
36
|
|
|
28
37
|
const debounceInterval = 20;
|
|
29
38
|
|
|
@@ -53,6 +62,8 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
53
62
|
|
|
54
63
|
private log = log;
|
|
55
64
|
|
|
65
|
+
private iceLog = log;
|
|
66
|
+
|
|
56
67
|
private loggerOptions: LoggerOptions;
|
|
57
68
|
|
|
58
69
|
private ddExtID = 0;
|
|
@@ -95,8 +106,12 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
95
106
|
|
|
96
107
|
constructor(config?: RTCConfiguration, loggerOptions: LoggerOptions = {}) {
|
|
97
108
|
super();
|
|
98
|
-
this.log = getLogger(loggerOptions.loggerName ?? LoggerNames.PCTransport);
|
|
99
109
|
this.loggerOptions = loggerOptions;
|
|
110
|
+
this.log = getLogger(
|
|
111
|
+
loggerOptions.loggerName ?? LoggerNames.PCTransport,
|
|
112
|
+
() => this.logContext,
|
|
113
|
+
);
|
|
114
|
+
this.iceLog = getLogger(LoggerNames.ICE, () => this.logContext);
|
|
100
115
|
this.config = config;
|
|
101
116
|
this._pc = this.createPC();
|
|
102
117
|
this.offerLock = new Mutex();
|
|
@@ -107,24 +122,33 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
107
122
|
|
|
108
123
|
pc.onicecandidate = (ev) => {
|
|
109
124
|
if (!ev.candidate) return;
|
|
125
|
+
this.iceLog.debug('local ICE candidate gathered', { candidate: ev.candidate.candidate });
|
|
110
126
|
this.onIceCandidate?.(ev.candidate);
|
|
111
127
|
};
|
|
112
128
|
pc.onicecandidateerror = (ev) => {
|
|
129
|
+
this.iceLog.debug('ICE candidate error', { event: ev });
|
|
113
130
|
this.onIceCandidateError?.(ev);
|
|
114
131
|
};
|
|
115
132
|
|
|
116
133
|
pc.oniceconnectionstatechange = () => {
|
|
134
|
+
this.iceLog.debug(`ICE connection state: ${pc.iceConnectionState}`);
|
|
117
135
|
this.onIceConnectionStateChange?.(pc.iceConnectionState);
|
|
118
136
|
};
|
|
119
137
|
|
|
120
138
|
pc.onsignalingstatechange = () => {
|
|
139
|
+
this.log.debug(`signaling state: ${pc.signalingState}`);
|
|
121
140
|
this.onSignalingStatechange?.(pc.signalingState);
|
|
122
141
|
};
|
|
123
142
|
|
|
124
143
|
pc.onconnectionstatechange = () => {
|
|
144
|
+
this.log.debug(`connection state: ${pc.connectionState}`);
|
|
125
145
|
this.onConnectionStateChange?.(pc.connectionState);
|
|
126
146
|
};
|
|
127
147
|
pc.ondatachannel = (ev) => {
|
|
148
|
+
this.log.debug('data channel opened by peer', {
|
|
149
|
+
label: ev.channel.label,
|
|
150
|
+
id: ev.channel.id,
|
|
151
|
+
});
|
|
128
152
|
this.onDataChannel?.(ev);
|
|
129
153
|
};
|
|
130
154
|
pc.ontrack = (ev) => {
|
|
@@ -150,6 +174,9 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
150
174
|
if (this.pc.remoteDescription && !this.restartingIce) {
|
|
151
175
|
return this.pc.addIceCandidate(candidate);
|
|
152
176
|
}
|
|
177
|
+
this.iceLog.debug('queuing remote ICE candidate until remote description applied', {
|
|
178
|
+
pendingCount: this.pendingCandidates.length + 1,
|
|
179
|
+
});
|
|
153
180
|
this.pendingCandidates.push(candidate);
|
|
154
181
|
}
|
|
155
182
|
|
|
@@ -161,7 +188,6 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
161
188
|
offerId !== this.latestOfferId
|
|
162
189
|
) {
|
|
163
190
|
this.log.warn('ignoring answer for old offer', {
|
|
164
|
-
...this.logContext,
|
|
165
191
|
offerId,
|
|
166
192
|
latestOfferId: this.latestOfferId,
|
|
167
193
|
});
|
|
@@ -180,7 +206,7 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
180
206
|
sdpParsed.media.forEach((media) => {
|
|
181
207
|
ensureIPAddrMatchVersion(media);
|
|
182
208
|
});
|
|
183
|
-
this.log.debug('setting pending initial offer before processing answer'
|
|
209
|
+
this.log.debug('setting pending initial offer before processing answer');
|
|
184
210
|
await this.setMungedSDP(initialOffer, write(sdpParsed));
|
|
185
211
|
}
|
|
186
212
|
const sdpParsed = parse(sd.sdp ?? '');
|
|
@@ -249,6 +275,11 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
249
275
|
}
|
|
250
276
|
await this.setMungedSDP(sd, mungedSDP, true);
|
|
251
277
|
|
|
278
|
+
if (this.pendingCandidates.length > 0) {
|
|
279
|
+
this.iceLog.debug('flushing queued ICE candidates', {
|
|
280
|
+
count: this.pendingCandidates.length,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
252
283
|
this.pendingCandidates.forEach((candidate) => {
|
|
253
284
|
this.pc.addIceCandidate(candidate);
|
|
254
285
|
});
|
|
@@ -298,10 +329,7 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
298
329
|
const unlock = await this.offerLock.lock();
|
|
299
330
|
try {
|
|
300
331
|
if (this.pc.signalingState !== 'stable') {
|
|
301
|
-
this.log.warn(
|
|
302
|
-
'signaling state is not stable, cannot create initial offer',
|
|
303
|
-
this.logContext,
|
|
304
|
-
);
|
|
332
|
+
this.log.warn('signaling state is not stable, cannot create initial offer');
|
|
305
333
|
return;
|
|
306
334
|
}
|
|
307
335
|
const offerId = this.latestOfferId + 1;
|
|
@@ -328,7 +356,7 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
328
356
|
}
|
|
329
357
|
|
|
330
358
|
if (options?.iceRestart) {
|
|
331
|
-
this.
|
|
359
|
+
this.iceLog.debug('restarting ICE');
|
|
332
360
|
this.restartingIce = true;
|
|
333
361
|
}
|
|
334
362
|
|
|
@@ -345,21 +373,21 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
345
373
|
await this._pc.setRemoteDescription(currentSD);
|
|
346
374
|
} else {
|
|
347
375
|
this.renegotiate = true;
|
|
348
|
-
this.log.debug('requesting renegotiation'
|
|
376
|
+
this.log.debug('requesting renegotiation');
|
|
349
377
|
return;
|
|
350
378
|
}
|
|
351
379
|
} else if (!this._pc || this._pc.signalingState === 'closed') {
|
|
352
|
-
this.log.warn('could not createOffer with closed peer connection'
|
|
380
|
+
this.log.warn('could not createOffer with closed peer connection');
|
|
353
381
|
return;
|
|
354
382
|
}
|
|
355
383
|
|
|
356
384
|
// actually negotiate
|
|
357
|
-
this.log.debug('starting to negotiate'
|
|
385
|
+
this.log.debug('starting to negotiate');
|
|
358
386
|
// increase the offer id at the start to ensure the offer is always > 0 so that we can use 0 as a default value for legacy behavior
|
|
359
387
|
const offerId = this.latestOfferId + 1;
|
|
360
388
|
this.latestOfferId = offerId;
|
|
361
389
|
const offer = await this.pc.createOffer(options);
|
|
362
|
-
this.log.debug('original offer', { sdp: offer.sdp
|
|
390
|
+
this.log.debug('original offer', { sdp: offer.sdp });
|
|
363
391
|
|
|
364
392
|
const sdpParsed = parse(offer.sdp ?? '');
|
|
365
393
|
sdpParsed.media.forEach((media) => {
|
|
@@ -390,14 +418,18 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
390
418
|
}
|
|
391
419
|
|
|
392
420
|
// mung sdp for bitrate setting that can't apply by sendEncoding
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
const startBitrate =
|
|
421
|
+
// Use 90% of target bitrate, capped at 1 Mbps for camera to prevent BWE from starting too aggressively
|
|
422
|
+
// Screen share is not capped since text/UI clarity requires high bitrate from the start
|
|
423
|
+
// TODO: dynamically adjust start bitrate based on network conditions (e.g., use previous BWE estimate)
|
|
424
|
+
const calculatedStartBitrate = Math.round(trackbr.maxbr * startBitrateMultiplier);
|
|
425
|
+
const startBitrate = trackbr.isScreenShare
|
|
426
|
+
? calculatedStartBitrate
|
|
427
|
+
: Math.min(calculatedStartBitrate, maxStartBitrateKbps);
|
|
398
428
|
|
|
429
|
+
let fmtpFound = false;
|
|
399
430
|
for (const fmtp of media.fmtp) {
|
|
400
431
|
if (fmtp.payload === codecPayload) {
|
|
432
|
+
fmtpFound = true;
|
|
401
433
|
// if another track's fmtp already is set, we cannot override the bitrate
|
|
402
434
|
// this has the unfortunate consequence of being forced to use the
|
|
403
435
|
// initial track's bitrate for all tracks
|
|
@@ -407,6 +439,13 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
407
439
|
break;
|
|
408
440
|
}
|
|
409
441
|
}
|
|
442
|
+
// VP8 and some codecs may not have an existing fmtp line - create one
|
|
443
|
+
if (!fmtpFound) {
|
|
444
|
+
media.fmtp.push({
|
|
445
|
+
payload: codecPayload,
|
|
446
|
+
config: `x-google-start-bitrate=${startBitrate}`,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
410
449
|
return true;
|
|
411
450
|
});
|
|
412
451
|
}
|
|
@@ -424,7 +463,6 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
424
463
|
}
|
|
425
464
|
if (this.latestOfferId > offerId) {
|
|
426
465
|
this.log.warn('latestOfferId mismatch', {
|
|
427
|
-
...this.logContext,
|
|
428
466
|
latestOfferId: this.latestOfferId,
|
|
429
467
|
offerId,
|
|
430
468
|
});
|
|
@@ -576,6 +614,7 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
576
614
|
if (!this._pc) {
|
|
577
615
|
return;
|
|
578
616
|
}
|
|
617
|
+
this.log.debug('closing peer connection');
|
|
579
618
|
this.pendingInitialOffer = undefined;
|
|
580
619
|
this._pc.close();
|
|
581
620
|
this._pc.onconnectionstatechange = null;
|
|
@@ -597,10 +636,7 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
597
636
|
if (munged) {
|
|
598
637
|
sd.sdp = munged;
|
|
599
638
|
try {
|
|
600
|
-
this.log.debug(
|
|
601
|
-
`setting munged ${remote ? 'remote' : 'local'} description`,
|
|
602
|
-
this.logContext,
|
|
603
|
-
);
|
|
639
|
+
this.log.debug(`setting munged ${remote ? 'remote' : 'local'} description`);
|
|
604
640
|
if (remote) {
|
|
605
641
|
await this.pc.setRemoteDescription(sd);
|
|
606
642
|
} else {
|
|
@@ -609,7 +645,6 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
609
645
|
return;
|
|
610
646
|
} catch (e) {
|
|
611
647
|
this.log.warn(`not able to set ${sd.type}, falling back to unmodified sdp`, {
|
|
612
|
-
...this.logContext,
|
|
613
648
|
error: e,
|
|
614
649
|
mungedSdp: munged,
|
|
615
650
|
originalSdp,
|
|
@@ -642,7 +677,7 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
642
677
|
if (!remote && this.pc.remoteDescription) {
|
|
643
678
|
fields.remoteSdp = this.pc.remoteDescription;
|
|
644
679
|
}
|
|
645
|
-
this.log.error(`unable to set ${sd.type}`, {
|
|
680
|
+
this.log.error(`unable to set ${sd.type}`, { fields });
|
|
646
681
|
throw new NegotiationError(msg);
|
|
647
682
|
}
|
|
648
683
|
}
|
|
@@ -65,6 +65,8 @@ export class PCTransportManager {
|
|
|
65
65
|
|
|
66
66
|
private log = log;
|
|
67
67
|
|
|
68
|
+
private iceLog = log;
|
|
69
|
+
|
|
68
70
|
private loggerOptions: LoggerOptions;
|
|
69
71
|
|
|
70
72
|
private _mode: PCMode;
|
|
@@ -74,8 +76,9 @@ export class PCTransportManager {
|
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
constructor(mode: PCMode, loggerOptions: LoggerOptions, rtcConfig?: RTCConfiguration) {
|
|
77
|
-
this.log = getLogger(loggerOptions.loggerName ?? LoggerNames.PCManager);
|
|
78
79
|
this.loggerOptions = loggerOptions;
|
|
80
|
+
this.log = getLogger(loggerOptions.loggerName ?? LoggerNames.PCManager, () => this.logContext);
|
|
81
|
+
this.iceLog = getLogger(LoggerNames.ICE, () => this.logContext);
|
|
79
82
|
|
|
80
83
|
this.isPublisherConnectionRequired = mode !== 'subscriber-primary';
|
|
81
84
|
this.isSubscriberConnectionRequired = mode === 'subscriber-primary';
|
|
@@ -151,7 +154,7 @@ export class PCTransportManager {
|
|
|
151
154
|
publisher.removeTrack(sender);
|
|
152
155
|
}
|
|
153
156
|
} catch (e) {
|
|
154
|
-
this.log.warn('could not removeTrack', {
|
|
157
|
+
this.log.warn('could not removeTrack', { error: e });
|
|
155
158
|
}
|
|
156
159
|
}
|
|
157
160
|
}
|
|
@@ -160,6 +163,7 @@ export class PCTransportManager {
|
|
|
160
163
|
}
|
|
161
164
|
|
|
162
165
|
async triggerIceRestart() {
|
|
166
|
+
this.iceLog.warn('triggering ICE restart');
|
|
163
167
|
if (this.subscriber) {
|
|
164
168
|
this.subscriber.restartingIce = true;
|
|
165
169
|
}
|
|
@@ -170,6 +174,7 @@ export class PCTransportManager {
|
|
|
170
174
|
}
|
|
171
175
|
|
|
172
176
|
async addIceCandidate(candidate: RTCIceCandidateInit, target: SignalTarget) {
|
|
177
|
+
this.iceLog.debug('adding remote ICE candidate', { target, candidate });
|
|
173
178
|
if (target === SignalTarget.PUBLISHER) {
|
|
174
179
|
await this.publisher.addIceCandidate(candidate);
|
|
175
180
|
} else {
|
|
@@ -179,7 +184,6 @@ export class PCTransportManager {
|
|
|
179
184
|
|
|
180
185
|
async createSubscriberAnswerFromOffer(sd: RTCSessionDescriptionInit, offerId: number) {
|
|
181
186
|
this.log.debug('received server offer', {
|
|
182
|
-
...this.logContext,
|
|
183
187
|
RTCSdpType: sd.type,
|
|
184
188
|
sdp: sd.sdp,
|
|
185
189
|
signalingState: this.subscriber?.getSignallingState().toString(),
|
|
@@ -200,6 +204,7 @@ export class PCTransportManager {
|
|
|
200
204
|
}
|
|
201
205
|
|
|
202
206
|
updateConfiguration(config: RTCConfiguration, iceRestart?: boolean) {
|
|
207
|
+
this.log.debug('updating rtc configuration', { iceRestart });
|
|
203
208
|
this.publisher.setConfiguration(config);
|
|
204
209
|
this.subscriber?.setConfiguration(config);
|
|
205
210
|
if (iceRestart) {
|
|
@@ -215,7 +220,7 @@ export class PCTransportManager {
|
|
|
215
220
|
this.publisher.getConnectionState() !== 'connected' &&
|
|
216
221
|
this.publisher.getConnectionState() !== 'connecting'
|
|
217
222
|
) {
|
|
218
|
-
this.log.debug('negotiation required, start negotiating'
|
|
223
|
+
this.log.debug('negotiation required, start negotiating');
|
|
219
224
|
this.publisher.negotiate();
|
|
220
225
|
}
|
|
221
226
|
await Promise.all(
|
|
@@ -368,7 +373,6 @@ export class PCTransportManager {
|
|
|
368
373
|
`pc state change: from ${PCTransportState[previousState]} to ${
|
|
369
374
|
PCTransportState[this.state]
|
|
370
375
|
}`,
|
|
371
|
-
this.logContext,
|
|
372
376
|
);
|
|
373
377
|
this.onStateChange?.(
|
|
374
378
|
this.state,
|
|
@@ -390,7 +394,7 @@ export class PCTransportManager {
|
|
|
390
394
|
|
|
391
395
|
return new Promise<void>(async (resolve, reject) => {
|
|
392
396
|
const abortHandler = () => {
|
|
393
|
-
this.log.warn('abort transport connection'
|
|
397
|
+
this.log.warn('abort transport connection');
|
|
394
398
|
CriticalTimers.clearTimeout(connectTimeout);
|
|
395
399
|
|
|
396
400
|
reject(ConnectionError.cancelled('room connection has been cancelled'));
|