@4players/odin
Version:
A cross-platform SDK enabling developers to integrate real-time VoIP chat technology into their projects
1,031 lines (1,030 loc) • 48.9 kB
JavaScript
;
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
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_removeAudioOutput, _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_addAudioOutput, _Room_addVideoOutput, _Room_removeVideoOutput, _Room_startAudioPlayback, _Room_addRemotePeers, _Room_dispatchMediaStarted, _Room_dispatchMediaStopped, _Room_dispatchAudioOutputStarted, _Room_dispatchAudioOutputStopped, _Room_dispatchAudioInputStarted, _Room_dispatchAudioInputStopped, _Room_dispatchVideoOutputAdded, _Room_dispatchVideoOutputRemoved, _Room_dispatchVideoInputStarted, _Room_dispatchVideoInputStopped, _Room_waitForConnect;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Room = void 0;
const odin_event_target_1 = require("../../utils/odin-event-target");
const odin_common_1 = require("@4players/odin-common");
const api_1 = require("../../api");
const audio_output_1 = require("../media/audio-output");
const helpers_1 = require("../../utils/helpers");
const token_1 = require("../../utils/token");
const video_output_1 = require("../media/video-output");
const remote_peer_1 = require("../peer/remote-peer");
const local_peer_1 = require("../peer/local-peer");
/**
* 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.
*/
class Room extends odin_event_target_1.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, {
...odin_common_1.CONNECTION_STATS_INITIAL,
});
// AudioMedia EventCbs
_Room_activityCb.set(this, (event) => {
this.dispatchEvent(new odin_event_target_1.OdinEvent('AudioActivity', event.payload));
this.onAudioActivity?.(event.payload);
});
_Room_rmsDBFSCb.set(this, (event) => {
this.dispatchEvent(new odin_event_target_1.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: (0, helpers_1.calculateBytesPerSeconds)(__classPrivateFieldGet(this, _Room_room, "f").connectionStats.bytesReceived, __classPrivateFieldGet(this, _Room_previousConnectionStats, "f").bytesReceived, this.connectionStatsInterval),
bytesSentLastSecond: (0, helpers_1.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 odin_event_target_1.OdinEvent('ConnectionStats', connectionStats));
}
await (0, helpers_1.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 {
...odin_common_1.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) {
(0, helpers_1.assert)(this.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) {
(0, helpers_1.assert)(this.plugin, "Can't add an AudioInput, no AudioPlugin");
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"));
__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) {
(0, helpers_1.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) {
this.pauseMedia(output.mediaId).then();
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 = {}) {
(0, helpers_1.assert)(this.plugin, "Can't change the AudioPut device, no AudioPlugin initialized.");
await this.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");
(0, helpers_1.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 token_1.RoomToken(token), "f");
const url = (0, helpers_1.normalizeUrl)(options?.gateway ?? this.defaultGateway);
const joinParams = {
url,
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 {
room = this.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 helpers_1.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);
// Start AudioInputs that were already added before.
this.audioInputs.forEach((input) => {
room.link(input.capture);
if (this.ownPeer) {
input.addEventListener('Activity', this.ownPeer.audioActivityHandler);
input.addEventListener('PowerLevel', this.ownPeer.rmsDBFSHandler);
}
});
__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() {
(0, helpers_1.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) {
(0, helpers_1.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) {
(0, helpers_1.assert)(__classPrivateFieldGet(this, _Room_room, "f"), `Can't resume the media with id ${mediaId}`);
try {
const response = await this.request('ResumeMedia', {
media_id: mediaId,
});
}
catch (e) {
console.warn(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) {
(0, helpers_1.assert)(__classPrivateFieldGet(this, _Room_room, "f"), `Can't pause the media with id ${mediaId}`);
try {
const response = await this.request('PauseMedia', {
media_id: mediaId,
});
}
catch (e) {
console.warn('An Error occurred when pausing the Media on the Server: ', e);
}
}
get plugin() {
if (!api_1.PLUGIN) {
throw new Error('No Backed Plugin');
}
return api_1.PLUGIN;
}
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 odin_event_target_1.OdinEvent('RoomStatusChanged', payload));
this.onStatusChanged?.(payload);
}
}
}
exports.Room = Room;
_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_removeAudioOutput = function _Room_removeAudioOutput(output) {
output.close();
if (__classPrivateFieldGet(this, _Room_room, "f")) {
__classPrivateFieldGet(this, _Room_room, "f").unlink(output.playback);
}
__classPrivateFieldSet(this, _Room_outputs, __classPrivateFieldGet(this, _Room_outputs, "f").filter((audioOutput) => audioOutput !== output), "f");
__classPrivateFieldGet(this, _Room_instances, "m", _Room_dispatchAudioOutputStopped).call(this, output);
}, _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);
}
}
__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(odin_common_1.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 token_1.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 odin_event_target_1.OdinEvent('RoomDataChanged', {
room: this,
}));
this.onDataChanged?.({ room: this });
break;
}
}
}, _Room_dispatchJoined = function _Room_dispatchJoined(payload) {
this.dispatchEvent(new odin_event_target_1.OdinEvent('Joined', payload));
this.status = {
status: 'joined',
};
this.onJoined?.(payload);
}, _Room_dispatchLeft = function _Room_dispatchLeft(payload) {
this.dispatchEvent(new odin_event_target_1.OdinEvent('Left', payload));
if (this.onLeft) {
this.onLeft(payload);
}
}, _Room_onPeerJoined = function _Room_onPeerJoined(peer) {
this.dispatchEvent(new odin_event_target_1.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_removeAudioOutput).call(this, output);
__classPrivateFieldGet(this, _Room_instances, "m", _Room_dispatchAudioOutputStopped).call(this, output);
}
this.peers.delete(peer.id);
this.dispatchEvent(new odin_event_target_1.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') {
await __classPrivateFieldGet(this, _Room_instances, "m", _Room_addVideoOutput).call(this, media, peer);
}
else {
const audioOutput = await __classPrivateFieldGet(this, _Room_instances, "m", _Room_addAudioOutput).call(this, media, peer);
__classPrivateFieldGet(this, _Room_instances, "m", _Room_dispatchAudioOutputStarted).call(this, {
peer,
room: this,
media: audioOutput,
});
}
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_removeAudioOutput).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 odin_event_target_1.OdinEvent('UserDataChanged', payload));
payload.peer.onUserDataChanged?.(payload);
this.dispatchEvent(new odin_event_target_1.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 odin_event_target_1.OdinEvent('MessageReceived', payload));
this.dispatchEvent(new odin_event_target_1.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 local_peer_1.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 remote_peer_1.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 {
await __classPrivateFieldGet(this, _Room_instances, "m", _Room_addAudioOutput).call(this, media, peer);
}
}
__classPrivateFieldGet(this, _Room_peers, "f").set(data.id, peer);
return peer;
}, _Room_addAudioOutput = async function _Room_addAudioOutput(media, peer) {
const playback = await __classPrivateFieldGet(this, _Room_instances, "m", _Room_startAudioPlayback).call(this, media, peer);
const audioOutput = new audio_output_1.AudioOutput(playback, media, peer, this);
audioOutput.addEventListener('Activity', __classPrivateFieldGet(this, _Room_activityCb, "f"));
audioOutput.addEventListener('Activity', peer.audioActivityHandler.bind(peer));
__classPrivateFieldGet(this, _Room_outputs, "f").push(audioOutput);
return audioOutput;
}, _Room_addVideoOutput = async function _Room_addVideoOutput(media, peer) {
if (media.properties.kind !== 'video') {
throw new Error('Error when adding remote video');
}
const playback = await this.plugin.createVideoPlayback({
id: media.properties.id ?? '',
uid: media.properties.uid,
customType: media.properties.customType,
});
const videoOutput = new video_output_1.VideoOutput(media, playback, peer, this);
__classPrivateFieldGet(this, _Room_outputs, "f").push(videoOutput);
__classPrivateFieldGet(this, _Room_instances, "m", _Room_dispatchVideoOutputAdded).call(this, {
peer,
room: this,
media: videoOutput,
});
return videoOutput;
}, _Room_removeVideoOutput = function _Room_removeVideoOutput(output) {
__classPrivateFieldSet(this, _Room_outputs, __classPrivateFieldGet(this, _Room_outputs, "f").filter((videoOutput) => videoOutput !== output), "f");
__classPrivateFieldGet(this, _Room_instances, "m", _Room_dispatchVideoOutputRemoved).call(this, output);
}, _Room_startAudioPlayback = async function _Room_startAudioPlayback(media, peer) {
(0, helpers_1.assert)(this.plugin && __classPrivateFieldGet(this, _Room_room, "f"), "Can't start the Playback, no Plugin or Room available");
const playback = await this.plugin.createAudioPlayback({
customType: media.properties.customType,
uid: media.properties.uid,
volume: (0, helpers_1.calcPlaybackVolume)([peer.volume, this.volume]),
});
await __classPrivateFieldGet(this, _Room_room, "f").link(playback);
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 odin_event_target_1.OdinEvent(event, payload));
peer.onMediaStarted?.(payload);
this.dispatchEvent(new odin_event_target_1.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 odin_event_target_1.OdinEvent(event, payload));
peer.onMediaStopped?.(payload);
this.dispatchEvent(new odin_event_target_1.OdinEvent(event, payload));
this.onMediaStopped?.(payload);
}, _Room_dispatchAudioOutputStarted = function _Room_dispatchAudioOutputStarted(payload) {
const event = 'AudioOutputStarted';
payload.peer.dispatchEvent(new odin_event_target_1.OdinEvent(event, payload));
payload.peer.onAudioOutputStarted?.(payload);
this.dispatchEvent(new odin_event_target_1.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 odin_event_target_1.OdinEvent(event, payload));
output.peer.onAudioOutputStopped?.(payload);
this.dispatchEvent(new odin_event_target_1.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 odin_event_target_1.OdinEvent(event, payload));
this.ownPeer.onAudioInputStarted?.(payload);
this.dispatchEvent(new odin_event_target_1.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 odin_event_target_1.OdinEvent(event, payload));
this.ownPeer.onAudioInputStopped?.(payload);
this.dispatchEvent(new odin_event_target_1.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 odin_event_target_1.OdinEvent(event, payload));
payload.peer.onVideoOutputStarted?.(payload);
this.dispatchEvent(new odin_event_target_1.OdinEvent(event, payload));
this.onVideoOutputStarted?.(payload);
__classPrivateFieldGet(this, _Room_instances, "m", _Room_dispatchMediaStarted).call(this, payload.peer, payload.media);
}, _Room_dispatchVideoOutputRemoved = function _Room_dispatchVideoOutputRemoved(output) {
const event = 'VideoOutputRemoved';
const payload = {
room: this,
peer: output.peer,
media: output,
};
output.peer.dispatchEvent(new odin_event_target_1.OdinEvent(event, payload));
output.peer.onVideoOutputStopped?.(payload);
this.dispatchEvent(new odin_event_target_1.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 odin_event_target_1.OdinEvent(event, payload));
this.ownPeer.onVideoInputStarted?.(payload);
this.dispatchEvent(new odin_event_target_1.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 odin_event_target_1.OdinEvent(event, payload));
this.ownPeer.onVideoInputStopped?.(payload);
this.dispatchEvent(new odin_event_target_1.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 helpers_1.OdinError(buildErrorMsg(this.status)));
}
this.addEventListener('RoomStatusChanged', (status) => {
if (status.payload.newState.status === 'joined' && this.ownPeer) {
resolve();
}
else {
reject(new helpers_1.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;
}