From 29ecccb26af14b0503134e942d0d6146fd975025 Mon Sep 17 00:00:00 2001
From: Vanilagy <1696106+Vanilagy@users.noreply.github.com>
Date: Sun, 24 Nov 2024 14:57:31 +0100
Subject: [PATCH] Add WebVTT subtitle support to MP4 & a bunch of misc changes
---
dev/index.html | 60 +-
dist/metamuxer.js | 1402 ++++++++++++++++++--------------
dist/metamuxer.min.js | 14 +-
dist/metamuxer.min.mjs | 14 +-
dist/metamuxer.mjs | 1402 ++++++++++++++++++--------------
src/isobmff/isobmff_boxes.ts | 208 ++++-
src/isobmff/isobmff_muxer.ts | 451 ++++++----
src/matroska/ebml.ts | 7 +-
src/matroska/matroska_muxer.ts | 112 ++-
src/misc.ts | 4 +-
src/muxer.ts | 57 +-
src/output_format.ts | 2 +
src/source.ts | 32 +-
src/subtitles.ts | 143 ++--
src/writer.ts | 4 +
15 files changed, 2355 insertions(+), 1557 deletions(-)
diff --git a/dev/index.html b/dev/index.html
index d5bc908..d679e00 100644
--- a/dev/index.html
+++ b/dev/index.html
@@ -36,7 +36,8 @@
canvas.height = 720;
const context = canvas.getContext('2d');
- let format = new Metamuxer.WebMOutputFormat();// new Metamuxer.MkvOutputFormat();// new Metamuxer.Mp4OutputFormat({ fastStart: false });
+ let format = new Metamuxer.WebMOutputFormat();
+ format = new Metamuxer.Mp4OutputFormat({ fastStart: false }); // new Metamuxer.MkvOutputFormat();// new Metamuxer.Mp4OutputFormat({ fastStart: false });
let target = new Metamuxer.ArrayBufferTarget();
let output = new Metamuxer.Output({ format, target });
@@ -71,11 +72,11 @@
*/
let videoSource = new Metamuxer.CanvasSource(canvas, {
- codec: 'vp9',
+ codec: 'avc',
bitrate: 1e6
});
let audioSource = new Metamuxer.AudioBufferSource({
- codec: 'opus',
+ codec: 'aac',
bitrate: 128e3,
});
let subtitleSource = new Metamuxer.TextSubtitleSource('webvtt');
@@ -89,9 +90,58 @@
let simpleWebvttFile =
`WEBVTT
-00:00:00.000 --> 00:00:10.000
+1
+00:00:02.000 --> 00:00:10.000
Example entry 1: Hello world.
`;
+
+if (false) simpleWebvttFile =
+`WEBVTT
+
+1
+00:05.000 --> 00:12.500 align:start line:10
+We are in New York City.
+We are looking straight down 5th Avenue.
+
+00:13.000 --> 00:18.000
+Didn't you already say that?
+
+2
+00:17.000 --> 00:20.000
+Testing... <00:17.350>One... <00:18.125>Two...
+`
+
+ simpleWebvttFile =
+`WEBVTT
+
+00:00:01.000 --> 00:00:01.500 align:start line:1
+1. justify (top, left),
+
+00:00:01.500 --> 00:00:02.000 align:center line:1.5
+2. justify (top, center),
+
+00:00:02.000 --> 00:00:02.500 align:end line:2
+3. justify (top, right),
+
+00:00:02.500 --> 00:00:03.000 align:start line:48%
+4. justify (center, left),
+
+00:00:03.000 --> 00:00:03.500 align:center line:50%
+5. justify (center, center),
+
+00:00:03.500 --> 00:00:04.000 align:end line:52%
+6. justify (center, right),
+
+00:00:04.000 --> 00:00:04.500 align:start line:-1
+7. justify (bottom, left),
+
+00:00:04.500 --> 00:00:05.000 align:center line:-1.5
+8. justify (bottom, center),
+
+00:00:05.000 --> 00:00:05.500 align:end line:-2
+9. justify (bottom, right).
+`;
+
subtitleSource.digest(simpleWebvttFile);
subtitleSource.close();
@@ -112,5 +162,5 @@ Example entry 1: Hello world.
await output.finalize();
console.log(target);
- download(new Blob([target.buffer]), 'test.webm');
+ download(new Blob([target.buffer]), 'test.mp4');
\ No newline at end of file
diff --git a/dist/metamuxer.js b/dist/metamuxer.js
index 1c45f0e..501c0cf 100644
--- a/dist/metamuxer.js
+++ b/dist/metamuxer.js
@@ -144,8 +144,168 @@ var Metamuxer = (() => {
return new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
}
};
+ var textEncoder = new TextEncoder();
+
+ // 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;
+ var preambleStartRegex = /^WEBVTT(.|\n)*?\n{2}/;
+ var inlineTimestampRegex = /<(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})>/g;
+ var SubtitleParser = class {
+ #options;
+ #preambleText = null;
+ #preambleEmitted = false;
+ constructor(options) {
+ this.#options = options;
+ }
+ parse(text) {
+ text = text.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
+ cueBlockHeaderRegex.lastIndex = 0;
+ let match;
+ if (!this.#preambleText) {
+ if (!preambleStartRegex.test(text)) {
+ let error = new Error("WebVTT preamble incorrect.");
+ this.#options.error(error);
+ throw error;
+ }
+ match = cueBlockHeaderRegex.exec(text);
+ let preamble = text.slice(0, match?.index ?? text.length).trimEnd();
+ if (!preamble) {
+ let error = new Error("No WebVTT preamble provided.");
+ this.#options.error(error);
+ throw error;
+ }
+ this.#preambleText = preamble;
+ if (match) {
+ text = text.slice(match.index);
+ cueBlockHeaderRegex.lastIndex = 0;
+ }
+ }
+ while (match = cueBlockHeaderRegex.exec(text)) {
+ let notes = text.slice(0, match.index);
+ let cueIdentifier = match[1];
+ let matchEnd = match.index + match[0].length;
+ let bodyStart = text.indexOf("\n", matchEnd) + 1;
+ let cueSettings = text.slice(matchEnd, bodyStart).trim();
+ let bodyEnd = text.indexOf("\n\n", matchEnd);
+ if (bodyEnd === -1) bodyEnd = text.length;
+ let startTime = parseSubtitleTimestamp(match[2]);
+ let endTime = parseSubtitleTimestamp(match[3]);
+ let duration = endTime - startTime;
+ let body = text.slice(bodyStart, bodyEnd).trim();
+ text = text.slice(bodyEnd).trimStart();
+ cueBlockHeaderRegex.lastIndex = 0;
+ let cue = {
+ timestamp: startTime / 1e3,
+ duration: duration / 1e3,
+ text: body,
+ identifier: cueIdentifier,
+ settings: cueSettings,
+ notes
+ };
+ let meta = {};
+ if (!this.#preambleEmitted) {
+ meta.config = {
+ description: this.#preambleText
+ };
+ this.#preambleEmitted = true;
+ }
+ this.#options.output(cue, meta);
+ }
+ }
+ };
+ var timestampRegex = /(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})/;
+ var parseSubtitleTimestamp = (string) => {
+ let match = timestampRegex.exec(string);
+ if (!match) throw new Error("Expected match.");
+ return 60 * 60 * 1e3 * Number(match[1] || "0") + 60 * 1e3 * Number(match[2]) + 1e3 * Number(match[3]) + Number(match[4]);
+ };
+ var formatSubtitleTimestamp = (timestamp) => {
+ let hours = Math.floor(timestamp / (60 * 60 * 1e3));
+ let minutes = Math.floor(timestamp % (60 * 60 * 1e3) / (60 * 1e3));
+ let seconds = Math.floor(timestamp % (60 * 1e3) / 1e3);
+ let milliseconds = timestamp % 1e3;
+ return hours.toString().padStart(2, "0") + ":" + minutes.toString().padStart(2, "0") + ":" + seconds.toString().padStart(2, "0") + "." + milliseconds.toString().padStart(3, "0");
+ };
// src/isobmff/isobmff_boxes.ts
+ var IsobmffBoxWriter = class {
+ constructor(writer) {
+ this.writer = writer;
+ this.helper = new Uint8Array(8);
+ this.helperView = new DataView(this.helper.buffer);
+ /**
+ * Stores the position from the start of the file to where boxes elements have been written. This is used to
+ * rewrite/edit elements that were already added before, and to measure sizes of things.
+ */
+ this.offsets = /* @__PURE__ */ new WeakMap();
+ }
+ writeU32(value) {
+ this.helperView.setUint32(0, value, false);
+ this.writer.write(this.helper.subarray(0, 4));
+ }
+ writeU64(value) {
+ this.helperView.setUint32(0, Math.floor(value / 2 ** 32), false);
+ this.helperView.setUint32(4, value, false);
+ this.writer.write(this.helper.subarray(0, 8));
+ }
+ writeAscii(text) {
+ for (let i = 0; i < text.length; i++) {
+ this.helperView.setUint8(i % 8, text.charCodeAt(i));
+ if (i % 8 === 7) this.writer.write(this.helper);
+ }
+ if (text.length % 8 !== 0) {
+ this.writer.write(this.helper.subarray(0, text.length % 8));
+ }
+ }
+ writeBox(box2) {
+ this.offsets.set(box2, this.writer.getPos());
+ if (box2.contents && !box2.children) {
+ this.writeBoxHeader(box2, box2.size ?? box2.contents.byteLength + 8);
+ this.writer.write(box2.contents);
+ } else {
+ let startPos = this.writer.getPos();
+ this.writeBoxHeader(box2, 0);
+ if (box2.contents) this.writer.write(box2.contents);
+ if (box2.children) {
+ for (let child of box2.children) if (child) this.writeBox(child);
+ }
+ let endPos = this.writer.getPos();
+ let size = box2.size ?? endPos - startPos;
+ this.writer.seek(startPos);
+ this.writeBoxHeader(box2, size);
+ this.writer.seek(endPos);
+ }
+ }
+ writeBoxHeader(box2, size) {
+ this.writeU32(box2.largeSize ? 1 : size);
+ this.writeAscii(box2.type);
+ if (box2.largeSize) this.writeU64(size);
+ }
+ measureBoxHeader(box2) {
+ return 8 + (box2.largeSize ? 8 : 0);
+ }
+ patchBox(box2) {
+ const boxOffset = this.offsets.get(box2);
+ assert(boxOffset !== void 0);
+ let endPos = this.writer.getPos();
+ this.writer.seek(boxOffset);
+ this.writeBox(box2);
+ this.writer.seek(endPos);
+ }
+ measureBox(box2) {
+ if (box2.contents && !box2.children) {
+ let headerSize = this.measureBoxHeader(box2);
+ return headerSize + box2.contents.byteLength;
+ } else {
+ let result = this.measureBoxHeader(box2);
+ if (box2.contents) result += box2.contents.byteLength;
+ if (box2.children) {
+ for (let child of box2.children) if (child) result += this.measureBox(child);
+ }
+ return result;
+ }
+ }
+ };
var bytes = new Uint8Array(8);
var view = new DataView(bytes.buffer);
var u8 = (value) => {
@@ -340,7 +500,7 @@ var Metamuxer = (() => {
// Reserved
u16(0),
// Layer
- u16(0),
+ u16(trackData.track.id),
// Alternate group
fixed_8_8(trackData.type === "audio" ? 1 : 0),
// Volume
@@ -356,7 +516,7 @@ var Metamuxer = (() => {
};
var mdia = (trackData, creationTime) => box("mdia", void 0, [
mdhd(trackData, creationTime),
- hdlr(trackData.type === "video" ? "vide" : "soun"),
+ hdlr(trackData),
minf(trackData)
]);
var mdhd = (trackData, creationTime) => {
@@ -382,10 +542,20 @@ var Metamuxer = (() => {
// Quality
]);
};
- var hdlr = (componentSubtype) => fullBox("hdlr", 0, 0, [
+ var TRACK_TYPE_TO_COMPONENT_SUBTYPE = {
+ video: "vide",
+ audio: "soun",
+ subtitle: "text"
+ };
+ var TRACK_TYPE_TO_HANDLER_NAME = {
+ video: "VideoHandler",
+ audio: "SoundHandler",
+ subtitle: "TextHandler"
+ };
+ var hdlr = (trackData) => fullBox("hdlr", 0, 0, [
ascii("mhlr"),
// Component type
- ascii(componentSubtype),
+ ascii(TRACK_TYPE_TO_COMPONENT_SUBTYPE[trackData.type]),
// Component subtype
u32(0),
// Component manufacturer
@@ -393,12 +563,11 @@ var Metamuxer = (() => {
// Component flags
u32(0),
// Component flags mask
- // TODO:
- ascii("mp4-muxer-hdlr", true)
+ ascii(TRACK_TYPE_TO_HANDLER_NAME[trackData.type], true)
// Component name
]);
var minf = (trackData) => box("minf", void 0, [
- trackData.type === "video" ? vmhd() : smhd(),
+ TRACK_TYPE_TO_HEADER_BOX[trackData.type](),
dinf(),
stbl(trackData)
]);
@@ -418,6 +587,12 @@ var Metamuxer = (() => {
u16(0)
// Reserved
]);
+ var nmhd = () => fullBox("nmhd", 0, 0);
+ var TRACK_TYPE_TO_HEADER_BOX = {
+ video: vmhd,
+ audio: smhd,
+ subtitle: nmhd
+ };
var dinf = () => box("dinf", void 0, [
dref()
]);
@@ -440,18 +615,32 @@ var Metamuxer = (() => {
needsCtts ? ctts(trackData) : null
]);
};
- var stsd = (trackData) => fullBox("stsd", 0, 0, [
- u32(1)
- // Entry count
- ], [
- trackData.type === "video" ? videoSampleDescription(
- VIDEO_CODEC_TO_BOX_NAME[trackData.track.source.codec],
- trackData
- ) : soundSampleDescription(
- AUDIO_CODEC_TO_BOX_NAME[trackData.track.source.codec],
- trackData
- )
- ]);
+ var stsd = (trackData) => {
+ let sampleDescription;
+ if (trackData.type === "video") {
+ sampleDescription = videoSampleDescription(
+ VIDEO_CODEC_TO_BOX_NAME[trackData.track.source.codec],
+ trackData
+ );
+ } else if (trackData.type === "audio") {
+ sampleDescription = soundSampleDescription(
+ AUDIO_CODEC_TO_BOX_NAME[trackData.track.source.codec],
+ trackData
+ );
+ } else if (trackData.type === "subtitle") {
+ sampleDescription = subtitleSampleDescription(
+ SUBTITLE_CODEC_TO_BOX_NAME[trackData.track.source.codec],
+ trackData
+ );
+ }
+ assert(sampleDescription);
+ return fullBox("stsd", 0, 0, [
+ u32(1)
+ // Entry count
+ ], [
+ sampleDescription
+ ]);
+ };
var videoSampleDescription = (compressionType, trackData) => box(compressionType, [
Array(6).fill(0),
// Reserved
@@ -483,6 +672,7 @@ var Metamuxer = (() => {
// Pre-defined
], [
VIDEO_CODEC_TO_CONFIGURATION_BOX[trackData.track.source.codec](trackData)
+ // TODO colr
]);
var avcC = (trackData) => trackData.info.decoderConfig && box("avcC", [
// For AVC, description is an AVCDecoderConfigurationRecord, so nothing else to do here
@@ -626,6 +816,17 @@ var Metamuxer = (() => {
// ChannelMappingFamily
]);
};
+ var subtitleSampleDescription = (compressionType, trackData) => box(compressionType, [
+ Array(6).fill(0),
+ // Reserved
+ u16(1)
+ // Data reference index
+ ], [
+ SUBTITLE_CODEC_TO_CONFIGURATION_BOX[trackData.track.source.codec](trackData)
+ ]);
+ var vttC = (trackData) => box("vttC", [
+ ...textEncoder.encode(trackData.info.config.description)
+ ]);
var stts = (trackData) => {
return fullBox("stts", 0, 0, [
u32(trackData.timeToSampleTable.length),
@@ -859,6 +1060,15 @@ var Metamuxer = (() => {
// Size
]);
};
+ var vtte = () => box("vtte");
+ var vttc = (payload, timestamp, identifier, settings, sourceId) => box("vttc", void 0, [
+ sourceId !== null ? box("vsid", [i32(sourceId)]) : null,
+ identifier !== null ? box("iden", [...textEncoder.encode(identifier)]) : null,
+ timestamp !== null ? box("ctim", [...textEncoder.encode(formatSubtitleTimestamp(timestamp))]) : null,
+ settings !== null ? box("sttg", [...textEncoder.encode(settings)]) : null,
+ box("payl", [...textEncoder.encode(payload)])
+ ]);
+ var vtta = (notes) => box("vtta", [...textEncoder.encode(notes)]);
var VIDEO_CODEC_TO_BOX_NAME = {
"avc": "avc1",
"hevc": "hvc1",
@@ -881,16 +1091,342 @@ var Metamuxer = (() => {
"aac": esds,
"opus": dOps
};
+ var SUBTITLE_CODEC_TO_BOX_NAME = {
+ "webvtt": "wvtt"
+ };
+ var SUBTITLE_CODEC_TO_CONFIGURATION_BOX = {
+ "webvtt": vttC
+ };
// src/muxer.ts
var Muxer = class {
constructor(output) {
+ this.trackTimestampInfo = /* @__PURE__ */ new WeakMap();
this.output = output;
}
beforeTrackAdd(track) {
}
onTrackClose(track) {
}
+ validateAndNormalizeTimestamp(track, rawTimestampInUs, isKeyFrame) {
+ let timestampInSeconds = rawTimestampInUs / 1e6;
+ let timestampInfo = this.trackTimestampInfo.get(track);
+ if (!timestampInfo) {
+ if (!isKeyFrame) {
+ throw new Error("First frame must be a key frame.");
+ }
+ if (this.timestampsMustStartAtZero && timestampInSeconds > 0) {
+ throw new Error(`Timestamps must start at zero (got ${timestampInSeconds}s).`);
+ }
+ timestampInfo = {
+ timestampOffset: timestampInSeconds,
+ maxTimestamp: track.source.offsetTimestamps ? 0 : timestampInSeconds,
+ lastKeyFrameTimestamp: track.source.offsetTimestamps ? 0 : timestampInSeconds
+ };
+ this.trackTimestampInfo.set(track, timestampInfo);
+ }
+ if (track.source.offsetTimestamps) {
+ timestampInSeconds -= timestampInfo.timestampOffset;
+ }
+ if (timestampInSeconds < 0) {
+ throw new Error(`Timestamps must be non-negative (got ${timestampInSeconds}s).`);
+ }
+ if (timestampInSeconds < timestampInfo.lastKeyFrameTimestamp) {
+ throw new Error(`Timestamp cannot be smaller than last key frame's timestamp (got ${timestampInSeconds}s, last key frame at ${timestampInfo.lastKeyFrameTimestamp}s).`);
+ }
+ if (isKeyFrame) {
+ if (timestampInSeconds < timestampInfo.maxTimestamp) {
+ throw new Error(`Key frame timestamps cannot be smaller than any timestamp that came before (got ${timestampInSeconds}s, max timestamp was ${timestampInfo.maxTimestamp}s).`);
+ }
+ timestampInfo.lastKeyFrameTimestamp = timestampInSeconds;
+ }
+ timestampInfo.maxTimestamp = Math.max(timestampInfo.maxTimestamp, timestampInSeconds);
+ return timestampInSeconds;
+ }
+ };
+
+ // src/writer.ts
+ var Writer = class {
+ };
+ var ArrayBufferTargetWriter = class extends Writer {
+ #pos = 0;
+ #target;
+ #buffer = new ArrayBuffer(2 ** 16);
+ #bytes = new Uint8Array(this.#buffer);
+ #maxPos = 0;
+ constructor(target) {
+ super();
+ this.#target = target;
+ }
+ #ensureSize(size) {
+ let newLength = this.#buffer.byteLength;
+ while (newLength < size) newLength *= 2;
+ if (newLength === this.#buffer.byteLength) return;
+ let newBuffer = new ArrayBuffer(newLength);
+ let newBytes = new Uint8Array(newBuffer);
+ newBytes.set(this.#bytes, 0);
+ this.#buffer = newBuffer;
+ this.#bytes = newBytes;
+ }
+ write(data) {
+ this.#ensureSize(this.#pos + data.byteLength);
+ this.#bytes.set(data, this.#pos);
+ this.#pos += data.byteLength;
+ this.#maxPos = Math.max(this.#maxPos, this.#pos);
+ }
+ seek(newPos) {
+ this.#pos = newPos;
+ }
+ getPos() {
+ return this.#pos;
+ }
+ flush() {
+ }
+ finalize() {
+ this.#ensureSize(this.#pos);
+ this.#target.buffer = this.#buffer.slice(0, Math.max(this.#maxPos, this.#pos));
+ }
+ getSlice(start, end) {
+ return this.#bytes.slice(start, end);
+ }
+ };
+ var StreamTargetWriter = class extends Writer {
+ #pos = 0;
+ #target;
+ #sections = [];
+ constructor(target) {
+ super();
+ this.#target = target;
+ }
+ write(data) {
+ this.#sections.push({
+ data: data.slice(),
+ start: this.#pos
+ });
+ this.#pos += data.byteLength;
+ }
+ seek(newPos) {
+ this.#pos = newPos;
+ }
+ getPos() {
+ return this.#pos;
+ }
+ flush() {
+ if (this.#sections.length === 0) return;
+ let chunks = [];
+ let sorted = [...this.#sections].sort((a, b) => a.start - b.start);
+ chunks.push({
+ start: sorted[0].start,
+ size: sorted[0].data.byteLength
+ });
+ for (let i = 1; i < sorted.length; i++) {
+ let lastChunk = chunks[chunks.length - 1];
+ let section = sorted[i];
+ if (section.start <= lastChunk.start + lastChunk.size) {
+ lastChunk.size = Math.max(lastChunk.size, section.start + section.data.byteLength - lastChunk.start);
+ } else {
+ chunks.push({
+ start: section.start,
+ size: section.data.byteLength
+ });
+ }
+ }
+ for (let chunk of chunks) {
+ chunk.data = new Uint8Array(chunk.size);
+ for (let section of this.#sections) {
+ if (chunk.start <= section.start && section.start < chunk.start + chunk.size) {
+ chunk.data.set(section.data, section.start - chunk.start);
+ }
+ }
+ this.#target.options.onData?.(chunk.data, chunk.start);
+ }
+ this.#sections.length = 0;
+ }
+ finalize() {
+ }
+ };
+ var DEFAULT_CHUNK_SIZE = 2 ** 24;
+ var MAX_CHUNKS_AT_ONCE = 2;
+ var ChunkedStreamTargetWriter = class extends Writer {
+ #pos = 0;
+ #target;
+ #chunkSize;
+ /**
+ * The data is divided up into fixed-size chunks, whose contents are first filled in RAM and then flushed out.
+ * A chunk is flushed if all of its contents have been written.
+ */
+ #chunks = [];
+ constructor(target) {
+ super();
+ this.#target = target;
+ this.#chunkSize = target.options?.chunkSize ?? DEFAULT_CHUNK_SIZE;
+ if (!Number.isInteger(this.#chunkSize) || this.#chunkSize < 2 ** 10) {
+ throw new Error("Invalid StreamTarget options: chunkSize must be an integer not smaller than 1024.");
+ }
+ }
+ write(data) {
+ this.#writeDataIntoChunks(data, this.#pos);
+ this.#flushChunks();
+ this.#pos += data.byteLength;
+ }
+ seek(newPos) {
+ this.#pos = newPos;
+ }
+ getPos() {
+ return this.#pos;
+ }
+ #writeDataIntoChunks(data, position) {
+ let chunkIndex = this.#chunks.findIndex((x) => x.start <= position && position < x.start + this.#chunkSize);
+ if (chunkIndex === -1) chunkIndex = this.#createChunk(position);
+ let chunk = this.#chunks[chunkIndex];
+ let relativePosition = position - chunk.start;
+ let toWrite = data.subarray(0, Math.min(this.#chunkSize - relativePosition, data.byteLength));
+ chunk.data.set(toWrite, relativePosition);
+ let section = {
+ start: relativePosition,
+ end: relativePosition + toWrite.byteLength
+ };
+ this.#insertSectionIntoChunk(chunk, section);
+ if (chunk.written[0].start === 0 && chunk.written[0].end === this.#chunkSize) {
+ chunk.shouldFlush = true;
+ }
+ if (this.#chunks.length > MAX_CHUNKS_AT_ONCE) {
+ for (let i = 0; i < this.#chunks.length - 1; i++) {
+ this.#chunks[i].shouldFlush = true;
+ }
+ this.#flushChunks();
+ }
+ if (toWrite.byteLength < data.byteLength) {
+ this.#writeDataIntoChunks(data.subarray(toWrite.byteLength), position + toWrite.byteLength);
+ }
+ }
+ #insertSectionIntoChunk(chunk, section) {
+ let low = 0;
+ let high = chunk.written.length - 1;
+ let index = -1;
+ while (low <= high) {
+ let mid = Math.floor(low + (high - low + 1) / 2);
+ if (chunk.written[mid].start <= section.start) {
+ low = mid + 1;
+ index = mid;
+ } else {
+ high = mid - 1;
+ }
+ }
+ chunk.written.splice(index + 1, 0, section);
+ if (index === -1 || chunk.written[index].end < section.start) index++;
+ while (index < chunk.written.length - 1 && chunk.written[index].end >= chunk.written[index + 1].start) {
+ chunk.written[index].end = Math.max(chunk.written[index].end, chunk.written[index + 1].end);
+ chunk.written.splice(index + 1, 1);
+ }
+ }
+ #createChunk(includesPosition) {
+ let start = Math.floor(includesPosition / this.#chunkSize) * this.#chunkSize;
+ let chunk = {
+ start,
+ data: new Uint8Array(this.#chunkSize),
+ written: [],
+ shouldFlush: false
+ };
+ this.#chunks.push(chunk);
+ this.#chunks.sort((a, b) => a.start - b.start);
+ return this.#chunks.indexOf(chunk);
+ }
+ #flushChunks(force = false) {
+ for (let i = 0; i < this.#chunks.length; i++) {
+ let chunk = this.#chunks[i];
+ if (!chunk.shouldFlush && !force) continue;
+ for (let section of chunk.written) {
+ this.#target.options.onData?.(
+ chunk.data.subarray(section.start, section.end),
+ chunk.start + section.start
+ );
+ }
+ this.#chunks.splice(i--, 1);
+ }
+ }
+ flush() {
+ }
+ finalize() {
+ this.#flushChunks(true);
+ }
+ };
+ var FileSystemWritableFileStreamTargetWriter = class extends ChunkedStreamTargetWriter {
+ constructor(target) {
+ super(new StreamTarget({
+ onData: (data, position) => target.stream.write({
+ type: "write",
+ data,
+ position
+ }),
+ chunkSize: target.options?.chunkSize
+ }));
+ }
+ };
+
+ // src/target.ts
+ var Target = class {
+ constructor() {
+ this.output = null;
+ }
+ };
+ var ArrayBufferTarget2 = 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 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");
+ }
+ }
+ }
+ createWriter() {
+ return new FileSystemWritableFileStreamTargetWriter(this);
+ }
};
// src/isobmff/isobmff_muxer.ts
@@ -903,13 +1439,10 @@ var Metamuxer = (() => {
var IsobmffMuxer = class extends Muxer {
constructor(output, format) {
super(output);
- this.#helper = new Uint8Array(8);
- this.#helperView = new DataView(this.#helper.buffer);
- /**
- * Stores the position from the start of the file to where boxes elements have been written. This is used to
- * rewrite/edit elements that were already added before, and to measure sizes of things.
- */
- this.offsets = /* @__PURE__ */ new WeakMap();
+ this.timestampsMustStartAtZero = true;
+ this.#auxTarget = new ArrayBufferTarget2();
+ this.#auxWriter = this.#auxTarget.createWriter();
+ this.#auxBoxWriter = new IsobmffBoxWriter(this.#auxWriter);
this.#ftypSize = null;
this.#mdat = null;
this.#trackDatas = [];
@@ -917,87 +1450,24 @@ var Metamuxer = (() => {
this.#finalizedChunks = [];
this.#nextFragmentNumber = 1;
this.#writer = output.writer;
+ this.#boxWriter = new IsobmffBoxWriter(this.#writer);
this.#format = format;
}
#writer;
+ #boxWriter;
#format;
- #helper;
- #helperView;
+ #auxTarget;
+ #auxWriter;
+ #auxBoxWriter;
#ftypSize;
#mdat;
#trackDatas;
#creationTime;
#finalizedChunks;
#nextFragmentNumber;
- writeU32(value) {
- this.#helperView.setUint32(0, value, false);
- this.#writer.write(this.#helper.subarray(0, 4));
- }
- writeU64(value) {
- this.#helperView.setUint32(0, Math.floor(value / 2 ** 32), false);
- this.#helperView.setUint32(4, value, false);
- this.#writer.write(this.#helper.subarray(0, 8));
- }
- writeAscii(text) {
- for (let i = 0; i < text.length; i++) {
- this.#helperView.setUint8(i % 8, text.charCodeAt(i));
- if (i % 8 === 7) this.#writer.write(this.#helper);
- }
- if (text.length % 8 !== 0) {
- this.#writer.write(this.#helper.subarray(0, text.length % 8));
- }
- }
- writeBox(box2) {
- this.offsets.set(box2, this.#writer.getPos());
- if (box2.contents && !box2.children) {
- this.writeBoxHeader(box2, box2.size ?? box2.contents.byteLength + 8);
- this.#writer.write(box2.contents);
- } else {
- let startPos = this.#writer.getPos();
- this.writeBoxHeader(box2, 0);
- if (box2.contents) this.#writer.write(box2.contents);
- if (box2.children) {
- for (let child of box2.children) if (child) this.writeBox(child);
- }
- let endPos = this.#writer.getPos();
- let size = box2.size ?? endPos - startPos;
- this.#writer.seek(startPos);
- this.writeBoxHeader(box2, size);
- this.#writer.seek(endPos);
- }
- }
- writeBoxHeader(box2, size) {
- this.writeU32(box2.largeSize ? 1 : size);
- this.writeAscii(box2.type);
- if (box2.largeSize) this.writeU64(size);
- }
- measureBoxHeader(box2) {
- return 8 + (box2.largeSize ? 8 : 0);
- }
- patchBox(box2) {
- const boxOffset = this.offsets.get(box2);
- assert(boxOffset !== void 0);
- let endPos = this.#writer.getPos();
- this.#writer.seek(boxOffset);
- this.writeBox(box2);
- this.#writer.seek(endPos);
- }
- measureBox(box2) {
- if (box2.contents && !box2.children) {
- let headerSize = this.measureBoxHeader(box2);
- return headerSize + box2.contents.byteLength;
- } else {
- let result = this.measureBoxHeader(box2);
- if (box2.contents) result += box2.contents.byteLength;
- if (box2.children) {
- for (let child of box2.children) if (child) result += this.measureBox(child);
- }
- return result;
- }
- }
start() {
const holdsAvc = this.output.tracks.some((x) => x.type === "video" && x.source.codec === "avc");
- this.writeBox(ftyp({
+ this.#boxWriter.writeBox(ftyp({
holdsAvc,
fragmented: this.#format.options.fastStart === "fragmented"
}));
@@ -1011,7 +1481,7 @@ var Metamuxer = (() => {
this.#writer.seek(this.#writer.getPos() + moovSizeUpperBound);
}
this.#mdat = mdat(true);
- this.writeBox(this.#mdat);
+ this.#boxWriter.writeBox(this.#mdat);
}
this.#writer.flush();
}
@@ -1054,8 +1524,6 @@ var Metamuxer = (() => {
samples: [],
sampleQueue: [],
timestampProcessingQueue: [],
- firstTimestamp: null,
- lastKeyFrameTimestamp: null,
timeToSampleTable: [],
compositionTimeOffsetTable: [],
lastTimescaleUnits: null,
@@ -1087,8 +1555,6 @@ var Metamuxer = (() => {
samples: [],
sampleQueue: [],
timestampProcessingQueue: [],
- firstTimestamp: null,
- lastKeyFrameTimestamp: null,
timeToSampleTable: [],
compositionTimeOffsetTable: [],
lastTimescaleUnits: null,
@@ -1101,17 +1567,55 @@ var Metamuxer = (() => {
this.#trackDatas.sort((a, b) => a.track.id - b.track.id);
return newTrackData;
}
+ #getSubtitleTrackData(track, meta) {
+ const existingTrackData = this.#trackDatas.find((x) => x.track === track);
+ if (existingTrackData) {
+ return existingTrackData;
+ }
+ assert(meta);
+ assert(meta.config);
+ const newTrackData = {
+ track,
+ type: "subtitle",
+ info: {
+ config: meta.config
+ },
+ timescale: 1e3,
+ // Reasonable
+ samples: [],
+ sampleQueue: [],
+ timestampProcessingQueue: [],
+ timeToSampleTable: [],
+ compositionTimeOffsetTable: [],
+ lastTimescaleUnits: null,
+ lastSample: null,
+ finalizedChunks: [],
+ currentChunk: null,
+ compactlyCodedChunkTable: [],
+ lastCueEndTimestamp: 0,
+ cueQueue: [],
+ nextSourceId: 0,
+ cueToSourceId: /* @__PURE__ */ new WeakMap()
+ };
+ this.#trackDatas.push(newTrackData);
+ this.#trackDatas.sort((a, b) => a.track.id - b.track.id);
+ this.validateAndNormalizeTimestamp(track, 0, true);
+ return newTrackData;
+ }
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 videoSample = this.#createSampleForTrack(trackData, chunk);
+ 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(videoSample);
+ trackData.sampleQueue.push(sample);
this.#interleaveSamples();
} else {
- this.#addSampleToTrack(trackData, videoSample);
+ this.#addSampleToTrack(trackData, sample);
}
}
addEncodedAudioChunk(track, chunk, meta) {
@@ -1119,29 +1623,101 @@ var Metamuxer = (() => {
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 audioSample = this.#createSampleForTrack(trackData, chunk);
- if (this.#format.options.fastStart === "fragmented") {
- trackData.sampleQueue.push(audioSample);
- this.#interleaveSamples();
- } else {
- this.#addSampleToTrack(trackData, audioSample);
- }
- }
- #createSampleForTrack(trackData, chunk) {
- let timestampInSeconds = this.#validateTimestamp(trackData, chunk);
- let durationInSeconds = (chunk.duration ?? 0) / 1e6;
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);
+ }
+ }
+ addSubtitleCue(track, cue, meta) {
+ const trackData = this.#getSubtitleTrackData(track, meta);
+ this.validateAndNormalizeTimestamp(trackData.track, 1e6 * cue.timestamp, true);
+ if (track.source.codec === "webvtt") {
+ trackData.cueQueue.push(cue);
+ this.#processWebVTTCues(trackData, cue.timestamp);
+ } else {
+ }
+ }
+ #processWebVTTCues(trackData, until) {
+ while (trackData.cueQueue.length > 0) {
+ let timestamps = /* @__PURE__ */ new Set([]);
+ for (let cue of trackData.cueQueue) {
+ assert(cue.timestamp <= until);
+ assert(trackData.lastCueEndTimestamp <= cue.timestamp + cue.duration);
+ timestamps.add(Math.max(cue.timestamp, trackData.lastCueEndTimestamp));
+ timestamps.add(cue.timestamp + cue.duration);
+ }
+ let sortedTimestamps = [...timestamps].sort((a, b) => a - b);
+ let sampleStart = sortedTimestamps[0];
+ let sampleEnd = sortedTimestamps[1] ?? sampleStart;
+ if (until < sampleEnd) {
+ break;
+ }
+ if (trackData.lastCueEndTimestamp < sampleStart) {
+ this.#auxWriter.seek(0);
+ let box2 = vtte();
+ 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);
+ }
+ trackData.lastCueEndTimestamp = sampleStart;
+ }
+ this.#auxWriter.seek(0);
+ for (let i = 0; i < trackData.cueQueue.length; i++) {
+ let cue = trackData.cueQueue[i];
+ if (cue.timestamp >= sampleEnd) {
+ break;
+ }
+ inlineTimestampRegex.lastIndex = 0;
+ let containsTimestamp = inlineTimestampRegex.test(cue.text);
+ let endTimestamp = cue.timestamp + cue.duration;
+ let sourceId = trackData.cueToSourceId.get(cue);
+ if (sourceId === void 0 && sampleEnd < endTimestamp) {
+ sourceId = trackData.nextSourceId++;
+ trackData.cueToSourceId.set(cue, sourceId);
+ }
+ if (cue.notes) {
+ let box3 = vtta(cue.notes);
+ this.#auxBoxWriter.writeBox(box3);
+ }
+ let box2 = vttc(cue.text, containsTimestamp ? sampleStart : null, cue.identifier ?? null, cue.settings ?? null, sourceId ?? null);
+ this.#auxBoxWriter.writeBox(box2);
+ if (endTimestamp === sampleEnd) {
+ trackData.cueQueue.splice(i--, 1);
+ }
+ }
+ 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);
+ }
+ trackData.lastCueEndTimestamp = sampleEnd;
+ }
+ }
+ #createSampleForTrack(trackData, data, timestamp, duration, type) {
let sample = {
- timestamp: timestampInSeconds,
- decodeTimestamp: timestampInSeconds,
- // We may refine this later
- duration: durationInSeconds,
+ timestamp,
+ decodeTimestamp: timestamp,
+ // This may be refined later
+ duration,
data,
size: data.byteLength,
- type: chunk.type,
+ type,
// Will be refined once the next sample comes in
- timescaleUnitsToNextSample: intoTimescale(durationInSeconds, trackData.timescale)
+ timescaleUnitsToNextSample: intoTimescale(duration, trackData.timescale)
};
return sample;
}
@@ -1154,6 +1730,7 @@ var Metamuxer = (() => {
const sample = trackData.timestampProcessingQueue[i];
sample.decodeTimestamp = sortedTimestamps[i];
const sampleCompositionTimeOffset = intoTimescale(sample.timestamp - sample.decodeTimestamp, trackData.timescale);
+ const durationInTimescale = intoTimescale(sample.duration, trackData.timescale);
if (trackData.lastTimescaleUnits !== null) {
assert(trackData.lastSample);
let timescaleUnits = intoTimescale(sample.decodeTimestamp, trackData.timescale, false);
@@ -1165,14 +1742,25 @@ var Metamuxer = (() => {
assert(lastTableEntry);
if (lastTableEntry.sampleCount === 1) {
lastTableEntry.sampleDelta = delta;
- lastTableEntry.sampleCount++;
- } else if (lastTableEntry.sampleDelta === delta) {
+ let entryBefore = trackData.timeToSampleTable[trackData.timeToSampleTable.length - 2];
+ if (entryBefore && entryBefore.sampleDelta === delta) {
+ entryBefore.sampleCount++;
+ trackData.timeToSampleTable.pop();
+ lastTableEntry = entryBefore;
+ }
+ } else if (lastTableEntry.sampleDelta !== delta) {
+ lastTableEntry.sampleCount--;
+ trackData.timeToSampleTable.push(lastTableEntry = {
+ sampleCount: 1,
+ sampleDelta: delta
+ });
+ }
+ if (lastTableEntry.sampleDelta === durationInTimescale) {
lastTableEntry.sampleCount++;
} else {
- lastTableEntry.sampleCount--;
trackData.timeToSampleTable.push({
- sampleCount: 2,
- sampleDelta: delta
+ sampleCount: 1,
+ sampleDelta: durationInTimescale
});
}
const lastCompositionTimeOffsetTableEntry = last(trackData.compositionTimeOffsetTable);
@@ -1191,7 +1779,7 @@ var Metamuxer = (() => {
if (this.#format.options.fastStart !== "fragmented") {
trackData.timeToSampleTable.push({
sampleCount: 1,
- sampleDelta: intoTimescale(sample.duration, trackData.timescale)
+ sampleDelta: durationInTimescale
});
trackData.compositionTimeOffsetTable.push({
sampleCount: 1,
@@ -1246,23 +1834,6 @@ var Metamuxer = (() => {
trackData.currentChunk.samples.push(sample);
trackData.timestampProcessingQueue.push(sample);
}
- #validateTimestamp(trackData, chunk) {
- let timestampInSeconds = chunk.timestamp / 1e6;
- if (timestampInSeconds < 0) {
- throw new Error(`Timestamps must be non-negative (got ${timestampInSeconds}s).`);
- }
- if (trackData.firstTimestamp === null) {
- trackData.firstTimestamp = timestampInSeconds;
- }
- timestampInSeconds -= trackData.firstTimestamp;
- if (trackData.lastKeyFrameTimestamp !== null && timestampInSeconds < trackData.lastKeyFrameTimestamp) {
- throw new Error(`Timestamp cannot be before last key frame's timestamp (got ${timestampInSeconds}s, last key frame at ${trackData.lastKeyFrameTimestamp}s).`);
- }
- if (chunk.type === "key") {
- trackData.lastKeyFrameTimestamp = timestampInSeconds;
- }
- return timestampInSeconds;
- }
#finalizeCurrentChunk(trackData) {
assert(this.#format.options.fastStart !== "fragmented");
if (!trackData.currentChunk) return;
@@ -1289,18 +1860,20 @@ var Metamuxer = (() => {
}
#interleaveSamples() {
assert(this.#format.options.fastStart === "fragmented");
- if (this.#trackDatas.length < this.output.tracks.length) {
- return;
+ for (const track of this.output.tracks) {
+ if (!track.source.closed && !this.#trackDatas.some((x) => x.track === track)) {
+ return;
+ }
}
outer:
while (true) {
let trackWithMinTimestamp = null;
let minTimestamp = Infinity;
for (let trackData of this.#trackDatas) {
- if (trackData.sampleQueue.length === 0) {
+ if (trackData.sampleQueue.length === 0 && !trackData.track.source.closed) {
break outer;
}
- if (trackData.sampleQueue[0].timestamp < minTimestamp) {
+ if (trackData.sampleQueue.length > 0 && trackData.sampleQueue[0].timestamp < minTimestamp) {
trackWithMinTimestamp = trackData;
minTimestamp = trackData.sampleQueue[0].timestamp;
}
@@ -1317,11 +1890,11 @@ var Metamuxer = (() => {
let fragmentNumber = this.#nextFragmentNumber++;
if (fragmentNumber === 1) {
let movieBox = moov(this.#trackDatas, this.#creationTime, true);
- this.writeBox(movieBox);
+ this.#boxWriter.writeBox(movieBox);
}
let moofOffset = this.#writer.getPos();
let moofBox = moof(fragmentNumber, this.#trackDatas);
- this.writeBox(moofBox);
+ this.#boxWriter.writeBox(moofBox);
{
let mdatBox = mdat(false);
let totalTrackSampleSize = 0;
@@ -1331,13 +1904,13 @@ var Metamuxer = (() => {
totalTrackSampleSize += sample.size;
}
}
- let mdatSize = this.measureBox(mdatBox) + totalTrackSampleSize;
+ let mdatSize = this.#boxWriter.measureBox(mdatBox) + totalTrackSampleSize;
if (mdatSize >= 2 ** 32) {
mdatBox.largeSize = true;
- mdatSize = this.measureBox(mdatBox) + totalTrackSampleSize;
+ mdatSize = this.#boxWriter.measureBox(mdatBox) + totalTrackSampleSize;
}
mdatBox.size = mdatSize;
- this.writeBox(mdatBox);
+ this.#boxWriter.writeBox(mdatBox);
}
for (let trackData of this.#trackDatas) {
trackData.currentChunk.offset = this.#writer.getPos();
@@ -1348,9 +1921,9 @@ var Metamuxer = (() => {
}
}
let endPos = this.#writer.getPos();
- this.#writer.seek(this.offsets.get(moofBox));
+ this.#writer.seek(this.#boxWriter.offsets.get(moofBox));
let newMoofBox = moof(fragmentNumber, this.#trackDatas);
- this.writeBox(newMoofBox);
+ this.#boxWriter.writeBox(newMoofBox);
this.#writer.seek(endPos);
for (let trackData of this.#trackDatas) {
trackData.finalizedChunks.push(trackData.currentChunk);
@@ -1361,8 +1934,24 @@ var Metamuxer = (() => {
this.#writer.flush();
}
}
+ onTrackClose(track) {
+ if (track.type === "subtitle" && track.source.codec === "webvtt") {
+ let trackData = this.#trackDatas.find((x) => x.track === track);
+ if (trackData) {
+ this.#processWebVTTCues(trackData, Infinity);
+ }
+ }
+ if (this.#format.options.fastStart === "fragmented") {
+ this.#interleaveSamples();
+ }
+ }
/** Finalizes the file, making it ready for use. Must be called after all video and audio chunks have been added. */
finalize() {
+ for (let trackData of this.#trackDatas) {
+ if (trackData.type === "subtitle" && trackData.track.source.codec === "webvtt") {
+ this.#processWebVTTCues(trackData, Infinity);
+ }
+ }
if (this.#format.options.fastStart === "fragmented") {
for (let trackData of this.#trackDatas) {
for (let sample of trackData.sampleQueue) {
@@ -1382,8 +1971,8 @@ var Metamuxer = (() => {
let mdatSize;
for (let i = 0; i < 2; i++) {
let movieBox2 = moov(this.#trackDatas, this.#creationTime);
- let movieBoxSize = this.measureBox(movieBox2);
- mdatSize = this.measureBox(this.#mdat);
+ let movieBoxSize = this.#boxWriter.measureBox(movieBox2);
+ mdatSize = this.#boxWriter.measureBox(this.#mdat);
let currentChunkPos = this.#writer.getPos() + movieBoxSize + mdatSize;
for (let chunk of this.#finalizedChunks) {
chunk.offset = currentChunkPos;
@@ -1397,9 +1986,9 @@ var Metamuxer = (() => {
if (mdatSize >= 2 ** 32) this.#mdat.largeSize = true;
}
let movieBox = moov(this.#trackDatas, this.#creationTime);
- this.writeBox(movieBox);
+ this.#boxWriter.writeBox(movieBox);
this.#mdat.size = mdatSize;
- this.writeBox(this.#mdat);
+ this.#boxWriter.writeBox(this.#mdat);
for (let chunk of this.#finalizedChunks) {
for (let sample of chunk.samples) {
assert(sample.data);
@@ -1410,27 +1999,27 @@ var Metamuxer = (() => {
} else if (this.#format.options.fastStart === "fragmented") {
let startPos = this.#writer.getPos();
let mfraBox = mfra(this.#trackDatas);
- this.writeBox(mfraBox);
+ this.#boxWriter.writeBox(mfraBox);
let mfraBoxSize = this.#writer.getPos() - startPos;
this.#writer.seek(this.#writer.getPos() - 4);
- this.writeU32(mfraBoxSize);
+ this.#boxWriter.writeU32(mfraBoxSize);
} else {
assert(this.#mdat);
assert(this.#ftypSize !== null);
- let mdatPos = this.offsets.get(this.#mdat);
+ let mdatPos = this.#boxWriter.offsets.get(this.#mdat);
assert(mdatPos !== void 0);
let mdatSize = this.#writer.getPos() - mdatPos;
this.#mdat.size = mdatSize;
this.#mdat.largeSize = mdatSize >= 2 ** 32;
- this.patchBox(this.#mdat);
+ this.#boxWriter.patchBox(this.#mdat);
let movieBox = moov(this.#trackDatas, this.#creationTime);
if (typeof this.#format.options.fastStart === "object") {
this.#writer.seek(this.#ftypSize);
- this.writeBox(movieBox);
+ this.#boxWriter.writeBox(movieBox);
let remainingBytes = mdatPos - this.#writer.getPos();
- this.writeBox(free(remainingBytes));
+ this.#boxWriter.writeBox(free(remainingBytes));
} else {
- this.writeBox(movieBox);
+ this.#boxWriter.writeBox(movieBox);
}
}
}
@@ -1513,7 +2102,6 @@ var Metamuxer = (() => {
av1: "V_AV1",
aac: "A_AAC",
opus: "A_OPUS",
- vorbis: "A_VORBIS",
webvtt: "S_TEXT/WEBVTT"
};
var TRACK_TYPE_MAP = {
@@ -1524,6 +2112,7 @@ var Metamuxer = (() => {
var MatroskaMuxer = class extends Muxer {
constructor(output, format) {
super(output);
+ this.timestampsMustStartAtZero = true;
this.#helper = new Uint8Array(8);
this.#helperView = new DataView(this.#helper.buffer);
/**
@@ -1784,8 +2373,8 @@ var Metamuxer = (() => {
{ id: 131 /* TrackType */, data: TRACK_TYPE_MAP[trackData.type] },
// TODO Subtitle case
{ id: 134 /* CodecID */, data: CODEC_STRING_MAP[trackData.track.source.codec] },
- trackData.info.decoderConfig.description ? { id: 25506 /* CodecPrivate */, data: toUint8Array(trackData.info.decoderConfig.description) } : null,
...trackData.type === "video" ? [
+ trackData.info.decoderConfig.description ? { id: 25506 /* CodecPrivate */, data: toUint8Array(trackData.info.decoderConfig.description) } : null,
trackData.track.metadata.frameRate ? { id: 2352003 /* DefaultDuration */, data: 1e9 / trackData.track.metadata.frameRate } : null,
{ id: 224 /* Video */, data: [
{ id: 176 /* PixelWidth */, data: trackData.info.width },
@@ -1821,11 +2410,15 @@ var Metamuxer = (() => {
] }
] : [],
...trackData.type === "audio" ? [
+ trackData.info.decoderConfig.description ? { id: 25506 /* CodecPrivate */, data: toUint8Array(trackData.info.decoderConfig.description) } : null,
{ id: 225 /* Audio */, data: [
{ id: 181 /* SamplingFrequency */, data: new EBMLFloat32(trackData.info.sampleRate) },
{ id: 159 /* Channels */, data: trackData.info.numberOfChannels }
// Bit depth for when PCM is a thing
] }
+ ] : [],
+ ...trackData.type === "subtitle" ? [
+ { id: 25506 /* CodecPrivate */, data: textEncoder.encode(trackData.info.config.description) }
] : []
] });
}
@@ -1868,8 +2461,6 @@ var Metamuxer = (() => {
decoderConfig: meta.decoderConfig
},
chunkQueue: [],
- firstTimestamp: null,
- lastKeyFrameTimestamp: null,
lastWrittenMsTimestamp: null
};
this.#trackDatas.push(newTrackData);
@@ -1892,8 +2483,6 @@ var Metamuxer = (() => {
decoderConfig: meta.decoderConfig
},
chunkQueue: [],
- firstTimestamp: null,
- lastKeyFrameTimestamp: null,
lastWrittenMsTimestamp: null
};
this.#trackDatas.push(newTrackData);
@@ -1906,16 +2495,14 @@ var Metamuxer = (() => {
return existingTrackData;
}
assert(meta);
- assert(meta.decoderConfig);
+ assert(meta.config);
const newTrackData = {
track,
type: "subtitle",
info: {
- decoderConfig: meta.decoderConfig
+ config: meta.config
},
chunkQueue: [],
- firstTimestamp: null,
- lastKeyFrameTimestamp: null,
lastWrittenMsTimestamp: null
};
this.#trackDatas.push(newTrackData);
@@ -1926,7 +2513,8 @@ var Metamuxer = (() => {
const trackData = this.#getVideoTrackData(track, meta);
let data = new Uint8Array(chunk.byteLength);
chunk.copyTo(data);
- let videoChunk = this.#createInternalChunk(trackData, data, chunk.timestamp, chunk.duration ?? 0, chunk.type);
+ let timestamp = this.validateAndNormalizeTimestamp(trackData.track, chunk.timestamp, chunk.type === "key");
+ let videoChunk = this.#createInternalChunk(data, timestamp, (chunk.duration ?? 0) / 1e6, chunk.type);
if (track.source.codec === "vp9") this.#fixVP9ColorSpace(trackData, videoChunk);
trackData.chunkQueue.push(videoChunk);
this.#interleaveChunks();
@@ -1935,21 +2523,35 @@ var Metamuxer = (() => {
const trackData = this.#getAudioTrackData(track, meta);
let data = new Uint8Array(chunk.byteLength);
chunk.copyTo(data);
- let audioChunk = this.#createInternalChunk(trackData, data, chunk.timestamp, chunk.duration ?? 0, chunk.type);
+ let timestamp = this.validateAndNormalizeTimestamp(trackData.track, chunk.timestamp, chunk.type === "key");
+ let audioChunk = this.#createInternalChunk(data, timestamp, (chunk.duration ?? 0) / 1e6, chunk.type);
trackData.chunkQueue.push(audioChunk);
this.#interleaveChunks();
}
- addEncodedSubtitleChunk(track, chunk, meta) {
+ addSubtitleCue(track, cue, meta) {
const trackData = this.#getSubtitleTrackData(track, meta);
- let subtitleChunk = this.#createInternalChunk(trackData, chunk.body, chunk.timestamp, chunk.duration, "key", chunk.additions);
+ const timestamp = this.validateAndNormalizeTimestamp(trackData.track, 1e6 * cue.timestamp, true);
+ let bodyText = cue.text;
+ const timestampMs = Math.floor(timestamp * 1e3);
+ inlineTimestampRegex.lastIndex = 0;
+ bodyText = bodyText.replace(inlineTimestampRegex, (match) => {
+ let time = parseSubtitleTimestamp(match.slice(1, -1));
+ let offsetTime = time - timestampMs;
+ return `<${formatSubtitleTimestamp(offsetTime)}>`;
+ });
+ const body = textEncoder.encode(bodyText);
+ const additions = `${cue.settings ?? ""}
+${cue.identifier ?? ""}
+${cue.notes ?? ""}`;
+ let subtitleChunk = this.#createInternalChunk(body, timestamp, cue.duration, "key", additions.trim() ? textEncoder.encode(additions) : null);
trackData.chunkQueue.push(subtitleChunk);
this.#interleaveChunks();
}
#interleaveChunks() {
- let openTrackCount = 0;
- for (const trackData of this.#trackDatas) if (!trackData.track.source.closed) openTrackCount++;
- if (this.#trackDatas.length < openTrackCount) {
- return;
+ for (const track of this.output.tracks) {
+ if (!track.source.closed && !this.#trackDatas.some((x) => x.track === track)) {
+ return;
+ }
}
outer:
while (true) {
@@ -2004,34 +2606,16 @@ var Metamuxer = (() => {
writeBits(chunk.data, i + 0, i + 3, colorSpaceID);
}
/** Converts a read-only external chunk into an internal one for easier use. */
- #createInternalChunk(trackData, data, timestamp, duration, type, additions = null) {
- let adjustedTimestamp = this.#validateTimestamp(trackData, timestamp, type === "key");
+ #createInternalChunk(data, timestamp, duration, type, additions = null) {
let internalChunk = {
data,
type,
- timestamp: adjustedTimestamp,
- duration: duration / 1e6,
+ timestamp,
+ duration,
additions
};
return internalChunk;
}
- #validateTimestamp(trackData, timestamp, isKeyFrame) {
- let timestampInSeconds = timestamp / 1e6;
- if (timestampInSeconds < 0) {
- throw new Error(`Timestamps must be non-negative (got ${timestampInSeconds}s).`);
- }
- if (trackData.firstTimestamp === null) {
- trackData.firstTimestamp = timestampInSeconds;
- }
- timestampInSeconds -= trackData.firstTimestamp;
- if (trackData.lastKeyFrameTimestamp !== null && timestampInSeconds < trackData.lastKeyFrameTimestamp) {
- throw new Error(`Timestamp cannot be before last key frame's timestamp (got ${timestampInSeconds}s, last key frame at ${trackData.lastKeyFrameTimestamp}s).`);
- }
- if (isKeyFrame) {
- trackData.lastKeyFrameTimestamp = timestampInSeconds;
- }
- return timestampInSeconds;
- }
/** Writes a block containing media data to the file. */
#writeBlock(trackData, chunk) {
if (!this.#segment) {
@@ -2040,6 +2624,9 @@ var Metamuxer = (() => {
}
let msTimestamp = Math.floor(1e3 * chunk.timestamp);
const keyFrameQueuedEverywhere = this.#trackDatas.every((otherTrackData) => {
+ if (otherTrackData.track.source.closed) {
+ return true;
+ }
if (trackData === otherTrackData) {
return chunk.type === "key";
}
@@ -2078,8 +2665,13 @@ var Metamuxer = (() => {
chunk.data
] },
chunk.type === "delta" ? { id: 251 /* ReferenceBlock */, data: new EBMLSignedInt(trackData.lastWrittenMsTimestamp - msTimestamp) } : null,
- msDuration > 0 ? { id: 155 /* BlockDuration */, data: msDuration } : null,
- chunk.additions ? { id: 30113 /* BlockAdditions */, data: chunk.additions } : null
+ chunk.additions ? { id: 30113 /* BlockAdditions */, data: [
+ { id: 166 /* BlockMore */, data: [
+ { id: 165 /* BlockAdditional */, data: chunk.additions },
+ { id: 238 /* BlockAddID */, data: 1 }
+ ] }
+ ] } : null,
+ msDuration > 0 ? { id: 155 /* BlockDuration */, data: msDuration } : null
] };
this.writeEBML(blockGroup);
}
@@ -2271,110 +2863,12 @@ var Metamuxer = (() => {
throw new Error(`Unhandled codec '${codec}'.`);
};
- // 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;
- var preambleStartRegex = /^WEBVTT.*?\n{2}/;
- var timestampRegex = /(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})/;
- var inlineTimestampRegex = /<(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})>/g;
- var textEncoder = new TextEncoder();
- var SubtitleEncoder = class {
- #options;
- #config = null;
- #preambleBytes = null;
- #preambleEmitted = false;
- constructor(options) {
- this.#options = options;
- }
- configure(config) {
- if (config.codec !== "webvtt") {
- throw new Error("Codec must be 'webvtt'.");
- }
- this.#config = config;
- }
- encode(text) {
- if (!this.#config) {
- throw new Error("Encoder not configured.");
- }
- text = text.replace("\r\n", "\n").replace("\r", "\n");
- cueBlockHeaderRegex.lastIndex = 0;
- let match;
- if (!this.#preambleBytes) {
- if (!preambleStartRegex.test(text)) {
- let error = new Error("WebVTT preamble incorrect.");
- this.#options.error(error);
- throw error;
- }
- match = cueBlockHeaderRegex.exec(text);
- let preamble = text.slice(0, match?.index ?? text.length).trimEnd();
- if (!preamble) {
- let error = new Error("No WebVTT preamble provided.");
- this.#options.error(error);
- throw error;
- }
- this.#preambleBytes = textEncoder.encode(preamble);
- if (match) {
- text = text.slice(match.index);
- cueBlockHeaderRegex.lastIndex = 0;
- }
- }
- while (match = cueBlockHeaderRegex.exec(text)) {
- let notes = text.slice(0, match.index);
- let cueIdentifier = match[1] || "";
- let matchEnd = match.index + match[0].length;
- let bodyStart = text.indexOf("\n", matchEnd) + 1;
- let cueSettings = text.slice(matchEnd, bodyStart).trim();
- let bodyEnd = text.indexOf("\n\n", matchEnd);
- if (bodyEnd === -1) bodyEnd = text.length;
- let startTime = this.#parseTimestamp(match[2]);
- let endTime = this.#parseTimestamp(match[3]);
- let duration = endTime - startTime;
- let body = text.slice(bodyStart, bodyEnd);
- let additions = `${cueSettings}
-${cueIdentifier}
-${notes}`;
- inlineTimestampRegex.lastIndex = 0;
- body = body.replace(inlineTimestampRegex, (match2) => {
- let time = this.#parseTimestamp(match2.slice(1, -1));
- let offsetTime = time - startTime;
- return `<${this.#formatTimestamp(offsetTime)}>`;
- });
- text = text.slice(bodyEnd).trimStart();
- cueBlockHeaderRegex.lastIndex = 0;
- let chunk = {
- body: textEncoder.encode(body),
- additions: additions.trim() === "" ? null : textEncoder.encode(additions),
- timestamp: startTime * 1e3,
- duration: duration * 1e3
- };
- let meta = {};
- if (!this.#preambleEmitted) {
- meta.decoderConfig = {
- description: this.#preambleBytes
- };
- this.#preambleEmitted = true;
- }
- this.#options.output(chunk, meta);
- }
- }
- #parseTimestamp(string) {
- let match = timestampRegex.exec(string);
- if (!match) throw new Error("Expected match.");
- return 60 * 60 * 1e3 * Number(match[1] || "0") + 60 * 1e3 * Number(match[2]) + 1e3 * Number(match[3]) + Number(match[4]);
- }
- #formatTimestamp(timestamp) {
- let hours = Math.floor(timestamp / (60 * 60 * 1e3));
- let minutes = Math.floor(timestamp % (60 * 60 * 1e3) / (60 * 1e3));
- let seconds = Math.floor(timestamp % (60 * 1e3) / 1e3);
- let milliseconds = timestamp % 1e3;
- return hours.toString().padStart(2, "0") + ":" + minutes.toString().padStart(2, "0") + ":" + seconds.toString().padStart(2, "0") + "." + milliseconds.toString().padStart(3, "0");
- }
- };
-
// src/source.ts
var MediaSource = class {
constructor() {
this.connectedTrack = null;
this.closed = false;
+ this.offsetTimestamps = false;
}
ensureValidDigest() {
if (!this.connectedTrack) {
@@ -2459,7 +2953,9 @@ ${notes}`;
codec: buildVideoCodecString(this.codecConfig.codec, videoFrame.codedWidth, videoFrame.codedHeight),
width: videoFrame.codedWidth,
height: videoFrame.codedHeight,
- bitrate: this.codecConfig.bitrate
+ bitrate: this.codecConfig.bitrate,
+ framerate: this.source.connectedTrack?.metadata.frameRate,
+ latencyMode: this.codecConfig.latencyMode
});
}
async flush() {
@@ -2487,7 +2983,8 @@ ${notes}`;
digest(timestamp, duration = 0) {
const frame = new VideoFrame(this.canvas, {
timestamp: Math.round(1e6 * timestamp),
- duration: Math.round(1e6 * duration)
+ duration: Math.round(1e6 * duration),
+ alpha: "discard"
});
this.encoder.digest(frame);
frame.close();
@@ -2501,6 +2998,7 @@ ${notes}`;
super(codecConfig.codec);
this.track = track;
this.abortController = null;
+ this.offsetTimestamps = true;
this.encoder = new VideoEncoderWrapper(this, codecConfig);
}
start() {
@@ -2624,6 +3122,7 @@ ${notes}`;
super(codecConfig.codec);
this.track = track;
this.abortController = null;
+ this.offsetTimestamps = true;
this.encoder = new AudioEncoderWrapper(this, codecConfig);
}
start() {
@@ -2660,297 +3159,16 @@ ${notes}`;
var TextSubtitleSource = class extends SubtitleSource {
constructor(codec) {
super(codec);
- this.encoder = new SubtitleEncoder({
- output: (chunk, metadata) => this.connectedTrack?.output.muxer.addEncodedSubtitleChunk(this.connectedTrack, chunk, metadata),
+ this.parser = new SubtitleParser({
+ codec,
+ output: (cue, metadata) => this.connectedTrack?.output.muxer.addSubtitleCue(this.connectedTrack, cue, metadata),
error: (error) => console.error(error)
// TODO
});
- this.encoder.configure({ codec });
}
digest(text) {
this.ensureValidDigest();
- this.encoder.encode(text);
- }
- };
-
- // src/writer.ts
- var Writer = class {
- };
- var ArrayBufferTargetWriter = class extends Writer {
- #pos = 0;
- #target;
- #buffer = new ArrayBuffer(2 ** 16);
- #bytes = new Uint8Array(this.#buffer);
- #maxPos = 0;
- constructor(target) {
- super();
- this.#target = target;
- }
- #ensureSize(size) {
- let newLength = this.#buffer.byteLength;
- while (newLength < size) newLength *= 2;
- if (newLength === this.#buffer.byteLength) return;
- let newBuffer = new ArrayBuffer(newLength);
- let newBytes = new Uint8Array(newBuffer);
- newBytes.set(this.#bytes, 0);
- this.#buffer = newBuffer;
- this.#bytes = newBytes;
- }
- write(data) {
- this.#ensureSize(this.#pos + data.byteLength);
- this.#bytes.set(data, this.#pos);
- this.#pos += data.byteLength;
- this.#maxPos = Math.max(this.#maxPos, this.#pos);
- }
- seek(newPos) {
- this.#pos = newPos;
- }
- getPos() {
- return this.#pos;
- }
- flush() {
- }
- finalize() {
- this.#ensureSize(this.#pos);
- this.#target.buffer = this.#buffer.slice(0, Math.max(this.#maxPos, this.#pos));
- }
- };
- var StreamTargetWriter = class extends Writer {
- #pos = 0;
- #target;
- #sections = [];
- constructor(target) {
- super();
- this.#target = target;
- }
- write(data) {
- this.#sections.push({
- data: data.slice(),
- start: this.#pos
- });
- this.#pos += data.byteLength;
- }
- seek(newPos) {
- this.#pos = newPos;
- }
- getPos() {
- return this.#pos;
- }
- flush() {
- if (this.#sections.length === 0) return;
- let chunks = [];
- let sorted = [...this.#sections].sort((a, b) => a.start - b.start);
- chunks.push({
- start: sorted[0].start,
- size: sorted[0].data.byteLength
- });
- for (let i = 1; i < sorted.length; i++) {
- let lastChunk = chunks[chunks.length - 1];
- let section = sorted[i];
- if (section.start <= lastChunk.start + lastChunk.size) {
- lastChunk.size = Math.max(lastChunk.size, section.start + section.data.byteLength - lastChunk.start);
- } else {
- chunks.push({
- start: section.start,
- size: section.data.byteLength
- });
- }
- }
- for (let chunk of chunks) {
- chunk.data = new Uint8Array(chunk.size);
- for (let section of this.#sections) {
- if (chunk.start <= section.start && section.start < chunk.start + chunk.size) {
- chunk.data.set(section.data, section.start - chunk.start);
- }
- }
- this.#target.options.onData?.(chunk.data, chunk.start);
- }
- this.#sections.length = 0;
- }
- finalize() {
- }
- };
- var DEFAULT_CHUNK_SIZE = 2 ** 24;
- var MAX_CHUNKS_AT_ONCE = 2;
- var ChunkedStreamTargetWriter = class extends Writer {
- #pos = 0;
- #target;
- #chunkSize;
- /**
- * The data is divided up into fixed-size chunks, whose contents are first filled in RAM and then flushed out.
- * A chunk is flushed if all of its contents have been written.
- */
- #chunks = [];
- constructor(target) {
- super();
- this.#target = target;
- this.#chunkSize = target.options?.chunkSize ?? DEFAULT_CHUNK_SIZE;
- if (!Number.isInteger(this.#chunkSize) || this.#chunkSize < 2 ** 10) {
- throw new Error("Invalid StreamTarget options: chunkSize must be an integer not smaller than 1024.");
- }
- }
- write(data) {
- this.#writeDataIntoChunks(data, this.#pos);
- this.#flushChunks();
- this.#pos += data.byteLength;
- }
- seek(newPos) {
- this.#pos = newPos;
- }
- getPos() {
- return this.#pos;
- }
- #writeDataIntoChunks(data, position) {
- let chunkIndex = this.#chunks.findIndex((x) => x.start <= position && position < x.start + this.#chunkSize);
- if (chunkIndex === -1) chunkIndex = this.#createChunk(position);
- let chunk = this.#chunks[chunkIndex];
- let relativePosition = position - chunk.start;
- let toWrite = data.subarray(0, Math.min(this.#chunkSize - relativePosition, data.byteLength));
- chunk.data.set(toWrite, relativePosition);
- let section = {
- start: relativePosition,
- end: relativePosition + toWrite.byteLength
- };
- this.#insertSectionIntoChunk(chunk, section);
- if (chunk.written[0].start === 0 && chunk.written[0].end === this.#chunkSize) {
- chunk.shouldFlush = true;
- }
- if (this.#chunks.length > MAX_CHUNKS_AT_ONCE) {
- for (let i = 0; i < this.#chunks.length - 1; i++) {
- this.#chunks[i].shouldFlush = true;
- }
- this.#flushChunks();
- }
- if (toWrite.byteLength < data.byteLength) {
- this.#writeDataIntoChunks(data.subarray(toWrite.byteLength), position + toWrite.byteLength);
- }
- }
- #insertSectionIntoChunk(chunk, section) {
- let low = 0;
- let high = chunk.written.length - 1;
- let index = -1;
- while (low <= high) {
- let mid = Math.floor(low + (high - low + 1) / 2);
- if (chunk.written[mid].start <= section.start) {
- low = mid + 1;
- index = mid;
- } else {
- high = mid - 1;
- }
- }
- chunk.written.splice(index + 1, 0, section);
- if (index === -1 || chunk.written[index].end < section.start) index++;
- while (index < chunk.written.length - 1 && chunk.written[index].end >= chunk.written[index + 1].start) {
- chunk.written[index].end = Math.max(chunk.written[index].end, chunk.written[index + 1].end);
- chunk.written.splice(index + 1, 1);
- }
- }
- #createChunk(includesPosition) {
- let start = Math.floor(includesPosition / this.#chunkSize) * this.#chunkSize;
- let chunk = {
- start,
- data: new Uint8Array(this.#chunkSize),
- written: [],
- shouldFlush: false
- };
- this.#chunks.push(chunk);
- this.#chunks.sort((a, b) => a.start - b.start);
- return this.#chunks.indexOf(chunk);
- }
- #flushChunks(force = false) {
- for (let i = 0; i < this.#chunks.length; i++) {
- let chunk = this.#chunks[i];
- if (!chunk.shouldFlush && !force) continue;
- for (let section of chunk.written) {
- this.#target.options.onData?.(
- chunk.data.subarray(section.start, section.end),
- chunk.start + section.start
- );
- }
- this.#chunks.splice(i--, 1);
- }
- }
- flush() {
- }
- finalize() {
- this.#flushChunks(true);
- }
- };
- var FileSystemWritableFileStreamTargetWriter = class extends ChunkedStreamTargetWriter {
- constructor(target) {
- super(new StreamTarget({
- onData: (data, position) => target.stream.write({
- type: "write",
- data,
- position
- }),
- chunkSize: target.options?.chunkSize
- }));
- }
- };
-
- // src/target.ts
- var Target = class {
- constructor() {
- this.output = null;
- }
- };
- var ArrayBufferTarget2 = 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 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");
- }
- }
- }
- createWriter() {
- return new FileSystemWritableFileStreamTargetWriter(this);
+ this.parser.parse(text);
}
};
return __toCommonJS(src_exports);
diff --git a/dist/metamuxer.min.js b/dist/metamuxer.min.js
index d04301c..d1ed94e 100644
--- a/dist/metamuxer.min.js
+++ b/dist/metamuxer.min.js
@@ -1,10 +1,10 @@
-"use strict";var Metamuxer=(()=>{var ge=Object.defineProperty;var Ge=Object.getOwnPropertyDescriptor;var je=Object.getOwnPropertyNames;var qe=Object.prototype.hasOwnProperty;var Xe=(s,r)=>{for(var e in r)ge(s,e,{get:r[e],enumerable:!0})},Le=(s,r,e,t)=>{if(r&&typeof r=="object"||typeof r=="function")for(let i of je(r))!qe.call(s,i)&&i!==e&&ge(s,i,{get:()=>r[i],enumerable:!(t=Ge(r,i))||t.enumerable});return s};var Ye=s=>Le(ge({},"__esModule",{value:!0}),s);var qt={};Xe(qt,{ArrayBufferTarget:()=>be,AudioBufferSource:()=>de,AudioDataSource:()=>le,CanvasSource:()=>oe,EncodedAudioChunkSource:()=>ue,EncodedVideoChunkSource:()=>se,FileSystemWritableFileStreamTarget:()=>ke,MediaStreamAudioTrackSource:()=>ce,MediaStreamVideoTrackSource:()=>ae,MkvOutputFormat:()=>H,Mp4OutputFormat:()=>re,Output:()=>L,StreamTarget:()=>_,Target:()=>U,TextSubtitleSource:()=>he,VideoFrameSource:()=>ne,WebMOutputFormat:()=>E});var L=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 i={id:this.tracks.length+1,output:this,type:r,source:e,metadata:t};this.muxer.beforeTrackAdd(i),this.tracks.push(i),e.connectedTrack=i}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 d(s){if(!s)throw new Error("Assertion failed.")}var M=s=>s&&s[s.length-1],O=s=>s>=0&&s<2**32,v=(s,r,e)=>{let t=0;for(let i=r;i>l;t<<=1,t|=a}return t},Ve=(s,r,e,t)=>{for(let i=r;i>e-i-1<s instanceof ArrayBuffer?new Uint8Array(s):new Uint8Array(s.buffer,s.byteOffset,s.byteLength);var h=new Uint8Array(8),w=new DataView(h.buffer),k=s=>[(s%256+256)%256],m=s=>(w.setUint16(0,s,!1),[h[0],h[1]]),Ze=s=>(w.setInt16(0,s,!1),[h[0],h[1]]),Ue=s=>(w.setUint32(0,s,!1),[h[1],h[2],h[3]]),u=s=>(w.setUint32(0,s,!1),[h[0],h[1],h[2],h[3]]),Je=s=>(w.setInt32(0,s,!1),[h[0],h[1],h[2],h[3]]),V=s=>(w.setUint32(0,Math.floor(s/2**32),!1),w.setUint32(4,s,!1),[h[0],h[1],h[2],h[3],h[4],h[5],h[6],h[7]]),Ce=s=>(w.setInt16(0,2**8*s,!1),[h[0],h[1]]),S=s=>(w.setInt32(0,2**16*s,!1),[h[0],h[1],h[2],h[3]]),Te=s=>(w.setInt32(0,2**30*s,!1),[h[0],h[1],h[2],h[3]]),x=(s,r=!1)=>{let e=Array(s.length).fill(null).map((t,i)=>s.charCodeAt(i));return r&&e.push(0),e},xe=s=>{let r=null;for(let e of s)(!r||e.timestamp>r.timestamp)&&(r=e);return r},ze=s=>{let r=s*(Math.PI/180),e=Math.cos(r),t=Math.sin(r);return[e,t,0,-t,e,0,0,0,1]},Me=ze(0),Pe=s=>[S(s[0]),S(s[1]),Te(s[2]),S(s[3]),S(s[4]),Te(s[5]),S(s[6]),S(s[7]),Te(s[8])],p=(s,r,e)=>({type:s,contents:r&&new Uint8Array(r.flat(10)),children:e}),f=(s,r,e,t,i)=>p(s,[k(r),Ue(e),t??[]],i),Fe=s=>{let r=512;return s.fragmented?p("ftyp",[x("iso5"),u(r),x("iso5"),x("iso6"),x("mp41")]):p("ftyp",[x("isom"),u(r),x("isom"),s.holdsAvc?x("avc1"):[],x("mp41")])},Z=s=>({type:"mdat",largeSize:s}),De=s=>({type:"free",size:s}),N=(s,r,e=!1)=>p("moov",void 0,[et(r,s),...s.map(t=>tt(t,r)),e?vt(s):null]),et=(s,r)=>{let e=y(Math.max(0,...r.filter(o=>o.samples.length>0).map(o=>{let l=xe(o.samples);return l.timestamp+l.duration})),Y),t=Math.max(...r.map(o=>o.track.id))+1,i=!O(s)||!O(e),n=i?V:u;return f("mvhd",+i,0,[n(s),n(s),u(Y),n(e),S(1),Ce(1),Array(10).fill(0),Pe(Me),Array(24).fill(0),u(t)])},tt=(s,r)=>p("trak",void 0,[rt(s,r),it(s,r)]),rt=(s,r)=>{let e=xe(s.samples),t=y(e?e.timestamp+e.duration:0,Y),i=!O(r)||!O(t),n=i?V:u,o;if(s.type==="video"){let l=s.track.metadata.rotation;o=l===void 0||typeof l=="number"?ze(l??0):l}else o=Me;return f("tkhd",+i,3,[n(r),n(r),u(s.track.id),u(0),n(t),Array(8).fill(0),m(0),m(0),Ce(s.type==="audio"?1:0),m(0),Pe(o),S(s.type==="video"?s.info.width:0),S(s.type==="video"?s.info.height:0)])},it=(s,r)=>p("mdia",void 0,[st(s,r),nt(s.type==="video"?"vide":"soun"),ot(s)]),st=(s,r)=>{let e=xe(s.samples),t=y(e?e.timestamp+e.duration:0,s.timescale),i=!O(r)||!O(t),n=i?V:u;return f("mdhd",+i,0,[n(r),n(r),u(s.timescale),n(t),m(21956),m(0)])},nt=s=>f("hdlr",0,0,[x("mhlr"),x(s),u(0),u(0),u(0),x("mp4-muxer-hdlr",!0)]),ot=s=>p("minf",void 0,[s.type==="video"?at():ut(),lt(),ht(s)]),at=()=>f("vmhd",0,1,[m(0),m(0),m(0),m(0)]),ut=()=>f("smhd",0,0,[m(0),m(0)]),lt=()=>p("dinf",void 0,[dt()]),dt=()=>f("dref",0,0,[u(1)],[ct()]),ct=()=>f("url ",0,1),ht=s=>{let r=s.compositionTimeOffsetTable.length>1||s.compositionTimeOffsetTable.some(e=>e.sampleCompositionTimeOffset!==0);return p("stbl",void 0,[mt(s),xt(s),yt(s),St(s),wt(s),At(s),r?Ot(s):null])},mt=s=>f("stsd",0,0,[u(1)],[s.type==="video"?ft(Bt[s.track.source.codec],s):gt(_t[s.track.source.codec],s)]),ft=(s,r)=>p(s,[Array(6).fill(0),m(1),m(0),m(0),Array(12).fill(0),m(r.info.width),m(r.info.height),u(4718592),u(4718592),u(0),m(1),Array(32).fill(0),m(24),Ze(65535)],[It[r.track.source.codec](r)]),pt=s=>s.info.decoderConfig&&p("avcC",[...P(s.info.decoderConfig.description)]),bt=s=>s.info.decoderConfig&&p("hvcC",[...P(s.info.decoderConfig.description)]),Ee=s=>{if(!s.info.decoderConfig)return null;let r=s.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]),i=Number(e[2]),l=(Number(e[3])<<4)+(0<<1)+Number(r.colorSpace.fullRange);return f("vpcC",1,0,[k(t),k(i),k(l),k(2),k(2),k(2),m(0)])},kt=()=>{let e=(1<<7)+1;return p("av1C",[e,0,0,0])},gt=(s,r)=>p(s,[Array(6).fill(0),m(1),m(0),m(0),u(0),m(r.info.numberOfChannels),m(16),m(0),m(0),S(r.info.sampleRate)],[Wt[r.track.source.codec](r)]),Tt=s=>{let r=P(s.info.decoderConfig.description??new ArrayBuffer(0));return f("esds",0,0,[u(58753152),k(32+r.byteLength),m(1),k(0),u(75530368),k(18+r.byteLength),k(64),k(21),Ue(0),u(130071),u(130071),u(92307584),k(r.byteLength),...r,u(109084800),k(1),k(2)])},Ct=s=>{let r=3840,e=0,t=s.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 i=ArrayBuffer.isView(t)?new DataView(t.buffer,t.byteOffset,t.byteLength):new DataView(t);r=i.getUint16(10,!0),e=i.getInt16(14,!0)}return p("dOps",[k(0),k(s.info.numberOfChannels),m(r),u(s.info.sampleRate),Ce(e),k(0)])},xt=s=>f("stts",0,0,[u(s.timeToSampleTable.length),s.timeToSampleTable.map(r=>[u(r.sampleCount),u(r.sampleDelta)])]),yt=s=>{if(s.samples.every(e=>e.type==="key"))return null;let r=[...s.samples.entries()].filter(([,e])=>e.type==="key");return f("stss",0,0,[u(r.length),r.map(([e])=>u(e+1))])},St=s=>f("stsc",0,0,[u(s.compactlyCodedChunkTable.length),s.compactlyCodedChunkTable.map(r=>[u(r.firstChunk),u(r.samplesPerChunk),u(1)])]),wt=s=>f("stsz",0,0,[u(0),u(s.samples.length),s.samples.map(r=>u(r.size))]),At=s=>s.finalizedChunks.length>0&&M(s.finalizedChunks).offset>=2**32?f("co64",0,0,[u(s.finalizedChunks.length),s.finalizedChunks.map(r=>V(r.offset))]):f("stco",0,0,[u(s.finalizedChunks.length),s.finalizedChunks.map(r=>u(r.offset))]),Ot=s=>f("ctts",0,0,[u(s.compositionTimeOffsetTable.length),s.compositionTimeOffsetTable.map(r=>[u(r.sampleCount),u(r.sampleCompositionTimeOffset)])]),vt=s=>p("mvex",void 0,s.map(Vt)),Vt=s=>f("trex",0,0,[u(s.track.id),u(1),u(0),u(0),u(0)]),ye=(s,r)=>p("moof",void 0,[Et(s),...r.map(Ut)]),Et=s=>f("mfhd",0,0,[u(s)]),Be=s=>{let r=0,e=0,t=0,i=0,n=s.type==="delta";return e|=+n,n?r|=1:r|=2,r<<24|e<<16|t<<8|i},Ut=s=>p("traf",void 0,[zt(s),Mt(s),Pt(s)]),zt=s=>{d(s.currentChunk);let r=0;r|=8,r|=16,r|=32,r|=131072;let e=s.currentChunk.samples[1]??s.currentChunk.samples[0],t={duration:e.timescaleUnitsToNextSample,size:e.size,flags:Be(e)};return f("tfhd",0,r,[u(s.track.id),u(t.duration),u(t.size),u(t.flags)])},Mt=s=>(d(s.currentChunk),f("tfdt",1,0,[V(y(s.currentChunk.startTimestamp,s.timescale))])),Pt=s=>{d(s.currentChunk);let r=s.currentChunk.samples.map(T=>T.timescaleUnitsToNextSample),e=s.currentChunk.samples.map(T=>T.size),t=s.currentChunk.samples.map(Be),i=s.currentChunk.samples.map(T=>y(T.timestamp-T.decodeTimestamp,s.timescale)),n=new Set(r),o=new Set(e),l=new Set(t),a=new Set(i),c=l.size===2&&t[0]!==t[1],b=n.size>1,g=o.size>1,C=!c&&l.size>1,W=a.size>1||[...a].some(T=>T!==0),A=0;return A|=1,A|=4*+c,A|=256*+b,A|=512*+g,A|=1024*+C,A|=2048*+W,f("trun",1,A,[u(s.currentChunk.samples.length),u(s.currentChunk.offset-s.currentChunk.moofOffset||0),c?u(t[0]):[],s.currentChunk.samples.map((T,z)=>[b?u(r[z]):[],g?u(e[z]):[],C?u(t[z]):[],W?Je(i[z]):[]])])},Ie=s=>p("mfra",void 0,[...s.map(Ft),Dt()]),Ft=(s,r)=>f("tfra",1,0,[u(s.track.id),u(63),u(s.finalizedChunks.length),s.finalizedChunks.map(t=>[V(y(t.startTimestamp,s.timescale)),V(t.moofOffset),u(r+1),u(1),u(1)])]),Dt=()=>f("mfro",0,0,[u(0)]),Bt={avc:"avc1",hevc:"hvc1",vp8:"vp08",vp9:"vp09",av1:"av01"},It={avc:pt,hevc:bt,vp8:Ee,vp9:Ee,av1:kt},_t={aac:"mp4a",opus:"Opus"},Wt={aac:Tt,opus:Ct};var F=class{constructor(r){this.output=r}beforeTrackAdd(r){}onTrackClose(r){}};var Y=1e3,Nt=2082844800,y=(s,r,e=!0)=>{let t=s*r;return e?Math.round(t):t},J=class extends F{constructor(e,t){super(e);this.#i=new Uint8Array(8);this.#t=new DataView(this.#i.buffer);this.offsets=new WeakMap;this.#s=null;this.#o=null;this.#n=[];this.#a=Math.floor(Date.now()/1e3)+Nt;this.#l=[];this.#h=1;this.#e=e.writer,this.#r=t}#e;#r;#i;#t;#s;#o;#n;#a;#l;#h;writeU32(e){this.#t.setUint32(0,e,!1),this.#e.write(this.#i.subarray(0,4))}writeU64(e){this.#t.setUint32(0,Math.floor(e/2**32),!1),this.#t.setUint32(4,e,!1),this.#e.write(this.#i.subarray(0,8))}writeAscii(e){for(let t=0;tt.type==="video"&&t.source.codec==="avc");if(this.writeBox(Fe({holdsAvc:e,fragmented:this.#r.options.fastStart==="fragmented"})),this.#s=this.#e.getPos(),this.#r.options.fastStart==="in-memory")this.#o=Z(!1);else if(this.#r.options.fastStart!=="fragmented"){if(typeof this.#r.options.fastStart=="object"){let t=this.#d();this.#e.seek(this.#e.getPos()+t)}this.#o=Z(!0),this.writeBox(this.#o)}this.#e.flush()}#d(){d(typeof this.#r.options.fastStart=="object");let e=0,t=[this.#r.options.fastStart.expectedVideoChunks,this.#r.options.fastStart.expectedAudioChunks];for(let i of t)i&&(e+=8*Math.ceil(2/3*i),e+=4*i,e+=12*Math.ceil(2/3*i),e+=4*i,e+=8*i);return e+=4096,e}#u(e,t){let i=this.#n.find(o=>o.track===e);if(i)return i;d(t),d(t.decoderConfig),d(t.decoderConfig.codedWidth!==void 0),d(t.decoderConfig.codedHeight!==void 0);let n={track:e,type:"video",info:{width:t.decoderConfig.codedWidth,height:t.decoderConfig.codedHeight,decoderConfig:t.decoderConfig},timescale:e.metadata.frameRate??57600,samples:[],sampleQueue:[],timestampProcessingQueue:[],firstTimestamp:null,lastKeyFrameTimestamp:null,timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.#n.push(n),this.#n.sort((o,l)=>o.track.id-l.track.id),n}#m(e,t){let i=this.#n.find(o=>o.track===e);if(i)return i;d(t),d(t.decoderConfig);let n={track:e,type:"audio",info:{numberOfChannels:t.decoderConfig.numberOfChannels,sampleRate:t.decoderConfig.sampleRate,decoderConfig:t.decoderConfig},timescale:t.decoderConfig.sampleRate,samples:[],sampleQueue:[],timestampProcessingQueue:[],firstTimestamp:null,lastKeyFrameTimestamp:null,timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.#n.push(n),this.#n.sort((o,l)=>o.track.id-l.track.id),n}addEncodedVideoChunk(e,t,i){let n=this.#u(e,i);if(typeof this.#r.options.fastStart=="object"&&n.samples.length===this.#r.options.fastStart.expectedVideoChunks)throw new Error(`Cannot add more video chunks than specified in 'fastStart' (${this.#r.options.fastStart.expectedVideoChunks}).`);let o=this.#f(n,t);this.#r.options.fastStart==="fragmented"?(n.sampleQueue.push(o),this.#b()):this.#p(n,o)}addEncodedAudioChunk(e,t,i){let n=this.#m(e,i);if(typeof this.#r.options.fastStart=="object"&&n.samples.length===this.#r.options.fastStart.expectedAudioChunks)throw new Error(`Cannot add more audio chunks than specified in 'fastStart' (${this.#r.options.fastStart.expectedAudioChunks}).`);let o=this.#f(n,t);this.#r.options.fastStart==="fragmented"?(n.sampleQueue.push(o),this.#b()):this.#p(n,o)}#f(e,t){let i=this.#x(e,t),n=(t.duration??0)/1e6,o=new Uint8Array(t.byteLength);return t.copyTo(o),{timestamp:i,decodeTimestamp:i,duration:n,data:o,size:o.byteLength,type:t.type,timescaleUnitsToNextSample:y(n,e.timescale)}}#c(e){if(e.timestampProcessingQueue.length===0)return;let t=e.timestampProcessingQueue.map(i=>i.timestamp).sort((i,n)=>i-n);for(let i=0;i{if(e===l)return t.type==="key";let a=l.sampleQueue[0];return a&&a.type==="key"});n>=1&&o&&(i=!0,this.#T())}else i=n>=.5}i&&(e.currentChunk&&this.#g(e),e.currentChunk={startTimestamp:t.timestamp,samples:[],offset:null,moofOffset:null}),d(e.currentChunk),e.currentChunk.samples.push(t),e.timestampProcessingQueue.push(t)}#x(e,t){let i=t.timestamp/1e6;if(i<0)throw new Error(`Timestamps must be non-negative (got ${i}s).`);if(e.firstTimestamp===null&&(e.firstTimestamp=i),i-=e.firstTimestamp,e.lastKeyFrameTimestamp!==null&&i=2**32&&(a.largeSize=!0,b=this.measureBox(a)+c),a.size=b,this.writeBox(a)}for(let a of this.#n){a.currentChunk.offset=this.#e.getPos(),a.currentChunk.moofOffset=i;for(let c of a.currentChunk.samples)this.#e.write(c.data),c.data=null}let o=this.#e.getPos();this.#e.seek(this.offsets.get(n));let l=ye(t,this.#n);this.writeBox(l),this.#e.seek(o);for(let a of this.#n)a.finalizedChunks.push(a.currentChunk),this.#l.push(a.currentChunk),a.currentChunk=null;e&&this.#e.flush()}finalize(){if(this.#r.options.fastStart==="fragmented"){for(let e of this.#n){for(let t of e.sampleQueue)this.#p(e,t);this.#c(e)}this.#T(!1)}else for(let e of this.#n)this.#c(e),this.#g(e);if(this.#r.options.fastStart==="in-memory"){d(this.#o);let e;for(let i=0;i<2;i++){let n=N(this.#n,this.#a),o=this.measureBox(n);e=this.measureBox(this.#o);let l=this.#e.getPos()+o+e;for(let a of this.#l){a.offset=l;for(let{data:c}of a.samples)d(c),l+=c.byteLength,e+=c.byteLength}if(l<2**32)break;e>=2**32&&(this.#o.largeSize=!0)}let t=N(this.#n,this.#a);this.writeBox(t),this.#o.size=e,this.writeBox(this.#o);for(let i of this.#l)for(let n of i.samples)d(n.data),this.#e.write(n.data),n.data=null}else if(this.#r.options.fastStart==="fragmented"){let e=this.#e.getPos(),t=Ie(this.#n);this.writeBox(t);let i=this.#e.getPos()-e;this.#e.seek(this.#e.getPos()-4),this.writeU32(i)}else{d(this.#o),d(this.#s!==null);let e=this.offsets.get(this.#o);d(e!==void 0);let t=this.#e.getPos()-e;this.#o.size=t,this.#o.largeSize=t>=2**32,this.patchBox(this.#o);let i=N(this.#n,this.#a);if(typeof this.#r.options.fastStart=="object"){this.#e.seek(this.#s),this.writeBox(i);let n=e-this.#e.getPos();this.writeBox(De(n))}else this.writeBox(i)}}};var R=class{constructor(r){this.value=r}},D=class{constructor(r){this.value=r}},Q=class{constructor(r){this.value=r}};var Se=s=>s<256?1:s<65536?2:s<1<<24?3:s<2**32?4:s<2**40?5:6,we=s=>s>=-64&&s<64?1:s>=-8192&&s<8192?2:s>=-(1<<20)&&s<1<<20?3:s>=-(1<<27)&&s<1<<27?4:s>=-(2**34)&&s<2**34?5:6,_e=s=>{if(s<127)return 1;if(s<16383)return 2;if(s<(1<<21)-1)return 3;if(s<(1<<28)-1)return 4;if(s<2**35-1)return 5;if(s<2**42-1)return 6;throw new Error("EBML VINT size not supported "+s)};var Ae=2**15,We="https://github.com/Vanilagy/webm-muxer",Ne=6,Re=5,Rt={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",vorbis:"A_VORBIS",webvtt:"S_TEXT/WEBVTT"},Qt={video:1,audio:2,subtitle:17},ee=class extends F{constructor(e,t){super(e);this.#i=new Uint8Array(8);this.#t=new DataView(this.#i.buffer);this.offsets=new WeakMap;this.dataOffsets=new WeakMap;this.#s=[];this.#o=null;this.#n=null;this.#a=null;this.#l=null;this.#h=null;this.#d=null;this.#u=null;this.#m=null;this.#f=new Set;this.#c=0;this.#e=e.writer,this.#r=t}#e;#r;#i;#t;#s;#o;#n;#a;#l;#h;#d;#u;#m;#f;#c;#p(e){this.#t.setUint8(0,e),this.#e.write(this.#i.subarray(0,1))}#x(e){this.#t.setFloat32(0,e,!1),this.#e.write(this.#i.subarray(0,4))}#g(e){this.#t.setFloat64(0,e,!1),this.#e.write(this.#i)}#b(e,t=Se(e)){let i=0;switch(t){case 6:this.#t.setUint8(i++,e/2**40|0);case 5:this.#t.setUint8(i++,e/2**32|0);case 4:this.#t.setUint8(i++,e>>24);case 3:this.#t.setUint8(i++,e>>16);case 2:this.#t.setUint8(i++,e>>8);case 1:this.#t.setUint8(i++,e);break;default:throw new Error("Bad UINT size "+t)}this.#e.write(this.#i.subarray(0,i))}#T(e,t=we(e)){e<0&&(e+=2**(t*8)),this.#b(e,t)}writeEBMLVarInt(e,t=_e(e)){let i=0;switch(t){case 1:this.#t.setUint8(i++,128|e);break;case 2:this.#t.setUint8(i++,64|e>>8),this.#t.setUint8(i++,e);break;case 3:this.#t.setUint8(i++,32|e>>16),this.#t.setUint8(i++,e>>8),this.#t.setUint8(i++,e);break;case 4:this.#t.setUint8(i++,16|e>>24),this.#t.setUint8(i++,e>>16),this.#t.setUint8(i++,e>>8),this.#t.setUint8(i++,e);break;case 5:this.#t.setUint8(i++,8|e/2**32&7),this.#t.setUint8(i++,e>>24),this.#t.setUint8(i++,e>>16),this.#t.setUint8(i++,e>>8),this.#t.setUint8(i++,e);break;case 6:this.#t.setUint8(i++,4|e/2**40&3),this.#t.setUint8(i++,e/2**32|0),this.#t.setUint8(i++,e>>24),this.#t.setUint8(i++,e>>16),this.#t.setUint8(i++,e>>8),this.#t.setUint8(i++,e);break;default:throw new Error("Bad EBML VINT size "+t)}this.#e.write(this.#i.subarray(0,i))}#A(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.#b(e.id),Array.isArray(e.data)){let t=this.#e.getPos(),i=e.size===-1?1:e.size??4;e.size===-1?this.#p(255):this.#e.seek(this.#e.getPos()+i);let n=this.#e.getPos();if(this.dataOffsets.set(e,n),this.writeEBML(e.data),e.size!==-1){let o=this.#e.getPos()-n,l=this.#e.getPos();this.#e.seek(t),this.writeEBMLVarInt(o,i),this.#e.seek(l)}}else if(typeof e.data=="number"){let t=e.size??Se(e.data);this.writeEBMLVarInt(t),this.#b(e.data,t)}else if(typeof e.data=="string")this.writeEBMLVarInt(e.data.length),this.#A(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 R)this.writeEBMLVarInt(4),this.#x(e.data.value);else if(e.data instanceof D)this.writeEBMLVarInt(8),this.#g(e.data.value);else if(e.data instanceof Q){let t=e.size??we(e.data.value);this.writeEBMLVarInt(t),this.#T(e.data.value,t)}}}beforeTrackAdd(e){if(this.#r instanceof E)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.#O(),this.#r.options.streaming||this.#v(),this.#V(),this.#z(),this.#e.flush()}#O(){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 E?"webm":"matroska"},{id:17031,data:2},{id:17029,data:2}]};this.writeEBML(e)}#v(){let e=new Uint8Array([28,83,187,107]),t=new Uint8Array([21,73,169,102]),i=new Uint8Array([22,84,174,107]),n={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:i},{id:21420,size:5,data:0}]}]};this.#a=n}#V(){let e={id:17545,data:new D(0)};this.#h=e;let t={id:357149030,data:[{id:2807729,data:1e6},{id:19840,data:We},{id:22337,data:We},this.#r.options.streaming?null:e]};this.#n=t}#E(){let e={id:374648427,data:[]};this.#l=e;for(let t of this.#s)e.data.push({id:174,data:[{id:215,data:t.track.id},{id:29637,data:t.track.id},{id:131,data:Qt[t.type]},{id:134,data:Rt[t.track.source.codec]},t.info.decoderConfig.description?{id:25506,data:P(t.info.decoderConfig.description)}:null,...t.type==="video"?[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 i=t.info.decoderConfig.colorSpace;return!i.matrix||!i.transfer||!i.primaries||i.fullRange==null?null:{id:21936,data:[{id:21937,data:{rgb:1,bt709:1,bt470bg:5,smpte170m:6}[i.matrix]},{id:21946,data:{bt709:1,smpte170m:6,"iec61966-2-1":13}[i.transfer]},{id:21947,data:{bt709:1,bt470bg:5,smpte170m:6}[i.primaries]},{id:21945,data:[1,2][Number(i.fullRange)]}]}}return null})()]}]:[],...t.type==="audio"?[{id:225,data:[{id:181,data:new R(t.info.sampleRate)},{id:159,data:t.info.numberOfChannels}]}]:[]]})}#U(){let e={id:408125543,size:this.#r.options.streaming?-1:Ne,data:[this.#r.options.streaming?null:this.#a,this.#n,this.#l]};this.#o=e,this.writeEBML(e)}#z(){this.#d={id:475249515,data:[]}}get#k(){return d(this.#o),this.dataOffsets.get(this.#o)}#M(e,t){let i=this.#s.find(o=>o.track===e);if(i)return i;d(t),d(t.decoderConfig),d(t.decoderConfig.codedWidth!==void 0),d(t.decoderConfig.codedHeight!==void 0);let n={track:e,type:"video",info:{width:t.decoderConfig.codedWidth,height:t.decoderConfig.codedHeight,decoderConfig:t.decoderConfig},chunkQueue:[],firstTimestamp:null,lastKeyFrameTimestamp:null,lastWrittenMsTimestamp:null};return this.#s.push(n),this.#s.sort((o,l)=>o.track.id-l.track.id),n}#P(e,t){let i=this.#s.find(o=>o.track===e);if(i)return i;d(t),d(t.decoderConfig);let n={track:e,type:"audio",info:{numberOfChannels:t.decoderConfig.numberOfChannels,sampleRate:t.decoderConfig.sampleRate,decoderConfig:t.decoderConfig},chunkQueue:[],firstTimestamp:null,lastKeyFrameTimestamp:null,lastWrittenMsTimestamp:null};return this.#s.push(n),this.#s.sort((o,l)=>o.track.id-l.track.id),n}#F(e,t){let i=this.#s.find(o=>o.track===e);if(i)return i;d(t),d(t.decoderConfig);let n={track:e,type:"subtitle",info:{decoderConfig:t.decoderConfig},chunkQueue:[],firstTimestamp:null,lastKeyFrameTimestamp:null,lastWrittenMsTimestamp:null};return this.#s.push(n),this.#s.sort((o,l)=>o.track.id-l.track.id),n}addEncodedVideoChunk(e,t,i){let n=this.#M(e,i),o=new Uint8Array(t.byteLength);t.copyTo(o);let l=this.#y(n,o,t.timestamp,t.duration??0,t.type);e.source.codec==="vp9"&&this.#D(n,l),n.chunkQueue.push(l),this.#C()}addEncodedAudioChunk(e,t,i){let n=this.#P(e,i),o=new Uint8Array(t.byteLength);t.copyTo(o);let l=this.#y(n,o,t.timestamp,t.duration??0,t.type);n.chunkQueue.push(l),this.#C()}addEncodedSubtitleChunk(e,t,i){let n=this.#F(e,i),o=this.#y(n,t.body,t.timestamp,t.duration,"key",t.additions);n.chunkQueue.push(o),this.#C()}#C(){let e=0;for(let t of this.#s)t.track.source.closed||e++;if(!(this.#s.length0&&o.chunkQueue[0].timestamp=2&&i++;let c={rgb:7,bt709:2,bt470bg:1,smpte170m:3}[e.info.decoderConfig.colorSpace.matrix];Ve(t.data,i+0,i+3,c)}#y(e,t,i,n,o,l=null){let a=this.#B(e,i,o==="key");return{data:t,type:o,timestamp:a,duration:n/1e6,additions:l}}#B(e,t,i){let n=t/1e6;if(n<0)throw new Error(`Timestamps must be non-negative (got ${n}s).`);if(e.firstTimestamp===null&&(e.firstTimestamp=n),n-=e.firstTimestamp,e.lastKeyFrameTimestamp!==null&&n{if(e===g)return t.type==="key";let C=g.chunkQueue[0];return C&&C.type==="key"});(!this.#u||n&&i-this.#m>=1e3)&&this.#I(i);let o=i-this.#m;if(o<0)return;if(o>=Ae)throw new Error(`Current Matroska cluster exceeded its maximum allowed length of ${Ae} milliseconds. In order to produce a correct WebM file, you must pass in a key frame at least every ${Ae} milliseconds.`);let a=new Uint8Array(4),c=new DataView(a.buffer);c.setUint8(0,128|e.track.id),c.setInt16(1,o,!1);let b=Math.floor(1e3*t.duration);if(b===0&&!t.additions){c.setUint8(3,+(t.type==="key")<<7);let g={id:163,data:[a,t.data]};this.writeEBML(g)}else{let g={id:160,data:[{id:161,data:[a,t.data]},t.type==="delta"?{id:251,data:new Q(e.lastWrittenMsTimestamp-i)}:null,b>0?{id:155,data:b}:null,t.additions?{id:30113,data:t.additions}:null]};this.writeEBML(g)}this.#c=Math.max(this.#c,i+b),e.lastWrittenMsTimestamp=i,this.#f.add(e)}#I(e){this.#u&&!this.#r.options.streaming&&this.#w(),this.#u={id:524531317,size:this.#r.options.streaming?-1:Re,data:[{id:231,data:e}]},this.writeEBML(this.#u),this.#m=e,this.#f.clear()}#w(){d(this.#u);let e=this.#e.getPos()-this.dataOffsets.get(this.#u),t=this.#e.getPos();this.#e.seek(this.offsets.get(this.#u)+4),this.writeEBMLVarInt(e,Re),this.#e.seek(t);let i=this.offsets.get(this.#u)-this.#k;d(this.#d),this.#d.data.push({id:187,data:[{id:179,data:this.#m},...[...this.#f].map(n=>({id:183,data:[{id:247,data:n.track.id},{id:241,data:i}]}))]})}onTrackClose(){this.#C()}finalize(){for(let e of this.#s)for(;e.chunkQueue.length>0;)this.#S(e,e.chunkQueue.shift());if(this.#r.options.streaming||this.#w(),d(this.#d),this.writeEBML(this.#d),!this.#r.options.streaming){let e=this.#e.getPos(),t=this.#e.getPos()-this.#k;this.#e.seek(this.offsets.get(this.#o)+4),this.writeEBMLVarInt(t,Ne),this.#h.data=new D(this.#c),this.#e.seek(this.offsets.get(this.#h)),this.writeEBML(this.#h),this.#a.data[0].data[1].data=this.offsets.get(this.#d)-this.#k,this.#a.data[1].data[1].data=this.offsets.get(this.#n)-this.#k,this.#a.data[2].data[1].data=this.offsets.get(this.#l)-this.#k,this.#e.seek(this.offsets.get(this.#a)),this.writeEBML(this.#a),this.#e.seek(e)}}};var te=class{},re=class extends te{constructor(e){super();this.options=e}createMuxer(e){return new J(e,this)}},H=class extends te{constructor(e={}){super();this.options=e}createMuxer(e){return new ee(e,this)}},E=class extends H{};var Qe=(s,r,e)=>{if(s==="avc"){let t=100;r<=768&&e<=432?t=66:r<=1920&&e<=1080&&(t=77);let i=0,n=r>1920||e>1080?50:41,o=t.toString(16).padStart(2,"0"),l=i.toString(16).padStart(2,"0"),a=n.toString(16).padStart(2,"0");return`avc1.${o}${l}${a}`}else if(s==="hevc"){let t=0,i=1,n=Array(32).fill(0);n[i]=1;let o=parseInt(n.reverse().join(""),2).toString(16).replace(/^0+/,""),l="L",a=120;return r<=1280&&e<=720?a=93:r<=1920&&e<=1080?a=120:r<=3840&&e<=2160?a=150:(l="H",a=180),`hev1.${t===0?"":String.fromCharCode(65+t-1)}${i}.${o}.${l}${a}.B0`}else{if(s==="vp8")return"vp8";if(s==="vp9"){let t="00",i;return r<=854&&e<=480?i="21":r<=1280&&e<=720?i="31":r<=1920&&e<=1080?i="41":r<=3840&&e<=2160?i="51":i="61",`vp09.${t}.${i}.08`}else if(s==="av1"){let i;return r<=854&&e<=480?i="01":r<=1280&&e<=720?i="03":r<=1920&&e<=1080?i="04":r<=3840&&e<=2160?i="07":i="09",`av01.0.${i}M.08`}}throw new Error(`Unhandled codec '${s}'.`)},He=(s,r,e)=>{if(s==="aac")return r>=2&&e<=24e3?"mp4a.40.29":e<=24e3?"mp4a.40.5":"mp4a.40.2";if(s==="opus")return"opus";if(s==="vorbis")return"vorbis";throw new Error(`Unhandled codec '${s}'.`)};var $=/(?:(.+?)\n)?((?:\d{2}:)?\d{2}:\d{2}.\d{3})\s+-->\s+((?:\d{2}:)?\d{2}:\d{2}.\d{3})/g,Ht=/^WEBVTT.*?\n{2}/,$t=/(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})/,$e=/<(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})>/g,Oe=new TextEncoder,ie=class{#e;#r=null;#i=null;#t=!1;constructor(r){this.#e=r}configure(r){if(r.codec!=="webvtt")throw new Error("Codec must be 'webvtt'.");this.#r=r}encode(r){if(!this.#r)throw new Error("Encoder not configured.");r=r.replace(`\r
+"use strict";var Metamuxer=(()=>{var Se=Object.defineProperty;var Ze=Object.getOwnPropertyDescriptor;var Je=Object.getOwnPropertyNames;var et=Object.prototype.hasOwnProperty;var tt=(i,t)=>{for(var e in t)Se(i,e,{get:t[e],enumerable:!0})},rt=(i,t,e,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of Je(t))!et.call(i,s)&&s!==e&&Se(i,s,{get:()=>t[s],enumerable:!(r=Ze(t,s))||r.enumerable});return i};var it=i=>rt(Se({},"__esModule",{value:!0}),i);var ar={};tt(ar,{ArrayBufferTarget:()=>W,AudioBufferSource:()=>Ce,AudioDataSource:()=>ke,CanvasSource:()=>be,EncodedAudioChunkSource:()=>ge,EncodedVideoChunkSource:()=>pe,FileSystemWritableFileStreamTarget:()=>le,MediaStreamAudioTrackSource:()=>xe,MediaStreamVideoTrackSource:()=>Te,MkvOutputFormat:()=>Y,Mp4OutputFormat:()=>fe,Output:()=>ee,StreamTarget:()=>F,Target:()=>E,TextSubtitleSource:()=>ye,VideoFrameSource:()=>he,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 B=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},Ee=(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;var H=/(?:(.+?)\n)?((?:\d{2}:)?\d{2}:\d{2}.\d{3})\s+-->\s+((?:\d{2}:)?\d{2}:\d{2}.\d{3})/g,st=/^WEBVTT(.|\n)*?\n{2}/,_=/<(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})>/g,te=class{#e;#i=null;#t=!1;constructor(t){this.#e=t}parse(t){t=t.replaceAll(`\r
`,`
-`).replace("\r",`
-`),$.lastIndex=0;let e;if(!this.#i){if(!Ht.test(r)){let i=new Error("WebVTT preamble incorrect.");throw this.#e.error(i),i}e=$.exec(r);let t=r.slice(0,e?.index??r.length).trimEnd();if(!t){let i=new Error("No WebVTT preamble provided.");throw this.#e.error(i),i}this.#i=Oe.encode(t),e&&(r=r.slice(e.index),$.lastIndex=0)}for(;e=$.exec(r);){let t=r.slice(0,e.index),i=e[1]||"",n=e.index+e[0].length,o=r.indexOf(`
-`,n)+1,l=r.slice(n,o).trim(),a=r.indexOf(`
+`).replaceAll("\r",`
+`),H.lastIndex=0;let e;if(!this.#i){if(!st.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(`
+`,o)+1,a=t.slice(o,n).trim(),u=t.indexOf(`
-`,n);a===-1&&(a=r.length);let c=this.#s(e[2]),g=this.#s(e[3])-c,C=r.slice(o,a),W=`${l}
-${i}
-${t}`;$e.lastIndex=0,C=C.replace($e,z=>{let Ke=this.#s(z.slice(1,-1))-c;return`<${this.#o(Ke)}>`}),r=r.slice(a).trimStart(),$.lastIndex=0;let A={body:Oe.encode(C),additions:W.trim()===""?null:Oe.encode(W),timestamp:c*1e3,duration:g*1e3},T={};this.#t||(T.decoderConfig={description:this.#i},this.#t=!0),this.#e.output(A,T)}}#s(r){let e=$t.exec(r);if(!e)throw new Error("Expected match.");return 60*60*1e3*Number(e[1]||"0")+60*1e3*Number(e[2])+1e3*Number(e[3])+Number(e[4])}#o(r){let e=Math.floor(r/36e5),t=Math.floor(r%(60*60*1e3)/(60*1e3)),i=Math.floor(r%(60*1e3)/1e3),n=r%1e3;return e.toString().padStart(2,"0")+":"+t.toString().padStart(2,"0")+":"+i.toString().padStart(2,"0")+"."+n.toString().padStart(3,"0")}};var K=class{constructor(){this.connectedTrack=null;this.closed=!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)}},B=class extends K{constructor(e){super();this.connectedTrack=null;this.codec=e}},se=class extends B{constructor(r){super(r)}digest(r,e){this.ensureValidDigest(),this.connectedTrack?.output.muxer.addEncodedVideoChunk(this.connectedTrack,r,e)}},Kt=5,G=class{constructor(r,e){this.source=r;this.codecConfig=e;this.encoder=null;this.lastMultipleOfKeyFrameInterval=-1}digest(r){this.source.ensureValidDigest(),this.ensureEncoder(r),d(this.encoder);let e=Math.floor(r.timestamp/1e6/Kt);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:Qe(this.codecConfig.codec,r.codedWidth,r.codedHeight),width:r.codedWidth,height:r.codedHeight,bitrate:this.codecConfig.bitrate}))}async flush(){return this.encoder?.flush()}},ne=class extends B{constructor(r){super(r.codec),this.encoder=new G(this,r)}digest(r){this.encoder.digest(r)}flush(){return this.encoder.flush()}},oe=class extends B{constructor(e,t){super(t.codec);this.canvas=e;this.encoder=new G(this,t)}digest(e,t=0){let i=new VideoFrame(this.canvas,{timestamp:Math.round(1e6*e),duration:Math.round(1e6*t)});this.encoder.digest(i),i.close()}flush(){return this.encoder.flush()}},ae=class extends B{constructor(e,t){super(t.codec);this.track=e;this.abortController=null;this.encoder=new G(this,t)}start(){this.abortController=new AbortController;let e=new MediaStreamTrackProcessor({track:this.track}),t=new WritableStream({write:i=>{this.encoder.digest(i),i.close()}});e.readable.pipeTo(t,{signal:this.abortController.signal}).catch(i=>{i instanceof DOMException&&i.name==="AbortError"||console.error("Pipe error:",i)})}async flush(){this.abortController&&(this.abortController.abort(),this.abortController=null),await this.encoder.flush()}},I=class extends K{constructor(e){super();this.connectedTrack=null;this.codec=e}},ue=class extends I{constructor(r){super(r)}digest(r,e){this.ensureValidDigest(),this.connectedTrack?.output.muxer.addEncodedAudioChunk(this.connectedTrack,r,e)}},j=class{constructor(r,e){this.source=r;this.codecConfig=e;this.encoder=null}digest(r){this.source.ensureValidDigest(),this.ensureEncoder(r),d(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:He(this.codecConfig.codec,r.numberOfChannels,r.sampleRate),numberOfChannels:r.numberOfChannels,sampleRate:r.sampleRate,bitrate:this.codecConfig.bitrate}))}async flush(){return this.encoder?.flush()}},le=class extends I{constructor(r){super(r.codec),this.encoder=new j(this,r)}digest(r){this.encoder.digest(r)}flush(){return this.encoder.flush()}},de=class extends I{constructor(e){super(e.codec);this.accumulatedFrameCount=0;this.encoder=new j(this,e)}digest(e){let t=e.numberOfChannels,i=e.sampleRate,n=e.length,o=new Float32Array(t*n);for(let a=0;a{this.encoder.digest(i),i.close()}});e.readable.pipeTo(t,{signal:this.abortController.signal}).catch(i=>{i instanceof DOMException&&i.name==="AbortError"||console.error("Pipe error:",i)})}async flush(){this.abortController&&(this.abortController.abort(),this.abortController=null),await this.encoder.flush()}},ve=class extends K{constructor(e){super();this.connectedTrack=null;this.codec=e}},he=class extends ve{constructor(r){super(r),this.encoder=new ie({output:(e,t)=>this.connectedTrack?.output.muxer.addEncodedSubtitleChunk(this.connectedTrack,e,t),error:e=>console.error(e)}),this.encoder.configure({codec:r})}digest(r){this.ensureValidDigest(),this.encoder.encode(r)}};var q=class{},me=class extends q{#e=0;#r;#i=new ArrayBuffer(2**16);#t=new Uint8Array(this.#i);#s=0;constructor(r){super(),this.#r=r}#o(r){let e=this.#i.byteLength;for(;et.start-i.start);r.push({start:e[0].start,size:e[0].data.byteLength});for(let t=1;ta.start<=e&&ejt){for(let a=0;a=r.written[n+1].start;)r.written[n].end=Math.max(r.written[n].end,r.written[n+1].end),r.written.splice(n+1,1)}#n(r){let t={start:Math.floor(r/this.#i)*this.#i,data:new Uint8Array(this.#i),written:[],shouldFlush:!1};return this.#t.push(t),this.#t.sort((i,n)=>i.start-n.start),this.#t.indexOf(t)}#a(r=!1){for(let e=0;er.stream.write({type:"write",data:e,position:t}),chunkSize:r.options?.chunkSize}))}};var U=class{constructor(){this.output=null}},be=class extends U{constructor(){super(...arguments);this.buffer=null}createWriter(){return new me(this)}},_=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 fe(this)}},ke=class extends U{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 pe(this)}};return Ye(qt);})();
+`,o);u===-1&&(u=t.length);let m=re(e[2]),f=re(e[3])-m,g=t.slice(n,u).trim();t=t.slice(u).trimStart(),H.lastIndex=0;let v={timestamp:m/1e3,duration:f/1e3,text:g,identifier:s,settings:a,notes:r},C={};this.#t||(C.config={description:this.#i},this.#t=!0),this.#e.output(v,C)}}},ot=/(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})/,re=i=>{let t=ot.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])},ie=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]]),nt=i=>(O.setInt16(0,i,!1),[p[0],p[1]]),Ie=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]]),Be=i=>(O.setInt32(0,i,!1),[p[0],p[1],p[2],p[3]]),U=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]]),Ae=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]]),we=i=>(O.setInt32(0,2**30*i,!1),[p[0],p[1],p[2],p[3]]),S=(i,t=!1)=>{let e=Array(i.length).fill(null).map((r,s)=>i.charCodeAt(s));return t&&e.push(0),e},Oe=i=>{let t=null;for(let e of i)(!t||e.timestamp>t.timestamp)&&(t=e);return t},_e=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]},De=_e(0),Fe=i=>[A(i[0]),A(i[1]),we(i[2]),A(i[3]),A(i[4]),we(i[5]),A(i[6]),A(i[7]),we(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,[k(t),Ie(e),r??[]],s),We=i=>{let t=512;return i.fragmented?b("ftyp",[S("iso5"),l(t),S("iso5"),S("iso6"),S("mp41")]):b("ftyp",[S("isom"),l(t),S("isom"),i.holdsAvc?S("avc1"):[],S("mp41")])},oe=i=>({type:"mdat",largeSize:i}),Ne=i=>({type:"free",size:i}),K=(i,t,e=!1)=>b("moov",void 0,[at(t,i),...i.map(r=>ut(r,t)),e?Nt(i):null]),at=(i,t)=>{let e=w(Math.max(0,...t.filter(n=>n.samples.length>0).map(n=>{let a=Oe(n.samples);return a.timestamp+a.duration})),se),r=Math.max(...t.map(n=>n.track.id))+1,s=!V(i)||!V(e),o=s?U:l;return T("mvhd",+s,0,[o(i),o(i),l(se),o(e),A(1),Ae(1),Array(10).fill(0),Fe(De),Array(24).fill(0),l(r)])},ut=(i,t)=>b("trak",void 0,[lt(i,t),ct(i,t)]),lt=(i,t)=>{let e=Oe(i.samples),r=w(e?e.timestamp+e.duration:0,se),s=!V(t)||!V(r),o=s?U:l,n;if(i.type==="video"){let a=i.track.metadata.rotation;n=a===void 0||typeof a=="number"?_e(a??0):a}else n=De;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),Ae(i.type==="audio"?1:0),h(0),Fe(n),A(i.type==="video"?i.info.width:0),A(i.type==="video"?i.info.height:0)])},ct=(i,t)=>b("mdia",void 0,[dt(i,t),pt(i),ht(i)]),dt=(i,t)=>{let e=Oe(i.samples),r=w(e?e.timestamp+e.duration:0,i.timescale),s=!V(t)||!V(r),o=s?U:l;return T("mdhd",+s,0,[o(t),o(t),l(i.timescale),o(r),h(21956),h(0)])},mt={video:"vide",audio:"soun",subtitle:"text"},ft={video:"VideoHandler",audio:"SoundHandler",subtitle:"TextHandler"},pt=i=>T("hdlr",0,0,[S("mhlr"),S(mt[i.type]),l(0),l(0),l(0),S(ft[i.type],!0)]),ht=i=>b("minf",void 0,[kt[i.type](),Ct(),St(i)]),bt=()=>T("vmhd",0,1,[h(0),h(0),h(0),h(0)]),Tt=()=>T("smhd",0,0,[h(0),h(0)]),gt=()=>T("nmhd",0,0),kt={video:bt,audio:Tt,subtitle:gt},Ct=()=>b("dinf",void 0,[xt()]),xt=()=>T("dref",0,0,[l(1)],[yt()]),yt=()=>T("url ",0,1),St=i=>{let t=i.compositionTimeOffsetTable.length>1||i.compositionTimeOffsetTable.some(e=>e.sampleCompositionTimeOffset!==0);return b("stbl",void 0,[wt(i),It(i),Bt(i),_t(i),Dt(i),Ft(i),t?Wt(i):null])},wt=i=>{let t;return i.type==="video"?t=At(qt[i.track.source.codec],i):i.type==="audio"?t=Mt(Lt[i.track.source.codec],i):i.type==="subtitle"&&(t=Et(Jt[i.track.source.codec],i)),c(t),T("stsd",0,0,[l(1)],[t])},At=(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),nt(65535)],[Yt[t.track.source.codec](t)]),Ot=i=>i.info.decoderConfig&&b("avcC",[...z(i.info.decoderConfig.description)]),vt=i=>i.info.decoderConfig&&b("hvcC",[...z(i.info.decoderConfig.description)]),Pe=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,[k(r),k(s),k(a),k(2),k(2),k(2),h(0)])},Vt=()=>{let e=(1<<7)+1;return b("av1C",[e,0,0,0])},Mt=(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)],[Zt[t.track.source.codec](t)]),zt=i=>{let t=z(i.info.decoderConfig.description??new ArrayBuffer(0));return T("esds",0,0,[l(58753152),k(32+t.byteLength),h(1),k(0),l(75530368),k(18+t.byteLength),k(64),k(21),Ie(0),l(130071),l(130071),l(92307584),k(t.byteLength),...t,l(109084800),k(1),k(2)])},Ut=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",[k(0),k(i.info.numberOfChannels),h(t),l(i.info.sampleRate),Ae(e),k(0)])},Et=(i,t)=>b(i,[Array(6).fill(0),h(1)],[er[t.track.source.codec](t)]),Pt=i=>b("vttC",[...y.encode(i.info.config.description)]);var It=i=>T("stts",0,0,[l(i.timeToSampleTable.length),i.timeToSampleTable.map(t=>[l(t.sampleCount),l(t.sampleDelta)])]),Bt=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))])},_t=i=>T("stsc",0,0,[l(i.compactlyCodedChunkTable.length),i.compactlyCodedChunkTable.map(t=>[l(t.firstChunk),l(t.samplesPerChunk),l(1)])]),Dt=i=>T("stsz",0,0,[l(0),l(i.samples.length),i.samples.map(t=>l(t.size))]),Ft=i=>i.finalizedChunks.length>0&&B(i.finalizedChunks).offset>=2**32?T("co64",0,0,[l(i.finalizedChunks.length),i.finalizedChunks.map(t=>U(t.offset))]):T("stco",0,0,[l(i.finalizedChunks.length),i.finalizedChunks.map(t=>l(t.offset))]),Wt=i=>T("ctts",0,0,[l(i.compositionTimeOffsetTable.length),i.compositionTimeOffsetTable.map(t=>[l(t.sampleCount),l(t.sampleCompositionTimeOffset)])]),Nt=i=>b("mvex",void 0,i.map(Rt)),Rt=i=>T("trex",0,0,[l(i.track.id),l(1),l(0),l(0),l(0)]),ve=(i,t)=>b("moof",void 0,[Qt(i),...t.map(Ht)]),Qt=i=>T("mfhd",0,0,[l(i)]),Re=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},Ht=i=>b("traf",void 0,[$t(i),Kt(i),Gt(i)]),$t=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:Re(e)};return T("tfhd",0,t,[l(i.track.id),l(r.duration),l(r.size),l(r.flags)])},Kt=i=>(c(i.currentChunk),T("tfdt",1,0,[U(w(i.currentChunk.startTimestamp,i.timescale))])),Gt=i=>{c(i.currentChunk);let t=i.currentChunk.samples.map(x=>x.timescaleUnitsToNextSample),e=i.currentChunk.samples.map(x=>x.size),r=i.currentChunk.samples.map(Re),s=i.currentChunk.samples.map(x=>w(x.timestamp-x.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,g=!m&&a.size>1,v=u.size>1||[...u].some(x=>x!==0),C=0;return C|=1,C|=4*+m,C|=256*+d,C|=512*+f,C|=1024*+g,C|=2048*+v,T("trun",1,C,[l(i.currentChunk.samples.length),l(i.currentChunk.offset-i.currentChunk.moofOffset||0),m?l(r[0]):[],i.currentChunk.samples.map((x,I)=>[d?l(t[I]):[],f?l(e[I]):[],g?l(r[I]):[],v?Be(s[I]):[]])])},Qe=i=>b("mfra",void 0,[...i.map(jt),Xt()]),jt=(i,t)=>T("tfra",1,0,[l(i.track.id),l(63),l(i.finalizedChunks.length),i.finalizedChunks.map(r=>[U(w(r.startTimestamp,i.timescale)),U(r.moofOffset),l(t+1),l(1),l(1)])]),Xt=()=>T("mfro",0,0,[l(0)]),He=()=>b("vtte"),$e=(i,t,e,r,s)=>b("vttc",void 0,[s!==null?b("vsid",[Be(s)]):null,e!==null?b("iden",[...y.encode(e)]):null,t!==null?b("ctim",[...y.encode(ie(t))]):null,r!==null?b("sttg",[...y.encode(r)]):null,b("payl",[...y.encode(i)])]),Ke=i=>b("vtta",[...y.encode(i)]),qt={avc:"avc1",hevc:"hvc1",vp8:"vp08",vp9:"vp09",av1:"av01"},Yt={avc:Ot,hevc:vt,vp8:Pe,vp9:Pe,av1:Vt},Lt={aac:"mp4a",opus:"Opus"},Zt={aac:zt,opus:Ut},Jt={webvtt:"wvtt"},er={webvtt:Pt};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&&err){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 E=class{constructor(){this.output=null}},W=class extends E{constructor(){super(...arguments);this.buffer=null}createWriter(){return new ne(this)}},F=class extends E{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 j(this):new ae(this)}},le=class extends E{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 ue(this)}};var se=1e3,ir=2082844800,w=(i,t,e=!0)=>{let r=i*t;return e?Math.round(r):r},ce=class extends D{constructor(e,r){super(e);this.timestampsMustStartAtZero=!0;this.#r=new W;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)+ir;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(We({holdsAvc:e,fragmented:this.#t.options.fastStart==="fragmented"})),this.#c=this.#e.getPos(),this.#t.options.fastStart==="in-memory")this.#n=oe(!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=oe(!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}#g(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.#g(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.#k(o,r.timestamp))}#k(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;_.lastIndex=0;let g=_.test(f.text),v=f.timestamp+f.duration,C=e.cueToSourceId.get(f);if(C===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.#y())}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||B(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=ve(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.#k(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.#k(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.#y(!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=Qe(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(Ne(o))}else this.#i.writeBox(s)}}};var X=class{constructor(t){this.value=t}},N=class{constructor(t){this.value=t}},q=class{constructor(t){this.value=t}};var Ve=i=>i<256?1:i<65536?2:i<1<<24?3:i<2**32?4:i<2**40?5:6,Me=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,Ge=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 ze=2**15,je="https://github.com/Vanilagy/webm-muxer",Xe=6,qe=5,sr={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"},or={video:1,audio:2,subtitle:17},de=class extends D{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.#g=0;this.#e=e.writer,this.#i=r}#e;#i;#t;#r;#o;#a;#c;#n;#s;#d;#u;#l;#h;#T;#g;#w(e){this.#r.setUint8(0,e),this.#e.write(this.#t.subarray(0,1))}#k(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=Ve(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=Me(e)){e<0&&(e+=2**(r*8)),this.#f(e,r)}writeEBMLVarInt(e,r=Ge(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??Ve(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 X)this.writeEBMLVarInt(4),this.#k(e.data.value);else if(e.data instanceof N)this.writeEBMLVarInt(8),this.#b(e.data.value);else if(e.data instanceof q){let r=e.size??Me(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.#y(),this.#V(),this.#U(),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)}#y(){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:je},{id:22337,data:je},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:or[r.type]},{id:134,data:sr[r.track.source.codec]},...r.type==="video"?[r.info.decoderConfig.description?{id:25506,data:z(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!s.matrix||!s.transfer||!s.primaries||s.fullRange==null?null:{id:21936,data:[{id:21937,data:{rgb:1,bt709:1,bt470bg:5,smpte170m:6}[s.matrix]},{id:21946,data:{bt709:1,smpte170m:6,"iec61966-2-1":13}[s.transfer]},{id:21947,data:{bt709:1,bt470bg:5,smpte170m:6}[s.primaries]},{id:21945,data:[1,2][Number(s.fullRange)]}]}}return null})()]}]:[],...r.type==="audio"?[r.info.decoderConfig.description?{id:25506,data:z(r.info.decoderConfig.description)}:null,{id:225,data:[{id:181,data:new X(r.info.sampleRate)},{id:159,data:r.info.numberOfChannels}]}]:[],...r.type==="subtitle"?[{id:25506,data:y.encode(r.info.config.description)}]:[]]})}#z(){let e={id:408125543,size:this.#i.options.streaming?-1:Xe,data:[this.#i.options.streaming?null:this.#n,this.#c,this.#s]};this.#a=e,this.writeEBML(e)}#U(){this.#u={id:475249515,data:[]}}get#C(){return c(this.#a),this.dataOffsets.get(this.#a)}#E(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.#E(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.#B(o,u),o.chunkQueue.push(u),this.#S()}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.#S()}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);_.lastIndex=0,a=a.replace(_,g=>{let C=re(g.slice(1,-1))-u;return`<${ie(C)}>`});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.#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 m={rgb:7,bt709:2,bt470bg:1,smpte170m:3}[e.info.decoderConfig.colorSpace.matrix];Ee(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.#z());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 g=f.chunkQueue[0];return g&&g.type==="key"});(!this.#l||o&&s-this.#h>=1e3)&&this.#_(s);let n=s-this.#h;if(n<0)return;if(n>=ze)throw new Error(`Current Matroska cluster exceeded its maximum allowed length of ${ze} milliseconds. In order to produce a correct WebM file, you must pass in a key frame at least every ${ze} 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 q(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.#g=Math.max(this.#g,s+d),e.lastWrittenMsTimestamp=s,this.#T.add(e)}#_(e){this.#l&&!this.#i.options.streaming&&this.#v(),this.#l={id:524531317,size:this.#i.options.streaming?-1:qe,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,qe),this.#e.seek(r);let s=this.offsets.get(this.#l)-this.#C;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.#S()}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.#C;this.#e.seek(this.offsets.get(this.#a)+4),this.writeEBMLVarInt(r,Xe),this.#d.data=new N(this.#g),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.#C,this.#n.data[1].data[1].data=this.offsets.get(this.#c)-this.#C,this.#n.data[2].data[1].data=this.offsets.get(this.#s)-this.#C,this.#e.seek(this.offsets.get(this.#n)),this.writeEBML(this.#n),this.#e.seek(e)}}};var me=class{},fe=class extends me{constructor(e){super();this.options=e}createMuxer(e){return new ce(e,this)}},Y=class extends me{constructor(e={}){super();this.options=e}createMuxer(e){return new de(e,this)}},P=class extends Y{};var Ye=(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}'.`)},Le=(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 L=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)}},R=class extends L{constructor(e){super();this.connectedTrack=null;this.codec=e}},pe=class extends R{constructor(t){super(t)}digest(t,e){this.ensureValidDigest(),this.connectedTrack?.output.muxer.addEncodedVideoChunk(this.connectedTrack,t,e)}},nr=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/nr);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:Ye(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()}},he=class extends R{constructor(t){super(t.codec),this.encoder=new Z(this,t)}digest(t){this.encoder.digest(t)}flush(){return this.encoder.flush()}},be=class extends R{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()}},Te=class extends R{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 L{constructor(e){super();this.connectedTrack=null;this.codec=e}},ge=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:Le(this.codecConfig.codec,t.numberOfChannels,t.sampleRate),numberOfChannels:t.numberOfChannels,sampleRate:t.sampleRate,bitrate:this.codecConfig.bitrate}))}async flush(){return this.encoder?.flush()}},ke=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()}},Ce=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()}},Ue=class extends L{constructor(e){super();this.connectedTrack=null;this.codec=e}},ye=class extends Ue{constructor(t){super(t),this.parser=new te({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 it(ar);})();
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 b10b951..df1f704 100644
--- a/dist/metamuxer.min.mjs
+++ b/dist/metamuxer.min.mjs
@@ -1,9 +1,9 @@
-var ne=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 i={id:this.tracks.length+1,output:this,type:r,source:e,metadata:t};this.muxer.beforeTrackAdd(i),this.tracks.push(i),e.connectedTrack=i}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 d(s){if(!s)throw new Error("Assertion failed.")}var U=s=>s&&s[s.length-1],O=s=>s>=0&&s<2**32,v=(s,r,e)=>{let t=0;for(let i=r;i>l;t<<=1,t|=a}return t},ve=(s,r,e,t)=>{for(let i=r;i>e-i-1<s instanceof ArrayBuffer?new Uint8Array(s):new Uint8Array(s.buffer,s.byteOffset,s.byteLength);var h=new Uint8Array(8),w=new DataView(h.buffer),k=s=>[(s%256+256)%256],m=s=>(w.setUint16(0,s,!1),[h[0],h[1]]),Ke=s=>(w.setInt16(0,s,!1),[h[0],h[1]]),Ee=s=>(w.setUint32(0,s,!1),[h[1],h[2],h[3]]),u=s=>(w.setUint32(0,s,!1),[h[0],h[1],h[2],h[3]]),Ge=s=>(w.setInt32(0,s,!1),[h[0],h[1],h[2],h[3]]),V=s=>(w.setUint32(0,Math.floor(s/2**32),!1),w.setUint32(4,s,!1),[h[0],h[1],h[2],h[3],h[4],h[5],h[6],h[7]]),ae=s=>(w.setInt16(0,2**8*s,!1),[h[0],h[1]]),S=s=>(w.setInt32(0,2**16*s,!1),[h[0],h[1],h[2],h[3]]),oe=s=>(w.setInt32(0,2**30*s,!1),[h[0],h[1],h[2],h[3]]),x=(s,r=!1)=>{let e=Array(s.length).fill(null).map((t,i)=>s.charCodeAt(i));return r&&e.push(0),e},ue=s=>{let r=null;for(let e of s)(!r||e.timestamp>r.timestamp)&&(r=e);return r},Ue=s=>{let r=s*(Math.PI/180),e=Math.cos(r),t=Math.sin(r);return[e,t,0,-t,e,0,0,0,1]},ze=Ue(0),Me=s=>[S(s[0]),S(s[1]),oe(s[2]),S(s[3]),S(s[4]),oe(s[5]),S(s[6]),S(s[7]),oe(s[8])],p=(s,r,e)=>({type:s,contents:r&&new Uint8Array(r.flat(10)),children:e}),f=(s,r,e,t,i)=>p(s,[k(r),Ee(e),t??[]],i),Pe=s=>{let r=512;return s.fragmented?p("ftyp",[x("iso5"),u(r),x("iso5"),x("iso6"),x("mp41")]):p("ftyp",[x("isom"),u(r),x("isom"),s.holdsAvc?x("avc1"):[],x("mp41")])},L=s=>({type:"mdat",largeSize:s}),Fe=s=>({type:"free",size:s}),W=(s,r,e=!1)=>p("moov",void 0,[je(r,s),...s.map(t=>qe(t,r)),e?Ct(s):null]),je=(s,r)=>{let e=y(Math.max(0,...r.filter(o=>o.samples.length>0).map(o=>{let l=ue(o.samples);return l.timestamp+l.duration})),X),t=Math.max(...r.map(o=>o.track.id))+1,i=!O(s)||!O(e),n=i?V:u;return f("mvhd",+i,0,[n(s),n(s),u(X),n(e),S(1),ae(1),Array(10).fill(0),Me(ze),Array(24).fill(0),u(t)])},qe=(s,r)=>p("trak",void 0,[Xe(s,r),Le(s,r)]),Xe=(s,r)=>{let e=ue(s.samples),t=y(e?e.timestamp+e.duration:0,X),i=!O(r)||!O(t),n=i?V:u,o;if(s.type==="video"){let l=s.track.metadata.rotation;o=l===void 0||typeof l=="number"?Ue(l??0):l}else o=ze;return f("tkhd",+i,3,[n(r),n(r),u(s.track.id),u(0),n(t),Array(8).fill(0),m(0),m(0),ae(s.type==="audio"?1:0),m(0),Me(o),S(s.type==="video"?s.info.width:0),S(s.type==="video"?s.info.height:0)])},Le=(s,r)=>p("mdia",void 0,[Ye(s,r),Ze(s.type==="video"?"vide":"soun"),Je(s)]),Ye=(s,r)=>{let e=ue(s.samples),t=y(e?e.timestamp+e.duration:0,s.timescale),i=!O(r)||!O(t),n=i?V:u;return f("mdhd",+i,0,[n(r),n(r),u(s.timescale),n(t),m(21956),m(0)])},Ze=s=>f("hdlr",0,0,[x("mhlr"),x(s),u(0),u(0),u(0),x("mp4-muxer-hdlr",!0)]),Je=s=>p("minf",void 0,[s.type==="video"?et():tt(),rt(),nt(s)]),et=()=>f("vmhd",0,1,[m(0),m(0),m(0),m(0)]),tt=()=>f("smhd",0,0,[m(0),m(0)]),rt=()=>p("dinf",void 0,[it()]),it=()=>f("dref",0,0,[u(1)],[st()]),st=()=>f("url ",0,1),nt=s=>{let r=s.compositionTimeOffsetTable.length>1||s.compositionTimeOffsetTable.some(e=>e.sampleCompositionTimeOffset!==0);return p("stbl",void 0,[ot(s),ft(s),pt(s),bt(s),kt(s),gt(s),r?Tt(s):null])},ot=s=>f("stsd",0,0,[u(1)],[s.type==="video"?at(Et[s.track.source.codec],s):ct(zt[s.track.source.codec],s)]),at=(s,r)=>p(s,[Array(6).fill(0),m(1),m(0),m(0),Array(12).fill(0),m(r.info.width),m(r.info.height),u(4718592),u(4718592),u(0),m(1),Array(32).fill(0),m(24),Ke(65535)],[Ut[r.track.source.codec](r)]),ut=s=>s.info.decoderConfig&&p("avcC",[...z(s.info.decoderConfig.description)]),lt=s=>s.info.decoderConfig&&p("hvcC",[...z(s.info.decoderConfig.description)]),Ve=s=>{if(!s.info.decoderConfig)return null;let r=s.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]),i=Number(e[2]),l=(Number(e[3])<<4)+(0<<1)+Number(r.colorSpace.fullRange);return f("vpcC",1,0,[k(t),k(i),k(l),k(2),k(2),k(2),m(0)])},dt=()=>{let e=(1<<7)+1;return p("av1C",[e,0,0,0])},ct=(s,r)=>p(s,[Array(6).fill(0),m(1),m(0),m(0),u(0),m(r.info.numberOfChannels),m(16),m(0),m(0),S(r.info.sampleRate)],[Mt[r.track.source.codec](r)]),ht=s=>{let r=z(s.info.decoderConfig.description??new ArrayBuffer(0));return f("esds",0,0,[u(58753152),k(32+r.byteLength),m(1),k(0),u(75530368),k(18+r.byteLength),k(64),k(21),Ee(0),u(130071),u(130071),u(92307584),k(r.byteLength),...r,u(109084800),k(1),k(2)])},mt=s=>{let r=3840,e=0,t=s.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 i=ArrayBuffer.isView(t)?new DataView(t.buffer,t.byteOffset,t.byteLength):new DataView(t);r=i.getUint16(10,!0),e=i.getInt16(14,!0)}return p("dOps",[k(0),k(s.info.numberOfChannels),m(r),u(s.info.sampleRate),ae(e),k(0)])},ft=s=>f("stts",0,0,[u(s.timeToSampleTable.length),s.timeToSampleTable.map(r=>[u(r.sampleCount),u(r.sampleDelta)])]),pt=s=>{if(s.samples.every(e=>e.type==="key"))return null;let r=[...s.samples.entries()].filter(([,e])=>e.type==="key");return f("stss",0,0,[u(r.length),r.map(([e])=>u(e+1))])},bt=s=>f("stsc",0,0,[u(s.compactlyCodedChunkTable.length),s.compactlyCodedChunkTable.map(r=>[u(r.firstChunk),u(r.samplesPerChunk),u(1)])]),kt=s=>f("stsz",0,0,[u(0),u(s.samples.length),s.samples.map(r=>u(r.size))]),gt=s=>s.finalizedChunks.length>0&&U(s.finalizedChunks).offset>=2**32?f("co64",0,0,[u(s.finalizedChunks.length),s.finalizedChunks.map(r=>V(r.offset))]):f("stco",0,0,[u(s.finalizedChunks.length),s.finalizedChunks.map(r=>u(r.offset))]),Tt=s=>f("ctts",0,0,[u(s.compositionTimeOffsetTable.length),s.compositionTimeOffsetTable.map(r=>[u(r.sampleCount),u(r.sampleCompositionTimeOffset)])]),Ct=s=>p("mvex",void 0,s.map(xt)),xt=s=>f("trex",0,0,[u(s.track.id),u(1),u(0),u(0),u(0)]),le=(s,r)=>p("moof",void 0,[yt(s),...r.map(St)]),yt=s=>f("mfhd",0,0,[u(s)]),De=s=>{let r=0,e=0,t=0,i=0,n=s.type==="delta";return e|=+n,n?r|=1:r|=2,r<<24|e<<16|t<<8|i},St=s=>p("traf",void 0,[wt(s),At(s),Ot(s)]),wt=s=>{d(s.currentChunk);let r=0;r|=8,r|=16,r|=32,r|=131072;let e=s.currentChunk.samples[1]??s.currentChunk.samples[0],t={duration:e.timescaleUnitsToNextSample,size:e.size,flags:De(e)};return f("tfhd",0,r,[u(s.track.id),u(t.duration),u(t.size),u(t.flags)])},At=s=>(d(s.currentChunk),f("tfdt",1,0,[V(y(s.currentChunk.startTimestamp,s.timescale))])),Ot=s=>{d(s.currentChunk);let r=s.currentChunk.samples.map(T=>T.timescaleUnitsToNextSample),e=s.currentChunk.samples.map(T=>T.size),t=s.currentChunk.samples.map(De),i=s.currentChunk.samples.map(T=>y(T.timestamp-T.decodeTimestamp,s.timescale)),n=new Set(r),o=new Set(e),l=new Set(t),a=new Set(i),c=l.size===2&&t[0]!==t[1],b=n.size>1,g=o.size>1,C=!c&&l.size>1,_=a.size>1||[...a].some(T=>T!==0),A=0;return A|=1,A|=4*+c,A|=256*+b,A|=512*+g,A|=1024*+C,A|=2048*+_,f("trun",1,A,[u(s.currentChunk.samples.length),u(s.currentChunk.offset-s.currentChunk.moofOffset||0),c?u(t[0]):[],s.currentChunk.samples.map((T,E)=>[b?u(r[E]):[],g?u(e[E]):[],C?u(t[E]):[],_?Ge(i[E]):[]])])},Be=s=>p("mfra",void 0,[...s.map(vt),Vt()]),vt=(s,r)=>f("tfra",1,0,[u(s.track.id),u(63),u(s.finalizedChunks.length),s.finalizedChunks.map(t=>[V(y(t.startTimestamp,s.timescale)),V(t.moofOffset),u(r+1),u(1),u(1)])]),Vt=()=>f("mfro",0,0,[u(0)]),Et={avc:"avc1",hevc:"hvc1",vp8:"vp08",vp9:"vp09",av1:"av01"},Ut={avc:ut,hevc:lt,vp8:Ve,vp9:Ve,av1:dt},zt={aac:"mp4a",opus:"Opus"},Mt={aac:ht,opus:mt};var M=class{constructor(r){this.output=r}beforeTrackAdd(r){}onTrackClose(r){}};var X=1e3,Pt=2082844800,y=(s,r,e=!0)=>{let t=s*r;return e?Math.round(t):t},Y=class extends M{constructor(e,t){super(e);this.#i=new Uint8Array(8);this.#t=new DataView(this.#i.buffer);this.offsets=new WeakMap;this.#s=null;this.#o=null;this.#n=[];this.#a=Math.floor(Date.now()/1e3)+Pt;this.#l=[];this.#h=1;this.#e=e.writer,this.#r=t}#e;#r;#i;#t;#s;#o;#n;#a;#l;#h;writeU32(e){this.#t.setUint32(0,e,!1),this.#e.write(this.#i.subarray(0,4))}writeU64(e){this.#t.setUint32(0,Math.floor(e/2**32),!1),this.#t.setUint32(4,e,!1),this.#e.write(this.#i.subarray(0,8))}writeAscii(e){for(let t=0;tt.type==="video"&&t.source.codec==="avc");if(this.writeBox(Pe({holdsAvc:e,fragmented:this.#r.options.fastStart==="fragmented"})),this.#s=this.#e.getPos(),this.#r.options.fastStart==="in-memory")this.#o=L(!1);else if(this.#r.options.fastStart!=="fragmented"){if(typeof this.#r.options.fastStart=="object"){let t=this.#d();this.#e.seek(this.#e.getPos()+t)}this.#o=L(!0),this.writeBox(this.#o)}this.#e.flush()}#d(){d(typeof this.#r.options.fastStart=="object");let e=0,t=[this.#r.options.fastStart.expectedVideoChunks,this.#r.options.fastStart.expectedAudioChunks];for(let i of t)i&&(e+=8*Math.ceil(2/3*i),e+=4*i,e+=12*Math.ceil(2/3*i),e+=4*i,e+=8*i);return e+=4096,e}#u(e,t){let i=this.#n.find(o=>o.track===e);if(i)return i;d(t),d(t.decoderConfig),d(t.decoderConfig.codedWidth!==void 0),d(t.decoderConfig.codedHeight!==void 0);let n={track:e,type:"video",info:{width:t.decoderConfig.codedWidth,height:t.decoderConfig.codedHeight,decoderConfig:t.decoderConfig},timescale:e.metadata.frameRate??57600,samples:[],sampleQueue:[],timestampProcessingQueue:[],firstTimestamp:null,lastKeyFrameTimestamp:null,timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.#n.push(n),this.#n.sort((o,l)=>o.track.id-l.track.id),n}#m(e,t){let i=this.#n.find(o=>o.track===e);if(i)return i;d(t),d(t.decoderConfig);let n={track:e,type:"audio",info:{numberOfChannels:t.decoderConfig.numberOfChannels,sampleRate:t.decoderConfig.sampleRate,decoderConfig:t.decoderConfig},timescale:t.decoderConfig.sampleRate,samples:[],sampleQueue:[],timestampProcessingQueue:[],firstTimestamp:null,lastKeyFrameTimestamp:null,timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.#n.push(n),this.#n.sort((o,l)=>o.track.id-l.track.id),n}addEncodedVideoChunk(e,t,i){let n=this.#u(e,i);if(typeof this.#r.options.fastStart=="object"&&n.samples.length===this.#r.options.fastStart.expectedVideoChunks)throw new Error(`Cannot add more video chunks than specified in 'fastStart' (${this.#r.options.fastStart.expectedVideoChunks}).`);let o=this.#f(n,t);this.#r.options.fastStart==="fragmented"?(n.sampleQueue.push(o),this.#b()):this.#p(n,o)}addEncodedAudioChunk(e,t,i){let n=this.#m(e,i);if(typeof this.#r.options.fastStart=="object"&&n.samples.length===this.#r.options.fastStart.expectedAudioChunks)throw new Error(`Cannot add more audio chunks than specified in 'fastStart' (${this.#r.options.fastStart.expectedAudioChunks}).`);let o=this.#f(n,t);this.#r.options.fastStart==="fragmented"?(n.sampleQueue.push(o),this.#b()):this.#p(n,o)}#f(e,t){let i=this.#x(e,t),n=(t.duration??0)/1e6,o=new Uint8Array(t.byteLength);return t.copyTo(o),{timestamp:i,decodeTimestamp:i,duration:n,data:o,size:o.byteLength,type:t.type,timescaleUnitsToNextSample:y(n,e.timescale)}}#c(e){if(e.timestampProcessingQueue.length===0)return;let t=e.timestampProcessingQueue.map(i=>i.timestamp).sort((i,n)=>i-n);for(let i=0;i{if(e===l)return t.type==="key";let a=l.sampleQueue[0];return a&&a.type==="key"});n>=1&&o&&(i=!0,this.#T())}else i=n>=.5}i&&(e.currentChunk&&this.#g(e),e.currentChunk={startTimestamp:t.timestamp,samples:[],offset:null,moofOffset:null}),d(e.currentChunk),e.currentChunk.samples.push(t),e.timestampProcessingQueue.push(t)}#x(e,t){let i=t.timestamp/1e6;if(i<0)throw new Error(`Timestamps must be non-negative (got ${i}s).`);if(e.firstTimestamp===null&&(e.firstTimestamp=i),i-=e.firstTimestamp,e.lastKeyFrameTimestamp!==null&&i=2**32&&(a.largeSize=!0,b=this.measureBox(a)+c),a.size=b,this.writeBox(a)}for(let a of this.#n){a.currentChunk.offset=this.#e.getPos(),a.currentChunk.moofOffset=i;for(let c of a.currentChunk.samples)this.#e.write(c.data),c.data=null}let o=this.#e.getPos();this.#e.seek(this.offsets.get(n));let l=le(t,this.#n);this.writeBox(l),this.#e.seek(o);for(let a of this.#n)a.finalizedChunks.push(a.currentChunk),this.#l.push(a.currentChunk),a.currentChunk=null;e&&this.#e.flush()}finalize(){if(this.#r.options.fastStart==="fragmented"){for(let e of this.#n){for(let t of e.sampleQueue)this.#p(e,t);this.#c(e)}this.#T(!1)}else for(let e of this.#n)this.#c(e),this.#g(e);if(this.#r.options.fastStart==="in-memory"){d(this.#o);let e;for(let i=0;i<2;i++){let n=W(this.#n,this.#a),o=this.measureBox(n);e=this.measureBox(this.#o);let l=this.#e.getPos()+o+e;for(let a of this.#l){a.offset=l;for(let{data:c}of a.samples)d(c),l+=c.byteLength,e+=c.byteLength}if(l<2**32)break;e>=2**32&&(this.#o.largeSize=!0)}let t=W(this.#n,this.#a);this.writeBox(t),this.#o.size=e,this.writeBox(this.#o);for(let i of this.#l)for(let n of i.samples)d(n.data),this.#e.write(n.data),n.data=null}else if(this.#r.options.fastStart==="fragmented"){let e=this.#e.getPos(),t=Be(this.#n);this.writeBox(t);let i=this.#e.getPos()-e;this.#e.seek(this.#e.getPos()-4),this.writeU32(i)}else{d(this.#o),d(this.#s!==null);let e=this.offsets.get(this.#o);d(e!==void 0);let t=this.#e.getPos()-e;this.#o.size=t,this.#o.largeSize=t>=2**32,this.patchBox(this.#o);let i=W(this.#n,this.#a);if(typeof this.#r.options.fastStart=="object"){this.#e.seek(this.#s),this.writeBox(i);let n=e-this.#e.getPos();this.writeBox(Fe(n))}else this.writeBox(i)}}};var N=class{constructor(r){this.value=r}},P=class{constructor(r){this.value=r}},R=class{constructor(r){this.value=r}};var de=s=>s<256?1:s<65536?2:s<1<<24?3:s<2**32?4:s<2**40?5:6,ce=s=>s>=-64&&s<64?1:s>=-8192&&s<8192?2:s>=-(1<<20)&&s<1<<20?3:s>=-(1<<27)&&s<1<<27?4:s>=-(2**34)&&s<2**34?5:6,Ie=s=>{if(s<127)return 1;if(s<16383)return 2;if(s<(1<<21)-1)return 3;if(s<(1<<28)-1)return 4;if(s<2**35-1)return 5;if(s<2**42-1)return 6;throw new Error("EBML VINT size not supported "+s)};var he=2**15,_e="https://github.com/Vanilagy/webm-muxer",We=6,Ne=5,Ft={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",vorbis:"A_VORBIS",webvtt:"S_TEXT/WEBVTT"},Dt={video:1,audio:2,subtitle:17},Z=class extends M{constructor(e,t){super(e);this.#i=new Uint8Array(8);this.#t=new DataView(this.#i.buffer);this.offsets=new WeakMap;this.dataOffsets=new WeakMap;this.#s=[];this.#o=null;this.#n=null;this.#a=null;this.#l=null;this.#h=null;this.#d=null;this.#u=null;this.#m=null;this.#f=new Set;this.#c=0;this.#e=e.writer,this.#r=t}#e;#r;#i;#t;#s;#o;#n;#a;#l;#h;#d;#u;#m;#f;#c;#p(e){this.#t.setUint8(0,e),this.#e.write(this.#i.subarray(0,1))}#x(e){this.#t.setFloat32(0,e,!1),this.#e.write(this.#i.subarray(0,4))}#g(e){this.#t.setFloat64(0,e,!1),this.#e.write(this.#i)}#b(e,t=de(e)){let i=0;switch(t){case 6:this.#t.setUint8(i++,e/2**40|0);case 5:this.#t.setUint8(i++,e/2**32|0);case 4:this.#t.setUint8(i++,e>>24);case 3:this.#t.setUint8(i++,e>>16);case 2:this.#t.setUint8(i++,e>>8);case 1:this.#t.setUint8(i++,e);break;default:throw new Error("Bad UINT size "+t)}this.#e.write(this.#i.subarray(0,i))}#T(e,t=ce(e)){e<0&&(e+=2**(t*8)),this.#b(e,t)}writeEBMLVarInt(e,t=Ie(e)){let i=0;switch(t){case 1:this.#t.setUint8(i++,128|e);break;case 2:this.#t.setUint8(i++,64|e>>8),this.#t.setUint8(i++,e);break;case 3:this.#t.setUint8(i++,32|e>>16),this.#t.setUint8(i++,e>>8),this.#t.setUint8(i++,e);break;case 4:this.#t.setUint8(i++,16|e>>24),this.#t.setUint8(i++,e>>16),this.#t.setUint8(i++,e>>8),this.#t.setUint8(i++,e);break;case 5:this.#t.setUint8(i++,8|e/2**32&7),this.#t.setUint8(i++,e>>24),this.#t.setUint8(i++,e>>16),this.#t.setUint8(i++,e>>8),this.#t.setUint8(i++,e);break;case 6:this.#t.setUint8(i++,4|e/2**40&3),this.#t.setUint8(i++,e/2**32|0),this.#t.setUint8(i++,e>>24),this.#t.setUint8(i++,e>>16),this.#t.setUint8(i++,e>>8),this.#t.setUint8(i++,e);break;default:throw new Error("Bad EBML VINT size "+t)}this.#e.write(this.#i.subarray(0,i))}#A(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.#b(e.id),Array.isArray(e.data)){let t=this.#e.getPos(),i=e.size===-1?1:e.size??4;e.size===-1?this.#p(255):this.#e.seek(this.#e.getPos()+i);let n=this.#e.getPos();if(this.dataOffsets.set(e,n),this.writeEBML(e.data),e.size!==-1){let o=this.#e.getPos()-n,l=this.#e.getPos();this.#e.seek(t),this.writeEBMLVarInt(o,i),this.#e.seek(l)}}else if(typeof e.data=="number"){let t=e.size??de(e.data);this.writeEBMLVarInt(t),this.#b(e.data,t)}else if(typeof e.data=="string")this.writeEBMLVarInt(e.data.length),this.#A(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 N)this.writeEBMLVarInt(4),this.#x(e.data.value);else if(e.data instanceof P)this.writeEBMLVarInt(8),this.#g(e.data.value);else if(e.data instanceof R){let t=e.size??ce(e.data.value);this.writeEBMLVarInt(t),this.#T(e.data.value,t)}}}beforeTrackAdd(e){if(this.#r 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.#O(),this.#r.options.streaming||this.#v(),this.#V(),this.#z(),this.#e.flush()}#O(){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 F?"webm":"matroska"},{id:17031,data:2},{id:17029,data:2}]};this.writeEBML(e)}#v(){let e=new Uint8Array([28,83,187,107]),t=new Uint8Array([21,73,169,102]),i=new Uint8Array([22,84,174,107]),n={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:i},{id:21420,size:5,data:0}]}]};this.#a=n}#V(){let e={id:17545,data:new P(0)};this.#h=e;let t={id:357149030,data:[{id:2807729,data:1e6},{id:19840,data:_e},{id:22337,data:_e},this.#r.options.streaming?null:e]};this.#n=t}#E(){let e={id:374648427,data:[]};this.#l=e;for(let t of this.#s)e.data.push({id:174,data:[{id:215,data:t.track.id},{id:29637,data:t.track.id},{id:131,data:Dt[t.type]},{id:134,data:Ft[t.track.source.codec]},t.info.decoderConfig.description?{id:25506,data:z(t.info.decoderConfig.description)}:null,...t.type==="video"?[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 i=t.info.decoderConfig.colorSpace;return!i.matrix||!i.transfer||!i.primaries||i.fullRange==null?null:{id:21936,data:[{id:21937,data:{rgb:1,bt709:1,bt470bg:5,smpte170m:6}[i.matrix]},{id:21946,data:{bt709:1,smpte170m:6,"iec61966-2-1":13}[i.transfer]},{id:21947,data:{bt709:1,bt470bg:5,smpte170m:6}[i.primaries]},{id:21945,data:[1,2][Number(i.fullRange)]}]}}return null})()]}]:[],...t.type==="audio"?[{id:225,data:[{id:181,data:new N(t.info.sampleRate)},{id:159,data:t.info.numberOfChannels}]}]:[]]})}#U(){let e={id:408125543,size:this.#r.options.streaming?-1:We,data:[this.#r.options.streaming?null:this.#a,this.#n,this.#l]};this.#o=e,this.writeEBML(e)}#z(){this.#d={id:475249515,data:[]}}get#k(){return d(this.#o),this.dataOffsets.get(this.#o)}#M(e,t){let i=this.#s.find(o=>o.track===e);if(i)return i;d(t),d(t.decoderConfig),d(t.decoderConfig.codedWidth!==void 0),d(t.decoderConfig.codedHeight!==void 0);let n={track:e,type:"video",info:{width:t.decoderConfig.codedWidth,height:t.decoderConfig.codedHeight,decoderConfig:t.decoderConfig},chunkQueue:[],firstTimestamp:null,lastKeyFrameTimestamp:null,lastWrittenMsTimestamp:null};return this.#s.push(n),this.#s.sort((o,l)=>o.track.id-l.track.id),n}#P(e,t){let i=this.#s.find(o=>o.track===e);if(i)return i;d(t),d(t.decoderConfig);let n={track:e,type:"audio",info:{numberOfChannels:t.decoderConfig.numberOfChannels,sampleRate:t.decoderConfig.sampleRate,decoderConfig:t.decoderConfig},chunkQueue:[],firstTimestamp:null,lastKeyFrameTimestamp:null,lastWrittenMsTimestamp:null};return this.#s.push(n),this.#s.sort((o,l)=>o.track.id-l.track.id),n}#F(e,t){let i=this.#s.find(o=>o.track===e);if(i)return i;d(t),d(t.decoderConfig);let n={track:e,type:"subtitle",info:{decoderConfig:t.decoderConfig},chunkQueue:[],firstTimestamp:null,lastKeyFrameTimestamp:null,lastWrittenMsTimestamp:null};return this.#s.push(n),this.#s.sort((o,l)=>o.track.id-l.track.id),n}addEncodedVideoChunk(e,t,i){let n=this.#M(e,i),o=new Uint8Array(t.byteLength);t.copyTo(o);let l=this.#y(n,o,t.timestamp,t.duration??0,t.type);e.source.codec==="vp9"&&this.#D(n,l),n.chunkQueue.push(l),this.#C()}addEncodedAudioChunk(e,t,i){let n=this.#P(e,i),o=new Uint8Array(t.byteLength);t.copyTo(o);let l=this.#y(n,o,t.timestamp,t.duration??0,t.type);n.chunkQueue.push(l),this.#C()}addEncodedSubtitleChunk(e,t,i){let n=this.#F(e,i),o=this.#y(n,t.body,t.timestamp,t.duration,"key",t.additions);n.chunkQueue.push(o),this.#C()}#C(){let e=0;for(let t of this.#s)t.track.source.closed||e++;if(!(this.#s.length0&&o.chunkQueue[0].timestamp=2&&i++;let c={rgb:7,bt709:2,bt470bg:1,smpte170m:3}[e.info.decoderConfig.colorSpace.matrix];ve(t.data,i+0,i+3,c)}#y(e,t,i,n,o,l=null){let a=this.#B(e,i,o==="key");return{data:t,type:o,timestamp:a,duration:n/1e6,additions:l}}#B(e,t,i){let n=t/1e6;if(n<0)throw new Error(`Timestamps must be non-negative (got ${n}s).`);if(e.firstTimestamp===null&&(e.firstTimestamp=n),n-=e.firstTimestamp,e.lastKeyFrameTimestamp!==null&&n{if(e===g)return t.type==="key";let C=g.chunkQueue[0];return C&&C.type==="key"});(!this.#u||n&&i-this.#m>=1e3)&&this.#I(i);let o=i-this.#m;if(o<0)return;if(o>=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 a=new Uint8Array(4),c=new DataView(a.buffer);c.setUint8(0,128|e.track.id),c.setInt16(1,o,!1);let b=Math.floor(1e3*t.duration);if(b===0&&!t.additions){c.setUint8(3,+(t.type==="key")<<7);let g={id:163,data:[a,t.data]};this.writeEBML(g)}else{let g={id:160,data:[{id:161,data:[a,t.data]},t.type==="delta"?{id:251,data:new R(e.lastWrittenMsTimestamp-i)}:null,b>0?{id:155,data:b}:null,t.additions?{id:30113,data:t.additions}:null]};this.writeEBML(g)}this.#c=Math.max(this.#c,i+b),e.lastWrittenMsTimestamp=i,this.#f.add(e)}#I(e){this.#u&&!this.#r.options.streaming&&this.#w(),this.#u={id:524531317,size:this.#r.options.streaming?-1:Ne,data:[{id:231,data:e}]},this.writeEBML(this.#u),this.#m=e,this.#f.clear()}#w(){d(this.#u);let e=this.#e.getPos()-this.dataOffsets.get(this.#u),t=this.#e.getPos();this.#e.seek(this.offsets.get(this.#u)+4),this.writeEBMLVarInt(e,Ne),this.#e.seek(t);let i=this.offsets.get(this.#u)-this.#k;d(this.#d),this.#d.data.push({id:187,data:[{id:179,data:this.#m},...[...this.#f].map(n=>({id:183,data:[{id:247,data:n.track.id},{id:241,data:i}]}))]})}onTrackClose(){this.#C()}finalize(){for(let e of this.#s)for(;e.chunkQueue.length>0;)this.#S(e,e.chunkQueue.shift());if(this.#r.options.streaming||this.#w(),d(this.#d),this.writeEBML(this.#d),!this.#r.options.streaming){let e=this.#e.getPos(),t=this.#e.getPos()-this.#k;this.#e.seek(this.offsets.get(this.#o)+4),this.writeEBMLVarInt(t,We),this.#h.data=new P(this.#c),this.#e.seek(this.offsets.get(this.#h)),this.writeEBML(this.#h),this.#a.data[0].data[1].data=this.offsets.get(this.#d)-this.#k,this.#a.data[1].data[1].data=this.offsets.get(this.#n)-this.#k,this.#a.data[2].data[1].data=this.offsets.get(this.#l)-this.#k,this.#e.seek(this.offsets.get(this.#a)),this.writeEBML(this.#a),this.#e.seek(e)}}};var J=class{},me=class extends J{constructor(e){super();this.options=e}createMuxer(e){return new Y(e,this)}},ee=class extends J{constructor(e={}){super();this.options=e}createMuxer(e){return new Z(e,this)}},F=class extends ee{};var Re=(s,r,e)=>{if(s==="avc"){let t=100;r<=768&&e<=432?t=66:r<=1920&&e<=1080&&(t=77);let i=0,n=r>1920||e>1080?50:41,o=t.toString(16).padStart(2,"0"),l=i.toString(16).padStart(2,"0"),a=n.toString(16).padStart(2,"0");return`avc1.${o}${l}${a}`}else if(s==="hevc"){let t=0,i=1,n=Array(32).fill(0);n[i]=1;let o=parseInt(n.reverse().join(""),2).toString(16).replace(/^0+/,""),l="L",a=120;return r<=1280&&e<=720?a=93:r<=1920&&e<=1080?a=120:r<=3840&&e<=2160?a=150:(l="H",a=180),`hev1.${t===0?"":String.fromCharCode(65+t-1)}${i}.${o}.${l}${a}.B0`}else{if(s==="vp8")return"vp8";if(s==="vp9"){let t="00",i;return r<=854&&e<=480?i="21":r<=1280&&e<=720?i="31":r<=1920&&e<=1080?i="41":r<=3840&&e<=2160?i="51":i="61",`vp09.${t}.${i}.08`}else if(s==="av1"){let i;return r<=854&&e<=480?i="01":r<=1280&&e<=720?i="03":r<=1920&&e<=1080?i="04":r<=3840&&e<=2160?i="07":i="09",`av01.0.${i}M.08`}}throw new Error(`Unhandled codec '${s}'.`)},Qe=(s,r,e)=>{if(s==="aac")return r>=2&&e<=24e3?"mp4a.40.29":e<=24e3?"mp4a.40.5":"mp4a.40.2";if(s==="opus")return"opus";if(s==="vorbis")return"vorbis";throw new Error(`Unhandled codec '${s}'.`)};var Q=/(?:(.+?)\n)?((?:\d{2}:)?\d{2}:\d{2}.\d{3})\s+-->\s+((?:\d{2}:)?\d{2}:\d{2}.\d{3})/g,Bt=/^WEBVTT.*?\n{2}/,It=/(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})/,He=/<(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})>/g,fe=new TextEncoder,te=class{#e;#r=null;#i=null;#t=!1;constructor(r){this.#e=r}configure(r){if(r.codec!=="webvtt")throw new Error("Codec must be 'webvtt'.");this.#r=r}encode(r){if(!this.#r)throw new Error("Encoder not configured.");r=r.replace(`\r
+var de=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},Ue=(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;var R=/(?:(.+?)\n)?((?:\d{2}:)?\d{2}:\d{2}.\d{3})\s+-->\s+((?:\d{2}:)?\d{2}:\d{2}.\d{3})/g,Le=/^WEBVTT(.|\n)*?\n{2}/,I=/<(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})>/g,J=class{#e;#i=null;#t=!1;constructor(r){this.#e=r}parse(r){r=r.replaceAll(`\r
`,`
-`).replace("\r",`
-`),Q.lastIndex=0;let e;if(!this.#i){if(!Bt.test(r)){let i=new Error("WebVTT preamble incorrect.");throw this.#e.error(i),i}e=Q.exec(r);let t=r.slice(0,e?.index??r.length).trimEnd();if(!t){let i=new Error("No WebVTT preamble provided.");throw this.#e.error(i),i}this.#i=fe.encode(t),e&&(r=r.slice(e.index),Q.lastIndex=0)}for(;e=Q.exec(r);){let t=r.slice(0,e.index),i=e[1]||"",n=e.index+e[0].length,o=r.indexOf(`
-`,n)+1,l=r.slice(n,o).trim(),a=r.indexOf(`
+`).replaceAll("\r",`
+`),R.lastIndex=0;let e;if(!this.#i){if(!Le.test(r)){let s=new Error("WebVTT preamble incorrect.");throw this.#e.error(s),s}e=R.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),R.lastIndex=0)}for(;e=R.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(`
-`,n);a===-1&&(a=r.length);let c=this.#s(e[2]),g=this.#s(e[3])-c,C=r.slice(o,a),_=`${l}
-${i}
-${t}`;He.lastIndex=0,C=C.replace(He,E=>{let $e=this.#s(E.slice(1,-1))-c;return`<${this.#o($e)}>`}),r=r.slice(a).trimStart(),Q.lastIndex=0;let A={body:fe.encode(C),additions:_.trim()===""?null:fe.encode(_),timestamp:c*1e3,duration:g*1e3},T={};this.#t||(T.decoderConfig={description:this.#i},this.#t=!0),this.#e.output(A,T)}}#s(r){let e=It.exec(r);if(!e)throw new Error("Expected match.");return 60*60*1e3*Number(e[1]||"0")+60*1e3*Number(e[2])+1e3*Number(e[3])+Number(e[4])}#o(r){let e=Math.floor(r/36e5),t=Math.floor(r%(60*60*1e3)/(60*1e3)),i=Math.floor(r%(60*1e3)/1e3),n=r%1e3;return e.toString().padStart(2,"0")+":"+t.toString().padStart(2,"0")+":"+i.toString().padStart(2,"0")+"."+n.toString().padStart(3,"0")}};var H=class{constructor(){this.connectedTrack=null;this.closed=!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 H{constructor(e){super();this.connectedTrack=null;this.codec=e}},pe=class extends D{constructor(r){super(r)}digest(r,e){this.ensureValidDigest(),this.connectedTrack?.output.muxer.addEncodedVideoChunk(this.connectedTrack,r,e)}},_t=5,$=class{constructor(r,e){this.source=r;this.codecConfig=e;this.encoder=null;this.lastMultipleOfKeyFrameInterval=-1}digest(r){this.source.ensureValidDigest(),this.ensureEncoder(r),d(this.encoder);let e=Math.floor(r.timestamp/1e6/_t);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:Re(this.codecConfig.codec,r.codedWidth,r.codedHeight),width:r.codedWidth,height:r.codedHeight,bitrate:this.codecConfig.bitrate}))}async flush(){return this.encoder?.flush()}},be=class extends D{constructor(r){super(r.codec),this.encoder=new $(this,r)}digest(r){this.encoder.digest(r)}flush(){return this.encoder.flush()}},ke=class extends D{constructor(e,t){super(t.codec);this.canvas=e;this.encoder=new $(this,t)}digest(e,t=0){let i=new VideoFrame(this.canvas,{timestamp:Math.round(1e6*e),duration:Math.round(1e6*t)});this.encoder.digest(i),i.close()}flush(){return this.encoder.flush()}},ge=class extends D{constructor(e,t){super(t.codec);this.track=e;this.abortController=null;this.encoder=new $(this,t)}start(){this.abortController=new AbortController;let e=new MediaStreamTrackProcessor({track:this.track}),t=new WritableStream({write:i=>{this.encoder.digest(i),i.close()}});e.readable.pipeTo(t,{signal:this.abortController.signal}).catch(i=>{i instanceof DOMException&&i.name==="AbortError"||console.error("Pipe error:",i)})}async flush(){this.abortController&&(this.abortController.abort(),this.abortController=null),await this.encoder.flush()}},B=class extends H{constructor(e){super();this.connectedTrack=null;this.codec=e}},Te=class extends B{constructor(r){super(r)}digest(r,e){this.ensureValidDigest(),this.connectedTrack?.output.muxer.addEncodedAudioChunk(this.connectedTrack,r,e)}},K=class{constructor(r,e){this.source=r;this.codecConfig=e;this.encoder=null}digest(r){this.source.ensureValidDigest(),this.ensureEncoder(r),d(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:Qe(this.codecConfig.codec,r.numberOfChannels,r.sampleRate),numberOfChannels:r.numberOfChannels,sampleRate:r.sampleRate,bitrate:this.codecConfig.bitrate}))}async flush(){return this.encoder?.flush()}},Ce=class extends B{constructor(r){super(r.codec),this.encoder=new K(this,r)}digest(r){this.encoder.digest(r)}flush(){return this.encoder.flush()}},xe=class extends B{constructor(e){super(e.codec);this.accumulatedFrameCount=0;this.encoder=new K(this,e)}digest(e){let t=e.numberOfChannels,i=e.sampleRate,n=e.length,o=new Float32Array(t*n);for(let a=0;a{this.encoder.digest(i),i.close()}});e.readable.pipeTo(t,{signal:this.abortController.signal}).catch(i=>{i instanceof DOMException&&i.name==="AbortError"||console.error("Pipe error:",i)})}async flush(){this.abortController&&(this.abortController.abort(),this.abortController=null),await this.encoder.flush()}},Se=class extends H{constructor(e){super();this.connectedTrack=null;this.codec=e}},we=class extends Se{constructor(r){super(r),this.encoder=new te({output:(e,t)=>this.connectedTrack?.output.muxer.addEncodedSubtitleChunk(this.connectedTrack,e,t),error:e=>console.error(e)}),this.encoder.configure({codec:r})}digest(r){this.ensureValidDigest(),this.encoder.encode(r)}};var G=class{},re=class extends G{#e=0;#r;#i=new ArrayBuffer(2**16);#t=new Uint8Array(this.#i);#s=0;constructor(r){super(),this.#r=r}#o(r){let e=this.#i.byteLength;for(;et.start-i.start);r.push({start:e[0].start,size:e[0].data.byteLength});for(let t=1;ta.start<=e&&eNt){for(let a=0;a=r.written[n+1].start;)r.written[n].end=Math.max(r.written[n].end,r.written[n+1].end),r.written.splice(n+1,1)}#n(r){let t={start:Math.floor(r/this.#i)*this.#i,data:new Uint8Array(this.#i),written:[],shouldFlush:!1};return this.#t.push(t),this.#t.sort((i,n)=>i.start-n.start),this.#t.indexOf(t)}#a(r=!1){for(let e=0;er.stream.write({type:"write",data:e,position:t}),chunkSize:r.options?.chunkSize}))}};var I=class{constructor(){this.output=null}},Ae=class extends I{constructor(){super(...arguments);this.buffer=null}createWriter(){return new re(this)}},q=class extends I{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 j(this):new ie(this)}},Oe=class extends I{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 se(this)}};export{Ae as ArrayBufferTarget,xe as AudioBufferSource,Ce as AudioDataSource,ke as CanvasSource,Te as EncodedAudioChunkSource,pe as EncodedVideoChunkSource,Oe as FileSystemWritableFileStreamTarget,ye as MediaStreamAudioTrackSource,ge as MediaStreamVideoTrackSource,ee as MkvOutputFormat,me as Mp4OutputFormat,ne as Output,q as StreamTarget,I as Target,we as TextSubtitleSource,be as VideoFrameSource,F as WebMOutputFormat};
+`,o);u===-1&&(u=r.length);let m=ee(e[2]),f=ee(e[3])-m,g=r.slice(n,u).trim();r=r.slice(u).trimStart(),R.lastIndex=0;let v={timestamp:m/1e3,duration:f/1e3,text:g,identifier:s,settings:a,notes:t},C={};this.#t||(C.config={description:this.#i},this.#t=!0),this.#e.output(v,C)}}},Ze=/(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})/,ee=i=>{let r=Ze.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])},te=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]]),Je=i=>(O.setInt16(0,i,!1),[p[0],p[1]]),Pe=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]]),Ie=i=>(O.setInt32(0,i,!1),[p[0],p[1],p[2],p[3]]),U=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]]),fe=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]]),me=i=>(O.setInt32(0,2**30*i,!1),[p[0],p[1],p[2],p[3]]),S=(i,r=!1)=>{let e=Array(i.length).fill(null).map((t,s)=>i.charCodeAt(s));return r&&e.push(0),e},pe=i=>{let r=null;for(let e of i)(!r||e.timestamp>r.timestamp)&&(r=e);return r},Be=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]},_e=Be(0),De=i=>[A(i[0]),A(i[1]),me(i[2]),A(i[3]),A(i[4]),me(i[5]),A(i[6]),A(i[7]),me(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,[k(r),Pe(e),t??[]],s),Fe=i=>{let r=512;return i.fragmented?b("ftyp",[S("iso5"),l(r),S("iso5"),S("iso6"),S("mp41")]):b("ftyp",[S("isom"),l(r),S("isom"),i.holdsAvc?S("avc1"):[],S("mp41")])},ie=i=>({type:"mdat",largeSize:i}),We=i=>({type:"free",size:i}),H=(i,r,e=!1)=>b("moov",void 0,[et(r,i),...i.map(t=>tt(t,r)),e?Pt(i):null]),et=(i,r)=>{let e=w(Math.max(0,...r.filter(n=>n.samples.length>0).map(n=>{let a=pe(n.samples);return a.timestamp+a.duration})),re),t=Math.max(...r.map(n=>n.track.id))+1,s=!V(i)||!V(e),o=s?U:l;return T("mvhd",+s,0,[o(i),o(i),l(re),o(e),A(1),fe(1),Array(10).fill(0),De(_e),Array(24).fill(0),l(t)])},tt=(i,r)=>b("trak",void 0,[rt(i,r),it(i,r)]),rt=(i,r)=>{let e=pe(i.samples),t=w(e?e.timestamp+e.duration:0,re),s=!V(r)||!V(t),o=s?U:l,n;if(i.type==="video"){let a=i.track.metadata.rotation;n=a===void 0||typeof a=="number"?Be(a??0):a}else n=_e;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),fe(i.type==="audio"?1:0),h(0),De(n),A(i.type==="video"?i.info.width:0),A(i.type==="video"?i.info.height:0)])},it=(i,r)=>b("mdia",void 0,[st(i,r),at(i),ut(i)]),st=(i,r)=>{let e=pe(i.samples),t=w(e?e.timestamp+e.duration:0,i.timescale),s=!V(r)||!V(t),o=s?U:l;return T("mdhd",+s,0,[o(r),o(r),l(i.timescale),o(t),h(21956),h(0)])},ot={video:"vide",audio:"soun",subtitle:"text"},nt={video:"VideoHandler",audio:"SoundHandler",subtitle:"TextHandler"},at=i=>T("hdlr",0,0,[S("mhlr"),S(ot[i.type]),l(0),l(0),l(0),S(nt[i.type],!0)]),ut=i=>b("minf",void 0,[mt[i.type](),ft(),bt(i)]),lt=()=>T("vmhd",0,1,[h(0),h(0),h(0),h(0)]),ct=()=>T("smhd",0,0,[h(0),h(0)]),dt=()=>T("nmhd",0,0),mt={video:lt,audio:ct,subtitle:dt},ft=()=>b("dinf",void 0,[pt()]),pt=()=>T("dref",0,0,[l(1)],[ht()]),ht=()=>T("url ",0,1),bt=i=>{let r=i.compositionTimeOffsetTable.length>1||i.compositionTimeOffsetTable.some(e=>e.sampleCompositionTimeOffset!==0);return b("stbl",void 0,[Tt(i),vt(i),Vt(i),Mt(i),zt(i),Ut(i),r?Et(i):null])},Tt=i=>{let r;return i.type==="video"?r=gt(Qt[i.track.source.codec],i):i.type==="audio"?r=yt($t[i.track.source.codec],i):i.type==="subtitle"&&(r=At(Gt[i.track.source.codec],i)),c(r),T("stsd",0,0,[l(1)],[r])},gt=(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),Je(65535)],[Ht[r.track.source.codec](r)]),kt=i=>i.info.decoderConfig&&b("avcC",[...z(i.info.decoderConfig.description)]),Ct=i=>i.info.decoderConfig&&b("hvcC",[...z(i.info.decoderConfig.description)]),Ee=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,[k(t),k(s),k(a),k(2),k(2),k(2),h(0)])},xt=()=>{let e=(1<<7)+1;return b("av1C",[e,0,0,0])},yt=(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)],[Kt[r.track.source.codec](r)]),St=i=>{let r=z(i.info.decoderConfig.description??new ArrayBuffer(0));return T("esds",0,0,[l(58753152),k(32+r.byteLength),h(1),k(0),l(75530368),k(18+r.byteLength),k(64),k(21),Pe(0),l(130071),l(130071),l(92307584),k(r.byteLength),...r,l(109084800),k(1),k(2)])},wt=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",[k(0),k(i.info.numberOfChannels),h(r),l(i.info.sampleRate),fe(e),k(0)])},At=(i,r)=>b(i,[Array(6).fill(0),h(1)],[jt[r.track.source.codec](r)]),Ot=i=>b("vttC",[...y.encode(i.info.config.description)]);var vt=i=>T("stts",0,0,[l(i.timeToSampleTable.length),i.timeToSampleTable.map(r=>[l(r.sampleCount),l(r.sampleDelta)])]),Vt=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))])},Mt=i=>T("stsc",0,0,[l(i.compactlyCodedChunkTable.length),i.compactlyCodedChunkTable.map(r=>[l(r.firstChunk),l(r.samplesPerChunk),l(1)])]),zt=i=>T("stsz",0,0,[l(0),l(i.samples.length),i.samples.map(r=>l(r.size))]),Ut=i=>i.finalizedChunks.length>0&&P(i.finalizedChunks).offset>=2**32?T("co64",0,0,[l(i.finalizedChunks.length),i.finalizedChunks.map(r=>U(r.offset))]):T("stco",0,0,[l(i.finalizedChunks.length),i.finalizedChunks.map(r=>l(r.offset))]),Et=i=>T("ctts",0,0,[l(i.compositionTimeOffsetTable.length),i.compositionTimeOffsetTable.map(r=>[l(r.sampleCount),l(r.sampleCompositionTimeOffset)])]),Pt=i=>b("mvex",void 0,i.map(It)),It=i=>T("trex",0,0,[l(i.track.id),l(1),l(0),l(0),l(0)]),he=(i,r)=>b("moof",void 0,[Bt(i),...r.map(_t)]),Bt=i=>T("mfhd",0,0,[l(i)]),Ne=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},_t=i=>b("traf",void 0,[Dt(i),Ft(i),Wt(i)]),Dt=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:Ne(e)};return T("tfhd",0,r,[l(i.track.id),l(t.duration),l(t.size),l(t.flags)])},Ft=i=>(c(i.currentChunk),T("tfdt",1,0,[U(w(i.currentChunk.startTimestamp,i.timescale))])),Wt=i=>{c(i.currentChunk);let r=i.currentChunk.samples.map(x=>x.timescaleUnitsToNextSample),e=i.currentChunk.samples.map(x=>x.size),t=i.currentChunk.samples.map(Ne),s=i.currentChunk.samples.map(x=>w(x.timestamp-x.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,g=!m&&a.size>1,v=u.size>1||[...u].some(x=>x!==0),C=0;return C|=1,C|=4*+m,C|=256*+d,C|=512*+f,C|=1024*+g,C|=2048*+v,T("trun",1,C,[l(i.currentChunk.samples.length),l(i.currentChunk.offset-i.currentChunk.moofOffset||0),m?l(t[0]):[],i.currentChunk.samples.map((x,E)=>[d?l(r[E]):[],f?l(e[E]):[],g?l(t[E]):[],v?Ie(s[E]):[]])])},Re=i=>b("mfra",void 0,[...i.map(Nt),Rt()]),Nt=(i,r)=>T("tfra",1,0,[l(i.track.id),l(63),l(i.finalizedChunks.length),i.finalizedChunks.map(t=>[U(w(t.startTimestamp,i.timescale)),U(t.moofOffset),l(r+1),l(1),l(1)])]),Rt=()=>T("mfro",0,0,[l(0)]),Qe=()=>b("vtte"),He=(i,r,e,t,s)=>b("vttc",void 0,[s!==null?b("vsid",[Ie(s)]):null,e!==null?b("iden",[...y.encode(e)]):null,r!==null?b("ctim",[...y.encode(te(r))]):null,t!==null?b("sttg",[...y.encode(t)]):null,b("payl",[...y.encode(i)])]),$e=i=>b("vtta",[...y.encode(i)]),Qt={avc:"avc1",hevc:"hvc1",vp8:"vp08",vp9:"vp09",av1:"av01"},Ht={avc:kt,hevc:Ct,vp8:Ee,vp9:Ee,av1:xt},$t={aac:"mp4a",opus:"Opus"},Kt={aac:St,opus:wt},Gt={webvtt:"wvtt"},jt={webvtt:Ot};var B=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&&eqt){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 _=class{constructor(){this.output=null}},j=class extends _{constructor(){super(...arguments);this.buffer=null}createWriter(){return new se(this)}},G=class extends _{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 oe(this)}},be=class extends _{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 ne(this)}};var re=1e3,Yt=2082844800,w=(i,r,e=!0)=>{let t=i*r;return e?Math.round(t):t},ae=class extends B{constructor(e,t){super(e);this.timestampsMustStartAtZero=!0;this.#r=new j;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)+Yt;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(Fe({holdsAvc:e,fragmented:this.#t.options.fastStart==="fragmented"})),this.#c=this.#e.getPos(),this.#t.options.fastStart==="in-memory")this.#n=ie(!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=ie(!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}#g(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.#g(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.#k(o,t.timestamp))}#k(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 g=I.test(f.text),v=f.timestamp+f.duration,C=e.cueToSourceId.get(f);if(C===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.#y())}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=he(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.#k(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.#k(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.#y(!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=Re(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(We(o))}else this.#i.writeBox(s)}}};var X=class{constructor(r){this.value=r}},D=class{constructor(r){this.value=r}},q=class{constructor(r){this.value=r}};var Te=i=>i<256?1:i<65536?2:i<1<<24?3:i<2**32?4:i<2**40?5:6,ge=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,Ke=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 ke=2**15,Ge="https://github.com/Vanilagy/webm-muxer",je=6,Xe=5,Lt={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"},Zt={video:1,audio:2,subtitle:17},ue=class extends B{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.#g=0;this.#e=e.writer,this.#i=t}#e;#i;#t;#r;#o;#a;#c;#n;#s;#d;#u;#l;#h;#T;#g;#w(e){this.#r.setUint8(0,e),this.#e.write(this.#t.subarray(0,1))}#k(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=Te(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=ge(e)){e<0&&(e+=2**(t*8)),this.#f(e,t)}writeEBMLVarInt(e,t=Ke(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??Te(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 X)this.writeEBMLVarInt(4),this.#k(e.data.value);else if(e.data instanceof D)this.writeEBMLVarInt(8),this.#b(e.data.value);else if(e.data instanceof q){let t=e.size??ge(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.#y(),this.#V(),this.#U(),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)}#y(){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 D(0)};this.#d=e;let t={id:357149030,data:[{id:2807729,data:1e6},{id:19840,data:Ge},{id:22337,data:Ge},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:Zt[t.type]},{id:134,data:Lt[t.track.source.codec]},...t.type==="video"?[t.info.decoderConfig.description?{id:25506,data:z(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!s.matrix||!s.transfer||!s.primaries||s.fullRange==null?null:{id:21936,data:[{id:21937,data:{rgb:1,bt709:1,bt470bg:5,smpte170m:6}[s.matrix]},{id:21946,data:{bt709:1,smpte170m:6,"iec61966-2-1":13}[s.transfer]},{id:21947,data:{bt709:1,bt470bg:5,smpte170m:6}[s.primaries]},{id:21945,data:[1,2][Number(s.fullRange)]}]}}return null})()]}]:[],...t.type==="audio"?[t.info.decoderConfig.description?{id:25506,data:z(t.info.decoderConfig.description)}:null,{id:225,data:[{id:181,data:new X(t.info.sampleRate)},{id:159,data:t.info.numberOfChannels}]}]:[],...t.type==="subtitle"?[{id:25506,data:y.encode(t.info.config.description)}]:[]]})}#z(){let e={id:408125543,size:this.#i.options.streaming?-1:je,data:[this.#i.options.streaming?null:this.#n,this.#c,this.#s]};this.#a=e,this.writeEBML(e)}#U(){this.#u={id:475249515,data:[]}}get#C(){return c(this.#a),this.dataOffsets.get(this.#a)}#E(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.#E(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.#B(o,u),o.chunkQueue.push(u),this.#S()}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.#S()}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,g=>{let C=ee(g.slice(1,-1))-u;return`<${te(C)}>`});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.#S()}#S(){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];Ue(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.#z());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 g=f.chunkQueue[0];return g&&g.type==="key"});(!this.#l||o&&s-this.#h>=1e3)&&this.#_(s);let n=s-this.#h;if(n<0)return;if(n>=ke)throw new Error(`Current Matroska cluster exceeded its maximum allowed length of ${ke} milliseconds. In order to produce a correct WebM file, you must pass in a key frame at least every ${ke} 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 q(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.#g=Math.max(this.#g,s+d),e.lastWrittenMsTimestamp=s,this.#T.add(e)}#_(e){this.#l&&!this.#i.options.streaming&&this.#v(),this.#l={id:524531317,size:this.#i.options.streaming?-1:Xe,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,Xe),this.#e.seek(t);let s=this.offsets.get(this.#l)-this.#C;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.#S()}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.#C;this.#e.seek(this.offsets.get(this.#a)+4),this.writeEBMLVarInt(t,je),this.#d.data=new D(this.#g),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.#C,this.#n.data[1].data[1].data=this.offsets.get(this.#c)-this.#C,this.#n.data[2].data[1].data=this.offsets.get(this.#s)-this.#C,this.#e.seek(this.offsets.get(this.#n)),this.writeEBML(this.#n),this.#e.seek(e)}}};var le=class{},Ce=class extends le{constructor(e){super();this.options=e}createMuxer(e){return new ae(e,this)}},ce=class extends le{constructor(e={}){super();this.options=e}createMuxer(e){return new ue(e,this)}},F=class extends ce{};var qe=(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}'.`)},Ye=(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 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}},xe=class extends W{constructor(r){super(r)}digest(r,e){this.ensureValidDigest(),this.connectedTrack?.output.muxer.addEncodedVideoChunk(this.connectedTrack,r,e)}},Jt=5,L=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/Jt);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:qe(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()}},ye=class extends W{constructor(r){super(r.codec),this.encoder=new L(this,r)}digest(r){this.encoder.digest(r)}flush(){return this.encoder.flush()}},Se=class extends W{constructor(e,t){super(t.codec);this.canvas=e;this.encoder=new L(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()}},we=class extends W{constructor(e,t){super(t.codec);this.track=e;this.abortController=null;this.offsetTimestamps=!0;this.encoder=new L(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 Y{constructor(e){super();this.connectedTrack=null;this.codec=e}},Ae=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:Ye(this.codecConfig.codec,r.numberOfChannels,r.sampleRate),numberOfChannels:r.numberOfChannels,sampleRate:r.sampleRate,bitrate:this.codecConfig.bitrate}))}async flush(){return this.encoder?.flush()}},Oe=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()}},ve=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()}},Me=class extends Y{constructor(e){super();this.connectedTrack=null;this.codec=e}},ze=class extends Me{constructor(r){super(r),this.parser=new J({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{j as ArrayBufferTarget,ve as AudioBufferSource,Oe as AudioDataSource,Se as CanvasSource,Ae as EncodedAudioChunkSource,xe as EncodedVideoChunkSource,be as FileSystemWritableFileStreamTarget,Ve as MediaStreamAudioTrackSource,we as MediaStreamVideoTrackSource,ce as MkvOutputFormat,Ce as Mp4OutputFormat,de as Output,G as StreamTarget,_ as Target,ze as TextSubtitleSource,ye as VideoFrameSource,F as WebMOutputFormat};
diff --git a/dist/metamuxer.mjs b/dist/metamuxer.mjs
index f4cf278..246d675 100644
--- a/dist/metamuxer.mjs
+++ b/dist/metamuxer.mjs
@@ -102,8 +102,168 @@ var toUint8Array = (source) => {
return new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
}
};
+var textEncoder = new TextEncoder();
+
+// 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;
+var preambleStartRegex = /^WEBVTT(.|\n)*?\n{2}/;
+var inlineTimestampRegex = /<(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})>/g;
+var SubtitleParser = class {
+ #options;
+ #preambleText = null;
+ #preambleEmitted = false;
+ constructor(options) {
+ this.#options = options;
+ }
+ parse(text) {
+ text = text.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
+ cueBlockHeaderRegex.lastIndex = 0;
+ let match;
+ if (!this.#preambleText) {
+ if (!preambleStartRegex.test(text)) {
+ let error = new Error("WebVTT preamble incorrect.");
+ this.#options.error(error);
+ throw error;
+ }
+ match = cueBlockHeaderRegex.exec(text);
+ let preamble = text.slice(0, match?.index ?? text.length).trimEnd();
+ if (!preamble) {
+ let error = new Error("No WebVTT preamble provided.");
+ this.#options.error(error);
+ throw error;
+ }
+ this.#preambleText = preamble;
+ if (match) {
+ text = text.slice(match.index);
+ cueBlockHeaderRegex.lastIndex = 0;
+ }
+ }
+ while (match = cueBlockHeaderRegex.exec(text)) {
+ let notes = text.slice(0, match.index);
+ let cueIdentifier = match[1];
+ let matchEnd = match.index + match[0].length;
+ let bodyStart = text.indexOf("\n", matchEnd) + 1;
+ let cueSettings = text.slice(matchEnd, bodyStart).trim();
+ let bodyEnd = text.indexOf("\n\n", matchEnd);
+ if (bodyEnd === -1) bodyEnd = text.length;
+ let startTime = parseSubtitleTimestamp(match[2]);
+ let endTime = parseSubtitleTimestamp(match[3]);
+ let duration = endTime - startTime;
+ let body = text.slice(bodyStart, bodyEnd).trim();
+ text = text.slice(bodyEnd).trimStart();
+ cueBlockHeaderRegex.lastIndex = 0;
+ let cue = {
+ timestamp: startTime / 1e3,
+ duration: duration / 1e3,
+ text: body,
+ identifier: cueIdentifier,
+ settings: cueSettings,
+ notes
+ };
+ let meta = {};
+ if (!this.#preambleEmitted) {
+ meta.config = {
+ description: this.#preambleText
+ };
+ this.#preambleEmitted = true;
+ }
+ this.#options.output(cue, meta);
+ }
+ }
+};
+var timestampRegex = /(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})/;
+var parseSubtitleTimestamp = (string) => {
+ let match = timestampRegex.exec(string);
+ if (!match) throw new Error("Expected match.");
+ return 60 * 60 * 1e3 * Number(match[1] || "0") + 60 * 1e3 * Number(match[2]) + 1e3 * Number(match[3]) + Number(match[4]);
+};
+var formatSubtitleTimestamp = (timestamp) => {
+ let hours = Math.floor(timestamp / (60 * 60 * 1e3));
+ let minutes = Math.floor(timestamp % (60 * 60 * 1e3) / (60 * 1e3));
+ let seconds = Math.floor(timestamp % (60 * 1e3) / 1e3);
+ let milliseconds = timestamp % 1e3;
+ return hours.toString().padStart(2, "0") + ":" + minutes.toString().padStart(2, "0") + ":" + seconds.toString().padStart(2, "0") + "." + milliseconds.toString().padStart(3, "0");
+};
// src/isobmff/isobmff_boxes.ts
+var IsobmffBoxWriter = class {
+ constructor(writer) {
+ this.writer = writer;
+ this.helper = new Uint8Array(8);
+ this.helperView = new DataView(this.helper.buffer);
+ /**
+ * Stores the position from the start of the file to where boxes elements have been written. This is used to
+ * rewrite/edit elements that were already added before, and to measure sizes of things.
+ */
+ this.offsets = /* @__PURE__ */ new WeakMap();
+ }
+ writeU32(value) {
+ this.helperView.setUint32(0, value, false);
+ this.writer.write(this.helper.subarray(0, 4));
+ }
+ writeU64(value) {
+ this.helperView.setUint32(0, Math.floor(value / 2 ** 32), false);
+ this.helperView.setUint32(4, value, false);
+ this.writer.write(this.helper.subarray(0, 8));
+ }
+ writeAscii(text) {
+ for (let i = 0; i < text.length; i++) {
+ this.helperView.setUint8(i % 8, text.charCodeAt(i));
+ if (i % 8 === 7) this.writer.write(this.helper);
+ }
+ if (text.length % 8 !== 0) {
+ this.writer.write(this.helper.subarray(0, text.length % 8));
+ }
+ }
+ writeBox(box2) {
+ this.offsets.set(box2, this.writer.getPos());
+ if (box2.contents && !box2.children) {
+ this.writeBoxHeader(box2, box2.size ?? box2.contents.byteLength + 8);
+ this.writer.write(box2.contents);
+ } else {
+ let startPos = this.writer.getPos();
+ this.writeBoxHeader(box2, 0);
+ if (box2.contents) this.writer.write(box2.contents);
+ if (box2.children) {
+ for (let child of box2.children) if (child) this.writeBox(child);
+ }
+ let endPos = this.writer.getPos();
+ let size = box2.size ?? endPos - startPos;
+ this.writer.seek(startPos);
+ this.writeBoxHeader(box2, size);
+ this.writer.seek(endPos);
+ }
+ }
+ writeBoxHeader(box2, size) {
+ this.writeU32(box2.largeSize ? 1 : size);
+ this.writeAscii(box2.type);
+ if (box2.largeSize) this.writeU64(size);
+ }
+ measureBoxHeader(box2) {
+ return 8 + (box2.largeSize ? 8 : 0);
+ }
+ patchBox(box2) {
+ const boxOffset = this.offsets.get(box2);
+ assert(boxOffset !== void 0);
+ let endPos = this.writer.getPos();
+ this.writer.seek(boxOffset);
+ this.writeBox(box2);
+ this.writer.seek(endPos);
+ }
+ measureBox(box2) {
+ if (box2.contents && !box2.children) {
+ let headerSize = this.measureBoxHeader(box2);
+ return headerSize + box2.contents.byteLength;
+ } else {
+ let result = this.measureBoxHeader(box2);
+ if (box2.contents) result += box2.contents.byteLength;
+ if (box2.children) {
+ for (let child of box2.children) if (child) result += this.measureBox(child);
+ }
+ return result;
+ }
+ }
+};
var bytes = new Uint8Array(8);
var view = new DataView(bytes.buffer);
var u8 = (value) => {
@@ -298,7 +458,7 @@ var tkhd = (trackData, creationTime) => {
// Reserved
u16(0),
// Layer
- u16(0),
+ u16(trackData.track.id),
// Alternate group
fixed_8_8(trackData.type === "audio" ? 1 : 0),
// Volume
@@ -314,7 +474,7 @@ var tkhd = (trackData, creationTime) => {
};
var mdia = (trackData, creationTime) => box("mdia", void 0, [
mdhd(trackData, creationTime),
- hdlr(trackData.type === "video" ? "vide" : "soun"),
+ hdlr(trackData),
minf(trackData)
]);
var mdhd = (trackData, creationTime) => {
@@ -340,10 +500,20 @@ var mdhd = (trackData, creationTime) => {
// Quality
]);
};
-var hdlr = (componentSubtype) => fullBox("hdlr", 0, 0, [
+var TRACK_TYPE_TO_COMPONENT_SUBTYPE = {
+ video: "vide",
+ audio: "soun",
+ subtitle: "text"
+};
+var TRACK_TYPE_TO_HANDLER_NAME = {
+ video: "VideoHandler",
+ audio: "SoundHandler",
+ subtitle: "TextHandler"
+};
+var hdlr = (trackData) => fullBox("hdlr", 0, 0, [
ascii("mhlr"),
// Component type
- ascii(componentSubtype),
+ ascii(TRACK_TYPE_TO_COMPONENT_SUBTYPE[trackData.type]),
// Component subtype
u32(0),
// Component manufacturer
@@ -351,12 +521,11 @@ var hdlr = (componentSubtype) => fullBox("hdlr", 0, 0, [
// Component flags
u32(0),
// Component flags mask
- // TODO:
- ascii("mp4-muxer-hdlr", true)
+ ascii(TRACK_TYPE_TO_HANDLER_NAME[trackData.type], true)
// Component name
]);
var minf = (trackData) => box("minf", void 0, [
- trackData.type === "video" ? vmhd() : smhd(),
+ TRACK_TYPE_TO_HEADER_BOX[trackData.type](),
dinf(),
stbl(trackData)
]);
@@ -376,6 +545,12 @@ var smhd = () => fullBox("smhd", 0, 0, [
u16(0)
// Reserved
]);
+var nmhd = () => fullBox("nmhd", 0, 0);
+var TRACK_TYPE_TO_HEADER_BOX = {
+ video: vmhd,
+ audio: smhd,
+ subtitle: nmhd
+};
var dinf = () => box("dinf", void 0, [
dref()
]);
@@ -398,18 +573,32 @@ var stbl = (trackData) => {
needsCtts ? ctts(trackData) : null
]);
};
-var stsd = (trackData) => fullBox("stsd", 0, 0, [
- u32(1)
- // Entry count
-], [
- trackData.type === "video" ? videoSampleDescription(
- VIDEO_CODEC_TO_BOX_NAME[trackData.track.source.codec],
- trackData
- ) : soundSampleDescription(
- AUDIO_CODEC_TO_BOX_NAME[trackData.track.source.codec],
- trackData
- )
-]);
+var stsd = (trackData) => {
+ let sampleDescription;
+ if (trackData.type === "video") {
+ sampleDescription = videoSampleDescription(
+ VIDEO_CODEC_TO_BOX_NAME[trackData.track.source.codec],
+ trackData
+ );
+ } else if (trackData.type === "audio") {
+ sampleDescription = soundSampleDescription(
+ AUDIO_CODEC_TO_BOX_NAME[trackData.track.source.codec],
+ trackData
+ );
+ } else if (trackData.type === "subtitle") {
+ sampleDescription = subtitleSampleDescription(
+ SUBTITLE_CODEC_TO_BOX_NAME[trackData.track.source.codec],
+ trackData
+ );
+ }
+ assert(sampleDescription);
+ return fullBox("stsd", 0, 0, [
+ u32(1)
+ // Entry count
+ ], [
+ sampleDescription
+ ]);
+};
var videoSampleDescription = (compressionType, trackData) => box(compressionType, [
Array(6).fill(0),
// Reserved
@@ -441,6 +630,7 @@ var videoSampleDescription = (compressionType, trackData) => box(compressionType
// Pre-defined
], [
VIDEO_CODEC_TO_CONFIGURATION_BOX[trackData.track.source.codec](trackData)
+ // TODO colr
]);
var avcC = (trackData) => trackData.info.decoderConfig && box("avcC", [
// For AVC, description is an AVCDecoderConfigurationRecord, so nothing else to do here
@@ -584,6 +774,17 @@ var dOps = (trackData) => {
// ChannelMappingFamily
]);
};
+var subtitleSampleDescription = (compressionType, trackData) => box(compressionType, [
+ Array(6).fill(0),
+ // Reserved
+ u16(1)
+ // Data reference index
+], [
+ SUBTITLE_CODEC_TO_CONFIGURATION_BOX[trackData.track.source.codec](trackData)
+]);
+var vttC = (trackData) => box("vttC", [
+ ...textEncoder.encode(trackData.info.config.description)
+]);
var stts = (trackData) => {
return fullBox("stts", 0, 0, [
u32(trackData.timeToSampleTable.length),
@@ -817,6 +1018,15 @@ var mfro = () => {
// Size
]);
};
+var vtte = () => box("vtte");
+var vttc = (payload, timestamp, identifier, settings, sourceId) => box("vttc", void 0, [
+ sourceId !== null ? box("vsid", [i32(sourceId)]) : null,
+ identifier !== null ? box("iden", [...textEncoder.encode(identifier)]) : null,
+ timestamp !== null ? box("ctim", [...textEncoder.encode(formatSubtitleTimestamp(timestamp))]) : null,
+ settings !== null ? box("sttg", [...textEncoder.encode(settings)]) : null,
+ box("payl", [...textEncoder.encode(payload)])
+]);
+var vtta = (notes) => box("vtta", [...textEncoder.encode(notes)]);
var VIDEO_CODEC_TO_BOX_NAME = {
"avc": "avc1",
"hevc": "hvc1",
@@ -839,16 +1049,342 @@ var AUDIO_CODEC_TO_CONFIGURATION_BOX = {
"aac": esds,
"opus": dOps
};
+var SUBTITLE_CODEC_TO_BOX_NAME = {
+ "webvtt": "wvtt"
+};
+var SUBTITLE_CODEC_TO_CONFIGURATION_BOX = {
+ "webvtt": vttC
+};
// src/muxer.ts
var Muxer = class {
constructor(output) {
+ this.trackTimestampInfo = /* @__PURE__ */ new WeakMap();
this.output = output;
}
beforeTrackAdd(track) {
}
onTrackClose(track) {
}
+ validateAndNormalizeTimestamp(track, rawTimestampInUs, isKeyFrame) {
+ let timestampInSeconds = rawTimestampInUs / 1e6;
+ let timestampInfo = this.trackTimestampInfo.get(track);
+ if (!timestampInfo) {
+ if (!isKeyFrame) {
+ throw new Error("First frame must be a key frame.");
+ }
+ if (this.timestampsMustStartAtZero && timestampInSeconds > 0) {
+ throw new Error(`Timestamps must start at zero (got ${timestampInSeconds}s).`);
+ }
+ timestampInfo = {
+ timestampOffset: timestampInSeconds,
+ maxTimestamp: track.source.offsetTimestamps ? 0 : timestampInSeconds,
+ lastKeyFrameTimestamp: track.source.offsetTimestamps ? 0 : timestampInSeconds
+ };
+ this.trackTimestampInfo.set(track, timestampInfo);
+ }
+ if (track.source.offsetTimestamps) {
+ timestampInSeconds -= timestampInfo.timestampOffset;
+ }
+ if (timestampInSeconds < 0) {
+ throw new Error(`Timestamps must be non-negative (got ${timestampInSeconds}s).`);
+ }
+ if (timestampInSeconds < timestampInfo.lastKeyFrameTimestamp) {
+ throw new Error(`Timestamp cannot be smaller than last key frame's timestamp (got ${timestampInSeconds}s, last key frame at ${timestampInfo.lastKeyFrameTimestamp}s).`);
+ }
+ if (isKeyFrame) {
+ if (timestampInSeconds < timestampInfo.maxTimestamp) {
+ throw new Error(`Key frame timestamps cannot be smaller than any timestamp that came before (got ${timestampInSeconds}s, max timestamp was ${timestampInfo.maxTimestamp}s).`);
+ }
+ timestampInfo.lastKeyFrameTimestamp = timestampInSeconds;
+ }
+ timestampInfo.maxTimestamp = Math.max(timestampInfo.maxTimestamp, timestampInSeconds);
+ return timestampInSeconds;
+ }
+};
+
+// src/writer.ts
+var Writer = class {
+};
+var ArrayBufferTargetWriter = class extends Writer {
+ #pos = 0;
+ #target;
+ #buffer = new ArrayBuffer(2 ** 16);
+ #bytes = new Uint8Array(this.#buffer);
+ #maxPos = 0;
+ constructor(target) {
+ super();
+ this.#target = target;
+ }
+ #ensureSize(size) {
+ let newLength = this.#buffer.byteLength;
+ while (newLength < size) newLength *= 2;
+ if (newLength === this.#buffer.byteLength) return;
+ let newBuffer = new ArrayBuffer(newLength);
+ let newBytes = new Uint8Array(newBuffer);
+ newBytes.set(this.#bytes, 0);
+ this.#buffer = newBuffer;
+ this.#bytes = newBytes;
+ }
+ write(data) {
+ this.#ensureSize(this.#pos + data.byteLength);
+ this.#bytes.set(data, this.#pos);
+ this.#pos += data.byteLength;
+ this.#maxPos = Math.max(this.#maxPos, this.#pos);
+ }
+ seek(newPos) {
+ this.#pos = newPos;
+ }
+ getPos() {
+ return this.#pos;
+ }
+ flush() {
+ }
+ finalize() {
+ this.#ensureSize(this.#pos);
+ this.#target.buffer = this.#buffer.slice(0, Math.max(this.#maxPos, this.#pos));
+ }
+ getSlice(start, end) {
+ return this.#bytes.slice(start, end);
+ }
+};
+var StreamTargetWriter = class extends Writer {
+ #pos = 0;
+ #target;
+ #sections = [];
+ constructor(target) {
+ super();
+ this.#target = target;
+ }
+ write(data) {
+ this.#sections.push({
+ data: data.slice(),
+ start: this.#pos
+ });
+ this.#pos += data.byteLength;
+ }
+ seek(newPos) {
+ this.#pos = newPos;
+ }
+ getPos() {
+ return this.#pos;
+ }
+ flush() {
+ if (this.#sections.length === 0) return;
+ let chunks = [];
+ let sorted = [...this.#sections].sort((a, b) => a.start - b.start);
+ chunks.push({
+ start: sorted[0].start,
+ size: sorted[0].data.byteLength
+ });
+ for (let i = 1; i < sorted.length; i++) {
+ let lastChunk = chunks[chunks.length - 1];
+ let section = sorted[i];
+ if (section.start <= lastChunk.start + lastChunk.size) {
+ lastChunk.size = Math.max(lastChunk.size, section.start + section.data.byteLength - lastChunk.start);
+ } else {
+ chunks.push({
+ start: section.start,
+ size: section.data.byteLength
+ });
+ }
+ }
+ for (let chunk of chunks) {
+ chunk.data = new Uint8Array(chunk.size);
+ for (let section of this.#sections) {
+ if (chunk.start <= section.start && section.start < chunk.start + chunk.size) {
+ chunk.data.set(section.data, section.start - chunk.start);
+ }
+ }
+ this.#target.options.onData?.(chunk.data, chunk.start);
+ }
+ this.#sections.length = 0;
+ }
+ finalize() {
+ }
+};
+var DEFAULT_CHUNK_SIZE = 2 ** 24;
+var MAX_CHUNKS_AT_ONCE = 2;
+var ChunkedStreamTargetWriter = class extends Writer {
+ #pos = 0;
+ #target;
+ #chunkSize;
+ /**
+ * The data is divided up into fixed-size chunks, whose contents are first filled in RAM and then flushed out.
+ * A chunk is flushed if all of its contents have been written.
+ */
+ #chunks = [];
+ constructor(target) {
+ super();
+ this.#target = target;
+ this.#chunkSize = target.options?.chunkSize ?? DEFAULT_CHUNK_SIZE;
+ if (!Number.isInteger(this.#chunkSize) || this.#chunkSize < 2 ** 10) {
+ throw new Error("Invalid StreamTarget options: chunkSize must be an integer not smaller than 1024.");
+ }
+ }
+ write(data) {
+ this.#writeDataIntoChunks(data, this.#pos);
+ this.#flushChunks();
+ this.#pos += data.byteLength;
+ }
+ seek(newPos) {
+ this.#pos = newPos;
+ }
+ getPos() {
+ return this.#pos;
+ }
+ #writeDataIntoChunks(data, position) {
+ let chunkIndex = this.#chunks.findIndex((x) => x.start <= position && position < x.start + this.#chunkSize);
+ if (chunkIndex === -1) chunkIndex = this.#createChunk(position);
+ let chunk = this.#chunks[chunkIndex];
+ let relativePosition = position - chunk.start;
+ let toWrite = data.subarray(0, Math.min(this.#chunkSize - relativePosition, data.byteLength));
+ chunk.data.set(toWrite, relativePosition);
+ let section = {
+ start: relativePosition,
+ end: relativePosition + toWrite.byteLength
+ };
+ this.#insertSectionIntoChunk(chunk, section);
+ if (chunk.written[0].start === 0 && chunk.written[0].end === this.#chunkSize) {
+ chunk.shouldFlush = true;
+ }
+ if (this.#chunks.length > MAX_CHUNKS_AT_ONCE) {
+ for (let i = 0; i < this.#chunks.length - 1; i++) {
+ this.#chunks[i].shouldFlush = true;
+ }
+ this.#flushChunks();
+ }
+ if (toWrite.byteLength < data.byteLength) {
+ this.#writeDataIntoChunks(data.subarray(toWrite.byteLength), position + toWrite.byteLength);
+ }
+ }
+ #insertSectionIntoChunk(chunk, section) {
+ let low = 0;
+ let high = chunk.written.length - 1;
+ let index = -1;
+ while (low <= high) {
+ let mid = Math.floor(low + (high - low + 1) / 2);
+ if (chunk.written[mid].start <= section.start) {
+ low = mid + 1;
+ index = mid;
+ } else {
+ high = mid - 1;
+ }
+ }
+ chunk.written.splice(index + 1, 0, section);
+ if (index === -1 || chunk.written[index].end < section.start) index++;
+ while (index < chunk.written.length - 1 && chunk.written[index].end >= chunk.written[index + 1].start) {
+ chunk.written[index].end = Math.max(chunk.written[index].end, chunk.written[index + 1].end);
+ chunk.written.splice(index + 1, 1);
+ }
+ }
+ #createChunk(includesPosition) {
+ let start = Math.floor(includesPosition / this.#chunkSize) * this.#chunkSize;
+ let chunk = {
+ start,
+ data: new Uint8Array(this.#chunkSize),
+ written: [],
+ shouldFlush: false
+ };
+ this.#chunks.push(chunk);
+ this.#chunks.sort((a, b) => a.start - b.start);
+ return this.#chunks.indexOf(chunk);
+ }
+ #flushChunks(force = false) {
+ for (let i = 0; i < this.#chunks.length; i++) {
+ let chunk = this.#chunks[i];
+ if (!chunk.shouldFlush && !force) continue;
+ for (let section of chunk.written) {
+ this.#target.options.onData?.(
+ chunk.data.subarray(section.start, section.end),
+ chunk.start + section.start
+ );
+ }
+ this.#chunks.splice(i--, 1);
+ }
+ }
+ flush() {
+ }
+ finalize() {
+ this.#flushChunks(true);
+ }
+};
+var FileSystemWritableFileStreamTargetWriter = class extends ChunkedStreamTargetWriter {
+ constructor(target) {
+ super(new StreamTarget({
+ onData: (data, position) => target.stream.write({
+ type: "write",
+ data,
+ position
+ }),
+ chunkSize: target.options?.chunkSize
+ }));
+ }
+};
+
+// src/target.ts
+var Target = class {
+ constructor() {
+ this.output = null;
+ }
+};
+var ArrayBufferTarget2 = 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 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");
+ }
+ }
+ }
+ createWriter() {
+ return new FileSystemWritableFileStreamTargetWriter(this);
+ }
};
// src/isobmff/isobmff_muxer.ts
@@ -861,13 +1397,10 @@ var intoTimescale = (timeInSeconds, timescale, round = true) => {
var IsobmffMuxer = class extends Muxer {
constructor(output, format) {
super(output);
- this.#helper = new Uint8Array(8);
- this.#helperView = new DataView(this.#helper.buffer);
- /**
- * Stores the position from the start of the file to where boxes elements have been written. This is used to
- * rewrite/edit elements that were already added before, and to measure sizes of things.
- */
- this.offsets = /* @__PURE__ */ new WeakMap();
+ this.timestampsMustStartAtZero = true;
+ this.#auxTarget = new ArrayBufferTarget2();
+ this.#auxWriter = this.#auxTarget.createWriter();
+ this.#auxBoxWriter = new IsobmffBoxWriter(this.#auxWriter);
this.#ftypSize = null;
this.#mdat = null;
this.#trackDatas = [];
@@ -875,87 +1408,24 @@ var IsobmffMuxer = class extends Muxer {
this.#finalizedChunks = [];
this.#nextFragmentNumber = 1;
this.#writer = output.writer;
+ this.#boxWriter = new IsobmffBoxWriter(this.#writer);
this.#format = format;
}
#writer;
+ #boxWriter;
#format;
- #helper;
- #helperView;
+ #auxTarget;
+ #auxWriter;
+ #auxBoxWriter;
#ftypSize;
#mdat;
#trackDatas;
#creationTime;
#finalizedChunks;
#nextFragmentNumber;
- writeU32(value) {
- this.#helperView.setUint32(0, value, false);
- this.#writer.write(this.#helper.subarray(0, 4));
- }
- writeU64(value) {
- this.#helperView.setUint32(0, Math.floor(value / 2 ** 32), false);
- this.#helperView.setUint32(4, value, false);
- this.#writer.write(this.#helper.subarray(0, 8));
- }
- writeAscii(text) {
- for (let i = 0; i < text.length; i++) {
- this.#helperView.setUint8(i % 8, text.charCodeAt(i));
- if (i % 8 === 7) this.#writer.write(this.#helper);
- }
- if (text.length % 8 !== 0) {
- this.#writer.write(this.#helper.subarray(0, text.length % 8));
- }
- }
- writeBox(box2) {
- this.offsets.set(box2, this.#writer.getPos());
- if (box2.contents && !box2.children) {
- this.writeBoxHeader(box2, box2.size ?? box2.contents.byteLength + 8);
- this.#writer.write(box2.contents);
- } else {
- let startPos = this.#writer.getPos();
- this.writeBoxHeader(box2, 0);
- if (box2.contents) this.#writer.write(box2.contents);
- if (box2.children) {
- for (let child of box2.children) if (child) this.writeBox(child);
- }
- let endPos = this.#writer.getPos();
- let size = box2.size ?? endPos - startPos;
- this.#writer.seek(startPos);
- this.writeBoxHeader(box2, size);
- this.#writer.seek(endPos);
- }
- }
- writeBoxHeader(box2, size) {
- this.writeU32(box2.largeSize ? 1 : size);
- this.writeAscii(box2.type);
- if (box2.largeSize) this.writeU64(size);
- }
- measureBoxHeader(box2) {
- return 8 + (box2.largeSize ? 8 : 0);
- }
- patchBox(box2) {
- const boxOffset = this.offsets.get(box2);
- assert(boxOffset !== void 0);
- let endPos = this.#writer.getPos();
- this.#writer.seek(boxOffset);
- this.writeBox(box2);
- this.#writer.seek(endPos);
- }
- measureBox(box2) {
- if (box2.contents && !box2.children) {
- let headerSize = this.measureBoxHeader(box2);
- return headerSize + box2.contents.byteLength;
- } else {
- let result = this.measureBoxHeader(box2);
- if (box2.contents) result += box2.contents.byteLength;
- if (box2.children) {
- for (let child of box2.children) if (child) result += this.measureBox(child);
- }
- return result;
- }
- }
start() {
const holdsAvc = this.output.tracks.some((x) => x.type === "video" && x.source.codec === "avc");
- this.writeBox(ftyp({
+ this.#boxWriter.writeBox(ftyp({
holdsAvc,
fragmented: this.#format.options.fastStart === "fragmented"
}));
@@ -969,7 +1439,7 @@ var IsobmffMuxer = class extends Muxer {
this.#writer.seek(this.#writer.getPos() + moovSizeUpperBound);
}
this.#mdat = mdat(true);
- this.writeBox(this.#mdat);
+ this.#boxWriter.writeBox(this.#mdat);
}
this.#writer.flush();
}
@@ -1012,8 +1482,6 @@ var IsobmffMuxer = class extends Muxer {
samples: [],
sampleQueue: [],
timestampProcessingQueue: [],
- firstTimestamp: null,
- lastKeyFrameTimestamp: null,
timeToSampleTable: [],
compositionTimeOffsetTable: [],
lastTimescaleUnits: null,
@@ -1045,8 +1513,6 @@ var IsobmffMuxer = class extends Muxer {
samples: [],
sampleQueue: [],
timestampProcessingQueue: [],
- firstTimestamp: null,
- lastKeyFrameTimestamp: null,
timeToSampleTable: [],
compositionTimeOffsetTable: [],
lastTimescaleUnits: null,
@@ -1059,17 +1525,55 @@ var IsobmffMuxer = class extends Muxer {
this.#trackDatas.sort((a, b) => a.track.id - b.track.id);
return newTrackData;
}
+ #getSubtitleTrackData(track, meta) {
+ const existingTrackData = this.#trackDatas.find((x) => x.track === track);
+ if (existingTrackData) {
+ return existingTrackData;
+ }
+ assert(meta);
+ assert(meta.config);
+ const newTrackData = {
+ track,
+ type: "subtitle",
+ info: {
+ config: meta.config
+ },
+ timescale: 1e3,
+ // Reasonable
+ samples: [],
+ sampleQueue: [],
+ timestampProcessingQueue: [],
+ timeToSampleTable: [],
+ compositionTimeOffsetTable: [],
+ lastTimescaleUnits: null,
+ lastSample: null,
+ finalizedChunks: [],
+ currentChunk: null,
+ compactlyCodedChunkTable: [],
+ lastCueEndTimestamp: 0,
+ cueQueue: [],
+ nextSourceId: 0,
+ cueToSourceId: /* @__PURE__ */ new WeakMap()
+ };
+ this.#trackDatas.push(newTrackData);
+ this.#trackDatas.sort((a, b) => a.track.id - b.track.id);
+ this.validateAndNormalizeTimestamp(track, 0, true);
+ return newTrackData;
+ }
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 videoSample = this.#createSampleForTrack(trackData, chunk);
+ 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(videoSample);
+ trackData.sampleQueue.push(sample);
this.#interleaveSamples();
} else {
- this.#addSampleToTrack(trackData, videoSample);
+ this.#addSampleToTrack(trackData, sample);
}
}
addEncodedAudioChunk(track, chunk, meta) {
@@ -1077,29 +1581,101 @@ var IsobmffMuxer = class extends Muxer {
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 audioSample = this.#createSampleForTrack(trackData, chunk);
- if (this.#format.options.fastStart === "fragmented") {
- trackData.sampleQueue.push(audioSample);
- this.#interleaveSamples();
- } else {
- this.#addSampleToTrack(trackData, audioSample);
- }
- }
- #createSampleForTrack(trackData, chunk) {
- let timestampInSeconds = this.#validateTimestamp(trackData, chunk);
- let durationInSeconds = (chunk.duration ?? 0) / 1e6;
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);
+ }
+ }
+ addSubtitleCue(track, cue, meta) {
+ const trackData = this.#getSubtitleTrackData(track, meta);
+ this.validateAndNormalizeTimestamp(trackData.track, 1e6 * cue.timestamp, true);
+ if (track.source.codec === "webvtt") {
+ trackData.cueQueue.push(cue);
+ this.#processWebVTTCues(trackData, cue.timestamp);
+ } else {
+ }
+ }
+ #processWebVTTCues(trackData, until) {
+ while (trackData.cueQueue.length > 0) {
+ let timestamps = /* @__PURE__ */ new Set([]);
+ for (let cue of trackData.cueQueue) {
+ assert(cue.timestamp <= until);
+ assert(trackData.lastCueEndTimestamp <= cue.timestamp + cue.duration);
+ timestamps.add(Math.max(cue.timestamp, trackData.lastCueEndTimestamp));
+ timestamps.add(cue.timestamp + cue.duration);
+ }
+ let sortedTimestamps = [...timestamps].sort((a, b) => a - b);
+ let sampleStart = sortedTimestamps[0];
+ let sampleEnd = sortedTimestamps[1] ?? sampleStart;
+ if (until < sampleEnd) {
+ break;
+ }
+ if (trackData.lastCueEndTimestamp < sampleStart) {
+ this.#auxWriter.seek(0);
+ let box2 = vtte();
+ 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);
+ }
+ trackData.lastCueEndTimestamp = sampleStart;
+ }
+ this.#auxWriter.seek(0);
+ for (let i = 0; i < trackData.cueQueue.length; i++) {
+ let cue = trackData.cueQueue[i];
+ if (cue.timestamp >= sampleEnd) {
+ break;
+ }
+ inlineTimestampRegex.lastIndex = 0;
+ let containsTimestamp = inlineTimestampRegex.test(cue.text);
+ let endTimestamp = cue.timestamp + cue.duration;
+ let sourceId = trackData.cueToSourceId.get(cue);
+ if (sourceId === void 0 && sampleEnd < endTimestamp) {
+ sourceId = trackData.nextSourceId++;
+ trackData.cueToSourceId.set(cue, sourceId);
+ }
+ if (cue.notes) {
+ let box3 = vtta(cue.notes);
+ this.#auxBoxWriter.writeBox(box3);
+ }
+ let box2 = vttc(cue.text, containsTimestamp ? sampleStart : null, cue.identifier ?? null, cue.settings ?? null, sourceId ?? null);
+ this.#auxBoxWriter.writeBox(box2);
+ if (endTimestamp === sampleEnd) {
+ trackData.cueQueue.splice(i--, 1);
+ }
+ }
+ 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);
+ }
+ trackData.lastCueEndTimestamp = sampleEnd;
+ }
+ }
+ #createSampleForTrack(trackData, data, timestamp, duration, type) {
let sample = {
- timestamp: timestampInSeconds,
- decodeTimestamp: timestampInSeconds,
- // We may refine this later
- duration: durationInSeconds,
+ timestamp,
+ decodeTimestamp: timestamp,
+ // This may be refined later
+ duration,
data,
size: data.byteLength,
- type: chunk.type,
+ type,
// Will be refined once the next sample comes in
- timescaleUnitsToNextSample: intoTimescale(durationInSeconds, trackData.timescale)
+ timescaleUnitsToNextSample: intoTimescale(duration, trackData.timescale)
};
return sample;
}
@@ -1112,6 +1688,7 @@ var IsobmffMuxer = class extends Muxer {
const sample = trackData.timestampProcessingQueue[i];
sample.decodeTimestamp = sortedTimestamps[i];
const sampleCompositionTimeOffset = intoTimescale(sample.timestamp - sample.decodeTimestamp, trackData.timescale);
+ const durationInTimescale = intoTimescale(sample.duration, trackData.timescale);
if (trackData.lastTimescaleUnits !== null) {
assert(trackData.lastSample);
let timescaleUnits = intoTimescale(sample.decodeTimestamp, trackData.timescale, false);
@@ -1123,14 +1700,25 @@ var IsobmffMuxer = class extends Muxer {
assert(lastTableEntry);
if (lastTableEntry.sampleCount === 1) {
lastTableEntry.sampleDelta = delta;
- lastTableEntry.sampleCount++;
- } else if (lastTableEntry.sampleDelta === delta) {
+ let entryBefore = trackData.timeToSampleTable[trackData.timeToSampleTable.length - 2];
+ if (entryBefore && entryBefore.sampleDelta === delta) {
+ entryBefore.sampleCount++;
+ trackData.timeToSampleTable.pop();
+ lastTableEntry = entryBefore;
+ }
+ } else if (lastTableEntry.sampleDelta !== delta) {
+ lastTableEntry.sampleCount--;
+ trackData.timeToSampleTable.push(lastTableEntry = {
+ sampleCount: 1,
+ sampleDelta: delta
+ });
+ }
+ if (lastTableEntry.sampleDelta === durationInTimescale) {
lastTableEntry.sampleCount++;
} else {
- lastTableEntry.sampleCount--;
trackData.timeToSampleTable.push({
- sampleCount: 2,
- sampleDelta: delta
+ sampleCount: 1,
+ sampleDelta: durationInTimescale
});
}
const lastCompositionTimeOffsetTableEntry = last(trackData.compositionTimeOffsetTable);
@@ -1149,7 +1737,7 @@ var IsobmffMuxer = class extends Muxer {
if (this.#format.options.fastStart !== "fragmented") {
trackData.timeToSampleTable.push({
sampleCount: 1,
- sampleDelta: intoTimescale(sample.duration, trackData.timescale)
+ sampleDelta: durationInTimescale
});
trackData.compositionTimeOffsetTable.push({
sampleCount: 1,
@@ -1204,23 +1792,6 @@ var IsobmffMuxer = class extends Muxer {
trackData.currentChunk.samples.push(sample);
trackData.timestampProcessingQueue.push(sample);
}
- #validateTimestamp(trackData, chunk) {
- let timestampInSeconds = chunk.timestamp / 1e6;
- if (timestampInSeconds < 0) {
- throw new Error(`Timestamps must be non-negative (got ${timestampInSeconds}s).`);
- }
- if (trackData.firstTimestamp === null) {
- trackData.firstTimestamp = timestampInSeconds;
- }
- timestampInSeconds -= trackData.firstTimestamp;
- if (trackData.lastKeyFrameTimestamp !== null && timestampInSeconds < trackData.lastKeyFrameTimestamp) {
- throw new Error(`Timestamp cannot be before last key frame's timestamp (got ${timestampInSeconds}s, last key frame at ${trackData.lastKeyFrameTimestamp}s).`);
- }
- if (chunk.type === "key") {
- trackData.lastKeyFrameTimestamp = timestampInSeconds;
- }
- return timestampInSeconds;
- }
#finalizeCurrentChunk(trackData) {
assert(this.#format.options.fastStart !== "fragmented");
if (!trackData.currentChunk) return;
@@ -1247,18 +1818,20 @@ var IsobmffMuxer = class extends Muxer {
}
#interleaveSamples() {
assert(this.#format.options.fastStart === "fragmented");
- if (this.#trackDatas.length < this.output.tracks.length) {
- return;
+ for (const track of this.output.tracks) {
+ if (!track.source.closed && !this.#trackDatas.some((x) => x.track === track)) {
+ return;
+ }
}
outer:
while (true) {
let trackWithMinTimestamp = null;
let minTimestamp = Infinity;
for (let trackData of this.#trackDatas) {
- if (trackData.sampleQueue.length === 0) {
+ if (trackData.sampleQueue.length === 0 && !trackData.track.source.closed) {
break outer;
}
- if (trackData.sampleQueue[0].timestamp < minTimestamp) {
+ if (trackData.sampleQueue.length > 0 && trackData.sampleQueue[0].timestamp < minTimestamp) {
trackWithMinTimestamp = trackData;
minTimestamp = trackData.sampleQueue[0].timestamp;
}
@@ -1275,11 +1848,11 @@ var IsobmffMuxer = class extends Muxer {
let fragmentNumber = this.#nextFragmentNumber++;
if (fragmentNumber === 1) {
let movieBox = moov(this.#trackDatas, this.#creationTime, true);
- this.writeBox(movieBox);
+ this.#boxWriter.writeBox(movieBox);
}
let moofOffset = this.#writer.getPos();
let moofBox = moof(fragmentNumber, this.#trackDatas);
- this.writeBox(moofBox);
+ this.#boxWriter.writeBox(moofBox);
{
let mdatBox = mdat(false);
let totalTrackSampleSize = 0;
@@ -1289,13 +1862,13 @@ var IsobmffMuxer = class extends Muxer {
totalTrackSampleSize += sample.size;
}
}
- let mdatSize = this.measureBox(mdatBox) + totalTrackSampleSize;
+ let mdatSize = this.#boxWriter.measureBox(mdatBox) + totalTrackSampleSize;
if (mdatSize >= 2 ** 32) {
mdatBox.largeSize = true;
- mdatSize = this.measureBox(mdatBox) + totalTrackSampleSize;
+ mdatSize = this.#boxWriter.measureBox(mdatBox) + totalTrackSampleSize;
}
mdatBox.size = mdatSize;
- this.writeBox(mdatBox);
+ this.#boxWriter.writeBox(mdatBox);
}
for (let trackData of this.#trackDatas) {
trackData.currentChunk.offset = this.#writer.getPos();
@@ -1306,9 +1879,9 @@ var IsobmffMuxer = class extends Muxer {
}
}
let endPos = this.#writer.getPos();
- this.#writer.seek(this.offsets.get(moofBox));
+ this.#writer.seek(this.#boxWriter.offsets.get(moofBox));
let newMoofBox = moof(fragmentNumber, this.#trackDatas);
- this.writeBox(newMoofBox);
+ this.#boxWriter.writeBox(newMoofBox);
this.#writer.seek(endPos);
for (let trackData of this.#trackDatas) {
trackData.finalizedChunks.push(trackData.currentChunk);
@@ -1319,8 +1892,24 @@ var IsobmffMuxer = class extends Muxer {
this.#writer.flush();
}
}
+ onTrackClose(track) {
+ if (track.type === "subtitle" && track.source.codec === "webvtt") {
+ let trackData = this.#trackDatas.find((x) => x.track === track);
+ if (trackData) {
+ this.#processWebVTTCues(trackData, Infinity);
+ }
+ }
+ if (this.#format.options.fastStart === "fragmented") {
+ this.#interleaveSamples();
+ }
+ }
/** Finalizes the file, making it ready for use. Must be called after all video and audio chunks have been added. */
finalize() {
+ for (let trackData of this.#trackDatas) {
+ if (trackData.type === "subtitle" && trackData.track.source.codec === "webvtt") {
+ this.#processWebVTTCues(trackData, Infinity);
+ }
+ }
if (this.#format.options.fastStart === "fragmented") {
for (let trackData of this.#trackDatas) {
for (let sample of trackData.sampleQueue) {
@@ -1340,8 +1929,8 @@ var IsobmffMuxer = class extends Muxer {
let mdatSize;
for (let i = 0; i < 2; i++) {
let movieBox2 = moov(this.#trackDatas, this.#creationTime);
- let movieBoxSize = this.measureBox(movieBox2);
- mdatSize = this.measureBox(this.#mdat);
+ let movieBoxSize = this.#boxWriter.measureBox(movieBox2);
+ mdatSize = this.#boxWriter.measureBox(this.#mdat);
let currentChunkPos = this.#writer.getPos() + movieBoxSize + mdatSize;
for (let chunk of this.#finalizedChunks) {
chunk.offset = currentChunkPos;
@@ -1355,9 +1944,9 @@ var IsobmffMuxer = class extends Muxer {
if (mdatSize >= 2 ** 32) this.#mdat.largeSize = true;
}
let movieBox = moov(this.#trackDatas, this.#creationTime);
- this.writeBox(movieBox);
+ this.#boxWriter.writeBox(movieBox);
this.#mdat.size = mdatSize;
- this.writeBox(this.#mdat);
+ this.#boxWriter.writeBox(this.#mdat);
for (let chunk of this.#finalizedChunks) {
for (let sample of chunk.samples) {
assert(sample.data);
@@ -1368,27 +1957,27 @@ var IsobmffMuxer = class extends Muxer {
} else if (this.#format.options.fastStart === "fragmented") {
let startPos = this.#writer.getPos();
let mfraBox = mfra(this.#trackDatas);
- this.writeBox(mfraBox);
+ this.#boxWriter.writeBox(mfraBox);
let mfraBoxSize = this.#writer.getPos() - startPos;
this.#writer.seek(this.#writer.getPos() - 4);
- this.writeU32(mfraBoxSize);
+ this.#boxWriter.writeU32(mfraBoxSize);
} else {
assert(this.#mdat);
assert(this.#ftypSize !== null);
- let mdatPos = this.offsets.get(this.#mdat);
+ let mdatPos = this.#boxWriter.offsets.get(this.#mdat);
assert(mdatPos !== void 0);
let mdatSize = this.#writer.getPos() - mdatPos;
this.#mdat.size = mdatSize;
this.#mdat.largeSize = mdatSize >= 2 ** 32;
- this.patchBox(this.#mdat);
+ this.#boxWriter.patchBox(this.#mdat);
let movieBox = moov(this.#trackDatas, this.#creationTime);
if (typeof this.#format.options.fastStart === "object") {
this.#writer.seek(this.#ftypSize);
- this.writeBox(movieBox);
+ this.#boxWriter.writeBox(movieBox);
let remainingBytes = mdatPos - this.#writer.getPos();
- this.writeBox(free(remainingBytes));
+ this.#boxWriter.writeBox(free(remainingBytes));
} else {
- this.writeBox(movieBox);
+ this.#boxWriter.writeBox(movieBox);
}
}
}
@@ -1471,7 +2060,6 @@ var CODEC_STRING_MAP = {
av1: "V_AV1",
aac: "A_AAC",
opus: "A_OPUS",
- vorbis: "A_VORBIS",
webvtt: "S_TEXT/WEBVTT"
};
var TRACK_TYPE_MAP = {
@@ -1482,6 +2070,7 @@ var TRACK_TYPE_MAP = {
var MatroskaMuxer = class extends Muxer {
constructor(output, format) {
super(output);
+ this.timestampsMustStartAtZero = true;
this.#helper = new Uint8Array(8);
this.#helperView = new DataView(this.#helper.buffer);
/**
@@ -1742,8 +2331,8 @@ var MatroskaMuxer = class extends Muxer {
{ id: 131 /* TrackType */, data: TRACK_TYPE_MAP[trackData.type] },
// TODO Subtitle case
{ id: 134 /* CodecID */, data: CODEC_STRING_MAP[trackData.track.source.codec] },
- trackData.info.decoderConfig.description ? { id: 25506 /* CodecPrivate */, data: toUint8Array(trackData.info.decoderConfig.description) } : null,
...trackData.type === "video" ? [
+ trackData.info.decoderConfig.description ? { id: 25506 /* CodecPrivate */, data: toUint8Array(trackData.info.decoderConfig.description) } : null,
trackData.track.metadata.frameRate ? { id: 2352003 /* DefaultDuration */, data: 1e9 / trackData.track.metadata.frameRate } : null,
{ id: 224 /* Video */, data: [
{ id: 176 /* PixelWidth */, data: trackData.info.width },
@@ -1779,11 +2368,15 @@ var MatroskaMuxer = class extends Muxer {
] }
] : [],
...trackData.type === "audio" ? [
+ trackData.info.decoderConfig.description ? { id: 25506 /* CodecPrivate */, data: toUint8Array(trackData.info.decoderConfig.description) } : null,
{ id: 225 /* Audio */, data: [
{ id: 181 /* SamplingFrequency */, data: new EBMLFloat32(trackData.info.sampleRate) },
{ id: 159 /* Channels */, data: trackData.info.numberOfChannels }
// Bit depth for when PCM is a thing
] }
+ ] : [],
+ ...trackData.type === "subtitle" ? [
+ { id: 25506 /* CodecPrivate */, data: textEncoder.encode(trackData.info.config.description) }
] : []
] });
}
@@ -1826,8 +2419,6 @@ var MatroskaMuxer = class extends Muxer {
decoderConfig: meta.decoderConfig
},
chunkQueue: [],
- firstTimestamp: null,
- lastKeyFrameTimestamp: null,
lastWrittenMsTimestamp: null
};
this.#trackDatas.push(newTrackData);
@@ -1850,8 +2441,6 @@ var MatroskaMuxer = class extends Muxer {
decoderConfig: meta.decoderConfig
},
chunkQueue: [],
- firstTimestamp: null,
- lastKeyFrameTimestamp: null,
lastWrittenMsTimestamp: null
};
this.#trackDatas.push(newTrackData);
@@ -1864,16 +2453,14 @@ var MatroskaMuxer = class extends Muxer {
return existingTrackData;
}
assert(meta);
- assert(meta.decoderConfig);
+ assert(meta.config);
const newTrackData = {
track,
type: "subtitle",
info: {
- decoderConfig: meta.decoderConfig
+ config: meta.config
},
chunkQueue: [],
- firstTimestamp: null,
- lastKeyFrameTimestamp: null,
lastWrittenMsTimestamp: null
};
this.#trackDatas.push(newTrackData);
@@ -1884,7 +2471,8 @@ var MatroskaMuxer = class extends Muxer {
const trackData = this.#getVideoTrackData(track, meta);
let data = new Uint8Array(chunk.byteLength);
chunk.copyTo(data);
- let videoChunk = this.#createInternalChunk(trackData, data, chunk.timestamp, chunk.duration ?? 0, chunk.type);
+ let timestamp = this.validateAndNormalizeTimestamp(trackData.track, chunk.timestamp, chunk.type === "key");
+ let videoChunk = this.#createInternalChunk(data, timestamp, (chunk.duration ?? 0) / 1e6, chunk.type);
if (track.source.codec === "vp9") this.#fixVP9ColorSpace(trackData, videoChunk);
trackData.chunkQueue.push(videoChunk);
this.#interleaveChunks();
@@ -1893,21 +2481,35 @@ var MatroskaMuxer = class extends Muxer {
const trackData = this.#getAudioTrackData(track, meta);
let data = new Uint8Array(chunk.byteLength);
chunk.copyTo(data);
- let audioChunk = this.#createInternalChunk(trackData, data, chunk.timestamp, chunk.duration ?? 0, chunk.type);
+ let timestamp = this.validateAndNormalizeTimestamp(trackData.track, chunk.timestamp, chunk.type === "key");
+ let audioChunk = this.#createInternalChunk(data, timestamp, (chunk.duration ?? 0) / 1e6, chunk.type);
trackData.chunkQueue.push(audioChunk);
this.#interleaveChunks();
}
- addEncodedSubtitleChunk(track, chunk, meta) {
+ addSubtitleCue(track, cue, meta) {
const trackData = this.#getSubtitleTrackData(track, meta);
- let subtitleChunk = this.#createInternalChunk(trackData, chunk.body, chunk.timestamp, chunk.duration, "key", chunk.additions);
+ const timestamp = this.validateAndNormalizeTimestamp(trackData.track, 1e6 * cue.timestamp, true);
+ let bodyText = cue.text;
+ const timestampMs = Math.floor(timestamp * 1e3);
+ inlineTimestampRegex.lastIndex = 0;
+ bodyText = bodyText.replace(inlineTimestampRegex, (match) => {
+ let time = parseSubtitleTimestamp(match.slice(1, -1));
+ let offsetTime = time - timestampMs;
+ return `<${formatSubtitleTimestamp(offsetTime)}>`;
+ });
+ const body = textEncoder.encode(bodyText);
+ const additions = `${cue.settings ?? ""}
+${cue.identifier ?? ""}
+${cue.notes ?? ""}`;
+ let subtitleChunk = this.#createInternalChunk(body, timestamp, cue.duration, "key", additions.trim() ? textEncoder.encode(additions) : null);
trackData.chunkQueue.push(subtitleChunk);
this.#interleaveChunks();
}
#interleaveChunks() {
- let openTrackCount = 0;
- for (const trackData of this.#trackDatas) if (!trackData.track.source.closed) openTrackCount++;
- if (this.#trackDatas.length < openTrackCount) {
- return;
+ for (const track of this.output.tracks) {
+ if (!track.source.closed && !this.#trackDatas.some((x) => x.track === track)) {
+ return;
+ }
}
outer:
while (true) {
@@ -1962,34 +2564,16 @@ var MatroskaMuxer = class extends Muxer {
writeBits(chunk.data, i + 0, i + 3, colorSpaceID);
}
/** Converts a read-only external chunk into an internal one for easier use. */
- #createInternalChunk(trackData, data, timestamp, duration, type, additions = null) {
- let adjustedTimestamp = this.#validateTimestamp(trackData, timestamp, type === "key");
+ #createInternalChunk(data, timestamp, duration, type, additions = null) {
let internalChunk = {
data,
type,
- timestamp: adjustedTimestamp,
- duration: duration / 1e6,
+ timestamp,
+ duration,
additions
};
return internalChunk;
}
- #validateTimestamp(trackData, timestamp, isKeyFrame) {
- let timestampInSeconds = timestamp / 1e6;
- if (timestampInSeconds < 0) {
- throw new Error(`Timestamps must be non-negative (got ${timestampInSeconds}s).`);
- }
- if (trackData.firstTimestamp === null) {
- trackData.firstTimestamp = timestampInSeconds;
- }
- timestampInSeconds -= trackData.firstTimestamp;
- if (trackData.lastKeyFrameTimestamp !== null && timestampInSeconds < trackData.lastKeyFrameTimestamp) {
- throw new Error(`Timestamp cannot be before last key frame's timestamp (got ${timestampInSeconds}s, last key frame at ${trackData.lastKeyFrameTimestamp}s).`);
- }
- if (isKeyFrame) {
- trackData.lastKeyFrameTimestamp = timestampInSeconds;
- }
- return timestampInSeconds;
- }
/** Writes a block containing media data to the file. */
#writeBlock(trackData, chunk) {
if (!this.#segment) {
@@ -1998,6 +2582,9 @@ var MatroskaMuxer = class extends Muxer {
}
let msTimestamp = Math.floor(1e3 * chunk.timestamp);
const keyFrameQueuedEverywhere = this.#trackDatas.every((otherTrackData) => {
+ if (otherTrackData.track.source.closed) {
+ return true;
+ }
if (trackData === otherTrackData) {
return chunk.type === "key";
}
@@ -2036,8 +2623,13 @@ var MatroskaMuxer = class extends Muxer {
chunk.data
] },
chunk.type === "delta" ? { id: 251 /* ReferenceBlock */, data: new EBMLSignedInt(trackData.lastWrittenMsTimestamp - msTimestamp) } : null,
- msDuration > 0 ? { id: 155 /* BlockDuration */, data: msDuration } : null,
- chunk.additions ? { id: 30113 /* BlockAdditions */, data: chunk.additions } : null
+ chunk.additions ? { id: 30113 /* BlockAdditions */, data: [
+ { id: 166 /* BlockMore */, data: [
+ { id: 165 /* BlockAdditional */, data: chunk.additions },
+ { id: 238 /* BlockAddID */, data: 1 }
+ ] }
+ ] } : null,
+ msDuration > 0 ? { id: 155 /* BlockDuration */, data: msDuration } : null
] };
this.writeEBML(blockGroup);
}
@@ -2229,110 +2821,12 @@ var buildAudioCodecString = (codec, numberOfChannels, sampleRate) => {
throw new Error(`Unhandled codec '${codec}'.`);
};
-// 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;
-var preambleStartRegex = /^WEBVTT.*?\n{2}/;
-var timestampRegex = /(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})/;
-var inlineTimestampRegex = /<(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})>/g;
-var textEncoder = new TextEncoder();
-var SubtitleEncoder = class {
- #options;
- #config = null;
- #preambleBytes = null;
- #preambleEmitted = false;
- constructor(options) {
- this.#options = options;
- }
- configure(config) {
- if (config.codec !== "webvtt") {
- throw new Error("Codec must be 'webvtt'.");
- }
- this.#config = config;
- }
- encode(text) {
- if (!this.#config) {
- throw new Error("Encoder not configured.");
- }
- text = text.replace("\r\n", "\n").replace("\r", "\n");
- cueBlockHeaderRegex.lastIndex = 0;
- let match;
- if (!this.#preambleBytes) {
- if (!preambleStartRegex.test(text)) {
- let error = new Error("WebVTT preamble incorrect.");
- this.#options.error(error);
- throw error;
- }
- match = cueBlockHeaderRegex.exec(text);
- let preamble = text.slice(0, match?.index ?? text.length).trimEnd();
- if (!preamble) {
- let error = new Error("No WebVTT preamble provided.");
- this.#options.error(error);
- throw error;
- }
- this.#preambleBytes = textEncoder.encode(preamble);
- if (match) {
- text = text.slice(match.index);
- cueBlockHeaderRegex.lastIndex = 0;
- }
- }
- while (match = cueBlockHeaderRegex.exec(text)) {
- let notes = text.slice(0, match.index);
- let cueIdentifier = match[1] || "";
- let matchEnd = match.index + match[0].length;
- let bodyStart = text.indexOf("\n", matchEnd) + 1;
- let cueSettings = text.slice(matchEnd, bodyStart).trim();
- let bodyEnd = text.indexOf("\n\n", matchEnd);
- if (bodyEnd === -1) bodyEnd = text.length;
- let startTime = this.#parseTimestamp(match[2]);
- let endTime = this.#parseTimestamp(match[3]);
- let duration = endTime - startTime;
- let body = text.slice(bodyStart, bodyEnd);
- let additions = `${cueSettings}
-${cueIdentifier}
-${notes}`;
- inlineTimestampRegex.lastIndex = 0;
- body = body.replace(inlineTimestampRegex, (match2) => {
- let time = this.#parseTimestamp(match2.slice(1, -1));
- let offsetTime = time - startTime;
- return `<${this.#formatTimestamp(offsetTime)}>`;
- });
- text = text.slice(bodyEnd).trimStart();
- cueBlockHeaderRegex.lastIndex = 0;
- let chunk = {
- body: textEncoder.encode(body),
- additions: additions.trim() === "" ? null : textEncoder.encode(additions),
- timestamp: startTime * 1e3,
- duration: duration * 1e3
- };
- let meta = {};
- if (!this.#preambleEmitted) {
- meta.decoderConfig = {
- description: this.#preambleBytes
- };
- this.#preambleEmitted = true;
- }
- this.#options.output(chunk, meta);
- }
- }
- #parseTimestamp(string) {
- let match = timestampRegex.exec(string);
- if (!match) throw new Error("Expected match.");
- return 60 * 60 * 1e3 * Number(match[1] || "0") + 60 * 1e3 * Number(match[2]) + 1e3 * Number(match[3]) + Number(match[4]);
- }
- #formatTimestamp(timestamp) {
- let hours = Math.floor(timestamp / (60 * 60 * 1e3));
- let minutes = Math.floor(timestamp % (60 * 60 * 1e3) / (60 * 1e3));
- let seconds = Math.floor(timestamp % (60 * 1e3) / 1e3);
- let milliseconds = timestamp % 1e3;
- return hours.toString().padStart(2, "0") + ":" + minutes.toString().padStart(2, "0") + ":" + seconds.toString().padStart(2, "0") + "." + milliseconds.toString().padStart(3, "0");
- }
-};
-
// src/source.ts
var MediaSource = class {
constructor() {
this.connectedTrack = null;
this.closed = false;
+ this.offsetTimestamps = false;
}
ensureValidDigest() {
if (!this.connectedTrack) {
@@ -2417,7 +2911,9 @@ var VideoEncoderWrapper = class {
codec: buildVideoCodecString(this.codecConfig.codec, videoFrame.codedWidth, videoFrame.codedHeight),
width: videoFrame.codedWidth,
height: videoFrame.codedHeight,
- bitrate: this.codecConfig.bitrate
+ bitrate: this.codecConfig.bitrate,
+ framerate: this.source.connectedTrack?.metadata.frameRate,
+ latencyMode: this.codecConfig.latencyMode
});
}
async flush() {
@@ -2445,7 +2941,8 @@ var CanvasSource = class extends VideoSource {
digest(timestamp, duration = 0) {
const frame = new VideoFrame(this.canvas, {
timestamp: Math.round(1e6 * timestamp),
- duration: Math.round(1e6 * duration)
+ duration: Math.round(1e6 * duration),
+ alpha: "discard"
});
this.encoder.digest(frame);
frame.close();
@@ -2459,6 +2956,7 @@ var MediaStreamVideoTrackSource = class extends VideoSource {
super(codecConfig.codec);
this.track = track;
this.abortController = null;
+ this.offsetTimestamps = true;
this.encoder = new VideoEncoderWrapper(this, codecConfig);
}
start() {
@@ -2582,6 +3080,7 @@ var MediaStreamAudioTrackSource = class extends AudioSource {
super(codecConfig.codec);
this.track = track;
this.abortController = null;
+ this.offsetTimestamps = true;
this.encoder = new AudioEncoderWrapper(this, codecConfig);
}
start() {
@@ -2618,297 +3117,16 @@ var SubtitleSource = class extends MediaSource {
var TextSubtitleSource = class extends SubtitleSource {
constructor(codec) {
super(codec);
- this.encoder = new SubtitleEncoder({
- output: (chunk, metadata) => this.connectedTrack?.output.muxer.addEncodedSubtitleChunk(this.connectedTrack, chunk, metadata),
+ this.parser = new SubtitleParser({
+ codec,
+ output: (cue, metadata) => this.connectedTrack?.output.muxer.addSubtitleCue(this.connectedTrack, cue, metadata),
error: (error) => console.error(error)
// TODO
});
- this.encoder.configure({ codec });
}
digest(text) {
this.ensureValidDigest();
- this.encoder.encode(text);
- }
-};
-
-// src/writer.ts
-var Writer = class {
-};
-var ArrayBufferTargetWriter = class extends Writer {
- #pos = 0;
- #target;
- #buffer = new ArrayBuffer(2 ** 16);
- #bytes = new Uint8Array(this.#buffer);
- #maxPos = 0;
- constructor(target) {
- super();
- this.#target = target;
- }
- #ensureSize(size) {
- let newLength = this.#buffer.byteLength;
- while (newLength < size) newLength *= 2;
- if (newLength === this.#buffer.byteLength) return;
- let newBuffer = new ArrayBuffer(newLength);
- let newBytes = new Uint8Array(newBuffer);
- newBytes.set(this.#bytes, 0);
- this.#buffer = newBuffer;
- this.#bytes = newBytes;
- }
- write(data) {
- this.#ensureSize(this.#pos + data.byteLength);
- this.#bytes.set(data, this.#pos);
- this.#pos += data.byteLength;
- this.#maxPos = Math.max(this.#maxPos, this.#pos);
- }
- seek(newPos) {
- this.#pos = newPos;
- }
- getPos() {
- return this.#pos;
- }
- flush() {
- }
- finalize() {
- this.#ensureSize(this.#pos);
- this.#target.buffer = this.#buffer.slice(0, Math.max(this.#maxPos, this.#pos));
- }
-};
-var StreamTargetWriter = class extends Writer {
- #pos = 0;
- #target;
- #sections = [];
- constructor(target) {
- super();
- this.#target = target;
- }
- write(data) {
- this.#sections.push({
- data: data.slice(),
- start: this.#pos
- });
- this.#pos += data.byteLength;
- }
- seek(newPos) {
- this.#pos = newPos;
- }
- getPos() {
- return this.#pos;
- }
- flush() {
- if (this.#sections.length === 0) return;
- let chunks = [];
- let sorted = [...this.#sections].sort((a, b) => a.start - b.start);
- chunks.push({
- start: sorted[0].start,
- size: sorted[0].data.byteLength
- });
- for (let i = 1; i < sorted.length; i++) {
- let lastChunk = chunks[chunks.length - 1];
- let section = sorted[i];
- if (section.start <= lastChunk.start + lastChunk.size) {
- lastChunk.size = Math.max(lastChunk.size, section.start + section.data.byteLength - lastChunk.start);
- } else {
- chunks.push({
- start: section.start,
- size: section.data.byteLength
- });
- }
- }
- for (let chunk of chunks) {
- chunk.data = new Uint8Array(chunk.size);
- for (let section of this.#sections) {
- if (chunk.start <= section.start && section.start < chunk.start + chunk.size) {
- chunk.data.set(section.data, section.start - chunk.start);
- }
- }
- this.#target.options.onData?.(chunk.data, chunk.start);
- }
- this.#sections.length = 0;
- }
- finalize() {
- }
-};
-var DEFAULT_CHUNK_SIZE = 2 ** 24;
-var MAX_CHUNKS_AT_ONCE = 2;
-var ChunkedStreamTargetWriter = class extends Writer {
- #pos = 0;
- #target;
- #chunkSize;
- /**
- * The data is divided up into fixed-size chunks, whose contents are first filled in RAM and then flushed out.
- * A chunk is flushed if all of its contents have been written.
- */
- #chunks = [];
- constructor(target) {
- super();
- this.#target = target;
- this.#chunkSize = target.options?.chunkSize ?? DEFAULT_CHUNK_SIZE;
- if (!Number.isInteger(this.#chunkSize) || this.#chunkSize < 2 ** 10) {
- throw new Error("Invalid StreamTarget options: chunkSize must be an integer not smaller than 1024.");
- }
- }
- write(data) {
- this.#writeDataIntoChunks(data, this.#pos);
- this.#flushChunks();
- this.#pos += data.byteLength;
- }
- seek(newPos) {
- this.#pos = newPos;
- }
- getPos() {
- return this.#pos;
- }
- #writeDataIntoChunks(data, position) {
- let chunkIndex = this.#chunks.findIndex((x) => x.start <= position && position < x.start + this.#chunkSize);
- if (chunkIndex === -1) chunkIndex = this.#createChunk(position);
- let chunk = this.#chunks[chunkIndex];
- let relativePosition = position - chunk.start;
- let toWrite = data.subarray(0, Math.min(this.#chunkSize - relativePosition, data.byteLength));
- chunk.data.set(toWrite, relativePosition);
- let section = {
- start: relativePosition,
- end: relativePosition + toWrite.byteLength
- };
- this.#insertSectionIntoChunk(chunk, section);
- if (chunk.written[0].start === 0 && chunk.written[0].end === this.#chunkSize) {
- chunk.shouldFlush = true;
- }
- if (this.#chunks.length > MAX_CHUNKS_AT_ONCE) {
- for (let i = 0; i < this.#chunks.length - 1; i++) {
- this.#chunks[i].shouldFlush = true;
- }
- this.#flushChunks();
- }
- if (toWrite.byteLength < data.byteLength) {
- this.#writeDataIntoChunks(data.subarray(toWrite.byteLength), position + toWrite.byteLength);
- }
- }
- #insertSectionIntoChunk(chunk, section) {
- let low = 0;
- let high = chunk.written.length - 1;
- let index = -1;
- while (low <= high) {
- let mid = Math.floor(low + (high - low + 1) / 2);
- if (chunk.written[mid].start <= section.start) {
- low = mid + 1;
- index = mid;
- } else {
- high = mid - 1;
- }
- }
- chunk.written.splice(index + 1, 0, section);
- if (index === -1 || chunk.written[index].end < section.start) index++;
- while (index < chunk.written.length - 1 && chunk.written[index].end >= chunk.written[index + 1].start) {
- chunk.written[index].end = Math.max(chunk.written[index].end, chunk.written[index + 1].end);
- chunk.written.splice(index + 1, 1);
- }
- }
- #createChunk(includesPosition) {
- let start = Math.floor(includesPosition / this.#chunkSize) * this.#chunkSize;
- let chunk = {
- start,
- data: new Uint8Array(this.#chunkSize),
- written: [],
- shouldFlush: false
- };
- this.#chunks.push(chunk);
- this.#chunks.sort((a, b) => a.start - b.start);
- return this.#chunks.indexOf(chunk);
- }
- #flushChunks(force = false) {
- for (let i = 0; i < this.#chunks.length; i++) {
- let chunk = this.#chunks[i];
- if (!chunk.shouldFlush && !force) continue;
- for (let section of chunk.written) {
- this.#target.options.onData?.(
- chunk.data.subarray(section.start, section.end),
- chunk.start + section.start
- );
- }
- this.#chunks.splice(i--, 1);
- }
- }
- flush() {
- }
- finalize() {
- this.#flushChunks(true);
- }
-};
-var FileSystemWritableFileStreamTargetWriter = class extends ChunkedStreamTargetWriter {
- constructor(target) {
- super(new StreamTarget({
- onData: (data, position) => target.stream.write({
- type: "write",
- data,
- position
- }),
- chunkSize: target.options?.chunkSize
- }));
- }
-};
-
-// src/target.ts
-var Target = class {
- constructor() {
- this.output = null;
- }
-};
-var ArrayBufferTarget2 = 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 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");
- }
- }
- }
- createWriter() {
- return new FileSystemWritableFileStreamTargetWriter(this);
+ this.parser.parse(text);
}
};
export {
diff --git a/src/isobmff/isobmff_boxes.ts b/src/isobmff/isobmff_boxes.ts
index dd6cded..e464ac0 100644
--- a/src/isobmff/isobmff_boxes.ts
+++ b/src/isobmff/isobmff_boxes.ts
@@ -1,6 +1,97 @@
-import { toUint8Array, assert, isU32, last, TransformationMatrix } from '../misc';
-import { AudioCodec, AudioSource, VideoCodec, VideoSource } from '../source';
-import { GLOBAL_TIMESCALE, intoTimescale, IsobmffAudioTrackData, IsobmffTrackData, IsobmffVideoTrackData, Sample } from './isobmff_muxer';
+import { toUint8Array, assert, isU32, last, TransformationMatrix, textEncoder } from '../misc';
+import { AudioCodec, AudioSource, SubtitleCodec, VideoCodec, VideoSource } from '../source';
+import { formatSubtitleTimestamp } from '../subtitles';
+import { Writer } from '../writer';
+import { GLOBAL_TIMESCALE, intoTimescale, IsobmffAudioTrackData, IsobmffSubtitleTrackData, IsobmffTrackData, IsobmffVideoTrackData, Sample } from './isobmff_muxer';
+
+export class IsobmffBoxWriter {
+ private helper = new Uint8Array(8);
+ private helperView = new DataView(this.helper.buffer);
+
+ /**
+ * Stores the position from the start of the file to where boxes elements have been written. This is used to
+ * rewrite/edit elements that were already added before, and to measure sizes of things.
+ */
+ offsets = new WeakMap();
+
+ constructor(private writer: Writer) {}
+
+ writeU32(value: number) {
+ this.helperView.setUint32(0, value, false);
+ this.writer.write(this.helper.subarray(0, 4));
+ }
+
+ writeU64(value: number) {
+ this.helperView.setUint32(0, Math.floor(value / 2**32), false);
+ this.helperView.setUint32(4, value, false);
+ this.writer.write(this.helper.subarray(0, 8));
+ }
+
+ writeAscii(text: string) {
+ for (let i = 0; i < text.length; i++) {
+ this.helperView.setUint8(i % 8, text.charCodeAt(i));
+ if (i % 8 === 7) this.writer.write(this.helper);
+ }
+
+ if (text.length % 8 !== 0) {
+ this.writer.write(this.helper.subarray(0, text.length % 8));
+ }
+ }
+
+ writeBox(box: Box) {
+ this.offsets.set(box, this.writer.getPos());
+
+ if (box.contents && !box.children) {
+ this.writeBoxHeader(box, box.size ?? box.contents.byteLength + 8);
+ this.writer.write(box.contents);
+ } else {
+ let startPos = this.writer.getPos();
+ this.writeBoxHeader(box, 0);
+
+ if (box.contents) this.writer.write(box.contents);
+ if (box.children) for (let child of box.children) if (child) this.writeBox(child);
+
+ let endPos = this.writer.getPos();
+ let size = box.size ?? endPos - startPos;
+ this.writer.seek(startPos);
+ this.writeBoxHeader(box, size);
+ this.writer.seek(endPos);
+ }
+ }
+
+ writeBoxHeader(box: Box, size: number) {
+ this.writeU32(box.largeSize ? 1 : size);
+ this.writeAscii(box.type);
+ if (box.largeSize) this.writeU64(size);
+ }
+
+ measureBoxHeader(box: Box) {
+ return 8 + (box.largeSize ? 8 : 0);
+ }
+
+ patchBox(box: Box) {
+ const boxOffset = this.offsets.get(box);
+ assert(boxOffset !== undefined);
+
+ let endPos = this.writer.getPos();
+ this.writer.seek(boxOffset);
+ this.writeBox(box);
+ this.writer.seek(endPos);
+ }
+
+ measureBox(box: Box) {
+ if (box.contents && !box.children) {
+ let headerSize = this.measureBoxHeader(box);
+ return headerSize + box.contents.byteLength;
+ } else {
+ let result = this.measureBoxHeader(box);
+ if (box.contents) result += box.contents.byteLength;
+ if (box.children) for (let child of box.children) if (child) result += this.measureBox(child);
+
+ return result;
+ }
+ }
+}
let bytes = new Uint8Array(8);
let view = new DataView(bytes.buffer);
@@ -248,7 +339,7 @@ export const tkhd = (
u32OrU64(durationInGlobalTimescale), // Duration
Array(8).fill(0), // Reserved
u16(0), // Layer
- u16(0), // Alternate group
+ u16(trackData.track.id), // Alternate group
fixed_8_8(trackData.type === 'audio' ? 1 : 0), // Volume
u16(0), // Reserved
matrixToBytes(matrix), // Matrix
@@ -260,7 +351,7 @@ export const tkhd = (
/** Media Box: Describes and define a track's media type and sample data. */
export const mdia = (trackData: IsobmffTrackData, creationTime: number) => box('mdia', undefined, [
mdhd(trackData, creationTime),
- hdlr(trackData.type === 'video' ? 'vide' : 'soun'),
+ hdlr(trackData),
minf(trackData)
]);
@@ -288,15 +379,26 @@ export const mdhd = (
]);
};
+const TRACK_TYPE_TO_COMPONENT_SUBTYPE: Record = {
+ video: 'vide',
+ audio: 'soun',
+ subtitle: 'text'
+};
+
+const TRACK_TYPE_TO_HANDLER_NAME: Record = {
+ video: 'VideoHandler',
+ audio: 'SoundHandler',
+ subtitle: 'TextHandler'
+};
+
/** Handler Reference Box: Specifies the media handler component that is to be used to interpret the media's data. */
-export const hdlr = (componentSubtype: string) => fullBox('hdlr', 0, 0, [
+export const hdlr = (trackData: IsobmffTrackData) => fullBox('hdlr', 0, 0, [
ascii('mhlr'), // Component type
- ascii(componentSubtype), // Component subtype
+ ascii(TRACK_TYPE_TO_COMPONENT_SUBTYPE[trackData.type]), // Component subtype
u32(0), // Component manufacturer
u32(0), // Component flags
u32(0), // Component flags mask
- // TODO:
- ascii('mp4-muxer-hdlr', true) // Component name
+ ascii(TRACK_TYPE_TO_HANDLER_NAME[trackData.type], true) // Component name
]);
/**
@@ -304,7 +406,7 @@ export const hdlr = (componentSubtype: string) => fullBox('hdlr', 0, 0, [
* information to map from media time to media data and to process the media data.
*/
export const minf = (trackData: IsobmffTrackData) => box('minf', undefined, [
- trackData.type === 'video' ? vmhd() : smhd(),
+ TRACK_TYPE_TO_HEADER_BOX[trackData.type](),
dinf(),
stbl(trackData)
]);
@@ -323,6 +425,15 @@ export const smhd = () => fullBox('smhd', 0, 0, [
u16(0) // Reserved
]);
+/** Null Media Header Box. */
+export const nmhd = () => fullBox('nmhd', 0, 0);
+
+const TRACK_TYPE_TO_HEADER_BOX: Record Box> = {
+ video: vmhd,
+ audio: smhd,
+ subtitle: nmhd
+};
+
/**
* Data Information Box: Contains information specifying the data handler component that provides access to the
* media data. The data handler component uses the Data Information Box to interpret the media's data.
@@ -365,19 +476,34 @@ export const stbl = (trackData: IsobmffTrackData) => {
* Sample Description Box: Stores information that allows you to decode samples in the media. The data stored in the
* sample description varies, depending on the media type.
*/
-export const stsd = (trackData: IsobmffTrackData) => fullBox('stsd', 0, 0, [
- u32(1) // Entry count
-], [
- trackData.type === 'video'
- ? videoSampleDescription(
+export const stsd = (trackData: IsobmffTrackData) => {
+ let sampleDescription: Box;
+
+ if (trackData.type === 'video') {
+ sampleDescription = videoSampleDescription(
VIDEO_CODEC_TO_BOX_NAME[trackData.track.source.codec],
- trackData as IsobmffVideoTrackData
+ trackData
)
- : soundSampleDescription(
+ } else if (trackData.type === 'audio') {
+ sampleDescription = soundSampleDescription(
AUDIO_CODEC_TO_BOX_NAME[trackData.track.source.codec],
- trackData as IsobmffAudioTrackData
- )
-]);
+ trackData
+ );
+ } else if (trackData.type === 'subtitle') {
+ sampleDescription = subtitleSampleDescription(
+ SUBTITLE_CODEC_TO_BOX_NAME[trackData.track.source.codec],
+ trackData
+ );
+ }
+
+ assert(sampleDescription!);
+
+ return fullBox('stsd', 0, 0, [
+ u32(1) // Entry count
+ ], [
+ sampleDescription
+ ]);
+};
/** Video Sample Description Box: Contains information that defines how to interpret video media data. */
export const videoSampleDescription = (
@@ -400,6 +526,7 @@ export const videoSampleDescription = (
i16(0xffff) // Pre-defined
], [
VIDEO_CODEC_TO_CONFIGURATION_BOX[trackData.track.source.codec](trackData)
+ // TODO colr
]);
// TODO: All muxers should ensure that the decoder config description is provided for the codecs that require it. This
@@ -550,6 +677,24 @@ export const dOps = (trackData: IsobmffAudioTrackData) => {
]);
};
+export const subtitleSampleDescription = (
+ compressionType: string,
+ trackData: IsobmffSubtitleTrackData
+) => box(compressionType, [
+ Array(6).fill(0), // Reserved
+ u16(1), // Data reference index
+], [
+ SUBTITLE_CODEC_TO_CONFIGURATION_BOX[trackData.track.source.codec](trackData)
+])
+
+export const vttC = (trackData: IsobmffSubtitleTrackData) => box('vttC', [
+ ...textEncoder.encode(trackData.info.config.description)
+]);
+
+export const txtC = (textConfig: Uint8Array) => fullBox('txtC', 0, 0, [
+ ...textConfig, 0 // Text config (null-terminated)
+]);
+
/**
* Time-To-Sample Box: Stores duration information for a media's samples, providing a mapping from a time in a media
* to the corresponding data sample. The table is compact, meaning that consecutive samples with the same time delta
@@ -817,6 +962,21 @@ export const mfro = () => {
]);
};
+/** VTT Empty Cue Box */
+export const vtte = () => box('vtte');
+
+/** VTT Cue Box */
+export const vttc = (payload: string, timestamp: number | null, identifier: string | null, settings: string | null, sourceId: number | null) => box('vttc', undefined, [
+ sourceId !== null ? box('vsid', [i32(sourceId)]) : null,
+ identifier !== null ? box('iden', [...textEncoder.encode(identifier)]) : null,
+ timestamp !== null ? box('ctim', [...textEncoder.encode(formatSubtitleTimestamp(timestamp))]) : null,
+ settings !== null ? box('sttg', [...textEncoder.encode(settings)]) : null,
+ box('payl', [...textEncoder.encode(payload)])
+]);
+
+/** VTT Additional Text Box */
+export const vtta = (notes: string) => box('vtta', [...textEncoder.encode(notes)]);
+
const VIDEO_CODEC_TO_BOX_NAME: Record = {
'avc': 'avc1',
'hevc': 'hvc1',
@@ -842,3 +1002,11 @@ const AUDIO_CODEC_TO_CONFIGURATION_BOX: Record = {
+ 'webvtt': 'wvtt'
+};
+
+const SUBTITLE_CODEC_TO_CONFIGURATION_BOX: Record Box | null> = {
+ 'webvtt': vttC
+};
\ No newline at end of file
diff --git a/src/isobmff/isobmff_muxer.ts b/src/isobmff/isobmff_muxer.ts
index 8ea7ee4..e4e687a 100644
--- a/src/isobmff/isobmff_muxer.ts
+++ b/src/isobmff/isobmff_muxer.ts
@@ -1,9 +1,11 @@
-import { Box, free, ftyp, mdat, mfra, moof, moov } from './isobmff_boxes';
+import { Box, free, ftyp, IsobmffBoxWriter, mdat, mfra, moof, moov, vtta, vttc, vtte } from './isobmff_boxes';
import { Muxer } from '../muxer';
-import { Output, OutputAudioTrack, OutputTrack, OutputVideoTrack } from '../output';
+import { Output, OutputAudioTrack, OutputSubtitleTrack, OutputTrack, OutputVideoTrack } from '../output';
import { 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';
export const GLOBAL_TIMESCALE = 1000;
const TIMESTAMP_OFFSET = 2_082_844_800; // Seconds between Jan 1 1904 and Jan 1 1970
@@ -32,9 +34,6 @@ export type IsobmffTrackData = {
sampleQueue: Sample[], // For fragmented files
timestampProcessingQueue: Sample[],
- firstTimestamp: number | null,
- lastKeyFrameTimestamp: number | null,
-
timeToSampleTable: { sampleCount: number, sampleDelta: number }[];
compositionTimeOffsetTable: { sampleCount: number, sampleCompositionTimeOffset: number }[];
lastTimescaleUnits: number | null,
@@ -62,10 +61,21 @@ export type IsobmffTrackData = {
sampleRate: number,
decoderConfig: AudioDecoderConfig
}
+} | {
+ track: OutputSubtitleTrack,
+ type: 'subtitle',
+ info: {
+ config: SubtitleConfig
+ },
+ lastCueEndTimestamp: number,
+ cueQueue: SubtitleCue[],
+ nextSourceId: number,
+ cueToSourceId: WeakMap
});
export type IsobmffVideoTrackData = IsobmffTrackData & { type: 'video' };
export type IsobmffAudioTrackData = IsobmffTrackData & { type: 'audio' };
+export type IsobmffSubtitleTrackData = IsobmffTrackData & { type: 'subtitle' };
export const intoTimescale = (timeInSeconds: number, timescale: number, round = true) => {
let value = timeInSeconds * timescale;
@@ -73,16 +83,15 @@ export const intoTimescale = (timeInSeconds: number, timescale: number, round =
};
export class IsobmffMuxer extends Muxer {
- #writer: Writer;
- #format: Mp4OutputFormat;
- #helper = new Uint8Array(8);
- #helperView = new DataView(this.#helper.buffer);
+ override timestampsMustStartAtZero = true;
- /**
- * Stores the position from the start of the file to where boxes elements have been written. This is used to
- * rewrite/edit elements that were already added before, and to measure sizes of things.
- */
- offsets = new WeakMap();
+ #writer: Writer;
+ #boxWriter: IsobmffBoxWriter;
+ #format: Mp4OutputFormat;
+
+ #auxTarget = new ArrayBufferTarget();
+ #auxWriter = this.#auxTarget.createWriter();
+ #auxBoxWriter = new IsobmffBoxWriter(this.#auxWriter);
#ftypSize: number | null = null;
#mdat: Box | null = null;
@@ -98,90 +107,15 @@ export class IsobmffMuxer extends Muxer {
super(output);
this.#writer = output.writer;
+ this.#boxWriter = new IsobmffBoxWriter(this.#writer);
this.#format = format;
}
- writeU32(value: number) {
- this.#helperView.setUint32(0, value, false);
- this.#writer.write(this.#helper.subarray(0, 4));
- }
-
- writeU64(value: number) {
- this.#helperView.setUint32(0, Math.floor(value / 2**32), false);
- this.#helperView.setUint32(4, value, false);
- this.#writer.write(this.#helper.subarray(0, 8));
- }
-
- writeAscii(text: string) {
- for (let i = 0; i < text.length; i++) {
- this.#helperView.setUint8(i % 8, text.charCodeAt(i));
- if (i % 8 === 7) this.#writer.write(this.#helper);
- }
-
- if (text.length % 8 !== 0) {
- this.#writer.write(this.#helper.subarray(0, text.length % 8));
- }
- }
-
- writeBox(box: Box) {
- this.offsets.set(box, this.#writer.getPos());
-
- if (box.contents && !box.children) {
- this.writeBoxHeader(box, box.size ?? box.contents.byteLength + 8);
- this.#writer.write(box.contents);
- } else {
- let startPos = this.#writer.getPos();
- this.writeBoxHeader(box, 0);
-
- if (box.contents) this.#writer.write(box.contents);
- if (box.children) for (let child of box.children) if (child) this.writeBox(child);
-
- let endPos = this.#writer.getPos();
- let size = box.size ?? endPos - startPos;
- this.#writer.seek(startPos);
- this.writeBoxHeader(box, size);
- this.#writer.seek(endPos);
- }
- }
-
- writeBoxHeader(box: Box, size: number) {
- this.writeU32(box.largeSize ? 1 : size);
- this.writeAscii(box.type);
- if (box.largeSize) this.writeU64(size);
- }
-
- measureBoxHeader(box: Box) {
- return 8 + (box.largeSize ? 8 : 0);
- }
-
- patchBox(box: Box) {
- const boxOffset = this.offsets.get(box);
- assert(boxOffset !== undefined);
-
- let endPos = this.#writer.getPos();
- this.#writer.seek(boxOffset);
- this.writeBox(box);
- this.#writer.seek(endPos);
- }
-
- measureBox(box: Box) {
- if (box.contents && !box.children) {
- let headerSize = this.measureBoxHeader(box);
- return headerSize + box.contents.byteLength;
- } else {
- let result = this.measureBoxHeader(box);
- if (box.contents) result += box.contents.byteLength;
- if (box.children) for (let child of box.children) if (child) result += this.measureBox(child);
-
- return result;
- }
- }
-
start() {
const holdsAvc = this.output.tracks.some(x => x.type === 'video' && x.source.codec === 'avc');
// Write the header
- this.writeBox(ftyp({
+ this.#boxWriter.writeBox(ftyp({
holdsAvc: holdsAvc,
fragmented: this.#format.options.fastStart === 'fragmented'
}));
@@ -199,7 +133,7 @@ export class IsobmffMuxer extends Muxer {
}
this.#mdat = mdat(true); // Reserve large size by default, can refine this when finalizing.
- this.writeBox(this.#mdat);
+ this.#boxWriter.writeBox(this.#mdat);
}
this.#writer.flush();
@@ -240,7 +174,7 @@ export class IsobmffMuxer extends Muxer {
#getVideoTrackData(track: OutputVideoTrack, meta?: EncodedVideoChunkMetadata) {
const existingTrackData = this.#trackDatas.find(x => x.track === track);
if (existingTrackData) {
- return existingTrackData;
+ return existingTrackData as IsobmffVideoTrackData;
}
// TODO Make proper errors for these
@@ -249,7 +183,7 @@ export class IsobmffMuxer extends Muxer {
assert(meta.decoderConfig.codedWidth !== undefined);
assert(meta.decoderConfig.codedHeight !== undefined);
- const newTrackData: IsobmffTrackData = {
+ const newTrackData: IsobmffVideoTrackData = {
track,
type: 'video',
info: {
@@ -261,8 +195,6 @@ export class IsobmffMuxer extends Muxer {
samples: [],
sampleQueue: [],
timestampProcessingQueue: [],
- firstTimestamp: null,
- lastKeyFrameTimestamp: null,
timeToSampleTable: [],
compositionTimeOffsetTable: [],
lastTimescaleUnits: null,
@@ -281,14 +213,14 @@ export class IsobmffMuxer extends Muxer {
#getAudioTrackData(track: OutputAudioTrack, meta?: EncodedAudioChunkMetadata) {
const existingTrackData = this.#trackDatas.find(x => x.track === track);
if (existingTrackData) {
- return existingTrackData;
+ return existingTrackData as IsobmffAudioTrackData;
}
// TODO Make proper errors for these
assert(meta);
assert(meta.decoderConfig);
- const newTrackData: IsobmffTrackData = {
+ const newTrackData: IsobmffAudioTrackData = {
track,
type: 'audio',
info: {
@@ -300,8 +232,6 @@ export class IsobmffMuxer extends Muxer {
samples: [],
sampleQueue: [],
timestampProcessingQueue: [],
- firstTimestamp: null,
- lastKeyFrameTimestamp: null,
timeToSampleTable: [],
compositionTimeOffsetTable: [],
lastTimescaleUnits: null,
@@ -317,6 +247,49 @@ export class IsobmffMuxer extends Muxer {
return newTrackData;
}
+ #getSubtitleTrackData(track: OutputSubtitleTrack, meta?: SubtitleMetadata) {
+ const existingTrackData = this.#trackDatas.find(x => x.track === track);
+ if (existingTrackData) {
+ return existingTrackData as IsobmffSubtitleTrackData;
+ }
+
+ // TODO Make proper errors for these
+ assert(meta);
+ assert(meta.config);
+
+ const newTrackData: IsobmffSubtitleTrackData = {
+ track,
+ type: 'subtitle',
+ info: {
+ config: meta.config
+ },
+ timescale: 1000, // Reasonable
+ samples: [],
+ sampleQueue: [],
+ timestampProcessingQueue: [],
+ timeToSampleTable: [],
+ compositionTimeOffsetTable: [],
+ lastTimescaleUnits: null,
+ lastSample: null,
+ finalizedChunks: [],
+ currentChunk: null,
+ compactlyCodedChunkTable: [],
+ lastCueEndTimestamp: 0,
+ cueQueue: [],
+ nextSourceId: 0,
+ cueToSourceId: new WeakMap()
+ };
+
+ this.#trackDatas.push(newTrackData);
+ this.#trackDatas.sort((a, b) => a.track.id - b.track.id);
+
+ // Subtitle cues don't need to start at 0, so let's register a timestamp at 0 to satisfy the
+ // "timestamps must start a zero" constraint
+ this.validateAndNormalizeTimestamp(track, 0, true);
+
+ return newTrackData;
+ }
+
addEncodedVideoChunk(track: OutputVideoTrack, chunk: EncodedVideoChunk, meta?: EncodedVideoChunkMetadata) {
const trackData = this.#getVideoTrackData(track, meta);
@@ -330,13 +303,17 @@ export class IsobmffMuxer extends Muxer {
}).`);
}
- let videoSample = this.#createSampleForTrack(trackData, chunk);
+ 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(videoSample);
+ trackData.sampleQueue.push(sample);
this.#interleaveSamples();
} else {
- this.#addSampleToTrack(trackData, videoSample);
+ this.#addSampleToTrack(trackData, sample);
}
}
@@ -353,35 +330,146 @@ export class IsobmffMuxer extends Muxer {
}).`);
}
- let audioSample = this.#createSampleForTrack(trackData, chunk);
+ 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(audioSample);
+ trackData.sampleQueue.push(sample);
this.#interleaveSamples();
} else {
- this.#addSampleToTrack(trackData, audioSample);
+ this.#addSampleToTrack(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') {
+ trackData.cueQueue.push(cue);
+ this.#processWebVTTCues(trackData, cue.timestamp);
+ } else {
+ // TODO
+ }
+ }
+
+ #processWebVTTCues(trackData: IsobmffSubtitleTrackData, until: number) {
+ // WebVTT cues need to undergo special processing as empty sections need to be padded out with samples, and
+ // overlapping samples require special logic. The algorithm produces the format specified in ISO 14496-30.
+
+ while (trackData.cueQueue.length > 0) {
+ let timestamps = new Set([]);
+ for (let cue of trackData.cueQueue) {
+ assert(cue.timestamp <= until);
+ assert(trackData.lastCueEndTimestamp <= cue.timestamp + cue.duration);
+
+ timestamps.add(Math.max(cue.timestamp, trackData.lastCueEndTimestamp)); // Start timestamp
+ timestamps.add(cue.timestamp + cue.duration); // End timestamp
+ }
+
+ let sortedTimestamps = [...timestamps].sort((a, b) => a - b);
+
+ // These are the timestamps of the next sample we'll create:
+ let sampleStart = sortedTimestamps[0]!;
+ let sampleEnd = sortedTimestamps[1] ?? sampleStart;
+
+ if (until < sampleEnd) {
+ break;
+ }
+
+ // We may need to pad out empty space with an vtte box
+ if (trackData.lastCueEndTimestamp < sampleStart) {
+ this.#auxWriter.seek(0);
+ let box = vtte();
+ this.#auxBoxWriter.writeBox(box);
+
+ 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);
+ }
+
+ trackData.lastCueEndTimestamp = sampleStart;
+ }
+
+ this.#auxWriter.seek(0);
+
+ for (let i = 0; i < trackData.cueQueue.length; i++) {
+ let cue = trackData.cueQueue[i]!
+
+ if (cue.timestamp >= sampleEnd) {
+ break;
+ }
+
+ inlineTimestampRegex.lastIndex = 0;
+ let containsTimestamp = inlineTimestampRegex.test(cue.text);
+
+ let endTimestamp = cue.timestamp + cue.duration;
+ let sourceId = trackData.cueToSourceId.get(cue);
+ if (sourceId === undefined && sampleEnd < endTimestamp) {
+ // We know this cue will appear in more than one sample, therefore we need to mark it with a
+ // unique ID
+ sourceId = trackData.nextSourceId++;
+ trackData.cueToSourceId.set(cue, sourceId);
+ }
+
+ if (cue.notes) {
+ // Any notes/comments are included in a special vtta box
+ let box = vtta(cue.notes);
+ this.#auxBoxWriter.writeBox(box);
+ }
+
+ let box = vttc(cue.text, containsTimestamp ? sampleStart : null, cue.identifier ?? null, cue.settings ?? null, sourceId ?? null);
+ this.#auxBoxWriter.writeBox(box);
+
+ if (endTimestamp === sampleEnd) {
+ // The cue won't appear in any future sample, so we're done with it
+ trackData.cueQueue.splice(i--, 1);
+ }
+ }
+
+ 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);
+ }
+
+ trackData.lastCueEndTimestamp = sampleEnd;
}
}
#createSampleForTrack(
trackData: IsobmffTrackData,
- chunk: EncodedVideoChunk | EncodedAudioChunk
+ data: Uint8Array,
+ timestamp: number,
+ duration: number,
+ type: 'key' | 'delta'
) {
- let timestampInSeconds = this.#validateTimestamp(trackData, chunk);
- let durationInSeconds = (chunk.duration ?? 0) / 1e6;
-
- let data = new Uint8Array(chunk.byteLength);
- chunk.copyTo(data);
-
let sample: Sample = {
- timestamp: timestampInSeconds,
- decodeTimestamp: timestampInSeconds, // We may refine this later
- duration: durationInSeconds,
- data: data,
+ timestamp,
+ decodeTimestamp: timestamp, // This may be refined later
+ duration,
+ data,
size: data.byteLength,
- type: chunk.type,
+ type,
// Will be refined once the next sample comes in
- timescaleUnitsToNextSample: intoTimescale(durationInSeconds, trackData.timescale)
+ timescaleUnitsToNextSample: intoTimescale(duration, trackData.timescale)
};
return sample;
@@ -405,6 +493,7 @@ export class IsobmffMuxer extends Muxer {
const sampleCompositionTimeOffset =
intoTimescale(sample.timestamp - sample.decodeTimestamp, trackData.timescale);
+ const durationInTimescale = intoTimescale(sample.duration, trackData.timescale);
if (trackData.lastTimescaleUnits !== null) {
assert(trackData.lastSample);
@@ -417,20 +506,34 @@ export class IsobmffMuxer extends Muxer {
if (this.#format.options.fastStart !== 'fragmented') {
let lastTableEntry = last(trackData.timeToSampleTable);
assert(lastTableEntry);
-
+
if (lastTableEntry.sampleCount === 1) {
- // If we hit this case, we're the second sample
lastTableEntry.sampleDelta = delta;
- lastTableEntry.sampleCount++;
- } else if (lastTableEntry.sampleDelta === delta) {
- // Simply increment the count
+
+ let entryBefore = trackData.timeToSampleTable[trackData.timeToSampleTable.length - 2];
+ if (entryBefore && entryBefore.sampleDelta === delta) {
+ // If the delta is the same as the previous one, merge the two entries
+ entryBefore.sampleCount++;
+ trackData.timeToSampleTable.pop();
+ lastTableEntry = entryBefore;
+ }
+ } else if (lastTableEntry.sampleDelta !== delta) {
+ // The delta has changed, so we need a new entry to reach the current sample
+ lastTableEntry.sampleCount--;
+ trackData.timeToSampleTable.push(lastTableEntry = {
+ sampleCount: 1,
+ sampleDelta: delta
+ });
+ }
+
+ if (lastTableEntry.sampleDelta === durationInTimescale) {
+ // The sample's duration matches the delta, so we can increment the count
lastTableEntry.sampleCount++;
} else {
- // The delta has changed, subtract one from the previous run and create a new run with the new delta
- lastTableEntry.sampleCount--;
+ // Add a new entry in order to maintain the last sample's true duration
trackData.timeToSampleTable.push({
- sampleCount: 2,
- sampleDelta: delta
+ sampleCount: 1,
+ sampleDelta: durationInTimescale
});
}
@@ -455,7 +558,7 @@ export class IsobmffMuxer extends Muxer {
if (this.#format.options.fastStart !== 'fragmented') {
trackData.timeToSampleTable.push({
sampleCount: 1,
- sampleDelta: intoTimescale(sample.duration, trackData.timescale)
+ sampleDelta: durationInTimescale
});
trackData.compositionTimeOffsetTable.push({
sampleCount: 1,
@@ -527,30 +630,6 @@ export class IsobmffMuxer extends Muxer {
trackData.timestampProcessingQueue.push(sample);
}
- #validateTimestamp(trackData: IsobmffTrackData, chunk: EncodedVideoChunk | EncodedAudioChunk) {
- let timestampInSeconds = chunk.timestamp / 1e6;
-
- if (timestampInSeconds < 0) {
- throw new Error(`Timestamps must be non-negative (got ${timestampInSeconds}s).`);
- }
-
- if (trackData.firstTimestamp === null) {
- trackData.firstTimestamp = timestampInSeconds;
- }
-
- timestampInSeconds -= trackData.firstTimestamp;
-
- if (trackData.lastKeyFrameTimestamp !== null && timestampInSeconds < trackData.lastKeyFrameTimestamp) {
- throw new Error(`Timestamp cannot be before last key frame's timestamp (got ${timestampInSeconds}s, last key frame at ${trackData.lastKeyFrameTimestamp}s).`);
- }
-
- if (chunk.type === 'key') {
- trackData.lastKeyFrameTimestamp = timestampInSeconds;
- }
-
- return timestampInSeconds;
- }
-
#finalizeCurrentChunk(trackData: IsobmffTrackData) {
assert(this.#format.options.fastStart !== 'fragmented');
@@ -588,8 +667,10 @@ export class IsobmffMuxer extends Muxer {
#interleaveSamples() {
assert(this.#format.options.fastStart === 'fragmented');
- if (this.#trackDatas.length < this.output.tracks.length) {
- return; // We haven't seen a sample from each track yet
+ for (const track of this.output.tracks) {
+ if (!track.source.closed && !this.#trackDatas.some(x => x.track === track)) {
+ return; // We haven't seen a sample from this open track yet
+ }
}
outer:
@@ -598,11 +679,11 @@ export class IsobmffMuxer extends Muxer {
let minTimestamp = Infinity;
for (let trackData of this.#trackDatas) {
- if (trackData.sampleQueue.length === 0) {
+ if (trackData.sampleQueue.length === 0 && !trackData.track.source.closed) {
break outer;
}
- if (trackData.sampleQueue[0]!.timestamp < minTimestamp) {
+ if (trackData.sampleQueue.length > 0 && trackData.sampleQueue[0]!.timestamp < minTimestamp) {
trackWithMinTimestamp = trackData;
minTimestamp = trackData.sampleQueue[0]!.timestamp;
}
@@ -625,13 +706,13 @@ export class IsobmffMuxer extends Muxer {
if (fragmentNumber === 1) {
// Write the moov box now that we have all decoder configs
let movieBox = moov(this.#trackDatas, this.#creationTime, true);
- this.writeBox(movieBox);
+ this.#boxWriter.writeBox(movieBox);
}
// Write out an initial moof box; will be overwritten later once actual chunk offsets are known
let moofOffset = this.#writer.getPos();
let moofBox = moof(fragmentNumber, this.#trackDatas);
- this.writeBox(moofBox);
+ this.#boxWriter.writeBox(moofBox);
// Create the mdat box
{
@@ -646,15 +727,15 @@ export class IsobmffMuxer extends Muxer {
}
}
- let mdatSize = this.measureBox(mdatBox) + totalTrackSampleSize;
+ let mdatSize = this.#boxWriter.measureBox(mdatBox) + totalTrackSampleSize;
if (mdatSize >= 2**32) {
// Fragment is larger than 4 GiB, we need to use the large size
mdatBox.largeSize = true;
- mdatSize = this.measureBox(mdatBox) + totalTrackSampleSize;
+ mdatSize = this.#boxWriter.measureBox(mdatBox) + totalTrackSampleSize;
}
mdatBox.size = mdatSize;
- this.writeBox(mdatBox);
+ this.#boxWriter.writeBox(mdatBox);
}
// Write sample data
@@ -670,9 +751,9 @@ export class IsobmffMuxer extends Muxer {
// Now that we set the actual chunk offsets, fix the moof box
let endPos = this.#writer.getPos();
- this.#writer.seek(this.offsets.get(moofBox)!);
+ this.#writer.seek(this.#boxWriter.offsets.get(moofBox)!);
let newMoofBox = moof(fragmentNumber, this.#trackDatas);
- this.writeBox(newMoofBox);
+ this.#boxWriter.writeBox(newMoofBox);
this.#writer.seek(endPos);
for (let trackData of this.#trackDatas) {
@@ -686,8 +767,28 @@ export class IsobmffMuxer extends Muxer {
}
}
+ override onTrackClose(track: OutputTrack) {
+ if (track.type === 'subtitle' && track.source.codec === 'webvtt') {
+ let trackData = this.#trackDatas.find(x => x.track === track) as IsobmffSubtitleTrackData;
+ if (trackData) {
+ this.#processWebVTTCues(trackData, Infinity);
+ }
+ }
+
+ if (this.#format.options.fastStart === 'fragmented') {
+ // Since a track is now closed, we may be able to write out chunks that were previously waiting
+ this.#interleaveSamples();
+ }
+ }
+
/** Finalizes the file, making it ready for use. Must be called after all video and audio chunks have been added. */
finalize() {
+ for (let trackData of this.#trackDatas) {
+ if (trackData.type === 'subtitle' && trackData.track.source.codec === 'webvtt') {
+ this.#processWebVTTCues(trackData, Infinity);
+ }
+ }
+
if (this.#format.options.fastStart === 'fragmented') {
for (let trackData of this.#trackDatas) {
for (let sample of trackData.sampleQueue) {
@@ -719,8 +820,8 @@ export class IsobmffMuxer extends Muxer {
for (let i = 0; i < 2; i++) {
let movieBox = moov(this.#trackDatas, this.#creationTime);
- let movieBoxSize = this.measureBox(movieBox);
- mdatSize = this.measureBox(this.#mdat);
+ let movieBoxSize = this.#boxWriter.measureBox(movieBox);
+ mdatSize = this.#boxWriter.measureBox(this.#mdat);
let currentChunkPos = this.#writer.getPos() + movieBoxSize + mdatSize;
for (let chunk of this.#finalizedChunks) {
@@ -737,10 +838,10 @@ export class IsobmffMuxer extends Muxer {
}
let movieBox = moov(this.#trackDatas, this.#creationTime);
- this.writeBox(movieBox);
+ this.#boxWriter.writeBox(movieBox);
this.#mdat.size = mdatSize!;
- this.writeBox(this.#mdat);
+ this.#boxWriter.writeBox(this.#mdat);
for (let chunk of this.#finalizedChunks) {
for (let sample of chunk.samples) {
@@ -753,33 +854,33 @@ export class IsobmffMuxer extends Muxer {
// Append the mfra box to the end of the file for better random access
let startPos = this.#writer.getPos();
let mfraBox = mfra(this.#trackDatas);
- this.writeBox(mfraBox);
+ this.#boxWriter.writeBox(mfraBox);
// Patch the 'size' field of the mfro box at the end of the mfra box now that we know its actual size
let mfraBoxSize = this.#writer.getPos() - startPos;
this.#writer.seek(this.#writer.getPos() - 4);
- this.writeU32(mfraBoxSize);
+ this.#boxWriter.writeU32(mfraBoxSize);
} else {
assert(this.#mdat);
assert(this.#ftypSize !== null);
- let mdatPos = this.offsets.get(this.#mdat);
+ let mdatPos = this.#boxWriter.offsets.get(this.#mdat);
assert(mdatPos !== undefined);
let mdatSize = this.#writer.getPos() - mdatPos;
this.#mdat.size = mdatSize;
this.#mdat.largeSize = mdatSize >= 2**32; // Only use the large size if we need it
- this.patchBox(this.#mdat);
+ this.#boxWriter.patchBox(this.#mdat);
let movieBox = moov(this.#trackDatas, this.#creationTime);
if (typeof this.#format.options.fastStart === 'object') {
this.#writer.seek(this.#ftypSize);
- this.writeBox(movieBox);
+ this.#boxWriter.writeBox(movieBox);
let remainingBytes = mdatPos - this.#writer.getPos();
- this.writeBox(free(remainingBytes));
+ this.#boxWriter.writeBox(free(remainingBytes));
} else {
- this.writeBox(movieBox);
+ this.#boxWriter.writeBox(movieBox);
}
}
}
diff --git a/src/matroska/ebml.ts b/src/matroska/ebml.ts
index ece2659..8683a72 100644
--- a/src/matroska/ebml.ts
+++ b/src/matroska/ebml.ts
@@ -63,7 +63,6 @@ export enum EBMLId {
Video = 0xe0,
PixelWidth = 0xb0,
PixelHeight = 0xba,
- Void = 0xec,
Audio = 0xe1,
SamplingFrequency = 0xb5,
Channels = 0x9f,
@@ -73,6 +72,9 @@ export enum EBMLId {
BlockGroup = 0xa0,
Block = 0xa1,
BlockAdditions = 0x75a1,
+ BlockMore = 0xa6,
+ BlockAdditional = 0xa5,
+ BlockAddID = 0xee,
BlockDuration = 0x9b,
ReferenceBlock = 0xfb,
Cluster = 0x1f43b675,
@@ -87,8 +89,7 @@ export enum EBMLId {
MatrixCoefficients = 0x55b1,
TransferCharacteristics = 0x55ba,
Primaries = 0x55bb,
- Range = 0x55b9,
- AlphaMode = 0x53c0
+ Range = 0x55b9
}
export const measureUnsignedInt = (value: number) => {
diff --git a/src/matroska/matroska_muxer.ts b/src/matroska/matroska_muxer.ts
index 7fb4489..5249a23 100644
--- a/src/matroska/matroska_muxer.ts
+++ b/src/matroska/matroska_muxer.ts
@@ -1,9 +1,9 @@
-import { assert, readBits, toUint8Array, writeBits } from '../misc';
+import { assert, readBits, textEncoder, toUint8Array, writeBits } from '../misc';
import { Muxer } from '../muxer';
import { Output, OutputAudioTrack, OutputSubtitleTrack, OutputTrack, OutputVideoTrack } from '../output';
import { MkvOutputFormat, WebMOutputFormat } from '../output_format';
import { AudioCodec, SubtitleCodec, VideoCodec } from '../source';
-import { EncodedSubtitleChunk, EncodedSubtitleChunkMetadata, SubtitleDecoderConfig } from '../subtitles';
+import { formatSubtitleTimestamp, inlineTimestampRegex, parseSubtitleTimestamp, SubtitleConfig, SubtitleCue, SubtitleMetadata } from '../subtitles';
import { Writer } from '../writer';
import { EBML, EBMLElement, EBMLFloat32, EBMLFloat64, EBMLId, EBMLSignedInt, measureEBMLVarInt, measureSignedInt, measureUnsignedInt } from './ebml';
@@ -38,9 +38,6 @@ type SeekHead = {
type MatroskaTrackData = {
chunkQueue: InternalMediaChunk[],
-
- firstTimestamp: number | null,
- lastKeyFrameTimestamp: number | null,
lastWrittenMsTimestamp: number | null
} & ({
track: OutputVideoTrack,
@@ -62,7 +59,7 @@ type MatroskaTrackData = {
track: OutputSubtitleTrack,
type: 'subtitle',
info: {
- decoderConfig: SubtitleDecoderConfig
+ config: SubtitleConfig
}
});
@@ -78,7 +75,6 @@ const CODEC_STRING_MAP: Record
av1: 'V_AV1',
aac: 'A_AAC',
opus: 'A_OPUS',
- vorbis: 'A_VORBIS',
webvtt: 'S_TEXT/WEBVTT'
};
@@ -94,6 +90,8 @@ const TRACK_TYPE_MAP: Record = {
// 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;
+
#writer: Writer;
#format: WebMOutputFormat | MkvOutputFormat;
@@ -391,8 +389,8 @@ export class MatroskaMuxer extends Muxer {
{ id: EBMLId.TrackUID, data: trackData.track.id },
{ id: EBMLId.TrackType, data: TRACK_TYPE_MAP[trackData.type] }, // TODO Subtitle case
{ id: EBMLId.CodecID, data: CODEC_STRING_MAP[trackData.track.source.codec] },
- (trackData.info.decoderConfig.description ? { id: EBMLId.CodecPrivate, data: toUint8Array(trackData.info.decoderConfig.description) } : null),
...(trackData.type === 'video' ? [
+ (trackData.info.decoderConfig.description ? { id: EBMLId.CodecPrivate, data: toUint8Array(trackData.info.decoderConfig.description) } : null),
(trackData.track.metadata.frameRate ? { id: EBMLId.DefaultDuration, data: 1e9 / trackData.track.metadata.frameRate } : null),
{ id: EBMLId.Video, data: [
{ id: EBMLId.PixelWidth, data: trackData.info.width },
@@ -430,12 +428,16 @@ export class MatroskaMuxer extends Muxer {
] }
] : []),
...(trackData.type === 'audio' ? [
+ (trackData.info.decoderConfig.description ? { id: EBMLId.CodecPrivate, data: toUint8Array(trackData.info.decoderConfig.description) } : null),
{ id: EBMLId.Audio, data: [
{ id: EBMLId.SamplingFrequency, data: new EBMLFloat32(trackData.info.sampleRate) },
{ id: EBMLId.Channels, data: trackData.info.numberOfChannels },
// Bit depth for when PCM is a thing
] }
- ] : [])
+ ] : []),
+ ...(trackData.type === 'subtitle' ? [
+ { id: EBMLId.CodecPrivate, data: textEncoder.encode(trackData.info.config.description) }
+ ] : []),
] })
}
}
@@ -492,8 +494,6 @@ export class MatroskaMuxer extends Muxer {
decoderConfig: meta.decoderConfig
},
chunkQueue: [],
- firstTimestamp: null,
- lastKeyFrameTimestamp: null,
lastWrittenMsTimestamp: null
};
@@ -522,8 +522,6 @@ export class MatroskaMuxer extends Muxer {
decoderConfig: meta.decoderConfig
},
chunkQueue: [],
- firstTimestamp: null,
- lastKeyFrameTimestamp: null,
lastWrittenMsTimestamp: null
};
@@ -533,7 +531,7 @@ export class MatroskaMuxer extends Muxer {
return newTrackData;
}
- #getSubtitleTrackData(track: OutputSubtitleTrack, meta?: EncodedSubtitleChunkMetadata) {
+ #getSubtitleTrackData(track: OutputSubtitleTrack, meta?: SubtitleMetadata) {
const existingTrackData = this.#trackDatas.find(x => x.track === track);
if (existingTrackData) {
return existingTrackData as MatroskaAudioTrackData;
@@ -541,17 +539,15 @@ export class MatroskaMuxer extends Muxer {
// TODO Make proper errors for these
assert(meta);
- assert(meta.decoderConfig);
+ assert(meta.config);
const newTrackData: MatroskaSubtitleTrackData = {
track,
type: 'subtitle',
info: {
- decoderConfig: meta.decoderConfig
+ config: meta.config
},
chunkQueue: [],
- firstTimestamp: null,
- lastKeyFrameTimestamp: null,
lastWrittenMsTimestamp: null
};
@@ -567,7 +563,8 @@ export class MatroskaMuxer extends Muxer {
let data = new Uint8Array(chunk.byteLength);
chunk.copyTo(data);
- let videoChunk = this.#createInternalChunk(trackData, data, chunk.timestamp, chunk.duration ?? 0, chunk.type);
+ let timestamp = this.validateAndNormalizeTimestamp(trackData.track, chunk.timestamp, chunk.type === 'key');
+ let videoChunk = this.#createInternalChunk(data, timestamp, (chunk.duration ?? 0) / 1e6, chunk.type);
if (track.source.codec === 'vp9') this.#fixVP9ColorSpace(trackData, videoChunk);
trackData.chunkQueue.push(videoChunk);
@@ -580,27 +577,44 @@ export class MatroskaMuxer extends Muxer {
let data = new Uint8Array(chunk.byteLength);
chunk.copyTo(data);
- let audioChunk = this.#createInternalChunk(trackData, data, chunk.timestamp, chunk.duration ?? 0, chunk.type);
+ let timestamp = this.validateAndNormalizeTimestamp(trackData.track, chunk.timestamp, chunk.type === 'key');
+ let audioChunk = this.#createInternalChunk(data, timestamp, (chunk.duration ?? 0) / 1e6, chunk.type);
trackData.chunkQueue.push(audioChunk);
this.#interleaveChunks();
}
- addEncodedSubtitleChunk(track: OutputSubtitleTrack, chunk: EncodedSubtitleChunk, meta?: EncodedSubtitleChunkMetadata) {
+ addSubtitleCue(track: OutputSubtitleTrack, cue: SubtitleCue, meta?: SubtitleMetadata) {
const trackData = this.#getSubtitleTrackData(track, meta);
- let subtitleChunk = this.#createInternalChunk(trackData, chunk.body, chunk.timestamp, chunk.duration, 'key', chunk.additions);
+ const timestamp = this.validateAndNormalizeTimestamp(trackData.track, 1e6 * cue.timestamp, true);
+
+ let bodyText = cue.text;
+ const timestampMs = Math.floor(timestamp * 1000);
+
+ // Replace in-body timestamps so that they're relative to the cue start time
+ inlineTimestampRegex.lastIndex = 0;
+ bodyText = bodyText.replace(inlineTimestampRegex, (match) => {
+ let time = parseSubtitleTimestamp(match.slice(1, -1));
+ let offsetTime = time - timestampMs;
+
+ return `<${formatSubtitleTimestamp(offsetTime)}>`;
+ });
+
+ const body = textEncoder.encode(bodyText);
+ const additions = `${cue.settings ?? ''}\n${cue.identifier ?? ''}\n${cue.notes ?? ''}`;
+
+ let subtitleChunk = this.#createInternalChunk(body, timestamp, cue.duration, 'key', additions.trim() ? textEncoder.encode(additions) : null);
trackData.chunkQueue.push(subtitleChunk);
this.#interleaveChunks();
}
#interleaveChunks() {
- let openTrackCount = 0;
- for (const trackData of this.#trackDatas) if (!trackData.track.source.closed) openTrackCount++;
-
- if (this.#trackDatas.length < openTrackCount) {
- return; // We haven't seen a sample from every open track yet
+ for (const track of this.output.tracks) {
+ if (!track.source.closed && !this.#trackDatas.some(x => x.track === track)) {
+ return; // We haven't seen a sample from this open track yet
+ }
}
outer:
@@ -668,50 +682,23 @@ export class MatroskaMuxer extends Muxer {
/** Converts a read-only external chunk into an internal one for easier use. */
#createInternalChunk(
- trackData: MatroskaTrackData,
data: Uint8Array,
timestamp: number,
duration: number,
type: 'key' | 'delta',
additions: Uint8Array | null = null
) {
- let adjustedTimestamp = this.#validateTimestamp(trackData, timestamp, type === 'key');
-
let internalChunk: InternalMediaChunk = {
data,
type,
- timestamp: adjustedTimestamp,
- duration: duration / 1e6,
+ timestamp,
+ duration,
additions
};
return internalChunk;
}
- #validateTimestamp(trackData: MatroskaTrackData, timestamp: number, isKeyFrame: boolean) {
- let timestampInSeconds = timestamp / 1e6;
-
- if (timestampInSeconds < 0) {
- throw new Error(`Timestamps must be non-negative (got ${timestampInSeconds}s).`);
- }
-
- if (trackData.firstTimestamp === null) {
- trackData.firstTimestamp = timestampInSeconds;
- }
-
- timestampInSeconds -= trackData.firstTimestamp;
-
- if (trackData.lastKeyFrameTimestamp !== null && timestampInSeconds < trackData.lastKeyFrameTimestamp) {
- throw new Error(`Timestamp cannot be before last key frame's timestamp (got ${timestampInSeconds}s, last key frame at ${trackData.lastKeyFrameTimestamp}s).`);
- }
-
- if (isKeyFrame) {
- trackData.lastKeyFrameTimestamp = timestampInSeconds;
- }
-
- return timestampInSeconds;
- }
-
/** Writes a block containing media data to the file. */
#writeBlock(trackData: MatroskaTrackData, chunk: InternalMediaChunk) {
// TODO Update this comment. This code always runs now
@@ -726,6 +713,10 @@ export class MatroskaMuxer extends Muxer {
// 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 => {
+ if (otherTrackData.track.source.closed) {
+ return true;
+ }
+
if (trackData === otherTrackData) {
return chunk.type === 'key';
}
@@ -780,8 +771,13 @@ export class MatroskaMuxer extends Muxer {
chunk.data
] },
chunk.type === 'delta' ? { id: EBMLId.ReferenceBlock, data: new EBMLSignedInt(trackData.lastWrittenMsTimestamp! - msTimestamp) } : null,
- msDuration > 0 ? { id: EBMLId.BlockDuration, data: msDuration } : null,
- chunk.additions ? { id: EBMLId.BlockAdditions, data: chunk.additions } : null
+ chunk.additions ? { id: EBMLId.BlockAdditions, data: [
+ { id: EBMLId.BlockMore, data: [
+ { id: EBMLId.BlockAdditional, data: chunk.additions },
+ { id: EBMLId.BlockAddID, data: 1 }
+ ] }
+ ] } : null,
+ msDuration > 0 ? { id: EBMLId.BlockDuration, data: msDuration } : null
] };
this.writeEBML(blockGroup);
}
diff --git a/src/misc.ts b/src/misc.ts
index 98ca7f7..945ce88 100644
--- a/src/misc.ts
+++ b/src/misc.ts
@@ -48,4 +48,6 @@ export const toUint8Array = (source: AllowSharedBufferSource): Uint8Array => {
} else {
return new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
}
-};
\ No newline at end of file
+};
+
+export const textEncoder = new TextEncoder();
\ No newline at end of file
diff --git a/src/muxer.ts b/src/muxer.ts
index 1bcf9d0..b4689d2 100644
--- a/src/muxer.ts
+++ b/src/muxer.ts
@@ -1,5 +1,5 @@
import { Output, OutputAudioTrack, OutputSubtitleTrack, OutputTrack, OutputVideoTrack } from "./output";
-import { EncodedSubtitleChunk, EncodedSubtitleChunkMetadata } from "./subtitles";
+import { SubtitleCue, SubtitleMetadata } from "./subtitles";
export abstract class Muxer {
output: Output;
@@ -11,9 +11,62 @@ export abstract class Muxer {
abstract start(): void;
abstract addEncodedVideoChunk(track: OutputVideoTrack, chunk: EncodedVideoChunk, meta?: EncodedVideoChunkMetadata): void;
abstract addEncodedAudioChunk(track: OutputAudioTrack, chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata): void;
- abstract addEncodedSubtitleChunk(track: OutputSubtitleTrack, chunk: EncodedSubtitleChunk, meta?: EncodedSubtitleChunkMetadata): void;
+ abstract addSubtitleCue(track: OutputSubtitleTrack, cue: SubtitleCue, meta?: SubtitleMetadata): void;
abstract finalize(): void;
beforeTrackAdd(track: OutputTrack) {}
onTrackClose(track: OutputTrack) {}
+
+ private trackTimestampInfo = new WeakMap();
+
+ abstract timestampsMustStartAtZero: boolean;
+ protected validateAndNormalizeTimestamp(track: OutputTrack, rawTimestampInUs: number, isKeyFrame: boolean) {
+ let timestampInSeconds = rawTimestampInUs / 1e6;
+
+ let timestampInfo = this.trackTimestampInfo.get(track);
+ if (!timestampInfo) {
+ if (!isKeyFrame) {
+ throw new Error('First frame must be a key frame.');
+ }
+
+ if (this.timestampsMustStartAtZero && timestampInSeconds > 0) {
+ throw new Error(`Timestamps must start at zero (got ${timestampInSeconds}s).`);
+ }
+
+ timestampInfo = {
+ timestampOffset: timestampInSeconds,
+ maxTimestamp: track.source.offsetTimestamps ? 0 : timestampInSeconds,
+ lastKeyFrameTimestamp: track.source.offsetTimestamps ? 0 : timestampInSeconds
+ };
+ this.trackTimestampInfo.set(track, timestampInfo);
+ }
+
+ if (track.source.offsetTimestamps) {
+ timestampInSeconds -= timestampInfo.timestampOffset;
+ }
+
+ if (timestampInSeconds < 0) {
+ throw new Error(`Timestamps must be non-negative (got ${timestampInSeconds}s).`);
+ }
+
+ if (timestampInSeconds < timestampInfo.lastKeyFrameTimestamp) {
+ throw new Error(`Timestamp cannot be smaller than last key frame's timestamp (got ${timestampInSeconds}s, last key frame at ${timestampInfo.lastKeyFrameTimestamp}s).`);
+ }
+
+ if (isKeyFrame) {
+ if (timestampInSeconds < timestampInfo.maxTimestamp) {
+ throw new Error(`Key frame timestamps cannot be smaller than any timestamp that came before (got ${timestampInSeconds}s, max timestamp was ${timestampInfo.maxTimestamp}s).`);
+ }
+
+ timestampInfo.lastKeyFrameTimestamp = timestampInSeconds;
+ }
+
+ timestampInfo.maxTimestamp = Math.max(timestampInfo.maxTimestamp, timestampInSeconds);
+
+ return timestampInSeconds;
+ }
}
\ No newline at end of file
diff --git a/src/output_format.ts b/src/output_format.ts
index c87326b..78cd95e 100644
--- a/src/output_format.ts
+++ b/src/output_format.ts
@@ -9,7 +9,9 @@ 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
},
diff --git a/src/source.ts b/src/source.ts
index c7dd732..a892e52 100644
--- a/src/source.ts
+++ b/src/source.ts
@@ -1,15 +1,16 @@
import { buildAudioCodecString, buildVideoCodecString } from "./codec";
-import { assert, TransformationMatrix } from "./misc";
+import { assert } from "./misc";
import { OutputAudioTrack, OutputSubtitleTrack, OutputTrack, OutputVideoTrack } from "./output";
-import { SubtitleEncoder } from "./subtitles";
+import { SubtitleParser } from "./subtitles";
export type VideoCodec = 'avc' | 'hevc' | 'vp8' | 'vp9' | 'av1';
-export type AudioCodec = 'aac' | 'opus' | 'vorbis'; // TODO add the rest
+export type AudioCodec = 'aac' | 'opus'; // TODO add the rest
export type SubtitleCodec = 'webvtt';
export abstract class MediaSource {
connectedTrack: OutputTrack | null = null;
closed = false;
+ offsetTimestamps = false;
ensureValidDigest() {
if (!this.connectedTrack) {
@@ -83,8 +84,9 @@ export class EncodedVideoChunkSource extends VideoSource {
const KEY_FRAME_INTERVAL = 5;
type VideoCodecConfig = {
- codec: 'avc' | 'hevc' | 'vp8' | 'vp9' | 'av1',
- bitrate: number
+ codec: VideoCodec,
+ bitrate: number,
+ latencyMode?: VideoEncoderConfig['latencyMode']
};
class VideoEncoderWrapper {
@@ -125,6 +127,8 @@ class VideoEncoderWrapper {
width: videoFrame.codedWidth,
height: videoFrame.codedHeight,
bitrate: this.codecConfig.bitrate,
+ framerate: this.source.connectedTrack?.metadata.frameRate,
+ latencyMode: this.codecConfig.latencyMode,
});
}
@@ -162,6 +166,7 @@ export class CanvasSource extends VideoSource {
const frame = new VideoFrame(this.canvas, {
timestamp: Math.round(1e6 * timestamp),
duration: Math.round(1e6 * duration),
+ alpha: 'discard',
});
this.encoder.digest(frame);
@@ -177,6 +182,8 @@ export class MediaStreamVideoTrackSource extends VideoSource {
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);
@@ -236,7 +243,7 @@ export class EncodedAudioChunkSource extends AudioSource {
}
type AudioCodecConfig = {
- codec: 'aac' | 'opus' | 'vorbis',
+ codec: AudioCodec,
bitrate: number
};
@@ -340,6 +347,8 @@ export class MediaStreamAudioTrackSource extends AudioSource {
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);
@@ -388,21 +397,20 @@ export abstract class SubtitleSource extends MediaSource {
}
export class TextSubtitleSource extends SubtitleSource {
- private encoder: SubtitleEncoder;
+ private parser: SubtitleParser;
constructor(codec: SubtitleCodec) {
super(codec);
- this.encoder = new SubtitleEncoder({
- output: (chunk, metadata) => this.connectedTrack?.output.muxer.addEncodedSubtitleChunk(this.connectedTrack, chunk, metadata),
+ this.parser = new SubtitleParser({
+ codec,
+ output: (cue, metadata) => this.connectedTrack?.output.muxer.addSubtitleCue(this.connectedTrack, cue, metadata),
error: (error) => console.error(error) // TODO
});
-
- this.encoder.configure({ codec });
}
digest(text: string) {
this.ensureValidDigest();
- this.encoder.encode(text);
+ this.parser.parse(text);
}
}
\ No newline at end of file
diff --git a/src/subtitles.ts b/src/subtitles.ts
index 1007aaa..8badafb 100644
--- a/src/subtitles.ts
+++ b/src/subtitles.ts
@@ -1,62 +1,46 @@
-export type EncodedSubtitleChunk = {
- body: Uint8Array,
- additions: Uint8Array | null,
- timestamp: number,
- duration: number
+export type SubtitleCue = {
+ timestamp: number, // in seconds
+ duration: number, // in seconds
+ text: string,
+ identifier?: string,
+ settings?: string,
+ notes?: string
};
-export type SubtitleDecoderConfig = {
- description: Uint8Array
+export type SubtitleConfig = {
+ description: string
};
-export type EncodedSubtitleChunkMetadata = {
- decoderConfig?: SubtitleDecoderConfig
+export type SubtitleMetadata = {
+ config?: SubtitleConfig
};
-interface SubtitleEncoderOptions {
- output: (chunk: EncodedSubtitleChunk, metadata: EncodedSubtitleChunkMetadata) => unknown,
+type SubtitleParserOptions = {
+ codec: 'webvtt',
+ output: (cue: SubtitleCue, metadata: SubtitleMetadata) => unknown,
error: (error: Error) => unknown
-}
-
-interface SubtitleEncoderConfig {
- codec: 'webvtt'
-}
+};
const cueBlockHeaderRegex = /(?:(.+?)\n)?((?:\d{2}:)?\d{2}:\d{2}.\d{3})\s+-->\s+((?:\d{2}:)?\d{2}:\d{2}.\d{3})/g;
-const preambleStartRegex = /^WEBVTT.*?\n{2}/;
-const timestampRegex = /(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})/;
-const inlineTimestampRegex = /<(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})>/g;
-const textEncoder = new TextEncoder();
+const preambleStartRegex = /^WEBVTT(.|\n)*?\n{2}/;
+export const inlineTimestampRegex = /<(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})>/g;
-export class SubtitleEncoder {
- #options: SubtitleEncoderOptions;
- #config: SubtitleEncoderConfig | null = null;
- #preambleBytes: Uint8Array | null = null;
+export class SubtitleParser {
+ #options: SubtitleParserOptions;
+ #preambleText: string | null = null;
#preambleEmitted = false;
- constructor(options: SubtitleEncoderOptions) {
+ constructor(options: SubtitleParserOptions) {
this.#options = options;
}
- configure(config: SubtitleEncoderConfig) {
- if (config.codec !== 'webvtt') {
- throw new Error("Codec must be 'webvtt'.");
- }
-
- this.#config = config;
- }
-
- encode(text: string) {
- if (!this.#config) {
- throw new Error('Encoder not configured.');
- }
-
- text = text.replace('\r\n', '\n').replace('\r', '\n');
+ parse(text: string) {
+ text = text.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
cueBlockHeaderRegex.lastIndex = 0;
let match: RegExpMatchArray | null;
- if (!this.#preambleBytes) {
+ if (!this.#preambleText) {
if (!preambleStartRegex.test(text)) {
let error = new Error('WebVTT preamble incorrect.');
this.#options.error(error);
@@ -72,7 +56,7 @@ export class SubtitleEncoder {
throw error;
}
- this.#preambleBytes = textEncoder.encode(preamble);
+ this.#preambleText = preamble;
if (match) {
text = text.slice(match.index);
@@ -82,70 +66,63 @@ export class SubtitleEncoder {
while (match = cueBlockHeaderRegex.exec(text)) {
let notes = text.slice(0, match.index);
- let cueIdentifier = match[1] || '';
+ let cueIdentifier = match[1];
let matchEnd = match.index! + match[0].length;
let bodyStart = text.indexOf('\n', matchEnd) + 1;
let cueSettings = text.slice(matchEnd, bodyStart).trim();
let bodyEnd = text.indexOf('\n\n', matchEnd);
if (bodyEnd === -1) bodyEnd = text.length;
- let startTime = this.#parseTimestamp(match[2]!);
- let endTime = this.#parseTimestamp(match[3]!);
+ let startTime = parseSubtitleTimestamp(match[2]!);
+ let endTime = parseSubtitleTimestamp(match[3]!);
let duration = endTime - startTime;
- let body = text.slice(bodyStart, bodyEnd);
- let additions = `${cueSettings}\n${cueIdentifier}\n${notes}`;
-
- // Replace in-body timestamps so that they're relative to the cue start time
- inlineTimestampRegex.lastIndex = 0;
- body = body.replace(inlineTimestampRegex, (match) => {
- let time = this.#parseTimestamp(match.slice(1, -1));
- let offsetTime = time - startTime;
-
- return `<${this.#formatTimestamp(offsetTime)}>`;
- });
+ let body = text.slice(bodyStart, bodyEnd).trim();
text = text.slice(bodyEnd).trimStart();
cueBlockHeaderRegex.lastIndex = 0;
- let chunk: EncodedSubtitleChunk = {
- body: textEncoder.encode(body),
- additions: additions.trim() === '' ? null : textEncoder.encode(additions),
- timestamp: startTime * 1000,
- duration: duration * 1000
+ let cue: SubtitleCue = {
+ timestamp: startTime / 1000,
+ duration: duration / 1000,
+ text: body,
+ identifier: cueIdentifier,
+ settings: cueSettings,
+ notes
};
- let meta: EncodedSubtitleChunkMetadata = {};
+ let meta: SubtitleMetadata = {};
if (!this.#preambleEmitted) {
- meta.decoderConfig = {
- description: this.#preambleBytes
+ meta.config = {
+ description: this.#preambleText
};
this.#preambleEmitted = true;
}
- this.#options.output(chunk, meta);
+ this.#options.output(cue, meta);
}
}
+}
- #parseTimestamp(string: string) {
- let match = timestampRegex.exec(string);
- if (!match) throw new Error('Expected match.');
+const timestampRegex = /(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})/;
+export const parseSubtitleTimestamp = (string: string) => {
+ let match = timestampRegex.exec(string);
+ if (!match) throw new Error('Expected match.');
- return 60 * 60 * 1000 * Number(match[1] || '0') +
- 60 * 1000 * Number(match[2]) +
- 1000 * Number(match[3]) +
- Number(match[4]);
- }
+ return 60 * 60 * 1000 * Number(match[1] || '0') +
+ 60 * 1000 * Number(match[2]) +
+ 1000 * Number(match[3]) +
+ Number(match[4]);
+};
- #formatTimestamp(timestamp: number) {
- let hours = Math.floor(timestamp / (60 * 60 * 1000));
- let minutes = Math.floor((timestamp % (60 * 60 * 1000)) / (60 * 1000));
- let seconds = Math.floor((timestamp % (60 * 1000)) / 1000);
- let milliseconds = timestamp % 1000;
+export const formatSubtitleTimestamp = (timestamp: number) => {
+ let hours = Math.floor(timestamp / (60 * 60 * 1000));
+ let minutes = Math.floor((timestamp % (60 * 60 * 1000)) / (60 * 1000));
+ let seconds = Math.floor((timestamp % (60 * 1000)) / 1000);
+ let milliseconds = timestamp % 1000;
- return hours.toString().padStart(2, '0') + ':' +
- minutes.toString().padStart(2, '0') + ':' +
- seconds.toString().padStart(2, '0') + '.' +
- milliseconds.toString().padStart(3, '0');
- }
-}
\ No newline at end of file
+ return hours.toString().padStart(2, '0') + ':' +
+ minutes.toString().padStart(2, '0') + ':' +
+ seconds.toString().padStart(2, '0') + '.' +
+ milliseconds.toString().padStart(3, '0');
+};
\ No newline at end of file
diff --git a/src/writer.ts b/src/writer.ts
index e5e8847..244fc41 100644
--- a/src/writer.ts
+++ b/src/writer.ts
@@ -67,6 +67,10 @@ export class ArrayBufferTargetWriter extends Writer {
this.#ensureSize(this.#pos);
this.#target.buffer = this.#buffer.slice(0, Math.max(this.#maxPos, this.#pos));
}
+
+ getSlice(start: number, end: number) {
+ return this.#bytes.slice(start, end);
+ }
}
/**