UNPKG

@4players/odin

Version:

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

1,046 lines (1,045 loc) 48.3 kB
var _Room_instances, _Room_id, _Room_ownPeerId, _Room_token, _Room_room, _Room_position, _Room_roomData, _Room_userData, _Room_status, _Room_peers, _Room_outputs, _Room_audioInputs, _Room_volume, _Room_previousConnectionStats, _Room_activityCb, _Room_rmsDBFSCb, _Room_connectionStatsHandler, _Room_reset, _Room_roomEventHandler, _Room_onRoomUpdate, _Room_dispatchJoined, _Room_dispatchLeft, _Room_onPeerJoined, _Room_onPeerLeft, _Room_onPeerUpdate, _Room_dispatchUserDataChanged, _Room_onMessageReceived, _Room_onRoomStatusChanged, _Room_addOwnPeer, _Room_addRemotePeer, _Room_cleanupAudioOutput, _Room_addVideoOutput, _Room_removeVideoOutput, _Room_createAudioPlayback, _Room_addRemotePeers, _Room_dispatchMediaStarted, _Room_dispatchMediaStopped, _Room_dispatchAudioOutputStarted, _Room_dispatchAudioOutputStopped, _Room_dispatchAudioInputStarted, _Room_dispatchAudioInputStopped, _Room_dispatchVideoOutputAdded, _Room_dispatchVideoOutputStopped, _Room_dispatchVideoInputStarted, _Room_dispatchVideoInputStopped, _Room_waitForConnect; import { __classPrivateFieldGet, __classPrivateFieldSet } from "tslib"; import { assert, CONNECTION_STATS_INITIAL, normalizeUrl, sleep, } from '@4players/odin-common'; import { RoomNotificationsRpc, } from '@4players/odin-common/api'; import { OdinEvent, OdinEventTarget } from '../../utils/odin-event-target'; import { ensurePlugin } from '../../api'; import { AudioOutput } from '../media/audio-output'; import { calcPlaybackVolume, calculateBytesPerSeconds, OdinError, } from '../../utils/helpers'; import { RoomToken } from '../../utils/token'; import { VideoOutput } from '../media/video-output'; import { RemotePeer } from '../peer/remote-peer'; import { LocalPeer } from '../peer/local-peer'; import { unregisterAudioOutput } from '../../media-service'; /** * Represents a room within the Odin framework, managing connections, peers, and media. * The `Room` class provides functionality to join and interact with an audio/video room, * monitor its state, handle events, and manage media streams. * * This class extends `OdinEventTarget` to handle custom event types and listeners. */ export class Room extends OdinEventTarget { constructor() { super(); _Room_instances.add(this); _Room_id.set(this, void 0); _Room_ownPeerId.set(this, 0); _Room_token.set(this, void 0); _Room_room.set(this, void 0); _Room_position.set(this, [0, 0, 0]); _Room_roomData.set(this, new Uint8Array()); _Room_userData.set(this, new Uint8Array()); _Room_status.set(this, { status: 'disconnected', }); _Room_peers.set(this, new Map()); _Room_outputs.set(this, []); _Room_audioInputs.set(this, []); _Room_volume.set(this, [1, 1]); /** * Interval how often the connection statistics are updated. */ this.connectionStatsInterval = 1000; _Room_previousConnectionStats.set(this, { ...CONNECTION_STATS_INITIAL, }); // AudioMedia EventCbs _Room_activityCb.set(this, (event) => { this.dispatchEvent(new OdinEvent('AudioActivity', event.payload)); this.onAudioActivity?.(event.payload); }); _Room_rmsDBFSCb.set(this, (event) => { this.dispatchEvent(new OdinEvent('AudioPowerLevel', event.payload)); this.onAudioPowerLevel?.(event.payload); }); _Room_connectionStatsHandler.set(this, async () => { while (this.status.status !== 'disconnected') { if (__classPrivateFieldGet(this, _Room_room, "f")) { const connectionStats = { ...__classPrivateFieldGet(this, _Room_room, "f").connectionStats, bytesReceivedLastSecond: calculateBytesPerSeconds(__classPrivateFieldGet(this, _Room_room, "f").connectionStats.bytesReceived, __classPrivateFieldGet(this, _Room_previousConnectionStats, "f").bytesReceived, this.connectionStatsInterval), bytesSentLastSecond: calculateBytesPerSeconds(__classPrivateFieldGet(this, _Room_room, "f").connectionStats.bytesSent, __classPrivateFieldGet(this, _Room_previousConnectionStats, "f").bytesSent, this.connectionStatsInterval), }; __classPrivateFieldSet(this, _Room_previousConnectionStats, { ...__classPrivateFieldGet(this, _Room_room, "f").connectionStats }, "f"); this.onConnectionStats?.(connectionStats); this.dispatchEvent(new OdinEvent('ConnectionStats', connectionStats)); } await sleep(this.connectionStatsInterval); } }); /** * The default gateway if no gateway was specified when joining the room. */ this.defaultGateway = 'https://gateway.odin.4players.io'; } /** * The id of the room. If the room was not joined, the id is undefined as * the id is provided by the token provided when joining. * * @return {string|undefined} The unique identifier of the object. */ get id() { return __classPrivateFieldGet(this, _Room_id, "f"); } /** * The latest token (inclusive reconnect tokens). * * @return {RoomToken | undefined} The current RoomToken if available, otherwise undefined. */ get token() { return __classPrivateFieldGet(this, _Room_token, "f"); } /** * Retrieves the cipher associated with the current room, if available. * * @return {Cipher|undefined} The cipher object if it exists; otherwise, undefined. */ get cipher() { return __classPrivateFieldGet(this, _Room_room, "f")?.cipher; } /** * Retrieves connection statistics for the current room. * * @return {ConnectionStats} An object containing the connection statistics, including: * - `bytesSent`: Total bytes sent. * - `bytesReceived`: Total bytes received. * - `packetsSent`: Total packets sent. * - `packetsReceived`: Total packets received. * - `rtt`: Round-trip time in milliseconds. * If the room isn't connected, returns default stats with all values set to 0. */ get connectionStats() { if (!__classPrivateFieldGet(this, _Room_room, "f")) { return { ...CONNECTION_STATS_INITIAL, }; } return __classPrivateFieldGet(this, _Room_room, "f").connectionStats; } /** * Address of the SFU (Voice Server) which was either assigned by the gateway or provided by the token when joining the room. * * @return {string} The address of the sfu if available, otherwise returns an empty string. */ get address() { if (this.token?.address) { return this.token.address; } else { return ''; } } /** * The id of the own peer. Id 0 means not connected. * * @return {number} The ID of the own peer. */ get ownPeerId() { return __classPrivateFieldGet(this, _Room_ownPeerId, "f"); } /** * A collection of peers. * * @return {Map<number, Peer>} A map containing peers with their numeric IDs as keys. */ get peers() { return __classPrivateFieldGet(this, _Room_peers, "f"); } /** * Retrieves the "own" Peer instance associated with the current `ownPeerId` while connected. * * @return {Peer | undefined} The Peer instance if it exists, or `undefined` if not found. */ get ownPeer() { const peer = this.peers.get(this.ownPeerId); if (!peer?.isRemote) { return peer; } return undefined; } get remotePeers() { const remotePeers = new Map(); for (const [id, peer] of this.peers) { if (peer.isRemote) { remotePeers.set(id, peer); } } return remotePeers; } /** * Retrieves the customer ID associated with the current token, if available. * * @return {string | undefined} The customer ID as a string if present, otherwise undefined. */ get customer() { return __classPrivateFieldGet(this, _Room_token, "f")?.customerId; } /** * Retrieves the current position as a 3D coordinate. * * @return {Array<number>} An array containing three numbers representing the x, y, and z coordinates of the position. */ get position() { return __classPrivateFieldGet(this, _Room_position, "f"); } /** * Gets the room data. * * @return {Uint8Array | undefined} The room data as a Uint8Array or undefined if no data is available. */ get roomData() { return __classPrivateFieldGet(this, _Room_roomData, "f"); } /** * Sets the user data of the own peer with the provided Uint8Array. * * @param {Uint8Array} data - The data to be set as user data. */ set userData(data) { __classPrivateFieldSet(this, _Room_userData, data, "f"); } /** * Retrieves the own user data. * * @return {Uint8Array} The user data as a Uint8Array. */ get userData() { return __classPrivateFieldGet(this, _Room_userData, "f"); } /** * Retrieves the volume of the current instance. * * @return {PlaybackVolume} The current volume value. */ get volume() { return __classPrivateFieldGet(this, _Room_volume, "f"); } /** * Retrieves the current status of the room. * * @return {RoomStatus} The current status of the room. */ get status() { return __classPrivateFieldGet(this, _Room_status, "f"); } /** * Retrieves a list of all AudioOutputs that are assigned to this room. * * @return {AudioOutput[]} An array of AudioOutput devices filtered from the outputs list. */ get audioOutputs() { return __classPrivateFieldGet(this, _Room_outputs, "f").filter((output) => output.kind === 'audio-output'); } /** * Retrieves a list of VideoOutput devices that are assigned to this room. * * This method filters the available outputs to return only those * categorized as VideoOutput devices. * * @return {VideoOutput[]} An array of VideoOutput devices. */ get videoOutputs() { return __classPrivateFieldGet(this, _Room_outputs, "f").filter((output) => output.kind === 'video-output'); } /** * Sets the volume for the audio outputs. * * @param {PlaybackVolume} value - The volume level to set. * @return {Promise<void>} A promise that resolves when the volume is set for all audio outputs. */ async setVolume(value) { __classPrivateFieldSet(this, _Room_volume, typeof value === 'number' ? [value, value] : value, "f"); for (const output of this.audioOutputs) { await output.setVolume(); } } /** * Retrieves the available audio input devices that are assigned to this room. * * @return {AudioInput[]} An array of AudioInput objects representing the currently available audio inputs. */ get audioInputs() { const inputs = []; for (const inputRef of __classPrivateFieldGet(this, _Room_audioInputs, "f")) { const input = inputRef.deref(); if (input) { inputs.push(input); } } return inputs; } /** * Retrieves an AudioOutput device by its media ID. * * @param {number} mediaId - The media id of the desired AudioOutput. * @return {AudioOutput | undefined} The AudioOutput object if found, otherwise undefined. */ getAudioOutputById(mediaId) { return this.audioOutputs.find((output) => output.mediaId === mediaId); } /** * Retrieves an VideoOutput device by its media ID. * * @param {number} mediaId - The media id of the desired VideoOutput. * @return {VideoOutput | undefined} The VideoOutput object if found, otherwise undefined. */ getVideoOutputById(mediaId) { return this.videoOutputs.find((output) => output.mediaId === mediaId); } /** * Adds a vVideoInput to start streaming. * * @param {VideoInput} input - The VideoInput source to be added. * @return {Promise<void>} Resolves when the VideoInput has been successfully added to the room. */ async addVideoInput(input) { const plugin = await ensurePlugin(); assert(plugin && this.ownPeer, "Can't add an VideoInput, no AudioPlugin or Peer initialized"); if (__classPrivateFieldGet(this, _Room_room, "f")) { await __classPrivateFieldGet(this, _Room_room, "f").link(input.capture); } __classPrivateFieldGet(this, _Room_instances, "m", _Room_dispatchVideoInputStarted).call(this, input); } /** * Adds an AudioInput to start streaming. * * @param {AudioInput} input - The AudioInput instance to be added. * @return {Promise<void>} Resolves when the AudioInput is successfully added. * @throws Will throw an error if no audio plugin is available. */ async addAudioInput(input) { await ensurePlugin(); if (__classPrivateFieldGet(this, _Room_audioInputs, "f").find((inputRef) => inputRef.deref() === input)) { return; } __classPrivateFieldGet(this, _Room_audioInputs, "f").push(new WeakRef(input)); input.addEventListener('Activity', __classPrivateFieldGet(this, _Room_activityCb, "f")); input.addEventListener('PowerLevel', __classPrivateFieldGet(this, _Room_rmsDBFSCb, "f")); if (this.ownPeer) { input.addEventListener('Activity', this.ownPeer.audioActivityHandler); input.addEventListener('PowerLevel', this.ownPeer.rmsDBFSHandler); } if (__classPrivateFieldGet(this, _Room_room, "f")) { __classPrivateFieldGet(this, _Room_room, "f").link(input.capture); } __classPrivateFieldGet(this, _Room_instances, "m", _Room_dispatchAudioInputStarted).call(this, input); } /** * Removes the specified AudioInput and stops streaming. * * @param {AudioInput} input - The AudioInput instance to be removed. * @return {void} Does not return a value. */ removeAudioInput(input) { if (__classPrivateFieldGet(this, _Room_room, "f")) { __classPrivateFieldGet(this, _Room_room, "f").unlink(input.capture); } input.removeEventListener('Activity', __classPrivateFieldGet(this, _Room_activityCb, "f")); input.removeEventListener('PowerLevel', __classPrivateFieldGet(this, _Room_rmsDBFSCb, "f")); if (this.ownPeer) { input.removeEventListener('Activity', this.ownPeer.audioActivityHandler); input.removeEventListener('PowerLevel', this.ownPeer.rmsDBFSHandler); } __classPrivateFieldSet(this, _Room_audioInputs, __classPrivateFieldGet(this, _Room_audioInputs, "f").filter((inputRef) => { const deferredInput = inputRef.deref(); return deferredInput !== input; }), "f"); __classPrivateFieldGet(this, _Room_instances, "m", _Room_dispatchAudioInputStopped).call(this, input); } /** * Removes the specified VideoInput and stops streaming. * * @param {VideoInput} input - The VideoInput instance to be removed. * @return {void} Does not return a value. */ removeVideoInput(input) { if (__classPrivateFieldGet(this, _Room_room, "f")) { __classPrivateFieldGet(this, _Room_room, "f").unlink(input.capture); } __classPrivateFieldGet(this, _Room_instances, "m", _Room_dispatchVideoInputStopped).call(this, input); } /** * Starts the specified VideoOutput. After the VideoOutput was started, its MediaStream will be available. * * @param {VideoOutput} output - The VideoOutput object to start. The output must already exist in the room. * @return {Promise<void>} A promise that resolves when the video output is successfully started. */ async startVideoOutput(output) { assert(__classPrivateFieldGet(this, _Room_outputs, "f").find((videoOutput) => videoOutput === output), "Can't start the VideoOutput because the it doesn't exists on the room"); await this.resumeMedia(output.mediaId); await __classPrivateFieldGet(this, _Room_room, "f")?.link(output.playback); } /** * Stops the VideoOutput and stops streaming. * * @param {VideoOutput} output - The VideoOutput object containing the media and playback information. * @return {void} No return value. */ stopVideoOutput(output) { if (__classPrivateFieldGet(this, _Room_room, "f")) { __classPrivateFieldGet(this, _Room_room, "f").unlink(output.playback); } } /** * Sets the audio output device to the specified device. * Currently, the same audio output is used across all rooms (in the same plugin that was used). * * @param {DeviceParameters} [device={}] - The parameters of the audio output device to be set. * @return {Promise<void>} A promise that resolves when the audio output device has been successfully set. */ async setAudioOutputDevice(device = {}) { const plugin = await ensurePlugin(); await plugin.setOutputDevice(device); } /** * Updates the position of the peer within the room. * * @param {number} offsetX - The X-coordinate of the new position. * @param {number} offsetY - The Y-coordinate of the new position. * @param {number} offsetZ - The Z-coordinate of the new position. * @return {Promise<void>} A promise that resolves when the position has been successfully updated. * @throws {Error} If the room is not joined. */ async setPosition(offsetX, offsetY, offsetZ) { __classPrivateFieldSet(this, _Room_position, [offsetX, offsetY, offsetZ], "f"); assert(__classPrivateFieldGet(this, _Room_room, "f") && this.status.status === 'joined', "Can't set the position, room not joined."); await this.request('SetPeerPosition', { position: [offsetX, offsetY, offsetZ], }); } /** * Joins the room using the provided token and options. * * @param {string} token - The token that was generated with an access key and provided to the app. * @param {JoinParams} [options] - An optional object containing additional parameters for joining the room. * @param {string} [options.gateway] - The gateway URL to connect to. * @param {string} [options.roomId] - Specifies the unique ID of the room to join. Only useful for multi-room scenarios. * @param {object} [options.userData] - Own user data that other peers can see after the room was joined. * @param {boolean} [options.position] - Indicates whether to use a specific joining position. * @param {object} [options.cipher] - Configuration for encryption settings, if applicable. * @param {Transport} [options.transport] - Specifies the transport layer configuration. * @return {Promise<void>} Resolves when the join request is successful and the room is connected. * @throws {OdinError} Throws an error if the room joining process fails. */ async join(token, options) { __classPrivateFieldSet(this, _Room_token, new RoomToken(token), "f"); const url = normalizeUrl(options?.gateway ?? this.defaultGateway); assert(url.type === 'Success', 'Joining the room failed, invalid url specified'); const joinParams = { url: url.value.toString(), token, onEvent: __classPrivateFieldGet(this, _Room_instances, "m", _Room_roomEventHandler).bind(this), position: options?.position ?? __classPrivateFieldGet(this, _Room_position, "f"), cipher: options?.cipher, transport: options?.transport, }; if (options?.roomId) { joinParams.roomId = options.roomId; } if (options?.userData) { joinParams.userData = options.userData; this.userData = options.userData; } this.status = { status: 'joining', }; let room; try { const plugin = await ensurePlugin(); room = plugin.joinRoom(joinParams); } catch (e) { const reason = `Joining the room failed, reason was: ${typeof e === 'string' ? e : 'No info provided.'}`; this.status = { status: 'disconnected', reason, }; throw new OdinError(reason); } __classPrivateFieldSet(this, _Room_id, __classPrivateFieldGet(this, _Room_token, "f").rooms[0], "f"); if (options?.roomId) { __classPrivateFieldSet(this, _Room_id, options.roomId, "f"); } __classPrivateFieldSet(this, _Room_room, room, "f"); await __classPrivateFieldGet(this, _Room_instances, "m", _Room_waitForConnect).call(this); __classPrivateFieldGet(this, _Room_connectionStatsHandler, "f").call(this).then(); } /** * Disconnects the room resets the connection state. * * @param {string} [reason='Room left by user request'] - The reason for leaving the room. * @return {void} Does not return a value. */ leave(reason = 'Room left by user request') { this.status = { status: 'disconnected', reason, }; __classPrivateFieldSet(this, _Room_id, undefined, "f"); __classPrivateFieldGet(this, _Room_room, "f")?.close(); __classPrivateFieldGet(this, _Room_instances, "m", _Room_reset).call(this); } /** * Flushes the current user's data by sending an update request to the server. * Ensures the room is joined before proceeding with the operation. * * @return {Promise<void>} A promise that resolves once the user data update request is completed. */ async flushUserData() { assert(__classPrivateFieldGet(this, _Room_status, "f").status === 'joined' && __classPrivateFieldGet(this, _Room_room, "f"), "Can't flush user data, not connected."); await this.request('UpdatePeer', { user_data: __classPrivateFieldGet(this, _Room_userData, "f"), }); } /** * Send a rpc to the SFU. * * @param {Name} name - The name of the command to be executed. Must be a key of RoomCommands. * @param {Infer<RoomCommands[Name]['request']>} properties - The properties required for the specified command. * @return {Promise<void>} A promise that resolves when the request is successfully processed. */ async request(name, properties) { await __classPrivateFieldGet(this, _Room_room, "f")?.request(name, properties); } /** * Sends a message with arbitrary data to all peers in the room or optionally to a list of specified peers. * * @param {Uint8Array} message - The message content to be sent. * @param {?number[]} [targetPeerIds] - An optional array of peer IDs to specify the message recipients. * If not provided, the message will be broadcasted to all peers. * @return {Promise<void>} A promise that resolves once the message has been successfully sent. * If the client is not connected or not in the room, an error will be thrown. */ async sendMessage(message, targetPeerIds) { assert(__classPrivateFieldGet(this, _Room_status, "f").status === 'joined' && __classPrivateFieldGet(this, _Room_room, "f"), "Can't send the message, not connected."); const params = { message }; if (Array.isArray(targetPeerIds) && targetPeerIds.length > 0) { params.target_peer_ids = targetPeerIds; } await this.request('SendMessage', params); } /** * Resumes a remote media on the audio server that was paused before. * The MediaID can be found at the AudioOutput of a peer. * * @param {number} mediaId - The unique identifier of the media to resume. * @return {Promise<void>} A promise that resolves when the media is successfully resumed or rejects with an error. */ async resumeMedia(mediaId) { assert(__classPrivateFieldGet(this, _Room_room, "f"), `Can't resume the media with id ${mediaId}`); try { await this.request('ResumeMedia', { media_id: mediaId, }); } catch (e) { console.warn(`Failed to resume media ${mediaId} on the server:`, e); throw e; } } /** * Pauses a remote media on the audio server that was paused before. * The MediaID can be found at the AudioOutput of a peer. * @TODO Add a pause method to peers. * * @param {number} mediaId - The id of the media to be paused. * @return {Promise<void>} Resolves when the media is successfully paused or logs a warning if an error occurs. */ async pauseMedia(mediaId) { assert(__classPrivateFieldGet(this, _Room_room, "f"), `Can't pause the media with id ${mediaId}`); try { await this.request('PauseMedia', { media_id: mediaId, }); } catch (e) { console.warn(`Failed to pause media ${mediaId} on the server:`, e); throw e; } } getPlugin() { return ensurePlugin(); } set status(state) { const oldState = __classPrivateFieldGet(this, _Room_status, "f"); __classPrivateFieldSet(this, _Room_status, state, "f"); if (oldState.status !== state.status) { const payload = { oldState, newState: state, }; this.dispatchEvent(new OdinEvent('RoomStatusChanged', payload)); this.onStatusChanged?.(payload); } } async removeAudioOutput(output) { if (__classPrivateFieldGet(this, _Room_room, "f")) { __classPrivateFieldGet(this, _Room_room, "f").unlink(output.playback); } output.removeEventListener('Activity', __classPrivateFieldGet(this, _Room_activityCb, "f")); output.removeEventListener('Activity', output.peer.audioActivityHandler); await this.setVolume(__classPrivateFieldGet(this, _Room_volume, "f")); __classPrivateFieldSet(this, _Room_outputs, __classPrivateFieldGet(this, _Room_outputs, "f").filter((audioOutput) => audioOutput !== output), "f"); } async addAudioOutput(output) { if (this.audioOutputs.find((o) => o === output)) { return output; } output.addEventListener('Activity', __classPrivateFieldGet(this, _Room_activityCb, "f")); output.addEventListener('Activity', output.peer.audioActivityHandler); if (__classPrivateFieldGet(this, _Room_room, "f")) { await __classPrivateFieldGet(this, _Room_room, "f").link(output.playback); } __classPrivateFieldGet(this, _Room_outputs, "f").push(output); await this.setVolume(__classPrivateFieldGet(this, _Room_volume, "f")); return output; } } _Room_id = new WeakMap(), _Room_ownPeerId = new WeakMap(), _Room_token = new WeakMap(), _Room_room = new WeakMap(), _Room_position = new WeakMap(), _Room_roomData = new WeakMap(), _Room_userData = new WeakMap(), _Room_status = new WeakMap(), _Room_peers = new WeakMap(), _Room_outputs = new WeakMap(), _Room_audioInputs = new WeakMap(), _Room_volume = new WeakMap(), _Room_previousConnectionStats = new WeakMap(), _Room_activityCb = new WeakMap(), _Room_rmsDBFSCb = new WeakMap(), _Room_connectionStatsHandler = new WeakMap(), _Room_instances = new WeakSet(), _Room_reset = function _Room_reset() { for (const output of __classPrivateFieldGet(this, _Room_outputs, "f")) { if (output.kind === 'audio-output') { __classPrivateFieldGet(this, _Room_instances, "m", _Room_dispatchAudioOutputStopped).call(this, output); } if (output.kind === 'video-output') { __classPrivateFieldGet(this, _Room_instances, "m", _Room_dispatchVideoOutputStopped).call(this, output); } } __classPrivateFieldSet(this, _Room_outputs, [], "f"); for (const [id, peer] of __classPrivateFieldGet(this, _Room_peers, "f")) { if (!peer.isRemote) { for (const input of peer.audioInputs) { input.removeEventListener('Activity', peer.audioActivityHandler); input.removeEventListener('PowerLevel', peer.rmsDBFSHandler); } } __classPrivateFieldGet(this, _Room_instances, "m", _Room_onPeerLeft).call(this, id); } __classPrivateFieldSet(this, _Room_peers, new Map(), "f"); __classPrivateFieldSet(this, _Room_ownPeerId, 0, "f"); }, _Room_roomEventHandler = async function _Room_roomEventHandler(method, properties) { const rpc = parseRpcMessage(RoomNotificationsRpc, { name: method, properties, }); if (rpc !== undefined) { switch (rpc.name) { case 'RoomStatusChanged': { __classPrivateFieldGet(this, _Room_instances, "m", _Room_onRoomStatusChanged).call(this, rpc.properties); break; } case 'RoomUpdated': { rpc.properties.updates.forEach((update) => { __classPrivateFieldGet(this, _Room_instances, "m", _Room_onRoomUpdate).call(this, update).then(); }); break; } case 'PeerUpdated': { await __classPrivateFieldGet(this, _Room_instances, "m", _Room_onPeerUpdate).call(this, rpc.properties); break; } case 'MessageReceived': { __classPrivateFieldGet(this, _Room_instances, "m", _Room_onMessageReceived).call(this, rpc.properties); break; } } } }, _Room_onRoomUpdate = async function _Room_onRoomUpdate(update) { switch (update.kind) { case 'Joined': { if (!__classPrivateFieldGet(this, _Room_room, "f")) { this.leave('Got room update but room is undefined.'); return; } __classPrivateFieldSet(this, _Room_roomData, update.room.user_data, "f"); __classPrivateFieldSet(this, _Room_token, new RoomToken(__classPrivateFieldGet(this, _Room_room, "f").token), "f"); await __classPrivateFieldGet(this, _Room_instances, "m", _Room_addRemotePeers).call(this, update.room.peers); const localPeer = __classPrivateFieldGet(this, _Room_instances, "m", _Room_addOwnPeer).call(this, update.own_peer_id); __classPrivateFieldGet(this, _Room_peers, "f").forEach((peer) => { __classPrivateFieldGet(this, _Room_instances, "m", _Room_onPeerJoined).call(this, peer); }); __classPrivateFieldGet(this, _Room_instances, "m", _Room_dispatchJoined).call(this, { room: this, peer: localPeer }); break; } case 'Left': { __classPrivateFieldGet(this, _Room_instances, "m", _Room_dispatchLeft).call(this, { room: this, reason: update.reason }); this.leave(update.reason); break; } case 'PeerJoined': { const peer = await __classPrivateFieldGet(this, _Room_instances, "m", _Room_addRemotePeer).call(this, update.peer); __classPrivateFieldGet(this, _Room_instances, "m", _Room_onPeerJoined).call(this, peer); break; } case 'PeerLeft': { __classPrivateFieldGet(this, _Room_instances, "m", _Room_onPeerLeft).call(this, update.peer_id); break; } case 'UserDataChanged': { __classPrivateFieldSet(this, _Room_roomData, update.user_data, "f"); this.dispatchEvent(new OdinEvent('RoomDataChanged', { room: this, })); this.onDataChanged?.({ room: this }); break; } } }, _Room_dispatchJoined = function _Room_dispatchJoined(payload) { this.dispatchEvent(new OdinEvent('Joined', payload)); this.status = { status: 'joined', }; this.onJoined?.(payload); }, _Room_dispatchLeft = function _Room_dispatchLeft(payload) { this.dispatchEvent(new OdinEvent('Left', payload)); if (this.onLeft) { this.onLeft(payload); } }, _Room_onPeerJoined = function _Room_onPeerJoined(peer) { this.dispatchEvent(new OdinEvent('PeerJoined', { room: this, peer })); this.onPeerJoined?.({ room: this, peer }); if (peer.isRemote) { peer.videoOutputs.forEach((media) => { __classPrivateFieldGet(this, _Room_instances, "m", _Room_dispatchVideoOutputAdded).call(this, { room: this, peer, media, }); }); peer.audioOutputs.forEach((media) => { __classPrivateFieldGet(this, _Room_instances, "m", _Room_dispatchAudioOutputStarted).call(this, { room: this, peer, media, }); }); } }, _Room_onPeerLeft = function _Room_onPeerLeft(peerId) { const peer = this.peers.get(peerId); if (peer && peer.isRemote) { for (const output of peer.audioOutputs) { __classPrivateFieldGet(this, _Room_instances, "m", _Room_cleanupAudioOutput).call(this, output); __classPrivateFieldGet(this, _Room_instances, "m", _Room_dispatchAudioOutputStopped).call(this, output); } for (const output of peer.videoOutputs) { __classPrivateFieldGet(this, _Room_instances, "m", _Room_removeVideoOutput).call(this, output); } this.peers.delete(peer.id); this.dispatchEvent(new OdinEvent('PeerLeft', { room: this, peer })); this.onPeerLeft?.({ room: this, peer }); } }, _Room_onPeerUpdate = async function _Room_onPeerUpdate(update) { const peer = __classPrivateFieldGet(this, _Room_peers, "f").get(update.peer_id); if (!peer || !peer.isRemote) { return; } switch (update.kind) { case 'MediaStarted': { const media = update.media; if (media.properties.kind === 'video') { const videoOutput = await __classPrivateFieldGet(this, _Room_instances, "m", _Room_addVideoOutput).call(this, media, peer); __classPrivateFieldGet(this, _Room_instances, "m", _Room_dispatchVideoOutputAdded).call(this, { peer, room: this, media: videoOutput, }); } else { const playback = await __classPrivateFieldGet(this, _Room_instances, "m", _Room_createAudioPlayback).call(this, media, peer); const output = new AudioOutput(playback, media, peer, this); await this.addAudioOutput(output); __classPrivateFieldGet(this, _Room_instances, "m", _Room_dispatchAudioOutputStarted).call(this, { peer: output.peer, room: this, media: output, }); } break; } case 'MediaStopped': { const output = __classPrivateFieldGet(this, _Room_outputs, "f").find((output) => output.mediaId === update.media_id); if (!output) return; if (output.kind === 'video-output') { __classPrivateFieldGet(this, _Room_instances, "m", _Room_removeVideoOutput).call(this, output); } else { __classPrivateFieldGet(this, _Room_instances, "m", _Room_cleanupAudioOutput).call(this, output); __classPrivateFieldGet(this, _Room_instances, "m", _Room_dispatchAudioOutputStopped).call(this, output); } break; } case 'UserDataChanged': { peer.data = update.user_data; __classPrivateFieldGet(this, _Room_instances, "m", _Room_dispatchUserDataChanged).call(this, { room: this, peer }); break; } } }, _Room_dispatchUserDataChanged = function _Room_dispatchUserDataChanged(payload) { payload.peer.dispatchEvent(new OdinEvent('UserDataChanged', payload)); payload.peer.onUserDataChanged?.(payload); this.dispatchEvent(new OdinEvent('UserDataChanged', payload)); this.onUserDataChanged?.(payload); }, _Room_onMessageReceived = function _Room_onMessageReceived(params) { const peer = __classPrivateFieldGet(this, _Room_peers, "f").get(params.sender_peer_id); if (!peer?.isRemote) return; const payload = { room: this, peer, message: params.message, }; peer?.dispatchEvent(new OdinEvent('MessageReceived', payload)); this.dispatchEvent(new OdinEvent('MessageReceived', payload)); this.onMessageReceived?.(payload); }, _Room_onRoomStatusChanged = function _Room_onRoomStatusChanged(params) { if (__classPrivateFieldGet(this, _Room_room, "f") && params.status === 'Joining' && __classPrivateFieldGet(this, _Room_status, "f").status === 'joined') { __classPrivateFieldGet(this, _Room_instances, "m", _Room_reset).call(this); this.status = { status: 'reconnecting', reason: params.message, }; } if (__classPrivateFieldGet(this, _Room_room, "f") && params.status === 'Closed') { this.leave(params.message); } }, _Room_addOwnPeer = function _Room_addOwnPeer(id) { __classPrivateFieldSet(this, _Room_ownPeerId, id, "f"); const peerData = { id, user_id: __classPrivateFieldGet(this, _Room_token, "f")?.userId ?? '', user_data: this.userData, medias: [], }; const peer = new LocalPeer(peerData, this); __classPrivateFieldGet(this, _Room_peers, "f").set(__classPrivateFieldGet(this, _Room_ownPeerId, "f"), peer); for (const input of this.audioInputs) { input.addEventListener('Activity', peer.audioActivityHandler); input.addEventListener('PowerLevel', peer.rmsDBFSHandler); if (__classPrivateFieldGet(this, _Room_room, "f")) { __classPrivateFieldGet(this, _Room_room, "f").link(input.capture).then(() => { __classPrivateFieldGet(this, _Room_instances, "m", _Room_dispatchAudioInputStarted).call(this, input); }); } } return peer; }, _Room_addRemotePeer = /** * Creates a new peer instance and adds it to the room. * * @param data The ID of the peer */ async function _Room_addRemotePeer(data) { if (data.id === __classPrivateFieldGet(this, _Room_ownPeerId, "f")) { throw new Error('Can not add the own peer with this method\n'); } const peer = new RemotePeer(data, this); for (const media of data.medias) { if (media.properties.kind === 'video') { await __classPrivateFieldGet(this, _Room_instances, "m", _Room_addVideoOutput).call(this, media, peer); } else { const playback = await __classPrivateFieldGet(this, _Room_instances, "m", _Room_createAudioPlayback).call(this, media, peer); const output = new AudioOutput(playback, media, peer, this); await this.addAudioOutput(output); } } __classPrivateFieldGet(this, _Room_peers, "f").set(data.id, peer); return peer; }, _Room_cleanupAudioOutput = function _Room_cleanupAudioOutput(output) { const uidInUse = __classPrivateFieldGet(this, _Room_outputs, "f").some((o) => o !== output && o.uid === output.uid); if (uidInUse) { unregisterAudioOutput(output); __classPrivateFieldSet(this, _Room_outputs, __classPrivateFieldGet(this, _Room_outputs, "f").filter((o) => o !== output), "f"); } else { output.close(); this.removeAudioOutput(output); } }, _Room_addVideoOutput = async function _Room_addVideoOutput(media, peer) { if (media.properties.kind !== 'video') { throw new Error('Error when adding remote video'); } const plugin = await ensurePlugin(); const playback = await plugin.createVideoPlayback({ uid: media.properties.uid, customType: media.properties.customType, }); const videoOutput = new VideoOutput(media, playback, peer, this); __classPrivateFieldGet(this, _Room_outputs, "f").push(videoOutput); return videoOutput; }, _Room_removeVideoOutput = function _Room_removeVideoOutput(output) { this.stopVideoOutput(output); __classPrivateFieldSet(this, _Room_outputs, __classPrivateFieldGet(this, _Room_outputs, "f").filter((videoOutput) => videoOutput !== output), "f"); __classPrivateFieldGet(this, _Room_instances, "m", _Room_dispatchVideoOutputStopped).call(this, output); }, _Room_createAudioPlayback = async function _Room_createAudioPlayback(media, peer) { const plugin = await ensurePlugin(); assert(__classPrivateFieldGet(this, _Room_room, "f"), "Can't start the Playback, no Room available"); const playback = await plugin.createAudioPlayback({ customType: media.properties.customType, uid: media.properties.uid, volume: calcPlaybackVolume([peer.volume, this.volume]), }); return playback; }, _Room_addRemotePeers = async function _Room_addRemotePeers(data) { for (const remotePeer of data) { await __classPrivateFieldGet(this, _Room_instances, "m", _Room_addRemotePeer).call(this, remotePeer); } }, _Room_dispatchMediaStarted = function _Room_dispatchMediaStarted(peer, media) { const event = 'MediaStarted'; const payload = { room: this, peer, media, }; peer.dispatchEvent(new OdinEvent(event, payload)); peer.onMediaStarted?.(payload); this.dispatchEvent(new OdinEvent(event, payload)); this.onMediaStarted?.(payload); }, _Room_dispatchMediaStopped = function _Room_dispatchMediaStopped(peer, media) { const event = 'MediaStopped'; const payload = { room: this, peer, media, }; peer.dispatchEvent(new OdinEvent(event, payload)); peer.onMediaStopped?.(payload); this.dispatchEvent(new OdinEvent(event, payload)); this.onMediaStopped?.(payload); }, _Room_dispatchAudioOutputStarted = function _Room_dispatchAudioOutputStarted(payload) { const event = 'AudioOutputStarted'; payload.peer.dispatchEvent(new OdinEvent(event, payload)); payload.peer.onAudioOutputStarted?.(payload); this.dispatchEvent(new OdinEvent(event, payload)); this.onAudioOutputStarted?.(payload); __classPrivateFieldGet(this, _Room_instances, "m", _Room_dispatchMediaStarted).call(this, payload.peer, payload.media); }, _Room_dispatchAudioOutputStopped = function _Room_dispatchAudioOutputStopped(output) { const event = 'AudioOutputStopped'; const payload = { room: this, peer: output.peer, media: output, }; output.peer.dispatchEvent(new OdinEvent(event, payload)); output.peer.onAudioOutputStopped?.(payload); this.dispatchEvent(new OdinEvent(event, payload)); this.onAudioOutputStopped?.(payload); __classPrivateFieldGet(this, _Room_instances, "m", _Room_dispatchMediaStopped).call(this, output.peer, output); }, _Room_dispatchAudioInputStarted = function _Room_dispatchAudioInputStarted(input) { const event = 'AudioInputStarted'; if (this.ownPeer) { const payload = { room: this, peer: this.ownPeer, media: input, }; this.ownPeer.dispatchEvent(new OdinEvent(event, payload)); this.ownPeer.onAudioInputStarted?.(payload); this.dispatchEvent(new OdinEvent(event, payload)); this.onAudioInputStarted?.(payload); __classPrivateFieldGet(this, _Room_instances, "m", _Room_dispatchMediaStarted).call(this, this.ownPeer, input); } }, _Room_dispatchAudioInputStopped = function _Room_dispatchAudioInputStopped(input) { const event = 'AudioInputStopped'; if (this.ownPeer) { const payload = { room: this, peer: this.ownPeer, media: input, }; this.ownPeer.dispatchEvent(new OdinEvent(event, payload)); this.ownPeer.onAudioInputStopped?.(payload); this.dispatchEvent(new OdinEvent(event, payload)); this.onAudioInputStopped?.(payload); __classPrivateFieldGet(this, _Room_instances, "m", _Room_dispatchMediaStopped).call(this, this.ownPeer, input); } }, _Room_dispatchVideoOutputAdded = function _Room_dispatchVideoOutputAdded(payload) { const event = 'VideoOutputAdded'; payload.peer.dispatchEvent(new OdinEvent(event, payload)); payload.peer.onVideoOutputStarted?.(payload); this.dispatchEvent(new OdinEvent(event, payload)); this.onVideoOutputStarted?.(payload); __classPrivateFieldGet(this, _Room_instances, "m", _Room_dispatchMediaStarted).call(this, payload.peer, payload.media); }, _Room_dispatchVideoOutputStopped = function _Room_dispatchVideoOutputStopped(output) { const event = 'VideoOutputRemoved'; const payload = { room: this, peer: output.peer, media: output, }; output.peer.dispatchEvent(new OdinEvent(event, payload)); output.peer.onVideoOutputStopped?.(payload); this.dispatchEvent(new OdinEvent(event, payload)); this.onVideoOutputStopped?.(payload); __classPrivateFieldGet(this, _Room_instances, "m", _Room_dispatchMediaStopped).call(this, output.peer, output); }, _Room_dispatchVideoInputStarted = function _Room_dispatchVideoInputStarted(input) { const event = 'VideoInputStarted'; if (this.ownPeer) { const payload = { room: this, peer: this.ownPeer, media: input, }; this.ownPeer.dispatchEvent(new OdinEvent(event, payload)); this.ownPeer.onVideoInputStarted?.(payload); this.dispatchEvent(new OdinEvent(event, payload)); this.onVideoInputStarted?.(payload); __classPrivateFieldGet(this, _Room_instances, "m", _Room_dispatchMediaStarted).call(this, this.ownPeer, input); } }, _Room_dispatchVideoInputStopped = function _Room_dispatchVideoInputStopped(input) { const event = 'VideoInputStopped'; if (this.ownPeer) { const payload = { room: this, peer: this.ownPeer, media: input, }; this.ownPeer.dispatchEvent(new OdinEvent(event, payload)); this.ownPeer.onVideoInputStopped?.(payload); this.dispatchEvent(new OdinEvent(event, payload)); this.onVideoInputStopped?.(payload); __classPrivateFieldGet(this, _Room_instances, "m", _Room_dispatchMediaStopped).call(this, this.ownPeer, input); } }, _Room_waitForConnect = /** * Resolves once the room is connected. * @ */ async function _Room_waitForConnect() { const buildErrorMsg = (status) => { return `Stop connecting, room status changed to ${status.status}. Reason: ${status.reason}`; }; await new Promise((resolve, reject) => { if (this.status.status === 'joined' && this.ownPeer) { resolve(); } if (this.status.status === 'disconnected') { reject(new OdinError(buildErrorMsg(this.status))); } this.addEventListener('RoomStatusChanged', (status) => { if (status.payload.newState.status === 'joined' && this.ownPeer) { resolve(); } else { reject(new OdinError(buildErrorMsg(this.status))); } }, { once: true }); }); }; function parseRpcMessage(events, rpc) { if (!isKnownRpcMessage(events, rpc.name)) return undefined; const parsed = events[rpc.name].safeParse(rpc.properties); if (!parsed.success) { return undefined; } return { name: rpc.name, properties: parsed.data, }; } function isKnownRpcMessage(events, name) { return name in events; }