UNPKG

@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
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', };