diff --git a/dev/index.html b/dev/index.html
index 0f22b80..28fb0b6 100644
--- a/dev/index.html
+++ b/dev/index.html
@@ -36,17 +36,17 @@
canvas.height = 720;
const context = canvas.getContext('2d');
- let format = new Metamuxer.Mp4OutputFormat({ fastStart: false });
+ let format = new Metamuxer.WebMOutputFormat(); //new Metamuxer.Mp4OutputFormat({ fastStart: false });
let target = new Metamuxer.ArrayBufferTarget();
let output = new Metamuxer.Output({ format, target });
let videoSource = new Metamuxer.CanvasSource(canvas, {
- codec: 'vp8',
+ codec: 'avc',
bitrate: 1e6
});
let audioSource = new Metamuxer.AudioBufferSource({
- codec: 'aac',
+ codec: 'opus',
bitrate: 128e3,
});
@@ -72,5 +72,5 @@
await output.finalize();
console.log(target);
- download(new Blob([target.buffer]), 'test.mp4');
+ download(new Blob([target.buffer]), 'test.webm');
\ No newline at end of file
diff --git a/dist/metamuxer.js b/dist/metamuxer.js
index 062bd9b..cb95cea 100644
--- a/dist/metamuxer.js
+++ b/dist/metamuxer.js
@@ -28,11 +28,13 @@ var Metamuxer = (() => {
FileSystemWritableFileStreamTarget: () => FileSystemWritableFileStreamTarget2,
MediaStreamAudioTrackSource: () => MediaStreamAudioTrackSource,
MediaStreamVideoTrackSource: () => MediaStreamVideoTrackSource,
+ MkvOutputFormat: () => MkvOutputFormat2,
Mp4OutputFormat: () => Mp4OutputFormat,
Output: () => Output,
StreamTarget: () => StreamTarget,
Target: () => Target,
- VideoFrameSource: () => VideoFrameSource
+ VideoFrameSource: () => VideoFrameSource,
+ WebMOutputFormat: () => WebMOutputFormat
});
// src/codec.ts
@@ -138,6 +140,35 @@ var Metamuxer = (() => {
var isU32 = (value) => {
return value >= 0 && value < 2 ** 32;
};
+ var readBits = (bytes2, start, end) => {
+ let result = 0;
+ for (let i = start; i < end; i++) {
+ let byteIndex = Math.floor(i / 8);
+ let byte = bytes2[byteIndex];
+ let bitIndex = 7 - (i & 7);
+ let bit = (byte & 1 << bitIndex) >> bitIndex;
+ result <<= 1;
+ result |= bit;
+ }
+ return result;
+ };
+ var writeBits = (bytes2, start, end, value) => {
+ for (let i = start; i < end; i++) {
+ let byteIndex = Math.floor(i / 8);
+ let byte = bytes2[byteIndex];
+ let bitIndex = 7 - (i & 7);
+ byte &= ~(1 << bitIndex);
+ byte |= (value & 1 << end - i - 1) >> end - i - 1 << bitIndex;
+ bytes2[byteIndex] = byte;
+ }
+ };
+ var toUint8Array = (source) => {
+ if (source instanceof ArrayBuffer) {
+ return new Uint8Array(source);
+ } else {
+ return new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
+ }
+ };
// src/source.ts
var VideoSource = class {
@@ -398,6 +429,7 @@ var Metamuxer = (() => {
type: source instanceof VideoSource ? "video" : "audio",
source
};
+ this.muxer.beforeTrackAdd(track);
this.tracks.push(track);
source.connectedTrack = track;
}
@@ -672,6 +704,7 @@ var Metamuxer = (() => {
// Component flags
u32(0),
// Component flags mask
+ // TODO:
ascii("mp4-muxer-hdlr", true)
// Component name
]);
@@ -764,11 +797,11 @@ var Metamuxer = (() => {
]);
var avcC = (trackData) => trackData.info.decoderConfig && box("avcC", [
// For AVC, description is an AVCDecoderConfigurationRecord, so nothing else to do here
- ...new Uint8Array(trackData.info.decoderConfig.description)
+ ...toUint8Array(trackData.info.decoderConfig.description)
]);
var hvcC = (trackData) => trackData.info.decoderConfig && box("hvcC", [
// For HEVC, description is a HEVCDecoderConfigurationRecord, so nothing else to do here
- ...new Uint8Array(trackData.info.decoderConfig.description)
+ ...toUint8Array(trackData.info.decoderConfig.description)
]);
var vpcC = (trackData) => {
if (!trackData.info.decoderConfig) {
@@ -840,7 +873,7 @@ var Metamuxer = (() => {
AUDIO_CODEC_TO_CONFIGURATION_BOX[trackData.track.source.codec](trackData)
]);
var esds = (trackData) => {
- let description = new Uint8Array(trackData.info.decoderConfig.description);
+ let description = toUint8Array(trackData.info.decoderConfig.description ?? new ArrayBuffer(0));
return fullBox("esds", 0, 0, [
// https://stackoverflow.com/a/54803118
u32(58753152),
@@ -1165,6 +1198,8 @@ var Metamuxer = (() => {
constructor(output) {
this.output = output;
}
+ beforeTrackAdd(track) {
+ }
};
// src/isobmff/isobmff_muxer.ts
@@ -1270,9 +1305,6 @@ var Metamuxer = (() => {
}
}
start() {
- this.#writeHeader();
- }
- #writeHeader() {
const holdsAvc = this.output.tracks.some((x) => x.type === "video" && x.source.codec === "avc");
this.writeBox(ftyp({
holdsAvc,
@@ -1317,8 +1349,8 @@ var Metamuxer = (() => {
}
assert(meta);
assert(meta.decoderConfig);
- assert(meta.decoderConfig.codedWidth);
- assert(meta.decoderConfig.codedHeight);
+ assert(meta.decoderConfig.codedWidth !== void 0);
+ assert(meta.decoderConfig.codedHeight !== void 0);
const newTrackData = {
track,
type: "video",
@@ -1510,6 +1542,9 @@ var Metamuxer = (() => {
trackData.currentChunk.samples.push(sample);
}
#validateTimestamp(trackData, presentationTimestamp, decodeTimestamp) {
+ if (decodeTimestamp < 0) {
+ throw new Error(`Timestamps must be non-negative (got ${decodeTimestamp}s).`);
+ }
if (trackData.firstDecodeTimestamp === null) {
trackData.firstDecodeTimestamp = decodeTimestamp;
}
@@ -1694,6 +1729,721 @@ var Metamuxer = (() => {
}
};
+ // src/matroska/ebml.ts
+ var EBMLFloat32 = class {
+ constructor(value) {
+ this.value = value;
+ }
+ };
+ var EBMLFloat64 = class {
+ constructor(value) {
+ this.value = value;
+ }
+ };
+ var measureUnsignedInt = (value) => {
+ if (value < 1 << 8) {
+ return 1;
+ } else if (value < 1 << 16) {
+ return 2;
+ } else if (value < 1 << 24) {
+ return 3;
+ } else if (value < 2 ** 32) {
+ return 4;
+ } else if (value < 2 ** 40) {
+ return 5;
+ } else {
+ return 6;
+ }
+ };
+ var measureEBMLVarInt = (value) => {
+ if (value < (1 << 7) - 1) {
+ return 1;
+ } else if (value < (1 << 14) - 1) {
+ return 2;
+ } else if (value < (1 << 21) - 1) {
+ return 3;
+ } else if (value < (1 << 28) - 1) {
+ return 4;
+ } else if (value < 2 ** 35 - 1) {
+ return 5;
+ } else if (value < 2 ** 42 - 1) {
+ return 6;
+ } else {
+ throw new Error("EBML VINT size not supported " + value);
+ }
+ };
+
+ // src/matroska/matroska_muxer.ts
+ var VIDEO_TRACK_TYPE = 1;
+ var AUDIO_TRACK_TYPE = 2;
+ var MAX_CHUNK_LENGTH_MS = 2 ** 15;
+ var APP_NAME = "https://github.com/Vanilagy/webm-muxer";
+ var SEGMENT_SIZE_BYTES = 6;
+ var CLUSTER_SIZE_BYTES = 5;
+ var CODEC_STRING_MAP = {
+ 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"
+ };
+ var MatroskaMuxer = 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 EBML 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();
+ /** Same as offsets, but stores position where the element's data starts (after ID and size fields). */
+ this.dataOffsets = /* @__PURE__ */ new WeakMap();
+ this.#trackDatas = [];
+ this.#segment = null;
+ this.#segmentInfo = null;
+ this.#seekHead = null;
+ this.#tracksElement = null;
+ this.#segmentDuration = null;
+ this.#cues = null;
+ this.#currentCluster = null;
+ this.#currentClusterTimestamp = null;
+ this.#trackDatasInCurrentCluster = /* @__PURE__ */ new Set();
+ this.#duration = 0;
+ this.#writer = output.writer;
+ this.#format = format;
+ }
+ #writer;
+ #format;
+ #helper;
+ #helperView;
+ #trackDatas;
+ #segment;
+ #segmentInfo;
+ #seekHead;
+ #tracksElement;
+ #segmentDuration;
+ #cues;
+ #currentCluster;
+ #currentClusterTimestamp;
+ #trackDatasInCurrentCluster;
+ #duration;
+ #writeByte(value) {
+ this.#helperView.setUint8(0, value);
+ this.#writer.write(this.#helper.subarray(0, 1));
+ }
+ #writeFloat32(value) {
+ this.#helperView.setFloat32(0, value, false);
+ this.#writer.write(this.#helper.subarray(0, 4));
+ }
+ #writeFloat64(value) {
+ this.#helperView.setFloat64(0, value, false);
+ this.#writer.write(this.#helper);
+ }
+ #writeUnsignedInt(value, width = measureUnsignedInt(value)) {
+ let pos = 0;
+ switch (width) {
+ case 6:
+ this.#helperView.setUint8(pos++, value / 2 ** 40 | 0);
+ case 5:
+ this.#helperView.setUint8(pos++, value / 2 ** 32 | 0);
+ case 4:
+ this.#helperView.setUint8(pos++, value >> 24);
+ case 3:
+ this.#helperView.setUint8(pos++, value >> 16);
+ case 2:
+ this.#helperView.setUint8(pos++, value >> 8);
+ case 1:
+ this.#helperView.setUint8(pos++, value);
+ break;
+ default:
+ throw new Error("Bad UINT size " + width);
+ }
+ this.#writer.write(this.#helper.subarray(0, pos));
+ }
+ writeEBMLVarInt(value, width = measureEBMLVarInt(value)) {
+ let pos = 0;
+ switch (width) {
+ case 1:
+ this.#helperView.setUint8(pos++, 1 << 7 | value);
+ break;
+ case 2:
+ this.#helperView.setUint8(pos++, 1 << 6 | value >> 8);
+ this.#helperView.setUint8(pos++, value);
+ break;
+ case 3:
+ this.#helperView.setUint8(pos++, 1 << 5 | value >> 16);
+ this.#helperView.setUint8(pos++, value >> 8);
+ this.#helperView.setUint8(pos++, value);
+ break;
+ case 4:
+ this.#helperView.setUint8(pos++, 1 << 4 | value >> 24);
+ this.#helperView.setUint8(pos++, value >> 16);
+ this.#helperView.setUint8(pos++, value >> 8);
+ this.#helperView.setUint8(pos++, value);
+ break;
+ case 5:
+ this.#helperView.setUint8(pos++, 1 << 3 | value / 2 ** 32 & 7);
+ this.#helperView.setUint8(pos++, value >> 24);
+ this.#helperView.setUint8(pos++, value >> 16);
+ this.#helperView.setUint8(pos++, value >> 8);
+ this.#helperView.setUint8(pos++, value);
+ break;
+ case 6:
+ this.#helperView.setUint8(pos++, 1 << 2 | value / 2 ** 40 & 3);
+ this.#helperView.setUint8(pos++, value / 2 ** 32 | 0);
+ this.#helperView.setUint8(pos++, value >> 24);
+ this.#helperView.setUint8(pos++, value >> 16);
+ this.#helperView.setUint8(pos++, value >> 8);
+ this.#helperView.setUint8(pos++, value);
+ break;
+ default:
+ throw new Error("Bad EBML VINT size " + width);
+ }
+ this.#writer.write(this.#helper.subarray(0, pos));
+ }
+ // Assumes the string is ASCII
+ #writeString(str) {
+ this.#writer.write(new Uint8Array(str.split("").map((x) => x.charCodeAt(0))));
+ }
+ writeEBML(data) {
+ if (data === null) return;
+ if (data instanceof Uint8Array) {
+ this.#writer.write(data);
+ } else if (Array.isArray(data)) {
+ for (let elem of data) {
+ this.writeEBML(elem);
+ }
+ } else {
+ this.offsets.set(data, this.#writer.getPos());
+ this.#writeUnsignedInt(data.id);
+ if (Array.isArray(data.data)) {
+ let sizePos = this.#writer.getPos();
+ let sizeSize = data.size === -1 ? 1 : data.size ?? 4;
+ if (data.size === -1) {
+ this.#writeByte(255);
+ } else {
+ this.#writer.seek(this.#writer.getPos() + sizeSize);
+ }
+ let startPos = this.#writer.getPos();
+ this.dataOffsets.set(data, startPos);
+ this.writeEBML(data.data);
+ if (data.size !== -1) {
+ let size = this.#writer.getPos() - startPos;
+ let endPos = this.#writer.getPos();
+ this.#writer.seek(sizePos);
+ this.writeEBMLVarInt(size, sizeSize);
+ this.#writer.seek(endPos);
+ }
+ } else if (typeof data.data === "number") {
+ let size = data.size ?? measureUnsignedInt(data.data);
+ this.writeEBMLVarInt(size);
+ this.#writeUnsignedInt(data.data, size);
+ } else if (typeof data.data === "string") {
+ this.writeEBMLVarInt(data.data.length);
+ this.#writeString(data.data);
+ } else if (data.data instanceof Uint8Array) {
+ this.writeEBMLVarInt(data.data.byteLength, data.size);
+ this.#writer.write(data.data);
+ } else if (data.data instanceof EBMLFloat32) {
+ this.writeEBMLVarInt(4);
+ this.#writeFloat32(data.data.value);
+ } else if (data.data instanceof EBMLFloat64) {
+ this.writeEBMLVarInt(8);
+ this.#writeFloat64(data.data.value);
+ }
+ }
+ }
+ beforeTrackAdd(track) {
+ if (!(this.#format instanceof WebMOutputFormat)) {
+ return;
+ }
+ if (track.type === "video") {
+ if (!["vp8", "vp9", "av1"].includes(track.source.codec)) {
+ throw new Error(`WebM only supports VP8, VP9 and AV1 as video codecs. Switching to MKV removes this restriction.`);
+ }
+ } else {
+ if (!["opus", "vorbis"].includes(track.source.codec)) {
+ throw new Error(`WebM only supports Opus and Vorbis as audio codecs. Switching to MKV removes this restriction.`);
+ }
+ }
+ }
+ start() {
+ this.#writeEBMLHeader();
+ if (!this.#format.options.streaming) {
+ this.#createSeekHead();
+ }
+ this.#createSegmentInfo();
+ this.#createCues();
+ this.#writer.flush();
+ }
+ #writeEBMLHeader() {
+ let ebmlHeader = { id: 440786851 /* EBML */, data: [
+ { id: 17030 /* EBMLVersion */, data: 1 },
+ { id: 17143 /* EBMLReadVersion */, data: 1 },
+ { id: 17138 /* EBMLMaxIDLength */, data: 4 },
+ { id: 17139 /* EBMLMaxSizeLength */, data: 8 },
+ { id: 17026 /* DocType */, data: this.#format instanceof WebMOutputFormat ? "webm" : "matroska" },
+ { id: 17031 /* DocTypeVersion */, data: 2 },
+ { id: 17029 /* DocTypeReadVersion */, data: 2 }
+ ] };
+ this.writeEBML(ebmlHeader);
+ }
+ /**
+ * Creates a SeekHead element which is positioned near the start of the file and allows the media player to seek to
+ * relevant sections more easily. Since we don't know the positions of those sections yet, we'll set them later.
+ */
+ #createSeekHead() {
+ const kaxCues = new Uint8Array([28, 83, 187, 107]);
+ const kaxInfo = new Uint8Array([21, 73, 169, 102]);
+ const kaxTracks = new Uint8Array([22, 84, 174, 107]);
+ let seekHead = { id: 290298740 /* SeekHead */, data: [
+ { id: 19899 /* Seek */, data: [
+ { id: 21419 /* SeekID */, data: kaxCues },
+ { id: 21420 /* SeekPosition */, size: 5, data: 0 }
+ ] },
+ { id: 19899 /* Seek */, data: [
+ { id: 21419 /* SeekID */, data: kaxInfo },
+ { id: 21420 /* SeekPosition */, size: 5, data: 0 }
+ ] },
+ { id: 19899 /* Seek */, data: [
+ { id: 21419 /* SeekID */, data: kaxTracks },
+ { id: 21420 /* SeekPosition */, size: 5, data: 0 }
+ ] }
+ ] };
+ this.#seekHead = seekHead;
+ }
+ #createSegmentInfo() {
+ let segmentDuration = { id: 17545 /* Duration */, data: new EBMLFloat64(0) };
+ this.#segmentDuration = segmentDuration;
+ let segmentInfo = { id: 357149030 /* Info */, data: [
+ { id: 2807729 /* TimestampScale */, data: 1e6 },
+ { id: 19840 /* MuxingApp */, data: APP_NAME },
+ { id: 22337 /* WritingApp */, data: APP_NAME },
+ !this.#format.options.streaming ? segmentDuration : null
+ ] };
+ this.#segmentInfo = segmentInfo;
+ }
+ #createTracks() {
+ let tracksElement = { id: 374648427 /* Tracks */, data: [] };
+ this.#tracksElement = tracksElement;
+ for (let trackData of this.#trackDatas) {
+ tracksElement.data.push({ id: 174 /* TrackEntry */, data: [
+ { id: 215 /* TrackNumber */, data: trackData.track.id },
+ { id: 29637 /* TrackUID */, data: trackData.track.id },
+ { id: 131 /* TrackType */, data: trackData.type === "video" ? VIDEO_TRACK_TYPE : AUDIO_TRACK_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.track.source.metadata.frameRate ? { id: 2352003 /* DefaultDuration */, data: 1e9 / trackData.track.source.metadata.frameRate } : null,
+ { id: 224 /* Video */, data: [
+ { id: 176 /* PixelWidth */, data: trackData.info.width },
+ { id: 186 /* PixelHeight */, data: trackData.info.height },
+ (() => {
+ if (trackData.info.decoderConfig.colorSpace) {
+ let colorSpace = trackData.info.decoderConfig.colorSpace;
+ if (!colorSpace.matrix || !colorSpace.transfer || !colorSpace.primaries || colorSpace.fullRange == null) {
+ return null;
+ }
+ return { id: 21936 /* Colour */, data: [
+ { id: 21937 /* MatrixCoefficients */, data: {
+ "rgb": 1,
+ "bt709": 1,
+ "bt470bg": 5,
+ "smpte170m": 6
+ }[colorSpace.matrix] },
+ { id: 21946 /* TransferCharacteristics */, data: {
+ "bt709": 1,
+ "smpte170m": 6,
+ "iec61966-2-1": 13
+ }[colorSpace.transfer] },
+ { id: 21947 /* Primaries */, data: {
+ "bt709": 1,
+ "bt470bg": 5,
+ "smpte170m": 6
+ }[colorSpace.primaries] },
+ { id: 21945 /* Range */, data: [1, 2][Number(colorSpace.fullRange)] }
+ ] };
+ }
+ return null;
+ })()
+ ] }
+ ] : [],
+ ...trackData.type === "audio" ? [
+ { 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
+ ] }
+ ] : []
+ ] });
+ }
+ }
+ #createSegment() {
+ let segment = {
+ id: 408125543 /* Segment */,
+ size: this.#format.options.streaming ? -1 : SEGMENT_SIZE_BYTES,
+ data: [
+ !this.#format.options.streaming ? this.#seekHead : null,
+ this.#segmentInfo,
+ this.#tracksElement
+ ]
+ };
+ this.#segment = segment;
+ this.writeEBML(segment);
+ }
+ #createCues() {
+ this.#cues = { id: 475249515 /* Cues */, data: [] };
+ }
+ get #segmentDataOffset() {
+ assert(this.#segment);
+ return this.dataOffsets.get(this.#segment);
+ }
+ #getVideoTrackData(track, chunk, meta) {
+ const existingTrackData = this.#trackDatas.find((x) => x.track === track);
+ if (existingTrackData) {
+ return existingTrackData;
+ }
+ assert(meta);
+ assert(meta.decoderConfig);
+ assert(meta.decoderConfig.codedWidth !== void 0);
+ assert(meta.decoderConfig.codedHeight !== void 0);
+ const newTrackData = {
+ track,
+ type: "video",
+ info: {
+ width: meta.decoderConfig.codedWidth,
+ height: meta.decoderConfig.codedHeight,
+ decoderConfig: meta.decoderConfig
+ },
+ chunkQueue: [],
+ firstTimestamp: null,
+ lastTimestamp: null,
+ lastWrittenTimestamp: null
+ };
+ this.#trackDatas.push(newTrackData);
+ this.#trackDatas.sort((a, b) => a.track.id - b.track.id);
+ return newTrackData;
+ }
+ #getAudioTrackData(track, chunk, meta) {
+ const existingTrackData = this.#trackDatas.find((x) => x.track === track);
+ if (existingTrackData) {
+ return existingTrackData;
+ }
+ assert(meta);
+ assert(meta.decoderConfig);
+ const newTrackData = {
+ track,
+ type: "audio",
+ info: {
+ numberOfChannels: meta.decoderConfig.numberOfChannels,
+ sampleRate: meta.decoderConfig.sampleRate,
+ decoderConfig: meta.decoderConfig
+ },
+ chunkQueue: [],
+ firstTimestamp: null,
+ lastTimestamp: null,
+ lastWrittenTimestamp: null
+ };
+ this.#trackDatas.push(newTrackData);
+ this.#trackDatas.sort((a, b) => a.track.id - b.track.id);
+ return newTrackData;
+ }
+ addEncodedVideoChunk(track, chunk, meta, compositionTimeOffset) {
+ const trackData = this.#getVideoTrackData(track, chunk, meta);
+ let videoChunk = this.#createInternalChunk(trackData, chunk);
+ if (track.source.codec === "vp9") this.#fixVP9ColorSpace(trackData, videoChunk);
+ trackData.lastTimestamp = videoChunk.timestamp;
+ trackData.chunkQueue.push(videoChunk);
+ this.#interleaveChunks();
+ this.#writer.flush();
+ }
+ addEncodedAudioChunk(track, chunk, meta) {
+ const trackData = this.#getAudioTrackData(track, chunk, meta);
+ let audioChunk = this.#createInternalChunk(trackData, chunk);
+ trackData.lastTimestamp = audioChunk.timestamp;
+ trackData.chunkQueue.push(audioChunk);
+ this.#interleaveChunks();
+ this.#writer.flush();
+ }
+ #interleaveChunks() {
+ if (this.#trackDatas.length < this.output.tracks.length) {
+ return;
+ }
+ outer:
+ while (true) {
+ let trackWithMinTimestamp = null;
+ let minTimestamp = Infinity;
+ for (let trackData of this.#trackDatas) {
+ if (trackData.chunkQueue.length === 0) {
+ break outer;
+ }
+ if (trackData.chunkQueue[0].timestamp < minTimestamp) {
+ trackWithMinTimestamp = trackData;
+ minTimestamp = trackData.chunkQueue[0].timestamp;
+ }
+ }
+ if (!trackWithMinTimestamp) {
+ break;
+ }
+ let chunk = trackWithMinTimestamp.chunkQueue.shift();
+ this.#writeBlock(trackWithMinTimestamp, chunk);
+ }
+ }
+ /** Due to [a bug in Chromium](https://bugs.chromium.org/p/chromium/issues/detail?id=1377842), VP9 streams often
+ * lack color space information. This method patches in that information. */
+ // http://downloads.webmproject.org/docs/vp9/vp9-bitstream_superframe-and-uncompressed-header_v1.0.pdf
+ #fixVP9ColorSpace(trackData, chunk) {
+ if (chunk.type !== "key") return;
+ if (!trackData.info.decoderConfig.colorSpace || !trackData.info.decoderConfig.colorSpace.matrix) return;
+ let i = 0;
+ if (readBits(chunk.data, 0, 2) !== 2) return;
+ i += 2;
+ let profile = (readBits(chunk.data, i + 1, i + 2) << 1) + readBits(chunk.data, i + 0, i + 1);
+ i += 2;
+ if (profile === 3) i++;
+ let showExistingFrame = readBits(chunk.data, i + 0, i + 1);
+ i++;
+ if (showExistingFrame) return;
+ let frameType = readBits(chunk.data, i + 0, i + 1);
+ i++;
+ if (frameType !== 0) return;
+ i += 2;
+ let syncCode = readBits(chunk.data, i + 0, i + 24);
+ i += 24;
+ if (syncCode !== 4817730) return;
+ if (profile >= 2) i++;
+ let colorSpaceID = {
+ "rgb": 7,
+ "bt709": 2,
+ "bt470bg": 1,
+ "smpte170m": 3
+ }[trackData.info.decoderConfig.colorSpace.matrix];
+ writeBits(chunk.data, i + 0, i + 3, colorSpaceID);
+ }
+ /*
+ addSubtitleChunk(chunk: EncodedSubtitleChunk, meta: EncodedSubtitleChunkMetadata, timestamp?: number) {
+ if (typeof chunk !== 'object' || !chunk) {
+ throw new TypeError("addSubtitleChunk's first argument (chunk) must be an object.");
+ } else {
+ // We can't simply do an instanceof check, so let's check the structure itself:
+ if (!(chunk.body instanceof Uint8Array)) {
+ throw new TypeError('body must be an instance of Uint8Array.');
+ }
+ if (!Number.isFinite(chunk.timestamp) || chunk.timestamp < 0) {
+ throw new TypeError('timestamp must be a non-negative real number.');
+ }
+ if (!Number.isFinite(chunk.duration) || chunk.duration < 0) {
+ throw new TypeError('duration must be a non-negative real number.');
+ }
+ if (chunk.additions && !(chunk.additions instanceof Uint8Array)) {
+ throw new TypeError('additions, when present, must be an instance of Uint8Array.');
+ }
+ }
+
+ if (typeof meta !== 'object') {
+ throw new TypeError("addSubtitleChunk's second argument (meta) must be an object.");
+ }
+
+ this.#ensureNotFinalized();
+ if (!this.#options.subtitles) throw new Error('No subtitle track declared.');
+
+ // Write possible subtitle decoder metadata to the file
+ if (meta?.decoderConfig) {
+ if (this.#options.streaming) {
+ this.#subtitleCodecPrivate = this.#createCodecPrivateElement(meta.decoderConfig.description);
+ } else {
+ this.#writeCodecPrivate(this.#subtitleCodecPrivate, meta.decoderConfig.description);
+ }
+ }
+
+ let subtitleChunk = this.#createInternalChunk(
+ chunk.body,
+ 'key',
+ timestamp ?? chunk.timestamp,
+ SUBTITLE_TRACK_NUMBER,
+ chunk.duration,
+ chunk.additions
+ );
+
+ this.#lastSubtitleTimestamp = subtitleChunk.timestamp;
+ this.#subtitleChunkQueue.push(subtitleChunk);
+
+ this.#writeSubtitleChunks();
+ this.#maybeFlushStreamingTargetWriter();
+ }
+
+ #writeSubtitleChunks() {
+ // Writing subtitle chunks is different from video and audio: A subtitle chunk will be written if it's
+ // guaranteed that no more media chunks will be written before it, to ensure monotonicity. However, media chunks
+ // will NOT wait for subtitle chunks to arrive, as they may never arrive, so that's how non-monotonicity can
+ // arrive. But it should be fine, since it's all still in one cluster.
+
+ let lastWrittenMediaTimestamp = Math.min(
+ this.#options.video ? this.#lastVideoTimestamp : Infinity,
+ this.#options.audio ? this.#lastAudioTimestamp : Infinity
+ );
+
+ let queue = this.#subtitleChunkQueue;
+ while (queue.length > 0 && queue[0].timestamp <= lastWrittenMediaTimestamp) {
+ this.#writeBlock(queue.shift(), !this.#options.video && !this.#options.audio);
+ }
+ }
+ */
+ /** Converts a read-only external chunk into an internal one for easier use. */
+ #createInternalChunk(trackData, chunk) {
+ let adjustedTimestamp = this.#validateTimestamp(trackData, chunk.timestamp);
+ let data = new Uint8Array(chunk.byteLength);
+ chunk.copyTo(data);
+ let internalChunk = {
+ data,
+ type: chunk.type,
+ timestamp: adjustedTimestamp,
+ duration: chunk.duration,
+ additions: null
+ };
+ return internalChunk;
+ }
+ #validateTimestamp(trackData, timestamp) {
+ if (timestamp < 0) {
+ throw new Error(`Timestamps must be non-negative (got ${timestamp}s).`);
+ }
+ if (trackData.firstTimestamp === null) {
+ trackData.firstTimestamp = timestamp;
+ }
+ timestamp -= trackData.firstTimestamp;
+ if (trackData.lastTimestamp !== null && timestamp < trackData.lastTimestamp) {
+ throw new Error(
+ `Timestamps must be monotonically increasing (timestamp went from ${trackData.lastTimestamp}s to ${timestamp}s).`
+ );
+ }
+ return timestamp;
+ }
+ /** Writes a block containing media data to the file. */
+ #writeBlock(trackData, chunk) {
+ if (!this.#segment) {
+ this.#createTracks();
+ this.#createSegment();
+ }
+ let msTimestamp = Math.floor(chunk.timestamp / 1e3);
+ const keyFrameQueuedEverywhere = this.#trackDatas.every((otherTrackData) => {
+ if (trackData === otherTrackData) {
+ return chunk.type === "key";
+ }
+ const firstQueuedSample = otherTrackData.chunkQueue[0];
+ return firstQueuedSample && firstQueuedSample.type === "key";
+ });
+ if (!this.#currentCluster || keyFrameQueuedEverywhere && msTimestamp - this.#currentClusterTimestamp >= 1e3) {
+ this.#createNewCluster(msTimestamp);
+ }
+ let relativeTimestamp = msTimestamp - this.#currentClusterTimestamp;
+ if (relativeTimestamp < 0) {
+ return;
+ }
+ let clusterIsTooLong = relativeTimestamp >= MAX_CHUNK_LENGTH_MS;
+ if (clusterIsTooLong) {
+ throw new Error(
+ `Current Matroska cluster exceeded its maximum allowed length of ${MAX_CHUNK_LENGTH_MS} milliseconds. In order to produce a correct WebM file, you must pass in a key frame at least every ${MAX_CHUNK_LENGTH_MS} milliseconds.`
+ );
+ }
+ let prelude = new Uint8Array(4);
+ let view2 = new DataView(prelude.buffer);
+ view2.setUint8(0, 128 | trackData.track.id);
+ view2.setInt16(1, relativeTimestamp, false);
+ let msDuration = Math.floor((chunk.duration ?? 0) / 1e3);
+ if (msDuration === 0 && !chunk.additions) {
+ view2.setUint8(3, Number(chunk.type === "key") << 7);
+ let simpleBlock = { id: 163 /* SimpleBlock */, data: [
+ prelude,
+ chunk.data
+ ] };
+ this.writeEBML(simpleBlock);
+ } else {
+ let blockGroup = { id: 160 /* BlockGroup */, data: [
+ { id: 161 /* Block */, data: [
+ prelude,
+ chunk.data
+ ] },
+ chunk.type === "delta" ? { id: 251 /* ReferenceBlock */, data: trackData.lastWrittenTimestamp - msTimestamp } : null,
+ chunk.duration !== null ? { id: 155 /* BlockDuration */, data: msDuration } : null,
+ chunk.additions ? { id: 30113 /* BlockAdditions */, data: chunk.additions } : null
+ ] };
+ this.writeEBML(blockGroup);
+ }
+ this.#duration = Math.max(this.#duration, msTimestamp + msDuration);
+ trackData.lastWrittenTimestamp = msTimestamp;
+ this.#trackDatasInCurrentCluster.add(trackData);
+ }
+ /** Creates a new Cluster element to contain media chunks. */
+ #createNewCluster(timestamp) {
+ if (this.#currentCluster && !this.#format.options.streaming) {
+ this.#finalizeCurrentCluster();
+ }
+ this.#currentCluster = {
+ id: 524531317 /* Cluster */,
+ size: this.#format.options.streaming ? -1 : CLUSTER_SIZE_BYTES,
+ data: [
+ { id: 231 /* Timestamp */, data: timestamp }
+ ]
+ };
+ this.writeEBML(this.#currentCluster);
+ this.#currentClusterTimestamp = timestamp;
+ this.#trackDatasInCurrentCluster.clear();
+ }
+ #finalizeCurrentCluster() {
+ assert(this.#currentCluster);
+ let clusterSize = this.#writer.getPos() - this.dataOffsets.get(this.#currentCluster);
+ let endPos = this.#writer.getPos();
+ this.#writer.seek(this.offsets.get(this.#currentCluster) + 4);
+ this.writeEBMLVarInt(clusterSize, CLUSTER_SIZE_BYTES);
+ this.#writer.seek(endPos);
+ let clusterOffsetFromSegment = this.offsets.get(this.#currentCluster) - this.#segmentDataOffset;
+ assert(this.#cues);
+ this.#cues.data.push({ id: 187 /* CuePoint */, data: [
+ { id: 179 /* CueTime */, data: this.#currentClusterTimestamp },
+ // We only write out cues for tracks that have at least one chunk in this cluster
+ ...[...this.#trackDatasInCurrentCluster].map((trackData) => {
+ return { id: 183 /* CueTrackPositions */, data: [
+ { id: 247 /* CueTrack */, data: trackData.track.id },
+ { id: 241 /* CueClusterPosition */, data: clusterOffsetFromSegment }
+ ] };
+ })
+ ] });
+ }
+ /** Finalizes the file, making it ready for use. Must be called after all media chunks have been added. */
+ finalize() {
+ for (let trackData of this.#trackDatas) {
+ while (trackData.chunkQueue.length > 0) {
+ this.#writeBlock(trackData, trackData.chunkQueue.shift());
+ }
+ }
+ if (!this.#format.options.streaming) {
+ this.#finalizeCurrentCluster();
+ }
+ assert(this.#cues);
+ this.writeEBML(this.#cues);
+ if (!this.#format.options.streaming) {
+ let endPos = this.#writer.getPos();
+ let segmentSize = this.#writer.getPos() - this.#segmentDataOffset;
+ this.#writer.seek(this.offsets.get(this.#segment) + 4);
+ this.writeEBMLVarInt(segmentSize, SEGMENT_SIZE_BYTES);
+ this.#segmentDuration.data = new EBMLFloat64(this.#duration);
+ this.#writer.seek(this.offsets.get(this.#segmentDuration));
+ this.writeEBML(this.#segmentDuration);
+ this.#seekHead.data[0].data[1].data = this.offsets.get(this.#cues) - this.#segmentDataOffset;
+ this.#seekHead.data[1].data[1].data = this.offsets.get(this.#segmentInfo) - this.#segmentDataOffset;
+ this.#seekHead.data[2].data[1].data = this.offsets.get(this.#tracksElement) - this.#segmentDataOffset;
+ this.#writer.seek(this.offsets.get(this.#seekHead));
+ this.writeEBML(this.#seekHead);
+ this.#writer.seek(endPos);
+ }
+ }
+ };
+
// src/output_format.ts
var OutputFormat = class {
};
@@ -1706,6 +2456,17 @@ var Metamuxer = (() => {
return new IsobmffMuxer(output, this);
}
};
+ var MkvOutputFormat2 = class extends OutputFormat {
+ constructor(options = {}) {
+ super();
+ this.options = options;
+ }
+ createMuxer(output) {
+ return new MatroskaMuxer(output, this);
+ }
+ };
+ var WebMOutputFormat = class extends MkvOutputFormat2 {
+ };
// src/writer.ts
var Writer = class {
diff --git a/dist/metamuxer.min.js b/dist/metamuxer.min.js
index 8d5f5c9..8eb851b 100644
--- a/dist/metamuxer.min.js
+++ b/dist/metamuxer.min.js
@@ -1,2 +1,2 @@
-"use strict";var Metamuxer=(()=>{var re=Object.defineProperty;var xe=Object.getOwnPropertyDescriptor;var ye=Object.getOwnPropertyNames;var ke=Object.prototype.hasOwnProperty;var Se=(t,r)=>{for(var e in r)re(t,e,{get:r[e],enumerable:!0})},we=(t,r,e,s)=>{if(r&&typeof r=="object"||typeof r=="function")for(let o of ye(r))!ke.call(t,o)&&o!==e&&re(t,o,{get:()=>r[o],enumerable:!(s=xe(r,o))||s.enumerable});return t};var ve=t=>we(re({},"__esModule",{value:!0}),t);var gt={};Se(gt,{ArrayBufferTarget:()=>ee,AudioBufferSource:()=>$,AudioDataSource:()=>R,CanvasSource:()=>W,FileSystemWritableFileStreamTarget:()=>te,MediaStreamAudioTrackSource:()=>H,MediaStreamVideoTrackSource:()=>D,Mp4OutputFormat:()=>G,Output:()=>Q,StreamTarget:()=>I,Target:()=>O,VideoFrameSource:()=>_});var le=(t,r,e)=>{if(t==="avc"){let s=100;r<=768&&e<=432?s=66:r<=1920&&e<=1080&&(s=77);let o=0,i=r>1920||e>1080?50:41,a=s.toString(16).padStart(2,"0"),l=o.toString(16).padStart(2,"0"),u=i.toString(16).padStart(2,"0");return`avc1.${a}${l}${u}`}else if(t==="hevc"){let s=0,o=1,i=Array(32).fill(0);i[o]=1;let a=parseInt(i.reverse().join(""),2).toString(16).replace(/^0+/,""),l="L",u=120;return r<=1280&&e<=720?u=93:r<=1920&&e<=1080?u=120:r<=3840&&e<=2160?u=150:(l="H",u=180),`hev1.${s===0?"":String.fromCharCode(65+s-1)}${o}.${a}.${l}${u}.B0`}else{if(t==="vp8")return"vp8";if(t==="vp9"){let s="00",o;return r<=854&&e<=480?o="21":r<=1280&&e<=720?o="31":r<=1920&&e<=1080?o="41":r<=3840&&e<=2160?o="51":o="61",`vp09.${s}.${o}.08`}else if(t==="av1"){let o;return r<=854&&e<=480?o="01":r<=1280&&e<=720?o="03":r<=1920&&e<=1080?o="04":r<=3840&&e<=2160?o="07":o="09",`av01.0.${o}M.08`}}throw new Error(`Unhandled codec '${t}'.`)},de=(t,r,e)=>{if(t==="aac")return r>=2&&e<=24e3?"mp4a.40.29":e<=24e3?"mp4a.40.5":"mp4a.40.2";if(t==="opus")return"opus";if(t==="vorbis")return"vorbis";throw new Error(`Unhandled codec '${t}'.`)};function c(t){if(!t)throw new Error("Assertion failed.")}var z=t=>t&&t[t.length-1],w=t=>t>=0&&t<2**32;var v=class{constructor(r,e){this.connectedTrack=null;this.codec=r,this.metadata=e}ensureNotFinalizing(){if(this.connectedTrack?.output.finalizing)throw new Error("Cannot call digest after output has started finalizing.")}start(){}async flush(){}},M=class{constructor(r,e){this.connectedTrack=null;this.codec=r,this.metadata=e}ensureNotFinalizing(){if(this.connectedTrack?.output.finalizing)throw new Error("Cannot call digest after output has started finalizing.")}start(){}async flush(){}};var Ae=5,V=class{constructor(r,e){this.source=r;this.codecConfig=e;this.encoder=null;this.lastMultipleOfKeyFrameInterval=-1}digest(r){this.source.ensureNotFinalizing(),this.ensureEncoder(r),c(this.encoder);let e=Math.floor(r.timestamp/1e6/Ae);this.encoder.encode(r,{keyFrame:e!==this.lastMultipleOfKeyFrameInterval}),this.lastMultipleOfKeyFrameInterval=e}ensureEncoder(r){this.encoder||(this.encoder=new VideoEncoder({output:(e,s)=>this.source.connectedTrack?.output.muxer.addEncodedVideoChunk(this.source.connectedTrack,e,s),error:e=>console.error(e)}),this.encoder.configure({codec:le(this.codecConfig.codec,r.codedWidth,r.codedHeight),width:r.codedWidth,height:r.codedHeight,bitrate:this.codecConfig.bitrate}))}async flush(){return this.encoder?.flush()}},_=class extends v{constructor(r,e={}){super(r.codec,e),this.encoder=new V(this,r)}digest(r){this.encoder.digest(r)}flush(){return this.encoder.flush()}},W=class extends v{constructor(e,s,o={}){super(s.codec,o);this.canvas=e;this.encoder=new V(this,s)}digest(e,s=0){let o=new VideoFrame(this.canvas,{timestamp:Math.round(1e6*e),duration:Math.round(1e6*s)});this.encoder.digest(o),o.close()}flush(){return this.encoder.flush()}},D=class extends v{constructor(e,s,o={}){super(s.codec,o);this.track=e;this.abortController=null;this.encoder=new V(this,s)}start(){this.abortController=new AbortController;let e=new MediaStreamTrackProcessor({track:this.track}),s=new WritableStream({write:o=>{this.encoder.digest(o),o.close()}});e.readable.pipeTo(s,{signal:this.abortController.signal}).catch(o=>{o instanceof DOMException&&o.name==="AbortError"||console.error("Pipe error:",o)})}async flush(){this.abortController&&(this.abortController.abort(),this.abortController=null),await this.encoder.flush()}};var B=class{constructor(r,e){this.source=r;this.codecConfig=e;this.encoder=null}digest(r){this.source.ensureNotFinalizing(),this.ensureEncoder(r),c(this.encoder),this.encoder.encode(r)}ensureEncoder(r){this.encoder||(this.encoder=new AudioEncoder({output:(e,s)=>this.source.connectedTrack?.output.muxer.addEncodedAudioChunk(this.source.connectedTrack,e,s),error:e=>console.error(e)}),this.encoder.configure({codec:de(this.codecConfig.codec,r.numberOfChannels,r.sampleRate),numberOfChannels:r.numberOfChannels,sampleRate:r.sampleRate,bitrate:this.codecConfig.bitrate}))}async flush(){return this.encoder?.flush()}},R=class extends M{constructor(r,e={}){super(r.codec,e),this.encoder=new B(this,r)}digest(r){this.encoder.digest(r)}flush(){return this.encoder.flush()}},$=class extends M{constructor(e,s={}){super(e.codec,s);this.accumulatedFrameCount=0;this.encoder=new B(this,e)}digest(e){let s=e.numberOfChannels,o=e.sampleRate,i=e.length,a=new Float32Array(s*i);for(let u=0;u{this.encoder.digest(o),o.close()}});e.readable.pipeTo(s,{signal:this.abortController.signal}).catch(o=>{o instanceof DOMException&&o.name==="AbortError"||console.error("Pipe error:",o)})}async flush(){this.abortController&&(this.abortController.abort(),this.abortController=null),await this.encoder.flush()}};var Q=class{constructor(r){this.tracks=[];this.started=!1;this.finalizing=!1;this.writer=r.target.createWriter(),this.muxer=r.format.createMuxer(this)}addTrack(r){if(this.started)throw new Error("Cannot add track after output has started.");if(r.connectedTrack)throw new Error("Source is already used for a track.");let e={id:this.tracks.length+1,output:this,type:r instanceof v?"video":"audio",source:r};this.tracks.push(e),r.connectedTrack=e}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()}};var d=new Uint8Array(8),x=new DataView(d.buffer),b=t=>[(t%256+256)%256],m=t=>(x.setUint16(0,t,!1),[d[0],d[1]]),Oe=t=>(x.setInt16(0,t,!1),[d[0],d[1]]),fe=t=>(x.setUint32(0,t,!1),[d[1],d[2],d[3]]),n=t=>(x.setUint32(0,t,!1),[d[0],d[1],d[2],d[3]]),ze=t=>(x.setInt32(0,t,!1),[d[0],d[1],d[2],d[3]]),A=t=>(x.setUint32(0,Math.floor(t/2**32),!1),x.setUint32(4,t,!1),[d[0],d[1],d[2],d[3],d[4],d[5],d[6],d[7]]),oe=t=>(x.setInt16(0,2**8*t,!1),[d[0],d[1]]),g=t=>(x.setInt32(0,2**16*t,!1),[d[0],d[1],d[2],d[3]]),se=t=>(x.setInt32(0,2**30*t,!1),[d[0],d[1],d[2],d[3]]),C=(t,r=!1)=>{let e=Array(t.length).fill(null).map((s,o)=>t.charCodeAt(o));return r&&e.push(0),e},ie=t=>{let r=null;for(let e of t)(!r||e.presentationTimestamp>r.presentationTimestamp)&&(r=e);return r},me=t=>{let r=t*(Math.PI/180),e=Math.cos(r),s=Math.sin(r);return[e,s,0,-s,e,0,0,0,1]},he=me(0),pe=t=>[g(t[0]),g(t[1]),se(t[2]),g(t[3]),g(t[4]),se(t[5]),g(t[6]),g(t[7]),se(t[8])],p=(t,r,e)=>({type:t,contents:r&&new Uint8Array(r.flat(10)),children:e}),h=(t,r,e,s,o)=>p(t,[b(r),fe(e),s??[]],o),be=t=>{let r=512;return t.fragmented?p("ftyp",[C("iso5"),n(r),C("iso5"),C("iso6"),C("mp41")]):p("ftyp",[C("isom"),n(r),C("isom"),t.holdsAvc?C("avc1"):[],C("mp41")])},q=t=>({type:"mdat",largeSize:t}),Ce=t=>({type:"free",size:t}),U=(t,r,e=!1)=>p("moov",void 0,[Ie(r,t),...t.map(s=>Ee(s,r)),e?rt(t):null]),Ie=(t,r)=>{let e=T(Math.max(0,...r.filter(a=>a.samples.length>0).map(a=>{let l=ie(a.samples);return l.presentationTimestamp+l.duration})),j),s=Math.max(...r.map(a=>a.track.id))+1,o=!w(t)||!w(e),i=o?A:n;return h("mvhd",+o,0,[i(t),i(t),n(j),i(e),g(1),oe(1),Array(10).fill(0),pe(he),Array(24).fill(0),n(s)])},Ee=(t,r)=>p("trak",void 0,[Me(t,r),Ve(t,r)]),Me=(t,r)=>{let e=ie(t.samples),s=T(e?e.presentationTimestamp+e.duration:0,j),o=!w(r)||!w(s),i=o?A:n,a;if(t.type==="video"){let l=t.track.source.metadata.rotation;a=l===void 0||typeof l=="number"?me(l??0):l}else a=he;return h("tkhd",+o,3,[i(r),i(r),n(t.track.id),n(0),i(s),Array(8).fill(0),m(0),m(0),oe(t.type==="audio"?1:0),m(0),pe(a),g(t.type==="video"?t.info.width:0),g(t.type==="video"?t.info.height:0)])},Ve=(t,r)=>p("mdia",void 0,[Be(t,r),Ue(t.type==="video"?"vide":"soun"),Fe(t)]),Be=(t,r)=>{let e=ie(t.samples),s=T(e?e.presentationTimestamp+e.duration:0,t.timescale),o=!w(r)||!w(s),i=o?A:n;return h("mdhd",+o,0,[i(r),i(r),n(t.timescale),i(s),m(21956),m(0)])},Ue=t=>h("hdlr",0,0,[C("mhlr"),C(t),n(0),n(0),n(0),C("mp4-muxer-hdlr",!0)]),Fe=t=>p("minf",void 0,[t.type==="video"?Pe():Ne(),Le(),De(t)]),Pe=()=>h("vmhd",0,1,[m(0),m(0),m(0),m(0)]),Ne=()=>h("smhd",0,0,[m(0),m(0)]),Le=()=>p("dinf",void 0,[_e()]),_e=()=>h("dref",0,0,[n(1)],[We()]),We=()=>h("url ",0,1),De=t=>{let r=t.compositionTimeOffsetTable.length>1||t.compositionTimeOffsetTable.some(e=>e.sampleCompositionTimeOffset!==0);return p("stbl",void 0,[Re(t),Ge(t),Ye(t),Ze(t),Je(t),et(t),r?tt(t):null])},Re=t=>h("stsd",0,0,[n(1)],[t.type==="video"?$e(ct[t.track.source.codec],t):qe(mt[t.track.source.codec],t)]),$e=(t,r)=>p(t,[Array(6).fill(0),m(1),m(0),m(0),Array(12).fill(0),m(r.info.width),m(r.info.height),n(4718592),n(4718592),n(0),m(1),Array(32).fill(0),m(24),Oe(65535)],[ft[r.track.source.codec](r)]),He=t=>t.info.decoderConfig&&p("avcC",[...new Uint8Array(t.info.decoderConfig.description)]),Qe=t=>t.info.decoderConfig&&p("hvcC",[...new Uint8Array(t.info.decoderConfig.description)]),ce=t=>{if(!t.info.decoderConfig)return null;let r=t.info.decoderConfig;if(!r.colorSpace)throw new Error("'colorSpace' is required in the decoder config for VP8/VP9.");let e=r.codec.split("."),s=Number(e[1]),o=Number(e[2]),l=(Number(e[3])<<4)+(0<<1)+Number(r.colorSpace.fullRange);return h("vpcC",1,0,[b(s),b(o),b(l),b(2),b(2),b(2),m(0)])},je=()=>{let e=(1<<7)+1;return p("av1C",[e,0,0,0])},qe=(t,r)=>p(t,[Array(6).fill(0),m(1),m(0),m(0),n(0),m(r.info.numberOfChannels),m(16),m(0),m(0),g(r.info.sampleRate)],[ht[r.track.source.codec](r)]),Ke=t=>{let r=new Uint8Array(t.info.decoderConfig.description);return h("esds",0,0,[n(58753152),b(32+r.byteLength),m(1),b(0),n(75530368),b(18+r.byteLength),b(64),b(21),fe(0),n(130071),n(130071),n(92307584),b(r.byteLength),...r,n(109084800),b(1),b(2)])},Xe=t=>{let r=3840,e=0,s=t.info.decoderConfig?.description;if(s){if(s.byteLength<18)throw new TypeError("Invalid decoder description provided for Opus; must be at least 18 bytes long.");let o=ArrayBuffer.isView(s)?new DataView(s.buffer,s.byteOffset,s.byteLength):new DataView(s);r=o.getUint16(10,!0),e=o.getInt16(14,!0)}return p("dOps",[b(0),b(t.info.numberOfChannels),m(r),n(t.info.sampleRate),oe(e),b(0)])},Ge=t=>h("stts",0,0,[n(t.timeToSampleTable.length),t.timeToSampleTable.map(r=>[n(r.sampleCount),n(r.sampleDelta)])]),Ye=t=>{if(t.samples.every(e=>e.type==="key"))return null;let r=[...t.samples.entries()].filter(([,e])=>e.type==="key");return h("stss",0,0,[n(r.length),r.map(([e])=>n(e+1))])},Ze=t=>h("stsc",0,0,[n(t.compactlyCodedChunkTable.length),t.compactlyCodedChunkTable.map(r=>[n(r.firstChunk),n(r.samplesPerChunk),n(1)])]),Je=t=>h("stsz",0,0,[n(0),n(t.samples.length),t.samples.map(r=>n(r.size))]),et=t=>t.finalizedChunks.length>0&&z(t.finalizedChunks).offset>=2**32?h("co64",0,0,[n(t.finalizedChunks.length),t.finalizedChunks.map(r=>A(r.offset))]):h("stco",0,0,[n(t.finalizedChunks.length),t.finalizedChunks.map(r=>n(r.offset))]),tt=t=>h("ctts",0,0,[n(t.compositionTimeOffsetTable.length),t.compositionTimeOffsetTable.map(r=>[n(r.sampleCount),n(r.sampleCompositionTimeOffset)])]),rt=t=>p("mvex",void 0,t.map(st)),st=t=>h("trex",0,0,[n(t.track.id),n(1),n(0),n(0),n(0)]),ne=(t,r)=>p("moof",void 0,[ot(t),...r.map(it)]),ot=t=>h("mfhd",0,0,[n(t)]),Te=t=>{let r=0,e=0,s=0,o=0,i=t.type==="delta";return e|=+i,i?r|=1:r|=2,r<<24|e<<16|s<<8|o},it=t=>p("traf",void 0,[nt(t),at(t),ut(t)]),nt=t=>{c(t.currentChunk);let r=0;r|=8,r|=16,r|=32,r|=131072;let e=t.currentChunk.samples[1]??t.currentChunk.samples[0],s={duration:e.timescaleUnitsToNextSample,size:e.size,flags:Te(e)};return h("tfhd",0,r,[n(t.track.id),n(s.duration),n(s.size),n(s.flags)])},at=t=>(c(t.currentChunk),h("tfdt",1,0,[A(T(t.currentChunk.startTimestamp,t.timescale))])),ut=t=>{c(t.currentChunk);let r=t.currentChunk.samples.map(k=>k.timescaleUnitsToNextSample),e=t.currentChunk.samples.map(k=>k.size),s=t.currentChunk.samples.map(Te),o=t.currentChunk.samples.map(k=>T(k.presentationTimestamp-k.decodeTimestamp,t.timescale)),i=new Set(r),a=new Set(e),l=new Set(s),u=new Set(o),f=l.size===2&&s[0]!==s[1],y=i.size>1,E=a.size>1,N=!f&&l.size>1,ue=u.size>1||[...u].some(k=>k!==0),S=0;return S|=1,S|=4*+f,S|=256*+y,S|=512*+E,S|=1024*+N,S|=2048*+ue,h("trun",1,S,[n(t.currentChunk.samples.length),n(t.currentChunk.offset-t.currentChunk.moofOffset||0),f?n(s[0]):[],t.currentChunk.samples.map((k,L)=>[y?n(r[L]):[],E?n(e[L]):[],N?n(s[L]):[],ue?ze(o[L]):[]])])},ge=t=>p("mfra",void 0,[...t.map(lt),dt()]),lt=(t,r)=>h("tfra",1,0,[n(t.track.id),n(63),n(t.finalizedChunks.length),t.finalizedChunks.map(s=>[A(T(s.startTimestamp,t.timescale)),A(s.moofOffset),n(r+1),n(1),n(1)])]),dt=()=>h("mfro",0,0,[n(0)]),ct={avc:"avc1",hevc:"hvc1",vp8:"vp08",vp9:"vp09",av1:"av01"},ft={avc:He,hevc:Qe,vp8:ce,vp9:ce,av1:je},mt={aac:"mp4a",opus:"Opus"},ht={aac:Ke,opus:Xe};var K=class{constructor(r){this.output=r}};var j=1e3,pt=2082844800,T=(t,r,e=!0)=>{let s=t*r;return e?Math.round(s):s},X=class extends K{constructor(e,s){super(e);this.#r=new Uint8Array(8);this.#o=new DataView(this.#r.buffer);this.offsets=new WeakMap;this.#n=null;this.#i=null;this.#s=[];this.#a=Math.floor(Date.now()/1e3)+pt;this.#u=[];this.#h=1;this.#e=e.writer,this.#t=s}#e;#t;#r;#o;#n;#i;#s;#a;#u;#h;writeU32(e){this.#o.setUint32(0,e,!1),this.#e.write(this.#r.subarray(0,4))}writeU64(e){this.#o.setUint32(0,Math.floor(e/2**32),!1),this.#o.setUint32(4,e,!1),this.#e.write(this.#r.subarray(0,8))}writeAscii(e){for(let s=0;ss.type==="video"&&s.source.codec==="avc");if(this.writeBox(be({holdsAvc:e,fragmented:this.#t.options.fastStart==="fragmented"})),this.#n=this.#e.getPos(),this.#t.options.fastStart==="in-memory")this.#i=q(!1);else if(this.#t.options.fastStart!=="fragmented"){if(typeof this.#t.options.fastStart=="object"){let s=this.#b();this.#e.seek(this.#e.getPos()+s)}this.#i=q(!0),this.writeBox(this.#i)}this.#e.flush()}#b(){c(typeof this.#t.options.fastStart=="object");let e=0,s=[this.#t.options.fastStart.expectedVideoChunks,this.#t.options.fastStart.expectedAudioChunks];for(let o of s)o&&(e+=8*Math.ceil(2/3*o),e+=4*o,e+=12*Math.ceil(2/3*o),e+=4*o,e+=8*o);return e+=4096,e}#C(e,s,o){let i=this.#s.find(l=>l.track===e);if(i)return i;c(o),c(o.decoderConfig),c(o.decoderConfig.codedWidth),c(o.decoderConfig.codedHeight);let a={track:e,type:"video",info:{width:o.decoderConfig.codedWidth,height:o.decoderConfig.codedHeight,decoderConfig:o.decoderConfig},timescale:e.source.metadata.frameRate??57600,samples:[],sampleQueue:[],firstDecodeTimestamp:null,lastDecodeTimestamp:-1,timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.#s.push(a),this.#s.sort((l,u)=>l.track.id-u.track.id),a}#T(e,s,o){let i=this.#s.find(l=>l.track===e);if(i)return i;c(o),c(o.decoderConfig);let a={track:e,type:"audio",info:{numberOfChannels:o.decoderConfig.numberOfChannels,sampleRate:o.decoderConfig.sampleRate,decoderConfig:o.decoderConfig},timescale:o.decoderConfig.sampleRate,samples:[],sampleQueue:[],firstDecodeTimestamp:null,lastDecodeTimestamp:-1,timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.#s.push(a),this.#s.sort((l,u)=>l.track.id-u.track.id),a}addEncodedVideoChunk(e,s,o,i){let a=this.#C(e,s,o);if(typeof this.#t.options.fastStart=="object"&&a.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 l=this.#d(a,s,i);this.#t.options.fastStart==="fragmented"?(a.sampleQueue.push(l),this.#f()):this.#l(a,l)}addEncodedAudioChunk(e,s,o){let i=this.#T(e,s,o);if(typeof this.#t.options.fastStart=="object"&&i.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 a=this.#d(i,s);this.#t.options.fastStart==="fragmented"?(i.sampleQueue.push(a),this.#f()):this.#l(i,a)}#d(e,s,o){let i=s.timestamp/1e6,a=(s.timestamp-(o??0))/1e6,l=(s.duration??0)/1e6,u=this.#g(e,i,a);i=u.presentationTimestamp,a=u.decodeTimestamp;let f=new Uint8Array(s.byteLength);return s.copyTo(f),{presentationTimestamp:i,decodeTimestamp:a,duration:l,data:f,size:f.byteLength,type:s.type,timescaleUnitsToNextSample:T(l,e.timescale)}}#l(e,s){this.#t.options.fastStart!=="fragmented"&&e.samples.push(s);let o=T(s.presentationTimestamp-s.decodeTimestamp,e.timescale);if(e.lastTimescaleUnits!==null){c(e.lastSample);let a=T(s.decodeTimestamp,e.timescale,!1),l=Math.round(a-e.lastTimescaleUnits);if(e.lastTimescaleUnits+=l,e.lastSample.timescaleUnitsToNextSample=l,this.#t.options.fastStart!=="fragmented"){let u=z(e.timeToSampleTable);c(u),u.sampleCount===1?(u.sampleDelta=l,u.sampleCount++):u.sampleDelta===l?u.sampleCount++:(u.sampleCount--,e.timeToSampleTable.push({sampleCount:2,sampleDelta:l}));let f=z(e.compositionTimeOffsetTable);c(f),f.sampleCompositionTimeOffset===o?f.sampleCount++:e.compositionTimeOffsetTable.push({sampleCount:1,sampleCompositionTimeOffset:o})}}else e.lastTimescaleUnits=0,this.#t.options.fastStart!=="fragmented"&&(e.timeToSampleTable.push({sampleCount:1,sampleDelta:T(s.duration,e.timescale)}),e.compositionTimeOffsetTable.push({sampleCount:1,sampleCompositionTimeOffset:o}));e.lastSample=s;let i=!1;if(!e.currentChunk)i=!0;else{let a=s.presentationTimestamp-e.currentChunk.startTimestamp;if(this.#t.options.fastStart==="fragmented"){let l=this.#s.every(u=>{if(e===u)return s.type==="key";let f=u.sampleQueue[0];return f&&f.type==="key"});a>=1&&l&&(i=!0,this.#m())}else i=a>=.5}i&&(e.currentChunk&&this.#c(e),e.currentChunk={startTimestamp:s.presentationTimestamp,samples:[],offset:null,moofOffset:null}),c(e.currentChunk),e.currentChunk.samples.push(s)}#g(e,s,o){if(e.firstDecodeTimestamp===null&&(e.firstDecodeTimestamp=o),o-=e.firstDecodeTimestamp,s-=e.firstDecodeTimestamp,o=2**32&&(u.largeSize=!0,y=this.measureBox(u)+f),u.size=y,this.writeBox(u)}for(let u of this.#s){u.currentChunk.offset=this.#e.getPos(),u.currentChunk.moofOffset=o;for(let f of u.currentChunk.samples)this.#e.write(f.data),f.data=null}let a=this.#e.getPos();this.#e.seek(this.offsets.get(i));let l=ne(s,this.#s);this.writeBox(l),this.#e.seek(a);for(let u of this.#s)u.finalizedChunks.push(u.currentChunk),this.#u.push(u.currentChunk),u.currentChunk=null;e&&this.#e.flush()}finalize(){if(this.#t.options.fastStart==="fragmented"){for(let e of this.#s)for(let s of e.sampleQueue)this.#l(e,s);this.#m(!1)}else for(let e of this.#s)this.#c(e);if(this.#t.options.fastStart==="in-memory"){c(this.#i);let e;for(let o=0;o<2;o++){let i=U(this.#s,this.#a),a=this.measureBox(i);e=this.measureBox(this.#i);let l=this.#e.getPos()+a+e;for(let u of this.#u){u.offset=l;for(let{data:f}of u.samples)c(f),l+=f.byteLength,e+=f.byteLength}if(l<2**32)break;e>=2**32&&(this.#i.largeSize=!0)}let s=U(this.#s,this.#a);this.writeBox(s),this.#i.size=e,this.writeBox(this.#i);for(let o of this.#u)for(let i of o.samples)c(i.data),this.#e.write(i.data),i.data=null}else if(this.#t.options.fastStart==="fragmented"){let e=this.#e.getPos(),s=ge(this.#s);this.writeBox(s);let o=this.#e.getPos()-e;this.#e.seek(this.#e.getPos()-4),this.writeU32(o)}else{c(this.#i),c(this.#n!==null);let e=this.offsets.get(this.#i);c(e!==void 0);let s=this.#e.getPos()-e;this.#i.size=s,this.#i.largeSize=s>=2**32,this.patchBox(this.#i);let o=U(this.#s,this.#a);if(typeof this.#t.options.fastStart=="object"){this.#e.seek(this.#n),this.writeBox(o);let i=e-this.#e.getPos();this.writeBox(Ce(i))}else this.writeBox(o)}}};var ae=class{},G=class extends ae{constructor(e){super();this.options=e}createMuxer(e){return new X(e,this)}};var F=class{},Y=class extends F{#e=0;#t;#r=new ArrayBuffer(2**16);#o=new Uint8Array(this.#r);#n=0;constructor(r){super(),this.#t=r}#i(r){let e=this.#r.byteLength;for(;es.start-o.start);r.push({start:e[0].start,size:e[0].data.byteLength});for(let s=1;su.start<=e&&eCt){for(let u=0;u=r.written[i+1].start;)r.written[i].end=Math.max(r.written[i].end,r.written[i+1].end),r.written.splice(i+1,1)}#s(r){let s={start:Math.floor(r/this.#r)*this.#r,data:new Uint8Array(this.#r),written:[],shouldFlush:!1};return this.#o.push(s),this.#o.sort((o,i)=>o.start-i.start),this.#o.indexOf(s)}#a(r=!1){for(let e=0;er.stream.write({type:"write",data:e,position:s}),chunkSize:r.options?.chunkSize}))}};var Tt=Symbol("isTarget");Tt;var O=class{},ee=class extends O{constructor(){super(...arguments);this.buffer=null}createWriter(){return new Y(this)}},I=class extends O{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 P(this):new Z(this)}},te=class extends O{constructor(e,s){super();this.stream=e;this.options=s;if(!(e instanceof FileSystemWritableFileStream))throw new TypeError("FileSystemWritableFileStreamTarget requires a FileSystemWritableFileStream instance.");if(s!==void 0&&typeof s!="object")throw new TypeError("FileSystemWritableFileStreamTarget's options, when provided, must be an object.");if(s&&s.chunkSize!==void 0&&(!Number.isInteger(s.chunkSize)||s.chunkSize<=0))throw new TypeError("options.chunkSize, when provided, must be a positive integer")}createWriter(){return new J(this)}};return ve(gt);})();
+"use strict";var Metamuxer=(()=>{var de=Object.defineProperty;var De=Object.getOwnPropertyDescriptor;var Fe=Object.getOwnPropertyNames;var _e=Object.prototype.hasOwnProperty;var Be=(i,s)=>{for(var e in s)de(i,e,{get:s[e],enumerable:!0})},Ie=(i,s,e,t)=>{if(s&&typeof s=="object"||typeof s=="function")for(let r of Fe(s))!_e.call(i,r)&&r!==e&&de(i,r,{get:()=>s[r],enumerable:!(t=De(s,r))||t.enumerable});return i};var Ne=i=>Ie(de({},"__esModule",{value:!0}),i);var _t={};Be(_t,{ArrayBufferTarget:()=>ue,AudioBufferSource:()=>Y,AudioDataSource:()=>X,CanvasSource:()=>K,FileSystemWritableFileStreamTarget:()=>le,MediaStreamAudioTrackSource:()=>L,MediaStreamVideoTrackSource:()=>q,MkvOutputFormat:()=>H,Mp4OutputFormat:()=>se,Output:()=>Z,StreamTarget:()=>_,Target:()=>M,VideoFrameSource:()=>j,WebMOutputFormat:()=>U});var ge=(i,s,e)=>{if(i==="avc"){let t=100;s<=768&&e<=432?t=66:s<=1920&&e<=1080&&(t=77);let r=0,n=s>1920||e>1080?50:41,o=t.toString(16).padStart(2,"0"),a=r.toString(16).padStart(2,"0"),u=n.toString(16).padStart(2,"0");return`avc1.${o}${a}${u}`}else if(i==="hevc"){let t=0,r=1,n=Array(32).fill(0);n[r]=1;let o=parseInt(n.reverse().join(""),2).toString(16).replace(/^0+/,""),a="L",u=120;return s<=1280&&e<=720?u=93:s<=1920&&e<=1080?u=120:s<=3840&&e<=2160?u=150:(a="H",u=180),`hev1.${t===0?"":String.fromCharCode(65+t-1)}${r}.${o}.${a}${u}.B0`}else{if(i==="vp8")return"vp8";if(i==="vp9"){let t="00",r;return s<=854&&e<=480?r="21":s<=1280&&e<=720?r="31":s<=1920&&e<=1080?r="41":s<=3840&&e<=2160?r="51":r="61",`vp09.${t}.${r}.08`}else if(i==="av1"){let r;return s<=854&&e<=480?r="01":s<=1280&&e<=720?r="03":s<=1920&&e<=1080?r="04":s<=3840&&e<=2160?r="07":r="09",`av01.0.${r}M.08`}}throw new Error(`Unhandled codec '${i}'.`)},Te=(i,s,e)=>{if(i==="aac")return s>=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}'.`)};function d(i){if(!i)throw new Error("Assertion failed.")}var P=i=>i&&i[i.length-1],O=i=>i>=0&&i<2**32,v=(i,s,e)=>{let t=0;for(let r=s;r>a;t<<=1,t|=u}return t},ke=(i,s,e,t)=>{for(let r=s;r>e-r-1<i instanceof ArrayBuffer?new Uint8Array(i):new Uint8Array(i.buffer,i.byteOffset,i.byteLength);var V=class{constructor(s,e){this.connectedTrack=null;this.codec=s,this.metadata=e}ensureNotFinalizing(){if(this.connectedTrack?.output.finalizing)throw new Error("Cannot call digest after output has started finalizing.")}start(){}async flush(){}},B=class{constructor(s,e){this.connectedTrack=null;this.codec=s,this.metadata=e}ensureNotFinalizing(){if(this.connectedTrack?.output.finalizing)throw new Error("Cannot call digest after output has started finalizing.")}start(){}async flush(){}};var We=5,I=class{constructor(s,e){this.source=s;this.codecConfig=e;this.encoder=null;this.lastMultipleOfKeyFrameInterval=-1}digest(s){this.source.ensureNotFinalizing(),this.ensureEncoder(s),d(this.encoder);let e=Math.floor(s.timestamp/1e6/We);this.encoder.encode(s,{keyFrame:e!==this.lastMultipleOfKeyFrameInterval}),this.lastMultipleOfKeyFrameInterval=e}ensureEncoder(s){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:ge(this.codecConfig.codec,s.codedWidth,s.codedHeight),width:s.codedWidth,height:s.codedHeight,bitrate:this.codecConfig.bitrate}))}async flush(){return this.encoder?.flush()}},j=class extends V{constructor(s,e={}){super(s.codec,e),this.encoder=new I(this,s)}digest(s){this.encoder.digest(s)}flush(){return this.encoder.flush()}},K=class extends V{constructor(e,t,r={}){super(t.codec,r);this.canvas=e;this.encoder=new I(this,t)}digest(e,t=0){let r=new VideoFrame(this.canvas,{timestamp:Math.round(1e6*e),duration:Math.round(1e6*t)});this.encoder.digest(r),r.close()}flush(){return this.encoder.flush()}},q=class extends V{constructor(e,t,r={}){super(t.codec,r);this.track=e;this.abortController=null;this.encoder=new I(this,t)}start(){this.abortController=new AbortController;let e=new MediaStreamTrackProcessor({track:this.track}),t=new WritableStream({write:r=>{this.encoder.digest(r),r.close()}});e.readable.pipeTo(t,{signal:this.abortController.signal}).catch(r=>{r instanceof DOMException&&r.name==="AbortError"||console.error("Pipe error:",r)})}async flush(){this.abortController&&(this.abortController.abort(),this.abortController=null),await this.encoder.flush()}};var N=class{constructor(s,e){this.source=s;this.codecConfig=e;this.encoder=null}digest(s){this.source.ensureNotFinalizing(),this.ensureEncoder(s),d(this.encoder),this.encoder.encode(s)}ensureEncoder(s){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:Te(this.codecConfig.codec,s.numberOfChannels,s.sampleRate),numberOfChannels:s.numberOfChannels,sampleRate:s.sampleRate,bitrate:this.codecConfig.bitrate}))}async flush(){return this.encoder?.flush()}},X=class extends B{constructor(s,e={}){super(s.codec,e),this.encoder=new N(this,s)}digest(s){this.encoder.digest(s)}flush(){return this.encoder.flush()}},Y=class extends B{constructor(e,t={}){super(e.codec,t);this.accumulatedFrameCount=0;this.encoder=new N(this,e)}digest(e){let t=e.numberOfChannels,r=e.sampleRate,n=e.length,o=new Float32Array(t*n);for(let u=0;u{this.encoder.digest(r),r.close()}});e.readable.pipeTo(t,{signal:this.abortController.signal}).catch(r=>{r instanceof DOMException&&r.name==="AbortError"||console.error("Pipe error:",r)})}async flush(){this.abortController&&(this.abortController.abort(),this.abortController=null),await this.encoder.flush()}};var Z=class{constructor(s){this.tracks=[];this.started=!1;this.finalizing=!1;this.writer=s.target.createWriter(),this.muxer=s.format.createMuxer(this)}addTrack(s){if(this.started)throw new Error("Cannot add track after output has started.");if(s.connectedTrack)throw new Error("Source is already used for a track.");let e={id:this.tracks.length+1,output:this,type:s instanceof V?"video":"audio",source:s};this.muxer.beforeTrackAdd(e),this.tracks.push(e),s.connectedTrack=e}start(){if(this.started)throw new Error("Output already started.");this.started=!0,this.muxer.start();for(let s of this.tracks)s.source.start()}async finalize(){if(this.finalizing)throw new Error("Cannot call finalize twice.");this.finalizing=!0;let s=this.tracks.map(e=>e.source.flush());await Promise.all(s),this.muxer.finalize(),this.writer.flush(),this.writer.finalize()}};var f=new Uint8Array(8),y=new DataView(f.buffer),b=i=>[(i%256+256)%256],h=i=>(y.setUint16(0,i,!1),[f[0],f[1]]),Re=i=>(y.setInt16(0,i,!1),[f[0],f[1]]),ye=i=>(y.setUint32(0,i,!1),[f[1],f[2],f[3]]),l=i=>(y.setUint32(0,i,!1),[f[0],f[1],f[2],f[3]]),He=i=>(y.setInt32(0,i,!1),[f[0],f[1],f[2],f[3]]),z=i=>(y.setUint32(0,Math.floor(i/2**32),!1),y.setUint32(4,i,!1),[f[0],f[1],f[2],f[3],f[4],f[5],f[6],f[7]]),fe=i=>(y.setInt16(0,2**8*i,!1),[f[0],f[1]]),x=i=>(y.setInt32(0,2**16*i,!1),[f[0],f[1],f[2],f[3]]),ce=i=>(y.setInt32(0,2**30*i,!1),[f[0],f[1],f[2],f[3]]),T=(i,s=!1)=>{let e=Array(i.length).fill(null).map((t,r)=>i.charCodeAt(r));return s&&e.push(0),e},he=i=>{let s=null;for(let e of i)(!s||e.presentationTimestamp>s.presentationTimestamp)&&(s=e);return s},we=i=>{let s=i*(Math.PI/180),e=Math.cos(s),t=Math.sin(s);return[e,t,0,-t,e,0,0,0,1]},Se=we(0),Ae=i=>[x(i[0]),x(i[1]),ce(i[2]),x(i[3]),x(i[4]),ce(i[5]),x(i[6]),x(i[7]),ce(i[8])],p=(i,s,e)=>({type:i,contents:s&&new Uint8Array(s.flat(10)),children:e}),m=(i,s,e,t,r)=>p(i,[b(s),ye(e),t??[]],r),Oe=i=>{let s=512;return i.fragmented?p("ftyp",[T("iso5"),l(s),T("iso5"),T("iso6"),T("mp41")]):p("ftyp",[T("isom"),l(s),T("isom"),i.holdsAvc?T("avc1"):[],T("mp41")])},ee=i=>({type:"mdat",largeSize:i}),ve=i=>({type:"free",size:i}),W=(i,s,e=!1)=>p("moov",void 0,[$e(s,i),...i.map(t=>Qe(t,s)),e?bt(i):null]),$e=(i,s)=>{let e=k(Math.max(0,...s.filter(o=>o.samples.length>0).map(o=>{let a=he(o.samples);return a.presentationTimestamp+a.duration})),J),t=Math.max(...s.map(o=>o.track.id))+1,r=!O(i)||!O(e),n=r?z:l;return m("mvhd",+r,0,[n(i),n(i),l(J),n(e),x(1),fe(1),Array(10).fill(0),Ae(Se),Array(24).fill(0),l(t)])},Qe=(i,s)=>p("trak",void 0,[Ge(i,s),je(i,s)]),Ge=(i,s)=>{let e=he(i.samples),t=k(e?e.presentationTimestamp+e.duration:0,J),r=!O(s)||!O(t),n=r?z:l,o;if(i.type==="video"){let a=i.track.source.metadata.rotation;o=a===void 0||typeof a=="number"?we(a??0):a}else o=Se;return m("tkhd",+r,3,[n(s),n(s),l(i.track.id),l(0),n(t),Array(8).fill(0),h(0),h(0),fe(i.type==="audio"?1:0),h(0),Ae(o),x(i.type==="video"?i.info.width:0),x(i.type==="video"?i.info.height:0)])},je=(i,s)=>p("mdia",void 0,[Ke(i,s),qe(i.type==="video"?"vide":"soun"),Xe(i)]),Ke=(i,s)=>{let e=he(i.samples),t=k(e?e.presentationTimestamp+e.duration:0,i.timescale),r=!O(s)||!O(t),n=r?z:l;return m("mdhd",+r,0,[n(s),n(s),l(i.timescale),n(t),h(21956),h(0)])},qe=i=>m("hdlr",0,0,[T("mhlr"),T(i),l(0),l(0),l(0),T("mp4-muxer-hdlr",!0)]),Xe=i=>p("minf",void 0,[i.type==="video"?Ye():Le(),Ze(),tt(i)]),Ye=()=>m("vmhd",0,1,[h(0),h(0),h(0),h(0)]),Le=()=>m("smhd",0,0,[h(0),h(0)]),Ze=()=>p("dinf",void 0,[Je()]),Je=()=>m("dref",0,0,[l(1)],[et()]),et=()=>m("url ",0,1),tt=i=>{let s=i.compositionTimeOffsetTable.length>1||i.compositionTimeOffsetTable.some(e=>e.sampleCompositionTimeOffset!==0);return p("stbl",void 0,[rt(i),dt(i),ct(i),ft(i),ht(i),mt(i),s?pt(i):null])},rt=i=>m("stsd",0,0,[l(1)],[i.type==="video"?it(At[i.track.source.codec],i):at(vt[i.track.source.codec],i)]),it=(i,s)=>p(i,[Array(6).fill(0),h(1),h(0),h(0),Array(12).fill(0),h(s.info.width),h(s.info.height),l(4718592),l(4718592),l(0),h(1),Array(32).fill(0),h(24),Re(65535)],[Ot[s.track.source.codec](s)]),st=i=>i.info.decoderConfig&&p("avcC",[...E(i.info.decoderConfig.description)]),nt=i=>i.info.decoderConfig&&p("hvcC",[...E(i.info.decoderConfig.description)]),xe=i=>{if(!i.info.decoderConfig)return null;let s=i.info.decoderConfig;if(!s.colorSpace)throw new Error("'colorSpace' is required in the decoder config for VP8/VP9.");let e=s.codec.split("."),t=Number(e[1]),r=Number(e[2]),a=(Number(e[3])<<4)+(0<<1)+Number(s.colorSpace.fullRange);return m("vpcC",1,0,[b(t),b(r),b(a),b(2),b(2),b(2),h(0)])},ot=()=>{let e=(1<<7)+1;return p("av1C",[e,0,0,0])},at=(i,s)=>p(i,[Array(6).fill(0),h(1),h(0),h(0),l(0),h(s.info.numberOfChannels),h(16),h(0),h(0),x(s.info.sampleRate)],[Vt[s.track.source.codec](s)]),ut=i=>{let s=E(i.info.decoderConfig.description??new ArrayBuffer(0));return m("esds",0,0,[l(58753152),b(32+s.byteLength),h(1),b(0),l(75530368),b(18+s.byteLength),b(64),b(21),ye(0),l(130071),l(130071),l(92307584),b(s.byteLength),...s,l(109084800),b(1),b(2)])},lt=i=>{let s=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 r=ArrayBuffer.isView(t)?new DataView(t.buffer,t.byteOffset,t.byteLength):new DataView(t);s=r.getUint16(10,!0),e=r.getInt16(14,!0)}return p("dOps",[b(0),b(i.info.numberOfChannels),h(s),l(i.info.sampleRate),fe(e),b(0)])},dt=i=>m("stts",0,0,[l(i.timeToSampleTable.length),i.timeToSampleTable.map(s=>[l(s.sampleCount),l(s.sampleDelta)])]),ct=i=>{if(i.samples.every(e=>e.type==="key"))return null;let s=[...i.samples.entries()].filter(([,e])=>e.type==="key");return m("stss",0,0,[l(s.length),s.map(([e])=>l(e+1))])},ft=i=>m("stsc",0,0,[l(i.compactlyCodedChunkTable.length),i.compactlyCodedChunkTable.map(s=>[l(s.firstChunk),l(s.samplesPerChunk),l(1)])]),ht=i=>m("stsz",0,0,[l(0),l(i.samples.length),i.samples.map(s=>l(s.size))]),mt=i=>i.finalizedChunks.length>0&&P(i.finalizedChunks).offset>=2**32?m("co64",0,0,[l(i.finalizedChunks.length),i.finalizedChunks.map(s=>z(s.offset))]):m("stco",0,0,[l(i.finalizedChunks.length),i.finalizedChunks.map(s=>l(s.offset))]),pt=i=>m("ctts",0,0,[l(i.compositionTimeOffsetTable.length),i.compositionTimeOffsetTable.map(s=>[l(s.sampleCount),l(s.sampleCompositionTimeOffset)])]),bt=i=>p("mvex",void 0,i.map(Ct)),Ct=i=>m("trex",0,0,[l(i.track.id),l(1),l(0),l(0),l(0)]),me=(i,s)=>p("moof",void 0,[gt(i),...s.map(Tt)]),gt=i=>m("mfhd",0,0,[l(i)]),Ve=i=>{let s=0,e=0,t=0,r=0,n=i.type==="delta";return e|=+n,n?s|=1:s|=2,s<<24|e<<16|t<<8|r},Tt=i=>p("traf",void 0,[kt(i),xt(i),yt(i)]),kt=i=>{d(i.currentChunk);let s=0;s|=8,s|=16,s|=32,s|=131072;let e=i.currentChunk.samples[1]??i.currentChunk.samples[0],t={duration:e.timescaleUnitsToNextSample,size:e.size,flags:Ve(e)};return m("tfhd",0,s,[l(i.track.id),l(t.duration),l(t.size),l(t.flags)])},xt=i=>(d(i.currentChunk),m("tfdt",1,0,[z(k(i.currentChunk.startTimestamp,i.timescale))])),yt=i=>{d(i.currentChunk);let s=i.currentChunk.samples.map(w=>w.timescaleUnitsToNextSample),e=i.currentChunk.samples.map(w=>w.size),t=i.currentChunk.samples.map(Ve),r=i.currentChunk.samples.map(w=>k(w.presentationTimestamp-w.decodeTimestamp,i.timescale)),n=new Set(s),o=new Set(e),a=new Set(t),u=new Set(r),c=a.size===2&&t[0]!==t[1],C=n.size>1,g=o.size>1,S=!c&&a.size>1,Ce=u.size>1||[...u].some(w=>w!==0),A=0;return A|=1,A|=4*+c,A|=256*+C,A|=512*+g,A|=1024*+S,A|=2048*+Ce,m("trun",1,A,[l(i.currentChunk.samples.length),l(i.currentChunk.offset-i.currentChunk.moofOffset||0),c?l(t[0]):[],i.currentChunk.samples.map((w,G)=>[C?l(s[G]):[],g?l(e[G]):[],S?l(t[G]):[],Ce?He(r[G]):[]])])},ze=i=>p("mfra",void 0,[...i.map(wt),St()]),wt=(i,s)=>m("tfra",1,0,[l(i.track.id),l(63),l(i.finalizedChunks.length),i.finalizedChunks.map(t=>[z(k(t.startTimestamp,i.timescale)),z(t.moofOffset),l(s+1),l(1),l(1)])]),St=()=>m("mfro",0,0,[l(0)]),At={avc:"avc1",hevc:"hvc1",vp8:"vp08",vp9:"vp09",av1:"av01"},Ot={avc:st,hevc:nt,vp8:xe,vp9:xe,av1:ot},vt={aac:"mp4a",opus:"Opus"},Vt={aac:ut,opus:lt};var D=class{constructor(s){this.output=s}beforeTrackAdd(s){}};var J=1e3,zt=2082844800,k=(i,s,e=!0)=>{let t=i*s;return e?Math.round(t):t},te=class extends D{constructor(e,t){super(e);this.#i=new Uint8Array(8);this.#t=new DataView(this.#i.buffer);this.offsets=new WeakMap;this.#o=null;this.#n=null;this.#s=[];this.#a=Math.floor(Date.now()/1e3)+zt;this.#l=[];this.#f=1;this.#e=e.writer,this.#r=t}#e;#r;#i;#t;#o;#n;#s;#a;#l;#f;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(Oe({holdsAvc:e,fragmented:this.#r.options.fastStart==="fragmented"})),this.#o=this.#e.getPos(),this.#r.options.fastStart==="in-memory")this.#n=ee(!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.#n=ee(!0),this.writeBox(this.#n)}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 r of t)r&&(e+=8*Math.ceil(2/3*r),e+=4*r,e+=12*Math.ceil(2/3*r),e+=4*r,e+=8*r);return e+=4096,e}#u(e,t,r){let n=this.#s.find(a=>a.track===e);if(n)return n;d(r),d(r.decoderConfig),d(r.decoderConfig.codedWidth!==void 0),d(r.decoderConfig.codedHeight!==void 0);let o={track:e,type:"video",info:{width:r.decoderConfig.codedWidth,height:r.decoderConfig.codedHeight,decoderConfig:r.decoderConfig},timescale:e.source.metadata.frameRate??57600,samples:[],sampleQueue:[],firstDecodeTimestamp:null,lastDecodeTimestamp:-1,timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.#s.push(o),this.#s.sort((a,u)=>a.track.id-u.track.id),o}#h(e,t,r){let n=this.#s.find(a=>a.track===e);if(n)return n;d(r),d(r.decoderConfig);let o={track:e,type:"audio",info:{numberOfChannels:r.decoderConfig.numberOfChannels,sampleRate:r.decoderConfig.sampleRate,decoderConfig:r.decoderConfig},timescale:r.decoderConfig.sampleRate,samples:[],sampleQueue:[],firstDecodeTimestamp:null,lastDecodeTimestamp:-1,timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.#s.push(o),this.#s.sort((a,u)=>a.track.id-u.track.id),o}addEncodedVideoChunk(e,t,r,n){let o=this.#u(e,t,r);if(typeof this.#r.options.fastStart=="object"&&o.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 a=this.#m(o,t,n);this.#r.options.fastStart==="fragmented"?(o.sampleQueue.push(a),this.#g()):this.#c(o,a)}addEncodedAudioChunk(e,t,r){let n=this.#h(e,t,r);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.#m(n,t);this.#r.options.fastStart==="fragmented"?(n.sampleQueue.push(o),this.#g()):this.#c(n,o)}#m(e,t,r){let n=t.timestamp/1e6,o=(t.timestamp-(r??0))/1e6,a=(t.duration??0)/1e6,u=this.#T(e,n,o);n=u.presentationTimestamp,o=u.decodeTimestamp;let c=new Uint8Array(t.byteLength);return t.copyTo(c),{presentationTimestamp:n,decodeTimestamp:o,duration:a,data:c,size:c.byteLength,type:t.type,timescaleUnitsToNextSample:k(a,e.timescale)}}#c(e,t){this.#r.options.fastStart!=="fragmented"&&e.samples.push(t);let r=k(t.presentationTimestamp-t.decodeTimestamp,e.timescale);if(e.lastTimescaleUnits!==null){d(e.lastSample);let o=k(t.decodeTimestamp,e.timescale,!1),a=Math.round(o-e.lastTimescaleUnits);if(e.lastTimescaleUnits+=a,e.lastSample.timescaleUnitsToNextSample=a,this.#r.options.fastStart!=="fragmented"){let u=P(e.timeToSampleTable);d(u),u.sampleCount===1?(u.sampleDelta=a,u.sampleCount++):u.sampleDelta===a?u.sampleCount++:(u.sampleCount--,e.timeToSampleTable.push({sampleCount:2,sampleDelta:a}));let c=P(e.compositionTimeOffsetTable);d(c),c.sampleCompositionTimeOffset===r?c.sampleCount++:e.compositionTimeOffsetTable.push({sampleCount:1,sampleCompositionTimeOffset:r})}}else e.lastTimescaleUnits=0,this.#r.options.fastStart!=="fragmented"&&(e.timeToSampleTable.push({sampleCount:1,sampleDelta:k(t.duration,e.timescale)}),e.compositionTimeOffsetTable.push({sampleCount:1,sampleCompositionTimeOffset:r}));e.lastSample=t;let n=!1;if(!e.currentChunk)n=!0;else{let o=t.presentationTimestamp-e.currentChunk.startTimestamp;if(this.#r.options.fastStart==="fragmented"){let a=this.#s.every(u=>{if(e===u)return t.type==="key";let c=u.sampleQueue[0];return c&&c.type==="key"});o>=1&&a&&(n=!0,this.#p())}else n=o>=.5}n&&(e.currentChunk&&this.#C(e),e.currentChunk={startTimestamp:t.presentationTimestamp,samples:[],offset:null,moofOffset:null}),d(e.currentChunk),e.currentChunk.samples.push(t)}#T(e,t,r){if(r<0)throw new Error(`Timestamps must be non-negative (got ${r}s).`);if(e.firstDecodeTimestamp===null&&(e.firstDecodeTimestamp=r),r-=e.firstDecodeTimestamp,t-=e.firstDecodeTimestamp,r=2**32&&(u.largeSize=!0,C=this.measureBox(u)+c),u.size=C,this.writeBox(u)}for(let u of this.#s){u.currentChunk.offset=this.#e.getPos(),u.currentChunk.moofOffset=r;for(let c of u.currentChunk.samples)this.#e.write(c.data),c.data=null}let o=this.#e.getPos();this.#e.seek(this.offsets.get(n));let a=me(t,this.#s);this.writeBox(a),this.#e.seek(o);for(let u of this.#s)u.finalizedChunks.push(u.currentChunk),this.#l.push(u.currentChunk),u.currentChunk=null;e&&this.#e.flush()}finalize(){if(this.#r.options.fastStart==="fragmented"){for(let e of this.#s)for(let t of e.sampleQueue)this.#c(e,t);this.#p(!1)}else for(let e of this.#s)this.#C(e);if(this.#r.options.fastStart==="in-memory"){d(this.#n);let e;for(let r=0;r<2;r++){let n=W(this.#s,this.#a),o=this.measureBox(n);e=this.measureBox(this.#n);let a=this.#e.getPos()+o+e;for(let u of this.#l){u.offset=a;for(let{data:c}of u.samples)d(c),a+=c.byteLength,e+=c.byteLength}if(a<2**32)break;e>=2**32&&(this.#n.largeSize=!0)}let t=W(this.#s,this.#a);this.writeBox(t),this.#n.size=e,this.writeBox(this.#n);for(let r of this.#l)for(let n of r.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=ze(this.#s);this.writeBox(t);let r=this.#e.getPos()-e;this.#e.seek(this.#e.getPos()-4),this.writeU32(r)}else{d(this.#n),d(this.#o!==null);let e=this.offsets.get(this.#n);d(e!==void 0);let t=this.#e.getPos()-e;this.#n.size=t,this.#n.largeSize=t>=2**32,this.patchBox(this.#n);let r=W(this.#s,this.#a);if(typeof this.#r.options.fastStart=="object"){this.#e.seek(this.#o),this.writeBox(r);let n=e-this.#e.getPos();this.writeBox(ve(n))}else this.writeBox(r)}}};var R=class{constructor(s){this.value=s}},F=class{constructor(s){this.value=s}};var pe=i=>i<256?1:i<65536?2:i<1<<24?3:i<2**32?4:i<2**40?5:6,Ue=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 Ut=1,Mt=2,be=2**15,Me="https://github.com/Vanilagy/webm-muxer",Pe=6,Ee=5,Pt={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"},re=class extends D{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.#o=[];this.#n=null;this.#s=null;this.#a=null;this.#l=null;this.#f=null;this.#d=null;this.#u=null;this.#h=null;this.#m=new Set;this.#c=0;this.#e=e.writer,this.#r=t}#e;#r;#i;#t;#o;#n;#s;#a;#l;#f;#d;#u;#h;#m;#c;#T(e){this.#t.setUint8(0,e),this.#e.write(this.#i.subarray(0,1))}#C(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)}#p(e,t=pe(e)){let r=0;switch(t){case 6:this.#t.setUint8(r++,e/2**40|0);case 5:this.#t.setUint8(r++,e/2**32|0);case 4:this.#t.setUint8(r++,e>>24);case 3:this.#t.setUint8(r++,e>>16);case 2:this.#t.setUint8(r++,e>>8);case 1:this.#t.setUint8(r++,e);break;default:throw new Error("Bad UINT size "+t)}this.#e.write(this.#i.subarray(0,r))}writeEBMLVarInt(e,t=Ue(e)){let r=0;switch(t){case 1:this.#t.setUint8(r++,128|e);break;case 2:this.#t.setUint8(r++,64|e>>8),this.#t.setUint8(r++,e);break;case 3:this.#t.setUint8(r++,32|e>>16),this.#t.setUint8(r++,e>>8),this.#t.setUint8(r++,e);break;case 4:this.#t.setUint8(r++,16|e>>24),this.#t.setUint8(r++,e>>16),this.#t.setUint8(r++,e>>8),this.#t.setUint8(r++,e);break;case 5:this.#t.setUint8(r++,8|e/2**32&7),this.#t.setUint8(r++,e>>24),this.#t.setUint8(r++,e>>16),this.#t.setUint8(r++,e>>8),this.#t.setUint8(r++,e);break;case 6:this.#t.setUint8(r++,4|e/2**40&3),this.#t.setUint8(r++,e/2**32|0),this.#t.setUint8(r++,e>>24),this.#t.setUint8(r++,e>>16),this.#t.setUint8(r++,e>>8),this.#t.setUint8(r++,e);break;default:throw new Error("Bad EBML VINT size "+t)}this.#e.write(this.#i.subarray(0,r))}#S(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.#p(e.id),Array.isArray(e.data)){let t=this.#e.getPos(),r=e.size===-1?1:e.size??4;e.size===-1?this.#T(255):this.#e.seek(this.#e.getPos()+r);let n=this.#e.getPos();if(this.dataOffsets.set(e,n),this.writeEBML(e.data),e.size!==-1){let o=this.#e.getPos()-n,a=this.#e.getPos();this.#e.seek(t),this.writeEBMLVarInt(o,r),this.#e.seek(a)}}else if(typeof e.data=="number"){let t=e.size??pe(e.data);this.writeEBMLVarInt(t),this.#p(e.data,t)}else typeof e.data=="string"?(this.writeEBMLVarInt(e.data.length),this.#S(e.data)):e.data instanceof Uint8Array?(this.writeEBMLVarInt(e.data.byteLength,e.size),this.#e.write(e.data)):e.data instanceof R?(this.writeEBMLVarInt(4),this.#C(e.data.value)):e.data instanceof F&&(this.writeEBMLVarInt(8),this.#g(e.data.value))}beforeTrackAdd(e){if(this.#r instanceof U){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(!["opus","vorbis"].includes(e.source.codec))throw new Error("WebM only supports Opus and Vorbis as audio codecs. Switching to MKV removes this restriction.")}}start(){this.#A(),this.#r.options.streaming||this.#O(),this.#v(),this.#U(),this.#e.flush()}#A(){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 U?"webm":"matroska"},{id:17031,data:2},{id:17029,data:2}]};this.writeEBML(e)}#O(){let e=new Uint8Array([28,83,187,107]),t=new Uint8Array([21,73,169,102]),r=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:r},{id:21420,size:5,data:0}]}]};this.#a=n}#v(){let e={id:17545,data:new F(0)};this.#f=e;let t={id:357149030,data:[{id:2807729,data:1e6},{id:19840,data:Me},{id:22337,data:Me},this.#r.options.streaming?null:e]};this.#s=t}#V(){let e={id:374648427,data:[]};this.#l=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:t.type==="video"?Ut:Mt},{id:134,data:Pt[t.track.source.codec]},t.info.decoderConfig.description?{id:25506,data:E(t.info.decoderConfig.description)}:null,...t.type==="video"?[t.track.source.metadata.frameRate?{id:2352003,data:1e9/t.track.source.metadata.frameRate}:null,{id:224,data:[{id:176,data:t.info.width},{id:186,data:t.info.height},(()=>{if(t.info.decoderConfig.colorSpace){let r=t.info.decoderConfig.colorSpace;return!r.matrix||!r.transfer||!r.primaries||r.fullRange==null?null:{id:21936,data:[{id:21937,data:{rgb:1,bt709:1,bt470bg:5,smpte170m:6}[r.matrix]},{id:21946,data:{bt709:1,smpte170m:6,"iec61966-2-1":13}[r.transfer]},{id:21947,data:{bt709:1,bt470bg:5,smpte170m:6}[r.primaries]},{id:21945,data:[1,2][Number(r.fullRange)]}]}}return null})()]}]:[],...t.type==="audio"?[{id:225,data:[{id:181,data:new R(t.info.sampleRate)},{id:159,data:t.info.numberOfChannels}]}]:[]]})}#z(){let e={id:408125543,size:this.#r.options.streaming?-1:Pe,data:[this.#r.options.streaming?null:this.#a,this.#s,this.#l]};this.#n=e,this.writeEBML(e)}#U(){this.#d={id:475249515,data:[]}}get#b(){return d(this.#n),this.dataOffsets.get(this.#n)}#M(e,t,r){let n=this.#o.find(a=>a.track===e);if(n)return n;d(r),d(r.decoderConfig),d(r.decoderConfig.codedWidth!==void 0),d(r.decoderConfig.codedHeight!==void 0);let o={track:e,type:"video",info:{width:r.decoderConfig.codedWidth,height:r.decoderConfig.codedHeight,decoderConfig:r.decoderConfig},chunkQueue:[],firstTimestamp:null,lastTimestamp:null,lastWrittenTimestamp:null};return this.#o.push(o),this.#o.sort((a,u)=>a.track.id-u.track.id),o}#P(e,t,r){let n=this.#o.find(a=>a.track===e);if(n)return n;d(r),d(r.decoderConfig);let o={track:e,type:"audio",info:{numberOfChannels:r.decoderConfig.numberOfChannels,sampleRate:r.decoderConfig.sampleRate,decoderConfig:r.decoderConfig},chunkQueue:[],firstTimestamp:null,lastTimestamp:null,lastWrittenTimestamp:null};return this.#o.push(o),this.#o.sort((a,u)=>a.track.id-u.track.id),o}addEncodedVideoChunk(e,t,r,n){let o=this.#M(e,t,r),a=this.#x(o,t);e.source.codec==="vp9"&&this.#E(o,a),o.lastTimestamp=a.timestamp,o.chunkQueue.push(a),this.#k(),this.#e.flush()}addEncodedAudioChunk(e,t,r){let n=this.#P(e,t,r),o=this.#x(n,t);n.lastTimestamp=o.timestamp,n.chunkQueue.push(o),this.#k(),this.#e.flush()}#k(){if(!(this.#o.length=2&&r++;let c={rgb:7,bt709:2,bt470bg:1,smpte170m:3}[e.info.decoderConfig.colorSpace.matrix];ke(t.data,r+0,r+3,c)}#x(e,t){let r=this.#D(e,t.timestamp),n=new Uint8Array(t.byteLength);return t.copyTo(n),{data:n,type:t.type,timestamp:r,duration:t.duration,additions:null}}#D(e,t){if(t<0)throw new Error(`Timestamps must be non-negative (got ${t}s).`);if(e.firstTimestamp===null&&(e.firstTimestamp=t),t-=e.firstTimestamp,e.lastTimestamp!==null&&t{if(e===g)return t.type==="key";let S=g.chunkQueue[0];return S&&S.type==="key"});(!this.#u||n&&r-this.#h>=1e3)&&this.#F(r);let o=r-this.#h;if(o<0)return;if(o>=be)throw new Error(`Current Matroska cluster exceeded its maximum allowed length of ${be} milliseconds. In order to produce a correct WebM file, you must pass in a key frame at least every ${be} milliseconds.`);let u=new Uint8Array(4),c=new DataView(u.buffer);c.setUint8(0,128|e.track.id),c.setInt16(1,o,!1);let C=Math.floor((t.duration??0)/1e3);if(C===0&&!t.additions){c.setUint8(3,+(t.type==="key")<<7);let g={id:163,data:[u,t.data]};this.writeEBML(g)}else{let g={id:160,data:[{id:161,data:[u,t.data]},t.type==="delta"?{id:251,data:e.lastWrittenTimestamp-r}:null,t.duration!==null?{id:155,data:C}:null,t.additions?{id:30113,data:t.additions}:null]};this.writeEBML(g)}this.#c=Math.max(this.#c,r+C),e.lastWrittenTimestamp=r,this.#m.add(e)}#F(e){this.#u&&!this.#r.options.streaming&&this.#w(),this.#u={id:524531317,size:this.#r.options.streaming?-1:Ee,data:[{id:231,data:e}]},this.writeEBML(this.#u),this.#h=e,this.#m.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,Ee),this.#e.seek(t);let r=this.offsets.get(this.#u)-this.#b;d(this.#d),this.#d.data.push({id:187,data:[{id:179,data:this.#h},...[...this.#m].map(n=>({id:183,data:[{id:247,data:n.track.id},{id:241,data:r}]}))]})}finalize(){for(let e of this.#o)for(;e.chunkQueue.length>0;)this.#y(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.#b;this.#e.seek(this.offsets.get(this.#n)+4),this.writeEBMLVarInt(t,Pe),this.#f.data=new F(this.#c),this.#e.seek(this.offsets.get(this.#f)),this.writeEBML(this.#f),this.#a.data[0].data[1].data=this.offsets.get(this.#d)-this.#b,this.#a.data[1].data[1].data=this.offsets.get(this.#s)-this.#b,this.#a.data[2].data[1].data=this.offsets.get(this.#l)-this.#b,this.#e.seek(this.offsets.get(this.#a)),this.writeEBML(this.#a),this.#e.seek(e)}}};var ie=class{},se=class extends ie{constructor(e){super();this.options=e}createMuxer(e){return new te(e,this)}},H=class extends ie{constructor(e={}){super();this.options=e}createMuxer(e){return new re(e,this)}},U=class extends H{};var $=class{},ne=class extends ${#e=0;#r;#i=new ArrayBuffer(2**16);#t=new Uint8Array(this.#i);#o=0;constructor(s){super(),this.#r=s}#n(s){let e=this.#i.byteLength;for(;et.start-r.start);s.push({start:e[0].start,size:e[0].data.byteLength});for(let t=1;tu.start<=e&&eDt){for(let u=0;u=s.written[n+1].start;)s.written[n].end=Math.max(s.written[n].end,s.written[n+1].end),s.written.splice(n+1,1)}#s(s){let t={start:Math.floor(s/this.#i)*this.#i,data:new Uint8Array(this.#i),written:[],shouldFlush:!1};return this.#t.push(t),this.#t.sort((r,n)=>r.start-n.start),this.#t.indexOf(t)}#a(s=!1){for(let e=0;es.stream.write({type:"write",data:e,position:t}),chunkSize:s.options?.chunkSize}))}};var Ft=Symbol("isTarget");Ft;var M=class{},ue=class extends M{constructor(){super(...arguments);this.buffer=null}createWriter(){return new ne(this)}},_=class extends M{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 Q(this):new oe(this)}},le=class extends M{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 ae(this)}};return Ne(_t);})();
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 4b5899d..01891b7 100644
--- a/dist/metamuxer.min.mjs
+++ b/dist/metamuxer.min.mjs
@@ -1 +1 @@
-var ue=(t,r,e)=>{if(t==="avc"){let s=100;r<=768&&e<=432?s=66:r<=1920&&e<=1080&&(s=77);let o=0,i=r>1920||e>1080?50:41,a=s.toString(16).padStart(2,"0"),l=o.toString(16).padStart(2,"0"),u=i.toString(16).padStart(2,"0");return`avc1.${a}${l}${u}`}else if(t==="hevc"){let s=0,o=1,i=Array(32).fill(0);i[o]=1;let a=parseInt(i.reverse().join(""),2).toString(16).replace(/^0+/,""),l="L",u=120;return r<=1280&&e<=720?u=93:r<=1920&&e<=1080?u=120:r<=3840&&e<=2160?u=150:(l="H",u=180),`hev1.${s===0?"":String.fromCharCode(65+s-1)}${o}.${a}.${l}${u}.B0`}else{if(t==="vp8")return"vp8";if(t==="vp9"){let s="00",o;return r<=854&&e<=480?o="21":r<=1280&&e<=720?o="31":r<=1920&&e<=1080?o="41":r<=3840&&e<=2160?o="51":o="61",`vp09.${s}.${o}.08`}else if(t==="av1"){let o;return r<=854&&e<=480?o="01":r<=1280&&e<=720?o="03":r<=1920&&e<=1080?o="04":r<=3840&&e<=2160?o="07":o="09",`av01.0.${o}M.08`}}throw new Error(`Unhandled codec '${t}'.`)},le=(t,r,e)=>{if(t==="aac")return r>=2&&e<=24e3?"mp4a.40.29":e<=24e3?"mp4a.40.5":"mp4a.40.2";if(t==="opus")return"opus";if(t==="vorbis")return"vorbis";throw new Error(`Unhandled codec '${t}'.`)};function c(t){if(!t)throw new Error("Assertion failed.")}var O=t=>t&&t[t.length-1],w=t=>t>=0&&t<2**32;var v=class{constructor(r,e){this.connectedTrack=null;this.codec=r,this.metadata=e}ensureNotFinalizing(){if(this.connectedTrack?.output.finalizing)throw new Error("Cannot call digest after output has started finalizing.")}start(){}async flush(){}},E=class{constructor(r,e){this.connectedTrack=null;this.codec=r,this.metadata=e}ensureNotFinalizing(){if(this.connectedTrack?.output.finalizing)throw new Error("Cannot call digest after output has started finalizing.")}start(){}async flush(){}};var ge=5,M=class{constructor(r,e){this.source=r;this.codecConfig=e;this.encoder=null;this.lastMultipleOfKeyFrameInterval=-1}digest(r){this.source.ensureNotFinalizing(),this.ensureEncoder(r),c(this.encoder);let e=Math.floor(r.timestamp/1e6/ge);this.encoder.encode(r,{keyFrame:e!==this.lastMultipleOfKeyFrameInterval}),this.lastMultipleOfKeyFrameInterval=e}ensureEncoder(r){this.encoder||(this.encoder=new VideoEncoder({output:(e,s)=>this.source.connectedTrack?.output.muxer.addEncodedVideoChunk(this.source.connectedTrack,e,s),error:e=>console.error(e)}),this.encoder.configure({codec:ue(this.codecConfig.codec,r.codedWidth,r.codedHeight),width:r.codedWidth,height:r.codedHeight,bitrate:this.codecConfig.bitrate}))}async flush(){return this.encoder?.flush()}},j=class extends v{constructor(r,e={}){super(r.codec,e),this.encoder=new M(this,r)}digest(r){this.encoder.digest(r)}flush(){return this.encoder.flush()}},q=class extends v{constructor(e,s,o={}){super(s.codec,o);this.canvas=e;this.encoder=new M(this,s)}digest(e,s=0){let o=new VideoFrame(this.canvas,{timestamp:Math.round(1e6*e),duration:Math.round(1e6*s)});this.encoder.digest(o),o.close()}flush(){return this.encoder.flush()}},K=class extends v{constructor(e,s,o={}){super(s.codec,o);this.track=e;this.abortController=null;this.encoder=new M(this,s)}start(){this.abortController=new AbortController;let e=new MediaStreamTrackProcessor({track:this.track}),s=new WritableStream({write:o=>{this.encoder.digest(o),o.close()}});e.readable.pipeTo(s,{signal:this.abortController.signal}).catch(o=>{o instanceof DOMException&&o.name==="AbortError"||console.error("Pipe error:",o)})}async flush(){this.abortController&&(this.abortController.abort(),this.abortController=null),await this.encoder.flush()}};var V=class{constructor(r,e){this.source=r;this.codecConfig=e;this.encoder=null}digest(r){this.source.ensureNotFinalizing(),this.ensureEncoder(r),c(this.encoder),this.encoder.encode(r)}ensureEncoder(r){this.encoder||(this.encoder=new AudioEncoder({output:(e,s)=>this.source.connectedTrack?.output.muxer.addEncodedAudioChunk(this.source.connectedTrack,e,s),error:e=>console.error(e)}),this.encoder.configure({codec:le(this.codecConfig.codec,r.numberOfChannels,r.sampleRate),numberOfChannels:r.numberOfChannels,sampleRate:r.sampleRate,bitrate:this.codecConfig.bitrate}))}async flush(){return this.encoder?.flush()}},X=class extends E{constructor(r,e={}){super(r.codec,e),this.encoder=new V(this,r)}digest(r){this.encoder.digest(r)}flush(){return this.encoder.flush()}},G=class extends E{constructor(e,s={}){super(e.codec,s);this.accumulatedFrameCount=0;this.encoder=new V(this,e)}digest(e){let s=e.numberOfChannels,o=e.sampleRate,i=e.length,a=new Float32Array(s*i);for(let u=0;u{this.encoder.digest(o),o.close()}});e.readable.pipeTo(s,{signal:this.abortController.signal}).catch(o=>{o instanceof DOMException&&o.name==="AbortError"||console.error("Pipe error:",o)})}async flush(){this.abortController&&(this.abortController.abort(),this.abortController=null),await this.encoder.flush()}};var Z=class{constructor(r){this.tracks=[];this.started=!1;this.finalizing=!1;this.writer=r.target.createWriter(),this.muxer=r.format.createMuxer(this)}addTrack(r){if(this.started)throw new Error("Cannot add track after output has started.");if(r.connectedTrack)throw new Error("Source is already used for a track.");let e={id:this.tracks.length+1,output:this,type:r instanceof v?"video":"audio",source:r};this.tracks.push(e),r.connectedTrack=e}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()}};var d=new Uint8Array(8),x=new DataView(d.buffer),b=t=>[(t%256+256)%256],m=t=>(x.setUint16(0,t,!1),[d[0],d[1]]),xe=t=>(x.setInt16(0,t,!1),[d[0],d[1]]),ce=t=>(x.setUint32(0,t,!1),[d[1],d[2],d[3]]),n=t=>(x.setUint32(0,t,!1),[d[0],d[1],d[2],d[3]]),ye=t=>(x.setInt32(0,t,!1),[d[0],d[1],d[2],d[3]]),A=t=>(x.setUint32(0,Math.floor(t/2**32),!1),x.setUint32(4,t,!1),[d[0],d[1],d[2],d[3],d[4],d[5],d[6],d[7]]),ee=t=>(x.setInt16(0,2**8*t,!1),[d[0],d[1]]),g=t=>(x.setInt32(0,2**16*t,!1),[d[0],d[1],d[2],d[3]]),J=t=>(x.setInt32(0,2**30*t,!1),[d[0],d[1],d[2],d[3]]),C=(t,r=!1)=>{let e=Array(t.length).fill(null).map((s,o)=>t.charCodeAt(o));return r&&e.push(0),e},te=t=>{let r=null;for(let e of t)(!r||e.presentationTimestamp>r.presentationTimestamp)&&(r=e);return r},fe=t=>{let r=t*(Math.PI/180),e=Math.cos(r),s=Math.sin(r);return[e,s,0,-s,e,0,0,0,1]},me=fe(0),he=t=>[g(t[0]),g(t[1]),J(t[2]),g(t[3]),g(t[4]),J(t[5]),g(t[6]),g(t[7]),J(t[8])],p=(t,r,e)=>({type:t,contents:r&&new Uint8Array(r.flat(10)),children:e}),h=(t,r,e,s,o)=>p(t,[b(r),ce(e),s??[]],o),pe=t=>{let r=512;return t.fragmented?p("ftyp",[C("iso5"),n(r),C("iso5"),C("iso6"),C("mp41")]):p("ftyp",[C("isom"),n(r),C("isom"),t.holdsAvc?C("avc1"):[],C("mp41")])},W=t=>({type:"mdat",largeSize:t}),be=t=>({type:"free",size:t}),B=(t,r,e=!1)=>p("moov",void 0,[ke(r,t),...t.map(s=>Se(s,r)),e?Xe(t):null]),ke=(t,r)=>{let e=T(Math.max(0,...r.filter(a=>a.samples.length>0).map(a=>{let l=te(a.samples);return l.presentationTimestamp+l.duration})),_),s=Math.max(...r.map(a=>a.track.id))+1,o=!w(t)||!w(e),i=o?A:n;return h("mvhd",+o,0,[i(t),i(t),n(_),i(e),g(1),ee(1),Array(10).fill(0),he(me),Array(24).fill(0),n(s)])},Se=(t,r)=>p("trak",void 0,[we(t,r),ve(t,r)]),we=(t,r)=>{let e=te(t.samples),s=T(e?e.presentationTimestamp+e.duration:0,_),o=!w(r)||!w(s),i=o?A:n,a;if(t.type==="video"){let l=t.track.source.metadata.rotation;a=l===void 0||typeof l=="number"?fe(l??0):l}else a=me;return h("tkhd",+o,3,[i(r),i(r),n(t.track.id),n(0),i(s),Array(8).fill(0),m(0),m(0),ee(t.type==="audio"?1:0),m(0),he(a),g(t.type==="video"?t.info.width:0),g(t.type==="video"?t.info.height:0)])},ve=(t,r)=>p("mdia",void 0,[Ae(t,r),Oe(t.type==="video"?"vide":"soun"),ze(t)]),Ae=(t,r)=>{let e=te(t.samples),s=T(e?e.presentationTimestamp+e.duration:0,t.timescale),o=!w(r)||!w(s),i=o?A:n;return h("mdhd",+o,0,[i(r),i(r),n(t.timescale),i(s),m(21956),m(0)])},Oe=t=>h("hdlr",0,0,[C("mhlr"),C(t),n(0),n(0),n(0),C("mp4-muxer-hdlr",!0)]),ze=t=>p("minf",void 0,[t.type==="video"?Ie():Ee(),Me(),Ue(t)]),Ie=()=>h("vmhd",0,1,[m(0),m(0),m(0),m(0)]),Ee=()=>h("smhd",0,0,[m(0),m(0)]),Me=()=>p("dinf",void 0,[Ve()]),Ve=()=>h("dref",0,0,[n(1)],[Be()]),Be=()=>h("url ",0,1),Ue=t=>{let r=t.compositionTimeOffsetTable.length>1||t.compositionTimeOffsetTable.some(e=>e.sampleCompositionTimeOffset!==0);return p("stbl",void 0,[Fe(t),$e(t),He(t),Qe(t),je(t),qe(t),r?Ke(t):null])},Fe=t=>h("stsd",0,0,[n(1)],[t.type==="video"?Pe(ot[t.track.source.codec],t):We(nt[t.track.source.codec],t)]),Pe=(t,r)=>p(t,[Array(6).fill(0),m(1),m(0),m(0),Array(12).fill(0),m(r.info.width),m(r.info.height),n(4718592),n(4718592),n(0),m(1),Array(32).fill(0),m(24),xe(65535)],[it[r.track.source.codec](r)]),Ne=t=>t.info.decoderConfig&&p("avcC",[...new Uint8Array(t.info.decoderConfig.description)]),Le=t=>t.info.decoderConfig&&p("hvcC",[...new Uint8Array(t.info.decoderConfig.description)]),de=t=>{if(!t.info.decoderConfig)return null;let r=t.info.decoderConfig;if(!r.colorSpace)throw new Error("'colorSpace' is required in the decoder config for VP8/VP9.");let e=r.codec.split("."),s=Number(e[1]),o=Number(e[2]),l=(Number(e[3])<<4)+(0<<1)+Number(r.colorSpace.fullRange);return h("vpcC",1,0,[b(s),b(o),b(l),b(2),b(2),b(2),m(0)])},_e=()=>{let e=(1<<7)+1;return p("av1C",[e,0,0,0])},We=(t,r)=>p(t,[Array(6).fill(0),m(1),m(0),m(0),n(0),m(r.info.numberOfChannels),m(16),m(0),m(0),g(r.info.sampleRate)],[at[r.track.source.codec](r)]),De=t=>{let r=new Uint8Array(t.info.decoderConfig.description);return h("esds",0,0,[n(58753152),b(32+r.byteLength),m(1),b(0),n(75530368),b(18+r.byteLength),b(64),b(21),ce(0),n(130071),n(130071),n(92307584),b(r.byteLength),...r,n(109084800),b(1),b(2)])},Re=t=>{let r=3840,e=0,s=t.info.decoderConfig?.description;if(s){if(s.byteLength<18)throw new TypeError("Invalid decoder description provided for Opus; must be at least 18 bytes long.");let o=ArrayBuffer.isView(s)?new DataView(s.buffer,s.byteOffset,s.byteLength):new DataView(s);r=o.getUint16(10,!0),e=o.getInt16(14,!0)}return p("dOps",[b(0),b(t.info.numberOfChannels),m(r),n(t.info.sampleRate),ee(e),b(0)])},$e=t=>h("stts",0,0,[n(t.timeToSampleTable.length),t.timeToSampleTable.map(r=>[n(r.sampleCount),n(r.sampleDelta)])]),He=t=>{if(t.samples.every(e=>e.type==="key"))return null;let r=[...t.samples.entries()].filter(([,e])=>e.type==="key");return h("stss",0,0,[n(r.length),r.map(([e])=>n(e+1))])},Qe=t=>h("stsc",0,0,[n(t.compactlyCodedChunkTable.length),t.compactlyCodedChunkTable.map(r=>[n(r.firstChunk),n(r.samplesPerChunk),n(1)])]),je=t=>h("stsz",0,0,[n(0),n(t.samples.length),t.samples.map(r=>n(r.size))]),qe=t=>t.finalizedChunks.length>0&&O(t.finalizedChunks).offset>=2**32?h("co64",0,0,[n(t.finalizedChunks.length),t.finalizedChunks.map(r=>A(r.offset))]):h("stco",0,0,[n(t.finalizedChunks.length),t.finalizedChunks.map(r=>n(r.offset))]),Ke=t=>h("ctts",0,0,[n(t.compositionTimeOffsetTable.length),t.compositionTimeOffsetTable.map(r=>[n(r.sampleCount),n(r.sampleCompositionTimeOffset)])]),Xe=t=>p("mvex",void 0,t.map(Ge)),Ge=t=>h("trex",0,0,[n(t.track.id),n(1),n(0),n(0),n(0)]),re=(t,r)=>p("moof",void 0,[Ye(t),...r.map(Ze)]),Ye=t=>h("mfhd",0,0,[n(t)]),Ce=t=>{let r=0,e=0,s=0,o=0,i=t.type==="delta";return e|=+i,i?r|=1:r|=2,r<<24|e<<16|s<<8|o},Ze=t=>p("traf",void 0,[Je(t),et(t),tt(t)]),Je=t=>{c(t.currentChunk);let r=0;r|=8,r|=16,r|=32,r|=131072;let e=t.currentChunk.samples[1]??t.currentChunk.samples[0],s={duration:e.timescaleUnitsToNextSample,size:e.size,flags:Ce(e)};return h("tfhd",0,r,[n(t.track.id),n(s.duration),n(s.size),n(s.flags)])},et=t=>(c(t.currentChunk),h("tfdt",1,0,[A(T(t.currentChunk.startTimestamp,t.timescale))])),tt=t=>{c(t.currentChunk);let r=t.currentChunk.samples.map(k=>k.timescaleUnitsToNextSample),e=t.currentChunk.samples.map(k=>k.size),s=t.currentChunk.samples.map(Ce),o=t.currentChunk.samples.map(k=>T(k.presentationTimestamp-k.decodeTimestamp,t.timescale)),i=new Set(r),a=new Set(e),l=new Set(s),u=new Set(o),f=l.size===2&&s[0]!==s[1],y=i.size>1,I=a.size>1,N=!f&&l.size>1,ae=u.size>1||[...u].some(k=>k!==0),S=0;return S|=1,S|=4*+f,S|=256*+y,S|=512*+I,S|=1024*+N,S|=2048*+ae,h("trun",1,S,[n(t.currentChunk.samples.length),n(t.currentChunk.offset-t.currentChunk.moofOffset||0),f?n(s[0]):[],t.currentChunk.samples.map((k,L)=>[y?n(r[L]):[],I?n(e[L]):[],N?n(s[L]):[],ae?ye(o[L]):[]])])},Te=t=>p("mfra",void 0,[...t.map(rt),st()]),rt=(t,r)=>h("tfra",1,0,[n(t.track.id),n(63),n(t.finalizedChunks.length),t.finalizedChunks.map(s=>[A(T(s.startTimestamp,t.timescale)),A(s.moofOffset),n(r+1),n(1),n(1)])]),st=()=>h("mfro",0,0,[n(0)]),ot={avc:"avc1",hevc:"hvc1",vp8:"vp08",vp9:"vp09",av1:"av01"},it={avc:Ne,hevc:Le,vp8:de,vp9:de,av1:_e},nt={aac:"mp4a",opus:"Opus"},at={aac:De,opus:Re};var D=class{constructor(r){this.output=r}};var _=1e3,ut=2082844800,T=(t,r,e=!0)=>{let s=t*r;return e?Math.round(s):s},R=class extends D{constructor(e,s){super(e);this.#r=new Uint8Array(8);this.#o=new DataView(this.#r.buffer);this.offsets=new WeakMap;this.#n=null;this.#i=null;this.#s=[];this.#a=Math.floor(Date.now()/1e3)+ut;this.#u=[];this.#h=1;this.#e=e.writer,this.#t=s}#e;#t;#r;#o;#n;#i;#s;#a;#u;#h;writeU32(e){this.#o.setUint32(0,e,!1),this.#e.write(this.#r.subarray(0,4))}writeU64(e){this.#o.setUint32(0,Math.floor(e/2**32),!1),this.#o.setUint32(4,e,!1),this.#e.write(this.#r.subarray(0,8))}writeAscii(e){for(let s=0;ss.type==="video"&&s.source.codec==="avc");if(this.writeBox(pe({holdsAvc:e,fragmented:this.#t.options.fastStart==="fragmented"})),this.#n=this.#e.getPos(),this.#t.options.fastStart==="in-memory")this.#i=W(!1);else if(this.#t.options.fastStart!=="fragmented"){if(typeof this.#t.options.fastStart=="object"){let s=this.#b();this.#e.seek(this.#e.getPos()+s)}this.#i=W(!0),this.writeBox(this.#i)}this.#e.flush()}#b(){c(typeof this.#t.options.fastStart=="object");let e=0,s=[this.#t.options.fastStart.expectedVideoChunks,this.#t.options.fastStart.expectedAudioChunks];for(let o of s)o&&(e+=8*Math.ceil(2/3*o),e+=4*o,e+=12*Math.ceil(2/3*o),e+=4*o,e+=8*o);return e+=4096,e}#C(e,s,o){let i=this.#s.find(l=>l.track===e);if(i)return i;c(o),c(o.decoderConfig),c(o.decoderConfig.codedWidth),c(o.decoderConfig.codedHeight);let a={track:e,type:"video",info:{width:o.decoderConfig.codedWidth,height:o.decoderConfig.codedHeight,decoderConfig:o.decoderConfig},timescale:e.source.metadata.frameRate??57600,samples:[],sampleQueue:[],firstDecodeTimestamp:null,lastDecodeTimestamp:-1,timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.#s.push(a),this.#s.sort((l,u)=>l.track.id-u.track.id),a}#T(e,s,o){let i=this.#s.find(l=>l.track===e);if(i)return i;c(o),c(o.decoderConfig);let a={track:e,type:"audio",info:{numberOfChannels:o.decoderConfig.numberOfChannels,sampleRate:o.decoderConfig.sampleRate,decoderConfig:o.decoderConfig},timescale:o.decoderConfig.sampleRate,samples:[],sampleQueue:[],firstDecodeTimestamp:null,lastDecodeTimestamp:-1,timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.#s.push(a),this.#s.sort((l,u)=>l.track.id-u.track.id),a}addEncodedVideoChunk(e,s,o,i){let a=this.#C(e,s,o);if(typeof this.#t.options.fastStart=="object"&&a.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 l=this.#d(a,s,i);this.#t.options.fastStart==="fragmented"?(a.sampleQueue.push(l),this.#f()):this.#l(a,l)}addEncodedAudioChunk(e,s,o){let i=this.#T(e,s,o);if(typeof this.#t.options.fastStart=="object"&&i.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 a=this.#d(i,s);this.#t.options.fastStart==="fragmented"?(i.sampleQueue.push(a),this.#f()):this.#l(i,a)}#d(e,s,o){let i=s.timestamp/1e6,a=(s.timestamp-(o??0))/1e6,l=(s.duration??0)/1e6,u=this.#g(e,i,a);i=u.presentationTimestamp,a=u.decodeTimestamp;let f=new Uint8Array(s.byteLength);return s.copyTo(f),{presentationTimestamp:i,decodeTimestamp:a,duration:l,data:f,size:f.byteLength,type:s.type,timescaleUnitsToNextSample:T(l,e.timescale)}}#l(e,s){this.#t.options.fastStart!=="fragmented"&&e.samples.push(s);let o=T(s.presentationTimestamp-s.decodeTimestamp,e.timescale);if(e.lastTimescaleUnits!==null){c(e.lastSample);let a=T(s.decodeTimestamp,e.timescale,!1),l=Math.round(a-e.lastTimescaleUnits);if(e.lastTimescaleUnits+=l,e.lastSample.timescaleUnitsToNextSample=l,this.#t.options.fastStart!=="fragmented"){let u=O(e.timeToSampleTable);c(u),u.sampleCount===1?(u.sampleDelta=l,u.sampleCount++):u.sampleDelta===l?u.sampleCount++:(u.sampleCount--,e.timeToSampleTable.push({sampleCount:2,sampleDelta:l}));let f=O(e.compositionTimeOffsetTable);c(f),f.sampleCompositionTimeOffset===o?f.sampleCount++:e.compositionTimeOffsetTable.push({sampleCount:1,sampleCompositionTimeOffset:o})}}else e.lastTimescaleUnits=0,this.#t.options.fastStart!=="fragmented"&&(e.timeToSampleTable.push({sampleCount:1,sampleDelta:T(s.duration,e.timescale)}),e.compositionTimeOffsetTable.push({sampleCount:1,sampleCompositionTimeOffset:o}));e.lastSample=s;let i=!1;if(!e.currentChunk)i=!0;else{let a=s.presentationTimestamp-e.currentChunk.startTimestamp;if(this.#t.options.fastStart==="fragmented"){let l=this.#s.every(u=>{if(e===u)return s.type==="key";let f=u.sampleQueue[0];return f&&f.type==="key"});a>=1&&l&&(i=!0,this.#m())}else i=a>=.5}i&&(e.currentChunk&&this.#c(e),e.currentChunk={startTimestamp:s.presentationTimestamp,samples:[],offset:null,moofOffset:null}),c(e.currentChunk),e.currentChunk.samples.push(s)}#g(e,s,o){if(e.firstDecodeTimestamp===null&&(e.firstDecodeTimestamp=o),o-=e.firstDecodeTimestamp,s-=e.firstDecodeTimestamp,o=2**32&&(u.largeSize=!0,y=this.measureBox(u)+f),u.size=y,this.writeBox(u)}for(let u of this.#s){u.currentChunk.offset=this.#e.getPos(),u.currentChunk.moofOffset=o;for(let f of u.currentChunk.samples)this.#e.write(f.data),f.data=null}let a=this.#e.getPos();this.#e.seek(this.offsets.get(i));let l=re(s,this.#s);this.writeBox(l),this.#e.seek(a);for(let u of this.#s)u.finalizedChunks.push(u.currentChunk),this.#u.push(u.currentChunk),u.currentChunk=null;e&&this.#e.flush()}finalize(){if(this.#t.options.fastStart==="fragmented"){for(let e of this.#s)for(let s of e.sampleQueue)this.#l(e,s);this.#m(!1)}else for(let e of this.#s)this.#c(e);if(this.#t.options.fastStart==="in-memory"){c(this.#i);let e;for(let o=0;o<2;o++){let i=B(this.#s,this.#a),a=this.measureBox(i);e=this.measureBox(this.#i);let l=this.#e.getPos()+a+e;for(let u of this.#u){u.offset=l;for(let{data:f}of u.samples)c(f),l+=f.byteLength,e+=f.byteLength}if(l<2**32)break;e>=2**32&&(this.#i.largeSize=!0)}let s=B(this.#s,this.#a);this.writeBox(s),this.#i.size=e,this.writeBox(this.#i);for(let o of this.#u)for(let i of o.samples)c(i.data),this.#e.write(i.data),i.data=null}else if(this.#t.options.fastStart==="fragmented"){let e=this.#e.getPos(),s=Te(this.#s);this.writeBox(s);let o=this.#e.getPos()-e;this.#e.seek(this.#e.getPos()-4),this.writeU32(o)}else{c(this.#i),c(this.#n!==null);let e=this.offsets.get(this.#i);c(e!==void 0);let s=this.#e.getPos()-e;this.#i.size=s,this.#i.largeSize=s>=2**32,this.patchBox(this.#i);let o=B(this.#s,this.#a);if(typeof this.#t.options.fastStart=="object"){this.#e.seek(this.#n),this.writeBox(o);let i=e-this.#e.getPos();this.writeBox(be(i))}else this.writeBox(o)}}};var se=class{},oe=class extends se{constructor(e){super();this.options=e}createMuxer(e){return new R(e,this)}};var U=class{},$=class extends U{#e=0;#t;#r=new ArrayBuffer(2**16);#o=new Uint8Array(this.#r);#n=0;constructor(r){super(),this.#t=r}#i(r){let e=this.#r.byteLength;for(;es.start-o.start);r.push({start:e[0].start,size:e[0].data.byteLength});for(let s=1;su.start<=e&&edt){for(let u=0;u=r.written[i+1].start;)r.written[i].end=Math.max(r.written[i].end,r.written[i+1].end),r.written.splice(i+1,1)}#s(r){let s={start:Math.floor(r/this.#r)*this.#r,data:new Uint8Array(this.#r),written:[],shouldFlush:!1};return this.#o.push(s),this.#o.sort((o,i)=>o.start-i.start),this.#o.indexOf(s)}#a(r=!1){for(let e=0;er.stream.write({type:"write",data:e,position:s}),chunkSize:r.options?.chunkSize}))}};var ct=Symbol("isTarget");ct;var z=class{},ie=class extends z{constructor(){super(...arguments);this.buffer=null}createWriter(){return new $(this)}},P=class extends z{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 F(this):new H(this)}},ne=class extends z{constructor(e,s){super();this.stream=e;this.options=s;if(!(e instanceof FileSystemWritableFileStream))throw new TypeError("FileSystemWritableFileStreamTarget requires a FileSystemWritableFileStream instance.");if(s!==void 0&&typeof s!="object")throw new TypeError("FileSystemWritableFileStreamTarget's options, when provided, must be an object.");if(s&&s.chunkSize!==void 0&&(!Number.isInteger(s.chunkSize)||s.chunkSize<=0))throw new TypeError("options.chunkSize, when provided, must be a positive integer")}createWriter(){return new Q(this)}};export{ie as ArrayBufferTarget,G as AudioBufferSource,X as AudioDataSource,q as CanvasSource,ne as FileSystemWritableFileStreamTarget,Y as MediaStreamAudioTrackSource,K as MediaStreamVideoTrackSource,oe as Mp4OutputFormat,Z as Output,P as StreamTarget,z as Target,j as VideoFrameSource};
+var Ce=(i,s,e)=>{if(i==="avc"){let t=100;s<=768&&e<=432?t=66:s<=1920&&e<=1080&&(t=77);let r=0,n=s>1920||e>1080?50:41,o=t.toString(16).padStart(2,"0"),a=r.toString(16).padStart(2,"0"),u=n.toString(16).padStart(2,"0");return`avc1.${o}${a}${u}`}else if(i==="hevc"){let t=0,r=1,n=Array(32).fill(0);n[r]=1;let o=parseInt(n.reverse().join(""),2).toString(16).replace(/^0+/,""),a="L",u=120;return s<=1280&&e<=720?u=93:s<=1920&&e<=1080?u=120:s<=3840&&e<=2160?u=150:(a="H",u=180),`hev1.${t===0?"":String.fromCharCode(65+t-1)}${r}.${o}.${a}${u}.B0`}else{if(i==="vp8")return"vp8";if(i==="vp9"){let t="00",r;return s<=854&&e<=480?r="21":s<=1280&&e<=720?r="31":s<=1920&&e<=1080?r="41":s<=3840&&e<=2160?r="51":r="61",`vp09.${t}.${r}.08`}else if(i==="av1"){let r;return s<=854&&e<=480?r="01":s<=1280&&e<=720?r="03":s<=1920&&e<=1080?r="04":s<=3840&&e<=2160?r="07":r="09",`av01.0.${r}M.08`}}throw new Error(`Unhandled codec '${i}'.`)},ge=(i,s,e)=>{if(i==="aac")return s>=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}'.`)};function d(i){if(!i)throw new Error("Assertion failed.")}var U=i=>i&&i[i.length-1],O=i=>i>=0&&i<2**32,v=(i,s,e)=>{let t=0;for(let r=s;r>a;t<<=1,t|=u}return t},Te=(i,s,e,t)=>{for(let r=s;r>e-r-1<i instanceof ArrayBuffer?new Uint8Array(i):new Uint8Array(i.buffer,i.byteOffset,i.byteLength);var V=class{constructor(s,e){this.connectedTrack=null;this.codec=s,this.metadata=e}ensureNotFinalizing(){if(this.connectedTrack?.output.finalizing)throw new Error("Cannot call digest after output has started finalizing.")}start(){}async flush(){}},_=class{constructor(s,e){this.connectedTrack=null;this.codec=s,this.metadata=e}ensureNotFinalizing(){if(this.connectedTrack?.output.finalizing)throw new Error("Cannot call digest after output has started finalizing.")}start(){}async flush(){}};var Ee=5,B=class{constructor(s,e){this.source=s;this.codecConfig=e;this.encoder=null;this.lastMultipleOfKeyFrameInterval=-1}digest(s){this.source.ensureNotFinalizing(),this.ensureEncoder(s),d(this.encoder);let e=Math.floor(s.timestamp/1e6/Ee);this.encoder.encode(s,{keyFrame:e!==this.lastMultipleOfKeyFrameInterval}),this.lastMultipleOfKeyFrameInterval=e}ensureEncoder(s){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:Ce(this.codecConfig.codec,s.codedWidth,s.codedHeight),width:s.codedWidth,height:s.codedHeight,bitrate:this.codecConfig.bitrate}))}async flush(){return this.encoder?.flush()}},ee=class extends V{constructor(s,e={}){super(s.codec,e),this.encoder=new B(this,s)}digest(s){this.encoder.digest(s)}flush(){return this.encoder.flush()}},te=class extends V{constructor(e,t,r={}){super(t.codec,r);this.canvas=e;this.encoder=new B(this,t)}digest(e,t=0){let r=new VideoFrame(this.canvas,{timestamp:Math.round(1e6*e),duration:Math.round(1e6*t)});this.encoder.digest(r),r.close()}flush(){return this.encoder.flush()}},re=class extends V{constructor(e,t,r={}){super(t.codec,r);this.track=e;this.abortController=null;this.encoder=new B(this,t)}start(){this.abortController=new AbortController;let e=new MediaStreamTrackProcessor({track:this.track}),t=new WritableStream({write:r=>{this.encoder.digest(r),r.close()}});e.readable.pipeTo(t,{signal:this.abortController.signal}).catch(r=>{r instanceof DOMException&&r.name==="AbortError"||console.error("Pipe error:",r)})}async flush(){this.abortController&&(this.abortController.abort(),this.abortController=null),await this.encoder.flush()}};var I=class{constructor(s,e){this.source=s;this.codecConfig=e;this.encoder=null}digest(s){this.source.ensureNotFinalizing(),this.ensureEncoder(s),d(this.encoder),this.encoder.encode(s)}ensureEncoder(s){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:ge(this.codecConfig.codec,s.numberOfChannels,s.sampleRate),numberOfChannels:s.numberOfChannels,sampleRate:s.sampleRate,bitrate:this.codecConfig.bitrate}))}async flush(){return this.encoder?.flush()}},ie=class extends _{constructor(s,e={}){super(s.codec,e),this.encoder=new I(this,s)}digest(s){this.encoder.digest(s)}flush(){return this.encoder.flush()}},se=class extends _{constructor(e,t={}){super(e.codec,t);this.accumulatedFrameCount=0;this.encoder=new I(this,e)}digest(e){let t=e.numberOfChannels,r=e.sampleRate,n=e.length,o=new Float32Array(t*n);for(let u=0;u{this.encoder.digest(r),r.close()}});e.readable.pipeTo(t,{signal:this.abortController.signal}).catch(r=>{r instanceof DOMException&&r.name==="AbortError"||console.error("Pipe error:",r)})}async flush(){this.abortController&&(this.abortController.abort(),this.abortController=null),await this.encoder.flush()}};var oe=class{constructor(s){this.tracks=[];this.started=!1;this.finalizing=!1;this.writer=s.target.createWriter(),this.muxer=s.format.createMuxer(this)}addTrack(s){if(this.started)throw new Error("Cannot add track after output has started.");if(s.connectedTrack)throw new Error("Source is already used for a track.");let e={id:this.tracks.length+1,output:this,type:s instanceof V?"video":"audio",source:s};this.muxer.beforeTrackAdd(e),this.tracks.push(e),s.connectedTrack=e}start(){if(this.started)throw new Error("Output already started.");this.started=!0,this.muxer.start();for(let s of this.tracks)s.source.start()}async finalize(){if(this.finalizing)throw new Error("Cannot call finalize twice.");this.finalizing=!0;let s=this.tracks.map(e=>e.source.flush());await Promise.all(s),this.muxer.finalize(),this.writer.flush(),this.writer.finalize()}};var f=new Uint8Array(8),y=new DataView(f.buffer),b=i=>[(i%256+256)%256],h=i=>(y.setUint16(0,i,!1),[f[0],f[1]]),De=i=>(y.setInt16(0,i,!1),[f[0],f[1]]),xe=i=>(y.setUint32(0,i,!1),[f[1],f[2],f[3]]),l=i=>(y.setUint32(0,i,!1),[f[0],f[1],f[2],f[3]]),Fe=i=>(y.setInt32(0,i,!1),[f[0],f[1],f[2],f[3]]),z=i=>(y.setUint32(0,Math.floor(i/2**32),!1),y.setUint32(4,i,!1),[f[0],f[1],f[2],f[3],f[4],f[5],f[6],f[7]]),ue=i=>(y.setInt16(0,2**8*i,!1),[f[0],f[1]]),x=i=>(y.setInt32(0,2**16*i,!1),[f[0],f[1],f[2],f[3]]),ae=i=>(y.setInt32(0,2**30*i,!1),[f[0],f[1],f[2],f[3]]),T=(i,s=!1)=>{let e=Array(i.length).fill(null).map((t,r)=>i.charCodeAt(r));return s&&e.push(0),e},le=i=>{let s=null;for(let e of i)(!s||e.presentationTimestamp>s.presentationTimestamp)&&(s=e);return s},ye=i=>{let s=i*(Math.PI/180),e=Math.cos(s),t=Math.sin(s);return[e,t,0,-t,e,0,0,0,1]},we=ye(0),Se=i=>[x(i[0]),x(i[1]),ae(i[2]),x(i[3]),x(i[4]),ae(i[5]),x(i[6]),x(i[7]),ae(i[8])],p=(i,s,e)=>({type:i,contents:s&&new Uint8Array(s.flat(10)),children:e}),m=(i,s,e,t,r)=>p(i,[b(s),xe(e),t??[]],r),Ae=i=>{let s=512;return i.fragmented?p("ftyp",[T("iso5"),l(s),T("iso5"),T("iso6"),T("mp41")]):p("ftyp",[T("isom"),l(s),T("isom"),i.holdsAvc?T("avc1"):[],T("mp41")])},j=i=>({type:"mdat",largeSize:i}),Oe=i=>({type:"free",size:i}),N=(i,s,e=!1)=>p("moov",void 0,[_e(s,i),...i.map(t=>Be(t,s)),e?lt(i):null]),_e=(i,s)=>{let e=k(Math.max(0,...s.filter(o=>o.samples.length>0).map(o=>{let a=le(o.samples);return a.presentationTimestamp+a.duration})),G),t=Math.max(...s.map(o=>o.track.id))+1,r=!O(i)||!O(e),n=r?z:l;return m("mvhd",+r,0,[n(i),n(i),l(G),n(e),x(1),ue(1),Array(10).fill(0),Se(we),Array(24).fill(0),l(t)])},Be=(i,s)=>p("trak",void 0,[Ie(i,s),Ne(i,s)]),Ie=(i,s)=>{let e=le(i.samples),t=k(e?e.presentationTimestamp+e.duration:0,G),r=!O(s)||!O(t),n=r?z:l,o;if(i.type==="video"){let a=i.track.source.metadata.rotation;o=a===void 0||typeof a=="number"?ye(a??0):a}else o=we;return m("tkhd",+r,3,[n(s),n(s),l(i.track.id),l(0),n(t),Array(8).fill(0),h(0),h(0),ue(i.type==="audio"?1:0),h(0),Se(o),x(i.type==="video"?i.info.width:0),x(i.type==="video"?i.info.height:0)])},Ne=(i,s)=>p("mdia",void 0,[We(i,s),Re(i.type==="video"?"vide":"soun"),He(i)]),We=(i,s)=>{let e=le(i.samples),t=k(e?e.presentationTimestamp+e.duration:0,i.timescale),r=!O(s)||!O(t),n=r?z:l;return m("mdhd",+r,0,[n(s),n(s),l(i.timescale),n(t),h(21956),h(0)])},Re=i=>m("hdlr",0,0,[T("mhlr"),T(i),l(0),l(0),l(0),T("mp4-muxer-hdlr",!0)]),He=i=>p("minf",void 0,[i.type==="video"?$e():Qe(),Ge(),qe(i)]),$e=()=>m("vmhd",0,1,[h(0),h(0),h(0),h(0)]),Qe=()=>m("smhd",0,0,[h(0),h(0)]),Ge=()=>p("dinf",void 0,[je()]),je=()=>m("dref",0,0,[l(1)],[Ke()]),Ke=()=>m("url ",0,1),qe=i=>{let s=i.compositionTimeOffsetTable.length>1||i.compositionTimeOffsetTable.some(e=>e.sampleCompositionTimeOffset!==0);return p("stbl",void 0,[Xe(i),it(i),st(i),nt(i),ot(i),at(i),s?ut(i):null])},Xe=i=>m("stsd",0,0,[l(1)],[i.type==="video"?Ye(gt[i.track.source.codec],i):et(kt[i.track.source.codec],i)]),Ye=(i,s)=>p(i,[Array(6).fill(0),h(1),h(0),h(0),Array(12).fill(0),h(s.info.width),h(s.info.height),l(4718592),l(4718592),l(0),h(1),Array(32).fill(0),h(24),De(65535)],[Tt[s.track.source.codec](s)]),Le=i=>i.info.decoderConfig&&p("avcC",[...M(i.info.decoderConfig.description)]),Ze=i=>i.info.decoderConfig&&p("hvcC",[...M(i.info.decoderConfig.description)]),ke=i=>{if(!i.info.decoderConfig)return null;let s=i.info.decoderConfig;if(!s.colorSpace)throw new Error("'colorSpace' is required in the decoder config for VP8/VP9.");let e=s.codec.split("."),t=Number(e[1]),r=Number(e[2]),a=(Number(e[3])<<4)+(0<<1)+Number(s.colorSpace.fullRange);return m("vpcC",1,0,[b(t),b(r),b(a),b(2),b(2),b(2),h(0)])},Je=()=>{let e=(1<<7)+1;return p("av1C",[e,0,0,0])},et=(i,s)=>p(i,[Array(6).fill(0),h(1),h(0),h(0),l(0),h(s.info.numberOfChannels),h(16),h(0),h(0),x(s.info.sampleRate)],[xt[s.track.source.codec](s)]),tt=i=>{let s=M(i.info.decoderConfig.description??new ArrayBuffer(0));return m("esds",0,0,[l(58753152),b(32+s.byteLength),h(1),b(0),l(75530368),b(18+s.byteLength),b(64),b(21),xe(0),l(130071),l(130071),l(92307584),b(s.byteLength),...s,l(109084800),b(1),b(2)])},rt=i=>{let s=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 r=ArrayBuffer.isView(t)?new DataView(t.buffer,t.byteOffset,t.byteLength):new DataView(t);s=r.getUint16(10,!0),e=r.getInt16(14,!0)}return p("dOps",[b(0),b(i.info.numberOfChannels),h(s),l(i.info.sampleRate),ue(e),b(0)])},it=i=>m("stts",0,0,[l(i.timeToSampleTable.length),i.timeToSampleTable.map(s=>[l(s.sampleCount),l(s.sampleDelta)])]),st=i=>{if(i.samples.every(e=>e.type==="key"))return null;let s=[...i.samples.entries()].filter(([,e])=>e.type==="key");return m("stss",0,0,[l(s.length),s.map(([e])=>l(e+1))])},nt=i=>m("stsc",0,0,[l(i.compactlyCodedChunkTable.length),i.compactlyCodedChunkTable.map(s=>[l(s.firstChunk),l(s.samplesPerChunk),l(1)])]),ot=i=>m("stsz",0,0,[l(0),l(i.samples.length),i.samples.map(s=>l(s.size))]),at=i=>i.finalizedChunks.length>0&&U(i.finalizedChunks).offset>=2**32?m("co64",0,0,[l(i.finalizedChunks.length),i.finalizedChunks.map(s=>z(s.offset))]):m("stco",0,0,[l(i.finalizedChunks.length),i.finalizedChunks.map(s=>l(s.offset))]),ut=i=>m("ctts",0,0,[l(i.compositionTimeOffsetTable.length),i.compositionTimeOffsetTable.map(s=>[l(s.sampleCount),l(s.sampleCompositionTimeOffset)])]),lt=i=>p("mvex",void 0,i.map(dt)),dt=i=>m("trex",0,0,[l(i.track.id),l(1),l(0),l(0),l(0)]),de=(i,s)=>p("moof",void 0,[ct(i),...s.map(ft)]),ct=i=>m("mfhd",0,0,[l(i)]),ve=i=>{let s=0,e=0,t=0,r=0,n=i.type==="delta";return e|=+n,n?s|=1:s|=2,s<<24|e<<16|t<<8|r},ft=i=>p("traf",void 0,[ht(i),mt(i),pt(i)]),ht=i=>{d(i.currentChunk);let s=0;s|=8,s|=16,s|=32,s|=131072;let e=i.currentChunk.samples[1]??i.currentChunk.samples[0],t={duration:e.timescaleUnitsToNextSample,size:e.size,flags:ve(e)};return m("tfhd",0,s,[l(i.track.id),l(t.duration),l(t.size),l(t.flags)])},mt=i=>(d(i.currentChunk),m("tfdt",1,0,[z(k(i.currentChunk.startTimestamp,i.timescale))])),pt=i=>{d(i.currentChunk);let s=i.currentChunk.samples.map(w=>w.timescaleUnitsToNextSample),e=i.currentChunk.samples.map(w=>w.size),t=i.currentChunk.samples.map(ve),r=i.currentChunk.samples.map(w=>k(w.presentationTimestamp-w.decodeTimestamp,i.timescale)),n=new Set(s),o=new Set(e),a=new Set(t),u=new Set(r),c=a.size===2&&t[0]!==t[1],C=n.size>1,g=o.size>1,S=!c&&a.size>1,be=u.size>1||[...u].some(w=>w!==0),A=0;return A|=1,A|=4*+c,A|=256*+C,A|=512*+g,A|=1024*+S,A|=2048*+be,m("trun",1,A,[l(i.currentChunk.samples.length),l(i.currentChunk.offset-i.currentChunk.moofOffset||0),c?l(t[0]):[],i.currentChunk.samples.map((w,Q)=>[C?l(s[Q]):[],g?l(e[Q]):[],S?l(t[Q]):[],be?Fe(r[Q]):[]])])},Ve=i=>p("mfra",void 0,[...i.map(bt),Ct()]),bt=(i,s)=>m("tfra",1,0,[l(i.track.id),l(63),l(i.finalizedChunks.length),i.finalizedChunks.map(t=>[z(k(t.startTimestamp,i.timescale)),z(t.moofOffset),l(s+1),l(1),l(1)])]),Ct=()=>m("mfro",0,0,[l(0)]),gt={avc:"avc1",hevc:"hvc1",vp8:"vp08",vp9:"vp09",av1:"av01"},Tt={avc:Le,hevc:Ze,vp8:ke,vp9:ke,av1:Je},kt={aac:"mp4a",opus:"Opus"},xt={aac:tt,opus:rt};var P=class{constructor(s){this.output=s}beforeTrackAdd(s){}};var G=1e3,yt=2082844800,k=(i,s,e=!0)=>{let t=i*s;return e?Math.round(t):t},K=class extends P{constructor(e,t){super(e);this.#i=new Uint8Array(8);this.#t=new DataView(this.#i.buffer);this.offsets=new WeakMap;this.#o=null;this.#n=null;this.#s=[];this.#a=Math.floor(Date.now()/1e3)+yt;this.#l=[];this.#f=1;this.#e=e.writer,this.#r=t}#e;#r;#i;#t;#o;#n;#s;#a;#l;#f;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(Ae({holdsAvc:e,fragmented:this.#r.options.fastStart==="fragmented"})),this.#o=this.#e.getPos(),this.#r.options.fastStart==="in-memory")this.#n=j(!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.#n=j(!0),this.writeBox(this.#n)}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 r of t)r&&(e+=8*Math.ceil(2/3*r),e+=4*r,e+=12*Math.ceil(2/3*r),e+=4*r,e+=8*r);return e+=4096,e}#u(e,t,r){let n=this.#s.find(a=>a.track===e);if(n)return n;d(r),d(r.decoderConfig),d(r.decoderConfig.codedWidth!==void 0),d(r.decoderConfig.codedHeight!==void 0);let o={track:e,type:"video",info:{width:r.decoderConfig.codedWidth,height:r.decoderConfig.codedHeight,decoderConfig:r.decoderConfig},timescale:e.source.metadata.frameRate??57600,samples:[],sampleQueue:[],firstDecodeTimestamp:null,lastDecodeTimestamp:-1,timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.#s.push(o),this.#s.sort((a,u)=>a.track.id-u.track.id),o}#h(e,t,r){let n=this.#s.find(a=>a.track===e);if(n)return n;d(r),d(r.decoderConfig);let o={track:e,type:"audio",info:{numberOfChannels:r.decoderConfig.numberOfChannels,sampleRate:r.decoderConfig.sampleRate,decoderConfig:r.decoderConfig},timescale:r.decoderConfig.sampleRate,samples:[],sampleQueue:[],firstDecodeTimestamp:null,lastDecodeTimestamp:-1,timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.#s.push(o),this.#s.sort((a,u)=>a.track.id-u.track.id),o}addEncodedVideoChunk(e,t,r,n){let o=this.#u(e,t,r);if(typeof this.#r.options.fastStart=="object"&&o.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 a=this.#m(o,t,n);this.#r.options.fastStart==="fragmented"?(o.sampleQueue.push(a),this.#g()):this.#c(o,a)}addEncodedAudioChunk(e,t,r){let n=this.#h(e,t,r);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.#m(n,t);this.#r.options.fastStart==="fragmented"?(n.sampleQueue.push(o),this.#g()):this.#c(n,o)}#m(e,t,r){let n=t.timestamp/1e6,o=(t.timestamp-(r??0))/1e6,a=(t.duration??0)/1e6,u=this.#T(e,n,o);n=u.presentationTimestamp,o=u.decodeTimestamp;let c=new Uint8Array(t.byteLength);return t.copyTo(c),{presentationTimestamp:n,decodeTimestamp:o,duration:a,data:c,size:c.byteLength,type:t.type,timescaleUnitsToNextSample:k(a,e.timescale)}}#c(e,t){this.#r.options.fastStart!=="fragmented"&&e.samples.push(t);let r=k(t.presentationTimestamp-t.decodeTimestamp,e.timescale);if(e.lastTimescaleUnits!==null){d(e.lastSample);let o=k(t.decodeTimestamp,e.timescale,!1),a=Math.round(o-e.lastTimescaleUnits);if(e.lastTimescaleUnits+=a,e.lastSample.timescaleUnitsToNextSample=a,this.#r.options.fastStart!=="fragmented"){let u=U(e.timeToSampleTable);d(u),u.sampleCount===1?(u.sampleDelta=a,u.sampleCount++):u.sampleDelta===a?u.sampleCount++:(u.sampleCount--,e.timeToSampleTable.push({sampleCount:2,sampleDelta:a}));let c=U(e.compositionTimeOffsetTable);d(c),c.sampleCompositionTimeOffset===r?c.sampleCount++:e.compositionTimeOffsetTable.push({sampleCount:1,sampleCompositionTimeOffset:r})}}else e.lastTimescaleUnits=0,this.#r.options.fastStart!=="fragmented"&&(e.timeToSampleTable.push({sampleCount:1,sampleDelta:k(t.duration,e.timescale)}),e.compositionTimeOffsetTable.push({sampleCount:1,sampleCompositionTimeOffset:r}));e.lastSample=t;let n=!1;if(!e.currentChunk)n=!0;else{let o=t.presentationTimestamp-e.currentChunk.startTimestamp;if(this.#r.options.fastStart==="fragmented"){let a=this.#s.every(u=>{if(e===u)return t.type==="key";let c=u.sampleQueue[0];return c&&c.type==="key"});o>=1&&a&&(n=!0,this.#p())}else n=o>=.5}n&&(e.currentChunk&&this.#C(e),e.currentChunk={startTimestamp:t.presentationTimestamp,samples:[],offset:null,moofOffset:null}),d(e.currentChunk),e.currentChunk.samples.push(t)}#T(e,t,r){if(r<0)throw new Error(`Timestamps must be non-negative (got ${r}s).`);if(e.firstDecodeTimestamp===null&&(e.firstDecodeTimestamp=r),r-=e.firstDecodeTimestamp,t-=e.firstDecodeTimestamp,r=2**32&&(u.largeSize=!0,C=this.measureBox(u)+c),u.size=C,this.writeBox(u)}for(let u of this.#s){u.currentChunk.offset=this.#e.getPos(),u.currentChunk.moofOffset=r;for(let c of u.currentChunk.samples)this.#e.write(c.data),c.data=null}let o=this.#e.getPos();this.#e.seek(this.offsets.get(n));let a=de(t,this.#s);this.writeBox(a),this.#e.seek(o);for(let u of this.#s)u.finalizedChunks.push(u.currentChunk),this.#l.push(u.currentChunk),u.currentChunk=null;e&&this.#e.flush()}finalize(){if(this.#r.options.fastStart==="fragmented"){for(let e of this.#s)for(let t of e.sampleQueue)this.#c(e,t);this.#p(!1)}else for(let e of this.#s)this.#C(e);if(this.#r.options.fastStart==="in-memory"){d(this.#n);let e;for(let r=0;r<2;r++){let n=N(this.#s,this.#a),o=this.measureBox(n);e=this.measureBox(this.#n);let a=this.#e.getPos()+o+e;for(let u of this.#l){u.offset=a;for(let{data:c}of u.samples)d(c),a+=c.byteLength,e+=c.byteLength}if(a<2**32)break;e>=2**32&&(this.#n.largeSize=!0)}let t=N(this.#s,this.#a);this.writeBox(t),this.#n.size=e,this.writeBox(this.#n);for(let r of this.#l)for(let n of r.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=Ve(this.#s);this.writeBox(t);let r=this.#e.getPos()-e;this.#e.seek(this.#e.getPos()-4),this.writeU32(r)}else{d(this.#n),d(this.#o!==null);let e=this.offsets.get(this.#n);d(e!==void 0);let t=this.#e.getPos()-e;this.#n.size=t,this.#n.largeSize=t>=2**32,this.patchBox(this.#n);let r=N(this.#s,this.#a);if(typeof this.#r.options.fastStart=="object"){this.#e.seek(this.#o),this.writeBox(r);let n=e-this.#e.getPos();this.writeBox(Oe(n))}else this.writeBox(r)}}};var W=class{constructor(s){this.value=s}},E=class{constructor(s){this.value=s}};var ce=i=>i<256?1:i<65536?2:i<1<<24?3:i<2**32?4:i<2**40?5:6,ze=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 wt=1,St=2,fe=2**15,Ue="https://github.com/Vanilagy/webm-muxer",Me=6,Pe=5,At={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"},q=class extends P{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.#o=[];this.#n=null;this.#s=null;this.#a=null;this.#l=null;this.#f=null;this.#d=null;this.#u=null;this.#h=null;this.#m=new Set;this.#c=0;this.#e=e.writer,this.#r=t}#e;#r;#i;#t;#o;#n;#s;#a;#l;#f;#d;#u;#h;#m;#c;#T(e){this.#t.setUint8(0,e),this.#e.write(this.#i.subarray(0,1))}#C(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)}#p(e,t=ce(e)){let r=0;switch(t){case 6:this.#t.setUint8(r++,e/2**40|0);case 5:this.#t.setUint8(r++,e/2**32|0);case 4:this.#t.setUint8(r++,e>>24);case 3:this.#t.setUint8(r++,e>>16);case 2:this.#t.setUint8(r++,e>>8);case 1:this.#t.setUint8(r++,e);break;default:throw new Error("Bad UINT size "+t)}this.#e.write(this.#i.subarray(0,r))}writeEBMLVarInt(e,t=ze(e)){let r=0;switch(t){case 1:this.#t.setUint8(r++,128|e);break;case 2:this.#t.setUint8(r++,64|e>>8),this.#t.setUint8(r++,e);break;case 3:this.#t.setUint8(r++,32|e>>16),this.#t.setUint8(r++,e>>8),this.#t.setUint8(r++,e);break;case 4:this.#t.setUint8(r++,16|e>>24),this.#t.setUint8(r++,e>>16),this.#t.setUint8(r++,e>>8),this.#t.setUint8(r++,e);break;case 5:this.#t.setUint8(r++,8|e/2**32&7),this.#t.setUint8(r++,e>>24),this.#t.setUint8(r++,e>>16),this.#t.setUint8(r++,e>>8),this.#t.setUint8(r++,e);break;case 6:this.#t.setUint8(r++,4|e/2**40&3),this.#t.setUint8(r++,e/2**32|0),this.#t.setUint8(r++,e>>24),this.#t.setUint8(r++,e>>16),this.#t.setUint8(r++,e>>8),this.#t.setUint8(r++,e);break;default:throw new Error("Bad EBML VINT size "+t)}this.#e.write(this.#i.subarray(0,r))}#S(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.#p(e.id),Array.isArray(e.data)){let t=this.#e.getPos(),r=e.size===-1?1:e.size??4;e.size===-1?this.#T(255):this.#e.seek(this.#e.getPos()+r);let n=this.#e.getPos();if(this.dataOffsets.set(e,n),this.writeEBML(e.data),e.size!==-1){let o=this.#e.getPos()-n,a=this.#e.getPos();this.#e.seek(t),this.writeEBMLVarInt(o,r),this.#e.seek(a)}}else if(typeof e.data=="number"){let t=e.size??ce(e.data);this.writeEBMLVarInt(t),this.#p(e.data,t)}else typeof e.data=="string"?(this.writeEBMLVarInt(e.data.length),this.#S(e.data)):e.data instanceof Uint8Array?(this.writeEBMLVarInt(e.data.byteLength,e.size),this.#e.write(e.data)):e.data instanceof W?(this.writeEBMLVarInt(4),this.#C(e.data.value)):e.data instanceof E&&(this.writeEBMLVarInt(8),this.#g(e.data.value))}beforeTrackAdd(e){if(this.#r instanceof D){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(!["opus","vorbis"].includes(e.source.codec))throw new Error("WebM only supports Opus and Vorbis as audio codecs. Switching to MKV removes this restriction.")}}start(){this.#A(),this.#r.options.streaming||this.#O(),this.#v(),this.#U(),this.#e.flush()}#A(){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 D?"webm":"matroska"},{id:17031,data:2},{id:17029,data:2}]};this.writeEBML(e)}#O(){let e=new Uint8Array([28,83,187,107]),t=new Uint8Array([21,73,169,102]),r=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:r},{id:21420,size:5,data:0}]}]};this.#a=n}#v(){let e={id:17545,data:new E(0)};this.#f=e;let t={id:357149030,data:[{id:2807729,data:1e6},{id:19840,data:Ue},{id:22337,data:Ue},this.#r.options.streaming?null:e]};this.#s=t}#V(){let e={id:374648427,data:[]};this.#l=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:t.type==="video"?wt:St},{id:134,data:At[t.track.source.codec]},t.info.decoderConfig.description?{id:25506,data:M(t.info.decoderConfig.description)}:null,...t.type==="video"?[t.track.source.metadata.frameRate?{id:2352003,data:1e9/t.track.source.metadata.frameRate}:null,{id:224,data:[{id:176,data:t.info.width},{id:186,data:t.info.height},(()=>{if(t.info.decoderConfig.colorSpace){let r=t.info.decoderConfig.colorSpace;return!r.matrix||!r.transfer||!r.primaries||r.fullRange==null?null:{id:21936,data:[{id:21937,data:{rgb:1,bt709:1,bt470bg:5,smpte170m:6}[r.matrix]},{id:21946,data:{bt709:1,smpte170m:6,"iec61966-2-1":13}[r.transfer]},{id:21947,data:{bt709:1,bt470bg:5,smpte170m:6}[r.primaries]},{id:21945,data:[1,2][Number(r.fullRange)]}]}}return null})()]}]:[],...t.type==="audio"?[{id:225,data:[{id:181,data:new W(t.info.sampleRate)},{id:159,data:t.info.numberOfChannels}]}]:[]]})}#z(){let e={id:408125543,size:this.#r.options.streaming?-1:Me,data:[this.#r.options.streaming?null:this.#a,this.#s,this.#l]};this.#n=e,this.writeEBML(e)}#U(){this.#d={id:475249515,data:[]}}get#b(){return d(this.#n),this.dataOffsets.get(this.#n)}#M(e,t,r){let n=this.#o.find(a=>a.track===e);if(n)return n;d(r),d(r.decoderConfig),d(r.decoderConfig.codedWidth!==void 0),d(r.decoderConfig.codedHeight!==void 0);let o={track:e,type:"video",info:{width:r.decoderConfig.codedWidth,height:r.decoderConfig.codedHeight,decoderConfig:r.decoderConfig},chunkQueue:[],firstTimestamp:null,lastTimestamp:null,lastWrittenTimestamp:null};return this.#o.push(o),this.#o.sort((a,u)=>a.track.id-u.track.id),o}#P(e,t,r){let n=this.#o.find(a=>a.track===e);if(n)return n;d(r),d(r.decoderConfig);let o={track:e,type:"audio",info:{numberOfChannels:r.decoderConfig.numberOfChannels,sampleRate:r.decoderConfig.sampleRate,decoderConfig:r.decoderConfig},chunkQueue:[],firstTimestamp:null,lastTimestamp:null,lastWrittenTimestamp:null};return this.#o.push(o),this.#o.sort((a,u)=>a.track.id-u.track.id),o}addEncodedVideoChunk(e,t,r,n){let o=this.#M(e,t,r),a=this.#x(o,t);e.source.codec==="vp9"&&this.#E(o,a),o.lastTimestamp=a.timestamp,o.chunkQueue.push(a),this.#k(),this.#e.flush()}addEncodedAudioChunk(e,t,r){let n=this.#P(e,t,r),o=this.#x(n,t);n.lastTimestamp=o.timestamp,n.chunkQueue.push(o),this.#k(),this.#e.flush()}#k(){if(!(this.#o.length=2&&r++;let c={rgb:7,bt709:2,bt470bg:1,smpte170m:3}[e.info.decoderConfig.colorSpace.matrix];Te(t.data,r+0,r+3,c)}#x(e,t){let r=this.#D(e,t.timestamp),n=new Uint8Array(t.byteLength);return t.copyTo(n),{data:n,type:t.type,timestamp:r,duration:t.duration,additions:null}}#D(e,t){if(t<0)throw new Error(`Timestamps must be non-negative (got ${t}s).`);if(e.firstTimestamp===null&&(e.firstTimestamp=t),t-=e.firstTimestamp,e.lastTimestamp!==null&&t{if(e===g)return t.type==="key";let S=g.chunkQueue[0];return S&&S.type==="key"});(!this.#u||n&&r-this.#h>=1e3)&&this.#F(r);let o=r-this.#h;if(o<0)return;if(o>=fe)throw new Error(`Current Matroska cluster exceeded its maximum allowed length of ${fe} milliseconds. In order to produce a correct WebM file, you must pass in a key frame at least every ${fe} milliseconds.`);let u=new Uint8Array(4),c=new DataView(u.buffer);c.setUint8(0,128|e.track.id),c.setInt16(1,o,!1);let C=Math.floor((t.duration??0)/1e3);if(C===0&&!t.additions){c.setUint8(3,+(t.type==="key")<<7);let g={id:163,data:[u,t.data]};this.writeEBML(g)}else{let g={id:160,data:[{id:161,data:[u,t.data]},t.type==="delta"?{id:251,data:e.lastWrittenTimestamp-r}:null,t.duration!==null?{id:155,data:C}:null,t.additions?{id:30113,data:t.additions}:null]};this.writeEBML(g)}this.#c=Math.max(this.#c,r+C),e.lastWrittenTimestamp=r,this.#m.add(e)}#F(e){this.#u&&!this.#r.options.streaming&&this.#w(),this.#u={id:524531317,size:this.#r.options.streaming?-1:Pe,data:[{id:231,data:e}]},this.writeEBML(this.#u),this.#h=e,this.#m.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,Pe),this.#e.seek(t);let r=this.offsets.get(this.#u)-this.#b;d(this.#d),this.#d.data.push({id:187,data:[{id:179,data:this.#h},...[...this.#m].map(n=>({id:183,data:[{id:247,data:n.track.id},{id:241,data:r}]}))]})}finalize(){for(let e of this.#o)for(;e.chunkQueue.length>0;)this.#y(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.#b;this.#e.seek(this.offsets.get(this.#n)+4),this.writeEBMLVarInt(t,Me),this.#f.data=new E(this.#c),this.#e.seek(this.offsets.get(this.#f)),this.writeEBML(this.#f),this.#a.data[0].data[1].data=this.offsets.get(this.#d)-this.#b,this.#a.data[1].data[1].data=this.offsets.get(this.#s)-this.#b,this.#a.data[2].data[1].data=this.offsets.get(this.#l)-this.#b,this.#e.seek(this.offsets.get(this.#a)),this.writeEBML(this.#a),this.#e.seek(e)}}};var X=class{},he=class extends X{constructor(e){super();this.options=e}createMuxer(e){return new K(e,this)}},Y=class extends X{constructor(e={}){super();this.options=e}createMuxer(e){return new q(e,this)}},D=class extends Y{};var R=class{},L=class extends R{#e=0;#r;#i=new ArrayBuffer(2**16);#t=new Uint8Array(this.#i);#o=0;constructor(s){super(),this.#r=s}#n(s){let e=this.#i.byteLength;for(;et.start-r.start);s.push({start:e[0].start,size:e[0].data.byteLength});for(let t=1;tu.start<=e&&evt){for(let u=0;u=s.written[n+1].start;)s.written[n].end=Math.max(s.written[n].end,s.written[n+1].end),s.written.splice(n+1,1)}#s(s){let t={start:Math.floor(s/this.#i)*this.#i,data:new Uint8Array(this.#i),written:[],shouldFlush:!1};return this.#t.push(t),this.#t.sort((r,n)=>r.start-n.start),this.#t.indexOf(t)}#a(s=!1){for(let e=0;es.stream.write({type:"write",data:e,position:t}),chunkSize:s.options?.chunkSize}))}};var Vt=Symbol("isTarget");Vt;var F=class{},me=class extends F{constructor(){super(...arguments);this.buffer=null}createWriter(){return new L(this)}},$=class extends F{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 H(this):new Z(this)}},pe=class extends F{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 J(this)}};export{me as ArrayBufferTarget,se as AudioBufferSource,ie as AudioDataSource,te as CanvasSource,pe as FileSystemWritableFileStreamTarget,ne as MediaStreamAudioTrackSource,re as MediaStreamVideoTrackSource,Y as MkvOutputFormat,he as Mp4OutputFormat,oe as Output,$ as StreamTarget,F as Target,ee as VideoFrameSource,D as WebMOutputFormat};
diff --git a/dist/metamuxer.mjs b/dist/metamuxer.mjs
index 15d4e02..efc04d7 100644
--- a/dist/metamuxer.mjs
+++ b/dist/metamuxer.mjs
@@ -101,6 +101,35 @@ var last = (arr) => {
var isU32 = (value) => {
return value >= 0 && value < 2 ** 32;
};
+var readBits = (bytes2, start, end) => {
+ let result = 0;
+ for (let i = start; i < end; i++) {
+ let byteIndex = Math.floor(i / 8);
+ let byte = bytes2[byteIndex];
+ let bitIndex = 7 - (i & 7);
+ let bit = (byte & 1 << bitIndex) >> bitIndex;
+ result <<= 1;
+ result |= bit;
+ }
+ return result;
+};
+var writeBits = (bytes2, start, end, value) => {
+ for (let i = start; i < end; i++) {
+ let byteIndex = Math.floor(i / 8);
+ let byte = bytes2[byteIndex];
+ let bitIndex = 7 - (i & 7);
+ byte &= ~(1 << bitIndex);
+ byte |= (value & 1 << end - i - 1) >> end - i - 1 << bitIndex;
+ bytes2[byteIndex] = byte;
+ }
+};
+var toUint8Array = (source) => {
+ if (source instanceof ArrayBuffer) {
+ return new Uint8Array(source);
+ } else {
+ return new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
+ }
+};
// src/source.ts
var VideoSource = class {
@@ -361,6 +390,7 @@ var Output = class {
type: source instanceof VideoSource ? "video" : "audio",
source
};
+ this.muxer.beforeTrackAdd(track);
this.tracks.push(track);
source.connectedTrack = track;
}
@@ -635,6 +665,7 @@ var hdlr = (componentSubtype) => fullBox("hdlr", 0, 0, [
// Component flags
u32(0),
// Component flags mask
+ // TODO:
ascii("mp4-muxer-hdlr", true)
// Component name
]);
@@ -727,11 +758,11 @@ var videoSampleDescription = (compressionType, trackData) => box(compressionType
]);
var avcC = (trackData) => trackData.info.decoderConfig && box("avcC", [
// For AVC, description is an AVCDecoderConfigurationRecord, so nothing else to do here
- ...new Uint8Array(trackData.info.decoderConfig.description)
+ ...toUint8Array(trackData.info.decoderConfig.description)
]);
var hvcC = (trackData) => trackData.info.decoderConfig && box("hvcC", [
// For HEVC, description is a HEVCDecoderConfigurationRecord, so nothing else to do here
- ...new Uint8Array(trackData.info.decoderConfig.description)
+ ...toUint8Array(trackData.info.decoderConfig.description)
]);
var vpcC = (trackData) => {
if (!trackData.info.decoderConfig) {
@@ -803,7 +834,7 @@ var soundSampleDescription = (compressionType, trackData) => box(compressionType
AUDIO_CODEC_TO_CONFIGURATION_BOX[trackData.track.source.codec](trackData)
]);
var esds = (trackData) => {
- let description = new Uint8Array(trackData.info.decoderConfig.description);
+ let description = toUint8Array(trackData.info.decoderConfig.description ?? new ArrayBuffer(0));
return fullBox("esds", 0, 0, [
// https://stackoverflow.com/a/54803118
u32(58753152),
@@ -1128,6 +1159,8 @@ var Muxer = class {
constructor(output) {
this.output = output;
}
+ beforeTrackAdd(track) {
+ }
};
// src/isobmff/isobmff_muxer.ts
@@ -1233,9 +1266,6 @@ var IsobmffMuxer = class extends Muxer {
}
}
start() {
- this.#writeHeader();
- }
- #writeHeader() {
const holdsAvc = this.output.tracks.some((x) => x.type === "video" && x.source.codec === "avc");
this.writeBox(ftyp({
holdsAvc,
@@ -1280,8 +1310,8 @@ var IsobmffMuxer = class extends Muxer {
}
assert(meta);
assert(meta.decoderConfig);
- assert(meta.decoderConfig.codedWidth);
- assert(meta.decoderConfig.codedHeight);
+ assert(meta.decoderConfig.codedWidth !== void 0);
+ assert(meta.decoderConfig.codedHeight !== void 0);
const newTrackData = {
track,
type: "video",
@@ -1473,6 +1503,9 @@ var IsobmffMuxer = class extends Muxer {
trackData.currentChunk.samples.push(sample);
}
#validateTimestamp(trackData, presentationTimestamp, decodeTimestamp) {
+ if (decodeTimestamp < 0) {
+ throw new Error(`Timestamps must be non-negative (got ${decodeTimestamp}s).`);
+ }
if (trackData.firstDecodeTimestamp === null) {
trackData.firstDecodeTimestamp = decodeTimestamp;
}
@@ -1657,6 +1690,721 @@ var IsobmffMuxer = class extends Muxer {
}
};
+// src/matroska/ebml.ts
+var EBMLFloat32 = class {
+ constructor(value) {
+ this.value = value;
+ }
+};
+var EBMLFloat64 = class {
+ constructor(value) {
+ this.value = value;
+ }
+};
+var measureUnsignedInt = (value) => {
+ if (value < 1 << 8) {
+ return 1;
+ } else if (value < 1 << 16) {
+ return 2;
+ } else if (value < 1 << 24) {
+ return 3;
+ } else if (value < 2 ** 32) {
+ return 4;
+ } else if (value < 2 ** 40) {
+ return 5;
+ } else {
+ return 6;
+ }
+};
+var measureEBMLVarInt = (value) => {
+ if (value < (1 << 7) - 1) {
+ return 1;
+ } else if (value < (1 << 14) - 1) {
+ return 2;
+ } else if (value < (1 << 21) - 1) {
+ return 3;
+ } else if (value < (1 << 28) - 1) {
+ return 4;
+ } else if (value < 2 ** 35 - 1) {
+ return 5;
+ } else if (value < 2 ** 42 - 1) {
+ return 6;
+ } else {
+ throw new Error("EBML VINT size not supported " + value);
+ }
+};
+
+// src/matroska/matroska_muxer.ts
+var VIDEO_TRACK_TYPE = 1;
+var AUDIO_TRACK_TYPE = 2;
+var MAX_CHUNK_LENGTH_MS = 2 ** 15;
+var APP_NAME = "https://github.com/Vanilagy/webm-muxer";
+var SEGMENT_SIZE_BYTES = 6;
+var CLUSTER_SIZE_BYTES = 5;
+var CODEC_STRING_MAP = {
+ 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"
+};
+var MatroskaMuxer = 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 EBML 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();
+ /** Same as offsets, but stores position where the element's data starts (after ID and size fields). */
+ this.dataOffsets = /* @__PURE__ */ new WeakMap();
+ this.#trackDatas = [];
+ this.#segment = null;
+ this.#segmentInfo = null;
+ this.#seekHead = null;
+ this.#tracksElement = null;
+ this.#segmentDuration = null;
+ this.#cues = null;
+ this.#currentCluster = null;
+ this.#currentClusterTimestamp = null;
+ this.#trackDatasInCurrentCluster = /* @__PURE__ */ new Set();
+ this.#duration = 0;
+ this.#writer = output.writer;
+ this.#format = format;
+ }
+ #writer;
+ #format;
+ #helper;
+ #helperView;
+ #trackDatas;
+ #segment;
+ #segmentInfo;
+ #seekHead;
+ #tracksElement;
+ #segmentDuration;
+ #cues;
+ #currentCluster;
+ #currentClusterTimestamp;
+ #trackDatasInCurrentCluster;
+ #duration;
+ #writeByte(value) {
+ this.#helperView.setUint8(0, value);
+ this.#writer.write(this.#helper.subarray(0, 1));
+ }
+ #writeFloat32(value) {
+ this.#helperView.setFloat32(0, value, false);
+ this.#writer.write(this.#helper.subarray(0, 4));
+ }
+ #writeFloat64(value) {
+ this.#helperView.setFloat64(0, value, false);
+ this.#writer.write(this.#helper);
+ }
+ #writeUnsignedInt(value, width = measureUnsignedInt(value)) {
+ let pos = 0;
+ switch (width) {
+ case 6:
+ this.#helperView.setUint8(pos++, value / 2 ** 40 | 0);
+ case 5:
+ this.#helperView.setUint8(pos++, value / 2 ** 32 | 0);
+ case 4:
+ this.#helperView.setUint8(pos++, value >> 24);
+ case 3:
+ this.#helperView.setUint8(pos++, value >> 16);
+ case 2:
+ this.#helperView.setUint8(pos++, value >> 8);
+ case 1:
+ this.#helperView.setUint8(pos++, value);
+ break;
+ default:
+ throw new Error("Bad UINT size " + width);
+ }
+ this.#writer.write(this.#helper.subarray(0, pos));
+ }
+ writeEBMLVarInt(value, width = measureEBMLVarInt(value)) {
+ let pos = 0;
+ switch (width) {
+ case 1:
+ this.#helperView.setUint8(pos++, 1 << 7 | value);
+ break;
+ case 2:
+ this.#helperView.setUint8(pos++, 1 << 6 | value >> 8);
+ this.#helperView.setUint8(pos++, value);
+ break;
+ case 3:
+ this.#helperView.setUint8(pos++, 1 << 5 | value >> 16);
+ this.#helperView.setUint8(pos++, value >> 8);
+ this.#helperView.setUint8(pos++, value);
+ break;
+ case 4:
+ this.#helperView.setUint8(pos++, 1 << 4 | value >> 24);
+ this.#helperView.setUint8(pos++, value >> 16);
+ this.#helperView.setUint8(pos++, value >> 8);
+ this.#helperView.setUint8(pos++, value);
+ break;
+ case 5:
+ this.#helperView.setUint8(pos++, 1 << 3 | value / 2 ** 32 & 7);
+ this.#helperView.setUint8(pos++, value >> 24);
+ this.#helperView.setUint8(pos++, value >> 16);
+ this.#helperView.setUint8(pos++, value >> 8);
+ this.#helperView.setUint8(pos++, value);
+ break;
+ case 6:
+ this.#helperView.setUint8(pos++, 1 << 2 | value / 2 ** 40 & 3);
+ this.#helperView.setUint8(pos++, value / 2 ** 32 | 0);
+ this.#helperView.setUint8(pos++, value >> 24);
+ this.#helperView.setUint8(pos++, value >> 16);
+ this.#helperView.setUint8(pos++, value >> 8);
+ this.#helperView.setUint8(pos++, value);
+ break;
+ default:
+ throw new Error("Bad EBML VINT size " + width);
+ }
+ this.#writer.write(this.#helper.subarray(0, pos));
+ }
+ // Assumes the string is ASCII
+ #writeString(str) {
+ this.#writer.write(new Uint8Array(str.split("").map((x) => x.charCodeAt(0))));
+ }
+ writeEBML(data) {
+ if (data === null) return;
+ if (data instanceof Uint8Array) {
+ this.#writer.write(data);
+ } else if (Array.isArray(data)) {
+ for (let elem of data) {
+ this.writeEBML(elem);
+ }
+ } else {
+ this.offsets.set(data, this.#writer.getPos());
+ this.#writeUnsignedInt(data.id);
+ if (Array.isArray(data.data)) {
+ let sizePos = this.#writer.getPos();
+ let sizeSize = data.size === -1 ? 1 : data.size ?? 4;
+ if (data.size === -1) {
+ this.#writeByte(255);
+ } else {
+ this.#writer.seek(this.#writer.getPos() + sizeSize);
+ }
+ let startPos = this.#writer.getPos();
+ this.dataOffsets.set(data, startPos);
+ this.writeEBML(data.data);
+ if (data.size !== -1) {
+ let size = this.#writer.getPos() - startPos;
+ let endPos = this.#writer.getPos();
+ this.#writer.seek(sizePos);
+ this.writeEBMLVarInt(size, sizeSize);
+ this.#writer.seek(endPos);
+ }
+ } else if (typeof data.data === "number") {
+ let size = data.size ?? measureUnsignedInt(data.data);
+ this.writeEBMLVarInt(size);
+ this.#writeUnsignedInt(data.data, size);
+ } else if (typeof data.data === "string") {
+ this.writeEBMLVarInt(data.data.length);
+ this.#writeString(data.data);
+ } else if (data.data instanceof Uint8Array) {
+ this.writeEBMLVarInt(data.data.byteLength, data.size);
+ this.#writer.write(data.data);
+ } else if (data.data instanceof EBMLFloat32) {
+ this.writeEBMLVarInt(4);
+ this.#writeFloat32(data.data.value);
+ } else if (data.data instanceof EBMLFloat64) {
+ this.writeEBMLVarInt(8);
+ this.#writeFloat64(data.data.value);
+ }
+ }
+ }
+ beforeTrackAdd(track) {
+ if (!(this.#format instanceof WebMOutputFormat)) {
+ return;
+ }
+ if (track.type === "video") {
+ if (!["vp8", "vp9", "av1"].includes(track.source.codec)) {
+ throw new Error(`WebM only supports VP8, VP9 and AV1 as video codecs. Switching to MKV removes this restriction.`);
+ }
+ } else {
+ if (!["opus", "vorbis"].includes(track.source.codec)) {
+ throw new Error(`WebM only supports Opus and Vorbis as audio codecs. Switching to MKV removes this restriction.`);
+ }
+ }
+ }
+ start() {
+ this.#writeEBMLHeader();
+ if (!this.#format.options.streaming) {
+ this.#createSeekHead();
+ }
+ this.#createSegmentInfo();
+ this.#createCues();
+ this.#writer.flush();
+ }
+ #writeEBMLHeader() {
+ let ebmlHeader = { id: 440786851 /* EBML */, data: [
+ { id: 17030 /* EBMLVersion */, data: 1 },
+ { id: 17143 /* EBMLReadVersion */, data: 1 },
+ { id: 17138 /* EBMLMaxIDLength */, data: 4 },
+ { id: 17139 /* EBMLMaxSizeLength */, data: 8 },
+ { id: 17026 /* DocType */, data: this.#format instanceof WebMOutputFormat ? "webm" : "matroska" },
+ { id: 17031 /* DocTypeVersion */, data: 2 },
+ { id: 17029 /* DocTypeReadVersion */, data: 2 }
+ ] };
+ this.writeEBML(ebmlHeader);
+ }
+ /**
+ * Creates a SeekHead element which is positioned near the start of the file and allows the media player to seek to
+ * relevant sections more easily. Since we don't know the positions of those sections yet, we'll set them later.
+ */
+ #createSeekHead() {
+ const kaxCues = new Uint8Array([28, 83, 187, 107]);
+ const kaxInfo = new Uint8Array([21, 73, 169, 102]);
+ const kaxTracks = new Uint8Array([22, 84, 174, 107]);
+ let seekHead = { id: 290298740 /* SeekHead */, data: [
+ { id: 19899 /* Seek */, data: [
+ { id: 21419 /* SeekID */, data: kaxCues },
+ { id: 21420 /* SeekPosition */, size: 5, data: 0 }
+ ] },
+ { id: 19899 /* Seek */, data: [
+ { id: 21419 /* SeekID */, data: kaxInfo },
+ { id: 21420 /* SeekPosition */, size: 5, data: 0 }
+ ] },
+ { id: 19899 /* Seek */, data: [
+ { id: 21419 /* SeekID */, data: kaxTracks },
+ { id: 21420 /* SeekPosition */, size: 5, data: 0 }
+ ] }
+ ] };
+ this.#seekHead = seekHead;
+ }
+ #createSegmentInfo() {
+ let segmentDuration = { id: 17545 /* Duration */, data: new EBMLFloat64(0) };
+ this.#segmentDuration = segmentDuration;
+ let segmentInfo = { id: 357149030 /* Info */, data: [
+ { id: 2807729 /* TimestampScale */, data: 1e6 },
+ { id: 19840 /* MuxingApp */, data: APP_NAME },
+ { id: 22337 /* WritingApp */, data: APP_NAME },
+ !this.#format.options.streaming ? segmentDuration : null
+ ] };
+ this.#segmentInfo = segmentInfo;
+ }
+ #createTracks() {
+ let tracksElement = { id: 374648427 /* Tracks */, data: [] };
+ this.#tracksElement = tracksElement;
+ for (let trackData of this.#trackDatas) {
+ tracksElement.data.push({ id: 174 /* TrackEntry */, data: [
+ { id: 215 /* TrackNumber */, data: trackData.track.id },
+ { id: 29637 /* TrackUID */, data: trackData.track.id },
+ { id: 131 /* TrackType */, data: trackData.type === "video" ? VIDEO_TRACK_TYPE : AUDIO_TRACK_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.track.source.metadata.frameRate ? { id: 2352003 /* DefaultDuration */, data: 1e9 / trackData.track.source.metadata.frameRate } : null,
+ { id: 224 /* Video */, data: [
+ { id: 176 /* PixelWidth */, data: trackData.info.width },
+ { id: 186 /* PixelHeight */, data: trackData.info.height },
+ (() => {
+ if (trackData.info.decoderConfig.colorSpace) {
+ let colorSpace = trackData.info.decoderConfig.colorSpace;
+ if (!colorSpace.matrix || !colorSpace.transfer || !colorSpace.primaries || colorSpace.fullRange == null) {
+ return null;
+ }
+ return { id: 21936 /* Colour */, data: [
+ { id: 21937 /* MatrixCoefficients */, data: {
+ "rgb": 1,
+ "bt709": 1,
+ "bt470bg": 5,
+ "smpte170m": 6
+ }[colorSpace.matrix] },
+ { id: 21946 /* TransferCharacteristics */, data: {
+ "bt709": 1,
+ "smpte170m": 6,
+ "iec61966-2-1": 13
+ }[colorSpace.transfer] },
+ { id: 21947 /* Primaries */, data: {
+ "bt709": 1,
+ "bt470bg": 5,
+ "smpte170m": 6
+ }[colorSpace.primaries] },
+ { id: 21945 /* Range */, data: [1, 2][Number(colorSpace.fullRange)] }
+ ] };
+ }
+ return null;
+ })()
+ ] }
+ ] : [],
+ ...trackData.type === "audio" ? [
+ { 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
+ ] }
+ ] : []
+ ] });
+ }
+ }
+ #createSegment() {
+ let segment = {
+ id: 408125543 /* Segment */,
+ size: this.#format.options.streaming ? -1 : SEGMENT_SIZE_BYTES,
+ data: [
+ !this.#format.options.streaming ? this.#seekHead : null,
+ this.#segmentInfo,
+ this.#tracksElement
+ ]
+ };
+ this.#segment = segment;
+ this.writeEBML(segment);
+ }
+ #createCues() {
+ this.#cues = { id: 475249515 /* Cues */, data: [] };
+ }
+ get #segmentDataOffset() {
+ assert(this.#segment);
+ return this.dataOffsets.get(this.#segment);
+ }
+ #getVideoTrackData(track, chunk, meta) {
+ const existingTrackData = this.#trackDatas.find((x) => x.track === track);
+ if (existingTrackData) {
+ return existingTrackData;
+ }
+ assert(meta);
+ assert(meta.decoderConfig);
+ assert(meta.decoderConfig.codedWidth !== void 0);
+ assert(meta.decoderConfig.codedHeight !== void 0);
+ const newTrackData = {
+ track,
+ type: "video",
+ info: {
+ width: meta.decoderConfig.codedWidth,
+ height: meta.decoderConfig.codedHeight,
+ decoderConfig: meta.decoderConfig
+ },
+ chunkQueue: [],
+ firstTimestamp: null,
+ lastTimestamp: null,
+ lastWrittenTimestamp: null
+ };
+ this.#trackDatas.push(newTrackData);
+ this.#trackDatas.sort((a, b) => a.track.id - b.track.id);
+ return newTrackData;
+ }
+ #getAudioTrackData(track, chunk, meta) {
+ const existingTrackData = this.#trackDatas.find((x) => x.track === track);
+ if (existingTrackData) {
+ return existingTrackData;
+ }
+ assert(meta);
+ assert(meta.decoderConfig);
+ const newTrackData = {
+ track,
+ type: "audio",
+ info: {
+ numberOfChannels: meta.decoderConfig.numberOfChannels,
+ sampleRate: meta.decoderConfig.sampleRate,
+ decoderConfig: meta.decoderConfig
+ },
+ chunkQueue: [],
+ firstTimestamp: null,
+ lastTimestamp: null,
+ lastWrittenTimestamp: null
+ };
+ this.#trackDatas.push(newTrackData);
+ this.#trackDatas.sort((a, b) => a.track.id - b.track.id);
+ return newTrackData;
+ }
+ addEncodedVideoChunk(track, chunk, meta, compositionTimeOffset) {
+ const trackData = this.#getVideoTrackData(track, chunk, meta);
+ let videoChunk = this.#createInternalChunk(trackData, chunk);
+ if (track.source.codec === "vp9") this.#fixVP9ColorSpace(trackData, videoChunk);
+ trackData.lastTimestamp = videoChunk.timestamp;
+ trackData.chunkQueue.push(videoChunk);
+ this.#interleaveChunks();
+ this.#writer.flush();
+ }
+ addEncodedAudioChunk(track, chunk, meta) {
+ const trackData = this.#getAudioTrackData(track, chunk, meta);
+ let audioChunk = this.#createInternalChunk(trackData, chunk);
+ trackData.lastTimestamp = audioChunk.timestamp;
+ trackData.chunkQueue.push(audioChunk);
+ this.#interleaveChunks();
+ this.#writer.flush();
+ }
+ #interleaveChunks() {
+ if (this.#trackDatas.length < this.output.tracks.length) {
+ return;
+ }
+ outer:
+ while (true) {
+ let trackWithMinTimestamp = null;
+ let minTimestamp = Infinity;
+ for (let trackData of this.#trackDatas) {
+ if (trackData.chunkQueue.length === 0) {
+ break outer;
+ }
+ if (trackData.chunkQueue[0].timestamp < minTimestamp) {
+ trackWithMinTimestamp = trackData;
+ minTimestamp = trackData.chunkQueue[0].timestamp;
+ }
+ }
+ if (!trackWithMinTimestamp) {
+ break;
+ }
+ let chunk = trackWithMinTimestamp.chunkQueue.shift();
+ this.#writeBlock(trackWithMinTimestamp, chunk);
+ }
+ }
+ /** Due to [a bug in Chromium](https://bugs.chromium.org/p/chromium/issues/detail?id=1377842), VP9 streams often
+ * lack color space information. This method patches in that information. */
+ // http://downloads.webmproject.org/docs/vp9/vp9-bitstream_superframe-and-uncompressed-header_v1.0.pdf
+ #fixVP9ColorSpace(trackData, chunk) {
+ if (chunk.type !== "key") return;
+ if (!trackData.info.decoderConfig.colorSpace || !trackData.info.decoderConfig.colorSpace.matrix) return;
+ let i = 0;
+ if (readBits(chunk.data, 0, 2) !== 2) return;
+ i += 2;
+ let profile = (readBits(chunk.data, i + 1, i + 2) << 1) + readBits(chunk.data, i + 0, i + 1);
+ i += 2;
+ if (profile === 3) i++;
+ let showExistingFrame = readBits(chunk.data, i + 0, i + 1);
+ i++;
+ if (showExistingFrame) return;
+ let frameType = readBits(chunk.data, i + 0, i + 1);
+ i++;
+ if (frameType !== 0) return;
+ i += 2;
+ let syncCode = readBits(chunk.data, i + 0, i + 24);
+ i += 24;
+ if (syncCode !== 4817730) return;
+ if (profile >= 2) i++;
+ let colorSpaceID = {
+ "rgb": 7,
+ "bt709": 2,
+ "bt470bg": 1,
+ "smpte170m": 3
+ }[trackData.info.decoderConfig.colorSpace.matrix];
+ writeBits(chunk.data, i + 0, i + 3, colorSpaceID);
+ }
+ /*
+ addSubtitleChunk(chunk: EncodedSubtitleChunk, meta: EncodedSubtitleChunkMetadata, timestamp?: number) {
+ if (typeof chunk !== 'object' || !chunk) {
+ throw new TypeError("addSubtitleChunk's first argument (chunk) must be an object.");
+ } else {
+ // We can't simply do an instanceof check, so let's check the structure itself:
+ if (!(chunk.body instanceof Uint8Array)) {
+ throw new TypeError('body must be an instance of Uint8Array.');
+ }
+ if (!Number.isFinite(chunk.timestamp) || chunk.timestamp < 0) {
+ throw new TypeError('timestamp must be a non-negative real number.');
+ }
+ if (!Number.isFinite(chunk.duration) || chunk.duration < 0) {
+ throw new TypeError('duration must be a non-negative real number.');
+ }
+ if (chunk.additions && !(chunk.additions instanceof Uint8Array)) {
+ throw new TypeError('additions, when present, must be an instance of Uint8Array.');
+ }
+ }
+
+ if (typeof meta !== 'object') {
+ throw new TypeError("addSubtitleChunk's second argument (meta) must be an object.");
+ }
+
+ this.#ensureNotFinalized();
+ if (!this.#options.subtitles) throw new Error('No subtitle track declared.');
+
+ // Write possible subtitle decoder metadata to the file
+ if (meta?.decoderConfig) {
+ if (this.#options.streaming) {
+ this.#subtitleCodecPrivate = this.#createCodecPrivateElement(meta.decoderConfig.description);
+ } else {
+ this.#writeCodecPrivate(this.#subtitleCodecPrivate, meta.decoderConfig.description);
+ }
+ }
+
+ let subtitleChunk = this.#createInternalChunk(
+ chunk.body,
+ 'key',
+ timestamp ?? chunk.timestamp,
+ SUBTITLE_TRACK_NUMBER,
+ chunk.duration,
+ chunk.additions
+ );
+
+ this.#lastSubtitleTimestamp = subtitleChunk.timestamp;
+ this.#subtitleChunkQueue.push(subtitleChunk);
+
+ this.#writeSubtitleChunks();
+ this.#maybeFlushStreamingTargetWriter();
+ }
+
+ #writeSubtitleChunks() {
+ // Writing subtitle chunks is different from video and audio: A subtitle chunk will be written if it's
+ // guaranteed that no more media chunks will be written before it, to ensure monotonicity. However, media chunks
+ // will NOT wait for subtitle chunks to arrive, as they may never arrive, so that's how non-monotonicity can
+ // arrive. But it should be fine, since it's all still in one cluster.
+
+ let lastWrittenMediaTimestamp = Math.min(
+ this.#options.video ? this.#lastVideoTimestamp : Infinity,
+ this.#options.audio ? this.#lastAudioTimestamp : Infinity
+ );
+
+ let queue = this.#subtitleChunkQueue;
+ while (queue.length > 0 && queue[0].timestamp <= lastWrittenMediaTimestamp) {
+ this.#writeBlock(queue.shift(), !this.#options.video && !this.#options.audio);
+ }
+ }
+ */
+ /** Converts a read-only external chunk into an internal one for easier use. */
+ #createInternalChunk(trackData, chunk) {
+ let adjustedTimestamp = this.#validateTimestamp(trackData, chunk.timestamp);
+ let data = new Uint8Array(chunk.byteLength);
+ chunk.copyTo(data);
+ let internalChunk = {
+ data,
+ type: chunk.type,
+ timestamp: adjustedTimestamp,
+ duration: chunk.duration,
+ additions: null
+ };
+ return internalChunk;
+ }
+ #validateTimestamp(trackData, timestamp) {
+ if (timestamp < 0) {
+ throw new Error(`Timestamps must be non-negative (got ${timestamp}s).`);
+ }
+ if (trackData.firstTimestamp === null) {
+ trackData.firstTimestamp = timestamp;
+ }
+ timestamp -= trackData.firstTimestamp;
+ if (trackData.lastTimestamp !== null && timestamp < trackData.lastTimestamp) {
+ throw new Error(
+ `Timestamps must be monotonically increasing (timestamp went from ${trackData.lastTimestamp}s to ${timestamp}s).`
+ );
+ }
+ return timestamp;
+ }
+ /** Writes a block containing media data to the file. */
+ #writeBlock(trackData, chunk) {
+ if (!this.#segment) {
+ this.#createTracks();
+ this.#createSegment();
+ }
+ let msTimestamp = Math.floor(chunk.timestamp / 1e3);
+ const keyFrameQueuedEverywhere = this.#trackDatas.every((otherTrackData) => {
+ if (trackData === otherTrackData) {
+ return chunk.type === "key";
+ }
+ const firstQueuedSample = otherTrackData.chunkQueue[0];
+ return firstQueuedSample && firstQueuedSample.type === "key";
+ });
+ if (!this.#currentCluster || keyFrameQueuedEverywhere && msTimestamp - this.#currentClusterTimestamp >= 1e3) {
+ this.#createNewCluster(msTimestamp);
+ }
+ let relativeTimestamp = msTimestamp - this.#currentClusterTimestamp;
+ if (relativeTimestamp < 0) {
+ return;
+ }
+ let clusterIsTooLong = relativeTimestamp >= MAX_CHUNK_LENGTH_MS;
+ if (clusterIsTooLong) {
+ throw new Error(
+ `Current Matroska cluster exceeded its maximum allowed length of ${MAX_CHUNK_LENGTH_MS} milliseconds. In order to produce a correct WebM file, you must pass in a key frame at least every ${MAX_CHUNK_LENGTH_MS} milliseconds.`
+ );
+ }
+ let prelude = new Uint8Array(4);
+ let view2 = new DataView(prelude.buffer);
+ view2.setUint8(0, 128 | trackData.track.id);
+ view2.setInt16(1, relativeTimestamp, false);
+ let msDuration = Math.floor((chunk.duration ?? 0) / 1e3);
+ if (msDuration === 0 && !chunk.additions) {
+ view2.setUint8(3, Number(chunk.type === "key") << 7);
+ let simpleBlock = { id: 163 /* SimpleBlock */, data: [
+ prelude,
+ chunk.data
+ ] };
+ this.writeEBML(simpleBlock);
+ } else {
+ let blockGroup = { id: 160 /* BlockGroup */, data: [
+ { id: 161 /* Block */, data: [
+ prelude,
+ chunk.data
+ ] },
+ chunk.type === "delta" ? { id: 251 /* ReferenceBlock */, data: trackData.lastWrittenTimestamp - msTimestamp } : null,
+ chunk.duration !== null ? { id: 155 /* BlockDuration */, data: msDuration } : null,
+ chunk.additions ? { id: 30113 /* BlockAdditions */, data: chunk.additions } : null
+ ] };
+ this.writeEBML(blockGroup);
+ }
+ this.#duration = Math.max(this.#duration, msTimestamp + msDuration);
+ trackData.lastWrittenTimestamp = msTimestamp;
+ this.#trackDatasInCurrentCluster.add(trackData);
+ }
+ /** Creates a new Cluster element to contain media chunks. */
+ #createNewCluster(timestamp) {
+ if (this.#currentCluster && !this.#format.options.streaming) {
+ this.#finalizeCurrentCluster();
+ }
+ this.#currentCluster = {
+ id: 524531317 /* Cluster */,
+ size: this.#format.options.streaming ? -1 : CLUSTER_SIZE_BYTES,
+ data: [
+ { id: 231 /* Timestamp */, data: timestamp }
+ ]
+ };
+ this.writeEBML(this.#currentCluster);
+ this.#currentClusterTimestamp = timestamp;
+ this.#trackDatasInCurrentCluster.clear();
+ }
+ #finalizeCurrentCluster() {
+ assert(this.#currentCluster);
+ let clusterSize = this.#writer.getPos() - this.dataOffsets.get(this.#currentCluster);
+ let endPos = this.#writer.getPos();
+ this.#writer.seek(this.offsets.get(this.#currentCluster) + 4);
+ this.writeEBMLVarInt(clusterSize, CLUSTER_SIZE_BYTES);
+ this.#writer.seek(endPos);
+ let clusterOffsetFromSegment = this.offsets.get(this.#currentCluster) - this.#segmentDataOffset;
+ assert(this.#cues);
+ this.#cues.data.push({ id: 187 /* CuePoint */, data: [
+ { id: 179 /* CueTime */, data: this.#currentClusterTimestamp },
+ // We only write out cues for tracks that have at least one chunk in this cluster
+ ...[...this.#trackDatasInCurrentCluster].map((trackData) => {
+ return { id: 183 /* CueTrackPositions */, data: [
+ { id: 247 /* CueTrack */, data: trackData.track.id },
+ { id: 241 /* CueClusterPosition */, data: clusterOffsetFromSegment }
+ ] };
+ })
+ ] });
+ }
+ /** Finalizes the file, making it ready for use. Must be called after all media chunks have been added. */
+ finalize() {
+ for (let trackData of this.#trackDatas) {
+ while (trackData.chunkQueue.length > 0) {
+ this.#writeBlock(trackData, trackData.chunkQueue.shift());
+ }
+ }
+ if (!this.#format.options.streaming) {
+ this.#finalizeCurrentCluster();
+ }
+ assert(this.#cues);
+ this.writeEBML(this.#cues);
+ if (!this.#format.options.streaming) {
+ let endPos = this.#writer.getPos();
+ let segmentSize = this.#writer.getPos() - this.#segmentDataOffset;
+ this.#writer.seek(this.offsets.get(this.#segment) + 4);
+ this.writeEBMLVarInt(segmentSize, SEGMENT_SIZE_BYTES);
+ this.#segmentDuration.data = new EBMLFloat64(this.#duration);
+ this.#writer.seek(this.offsets.get(this.#segmentDuration));
+ this.writeEBML(this.#segmentDuration);
+ this.#seekHead.data[0].data[1].data = this.offsets.get(this.#cues) - this.#segmentDataOffset;
+ this.#seekHead.data[1].data[1].data = this.offsets.get(this.#segmentInfo) - this.#segmentDataOffset;
+ this.#seekHead.data[2].data[1].data = this.offsets.get(this.#tracksElement) - this.#segmentDataOffset;
+ this.#writer.seek(this.offsets.get(this.#seekHead));
+ this.writeEBML(this.#seekHead);
+ this.#writer.seek(endPos);
+ }
+ }
+};
+
// src/output_format.ts
var OutputFormat = class {
};
@@ -1669,6 +2417,17 @@ var Mp4OutputFormat = class extends OutputFormat {
return new IsobmffMuxer(output, this);
}
};
+var MkvOutputFormat2 = class extends OutputFormat {
+ constructor(options = {}) {
+ super();
+ this.options = options;
+ }
+ createMuxer(output) {
+ return new MatroskaMuxer(output, this);
+ }
+};
+var WebMOutputFormat = class extends MkvOutputFormat2 {
+};
// src/writer.ts
var Writer = class {
@@ -1957,9 +2716,11 @@ export {
FileSystemWritableFileStreamTarget2 as FileSystemWritableFileStreamTarget,
MediaStreamAudioTrackSource,
MediaStreamVideoTrackSource,
+ MkvOutputFormat2 as MkvOutputFormat,
Mp4OutputFormat,
Output,
StreamTarget,
Target,
- VideoFrameSource
+ VideoFrameSource,
+ WebMOutputFormat
};
diff --git a/src/index.ts b/src/index.ts
index bcb7405..e8b9a14 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,4 +1,4 @@
export { Output } from './output';
-export { Mp4OutputFormat } from './output_format';
+export { Mp4OutputFormat, MkvOutputFormat, WebMOutputFormat } from './output_format';
export { VideoFrameSource, CanvasSource, MediaStreamVideoTrackSource, AudioDataSource, AudioBufferSource, MediaStreamAudioTrackSource } from './source';
export { Target, ArrayBufferTarget, StreamTarget, FileSystemWritableFileStreamTarget } from './target';
diff --git a/src/isobmff/isobmff_boxes.ts b/src/isobmff/isobmff_boxes.ts
index 0ce4c51..294e69a 100644
--- a/src/isobmff/isobmff_boxes.ts
+++ b/src/isobmff/isobmff_boxes.ts
@@ -1,4 +1,4 @@
-import { assert, isU32, last, TransformationMatrix } from '../misc';
+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';
@@ -295,6 +295,7 @@ export const hdlr = (componentSubtype: string) => fullBox('hdlr', 0, 0, [
u32(0), // Component manufacturer
u32(0), // Component flags
u32(0), // Component flags mask
+ // TODO:
ascii('mp4-muxer-hdlr', true) // Component name
]);
@@ -401,16 +402,19 @@ export const videoSampleDescription = (
VIDEO_CODEC_TO_CONFIGURATION_BOX[trackData.track.source.codec](trackData)
]);
+// TODO: All muxers should ensure that the decoder config description is provided for the codecs that require it. This
+// is relevant when the user skips WebCodecs and uses their own encoder.
+
/** AVC Configuration Box: Provides additional information to the decoder. */
export const avcC = (trackData: IsobmffVideoTrackData) => trackData.info.decoderConfig && box('avcC', [
// For AVC, description is an AVCDecoderConfigurationRecord, so nothing else to do here
- ...new Uint8Array(trackData.info.decoderConfig.description as ArrayBuffer)
+ ...toUint8Array(trackData.info.decoderConfig.description!)
]);
/** HEVC Configuration Box: Provides additional information to the decoder. */
export const hvcC = (trackData: IsobmffVideoTrackData) => trackData.info.decoderConfig && box('hvcC', [
// For HEVC, description is a HEVCDecoderConfigurationRecord, so nothing else to do here
- ...new Uint8Array(trackData.info.decoderConfig.description as ArrayBuffer)
+ ...toUint8Array(trackData.info.decoderConfig.description!)
]);
/** VP Configuration Box: Provides additional information to the decoder. */
@@ -489,7 +493,7 @@ export const soundSampleDescription = (
/** MPEG-4 Elementary Stream Descriptor Box. */
export const esds = (trackData: IsobmffAudioTrackData) => {
- let description = new Uint8Array(trackData.info.decoderConfig.description as ArrayBuffer);
+ let description = toUint8Array(trackData.info.decoderConfig.description ?? new ArrayBuffer(0));
// TODO Compact the 808080 stuff, it's superfluous
diff --git a/src/isobmff/isobmff_muxer.ts b/src/isobmff/isobmff_muxer.ts
index af738ba..21483cb 100644
--- a/src/isobmff/isobmff_muxer.ts
+++ b/src/isobmff/isobmff_muxer.ts
@@ -177,12 +177,9 @@ export class IsobmffMuxer extends Muxer {
}
start() {
- this.#writeHeader();
- }
-
- #writeHeader() {
const holdsAvc = this.output.tracks.some(x => x.type === 'video' && x.source.codec === 'avc');
-
+
+ // Write the header
this.writeBox(ftyp({
holdsAvc: holdsAvc,
fragmented: this.#format.options.fastStart === 'fragmented'
@@ -248,8 +245,8 @@ export class IsobmffMuxer extends Muxer {
// TODO Make proper errors for these
assert(meta);
assert(meta.decoderConfig);
- assert(meta.decoderConfig.codedWidth);
- assert(meta.decoderConfig.codedHeight);
+ assert(meta.decoderConfig.codedWidth !== undefined);
+ assert(meta.decoderConfig.codedHeight !== undefined);
const newTrackData: IsobmffTrackData = {
track,
@@ -509,6 +506,10 @@ export class IsobmffMuxer extends Muxer {
}
#validateTimestamp(trackData: IsobmffTrackData, presentationTimestamp: number, decodeTimestamp: number) {
+ if (decodeTimestamp < 0) {
+ throw new Error(`Timestamps must be non-negative (got ${decodeTimestamp}s).`);
+ }
+
if (trackData.firstDecodeTimestamp === null) {
trackData.firstDecodeTimestamp = decodeTimestamp;
}
diff --git a/src/matroska/ebml.ts b/src/matroska/ebml.ts
new file mode 100644
index 0000000..20c4c34
--- /dev/null
+++ b/src/matroska/ebml.ts
@@ -0,0 +1,122 @@
+export interface EBMLElement {
+ id: number,
+ size?: number,
+ data: number | string | Uint8Array | EBMLFloat32 | EBMLFloat64 | (EBML | null)[]
+}
+
+export type EBML = EBMLElement | Uint8Array | (EBML | null)[];
+
+/** Wrapper around a number to be able to differentiate it in the writer. */
+export class EBMLFloat32 {
+ value: number;
+
+ constructor(value: number) {
+ this.value = value;
+ }
+}
+
+/** Wrapper around a number to be able to differentiate it in the writer. */
+export class EBMLFloat64 {
+ value: number;
+
+ constructor(value: number) {
+ this.value = value;
+ }
+}
+
+/** Defines some of the EBML IDs used by Matroska files. */
+export enum EBMLId {
+ EBML = 0x1a45dfa3,
+ EBMLVersion = 0x4286,
+ EBMLReadVersion = 0x42f7,
+ EBMLMaxIDLength = 0x42f2,
+ EBMLMaxSizeLength = 0x42f3,
+ DocType = 0x4282,
+ DocTypeVersion = 0x4287,
+ DocTypeReadVersion = 0x4285,
+ SeekHead = 0x114d9b74,
+ Seek = 0x4dbb,
+ SeekID = 0x53ab,
+ SeekPosition = 0x53ac,
+ Duration = 0x4489,
+ Info = 0x1549a966,
+ TimestampScale = 0x2ad7b1,
+ MuxingApp = 0x4d80,
+ WritingApp = 0x5741,
+ Tracks = 0x1654ae6b,
+ TrackEntry = 0xae,
+ TrackNumber = 0xd7,
+ TrackUID = 0x73c5,
+ TrackType = 0x83,
+ CodecID = 0x86,
+ CodecPrivate = 0x63a2,
+ DefaultDuration = 0x23e383,
+ Video = 0xe0,
+ PixelWidth = 0xb0,
+ PixelHeight = 0xba,
+ Void = 0xec,
+ Audio = 0xe1,
+ SamplingFrequency = 0xb5,
+ Channels = 0x9f,
+ BitDepth = 0x6264,
+ Segment = 0x18538067,
+ SimpleBlock = 0xa3,
+ BlockGroup = 0xa0,
+ Block = 0xa1,
+ BlockAdditions = 0x75a1,
+ BlockDuration = 0x9b,
+ ReferenceBlock = 0xfb,
+ Cluster = 0x1f43b675,
+ Timestamp = 0xe7,
+ Cues = 0x1c53bb6b,
+ CuePoint = 0xbb,
+ CueTime = 0xb3,
+ CueTrackPositions = 0xb7,
+ CueTrack = 0xf7,
+ CueClusterPosition = 0xf1,
+ Colour = 0x55b0,
+ MatrixCoefficients = 0x55b1,
+ TransferCharacteristics = 0x55ba,
+ Primaries = 0x55bb,
+ Range = 0x55b9,
+ AlphaMode = 0x53c0
+}
+
+export const measureUnsignedInt = (value: number) => {
+ // Force to 32-bit unsigned integer
+ if (value < (1 << 8)) {
+ return 1;
+ } else if (value < (1 << 16)) {
+ return 2;
+ } else if (value < (1 << 24)) {
+ return 3;
+ } else if (value < 2**32) {
+ return 4;
+ } else if (value < 2**40) {
+ return 5;
+ } else {
+ return 6;
+ }
+};
+
+export const measureEBMLVarInt = (value: number) => {
+ if (value < (1 << 7) - 1) {
+ /** Top bit is set, leaving 7 bits to hold the integer, but we can't store
+ * 127 because "all bits set to one" is a reserved value. Same thing for the
+ * other cases below:
+ */
+ return 1;
+ } else if (value < (1 << 14) - 1) {
+ return 2;
+ } else if (value < (1 << 21) - 1) {
+ return 3;
+ } else if (value < (1 << 28) - 1) {
+ return 4;
+ } else if (value < 2**35-1) {
+ return 5;
+ } else if (value < 2**42-1) {
+ return 6;
+ } else {
+ throw new Error('EBML VINT size not supported ' + value);
+ }
+};
\ No newline at end of file
diff --git a/src/matroska/matroska_muxer.ts b/src/matroska/matroska_muxer.ts
new file mode 100644
index 0000000..e477fa1
--- /dev/null
+++ b/src/matroska/matroska_muxer.ts
@@ -0,0 +1,915 @@
+import { assert, readBits, toUint8Array, writeBits } from '../misc';
+import { Muxer } from '../muxer';
+import { Output, OutputAudioTrack, OutputTrack, OutputVideoTrack } from '../output';
+import { MkvOutputFormat, WebMOutputFormat } from '../output_format';
+import { AudioCodec, VideoCodec } from '../source';
+import { Writer } from '../writer';
+import { EBML, EBMLElement, EBMLFloat32, EBMLFloat64, EBMLId, measureEBMLVarInt, measureUnsignedInt } from './ebml';
+
+const VIDEO_TRACK_TYPE = 1;
+const AUDIO_TRACK_TYPE = 2;
+const MAX_CHUNK_LENGTH_MS = 2**15;
+const APP_NAME = 'https://github.com/Vanilagy/webm-muxer'; // TODO
+const SEGMENT_SIZE_BYTES = 6;
+const CLUSTER_SIZE_BYTES = 5;
+
+type InternalMediaChunk = {
+ data: Uint8Array,
+ type: 'key' | 'delta',
+ timestamp: number,
+ duration: number | null,
+ additions: Uint8Array | null,
+};
+
+type SeekHead = {
+ id: number,
+ data: {
+ id: number,
+ data: ({
+ id: number,
+ data: Uint8Array,
+ size?: undefined
+ } | {
+ id: number,
+ size: number,
+ data: number
+ })[]
+ }[]
+};
+
+type MatroskaTrackData = {
+ chunkQueue: InternalMediaChunk[],
+
+ firstTimestamp: number | null,
+ lastTimestamp: number | null,
+ lastWrittenTimestamp: number | null
+} & ({
+ track: OutputVideoTrack,
+ type: 'video',
+ info: {
+ width: number,
+ height: number,
+ decoderConfig: VideoDecoderConfig
+ }
+} | {
+ track: OutputAudioTrack,
+ type: 'audio',
+ info: {
+ numberOfChannels: number,
+ sampleRate: number,
+ decoderConfig: AudioDecoderConfig
+ }
+});
+
+type MatroskaVideoTrackData = MatroskaTrackData & { type: 'video' };
+type MatroskaAudioTrackData = MatroskaTrackData & { type: 'audio' };
+
+const CODEC_STRING_MAP: Record = {
+ 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',
+};
+
+// TODO: Unify the timestamps in this. Some timestamps are in us, some are in ms, yuck.
+// TODO: Perhaps we can make this muxer always be streamable. We can do it similar to the MP4 muxer, where for each
+// cluster, we hold onto all of the chunks (called sample there), until it's done, and then we write it out in one go.
+// This way, we can set proper headers. Will just mean a bit more memory usage.
+// Update: Not really. There are duration fields and seek fields that are just uneditable if streaming is required.
+
+export class MatroskaMuxer extends Muxer {
+ #writer: Writer;
+ #format: WebMOutputFormat | MkvOutputFormat;
+
+ #helper = new Uint8Array(8);
+ #helperView = new DataView(this.#helper.buffer);
+
+ /**
+ * Stores the position from the start of the file to where EBML 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();
+ /** Same as offsets, but stores position where the element's data starts (after ID and size fields). */
+ dataOffsets = new WeakMap();
+
+ #trackDatas: MatroskaTrackData[] = [];
+
+ #segment: EBMLElement | null = null;
+ #segmentInfo: EBMLElement | null = null;
+ #seekHead: SeekHead | null = null;
+ #tracksElement: EBMLElement | null = null;
+ #segmentDuration: EBMLElement | null = null;
+ #cues: EBMLElement | null = null;
+
+ #currentCluster: EBMLElement | null = null;
+ #currentClusterTimestamp: number | null = null;
+ #trackDatasInCurrentCluster = new Set();
+
+ #duration = 0;
+
+ constructor(output: Output, format: MkvOutputFormat) {
+ super(output);
+
+ this.#writer = output.writer;
+ this.#format = format;
+ }
+
+ #writeByte(value: number) {
+ this.#helperView.setUint8(0, value);
+ this.#writer.write(this.#helper.subarray(0, 1));
+ }
+
+ #writeFloat32(value: number) {
+ this.#helperView.setFloat32(0, value, false);
+ this.#writer.write(this.#helper.subarray(0, 4));
+ }
+
+ #writeFloat64(value: number) {
+ this.#helperView.setFloat64(0, value, false);
+ this.#writer.write(this.#helper);
+ }
+
+ #writeUnsignedInt(value: number, width: number = measureUnsignedInt(value)) {
+ let pos = 0;
+
+ // Each case falls through:
+ switch (width) {
+ case 6:
+ // Need to use division to access >32 bits of floating point var
+ this.#helperView.setUint8(pos++, (value / 2**40) | 0);
+ case 5:
+ this.#helperView.setUint8(pos++, (value / 2**32) | 0);
+ case 4:
+ this.#helperView.setUint8(pos++, value >> 24);
+ case 3:
+ this.#helperView.setUint8(pos++, value >> 16);
+ case 2:
+ this.#helperView.setUint8(pos++, value >> 8);
+ case 1:
+ this.#helperView.setUint8(pos++, value);
+ break;
+ default:
+ throw new Error('Bad UINT size ' + width);
+ }
+
+ this.#writer.write(this.#helper.subarray(0, pos));
+ }
+
+ writeEBMLVarInt(value: number, width: number = measureEBMLVarInt(value)) {
+ let pos = 0;
+
+ switch (width) {
+ case 1:
+ this.#helperView.setUint8(pos++, (1 << 7) | value);
+ break;
+ case 2:
+ this.#helperView.setUint8(pos++, (1 << 6) | (value >> 8));
+ this.#helperView.setUint8(pos++, value);
+ break;
+ case 3:
+ this.#helperView.setUint8(pos++, (1 << 5) | (value >> 16));
+ this.#helperView.setUint8(pos++, value >> 8);
+ this.#helperView.setUint8(pos++, value);
+ break;
+ case 4:
+ this.#helperView.setUint8(pos++, (1 << 4) | (value >> 24));
+ this.#helperView.setUint8(pos++, value >> 16);
+ this.#helperView.setUint8(pos++, value >> 8);
+ this.#helperView.setUint8(pos++, value);
+ break;
+ case 5:
+ /**
+ * JavaScript converts its doubles to 32-bit integers for bitwise
+ * operations, so we need to do a division by 2^32 instead of a
+ * right-shift of 32 to retain those top 3 bits
+ */
+ this.#helperView.setUint8(pos++, (1 << 3) | ((value / 2**32) & 0x7));
+ this.#helperView.setUint8(pos++, value >> 24);
+ this.#helperView.setUint8(pos++, value >> 16);
+ this.#helperView.setUint8(pos++, value >> 8);
+ this.#helperView.setUint8(pos++, value);
+ break;
+ case 6:
+ this.#helperView.setUint8(pos++, (1 << 2) | ((value / 2**40) & 0x3));
+ this.#helperView.setUint8(pos++, (value / 2**32) | 0);
+ this.#helperView.setUint8(pos++, value >> 24);
+ this.#helperView.setUint8(pos++, value >> 16);
+ this.#helperView.setUint8(pos++, value >> 8);
+ this.#helperView.setUint8(pos++, value);
+ break;
+ default:
+ throw new Error('Bad EBML VINT size ' + width);
+ }
+
+ this.#writer.write(this.#helper.subarray(0, pos));
+ }
+
+ // Assumes the string is ASCII
+ #writeString(str: string) {
+ this.#writer.write(new Uint8Array(str.split('').map(x => x.charCodeAt(0))));
+ }
+
+ writeEBML(data: EBML | null) {
+ if (data === null) return;
+
+ if (data instanceof Uint8Array) {
+ this.#writer.write(data);
+ } else if (Array.isArray(data)) {
+ for (let elem of data) {
+ this.writeEBML(elem);
+ }
+ } else {
+ this.offsets.set(data, this.#writer.getPos());
+
+ this.#writeUnsignedInt(data.id); // ID field
+
+ if (Array.isArray(data.data)) {
+ let sizePos = this.#writer.getPos();
+ let sizeSize = data.size === -1 ? 1 : (data.size ?? 4);
+
+ if (data.size === -1) {
+ // Write the reserved all-one-bits marker for unknown/unbounded size.
+ this.#writeByte(0xff);
+ } else {
+ this.#writer.seek(this.#writer.getPos() + sizeSize);
+ }
+
+ let startPos = this.#writer.getPos();
+ this.dataOffsets.set(data, startPos);
+ this.writeEBML(data.data);
+
+ if (data.size !== -1) {
+ let size = this.#writer.getPos() - startPos;
+ let endPos = this.#writer.getPos();
+ this.#writer.seek(sizePos);
+ this.writeEBMLVarInt(size, sizeSize);
+ this.#writer.seek(endPos);
+ }
+ } else if (typeof data.data === 'number') {
+ let size = data.size ?? measureUnsignedInt(data.data);
+ this.writeEBMLVarInt(size);
+ this.#writeUnsignedInt(data.data, size);
+ } else if (typeof data.data === 'string') {
+ this.writeEBMLVarInt(data.data.length);
+ this.#writeString(data.data);
+ } else if (data.data instanceof Uint8Array) {
+ this.writeEBMLVarInt(data.data.byteLength, data.size);
+ this.#writer.write(data.data);
+ } else if (data.data instanceof EBMLFloat32) {
+ this.writeEBMLVarInt(4);
+ this.#writeFloat32(data.data.value);
+ } else if (data.data instanceof EBMLFloat64) {
+ this.writeEBMLVarInt(8);
+ this.#writeFloat64(data.data.value);
+ }
+ }
+ }
+
+ override beforeTrackAdd(track: OutputTrack) {
+ if (!(this.#format instanceof WebMOutputFormat)) {
+ return;
+ }
+
+ if (track.type === 'video') {
+ if (!['vp8', 'vp9', 'av1'].includes(track.source.codec)) {
+ throw new Error(`WebM only supports VP8, VP9 and AV1 as video codecs. Switching to MKV removes this restriction.`);
+ }
+ } else {
+ if (!['opus', 'vorbis'].includes(track.source.codec)) {
+ throw new Error(`WebM only supports Opus and Vorbis as audio codecs. Switching to MKV removes this restriction.`);
+ }
+ }
+ }
+
+ start() {
+ this.#writeEBMLHeader();
+
+ if (!this.#format.options.streaming) {
+ this.#createSeekHead();
+ }
+
+ this.#createSegmentInfo();
+ this.#createCues();
+
+ this.#writer.flush();
+ }
+
+ #writeEBMLHeader() {
+ let ebmlHeader: EBML = { id: EBMLId.EBML, data: [
+ { id: EBMLId.EBMLVersion, data: 1 },
+ { id: EBMLId.EBMLReadVersion, data: 1 },
+ { id: EBMLId.EBMLMaxIDLength, data: 4 },
+ { id: EBMLId.EBMLMaxSizeLength, data: 8 },
+ { id: EBMLId.DocType, data: this.#format instanceof WebMOutputFormat ? 'webm' : 'matroska' },
+ { id: EBMLId.DocTypeVersion, data: 2 },
+ { id: EBMLId.DocTypeReadVersion, data: 2 }
+ ] };
+ this.writeEBML(ebmlHeader);
+ }
+
+ /**
+ * Creates a SeekHead element which is positioned near the start of the file and allows the media player to seek to
+ * relevant sections more easily. Since we don't know the positions of those sections yet, we'll set them later.
+ */
+ #createSeekHead() {
+ const kaxCues = new Uint8Array([ 0x1c, 0x53, 0xbb, 0x6b ]);
+ const kaxInfo = new Uint8Array([ 0x15, 0x49, 0xa9, 0x66 ]);
+ const kaxTracks = new Uint8Array([ 0x16, 0x54, 0xae, 0x6b ]);
+
+ let seekHead = { id: EBMLId.SeekHead, data: [
+ { id: EBMLId.Seek, data: [
+ { id: EBMLId.SeekID, data: kaxCues },
+ { id: EBMLId.SeekPosition, size: 5, data: 0 }
+ ] },
+ { id: EBMLId.Seek, data: [
+ { id: EBMLId.SeekID, data: kaxInfo },
+ { id: EBMLId.SeekPosition, size: 5, data: 0 }
+ ] },
+ { id: EBMLId.Seek, data: [
+ { id: EBMLId.SeekID, data: kaxTracks },
+ { id: EBMLId.SeekPosition, size: 5, data: 0 }
+ ] }
+ ] };
+ this.#seekHead = seekHead;
+ }
+
+ #createSegmentInfo() {
+ let segmentDuration: EBML = { id: EBMLId.Duration, data: new EBMLFloat64(0) };
+ this.#segmentDuration = segmentDuration;
+
+ let segmentInfo: EBML = { id: EBMLId.Info, data: [
+ { id: EBMLId.TimestampScale, data: 1e6 },
+ { id: EBMLId.MuxingApp, data: APP_NAME },
+ { id: EBMLId.WritingApp, data: APP_NAME },
+ !this.#format.options.streaming ? segmentDuration : null
+ ] };
+ this.#segmentInfo = segmentInfo;
+ }
+
+ #createTracks() {
+ let tracksElement = { id: EBMLId.Tracks, data: [] as EBML[] };
+ this.#tracksElement = tracksElement;
+
+ for (let trackData of this.#trackDatas) {
+ tracksElement.data.push({ id: EBMLId.TrackEntry, data: [
+ { id: EBMLId.TrackNumber, data: trackData.track.id },
+ { id: EBMLId.TrackUID, data: trackData.track.id },
+ { id: EBMLId.TrackType, data: trackData.type === 'video' ? VIDEO_TRACK_TYPE : AUDIO_TRACK_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.track.source.metadata.frameRate ? { id: EBMLId.DefaultDuration, data: 1e9 / trackData.track.source.metadata.frameRate } : null),
+ { id: EBMLId.Video, data: [
+ { id: EBMLId.PixelWidth, data: trackData.info.width },
+ { id: EBMLId.PixelHeight, data: trackData.info.height },
+ (() => {
+ if (trackData.info.decoderConfig.colorSpace) {
+ let colorSpace = trackData.info.decoderConfig.colorSpace;
+ if (!colorSpace.matrix || !colorSpace.transfer || !colorSpace.primaries || colorSpace.fullRange == null) {
+ return null;
+ }
+
+ return {id: EBMLId.Colour, data: [
+ { id: EBMLId.MatrixCoefficients, data: {
+ 'rgb': 1,
+ 'bt709': 1,
+ 'bt470bg': 5,
+ 'smpte170m': 6
+ }[colorSpace.matrix] },
+ { id: EBMLId.TransferCharacteristics, data: {
+ 'bt709': 1,
+ 'smpte170m': 6,
+ 'iec61966-2-1': 13
+ }[colorSpace.transfer] },
+ { id: EBMLId.Primaries, data: {
+ 'bt709': 1,
+ 'bt470bg': 5,
+ 'smpte170m': 6
+ }[colorSpace.primaries] },
+ { id: EBMLId.Range, data: [1, 2][Number(colorSpace.fullRange)]! }
+ ] };
+ }
+
+ return null;
+ })()
+ ] }
+ ] : []),
+ ...(trackData.type === 'audio' ? [
+ { 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
+ ] }
+ ] : [])
+ ] })
+ }
+
+ /*
+ if (this.#options.subtitles) {
+ tracksElement.data.push({ id: EBMLId.TrackEntry, data: [
+ { id: EBMLId.TrackNumber, data: SUBTITLE_TRACK_NUMBER },
+ { id: EBMLId.TrackUID, data: SUBTITLE_TRACK_NUMBER },
+ { id: EBMLId.TrackType, data: SUBTITLE_TRACK_TYPE },
+ { id: EBMLId.CodecID, data: this.#options.subtitles.codec },
+ this.#subtitleCodecPrivate
+ ] });
+ }
+ */
+ }
+
+ #createSegment() {
+ let segment: EBML = {
+ id: EBMLId.Segment,
+ size: this.#format.options.streaming ? -1 : SEGMENT_SIZE_BYTES,
+ data: [
+ !this.#format.options.streaming ? this.#seekHead as EBML : null,
+ this.#segmentInfo,
+ this.#tracksElement
+ ]
+ };
+ this.#segment = segment;
+
+ this.writeEBML(segment);
+
+ /*
+ if (this.#writer instanceof BaseStreamTargetWriter && this.#writer.target.options.onHeader) {
+ let { data, start } = this.#writer.getTrackedWrites(); // start should be 0
+ this.#writer.target.options.onHeader(data, start);
+ }
+ */
+ }
+
+ #createCues() {
+ this.#cues = { id: EBMLId.Cues, data: [] };
+ }
+
+ get #segmentDataOffset() {
+ assert(this.#segment);
+ return this.dataOffsets.get(this.#segment)!;
+ }
+
+ #getVideoTrackData(track: OutputVideoTrack, chunk: EncodedVideoChunk, meta?: EncodedVideoChunkMetadata) {
+ const existingTrackData = this.#trackDatas.find(x => x.track === track);
+ if (existingTrackData) {
+ return existingTrackData as MatroskaVideoTrackData;
+ }
+
+ // TODO Make proper errors for these
+ assert(meta);
+ assert(meta.decoderConfig);
+ assert(meta.decoderConfig.codedWidth !== undefined);
+ assert(meta.decoderConfig.codedHeight !== undefined);
+
+ const newTrackData: MatroskaVideoTrackData = {
+ track,
+ type: 'video',
+ info: {
+ width: meta.decoderConfig.codedWidth,
+ height: meta.decoderConfig.codedHeight,
+ decoderConfig: meta.decoderConfig
+ },
+ chunkQueue: [],
+ firstTimestamp: null,
+ lastTimestamp: null,
+ lastWrittenTimestamp: null
+ };
+
+ this.#trackDatas.push(newTrackData);
+ this.#trackDatas.sort((a, b) => a.track.id - b.track.id);
+
+ return newTrackData;
+ }
+
+ #getAudioTrackData(track: OutputAudioTrack, chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata) {
+ const existingTrackData = this.#trackDatas.find(x => x.track === track);
+ if (existingTrackData) {
+ return existingTrackData as MatroskaAudioTrackData;
+ }
+
+ // TODO Make proper errors for these
+ assert(meta);
+ assert(meta.decoderConfig);
+
+ const newTrackData: MatroskaAudioTrackData = {
+ track,
+ type: 'audio',
+ info: {
+ numberOfChannels: meta.decoderConfig.numberOfChannels,
+ sampleRate: meta.decoderConfig.sampleRate,
+ decoderConfig: meta.decoderConfig
+ },
+ chunkQueue: [],
+ firstTimestamp: null,
+ lastTimestamp: null,
+ lastWrittenTimestamp: null
+ };
+
+ this.#trackDatas.push(newTrackData);
+ this.#trackDatas.sort((a, b) => a.track.id - b.track.id);
+
+ return newTrackData;
+ }
+
+ addEncodedVideoChunk(track: OutputVideoTrack, chunk: EncodedVideoChunk, meta?: EncodedVideoChunkMetadata, compositionTimeOffset?: number) {
+ const trackData = this.#getVideoTrackData(track, chunk, meta);
+
+ let videoChunk = this.#createInternalChunk(trackData, chunk);
+ if (track.source.codec === 'vp9') this.#fixVP9ColorSpace(trackData, videoChunk);
+
+ trackData.lastTimestamp = videoChunk.timestamp;
+
+ trackData.chunkQueue.push(videoChunk);
+ this.#interleaveChunks();
+
+ //this.#writeSubtitleChunks();
+ this.#writer.flush();
+ }
+
+ addEncodedAudioChunk(track: OutputAudioTrack, chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata) {
+ const trackData = this.#getAudioTrackData(track, chunk, meta);
+
+ let audioChunk = this.#createInternalChunk(trackData, chunk);
+ trackData.lastTimestamp = audioChunk.timestamp;
+
+ trackData.chunkQueue.push(audioChunk);
+ this.#interleaveChunks();
+
+ //this.#writeSubtitleChunks();
+ this.#writer.flush();
+ }
+
+ #interleaveChunks() {
+ if (this.#trackDatas.length < this.output.tracks.length) {
+ return; // We haven't seen a sample from each track yet
+ }
+
+ outer:
+ while (true) {
+ let trackWithMinTimestamp: MatroskaTrackData | null = null;
+ let minTimestamp = Infinity;
+
+ for (let trackData of this.#trackDatas) {
+ if (trackData.chunkQueue.length === 0) {
+ break outer;
+ }
+
+ if (trackData.chunkQueue[0]!.timestamp < minTimestamp) {
+ trackWithMinTimestamp = trackData;
+ minTimestamp = trackData.chunkQueue[0]!.timestamp;
+ }
+ }
+
+ if (!trackWithMinTimestamp) {
+ break;
+ }
+
+ let chunk = trackWithMinTimestamp.chunkQueue.shift()!;
+ this.#writeBlock(trackWithMinTimestamp, chunk);
+ }
+ }
+
+ /** Due to [a bug in Chromium](https://bugs.chromium.org/p/chromium/issues/detail?id=1377842), VP9 streams often
+ * lack color space information. This method patches in that information. */
+ // http://downloads.webmproject.org/docs/vp9/vp9-bitstream_superframe-and-uncompressed-header_v1.0.pdf
+ #fixVP9ColorSpace(trackData: MatroskaVideoTrackData, chunk: InternalMediaChunk) {
+ if (chunk.type !== 'key') return;
+ if (!trackData.info.decoderConfig.colorSpace || !trackData.info.decoderConfig.colorSpace.matrix) return;
+
+ let i = 0;
+ // Check if it's a "superframe"
+ if (readBits(chunk.data, 0, 2) !== 0b10) return; i += 2;
+
+ let profile = (readBits(chunk.data, i+1, i+2) << 1) + readBits(chunk.data, i+0, i+1); i += 2;
+ if (profile === 3) i++;
+
+ let showExistingFrame = readBits(chunk.data, i+0, i+1); i++;
+ if (showExistingFrame) return;
+
+ let frameType = readBits(chunk.data, i+0, i+1); i++;
+ if (frameType !== 0) return; // Just to be sure
+
+ i += 2;
+
+ let syncCode = readBits(chunk.data, i+0, i+24); i += 24;
+ if (syncCode !== 0x498342) return;
+
+ if (profile >= 2) i++;
+
+ let colorSpaceID = {
+ 'rgb': 7,
+ 'bt709': 2,
+ 'bt470bg': 1,
+ 'smpte170m': 3
+ }[trackData.info.decoderConfig.colorSpace.matrix];
+ writeBits(chunk.data, i+0, i+3, colorSpaceID);
+ }
+
+ /*
+ addSubtitleChunk(chunk: EncodedSubtitleChunk, meta: EncodedSubtitleChunkMetadata, timestamp?: number) {
+ if (typeof chunk !== 'object' || !chunk) {
+ throw new TypeError("addSubtitleChunk's first argument (chunk) must be an object.");
+ } else {
+ // We can't simply do an instanceof check, so let's check the structure itself:
+ if (!(chunk.body instanceof Uint8Array)) {
+ throw new TypeError('body must be an instance of Uint8Array.');
+ }
+ if (!Number.isFinite(chunk.timestamp) || chunk.timestamp < 0) {
+ throw new TypeError('timestamp must be a non-negative real number.');
+ }
+ if (!Number.isFinite(chunk.duration) || chunk.duration < 0) {
+ throw new TypeError('duration must be a non-negative real number.');
+ }
+ if (chunk.additions && !(chunk.additions instanceof Uint8Array)) {
+ throw new TypeError('additions, when present, must be an instance of Uint8Array.');
+ }
+ }
+
+ if (typeof meta !== 'object') {
+ throw new TypeError("addSubtitleChunk's second argument (meta) must be an object.");
+ }
+
+ this.#ensureNotFinalized();
+ if (!this.#options.subtitles) throw new Error('No subtitle track declared.');
+
+ // Write possible subtitle decoder metadata to the file
+ if (meta?.decoderConfig) {
+ if (this.#options.streaming) {
+ this.#subtitleCodecPrivate = this.#createCodecPrivateElement(meta.decoderConfig.description);
+ } else {
+ this.#writeCodecPrivate(this.#subtitleCodecPrivate, meta.decoderConfig.description);
+ }
+ }
+
+ let subtitleChunk = this.#createInternalChunk(
+ chunk.body,
+ 'key',
+ timestamp ?? chunk.timestamp,
+ SUBTITLE_TRACK_NUMBER,
+ chunk.duration,
+ chunk.additions
+ );
+
+ this.#lastSubtitleTimestamp = subtitleChunk.timestamp;
+ this.#subtitleChunkQueue.push(subtitleChunk);
+
+ this.#writeSubtitleChunks();
+ this.#maybeFlushStreamingTargetWriter();
+ }
+
+ #writeSubtitleChunks() {
+ // Writing subtitle chunks is different from video and audio: A subtitle chunk will be written if it's
+ // guaranteed that no more media chunks will be written before it, to ensure monotonicity. However, media chunks
+ // will NOT wait for subtitle chunks to arrive, as they may never arrive, so that's how non-monotonicity can
+ // arrive. But it should be fine, since it's all still in one cluster.
+
+ let lastWrittenMediaTimestamp = Math.min(
+ this.#options.video ? this.#lastVideoTimestamp : Infinity,
+ this.#options.audio ? this.#lastAudioTimestamp : Infinity
+ );
+
+ let queue = this.#subtitleChunkQueue;
+ while (queue.length > 0 && queue[0].timestamp <= lastWrittenMediaTimestamp) {
+ this.#writeBlock(queue.shift(), !this.#options.video && !this.#options.audio);
+ }
+ }
+ */
+
+ /** Converts a read-only external chunk into an internal one for easier use. */
+ #createInternalChunk(
+ trackData: MatroskaTrackData,
+ chunk: EncodedVideoChunk | EncodedAudioChunk
+ ) {
+ let adjustedTimestamp = this.#validateTimestamp(trackData, chunk.timestamp);
+
+ let data = new Uint8Array(chunk.byteLength);
+ chunk.copyTo(data);
+
+ let internalChunk: InternalMediaChunk = {
+ data,
+ type: chunk.type,
+ timestamp: adjustedTimestamp,
+ duration: chunk.duration,
+ additions: null
+ };
+
+ return internalChunk;
+ }
+
+ #validateTimestamp(trackData: MatroskaTrackData, timestamp: number) {
+ if (timestamp < 0) {
+ throw new Error(`Timestamps must be non-negative (got ${timestamp}s).`);
+ }
+
+ if (trackData.firstTimestamp === null) {
+ trackData.firstTimestamp = timestamp;
+ }
+
+ timestamp -= trackData.firstTimestamp;
+
+ if (trackData.lastTimestamp !== null && timestamp < trackData.lastTimestamp) {
+ throw new Error(
+ `Timestamps must be monotonically increasing ` +
+ `(timestamp went from ${trackData.lastTimestamp}s to ${timestamp}s).`
+ );
+ }
+
+ return timestamp;
+ }
+
+ /** Writes a block containing media data to the file. */
+ #writeBlock(trackData: MatroskaTrackData, chunk: InternalMediaChunk) {
+ // TODO Update this comment. This code always runs now
+ // When streaming, we create the tracks and segment after we've received the first media chunks.
+ // Due to the interlacing algorithm, this code will be run once we've seen one chunk from every media track.
+ if (!this.#segment) {
+ this.#createTracks();
+ this.#createSegment();
+ }
+
+ let msTimestamp = Math.floor(chunk.timestamp / 1000);
+ // 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 (trackData === otherTrackData) {
+ return chunk.type === 'key';
+ }
+
+ const firstQueuedSample = otherTrackData.chunkQueue[0];
+ return firstQueuedSample && firstQueuedSample.type === 'key';
+ });
+
+ if (
+ !this.#currentCluster ||
+ (keyFrameQueuedEverywhere && msTimestamp - this.#currentClusterTimestamp! >= 1000)
+ ) {
+ this.#createNewCluster(msTimestamp);
+ }
+
+ let relativeTimestamp = msTimestamp - this.#currentClusterTimestamp!;
+ if (relativeTimestamp < 0) {
+ // The chunk lies outside of the current cluster
+ return;
+ }
+
+ let clusterIsTooLong = relativeTimestamp >= MAX_CHUNK_LENGTH_MS;
+ if (clusterIsTooLong) {
+ throw new Error(
+ `Current Matroska cluster exceeded its maximum allowed length of ${MAX_CHUNK_LENGTH_MS} ` +
+ `milliseconds. In order to produce a correct WebM file, you must pass in a key frame at least every ` +
+ `${MAX_CHUNK_LENGTH_MS} milliseconds.`
+ );
+ }
+
+ let prelude = new Uint8Array(4);
+ let view = new DataView(prelude.buffer);
+ // 0x80 to indicate it's the last byte of a multi-byte number
+ view.setUint8(0, 0x80 | trackData.track.id);
+ view.setInt16(1, relativeTimestamp, false);
+
+ let msDuration = Math.floor((chunk.duration ?? 0) / 1000);
+
+ if (msDuration === 0 && !chunk.additions) {
+ // No duration or additions, we can write out a SimpleBlock
+ view.setUint8(3, Number(chunk.type === 'key') << 7); // Flags (keyframe flag only present for SimpleBlock)
+
+ let simpleBlock = { id: EBMLId.SimpleBlock, data: [
+ prelude,
+ chunk.data
+ ] };
+ this.writeEBML(simpleBlock);
+ } else {
+ let blockGroup = { id: EBMLId.BlockGroup, data: [
+ { id: EBMLId.Block, data: [
+ prelude,
+ chunk.data
+ ] },
+ chunk.type === 'delta' ? { id: EBMLId.ReferenceBlock, data: trackData.lastWrittenTimestamp! - msTimestamp } : null,
+ chunk.duration !== null ? { id: EBMLId.BlockDuration, data: msDuration } : null,
+ chunk.additions ? { id: EBMLId.BlockAdditions, data: chunk.additions } : null
+ ] };
+ this.writeEBML(blockGroup);
+ }
+
+ this.#duration = Math.max(this.#duration, msTimestamp + msDuration);
+ trackData.lastWrittenTimestamp = msTimestamp;
+
+ this.#trackDatasInCurrentCluster.add(trackData);
+ }
+
+ /** Creates a new Cluster element to contain media chunks. */
+ #createNewCluster(timestamp: number) {
+ if (this.#currentCluster && !this.#format.options.streaming) {
+ this.#finalizeCurrentCluster();
+ }
+
+ /*
+ if (this.#writer instanceof BaseStreamTargetWriter && this.#writer.target.options.onCluster) {
+ this.#writer.startTrackingWrites();
+ }
+ */
+
+ this.#currentCluster = {
+ id: EBMLId.Cluster,
+ size: this.#format.options.streaming ? -1 : CLUSTER_SIZE_BYTES,
+ data: [
+ { id: EBMLId.Timestamp, data: timestamp }
+ ]
+ };
+ this.writeEBML(this.#currentCluster);
+
+ this.#currentClusterTimestamp = timestamp;
+ this.#trackDatasInCurrentCluster.clear();
+ }
+
+ #finalizeCurrentCluster() {
+ assert(this.#currentCluster);
+ let clusterSize = this.#writer.getPos() - this.dataOffsets.get(this.#currentCluster)!;
+ let endPos = this.#writer.getPos();
+
+ // Write the size now that we know it
+ this.#writer.seek(this.offsets.get(this.#currentCluster)! + 4);
+ this.writeEBMLVarInt(clusterSize, CLUSTER_SIZE_BYTES);
+ this.#writer.seek(endPos);
+
+ /*
+ if (this.#writer instanceof BaseStreamTargetWriter && this.#writer.target.options.onCluster) {
+ let { data, start } = this.#writer.getTrackedWrites();
+ this.#writer.target.options.onCluster(data, start, this.#currentClusterTimestamp);
+ }
+ */
+
+ let clusterOffsetFromSegment =
+ this.offsets.get(this.#currentCluster)! - this.#segmentDataOffset;
+
+ assert(this.#cues);
+
+ // Add a CuePoint to the Cues element for better seeking
+ // TODO: Should this include subtitle tracks?
+ (this.#cues.data as EBML[]).push({ id: EBMLId.CuePoint, data: [
+ { id: EBMLId.CueTime, data: this.#currentClusterTimestamp! },
+ // We only write out cues for tracks that have at least one chunk in this cluster
+ ...[...this.#trackDatasInCurrentCluster].map(trackData => {
+ return { id: EBMLId.CueTrackPositions, data: [
+ { id: EBMLId.CueTrack, data: trackData.track.id },
+ { id: EBMLId.CueClusterPosition, data: clusterOffsetFromSegment }
+ ] };
+ })
+ ] });
+ }
+
+ /** Finalizes the file, making it ready for use. Must be called after all media chunks have been added. */
+ finalize() {
+ // Flush any remaining queued chunks to the file
+ for (let trackData of this.#trackDatas) {
+ while (trackData.chunkQueue.length > 0) {
+ this.#writeBlock(trackData, trackData.chunkQueue.shift()!);
+ }
+ }
+
+ if (!this.#format.options.streaming) {
+ this.#finalizeCurrentCluster();
+ }
+
+ /*
+ while (this.#videoChunkQueue.length > 0) this.#writeBlock(this.#videoChunkQueue.shift(), true);
+ while (this.#audioChunkQueue.length > 0) this.#writeBlock(this.#audioChunkQueue.shift(), true);
+ while (this.#subtitleChunkQueue.length > 0 && this.#subtitleChunkQueue[0].timestamp <= this.#duration) {
+ this.#writeBlock(this.#subtitleChunkQueue.shift(), false);
+ }
+ */
+
+ assert(this.#cues);
+ this.writeEBML(this.#cues);
+
+ if (!this.#format.options.streaming) {
+ let endPos = this.#writer.getPos();
+
+ // Write the Segment size
+ let segmentSize = this.#writer.getPos() - this.#segmentDataOffset;
+ this.#writer.seek(this.offsets.get(this.#segment!)! + 4);
+ this.writeEBMLVarInt(segmentSize, SEGMENT_SIZE_BYTES);
+
+ // Write the duration of the media to the Segment
+ this.#segmentDuration!.data = new EBMLFloat64(this.#duration);
+ this.#writer.seek(this.offsets.get(this.#segmentDuration!)!);
+ this.writeEBML(this.#segmentDuration!);
+
+ // Fill in SeekHead position data and write it again
+ this.#seekHead!.data[0]!.data[1]!.data =
+ this.offsets.get(this.#cues)! - this.#segmentDataOffset;
+ this.#seekHead!.data[1]!.data[1]!.data =
+ this.offsets.get(this.#segmentInfo!)! - this.#segmentDataOffset;
+ this.#seekHead!.data[2]!.data[1]!.data =
+ this.offsets.get(this.#tracksElement!)! - this.#segmentDataOffset;
+
+ this.#writer.seek(this.offsets.get(this.#seekHead!)!);
+ this.writeEBML(this.#seekHead!);
+
+ this.#writer.seek(endPos);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/misc.ts b/src/misc.ts
index 10ae52e..98ca7f7 100644
--- a/src/misc.ts
+++ b/src/misc.ts
@@ -13,3 +13,39 @@ export const last = (arr: T[]) => {
export const isU32 = (value: number) => {
return value >= 0 && value < 2**32;
};
+
+export const readBits = (bytes: Uint8Array, start: number, end: number) => {
+ let result = 0;
+
+ for (let i = start; i < end; i++) {
+ let byteIndex = Math.floor(i / 8);
+ let byte = bytes[byteIndex]!;
+ let bitIndex = 0b111 - (i & 0b111);
+ let bit = (byte & (1 << bitIndex)) >> bitIndex;
+
+ result <<= 1;
+ result |= bit;
+ }
+
+ return result;
+};
+
+export const writeBits = (bytes: Uint8Array, start: number, end: number, value: number) => {
+ for (let i = start; i < end; i++) {
+ let byteIndex = Math.floor(i / 8);
+ let byte = bytes[byteIndex]!;
+ let bitIndex = 0b111 - (i & 0b111);
+
+ byte &= ~(1 << bitIndex);
+ byte |= ((value & (1 << (end - i - 1))) >> (end - i - 1)) << bitIndex;
+ bytes[byteIndex] = byte;
+ }
+};
+
+export const toUint8Array = (source: AllowSharedBufferSource): Uint8Array => {
+ if (source instanceof ArrayBuffer) {
+ return new Uint8Array(source);
+ } else {
+ return new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
+ }
+};
\ No newline at end of file
diff --git a/src/muxer.ts b/src/muxer.ts
index 936b839..05c021b 100644
--- a/src/muxer.ts
+++ b/src/muxer.ts
@@ -11,4 +11,6 @@ export abstract class Muxer {
abstract addEncodedVideoChunk(track: OutputVideoTrack, chunk: EncodedVideoChunk, meta?: EncodedVideoChunkMetadata, compositionTimeOffset?: number): void;
abstract addEncodedAudioChunk(track: OutputAudioTrack, chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata): void;
abstract finalize(): void;
+
+ beforeTrackAdd(track: OutputTrack) {}
}
\ No newline at end of file
diff --git a/src/output.ts b/src/output.ts
index 2f57f3a..0ada5ba 100644
--- a/src/output.ts
+++ b/src/output.ts
@@ -50,6 +50,8 @@ export class Output {
source
} as OutputTrack;
+ this.muxer.beforeTrackAdd(track);
+
this.tracks.push(track);
source.connectedTrack = track;
}
diff --git a/src/output_format.ts b/src/output_format.ts
index b7d45aa..c87326b 100644
--- a/src/output_format.ts
+++ b/src/output_format.ts
@@ -1,4 +1,5 @@
import { IsobmffMuxer } from "./isobmff/isobmff_muxer";
+import { MatroskaMuxer } from "./matroska/matroska_muxer";
import { Muxer } from "./muxer";
import { Output } from "./output";
@@ -20,3 +21,17 @@ export class Mp4OutputFormat extends OutputFormat {
return new IsobmffMuxer(output, this);
}
}
+
+export class MkvOutputFormat extends OutputFormat {
+ constructor(public options: {
+ streaming?: boolean // TODO: Is there a better name?
+ } = {}) {
+ super();
+ }
+
+ override createMuxer(output: Output) {
+ return new MatroskaMuxer(output, this);
+ }
+}
+
+export class WebMOutputFormat extends MkvOutputFormat {}
\ No newline at end of file