diff --git a/dev/index.html b/dev/index.html
index d679e00..858e1c8 100644
--- a/dev/index.html
+++ b/dev/index.html
@@ -36,9 +36,10 @@
canvas.height = 720;
const context = canvas.getContext('2d');
- let format = new Metamuxer.WebMOutputFormat();
+ let format = new Metamuxer.WebMOutputFormat({ streamable: true });
format = new Metamuxer.Mp4OutputFormat({ fastStart: false }); // new Metamuxer.MkvOutputFormat();// new Metamuxer.Mp4OutputFormat({ fastStart: false });
let target = new Metamuxer.ArrayBufferTarget();
+ //target = new Metamuxer.StreamTarget({ onData: (data, pos) => console.log(data, pos), chunked: true }) ??
let output = new Metamuxer.Output({ format, target });
@@ -142,8 +143,8 @@ Testing... <00:17.350>One... <00:18.125>Two...
9. justify (bottom, right).
`;
- subtitleSource.digest(simpleWebvttFile);
- subtitleSource.close();
+ //subtitleSource.digest(simpleWebvttFile);
+ //subtitleSource.close();
for (let i = 0; i < 100; i++) {
context.fillStyle = ['red', 'green', 'blue', 'yellow'][i % 4];
diff --git a/dist/metamuxer.js b/dist/metamuxer.js
index ab491ab..4d29cb1 100644
--- a/dist/metamuxer.js
+++ b/dist/metamuxer.js
@@ -21,13 +21,13 @@ var Metamuxer = (() => {
// src/index.ts
var src_exports = {};
__export(src_exports, {
- ArrayBufferTarget: () => ArrayBufferTarget2,
+ ArrayBufferTarget: () => ArrayBufferTarget,
AudioBufferSource: () => AudioBufferSource,
AudioDataSource: () => AudioDataSource,
CanvasSource: () => CanvasSource,
EncodedAudioChunkSource: () => EncodedAudioChunkSource,
EncodedVideoChunkSource: () => EncodedVideoChunkSource,
- FileSystemWritableFileStreamTarget: () => FileSystemWritableFileStreamTarget2,
+ FileSystemWritableFileStreamTarget: () => FileSystemWritableFileStreamTarget,
MediaStreamAudioTrackSource: () => MediaStreamAudioTrackSource,
MediaStreamVideoTrackSource: () => MediaStreamVideoTrackSource,
MkvOutputFormat: () => MkvOutputFormat2,
@@ -40,69 +40,6 @@ var Metamuxer = (() => {
WebMOutputFormat: () => WebMOutputFormat
});
- // src/output.ts
- var Output = class {
- constructor(options) {
- this.tracks = [];
- this.started = false;
- this.finalizing = false;
- if (options.target.output) {
- throw new Error("Target is already used for another output.");
- }
- options.target.output = this;
- this.writer = options.target.createWriter();
- this.muxer = options.format.createMuxer(this);
- }
- addVideoTrack(source, metadata = {}) {
- this.addTrack("video", source, metadata);
- }
- addAudioTrack(source, metadata = {}) {
- this.addTrack("audio", source, metadata);
- }
- addSubtitleTrack(source, metadata = {}) {
- this.addTrack("subtitle", source, metadata);
- }
- addTrack(type, source, metadata) {
- if (this.started) {
- throw new Error("Cannot add track after output has started.");
- }
- if (source.connectedTrack) {
- throw new Error("Source is already used for a track.");
- }
- const track = {
- id: this.tracks.length + 1,
- output: this,
- type,
- source,
- metadata
- };
- this.muxer.beforeTrackAdd(track);
- this.tracks.push(track);
- source.connectedTrack = track;
- }
- start() {
- if (this.started) {
- throw new Error("Output already started.");
- }
- this.started = true;
- this.muxer.start();
- for (const track of this.tracks) {
- track.source.start();
- }
- }
- async finalize() {
- if (this.finalizing) {
- throw new Error("Cannot call finalize twice.");
- }
- this.finalizing = true;
- const promises = this.tracks.map((x) => x.source.flush());
- await Promise.all(promises);
- this.muxer.finalize();
- this.writer.flush();
- this.writer.finalize();
- }
- };
-
// src/misc.ts
function assert(x) {
if (!x) {
@@ -174,6 +111,9 @@ var Metamuxer = (() => {
var colorSpaceIsComplete = (colorSpace) => {
return !!colorSpace && !!colorSpace.primaries && !!colorSpace.transfer && !!colorSpace.matrix && colorSpace.fullRange !== void 0;
};
+ var isAllowSharedBufferSource = (x) => {
+ return x instanceof ArrayBuffer || typeof SharedArrayBuffer !== "undefined" && x instanceof SharedArrayBuffer || ArrayBuffer.isView(x) && !(x instanceof DataView);
+ };
// src/subtitles.ts
var cueBlockHeaderRegex = /(?:(.+?)\n)?((?:\d{2}:)?\d{2}:\d{2}.\d{3})\s+-->\s+((?:\d{2}:)?\d{2}:\d{2}.\d{3})/g;
@@ -377,6 +317,22 @@ var Metamuxer = (() => {
view.setInt32(0, 2 ** 30 * value, false);
return [bytes[0], bytes[1], bytes[2], bytes[3]];
};
+ var variableUnsignedInt = (value, byteLength) => {
+ let bytes2 = [];
+ let remaining = value;
+ do {
+ let byte = remaining & 127;
+ remaining >>= 7;
+ if (bytes2.length > 0) {
+ byte |= 128;
+ }
+ bytes2.push(byte);
+ if (byteLength !== void 0) {
+ byteLength--;
+ }
+ } while (remaining > 0 || byteLength);
+ return bytes2.reverse();
+ };
var ascii = (text, nullTerminated = false) => {
let bytes2 = Array(text.length).fill(null).map((_, i) => text.charCodeAt(i));
if (nullTerminated) bytes2.push(0);
@@ -469,7 +425,7 @@ var Metamuxer = (() => {
return lastSample.timestamp + lastSample.duration;
})
), GLOBAL_TIMESCALE);
- let nextTrackId = Math.max(...trackDatas.map((x) => x.track.id)) + 1;
+ let nextTrackId = Math.max(0, ...trackDatas.map((x) => x.track.id)) + 1;
let needsU64 = !isU32(creationTime) || !isU32(duration);
let u32OrU64 = needsU64 ? u64 : u32;
return fullBox("mvhd", +needsU64, 0, [
@@ -720,7 +676,7 @@ var Metamuxer = (() => {
...toUint8Array(trackData.info.decoderConfig.description)
]);
var hvcC = (trackData) => trackData.info.decoderConfig && box("hvcC", [
- // For HEVC, description is a HEVCDecoderConfigurationRecord, so nothing else to do here
+ // For HEVC, description is an HEVCDecoderConfigurationRecord, so nothing else to do here
...toUint8Array(trackData.info.decoderConfig.description)
]);
var vpcC = (trackData) => {
@@ -728,9 +684,7 @@ var Metamuxer = (() => {
return null;
}
let decoderConfig = trackData.info.decoderConfig;
- if (!decoderConfig.colorSpace) {
- throw new Error(`'colorSpace' is required in the decoder config for VP8/VP9.`);
- }
+ assert(decoderConfig.colorSpace);
let parts = decoderConfig.codec.split(".");
let profile = Number(parts[1]);
let level = Number(parts[2]);
@@ -794,51 +748,55 @@ var Metamuxer = (() => {
]);
var esds = (trackData) => {
let description = toUint8Array(trackData.info.decoderConfig.description ?? new ArrayBuffer(0));
- return fullBox("esds", 0, 0, [
- // https://stackoverflow.com/a/54803118
- u32(58753152),
- // TAG(3) = Object Descriptor ([2])
- u8(32 + description.byteLength),
- // length of this OD (which includes the next 2 tags)
- u16(1),
- // ES_ID = 1
- u8(0),
- // flags etc = 0
- u32(75530368),
- // TAG(4) = ES Descriptor ([2]) embedded in above OD
- u8(18 + description.byteLength),
- // length of this ESD
- u8(64),
+ let bytes2 = [
+ ...description
+ ];
+ bytes2 = [
+ ...u8(64),
// MPEG-4 Audio
- u8(21),
+ ...u8(21),
// stream type(6bits)=5 audio, flags(2bits)=1
- u24(0),
+ ...u24(0),
// 24bit buffer size
- u32(130071),
+ ...u32(0),
// max bitrate
- u32(130071),
+ ...u32(0),
// avg bitrate
- u32(92307584),
+ ...u8(5),
// TAG(5) = ASC ([2],[3]) embedded in above OD
- u8(description.byteLength),
- // length
- ...description,
- u32(109084800),
+ ...variableUnsignedInt(bytes2.length),
+ ...bytes2
+ ];
+ bytes2 = [
+ ...u16(1),
+ // ES_ID = 1
+ ...u8(0),
+ // flags etc = 0
+ ...u8(4),
+ // TAG(4) = ES Descriptor ([2]) embedded in above OD
+ ...variableUnsignedInt(bytes2.length),
+ ...bytes2,
+ ...u8(6),
// TAG(6)
- u8(1),
+ ...u8(1),
// length
- u8(2)
+ ...u8(2)
// data
- ]);
+ ];
+ bytes2 = [
+ ...u8(3),
+ // TAG(3) = Object Descriptor ([2])
+ ...variableUnsignedInt(bytes2.length),
+ ...bytes2
+ ];
+ return fullBox("esds", 0, 0, bytes2);
};
var dOps = (trackData) => {
let preskip = 3840;
let gain = 0;
const description = trackData.info.decoderConfig?.description;
if (description) {
- if (description.byteLength < 18) {
- throw new TypeError("Invalid decoder description provided for Opus; must be at least 18 bytes long.");
- }
+ assert(description.byteLength < 18);
const view2 = ArrayBuffer.isView(description) ? new DataView(description.buffer, description.byteOffset, description.byteLength) : new DataView(description);
preskip = view2.getUint16(10, true);
gain = view2.getInt16(14, true);
@@ -1186,10 +1144,79 @@ var Metamuxer = (() => {
}
};
- // src/writer.ts
- var Writer = class {
+ // src/target.ts
+ var Target = class {
+ constructor() {
+ this.output = null;
+ }
};
- var ArrayBufferTargetWriter = class extends Writer {
+ var ArrayBufferTarget = class extends Target {
+ constructor() {
+ super(...arguments);
+ this.buffer = null;
+ }
+ createWriter() {
+ return new ArrayBufferTargetWriter(this);
+ }
+ };
+ var StreamTarget = class extends Target {
+ constructor(options) {
+ super();
+ this.options = options;
+ if (typeof options !== "object") {
+ throw new TypeError("StreamTarget requires an options object to be passed to its constructor.");
+ }
+ if (options.onData) {
+ if (typeof options.onData !== "function") {
+ throw new TypeError("options.onData, when provided, must be a function.");
+ }
+ if (options.onData.length < 2) {
+ throw new TypeError(
+ "options.onData, when provided, must be a function that takes in at least two arguments (data and position). Ignoring the position argument, which specifies the byte offset at which the data is to be written, can lead to broken outputs."
+ );
+ }
+ }
+ if (options.chunked !== void 0 && typeof options.chunked !== "boolean") {
+ throw new TypeError("options.chunked, when provided, must be a boolean.");
+ }
+ if (options.chunkSize !== void 0 && (!Number.isInteger(options.chunkSize) || options.chunkSize <= 0)) {
+ throw new TypeError("options.chunkSize, when provided, must be a positive integer.");
+ }
+ }
+ createWriter() {
+ return this.options.chunked ? new ChunkedStreamTargetWriter(this) : new StreamTargetWriter(this);
+ }
+ };
+ var FileSystemWritableFileStreamTarget = class extends Target {
+ constructor(stream, options) {
+ super();
+ this.stream = stream;
+ this.options = options;
+ if (!(stream instanceof FileSystemWritableFileStream)) {
+ throw new TypeError("FileSystemWritableFileStreamTarget requires a FileSystemWritableFileStream instance.");
+ }
+ if (options !== void 0 && typeof options !== "object") {
+ throw new TypeError("FileSystemWritableFileStreamTarget's options, when provided, must be an object.");
+ }
+ if (options) {
+ if (options.chunkSize !== void 0 && (!Number.isInteger(options.chunkSize) || options.chunkSize <= 0)) {
+ throw new TypeError("options.chunkSize, when provided, must be a positive integer");
+ }
+ }
+ }
+ createWriter() {
+ return new FileSystemWritableFileStreamTargetWriter(this);
+ }
+ };
+
+ // src/writer.ts
+ var Writer2 = class {
+ constructor() {
+ /** Setting this to true will cause the writer to ensure data is written in a strictly monotonic, streamable way. */
+ this.ensureMonotonicity = false;
+ }
+ };
+ var ArrayBufferTargetWriter = class extends Writer2 {
#pos = 0;
#target;
#buffer = new ArrayBuffer(2 ** 16);
@@ -1231,10 +1258,11 @@ var Metamuxer = (() => {
return this.#bytes.slice(start, end);
}
};
- var StreamTargetWriter = class extends Writer {
+ var StreamTargetWriter = class extends Writer2 {
#pos = 0;
#target;
#sections = [];
+ #lastFlushEnd = 0;
constructor(target) {
super();
this.#target = target;
@@ -1279,7 +1307,11 @@ var Metamuxer = (() => {
chunk.data.set(section.data, section.start - chunk.start);
}
}
+ if (this.ensureMonotonicity && chunk.start !== this.#lastFlushEnd) {
+ throw new Error("Internal error: Monotonicity violation.");
+ }
this.#target.options.onData?.(chunk.data, chunk.start);
+ this.#lastFlushEnd = chunk.start + chunk.data.byteLength;
}
this.#sections.length = 0;
}
@@ -1288,7 +1320,7 @@ var Metamuxer = (() => {
};
var DEFAULT_CHUNK_SIZE = 2 ** 24;
var MAX_CHUNKS_AT_ONCE = 2;
- var ChunkedStreamTargetWriter = class extends Writer {
+ var ChunkedStreamTargetWriter = class extends Writer2 {
#pos = 0;
#target;
#chunkSize;
@@ -1297,6 +1329,7 @@ var Metamuxer = (() => {
* A chunk is flushed if all of its contents have been written.
*/
#chunks = [];
+ #lastFlushEnd = 0;
constructor(target) {
super();
this.#target = target;
@@ -1378,10 +1411,14 @@ var Metamuxer = (() => {
let chunk = this.#chunks[i];
if (!chunk.shouldFlush && !force) continue;
for (let section of chunk.written) {
+ if (this.ensureMonotonicity && chunk.start + section.start !== this.#lastFlushEnd) {
+ throw new Error("Internal error: Monotonicity violation.");
+ }
this.#target.options.onData?.(
chunk.data.subarray(section.start, section.end),
chunk.start + section.start
);
+ this.#lastFlushEnd = chunk.start + section.end;
}
this.#chunks.splice(i--, 1);
}
@@ -1405,68 +1442,200 @@ var Metamuxer = (() => {
}
};
- // src/target.ts
- var Target = class {
- constructor() {
- this.output = null;
+ // src/codec.ts
+ var buildVideoCodecString = (codec, width, height) => {
+ if (codec === "avc") {
+ let profileIndication = 100;
+ if (width <= 768 && height <= 432) {
+ profileIndication = 66;
+ } else if (width <= 1920 && height <= 1080) {
+ profileIndication = 77;
+ }
+ const profileCompatibility = 0;
+ const levelIndication = width > 1920 || height > 1080 ? 50 : 41;
+ const hexProfileIndication = profileIndication.toString(16).padStart(2, "0");
+ const hexProfileCompatibility = profileCompatibility.toString(16).padStart(2, "0");
+ const hexLevelIndication = levelIndication.toString(16).padStart(2, "0");
+ return `avc1.${hexProfileIndication}${hexProfileCompatibility}${hexLevelIndication}`;
+ } else if (codec === "hevc") {
+ let profileSpace = 0;
+ let profileIdc = 1;
+ const compatibilityFlags = Array(32).fill(0);
+ compatibilityFlags[profileIdc] = 1;
+ const compatibilityHex = parseInt(compatibilityFlags.reverse().join(""), 2).toString(16).replace(/^0+/, "");
+ let tier = "L";
+ let level = 120;
+ if (width <= 1280 && height <= 720) {
+ level = 93;
+ } else if (width <= 1920 && height <= 1080) {
+ level = 120;
+ } else if (width <= 3840 && height <= 2160) {
+ level = 150;
+ } else {
+ tier = "H";
+ level = 180;
+ }
+ const constraintFlags = "B0";
+ const profilePrefix = profileSpace === 0 ? "" : String.fromCharCode(65 + profileSpace - 1);
+ return `hev1.${profilePrefix}${profileIdc}.${compatibilityHex}.${tier}${level}.${constraintFlags}`;
+ } else if (codec === "vp8") {
+ return "vp8";
+ } else if (codec === "vp9") {
+ const profile = "00";
+ let level;
+ if (width <= 854 && height <= 480) {
+ level = "21";
+ } else if (width <= 1280 && height <= 720) {
+ level = "31";
+ } else if (width <= 1920 && height <= 1080) {
+ level = "41";
+ } else if (width <= 3840 && height <= 2160) {
+ level = "51";
+ } else {
+ level = "61";
+ }
+ const bitDepth = "08";
+ return `vp09.${profile}.${level}.${bitDepth}`;
+ } else if (codec === "av1") {
+ const profile = 0;
+ let level;
+ if (width <= 854 && height <= 480) {
+ level = "01";
+ } else if (width <= 1280 && height <= 720) {
+ level = "03";
+ } else if (width <= 1920 && height <= 1080) {
+ level = "04";
+ } else if (width <= 3840 && height <= 2160) {
+ level = "07";
+ } else {
+ level = "09";
+ }
+ const tier = "M";
+ const bitDepth = "08";
+ return `av01.${profile}.${level}${tier}.${bitDepth}`;
+ }
+ throw new TypeError(`Unhandled codec '${codec}'.`);
+ };
+ var buildAudioCodecString = (codec, numberOfChannels, sampleRate) => {
+ if (codec === "aac") {
+ if (numberOfChannels >= 2 && sampleRate <= 24e3) {
+ return "mp4a.40.29";
+ }
+ if (sampleRate <= 24e3) {
+ return "mp4a.40.5";
+ }
+ return "mp4a.40.2";
+ } else if (codec === "opus") {
+ return "opus";
+ } else if (codec === "vorbis") {
+ return "vorbis";
+ }
+ throw new TypeError(`Unhandled codec '${codec}'.`);
+ };
+ var validateVideoChunkMetadata = (metadata) => {
+ if (!metadata) {
+ throw new TypeError("Video chunk metadata must be provided.");
+ }
+ if (typeof metadata !== "object") {
+ throw new TypeError("Video chunk metadata must be an object.");
+ }
+ if (!metadata.decoderConfig) {
+ throw new TypeError("Video chunk metadata must include a decoder configuration.");
+ }
+ if (typeof metadata.decoderConfig !== "object") {
+ throw new TypeError("Video chunk metadata decoder configuration must be an object.");
+ }
+ if (typeof metadata.decoderConfig.codec !== "string") {
+ throw new TypeError("Video chunk metadata decoder configuration must specify a codec string.");
+ }
+ if (!Number.isInteger(metadata.decoderConfig.codedWidth) || metadata.decoderConfig.codedWidth <= 0) {
+ throw new TypeError("Video chunk metadata decoder configuration must specify a valid codedWidth (positive integer).");
+ }
+ if (!Number.isInteger(metadata.decoderConfig.codedHeight) || metadata.decoderConfig.codedHeight <= 0) {
+ throw new TypeError("Video chunk metadata decoder configuration must specify a valid codedHeight (positive integer).");
+ }
+ if (metadata.decoderConfig.description !== void 0) {
+ if (!isAllowSharedBufferSource(metadata.decoderConfig.description)) {
+ throw new TypeError("Video chunk metadata decoder configuration description, when defined, must be an ArrayBuffer or an ArrayBuffer view.");
+ }
+ }
+ if (metadata.decoderConfig.colorSpace !== void 0) {
+ let { colorSpace } = metadata.decoderConfig;
+ if (typeof colorSpace !== "object") {
+ throw new TypeError("Video chunk metadata decoder configuration colorSpace, when provided, must be an object.");
+ }
+ let primariesValues = Object.keys(COLOR_PRIMARIES_MAP);
+ if (colorSpace.primaries != null && !primariesValues.includes(colorSpace.primaries)) {
+ throw new TypeError(`Video chunk metadata decoder configuration colorSpace primaries, when defined, must be one of ${primariesValues.join(", ")}.`);
+ }
+ let transferValues = Object.keys(TRANSFER_CHARACTERISTICS_MAP);
+ if (colorSpace.transfer != null && !transferValues.includes(colorSpace.transfer)) {
+ throw new TypeError(`Video chunk metadata decoder configuration colorSpace transfer, when defined, must be one of ${transferValues.join(", ")}.`);
+ }
+ let matrixValues = Object.keys(MATRIX_COEFFICIENTS_MAP);
+ if (colorSpace.matrix != null && !matrixValues.includes(colorSpace.matrix)) {
+ throw new TypeError(`Video chunk metadata decoder configuration colorSpace matrix, when defined, must be one of ${matrixValues.join(", ")}.`);
+ }
+ if (colorSpace.fullRange != null && typeof colorSpace.fullRange !== "boolean") {
+ throw new TypeError("Video chunk metadata decoder configuration colorSpace fullRange, when defined, must be a boolean.");
+ }
+ }
+ if ((metadata.decoderConfig.codec.startsWith("avc1") || metadata.decoderConfig.codec.startsWith("avc3")) && !metadata.decoderConfig.description) {
+ throw new TypeError("Video chunk metadata decoder configuration for AVC must include a description, which is expected to be an AVCDecoderConfigurationRecord as specified in ISO 14496-15.");
+ }
+ if ((metadata.decoderConfig.codec.startsWith("hev1") || metadata.decoderConfig.codec.startsWith("hvc1")) && !metadata.decoderConfig.description) {
+ throw new TypeError("Video chunk metadata decoder configuration for HEVC must include a description, which is expected to be an HEVCDecoderConfigurationRecord as specified in ISO 14496-15.");
+ }
+ if ((metadata.decoderConfig.codec === "vp8" || metadata.decoderConfig.codec.startsWith("vp09")) && metadata.decoderConfig.colorSpace === void 0) {
+ throw new TypeError("Video chunk metadata decoder configuration for VP8/VP9 must include a colorSpace.");
}
};
- var ArrayBufferTarget2 = class extends Target {
- constructor() {
- super(...arguments);
- this.buffer = null;
+ var validateAudioChunkMetadata = (metadata) => {
+ if (!metadata) {
+ throw new TypeError("Audio chunk metadata must be provided.");
}
- createWriter() {
- return new ArrayBufferTargetWriter(this);
+ if (typeof metadata !== "object") {
+ throw new TypeError("Audio chunk metadata must be an object.");
+ }
+ if (!metadata.decoderConfig) {
+ throw new TypeError("Audio chunk metadata must include a decoder configuration.");
+ }
+ if (typeof metadata.decoderConfig !== "object") {
+ throw new TypeError("Audio chunk metadata decoder configuration must be an object.");
+ }
+ if (typeof metadata.decoderConfig.codec !== "string") {
+ throw new TypeError("Audio chunk metadata decoder configuration must specify a codec string.");
+ }
+ if (!Number.isInteger(metadata.decoderConfig.sampleRate) || metadata.decoderConfig.sampleRate <= 0) {
+ throw new TypeError("Audio chunk metadata decoder configuration must specify a valid sampleRate (positive integer).");
+ }
+ if (!Number.isInteger(metadata.decoderConfig.numberOfChannels) || metadata.decoderConfig.numberOfChannels <= 0) {
+ throw new TypeError("Audio chunk metadata decoder configuration must specify a valid numberOfChannels (positive integer).");
+ }
+ if (metadata.decoderConfig.description !== void 0) {
+ if (!isAllowSharedBufferSource(metadata.decoderConfig.description)) {
+ throw new TypeError("Audio chunk metadata decoder configuration description, when defined, must be an ArrayBuffer or an ArrayBuffer view.");
+ }
+ }
+ if (metadata.decoderConfig.codec === "opus" && metadata.decoderConfig.description && metadata.decoderConfig.description.byteLength < 18) {
+ throw new TypeError("Invalid decoder description provided for Opus; must be at least 18 bytes long.");
}
};
- var StreamTarget = class extends Target {
- constructor(options) {
- super();
- this.options = options;
- if (typeof options !== "object") {
- throw new TypeError("StreamTarget requires an options object to be passed to its constructor.");
- }
- if (options.onData) {
- if (typeof options.onData !== "function") {
- throw new TypeError("options.onData, when provided, must be a function.");
- }
- if (options.onData.length < 2) {
- throw new TypeError(
- "options.onData, when provided, must be a function that takes in at least two arguments (data and position). Ignoring the position argument, which specifies the byte offset at which the data is to be written, can lead to broken outputs."
- );
- }
- }
- if (options.chunked !== void 0 && typeof options.chunked !== "boolean") {
- throw new TypeError("options.chunked, when provided, must be a boolean.");
- }
- if (options.chunkSize !== void 0 && (!Number.isInteger(options.chunkSize) || options.chunkSize <= 0)) {
- throw new TypeError("options.chunkSize, when provided, must be a positive integer.");
- }
+ var validateSubtitleMetadata = (metadata) => {
+ if (!metadata) {
+ throw new TypeError("Subtitle metadata must be provided.");
}
- createWriter() {
- return this.options.chunked ? new ChunkedStreamTargetWriter(this) : new StreamTargetWriter(this);
+ if (typeof metadata !== "object") {
+ throw new TypeError("Subtitle metadata must be an object.");
}
- };
- var FileSystemWritableFileStreamTarget2 = class extends Target {
- constructor(stream, options) {
- super();
- this.stream = stream;
- this.options = options;
- if (!(stream instanceof FileSystemWritableFileStream)) {
- throw new TypeError("FileSystemWritableFileStreamTarget requires a FileSystemWritableFileStream instance.");
- }
- if (options !== void 0 && typeof options !== "object") {
- throw new TypeError("FileSystemWritableFileStreamTarget's options, when provided, must be an object.");
- }
- if (options) {
- if (options.chunkSize !== void 0 && (!Number.isInteger(options.chunkSize) || options.chunkSize <= 0)) {
- throw new TypeError("options.chunkSize, when provided, must be a positive integer");
- }
- }
+ if (!metadata.config) {
+ throw new TypeError("Subtitle metadata must include a config object.");
}
- createWriter() {
- return new FileSystemWritableFileStreamTargetWriter(this);
+ if (typeof metadata.config !== "object") {
+ throw new TypeError("Subtitle metadata config must be an object.");
+ }
+ if (typeof metadata.config.description !== "string") {
+ throw new TypeError("Subtitle metadata config description must be a string.");
}
};
@@ -1481,7 +1650,7 @@ var Metamuxer = (() => {
constructor(output, format) {
super(output);
this.timestampsMustStartAtZero = true;
- this.#auxTarget = new ArrayBufferTarget2();
+ this.#auxTarget = new ArrayBufferTarget();
this.#auxWriter = this.#auxTarget.createWriter();
this.#auxBoxWriter = new IsobmffBoxWriter(this.#auxWriter);
this.#ftypSize = null;
@@ -1493,10 +1662,15 @@ var Metamuxer = (() => {
this.#writer = output.writer;
this.#boxWriter = new IsobmffBoxWriter(this.#writer);
this.#format = format;
+ this.#fastStart = format.options.fastStart ?? (this.#writer instanceof ArrayBufferTargetWriter ? "in-memory" : false);
+ if (this.#fastStart === "in-memory" || this.#fastStart === "fragmented") {
+ this.#writer.ensureMonotonicity = true;
+ }
}
#writer;
#boxWriter;
#format;
+ #fastStart;
#auxTarget;
#auxWriter;
#auxBoxWriter;
@@ -1510,45 +1684,24 @@ var Metamuxer = (() => {
const holdsAvc = this.output.tracks.some((x) => x.type === "video" && x.source.codec === "avc");
this.#boxWriter.writeBox(ftyp({
holdsAvc,
- fragmented: this.#format.options.fastStart === "fragmented"
+ fragmented: this.#fastStart === "fragmented"
}));
this.#ftypSize = this.#writer.getPos();
- if (this.#format.options.fastStart === "in-memory") {
+ if (this.#fastStart === "in-memory") {
this.#mdat = mdat(false);
- } else if (this.#format.options.fastStart === "fragmented") {
+ } else if (this.#fastStart === "fragmented") {
} else {
- if (typeof this.#format.options.fastStart === "object") {
- let moovSizeUpperBound = this.#computeMoovSizeUpperBound();
- this.#writer.seek(this.#writer.getPos() + moovSizeUpperBound);
- }
this.#mdat = mdat(true);
this.#boxWriter.writeBox(this.#mdat);
}
this.#writer.flush();
}
- #computeMoovSizeUpperBound() {
- assert(typeof this.#format.options.fastStart === "object");
- let upperBound = 0;
- let sampleCounts = [
- this.#format.options.fastStart.expectedVideoChunks,
- this.#format.options.fastStart.expectedAudioChunks
- ];
- for (let n of sampleCounts) {
- if (!n) continue;
- upperBound += (4 + 4) * Math.ceil(2 / 3 * n);
- upperBound += 4 * n;
- upperBound += (4 + 4 + 4) * Math.ceil(2 / 3 * n);
- upperBound += 4 * n;
- upperBound += 8 * n;
- }
- upperBound += 4096;
- return upperBound;
- }
#getVideoTrackData(track, meta) {
const existingTrackData = this.#trackDatas.find((x) => x.track === track);
if (existingTrackData) {
return existingTrackData;
}
+ validateVideoChunkMetadata(meta);
assert(meta);
assert(meta.decoderConfig);
assert(meta.decoderConfig.codedWidth !== void 0);
@@ -1582,6 +1735,7 @@ var Metamuxer = (() => {
if (existingTrackData) {
return existingTrackData;
}
+ validateAudioChunkMetadata(meta);
assert(meta);
assert(meta.decoderConfig);
const newTrackData = {
@@ -1613,6 +1767,7 @@ var Metamuxer = (() => {
if (existingTrackData) {
return existingTrackData;
}
+ validateSubtitleMetadata(meta);
assert(meta);
assert(meta.config);
const newTrackData = {
@@ -1645,35 +1800,19 @@ var Metamuxer = (() => {
}
addEncodedVideoChunk(track, chunk, meta) {
const trackData = this.#getVideoTrackData(track, meta);
- if (typeof this.#format.options.fastStart === "object" && trackData.samples.length === this.#format.options.fastStart.expectedVideoChunks) {
- throw new Error(`Cannot add more video chunks than specified in 'fastStart' (${this.#format.options.fastStart.expectedVideoChunks}).`);
- }
let data = new Uint8Array(chunk.byteLength);
chunk.copyTo(data);
let timestamp = this.validateAndNormalizeTimestamp(trackData.track, chunk.timestamp, chunk.type === "key");
let sample = this.#createSampleForTrack(trackData, data, timestamp, (chunk.duration ?? 0) / 1e6, chunk.type);
- if (this.#format.options.fastStart === "fragmented") {
- trackData.sampleQueue.push(sample);
- this.#interleaveSamples();
- } else {
- this.#addSampleToTrack(trackData, sample);
- }
+ this.#registerSample(trackData, sample);
}
addEncodedAudioChunk(track, chunk, meta) {
const trackData = this.#getAudioTrackData(track, meta);
- if (typeof this.#format.options.fastStart === "object" && trackData.samples.length === this.#format.options.fastStart.expectedAudioChunks) {
- throw new Error(`Cannot add more audio chunks than specified in 'fastStart' (${this.#format.options.fastStart.expectedAudioChunks}).`);
- }
let data = new Uint8Array(chunk.byteLength);
chunk.copyTo(data);
let timestamp = this.validateAndNormalizeTimestamp(trackData.track, chunk.timestamp, chunk.type === "key");
let sample = this.#createSampleForTrack(trackData, data, timestamp, (chunk.duration ?? 0) / 1e6, chunk.type);
- if (this.#format.options.fastStart === "fragmented") {
- trackData.sampleQueue.push(sample);
- this.#interleaveSamples();
- } else {
- this.#addSampleToTrack(trackData, sample);
- }
+ this.#registerSample(trackData, sample);
}
addSubtitleCue(track, cue, meta) {
const trackData = this.#getSubtitleTrackData(track, meta);
@@ -1705,12 +1844,7 @@ var Metamuxer = (() => {
this.#auxBoxWriter.writeBox(box2);
let body2 = this.#auxWriter.getSlice(0, this.#auxWriter.getPos());
let sample2 = this.#createSampleForTrack(trackData, body2, trackData.lastCueEndTimestamp, sampleStart - trackData.lastCueEndTimestamp, "key");
- if (this.#format.options.fastStart === "fragmented") {
- trackData.sampleQueue.push(sample2);
- this.#interleaveSamples();
- } else {
- this.#addSampleToTrack(trackData, sample2);
- }
+ this.#registerSample(trackData, sample2);
trackData.lastCueEndTimestamp = sampleStart;
}
this.#auxWriter.seek(0);
@@ -1739,12 +1873,7 @@ var Metamuxer = (() => {
}
let body = this.#auxWriter.getSlice(0, this.#auxWriter.getPos());
let sample = this.#createSampleForTrack(trackData, body, sampleStart, sampleEnd - sampleStart, "key");
- if (this.#format.options.fastStart === "fragmented") {
- trackData.sampleQueue.push(sample);
- this.#interleaveSamples();
- } else {
- this.#addSampleToTrack(trackData, sample);
- }
+ this.#registerSample(trackData, sample);
trackData.lastCueEndTimestamp = sampleEnd;
}
}
@@ -1778,7 +1907,7 @@ var Metamuxer = (() => {
let delta = Math.round(timescaleUnits - trackData.lastTimescaleUnits);
trackData.lastTimescaleUnits += delta;
trackData.lastSample.timescaleUnitsToNextSample = delta;
- if (this.#format.options.fastStart !== "fragmented") {
+ if (this.#fastStart !== "fragmented") {
let lastTableEntry = last(trackData.timeToSampleTable);
assert(lastTableEntry);
if (lastTableEntry.sampleCount === 1) {
@@ -1817,7 +1946,7 @@ var Metamuxer = (() => {
}
} else {
trackData.lastTimescaleUnits = 0;
- if (this.#format.options.fastStart !== "fragmented") {
+ if (this.#fastStart !== "fragmented") {
trackData.timeToSampleTable.push({
sampleCount: 1,
sampleDelta: durationInTimescale
@@ -1832,11 +1961,19 @@ var Metamuxer = (() => {
}
trackData.timestampProcessingQueue.length = 0;
}
+ #registerSample(trackData, sample) {
+ if (this.#fastStart === "fragmented") {
+ trackData.sampleQueue.push(sample);
+ this.#interleaveSamples();
+ } else {
+ this.#addSampleToTrack(trackData, sample);
+ }
+ }
#addSampleToTrack(trackData, sample) {
if (sample.type === "key") {
this.#processTimestamps(trackData);
}
- if (this.#format.options.fastStart !== "fragmented") {
+ if (this.#fastStart !== "fragmented") {
trackData.samples.push(sample);
}
let beginNewChunk = false;
@@ -1844,7 +1981,7 @@ var Metamuxer = (() => {
beginNewChunk = true;
} else {
let currentChunkDuration = sample.timestamp - trackData.currentChunk.startTimestamp;
- if (this.#format.options.fastStart === "fragmented") {
+ if (this.#fastStart === "fragmented") {
const keyFrameQueuedEverywhere = this.#trackDatas.every((otherTrackData) => {
if (trackData === otherTrackData) {
return sample.type === "key";
@@ -1876,7 +2013,7 @@ var Metamuxer = (() => {
trackData.timestampProcessingQueue.push(sample);
}
#finalizeCurrentChunk(trackData) {
- assert(this.#format.options.fastStart !== "fragmented");
+ assert(this.#fastStart !== "fragmented");
if (!trackData.currentChunk) return;
trackData.finalizedChunks.push(trackData.currentChunk);
this.#finalizedChunks.push(trackData.currentChunk);
@@ -1887,7 +2024,7 @@ var Metamuxer = (() => {
samplesPerChunk: trackData.currentChunk.samples.length
});
}
- if (this.#format.options.fastStart === "in-memory") {
+ if (this.#fastStart === "in-memory") {
trackData.currentChunk.offset = 0;
return;
}
@@ -1900,7 +2037,7 @@ var Metamuxer = (() => {
this.#writer.flush();
}
#interleaveSamples() {
- assert(this.#format.options.fastStart === "fragmented");
+ assert(this.#fastStart === "fragmented");
for (const track of this.output.tracks) {
if (!track.source.closed && !this.#trackDatas.some((x) => x.track === track)) {
return;
@@ -1927,7 +2064,7 @@ var Metamuxer = (() => {
}
}
#finalizeFragment(flushWriter = true) {
- assert(this.#format.options.fastStart === "fragmented");
+ assert(this.#fastStart === "fragmented");
let fragmentNumber = this.#nextFragmentNumber++;
if (fragmentNumber === 1) {
let movieBox = moov(this.#trackDatas, this.#creationTime, true);
@@ -1982,7 +2119,7 @@ var Metamuxer = (() => {
this.#processWebVTTCues(trackData, Infinity);
}
}
- if (this.#format.options.fastStart === "fragmented") {
+ if (this.#fastStart === "fragmented") {
this.#interleaveSamples();
}
}
@@ -1993,7 +2130,7 @@ var Metamuxer = (() => {
this.#processWebVTTCues(trackData, Infinity);
}
}
- if (this.#format.options.fastStart === "fragmented") {
+ if (this.#fastStart === "fragmented") {
for (let trackData of this.#trackDatas) {
for (let sample of trackData.sampleQueue) {
this.#addSampleToTrack(trackData, sample);
@@ -2007,7 +2144,7 @@ var Metamuxer = (() => {
this.#finalizeCurrentChunk(trackData);
}
}
- if (this.#format.options.fastStart === "in-memory") {
+ if (this.#fastStart === "in-memory") {
assert(this.#mdat);
let mdatSize;
for (let i = 0; i < 2; i++) {
@@ -2037,7 +2174,7 @@ var Metamuxer = (() => {
sample.data = null;
}
}
- } else if (this.#format.options.fastStart === "fragmented") {
+ } else if (this.#fastStart === "fragmented") {
let startPos = this.#writer.getPos();
let mfraBox = mfra(this.#trackDatas);
this.#boxWriter.writeBox(mfraBox);
@@ -2054,7 +2191,7 @@ var Metamuxer = (() => {
this.#mdat.largeSize = mdatSize >= 2 ** 32;
this.#boxWriter.patchBox(this.#mdat);
let movieBox = moov(this.#trackDatas, this.#creationTime);
- if (typeof this.#format.options.fastStart === "object") {
+ if (typeof this.#fastStart === "object") {
this.#writer.seek(this.#ftypSize);
this.#boxWriter.writeBox(movieBox);
let remainingBytes = mdatPos - this.#writer.getPos();
@@ -2153,7 +2290,7 @@ var Metamuxer = (() => {
var MatroskaMuxer = class extends Muxer {
constructor(output, format) {
super(output);
- this.timestampsMustStartAtZero = true;
+ this.timestampsMustStartAtZero = false;
this.#helper = new Uint8Array(8);
this.#helperView = new DataView(this.#helper.buffer);
/**
@@ -2176,6 +2313,9 @@ var Metamuxer = (() => {
this.#duration = 0;
this.#writer = output.writer;
this.#format = format;
+ if (this.#format.options.streamable) {
+ this.#writer.ensureMonotonicity = true;
+ }
}
#writer;
#format;
@@ -2350,7 +2490,7 @@ var Metamuxer = (() => {
}
start() {
this.#writeEBMLHeader();
- if (!this.#format.options.streaming) {
+ if (!this.#format.options.streamable) {
this.#createSeekHead();
}
this.#createSegmentInfo();
@@ -2400,7 +2540,7 @@ var Metamuxer = (() => {
{ id: 2807729 /* TimestampScale */, data: 1e6 },
{ id: 19840 /* MuxingApp */, data: APP_NAME },
{ id: 22337 /* WritingApp */, data: APP_NAME },
- !this.#format.options.streaming ? segmentDuration : null
+ !this.#format.options.streamable ? segmentDuration : null
] };
this.#segmentInfo = segmentInfo;
}
@@ -2412,7 +2552,6 @@ var Metamuxer = (() => {
{ id: 215 /* TrackNumber */, data: trackData.track.id },
{ id: 29637 /* TrackUID */, data: trackData.track.id },
{ id: 131 /* TrackType */, data: TRACK_TYPE_MAP[trackData.type] },
- // TODO Subtitle case
{ id: 134 /* CodecID */, data: CODEC_STRING_MAP[trackData.track.source.codec] },
...trackData.type === "video" ? [
trackData.info.decoderConfig.description ? { id: 25506 /* CodecPrivate */, data: toUint8Array(trackData.info.decoderConfig.description) } : null,
@@ -2454,9 +2593,9 @@ var Metamuxer = (() => {
#createSegment() {
let segment = {
id: 408125543 /* Segment */,
- size: this.#format.options.streaming ? -1 : SEGMENT_SIZE_BYTES,
+ size: this.#format.options.streamable ? -1 : SEGMENT_SIZE_BYTES,
data: [
- !this.#format.options.streaming ? this.#seekHead : null,
+ !this.#format.options.streamable ? this.#seekHead : null,
this.#segmentInfo,
this.#tracksElement
]
@@ -2476,6 +2615,7 @@ var Metamuxer = (() => {
if (existingTrackData) {
return existingTrackData;
}
+ validateVideoChunkMetadata(meta);
assert(meta);
assert(meta.decoderConfig);
assert(meta.decoderConfig.codedWidth !== void 0);
@@ -2500,6 +2640,7 @@ var Metamuxer = (() => {
if (existingTrackData) {
return existingTrackData;
}
+ validateAudioChunkMetadata(meta);
assert(meta);
assert(meta.decoderConfig);
const newTrackData = {
@@ -2522,6 +2663,7 @@ var Metamuxer = (() => {
if (existingTrackData) {
return existingTrackData;
}
+ validateSubtitleMetadata(meta);
assert(meta);
assert(meta.config);
const newTrackData = {
@@ -2709,12 +2851,12 @@ ${cue.notes ?? ""}`;
}
/** Creates a new Cluster element to contain media chunks. */
#createNewCluster(msTimestamp) {
- if (this.#currentCluster && !this.#format.options.streaming) {
+ if (this.#currentCluster && !this.#format.options.streamable) {
this.#finalizeCurrentCluster();
}
this.#currentCluster = {
id: 524531317 /* Cluster */,
- size: this.#format.options.streaming ? -1 : CLUSTER_SIZE_BYTES,
+ size: this.#format.options.streamable ? -1 : CLUSTER_SIZE_BYTES,
data: [
{ id: 231 /* Timestamp */, data: msTimestamp }
]
@@ -2748,17 +2890,21 @@ ${cue.notes ?? ""}`;
}
/** Finalizes the file, making it ready for use. Must be called after all media chunks have been added. */
finalize() {
+ if (!this.#segment) {
+ this.#createTracks();
+ this.#createSegment();
+ }
for (let trackData of this.#trackDatas) {
while (trackData.chunkQueue.length > 0) {
this.#writeBlock(trackData, trackData.chunkQueue.shift());
}
}
- if (!this.#format.options.streaming) {
+ if (!this.#format.options.streamable && this.#currentCluster) {
this.#finalizeCurrentCluster();
}
assert(this.#cues);
this.writeEBML(this.#cues);
- if (!this.#format.options.streaming) {
+ if (!this.#format.options.streamable) {
let endPos = this.#writer.getPos();
let segmentSize = this.#writer.getPos() - this.#segmentDataOffset;
this.#writer.seek(this.offsets.get(this.#segment) + 4);
@@ -2780,7 +2926,13 @@ ${cue.notes ?? ""}`;
var OutputFormat = class {
};
var Mp4OutputFormat = class extends OutputFormat {
- constructor(options) {
+ constructor(options = {}) {
+ if (!options || typeof options !== "object") {
+ throw new TypeError("options must be an object.");
+ }
+ if (options.fastStart !== void 0 && ![false, "in-memory", "fragmented"].includes(options.fastStart)) {
+ throw new TypeError('options.fastStart, when provided, must be false, "in-memory", or "fragmented".');
+ }
super();
this.options = options;
}
@@ -2790,6 +2942,12 @@ ${cue.notes ?? ""}`;
};
var MkvOutputFormat2 = class extends OutputFormat {
constructor(options = {}) {
+ if (!options || typeof options !== "object") {
+ throw new TypeError("options must be an object.");
+ }
+ if (options.streamable !== void 0 && typeof options.streamable !== "boolean") {
+ throw new TypeError("options.streamable, when provided, must be a boolean.");
+ }
super();
this.options = options;
}
@@ -2800,104 +2958,17 @@ ${cue.notes ?? ""}`;
var WebMOutputFormat = class extends MkvOutputFormat2 {
};
- // src/codec.ts
- var buildVideoCodecString = (codec, width, height) => {
- if (codec === "avc") {
- let profileIndication = 100;
- if (width <= 768 && height <= 432) {
- profileIndication = 66;
- } else if (width <= 1920 && height <= 1080) {
- profileIndication = 77;
- }
- const profileCompatibility = 0;
- const levelIndication = width > 1920 || height > 1080 ? 50 : 41;
- const hexProfileIndication = profileIndication.toString(16).padStart(2, "0");
- const hexProfileCompatibility = profileCompatibility.toString(16).padStart(2, "0");
- const hexLevelIndication = levelIndication.toString(16).padStart(2, "0");
- return `avc1.${hexProfileIndication}${hexProfileCompatibility}${hexLevelIndication}`;
- } else if (codec === "hevc") {
- let profileSpace = 0;
- let profileIdc = 1;
- const compatibilityFlags = Array(32).fill(0);
- compatibilityFlags[profileIdc] = 1;
- const compatibilityHex = parseInt(compatibilityFlags.reverse().join(""), 2).toString(16).replace(/^0+/, "");
- let tier = "L";
- let level = 120;
- if (width <= 1280 && height <= 720) {
- level = 93;
- } else if (width <= 1920 && height <= 1080) {
- level = 120;
- } else if (width <= 3840 && height <= 2160) {
- level = 150;
- } else {
- tier = "H";
- level = 180;
- }
- const constraintFlags = "B0";
- const profilePrefix = profileSpace === 0 ? "" : String.fromCharCode(65 + profileSpace - 1);
- return `hev1.${profilePrefix}${profileIdc}.${compatibilityHex}.${tier}${level}.${constraintFlags}`;
- } else if (codec === "vp8") {
- return "vp8";
- } else if (codec === "vp9") {
- const profile = "00";
- let level;
- if (width <= 854 && height <= 480) {
- level = "21";
- } else if (width <= 1280 && height <= 720) {
- level = "31";
- } else if (width <= 1920 && height <= 1080) {
- level = "41";
- } else if (width <= 3840 && height <= 2160) {
- level = "51";
- } else {
- level = "61";
- }
- const bitDepth = "08";
- return `vp09.${profile}.${level}.${bitDepth}`;
- } else if (codec === "av1") {
- const profile = 0;
- let level;
- if (width <= 854 && height <= 480) {
- level = "01";
- } else if (width <= 1280 && height <= 720) {
- level = "03";
- } else if (width <= 1920 && height <= 1080) {
- level = "04";
- } else if (width <= 3840 && height <= 2160) {
- level = "07";
- } else {
- level = "09";
- }
- const tier = "M";
- const bitDepth = "08";
- return `av01.${profile}.${level}${tier}.${bitDepth}`;
- }
- throw new Error(`Unhandled codec '${codec}'.`);
- };
- var buildAudioCodecString = (codec, numberOfChannels, sampleRate) => {
- if (codec === "aac") {
- if (numberOfChannels >= 2 && sampleRate <= 24e3) {
- return "mp4a.40.29";
- }
- if (sampleRate <= 24e3) {
- return "mp4a.40.5";
- }
- return "mp4a.40.2";
- } else if (codec === "opus") {
- return "opus";
- } else if (codec === "vorbis") {
- return "vorbis";
- }
- throw new Error(`Unhandled codec '${codec}'.`);
- };
-
// src/source.ts
+ var VIDEO_CODECS = ["avc", "hevc", "vp8", "vp9", "av1"];
+ var AUDIO_CODECS = ["aac", "opus"];
+ var SUBTITLE_CODECS = ["webvtt"];
var MediaSource = class {
constructor() {
this.connectedTrack = null;
this.closed = false;
this.offsetTimestamps = false;
}
+ // TODO this is also just internal:
ensureValidDigest() {
if (!this.connectedTrack) {
throw new Error("Cannot call digest without connecting the source to an output track.");
@@ -2938,6 +3009,9 @@ ${cue.notes ?? ""}`;
constructor(codec) {
super();
this.connectedTrack = null;
+ if (!VIDEO_CODECS.includes(codec)) {
+ throw new TypeError(`Invalid video codec '${codec}'. Must be one of: ${VIDEO_CODECS.join(", ")}.`);
+ }
this.codec = codec;
}
};
@@ -2945,23 +3019,49 @@ ${cue.notes ?? ""}`;
constructor(codec) {
super(codec);
}
- // TODO: Ensure that the first chunk is a key frame (same for the audio case)
digest(chunk, meta) {
+ if (!(chunk instanceof EncodedVideoChunk)) {
+ throw new TypeError("chunk must be an EncodedVideoChunk.");
+ }
this.ensureValidDigest();
this.connectedTrack?.output.muxer.addEncodedVideoChunk(this.connectedTrack, chunk, meta);
}
};
var KEY_FRAME_INTERVAL = 5;
+ var validateVideoCodecConfig = (config) => {
+ if (!config || typeof config !== "object") {
+ throw new TypeError("Codec config must be an object.");
+ }
+ if (!VIDEO_CODECS.includes(config.codec)) {
+ throw new TypeError(`Invalid video codec '${config.codec}'. Must be one of: ${VIDEO_CODECS.join(", ")}.`);
+ }
+ if (!Number.isInteger(config.bitrate) || config.bitrate <= 0) {
+ throw new TypeError("config.bitrate must be a positive integer.");
+ }
+ if (config.latencyMode !== void 0 && ["quality", "realtime"].includes(config.latencyMode)) {
+ throw new TypeError("config.latencyMode, when provided, must be 'quality' or 'realtime'.");
+ }
+ };
var VideoEncoderWrapper = class {
constructor(source, codecConfig) {
this.source = source;
this.codecConfig = codecConfig;
this.encoder = null;
this.lastMultipleOfKeyFrameInterval = -1;
+ this.lastWidth = null;
+ this.lastHeight = null;
+ validateVideoCodecConfig(codecConfig);
}
- // TODO: Ensure video frame size remains constant
digest(videoFrame) {
this.source.ensureValidDigest();
+ if (this.lastWidth !== null && this.lastHeight !== null) {
+ if (videoFrame.codedWidth !== this.lastWidth || videoFrame.codedHeight !== this.lastHeight) {
+ throw new Error(`Video frame size must remain constant. Expected ${this.lastWidth}x${this.lastHeight}, got ${videoFrame.codedWidth}x${videoFrame.codedHeight}.`);
+ }
+ } else {
+ this.lastWidth = videoFrame.codedWidth;
+ this.lastHeight = videoFrame.codedHeight;
+ }
this.ensureEncoder(videoFrame);
assert(this.encoder);
const multipleOfKeyFrameInterval = Math.floor(videoFrame.timestamp / 1e6 / KEY_FRAME_INTERVAL);
@@ -2974,8 +3074,7 @@ ${cue.notes ?? ""}`;
}
this.encoder = new VideoEncoder({
output: (chunk, meta) => this.source.connectedTrack?.output.muxer.addEncodedVideoChunk(this.source.connectedTrack, chunk, meta),
- error: (error) => console.error(error)
- // TODO
+ error: (error) => console.error("Video encode error:", error)
});
this.encoder.configure({
codec: buildVideoCodecString(this.codecConfig.codec, videoFrame.codedWidth, videoFrame.codedHeight),
@@ -2996,6 +3095,9 @@ ${cue.notes ?? ""}`;
this.encoder = new VideoEncoderWrapper(this, codecConfig);
}
digest(videoFrame) {
+ if (!(videoFrame instanceof VideoFrame)) {
+ throw new TypeError("videoFrame must be a VideoFrame.");
+ }
this.encoder.digest(videoFrame);
}
flush() {
@@ -3004,11 +3106,20 @@ ${cue.notes ?? ""}`;
};
var CanvasSource = class extends VideoSource {
constructor(canvas, codecConfig) {
+ if (!(canvas instanceof HTMLCanvasElement)) {
+ throw new TypeError("canvas must be an HTMLCanvasElement.");
+ }
super(codecConfig.codec);
this.canvas = canvas;
this.encoder = new VideoEncoderWrapper(this, codecConfig);
}
digest(timestamp, duration = 0) {
+ if (!Number.isFinite(timestamp) || timestamp < 0) {
+ throw new TypeError("timestamp must be a non-negative number.");
+ }
+ if (!Number.isFinite(duration) || duration < 0) {
+ throw new TypeError("duration must be a non-negative number.");
+ }
const frame = new VideoFrame(this.canvas, {
timestamp: Math.round(1e6 * timestamp),
duration: Math.round(1e6 * duration),
@@ -3023,6 +3134,9 @@ ${cue.notes ?? ""}`;
};
var MediaStreamVideoTrackSource = class extends VideoSource {
constructor(track, codecConfig) {
+ if (!(track instanceof MediaStreamTrack) || track.kind !== "video") {
+ throw new TypeError("track must be a video MediaStreamTrack.");
+ }
super(codecConfig.codec);
this.track = track;
this.abortController = null;
@@ -3057,6 +3171,9 @@ ${cue.notes ?? ""}`;
constructor(codec) {
super();
this.connectedTrack = null;
+ if (!AUDIO_CODECS.includes(codec)) {
+ throw new TypeError(`Invalid audio codec '${codec}'. Must be one of: ${AUDIO_CODECS.join(", ")}.`);
+ }
this.codec = codec;
}
};
@@ -3065,19 +3182,43 @@ ${cue.notes ?? ""}`;
super(codec);
}
digest(chunk, meta) {
+ if (!(chunk instanceof EncodedAudioChunk)) {
+ throw new TypeError("chunk must be an EncodedAudioChunk.");
+ }
this.ensureValidDigest();
this.connectedTrack?.output.muxer.addEncodedAudioChunk(this.connectedTrack, chunk, meta);
}
};
+ var validateAudioCodecConfig = (config) => {
+ if (!config || typeof config !== "object") {
+ throw new TypeError("Codec config must be an object.");
+ }
+ if (!AUDIO_CODECS.includes(config.codec)) {
+ throw new TypeError(`Invalid audio codec '${config.codec}'. Must be one of: ${AUDIO_CODECS.join(", ")}.`);
+ }
+ if (!Number.isInteger(config.bitrate) || config.bitrate <= 0) {
+ throw new TypeError("config.bitrate must be a positive integer.");
+ }
+ };
var AudioEncoderWrapper = class {
constructor(source, codecConfig) {
this.source = source;
this.codecConfig = codecConfig;
this.encoder = null;
+ this.lastNumberOfChannels = null;
+ this.lastSampleRate = null;
+ validateAudioCodecConfig(codecConfig);
}
- // TODO: Ensure audio parameters remain constant
digest(audioData) {
this.source.ensureValidDigest();
+ if (this.lastNumberOfChannels !== null && this.lastSampleRate !== null) {
+ if (audioData.numberOfChannels !== this.lastNumberOfChannels || audioData.sampleRate !== this.lastSampleRate) {
+ throw new Error(`Audio parameters must remain constant. Expected ${this.lastNumberOfChannels} channels at ${this.lastSampleRate} Hz, got ${audioData.numberOfChannels} channels at ${audioData.sampleRate} Hz.`);
+ }
+ } else {
+ this.lastNumberOfChannels = audioData.numberOfChannels;
+ this.lastSampleRate = audioData.sampleRate;
+ }
this.ensureEncoder(audioData);
assert(this.encoder);
this.encoder.encode(audioData);
@@ -3088,8 +3229,7 @@ ${cue.notes ?? ""}`;
}
this.encoder = new AudioEncoder({
output: (chunk, meta) => this.source.connectedTrack?.output.muxer.addEncodedAudioChunk(this.source.connectedTrack, chunk, meta),
- error: (error) => console.error(error)
- // TODO
+ error: (error) => console.error("Audio encode error:", error)
});
this.encoder.configure({
codec: buildAudioCodecString(this.codecConfig.codec, audioData.numberOfChannels, audioData.sampleRate),
@@ -3108,6 +3248,9 @@ ${cue.notes ?? ""}`;
this.encoder = new AudioEncoderWrapper(this, codecConfig);
}
digest(audioData) {
+ if (!(audioData instanceof AudioData)) {
+ throw new TypeError("audioData must be an AudioData.");
+ }
this.encoder.digest(audioData);
}
flush() {
@@ -3121,6 +3264,9 @@ ${cue.notes ?? ""}`;
this.encoder = new AudioEncoderWrapper(this, codecConfig);
}
digest(audioBuffer) {
+ if (!(audioBuffer instanceof AudioBuffer)) {
+ throw new TypeError("audioBuffer must be an AudioBuffer.");
+ }
const numberOfChannels = audioBuffer.numberOfChannels;
const sampleRate = audioBuffer.sampleRate;
const numberOfFrames = audioBuffer.length;
@@ -3147,6 +3293,9 @@ ${cue.notes ?? ""}`;
};
var MediaStreamAudioTrackSource = class extends AudioSource {
constructor(track, codecConfig) {
+ if (!(track instanceof MediaStreamTrack) || track.kind !== "audio") {
+ throw new TypeError("track must be an audio MediaStreamTrack.");
+ }
super(codecConfig.codec);
this.track = track;
this.abortController = null;
@@ -3181,6 +3330,9 @@ ${cue.notes ?? ""}`;
constructor(codec) {
super();
this.connectedTrack = null;
+ if (!SUBTITLE_CODECS.includes(codec)) {
+ throw new TypeError(`Invalid subtitle codec '${codec}'. Must be one of: ${SUBTITLE_CODECS.join(", ")}.`);
+ }
this.codec = codec;
}
};
@@ -3190,15 +3342,117 @@ ${cue.notes ?? ""}`;
this.parser = new SubtitleParser({
codec,
output: (cue, metadata) => this.connectedTrack?.output.muxer.addSubtitleCue(this.connectedTrack, cue, metadata),
- error: (error) => console.error(error)
- // TODO
+ error: (error) => console.error("Subtitle parse error:", error)
});
}
digest(text) {
+ if (typeof text !== "string") {
+ throw new TypeError("text must be a string.");
+ }
this.ensureValidDigest();
this.parser.parse(text);
}
};
+
+ // src/output.ts
+ var Output = class {
+ constructor(options) {
+ this.tracks = [];
+ this.started = false;
+ this.finalizing = false;
+ if (!options || typeof options !== "object") {
+ throw new TypeError("options must be an object.");
+ }
+ if (!(options.format instanceof OutputFormat)) {
+ throw new TypeError("options.format must be an OutputFormat.");
+ }
+ if (!(options.target instanceof Target)) {
+ throw new TypeError("options.target must be a Target.");
+ }
+ if (options.target.output) {
+ throw new Error("Target is already used for another output.");
+ }
+ options.target.output = this;
+ this.writer = options.target.createWriter();
+ this.muxer = options.format.createMuxer(this);
+ }
+ addVideoTrack(source, metadata = {}) {
+ if (!(source instanceof VideoSource)) {
+ throw new TypeError("source must be a VideoSource.");
+ }
+ if (!metadata || typeof metadata !== "object") {
+ throw new TypeError("metadata must be an object.");
+ }
+ if (typeof metadata.rotation === "number" && ![0, 90, 180, 270].includes(metadata.rotation)) {
+ throw new TypeError(`Invalid video rotation: ${metadata.rotation}. Has to be 0, 90, 180 or 270.`);
+ } else if (Array.isArray(metadata.rotation) && (metadata.rotation.length !== 9 || metadata.rotation.some((value) => !Number.isFinite(value)))) {
+ throw new TypeError(`Invalid video transformation matrix: ${metadata.rotation.join()}`);
+ }
+ if (metadata.frameRate !== void 0 && (!Number.isInteger(metadata.frameRate) || metadata.frameRate <= 0)) {
+ throw new TypeError(
+ `Invalid video frame rate: ${metadata.frameRate}. Must be a positive integer.`
+ );
+ }
+ this.addTrack("video", source, metadata);
+ }
+ addAudioTrack(source, metadata = {}) {
+ if (!(source instanceof AudioSource)) {
+ throw new TypeError("source must be an AudioSource.");
+ }
+ if (!metadata || typeof metadata !== "object") {
+ throw new TypeError("metadata must be an object.");
+ }
+ this.addTrack("audio", source, metadata);
+ }
+ addSubtitleTrack(source, metadata = {}) {
+ if (!(source instanceof SubtitleSource)) {
+ throw new TypeError("source must be a SubtitleSource.");
+ }
+ if (!metadata || typeof metadata !== "object") {
+ throw new TypeError("metadata must be an object.");
+ }
+ this.addTrack("subtitle", source, metadata);
+ }
+ addTrack(type, source, metadata) {
+ if (this.started) {
+ throw new Error("Cannot add track after output has started.");
+ }
+ if (source.connectedTrack) {
+ throw new Error("Source is already used for a track.");
+ }
+ const track = {
+ id: this.tracks.length + 1,
+ output: this,
+ type,
+ source,
+ metadata
+ };
+ this.muxer.beforeTrackAdd(track);
+ this.tracks.push(track);
+ source.connectedTrack = track;
+ }
+ start() {
+ if (this.started) {
+ throw new Error("Output already started.");
+ }
+ this.started = true;
+ this.muxer.start();
+ for (const track of this.tracks) {
+ track.source.start();
+ }
+ }
+ async finalize() {
+ if (this.finalizing) {
+ throw new Error("Cannot call finalize twice.");
+ }
+ this.finalizing = true;
+ const promises = this.tracks.map((x) => x.source.flush());
+ await Promise.all(promises);
+ this.muxer.finalize();
+ this.writer.flush();
+ this.writer.finalize();
+ }
+ };
return __toCommonJS(src_exports);
})();
if (typeof module === "object" && typeof module.exports === "object") Object.assign(module.exports, Metamuxer)
diff --git a/dist/metamuxer.min.js b/dist/metamuxer.min.js
index 6ba997a..89da3dd 100644
--- a/dist/metamuxer.min.js
+++ b/dist/metamuxer.min.js
@@ -1,10 +1,10 @@
-"use strict";var Metamuxer=(()=>{var ve=Object.defineProperty;var rt=Object.getOwnPropertyDescriptor;var it=Object.getOwnPropertyNames;var st=Object.prototype.hasOwnProperty;var ot=(i,t)=>{for(var e in t)ve(i,e,{get:t[e],enumerable:!0})},nt=(i,t,e,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of it(t))!st.call(i,s)&&s!==e&&ve(i,s,{get:()=>t[s],enumerable:!(r=rt(t,s))||r.enumerable});return i};var at=i=>nt(ve({},"__esModule",{value:!0}),i);var mr={};ot(mr,{ArrayBufferTarget:()=>D,AudioBufferSource:()=>we,AudioDataSource:()=>ye,CanvasSource:()=>ke,EncodedAudioChunkSource:()=>Se,EncodedVideoChunkSource:()=>Ce,FileSystemWritableFileStreamTarget:()=>fe,MediaStreamAudioTrackSource:()=>Ae,MediaStreamVideoTrackSource:()=>xe,MkvOutputFormat:()=>q,Mp4OutputFormat:()=>Te,Output:()=>ee,StreamTarget:()=>F,Target:()=>U,TextSubtitleSource:()=>Oe,VideoFrameSource:()=>ge,WebMOutputFormat:()=>P});var ee=class{constructor(t){this.tracks=[];this.started=!1;this.finalizing=!1;if(t.target.output)throw new Error("Target is already used for another output.");t.target.output=this,this.writer=t.target.createWriter(),this.muxer=t.format.createMuxer(this)}addVideoTrack(t,e={}){this.addTrack("video",t,e)}addAudioTrack(t,e={}){this.addTrack("audio",t,e)}addSubtitleTrack(t,e={}){this.addTrack("subtitle",t,e)}addTrack(t,e,r){if(this.started)throw new Error("Cannot add track after output has started.");if(e.connectedTrack)throw new Error("Source is already used for a track.");let s={id:this.tracks.length+1,output:this,type:t,source:e,metadata:r};this.muxer.beforeTrackAdd(s),this.tracks.push(s),e.connectedTrack=s}start(){if(this.started)throw new Error("Output already started.");this.started=!0,this.muxer.start();for(let t of this.tracks)t.source.start()}async finalize(){if(this.finalizing)throw new Error("Cannot call finalize twice.");this.finalizing=!0;let t=this.tracks.map(e=>e.source.flush());await Promise.all(t),this.muxer.finalize(),this.writer.flush(),this.writer.finalize()}};function c(i){if(!i)throw new Error("Assertion failed.")}var _=i=>i&&i[i.length-1],V=i=>i>=0&&i<2**32,M=(i,t,e)=>{let r=0;for(let s=t;s>a;r<<=1,r|=u}return r},Re=(i,t,e,r)=>{for(let s=t;s>e-s-1<i instanceof ArrayBuffer?new Uint8Array(i):new Uint8Array(i.buffer,i.byteOffset,i.byteLength),y=new TextEncoder,te={bt709:1,bt470bg:5,smpte170m:6},re={bt709:1,smpte170m:6,"iec61966-2-1":13},ie={rgb:0,bt709:1,bt470bg:5,smpte170m:6},se=i=>!!i&&!!i.primaries&&!!i.transfer&&!!i.matrix&&i.fullRange!==void 0;var H=/(?:(.+?)\n)?((?:\d{2}:)?\d{2}:\d{2}.\d{3})\s+-->\s+((?:\d{2}:)?\d{2}:\d{2}.\d{3})/g,ut=/^WEBVTT(.|\n)*?\n{2}/,R=/<(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})>/g,oe=class{#e;#i=null;#t=!1;constructor(t){this.#e=t}parse(t){t=t.replaceAll(`\r
+"use strict";var Metamuxer=(()=>{var Ue=Object.defineProperty;var dt=Object.getOwnPropertyDescriptor;var ct=Object.getOwnPropertyNames;var ft=Object.prototype.hasOwnProperty;var mt=(r,t)=>{for(var e in t)Ue(r,e,{get:t[e],enumerable:!0})},ht=(r,t,e,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of ct(t))!ft.call(r,s)&&s!==e&&Ue(r,s,{get:()=>t[s],enumerable:!(i=dt(t,s))||i.enumerable});return r};var pt=r=>ht(Ue({},"__esModule",{value:!0}),r);var kr={};mt(kr,{ArrayBufferTarget:()=>$,AudioBufferSource:()=>Ve,AudioDataSource:()=>Oe,CanvasSource:()=>Ae,EncodedAudioChunkSource:()=>Ee,EncodedVideoChunkSource:()=>xe,FileSystemWritableFileStreamTarget:()=>fe,MediaStreamAudioTrackSource:()=>Me,MediaStreamVideoTrackSource:()=>ve,MkvOutputFormat:()=>te,Mp4OutputFormat:()=>ye,Output:()=>ze,StreamTarget:()=>j,Target:()=>E,TextSubtitleSource:()=>Ie,VideoFrameSource:()=>Se,WebMOutputFormat:()=>R});function d(r){if(!r)throw new Error("Assertion failed.")}var B=r=>r&&r[r.length-1],I=r=>r>=0&&r<2**32,z=(r,t,e)=>{let i=0;for(let s=t;s>a;i<<=1,i|=u}return i},$e=(r,t,e,i)=>{for(let s=t;s>e-s-1<r instanceof ArrayBuffer?new Uint8Array(r):new Uint8Array(r.buffer,r.byteOffset,r.byteLength),x=new TextEncoder,N={bt709:1,bt470bg:5,smpte170m:6},F={bt709:1,smpte170m:6,"iec61966-2-1":13},W={rgb:0,bt709:1,bt470bg:5,smpte170m:6},ne=r=>!!r&&!!r.primaries&&!!r.transfer&&!!r.matrix&&r.fullRange!==void 0,Pe=r=>r instanceof ArrayBuffer||typeof SharedArrayBuffer<"u"&&r instanceof SharedArrayBuffer||ArrayBuffer.isView(r)&&!(r instanceof DataView);var G=/(?:(.+?)\n)?((?:\d{2}:)?\d{2}:\d{2}.\d{3})\s+-->\s+((?:\d{2}:)?\d{2}:\d{2}.\d{3})/g,bt=/^WEBVTT(.|\n)*?\n{2}/,D=/<(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})>/g,ae=class{#e;#r=null;#i=!1;constructor(t){this.#e=t}parse(t){t=t.replaceAll(`\r
`,`
`).replaceAll("\r",`
-`),H.lastIndex=0;let e;if(!this.#i){if(!ut.test(t)){let s=new Error("WebVTT preamble incorrect.");throw this.#e.error(s),s}e=H.exec(t);let r=t.slice(0,e?.index??t.length).trimEnd();if(!r){let s=new Error("No WebVTT preamble provided.");throw this.#e.error(s),s}this.#i=r,e&&(t=t.slice(e.index),H.lastIndex=0)}for(;e=H.exec(t);){let r=t.slice(0,e.index),s=e[1],o=e.index+e[0].length,n=t.indexOf(`
+`),G.lastIndex=0;let e;if(!this.#r){if(!bt.test(t)){let s=new Error("WebVTT preamble incorrect.");throw this.#e.error(s),s}e=G.exec(t);let i=t.slice(0,e?.index??t.length).trimEnd();if(!i){let s=new Error("No WebVTT preamble provided.");throw this.#e.error(s),s}this.#r=i,e&&(t=t.slice(e.index),G.lastIndex=0)}for(;e=G.exec(t);){let i=t.slice(0,e.index),s=e[1],o=e.index+e[0].length,n=t.indexOf(`
`,o)+1,a=t.slice(o,n).trim(),u=t.indexOf(`
-`,o);u===-1&&(u=t.length);let m=ne(e[2]),f=ne(e[3])-m,C=t.slice(n,u).trim();t=t.slice(u).trimStart(),H.lastIndex=0;let v={timestamp:m/1e3,duration:f/1e3,text:C,identifier:s,settings:a,notes:r},k={};this.#t||(k.config={description:this.#i},this.#t=!0),this.#e.output(v,k)}}},lt=/(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})/,ne=i=>{let t=lt.exec(i);if(!t)throw new Error("Expected match.");return 60*60*1e3*Number(t[1]||"0")+60*1e3*Number(t[2])+1e3*Number(t[3])+Number(t[4])},ae=i=>{let t=Math.floor(i/36e5),e=Math.floor(i%(60*60*1e3)/(60*1e3)),r=Math.floor(i%(60*1e3)/1e3),s=i%1e3;return t.toString().padStart(2,"0")+":"+e.toString().padStart(2,"0")+":"+r.toString().padStart(2,"0")+"."+s.toString().padStart(3,"0")};var $=class{constructor(t){this.writer=t;this.helper=new Uint8Array(8);this.helperView=new DataView(this.helper.buffer);this.offsets=new WeakMap}writeU32(t){this.helperView.setUint32(0,t,!1),this.writer.write(this.helper.subarray(0,4))}writeU64(t){this.helperView.setUint32(0,Math.floor(t/2**32),!1),this.helperView.setUint32(4,t,!1),this.writer.write(this.helper.subarray(0,8))}writeAscii(t){for(let e=0;e[(i%256+256)%256],h=i=>(O.setUint16(0,i,!1),[p[0],p[1]]),ct=i=>(O.setInt16(0,i,!1),[p[0],p[1]]),Fe=i=>(O.setUint32(0,i,!1),[p[1],p[2],p[3]]),l=i=>(O.setUint32(0,i,!1),[p[0],p[1],p[2],p[3]]),De=i=>(O.setInt32(0,i,!1),[p[0],p[1],p[2],p[3]]),z=i=>(O.setUint32(0,Math.floor(i/2**32),!1),O.setUint32(4,i,!1),[p[0],p[1],p[2],p[3],p[4],p[5],p[6],p[7]]),Me=i=>(O.setInt16(0,2**8*i,!1),[p[0],p[1]]),A=i=>(O.setInt32(0,2**16*i,!1),[p[0],p[1],p[2],p[3]]),Ve=i=>(O.setInt32(0,2**30*i,!1),[p[0],p[1],p[2],p[3]]),x=(i,t=!1)=>{let e=Array(i.length).fill(null).map((r,s)=>i.charCodeAt(s));return t&&e.push(0),e},Ee=i=>{let t=null;for(let e of i)(!t||e.timestamp>t.timestamp)&&(t=e);return t},Ne=i=>{let t=i*(Math.PI/180),e=Math.cos(t),r=Math.sin(t);return[e,r,0,-r,e,0,0,0,1]},We=Ne(0),Qe=i=>[A(i[0]),A(i[1]),Ve(i[2]),A(i[3]),A(i[4]),Ve(i[5]),A(i[6]),A(i[7]),Ve(i[8])],b=(i,t,e)=>({type:i,contents:t&&new Uint8Array(t.flat(10)),children:e}),T=(i,t,e,r,s)=>b(i,[g(t),Fe(e),r??[]],s),He=i=>{let t=512;return i.fragmented?b("ftyp",[x("iso5"),l(t),x("iso5"),x("iso6"),x("mp41")]):b("ftyp",[x("isom"),l(t),x("isom"),i.holdsAvc?x("avc1"):[],x("mp41")])},le=i=>({type:"mdat",largeSize:i}),$e=i=>({type:"free",size:i}),K=(i,t,e=!1)=>b("moov",void 0,[dt(t,i),...i.map(r=>mt(r,t)),e?Kt(i):null]),dt=(i,t)=>{let e=w(Math.max(0,...t.filter(n=>n.samples.length>0).map(n=>{let a=Ee(n.samples);return a.timestamp+a.duration})),ue),r=Math.max(...t.map(n=>n.track.id))+1,s=!V(i)||!V(e),o=s?z:l;return T("mvhd",+s,0,[o(i),o(i),l(ue),o(e),A(1),Me(1),Array(10).fill(0),Qe(We),Array(24).fill(0),l(r)])},mt=(i,t)=>b("trak",void 0,[ft(i,t),pt(i,t)]),ft=(i,t)=>{let e=Ee(i.samples),r=w(e?e.timestamp+e.duration:0,ue),s=!V(t)||!V(r),o=s?z:l,n;if(i.type==="video"){let a=i.track.metadata.rotation;n=a===void 0||typeof a=="number"?Ne(a??0):a}else n=We;return T("tkhd",+s,3,[o(t),o(t),l(i.track.id),l(0),o(r),Array(8).fill(0),h(0),h(i.track.id),Me(i.type==="audio"?1:0),h(0),Qe(n),A(i.type==="video"?i.info.width:0),A(i.type==="video"?i.info.height:0)])},pt=(i,t)=>b("mdia",void 0,[ht(i,t),Ct(i),gt(i)]),ht=(i,t)=>{let e=Ee(i.samples),r=w(e?e.timestamp+e.duration:0,i.timescale),s=!V(t)||!V(r),o=s?z:l;return T("mdhd",+s,0,[o(t),o(t),l(i.timescale),o(r),h(21956),h(0)])},bt={video:"vide",audio:"soun",subtitle:"text"},Tt={video:"VideoHandler",audio:"SoundHandler",subtitle:"TextHandler"},Ct=i=>T("hdlr",0,0,[x("mhlr"),x(bt[i.type]),l(0),l(0),l(0),x(Tt[i.type],!0)]),gt=i=>b("minf",void 0,[yt[i.type](),wt(),vt(i)]),kt=()=>T("vmhd",0,1,[h(0),h(0),h(0),h(0)]),xt=()=>T("smhd",0,0,[h(0),h(0)]),St=()=>T("nmhd",0,0),yt={video:kt,audio:xt,subtitle:St},wt=()=>b("dinf",void 0,[At()]),At=()=>T("dref",0,0,[l(1)],[Ot()]),Ot=()=>T("url ",0,1),vt=i=>{let t=i.compositionTimeOffsetTable.length>1||i.compositionTimeOffsetTable.some(e=>e.sampleCompositionTimeOffset!==0);return b("stbl",void 0,[Vt(i),Dt(i),Nt(i),Wt(i),Qt(i),Ht(i),t?$t(i):null])},Vt=i=>{let t;return i.type==="video"?t=Mt(er[i.track.source.codec],i):i.type==="audio"?t=It(rr[i.track.source.codec],i):i.type==="subtitle"&&(t=Bt(sr[i.track.source.codec],i)),c(t),T("stsd",0,0,[l(1)],[t])},Mt=(i,t)=>b(i,[Array(6).fill(0),h(1),h(0),h(0),Array(12).fill(0),h(t.info.width),h(t.info.height),l(4718592),l(4718592),l(0),h(1),Array(32).fill(0),h(24),ct(65535)],[tr[t.track.source.codec](t),se(t.info.decoderConfig.colorSpace)?Et(t):null]),Et=i=>b("colr",[x("nclx"),h(te[i.info.decoderConfig.colorSpace.primaries]),h(re[i.info.decoderConfig.colorSpace.transfer]),h(ie[i.info.decoderConfig.colorSpace.matrix]),g((i.info.decoderConfig.colorSpace.fullRange?1:0)<<7)]),zt=i=>i.info.decoderConfig&&b("avcC",[...E(i.info.decoderConfig.description)]),Ut=i=>i.info.decoderConfig&&b("hvcC",[...E(i.info.decoderConfig.description)]),Be=i=>{if(!i.info.decoderConfig)return null;let t=i.info.decoderConfig;if(!t.colorSpace)throw new Error("'colorSpace' is required in the decoder config for VP8/VP9.");let e=t.codec.split("."),r=Number(e[1]),s=Number(e[2]),a=(Number(e[3])<<4)+(0<<1)+Number(t.colorSpace.fullRange);return T("vpcC",1,0,[g(r),g(s),g(a),g(2),g(2),g(2),h(0)])},Pt=()=>{let e=(1<<7)+1;return b("av1C",[e,0,0,0])},It=(i,t)=>b(i,[Array(6).fill(0),h(1),h(0),h(0),l(0),h(t.info.numberOfChannels),h(16),h(0),h(0),A(t.info.sampleRate)],[ir[t.track.source.codec](t)]),_t=i=>{let t=E(i.info.decoderConfig.description??new ArrayBuffer(0));return T("esds",0,0,[l(58753152),g(32+t.byteLength),h(1),g(0),l(75530368),g(18+t.byteLength),g(64),g(21),Fe(0),l(130071),l(130071),l(92307584),g(t.byteLength),...t,l(109084800),g(1),g(2)])},Rt=i=>{let t=3840,e=0,r=i.info.decoderConfig?.description;if(r){if(r.byteLength<18)throw new TypeError("Invalid decoder description provided for Opus; must be at least 18 bytes long.");let s=ArrayBuffer.isView(r)?new DataView(r.buffer,r.byteOffset,r.byteLength):new DataView(r);t=s.getUint16(10,!0),e=s.getInt16(14,!0)}return b("dOps",[g(0),g(i.info.numberOfChannels),h(t),l(i.info.sampleRate),Me(e),g(0)])},Bt=(i,t)=>b(i,[Array(6).fill(0),h(1)],[or[t.track.source.codec](t)]),Ft=i=>b("vttC",[...y.encode(i.info.config.description)]);var Dt=i=>T("stts",0,0,[l(i.timeToSampleTable.length),i.timeToSampleTable.map(t=>[l(t.sampleCount),l(t.sampleDelta)])]),Nt=i=>{if(i.samples.every(e=>e.type==="key"))return null;let t=[...i.samples.entries()].filter(([,e])=>e.type==="key");return T("stss",0,0,[l(t.length),t.map(([e])=>l(e+1))])},Wt=i=>T("stsc",0,0,[l(i.compactlyCodedChunkTable.length),i.compactlyCodedChunkTable.map(t=>[l(t.firstChunk),l(t.samplesPerChunk),l(1)])]),Qt=i=>T("stsz",0,0,[l(0),l(i.samples.length),i.samples.map(t=>l(t.size))]),Ht=i=>i.finalizedChunks.length>0&&_(i.finalizedChunks).offset>=2**32?T("co64",0,0,[l(i.finalizedChunks.length),i.finalizedChunks.map(t=>z(t.offset))]):T("stco",0,0,[l(i.finalizedChunks.length),i.finalizedChunks.map(t=>l(t.offset))]),$t=i=>T("ctts",0,0,[l(i.compositionTimeOffsetTable.length),i.compositionTimeOffsetTable.map(t=>[l(t.sampleCount),l(t.sampleCompositionTimeOffset)])]),Kt=i=>b("mvex",void 0,i.map(Gt)),Gt=i=>T("trex",0,0,[l(i.track.id),l(1),l(0),l(0),l(0)]),ze=(i,t)=>b("moof",void 0,[Xt(i),...t.map(jt)]),Xt=i=>T("mfhd",0,0,[l(i)]),Ke=i=>{let t=0,e=0,r=0,s=0,o=i.type==="delta";return e|=+o,o?t|=1:t|=2,t<<24|e<<16|r<<8|s},jt=i=>b("traf",void 0,[Lt(i),qt(i),Yt(i)]),Lt=i=>{c(i.currentChunk);let t=0;t|=8,t|=16,t|=32,t|=131072;let e=i.currentChunk.samples[1]??i.currentChunk.samples[0],r={duration:e.timescaleUnitsToNextSample,size:e.size,flags:Ke(e)};return T("tfhd",0,t,[l(i.track.id),l(r.duration),l(r.size),l(r.flags)])},qt=i=>(c(i.currentChunk),T("tfdt",1,0,[z(w(i.currentChunk.startTimestamp,i.timescale))])),Yt=i=>{c(i.currentChunk);let t=i.currentChunk.samples.map(S=>S.timescaleUnitsToNextSample),e=i.currentChunk.samples.map(S=>S.size),r=i.currentChunk.samples.map(Ke),s=i.currentChunk.samples.map(S=>w(S.timestamp-S.decodeTimestamp,i.timescale)),o=new Set(t),n=new Set(e),a=new Set(r),u=new Set(s),m=a.size===2&&r[0]!==r[1],d=o.size>1,f=n.size>1,C=!m&&a.size>1,v=u.size>1||[...u].some(S=>S!==0),k=0;return k|=1,k|=4*+m,k|=256*+d,k|=512*+f,k|=1024*+C,k|=2048*+v,T("trun",1,k,[l(i.currentChunk.samples.length),l(i.currentChunk.offset-i.currentChunk.moofOffset||0),m?l(r[0]):[],i.currentChunk.samples.map((S,I)=>[d?l(t[I]):[],f?l(e[I]):[],C?l(r[I]):[],v?De(s[I]):[]])])},Ge=i=>b("mfra",void 0,[...i.map(Zt),Jt()]),Zt=(i,t)=>T("tfra",1,0,[l(i.track.id),l(63),l(i.finalizedChunks.length),i.finalizedChunks.map(r=>[z(w(r.startTimestamp,i.timescale)),z(r.moofOffset),l(t+1),l(1),l(1)])]),Jt=()=>T("mfro",0,0,[l(0)]),Xe=()=>b("vtte"),je=(i,t,e,r,s)=>b("vttc",void 0,[s!==null?b("vsid",[De(s)]):null,e!==null?b("iden",[...y.encode(e)]):null,t!==null?b("ctim",[...y.encode(ae(t))]):null,r!==null?b("sttg",[...y.encode(r)]):null,b("payl",[...y.encode(i)])]),Le=i=>b("vtta",[...y.encode(i)]),er={avc:"avc1",hevc:"hvc1",vp8:"vp08",vp9:"vp09",av1:"av01"},tr={avc:zt,hevc:Ut,vp8:Be,vp9:Be,av1:Pt},rr={aac:"mp4a",opus:"Opus"},ir={aac:_t,opus:Rt},sr={webvtt:"wvtt"},or={webvtt:Ft};var B=class{constructor(t){this.trackTimestampInfo=new WeakMap;this.output=t}beforeTrackAdd(t){}onTrackClose(t){}validateAndNormalizeTimestamp(t,e,r){let s=e/1e6,o=this.trackTimestampInfo.get(t);if(!o){if(!r)throw new Error("First frame must be a key frame.");if(this.timestampsMustStartAtZero&&s>0)throw new Error(`Timestamps must start at zero (got ${s}s).`);o={timestampOffset:s,maxTimestamp:t.source.offsetTimestamps?0:s,lastKeyFrameTimestamp:t.source.offsetTimestamps?0:s},this.trackTimestampInfo.set(t,o)}if(t.source.offsetTimestamps&&(s-=o.timestampOffset),s<0)throw new Error(`Timestamps must be non-negative (got ${s}s).`);if(sr.start-s.start);t.push({start:e[0].start,size:e[0].data.byteLength});for(let r=1;ru.start<=e&&ear){for(let u=0;u=t.written[o+1].start;)t.written[o].end=Math.max(t.written[o].end,t.written[o+1].end),t.written.splice(o+1,1)}#c(t){let r={start:Math.floor(t/this.#t)*this.#t,data:new Uint8Array(this.#t),written:[],shouldFlush:!1};return this.#r.push(r),this.#r.sort((s,o)=>s.start-o.start),this.#r.indexOf(r)}#n(t=!1){for(let e=0;et.stream.write({type:"write",data:e,position:r}),chunkSize:t.options?.chunkSize}))}};var U=class{constructor(){this.output=null}},D=class extends U{constructor(){super(...arguments);this.buffer=null}createWriter(){return new ce(this)}},F=class extends U{constructor(e){super();this.options=e;if(typeof e!="object")throw new TypeError("StreamTarget requires an options object to be passed to its constructor.");if(e.onData){if(typeof e.onData!="function")throw new TypeError("options.onData, when provided, must be a function.");if(e.onData.length<2)throw new TypeError("options.onData, when provided, must be a function that takes in at least two arguments (data and position). Ignoring the position argument, which specifies the byte offset at which the data is to be written, can lead to broken outputs.")}if(e.chunked!==void 0&&typeof e.chunked!="boolean")throw new TypeError("options.chunked, when provided, must be a boolean.");if(e.chunkSize!==void 0&&(!Number.isInteger(e.chunkSize)||e.chunkSize<=0))throw new TypeError("options.chunkSize, when provided, must be a positive integer.")}createWriter(){return this.options.chunked?new X(this):new de(this)}},fe=class extends U{constructor(e,r){super();this.stream=e;this.options=r;if(!(e instanceof FileSystemWritableFileStream))throw new TypeError("FileSystemWritableFileStreamTarget requires a FileSystemWritableFileStream instance.");if(r!==void 0&&typeof r!="object")throw new TypeError("FileSystemWritableFileStreamTarget's options, when provided, must be an object.");if(r&&r.chunkSize!==void 0&&(!Number.isInteger(r.chunkSize)||r.chunkSize<=0))throw new TypeError("options.chunkSize, when provided, must be a positive integer")}createWriter(){return new me(this)}};var ue=1e3,ur=2082844800,w=(i,t,e=!0)=>{let r=i*t;return e?Math.round(r):r},pe=class extends B{constructor(e,r){super(e);this.timestampsMustStartAtZero=!0;this.#r=new D;this.#o=this.#r.createWriter();this.#a=new $(this.#o);this.#c=null;this.#n=null;this.#s=[];this.#d=Math.floor(Date.now()/1e3)+ur;this.#u=[];this.#l=1;this.#e=e.writer,this.#i=new $(this.#e),this.#t=r}#e;#i;#t;#r;#o;#a;#c;#n;#s;#d;#u;#l;start(){let e=this.output.tracks.some(r=>r.type==="video"&&r.source.codec==="avc");if(this.#i.writeBox(He({holdsAvc:e,fragmented:this.#t.options.fastStart==="fragmented"})),this.#c=this.#e.getPos(),this.#t.options.fastStart==="in-memory")this.#n=le(!1);else if(this.#t.options.fastStart!=="fragmented"){if(typeof this.#t.options.fastStart=="object"){let r=this.#h();this.#e.seek(this.#e.getPos()+r)}this.#n=le(!0),this.#i.writeBox(this.#n)}this.#e.flush()}#h(){c(typeof this.#t.options.fastStart=="object");let e=0,r=[this.#t.options.fastStart.expectedVideoChunks,this.#t.options.fastStart.expectedAudioChunks];for(let s of r)s&&(e+=8*Math.ceil(2/3*s),e+=4*s,e+=12*Math.ceil(2/3*s),e+=4*s,e+=8*s);return e+=4096,e}#T(e,r){let s=this.#s.find(n=>n.track===e);if(s)return s;c(r),c(r.decoderConfig),c(r.decoderConfig.codedWidth!==void 0),c(r.decoderConfig.codedHeight!==void 0);let o={track:e,type:"video",info:{width:r.decoderConfig.codedWidth,height:r.decoderConfig.codedHeight,decoderConfig:r.decoderConfig},timescale:e.metadata.frameRate??57600,samples:[],sampleQueue:[],timestampProcessingQueue:[],timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.#s.push(o),this.#s.sort((n,a)=>n.track.id-a.track.id),o}#C(e,r){let s=this.#s.find(n=>n.track===e);if(s)return s;c(r),c(r.decoderConfig);let o={track:e,type:"audio",info:{numberOfChannels:r.decoderConfig.numberOfChannels,sampleRate:r.decoderConfig.sampleRate,decoderConfig:r.decoderConfig},timescale:r.decoderConfig.sampleRate,samples:[],sampleQueue:[],timestampProcessingQueue:[],timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.#s.push(o),this.#s.sort((n,a)=>n.track.id-a.track.id),o}#w(e,r){let s=this.#s.find(n=>n.track===e);if(s)return s;c(r),c(r.config);let o={track:e,type:"subtitle",info:{config:r.config},timescale:1e3,samples:[],sampleQueue:[],timestampProcessingQueue:[],timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[],lastCueEndTimestamp:0,cueQueue:[],nextSourceId:0,cueToSourceId:new WeakMap};return this.#s.push(o),this.#s.sort((n,a)=>n.track.id-a.track.id),this.validateAndNormalizeTimestamp(e,0,!0),o}addEncodedVideoChunk(e,r,s){let o=this.#T(e,s);if(typeof this.#t.options.fastStart=="object"&&o.samples.length===this.#t.options.fastStart.expectedVideoChunks)throw new Error(`Cannot add more video chunks than specified in 'fastStart' (${this.#t.options.fastStart.expectedVideoChunks}).`);let n=new Uint8Array(r.byteLength);r.copyTo(n);let a=this.validateAndNormalizeTimestamp(o.track,r.timestamp,r.type==="key"),u=this.#b(o,n,a,(r.duration??0)/1e6,r.type);this.#t.options.fastStart==="fragmented"?(o.sampleQueue.push(u),this.#p()):this.#m(o,u)}addEncodedAudioChunk(e,r,s){let o=this.#C(e,s);if(typeof this.#t.options.fastStart=="object"&&o.samples.length===this.#t.options.fastStart.expectedAudioChunks)throw new Error(`Cannot add more audio chunks than specified in 'fastStart' (${this.#t.options.fastStart.expectedAudioChunks}).`);let n=new Uint8Array(r.byteLength);r.copyTo(n);let a=this.validateAndNormalizeTimestamp(o.track,r.timestamp,r.type==="key"),u=this.#b(o,n,a,(r.duration??0)/1e6,r.type);this.#t.options.fastStart==="fragmented"?(o.sampleQueue.push(u),this.#p()):this.#m(o,u)}addSubtitleCue(e,r,s){let o=this.#w(e,s);this.validateAndNormalizeTimestamp(o.track,1e6*r.timestamp,!0),e.source.codec==="webvtt"&&(o.cueQueue.push(r),this.#g(o,r.timestamp))}#g(e,r){for(;e.cueQueue.length>0;){let s=new Set([]);for(let d of e.cueQueue)c(d.timestamp<=r),c(e.lastCueEndTimestamp<=d.timestamp+d.duration),s.add(Math.max(d.timestamp,e.lastCueEndTimestamp)),s.add(d.timestamp+d.duration);let o=[...s].sort((d,f)=>d-f),n=o[0],a=o[1]??n;if(r=a)break;R.lastIndex=0;let C=R.test(f.text),v=f.timestamp+f.duration,k=e.cueToSourceId.get(f);if(k===void 0&&as.timestamp).sort((s,o)=>s-o);for(let s=0;s{if(e===a)return r.type==="key";let u=a.sampleQueue[0];return u&&u.type==="key"});o>=1&&n&&(s=!0,this.#S())}else s=o>=.5}s&&(e.currentChunk&&this.#x(e),e.currentChunk={startTimestamp:r.timestamp,samples:[],offset:null,moofOffset:null}),c(e.currentChunk),e.currentChunk.samples.push(r),e.timestampProcessingQueue.push(r)}#x(e){if(c(this.#t.options.fastStart!=="fragmented"),!!e.currentChunk){if(e.finalizedChunks.push(e.currentChunk),this.#u.push(e.currentChunk),(e.compactlyCodedChunkTable.length===0||_(e.compactlyCodedChunkTable).samplesPerChunk!==e.currentChunk.samples.length)&&e.compactlyCodedChunkTable.push({firstChunk:e.finalizedChunks.length,samplesPerChunk:e.currentChunk.samples.length}),this.#t.options.fastStart==="in-memory"){e.currentChunk.offset=0;return}e.currentChunk.offset=this.#e.getPos();for(let r of e.currentChunk.samples)c(r.data),this.#e.write(r.data),r.data=null;this.#e.flush()}}#p(){c(this.#t.options.fastStart==="fragmented");for(let e of this.output.tracks)if(!e.source.closed&&!this.#s.some(r=>r.track===e))return;e:for(;;){let e=null,r=1/0;for(let o of this.#s){if(o.sampleQueue.length===0&&!o.track.source.closed)break e;o.sampleQueue.length>0&&o.sampleQueue[0].timestamp=2**32&&(u.largeSize=!0,d=this.#i.measureBox(u)+m),u.size=d,this.#i.writeBox(u)}for(let u of this.#s){u.currentChunk.offset=this.#e.getPos(),u.currentChunk.moofOffset=s;for(let m of u.currentChunk.samples)this.#e.write(m.data),m.data=null}let n=this.#e.getPos();this.#e.seek(this.#i.offsets.get(o));let a=ze(r,this.#s);this.#i.writeBox(a),this.#e.seek(n);for(let u of this.#s)u.finalizedChunks.push(u.currentChunk),this.#u.push(u.currentChunk),u.currentChunk=null;e&&this.#e.flush()}onTrackClose(e){if(e.type==="subtitle"&&e.source.codec==="webvtt"){let r=this.#s.find(s=>s.track===e);r&&this.#g(r,1/0)}this.#t.options.fastStart==="fragmented"&&this.#p()}finalize(){for(let e of this.#s)e.type==="subtitle"&&e.track.source.codec==="webvtt"&&this.#g(e,1/0);if(this.#t.options.fastStart==="fragmented"){for(let e of this.#s){for(let r of e.sampleQueue)this.#m(e,r);this.#f(e)}this.#S(!1)}else for(let e of this.#s)this.#f(e),this.#x(e);if(this.#t.options.fastStart==="in-memory"){c(this.#n);let e;for(let s=0;s<2;s++){let o=K(this.#s,this.#d),n=this.#i.measureBox(o);e=this.#i.measureBox(this.#n);let a=this.#e.getPos()+n+e;for(let u of this.#u){u.offset=a;for(let{data:m}of u.samples)c(m),a+=m.byteLength,e+=m.byteLength}if(a<2**32)break;e>=2**32&&(this.#n.largeSize=!0)}let r=K(this.#s,this.#d);this.#i.writeBox(r),this.#n.size=e,this.#i.writeBox(this.#n);for(let s of this.#u)for(let o of s.samples)c(o.data),this.#e.write(o.data),o.data=null}else if(this.#t.options.fastStart==="fragmented"){let e=this.#e.getPos(),r=Ge(this.#s);this.#i.writeBox(r);let s=this.#e.getPos()-e;this.#e.seek(this.#e.getPos()-4),this.#i.writeU32(s)}else{c(this.#n),c(this.#c!==null);let e=this.#i.offsets.get(this.#n);c(e!==void 0);let r=this.#e.getPos()-e;this.#n.size=r,this.#n.largeSize=r>=2**32,this.#i.patchBox(this.#n);let s=K(this.#s,this.#d);if(typeof this.#t.options.fastStart=="object"){this.#e.seek(this.#c),this.#i.writeBox(s);let o=e-this.#e.getPos();this.#i.writeBox($e(o))}else this.#i.writeBox(s)}}};var j=class{constructor(t){this.value=t}},N=class{constructor(t){this.value=t}},L=class{constructor(t){this.value=t}};var Ue=i=>i<256?1:i<65536?2:i<1<<24?3:i<2**32?4:i<2**40?5:6,Pe=i=>i>=-64&&i<64?1:i>=-8192&&i<8192?2:i>=-(1<<20)&&i<1<<20?3:i>=-(1<<27)&&i<1<<27?4:i>=-(2**34)&&i<2**34?5:6,qe=i=>{if(i<127)return 1;if(i<16383)return 2;if(i<(1<<21)-1)return 3;if(i<(1<<28)-1)return 4;if(i<2**35-1)return 5;if(i<2**42-1)return 6;throw new Error("EBML VINT size not supported "+i)};var Ie=2**15,Ye="https://github.com/Vanilagy/webm-muxer",Ze=6,Je=5,lr={avc:"V_MPEG4/ISO/AVC",hevc:"V_MPEGH/ISO/HEVC",vp8:"V_VP8",vp9:"V_VP9",av1:"V_AV1",aac:"A_AAC",opus:"A_OPUS",webvtt:"S_TEXT/WEBVTT"},cr={video:1,audio:2,subtitle:17},he=class extends B{constructor(e,r){super(e);this.timestampsMustStartAtZero=!0;this.#t=new Uint8Array(8);this.#r=new DataView(this.#t.buffer);this.offsets=new WeakMap;this.dataOffsets=new WeakMap;this.#o=[];this.#a=null;this.#c=null;this.#n=null;this.#s=null;this.#d=null;this.#u=null;this.#l=null;this.#h=null;this.#T=new Set;this.#C=0;this.#e=e.writer,this.#i=r}#e;#i;#t;#r;#o;#a;#c;#n;#s;#d;#u;#l;#h;#T;#C;#w(e){this.#r.setUint8(0,e),this.#e.write(this.#t.subarray(0,1))}#g(e){this.#r.setFloat32(0,e,!1),this.#e.write(this.#t.subarray(0,4))}#b(e){this.#r.setFloat64(0,e,!1),this.#e.write(this.#t)}#f(e,r=Ue(e)){let s=0;switch(r){case 6:this.#r.setUint8(s++,e/2**40|0);case 5:this.#r.setUint8(s++,e/2**32|0);case 4:this.#r.setUint8(s++,e>>24);case 3:this.#r.setUint8(s++,e>>16);case 2:this.#r.setUint8(s++,e>>8);case 1:this.#r.setUint8(s++,e);break;default:throw new Error("Bad UINT size "+r)}this.#e.write(this.#t.subarray(0,s))}#m(e,r=Pe(e)){e<0&&(e+=2**(r*8)),this.#f(e,r)}writeEBMLVarInt(e,r=qe(e)){let s=0;switch(r){case 1:this.#r.setUint8(s++,128|e);break;case 2:this.#r.setUint8(s++,64|e>>8),this.#r.setUint8(s++,e);break;case 3:this.#r.setUint8(s++,32|e>>16),this.#r.setUint8(s++,e>>8),this.#r.setUint8(s++,e);break;case 4:this.#r.setUint8(s++,16|e>>24),this.#r.setUint8(s++,e>>16),this.#r.setUint8(s++,e>>8),this.#r.setUint8(s++,e);break;case 5:this.#r.setUint8(s++,8|e/2**32&7),this.#r.setUint8(s++,e>>24),this.#r.setUint8(s++,e>>16),this.#r.setUint8(s++,e>>8),this.#r.setUint8(s++,e);break;case 6:this.#r.setUint8(s++,4|e/2**40&3),this.#r.setUint8(s++,e/2**32|0),this.#r.setUint8(s++,e>>24),this.#r.setUint8(s++,e>>16),this.#r.setUint8(s++,e>>8),this.#r.setUint8(s++,e);break;default:throw new Error("Bad EBML VINT size "+r)}this.#e.write(this.#t.subarray(0,s))}#x(e){this.#e.write(new Uint8Array(e.split("").map(r=>r.charCodeAt(0))))}writeEBML(e){if(e!==null){if(e instanceof Uint8Array)this.#e.write(e);else if(Array.isArray(e))for(let r of e)this.writeEBML(r);else if(this.offsets.set(e,this.#e.getPos()),this.#f(e.id),Array.isArray(e.data)){let r=this.#e.getPos(),s=e.size===-1?1:e.size??4;e.size===-1?this.#w(255):this.#e.seek(this.#e.getPos()+s);let o=this.#e.getPos();if(this.dataOffsets.set(e,o),this.writeEBML(e.data),e.size!==-1){let n=this.#e.getPos()-o,a=this.#e.getPos();this.#e.seek(r),this.writeEBMLVarInt(n,s),this.#e.seek(a)}}else if(typeof e.data=="number"){let r=e.size??Ue(e.data);this.writeEBMLVarInt(r),this.#f(e.data,r)}else if(typeof e.data=="string")this.writeEBMLVarInt(e.data.length),this.#x(e.data);else if(e.data instanceof Uint8Array)this.writeEBMLVarInt(e.data.byteLength,e.size),this.#e.write(e.data);else if(e.data instanceof j)this.writeEBMLVarInt(4),this.#g(e.data.value);else if(e.data instanceof N)this.writeEBMLVarInt(8),this.#b(e.data.value);else if(e.data instanceof L){let r=e.size??Pe(e.data.value);this.writeEBMLVarInt(r),this.#m(e.data.value,r)}}}beforeTrackAdd(e){if(this.#i instanceof P)if(e.type==="video"){if(!["vp8","vp9","av1"].includes(e.source.codec))throw new Error("WebM only supports VP8, VP9 and AV1 as video codecs. Switching to MKV removes this restriction.")}else if(e.type==="audio"){if(!["opus","vorbis"].includes(e.source.codec))throw new Error("WebM only supports Opus and Vorbis as audio codecs. Switching to MKV removes this restriction.")}else if(e.type==="subtitle"){if(e.source.codec!=="webvtt")throw new Error("WebM only supports WebVTT as subtitle codec. Switching to MKV removes this restriction.")}else throw new Error("WebM only supports video, audio and subtitle tracks. Switching to MKV removes this restriction.")}start(){this.#p(),this.#i.options.streaming||this.#S(),this.#V(),this.#z(),this.#e.flush()}#p(){let e={id:440786851,data:[{id:17030,data:1},{id:17143,data:1},{id:17138,data:4},{id:17139,data:8},{id:17026,data:this.#i instanceof P?"webm":"matroska"},{id:17031,data:2},{id:17029,data:2}]};this.writeEBML(e)}#S(){let e=new Uint8Array([28,83,187,107]),r=new Uint8Array([21,73,169,102]),s=new Uint8Array([22,84,174,107]),o={id:290298740,data:[{id:19899,data:[{id:21419,data:e},{id:21420,size:5,data:0}]},{id:19899,data:[{id:21419,data:r},{id:21420,size:5,data:0}]},{id:19899,data:[{id:21419,data:s},{id:21420,size:5,data:0}]}]};this.#n=o}#V(){let e={id:17545,data:new N(0)};this.#d=e;let r={id:357149030,data:[{id:2807729,data:1e6},{id:19840,data:Ye},{id:22337,data:Ye},this.#i.options.streaming?null:e]};this.#c=r}#M(){let e={id:374648427,data:[]};this.#s=e;for(let r of this.#o)e.data.push({id:174,data:[{id:215,data:r.track.id},{id:29637,data:r.track.id},{id:131,data:cr[r.type]},{id:134,data:lr[r.track.source.codec]},...r.type==="video"?[r.info.decoderConfig.description?{id:25506,data:E(r.info.decoderConfig.description)}:null,r.track.metadata.frameRate?{id:2352003,data:1e9/r.track.metadata.frameRate}:null,{id:224,data:[{id:176,data:r.info.width},{id:186,data:r.info.height},(()=>{if(r.info.decoderConfig.colorSpace){let s=r.info.decoderConfig.colorSpace;return se(s)?{id:21936,data:[{id:21937,data:ie[s.matrix]},{id:21946,data:re[s.transfer]},{id:21947,data:te[s.primaries]},{id:21945,data:[1,2][Number(s.fullRange)]}]}:null}return null})()]}]:[],...r.type==="audio"?[r.info.decoderConfig.description?{id:25506,data:E(r.info.decoderConfig.description)}:null,{id:225,data:[{id:181,data:new j(r.info.sampleRate)},{id:159,data:r.info.numberOfChannels}]}]:[],...r.type==="subtitle"?[{id:25506,data:y.encode(r.info.config.description)}]:[]]})}#E(){let e={id:408125543,size:this.#i.options.streaming?-1:Ze,data:[this.#i.options.streaming?null:this.#n,this.#c,this.#s]};this.#a=e,this.writeEBML(e)}#z(){this.#u={id:475249515,data:[]}}get#k(){return c(this.#a),this.dataOffsets.get(this.#a)}#U(e,r){let s=this.#o.find(n=>n.track===e);if(s)return s;c(r),c(r.decoderConfig),c(r.decoderConfig.codedWidth!==void 0),c(r.decoderConfig.codedHeight!==void 0);let o={track:e,type:"video",info:{width:r.decoderConfig.codedWidth,height:r.decoderConfig.codedHeight,decoderConfig:r.decoderConfig},chunkQueue:[],lastWrittenMsTimestamp:null};return this.#o.push(o),this.#o.sort((n,a)=>n.track.id-a.track.id),o}#P(e,r){let s=this.#o.find(n=>n.track===e);if(s)return s;c(r),c(r.decoderConfig);let o={track:e,type:"audio",info:{numberOfChannels:r.decoderConfig.numberOfChannels,sampleRate:r.decoderConfig.sampleRate,decoderConfig:r.decoderConfig},chunkQueue:[],lastWrittenMsTimestamp:null};return this.#o.push(o),this.#o.sort((n,a)=>n.track.id-a.track.id),o}#I(e,r){let s=this.#o.find(n=>n.track===e);if(s)return s;c(r),c(r.config);let o={track:e,type:"subtitle",info:{config:r.config},chunkQueue:[],lastWrittenMsTimestamp:null};return this.#o.push(o),this.#o.sort((n,a)=>n.track.id-a.track.id),o}addEncodedVideoChunk(e,r,s){let o=this.#U(e,s),n=new Uint8Array(r.byteLength);r.copyTo(n);let a=this.validateAndNormalizeTimestamp(o.track,r.timestamp,r.type==="key"),u=this.#A(n,a,(r.duration??0)/1e6,r.type);e.source.codec==="vp9"&&this.#_(o,u),o.chunkQueue.push(u),this.#y()}addEncodedAudioChunk(e,r,s){let o=this.#P(e,s),n=new Uint8Array(r.byteLength);r.copyTo(n);let a=this.validateAndNormalizeTimestamp(o.track,r.timestamp,r.type==="key"),u=this.#A(n,a,(r.duration??0)/1e6,r.type);o.chunkQueue.push(u),this.#y()}addSubtitleCue(e,r,s){let o=this.#I(e,s),n=this.validateAndNormalizeTimestamp(o.track,1e6*r.timestamp,!0),a=r.text,u=Math.floor(n*1e3);R.lastIndex=0,a=a.replace(R,C=>{let k=ne(C.slice(1,-1))-u;return`<${ae(k)}>`});let m=y.encode(a),d=`${r.settings??""}
-${r.identifier??""}
-${r.notes??""}`,f=this.#A(m,n,r.duration,"key",d.trim()?y.encode(d):null);o.chunkQueue.push(f),this.#y()}#y(){for(let e of this.output.tracks)if(!e.source.closed&&!this.#o.some(r=>r.track===e))return;e:for(;;){let e=null,r=1/0;for(let o of this.#o){if(o.chunkQueue.length===0&&!o.track.source.closed)break e;o.chunkQueue.length>0&&o.chunkQueue[0].timestamp=2&&s++;let m={rgb:7,bt709:2,bt470bg:1,smpte170m:3}[e.info.decoderConfig.colorSpace.matrix];Re(r.data,s+0,s+3,m)}#A(e,r,s,o,n=null){return{data:e,type:o,timestamp:r,duration:s,additions:n}}#O(e,r){this.#a||(this.#M(),this.#E());let s=Math.floor(1e3*r.timestamp),o=this.#o.every(f=>{if(f.track.source.closed)return!0;if(e===f)return r.type==="key";let C=f.chunkQueue[0];return C&&C.type==="key"});(!this.#l||o&&s-this.#h>=1e3)&&this.#R(s);let n=s-this.#h;if(n<0)return;if(n>=Ie)throw new Error(`Current Matroska cluster exceeded its maximum allowed length of ${Ie} milliseconds. In order to produce a correct WebM file, you must pass in a key frame at least every ${Ie} milliseconds.`);let u=new Uint8Array(4),m=new DataView(u.buffer);m.setUint8(0,128|e.track.id),m.setInt16(1,n,!1);let d=Math.floor(1e3*r.duration);if(d===0&&!r.additions){m.setUint8(3,+(r.type==="key")<<7);let f={id:163,data:[u,r.data]};this.writeEBML(f)}else{let f={id:160,data:[{id:161,data:[u,r.data]},r.type==="delta"?{id:251,data:new L(e.lastWrittenMsTimestamp-s)}:null,r.additions?{id:30113,data:[{id:166,data:[{id:165,data:r.additions},{id:238,data:1}]}]}:null,d>0?{id:155,data:d}:null]};this.writeEBML(f)}this.#C=Math.max(this.#C,s+d),e.lastWrittenMsTimestamp=s,this.#T.add(e)}#R(e){this.#l&&!this.#i.options.streaming&&this.#v(),this.#l={id:524531317,size:this.#i.options.streaming?-1:Je,data:[{id:231,data:e}]},this.writeEBML(this.#l),this.#h=e,this.#T.clear()}#v(){c(this.#l);let e=this.#e.getPos()-this.dataOffsets.get(this.#l),r=this.#e.getPos();this.#e.seek(this.offsets.get(this.#l)+4),this.writeEBMLVarInt(e,Je),this.#e.seek(r);let s=this.offsets.get(this.#l)-this.#k;c(this.#u),this.#u.data.push({id:187,data:[{id:179,data:this.#h},...[...this.#T].map(o=>({id:183,data:[{id:247,data:o.track.id},{id:241,data:s}]}))]})}onTrackClose(){this.#y()}finalize(){for(let e of this.#o)for(;e.chunkQueue.length>0;)this.#O(e,e.chunkQueue.shift());if(this.#i.options.streaming||this.#v(),c(this.#u),this.writeEBML(this.#u),!this.#i.options.streaming){let e=this.#e.getPos(),r=this.#e.getPos()-this.#k;this.#e.seek(this.offsets.get(this.#a)+4),this.writeEBMLVarInt(r,Ze),this.#d.data=new N(this.#C),this.#e.seek(this.offsets.get(this.#d)),this.writeEBML(this.#d),this.#n.data[0].data[1].data=this.offsets.get(this.#u)-this.#k,this.#n.data[1].data[1].data=this.offsets.get(this.#c)-this.#k,this.#n.data[2].data[1].data=this.offsets.get(this.#s)-this.#k,this.#e.seek(this.offsets.get(this.#n)),this.writeEBML(this.#n),this.#e.seek(e)}}};var be=class{},Te=class extends be{constructor(e){super();this.options=e}createMuxer(e){return new pe(e,this)}},q=class extends be{constructor(e={}){super();this.options=e}createMuxer(e){return new he(e,this)}},P=class extends q{};var et=(i,t,e)=>{if(i==="avc"){let r=100;t<=768&&e<=432?r=66:t<=1920&&e<=1080&&(r=77);let s=0,o=t>1920||e>1080?50:41,n=r.toString(16).padStart(2,"0"),a=s.toString(16).padStart(2,"0"),u=o.toString(16).padStart(2,"0");return`avc1.${n}${a}${u}`}else if(i==="hevc"){let r=0,s=1,o=Array(32).fill(0);o[s]=1;let n=parseInt(o.reverse().join(""),2).toString(16).replace(/^0+/,""),a="L",u=120;return t<=1280&&e<=720?u=93:t<=1920&&e<=1080?u=120:t<=3840&&e<=2160?u=150:(a="H",u=180),`hev1.${r===0?"":String.fromCharCode(65+r-1)}${s}.${n}.${a}${u}.B0`}else{if(i==="vp8")return"vp8";if(i==="vp9"){let r="00",s;return t<=854&&e<=480?s="21":t<=1280&&e<=720?s="31":t<=1920&&e<=1080?s="41":t<=3840&&e<=2160?s="51":s="61",`vp09.${r}.${s}.08`}else if(i==="av1"){let s;return t<=854&&e<=480?s="01":t<=1280&&e<=720?s="03":t<=1920&&e<=1080?s="04":t<=3840&&e<=2160?s="07":s="09",`av01.0.${s}M.08`}}throw new Error(`Unhandled codec '${i}'.`)},tt=(i,t,e)=>{if(i==="aac")return t>=2&&e<=24e3?"mp4a.40.29":e<=24e3?"mp4a.40.5":"mp4a.40.2";if(i==="opus")return"opus";if(i==="vorbis")return"vorbis";throw new Error(`Unhandled codec '${i}'.`)};var Y=class{constructor(){this.connectedTrack=null;this.closed=!1;this.offsetTimestamps=!1}ensureValidDigest(){if(!this.connectedTrack)throw new Error("Cannot call digest without connecting the source to an output track.");if(!this.connectedTrack.output.started)throw new Error("Cannot call digest before output has been started.");if(this.connectedTrack.output.finalizing)throw new Error("Cannot call digest after output has started finalizing.");if(this.closed)throw new Error("Cannot call digest after source has been closed.")}start(){}async flush(){}close(){if(this.closed)throw new Error("Source already closed.");if(!this.connectedTrack)throw new Error("Cannot call close without connecting the source to an output track.");if(!this.connectedTrack.output.started)throw new Error("Cannot call close before output has been started.");this.closed=!0,!this.connectedTrack.output.finalizing&&this.connectedTrack.output.muxer.onTrackClose(this.connectedTrack)}},W=class extends Y{constructor(e){super();this.connectedTrack=null;this.codec=e}},Ce=class extends W{constructor(t){super(t)}digest(t,e){this.ensureValidDigest(),this.connectedTrack?.output.muxer.addEncodedVideoChunk(this.connectedTrack,t,e)}},dr=5,Z=class{constructor(t,e){this.source=t;this.codecConfig=e;this.encoder=null;this.lastMultipleOfKeyFrameInterval=-1}digest(t){this.source.ensureValidDigest(),this.ensureEncoder(t),c(this.encoder);let e=Math.floor(t.timestamp/1e6/dr);this.encoder.encode(t,{keyFrame:e!==this.lastMultipleOfKeyFrameInterval}),this.lastMultipleOfKeyFrameInterval=e}ensureEncoder(t){this.encoder||(this.encoder=new VideoEncoder({output:(e,r)=>this.source.connectedTrack?.output.muxer.addEncodedVideoChunk(this.source.connectedTrack,e,r),error:e=>console.error(e)}),this.encoder.configure({codec:et(this.codecConfig.codec,t.codedWidth,t.codedHeight),width:t.codedWidth,height:t.codedHeight,bitrate:this.codecConfig.bitrate,framerate:this.source.connectedTrack?.metadata.frameRate,latencyMode:this.codecConfig.latencyMode}))}async flush(){return this.encoder?.flush()}},ge=class extends W{constructor(t){super(t.codec),this.encoder=new Z(this,t)}digest(t){this.encoder.digest(t)}flush(){return this.encoder.flush()}},ke=class extends W{constructor(e,r){super(r.codec);this.canvas=e;this.encoder=new Z(this,r)}digest(e,r=0){let s=new VideoFrame(this.canvas,{timestamp:Math.round(1e6*e),duration:Math.round(1e6*r),alpha:"discard"});this.encoder.digest(s),s.close()}flush(){return this.encoder.flush()}},xe=class extends W{constructor(e,r){super(r.codec);this.track=e;this.abortController=null;this.offsetTimestamps=!0;this.encoder=new Z(this,r)}start(){this.abortController=new AbortController;let e=new MediaStreamTrackProcessor({track:this.track}),r=new WritableStream({write:s=>{this.encoder.digest(s),s.close()}});e.readable.pipeTo(r,{signal:this.abortController.signal}).catch(s=>{s instanceof DOMException&&s.name==="AbortError"||console.error("Pipe error:",s)})}async flush(){this.abortController&&(this.abortController.abort(),this.abortController=null),await this.encoder.flush()}},Q=class extends Y{constructor(e){super();this.connectedTrack=null;this.codec=e}},Se=class extends Q{constructor(t){super(t)}digest(t,e){this.ensureValidDigest(),this.connectedTrack?.output.muxer.addEncodedAudioChunk(this.connectedTrack,t,e)}},J=class{constructor(t,e){this.source=t;this.codecConfig=e;this.encoder=null}digest(t){this.source.ensureValidDigest(),this.ensureEncoder(t),c(this.encoder),this.encoder.encode(t)}ensureEncoder(t){this.encoder||(this.encoder=new AudioEncoder({output:(e,r)=>this.source.connectedTrack?.output.muxer.addEncodedAudioChunk(this.source.connectedTrack,e,r),error:e=>console.error(e)}),this.encoder.configure({codec:tt(this.codecConfig.codec,t.numberOfChannels,t.sampleRate),numberOfChannels:t.numberOfChannels,sampleRate:t.sampleRate,bitrate:this.codecConfig.bitrate}))}async flush(){return this.encoder?.flush()}},ye=class extends Q{constructor(t){super(t.codec),this.encoder=new J(this,t)}digest(t){this.encoder.digest(t)}flush(){return this.encoder.flush()}},we=class extends Q{constructor(e){super(e.codec);this.accumulatedFrameCount=0;this.encoder=new J(this,e)}digest(e){let r=e.numberOfChannels,s=e.sampleRate,o=e.length,n=new Float32Array(r*o);for(let u=0;u{this.encoder.digest(s),s.close()}});e.readable.pipeTo(r,{signal:this.abortController.signal}).catch(s=>{s instanceof DOMException&&s.name==="AbortError"||console.error("Pipe error:",s)})}async flush(){this.abortController&&(this.abortController.abort(),this.abortController=null),await this.encoder.flush()}},_e=class extends Y{constructor(e){super();this.connectedTrack=null;this.codec=e}},Oe=class extends _e{constructor(t){super(t),this.parser=new oe({codec:t,output:(e,r)=>this.connectedTrack?.output.muxer.addSubtitleCue(this.connectedTrack,e,r),error:e=>console.error(e)})}digest(t){this.ensureValidDigest(),this.parser.parse(t)}};return at(mr);})();
+`,o);u===-1&&(u=t.length);let f=ue(e[2]),m=ue(e[3])-f,g=t.slice(n,u).trim();t=t.slice(u).trimStart(),G.lastIndex=0;let O={timestamp:f/1e3,duration:m/1e3,text:g,identifier:s,settings:a,notes:i},y={};this.#i||(y.config={description:this.#r},this.#i=!0),this.#e.output(O,y)}}},Tt=/(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})/,ue=r=>{let t=Tt.exec(r);if(!t)throw new Error("Expected match.");return 60*60*1e3*Number(t[1]||"0")+60*1e3*Number(t[2])+1e3*Number(t[3])+Number(t[4])},le=r=>{let t=Math.floor(r/36e5),e=Math.floor(r%(60*60*1e3)/(60*1e3)),i=Math.floor(r%(60*1e3)/1e3),s=r%1e3;return t.toString().padStart(2,"0")+":"+e.toString().padStart(2,"0")+":"+i.toString().padStart(2,"0")+"."+s.toString().padStart(3,"0")};var L=class{constructor(t){this.writer=t;this.helper=new Uint8Array(8);this.helperView=new DataView(this.helper.buffer);this.offsets=new WeakMap}writeU32(t){this.helperView.setUint32(0,t,!1),this.writer.write(this.helper.subarray(0,4))}writeU64(t){this.helperView.setUint32(0,Math.floor(t/2**32),!1),this.helperView.setUint32(4,t,!1),this.writer.write(this.helper.subarray(0,8))}writeAscii(t){for(let e=0;e[(r%256+256)%256],p=r=>(v.setUint16(0,r,!1),[h[0],h[1]]),gt=r=>(v.setInt16(0,r,!1),[h[0],h[1]]),Qe=r=>(v.setUint32(0,r,!1),[h[1],h[2],h[3]]),l=r=>(v.setUint32(0,r,!1),[h[0],h[1],h[2],h[3]]),Ke=r=>(v.setInt32(0,r,!1),[h[0],h[1],h[2],h[3]]),P=r=>(v.setUint32(0,Math.floor(r/2**32),!1),v.setUint32(4,r,!1),[h[0],h[1],h[2],h[3],h[4],h[5],h[6],h[7]]),Be=r=>(v.setInt16(0,2**8*r,!1),[h[0],h[1]]),A=r=>(v.setInt32(0,2**16*r,!1),[h[0],h[1],h[2],h[3]]),Re=r=>(v.setInt32(0,2**30*r,!1),[h[0],h[1],h[2],h[3]]),_e=(r,t)=>{let e=[],i=r;do{let s=i&127;i>>=7,e.length>0&&(s|=128),e.push(s),t!==void 0&&t--}while(i>0||t);return e.reverse()},w=(r,t=!1)=>{let e=Array(r.length).fill(null).map((i,s)=>r.charCodeAt(s));return t&&e.push(0),e},Ne=r=>{let t=null;for(let e of r)(!t||e.timestamp>t.timestamp)&&(t=e);return t},Xe=r=>{let t=r*(Math.PI/180),e=Math.cos(t),i=Math.sin(t);return[e,i,0,-i,e,0,0,0,1]},Ge=Xe(0),Le=r=>[A(r[0]),A(r[1]),Re(r[2]),A(r[3]),A(r[4]),Re(r[5]),A(r[6]),A(r[7]),Re(r[8])],b=(r,t,e)=>({type:r,contents:t&&new Uint8Array(t.flat(10)),children:e}),T=(r,t,e,i,s)=>b(r,[C(t),Qe(e),i??[]],s),qe=r=>{let t=512;return r.fragmented?b("ftyp",[w("iso5"),l(t),w("iso5"),w("iso6"),w("mp41")]):b("ftyp",[w("isom"),l(t),w("isom"),r.holdsAvc?w("avc1"):[],w("mp41")])},ce=r=>({type:"mdat",largeSize:r}),Ye=r=>({type:"free",size:r}),q=(r,t,e=!1)=>b("moov",void 0,[Ct(t,r),...r.map(i=>yt(i,t)),e?Jt(r):null]),Ct=(r,t)=>{let e=S(Math.max(0,...t.filter(n=>n.samples.length>0).map(n=>{let a=Ne(n.samples);return a.timestamp+a.duration})),de),i=Math.max(0,...t.map(n=>n.track.id))+1,s=!I(r)||!I(e),o=s?P:l;return T("mvhd",+s,0,[o(r),o(r),l(de),o(e),A(1),Be(1),Array(10).fill(0),Le(Ge),Array(24).fill(0),l(i)])},yt=(r,t)=>b("trak",void 0,[wt(r,t),kt(r,t)]),wt=(r,t)=>{let e=Ne(r.samples),i=S(e?e.timestamp+e.duration:0,de),s=!I(t)||!I(i),o=s?P:l,n;if(r.type==="video"){let a=r.track.metadata.rotation;n=a===void 0||typeof a=="number"?Xe(a??0):a}else n=Ge;return T("tkhd",+s,3,[o(t),o(t),l(r.track.id),l(0),o(i),Array(8).fill(0),p(0),p(r.track.id),Be(r.type==="audio"?1:0),p(0),Le(n),A(r.type==="video"?r.info.width:0),A(r.type==="video"?r.info.height:0)])},kt=(r,t)=>b("mdia",void 0,[xt(r,t),vt(r),Et(r)]),xt=(r,t)=>{let e=Ne(r.samples),i=S(e?e.timestamp+e.duration:0,r.timescale),s=!I(t)||!I(i),o=s?P:l;return T("mdhd",+s,0,[o(t),o(t),l(r.timescale),o(i),p(21956),p(0)])},St={video:"vide",audio:"soun",subtitle:"text"},At={video:"VideoHandler",audio:"SoundHandler",subtitle:"TextHandler"},vt=r=>T("hdlr",0,0,[w("mhlr"),w(St[r.type]),l(0),l(0),l(0),w(At[r.type],!0)]),Et=r=>b("minf",void 0,[It[r.type](),zt(),Rt(r)]),Ot=()=>T("vmhd",0,1,[p(0),p(0),p(0),p(0)]),Vt=()=>T("smhd",0,0,[p(0),p(0)]),Mt=()=>T("nmhd",0,0),It={video:Ot,audio:Vt,subtitle:Mt},zt=()=>b("dinf",void 0,[Ut()]),Ut=()=>T("dref",0,0,[l(1)],[Pt()]),Pt=()=>T("url ",0,1),Rt=r=>{let t=r.compositionTimeOffsetTable.length>1||r.compositionTimeOffsetTable.some(e=>e.sampleCompositionTimeOffset!==0);return b("stbl",void 0,[_t(r),Xt(r),Gt(r),Lt(r),qt(r),Yt(r),t?Zt(r):null])},_t=r=>{let t;return r.type==="video"?t=Bt(ur[r.track.source.codec],r):r.type==="audio"?t=Ht(dr[r.track.source.codec],r):r.type==="subtitle"&&(t=Qt(fr[r.track.source.codec],r)),d(t),T("stsd",0,0,[l(1)],[t])},Bt=(r,t)=>b(r,[Array(6).fill(0),p(1),p(0),p(0),Array(12).fill(0),p(t.info.width),p(t.info.height),l(4718592),l(4718592),l(0),p(1),Array(32).fill(0),p(24),gt(65535)],[lr[t.track.source.codec](t),ne(t.info.decoderConfig.colorSpace)?Nt(t):null]),Nt=r=>b("colr",[w("nclx"),p(N[r.info.decoderConfig.colorSpace.primaries]),p(F[r.info.decoderConfig.colorSpace.transfer]),p(W[r.info.decoderConfig.colorSpace.matrix]),C((r.info.decoderConfig.colorSpace.fullRange?1:0)<<7)]),Ft=r=>r.info.decoderConfig&&b("avcC",[...U(r.info.decoderConfig.description)]),Wt=r=>r.info.decoderConfig&&b("hvcC",[...U(r.info.decoderConfig.description)]),je=r=>{if(!r.info.decoderConfig)return null;let t=r.info.decoderConfig;d(t.colorSpace);let e=t.codec.split("."),i=Number(e[1]),s=Number(e[2]),a=(Number(e[3])<<4)+(0<<1)+Number(t.colorSpace.fullRange);return T("vpcC",1,0,[C(i),C(s),C(a),C(2),C(2),C(2),p(0)])},Dt=()=>{let e=(1<<7)+1;return b("av1C",[e,0,0,0])},Ht=(r,t)=>b(r,[Array(6).fill(0),p(1),p(0),p(0),l(0),p(t.info.numberOfChannels),p(16),p(0),p(0),A(t.info.sampleRate)],[cr[t.track.source.codec](t)]),$t=r=>{let e=[...U(r.info.decoderConfig.description??new ArrayBuffer(0))];return e=[...C(64),...C(21),...Qe(0),...l(0),...l(0),...C(5),..._e(e.length),...e],e=[...p(1),...C(0),...C(4),..._e(e.length),...e,...C(6),...C(1),...C(2)],e=[...C(3),..._e(e.length),...e],T("esds",0,0,e)},jt=r=>{let t=3840,e=0,i=r.info.decoderConfig?.description;if(i){d(i.byteLength<18);let s=ArrayBuffer.isView(i)?new DataView(i.buffer,i.byteOffset,i.byteLength):new DataView(i);t=s.getUint16(10,!0),e=s.getInt16(14,!0)}return b("dOps",[C(0),C(r.info.numberOfChannels),p(t),l(r.info.sampleRate),Be(e),C(0)])},Qt=(r,t)=>b(r,[Array(6).fill(0),p(1)],[mr[t.track.source.codec](t)]),Kt=r=>b("vttC",[...x.encode(r.info.config.description)]);var Xt=r=>T("stts",0,0,[l(r.timeToSampleTable.length),r.timeToSampleTable.map(t=>[l(t.sampleCount),l(t.sampleDelta)])]),Gt=r=>{if(r.samples.every(e=>e.type==="key"))return null;let t=[...r.samples.entries()].filter(([,e])=>e.type==="key");return T("stss",0,0,[l(t.length),t.map(([e])=>l(e+1))])},Lt=r=>T("stsc",0,0,[l(r.compactlyCodedChunkTable.length),r.compactlyCodedChunkTable.map(t=>[l(t.firstChunk),l(t.samplesPerChunk),l(1)])]),qt=r=>T("stsz",0,0,[l(0),l(r.samples.length),r.samples.map(t=>l(t.size))]),Yt=r=>r.finalizedChunks.length>0&&B(r.finalizedChunks).offset>=2**32?T("co64",0,0,[l(r.finalizedChunks.length),r.finalizedChunks.map(t=>P(t.offset))]):T("stco",0,0,[l(r.finalizedChunks.length),r.finalizedChunks.map(t=>l(t.offset))]),Zt=r=>T("ctts",0,0,[l(r.compositionTimeOffsetTable.length),r.compositionTimeOffsetTable.map(t=>[l(t.sampleCount),l(t.sampleCompositionTimeOffset)])]),Jt=r=>b("mvex",void 0,r.map(er)),er=r=>T("trex",0,0,[l(r.track.id),l(1),l(0),l(0),l(0)]),Fe=(r,t)=>b("moof",void 0,[tr(r),...t.map(rr)]),tr=r=>T("mfhd",0,0,[l(r)]),Ze=r=>{let t=0,e=0,i=0,s=0,o=r.type==="delta";return e|=+o,o?t|=1:t|=2,t<<24|e<<16|i<<8|s},rr=r=>b("traf",void 0,[ir(r),sr(r),or(r)]),ir=r=>{d(r.currentChunk);let t=0;t|=8,t|=16,t|=32,t|=131072;let e=r.currentChunk.samples[1]??r.currentChunk.samples[0],i={duration:e.timescaleUnitsToNextSample,size:e.size,flags:Ze(e)};return T("tfhd",0,t,[l(r.track.id),l(i.duration),l(i.size),l(i.flags)])},sr=r=>(d(r.currentChunk),T("tfdt",1,0,[P(S(r.currentChunk.startTimestamp,r.timescale))])),or=r=>{d(r.currentChunk);let t=r.currentChunk.samples.map(k=>k.timescaleUnitsToNextSample),e=r.currentChunk.samples.map(k=>k.size),i=r.currentChunk.samples.map(Ze),s=r.currentChunk.samples.map(k=>S(k.timestamp-k.decodeTimestamp,r.timescale)),o=new Set(t),n=new Set(e),a=new Set(i),u=new Set(s),f=a.size===2&&i[0]!==i[1],c=o.size>1,m=n.size>1,g=!f&&a.size>1,O=u.size>1||[...u].some(k=>k!==0),y=0;return y|=1,y|=4*+f,y|=256*+c,y|=512*+m,y|=1024*+g,y|=2048*+O,T("trun",1,y,[l(r.currentChunk.samples.length),l(r.currentChunk.offset-r.currentChunk.moofOffset||0),f?l(i[0]):[],r.currentChunk.samples.map((k,_)=>[c?l(t[_]):[],m?l(e[_]):[],g?l(i[_]):[],O?Ke(s[_]):[]])])},Je=r=>b("mfra",void 0,[...r.map(nr),ar()]),nr=(r,t)=>T("tfra",1,0,[l(r.track.id),l(63),l(r.finalizedChunks.length),r.finalizedChunks.map(i=>[P(S(i.startTimestamp,r.timescale)),P(i.moofOffset),l(t+1),l(1),l(1)])]),ar=()=>T("mfro",0,0,[l(0)]),et=()=>b("vtte"),tt=(r,t,e,i,s)=>b("vttc",void 0,[s!==null?b("vsid",[Ke(s)]):null,e!==null?b("iden",[...x.encode(e)]):null,t!==null?b("ctim",[...x.encode(le(t))]):null,i!==null?b("sttg",[...x.encode(i)]):null,b("payl",[...x.encode(r)])]),rt=r=>b("vtta",[...x.encode(r)]),ur={avc:"avc1",hevc:"hvc1",vp8:"vp08",vp9:"vp09",av1:"av01"},lr={avc:Ft,hevc:Wt,vp8:je,vp9:je,av1:Dt},dr={aac:"mp4a",opus:"Opus"},cr={aac:$t,opus:jt},fr={webvtt:"wvtt"},mr={webvtt:Kt};var H=class{constructor(t){this.trackTimestampInfo=new WeakMap;this.output=t}beforeTrackAdd(t){}onTrackClose(t){}validateAndNormalizeTimestamp(t,e,i){let s=e/1e6,o=this.trackTimestampInfo.get(t);if(!o){if(!i)throw new Error("First frame must be a key frame.");if(this.timestampsMustStartAtZero&&s>0)throw new Error(`Timestamps must start at zero (got ${s}s).`);o={timestampOffset:s,maxTimestamp:t.source.offsetTimestamps?0:s,lastKeyFrameTimestamp:t.source.offsetTimestamps?0:s},this.trackTimestampInfo.set(t,o)}if(t.source.offsetTimestamps&&(s-=o.timestampOffset),s<0)throw new Error(`Timestamps must be non-negative (got ${s}s).`);if(si.start-s.start);t.push({start:e[0].start,size:e[0].data.byteLength});for(let i=1;iu.start<=e&&epr){for(let u=0;u=t.written[o+1].start;)t.written[o].end=Math.max(t.written[o].end,t.written[o+1].end),t.written.splice(o+1,1)}#l(t){let i={start:Math.floor(t/this.#i)*this.#i,data:new Uint8Array(this.#i),written:[],shouldFlush:!1};return this.#t.push(i),this.#t.sort((s,o)=>s.start-o.start),this.#t.indexOf(i)}#n(t=!1){for(let e=0;et.stream.write({type:"write",data:e,position:i}),chunkSize:t.options?.chunkSize}))}};var it=(r,t,e)=>{if(r==="avc"){let i=100;t<=768&&e<=432?i=66:t<=1920&&e<=1080&&(i=77);let s=0,o=t>1920||e>1080?50:41,n=i.toString(16).padStart(2,"0"),a=s.toString(16).padStart(2,"0"),u=o.toString(16).padStart(2,"0");return`avc1.${n}${a}${u}`}else if(r==="hevc"){let i=0,s=1,o=Array(32).fill(0);o[s]=1;let n=parseInt(o.reverse().join(""),2).toString(16).replace(/^0+/,""),a="L",u=120;return t<=1280&&e<=720?u=93:t<=1920&&e<=1080?u=120:t<=3840&&e<=2160?u=150:(a="H",u=180),`hev1.${i===0?"":String.fromCharCode(65+i-1)}${s}.${n}.${a}${u}.B0`}else{if(r==="vp8")return"vp8";if(r==="vp9"){let i="00",s;return t<=854&&e<=480?s="21":t<=1280&&e<=720?s="31":t<=1920&&e<=1080?s="41":t<=3840&&e<=2160?s="51":s="61",`vp09.${i}.${s}.08`}else if(r==="av1"){let s;return t<=854&&e<=480?s="01":t<=1280&&e<=720?s="03":t<=1920&&e<=1080?s="04":t<=3840&&e<=2160?s="07":s="09",`av01.0.${s}M.08`}}throw new TypeError(`Unhandled codec '${r}'.`)},st=(r,t,e)=>{if(r==="aac")return t>=2&&e<=24e3?"mp4a.40.29":e<=24e3?"mp4a.40.5":"mp4a.40.2";if(r==="opus")return"opus";if(r==="vorbis")return"vorbis";throw new TypeError(`Unhandled codec '${r}'.`)},pe=r=>{if(!r)throw new TypeError("Video chunk metadata must be provided.");if(typeof r!="object")throw new TypeError("Video chunk metadata must be an object.");if(!r.decoderConfig)throw new TypeError("Video chunk metadata must include a decoder configuration.");if(typeof r.decoderConfig!="object")throw new TypeError("Video chunk metadata decoder configuration must be an object.");if(typeof r.decoderConfig.codec!="string")throw new TypeError("Video chunk metadata decoder configuration must specify a codec string.");if(!Number.isInteger(r.decoderConfig.codedWidth)||r.decoderConfig.codedWidth<=0)throw new TypeError("Video chunk metadata decoder configuration must specify a valid codedWidth (positive integer).");if(!Number.isInteger(r.decoderConfig.codedHeight)||r.decoderConfig.codedHeight<=0)throw new TypeError("Video chunk metadata decoder configuration must specify a valid codedHeight (positive integer).");if(r.decoderConfig.description!==void 0&&!Pe(r.decoderConfig.description))throw new TypeError("Video chunk metadata decoder configuration description, when defined, must be an ArrayBuffer or an ArrayBuffer view.");if(r.decoderConfig.colorSpace!==void 0){let{colorSpace:t}=r.decoderConfig;if(typeof t!="object")throw new TypeError("Video chunk metadata decoder configuration colorSpace, when provided, must be an object.");let e=Object.keys(N);if(t.primaries!=null&&!e.includes(t.primaries))throw new TypeError(`Video chunk metadata decoder configuration colorSpace primaries, when defined, must be one of ${e.join(", ")}.`);let i=Object.keys(F);if(t.transfer!=null&&!i.includes(t.transfer))throw new TypeError(`Video chunk metadata decoder configuration colorSpace transfer, when defined, must be one of ${i.join(", ")}.`);let s=Object.keys(W);if(t.matrix!=null&&!s.includes(t.matrix))throw new TypeError(`Video chunk metadata decoder configuration colorSpace matrix, when defined, must be one of ${s.join(", ")}.`);if(t.fullRange!=null&&typeof t.fullRange!="boolean")throw new TypeError("Video chunk metadata decoder configuration colorSpace fullRange, when defined, must be a boolean.")}if((r.decoderConfig.codec.startsWith("avc1")||r.decoderConfig.codec.startsWith("avc3"))&&!r.decoderConfig.description)throw new TypeError("Video chunk metadata decoder configuration for AVC must include a description, which is expected to be an AVCDecoderConfigurationRecord as specified in ISO 14496-15.");if((r.decoderConfig.codec.startsWith("hev1")||r.decoderConfig.codec.startsWith("hvc1"))&&!r.decoderConfig.description)throw new TypeError("Video chunk metadata decoder configuration for HEVC must include a description, which is expected to be an HEVCDecoderConfigurationRecord as specified in ISO 14496-15.");if((r.decoderConfig.codec==="vp8"||r.decoderConfig.codec.startsWith("vp09"))&&r.decoderConfig.colorSpace===void 0)throw new TypeError("Video chunk metadata decoder configuration for VP8/VP9 must include a colorSpace.")},be=r=>{if(!r)throw new TypeError("Audio chunk metadata must be provided.");if(typeof r!="object")throw new TypeError("Audio chunk metadata must be an object.");if(!r.decoderConfig)throw new TypeError("Audio chunk metadata must include a decoder configuration.");if(typeof r.decoderConfig!="object")throw new TypeError("Audio chunk metadata decoder configuration must be an object.");if(typeof r.decoderConfig.codec!="string")throw new TypeError("Audio chunk metadata decoder configuration must specify a codec string.");if(!Number.isInteger(r.decoderConfig.sampleRate)||r.decoderConfig.sampleRate<=0)throw new TypeError("Audio chunk metadata decoder configuration must specify a valid sampleRate (positive integer).");if(!Number.isInteger(r.decoderConfig.numberOfChannels)||r.decoderConfig.numberOfChannels<=0)throw new TypeError("Audio chunk metadata decoder configuration must specify a valid numberOfChannels (positive integer).");if(r.decoderConfig.description!==void 0&&!Pe(r.decoderConfig.description))throw new TypeError("Audio chunk metadata decoder configuration description, when defined, must be an ArrayBuffer or an ArrayBuffer view.");if(r.decoderConfig.codec==="opus"&&r.decoderConfig.description&&r.decoderConfig.description.byteLength<18)throw new TypeError("Invalid decoder description provided for Opus; must be at least 18 bytes long.")},Te=r=>{if(!r)throw new TypeError("Subtitle metadata must be provided.");if(typeof r!="object")throw new TypeError("Subtitle metadata must be an object.");if(!r.config)throw new TypeError("Subtitle metadata must include a config object.");if(typeof r.config!="object")throw new TypeError("Subtitle metadata config must be an object.");if(typeof r.config.description!="string")throw new TypeError("Subtitle metadata config description must be a string.")};var de=1e3,br=2082844800,S=(r,t,e=!0)=>{let i=r*t;return e?Math.round(i):i},ge=class extends H{constructor(e,i){super(e);this.timestampsMustStartAtZero=!0;this.#o=new $;this.#a=this.#o.createWriter();this.#c=new L(this.#a);this.#l=null;this.#n=null;this.#s=[];this.#d=Math.floor(Date.now()/1e3)+br;this.#u=[];this.#m=1;this.#e=e.writer,this.#r=new L(this.#e),this.#i=i,this.#t=i.options.fastStart??(this.#e instanceof Q?"in-memory":!1),(this.#t==="in-memory"||this.#t==="fragmented")&&(this.#e.ensureMonotonicity=!0)}#e;#r;#i;#t;#o;#a;#c;#l;#n;#s;#d;#u;#m;start(){let e=this.output.tracks.some(i=>i.type==="video"&&i.source.codec==="avc");this.#r.writeBox(qe({holdsAvc:e,fragmented:this.#t==="fragmented"})),this.#l=this.#e.getPos(),this.#t==="in-memory"?this.#n=ce(!1):this.#t==="fragmented"||(this.#n=ce(!0),this.#r.writeBox(this.#n)),this.#e.flush()}#b(e,i){let s=this.#s.find(n=>n.track===e);if(s)return s;pe(i),d(i),d(i.decoderConfig),d(i.decoderConfig.codedWidth!==void 0),d(i.decoderConfig.codedHeight!==void 0);let o={track:e,type:"video",info:{width:i.decoderConfig.codedWidth,height:i.decoderConfig.codedHeight,decoderConfig:i.decoderConfig},timescale:e.metadata.frameRate??57600,samples:[],sampleQueue:[],timestampProcessingQueue:[],timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.#s.push(o),this.#s.sort((n,a)=>n.track.id-a.track.id),o}#T(e,i){let s=this.#s.find(n=>n.track===e);if(s)return s;be(i),d(i),d(i.decoderConfig);let o={track:e,type:"audio",info:{numberOfChannels:i.decoderConfig.numberOfChannels,sampleRate:i.decoderConfig.sampleRate,decoderConfig:i.decoderConfig},timescale:i.decoderConfig.sampleRate,samples:[],sampleQueue:[],timestampProcessingQueue:[],timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.#s.push(o),this.#s.sort((n,a)=>n.track.id-a.track.id),o}#A(e,i){let s=this.#s.find(n=>n.track===e);if(s)return s;Te(i),d(i),d(i.config);let o={track:e,type:"subtitle",info:{config:i.config},timescale:1e3,samples:[],sampleQueue:[],timestampProcessingQueue:[],timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[],lastCueEndTimestamp:0,cueQueue:[],nextSourceId:0,cueToSourceId:new WeakMap};return this.#s.push(o),this.#s.sort((n,a)=>n.track.id-a.track.id),this.validateAndNormalizeTimestamp(e,0,!0),o}addEncodedVideoChunk(e,i,s){let o=this.#b(e,s),n=new Uint8Array(i.byteLength);i.copyTo(n);let a=this.validateAndNormalizeTimestamp(o.track,i.timestamp,i.type==="key"),u=this.#h(o,n,a,(i.duration??0)/1e6,i.type);this.#p(o,u)}addEncodedAudioChunk(e,i,s){let o=this.#T(e,s),n=new Uint8Array(i.byteLength);i.copyTo(n);let a=this.validateAndNormalizeTimestamp(o.track,i.timestamp,i.type==="key"),u=this.#h(o,n,a,(i.duration??0)/1e6,i.type);this.#p(o,u)}addSubtitleCue(e,i,s){let o=this.#A(e,s);this.validateAndNormalizeTimestamp(o.track,1e6*i.timestamp,!0),e.source.codec==="webvtt"&&(o.cueQueue.push(i),this.#g(o,i.timestamp))}#g(e,i){for(;e.cueQueue.length>0;){let s=new Set([]);for(let c of e.cueQueue)d(c.timestamp<=i),d(e.lastCueEndTimestamp<=c.timestamp+c.duration),s.add(Math.max(c.timestamp,e.lastCueEndTimestamp)),s.add(c.timestamp+c.duration);let o=[...s].sort((c,m)=>c-m),n=o[0],a=o[1]??n;if(i=a)break;D.lastIndex=0;let g=D.test(m.text),O=m.timestamp+m.duration,y=e.cueToSourceId.get(m);if(y===void 0&&as.timestamp).sort((s,o)=>s-o);for(let s=0;s{if(e===a)return i.type==="key";let u=a.sampleQueue[0];return u&&u.type==="key"});o>=1&&n&&(s=!0,this.#x())}else s=o>=.5}s&&(e.currentChunk&&this.#w(e),e.currentChunk={startTimestamp:i.timestamp,samples:[],offset:null,moofOffset:null}),d(e.currentChunk),e.currentChunk.samples.push(i),e.timestampProcessingQueue.push(i)}#w(e){if(d(this.#t!=="fragmented"),!!e.currentChunk){if(e.finalizedChunks.push(e.currentChunk),this.#u.push(e.currentChunk),(e.compactlyCodedChunkTable.length===0||B(e.compactlyCodedChunkTable).samplesPerChunk!==e.currentChunk.samples.length)&&e.compactlyCodedChunkTable.push({firstChunk:e.finalizedChunks.length,samplesPerChunk:e.currentChunk.samples.length}),this.#t==="in-memory"){e.currentChunk.offset=0;return}e.currentChunk.offset=this.#e.getPos();for(let i of e.currentChunk.samples)d(i.data),this.#e.write(i.data),i.data=null;this.#e.flush()}}#k(){d(this.#t==="fragmented");for(let e of this.output.tracks)if(!e.source.closed&&!this.#s.some(i=>i.track===e))return;e:for(;;){let e=null,i=1/0;for(let o of this.#s){if(o.sampleQueue.length===0&&!o.track.source.closed)break e;o.sampleQueue.length>0&&o.sampleQueue[0].timestamp=2**32&&(u.largeSize=!0,c=this.#r.measureBox(u)+f),u.size=c,this.#r.writeBox(u)}for(let u of this.#s){u.currentChunk.offset=this.#e.getPos(),u.currentChunk.moofOffset=s;for(let f of u.currentChunk.samples)this.#e.write(f.data),f.data=null}let n=this.#e.getPos();this.#e.seek(this.#r.offsets.get(o));let a=Fe(i,this.#s);this.#r.writeBox(a),this.#e.seek(n);for(let u of this.#s)u.finalizedChunks.push(u.currentChunk),this.#u.push(u.currentChunk),u.currentChunk=null;e&&this.#e.flush()}onTrackClose(e){if(e.type==="subtitle"&&e.source.codec==="webvtt"){let i=this.#s.find(s=>s.track===e);i&&this.#g(i,1/0)}this.#t==="fragmented"&&this.#k()}finalize(){for(let e of this.#s)e.type==="subtitle"&&e.track.source.codec==="webvtt"&&this.#g(e,1/0);if(this.#t==="fragmented"){for(let e of this.#s){for(let i of e.sampleQueue)this.#C(e,i);this.#f(e)}this.#x(!1)}else for(let e of this.#s)this.#f(e),this.#w(e);if(this.#t==="in-memory"){d(this.#n);let e;for(let s=0;s<2;s++){let o=q(this.#s,this.#d),n=this.#r.measureBox(o);e=this.#r.measureBox(this.#n);let a=this.#e.getPos()+n+e;for(let u of this.#u){u.offset=a;for(let{data:f}of u.samples)d(f),a+=f.byteLength,e+=f.byteLength}if(a<2**32)break;e>=2**32&&(this.#n.largeSize=!0)}let i=q(this.#s,this.#d);this.#r.writeBox(i),this.#n.size=e,this.#r.writeBox(this.#n);for(let s of this.#u)for(let o of s.samples)d(o.data),this.#e.write(o.data),o.data=null}else if(this.#t==="fragmented"){let e=this.#e.getPos(),i=Je(this.#s);this.#r.writeBox(i);let s=this.#e.getPos()-e;this.#e.seek(this.#e.getPos()-4),this.#r.writeU32(s)}else{d(this.#n),d(this.#l!==null);let e=this.#r.offsets.get(this.#n);d(e!==void 0);let i=this.#e.getPos()-e;this.#n.size=i,this.#n.largeSize=i>=2**32,this.#r.patchBox(this.#n);let s=q(this.#s,this.#d);if(typeof this.#t=="object"){this.#e.seek(this.#l),this.#r.writeBox(s);let o=e-this.#e.getPos();this.#r.writeBox(Ye(o))}else this.#r.writeBox(s)}}};var J=class{constructor(t){this.value=t}},K=class{constructor(t){this.value=t}},ee=class{constructor(t){this.value=t}};var We=r=>r<256?1:r<65536?2:r<1<<24?3:r<2**32?4:r<2**40?5:6,De=r=>r>=-64&&r<64?1:r>=-8192&&r<8192?2:r>=-(1<<20)&&r<1<<20?3:r>=-(1<<27)&&r<1<<27?4:r>=-(2**34)&&r<2**34?5:6,ot=r=>{if(r<127)return 1;if(r<16383)return 2;if(r<(1<<21)-1)return 3;if(r<(1<<28)-1)return 4;if(r<2**35-1)return 5;if(r<2**42-1)return 6;throw new Error("EBML VINT size not supported "+r)};var He=2**15,nt="https://github.com/Vanilagy/webm-muxer",at=6,ut=5,Tr={avc:"V_MPEG4/ISO/AVC",hevc:"V_MPEGH/ISO/HEVC",vp8:"V_VP8",vp9:"V_VP9",av1:"V_AV1",aac:"A_AAC",opus:"A_OPUS",webvtt:"S_TEXT/WEBVTT"},gr={video:1,audio:2,subtitle:17},Ce=class extends H{constructor(e,i){super(e);this.timestampsMustStartAtZero=!1;this.#i=new Uint8Array(8);this.#t=new DataView(this.#i.buffer);this.offsets=new WeakMap;this.dataOffsets=new WeakMap;this.#o=[];this.#a=null;this.#c=null;this.#l=null;this.#n=null;this.#s=null;this.#d=null;this.#u=null;this.#m=null;this.#b=new Set;this.#T=0;this.#e=e.writer,this.#r=i,this.#r.options.streamable&&(this.#e.ensureMonotonicity=!0)}#e;#r;#i;#t;#o;#a;#c;#l;#n;#s;#d;#u;#m;#b;#T;#A(e){this.#t.setUint8(0,e),this.#e.write(this.#i.subarray(0,1))}#g(e){this.#t.setFloat32(0,e,!1),this.#e.write(this.#i.subarray(0,4))}#h(e){this.#t.setFloat64(0,e,!1),this.#e.write(this.#i)}#f(e,i=We(e)){let s=0;switch(i){case 6:this.#t.setUint8(s++,e/2**40|0);case 5:this.#t.setUint8(s++,e/2**32|0);case 4:this.#t.setUint8(s++,e>>24);case 3:this.#t.setUint8(s++,e>>16);case 2:this.#t.setUint8(s++,e>>8);case 1:this.#t.setUint8(s++,e);break;default:throw new Error("Bad UINT size "+i)}this.#e.write(this.#i.subarray(0,s))}#p(e,i=De(e)){e<0&&(e+=2**(i*8)),this.#f(e,i)}writeEBMLVarInt(e,i=ot(e)){let s=0;switch(i){case 1:this.#t.setUint8(s++,128|e);break;case 2:this.#t.setUint8(s++,64|e>>8),this.#t.setUint8(s++,e);break;case 3:this.#t.setUint8(s++,32|e>>16),this.#t.setUint8(s++,e>>8),this.#t.setUint8(s++,e);break;case 4:this.#t.setUint8(s++,16|e>>24),this.#t.setUint8(s++,e>>16),this.#t.setUint8(s++,e>>8),this.#t.setUint8(s++,e);break;case 5:this.#t.setUint8(s++,8|e/2**32&7),this.#t.setUint8(s++,e>>24),this.#t.setUint8(s++,e>>16),this.#t.setUint8(s++,e>>8),this.#t.setUint8(s++,e);break;case 6:this.#t.setUint8(s++,4|e/2**40&3),this.#t.setUint8(s++,e/2**32|0),this.#t.setUint8(s++,e>>24),this.#t.setUint8(s++,e>>16),this.#t.setUint8(s++,e>>8),this.#t.setUint8(s++,e);break;default:throw new Error("Bad EBML VINT size "+i)}this.#e.write(this.#i.subarray(0,s))}#C(e){this.#e.write(new Uint8Array(e.split("").map(i=>i.charCodeAt(0))))}writeEBML(e){if(e!==null){if(e instanceof Uint8Array)this.#e.write(e);else if(Array.isArray(e))for(let i of e)this.writeEBML(i);else if(this.offsets.set(e,this.#e.getPos()),this.#f(e.id),Array.isArray(e.data)){let i=this.#e.getPos(),s=e.size===-1?1:e.size??4;e.size===-1?this.#A(255):this.#e.seek(this.#e.getPos()+s);let o=this.#e.getPos();if(this.dataOffsets.set(e,o),this.writeEBML(e.data),e.size!==-1){let n=this.#e.getPos()-o,a=this.#e.getPos();this.#e.seek(i),this.writeEBMLVarInt(n,s),this.#e.seek(a)}}else if(typeof e.data=="number"){let i=e.size??We(e.data);this.writeEBMLVarInt(i),this.#f(e.data,i)}else if(typeof e.data=="string")this.writeEBMLVarInt(e.data.length),this.#C(e.data);else if(e.data instanceof Uint8Array)this.writeEBMLVarInt(e.data.byteLength,e.size),this.#e.write(e.data);else if(e.data instanceof J)this.writeEBMLVarInt(4),this.#g(e.data.value);else if(e.data instanceof K)this.writeEBMLVarInt(8),this.#h(e.data.value);else if(e.data instanceof ee){let i=e.size??De(e.data.value);this.writeEBMLVarInt(i),this.#p(e.data.value,i)}}}beforeTrackAdd(e){if(this.#r instanceof R)if(e.type==="video"){if(!["vp8","vp9","av1"].includes(e.source.codec))throw new Error("WebM only supports VP8, VP9 and AV1 as video codecs. Switching to MKV removes this restriction.")}else if(e.type==="audio"){if(!["opus","vorbis"].includes(e.source.codec))throw new Error("WebM only supports Opus and Vorbis as audio codecs. Switching to MKV removes this restriction.")}else if(e.type==="subtitle"){if(e.source.codec!=="webvtt")throw new Error("WebM only supports WebVTT as subtitle codec. Switching to MKV removes this restriction.")}else throw new Error("WebM only supports video, audio and subtitle tracks. Switching to MKV removes this restriction.")}start(){this.#w(),this.#r.options.streamable||this.#k(),this.#x(),this.#I(),this.#e.flush()}#w(){let e={id:440786851,data:[{id:17030,data:1},{id:17143,data:1},{id:17138,data:4},{id:17139,data:8},{id:17026,data:this.#r instanceof R?"webm":"matroska"},{id:17031,data:2},{id:17029,data:2}]};this.writeEBML(e)}#k(){let e=new Uint8Array([28,83,187,107]),i=new Uint8Array([21,73,169,102]),s=new Uint8Array([22,84,174,107]),o={id:290298740,data:[{id:19899,data:[{id:21419,data:e},{id:21420,size:5,data:0}]},{id:19899,data:[{id:21419,data:i},{id:21420,size:5,data:0}]},{id:19899,data:[{id:21419,data:s},{id:21420,size:5,data:0}]}]};this.#l=o}#x(){let e={id:17545,data:new K(0)};this.#s=e;let i={id:357149030,data:[{id:2807729,data:1e6},{id:19840,data:nt},{id:22337,data:nt},this.#r.options.streamable?null:e]};this.#c=i}#E(){let e={id:374648427,data:[]};this.#n=e;for(let i of this.#o)e.data.push({id:174,data:[{id:215,data:i.track.id},{id:29637,data:i.track.id},{id:131,data:gr[i.type]},{id:134,data:Tr[i.track.source.codec]},...i.type==="video"?[i.info.decoderConfig.description?{id:25506,data:U(i.info.decoderConfig.description)}:null,i.track.metadata.frameRate?{id:2352003,data:1e9/i.track.metadata.frameRate}:null,{id:224,data:[{id:176,data:i.info.width},{id:186,data:i.info.height},(()=>{if(i.info.decoderConfig.colorSpace){let s=i.info.decoderConfig.colorSpace;return ne(s)?{id:21936,data:[{id:21937,data:W[s.matrix]},{id:21946,data:F[s.transfer]},{id:21947,data:N[s.primaries]},{id:21945,data:[1,2][Number(s.fullRange)]}]}:null}return null})()]}]:[],...i.type==="audio"?[i.info.decoderConfig.description?{id:25506,data:U(i.info.decoderConfig.description)}:null,{id:225,data:[{id:181,data:new J(i.info.sampleRate)},{id:159,data:i.info.numberOfChannels}]}]:[],...i.type==="subtitle"?[{id:25506,data:x.encode(i.info.config.description)}]:[]]})}#O(){let e={id:408125543,size:this.#r.options.streamable?-1:at,data:[this.#r.options.streamable?null:this.#l,this.#c,this.#n]};this.#a=e,this.writeEBML(e)}#I(){this.#d={id:475249515,data:[]}}get#y(){return d(this.#a),this.dataOffsets.get(this.#a)}#z(e,i){let s=this.#o.find(n=>n.track===e);if(s)return s;pe(i),d(i),d(i.decoderConfig),d(i.decoderConfig.codedWidth!==void 0),d(i.decoderConfig.codedHeight!==void 0);let o={track:e,type:"video",info:{width:i.decoderConfig.codedWidth,height:i.decoderConfig.codedHeight,decoderConfig:i.decoderConfig},chunkQueue:[],lastWrittenMsTimestamp:null};return this.#o.push(o),this.#o.sort((n,a)=>n.track.id-a.track.id),o}#U(e,i){let s=this.#o.find(n=>n.track===e);if(s)return s;be(i),d(i),d(i.decoderConfig);let o={track:e,type:"audio",info:{numberOfChannels:i.decoderConfig.numberOfChannels,sampleRate:i.decoderConfig.sampleRate,decoderConfig:i.decoderConfig},chunkQueue:[],lastWrittenMsTimestamp:null};return this.#o.push(o),this.#o.sort((n,a)=>n.track.id-a.track.id),o}#P(e,i){let s=this.#o.find(n=>n.track===e);if(s)return s;Te(i),d(i),d(i.config);let o={track:e,type:"subtitle",info:{config:i.config},chunkQueue:[],lastWrittenMsTimestamp:null};return this.#o.push(o),this.#o.sort((n,a)=>n.track.id-a.track.id),o}addEncodedVideoChunk(e,i,s){let o=this.#z(e,s),n=new Uint8Array(i.byteLength);i.copyTo(n);let a=this.validateAndNormalizeTimestamp(o.track,i.timestamp,i.type==="key"),u=this.#v(n,a,(i.duration??0)/1e6,i.type);e.source.codec==="vp9"&&this.#R(o,u),o.chunkQueue.push(u),this.#S()}addEncodedAudioChunk(e,i,s){let o=this.#U(e,s),n=new Uint8Array(i.byteLength);i.copyTo(n);let a=this.validateAndNormalizeTimestamp(o.track,i.timestamp,i.type==="key"),u=this.#v(n,a,(i.duration??0)/1e6,i.type);o.chunkQueue.push(u),this.#S()}addSubtitleCue(e,i,s){let o=this.#P(e,s),n=this.validateAndNormalizeTimestamp(o.track,1e6*i.timestamp,!0),a=i.text,u=Math.floor(n*1e3);D.lastIndex=0,a=a.replace(D,g=>{let y=ue(g.slice(1,-1))-u;return`<${le(y)}>`});let f=x.encode(a),c=`${i.settings??""}
+${i.identifier??""}
+${i.notes??""}`,m=this.#v(f,n,i.duration,"key",c.trim()?x.encode(c):null);o.chunkQueue.push(m),this.#S()}#S(){for(let e of this.output.tracks)if(!e.source.closed&&!this.#o.some(i=>i.track===e))return;e:for(;;){let e=null,i=1/0;for(let o of this.#o){if(o.chunkQueue.length===0&&!o.track.source.closed)break e;o.chunkQueue.length>0&&o.chunkQueue[0].timestamp=2&&s++;let f={rgb:7,bt709:2,bt470bg:1,smpte170m:3}[e.info.decoderConfig.colorSpace.matrix];$e(i.data,s+0,s+3,f)}#v(e,i,s,o,n=null){return{data:e,type:o,timestamp:i,duration:s,additions:n}}#V(e,i){this.#a||(this.#E(),this.#O());let s=Math.floor(1e3*i.timestamp),o=this.#o.every(m=>{if(m.track.source.closed)return!0;if(e===m)return i.type==="key";let g=m.chunkQueue[0];return g&&g.type==="key"});(!this.#u||o&&s-this.#m>=1e3)&&this.#_(s);let n=s-this.#m;if(n<0)return;if(n>=He)throw new Error(`Current Matroska cluster exceeded its maximum allowed length of ${He} milliseconds. In order to produce a correct WebM file, you must pass in a key frame at least every ${He} milliseconds.`);let u=new Uint8Array(4),f=new DataView(u.buffer);f.setUint8(0,128|e.track.id),f.setInt16(1,n,!1);let c=Math.floor(1e3*i.duration);if(c===0&&!i.additions){f.setUint8(3,+(i.type==="key")<<7);let m={id:163,data:[u,i.data]};this.writeEBML(m)}else{let m={id:160,data:[{id:161,data:[u,i.data]},i.type==="delta"?{id:251,data:new ee(e.lastWrittenMsTimestamp-s)}:null,i.additions?{id:30113,data:[{id:166,data:[{id:165,data:i.additions},{id:238,data:1}]}]}:null,c>0?{id:155,data:c}:null]};this.writeEBML(m)}this.#T=Math.max(this.#T,s+c),e.lastWrittenMsTimestamp=s,this.#b.add(e)}#_(e){this.#u&&!this.#r.options.streamable&&this.#M(),this.#u={id:524531317,size:this.#r.options.streamable?-1:ut,data:[{id:231,data:e}]},this.writeEBML(this.#u),this.#m=e,this.#b.clear()}#M(){d(this.#u);let e=this.#e.getPos()-this.dataOffsets.get(this.#u),i=this.#e.getPos();this.#e.seek(this.offsets.get(this.#u)+4),this.writeEBMLVarInt(e,ut),this.#e.seek(i);let s=this.offsets.get(this.#u)-this.#y;d(this.#d),this.#d.data.push({id:187,data:[{id:179,data:this.#m},...[...this.#b].map(o=>({id:183,data:[{id:247,data:o.track.id},{id:241,data:s}]}))]})}onTrackClose(){this.#S()}finalize(){this.#a||(this.#E(),this.#O());for(let e of this.#o)for(;e.chunkQueue.length>0;)this.#V(e,e.chunkQueue.shift());if(!this.#r.options.streamable&&this.#u&&this.#M(),d(this.#d),this.writeEBML(this.#d),!this.#r.options.streamable){let e=this.#e.getPos(),i=this.#e.getPos()-this.#y;this.#e.seek(this.offsets.get(this.#a)+4),this.writeEBMLVarInt(i,at),this.#s.data=new K(this.#T),this.#e.seek(this.offsets.get(this.#s)),this.writeEBML(this.#s),this.#l.data[0].data[1].data=this.offsets.get(this.#d)-this.#y,this.#l.data[1].data[1].data=this.offsets.get(this.#c)-this.#y,this.#l.data[2].data[1].data=this.offsets.get(this.#n)-this.#y,this.#e.seek(this.offsets.get(this.#l)),this.writeEBML(this.#l),this.#e.seek(e)}}};var X=class{},ye=class extends X{constructor(e={}){if(!e||typeof e!="object")throw new TypeError("options must be an object.");if(e.fastStart!==void 0&&![!1,"in-memory","fragmented"].includes(e.fastStart))throw new TypeError('options.fastStart, when provided, must be false, "in-memory", or "fragmented".');super();this.options=e}createMuxer(e){return new ge(e,this)}},te=class extends X{constructor(e={}){if(!e||typeof e!="object")throw new TypeError("options must be an object.");if(e.streamable!==void 0&&typeof e.streamable!="boolean")throw new TypeError("options.streamable, when provided, must be a boolean.");super();this.options=e}createMuxer(e){return new Ce(e,this)}},R=class extends te{};var we=["avc","hevc","vp8","vp9","av1"],ke=["aac","opus"],lt=["webvtt"],re=class{constructor(){this.connectedTrack=null;this.closed=!1;this.offsetTimestamps=!1}ensureValidDigest(){if(!this.connectedTrack)throw new Error("Cannot call digest without connecting the source to an output track.");if(!this.connectedTrack.output.started)throw new Error("Cannot call digest before output has been started.");if(this.connectedTrack.output.finalizing)throw new Error("Cannot call digest after output has started finalizing.");if(this.closed)throw new Error("Cannot call digest after source has been closed.")}start(){}async flush(){}close(){if(this.closed)throw new Error("Source already closed.");if(!this.connectedTrack)throw new Error("Cannot call close without connecting the source to an output track.");if(!this.connectedTrack.output.started)throw new Error("Cannot call close before output has been started.");this.closed=!0,!this.connectedTrack.output.finalizing&&this.connectedTrack.output.muxer.onTrackClose(this.connectedTrack)}},V=class extends re{constructor(e){super();this.connectedTrack=null;if(!we.includes(e))throw new TypeError(`Invalid video codec '${e}'. Must be one of: ${we.join(", ")}.`);this.codec=e}},xe=class extends V{constructor(t){super(t)}digest(t,e){if(!(t instanceof EncodedVideoChunk))throw new TypeError("chunk must be an EncodedVideoChunk.");this.ensureValidDigest(),this.connectedTrack?.output.muxer.addEncodedVideoChunk(this.connectedTrack,t,e)}},Cr=5,yr=r=>{if(!r||typeof r!="object")throw new TypeError("Codec config must be an object.");if(!we.includes(r.codec))throw new TypeError(`Invalid video codec '${r.codec}'. Must be one of: ${we.join(", ")}.`);if(!Number.isInteger(r.bitrate)||r.bitrate<=0)throw new TypeError("config.bitrate must be a positive integer.");if(r.latencyMode!==void 0&&["quality","realtime"].includes(r.latencyMode))throw new TypeError("config.latencyMode, when provided, must be 'quality' or 'realtime'.")},ie=class{constructor(t,e){this.source=t;this.codecConfig=e;this.encoder=null;this.lastMultipleOfKeyFrameInterval=-1;this.lastWidth=null;this.lastHeight=null;yr(e)}digest(t){if(this.source.ensureValidDigest(),this.lastWidth!==null&&this.lastHeight!==null){if(t.codedWidth!==this.lastWidth||t.codedHeight!==this.lastHeight)throw new Error(`Video frame size must remain constant. Expected ${this.lastWidth}x${this.lastHeight}, got ${t.codedWidth}x${t.codedHeight}.`)}else this.lastWidth=t.codedWidth,this.lastHeight=t.codedHeight;this.ensureEncoder(t),d(this.encoder);let e=Math.floor(t.timestamp/1e6/Cr);this.encoder.encode(t,{keyFrame:e!==this.lastMultipleOfKeyFrameInterval}),this.lastMultipleOfKeyFrameInterval=e}ensureEncoder(t){this.encoder||(this.encoder=new VideoEncoder({output:(e,i)=>this.source.connectedTrack?.output.muxer.addEncodedVideoChunk(this.source.connectedTrack,e,i),error:e=>console.error("Video encode error:",e)}),this.encoder.configure({codec:it(this.codecConfig.codec,t.codedWidth,t.codedHeight),width:t.codedWidth,height:t.codedHeight,bitrate:this.codecConfig.bitrate,framerate:this.source.connectedTrack?.metadata.frameRate,latencyMode:this.codecConfig.latencyMode}))}async flush(){return this.encoder?.flush()}},Se=class extends V{constructor(t){super(t.codec),this.encoder=new ie(this,t)}digest(t){if(!(t instanceof VideoFrame))throw new TypeError("videoFrame must be a VideoFrame.");this.encoder.digest(t)}flush(){return this.encoder.flush()}},Ae=class extends V{constructor(e,i){if(!(e instanceof HTMLCanvasElement))throw new TypeError("canvas must be an HTMLCanvasElement.");super(i.codec);this.canvas=e;this.encoder=new ie(this,i)}digest(e,i=0){if(!Number.isFinite(e)||e<0)throw new TypeError("timestamp must be a non-negative number.");if(!Number.isFinite(i)||i<0)throw new TypeError("duration must be a non-negative number.");let s=new VideoFrame(this.canvas,{timestamp:Math.round(1e6*e),duration:Math.round(1e6*i),alpha:"discard"});this.encoder.digest(s),s.close()}flush(){return this.encoder.flush()}},ve=class extends V{constructor(e,i){if(!(e instanceof MediaStreamTrack)||e.kind!=="video")throw new TypeError("track must be a video MediaStreamTrack.");super(i.codec);this.track=e;this.abortController=null;this.offsetTimestamps=!0;this.encoder=new ie(this,i)}start(){this.abortController=new AbortController;let e=new MediaStreamTrackProcessor({track:this.track}),i=new WritableStream({write:s=>{this.encoder.digest(s),s.close()}});e.readable.pipeTo(i,{signal:this.abortController.signal}).catch(s=>{s instanceof DOMException&&s.name==="AbortError"||console.error("Pipe error:",s)})}async flush(){this.abortController&&(this.abortController.abort(),this.abortController=null),await this.encoder.flush()}},M=class extends re{constructor(e){super();this.connectedTrack=null;if(!ke.includes(e))throw new TypeError(`Invalid audio codec '${e}'. Must be one of: ${ke.join(", ")}.`);this.codec=e}},Ee=class extends M{constructor(t){super(t)}digest(t,e){if(!(t instanceof EncodedAudioChunk))throw new TypeError("chunk must be an EncodedAudioChunk.");this.ensureValidDigest(),this.connectedTrack?.output.muxer.addEncodedAudioChunk(this.connectedTrack,t,e)}},wr=r=>{if(!r||typeof r!="object")throw new TypeError("Codec config must be an object.");if(!ke.includes(r.codec))throw new TypeError(`Invalid audio codec '${r.codec}'. Must be one of: ${ke.join(", ")}.`);if(!Number.isInteger(r.bitrate)||r.bitrate<=0)throw new TypeError("config.bitrate must be a positive integer.")},se=class{constructor(t,e){this.source=t;this.codecConfig=e;this.encoder=null;this.lastNumberOfChannels=null;this.lastSampleRate=null;wr(e)}digest(t){if(this.source.ensureValidDigest(),this.lastNumberOfChannels!==null&&this.lastSampleRate!==null){if(t.numberOfChannels!==this.lastNumberOfChannels||t.sampleRate!==this.lastSampleRate)throw new Error(`Audio parameters must remain constant. Expected ${this.lastNumberOfChannels} channels at ${this.lastSampleRate} Hz, got ${t.numberOfChannels} channels at ${t.sampleRate} Hz.`)}else this.lastNumberOfChannels=t.numberOfChannels,this.lastSampleRate=t.sampleRate;this.ensureEncoder(t),d(this.encoder),this.encoder.encode(t)}ensureEncoder(t){this.encoder||(this.encoder=new AudioEncoder({output:(e,i)=>this.source.connectedTrack?.output.muxer.addEncodedAudioChunk(this.source.connectedTrack,e,i),error:e=>console.error("Audio encode error:",e)}),this.encoder.configure({codec:st(this.codecConfig.codec,t.numberOfChannels,t.sampleRate),numberOfChannels:t.numberOfChannels,sampleRate:t.sampleRate,bitrate:this.codecConfig.bitrate}))}async flush(){return this.encoder?.flush()}},Oe=class extends M{constructor(t){super(t.codec),this.encoder=new se(this,t)}digest(t){if(!(t instanceof AudioData))throw new TypeError("audioData must be an AudioData.");this.encoder.digest(t)}flush(){return this.encoder.flush()}},Ve=class extends M{constructor(e){super(e.codec);this.accumulatedFrameCount=0;this.encoder=new se(this,e)}digest(e){if(!(e instanceof AudioBuffer))throw new TypeError("audioBuffer must be an AudioBuffer.");let i=e.numberOfChannels,s=e.sampleRate,o=e.length,n=new Float32Array(i*o);for(let u=0;u{this.encoder.digest(s),s.close()}});e.readable.pipeTo(i,{signal:this.abortController.signal}).catch(s=>{s instanceof DOMException&&s.name==="AbortError"||console.error("Pipe error:",s)})}async flush(){this.abortController&&(this.abortController.abort(),this.abortController=null),await this.encoder.flush()}},oe=class extends re{constructor(e){super();this.connectedTrack=null;if(!lt.includes(e))throw new TypeError(`Invalid subtitle codec '${e}'. Must be one of: ${lt.join(", ")}.`);this.codec=e}},Ie=class extends oe{constructor(t){super(t),this.parser=new ae({codec:t,output:(e,i)=>this.connectedTrack?.output.muxer.addSubtitleCue(this.connectedTrack,e,i),error:e=>console.error("Subtitle parse error:",e)})}digest(t){if(typeof t!="string")throw new TypeError("text must be a string.");this.ensureValidDigest(),this.parser.parse(t)}};var ze=class{constructor(t){this.tracks=[];this.started=!1;this.finalizing=!1;if(!t||typeof t!="object")throw new TypeError("options must be an object.");if(!(t.format instanceof X))throw new TypeError("options.format must be an OutputFormat.");if(!(t.target instanceof E))throw new TypeError("options.target must be a Target.");if(t.target.output)throw new Error("Target is already used for another output.");t.target.output=this,this.writer=t.target.createWriter(),this.muxer=t.format.createMuxer(this)}addVideoTrack(t,e={}){if(!(t instanceof V))throw new TypeError("source must be a VideoSource.");if(!e||typeof e!="object")throw new TypeError("metadata must be an object.");if(typeof e.rotation=="number"&&![0,90,180,270].includes(e.rotation))throw new TypeError(`Invalid video rotation: ${e.rotation}. Has to be 0, 90, 180 or 270.`);if(Array.isArray(e.rotation)&&(e.rotation.length!==9||e.rotation.some(i=>!Number.isFinite(i))))throw new TypeError(`Invalid video transformation matrix: ${e.rotation.join()}`);if(e.frameRate!==void 0&&(!Number.isInteger(e.frameRate)||e.frameRate<=0))throw new TypeError(`Invalid video frame rate: ${e.frameRate}. Must be a positive integer.`);this.addTrack("video",t,e)}addAudioTrack(t,e={}){if(!(t instanceof M))throw new TypeError("source must be an AudioSource.");if(!e||typeof e!="object")throw new TypeError("metadata must be an object.");this.addTrack("audio",t,e)}addSubtitleTrack(t,e={}){if(!(t instanceof oe))throw new TypeError("source must be a SubtitleSource.");if(!e||typeof e!="object")throw new TypeError("metadata must be an object.");this.addTrack("subtitle",t,e)}addTrack(t,e,i){if(this.started)throw new Error("Cannot add track after output has started.");if(e.connectedTrack)throw new Error("Source is already used for a track.");let s={id:this.tracks.length+1,output:this,type:t,source:e,metadata:i};this.muxer.beforeTrackAdd(s),this.tracks.push(s),e.connectedTrack=s}start(){if(this.started)throw new Error("Output already started.");this.started=!0,this.muxer.start();for(let t of this.tracks)t.source.start()}async finalize(){if(this.finalizing)throw new Error("Cannot call finalize twice.");this.finalizing=!0;let t=this.tracks.map(e=>e.source.flush());await Promise.all(t),this.muxer.finalize(),this.writer.flush(),this.writer.finalize()}};return pt(kr);})();
if (typeof module === "object" && typeof module.exports === "object") Object.assign(module.exports, Metamuxer)
diff --git a/dist/metamuxer.min.mjs b/dist/metamuxer.min.mjs
index 86c973e..185fcd8 100644
--- a/dist/metamuxer.min.mjs
+++ b/dist/metamuxer.min.mjs
@@ -1,9 +1,9 @@
-var he=class{constructor(r){this.tracks=[];this.started=!1;this.finalizing=!1;if(r.target.output)throw new Error("Target is already used for another output.");r.target.output=this,this.writer=r.target.createWriter(),this.muxer=r.format.createMuxer(this)}addVideoTrack(r,e={}){this.addTrack("video",r,e)}addAudioTrack(r,e={}){this.addTrack("audio",r,e)}addSubtitleTrack(r,e={}){this.addTrack("subtitle",r,e)}addTrack(r,e,t){if(this.started)throw new Error("Cannot add track after output has started.");if(e.connectedTrack)throw new Error("Source is already used for a track.");let s={id:this.tracks.length+1,output:this,type:r,source:e,metadata:t};this.muxer.beforeTrackAdd(s),this.tracks.push(s),e.connectedTrack=s}start(){if(this.started)throw new Error("Output already started.");this.started=!0,this.muxer.start();for(let r of this.tracks)r.source.start()}async finalize(){if(this.finalizing)throw new Error("Cannot call finalize twice.");this.finalizing=!0;let r=this.tracks.map(e=>e.source.flush());await Promise.all(r),this.muxer.finalize(),this.writer.flush(),this.writer.finalize()}};function c(i){if(!i)throw new Error("Assertion failed.")}var P=i=>i&&i[i.length-1],V=i=>i>=0&&i<2**32,M=(i,r,e)=>{let t=0;for(let s=r;s>a;t<<=1,t|=u}return t},_e=(i,r,e,t)=>{for(let s=r;s>e-s-1<i instanceof ArrayBuffer?new Uint8Array(i):new Uint8Array(i.buffer,i.byteOffset,i.byteLength),y=new TextEncoder,J={bt709:1,bt470bg:5,smpte170m:6},ee={bt709:1,smpte170m:6,"iec61966-2-1":13},te={rgb:0,bt709:1,bt470bg:5,smpte170m:6},re=i=>!!i&&!!i.primaries&&!!i.transfer&&!!i.matrix&&i.fullRange!==void 0;var W=/(?:(.+?)\n)?((?:\d{2}:)?\d{2}:\d{2}.\d{3})\s+-->\s+((?:\d{2}:)?\d{2}:\d{2}.\d{3})/g,tt=/^WEBVTT(.|\n)*?\n{2}/,I=/<(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})>/g,ie=class{#e;#i=null;#t=!1;constructor(r){this.#e=r}parse(r){r=r.replaceAll(`\r
+function d(i){if(!i)throw new Error("Assertion failed.")}var _=i=>i&&i[i.length-1],I=i=>i>=0&&i<2**32,z=(i,t,e)=>{let r=0;for(let s=t;s>a;r<<=1,r|=u}return r},He=(i,t,e,r)=>{for(let s=t;s>e-s-1<i instanceof ArrayBuffer?new Uint8Array(i):new Uint8Array(i.buffer,i.byteOffset,i.byteLength),x=new TextEncoder,B={bt709:1,bt470bg:5,smpte170m:6},N={bt709:1,smpte170m:6,"iec61966-2-1":13},F={rgb:0,bt709:1,bt470bg:5,smpte170m:6},oe=i=>!!i&&!!i.primaries&&!!i.transfer&&!!i.matrix&&i.fullRange!==void 0,we=i=>i instanceof ArrayBuffer||typeof SharedArrayBuffer<"u"&&i instanceof SharedArrayBuffer||ArrayBuffer.isView(i)&&!(i instanceof DataView);var K=/(?:(.+?)\n)?((?:\d{2}:)?\d{2}:\d{2}.\d{3})\s+-->\s+((?:\d{2}:)?\d{2}:\d{2}.\d{3})/g,lt=/^WEBVTT(.|\n)*?\n{2}/,W=/<(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})>/g,ne=class{#e;#r=null;#i=!1;constructor(t){this.#e=t}parse(t){t=t.replaceAll(`\r
`,`
`).replaceAll("\r",`
-`),W.lastIndex=0;let e;if(!this.#i){if(!tt.test(r)){let s=new Error("WebVTT preamble incorrect.");throw this.#e.error(s),s}e=W.exec(r);let t=r.slice(0,e?.index??r.length).trimEnd();if(!t){let s=new Error("No WebVTT preamble provided.");throw this.#e.error(s),s}this.#i=t,e&&(r=r.slice(e.index),W.lastIndex=0)}for(;e=W.exec(r);){let t=r.slice(0,e.index),s=e[1],o=e.index+e[0].length,n=r.indexOf(`
-`,o)+1,a=r.slice(o,n).trim(),u=r.indexOf(`
+`),K.lastIndex=0;let e;if(!this.#r){if(!lt.test(t)){let s=new Error("WebVTT preamble incorrect.");throw this.#e.error(s),s}e=K.exec(t);let r=t.slice(0,e?.index??t.length).trimEnd();if(!r){let s=new Error("No WebVTT preamble provided.");throw this.#e.error(s),s}this.#r=r,e&&(t=t.slice(e.index),K.lastIndex=0)}for(;e=K.exec(t);){let r=t.slice(0,e.index),s=e[1],o=e.index+e[0].length,n=t.indexOf(`
+`,o)+1,a=t.slice(o,n).trim(),u=t.indexOf(`
-`,o);u===-1&&(u=r.length);let m=se(e[2]),f=se(e[3])-m,C=r.slice(n,u).trim();r=r.slice(u).trimStart(),W.lastIndex=0;let v={timestamp:m/1e3,duration:f/1e3,text:C,identifier:s,settings:a,notes:t},k={};this.#t||(k.config={description:this.#i},this.#t=!0),this.#e.output(v,k)}}},rt=/(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})/,se=i=>{let r=rt.exec(i);if(!r)throw new Error("Expected match.");return 60*60*1e3*Number(r[1]||"0")+60*1e3*Number(r[2])+1e3*Number(r[3])+Number(r[4])},oe=i=>{let r=Math.floor(i/36e5),e=Math.floor(i%(60*60*1e3)/(60*1e3)),t=Math.floor(i%(60*1e3)/1e3),s=i%1e3;return r.toString().padStart(2,"0")+":"+e.toString().padStart(2,"0")+":"+t.toString().padStart(2,"0")+"."+s.toString().padStart(3,"0")};var Q=class{constructor(r){this.writer=r;this.helper=new Uint8Array(8);this.helperView=new DataView(this.helper.buffer);this.offsets=new WeakMap}writeU32(r){this.helperView.setUint32(0,r,!1),this.writer.write(this.helper.subarray(0,4))}writeU64(r){this.helperView.setUint32(0,Math.floor(r/2**32),!1),this.helperView.setUint32(4,r,!1),this.writer.write(this.helper.subarray(0,8))}writeAscii(r){for(let e=0;e[(i%256+256)%256],h=i=>(O.setUint16(0,i,!1),[p[0],p[1]]),it=i=>(O.setInt16(0,i,!1),[p[0],p[1]]),Be=i=>(O.setUint32(0,i,!1),[p[1],p[2],p[3]]),l=i=>(O.setUint32(0,i,!1),[p[0],p[1],p[2],p[3]]),Fe=i=>(O.setInt32(0,i,!1),[p[0],p[1],p[2],p[3]]),z=i=>(O.setUint32(0,Math.floor(i/2**32),!1),O.setUint32(4,i,!1),[p[0],p[1],p[2],p[3],p[4],p[5],p[6],p[7]]),Te=i=>(O.setInt16(0,2**8*i,!1),[p[0],p[1]]),A=i=>(O.setInt32(0,2**16*i,!1),[p[0],p[1],p[2],p[3]]),be=i=>(O.setInt32(0,2**30*i,!1),[p[0],p[1],p[2],p[3]]),x=(i,r=!1)=>{let e=Array(i.length).fill(null).map((t,s)=>i.charCodeAt(s));return r&&e.push(0),e},Ce=i=>{let r=null;for(let e of i)(!r||e.timestamp>r.timestamp)&&(r=e);return r},De=i=>{let r=i*(Math.PI/180),e=Math.cos(r),t=Math.sin(r);return[e,t,0,-t,e,0,0,0,1]},Ne=De(0),We=i=>[A(i[0]),A(i[1]),be(i[2]),A(i[3]),A(i[4]),be(i[5]),A(i[6]),A(i[7]),be(i[8])],b=(i,r,e)=>({type:i,contents:r&&new Uint8Array(r.flat(10)),children:e}),T=(i,r,e,t,s)=>b(i,[g(r),Be(e),t??[]],s),Qe=i=>{let r=512;return i.fragmented?b("ftyp",[x("iso5"),l(r),x("iso5"),x("iso6"),x("mp41")]):b("ftyp",[x("isom"),l(r),x("isom"),i.holdsAvc?x("avc1"):[],x("mp41")])},ae=i=>({type:"mdat",largeSize:i}),He=i=>({type:"free",size:i}),H=(i,r,e=!1)=>b("moov",void 0,[st(r,i),...i.map(t=>ot(t,r)),e?Ft(i):null]),st=(i,r)=>{let e=w(Math.max(0,...r.filter(n=>n.samples.length>0).map(n=>{let a=Ce(n.samples);return a.timestamp+a.duration})),ne),t=Math.max(...r.map(n=>n.track.id))+1,s=!V(i)||!V(e),o=s?z:l;return T("mvhd",+s,0,[o(i),o(i),l(ne),o(e),A(1),Te(1),Array(10).fill(0),We(Ne),Array(24).fill(0),l(t)])},ot=(i,r)=>b("trak",void 0,[nt(i,r),at(i,r)]),nt=(i,r)=>{let e=Ce(i.samples),t=w(e?e.timestamp+e.duration:0,ne),s=!V(r)||!V(t),o=s?z:l,n;if(i.type==="video"){let a=i.track.metadata.rotation;n=a===void 0||typeof a=="number"?De(a??0):a}else n=Ne;return T("tkhd",+s,3,[o(r),o(r),l(i.track.id),l(0),o(t),Array(8).fill(0),h(0),h(i.track.id),Te(i.type==="audio"?1:0),h(0),We(n),A(i.type==="video"?i.info.width:0),A(i.type==="video"?i.info.height:0)])},at=(i,r)=>b("mdia",void 0,[ut(i,r),dt(i),mt(i)]),ut=(i,r)=>{let e=Ce(i.samples),t=w(e?e.timestamp+e.duration:0,i.timescale),s=!V(r)||!V(t),o=s?z:l;return T("mdhd",+s,0,[o(r),o(r),l(i.timescale),o(t),h(21956),h(0)])},lt={video:"vide",audio:"soun",subtitle:"text"},ct={video:"VideoHandler",audio:"SoundHandler",subtitle:"TextHandler"},dt=i=>T("hdlr",0,0,[x("mhlr"),x(lt[i.type]),l(0),l(0),l(0),x(ct[i.type],!0)]),mt=i=>b("minf",void 0,[bt[i.type](),Tt(),kt(i)]),ft=()=>T("vmhd",0,1,[h(0),h(0),h(0),h(0)]),pt=()=>T("smhd",0,0,[h(0),h(0)]),ht=()=>T("nmhd",0,0),bt={video:ft,audio:pt,subtitle:ht},Tt=()=>b("dinf",void 0,[Ct()]),Ct=()=>T("dref",0,0,[l(1)],[gt()]),gt=()=>T("url ",0,1),kt=i=>{let r=i.compositionTimeOffsetTable.length>1||i.compositionTimeOffsetTable.some(e=>e.sampleCompositionTimeOffset!==0);return b("stbl",void 0,[xt(i),Ut(i),Pt(i),It(i),_t(i),Rt(i),r?Bt(i):null])},xt=i=>{let r;return i.type==="video"?r=St(Xt[i.track.source.codec],i):i.type==="audio"?r=vt(Lt[i.track.source.codec],i):i.type==="subtitle"&&(r=Et(Yt[i.track.source.codec],i)),c(r),T("stsd",0,0,[l(1)],[r])},St=(i,r)=>b(i,[Array(6).fill(0),h(1),h(0),h(0),Array(12).fill(0),h(r.info.width),h(r.info.height),l(4718592),l(4718592),l(0),h(1),Array(32).fill(0),h(24),it(65535)],[jt[r.track.source.codec](r),re(r.info.decoderConfig.colorSpace)?yt(r):null]),yt=i=>b("colr",[x("nclx"),h(J[i.info.decoderConfig.colorSpace.primaries]),h(ee[i.info.decoderConfig.colorSpace.transfer]),h(te[i.info.decoderConfig.colorSpace.matrix]),g((i.info.decoderConfig.colorSpace.fullRange?1:0)<<7)]),wt=i=>i.info.decoderConfig&&b("avcC",[...E(i.info.decoderConfig.description)]),At=i=>i.info.decoderConfig&&b("hvcC",[...E(i.info.decoderConfig.description)]),Re=i=>{if(!i.info.decoderConfig)return null;let r=i.info.decoderConfig;if(!r.colorSpace)throw new Error("'colorSpace' is required in the decoder config for VP8/VP9.");let e=r.codec.split("."),t=Number(e[1]),s=Number(e[2]),a=(Number(e[3])<<4)+(0<<1)+Number(r.colorSpace.fullRange);return T("vpcC",1,0,[g(t),g(s),g(a),g(2),g(2),g(2),h(0)])},Ot=()=>{let e=(1<<7)+1;return b("av1C",[e,0,0,0])},vt=(i,r)=>b(i,[Array(6).fill(0),h(1),h(0),h(0),l(0),h(r.info.numberOfChannels),h(16),h(0),h(0),A(r.info.sampleRate)],[qt[r.track.source.codec](r)]),Vt=i=>{let r=E(i.info.decoderConfig.description??new ArrayBuffer(0));return T("esds",0,0,[l(58753152),g(32+r.byteLength),h(1),g(0),l(75530368),g(18+r.byteLength),g(64),g(21),Be(0),l(130071),l(130071),l(92307584),g(r.byteLength),...r,l(109084800),g(1),g(2)])},Mt=i=>{let r=3840,e=0,t=i.info.decoderConfig?.description;if(t){if(t.byteLength<18)throw new TypeError("Invalid decoder description provided for Opus; must be at least 18 bytes long.");let s=ArrayBuffer.isView(t)?new DataView(t.buffer,t.byteOffset,t.byteLength):new DataView(t);r=s.getUint16(10,!0),e=s.getInt16(14,!0)}return b("dOps",[g(0),g(i.info.numberOfChannels),h(r),l(i.info.sampleRate),Te(e),g(0)])},Et=(i,r)=>b(i,[Array(6).fill(0),h(1)],[Zt[r.track.source.codec](r)]),zt=i=>b("vttC",[...y.encode(i.info.config.description)]);var Ut=i=>T("stts",0,0,[l(i.timeToSampleTable.length),i.timeToSampleTable.map(r=>[l(r.sampleCount),l(r.sampleDelta)])]),Pt=i=>{if(i.samples.every(e=>e.type==="key"))return null;let r=[...i.samples.entries()].filter(([,e])=>e.type==="key");return T("stss",0,0,[l(r.length),r.map(([e])=>l(e+1))])},It=i=>T("stsc",0,0,[l(i.compactlyCodedChunkTable.length),i.compactlyCodedChunkTable.map(r=>[l(r.firstChunk),l(r.samplesPerChunk),l(1)])]),_t=i=>T("stsz",0,0,[l(0),l(i.samples.length),i.samples.map(r=>l(r.size))]),Rt=i=>i.finalizedChunks.length>0&&P(i.finalizedChunks).offset>=2**32?T("co64",0,0,[l(i.finalizedChunks.length),i.finalizedChunks.map(r=>z(r.offset))]):T("stco",0,0,[l(i.finalizedChunks.length),i.finalizedChunks.map(r=>l(r.offset))]),Bt=i=>T("ctts",0,0,[l(i.compositionTimeOffsetTable.length),i.compositionTimeOffsetTable.map(r=>[l(r.sampleCount),l(r.sampleCompositionTimeOffset)])]),Ft=i=>b("mvex",void 0,i.map(Dt)),Dt=i=>T("trex",0,0,[l(i.track.id),l(1),l(0),l(0),l(0)]),ge=(i,r)=>b("moof",void 0,[Nt(i),...r.map(Wt)]),Nt=i=>T("mfhd",0,0,[l(i)]),$e=i=>{let r=0,e=0,t=0,s=0,o=i.type==="delta";return e|=+o,o?r|=1:r|=2,r<<24|e<<16|t<<8|s},Wt=i=>b("traf",void 0,[Qt(i),Ht(i),$t(i)]),Qt=i=>{c(i.currentChunk);let r=0;r|=8,r|=16,r|=32,r|=131072;let e=i.currentChunk.samples[1]??i.currentChunk.samples[0],t={duration:e.timescaleUnitsToNextSample,size:e.size,flags:$e(e)};return T("tfhd",0,r,[l(i.track.id),l(t.duration),l(t.size),l(t.flags)])},Ht=i=>(c(i.currentChunk),T("tfdt",1,0,[z(w(i.currentChunk.startTimestamp,i.timescale))])),$t=i=>{c(i.currentChunk);let r=i.currentChunk.samples.map(S=>S.timescaleUnitsToNextSample),e=i.currentChunk.samples.map(S=>S.size),t=i.currentChunk.samples.map($e),s=i.currentChunk.samples.map(S=>w(S.timestamp-S.decodeTimestamp,i.timescale)),o=new Set(r),n=new Set(e),a=new Set(t),u=new Set(s),m=a.size===2&&t[0]!==t[1],d=o.size>1,f=n.size>1,C=!m&&a.size>1,v=u.size>1||[...u].some(S=>S!==0),k=0;return k|=1,k|=4*+m,k|=256*+d,k|=512*+f,k|=1024*+C,k|=2048*+v,T("trun",1,k,[l(i.currentChunk.samples.length),l(i.currentChunk.offset-i.currentChunk.moofOffset||0),m?l(t[0]):[],i.currentChunk.samples.map((S,U)=>[d?l(r[U]):[],f?l(e[U]):[],C?l(t[U]):[],v?Fe(s[U]):[]])])},Ke=i=>b("mfra",void 0,[...i.map(Kt),Gt()]),Kt=(i,r)=>T("tfra",1,0,[l(i.track.id),l(63),l(i.finalizedChunks.length),i.finalizedChunks.map(t=>[z(w(t.startTimestamp,i.timescale)),z(t.moofOffset),l(r+1),l(1),l(1)])]),Gt=()=>T("mfro",0,0,[l(0)]),Ge=()=>b("vtte"),Xe=(i,r,e,t,s)=>b("vttc",void 0,[s!==null?b("vsid",[Fe(s)]):null,e!==null?b("iden",[...y.encode(e)]):null,r!==null?b("ctim",[...y.encode(oe(r))]):null,t!==null?b("sttg",[...y.encode(t)]):null,b("payl",[...y.encode(i)])]),je=i=>b("vtta",[...y.encode(i)]),Xt={avc:"avc1",hevc:"hvc1",vp8:"vp08",vp9:"vp09",av1:"av01"},jt={avc:wt,hevc:At,vp8:Re,vp9:Re,av1:Ot},Lt={aac:"mp4a",opus:"Opus"},qt={aac:Vt,opus:Mt},Yt={webvtt:"wvtt"},Zt={webvtt:zt};var _=class{constructor(r){this.trackTimestampInfo=new WeakMap;this.output=r}beforeTrackAdd(r){}onTrackClose(r){}validateAndNormalizeTimestamp(r,e,t){let s=e/1e6,o=this.trackTimestampInfo.get(r);if(!o){if(!t)throw new Error("First frame must be a key frame.");if(this.timestampsMustStartAtZero&&s>0)throw new Error(`Timestamps must start at zero (got ${s}s).`);o={timestampOffset:s,maxTimestamp:r.source.offsetTimestamps?0:s,lastKeyFrameTimestamp:r.source.offsetTimestamps?0:s},this.trackTimestampInfo.set(r,o)}if(r.source.offsetTimestamps&&(s-=o.timestampOffset),s<0)throw new Error(`Timestamps must be non-negative (got ${s}s).`);if(st.start-s.start);r.push({start:e[0].start,size:e[0].data.byteLength});for(let t=1;tu.start<=e&&eer){for(let u=0;u=r.written[o+1].start;)r.written[o].end=Math.max(r.written[o].end,r.written[o+1].end),r.written.splice(o+1,1)}#c(r){let t={start:Math.floor(r/this.#t)*this.#t,data:new Uint8Array(this.#t),written:[],shouldFlush:!1};return this.#r.push(t),this.#r.sort((s,o)=>s.start-o.start),this.#r.indexOf(t)}#n(r=!1){for(let e=0;er.stream.write({type:"write",data:e,position:t}),chunkSize:r.options?.chunkSize}))}};var R=class{constructor(){this.output=null}},X=class extends R{constructor(){super(...arguments);this.buffer=null}createWriter(){return new ue(this)}},G=class extends R{constructor(e){super();this.options=e;if(typeof e!="object")throw new TypeError("StreamTarget requires an options object to be passed to its constructor.");if(e.onData){if(typeof e.onData!="function")throw new TypeError("options.onData, when provided, must be a function.");if(e.onData.length<2)throw new TypeError("options.onData, when provided, must be a function that takes in at least two arguments (data and position). Ignoring the position argument, which specifies the byte offset at which the data is to be written, can lead to broken outputs.")}if(e.chunked!==void 0&&typeof e.chunked!="boolean")throw new TypeError("options.chunked, when provided, must be a boolean.");if(e.chunkSize!==void 0&&(!Number.isInteger(e.chunkSize)||e.chunkSize<=0))throw new TypeError("options.chunkSize, when provided, must be a positive integer.")}createWriter(){return this.options.chunked?new K(this):new le(this)}},ke=class extends R{constructor(e,t){super();this.stream=e;this.options=t;if(!(e instanceof FileSystemWritableFileStream))throw new TypeError("FileSystemWritableFileStreamTarget requires a FileSystemWritableFileStream instance.");if(t!==void 0&&typeof t!="object")throw new TypeError("FileSystemWritableFileStreamTarget's options, when provided, must be an object.");if(t&&t.chunkSize!==void 0&&(!Number.isInteger(t.chunkSize)||t.chunkSize<=0))throw new TypeError("options.chunkSize, when provided, must be a positive integer")}createWriter(){return new ce(this)}};var ne=1e3,tr=2082844800,w=(i,r,e=!0)=>{let t=i*r;return e?Math.round(t):t},de=class extends _{constructor(e,t){super(e);this.timestampsMustStartAtZero=!0;this.#r=new X;this.#o=this.#r.createWriter();this.#a=new Q(this.#o);this.#c=null;this.#n=null;this.#s=[];this.#d=Math.floor(Date.now()/1e3)+tr;this.#u=[];this.#l=1;this.#e=e.writer,this.#i=new Q(this.#e),this.#t=t}#e;#i;#t;#r;#o;#a;#c;#n;#s;#d;#u;#l;start(){let e=this.output.tracks.some(t=>t.type==="video"&&t.source.codec==="avc");if(this.#i.writeBox(Qe({holdsAvc:e,fragmented:this.#t.options.fastStart==="fragmented"})),this.#c=this.#e.getPos(),this.#t.options.fastStart==="in-memory")this.#n=ae(!1);else if(this.#t.options.fastStart!=="fragmented"){if(typeof this.#t.options.fastStart=="object"){let t=this.#h();this.#e.seek(this.#e.getPos()+t)}this.#n=ae(!0),this.#i.writeBox(this.#n)}this.#e.flush()}#h(){c(typeof this.#t.options.fastStart=="object");let e=0,t=[this.#t.options.fastStart.expectedVideoChunks,this.#t.options.fastStart.expectedAudioChunks];for(let s of t)s&&(e+=8*Math.ceil(2/3*s),e+=4*s,e+=12*Math.ceil(2/3*s),e+=4*s,e+=8*s);return e+=4096,e}#T(e,t){let s=this.#s.find(n=>n.track===e);if(s)return s;c(t),c(t.decoderConfig),c(t.decoderConfig.codedWidth!==void 0),c(t.decoderConfig.codedHeight!==void 0);let o={track:e,type:"video",info:{width:t.decoderConfig.codedWidth,height:t.decoderConfig.codedHeight,decoderConfig:t.decoderConfig},timescale:e.metadata.frameRate??57600,samples:[],sampleQueue:[],timestampProcessingQueue:[],timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.#s.push(o),this.#s.sort((n,a)=>n.track.id-a.track.id),o}#C(e,t){let s=this.#s.find(n=>n.track===e);if(s)return s;c(t),c(t.decoderConfig);let o={track:e,type:"audio",info:{numberOfChannels:t.decoderConfig.numberOfChannels,sampleRate:t.decoderConfig.sampleRate,decoderConfig:t.decoderConfig},timescale:t.decoderConfig.sampleRate,samples:[],sampleQueue:[],timestampProcessingQueue:[],timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.#s.push(o),this.#s.sort((n,a)=>n.track.id-a.track.id),o}#w(e,t){let s=this.#s.find(n=>n.track===e);if(s)return s;c(t),c(t.config);let o={track:e,type:"subtitle",info:{config:t.config},timescale:1e3,samples:[],sampleQueue:[],timestampProcessingQueue:[],timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[],lastCueEndTimestamp:0,cueQueue:[],nextSourceId:0,cueToSourceId:new WeakMap};return this.#s.push(o),this.#s.sort((n,a)=>n.track.id-a.track.id),this.validateAndNormalizeTimestamp(e,0,!0),o}addEncodedVideoChunk(e,t,s){let o=this.#T(e,s);if(typeof this.#t.options.fastStart=="object"&&o.samples.length===this.#t.options.fastStart.expectedVideoChunks)throw new Error(`Cannot add more video chunks than specified in 'fastStart' (${this.#t.options.fastStart.expectedVideoChunks}).`);let n=new Uint8Array(t.byteLength);t.copyTo(n);let a=this.validateAndNormalizeTimestamp(o.track,t.timestamp,t.type==="key"),u=this.#b(o,n,a,(t.duration??0)/1e6,t.type);this.#t.options.fastStart==="fragmented"?(o.sampleQueue.push(u),this.#p()):this.#m(o,u)}addEncodedAudioChunk(e,t,s){let o=this.#C(e,s);if(typeof this.#t.options.fastStart=="object"&&o.samples.length===this.#t.options.fastStart.expectedAudioChunks)throw new Error(`Cannot add more audio chunks than specified in 'fastStart' (${this.#t.options.fastStart.expectedAudioChunks}).`);let n=new Uint8Array(t.byteLength);t.copyTo(n);let a=this.validateAndNormalizeTimestamp(o.track,t.timestamp,t.type==="key"),u=this.#b(o,n,a,(t.duration??0)/1e6,t.type);this.#t.options.fastStart==="fragmented"?(o.sampleQueue.push(u),this.#p()):this.#m(o,u)}addSubtitleCue(e,t,s){let o=this.#w(e,s);this.validateAndNormalizeTimestamp(o.track,1e6*t.timestamp,!0),e.source.codec==="webvtt"&&(o.cueQueue.push(t),this.#g(o,t.timestamp))}#g(e,t){for(;e.cueQueue.length>0;){let s=new Set([]);for(let d of e.cueQueue)c(d.timestamp<=t),c(e.lastCueEndTimestamp<=d.timestamp+d.duration),s.add(Math.max(d.timestamp,e.lastCueEndTimestamp)),s.add(d.timestamp+d.duration);let o=[...s].sort((d,f)=>d-f),n=o[0],a=o[1]??n;if(t=a)break;I.lastIndex=0;let C=I.test(f.text),v=f.timestamp+f.duration,k=e.cueToSourceId.get(f);if(k===void 0&&as.timestamp).sort((s,o)=>s-o);for(let s=0;s{if(e===a)return t.type==="key";let u=a.sampleQueue[0];return u&&u.type==="key"});o>=1&&n&&(s=!0,this.#S())}else s=o>=.5}s&&(e.currentChunk&&this.#x(e),e.currentChunk={startTimestamp:t.timestamp,samples:[],offset:null,moofOffset:null}),c(e.currentChunk),e.currentChunk.samples.push(t),e.timestampProcessingQueue.push(t)}#x(e){if(c(this.#t.options.fastStart!=="fragmented"),!!e.currentChunk){if(e.finalizedChunks.push(e.currentChunk),this.#u.push(e.currentChunk),(e.compactlyCodedChunkTable.length===0||P(e.compactlyCodedChunkTable).samplesPerChunk!==e.currentChunk.samples.length)&&e.compactlyCodedChunkTable.push({firstChunk:e.finalizedChunks.length,samplesPerChunk:e.currentChunk.samples.length}),this.#t.options.fastStart==="in-memory"){e.currentChunk.offset=0;return}e.currentChunk.offset=this.#e.getPos();for(let t of e.currentChunk.samples)c(t.data),this.#e.write(t.data),t.data=null;this.#e.flush()}}#p(){c(this.#t.options.fastStart==="fragmented");for(let e of this.output.tracks)if(!e.source.closed&&!this.#s.some(t=>t.track===e))return;e:for(;;){let e=null,t=1/0;for(let o of this.#s){if(o.sampleQueue.length===0&&!o.track.source.closed)break e;o.sampleQueue.length>0&&o.sampleQueue[0].timestamp=2**32&&(u.largeSize=!0,d=this.#i.measureBox(u)+m),u.size=d,this.#i.writeBox(u)}for(let u of this.#s){u.currentChunk.offset=this.#e.getPos(),u.currentChunk.moofOffset=s;for(let m of u.currentChunk.samples)this.#e.write(m.data),m.data=null}let n=this.#e.getPos();this.#e.seek(this.#i.offsets.get(o));let a=ge(t,this.#s);this.#i.writeBox(a),this.#e.seek(n);for(let u of this.#s)u.finalizedChunks.push(u.currentChunk),this.#u.push(u.currentChunk),u.currentChunk=null;e&&this.#e.flush()}onTrackClose(e){if(e.type==="subtitle"&&e.source.codec==="webvtt"){let t=this.#s.find(s=>s.track===e);t&&this.#g(t,1/0)}this.#t.options.fastStart==="fragmented"&&this.#p()}finalize(){for(let e of this.#s)e.type==="subtitle"&&e.track.source.codec==="webvtt"&&this.#g(e,1/0);if(this.#t.options.fastStart==="fragmented"){for(let e of this.#s){for(let t of e.sampleQueue)this.#m(e,t);this.#f(e)}this.#S(!1)}else for(let e of this.#s)this.#f(e),this.#x(e);if(this.#t.options.fastStart==="in-memory"){c(this.#n);let e;for(let s=0;s<2;s++){let o=H(this.#s,this.#d),n=this.#i.measureBox(o);e=this.#i.measureBox(this.#n);let a=this.#e.getPos()+n+e;for(let u of this.#u){u.offset=a;for(let{data:m}of u.samples)c(m),a+=m.byteLength,e+=m.byteLength}if(a<2**32)break;e>=2**32&&(this.#n.largeSize=!0)}let t=H(this.#s,this.#d);this.#i.writeBox(t),this.#n.size=e,this.#i.writeBox(this.#n);for(let s of this.#u)for(let o of s.samples)c(o.data),this.#e.write(o.data),o.data=null}else if(this.#t.options.fastStart==="fragmented"){let e=this.#e.getPos(),t=Ke(this.#s);this.#i.writeBox(t);let s=this.#e.getPos()-e;this.#e.seek(this.#e.getPos()-4),this.#i.writeU32(s)}else{c(this.#n),c(this.#c!==null);let e=this.#i.offsets.get(this.#n);c(e!==void 0);let t=this.#e.getPos()-e;this.#n.size=t,this.#n.largeSize=t>=2**32,this.#i.patchBox(this.#n);let s=H(this.#s,this.#d);if(typeof this.#t.options.fastStart=="object"){this.#e.seek(this.#c),this.#i.writeBox(s);let o=e-this.#e.getPos();this.#i.writeBox(He(o))}else this.#i.writeBox(s)}}};var j=class{constructor(r){this.value=r}},B=class{constructor(r){this.value=r}},L=class{constructor(r){this.value=r}};var xe=i=>i<256?1:i<65536?2:i<1<<24?3:i<2**32?4:i<2**40?5:6,Se=i=>i>=-64&&i<64?1:i>=-8192&&i<8192?2:i>=-(1<<20)&&i<1<<20?3:i>=-(1<<27)&&i<1<<27?4:i>=-(2**34)&&i<2**34?5:6,Le=i=>{if(i<127)return 1;if(i<16383)return 2;if(i<(1<<21)-1)return 3;if(i<(1<<28)-1)return 4;if(i<2**35-1)return 5;if(i<2**42-1)return 6;throw new Error("EBML VINT size not supported "+i)};var ye=2**15,qe="https://github.com/Vanilagy/webm-muxer",Ye=6,Ze=5,rr={avc:"V_MPEG4/ISO/AVC",hevc:"V_MPEGH/ISO/HEVC",vp8:"V_VP8",vp9:"V_VP9",av1:"V_AV1",aac:"A_AAC",opus:"A_OPUS",webvtt:"S_TEXT/WEBVTT"},ir={video:1,audio:2,subtitle:17},me=class extends _{constructor(e,t){super(e);this.timestampsMustStartAtZero=!0;this.#t=new Uint8Array(8);this.#r=new DataView(this.#t.buffer);this.offsets=new WeakMap;this.dataOffsets=new WeakMap;this.#o=[];this.#a=null;this.#c=null;this.#n=null;this.#s=null;this.#d=null;this.#u=null;this.#l=null;this.#h=null;this.#T=new Set;this.#C=0;this.#e=e.writer,this.#i=t}#e;#i;#t;#r;#o;#a;#c;#n;#s;#d;#u;#l;#h;#T;#C;#w(e){this.#r.setUint8(0,e),this.#e.write(this.#t.subarray(0,1))}#g(e){this.#r.setFloat32(0,e,!1),this.#e.write(this.#t.subarray(0,4))}#b(e){this.#r.setFloat64(0,e,!1),this.#e.write(this.#t)}#f(e,t=xe(e)){let s=0;switch(t){case 6:this.#r.setUint8(s++,e/2**40|0);case 5:this.#r.setUint8(s++,e/2**32|0);case 4:this.#r.setUint8(s++,e>>24);case 3:this.#r.setUint8(s++,e>>16);case 2:this.#r.setUint8(s++,e>>8);case 1:this.#r.setUint8(s++,e);break;default:throw new Error("Bad UINT size "+t)}this.#e.write(this.#t.subarray(0,s))}#m(e,t=Se(e)){e<0&&(e+=2**(t*8)),this.#f(e,t)}writeEBMLVarInt(e,t=Le(e)){let s=0;switch(t){case 1:this.#r.setUint8(s++,128|e);break;case 2:this.#r.setUint8(s++,64|e>>8),this.#r.setUint8(s++,e);break;case 3:this.#r.setUint8(s++,32|e>>16),this.#r.setUint8(s++,e>>8),this.#r.setUint8(s++,e);break;case 4:this.#r.setUint8(s++,16|e>>24),this.#r.setUint8(s++,e>>16),this.#r.setUint8(s++,e>>8),this.#r.setUint8(s++,e);break;case 5:this.#r.setUint8(s++,8|e/2**32&7),this.#r.setUint8(s++,e>>24),this.#r.setUint8(s++,e>>16),this.#r.setUint8(s++,e>>8),this.#r.setUint8(s++,e);break;case 6:this.#r.setUint8(s++,4|e/2**40&3),this.#r.setUint8(s++,e/2**32|0),this.#r.setUint8(s++,e>>24),this.#r.setUint8(s++,e>>16),this.#r.setUint8(s++,e>>8),this.#r.setUint8(s++,e);break;default:throw new Error("Bad EBML VINT size "+t)}this.#e.write(this.#t.subarray(0,s))}#x(e){this.#e.write(new Uint8Array(e.split("").map(t=>t.charCodeAt(0))))}writeEBML(e){if(e!==null){if(e instanceof Uint8Array)this.#e.write(e);else if(Array.isArray(e))for(let t of e)this.writeEBML(t);else if(this.offsets.set(e,this.#e.getPos()),this.#f(e.id),Array.isArray(e.data)){let t=this.#e.getPos(),s=e.size===-1?1:e.size??4;e.size===-1?this.#w(255):this.#e.seek(this.#e.getPos()+s);let o=this.#e.getPos();if(this.dataOffsets.set(e,o),this.writeEBML(e.data),e.size!==-1){let n=this.#e.getPos()-o,a=this.#e.getPos();this.#e.seek(t),this.writeEBMLVarInt(n,s),this.#e.seek(a)}}else if(typeof e.data=="number"){let t=e.size??xe(e.data);this.writeEBMLVarInt(t),this.#f(e.data,t)}else if(typeof e.data=="string")this.writeEBMLVarInt(e.data.length),this.#x(e.data);else if(e.data instanceof Uint8Array)this.writeEBMLVarInt(e.data.byteLength,e.size),this.#e.write(e.data);else if(e.data instanceof j)this.writeEBMLVarInt(4),this.#g(e.data.value);else if(e.data instanceof B)this.writeEBMLVarInt(8),this.#b(e.data.value);else if(e.data instanceof L){let t=e.size??Se(e.data.value);this.writeEBMLVarInt(t),this.#m(e.data.value,t)}}}beforeTrackAdd(e){if(this.#i instanceof F)if(e.type==="video"){if(!["vp8","vp9","av1"].includes(e.source.codec))throw new Error("WebM only supports VP8, VP9 and AV1 as video codecs. Switching to MKV removes this restriction.")}else if(e.type==="audio"){if(!["opus","vorbis"].includes(e.source.codec))throw new Error("WebM only supports Opus and Vorbis as audio codecs. Switching to MKV removes this restriction.")}else if(e.type==="subtitle"){if(e.source.codec!=="webvtt")throw new Error("WebM only supports WebVTT as subtitle codec. Switching to MKV removes this restriction.")}else throw new Error("WebM only supports video, audio and subtitle tracks. Switching to MKV removes this restriction.")}start(){this.#p(),this.#i.options.streaming||this.#S(),this.#V(),this.#z(),this.#e.flush()}#p(){let e={id:440786851,data:[{id:17030,data:1},{id:17143,data:1},{id:17138,data:4},{id:17139,data:8},{id:17026,data:this.#i instanceof F?"webm":"matroska"},{id:17031,data:2},{id:17029,data:2}]};this.writeEBML(e)}#S(){let e=new Uint8Array([28,83,187,107]),t=new Uint8Array([21,73,169,102]),s=new Uint8Array([22,84,174,107]),o={id:290298740,data:[{id:19899,data:[{id:21419,data:e},{id:21420,size:5,data:0}]},{id:19899,data:[{id:21419,data:t},{id:21420,size:5,data:0}]},{id:19899,data:[{id:21419,data:s},{id:21420,size:5,data:0}]}]};this.#n=o}#V(){let e={id:17545,data:new B(0)};this.#d=e;let t={id:357149030,data:[{id:2807729,data:1e6},{id:19840,data:qe},{id:22337,data:qe},this.#i.options.streaming?null:e]};this.#c=t}#M(){let e={id:374648427,data:[]};this.#s=e;for(let t of this.#o)e.data.push({id:174,data:[{id:215,data:t.track.id},{id:29637,data:t.track.id},{id:131,data:ir[t.type]},{id:134,data:rr[t.track.source.codec]},...t.type==="video"?[t.info.decoderConfig.description?{id:25506,data:E(t.info.decoderConfig.description)}:null,t.track.metadata.frameRate?{id:2352003,data:1e9/t.track.metadata.frameRate}:null,{id:224,data:[{id:176,data:t.info.width},{id:186,data:t.info.height},(()=>{if(t.info.decoderConfig.colorSpace){let s=t.info.decoderConfig.colorSpace;return re(s)?{id:21936,data:[{id:21937,data:te[s.matrix]},{id:21946,data:ee[s.transfer]},{id:21947,data:J[s.primaries]},{id:21945,data:[1,2][Number(s.fullRange)]}]}:null}return null})()]}]:[],...t.type==="audio"?[t.info.decoderConfig.description?{id:25506,data:E(t.info.decoderConfig.description)}:null,{id:225,data:[{id:181,data:new j(t.info.sampleRate)},{id:159,data:t.info.numberOfChannels}]}]:[],...t.type==="subtitle"?[{id:25506,data:y.encode(t.info.config.description)}]:[]]})}#E(){let e={id:408125543,size:this.#i.options.streaming?-1:Ye,data:[this.#i.options.streaming?null:this.#n,this.#c,this.#s]};this.#a=e,this.writeEBML(e)}#z(){this.#u={id:475249515,data:[]}}get#k(){return c(this.#a),this.dataOffsets.get(this.#a)}#U(e,t){let s=this.#o.find(n=>n.track===e);if(s)return s;c(t),c(t.decoderConfig),c(t.decoderConfig.codedWidth!==void 0),c(t.decoderConfig.codedHeight!==void 0);let o={track:e,type:"video",info:{width:t.decoderConfig.codedWidth,height:t.decoderConfig.codedHeight,decoderConfig:t.decoderConfig},chunkQueue:[],lastWrittenMsTimestamp:null};return this.#o.push(o),this.#o.sort((n,a)=>n.track.id-a.track.id),o}#P(e,t){let s=this.#o.find(n=>n.track===e);if(s)return s;c(t),c(t.decoderConfig);let o={track:e,type:"audio",info:{numberOfChannels:t.decoderConfig.numberOfChannels,sampleRate:t.decoderConfig.sampleRate,decoderConfig:t.decoderConfig},chunkQueue:[],lastWrittenMsTimestamp:null};return this.#o.push(o),this.#o.sort((n,a)=>n.track.id-a.track.id),o}#I(e,t){let s=this.#o.find(n=>n.track===e);if(s)return s;c(t),c(t.config);let o={track:e,type:"subtitle",info:{config:t.config},chunkQueue:[],lastWrittenMsTimestamp:null};return this.#o.push(o),this.#o.sort((n,a)=>n.track.id-a.track.id),o}addEncodedVideoChunk(e,t,s){let o=this.#U(e,s),n=new Uint8Array(t.byteLength);t.copyTo(n);let a=this.validateAndNormalizeTimestamp(o.track,t.timestamp,t.type==="key"),u=this.#A(n,a,(t.duration??0)/1e6,t.type);e.source.codec==="vp9"&&this.#_(o,u),o.chunkQueue.push(u),this.#y()}addEncodedAudioChunk(e,t,s){let o=this.#P(e,s),n=new Uint8Array(t.byteLength);t.copyTo(n);let a=this.validateAndNormalizeTimestamp(o.track,t.timestamp,t.type==="key"),u=this.#A(n,a,(t.duration??0)/1e6,t.type);o.chunkQueue.push(u),this.#y()}addSubtitleCue(e,t,s){let o=this.#I(e,s),n=this.validateAndNormalizeTimestamp(o.track,1e6*t.timestamp,!0),a=t.text,u=Math.floor(n*1e3);I.lastIndex=0,a=a.replace(I,C=>{let k=se(C.slice(1,-1))-u;return`<${oe(k)}>`});let m=y.encode(a),d=`${t.settings??""}
-${t.identifier??""}
-${t.notes??""}`,f=this.#A(m,n,t.duration,"key",d.trim()?y.encode(d):null);o.chunkQueue.push(f),this.#y()}#y(){for(let e of this.output.tracks)if(!e.source.closed&&!this.#o.some(t=>t.track===e))return;e:for(;;){let e=null,t=1/0;for(let o of this.#o){if(o.chunkQueue.length===0&&!o.track.source.closed)break e;o.chunkQueue.length>0&&o.chunkQueue[0].timestamp=2&&s++;let m={rgb:7,bt709:2,bt470bg:1,smpte170m:3}[e.info.decoderConfig.colorSpace.matrix];_e(t.data,s+0,s+3,m)}#A(e,t,s,o,n=null){return{data:e,type:o,timestamp:t,duration:s,additions:n}}#O(e,t){this.#a||(this.#M(),this.#E());let s=Math.floor(1e3*t.timestamp),o=this.#o.every(f=>{if(f.track.source.closed)return!0;if(e===f)return t.type==="key";let C=f.chunkQueue[0];return C&&C.type==="key"});(!this.#l||o&&s-this.#h>=1e3)&&this.#R(s);let n=s-this.#h;if(n<0)return;if(n>=ye)throw new Error(`Current Matroska cluster exceeded its maximum allowed length of ${ye} milliseconds. In order to produce a correct WebM file, you must pass in a key frame at least every ${ye} milliseconds.`);let u=new Uint8Array(4),m=new DataView(u.buffer);m.setUint8(0,128|e.track.id),m.setInt16(1,n,!1);let d=Math.floor(1e3*t.duration);if(d===0&&!t.additions){m.setUint8(3,+(t.type==="key")<<7);let f={id:163,data:[u,t.data]};this.writeEBML(f)}else{let f={id:160,data:[{id:161,data:[u,t.data]},t.type==="delta"?{id:251,data:new L(e.lastWrittenMsTimestamp-s)}:null,t.additions?{id:30113,data:[{id:166,data:[{id:165,data:t.additions},{id:238,data:1}]}]}:null,d>0?{id:155,data:d}:null]};this.writeEBML(f)}this.#C=Math.max(this.#C,s+d),e.lastWrittenMsTimestamp=s,this.#T.add(e)}#R(e){this.#l&&!this.#i.options.streaming&&this.#v(),this.#l={id:524531317,size:this.#i.options.streaming?-1:Ze,data:[{id:231,data:e}]},this.writeEBML(this.#l),this.#h=e,this.#T.clear()}#v(){c(this.#l);let e=this.#e.getPos()-this.dataOffsets.get(this.#l),t=this.#e.getPos();this.#e.seek(this.offsets.get(this.#l)+4),this.writeEBMLVarInt(e,Ze),this.#e.seek(t);let s=this.offsets.get(this.#l)-this.#k;c(this.#u),this.#u.data.push({id:187,data:[{id:179,data:this.#h},...[...this.#T].map(o=>({id:183,data:[{id:247,data:o.track.id},{id:241,data:s}]}))]})}onTrackClose(){this.#y()}finalize(){for(let e of this.#o)for(;e.chunkQueue.length>0;)this.#O(e,e.chunkQueue.shift());if(this.#i.options.streaming||this.#v(),c(this.#u),this.writeEBML(this.#u),!this.#i.options.streaming){let e=this.#e.getPos(),t=this.#e.getPos()-this.#k;this.#e.seek(this.offsets.get(this.#a)+4),this.writeEBMLVarInt(t,Ye),this.#d.data=new B(this.#C),this.#e.seek(this.offsets.get(this.#d)),this.writeEBML(this.#d),this.#n.data[0].data[1].data=this.offsets.get(this.#u)-this.#k,this.#n.data[1].data[1].data=this.offsets.get(this.#c)-this.#k,this.#n.data[2].data[1].data=this.offsets.get(this.#s)-this.#k,this.#e.seek(this.offsets.get(this.#n)),this.writeEBML(this.#n),this.#e.seek(e)}}};var fe=class{},we=class extends fe{constructor(e){super();this.options=e}createMuxer(e){return new de(e,this)}},pe=class extends fe{constructor(e={}){super();this.options=e}createMuxer(e){return new me(e,this)}},F=class extends pe{};var Je=(i,r,e)=>{if(i==="avc"){let t=100;r<=768&&e<=432?t=66:r<=1920&&e<=1080&&(t=77);let s=0,o=r>1920||e>1080?50:41,n=t.toString(16).padStart(2,"0"),a=s.toString(16).padStart(2,"0"),u=o.toString(16).padStart(2,"0");return`avc1.${n}${a}${u}`}else if(i==="hevc"){let t=0,s=1,o=Array(32).fill(0);o[s]=1;let n=parseInt(o.reverse().join(""),2).toString(16).replace(/^0+/,""),a="L",u=120;return r<=1280&&e<=720?u=93:r<=1920&&e<=1080?u=120:r<=3840&&e<=2160?u=150:(a="H",u=180),`hev1.${t===0?"":String.fromCharCode(65+t-1)}${s}.${n}.${a}${u}.B0`}else{if(i==="vp8")return"vp8";if(i==="vp9"){let t="00",s;return r<=854&&e<=480?s="21":r<=1280&&e<=720?s="31":r<=1920&&e<=1080?s="41":r<=3840&&e<=2160?s="51":s="61",`vp09.${t}.${s}.08`}else if(i==="av1"){let s;return r<=854&&e<=480?s="01":r<=1280&&e<=720?s="03":r<=1920&&e<=1080?s="04":r<=3840&&e<=2160?s="07":s="09",`av01.0.${s}M.08`}}throw new Error(`Unhandled codec '${i}'.`)},et=(i,r,e)=>{if(i==="aac")return r>=2&&e<=24e3?"mp4a.40.29":e<=24e3?"mp4a.40.5":"mp4a.40.2";if(i==="opus")return"opus";if(i==="vorbis")return"vorbis";throw new Error(`Unhandled codec '${i}'.`)};var q=class{constructor(){this.connectedTrack=null;this.closed=!1;this.offsetTimestamps=!1}ensureValidDigest(){if(!this.connectedTrack)throw new Error("Cannot call digest without connecting the source to an output track.");if(!this.connectedTrack.output.started)throw new Error("Cannot call digest before output has been started.");if(this.connectedTrack.output.finalizing)throw new Error("Cannot call digest after output has started finalizing.");if(this.closed)throw new Error("Cannot call digest after source has been closed.")}start(){}async flush(){}close(){if(this.closed)throw new Error("Source already closed.");if(!this.connectedTrack)throw new Error("Cannot call close without connecting the source to an output track.");if(!this.connectedTrack.output.started)throw new Error("Cannot call close before output has been started.");this.closed=!0,!this.connectedTrack.output.finalizing&&this.connectedTrack.output.muxer.onTrackClose(this.connectedTrack)}},D=class extends q{constructor(e){super();this.connectedTrack=null;this.codec=e}},Ae=class extends D{constructor(r){super(r)}digest(r,e){this.ensureValidDigest(),this.connectedTrack?.output.muxer.addEncodedVideoChunk(this.connectedTrack,r,e)}},sr=5,Y=class{constructor(r,e){this.source=r;this.codecConfig=e;this.encoder=null;this.lastMultipleOfKeyFrameInterval=-1}digest(r){this.source.ensureValidDigest(),this.ensureEncoder(r),c(this.encoder);let e=Math.floor(r.timestamp/1e6/sr);this.encoder.encode(r,{keyFrame:e!==this.lastMultipleOfKeyFrameInterval}),this.lastMultipleOfKeyFrameInterval=e}ensureEncoder(r){this.encoder||(this.encoder=new VideoEncoder({output:(e,t)=>this.source.connectedTrack?.output.muxer.addEncodedVideoChunk(this.source.connectedTrack,e,t),error:e=>console.error(e)}),this.encoder.configure({codec:Je(this.codecConfig.codec,r.codedWidth,r.codedHeight),width:r.codedWidth,height:r.codedHeight,bitrate:this.codecConfig.bitrate,framerate:this.source.connectedTrack?.metadata.frameRate,latencyMode:this.codecConfig.latencyMode}))}async flush(){return this.encoder?.flush()}},Oe=class extends D{constructor(r){super(r.codec),this.encoder=new Y(this,r)}digest(r){this.encoder.digest(r)}flush(){return this.encoder.flush()}},ve=class extends D{constructor(e,t){super(t.codec);this.canvas=e;this.encoder=new Y(this,t)}digest(e,t=0){let s=new VideoFrame(this.canvas,{timestamp:Math.round(1e6*e),duration:Math.round(1e6*t),alpha:"discard"});this.encoder.digest(s),s.close()}flush(){return this.encoder.flush()}},Ve=class extends D{constructor(e,t){super(t.codec);this.track=e;this.abortController=null;this.offsetTimestamps=!0;this.encoder=new Y(this,t)}start(){this.abortController=new AbortController;let e=new MediaStreamTrackProcessor({track:this.track}),t=new WritableStream({write:s=>{this.encoder.digest(s),s.close()}});e.readable.pipeTo(t,{signal:this.abortController.signal}).catch(s=>{s instanceof DOMException&&s.name==="AbortError"||console.error("Pipe error:",s)})}async flush(){this.abortController&&(this.abortController.abort(),this.abortController=null),await this.encoder.flush()}},N=class extends q{constructor(e){super();this.connectedTrack=null;this.codec=e}},Me=class extends N{constructor(r){super(r)}digest(r,e){this.ensureValidDigest(),this.connectedTrack?.output.muxer.addEncodedAudioChunk(this.connectedTrack,r,e)}},Z=class{constructor(r,e){this.source=r;this.codecConfig=e;this.encoder=null}digest(r){this.source.ensureValidDigest(),this.ensureEncoder(r),c(this.encoder),this.encoder.encode(r)}ensureEncoder(r){this.encoder||(this.encoder=new AudioEncoder({output:(e,t)=>this.source.connectedTrack?.output.muxer.addEncodedAudioChunk(this.source.connectedTrack,e,t),error:e=>console.error(e)}),this.encoder.configure({codec:et(this.codecConfig.codec,r.numberOfChannels,r.sampleRate),numberOfChannels:r.numberOfChannels,sampleRate:r.sampleRate,bitrate:this.codecConfig.bitrate}))}async flush(){return this.encoder?.flush()}},Ee=class extends N{constructor(r){super(r.codec),this.encoder=new Z(this,r)}digest(r){this.encoder.digest(r)}flush(){return this.encoder.flush()}},ze=class extends N{constructor(e){super(e.codec);this.accumulatedFrameCount=0;this.encoder=new Z(this,e)}digest(e){let t=e.numberOfChannels,s=e.sampleRate,o=e.length,n=new Float32Array(t*o);for(let u=0;u{this.encoder.digest(s),s.close()}});e.readable.pipeTo(t,{signal:this.abortController.signal}).catch(s=>{s instanceof DOMException&&s.name==="AbortError"||console.error("Pipe error:",s)})}async flush(){this.abortController&&(this.abortController.abort(),this.abortController=null),await this.encoder.flush()}},Pe=class extends q{constructor(e){super();this.connectedTrack=null;this.codec=e}},Ie=class extends Pe{constructor(r){super(r),this.parser=new ie({codec:r,output:(e,t)=>this.connectedTrack?.output.muxer.addSubtitleCue(this.connectedTrack,e,t),error:e=>console.error(e)})}digest(r){this.ensureValidDigest(),this.parser.parse(r)}};export{X as ArrayBufferTarget,ze as AudioBufferSource,Ee as AudioDataSource,ve as CanvasSource,Me as EncodedAudioChunkSource,Ae as EncodedVideoChunkSource,ke as FileSystemWritableFileStreamTarget,Ue as MediaStreamAudioTrackSource,Ve as MediaStreamVideoTrackSource,pe as MkvOutputFormat,we as Mp4OutputFormat,he as Output,G as StreamTarget,R as Target,Ie as TextSubtitleSource,Oe as VideoFrameSource,F as WebMOutputFormat};
+`,o);u===-1&&(u=t.length);let f=ae(e[2]),m=ae(e[3])-f,g=t.slice(n,u).trim();t=t.slice(u).trimStart(),K.lastIndex=0;let E={timestamp:f/1e3,duration:m/1e3,text:g,identifier:s,settings:a,notes:r},y={};this.#i||(y.config={description:this.#r},this.#i=!0),this.#e.output(E,y)}}},dt=/(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})/,ae=i=>{let t=dt.exec(i);if(!t)throw new Error("Expected match.");return 60*60*1e3*Number(t[1]||"0")+60*1e3*Number(t[2])+1e3*Number(t[3])+Number(t[4])},ue=i=>{let t=Math.floor(i/36e5),e=Math.floor(i%(60*60*1e3)/(60*1e3)),r=Math.floor(i%(60*1e3)/1e3),s=i%1e3;return t.toString().padStart(2,"0")+":"+e.toString().padStart(2,"0")+":"+r.toString().padStart(2,"0")+"."+s.toString().padStart(3,"0")};var X=class{constructor(t){this.writer=t;this.helper=new Uint8Array(8);this.helperView=new DataView(this.helper.buffer);this.offsets=new WeakMap}writeU32(t){this.helperView.setUint32(0,t,!1),this.writer.write(this.helper.subarray(0,4))}writeU64(t){this.helperView.setUint32(0,Math.floor(t/2**32),!1),this.helperView.setUint32(4,t,!1),this.writer.write(this.helper.subarray(0,8))}writeAscii(t){for(let e=0;e[(i%256+256)%256],p=i=>(v.setUint16(0,i,!1),[h[0],h[1]]),ct=i=>(v.setInt16(0,i,!1),[h[0],h[1]]),je=i=>(v.setUint32(0,i,!1),[h[1],h[2],h[3]]),l=i=>(v.setUint32(0,i,!1),[h[0],h[1],h[2],h[3]]),Qe=i=>(v.setInt32(0,i,!1),[h[0],h[1],h[2],h[3]]),P=i=>(v.setUint32(0,Math.floor(i/2**32),!1),v.setUint32(4,i,!1),[h[0],h[1],h[2],h[3],h[4],h[5],h[6],h[7]]),Se=i=>(v.setInt16(0,2**8*i,!1),[h[0],h[1]]),A=i=>(v.setInt32(0,2**16*i,!1),[h[0],h[1],h[2],h[3]]),ke=i=>(v.setInt32(0,2**30*i,!1),[h[0],h[1],h[2],h[3]]),xe=(i,t)=>{let e=[],r=i;do{let s=r&127;r>>=7,e.length>0&&(s|=128),e.push(s),t!==void 0&&t--}while(r>0||t);return e.reverse()},w=(i,t=!1)=>{let e=Array(i.length).fill(null).map((r,s)=>i.charCodeAt(s));return t&&e.push(0),e},Ae=i=>{let t=null;for(let e of i)(!t||e.timestamp>t.timestamp)&&(t=e);return t},Ke=i=>{let t=i*(Math.PI/180),e=Math.cos(t),r=Math.sin(t);return[e,r,0,-r,e,0,0,0,1]},Xe=Ke(0),Ge=i=>[A(i[0]),A(i[1]),ke(i[2]),A(i[3]),A(i[4]),ke(i[5]),A(i[6]),A(i[7]),ke(i[8])],b=(i,t,e)=>({type:i,contents:t&&new Uint8Array(t.flat(10)),children:e}),T=(i,t,e,r,s)=>b(i,[C(t),je(e),r??[]],s),Le=i=>{let t=512;return i.fragmented?b("ftyp",[w("iso5"),l(t),w("iso5"),w("iso6"),w("mp41")]):b("ftyp",[w("isom"),l(t),w("isom"),i.holdsAvc?w("avc1"):[],w("mp41")])},de=i=>({type:"mdat",largeSize:i}),qe=i=>({type:"free",size:i}),G=(i,t,e=!1)=>b("moov",void 0,[ft(t,i),...i.map(r=>mt(r,t)),e?Kt(i):null]),ft=(i,t)=>{let e=S(Math.max(0,...t.filter(n=>n.samples.length>0).map(n=>{let a=Ae(n.samples);return a.timestamp+a.duration})),le),r=Math.max(0,...t.map(n=>n.track.id))+1,s=!I(i)||!I(e),o=s?P:l;return T("mvhd",+s,0,[o(i),o(i),l(le),o(e),A(1),Se(1),Array(10).fill(0),Ge(Xe),Array(24).fill(0),l(r)])},mt=(i,t)=>b("trak",void 0,[ht(i,t),pt(i,t)]),ht=(i,t)=>{let e=Ae(i.samples),r=S(e?e.timestamp+e.duration:0,le),s=!I(t)||!I(r),o=s?P:l,n;if(i.type==="video"){let a=i.track.metadata.rotation;n=a===void 0||typeof a=="number"?Ke(a??0):a}else n=Xe;return T("tkhd",+s,3,[o(t),o(t),l(i.track.id),l(0),o(r),Array(8).fill(0),p(0),p(i.track.id),Se(i.type==="audio"?1:0),p(0),Ge(n),A(i.type==="video"?i.info.width:0),A(i.type==="video"?i.info.height:0)])},pt=(i,t)=>b("mdia",void 0,[bt(i,t),Ct(i),yt(i)]),bt=(i,t)=>{let e=Ae(i.samples),r=S(e?e.timestamp+e.duration:0,i.timescale),s=!I(t)||!I(r),o=s?P:l;return T("mdhd",+s,0,[o(t),o(t),l(i.timescale),o(r),p(21956),p(0)])},Tt={video:"vide",audio:"soun",subtitle:"text"},gt={video:"VideoHandler",audio:"SoundHandler",subtitle:"TextHandler"},Ct=i=>T("hdlr",0,0,[w("mhlr"),w(Tt[i.type]),l(0),l(0),l(0),w(gt[i.type],!0)]),yt=i=>b("minf",void 0,[St[i.type](),At(),Ot(i)]),wt=()=>T("vmhd",0,1,[p(0),p(0),p(0),p(0)]),kt=()=>T("smhd",0,0,[p(0),p(0)]),xt=()=>T("nmhd",0,0),St={video:wt,audio:kt,subtitle:xt},At=()=>b("dinf",void 0,[vt()]),vt=()=>T("dref",0,0,[l(1)],[Et()]),Et=()=>T("url ",0,1),Ot=i=>{let t=i.compositionTimeOffsetTable.length>1||i.compositionTimeOffsetTable.some(e=>e.sampleCompositionTimeOffset!==0);return b("stbl",void 0,[Vt(i),Wt(i),Dt(i),Ht(i),$t(i),jt(i),t?Qt(i):null])},Vt=i=>{let t;return i.type==="video"?t=Mt(tr[i.track.source.codec],i):i.type==="audio"?t=Rt(ir[i.track.source.codec],i):i.type==="subtitle"&&(t=Nt(or[i.track.source.codec],i)),d(t),T("stsd",0,0,[l(1)],[t])},Mt=(i,t)=>b(i,[Array(6).fill(0),p(1),p(0),p(0),Array(12).fill(0),p(t.info.width),p(t.info.height),l(4718592),l(4718592),l(0),p(1),Array(32).fill(0),p(24),ct(65535)],[rr[t.track.source.codec](t),oe(t.info.decoderConfig.colorSpace)?It(t):null]),It=i=>b("colr",[w("nclx"),p(B[i.info.decoderConfig.colorSpace.primaries]),p(N[i.info.decoderConfig.colorSpace.transfer]),p(F[i.info.decoderConfig.colorSpace.matrix]),C((i.info.decoderConfig.colorSpace.fullRange?1:0)<<7)]),zt=i=>i.info.decoderConfig&&b("avcC",[...U(i.info.decoderConfig.description)]),Ut=i=>i.info.decoderConfig&&b("hvcC",[...U(i.info.decoderConfig.description)]),$e=i=>{if(!i.info.decoderConfig)return null;let t=i.info.decoderConfig;d(t.colorSpace);let e=t.codec.split("."),r=Number(e[1]),s=Number(e[2]),a=(Number(e[3])<<4)+(0<<1)+Number(t.colorSpace.fullRange);return T("vpcC",1,0,[C(r),C(s),C(a),C(2),C(2),C(2),p(0)])},Pt=()=>{let e=(1<<7)+1;return b("av1C",[e,0,0,0])},Rt=(i,t)=>b(i,[Array(6).fill(0),p(1),p(0),p(0),l(0),p(t.info.numberOfChannels),p(16),p(0),p(0),A(t.info.sampleRate)],[sr[t.track.source.codec](t)]),_t=i=>{let e=[...U(i.info.decoderConfig.description??new ArrayBuffer(0))];return e=[...C(64),...C(21),...je(0),...l(0),...l(0),...C(5),...xe(e.length),...e],e=[...p(1),...C(0),...C(4),...xe(e.length),...e,...C(6),...C(1),...C(2)],e=[...C(3),...xe(e.length),...e],T("esds",0,0,e)},Bt=i=>{let t=3840,e=0,r=i.info.decoderConfig?.description;if(r){d(r.byteLength<18);let s=ArrayBuffer.isView(r)?new DataView(r.buffer,r.byteOffset,r.byteLength):new DataView(r);t=s.getUint16(10,!0),e=s.getInt16(14,!0)}return b("dOps",[C(0),C(i.info.numberOfChannels),p(t),l(i.info.sampleRate),Se(e),C(0)])},Nt=(i,t)=>b(i,[Array(6).fill(0),p(1)],[nr[t.track.source.codec](t)]),Ft=i=>b("vttC",[...x.encode(i.info.config.description)]);var Wt=i=>T("stts",0,0,[l(i.timeToSampleTable.length),i.timeToSampleTable.map(t=>[l(t.sampleCount),l(t.sampleDelta)])]),Dt=i=>{if(i.samples.every(e=>e.type==="key"))return null;let t=[...i.samples.entries()].filter(([,e])=>e.type==="key");return T("stss",0,0,[l(t.length),t.map(([e])=>l(e+1))])},Ht=i=>T("stsc",0,0,[l(i.compactlyCodedChunkTable.length),i.compactlyCodedChunkTable.map(t=>[l(t.firstChunk),l(t.samplesPerChunk),l(1)])]),$t=i=>T("stsz",0,0,[l(0),l(i.samples.length),i.samples.map(t=>l(t.size))]),jt=i=>i.finalizedChunks.length>0&&_(i.finalizedChunks).offset>=2**32?T("co64",0,0,[l(i.finalizedChunks.length),i.finalizedChunks.map(t=>P(t.offset))]):T("stco",0,0,[l(i.finalizedChunks.length),i.finalizedChunks.map(t=>l(t.offset))]),Qt=i=>T("ctts",0,0,[l(i.compositionTimeOffsetTable.length),i.compositionTimeOffsetTable.map(t=>[l(t.sampleCount),l(t.sampleCompositionTimeOffset)])]),Kt=i=>b("mvex",void 0,i.map(Xt)),Xt=i=>T("trex",0,0,[l(i.track.id),l(1),l(0),l(0),l(0)]),ve=(i,t)=>b("moof",void 0,[Gt(i),...t.map(Lt)]),Gt=i=>T("mfhd",0,0,[l(i)]),Ye=i=>{let t=0,e=0,r=0,s=0,o=i.type==="delta";return e|=+o,o?t|=1:t|=2,t<<24|e<<16|r<<8|s},Lt=i=>b("traf",void 0,[qt(i),Yt(i),Zt(i)]),qt=i=>{d(i.currentChunk);let t=0;t|=8,t|=16,t|=32,t|=131072;let e=i.currentChunk.samples[1]??i.currentChunk.samples[0],r={duration:e.timescaleUnitsToNextSample,size:e.size,flags:Ye(e)};return T("tfhd",0,t,[l(i.track.id),l(r.duration),l(r.size),l(r.flags)])},Yt=i=>(d(i.currentChunk),T("tfdt",1,0,[P(S(i.currentChunk.startTimestamp,i.timescale))])),Zt=i=>{d(i.currentChunk);let t=i.currentChunk.samples.map(k=>k.timescaleUnitsToNextSample),e=i.currentChunk.samples.map(k=>k.size),r=i.currentChunk.samples.map(Ye),s=i.currentChunk.samples.map(k=>S(k.timestamp-k.decodeTimestamp,i.timescale)),o=new Set(t),n=new Set(e),a=new Set(r),u=new Set(s),f=a.size===2&&r[0]!==r[1],c=o.size>1,m=n.size>1,g=!f&&a.size>1,E=u.size>1||[...u].some(k=>k!==0),y=0;return y|=1,y|=4*+f,y|=256*+c,y|=512*+m,y|=1024*+g,y|=2048*+E,T("trun",1,y,[l(i.currentChunk.samples.length),l(i.currentChunk.offset-i.currentChunk.moofOffset||0),f?l(r[0]):[],i.currentChunk.samples.map((k,R)=>[c?l(t[R]):[],m?l(e[R]):[],g?l(r[R]):[],E?Qe(s[R]):[]])])},Ze=i=>b("mfra",void 0,[...i.map(Jt),er()]),Jt=(i,t)=>T("tfra",1,0,[l(i.track.id),l(63),l(i.finalizedChunks.length),i.finalizedChunks.map(r=>[P(S(r.startTimestamp,i.timescale)),P(r.moofOffset),l(t+1),l(1),l(1)])]),er=()=>T("mfro",0,0,[l(0)]),Je=()=>b("vtte"),et=(i,t,e,r,s)=>b("vttc",void 0,[s!==null?b("vsid",[Qe(s)]):null,e!==null?b("iden",[...x.encode(e)]):null,t!==null?b("ctim",[...x.encode(ue(t))]):null,r!==null?b("sttg",[...x.encode(r)]):null,b("payl",[...x.encode(i)])]),tt=i=>b("vtta",[...x.encode(i)]),tr={avc:"avc1",hevc:"hvc1",vp8:"vp08",vp9:"vp09",av1:"av01"},rr={avc:zt,hevc:Ut,vp8:$e,vp9:$e,av1:Pt},ir={aac:"mp4a",opus:"Opus"},sr={aac:_t,opus:Bt},or={webvtt:"wvtt"},nr={webvtt:Ft};var D=class{constructor(t){this.trackTimestampInfo=new WeakMap;this.output=t}beforeTrackAdd(t){}onTrackClose(t){}validateAndNormalizeTimestamp(t,e,r){let s=e/1e6,o=this.trackTimestampInfo.get(t);if(!o){if(!r)throw new Error("First frame must be a key frame.");if(this.timestampsMustStartAtZero&&s>0)throw new Error(`Timestamps must start at zero (got ${s}s).`);o={timestampOffset:s,maxTimestamp:t.source.offsetTimestamps?0:s,lastKeyFrameTimestamp:t.source.offsetTimestamps?0:s},this.trackTimestampInfo.set(t,o)}if(t.source.offsetTimestamps&&(s-=o.timestampOffset),s<0)throw new Error(`Timestamps must be non-negative (got ${s}s).`);if(sr.start-s.start);t.push({start:e[0].start,size:e[0].data.byteLength});for(let r=1;ru.start<=e&&eur){for(let u=0;u=t.written[o+1].start;)t.written[o].end=Math.max(t.written[o].end,t.written[o+1].end),t.written.splice(o+1,1)}#l(t){let r={start:Math.floor(t/this.#i)*this.#i,data:new Uint8Array(this.#i),written:[],shouldFlush:!1};return this.#t.push(r),this.#t.sort((s,o)=>s.start-o.start),this.#t.indexOf(r)}#n(t=!1){for(let e=0;et.stream.write({type:"write",data:e,position:r}),chunkSize:t.options?.chunkSize}))}};var rt=(i,t,e)=>{if(i==="avc"){let r=100;t<=768&&e<=432?r=66:t<=1920&&e<=1080&&(r=77);let s=0,o=t>1920||e>1080?50:41,n=r.toString(16).padStart(2,"0"),a=s.toString(16).padStart(2,"0"),u=o.toString(16).padStart(2,"0");return`avc1.${n}${a}${u}`}else if(i==="hevc"){let r=0,s=1,o=Array(32).fill(0);o[s]=1;let n=parseInt(o.reverse().join(""),2).toString(16).replace(/^0+/,""),a="L",u=120;return t<=1280&&e<=720?u=93:t<=1920&&e<=1080?u=120:t<=3840&&e<=2160?u=150:(a="H",u=180),`hev1.${r===0?"":String.fromCharCode(65+r-1)}${s}.${n}.${a}${u}.B0`}else{if(i==="vp8")return"vp8";if(i==="vp9"){let r="00",s;return t<=854&&e<=480?s="21":t<=1280&&e<=720?s="31":t<=1920&&e<=1080?s="41":t<=3840&&e<=2160?s="51":s="61",`vp09.${r}.${s}.08`}else if(i==="av1"){let s;return t<=854&&e<=480?s="01":t<=1280&&e<=720?s="03":t<=1920&&e<=1080?s="04":t<=3840&&e<=2160?s="07":s="09",`av01.0.${s}M.08`}}throw new TypeError(`Unhandled codec '${i}'.`)},it=(i,t,e)=>{if(i==="aac")return t>=2&&e<=24e3?"mp4a.40.29":e<=24e3?"mp4a.40.5":"mp4a.40.2";if(i==="opus")return"opus";if(i==="vorbis")return"vorbis";throw new TypeError(`Unhandled codec '${i}'.`)},me=i=>{if(!i)throw new TypeError("Video chunk metadata must be provided.");if(typeof i!="object")throw new TypeError("Video chunk metadata must be an object.");if(!i.decoderConfig)throw new TypeError("Video chunk metadata must include a decoder configuration.");if(typeof i.decoderConfig!="object")throw new TypeError("Video chunk metadata decoder configuration must be an object.");if(typeof i.decoderConfig.codec!="string")throw new TypeError("Video chunk metadata decoder configuration must specify a codec string.");if(!Number.isInteger(i.decoderConfig.codedWidth)||i.decoderConfig.codedWidth<=0)throw new TypeError("Video chunk metadata decoder configuration must specify a valid codedWidth (positive integer).");if(!Number.isInteger(i.decoderConfig.codedHeight)||i.decoderConfig.codedHeight<=0)throw new TypeError("Video chunk metadata decoder configuration must specify a valid codedHeight (positive integer).");if(i.decoderConfig.description!==void 0&&!we(i.decoderConfig.description))throw new TypeError("Video chunk metadata decoder configuration description, when defined, must be an ArrayBuffer or an ArrayBuffer view.");if(i.decoderConfig.colorSpace!==void 0){let{colorSpace:t}=i.decoderConfig;if(typeof t!="object")throw new TypeError("Video chunk metadata decoder configuration colorSpace, when provided, must be an object.");let e=Object.keys(B);if(t.primaries!=null&&!e.includes(t.primaries))throw new TypeError(`Video chunk metadata decoder configuration colorSpace primaries, when defined, must be one of ${e.join(", ")}.`);let r=Object.keys(N);if(t.transfer!=null&&!r.includes(t.transfer))throw new TypeError(`Video chunk metadata decoder configuration colorSpace transfer, when defined, must be one of ${r.join(", ")}.`);let s=Object.keys(F);if(t.matrix!=null&&!s.includes(t.matrix))throw new TypeError(`Video chunk metadata decoder configuration colorSpace matrix, when defined, must be one of ${s.join(", ")}.`);if(t.fullRange!=null&&typeof t.fullRange!="boolean")throw new TypeError("Video chunk metadata decoder configuration colorSpace fullRange, when defined, must be a boolean.")}if((i.decoderConfig.codec.startsWith("avc1")||i.decoderConfig.codec.startsWith("avc3"))&&!i.decoderConfig.description)throw new TypeError("Video chunk metadata decoder configuration for AVC must include a description, which is expected to be an AVCDecoderConfigurationRecord as specified in ISO 14496-15.");if((i.decoderConfig.codec.startsWith("hev1")||i.decoderConfig.codec.startsWith("hvc1"))&&!i.decoderConfig.description)throw new TypeError("Video chunk metadata decoder configuration for HEVC must include a description, which is expected to be an HEVCDecoderConfigurationRecord as specified in ISO 14496-15.");if((i.decoderConfig.codec==="vp8"||i.decoderConfig.codec.startsWith("vp09"))&&i.decoderConfig.colorSpace===void 0)throw new TypeError("Video chunk metadata decoder configuration for VP8/VP9 must include a colorSpace.")},he=i=>{if(!i)throw new TypeError("Audio chunk metadata must be provided.");if(typeof i!="object")throw new TypeError("Audio chunk metadata must be an object.");if(!i.decoderConfig)throw new TypeError("Audio chunk metadata must include a decoder configuration.");if(typeof i.decoderConfig!="object")throw new TypeError("Audio chunk metadata decoder configuration must be an object.");if(typeof i.decoderConfig.codec!="string")throw new TypeError("Audio chunk metadata decoder configuration must specify a codec string.");if(!Number.isInteger(i.decoderConfig.sampleRate)||i.decoderConfig.sampleRate<=0)throw new TypeError("Audio chunk metadata decoder configuration must specify a valid sampleRate (positive integer).");if(!Number.isInteger(i.decoderConfig.numberOfChannels)||i.decoderConfig.numberOfChannels<=0)throw new TypeError("Audio chunk metadata decoder configuration must specify a valid numberOfChannels (positive integer).");if(i.decoderConfig.description!==void 0&&!we(i.decoderConfig.description))throw new TypeError("Audio chunk metadata decoder configuration description, when defined, must be an ArrayBuffer or an ArrayBuffer view.");if(i.decoderConfig.codec==="opus"&&i.decoderConfig.description&&i.decoderConfig.description.byteLength<18)throw new TypeError("Invalid decoder description provided for Opus; must be at least 18 bytes long.")},pe=i=>{if(!i)throw new TypeError("Subtitle metadata must be provided.");if(typeof i!="object")throw new TypeError("Subtitle metadata must be an object.");if(!i.config)throw new TypeError("Subtitle metadata must include a config object.");if(typeof i.config!="object")throw new TypeError("Subtitle metadata config must be an object.");if(typeof i.config.description!="string")throw new TypeError("Subtitle metadata config description must be a string.")};var le=1e3,lr=2082844800,S=(i,t,e=!0)=>{let r=i*t;return e?Math.round(r):r},be=class extends D{constructor(e,r){super(e);this.timestampsMustStartAtZero=!0;this.#o=new L;this.#a=this.#o.createWriter();this.#c=new X(this.#a);this.#l=null;this.#n=null;this.#s=[];this.#d=Math.floor(Date.now()/1e3)+lr;this.#u=[];this.#m=1;this.#e=e.writer,this.#r=new X(this.#e),this.#i=r,this.#t=r.options.fastStart??(this.#e instanceof H?"in-memory":!1),(this.#t==="in-memory"||this.#t==="fragmented")&&(this.#e.ensureMonotonicity=!0)}#e;#r;#i;#t;#o;#a;#c;#l;#n;#s;#d;#u;#m;start(){let e=this.output.tracks.some(r=>r.type==="video"&&r.source.codec==="avc");this.#r.writeBox(Le({holdsAvc:e,fragmented:this.#t==="fragmented"})),this.#l=this.#e.getPos(),this.#t==="in-memory"?this.#n=de(!1):this.#t==="fragmented"||(this.#n=de(!0),this.#r.writeBox(this.#n)),this.#e.flush()}#b(e,r){let s=this.#s.find(n=>n.track===e);if(s)return s;me(r),d(r),d(r.decoderConfig),d(r.decoderConfig.codedWidth!==void 0),d(r.decoderConfig.codedHeight!==void 0);let o={track:e,type:"video",info:{width:r.decoderConfig.codedWidth,height:r.decoderConfig.codedHeight,decoderConfig:r.decoderConfig},timescale:e.metadata.frameRate??57600,samples:[],sampleQueue:[],timestampProcessingQueue:[],timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.#s.push(o),this.#s.sort((n,a)=>n.track.id-a.track.id),o}#T(e,r){let s=this.#s.find(n=>n.track===e);if(s)return s;he(r),d(r),d(r.decoderConfig);let o={track:e,type:"audio",info:{numberOfChannels:r.decoderConfig.numberOfChannels,sampleRate:r.decoderConfig.sampleRate,decoderConfig:r.decoderConfig},timescale:r.decoderConfig.sampleRate,samples:[],sampleQueue:[],timestampProcessingQueue:[],timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.#s.push(o),this.#s.sort((n,a)=>n.track.id-a.track.id),o}#A(e,r){let s=this.#s.find(n=>n.track===e);if(s)return s;pe(r),d(r),d(r.config);let o={track:e,type:"subtitle",info:{config:r.config},timescale:1e3,samples:[],sampleQueue:[],timestampProcessingQueue:[],timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[],lastCueEndTimestamp:0,cueQueue:[],nextSourceId:0,cueToSourceId:new WeakMap};return this.#s.push(o),this.#s.sort((n,a)=>n.track.id-a.track.id),this.validateAndNormalizeTimestamp(e,0,!0),o}addEncodedVideoChunk(e,r,s){let o=this.#b(e,s),n=new Uint8Array(r.byteLength);r.copyTo(n);let a=this.validateAndNormalizeTimestamp(o.track,r.timestamp,r.type==="key"),u=this.#h(o,n,a,(r.duration??0)/1e6,r.type);this.#p(o,u)}addEncodedAudioChunk(e,r,s){let o=this.#T(e,s),n=new Uint8Array(r.byteLength);r.copyTo(n);let a=this.validateAndNormalizeTimestamp(o.track,r.timestamp,r.type==="key"),u=this.#h(o,n,a,(r.duration??0)/1e6,r.type);this.#p(o,u)}addSubtitleCue(e,r,s){let o=this.#A(e,s);this.validateAndNormalizeTimestamp(o.track,1e6*r.timestamp,!0),e.source.codec==="webvtt"&&(o.cueQueue.push(r),this.#g(o,r.timestamp))}#g(e,r){for(;e.cueQueue.length>0;){let s=new Set([]);for(let c of e.cueQueue)d(c.timestamp<=r),d(e.lastCueEndTimestamp<=c.timestamp+c.duration),s.add(Math.max(c.timestamp,e.lastCueEndTimestamp)),s.add(c.timestamp+c.duration);let o=[...s].sort((c,m)=>c-m),n=o[0],a=o[1]??n;if(r=a)break;W.lastIndex=0;let g=W.test(m.text),E=m.timestamp+m.duration,y=e.cueToSourceId.get(m);if(y===void 0&&as.timestamp).sort((s,o)=>s-o);for(let s=0;s{if(e===a)return r.type==="key";let u=a.sampleQueue[0];return u&&u.type==="key"});o>=1&&n&&(s=!0,this.#x())}else s=o>=.5}s&&(e.currentChunk&&this.#w(e),e.currentChunk={startTimestamp:r.timestamp,samples:[],offset:null,moofOffset:null}),d(e.currentChunk),e.currentChunk.samples.push(r),e.timestampProcessingQueue.push(r)}#w(e){if(d(this.#t!=="fragmented"),!!e.currentChunk){if(e.finalizedChunks.push(e.currentChunk),this.#u.push(e.currentChunk),(e.compactlyCodedChunkTable.length===0||_(e.compactlyCodedChunkTable).samplesPerChunk!==e.currentChunk.samples.length)&&e.compactlyCodedChunkTable.push({firstChunk:e.finalizedChunks.length,samplesPerChunk:e.currentChunk.samples.length}),this.#t==="in-memory"){e.currentChunk.offset=0;return}e.currentChunk.offset=this.#e.getPos();for(let r of e.currentChunk.samples)d(r.data),this.#e.write(r.data),r.data=null;this.#e.flush()}}#k(){d(this.#t==="fragmented");for(let e of this.output.tracks)if(!e.source.closed&&!this.#s.some(r=>r.track===e))return;e:for(;;){let e=null,r=1/0;for(let o of this.#s){if(o.sampleQueue.length===0&&!o.track.source.closed)break e;o.sampleQueue.length>0&&o.sampleQueue[0].timestamp=2**32&&(u.largeSize=!0,c=this.#r.measureBox(u)+f),u.size=c,this.#r.writeBox(u)}for(let u of this.#s){u.currentChunk.offset=this.#e.getPos(),u.currentChunk.moofOffset=s;for(let f of u.currentChunk.samples)this.#e.write(f.data),f.data=null}let n=this.#e.getPos();this.#e.seek(this.#r.offsets.get(o));let a=ve(r,this.#s);this.#r.writeBox(a),this.#e.seek(n);for(let u of this.#s)u.finalizedChunks.push(u.currentChunk),this.#u.push(u.currentChunk),u.currentChunk=null;e&&this.#e.flush()}onTrackClose(e){if(e.type==="subtitle"&&e.source.codec==="webvtt"){let r=this.#s.find(s=>s.track===e);r&&this.#g(r,1/0)}this.#t==="fragmented"&&this.#k()}finalize(){for(let e of this.#s)e.type==="subtitle"&&e.track.source.codec==="webvtt"&&this.#g(e,1/0);if(this.#t==="fragmented"){for(let e of this.#s){for(let r of e.sampleQueue)this.#C(e,r);this.#f(e)}this.#x(!1)}else for(let e of this.#s)this.#f(e),this.#w(e);if(this.#t==="in-memory"){d(this.#n);let e;for(let s=0;s<2;s++){let o=G(this.#s,this.#d),n=this.#r.measureBox(o);e=this.#r.measureBox(this.#n);let a=this.#e.getPos()+n+e;for(let u of this.#u){u.offset=a;for(let{data:f}of u.samples)d(f),a+=f.byteLength,e+=f.byteLength}if(a<2**32)break;e>=2**32&&(this.#n.largeSize=!0)}let r=G(this.#s,this.#d);this.#r.writeBox(r),this.#n.size=e,this.#r.writeBox(this.#n);for(let s of this.#u)for(let o of s.samples)d(o.data),this.#e.write(o.data),o.data=null}else if(this.#t==="fragmented"){let e=this.#e.getPos(),r=Ze(this.#s);this.#r.writeBox(r);let s=this.#e.getPos()-e;this.#e.seek(this.#e.getPos()-4),this.#r.writeU32(s)}else{d(this.#n),d(this.#l!==null);let e=this.#r.offsets.get(this.#n);d(e!==void 0);let r=this.#e.getPos()-e;this.#n.size=r,this.#n.largeSize=r>=2**32,this.#r.patchBox(this.#n);let s=G(this.#s,this.#d);if(typeof this.#t=="object"){this.#e.seek(this.#l),this.#r.writeBox(s);let o=e-this.#e.getPos();this.#r.writeBox(qe(o))}else this.#r.writeBox(s)}}};var J=class{constructor(t){this.value=t}},$=class{constructor(t){this.value=t}},ee=class{constructor(t){this.value=t}};var Oe=i=>i<256?1:i<65536?2:i<1<<24?3:i<2**32?4:i<2**40?5:6,Ve=i=>i>=-64&&i<64?1:i>=-8192&&i<8192?2:i>=-(1<<20)&&i<1<<20?3:i>=-(1<<27)&&i<1<<27?4:i>=-(2**34)&&i<2**34?5:6,st=i=>{if(i<127)return 1;if(i<16383)return 2;if(i<(1<<21)-1)return 3;if(i<(1<<28)-1)return 4;if(i<2**35-1)return 5;if(i<2**42-1)return 6;throw new Error("EBML VINT size not supported "+i)};var Me=2**15,ot="https://github.com/Vanilagy/webm-muxer",nt=6,at=5,dr={avc:"V_MPEG4/ISO/AVC",hevc:"V_MPEGH/ISO/HEVC",vp8:"V_VP8",vp9:"V_VP9",av1:"V_AV1",aac:"A_AAC",opus:"A_OPUS",webvtt:"S_TEXT/WEBVTT"},cr={video:1,audio:2,subtitle:17},Te=class extends D{constructor(e,r){super(e);this.timestampsMustStartAtZero=!1;this.#i=new Uint8Array(8);this.#t=new DataView(this.#i.buffer);this.offsets=new WeakMap;this.dataOffsets=new WeakMap;this.#o=[];this.#a=null;this.#c=null;this.#l=null;this.#n=null;this.#s=null;this.#d=null;this.#u=null;this.#m=null;this.#b=new Set;this.#T=0;this.#e=e.writer,this.#r=r,this.#r.options.streamable&&(this.#e.ensureMonotonicity=!0)}#e;#r;#i;#t;#o;#a;#c;#l;#n;#s;#d;#u;#m;#b;#T;#A(e){this.#t.setUint8(0,e),this.#e.write(this.#i.subarray(0,1))}#g(e){this.#t.setFloat32(0,e,!1),this.#e.write(this.#i.subarray(0,4))}#h(e){this.#t.setFloat64(0,e,!1),this.#e.write(this.#i)}#f(e,r=Oe(e)){let s=0;switch(r){case 6:this.#t.setUint8(s++,e/2**40|0);case 5:this.#t.setUint8(s++,e/2**32|0);case 4:this.#t.setUint8(s++,e>>24);case 3:this.#t.setUint8(s++,e>>16);case 2:this.#t.setUint8(s++,e>>8);case 1:this.#t.setUint8(s++,e);break;default:throw new Error("Bad UINT size "+r)}this.#e.write(this.#i.subarray(0,s))}#p(e,r=Ve(e)){e<0&&(e+=2**(r*8)),this.#f(e,r)}writeEBMLVarInt(e,r=st(e)){let s=0;switch(r){case 1:this.#t.setUint8(s++,128|e);break;case 2:this.#t.setUint8(s++,64|e>>8),this.#t.setUint8(s++,e);break;case 3:this.#t.setUint8(s++,32|e>>16),this.#t.setUint8(s++,e>>8),this.#t.setUint8(s++,e);break;case 4:this.#t.setUint8(s++,16|e>>24),this.#t.setUint8(s++,e>>16),this.#t.setUint8(s++,e>>8),this.#t.setUint8(s++,e);break;case 5:this.#t.setUint8(s++,8|e/2**32&7),this.#t.setUint8(s++,e>>24),this.#t.setUint8(s++,e>>16),this.#t.setUint8(s++,e>>8),this.#t.setUint8(s++,e);break;case 6:this.#t.setUint8(s++,4|e/2**40&3),this.#t.setUint8(s++,e/2**32|0),this.#t.setUint8(s++,e>>24),this.#t.setUint8(s++,e>>16),this.#t.setUint8(s++,e>>8),this.#t.setUint8(s++,e);break;default:throw new Error("Bad EBML VINT size "+r)}this.#e.write(this.#i.subarray(0,s))}#C(e){this.#e.write(new Uint8Array(e.split("").map(r=>r.charCodeAt(0))))}writeEBML(e){if(e!==null){if(e instanceof Uint8Array)this.#e.write(e);else if(Array.isArray(e))for(let r of e)this.writeEBML(r);else if(this.offsets.set(e,this.#e.getPos()),this.#f(e.id),Array.isArray(e.data)){let r=this.#e.getPos(),s=e.size===-1?1:e.size??4;e.size===-1?this.#A(255):this.#e.seek(this.#e.getPos()+s);let o=this.#e.getPos();if(this.dataOffsets.set(e,o),this.writeEBML(e.data),e.size!==-1){let n=this.#e.getPos()-o,a=this.#e.getPos();this.#e.seek(r),this.writeEBMLVarInt(n,s),this.#e.seek(a)}}else if(typeof e.data=="number"){let r=e.size??Oe(e.data);this.writeEBMLVarInt(r),this.#f(e.data,r)}else if(typeof e.data=="string")this.writeEBMLVarInt(e.data.length),this.#C(e.data);else if(e.data instanceof Uint8Array)this.writeEBMLVarInt(e.data.byteLength,e.size),this.#e.write(e.data);else if(e.data instanceof J)this.writeEBMLVarInt(4),this.#g(e.data.value);else if(e.data instanceof $)this.writeEBMLVarInt(8),this.#h(e.data.value);else if(e.data instanceof ee){let r=e.size??Ve(e.data.value);this.writeEBMLVarInt(r),this.#p(e.data.value,r)}}}beforeTrackAdd(e){if(this.#r instanceof j)if(e.type==="video"){if(!["vp8","vp9","av1"].includes(e.source.codec))throw new Error("WebM only supports VP8, VP9 and AV1 as video codecs. Switching to MKV removes this restriction.")}else if(e.type==="audio"){if(!["opus","vorbis"].includes(e.source.codec))throw new Error("WebM only supports Opus and Vorbis as audio codecs. Switching to MKV removes this restriction.")}else if(e.type==="subtitle"){if(e.source.codec!=="webvtt")throw new Error("WebM only supports WebVTT as subtitle codec. Switching to MKV removes this restriction.")}else throw new Error("WebM only supports video, audio and subtitle tracks. Switching to MKV removes this restriction.")}start(){this.#w(),this.#r.options.streamable||this.#k(),this.#x(),this.#I(),this.#e.flush()}#w(){let e={id:440786851,data:[{id:17030,data:1},{id:17143,data:1},{id:17138,data:4},{id:17139,data:8},{id:17026,data:this.#r instanceof j?"webm":"matroska"},{id:17031,data:2},{id:17029,data:2}]};this.writeEBML(e)}#k(){let e=new Uint8Array([28,83,187,107]),r=new Uint8Array([21,73,169,102]),s=new Uint8Array([22,84,174,107]),o={id:290298740,data:[{id:19899,data:[{id:21419,data:e},{id:21420,size:5,data:0}]},{id:19899,data:[{id:21419,data:r},{id:21420,size:5,data:0}]},{id:19899,data:[{id:21419,data:s},{id:21420,size:5,data:0}]}]};this.#l=o}#x(){let e={id:17545,data:new $(0)};this.#s=e;let r={id:357149030,data:[{id:2807729,data:1e6},{id:19840,data:ot},{id:22337,data:ot},this.#r.options.streamable?null:e]};this.#c=r}#E(){let e={id:374648427,data:[]};this.#n=e;for(let r of this.#o)e.data.push({id:174,data:[{id:215,data:r.track.id},{id:29637,data:r.track.id},{id:131,data:cr[r.type]},{id:134,data:dr[r.track.source.codec]},...r.type==="video"?[r.info.decoderConfig.description?{id:25506,data:U(r.info.decoderConfig.description)}:null,r.track.metadata.frameRate?{id:2352003,data:1e9/r.track.metadata.frameRate}:null,{id:224,data:[{id:176,data:r.info.width},{id:186,data:r.info.height},(()=>{if(r.info.decoderConfig.colorSpace){let s=r.info.decoderConfig.colorSpace;return oe(s)?{id:21936,data:[{id:21937,data:F[s.matrix]},{id:21946,data:N[s.transfer]},{id:21947,data:B[s.primaries]},{id:21945,data:[1,2][Number(s.fullRange)]}]}:null}return null})()]}]:[],...r.type==="audio"?[r.info.decoderConfig.description?{id:25506,data:U(r.info.decoderConfig.description)}:null,{id:225,data:[{id:181,data:new J(r.info.sampleRate)},{id:159,data:r.info.numberOfChannels}]}]:[],...r.type==="subtitle"?[{id:25506,data:x.encode(r.info.config.description)}]:[]]})}#O(){let e={id:408125543,size:this.#r.options.streamable?-1:nt,data:[this.#r.options.streamable?null:this.#l,this.#c,this.#n]};this.#a=e,this.writeEBML(e)}#I(){this.#d={id:475249515,data:[]}}get#y(){return d(this.#a),this.dataOffsets.get(this.#a)}#z(e,r){let s=this.#o.find(n=>n.track===e);if(s)return s;me(r),d(r),d(r.decoderConfig),d(r.decoderConfig.codedWidth!==void 0),d(r.decoderConfig.codedHeight!==void 0);let o={track:e,type:"video",info:{width:r.decoderConfig.codedWidth,height:r.decoderConfig.codedHeight,decoderConfig:r.decoderConfig},chunkQueue:[],lastWrittenMsTimestamp:null};return this.#o.push(o),this.#o.sort((n,a)=>n.track.id-a.track.id),o}#U(e,r){let s=this.#o.find(n=>n.track===e);if(s)return s;he(r),d(r),d(r.decoderConfig);let o={track:e,type:"audio",info:{numberOfChannels:r.decoderConfig.numberOfChannels,sampleRate:r.decoderConfig.sampleRate,decoderConfig:r.decoderConfig},chunkQueue:[],lastWrittenMsTimestamp:null};return this.#o.push(o),this.#o.sort((n,a)=>n.track.id-a.track.id),o}#P(e,r){let s=this.#o.find(n=>n.track===e);if(s)return s;pe(r),d(r),d(r.config);let o={track:e,type:"subtitle",info:{config:r.config},chunkQueue:[],lastWrittenMsTimestamp:null};return this.#o.push(o),this.#o.sort((n,a)=>n.track.id-a.track.id),o}addEncodedVideoChunk(e,r,s){let o=this.#z(e,s),n=new Uint8Array(r.byteLength);r.copyTo(n);let a=this.validateAndNormalizeTimestamp(o.track,r.timestamp,r.type==="key"),u=this.#v(n,a,(r.duration??0)/1e6,r.type);e.source.codec==="vp9"&&this.#R(o,u),o.chunkQueue.push(u),this.#S()}addEncodedAudioChunk(e,r,s){let o=this.#U(e,s),n=new Uint8Array(r.byteLength);r.copyTo(n);let a=this.validateAndNormalizeTimestamp(o.track,r.timestamp,r.type==="key"),u=this.#v(n,a,(r.duration??0)/1e6,r.type);o.chunkQueue.push(u),this.#S()}addSubtitleCue(e,r,s){let o=this.#P(e,s),n=this.validateAndNormalizeTimestamp(o.track,1e6*r.timestamp,!0),a=r.text,u=Math.floor(n*1e3);W.lastIndex=0,a=a.replace(W,g=>{let y=ae(g.slice(1,-1))-u;return`<${ue(y)}>`});let f=x.encode(a),c=`${r.settings??""}
+${r.identifier??""}
+${r.notes??""}`,m=this.#v(f,n,r.duration,"key",c.trim()?x.encode(c):null);o.chunkQueue.push(m),this.#S()}#S(){for(let e of this.output.tracks)if(!e.source.closed&&!this.#o.some(r=>r.track===e))return;e:for(;;){let e=null,r=1/0;for(let o of this.#o){if(o.chunkQueue.length===0&&!o.track.source.closed)break e;o.chunkQueue.length>0&&o.chunkQueue[0].timestamp=2&&s++;let f={rgb:7,bt709:2,bt470bg:1,smpte170m:3}[e.info.decoderConfig.colorSpace.matrix];He(r.data,s+0,s+3,f)}#v(e,r,s,o,n=null){return{data:e,type:o,timestamp:r,duration:s,additions:n}}#V(e,r){this.#a||(this.#E(),this.#O());let s=Math.floor(1e3*r.timestamp),o=this.#o.every(m=>{if(m.track.source.closed)return!0;if(e===m)return r.type==="key";let g=m.chunkQueue[0];return g&&g.type==="key"});(!this.#u||o&&s-this.#m>=1e3)&&this.#_(s);let n=s-this.#m;if(n<0)return;if(n>=Me)throw new Error(`Current Matroska cluster exceeded its maximum allowed length of ${Me} milliseconds. In order to produce a correct WebM file, you must pass in a key frame at least every ${Me} milliseconds.`);let u=new Uint8Array(4),f=new DataView(u.buffer);f.setUint8(0,128|e.track.id),f.setInt16(1,n,!1);let c=Math.floor(1e3*r.duration);if(c===0&&!r.additions){f.setUint8(3,+(r.type==="key")<<7);let m={id:163,data:[u,r.data]};this.writeEBML(m)}else{let m={id:160,data:[{id:161,data:[u,r.data]},r.type==="delta"?{id:251,data:new ee(e.lastWrittenMsTimestamp-s)}:null,r.additions?{id:30113,data:[{id:166,data:[{id:165,data:r.additions},{id:238,data:1}]}]}:null,c>0?{id:155,data:c}:null]};this.writeEBML(m)}this.#T=Math.max(this.#T,s+c),e.lastWrittenMsTimestamp=s,this.#b.add(e)}#_(e){this.#u&&!this.#r.options.streamable&&this.#M(),this.#u={id:524531317,size:this.#r.options.streamable?-1:at,data:[{id:231,data:e}]},this.writeEBML(this.#u),this.#m=e,this.#b.clear()}#M(){d(this.#u);let e=this.#e.getPos()-this.dataOffsets.get(this.#u),r=this.#e.getPos();this.#e.seek(this.offsets.get(this.#u)+4),this.writeEBMLVarInt(e,at),this.#e.seek(r);let s=this.offsets.get(this.#u)-this.#y;d(this.#d),this.#d.data.push({id:187,data:[{id:179,data:this.#m},...[...this.#b].map(o=>({id:183,data:[{id:247,data:o.track.id},{id:241,data:s}]}))]})}onTrackClose(){this.#S()}finalize(){this.#a||(this.#E(),this.#O());for(let e of this.#o)for(;e.chunkQueue.length>0;)this.#V(e,e.chunkQueue.shift());if(!this.#r.options.streamable&&this.#u&&this.#M(),d(this.#d),this.writeEBML(this.#d),!this.#r.options.streamable){let e=this.#e.getPos(),r=this.#e.getPos()-this.#y;this.#e.seek(this.offsets.get(this.#a)+4),this.writeEBMLVarInt(r,nt),this.#s.data=new $(this.#T),this.#e.seek(this.offsets.get(this.#s)),this.writeEBML(this.#s),this.#l.data[0].data[1].data=this.offsets.get(this.#d)-this.#y,this.#l.data[1].data[1].data=this.offsets.get(this.#c)-this.#y,this.#l.data[2].data[1].data=this.offsets.get(this.#n)-this.#y,this.#e.seek(this.offsets.get(this.#l)),this.writeEBML(this.#l),this.#e.seek(e)}}};var Q=class{},Ie=class extends Q{constructor(e={}){if(!e||typeof e!="object")throw new TypeError("options must be an object.");if(e.fastStart!==void 0&&![!1,"in-memory","fragmented"].includes(e.fastStart))throw new TypeError('options.fastStart, when provided, must be false, "in-memory", or "fragmented".');super();this.options=e}createMuxer(e){return new be(e,this)}},ge=class extends Q{constructor(e={}){if(!e||typeof e!="object")throw new TypeError("options must be an object.");if(e.streamable!==void 0&&typeof e.streamable!="boolean")throw new TypeError("options.streamable, when provided, must be a boolean.");super();this.options=e}createMuxer(e){return new Te(e,this)}},j=class extends ge{};var Ce=["avc","hevc","vp8","vp9","av1"],ye=["aac","opus"],ut=["webvtt"],te=class{constructor(){this.connectedTrack=null;this.closed=!1;this.offsetTimestamps=!1}ensureValidDigest(){if(!this.connectedTrack)throw new Error("Cannot call digest without connecting the source to an output track.");if(!this.connectedTrack.output.started)throw new Error("Cannot call digest before output has been started.");if(this.connectedTrack.output.finalizing)throw new Error("Cannot call digest after output has started finalizing.");if(this.closed)throw new Error("Cannot call digest after source has been closed.")}start(){}async flush(){}close(){if(this.closed)throw new Error("Source already closed.");if(!this.connectedTrack)throw new Error("Cannot call close without connecting the source to an output track.");if(!this.connectedTrack.output.started)throw new Error("Cannot call close before output has been started.");this.closed=!0,!this.connectedTrack.output.finalizing&&this.connectedTrack.output.muxer.onTrackClose(this.connectedTrack)}},V=class extends te{constructor(e){super();this.connectedTrack=null;if(!Ce.includes(e))throw new TypeError(`Invalid video codec '${e}'. Must be one of: ${Ce.join(", ")}.`);this.codec=e}},ze=class extends V{constructor(t){super(t)}digest(t,e){if(!(t instanceof EncodedVideoChunk))throw new TypeError("chunk must be an EncodedVideoChunk.");this.ensureValidDigest(),this.connectedTrack?.output.muxer.addEncodedVideoChunk(this.connectedTrack,t,e)}},fr=5,mr=i=>{if(!i||typeof i!="object")throw new TypeError("Codec config must be an object.");if(!Ce.includes(i.codec))throw new TypeError(`Invalid video codec '${i.codec}'. Must be one of: ${Ce.join(", ")}.`);if(!Number.isInteger(i.bitrate)||i.bitrate<=0)throw new TypeError("config.bitrate must be a positive integer.");if(i.latencyMode!==void 0&&["quality","realtime"].includes(i.latencyMode))throw new TypeError("config.latencyMode, when provided, must be 'quality' or 'realtime'.")},re=class{constructor(t,e){this.source=t;this.codecConfig=e;this.encoder=null;this.lastMultipleOfKeyFrameInterval=-1;this.lastWidth=null;this.lastHeight=null;mr(e)}digest(t){if(this.source.ensureValidDigest(),this.lastWidth!==null&&this.lastHeight!==null){if(t.codedWidth!==this.lastWidth||t.codedHeight!==this.lastHeight)throw new Error(`Video frame size must remain constant. Expected ${this.lastWidth}x${this.lastHeight}, got ${t.codedWidth}x${t.codedHeight}.`)}else this.lastWidth=t.codedWidth,this.lastHeight=t.codedHeight;this.ensureEncoder(t),d(this.encoder);let e=Math.floor(t.timestamp/1e6/fr);this.encoder.encode(t,{keyFrame:e!==this.lastMultipleOfKeyFrameInterval}),this.lastMultipleOfKeyFrameInterval=e}ensureEncoder(t){this.encoder||(this.encoder=new VideoEncoder({output:(e,r)=>this.source.connectedTrack?.output.muxer.addEncodedVideoChunk(this.source.connectedTrack,e,r),error:e=>console.error("Video encode error:",e)}),this.encoder.configure({codec:rt(this.codecConfig.codec,t.codedWidth,t.codedHeight),width:t.codedWidth,height:t.codedHeight,bitrate:this.codecConfig.bitrate,framerate:this.source.connectedTrack?.metadata.frameRate,latencyMode:this.codecConfig.latencyMode}))}async flush(){return this.encoder?.flush()}},Ue=class extends V{constructor(t){super(t.codec),this.encoder=new re(this,t)}digest(t){if(!(t instanceof VideoFrame))throw new TypeError("videoFrame must be a VideoFrame.");this.encoder.digest(t)}flush(){return this.encoder.flush()}},Pe=class extends V{constructor(e,r){if(!(e instanceof HTMLCanvasElement))throw new TypeError("canvas must be an HTMLCanvasElement.");super(r.codec);this.canvas=e;this.encoder=new re(this,r)}digest(e,r=0){if(!Number.isFinite(e)||e<0)throw new TypeError("timestamp must be a non-negative number.");if(!Number.isFinite(r)||r<0)throw new TypeError("duration must be a non-negative number.");let s=new VideoFrame(this.canvas,{timestamp:Math.round(1e6*e),duration:Math.round(1e6*r),alpha:"discard"});this.encoder.digest(s),s.close()}flush(){return this.encoder.flush()}},Re=class extends V{constructor(e,r){if(!(e instanceof MediaStreamTrack)||e.kind!=="video")throw new TypeError("track must be a video MediaStreamTrack.");super(r.codec);this.track=e;this.abortController=null;this.offsetTimestamps=!0;this.encoder=new re(this,r)}start(){this.abortController=new AbortController;let e=new MediaStreamTrackProcessor({track:this.track}),r=new WritableStream({write:s=>{this.encoder.digest(s),s.close()}});e.readable.pipeTo(r,{signal:this.abortController.signal}).catch(s=>{s instanceof DOMException&&s.name==="AbortError"||console.error("Pipe error:",s)})}async flush(){this.abortController&&(this.abortController.abort(),this.abortController=null),await this.encoder.flush()}},M=class extends te{constructor(e){super();this.connectedTrack=null;if(!ye.includes(e))throw new TypeError(`Invalid audio codec '${e}'. Must be one of: ${ye.join(", ")}.`);this.codec=e}},_e=class extends M{constructor(t){super(t)}digest(t,e){if(!(t instanceof EncodedAudioChunk))throw new TypeError("chunk must be an EncodedAudioChunk.");this.ensureValidDigest(),this.connectedTrack?.output.muxer.addEncodedAudioChunk(this.connectedTrack,t,e)}},hr=i=>{if(!i||typeof i!="object")throw new TypeError("Codec config must be an object.");if(!ye.includes(i.codec))throw new TypeError(`Invalid audio codec '${i.codec}'. Must be one of: ${ye.join(", ")}.`);if(!Number.isInteger(i.bitrate)||i.bitrate<=0)throw new TypeError("config.bitrate must be a positive integer.")},ie=class{constructor(t,e){this.source=t;this.codecConfig=e;this.encoder=null;this.lastNumberOfChannels=null;this.lastSampleRate=null;hr(e)}digest(t){if(this.source.ensureValidDigest(),this.lastNumberOfChannels!==null&&this.lastSampleRate!==null){if(t.numberOfChannels!==this.lastNumberOfChannels||t.sampleRate!==this.lastSampleRate)throw new Error(`Audio parameters must remain constant. Expected ${this.lastNumberOfChannels} channels at ${this.lastSampleRate} Hz, got ${t.numberOfChannels} channels at ${t.sampleRate} Hz.`)}else this.lastNumberOfChannels=t.numberOfChannels,this.lastSampleRate=t.sampleRate;this.ensureEncoder(t),d(this.encoder),this.encoder.encode(t)}ensureEncoder(t){this.encoder||(this.encoder=new AudioEncoder({output:(e,r)=>this.source.connectedTrack?.output.muxer.addEncodedAudioChunk(this.source.connectedTrack,e,r),error:e=>console.error("Audio encode error:",e)}),this.encoder.configure({codec:it(this.codecConfig.codec,t.numberOfChannels,t.sampleRate),numberOfChannels:t.numberOfChannels,sampleRate:t.sampleRate,bitrate:this.codecConfig.bitrate}))}async flush(){return this.encoder?.flush()}},Be=class extends M{constructor(t){super(t.codec),this.encoder=new ie(this,t)}digest(t){if(!(t instanceof AudioData))throw new TypeError("audioData must be an AudioData.");this.encoder.digest(t)}flush(){return this.encoder.flush()}},Ne=class extends M{constructor(e){super(e.codec);this.accumulatedFrameCount=0;this.encoder=new ie(this,e)}digest(e){if(!(e instanceof AudioBuffer))throw new TypeError("audioBuffer must be an AudioBuffer.");let r=e.numberOfChannels,s=e.sampleRate,o=e.length,n=new Float32Array(r*o);for(let u=0;u{this.encoder.digest(s),s.close()}});e.readable.pipeTo(r,{signal:this.abortController.signal}).catch(s=>{s instanceof DOMException&&s.name==="AbortError"||console.error("Pipe error:",s)})}async flush(){this.abortController&&(this.abortController.abort(),this.abortController=null),await this.encoder.flush()}},se=class extends te{constructor(e){super();this.connectedTrack=null;if(!ut.includes(e))throw new TypeError(`Invalid subtitle codec '${e}'. Must be one of: ${ut.join(", ")}.`);this.codec=e}},We=class extends se{constructor(t){super(t),this.parser=new ne({codec:t,output:(e,r)=>this.connectedTrack?.output.muxer.addSubtitleCue(this.connectedTrack,e,r),error:e=>console.error("Subtitle parse error:",e)})}digest(t){if(typeof t!="string")throw new TypeError("text must be a string.");this.ensureValidDigest(),this.parser.parse(t)}};var De=class{constructor(t){this.tracks=[];this.started=!1;this.finalizing=!1;if(!t||typeof t!="object")throw new TypeError("options must be an object.");if(!(t.format instanceof Q))throw new TypeError("options.format must be an OutputFormat.");if(!(t.target instanceof O))throw new TypeError("options.target must be a Target.");if(t.target.output)throw new Error("Target is already used for another output.");t.target.output=this,this.writer=t.target.createWriter(),this.muxer=t.format.createMuxer(this)}addVideoTrack(t,e={}){if(!(t instanceof V))throw new TypeError("source must be a VideoSource.");if(!e||typeof e!="object")throw new TypeError("metadata must be an object.");if(typeof e.rotation=="number"&&![0,90,180,270].includes(e.rotation))throw new TypeError(`Invalid video rotation: ${e.rotation}. Has to be 0, 90, 180 or 270.`);if(Array.isArray(e.rotation)&&(e.rotation.length!==9||e.rotation.some(r=>!Number.isFinite(r))))throw new TypeError(`Invalid video transformation matrix: ${e.rotation.join()}`);if(e.frameRate!==void 0&&(!Number.isInteger(e.frameRate)||e.frameRate<=0))throw new TypeError(`Invalid video frame rate: ${e.frameRate}. Must be a positive integer.`);this.addTrack("video",t,e)}addAudioTrack(t,e={}){if(!(t instanceof M))throw new TypeError("source must be an AudioSource.");if(!e||typeof e!="object")throw new TypeError("metadata must be an object.");this.addTrack("audio",t,e)}addSubtitleTrack(t,e={}){if(!(t instanceof se))throw new TypeError("source must be a SubtitleSource.");if(!e||typeof e!="object")throw new TypeError("metadata must be an object.");this.addTrack("subtitle",t,e)}addTrack(t,e,r){if(this.started)throw new Error("Cannot add track after output has started.");if(e.connectedTrack)throw new Error("Source is already used for a track.");let s={id:this.tracks.length+1,output:this,type:t,source:e,metadata:r};this.muxer.beforeTrackAdd(s),this.tracks.push(s),e.connectedTrack=s}start(){if(this.started)throw new Error("Output already started.");this.started=!0,this.muxer.start();for(let t of this.tracks)t.source.start()}async finalize(){if(this.finalizing)throw new Error("Cannot call finalize twice.");this.finalizing=!0;let t=this.tracks.map(e=>e.source.flush());await Promise.all(t),this.muxer.finalize(),this.writer.flush(),this.writer.finalize()}};export{L as ArrayBufferTarget,Ne as AudioBufferSource,Be as AudioDataSource,Pe as CanvasSource,_e as EncodedAudioChunkSource,ze as EncodedVideoChunkSource,Ee as FileSystemWritableFileStreamTarget,Fe as MediaStreamAudioTrackSource,Re as MediaStreamVideoTrackSource,ge as MkvOutputFormat,Ie as Mp4OutputFormat,De as Output,q as StreamTarget,O as Target,We as TextSubtitleSource,Ue as VideoFrameSource,j as WebMOutputFormat};
diff --git a/dist/metamuxer.mjs b/dist/metamuxer.mjs
index 1002f73..abe00fe 100644
--- a/dist/metamuxer.mjs
+++ b/dist/metamuxer.mjs
@@ -1,66 +1,3 @@
-// src/output.ts
-var Output = class {
- constructor(options) {
- this.tracks = [];
- this.started = false;
- this.finalizing = false;
- if (options.target.output) {
- throw new Error("Target is already used for another output.");
- }
- options.target.output = this;
- this.writer = options.target.createWriter();
- this.muxer = options.format.createMuxer(this);
- }
- addVideoTrack(source, metadata = {}) {
- this.addTrack("video", source, metadata);
- }
- addAudioTrack(source, metadata = {}) {
- this.addTrack("audio", source, metadata);
- }
- addSubtitleTrack(source, metadata = {}) {
- this.addTrack("subtitle", source, metadata);
- }
- addTrack(type, source, metadata) {
- if (this.started) {
- throw new Error("Cannot add track after output has started.");
- }
- if (source.connectedTrack) {
- throw new Error("Source is already used for a track.");
- }
- const track = {
- id: this.tracks.length + 1,
- output: this,
- type,
- source,
- metadata
- };
- this.muxer.beforeTrackAdd(track);
- this.tracks.push(track);
- source.connectedTrack = track;
- }
- start() {
- if (this.started) {
- throw new Error("Output already started.");
- }
- this.started = true;
- this.muxer.start();
- for (const track of this.tracks) {
- track.source.start();
- }
- }
- async finalize() {
- if (this.finalizing) {
- throw new Error("Cannot call finalize twice.");
- }
- this.finalizing = true;
- const promises = this.tracks.map((x) => x.source.flush());
- await Promise.all(promises);
- this.muxer.finalize();
- this.writer.flush();
- this.writer.finalize();
- }
-};
-
// src/misc.ts
function assert(x) {
if (!x) {
@@ -132,6 +69,9 @@ var MATRIX_COEFFICIENTS_MAP = {
var colorSpaceIsComplete = (colorSpace) => {
return !!colorSpace && !!colorSpace.primaries && !!colorSpace.transfer && !!colorSpace.matrix && colorSpace.fullRange !== void 0;
};
+var isAllowSharedBufferSource = (x) => {
+ return x instanceof ArrayBuffer || typeof SharedArrayBuffer !== "undefined" && x instanceof SharedArrayBuffer || ArrayBuffer.isView(x) && !(x instanceof DataView);
+};
// src/subtitles.ts
var cueBlockHeaderRegex = /(?:(.+?)\n)?((?:\d{2}:)?\d{2}:\d{2}.\d{3})\s+-->\s+((?:\d{2}:)?\d{2}:\d{2}.\d{3})/g;
@@ -335,6 +275,22 @@ var fixed_2_30 = (value) => {
view.setInt32(0, 2 ** 30 * value, false);
return [bytes[0], bytes[1], bytes[2], bytes[3]];
};
+var variableUnsignedInt = (value, byteLength) => {
+ let bytes2 = [];
+ let remaining = value;
+ do {
+ let byte = remaining & 127;
+ remaining >>= 7;
+ if (bytes2.length > 0) {
+ byte |= 128;
+ }
+ bytes2.push(byte);
+ if (byteLength !== void 0) {
+ byteLength--;
+ }
+ } while (remaining > 0 || byteLength);
+ return bytes2.reverse();
+};
var ascii = (text, nullTerminated = false) => {
let bytes2 = Array(text.length).fill(null).map((_, i) => text.charCodeAt(i));
if (nullTerminated) bytes2.push(0);
@@ -427,7 +383,7 @@ var mvhd = (creationTime, trackDatas) => {
return lastSample.timestamp + lastSample.duration;
})
), GLOBAL_TIMESCALE);
- let nextTrackId = Math.max(...trackDatas.map((x) => x.track.id)) + 1;
+ let nextTrackId = Math.max(0, ...trackDatas.map((x) => x.track.id)) + 1;
let needsU64 = !isU32(creationTime) || !isU32(duration);
let u32OrU64 = needsU64 ? u64 : u32;
return fullBox("mvhd", +needsU64, 0, [
@@ -678,7 +634,7 @@ var avcC = (trackData) => trackData.info.decoderConfig && box("avcC", [
...toUint8Array(trackData.info.decoderConfig.description)
]);
var hvcC = (trackData) => trackData.info.decoderConfig && box("hvcC", [
- // For HEVC, description is a HEVCDecoderConfigurationRecord, so nothing else to do here
+ // For HEVC, description is an HEVCDecoderConfigurationRecord, so nothing else to do here
...toUint8Array(trackData.info.decoderConfig.description)
]);
var vpcC = (trackData) => {
@@ -686,9 +642,7 @@ var vpcC = (trackData) => {
return null;
}
let decoderConfig = trackData.info.decoderConfig;
- if (!decoderConfig.colorSpace) {
- throw new Error(`'colorSpace' is required in the decoder config for VP8/VP9.`);
- }
+ assert(decoderConfig.colorSpace);
let parts = decoderConfig.codec.split(".");
let profile = Number(parts[1]);
let level = Number(parts[2]);
@@ -752,51 +706,55 @@ var soundSampleDescription = (compressionType, trackData) => box(compressionType
]);
var esds = (trackData) => {
let description = toUint8Array(trackData.info.decoderConfig.description ?? new ArrayBuffer(0));
- return fullBox("esds", 0, 0, [
- // https://stackoverflow.com/a/54803118
- u32(58753152),
- // TAG(3) = Object Descriptor ([2])
- u8(32 + description.byteLength),
- // length of this OD (which includes the next 2 tags)
- u16(1),
- // ES_ID = 1
- u8(0),
- // flags etc = 0
- u32(75530368),
- // TAG(4) = ES Descriptor ([2]) embedded in above OD
- u8(18 + description.byteLength),
- // length of this ESD
- u8(64),
+ let bytes2 = [
+ ...description
+ ];
+ bytes2 = [
+ ...u8(64),
// MPEG-4 Audio
- u8(21),
+ ...u8(21),
// stream type(6bits)=5 audio, flags(2bits)=1
- u24(0),
+ ...u24(0),
// 24bit buffer size
- u32(130071),
+ ...u32(0),
// max bitrate
- u32(130071),
+ ...u32(0),
// avg bitrate
- u32(92307584),
+ ...u8(5),
// TAG(5) = ASC ([2],[3]) embedded in above OD
- u8(description.byteLength),
- // length
- ...description,
- u32(109084800),
+ ...variableUnsignedInt(bytes2.length),
+ ...bytes2
+ ];
+ bytes2 = [
+ ...u16(1),
+ // ES_ID = 1
+ ...u8(0),
+ // flags etc = 0
+ ...u8(4),
+ // TAG(4) = ES Descriptor ([2]) embedded in above OD
+ ...variableUnsignedInt(bytes2.length),
+ ...bytes2,
+ ...u8(6),
// TAG(6)
- u8(1),
+ ...u8(1),
// length
- u8(2)
+ ...u8(2)
// data
- ]);
+ ];
+ bytes2 = [
+ ...u8(3),
+ // TAG(3) = Object Descriptor ([2])
+ ...variableUnsignedInt(bytes2.length),
+ ...bytes2
+ ];
+ return fullBox("esds", 0, 0, bytes2);
};
var dOps = (trackData) => {
let preskip = 3840;
let gain = 0;
const description = trackData.info.decoderConfig?.description;
if (description) {
- if (description.byteLength < 18) {
- throw new TypeError("Invalid decoder description provided for Opus; must be at least 18 bytes long.");
- }
+ assert(description.byteLength < 18);
const view2 = ArrayBuffer.isView(description) ? new DataView(description.buffer, description.byteOffset, description.byteLength) : new DataView(description);
preskip = view2.getUint16(10, true);
gain = view2.getInt16(14, true);
@@ -1144,10 +1102,79 @@ var Muxer = class {
}
};
-// src/writer.ts
-var Writer = class {
+// src/target.ts
+var Target = class {
+ constructor() {
+ this.output = null;
+ }
};
-var ArrayBufferTargetWriter = class extends Writer {
+var ArrayBufferTarget = class extends Target {
+ constructor() {
+ super(...arguments);
+ this.buffer = null;
+ }
+ createWriter() {
+ return new ArrayBufferTargetWriter(this);
+ }
+};
+var StreamTarget = class extends Target {
+ constructor(options) {
+ super();
+ this.options = options;
+ if (typeof options !== "object") {
+ throw new TypeError("StreamTarget requires an options object to be passed to its constructor.");
+ }
+ if (options.onData) {
+ if (typeof options.onData !== "function") {
+ throw new TypeError("options.onData, when provided, must be a function.");
+ }
+ if (options.onData.length < 2) {
+ throw new TypeError(
+ "options.onData, when provided, must be a function that takes in at least two arguments (data and position). Ignoring the position argument, which specifies the byte offset at which the data is to be written, can lead to broken outputs."
+ );
+ }
+ }
+ if (options.chunked !== void 0 && typeof options.chunked !== "boolean") {
+ throw new TypeError("options.chunked, when provided, must be a boolean.");
+ }
+ if (options.chunkSize !== void 0 && (!Number.isInteger(options.chunkSize) || options.chunkSize <= 0)) {
+ throw new TypeError("options.chunkSize, when provided, must be a positive integer.");
+ }
+ }
+ createWriter() {
+ return this.options.chunked ? new ChunkedStreamTargetWriter(this) : new StreamTargetWriter(this);
+ }
+};
+var FileSystemWritableFileStreamTarget = class extends Target {
+ constructor(stream, options) {
+ super();
+ this.stream = stream;
+ this.options = options;
+ if (!(stream instanceof FileSystemWritableFileStream)) {
+ throw new TypeError("FileSystemWritableFileStreamTarget requires a FileSystemWritableFileStream instance.");
+ }
+ if (options !== void 0 && typeof options !== "object") {
+ throw new TypeError("FileSystemWritableFileStreamTarget's options, when provided, must be an object.");
+ }
+ if (options) {
+ if (options.chunkSize !== void 0 && (!Number.isInteger(options.chunkSize) || options.chunkSize <= 0)) {
+ throw new TypeError("options.chunkSize, when provided, must be a positive integer");
+ }
+ }
+ }
+ createWriter() {
+ return new FileSystemWritableFileStreamTargetWriter(this);
+ }
+};
+
+// src/writer.ts
+var Writer2 = class {
+ constructor() {
+ /** Setting this to true will cause the writer to ensure data is written in a strictly monotonic, streamable way. */
+ this.ensureMonotonicity = false;
+ }
+};
+var ArrayBufferTargetWriter = class extends Writer2 {
#pos = 0;
#target;
#buffer = new ArrayBuffer(2 ** 16);
@@ -1189,10 +1216,11 @@ var ArrayBufferTargetWriter = class extends Writer {
return this.#bytes.slice(start, end);
}
};
-var StreamTargetWriter = class extends Writer {
+var StreamTargetWriter = class extends Writer2 {
#pos = 0;
#target;
#sections = [];
+ #lastFlushEnd = 0;
constructor(target) {
super();
this.#target = target;
@@ -1237,7 +1265,11 @@ var StreamTargetWriter = class extends Writer {
chunk.data.set(section.data, section.start - chunk.start);
}
}
+ if (this.ensureMonotonicity && chunk.start !== this.#lastFlushEnd) {
+ throw new Error("Internal error: Monotonicity violation.");
+ }
this.#target.options.onData?.(chunk.data, chunk.start);
+ this.#lastFlushEnd = chunk.start + chunk.data.byteLength;
}
this.#sections.length = 0;
}
@@ -1246,7 +1278,7 @@ var StreamTargetWriter = class extends Writer {
};
var DEFAULT_CHUNK_SIZE = 2 ** 24;
var MAX_CHUNKS_AT_ONCE = 2;
-var ChunkedStreamTargetWriter = class extends Writer {
+var ChunkedStreamTargetWriter = class extends Writer2 {
#pos = 0;
#target;
#chunkSize;
@@ -1255,6 +1287,7 @@ var ChunkedStreamTargetWriter = class extends Writer {
* A chunk is flushed if all of its contents have been written.
*/
#chunks = [];
+ #lastFlushEnd = 0;
constructor(target) {
super();
this.#target = target;
@@ -1336,10 +1369,14 @@ var ChunkedStreamTargetWriter = class extends Writer {
let chunk = this.#chunks[i];
if (!chunk.shouldFlush && !force) continue;
for (let section of chunk.written) {
+ if (this.ensureMonotonicity && chunk.start + section.start !== this.#lastFlushEnd) {
+ throw new Error("Internal error: Monotonicity violation.");
+ }
this.#target.options.onData?.(
chunk.data.subarray(section.start, section.end),
chunk.start + section.start
);
+ this.#lastFlushEnd = chunk.start + section.end;
}
this.#chunks.splice(i--, 1);
}
@@ -1363,68 +1400,200 @@ var FileSystemWritableFileStreamTargetWriter = class extends ChunkedStreamTarget
}
};
-// src/target.ts
-var Target = class {
- constructor() {
- this.output = null;
+// src/codec.ts
+var buildVideoCodecString = (codec, width, height) => {
+ if (codec === "avc") {
+ let profileIndication = 100;
+ if (width <= 768 && height <= 432) {
+ profileIndication = 66;
+ } else if (width <= 1920 && height <= 1080) {
+ profileIndication = 77;
+ }
+ const profileCompatibility = 0;
+ const levelIndication = width > 1920 || height > 1080 ? 50 : 41;
+ const hexProfileIndication = profileIndication.toString(16).padStart(2, "0");
+ const hexProfileCompatibility = profileCompatibility.toString(16).padStart(2, "0");
+ const hexLevelIndication = levelIndication.toString(16).padStart(2, "0");
+ return `avc1.${hexProfileIndication}${hexProfileCompatibility}${hexLevelIndication}`;
+ } else if (codec === "hevc") {
+ let profileSpace = 0;
+ let profileIdc = 1;
+ const compatibilityFlags = Array(32).fill(0);
+ compatibilityFlags[profileIdc] = 1;
+ const compatibilityHex = parseInt(compatibilityFlags.reverse().join(""), 2).toString(16).replace(/^0+/, "");
+ let tier = "L";
+ let level = 120;
+ if (width <= 1280 && height <= 720) {
+ level = 93;
+ } else if (width <= 1920 && height <= 1080) {
+ level = 120;
+ } else if (width <= 3840 && height <= 2160) {
+ level = 150;
+ } else {
+ tier = "H";
+ level = 180;
+ }
+ const constraintFlags = "B0";
+ const profilePrefix = profileSpace === 0 ? "" : String.fromCharCode(65 + profileSpace - 1);
+ return `hev1.${profilePrefix}${profileIdc}.${compatibilityHex}.${tier}${level}.${constraintFlags}`;
+ } else if (codec === "vp8") {
+ return "vp8";
+ } else if (codec === "vp9") {
+ const profile = "00";
+ let level;
+ if (width <= 854 && height <= 480) {
+ level = "21";
+ } else if (width <= 1280 && height <= 720) {
+ level = "31";
+ } else if (width <= 1920 && height <= 1080) {
+ level = "41";
+ } else if (width <= 3840 && height <= 2160) {
+ level = "51";
+ } else {
+ level = "61";
+ }
+ const bitDepth = "08";
+ return `vp09.${profile}.${level}.${bitDepth}`;
+ } else if (codec === "av1") {
+ const profile = 0;
+ let level;
+ if (width <= 854 && height <= 480) {
+ level = "01";
+ } else if (width <= 1280 && height <= 720) {
+ level = "03";
+ } else if (width <= 1920 && height <= 1080) {
+ level = "04";
+ } else if (width <= 3840 && height <= 2160) {
+ level = "07";
+ } else {
+ level = "09";
+ }
+ const tier = "M";
+ const bitDepth = "08";
+ return `av01.${profile}.${level}${tier}.${bitDepth}`;
+ }
+ throw new TypeError(`Unhandled codec '${codec}'.`);
+};
+var buildAudioCodecString = (codec, numberOfChannels, sampleRate) => {
+ if (codec === "aac") {
+ if (numberOfChannels >= 2 && sampleRate <= 24e3) {
+ return "mp4a.40.29";
+ }
+ if (sampleRate <= 24e3) {
+ return "mp4a.40.5";
+ }
+ return "mp4a.40.2";
+ } else if (codec === "opus") {
+ return "opus";
+ } else if (codec === "vorbis") {
+ return "vorbis";
+ }
+ throw new TypeError(`Unhandled codec '${codec}'.`);
+};
+var validateVideoChunkMetadata = (metadata) => {
+ if (!metadata) {
+ throw new TypeError("Video chunk metadata must be provided.");
+ }
+ if (typeof metadata !== "object") {
+ throw new TypeError("Video chunk metadata must be an object.");
+ }
+ if (!metadata.decoderConfig) {
+ throw new TypeError("Video chunk metadata must include a decoder configuration.");
+ }
+ if (typeof metadata.decoderConfig !== "object") {
+ throw new TypeError("Video chunk metadata decoder configuration must be an object.");
+ }
+ if (typeof metadata.decoderConfig.codec !== "string") {
+ throw new TypeError("Video chunk metadata decoder configuration must specify a codec string.");
+ }
+ if (!Number.isInteger(metadata.decoderConfig.codedWidth) || metadata.decoderConfig.codedWidth <= 0) {
+ throw new TypeError("Video chunk metadata decoder configuration must specify a valid codedWidth (positive integer).");
+ }
+ if (!Number.isInteger(metadata.decoderConfig.codedHeight) || metadata.decoderConfig.codedHeight <= 0) {
+ throw new TypeError("Video chunk metadata decoder configuration must specify a valid codedHeight (positive integer).");
+ }
+ if (metadata.decoderConfig.description !== void 0) {
+ if (!isAllowSharedBufferSource(metadata.decoderConfig.description)) {
+ throw new TypeError("Video chunk metadata decoder configuration description, when defined, must be an ArrayBuffer or an ArrayBuffer view.");
+ }
+ }
+ if (metadata.decoderConfig.colorSpace !== void 0) {
+ let { colorSpace } = metadata.decoderConfig;
+ if (typeof colorSpace !== "object") {
+ throw new TypeError("Video chunk metadata decoder configuration colorSpace, when provided, must be an object.");
+ }
+ let primariesValues = Object.keys(COLOR_PRIMARIES_MAP);
+ if (colorSpace.primaries != null && !primariesValues.includes(colorSpace.primaries)) {
+ throw new TypeError(`Video chunk metadata decoder configuration colorSpace primaries, when defined, must be one of ${primariesValues.join(", ")}.`);
+ }
+ let transferValues = Object.keys(TRANSFER_CHARACTERISTICS_MAP);
+ if (colorSpace.transfer != null && !transferValues.includes(colorSpace.transfer)) {
+ throw new TypeError(`Video chunk metadata decoder configuration colorSpace transfer, when defined, must be one of ${transferValues.join(", ")}.`);
+ }
+ let matrixValues = Object.keys(MATRIX_COEFFICIENTS_MAP);
+ if (colorSpace.matrix != null && !matrixValues.includes(colorSpace.matrix)) {
+ throw new TypeError(`Video chunk metadata decoder configuration colorSpace matrix, when defined, must be one of ${matrixValues.join(", ")}.`);
+ }
+ if (colorSpace.fullRange != null && typeof colorSpace.fullRange !== "boolean") {
+ throw new TypeError("Video chunk metadata decoder configuration colorSpace fullRange, when defined, must be a boolean.");
+ }
+ }
+ if ((metadata.decoderConfig.codec.startsWith("avc1") || metadata.decoderConfig.codec.startsWith("avc3")) && !metadata.decoderConfig.description) {
+ throw new TypeError("Video chunk metadata decoder configuration for AVC must include a description, which is expected to be an AVCDecoderConfigurationRecord as specified in ISO 14496-15.");
+ }
+ if ((metadata.decoderConfig.codec.startsWith("hev1") || metadata.decoderConfig.codec.startsWith("hvc1")) && !metadata.decoderConfig.description) {
+ throw new TypeError("Video chunk metadata decoder configuration for HEVC must include a description, which is expected to be an HEVCDecoderConfigurationRecord as specified in ISO 14496-15.");
+ }
+ if ((metadata.decoderConfig.codec === "vp8" || metadata.decoderConfig.codec.startsWith("vp09")) && metadata.decoderConfig.colorSpace === void 0) {
+ throw new TypeError("Video chunk metadata decoder configuration for VP8/VP9 must include a colorSpace.");
}
};
-var ArrayBufferTarget2 = class extends Target {
- constructor() {
- super(...arguments);
- this.buffer = null;
+var validateAudioChunkMetadata = (metadata) => {
+ if (!metadata) {
+ throw new TypeError("Audio chunk metadata must be provided.");
}
- createWriter() {
- return new ArrayBufferTargetWriter(this);
+ if (typeof metadata !== "object") {
+ throw new TypeError("Audio chunk metadata must be an object.");
+ }
+ if (!metadata.decoderConfig) {
+ throw new TypeError("Audio chunk metadata must include a decoder configuration.");
+ }
+ if (typeof metadata.decoderConfig !== "object") {
+ throw new TypeError("Audio chunk metadata decoder configuration must be an object.");
+ }
+ if (typeof metadata.decoderConfig.codec !== "string") {
+ throw new TypeError("Audio chunk metadata decoder configuration must specify a codec string.");
+ }
+ if (!Number.isInteger(metadata.decoderConfig.sampleRate) || metadata.decoderConfig.sampleRate <= 0) {
+ throw new TypeError("Audio chunk metadata decoder configuration must specify a valid sampleRate (positive integer).");
+ }
+ if (!Number.isInteger(metadata.decoderConfig.numberOfChannels) || metadata.decoderConfig.numberOfChannels <= 0) {
+ throw new TypeError("Audio chunk metadata decoder configuration must specify a valid numberOfChannels (positive integer).");
+ }
+ if (metadata.decoderConfig.description !== void 0) {
+ if (!isAllowSharedBufferSource(metadata.decoderConfig.description)) {
+ throw new TypeError("Audio chunk metadata decoder configuration description, when defined, must be an ArrayBuffer or an ArrayBuffer view.");
+ }
+ }
+ if (metadata.decoderConfig.codec === "opus" && metadata.decoderConfig.description && metadata.decoderConfig.description.byteLength < 18) {
+ throw new TypeError("Invalid decoder description provided for Opus; must be at least 18 bytes long.");
}
};
-var StreamTarget = class extends Target {
- constructor(options) {
- super();
- this.options = options;
- if (typeof options !== "object") {
- throw new TypeError("StreamTarget requires an options object to be passed to its constructor.");
- }
- if (options.onData) {
- if (typeof options.onData !== "function") {
- throw new TypeError("options.onData, when provided, must be a function.");
- }
- if (options.onData.length < 2) {
- throw new TypeError(
- "options.onData, when provided, must be a function that takes in at least two arguments (data and position). Ignoring the position argument, which specifies the byte offset at which the data is to be written, can lead to broken outputs."
- );
- }
- }
- if (options.chunked !== void 0 && typeof options.chunked !== "boolean") {
- throw new TypeError("options.chunked, when provided, must be a boolean.");
- }
- if (options.chunkSize !== void 0 && (!Number.isInteger(options.chunkSize) || options.chunkSize <= 0)) {
- throw new TypeError("options.chunkSize, when provided, must be a positive integer.");
- }
+var validateSubtitleMetadata = (metadata) => {
+ if (!metadata) {
+ throw new TypeError("Subtitle metadata must be provided.");
}
- createWriter() {
- return this.options.chunked ? new ChunkedStreamTargetWriter(this) : new StreamTargetWriter(this);
+ if (typeof metadata !== "object") {
+ throw new TypeError("Subtitle metadata must be an object.");
}
-};
-var FileSystemWritableFileStreamTarget2 = class extends Target {
- constructor(stream, options) {
- super();
- this.stream = stream;
- this.options = options;
- if (!(stream instanceof FileSystemWritableFileStream)) {
- throw new TypeError("FileSystemWritableFileStreamTarget requires a FileSystemWritableFileStream instance.");
- }
- if (options !== void 0 && typeof options !== "object") {
- throw new TypeError("FileSystemWritableFileStreamTarget's options, when provided, must be an object.");
- }
- if (options) {
- if (options.chunkSize !== void 0 && (!Number.isInteger(options.chunkSize) || options.chunkSize <= 0)) {
- throw new TypeError("options.chunkSize, when provided, must be a positive integer");
- }
- }
+ if (!metadata.config) {
+ throw new TypeError("Subtitle metadata must include a config object.");
}
- createWriter() {
- return new FileSystemWritableFileStreamTargetWriter(this);
+ if (typeof metadata.config !== "object") {
+ throw new TypeError("Subtitle metadata config must be an object.");
+ }
+ if (typeof metadata.config.description !== "string") {
+ throw new TypeError("Subtitle metadata config description must be a string.");
}
};
@@ -1439,7 +1608,7 @@ var IsobmffMuxer = class extends Muxer {
constructor(output, format) {
super(output);
this.timestampsMustStartAtZero = true;
- this.#auxTarget = new ArrayBufferTarget2();
+ this.#auxTarget = new ArrayBufferTarget();
this.#auxWriter = this.#auxTarget.createWriter();
this.#auxBoxWriter = new IsobmffBoxWriter(this.#auxWriter);
this.#ftypSize = null;
@@ -1451,10 +1620,15 @@ var IsobmffMuxer = class extends Muxer {
this.#writer = output.writer;
this.#boxWriter = new IsobmffBoxWriter(this.#writer);
this.#format = format;
+ this.#fastStart = format.options.fastStart ?? (this.#writer instanceof ArrayBufferTargetWriter ? "in-memory" : false);
+ if (this.#fastStart === "in-memory" || this.#fastStart === "fragmented") {
+ this.#writer.ensureMonotonicity = true;
+ }
}
#writer;
#boxWriter;
#format;
+ #fastStart;
#auxTarget;
#auxWriter;
#auxBoxWriter;
@@ -1468,45 +1642,24 @@ var IsobmffMuxer = class extends Muxer {
const holdsAvc = this.output.tracks.some((x) => x.type === "video" && x.source.codec === "avc");
this.#boxWriter.writeBox(ftyp({
holdsAvc,
- fragmented: this.#format.options.fastStart === "fragmented"
+ fragmented: this.#fastStart === "fragmented"
}));
this.#ftypSize = this.#writer.getPos();
- if (this.#format.options.fastStart === "in-memory") {
+ if (this.#fastStart === "in-memory") {
this.#mdat = mdat(false);
- } else if (this.#format.options.fastStart === "fragmented") {
+ } else if (this.#fastStart === "fragmented") {
} else {
- if (typeof this.#format.options.fastStart === "object") {
- let moovSizeUpperBound = this.#computeMoovSizeUpperBound();
- this.#writer.seek(this.#writer.getPos() + moovSizeUpperBound);
- }
this.#mdat = mdat(true);
this.#boxWriter.writeBox(this.#mdat);
}
this.#writer.flush();
}
- #computeMoovSizeUpperBound() {
- assert(typeof this.#format.options.fastStart === "object");
- let upperBound = 0;
- let sampleCounts = [
- this.#format.options.fastStart.expectedVideoChunks,
- this.#format.options.fastStart.expectedAudioChunks
- ];
- for (let n of sampleCounts) {
- if (!n) continue;
- upperBound += (4 + 4) * Math.ceil(2 / 3 * n);
- upperBound += 4 * n;
- upperBound += (4 + 4 + 4) * Math.ceil(2 / 3 * n);
- upperBound += 4 * n;
- upperBound += 8 * n;
- }
- upperBound += 4096;
- return upperBound;
- }
#getVideoTrackData(track, meta) {
const existingTrackData = this.#trackDatas.find((x) => x.track === track);
if (existingTrackData) {
return existingTrackData;
}
+ validateVideoChunkMetadata(meta);
assert(meta);
assert(meta.decoderConfig);
assert(meta.decoderConfig.codedWidth !== void 0);
@@ -1540,6 +1693,7 @@ var IsobmffMuxer = class extends Muxer {
if (existingTrackData) {
return existingTrackData;
}
+ validateAudioChunkMetadata(meta);
assert(meta);
assert(meta.decoderConfig);
const newTrackData = {
@@ -1571,6 +1725,7 @@ var IsobmffMuxer = class extends Muxer {
if (existingTrackData) {
return existingTrackData;
}
+ validateSubtitleMetadata(meta);
assert(meta);
assert(meta.config);
const newTrackData = {
@@ -1603,35 +1758,19 @@ var IsobmffMuxer = class extends Muxer {
}
addEncodedVideoChunk(track, chunk, meta) {
const trackData = this.#getVideoTrackData(track, meta);
- if (typeof this.#format.options.fastStart === "object" && trackData.samples.length === this.#format.options.fastStart.expectedVideoChunks) {
- throw new Error(`Cannot add more video chunks than specified in 'fastStart' (${this.#format.options.fastStart.expectedVideoChunks}).`);
- }
let data = new Uint8Array(chunk.byteLength);
chunk.copyTo(data);
let timestamp = this.validateAndNormalizeTimestamp(trackData.track, chunk.timestamp, chunk.type === "key");
let sample = this.#createSampleForTrack(trackData, data, timestamp, (chunk.duration ?? 0) / 1e6, chunk.type);
- if (this.#format.options.fastStart === "fragmented") {
- trackData.sampleQueue.push(sample);
- this.#interleaveSamples();
- } else {
- this.#addSampleToTrack(trackData, sample);
- }
+ this.#registerSample(trackData, sample);
}
addEncodedAudioChunk(track, chunk, meta) {
const trackData = this.#getAudioTrackData(track, meta);
- if (typeof this.#format.options.fastStart === "object" && trackData.samples.length === this.#format.options.fastStart.expectedAudioChunks) {
- throw new Error(`Cannot add more audio chunks than specified in 'fastStart' (${this.#format.options.fastStart.expectedAudioChunks}).`);
- }
let data = new Uint8Array(chunk.byteLength);
chunk.copyTo(data);
let timestamp = this.validateAndNormalizeTimestamp(trackData.track, chunk.timestamp, chunk.type === "key");
let sample = this.#createSampleForTrack(trackData, data, timestamp, (chunk.duration ?? 0) / 1e6, chunk.type);
- if (this.#format.options.fastStart === "fragmented") {
- trackData.sampleQueue.push(sample);
- this.#interleaveSamples();
- } else {
- this.#addSampleToTrack(trackData, sample);
- }
+ this.#registerSample(trackData, sample);
}
addSubtitleCue(track, cue, meta) {
const trackData = this.#getSubtitleTrackData(track, meta);
@@ -1663,12 +1802,7 @@ var IsobmffMuxer = class extends Muxer {
this.#auxBoxWriter.writeBox(box2);
let body2 = this.#auxWriter.getSlice(0, this.#auxWriter.getPos());
let sample2 = this.#createSampleForTrack(trackData, body2, trackData.lastCueEndTimestamp, sampleStart - trackData.lastCueEndTimestamp, "key");
- if (this.#format.options.fastStart === "fragmented") {
- trackData.sampleQueue.push(sample2);
- this.#interleaveSamples();
- } else {
- this.#addSampleToTrack(trackData, sample2);
- }
+ this.#registerSample(trackData, sample2);
trackData.lastCueEndTimestamp = sampleStart;
}
this.#auxWriter.seek(0);
@@ -1697,12 +1831,7 @@ var IsobmffMuxer = class extends Muxer {
}
let body = this.#auxWriter.getSlice(0, this.#auxWriter.getPos());
let sample = this.#createSampleForTrack(trackData, body, sampleStart, sampleEnd - sampleStart, "key");
- if (this.#format.options.fastStart === "fragmented") {
- trackData.sampleQueue.push(sample);
- this.#interleaveSamples();
- } else {
- this.#addSampleToTrack(trackData, sample);
- }
+ this.#registerSample(trackData, sample);
trackData.lastCueEndTimestamp = sampleEnd;
}
}
@@ -1736,7 +1865,7 @@ var IsobmffMuxer = class extends Muxer {
let delta = Math.round(timescaleUnits - trackData.lastTimescaleUnits);
trackData.lastTimescaleUnits += delta;
trackData.lastSample.timescaleUnitsToNextSample = delta;
- if (this.#format.options.fastStart !== "fragmented") {
+ if (this.#fastStart !== "fragmented") {
let lastTableEntry = last(trackData.timeToSampleTable);
assert(lastTableEntry);
if (lastTableEntry.sampleCount === 1) {
@@ -1775,7 +1904,7 @@ var IsobmffMuxer = class extends Muxer {
}
} else {
trackData.lastTimescaleUnits = 0;
- if (this.#format.options.fastStart !== "fragmented") {
+ if (this.#fastStart !== "fragmented") {
trackData.timeToSampleTable.push({
sampleCount: 1,
sampleDelta: durationInTimescale
@@ -1790,11 +1919,19 @@ var IsobmffMuxer = class extends Muxer {
}
trackData.timestampProcessingQueue.length = 0;
}
+ #registerSample(trackData, sample) {
+ if (this.#fastStart === "fragmented") {
+ trackData.sampleQueue.push(sample);
+ this.#interleaveSamples();
+ } else {
+ this.#addSampleToTrack(trackData, sample);
+ }
+ }
#addSampleToTrack(trackData, sample) {
if (sample.type === "key") {
this.#processTimestamps(trackData);
}
- if (this.#format.options.fastStart !== "fragmented") {
+ if (this.#fastStart !== "fragmented") {
trackData.samples.push(sample);
}
let beginNewChunk = false;
@@ -1802,7 +1939,7 @@ var IsobmffMuxer = class extends Muxer {
beginNewChunk = true;
} else {
let currentChunkDuration = sample.timestamp - trackData.currentChunk.startTimestamp;
- if (this.#format.options.fastStart === "fragmented") {
+ if (this.#fastStart === "fragmented") {
const keyFrameQueuedEverywhere = this.#trackDatas.every((otherTrackData) => {
if (trackData === otherTrackData) {
return sample.type === "key";
@@ -1834,7 +1971,7 @@ var IsobmffMuxer = class extends Muxer {
trackData.timestampProcessingQueue.push(sample);
}
#finalizeCurrentChunk(trackData) {
- assert(this.#format.options.fastStart !== "fragmented");
+ assert(this.#fastStart !== "fragmented");
if (!trackData.currentChunk) return;
trackData.finalizedChunks.push(trackData.currentChunk);
this.#finalizedChunks.push(trackData.currentChunk);
@@ -1845,7 +1982,7 @@ var IsobmffMuxer = class extends Muxer {
samplesPerChunk: trackData.currentChunk.samples.length
});
}
- if (this.#format.options.fastStart === "in-memory") {
+ if (this.#fastStart === "in-memory") {
trackData.currentChunk.offset = 0;
return;
}
@@ -1858,7 +1995,7 @@ var IsobmffMuxer = class extends Muxer {
this.#writer.flush();
}
#interleaveSamples() {
- assert(this.#format.options.fastStart === "fragmented");
+ assert(this.#fastStart === "fragmented");
for (const track of this.output.tracks) {
if (!track.source.closed && !this.#trackDatas.some((x) => x.track === track)) {
return;
@@ -1885,7 +2022,7 @@ var IsobmffMuxer = class extends Muxer {
}
}
#finalizeFragment(flushWriter = true) {
- assert(this.#format.options.fastStart === "fragmented");
+ assert(this.#fastStart === "fragmented");
let fragmentNumber = this.#nextFragmentNumber++;
if (fragmentNumber === 1) {
let movieBox = moov(this.#trackDatas, this.#creationTime, true);
@@ -1940,7 +2077,7 @@ var IsobmffMuxer = class extends Muxer {
this.#processWebVTTCues(trackData, Infinity);
}
}
- if (this.#format.options.fastStart === "fragmented") {
+ if (this.#fastStart === "fragmented") {
this.#interleaveSamples();
}
}
@@ -1951,7 +2088,7 @@ var IsobmffMuxer = class extends Muxer {
this.#processWebVTTCues(trackData, Infinity);
}
}
- if (this.#format.options.fastStart === "fragmented") {
+ if (this.#fastStart === "fragmented") {
for (let trackData of this.#trackDatas) {
for (let sample of trackData.sampleQueue) {
this.#addSampleToTrack(trackData, sample);
@@ -1965,7 +2102,7 @@ var IsobmffMuxer = class extends Muxer {
this.#finalizeCurrentChunk(trackData);
}
}
- if (this.#format.options.fastStart === "in-memory") {
+ if (this.#fastStart === "in-memory") {
assert(this.#mdat);
let mdatSize;
for (let i = 0; i < 2; i++) {
@@ -1995,7 +2132,7 @@ var IsobmffMuxer = class extends Muxer {
sample.data = null;
}
}
- } else if (this.#format.options.fastStart === "fragmented") {
+ } else if (this.#fastStart === "fragmented") {
let startPos = this.#writer.getPos();
let mfraBox = mfra(this.#trackDatas);
this.#boxWriter.writeBox(mfraBox);
@@ -2012,7 +2149,7 @@ var IsobmffMuxer = class extends Muxer {
this.#mdat.largeSize = mdatSize >= 2 ** 32;
this.#boxWriter.patchBox(this.#mdat);
let movieBox = moov(this.#trackDatas, this.#creationTime);
- if (typeof this.#format.options.fastStart === "object") {
+ if (typeof this.#fastStart === "object") {
this.#writer.seek(this.#ftypSize);
this.#boxWriter.writeBox(movieBox);
let remainingBytes = mdatPos - this.#writer.getPos();
@@ -2111,7 +2248,7 @@ var TRACK_TYPE_MAP = {
var MatroskaMuxer = class extends Muxer {
constructor(output, format) {
super(output);
- this.timestampsMustStartAtZero = true;
+ this.timestampsMustStartAtZero = false;
this.#helper = new Uint8Array(8);
this.#helperView = new DataView(this.#helper.buffer);
/**
@@ -2134,6 +2271,9 @@ var MatroskaMuxer = class extends Muxer {
this.#duration = 0;
this.#writer = output.writer;
this.#format = format;
+ if (this.#format.options.streamable) {
+ this.#writer.ensureMonotonicity = true;
+ }
}
#writer;
#format;
@@ -2308,7 +2448,7 @@ var MatroskaMuxer = class extends Muxer {
}
start() {
this.#writeEBMLHeader();
- if (!this.#format.options.streaming) {
+ if (!this.#format.options.streamable) {
this.#createSeekHead();
}
this.#createSegmentInfo();
@@ -2358,7 +2498,7 @@ var MatroskaMuxer = class extends Muxer {
{ id: 2807729 /* TimestampScale */, data: 1e6 },
{ id: 19840 /* MuxingApp */, data: APP_NAME },
{ id: 22337 /* WritingApp */, data: APP_NAME },
- !this.#format.options.streaming ? segmentDuration : null
+ !this.#format.options.streamable ? segmentDuration : null
] };
this.#segmentInfo = segmentInfo;
}
@@ -2370,7 +2510,6 @@ var MatroskaMuxer = class extends Muxer {
{ id: 215 /* TrackNumber */, data: trackData.track.id },
{ id: 29637 /* TrackUID */, data: trackData.track.id },
{ id: 131 /* TrackType */, data: TRACK_TYPE_MAP[trackData.type] },
- // TODO Subtitle case
{ id: 134 /* CodecID */, data: CODEC_STRING_MAP[trackData.track.source.codec] },
...trackData.type === "video" ? [
trackData.info.decoderConfig.description ? { id: 25506 /* CodecPrivate */, data: toUint8Array(trackData.info.decoderConfig.description) } : null,
@@ -2412,9 +2551,9 @@ var MatroskaMuxer = class extends Muxer {
#createSegment() {
let segment = {
id: 408125543 /* Segment */,
- size: this.#format.options.streaming ? -1 : SEGMENT_SIZE_BYTES,
+ size: this.#format.options.streamable ? -1 : SEGMENT_SIZE_BYTES,
data: [
- !this.#format.options.streaming ? this.#seekHead : null,
+ !this.#format.options.streamable ? this.#seekHead : null,
this.#segmentInfo,
this.#tracksElement
]
@@ -2434,6 +2573,7 @@ var MatroskaMuxer = class extends Muxer {
if (existingTrackData) {
return existingTrackData;
}
+ validateVideoChunkMetadata(meta);
assert(meta);
assert(meta.decoderConfig);
assert(meta.decoderConfig.codedWidth !== void 0);
@@ -2458,6 +2598,7 @@ var MatroskaMuxer = class extends Muxer {
if (existingTrackData) {
return existingTrackData;
}
+ validateAudioChunkMetadata(meta);
assert(meta);
assert(meta.decoderConfig);
const newTrackData = {
@@ -2480,6 +2621,7 @@ var MatroskaMuxer = class extends Muxer {
if (existingTrackData) {
return existingTrackData;
}
+ validateSubtitleMetadata(meta);
assert(meta);
assert(meta.config);
const newTrackData = {
@@ -2667,12 +2809,12 @@ ${cue.notes ?? ""}`;
}
/** Creates a new Cluster element to contain media chunks. */
#createNewCluster(msTimestamp) {
- if (this.#currentCluster && !this.#format.options.streaming) {
+ if (this.#currentCluster && !this.#format.options.streamable) {
this.#finalizeCurrentCluster();
}
this.#currentCluster = {
id: 524531317 /* Cluster */,
- size: this.#format.options.streaming ? -1 : CLUSTER_SIZE_BYTES,
+ size: this.#format.options.streamable ? -1 : CLUSTER_SIZE_BYTES,
data: [
{ id: 231 /* Timestamp */, data: msTimestamp }
]
@@ -2706,17 +2848,21 @@ ${cue.notes ?? ""}`;
}
/** Finalizes the file, making it ready for use. Must be called after all media chunks have been added. */
finalize() {
+ if (!this.#segment) {
+ this.#createTracks();
+ this.#createSegment();
+ }
for (let trackData of this.#trackDatas) {
while (trackData.chunkQueue.length > 0) {
this.#writeBlock(trackData, trackData.chunkQueue.shift());
}
}
- if (!this.#format.options.streaming) {
+ if (!this.#format.options.streamable && this.#currentCluster) {
this.#finalizeCurrentCluster();
}
assert(this.#cues);
this.writeEBML(this.#cues);
- if (!this.#format.options.streaming) {
+ if (!this.#format.options.streamable) {
let endPos = this.#writer.getPos();
let segmentSize = this.#writer.getPos() - this.#segmentDataOffset;
this.#writer.seek(this.offsets.get(this.#segment) + 4);
@@ -2738,7 +2884,13 @@ ${cue.notes ?? ""}`;
var OutputFormat = class {
};
var Mp4OutputFormat = class extends OutputFormat {
- constructor(options) {
+ constructor(options = {}) {
+ if (!options || typeof options !== "object") {
+ throw new TypeError("options must be an object.");
+ }
+ if (options.fastStart !== void 0 && ![false, "in-memory", "fragmented"].includes(options.fastStart)) {
+ throw new TypeError('options.fastStart, when provided, must be false, "in-memory", or "fragmented".');
+ }
super();
this.options = options;
}
@@ -2748,6 +2900,12 @@ var Mp4OutputFormat = class extends OutputFormat {
};
var MkvOutputFormat2 = class extends OutputFormat {
constructor(options = {}) {
+ if (!options || typeof options !== "object") {
+ throw new TypeError("options must be an object.");
+ }
+ if (options.streamable !== void 0 && typeof options.streamable !== "boolean") {
+ throw new TypeError("options.streamable, when provided, must be a boolean.");
+ }
super();
this.options = options;
}
@@ -2758,104 +2916,17 @@ var MkvOutputFormat2 = class extends OutputFormat {
var WebMOutputFormat = class extends MkvOutputFormat2 {
};
-// src/codec.ts
-var buildVideoCodecString = (codec, width, height) => {
- if (codec === "avc") {
- let profileIndication = 100;
- if (width <= 768 && height <= 432) {
- profileIndication = 66;
- } else if (width <= 1920 && height <= 1080) {
- profileIndication = 77;
- }
- const profileCompatibility = 0;
- const levelIndication = width > 1920 || height > 1080 ? 50 : 41;
- const hexProfileIndication = profileIndication.toString(16).padStart(2, "0");
- const hexProfileCompatibility = profileCompatibility.toString(16).padStart(2, "0");
- const hexLevelIndication = levelIndication.toString(16).padStart(2, "0");
- return `avc1.${hexProfileIndication}${hexProfileCompatibility}${hexLevelIndication}`;
- } else if (codec === "hevc") {
- let profileSpace = 0;
- let profileIdc = 1;
- const compatibilityFlags = Array(32).fill(0);
- compatibilityFlags[profileIdc] = 1;
- const compatibilityHex = parseInt(compatibilityFlags.reverse().join(""), 2).toString(16).replace(/^0+/, "");
- let tier = "L";
- let level = 120;
- if (width <= 1280 && height <= 720) {
- level = 93;
- } else if (width <= 1920 && height <= 1080) {
- level = 120;
- } else if (width <= 3840 && height <= 2160) {
- level = 150;
- } else {
- tier = "H";
- level = 180;
- }
- const constraintFlags = "B0";
- const profilePrefix = profileSpace === 0 ? "" : String.fromCharCode(65 + profileSpace - 1);
- return `hev1.${profilePrefix}${profileIdc}.${compatibilityHex}.${tier}${level}.${constraintFlags}`;
- } else if (codec === "vp8") {
- return "vp8";
- } else if (codec === "vp9") {
- const profile = "00";
- let level;
- if (width <= 854 && height <= 480) {
- level = "21";
- } else if (width <= 1280 && height <= 720) {
- level = "31";
- } else if (width <= 1920 && height <= 1080) {
- level = "41";
- } else if (width <= 3840 && height <= 2160) {
- level = "51";
- } else {
- level = "61";
- }
- const bitDepth = "08";
- return `vp09.${profile}.${level}.${bitDepth}`;
- } else if (codec === "av1") {
- const profile = 0;
- let level;
- if (width <= 854 && height <= 480) {
- level = "01";
- } else if (width <= 1280 && height <= 720) {
- level = "03";
- } else if (width <= 1920 && height <= 1080) {
- level = "04";
- } else if (width <= 3840 && height <= 2160) {
- level = "07";
- } else {
- level = "09";
- }
- const tier = "M";
- const bitDepth = "08";
- return `av01.${profile}.${level}${tier}.${bitDepth}`;
- }
- throw new Error(`Unhandled codec '${codec}'.`);
-};
-var buildAudioCodecString = (codec, numberOfChannels, sampleRate) => {
- if (codec === "aac") {
- if (numberOfChannels >= 2 && sampleRate <= 24e3) {
- return "mp4a.40.29";
- }
- if (sampleRate <= 24e3) {
- return "mp4a.40.5";
- }
- return "mp4a.40.2";
- } else if (codec === "opus") {
- return "opus";
- } else if (codec === "vorbis") {
- return "vorbis";
- }
- throw new Error(`Unhandled codec '${codec}'.`);
-};
-
// src/source.ts
+var VIDEO_CODECS = ["avc", "hevc", "vp8", "vp9", "av1"];
+var AUDIO_CODECS = ["aac", "opus"];
+var SUBTITLE_CODECS = ["webvtt"];
var MediaSource = class {
constructor() {
this.connectedTrack = null;
this.closed = false;
this.offsetTimestamps = false;
}
+ // TODO this is also just internal:
ensureValidDigest() {
if (!this.connectedTrack) {
throw new Error("Cannot call digest without connecting the source to an output track.");
@@ -2896,6 +2967,9 @@ var VideoSource = class extends MediaSource {
constructor(codec) {
super();
this.connectedTrack = null;
+ if (!VIDEO_CODECS.includes(codec)) {
+ throw new TypeError(`Invalid video codec '${codec}'. Must be one of: ${VIDEO_CODECS.join(", ")}.`);
+ }
this.codec = codec;
}
};
@@ -2903,23 +2977,49 @@ var EncodedVideoChunkSource = class extends VideoSource {
constructor(codec) {
super(codec);
}
- // TODO: Ensure that the first chunk is a key frame (same for the audio case)
digest(chunk, meta) {
+ if (!(chunk instanceof EncodedVideoChunk)) {
+ throw new TypeError("chunk must be an EncodedVideoChunk.");
+ }
this.ensureValidDigest();
this.connectedTrack?.output.muxer.addEncodedVideoChunk(this.connectedTrack, chunk, meta);
}
};
var KEY_FRAME_INTERVAL = 5;
+var validateVideoCodecConfig = (config) => {
+ if (!config || typeof config !== "object") {
+ throw new TypeError("Codec config must be an object.");
+ }
+ if (!VIDEO_CODECS.includes(config.codec)) {
+ throw new TypeError(`Invalid video codec '${config.codec}'. Must be one of: ${VIDEO_CODECS.join(", ")}.`);
+ }
+ if (!Number.isInteger(config.bitrate) || config.bitrate <= 0) {
+ throw new TypeError("config.bitrate must be a positive integer.");
+ }
+ if (config.latencyMode !== void 0 && ["quality", "realtime"].includes(config.latencyMode)) {
+ throw new TypeError("config.latencyMode, when provided, must be 'quality' or 'realtime'.");
+ }
+};
var VideoEncoderWrapper = class {
constructor(source, codecConfig) {
this.source = source;
this.codecConfig = codecConfig;
this.encoder = null;
this.lastMultipleOfKeyFrameInterval = -1;
+ this.lastWidth = null;
+ this.lastHeight = null;
+ validateVideoCodecConfig(codecConfig);
}
- // TODO: Ensure video frame size remains constant
digest(videoFrame) {
this.source.ensureValidDigest();
+ if (this.lastWidth !== null && this.lastHeight !== null) {
+ if (videoFrame.codedWidth !== this.lastWidth || videoFrame.codedHeight !== this.lastHeight) {
+ throw new Error(`Video frame size must remain constant. Expected ${this.lastWidth}x${this.lastHeight}, got ${videoFrame.codedWidth}x${videoFrame.codedHeight}.`);
+ }
+ } else {
+ this.lastWidth = videoFrame.codedWidth;
+ this.lastHeight = videoFrame.codedHeight;
+ }
this.ensureEncoder(videoFrame);
assert(this.encoder);
const multipleOfKeyFrameInterval = Math.floor(videoFrame.timestamp / 1e6 / KEY_FRAME_INTERVAL);
@@ -2932,8 +3032,7 @@ var VideoEncoderWrapper = class {
}
this.encoder = new VideoEncoder({
output: (chunk, meta) => this.source.connectedTrack?.output.muxer.addEncodedVideoChunk(this.source.connectedTrack, chunk, meta),
- error: (error) => console.error(error)
- // TODO
+ error: (error) => console.error("Video encode error:", error)
});
this.encoder.configure({
codec: buildVideoCodecString(this.codecConfig.codec, videoFrame.codedWidth, videoFrame.codedHeight),
@@ -2954,6 +3053,9 @@ var VideoFrameSource = class extends VideoSource {
this.encoder = new VideoEncoderWrapper(this, codecConfig);
}
digest(videoFrame) {
+ if (!(videoFrame instanceof VideoFrame)) {
+ throw new TypeError("videoFrame must be a VideoFrame.");
+ }
this.encoder.digest(videoFrame);
}
flush() {
@@ -2962,11 +3064,20 @@ var VideoFrameSource = class extends VideoSource {
};
var CanvasSource = class extends VideoSource {
constructor(canvas, codecConfig) {
+ if (!(canvas instanceof HTMLCanvasElement)) {
+ throw new TypeError("canvas must be an HTMLCanvasElement.");
+ }
super(codecConfig.codec);
this.canvas = canvas;
this.encoder = new VideoEncoderWrapper(this, codecConfig);
}
digest(timestamp, duration = 0) {
+ if (!Number.isFinite(timestamp) || timestamp < 0) {
+ throw new TypeError("timestamp must be a non-negative number.");
+ }
+ if (!Number.isFinite(duration) || duration < 0) {
+ throw new TypeError("duration must be a non-negative number.");
+ }
const frame = new VideoFrame(this.canvas, {
timestamp: Math.round(1e6 * timestamp),
duration: Math.round(1e6 * duration),
@@ -2981,6 +3092,9 @@ var CanvasSource = class extends VideoSource {
};
var MediaStreamVideoTrackSource = class extends VideoSource {
constructor(track, codecConfig) {
+ if (!(track instanceof MediaStreamTrack) || track.kind !== "video") {
+ throw new TypeError("track must be a video MediaStreamTrack.");
+ }
super(codecConfig.codec);
this.track = track;
this.abortController = null;
@@ -3015,6 +3129,9 @@ var AudioSource = class extends MediaSource {
constructor(codec) {
super();
this.connectedTrack = null;
+ if (!AUDIO_CODECS.includes(codec)) {
+ throw new TypeError(`Invalid audio codec '${codec}'. Must be one of: ${AUDIO_CODECS.join(", ")}.`);
+ }
this.codec = codec;
}
};
@@ -3023,19 +3140,43 @@ var EncodedAudioChunkSource = class extends AudioSource {
super(codec);
}
digest(chunk, meta) {
+ if (!(chunk instanceof EncodedAudioChunk)) {
+ throw new TypeError("chunk must be an EncodedAudioChunk.");
+ }
this.ensureValidDigest();
this.connectedTrack?.output.muxer.addEncodedAudioChunk(this.connectedTrack, chunk, meta);
}
};
+var validateAudioCodecConfig = (config) => {
+ if (!config || typeof config !== "object") {
+ throw new TypeError("Codec config must be an object.");
+ }
+ if (!AUDIO_CODECS.includes(config.codec)) {
+ throw new TypeError(`Invalid audio codec '${config.codec}'. Must be one of: ${AUDIO_CODECS.join(", ")}.`);
+ }
+ if (!Number.isInteger(config.bitrate) || config.bitrate <= 0) {
+ throw new TypeError("config.bitrate must be a positive integer.");
+ }
+};
var AudioEncoderWrapper = class {
constructor(source, codecConfig) {
this.source = source;
this.codecConfig = codecConfig;
this.encoder = null;
+ this.lastNumberOfChannels = null;
+ this.lastSampleRate = null;
+ validateAudioCodecConfig(codecConfig);
}
- // TODO: Ensure audio parameters remain constant
digest(audioData) {
this.source.ensureValidDigest();
+ if (this.lastNumberOfChannels !== null && this.lastSampleRate !== null) {
+ if (audioData.numberOfChannels !== this.lastNumberOfChannels || audioData.sampleRate !== this.lastSampleRate) {
+ throw new Error(`Audio parameters must remain constant. Expected ${this.lastNumberOfChannels} channels at ${this.lastSampleRate} Hz, got ${audioData.numberOfChannels} channels at ${audioData.sampleRate} Hz.`);
+ }
+ } else {
+ this.lastNumberOfChannels = audioData.numberOfChannels;
+ this.lastSampleRate = audioData.sampleRate;
+ }
this.ensureEncoder(audioData);
assert(this.encoder);
this.encoder.encode(audioData);
@@ -3046,8 +3187,7 @@ var AudioEncoderWrapper = class {
}
this.encoder = new AudioEncoder({
output: (chunk, meta) => this.source.connectedTrack?.output.muxer.addEncodedAudioChunk(this.source.connectedTrack, chunk, meta),
- error: (error) => console.error(error)
- // TODO
+ error: (error) => console.error("Audio encode error:", error)
});
this.encoder.configure({
codec: buildAudioCodecString(this.codecConfig.codec, audioData.numberOfChannels, audioData.sampleRate),
@@ -3066,6 +3206,9 @@ var AudioDataSource = class extends AudioSource {
this.encoder = new AudioEncoderWrapper(this, codecConfig);
}
digest(audioData) {
+ if (!(audioData instanceof AudioData)) {
+ throw new TypeError("audioData must be an AudioData.");
+ }
this.encoder.digest(audioData);
}
flush() {
@@ -3079,6 +3222,9 @@ var AudioBufferSource = class extends AudioSource {
this.encoder = new AudioEncoderWrapper(this, codecConfig);
}
digest(audioBuffer) {
+ if (!(audioBuffer instanceof AudioBuffer)) {
+ throw new TypeError("audioBuffer must be an AudioBuffer.");
+ }
const numberOfChannels = audioBuffer.numberOfChannels;
const sampleRate = audioBuffer.sampleRate;
const numberOfFrames = audioBuffer.length;
@@ -3105,6 +3251,9 @@ var AudioBufferSource = class extends AudioSource {
};
var MediaStreamAudioTrackSource = class extends AudioSource {
constructor(track, codecConfig) {
+ if (!(track instanceof MediaStreamTrack) || track.kind !== "audio") {
+ throw new TypeError("track must be an audio MediaStreamTrack.");
+ }
super(codecConfig.codec);
this.track = track;
this.abortController = null;
@@ -3139,6 +3288,9 @@ var SubtitleSource = class extends MediaSource {
constructor(codec) {
super();
this.connectedTrack = null;
+ if (!SUBTITLE_CODECS.includes(codec)) {
+ throw new TypeError(`Invalid subtitle codec '${codec}'. Must be one of: ${SUBTITLE_CODECS.join(", ")}.`);
+ }
this.codec = codec;
}
};
@@ -3148,23 +3300,125 @@ var TextSubtitleSource = class extends SubtitleSource {
this.parser = new SubtitleParser({
codec,
output: (cue, metadata) => this.connectedTrack?.output.muxer.addSubtitleCue(this.connectedTrack, cue, metadata),
- error: (error) => console.error(error)
- // TODO
+ error: (error) => console.error("Subtitle parse error:", error)
});
}
digest(text) {
+ if (typeof text !== "string") {
+ throw new TypeError("text must be a string.");
+ }
this.ensureValidDigest();
this.parser.parse(text);
}
};
+
+// src/output.ts
+var Output = class {
+ constructor(options) {
+ this.tracks = [];
+ this.started = false;
+ this.finalizing = false;
+ if (!options || typeof options !== "object") {
+ throw new TypeError("options must be an object.");
+ }
+ if (!(options.format instanceof OutputFormat)) {
+ throw new TypeError("options.format must be an OutputFormat.");
+ }
+ if (!(options.target instanceof Target)) {
+ throw new TypeError("options.target must be a Target.");
+ }
+ if (options.target.output) {
+ throw new Error("Target is already used for another output.");
+ }
+ options.target.output = this;
+ this.writer = options.target.createWriter();
+ this.muxer = options.format.createMuxer(this);
+ }
+ addVideoTrack(source, metadata = {}) {
+ if (!(source instanceof VideoSource)) {
+ throw new TypeError("source must be a VideoSource.");
+ }
+ if (!metadata || typeof metadata !== "object") {
+ throw new TypeError("metadata must be an object.");
+ }
+ if (typeof metadata.rotation === "number" && ![0, 90, 180, 270].includes(metadata.rotation)) {
+ throw new TypeError(`Invalid video rotation: ${metadata.rotation}. Has to be 0, 90, 180 or 270.`);
+ } else if (Array.isArray(metadata.rotation) && (metadata.rotation.length !== 9 || metadata.rotation.some((value) => !Number.isFinite(value)))) {
+ throw new TypeError(`Invalid video transformation matrix: ${metadata.rotation.join()}`);
+ }
+ if (metadata.frameRate !== void 0 && (!Number.isInteger(metadata.frameRate) || metadata.frameRate <= 0)) {
+ throw new TypeError(
+ `Invalid video frame rate: ${metadata.frameRate}. Must be a positive integer.`
+ );
+ }
+ this.addTrack("video", source, metadata);
+ }
+ addAudioTrack(source, metadata = {}) {
+ if (!(source instanceof AudioSource)) {
+ throw new TypeError("source must be an AudioSource.");
+ }
+ if (!metadata || typeof metadata !== "object") {
+ throw new TypeError("metadata must be an object.");
+ }
+ this.addTrack("audio", source, metadata);
+ }
+ addSubtitleTrack(source, metadata = {}) {
+ if (!(source instanceof SubtitleSource)) {
+ throw new TypeError("source must be a SubtitleSource.");
+ }
+ if (!metadata || typeof metadata !== "object") {
+ throw new TypeError("metadata must be an object.");
+ }
+ this.addTrack("subtitle", source, metadata);
+ }
+ addTrack(type, source, metadata) {
+ if (this.started) {
+ throw new Error("Cannot add track after output has started.");
+ }
+ if (source.connectedTrack) {
+ throw new Error("Source is already used for a track.");
+ }
+ const track = {
+ id: this.tracks.length + 1,
+ output: this,
+ type,
+ source,
+ metadata
+ };
+ this.muxer.beforeTrackAdd(track);
+ this.tracks.push(track);
+ source.connectedTrack = track;
+ }
+ start() {
+ if (this.started) {
+ throw new Error("Output already started.");
+ }
+ this.started = true;
+ this.muxer.start();
+ for (const track of this.tracks) {
+ track.source.start();
+ }
+ }
+ async finalize() {
+ if (this.finalizing) {
+ throw new Error("Cannot call finalize twice.");
+ }
+ this.finalizing = true;
+ const promises = this.tracks.map((x) => x.source.flush());
+ await Promise.all(promises);
+ this.muxer.finalize();
+ this.writer.flush();
+ this.writer.finalize();
+ }
+};
export {
- ArrayBufferTarget2 as ArrayBufferTarget,
+ ArrayBufferTarget,
AudioBufferSource,
AudioDataSource,
CanvasSource,
EncodedAudioChunkSource,
EncodedVideoChunkSource,
- FileSystemWritableFileStreamTarget2 as FileSystemWritableFileStreamTarget,
+ FileSystemWritableFileStreamTarget,
MediaStreamAudioTrackSource,
MediaStreamVideoTrackSource,
MkvOutputFormat2 as MkvOutputFormat,
diff --git a/src/codec.ts b/src/codec.ts
index 73a4df7..77746fc 100644
--- a/src/codec.ts
+++ b/src/codec.ts
@@ -1,4 +1,6 @@
+import { COLOR_PRIMARIES_MAP, isAllowSharedBufferSource, MATRIX_COEFFICIENTS_MAP, TRANSFER_CHARACTERISTICS_MAP } from "./misc";
import { AudioCodec, VideoCodec } from "./source";
+import { SubtitleMetadata } from "./subtitles";
export const buildVideoCodecString = (codec: VideoCodec, width: number, height: number) => {
if (codec === 'avc') {
@@ -63,56 +65,56 @@ export const buildVideoCodecString = (codec: VideoCodec, width: number, height:
return 'vp8'; // Easy, this one
} else if (codec === 'vp9') {
// Default to Profile 0 (most common)
- const profile = "00";
-
- // Determine level based on resolution
- // VP9 levels are specified as two digits: major.minor
- let level;
- if (width <= 854 && height <= 480) {
- level = "21"; // Level 2.1
- } else if (width <= 1280 && height <= 720) {
- level = "31"; // Level 3.1
- } else if (width <= 1920 && height <= 1080) {
- level = "41"; // Level 4.1
- } else if (width <= 3840 && height <= 2160) {
- level = "51"; // Level 5.1
- } else {
- level = "61"; // Level 6.1
- }
-
- // Default to 8-bit depth
- const bitDepth = "08";
-
- return `vp09.${profile}.${level}.${bitDepth}`;
+ const profile = "00";
+
+ // Determine level based on resolution
+ // VP9 levels are specified as two digits: major.minor
+ let level;
+ if (width <= 854 && height <= 480) {
+ level = "21"; // Level 2.1
+ } else if (width <= 1280 && height <= 720) {
+ level = "31"; // Level 3.1
+ } else if (width <= 1920 && height <= 1080) {
+ level = "41"; // Level 4.1
+ } else if (width <= 3840 && height <= 2160) {
+ level = "51"; // Level 5.1
+ } else {
+ level = "61"; // Level 6.1
+ }
+
+ // Default to 8-bit depth
+ const bitDepth = "08";
+
+ return `vp09.${profile}.${level}.${bitDepth}`;
} else if (codec === 'av1') {
// Default to Main Profile (0)
- const profile = 0;
-
- // Determine level based on resolution
- // Using a simplified level selection based on common resolutions
- let level;
- if (width <= 854 && height <= 480) {
- level = "01"; // Level 2.1
- } else if (width <= 1280 && height <= 720) {
- level = "03"; // Level 2.3
- } else if (width <= 1920 && height <= 1080) {
- level = "04"; // Level 3.0
- } else if (width <= 3840 && height <= 2160) {
- level = "07"; // Level 4.0
- } else {
- level = "09"; // Level 4.2
- }
-
- // Default to Main tier
- const tier = "M";
-
- // Default to 8-bit depth
- const bitDepth = "08";
+ const profile = 0;
+
+ // Determine level based on resolution
+ // Using a simplified level selection based on common resolutions
+ let level;
+ if (width <= 854 && height <= 480) {
+ level = "01"; // Level 2.1
+ } else if (width <= 1280 && height <= 720) {
+ level = "03"; // Level 2.3
+ } else if (width <= 1920 && height <= 1080) {
+ level = "04"; // Level 3.0
+ } else if (width <= 3840 && height <= 2160) {
+ level = "07"; // Level 4.0
+ } else {
+ level = "09"; // Level 4.2
+ }
+
+ // Default to Main tier
+ const tier = "M";
+
+ // Default to 8-bit depth
+ const bitDepth = "08";
- return `av01.${profile}.${level}${tier}.${bitDepth}`;
+ return `av01.${profile}.${level}${tier}.${bitDepth}`;
}
- throw new Error(`Unhandled codec '${codec}'.`);
+ throw new TypeError(`Unhandled codec '${codec}'.`);
};
export const buildAudioCodecString = (codec: AudioCodec, numberOfChannels: number, sampleRate: number) => {
@@ -135,5 +137,125 @@ export const buildAudioCodecString = (codec: AudioCodec, numberOfChannels: numbe
return 'vorbis'; // Also easy, this one
}
- throw new Error(`Unhandled codec '${codec}'.`);
-};
\ No newline at end of file
+ throw new TypeError(`Unhandled codec '${codec}'.`);
+};
+
+export const validateVideoChunkMetadata = (metadata: EncodedVideoChunkMetadata | undefined) => {
+ if (!metadata) {
+ throw new TypeError("Video chunk metadata must be provided.");
+ }
+ if (typeof metadata !== 'object') {
+ throw new TypeError("Video chunk metadata must be an object.");
+ }
+ if (!metadata.decoderConfig) {
+ throw new TypeError("Video chunk metadata must include a decoder configuration.");
+ }
+ if (typeof metadata.decoderConfig !== 'object') {
+ throw new TypeError("Video chunk metadata decoder configuration must be an object.");
+ }
+ if (typeof metadata.decoderConfig.codec !== 'string') {
+ throw new TypeError("Video chunk metadata decoder configuration must specify a codec string.");
+ }
+ if (!Number.isInteger(metadata.decoderConfig.codedWidth) || metadata.decoderConfig.codedWidth! <= 0) {
+ throw new TypeError("Video chunk metadata decoder configuration must specify a valid codedWidth (positive integer).");
+ }
+ if (!Number.isInteger(metadata.decoderConfig.codedHeight) || metadata.decoderConfig.codedHeight! <= 0) {
+ throw new TypeError("Video chunk metadata decoder configuration must specify a valid codedHeight (positive integer).");
+ }
+ if (metadata.decoderConfig.description !== undefined) {
+ if (!isAllowSharedBufferSource(metadata.decoderConfig.description)) {
+ throw new TypeError("Video chunk metadata decoder configuration description, when defined, must be an ArrayBuffer or an ArrayBuffer view.");
+ }
+ }
+ if (metadata.decoderConfig.colorSpace !== undefined) {
+ let { colorSpace } = metadata.decoderConfig;
+
+ if (typeof colorSpace !== 'object') {
+ throw new TypeError("Video chunk metadata decoder configuration colorSpace, when provided, must be an object.");
+ }
+
+ let primariesValues = Object.keys(COLOR_PRIMARIES_MAP);
+ if (colorSpace.primaries != null && !primariesValues.includes(colorSpace.primaries)) {
+ throw new TypeError(`Video chunk metadata decoder configuration colorSpace primaries, when defined, must be one of ${primariesValues.join(', ')}.`);
+ }
+
+ let transferValues = Object.keys(TRANSFER_CHARACTERISTICS_MAP);
+ if (colorSpace.transfer != null && !transferValues.includes(colorSpace.transfer)) {
+ throw new TypeError(`Video chunk metadata decoder configuration colorSpace transfer, when defined, must be one of ${transferValues.join(', ')}.`);
+ }
+
+ let matrixValues = Object.keys(MATRIX_COEFFICIENTS_MAP);
+ if (colorSpace.matrix != null && !matrixValues.includes(colorSpace.matrix)) {
+ throw new TypeError(`Video chunk metadata decoder configuration colorSpace matrix, when defined, must be one of ${matrixValues.join(', ')}.`);
+ }
+
+ if (colorSpace.fullRange != null && typeof colorSpace.fullRange !== 'boolean') {
+ throw new TypeError("Video chunk metadata decoder configuration colorSpace fullRange, when defined, must be a boolean.");
+ }
+ }
+
+ if ((metadata.decoderConfig.codec.startsWith('avc1') || metadata.decoderConfig.codec.startsWith('avc3')) && !metadata.decoderConfig.description) {
+ // TODO Technically not correct, they can also lack a description in which case the chunks are annexb. Generally,
+ // go through all the codec registrations and catch these edge cases.
+ throw new TypeError("Video chunk metadata decoder configuration for AVC must include a description, which is expected to be an AVCDecoderConfigurationRecord as specified in ISO 14496-15.");
+ }
+ if ((metadata.decoderConfig.codec.startsWith('hev1') || metadata.decoderConfig.codec.startsWith('hvc1')) && !metadata.decoderConfig.description) {
+ // TODO Same as above
+ throw new TypeError("Video chunk metadata decoder configuration for HEVC must include a description, which is expected to be an HEVCDecoderConfigurationRecord as specified in ISO 14496-15.");
+ }
+ if ((metadata.decoderConfig.codec === 'vp8' || metadata.decoderConfig.codec.startsWith('vp09')) && metadata.decoderConfig.colorSpace === undefined) {
+ throw new TypeError("Video chunk metadata decoder configuration for VP8/VP9 must include a colorSpace.");
+ }
+ // No added requirements for AV1 (based)
+};
+
+export const validateAudioChunkMetadata = (metadata: EncodedAudioChunkMetadata | undefined) => {
+ if (!metadata) {
+ throw new TypeError("Audio chunk metadata must be provided.");
+ }
+ if (typeof metadata !== 'object') {
+ throw new TypeError("Audio chunk metadata must be an object.");
+ }
+ if (!metadata.decoderConfig) {
+ throw new TypeError("Audio chunk metadata must include a decoder configuration.");
+ }
+ if (typeof metadata.decoderConfig !== 'object') {
+ throw new TypeError("Audio chunk metadata decoder configuration must be an object.");
+ }
+ if (typeof metadata.decoderConfig.codec !== 'string') {
+ throw new TypeError("Audio chunk metadata decoder configuration must specify a codec string.");
+ }
+ if (!Number.isInteger(metadata.decoderConfig.sampleRate) || metadata.decoderConfig.sampleRate! <= 0) {
+ throw new TypeError("Audio chunk metadata decoder configuration must specify a valid sampleRate (positive integer).");
+ }
+ if (!Number.isInteger(metadata.decoderConfig.numberOfChannels) || metadata.decoderConfig.numberOfChannels! <= 0) {
+ throw new TypeError("Audio chunk metadata decoder configuration must specify a valid numberOfChannels (positive integer).");
+ }
+ if (metadata.decoderConfig.description !== undefined) {
+ if (!isAllowSharedBufferSource(metadata.decoderConfig.description)) {
+ throw new TypeError("Audio chunk metadata decoder configuration description, when defined, must be an ArrayBuffer or an ArrayBuffer view.");
+ }
+ }
+
+ if (metadata.decoderConfig.codec === 'opus' && metadata.decoderConfig.description && metadata.decoderConfig.description.byteLength < 18) {
+ throw new TypeError('Invalid decoder description provided for Opus; must be at least 18 bytes long.');
+ }
+};
+
+export const validateSubtitleMetadata = (metadata: SubtitleMetadata | undefined) => {
+ if (!metadata) {
+ throw new TypeError("Subtitle metadata must be provided.");
+ }
+ if (typeof metadata !== 'object') {
+ throw new TypeError("Subtitle metadata must be an object.");
+ }
+ if (!metadata.config) {
+ throw new TypeError("Subtitle metadata must include a config object.");
+ }
+ if (typeof metadata.config !== 'object') {
+ throw new TypeError("Subtitle metadata config must be an object.");
+ }
+ if (typeof metadata.config.description !== 'string') {
+ throw new TypeError("Subtitle metadata config description must be a string.");
+ }
+}
\ No newline at end of file
diff --git a/src/isobmff/isobmff_boxes.ts b/src/isobmff/isobmff_boxes.ts
index bb57e3c..a4b7231 100644
--- a/src/isobmff/isobmff_boxes.ts
+++ b/src/isobmff/isobmff_boxes.ts
@@ -146,6 +146,31 @@ const fixed_2_30 = (value: number) => {
return [bytes[0], bytes[1], bytes[2], bytes[3]] as number[];
};
+const variableUnsignedInt = (value: number, byteLength?: number) => {
+ let bytes: number[] = [];
+ let remaining = value;
+
+ do {
+ let byte = remaining & 0x7f;
+ remaining >>= 7;
+
+ // If this isn't the first byte we're adding (meaning there will be more bytes after it
+ // when we reverse the array), set the continuation bit
+ if (bytes.length > 0) {
+ byte |= 0x80;
+ }
+
+ bytes.push(byte);
+
+ if (byteLength !== undefined) {
+ byteLength--;
+ }
+ } while (remaining > 0 || byteLength);
+
+ // Reverse the array since we built it backwards
+ return bytes.reverse();
+};
+
const ascii = (text: string, nullTerminated = false) => {
let bytes = Array(text.length).fill(null).map((_, i) => text.charCodeAt(i));
if (nullTerminated) bytes.push(0x00);
@@ -279,7 +304,7 @@ export const mvhd = (
return lastSample.timestamp + lastSample.duration;
})
), GLOBAL_TIMESCALE);
- let nextTrackId = Math.max(...trackDatas.map(x => x.track.id)) + 1;
+ let nextTrackId = Math.max(0, ...trackDatas.map(x => x.track.id)) + 1;
// Conditionally use u64 if u32 isn't enough
let needsU64 = !isU32(creationTime) || !isU32(duration);
@@ -538,9 +563,6 @@ export const colr = (trackData: IsobmffVideoTrackData) => box('colr', [
u8((trackData.info.decoderConfig.colorSpace!.fullRange ? 1 : 0) << 7) // Full range flag
]);
-// TODO: All muxers should ensure that the decoder config description is provided for the codecs that require it. This
-// is relevant when the user skips WebCodecs and uses their own encoder.
-
/** AVC Configuration Box: Provides additional information to the decoder. */
export const avcC = (trackData: IsobmffVideoTrackData) => trackData.info.decoderConfig && box('avcC', [
// For AVC, description is an AVCDecoderConfigurationRecord, so nothing else to do here
@@ -549,7 +571,7 @@ export const avcC = (trackData: IsobmffVideoTrackData) => trackData.info.decoder
/** HEVC Configuration Box: Provides additional information to the decoder. */
export const hvcC = (trackData: IsobmffVideoTrackData) => trackData.info.decoderConfig && box('hvcC', [
- // For HEVC, description is a HEVCDecoderConfigurationRecord, so nothing else to do here
+ // For HEVC, description is an HEVCDecoderConfigurationRecord, so nothing else to do here
...toUint8Array(trackData.info.decoderConfig.description!)
]);
@@ -562,9 +584,7 @@ export const vpcC = (trackData: IsobmffVideoTrackData) => {
}
let decoderConfig = trackData.info.decoderConfig;
- if (!decoderConfig.colorSpace) {
- throw new Error(`'colorSpace' is required in the decoder config for VP8/VP9.`);
- }
+ assert(decoderConfig.colorSpace); // This is guaranteed by an earlier validation step
let parts = decoderConfig.codec.split('.');
let profile = Number(parts[1]);
@@ -631,28 +651,39 @@ export const soundSampleDescription = (
export const esds = (trackData: IsobmffAudioTrackData) => {
let description = toUint8Array(trackData.info.decoderConfig.description ?? new ArrayBuffer(0));
- // TODO Compact the 808080 stuff, it's superfluous
+ // Adapted from https://stackoverflow.com/a/54803118
+ // We build up the bytes in a layered way which reflects the nested structure
- return fullBox('esds', 0, 0, [
- // https://stackoverflow.com/a/54803118
- u32(0x03808080), // TAG(3) = Object Descriptor ([2])
- u8(0x20 + description.byteLength), // length of this OD (which includes the next 2 tags)
- u16(1), // ES_ID = 1
- u8(0x00), // flags etc = 0
- u32(0x04808080), // TAG(4) = ES Descriptor ([2]) embedded in above OD
- u8(0x12 + description.byteLength), // length of this ESD
- u8(0x40), // MPEG-4 Audio
- u8(0x15), // stream type(6bits)=5 audio, flags(2bits)=1
- u24(0), // 24bit buffer size
- u32(0x0001FC17), // max bitrate
- u32(0x0001FC17), // avg bitrate
- u32(0x05808080), // TAG(5) = ASC ([2],[3]) embedded in above OD
- u8(description.byteLength), // length
- ...description,
- u32(0x06808080), // TAG(6)
- u8(0x01), // length
- u8(0x02) // data
- ]);
+ let bytes = [
+ ...description
+ ];
+ bytes = [
+ ...u8(0x40), // MPEG-4 Audio
+ ...u8(0x15), // stream type(6bits)=5 audio, flags(2bits)=1
+ ...u24(0), // 24bit buffer size
+ ...u32(0), // max bitrate
+ ...u32(0), // avg bitrate
+ ...u8(0x05), // TAG(5) = ASC ([2],[3]) embedded in above OD
+ ...variableUnsignedInt(bytes.length),
+ ...bytes
+ ];
+ bytes = [
+ ...u16(1), // ES_ID = 1
+ ...u8(0x00), // flags etc = 0
+ ...u8(0x04), // TAG(4) = ES Descriptor ([2]) embedded in above OD
+ ...variableUnsignedInt(bytes.length),
+ ...bytes,
+ ...u8(0x06), // TAG(6)
+ ...u8(0x01), // length
+ ...u8(0x02) // data
+ ];
+ bytes = [
+ ...u8(0x03), // TAG(3) = Object Descriptor ([2])
+ ...variableUnsignedInt(bytes.length),
+ ...bytes
+ ];
+
+ return fullBox('esds', 0, 0, bytes);
};
/** Opus Specific Box. */
@@ -665,9 +696,7 @@ export const dOps = (trackData: IsobmffAudioTrackData) => {
// https://www.rfc-editor.org/rfc/rfc7845#section-5
const description = trackData.info.decoderConfig?.description;
if (description) {
- if (description.byteLength < 18) {
- throw new TypeError('Invalid decoder description provided for Opus; must be at least 18 bytes long.');
- }
+ assert(description.byteLength < 18); // Is validated in an earlier step
const view = ArrayBuffer.isView(description)
? new DataView(description.buffer, description.byteOffset, description.byteLength)
diff --git a/src/isobmff/isobmff_muxer.ts b/src/isobmff/isobmff_muxer.ts
index e4e687a..6249e64 100644
--- a/src/isobmff/isobmff_muxer.ts
+++ b/src/isobmff/isobmff_muxer.ts
@@ -1,11 +1,12 @@
import { Box, free, ftyp, IsobmffBoxWriter, mdat, mfra, moof, moov, vtta, vttc, vtte } from './isobmff_boxes';
import { Muxer } from '../muxer';
import { Output, OutputAudioTrack, OutputSubtitleTrack, OutputTrack, OutputVideoTrack } from '../output';
-import { Writer } from '../writer';
+import { ArrayBufferTargetWriter, Writer } from '../writer';
import { assert, last, TransformationMatrix } from '../misc';
import { Mp4OutputFormat } from '../output_format';
import { inlineTimestampRegex, SubtitleConfig, SubtitleCue, SubtitleMetadata } from '../subtitles';
import { ArrayBufferTarget } from '../target';
+import { validateAudioChunkMetadata, validateSubtitleMetadata, validateVideoChunkMetadata } from '../codec';
export const GLOBAL_TIMESCALE = 1000;
const TIMESTAMP_OFFSET = 2_082_844_800; // Seconds between Jan 1 1904 and Jan 1 1970
@@ -88,6 +89,7 @@ export class IsobmffMuxer extends Muxer {
#writer: Writer;
#boxWriter: IsobmffBoxWriter;
#format: Mp4OutputFormat;
+ #fastStart: NonNullable;
#auxTarget = new ArrayBufferTarget();
#auxWriter = this.#auxTarget.createWriter();
@@ -109,6 +111,14 @@ export class IsobmffMuxer extends Muxer {
this.#writer = output.writer;
this.#boxWriter = new IsobmffBoxWriter(this.#writer);
this.#format = format;
+
+ // If the fastStart option isn't defined, enable in-memory fast start if the target is an ArrayBuffer, as the
+ // memory usage remains identical
+ this.#fastStart = format.options.fastStart ?? (this.#writer instanceof ArrayBufferTargetWriter ? 'in-memory' : false);
+
+ if (this.#fastStart === 'in-memory' || this.#fastStart === 'fragmented') {
+ this.#writer.ensureMonotonicity = true;
+ }
}
start() {
@@ -117,21 +127,16 @@ export class IsobmffMuxer extends Muxer {
// Write the header
this.#boxWriter.writeBox(ftyp({
holdsAvc: holdsAvc,
- fragmented: this.#format.options.fastStart === 'fragmented'
+ fragmented: this.#fastStart === 'fragmented'
}));
this.#ftypSize = this.#writer.getPos();
- if (this.#format.options.fastStart === 'in-memory') {
+ if (this.#fastStart === 'in-memory') {
this.#mdat = mdat(false);
- } else if (this.#format.options.fastStart === 'fragmented') {
+ } else if (this.#fastStart === 'fragmented') {
// We write the moov box once we write out the first fragment to make sure we get the decoder configs
} else {
- if (typeof this.#format.options.fastStart === 'object') {
- let moovSizeUpperBound = this.#computeMoovSizeUpperBound();
- this.#writer.seek(this.#writer.getPos() + moovSizeUpperBound);
- }
-
this.#mdat = mdat(true); // Reserve large size by default, can refine this when finalizing.
this.#boxWriter.writeBox(this.#mdat);
}
@@ -139,45 +144,14 @@ export class IsobmffMuxer extends Muxer {
this.#writer.flush();
}
- #computeMoovSizeUpperBound() {
- assert(typeof this.#format.options.fastStart === 'object');
-
- let upperBound = 0;
- let sampleCounts = [
- this.#format.options.fastStart.expectedVideoChunks,
- this.#format.options.fastStart.expectedAudioChunks
- ];
-
- for (let n of sampleCounts) {
- if (!n) continue;
-
- // Given the max allowed sample count, compute the space they'll take up in the Sample Table Box, assuming
- // the worst case for each individual box:
-
- // stts box - since it is compactly coded, the maximum length of this table will be 2/3n
- upperBound += (4 + 4) * Math.ceil(2/3 * n);
- // stss box - 1 entry per sample
- upperBound += 4 * n;
- // stsc box - since it is compactly coded, the maximum length of this table will be 2/3n
- upperBound += (4 + 4 + 4) * Math.ceil(2/3 * n);
- // stsz box - 1 entry per sample
- upperBound += 4 * n;
- // co64 box - we assume 1 sample per chunk and 64-bit chunk offsets
- upperBound += 8 * n;
- }
-
- upperBound += 4096; // Assume a generous 4 kB for everything else: Track metadata, codec descriptors, etc.
-
- return upperBound;
- }
-
#getVideoTrackData(track: OutputVideoTrack, meta?: EncodedVideoChunkMetadata) {
const existingTrackData = this.#trackDatas.find(x => x.track === track);
if (existingTrackData) {
return existingTrackData as IsobmffVideoTrackData;
}
- // TODO Make proper errors for these
+ validateVideoChunkMetadata(meta);
+
assert(meta);
assert(meta.decoderConfig);
assert(meta.decoderConfig.codedWidth !== undefined);
@@ -216,7 +190,8 @@ export class IsobmffMuxer extends Muxer {
return existingTrackData as IsobmffAudioTrackData;
}
- // TODO Make proper errors for these
+ validateAudioChunkMetadata(meta);
+
assert(meta);
assert(meta.decoderConfig);
@@ -253,7 +228,8 @@ export class IsobmffMuxer extends Muxer {
return existingTrackData as IsobmffSubtitleTrackData;
}
- // TODO Make proper errors for these
+ validateSubtitleMetadata(meta);
+
assert(meta);
assert(meta.config);
@@ -293,62 +269,30 @@ export class IsobmffMuxer extends Muxer {
addEncodedVideoChunk(track: OutputVideoTrack, chunk: EncodedVideoChunk, meta?: EncodedVideoChunkMetadata) {
const trackData = this.#getVideoTrackData(track, meta);
- if (
- typeof this.#format.options.fastStart === 'object' &&
- trackData.samples.length === this.#format.options.fastStart.expectedVideoChunks
- ) {
- // TODO reference track id
- throw new Error(`Cannot add more video chunks than specified in 'fastStart' (${
- this.#format.options.fastStart.expectedVideoChunks
- }).`);
- }
-
let data = new Uint8Array(chunk.byteLength);
chunk.copyTo(data);
let timestamp = this.validateAndNormalizeTimestamp(trackData.track, chunk.timestamp, chunk.type === 'key');
let sample = this.#createSampleForTrack(trackData, data, timestamp, (chunk.duration ?? 0) / 1e6, chunk.type);
- if (this.#format.options.fastStart === 'fragmented') {
- trackData.sampleQueue.push(sample);
- this.#interleaveSamples();
- } else {
- this.#addSampleToTrack(trackData, sample);
- }
+ this.#registerSample(trackData, sample);
}
addEncodedAudioChunk(track: OutputAudioTrack, chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata) {
const trackData = this.#getAudioTrackData(track, meta);
- if (
- typeof this.#format.options.fastStart === 'object' &&
- trackData.samples.length === this.#format.options.fastStart.expectedAudioChunks
- ) {
- // TODO reference track id
- throw new Error(`Cannot add more audio chunks than specified in 'fastStart' (${
- this.#format.options.fastStart.expectedAudioChunks
- }).`);
- }
-
let data = new Uint8Array(chunk.byteLength);
chunk.copyTo(data);
let timestamp = this.validateAndNormalizeTimestamp(trackData.track, chunk.timestamp, chunk.type === 'key');
let sample = this.#createSampleForTrack(trackData, data, timestamp, (chunk.duration ?? 0) / 1e6, chunk.type);
- if (this.#format.options.fastStart === 'fragmented') {
- trackData.sampleQueue.push(sample);
- this.#interleaveSamples();
- } else {
- this.#addSampleToTrack(trackData, sample);
- }
+ this.#registerSample(trackData, sample);
}
addSubtitleCue(track: OutputSubtitleTrack, cue: SubtitleCue, meta?: SubtitleMetadata) {
const trackData = this.#getSubtitleTrackData(track, meta);
- // TODO the expectedSubtitleChunks thing
-
this.validateAndNormalizeTimestamp(trackData.track, 1e6 * cue.timestamp, true);
if (track.source.codec === 'webvtt') {
@@ -392,14 +336,7 @@ export class IsobmffMuxer extends Muxer {
let body = this.#auxWriter.getSlice(0, this.#auxWriter.getPos());
let sample = this.#createSampleForTrack(trackData, body, trackData.lastCueEndTimestamp, sampleStart - trackData.lastCueEndTimestamp, 'key');
- // todo extract this into reusable thing
- if (this.#format.options.fastStart === 'fragmented') {
- trackData.sampleQueue.push(sample);
- this.#interleaveSamples();
- } else {
- this.#addSampleToTrack(trackData, sample);
- }
-
+ this.#registerSample(trackData, sample);
trackData.lastCueEndTimestamp = sampleStart;
}
@@ -442,14 +379,7 @@ export class IsobmffMuxer extends Muxer {
let body = this.#auxWriter.getSlice(0, this.#auxWriter.getPos());
let sample = this.#createSampleForTrack(trackData, body, sampleStart, sampleEnd - sampleStart, 'key');
- // todo extract this into reusable thing
- if (this.#format.options.fastStart === 'fragmented') {
- trackData.sampleQueue.push(sample);
- this.#interleaveSamples();
- } else {
- this.#addSampleToTrack(trackData, sample);
- }
-
+ this.#registerSample(trackData, sample);
trackData.lastCueEndTimestamp = sampleEnd;
}
}
@@ -503,7 +433,7 @@ export class IsobmffMuxer extends Muxer {
trackData.lastTimescaleUnits += delta;
trackData.lastSample.timescaleUnitsToNextSample = delta;
- if (this.#format.options.fastStart !== 'fragmented') {
+ if (this.#fastStart !== 'fragmented') {
let lastTableEntry = last(trackData.timeToSampleTable);
assert(lastTableEntry);
@@ -555,7 +485,7 @@ export class IsobmffMuxer extends Muxer {
} else {
trackData.lastTimescaleUnits = 0;
- if (this.#format.options.fastStart !== 'fragmented') {
+ if (this.#fastStart !== 'fragmented') {
trackData.timeToSampleTable.push({
sampleCount: 1,
sampleDelta: durationInTimescale
@@ -573,15 +503,21 @@ export class IsobmffMuxer extends Muxer {
trackData.timestampProcessingQueue.length = 0;
}
- #addSampleToTrack(
- trackData: IsobmffTrackData,
- sample: Sample
- ) {
+ #registerSample(trackData: IsobmffTrackData, sample: Sample) {
+ if (this.#fastStart === 'fragmented') {
+ trackData.sampleQueue.push(sample);
+ this.#interleaveSamples();
+ } else {
+ this.#addSampleToTrack(trackData, sample);
+ }
+ }
+
+ #addSampleToTrack(trackData: IsobmffTrackData, sample: Sample) {
if (sample.type === 'key') {
this.#processTimestamps(trackData);
}
- if (this.#format.options.fastStart !== 'fragmented') {
+ if (this.#fastStart !== 'fragmented') {
trackData.samples.push(sample);
}
@@ -591,7 +527,7 @@ export class IsobmffMuxer extends Muxer {
} else {
let currentChunkDuration = sample.timestamp - trackData.currentChunk.startTimestamp;
- if (this.#format.options.fastStart === 'fragmented') {
+ if (this.#fastStart === 'fragmented') {
// We can only finalize this fragment (and begin a new one) if we know that each track will be able to
// start the new one with a key frame.
const keyFrameQueuedEverywhere = this.#trackDatas.every(otherTrackData => {
@@ -631,7 +567,7 @@ export class IsobmffMuxer extends Muxer {
}
#finalizeCurrentChunk(trackData: IsobmffTrackData) {
- assert(this.#format.options.fastStart !== 'fragmented');
+ assert(this.#fastStart !== 'fragmented');
if (!trackData.currentChunk) return;
@@ -648,7 +584,7 @@ export class IsobmffMuxer extends Muxer {
});
}
- if (this.#format.options.fastStart === 'in-memory') {
+ if (this.#fastStart === 'in-memory') {
trackData.currentChunk.offset = 0; // We'll compute the proper offset when finalizing
return;
}
@@ -665,7 +601,7 @@ export class IsobmffMuxer extends Muxer {
}
#interleaveSamples() {
- assert(this.#format.options.fastStart === 'fragmented');
+ assert(this.#fastStart === 'fragmented');
for (const track of this.output.tracks) {
if (!track.source.closed && !this.#trackDatas.some(x => x.track === track)) {
@@ -699,7 +635,7 @@ export class IsobmffMuxer extends Muxer {
}
#finalizeFragment(flushWriter = true) {
- assert(this.#format.options.fastStart === 'fragmented');
+ assert(this.#fastStart === 'fragmented');
let fragmentNumber = this.#nextFragmentNumber++;
@@ -775,7 +711,7 @@ export class IsobmffMuxer extends Muxer {
}
}
- if (this.#format.options.fastStart === 'fragmented') {
+ if (this.#fastStart === 'fragmented') {
// Since a track is now closed, we may be able to write out chunks that were previously waiting
this.#interleaveSamples();
}
@@ -789,7 +725,7 @@ export class IsobmffMuxer extends Muxer {
}
}
- if (this.#format.options.fastStart === 'fragmented') {
+ if (this.#fastStart === 'fragmented') {
for (let trackData of this.#trackDatas) {
for (let sample of trackData.sampleQueue) {
this.#addSampleToTrack(trackData, sample);
@@ -800,13 +736,13 @@ export class IsobmffMuxer extends Muxer {
this.#finalizeFragment(false); // Don't flush the last fragment as we will flush it with the mfra box soon
} else {
- for (let trackData of this.#trackDatas) {
+ for (let trackData of this.#trackDatas) {
this.#processTimestamps(trackData);
this.#finalizeCurrentChunk(trackData);
}
}
- if (this.#format.options.fastStart === 'in-memory') {
+ if (this.#fastStart === 'in-memory') {
assert(this.#mdat);
let mdatSize: number;
@@ -850,7 +786,7 @@ export class IsobmffMuxer extends Muxer {
sample.data = null;
}
}
- } else if (this.#format.options.fastStart === 'fragmented') {
+ } else if (this.#fastStart === 'fragmented') {
// Append the mfra box to the end of the file for better random access
let startPos = this.#writer.getPos();
let mfraBox = mfra(this.#trackDatas);
@@ -873,7 +809,7 @@ export class IsobmffMuxer extends Muxer {
let movieBox = moov(this.#trackDatas, this.#creationTime);
- if (typeof this.#format.options.fastStart === 'object') {
+ if (typeof this.#fastStart === 'object') {
this.#writer.seek(this.#ftypSize);
this.#boxWriter.writeBox(movieBox);
diff --git a/src/matroska/matroska_muxer.ts b/src/matroska/matroska_muxer.ts
index 3165f9f..63c0328 100644
--- a/src/matroska/matroska_muxer.ts
+++ b/src/matroska/matroska_muxer.ts
@@ -1,3 +1,4 @@
+import { validateAudioChunkMetadata, validateSubtitleMetadata, validateVideoChunkMetadata } from '../codec';
import { assert, COLOR_PRIMARIES_MAP, colorSpaceIsComplete, MATRIX_COEFFICIENTS_MAP, readBits, textEncoder, toUint8Array, TRANSFER_CHARACTERISTICS_MAP, writeBits } from '../misc';
import { Muxer } from '../muxer';
import { Output, OutputAudioTrack, OutputSubtitleTrack, OutputTrack, OutputVideoTrack } from '../output';
@@ -84,13 +85,8 @@ const TRACK_TYPE_MAP: Record = {
subtitle: 17
};
-// TODO: Perhaps we can make this muxer always be streamable. We can do it similar to the MP4 muxer, where for each
-// cluster, we hold onto all of the chunks (called sample there), until it's done, and then we write it out in one go.
-// This way, we can set proper headers. Will just mean a bit more memory usage.
-// Update: Not really. There are duration fields and seek fields that are just uneditable if streaming is required.
-
export class MatroskaMuxer extends Muxer {
- override timestampsMustStartAtZero = true;
+ override timestampsMustStartAtZero = false;
#writer: Writer;
#format: WebMOutputFormat | MkvOutputFormat;
@@ -126,6 +122,10 @@ export class MatroskaMuxer extends Muxer {
this.#writer = output.writer;
this.#format = format;
+
+ if (this.#format.options.streamable) {
+ this.#writer.ensureMonotonicity = true;
+ }
}
#writeByte(value: number) {
@@ -317,7 +317,7 @@ export class MatroskaMuxer extends Muxer {
start() {
this.#writeEBMLHeader();
- if (!this.#format.options.streaming) {
+ if (!this.#format.options.streamable) {
this.#createSeekHead();
}
@@ -374,7 +374,7 @@ export class MatroskaMuxer extends Muxer {
{ id: EBMLId.TimestampScale, data: 1e6 },
{ id: EBMLId.MuxingApp, data: APP_NAME },
{ id: EBMLId.WritingApp, data: APP_NAME },
- !this.#format.options.streaming ? segmentDuration : null
+ !this.#format.options.streamable ? segmentDuration : null
] };
this.#segmentInfo = segmentInfo;
}
@@ -387,7 +387,7 @@ export class MatroskaMuxer extends Muxer {
tracksElement.data.push({ id: EBMLId.TrackEntry, data: [
{ id: EBMLId.TrackNumber, data: trackData.track.id },
{ id: EBMLId.TrackUID, data: trackData.track.id },
- { id: EBMLId.TrackType, data: TRACK_TYPE_MAP[trackData.type] }, // TODO Subtitle case
+ { id: EBMLId.TrackType, data: TRACK_TYPE_MAP[trackData.type] },
{ id: EBMLId.CodecID, data: CODEC_STRING_MAP[trackData.track.source.codec] },
...(trackData.type === 'video' ? [
(trackData.info.decoderConfig.description ? { id: EBMLId.CodecPrivate, data: toUint8Array(trackData.info.decoderConfig.description) } : null),
@@ -432,9 +432,9 @@ export class MatroskaMuxer extends Muxer {
#createSegment() {
let segment: EBML = {
id: EBMLId.Segment,
- size: this.#format.options.streaming ? -1 : SEGMENT_SIZE_BYTES,
+ size: this.#format.options.streamable ? -1 : SEGMENT_SIZE_BYTES,
data: [
- !this.#format.options.streaming ? this.#seekHead as EBML : null,
+ !this.#format.options.streamable ? this.#seekHead as EBML : null,
this.#segmentInfo,
this.#tracksElement
]
@@ -466,7 +466,8 @@ export class MatroskaMuxer extends Muxer {
return existingTrackData as MatroskaVideoTrackData;
}
- // TODO Make proper errors for these
+ validateVideoChunkMetadata(meta);
+
assert(meta);
assert(meta.decoderConfig);
assert(meta.decoderConfig.codedWidth !== undefined);
@@ -496,7 +497,8 @@ export class MatroskaMuxer extends Muxer {
return existingTrackData as MatroskaAudioTrackData;
}
- // TODO Make proper errors for these
+ validateAudioChunkMetadata(meta);
+
assert(meta);
assert(meta.decoderConfig);
@@ -524,7 +526,8 @@ export class MatroskaMuxer extends Muxer {
return existingTrackData as MatroskaAudioTrackData;
}
- // TODO Make proper errors for these
+ validateSubtitleMetadata(meta);
+
assert(meta);
assert(meta.config);
@@ -688,8 +691,6 @@ export class MatroskaMuxer extends Muxer {
/** Writes a block containing media data to the file. */
#writeBlock(trackData: MatroskaTrackData, chunk: InternalMediaChunk) {
- // TODO Update this comment. This code always runs now
- // When streaming, we create the tracks and segment after we've received the first media chunks.
// Due to the interlacing algorithm, this code will be run once we've seen one chunk from every media track.
if (!this.#segment) {
this.#createTracks();
@@ -777,7 +778,7 @@ export class MatroskaMuxer extends Muxer {
/** Creates a new Cluster element to contain media chunks. */
#createNewCluster(msTimestamp: number) {
- if (this.#currentCluster && !this.#format.options.streaming) {
+ if (this.#currentCluster && !this.#format.options.streamable) {
this.#finalizeCurrentCluster();
}
@@ -789,7 +790,7 @@ export class MatroskaMuxer extends Muxer {
this.#currentCluster = {
id: EBMLId.Cluster,
- size: this.#format.options.streaming ? -1 : CLUSTER_SIZE_BYTES,
+ size: this.#format.options.streamable ? -1 : CLUSTER_SIZE_BYTES,
data: [
{ id: EBMLId.Timestamp, data: msTimestamp }
]
@@ -842,6 +843,11 @@ export class MatroskaMuxer extends Muxer {
/** Finalizes the file, making it ready for use. Must be called after all media chunks have been added. */
finalize() {
+ if (!this.#segment) {
+ this.#createTracks();
+ this.#createSegment();
+ }
+
// Flush any remaining queued chunks to the file
for (let trackData of this.#trackDatas) {
while (trackData.chunkQueue.length > 0) {
@@ -849,14 +855,14 @@ export class MatroskaMuxer extends Muxer {
}
}
- if (!this.#format.options.streaming) {
+ if (!this.#format.options.streamable && this.#currentCluster) {
this.#finalizeCurrentCluster();
}
assert(this.#cues);
this.writeEBML(this.#cues);
- if (!this.#format.options.streaming) {
+ if (!this.#format.options.streamable) {
let endPos = this.#writer.getPos();
// Write the Segment size
diff --git a/src/misc.ts b/src/misc.ts
index 613fe95..0687434 100644
--- a/src/misc.ts
+++ b/src/misc.ts
@@ -73,4 +73,9 @@ export const MATRIX_COEFFICIENTS_MAP: Record =
export const colorSpaceIsComplete = (colorSpace: VideoColorSpaceInit | undefined) => {
return !!colorSpace && !!colorSpace.primaries && !!colorSpace.transfer && !!colorSpace.matrix && colorSpace.fullRange !== undefined;
+};
+
+export const isAllowSharedBufferSource = (x: unknown) => {
+ // Quite a mouthful:
+ return x instanceof ArrayBuffer || (typeof SharedArrayBuffer !== 'undefined' && x instanceof SharedArrayBuffer) || (ArrayBuffer.isView(x) && !(x instanceof DataView));
};
\ No newline at end of file
diff --git a/src/output.ts b/src/output.ts
index 475215b..164fea7 100644
--- a/src/output.ts
+++ b/src/output.ts
@@ -32,7 +32,7 @@ export type OutputAudioTrack = OutputTrack & { type: 'audio' };
export type OutputSubtitleTrack = OutputTrack & { type: 'subtitle' };
type VideoTrackMetadata = {
- rotation?: 0 | 90 | 180 | 270 | TransformationMatrix,
+ rotation?: 0 | 90 | 180 | 270 | TransformationMatrix, // TODO respect this field for Matroska
frameRate?: number
};
type AudioTrackMetadata = {};
@@ -46,6 +46,16 @@ export class Output {
finalizing = false;
constructor(options: OutputOptions) {
+ if (!options || typeof options !== 'object') {
+ throw new TypeError('options must be an object.');
+ }
+ if (!(options.format instanceof OutputFormat)) {
+ throw new TypeError('options.format must be an OutputFormat.');
+ }
+ if (!(options.target instanceof Target)) {
+ throw new TypeError('options.target must be a Target.');
+ }
+
if (options.target.output) {
throw new Error('Target is already used for another output.');
}
@@ -56,14 +66,51 @@ export class Output {
}
addVideoTrack(source: VideoSource, metadata: VideoTrackMetadata = {}) {
+ if (!(source instanceof VideoSource)) {
+ throw new TypeError('source must be a VideoSource.');
+ }
+ if (!metadata || typeof metadata !== 'object') {
+ throw new TypeError('metadata must be an object.');
+ }
+ if (typeof metadata.rotation === 'number' && ![0, 90, 180, 270].includes(metadata.rotation)) {
+ throw new TypeError(`Invalid video rotation: ${metadata.rotation}. Has to be 0, 90, 180 or 270.`);
+ } else if (
+ Array.isArray(metadata.rotation) &&
+ (metadata.rotation.length !== 9 || metadata.rotation.some(value => !Number.isFinite(value)))
+ ) {
+ throw new TypeError(`Invalid video transformation matrix: ${metadata.rotation.join()}`);
+ }
+ if (
+ metadata.frameRate !== undefined &&
+ (!Number.isInteger(metadata.frameRate) || metadata.frameRate <= 0)
+ ) {
+ throw new TypeError(
+ `Invalid video frame rate: ${metadata.frameRate}. Must be a positive integer.`
+ );
+ }
+
this.addTrack('video', source, metadata);
}
addAudioTrack(source: AudioSource, metadata: AudioTrackMetadata = {}) {
+ if (!(source instanceof AudioSource)) {
+ throw new TypeError('source must be an AudioSource.');
+ }
+ if (!metadata || typeof metadata !== 'object') {
+ throw new TypeError('metadata must be an object.');
+ }
+
this.addTrack('audio', source, metadata);
}
addSubtitleTrack(source: SubtitleSource, metadata: SubtitleTrackMetadata = {}) {
+ if (!(source instanceof SubtitleSource)) {
+ throw new TypeError('source must be a SubtitleSource.');
+ }
+ if (!metadata || typeof metadata !== 'object') {
+ throw new TypeError('metadata must be an object.');
+ }
+
this.addTrack('subtitle', source, metadata);
}
@@ -90,8 +137,6 @@ export class Output {
}
start() {
- // TODO: Warn / throw if there are no sources
-
if (this.started) {
throw new Error('Output already started.');
}
@@ -105,8 +150,6 @@ export class Output {
}
async finalize() {
- // TODO: Test what happens when finalizing without a single chunk of media
-
if (this.finalizing) {
throw new Error('Cannot call finalize twice.');
}
diff --git a/src/output_format.ts b/src/output_format.ts
index 78cd95e..cc4141a 100644
--- a/src/output_format.ts
+++ b/src/output_format.ts
@@ -9,13 +9,15 @@ export abstract class OutputFormat {
export class Mp4OutputFormat extends OutputFormat {
constructor(public options: {
- // TODO: Make this optional with a smart default
- fastStart: false | 'in-memory' | 'fragmented' | {
- // TODO: This is too simple now that multiple tracks can exist.
- expectedVideoChunks?: number,
- expectedAudioChunks?: number
- },
- }) {
+ fastStart?: false | 'in-memory' | 'fragmented',
+ } = {}) {
+ if (!options || typeof options !== 'object') {
+ throw new TypeError('options must be an object.');
+ }
+ if (options.fastStart !== undefined && ![false, 'in-memory', 'fragmented'].includes(options.fastStart)) {
+ throw new TypeError('options.fastStart, when provided, must be false, "in-memory", or "fragmented".');
+ }
+
super();
}
@@ -26,8 +28,15 @@ export class Mp4OutputFormat extends OutputFormat {
export class MkvOutputFormat extends OutputFormat {
constructor(public options: {
- streaming?: boolean // TODO: Is there a better name?
+ streamable?: boolean
} = {}) {
+ if (!options || typeof options !== 'object') {
+ throw new TypeError('options must be an object.');
+ }
+ if (options.streamable !== undefined && typeof options.streamable !== 'boolean') {
+ throw new TypeError('options.streamable, when provided, must be a boolean.');
+ }
+
super();
}
diff --git a/src/source.ts b/src/source.ts
index a892e52..ef8c27a 100644
--- a/src/source.ts
+++ b/src/source.ts
@@ -3,15 +3,20 @@ import { assert } from "./misc";
import { OutputAudioTrack, OutputSubtitleTrack, OutputTrack, OutputVideoTrack } from "./output";
import { SubtitleParser } from "./subtitles";
-export type VideoCodec = 'avc' | 'hevc' | 'vp8' | 'vp9' | 'av1';
-export type AudioCodec = 'aac' | 'opus'; // TODO add the rest
-export type SubtitleCodec = 'webvtt';
+const VIDEO_CODECS = ['avc', 'hevc', 'vp8', 'vp9', 'av1'] as const;
+const AUDIO_CODECS = ['aac', 'opus'] as const; // TODO add the rest
+const SUBTITLE_CODECS = ['webvtt'] as const; // TODO add the rest
+
+export type VideoCodec = typeof VIDEO_CODECS[number];
+export type AudioCodec = typeof AUDIO_CODECS[number];
+export type SubtitleCodec = typeof SUBTITLE_CODECS[number];
export abstract class MediaSource {
connectedTrack: OutputTrack | null = null;
closed = false;
offsetTimestamps = false;
+ // TODO this is also just internal:
ensureValidDigest() {
if (!this.connectedTrack) {
throw new Error('Cannot call digest without connecting the source to an output track.');
@@ -64,6 +69,10 @@ export abstract class VideoSource extends MediaSource {
constructor(codec: VideoCodec) {
super();
+ if (!VIDEO_CODECS.includes(codec)) {
+ throw new TypeError(`Invalid video codec '${codec}'. Must be one of: ${VIDEO_CODECS.join(', ')}.`);
+ }
+
this.codec = codec;
}
}
@@ -73,9 +82,12 @@ export class EncodedVideoChunkSource extends VideoSource {
super(codec);
}
- // TODO: Ensure that the first chunk is a key frame (same for the audio case)
-
digest(chunk: EncodedVideoChunk, meta?: EncodedVideoChunkMetadata) {
+ if (!(chunk instanceof EncodedVideoChunk)) {
+ // TODO add polyfill for browsers that don't have this
+ throw new TypeError('chunk must be an EncodedVideoChunk.');
+ }
+
this.ensureValidDigest();
this.connectedTrack?.output.muxer.addEncodedVideoChunk(this.connectedTrack, chunk, meta);
}
@@ -89,16 +101,44 @@ type VideoCodecConfig = {
latencyMode?: VideoEncoderConfig['latencyMode']
};
+const validateVideoCodecConfig = (config: VideoCodecConfig) => {
+ if (!config || typeof config !== 'object') {
+ throw new TypeError('Codec config must be an object.');
+ }
+ if (!VIDEO_CODECS.includes(config.codec)) {
+ throw new TypeError(`Invalid video codec '${config.codec}'. Must be one of: ${VIDEO_CODECS.join(', ')}.`);
+ }
+ if (!Number.isInteger(config.bitrate) || config.bitrate <= 0) {
+ throw new TypeError('config.bitrate must be a positive integer.');
+ }
+ if (config.latencyMode !== undefined && ['quality', 'realtime'].includes(config.latencyMode)) {
+ throw new TypeError("config.latencyMode, when provided, must be 'quality' or 'realtime'.");
+ }
+};
+
class VideoEncoderWrapper {
private encoder: VideoEncoder | null = null;
private lastMultipleOfKeyFrameInterval = -1;
+ private lastWidth: number | null = null;
+ private lastHeight: number | null = null;
- constructor(private source: VideoSource, private codecConfig: VideoCodecConfig) {}
+ constructor(private source: VideoSource, private codecConfig: VideoCodecConfig) {
+ validateVideoCodecConfig(codecConfig);
+ }
- // TODO: Ensure video frame size remains constant
digest(videoFrame: VideoFrame) {
this.source.ensureValidDigest();
+ // Ensure video frame size remains constant
+ if (this.lastWidth !== null && this.lastHeight !== null) {
+ if (videoFrame.codedWidth !== this.lastWidth || videoFrame.codedHeight !== this.lastHeight) {
+ throw new Error(`Video frame size must remain constant. Expected ${this.lastWidth}x${this.lastHeight}, got ${videoFrame.codedWidth}x${videoFrame.codedHeight}.`);
+ }
+ } else {
+ this.lastWidth = videoFrame.codedWidth;
+ this.lastHeight = videoFrame.codedHeight;
+ }
+
this.ensureEncoder(videoFrame);
assert(this.encoder);
@@ -119,7 +159,7 @@ class VideoEncoderWrapper {
this.encoder = new VideoEncoder({
output: (chunk, meta) => this.source.connectedTrack?.output.muxer.addEncodedVideoChunk(this.source.connectedTrack, chunk, meta),
- error: (error) => console.error(error), // TODO
+ error: (error) => console.error('Video encode error:', error),
});
this.encoder.configure({
@@ -146,6 +186,10 @@ export class VideoFrameSource extends VideoSource {
}
digest(videoFrame: VideoFrame) {
+ if (!(videoFrame instanceof VideoFrame)) {
+ throw new TypeError('videoFrame must be a VideoFrame.');
+ }
+
this.encoder.digest(videoFrame);
}
@@ -158,11 +202,22 @@ export class CanvasSource extends VideoSource {
private encoder: VideoEncoderWrapper;
constructor(private canvas: HTMLCanvasElement, codecConfig: VideoCodecConfig) {
+ if (!(canvas instanceof HTMLCanvasElement)) {
+ throw new TypeError('canvas must be an HTMLCanvasElement.');
+ }
+
super(codecConfig.codec);
this.encoder = new VideoEncoderWrapper(this, codecConfig);
}
digest(timestamp: number, duration = 0) {
+ if (!Number.isFinite(timestamp) || timestamp < 0) {
+ throw new TypeError('timestamp must be a non-negative number.');
+ }
+ if (!Number.isFinite(duration) || duration < 0) {
+ throw new TypeError('duration must be a non-negative number.');
+ }
+
const frame = new VideoFrame(this.canvas, {
timestamp: Math.round(1e6 * timestamp),
duration: Math.round(1e6 * duration),
@@ -179,45 +234,49 @@ export class CanvasSource extends VideoSource {
}
export class MediaStreamVideoTrackSource extends VideoSource {
- private encoder: VideoEncoderWrapper;
- private abortController: AbortController | null = null;
+ private encoder: VideoEncoderWrapper;
+ private abortController: AbortController | null = null;
override offsetTimestamps = true;
- constructor(private track: MediaStreamVideoTrack, codecConfig: VideoCodecConfig) {
- super(codecConfig.codec);
- this.encoder = new VideoEncoderWrapper(this, codecConfig);
- }
+ constructor(private track: MediaStreamVideoTrack, codecConfig: VideoCodecConfig) {
+ if (!(track instanceof MediaStreamTrack) || track.kind !== 'video') {
+ throw new TypeError('track must be a video MediaStreamTrack.');
+ }
- override start() {
- this.abortController = new AbortController();
-
- const processor = new MediaStreamTrackProcessor({ track: this.track });
- const consumer = new WritableStream({
- write: (videoFrame) => {
- this.encoder.digest(videoFrame);
- videoFrame.close();
- }
- });
+ super(codecConfig.codec);
+ this.encoder = new VideoEncoderWrapper(this, codecConfig);
+ }
- processor.readable.pipeTo(consumer, {
- signal: this.abortController.signal
- }).catch(err => {
- // Handle abort error silently
- if (err instanceof DOMException && err.name === 'AbortError') return;
- // Handle other errors
- console.error('Pipe error:', err);
- });
- }
+ override start() {
+ this.abortController = new AbortController();
+
+ const processor = new MediaStreamTrackProcessor({ track: this.track });
+ const consumer = new WritableStream({
+ write: (videoFrame) => {
+ this.encoder.digest(videoFrame);
+ videoFrame.close();
+ }
+ });
- override async flush() {
- if (this.abortController) {
- this.abortController.abort();
- this.abortController = null;
- }
+ processor.readable.pipeTo(consumer, {
+ signal: this.abortController.signal
+ }).catch(err => {
+ // Handle abort error silently
+ if (err instanceof DOMException && err.name === 'AbortError') return;
+ // Handle other errors
+ console.error('Pipe error:', err);
+ });
+ }
- await this.encoder.flush();
- }
+ override async flush() {
+ if (this.abortController) {
+ this.abortController.abort();
+ this.abortController = null;
+ }
+
+ await this.encoder.flush();
+ }
}
export abstract class AudioSource extends MediaSource {
@@ -227,6 +286,10 @@ export abstract class AudioSource extends MediaSource {
constructor(codec: AudioCodec) {
super();
+ if (!AUDIO_CODECS.includes(codec)) {
+ throw new TypeError(`Invalid audio codec '${codec}'. Must be one of: ${AUDIO_CODECS.join(', ')}.`);
+ }
+
this.codec = codec;
}
}
@@ -237,6 +300,11 @@ export class EncodedAudioChunkSource extends AudioSource {
}
digest(chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata) {
+ if (!(chunk instanceof EncodedAudioChunk)) {
+ // TODO add polyfill for browsers that don't have this
+ throw new TypeError('chunk must be an EncodedAudioChunk.');
+ }
+
this.ensureValidDigest();
this.connectedTrack?.output.muxer.addEncodedAudioChunk(this.connectedTrack, chunk, meta);
}
@@ -247,142 +315,179 @@ type AudioCodecConfig = {
bitrate: number
};
+const validateAudioCodecConfig = (config: AudioCodecConfig) => {
+ if (!config || typeof config !== 'object') {
+ throw new TypeError('Codec config must be an object.');
+ }
+ if (!AUDIO_CODECS.includes(config.codec)) {
+ throw new TypeError(`Invalid audio codec '${config.codec}'. Must be one of: ${AUDIO_CODECS.join(', ')}.`);
+ }
+ if (!Number.isInteger(config.bitrate) || config.bitrate <= 0) {
+ throw new TypeError('config.bitrate must be a positive integer.');
+ }
+};
+
class AudioEncoderWrapper {
- private encoder: AudioEncoder | null = null;
+ private encoder: AudioEncoder | null = null;
+ private lastNumberOfChannels: number | null = null;
+ private lastSampleRate: number | null = null;
- constructor(private source: AudioSource, private codecConfig: AudioCodecConfig) {}
+ constructor(private source: AudioSource, private codecConfig: AudioCodecConfig) {
+ validateAudioCodecConfig(codecConfig);
+ }
- // TODO: Ensure audio parameters remain constant
- digest(audioData: AudioData) {
- this.source.ensureValidDigest();
+ digest(audioData: AudioData) {
+ this.source.ensureValidDigest();
- this.ensureEncoder(audioData);
- assert(this.encoder);
+ // Ensure audio parameters remain constant
+ if (this.lastNumberOfChannels !== null && this.lastSampleRate !== null) {
+ if (audioData.numberOfChannels !== this.lastNumberOfChannels || audioData.sampleRate !== this.lastSampleRate) {
+ throw new Error(`Audio parameters must remain constant. Expected ${this.lastNumberOfChannels} channels at ${this.lastSampleRate} Hz, got ${audioData.numberOfChannels} channels at ${audioData.sampleRate} Hz.`);
+ }
+ } else {
+ this.lastNumberOfChannels = audioData.numberOfChannels;
+ this.lastSampleRate = audioData.sampleRate;
+ }
- this.encoder.encode(audioData);
- }
+ this.ensureEncoder(audioData);
+ assert(this.encoder);
- private ensureEncoder(audioData: AudioData) {
- if (this.encoder) {
- return;
- }
+ this.encoder.encode(audioData);
+ }
- this.encoder = new AudioEncoder({
- output: (chunk, meta) => this.source.connectedTrack?.output.muxer.addEncodedAudioChunk(this.source.connectedTrack, chunk, meta),
- error: (error) => console.error(error), // TODO
- });
+ private ensureEncoder(audioData: AudioData) {
+ if (this.encoder) {
+ return;
+ }
- this.encoder.configure({
- codec: buildAudioCodecString(this.codecConfig.codec, audioData.numberOfChannels, audioData.sampleRate),
- numberOfChannels: audioData.numberOfChannels,
- sampleRate: audioData.sampleRate,
- bitrate: this.codecConfig.bitrate,
- });
- }
-
- async flush() {
- return this.encoder?.flush();
- }
+ this.encoder = new AudioEncoder({
+ output: (chunk, meta) => this.source.connectedTrack?.output.muxer.addEncodedAudioChunk(this.source.connectedTrack, chunk, meta),
+ error: (error) => console.error('Audio encode error:', error),
+ });
+
+ this.encoder.configure({
+ codec: buildAudioCodecString(this.codecConfig.codec, audioData.numberOfChannels, audioData.sampleRate),
+ numberOfChannels: audioData.numberOfChannels,
+ sampleRate: audioData.sampleRate,
+ bitrate: this.codecConfig.bitrate,
+ });
+ }
+
+ async flush() {
+ return this.encoder?.flush();
+ }
}
export class AudioDataSource extends AudioSource {
- private encoder: AudioEncoderWrapper;
+ private encoder: AudioEncoderWrapper;
- constructor(codecConfig: AudioCodecConfig) {
- super(codecConfig.codec);
- this.encoder = new AudioEncoderWrapper(this, codecConfig);
- }
+ constructor(codecConfig: AudioCodecConfig) {
+ super(codecConfig.codec);
+ this.encoder = new AudioEncoderWrapper(this, codecConfig);
+ }
- digest(audioData: AudioData) {
- this.encoder.digest(audioData);
- }
+ digest(audioData: AudioData) {
+ if (!(audioData instanceof AudioData)) {
+ throw new TypeError('audioData must be an AudioData.');
+ }
- override flush() {
- return this.encoder.flush();
- }
+ this.encoder.digest(audioData);
+ }
+
+ override flush() {
+ return this.encoder.flush();
+ }
}
export class AudioBufferSource extends AudioSource {
- private encoder: AudioEncoderWrapper;
- private accumulatedFrameCount = 0;
+ private encoder: AudioEncoderWrapper;
+ private accumulatedFrameCount = 0;
- constructor(codecConfig: AudioCodecConfig) {
- super(codecConfig.codec);
- this.encoder = new AudioEncoderWrapper(this, codecConfig);
- }
+ constructor(codecConfig: AudioCodecConfig) {
+ super(codecConfig.codec);
+ this.encoder = new AudioEncoderWrapper(this, codecConfig);
+ }
- digest(audioBuffer: AudioBuffer) {
- const numberOfChannels = audioBuffer.numberOfChannels;
- const sampleRate = audioBuffer.sampleRate;
- const numberOfFrames = audioBuffer.length;
-
- // Create a planar F32 array containing all channels
- const data = new Float32Array(numberOfChannels * numberOfFrames);
- for (let channel = 0; channel < numberOfChannels; channel++) {
- const channelData = audioBuffer.getChannelData(channel);
- data.set(channelData, channel * numberOfFrames);
- }
+ digest(audioBuffer: AudioBuffer) {
+ if (!(audioBuffer instanceof AudioBuffer)) {
+ throw new TypeError('audioBuffer must be an AudioBuffer.');
+ }
- const audioData = new AudioData({
- format: 'f32-planar',
- sampleRate,
- numberOfFrames,
- numberOfChannels,
- timestamp: Math.round(1e6 * this.accumulatedFrameCount / sampleRate),
- data: data
- });
+ const numberOfChannels = audioBuffer.numberOfChannels;
+ const sampleRate = audioBuffer.sampleRate;
+ const numberOfFrames = audioBuffer.length;
+
+ // Create a planar F32 array containing all channels
+ const data = new Float32Array(numberOfChannels * numberOfFrames);
+ for (let channel = 0; channel < numberOfChannels; channel++) {
+ const channelData = audioBuffer.getChannelData(channel);
+ data.set(channelData, channel * numberOfFrames);
+ }
- this.encoder.digest(audioData);
- audioData.close();
+ const audioData = new AudioData({
+ format: 'f32-planar',
+ sampleRate,
+ numberOfFrames,
+ numberOfChannels,
+ timestamp: Math.round(1e6 * this.accumulatedFrameCount / sampleRate),
+ data: data
+ });
+
+ this.encoder.digest(audioData);
+ audioData.close();
this.accumulatedFrameCount += numberOfFrames;
- }
+ }
- override flush() {
- return this.encoder.flush();
- }
+ override flush() {
+ return this.encoder.flush();
+ }
}
export class MediaStreamAudioTrackSource extends AudioSource {
- private encoder: AudioEncoderWrapper;
- private abortController: AbortController | null = null;
+ private encoder: AudioEncoderWrapper;
+ private abortController: AbortController | null = null;
override offsetTimestamps = true;
- constructor(private track: MediaStreamAudioTrack, codecConfig: AudioCodecConfig) {
- super(codecConfig.codec);
- this.encoder = new AudioEncoderWrapper(this, codecConfig);
- }
+ constructor(private track: MediaStreamAudioTrack, codecConfig: AudioCodecConfig) {
+ if (!(track instanceof MediaStreamTrack) || track.kind !== 'audio') {
+ throw new TypeError('track must be an audio MediaStreamTrack.');
+ }
- override start() {
- this.abortController = new AbortController();
-
- const processor = new MediaStreamTrackProcessor({ track: this.track });
- const consumer = new WritableStream({
- write: (audioData) => {
- this.encoder.digest(audioData);
- audioData.close();
- }
- });
+ super(codecConfig.codec);
+ this.encoder = new AudioEncoderWrapper(this, codecConfig);
+ }
- processor.readable.pipeTo(consumer, {
- signal: this.abortController.signal
- }).catch(err => {
- // Handle abort error silently
- if (err instanceof DOMException && err.name === 'AbortError') return;
- // Handle other errors
- console.error('Pipe error:', err);
- });
- }
+ override start() {
+ this.abortController = new AbortController();
+
+ const processor = new MediaStreamTrackProcessor({ track: this.track });
+ const consumer = new WritableStream({
+ write: (audioData) => {
+ this.encoder.digest(audioData);
+ audioData.close();
+ }
+ });
- override async flush() {
- if (this.abortController) {
- this.abortController.abort();
- this.abortController = null;
- }
+ processor.readable.pipeTo(consumer, {
+ signal: this.abortController.signal
+ }).catch(err => {
+ // Handle abort error silently
+ if (err instanceof DOMException && err.name === 'AbortError') return;
+ // Handle other errors
+ console.error('Pipe error:', err);
+ });
+ }
- await this.encoder.flush();
- }
+ override async flush() {
+ if (this.abortController) {
+ this.abortController.abort();
+ this.abortController = null;
+ }
+
+ await this.encoder.flush();
+ }
}
export abstract class SubtitleSource extends MediaSource {
@@ -392,6 +497,10 @@ export abstract class SubtitleSource extends MediaSource {
constructor(codec: SubtitleCodec) {
super();
+ if (!SUBTITLE_CODECS.includes(codec)) {
+ throw new TypeError(`Invalid subtitle codec '${codec}'. Must be one of: ${SUBTITLE_CODECS.join(', ')}.`);
+ }
+
this.codec = codec;
}
}
@@ -405,11 +514,15 @@ export class TextSubtitleSource extends SubtitleSource {
this.parser = new SubtitleParser({
codec,
output: (cue, metadata) => this.connectedTrack?.output.muxer.addSubtitleCue(this.connectedTrack, cue, metadata),
- error: (error) => console.error(error) // TODO
+ error: (error) => console.error('Subtitle parse error:', error)
});
}
digest(text: string) {
+ if (typeof text !== 'string') {
+ throw new TypeError('text must be a string.');
+ }
+
this.ensureValidDigest();
this.parser.parse(text);
}
diff --git a/src/writer.ts b/src/writer.ts
index 244fc41..5bd384e 100644
--- a/src/writer.ts
+++ b/src/writer.ts
@@ -1,6 +1,9 @@
import { ArrayBufferTarget, FileSystemWritableFileStreamTarget, StreamTarget } from './target';
export abstract class Writer {
+ /** Setting this to true will cause the writer to ensure data is written in a strictly monotonic, streamable way. */
+ ensureMonotonicity = false;
+
/** Writes the given data to the target, at the current position. */
abstract write(data: Uint8Array): void;
/** Sets the current position for future writes to a new one. */
@@ -84,6 +87,7 @@ export class StreamTargetWriter extends Writer {
data: Uint8Array,
start: number
}[] = [];
+ #lastFlushEnd = 0;
constructor(target: StreamTarget) {
super();
@@ -148,7 +152,12 @@ export class StreamTargetWriter extends Writer {
}
}
+ if (this.ensureMonotonicity && chunk.start !== this.#lastFlushEnd) {
+ throw new Error('Internal error: Monotonicity violation.');
+ }
+
this.#target.options.onData?.(chunk.data, chunk.start);
+ this.#lastFlushEnd = chunk.start + chunk.data.byteLength;
}
this.#sections.length = 0;
@@ -185,6 +194,7 @@ export class ChunkedStreamTargetWriter extends Writer {
* A chunk is flushed if all of its contents have been written.
*/
#chunks: Chunk[] = [];
+ #lastFlushEnd = 0;
constructor(target: StreamTarget) {
super();
@@ -298,10 +308,15 @@ export class ChunkedStreamTargetWriter extends Writer {
if (!chunk.shouldFlush && !force) continue;
for (let section of chunk.written) {
+ if (this.ensureMonotonicity && chunk.start + section.start !== this.#lastFlushEnd) {
+ throw new Error('Internal error: Monotonicity violation.');
+ }
+
this.#target.options.onData?.(
chunk.data.subarray(section.start, section.end),
chunk.start + section.start
);
+ this.#lastFlushEnd = chunk.start + section.end;
}
this.#chunks.splice(i--, 1);
}