UNPKG

@4players/odin

Version:

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

716 lines (715 loc) 28.3 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.OdinRoom = void 0; const types_1 = require("./types"); const audio_1 = require("./audio"); const peer_1 = require("./peer"); const media_1 = require("./media"); const stream_1 = require("./stream"); const utils_1 = require("./utils"); const schema_types_1 = require("./schema-types"); /** * Class describing an `OdinRoom`. */ class OdinRoom { /** * Creates a new `OdinRoom` instance. * * @param _id The ID of the new room * @param _user_id The user ID specified in the authentication token * @param _address The address of the ODIN SFU this room lives on * @param _mainStream The main stream connection this room is based on * @ignore */ constructor(_id, _user_id, _address, _mainStream) { this._id = _id; this._user_id = _user_id; this._address = _address; this._mainStream = _mainStream; /** * An instance of `EventTarget` for handling events related to this room. */ this._eventTarget = new EventTarget(); /** * The current connection state of the room, defaulting to 'disconnected'. */ this._connectionState = 'disconnected'; /** * The arbitrary user data for the room. */ this._data = new Uint8Array(); /** * A Map storing all remote `OdinPeer` instances within the room, using the peer ID as the key. */ this._remotePeers = new Map(); /** * The customer identifier to which this room is assigned. */ this._customer = '<no customer>'; const audioService = audio_1.OdinAudioService.getInstance(); if (audioService) { this._audioService = audioService; this._audioService.room = this; } } /** * The ID of the room. */ get id() { return this._id; } /** * The customer identifier this room is assigned to. */ get customer() { return this._customer; } /** * The arbitrary user data of the room. */ get data() { return this._data; } /** * The current state of the room stream connection. */ get connectionState() { return this._connectionState; } /** * Update the connection state of the room. */ set connectionState(state) { const oldState = this._connectionState; this._connectionState = state; if (oldState !== state) { this.eventTarget.dispatchEvent(new types_1.OdinEvent('ConnectionStateChanged', { oldState, newState: state })); if (state === 'disconnected') { this.eventTarget.dispatchEvent(new types_1.OdinEvent('Left', { room: this, })); } } } /** * An instance of your own `OdinPeer` in the room. */ get ownPeer() { return this._ownPeer; } /** * A map of all remote `OdinPeer` instances in the room using the peer ID as index. */ get remotePeers() { return this._remotePeers; } /** * The current three-dimensional position of our own `OdinPeer` in the room. */ get position() { return this._position; } /** * An event target handler for the room. * * @ignore */ get eventTarget() { return this._eventTarget; } /** * The address of the voice server this room is living on. */ get serverAddress() { return this._address; } /** * Joins the room and returns your own peer instance after the room was successfully joined. * * @param userData Optional user data to set for the peer when connecting * @param position Optional coordinates to set the three-dimensional position of the peer in the room when connecting * @returns A promise of the own OdinPeer which yields when the room was joined */ join(userData, position) { return __awaiter(this, void 0, void 0, function* () { this.connectionState = 'connecting'; if (!position) { position = [0.0, 0.0, 0.0]; } if (!userData) { userData = new Uint8Array(); } const streamUrl = `wss://${this._address}/room`; let streamId; try { try { const result = (yield this._mainStream.request('JoinRoom', { room_id: this.id, user_data: userData, position, })); streamId = result.stream_id; } catch (e) { throw new Error('RPC JoinRoom failed\n' + e); } if (!streamId) throw new Error('No stream ID received\n'); try { this._roomStream = yield (0, utils_1.openStream)(`${streamUrl}?${streamId}`, this.streamHandler.bind(this)); this._roomStream.onclose = () => { this.disconnect(); }; } catch (e) { throw new Error('Failed to open room stream\n' + e); } yield new Promise((resolve, reject) => this.addEventListener('ConnectionStateChanged', (connection) => { if (connection.payload.newState === 'connected') { resolve(); } })); this._ownPeer.data = userData; this.eventTarget.dispatchEvent(new types_1.OdinEvent('PeerJoined', { room: this, peer: this._ownPeer })); this.eventTarget.dispatchEvent(new types_1.OdinEvent('Joined', { room: this })); return this._ownPeer; } catch (e) { this.connectionState = 'error'; throw new Error(`Failed to join room '${this.id}'\n` + e); } }); } /** * Changes the active capture stream (e.g. when switching to another input device). * * @param mediaStream The capture stream of the input device */ changeMediaStream(mediaStream) { return __awaiter(this, void 0, void 0, function* () { if (!this._audioService) { throw new Error('Unable to change media stream; audio service is not available\n'); } else if (this.connectionState !== 'connected') { throw new Error('Unable to change media stream; room is not connected\n'); } try { yield this._audioService.updateInputStream(mediaStream); } catch (e) { throw new Error('Failed to change media stream\n' + e); } }); } /** * Creates a new local media using the specified stream. * * @param mediaStream The capture stream of the input device * @param audioSettings Optional audio settings like VAD or master volume used to initialize audio * @returns A promise of the newly created OdinMedia */ createMedia(mediaStream, audioSettings) { return __awaiter(this, void 0, void 0, function* () { if (!this._audioService) { throw new Error('Unable to create new media; audio service is not available\n'); } else if (this.connectionState !== 'connected') { throw new Error('Unable to create new media; room is not connected\n'); } else if (!this._ownPeer) { throw new Error('Unable to create new media; own peer information is not available\n'); } if (audioSettings) { this._audioService.setVoiceProcessingConfig(audioSettings); } yield this._audioService.updateInputStream(mediaStream); const newMedia = this._ownPeer.createMedia(); this._ownPeer.eventTarget.dispatchEvent(new types_1.OdinEvent('MediaStarted', { room: this, peer: this._ownPeer, media: newMedia, })); this.eventTarget.dispatchEvent(new types_1.OdinEvent('MediaStarted', { room: this, peer: this._ownPeer, media: newMedia, })); return newMedia; }); } /** * Adds a local media stream to the room. * * @param media The media instance to be added * @ignore */ addMedia(media) { var _a; return __awaiter(this, void 0, void 0, function* () { if (media.remote) { throw new Error('Unable to add new media; media is owned by a remote peer\n'); } yield ((_a = this._roomStream) === null || _a === void 0 ? void 0 : _a.request('StartMedia', { media_id: media.id, properties: {}, })); }); } /** * Removes a local media stream from the room. * * @param media The media instance to be removed * @ignore */ removeMedia(media) { var _a; return __awaiter(this, void 0, void 0, function* () { if (media.remote) { throw new Error('Unable to remove media; media is owned by a remote peer\n'); } yield ((_a = this._roomStream) === null || _a === void 0 ? void 0 : _a.request('StopMedia', { media_id: media.id, properties: {}, })); }); } /** * Pauses a media stream and stops receiving data on it. * * @param media The remote media instance or ID to be paused * @ignore */ pauseMedia(media) { var _a; return __awaiter(this, void 0, void 0, function* () { if (!media.remote) { throw new Error('Unable to pause media; media is owned by a local peer\n'); } else if (media.paused) { throw new Error('Unable to pause media; media is already paused\n'); } yield ((_a = this._roomStream) === null || _a === void 0 ? void 0 : _a.request('PauseMedia', { media_id: media.id, })); media.paused = true; }); } /** * Pauses a media stream and stops receiving data on it. * * @param media The remote media instance to be paused * @ignore */ resumeMedia(media) { var _a; return __awaiter(this, void 0, void 0, function* () { if (!media.remote) { throw new Error('Unable to resume media; media is owned by a local peer\n'); } else if (!media.paused) { throw new Error('Unable to resume media; media is already resumed\n'); } yield ((_a = this._roomStream) === null || _a === void 0 ? void 0 : _a.request('ResumeMedia', { media_id: media.id, })); media.paused = false; }); } /** * Returns the `OdinMedia` instance matching the specified ID. * * @param id The media ID to search for * @returns The media instance if available */ getMediaById(id) { var _a; return (_a = this._audioService) === null || _a === void 0 ? void 0 : _a.getMedia(id); } /** * Returns the `OdinPeer` instance matching the specified ID. * * @param id The peer ID to search for * @returns The peer instance if available */ getPeerById(id) { return this._ownPeer.id === id ? this._ownPeer : this._remotePeers.get(id); } /** * Updates the three-dimensional position of our own `OdinPeer` in the room to apply server-side culling. * * @param offsetX The new X coordinate for the peers position in the room * @param offsetY The new Y coordinate for the peers position in the room * @param offsetZ The new Z coordinate for the peers position in the room */ setPosition(offsetX, offsetY, offsetZ) { var _a; this._position = [offsetX, offsetY, offsetZ]; if (this._connectionState === 'connected') { (_a = this._roomStream) === null || _a === void 0 ? void 0 : _a.request('SetPeerPosition', { position: [offsetX, offsetY, offsetZ], }); } } /** * Sends updated user data of your own peer to the server. */ flushOwnPeerDataUpdate() { var _a; return __awaiter(this, void 0, void 0, function* () { yield ((_a = this._roomStream) === null || _a === void 0 ? void 0 : _a.request('UpdatePeer', { user_data: this.ownPeer.data, })); }); } /** * Sends a message with arbitrary data to all peers in the room or optionally to a list of specified peers. * * @param message Byte array of arbitrary data to send * @param targetPeerIds Optional list of target peer IDs */ sendMessage(message, targetPeerIds) { var _a; return __awaiter(this, void 0, void 0, function* () { const params = { message }; if (targetPeerIds) { params.target_peer_ids = targetPeerIds; } yield ((_a = this._roomStream) === null || _a === void 0 ? void 0 : _a.request('SendMessage', params)); }); } /** * Leaves the room and closes the connection to the server. * * @ignore */ disconnect(state = 'disconnected') { var _a; this._remotePeers.clear(); (_a = this._roomStream) === null || _a === void 0 ? void 0 : _a.close(); this.connectionState = state; } /** * Internal handler for room stream events. * * @private */ streamHandler(method, params) { const handler = (0, stream_1.makeHandler)(schema_types_1.EVENT_SCHEMAS, { RoomUpdated(params, room) { return __awaiter(this, void 0, void 0, function* () { for (const update of params.updates) { yield room.roomUpdated(update); } }); }, PeerUpdated(params, room) { return __awaiter(this, void 0, void 0, function* () { room.peerUpdated(params); }); }, MessageReceived(params, room) { return __awaiter(this, void 0, void 0, function* () { const peer = room._remotePeers.get(params.sender_peer_id); const payload = { room, senderId: params.sender_peer_id, message: params.message, }; peer === null || peer === void 0 ? void 0 : peer.eventTarget.dispatchEvent(new types_1.OdinEvent('MessageReceived', payload)); room.eventTarget.dispatchEvent(new types_1.OdinEvent('MessageReceived', payload)); }); }, }, this); handler(method, params); } /** * Internal handler for room updates. * * @private */ roomUpdated(roomUpdate) { return __awaiter(this, void 0, void 0, function* () { switch (roomUpdate.kind) { case 'Joined': { if (!roomUpdate.room || !roomUpdate.own_peer_id || !roomUpdate.media_ids) { throw Error(`The room update of kind ${roomUpdate.kind} is missing fields.`); } this._data = roomUpdate.room.user_data; this._customer = roomUpdate.room.customer; this._ownPeer = new peer_1.OdinPeer(this._roomStream, roomUpdate.own_peer_id, this._user_id, false); this._ownPeer.setFreeMediaIds(roomUpdate.media_ids); for (const remotePeer of roomUpdate.room.peers) { const peer = this.addRemotePeer(remotePeer.id, remotePeer.user_id, remotePeer.medias, remotePeer.user_data); this.eventTarget.dispatchEvent(new types_1.OdinEvent('PeerJoined', { room: this, peer })); peer.medias.forEach((media) => { peer.eventTarget.dispatchEvent(new types_1.OdinEvent('MediaStarted', { room: this, peer, media })); this.eventTarget.dispatchEvent(new types_1.OdinEvent('MediaStarted', { room: this, peer, media })); }); } this.connectionState = 'connected'; break; } case 'UserDataChanged': { if (!roomUpdate.user_data) { throw Error(`The room update of kind ${roomUpdate.kind} is missing fields.`); } this._data = roomUpdate.user_data; this.eventTarget.dispatchEvent(new types_1.OdinEvent('UserDataChanged', { room: this })); break; } case 'PeerJoined': { if (!roomUpdate.peer) { throw Error(`The room update of kind ${roomUpdate.kind} is missing fields.`); } const peer = this.addRemotePeer(roomUpdate.peer.id, roomUpdate.peer.user_id, roomUpdate.peer.medias, roomUpdate.peer.user_data); if (peer) { this.eventTarget.dispatchEvent(new types_1.OdinEvent('PeerJoined', { room: this, peer })); peer.medias.forEach((media) => { peer.eventTarget.dispatchEvent(new types_1.OdinEvent('MediaStarted', { room: this, peer, media })); this.eventTarget.dispatchEvent(new types_1.OdinEvent('MediaStarted', { room: this, peer, media })); }); } break; } case 'PeerLeft': { if (!roomUpdate.peer_id) { throw Error(`The room update of kind ${roomUpdate.kind} is missing fields.`); } yield this.removePeer(roomUpdate.peer_id); break; } } }); } /** * Internal handler for peer updates. * * @private */ peerUpdated(update) { var _a; const peer = this._remotePeers.get(update.peer_id); if (!peer) return; switch (update.kind) { case 'MediaStarted': { if (!update.media) { throw Error(`The peer update of kind ${update.kind} is missing fields.`); } else if (((_a = update.media.properties) === null || _a === void 0 ? void 0 : _a.kind) === 'video') { // support for video medias will be added in a future sdk releases return; } const media = new media_1.OdinMedia(update.media.id, update.peer_id, true); if (media.paused) { // audio medias are unpaused by default so we only set this if the media is explicitly announced as paused media.paused = true; } peer.addMedia(media); peer.eventTarget.dispatchEvent(new types_1.OdinEvent('MediaStarted', { room: this, peer, media })); this.eventTarget.dispatchEvent(new types_1.OdinEvent('MediaStarted', { room: this, peer, media })); break; } case 'MediaStopped': { if (!update.media_id) { throw Error(`The peer update of kind ${update.kind} is missing fields.`); } const media = peer.medias.get(update.media_id); peer.removeMediaById(update.media_id); if (media) { peer.eventTarget.dispatchEvent(new types_1.OdinEvent('MediaStopped', { room: this, peer, media })); this.eventTarget.dispatchEvent(new types_1.OdinEvent('MediaStopped', { room: this, peer, media })); } break; } case 'UserDataChanged': { if (!update.user_data) { throw Error(`The peer update of kind ${update.kind} is missing fields.`); } peer.data = update.user_data; peer.eventTarget.dispatchEvent(new types_1.OdinEvent('UserDataChanged', { room: this, peer })); this.eventTarget.dispatchEvent(new types_1.OdinEvent('PeerUserDataChanged', { room: this, peer })); break; } } } /** * Change the global master volume for the room (should be between 0 and 2). * * @param volume The new volume */ changeVolume(volume) { if (!this._audioService) return; this._audioService.audioWorker.postMessage({ type: 'set_volume', media_id: 0, value: volume, }); } /** * Creates a new peer instance and adds it to the room. * * @param peerId The ID of the peer * @param userId The identifier of the peer specified during authentication * @param medias A list of media IDs to initialize for the peer * @param data The user data for the peer */ addRemotePeer(peerId, userId, medias, data) { if (peerId === this._ownPeer.id) { throw new Error('Unable to add remote peer; invalid type\n'); } const peer = new peer_1.OdinPeer(this._roomStream, peerId, userId, true); peer.data = data; medias.forEach((media) => { var _a; if (((_a = media.properties) === null || _a === void 0 ? void 0 : _a.kind) === 'video') { // support for video medias will be added in a future sdk releases return; } const mediaInstance = new media_1.OdinMedia(media.id, peerId, true); if (media.paused) { // audio medias are unpaused by default so we only set this if the media is explicitly announced as paused mediaInstance.paused = true; } peer.addMedia(mediaInstance); }); this._remotePeers.set(peerId, peer); return peer; } /** * Removes the peer with the specified ID from the room. * * @param peerId The id of the peer to remove */ removePeer(peerId) { return __awaiter(this, void 0, void 0, function* () { if (peerId === this._ownPeer.id) { return; } const peer = this._remotePeers.get(peerId); if (!peer) { return; } for (const media of peer.medias.entries()) { peer.eventTarget.dispatchEvent(new types_1.OdinEvent('MediaStopped', { room: this, peer, media: media[1] })); this.eventTarget.dispatchEvent(new types_1.OdinEvent('MediaStopped', { room: this, peer, media: media[1] })); yield media[1].stop(); peer.medias.delete(media[1].id); } this.eventTarget.dispatchEvent(new types_1.OdinEvent('PeerLeft', { room: this, peer })); this._remotePeers.delete(peerId); return peer; }); } /** * Disables RNN-based voice activity detection. */ disableVAD() { if (!this._audioService) return; const config = this._audioService.getVoiceProcessingConfig(); config.voiceActivityDetection = false; this._audioService.setVoiceProcessingConfig(config); } /** * Enables RNN-based voice activity detection. */ enableVAD() { if (!this._audioService) return; const config = this._audioService.getVoiceProcessingConfig(); config.voiceActivityDetection = true; this._audioService.setVoiceProcessingConfig(config); } /** * Enables emitting of RNN-based voice activity detection statistics. */ startVADMeter() { if (!this._audioService) return; this._audioService.setVoiceProcessingStatsEnabled(true); } /** * Disables emitting of RNN-based voice activity detection statistics. */ stopVADMeter() { if (!this._audioService) return; this._audioService.setVoiceProcessingStatsEnabled(false); } /** * Updates thresholds for vice activity detection (between 0 and 1). * * @param attackProbability Voice probability value when the VAD should engage * @param releaseProbability Voice probability value when the VAD should disengage */ updateVADThresholds(attackProbability, releaseProbability) { if (!this._audioService) return; const config = this._audioService.getVoiceProcessingConfig(); config.voiceActivityDetectionAttackProbability = attackProbability; config.voiceActivityDetectionReleaseProbability = releaseProbability !== null && releaseProbability !== void 0 ? releaseProbability : attackProbability - 0.1; this._audioService.setVoiceProcessingConfig(config); } /** * Disables RNN-based voice activity detection. */ disableVolumeGate() { if (!this._audioService) return; const config = this._audioService.getVoiceProcessingConfig(); config.volumeGate = false; this._audioService.setVoiceProcessingConfig(config); } /** * Enables RNN-based voice activity detection. */ enableVolumeGate() { if (!this._audioService) return; const config = this._audioService.getVoiceProcessingConfig(); config.volumeGate = true; this._audioService.setVoiceProcessingConfig(config); } /** * Updates thresholds for the input volume gate (between -90 and 0). * * @param attackLoudness Root mean square power (dBFS) when the volume gate should engage * @param releaseLoudness Root mean square power (dBFS) when the volume gate should disengage */ updateVolumeGateThresholds(attackLoudness, releaseLoudness) { if (!this._audioService) return; const config = this._audioService.getVoiceProcessingConfig(); config.volumeGateAttackLoudness = attackLoudness; config.volumeGateReleaseLoudness = releaseLoudness !== null && releaseLoudness !== void 0 ? releaseLoudness : attackLoudness - 10; this._audioService.setVoiceProcessingConfig(config); } /** * Returns the current voice processing config for VAD and volume gate. */ getAudioSettings() { var _a; return (_a = this._audioService) === null || _a === void 0 ? void 0 : _a.getVoiceProcessingConfig(); } /** * Register to peer events from `IOdinRoomEvents`. * * @param eventName The name of the event to listen to * @param handler The callback to handle the event */ addEventListener(eventName, handler) { this._eventTarget.addEventListener(eventName, handler); } } exports.OdinRoom = OdinRoom;