media-stream-library
Version:
Media stream library for Node & the Web.
2,024 lines (1,995 loc) • 169 kB
JavaScript
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.