livekit-client 0.15.0 → 0.15.4
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/.gitmodules +3 -0
- package/README.md +21 -4
- package/dist/api/SignalClient.d.ts +11 -2
- package/dist/api/SignalClient.js +92 -25
- package/dist/api/SignalClient.js.map +1 -1
- package/dist/connect.js +3 -0
- package/dist/connect.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/logger.js +1 -0
- package/dist/logger.js.map +1 -1
- package/dist/options.d.ts +28 -14
- package/dist/proto/livekit_models.d.ts +48 -0
- package/dist/proto/livekit_models.js +367 -5
- package/dist/proto/livekit_models.js.map +1 -1
- package/dist/proto/livekit_rtc.d.ts +100 -1
- package/dist/proto/livekit_rtc.js +745 -3
- package/dist/proto/livekit_rtc.js.map +1 -1
- package/dist/room/PCTransport.js +4 -0
- package/dist/room/PCTransport.js.map +1 -1
- package/dist/room/RTCEngine.d.ts +4 -0
- package/dist/room/RTCEngine.js +73 -34
- package/dist/room/RTCEngine.js.map +1 -1
- package/dist/room/Room.d.ts +15 -0
- package/dist/room/Room.js +172 -59
- package/dist/room/Room.js.map +1 -1
- package/dist/room/events.d.ts +60 -24
- package/dist/room/events.js +58 -22
- package/dist/room/events.js.map +1 -1
- package/dist/room/participant/LocalParticipant.d.ts +26 -2
- package/dist/room/participant/LocalParticipant.js +69 -21
- package/dist/room/participant/LocalParticipant.js.map +1 -1
- package/dist/room/participant/Participant.d.ts +3 -1
- package/dist/room/participant/Participant.js +1 -0
- package/dist/room/participant/Participant.js.map +1 -1
- package/dist/room/participant/ParticipantTrackPermission.d.ts +19 -0
- package/dist/room/participant/ParticipantTrackPermission.js +16 -0
- package/dist/room/participant/ParticipantTrackPermission.js.map +1 -0
- package/dist/room/participant/RemoteParticipant.d.ts +2 -2
- package/dist/room/participant/RemoteParticipant.js +9 -15
- package/dist/room/participant/RemoteParticipant.js.map +1 -1
- package/dist/room/participant/publishUtils.d.ts +1 -1
- package/dist/room/participant/publishUtils.js +4 -4
- package/dist/room/participant/publishUtils.js.map +1 -1
- package/dist/room/participant/publishUtils.test.js +10 -1
- package/dist/room/participant/publishUtils.test.js.map +1 -1
- package/dist/room/stats.d.ts +21 -6
- package/dist/room/stats.js +22 -1
- package/dist/room/stats.js.map +1 -1
- package/dist/room/track/LocalAudioTrack.d.ts +5 -1
- package/dist/room/track/LocalAudioTrack.js +45 -1
- package/dist/room/track/LocalAudioTrack.js.map +1 -1
- package/dist/room/track/LocalTrack.js +1 -1
- package/dist/room/track/LocalTrack.js.map +1 -1
- package/dist/room/track/LocalTrackPublication.d.ts +3 -1
- package/dist/room/track/LocalTrackPublication.js +15 -5
- package/dist/room/track/LocalTrackPublication.js.map +1 -1
- package/dist/room/track/LocalVideoTrack.d.ts +8 -1
- package/dist/room/track/LocalVideoTrack.js +117 -52
- package/dist/room/track/LocalVideoTrack.js.map +1 -1
- package/dist/room/track/RemoteAudioTrack.d.ts +6 -8
- package/dist/room/track/RemoteAudioTrack.js +55 -19
- package/dist/room/track/RemoteAudioTrack.js.map +1 -1
- package/dist/room/track/RemoteTrack.d.ts +14 -0
- package/dist/room/track/RemoteTrack.js +47 -0
- package/dist/room/track/RemoteTrack.js.map +1 -0
- package/dist/room/track/RemoteTrackPublication.d.ts +10 -2
- package/dist/room/track/RemoteTrackPublication.js +49 -16
- package/dist/room/track/RemoteTrackPublication.js.map +1 -1
- package/dist/room/track/RemoteVideoTrack.d.ts +7 -7
- package/dist/room/track/RemoteVideoTrack.js +66 -22
- package/dist/room/track/RemoteVideoTrack.js.map +1 -1
- package/dist/room/track/Track.d.ts +12 -0
- package/dist/room/track/Track.js +33 -0
- package/dist/room/track/Track.js.map +1 -1
- package/dist/room/track/TrackPublication.d.ts +14 -1
- package/dist/room/track/TrackPublication.js +24 -7
- package/dist/room/track/TrackPublication.js.map +1 -1
- package/dist/room/track/create.d.ts +23 -0
- package/dist/room/track/create.js +130 -0
- package/dist/room/track/create.js.map +1 -0
- package/dist/room/track/defaults.d.ts +4 -0
- package/dist/room/track/defaults.js +21 -0
- package/dist/room/track/defaults.js.map +1 -0
- package/dist/room/utils.d.ts +3 -1
- package/dist/room/utils.js +36 -6
- package/dist/room/utils.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -3
- package/src/api/SignalClient.ts +434 -0
- package/src/connect.ts +100 -0
- package/src/index.ts +47 -0
- package/src/logger.ts +22 -0
- package/src/options.ts +152 -0
- package/src/proto/livekit_models.ts +1863 -0
- package/src/proto/livekit_rtc.ts +3401 -0
- package/src/room/DeviceManager.ts +57 -0
- package/src/room/PCTransport.ts +86 -0
- package/src/room/RTCEngine.ts +484 -0
- package/src/room/Room.ts +785 -0
- package/src/room/errors.ts +65 -0
- package/src/room/events.ts +396 -0
- package/src/room/participant/LocalParticipant.ts +685 -0
- package/src/room/participant/Participant.ts +214 -0
- package/src/room/participant/ParticipantTrackPermission.ts +32 -0
- package/src/room/participant/RemoteParticipant.ts +238 -0
- package/src/room/participant/publishUtils.test.ts +105 -0
- package/src/room/participant/publishUtils.ts +180 -0
- package/src/room/stats.ts +130 -0
- package/src/room/track/LocalAudioTrack.ts +112 -0
- package/src/room/track/LocalTrack.ts +124 -0
- package/src/room/track/LocalTrackPublication.ts +63 -0
- package/src/room/track/LocalVideoTrack.test.ts +70 -0
- package/src/room/track/LocalVideoTrack.ts +416 -0
- package/src/room/track/RemoteAudioTrack.ts +58 -0
- package/src/room/track/RemoteTrack.ts +59 -0
- package/src/room/track/RemoteTrackPublication.ts +192 -0
- package/src/room/track/RemoteVideoTrack.ts +213 -0
- package/src/room/track/Track.ts +301 -0
- package/src/room/track/TrackPublication.ts +120 -0
- package/src/room/track/create.ts +120 -0
- package/src/room/track/defaults.ts +23 -0
- package/src/room/track/options.ts +229 -0
- package/src/room/track/types.ts +8 -0
- package/src/room/track/utils.test.ts +93 -0
- package/src/room/track/utils.ts +76 -0
- package/src/room/utils.ts +74 -0
- package/src/version.ts +2 -0
- package/.github/workflows/publish.yaml +0 -55
- package/.github/workflows/test.yaml +0 -36
- package/example/index.html +0 -237
- package/example/sample.ts +0 -575
- package/example/styles.css +0 -144
- package/example/webpack.config.js +0 -33
@@ -0,0 +1,57 @@
|
|
1
|
+
const defaultId = 'default';
|
2
|
+
|
3
|
+
export default class DeviceManager {
|
4
|
+
private static instance?: DeviceManager;
|
5
|
+
|
6
|
+
static mediaDeviceKinds: MediaDeviceKind[] = [
|
7
|
+
'audioinput',
|
8
|
+
'audiooutput',
|
9
|
+
'videoinput',
|
10
|
+
];
|
11
|
+
|
12
|
+
static getInstance(): DeviceManager {
|
13
|
+
if (this.instance === undefined) {
|
14
|
+
this.instance = new DeviceManager();
|
15
|
+
}
|
16
|
+
return this.instance;
|
17
|
+
}
|
18
|
+
|
19
|
+
async getDevices(kind: MediaDeviceKind): Promise<MediaDeviceInfo[]> {
|
20
|
+
let devices = await navigator.mediaDevices.enumerateDevices();
|
21
|
+
devices = devices.filter((device) => device.kind === kind);
|
22
|
+
// Chrome returns 'default' devices, we would filter them out, but put the default
|
23
|
+
// device at first
|
24
|
+
// we would only do this if there are more than 1 device though
|
25
|
+
if (devices.length > 1 && devices[0].deviceId === defaultId) {
|
26
|
+
// find another device with matching group id, and move that to 0
|
27
|
+
const defaultDevice = devices[0];
|
28
|
+
for (let i = 1; i < devices.length; i += 1) {
|
29
|
+
if (devices[i].groupId === defaultDevice.groupId) {
|
30
|
+
const temp = devices[0];
|
31
|
+
devices[0] = devices[i];
|
32
|
+
devices[i] = temp;
|
33
|
+
break;
|
34
|
+
}
|
35
|
+
}
|
36
|
+
return devices.filter((device) => device !== defaultDevice);
|
37
|
+
}
|
38
|
+
|
39
|
+
return devices;
|
40
|
+
}
|
41
|
+
|
42
|
+
async normalizeDeviceId(
|
43
|
+
kind: MediaDeviceKind, deviceId?: string, groupId?: string,
|
44
|
+
): Promise<string | undefined> {
|
45
|
+
if (deviceId !== defaultId) {
|
46
|
+
return deviceId;
|
47
|
+
}
|
48
|
+
|
49
|
+
// resolve actual device id if it's 'default': Chrome returns it when no
|
50
|
+
// device has been chosen
|
51
|
+
const devices = await this.getDevices(kind);
|
52
|
+
|
53
|
+
const device = devices.find((d) => d.groupId === groupId && d.deviceId !== defaultId);
|
54
|
+
|
55
|
+
return device?.deviceId;
|
56
|
+
}
|
57
|
+
}
|
@@ -0,0 +1,86 @@
|
|
1
|
+
import { debounce } from 'ts-debounce';
|
2
|
+
import log from '../logger';
|
3
|
+
|
4
|
+
/** @internal */
|
5
|
+
export default class PCTransport {
|
6
|
+
pc: RTCPeerConnection;
|
7
|
+
|
8
|
+
pendingCandidates: RTCIceCandidateInit[] = [];
|
9
|
+
|
10
|
+
restartingIce: boolean = false;
|
11
|
+
|
12
|
+
renegotiate: boolean = false;
|
13
|
+
|
14
|
+
onOffer?: (offer: RTCSessionDescriptionInit) => void;
|
15
|
+
|
16
|
+
constructor(config?: RTCConfiguration) {
|
17
|
+
this.pc = new RTCPeerConnection(config);
|
18
|
+
}
|
19
|
+
|
20
|
+
get isICEConnected(): boolean {
|
21
|
+
return this.pc.iceConnectionState === 'connected' || this.pc.iceConnectionState === 'completed';
|
22
|
+
}
|
23
|
+
|
24
|
+
async addIceCandidate(candidate: RTCIceCandidateInit): Promise<void> {
|
25
|
+
if (this.pc.remoteDescription && !this.restartingIce) {
|
26
|
+
return this.pc.addIceCandidate(candidate);
|
27
|
+
}
|
28
|
+
this.pendingCandidates.push(candidate);
|
29
|
+
}
|
30
|
+
|
31
|
+
async setRemoteDescription(sd: RTCSessionDescriptionInit): Promise<void> {
|
32
|
+
await this.pc.setRemoteDescription(sd);
|
33
|
+
|
34
|
+
this.pendingCandidates.forEach((candidate) => {
|
35
|
+
this.pc.addIceCandidate(candidate);
|
36
|
+
});
|
37
|
+
this.pendingCandidates = [];
|
38
|
+
this.restartingIce = false;
|
39
|
+
|
40
|
+
if (this.renegotiate) {
|
41
|
+
this.renegotiate = false;
|
42
|
+
this.createAndSendOffer();
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
// debounced negotiate interface
|
47
|
+
negotiate = debounce(() => { this.createAndSendOffer(); }, 100);
|
48
|
+
|
49
|
+
async createAndSendOffer(options?: RTCOfferOptions) {
|
50
|
+
if (this.onOffer === undefined) {
|
51
|
+
return;
|
52
|
+
}
|
53
|
+
|
54
|
+
if (options?.iceRestart) {
|
55
|
+
log.debug('restarting ICE');
|
56
|
+
this.restartingIce = true;
|
57
|
+
}
|
58
|
+
|
59
|
+
if (this.pc.signalingState === 'have-local-offer') {
|
60
|
+
// we're waiting for the peer to accept our offer, so we'll just wait
|
61
|
+
// the only exception to this is when ICE restart is needed
|
62
|
+
const currentSD = this.pc.remoteDescription;
|
63
|
+
if (options?.iceRestart && currentSD) {
|
64
|
+
// TODO: handle when ICE restart is needed but we don't have a remote description
|
65
|
+
// the best thing to do is to recreate the peerconnection
|
66
|
+
await this.pc.setRemoteDescription(currentSD);
|
67
|
+
} else {
|
68
|
+
this.renegotiate = true;
|
69
|
+
return;
|
70
|
+
}
|
71
|
+
} else if (this.pc.signalingState === 'closed') {
|
72
|
+
log.warn('could not createOffer with closed peer connection');
|
73
|
+
return;
|
74
|
+
}
|
75
|
+
|
76
|
+
// actually negotiate
|
77
|
+
log.debug('starting to negotiate');
|
78
|
+
const offer = await this.pc.createOffer(options);
|
79
|
+
await this.pc.setLocalDescription(offer);
|
80
|
+
this.onOffer(offer);
|
81
|
+
}
|
82
|
+
|
83
|
+
close() {
|
84
|
+
this.pc.close();
|
85
|
+
}
|
86
|
+
}
|
@@ -0,0 +1,484 @@
|
|
1
|
+
import { EventEmitter } from 'events';
|
2
|
+
import { SignalClient, SignalOptions } from '../api/SignalClient';
|
3
|
+
import log from '../logger';
|
4
|
+
import { DataPacket, DataPacket_Kind, TrackInfo } from '../proto/livekit_models';
|
5
|
+
import {
|
6
|
+
AddTrackRequest, JoinResponse,
|
7
|
+
SignalTarget,
|
8
|
+
TrackPublishedResponse,
|
9
|
+
} from '../proto/livekit_rtc';
|
10
|
+
import { ConnectionError, TrackInvalidError, UnexpectedConnectionState } from './errors';
|
11
|
+
import { EngineEvent } from './events';
|
12
|
+
import PCTransport from './PCTransport';
|
13
|
+
import { sleep } from './utils';
|
14
|
+
|
15
|
+
const lossyDataChannel = '_lossy';
|
16
|
+
const reliableDataChannel = '_reliable';
|
17
|
+
const maxReconnectRetries = 5;
|
18
|
+
export const maxICEConnectTimeout = 5 * 1000;
|
19
|
+
|
20
|
+
/** @internal */
|
21
|
+
export default class RTCEngine extends EventEmitter {
|
22
|
+
publisher?: PCTransport;
|
23
|
+
|
24
|
+
subscriber?: PCTransport;
|
25
|
+
|
26
|
+
client: SignalClient;
|
27
|
+
|
28
|
+
rtcConfig: RTCConfiguration = {};
|
29
|
+
|
30
|
+
private lossyDC?: RTCDataChannel;
|
31
|
+
|
32
|
+
// @ts-ignore noUnusedLocals
|
33
|
+
private lossyDCSub?: RTCDataChannel;
|
34
|
+
|
35
|
+
private reliableDC?: RTCDataChannel;
|
36
|
+
|
37
|
+
// @ts-ignore noUnusedLocals
|
38
|
+
private reliableDCSub?: RTCDataChannel;
|
39
|
+
|
40
|
+
private subscriberPrimary: boolean = false;
|
41
|
+
|
42
|
+
private iceConnected: boolean = false;
|
43
|
+
|
44
|
+
private isClosed: boolean = true;
|
45
|
+
|
46
|
+
private pendingTrackResolvers: { [key: string]: (info: TrackInfo) => void } = {};
|
47
|
+
|
48
|
+
// true if publisher connection has already been established.
|
49
|
+
// this is helpful to know if we need to restart ICE on the publisher connection
|
50
|
+
private hasPublished: boolean = false;
|
51
|
+
|
52
|
+
// keep join info around for reconnect
|
53
|
+
private url?: string;
|
54
|
+
|
55
|
+
private token?: string;
|
56
|
+
|
57
|
+
private reconnectAttempts: number = 0;
|
58
|
+
|
59
|
+
private connectedServerAddr?: string;
|
60
|
+
|
61
|
+
constructor() {
|
62
|
+
super();
|
63
|
+
this.client = new SignalClient();
|
64
|
+
}
|
65
|
+
|
66
|
+
async join(url: string, token: string, opts?: SignalOptions): Promise<JoinResponse> {
|
67
|
+
this.url = url;
|
68
|
+
this.token = token;
|
69
|
+
|
70
|
+
const joinResponse = await this.client.join(url, token, opts);
|
71
|
+
this.emit(EngineEvent.SignalConnected);
|
72
|
+
this.isClosed = false;
|
73
|
+
|
74
|
+
this.subscriberPrimary = joinResponse.subscriberPrimary;
|
75
|
+
if (!this.publisher) {
|
76
|
+
this.configure(joinResponse);
|
77
|
+
}
|
78
|
+
|
79
|
+
// create offer
|
80
|
+
if (!this.subscriberPrimary) {
|
81
|
+
this.negotiate();
|
82
|
+
}
|
83
|
+
|
84
|
+
return joinResponse;
|
85
|
+
}
|
86
|
+
|
87
|
+
close() {
|
88
|
+
this.isClosed = true;
|
89
|
+
|
90
|
+
this.removeAllListeners();
|
91
|
+
if (this.publisher && this.publisher.pc.signalingState !== 'closed') {
|
92
|
+
this.publisher.pc.getSenders().forEach((sender) => {
|
93
|
+
try {
|
94
|
+
this.publisher?.pc.removeTrack(sender);
|
95
|
+
} catch (e) {
|
96
|
+
log.warn('could not removeTrack', e);
|
97
|
+
}
|
98
|
+
});
|
99
|
+
this.publisher.close();
|
100
|
+
this.publisher = undefined;
|
101
|
+
}
|
102
|
+
if (this.subscriber) {
|
103
|
+
this.subscriber.close();
|
104
|
+
this.subscriber = undefined;
|
105
|
+
}
|
106
|
+
this.client.close();
|
107
|
+
}
|
108
|
+
|
109
|
+
addTrack(req: AddTrackRequest): Promise<TrackInfo> {
|
110
|
+
if (this.pendingTrackResolvers[req.cid]) {
|
111
|
+
throw new TrackInvalidError(
|
112
|
+
'a track with the same ID has already been published',
|
113
|
+
);
|
114
|
+
}
|
115
|
+
return new Promise<TrackInfo>((resolve) => {
|
116
|
+
this.pendingTrackResolvers[req.cid] = resolve;
|
117
|
+
this.client.sendAddTrack(req);
|
118
|
+
});
|
119
|
+
}
|
120
|
+
|
121
|
+
updateMuteStatus(trackSid: string, muted: boolean) {
|
122
|
+
this.client.sendMuteTrack(trackSid, muted);
|
123
|
+
}
|
124
|
+
|
125
|
+
get dataSubscriberReadyState(): string | undefined {
|
126
|
+
return this.reliableDCSub?.readyState;
|
127
|
+
}
|
128
|
+
|
129
|
+
get connectedServerAddress(): string | undefined {
|
130
|
+
return this.connectedServerAddr;
|
131
|
+
}
|
132
|
+
|
133
|
+
private configure(joinResponse: JoinResponse) {
|
134
|
+
// already configured
|
135
|
+
if (this.publisher || this.subscriber) {
|
136
|
+
return;
|
137
|
+
}
|
138
|
+
|
139
|
+
// update ICE servers before creating PeerConnection
|
140
|
+
if (joinResponse.iceServers && !this.rtcConfig.iceServers) {
|
141
|
+
const rtcIceServers: RTCIceServer[] = [];
|
142
|
+
joinResponse.iceServers.forEach((iceServer) => {
|
143
|
+
const rtcIceServer: RTCIceServer = {
|
144
|
+
urls: iceServer.urls,
|
145
|
+
};
|
146
|
+
if (iceServer.username) rtcIceServer.username = iceServer.username;
|
147
|
+
if (iceServer.credential) { rtcIceServer.credential = iceServer.credential; }
|
148
|
+
rtcIceServers.push(rtcIceServer);
|
149
|
+
});
|
150
|
+
this.rtcConfig.iceServers = rtcIceServers;
|
151
|
+
}
|
152
|
+
|
153
|
+
this.publisher = new PCTransport(this.rtcConfig);
|
154
|
+
this.subscriber = new PCTransport(this.rtcConfig);
|
155
|
+
|
156
|
+
this.publisher.pc.onicecandidate = (ev) => {
|
157
|
+
if (!ev.candidate) return;
|
158
|
+
log.trace('adding ICE candidate for peer', ev.candidate);
|
159
|
+
this.client.sendIceCandidate(ev.candidate, SignalTarget.PUBLISHER);
|
160
|
+
};
|
161
|
+
|
162
|
+
this.subscriber.pc.onicecandidate = (ev) => {
|
163
|
+
if (!ev.candidate) return;
|
164
|
+
this.client.sendIceCandidate(ev.candidate, SignalTarget.SUBSCRIBER);
|
165
|
+
};
|
166
|
+
|
167
|
+
this.publisher.onOffer = (offer) => {
|
168
|
+
this.client.sendOffer(offer);
|
169
|
+
};
|
170
|
+
|
171
|
+
let primaryPC = this.publisher.pc;
|
172
|
+
if (joinResponse.subscriberPrimary) {
|
173
|
+
primaryPC = this.subscriber.pc;
|
174
|
+
// in subscriber primary mode, server side opens sub data channels.
|
175
|
+
this.subscriber.pc.ondatachannel = this.handleDataChannel;
|
176
|
+
}
|
177
|
+
primaryPC.oniceconnectionstatechange = () => {
|
178
|
+
if (primaryPC.iceConnectionState === 'connected') {
|
179
|
+
log.trace('ICE connected');
|
180
|
+
if (!this.iceConnected) {
|
181
|
+
this.iceConnected = true;
|
182
|
+
this.emit(EngineEvent.Connected);
|
183
|
+
}
|
184
|
+
getConnectedAddress(primaryPC).then((v) => {
|
185
|
+
this.connectedServerAddr = v;
|
186
|
+
});
|
187
|
+
} else if (primaryPC.iceConnectionState === 'failed') {
|
188
|
+
// on Safari, PeerConnection will switch to 'disconnected' during renegotiation
|
189
|
+
log.trace('ICE disconnected');
|
190
|
+
if (this.iceConnected) {
|
191
|
+
this.iceConnected = false;
|
192
|
+
|
193
|
+
this.handleDisconnect('peerconnection');
|
194
|
+
}
|
195
|
+
}
|
196
|
+
};
|
197
|
+
|
198
|
+
this.subscriber.pc.ontrack = (ev: RTCTrackEvent) => {
|
199
|
+
this.emit(EngineEvent.MediaTrackAdded, ev.track, ev.streams[0], ev.receiver);
|
200
|
+
};
|
201
|
+
|
202
|
+
// data channels
|
203
|
+
this.lossyDC = this.publisher.pc.createDataChannel(lossyDataChannel, {
|
204
|
+
// will drop older packets that arrive
|
205
|
+
ordered: true,
|
206
|
+
maxRetransmits: 0,
|
207
|
+
});
|
208
|
+
this.reliableDC = this.publisher.pc.createDataChannel(reliableDataChannel, {
|
209
|
+
ordered: true,
|
210
|
+
});
|
211
|
+
|
212
|
+
// also handle messages over the pub channel, for backwards compatibility
|
213
|
+
this.lossyDC.onmessage = this.handleDataMessage;
|
214
|
+
this.reliableDC.onmessage = this.handleDataMessage;
|
215
|
+
|
216
|
+
// configure signaling client
|
217
|
+
this.client.onAnswer = async (sd) => {
|
218
|
+
if (!this.publisher) {
|
219
|
+
return;
|
220
|
+
}
|
221
|
+
log.debug(
|
222
|
+
'received server answer',
|
223
|
+
sd.type,
|
224
|
+
this.publisher.pc.signalingState,
|
225
|
+
);
|
226
|
+
await this.publisher.setRemoteDescription(sd);
|
227
|
+
};
|
228
|
+
|
229
|
+
// add candidate on trickle
|
230
|
+
this.client.onTrickle = (candidate, target) => {
|
231
|
+
if (!this.publisher || !this.subscriber) {
|
232
|
+
return;
|
233
|
+
}
|
234
|
+
log.trace('got ICE candidate from peer', candidate, target);
|
235
|
+
if (target === SignalTarget.PUBLISHER) {
|
236
|
+
this.publisher.addIceCandidate(candidate);
|
237
|
+
} else {
|
238
|
+
this.subscriber.addIceCandidate(candidate);
|
239
|
+
}
|
240
|
+
};
|
241
|
+
|
242
|
+
// when server creates an offer for the client
|
243
|
+
this.client.onOffer = async (sd) => {
|
244
|
+
if (!this.subscriber) {
|
245
|
+
return;
|
246
|
+
}
|
247
|
+
log.debug(
|
248
|
+
'received server offer',
|
249
|
+
sd.type,
|
250
|
+
this.subscriber.pc.signalingState,
|
251
|
+
);
|
252
|
+
await this.subscriber.setRemoteDescription(sd);
|
253
|
+
|
254
|
+
// answer the offer
|
255
|
+
const answer = await this.subscriber.pc.createAnswer();
|
256
|
+
await this.subscriber.pc.setLocalDescription(answer);
|
257
|
+
this.client.sendAnswer(answer);
|
258
|
+
};
|
259
|
+
|
260
|
+
this.client.onLocalTrackPublished = (res: TrackPublishedResponse) => {
|
261
|
+
log.debug('received trackPublishedResponse', res);
|
262
|
+
const resolve = this.pendingTrackResolvers[res.cid];
|
263
|
+
if (!resolve) {
|
264
|
+
log.error('missing track resolver for ', res.cid);
|
265
|
+
return;
|
266
|
+
}
|
267
|
+
delete this.pendingTrackResolvers[res.cid];
|
268
|
+
resolve(res.track!);
|
269
|
+
};
|
270
|
+
|
271
|
+
this.client.onClose = () => {
|
272
|
+
this.handleDisconnect('signal');
|
273
|
+
};
|
274
|
+
|
275
|
+
this.client.onLeave = () => {
|
276
|
+
this.emit(EngineEvent.Disconnected);
|
277
|
+
this.close();
|
278
|
+
};
|
279
|
+
}
|
280
|
+
|
281
|
+
private handleDataChannel = async ({ channel }: RTCDataChannelEvent) => {
|
282
|
+
if (!channel) {
|
283
|
+
return;
|
284
|
+
}
|
285
|
+
if (channel.label === reliableDataChannel) {
|
286
|
+
this.reliableDCSub = channel;
|
287
|
+
} else if (channel.label === lossyDataChannel) {
|
288
|
+
this.lossyDCSub = channel;
|
289
|
+
} else {
|
290
|
+
return;
|
291
|
+
}
|
292
|
+
channel.onmessage = this.handleDataMessage;
|
293
|
+
};
|
294
|
+
|
295
|
+
private handleDataMessage = async (message: MessageEvent) => {
|
296
|
+
// decode
|
297
|
+
let buffer: ArrayBuffer | undefined;
|
298
|
+
if (message.data instanceof ArrayBuffer) {
|
299
|
+
buffer = message.data;
|
300
|
+
} else if (message.data instanceof Blob) {
|
301
|
+
buffer = await message.data.arrayBuffer();
|
302
|
+
} else {
|
303
|
+
log.error('unsupported data type', message.data);
|
304
|
+
return;
|
305
|
+
}
|
306
|
+
const dp = DataPacket.decode(new Uint8Array(buffer));
|
307
|
+
if (dp.speaker) {
|
308
|
+
// dispatch speaker updates
|
309
|
+
this.emit(EngineEvent.ActiveSpeakersUpdate, dp.speaker.speakers);
|
310
|
+
} else if (dp.user) {
|
311
|
+
this.emit(EngineEvent.DataPacketReceived, dp.user, dp.kind);
|
312
|
+
}
|
313
|
+
};
|
314
|
+
|
315
|
+
// websocket reconnect behavior. if websocket is interrupted, and the PeerConnection
|
316
|
+
// continues to work, we can reconnect to websocket to continue the session
|
317
|
+
// after a number of retries, we'll close and give up permanently
|
318
|
+
private handleDisconnect = (connection: string) => {
|
319
|
+
if (this.isClosed) {
|
320
|
+
return;
|
321
|
+
}
|
322
|
+
log.debug(`${connection} disconnected`);
|
323
|
+
if (this.reconnectAttempts >= maxReconnectRetries) {
|
324
|
+
log.info(
|
325
|
+
'could not connect to signal after',
|
326
|
+
maxReconnectRetries,
|
327
|
+
'attempts. giving up',
|
328
|
+
);
|
329
|
+
this.emit(EngineEvent.Disconnected);
|
330
|
+
this.close();
|
331
|
+
return;
|
332
|
+
}
|
333
|
+
|
334
|
+
const delay = (this.reconnectAttempts * this.reconnectAttempts) * 300;
|
335
|
+
setTimeout(() => {
|
336
|
+
this.reconnect()
|
337
|
+
.then(() => {
|
338
|
+
this.reconnectAttempts = 0;
|
339
|
+
})
|
340
|
+
.catch(this.handleDisconnect);
|
341
|
+
}, delay);
|
342
|
+
};
|
343
|
+
|
344
|
+
private async reconnect(): Promise<void> {
|
345
|
+
if (this.isClosed) {
|
346
|
+
return;
|
347
|
+
}
|
348
|
+
if (!this.url || !this.token) {
|
349
|
+
throw new ConnectionError('could not reconnect, url or token not saved');
|
350
|
+
}
|
351
|
+
log.info('reconnecting to signal connection, attempt', this.reconnectAttempts);
|
352
|
+
|
353
|
+
if (this.reconnectAttempts === 0) {
|
354
|
+
this.emit(EngineEvent.Reconnecting);
|
355
|
+
}
|
356
|
+
this.reconnectAttempts += 1;
|
357
|
+
|
358
|
+
await this.client.reconnect(this.url, this.token);
|
359
|
+
this.emit(EngineEvent.SignalConnected);
|
360
|
+
|
361
|
+
// trigger publisher reconnect
|
362
|
+
if (!this.publisher || !this.subscriber) {
|
363
|
+
throw new UnexpectedConnectionState('publisher and subscriber connections unset');
|
364
|
+
}
|
365
|
+
this.subscriber.restartingIce = true;
|
366
|
+
|
367
|
+
// only restart publisher if it's needed
|
368
|
+
if (this.hasPublished) {
|
369
|
+
await this.publisher.createAndSendOffer({ iceRestart: true });
|
370
|
+
}
|
371
|
+
|
372
|
+
const startTime = (new Date()).getTime();
|
373
|
+
|
374
|
+
while ((new Date()).getTime() - startTime < maxICEConnectTimeout * 2) {
|
375
|
+
if (this.iceConnected) {
|
376
|
+
// reconnect success
|
377
|
+
this.emit(EngineEvent.Reconnected);
|
378
|
+
return;
|
379
|
+
}
|
380
|
+
await sleep(100);
|
381
|
+
}
|
382
|
+
|
383
|
+
// have not reconnected, throw
|
384
|
+
throw new ConnectionError('could not establish ICE connection');
|
385
|
+
}
|
386
|
+
|
387
|
+
/* @internal */
|
388
|
+
async sendDataPacket(packet: DataPacket, kind: DataPacket_Kind) {
|
389
|
+
const msg = DataPacket.encode(packet).finish();
|
390
|
+
|
391
|
+
// make sure we do have a data connection
|
392
|
+
await this.ensurePublisherConnected(kind);
|
393
|
+
|
394
|
+
if (kind === DataPacket_Kind.LOSSY && this.lossyDC) {
|
395
|
+
this.lossyDC.send(msg);
|
396
|
+
} else if (kind === DataPacket_Kind.RELIABLE && this.reliableDC) {
|
397
|
+
this.reliableDC.send(msg);
|
398
|
+
}
|
399
|
+
}
|
400
|
+
|
401
|
+
private async ensurePublisherConnected(kind: DataPacket_Kind) {
|
402
|
+
if (!this.subscriberPrimary) {
|
403
|
+
return;
|
404
|
+
}
|
405
|
+
|
406
|
+
if (!this.publisher) {
|
407
|
+
throw new ConnectionError('publisher connection not set');
|
408
|
+
}
|
409
|
+
|
410
|
+
if (!this.publisher.isICEConnected && this.publisher.pc.iceConnectionState !== 'checking') {
|
411
|
+
// start negotiation
|
412
|
+
this.negotiate();
|
413
|
+
}
|
414
|
+
|
415
|
+
const targetChannel = this.dataChannelForKind(kind);
|
416
|
+
if (targetChannel?.readyState === 'open') {
|
417
|
+
return;
|
418
|
+
}
|
419
|
+
|
420
|
+
// wait until publisher ICE connected
|
421
|
+
const endTime = (new Date()).getTime() + maxICEConnectTimeout;
|
422
|
+
while ((new Date()).getTime() < endTime) {
|
423
|
+
if (this.publisher.isICEConnected && this.dataChannelForKind(kind)?.readyState === 'open') {
|
424
|
+
return;
|
425
|
+
}
|
426
|
+
await sleep(50);
|
427
|
+
}
|
428
|
+
|
429
|
+
throw new ConnectionError(`could not establish publisher connection, state ${this.publisher?.pc.iceConnectionState}`);
|
430
|
+
}
|
431
|
+
|
432
|
+
/** @internal */
|
433
|
+
negotiate() {
|
434
|
+
if (!this.publisher) {
|
435
|
+
return;
|
436
|
+
}
|
437
|
+
|
438
|
+
this.hasPublished = true;
|
439
|
+
|
440
|
+
this.publisher.negotiate();
|
441
|
+
}
|
442
|
+
|
443
|
+
private dataChannelForKind(kind: DataPacket_Kind): RTCDataChannel | undefined {
|
444
|
+
if (kind === DataPacket_Kind.LOSSY) {
|
445
|
+
return this.lossyDC;
|
446
|
+
} if (kind === DataPacket_Kind.RELIABLE) {
|
447
|
+
return this.reliableDC;
|
448
|
+
}
|
449
|
+
}
|
450
|
+
}
|
451
|
+
|
452
|
+
async function getConnectedAddress(pc: RTCPeerConnection): Promise<string | undefined> {
|
453
|
+
let selectedCandidatePairId = '';
|
454
|
+
const candidatePairs = new Map<string, RTCIceCandidatePairStats>();
|
455
|
+
// id -> candidate ip
|
456
|
+
const candidates = new Map<string, string>();
|
457
|
+
const stats: RTCStatsReport = await pc.getStats();
|
458
|
+
stats.forEach((v) => {
|
459
|
+
switch (v.type) {
|
460
|
+
case 'transport':
|
461
|
+
selectedCandidatePairId = v.selectedCandidatePairId;
|
462
|
+
break;
|
463
|
+
case 'candidate-pair':
|
464
|
+
if (selectedCandidatePairId === '' && v.selected) {
|
465
|
+
selectedCandidatePairId = v.id;
|
466
|
+
}
|
467
|
+
candidatePairs.set(v.id, v);
|
468
|
+
break;
|
469
|
+
case 'remote-candidate':
|
470
|
+
candidates.set(v.id, `${v.address}:${v.port}`);
|
471
|
+
break;
|
472
|
+
default:
|
473
|
+
}
|
474
|
+
});
|
475
|
+
|
476
|
+
if (selectedCandidatePairId === '') {
|
477
|
+
return undefined;
|
478
|
+
}
|
479
|
+
const selectedID = candidatePairs.get(selectedCandidatePairId)?.remoteCandidateId;
|
480
|
+
if (selectedID === undefined) {
|
481
|
+
return undefined;
|
482
|
+
}
|
483
|
+
return candidates.get(selectedID);
|
484
|
+
}
|