@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
JavaScript
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 };