UNPKG

@jellyfish-dev/membrane-webrtc-js

Version:
1,073 lines (1,072 loc) 40.4 kB
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 };