UNPKG

media-stream-library

Version:

Media stream library for Node & the Web.

2,024 lines (1,995 loc) 169 kB
var __defProp = Object.defineProperty; var __defProps = Object.defineProperties; var __getOwnPropDescs = Object.getOwnPropertyDescriptors; var __getOwnPropSymbols = Object.getOwnPropertySymbols; var __hasOwnProp = Object.prototype.hasOwnProperty; var __propIsEnum = Object.prototype.propertyIsEnumerable; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __spreadValues = (a, b) => { for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]); if (__getOwnPropSymbols) for (var prop of __getOwnPropSymbols(b)) { if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]); } return a; }; var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)); var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); var __async = (__this, __arguments, generator) => { return new Promise((resolve, reject) => { var fulfilled = (value) => { try { step(generator.next(value)); } catch (e) { reject(e); } }; var rejected = (value) => { try { step(generator.throw(value)); } catch (e) { reject(e); } }; var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected); step((generator = generator.apply(__this, __arguments)).next()); }); }; // src/components/index.browser.ts var index_browser_exports = {}; __export(index_browser_exports, { AACDepay: () => AACDepay, BasicDepay: () => BasicDepay, CanvasSink: () => CanvasSink, H264Depay: () => H264Depay, HttpSource: () => HttpSource, Inspector: () => Inspector, JPEGDepay: () => JPEGDepay, MessageType: () => MessageType, Mp4Capture: () => Mp4Capture, Mp4Muxer: () => Mp4Muxer, MseSink: () => MseSink, ONVIFDepay: () => ONVIFDepay, RTSPResponseError: () => RTSPResponseError, RTSP_METHOD: () => RTSP_METHOD, RtspParser: () => RtspParser, RtspSession: () => RtspSession, Sink: () => Sink, Source: () => Source, Tube: () => Tube, WSSource: () => WSSource, createTransform: () => createTransform }); // src/components/component.ts import { PassThrough, Readable as Readable2, Writable as Writable2 } from "stream"; // src/components/helpers/stream-factory.ts import { Readable, Transform, Writable } from "stream"; var StreamFactory = class { /** * Creates a writable stream that sends all messages written to the stream * to a callback function and then considers it written. * @param fn The callback to be invoked on the message */ static consumer(fn = () => { }) { return new Writable({ objectMode: true, write(msg, _encoding, callback) { fn(msg); callback(); } }); } static peeker(fn) { if (typeof fn !== "function") { throw new Error("you must supply a function"); } return new Transform({ objectMode: true, transform(msg, _encoding, callback) { fn(msg); callback(void 0, msg); } }); } /** * Creates a readable stream that sends a message for each element of an array. * @param arr The array with elements to be turned into a stream. */ static producer(messages) { let counter = 0; return new Readable({ objectMode: true, read() { if (messages !== void 0) { if (counter < messages.length) { this.push(messages[counter++]); } else { this.push(null); } } } }); } static recorder(type, fileStream) { return new Transform({ objectMode: true, transform(msg, encoding, callback) { const timestamp2 = Date.now(); const message = Object.assign({}, msg, { data: msg.data.toString("base64") }); fileStream.write(JSON.stringify({ type, timestamp: timestamp2, message }, null, 2)); fileStream.write(",\n"); callback(void 0, msg); } }); } /** * Yield binary messages from JSON packet array until depleted. * @return {Generator} Returns a JSON packet iterator. */ static replayer(packets) { let packetCounter = 0; let lastTimestamp = packets[0].timestamp; return new Readable({ objectMode: true, read() { const packet = packets[packetCounter++]; if (packet) { const { type, timestamp: timestamp2, message } = packet; const delay = timestamp2 - lastTimestamp; lastTimestamp = timestamp2; if (message) { const data = message.data ? Buffer.from(message.data, "base64") : Buffer.alloc(0); const msg = Object.assign({}, message, { data }); this.push({ type, delay, msg }); } else { this.push({ type, delay, msg: null }); } } else { this.push(null); } } }); } }; // src/components/component.ts var AbstractComponent = class { constructor() { __publicField(this, "_incomingErrorHandler"); __publicField(this, "_outgoingErrorHandler"); } }; var Source = class _Source extends AbstractComponent { constructor(incoming = new Readable2({ objectMode: true }), outgoing = new Writable2({ objectMode: true })) { super(); __publicField(this, "incoming"); __publicField(this, "outgoing"); __publicField(this, "next"); __publicField(this, "prev"); this.incoming = incoming; this.outgoing = outgoing; this.next = null; this.prev = null; } /** * Set up a source component that has a message list as data source. * * @param messages - List of objects (with data property) to emit on the * incoming stream */ static fromMessages(messages) { const component = new _Source( StreamFactory.producer(messages), StreamFactory.consumer() ); return component; } /** * Attach another component so the the 'down' stream flows into the * next component 'down' stream and the 'up' stream of the other component * flows into the 'up' stream of this component. This is what establishes the * meaning of 'up' and 'down'. * @param next - The component to connect. * @return A reference to the connected component. * * -------------- pipe -------------- * <- | outgoing | <- | outgoing | <- * | this | | next | * -> | incoming | -> | incoming | -> * -------------- pipe -------------- */ connect(next) { if (next === null) { return this; } else if (this.next !== null || next.prev !== null) { throw new Error("connection failed: component(s) already connected"); } if (!this.incoming.readable || !this.outgoing.writable) { throw new Error("connection failed: this component not compatible"); } if (!next.incoming.writable || !next.outgoing.readable) { throw new Error("connection failed: next component not compatible"); } try { this.incoming.pipe(next.incoming); next.outgoing.pipe(this.outgoing); } catch (e) { throw new Error(`connection failed: ${e.message}`); } const incomingErrorHandler = (err) => { this.incoming.emit("error", err); }; next.incoming.on("error", incomingErrorHandler); const outgoingErrorHandler = (err) => { next.outgoing.emit("error", err); }; this.outgoing.on("error", outgoingErrorHandler); this.next = next; next.prev = this; this._incomingErrorHandler = incomingErrorHandler; this._outgoingErrorHandler = outgoingErrorHandler; return next; } /** * Disconnect the next connected component. When there is no next component * the function will just do nothing. * @return {Component} - A reference to this component. */ disconnect() { const next = this.next; if (next !== null) { this.incoming.unpipe(next.incoming); next.outgoing.unpipe(this.outgoing); if (typeof this._incomingErrorHandler !== "undefined") { next.incoming.removeListener("error", this._incomingErrorHandler); } if (typeof this._outgoingErrorHandler !== "undefined") { this.outgoing.removeListener("error", this._outgoingErrorHandler); } this.next = null; next.prev = null; delete this._incomingErrorHandler; delete this._outgoingErrorHandler; } return this; } }; var Tube = class _Tube extends Source { constructor(incoming = new PassThrough({ objectMode: true }), outgoing = new PassThrough({ objectMode: true })) { super(incoming, outgoing); __publicField(this, "incoming"); __publicField(this, "outgoing"); this.incoming = incoming; this.outgoing = outgoing; } /** * Create a component that calls a handler function for each message passing * through, but otherwise just passes data through. * * Can be used to log messages passing through a pipeline. */ static fromHandlers(fnIncoming, fnOutgoing) { const incomingStream = fnIncoming ? StreamFactory.peeker(fnIncoming) : void 0; const outgoingStream = fnOutgoing ? StreamFactory.peeker(fnOutgoing) : void 0; return new _Tube(incomingStream, outgoingStream); } }; var Sink = class _Sink extends AbstractComponent { constructor(incoming = new Writable2({ objectMode: true }), outgoing = new Readable2({ objectMode: true })) { super(); __publicField(this, "incoming"); __publicField(this, "outgoing"); __publicField(this, "next"); __publicField(this, "prev"); this.incoming = incoming; this.outgoing = outgoing; this.next = null; this.prev = null; } /** * Create a component that swallows incoming data (calling fn on it). To * print data, you would use fn = console.log. * * @param fn - The callback to use for the incoming data. */ static fromHandler(fn) { const component = new _Sink( StreamFactory.consumer(fn), StreamFactory.producer(void 0) ); component.incoming.on("finish", () => { component.outgoing.push(null); }); return component; } connect() { throw new Error("connection failed: attempting to connect after a sink"); } disconnect() { return this; } }; // src/utils/bits.ts var POS = [128, 64, 32, 16, 8, 4, 2, 1]; // src/utils/protocols/rtp.ts var version = (buffer) => { return buffer[0] >>> 6; }; var padding = (buffer) => { return !!(buffer[0] & POS[2]); }; var extension = (buffer) => { return !!(buffer[0] & POS[3]); }; var cSrcCount = (buffer) => { return buffer[0] & 15; }; var marker = (buffer) => { return !!(buffer[1] & POS[0]); }; var payloadType = (buffer) => { return buffer[1] & 127; }; var sequenceNumber = (buffer) => { return buffer.readUInt16BE(2); }; var timestamp = (buffer) => { return buffer.readUInt32BE(4); }; var sSrc = (buffer) => { return buffer.readUInt32BE(8); }; var cSrc = (buffer, rank = 0) => { return cSrcCount(buffer) > rank ? buffer.readUInt32BE(12 + rank * 4) : 0; }; var extHeaderLength = (buffer) => { return !extension(buffer) ? 0 : buffer.readUInt16BE(12 + cSrcCount(buffer) * 4 + 2); }; var extHeader = (buffer) => { return extHeaderLength(buffer) === 0 ? Buffer.from([]) : buffer.slice( 12 + cSrcCount(buffer) * 4, 12 + cSrcCount(buffer) * 4 + 4 + extHeaderLength(buffer) * 4 ); }; var payload = (buffer) => { return !extension(buffer) ? buffer.slice(12 + cSrcCount(buffer) * 4) : buffer.slice(12 + cSrcCount(buffer) * 4 + 4 + extHeaderLength(buffer) * 4); }; // src/components/message.ts var MessageType = /* @__PURE__ */ ((MessageType2) => { MessageType2[MessageType2["UNKNOWN"] = 0] = "UNKNOWN"; MessageType2[MessageType2["RAW"] = 1] = "RAW"; MessageType2[MessageType2["RTP"] = 2] = "RTP"; MessageType2[MessageType2["RTCP"] = 3] = "RTCP"; MessageType2[MessageType2["RTSP"] = 4] = "RTSP"; MessageType2[MessageType2["SDP"] = 5] = "SDP"; MessageType2[MessageType2["ELEMENTARY"] = 6] = "ELEMENTARY"; MessageType2[MessageType2["H264"] = 7] = "H264"; MessageType2[MessageType2["ISOM"] = 8] = "ISOM"; MessageType2[MessageType2["XML"] = 9] = "XML"; MessageType2[MessageType2["JPEG"] = 10] = "JPEG"; return MessageType2; })(MessageType || {}); // src/components/messageStreams.ts import { Transform as Transform2 } from "stream"; var createTransform = (transform) => { return new Transform2({ objectMode: true, transform }); }; // src/components/aacdepay/parser.ts function parse(rtp, hasHeader, callback) { const buffer = payload(rtp.data); let headerLength = 0; if (hasHeader) { const auHeaderLengthInBits = buffer.readUInt16BE(0); headerLength = 2 + (auHeaderLengthInBits + auHeaderLengthInBits % 8) / 8; } const packet = { type: 6 /* ELEMENTARY */, data: buffer.slice(headerLength), payloadType: payloadType(rtp.data), timestamp: timestamp(rtp.data), ntpTimestamp: rtp.ntpTimestamp }; callback(packet); } // src/components/aacdepay/index.ts var AACDepay = class extends Tube { constructor() { let AACPayloadType; let hasHeader; const incoming = createTransform(function(msg, encoding, callback) { if (msg.type === 5 /* SDP */) { let validMedia; for (const media of msg.sdp.media) { if (media.type === "audio" && media.fmtp && media.fmtp.parameters && media.fmtp.parameters.mode === "AAC-hbr") { validMedia = media; } } if (validMedia && validMedia.rtpmap !== void 0) { AACPayloadType = Number(validMedia.rtpmap.payloadType); const parameters = validMedia.fmtp.parameters; const sizeLength = Number(parameters.sizelength) || 0; const indexLength = Number(parameters.indexlength) || 0; const indexDeltaLength = Number(parameters.indexdeltalength) || 0; const CTSDeltaLength = Number(parameters.ctsdeltalength) || 0; const DTSDeltaLength = Number(parameters.dtsdeltalength) || 0; const RandomAccessIndication = Number(parameters.randomaccessindication) || 0; const StreamStateIndication = Number(parameters.streamstateindication) || 0; const AuxiliaryDataSizeLength = Number(parameters.auxiliarydatasizelength) || 0; hasHeader = sizeLength + Math.max(indexLength, indexDeltaLength) + CTSDeltaLength + DTSDeltaLength + RandomAccessIndication + StreamStateIndication + AuxiliaryDataSizeLength > 0; } callback(void 0, msg); } else if (msg.type === 2 /* RTP */ && payloadType(msg.data) === AACPayloadType) { parse(msg, hasHeader, this.push.bind(this)); callback(); } else { callback(void 0, msg); } }); super(incoming); } }; // src/components/basicdepay/index.ts var BasicDepay = class extends Tube { constructor(rtpPayloadType) { if (rtpPayloadType === void 0) { throw new Error("you must supply a payload type to BasicDepayComponent"); } let buffer = Buffer.alloc(0); const incoming = createTransform(function(msg, encoding, callback) { if (msg.type === 2 /* RTP */ && payloadType(msg.data) === rtpPayloadType) { const rtpPayload = payload(msg.data); buffer = Buffer.concat([buffer, rtpPayload]); if (marker(msg.data)) { if (buffer.length > 0) { this.push({ data: buffer, timestamp: timestamp(msg.data), ntpTimestamp: msg.ntpTimestamp, payloadType: payloadType(msg.data), type: 6 /* ELEMENTARY */ }); } buffer = Buffer.alloc(0); } callback(); } else { callback(void 0, msg); } }); super(incoming); } }; // src/components/canvas/index.ts import { Readable as Readable3, Writable as Writable3 } from "stream"; // src/utils/clock.ts var Clock = class { constructor() { __publicField(this, "started"); __publicField(this, "stopped"); __publicField(this, "elapsed"); this.elapsed = 0; this.started = 0; this.stopped = true; } start() { if (this.stopped) { this.started = window.performance.now(); this.stopped = false; } } stop() { if (!this.stopped) { this.elapsed = this.now(); this.stopped = true; } } reset() { this.elapsed = 0; this.started = 0; this.stopped = true; } // Gives the elapsed time in milliseconds since the // clock was first started (after last reset). now() { if (this.stopped) { return this.elapsed; } return this.elapsed + (window.performance.now() - this.started); } play() { this.start(); } pause() { this.stop(); } // Gives the elapsed time in seconds since last reset. get currentTime() { return this.now() / 1e3; } }; // src/utils/scheduler.ts var DEFAULT_TOLERANCE = 10; var Scheduler = class { /** * Creates an instance of Scheduler. * @param clock - The clock to use (so we can control playback) * @param handler - The callback to invoke when a message is in sync * @param tolerance - The milliseconds defining "in sync" (default = 10) */ constructor(clock, handler, tolerance = DEFAULT_TOLERANCE) { __publicField(this, "_clock"); __publicField(this, "_handler"); __publicField(this, "_tolerance"); __publicField(this, "_nextRun"); __publicField(this, "_nextPlay"); __publicField(this, "_fifo"); __publicField(this, "_ntpPresentationTime"); __publicField(this, "_suspended"); this._clock = clock; this._handler = handler; this._tolerance = tolerance; this._nextRun = 0; this._nextPlay = 0; this._fifo = []; this._ntpPresentationTime = 0; this._suspended = false; } /** * Bring the scheduler back to it's initial state. */ reset() { clearTimeout(this._nextRun); clearTimeout(this._nextPlay); this._fifo = []; this._ntpPresentationTime = 0; this._suspended = false; } /** * Initialize the scheduler. * * @param ntpPresentationTime - The offset representing the start of the presentation */ init(ntpPresentationTime) { this._ntpPresentationTime = ntpPresentationTime; } /** * Suspend the scheduler. * * This releases control of the clock and stops any scheduling activity. * Note that this doesn't mean the clock will be in a particular state * (could be started or stopped), just that the scheduler will no longer * control it. */ suspend() { clearTimeout(this._nextPlay); this._suspended = true; } /** * Resume the scheduler. * * This gives back control of the clock and the ability * to schedule messages. The scheduler will immediately * try to do that on resume. */ resume() { this._suspended = false; this.run(void 0); } /** * Run the scheduler. * * @param newMessage - New message to schedule. */ run(newMessage) { clearTimeout(this._nextRun); if (typeof this._ntpPresentationTime === "undefined") { return; } if (typeof newMessage !== "undefined") { this._fifo.push(newMessage); } if (this._suspended) { return; } if (this._fifo.length === 0) { return; } let timeToPresent = 0; let currentMessage; do { const msg = this._fifo.shift(); if (msg === void 0) { throw new Error("internal error: message should never be undefined"); } currentMessage = msg; const ntpTimestamp = currentMessage.ntpTimestamp; if (ntpTimestamp === void 0) { continue; } const presentationTime = ntpTimestamp - this._ntpPresentationTime; timeToPresent = presentationTime - this._clock.currentTime * 1e3; if (Math.abs(timeToPresent) < this._tolerance) { this._handler && this._handler(currentMessage); } } while (timeToPresent < this._tolerance && this._fifo.length > 0); if (timeToPresent < -this._tolerance) { clearTimeout(this._nextPlay); this._clock.pause(); this._nextPlay = window.setTimeout( () => this._clock.play(), -timeToPresent ); } else if (timeToPresent > this._tolerance) { this._fifo.unshift(currentMessage); this._nextRun = window.setTimeout( () => this.run(void 0), timeToPresent ); } } }; // src/components/canvas/index.ts var resetInfo = (info) => { info.bitrate = 0; info.framerate = 0; info.renderedFrames = 0; }; var generateUpdateInfo = (clockrate) => { let cumulativeByteLength = 0; let cumulativeDuration = 0; let cumulativeFrames = 0; return (info, { byteLength, duration }) => { cumulativeByteLength += byteLength; cumulativeDuration += duration; cumulativeFrames++; if (cumulativeDuration >= clockrate) { const bits = 8 * cumulativeByteLength; const frames = cumulativeFrames; const seconds = cumulativeDuration / clockrate; info.bitrate = bits / seconds; info.framerate = frames / seconds; cumulativeByteLength = 0; cumulativeDuration = 0; cumulativeFrames = 0; } }; }; var CanvasSink = class extends Sink { /** * @param el - The <canvas> element to draw incoming JPEG messages on. */ constructor(el) { if (el === void 0) { throw new Error("canvas element argument missing"); } let firstTimestamp = 0; let lastTimestamp = 0; let clockrate = 0; const info = { bitrate: 0, framerate: 0, renderedFrames: 0 }; let updateInfo; let ctx = null; if (window.createImageBitmap !== void 0) { ctx = el.getContext("bitmaprenderer"); } if (ctx === null) { ctx = el.getContext("2d"); } let drawImageBlob; if (ctx === null) { drawImageBlob = () => { }; } else if ("transferFromImageBitmap" in ctx) { const ctxBitmaprenderer = ctx; drawImageBlob = ({ blob }) => { info.renderedFrames++; window.createImageBitmap(blob).then((imageBitmap) => { ctxBitmaprenderer.transferFromImageBitmap(imageBitmap); }).catch(() => { }); }; } else { const ctx2d = ctx; const img = new Image(); img.onload = () => { ctx2d.drawImage(img, 0, 0); }; drawImageBlob = ({ blob }) => { info.renderedFrames++; const url = window.URL.createObjectURL(blob); img.src = url; }; } const clock = new Clock(); const scheduler = new Scheduler(clock, drawImageBlob); let ntpPresentationTime = 0; const onCanplay = () => { this.onCanplay && this.onCanplay(); }; const onSync = (npt) => { this.onSync && this.onSync(npt); }; const incoming = new Writable3({ objectMode: true, write: (msg, _encoding, callback) => { if (msg.type === 5 /* SDP */) { clock.reset(); scheduler.reset(); firstTimestamp = 0; const jpegMedia = msg.sdp.media.find((media) => { return media.type === "video" && media.rtpmap !== void 0 && media.rtpmap.encodingName === "JPEG"; }); if (jpegMedia !== void 0 && jpegMedia.rtpmap !== void 0) { clockrate = jpegMedia.rtpmap.clockrate; resetInfo(info); updateInfo = generateUpdateInfo(clockrate); } callback(); } else if (msg.type === 10 /* JPEG */) { const { timestamp: timestamp2, ntpTimestamp } = msg; if (!firstTimestamp) { firstTimestamp = timestamp2; lastTimestamp = timestamp2; const { width, height } = msg.framesize; el.width = width; el.height = height; scheduler.init(0); } const presentationTime = 1e3 * (timestamp2 - firstTimestamp) / clockrate; const blob = new window.Blob([msg.data], { type: "image/jpeg" }); if (!ntpPresentationTime && ntpTimestamp) { ntpPresentationTime = ntpTimestamp - presentationTime; onSync(ntpPresentationTime); } scheduler.run({ ntpTimestamp: presentationTime, blob }); if (timestamp2 === firstTimestamp) { onCanplay(); } updateInfo(info, { byteLength: msg.data.length, duration: timestamp2 - lastTimestamp }); lastTimestamp = timestamp2; callback(); } else { callback(); } } }); const outgoing = new Readable3({ objectMode: true, read() { } }); outgoing.on("error", () => { console.warn("outgoing stream broke somewhere"); }); super(incoming, outgoing); __publicField(this, "onCanplay"); __publicField(this, "onSync"); __publicField(this, "_clock"); __publicField(this, "_scheduler"); __publicField(this, "_info"); this._clock = clock; this._scheduler = scheduler; this._info = info; this.onCanplay = void 0; this.onSync = void 0; } /** * Retrieve the current presentation time (seconds) */ get currentTime() { return this._clock.currentTime; } /** * Pause the presentation. */ pause() { this._scheduler.suspend(); this._clock.pause(); } /** * Start the presentation. */ play() { this._clock.play(); this._scheduler.resume(); } get bitrate() { return this._info.bitrate; } get framerate() { return this._info.framerate; } }; // src/components/h264depay/index.ts import { Transform as Transform3 } from "stream"; // src/components/h264depay/parser.ts import debug from "debug"; var h264Debug = debug("msl:h264depay"); var H264DepayParser = class { constructor() { __publicField(this, "_buffer"); this._buffer = Buffer.alloc(0); } parse(rtp) { const rtpPayload = payload(rtp.data); const type = rtpPayload[0] & 31; if (type === 28) { const fuIndicator = rtpPayload[0]; const fuHeader = rtpPayload[1]; const startBit = !!(fuHeader >> 7); const nalType = fuHeader & 31; const nal = fuIndicator & 224 | nalType; const stopBit = fuHeader & 64; if (startBit) { this._buffer = Buffer.concat([ Buffer.from([0, 0, 0, 0, nal]), rtpPayload.slice(2) ]); return null; } else if (stopBit) { const h264frame = Buffer.concat([ this._buffer, rtpPayload.slice(2) ]); h264frame.writeUInt32BE(h264frame.length - 4, 0); const msg = { data: h264frame, type: 7 /* H264 */, timestamp: timestamp(rtp.data), ntpTimestamp: rtp.ntpTimestamp, payloadType: payloadType(rtp.data), nalType }; this._buffer = Buffer.alloc(0); return msg; } this._buffer = Buffer.concat([this._buffer, rtpPayload.slice(2)]); return null; } else if ((type === 1 /* NON_IDR_PICTURE */ || type === 5 /* IDR_PICTURE */) && this._buffer.length === 0) { const h264frame = Buffer.concat([ Buffer.from([0, 0, 0, 0]), rtpPayload ]); h264frame.writeUInt32BE(h264frame.length - 4, 0); const msg = { data: h264frame, type: 7 /* H264 */, timestamp: timestamp(rtp.data), ntpTimestamp: rtp.ntpTimestamp, payloadType: payloadType(rtp.data), nalType: type }; this._buffer = Buffer.alloc(0); return msg; } h264Debug( `H264depayComponent can only extract types 1,5 and 28, got ${type}` ); this._buffer = Buffer.alloc(0); return null; } }; // src/components/h264depay/index.ts var H264Depay = class extends Tube { constructor() { let h264PayloadType; let idrFound = false; let packets = []; const h264DepayParser = new H264DepayParser(); const incoming = new Transform3({ objectMode: true, transform(msg, _encoding, callback) { if (msg.type === 5 /* SDP */) { const h264Media = msg.sdp.media.find((media) => { return media.type === "video" && media.rtpmap !== void 0 && media.rtpmap.encodingName === "H264"; }); if (h264Media !== void 0 && h264Media.rtpmap !== void 0) { h264PayloadType = h264Media.rtpmap.payloadType; } callback(void 0, msg); } else if (msg.type === 2 /* RTP */ && payloadType(msg.data) === h264PayloadType) { const endOfFrame = marker(msg.data); const h264Message = h264DepayParser.parse(msg); if (h264Message === null || !idrFound && h264Message.nalType !== 5 /* IDR_PICTURE */) { callback(); return; } idrFound = true; packets.push(h264Message.data); if (endOfFrame) { this.push(__spreadProps(__spreadValues({}, h264Message), { data: packets.length === 1 ? packets[0] : Buffer.concat(packets) })); packets = []; } callback(); } else { callback(void 0, msg); } } }); super(incoming); } }; // src/components/http-source/index.ts import registerDebug from "debug"; import { Readable as Readable4 } from "stream"; var debug2 = registerDebug("msl:http-source"); var HttpSource = class extends Source { /** * Create an HTTP component. * * The constructor sets a single readable stream from a fetch. */ constructor(config) { const { uri, options } = config; const incoming = new Readable4({ objectMode: true, read() { } }); incoming.on("error", (e) => { console.warn("closing socket due to incoming error", e); this._reader && this._reader.cancel().catch((err) => console.error(err)); }); super(incoming); __publicField(this, "uri"); __publicField(this, "options"); __publicField(this, "length"); __publicField(this, "onHeaders"); __publicField(this, "onServerClose"); __publicField(this, "_reader"); __publicField(this, "_abortController"); __publicField(this, "_allDone"); incoming._read = () => { this._pull(); }; this.uri = uri; this.options = options; this._allDone = false; } play() { if (this.uri === void 0) { throw new Error("cannot start playing when there is no URI"); } this._abortController = new AbortController(); this.length = 0; fetch(this.uri, __spreadValues({ credentials: "include", signal: this._abortController.signal }, this.options)).then((rsp) => { if (rsp.body === null) { throw new Error("empty response body"); } this.onHeaders && this.onHeaders(rsp.headers); this._reader = rsp.body.getReader(); this._pull(); }).catch((err) => { console.error("http-source: fetch failed: ", err); }); } abort() { this._reader && this._reader.cancel().catch((err) => { console.log("http-source: cancel reader failed: ", err); }); this._abortController && this._abortController.abort(); } _isClosed() { return this._allDone; } _close() { var _a; this._reader = void 0; this._allDone = true; this.incoming.push(null); (_a = this.onServerClose) == null ? void 0 : _a.call(this); } _pull() { if (this._reader === void 0) { return; } this._reader.read().then(({ done, value }) => { if (done) { if (!this._isClosed()) { debug2("fetch completed, total downloaded: ", this.length, " bytes"); this._close(); } return; } if (value === void 0) { throw new Error("expected value to be defined"); } if (this.length === void 0) { throw new Error("expected length to be defined"); } this.length += value.length; const buffer = Buffer.from(value); if (!this.incoming.push({ data: buffer, type: 1 /* RAW */ })) { debug2("downstream back pressure: pausing read"); } else { this._pull(); } }).catch((err) => { debug2("http-source: read failed: ", err); if (!this._isClosed()) { this._close(); } }); } }; // src/components/inspector/index.ts import { Transform as Transform4 } from "stream"; var generateLogger = (prefix, type) => { let lastTimestamp = Date.now(); const log = (msg) => { const timestamp2 = Date.now(); console.log(`${prefix}: +${timestamp2 - lastTimestamp}ms`, msg); lastTimestamp = timestamp2; }; if (type === void 0) { return log; } return (msg) => msg.type === type && log(msg); }; var Inspector = class extends Tube { /** * Create a new inspector component. * @argument {String} type The type of message to log (default is to log all). * @return {undefined} */ constructor(type) { const incomingLogger = generateLogger("incoming", type); const incoming = new Transform4({ objectMode: true, transform(msg, encoding, callback) { incomingLogger(msg); callback(void 0, msg); } }); const outgoingLogger = generateLogger("outgoing", type); const outgoing = new Transform4({ objectMode: true, transform(msg, encoding, callback) { outgoingLogger(msg); callback(void 0, msg); } }); super(incoming, outgoing); } }; // src/components/jpegdepay/index.ts import { Transform as Transform5 } from "stream"; // src/components/jpegdepay/headers.ts function makeImageHeader() { return Buffer.from([255, 216]); } function makeQuantHeader(precision, qTable) { const lumSize = precision & 1 ? 128 : 64; const chmSize = precision & 2 ? 128 : 64; if (qTable.length !== lumSize + chmSize) { throw new Error("invalid quantization table"); } const lumaPrefix = Buffer.from([255, 219, 0, lumSize + 3, 0]); const chromaPrefix = Buffer.from([255, 219, 0, chmSize + 3, 1]); return Buffer.concat([ lumaPrefix, qTable.slice(0, lumSize), chromaPrefix, qTable.slice(lumSize) ]); } function makeFrameHeader(width, height, type) { return Buffer.from([ 255, 192, // SOF_0 (Start Of Frame) 0, 17, 8, height >> 8, height, width >> 8, width, 3, 0, type === 0 ? 33 : 34, 0, 1, 17, 1, 2, 17, 1 ]); } var LUM_DC_CODELENS = [0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0]; var LUM_DC_SYMBOLS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; var LUM_AC_CODELENS = [0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 125]; var LUM_AC_SYMBOLS = [ 1, 2, 3, 0, 4, 17, 5, 18, 33, 49, 65, 6, 19, 81, 97, 7, 34, 113, 20, 50, 129, 145, 161, 8, 35, 66, 177, 193, 21, 82, 209, 240, 36, 51, 98, 114, 130, 9, 10, 22, 23, 24, 25, 26, 37, 38, 39, 40, 41, 42, 52, 53, 54, 55, 56, 57, 58, 67, 68, 69, 70, 71, 72, 73, 74, 83, 84, 85, 86, 87, 88, 89, 90, 99, 100, 101, 102, 103, 104, 105, 106, 115, 116, 117, 118, 119, 120, 121, 122, 131, 132, 133, 134, 135, 136, 137, 138, 146, 147, 148, 149, 150, 151, 152, 153, 154, 162, 163, 164, 165, 166, 167, 168, 169, 170, 178, 179, 180, 181, 182, 183, 184, 185, 186, 194, 195, 196, 197, 198, 199, 200, 201, 202, 210, 211, 212, 213, 214, 215, 216, 217, 218, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250 ]; var CHM_DC_CODELENS = [0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0]; var CHM_DC_SYMBOLS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; var CHM_AC_CODELENS = [0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, 1, 2, 119]; var CHM_AC_SYMBOLS = [ 0, 1, 2, 3, 17, 4, 5, 33, 49, 6, 18, 65, 81, 7, 97, 113, 19, 34, 50, 129, 8, 20, 66, 145, 161, 177, 193, 9, 35, 51, 82, 240, 21, 98, 114, 209, 10, 22, 36, 52, 225, 37, 241, 23, 24, 25, 26, 38, 39, 40, 41, 42, 53, 54, 55, 56, 57, 58, 67, 68, 69, 70, 71, 72, 73, 74, 83, 84, 85, 86, 87, 88, 89, 90, 99, 100, 101, 102, 103, 104, 105, 106, 115, 116, 117, 118, 119, 120, 121, 122, 130, 131, 132, 133, 134, 135, 136, 137, 138, 146, 147, 148, 149, 150, 151, 152, 153, 154, 162, 163, 164, 165, 166, 167, 168, 169, 170, 178, 179, 180, 181, 182, 183, 184, 185, 186, 194, 195, 196, 197, 198, 199, 200, 201, 202, 210, 211, 212, 213, 214, 215, 216, 217, 218, 226, 227, 228, 229, 230, 231, 232, 233, 234, 242, 243, 244, 245, 246, 247, 248, 249, 250 ]; function makeHuffmanHeader() { const LUM_DC_BUFFER = [ [ 255, 196, 0, 3 + LUM_DC_CODELENS.length + LUM_DC_SYMBOLS.length, 0 << 4 | 0 ], LUM_DC_CODELENS, LUM_DC_SYMBOLS ]; const LUM_AC_BUFFER = [ [ 255, 196, 0, 3 + LUM_AC_CODELENS.length + LUM_AC_SYMBOLS.length, 1 << 4 | 0 ], LUM_AC_CODELENS, LUM_AC_SYMBOLS ]; const CHM_DC_BUFFER = [ [ 255, 196, 0, 3 + CHM_DC_CODELENS.length + CHM_DC_SYMBOLS.length, 0 << 4 | 1 ], CHM_DC_CODELENS, CHM_DC_SYMBOLS ]; const CHM_AC_BUFFER = [ [ 255, 196, 0, 3 + CHM_AC_CODELENS.length + CHM_AC_SYMBOLS.length, 1 << 4 | 1 ], CHM_AC_CODELENS, CHM_AC_SYMBOLS ]; return Buffer.concat([ ...LUM_DC_BUFFER.map(Buffer.from), ...LUM_AC_BUFFER.map(Buffer.from), ...CHM_DC_BUFFER.map(Buffer.from), ...CHM_AC_BUFFER.map(Buffer.from) ]); } function makeScanHeader() { return Buffer.from([ 255, 218, // SOS (Start Of Scan) 0, 12, 3, 0, 0, 1, 17, 2, 17, 0, 63, 0 ]); } function makeDRIHeader(dri) { return Buffer.from([255, 221, 0, 4, dri >> 8, dri & 255]); } // src/utils/clamp.ts function clamp(val, min, max) { return val > max ? max : val < min ? min : val; } // src/components/jpegdepay/make-qtable.ts var jpegLumaQuantizer = [ 16, 11, 12, 14, 12, 10, 16, 14, 13, 14, 18, 17, 16, 19, 24, 40, 26, 24, 22, 22, 24, 49, 35, 37, 29, 40, 58, 51, 61, 60, 57, 51, 56, 55, 64, 72, 92, 78, 64, 68, 87, 69, 55, 56, 80, 109, 81, 87, 95, 98, 103, 104, 103, 62, 77, 113, 121, 112, 100, 120, 92, 101, 103, 99 ]; var jpeChromaQuantizer = [ 17, 18, 18, 24, 21, 24, 47, 26, 26, 47, 99, 66, 56, 66, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99 ]; function makeQtable(Q) { const factor = clamp(Q, 1, 99); const buffer = Buffer.alloc(128); const S = Q < 50 ? Math.floor(5e3 / factor) : 200 - factor * 2; for (let i = 0; i < 64; i++) { const lq = Math.floor((jpegLumaQuantizer[i] * S + 50) / 100); const cq = Math.floor((jpeChromaQuantizer[i] * S + 50) / 100); buffer.writeUInt8(clamp(lq, 1, 255), i); buffer.writeUInt8(clamp(cq, 1, 255), i + 64); } return buffer; } // src/components/jpegdepay/parser.ts function jpegDepayFactory(defaultWidth = 0, defaultHeight = 0) { const IMAGE_HEADER = makeImageHeader(); const HUFFMAN_HEADER = makeHuffmanHeader(); const SCAN_HEADER = makeScanHeader(); return function jpegDepay(packets) { let metadata; const fragments = []; for (const packet of packets) { let fragment = payload(packet); const typeSpecific = fragment.readUInt8(0); const fragmentOffset = fragment.readUInt8(1) << 16 | fragment.readUInt8(2) << 8 | fragment.readUInt8(3); const type2 = fragment.readUInt8(4); const Q = fragment.readUInt8(5); const width2 = fragment.readUInt8(6) * 8 || defaultWidth; const height2 = fragment.readUInt8(7) * 8 || defaultHeight; fragment = fragment.slice(8); let DRI = 0; if (type2 >= 64 && type2 <= 127) { DRI = fragment.readUInt16BE(0); fragment = fragment.slice(4); } if (Q >= 128 && fragmentOffset === 0) { const precision2 = fragment.readUInt8(1); const length = fragment.readUInt16BE(2); const qTable2 = fragment.slice(4, 4 + length); metadata = { typeSpecific, type: type2, width: width2, height: height2, DRI, precision: precision2, qTable: qTable2 }; fragment = fragment.slice(4 + length); } else if (Q < 128 && fragmentOffset === 0) { const precision2 = 0; const qTable2 = makeQtable(Q); metadata = { typeSpecific, type: type2, width: width2, height: height2, DRI, precision: precision2, qTable: qTable2 }; } fragments.push(fragment); } if (metadata === void 0) { throw new Error("no quantization header present"); } const { precision, qTable, type, width, height } = metadata; const quantHeader = makeQuantHeader(precision, qTable); const driHeader = metadata.DRI === 0 ? Buffer.alloc(0) : makeDRIHeader(metadata.DRI); const frameHeader = makeFrameHeader(width, height, type); return { size: { width, height }, data: Buffer.concat([ IMAGE_HEADER, quantHeader, driHeader, frameHeader, HUFFMAN_HEADER, SCAN_HEADER, ...fragments ]) }; }; } // src/components/jpegdepay/index.ts var JPEGDepay = class extends Tube { constructor() { let jpegPayloadType; let packets = []; let jpegDepay; const incoming = new Transform5({ objectMode: true, transform(msg, encoding, callback) { if (msg.type === 5 /* SDP */) { const jpegMedia = msg.sdp.media.find((media) => { return media.type === "video" && media.rtpmap !== void 0 && media.rtpmap.encodingName === "JPEG"; }); if (jpegMedia !== void 0 && jpegMedia.rtpmap !== void 0) { jpegPayloadType = Number(jpegMedia.rtpmap.payloadType); const framesize = jpegMedia.framesize; if (framesize !== void 0) { const [width, height] = framesize; jpegDepay = jpegDepayFactory(width, height); } else { jpegDepay = jpegDepayFactory(); } } callback(void 0, msg); } else if (msg.type === 2 /* RTP */ && payloadType(msg.data) === jpegPayloadType) { packets.push(msg.data); if (marker(msg.data) && packets.length > 0) { const jpegFrame = jpegDepay(packets); this.push({ timestamp: timestamp(msg.data), ntpTimestamp: msg.ntpTimestamp, payloadType: payloadType(msg.data), data: jpegFrame.data, framesize: jpegFrame.size, type: 10 /* JPEG */ }); packets = []; } callback(); } else { callback(void 0, msg); } } }); super(incoming); } }; // src/components/mp4capture/index.ts import debug3 from "debug"; import { Transform as Transform6 } from "stream"; var MAX_CAPTURE_BYTES = 225e6; var Mp4Capture = class extends Tube { constructor(maxSize = MAX_CAPTURE_BYTES) { const incoming = new Transform6({ objectMode: true, transform: (msg, _encoding, callback) => { if (this._active && msg.type === 8 /* ISOM */ && msg.tracks !== void 0) { this._capture = true; } if (this._capture && msg.type === 8 /* ISOM */) { if (this._bufferOffset < this._buffer.byteLength - msg.data.byteLength) { msg.data.copy(this._buffer, this._bufferOffset); this._bufferOffset += msg.data.byteLength; } else { this.stop(); } } callback(void 0, msg); } }); incoming.on("finish", () => { this.stop(); }); super(incoming); __publicField(this, "_active"); __publicField(this, "_capture"); __publicField(this, "_captureCallback"); __publicField(this, "_bufferOffset"); __publicField(this, "_bufferSize"); __publicField(this, "_buffer"); this._buffer = Buffer.allocUnsafe(0); this._bufferSize = maxSize; this._bufferOffset = 0; this._active = false; this._capture = false; this._captureCallback = () => { }; } /** * Activate video capture. The capture will begin when a new movie starts, * and will terminate when the movie ends or when the buffer is full. On * termination, the callback you passed will be called with the captured * data as argument. * @param callback Will be called when data is captured. */ start(callback) { if (!this._active) { debug3("msl:capture:start")(callback); this._captureCallback = callback; this._buffer = Buffer.allocUnsafe(this._bufferSize); this._bufferOffset = 0; this._active = true; } } /** * Deactivate video capture. This ends an ongoing capture and prevents * any further capturing. */ stop() { if (this._active) { debug3("msl:capture:stop")(`captured bytes: ${this._bufferOffset}`); try { this._captureCallback(this._buffer.slice(0, this._bufferOffset)); } catch (e) { console.error(e); } this._buffer = Buffer.allocUnsafe(0); this._bufferOffset = 0; this._active = false; this._capture = false; } } }; // src/components/mp4muxer/index.ts import debug4 from "debug"; import { Transform as Transform7 } from "stream"; // src/components/mp4muxer/helpers/isom.ts var UINT32_RANGE = Math.pow(2, 32); var BoxElement = class { constructor(size) { __publicField(this, "byteLength"); __publicField(this, "value"); this.byteLength = size; } }; var Empty = class extends BoxElement { constructor(size = 0) { super(size); __publicField(this, "copy", (buffer, offset) => { buffer.fill(0, offset, offset + this.byteLength); }); } load() { } }; var CharArray = class extends BoxElement { constructor(s) { super(s.length); __publicField(this, "value"); __publicField(this, "copy", (buffer, offset) => { for (let i = 0; i < this.byteLength; i += 1) { buffer[offset + i] = this.value.charCodeAt(i); } }); __publicField(this, "load", (buffer, offset) => { this.value = buffer.slice(offset, offset + this.byteLength).toString("ascii"); }); this.value = s; } }; var UInt8 = class extends BoxElement { constructor(scalar = 0) { super(1); __publicField(this, "value"); __publicField(this, "copy", (buffer, offset) => { buffer.writeUInt8(this.value, offset); }); __publicField(this, "load", (buffer, offset) => { this.value = buffer.readUInt8(offset); }); this.value = scalar; } }; var UInt8Array = class extends BoxElement { constructor(array) { super(array.length); __publicField(this, "value"); __publicField(this, "copy", (buffer, offset) => { for (let i = 0; i < this.value.length; ++i) { buffer.writeUInt8(this.value[i], offset + i); } }); __publicField(this, "load", (buffer, offset) => { for (let i = 0; i < this.value.length; ++i) { this.value[i] = buffer.readUInt8(offset + i); } }); this.value = array; } }; var UInt16BE = class extends BoxElement { constructor(scalar = 0) { super(2); __publicField(this, "value"); __publicField(this, "copy", (buffer, offset) => { buffer.writeUInt16BE(this.value, offset); }); __publicField(this, "load", (buffer, offset) => { this.value = buffer.readUInt16BE(offset); }); this.value = scalar; } }; var UInt24BE = class extends BoxElement { constructor(scalar = 0) { super(3); __publicField(this, "value"); __publicField(this, "copy", (buffer, offset) => { buffer.writeUInt8(this.value >> 16 & 255, offset); buffer.writeUInt8(this.value >> 8 & 255, offset + 1); buffer.writeUInt8(this.value & 255, offset + 2); }); __publicField(this, "load", (buffer, offset) => { this.value = buffer.readUInt8(offset) << 16 + buffer.readUInt8(offset + 1) << 8 + buffer.readUInt8(offset + 2); }); this.value = scalar; } }; var UInt16BEArray = class extends BoxElement { constructor(array) { super(array.length * 2); __publicField(this, "value"); __publicField(this, "copy", (buffer, offset) => { for (let i = 0; i < this.value.length; ++i) { buffer.writeUInt16BE(this.value[i], offset + 2 * i); } }); __publicField(this, "load", (buffer, offset) => { for (let i = 0; i < this.value.length; ++i) { this.value[i] = buffer.readUInt16BE(offset + 2 * i); } }); this.value = array; } }; var UInt32BE = class extends BoxElement { constructor(scalar = 0) { super(4); __publicField(this, "value"); __publicField(this, "copy", (buffer, offset) => { buffer.