UNPKG

@4players/odin

Version:

A cross-platform SDK enabling developers to integrate real-time VoIP chat technology into their projects

298 lines (297 loc) 12.4 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.OdinClient = void 0; const types_1 = require("./types"); const audio_1 = require("./audio"); const rtc_handler_1 = require("./rtc-handler"); const room_1 = require("./room"); const utils_1 = require("./utils"); const worker_1 = require("./worker"); /** * Class providing static methods to handle ODIN client connections. */ 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 types_1.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 = (0, utils_1.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(worker_1.workerScript); this._rtcHandler = new rtc_handler_1.OdinRtcHandler(this._worker, rtc); this._audioService = audio_1.OdinAudioService.setInstance(this._worker, this._rtcHandler.audioChannel, audioContexts); } const tokenClaims = (0, utils_1.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 (0, utils_1.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 room_1.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); } } exports.OdinClient = OdinClient; /** * 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', };