From a065a5ef1283b59b966a8218e64b533cc7aaf040 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Tue, 12 Nov 2024 23:24:43 +0100 Subject: [PATCH] Allow for out-of-presentation-order chunks, enabling P and B frames --- dev/index.html | 35 +++- dist/metamuxer.js | 332 ++++++++++++++++++++------------- dist/metamuxer.min.js | 2 +- dist/metamuxer.min.mjs | 2 +- dist/metamuxer.mjs | 332 ++++++++++++++++++++------------- src/index.ts | 2 +- src/isobmff/isobmff_boxes.ts | 12 +- src/isobmff/isobmff_muxer.ts | 221 ++++++++++++---------- src/matroska/ebml.ts | 28 ++- src/matroska/matroska_muxer.ts | 51 +++-- src/muxer.ts | 2 +- src/output.ts | 22 ++- src/source.ts | 68 +++---- 13 files changed, 682 insertions(+), 427 deletions(-) diff --git a/dev/index.html b/dev/index.html index 28fb0b6..c437536 100644 --- a/dev/index.html +++ b/dev/index.html @@ -36,17 +36,45 @@ canvas.height = 720; const context = canvas.getContext('2d'); - let format = new Metamuxer.WebMOutputFormat(); //new Metamuxer.Mp4OutputFormat({ fastStart: false }); + let format = new Metamuxer.MkvOutputFormat();// new Metamuxer.Mp4OutputFormat({ fastStart: false }); let target = new Metamuxer.ArrayBufferTarget(); let output = new Metamuxer.Output({ format, target }); + let videoSource = new Metamuxer.EncodedVideoChunkSource('avc'); + output.addTrack(videoSource); + + output.start(); + + const timestamps = [0, 0.5, 0.25, 0.75, 1, 1.9, 1.8, 1.7, 1.6, 1.5, 1.4, 1.3, 1.2, 1.1, 2]; + for (const timestamp of timestamps) { + const encodedVideoChunk = new EncodedVideoChunk({ + type: timestamp % 1 === 0 ? 'key' : 'delta', + timestamp: 1e6 * timestamp, + duration: 1e6 * 0.25, + data: new Uint8Array(1024) + }); + videoSource.digest(encodedVideoChunk, { + decoderConfig: { + codedWidth: 64, + codedHeight: 64, + description: new Uint8Array(64) + } + }); + } + + await output.finalize(); + + console.log(target); + download(new Blob([target.buffer]), 'test.mkv'); + + /* let videoSource = new Metamuxer.CanvasSource(canvas, { codec: 'avc', bitrate: 1e6 }); let audioSource = new Metamuxer.AudioBufferSource({ - codec: 'opus', + codec: 'aac', bitrate: 128e3, }); @@ -72,5 +100,6 @@ await output.finalize(); console.log(target); - download(new Blob([target.buffer]), 'test.webm'); + download(new Blob([target.buffer]), 'test.mp4'); + */ \ No newline at end of file diff --git a/dist/metamuxer.js b/dist/metamuxer.js index cb95cea..0e5aad6 100644 --- a/dist/metamuxer.js +++ b/dist/metamuxer.js @@ -25,6 +25,8 @@ var Metamuxer = (() => { AudioBufferSource: () => AudioBufferSource, AudioDataSource: () => AudioDataSource, CanvasSource: () => CanvasSource, + EncodedAudioChunkSource: () => EncodedAudioChunkSource, + EncodedVideoChunkSource: () => EncodedVideoChunkSource, FileSystemWritableFileStreamTarget: () => FileSystemWritableFileStreamTarget2, MediaStreamAudioTrackSource: () => MediaStreamAudioTrackSource, MediaStreamVideoTrackSource: () => MediaStreamVideoTrackSource, @@ -172,10 +174,30 @@ var Metamuxer = (() => { // src/source.ts var VideoSource = class { - constructor(codec, metadata) { + constructor(codec) { + this.connectedTrack = null; + this.codec = codec; + } + ensureValidDigest() { + if (!this.connectedTrack) { + throw new Error("Cannot call digest without connecting the source to an output track."); + } + if (!this.connectedTrack.output.started) { + throw new Error("Cannot call digest before output has been started."); + } + if (this.connectedTrack.output.finalizing) { + throw new Error("Cannot call digest after output has started finalizing."); + } + } + start() { + } + async flush() { + } + }; + var AudioSource = class { + constructor(codec) { this.connectedTrack = null; this.codec = codec; - this.metadata = metadata; } ensureNotFinalizing() { if (this.connectedTrack?.output.finalizing) { @@ -187,20 +209,14 @@ var Metamuxer = (() => { async flush() { } }; - var AudioSource = class { - constructor(codec, metadata) { - this.connectedTrack = null; - this.codec = codec; - this.metadata = metadata; + var EncodedVideoChunkSource = class extends VideoSource { + constructor(codec) { + super(codec); } - ensureNotFinalizing() { - if (this.connectedTrack?.output.finalizing) { - throw new Error("Cannot call digest after output has started finalizing."); - } - } - start() { - } - async flush() { + // TODO: Ensure that the first chunk is a key frame (same for the audio case) + digest(chunk, meta) { + this.ensureValidDigest(); + this.connectedTrack?.output.muxer.addEncodedVideoChunk(this.connectedTrack, chunk, meta); } }; var KEY_FRAME_INTERVAL = 5; @@ -211,8 +227,9 @@ var Metamuxer = (() => { this.encoder = null; this.lastMultipleOfKeyFrameInterval = -1; } + // TODO: Ensure video frame size remains constant digest(videoFrame) { - this.source.ensureNotFinalizing(); + this.source.ensureValidDigest(); this.ensureEncoder(videoFrame); assert(this.encoder); const multipleOfKeyFrameInterval = Math.floor(videoFrame.timestamp / 1e6 / KEY_FRAME_INTERVAL); @@ -240,8 +257,8 @@ var Metamuxer = (() => { } }; var VideoFrameSource = class extends VideoSource { - constructor(codecConfig, options = {}) { - super(codecConfig.codec, options); + constructor(codecConfig) { + super(codecConfig.codec); this.encoder = new VideoEncoderWrapper(this, codecConfig); } digest(videoFrame) { @@ -252,8 +269,8 @@ var Metamuxer = (() => { } }; var CanvasSource = class extends VideoSource { - constructor(canvas, codecConfig, options = {}) { - super(codecConfig.codec, options); + constructor(canvas, codecConfig) { + super(codecConfig.codec); this.canvas = canvas; this.encoder = new VideoEncoderWrapper(this, codecConfig); } @@ -270,8 +287,8 @@ var Metamuxer = (() => { } }; var MediaStreamVideoTrackSource = class extends VideoSource { - constructor(track, codecConfig, options = {}) { - super(codecConfig.codec, options); + constructor(track, codecConfig) { + super(codecConfig.codec); this.track = track; this.abortController = null; this.encoder = new VideoEncoderWrapper(this, codecConfig); @@ -300,12 +317,22 @@ var Metamuxer = (() => { await this.encoder.flush(); } }; + var EncodedAudioChunkSource = class extends AudioSource { + constructor(codec) { + super(codec); + } + digest(chunk, meta) { + this.ensureNotFinalizing(); + this.connectedTrack?.output.muxer.addEncodedAudioChunk(this.connectedTrack, chunk, meta); + } + }; var AudioEncoderWrapper = class { constructor(source, codecConfig) { this.source = source; this.codecConfig = codecConfig; this.encoder = null; } + // TODO: Ensure audio parameters remain constant digest(audioData) { this.source.ensureNotFinalizing(); this.ensureEncoder(audioData); @@ -333,8 +360,8 @@ var Metamuxer = (() => { } }; var AudioDataSource = class extends AudioSource { - constructor(codecConfig, options = {}) { - super(codecConfig.codec, options); + constructor(codecConfig) { + super(codecConfig.codec); this.encoder = new AudioEncoderWrapper(this, codecConfig); } digest(audioData) { @@ -345,8 +372,8 @@ var Metamuxer = (() => { } }; var AudioBufferSource = class extends AudioSource { - constructor(codecConfig, options = {}) { - super(codecConfig.codec, options); + constructor(codecConfig) { + super(codecConfig.codec); this.accumulatedFrameCount = 0; this.encoder = new AudioEncoderWrapper(this, codecConfig); } @@ -376,8 +403,8 @@ var Metamuxer = (() => { } }; var MediaStreamAudioTrackSource = class extends AudioSource { - constructor(track, codecConfig, options = {}) { - super(codecConfig.codec, options); + constructor(track, codecConfig) { + super(codecConfig.codec); this.track = track; this.abortController = null; this.encoder = new AudioEncoderWrapper(this, codecConfig); @@ -416,7 +443,7 @@ var Metamuxer = (() => { this.writer = options.target.createWriter(); this.muxer = options.format.createMuxer(this); } - addTrack(source) { + addTrack(source, metadata = {}) { if (this.started) { throw new Error("Cannot add track after output has started."); } @@ -427,7 +454,8 @@ var Metamuxer = (() => { id: this.tracks.length + 1, output: this, type: source instanceof VideoSource ? "video" : "audio", - source + source, + metadata }; this.muxer.beforeTrackAdd(track); this.tracks.push(track); @@ -507,7 +535,7 @@ var Metamuxer = (() => { var lastPresentedSample = (samples) => { let result = null; for (let sample of samples) { - if (!result || sample.presentationTimestamp > result.presentationTimestamp) { + if (!result || sample.timestamp > result.timestamp) { result = sample; } } @@ -588,7 +616,7 @@ var Metamuxer = (() => { 0, ...trackDatas.filter((x) => x.samples.length > 0).map((x) => { const lastSample = lastPresentedSample(x.samples); - return lastSample.presentationTimestamp + lastSample.duration; + return lastSample.timestamp + lastSample.duration; }) ), GLOBAL_TIMESCALE); let nextTrackId = Math.max(...trackDatas.map((x) => x.track.id)) + 1; @@ -624,14 +652,14 @@ var Metamuxer = (() => { var tkhd = (trackData, creationTime) => { let lastSample = lastPresentedSample(trackData.samples); let durationInGlobalTimescale = intoTimescale( - lastSample ? lastSample.presentationTimestamp + lastSample.duration : 0, + lastSample ? lastSample.timestamp + lastSample.duration : 0, GLOBAL_TIMESCALE ); let needsU64 = !isU32(creationTime) || !isU32(durationInGlobalTimescale); let u32OrU64 = needsU64 ? u64 : u32; let matrix; if (trackData.type === "video") { - const rotation = trackData.track.source.metadata.rotation; + const rotation = trackData.track.metadata.rotation; matrix = rotation === void 0 || typeof rotation === "number" ? rotationMatrix(rotation ?? 0) : rotation; } else { matrix = IDENTITY_MATRIX; @@ -673,7 +701,7 @@ var Metamuxer = (() => { var mdhd = (trackData, creationTime) => { let lastSample = lastPresentedSample(trackData.samples); let localDuration = intoTimescale( - lastSample ? lastSample.presentationTimestamp + lastSample.duration : 0, + lastSample ? lastSample.timestamp + lastSample.duration : 0, trackData.timescale ); let needsU64 = !isU32(creationTime) || !isU32(localDuration); @@ -1098,7 +1126,7 @@ var Metamuxer = (() => { let allSampleDurations = trackData.currentChunk.samples.map((x) => x.timescaleUnitsToNextSample); let allSampleSizes = trackData.currentChunk.samples.map((x) => x.size); let allSampleFlags = trackData.currentChunk.samples.map(fragmentSampleFlags); - let allSampleCompositionTimeOffsets = trackData.currentChunk.samples.map((x) => intoTimescale(x.presentationTimestamp - x.decodeTimestamp, trackData.timescale)); + let allSampleCompositionTimeOffsets = trackData.currentChunk.samples.map((x) => intoTimescale(x.timestamp - x.decodeTimestamp, trackData.timescale)); let uniqueSampleDurations = new Set(allSampleDurations); let uniqueSampleSizes = new Set(allSampleSizes); let uniqueSampleFlags = new Set(allSampleFlags); @@ -1359,11 +1387,12 @@ var Metamuxer = (() => { height: meta.decoderConfig.codedHeight, decoderConfig: meta.decoderConfig }, - timescale: track.source.metadata.frameRate ?? 57600, + timescale: track.metadata.frameRate ?? 57600, samples: [], sampleQueue: [], - firstDecodeTimestamp: null, - lastDecodeTimestamp: -1, + timestampProcessingQueue: [], + firstTimestamp: null, + lastKeyFrameTimestamp: null, timeToSampleTable: [], compositionTimeOffsetTable: [], lastTimescaleUnits: null, @@ -1394,8 +1423,9 @@ var Metamuxer = (() => { timescale: meta.decoderConfig.sampleRate, samples: [], sampleQueue: [], - firstDecodeTimestamp: null, - lastDecodeTimestamp: -1, + timestampProcessingQueue: [], + firstTimestamp: null, + lastKeyFrameTimestamp: null, timeToSampleTable: [], compositionTimeOffsetTable: [], lastTimescaleUnits: null, @@ -1408,12 +1438,12 @@ var Metamuxer = (() => { this.#trackDatas.sort((a, b) => a.track.id - b.track.id); return newTrackData; } - addEncodedVideoChunk(track, chunk, meta, compositionTimeOffset) { + addEncodedVideoChunk(track, chunk, meta) { const trackData = this.#getVideoTrackData(track, chunk, meta); if (typeof this.#format.options.fastStart === "object" && trackData.samples.length === this.#format.options.fastStart.expectedVideoChunks) { throw new Error(`Cannot add more video chunks than specified in 'fastStart' (${this.#format.options.fastStart.expectedVideoChunks}).`); } - let videoSample = this.#createSampleForTrack(trackData, chunk, compositionTimeOffset); + let videoSample = this.#createSampleForTrack(trackData, chunk); if (this.#format.options.fastStart === "fragmented") { trackData.sampleQueue.push(videoSample); this.#interleaveSamples(); @@ -1434,18 +1464,15 @@ var Metamuxer = (() => { this.#addSampleToTrack(trackData, audioSample); } } - #createSampleForTrack(trackData, chunk, compositionTimeOffset) { - let presentationTimestampInSeconds = chunk.timestamp / 1e6; - let decodeTimestampInSeconds = (chunk.timestamp - (compositionTimeOffset ?? 0)) / 1e6; + #createSampleForTrack(trackData, chunk) { + let timestampInSeconds = this.#validateTimestamp(trackData, chunk); let durationInSeconds = (chunk.duration ?? 0) / 1e6; - let adjusted = this.#validateTimestamp(trackData, presentationTimestampInSeconds, decodeTimestampInSeconds); - presentationTimestampInSeconds = adjusted.presentationTimestamp; - decodeTimestampInSeconds = adjusted.decodeTimestamp; let data = new Uint8Array(chunk.byteLength); chunk.copyTo(data); let sample = { - presentationTimestamp: presentationTimestampInSeconds, - decodeTimestamp: decodeTimestampInSeconds, + timestamp: timestampInSeconds, + decodeTimestamp: timestampInSeconds, + // We may refine this later duration: durationInSeconds, data, size: data.byteLength, @@ -1455,62 +1482,76 @@ var Metamuxer = (() => { }; return sample; } - #addSampleToTrack(trackData, sample) { - if (this.#format.options.fastStart !== "fragmented") { - trackData.samples.push(sample); + #processTimestamps(trackData) { + if (trackData.timestampProcessingQueue.length === 0) { + return; } - const sampleCompositionTimeOffset = intoTimescale(sample.presentationTimestamp - sample.decodeTimestamp, trackData.timescale); - if (trackData.lastTimescaleUnits !== null) { - assert(trackData.lastSample); - let timescaleUnits = intoTimescale(sample.decodeTimestamp, trackData.timescale, false); - let delta = Math.round(timescaleUnits - trackData.lastTimescaleUnits); - trackData.lastTimescaleUnits += delta; - trackData.lastSample.timescaleUnitsToNextSample = delta; - if (this.#format.options.fastStart !== "fragmented") { - let lastTableEntry = last(trackData.timeToSampleTable); - assert(lastTableEntry); - if (lastTableEntry.sampleCount === 1) { - lastTableEntry.sampleDelta = delta; - lastTableEntry.sampleCount++; - } else if (lastTableEntry.sampleDelta === delta) { - lastTableEntry.sampleCount++; - } else { - lastTableEntry.sampleCount--; - trackData.timeToSampleTable.push({ - sampleCount: 2, - sampleDelta: delta - }); + const sortedTimestamps = trackData.timestampProcessingQueue.map((x) => x.timestamp).sort((a, b) => a - b); + for (let i = 0; i < trackData.timestampProcessingQueue.length; i++) { + const sample = trackData.timestampProcessingQueue[i]; + sample.decodeTimestamp = sortedTimestamps[i]; + const sampleCompositionTimeOffset = intoTimescale(sample.timestamp - sample.decodeTimestamp, trackData.timescale); + if (trackData.lastTimescaleUnits !== null) { + assert(trackData.lastSample); + let timescaleUnits = intoTimescale(sample.decodeTimestamp, trackData.timescale, false); + let delta = Math.round(timescaleUnits - trackData.lastTimescaleUnits); + trackData.lastTimescaleUnits += delta; + trackData.lastSample.timescaleUnitsToNextSample = delta; + if (this.#format.options.fastStart !== "fragmented") { + let lastTableEntry = last(trackData.timeToSampleTable); + assert(lastTableEntry); + if (lastTableEntry.sampleCount === 1) { + lastTableEntry.sampleDelta = delta; + lastTableEntry.sampleCount++; + } else if (lastTableEntry.sampleDelta === delta) { + lastTableEntry.sampleCount++; + } else { + lastTableEntry.sampleCount--; + trackData.timeToSampleTable.push({ + sampleCount: 2, + sampleDelta: delta + }); + } + const lastCompositionTimeOffsetTableEntry = last(trackData.compositionTimeOffsetTable); + assert(lastCompositionTimeOffsetTableEntry); + if (lastCompositionTimeOffsetTableEntry.sampleCompositionTimeOffset === sampleCompositionTimeOffset) { + lastCompositionTimeOffsetTableEntry.sampleCount++; + } else { + trackData.compositionTimeOffsetTable.push({ + sampleCount: 1, + sampleCompositionTimeOffset + }); + } } - const lastCompositionTimeOffsetTableEntry = last(trackData.compositionTimeOffsetTable); - assert(lastCompositionTimeOffsetTableEntry); - if (lastCompositionTimeOffsetTableEntry.sampleCompositionTimeOffset === sampleCompositionTimeOffset) { - lastCompositionTimeOffsetTableEntry.sampleCount++; - } else { + } else { + trackData.lastTimescaleUnits = 0; + if (this.#format.options.fastStart !== "fragmented") { + trackData.timeToSampleTable.push({ + sampleCount: 1, + sampleDelta: intoTimescale(sample.duration, trackData.timescale) + }); trackData.compositionTimeOffsetTable.push({ sampleCount: 1, sampleCompositionTimeOffset }); } } - } else { - trackData.lastTimescaleUnits = 0; - if (this.#format.options.fastStart !== "fragmented") { - trackData.timeToSampleTable.push({ - sampleCount: 1, - sampleDelta: intoTimescale(sample.duration, trackData.timescale) - }); - trackData.compositionTimeOffsetTable.push({ - sampleCount: 1, - sampleCompositionTimeOffset - }); - } + trackData.lastSample = sample; + } + trackData.timestampProcessingQueue.length = 0; + } + #addSampleToTrack(trackData, sample) { + if (sample.type === "key") { + this.#processTimestamps(trackData); + } + if (this.#format.options.fastStart !== "fragmented") { + trackData.samples.push(sample); } - trackData.lastSample = sample; let beginNewChunk = false; if (!trackData.currentChunk) { beginNewChunk = true; } else { - let currentChunkDuration = sample.presentationTimestamp - trackData.currentChunk.startTimestamp; + let currentChunkDuration = sample.timestamp - trackData.currentChunk.startTimestamp; if (this.#format.options.fastStart === "fragmented") { const keyFrameQueuedEverywhere = this.#trackDatas.every((otherTrackData) => { if (trackData === otherTrackData) { @@ -1532,7 +1573,7 @@ var Metamuxer = (() => { this.#finalizeCurrentChunk(trackData); } trackData.currentChunk = { - startTimestamp: sample.presentationTimestamp, + startTimestamp: sample.timestamp, samples: [], offset: null, moofOffset: null @@ -1540,23 +1581,24 @@ var Metamuxer = (() => { } assert(trackData.currentChunk); trackData.currentChunk.samples.push(sample); + trackData.timestampProcessingQueue.push(sample); } - #validateTimestamp(trackData, presentationTimestamp, decodeTimestamp) { - if (decodeTimestamp < 0) { - throw new Error(`Timestamps must be non-negative (got ${decodeTimestamp}s).`); + #validateTimestamp(trackData, chunk) { + let timestampInSeconds = chunk.timestamp / 1e6; + if (timestampInSeconds < 0) { + throw new Error(`Timestamps must be non-negative (got ${timestampInSeconds}s).`); } - if (trackData.firstDecodeTimestamp === null) { - trackData.firstDecodeTimestamp = decodeTimestamp; + if (trackData.firstTimestamp === null) { + trackData.firstTimestamp = timestampInSeconds; } - decodeTimestamp -= trackData.firstDecodeTimestamp; - presentationTimestamp -= trackData.firstDecodeTimestamp; - if (decodeTimestamp < trackData.lastDecodeTimestamp) { - throw new Error( - `Timestamps must be monotonically increasing (timestamp went from ${trackData.lastDecodeTimestamp}s to ${decodeTimestamp}s).` - ); + timestampInSeconds -= trackData.firstTimestamp; + if (trackData.lastKeyFrameTimestamp !== null && timestampInSeconds < trackData.lastKeyFrameTimestamp) { + throw new Error(`Timestamp cannot be before last key frame's timestamp (got ${timestampInSeconds}s, last key frame at ${trackData.lastKeyFrameTimestamp}s).`); } - trackData.lastDecodeTimestamp = decodeTimestamp; - return { presentationTimestamp, decodeTimestamp }; + if (chunk.type === "key") { + trackData.lastKeyFrameTimestamp = timestampInSeconds; + } + return timestampInSeconds; } #finalizeCurrentChunk(trackData) { assert(this.#format.options.fastStart !== "fragmented"); @@ -1589,22 +1631,22 @@ var Metamuxer = (() => { } outer: while (true) { - let trackWithMinDecodeTimestamp = null; - let minDecodeTimestamp = Infinity; + let trackWithMinTimestamp = null; + let minTimestamp = Infinity; for (let trackData of this.#trackDatas) { if (trackData.sampleQueue.length === 0) { break outer; } - if (trackData.sampleQueue[0].decodeTimestamp < minDecodeTimestamp) { - trackWithMinDecodeTimestamp = trackData; - minDecodeTimestamp = trackData.sampleQueue[0].decodeTimestamp; + if (trackData.sampleQueue[0].timestamp < minTimestamp) { + trackWithMinTimestamp = trackData; + minTimestamp = trackData.sampleQueue[0].timestamp; } } - if (!trackWithMinDecodeTimestamp) { + if (!trackWithMinTimestamp) { break; } - let sample = trackWithMinDecodeTimestamp.sampleQueue.shift(); - this.#addSampleToTrack(trackWithMinDecodeTimestamp, sample); + let sample = trackWithMinTimestamp.sampleQueue.shift(); + this.#addSampleToTrack(trackWithMinTimestamp, sample); } } #finalizeFragment(flushWriter = true) { @@ -1663,10 +1705,12 @@ var Metamuxer = (() => { for (let sample of trackData.sampleQueue) { this.#addSampleToTrack(trackData, sample); } + this.#processTimestamps(trackData); } this.#finalizeFragment(false); } else { for (let trackData of this.#trackDatas) { + this.#processTimestamps(trackData); this.#finalizeCurrentChunk(trackData); } } @@ -1740,6 +1784,11 @@ var Metamuxer = (() => { this.value = value; } }; + var EBMLSignedInt = class { + constructor(value) { + this.value = value; + } + }; var measureUnsignedInt = (value) => { if (value < 1 << 8) { return 1; @@ -1755,6 +1804,21 @@ var Metamuxer = (() => { return 6; } }; + var measureSignedInt = (value) => { + if (value >= -(1 << 6) && value < 1 << 6) { + return 1; + } else if (value >= -(1 << 13) && value < 1 << 13) { + return 2; + } else if (value >= -(1 << 20) && value < 1 << 20) { + return 3; + } else if (value >= -(1 << 27) && value < 1 << 27) { + return 4; + } else if (value >= -(2 ** 34) && value < 2 ** 34) { + return 5; + } else { + return 6; + } + }; var measureEBMLVarInt = (value) => { if (value < (1 << 7) - 1) { return 1; @@ -1864,6 +1928,12 @@ var Metamuxer = (() => { } this.#writer.write(this.#helper.subarray(0, pos)); } + #writeSignedInt(value, width = measureSignedInt(value)) { + if (value < 0) { + value += 2 ** (width * 8); + } + this.#writeUnsignedInt(value, width); + } writeEBMLVarInt(value, width = measureEBMLVarInt(value)) { let pos = 0; switch (width) { @@ -1954,6 +2024,10 @@ var Metamuxer = (() => { } else if (data.data instanceof EBMLFloat64) { this.writeEBMLVarInt(8); this.#writeFloat64(data.data.value); + } else if (data.data instanceof EBMLSignedInt) { + let size = data.size ?? measureSignedInt(data.data.value); + this.writeEBMLVarInt(size); + this.#writeSignedInt(data.data.value, size); } } } @@ -2039,7 +2113,7 @@ var Metamuxer = (() => { { 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, + trackData.track.metadata.frameRate ? { id: 2352003 /* DefaultDuration */, data: 1e9 / trackData.track.metadata.frameRate } : null, { id: 224 /* Video */, data: [ { id: 176 /* PixelWidth */, data: trackData.info.width }, { id: 186 /* PixelHeight */, data: trackData.info.height }, @@ -2122,7 +2196,7 @@ var Metamuxer = (() => { }, chunkQueue: [], firstTimestamp: null, - lastTimestamp: null, + lastKeyFrameTimestamp: null, lastWrittenTimestamp: null }; this.#trackDatas.push(newTrackData); @@ -2146,18 +2220,17 @@ var Metamuxer = (() => { }, chunkQueue: [], firstTimestamp: null, - lastTimestamp: null, + lastKeyFrameTimestamp: 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) { + addEncodedVideoChunk(track, chunk, meta) { 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(); @@ -2165,7 +2238,6 @@ var Metamuxer = (() => { 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(); @@ -2296,7 +2368,7 @@ var Metamuxer = (() => { */ /** Converts a read-only external chunk into an internal one for easier use. */ #createInternalChunk(trackData, chunk) { - let adjustedTimestamp = this.#validateTimestamp(trackData, chunk.timestamp); + let adjustedTimestamp = this.#validateTimestamp(trackData, chunk); let data = new Uint8Array(chunk.byteLength); chunk.copyTo(data); let internalChunk = { @@ -2308,7 +2380,8 @@ var Metamuxer = (() => { }; return internalChunk; } - #validateTimestamp(trackData, timestamp) { + #validateTimestamp(trackData, chunk) { + let timestamp = chunk.timestamp; if (timestamp < 0) { throw new Error(`Timestamps must be non-negative (got ${timestamp}s).`); } @@ -2316,10 +2389,11 @@ var Metamuxer = (() => { 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).` - ); + if (trackData.lastKeyFrameTimestamp !== null && timestamp < trackData.lastKeyFrameTimestamp) { + throw new Error(`Timestamp cannot be before last key frame's timestamp (got ${timestamp / 1e6}s, last key frame at ${trackData.lastKeyFrameTimestamp / 1e6}s).`); + } + if (chunk.type === "key") { + trackData.lastKeyFrameTimestamp = timestamp; } return timestamp; } @@ -2368,7 +2442,7 @@ var Metamuxer = (() => { prelude, chunk.data ] }, - chunk.type === "delta" ? { id: 251 /* ReferenceBlock */, data: trackData.lastWrittenTimestamp - msTimestamp } : null, + chunk.type === "delta" ? { id: 251 /* ReferenceBlock */, data: new EBMLSignedInt(trackData.lastWrittenTimestamp - msTimestamp) } : null, chunk.duration !== null ? { id: 155 /* BlockDuration */, data: msDuration } : null, chunk.additions ? { id: 30113 /* BlockAdditions */, data: chunk.additions } : null ] }; diff --git a/dist/metamuxer.min.js b/dist/metamuxer.min.js index 8eb851b..0fc98f5 100644 --- a/dist/metamuxer.min.js +++ b/dist/metamuxer.min.js @@ -1,2 +1,2 @@ -"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);})(); +"use strict";var Metamuxer=(()=>{var me=Object.defineProperty;var Ie=Object.getOwnPropertyDescriptor;var We=Object.getOwnPropertyNames;var Ne=Object.prototype.hasOwnProperty;var Re=(i,s)=>{for(var e in s)me(i,e,{get:s[e],enumerable:!0})},Qe=(i,s,e,t)=>{if(s&&typeof s=="object"||typeof s=="function")for(let r of We(s))!Ne.call(i,r)&&r!==e&&me(i,r,{get:()=>s[r],enumerable:!(t=Ie(s,r))||t.enumerable});return i};var He=i=>Qe(me({},"__esModule",{value:!0}),i);var Nt={};Re(Nt,{ArrayBufferTarget:()=>ce,AudioBufferSource:()=>J,AudioDataSource:()=>Z,CanvasSource:()=>X,EncodedAudioChunkSource:()=>L,EncodedVideoChunkSource:()=>j,FileSystemWritableFileStreamTarget:()=>fe,MediaStreamAudioTrackSource:()=>ee,MediaStreamVideoTrackSource:()=>Y,MkvOutputFormat:()=>H,Mp4OutputFormat:()=>ae,Output:()=>te,StreamTarget:()=>D,Target:()=>P,VideoFrameSource:()=>q,WebMOutputFormat:()=>U});var ye=(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"),l=r.toString(16).padStart(2,"0"),a=n.toString(16).padStart(2,"0");return`avc1.${o}${l}${a}`}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+/,""),l="L",a=120;return s<=1280&&e<=720?a=93:s<=1920&&e<=1080?a=120:s<=3840&&e<=2160?a=150:(l="H",a=180),`hev1.${t===0?"":String.fromCharCode(65+t-1)}${r}.${o}.${l}${a}.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}'.`)},we=(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 E=i=>i&&i[i.length-1],v=i=>i>=0&&i<2**32,V=(i,s,e)=>{let t=0;for(let r=s;r>l;t<<=1,t|=a}return t},Se=(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 S=class{constructor(s){this.connectedTrack=null;this.codec=s}ensureValidDigest(){if(!this.connectedTrack)throw new Error("Cannot call digest without connecting the source to an output track.");if(!this.connectedTrack.output.started)throw new Error("Cannot call digest before output has been started.");if(this.connectedTrack.output.finalizing)throw new Error("Cannot call digest after output has started finalizing.")}start(){}async flush(){}},F=class{constructor(s){this.connectedTrack=null;this.codec=s}ensureNotFinalizing(){if(this.connectedTrack?.output.finalizing)throw new Error("Cannot call digest after output has started finalizing.")}start(){}async flush(){}},j=class extends S{constructor(s){super(s)}digest(s,e){this.ensureValidDigest(),this.connectedTrack?.output.muxer.addEncodedVideoChunk(this.connectedTrack,s,e)}},$e=5,I=class{constructor(s,e){this.source=s;this.codecConfig=e;this.encoder=null;this.lastMultipleOfKeyFrameInterval=-1}digest(s){this.source.ensureValidDigest(),this.ensureEncoder(s),d(this.encoder);let e=Math.floor(s.timestamp/1e6/$e);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:ye(this.codecConfig.codec,s.codedWidth,s.codedHeight),width:s.codedWidth,height:s.codedHeight,bitrate:this.codecConfig.bitrate}))}async flush(){return this.encoder?.flush()}},q=class extends S{constructor(s){super(s.codec),this.encoder=new I(this,s)}digest(s){this.encoder.digest(s)}flush(){return this.encoder.flush()}},X=class extends S{constructor(e,t){super(t.codec);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()}},Y=class extends S{constructor(e,t){super(t.codec);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()}},L=class extends F{constructor(s){super(s)}digest(s,e){this.ensureNotFinalizing(),this.connectedTrack?.output.muxer.addEncodedAudioChunk(this.connectedTrack,s,e)}},W=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:we(this.codecConfig.codec,s.numberOfChannels,s.sampleRate),numberOfChannels:s.numberOfChannels,sampleRate:s.sampleRate,bitrate:this.codecConfig.bitrate}))}async flush(){return this.encoder?.flush()}},Z=class extends F{constructor(s){super(s.codec),this.encoder=new W(this,s)}digest(s){this.encoder.digest(s)}flush(){return this.encoder.flush()}},J=class extends F{constructor(e){super(e.codec);this.accumulatedFrameCount=0;this.encoder=new W(this,e)}digest(e){let t=e.numberOfChannels,r=e.sampleRate,n=e.length,o=new Float32Array(t*n);for(let a=0;a{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 te=class{constructor(s){this.tracks=[];this.started=!1;this.finalizing=!1;this.writer=s.target.createWriter(),this.muxer=s.format.createMuxer(this)}addTrack(s,e={}){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 t={id:this.tracks.length+1,output:this,type:s instanceof S?"video":"audio",source:s,metadata:e};this.muxer.beforeTrackAdd(t),this.tracks.push(t),s.connectedTrack=t}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 c=new Uint8Array(8),y=new DataView(c.buffer),b=i=>[(i%256+256)%256],m=i=>(y.setUint16(0,i,!1),[c[0],c[1]]),Ke=i=>(y.setInt16(0,i,!1),[c[0],c[1]]),Oe=i=>(y.setUint32(0,i,!1),[c[1],c[2],c[3]]),u=i=>(y.setUint32(0,i,!1),[c[0],c[1],c[2],c[3]]),Ge=i=>(y.setInt32(0,i,!1),[c[0],c[1],c[2],c[3]]),z=i=>(y.setUint32(0,Math.floor(i/2**32),!1),y.setUint32(4,i,!1),[c[0],c[1],c[2],c[3],c[4],c[5],c[6],c[7]]),pe=i=>(y.setInt16(0,2**8*i,!1),[c[0],c[1]]),x=i=>(y.setInt32(0,2**16*i,!1),[c[0],c[1],c[2],c[3]]),he=i=>(y.setInt32(0,2**30*i,!1),[c[0],c[1],c[2],c[3]]),k=(i,s=!1)=>{let e=Array(i.length).fill(null).map((t,r)=>i.charCodeAt(r));return s&&e.push(0),e},be=i=>{let s=null;for(let e of i)(!s||e.timestamp>s.timestamp)&&(s=e);return s},ve=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]},Ve=ve(0),ze=i=>[x(i[0]),x(i[1]),he(i[2]),x(i[3]),x(i[4]),he(i[5]),x(i[6]),x(i[7]),he(i[8])],p=(i,s,e)=>({type:i,contents:s&&new Uint8Array(s.flat(10)),children:e}),h=(i,s,e,t,r)=>p(i,[b(s),Oe(e),t??[]],r),Ue=i=>{let s=512;return i.fragmented?p("ftyp",[k("iso5"),u(s),k("iso5"),k("iso6"),k("mp41")]):p("ftyp",[k("isom"),u(s),k("isom"),i.holdsAvc?k("avc1"):[],k("mp41")])},ie=i=>({type:"mdat",largeSize:i}),Pe=i=>({type:"free",size:i}),N=(i,s,e=!1)=>p("moov",void 0,[je(s,i),...i.map(t=>qe(t,s)),e?Tt(i):null]),je=(i,s)=>{let e=T(Math.max(0,...s.filter(o=>o.samples.length>0).map(o=>{let l=be(o.samples);return l.timestamp+l.duration})),re),t=Math.max(...s.map(o=>o.track.id))+1,r=!v(i)||!v(e),n=r?z:u;return h("mvhd",+r,0,[n(i),n(i),u(re),n(e),x(1),pe(1),Array(10).fill(0),ze(Ve),Array(24).fill(0),u(t)])},qe=(i,s)=>p("trak",void 0,[Xe(i,s),Ye(i,s)]),Xe=(i,s)=>{let e=be(i.samples),t=T(e?e.timestamp+e.duration:0,re),r=!v(s)||!v(t),n=r?z:u,o;if(i.type==="video"){let l=i.track.metadata.rotation;o=l===void 0||typeof l=="number"?ve(l??0):l}else o=Ve;return h("tkhd",+r,3,[n(s),n(s),u(i.track.id),u(0),n(t),Array(8).fill(0),m(0),m(0),pe(i.type==="audio"?1:0),m(0),ze(o),x(i.type==="video"?i.info.width:0),x(i.type==="video"?i.info.height:0)])},Ye=(i,s)=>p("mdia",void 0,[Le(i,s),Ze(i.type==="video"?"vide":"soun"),Je(i)]),Le=(i,s)=>{let e=be(i.samples),t=T(e?e.timestamp+e.duration:0,i.timescale),r=!v(s)||!v(t),n=r?z:u;return h("mdhd",+r,0,[n(s),n(s),u(i.timescale),n(t),m(21956),m(0)])},Ze=i=>h("hdlr",0,0,[k("mhlr"),k(i),u(0),u(0),u(0),k("mp4-muxer-hdlr",!0)]),Je=i=>p("minf",void 0,[i.type==="video"?et():tt(),rt(),nt(i)]),et=()=>h("vmhd",0,1,[m(0),m(0),m(0),m(0)]),tt=()=>h("smhd",0,0,[m(0),m(0)]),rt=()=>p("dinf",void 0,[it()]),it=()=>h("dref",0,0,[u(1)],[st()]),st=()=>h("url ",0,1),nt=i=>{let s=i.compositionTimeOffsetTable.length>1||i.compositionTimeOffsetTable.some(e=>e.sampleCompositionTimeOffset!==0);return p("stbl",void 0,[ot(i),ht(i),pt(i),bt(i),Ct(i),gt(i),s?kt(i):null])},ot=i=>h("stsd",0,0,[u(1)],[i.type==="video"?at(zt[i.track.source.codec],i):ct(Pt[i.track.source.codec],i)]),at=(i,s)=>p(i,[Array(6).fill(0),m(1),m(0),m(0),Array(12).fill(0),m(s.info.width),m(s.info.height),u(4718592),u(4718592),u(0),m(1),Array(32).fill(0),m(24),Ke(65535)],[Ut[s.track.source.codec](s)]),ut=i=>i.info.decoderConfig&&p("avcC",[...M(i.info.decoderConfig.description)]),lt=i=>i.info.decoderConfig&&p("hvcC",[...M(i.info.decoderConfig.description)]),Ae=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]),l=(Number(e[3])<<4)+(0<<1)+Number(s.colorSpace.fullRange);return h("vpcC",1,0,[b(t),b(r),b(l),b(2),b(2),b(2),m(0)])},dt=()=>{let e=(1<<7)+1;return p("av1C",[e,0,0,0])},ct=(i,s)=>p(i,[Array(6).fill(0),m(1),m(0),m(0),u(0),m(s.info.numberOfChannels),m(16),m(0),m(0),x(s.info.sampleRate)],[Et[s.track.source.codec](s)]),ft=i=>{let s=M(i.info.decoderConfig.description??new ArrayBuffer(0));return h("esds",0,0,[u(58753152),b(32+s.byteLength),m(1),b(0),u(75530368),b(18+s.byteLength),b(64),b(21),Oe(0),u(130071),u(130071),u(92307584),b(s.byteLength),...s,u(109084800),b(1),b(2)])},mt=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),m(s),u(i.info.sampleRate),pe(e),b(0)])},ht=i=>h("stts",0,0,[u(i.timeToSampleTable.length),i.timeToSampleTable.map(s=>[u(s.sampleCount),u(s.sampleDelta)])]),pt=i=>{if(i.samples.every(e=>e.type==="key"))return null;let s=[...i.samples.entries()].filter(([,e])=>e.type==="key");return h("stss",0,0,[u(s.length),s.map(([e])=>u(e+1))])},bt=i=>h("stsc",0,0,[u(i.compactlyCodedChunkTable.length),i.compactlyCodedChunkTable.map(s=>[u(s.firstChunk),u(s.samplesPerChunk),u(1)])]),Ct=i=>h("stsz",0,0,[u(0),u(i.samples.length),i.samples.map(s=>u(s.size))]),gt=i=>i.finalizedChunks.length>0&&E(i.finalizedChunks).offset>=2**32?h("co64",0,0,[u(i.finalizedChunks.length),i.finalizedChunks.map(s=>z(s.offset))]):h("stco",0,0,[u(i.finalizedChunks.length),i.finalizedChunks.map(s=>u(s.offset))]),kt=i=>h("ctts",0,0,[u(i.compositionTimeOffsetTable.length),i.compositionTimeOffsetTable.map(s=>[u(s.sampleCount),u(s.sampleCompositionTimeOffset)])]),Tt=i=>p("mvex",void 0,i.map(xt)),xt=i=>h("trex",0,0,[u(i.track.id),u(1),u(0),u(0),u(0)]),Ce=(i,s)=>p("moof",void 0,[yt(i),...s.map(wt)]),yt=i=>h("mfhd",0,0,[u(i)]),Ee=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},wt=i=>p("traf",void 0,[St(i),At(i),Ot(i)]),St=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:Ee(e)};return h("tfhd",0,s,[u(i.track.id),u(t.duration),u(t.size),u(t.flags)])},At=i=>(d(i.currentChunk),h("tfdt",1,0,[z(T(i.currentChunk.startTimestamp,i.timescale))])),Ot=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(Ee),r=i.currentChunk.samples.map(w=>T(w.timestamp-w.decodeTimestamp,i.timescale)),n=new Set(s),o=new Set(e),l=new Set(t),a=new Set(r),f=l.size===2&&t[0]!==t[1],C=n.size>1,g=o.size>1,A=!f&&l.size>1,xe=a.size>1||[...a].some(w=>w!==0),O=0;return O|=1,O|=4*+f,O|=256*+C,O|=512*+g,O|=1024*+A,O|=2048*+xe,h("trun",1,O,[u(i.currentChunk.samples.length),u(i.currentChunk.offset-i.currentChunk.moofOffset||0),f?u(t[0]):[],i.currentChunk.samples.map((w,G)=>[C?u(s[G]):[],g?u(e[G]):[],A?u(t[G]):[],xe?Ge(r[G]):[]])])},Me=i=>p("mfra",void 0,[...i.map(vt),Vt()]),vt=(i,s)=>h("tfra",1,0,[u(i.track.id),u(63),u(i.finalizedChunks.length),i.finalizedChunks.map(t=>[z(T(t.startTimestamp,i.timescale)),z(t.moofOffset),u(s+1),u(1),u(1)])]),Vt=()=>h("mfro",0,0,[u(0)]),zt={avc:"avc1",hevc:"hvc1",vp8:"vp08",vp9:"vp09",av1:"av01"},Ut={avc:ut,hevc:lt,vp8:Ae,vp9:Ae,av1:dt},Pt={aac:"mp4a",opus:"Opus"},Et={aac:ft,opus:mt};var B=class{constructor(s){this.output=s}beforeTrackAdd(s){}};var re=1e3,Mt=2082844800,T=(i,s,e=!0)=>{let t=i*s;return e?Math.round(t):t},se=class extends B{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)+Mt;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(Ue({holdsAvc:e,fragmented:this.#r.options.fastStart==="fragmented"})),this.#o=this.#e.getPos(),this.#r.options.fastStart==="in-memory")this.#n=ie(!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=ie(!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(l=>l.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.metadata.frameRate??57600,samples:[],sampleQueue:[],timestampProcessingQueue:[],firstTimestamp:null,lastKeyFrameTimestamp:null,timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.#s.push(o),this.#s.sort((l,a)=>l.track.id-a.track.id),o}#m(e,t,r){let n=this.#s.find(l=>l.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:[],timestampProcessingQueue:[],firstTimestamp:null,lastKeyFrameTimestamp:null,timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.#s.push(o),this.#s.sort((l,a)=>l.track.id-a.track.id),o}addEncodedVideoChunk(e,t,r){let n=this.#u(e,t,r);if(typeof this.#r.options.fastStart=="object"&&n.samples.length===this.#r.options.fastStart.expectedVideoChunks)throw new Error(`Cannot add more video chunks than specified in 'fastStart' (${this.#r.options.fastStart.expectedVideoChunks}).`);let o=this.#h(n,t);this.#r.options.fastStart==="fragmented"?(n.sampleQueue.push(o),this.#b()):this.#p(n,o)}addEncodedAudioChunk(e,t,r){let n=this.#m(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.#h(n,t);this.#r.options.fastStart==="fragmented"?(n.sampleQueue.push(o),this.#b()):this.#p(n,o)}#h(e,t){let r=this.#T(e,t),n=(t.duration??0)/1e6,o=new Uint8Array(t.byteLength);return t.copyTo(o),{timestamp:r,decodeTimestamp:r,duration:n,data:o,size:o.byteLength,type:t.type,timescaleUnitsToNextSample:T(n,e.timescale)}}#c(e){if(e.timestampProcessingQueue.length===0)return;let t=e.timestampProcessingQueue.map(r=>r.timestamp).sort((r,n)=>r-n);for(let r=0;r{if(e===l)return t.type==="key";let a=l.sampleQueue[0];return a&&a.type==="key"});n>=1&&o&&(r=!0,this.#k())}else r=n>=.5}r&&(e.currentChunk&&this.#g(e),e.currentChunk={startTimestamp:t.timestamp,samples:[],offset:null,moofOffset:null}),d(e.currentChunk),e.currentChunk.samples.push(t),e.timestampProcessingQueue.push(t)}#T(e,t){let r=t.timestamp/1e6;if(r<0)throw new Error(`Timestamps must be non-negative (got ${r}s).`);if(e.firstTimestamp===null&&(e.firstTimestamp=r),r-=e.firstTimestamp,e.lastKeyFrameTimestamp!==null&&r=2**32&&(a.largeSize=!0,C=this.measureBox(a)+f),a.size=C,this.writeBox(a)}for(let a of this.#s){a.currentChunk.offset=this.#e.getPos(),a.currentChunk.moofOffset=r;for(let f of a.currentChunk.samples)this.#e.write(f.data),f.data=null}let o=this.#e.getPos();this.#e.seek(this.offsets.get(n));let l=Ce(t,this.#s);this.writeBox(l),this.#e.seek(o);for(let a of this.#s)a.finalizedChunks.push(a.currentChunk),this.#l.push(a.currentChunk),a.currentChunk=null;e&&this.#e.flush()}finalize(){if(this.#r.options.fastStart==="fragmented"){for(let e of this.#s){for(let t of e.sampleQueue)this.#p(e,t);this.#c(e)}this.#k(!1)}else for(let e of this.#s)this.#c(e),this.#g(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 l=this.#e.getPos()+o+e;for(let a of this.#l){a.offset=l;for(let{data:f}of a.samples)d(f),l+=f.byteLength,e+=f.byteLength}if(l<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=Me(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(Pe(n))}else this.writeBox(r)}}};var R=class{constructor(s){this.value=s}},_=class{constructor(s){this.value=s}},Q=class{constructor(s){this.value=s}};var ge=i=>i<256?1:i<65536?2:i<1<<24?3:i<2**32?4:i<2**40?5:6,ke=i=>i>=-64&&i<64?1:i>=-8192&&i<8192?2:i>=-(1<<20)&&i<1<<20?3:i>=-(1<<27)&&i<1<<27?4:i>=-(2**34)&&i<2**34?5:6,Fe=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 Ft=1,Bt=2,Te=2**15,Be="https://github.com/Vanilagy/webm-muxer",_e=6,De=5,_t={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"},ne=class extends B{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.#m=null;this.#h=new Set;this.#c=0;this.#e=e.writer,this.#r=t}#e;#r;#i;#t;#o;#n;#s;#a;#l;#f;#d;#u;#m;#h;#c;#p(e){this.#t.setUint8(0,e),this.#e.write(this.#i.subarray(0,1))}#T(e){this.#t.setFloat32(0,e,!1),this.#e.write(this.#i.subarray(0,4))}#g(e){this.#t.setFloat64(0,e,!1),this.#e.write(this.#i)}#b(e,t=ge(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))}#k(e,t=ke(e)){e<0&&(e+=2**(t*8)),this.#b(e,t)}writeEBMLVarInt(e,t=Fe(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))}#A(e){this.#e.write(new Uint8Array(e.split("").map(t=>t.charCodeAt(0))))}writeEBML(e){if(e!==null){if(e instanceof Uint8Array)this.#e.write(e);else if(Array.isArray(e))for(let t of e)this.writeEBML(t);else if(this.offsets.set(e,this.#e.getPos()),this.#b(e.id),Array.isArray(e.data)){let t=this.#e.getPos(),r=e.size===-1?1:e.size??4;e.size===-1?this.#p(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,l=this.#e.getPos();this.#e.seek(t),this.writeEBMLVarInt(o,r),this.#e.seek(l)}}else if(typeof e.data=="number"){let t=e.size??ge(e.data);this.writeEBMLVarInt(t),this.#b(e.data,t)}else if(typeof e.data=="string")this.writeEBMLVarInt(e.data.length),this.#A(e.data);else if(e.data instanceof Uint8Array)this.writeEBMLVarInt(e.data.byteLength,e.size),this.#e.write(e.data);else if(e.data instanceof R)this.writeEBMLVarInt(4),this.#T(e.data.value);else if(e.data instanceof _)this.writeEBMLVarInt(8),this.#g(e.data.value);else if(e.data instanceof Q){let t=e.size??ke(e.data.value);this.writeEBMLVarInt(t),this.#k(e.data.value,t)}}}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.#O(),this.#r.options.streaming||this.#v(),this.#V(),this.#P(),this.#e.flush()}#O(){let e={id:440786851,data:[{id:17030,data:1},{id:17143,data:1},{id:17138,data:4},{id:17139,data:8},{id:17026,data:this.#r instanceof U?"webm":"matroska"},{id:17031,data:2},{id:17029,data:2}]};this.writeEBML(e)}#v(){let e=new Uint8Array([28,83,187,107]),t=new Uint8Array([21,73,169,102]),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 _(0)};this.#f=e;let t={id:357149030,data:[{id:2807729,data:1e6},{id:19840,data:Be},{id:22337,data:Be},this.#r.options.streaming?null:e]};this.#s=t}#z(){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"?Ft:Bt},{id:134,data:_t[t.track.source.codec]},t.info.decoderConfig.description?{id:25506,data:M(t.info.decoderConfig.description)}:null,...t.type==="video"?[t.track.metadata.frameRate?{id:2352003,data:1e9/t.track.metadata.frameRate}:null,{id:224,data:[{id:176,data:t.info.width},{id:186,data:t.info.height},(()=>{if(t.info.decoderConfig.colorSpace){let 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}]}]:[]]})}#U(){let e={id:408125543,size:this.#r.options.streaming?-1:_e,data:[this.#r.options.streaming?null:this.#a,this.#s,this.#l]};this.#n=e,this.writeEBML(e)}#P(){this.#d={id:475249515,data:[]}}get#C(){return d(this.#n),this.dataOffsets.get(this.#n)}#E(e,t,r){let n=this.#o.find(l=>l.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,lastKeyFrameTimestamp:null,lastWrittenTimestamp:null};return this.#o.push(o),this.#o.sort((l,a)=>l.track.id-a.track.id),o}#M(e,t,r){let n=this.#o.find(l=>l.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,lastKeyFrameTimestamp:null,lastWrittenTimestamp:null};return this.#o.push(o),this.#o.sort((l,a)=>l.track.id-a.track.id),o}addEncodedVideoChunk(e,t,r){let n=this.#E(e,t,r),o=this.#y(n,t);e.source.codec==="vp9"&&this.#F(n,o),n.chunkQueue.push(o),this.#x(),this.#e.flush()}addEncodedAudioChunk(e,t,r){let n=this.#M(e,t,r),o=this.#y(n,t);n.chunkQueue.push(o),this.#x(),this.#e.flush()}#x(){if(!(this.#o.length=2&&r++;let f={rgb:7,bt709:2,bt470bg:1,smpte170m:3}[e.info.decoderConfig.colorSpace.matrix];Se(t.data,r+0,r+3,f)}#y(e,t){let r=this.#B(e,t),n=new Uint8Array(t.byteLength);return t.copyTo(n),{data:n,type:t.type,timestamp:r,duration:t.duration,additions:null}}#B(e,t){let r=t.timestamp;if(r<0)throw new Error(`Timestamps must be non-negative (got ${r}s).`);if(e.firstTimestamp===null&&(e.firstTimestamp=r),r-=e.firstTimestamp,e.lastKeyFrameTimestamp!==null&&r{if(e===g)return t.type==="key";let A=g.chunkQueue[0];return A&&A.type==="key"});(!this.#u||n&&r-this.#m>=1e3)&&this.#_(r);let o=r-this.#m;if(o<0)return;if(o>=Te)throw new Error(`Current Matroska cluster exceeded its maximum allowed length of ${Te} milliseconds. In order to produce a correct WebM file, you must pass in a key frame at least every ${Te} milliseconds.`);let a=new Uint8Array(4),f=new DataView(a.buffer);f.setUint8(0,128|e.track.id),f.setInt16(1,o,!1);let C=Math.floor((t.duration??0)/1e3);if(C===0&&!t.additions){f.setUint8(3,+(t.type==="key")<<7);let g={id:163,data:[a,t.data]};this.writeEBML(g)}else{let g={id:160,data:[{id:161,data:[a,t.data]},t.type==="delta"?{id:251,data:new Q(e.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.#h.add(e)}#_(e){this.#u&&!this.#r.options.streaming&&this.#S(),this.#u={id:524531317,size:this.#r.options.streaming?-1:De,data:[{id:231,data:e}]},this.writeEBML(this.#u),this.#m=e,this.#h.clear()}#S(){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,De),this.#e.seek(t);let r=this.offsets.get(this.#u)-this.#C;d(this.#d),this.#d.data.push({id:187,data:[{id:179,data:this.#m},...[...this.#h].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.#w(e,e.chunkQueue.shift());if(this.#r.options.streaming||this.#S(),d(this.#d),this.writeEBML(this.#d),!this.#r.options.streaming){let e=this.#e.getPos(),t=this.#e.getPos()-this.#C;this.#e.seek(this.offsets.get(this.#n)+4),this.writeEBMLVarInt(t,_e),this.#f.data=new _(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.#C,this.#a.data[1].data[1].data=this.offsets.get(this.#s)-this.#C,this.#a.data[2].data[1].data=this.offsets.get(this.#l)-this.#C,this.#e.seek(this.offsets.get(this.#a)),this.writeEBML(this.#a),this.#e.seek(e)}}};var oe=class{},ae=class extends oe{constructor(e){super();this.options=e}createMuxer(e){return new se(e,this)}},H=class extends oe{constructor(e={}){super();this.options=e}createMuxer(e){return new ne(e,this)}},U=class extends H{};var $=class{},ue=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;ta.start<=e&&eIt){for(let a=0;a=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 Wt=Symbol("isTarget");Wt;var P=class{},ce=class extends P{constructor(){super(...arguments);this.buffer=null}createWriter(){return new ue(this)}},D=class extends P{constructor(e){super();this.options=e;if(typeof e!="object")throw new TypeError("StreamTarget requires an options object to be passed to its constructor.");if(e.onData){if(typeof e.onData!="function")throw new TypeError("options.onData, when provided, must be a function.");if(e.onData.length<2)throw new TypeError("options.onData, when provided, must be a function that takes in at least two arguments (data and position). Ignoring the position argument, which specifies the byte offset at which the data is to be written, can lead to broken outputs.")}if(e.chunked!==void 0&&typeof e.chunked!="boolean")throw new TypeError("options.chunked, when provided, must be a boolean.");if(e.chunkSize!==void 0&&(!Number.isInteger(e.chunkSize)||e.chunkSize<=0))throw new TypeError("options.chunkSize, when provided, must be a positive integer.")}createWriter(){return this.options.chunked?new K(this):new le(this)}},fe=class extends P{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 de(this)}};return He(Nt);})(); 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 01891b7..2c0167b 100644 --- a/dist/metamuxer.min.mjs +++ b/dist/metamuxer.min.mjs @@ -1 +1 @@ -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}; +var xe=(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"),l=r.toString(16).padStart(2,"0"),a=n.toString(16).padStart(2,"0");return`avc1.${o}${l}${a}`}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+/,""),l="L",a=120;return s<=1280&&e<=720?a=93:s<=1920&&e<=1080?a=120:s<=3840&&e<=2160?a=150:(l="H",a=180),`hev1.${t===0?"":String.fromCharCode(65+t-1)}${r}.${o}.${l}${a}.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}'.`)},ye=(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],v=i=>i>=0&&i<2**32,V=(i,s,e)=>{let t=0;for(let r=s;r>l;t<<=1,t|=a}return t},we=(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 S=class{constructor(s){this.connectedTrack=null;this.codec=s}ensureValidDigest(){if(!this.connectedTrack)throw new Error("Cannot call digest without connecting the source to an output track.");if(!this.connectedTrack.output.started)throw new Error("Cannot call digest before output has been started.");if(this.connectedTrack.output.finalizing)throw new Error("Cannot call digest after output has started finalizing.")}start(){}async flush(){}},E=class{constructor(s){this.connectedTrack=null;this.codec=s}ensureNotFinalizing(){if(this.connectedTrack?.output.finalizing)throw new Error("Cannot call digest after output has started finalizing.")}start(){}async flush(){}},te=class extends S{constructor(s){super(s)}digest(s,e){this.ensureValidDigest(),this.connectedTrack?.output.muxer.addEncodedVideoChunk(this.connectedTrack,s,e)}},De=5,D=class{constructor(s,e){this.source=s;this.codecConfig=e;this.encoder=null;this.lastMultipleOfKeyFrameInterval=-1}digest(s){this.source.ensureValidDigest(),this.ensureEncoder(s),d(this.encoder);let e=Math.floor(s.timestamp/1e6/De);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:xe(this.codecConfig.codec,s.codedWidth,s.codedHeight),width:s.codedWidth,height:s.codedHeight,bitrate:this.codecConfig.bitrate}))}async flush(){return this.encoder?.flush()}},re=class extends S{constructor(s){super(s.codec),this.encoder=new D(this,s)}digest(s){this.encoder.digest(s)}flush(){return this.encoder.flush()}},ie=class extends S{constructor(e,t){super(t.codec);this.canvas=e;this.encoder=new D(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()}},se=class extends S{constructor(e,t){super(t.codec);this.track=e;this.abortController=null;this.encoder=new D(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()}},ne=class extends E{constructor(s){super(s)}digest(s,e){this.ensureNotFinalizing(),this.connectedTrack?.output.muxer.addEncodedAudioChunk(this.connectedTrack,s,e)}},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:ye(this.codecConfig.codec,s.numberOfChannels,s.sampleRate),numberOfChannels:s.numberOfChannels,sampleRate:s.sampleRate,bitrate:this.codecConfig.bitrate}))}async flush(){return this.encoder?.flush()}},oe=class extends E{constructor(s){super(s.codec),this.encoder=new I(this,s)}digest(s){this.encoder.digest(s)}flush(){return this.encoder.flush()}},ae=class extends E{constructor(e){super(e.codec);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 a=0;a{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 le=class{constructor(s){this.tracks=[];this.started=!1;this.finalizing=!1;this.writer=s.target.createWriter(),this.muxer=s.format.createMuxer(this)}addTrack(s,e={}){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 t={id:this.tracks.length+1,output:this,type:s instanceof S?"video":"audio",source:s,metadata:e};this.muxer.beforeTrackAdd(t),this.tracks.push(t),s.connectedTrack=t}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 c=new Uint8Array(8),y=new DataView(c.buffer),b=i=>[(i%256+256)%256],m=i=>(y.setUint16(0,i,!1),[c[0],c[1]]),Ie=i=>(y.setInt16(0,i,!1),[c[0],c[1]]),Ae=i=>(y.setUint32(0,i,!1),[c[1],c[2],c[3]]),u=i=>(y.setUint32(0,i,!1),[c[0],c[1],c[2],c[3]]),We=i=>(y.setInt32(0,i,!1),[c[0],c[1],c[2],c[3]]),z=i=>(y.setUint32(0,Math.floor(i/2**32),!1),y.setUint32(4,i,!1),[c[0],c[1],c[2],c[3],c[4],c[5],c[6],c[7]]),ce=i=>(y.setInt16(0,2**8*i,!1),[c[0],c[1]]),x=i=>(y.setInt32(0,2**16*i,!1),[c[0],c[1],c[2],c[3]]),de=i=>(y.setInt32(0,2**30*i,!1),[c[0],c[1],c[2],c[3]]),k=(i,s=!1)=>{let e=Array(i.length).fill(null).map((t,r)=>i.charCodeAt(r));return s&&e.push(0),e},fe=i=>{let s=null;for(let e of i)(!s||e.timestamp>s.timestamp)&&(s=e);return s},Oe=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]},ve=Oe(0),Ve=i=>[x(i[0]),x(i[1]),de(i[2]),x(i[3]),x(i[4]),de(i[5]),x(i[6]),x(i[7]),de(i[8])],p=(i,s,e)=>({type:i,contents:s&&new Uint8Array(s.flat(10)),children:e}),h=(i,s,e,t,r)=>p(i,[b(s),Ae(e),t??[]],r),ze=i=>{let s=512;return i.fragmented?p("ftyp",[k("iso5"),u(s),k("iso5"),k("iso6"),k("mp41")]):p("ftyp",[k("isom"),u(s),k("isom"),i.holdsAvc?k("avc1"):[],k("mp41")])},j=i=>({type:"mdat",largeSize:i}),Ue=i=>({type:"free",size:i}),W=(i,s,e=!1)=>p("moov",void 0,[Ne(s,i),...i.map(t=>Re(t,s)),e?mt(i):null]),Ne=(i,s)=>{let e=T(Math.max(0,...s.filter(o=>o.samples.length>0).map(o=>{let l=fe(o.samples);return l.timestamp+l.duration})),G),t=Math.max(...s.map(o=>o.track.id))+1,r=!v(i)||!v(e),n=r?z:u;return h("mvhd",+r,0,[n(i),n(i),u(G),n(e),x(1),ce(1),Array(10).fill(0),Ve(ve),Array(24).fill(0),u(t)])},Re=(i,s)=>p("trak",void 0,[Qe(i,s),He(i,s)]),Qe=(i,s)=>{let e=fe(i.samples),t=T(e?e.timestamp+e.duration:0,G),r=!v(s)||!v(t),n=r?z:u,o;if(i.type==="video"){let l=i.track.metadata.rotation;o=l===void 0||typeof l=="number"?Oe(l??0):l}else o=ve;return h("tkhd",+r,3,[n(s),n(s),u(i.track.id),u(0),n(t),Array(8).fill(0),m(0),m(0),ce(i.type==="audio"?1:0),m(0),Ve(o),x(i.type==="video"?i.info.width:0),x(i.type==="video"?i.info.height:0)])},He=(i,s)=>p("mdia",void 0,[$e(i,s),Ke(i.type==="video"?"vide":"soun"),Ge(i)]),$e=(i,s)=>{let e=fe(i.samples),t=T(e?e.timestamp+e.duration:0,i.timescale),r=!v(s)||!v(t),n=r?z:u;return h("mdhd",+r,0,[n(s),n(s),u(i.timescale),n(t),m(21956),m(0)])},Ke=i=>h("hdlr",0,0,[k("mhlr"),k(i),u(0),u(0),u(0),k("mp4-muxer-hdlr",!0)]),Ge=i=>p("minf",void 0,[i.type==="video"?je():qe(),Xe(),Ze(i)]),je=()=>h("vmhd",0,1,[m(0),m(0),m(0),m(0)]),qe=()=>h("smhd",0,0,[m(0),m(0)]),Xe=()=>p("dinf",void 0,[Ye()]),Ye=()=>h("dref",0,0,[u(1)],[Le()]),Le=()=>h("url ",0,1),Ze=i=>{let s=i.compositionTimeOffsetTable.length>1||i.compositionTimeOffsetTable.some(e=>e.sampleCompositionTimeOffset!==0);return p("stbl",void 0,[Je(i),at(i),ut(i),lt(i),dt(i),ct(i),s?ft(i):null])},Je=i=>h("stsd",0,0,[u(1)],[i.type==="video"?et(yt[i.track.source.codec],i):st(St[i.track.source.codec],i)]),et=(i,s)=>p(i,[Array(6).fill(0),m(1),m(0),m(0),Array(12).fill(0),m(s.info.width),m(s.info.height),u(4718592),u(4718592),u(0),m(1),Array(32).fill(0),m(24),Ie(65535)],[wt[s.track.source.codec](s)]),tt=i=>i.info.decoderConfig&&p("avcC",[...P(i.info.decoderConfig.description)]),rt=i=>i.info.decoderConfig&&p("hvcC",[...P(i.info.decoderConfig.description)]),Se=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]),l=(Number(e[3])<<4)+(0<<1)+Number(s.colorSpace.fullRange);return h("vpcC",1,0,[b(t),b(r),b(l),b(2),b(2),b(2),m(0)])},it=()=>{let e=(1<<7)+1;return p("av1C",[e,0,0,0])},st=(i,s)=>p(i,[Array(6).fill(0),m(1),m(0),m(0),u(0),m(s.info.numberOfChannels),m(16),m(0),m(0),x(s.info.sampleRate)],[At[s.track.source.codec](s)]),nt=i=>{let s=P(i.info.decoderConfig.description??new ArrayBuffer(0));return h("esds",0,0,[u(58753152),b(32+s.byteLength),m(1),b(0),u(75530368),b(18+s.byteLength),b(64),b(21),Ae(0),u(130071),u(130071),u(92307584),b(s.byteLength),...s,u(109084800),b(1),b(2)])},ot=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),m(s),u(i.info.sampleRate),ce(e),b(0)])},at=i=>h("stts",0,0,[u(i.timeToSampleTable.length),i.timeToSampleTable.map(s=>[u(s.sampleCount),u(s.sampleDelta)])]),ut=i=>{if(i.samples.every(e=>e.type==="key"))return null;let s=[...i.samples.entries()].filter(([,e])=>e.type==="key");return h("stss",0,0,[u(s.length),s.map(([e])=>u(e+1))])},lt=i=>h("stsc",0,0,[u(i.compactlyCodedChunkTable.length),i.compactlyCodedChunkTable.map(s=>[u(s.firstChunk),u(s.samplesPerChunk),u(1)])]),dt=i=>h("stsz",0,0,[u(0),u(i.samples.length),i.samples.map(s=>u(s.size))]),ct=i=>i.finalizedChunks.length>0&&U(i.finalizedChunks).offset>=2**32?h("co64",0,0,[u(i.finalizedChunks.length),i.finalizedChunks.map(s=>z(s.offset))]):h("stco",0,0,[u(i.finalizedChunks.length),i.finalizedChunks.map(s=>u(s.offset))]),ft=i=>h("ctts",0,0,[u(i.compositionTimeOffsetTable.length),i.compositionTimeOffsetTable.map(s=>[u(s.sampleCount),u(s.sampleCompositionTimeOffset)])]),mt=i=>p("mvex",void 0,i.map(ht)),ht=i=>h("trex",0,0,[u(i.track.id),u(1),u(0),u(0),u(0)]),me=(i,s)=>p("moof",void 0,[pt(i),...s.map(bt)]),pt=i=>h("mfhd",0,0,[u(i)]),Pe=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},bt=i=>p("traf",void 0,[Ct(i),gt(i),kt(i)]),Ct=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:Pe(e)};return h("tfhd",0,s,[u(i.track.id),u(t.duration),u(t.size),u(t.flags)])},gt=i=>(d(i.currentChunk),h("tfdt",1,0,[z(T(i.currentChunk.startTimestamp,i.timescale))])),kt=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(Pe),r=i.currentChunk.samples.map(w=>T(w.timestamp-w.decodeTimestamp,i.timescale)),n=new Set(s),o=new Set(e),l=new Set(t),a=new Set(r),f=l.size===2&&t[0]!==t[1],C=n.size>1,g=o.size>1,A=!f&&l.size>1,Te=a.size>1||[...a].some(w=>w!==0),O=0;return O|=1,O|=4*+f,O|=256*+C,O|=512*+g,O|=1024*+A,O|=2048*+Te,h("trun",1,O,[u(i.currentChunk.samples.length),u(i.currentChunk.offset-i.currentChunk.moofOffset||0),f?u(t[0]):[],i.currentChunk.samples.map((w,K)=>[C?u(s[K]):[],g?u(e[K]):[],A?u(t[K]):[],Te?We(r[K]):[]])])},Ee=i=>p("mfra",void 0,[...i.map(Tt),xt()]),Tt=(i,s)=>h("tfra",1,0,[u(i.track.id),u(63),u(i.finalizedChunks.length),i.finalizedChunks.map(t=>[z(T(t.startTimestamp,i.timescale)),z(t.moofOffset),u(s+1),u(1),u(1)])]),xt=()=>h("mfro",0,0,[u(0)]),yt={avc:"avc1",hevc:"hvc1",vp8:"vp08",vp9:"vp09",av1:"av01"},wt={avc:tt,hevc:rt,vp8:Se,vp9:Se,av1:it},St={aac:"mp4a",opus:"Opus"},At={aac:nt,opus:ot};var M=class{constructor(s){this.output=s}beforeTrackAdd(s){}};var G=1e3,Ot=2082844800,T=(i,s,e=!0)=>{let t=i*s;return e?Math.round(t):t},q=class extends M{constructor(e,t){super(e);this.#i=new Uint8Array(8);this.#t=new DataView(this.#i.buffer);this.offsets=new WeakMap;this.#o=null;this.#n=null;this.#s=[];this.#a=Math.floor(Date.now()/1e3)+Ot;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(ze({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(l=>l.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.metadata.frameRate??57600,samples:[],sampleQueue:[],timestampProcessingQueue:[],firstTimestamp:null,lastKeyFrameTimestamp:null,timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.#s.push(o),this.#s.sort((l,a)=>l.track.id-a.track.id),o}#m(e,t,r){let n=this.#s.find(l=>l.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:[],timestampProcessingQueue:[],firstTimestamp:null,lastKeyFrameTimestamp:null,timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.#s.push(o),this.#s.sort((l,a)=>l.track.id-a.track.id),o}addEncodedVideoChunk(e,t,r){let n=this.#u(e,t,r);if(typeof this.#r.options.fastStart=="object"&&n.samples.length===this.#r.options.fastStart.expectedVideoChunks)throw new Error(`Cannot add more video chunks than specified in 'fastStart' (${this.#r.options.fastStart.expectedVideoChunks}).`);let o=this.#h(n,t);this.#r.options.fastStart==="fragmented"?(n.sampleQueue.push(o),this.#b()):this.#p(n,o)}addEncodedAudioChunk(e,t,r){let n=this.#m(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.#h(n,t);this.#r.options.fastStart==="fragmented"?(n.sampleQueue.push(o),this.#b()):this.#p(n,o)}#h(e,t){let r=this.#T(e,t),n=(t.duration??0)/1e6,o=new Uint8Array(t.byteLength);return t.copyTo(o),{timestamp:r,decodeTimestamp:r,duration:n,data:o,size:o.byteLength,type:t.type,timescaleUnitsToNextSample:T(n,e.timescale)}}#c(e){if(e.timestampProcessingQueue.length===0)return;let t=e.timestampProcessingQueue.map(r=>r.timestamp).sort((r,n)=>r-n);for(let r=0;r{if(e===l)return t.type==="key";let a=l.sampleQueue[0];return a&&a.type==="key"});n>=1&&o&&(r=!0,this.#k())}else r=n>=.5}r&&(e.currentChunk&&this.#g(e),e.currentChunk={startTimestamp:t.timestamp,samples:[],offset:null,moofOffset:null}),d(e.currentChunk),e.currentChunk.samples.push(t),e.timestampProcessingQueue.push(t)}#T(e,t){let r=t.timestamp/1e6;if(r<0)throw new Error(`Timestamps must be non-negative (got ${r}s).`);if(e.firstTimestamp===null&&(e.firstTimestamp=r),r-=e.firstTimestamp,e.lastKeyFrameTimestamp!==null&&r=2**32&&(a.largeSize=!0,C=this.measureBox(a)+f),a.size=C,this.writeBox(a)}for(let a of this.#s){a.currentChunk.offset=this.#e.getPos(),a.currentChunk.moofOffset=r;for(let f of a.currentChunk.samples)this.#e.write(f.data),f.data=null}let o=this.#e.getPos();this.#e.seek(this.offsets.get(n));let l=me(t,this.#s);this.writeBox(l),this.#e.seek(o);for(let a of this.#s)a.finalizedChunks.push(a.currentChunk),this.#l.push(a.currentChunk),a.currentChunk=null;e&&this.#e.flush()}finalize(){if(this.#r.options.fastStart==="fragmented"){for(let e of this.#s){for(let t of e.sampleQueue)this.#p(e,t);this.#c(e)}this.#k(!1)}else for(let e of this.#s)this.#c(e),this.#g(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 l=this.#e.getPos()+o+e;for(let a of this.#l){a.offset=l;for(let{data:f}of a.samples)d(f),l+=f.byteLength,e+=f.byteLength}if(l<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=Ee(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(Ue(n))}else this.writeBox(r)}}};var N=class{constructor(s){this.value=s}},F=class{constructor(s){this.value=s}},R=class{constructor(s){this.value=s}};var he=i=>i<256?1:i<65536?2:i<1<<24?3:i<2**32?4:i<2**40?5:6,pe=i=>i>=-64&&i<64?1:i>=-8192&&i<8192?2:i>=-(1<<20)&&i<1<<20?3:i>=-(1<<27)&&i<1<<27?4:i>=-(2**34)&&i<2**34?5:6,Me=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 vt=1,Vt=2,be=2**15,Fe="https://github.com/Vanilagy/webm-muxer",Be=6,_e=5,zt={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"},X=class extends M{constructor(e,t){super(e);this.#i=new Uint8Array(8);this.#t=new DataView(this.#i.buffer);this.offsets=new WeakMap;this.dataOffsets=new WeakMap;this.#o=[];this.#n=null;this.#s=null;this.#a=null;this.#l=null;this.#f=null;this.#d=null;this.#u=null;this.#m=null;this.#h=new Set;this.#c=0;this.#e=e.writer,this.#r=t}#e;#r;#i;#t;#o;#n;#s;#a;#l;#f;#d;#u;#m;#h;#c;#p(e){this.#t.setUint8(0,e),this.#e.write(this.#i.subarray(0,1))}#T(e){this.#t.setFloat32(0,e,!1),this.#e.write(this.#i.subarray(0,4))}#g(e){this.#t.setFloat64(0,e,!1),this.#e.write(this.#i)}#b(e,t=he(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))}#k(e,t=pe(e)){e<0&&(e+=2**(t*8)),this.#b(e,t)}writeEBMLVarInt(e,t=Me(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))}#A(e){this.#e.write(new Uint8Array(e.split("").map(t=>t.charCodeAt(0))))}writeEBML(e){if(e!==null){if(e instanceof Uint8Array)this.#e.write(e);else if(Array.isArray(e))for(let t of e)this.writeEBML(t);else if(this.offsets.set(e,this.#e.getPos()),this.#b(e.id),Array.isArray(e.data)){let t=this.#e.getPos(),r=e.size===-1?1:e.size??4;e.size===-1?this.#p(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,l=this.#e.getPos();this.#e.seek(t),this.writeEBMLVarInt(o,r),this.#e.seek(l)}}else if(typeof e.data=="number"){let t=e.size??he(e.data);this.writeEBMLVarInt(t),this.#b(e.data,t)}else if(typeof e.data=="string")this.writeEBMLVarInt(e.data.length),this.#A(e.data);else if(e.data instanceof Uint8Array)this.writeEBMLVarInt(e.data.byteLength,e.size),this.#e.write(e.data);else if(e.data instanceof N)this.writeEBMLVarInt(4),this.#T(e.data.value);else if(e.data instanceof F)this.writeEBMLVarInt(8),this.#g(e.data.value);else if(e.data instanceof R){let t=e.size??pe(e.data.value);this.writeEBMLVarInt(t),this.#k(e.data.value,t)}}}beforeTrackAdd(e){if(this.#r instanceof B){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.#O(),this.#r.options.streaming||this.#v(),this.#V(),this.#P(),this.#e.flush()}#O(){let e={id:440786851,data:[{id:17030,data:1},{id:17143,data:1},{id:17138,data:4},{id:17139,data:8},{id:17026,data:this.#r instanceof B?"webm":"matroska"},{id:17031,data:2},{id:17029,data:2}]};this.writeEBML(e)}#v(){let e=new Uint8Array([28,83,187,107]),t=new Uint8Array([21,73,169,102]),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:Fe},{id:22337,data:Fe},this.#r.options.streaming?null:e]};this.#s=t}#z(){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"?vt:Vt},{id:134,data:zt[t.track.source.codec]},t.info.decoderConfig.description?{id:25506,data:P(t.info.decoderConfig.description)}:null,...t.type==="video"?[t.track.metadata.frameRate?{id:2352003,data:1e9/t.track.metadata.frameRate}:null,{id:224,data:[{id:176,data:t.info.width},{id:186,data:t.info.height},(()=>{if(t.info.decoderConfig.colorSpace){let 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 N(t.info.sampleRate)},{id:159,data:t.info.numberOfChannels}]}]:[]]})}#U(){let e={id:408125543,size:this.#r.options.streaming?-1:Be,data:[this.#r.options.streaming?null:this.#a,this.#s,this.#l]};this.#n=e,this.writeEBML(e)}#P(){this.#d={id:475249515,data:[]}}get#C(){return d(this.#n),this.dataOffsets.get(this.#n)}#E(e,t,r){let n=this.#o.find(l=>l.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,lastKeyFrameTimestamp:null,lastWrittenTimestamp:null};return this.#o.push(o),this.#o.sort((l,a)=>l.track.id-a.track.id),o}#M(e,t,r){let n=this.#o.find(l=>l.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,lastKeyFrameTimestamp:null,lastWrittenTimestamp:null};return this.#o.push(o),this.#o.sort((l,a)=>l.track.id-a.track.id),o}addEncodedVideoChunk(e,t,r){let n=this.#E(e,t,r),o=this.#y(n,t);e.source.codec==="vp9"&&this.#F(n,o),n.chunkQueue.push(o),this.#x(),this.#e.flush()}addEncodedAudioChunk(e,t,r){let n=this.#M(e,t,r),o=this.#y(n,t);n.chunkQueue.push(o),this.#x(),this.#e.flush()}#x(){if(!(this.#o.length=2&&r++;let f={rgb:7,bt709:2,bt470bg:1,smpte170m:3}[e.info.decoderConfig.colorSpace.matrix];we(t.data,r+0,r+3,f)}#y(e,t){let r=this.#B(e,t),n=new Uint8Array(t.byteLength);return t.copyTo(n),{data:n,type:t.type,timestamp:r,duration:t.duration,additions:null}}#B(e,t){let r=t.timestamp;if(r<0)throw new Error(`Timestamps must be non-negative (got ${r}s).`);if(e.firstTimestamp===null&&(e.firstTimestamp=r),r-=e.firstTimestamp,e.lastKeyFrameTimestamp!==null&&r{if(e===g)return t.type==="key";let A=g.chunkQueue[0];return A&&A.type==="key"});(!this.#u||n&&r-this.#m>=1e3)&&this.#_(r);let o=r-this.#m;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 a=new Uint8Array(4),f=new DataView(a.buffer);f.setUint8(0,128|e.track.id),f.setInt16(1,o,!1);let C=Math.floor((t.duration??0)/1e3);if(C===0&&!t.additions){f.setUint8(3,+(t.type==="key")<<7);let g={id:163,data:[a,t.data]};this.writeEBML(g)}else{let g={id:160,data:[{id:161,data:[a,t.data]},t.type==="delta"?{id:251,data:new R(e.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.#h.add(e)}#_(e){this.#u&&!this.#r.options.streaming&&this.#S(),this.#u={id:524531317,size:this.#r.options.streaming?-1:_e,data:[{id:231,data:e}]},this.writeEBML(this.#u),this.#m=e,this.#h.clear()}#S(){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,_e),this.#e.seek(t);let r=this.offsets.get(this.#u)-this.#C;d(this.#d),this.#d.data.push({id:187,data:[{id:179,data:this.#m},...[...this.#h].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.#w(e,e.chunkQueue.shift());if(this.#r.options.streaming||this.#S(),d(this.#d),this.writeEBML(this.#d),!this.#r.options.streaming){let e=this.#e.getPos(),t=this.#e.getPos()-this.#C;this.#e.seek(this.offsets.get(this.#n)+4),this.writeEBMLVarInt(t,Be),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.#C,this.#a.data[1].data[1].data=this.offsets.get(this.#s)-this.#C,this.#a.data[2].data[1].data=this.offsets.get(this.#l)-this.#C,this.#e.seek(this.offsets.get(this.#a)),this.writeEBML(this.#a),this.#e.seek(e)}}};var Y=class{},Ce=class extends Y{constructor(e){super();this.options=e}createMuxer(e){return new q(e,this)}},L=class extends Y{constructor(e={}){super();this.options=e}createMuxer(e){return new X(e,this)}},B=class extends L{};var Q=class{},Z=class extends Q{#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;ta.start<=e&&ePt){for(let a=0;a=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 Et=Symbol("isTarget");Et;var _=class{},ge=class extends _{constructor(){super(...arguments);this.buffer=null}createWriter(){return new Z(this)}},$=class extends _{constructor(e){super();this.options=e;if(typeof e!="object")throw new TypeError("StreamTarget requires an options object to be passed to its constructor.");if(e.onData){if(typeof e.onData!="function")throw new TypeError("options.onData, when provided, must be a function.");if(e.onData.length<2)throw new TypeError("options.onData, when provided, must be a function that takes in at least two arguments (data and position). Ignoring the position argument, which specifies the byte offset at which the data is to be written, can lead to broken outputs.")}if(e.chunked!==void 0&&typeof e.chunked!="boolean")throw new TypeError("options.chunked, when provided, must be a boolean.");if(e.chunkSize!==void 0&&(!Number.isInteger(e.chunkSize)||e.chunkSize<=0))throw new TypeError("options.chunkSize, when provided, must be a positive integer.")}createWriter(){return this.options.chunked?new H(this):new J(this)}},ke=class extends _{constructor(e,t){super();this.stream=e;this.options=t;if(!(e instanceof FileSystemWritableFileStream))throw new TypeError("FileSystemWritableFileStreamTarget requires a FileSystemWritableFileStream instance.");if(t!==void 0&&typeof t!="object")throw new TypeError("FileSystemWritableFileStreamTarget's options, when provided, must be an object.");if(t&&t.chunkSize!==void 0&&(!Number.isInteger(t.chunkSize)||t.chunkSize<=0))throw new TypeError("options.chunkSize, when provided, must be a positive integer")}createWriter(){return new ee(this)}};export{ge as ArrayBufferTarget,ae as AudioBufferSource,oe as AudioDataSource,ie as CanvasSource,ne as EncodedAudioChunkSource,te as EncodedVideoChunkSource,ke as FileSystemWritableFileStreamTarget,ue as MediaStreamAudioTrackSource,se as MediaStreamVideoTrackSource,L as MkvOutputFormat,Ce as Mp4OutputFormat,le as Output,$ as StreamTarget,_ as Target,re as VideoFrameSource,B as WebMOutputFormat}; diff --git a/dist/metamuxer.mjs b/dist/metamuxer.mjs index efc04d7..f179c5f 100644 --- a/dist/metamuxer.mjs +++ b/dist/metamuxer.mjs @@ -133,10 +133,30 @@ var toUint8Array = (source) => { // src/source.ts var VideoSource = class { - constructor(codec, metadata) { + constructor(codec) { + this.connectedTrack = null; + this.codec = codec; + } + ensureValidDigest() { + if (!this.connectedTrack) { + throw new Error("Cannot call digest without connecting the source to an output track."); + } + if (!this.connectedTrack.output.started) { + throw new Error("Cannot call digest before output has been started."); + } + if (this.connectedTrack.output.finalizing) { + throw new Error("Cannot call digest after output has started finalizing."); + } + } + start() { + } + async flush() { + } +}; +var AudioSource = class { + constructor(codec) { this.connectedTrack = null; this.codec = codec; - this.metadata = metadata; } ensureNotFinalizing() { if (this.connectedTrack?.output.finalizing) { @@ -148,20 +168,14 @@ var VideoSource = class { async flush() { } }; -var AudioSource = class { - constructor(codec, metadata) { - this.connectedTrack = null; - this.codec = codec; - this.metadata = metadata; +var EncodedVideoChunkSource = class extends VideoSource { + constructor(codec) { + super(codec); } - ensureNotFinalizing() { - if (this.connectedTrack?.output.finalizing) { - throw new Error("Cannot call digest after output has started finalizing."); - } - } - start() { - } - async flush() { + // TODO: Ensure that the first chunk is a key frame (same for the audio case) + digest(chunk, meta) { + this.ensureValidDigest(); + this.connectedTrack?.output.muxer.addEncodedVideoChunk(this.connectedTrack, chunk, meta); } }; var KEY_FRAME_INTERVAL = 5; @@ -172,8 +186,9 @@ var VideoEncoderWrapper = class { this.encoder = null; this.lastMultipleOfKeyFrameInterval = -1; } + // TODO: Ensure video frame size remains constant digest(videoFrame) { - this.source.ensureNotFinalizing(); + this.source.ensureValidDigest(); this.ensureEncoder(videoFrame); assert(this.encoder); const multipleOfKeyFrameInterval = Math.floor(videoFrame.timestamp / 1e6 / KEY_FRAME_INTERVAL); @@ -201,8 +216,8 @@ var VideoEncoderWrapper = class { } }; var VideoFrameSource = class extends VideoSource { - constructor(codecConfig, options = {}) { - super(codecConfig.codec, options); + constructor(codecConfig) { + super(codecConfig.codec); this.encoder = new VideoEncoderWrapper(this, codecConfig); } digest(videoFrame) { @@ -213,8 +228,8 @@ var VideoFrameSource = class extends VideoSource { } }; var CanvasSource = class extends VideoSource { - constructor(canvas, codecConfig, options = {}) { - super(codecConfig.codec, options); + constructor(canvas, codecConfig) { + super(codecConfig.codec); this.canvas = canvas; this.encoder = new VideoEncoderWrapper(this, codecConfig); } @@ -231,8 +246,8 @@ var CanvasSource = class extends VideoSource { } }; var MediaStreamVideoTrackSource = class extends VideoSource { - constructor(track, codecConfig, options = {}) { - super(codecConfig.codec, options); + constructor(track, codecConfig) { + super(codecConfig.codec); this.track = track; this.abortController = null; this.encoder = new VideoEncoderWrapper(this, codecConfig); @@ -261,12 +276,22 @@ var MediaStreamVideoTrackSource = class extends VideoSource { await this.encoder.flush(); } }; +var EncodedAudioChunkSource = class extends AudioSource { + constructor(codec) { + super(codec); + } + digest(chunk, meta) { + this.ensureNotFinalizing(); + this.connectedTrack?.output.muxer.addEncodedAudioChunk(this.connectedTrack, chunk, meta); + } +}; var AudioEncoderWrapper = class { constructor(source, codecConfig) { this.source = source; this.codecConfig = codecConfig; this.encoder = null; } + // TODO: Ensure audio parameters remain constant digest(audioData) { this.source.ensureNotFinalizing(); this.ensureEncoder(audioData); @@ -294,8 +319,8 @@ var AudioEncoderWrapper = class { } }; var AudioDataSource = class extends AudioSource { - constructor(codecConfig, options = {}) { - super(codecConfig.codec, options); + constructor(codecConfig) { + super(codecConfig.codec); this.encoder = new AudioEncoderWrapper(this, codecConfig); } digest(audioData) { @@ -306,8 +331,8 @@ var AudioDataSource = class extends AudioSource { } }; var AudioBufferSource = class extends AudioSource { - constructor(codecConfig, options = {}) { - super(codecConfig.codec, options); + constructor(codecConfig) { + super(codecConfig.codec); this.accumulatedFrameCount = 0; this.encoder = new AudioEncoderWrapper(this, codecConfig); } @@ -337,8 +362,8 @@ var AudioBufferSource = class extends AudioSource { } }; var MediaStreamAudioTrackSource = class extends AudioSource { - constructor(track, codecConfig, options = {}) { - super(codecConfig.codec, options); + constructor(track, codecConfig) { + super(codecConfig.codec); this.track = track; this.abortController = null; this.encoder = new AudioEncoderWrapper(this, codecConfig); @@ -377,7 +402,7 @@ var Output = class { this.writer = options.target.createWriter(); this.muxer = options.format.createMuxer(this); } - addTrack(source) { + addTrack(source, metadata = {}) { if (this.started) { throw new Error("Cannot add track after output has started."); } @@ -388,7 +413,8 @@ var Output = class { id: this.tracks.length + 1, output: this, type: source instanceof VideoSource ? "video" : "audio", - source + source, + metadata }; this.muxer.beforeTrackAdd(track); this.tracks.push(track); @@ -468,7 +494,7 @@ var ascii = (text, nullTerminated = false) => { var lastPresentedSample = (samples) => { let result = null; for (let sample of samples) { - if (!result || sample.presentationTimestamp > result.presentationTimestamp) { + if (!result || sample.timestamp > result.timestamp) { result = sample; } } @@ -549,7 +575,7 @@ var mvhd = (creationTime, trackDatas) => { 0, ...trackDatas.filter((x) => x.samples.length > 0).map((x) => { const lastSample = lastPresentedSample(x.samples); - return lastSample.presentationTimestamp + lastSample.duration; + return lastSample.timestamp + lastSample.duration; }) ), GLOBAL_TIMESCALE); let nextTrackId = Math.max(...trackDatas.map((x) => x.track.id)) + 1; @@ -585,14 +611,14 @@ var trak = (trackData, creationTime) => box("trak", void 0, [ var tkhd = (trackData, creationTime) => { let lastSample = lastPresentedSample(trackData.samples); let durationInGlobalTimescale = intoTimescale( - lastSample ? lastSample.presentationTimestamp + lastSample.duration : 0, + lastSample ? lastSample.timestamp + lastSample.duration : 0, GLOBAL_TIMESCALE ); let needsU64 = !isU32(creationTime) || !isU32(durationInGlobalTimescale); let u32OrU64 = needsU64 ? u64 : u32; let matrix; if (trackData.type === "video") { - const rotation = trackData.track.source.metadata.rotation; + const rotation = trackData.track.metadata.rotation; matrix = rotation === void 0 || typeof rotation === "number" ? rotationMatrix(rotation ?? 0) : rotation; } else { matrix = IDENTITY_MATRIX; @@ -634,7 +660,7 @@ var mdia = (trackData, creationTime) => box("mdia", void 0, [ var mdhd = (trackData, creationTime) => { let lastSample = lastPresentedSample(trackData.samples); let localDuration = intoTimescale( - lastSample ? lastSample.presentationTimestamp + lastSample.duration : 0, + lastSample ? lastSample.timestamp + lastSample.duration : 0, trackData.timescale ); let needsU64 = !isU32(creationTime) || !isU32(localDuration); @@ -1059,7 +1085,7 @@ var trun = (trackData) => { let allSampleDurations = trackData.currentChunk.samples.map((x) => x.timescaleUnitsToNextSample); let allSampleSizes = trackData.currentChunk.samples.map((x) => x.size); let allSampleFlags = trackData.currentChunk.samples.map(fragmentSampleFlags); - let allSampleCompositionTimeOffsets = trackData.currentChunk.samples.map((x) => intoTimescale(x.presentationTimestamp - x.decodeTimestamp, trackData.timescale)); + let allSampleCompositionTimeOffsets = trackData.currentChunk.samples.map((x) => intoTimescale(x.timestamp - x.decodeTimestamp, trackData.timescale)); let uniqueSampleDurations = new Set(allSampleDurations); let uniqueSampleSizes = new Set(allSampleSizes); let uniqueSampleFlags = new Set(allSampleFlags); @@ -1320,11 +1346,12 @@ var IsobmffMuxer = class extends Muxer { height: meta.decoderConfig.codedHeight, decoderConfig: meta.decoderConfig }, - timescale: track.source.metadata.frameRate ?? 57600, + timescale: track.metadata.frameRate ?? 57600, samples: [], sampleQueue: [], - firstDecodeTimestamp: null, - lastDecodeTimestamp: -1, + timestampProcessingQueue: [], + firstTimestamp: null, + lastKeyFrameTimestamp: null, timeToSampleTable: [], compositionTimeOffsetTable: [], lastTimescaleUnits: null, @@ -1355,8 +1382,9 @@ var IsobmffMuxer = class extends Muxer { timescale: meta.decoderConfig.sampleRate, samples: [], sampleQueue: [], - firstDecodeTimestamp: null, - lastDecodeTimestamp: -1, + timestampProcessingQueue: [], + firstTimestamp: null, + lastKeyFrameTimestamp: null, timeToSampleTable: [], compositionTimeOffsetTable: [], lastTimescaleUnits: null, @@ -1369,12 +1397,12 @@ var IsobmffMuxer = class extends Muxer { this.#trackDatas.sort((a, b) => a.track.id - b.track.id); return newTrackData; } - addEncodedVideoChunk(track, chunk, meta, compositionTimeOffset) { + addEncodedVideoChunk(track, chunk, meta) { const trackData = this.#getVideoTrackData(track, chunk, meta); if (typeof this.#format.options.fastStart === "object" && trackData.samples.length === this.#format.options.fastStart.expectedVideoChunks) { throw new Error(`Cannot add more video chunks than specified in 'fastStart' (${this.#format.options.fastStart.expectedVideoChunks}).`); } - let videoSample = this.#createSampleForTrack(trackData, chunk, compositionTimeOffset); + let videoSample = this.#createSampleForTrack(trackData, chunk); if (this.#format.options.fastStart === "fragmented") { trackData.sampleQueue.push(videoSample); this.#interleaveSamples(); @@ -1395,18 +1423,15 @@ var IsobmffMuxer = class extends Muxer { this.#addSampleToTrack(trackData, audioSample); } } - #createSampleForTrack(trackData, chunk, compositionTimeOffset) { - let presentationTimestampInSeconds = chunk.timestamp / 1e6; - let decodeTimestampInSeconds = (chunk.timestamp - (compositionTimeOffset ?? 0)) / 1e6; + #createSampleForTrack(trackData, chunk) { + let timestampInSeconds = this.#validateTimestamp(trackData, chunk); let durationInSeconds = (chunk.duration ?? 0) / 1e6; - let adjusted = this.#validateTimestamp(trackData, presentationTimestampInSeconds, decodeTimestampInSeconds); - presentationTimestampInSeconds = adjusted.presentationTimestamp; - decodeTimestampInSeconds = adjusted.decodeTimestamp; let data = new Uint8Array(chunk.byteLength); chunk.copyTo(data); let sample = { - presentationTimestamp: presentationTimestampInSeconds, - decodeTimestamp: decodeTimestampInSeconds, + timestamp: timestampInSeconds, + decodeTimestamp: timestampInSeconds, + // We may refine this later duration: durationInSeconds, data, size: data.byteLength, @@ -1416,62 +1441,76 @@ var IsobmffMuxer = class extends Muxer { }; return sample; } - #addSampleToTrack(trackData, sample) { - if (this.#format.options.fastStart !== "fragmented") { - trackData.samples.push(sample); + #processTimestamps(trackData) { + if (trackData.timestampProcessingQueue.length === 0) { + return; } - const sampleCompositionTimeOffset = intoTimescale(sample.presentationTimestamp - sample.decodeTimestamp, trackData.timescale); - if (trackData.lastTimescaleUnits !== null) { - assert(trackData.lastSample); - let timescaleUnits = intoTimescale(sample.decodeTimestamp, trackData.timescale, false); - let delta = Math.round(timescaleUnits - trackData.lastTimescaleUnits); - trackData.lastTimescaleUnits += delta; - trackData.lastSample.timescaleUnitsToNextSample = delta; - if (this.#format.options.fastStart !== "fragmented") { - let lastTableEntry = last(trackData.timeToSampleTable); - assert(lastTableEntry); - if (lastTableEntry.sampleCount === 1) { - lastTableEntry.sampleDelta = delta; - lastTableEntry.sampleCount++; - } else if (lastTableEntry.sampleDelta === delta) { - lastTableEntry.sampleCount++; - } else { - lastTableEntry.sampleCount--; - trackData.timeToSampleTable.push({ - sampleCount: 2, - sampleDelta: delta - }); + const sortedTimestamps = trackData.timestampProcessingQueue.map((x) => x.timestamp).sort((a, b) => a - b); + for (let i = 0; i < trackData.timestampProcessingQueue.length; i++) { + const sample = trackData.timestampProcessingQueue[i]; + sample.decodeTimestamp = sortedTimestamps[i]; + const sampleCompositionTimeOffset = intoTimescale(sample.timestamp - sample.decodeTimestamp, trackData.timescale); + if (trackData.lastTimescaleUnits !== null) { + assert(trackData.lastSample); + let timescaleUnits = intoTimescale(sample.decodeTimestamp, trackData.timescale, false); + let delta = Math.round(timescaleUnits - trackData.lastTimescaleUnits); + trackData.lastTimescaleUnits += delta; + trackData.lastSample.timescaleUnitsToNextSample = delta; + if (this.#format.options.fastStart !== "fragmented") { + let lastTableEntry = last(trackData.timeToSampleTable); + assert(lastTableEntry); + if (lastTableEntry.sampleCount === 1) { + lastTableEntry.sampleDelta = delta; + lastTableEntry.sampleCount++; + } else if (lastTableEntry.sampleDelta === delta) { + lastTableEntry.sampleCount++; + } else { + lastTableEntry.sampleCount--; + trackData.timeToSampleTable.push({ + sampleCount: 2, + sampleDelta: delta + }); + } + const lastCompositionTimeOffsetTableEntry = last(trackData.compositionTimeOffsetTable); + assert(lastCompositionTimeOffsetTableEntry); + if (lastCompositionTimeOffsetTableEntry.sampleCompositionTimeOffset === sampleCompositionTimeOffset) { + lastCompositionTimeOffsetTableEntry.sampleCount++; + } else { + trackData.compositionTimeOffsetTable.push({ + sampleCount: 1, + sampleCompositionTimeOffset + }); + } } - const lastCompositionTimeOffsetTableEntry = last(trackData.compositionTimeOffsetTable); - assert(lastCompositionTimeOffsetTableEntry); - if (lastCompositionTimeOffsetTableEntry.sampleCompositionTimeOffset === sampleCompositionTimeOffset) { - lastCompositionTimeOffsetTableEntry.sampleCount++; - } else { + } else { + trackData.lastTimescaleUnits = 0; + if (this.#format.options.fastStart !== "fragmented") { + trackData.timeToSampleTable.push({ + sampleCount: 1, + sampleDelta: intoTimescale(sample.duration, trackData.timescale) + }); trackData.compositionTimeOffsetTable.push({ sampleCount: 1, sampleCompositionTimeOffset }); } } - } else { - trackData.lastTimescaleUnits = 0; - if (this.#format.options.fastStart !== "fragmented") { - trackData.timeToSampleTable.push({ - sampleCount: 1, - sampleDelta: intoTimescale(sample.duration, trackData.timescale) - }); - trackData.compositionTimeOffsetTable.push({ - sampleCount: 1, - sampleCompositionTimeOffset - }); - } + trackData.lastSample = sample; + } + trackData.timestampProcessingQueue.length = 0; + } + #addSampleToTrack(trackData, sample) { + if (sample.type === "key") { + this.#processTimestamps(trackData); + } + if (this.#format.options.fastStart !== "fragmented") { + trackData.samples.push(sample); } - trackData.lastSample = sample; let beginNewChunk = false; if (!trackData.currentChunk) { beginNewChunk = true; } else { - let currentChunkDuration = sample.presentationTimestamp - trackData.currentChunk.startTimestamp; + let currentChunkDuration = sample.timestamp - trackData.currentChunk.startTimestamp; if (this.#format.options.fastStart === "fragmented") { const keyFrameQueuedEverywhere = this.#trackDatas.every((otherTrackData) => { if (trackData === otherTrackData) { @@ -1493,7 +1532,7 @@ var IsobmffMuxer = class extends Muxer { this.#finalizeCurrentChunk(trackData); } trackData.currentChunk = { - startTimestamp: sample.presentationTimestamp, + startTimestamp: sample.timestamp, samples: [], offset: null, moofOffset: null @@ -1501,23 +1540,24 @@ var IsobmffMuxer = class extends Muxer { } assert(trackData.currentChunk); trackData.currentChunk.samples.push(sample); + trackData.timestampProcessingQueue.push(sample); } - #validateTimestamp(trackData, presentationTimestamp, decodeTimestamp) { - if (decodeTimestamp < 0) { - throw new Error(`Timestamps must be non-negative (got ${decodeTimestamp}s).`); + #validateTimestamp(trackData, chunk) { + let timestampInSeconds = chunk.timestamp / 1e6; + if (timestampInSeconds < 0) { + throw new Error(`Timestamps must be non-negative (got ${timestampInSeconds}s).`); } - if (trackData.firstDecodeTimestamp === null) { - trackData.firstDecodeTimestamp = decodeTimestamp; + if (trackData.firstTimestamp === null) { + trackData.firstTimestamp = timestampInSeconds; } - decodeTimestamp -= trackData.firstDecodeTimestamp; - presentationTimestamp -= trackData.firstDecodeTimestamp; - if (decodeTimestamp < trackData.lastDecodeTimestamp) { - throw new Error( - `Timestamps must be monotonically increasing (timestamp went from ${trackData.lastDecodeTimestamp}s to ${decodeTimestamp}s).` - ); + timestampInSeconds -= trackData.firstTimestamp; + if (trackData.lastKeyFrameTimestamp !== null && timestampInSeconds < trackData.lastKeyFrameTimestamp) { + throw new Error(`Timestamp cannot be before last key frame's timestamp (got ${timestampInSeconds}s, last key frame at ${trackData.lastKeyFrameTimestamp}s).`); } - trackData.lastDecodeTimestamp = decodeTimestamp; - return { presentationTimestamp, decodeTimestamp }; + if (chunk.type === "key") { + trackData.lastKeyFrameTimestamp = timestampInSeconds; + } + return timestampInSeconds; } #finalizeCurrentChunk(trackData) { assert(this.#format.options.fastStart !== "fragmented"); @@ -1550,22 +1590,22 @@ var IsobmffMuxer = class extends Muxer { } outer: while (true) { - let trackWithMinDecodeTimestamp = null; - let minDecodeTimestamp = Infinity; + let trackWithMinTimestamp = null; + let minTimestamp = Infinity; for (let trackData of this.#trackDatas) { if (trackData.sampleQueue.length === 0) { break outer; } - if (trackData.sampleQueue[0].decodeTimestamp < minDecodeTimestamp) { - trackWithMinDecodeTimestamp = trackData; - minDecodeTimestamp = trackData.sampleQueue[0].decodeTimestamp; + if (trackData.sampleQueue[0].timestamp < minTimestamp) { + trackWithMinTimestamp = trackData; + minTimestamp = trackData.sampleQueue[0].timestamp; } } - if (!trackWithMinDecodeTimestamp) { + if (!trackWithMinTimestamp) { break; } - let sample = trackWithMinDecodeTimestamp.sampleQueue.shift(); - this.#addSampleToTrack(trackWithMinDecodeTimestamp, sample); + let sample = trackWithMinTimestamp.sampleQueue.shift(); + this.#addSampleToTrack(trackWithMinTimestamp, sample); } } #finalizeFragment(flushWriter = true) { @@ -1624,10 +1664,12 @@ var IsobmffMuxer = class extends Muxer { for (let sample of trackData.sampleQueue) { this.#addSampleToTrack(trackData, sample); } + this.#processTimestamps(trackData); } this.#finalizeFragment(false); } else { for (let trackData of this.#trackDatas) { + this.#processTimestamps(trackData); this.#finalizeCurrentChunk(trackData); } } @@ -1701,6 +1743,11 @@ var EBMLFloat64 = class { this.value = value; } }; +var EBMLSignedInt = class { + constructor(value) { + this.value = value; + } +}; var measureUnsignedInt = (value) => { if (value < 1 << 8) { return 1; @@ -1716,6 +1763,21 @@ var measureUnsignedInt = (value) => { return 6; } }; +var measureSignedInt = (value) => { + if (value >= -(1 << 6) && value < 1 << 6) { + return 1; + } else if (value >= -(1 << 13) && value < 1 << 13) { + return 2; + } else if (value >= -(1 << 20) && value < 1 << 20) { + return 3; + } else if (value >= -(1 << 27) && value < 1 << 27) { + return 4; + } else if (value >= -(2 ** 34) && value < 2 ** 34) { + return 5; + } else { + return 6; + } +}; var measureEBMLVarInt = (value) => { if (value < (1 << 7) - 1) { return 1; @@ -1825,6 +1887,12 @@ var MatroskaMuxer = class extends Muxer { } this.#writer.write(this.#helper.subarray(0, pos)); } + #writeSignedInt(value, width = measureSignedInt(value)) { + if (value < 0) { + value += 2 ** (width * 8); + } + this.#writeUnsignedInt(value, width); + } writeEBMLVarInt(value, width = measureEBMLVarInt(value)) { let pos = 0; switch (width) { @@ -1915,6 +1983,10 @@ var MatroskaMuxer = class extends Muxer { } else if (data.data instanceof EBMLFloat64) { this.writeEBMLVarInt(8); this.#writeFloat64(data.data.value); + } else if (data.data instanceof EBMLSignedInt) { + let size = data.size ?? measureSignedInt(data.data.value); + this.writeEBMLVarInt(size); + this.#writeSignedInt(data.data.value, size); } } } @@ -2000,7 +2072,7 @@ var MatroskaMuxer = class extends Muxer { { 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, + trackData.track.metadata.frameRate ? { id: 2352003 /* DefaultDuration */, data: 1e9 / trackData.track.metadata.frameRate } : null, { id: 224 /* Video */, data: [ { id: 176 /* PixelWidth */, data: trackData.info.width }, { id: 186 /* PixelHeight */, data: trackData.info.height }, @@ -2083,7 +2155,7 @@ var MatroskaMuxer = class extends Muxer { }, chunkQueue: [], firstTimestamp: null, - lastTimestamp: null, + lastKeyFrameTimestamp: null, lastWrittenTimestamp: null }; this.#trackDatas.push(newTrackData); @@ -2107,18 +2179,17 @@ var MatroskaMuxer = class extends Muxer { }, chunkQueue: [], firstTimestamp: null, - lastTimestamp: null, + lastKeyFrameTimestamp: 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) { + addEncodedVideoChunk(track, chunk, meta) { 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(); @@ -2126,7 +2197,6 @@ var MatroskaMuxer = class extends Muxer { 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(); @@ -2257,7 +2327,7 @@ var MatroskaMuxer = class extends Muxer { */ /** Converts a read-only external chunk into an internal one for easier use. */ #createInternalChunk(trackData, chunk) { - let adjustedTimestamp = this.#validateTimestamp(trackData, chunk.timestamp); + let adjustedTimestamp = this.#validateTimestamp(trackData, chunk); let data = new Uint8Array(chunk.byteLength); chunk.copyTo(data); let internalChunk = { @@ -2269,7 +2339,8 @@ var MatroskaMuxer = class extends Muxer { }; return internalChunk; } - #validateTimestamp(trackData, timestamp) { + #validateTimestamp(trackData, chunk) { + let timestamp = chunk.timestamp; if (timestamp < 0) { throw new Error(`Timestamps must be non-negative (got ${timestamp}s).`); } @@ -2277,10 +2348,11 @@ var MatroskaMuxer = class extends Muxer { 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).` - ); + if (trackData.lastKeyFrameTimestamp !== null && timestamp < trackData.lastKeyFrameTimestamp) { + throw new Error(`Timestamp cannot be before last key frame's timestamp (got ${timestamp / 1e6}s, last key frame at ${trackData.lastKeyFrameTimestamp / 1e6}s).`); + } + if (chunk.type === "key") { + trackData.lastKeyFrameTimestamp = timestamp; } return timestamp; } @@ -2329,7 +2401,7 @@ var MatroskaMuxer = class extends Muxer { prelude, chunk.data ] }, - chunk.type === "delta" ? { id: 251 /* ReferenceBlock */, data: trackData.lastWrittenTimestamp - msTimestamp } : null, + chunk.type === "delta" ? { id: 251 /* ReferenceBlock */, data: new EBMLSignedInt(trackData.lastWrittenTimestamp - msTimestamp) } : null, chunk.duration !== null ? { id: 155 /* BlockDuration */, data: msDuration } : null, chunk.additions ? { id: 30113 /* BlockAdditions */, data: chunk.additions } : null ] }; @@ -2713,6 +2785,8 @@ export { AudioBufferSource, AudioDataSource, CanvasSource, + EncodedAudioChunkSource, + EncodedVideoChunkSource, FileSystemWritableFileStreamTarget2 as FileSystemWritableFileStreamTarget, MediaStreamAudioTrackSource, MediaStreamVideoTrackSource, diff --git a/src/index.ts b/src/index.ts index e8b9a14..3c2c242 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ export { Output } from './output'; export { Mp4OutputFormat, MkvOutputFormat, WebMOutputFormat } from './output_format'; -export { VideoFrameSource, CanvasSource, MediaStreamVideoTrackSource, AudioDataSource, AudioBufferSource, MediaStreamAudioTrackSource } from './source'; +export { EncodedVideoChunkSource, VideoFrameSource, CanvasSource, MediaStreamVideoTrackSource, EncodedAudioChunkSource, 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 294e69a..dd6cded 100644 --- a/src/isobmff/isobmff_boxes.ts +++ b/src/isobmff/isobmff_boxes.ts @@ -65,7 +65,7 @@ const lastPresentedSample = (samples: Sample[]) => { let result: Sample | null = null; for (let sample of samples) { - if (!result || sample.presentationTimestamp > result.presentationTimestamp) { + if (!result || sample.timestamp > result.timestamp) { result = sample; } } @@ -185,7 +185,7 @@ export const mvhd = ( filter(x => x.samples.length > 0). map(x => { const lastSample = lastPresentedSample(x.samples)!; - return lastSample.presentationTimestamp + lastSample.duration; + return lastSample.timestamp + lastSample.duration; }) ), GLOBAL_TIMESCALE); let nextTrackId = Math.max(...trackDatas.map(x => x.track.id)) + 1; @@ -225,7 +225,7 @@ export const tkhd = ( ) => { let lastSample = lastPresentedSample(trackData.samples); let durationInGlobalTimescale = intoTimescale( - lastSample ? lastSample.presentationTimestamp + lastSample.duration : 0, + lastSample ? lastSample.timestamp + lastSample.duration : 0, GLOBAL_TIMESCALE ); @@ -234,7 +234,7 @@ export const tkhd = ( let matrix: TransformationMatrix; if (trackData.type === 'video') { - const rotation = trackData.track.source.metadata.rotation; + const rotation = trackData.track.metadata.rotation; matrix = rotation === undefined || typeof rotation === 'number' ? rotationMatrix(rotation ?? 0) : rotation; } else { matrix = IDENTITY_MATRIX; @@ -271,7 +271,7 @@ export const mdhd = ( ) => { let lastSample = lastPresentedSample(trackData.samples); let localDuration = intoTimescale( - lastSample ? lastSample.presentationTimestamp + lastSample.duration : 0, + lastSample ? lastSample.timestamp + lastSample.duration : 0, trackData.timescale ); @@ -740,7 +740,7 @@ export const trun = (trackData: IsobmffTrackData) => { let allSampleSizes = trackData.currentChunk.samples.map(x => x.size); let allSampleFlags = trackData.currentChunk.samples.map(fragmentSampleFlags); let allSampleCompositionTimeOffsets = trackData.currentChunk.samples. - map(x => intoTimescale(x.presentationTimestamp - x.decodeTimestamp, trackData.timescale)); + map(x => intoTimescale(x.timestamp - x.decodeTimestamp, trackData.timescale)); let uniqueSampleDurations = new Set(allSampleDurations); let uniqueSampleSizes = new Set(allSampleSizes); diff --git a/src/isobmff/isobmff_muxer.ts b/src/isobmff/isobmff_muxer.ts index 21483cb..5368bdf 100644 --- a/src/isobmff/isobmff_muxer.ts +++ b/src/isobmff/isobmff_muxer.ts @@ -9,7 +9,7 @@ export const GLOBAL_TIMESCALE = 1000; const TIMESTAMP_OFFSET = 2_082_844_800; // Seconds between Jan 1 1904 and Jan 1 1970 export type Sample = { - presentationTimestamp: number, + timestamp: number, decodeTimestamp: number, duration: number, data: Uint8Array | null, @@ -30,9 +30,10 @@ export type IsobmffTrackData = { timescale: number, samples: Sample[], sampleQueue: Sample[], // For fragmented files + timestampProcessingQueue: Sample[], - firstDecodeTimestamp: number | null, - lastDecodeTimestamp: number, + firstTimestamp: number | null, + lastKeyFrameTimestamp: number | null, timeToSampleTable: { sampleCount: number, sampleDelta: number }[]; compositionTimeOffsetTable: { sampleCount: number, sampleCompositionTimeOffset: number }[]; @@ -256,11 +257,12 @@ export class IsobmffMuxer extends Muxer { height: meta.decoderConfig.codedHeight, decoderConfig: meta.decoderConfig }, - timescale: track.source.metadata.frameRate ?? 57600, + timescale: track.metadata.frameRate ?? 57600, samples: [], sampleQueue: [], - firstDecodeTimestamp: null, - lastDecodeTimestamp: -1, + timestampProcessingQueue: [], + firstTimestamp: null, + lastKeyFrameTimestamp: null, timeToSampleTable: [], compositionTimeOffsetTable: [], lastTimescaleUnits: null, @@ -297,8 +299,9 @@ export class IsobmffMuxer extends Muxer { timescale: meta.decoderConfig.sampleRate, samples: [], sampleQueue: [], - firstDecodeTimestamp: null, - lastDecodeTimestamp: -1, + timestampProcessingQueue: [], + firstTimestamp: null, + lastKeyFrameTimestamp: null, timeToSampleTable: [], compositionTimeOffsetTable: [], lastTimescaleUnits: null, @@ -314,7 +317,7 @@ export class IsobmffMuxer extends Muxer { return newTrackData; } - addEncodedVideoChunk(track: OutputVideoTrack, chunk: EncodedVideoChunk, meta?: EncodedVideoChunkMetadata, compositionTimeOffset?: number) { + addEncodedVideoChunk(track: OutputVideoTrack, chunk: EncodedVideoChunk, meta?: EncodedVideoChunkMetadata) { const trackData = this.#getVideoTrackData(track, chunk, meta); if ( @@ -327,7 +330,7 @@ export class IsobmffMuxer extends Muxer { }).`); } - let videoSample = this.#createSampleForTrack(trackData, chunk, compositionTimeOffset); + let videoSample = this.#createSampleForTrack(trackData, chunk); if (this.#format.options.fastStart === 'fragmented') { trackData.sampleQueue.push(videoSample); @@ -362,23 +365,17 @@ export class IsobmffMuxer extends Muxer { #createSampleForTrack( trackData: IsobmffTrackData, - chunk: EncodedVideoChunk | EncodedAudioChunk, - compositionTimeOffset?: number + chunk: EncodedVideoChunk | EncodedAudioChunk ) { - let presentationTimestampInSeconds = chunk.timestamp / 1e6; - let decodeTimestampInSeconds = (chunk.timestamp - (compositionTimeOffset ?? 0)) / 1e6; + let timestampInSeconds = this.#validateTimestamp(trackData, chunk); let durationInSeconds = (chunk.duration ?? 0) / 1e6; - let adjusted = this.#validateTimestamp(trackData, presentationTimestampInSeconds, decodeTimestampInSeconds); - presentationTimestampInSeconds = adjusted.presentationTimestamp; - decodeTimestampInSeconds = adjusted.decodeTimestamp; - let data = new Uint8Array(chunk.byteLength); chunk.copyTo(data); let sample: Sample = { - presentationTimestamp: presentationTimestampInSeconds, - decodeTimestamp: decodeTimestampInSeconds, + timestamp: timestampInSeconds, + decodeTimestamp: timestampInSeconds, // We may refine this later duration: durationInSeconds, data: data, size: data.byteLength, @@ -390,82 +387,106 @@ export class IsobmffMuxer extends Muxer { return sample; } - #addSampleToTrack( - trackData: IsobmffTrackData, - sample: Sample - ) { - if (this.#format.options.fastStart !== 'fragmented') { - trackData.samples.push(sample); + #processTimestamps(trackData: IsobmffTrackData) { + if (trackData.timestampProcessingQueue.length === 0) { + return; } - const sampleCompositionTimeOffset = - intoTimescale(sample.presentationTimestamp - sample.decodeTimestamp, trackData.timescale); + const sortedTimestamps = trackData.timestampProcessingQueue.map(x => x.timestamp).sort((a, b) => a - b); - if (trackData.lastTimescaleUnits !== null) { - assert(trackData.lastSample); + for (let i = 0; i < trackData.timestampProcessingQueue.length; i++) { + const sample = trackData.timestampProcessingQueue[i]!; - let timescaleUnits = intoTimescale(sample.decodeTimestamp, trackData.timescale, false); - let delta = Math.round(timescaleUnits - trackData.lastTimescaleUnits); - trackData.lastTimescaleUnits += delta; - trackData.lastSample.timescaleUnitsToNextSample = delta; + // Since the user only supplies presentation time, but these may be out of order, we reverse-engineer from + // that a sensible decode timestamp. The notion of a decode timestamp doesn't really make sense + // (presentation timestamp & decode order are all you need), but it is a concept in ISOBMFF so we need to + // model it. + sample.decodeTimestamp = sortedTimestamps[i]!; - if (this.#format.options.fastStart !== 'fragmented') { - let lastTableEntry = last(trackData.timeToSampleTable); - assert(lastTableEntry); + const sampleCompositionTimeOffset = + intoTimescale(sample.timestamp - sample.decodeTimestamp, trackData.timescale); - if (lastTableEntry.sampleCount === 1) { - // If we hit this case, we're the second sample - lastTableEntry.sampleDelta = delta; - lastTableEntry.sampleCount++; - } else if (lastTableEntry.sampleDelta === delta) { - // Simply increment the count - lastTableEntry.sampleCount++; - } else { - // The delta has changed, subtract one from the previous run and create a new run with the new delta - lastTableEntry.sampleCount--; - trackData.timeToSampleTable.push({ - sampleCount: 2, - sampleDelta: delta - }); + if (trackData.lastTimescaleUnits !== null) { + assert(trackData.lastSample); + + let timescaleUnits = intoTimescale(sample.decodeTimestamp, trackData.timescale, false); + let delta = Math.round(timescaleUnits - trackData.lastTimescaleUnits); + trackData.lastTimescaleUnits += delta; + trackData.lastSample.timescaleUnitsToNextSample = delta; + + if (this.#format.options.fastStart !== 'fragmented') { + let lastTableEntry = last(trackData.timeToSampleTable); + assert(lastTableEntry); + + if (lastTableEntry.sampleCount === 1) { + // If we hit this case, we're the second sample + lastTableEntry.sampleDelta = delta; + lastTableEntry.sampleCount++; + } else if (lastTableEntry.sampleDelta === delta) { + // Simply increment the count + lastTableEntry.sampleCount++; + } else { + // The delta has changed, subtract one from the previous run and create a new run with the new delta + lastTableEntry.sampleCount--; + trackData.timeToSampleTable.push({ + sampleCount: 2, + sampleDelta: delta + }); + } + + const lastCompositionTimeOffsetTableEntry = last(trackData.compositionTimeOffsetTable); + assert(lastCompositionTimeOffsetTableEntry); + + if (lastCompositionTimeOffsetTableEntry.sampleCompositionTimeOffset === sampleCompositionTimeOffset) { + // Simply increment the count + lastCompositionTimeOffsetTableEntry.sampleCount++; + } else { + // The composition time offset has changed, so create a new entry with the new composition time + // offset + trackData.compositionTimeOffsetTable.push({ + sampleCount: 1, + sampleCompositionTimeOffset: sampleCompositionTimeOffset + }); + } } - - const lastCompositionTimeOffsetTableEntry = last(trackData.compositionTimeOffsetTable); - assert(lastCompositionTimeOffsetTableEntry); - - if (lastCompositionTimeOffsetTableEntry.sampleCompositionTimeOffset === sampleCompositionTimeOffset) { - // Simply increment the count - lastCompositionTimeOffsetTableEntry.sampleCount++; - } else { - // The composition time offset has changed, so create a new entry with the new composition time - // offset + } else { + trackData.lastTimescaleUnits = 0; + + if (this.#format.options.fastStart !== 'fragmented') { + trackData.timeToSampleTable.push({ + sampleCount: 1, + sampleDelta: intoTimescale(sample.duration, trackData.timescale) + }); trackData.compositionTimeOffsetTable.push({ sampleCount: 1, sampleCompositionTimeOffset: sampleCompositionTimeOffset }); } } - } else { - trackData.lastTimescaleUnits = 0; - if (this.#format.options.fastStart !== 'fragmented') { - trackData.timeToSampleTable.push({ - sampleCount: 1, - sampleDelta: intoTimescale(sample.duration, trackData.timescale) - }); - trackData.compositionTimeOffsetTable.push({ - sampleCount: 1, - sampleCompositionTimeOffset: sampleCompositionTimeOffset - }); - } + trackData.lastSample = sample; } - trackData.lastSample = sample; + trackData.timestampProcessingQueue.length = 0; + } + + #addSampleToTrack( + trackData: IsobmffTrackData, + sample: Sample + ) { + if (sample.type === 'key') { + this.#processTimestamps(trackData); + } + + if (this.#format.options.fastStart !== 'fragmented') { + trackData.samples.push(sample); + } let beginNewChunk = false; if (!trackData.currentChunk) { beginNewChunk = true; } else { - let currentChunkDuration = sample.presentationTimestamp - trackData.currentChunk.startTimestamp; + let currentChunkDuration = sample.timestamp - trackData.currentChunk.startTimestamp; if (this.#format.options.fastStart === 'fragmented') { // We can only finalize this fragment (and begin a new one) if we know that each track will be able to @@ -494,7 +515,7 @@ export class IsobmffMuxer extends Muxer { } trackData.currentChunk = { - startTimestamp: sample.presentationTimestamp, + startTimestamp: sample.timestamp, samples: [], offset: null, moofOffset: null @@ -503,30 +524,31 @@ export class IsobmffMuxer extends Muxer { assert(trackData.currentChunk); trackData.currentChunk.samples.push(sample); + trackData.timestampProcessingQueue.push(sample); } - #validateTimestamp(trackData: IsobmffTrackData, presentationTimestamp: number, decodeTimestamp: number) { - if (decodeTimestamp < 0) { - throw new Error(`Timestamps must be non-negative (got ${decodeTimestamp}s).`); + #validateTimestamp(trackData: IsobmffTrackData, chunk: EncodedVideoChunk | EncodedAudioChunk) { + let timestampInSeconds = chunk.timestamp / 1e6; + + if (timestampInSeconds < 0) { + throw new Error(`Timestamps must be non-negative (got ${timestampInSeconds}s).`); } - if (trackData.firstDecodeTimestamp === null) { - trackData.firstDecodeTimestamp = decodeTimestamp; + if (trackData.firstTimestamp === null) { + trackData.firstTimestamp = timestampInSeconds; } - decodeTimestamp -= trackData.firstDecodeTimestamp; - presentationTimestamp -= trackData.firstDecodeTimestamp; + timestampInSeconds -= trackData.firstTimestamp; - if (decodeTimestamp < trackData.lastDecodeTimestamp) { - throw new Error( - `Timestamps must be monotonically increasing ` + - `(timestamp went from ${trackData.lastDecodeTimestamp}s to ${decodeTimestamp}s).` - ); + if (trackData.lastKeyFrameTimestamp !== null && timestampInSeconds < trackData.lastKeyFrameTimestamp) { + throw new Error(`Timestamp cannot be before last key frame's timestamp (got ${timestampInSeconds}s, last key frame at ${trackData.lastKeyFrameTimestamp}s).`); } - trackData.lastDecodeTimestamp = decodeTimestamp; + if (chunk.type === 'key') { + trackData.lastKeyFrameTimestamp = timestampInSeconds; + } - return { presentationTimestamp, decodeTimestamp }; + return timestampInSeconds; } #finalizeCurrentChunk(trackData: IsobmffTrackData) { @@ -572,26 +594,26 @@ export class IsobmffMuxer extends Muxer { outer: while (true) { - let trackWithMinDecodeTimestamp: IsobmffTrackData | null = null; - let minDecodeTimestamp = Infinity; + let trackWithMinTimestamp: IsobmffTrackData | null = null; + let minTimestamp = Infinity; for (let trackData of this.#trackDatas) { if (trackData.sampleQueue.length === 0) { break outer; } - if (trackData.sampleQueue[0]!.decodeTimestamp < minDecodeTimestamp) { - trackWithMinDecodeTimestamp = trackData; - minDecodeTimestamp = trackData.sampleQueue[0]!.decodeTimestamp; + if (trackData.sampleQueue[0]!.timestamp < minTimestamp) { + trackWithMinTimestamp = trackData; + minTimestamp = trackData.sampleQueue[0]!.timestamp; } } - if (!trackWithMinDecodeTimestamp) { + if (!trackWithMinTimestamp) { break; } - let sample = trackWithMinDecodeTimestamp.sampleQueue.shift()!; - this.#addSampleToTrack(trackWithMinDecodeTimestamp, sample); + let sample = trackWithMinTimestamp.sampleQueue.shift()!; + this.#addSampleToTrack(trackWithMinTimestamp, sample); } } @@ -671,11 +693,14 @@ export class IsobmffMuxer extends Muxer { for (let sample of trackData.sampleQueue) { this.#addSampleToTrack(trackData, sample); } + + this.#processTimestamps(trackData); } this.#finalizeFragment(false); // Don't flush the last fragment as we will flush it with the mfra box soon } else { for (let trackData of this.#trackDatas) { + this.#processTimestamps(trackData); this.#finalizeCurrentChunk(trackData); } } diff --git a/src/matroska/ebml.ts b/src/matroska/ebml.ts index 20c4c34..ece2659 100644 --- a/src/matroska/ebml.ts +++ b/src/matroska/ebml.ts @@ -1,7 +1,7 @@ export interface EBMLElement { id: number, size?: number, - data: number | string | Uint8Array | EBMLFloat32 | EBMLFloat64 | (EBML | null)[] + data: number | string | Uint8Array | EBMLFloat32 | EBMLFloat64 | EBMLSignedInt | (EBML | null)[] } export type EBML = EBMLElement | Uint8Array | (EBML | null)[]; @@ -24,6 +24,15 @@ export class EBMLFloat64 { } } +/** Wrapper around a number to be able to differentiate it in the writer. */ +export class EBMLSignedInt { + value: number; + + constructor(value: number) { + this.value = value; + } +} + /** Defines some of the EBML IDs used by Matroska files. */ export enum EBMLId { EBML = 0x1a45dfa3, @@ -83,7 +92,6 @@ export enum EBMLId { } export const measureUnsignedInt = (value: number) => { - // Force to 32-bit unsigned integer if (value < (1 << 8)) { return 1; } else if (value < (1 << 16)) { @@ -99,6 +107,22 @@ export const measureUnsignedInt = (value: number) => { } }; +export const measureSignedInt = (value: number) => { + if (value >= -(1 << 6) && value < (1 << 6)) { + return 1; + } else if (value >= -(1 << 13) && value < (1 << 13)) { + return 2; + } else if (value >= -(1 << 20) && value < (1 << 20)) { + return 3; + } else if (value >= -(1 << 27) && value < (1 << 27)) { + return 4; + } else if (value >= -(2**34) && value < 2**34) { + 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 diff --git a/src/matroska/matroska_muxer.ts b/src/matroska/matroska_muxer.ts index e477fa1..d953cf9 100644 --- a/src/matroska/matroska_muxer.ts +++ b/src/matroska/matroska_muxer.ts @@ -4,7 +4,7 @@ import { Output, OutputAudioTrack, OutputTrack, OutputVideoTrack } from '../outp 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'; +import { EBML, EBMLElement, EBMLFloat32, EBMLFloat64, EBMLId, EBMLSignedInt, measureEBMLVarInt, measureSignedInt, measureUnsignedInt } from './ebml'; const VIDEO_TRACK_TYPE = 1; const AUDIO_TRACK_TYPE = 2; @@ -41,7 +41,7 @@ type MatroskaTrackData = { chunkQueue: InternalMediaChunk[], firstTimestamp: number | null, - lastTimestamp: number | null, + lastKeyFrameTimestamp: number | null, lastWrittenTimestamp: number | null } & ({ track: OutputVideoTrack, @@ -133,7 +133,7 @@ export class MatroskaMuxer extends Muxer { this.#writer.write(this.#helper); } - #writeUnsignedInt(value: number, width: number = measureUnsignedInt(value)) { + #writeUnsignedInt(value: number, width = measureUnsignedInt(value)) { let pos = 0; // Each case falls through: @@ -159,7 +159,16 @@ export class MatroskaMuxer extends Muxer { this.#writer.write(this.#helper.subarray(0, pos)); } - writeEBMLVarInt(value: number, width: number = measureEBMLVarInt(value)) { + #writeSignedInt(value: number, width = measureSignedInt(value)) { + if (value < 0) { + // Two's complement stuff + value += 2 ** (width * 8); + } + + this.#writeUnsignedInt(value, width); + } + + writeEBMLVarInt(value: number, width = measureEBMLVarInt(value)) { let pos = 0; switch (width) { @@ -265,6 +274,10 @@ export class MatroskaMuxer extends Muxer { } else if (data.data instanceof EBMLFloat64) { this.writeEBMLVarInt(8); this.#writeFloat64(data.data.value); + } else if (data.data instanceof EBMLSignedInt) { + let size = data.size ?? measureSignedInt(data.data.value); + this.writeEBMLVarInt(size); + this.#writeSignedInt(data.data.value, size); } } } @@ -362,7 +375,7 @@ export class MatroskaMuxer extends Muxer { { 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), + (trackData.track.metadata.frameRate ? { id: EBMLId.DefaultDuration, data: 1e9 / trackData.track.metadata.frameRate } : null), { id: EBMLId.Video, data: [ { id: EBMLId.PixelWidth, data: trackData.info.width }, { id: EBMLId.PixelHeight, data: trackData.info.height }, @@ -474,7 +487,7 @@ export class MatroskaMuxer extends Muxer { }, chunkQueue: [], firstTimestamp: null, - lastTimestamp: null, + lastKeyFrameTimestamp: null, lastWrittenTimestamp: null }; @@ -504,7 +517,7 @@ export class MatroskaMuxer extends Muxer { }, chunkQueue: [], firstTimestamp: null, - lastTimestamp: null, + lastKeyFrameTimestamp: null, lastWrittenTimestamp: null }; @@ -514,14 +527,12 @@ export class MatroskaMuxer extends Muxer { return newTrackData; } - addEncodedVideoChunk(track: OutputVideoTrack, chunk: EncodedVideoChunk, meta?: EncodedVideoChunkMetadata, compositionTimeOffset?: number) { + addEncodedVideoChunk(track: OutputVideoTrack, chunk: EncodedVideoChunk, meta?: EncodedVideoChunkMetadata) { 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(); @@ -533,7 +544,6 @@ export class MatroskaMuxer extends Muxer { const trackData = this.#getAudioTrackData(track, chunk, meta); let audioChunk = this.#createInternalChunk(trackData, chunk); - trackData.lastTimestamp = audioChunk.timestamp; trackData.chunkQueue.push(audioChunk); this.#interleaveChunks(); @@ -683,7 +693,7 @@ export class MatroskaMuxer extends Muxer { trackData: MatroskaTrackData, chunk: EncodedVideoChunk | EncodedAudioChunk ) { - let adjustedTimestamp = this.#validateTimestamp(trackData, chunk.timestamp); + let adjustedTimestamp = this.#validateTimestamp(trackData, chunk); let data = new Uint8Array(chunk.byteLength); chunk.copyTo(data); @@ -699,7 +709,9 @@ export class MatroskaMuxer extends Muxer { return internalChunk; } - #validateTimestamp(trackData: MatroskaTrackData, timestamp: number) { + #validateTimestamp(trackData: MatroskaTrackData, chunk: EncodedVideoChunk | EncodedAudioChunk) { + let timestamp = chunk.timestamp; + if (timestamp < 0) { throw new Error(`Timestamps must be non-negative (got ${timestamp}s).`); } @@ -710,11 +722,12 @@ export class MatroskaMuxer extends Muxer { 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).` - ); + if (trackData.lastKeyFrameTimestamp !== null && timestamp < trackData.lastKeyFrameTimestamp) { + throw new Error(`Timestamp cannot be before last key frame's timestamp (got ${timestamp / 1e6}s, last key frame at ${trackData.lastKeyFrameTimestamp / 1e6}s).`); + } + + if (chunk.type === 'key') { + trackData.lastKeyFrameTimestamp = timestamp; } return timestamp; @@ -787,7 +800,7 @@ export class MatroskaMuxer extends Muxer { prelude, chunk.data ] }, - chunk.type === 'delta' ? { id: EBMLId.ReferenceBlock, data: trackData.lastWrittenTimestamp! - msTimestamp } : null, + chunk.type === 'delta' ? { id: EBMLId.ReferenceBlock, data: new EBMLSignedInt(trackData.lastWrittenTimestamp! - msTimestamp) } : null, chunk.duration !== null ? { id: EBMLId.BlockDuration, data: msDuration } : null, chunk.additions ? { id: EBMLId.BlockAdditions, data: chunk.additions } : null ] }; diff --git a/src/muxer.ts b/src/muxer.ts index 05c021b..0b4b25d 100644 --- a/src/muxer.ts +++ b/src/muxer.ts @@ -8,7 +8,7 @@ export abstract class Muxer { } abstract start(): void; - abstract addEncodedVideoChunk(track: OutputVideoTrack, chunk: EncodedVideoChunk, meta?: EncodedVideoChunkMetadata, compositionTimeOffset?: number): void; + abstract addEncodedVideoChunk(track: OutputVideoTrack, chunk: EncodedVideoChunk, meta?: EncodedVideoChunkMetadata): void; abstract addEncodedAudioChunk(track: OutputAudioTrack, chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata): void; abstract finalize(): void; diff --git a/src/output.ts b/src/output.ts index 0ada5ba..c5b4e38 100644 --- a/src/output.ts +++ b/src/output.ts @@ -1,3 +1,4 @@ +import { TransformationMatrix } from "./misc"; import { Muxer } from "./muxer"; import { OutputFormat } from "./output_format"; import { AudioSource, VideoSource } from "./source"; @@ -14,15 +15,23 @@ export type OutputTrack = { output: Output } & ({ type: 'video', - source: VideoSource + source: VideoSource, + metadata: VideoTrackMetadata } | { type: 'audio', - source: AudioSource + source: AudioSource, + metadata: AudioTrackMetadata }); export type OutputVideoTrack = OutputTrack & { type: 'video' }; export type OutputAudioTrack = OutputTrack & { type: 'audio' }; +type VideoTrackMetadata = { + rotation?: 0 | 90 | 180 | 270 | TransformationMatrix, + frameRate?: number +}; +type AudioTrackMetadata = {}; + export class Output { muxer: Muxer; writer: Writer; @@ -35,7 +44,7 @@ export class Output { this.muxer = options.format.createMuxer(this); } - addTrack(source: VideoSource | AudioSource) { + addTrack(source: VideoSource | AudioSource, metadata: VideoTrackMetadata | AudioTrackMetadata = {}) { if (this.started) { throw new Error('Cannot add track after output has started.'); } @@ -47,7 +56,8 @@ export class Output { id: this.tracks.length + 1, output: this, type: source instanceof VideoSource ? 'video' : 'audio', - source + source, + metadata } as OutputTrack; this.muxer.beforeTrackAdd(track); @@ -57,6 +67,8 @@ export class Output { } start() { + // TODO: Warn / throw if there are no sources + if (this.started) { throw new Error('Output already started.'); } @@ -70,6 +82,8 @@ export class Output { } async finalize() { + // TODO: Test what happens when finalizing without a single chunk of media + if (this.finalizing) { throw new Error('Cannot call finalize twice.'); } diff --git a/src/source.ts b/src/source.ts index 7c3d9cb..bced3c0 100644 --- a/src/source.ts +++ b/src/source.ts @@ -5,24 +5,24 @@ import { OutputAudioTrack, OutputTrack, OutputVideoTrack } from "./output"; export type VideoCodec = 'avc' | 'hevc' | 'vp8' | 'vp9' | 'av1'; export type AudioCodec = 'aac' | 'opus' | 'vorbis'; // TODO add the rest -type VideoSourceMetadata = { - rotation?: 0 | 90 | 180 | 270 | TransformationMatrix, - frameRate?: number -}; -type AudioSourceMetadata = {}; - export abstract class VideoSource { connectedTrack: OutputVideoTrack | null = null; codec: VideoCodec; - metadata: VideoSourceMetadata; - constructor(codec: VideoCodec, metadata: VideoSourceMetadata) { + constructor(codec: VideoCodec) { this.codec = codec; - this.metadata = metadata; } - ensureNotFinalizing() { - if (this.connectedTrack?.output.finalizing) { + ensureValidDigest() { + if (!this.connectedTrack) { + throw new Error('Cannot call digest without connecting the source to an output track.'); + } + + if (!this.connectedTrack.output.started) { + throw new Error('Cannot call digest before output has been started.'); + } + + if (this.connectedTrack.output.finalizing) { throw new Error('Cannot call digest after output has started finalizing.'); } } @@ -34,11 +34,9 @@ export abstract class VideoSource { export abstract class AudioSource { connectedTrack: OutputAudioTrack | null = null; codec: AudioCodec; - metadata: AudioSourceMetadata; - constructor(codec: AudioCodec, metadata: AudioSourceMetadata) { + constructor(codec: AudioCodec) { this.codec = codec; - this.metadata = metadata; } ensureNotFinalizing() { @@ -62,13 +60,15 @@ export type AudioCodecConfig = { }; export class EncodedVideoChunkSource extends VideoSource { - constructor(codec: VideoCodec, options: VideoSourceMetadata = {}) { - super(codec, options); + constructor(codec: VideoCodec) { + super(codec); } - digest(chunk: EncodedVideoChunk, meta?: EncodedVideoChunkMetadata, compositionTimeOffset?: number) { - this.ensureNotFinalizing(); - this.connectedTrack?.output.muxer.addEncodedVideoChunk(this.connectedTrack, chunk, meta, compositionTimeOffset); + // TODO: Ensure that the first chunk is a key frame (same for the audio case) + + digest(chunk: EncodedVideoChunk, meta?: EncodedVideoChunkMetadata) { + this.ensureValidDigest(); + this.connectedTrack?.output.muxer.addEncodedVideoChunk(this.connectedTrack, chunk, meta); } } @@ -80,8 +80,9 @@ class VideoEncoderWrapper { constructor(private source: VideoSource, private codecConfig: VideoCodecConfig) {} + // TODO: Ensure video frame size remains constant digest(videoFrame: VideoFrame) { - this.source.ensureNotFinalizing(); + this.source.ensureValidDigest(); this.ensureEncoder(videoFrame); assert(this.encoder); @@ -122,8 +123,8 @@ class VideoEncoderWrapper { export class VideoFrameSource extends VideoSource { private encoder: VideoEncoderWrapper; - constructor(codecConfig: VideoCodecConfig, options: VideoSourceMetadata = {}) { - super(codecConfig.codec, options); + constructor(codecConfig: VideoCodecConfig) { + super(codecConfig.codec); this.encoder = new VideoEncoderWrapper(this, codecConfig); } @@ -139,8 +140,8 @@ export class VideoFrameSource extends VideoSource { export class CanvasSource extends VideoSource { private encoder: VideoEncoderWrapper; - constructor(private canvas: HTMLCanvasElement, codecConfig: VideoCodecConfig, options: VideoSourceMetadata = {}) { - super(codecConfig.codec, options); + constructor(private canvas: HTMLCanvasElement, codecConfig: VideoCodecConfig) { + super(codecConfig.codec); this.encoder = new VideoEncoderWrapper(this, codecConfig); } @@ -163,8 +164,8 @@ export class MediaStreamVideoTrackSource extends VideoSource { private encoder: VideoEncoderWrapper; private abortController: AbortController | null = null; - constructor(private track: MediaStreamVideoTrack, codecConfig: VideoCodecConfig, options: VideoSourceMetadata = {}) { - super(codecConfig.codec, options); + constructor(private track: MediaStreamVideoTrack, codecConfig: VideoCodecConfig) { + super(codecConfig.codec); this.encoder = new VideoEncoderWrapper(this, codecConfig); } @@ -201,7 +202,7 @@ export class MediaStreamVideoTrackSource extends VideoSource { export class EncodedAudioChunkSource extends AudioSource { constructor(codec: AudioCodec) { - super(codec, {}); + super(codec); } digest(chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata) { @@ -215,6 +216,7 @@ class AudioEncoderWrapper { constructor(private source: AudioSource, private codecConfig: AudioCodecConfig) {} + // TODO: Ensure audio parameters remain constant digest(audioData: AudioData) { this.source.ensureNotFinalizing(); @@ -250,8 +252,8 @@ class AudioEncoderWrapper { export class AudioDataSource extends AudioSource { private encoder: AudioEncoderWrapper; - constructor(codecConfig: AudioCodecConfig, options: AudioSourceMetadata = {}) { - super(codecConfig.codec, options); + constructor(codecConfig: AudioCodecConfig) { + super(codecConfig.codec); this.encoder = new AudioEncoderWrapper(this, codecConfig); } @@ -268,8 +270,8 @@ export class AudioBufferSource extends AudioSource { private encoder: AudioEncoderWrapper; private accumulatedFrameCount = 0; - constructor(codecConfig: AudioCodecConfig, options: AudioSourceMetadata = {}) { - super(codecConfig.codec, options); + constructor(codecConfig: AudioCodecConfig) { + super(codecConfig.codec); this.encoder = new AudioEncoderWrapper(this, codecConfig); } @@ -309,8 +311,8 @@ export class MediaStreamAudioTrackSource extends AudioSource { private encoder: AudioEncoderWrapper; private abortController: AbortController | null = null; - constructor(private track: MediaStreamAudioTrack, codecConfig: AudioCodecConfig, options: AudioSourceMetadata = {}) { - super(codecConfig.codec, options); + constructor(private track: MediaStreamAudioTrack, codecConfig: AudioCodecConfig) { + super(codecConfig.codec); this.encoder = new AudioEncoderWrapper(this, codecConfig); }