UNPKG

@huddle01/web-core

Version:

The Huddle01 Javascript SDK offers a comprehensive suite of methods and event listeners that allow for seamless real-time audio and video communication with minimal coding required.

1,372 lines (1,371 loc) 89.7 kB
import { Transport_default } from './chunk-KLVG3BOK.js'; import { parseRouterRtpCapabilities } from './chunk-2OLUNNR5.js'; import { ActiveSpeakers_default } from './chunk-NOJW3KYK.js'; import { Room_default } from './chunk-5B3BC5M6.js'; import { Socket_default } from './chunk-RS7KXM4R.js'; import { Bot_default } from './chunk-6MHHX2FW.js'; import { DeviceHandler_default } from './chunk-HDCV2PNW.js'; import { getMediaDeviceKind, estimateSize } from './chunk-WA3QABYS.js'; import { Permissions_default, checkPermissions, checkProducePermissions } from './chunk-E2DU7IIE.js'; import { mainLogger } from './chunk-TOCFOGTC.js'; import { EnhancedEventEmitter } from './chunk-BW2DGP4D.js'; import { detectDevice, Device } from 'mediasoup-client'; var logger = mainLogger.createSubLogger("LocalPeer"); var MAX_DATA_MESSAGE_SIZE = 1 * 1024; var MAX_VOLATILE_DATA_MESSAGE_SIZE = 1e3 * 1024; var LocalPeer = class _LocalPeer extends EnhancedEventEmitter { /** * LocalPeer Instance, Singleton class, only one instance of this class can be created */ static __instance = null; /** * PeerId of the current client, specific to the Local Peer who joined the meeting * * `NOTE: Until you dont join the room, this will be *null*` */ peerId = null; /** * Current Devices of the current client * e.g. Chrome, Firefox, Safari, ReactNative */ __device = null; /** * custom handler factory for device, required when using aiortc client or something custom */ __handlerFactory; /** * SendTransport handles the sending of media from the client to the server */ __sendTransport = null; /** * RecvTransport handles the receiving of media from the server to the client */ __recvTransport = null; /** * Returns the room instance, throws an error if the room is not created * * @throws { Error } If the room is not created, Call createRoom() method before you can access the room in the LocalPeer */ get room() { const room = Room_default.getInstance(); if (!room) throw new Error("\u274C Room Not Initialized"); return room; } /** * Returns the underlying socket connection * @throws { Error } If the socket connection is not initialized */ get socket() { const socket = Socket_default.getInstance(); if (!socket) throw new Error("\u274C Socket Not Initialized"); return socket; } /** * Remote Peers Map, Stores all the remote peers */ get __remotePeers() { return this.room.remotePeers; } /** * Turn Server used for this client */ __turn = [ { username: "test-turn", urls: "turns:turn.huddle01.com:443", credential: "test-turn" }, { username: "test-turn", urls: "turn:turn.huddle01.com:443?transport=tcp", credential: "test-turn" }, { username: "test-turn", urls: "turn:turn.huddle01.com:443", credential: "test-turn" } ]; /** * Get the current device ( chrome, firefox, safari, reactnative ) for this client * * @throws { Error } If the device is not initialized */ get device() { if (!this.__device) throw new Error("Device Not Initialized"); const loaded = this.__device.loaded; if (!loaded) throw new Error("Device Not Loaded"); return this.__device; } // !important // Consumer creation tasks awaiting to be processed. // Stores the lables of the pending consumers { producerId: string ==> Promise<Consumer> } __pendingConsumerTasks = /* @__PURE__ */ new Map(); // !important // Producer creation tasks awaiting to be processed. // Stores the lables of the pending producers { label: string } // Used to handle transport callbacks most important; __pendingProducerTasks = /* @__PURE__ */ new Map(); // !important // Map to store pending tasks, Stores the label and the promise of the task __pendingTasks = /* @__PURE__ */ new Map(); /** * Pending Transport Tasks, Stores the transportType and the promise of the transport * * `NOTE: Useful to check if the transport is already being created and pause all producing * and consuming until the transport is created` */ __pendingTransportTasks = /* @__PURE__ */ new Map(); /** * Stores all the pending fetching stream tasks which are awaiting to be processed * If fetching called multiple times at once, it will handle the concurrency issues */ __pendingFetchingStream = /* @__PURE__ */ new Map(); /** * Stores all the pending produce tasks which are awaiting to be processed * Mostly used when the room is not joined and produce functionality needs to be handled * * Cases such as socket experiencing a reconnect and produce is called. * Or in the cases where room is not joined and enableVideo or enableAudio is called */ __waitingToProduce = /* @__PURE__ */ new Map(); /** * Stores all the pending consume tasks which are waiting for recv transport to be re-connected */ __waitingToConsume = []; /** * DeviceHandler Instance, Handles the media devices for this client * e.g. Camera, Microphone */ deviceHandler = new DeviceHandler_default(); /** * ActiveStream Map holds MediaStream as Value and Key as Label */ __activeStreams = /* @__PURE__ */ new Map(); /** * Handle the Client Side Permission for the Local Peer. */ __permissions = Permissions_default.createInstance(); /** * Stores the Metadata for the Local Peer. */ __metadata = null; /** * Variable to check if the user has joined the room */ joined = false; /** * Variable to know if Volatile Messaging is enabled for the LocalPeer, * when using Volatile Messaging, the messages are sent over UDP using WebRTC * which makes the messages faster but not reliable and have much higher rate limit * and lower Latency. */ __volatileMessaging = false; /** * Return the labels of the Media Stream that the Local Peer is producing to the room */ get labels() { const sendTransport = this.__sendTransport; if (sendTransport) { const labels = Array.from(sendTransport.labelToProducerId.keys()); return labels; } return []; } /** * Get the Permissions of the Local Peer. (e.g canProduce, canConsume, canSendData, canRecvData etc) */ get permissions() { const acl = this.__permissions.acl; return acl; } /** * Get the Role of the Local Peer. */ get role() { return this.__permissions.role; } /** * Returns the token of the current socket connection, specific to the Local Peer who joined the meeting */ get token() { return this.socket.token; } /** * Returns the roomId of the current joined room. */ get roomId() { return this.room.roomId; } /** * Returns the SendTransport * @returns { Transport } SendTransport * @throws { Error } If the SendTransport is not initialized */ get sendTransport() { if (!this.__sendTransport) throw new Error("Send Transport Not Initialized"); return this.__sendTransport; } /** * Returns the recvTransport * @returns { Transport } recvTransport * @throws { Error } If the recvTransport is not initialized */ get recvTransport() { if (!this.__recvTransport) throw new Error("Recv Transport Not Initialized"); return this.__recvTransport; } /** * Returns the metadata associated to the LocalPeer */ getMetadata() { const data = JSON.parse(this.__metadata || "{}"); return data; } /** * getStream returns the stream with the given label */ getStream = (data) => { const stream = this.__activeStreams.get(data.label); if (stream === void 0) { return null; } return stream; }; /** * Updates the metadata associated to the LocalPeer, Triggers the `metadata-updated` event * @param metadata */ __updateMetadata = (metadata) => { this.__metadata = metadata; const parse = JSON.parse(metadata); this.emit("metadata-updated", { metadata: parse }); }; /** * Returns the producer with the given label * @param label - Identifier of the producer * @returns { Producer } Producer * @returns { null } If the producer is not found */ getProducerWithLabel = (label) => { try { const producerId = this.__sendTransport?.labelToProducerId.get(label); if (!producerId) { throw new Error("\u274C Producer Not Found"); } const producer = this.sendTransport.getProducerById(producerId); return producer; } catch (error) { logger.error("\u274C Cannot Find Producer With Identifier: ", label); logger.error(error); return null; } }; /** * Registers the event handlers for the socket connection * @param socket - Socket Instance */ __registerHandlerEvents = (socket) => { const keys = Object.keys(this.__handler); for (const key of keys) { try { const fn = this.__handler[key]; if (fn) socket.subscribe(key, fn); } catch (error) { logger.error(`\u274C Error Registered For Event: ${key}`); logger.error(error); } } logger.info("\u2705 LocalPeerEventHandler Registered"); }; /** * Can be used to check which `direction` of webRTC connection is currently active for the peer. * * **NOTE: Peers with role as Bot will not have `recv` transport;** * @param transportType */ transportExists = (transportType) => { if (transportType === "recv") { return this.__recvTransport; } return this.__sendTransport; }; static create(options) { if (_LocalPeer.__instance) { return _LocalPeer.__instance; } _LocalPeer.__instance = new _LocalPeer(options); return _LocalPeer.__instance; } static getInstance() { if (!_LocalPeer.__instance) { throw new Error("LocalPeer not initialized"); } return _LocalPeer.__instance; } constructor(options) { super(); this.__registerHandlerEvents(this.socket); if (options.handlerFactory) { this.__handlerFactory = options?.handlerFactory; } this.__volatileMessaging = options.volatileMessages ?? false; this.__registerInternalListeners(); } /** * Destroy the current peer, closes all the transports, producers and consumers * * @param code - Close Code */ close = () => { this.__device = null; this.joined = false; if (typeof navigator !== "undefined" && navigator.mediaDevices) { navigator.mediaDevices.ondevicechange = null; } this.__pendingConsumerTasks.clear(); this.__pendingProducerTasks.clear(); this.__pendingTransportTasks.clear(); this.__pendingFetchingStream.clear(); this.__waitingToProduce.clear(); for (const stream of this.__activeStreams.values()) { for (const track of stream.getTracks()) track.stop(); } this.deviceHandler.destroy(); if (this.__sendTransport) { this.__sendTransport.close({ retries: 3 }); } if (this.__recvTransport) { this.__recvTransport.close({ retries: 3 }); } this.__sendTransport = null; this.__recvTransport = null; this.__permissions.reset(); this.emit("permissions-updated", { permissions: this.permissions, role: this.role ?? "" }); }; /** * Produce a stream with a given label and appData to all the Remote Peers * * `canProduce must be true to produce a stream` * * `NOTE: This will notify all the RemotePeers that this producer has started producing and they should start consuming it if they want to` * * @param data - Data to produce a stream * - `label` - Unique Identifier for the stream ( string ) * - `stream` - MediaStream to produce ( MediaStream ) * - `stopTrackOnClose` - If true, it will stop the track when the producer is closed using stopProducing ( boolean ) * - `appData` - Application level custom data which can be added to the producer for the LocalPeer, this data will be available in the producer object and can be used only by the LocalPeer. ( Unknown Object ) * - `prefferedCodec` - Preferred Codec to be used for the stream ( SupportedCodecs ), make sure the codec is supported by the device ( 'Browser' or 'ReactNative' ) * * @summary This function is used to produce a stream with a given label and appData to all the Remote Peers if the send transport is not initialised * it will create a send transport and then produce the stream with the given label and appData to all the Remote Peers * * Send Transport is a secure channel which is used to send media from LocalPeer to all the RemotePeers in the room * * `localPeer.on('new-producer', ({label: string; producer: Producer}) => {})` * * @throws { Error } If Failed to produce, Reason will be in the error message, * @throws { Error } if `prefferedCodec` is not supported by the Device ( 'Browser' or 'ReactNative' ) */ produce = checkPermissions({ canProduce: true }).validate( async (data) => { try { const error = checkProducePermissions(data.label); if (error) { throw new Error("Access Denied: Cannot Produce"); } const track = data.stream.getTracks()[0]; if (track) { track.addEventListener("ended", () => { this.stopProducing({ label: data.label }); if (data.label === "screen-share-audio") { this.stopProducing({ label: "screen-share-video" }); } }); } if (!this.joined || this.__sendTransport && this.__sendTransport?.connectionState !== "connected" && this.__sendTransport?.connectionState !== "new") { return new Promise((resolve) => { const fn = async () => { const producer2 = await this.produce(data).then((data2) => { resolve(data2); return data2; }).finally(() => { this.__pendingProducerTasks.delete(data.label); }); return producer2; }; this.__waitingToProduce.set(data.label, fn); }); } const { stream } = data; const producerPromise = this.__pendingProducerTasks.get(data.label); if (producerPromise) { logger.info( "\u{1F514} Producer Task Already Pending for this label ", data.label ); const producer2 = await producerPromise; return producer2; } if (!this.__sendTransport) { await this.__createTransportOnServer({ transportType: "send" }); } const ongoingPromise = this.__pendingProducerTasks.get(data.label); if (ongoingPromise) { const producer2 = await ongoingPromise; return producer2; } const promise = this.__createProducer({ stream, label: data.label, appData: { ...data.appData, label: data.label }, prefferedCodec: data.prefferedCodec, stopTrackOnClose: data.stopTrackOnClose }); this.__pendingProducerTasks.set(data.label, promise); const producer = await promise.catch(() => { logger.error("\u274C Error Create Producer Failed"); throw new Error("\u274C Error Create Producer Failed"); }).finally(() => { this.__pendingProducerTasks.delete(data.label); }); return producer; } catch (error) { this.stopProducing({ label: data.label }); throw new Error("\u274C Error Producing Stream, Can't Produce"); } } ); /** * Enables the local web cam and starts producing the stream with the label `video` * @param customVideoStream (Optional) Custom Video Stream to produce, if not provided it will fetch the stream from the device * @param prefferedCodec (Optional) Preferred Codec to be used for the stream ( SupportedCodecs ), make sure the codec is supported by the device ( 'Browser' or 'ReactNative' ) * * @summary This functions handle the producing of media streams to all the remote peers in the room. * it enables the local web cam fetches the stream opens you web cam indicator light on the device * upon successfull fetching of the stream it produces the stream with the label `video` and `stopTrackOnClose: true` to all the remote peers in the room. * when closing using disableVideo it will stop the local track and close the producer which will notify all the RemotePeers that this producer has stopped producing * and they should stop consuming it. * * `NOTE: You can only produce to a room when you have joined the room, if you try to produce before joining the room it will throw an error` * * @throws { Error } If the stream is not found * @throws { Error } If Preffered Codec is not supported by the Device ( 'Browser' or 'ReactNative' ) * @throws { Error } If Failed to produce, Reason will be in the error message */ enableVideo = checkPermissions({ canProduce: true, canProduceSources: { cam: true } }).validate( async (data) => { try { const existingStream = this.__activeStreams.get("video"); if (existingStream) { logger.warn("\u{1F514} Cam Stream Already Enabled"); return; } let stream; if (data?.customVideoStream) { stream = data?.customVideoStream; } else { const ongoingStreamPromise = this.__pendingFetchingStream.get("cam"); if (ongoingStreamPromise) { await ongoingStreamPromise; } else { const streamPromise = this.deviceHandler.fetchStream({ mediaDeviceKind: "cam" }); this.__pendingFetchingStream.set("cam", streamPromise); } const pendingPromise = this.__pendingFetchingStream.get("cam"); if (!pendingPromise) { logger.info("\u{1F514} Pending Promise Not Found"); return; } const { stream: fetchedStream, error } = await pendingPromise; if (error) { logger.error("\u274C Error Fetching Stream From Device"); logger.error(error); throw new Error("\u274C Error Fetching Stream From Device"); } if (!fetchedStream) { logger.error("\u274C Stream Not Found, cannot do enableVideo"); throw new Error("\u274C Stream Not Found"); } stream = fetchedStream; } this.__activeStreams.set("video", stream); this.emit("stream-fetched", { mediaKind: "cam", label: "video", stream }); this.produce({ label: "video", stream, appData: { producerPeerId: this.peerId }, stopTrackOnClose: true, prefferedCodec: data?.prefferedCodec }).then(() => { this.__pendingProducerTasks.delete("video"); }).catch((error) => { logger.error("\u274C Error Producing Video"); this.deviceHandler.stopStream(this.__activeStreams.get("video")); this.__activeStreams.delete("video"); this.__pendingFetchingStream.delete("cam"); logger.error(error); }); this.__pendingFetchingStream.delete("cam"); return stream; } catch (error) { logger.error("\u274C Error Enabling Video", error); this.deviceHandler.stopStream(this.__activeStreams.get("video")); this.__activeStreams.delete("video"); this.__pendingFetchingStream.delete("cam"); throw error; } } ); /** * Enables the local screen share and starts producing the screen sharing stream * * @param prefferedCodec (Optional) Preferred Codec to be used for the stream ( SupportedCodecs ), make sure the codec is supported by the device ( 'Browser' or 'ReactNative' ) * *`NOTE: You can only produce to a room when you have joined the room, if you try to produce before joining the room it will fetch the stream and start producing when you join the room` * * @summary This functions handle the producing of media streams to all the remote peers in the room. * it enables the local mic fetches the stream opens you mic active indicator light on the device * upon successfull fetching of the stream it produces the stream with the label `audio` and `stopTrackOnClose: true` to all the remote peers in the room. * when closing using `disableAudio` it will stop the local audio track and close the producer which will notify all the RemotePeers that this producer has stopped producing * and they should stop consuming it. * * @throws { Error } If Preffered Codec is not supported by the Device ( 'Browser' or 'ReactNative' ) * @throws { Error } If Failed to produce, Reason will be in the error message */ startScreenShare = checkPermissions({ canProduce: true, canProduceSources: { screen: true } }).validate(async (data) => { try { const existingStream = this.__activeStreams.get("screen-share"); if (existingStream) { logger.warn("\u{1F514} Screen Stream Already Enabled"); return; } const onGoingStreamPromise = this.__pendingFetchingStream.get("screen-share"); if (onGoingStreamPromise) { await onGoingStreamPromise; } else { const streamPromise = this.deviceHandler.fetchScreen(); this.__pendingFetchingStream.set("screen-share", streamPromise); } const pendingPromise = this.__pendingFetchingStream.get("screen-share"); if (!pendingPromise) { logger.info("\u{1F514} Pending Screen Share Promise Not Found"); return; } const { stream, error } = await pendingPromise; if (error) { logger.error("\u274C Error Fetching Screen Share Stream From Device"); logger.error(error); throw new Error("\u274C Error Fetching Screen ShareStream From Device"); } if (!stream) { logger.error("\u274C Stream Not Found, cannot do startScreenShare"); throw new Error("\u274C Stream Not Found, cannot do startScreenShare"); } this.__activeStreams.set("screen-share", stream); this.emit("stream-fetched", { mediaKind: "screen", label: "screen-share", stream }); const videoTrack = stream.getVideoTracks()?.[0]; const audioTrack = stream.getAudioTracks()?.[0]; const videoProduce = async () => { return this.produce({ label: "screen-share-video", stream: new MediaStream([videoTrack]), appData: { producerPeerId: this.peerId }, stopTrackOnClose: true, prefferedCodec: data?.prefferedCodec }); }; const audioProduce = async () => { return this.produce({ label: "screen-share-audio", stream: new MediaStream([audioTrack]), appData: { producerPeerId: this.peerId }, stopTrackOnClose: true }); }; if (videoTrack) { videoProduce().then(() => { this.__pendingProducerTasks.delete("screen-share-video"); }).catch((error2) => { logger.error("\u274C Error Producing Screen Share Video"); this.deviceHandler.stopStream( this.__activeStreams.get("screen-share") ); this.__activeStreams.delete("screen-share"); logger.error(error2); }); } if (audioTrack) { audioProduce().then(() => { this.__pendingProducerTasks.delete("screen-share-audio"); }).catch((error2) => { logger.error("\u274C Error Producing Audio"); this.deviceHandler.stopStream( this.__activeStreams.get("screen-share") ); this.__activeStreams.delete("screen-share"); logger.error(error2); }); } this.__pendingFetchingStream.delete("screen-share"); return stream; } catch (error) { logger.error("\u274C Error Enabling Screen Share"); logger.error(error); this.deviceHandler.stopStream(this.__activeStreams.get("screen-share")); this.__activeStreams.delete("screen-share"); this.__pendingFetchingStream.delete("screen-share"); throw error; } }); /** * Enables the local mic and starts producing the stream with the label `audio` * @param customVideoStream (Optional) Custom Video Stream to produce, if not provided it will fetch the stream from the device * @param prefferedCodec (Optional) Preferred Codec to be used for the stream ( SupportedCodecs ), make sure the codec is supported by the device ( 'Browser' or 'ReactNative' ) * * @summary This functions handle the producing of media streams to all the remote peers in the room. * it enables the local mic fetches the stream opens you mic active indicator light on the device * upon successfull fetching of the stream it produces the stream with the label `audio` and `stopTrackOnClose: true` to all the remote peers in the room. * when closing using `disableAudio` it will stop the local audio track and close the producer which will notify all the RemotePeers that this producer has stopped producing * and they should stop consuming it. * * `NOTE: You can only produce to a room when you have joined the room, if you try to produce before joining the room it will fetch the stream and start producing when you join the room` * * @throws { Error } If the stream is not found * @throws { Error } If Preffered Codec is not supported by the Device ( 'Browser' or 'ReactNative' ) * @throws { Error } If Failed to produce, Reason will be in the error message */ enableAudio = checkPermissions({ canProduce: true, canProduceSources: { mic: true } }).validate( async (data) => { try { const existingStream = this.__activeStreams.get("audio"); if (existingStream) { logger.warn("\u{1F514} Mic Stream Already Enabled"); return; } let stream; if (data?.customAudioStream) { stream = data?.customAudioStream; } else { const ongoingStreamPromise = this.__pendingFetchingStream.get("mic"); if (ongoingStreamPromise) { await ongoingStreamPromise; } else { const streamPromise = this.deviceHandler.fetchStream({ mediaDeviceKind: "mic" }); this.__pendingFetchingStream.set("mic", streamPromise); } const pendingPromise = this.__pendingFetchingStream.get("mic"); if (!pendingPromise) { logger.info("\u{1F514} Pending Mic Promise Not Found"); return; } const { stream: fetchedStream, error } = await pendingPromise; if (error) { logger.error("\u274C Error Fetching Stream From Device"); logger.error(error); throw new Error("\u274C Error Fetching Stream From Device"); } if (!fetchedStream) { logger.error("\u274C Stream Not Found, cannot do enableAudio"); throw new Error("\u274C Stream Not Found"); } stream = fetchedStream; } this.__activeStreams.set("audio", stream); this.emit("stream-fetched", { mediaKind: "mic", stream, label: "audio" }); this.produce({ label: "audio", stream, appData: { producerPeerId: this.peerId }, stopTrackOnClose: true, prefferedCodec: data?.prefferedCodec }).then(() => { this.__pendingProducerTasks.delete("audio"); }).catch((error) => { logger.error("\u274C Error Producing Audio"); this.deviceHandler.stopStream(this.__activeStreams.get("audio")); this.__activeStreams.delete("audio"); logger.error(error); }); this.__pendingFetchingStream.delete("mic"); return stream; } catch (error) { logger.error("\u274C Error Enabling Audio"); logger.error(error); this.deviceHandler.stopStream(this.__activeStreams.get("audio")); this.__activeStreams.delete("audio"); this.__pendingFetchingStream.delete("mic"); throw error; } } ); /** * Stops the underlying producing of a stream for a particular label * * `NOTE: This will notify all the RemotePeers that this producer has stopped producing and they should stop consuming it.` * * @param data Data to stop producing { label: string } */ stopProducing = (data) => { this.__waitingToProduce.delete(data.label); this.__pendingProducerTasks.delete(data.label); let closedStream = false; const producer = this.getProducerWithLabel(data.label); if (producer) { if (!producer.closed) producer.close(); closedStream = true; this.socket.publish("closeProducer", { producerId: producer.id }); } const closedStreamLabel = data.label.startsWith("screen-share") ? "screen-share" : data.label; const stream = this.__activeStreams.get(closedStreamLabel); if (stream) { this.deviceHandler.stopStream(stream); this.__activeStreams.delete(closedStreamLabel); closedStream = true; } if (closedStream) { this.emit("stream-closed", { label: data.label, reason: { code: 1200, tag: "STREAM_CLOSED", message: "Stopped Streaming" } }); } }; /** * Stops the underlying producing of a camera stream, stops the local track and closes the producer * * `NOTE: This will notify all the RemotePeers that this producer has stopped producing and they should stop consuming it. if you have joined the room, else it will just close the stream` * * @param data Data to stop producing { label: string } */ disableVideo = async () => { this.stopProducing({ label: "video" }); }; /** * Replaces the current video stream with the new stream * * if you have produced a stream with label `video` or used the default function `enableVideo` and you want to replace it with a new stream * @param stream - New Video Stream */ replaceVideoStream = async (stream) => { await this.replaceStream({ label: "video", newStream: stream }); }; /** * Changes the Video source to the given deviceId, sets the preferred cam device as the given deviceId * @param deviceId */ changeVideoSource = async (deviceId) => { this.deviceHandler.setPreferredDevice({ deviceId, deviceKind: "cam" }); const stream = this.__activeStreams.get("video"); if (!stream) { return; } const { stream: newStream } = await this.deviceHandler.fetchStream({ mediaDeviceKind: "cam" }); if (!newStream) return; await this.replaceVideoStream(newStream); }; /** * Replaces the current audio stream with the new stream * if you have produced a stream with label `audio` or used the default function `enableAudio` and you want to replace it with a new stream * @param stream - New Audio Stream * */ replaceAudioStream = async (stream) => { await this.replaceStream({ label: "audio", newStream: stream }); }; /** * Replace the current stream with the new stream based on the label used to produce the stream * * @example * For Video * await localPeer.replaceStream({ * label: 'video', * newStream: newStream * }) * * If any custom label used * await localPeer.replaceStream({ * label: 'custom', * newStream: newStream * }) * * @param data - { label: string, newStream: MediaStream } * @throws { Error } - If replace Stream failed */ replaceStream = async (data) => { logger.info(`\u{1F514} Replacing ${data.label} Stream `); const producer = this.getProducerWithLabel(data.label); const track = data.newStream.getTracks()[0]; if (track) { track.addEventListener("ended", () => { this.stopProducing({ label: data.label }); }); } if (producer) { if (producer.paused) { track.enabled = false; } await producer.replaceTrack({ track }); } const closedStreamLabel = data.label.startsWith("screen-share") ? "screen-share" : data.label; const prevStream = this.__activeStreams.get(closedStreamLabel); if (prevStream && !this.__waitingToProduce.has(closedStreamLabel)) { this.deviceHandler.stopStream(prevStream); this.__activeStreams.delete(closedStreamLabel); this.__activeStreams.set(closedStreamLabel, data.newStream); } else if (prevStream && this.__waitingToProduce.has(closedStreamLabel)) { for (const track2 of prevStream.getTracks()) { prevStream.removeTrack(track2); track2.stop(); } for (const track2 of data.newStream.getTracks()) { prevStream.addTrack(track2); } } const mediaDeviceKind = getMediaDeviceKind(track); this.emit("stream-fetched", { label: data.label, stream: data.newStream, mediaKind: mediaDeviceKind }); }; /** * Changes the Audio source to the given deviceId, sets the preferred mic device as the given deviceId * @param deviceId */ changeAudioSource = async (deviceId) => { this.deviceHandler.setPreferredDevice({ deviceId, deviceKind: "mic" }); const stream = this.__activeStreams.get("audio"); if (!stream) { return; } const { stream: newStream } = await this.deviceHandler.fetchStream({ mediaDeviceKind: "mic" }); if (!newStream) return; await this.replaceAudioStream(newStream); }; /** * Stops the underlying producing of a microphone stream, stops the local track and closes the producer * * `NOTE: This will notify all the RemotePeers that this producer has stopped producing and they should stop consuming it.` */ disableAudio = async () => { this.stopProducing({ label: "audio" }); }; /** * Pauses the audio stream, pauses the underlying producer but does not stop the local track - useful when user has joined the room and wants to pause the audio stream which is faster than disabling the audio stream * * `NOTE: Only use this function when peer has joined the room else use disableAudio, The LocalPeer will not be producing any audio stream but the local track will still be active, you can resume the audio stream using resumeAudio or stop the audio stream using disableAudio` * * @throws Error - If Pausing Audio Failed, it will disable the audio stream, and throw an error */ pauseAudio = async () => { try { logger.debug("\u{1F514} Pausing Audio Stream"); const audioStream = this.__activeStreams.get("audio"); if (!audioStream) { logger.error( "\u274C No Audio Stream Found, use enableAudio to enable audio first before pausing" ); return; } const track = audioStream.getAudioTracks()[0]; if (!track) { logger.error( "\u274C No Audio Track Found, use enableAudio to enable audio first before pausing" ); return; } const producer = this.getProducerWithLabel("audio"); if (!producer) { throw new Error("\u274C Cannot Pause Audio, Producer not found"); } producer.pause(); this.socket.publish("pauseProducer", { producerId: producer.id }); this.emit("stream-paused", { label: "audio", mediaKind: "mic" }); } catch (error) { logger.error("Error Pausing Audio", error); await this.disableAudio(); throw error; } }; /** * Resumes the audio stream, resumes the underlying producer and starts producing the audio stream - useful when user has `joined the room` and wants to resume the audio stream * * `Note: Works only if the audio stream is paused using pauseAudio` * * @throws Error - If Resuming Audio Failed, it will disable the audio stream, and throw an error */ resumeAudio = async () => { try { logger.debug("\u{1F514} Resuming Audio Stream"); const audioStream = this.__activeStreams.get("audio"); if (!audioStream) { logger.error( "\u274C No Audio Stream Found, use enableAudio to enable audio first before resuming" ); return; } const track = audioStream.getAudioTracks()[0]; if (!track) { logger.error( "\u274C No Audio Track Found, use enableAudio to enable audio first before resuming" ); return; } const producer = this.getProducerWithLabel("audio"); if (!producer) { throw new Error("\u274C Cannot Resume Audio, Producer not found"); } producer.resume(); this.socket.publish("resumeProducer", { producerId: producer.id }); this.emit("stream-playable", { label: "audio", producer }); } catch (error) { logger.error("Error Resuming Audio", error); await this.disableAudio(); throw error; } }; /** * Stops the underlying producing of a screen-share stream, stops the local track and closes the producer * * `NOTE: This will notify all the RemotePeers that this producer has stopped producing and they should stop consuming it. if you have joined the room, else it will just close the stream` */ stopScreenShare = async () => { try { this.stopProducing({ label: "screen-share-video" }); this.stopProducing({ label: "screen-share-audio" }); } catch (error) { logger.error("Error Disabling Screen Share", error); } }; /** * Consumes a stream with the producerId and peerId of the RemotePeer, appData is application level custom data which * can be added to the consumer for the LocalPeer, this data will be available in the consumer object and can be used only by the LocalPeer. * * `NOTE: This will not notify the RemotePeers that you are consuming a stream, you have to notify them manually` * @summary Every time a RemotePeer is producing a Media in the Room, LocalPeer will be notified about it and it will be able to consume it. * Consuming is a process where the media is received from the RemotePeer and the stream can be played on the LocalPeers device. * * To get the consumer back you can use * * const remotePeer = this.room.getRemotePeerById(data.peerId); * * `remotePeer.on('stream-playable', ({ label: string; consumer: Consumer }) => {})` * * @param data - {peerId: string, label: string, appData: Record<string, unknown>} * @throws { Error } - If Consuming Stream Failed */ consume = checkPermissions({ canConsume: true }).validate( async (data) => { const remotePeer = this.__remotePeers.get(data.peerId); if (!remotePeer) { throw new Error(`Remote Peer Not Found with PeerId ${data.peerId}`); } const labelData = remotePeer.getLabelData(data.label); if (!labelData) { throw new Error( `Remote Peer is not producing with Label ${data.label}` ); } const consumerExists = remotePeer.getConsumer(data.label); if (consumerExists?.consuming) { logger.warn("\u{1F514} Consumer Already Exists with label ", data.label); return consumerExists; } const pendingPromise = this.__pendingConsumerTasks.get( labelData?.producerId ); if (pendingPromise) { logger.warn( `\u{1F514} Consumer Task Pending to be Consumed with label ${data.label}, Returning` ); const consumer2 = await pendingPromise; return consumer2; } logger.info("\u{1F514} Consuming Stream with label ", data.label); if (!this.__recvTransport) { logger.info( "\u{1F514} Recv Transport Not Initialized, Creaitng RecvTransport" ); await this.__createTransportOnServer({ transportType: "recv" }); } const consumerPromise = new Promise((resolve, reject) => { const resolveConsumer = (streamData) => { if (streamData.label === data.label) { remotePeer.off("stream-playable", resolveConsumer); remotePeer.off("stream-available", resolveConsumer); const consumer2 = remotePeer.getConsumer(data.label); if (!consumer2) { reject(new Error("\u274C Consumer Not Found")); } else { if (consumer2.paused) { remotePeer.emit("stream-paused", { label: data.label, peerId: consumer2.producerPeerId, producerId: consumer2.producerId }); } resolve(consumer2); } } }; remotePeer.once("stream-playable", resolveConsumer); remotePeer.once("stream-available", resolveConsumer); this.socket.publish("consume", { appData: data.appData, producerId: labelData.producerId, producerPeerId: data.peerId }); }); this.__pendingConsumerTasks.set(labelData.producerId, consumerPromise); const consumer = await consumerPromise.catch((error) => { logger.error("\u274C Error Consuming Stream"); logger.error(error); throw error; }).finally(() => { this.__pendingConsumerTasks.delete(labelData.producerId); }); return consumer; } ); /** * Stops the underlying consuming of a stream for a particular label * * `NOTE: This does not notify the remote peers that you are not consuming a stream` * * @param data */ stopConsuming = (data) => { const remotePeer = this.room.getRemotePeerById(data.peerId); if (!remotePeer.hasLabel(data.label)) { logger.error( `\u274C Remote Peer is not producing anything with label: ${data.label}` ); return; } const consumer = this.recvTransport.getConsumer(data); if (!consumer) { logger.error("\u274C Consumer Not Found", data); return; } const consumerId = consumer.id; if (!consumerId) { logger.error("\u274C ConsumerId Not Found"); return; } this.socket.publish("closeConsumer", { consumerId: consumer.id }); remotePeer.emit("stream-closed", { label: data.label }); this.recvTransport.closeConsumer(data); }; /** * Activate Sending and Receving Volatile Messages, which uses WebRTC DataChannels over UDP * to send messages to the RemotePeers in the Room, this is faster than sending messages over WebSockets and has much lower Rate Limiting * Prefer using this for sending burst messages to the RemotePeers in the Room * @returns { ok: boolean, error?: Error } - error will be present if failed to activate volatile messaging */ __activateVolatileMessaging = async () => { try { logger.info("\u{1F514} Activating Volatile Messages"); const ongoingTask = this.__pendingTasks.get("volatileMessaging"); if (ongoingTask) { logger.debug("\u{1F514} Already Activating Volatile Message"); await ongoingTask; return { ok: true }; } const botDataConsumer = this.__sendTransport?.dataConsumers.get("bot"); if (botDataConsumer) { logger.debug("\u{1F514} Bot Data Consumer Already Exists"); return { ok: true }; } const ongoingActiveSpeakerPromise = this.__pendingTasks.get("volatileMessaging"); if (ongoingActiveSpeakerPromise) { logger.debug("\u{1F514} Bot Data Consumer Task Pending"); await ongoingActiveSpeakerPromise; return { ok: true }; } const fn = async () => { if (!this.__recvTransport) { await this.__createTransportOnServer({ transportType: "recv" }); } if (!this.__sendTransport) { await this.__createTransportOnServer({ transportType: "send" }); } await this.__createDataProducer({ label: "bot", maxRetransmits: 3, ordered: false }); }; const promise = fn(); this.__pendingTasks.set("volatileMessaging", promise); await promise; return { ok: true }; } catch (error) { logger.error( "\u274C Error Activating Speakers Notification Functionality", error ); return { ok: false, error: new Error( `Error: Activating Speakers Notification Functionality, ${error}` ) }; } }; /** * Send Data Gives the functionality to send data to other remote peers or the whole room, * NOTE: This will be sent over WebSockets which are over TCP which will be slower and can be rate limited. * @returns { success: boolean, error?: string } - error will be present if failed to send data */ sendData = checkPermissions({ canSendData: true }).validate( async (data) => { try { if (estimateSize(data.payload) > MAX_DATA_MESSAGE_SIZE) { logger.error("\u274C Data message exceeds 1Kb in size"); return { success: false, error: "Data message exceeds 1Kb in size" }; } const parsedTo = data.to === "*" ? ["*"] : data.to; this.socket.publish("sendData", { to: parsedTo, payload: data.payload, label: data.label }); return { success: true }; } catch (error) { logger.error("\u274C Error Sending Data"); logger.error(error); return { success: false, error: "Error Sending Data" }; } } ); /** * Send Volatile Data Gives the functionality to send data to other remote peers or the whole room, * NOTE: This will be sent over WebRTC DataChannels which are over UDP which will be faster and can be rate limited. * @returns { ok: boolean } - if the volatile data was sent, cannot guarantee if the data was received as its over UDP */ sendVolatileData = checkPermissions({ canSendData: true }).validate(async (data) => { try { if (!this.joined) { throw new Error( "\u274C Cannot Send Volatile Data, You have not joined the room yet" ); } if (!this.peerId) { throw new Error("\u274C Cannot Send Volatile Data, PeerId Not Found"); } if (estimateSize(data) > MAX_VOLATILE_DATA_MESSAGE_SIZE) { logger.error("\u274C Data message exceeds 1Mb in size"); return; } this.room.bot?.sendData({ from: this.peerId, label: data.label, payload: data.payload, to: "*" }); } catch (error) { logger.error("\u274C Error Sending Volatile Data", error); } }); /** * Send Message to update the metadata of the Local Peer * * `NOTE: This will notify every user in the room about the metadata update` * * @emits `localPeer.on(metadata-updated)` on succesful metadata update * * @throws { Error } - if unable to update Metadata for the peer. */ updateMetadata = checkPermissions({ canUpdateMetadata: true }).validate(async (data) => { if (!this.joined) { logger.error( "\u274C Cannot Update Metadata, You have not joined the room yet" ); return; } const peerId = this.peerId; if (!peerId) { logger.error("\u274C Cannot Update Metadata, PeerId Not Found"); return; } const newMetadata = JSON.stringify(data); this.socket.publish("updatePeerMetadata", { peerId, metadata: newMetadata }); }); /** * Update the role of the Remote Peer in the Room, this will emit an event `updated` with the updated role. * * @emits `localPeer.on('role-updated') in case role update was successful. */ updateRole = (data) => { try { if (!this.joined) { throw new Error( "\u274C Cannot Update Role, You have not joined the room yet" ); } if (data.role === this.role) { logger.warn("\u{1F514} Peer Role is already set to", data.role); return; } if (!this.peerId) { logger.error( "\u274C Cannot Update Role, PeerId Not Found, (You have not joined the room yet)" ); return; } this.socket.publish("updatePeerRole", { peerId: this.peerId, role: data.role, options: data.options }); } catch (error) { logger.error("\u{1F514} Error Updating Role", data); logger.error(error); } }; __handler = { error: (data) => { logger.error("\u274C Error Event"); logger.error(data); }, /** * When Huddle01 Node has successfully accepted the connection request * it sents back some usefull metadata for the client to use * at this point the socket is assumed to the connected and the localPeer is ready to join the room * * @param data - Data from server { peerId } */ hello: (data) => { logger.info("\u2705 Hello From Server, Connection Success", data); const { acl, peerId } = data; this.peerId = peerId; this.room.sessionId = data.sessionId; this.__permissions.updatePermissions(data.acl); if (data.role) this.__permissions.role = data.role; this.emit("permissions-updated", { permissions: acl, role: data.role }); if (data.metadata) { this.__update