@jellyfish-dev/membrane-webrtc-js
Version:
JS/TS client library for Membrane RTC Engine.
1,073 lines (1,072 loc) • 40.4 kB
JavaScript
var w = Object.defineProperty;
var C = (l, d, t) => d in l ? w(l, d, { enumerable: !0, configurable: !0, writable: !0, value: t }) : l[d] = t;
var o = (l, d, t) => (C(l, typeof d != "symbol" ? d + "" : d, t), t);
import { v4 as y } from "uuid";
import I from "events";
function b(l) {
return JSON.stringify(l);
}
function M(l) {
return JSON.parse(l);
}
function T(l, d) {
let t = { type: l };
return d && (t = { ...t, data: d }), t;
}
function g(l) {
return T("custom", l);
}
const P = {
direction: "sendonly",
// keep this array from low resolution to high resolution
// in other case lower resolution encoding can get
// higher max_bitrate
sendEncodings: [
{
rid: "l",
active: !1,
// maxBitrate: 4_000_000,
scaleResolutionDownBy: 4
// scalabilityMode: "L1T" + TEMPORAL_LAYERS_COUNT,
},
{
rid: "m",
active: !1,
scaleResolutionDownBy: 2
},
{
rid: "h",
active: !1
// maxBitrate: 4_000_000,
// scalabilityMode: "L1T" + TEMPORAL_LAYERS_COUNT,
}
]
}, R = { audio: 5e4, video: 25e5 }, S = {
h: 25e5,
m: 5e5,
l: 15e4
};
class f {
constructor() {
o(this, "promise");
o(this, "resolveFn", () => null);
o(this, "rejectFn", () => null);
this.promise = new Promise((d, t) => {
this.resolveFn = d, this.rejectFn = t;
});
}
resolve(d) {
this.resolveFn(d);
}
reject(d) {
this.rejectFn(d);
}
}
const B = ["speech", "silence"];
class v extends I {
constructor(t, a, e, r, n) {
super();
o(this, "endpoint");
o(this, "trackId");
o(this, "track", null);
o(this, "stream", null);
o(this, "metadata");
o(this, "rawMetadata");
o(this, "metadataParsingError");
o(this, "simulcastConfig");
o(this, "maxBandwidth", 0);
o(this, "encoding");
o(this, "encodingReason");
o(this, "vadStatus", "silence");
o(this, "negotiationStatus", "awaiting");
// Indicates that metadata were changed when in "offered" negotiationStatus
// and `updateTrackMetadata` Media Event should be sent after the transition to "done"
o(this, "pendingMetadataUpdate", !1);
this.endpoint = t, this.trackId = a;
try {
this.metadata = n(e);
} catch (i) {
this.metadataParsingError = i;
}
this.rawMetadata = e, this.simulcastConfig = r;
}
}
class O extends I {
constructor(t) {
var a, e;
super();
o(this, "trackIdToTrack", /* @__PURE__ */ new Map());
o(this, "connection");
o(this, "idToEndpoint", /* @__PURE__ */ new Map());
o(this, "localEndpoint", {
id: "",
type: "webrtc",
metadata: void 0,
rawMetadata: void 0,
tracks: /* @__PURE__ */ new Map()
});
o(this, "localTrackIdToTrack", /* @__PURE__ */ new Map());
o(this, "midToTrackId", /* @__PURE__ */ new Map());
o(this, "disabledTrackEncodings", /* @__PURE__ */ new Map());
o(this, "rtcConfig", {
bundlePolicy: "max-bundle",
iceServers: [],
iceTransportPolicy: "relay"
});
/**
* Indicates if an ongoing renegotiation is active.
* During renegotiation, both parties are expected to actively exchange events: renegotiateTracks, offerData, sdpOffer, sdpAnswer.
*/
o(this, "ongoingRenegotiation", !1);
o(this, "ongoingTrackReplacement", !1);
o(this, "commandsQueue", []);
o(this, "commandResolutionNotifier", null);
o(this, "endpointMetadataParser");
o(this, "trackMetadataParser");
/**
* Tries to connect to the RTC Engine. If user is successfully connected then {@link WebRTCEndpointEvents.connected}
* will be emitted.
*
* @param metadata - Any information that other endpoints will receive in {@link WebRTCEndpointEvents.endpointAdded}
* after accepting this endpoint
*
* @example
* ```ts
* let webrtc = new WebRTCEndpoint();
* webrtc.connect({displayName: "Bob"});
* ```
*/
o(this, "connect", (t) => {
try {
this.localEndpoint.metadata = this.endpointMetadataParser(t), this.localEndpoint.metadataParsingError = void 0;
} catch (e) {
throw this.localEndpoint.metadata = void 0, this.localEndpoint.metadataParsingError = e, e;
}
this.localEndpoint.rawMetadata = t;
const a = T("connect", {
metadata: this.localEndpoint.metadata
});
this.sendMediaEvent(a);
});
/**
* Feeds media event received from RTC Engine to {@link WebRTCEndpoint}.
* This function should be called whenever some media event from RTC Engine
* was received and can result in {@link WebRTCEndpoint} generating some other
* media events.
*
* @param mediaEvent - String data received over custom signalling layer.
*
* @example
* This example assumes phoenix channels as signalling layer.
* As phoenix channels require objects, RTC Engine encapsulates binary data into
* map with one field that is converted to object with one field on the TS side.
* ```ts
* webrtcChannel.on("mediaEvent", (event) => webrtc.receiveMediaEvent(event.data));
* ```
*/
o(this, "receiveMediaEvent", (t) => {
const a = M(t);
switch (a.type) {
case "connected": {
this.localEndpoint.id = a.data.id;
const r = a.data.otherEndpoints.map((n) => {
const i = this.mapMediaEventTracksToTrackContextImpl(
new Map(Object.entries(n.tracks)),
n
);
try {
return {
id: n.id,
type: n.type,
metadata: this.endpointMetadataParser(n.metadata),
rawMetadata: n.metadata,
metadataParsingError: void 0,
tracks: i
};
} catch (c) {
return {
id: n.id,
type: n.type,
metadata: void 0,
rawMetadata: n.metadata,
metadataParsingError: c,
tracks: i
};
}
});
this.emit("connected", a.data.id, r), r.forEach((n) => this.idToEndpoint.set(n.id, n)), r.forEach((n) => {
n.tracks.forEach((i, c) => {
this.trackIdToTrack.set(c, i), this.emit("trackAdded", i);
});
});
break;
}
default:
this.localEndpoint.id != null && this.handleMediaEvent(a);
}
});
o(this, "handleMediaEvent", (t) => {
let a, e;
switch (t.type) {
case "offerData": {
const r = t.data.integratedTurnServers;
this.setTurns(r);
const n = new Map(Object.entries(t.data.tracksTypes));
this.onOfferData(n);
break;
}
case "tracksAdded": {
if (this.ongoingRenegotiation = !0, e = t.data, this.getEndpointId() === e.endpointId)
return;
e.tracks = new Map(Object.entries(e.tracks)), a = this.idToEndpoint.get(e.endpointId);
const r = a.tracks;
e.tracks = this.mapMediaEventTracksToTrackContextImpl(e.tracks, a), a.tracks = new Map([...a.tracks, ...e.tracks]), this.idToEndpoint.set(a.id, a), Array.from(a.tracks.entries()).forEach(([n, i]) => {
r.has(n) || (this.trackIdToTrack.set(n, i), this.emit("trackAdded", i));
});
break;
}
case "tracksRemoved": {
this.ongoingRenegotiation = !0, e = t.data;
const r = e.endpointId;
if (this.getEndpointId() === r)
return;
e.trackIds.forEach((i) => {
const c = this.trackIdToTrack.get(i);
this.eraseTrack(i, r), this.emit("trackRemoved", c);
});
break;
}
case "sdpAnswer":
this.midToTrackId = new Map(Object.entries(t.data.midToTrackId));
for (const r of Object.values(t.data.midToTrackId)) {
const n = this.localTrackIdToTrack.get(r);
if (n) {
if (n.negotiationStatus = "done", n.pendingMetadataUpdate) {
const i = T("updateTrackMetadata", {
trackId: r,
trackMetadata: n.metadata
});
this.sendMediaEvent(i);
}
n.pendingMetadataUpdate = !1;
}
}
this.onAnswer(t.data), this.ongoingRenegotiation = !1, this.processNextCommand();
break;
case "candidate":
this.onRemoteCandidate(t.data);
break;
case "endpointAdded":
if (a = t.data, a.id === this.getEndpointId())
return;
a.rawMetadata = a.metadata;
try {
a.metadataParsingError = void 0, a.metadata = this.endpointMetadataParser(a.rawMetadata);
} catch (r) {
a.metadataParsingError = r, a.metadata = void 0;
}
this.addEndpoint(a), this.emit("endpointAdded", a);
break;
case "endpointRemoved":
if (t.data.id === this.localEndpoint.id) {
this.cleanUp(), this.emit("disconnected");
return;
}
if (a = this.idToEndpoint.get(t.data.id), a === void 0)
return;
Array.from(a.tracks.keys()).forEach((r) => {
this.emit("trackRemoved", this.trackIdToTrack.get(r));
}), this.eraseEndpoint(a), this.emit("endpointRemoved", a);
break;
case "endpointUpdated":
if (this.getEndpointId() === t.data.id)
return;
a = this.idToEndpoint.get(t.data.id);
try {
a.metadata = this.endpointMetadataParser(t.data.metadata), a.metadataParsingError = void 0;
} catch (r) {
a.metadata = void 0, a.metadataParsingError = r;
}
a.rawMetadata = t.data.metadata, this.addEndpoint(a), this.emit("endpointUpdated", a);
break;
case "trackUpdated": {
if (this.getEndpointId() === t.data.endpointId)
return;
if (a = this.idToEndpoint.get(t.data.endpointId), a == null)
throw `Endpoint with id: ${t.data.endpointId} doesn't exist`;
const r = t.data.trackId, n = t.data.metadata;
let i = a.tracks.get(r);
const c = this.trackIdToTrack.get(r);
try {
const s = this.trackMetadataParser(n);
i = { ...i, metadata: s, metadataParsingError: void 0 }, c.metadata = s, c.metadataParsingError = void 0;
} catch (s) {
i = { ...i, metadata: void 0, metadataParsingError: s }, c.metadataParsingError = s, c.metadata = void 0;
}
i = { ...i, rawMetadata: n }, c.rawMetadata = n, a.tracks.set(r, i), this.emit("trackUpdated", c);
break;
}
case "trackEncodingDisabled": {
if (this.getEndpointId() === t.data.endpointId)
return;
if (a = this.idToEndpoint.get(t.data.endpointId), a == null)
throw `Endpoint with id: ${t.data.endpointId} doesn't exist`;
const r = t.data.trackId, n = t.data.encoding, i = a.tracks.get(r);
this.emit("trackEncodingDisabled", i, n);
break;
}
case "trackEncodingEnabled": {
if (this.getEndpointId() === t.data.endpointId)
return;
if (a = this.idToEndpoint.get(t.data.endpointId), a == null)
throw `Endpoint with id: ${t.data.endpointId} doesn't exist`;
const r = t.data.trackId, n = t.data.encoding, i = a.tracks.get(r);
this.emit("trackEncodingEnabled", i, n);
break;
}
case "tracksPriority": {
const r = t.data.tracks.map(
(i) => this.trackIdToTrack.get(i)
), n = Array.from(this.trackIdToTrack.values()).filter(
(i) => !r.includes(i)
);
this.emit("tracksPriorityChanged", r, n);
break;
}
case "encodingSwitched": {
const r = t.data.trackId, n = this.trackIdToTrack.get(r);
n.encoding = t.data.encoding, n.encodingReason = t.data.reason, n.emit("encodingChanged", n);
break;
}
case "custom":
this.handleMediaEvent(t.data);
break;
case "error":
this.emit("connectionError", t.data.message), this.disconnect();
break;
case "vadNotification": {
const r = t.data.trackId, n = this.trackIdToTrack.get(r), i = t.data.status;
B.includes(i) ? (n.vadStatus = i, n.emit("voiceActivityChanged", n)) : console.warn("Received unknown vad status: ", i);
break;
}
case "bandwidthEstimation": {
const r = t.data.estimation;
this.emit("bandwidthEstimationChanged", r);
break;
}
default:
console.warn("Received unknown media event: ", t.type);
break;
}
});
o(this, "addTrackToConnection", (t) => {
const a = this.createTransceiverConfig(t), e = t.track;
this.connection.addTransceiver(e, a);
});
/**
* Updates the metadata for the current endpoint.
* @param metadata - Data about this endpoint that other endpoints will receive upon being added.
*
* If the metadata is different from what is already tracked in the room, the optional
* event `endpointUpdated` will be emitted for other endpoint in the room.
*/
o(this, "updateEndpointMetadata", (t) => {
this.localEndpoint.metadata = this.endpointMetadataParser(t), this.localEndpoint.rawMetadata = this.localEndpoint.metadata, this.localEndpoint.metadataParsingError = void 0;
const a = T("updateEndpointMetadata", {
metadata: this.localEndpoint.metadata
});
this.sendMediaEvent(a);
});
/**
* Updates the metadata for a specific track.
* @param trackId - trackId (generated in addTrack) of audio or video track.
* @param trackMetadata - Data about this track that other endpoint will receive upon being added.
*
* If the metadata is different from what is already tracked in the room, the optional
* event `trackUpdated` will be emitted for other endpoints in the room.
*/
o(this, "updateTrackMetadata", (t, a) => {
const e = this.localTrackIdToTrack.get(t), r = this.localEndpoint.tracks.get(t);
try {
e.metadata = this.trackMetadataParser(a), e.metadataParsingError = void 0, this.localEndpoint.tracks.set(t, {
...r,
metadata: e.metadata,
metadataParsingError: void 0
});
} catch (i) {
throw e.metadata = void 0, e.metadataParsingError = i, this.localEndpoint.tracks.set(t, { ...r, metadata: void 0, metadataParsingError: i }), i;
}
e.rawMetadata = e.metadata, this.localEndpoint.tracks.set(t, { ...r, rawMetadata: e.metadata }), this.localTrackIdToTrack.set(t, e);
const n = T("updateTrackMetadata", {
trackId: t,
trackMetadata: e.metadata
});
switch (e.negotiationStatus) {
case "done":
this.sendMediaEvent(n);
break;
case "offered":
e.pendingMetadataUpdate = !0;
break;
}
});
o(this, "getMidToTrackId", () => {
const t = {};
return this.connection ? (this.connection.getTransceivers().forEach((a) => {
var n;
const e = (n = a.sender.track) == null ? void 0 : n.id, r = a.mid;
if (e && r) {
const i = Array.from(this.localTrackIdToTrack.values()).find(
(c) => c.track.id === e
);
t[r] = i.trackId;
}
}), t) : null;
});
/**
* Disconnects from the room. This function should be called when user disconnects from the room
* in a clean way e.g. by clicking a dedicated, custom button `disconnect`.
* As a result there will be generated one more media event that should be
* sent to the RTC Engine. Thanks to it each other endpoint will be notified
* that endpoint was removed in {@link WebRTCEndpointEvents.endpointRemoved},
*/
o(this, "disconnect", () => {
const t = T("disconnect");
this.sendMediaEvent(t), this.cleanUp();
});
/**
* Cleans up {@link WebRTCEndpoint} instance.
*/
o(this, "cleanUp", () => {
this.connection && (this.connection.onicecandidate = null, this.connection.ontrack = null, this.connection.onconnectionstatechange = null, this.connection.onicecandidateerror = null, this.connection.oniceconnectionstatechange = null), this.connection = void 0;
});
o(this, "sendMediaEvent", (t) => {
const a = b(t);
this.emit("sendMediaEvent", a);
});
o(this, "onAnswer", async (t) => {
this.connection.ontrack = this.onTrack();
try {
await this.connection.setRemoteDescription(t), this.disabledTrackEncodings.forEach((a, e) => {
a.forEach((r) => this.disableTrackEncoding(e, r));
});
} catch (a) {
console.error(a);
}
});
o(this, "addTransceiversIfNeeded", (t) => {
var c;
const a = this.connection.getTransceivers().filter((s) => s.direction === "recvonly");
let e = [];
const r = (s) => {
let h = t.get(s);
h = h !== void 0 ? h : 0;
const k = a.filter((m) => m.receiver.track.kind === s).length;
return Array(h - k).fill(s);
}, n = r("audio"), i = r("video");
e = e.concat(n), e = e.concat(i);
for (const s of e)
(c = this.connection) == null || c.addTransceiver(s, { direction: "recvonly" });
});
o(this, "getTrackIdToMetadata", () => {
const t = {};
return Array.from(this.localEndpoint.tracks.entries()).forEach(([a, { metadata: e }]) => {
t[a] = e;
}), t;
});
o(this, "getTrackBitrates", (t) => {
const a = this.localTrackIdToTrack.get(t);
if (!a)
throw "Track with id ${trackId} not present in 'localTrackIdToTrack'";
const e = a.track.kind, n = this.findSender(a.track.id).getParameters().encodings;
if (n.length == 1 && !n[0].rid)
return n[0].maxBitrate || R[e];
if (e == "audio")
throw "Audio track cannot have multiple encodings";
const i = {};
return n.filter((c) => c.rid).forEach((c) => {
const s = c.rid;
i[s] = c.maxBitrate || S[s];
}), i;
});
o(this, "getTrackIdToTrackBitrates", () => {
const t = {};
return Array.from(this.localEndpoint.tracks.entries()).forEach(([a, e]) => {
t[a] = this.getTrackBitrates(a);
}), t;
});
o(this, "checkIfTrackBelongToEndpoint", (t, a) => Array.from(a.tracks.keys()).some((e) => t.startsWith(e)));
o(this, "onOfferData", async (t) => {
this.connection ? this.connection.restartIce() : (this.connection = new RTCPeerConnection(this.rtcConfig), this.connection.onicecandidate = this.onLocalCandidate(), this.connection.onicecandidateerror = this.onIceCandidateError, this.connection.onconnectionstatechange = this.onConnectionStateChange, this.connection.oniceconnectionstatechange = this.onIceConnectionStateChange, this.connection.onicegatheringstatechange = this.onIceGatheringStateChange, this.connection.onsignalingstatechange = this.onSignalingStateChange, Array.from(this.localTrackIdToTrack.values()).forEach((a) => this.addTrackToConnection(a)), this.connection.getTransceivers().forEach((a) => a.direction = "sendonly")), this.addTransceiversIfNeeded(t), await this.createAndSendOffer();
});
o(this, "onRemoteCandidate", (t) => {
try {
const a = new RTCIceCandidate(t);
if (!this.connection)
throw new Error("Received new remote candidate but RTCConnection is undefined");
this.connection.addIceCandidate(a);
} catch (a) {
console.error(a);
}
});
o(this, "onLocalCandidate", () => (t) => {
if (t.candidate) {
const a = g({
type: "candidate",
data: {
candidate: t.candidate.candidate,
sdpMLineIndex: t.candidate.sdpMLineIndex
}
});
this.sendMediaEvent(a);
}
});
o(this, "onIceCandidateError", (t) => {
console.warn(t);
});
o(this, "onConnectionStateChange", (t) => {
var a;
switch ((a = this.connection) == null ? void 0 : a.connectionState) {
case "connected":
this.processNextCommand();
break;
case "failed":
this.emit("connectionError", "Connection failed");
break;
}
});
o(this, "onIceConnectionStateChange", (t) => {
var e;
const a = "Ice connection failed";
switch ((e = this.connection) == null ? void 0 : e.iceConnectionState) {
case "disconnected":
console.warn("ICE connection: disconnected");
break;
case "failed":
this.emit("connectionError", a);
break;
case "connected":
this.processNextCommand();
break;
}
});
o(this, "onIceGatheringStateChange", (t) => {
var a;
switch ((a = this.connection) == null ? void 0 : a.iceGatheringState) {
case "complete":
this.processNextCommand();
break;
}
});
o(this, "onSignalingStateChange", (t) => {
var a;
switch ((a = this.connection) == null ? void 0 : a.signalingState) {
case "stable":
this.processNextCommand();
break;
}
});
o(this, "onTrack", () => (t) => {
const [a] = t.streams, e = t.transceiver.mid, r = this.midToTrackId.get(e);
if (this.checkIfTrackBelongToEndpoint(r, this.localEndpoint))
return;
const n = this.trackIdToTrack.get(r);
n.stream = a, n.track = t.track, this.emit("trackReady", n);
});
o(this, "setTurns", (t) => {
t.forEach((a) => {
let e, r;
a.transport == "tls" ? (e = "tcp", r = "turns") : (e = a.transport, r = "turn");
const n = {
credential: a.password,
urls: r.concat(":", a.serverAddr, ":", a.serverPort, "?transport=", e),
username: a.username
};
this.rtcConfig.iceServers.push(n);
});
});
o(this, "addEndpoint", (t) => {
Object.prototype.hasOwnProperty.call(t, "trackIdToMetadata") ? t.tracks = new Map(Object.entries(t.tracks)) : t.tracks = /* @__PURE__ */ new Map(), this.idToEndpoint.set(t.id, t);
});
o(this, "eraseEndpoint", (t) => {
const a = Array.from(t.tracks.keys());
a.forEach((e) => this.trackIdToTrack.delete(e)), Array.from(this.midToTrackId.entries()).forEach(([e, r]) => {
a.includes(r) && this.midToTrackId.delete(e);
}), this.idToEndpoint.delete(t.id);
});
o(this, "eraseTrack", (t, a) => {
this.trackIdToTrack.delete(t);
const e = Array.from(this.midToTrackId.entries()), [r, n] = e.find(([i, c]) => c === t);
this.midToTrackId.delete(r), this.idToEndpoint.get(a).tracks.delete(t), this.disabledTrackEncodings.delete(t);
});
o(this, "getEndpointId", () => this.localEndpoint.id);
o(this, "mapMediaEventTracksToTrackContextImpl", (t, a) => {
const e = Array.from(t).map(
([r, n]) => [
r,
new v(a, r, n.metadata, n.simulcastConfig, this.trackMetadataParser)
]
);
return new Map(e);
});
this.endpointMetadataParser = (a = t == null ? void 0 : t.endpointMetadataParser) != null ? a : (r) => r, this.trackMetadataParser = (e = t == null ? void 0 : t.trackMetadataParser) != null ? e : (r) => r;
}
/**
* Retrieves statistics related to the RTCPeerConnection.
* These statistics provide insights into the performance and status of the connection.
*
* @return {Promise<RTCStatsReport>}
*
* @external RTCPeerConnection#getStats()
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/getStats | MDN Web Docs: RTCPeerConnection.getStats()}
*/
async getStatistics(t) {
var a, e;
return (e = await ((a = this.connection) == null ? void 0 : a.getStats(t))) != null ? e : /* @__PURE__ */ new Map();
}
/**
* Returns a snapshot of currently received remote tracks.
*
* @example
* if (webRTCEndpoint.getRemoteTracks()[trackId]?.simulcastConfig?.enabled) {
* webRTCEndpoint.setTargetTrackEncoding(trackId, encoding);
* }
*/
getRemoteTracks() {
return Object.fromEntries(this.trackIdToTrack.entries());
}
/**
* Returns a snapshot of currently received remote endpoints.
*/
getRemoteEndpoints() {
return Object.fromEntries(this.idToEndpoint.entries());
}
/**
* Adds track that will be sent to the RTC Engine.
* @param track - Audio or video track e.g. from your microphone or camera.
* @param stream - Stream that this track belongs to.
* @param trackMetadata - Any information about this track that other endpoints will
* receive in {@link WebRTCEndpointEvents.endpointAdded}. E.g. this can source of the track - whether it's
* screensharing, webcam or some other media device.
* @param simulcastConfig - Simulcast configuration. By default simulcast is disabled.
* For more information refer to {@link SimulcastConfig}.
* @param maxBandwidth - maximal bandwidth this track can use.
* Defaults to 0 which is unlimited.
* This option has no effect for simulcast and audio tracks.
* For simulcast tracks use `{@link WebRTCEndpoint.setTrackBandwidth}.
* @returns {string} Returns id of added track
* @example
* ```ts
* let localStream: MediaStream = new MediaStream();
* try {
* localAudioStream = await navigator.mediaDevices.getUserMedia(
* AUDIO_CONSTRAINTS
* );
* localAudioStream
* .getTracks()
* .forEach((track) => localStream.addTrack(track));
* } catch (error) {
* console.error("Couldn't get microphone permission:", error);
* }
*
* try {
* localVideoStream = await navigator.mediaDevices.getUserMedia(
* VIDEO_CONSTRAINTS
* );
* localVideoStream
* .getTracks()
* .forEach((track) => localStream.addTrack(track));
* } catch (error) {
* console.error("Couldn't get camera permission:", error);
* }
*
* localStream
* .getTracks()
* .forEach((track) => webrtc.addTrack(track, localStream));
* ```
*/
addTrack(t, a, e, r = { enabled: !1, activeEncodings: [], disabledEncodings: [] }, n = 0) {
const i = new f(), c = this.getTrackId(y());
try {
const s = this.trackMetadataParser(e);
this.pushCommand({
commandType: "ADD-TRACK",
trackId: c,
track: t,
stream: a,
trackMetadata: s,
simulcastConfig: r,
maxBandwidth: n,
resolutionNotifier: i
});
} catch (s) {
i.reject(s);
}
return i.promise.then(() => c);
}
pushCommand(t) {
this.commandsQueue.push(t), this.processNextCommand();
}
handleCommand(t) {
switch (t.commandType) {
case "ADD-TRACK":
this.addTrackHandler(t);
break;
case "REMOVE-TRACK":
this.removeTrackHandler(t);
break;
case "REPLACE-TRACK":
this.replaceTrackHandler(t);
break;
}
}
processNextCommand() {
if (this.ongoingRenegotiation || this.ongoingTrackReplacement || this.connection && (this.connection.signalingState !== "stable" || this.connection.connectionState !== "connected" || this.connection.iceConnectionState !== "connected"))
return;
this.resolvePreviousCommand();
const t = this.commandsQueue.shift();
t && (this.commandResolutionNotifier = t.resolutionNotifier, this.handleCommand(t));
}
resolvePreviousCommand() {
this.commandResolutionNotifier && (this.commandResolutionNotifier.resolve(), this.commandResolutionNotifier = null);
}
addTrackHandler(t) {
var u, E;
const { simulcastConfig: a, maxBandwidth: e, track: r, stream: n, trackMetadata: i, trackId: c } = t, s = (u = this.connection) == null ? void 0 : u.getSenders().some((p) => p.track === r);
let h;
if (s && (h = "This track was already added to peerConnection, it can't be added again!"), !a.enabled && typeof e != "number" && (h = "Invalid type of `maxBandwidth` argument for a non-simulcast track, expected: number"), this.getEndpointId() === "" && (h = "Cannot add tracks before being accepted by the server"), h) {
(E = this.commandResolutionNotifier) == null || E.reject(h), this.commandResolutionNotifier = null, this.processNextCommand();
return;
}
this.ongoingRenegotiation = !0;
const k = new v(
this.localEndpoint,
c,
i,
a,
this.trackMetadataParser
);
k.track = r, k.stream = n, k.maxBandwidth = e, this.localEndpoint.tracks.set(c, k), this.localTrackIdToTrack.set(c, k), this.connection && (this.addTrackToConnection(k), this.connection.getTransceivers().forEach(
(p) => p.direction = p.direction === "sendrecv" ? "sendonly" : p.direction
));
const m = g({ type: "renegotiateTracks" });
this.sendMediaEvent(m);
}
createTransceiverConfig(t) {
let a;
return t.track.kind === "audio" ? a = this.createAudioTransceiverConfig(t) : a = this.createVideoTransceiverConfig(t), a;
}
createAudioTransceiverConfig(t) {
return { direction: "sendonly", streams: t.stream ? [t.stream] : [] };
}
createVideoTransceiverConfig(t) {
var e;
let a;
if (t.simulcastConfig.enabled) {
a = P;
const r = t.simulcastConfig.activeEncodings, n = [];
(e = a.sendEncodings) == null || e.forEach((i) => {
r.includes(i.rid) ? i.active = !0 : n.push(i.rid);
}), this.disabledTrackEncodings.set(t.trackId, n);
} else
a = {
direction: "sendonly",
sendEncodings: [
{
active: !0
}
],
streams: t.stream ? [t.stream] : []
};
return t.maxBandwidth && a.sendEncodings && this.applyBandwidthLimitation(a.sendEncodings, t.maxBandwidth), a;
}
applyBandwidthLimitation(t, a) {
typeof a == "number" ? this.splitBandwidth(t, a * 1024) : t.filter((e) => e.rid).forEach((e) => {
const r = a.get(e.rid) || 0;
r > 0 ? e.maxBitrate = r * 1024 : delete e.maxBitrate;
});
}
splitBandwidth(t, a) {
if (a === 0) {
t.forEach((i) => delete i.maxBitrate);
return;
}
if (t.length == 0) {
console.error("Attempted to limit bandwidth of the track that doesn't have any encodings");
return;
}
const e = t[0].scaleResolutionDownBy || 1, r = t.reduce(
(i, c) => i + (e / (c.scaleResolutionDownBy || 1)) ** 2,
0
), n = a / r;
t.forEach((i) => {
i.maxBitrate = n * (e / (i.scaleResolutionDownBy || 1)) ** 2;
});
}
/**
* Replaces a track that is being sent to the RTC Engine.
* @param trackId - Audio or video track.
* @param {string} trackId - Id of audio or video track to replace.
* @param {MediaStreamTrack} newTrack
* @param {any} [newTrackMetadata] - Optional track metadata to apply to the new track. If no
* track metadata is passed, the old track metadata is retained.
* @returns {Promise<boolean>} success
* @example
* ```ts
* // setup camera
* let localStream: MediaStream = new MediaStream();
* try {
* localVideoStream = await navigator.mediaDevices.getUserMedia(
* VIDEO_CONSTRAINTS
* );
* localVideoStream
* .getTracks()
* .forEach((track) => localStream.addTrack(track));
* } catch (error) {
* console.error("Couldn't get camera permission:", error);
* }
* let oldTrackId;
* localStream
* .getTracks()
* .forEach((track) => trackId = webrtc.addTrack(track, localStream));
*
* // change camera
* const oldTrack = localStream.getVideoTracks()[0];
* let videoDeviceId = "abcd-1234";
* navigator.mediaDevices.getUserMedia({
* video: {
* ...(VIDEO_CONSTRAINTS as {}),
* deviceId: {
* exact: videoDeviceId,
* },
* }
* })
* .then((stream) => {
* let videoTrack = stream.getVideoTracks()[0];
* webrtc.replaceTrack(oldTrackId, videoTrack);
* })
* .catch((error) => {
* console.error('Error switching camera', error);
* })
* ```
*/
async replaceTrack(t, a, e) {
const r = new f();
try {
const n = this.trackMetadataParser(e);
this.pushCommand({
commandType: "REPLACE-TRACK",
trackId: t,
newTrack: a,
newTrackMetadata: n,
resolutionNotifier: r
});
} catch (n) {
r.reject(n);
}
return r.promise;
}
replaceTrackHandler(t) {
const { trackId: a, newTrack: e, newTrackMetadata: r } = t, n = this.localTrackIdToTrack.get(a), i = this.findSender(n.track.id);
i && (this.ongoingTrackReplacement = !0, i.replaceTrack(e).then(() => {
const c = r || this.localTrackIdToTrack.get(a).metadata;
n.track = e, this.updateTrackMetadata(a, c);
}).finally(() => {
this.resolvePreviousCommand(), this.ongoingTrackReplacement = !1, this.processNextCommand();
}));
}
/**
* Updates maximum bandwidth for the track identified by trackId.
* This value directly translates to quality of the stream and, in case of video, to the amount of RTP packets being sent.
* In case trackId points at the simulcast track bandwidth is split between all of the variant streams proportionally to their resolution.
*
* @param {string} trackId
* @param {BandwidthLimit} bandwidth in kbps
* @returns {Promise<boolean>} success
*/
setTrackBandwidth(t, a) {
const e = this.localTrackIdToTrack.get(t);
if (!e)
return Promise.reject(`Track '${t}' doesn't exist`);
const r = this.findSender(e.track.id), n = r.getParameters();
return n.encodings.length === 0 ? n.encodings = [{}] : this.applyBandwidthLimitation(n.encodings, a), r.setParameters(n).then(() => {
const i = g({
type: "trackVariantBitrates",
data: {
trackId: t,
variantBitrates: this.getTrackBitrates(t)
}
});
return this.sendMediaEvent(i), !0;
}).catch((i) => !1);
}
/**
* Updates maximum bandwidth for the given simulcast encoding of the given track.
*
* @param {string} trackId - id of the track
* @param {string} rid - rid of the encoding
* @param {BandwidthLimit} bandwidth - desired max bandwidth used by the encoding (in kbps)
* @returns
*/
setEncodingBandwidth(t, a, e) {
const r = this.localTrackIdToTrack.get(t);
if (!r)
return Promise.reject(`Track '${t}' doesn't exist`);
const n = this.findSender(r.track.id), i = n.getParameters(), c = i.encodings.find((s) => s.rid === a);
if (c)
e === 0 ? delete c.maxBitrate : c.maxBitrate = e * 1024;
else
return Promise.reject(`Encoding with rid '${a}' doesn't exist`);
return n.setParameters(i).then(() => {
const s = g({
type: "trackVariantBitrates",
data: {
trackId: t,
variantBitrates: this.getTrackBitrates(t)
}
});
return this.sendMediaEvent(s), !0;
}).catch((s) => !1);
}
/**
* Removes a track from connection that was sent to the RTC Engine.
* @param {string} trackId - Id of audio or video track to remove.
* @example
* ```ts
* // setup camera
* let localStream: MediaStream = new MediaStream();
* try {
* localVideoStream = await navigator.mediaDevices.getUserMedia(
* VIDEO_CONSTRAINTS
* );
* localVideoStream
* .getTracks()
* .forEach((track) => localStream.addTrack(track));
* } catch (error) {
* console.error("Couldn't get camera permission:", error);
* }
*
* let trackId
* localStream
* .getTracks()
* .forEach((track) => trackId = webrtc.addTrack(track, localStream));
*
* // remove track
* webrtc.removeTrack(trackId)
* ```
*/
removeTrack(t) {
const a = new f();
return this.pushCommand({
commandType: "REMOVE-TRACK",
trackId: t,
resolutionNotifier: a
}), a.promise;
}
removeTrackHandler(t) {
const { trackId: a } = t, e = this.localTrackIdToTrack.get(a), r = this.findSender(e.track.id);
this.ongoingRenegotiation = !0, this.connection.removeTrack(r);
const n = g({ type: "renegotiateTracks" });
this.sendMediaEvent(n), this.localTrackIdToTrack.delete(a), this.localEndpoint.tracks.delete(a);
}
/**
* Sets track encoding that server should send to the client library.
*
* The encoding will be sent whenever it is available.
* If chosen encoding is temporarily unavailable, some other encoding
* will be sent until the chosen encoding becomes active again.
*
* @param {string} trackId - id of track
* @param {TrackEncoding} encoding - encoding to receive
* @example
* ```ts
* webrtc.setTargetTrackEncoding(incomingTrackCtx.trackId, "l")
* ```
*/
setTargetTrackEncoding(t, a) {
var n;
const e = this.trackIdToTrack.get(t);
if (!((n = e == null ? void 0 : e.simulcastConfig) != null && n.enabled) || !e.simulcastConfig.activeEncodings.includes(a)) {
console.warn("The track does not support changing its target encoding");
return;
}
const r = g({
type: "setTargetTrackVariant",
data: {
trackId: t,
variant: a
}
});
this.sendMediaEvent(r);
}
/**
* Enables track encoding so that it will be sent to the server.
* @param {string} trackId - id of track
* @param {TrackEncoding} encoding - encoding that will be enabled
* @example
* ```ts
* const trackId = webrtc.addTrack(track, stream, {}, {enabled: true, activeEncodings: ["l", "m", "h"]});
* webrtc.disableTrackEncoding(trackId, "l");
* // wait some time
* webrtc.enableTrackEncoding(trackId, "l");
* ```
*/
enableTrackEncoding(t, a) {
var s, h, k;
const e = (s = this.localTrackIdToTrack.get(t)) == null ? void 0 : s.track, r = (h = this.disabledTrackEncodings.get(t)) == null ? void 0 : h.filter((m) => m !== a);
this.disabledTrackEncodings.set(t, r);
const n = (k = this.connection) == null ? void 0 : k.getSenders().filter((m) => m.track === e)[0], i = n == null ? void 0 : n.getParameters();
i.encodings.filter((m) => m.rid == a)[0].active = !0, n == null || n.setParameters(i);
const c = T("enableTrackEncoding", { trackId: t, encoding: a });
this.sendMediaEvent(c);
}
/**
* Disables track encoding so that it will be no longer sent to the server.
* @param {string} trackId - id of track
* @param {rackEncoding} encoding - encoding that will be disabled
* @example
* ```ts
* const trackId = webrtc.addTrack(track, stream, {}, {enabled: true, activeEncodings: ["l", "m", "h"]});
* webrtc.disableTrackEncoding(trackId, "l");
* ```
*/
disableTrackEncoding(t, a) {
var c, s;
const e = (c = this.localTrackIdToTrack.get(t)) == null ? void 0 : c.track;
this.disabledTrackEncodings.get(t).push(a);
const r = (s = this.connection) == null ? void 0 : s.getSenders().filter((h) => h.track === e)[0], n = r == null ? void 0 : r.getParameters();
n.encodings.filter((h) => h.rid == a)[0].active = !1, r == null || r.setParameters(n);
const i = T("disableTrackEncoding", { trackId: t, encoding: a });
this.sendMediaEvent(i);
}
findSender(t) {
return this.connection.getSenders().find((a) => a.track && a.track.id === t);
}
getTrackId(t) {
return `${this.getEndpointId()}:${t}`;
}
async createAndSendOffer() {
if (this.connection)
try {
const t = await this.connection.createOffer();
await this.connection.setLocalDescription(t);
const a = g({
type: "sdpOffer",
data: {
sdpOffer: t,
trackIdToTrackMetadata: this.getTrackIdToMetadata(),
trackIdToTrackBitrates: this.getTrackIdToTrackBitrates(),
midToTrackId: this.getMidToTrackId()
}
});
this.sendMediaEvent(a);
for (const e of this.localTrackIdToTrack.values())
e.negotiationStatus = "offered";
} catch (t) {
console.error(t);
}
}
}
export {
O as WebRTCEndpoint
};