livekit-client 2.15.8 → 2.15.10
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 +589 -205
- 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 +769 -0
- package/src/api/SignalClient.ts +319 -162
- 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 +59 -17
- package/src/room/Room.ts +5 -2
- package/src/room/defaults.ts +1 -0
- package/src/room/participant/LocalParticipant.ts +2 -2
- 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
|
|
|
@@ -495,7 +503,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
495
503
|
if (!this.pcManager) {
|
|
496
504
|
return;
|
|
497
505
|
}
|
|
498
|
-
this.log.debug('received server answer', {
|
|
506
|
+
this.log.debug('received server answer', {
|
|
507
|
+
...this.logContext,
|
|
508
|
+
RTCSdpType: sd.type,
|
|
509
|
+
sdp: sd.sdp,
|
|
510
|
+
});
|
|
499
511
|
await this.pcManager.setPublisherAnswer(sd, offerId);
|
|
500
512
|
};
|
|
501
513
|
|
|
@@ -566,6 +578,18 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
566
578
|
this.emit(EngineEvent.RoomMoved, res);
|
|
567
579
|
};
|
|
568
580
|
|
|
581
|
+
this.client.onMediaSectionsRequirement = (requirement: MediaSectionsRequirement) => {
|
|
582
|
+
const transceiverInit: RTCRtpTransceiverInit = { direction: 'recvonly' };
|
|
583
|
+
for (let i: number = 0; i < requirement.numAudios; i++) {
|
|
584
|
+
this.pcManager?.addPublisherTransceiverOfKind('audio', transceiverInit);
|
|
585
|
+
}
|
|
586
|
+
for (let i: number = 0; i < requirement.numVideos; i++) {
|
|
587
|
+
this.pcManager?.addPublisherTransceiverOfKind('video', transceiverInit);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
this.negotiate();
|
|
591
|
+
};
|
|
592
|
+
|
|
569
593
|
this.client.onClose = () => {
|
|
570
594
|
this.handleDisconnect('signal', ReconnectReason.RR_SIGNAL_DISCONNECTED);
|
|
571
595
|
};
|
|
@@ -734,6 +758,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
734
758
|
const decryptedPacket = EncryptedPacketPayload.fromBinary(decryptedData.payload);
|
|
735
759
|
const newDp = new DataPacket({
|
|
736
760
|
value: decryptedPacket.value,
|
|
761
|
+
participantIdentity: dp.participantIdentity,
|
|
762
|
+
participantSid: dp.participantSid,
|
|
737
763
|
});
|
|
738
764
|
if (newDp.value?.case === 'user') {
|
|
739
765
|
// compatibility
|
|
@@ -1478,8 +1504,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1478
1504
|
this.log.warn('sync state cannot be sent without peer connection setup', this.logContext);
|
|
1479
1505
|
return;
|
|
1480
1506
|
}
|
|
1481
|
-
const
|
|
1482
|
-
const
|
|
1507
|
+
const previousPublisherOffer = this.pcManager.publisher.getLocalDescription();
|
|
1508
|
+
const previousPublisherAnswer = this.pcManager.publisher.getRemoteDescription();
|
|
1509
|
+
const previousSubscriberOffer = this.pcManager.subscriber?.getRemoteDescription();
|
|
1510
|
+
const previousSubscriberAnswer = this.pcManager.subscriber?.getLocalDescription();
|
|
1483
1511
|
|
|
1484
1512
|
/* 1. autosubscribe on, so subscribed tracks = all tracks - unsub tracks,
|
|
1485
1513
|
in this case, we send unsub tracks, so server add all tracks to this
|
|
@@ -1501,18 +1529,32 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1501
1529
|
|
|
1502
1530
|
this.client.sendSyncState(
|
|
1503
1531
|
new SyncState({
|
|
1504
|
-
answer:
|
|
1505
|
-
?
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1532
|
+
answer: this.options.singlePeerConnection
|
|
1533
|
+
? previousPublisherAnswer
|
|
1534
|
+
? toProtoSessionDescription({
|
|
1535
|
+
sdp: previousPublisherAnswer.sdp,
|
|
1536
|
+
type: previousPublisherAnswer.type,
|
|
1537
|
+
})
|
|
1538
|
+
: undefined
|
|
1539
|
+
: previousSubscriberAnswer
|
|
1540
|
+
? toProtoSessionDescription({
|
|
1541
|
+
sdp: previousSubscriberAnswer.sdp,
|
|
1542
|
+
type: previousSubscriberAnswer.type,
|
|
1543
|
+
})
|
|
1544
|
+
: undefined,
|
|
1545
|
+
offer: this.options.singlePeerConnection
|
|
1546
|
+
? previousPublisherOffer
|
|
1547
|
+
? toProtoSessionDescription({
|
|
1548
|
+
sdp: previousPublisherOffer.sdp,
|
|
1549
|
+
type: previousPublisherOffer.type,
|
|
1550
|
+
})
|
|
1551
|
+
: undefined
|
|
1552
|
+
: previousSubscriberOffer
|
|
1553
|
+
? toProtoSessionDescription({
|
|
1554
|
+
sdp: previousSubscriberOffer.sdp,
|
|
1555
|
+
type: previousSubscriberOffer.type,
|
|
1556
|
+
})
|
|
1557
|
+
: undefined,
|
|
1516
1558
|
subscription: new UpdateSubscription({
|
|
1517
1559
|
trackSids,
|
|
1518
1560
|
subscribe: !autoSubscribe,
|
|
@@ -1609,7 +1651,7 @@ export type EngineEventCallbacks = {
|
|
|
1609
1651
|
activeSpeakersUpdate: (speakers: Array<SpeakerInfo>) => void;
|
|
1610
1652
|
dataPacketReceived: (packet: DataPacket, encryptionType: Encryption_Type) => void;
|
|
1611
1653
|
transcriptionReceived: (transcription: Transcription) => void;
|
|
1612
|
-
transportsCreated: (publisher: PCTransport, subscriber
|
|
1654
|
+
transportsCreated: (publisher: PCTransport, subscriber?: PCTransport) => void;
|
|
1613
1655
|
/** @internal */
|
|
1614
1656
|
trackSenderAdded: (track: Track, sender: RTCRtpSender) => void;
|
|
1615
1657
|
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 = {
|
|
@@ -1771,9 +1771,9 @@ export default class LocalParticipant extends Participant {
|
|
|
1771
1771
|
destinationIdentity,
|
|
1772
1772
|
method,
|
|
1773
1773
|
payload,
|
|
1774
|
-
responseTimeout =
|
|
1774
|
+
responseTimeout = 15000,
|
|
1775
1775
|
}: PerformRpcParams): Promise<string> {
|
|
1776
|
-
const maxRoundTripLatency =
|
|
1776
|
+
const maxRoundTripLatency = 7000;
|
|
1777
1777
|
|
|
1778
1778
|
return new Promise(async (resolve, reject) => {
|
|
1779
1779
|
if (byteLength(payload) > MAX_PAYLOAD_BYTES) {
|
|
@@ -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)) {
|