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.

674 lines (671 loc) 23.9 kB
import { parseToProtoRtpParameters, parseRtpParameters } from './chunk-2OLUNNR5.js'; import { EnhancedMap } from './chunk-MVJZAXK3.js'; import { Socket_default } from './chunk-RS7KXM4R.js'; import { Consumer_default } from './chunk-YROZGIK5.js'; import { codecOptionsViaKind, encodingViaMediaType } from './chunk-Q4PETRM3.js'; import { getMediaStreamKind, getMediaTrack } from './chunk-WA3QABYS.js'; import { mainLogger } from './chunk-TOCFOGTC.js'; import { EnhancedEventEmitter } from './chunk-BW2DGP4D.js'; // src/Transport.ts var logger = mainLogger.createSubLogger("Transport"); var Transport = class _Transport extends EnhancedEventEmitter { /** * Peer Id, which handles the peer id. */ peerId; /** * MediaSoup Device Instance, which handles the browsers or mobile device init. */ __device; /** * MediaSoup Transport Instance, which handles the media transport. */ __mediasoupTransport; /** * Socket Instance, which handles the socket connection. */ __socket; /** * Map of Producers, which handles the producers. ( Sending out Media Streams ) * * `Mapped with {producerId => Producer}` */ __producers = /* @__PURE__ */ new Map(); /** * Map of DataProducers, which handles the dataProducers. ( Sending out Data ) * * `Mapped with {label => DataProducer}` */ __dataProducers = /* @__PURE__ */ new Map(); /** * Map of DataConsumers, which handles the dataConsumers. ( Receiving Media Streams ) * * `Mapped with {label:label => DataConsumer}` */ __dataConsumers = /* @__PURE__ */ new Map(); /** * Map of Consumers, which handles the consumers. ( Receiving Media Streams ) * * `Mapped with {label:RemotePeerId => Consumer}` */ __consumers = new EnhancedMap({}); /** * Map of Identifiers to Producer Ids, which handles the mapping of identifiers to producer ids. * * `identifiers` are the unique identifiers for the stream, which is used to identify the stream. */ labelToProducerId = /* @__PURE__ */ new Map(); /** * Transport Type, which handles the transport type. ( `send | recv` ) */ transportType; /** * Pending Producer Tasks, which handles the pending producer tasks. * callback function is necessary to be called when the producer is created * on the server as well as on the client side. */ __pendingProducerTasks = /* @__PURE__ */ new Map(); /** * Debounce to handle concurrent request to restart Ice. Waits for some time before sending * more requests to restart ice. */ __iceRestartDebounce = false; /** * WebRTC Device whose APIs for Connection Creation is used. */ get device() { return this.__device; } /** * WebRTC Connection, Which handles the producing and consuming of media. */ get mediasoupTransport() { return this.__mediasoupTransport; } /** * WebRTC Connection State. */ get connectionState() { return this.__mediasoupTransport.connectionState; } /** * Returns the Map of Producers, which handles the producers. ( Sending out Media Streams ) */ get producers() { return this.__producers; } /** * Returns the ids of the producers, which handles the producers. ( Sending out Media Streams ) */ get producerIds() { const producerIds = Array.from(this.__producers.keys()); return producerIds; } /** * Type of data which is supposed to be produced in the room. ( ArrayBuffer ) */ get dataProducers() { return this.__dataProducers; } /** * Type of data which is supposed to be consumed from the room. ( ArrayBuffer ) */ get dataConsumers() { return this.__dataConsumers; } /** * Entity which is supposed to consume the media stream. ( MediaStreamTrack ) */ get consumers() { return this.__consumers; } /** * Get a producer by its id ( ProducerId is generated by the Media Node ) * @param producerId - Producer Id of the producer to be fetched. * @returns - Producer * @throws - Throws an error if the producer is not found. */ getProducerById(producerId) { const producer = this.__producers.get(producerId); if (!producer) throw new Error("Producer not found"); return producer; } /** * Get the consumer by label and peerId * @param data * @returns Consumer | null; Returns null if consumer is not found */ getConsumer = (data) => { const consumer = this.__consumers.get(data.label, data.peerId); if (!consumer) { return null; } return consumer; }; /** * Getter for the transport instance. */ get transport() { const transport = this.__mediasoupTransport; if (!transport) throw new Error("Transport Not Initialized"); return transport; } /** * Every time a producer is created, it is added to the pending producer tasks. * Upon ack from the Media Node the producer is resolved and removed from the pending producer tasks. ( Using resolvePendingProducerTask ) * This is done to handle concurrency in the producer creation. * @param data - { label?: string, peerId: string, callback: ({ id }) => void } * @returns - void */ addPendingProducerTask = (data) => { const key = `${data.peerId}-${data?.label}`; logger.info("\u{1F514} Adding Pending Producer Task, key", key); if (this.__pendingProducerTasks.has(key)) { logger.debug("\u{1F534} Producer Creation is Pending for key: ", key); return; } this.__pendingProducerTasks.set(key, data.callback); }; /** * Every time a producer is created, it is added to the pending producer tasks. * Upon ack from the Media Node the producer is resolved and removed from the pending producer tasks. * This is done to handle concurrency in the producer creation. * @param data - { label: string, peerId: string, id: string } * @returns - void */ resolvePendingProducerTask = (data) => { const key = `${data.peerId}-${data.label}`; logger.info("\u{1F514} Resolving Pending Producer Task, key", key); const callback = this.__pendingProducerTasks.get(key); if (!callback) { logger.error("\u{1F534} Producer Creation is not Pending for key: ", key); return; } callback({ id: data.id }); this.__pendingProducerTasks.delete(key); }; /** * Creates a new Transport Instance, Transports are used to send and receive media streams or data. * Transport for the SDK mean a WebRTC Connection. * @param data * @returns */ static create = (data) => { try { logger.info( `\u{1F514} Creating Client Side Transport, type: ${data.transportType}` ); const { transportType, device } = data; const payload = { id: data.sdpInfo.id, iceParameters: data.sdpInfo.iceParameters, iceCandidates: data.sdpInfo.iceCandidates, iceServers: data.iceServers, dtlsParameters: data.sdpInfo.dtlsParameters, sctpParameters: data.sdpInfo.sctpParameters, proprietaryConstraints: {}, appData: {} }; const mediasoupTransport = transportType === "send" ? device.createSendTransport(payload) : device.createRecvTransport(payload); const transport = new _Transport({ peerId: data.peerId, device: data.device, transportType: data.transportType, mediasoupTransport }); return transport; } catch (error) { logger.error(`\u274C Transport.create(), type: ${data.transportType}`); logger.error(error); throw error; } }; constructor(data) { super(); this.__socket = Socket_default.getInstance(); this.__device = data.device; this.transportType = data.transportType; this.__mediasoupTransport = data.mediasoupTransport; this.__mediasoupTransport.on("connectionstatechange", (state) => { this.__connectionStateChangeHandler(state); }); this.peerId = data.peerId; this.__listenTransportConnect(); this.__listenTransportProduce(); if (this.transportType === "send") this.__listenTransportDataProduce(); logger.info(`\u2705 ${data.transportType} Transport Initialized`); } /** * Function to handle the event when transport is supposed to be connected. ( WebRTC ) */ __listenTransportConnect = () => { this.__mediasoupTransport.on( "connect", ({ dtlsParameters }, callback, errback) => { logger.info("\u{1F514} Transport Connect Event Called"); try { this.once("connectTransportResponse", () => { callback(); }); this.__socket.publish("connectTransport", { dtlsParameters, transportType: this.transportType }); } catch (error) { logger.error("\u274C Error Transport Connect Event"); logger.error(error); errback(error); } } ); }; /** * Function to handle the event when media is supposed produced over a Transport ( WebRTC ) */ __listenTransportProduce = () => { this.__mediasoupTransport.on( "produce", async ({ kind, rtpParameters, appData }, callback, errback) => { logger.info(`\u{1F514} ${this.transportType} Produce Event Called`); try { const label = appData?.label; if (!label) throw new Error("\u{1F534} Stream Identifier Not Found"); const parsedProtoRtpParameters = parseToProtoRtpParameters(rtpParameters); this.__socket.publish("produce", { rtpParameters: parsedProtoRtpParameters, kind, label, appData, paused: false }); this.addPendingProducerTask({ peerId: this.peerId, label, callback }); } catch (error) { logger.error("\u274C Error Transport Produce Event"); logger.error(error); errback(error); } } ); }; /** * Function to handle the event when data is supposed produced over a Transport ( WebRTC DataChannel ) */ __listenTransportDataProduce = () => { logger.info(`\u{1F514} producedata: ${this.transportType} `); this.__mediasoupTransport.on( "producedata", async ({ label, appData, sctpStreamParameters, protocol }, callback, errback) => { logger.info(`\u{1F514} ${this.transportType} Produce Data Event Called`); try { this.__socket.publish("produceData", { transportId: this.__mediasoupTransport.id, sctpStreamParameters, label, protocol, appData }); this.addPendingProducerTask({ peerId: this.peerId, label, callback }); } catch (error) { logger.error("\u274C Error Transport Produce Data Event"); logger.error(error); errback(error); } } ); }; /** * Function to handle the producing of a media stream to the room. * @param data - { stream: MediaStream, label: string, stopTrackOnClose: boolean, appData?: AppData } * @returns - Producer * NOTE: PrefferedCodec is optional, if not provided, H264 is used as the default codec. * is device does not support the passed codec, an error is thrown. */ produce = async (data) => { const kind = getMediaStreamKind(data.stream); const track = getMediaTrack({ stream: data.stream, kind }); logger.info(`\u{1F514} Produce Called for kind: ${kind}, label: ${data.label}`); try { if (!this.__device.loaded) { throw new Error("Device Not Loaded"); } if (!this.__device.rtpCapabilities.codecs) { throw new Error("No Codecs Found"); } if (!this.__device.canProduce(kind)) { throw new Error(`Device Cannot produce ${kind}`); } if (this.transportType !== "send") { throw new Error(`Cannot produce on ${this.transportType} transport`); } const codecs = this.__device.rtpCapabilities?.codecs; if (!codecs) { throw new Error("\u274C Device RTP Capabilities not found"); } const videoCodecMimeType = data.prefferedCodec ?? "video/h264"; const videoCodec = codecs.find( (codec) => codec.mimeType.toLowerCase() === videoCodecMimeType ); if (!videoCodec) { throw new Error( `\u274C Preffered codec: ${data.prefferedCodec} not found on the device` ); } const codecViaMediaType = { video: videoCodec, "screen-share-video": videoCodec, audio: void 0 }; const mediaType = data.label === "screen-share-video" ? "screen-share-video" : kind; const mediasoupProducer = await this.__mediasoupTransport.produce({ track, encodings: encodingViaMediaType[mediaType], codecOptions: codecOptionsViaKind[kind], codec: codecViaMediaType[mediaType], stopTracks: data.stopTrackOnClose, zeroRtpOnPause: true, disableTrackOnPause: true, appData: { ...data.appData, producerPeerId: this.peerId } }); mediasoupProducer.on("@close", () => { logger.info("\u{1F514} Closing Producer", { label: data.label, producerId: mediasoupProducer.id }); this.__producers.delete(mediasoupProducer.id); this.labelToProducerId.delete(data.label); }); this.__producers.set(mediasoupProducer.id, mediasoupProducer); this.labelToProducerId.set(data.label, mediasoupProducer.id); logger.info( `\u{1F514} Client Side Producer Created sucessfully with label : ${data.label}` ); return mediasoupProducer; } catch (error) { logger.error("\u274C Error Transport Produce Event"); logger.error(error); throw error; } }; /** * Function to handle the producing of a data stream to the room. * @param data - { label: string, ordered?: boolean, maxRetransmits?: number, maxPacketLifeTime?: number } * - `label` is the unique identifier for the data stream, Cannot use `bot` which is reserved. * - `ordered` is the boolean value to determine if the data should be ordered or not, or reliably. * - `maxRetransmits` When ordered is false indicates the maximum number of times a packet will be retransmitted. * - `maxPacketLifeTime` When ordered is false indicates the time (in milliseconds) after which a SCTP packet will stop being retransmitted. * @returns - DataProducer */ produceData = async (options) => { logger.info(`\u{1F514} Produce Data Called for label: ${options.label}`); try { if (this.transportType !== "send") { throw new Error( `Cannot produceData on ${this.transportType} transport` ); } const label = this.dataProducers.get(options.label); if (label) { throw new Error( `DataProducer with label ${options.label} already exists, please close it before creating a new one` ); } if (!this.__device.loaded) { throw new Error("Device Not Loaded"); } if (!this.__device.sctpCapabilities) { throw new Error("No SCTP Capabilities Found"); } const dataProducer = await this.__mediasoupTransport.produceData({ label: options.label, ordered: options.ordered ?? false, maxPacketLifeTime: options.maxPacketLifeTime, maxRetransmits: options.maxRetransmits }); dataProducer.on("open", () => { logger.info("\u2705 DataProducer opened", options); }); dataProducer.on("@close", () => { logger.info("\u{1F514} Closing DataProducer", { label: options.label, dataProducerId: dataProducer.id }); this.__dataProducers.delete(dataProducer.label); }); dataProducer.on("transportclose", () => { this.__dataProducers.delete(dataProducer.label); }); this.__dataProducers.set(dataProducer.label, dataProducer); logger.info( `\u{1F514} Client Side DataProducer Created sucessfully with label : ${options.label}` ); return dataProducer; } catch (error) { logger.error("\u274C Error Transport Produce Data Event", error); throw error; } }; /** * @description - Close the data producer by label * @param label - Label of the data producer to be closed */ closeDataProducer = (label) => { const dataProducer = this.__dataProducers.get(label); dataProducer?.close(); this.__dataProducers.delete(label); logger.info(`\u{1F514} DataProducer with label ${label} closed`); }; /** * Consume means to receive the media stream from the room. ( MediaStreamTrack ) * Using WebRTC Connection to receive the media stream. * @param data - { producerPeerId: string, kind: string, rtpParameters: any, appData?: AppData } * @returns - { consumer: Consumer, mediaSoupConsumer: mediasoup.types.Consumer } */ consume = async (data) => { const { label, producerPeerId, kind } = data; logger.info( `\u{1F514} Consume Called for ${kind} from remote peer ${producerPeerId}` ); try { if (this.transportType !== "recv") { throw new Error(`Cannot consume on ${this.transportType} transport`); } if (!this.__device.loaded) { throw new Error("Device Not Loaded"); } if (!this.__device.rtpCapabilities.codecs) { throw new Error("No Codecs Found"); } const parsedRtpParameters = parseRtpParameters(data.rtpParameters); const consumer = Consumer_default.create({ producerId: data.producerId, producerPaused: data.producerPaused ?? false, producerPeerId, label }); const mediaSoupConsumer = await this.__mediasoupTransport.consume({ id: data.consumerId, rtpParameters: parsedRtpParameters, kind: data.kind, producerId: data.producerId, appData: data.appData }); mediaSoupConsumer.on("@close", () => { this.__consumers.delete(label, producerPeerId); }); mediaSoupConsumer.on("transportclose", () => { this.closeConsumer({ label, peerId: producerPeerId }); }); mediaSoupConsumer.on("trackended", () => { this.closeConsumer({ label, peerId: producerPeerId }); }); this.__consumers.set(consumer.label, consumer.producerPeerId, consumer); consumer.setMediaSoupConsumer(mediaSoupConsumer); return { consumer, mediaSoupConsumer }; } catch (error) { logger.error(error); throw new Error("\u274C Error calling consume()"); } }; /** * Consume means to receive the data stream from the room. ( ArrayBuffer ) * Using WebRTC Connection to receive the data stream. * @param data - { label: string, appData?: AppData, dataProducerId: string, protocol: string, id: string, peerId: string, sctpStreamParameters: any } * @returns - DataConsumer */ consumeData = async (data) => { const { label, appData, dataProducerId, protocol, id, peerId, sctpStreamParameters } = data; logger.info( `\u{1F514} ConsumeData from producer ${peerId} consumerIdFromServer:${id}` ); try { if (this.transportType !== "recv") { throw new Error(`Cannot consume on ${this.transportType} transport`); } if (!this.__device.loaded) { throw new Error("Device Not Loaded"); } if (!this.__device.rtpCapabilities.codecs) { throw new Error("No Codecs Found"); } const dataConsumer = await this.transport.consumeData({ id, dataProducerId, sctpStreamParameters: { ...sctpStreamParameters, maxPacketLifeTime: sctpStreamParameters.maxPacketLifeTime === 0 ? void 0 : sctpStreamParameters.maxPacketLifeTime, maxRetransmits: sctpStreamParameters.maxRetransmits === 0 ? void 0 : sctpStreamParameters.maxRetransmits }, label, protocol, appData }); dataConsumer.on("open", () => { logger.info(`\u2705 DataConsumer with ${label} opened`); }); dataConsumer.on("close", () => { logger.warn(`\u2705 DataConsumer with ${label} closed`); dataConsumer.close(); dataConsumer.removeAllListeners(); this.__dataConsumers.delete(label); }); dataConsumer.on("transportclose", () => { console.info(`\u2705 DataConsumer with ${label} closed`); dataConsumer.close(); dataConsumer.removeAllListeners(); this.__dataConsumers.delete(label); }); dataConsumer.on("error", (error) => { logger.error(`\u2705 DataConsumer "error": ${error} closed`); dataConsumer.close(); dataConsumer.removeAllListeners(); this.__dataConsumers.delete(label); }); this.__dataConsumers.set(label, dataConsumer); return dataConsumer; } catch (error) { logger.error(error); throw new Error("\u274C Error calling consumeData()"); } }; /** * Close the consumer by label and peerId * @param data - { label: string, peerId: string } */ closeConsumer = (data) => { const consumer = this.getConsumer(data); consumer?.close(); this.__consumers.delete(data.label, data.peerId); }; /** * Close the underlying transport * @param data - { retries: number } * @returns - void */ close = async (data) => { try { if (data.retries <= 0) { logger.error("\u274C Error closing transport, max retries exceeded"); return; } logger.info(`\u{1F514} Closing ${this.transportType} transport`); this.__mediasoupTransport.close(); this.__producers.clear(); this.__consumers.clear(); this.__dataProducers.clear(); this.__dataConsumers.clear(); logger.info(`\u2705 ${this.transportType} transport closed`); } catch (error) { logger.error("\u274C Error closing transport"); logger.error(error); logger.error("Retrying..."); this.close({ retries: data.retries - 1 }); } }; /** * WebRTC Connection is prone to connection state changes, as its a peer to peer connection. * This function handles the connection state changes. and re establishes the connection if it is lost. * @param state - ConnectionState */ __connectionStateChangeHandler = (state) => { try { logger.debug( `\u{1F514} ${this.transportType} Transport Connection State Changed, state: ${state}` ); const transportType = this.transportType; const handler = { connected: () => { logger.debug(`\u{1F514} ${this.transportType} Transport Connected`); }, disconnected: () => { if (this.__iceRestartDebounce) return; this.__iceRestartDebounce = true; this.__socket.publish("restartTransportIce", { transportId: this.__mediasoupTransport.id, transportType }); setTimeout(() => { this.__iceRestartDebounce = false; }, 3e3); logger.debug(`\u{1F514} ${transportType} Transport Disconnected`); }, failed: () => { logger.debug(`\u{1F514} ${transportType} Transport Failed`); }, connecting: () => { logger.debug(`\u{1F514} ${transportType} Transport Connecting`); }, closed: () => { logger.debug(`\u{1F514} ${transportType} Transport closed`); }, new: () => { logger.debug(`\u{1F514} ${transportType} Transport new`); } }; handler[state](); } catch (err) { logger.error("\u274C Error in connectionStateChangeHandler"); logger.error(err); } }; }; var Transport_default = Transport; export { Transport_default };