@4players/odin-plugin-web 1.0.0-alpha.2 → 1.0.0-alpha.20
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/lib/common/msgpack.d.ts +2 -0
- package/lib/common/msgpack.js +117 -0
- package/{backend/lib → lib/plugin}/config.d.ts +5 -4
- package/lib/plugin/config.js +22 -0
- package/{backend/lib → lib/plugin}/connectionPool.d.ts +3 -3
- package/{backend/lib → lib/plugin}/connectionPool.js +32 -13
- package/lib/plugin/deviceManager.d.ts +12 -0
- package/lib/plugin/deviceManager.js +120 -0
- package/lib/plugin/h3/mainConnection.d.ts +31 -0
- package/lib/plugin/h3/mainConnection.js +195 -0
- package/{backend/lib → lib/plugin}/index.d.ts +30 -13
- package/lib/plugin/index.js +404 -0
- package/lib/plugin/mainConnection.d.ts +30 -0
- package/lib/plugin/mainConnection.js +21 -0
- package/{backend/lib/audioController.d.ts → lib/plugin/mediaController.d.ts} +12 -48
- package/lib/plugin/mediaController.js +215 -0
- package/lib/plugin/medias/audio-capture.d.ts +30 -0
- package/lib/plugin/medias/audio-capture.js +224 -0
- package/lib/plugin/medias/audio-playback.d.ts +13 -0
- package/lib/plugin/medias/audio-playback.js +40 -0
- package/lib/plugin/medias/video-capture.d.ts +15 -0
- package/lib/plugin/medias/video-capture.js +69 -0
- package/lib/plugin/medias/video-playback.d.ts +13 -0
- package/lib/plugin/medias/video-playback.js +20 -0
- package/{backend/lib → lib/plugin}/roomConnection.d.ts +9 -6
- package/{backend/lib → lib/plugin}/roomConnection.js +74 -53
- package/lib/plugin/rpcConnection.d.ts +57 -0
- package/{backend/lib → lib/plugin}/rpcConnection.js +129 -83
- package/lib/plugin/token.d.ts +13 -0
- package/{backend/lib → lib/plugin}/token.js +8 -9
- package/lib/plugin/webrtc/mainConnection.d.ts +53 -0
- package/lib/plugin/webrtc/mainConnection.js +455 -0
- package/lib/plugin/webrtc/websocket.d.ts +3 -0
- package/lib/plugin/webrtc/websocket.js +56 -0
- package/lib/worker/buffers.d.ts +42 -0
- package/lib/worker/buffers.js +79 -0
- package/{worker/lib/index.d.ts → lib/worker/common.d.ts} +7 -8
- package/lib/worker/common.js +7 -0
- package/lib/worker/config.d.ts +8 -0
- package/lib/worker/config.js +11 -0
- package/lib/worker/index.d.ts +3 -0
- package/lib/worker/index.js +1 -0
- package/lib/worker/memory.d.ts +9 -0
- package/lib/worker/memory.js +29 -0
- package/lib/worker/messages.d.ts +14 -0
- package/lib/worker/messages.js +15 -0
- package/lib/worker/packet.d.ts +11 -0
- package/lib/worker/packet.js +59 -0
- package/lib/worker/shared/capture.d.ts +27 -0
- package/lib/worker/shared/capture.js +63 -0
- package/lib/worker/shared/index.d.ts +3 -0
- package/lib/worker/shared/index.js +147 -0
- package/lib/worker/shared/messages.d.ts +67 -0
- package/lib/worker/shared/messages.js +64 -0
- package/lib/worker/shared/playback.d.ts +20 -0
- package/lib/worker/shared/playback.js +78 -0
- package/lib/worker/unshared/capture.d.ts +27 -0
- package/lib/worker/unshared/capture.js +66 -0
- package/lib/worker/unshared/index.d.ts +3 -0
- package/lib/worker/unshared/index.js +170 -0
- package/{worker/lib → lib/worker/unshared}/messages.d.ts +3 -15
- package/lib/worker/unshared/messages.js +80 -0
- package/lib/worker/unshared/playback.d.ts +22 -0
- package/lib/worker/unshared/playback.js +83 -0
- package/package.json +29 -26
- package/backend/lib/audioController.d.ts.map +0 -1
- package/backend/lib/audioController.js +0 -442
- package/backend/lib/audioController.js.map +0 -1
- package/backend/lib/config.d.ts.map +0 -1
- package/backend/lib/config.js +0 -11
- package/backend/lib/config.js.map +0 -1
- package/backend/lib/connectionPool.d.ts.map +0 -1
- package/backend/lib/connectionPool.js.map +0 -1
- package/backend/lib/index.d.ts.map +0 -1
- package/backend/lib/index.js +0 -293
- package/backend/lib/index.js.map +0 -1
- package/backend/lib/mainConnection.d.ts +0 -40
- package/backend/lib/mainConnection.d.ts.map +0 -1
- package/backend/lib/mainConnection.js +0 -294
- package/backend/lib/mainConnection.js.map +0 -1
- package/backend/lib/roomConnection.d.ts.map +0 -1
- package/backend/lib/roomConnection.js.map +0 -1
- package/backend/lib/rpcConnection.d.ts +0 -49
- package/backend/lib/rpcConnection.d.ts.map +0 -1
- package/backend/lib/rpcConnection.js.map +0 -1
- package/backend/lib/token.d.ts +0 -16
- package/backend/lib/token.d.ts.map +0 -1
- package/backend/lib/token.js.map +0 -1
- package/backend/package.json +0 -13
- package/wasm/lib/index.js +0 -19
- package/wasm/package.json +0 -14
- package/worker/lib/common.d.ts +0 -27
- package/worker/lib/index.js +0 -2
- package/worker/package.json +0 -13
- /package/{wasm/lib → lib/wasm}/index.d.ts +0 -0
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { MainCommandsRpc, MainNotificationsRpc, assert, extendUrl, failure, success, fail, } from '@4players/odin-common';
|
|
11
|
+
import { ConnectionStatus, } from '../mainConnection';
|
|
12
|
+
import { Token } from '../token';
|
|
13
|
+
import { RpcConnection, TypedRpcConnection } from '../rpcConnection';
|
|
14
|
+
import { connect } from './websocket';
|
|
15
|
+
import { HealthCheckTimeout, logger, MainConnectionPingInterval, WebRtcIceTimeout, WebRtcTimeout, } from '../config';
|
|
16
|
+
import { Transport } from '../index';
|
|
17
|
+
export class WebRtcMainConnection {
|
|
18
|
+
static create(id, address, onDatagram, signal) {
|
|
19
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
20
|
+
if (signal.aborted)
|
|
21
|
+
return failure('aborted');
|
|
22
|
+
let url;
|
|
23
|
+
try {
|
|
24
|
+
url = new URL(`wss://${address}`);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
return fail(`address in token(${address}) can not be parsed; ${error}`);
|
|
28
|
+
}
|
|
29
|
+
const webSocketUrl = extendUrl(url, 'main');
|
|
30
|
+
if (webSocketUrl.type !== 'Success')
|
|
31
|
+
return webSocketUrl;
|
|
32
|
+
const webSocket = yield connect(webSocketUrl.value, signal);
|
|
33
|
+
if (webSocket.type !== 'Success')
|
|
34
|
+
return webSocket;
|
|
35
|
+
const connection = new TypedRpcConnection(new RpcConnection(webSocket.value), MainCommandsRpc, MainNotificationsRpc);
|
|
36
|
+
try {
|
|
37
|
+
return success(new WebRtcMainConnection(id, address, url, onDatagram, connection, new RTCPeerConnection()));
|
|
38
|
+
}
|
|
39
|
+
catch (reason) {
|
|
40
|
+
connection.close();
|
|
41
|
+
return failure(`unable to create webrtc connection; ${reason}`);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
get transport() {
|
|
46
|
+
return Transport.H3;
|
|
47
|
+
}
|
|
48
|
+
get closePromise() {
|
|
49
|
+
return this._ClosePromise;
|
|
50
|
+
}
|
|
51
|
+
get closeSignal() {
|
|
52
|
+
return this.connection.closeSignal;
|
|
53
|
+
}
|
|
54
|
+
get closed() {
|
|
55
|
+
return this.connection.closed;
|
|
56
|
+
}
|
|
57
|
+
get status() {
|
|
58
|
+
switch (this.webRtc.connectionState) {
|
|
59
|
+
case 'connected':
|
|
60
|
+
return ConnectionStatus.Connected;
|
|
61
|
+
case 'failed':
|
|
62
|
+
case 'closed':
|
|
63
|
+
return ConnectionStatus.Closed;
|
|
64
|
+
default:
|
|
65
|
+
return ConnectionStatus.Connecting;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
waitForConnected() {
|
|
69
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
const onConnectionStateChanged = () => {
|
|
72
|
+
const status = this.status;
|
|
73
|
+
if (status === ConnectionStatus.Connecting)
|
|
74
|
+
return;
|
|
75
|
+
this.webRtc.removeEventListener('connectionstatechange', onConnectionStateChanged);
|
|
76
|
+
if (status === ConnectionStatus.Connected) {
|
|
77
|
+
resolve();
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
reject(new Error('connection closed'));
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
this.webRtc.addEventListener('connectionstatechange', onConnectionStateChanged);
|
|
84
|
+
onConnectionStateChanged();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
constructor(id, address, url, onDatagram, connection, webRtc) {
|
|
89
|
+
this.id = id;
|
|
90
|
+
this.address = address;
|
|
91
|
+
this.url = url;
|
|
92
|
+
this.onDatagram = onDatagram;
|
|
93
|
+
this.connection = connection;
|
|
94
|
+
this.webRtc = webRtc;
|
|
95
|
+
this._PeerIdMap = new WeakMap();
|
|
96
|
+
this._LocalVideos = new Map();
|
|
97
|
+
this._RemoteVideos = new Map();
|
|
98
|
+
this._Negotiation = { requested: false, running: false };
|
|
99
|
+
this._awaitingRemoteTracks = new Map();
|
|
100
|
+
logger.info(['Connection', 'WebRTC'], 'WebRtcMainConnection.constructor()');
|
|
101
|
+
this.closeSignal.addEventListener('abort', () => this._CleanUp(), {
|
|
102
|
+
once: true,
|
|
103
|
+
});
|
|
104
|
+
this._ClosePromise = new Promise((accept) => {
|
|
105
|
+
this.closeSignal.addEventListener('abort', () => {
|
|
106
|
+
logger.debug(['Connection', 'WebRTC'], 'WebRtcMainConnection.constructor(): _ClosePromise aborted');
|
|
107
|
+
accept('closed');
|
|
108
|
+
}, {
|
|
109
|
+
once: true,
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
this._AudioChannel = this.webRtc.createDataChannel('audio', {
|
|
113
|
+
id: 2,
|
|
114
|
+
negotiated: true,
|
|
115
|
+
ordered: false,
|
|
116
|
+
maxRetransmits: 0,
|
|
117
|
+
});
|
|
118
|
+
this._AudioChannel.binaryType = 'arraybuffer';
|
|
119
|
+
this._AudioChannel.onmessage = (event) => onDatagram(this.id, new Uint8Array(event.data));
|
|
120
|
+
this.webRtc.onicecandidate = this._OnIceCandidate.bind(this);
|
|
121
|
+
this.webRtc.onconnectionstatechange = () => this._OnWebRtcStatusChanged();
|
|
122
|
+
this.webRtc.ontrack = this.onTrack.bind(this);
|
|
123
|
+
this._SendOffer();
|
|
124
|
+
if (MainConnectionPingInterval !== undefined) {
|
|
125
|
+
this._PeriodicHealthCheck = setInterval(() => this.healthCheck(), MainConnectionPingInterval);
|
|
126
|
+
}
|
|
127
|
+
this._readWebRtcUpdate();
|
|
128
|
+
}
|
|
129
|
+
close() {
|
|
130
|
+
console.debug('MainConnection close(): ', this._PeerIdMap);
|
|
131
|
+
logger.log(['WebRTC', 'Connection'], 'MainConnection close(): ', this._PeerIdMap);
|
|
132
|
+
this.connection.close();
|
|
133
|
+
}
|
|
134
|
+
joinRoom(token, roomId, userData, position) {
|
|
135
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
136
|
+
logger.info(['Connection', 'WebRTC'], 'WebRtcMainConnection.joinRoom()');
|
|
137
|
+
const response = yield this.connection.request('JoinRoom', {
|
|
138
|
+
token: String(token),
|
|
139
|
+
room_id: roomId,
|
|
140
|
+
user_data: userData,
|
|
141
|
+
position,
|
|
142
|
+
});
|
|
143
|
+
if (response === 'closed' || response.type === 'Failure')
|
|
144
|
+
return response;
|
|
145
|
+
const responseValue = response.value;
|
|
146
|
+
if (!('token' in responseValue)) {
|
|
147
|
+
return failure('JoinRoom responded without token');
|
|
148
|
+
}
|
|
149
|
+
const url = extendUrl(this.url, `room?token=${responseValue.token}`);
|
|
150
|
+
if (url.type === 'Failure')
|
|
151
|
+
return url;
|
|
152
|
+
const webSocket = yield connect(url.value, this.closeSignal);
|
|
153
|
+
if (webSocket.type === 'Success') {
|
|
154
|
+
this._PeerIdMap.set(webSocket.value, responseValue.peer_id);
|
|
155
|
+
return success(new RpcConnection(webSocket.value));
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
return webSocket;
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
getReconnectToken(peerId) {
|
|
163
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
164
|
+
const response = yield this._Request('RequestReconnectToken', {
|
|
165
|
+
peer_id: peerId,
|
|
166
|
+
});
|
|
167
|
+
if (response.type === 'Failure')
|
|
168
|
+
return response;
|
|
169
|
+
return Token.parse(response.value);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
sendDatagram(datagram) {
|
|
173
|
+
if (this._AudioChannel.readyState === 'open') {
|
|
174
|
+
this._AudioChannel.send(datagram);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
healthCheck() {
|
|
178
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
179
|
+
const response = yield this._Request('Ping', {}, HealthCheckTimeout);
|
|
180
|
+
const success = response !== 'timeout' && response.type === 'Success';
|
|
181
|
+
if (!success)
|
|
182
|
+
this.close();
|
|
183
|
+
return success;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
_Request(name, properties, timeout) {
|
|
187
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
188
|
+
const result = yield (timeout === undefined
|
|
189
|
+
? this.connection.request(name, properties)
|
|
190
|
+
: this.connection.request(name, properties, timeout));
|
|
191
|
+
if (result === 'closed')
|
|
192
|
+
return failure('connection lost');
|
|
193
|
+
return result;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
_readWebRtcUpdate() {
|
|
197
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
198
|
+
while (true) {
|
|
199
|
+
const event = yield this.connection.recv();
|
|
200
|
+
if (event === 'closed')
|
|
201
|
+
break;
|
|
202
|
+
if (event.name === 'WebRtcUpdate') {
|
|
203
|
+
this._OnWebRtcUpdate(event.properties);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
this.close();
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
_SendOffer() {
|
|
210
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
211
|
+
var _a;
|
|
212
|
+
this.ensureTransceivers();
|
|
213
|
+
try {
|
|
214
|
+
const offer = yield this.webRtc.createOffer();
|
|
215
|
+
assert(offer.type === 'offer' && offer.sdp, 'Generated broken webrtc offer');
|
|
216
|
+
yield this.webRtc.setLocalDescription(offer);
|
|
217
|
+
const sdp = (_a = offer.sdp) !== null && _a !== void 0 ? _a : '';
|
|
218
|
+
const media_map = [];
|
|
219
|
+
for (const [media_id, { transceiver }] of this._LocalVideos) {
|
|
220
|
+
media_map.push([media_id, transceiver.mid]);
|
|
221
|
+
}
|
|
222
|
+
logger.info(['Connection', 'WebRTC'], 'WebRtcMainConnection._sendOffer(): media_map', media_map);
|
|
223
|
+
yield this._Request('WebRtcUpdate', {
|
|
224
|
+
kind: 'Sdp',
|
|
225
|
+
type: 'Offer',
|
|
226
|
+
sdp,
|
|
227
|
+
media_map,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
catch (_b) {
|
|
231
|
+
logger.error(['Connection', 'WebRTC'], 'WebRtcMainConnection._sendOffer(): Failed creating an offer');
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
_OnWebRtcStatusChanged() {
|
|
236
|
+
logger.info(['WebRTC', 'Connection'], `WebRtcMainConnection._OnWebRtcStatusChanged(): ${this.webRtc.connectionState}`);
|
|
237
|
+
switch (this.webRtc.connectionState) {
|
|
238
|
+
case 'closed':
|
|
239
|
+
case 'failed':
|
|
240
|
+
this.close();
|
|
241
|
+
break;
|
|
242
|
+
case 'connected':
|
|
243
|
+
clearTimeout(this._WebRtcIceTimeout);
|
|
244
|
+
this._WebRtcIceTimeout = undefined;
|
|
245
|
+
clearTimeout(this._WebRtcTimeout);
|
|
246
|
+
this._WebRtcTimeout = undefined;
|
|
247
|
+
break;
|
|
248
|
+
case 'disconnected':
|
|
249
|
+
if (typeof MainConnectionPingInterval !== 'undefined') {
|
|
250
|
+
this.healthCheck().then();
|
|
251
|
+
}
|
|
252
|
+
// falls through
|
|
253
|
+
default:
|
|
254
|
+
if (this._WebRtcIceTimeout === undefined) {
|
|
255
|
+
// Gets removed after successfully connected
|
|
256
|
+
this._WebRtcIceTimeout = setTimeout(() => {
|
|
257
|
+
logger.info(['WebRTC', 'Connection'], 'WebRtcMainConnection._OnWebRtcStatusChanged(): _WebRtcIceTimeout was reached');
|
|
258
|
+
this.webRtc.restartIce();
|
|
259
|
+
this._WebRtcIceTimeout = undefined;
|
|
260
|
+
}, WebRtcIceTimeout);
|
|
261
|
+
}
|
|
262
|
+
// Gets removed after successfully connected
|
|
263
|
+
if (this._WebRtcTimeout === undefined) {
|
|
264
|
+
this._WebRtcTimeout = setTimeout(() => {
|
|
265
|
+
logger.info(['WebRTC', 'Connection'], 'WebRtcMainConnection._OnWebRtcStatusChanged(): _WebRtcTimeout was reached');
|
|
266
|
+
this.close();
|
|
267
|
+
}, WebRtcTimeout);
|
|
268
|
+
}
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
_OnWebRtcUpdate(update) {
|
|
273
|
+
switch (update.kind) {
|
|
274
|
+
case 'Sdp':
|
|
275
|
+
this._UpdateRemoteSdp(update);
|
|
276
|
+
break;
|
|
277
|
+
case 'Trickle':
|
|
278
|
+
this._AddIceCandidate(update);
|
|
279
|
+
break;
|
|
280
|
+
case 'TrickleFinished':
|
|
281
|
+
this.webRtc.addIceCandidate(undefined);
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
_UpdateRemoteSdp(update) {
|
|
286
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
287
|
+
var _a;
|
|
288
|
+
logger.info(['WebRTC', 'Connection', 'Video'], 'WebRtcMainConnection._UpdateRemoteSdp(): update.media_map, this._RemoteVideos', update.media_map, this._RemoteVideos);
|
|
289
|
+
let remoteChanged = false;
|
|
290
|
+
for (const [media_id, mid] of update.media_map) {
|
|
291
|
+
const entry = this._RemoteVideos.get(media_id);
|
|
292
|
+
if ((entry === null || entry === void 0 ? void 0 : entry.state) === 'Requested') {
|
|
293
|
+
remoteChanged = true;
|
|
294
|
+
this._RemoteVideos.set(media_id, {
|
|
295
|
+
state: 'Mapped',
|
|
296
|
+
mid,
|
|
297
|
+
promise: entry.promise,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const type = update.type === 'Answer' ? 'answer' : 'offer';
|
|
302
|
+
// Set runnig to false
|
|
303
|
+
logger.info(['WebRTC', 'Connection', 'Video'], 'WebRtcMainConnection._UpdateRemoteSdp(): remoteChanged: ', remoteChanged);
|
|
304
|
+
(_a = this._ResolveOffer) === null || _a === void 0 ? void 0 : _a.call(this, true);
|
|
305
|
+
yield this.webRtc.setRemoteDescription({ type, sdp: update.sdp });
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
_AddIceCandidate(candidate) {
|
|
309
|
+
var _a, _b, _c;
|
|
310
|
+
const candidateInit = {
|
|
311
|
+
candidate: candidate.candidate,
|
|
312
|
+
sdpMid: (_a = candidate.spd_mid) !== null && _a !== void 0 ? _a : undefined,
|
|
313
|
+
sdpMLineIndex: (_b = candidate.spd_mline_index) !== null && _b !== void 0 ? _b : undefined,
|
|
314
|
+
usernameFragment: (_c = candidate.username_fragment) !== null && _c !== void 0 ? _c : undefined,
|
|
315
|
+
};
|
|
316
|
+
// HACK/TODO(max): find out why the server is not sending sdpMid or sdpMLineIndex
|
|
317
|
+
if (candidateInit.sdpMid === undefined &&
|
|
318
|
+
candidateInit.sdpMLineIndex === undefined) {
|
|
319
|
+
candidateInit.sdpMid = '0';
|
|
320
|
+
}
|
|
321
|
+
this.webRtc.addIceCandidate(candidateInit);
|
|
322
|
+
}
|
|
323
|
+
_OnIceCandidate(event) {
|
|
324
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
325
|
+
if (event.candidate) {
|
|
326
|
+
const { candidate, sdpMid, sdpMLineIndex, usernameFragment } = event.candidate;
|
|
327
|
+
this._Request('WebRtcUpdate', {
|
|
328
|
+
kind: 'Trickle',
|
|
329
|
+
candidate,
|
|
330
|
+
spd_mid: sdpMid !== null && sdpMid !== void 0 ? sdpMid : undefined,
|
|
331
|
+
spd_mline_index: sdpMLineIndex !== null && sdpMLineIndex !== void 0 ? sdpMLineIndex : undefined,
|
|
332
|
+
username_fragment: usernameFragment !== null && usernameFragment !== void 0 ? usernameFragment : undefined,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
this._Request('WebRtcUpdate', { kind: 'TrickleFinished' });
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
ensureTransceivers() {
|
|
341
|
+
const needed = this._LocalVideos.size + this._RemoteVideos.size;
|
|
342
|
+
const available = this.webRtc.getTransceivers().length;
|
|
343
|
+
for (let i = available; i < needed; i++) {
|
|
344
|
+
this.webRtc.addTransceiver('video');
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
startLocalVideo(mediaId, track) {
|
|
348
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
349
|
+
assert(!this._LocalVideos.has(mediaId), `No LocalVideo with mediaID ${mediaId}`);
|
|
350
|
+
logger.debug(['WebRTC', 'Connection', 'Video'], 'WebRtcMainConnection.startLocalVideo(): ', mediaId);
|
|
351
|
+
let transceiver = this.webRtc
|
|
352
|
+
.getTransceivers()
|
|
353
|
+
.find((transceiver) => transceiver.sender.track === track);
|
|
354
|
+
if (transceiver) {
|
|
355
|
+
logger.debug(['WebRTC', 'Connection', 'Video'], 'WebRtcMainConnection.startLocalVideo(): Transceiver found -> replaceTrack.');
|
|
356
|
+
yield transceiver.sender.replaceTrack(track);
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
const sender = this.webRtc.addTrack(track);
|
|
360
|
+
transceiver = this.webRtc
|
|
361
|
+
.getTransceivers()
|
|
362
|
+
.find((transceiver) => transceiver.sender === sender);
|
|
363
|
+
assert(transceiver, '');
|
|
364
|
+
}
|
|
365
|
+
this._LocalVideos.set(mediaId, { transceiver });
|
|
366
|
+
this.requestNegotiation(false);
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
stopLocalVideo(mediaId) {
|
|
370
|
+
const localVideo = this._LocalVideos.get(mediaId);
|
|
371
|
+
if (!localVideo)
|
|
372
|
+
return;
|
|
373
|
+
localVideo.transceiver.sender.track.enabled = false;
|
|
374
|
+
this._LocalVideos.delete(mediaId);
|
|
375
|
+
this.requestNegotiation(false);
|
|
376
|
+
}
|
|
377
|
+
startRemoteVideo(mediaId) {
|
|
378
|
+
logger.debug(['WebRTC', 'Connection', 'Video'], 'WebRtcMainConnection.startRemoteVideo()');
|
|
379
|
+
this.requestNegotiation(true);
|
|
380
|
+
return new Promise((resolve, reject) => {
|
|
381
|
+
this._RemoteVideos.set(mediaId, {
|
|
382
|
+
state: 'Requested',
|
|
383
|
+
promise: { resolve, reject },
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
stopRemoteVideo(mediaId) {
|
|
388
|
+
const video = this._RemoteVideos.get(mediaId);
|
|
389
|
+
if (!video)
|
|
390
|
+
return;
|
|
391
|
+
this._RemoteVideos.delete(mediaId);
|
|
392
|
+
if (video.state !== 'Active') {
|
|
393
|
+
video.promise.reject(new Error('video stopped'));
|
|
394
|
+
}
|
|
395
|
+
this.requestNegotiation(false);
|
|
396
|
+
}
|
|
397
|
+
onTrack(event) {
|
|
398
|
+
// If there was no entry found, store the track until a remote Video was started.
|
|
399
|
+
// When starting a remote Video, check if there is already a track available.
|
|
400
|
+
logger.debug(['WebRTC', 'Connection', 'Video'], 'WebRtcMainConnection.onTrack(): RemoteVideos: ', this._RemoteVideos);
|
|
401
|
+
const entry = Array.from(this._RemoteVideos).find((video) => {
|
|
402
|
+
return (video[1].state === 'Mapped' && video[1].mid === event.transceiver.mid);
|
|
403
|
+
});
|
|
404
|
+
logger.debug(['WebRTC', 'Connection', 'Video'], 'WebRtcMainConnection.onTrack(): Found the RemoteVideo', entry);
|
|
405
|
+
if (entry === undefined) {
|
|
406
|
+
logger.warn(['WebRTC', 'Connection', 'Video'], `WebRtcMainConnection.onTrack(): Unexpected track(mid: ${event.transceiver.mid})`);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const [media_id, video] = entry;
|
|
410
|
+
assert(video.state === 'Mapped', '');
|
|
411
|
+
this._RemoteVideos.set(media_id, {
|
|
412
|
+
state: 'Active',
|
|
413
|
+
transceiver: event.transceiver,
|
|
414
|
+
});
|
|
415
|
+
event.track.onended = () => logger.debug(['WebRTC', 'Connection', 'Video'], `WebRtcMainConnection.onTrack(): Track ended ${event.transceiver.mid}`, entry);
|
|
416
|
+
video.promise.resolve(event.track);
|
|
417
|
+
}
|
|
418
|
+
requestNegotiation(waitForAnswer) {
|
|
419
|
+
logger.debug(['WebRTC', 'Connection', 'Video'], 'WebRtcMainConnection.requestNegotiation():');
|
|
420
|
+
this._Negotiation.requested = true;
|
|
421
|
+
if (this._Negotiation.running)
|
|
422
|
+
return;
|
|
423
|
+
this._Negotiation.running = true;
|
|
424
|
+
setTimeout(() => __awaiter(this, void 0, void 0, function* () {
|
|
425
|
+
this._Negotiation.requested = false;
|
|
426
|
+
try {
|
|
427
|
+
yield this._SendOffer();
|
|
428
|
+
if (waitForAnswer) {
|
|
429
|
+
yield new Promise((resolve) => {
|
|
430
|
+
this._ResolveOffer = resolve;
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
finally {
|
|
435
|
+
logger.debug(['WebRTC', 'Connection', 'Video'], 'WebRtcMainConnection.requestNegotiation(): Finally');
|
|
436
|
+
this._Negotiation.running = false;
|
|
437
|
+
// @TODO Check if the retry is still needed.
|
|
438
|
+
const retry = Array.from(this._RemoteVideos).find((video) => {
|
|
439
|
+
return video[1].state === 'Requested';
|
|
440
|
+
});
|
|
441
|
+
if (this._Negotiation.requested) {
|
|
442
|
+
this.requestNegotiation(waitForAnswer);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}));
|
|
446
|
+
}
|
|
447
|
+
_CleanUp() {
|
|
448
|
+
console.debug(`${this.url} closed`);
|
|
449
|
+
this.connection.close();
|
|
450
|
+
this.webRtc.close();
|
|
451
|
+
clearInterval(this._PeriodicHealthCheck);
|
|
452
|
+
clearTimeout(this._WebRtcIceTimeout);
|
|
453
|
+
clearTimeout(this._WebRtcTimeout);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { ConnectInterval } from '../config';
|
|
11
|
+
import { abortableSleep, failure, success, } from '@4players/odin-common';
|
|
12
|
+
export function connect(url, signal) {
|
|
13
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
14
|
+
const start = performance.now();
|
|
15
|
+
let connection = failure('no connection was attempted');
|
|
16
|
+
for (let failures = 0; failures < ConnectInterval.length && !signal.aborted; failures++) {
|
|
17
|
+
const nextReconnectAt = start + whenToConnect(failures);
|
|
18
|
+
if ((yield abortableSleep(nextReconnectAt - performance.now(), signal)) ===
|
|
19
|
+
'aborted') {
|
|
20
|
+
return failure('aborted');
|
|
21
|
+
}
|
|
22
|
+
connection = yield connectOnce(url, signal);
|
|
23
|
+
if (connection.type === 'Success') {
|
|
24
|
+
return connection;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return connection;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export function connectOnce(url, signal) {
|
|
31
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
32
|
+
let webSocket;
|
|
33
|
+
try {
|
|
34
|
+
webSocket = new WebSocket(url);
|
|
35
|
+
}
|
|
36
|
+
catch (reason) {
|
|
37
|
+
return failure(`unable to create websocket; ${reason}`);
|
|
38
|
+
}
|
|
39
|
+
const abort = new AbortController();
|
|
40
|
+
const onOpen = new Promise((accept) => webSocket.addEventListener('open', () => accept(success(webSocket)), {
|
|
41
|
+
signal: abort.signal,
|
|
42
|
+
}));
|
|
43
|
+
const onClose = new Promise((accept) => webSocket.addEventListener('close', (event) => accept(failure(event.reason)), { signal: abort.signal }));
|
|
44
|
+
const onAbort = new Promise((accept) => signal.addEventListener('abort', () => accept(failure('aborted')), {
|
|
45
|
+
signal: abort.signal,
|
|
46
|
+
}));
|
|
47
|
+
const result = yield Promise.race([onOpen, onClose, onAbort]);
|
|
48
|
+
abort.abort();
|
|
49
|
+
return result;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function whenToConnect(failures) {
|
|
53
|
+
const past_connects = ConnectInterval.slice(0, failures).reduce((a, b) => a + b, 0);
|
|
54
|
+
const interval = ConnectInterval[failures] * Math.random();
|
|
55
|
+
return past_connects + interval;
|
|
56
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { OdinWasm } from '../../lib/wasm';
|
|
2
|
+
export interface SampleBuffer {
|
|
3
|
+
readonly headers: Int32Array;
|
|
4
|
+
readonly samples: Float32Array;
|
|
5
|
+
readonly length: number;
|
|
6
|
+
readonly sampleRate: number;
|
|
7
|
+
readonly stereo: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface SampleBufferHeader {
|
|
10
|
+
id: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function createSampleBuffer(wasm: typeof OdinWasm, sampleRate: number, length: number, stereo: boolean): SampleBuffer;
|
|
13
|
+
export declare function freeSampleBuffer(wasm: typeof OdinWasm, buffer: SampleBuffer): void;
|
|
14
|
+
export declare function samplesPerEntry(buffer: {
|
|
15
|
+
sampleRate: number;
|
|
16
|
+
stereo: boolean;
|
|
17
|
+
}): number;
|
|
18
|
+
export declare function getSamples(buffer: SampleBuffer, index: number): Float32Array;
|
|
19
|
+
export declare function getHeader(buffer: SampleBuffer, index: number): SampleBufferHeader;
|
|
20
|
+
export declare function setHeader(buffer: SampleBuffer, index: number, header: SampleBufferHeader): void;
|
|
21
|
+
export interface DecoderInformationBuffer {
|
|
22
|
+
readonly values: Float32Array;
|
|
23
|
+
readonly length: number;
|
|
24
|
+
}
|
|
25
|
+
export declare function createDecoderInformationBuffer(wasm: typeof OdinWasm, length: number): DecoderInformationBuffer;
|
|
26
|
+
export declare function freeDecoderInformationBuffer(wasm: typeof OdinWasm, buffer: DecoderInformationBuffer): void;
|
|
27
|
+
export declare function getGlobalDecoderVolume(buffer: DecoderInformationBuffer): number;
|
|
28
|
+
export declare function setGlobalDecoderVolume(buffer: DecoderInformationBuffer, value: number): void;
|
|
29
|
+
export declare function getDecoderRmsDBFS(buffer: DecoderInformationBuffer, index: number): number;
|
|
30
|
+
export declare function setDecoderRmsDBFS(buffer: DecoderInformationBuffer, index: number, value: number): void;
|
|
31
|
+
export declare function getDecoderVolume(buffer: DecoderInformationBuffer, index: number): number;
|
|
32
|
+
export declare function setDecoderVolume(buffer: DecoderInformationBuffer, index: number, value: number): void;
|
|
33
|
+
export interface EncoderInformationBuffer {
|
|
34
|
+
readonly values: Float32Array;
|
|
35
|
+
}
|
|
36
|
+
export declare function createEncoderInformationBuffer(wasm: typeof OdinWasm): EncoderInformationBuffer;
|
|
37
|
+
export declare function freeEncoderInformationBuffer(wasm: typeof OdinWasm, buffer: EncoderInformationBuffer): void;
|
|
38
|
+
export declare function getEncoderActive(buffer: EncoderInformationBuffer): boolean;
|
|
39
|
+
export declare function getEncoderSpeechProbability(buffer: EncoderInformationBuffer): number;
|
|
40
|
+
export declare function getEncoderRmsDBFS(buffer: EncoderInformationBuffer): number;
|
|
41
|
+
export declare function getEncoderVolume(buffer: EncoderInformationBuffer): number;
|
|
42
|
+
export declare function setEncoderVolume(buffer: EncoderInformationBuffer, value: number): number;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { free, malloc } from './memory';
|
|
2
|
+
export function createSampleBuffer(wasm, sampleRate, length, stereo) {
|
|
3
|
+
return {
|
|
4
|
+
headers: malloc(wasm, 'i32', length),
|
|
5
|
+
samples: malloc(wasm, 'f32', length * samplesPerEntry({ sampleRate, stereo })),
|
|
6
|
+
length,
|
|
7
|
+
sampleRate,
|
|
8
|
+
stereo,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function freeSampleBuffer(wasm, buffer) {
|
|
12
|
+
free(wasm, buffer.headers);
|
|
13
|
+
free(wasm, buffer.samples);
|
|
14
|
+
}
|
|
15
|
+
export function samplesPerEntry(buffer) {
|
|
16
|
+
return buffer.sampleRate * 0.02 * (buffer.stereo ? 2 : 1);
|
|
17
|
+
}
|
|
18
|
+
export function getSamples(buffer, index) {
|
|
19
|
+
const size = samplesPerEntry(buffer);
|
|
20
|
+
const begin = size * index;
|
|
21
|
+
const end = begin + size;
|
|
22
|
+
return buffer.samples.subarray(begin, end);
|
|
23
|
+
}
|
|
24
|
+
export function getHeader(buffer, index) {
|
|
25
|
+
return { id: buffer.headers[index] };
|
|
26
|
+
}
|
|
27
|
+
export function setHeader(buffer, index, header) {
|
|
28
|
+
buffer.headers[index] = header.id;
|
|
29
|
+
}
|
|
30
|
+
export function createDecoderInformationBuffer(wasm, length) {
|
|
31
|
+
return {
|
|
32
|
+
values: malloc(wasm, 'f32', 1 + length * 2),
|
|
33
|
+
length,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function freeDecoderInformationBuffer(wasm, buffer) {
|
|
37
|
+
free(wasm, buffer.values);
|
|
38
|
+
}
|
|
39
|
+
export function getGlobalDecoderVolume(buffer) {
|
|
40
|
+
return buffer.values[0];
|
|
41
|
+
}
|
|
42
|
+
export function setGlobalDecoderVolume(buffer, value) {
|
|
43
|
+
buffer.values[0] = value;
|
|
44
|
+
}
|
|
45
|
+
export function getDecoderRmsDBFS(buffer, index) {
|
|
46
|
+
return buffer.values[1 + index * 2];
|
|
47
|
+
}
|
|
48
|
+
export function setDecoderRmsDBFS(buffer, index, value) {
|
|
49
|
+
buffer.values[1 + index * 2] = value;
|
|
50
|
+
}
|
|
51
|
+
export function getDecoderVolume(buffer, index) {
|
|
52
|
+
return buffer.values[1 + index * 2 + 1];
|
|
53
|
+
}
|
|
54
|
+
export function setDecoderVolume(buffer, index, value) {
|
|
55
|
+
buffer.values[1 + index * 2 + 1] = value;
|
|
56
|
+
}
|
|
57
|
+
export function createEncoderInformationBuffer(wasm) {
|
|
58
|
+
return {
|
|
59
|
+
values: malloc(wasm, 'f32', 4),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export function freeEncoderInformationBuffer(wasm, buffer) {
|
|
63
|
+
free(wasm, buffer.values);
|
|
64
|
+
}
|
|
65
|
+
export function getEncoderActive(buffer) {
|
|
66
|
+
return buffer.values[0] !== 0;
|
|
67
|
+
}
|
|
68
|
+
export function getEncoderSpeechProbability(buffer) {
|
|
69
|
+
return buffer.values[1];
|
|
70
|
+
}
|
|
71
|
+
export function getEncoderRmsDBFS(buffer) {
|
|
72
|
+
return buffer.values[2];
|
|
73
|
+
}
|
|
74
|
+
export function getEncoderVolume(buffer) {
|
|
75
|
+
return buffer.values[3];
|
|
76
|
+
}
|
|
77
|
+
export function setEncoderVolume(buffer, value) {
|
|
78
|
+
return (buffer.values[3] = value);
|
|
79
|
+
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
export type CaptureSink = (connectionId: number, packet: Uint8Array) => void;
|
|
2
1
|
export interface Activity {
|
|
3
2
|
isSilent: boolean;
|
|
4
3
|
rmsDBFS: number;
|
|
5
4
|
}
|
|
6
|
-
export declare const
|
|
5
|
+
export declare const Silence: Activity;
|
|
7
6
|
export interface VadConfig {
|
|
8
7
|
volumeGate?: {
|
|
9
8
|
attackThreshold: number;
|
|
@@ -14,6 +13,7 @@ export interface VadConfig {
|
|
|
14
13
|
releaseThreshold: number;
|
|
15
14
|
};
|
|
16
15
|
}
|
|
16
|
+
export type CaptureSink = (connectionId: number, packet: Uint8Array) => void;
|
|
17
17
|
export interface CaptureWorker {
|
|
18
18
|
readonly worklet: AudioWorkletNode;
|
|
19
19
|
readonly worker: Worker;
|
|
@@ -28,11 +28,6 @@ export interface CaptureWorker {
|
|
|
28
28
|
setVad(config: VadConfig): void;
|
|
29
29
|
close(): void;
|
|
30
30
|
}
|
|
31
|
-
export declare function createCaptureWorker(context: AudioContext, sink: CaptureSink, refreshRate?: number): Promise<CaptureWorker>;
|
|
32
|
-
export interface PlaybackId {
|
|
33
|
-
connectionId: number;
|
|
34
|
-
mediaId: number;
|
|
35
|
-
}
|
|
36
31
|
export interface PlaybackWorker {
|
|
37
32
|
readonly audioContext: AudioContext;
|
|
38
33
|
receivePacket(connectionId: number, packet: Uint8Array): void;
|
|
@@ -43,4 +38,8 @@ export interface PlaybackWorker {
|
|
|
43
38
|
setVolume(id: PlaybackId, value: number): void;
|
|
44
39
|
close(): void;
|
|
45
40
|
}
|
|
46
|
-
export
|
|
41
|
+
export interface PlaybackId {
|
|
42
|
+
connectionId: number;
|
|
43
|
+
mediaId: number;
|
|
44
|
+
}
|
|
45
|
+
export declare function mergeIds(id: PlaybackId): number;
|