@4players/odin
Version:
A cross-platform SDK enabling developers to integrate real-time VoIP chat technology into their projects
286 lines (285 loc) • 11.6 kB
JavaScript
import { __awaiter } from "tslib";
import { OdinEvent, } from './types';
import { OdinAudioService } from './audio';
import { OdinRtcHandler } from './rtc-handler';
import { OdinRoom } from './room';
import { openStream, parseJwt, setupDefaultAudioContext } from './utils';
import { workerScript } from './worker';
/**
* Class providing static methods to handle ODIN client connections.
*/
export class OdinClient {
/**
* @ignore
*/
constructor() {
throw new Error('Not allowed to instantiate OdinClient');
}
/**
* An array of available `OdinRoom` instances.
*/
static get rooms() {
return this._rooms;
}
/**
* The current state of the main stream connection.
*/
static get connectionState() {
return this._state;
}
/**
* Updates the state of the connection.
*/
static set connectionState(state) {
const oldState = this.connectionState;
this._state = state;
if (oldState !== state) {
this.eventTarget.dispatchEvent(new OdinEvent('ConnectionStateChanged', { oldState, newState: state }));
}
}
/**
* Returns the event handler of the client.
*
* @ignore
*/
static get eventTarget() {
return this._eventTarget;
}
/**
* Authenticates the client, establishes the main stream connection, sets up audio context and WebRTC connection, and finally
* creates `OdinRoom` instances.
*
* @private
*/
static connect(token, server, audioContext) {
var _a, _b;
return __awaiter(this, void 0, void 0, function* () {
if (this.connectionState === 'connected') {
return this._rooms;
}
if (typeof audioContext === 'undefined') {
if (typeof AudioContext === 'undefined') {
console.warn('AudioContext is not available on this platform; disabling ODIN audio functionality');
}
else if (typeof Worker === 'undefined') {
console.warn('Worker is not available on this platform; disabling ODIN audio functionality');
}
else {
audioContext = new AudioContext(); // create fallback
}
}
if (audioContext) {
const audioContexts = setupDefaultAudioContext(audioContext);
yield audioContexts.input.resume();
yield audioContexts.output.resume();
const rtc = new RTCPeerConnection();
rtc.onconnectionstatechange = () => {
if (rtc.connectionState === 'failed') {
console.error('Failed to establish RTC peer connection; ODIN audio functionality is disrupted');
this.connectionState = 'incomplete';
}
};
this._worker = new Worker(workerScript);
this._rtcHandler = new OdinRtcHandler(this._worker, rtc);
this._audioService = OdinAudioService.setInstance(this._worker, this._rtcHandler.audioChannel, audioContexts);
}
const tokenClaims = parseJwt(token);
this.connectionState = 'connecting';
if (!server) {
server = this.config.gatewayUrl; // use default gateway as fallback
}
else if (server.indexOf('https://') !== -1) {
server = server.replace('https://', ''); // cleanup server address for now
}
// always authenticate against gateway unless we specifically created a token for sfu audience
if (tokenClaims.aud !== 'sfu') {
const authResult = yield this.authGateway(token, `https://${server}`);
server = authResult.address;
token = authResult.token;
}
const streamUrl = `wss://${server}/main`;
try {
this._mainStream = yield openStream(streamUrl, this.mainHandler);
this._mainStream.onclose = () => {
this.disconnect();
};
this._mainStream.onerror = () => {
this.disconnect('error');
};
let roomIds = [];
try {
const mainStreamAuthResult = (yield this._mainStream.request('Authenticate', {
token,
}));
if (mainStreamAuthResult && mainStreamAuthResult.room_ids) {
roomIds = mainStreamAuthResult.room_ids;
}
}
catch (err) {
if (err.message === 'unknown method') {
console.warn('Incompatible ODIN server version detected; please update your client');
}
throw err;
}
yield ((_a = this._rtcHandler) === null || _a === void 0 ? void 0 : _a.startRtc(this._mainStream));
yield ((_b = this._audioService) === null || _b === void 0 ? void 0 : _b.setupAudio());
this._rooms = roomIds.map((roomId) => {
var _a;
return new OdinRoom(roomId, (_a = tokenClaims.uid) !== null && _a !== void 0 ? _a : '', server !== null && server !== void 0 ? server : '', this._mainStream);
});
/**
* Currently, if the connection of the room gets closed, also close the connection of the OdinClient.
* This might change, once multiple rooms will get supported.
*/
for (const room of this._rooms) {
room.addEventListener('Left', (_) => {
this.disconnect();
});
}
this.connectionState = 'connected';
return this._rooms;
}
catch (e) {
this.connectionState = 'error';
throw new Error('Failed to open main stream\n' + e);
}
});
}
/**
* Authenticates against the ODIN server and returns `OdinRoom` instances for all rooms set in the specified token.
*
* This function accepts an optional `AudioContext` parameter for audio capture. The `AudioContext` interface is a part of
* the Web Audio API that represents an audio-processing graph, which can be used to control and manipulate audio signals
* in web applications. If the `AudioContext` is not provided or explicitly set to `undefined`, we will try to create one
* internally.
*
* @param token The room token for authentication
* @param gateway The gateway to authenticate against
* @param audioContext An optional audio context to use for capture
* @returns A promise of the available rooms
*/
static initRooms(token, gateway, audioContext) {
return __awaiter(this, void 0, void 0, function* () {
try {
return yield this.connect(token, gateway, audioContext);
}
catch (e) {
throw new Error('Failed to establish connection to server\n' + e);
}
});
}
/**
* Authenticates against the ODIN server and returns an `OdinRoom` instance for the first room set in the specified token.
*
* This function accepts an optional `AudioContext` parameter for audio capture. The `AudioContext` interface is a part of
* the Web Audio API that represents an audio-processing graph, which can be used to control and manipulate audio signals
* in web applications. If the `AudioContext` is not provided or explicitly set to `undefined`, we will try to create one
* internally.
*
* @param token The room token for authentication
* @param gateway The gateway to authenticate against
* @param audioContext An optional audio context to use for capture
* @returns A promise of the first available room
*/
static initRoom(token, gateway, audioContext) {
return __awaiter(this, void 0, void 0, function* () {
const rooms = yield this.initRooms(token, gateway, audioContext);
if (rooms.length) {
return rooms[0];
}
else {
throw new Error('Failed to initialize room\n');
}
});
}
/**
* Not implemented.
*
* @private
*/
static mainHandler(_method, _params) { }
/**
* Authenticates against the gateway and returns its result.
*
* @param token The token for authentication
* @param gateway The gateway to authenticate against
* @returns A promise resolving to the authentication result
*/
static authGateway(token, gateway) {
return __awaiter(this, void 0, void 0, function* () {
let response;
try {
response = yield fetch(gateway, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'Connect',
params: {},
}),
});
}
catch (e) {
throw new Error('Failed to authenticate against gateway\n' + e);
}
const body = yield response.json();
if (body.result) {
return body.result;
}
else {
throw new Error('Failed to authenticate against gateway\nError: ' + body.error.message);
}
});
}
/**
* Disconnects from all rooms and stops all audio handling.
*/
static disconnect(state = 'disconnected') {
var _a, _b, _c, _d;
if (this.connectionState === 'disconnected' || this.connectionState === 'error') {
return; // already disconnected
}
this.connectionState = state;
this._rooms.forEach((room) => {
if (room.connectionState !== 'disconnected') {
room.disconnect();
}
});
(_a = this._audioService) === null || _a === void 0 ? void 0 : _a.stopAllAudio();
(_b = this._mainStream) === null || _b === void 0 ? void 0 : _b.close();
(_c = this._worker) === null || _c === void 0 ? void 0 : _c.terminate();
(_d = this._rtcHandler) === null || _d === void 0 ? void 0 : _d.stopRtc();
this._rooms = [];
}
/**
* Registers to client events from `IOdinClientEvents`.
*
* @param eventName The name of the event to listen to
* @param handler The callback to handle the event
*/
static addEventListener(eventName, handler) {
this._eventTarget.addEventListener(eventName, handler);
}
}
/**
* EventTarget instance to listen for and dispatch custom events.
*/
OdinClient._eventTarget = new EventTarget();
/**
* Connection state of the client to the ODIN server.
*/
OdinClient._state = 'disconnected';
/**
* Array holding the currently available `OdinRoom` instances.
*/
OdinClient._rooms = [];
/**
* Global settings for ODIN connections.
*/
OdinClient.config = {
gatewayUrl: 'gateway.odin.4players.io',
};