@4players/odin-plugin-web 1.0.0-alpha.13 → 1.0.0-alpha.15
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/lib/plugin/config.d.ts +5 -3
- package/lib/plugin/config.js +15 -3
- package/lib/plugin/connectionPool.d.ts +3 -2
- package/lib/plugin/connectionPool.js +31 -13
- package/lib/plugin/deviceManager.d.ts +12 -0
- package/lib/plugin/deviceManager.js +120 -0
- package/lib/plugin/h3/mainConnection.d.ts +40 -0
- package/lib/plugin/h3/mainConnection.js +197 -0
- package/lib/plugin/index.d.ts +17 -6
- package/lib/plugin/index.js +158 -56
- package/lib/plugin/mainConnection.d.ts +23 -30
- package/lib/plugin/mainConnection.js +20 -291
- package/lib/plugin/{audioController.d.ts → mediaController.d.ts} +6 -46
- package/lib/plugin/mediaController.js +215 -0
- package/lib/plugin/medias/audio-capture.d.ts +29 -0
- package/lib/plugin/medias/audio-capture.js +203 -0
- package/lib/plugin/medias/audio-playback.d.ts +13 -0
- package/lib/plugin/medias/audio-playback.js +24 -0
- package/lib/plugin/medias/video-capture.d.ts +15 -0
- package/lib/plugin/medias/video-capture.js +70 -0
- package/lib/plugin/medias/video-playback.d.ts +13 -0
- package/lib/plugin/medias/video-playback.js +20 -0
- package/lib/plugin/roomConnection.d.ts +5 -2
- package/lib/plugin/roomConnection.js +54 -31
- package/lib/plugin/rpcConnection.d.ts +13 -7
- package/lib/plugin/rpcConnection.js +103 -33
- package/lib/plugin/token.d.ts +0 -2
- package/lib/plugin/token.js +0 -11
- package/lib/plugin/webrtc/mainConnection.d.ts +52 -0
- package/lib/plugin/webrtc/mainConnection.js +455 -0
- package/lib/plugin/{websocket.js → webrtc/websocket.js} +2 -2
- package/lib/version.d.ts +1 -0
- package/lib/version.js +2 -0
- package/lib/worker/index.js +1 -1
- package/package.json +18 -11
- package/lib/plugin/audioController.js +0 -513
- /package/lib/plugin/{websocket.d.ts → webrtc/websocket.d.ts} +0 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
export function getByteSize(buffer, offset) {
|
|
2
|
+
const nextOffset = getNextOffset(buffer, offset);
|
|
3
|
+
return nextOffset !== undefined ? nextOffset - offset : undefined;
|
|
4
|
+
}
|
|
5
|
+
export function getNextOffset(buffer, offset) {
|
|
6
|
+
if (offset >= buffer.length)
|
|
7
|
+
return undefined;
|
|
8
|
+
const tag = buffer[offset++];
|
|
9
|
+
switch (tag) {
|
|
10
|
+
case 0xc0:
|
|
11
|
+
case 0xc2:
|
|
12
|
+
case 0xc3:
|
|
13
|
+
return offset;
|
|
14
|
+
case 0xc4:
|
|
15
|
+
return skipBytes(buffer, offset + 1, read8bit(buffer, offset));
|
|
16
|
+
case 0xc5:
|
|
17
|
+
return skipBytes(buffer, offset + 2, read16bit(buffer, offset));
|
|
18
|
+
case 0xc6:
|
|
19
|
+
return skipBytes(buffer, offset + 4, read32bit(buffer, offset));
|
|
20
|
+
case 0xc7:
|
|
21
|
+
return skipBytes(buffer, offset + 2, read8bit(buffer, offset));
|
|
22
|
+
case 0xc8:
|
|
23
|
+
return skipBytes(buffer, offset + 3, read16bit(buffer, offset));
|
|
24
|
+
case 0xc9:
|
|
25
|
+
return skipBytes(buffer, offset + 5, read32bit(buffer, offset));
|
|
26
|
+
case 0xca:
|
|
27
|
+
return skipBytes(buffer, offset, 4);
|
|
28
|
+
case 0xcb:
|
|
29
|
+
return skipBytes(buffer, offset, 8);
|
|
30
|
+
case 0xcc:
|
|
31
|
+
return skipBytes(buffer, offset, 1);
|
|
32
|
+
case 0xcd:
|
|
33
|
+
return skipBytes(buffer, offset, 2);
|
|
34
|
+
case 0xce:
|
|
35
|
+
return skipBytes(buffer, offset, 4);
|
|
36
|
+
case 0xcf:
|
|
37
|
+
return skipBytes(buffer, offset, 8);
|
|
38
|
+
case 0xd0:
|
|
39
|
+
return skipBytes(buffer, offset, 1);
|
|
40
|
+
case 0xd1:
|
|
41
|
+
return skipBytes(buffer, offset, 2);
|
|
42
|
+
case 0xd2:
|
|
43
|
+
return skipBytes(buffer, offset, 4);
|
|
44
|
+
case 0xd3:
|
|
45
|
+
return skipBytes(buffer, offset, 8);
|
|
46
|
+
case 0xd4:
|
|
47
|
+
return skipBytes(buffer, offset, 2);
|
|
48
|
+
case 0xd5:
|
|
49
|
+
return skipBytes(buffer, offset, 3);
|
|
50
|
+
case 0xd6:
|
|
51
|
+
return skipBytes(buffer, offset, 5);
|
|
52
|
+
case 0xd7:
|
|
53
|
+
return skipBytes(buffer, offset, 9);
|
|
54
|
+
case 0xd8:
|
|
55
|
+
return skipBytes(buffer, offset, 17);
|
|
56
|
+
case 0xd9:
|
|
57
|
+
return skipBytes(buffer, offset + 1, read8bit(buffer, offset));
|
|
58
|
+
case 0xda:
|
|
59
|
+
return skipBytes(buffer, offset + 2, read16bit(buffer, offset));
|
|
60
|
+
case 0xdb:
|
|
61
|
+
return skipBytes(buffer, offset + 4, read32bit(buffer, offset));
|
|
62
|
+
case 0xdc:
|
|
63
|
+
return skipObjects(buffer, offset + 2, read16bit(buffer, offset));
|
|
64
|
+
case 0xdd:
|
|
65
|
+
return skipObjects(buffer, offset + 4, read32bit(buffer, offset));
|
|
66
|
+
case 0xde:
|
|
67
|
+
return skipObjects(buffer, offset + 2, read16bit(buffer, offset) * 2);
|
|
68
|
+
case 0xdf:
|
|
69
|
+
return skipObjects(buffer, offset + 4, read32bit(buffer, offset) * 2);
|
|
70
|
+
default:
|
|
71
|
+
if ((tag & 0x80) === 0x00)
|
|
72
|
+
return offset;
|
|
73
|
+
if ((tag & 0xf0) === 0x80) {
|
|
74
|
+
return skipObjects(buffer, offset, (tag & 0x0f) * 2);
|
|
75
|
+
}
|
|
76
|
+
if ((tag & 0xf0) === 0x90)
|
|
77
|
+
return skipObjects(buffer, offset, tag & 0x0f);
|
|
78
|
+
if ((tag & 0xe0) === 0xa0)
|
|
79
|
+
return skipBytes(buffer, offset, tag & 0x1f);
|
|
80
|
+
if ((tag & 0xe0) === 0xe0)
|
|
81
|
+
return offset;
|
|
82
|
+
throw new Error('invalid msgpack');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function skipBytes(buffer, offset, bytes) {
|
|
86
|
+
const newOffset = offset + bytes;
|
|
87
|
+
return isFinite(newOffset) ? newOffset : undefined;
|
|
88
|
+
}
|
|
89
|
+
function skipObjects(buffer, offset, objects) {
|
|
90
|
+
if (!isFinite(objects))
|
|
91
|
+
return undefined;
|
|
92
|
+
for (let i = 0; i < objects; i++) {
|
|
93
|
+
const newOffset = getNextOffset(buffer, offset);
|
|
94
|
+
if (newOffset === undefined)
|
|
95
|
+
return undefined;
|
|
96
|
+
offset = newOffset;
|
|
97
|
+
}
|
|
98
|
+
return offset;
|
|
99
|
+
}
|
|
100
|
+
function read8bit(bytes, offset) {
|
|
101
|
+
if (offset > bytes.length)
|
|
102
|
+
return NaN;
|
|
103
|
+
return bytes[offset];
|
|
104
|
+
}
|
|
105
|
+
function read16bit(bytes, offset) {
|
|
106
|
+
if (offset + 2 > bytes.length)
|
|
107
|
+
return NaN;
|
|
108
|
+
return (bytes[offset] << 8) | bytes[offset + 1];
|
|
109
|
+
}
|
|
110
|
+
function read32bit(bytes, offset) {
|
|
111
|
+
if (offset + 4 > bytes.length)
|
|
112
|
+
return NaN;
|
|
113
|
+
return ((bytes[offset] << 24) |
|
|
114
|
+
(bytes[offset + 1] << 16) |
|
|
115
|
+
(bytes[offset + 2] << 8) |
|
|
116
|
+
bytes[offset + 3]);
|
|
117
|
+
}
|
package/lib/plugin/config.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { Logger } from '@4players/odin-common';
|
|
1
2
|
export declare const HealthCheckTimeout = 1000;
|
|
2
3
|
export declare const ClockJumpCheckInterval = 1000;
|
|
3
4
|
export declare const ClockJumpThreshold = 10000;
|
|
4
|
-
export declare const MainConnectionPingInterval
|
|
5
|
-
export declare const
|
|
6
|
-
export declare const
|
|
5
|
+
export declare const MainConnectionPingInterval: undefined;
|
|
6
|
+
export declare const WebRtcIceTimeout = 3000;
|
|
7
|
+
export declare const WebRtcTimeout = 10000;
|
|
7
8
|
export declare const ConnectTimeout = 5000;
|
|
8
9
|
export declare const MinTokenLifetime = 60000;
|
|
9
10
|
export declare const MinTokenRefreshInterval = 5000;
|
|
10
11
|
export declare const ConnectInterval: number[];
|
|
12
|
+
export declare const logger: Logger;
|
package/lib/plugin/config.js
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
|
+
import { Logger } from '@4players/odin-common';
|
|
1
2
|
export const HealthCheckTimeout = 1000; /* ms */
|
|
2
3
|
export const ClockJumpCheckInterval = 1000; /* ms */
|
|
3
4
|
export const ClockJumpThreshold = 10000; /* ms */
|
|
4
|
-
export const MainConnectionPingInterval =
|
|
5
|
-
export const
|
|
6
|
-
export const
|
|
5
|
+
export const MainConnectionPingInterval = undefined; /* disabled, was 10000 ms */
|
|
6
|
+
export const WebRtcIceTimeout = 3000; /* ms */
|
|
7
|
+
export const WebRtcTimeout = 10000; /* ms */
|
|
7
8
|
export const ConnectTimeout = 5000; /* ms */
|
|
8
9
|
export const MinTokenLifetime = 60000; /* 1min */
|
|
9
10
|
export const MinTokenRefreshInterval = 5000; /* 5sec */
|
|
10
11
|
export const ConnectInterval = [0, 100, 200, 200, 500, 1000, 2000]; /* ms */
|
|
12
|
+
let loggerInstance;
|
|
13
|
+
const projectName = 'odin-plugin-web';
|
|
14
|
+
function createLogger() {
|
|
15
|
+
return new Logger(projectName, {
|
|
16
|
+
tags: [],
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
if (!loggerInstance) {
|
|
20
|
+
loggerInstance = createLogger();
|
|
21
|
+
}
|
|
22
|
+
export const logger = loggerInstance;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Result } from '@4players/odin-common';
|
|
1
|
+
import { Result, Backend } from '@4players/odin-common';
|
|
2
2
|
import { DatagramHandler, MainConnection } from './mainConnection';
|
|
3
|
+
import Transport = Backend.Transport;
|
|
3
4
|
export declare class ConnectionPool {
|
|
4
5
|
readonly onDatagram: DatagramHandler;
|
|
5
6
|
private _LastConnectionId;
|
|
@@ -9,7 +10,7 @@ export declare class ConnectionPool {
|
|
|
9
10
|
get closed(): boolean;
|
|
10
11
|
constructor(onDatagram: DatagramHandler);
|
|
11
12
|
sendDatagram(connectionId: number, packet: Uint8Array): void;
|
|
12
|
-
acquireConnection(
|
|
13
|
+
acquireConnection(transport: Transport, address: string, signal: AbortSignal): Promise<Result<MainConnection> | 'closed'>;
|
|
13
14
|
releaseConnection(connection: MainConnection): void;
|
|
14
15
|
checkHealth(): Promise<void>;
|
|
15
16
|
close(): void;
|
|
@@ -7,9 +7,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
-
import { failure, nextTick, success } from '@4players/odin-common';
|
|
11
|
-
import { ClockJumpCheckInterval, ClockJumpThreshold } from './config';
|
|
12
|
-
import {
|
|
10
|
+
import { failure, nextTick, success, } from '@4players/odin-common';
|
|
11
|
+
import { ClockJumpCheckInterval, ClockJumpThreshold, logger } from './config';
|
|
12
|
+
import { createMainConnection, } from './mainConnection';
|
|
13
13
|
export class ConnectionPool {
|
|
14
14
|
get closed() {
|
|
15
15
|
return this._Close.signal.aborted;
|
|
@@ -47,38 +47,52 @@ export class ConnectionPool {
|
|
|
47
47
|
connection.connection.sendDatagram(packet);
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
-
acquireConnection(
|
|
50
|
+
acquireConnection(transport, address, signal) {
|
|
51
51
|
return __awaiter(this, void 0, void 0, function* () {
|
|
52
|
+
logger.log(['Connection'], 'ConnectionPool.acquireConnection(): transport, address', transport, address);
|
|
52
53
|
yield nextTick(); // slow down acquireConnection
|
|
53
54
|
if (this.closed)
|
|
54
55
|
return 'closed';
|
|
55
|
-
|
|
56
|
+
const key = create_connection_key(transport, address);
|
|
57
|
+
let entry = this._ConnectionsByUrl.get(key);
|
|
56
58
|
if (entry === undefined ||
|
|
57
59
|
(entry.status === 'ready' && entry.connection.closed)) {
|
|
60
|
+
// Create a new Connection
|
|
58
61
|
const id = this._LastConnectionId++;
|
|
59
|
-
const promise =
|
|
60
|
-
entry = { status: 'init', id, promise };
|
|
61
|
-
this._ConnectionsByUrl.set(
|
|
62
|
+
const promise = createMainConnection(id, transport, address, this.onDatagram, signal);
|
|
63
|
+
entry = { status: 'init', id, promise, references: 1 };
|
|
64
|
+
this._ConnectionsByUrl.set(key, entry);
|
|
62
65
|
this._ConnectionsById.set(id, entry);
|
|
63
66
|
const connection = yield promise;
|
|
67
|
+
logger.log(['Connection'], 'ConnectionPool.acquireConnection(): MainConnection resolved.', connection);
|
|
64
68
|
if (connection.type !== 'Success') {
|
|
65
|
-
this._ConnectionsByUrl.delete(
|
|
69
|
+
this._ConnectionsByUrl.delete(key);
|
|
66
70
|
this._ConnectionsById.delete(id);
|
|
67
71
|
return connection;
|
|
68
72
|
}
|
|
69
|
-
entry = {
|
|
70
|
-
|
|
73
|
+
entry = {
|
|
74
|
+
status: 'ready',
|
|
75
|
+
connection: connection.value,
|
|
76
|
+
references: entry.references,
|
|
77
|
+
};
|
|
78
|
+
this._ConnectionsByUrl.set(key, entry);
|
|
71
79
|
this._ConnectionsById.set(id, entry);
|
|
72
80
|
return success(entry.connection);
|
|
73
81
|
}
|
|
74
82
|
else if (entry.status === 'init') {
|
|
83
|
+
logger.log(['Connection'], 'ConnectionPool.acquireConnection(): Found a MainConnection but it is still in init state.');
|
|
84
|
+
// Wait for the connection to connect
|
|
85
|
+
entry.references += 1;
|
|
75
86
|
const onAbort = new Promise((accept) => signal.addEventListener('abort', () => accept(), { once: true }));
|
|
76
87
|
const result = yield Promise.race([entry.promise, onAbort]);
|
|
88
|
+
logger.log(['Connection'], 'ConnectionPool.acquireConnection(): MainConnection was in init state but is now resolved', result);
|
|
77
89
|
if (result === undefined)
|
|
78
90
|
return failure('aborted');
|
|
79
91
|
return result;
|
|
80
92
|
}
|
|
81
93
|
else {
|
|
94
|
+
// Return the connection
|
|
95
|
+
logger.log(['Connection'], 'ConnectionPool.acquireConnection(): Found a MainConnection and return it');
|
|
82
96
|
entry.references += 1;
|
|
83
97
|
return success(entry.connection);
|
|
84
98
|
}
|
|
@@ -89,13 +103,14 @@ export class ConnectionPool {
|
|
|
89
103
|
if (entry === undefined)
|
|
90
104
|
return;
|
|
91
105
|
if (entry.status === 'init') {
|
|
92
|
-
|
|
106
|
+
logger.log(['Connection'], 'ConnectionPool.releaseConnection(): MainConnection state is init, return.');
|
|
93
107
|
return;
|
|
94
108
|
}
|
|
95
109
|
entry.references -= 1;
|
|
96
110
|
if (entry.references === 0) {
|
|
111
|
+
logger.log(['Connection'], 'ConnectionPool.releaseConnection(): References is 0, close the MainConnection.');
|
|
97
112
|
entry.connection.close();
|
|
98
|
-
this._ConnectionsByUrl.delete(connection.
|
|
113
|
+
this._ConnectionsByUrl.delete(create_connection_key(connection.transport, connection.address));
|
|
99
114
|
this._ConnectionsById.delete(connection.id);
|
|
100
115
|
}
|
|
101
116
|
}
|
|
@@ -108,3 +123,6 @@ export class ConnectionPool {
|
|
|
108
123
|
this._Close.abort();
|
|
109
124
|
}
|
|
110
125
|
}
|
|
126
|
+
function create_connection_key(transport, address) {
|
|
127
|
+
return `${transport} ${address}`;
|
|
128
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { MediaController, Device } from './mediaController';
|
|
2
|
+
import { DeviceParameters } from './index';
|
|
3
|
+
import { Backend, Result } from '@4players/odin-common';
|
|
4
|
+
export declare function openCaptureDevice(controller: MediaController, parameters: DeviceParameters, apm: Backend.ApmConfig): Promise<Result<MediaStreamAudioSourceNode>>;
|
|
5
|
+
export declare function stopMediaStream(ms: MediaStream): void;
|
|
6
|
+
export declare function enumerateDevices(): Promise<Device[]>;
|
|
7
|
+
/**
|
|
8
|
+
* Requests audio and video browser permissions. Optionally, requests either audio or video permissions.
|
|
9
|
+
* @private
|
|
10
|
+
* @throws
|
|
11
|
+
*/
|
|
12
|
+
export declare function requestMediaPermissions(): Promise<boolean>;
|
|
@@ -0,0 +1,120 @@
|
|
|
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 { Device } from './mediaController';
|
|
11
|
+
import { logger } from './index';
|
|
12
|
+
import { failure, sleep, success, } from '@4players/odin-common';
|
|
13
|
+
let hasAudioPermissions = false;
|
|
14
|
+
export function openCaptureDevice(controller, parameters, apm) {
|
|
15
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
16
|
+
const deviceId = parameters.device
|
|
17
|
+
? { exact: parameters.device.device.deviceId }
|
|
18
|
+
: undefined;
|
|
19
|
+
let stream;
|
|
20
|
+
try {
|
|
21
|
+
const result = yield Promise.race([
|
|
22
|
+
navigator.mediaDevices.getUserMedia({
|
|
23
|
+
audio: {
|
|
24
|
+
echoCancellation: apm.echoCanceller,
|
|
25
|
+
noiseSuppression: apm.noiseSuppression !== 'None',
|
|
26
|
+
autoGainControl: apm.gainController,
|
|
27
|
+
// @ts-ignore chrome fix
|
|
28
|
+
echoCancellationType: 'system',
|
|
29
|
+
// sampleRate: 48000,
|
|
30
|
+
deviceId,
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
33
|
+
sleep(30000, 'timeout'),
|
|
34
|
+
]);
|
|
35
|
+
if (result === 'timeout')
|
|
36
|
+
return failure('getUserMedia; timeout');
|
|
37
|
+
hasAudioPermissions = true;
|
|
38
|
+
stream = result;
|
|
39
|
+
}
|
|
40
|
+
catch (reason) {
|
|
41
|
+
return failure(`getUserMedia; ${reason}`);
|
|
42
|
+
}
|
|
43
|
+
const sampleRate = stream.getAudioTracks()[0].getSettings().sampleRate;
|
|
44
|
+
const context = yield controller.audioContexts.acquire(sampleRate);
|
|
45
|
+
return success(context.createMediaStreamSource(stream));
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
export function stopMediaStream(ms) {
|
|
49
|
+
ms.getTracks().forEach((track) => {
|
|
50
|
+
track.stop();
|
|
51
|
+
ms.removeTrack(track);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
export function enumerateDevices() {
|
|
55
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
56
|
+
hasAudioPermissions = yield requestMediaPermissions();
|
|
57
|
+
const mediaDeviceInfos = yield navigator.mediaDevices.enumerateDevices();
|
|
58
|
+
const devices = [];
|
|
59
|
+
let outputDeviceIndex = 0;
|
|
60
|
+
let inputDeviceIndex = 0;
|
|
61
|
+
for (const mediaDeviceInfo of mediaDeviceInfos) {
|
|
62
|
+
if (mediaDeviceInfo.kind === 'audioinput') {
|
|
63
|
+
if (inputDeviceIndex === 0) {
|
|
64
|
+
devices.push(new Device(mediaDeviceInfo, true));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
devices.push(new Device(mediaDeviceInfo, false));
|
|
68
|
+
}
|
|
69
|
+
inputDeviceIndex++;
|
|
70
|
+
}
|
|
71
|
+
if (mediaDeviceInfo.kind === 'audiooutput') {
|
|
72
|
+
if (outputDeviceIndex === 0) {
|
|
73
|
+
devices.push(new Device(mediaDeviceInfo, true));
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
devices.push(new Device(mediaDeviceInfo, false));
|
|
77
|
+
}
|
|
78
|
+
outputDeviceIndex++;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return devices;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Requests audio and video browser permissions. Optionally, requests either audio or video permissions.
|
|
86
|
+
* @private
|
|
87
|
+
* @throws
|
|
88
|
+
*/
|
|
89
|
+
export function requestMediaPermissions() {
|
|
90
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
91
|
+
if (hasAudioPermissions)
|
|
92
|
+
return true;
|
|
93
|
+
const constraints = {};
|
|
94
|
+
constraints.audio = {
|
|
95
|
+
sampleRate: undefined,
|
|
96
|
+
deviceId: '',
|
|
97
|
+
autoGainControl: false,
|
|
98
|
+
noiseSuppression: false,
|
|
99
|
+
advanced: undefined,
|
|
100
|
+
aspectRatio: undefined,
|
|
101
|
+
echoCancellation: false,
|
|
102
|
+
channelCount: 0,
|
|
103
|
+
facingMode: undefined,
|
|
104
|
+
frameRate: 0,
|
|
105
|
+
};
|
|
106
|
+
logger.debug(['Device'], 'requestMediaPermissions(): constraints', constraints);
|
|
107
|
+
try {
|
|
108
|
+
const ms = yield navigator.mediaDevices.getUserMedia(constraints);
|
|
109
|
+
ms === null || ms === void 0 ? void 0 : ms.getTracks().forEach((track) => {
|
|
110
|
+
track.stop();
|
|
111
|
+
ms.removeTrack(track);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
logger.error(['Device'], `requestMediaPermissions(): Requesting permissions for ${'audio'} failed.`, e);
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
return true;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ConnectionStatus, type DatagramHandler, type MainConnection } from '../mainConnection';
|
|
2
|
+
import { MainCommands, MainNotifications, PeerId, Result, Backend } from '@4players/odin-common';
|
|
3
|
+
import { Token } from '../token';
|
|
4
|
+
import { RpcConnection, TypedRpcConnection } from '../rpcConnection';
|
|
5
|
+
type IncomingStream = {
|
|
6
|
+
type: 'waiting';
|
|
7
|
+
resolve: (stream: WebTransportBidirectionalStream) => void;
|
|
8
|
+
} | {
|
|
9
|
+
type: 'received';
|
|
10
|
+
stream: WebTransportBidirectionalStream;
|
|
11
|
+
timeout: ReturnType<typeof setTimeout>;
|
|
12
|
+
};
|
|
13
|
+
export declare class H3MainConnection implements MainConnection {
|
|
14
|
+
readonly id: number;
|
|
15
|
+
readonly address: string;
|
|
16
|
+
readonly connection: WebTransport;
|
|
17
|
+
readonly rpc: TypedRpcConnection<MainCommands, MainNotifications>;
|
|
18
|
+
readonly closeController: AbortController;
|
|
19
|
+
status: ConnectionStatus;
|
|
20
|
+
readonly incomingStreams: Map<number, IncomingStream>;
|
|
21
|
+
readonly closePromise: Promise<'closed'>;
|
|
22
|
+
readonly datagramWriter: WritableStreamDefaultWriter<Uint8Array>;
|
|
23
|
+
get closeSignal(): AbortSignal;
|
|
24
|
+
get transport(): Backend.Transport;
|
|
25
|
+
get closed(): boolean;
|
|
26
|
+
static create(id: number, address: string, onDatagram: DatagramHandler, signal: AbortSignal): Promise<Result<MainConnection>>;
|
|
27
|
+
private constructor();
|
|
28
|
+
waitForConnected(): Promise<void>;
|
|
29
|
+
close(): void;
|
|
30
|
+
joinRoom(token: Token, roomId: string, userData: Uint8Array, position: [number, number, number]): Promise<'closed' | Result<RpcConnection>>;
|
|
31
|
+
getReconnectToken(peerId: PeerId): Promise<Result<Token>>;
|
|
32
|
+
healthCheck(): Promise<boolean>;
|
|
33
|
+
sendDatagram(datagram: Uint8Array): void;
|
|
34
|
+
startLocalVideo(mediaId: number, track: MediaStreamTrack): void;
|
|
35
|
+
stopLocalVideo(mediaId: number): void;
|
|
36
|
+
startRemoteVideo(mediaId: number): Promise<MediaStreamTrack>;
|
|
37
|
+
stopRemoteVideo(mediaId: number): void;
|
|
38
|
+
private _Request;
|
|
39
|
+
}
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,197 @@
|
|
|
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 { ConnectionStatus, } from '../mainConnection';
|
|
11
|
+
import { MainCommandsRpc, MainNotificationsRpc, RoomCommandsRpc, RoomNotificationsRpc, failure, success, Backend, } from '@4players/odin-common';
|
|
12
|
+
import { logger } from '..';
|
|
13
|
+
import { Token } from '../token';
|
|
14
|
+
import { RpcConnection, TypedRpcConnection } from '../rpcConnection';
|
|
15
|
+
import { HealthCheckTimeout } from '../config';
|
|
16
|
+
import { version } from '../../version';
|
|
17
|
+
var Transport = Backend.Transport;
|
|
18
|
+
export class H3MainConnection {
|
|
19
|
+
get closeSignal() {
|
|
20
|
+
return this.closeController.signal;
|
|
21
|
+
}
|
|
22
|
+
get transport() {
|
|
23
|
+
return Transport.H3;
|
|
24
|
+
}
|
|
25
|
+
get closed() {
|
|
26
|
+
return this.closeSignal.aborted;
|
|
27
|
+
}
|
|
28
|
+
static create(id, address, onDatagram, signal) {
|
|
29
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
30
|
+
try {
|
|
31
|
+
const url = new URL(`https://${address}?version=${version}`);
|
|
32
|
+
const closeController = new AbortController();
|
|
33
|
+
signal.addEventListener('abort', () => closeController.abort(), {
|
|
34
|
+
once: true,
|
|
35
|
+
});
|
|
36
|
+
const connection = new WebTransport(url);
|
|
37
|
+
closeController.signal.addEventListener('abort', () => __awaiter(this, void 0, void 0, function* () {
|
|
38
|
+
try {
|
|
39
|
+
yield connection.close();
|
|
40
|
+
}
|
|
41
|
+
catch (_a) {
|
|
42
|
+
/* drop silently */
|
|
43
|
+
}
|
|
44
|
+
}), { once: true });
|
|
45
|
+
yield connection.ready;
|
|
46
|
+
const stream = yield connection.createBidirectionalStream();
|
|
47
|
+
const rpc = new TypedRpcConnection(new RpcConnection(stream), MainCommandsRpc, MainNotificationsRpc);
|
|
48
|
+
closeController.signal.addEventListener('abort', () => rpc.close(), {
|
|
49
|
+
once: true,
|
|
50
|
+
});
|
|
51
|
+
rpc.notify('Hello', { stream: 'main' });
|
|
52
|
+
return success(new H3MainConnection(id, address, onDatagram, connection, rpc, closeController));
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
return failure(`connecting to ${address} failed with ${error}`);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
constructor(id, address, onDatagram, connection, rpc, closeController) {
|
|
60
|
+
this.id = id;
|
|
61
|
+
this.address = address;
|
|
62
|
+
this.connection = connection;
|
|
63
|
+
this.rpc = rpc;
|
|
64
|
+
this.closeController = closeController;
|
|
65
|
+
this.status = ConnectionStatus.Connecting;
|
|
66
|
+
this.incomingStreams = new Map();
|
|
67
|
+
logger.info(['Connection', 'H3'], 'H3MainConnection.constructor()');
|
|
68
|
+
this.closePromise = (() => __awaiter(this, void 0, void 0, function* () {
|
|
69
|
+
try {
|
|
70
|
+
yield this.connection.closed;
|
|
71
|
+
}
|
|
72
|
+
catch (_a) {
|
|
73
|
+
/* drop silently */
|
|
74
|
+
}
|
|
75
|
+
return 'closed';
|
|
76
|
+
}))();
|
|
77
|
+
this.datagramWriter = connection.datagrams.writable.getWriter();
|
|
78
|
+
this.connection.closed
|
|
79
|
+
.then(() => {
|
|
80
|
+
this.status = ConnectionStatus.Closed;
|
|
81
|
+
})
|
|
82
|
+
.catch(() => {
|
|
83
|
+
/* drop silently */
|
|
84
|
+
});
|
|
85
|
+
this.connection.ready
|
|
86
|
+
.then(() => {
|
|
87
|
+
this.status = ConnectionStatus.Connected;
|
|
88
|
+
})
|
|
89
|
+
.catch(() => {
|
|
90
|
+
/* drop silently */
|
|
91
|
+
});
|
|
92
|
+
readDatagrams(this.connection, this.id, onDatagram);
|
|
93
|
+
}
|
|
94
|
+
waitForConnected() {
|
|
95
|
+
return Promise.race([
|
|
96
|
+
this.connection.ready,
|
|
97
|
+
this.connection.closed.then(() => {
|
|
98
|
+
throw new Error('connection closed');
|
|
99
|
+
}),
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
102
|
+
close() {
|
|
103
|
+
this.closeController.abort('request');
|
|
104
|
+
}
|
|
105
|
+
joinRoom(token, roomId, userData, position) {
|
|
106
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
107
|
+
try {
|
|
108
|
+
if (this.closed)
|
|
109
|
+
return 'closed';
|
|
110
|
+
const stream = yield this.connection
|
|
111
|
+
.createBidirectionalStream()
|
|
112
|
+
.catch(() => 'closed');
|
|
113
|
+
if (stream === 'closed')
|
|
114
|
+
return 'closed';
|
|
115
|
+
const rpc = new TypedRpcConnection(new RpcConnection(stream), RoomCommandsRpc, RoomNotificationsRpc);
|
|
116
|
+
const response = yield rpc.request('Hello', {
|
|
117
|
+
stream: 'room',
|
|
118
|
+
token: String(token),
|
|
119
|
+
room_id: roomId,
|
|
120
|
+
user_data: userData,
|
|
121
|
+
position,
|
|
122
|
+
});
|
|
123
|
+
if (response === 'closed' || response.type === 'Failure')
|
|
124
|
+
return response;
|
|
125
|
+
return success(rpc.rpcConnection);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
return failure(`JoinRoom failed; ${error}`);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
getReconnectToken(peerId) {
|
|
133
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
134
|
+
const response = yield this._Request('RequestReconnectToken', {
|
|
135
|
+
peer_id: peerId,
|
|
136
|
+
});
|
|
137
|
+
if (response.type === 'Failure')
|
|
138
|
+
return response;
|
|
139
|
+
return Token.parse(response.value);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
healthCheck() {
|
|
143
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
144
|
+
const response = yield this._Request('Ping', {}, HealthCheckTimeout);
|
|
145
|
+
const success = response !== 'timeout' && response.type === 'Success';
|
|
146
|
+
if (!success)
|
|
147
|
+
this.close();
|
|
148
|
+
return success;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
sendDatagram(datagram) {
|
|
152
|
+
try {
|
|
153
|
+
this.datagramWriter.write(datagram);
|
|
154
|
+
}
|
|
155
|
+
catch (_a) {
|
|
156
|
+
/* drop silently */
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
startLocalVideo(mediaId, track) {
|
|
160
|
+
throw new Error('not supported.');
|
|
161
|
+
}
|
|
162
|
+
stopLocalVideo(mediaId) {
|
|
163
|
+
throw new Error('not supported.');
|
|
164
|
+
}
|
|
165
|
+
startRemoteVideo(mediaId) {
|
|
166
|
+
throw new Error('not supported.');
|
|
167
|
+
}
|
|
168
|
+
stopRemoteVideo(mediaId) {
|
|
169
|
+
throw new Error('not supported.');
|
|
170
|
+
}
|
|
171
|
+
_Request(name, properties, timeout) {
|
|
172
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
173
|
+
const result = yield (timeout === undefined
|
|
174
|
+
? this.rpc.request(name, properties)
|
|
175
|
+
: this.rpc.request(name, properties, timeout));
|
|
176
|
+
if (result === 'closed')
|
|
177
|
+
return failure('connection lost');
|
|
178
|
+
return result;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function readDatagrams(connection, connectionId, onDatagram) {
|
|
183
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
184
|
+
try {
|
|
185
|
+
const reader = connection.datagrams.readable.getReader();
|
|
186
|
+
for (;;) {
|
|
187
|
+
const read = yield reader.read();
|
|
188
|
+
if (read.done)
|
|
189
|
+
break;
|
|
190
|
+
onDatagram(connectionId, read.value);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch (_a) {
|
|
194
|
+
/* drop silently */
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|