livekit-client 2.15.8 → 2.15.9
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.esm.mjs +577 -202
- package/dist/livekit-client.esm.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 +31 -2
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/api/WebSocketStream.d.ts +29 -0
- package/dist/src/api/WebSocketStream.d.ts.map +1 -0
- package/dist/src/api/utils.d.ts +2 -0
- package/dist/src/api/utils.d.ts.map +1 -1
- package/dist/src/connectionHelper/checks/turn.d.ts.map +1 -1
- package/dist/src/connectionHelper/checks/websocket.d.ts.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/options.d.ts +6 -0
- package/dist/src/options.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +1 -0
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/PCTransportManager.d.ts +6 -4
- package/dist/src/room/PCTransportManager.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +1 -1
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/defaults.d.ts.map +1 -1
- package/dist/src/room/token-source/utils.d.ts +1 -1
- package/dist/src/room/token-source/utils.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +6 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/ts4.2/api/SignalClient.d.ts +31 -2
- package/dist/ts4.2/api/WebSocketStream.d.ts +29 -0
- package/dist/ts4.2/api/utils.d.ts +2 -0
- package/dist/ts4.2/index.d.ts +2 -2
- package/dist/ts4.2/options.d.ts +6 -0
- package/dist/ts4.2/room/PCTransport.d.ts +1 -0
- package/dist/ts4.2/room/PCTransportManager.d.ts +6 -4
- package/dist/ts4.2/room/RTCEngine.d.ts +1 -1
- package/dist/ts4.2/room/token-source/utils.d.ts +1 -1
- package/dist/ts4.2/room/utils.d.ts +6 -0
- package/package.json +1 -1
- package/src/api/SignalClient.test.ts +688 -0
- package/src/api/SignalClient.ts +308 -161
- package/src/api/WebSocketStream.test.ts +625 -0
- package/src/api/WebSocketStream.ts +118 -0
- package/src/api/utils.ts +10 -0
- package/src/connectionHelper/checks/turn.ts +1 -0
- package/src/connectionHelper/checks/webrtc.ts +1 -1
- package/src/connectionHelper/checks/websocket.ts +1 -0
- package/src/index.ts +2 -0
- package/src/options.ts +7 -0
- package/src/room/PCTransport.ts +7 -3
- package/src/room/PCTransportManager.ts +39 -35
- package/src/room/RTCEngine.ts +54 -16
- package/src/room/Room.ts +5 -2
- package/src/room/defaults.ts +1 -0
- package/src/room/token-source/TokenSource.ts +2 -2
- package/src/room/token-source/utils.test.ts +63 -0
- package/src/room/token-source/utils.ts +10 -5
- package/src/room/utils.ts +29 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// https://github.com/CarterLi/websocketstream-polyfill
|
|
2
|
+
import { sleep } from '../room/utils';
|
|
3
|
+
|
|
4
|
+
export interface WebSocketConnection<T extends ArrayBuffer | string = ArrayBuffer | string> {
|
|
5
|
+
readable: ReadableStream<T>;
|
|
6
|
+
writable: WritableStream<T>;
|
|
7
|
+
protocol: string;
|
|
8
|
+
extensions: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface WebSocketCloseInfo {
|
|
12
|
+
closeCode?: number;
|
|
13
|
+
reason?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface WebSocketStreamOptions {
|
|
17
|
+
protocols?: string[];
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) with [Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API)
|
|
23
|
+
*
|
|
24
|
+
* @see https://web.dev/websocketstream/
|
|
25
|
+
*/
|
|
26
|
+
export class WebSocketStream<T extends ArrayBuffer | string = ArrayBuffer | string> {
|
|
27
|
+
readonly url: string;
|
|
28
|
+
|
|
29
|
+
readonly opened: Promise<WebSocketConnection<T>>;
|
|
30
|
+
|
|
31
|
+
readonly closed: Promise<WebSocketCloseInfo>;
|
|
32
|
+
|
|
33
|
+
readonly close: (closeInfo?: WebSocketCloseInfo) => void;
|
|
34
|
+
|
|
35
|
+
get readyState(): number {
|
|
36
|
+
return this.ws.readyState;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private ws: WebSocket;
|
|
40
|
+
|
|
41
|
+
constructor(url: string, options: WebSocketStreamOptions = {}) {
|
|
42
|
+
if (options.signal?.aborted) {
|
|
43
|
+
throw new DOMException('This operation was aborted', 'AbortError');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this.url = url;
|
|
47
|
+
|
|
48
|
+
const ws = new WebSocket(url, options.protocols ?? []);
|
|
49
|
+
ws.binaryType = 'arraybuffer';
|
|
50
|
+
this.ws = ws;
|
|
51
|
+
|
|
52
|
+
const closeWithInfo = ({ closeCode: code, reason }: WebSocketCloseInfo = {}) =>
|
|
53
|
+
ws.close(code, reason);
|
|
54
|
+
|
|
55
|
+
this.opened = new Promise((resolve, reject) => {
|
|
56
|
+
ws.onopen = () => {
|
|
57
|
+
resolve({
|
|
58
|
+
readable: new ReadableStream<T>({
|
|
59
|
+
start(controller) {
|
|
60
|
+
ws.onmessage = ({ data }) => controller.enqueue(data);
|
|
61
|
+
ws.onerror = (e) => controller.error(e);
|
|
62
|
+
},
|
|
63
|
+
cancel: closeWithInfo,
|
|
64
|
+
}),
|
|
65
|
+
writable: new WritableStream<T>({
|
|
66
|
+
write(chunk) {
|
|
67
|
+
ws.send(chunk);
|
|
68
|
+
},
|
|
69
|
+
abort() {
|
|
70
|
+
ws.close();
|
|
71
|
+
},
|
|
72
|
+
close: closeWithInfo,
|
|
73
|
+
}),
|
|
74
|
+
protocol: ws.protocol,
|
|
75
|
+
extensions: ws.extensions,
|
|
76
|
+
});
|
|
77
|
+
ws.removeEventListener('error', reject);
|
|
78
|
+
};
|
|
79
|
+
ws.addEventListener('error', reject);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
this.closed = new Promise<WebSocketCloseInfo>((resolve, reject) => {
|
|
83
|
+
const rejectHandler = async () => {
|
|
84
|
+
const closePromise = new Promise<CloseEvent>((res) => {
|
|
85
|
+
if (ws.readyState === WebSocket.CLOSED) return;
|
|
86
|
+
else {
|
|
87
|
+
ws.addEventListener(
|
|
88
|
+
'close',
|
|
89
|
+
(closeEv: CloseEvent) => {
|
|
90
|
+
res(closeEv);
|
|
91
|
+
},
|
|
92
|
+
{ once: true },
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
const reason = await Promise.race([sleep(250), closePromise]);
|
|
97
|
+
if (!reason) {
|
|
98
|
+
reject(new Error('Encountered unspecified websocket error without a timely close event'));
|
|
99
|
+
} else {
|
|
100
|
+
// if we can infer the close reason from the close event then resolve the promise, we don't need to throw
|
|
101
|
+
resolve(reason);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
ws.onclose = ({ code, reason }) => {
|
|
105
|
+
resolve({ closeCode: code, reason });
|
|
106
|
+
ws.removeEventListener('error', rejectHandler);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
ws.addEventListener('error', rejectHandler);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (options.signal) {
|
|
113
|
+
options.signal.onabort = () => ws.close();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this.close = closeWithInfo;
|
|
117
|
+
}
|
|
118
|
+
}
|
package/src/api/utils.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SignalResponse } from '@livekit/protocol';
|
|
1
2
|
import { toHttpUrl, toWebsocketUrl } from '../room/utils';
|
|
2
3
|
|
|
3
4
|
export function createRtcUrl(url: string, searchParams: URLSearchParams) {
|
|
@@ -21,3 +22,12 @@ function appendUrlPath(urlObj: URL, path: string) {
|
|
|
21
22
|
urlObj.pathname = `${ensureTrailingSlash(urlObj.pathname)}${path}`;
|
|
22
23
|
return urlObj.toString();
|
|
23
24
|
}
|
|
25
|
+
|
|
26
|
+
export function parseSignalResponse(value: ArrayBuffer | string) {
|
|
27
|
+
if (typeof value === 'string') {
|
|
28
|
+
return SignalResponse.fromJson(JSON.parse(value), { ignoreUnknownFields: true });
|
|
29
|
+
} else if (value instanceof ArrayBuffer) {
|
|
30
|
+
return SignalResponse.fromBinary(new Uint8Array(value));
|
|
31
|
+
}
|
|
32
|
+
throw new Error(`could not decode websocket message: ${typeof value}`);
|
|
33
|
+
}
|
|
@@ -38,7 +38,7 @@ export class WebRTCCheck extends Checker {
|
|
|
38
38
|
}
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
-
if (this.room.engine.pcManager) {
|
|
41
|
+
if (this.room.engine.pcManager?.subscriber) {
|
|
42
42
|
this.room.engine.pcManager.subscriber.onIceCandidateError = (ev) => {
|
|
43
43
|
if (ev instanceof RTCPeerConnectionIceErrorEvent) {
|
|
44
44
|
this.appendWarning(
|
|
@@ -18,6 +18,7 @@ export class WebSocketCheck extends Checker {
|
|
|
18
18
|
maxRetries: 0,
|
|
19
19
|
e2eeEnabled: false,
|
|
20
20
|
websocketTimeout: 15_000,
|
|
21
|
+
singlePeerConnection: false,
|
|
21
22
|
});
|
|
22
23
|
this.appendMessage(`Connected to server, version ${joinRes.serverVersion}.`);
|
|
23
24
|
if (joinRes.serverInfo?.edition === ServerInfo_Edition.Cloud && joinRes.serverInfo?.region) {
|
package/src/index.ts
CHANGED
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
isVideoTrack,
|
|
51
51
|
supportsAV1,
|
|
52
52
|
supportsAdaptiveStream,
|
|
53
|
+
supportsAudioOutputSelection,
|
|
53
54
|
supportsDynacast,
|
|
54
55
|
supportsVP9,
|
|
55
56
|
} from './room/utils';
|
|
@@ -121,6 +122,7 @@ export {
|
|
|
121
122
|
setLogLevel,
|
|
122
123
|
supportsAV1,
|
|
123
124
|
supportsAdaptiveStream,
|
|
125
|
+
supportsAudioOutputSelection,
|
|
124
126
|
supportsDynacast,
|
|
125
127
|
supportsVP9,
|
|
126
128
|
Mutex,
|
package/src/options.ts
CHANGED
|
@@ -99,6 +99,13 @@ export interface InternalRoomOptions {
|
|
|
99
99
|
encryption?: E2EEOptions;
|
|
100
100
|
|
|
101
101
|
loggerName?: string;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @experimental
|
|
105
|
+
* only supported on LiveKit Cloud
|
|
106
|
+
* and LiveKit OSS >= 1.9.2
|
|
107
|
+
*/
|
|
108
|
+
singlePeerConnection: boolean;
|
|
102
109
|
}
|
|
103
110
|
|
|
104
111
|
/**
|
package/src/room/PCTransport.ts
CHANGED
|
@@ -167,7 +167,7 @@ export default class PCTransport extends EventEmitter {
|
|
|
167
167
|
sdpParsed.media.forEach((media) => {
|
|
168
168
|
const mid = getMidString(media.mid!);
|
|
169
169
|
if (media.type === 'audio') {
|
|
170
|
-
//
|
|
170
|
+
// munge sdp for opus bitrate settings
|
|
171
171
|
this.trackBitrates.some((trackbr): boolean => {
|
|
172
172
|
if (!trackbr.transceiver || mid != trackbr.transceiver.mid) {
|
|
173
173
|
return false;
|
|
@@ -297,7 +297,7 @@ export default class PCTransport extends EventEmitter {
|
|
|
297
297
|
sdpParsed.media.forEach((media) => {
|
|
298
298
|
ensureIPAddrMatchVersion(media);
|
|
299
299
|
if (media.type === 'audio') {
|
|
300
|
-
ensureAudioNackAndStereo(media, [], []);
|
|
300
|
+
ensureAudioNackAndStereo(media, ['all'], []);
|
|
301
301
|
} else if (media.type === 'video') {
|
|
302
302
|
this.trackBitrates.some((trackbr): boolean => {
|
|
303
303
|
if (!media.msid || !trackbr.cid || !media.msid.includes(trackbr.cid)) {
|
|
@@ -380,6 +380,10 @@ export default class PCTransport extends EventEmitter {
|
|
|
380
380
|
return this.pc.addTransceiver(mediaStreamTrack, transceiverInit);
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
+
addTransceiverOfKind(kind: 'audio' | 'video', transceiverInit: RTCRtpTransceiverInit) {
|
|
384
|
+
return this.pc.addTransceiver(kind, transceiverInit);
|
|
385
|
+
}
|
|
386
|
+
|
|
383
387
|
addTrack(track: MediaStreamTrack) {
|
|
384
388
|
if (!this._pc) {
|
|
385
389
|
throw new UnexpectedConnectionState('PC closed, cannot add track');
|
|
@@ -623,7 +627,7 @@ function ensureAudioNackAndStereo(
|
|
|
623
627
|
});
|
|
624
628
|
}
|
|
625
629
|
|
|
626
|
-
if (stereoMids.includes(mid)) {
|
|
630
|
+
if (stereoMids.includes(mid) || (stereoMids.length === 1 && stereoMids[0] === 'all')) {
|
|
627
631
|
media.fmtp.some((fmtp): boolean => {
|
|
628
632
|
if (fmtp.payload === opusPayload) {
|
|
629
633
|
if (!fmtp.config.includes('stereo=1')) {
|
|
@@ -17,10 +17,11 @@ export enum PCTransportState {
|
|
|
17
17
|
CLOSED,
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
type PCMode = 'subscriber-primary' | 'publisher-primary' | 'publisher-only';
|
|
20
21
|
export class PCTransportManager {
|
|
21
22
|
public publisher: PCTransport;
|
|
22
23
|
|
|
23
|
-
public subscriber
|
|
24
|
+
public subscriber?: PCTransport;
|
|
24
25
|
|
|
25
26
|
public peerConnectionTimeout: number = roomConnectOptionDefaults.peerConnectionTimeout;
|
|
26
27
|
|
|
@@ -39,7 +40,7 @@ export class PCTransportManager {
|
|
|
39
40
|
public onStateChange?: (
|
|
40
41
|
state: PCTransportState,
|
|
41
42
|
pubState: RTCPeerConnectionState,
|
|
42
|
-
subState
|
|
43
|
+
subState?: RTCPeerConnectionState,
|
|
43
44
|
) => void;
|
|
44
45
|
|
|
45
46
|
public onIceCandidate?: (ev: RTCIceCandidate, target: SignalTarget) => void;
|
|
@@ -64,38 +65,40 @@ export class PCTransportManager {
|
|
|
64
65
|
|
|
65
66
|
private loggerOptions: LoggerOptions;
|
|
66
67
|
|
|
67
|
-
constructor(
|
|
68
|
-
rtcConfig: RTCConfiguration,
|
|
69
|
-
subscriberPrimary: boolean,
|
|
70
|
-
loggerOptions: LoggerOptions,
|
|
71
|
-
) {
|
|
68
|
+
constructor(rtcConfig: RTCConfiguration, mode: PCMode, loggerOptions: LoggerOptions) {
|
|
72
69
|
this.log = getLogger(loggerOptions.loggerName ?? LoggerNames.PCManager);
|
|
73
70
|
this.loggerOptions = loggerOptions;
|
|
74
71
|
|
|
75
|
-
this.isPublisherConnectionRequired =
|
|
76
|
-
this.isSubscriberConnectionRequired =
|
|
72
|
+
this.isPublisherConnectionRequired = mode !== 'subscriber-primary';
|
|
73
|
+
this.isSubscriberConnectionRequired = mode === 'subscriber-primary';
|
|
77
74
|
this.publisher = new PCTransport(rtcConfig, loggerOptions);
|
|
78
|
-
|
|
75
|
+
if (mode !== 'publisher-only') {
|
|
76
|
+
this.subscriber = new PCTransport(rtcConfig, loggerOptions);
|
|
77
|
+
this.subscriber.onConnectionStateChange = this.updateState;
|
|
78
|
+
this.subscriber.onIceConnectionStateChange = this.updateState;
|
|
79
|
+
this.subscriber.onSignalingStatechange = this.updateState;
|
|
80
|
+
this.subscriber.onIceCandidate = (candidate) => {
|
|
81
|
+
this.onIceCandidate?.(candidate, SignalTarget.SUBSCRIBER);
|
|
82
|
+
};
|
|
83
|
+
// in subscriber primary mode, server side opens sub data channels.
|
|
84
|
+
this.subscriber.onDataChannel = (ev) => {
|
|
85
|
+
this.onDataChannel?.(ev);
|
|
86
|
+
};
|
|
87
|
+
this.subscriber.onTrack = (ev) => {
|
|
88
|
+
this.onTrack?.(ev);
|
|
89
|
+
};
|
|
90
|
+
}
|
|
79
91
|
|
|
80
92
|
this.publisher.onConnectionStateChange = this.updateState;
|
|
81
|
-
this.subscriber.onConnectionStateChange = this.updateState;
|
|
82
93
|
this.publisher.onIceConnectionStateChange = this.updateState;
|
|
83
|
-
this.subscriber.onIceConnectionStateChange = this.updateState;
|
|
84
94
|
this.publisher.onSignalingStatechange = this.updateState;
|
|
85
|
-
this.subscriber.onSignalingStatechange = this.updateState;
|
|
86
95
|
this.publisher.onIceCandidate = (candidate) => {
|
|
87
96
|
this.onIceCandidate?.(candidate, SignalTarget.PUBLISHER);
|
|
88
97
|
};
|
|
89
|
-
this.
|
|
90
|
-
this.onIceCandidate?.(candidate, SignalTarget.SUBSCRIBER);
|
|
91
|
-
};
|
|
92
|
-
// in subscriber primary mode, server side opens sub data channels.
|
|
93
|
-
this.subscriber.onDataChannel = (ev) => {
|
|
94
|
-
this.onDataChannel?.(ev);
|
|
95
|
-
};
|
|
96
|
-
this.subscriber.onTrack = (ev) => {
|
|
98
|
+
this.publisher.onTrack = (ev) => {
|
|
97
99
|
this.onTrack?.(ev);
|
|
98
100
|
};
|
|
101
|
+
|
|
99
102
|
this.publisher.onOffer = (offer, offerId) => {
|
|
100
103
|
this.onPublisherOffer?.(offer, offerId);
|
|
101
104
|
};
|
|
@@ -117,11 +120,6 @@ export class PCTransportManager {
|
|
|
117
120
|
this.updateState();
|
|
118
121
|
}
|
|
119
122
|
|
|
120
|
-
requireSubscriber(require = true) {
|
|
121
|
-
this.isSubscriberConnectionRequired = require;
|
|
122
|
-
this.updateState();
|
|
123
|
-
}
|
|
124
|
-
|
|
125
123
|
createAndSendPublisherOffer(options?: RTCOfferOptions) {
|
|
126
124
|
return this.publisher.createAndSendOffer(options);
|
|
127
125
|
}
|
|
@@ -148,12 +146,14 @@ export class PCTransportManager {
|
|
|
148
146
|
}
|
|
149
147
|
}
|
|
150
148
|
}
|
|
151
|
-
await Promise.all([this.publisher.close(), this.subscriber
|
|
149
|
+
await Promise.all([this.publisher.close(), this.subscriber?.close()]);
|
|
152
150
|
this.updateState();
|
|
153
151
|
}
|
|
154
152
|
|
|
155
153
|
async triggerIceRestart() {
|
|
156
|
-
this.subscriber
|
|
154
|
+
if (this.subscriber) {
|
|
155
|
+
this.subscriber.restartingIce = true;
|
|
156
|
+
}
|
|
157
157
|
// only restart publisher if it's needed
|
|
158
158
|
if (this.needsPublisher) {
|
|
159
159
|
await this.createAndSendPublisherOffer({ iceRestart: true });
|
|
@@ -164,7 +164,7 @@ export class PCTransportManager {
|
|
|
164
164
|
if (target === SignalTarget.PUBLISHER) {
|
|
165
165
|
await this.publisher.addIceCandidate(candidate);
|
|
166
166
|
} else {
|
|
167
|
-
await this.subscriber
|
|
167
|
+
await this.subscriber?.addIceCandidate(candidate);
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
|
|
@@ -173,17 +173,17 @@ export class PCTransportManager {
|
|
|
173
173
|
...this.logContext,
|
|
174
174
|
RTCSdpType: sd.type,
|
|
175
175
|
sdp: sd.sdp,
|
|
176
|
-
signalingState: this.subscriber
|
|
176
|
+
signalingState: this.subscriber?.getSignallingState().toString(),
|
|
177
177
|
});
|
|
178
178
|
const unlock = await this.remoteOfferLock.lock();
|
|
179
179
|
try {
|
|
180
|
-
const success = await this.subscriber
|
|
180
|
+
const success = await this.subscriber?.setRemoteDescription(sd, offerId);
|
|
181
181
|
if (!success) {
|
|
182
182
|
return undefined;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
// answer the offer
|
|
186
|
-
const answer = await this.subscriber
|
|
186
|
+
const answer = await this.subscriber?.createAndSetAnswer();
|
|
187
187
|
return answer;
|
|
188
188
|
} finally {
|
|
189
189
|
unlock();
|
|
@@ -192,7 +192,7 @@ export class PCTransportManager {
|
|
|
192
192
|
|
|
193
193
|
updateConfiguration(config: RTCConfiguration, iceRestart?: boolean) {
|
|
194
194
|
this.publisher.setConfiguration(config);
|
|
195
|
-
this.subscriber
|
|
195
|
+
this.subscriber?.setConfiguration(config);
|
|
196
196
|
if (iceRestart) {
|
|
197
197
|
this.triggerIceRestart();
|
|
198
198
|
}
|
|
@@ -252,6 +252,10 @@ export class PCTransportManager {
|
|
|
252
252
|
return this.publisher.addTransceiver(track, transceiverInit);
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
addPublisherTransceiverOfKind(kind: 'audio' | 'video', transceiverInit: RTCRtpTransceiverInit) {
|
|
256
|
+
return this.publisher.addTransceiverOfKind(kind, transceiverInit);
|
|
257
|
+
}
|
|
258
|
+
|
|
255
259
|
addPublisherTrack(track: MediaStreamTrack) {
|
|
256
260
|
return this.publisher.addTrack(track);
|
|
257
261
|
}
|
|
@@ -277,7 +281,7 @@ export class PCTransportManager {
|
|
|
277
281
|
if (this.isPublisherConnectionRequired) {
|
|
278
282
|
transports.push(this.publisher);
|
|
279
283
|
}
|
|
280
|
-
if (this.isSubscriberConnectionRequired) {
|
|
284
|
+
if (this.isSubscriberConnectionRequired && this.subscriber) {
|
|
281
285
|
transports.push(this.subscriber);
|
|
282
286
|
}
|
|
283
287
|
return transports;
|
|
@@ -311,7 +315,7 @@ export class PCTransportManager {
|
|
|
311
315
|
this.onStateChange?.(
|
|
312
316
|
this.state,
|
|
313
317
|
this.publisher.getConnectionState(),
|
|
314
|
-
this.subscriber
|
|
318
|
+
this.subscriber?.getConnectionState(),
|
|
315
319
|
);
|
|
316
320
|
}
|
|
317
321
|
};
|
package/src/room/RTCEngine.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type JoinResponse,
|
|
16
16
|
type LeaveRequest,
|
|
17
17
|
LeaveRequest_Action,
|
|
18
|
+
MediaSectionsRequirement,
|
|
18
19
|
ParticipantInfo,
|
|
19
20
|
ReconnectReason,
|
|
20
21
|
type ReconnectResponse,
|
|
@@ -425,7 +426,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
425
426
|
|
|
426
427
|
this.pcManager = new PCTransportManager(
|
|
427
428
|
rtcConfig,
|
|
428
|
-
|
|
429
|
+
this.options.singlePeerConnection
|
|
430
|
+
? 'publisher-only'
|
|
431
|
+
: joinResponse.subscriberPrimary
|
|
432
|
+
? 'subscriber-primary'
|
|
433
|
+
: 'publisher-primary',
|
|
429
434
|
this.loggerOptions,
|
|
430
435
|
);
|
|
431
436
|
|
|
@@ -481,6 +486,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
481
486
|
}
|
|
482
487
|
};
|
|
483
488
|
this.pcManager.onTrack = (ev: RTCTrackEvent) => {
|
|
489
|
+
// this fires after the underlying transceiver is stopped and potentially
|
|
490
|
+
// peer connection closed, so do not bubble up if there are no streams
|
|
491
|
+
if (ev.streams.length === 0) return;
|
|
484
492
|
this.emit(EngineEvent.MediaTrackAdded, ev.track, ev.streams[0], ev.receiver);
|
|
485
493
|
};
|
|
486
494
|
|
|
@@ -566,6 +574,18 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
566
574
|
this.emit(EngineEvent.RoomMoved, res);
|
|
567
575
|
};
|
|
568
576
|
|
|
577
|
+
this.client.onMediaSectionsRequirement = (requirement: MediaSectionsRequirement) => {
|
|
578
|
+
const transceiverInit: RTCRtpTransceiverInit = { direction: 'recvonly' };
|
|
579
|
+
for (let i: number = 0; i < requirement.numAudios; i++) {
|
|
580
|
+
this.pcManager?.addPublisherTransceiverOfKind('audio', transceiverInit);
|
|
581
|
+
}
|
|
582
|
+
for (let i: number = 0; i < requirement.numVideos; i++) {
|
|
583
|
+
this.pcManager?.addPublisherTransceiverOfKind('video', transceiverInit);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
this.negotiate();
|
|
587
|
+
};
|
|
588
|
+
|
|
569
589
|
this.client.onClose = () => {
|
|
570
590
|
this.handleDisconnect('signal', ReconnectReason.RR_SIGNAL_DISCONNECTED);
|
|
571
591
|
};
|
|
@@ -734,6 +754,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
734
754
|
const decryptedPacket = EncryptedPacketPayload.fromBinary(decryptedData.payload);
|
|
735
755
|
const newDp = new DataPacket({
|
|
736
756
|
value: decryptedPacket.value,
|
|
757
|
+
participantIdentity: dp.participantIdentity,
|
|
758
|
+
participantSid: dp.participantSid,
|
|
737
759
|
});
|
|
738
760
|
if (newDp.value?.case === 'user') {
|
|
739
761
|
// compatibility
|
|
@@ -1478,8 +1500,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1478
1500
|
this.log.warn('sync state cannot be sent without peer connection setup', this.logContext);
|
|
1479
1501
|
return;
|
|
1480
1502
|
}
|
|
1481
|
-
const
|
|
1482
|
-
const
|
|
1503
|
+
const previousPublisherOffer = this.pcManager.publisher.getLocalDescription();
|
|
1504
|
+
const previousPublisherAnswer = this.pcManager.publisher.getRemoteDescription();
|
|
1505
|
+
const previousSubscriberOffer = this.pcManager.subscriber?.getRemoteDescription();
|
|
1506
|
+
const previousSubscriberAnswer = this.pcManager.subscriber?.getLocalDescription();
|
|
1483
1507
|
|
|
1484
1508
|
/* 1. autosubscribe on, so subscribed tracks = all tracks - unsub tracks,
|
|
1485
1509
|
in this case, we send unsub tracks, so server add all tracks to this
|
|
@@ -1501,18 +1525,32 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1501
1525
|
|
|
1502
1526
|
this.client.sendSyncState(
|
|
1503
1527
|
new SyncState({
|
|
1504
|
-
answer:
|
|
1505
|
-
?
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1528
|
+
answer: this.options.singlePeerConnection
|
|
1529
|
+
? previousPublisherAnswer
|
|
1530
|
+
? toProtoSessionDescription({
|
|
1531
|
+
sdp: previousPublisherAnswer.sdp,
|
|
1532
|
+
type: previousPublisherAnswer.type,
|
|
1533
|
+
})
|
|
1534
|
+
: undefined
|
|
1535
|
+
: previousSubscriberAnswer
|
|
1536
|
+
? toProtoSessionDescription({
|
|
1537
|
+
sdp: previousSubscriberAnswer.sdp,
|
|
1538
|
+
type: previousSubscriberAnswer.type,
|
|
1539
|
+
})
|
|
1540
|
+
: undefined,
|
|
1541
|
+
offer: this.options.singlePeerConnection
|
|
1542
|
+
? previousPublisherOffer
|
|
1543
|
+
? toProtoSessionDescription({
|
|
1544
|
+
sdp: previousPublisherOffer.sdp,
|
|
1545
|
+
type: previousPublisherOffer.type,
|
|
1546
|
+
})
|
|
1547
|
+
: undefined
|
|
1548
|
+
: previousSubscriberOffer
|
|
1549
|
+
? toProtoSessionDescription({
|
|
1550
|
+
sdp: previousSubscriberOffer.sdp,
|
|
1551
|
+
type: previousSubscriberOffer.type,
|
|
1552
|
+
})
|
|
1553
|
+
: undefined,
|
|
1516
1554
|
subscription: new UpdateSubscription({
|
|
1517
1555
|
trackSids,
|
|
1518
1556
|
subscribe: !autoSubscribe,
|
|
@@ -1609,7 +1647,7 @@ export type EngineEventCallbacks = {
|
|
|
1609
1647
|
activeSpeakersUpdate: (speakers: Array<SpeakerInfo>) => void;
|
|
1610
1648
|
dataPacketReceived: (packet: DataPacket, encryptionType: Encryption_Type) => void;
|
|
1611
1649
|
transcriptionReceived: (transcription: Transcription) => void;
|
|
1612
|
-
transportsCreated: (publisher: PCTransport, subscriber
|
|
1650
|
+
transportsCreated: (publisher: PCTransport, subscriber?: PCTransport) => void;
|
|
1613
1651
|
/** @internal */
|
|
1614
1652
|
trackSenderAdded: (track: Track, sender: RTCRtpSender) => void;
|
|
1615
1653
|
rtpVideoMapUpdate: (rtpMap: Map<number, VideoCodec>) => void;
|
package/src/room/Room.ts
CHANGED
|
@@ -370,6 +370,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
370
370
|
if (e2eeOptions) {
|
|
371
371
|
if ('e2eeManager' in e2eeOptions) {
|
|
372
372
|
this.e2eeManager = e2eeOptions.e2eeManager;
|
|
373
|
+
this.e2eeManager.isDataChannelEncryptionEnabled = dcEncryptionEnabled;
|
|
373
374
|
} else {
|
|
374
375
|
this.e2eeManager = new E2EEManager(e2eeOptions, dcEncryptionEnabled);
|
|
375
376
|
}
|
|
@@ -761,6 +762,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
761
762
|
maxRetries: connectOptions.maxRetries,
|
|
762
763
|
e2eeEnabled: !!this.e2eeManager,
|
|
763
764
|
websocketTimeout: connectOptions.websocketTimeout,
|
|
765
|
+
singlePeerConnection: roomOptions.singlePeerConnection,
|
|
764
766
|
},
|
|
765
767
|
abortController.signal,
|
|
766
768
|
);
|
|
@@ -939,8 +941,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
939
941
|
this.isResuming
|
|
940
942
|
) {
|
|
941
943
|
// try aborting pending connection attempt
|
|
942
|
-
|
|
943
|
-
this.
|
|
944
|
+
const msg = 'Abort connection attempt due to user initiated disconnect';
|
|
945
|
+
this.log.warn(msg, this.logContext);
|
|
946
|
+
this.abortController?.abort(msg);
|
|
944
947
|
// in case the abort controller didn't manage to cancel the connection attempt, reject the connect promise explicitly
|
|
945
948
|
this.connectFuture?.reject?.(
|
|
946
949
|
new ConnectionError('Client initiated disconnect', ConnectionErrorReason.Cancelled),
|
package/src/room/defaults.ts
CHANGED
|
@@ -42,6 +42,7 @@ export const roomOptionDefaults: InternalRoomOptions = {
|
|
|
42
42
|
reconnectPolicy: new DefaultReconnectPolicy(),
|
|
43
43
|
disconnectOnPageLeave: true,
|
|
44
44
|
webAudioMix: false,
|
|
45
|
+
singlePeerConnection: false,
|
|
45
46
|
} as const;
|
|
46
47
|
|
|
47
48
|
export const roomConnectOptionDefaults: InternalRoomConnectOptions = {
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
TokenSourceFixed,
|
|
12
12
|
type TokenSourceResponseObject,
|
|
13
13
|
} from './types';
|
|
14
|
-
import { decodeTokenPayload,
|
|
14
|
+
import { decodeTokenPayload, isResponseTokenValid } from './utils';
|
|
15
15
|
|
|
16
16
|
/** A TokenSourceCached is a TokenSource which caches the last {@link TokenSourceResponseObject} value and returns it
|
|
17
17
|
* until a) it expires or b) the {@link TokenSourceFetchOptions} provided to .fetch(...) change. */
|
|
@@ -56,7 +56,7 @@ abstract class TokenSourceCached extends TokenSourceConfigurable {
|
|
|
56
56
|
if (!this.cachedResponse) {
|
|
57
57
|
return false;
|
|
58
58
|
}
|
|
59
|
-
if (
|
|
59
|
+
if (!isResponseTokenValid(this.cachedResponse)) {
|
|
60
60
|
return false;
|
|
61
61
|
}
|
|
62
62
|
if (this.isSameAsCachedFetchOptions(fetchOptions)) {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { TokenSourceResponse } from '@livekit/protocol';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { decodeTokenPayload, isResponseTokenValid } from './utils';
|
|
4
|
+
|
|
5
|
+
// Test JWTs created for test purposes only.
|
|
6
|
+
// None of these actually auth against anything.
|
|
7
|
+
const TOKENS = {
|
|
8
|
+
// Nbf date set at 1234567890 seconds (Fri Feb 13 2009 23:31:30 GMT+0000)
|
|
9
|
+
// Exp date set at 9876543210 seconds (Fri Dec 22 2282 20:13:30 GMT+0000)
|
|
10
|
+
// A dummy roomConfig value is also set, with room_config.name = "test room name", and room_config.agents = [{"agentName": "test agent name","metadata":"test agent metadata"}]
|
|
11
|
+
VALID:
|
|
12
|
+
'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZXhwIjo5ODc2NTQzMjEwLCJuYmYiOjEyMzQ1Njc4OTAsImlhdCI6MTIzNDU2Nzg5MCwicm9vbUNvbmZpZyI6eyJuYW1lIjoidGVzdCByb29tIG5hbWUiLCJlbXB0eVRpbWVvdXQiOjAsImRlcGFydHVyZVRpbWVvdXQiOjAsIm1heFBhcnRpY2lwYW50cyI6MCwibWluUGxheW91dERlbGF5IjowLCJtYXhQbGF5b3V0RGVsYXkiOjAsInN5bmNTdHJlYW1zIjpmYWxzZSwiYWdlbnRzIjpbeyJhZ2VudE5hbWUiOiJ0ZXN0IGFnZW50IG5hbWUiLCJtZXRhZGF0YSI6InRlc3QgYWdlbnQgbWV0YWRhdGEifV0sIm1ldGFkYXRhIjoiIn19.EDetpHG8cSubaApzgWJaQrpCiSy9KDBlfCfVdIydbQ-_CHiNnXOK_f_mCJbTf9A-duT1jmvPOkLrkkWFT60XPQ',
|
|
13
|
+
|
|
14
|
+
// Nbf date set at 9876543210 seconds (Fri Dec 22 2282 20:13:30 GMT+0000)
|
|
15
|
+
// Exp date set at 9876543211 seconds (Fri Dec 22 2282 20:13:31 GMT+0000)
|
|
16
|
+
NBF_IN_FUTURE:
|
|
17
|
+
'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZXhwIjo5ODc2NTQzMjExLCJuYmYiOjk4NzY1NDMyMTAsImlhdCI6MTIzNDU2Nzg5MH0.DcMmdKrD76eJg7IUBZqoTRDvBaXtCcwtuE5h7IwVXhG_6nvgxN_ix30_AmLgnYhvhkN-x9dTRPoHg-CME72AbQ',
|
|
18
|
+
|
|
19
|
+
// Nbf date set at 1234567890 seconds (Fri Feb 13 2009 23:31:30 GMT+0000)
|
|
20
|
+
// Exp date set at 1234567891 seconds (Fri Feb 13 2009 23:31:31 GMT+0000)
|
|
21
|
+
EXP_IN_PAST:
|
|
22
|
+
'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZXhwIjoxMjM0NTY3ODkxLCJuYmYiOjEyMzQ1Njc4OTAsImlhdCI6MTIzNDU2Nzg5MH0.OYP1NITayotBYt0mioInLJmaIM0bHyyR-yG6iwKyQDzhoGha15qbsc7dOJlzz4za1iW5EzCgjc2_xGxqaSu5XA',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
describe('isResponseTokenValid', () => {
|
|
26
|
+
it('should find a valid jwt not expired', () => {
|
|
27
|
+
const isValid = isResponseTokenValid(
|
|
28
|
+
TokenSourceResponse.fromJson({
|
|
29
|
+
serverUrl: 'ws://localhost:7800',
|
|
30
|
+
participantToken: TOKENS.VALID,
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
expect(isValid).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
it('should find a long ago expired jwt as expired', () => {
|
|
36
|
+
const isValid = isResponseTokenValid(
|
|
37
|
+
TokenSourceResponse.fromJson({
|
|
38
|
+
serverUrl: 'ws://localhost:7800',
|
|
39
|
+
participantToken: TOKENS.EXP_IN_PAST,
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
expect(isValid).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
it('should find a jwt that has not become active yet as expired', () => {
|
|
45
|
+
const isValid = isResponseTokenValid(
|
|
46
|
+
TokenSourceResponse.fromJson({
|
|
47
|
+
serverUrl: 'ws://localhost:7800',
|
|
48
|
+
participantToken: TOKENS.NBF_IN_FUTURE,
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
expect(isValid).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('decodeTokenPayload', () => {
|
|
56
|
+
it('should extract roomconfig metadata from a token', () => {
|
|
57
|
+
const payload = decodeTokenPayload(TOKENS.VALID);
|
|
58
|
+
expect(payload.roomConfig?.name).toBe('test room name');
|
|
59
|
+
expect(payload.roomConfig?.agents).toHaveLength(1);
|
|
60
|
+
expect(payload.roomConfig?.agents![0].agentName).toBe('test agent name');
|
|
61
|
+
expect(payload.roomConfig?.agents![0].metadata).toBe('test agent metadata');
|
|
62
|
+
});
|
|
63
|
+
});
|