From 39aea035657347492627396685b0283a2785d30e Mon Sep 17 00:00:00 2001 From: David Payr <1696106+Vanilagy@users.noreply.github.com> Date: Sat, 16 Nov 2024 17:23:24 +0100 Subject: [PATCH] Add subtitle tracks and implement subtitle tracks for Matroska muxer --- dev/index.html | 15 +- dist/metamuxer.js | 1114 ++++++++++++++++++-------------- dist/metamuxer.min.js | 10 +- dist/metamuxer.min.mjs | 10 +- dist/metamuxer.mjs | 1114 ++++++++++++++++++-------------- src/index.ts | 2 +- src/matroska/matroska_muxer.ts | 218 +++---- src/muxer.ts | 5 +- src/output.ts | 31 +- src/source.ts | 128 +++- src/subtitles.ts | 151 +++++ src/target.ts | 5 +- 12 files changed, 1653 insertions(+), 1150 deletions(-) create mode 100644 src/subtitles.ts diff --git a/dev/index.html b/dev/index.html index 7c2db70..d5bc908 100644 --- a/dev/index.html +++ b/dev/index.html @@ -78,12 +78,23 @@ codec: 'opus', bitrate: 128e3, }); + let subtitleSource = new Metamuxer.TextSubtitleSource('webvtt'); - output.addTrack(videoSource); - output.addTrack(audioSource); + output.addVideoTrack(videoSource); + output.addAudioTrack(audioSource); + output.addSubtitleTrack(subtitleSource); output.start(); + let simpleWebvttFile = +`WEBVTT + +00:00:00.000 --> 00:00:10.000 +Example entry 1: Hello world. +`; + subtitleSource.digest(simpleWebvttFile); + subtitleSource.close(); + for (let i = 0; i < 100; i++) { context.fillStyle = ['red', 'green', 'blue', 'yellow'][i % 4]; context.fillRect(canvas.width * Math.random(), canvas.height * Math.random(), canvas.width * Math.random(), canvas.height * Math.random()); diff --git a/dist/metamuxer.js b/dist/metamuxer.js index d5a8cb6..1c45f0e 100644 --- a/dist/metamuxer.js +++ b/dist/metamuxer.js @@ -35,99 +35,72 @@ var Metamuxer = (() => { Output: () => Output, StreamTarget: () => StreamTarget, Target: () => Target, + TextSubtitleSource: () => TextSubtitleSource, VideoFrameSource: () => VideoFrameSource, WebMOutputFormat: () => WebMOutputFormat }); - // src/codec.ts - var buildVideoCodecString = (codec, width, height) => { - if (codec === "avc") { - let profileIndication = 100; - if (width <= 768 && height <= 432) { - profileIndication = 66; - } else if (width <= 1920 && height <= 1080) { - profileIndication = 77; + // src/output.ts + var Output = class { + constructor(options) { + this.tracks = []; + this.started = false; + this.finalizing = false; + if (options.target.output) { + throw new Error("Target is already used for another output."); } - const profileCompatibility = 0; - const levelIndication = width > 1920 || height > 1080 ? 50 : 41; - const hexProfileIndication = profileIndication.toString(16).padStart(2, "0"); - const hexProfileCompatibility = profileCompatibility.toString(16).padStart(2, "0"); - const hexLevelIndication = levelIndication.toString(16).padStart(2, "0"); - return `avc1.${hexProfileIndication}${hexProfileCompatibility}${hexLevelIndication}`; - } else if (codec === "hevc") { - let profileSpace = 0; - let profileIdc = 1; - const compatibilityFlags = Array(32).fill(0); - compatibilityFlags[profileIdc] = 1; - const compatibilityHex = parseInt(compatibilityFlags.reverse().join(""), 2).toString(16).replace(/^0+/, ""); - let tier = "L"; - let level = 120; - if (width <= 1280 && height <= 720) { - level = 93; - } else if (width <= 1920 && height <= 1080) { - level = 120; - } else if (width <= 3840 && height <= 2160) { - level = 150; - } else { - tier = "H"; - level = 180; - } - const constraintFlags = "B0"; - const profilePrefix = profileSpace === 0 ? "" : String.fromCharCode(65 + profileSpace - 1); - return `hev1.${profilePrefix}${profileIdc}.${compatibilityHex}.${tier}${level}.${constraintFlags}`; - } else if (codec === "vp8") { - return "vp8"; - } else if (codec === "vp9") { - const profile = "00"; - let level; - if (width <= 854 && height <= 480) { - level = "21"; - } else if (width <= 1280 && height <= 720) { - level = "31"; - } else if (width <= 1920 && height <= 1080) { - level = "41"; - } else if (width <= 3840 && height <= 2160) { - level = "51"; - } else { - level = "61"; - } - const bitDepth = "08"; - return `vp09.${profile}.${level}.${bitDepth}`; - } else if (codec === "av1") { - const profile = 0; - let level; - if (width <= 854 && height <= 480) { - level = "01"; - } else if (width <= 1280 && height <= 720) { - level = "03"; - } else if (width <= 1920 && height <= 1080) { - level = "04"; - } else if (width <= 3840 && height <= 2160) { - level = "07"; - } else { - level = "09"; - } - const tier = "M"; - const bitDepth = "08"; - return `av01.${profile}.${level}${tier}.${bitDepth}`; + options.target.output = this; + this.writer = options.target.createWriter(); + this.muxer = options.format.createMuxer(this); } - throw new Error(`Unhandled codec '${codec}'.`); - }; - var buildAudioCodecString = (codec, numberOfChannels, sampleRate) => { - if (codec === "aac") { - if (numberOfChannels >= 2 && sampleRate <= 24e3) { - return "mp4a.40.29"; - } - if (sampleRate <= 24e3) { - return "mp4a.40.5"; - } - return "mp4a.40.2"; - } else if (codec === "opus") { - return "opus"; - } else if (codec === "vorbis") { - return "vorbis"; + addVideoTrack(source, metadata = {}) { + this.addTrack("video", source, metadata); + } + addAudioTrack(source, metadata = {}) { + this.addTrack("audio", source, metadata); + } + addSubtitleTrack(source, metadata = {}) { + this.addTrack("subtitle", source, metadata); + } + addTrack(type, source, metadata) { + if (this.started) { + throw new Error("Cannot add track after output has started."); + } + if (source.connectedTrack) { + throw new Error("Source is already used for a track."); + } + const track = { + id: this.tracks.length + 1, + output: this, + type, + source, + metadata + }; + this.muxer.beforeTrackAdd(track); + this.tracks.push(track); + source.connectedTrack = track; + } + start() { + if (this.started) { + throw new Error("Output already started."); + } + this.started = true; + this.muxer.start(); + for (const track of this.tracks) { + track.source.start(); + } + } + async finalize() { + if (this.finalizing) { + throw new Error("Cannot call finalize twice."); + } + this.finalizing = true; + const promises = this.tracks.map((x) => x.source.flush()); + await Promise.all(promises); + this.muxer.finalize(); + this.writer.flush(); + this.writer.finalize(); } - throw new Error(`Unhandled codec '${codec}'.`); }; // src/misc.ts @@ -172,318 +145,6 @@ var Metamuxer = (() => { } }; - // src/source.ts - var VideoSource = class { - 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; - } - ensureNotFinalizing() { - if (this.connectedTrack?.output.finalizing) { - throw new Error("Cannot call digest after output has started finalizing."); - } - } - start() { - } - async flush() { - } - }; - var EncodedVideoChunkSource = class extends VideoSource { - constructor(codec) { - super(codec); - } - // TODO: Ensure that the first chunk is a key frame (same for the audio case) - digest(chunk, meta) { - this.ensureValidDigest(); - this.connectedTrack?.output.muxer.addEncodedVideoChunk(this.connectedTrack, chunk, meta); - } - }; - var KEY_FRAME_INTERVAL = 5; - var VideoEncoderWrapper = class { - constructor(source, codecConfig) { - this.source = source; - this.codecConfig = codecConfig; - this.encoder = null; - this.lastMultipleOfKeyFrameInterval = -1; - } - // TODO: Ensure video frame size remains constant - digest(videoFrame) { - this.source.ensureValidDigest(); - this.ensureEncoder(videoFrame); - assert(this.encoder); - const multipleOfKeyFrameInterval = Math.floor(videoFrame.timestamp / 1e6 / KEY_FRAME_INTERVAL); - this.encoder.encode(videoFrame, { keyFrame: multipleOfKeyFrameInterval !== this.lastMultipleOfKeyFrameInterval }); - this.lastMultipleOfKeyFrameInterval = multipleOfKeyFrameInterval; - } - ensureEncoder(videoFrame) { - if (this.encoder) { - return; - } - this.encoder = new VideoEncoder({ - output: (chunk, meta) => this.source.connectedTrack?.output.muxer.addEncodedVideoChunk(this.source.connectedTrack, chunk, meta), - error: (error) => console.error(error) - // TODO - }); - this.encoder.configure({ - codec: buildVideoCodecString(this.codecConfig.codec, videoFrame.codedWidth, videoFrame.codedHeight), - width: videoFrame.codedWidth, - height: videoFrame.codedHeight, - bitrate: this.codecConfig.bitrate - }); - } - async flush() { - return this.encoder?.flush(); - } - }; - var VideoFrameSource = class extends VideoSource { - constructor(codecConfig) { - super(codecConfig.codec); - this.encoder = new VideoEncoderWrapper(this, codecConfig); - } - digest(videoFrame) { - this.encoder.digest(videoFrame); - } - flush() { - return this.encoder.flush(); - } - }; - var CanvasSource = class extends VideoSource { - constructor(canvas, codecConfig) { - super(codecConfig.codec); - this.canvas = canvas; - this.encoder = new VideoEncoderWrapper(this, codecConfig); - } - digest(timestamp, duration = 0) { - const frame = new VideoFrame(this.canvas, { - timestamp: Math.round(1e6 * timestamp), - duration: Math.round(1e6 * duration) - }); - this.encoder.digest(frame); - frame.close(); - } - flush() { - return this.encoder.flush(); - } - }; - var MediaStreamVideoTrackSource = class extends VideoSource { - constructor(track, codecConfig) { - super(codecConfig.codec); - this.track = track; - this.abortController = null; - this.encoder = new VideoEncoderWrapper(this, codecConfig); - } - start() { - this.abortController = new AbortController(); - const processor = new MediaStreamTrackProcessor({ track: this.track }); - const consumer = new WritableStream({ - write: (videoFrame) => { - this.encoder.digest(videoFrame); - videoFrame.close(); - } - }); - processor.readable.pipeTo(consumer, { - signal: this.abortController.signal - }).catch((err) => { - if (err instanceof DOMException && err.name === "AbortError") return; - console.error("Pipe error:", err); - }); - } - async flush() { - if (this.abortController) { - this.abortController.abort(); - this.abortController = null; - } - 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); - assert(this.encoder); - this.encoder.encode(audioData); - } - ensureEncoder(audioData) { - if (this.encoder) { - return; - } - this.encoder = new AudioEncoder({ - output: (chunk, meta) => this.source.connectedTrack?.output.muxer.addEncodedAudioChunk(this.source.connectedTrack, chunk, meta), - error: (error) => console.error(error) - // TODO - }); - this.encoder.configure({ - codec: buildAudioCodecString(this.codecConfig.codec, audioData.numberOfChannels, audioData.sampleRate), - numberOfChannels: audioData.numberOfChannels, - sampleRate: audioData.sampleRate, - bitrate: this.codecConfig.bitrate - }); - } - async flush() { - return this.encoder?.flush(); - } - }; - var AudioDataSource = class extends AudioSource { - constructor(codecConfig) { - super(codecConfig.codec); - this.encoder = new AudioEncoderWrapper(this, codecConfig); - } - digest(audioData) { - this.encoder.digest(audioData); - } - flush() { - return this.encoder.flush(); - } - }; - var AudioBufferSource = class extends AudioSource { - constructor(codecConfig) { - super(codecConfig.codec); - this.accumulatedFrameCount = 0; - this.encoder = new AudioEncoderWrapper(this, codecConfig); - } - digest(audioBuffer) { - const numberOfChannels = audioBuffer.numberOfChannels; - const sampleRate = audioBuffer.sampleRate; - const numberOfFrames = audioBuffer.length; - const data = new Float32Array(numberOfChannels * numberOfFrames); - for (let channel = 0; channel < numberOfChannels; channel++) { - const channelData = audioBuffer.getChannelData(channel); - data.set(channelData, channel * numberOfFrames); - } - const audioData = new AudioData({ - format: "f32-planar", - sampleRate, - numberOfFrames, - numberOfChannels, - timestamp: Math.round(1e6 * this.accumulatedFrameCount / sampleRate), - data - }); - this.encoder.digest(audioData); - audioData.close(); - this.accumulatedFrameCount += numberOfFrames; - } - flush() { - return this.encoder.flush(); - } - }; - var MediaStreamAudioTrackSource = class extends AudioSource { - constructor(track, codecConfig) { - super(codecConfig.codec); - this.track = track; - this.abortController = null; - this.encoder = new AudioEncoderWrapper(this, codecConfig); - } - start() { - this.abortController = new AbortController(); - const processor = new MediaStreamTrackProcessor({ track: this.track }); - const consumer = new WritableStream({ - write: (audioData) => { - this.encoder.digest(audioData); - audioData.close(); - } - }); - processor.readable.pipeTo(consumer, { - signal: this.abortController.signal - }).catch((err) => { - if (err instanceof DOMException && err.name === "AbortError") return; - console.error("Pipe error:", err); - }); - } - async flush() { - if (this.abortController) { - this.abortController.abort(); - this.abortController = null; - } - await this.encoder.flush(); - } - }; - - // src/output.ts - var Output = class { - constructor(options) { - this.tracks = []; - this.started = false; - this.finalizing = false; - this.writer = options.target.createWriter(); - this.muxer = options.format.createMuxer(this); - } - addTrack(source, metadata = {}) { - if (this.started) { - throw new Error("Cannot add track after output has started."); - } - if (source.connectedTrack) { - throw new Error("Source is already used for a track."); - } - const track = { - id: this.tracks.length + 1, - output: this, - type: source instanceof VideoSource ? "video" : "audio", - source, - metadata - }; - this.muxer.beforeTrackAdd(track); - this.tracks.push(track); - source.connectedTrack = track; - } - start() { - if (this.started) { - throw new Error("Output already started."); - } - this.started = true; - this.muxer.start(); - for (const track of this.tracks) { - track.source.start(); - } - } - async finalize() { - if (this.finalizing) { - throw new Error("Cannot call finalize twice."); - } - this.finalizing = true; - const promises = this.tracks.map((x) => x.source.flush()); - await Promise.all(promises); - this.muxer.finalize(); - this.writer.flush(); - this.writer.finalize(); - } - }; - // src/isobmff/isobmff_boxes.ts var bytes = new Uint8Array(8); var view = new DataView(bytes.buffer); @@ -1228,6 +889,8 @@ var Metamuxer = (() => { } beforeTrackAdd(track) { } + onTrackClose(track) { + } }; // src/isobmff/isobmff_muxer.ts @@ -1838,8 +1501,6 @@ var Metamuxer = (() => { }; // src/matroska/matroska_muxer.ts - var VIDEO_TRACK_TYPE = 1; - var AUDIO_TRACK_TYPE = 2; var MAX_CHUNK_LENGTH_MS = 2 ** 15; var APP_NAME = "https://github.com/Vanilagy/webm-muxer"; var SEGMENT_SIZE_BYTES = 6; @@ -1852,7 +1513,13 @@ var Metamuxer = (() => { av1: "V_AV1", aac: "A_AAC", opus: "A_OPUS", - vorbis: "A_VORBIS" + vorbis: "A_VORBIS", + webvtt: "S_TEXT/WEBVTT" + }; + var TRACK_TYPE_MAP = { + video: 1, + audio: 2, + subtitle: 17 }; var MatroskaMuxer = class extends Muxer { constructor(output, format) { @@ -2039,10 +1706,16 @@ var Metamuxer = (() => { if (!["vp8", "vp9", "av1"].includes(track.source.codec)) { throw new Error(`WebM only supports VP8, VP9 and AV1 as video codecs. Switching to MKV removes this restriction.`); } - } else { + } else if (track.type === "audio") { if (!["opus", "vorbis"].includes(track.source.codec)) { throw new Error(`WebM only supports Opus and Vorbis as audio codecs. Switching to MKV removes this restriction.`); } + } else if (track.type === "subtitle") { + if (track.source.codec !== "webvtt") { + throw new Error(`WebM only supports WebVTT as subtitle codec. Switching to MKV removes this restriction.`); + } + } else { + throw new Error("WebM only supports video, audio and subtitle tracks. Switching to MKV removes this restriction."); } } start() { @@ -2108,7 +1781,7 @@ var Metamuxer = (() => { tracksElement.data.push({ id: 174 /* TrackEntry */, data: [ { id: 215 /* TrackNumber */, data: trackData.track.id }, { id: 29637 /* TrackUID */, data: trackData.track.id }, - { id: 131 /* TrackType */, data: trackData.type === "video" ? VIDEO_TRACK_TYPE : AUDIO_TRACK_TYPE }, + { id: 131 /* TrackType */, data: TRACK_TYPE_MAP[trackData.type] }, // TODO Subtitle case { id: 134 /* CodecID */, data: CODEC_STRING_MAP[trackData.track.source.codec] }, trackData.info.decoderConfig.description ? { id: 25506 /* CodecPrivate */, data: toUint8Array(trackData.info.decoderConfig.description) } : null, @@ -2227,23 +1900,55 @@ var Metamuxer = (() => { this.#trackDatas.sort((a, b) => a.track.id - b.track.id); return newTrackData; } + #getSubtitleTrackData(track, meta) { + const existingTrackData = this.#trackDatas.find((x) => x.track === track); + if (existingTrackData) { + return existingTrackData; + } + assert(meta); + assert(meta.decoderConfig); + const newTrackData = { + track, + type: "subtitle", + info: { + decoderConfig: meta.decoderConfig + }, + chunkQueue: [], + firstTimestamp: null, + lastKeyFrameTimestamp: null, + lastWrittenMsTimestamp: null + }; + this.#trackDatas.push(newTrackData); + this.#trackDatas.sort((a, b) => a.track.id - b.track.id); + return newTrackData; + } addEncodedVideoChunk(track, chunk, meta) { const trackData = this.#getVideoTrackData(track, meta); - let videoChunk = this.#createInternalChunk(trackData, chunk); + let data = new Uint8Array(chunk.byteLength); + chunk.copyTo(data); + let videoChunk = this.#createInternalChunk(trackData, data, chunk.timestamp, chunk.duration ?? 0, chunk.type); if (track.source.codec === "vp9") this.#fixVP9ColorSpace(trackData, videoChunk); trackData.chunkQueue.push(videoChunk); this.#interleaveChunks(); - this.#writer.flush(); } addEncodedAudioChunk(track, chunk, meta) { const trackData = this.#getAudioTrackData(track, meta); - let audioChunk = this.#createInternalChunk(trackData, chunk); + let data = new Uint8Array(chunk.byteLength); + chunk.copyTo(data); + let audioChunk = this.#createInternalChunk(trackData, data, chunk.timestamp, chunk.duration ?? 0, chunk.type); trackData.chunkQueue.push(audioChunk); this.#interleaveChunks(); - this.#writer.flush(); + } + addEncodedSubtitleChunk(track, chunk, meta) { + const trackData = this.#getSubtitleTrackData(track, meta); + let subtitleChunk = this.#createInternalChunk(trackData, chunk.body, chunk.timestamp, chunk.duration, "key", chunk.additions); + trackData.chunkQueue.push(subtitleChunk); + this.#interleaveChunks(); } #interleaveChunks() { - if (this.#trackDatas.length < this.output.tracks.length) { + let openTrackCount = 0; + for (const trackData of this.#trackDatas) if (!trackData.track.source.closed) openTrackCount++; + if (this.#trackDatas.length < openTrackCount) { return; } outer: @@ -2251,10 +1956,10 @@ var Metamuxer = (() => { let trackWithMinTimestamp = null; let minTimestamp = Infinity; for (let trackData of this.#trackDatas) { - if (trackData.chunkQueue.length === 0) { + if (trackData.chunkQueue.length === 0 && !trackData.track.source.closed) { break outer; } - if (trackData.chunkQueue[0].timestamp < minTimestamp) { + if (trackData.chunkQueue.length > 0 && trackData.chunkQueue[0].timestamp < minTimestamp) { trackWithMinTimestamp = trackData; minTimestamp = trackData.chunkQueue[0].timestamp; } @@ -2265,6 +1970,7 @@ var Metamuxer = (() => { let chunk = trackWithMinTimestamp.chunkQueue.shift(); this.#writeBlock(trackWithMinTimestamp, chunk); } + this.#writer.flush(); } /** Due to [a bug in Chromium](https://bugs.chromium.org/p/chromium/issues/detail?id=1377842), VP9 streams often * lack color space information. This method patches in that information. */ @@ -2297,91 +2003,20 @@ var Metamuxer = (() => { }[trackData.info.decoderConfig.colorSpace.matrix]; writeBits(chunk.data, i + 0, i + 3, colorSpaceID); } - /* - addSubtitleChunk(chunk: EncodedSubtitleChunk, meta: EncodedSubtitleChunkMetadata, timestamp?: number) { - if (typeof chunk !== 'object' || !chunk) { - throw new TypeError("addSubtitleChunk's first argument (chunk) must be an object."); - } else { - // We can't simply do an instanceof check, so let's check the structure itself: - if (!(chunk.body instanceof Uint8Array)) { - throw new TypeError('body must be an instance of Uint8Array.'); - } - if (!Number.isFinite(chunk.timestamp) || chunk.timestamp < 0) { - throw new TypeError('timestamp must be a non-negative real number.'); - } - if (!Number.isFinite(chunk.duration) || chunk.duration < 0) { - throw new TypeError('duration must be a non-negative real number.'); - } - if (chunk.additions && !(chunk.additions instanceof Uint8Array)) { - throw new TypeError('additions, when present, must be an instance of Uint8Array.'); - } - } - - if (typeof meta !== 'object') { - throw new TypeError("addSubtitleChunk's second argument (meta) must be an object."); - } - - this.#ensureNotFinalized(); - if (!this.#options.subtitles) throw new Error('No subtitle track declared.'); - - // Write possible subtitle decoder metadata to the file - if (meta?.decoderConfig) { - if (this.#options.streaming) { - this.#subtitleCodecPrivate = this.#createCodecPrivateElement(meta.decoderConfig.description); - } else { - this.#writeCodecPrivate(this.#subtitleCodecPrivate, meta.decoderConfig.description); - } - } - - let subtitleChunk = this.#createInternalChunk( - chunk.body, - 'key', - timestamp ?? chunk.timestamp, - SUBTITLE_TRACK_NUMBER, - chunk.duration, - chunk.additions - ); - - this.#lastSubtitleTimestamp = subtitleChunk.timestamp; - this.#subtitleChunkQueue.push(subtitleChunk); - - this.#writeSubtitleChunks(); - this.#maybeFlushStreamingTargetWriter(); - } - - #writeSubtitleChunks() { - // Writing subtitle chunks is different from video and audio: A subtitle chunk will be written if it's - // guaranteed that no more media chunks will be written before it, to ensure monotonicity. However, media chunks - // will NOT wait for subtitle chunks to arrive, as they may never arrive, so that's how non-monotonicity can - // arrive. But it should be fine, since it's all still in one cluster. - - let lastWrittenMediaTimestamp = Math.min( - this.#options.video ? this.#lastVideoTimestamp : Infinity, - this.#options.audio ? this.#lastAudioTimestamp : Infinity - ); - - let queue = this.#subtitleChunkQueue; - while (queue.length > 0 && queue[0].timestamp <= lastWrittenMediaTimestamp) { - this.#writeBlock(queue.shift(), !this.#options.video && !this.#options.audio); - } - } - */ /** Converts a read-only external chunk into an internal one for easier use. */ - #createInternalChunk(trackData, chunk) { - let adjustedTimestamp = this.#validateTimestamp(trackData, chunk); - let data = new Uint8Array(chunk.byteLength); - chunk.copyTo(data); + #createInternalChunk(trackData, data, timestamp, duration, type, additions = null) { + let adjustedTimestamp = this.#validateTimestamp(trackData, timestamp, type === "key"); let internalChunk = { data, - type: chunk.type, + type, timestamp: adjustedTimestamp, - duration: (chunk.duration ?? 0) / 1e6, - additions: null + duration: duration / 1e6, + additions }; return internalChunk; } - #validateTimestamp(trackData, chunk) { - let timestampInSeconds = chunk.timestamp / 1e6; + #validateTimestamp(trackData, timestamp, isKeyFrame) { + let timestampInSeconds = timestamp / 1e6; if (timestampInSeconds < 0) { throw new Error(`Timestamps must be non-negative (got ${timestampInSeconds}s).`); } @@ -2392,7 +2027,7 @@ var Metamuxer = (() => { if (trackData.lastKeyFrameTimestamp !== null && timestampInSeconds < trackData.lastKeyFrameTimestamp) { throw new Error(`Timestamp cannot be before last key frame's timestamp (got ${timestampInSeconds}s, last key frame at ${trackData.lastKeyFrameTimestamp}s).`); } - if (chunk.type === "key") { + if (isKeyFrame) { trackData.lastKeyFrameTimestamp = timestampInSeconds; } return timestampInSeconds; @@ -2488,6 +2123,9 @@ var Metamuxer = (() => { }) ] }); } + onTrackClose() { + this.#interleaveChunks(); + } /** Finalizes the file, making it ready for use. Must be called after all media chunks have been added. */ finalize() { for (let trackData of this.#trackDatas) { @@ -2542,6 +2180,499 @@ var Metamuxer = (() => { var WebMOutputFormat = class extends MkvOutputFormat2 { }; + // src/codec.ts + var buildVideoCodecString = (codec, width, height) => { + if (codec === "avc") { + let profileIndication = 100; + if (width <= 768 && height <= 432) { + profileIndication = 66; + } else if (width <= 1920 && height <= 1080) { + profileIndication = 77; + } + const profileCompatibility = 0; + const levelIndication = width > 1920 || height > 1080 ? 50 : 41; + const hexProfileIndication = profileIndication.toString(16).padStart(2, "0"); + const hexProfileCompatibility = profileCompatibility.toString(16).padStart(2, "0"); + const hexLevelIndication = levelIndication.toString(16).padStart(2, "0"); + return `avc1.${hexProfileIndication}${hexProfileCompatibility}${hexLevelIndication}`; + } else if (codec === "hevc") { + let profileSpace = 0; + let profileIdc = 1; + const compatibilityFlags = Array(32).fill(0); + compatibilityFlags[profileIdc] = 1; + const compatibilityHex = parseInt(compatibilityFlags.reverse().join(""), 2).toString(16).replace(/^0+/, ""); + let tier = "L"; + let level = 120; + if (width <= 1280 && height <= 720) { + level = 93; + } else if (width <= 1920 && height <= 1080) { + level = 120; + } else if (width <= 3840 && height <= 2160) { + level = 150; + } else { + tier = "H"; + level = 180; + } + const constraintFlags = "B0"; + const profilePrefix = profileSpace === 0 ? "" : String.fromCharCode(65 + profileSpace - 1); + return `hev1.${profilePrefix}${profileIdc}.${compatibilityHex}.${tier}${level}.${constraintFlags}`; + } else if (codec === "vp8") { + return "vp8"; + } else if (codec === "vp9") { + const profile = "00"; + let level; + if (width <= 854 && height <= 480) { + level = "21"; + } else if (width <= 1280 && height <= 720) { + level = "31"; + } else if (width <= 1920 && height <= 1080) { + level = "41"; + } else if (width <= 3840 && height <= 2160) { + level = "51"; + } else { + level = "61"; + } + const bitDepth = "08"; + return `vp09.${profile}.${level}.${bitDepth}`; + } else if (codec === "av1") { + const profile = 0; + let level; + if (width <= 854 && height <= 480) { + level = "01"; + } else if (width <= 1280 && height <= 720) { + level = "03"; + } else if (width <= 1920 && height <= 1080) { + level = "04"; + } else if (width <= 3840 && height <= 2160) { + level = "07"; + } else { + level = "09"; + } + const tier = "M"; + const bitDepth = "08"; + return `av01.${profile}.${level}${tier}.${bitDepth}`; + } + throw new Error(`Unhandled codec '${codec}'.`); + }; + var buildAudioCodecString = (codec, numberOfChannels, sampleRate) => { + if (codec === "aac") { + if (numberOfChannels >= 2 && sampleRate <= 24e3) { + return "mp4a.40.29"; + } + if (sampleRate <= 24e3) { + return "mp4a.40.5"; + } + return "mp4a.40.2"; + } else if (codec === "opus") { + return "opus"; + } else if (codec === "vorbis") { + return "vorbis"; + } + throw new Error(`Unhandled codec '${codec}'.`); + }; + + // src/subtitles.ts + var cueBlockHeaderRegex = /(?:(.+?)\n)?((?:\d{2}:)?\d{2}:\d{2}.\d{3})\s+-->\s+((?:\d{2}:)?\d{2}:\d{2}.\d{3})/g; + var preambleStartRegex = /^WEBVTT.*?\n{2}/; + var timestampRegex = /(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})/; + var inlineTimestampRegex = /<(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})>/g; + var textEncoder = new TextEncoder(); + var SubtitleEncoder = class { + #options; + #config = null; + #preambleBytes = null; + #preambleEmitted = false; + constructor(options) { + this.#options = options; + } + configure(config) { + if (config.codec !== "webvtt") { + throw new Error("Codec must be 'webvtt'."); + } + this.#config = config; + } + encode(text) { + if (!this.#config) { + throw new Error("Encoder not configured."); + } + text = text.replace("\r\n", "\n").replace("\r", "\n"); + cueBlockHeaderRegex.lastIndex = 0; + let match; + if (!this.#preambleBytes) { + if (!preambleStartRegex.test(text)) { + let error = new Error("WebVTT preamble incorrect."); + this.#options.error(error); + throw error; + } + match = cueBlockHeaderRegex.exec(text); + let preamble = text.slice(0, match?.index ?? text.length).trimEnd(); + if (!preamble) { + let error = new Error("No WebVTT preamble provided."); + this.#options.error(error); + throw error; + } + this.#preambleBytes = textEncoder.encode(preamble); + if (match) { + text = text.slice(match.index); + cueBlockHeaderRegex.lastIndex = 0; + } + } + while (match = cueBlockHeaderRegex.exec(text)) { + let notes = text.slice(0, match.index); + let cueIdentifier = match[1] || ""; + let matchEnd = match.index + match[0].length; + let bodyStart = text.indexOf("\n", matchEnd) + 1; + let cueSettings = text.slice(matchEnd, bodyStart).trim(); + let bodyEnd = text.indexOf("\n\n", matchEnd); + if (bodyEnd === -1) bodyEnd = text.length; + let startTime = this.#parseTimestamp(match[2]); + let endTime = this.#parseTimestamp(match[3]); + let duration = endTime - startTime; + let body = text.slice(bodyStart, bodyEnd); + let additions = `${cueSettings} +${cueIdentifier} +${notes}`; + inlineTimestampRegex.lastIndex = 0; + body = body.replace(inlineTimestampRegex, (match2) => { + let time = this.#parseTimestamp(match2.slice(1, -1)); + let offsetTime = time - startTime; + return `<${this.#formatTimestamp(offsetTime)}>`; + }); + text = text.slice(bodyEnd).trimStart(); + cueBlockHeaderRegex.lastIndex = 0; + let chunk = { + body: textEncoder.encode(body), + additions: additions.trim() === "" ? null : textEncoder.encode(additions), + timestamp: startTime * 1e3, + duration: duration * 1e3 + }; + let meta = {}; + if (!this.#preambleEmitted) { + meta.decoderConfig = { + description: this.#preambleBytes + }; + this.#preambleEmitted = true; + } + this.#options.output(chunk, meta); + } + } + #parseTimestamp(string) { + let match = timestampRegex.exec(string); + if (!match) throw new Error("Expected match."); + return 60 * 60 * 1e3 * Number(match[1] || "0") + 60 * 1e3 * Number(match[2]) + 1e3 * Number(match[3]) + Number(match[4]); + } + #formatTimestamp(timestamp) { + let hours = Math.floor(timestamp / (60 * 60 * 1e3)); + let minutes = Math.floor(timestamp % (60 * 60 * 1e3) / (60 * 1e3)); + let seconds = Math.floor(timestamp % (60 * 1e3) / 1e3); + let milliseconds = timestamp % 1e3; + return hours.toString().padStart(2, "0") + ":" + minutes.toString().padStart(2, "0") + ":" + seconds.toString().padStart(2, "0") + "." + milliseconds.toString().padStart(3, "0"); + } + }; + + // src/source.ts + var MediaSource = class { + constructor() { + this.connectedTrack = null; + this.closed = false; + } + ensureValidDigest() { + if (!this.connectedTrack) { + throw new Error("Cannot call digest without connecting the source to an output track."); + } + if (!this.connectedTrack.output.started) { + throw new Error("Cannot call digest before output has been started."); + } + if (this.connectedTrack.output.finalizing) { + throw new Error("Cannot call digest after output has started finalizing."); + } + if (this.closed) { + throw new Error("Cannot call digest after source has been closed."); + } + } + // TODO: These are should not be called from the outside lib + start() { + } + async flush() { + } + close() { + if (this.closed) { + throw new Error("Source already closed."); + } + if (!this.connectedTrack) { + throw new Error("Cannot call close without connecting the source to an output track."); + } + if (!this.connectedTrack.output.started) { + throw new Error("Cannot call close before output has been started."); + } + this.closed = true; + if (this.connectedTrack.output.finalizing) { + return; + } + this.connectedTrack.output.muxer.onTrackClose(this.connectedTrack); + } + }; + var VideoSource = class extends MediaSource { + constructor(codec) { + super(); + this.connectedTrack = null; + this.codec = codec; + } + }; + var EncodedVideoChunkSource = class extends VideoSource { + constructor(codec) { + super(codec); + } + // TODO: Ensure that the first chunk is a key frame (same for the audio case) + digest(chunk, meta) { + this.ensureValidDigest(); + this.connectedTrack?.output.muxer.addEncodedVideoChunk(this.connectedTrack, chunk, meta); + } + }; + var KEY_FRAME_INTERVAL = 5; + var VideoEncoderWrapper = class { + constructor(source, codecConfig) { + this.source = source; + this.codecConfig = codecConfig; + this.encoder = null; + this.lastMultipleOfKeyFrameInterval = -1; + } + // TODO: Ensure video frame size remains constant + digest(videoFrame) { + this.source.ensureValidDigest(); + this.ensureEncoder(videoFrame); + assert(this.encoder); + const multipleOfKeyFrameInterval = Math.floor(videoFrame.timestamp / 1e6 / KEY_FRAME_INTERVAL); + this.encoder.encode(videoFrame, { keyFrame: multipleOfKeyFrameInterval !== this.lastMultipleOfKeyFrameInterval }); + this.lastMultipleOfKeyFrameInterval = multipleOfKeyFrameInterval; + } + ensureEncoder(videoFrame) { + if (this.encoder) { + return; + } + this.encoder = new VideoEncoder({ + output: (chunk, meta) => this.source.connectedTrack?.output.muxer.addEncodedVideoChunk(this.source.connectedTrack, chunk, meta), + error: (error) => console.error(error) + // TODO + }); + this.encoder.configure({ + codec: buildVideoCodecString(this.codecConfig.codec, videoFrame.codedWidth, videoFrame.codedHeight), + width: videoFrame.codedWidth, + height: videoFrame.codedHeight, + bitrate: this.codecConfig.bitrate + }); + } + async flush() { + return this.encoder?.flush(); + } + }; + var VideoFrameSource = class extends VideoSource { + constructor(codecConfig) { + super(codecConfig.codec); + this.encoder = new VideoEncoderWrapper(this, codecConfig); + } + digest(videoFrame) { + this.encoder.digest(videoFrame); + } + flush() { + return this.encoder.flush(); + } + }; + var CanvasSource = class extends VideoSource { + constructor(canvas, codecConfig) { + super(codecConfig.codec); + this.canvas = canvas; + this.encoder = new VideoEncoderWrapper(this, codecConfig); + } + digest(timestamp, duration = 0) { + const frame = new VideoFrame(this.canvas, { + timestamp: Math.round(1e6 * timestamp), + duration: Math.round(1e6 * duration) + }); + this.encoder.digest(frame); + frame.close(); + } + flush() { + return this.encoder.flush(); + } + }; + var MediaStreamVideoTrackSource = class extends VideoSource { + constructor(track, codecConfig) { + super(codecConfig.codec); + this.track = track; + this.abortController = null; + this.encoder = new VideoEncoderWrapper(this, codecConfig); + } + start() { + this.abortController = new AbortController(); + const processor = new MediaStreamTrackProcessor({ track: this.track }); + const consumer = new WritableStream({ + write: (videoFrame) => { + this.encoder.digest(videoFrame); + videoFrame.close(); + } + }); + processor.readable.pipeTo(consumer, { + signal: this.abortController.signal + }).catch((err) => { + if (err instanceof DOMException && err.name === "AbortError") return; + console.error("Pipe error:", err); + }); + } + async flush() { + if (this.abortController) { + this.abortController.abort(); + this.abortController = null; + } + await this.encoder.flush(); + } + }; + var AudioSource = class extends MediaSource { + constructor(codec) { + super(); + this.connectedTrack = null; + this.codec = codec; + } + }; + var EncodedAudioChunkSource = class extends AudioSource { + constructor(codec) { + super(codec); + } + digest(chunk, meta) { + this.ensureValidDigest(); + 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.ensureValidDigest(); + this.ensureEncoder(audioData); + assert(this.encoder); + this.encoder.encode(audioData); + } + ensureEncoder(audioData) { + if (this.encoder) { + return; + } + this.encoder = new AudioEncoder({ + output: (chunk, meta) => this.source.connectedTrack?.output.muxer.addEncodedAudioChunk(this.source.connectedTrack, chunk, meta), + error: (error) => console.error(error) + // TODO + }); + this.encoder.configure({ + codec: buildAudioCodecString(this.codecConfig.codec, audioData.numberOfChannels, audioData.sampleRate), + numberOfChannels: audioData.numberOfChannels, + sampleRate: audioData.sampleRate, + bitrate: this.codecConfig.bitrate + }); + } + async flush() { + return this.encoder?.flush(); + } + }; + var AudioDataSource = class extends AudioSource { + constructor(codecConfig) { + super(codecConfig.codec); + this.encoder = new AudioEncoderWrapper(this, codecConfig); + } + digest(audioData) { + this.encoder.digest(audioData); + } + flush() { + return this.encoder.flush(); + } + }; + var AudioBufferSource = class extends AudioSource { + constructor(codecConfig) { + super(codecConfig.codec); + this.accumulatedFrameCount = 0; + this.encoder = new AudioEncoderWrapper(this, codecConfig); + } + digest(audioBuffer) { + const numberOfChannels = audioBuffer.numberOfChannels; + const sampleRate = audioBuffer.sampleRate; + const numberOfFrames = audioBuffer.length; + const data = new Float32Array(numberOfChannels * numberOfFrames); + for (let channel = 0; channel < numberOfChannels; channel++) { + const channelData = audioBuffer.getChannelData(channel); + data.set(channelData, channel * numberOfFrames); + } + const audioData = new AudioData({ + format: "f32-planar", + sampleRate, + numberOfFrames, + numberOfChannels, + timestamp: Math.round(1e6 * this.accumulatedFrameCount / sampleRate), + data + }); + this.encoder.digest(audioData); + audioData.close(); + this.accumulatedFrameCount += numberOfFrames; + } + flush() { + return this.encoder.flush(); + } + }; + var MediaStreamAudioTrackSource = class extends AudioSource { + constructor(track, codecConfig) { + super(codecConfig.codec); + this.track = track; + this.abortController = null; + this.encoder = new AudioEncoderWrapper(this, codecConfig); + } + start() { + this.abortController = new AbortController(); + const processor = new MediaStreamTrackProcessor({ track: this.track }); + const consumer = new WritableStream({ + write: (audioData) => { + this.encoder.digest(audioData); + audioData.close(); + } + }); + processor.readable.pipeTo(consumer, { + signal: this.abortController.signal + }).catch((err) => { + if (err instanceof DOMException && err.name === "AbortError") return; + console.error("Pipe error:", err); + }); + } + async flush() { + if (this.abortController) { + this.abortController.abort(); + this.abortController = null; + } + await this.encoder.flush(); + } + }; + var SubtitleSource = class extends MediaSource { + constructor(codec) { + super(); + this.connectedTrack = null; + this.codec = codec; + } + }; + var TextSubtitleSource = class extends SubtitleSource { + constructor(codec) { + super(codec); + this.encoder = new SubtitleEncoder({ + output: (chunk, metadata) => this.connectedTrack?.output.muxer.addEncodedSubtitleChunk(this.connectedTrack, chunk, metadata), + error: (error) => console.error(error) + // TODO + }); + this.encoder.configure({ codec }); + } + digest(text) { + this.ensureValidDigest(); + this.encoder.encode(text); + } + }; + // src/writer.ts var Writer = class { }; @@ -2759,9 +2890,10 @@ var Metamuxer = (() => { }; // src/target.ts - var isTarget = Symbol("isTarget"); - isTarget; var Target = class { + constructor() { + this.output = null; + } }; var ArrayBufferTarget2 = class extends Target { constructor() { diff --git a/dist/metamuxer.min.js b/dist/metamuxer.min.js index 6810821..d04301c 100644 --- a/dist/metamuxer.min.js +++ b/dist/metamuxer.min.js @@ -1,2 +1,10 @@ -"use strict";var Metamuxer=(()=>{var me=Object.defineProperty;var De=Object.getOwnPropertyDescriptor;var We=Object.getOwnPropertyNames;var Ne=Object.prototype.hasOwnProperty;var Re=(r,i)=>{for(var e in i)me(r,e,{get:i[e],enumerable:!0})},Qe=(r,i,e,t)=>{if(i&&typeof i=="object"||typeof i=="function")for(let s of We(i))!Ne.call(r,s)&&s!==e&&me(r,s,{get:()=>i[s],enumerable:!(t=De(i,s))||t.enumerable});return r};var He=r=>Qe(me({},"__esModule",{value:!0}),r);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:()=>I,Target:()=>P,VideoFrameSource:()=>q,WebMOutputFormat:()=>U});var ye=(r,i,e)=>{if(r==="avc"){let t=100;i<=768&&e<=432?t=66:i<=1920&&e<=1080&&(t=77);let s=0,n=i>1920||e>1080?50:41,o=t.toString(16).padStart(2,"0"),l=s.toString(16).padStart(2,"0"),u=n.toString(16).padStart(2,"0");return`avc1.${o}${l}${u}`}else if(r==="hevc"){let t=0,s=1,n=Array(32).fill(0);n[s]=1;let o=parseInt(n.reverse().join(""),2).toString(16).replace(/^0+/,""),l="L",u=120;return i<=1280&&e<=720?u=93:i<=1920&&e<=1080?u=120:i<=3840&&e<=2160?u=150:(l="H",u=180),`hev1.${t===0?"":String.fromCharCode(65+t-1)}${s}.${o}.${l}${u}.B0`}else{if(r==="vp8")return"vp8";if(r==="vp9"){let t="00",s;return i<=854&&e<=480?s="21":i<=1280&&e<=720?s="31":i<=1920&&e<=1080?s="41":i<=3840&&e<=2160?s="51":s="61",`vp09.${t}.${s}.08`}else if(r==="av1"){let s;return i<=854&&e<=480?s="01":i<=1280&&e<=720?s="03":i<=1920&&e<=1080?s="04":i<=3840&&e<=2160?s="07":s="09",`av01.0.${s}M.08`}}throw new Error(`Unhandled codec '${r}'.`)},we=(r,i,e)=>{if(r==="aac")return i>=2&&e<=24e3?"mp4a.40.29":e<=24e3?"mp4a.40.5":"mp4a.40.2";if(r==="opus")return"opus";if(r==="vorbis")return"vorbis";throw new Error(`Unhandled codec '${r}'.`)};function d(r){if(!r)throw new Error("Assertion failed.")}var M=r=>r&&r[r.length-1],v=r=>r>=0&&r<2**32,V=(r,i,e)=>{let t=0;for(let s=i;s>l;t<<=1,t|=u}return t},Se=(r,i,e,t)=>{for(let s=i;s>e-s-1<r instanceof ArrayBuffer?new Uint8Array(r):new Uint8Array(r.buffer,r.byteOffset,r.byteLength);var S=class{constructor(i){this.connectedTrack=null;this.codec=i}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(i){this.connectedTrack=null;this.codec=i}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(i){super(i)}digest(i,e){this.ensureValidDigest(),this.connectedTrack?.output.muxer.addEncodedVideoChunk(this.connectedTrack,i,e)}},$e=5,D=class{constructor(i,e){this.source=i;this.codecConfig=e;this.encoder=null;this.lastMultipleOfKeyFrameInterval=-1}digest(i){this.source.ensureValidDigest(),this.ensureEncoder(i),d(this.encoder);let e=Math.floor(i.timestamp/1e6/$e);this.encoder.encode(i,{keyFrame:e!==this.lastMultipleOfKeyFrameInterval}),this.lastMultipleOfKeyFrameInterval=e}ensureEncoder(i){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,i.codedWidth,i.codedHeight),width:i.codedWidth,height:i.codedHeight,bitrate:this.codecConfig.bitrate}))}async flush(){return this.encoder?.flush()}},q=class extends S{constructor(i){super(i.codec),this.encoder=new D(this,i)}digest(i){this.encoder.digest(i)}flush(){return this.encoder.flush()}},X=class extends S{constructor(e,t){super(t.codec);this.canvas=e;this.encoder=new D(this,t)}digest(e,t=0){let s=new VideoFrame(this.canvas,{timestamp:Math.round(1e6*e),duration:Math.round(1e6*t)});this.encoder.digest(s),s.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 D(this,t)}start(){this.abortController=new AbortController;let e=new MediaStreamTrackProcessor({track:this.track}),t=new WritableStream({write:s=>{this.encoder.digest(s),s.close()}});e.readable.pipeTo(t,{signal:this.abortController.signal}).catch(s=>{s instanceof DOMException&&s.name==="AbortError"||console.error("Pipe error:",s)})}async flush(){this.abortController&&(this.abortController.abort(),this.abortController=null),await this.encoder.flush()}},L=class extends F{constructor(i){super(i)}digest(i,e){this.ensureNotFinalizing(),this.connectedTrack?.output.muxer.addEncodedAudioChunk(this.connectedTrack,i,e)}},W=class{constructor(i,e){this.source=i;this.codecConfig=e;this.encoder=null}digest(i){this.source.ensureNotFinalizing(),this.ensureEncoder(i),d(this.encoder),this.encoder.encode(i)}ensureEncoder(i){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,i.numberOfChannels,i.sampleRate),numberOfChannels:i.numberOfChannels,sampleRate:i.sampleRate,bitrate:this.codecConfig.bitrate}))}async flush(){return this.encoder?.flush()}},Z=class extends F{constructor(i){super(i.codec),this.encoder=new W(this,i)}digest(i){this.encoder.digest(i)}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,s=e.sampleRate,n=e.length,o=new Float32Array(t*n);for(let u=0;u{this.encoder.digest(s),s.close()}});e.readable.pipeTo(t,{signal:this.abortController.signal}).catch(s=>{s instanceof DOMException&&s.name==="AbortError"||console.error("Pipe error:",s)})}async flush(){this.abortController&&(this.abortController.abort(),this.abortController=null),await this.encoder.flush()}};var te=class{constructor(i){this.tracks=[];this.started=!1;this.finalizing=!1;this.writer=i.target.createWriter(),this.muxer=i.format.createMuxer(this)}addTrack(i,e={}){if(this.started)throw new Error("Cannot add track after output has started.");if(i.connectedTrack)throw new Error("Source is already used for a track.");let t={id:this.tracks.length+1,output:this,type:i instanceof S?"video":"audio",source:i,metadata:e};this.muxer.beforeTrackAdd(t),this.tracks.push(t),i.connectedTrack=t}start(){if(this.started)throw new Error("Output already started.");this.started=!0,this.muxer.start();for(let i of this.tracks)i.source.start()}async finalize(){if(this.finalizing)throw new Error("Cannot call finalize twice.");this.finalizing=!0;let i=this.tracks.map(e=>e.source.flush());await Promise.all(i),this.muxer.finalize(),this.writer.flush(),this.writer.finalize()}};var c=new Uint8Array(8),y=new DataView(c.buffer),b=r=>[(r%256+256)%256],m=r=>(y.setUint16(0,r,!1),[c[0],c[1]]),Ke=r=>(y.setInt16(0,r,!1),[c[0],c[1]]),Oe=r=>(y.setUint32(0,r,!1),[c[1],c[2],c[3]]),a=r=>(y.setUint32(0,r,!1),[c[0],c[1],c[2],c[3]]),Ge=r=>(y.setInt32(0,r,!1),[c[0],c[1],c[2],c[3]]),z=r=>(y.setUint32(0,Math.floor(r/2**32),!1),y.setUint32(4,r,!1),[c[0],c[1],c[2],c[3],c[4],c[5],c[6],c[7]]),pe=r=>(y.setInt16(0,2**8*r,!1),[c[0],c[1]]),x=r=>(y.setInt32(0,2**16*r,!1),[c[0],c[1],c[2],c[3]]),he=r=>(y.setInt32(0,2**30*r,!1),[c[0],c[1],c[2],c[3]]),k=(r,i=!1)=>{let e=Array(r.length).fill(null).map((t,s)=>r.charCodeAt(s));return i&&e.push(0),e},be=r=>{let i=null;for(let e of r)(!i||e.timestamp>i.timestamp)&&(i=e);return i},ve=r=>{let i=r*(Math.PI/180),e=Math.cos(i),t=Math.sin(i);return[e,t,0,-t,e,0,0,0,1]},Ve=ve(0),ze=r=>[x(r[0]),x(r[1]),he(r[2]),x(r[3]),x(r[4]),he(r[5]),x(r[6]),x(r[7]),he(r[8])],p=(r,i,e)=>({type:r,contents:i&&new Uint8Array(i.flat(10)),children:e}),h=(r,i,e,t,s)=>p(r,[b(i),Oe(e),t??[]],s),Ue=r=>{let i=512;return r.fragmented?p("ftyp",[k("iso5"),a(i),k("iso5"),k("iso6"),k("mp41")]):p("ftyp",[k("isom"),a(i),k("isom"),r.holdsAvc?k("avc1"):[],k("mp41")])},se=r=>({type:"mdat",largeSize:r}),Pe=r=>({type:"free",size:r}),N=(r,i,e=!1)=>p("moov",void 0,[je(i,r),...r.map(t=>qe(t,i)),e?Tt(r):null]),je=(r,i)=>{let e=T(Math.max(0,...i.filter(o=>o.samples.length>0).map(o=>{let l=be(o.samples);return l.timestamp+l.duration})),re),t=Math.max(...i.map(o=>o.track.id))+1,s=!v(r)||!v(e),n=s?z:a;return h("mvhd",+s,0,[n(r),n(r),a(re),n(e),x(1),pe(1),Array(10).fill(0),ze(Ve),Array(24).fill(0),a(t)])},qe=(r,i)=>p("trak",void 0,[Xe(r,i),Ye(r,i)]),Xe=(r,i)=>{let e=be(r.samples),t=T(e?e.timestamp+e.duration:0,re),s=!v(i)||!v(t),n=s?z:a,o;if(r.type==="video"){let l=r.track.metadata.rotation;o=l===void 0||typeof l=="number"?ve(l??0):l}else o=Ve;return h("tkhd",+s,3,[n(i),n(i),a(r.track.id),a(0),n(t),Array(8).fill(0),m(0),m(0),pe(r.type==="audio"?1:0),m(0),ze(o),x(r.type==="video"?r.info.width:0),x(r.type==="video"?r.info.height:0)])},Ye=(r,i)=>p("mdia",void 0,[Le(r,i),Ze(r.type==="video"?"vide":"soun"),Je(r)]),Le=(r,i)=>{let e=be(r.samples),t=T(e?e.timestamp+e.duration:0,r.timescale),s=!v(i)||!v(t),n=s?z:a;return h("mdhd",+s,0,[n(i),n(i),a(r.timescale),n(t),m(21956),m(0)])},Ze=r=>h("hdlr",0,0,[k("mhlr"),k(r),a(0),a(0),a(0),k("mp4-muxer-hdlr",!0)]),Je=r=>p("minf",void 0,[r.type==="video"?et():tt(),rt(),nt(r)]),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,[st()]),st=()=>h("dref",0,0,[a(1)],[it()]),it=()=>h("url ",0,1),nt=r=>{let i=r.compositionTimeOffsetTable.length>1||r.compositionTimeOffsetTable.some(e=>e.sampleCompositionTimeOffset!==0);return p("stbl",void 0,[ot(r),ht(r),pt(r),bt(r),gt(r),Ct(r),i?kt(r):null])},ot=r=>h("stsd",0,0,[a(1)],[r.type==="video"?at(zt[r.track.source.codec],r):ct(Pt[r.track.source.codec],r)]),at=(r,i)=>p(r,[Array(6).fill(0),m(1),m(0),m(0),Array(12).fill(0),m(i.info.width),m(i.info.height),a(4718592),a(4718592),a(0),m(1),Array(32).fill(0),m(24),Ke(65535)],[Ut[i.track.source.codec](i)]),ut=r=>r.info.decoderConfig&&p("avcC",[...E(r.info.decoderConfig.description)]),lt=r=>r.info.decoderConfig&&p("hvcC",[...E(r.info.decoderConfig.description)]),Ae=r=>{if(!r.info.decoderConfig)return null;let i=r.info.decoderConfig;if(!i.colorSpace)throw new Error("'colorSpace' is required in the decoder config for VP8/VP9.");let e=i.codec.split("."),t=Number(e[1]),s=Number(e[2]),l=(Number(e[3])<<4)+(0<<1)+Number(i.colorSpace.fullRange);return h("vpcC",1,0,[b(t),b(s),b(l),b(2),b(2),b(2),m(0)])},dt=()=>{let e=(1<<7)+1;return p("av1C",[e,0,0,0])},ct=(r,i)=>p(r,[Array(6).fill(0),m(1),m(0),m(0),a(0),m(i.info.numberOfChannels),m(16),m(0),m(0),x(i.info.sampleRate)],[Mt[i.track.source.codec](i)]),ft=r=>{let i=E(r.info.decoderConfig.description??new ArrayBuffer(0));return h("esds",0,0,[a(58753152),b(32+i.byteLength),m(1),b(0),a(75530368),b(18+i.byteLength),b(64),b(21),Oe(0),a(130071),a(130071),a(92307584),b(i.byteLength),...i,a(109084800),b(1),b(2)])},mt=r=>{let i=3840,e=0,t=r.info.decoderConfig?.description;if(t){if(t.byteLength<18)throw new TypeError("Invalid decoder description provided for Opus; must be at least 18 bytes long.");let s=ArrayBuffer.isView(t)?new DataView(t.buffer,t.byteOffset,t.byteLength):new DataView(t);i=s.getUint16(10,!0),e=s.getInt16(14,!0)}return p("dOps",[b(0),b(r.info.numberOfChannels),m(i),a(r.info.sampleRate),pe(e),b(0)])},ht=r=>h("stts",0,0,[a(r.timeToSampleTable.length),r.timeToSampleTable.map(i=>[a(i.sampleCount),a(i.sampleDelta)])]),pt=r=>{if(r.samples.every(e=>e.type==="key"))return null;let i=[...r.samples.entries()].filter(([,e])=>e.type==="key");return h("stss",0,0,[a(i.length),i.map(([e])=>a(e+1))])},bt=r=>h("stsc",0,0,[a(r.compactlyCodedChunkTable.length),r.compactlyCodedChunkTable.map(i=>[a(i.firstChunk),a(i.samplesPerChunk),a(1)])]),gt=r=>h("stsz",0,0,[a(0),a(r.samples.length),r.samples.map(i=>a(i.size))]),Ct=r=>r.finalizedChunks.length>0&&M(r.finalizedChunks).offset>=2**32?h("co64",0,0,[a(r.finalizedChunks.length),r.finalizedChunks.map(i=>z(i.offset))]):h("stco",0,0,[a(r.finalizedChunks.length),r.finalizedChunks.map(i=>a(i.offset))]),kt=r=>h("ctts",0,0,[a(r.compositionTimeOffsetTable.length),r.compositionTimeOffsetTable.map(i=>[a(i.sampleCount),a(i.sampleCompositionTimeOffset)])]),Tt=r=>p("mvex",void 0,r.map(xt)),xt=r=>h("trex",0,0,[a(r.track.id),a(1),a(0),a(0),a(0)]),ge=(r,i)=>p("moof",void 0,[yt(r),...i.map(wt)]),yt=r=>h("mfhd",0,0,[a(r)]),Me=r=>{let i=0,e=0,t=0,s=0,n=r.type==="delta";return e|=+n,n?i|=1:i|=2,i<<24|e<<16|t<<8|s},wt=r=>p("traf",void 0,[St(r),At(r),Ot(r)]),St=r=>{d(r.currentChunk);let i=0;i|=8,i|=16,i|=32,i|=131072;let e=r.currentChunk.samples[1]??r.currentChunk.samples[0],t={duration:e.timescaleUnitsToNextSample,size:e.size,flags:Me(e)};return h("tfhd",0,i,[a(r.track.id),a(t.duration),a(t.size),a(t.flags)])},At=r=>(d(r.currentChunk),h("tfdt",1,0,[z(T(r.currentChunk.startTimestamp,r.timescale))])),Ot=r=>{d(r.currentChunk);let i=r.currentChunk.samples.map(w=>w.timescaleUnitsToNextSample),e=r.currentChunk.samples.map(w=>w.size),t=r.currentChunk.samples.map(Me),s=r.currentChunk.samples.map(w=>T(w.timestamp-w.decodeTimestamp,r.timescale)),n=new Set(i),o=new Set(e),l=new Set(t),u=new Set(s),f=l.size===2&&t[0]!==t[1],g=n.size>1,C=o.size>1,A=!f&&l.size>1,xe=u.size>1||[...u].some(w=>w!==0),O=0;return O|=1,O|=4*+f,O|=256*+g,O|=512*+C,O|=1024*+A,O|=2048*+xe,h("trun",1,O,[a(r.currentChunk.samples.length),a(r.currentChunk.offset-r.currentChunk.moofOffset||0),f?a(t[0]):[],r.currentChunk.samples.map((w,G)=>[g?a(i[G]):[],C?a(e[G]):[],A?a(t[G]):[],xe?Ge(s[G]):[]])])},Ee=r=>p("mfra",void 0,[...r.map(vt),Vt()]),vt=(r,i)=>h("tfra",1,0,[a(r.track.id),a(63),a(r.finalizedChunks.length),r.finalizedChunks.map(t=>[z(T(t.startTimestamp,r.timescale)),z(t.moofOffset),a(i+1),a(1),a(1)])]),Vt=()=>h("mfro",0,0,[a(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"},Mt={aac:ft,opus:mt};var B=class{constructor(i){this.output=i}beforeTrackAdd(i){}};var re=1e3,Et=2082844800,T=(r,i,e=!0)=>{let t=r*i;return e?Math.round(t):t},ie=class extends B{constructor(e,t){super(e);this.#s=new Uint8Array(8);this.#t=new DataView(this.#s.buffer);this.offsets=new WeakMap;this.#o=null;this.#n=null;this.#i=[];this.#a=Math.floor(Date.now()/1e3)+Et;this.#l=[];this.#f=1;this.#e=e.writer,this.#r=t}#e;#r;#s;#t;#o;#n;#i;#a;#l;#f;writeU32(e){this.#t.setUint32(0,e,!1),this.#e.write(this.#s.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.#s.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=se(!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=se(!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 s of t)s&&(e+=8*Math.ceil(2/3*s),e+=4*s,e+=12*Math.ceil(2/3*s),e+=4*s,e+=8*s);return e+=4096,e}#u(e,t){let s=this.#i.find(o=>o.track===e);if(s)return s;d(t),d(t.decoderConfig),d(t.decoderConfig.codedWidth!==void 0),d(t.decoderConfig.codedHeight!==void 0);let n={track:e,type:"video",info:{width:t.decoderConfig.codedWidth,height:t.decoderConfig.codedHeight,decoderConfig:t.decoderConfig},timescale:e.metadata.frameRate??57600,samples:[],sampleQueue:[],timestampProcessingQueue:[],firstTimestamp:null,lastKeyFrameTimestamp:null,timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.#i.push(n),this.#i.sort((o,l)=>o.track.id-l.track.id),n}#m(e,t){let s=this.#i.find(o=>o.track===e);if(s)return s;d(t),d(t.decoderConfig);let n={track:e,type:"audio",info:{numberOfChannels:t.decoderConfig.numberOfChannels,sampleRate:t.decoderConfig.sampleRate,decoderConfig:t.decoderConfig},timescale:t.decoderConfig.sampleRate,samples:[],sampleQueue:[],timestampProcessingQueue:[],firstTimestamp:null,lastKeyFrameTimestamp:null,timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.#i.push(n),this.#i.sort((o,l)=>o.track.id-l.track.id),n}addEncodedVideoChunk(e,t,s){let n=this.#u(e,s);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,s){let n=this.#m(e,s);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 s=this.#T(e,t),n=(t.duration??0)/1e6,o=new Uint8Array(t.byteLength);return t.copyTo(o),{timestamp:s,decodeTimestamp:s,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(s=>s.timestamp).sort((s,n)=>s-n);for(let s=0;s{if(e===l)return t.type==="key";let u=l.sampleQueue[0];return u&&u.type==="key"});n>=1&&o&&(s=!0,this.#k())}else s=n>=.5}s&&(e.currentChunk&&this.#C(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 s=t.timestamp/1e6;if(s<0)throw new Error(`Timestamps must be non-negative (got ${s}s).`);if(e.firstTimestamp===null&&(e.firstTimestamp=s),s-=e.firstTimestamp,e.lastKeyFrameTimestamp!==null&&s=2**32&&(u.largeSize=!0,g=this.measureBox(u)+f),u.size=g,this.writeBox(u)}for(let u of this.#i){u.currentChunk.offset=this.#e.getPos(),u.currentChunk.moofOffset=s;for(let f of u.currentChunk.samples)this.#e.write(f.data),f.data=null}let o=this.#e.getPos();this.#e.seek(this.offsets.get(n));let l=ge(t,this.#i);this.writeBox(l),this.#e.seek(o);for(let u of this.#i)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.#i){for(let t of e.sampleQueue)this.#p(e,t);this.#c(e)}this.#k(!1)}else for(let e of this.#i)this.#c(e),this.#C(e);if(this.#r.options.fastStart==="in-memory"){d(this.#n);let e;for(let s=0;s<2;s++){let n=N(this.#i,this.#a),o=this.measureBox(n);e=this.measureBox(this.#n);let l=this.#e.getPos()+o+e;for(let u of this.#l){u.offset=l;for(let{data:f}of u.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.#i,this.#a);this.writeBox(t),this.#n.size=e,this.writeBox(this.#n);for(let s of this.#l)for(let n of s.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.#i);this.writeBox(t);let s=this.#e.getPos()-e;this.#e.seek(this.#e.getPos()-4),this.writeU32(s)}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 s=N(this.#i,this.#a);if(typeof this.#r.options.fastStart=="object"){this.#e.seek(this.#o),this.writeBox(s);let n=e-this.#e.getPos();this.writeBox(Pe(n))}else this.writeBox(s)}}};var R=class{constructor(i){this.value=i}},_=class{constructor(i){this.value=i}},Q=class{constructor(i){this.value=i}};var Ce=r=>r<256?1:r<65536?2:r<1<<24?3:r<2**32?4:r<2**40?5:6,ke=r=>r>=-64&&r<64?1:r>=-8192&&r<8192?2:r>=-(1<<20)&&r<1<<20?3:r>=-(1<<27)&&r<1<<27?4:r>=-(2**34)&&r<2**34?5:6,Fe=r=>{if(r<127)return 1;if(r<16383)return 2;if(r<(1<<21)-1)return 3;if(r<(1<<28)-1)return 4;if(r<2**35-1)return 5;if(r<2**42-1)return 6;throw new Error("EBML VINT size not supported "+r)};var Ft=1,Bt=2,Te=2**15,Be="https://github.com/Vanilagy/webm-muxer",_e=6,Ie=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.#s=new Uint8Array(8);this.#t=new DataView(this.#s.buffer);this.offsets=new WeakMap;this.dataOffsets=new WeakMap;this.#o=[];this.#n=null;this.#i=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;#s;#t;#o;#n;#i;#a;#l;#f;#d;#u;#m;#h;#c;#p(e){this.#t.setUint8(0,e),this.#e.write(this.#s.subarray(0,1))}#T(e){this.#t.setFloat32(0,e,!1),this.#e.write(this.#s.subarray(0,4))}#C(e){this.#t.setFloat64(0,e,!1),this.#e.write(this.#s)}#b(e,t=Ce(e)){let s=0;switch(t){case 6:this.#t.setUint8(s++,e/2**40|0);case 5:this.#t.setUint8(s++,e/2**32|0);case 4:this.#t.setUint8(s++,e>>24);case 3:this.#t.setUint8(s++,e>>16);case 2:this.#t.setUint8(s++,e>>8);case 1:this.#t.setUint8(s++,e);break;default:throw new Error("Bad UINT size "+t)}this.#e.write(this.#s.subarray(0,s))}#k(e,t=ke(e)){e<0&&(e+=2**(t*8)),this.#b(e,t)}writeEBMLVarInt(e,t=Fe(e)){let s=0;switch(t){case 1:this.#t.setUint8(s++,128|e);break;case 2:this.#t.setUint8(s++,64|e>>8),this.#t.setUint8(s++,e);break;case 3:this.#t.setUint8(s++,32|e>>16),this.#t.setUint8(s++,e>>8),this.#t.setUint8(s++,e);break;case 4:this.#t.setUint8(s++,16|e>>24),this.#t.setUint8(s++,e>>16),this.#t.setUint8(s++,e>>8),this.#t.setUint8(s++,e);break;case 5:this.#t.setUint8(s++,8|e/2**32&7),this.#t.setUint8(s++,e>>24),this.#t.setUint8(s++,e>>16),this.#t.setUint8(s++,e>>8),this.#t.setUint8(s++,e);break;case 6:this.#t.setUint8(s++,4|e/2**40&3),this.#t.setUint8(s++,e/2**32|0),this.#t.setUint8(s++,e>>24),this.#t.setUint8(s++,e>>16),this.#t.setUint8(s++,e>>8),this.#t.setUint8(s++,e);break;default:throw new Error("Bad EBML VINT size "+t)}this.#e.write(this.#s.subarray(0,s))}#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(),s=e.size===-1?1:e.size??4;e.size===-1?this.#p(255):this.#e.seek(this.#e.getPos()+s);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,s),this.#e.seek(l)}}else if(typeof e.data=="number"){let t=e.size??Ce(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.#C(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]),s=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:s},{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.#i=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:E(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 s=t.info.decoderConfig.colorSpace;return!s.matrix||!s.transfer||!s.primaries||s.fullRange==null?null:{id:21936,data:[{id:21937,data:{rgb:1,bt709:1,bt470bg:5,smpte170m:6}[s.matrix]},{id:21946,data:{bt709:1,smpte170m:6,"iec61966-2-1":13}[s.transfer]},{id:21947,data:{bt709:1,bt470bg:5,smpte170m:6}[s.primaries]},{id:21945,data:[1,2][Number(s.fullRange)]}]}}return null})()]}]:[],...t.type==="audio"?[{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.#i,this.#l]};this.#n=e,this.writeEBML(e)}#P(){this.#d={id:475249515,data:[]}}get#g(){return d(this.#n),this.dataOffsets.get(this.#n)}#M(e,t){let s=this.#o.find(o=>o.track===e);if(s)return s;d(t),d(t.decoderConfig),d(t.decoderConfig.codedWidth!==void 0),d(t.decoderConfig.codedHeight!==void 0);let n={track:e,type:"video",info:{width:t.decoderConfig.codedWidth,height:t.decoderConfig.codedHeight,decoderConfig:t.decoderConfig},chunkQueue:[],firstTimestamp:null,lastKeyFrameTimestamp:null,lastWrittenMsTimestamp:null};return this.#o.push(n),this.#o.sort((o,l)=>o.track.id-l.track.id),n}#E(e,t){let s=this.#o.find(o=>o.track===e);if(s)return s;d(t),d(t.decoderConfig);let n={track:e,type:"audio",info:{numberOfChannels:t.decoderConfig.numberOfChannels,sampleRate:t.decoderConfig.sampleRate,decoderConfig:t.decoderConfig},chunkQueue:[],firstTimestamp:null,lastKeyFrameTimestamp:null,lastWrittenMsTimestamp:null};return this.#o.push(n),this.#o.sort((o,l)=>o.track.id-l.track.id),n}addEncodedVideoChunk(e,t,s){let n=this.#M(e,s),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,s){let n=this.#E(e,s),o=this.#y(n,t);n.chunkQueue.push(o),this.#x(),this.#e.flush()}#x(){if(!(this.#o.length=2&&s++;let f={rgb:7,bt709:2,bt470bg:1,smpte170m:3}[e.info.decoderConfig.colorSpace.matrix];Se(t.data,s+0,s+3,f)}#y(e,t){let s=this.#B(e,t),n=new Uint8Array(t.byteLength);return t.copyTo(n),{data:n,type:t.type,timestamp:s,duration:(t.duration??0)/1e6,additions:null}}#B(e,t){let s=t.timestamp/1e6;if(s<0)throw new Error(`Timestamps must be non-negative (got ${s}s).`);if(e.firstTimestamp===null&&(e.firstTimestamp=s),s-=e.firstTimestamp,e.lastKeyFrameTimestamp!==null&&s{if(e===C)return t.type==="key";let A=C.chunkQueue[0];return A&&A.type==="key"});(!this.#u||n&&s-this.#m>=1e3)&&this.#_(s);let o=s-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 u=new Uint8Array(4),f=new DataView(u.buffer);f.setUint8(0,128|e.track.id),f.setInt16(1,o,!1);let g=Math.floor(1e3*t.duration);if(g===0&&!t.additions){f.setUint8(3,+(t.type==="key")<<7);let C={id:163,data:[u,t.data]};this.writeEBML(C)}else{let C={id:160,data:[{id:161,data:[u,t.data]},t.type==="delta"?{id:251,data:new Q(e.lastWrittenMsTimestamp-s)}:null,g>0?{id:155,data:g}:null,t.additions?{id:30113,data:t.additions}:null]};this.writeEBML(C)}this.#c=Math.max(this.#c,s+g),e.lastWrittenMsTimestamp=s,this.#h.add(e)}#_(e){this.#u&&!this.#r.options.streaming&&this.#S(),this.#u={id:524531317,size:this.#r.options.streaming?-1:Ie,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,Ie),this.#e.seek(t);let s=this.offsets.get(this.#u)-this.#g;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:s}]}))]})}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.#g;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.#g,this.#a.data[1].data[1].data=this.offsets.get(this.#i)-this.#g,this.#a.data[2].data[1].data=this.offsets.get(this.#l)-this.#g,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 ie(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;#s=new ArrayBuffer(2**16);#t=new Uint8Array(this.#s);#o=0;constructor(i){super(),this.#r=i}#n(i){let e=this.#s.byteLength;for(;et.start-s.start);i.push({start:e[0].start,size:e[0].data.byteLength});for(let t=1;tu.start<=e&&eDt){for(let u=0;u=i.written[n+1].start;)i.written[n].end=Math.max(i.written[n].end,i.written[n+1].end),i.written.splice(n+1,1)}#i(i){let t={start:Math.floor(i/this.#s)*this.#s,data:new Uint8Array(this.#s),written:[],shouldFlush:!1};return this.#t.push(t),this.#t.sort((s,n)=>s.start-n.start),this.#t.indexOf(t)}#a(i=!1){for(let e=0;ei.stream.write({type:"write",data:e,position:t}),chunkSize:i.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)}},I=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);})(); +"use strict";var Metamuxer=(()=>{var ge=Object.defineProperty;var Ge=Object.getOwnPropertyDescriptor;var je=Object.getOwnPropertyNames;var qe=Object.prototype.hasOwnProperty;var Xe=(s,r)=>{for(var e in r)ge(s,e,{get:r[e],enumerable:!0})},Le=(s,r,e,t)=>{if(r&&typeof r=="object"||typeof r=="function")for(let i of je(r))!qe.call(s,i)&&i!==e&&ge(s,i,{get:()=>r[i],enumerable:!(t=Ge(r,i))||t.enumerable});return s};var Ye=s=>Le(ge({},"__esModule",{value:!0}),s);var qt={};Xe(qt,{ArrayBufferTarget:()=>be,AudioBufferSource:()=>de,AudioDataSource:()=>le,CanvasSource:()=>oe,EncodedAudioChunkSource:()=>ue,EncodedVideoChunkSource:()=>se,FileSystemWritableFileStreamTarget:()=>ke,MediaStreamAudioTrackSource:()=>ce,MediaStreamVideoTrackSource:()=>ae,MkvOutputFormat:()=>H,Mp4OutputFormat:()=>re,Output:()=>L,StreamTarget:()=>_,Target:()=>U,TextSubtitleSource:()=>he,VideoFrameSource:()=>ne,WebMOutputFormat:()=>E});var L=class{constructor(r){this.tracks=[];this.started=!1;this.finalizing=!1;if(r.target.output)throw new Error("Target is already used for another output.");r.target.output=this,this.writer=r.target.createWriter(),this.muxer=r.format.createMuxer(this)}addVideoTrack(r,e={}){this.addTrack("video",r,e)}addAudioTrack(r,e={}){this.addTrack("audio",r,e)}addSubtitleTrack(r,e={}){this.addTrack("subtitle",r,e)}addTrack(r,e,t){if(this.started)throw new Error("Cannot add track after output has started.");if(e.connectedTrack)throw new Error("Source is already used for a track.");let i={id:this.tracks.length+1,output:this,type:r,source:e,metadata:t};this.muxer.beforeTrackAdd(i),this.tracks.push(i),e.connectedTrack=i}start(){if(this.started)throw new Error("Output already started.");this.started=!0,this.muxer.start();for(let r of this.tracks)r.source.start()}async finalize(){if(this.finalizing)throw new Error("Cannot call finalize twice.");this.finalizing=!0;let r=this.tracks.map(e=>e.source.flush());await Promise.all(r),this.muxer.finalize(),this.writer.flush(),this.writer.finalize()}};function d(s){if(!s)throw new Error("Assertion failed.")}var M=s=>s&&s[s.length-1],O=s=>s>=0&&s<2**32,v=(s,r,e)=>{let t=0;for(let i=r;i>l;t<<=1,t|=a}return t},Ve=(s,r,e,t)=>{for(let i=r;i>e-i-1<s instanceof ArrayBuffer?new Uint8Array(s):new Uint8Array(s.buffer,s.byteOffset,s.byteLength);var h=new Uint8Array(8),w=new DataView(h.buffer),k=s=>[(s%256+256)%256],m=s=>(w.setUint16(0,s,!1),[h[0],h[1]]),Ze=s=>(w.setInt16(0,s,!1),[h[0],h[1]]),Ue=s=>(w.setUint32(0,s,!1),[h[1],h[2],h[3]]),u=s=>(w.setUint32(0,s,!1),[h[0],h[1],h[2],h[3]]),Je=s=>(w.setInt32(0,s,!1),[h[0],h[1],h[2],h[3]]),V=s=>(w.setUint32(0,Math.floor(s/2**32),!1),w.setUint32(4,s,!1),[h[0],h[1],h[2],h[3],h[4],h[5],h[6],h[7]]),Ce=s=>(w.setInt16(0,2**8*s,!1),[h[0],h[1]]),S=s=>(w.setInt32(0,2**16*s,!1),[h[0],h[1],h[2],h[3]]),Te=s=>(w.setInt32(0,2**30*s,!1),[h[0],h[1],h[2],h[3]]),x=(s,r=!1)=>{let e=Array(s.length).fill(null).map((t,i)=>s.charCodeAt(i));return r&&e.push(0),e},xe=s=>{let r=null;for(let e of s)(!r||e.timestamp>r.timestamp)&&(r=e);return r},ze=s=>{let r=s*(Math.PI/180),e=Math.cos(r),t=Math.sin(r);return[e,t,0,-t,e,0,0,0,1]},Me=ze(0),Pe=s=>[S(s[0]),S(s[1]),Te(s[2]),S(s[3]),S(s[4]),Te(s[5]),S(s[6]),S(s[7]),Te(s[8])],p=(s,r,e)=>({type:s,contents:r&&new Uint8Array(r.flat(10)),children:e}),f=(s,r,e,t,i)=>p(s,[k(r),Ue(e),t??[]],i),Fe=s=>{let r=512;return s.fragmented?p("ftyp",[x("iso5"),u(r),x("iso5"),x("iso6"),x("mp41")]):p("ftyp",[x("isom"),u(r),x("isom"),s.holdsAvc?x("avc1"):[],x("mp41")])},Z=s=>({type:"mdat",largeSize:s}),De=s=>({type:"free",size:s}),N=(s,r,e=!1)=>p("moov",void 0,[et(r,s),...s.map(t=>tt(t,r)),e?vt(s):null]),et=(s,r)=>{let e=y(Math.max(0,...r.filter(o=>o.samples.length>0).map(o=>{let l=xe(o.samples);return l.timestamp+l.duration})),Y),t=Math.max(...r.map(o=>o.track.id))+1,i=!O(s)||!O(e),n=i?V:u;return f("mvhd",+i,0,[n(s),n(s),u(Y),n(e),S(1),Ce(1),Array(10).fill(0),Pe(Me),Array(24).fill(0),u(t)])},tt=(s,r)=>p("trak",void 0,[rt(s,r),it(s,r)]),rt=(s,r)=>{let e=xe(s.samples),t=y(e?e.timestamp+e.duration:0,Y),i=!O(r)||!O(t),n=i?V:u,o;if(s.type==="video"){let l=s.track.metadata.rotation;o=l===void 0||typeof l=="number"?ze(l??0):l}else o=Me;return f("tkhd",+i,3,[n(r),n(r),u(s.track.id),u(0),n(t),Array(8).fill(0),m(0),m(0),Ce(s.type==="audio"?1:0),m(0),Pe(o),S(s.type==="video"?s.info.width:0),S(s.type==="video"?s.info.height:0)])},it=(s,r)=>p("mdia",void 0,[st(s,r),nt(s.type==="video"?"vide":"soun"),ot(s)]),st=(s,r)=>{let e=xe(s.samples),t=y(e?e.timestamp+e.duration:0,s.timescale),i=!O(r)||!O(t),n=i?V:u;return f("mdhd",+i,0,[n(r),n(r),u(s.timescale),n(t),m(21956),m(0)])},nt=s=>f("hdlr",0,0,[x("mhlr"),x(s),u(0),u(0),u(0),x("mp4-muxer-hdlr",!0)]),ot=s=>p("minf",void 0,[s.type==="video"?at():ut(),lt(),ht(s)]),at=()=>f("vmhd",0,1,[m(0),m(0),m(0),m(0)]),ut=()=>f("smhd",0,0,[m(0),m(0)]),lt=()=>p("dinf",void 0,[dt()]),dt=()=>f("dref",0,0,[u(1)],[ct()]),ct=()=>f("url ",0,1),ht=s=>{let r=s.compositionTimeOffsetTable.length>1||s.compositionTimeOffsetTable.some(e=>e.sampleCompositionTimeOffset!==0);return p("stbl",void 0,[mt(s),xt(s),yt(s),St(s),wt(s),At(s),r?Ot(s):null])},mt=s=>f("stsd",0,0,[u(1)],[s.type==="video"?ft(Bt[s.track.source.codec],s):gt(_t[s.track.source.codec],s)]),ft=(s,r)=>p(s,[Array(6).fill(0),m(1),m(0),m(0),Array(12).fill(0),m(r.info.width),m(r.info.height),u(4718592),u(4718592),u(0),m(1),Array(32).fill(0),m(24),Ze(65535)],[It[r.track.source.codec](r)]),pt=s=>s.info.decoderConfig&&p("avcC",[...P(s.info.decoderConfig.description)]),bt=s=>s.info.decoderConfig&&p("hvcC",[...P(s.info.decoderConfig.description)]),Ee=s=>{if(!s.info.decoderConfig)return null;let r=s.info.decoderConfig;if(!r.colorSpace)throw new Error("'colorSpace' is required in the decoder config for VP8/VP9.");let e=r.codec.split("."),t=Number(e[1]),i=Number(e[2]),l=(Number(e[3])<<4)+(0<<1)+Number(r.colorSpace.fullRange);return f("vpcC",1,0,[k(t),k(i),k(l),k(2),k(2),k(2),m(0)])},kt=()=>{let e=(1<<7)+1;return p("av1C",[e,0,0,0])},gt=(s,r)=>p(s,[Array(6).fill(0),m(1),m(0),m(0),u(0),m(r.info.numberOfChannels),m(16),m(0),m(0),S(r.info.sampleRate)],[Wt[r.track.source.codec](r)]),Tt=s=>{let r=P(s.info.decoderConfig.description??new ArrayBuffer(0));return f("esds",0,0,[u(58753152),k(32+r.byteLength),m(1),k(0),u(75530368),k(18+r.byteLength),k(64),k(21),Ue(0),u(130071),u(130071),u(92307584),k(r.byteLength),...r,u(109084800),k(1),k(2)])},Ct=s=>{let r=3840,e=0,t=s.info.decoderConfig?.description;if(t){if(t.byteLength<18)throw new TypeError("Invalid decoder description provided for Opus; must be at least 18 bytes long.");let i=ArrayBuffer.isView(t)?new DataView(t.buffer,t.byteOffset,t.byteLength):new DataView(t);r=i.getUint16(10,!0),e=i.getInt16(14,!0)}return p("dOps",[k(0),k(s.info.numberOfChannels),m(r),u(s.info.sampleRate),Ce(e),k(0)])},xt=s=>f("stts",0,0,[u(s.timeToSampleTable.length),s.timeToSampleTable.map(r=>[u(r.sampleCount),u(r.sampleDelta)])]),yt=s=>{if(s.samples.every(e=>e.type==="key"))return null;let r=[...s.samples.entries()].filter(([,e])=>e.type==="key");return f("stss",0,0,[u(r.length),r.map(([e])=>u(e+1))])},St=s=>f("stsc",0,0,[u(s.compactlyCodedChunkTable.length),s.compactlyCodedChunkTable.map(r=>[u(r.firstChunk),u(r.samplesPerChunk),u(1)])]),wt=s=>f("stsz",0,0,[u(0),u(s.samples.length),s.samples.map(r=>u(r.size))]),At=s=>s.finalizedChunks.length>0&&M(s.finalizedChunks).offset>=2**32?f("co64",0,0,[u(s.finalizedChunks.length),s.finalizedChunks.map(r=>V(r.offset))]):f("stco",0,0,[u(s.finalizedChunks.length),s.finalizedChunks.map(r=>u(r.offset))]),Ot=s=>f("ctts",0,0,[u(s.compositionTimeOffsetTable.length),s.compositionTimeOffsetTable.map(r=>[u(r.sampleCount),u(r.sampleCompositionTimeOffset)])]),vt=s=>p("mvex",void 0,s.map(Vt)),Vt=s=>f("trex",0,0,[u(s.track.id),u(1),u(0),u(0),u(0)]),ye=(s,r)=>p("moof",void 0,[Et(s),...r.map(Ut)]),Et=s=>f("mfhd",0,0,[u(s)]),Be=s=>{let r=0,e=0,t=0,i=0,n=s.type==="delta";return e|=+n,n?r|=1:r|=2,r<<24|e<<16|t<<8|i},Ut=s=>p("traf",void 0,[zt(s),Mt(s),Pt(s)]),zt=s=>{d(s.currentChunk);let r=0;r|=8,r|=16,r|=32,r|=131072;let e=s.currentChunk.samples[1]??s.currentChunk.samples[0],t={duration:e.timescaleUnitsToNextSample,size:e.size,flags:Be(e)};return f("tfhd",0,r,[u(s.track.id),u(t.duration),u(t.size),u(t.flags)])},Mt=s=>(d(s.currentChunk),f("tfdt",1,0,[V(y(s.currentChunk.startTimestamp,s.timescale))])),Pt=s=>{d(s.currentChunk);let r=s.currentChunk.samples.map(T=>T.timescaleUnitsToNextSample),e=s.currentChunk.samples.map(T=>T.size),t=s.currentChunk.samples.map(Be),i=s.currentChunk.samples.map(T=>y(T.timestamp-T.decodeTimestamp,s.timescale)),n=new Set(r),o=new Set(e),l=new Set(t),a=new Set(i),c=l.size===2&&t[0]!==t[1],b=n.size>1,g=o.size>1,C=!c&&l.size>1,W=a.size>1||[...a].some(T=>T!==0),A=0;return A|=1,A|=4*+c,A|=256*+b,A|=512*+g,A|=1024*+C,A|=2048*+W,f("trun",1,A,[u(s.currentChunk.samples.length),u(s.currentChunk.offset-s.currentChunk.moofOffset||0),c?u(t[0]):[],s.currentChunk.samples.map((T,z)=>[b?u(r[z]):[],g?u(e[z]):[],C?u(t[z]):[],W?Je(i[z]):[]])])},Ie=s=>p("mfra",void 0,[...s.map(Ft),Dt()]),Ft=(s,r)=>f("tfra",1,0,[u(s.track.id),u(63),u(s.finalizedChunks.length),s.finalizedChunks.map(t=>[V(y(t.startTimestamp,s.timescale)),V(t.moofOffset),u(r+1),u(1),u(1)])]),Dt=()=>f("mfro",0,0,[u(0)]),Bt={avc:"avc1",hevc:"hvc1",vp8:"vp08",vp9:"vp09",av1:"av01"},It={avc:pt,hevc:bt,vp8:Ee,vp9:Ee,av1:kt},_t={aac:"mp4a",opus:"Opus"},Wt={aac:Tt,opus:Ct};var F=class{constructor(r){this.output=r}beforeTrackAdd(r){}onTrackClose(r){}};var Y=1e3,Nt=2082844800,y=(s,r,e=!0)=>{let t=s*r;return e?Math.round(t):t},J=class extends F{constructor(e,t){super(e);this.#i=new Uint8Array(8);this.#t=new DataView(this.#i.buffer);this.offsets=new WeakMap;this.#s=null;this.#o=null;this.#n=[];this.#a=Math.floor(Date.now()/1e3)+Nt;this.#l=[];this.#h=1;this.#e=e.writer,this.#r=t}#e;#r;#i;#t;#s;#o;#n;#a;#l;#h;writeU32(e){this.#t.setUint32(0,e,!1),this.#e.write(this.#i.subarray(0,4))}writeU64(e){this.#t.setUint32(0,Math.floor(e/2**32),!1),this.#t.setUint32(4,e,!1),this.#e.write(this.#i.subarray(0,8))}writeAscii(e){for(let t=0;tt.type==="video"&&t.source.codec==="avc");if(this.writeBox(Fe({holdsAvc:e,fragmented:this.#r.options.fastStart==="fragmented"})),this.#s=this.#e.getPos(),this.#r.options.fastStart==="in-memory")this.#o=Z(!1);else if(this.#r.options.fastStart!=="fragmented"){if(typeof this.#r.options.fastStart=="object"){let t=this.#d();this.#e.seek(this.#e.getPos()+t)}this.#o=Z(!0),this.writeBox(this.#o)}this.#e.flush()}#d(){d(typeof this.#r.options.fastStart=="object");let e=0,t=[this.#r.options.fastStart.expectedVideoChunks,this.#r.options.fastStart.expectedAudioChunks];for(let i of t)i&&(e+=8*Math.ceil(2/3*i),e+=4*i,e+=12*Math.ceil(2/3*i),e+=4*i,e+=8*i);return e+=4096,e}#u(e,t){let i=this.#n.find(o=>o.track===e);if(i)return i;d(t),d(t.decoderConfig),d(t.decoderConfig.codedWidth!==void 0),d(t.decoderConfig.codedHeight!==void 0);let n={track:e,type:"video",info:{width:t.decoderConfig.codedWidth,height:t.decoderConfig.codedHeight,decoderConfig:t.decoderConfig},timescale:e.metadata.frameRate??57600,samples:[],sampleQueue:[],timestampProcessingQueue:[],firstTimestamp:null,lastKeyFrameTimestamp:null,timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.#n.push(n),this.#n.sort((o,l)=>o.track.id-l.track.id),n}#m(e,t){let i=this.#n.find(o=>o.track===e);if(i)return i;d(t),d(t.decoderConfig);let n={track:e,type:"audio",info:{numberOfChannels:t.decoderConfig.numberOfChannels,sampleRate:t.decoderConfig.sampleRate,decoderConfig:t.decoderConfig},timescale:t.decoderConfig.sampleRate,samples:[],sampleQueue:[],timestampProcessingQueue:[],firstTimestamp:null,lastKeyFrameTimestamp:null,timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.#n.push(n),this.#n.sort((o,l)=>o.track.id-l.track.id),n}addEncodedVideoChunk(e,t,i){let n=this.#u(e,i);if(typeof this.#r.options.fastStart=="object"&&n.samples.length===this.#r.options.fastStart.expectedVideoChunks)throw new Error(`Cannot add more video chunks than specified in 'fastStart' (${this.#r.options.fastStart.expectedVideoChunks}).`);let o=this.#f(n,t);this.#r.options.fastStart==="fragmented"?(n.sampleQueue.push(o),this.#b()):this.#p(n,o)}addEncodedAudioChunk(e,t,i){let n=this.#m(e,i);if(typeof this.#r.options.fastStart=="object"&&n.samples.length===this.#r.options.fastStart.expectedAudioChunks)throw new Error(`Cannot add more audio chunks than specified in 'fastStart' (${this.#r.options.fastStart.expectedAudioChunks}).`);let o=this.#f(n,t);this.#r.options.fastStart==="fragmented"?(n.sampleQueue.push(o),this.#b()):this.#p(n,o)}#f(e,t){let i=this.#x(e,t),n=(t.duration??0)/1e6,o=new Uint8Array(t.byteLength);return t.copyTo(o),{timestamp:i,decodeTimestamp:i,duration:n,data:o,size:o.byteLength,type:t.type,timescaleUnitsToNextSample:y(n,e.timescale)}}#c(e){if(e.timestampProcessingQueue.length===0)return;let t=e.timestampProcessingQueue.map(i=>i.timestamp).sort((i,n)=>i-n);for(let i=0;i{if(e===l)return t.type==="key";let a=l.sampleQueue[0];return a&&a.type==="key"});n>=1&&o&&(i=!0,this.#T())}else i=n>=.5}i&&(e.currentChunk&&this.#g(e),e.currentChunk={startTimestamp:t.timestamp,samples:[],offset:null,moofOffset:null}),d(e.currentChunk),e.currentChunk.samples.push(t),e.timestampProcessingQueue.push(t)}#x(e,t){let i=t.timestamp/1e6;if(i<0)throw new Error(`Timestamps must be non-negative (got ${i}s).`);if(e.firstTimestamp===null&&(e.firstTimestamp=i),i-=e.firstTimestamp,e.lastKeyFrameTimestamp!==null&&i=2**32&&(a.largeSize=!0,b=this.measureBox(a)+c),a.size=b,this.writeBox(a)}for(let a of this.#n){a.currentChunk.offset=this.#e.getPos(),a.currentChunk.moofOffset=i;for(let c of a.currentChunk.samples)this.#e.write(c.data),c.data=null}let o=this.#e.getPos();this.#e.seek(this.offsets.get(n));let l=ye(t,this.#n);this.writeBox(l),this.#e.seek(o);for(let a of this.#n)a.finalizedChunks.push(a.currentChunk),this.#l.push(a.currentChunk),a.currentChunk=null;e&&this.#e.flush()}finalize(){if(this.#r.options.fastStart==="fragmented"){for(let e of this.#n){for(let t of e.sampleQueue)this.#p(e,t);this.#c(e)}this.#T(!1)}else for(let e of this.#n)this.#c(e),this.#g(e);if(this.#r.options.fastStart==="in-memory"){d(this.#o);let e;for(let i=0;i<2;i++){let n=N(this.#n,this.#a),o=this.measureBox(n);e=this.measureBox(this.#o);let l=this.#e.getPos()+o+e;for(let a of this.#l){a.offset=l;for(let{data:c}of a.samples)d(c),l+=c.byteLength,e+=c.byteLength}if(l<2**32)break;e>=2**32&&(this.#o.largeSize=!0)}let t=N(this.#n,this.#a);this.writeBox(t),this.#o.size=e,this.writeBox(this.#o);for(let i of this.#l)for(let n of i.samples)d(n.data),this.#e.write(n.data),n.data=null}else if(this.#r.options.fastStart==="fragmented"){let e=this.#e.getPos(),t=Ie(this.#n);this.writeBox(t);let i=this.#e.getPos()-e;this.#e.seek(this.#e.getPos()-4),this.writeU32(i)}else{d(this.#o),d(this.#s!==null);let e=this.offsets.get(this.#o);d(e!==void 0);let t=this.#e.getPos()-e;this.#o.size=t,this.#o.largeSize=t>=2**32,this.patchBox(this.#o);let i=N(this.#n,this.#a);if(typeof this.#r.options.fastStart=="object"){this.#e.seek(this.#s),this.writeBox(i);let n=e-this.#e.getPos();this.writeBox(De(n))}else this.writeBox(i)}}};var R=class{constructor(r){this.value=r}},D=class{constructor(r){this.value=r}},Q=class{constructor(r){this.value=r}};var Se=s=>s<256?1:s<65536?2:s<1<<24?3:s<2**32?4:s<2**40?5:6,we=s=>s>=-64&&s<64?1:s>=-8192&&s<8192?2:s>=-(1<<20)&&s<1<<20?3:s>=-(1<<27)&&s<1<<27?4:s>=-(2**34)&&s<2**34?5:6,_e=s=>{if(s<127)return 1;if(s<16383)return 2;if(s<(1<<21)-1)return 3;if(s<(1<<28)-1)return 4;if(s<2**35-1)return 5;if(s<2**42-1)return 6;throw new Error("EBML VINT size not supported "+s)};var Ae=2**15,We="https://github.com/Vanilagy/webm-muxer",Ne=6,Re=5,Rt={avc:"V_MPEG4/ISO/AVC",hevc:"V_MPEGH/ISO/HEVC",vp8:"V_VP8",vp9:"V_VP9",av1:"V_AV1",aac:"A_AAC",opus:"A_OPUS",vorbis:"A_VORBIS",webvtt:"S_TEXT/WEBVTT"},Qt={video:1,audio:2,subtitle:17},ee=class extends F{constructor(e,t){super(e);this.#i=new Uint8Array(8);this.#t=new DataView(this.#i.buffer);this.offsets=new WeakMap;this.dataOffsets=new WeakMap;this.#s=[];this.#o=null;this.#n=null;this.#a=null;this.#l=null;this.#h=null;this.#d=null;this.#u=null;this.#m=null;this.#f=new Set;this.#c=0;this.#e=e.writer,this.#r=t}#e;#r;#i;#t;#s;#o;#n;#a;#l;#h;#d;#u;#m;#f;#c;#p(e){this.#t.setUint8(0,e),this.#e.write(this.#i.subarray(0,1))}#x(e){this.#t.setFloat32(0,e,!1),this.#e.write(this.#i.subarray(0,4))}#g(e){this.#t.setFloat64(0,e,!1),this.#e.write(this.#i)}#b(e,t=Se(e)){let i=0;switch(t){case 6:this.#t.setUint8(i++,e/2**40|0);case 5:this.#t.setUint8(i++,e/2**32|0);case 4:this.#t.setUint8(i++,e>>24);case 3:this.#t.setUint8(i++,e>>16);case 2:this.#t.setUint8(i++,e>>8);case 1:this.#t.setUint8(i++,e);break;default:throw new Error("Bad UINT size "+t)}this.#e.write(this.#i.subarray(0,i))}#T(e,t=we(e)){e<0&&(e+=2**(t*8)),this.#b(e,t)}writeEBMLVarInt(e,t=_e(e)){let i=0;switch(t){case 1:this.#t.setUint8(i++,128|e);break;case 2:this.#t.setUint8(i++,64|e>>8),this.#t.setUint8(i++,e);break;case 3:this.#t.setUint8(i++,32|e>>16),this.#t.setUint8(i++,e>>8),this.#t.setUint8(i++,e);break;case 4:this.#t.setUint8(i++,16|e>>24),this.#t.setUint8(i++,e>>16),this.#t.setUint8(i++,e>>8),this.#t.setUint8(i++,e);break;case 5:this.#t.setUint8(i++,8|e/2**32&7),this.#t.setUint8(i++,e>>24),this.#t.setUint8(i++,e>>16),this.#t.setUint8(i++,e>>8),this.#t.setUint8(i++,e);break;case 6:this.#t.setUint8(i++,4|e/2**40&3),this.#t.setUint8(i++,e/2**32|0),this.#t.setUint8(i++,e>>24),this.#t.setUint8(i++,e>>16),this.#t.setUint8(i++,e>>8),this.#t.setUint8(i++,e);break;default:throw new Error("Bad EBML VINT size "+t)}this.#e.write(this.#i.subarray(0,i))}#A(e){this.#e.write(new Uint8Array(e.split("").map(t=>t.charCodeAt(0))))}writeEBML(e){if(e!==null){if(e instanceof Uint8Array)this.#e.write(e);else if(Array.isArray(e))for(let t of e)this.writeEBML(t);else if(this.offsets.set(e,this.#e.getPos()),this.#b(e.id),Array.isArray(e.data)){let t=this.#e.getPos(),i=e.size===-1?1:e.size??4;e.size===-1?this.#p(255):this.#e.seek(this.#e.getPos()+i);let n=this.#e.getPos();if(this.dataOffsets.set(e,n),this.writeEBML(e.data),e.size!==-1){let o=this.#e.getPos()-n,l=this.#e.getPos();this.#e.seek(t),this.writeEBMLVarInt(o,i),this.#e.seek(l)}}else if(typeof e.data=="number"){let t=e.size??Se(e.data);this.writeEBMLVarInt(t),this.#b(e.data,t)}else if(typeof e.data=="string")this.writeEBMLVarInt(e.data.length),this.#A(e.data);else if(e.data instanceof Uint8Array)this.writeEBMLVarInt(e.data.byteLength,e.size),this.#e.write(e.data);else if(e.data instanceof R)this.writeEBMLVarInt(4),this.#x(e.data.value);else if(e.data instanceof D)this.writeEBMLVarInt(8),this.#g(e.data.value);else if(e.data instanceof Q){let t=e.size??we(e.data.value);this.writeEBMLVarInt(t),this.#T(e.data.value,t)}}}beforeTrackAdd(e){if(this.#r instanceof E)if(e.type==="video"){if(!["vp8","vp9","av1"].includes(e.source.codec))throw new Error("WebM only supports VP8, VP9 and AV1 as video codecs. Switching to MKV removes this restriction.")}else if(e.type==="audio"){if(!["opus","vorbis"].includes(e.source.codec))throw new Error("WebM only supports Opus and Vorbis as audio codecs. Switching to MKV removes this restriction.")}else if(e.type==="subtitle"){if(e.source.codec!=="webvtt")throw new Error("WebM only supports WebVTT as subtitle codec. Switching to MKV removes this restriction.")}else throw new Error("WebM only supports video, audio and subtitle tracks. Switching to MKV removes this restriction.")}start(){this.#O(),this.#r.options.streaming||this.#v(),this.#V(),this.#z(),this.#e.flush()}#O(){let e={id:440786851,data:[{id:17030,data:1},{id:17143,data:1},{id:17138,data:4},{id:17139,data:8},{id:17026,data:this.#r instanceof E?"webm":"matroska"},{id:17031,data:2},{id:17029,data:2}]};this.writeEBML(e)}#v(){let e=new Uint8Array([28,83,187,107]),t=new Uint8Array([21,73,169,102]),i=new Uint8Array([22,84,174,107]),n={id:290298740,data:[{id:19899,data:[{id:21419,data:e},{id:21420,size:5,data:0}]},{id:19899,data:[{id:21419,data:t},{id:21420,size:5,data:0}]},{id:19899,data:[{id:21419,data:i},{id:21420,size:5,data:0}]}]};this.#a=n}#V(){let e={id:17545,data:new D(0)};this.#h=e;let t={id:357149030,data:[{id:2807729,data:1e6},{id:19840,data:We},{id:22337,data:We},this.#r.options.streaming?null:e]};this.#n=t}#E(){let e={id:374648427,data:[]};this.#l=e;for(let t of this.#s)e.data.push({id:174,data:[{id:215,data:t.track.id},{id:29637,data:t.track.id},{id:131,data:Qt[t.type]},{id:134,data:Rt[t.track.source.codec]},t.info.decoderConfig.description?{id:25506,data:P(t.info.decoderConfig.description)}:null,...t.type==="video"?[t.track.metadata.frameRate?{id:2352003,data:1e9/t.track.metadata.frameRate}:null,{id:224,data:[{id:176,data:t.info.width},{id:186,data:t.info.height},(()=>{if(t.info.decoderConfig.colorSpace){let i=t.info.decoderConfig.colorSpace;return!i.matrix||!i.transfer||!i.primaries||i.fullRange==null?null:{id:21936,data:[{id:21937,data:{rgb:1,bt709:1,bt470bg:5,smpte170m:6}[i.matrix]},{id:21946,data:{bt709:1,smpte170m:6,"iec61966-2-1":13}[i.transfer]},{id:21947,data:{bt709:1,bt470bg:5,smpte170m:6}[i.primaries]},{id:21945,data:[1,2][Number(i.fullRange)]}]}}return null})()]}]:[],...t.type==="audio"?[{id:225,data:[{id:181,data:new R(t.info.sampleRate)},{id:159,data:t.info.numberOfChannels}]}]:[]]})}#U(){let e={id:408125543,size:this.#r.options.streaming?-1:Ne,data:[this.#r.options.streaming?null:this.#a,this.#n,this.#l]};this.#o=e,this.writeEBML(e)}#z(){this.#d={id:475249515,data:[]}}get#k(){return d(this.#o),this.dataOffsets.get(this.#o)}#M(e,t){let i=this.#s.find(o=>o.track===e);if(i)return i;d(t),d(t.decoderConfig),d(t.decoderConfig.codedWidth!==void 0),d(t.decoderConfig.codedHeight!==void 0);let n={track:e,type:"video",info:{width:t.decoderConfig.codedWidth,height:t.decoderConfig.codedHeight,decoderConfig:t.decoderConfig},chunkQueue:[],firstTimestamp:null,lastKeyFrameTimestamp:null,lastWrittenMsTimestamp:null};return this.#s.push(n),this.#s.sort((o,l)=>o.track.id-l.track.id),n}#P(e,t){let i=this.#s.find(o=>o.track===e);if(i)return i;d(t),d(t.decoderConfig);let n={track:e,type:"audio",info:{numberOfChannels:t.decoderConfig.numberOfChannels,sampleRate:t.decoderConfig.sampleRate,decoderConfig:t.decoderConfig},chunkQueue:[],firstTimestamp:null,lastKeyFrameTimestamp:null,lastWrittenMsTimestamp:null};return this.#s.push(n),this.#s.sort((o,l)=>o.track.id-l.track.id),n}#F(e,t){let i=this.#s.find(o=>o.track===e);if(i)return i;d(t),d(t.decoderConfig);let n={track:e,type:"subtitle",info:{decoderConfig:t.decoderConfig},chunkQueue:[],firstTimestamp:null,lastKeyFrameTimestamp:null,lastWrittenMsTimestamp:null};return this.#s.push(n),this.#s.sort((o,l)=>o.track.id-l.track.id),n}addEncodedVideoChunk(e,t,i){let n=this.#M(e,i),o=new Uint8Array(t.byteLength);t.copyTo(o);let l=this.#y(n,o,t.timestamp,t.duration??0,t.type);e.source.codec==="vp9"&&this.#D(n,l),n.chunkQueue.push(l),this.#C()}addEncodedAudioChunk(e,t,i){let n=this.#P(e,i),o=new Uint8Array(t.byteLength);t.copyTo(o);let l=this.#y(n,o,t.timestamp,t.duration??0,t.type);n.chunkQueue.push(l),this.#C()}addEncodedSubtitleChunk(e,t,i){let n=this.#F(e,i),o=this.#y(n,t.body,t.timestamp,t.duration,"key",t.additions);n.chunkQueue.push(o),this.#C()}#C(){let e=0;for(let t of this.#s)t.track.source.closed||e++;if(!(this.#s.length0&&o.chunkQueue[0].timestamp=2&&i++;let c={rgb:7,bt709:2,bt470bg:1,smpte170m:3}[e.info.decoderConfig.colorSpace.matrix];Ve(t.data,i+0,i+3,c)}#y(e,t,i,n,o,l=null){let a=this.#B(e,i,o==="key");return{data:t,type:o,timestamp:a,duration:n/1e6,additions:l}}#B(e,t,i){let n=t/1e6;if(n<0)throw new Error(`Timestamps must be non-negative (got ${n}s).`);if(e.firstTimestamp===null&&(e.firstTimestamp=n),n-=e.firstTimestamp,e.lastKeyFrameTimestamp!==null&&n{if(e===g)return t.type==="key";let C=g.chunkQueue[0];return C&&C.type==="key"});(!this.#u||n&&i-this.#m>=1e3)&&this.#I(i);let o=i-this.#m;if(o<0)return;if(o>=Ae)throw new Error(`Current Matroska cluster exceeded its maximum allowed length of ${Ae} milliseconds. In order to produce a correct WebM file, you must pass in a key frame at least every ${Ae} milliseconds.`);let a=new Uint8Array(4),c=new DataView(a.buffer);c.setUint8(0,128|e.track.id),c.setInt16(1,o,!1);let b=Math.floor(1e3*t.duration);if(b===0&&!t.additions){c.setUint8(3,+(t.type==="key")<<7);let g={id:163,data:[a,t.data]};this.writeEBML(g)}else{let g={id:160,data:[{id:161,data:[a,t.data]},t.type==="delta"?{id:251,data:new Q(e.lastWrittenMsTimestamp-i)}:null,b>0?{id:155,data:b}:null,t.additions?{id:30113,data:t.additions}:null]};this.writeEBML(g)}this.#c=Math.max(this.#c,i+b),e.lastWrittenMsTimestamp=i,this.#f.add(e)}#I(e){this.#u&&!this.#r.options.streaming&&this.#w(),this.#u={id:524531317,size:this.#r.options.streaming?-1:Re,data:[{id:231,data:e}]},this.writeEBML(this.#u),this.#m=e,this.#f.clear()}#w(){d(this.#u);let e=this.#e.getPos()-this.dataOffsets.get(this.#u),t=this.#e.getPos();this.#e.seek(this.offsets.get(this.#u)+4),this.writeEBMLVarInt(e,Re),this.#e.seek(t);let i=this.offsets.get(this.#u)-this.#k;d(this.#d),this.#d.data.push({id:187,data:[{id:179,data:this.#m},...[...this.#f].map(n=>({id:183,data:[{id:247,data:n.track.id},{id:241,data:i}]}))]})}onTrackClose(){this.#C()}finalize(){for(let e of this.#s)for(;e.chunkQueue.length>0;)this.#S(e,e.chunkQueue.shift());if(this.#r.options.streaming||this.#w(),d(this.#d),this.writeEBML(this.#d),!this.#r.options.streaming){let e=this.#e.getPos(),t=this.#e.getPos()-this.#k;this.#e.seek(this.offsets.get(this.#o)+4),this.writeEBMLVarInt(t,Ne),this.#h.data=new D(this.#c),this.#e.seek(this.offsets.get(this.#h)),this.writeEBML(this.#h),this.#a.data[0].data[1].data=this.offsets.get(this.#d)-this.#k,this.#a.data[1].data[1].data=this.offsets.get(this.#n)-this.#k,this.#a.data[2].data[1].data=this.offsets.get(this.#l)-this.#k,this.#e.seek(this.offsets.get(this.#a)),this.writeEBML(this.#a),this.#e.seek(e)}}};var te=class{},re=class extends te{constructor(e){super();this.options=e}createMuxer(e){return new J(e,this)}},H=class extends te{constructor(e={}){super();this.options=e}createMuxer(e){return new ee(e,this)}},E=class extends H{};var Qe=(s,r,e)=>{if(s==="avc"){let t=100;r<=768&&e<=432?t=66:r<=1920&&e<=1080&&(t=77);let i=0,n=r>1920||e>1080?50:41,o=t.toString(16).padStart(2,"0"),l=i.toString(16).padStart(2,"0"),a=n.toString(16).padStart(2,"0");return`avc1.${o}${l}${a}`}else if(s==="hevc"){let t=0,i=1,n=Array(32).fill(0);n[i]=1;let o=parseInt(n.reverse().join(""),2).toString(16).replace(/^0+/,""),l="L",a=120;return r<=1280&&e<=720?a=93:r<=1920&&e<=1080?a=120:r<=3840&&e<=2160?a=150:(l="H",a=180),`hev1.${t===0?"":String.fromCharCode(65+t-1)}${i}.${o}.${l}${a}.B0`}else{if(s==="vp8")return"vp8";if(s==="vp9"){let t="00",i;return r<=854&&e<=480?i="21":r<=1280&&e<=720?i="31":r<=1920&&e<=1080?i="41":r<=3840&&e<=2160?i="51":i="61",`vp09.${t}.${i}.08`}else if(s==="av1"){let i;return r<=854&&e<=480?i="01":r<=1280&&e<=720?i="03":r<=1920&&e<=1080?i="04":r<=3840&&e<=2160?i="07":i="09",`av01.0.${i}M.08`}}throw new Error(`Unhandled codec '${s}'.`)},He=(s,r,e)=>{if(s==="aac")return r>=2&&e<=24e3?"mp4a.40.29":e<=24e3?"mp4a.40.5":"mp4a.40.2";if(s==="opus")return"opus";if(s==="vorbis")return"vorbis";throw new Error(`Unhandled codec '${s}'.`)};var $=/(?:(.+?)\n)?((?:\d{2}:)?\d{2}:\d{2}.\d{3})\s+-->\s+((?:\d{2}:)?\d{2}:\d{2}.\d{3})/g,Ht=/^WEBVTT.*?\n{2}/,$t=/(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})/,$e=/<(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})>/g,Oe=new TextEncoder,ie=class{#e;#r=null;#i=null;#t=!1;constructor(r){this.#e=r}configure(r){if(r.codec!=="webvtt")throw new Error("Codec must be 'webvtt'.");this.#r=r}encode(r){if(!this.#r)throw new Error("Encoder not configured.");r=r.replace(`\r +`,` +`).replace("\r",` +`),$.lastIndex=0;let e;if(!this.#i){if(!Ht.test(r)){let i=new Error("WebVTT preamble incorrect.");throw this.#e.error(i),i}e=$.exec(r);let t=r.slice(0,e?.index??r.length).trimEnd();if(!t){let i=new Error("No WebVTT preamble provided.");throw this.#e.error(i),i}this.#i=Oe.encode(t),e&&(r=r.slice(e.index),$.lastIndex=0)}for(;e=$.exec(r);){let t=r.slice(0,e.index),i=e[1]||"",n=e.index+e[0].length,o=r.indexOf(` +`,n)+1,l=r.slice(n,o).trim(),a=r.indexOf(` + +`,n);a===-1&&(a=r.length);let c=this.#s(e[2]),g=this.#s(e[3])-c,C=r.slice(o,a),W=`${l} +${i} +${t}`;$e.lastIndex=0,C=C.replace($e,z=>{let Ke=this.#s(z.slice(1,-1))-c;return`<${this.#o(Ke)}>`}),r=r.slice(a).trimStart(),$.lastIndex=0;let A={body:Oe.encode(C),additions:W.trim()===""?null:Oe.encode(W),timestamp:c*1e3,duration:g*1e3},T={};this.#t||(T.decoderConfig={description:this.#i},this.#t=!0),this.#e.output(A,T)}}#s(r){let e=$t.exec(r);if(!e)throw new Error("Expected match.");return 60*60*1e3*Number(e[1]||"0")+60*1e3*Number(e[2])+1e3*Number(e[3])+Number(e[4])}#o(r){let e=Math.floor(r/36e5),t=Math.floor(r%(60*60*1e3)/(60*1e3)),i=Math.floor(r%(60*1e3)/1e3),n=r%1e3;return e.toString().padStart(2,"0")+":"+t.toString().padStart(2,"0")+":"+i.toString().padStart(2,"0")+"."+n.toString().padStart(3,"0")}};var K=class{constructor(){this.connectedTrack=null;this.closed=!1}ensureValidDigest(){if(!this.connectedTrack)throw new Error("Cannot call digest without connecting the source to an output track.");if(!this.connectedTrack.output.started)throw new Error("Cannot call digest before output has been started.");if(this.connectedTrack.output.finalizing)throw new Error("Cannot call digest after output has started finalizing.");if(this.closed)throw new Error("Cannot call digest after source has been closed.")}start(){}async flush(){}close(){if(this.closed)throw new Error("Source already closed.");if(!this.connectedTrack)throw new Error("Cannot call close without connecting the source to an output track.");if(!this.connectedTrack.output.started)throw new Error("Cannot call close before output has been started.");this.closed=!0,!this.connectedTrack.output.finalizing&&this.connectedTrack.output.muxer.onTrackClose(this.connectedTrack)}},B=class extends K{constructor(e){super();this.connectedTrack=null;this.codec=e}},se=class extends B{constructor(r){super(r)}digest(r,e){this.ensureValidDigest(),this.connectedTrack?.output.muxer.addEncodedVideoChunk(this.connectedTrack,r,e)}},Kt=5,G=class{constructor(r,e){this.source=r;this.codecConfig=e;this.encoder=null;this.lastMultipleOfKeyFrameInterval=-1}digest(r){this.source.ensureValidDigest(),this.ensureEncoder(r),d(this.encoder);let e=Math.floor(r.timestamp/1e6/Kt);this.encoder.encode(r,{keyFrame:e!==this.lastMultipleOfKeyFrameInterval}),this.lastMultipleOfKeyFrameInterval=e}ensureEncoder(r){this.encoder||(this.encoder=new VideoEncoder({output:(e,t)=>this.source.connectedTrack?.output.muxer.addEncodedVideoChunk(this.source.connectedTrack,e,t),error:e=>console.error(e)}),this.encoder.configure({codec:Qe(this.codecConfig.codec,r.codedWidth,r.codedHeight),width:r.codedWidth,height:r.codedHeight,bitrate:this.codecConfig.bitrate}))}async flush(){return this.encoder?.flush()}},ne=class extends B{constructor(r){super(r.codec),this.encoder=new G(this,r)}digest(r){this.encoder.digest(r)}flush(){return this.encoder.flush()}},oe=class extends B{constructor(e,t){super(t.codec);this.canvas=e;this.encoder=new G(this,t)}digest(e,t=0){let i=new VideoFrame(this.canvas,{timestamp:Math.round(1e6*e),duration:Math.round(1e6*t)});this.encoder.digest(i),i.close()}flush(){return this.encoder.flush()}},ae=class extends B{constructor(e,t){super(t.codec);this.track=e;this.abortController=null;this.encoder=new G(this,t)}start(){this.abortController=new AbortController;let e=new MediaStreamTrackProcessor({track:this.track}),t=new WritableStream({write:i=>{this.encoder.digest(i),i.close()}});e.readable.pipeTo(t,{signal:this.abortController.signal}).catch(i=>{i instanceof DOMException&&i.name==="AbortError"||console.error("Pipe error:",i)})}async flush(){this.abortController&&(this.abortController.abort(),this.abortController=null),await this.encoder.flush()}},I=class extends K{constructor(e){super();this.connectedTrack=null;this.codec=e}},ue=class extends I{constructor(r){super(r)}digest(r,e){this.ensureValidDigest(),this.connectedTrack?.output.muxer.addEncodedAudioChunk(this.connectedTrack,r,e)}},j=class{constructor(r,e){this.source=r;this.codecConfig=e;this.encoder=null}digest(r){this.source.ensureValidDigest(),this.ensureEncoder(r),d(this.encoder),this.encoder.encode(r)}ensureEncoder(r){this.encoder||(this.encoder=new AudioEncoder({output:(e,t)=>this.source.connectedTrack?.output.muxer.addEncodedAudioChunk(this.source.connectedTrack,e,t),error:e=>console.error(e)}),this.encoder.configure({codec:He(this.codecConfig.codec,r.numberOfChannels,r.sampleRate),numberOfChannels:r.numberOfChannels,sampleRate:r.sampleRate,bitrate:this.codecConfig.bitrate}))}async flush(){return this.encoder?.flush()}},le=class extends I{constructor(r){super(r.codec),this.encoder=new j(this,r)}digest(r){this.encoder.digest(r)}flush(){return this.encoder.flush()}},de=class extends I{constructor(e){super(e.codec);this.accumulatedFrameCount=0;this.encoder=new j(this,e)}digest(e){let t=e.numberOfChannels,i=e.sampleRate,n=e.length,o=new Float32Array(t*n);for(let a=0;a{this.encoder.digest(i),i.close()}});e.readable.pipeTo(t,{signal:this.abortController.signal}).catch(i=>{i instanceof DOMException&&i.name==="AbortError"||console.error("Pipe error:",i)})}async flush(){this.abortController&&(this.abortController.abort(),this.abortController=null),await this.encoder.flush()}},ve=class extends K{constructor(e){super();this.connectedTrack=null;this.codec=e}},he=class extends ve{constructor(r){super(r),this.encoder=new ie({output:(e,t)=>this.connectedTrack?.output.muxer.addEncodedSubtitleChunk(this.connectedTrack,e,t),error:e=>console.error(e)}),this.encoder.configure({codec:r})}digest(r){this.ensureValidDigest(),this.encoder.encode(r)}};var q=class{},me=class extends q{#e=0;#r;#i=new ArrayBuffer(2**16);#t=new Uint8Array(this.#i);#s=0;constructor(r){super(),this.#r=r}#o(r){let e=this.#i.byteLength;for(;et.start-i.start);r.push({start:e[0].start,size:e[0].data.byteLength});for(let t=1;ta.start<=e&&ejt){for(let a=0;a=r.written[n+1].start;)r.written[n].end=Math.max(r.written[n].end,r.written[n+1].end),r.written.splice(n+1,1)}#n(r){let t={start:Math.floor(r/this.#i)*this.#i,data:new Uint8Array(this.#i),written:[],shouldFlush:!1};return this.#t.push(t),this.#t.sort((i,n)=>i.start-n.start),this.#t.indexOf(t)}#a(r=!1){for(let e=0;er.stream.write({type:"write",data:e,position:t}),chunkSize:r.options?.chunkSize}))}};var U=class{constructor(){this.output=null}},be=class extends U{constructor(){super(...arguments);this.buffer=null}createWriter(){return new me(this)}},_=class extends U{constructor(e){super();this.options=e;if(typeof e!="object")throw new TypeError("StreamTarget requires an options object to be passed to its constructor.");if(e.onData){if(typeof e.onData!="function")throw new TypeError("options.onData, when provided, must be a function.");if(e.onData.length<2)throw new TypeError("options.onData, when provided, must be a function that takes in at least two arguments (data and position). Ignoring the position argument, which specifies the byte offset at which the data is to be written, can lead to broken outputs.")}if(e.chunked!==void 0&&typeof e.chunked!="boolean")throw new TypeError("options.chunked, when provided, must be a boolean.");if(e.chunkSize!==void 0&&(!Number.isInteger(e.chunkSize)||e.chunkSize<=0))throw new TypeError("options.chunkSize, when provided, must be a positive integer.")}createWriter(){return this.options.chunked?new X(this):new fe(this)}},ke=class extends U{constructor(e,t){super();this.stream=e;this.options=t;if(!(e instanceof FileSystemWritableFileStream))throw new TypeError("FileSystemWritableFileStreamTarget requires a FileSystemWritableFileStream instance.");if(t!==void 0&&typeof t!="object")throw new TypeError("FileSystemWritableFileStreamTarget's options, when provided, must be an object.");if(t&&t.chunkSize!==void 0&&(!Number.isInteger(t.chunkSize)||t.chunkSize<=0))throw new TypeError("options.chunkSize, when provided, must be a positive integer")}createWriter(){return new pe(this)}};return Ye(qt);})(); 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 8469764..b10b951 100644 --- a/dist/metamuxer.min.mjs +++ b/dist/metamuxer.min.mjs @@ -1 +1,9 @@ -var xe=(r,i,e)=>{if(r==="avc"){let t=100;i<=768&&e<=432?t=66:i<=1920&&e<=1080&&(t=77);let s=0,n=i>1920||e>1080?50:41,o=t.toString(16).padStart(2,"0"),l=s.toString(16).padStart(2,"0"),u=n.toString(16).padStart(2,"0");return`avc1.${o}${l}${u}`}else if(r==="hevc"){let t=0,s=1,n=Array(32).fill(0);n[s]=1;let o=parseInt(n.reverse().join(""),2).toString(16).replace(/^0+/,""),l="L",u=120;return i<=1280&&e<=720?u=93:i<=1920&&e<=1080?u=120:i<=3840&&e<=2160?u=150:(l="H",u=180),`hev1.${t===0?"":String.fromCharCode(65+t-1)}${s}.${o}.${l}${u}.B0`}else{if(r==="vp8")return"vp8";if(r==="vp9"){let t="00",s;return i<=854&&e<=480?s="21":i<=1280&&e<=720?s="31":i<=1920&&e<=1080?s="41":i<=3840&&e<=2160?s="51":s="61",`vp09.${t}.${s}.08`}else if(r==="av1"){let s;return i<=854&&e<=480?s="01":i<=1280&&e<=720?s="03":i<=1920&&e<=1080?s="04":i<=3840&&e<=2160?s="07":s="09",`av01.0.${s}M.08`}}throw new Error(`Unhandled codec '${r}'.`)},ye=(r,i,e)=>{if(r==="aac")return i>=2&&e<=24e3?"mp4a.40.29":e<=24e3?"mp4a.40.5":"mp4a.40.2";if(r==="opus")return"opus";if(r==="vorbis")return"vorbis";throw new Error(`Unhandled codec '${r}'.`)};function d(r){if(!r)throw new Error("Assertion failed.")}var U=r=>r&&r[r.length-1],v=r=>r>=0&&r<2**32,V=(r,i,e)=>{let t=0;for(let s=i;s>l;t<<=1,t|=u}return t},we=(r,i,e,t)=>{for(let s=i;s>e-s-1<r instanceof ArrayBuffer?new Uint8Array(r):new Uint8Array(r.buffer,r.byteOffset,r.byteLength);var S=class{constructor(i){this.connectedTrack=null;this.codec=i}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(){}},M=class{constructor(i){this.connectedTrack=null;this.codec=i}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(i){super(i)}digest(i,e){this.ensureValidDigest(),this.connectedTrack?.output.muxer.addEncodedVideoChunk(this.connectedTrack,i,e)}},Ie=5,I=class{constructor(i,e){this.source=i;this.codecConfig=e;this.encoder=null;this.lastMultipleOfKeyFrameInterval=-1}digest(i){this.source.ensureValidDigest(),this.ensureEncoder(i),d(this.encoder);let e=Math.floor(i.timestamp/1e6/Ie);this.encoder.encode(i,{keyFrame:e!==this.lastMultipleOfKeyFrameInterval}),this.lastMultipleOfKeyFrameInterval=e}ensureEncoder(i){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,i.codedWidth,i.codedHeight),width:i.codedWidth,height:i.codedHeight,bitrate:this.codecConfig.bitrate}))}async flush(){return this.encoder?.flush()}},re=class extends S{constructor(i){super(i.codec),this.encoder=new I(this,i)}digest(i){this.encoder.digest(i)}flush(){return this.encoder.flush()}},se=class extends S{constructor(e,t){super(t.codec);this.canvas=e;this.encoder=new I(this,t)}digest(e,t=0){let s=new VideoFrame(this.canvas,{timestamp:Math.round(1e6*e),duration:Math.round(1e6*t)});this.encoder.digest(s),s.close()}flush(){return this.encoder.flush()}},ie=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:s=>{this.encoder.digest(s),s.close()}});e.readable.pipeTo(t,{signal:this.abortController.signal}).catch(s=>{s instanceof DOMException&&s.name==="AbortError"||console.error("Pipe error:",s)})}async flush(){this.abortController&&(this.abortController.abort(),this.abortController=null),await this.encoder.flush()}},ne=class extends M{constructor(i){super(i)}digest(i,e){this.ensureNotFinalizing(),this.connectedTrack?.output.muxer.addEncodedAudioChunk(this.connectedTrack,i,e)}},D=class{constructor(i,e){this.source=i;this.codecConfig=e;this.encoder=null}digest(i){this.source.ensureNotFinalizing(),this.ensureEncoder(i),d(this.encoder),this.encoder.encode(i)}ensureEncoder(i){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,i.numberOfChannels,i.sampleRate),numberOfChannels:i.numberOfChannels,sampleRate:i.sampleRate,bitrate:this.codecConfig.bitrate}))}async flush(){return this.encoder?.flush()}},oe=class extends M{constructor(i){super(i.codec),this.encoder=new D(this,i)}digest(i){this.encoder.digest(i)}flush(){return this.encoder.flush()}},ae=class extends M{constructor(e){super(e.codec);this.accumulatedFrameCount=0;this.encoder=new D(this,e)}digest(e){let t=e.numberOfChannels,s=e.sampleRate,n=e.length,o=new Float32Array(t*n);for(let u=0;u{this.encoder.digest(s),s.close()}});e.readable.pipeTo(t,{signal:this.abortController.signal}).catch(s=>{s instanceof DOMException&&s.name==="AbortError"||console.error("Pipe error:",s)})}async flush(){this.abortController&&(this.abortController.abort(),this.abortController=null),await this.encoder.flush()}};var le=class{constructor(i){this.tracks=[];this.started=!1;this.finalizing=!1;this.writer=i.target.createWriter(),this.muxer=i.format.createMuxer(this)}addTrack(i,e={}){if(this.started)throw new Error("Cannot add track after output has started.");if(i.connectedTrack)throw new Error("Source is already used for a track.");let t={id:this.tracks.length+1,output:this,type:i instanceof S?"video":"audio",source:i,metadata:e};this.muxer.beforeTrackAdd(t),this.tracks.push(t),i.connectedTrack=t}start(){if(this.started)throw new Error("Output already started.");this.started=!0,this.muxer.start();for(let i of this.tracks)i.source.start()}async finalize(){if(this.finalizing)throw new Error("Cannot call finalize twice.");this.finalizing=!0;let i=this.tracks.map(e=>e.source.flush());await Promise.all(i),this.muxer.finalize(),this.writer.flush(),this.writer.finalize()}};var c=new Uint8Array(8),y=new DataView(c.buffer),b=r=>[(r%256+256)%256],m=r=>(y.setUint16(0,r,!1),[c[0],c[1]]),De=r=>(y.setInt16(0,r,!1),[c[0],c[1]]),Ae=r=>(y.setUint32(0,r,!1),[c[1],c[2],c[3]]),a=r=>(y.setUint32(0,r,!1),[c[0],c[1],c[2],c[3]]),We=r=>(y.setInt32(0,r,!1),[c[0],c[1],c[2],c[3]]),z=r=>(y.setUint32(0,Math.floor(r/2**32),!1),y.setUint32(4,r,!1),[c[0],c[1],c[2],c[3],c[4],c[5],c[6],c[7]]),ce=r=>(y.setInt16(0,2**8*r,!1),[c[0],c[1]]),x=r=>(y.setInt32(0,2**16*r,!1),[c[0],c[1],c[2],c[3]]),de=r=>(y.setInt32(0,2**30*r,!1),[c[0],c[1],c[2],c[3]]),k=(r,i=!1)=>{let e=Array(r.length).fill(null).map((t,s)=>r.charCodeAt(s));return i&&e.push(0),e},fe=r=>{let i=null;for(let e of r)(!i||e.timestamp>i.timestamp)&&(i=e);return i},Oe=r=>{let i=r*(Math.PI/180),e=Math.cos(i),t=Math.sin(i);return[e,t,0,-t,e,0,0,0,1]},ve=Oe(0),Ve=r=>[x(r[0]),x(r[1]),de(r[2]),x(r[3]),x(r[4]),de(r[5]),x(r[6]),x(r[7]),de(r[8])],p=(r,i,e)=>({type:r,contents:i&&new Uint8Array(i.flat(10)),children:e}),h=(r,i,e,t,s)=>p(r,[b(i),Ae(e),t??[]],s),ze=r=>{let i=512;return r.fragmented?p("ftyp",[k("iso5"),a(i),k("iso5"),k("iso6"),k("mp41")]):p("ftyp",[k("isom"),a(i),k("isom"),r.holdsAvc?k("avc1"):[],k("mp41")])},j=r=>({type:"mdat",largeSize:r}),Ue=r=>({type:"free",size:r}),W=(r,i,e=!1)=>p("moov",void 0,[Ne(i,r),...r.map(t=>Re(t,i)),e?mt(r):null]),Ne=(r,i)=>{let e=T(Math.max(0,...i.filter(o=>o.samples.length>0).map(o=>{let l=fe(o.samples);return l.timestamp+l.duration})),G),t=Math.max(...i.map(o=>o.track.id))+1,s=!v(r)||!v(e),n=s?z:a;return h("mvhd",+s,0,[n(r),n(r),a(G),n(e),x(1),ce(1),Array(10).fill(0),Ve(ve),Array(24).fill(0),a(t)])},Re=(r,i)=>p("trak",void 0,[Qe(r,i),He(r,i)]),Qe=(r,i)=>{let e=fe(r.samples),t=T(e?e.timestamp+e.duration:0,G),s=!v(i)||!v(t),n=s?z:a,o;if(r.type==="video"){let l=r.track.metadata.rotation;o=l===void 0||typeof l=="number"?Oe(l??0):l}else o=ve;return h("tkhd",+s,3,[n(i),n(i),a(r.track.id),a(0),n(t),Array(8).fill(0),m(0),m(0),ce(r.type==="audio"?1:0),m(0),Ve(o),x(r.type==="video"?r.info.width:0),x(r.type==="video"?r.info.height:0)])},He=(r,i)=>p("mdia",void 0,[$e(r,i),Ke(r.type==="video"?"vide":"soun"),Ge(r)]),$e=(r,i)=>{let e=fe(r.samples),t=T(e?e.timestamp+e.duration:0,r.timescale),s=!v(i)||!v(t),n=s?z:a;return h("mdhd",+s,0,[n(i),n(i),a(r.timescale),n(t),m(21956),m(0)])},Ke=r=>h("hdlr",0,0,[k("mhlr"),k(r),a(0),a(0),a(0),k("mp4-muxer-hdlr",!0)]),Ge=r=>p("minf",void 0,[r.type==="video"?je():qe(),Xe(),Ze(r)]),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,[a(1)],[Le()]),Le=()=>h("url ",0,1),Ze=r=>{let i=r.compositionTimeOffsetTable.length>1||r.compositionTimeOffsetTable.some(e=>e.sampleCompositionTimeOffset!==0);return p("stbl",void 0,[Je(r),at(r),ut(r),lt(r),dt(r),ct(r),i?ft(r):null])},Je=r=>h("stsd",0,0,[a(1)],[r.type==="video"?et(yt[r.track.source.codec],r):it(St[r.track.source.codec],r)]),et=(r,i)=>p(r,[Array(6).fill(0),m(1),m(0),m(0),Array(12).fill(0),m(i.info.width),m(i.info.height),a(4718592),a(4718592),a(0),m(1),Array(32).fill(0),m(24),De(65535)],[wt[i.track.source.codec](i)]),tt=r=>r.info.decoderConfig&&p("avcC",[...P(r.info.decoderConfig.description)]),rt=r=>r.info.decoderConfig&&p("hvcC",[...P(r.info.decoderConfig.description)]),Se=r=>{if(!r.info.decoderConfig)return null;let i=r.info.decoderConfig;if(!i.colorSpace)throw new Error("'colorSpace' is required in the decoder config for VP8/VP9.");let e=i.codec.split("."),t=Number(e[1]),s=Number(e[2]),l=(Number(e[3])<<4)+(0<<1)+Number(i.colorSpace.fullRange);return h("vpcC",1,0,[b(t),b(s),b(l),b(2),b(2),b(2),m(0)])},st=()=>{let e=(1<<7)+1;return p("av1C",[e,0,0,0])},it=(r,i)=>p(r,[Array(6).fill(0),m(1),m(0),m(0),a(0),m(i.info.numberOfChannels),m(16),m(0),m(0),x(i.info.sampleRate)],[At[i.track.source.codec](i)]),nt=r=>{let i=P(r.info.decoderConfig.description??new ArrayBuffer(0));return h("esds",0,0,[a(58753152),b(32+i.byteLength),m(1),b(0),a(75530368),b(18+i.byteLength),b(64),b(21),Ae(0),a(130071),a(130071),a(92307584),b(i.byteLength),...i,a(109084800),b(1),b(2)])},ot=r=>{let i=3840,e=0,t=r.info.decoderConfig?.description;if(t){if(t.byteLength<18)throw new TypeError("Invalid decoder description provided for Opus; must be at least 18 bytes long.");let s=ArrayBuffer.isView(t)?new DataView(t.buffer,t.byteOffset,t.byteLength):new DataView(t);i=s.getUint16(10,!0),e=s.getInt16(14,!0)}return p("dOps",[b(0),b(r.info.numberOfChannels),m(i),a(r.info.sampleRate),ce(e),b(0)])},at=r=>h("stts",0,0,[a(r.timeToSampleTable.length),r.timeToSampleTable.map(i=>[a(i.sampleCount),a(i.sampleDelta)])]),ut=r=>{if(r.samples.every(e=>e.type==="key"))return null;let i=[...r.samples.entries()].filter(([,e])=>e.type==="key");return h("stss",0,0,[a(i.length),i.map(([e])=>a(e+1))])},lt=r=>h("stsc",0,0,[a(r.compactlyCodedChunkTable.length),r.compactlyCodedChunkTable.map(i=>[a(i.firstChunk),a(i.samplesPerChunk),a(1)])]),dt=r=>h("stsz",0,0,[a(0),a(r.samples.length),r.samples.map(i=>a(i.size))]),ct=r=>r.finalizedChunks.length>0&&U(r.finalizedChunks).offset>=2**32?h("co64",0,0,[a(r.finalizedChunks.length),r.finalizedChunks.map(i=>z(i.offset))]):h("stco",0,0,[a(r.finalizedChunks.length),r.finalizedChunks.map(i=>a(i.offset))]),ft=r=>h("ctts",0,0,[a(r.compositionTimeOffsetTable.length),r.compositionTimeOffsetTable.map(i=>[a(i.sampleCount),a(i.sampleCompositionTimeOffset)])]),mt=r=>p("mvex",void 0,r.map(ht)),ht=r=>h("trex",0,0,[a(r.track.id),a(1),a(0),a(0),a(0)]),me=(r,i)=>p("moof",void 0,[pt(r),...i.map(bt)]),pt=r=>h("mfhd",0,0,[a(r)]),Pe=r=>{let i=0,e=0,t=0,s=0,n=r.type==="delta";return e|=+n,n?i|=1:i|=2,i<<24|e<<16|t<<8|s},bt=r=>p("traf",void 0,[gt(r),Ct(r),kt(r)]),gt=r=>{d(r.currentChunk);let i=0;i|=8,i|=16,i|=32,i|=131072;let e=r.currentChunk.samples[1]??r.currentChunk.samples[0],t={duration:e.timescaleUnitsToNextSample,size:e.size,flags:Pe(e)};return h("tfhd",0,i,[a(r.track.id),a(t.duration),a(t.size),a(t.flags)])},Ct=r=>(d(r.currentChunk),h("tfdt",1,0,[z(T(r.currentChunk.startTimestamp,r.timescale))])),kt=r=>{d(r.currentChunk);let i=r.currentChunk.samples.map(w=>w.timescaleUnitsToNextSample),e=r.currentChunk.samples.map(w=>w.size),t=r.currentChunk.samples.map(Pe),s=r.currentChunk.samples.map(w=>T(w.timestamp-w.decodeTimestamp,r.timescale)),n=new Set(i),o=new Set(e),l=new Set(t),u=new Set(s),f=l.size===2&&t[0]!==t[1],g=n.size>1,C=o.size>1,A=!f&&l.size>1,Te=u.size>1||[...u].some(w=>w!==0),O=0;return O|=1,O|=4*+f,O|=256*+g,O|=512*+C,O|=1024*+A,O|=2048*+Te,h("trun",1,O,[a(r.currentChunk.samples.length),a(r.currentChunk.offset-r.currentChunk.moofOffset||0),f?a(t[0]):[],r.currentChunk.samples.map((w,K)=>[g?a(i[K]):[],C?a(e[K]):[],A?a(t[K]):[],Te?We(s[K]):[]])])},Me=r=>p("mfra",void 0,[...r.map(Tt),xt()]),Tt=(r,i)=>h("tfra",1,0,[a(r.track.id),a(63),a(r.finalizedChunks.length),r.finalizedChunks.map(t=>[z(T(t.startTimestamp,r.timescale)),z(t.moofOffset),a(i+1),a(1),a(1)])]),xt=()=>h("mfro",0,0,[a(0)]),yt={avc:"avc1",hevc:"hvc1",vp8:"vp08",vp9:"vp09",av1:"av01"},wt={avc:tt,hevc:rt,vp8:Se,vp9:Se,av1:st},St={aac:"mp4a",opus:"Opus"},At={aac:nt,opus:ot};var E=class{constructor(i){this.output=i}beforeTrackAdd(i){}};var G=1e3,Ot=2082844800,T=(r,i,e=!0)=>{let t=r*i;return e?Math.round(t):t},q=class extends E{constructor(e,t){super(e);this.#s=new Uint8Array(8);this.#t=new DataView(this.#s.buffer);this.offsets=new WeakMap;this.#o=null;this.#n=null;this.#i=[];this.#a=Math.floor(Date.now()/1e3)+Ot;this.#l=[];this.#f=1;this.#e=e.writer,this.#r=t}#e;#r;#s;#t;#o;#n;#i;#a;#l;#f;writeU32(e){this.#t.setUint32(0,e,!1),this.#e.write(this.#s.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.#s.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 s of t)s&&(e+=8*Math.ceil(2/3*s),e+=4*s,e+=12*Math.ceil(2/3*s),e+=4*s,e+=8*s);return e+=4096,e}#u(e,t){let s=this.#i.find(o=>o.track===e);if(s)return s;d(t),d(t.decoderConfig),d(t.decoderConfig.codedWidth!==void 0),d(t.decoderConfig.codedHeight!==void 0);let n={track:e,type:"video",info:{width:t.decoderConfig.codedWidth,height:t.decoderConfig.codedHeight,decoderConfig:t.decoderConfig},timescale:e.metadata.frameRate??57600,samples:[],sampleQueue:[],timestampProcessingQueue:[],firstTimestamp:null,lastKeyFrameTimestamp:null,timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.#i.push(n),this.#i.sort((o,l)=>o.track.id-l.track.id),n}#m(e,t){let s=this.#i.find(o=>o.track===e);if(s)return s;d(t),d(t.decoderConfig);let n={track:e,type:"audio",info:{numberOfChannels:t.decoderConfig.numberOfChannels,sampleRate:t.decoderConfig.sampleRate,decoderConfig:t.decoderConfig},timescale:t.decoderConfig.sampleRate,samples:[],sampleQueue:[],timestampProcessingQueue:[],firstTimestamp:null,lastKeyFrameTimestamp:null,timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.#i.push(n),this.#i.sort((o,l)=>o.track.id-l.track.id),n}addEncodedVideoChunk(e,t,s){let n=this.#u(e,s);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,s){let n=this.#m(e,s);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 s=this.#T(e,t),n=(t.duration??0)/1e6,o=new Uint8Array(t.byteLength);return t.copyTo(o),{timestamp:s,decodeTimestamp:s,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(s=>s.timestamp).sort((s,n)=>s-n);for(let s=0;s{if(e===l)return t.type==="key";let u=l.sampleQueue[0];return u&&u.type==="key"});n>=1&&o&&(s=!0,this.#k())}else s=n>=.5}s&&(e.currentChunk&&this.#C(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 s=t.timestamp/1e6;if(s<0)throw new Error(`Timestamps must be non-negative (got ${s}s).`);if(e.firstTimestamp===null&&(e.firstTimestamp=s),s-=e.firstTimestamp,e.lastKeyFrameTimestamp!==null&&s=2**32&&(u.largeSize=!0,g=this.measureBox(u)+f),u.size=g,this.writeBox(u)}for(let u of this.#i){u.currentChunk.offset=this.#e.getPos(),u.currentChunk.moofOffset=s;for(let f of u.currentChunk.samples)this.#e.write(f.data),f.data=null}let o=this.#e.getPos();this.#e.seek(this.offsets.get(n));let l=me(t,this.#i);this.writeBox(l),this.#e.seek(o);for(let u of this.#i)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.#i){for(let t of e.sampleQueue)this.#p(e,t);this.#c(e)}this.#k(!1)}else for(let e of this.#i)this.#c(e),this.#C(e);if(this.#r.options.fastStart==="in-memory"){d(this.#n);let e;for(let s=0;s<2;s++){let n=W(this.#i,this.#a),o=this.measureBox(n);e=this.measureBox(this.#n);let l=this.#e.getPos()+o+e;for(let u of this.#l){u.offset=l;for(let{data:f}of u.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.#i,this.#a);this.writeBox(t),this.#n.size=e,this.writeBox(this.#n);for(let s of this.#l)for(let n of s.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.#i);this.writeBox(t);let s=this.#e.getPos()-e;this.#e.seek(this.#e.getPos()-4),this.writeU32(s)}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 s=W(this.#i,this.#a);if(typeof this.#r.options.fastStart=="object"){this.#e.seek(this.#o),this.writeBox(s);let n=e-this.#e.getPos();this.writeBox(Ue(n))}else this.writeBox(s)}}};var N=class{constructor(i){this.value=i}},F=class{constructor(i){this.value=i}},R=class{constructor(i){this.value=i}};var he=r=>r<256?1:r<65536?2:r<1<<24?3:r<2**32?4:r<2**40?5:6,pe=r=>r>=-64&&r<64?1:r>=-8192&&r<8192?2:r>=-(1<<20)&&r<1<<20?3:r>=-(1<<27)&&r<1<<27?4:r>=-(2**34)&&r<2**34?5:6,Ee=r=>{if(r<127)return 1;if(r<16383)return 2;if(r<(1<<21)-1)return 3;if(r<(1<<28)-1)return 4;if(r<2**35-1)return 5;if(r<2**42-1)return 6;throw new Error("EBML VINT size not supported "+r)};var 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 E{constructor(e,t){super(e);this.#s=new Uint8Array(8);this.#t=new DataView(this.#s.buffer);this.offsets=new WeakMap;this.dataOffsets=new WeakMap;this.#o=[];this.#n=null;this.#i=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;#s;#t;#o;#n;#i;#a;#l;#f;#d;#u;#m;#h;#c;#p(e){this.#t.setUint8(0,e),this.#e.write(this.#s.subarray(0,1))}#T(e){this.#t.setFloat32(0,e,!1),this.#e.write(this.#s.subarray(0,4))}#C(e){this.#t.setFloat64(0,e,!1),this.#e.write(this.#s)}#b(e,t=he(e)){let s=0;switch(t){case 6:this.#t.setUint8(s++,e/2**40|0);case 5:this.#t.setUint8(s++,e/2**32|0);case 4:this.#t.setUint8(s++,e>>24);case 3:this.#t.setUint8(s++,e>>16);case 2:this.#t.setUint8(s++,e>>8);case 1:this.#t.setUint8(s++,e);break;default:throw new Error("Bad UINT size "+t)}this.#e.write(this.#s.subarray(0,s))}#k(e,t=pe(e)){e<0&&(e+=2**(t*8)),this.#b(e,t)}writeEBMLVarInt(e,t=Ee(e)){let s=0;switch(t){case 1:this.#t.setUint8(s++,128|e);break;case 2:this.#t.setUint8(s++,64|e>>8),this.#t.setUint8(s++,e);break;case 3:this.#t.setUint8(s++,32|e>>16),this.#t.setUint8(s++,e>>8),this.#t.setUint8(s++,e);break;case 4:this.#t.setUint8(s++,16|e>>24),this.#t.setUint8(s++,e>>16),this.#t.setUint8(s++,e>>8),this.#t.setUint8(s++,e);break;case 5:this.#t.setUint8(s++,8|e/2**32&7),this.#t.setUint8(s++,e>>24),this.#t.setUint8(s++,e>>16),this.#t.setUint8(s++,e>>8),this.#t.setUint8(s++,e);break;case 6:this.#t.setUint8(s++,4|e/2**40&3),this.#t.setUint8(s++,e/2**32|0),this.#t.setUint8(s++,e>>24),this.#t.setUint8(s++,e>>16),this.#t.setUint8(s++,e>>8),this.#t.setUint8(s++,e);break;default:throw new Error("Bad EBML VINT size "+t)}this.#e.write(this.#s.subarray(0,s))}#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(),s=e.size===-1?1:e.size??4;e.size===-1?this.#p(255):this.#e.seek(this.#e.getPos()+s);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,s),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.#C(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]),s=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:s},{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.#i=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 s=t.info.decoderConfig.colorSpace;return!s.matrix||!s.transfer||!s.primaries||s.fullRange==null?null:{id:21936,data:[{id:21937,data:{rgb:1,bt709:1,bt470bg:5,smpte170m:6}[s.matrix]},{id:21946,data:{bt709:1,smpte170m:6,"iec61966-2-1":13}[s.transfer]},{id:21947,data:{bt709:1,bt470bg:5,smpte170m:6}[s.primaries]},{id:21945,data:[1,2][Number(s.fullRange)]}]}}return null})()]}]:[],...t.type==="audio"?[{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.#i,this.#l]};this.#n=e,this.writeEBML(e)}#P(){this.#d={id:475249515,data:[]}}get#g(){return d(this.#n),this.dataOffsets.get(this.#n)}#M(e,t){let s=this.#o.find(o=>o.track===e);if(s)return s;d(t),d(t.decoderConfig),d(t.decoderConfig.codedWidth!==void 0),d(t.decoderConfig.codedHeight!==void 0);let n={track:e,type:"video",info:{width:t.decoderConfig.codedWidth,height:t.decoderConfig.codedHeight,decoderConfig:t.decoderConfig},chunkQueue:[],firstTimestamp:null,lastKeyFrameTimestamp:null,lastWrittenMsTimestamp:null};return this.#o.push(n),this.#o.sort((o,l)=>o.track.id-l.track.id),n}#E(e,t){let s=this.#o.find(o=>o.track===e);if(s)return s;d(t),d(t.decoderConfig);let n={track:e,type:"audio",info:{numberOfChannels:t.decoderConfig.numberOfChannels,sampleRate:t.decoderConfig.sampleRate,decoderConfig:t.decoderConfig},chunkQueue:[],firstTimestamp:null,lastKeyFrameTimestamp:null,lastWrittenMsTimestamp:null};return this.#o.push(n),this.#o.sort((o,l)=>o.track.id-l.track.id),n}addEncodedVideoChunk(e,t,s){let n=this.#M(e,s),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,s){let n=this.#E(e,s),o=this.#y(n,t);n.chunkQueue.push(o),this.#x(),this.#e.flush()}#x(){if(!(this.#o.length=2&&s++;let f={rgb:7,bt709:2,bt470bg:1,smpte170m:3}[e.info.decoderConfig.colorSpace.matrix];we(t.data,s+0,s+3,f)}#y(e,t){let s=this.#B(e,t),n=new Uint8Array(t.byteLength);return t.copyTo(n),{data:n,type:t.type,timestamp:s,duration:(t.duration??0)/1e6,additions:null}}#B(e,t){let s=t.timestamp/1e6;if(s<0)throw new Error(`Timestamps must be non-negative (got ${s}s).`);if(e.firstTimestamp===null&&(e.firstTimestamp=s),s-=e.firstTimestamp,e.lastKeyFrameTimestamp!==null&&s{if(e===C)return t.type==="key";let A=C.chunkQueue[0];return A&&A.type==="key"});(!this.#u||n&&s-this.#m>=1e3)&&this.#_(s);let o=s-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 u=new Uint8Array(4),f=new DataView(u.buffer);f.setUint8(0,128|e.track.id),f.setInt16(1,o,!1);let g=Math.floor(1e3*t.duration);if(g===0&&!t.additions){f.setUint8(3,+(t.type==="key")<<7);let C={id:163,data:[u,t.data]};this.writeEBML(C)}else{let C={id:160,data:[{id:161,data:[u,t.data]},t.type==="delta"?{id:251,data:new R(e.lastWrittenMsTimestamp-s)}:null,g>0?{id:155,data:g}:null,t.additions?{id:30113,data:t.additions}:null]};this.writeEBML(C)}this.#c=Math.max(this.#c,s+g),e.lastWrittenMsTimestamp=s,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 s=this.offsets.get(this.#u)-this.#g;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:s}]}))]})}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.#g;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.#g,this.#a.data[1].data[1].data=this.offsets.get(this.#i)-this.#g,this.#a.data[2].data[1].data=this.offsets.get(this.#l)-this.#g,this.#e.seek(this.offsets.get(this.#a)),this.writeEBML(this.#a),this.#e.seek(e)}}};var Y=class{},ge=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;#s=new ArrayBuffer(2**16);#t=new Uint8Array(this.#s);#o=0;constructor(i){super(),this.#r=i}#n(i){let e=this.#s.byteLength;for(;et.start-s.start);i.push({start:e[0].start,size:e[0].data.byteLength});for(let t=1;tu.start<=e&&ePt){for(let u=0;u=i.written[n+1].start;)i.written[n].end=Math.max(i.written[n].end,i.written[n+1].end),i.written.splice(n+1,1)}#i(i){let t={start:Math.floor(i/this.#s)*this.#s,data:new Uint8Array(this.#s),written:[],shouldFlush:!1};return this.#t.push(t),this.#t.sort((s,n)=>s.start-n.start),this.#t.indexOf(t)}#a(i=!1){for(let e=0;ei.stream.write({type:"write",data:e,position:t}),chunkSize:i.options?.chunkSize}))}};var Mt=Symbol("isTarget");Mt;var _=class{},Ce=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{Ce as ArrayBufferTarget,ae as AudioBufferSource,oe as AudioDataSource,se as CanvasSource,ne as EncodedAudioChunkSource,te as EncodedVideoChunkSource,ke as FileSystemWritableFileStreamTarget,ue as MediaStreamAudioTrackSource,ie as MediaStreamVideoTrackSource,L as MkvOutputFormat,ge as Mp4OutputFormat,le as Output,$ as StreamTarget,_ as Target,re as VideoFrameSource,B as WebMOutputFormat}; +var ne=class{constructor(r){this.tracks=[];this.started=!1;this.finalizing=!1;if(r.target.output)throw new Error("Target is already used for another output.");r.target.output=this,this.writer=r.target.createWriter(),this.muxer=r.format.createMuxer(this)}addVideoTrack(r,e={}){this.addTrack("video",r,e)}addAudioTrack(r,e={}){this.addTrack("audio",r,e)}addSubtitleTrack(r,e={}){this.addTrack("subtitle",r,e)}addTrack(r,e,t){if(this.started)throw new Error("Cannot add track after output has started.");if(e.connectedTrack)throw new Error("Source is already used for a track.");let i={id:this.tracks.length+1,output:this,type:r,source:e,metadata:t};this.muxer.beforeTrackAdd(i),this.tracks.push(i),e.connectedTrack=i}start(){if(this.started)throw new Error("Output already started.");this.started=!0,this.muxer.start();for(let r of this.tracks)r.source.start()}async finalize(){if(this.finalizing)throw new Error("Cannot call finalize twice.");this.finalizing=!0;let r=this.tracks.map(e=>e.source.flush());await Promise.all(r),this.muxer.finalize(),this.writer.flush(),this.writer.finalize()}};function d(s){if(!s)throw new Error("Assertion failed.")}var U=s=>s&&s[s.length-1],O=s=>s>=0&&s<2**32,v=(s,r,e)=>{let t=0;for(let i=r;i>l;t<<=1,t|=a}return t},ve=(s,r,e,t)=>{for(let i=r;i>e-i-1<s instanceof ArrayBuffer?new Uint8Array(s):new Uint8Array(s.buffer,s.byteOffset,s.byteLength);var h=new Uint8Array(8),w=new DataView(h.buffer),k=s=>[(s%256+256)%256],m=s=>(w.setUint16(0,s,!1),[h[0],h[1]]),Ke=s=>(w.setInt16(0,s,!1),[h[0],h[1]]),Ee=s=>(w.setUint32(0,s,!1),[h[1],h[2],h[3]]),u=s=>(w.setUint32(0,s,!1),[h[0],h[1],h[2],h[3]]),Ge=s=>(w.setInt32(0,s,!1),[h[0],h[1],h[2],h[3]]),V=s=>(w.setUint32(0,Math.floor(s/2**32),!1),w.setUint32(4,s,!1),[h[0],h[1],h[2],h[3],h[4],h[5],h[6],h[7]]),ae=s=>(w.setInt16(0,2**8*s,!1),[h[0],h[1]]),S=s=>(w.setInt32(0,2**16*s,!1),[h[0],h[1],h[2],h[3]]),oe=s=>(w.setInt32(0,2**30*s,!1),[h[0],h[1],h[2],h[3]]),x=(s,r=!1)=>{let e=Array(s.length).fill(null).map((t,i)=>s.charCodeAt(i));return r&&e.push(0),e},ue=s=>{let r=null;for(let e of s)(!r||e.timestamp>r.timestamp)&&(r=e);return r},Ue=s=>{let r=s*(Math.PI/180),e=Math.cos(r),t=Math.sin(r);return[e,t,0,-t,e,0,0,0,1]},ze=Ue(0),Me=s=>[S(s[0]),S(s[1]),oe(s[2]),S(s[3]),S(s[4]),oe(s[5]),S(s[6]),S(s[7]),oe(s[8])],p=(s,r,e)=>({type:s,contents:r&&new Uint8Array(r.flat(10)),children:e}),f=(s,r,e,t,i)=>p(s,[k(r),Ee(e),t??[]],i),Pe=s=>{let r=512;return s.fragmented?p("ftyp",[x("iso5"),u(r),x("iso5"),x("iso6"),x("mp41")]):p("ftyp",[x("isom"),u(r),x("isom"),s.holdsAvc?x("avc1"):[],x("mp41")])},L=s=>({type:"mdat",largeSize:s}),Fe=s=>({type:"free",size:s}),W=(s,r,e=!1)=>p("moov",void 0,[je(r,s),...s.map(t=>qe(t,r)),e?Ct(s):null]),je=(s,r)=>{let e=y(Math.max(0,...r.filter(o=>o.samples.length>0).map(o=>{let l=ue(o.samples);return l.timestamp+l.duration})),X),t=Math.max(...r.map(o=>o.track.id))+1,i=!O(s)||!O(e),n=i?V:u;return f("mvhd",+i,0,[n(s),n(s),u(X),n(e),S(1),ae(1),Array(10).fill(0),Me(ze),Array(24).fill(0),u(t)])},qe=(s,r)=>p("trak",void 0,[Xe(s,r),Le(s,r)]),Xe=(s,r)=>{let e=ue(s.samples),t=y(e?e.timestamp+e.duration:0,X),i=!O(r)||!O(t),n=i?V:u,o;if(s.type==="video"){let l=s.track.metadata.rotation;o=l===void 0||typeof l=="number"?Ue(l??0):l}else o=ze;return f("tkhd",+i,3,[n(r),n(r),u(s.track.id),u(0),n(t),Array(8).fill(0),m(0),m(0),ae(s.type==="audio"?1:0),m(0),Me(o),S(s.type==="video"?s.info.width:0),S(s.type==="video"?s.info.height:0)])},Le=(s,r)=>p("mdia",void 0,[Ye(s,r),Ze(s.type==="video"?"vide":"soun"),Je(s)]),Ye=(s,r)=>{let e=ue(s.samples),t=y(e?e.timestamp+e.duration:0,s.timescale),i=!O(r)||!O(t),n=i?V:u;return f("mdhd",+i,0,[n(r),n(r),u(s.timescale),n(t),m(21956),m(0)])},Ze=s=>f("hdlr",0,0,[x("mhlr"),x(s),u(0),u(0),u(0),x("mp4-muxer-hdlr",!0)]),Je=s=>p("minf",void 0,[s.type==="video"?et():tt(),rt(),nt(s)]),et=()=>f("vmhd",0,1,[m(0),m(0),m(0),m(0)]),tt=()=>f("smhd",0,0,[m(0),m(0)]),rt=()=>p("dinf",void 0,[it()]),it=()=>f("dref",0,0,[u(1)],[st()]),st=()=>f("url ",0,1),nt=s=>{let r=s.compositionTimeOffsetTable.length>1||s.compositionTimeOffsetTable.some(e=>e.sampleCompositionTimeOffset!==0);return p("stbl",void 0,[ot(s),ft(s),pt(s),bt(s),kt(s),gt(s),r?Tt(s):null])},ot=s=>f("stsd",0,0,[u(1)],[s.type==="video"?at(Et[s.track.source.codec],s):ct(zt[s.track.source.codec],s)]),at=(s,r)=>p(s,[Array(6).fill(0),m(1),m(0),m(0),Array(12).fill(0),m(r.info.width),m(r.info.height),u(4718592),u(4718592),u(0),m(1),Array(32).fill(0),m(24),Ke(65535)],[Ut[r.track.source.codec](r)]),ut=s=>s.info.decoderConfig&&p("avcC",[...z(s.info.decoderConfig.description)]),lt=s=>s.info.decoderConfig&&p("hvcC",[...z(s.info.decoderConfig.description)]),Ve=s=>{if(!s.info.decoderConfig)return null;let r=s.info.decoderConfig;if(!r.colorSpace)throw new Error("'colorSpace' is required in the decoder config for VP8/VP9.");let e=r.codec.split("."),t=Number(e[1]),i=Number(e[2]),l=(Number(e[3])<<4)+(0<<1)+Number(r.colorSpace.fullRange);return f("vpcC",1,0,[k(t),k(i),k(l),k(2),k(2),k(2),m(0)])},dt=()=>{let e=(1<<7)+1;return p("av1C",[e,0,0,0])},ct=(s,r)=>p(s,[Array(6).fill(0),m(1),m(0),m(0),u(0),m(r.info.numberOfChannels),m(16),m(0),m(0),S(r.info.sampleRate)],[Mt[r.track.source.codec](r)]),ht=s=>{let r=z(s.info.decoderConfig.description??new ArrayBuffer(0));return f("esds",0,0,[u(58753152),k(32+r.byteLength),m(1),k(0),u(75530368),k(18+r.byteLength),k(64),k(21),Ee(0),u(130071),u(130071),u(92307584),k(r.byteLength),...r,u(109084800),k(1),k(2)])},mt=s=>{let r=3840,e=0,t=s.info.decoderConfig?.description;if(t){if(t.byteLength<18)throw new TypeError("Invalid decoder description provided for Opus; must be at least 18 bytes long.");let i=ArrayBuffer.isView(t)?new DataView(t.buffer,t.byteOffset,t.byteLength):new DataView(t);r=i.getUint16(10,!0),e=i.getInt16(14,!0)}return p("dOps",[k(0),k(s.info.numberOfChannels),m(r),u(s.info.sampleRate),ae(e),k(0)])},ft=s=>f("stts",0,0,[u(s.timeToSampleTable.length),s.timeToSampleTable.map(r=>[u(r.sampleCount),u(r.sampleDelta)])]),pt=s=>{if(s.samples.every(e=>e.type==="key"))return null;let r=[...s.samples.entries()].filter(([,e])=>e.type==="key");return f("stss",0,0,[u(r.length),r.map(([e])=>u(e+1))])},bt=s=>f("stsc",0,0,[u(s.compactlyCodedChunkTable.length),s.compactlyCodedChunkTable.map(r=>[u(r.firstChunk),u(r.samplesPerChunk),u(1)])]),kt=s=>f("stsz",0,0,[u(0),u(s.samples.length),s.samples.map(r=>u(r.size))]),gt=s=>s.finalizedChunks.length>0&&U(s.finalizedChunks).offset>=2**32?f("co64",0,0,[u(s.finalizedChunks.length),s.finalizedChunks.map(r=>V(r.offset))]):f("stco",0,0,[u(s.finalizedChunks.length),s.finalizedChunks.map(r=>u(r.offset))]),Tt=s=>f("ctts",0,0,[u(s.compositionTimeOffsetTable.length),s.compositionTimeOffsetTable.map(r=>[u(r.sampleCount),u(r.sampleCompositionTimeOffset)])]),Ct=s=>p("mvex",void 0,s.map(xt)),xt=s=>f("trex",0,0,[u(s.track.id),u(1),u(0),u(0),u(0)]),le=(s,r)=>p("moof",void 0,[yt(s),...r.map(St)]),yt=s=>f("mfhd",0,0,[u(s)]),De=s=>{let r=0,e=0,t=0,i=0,n=s.type==="delta";return e|=+n,n?r|=1:r|=2,r<<24|e<<16|t<<8|i},St=s=>p("traf",void 0,[wt(s),At(s),Ot(s)]),wt=s=>{d(s.currentChunk);let r=0;r|=8,r|=16,r|=32,r|=131072;let e=s.currentChunk.samples[1]??s.currentChunk.samples[0],t={duration:e.timescaleUnitsToNextSample,size:e.size,flags:De(e)};return f("tfhd",0,r,[u(s.track.id),u(t.duration),u(t.size),u(t.flags)])},At=s=>(d(s.currentChunk),f("tfdt",1,0,[V(y(s.currentChunk.startTimestamp,s.timescale))])),Ot=s=>{d(s.currentChunk);let r=s.currentChunk.samples.map(T=>T.timescaleUnitsToNextSample),e=s.currentChunk.samples.map(T=>T.size),t=s.currentChunk.samples.map(De),i=s.currentChunk.samples.map(T=>y(T.timestamp-T.decodeTimestamp,s.timescale)),n=new Set(r),o=new Set(e),l=new Set(t),a=new Set(i),c=l.size===2&&t[0]!==t[1],b=n.size>1,g=o.size>1,C=!c&&l.size>1,_=a.size>1||[...a].some(T=>T!==0),A=0;return A|=1,A|=4*+c,A|=256*+b,A|=512*+g,A|=1024*+C,A|=2048*+_,f("trun",1,A,[u(s.currentChunk.samples.length),u(s.currentChunk.offset-s.currentChunk.moofOffset||0),c?u(t[0]):[],s.currentChunk.samples.map((T,E)=>[b?u(r[E]):[],g?u(e[E]):[],C?u(t[E]):[],_?Ge(i[E]):[]])])},Be=s=>p("mfra",void 0,[...s.map(vt),Vt()]),vt=(s,r)=>f("tfra",1,0,[u(s.track.id),u(63),u(s.finalizedChunks.length),s.finalizedChunks.map(t=>[V(y(t.startTimestamp,s.timescale)),V(t.moofOffset),u(r+1),u(1),u(1)])]),Vt=()=>f("mfro",0,0,[u(0)]),Et={avc:"avc1",hevc:"hvc1",vp8:"vp08",vp9:"vp09",av1:"av01"},Ut={avc:ut,hevc:lt,vp8:Ve,vp9:Ve,av1:dt},zt={aac:"mp4a",opus:"Opus"},Mt={aac:ht,opus:mt};var M=class{constructor(r){this.output=r}beforeTrackAdd(r){}onTrackClose(r){}};var X=1e3,Pt=2082844800,y=(s,r,e=!0)=>{let t=s*r;return e?Math.round(t):t},Y=class extends M{constructor(e,t){super(e);this.#i=new Uint8Array(8);this.#t=new DataView(this.#i.buffer);this.offsets=new WeakMap;this.#s=null;this.#o=null;this.#n=[];this.#a=Math.floor(Date.now()/1e3)+Pt;this.#l=[];this.#h=1;this.#e=e.writer,this.#r=t}#e;#r;#i;#t;#s;#o;#n;#a;#l;#h;writeU32(e){this.#t.setUint32(0,e,!1),this.#e.write(this.#i.subarray(0,4))}writeU64(e){this.#t.setUint32(0,Math.floor(e/2**32),!1),this.#t.setUint32(4,e,!1),this.#e.write(this.#i.subarray(0,8))}writeAscii(e){for(let t=0;tt.type==="video"&&t.source.codec==="avc");if(this.writeBox(Pe({holdsAvc:e,fragmented:this.#r.options.fastStart==="fragmented"})),this.#s=this.#e.getPos(),this.#r.options.fastStart==="in-memory")this.#o=L(!1);else if(this.#r.options.fastStart!=="fragmented"){if(typeof this.#r.options.fastStart=="object"){let t=this.#d();this.#e.seek(this.#e.getPos()+t)}this.#o=L(!0),this.writeBox(this.#o)}this.#e.flush()}#d(){d(typeof this.#r.options.fastStart=="object");let e=0,t=[this.#r.options.fastStart.expectedVideoChunks,this.#r.options.fastStart.expectedAudioChunks];for(let i of t)i&&(e+=8*Math.ceil(2/3*i),e+=4*i,e+=12*Math.ceil(2/3*i),e+=4*i,e+=8*i);return e+=4096,e}#u(e,t){let i=this.#n.find(o=>o.track===e);if(i)return i;d(t),d(t.decoderConfig),d(t.decoderConfig.codedWidth!==void 0),d(t.decoderConfig.codedHeight!==void 0);let n={track:e,type:"video",info:{width:t.decoderConfig.codedWidth,height:t.decoderConfig.codedHeight,decoderConfig:t.decoderConfig},timescale:e.metadata.frameRate??57600,samples:[],sampleQueue:[],timestampProcessingQueue:[],firstTimestamp:null,lastKeyFrameTimestamp:null,timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.#n.push(n),this.#n.sort((o,l)=>o.track.id-l.track.id),n}#m(e,t){let i=this.#n.find(o=>o.track===e);if(i)return i;d(t),d(t.decoderConfig);let n={track:e,type:"audio",info:{numberOfChannels:t.decoderConfig.numberOfChannels,sampleRate:t.decoderConfig.sampleRate,decoderConfig:t.decoderConfig},timescale:t.decoderConfig.sampleRate,samples:[],sampleQueue:[],timestampProcessingQueue:[],firstTimestamp:null,lastKeyFrameTimestamp:null,timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.#n.push(n),this.#n.sort((o,l)=>o.track.id-l.track.id),n}addEncodedVideoChunk(e,t,i){let n=this.#u(e,i);if(typeof this.#r.options.fastStart=="object"&&n.samples.length===this.#r.options.fastStart.expectedVideoChunks)throw new Error(`Cannot add more video chunks than specified in 'fastStart' (${this.#r.options.fastStart.expectedVideoChunks}).`);let o=this.#f(n,t);this.#r.options.fastStart==="fragmented"?(n.sampleQueue.push(o),this.#b()):this.#p(n,o)}addEncodedAudioChunk(e,t,i){let n=this.#m(e,i);if(typeof this.#r.options.fastStart=="object"&&n.samples.length===this.#r.options.fastStart.expectedAudioChunks)throw new Error(`Cannot add more audio chunks than specified in 'fastStart' (${this.#r.options.fastStart.expectedAudioChunks}).`);let o=this.#f(n,t);this.#r.options.fastStart==="fragmented"?(n.sampleQueue.push(o),this.#b()):this.#p(n,o)}#f(e,t){let i=this.#x(e,t),n=(t.duration??0)/1e6,o=new Uint8Array(t.byteLength);return t.copyTo(o),{timestamp:i,decodeTimestamp:i,duration:n,data:o,size:o.byteLength,type:t.type,timescaleUnitsToNextSample:y(n,e.timescale)}}#c(e){if(e.timestampProcessingQueue.length===0)return;let t=e.timestampProcessingQueue.map(i=>i.timestamp).sort((i,n)=>i-n);for(let i=0;i{if(e===l)return t.type==="key";let a=l.sampleQueue[0];return a&&a.type==="key"});n>=1&&o&&(i=!0,this.#T())}else i=n>=.5}i&&(e.currentChunk&&this.#g(e),e.currentChunk={startTimestamp:t.timestamp,samples:[],offset:null,moofOffset:null}),d(e.currentChunk),e.currentChunk.samples.push(t),e.timestampProcessingQueue.push(t)}#x(e,t){let i=t.timestamp/1e6;if(i<0)throw new Error(`Timestamps must be non-negative (got ${i}s).`);if(e.firstTimestamp===null&&(e.firstTimestamp=i),i-=e.firstTimestamp,e.lastKeyFrameTimestamp!==null&&i=2**32&&(a.largeSize=!0,b=this.measureBox(a)+c),a.size=b,this.writeBox(a)}for(let a of this.#n){a.currentChunk.offset=this.#e.getPos(),a.currentChunk.moofOffset=i;for(let c of a.currentChunk.samples)this.#e.write(c.data),c.data=null}let o=this.#e.getPos();this.#e.seek(this.offsets.get(n));let l=le(t,this.#n);this.writeBox(l),this.#e.seek(o);for(let a of this.#n)a.finalizedChunks.push(a.currentChunk),this.#l.push(a.currentChunk),a.currentChunk=null;e&&this.#e.flush()}finalize(){if(this.#r.options.fastStart==="fragmented"){for(let e of this.#n){for(let t of e.sampleQueue)this.#p(e,t);this.#c(e)}this.#T(!1)}else for(let e of this.#n)this.#c(e),this.#g(e);if(this.#r.options.fastStart==="in-memory"){d(this.#o);let e;for(let i=0;i<2;i++){let n=W(this.#n,this.#a),o=this.measureBox(n);e=this.measureBox(this.#o);let l=this.#e.getPos()+o+e;for(let a of this.#l){a.offset=l;for(let{data:c}of a.samples)d(c),l+=c.byteLength,e+=c.byteLength}if(l<2**32)break;e>=2**32&&(this.#o.largeSize=!0)}let t=W(this.#n,this.#a);this.writeBox(t),this.#o.size=e,this.writeBox(this.#o);for(let i of this.#l)for(let n of i.samples)d(n.data),this.#e.write(n.data),n.data=null}else if(this.#r.options.fastStart==="fragmented"){let e=this.#e.getPos(),t=Be(this.#n);this.writeBox(t);let i=this.#e.getPos()-e;this.#e.seek(this.#e.getPos()-4),this.writeU32(i)}else{d(this.#o),d(this.#s!==null);let e=this.offsets.get(this.#o);d(e!==void 0);let t=this.#e.getPos()-e;this.#o.size=t,this.#o.largeSize=t>=2**32,this.patchBox(this.#o);let i=W(this.#n,this.#a);if(typeof this.#r.options.fastStart=="object"){this.#e.seek(this.#s),this.writeBox(i);let n=e-this.#e.getPos();this.writeBox(Fe(n))}else this.writeBox(i)}}};var N=class{constructor(r){this.value=r}},P=class{constructor(r){this.value=r}},R=class{constructor(r){this.value=r}};var de=s=>s<256?1:s<65536?2:s<1<<24?3:s<2**32?4:s<2**40?5:6,ce=s=>s>=-64&&s<64?1:s>=-8192&&s<8192?2:s>=-(1<<20)&&s<1<<20?3:s>=-(1<<27)&&s<1<<27?4:s>=-(2**34)&&s<2**34?5:6,Ie=s=>{if(s<127)return 1;if(s<16383)return 2;if(s<(1<<21)-1)return 3;if(s<(1<<28)-1)return 4;if(s<2**35-1)return 5;if(s<2**42-1)return 6;throw new Error("EBML VINT size not supported "+s)};var he=2**15,_e="https://github.com/Vanilagy/webm-muxer",We=6,Ne=5,Ft={avc:"V_MPEG4/ISO/AVC",hevc:"V_MPEGH/ISO/HEVC",vp8:"V_VP8",vp9:"V_VP9",av1:"V_AV1",aac:"A_AAC",opus:"A_OPUS",vorbis:"A_VORBIS",webvtt:"S_TEXT/WEBVTT"},Dt={video:1,audio:2,subtitle:17},Z=class extends M{constructor(e,t){super(e);this.#i=new Uint8Array(8);this.#t=new DataView(this.#i.buffer);this.offsets=new WeakMap;this.dataOffsets=new WeakMap;this.#s=[];this.#o=null;this.#n=null;this.#a=null;this.#l=null;this.#h=null;this.#d=null;this.#u=null;this.#m=null;this.#f=new Set;this.#c=0;this.#e=e.writer,this.#r=t}#e;#r;#i;#t;#s;#o;#n;#a;#l;#h;#d;#u;#m;#f;#c;#p(e){this.#t.setUint8(0,e),this.#e.write(this.#i.subarray(0,1))}#x(e){this.#t.setFloat32(0,e,!1),this.#e.write(this.#i.subarray(0,4))}#g(e){this.#t.setFloat64(0,e,!1),this.#e.write(this.#i)}#b(e,t=de(e)){let i=0;switch(t){case 6:this.#t.setUint8(i++,e/2**40|0);case 5:this.#t.setUint8(i++,e/2**32|0);case 4:this.#t.setUint8(i++,e>>24);case 3:this.#t.setUint8(i++,e>>16);case 2:this.#t.setUint8(i++,e>>8);case 1:this.#t.setUint8(i++,e);break;default:throw new Error("Bad UINT size "+t)}this.#e.write(this.#i.subarray(0,i))}#T(e,t=ce(e)){e<0&&(e+=2**(t*8)),this.#b(e,t)}writeEBMLVarInt(e,t=Ie(e)){let i=0;switch(t){case 1:this.#t.setUint8(i++,128|e);break;case 2:this.#t.setUint8(i++,64|e>>8),this.#t.setUint8(i++,e);break;case 3:this.#t.setUint8(i++,32|e>>16),this.#t.setUint8(i++,e>>8),this.#t.setUint8(i++,e);break;case 4:this.#t.setUint8(i++,16|e>>24),this.#t.setUint8(i++,e>>16),this.#t.setUint8(i++,e>>8),this.#t.setUint8(i++,e);break;case 5:this.#t.setUint8(i++,8|e/2**32&7),this.#t.setUint8(i++,e>>24),this.#t.setUint8(i++,e>>16),this.#t.setUint8(i++,e>>8),this.#t.setUint8(i++,e);break;case 6:this.#t.setUint8(i++,4|e/2**40&3),this.#t.setUint8(i++,e/2**32|0),this.#t.setUint8(i++,e>>24),this.#t.setUint8(i++,e>>16),this.#t.setUint8(i++,e>>8),this.#t.setUint8(i++,e);break;default:throw new Error("Bad EBML VINT size "+t)}this.#e.write(this.#i.subarray(0,i))}#A(e){this.#e.write(new Uint8Array(e.split("").map(t=>t.charCodeAt(0))))}writeEBML(e){if(e!==null){if(e instanceof Uint8Array)this.#e.write(e);else if(Array.isArray(e))for(let t of e)this.writeEBML(t);else if(this.offsets.set(e,this.#e.getPos()),this.#b(e.id),Array.isArray(e.data)){let t=this.#e.getPos(),i=e.size===-1?1:e.size??4;e.size===-1?this.#p(255):this.#e.seek(this.#e.getPos()+i);let n=this.#e.getPos();if(this.dataOffsets.set(e,n),this.writeEBML(e.data),e.size!==-1){let o=this.#e.getPos()-n,l=this.#e.getPos();this.#e.seek(t),this.writeEBMLVarInt(o,i),this.#e.seek(l)}}else if(typeof e.data=="number"){let t=e.size??de(e.data);this.writeEBMLVarInt(t),this.#b(e.data,t)}else if(typeof e.data=="string")this.writeEBMLVarInt(e.data.length),this.#A(e.data);else if(e.data instanceof Uint8Array)this.writeEBMLVarInt(e.data.byteLength,e.size),this.#e.write(e.data);else if(e.data instanceof N)this.writeEBMLVarInt(4),this.#x(e.data.value);else if(e.data instanceof P)this.writeEBMLVarInt(8),this.#g(e.data.value);else if(e.data instanceof R){let t=e.size??ce(e.data.value);this.writeEBMLVarInt(t),this.#T(e.data.value,t)}}}beforeTrackAdd(e){if(this.#r instanceof F)if(e.type==="video"){if(!["vp8","vp9","av1"].includes(e.source.codec))throw new Error("WebM only supports VP8, VP9 and AV1 as video codecs. Switching to MKV removes this restriction.")}else if(e.type==="audio"){if(!["opus","vorbis"].includes(e.source.codec))throw new Error("WebM only supports Opus and Vorbis as audio codecs. Switching to MKV removes this restriction.")}else if(e.type==="subtitle"){if(e.source.codec!=="webvtt")throw new Error("WebM only supports WebVTT as subtitle codec. Switching to MKV removes this restriction.")}else throw new Error("WebM only supports video, audio and subtitle tracks. Switching to MKV removes this restriction.")}start(){this.#O(),this.#r.options.streaming||this.#v(),this.#V(),this.#z(),this.#e.flush()}#O(){let e={id:440786851,data:[{id:17030,data:1},{id:17143,data:1},{id:17138,data:4},{id:17139,data:8},{id:17026,data:this.#r instanceof F?"webm":"matroska"},{id:17031,data:2},{id:17029,data:2}]};this.writeEBML(e)}#v(){let e=new Uint8Array([28,83,187,107]),t=new Uint8Array([21,73,169,102]),i=new Uint8Array([22,84,174,107]),n={id:290298740,data:[{id:19899,data:[{id:21419,data:e},{id:21420,size:5,data:0}]},{id:19899,data:[{id:21419,data:t},{id:21420,size:5,data:0}]},{id:19899,data:[{id:21419,data:i},{id:21420,size:5,data:0}]}]};this.#a=n}#V(){let e={id:17545,data:new P(0)};this.#h=e;let t={id:357149030,data:[{id:2807729,data:1e6},{id:19840,data:_e},{id:22337,data:_e},this.#r.options.streaming?null:e]};this.#n=t}#E(){let e={id:374648427,data:[]};this.#l=e;for(let t of this.#s)e.data.push({id:174,data:[{id:215,data:t.track.id},{id:29637,data:t.track.id},{id:131,data:Dt[t.type]},{id:134,data:Ft[t.track.source.codec]},t.info.decoderConfig.description?{id:25506,data:z(t.info.decoderConfig.description)}:null,...t.type==="video"?[t.track.metadata.frameRate?{id:2352003,data:1e9/t.track.metadata.frameRate}:null,{id:224,data:[{id:176,data:t.info.width},{id:186,data:t.info.height},(()=>{if(t.info.decoderConfig.colorSpace){let i=t.info.decoderConfig.colorSpace;return!i.matrix||!i.transfer||!i.primaries||i.fullRange==null?null:{id:21936,data:[{id:21937,data:{rgb:1,bt709:1,bt470bg:5,smpte170m:6}[i.matrix]},{id:21946,data:{bt709:1,smpte170m:6,"iec61966-2-1":13}[i.transfer]},{id:21947,data:{bt709:1,bt470bg:5,smpte170m:6}[i.primaries]},{id:21945,data:[1,2][Number(i.fullRange)]}]}}return null})()]}]:[],...t.type==="audio"?[{id:225,data:[{id:181,data:new N(t.info.sampleRate)},{id:159,data:t.info.numberOfChannels}]}]:[]]})}#U(){let e={id:408125543,size:this.#r.options.streaming?-1:We,data:[this.#r.options.streaming?null:this.#a,this.#n,this.#l]};this.#o=e,this.writeEBML(e)}#z(){this.#d={id:475249515,data:[]}}get#k(){return d(this.#o),this.dataOffsets.get(this.#o)}#M(e,t){let i=this.#s.find(o=>o.track===e);if(i)return i;d(t),d(t.decoderConfig),d(t.decoderConfig.codedWidth!==void 0),d(t.decoderConfig.codedHeight!==void 0);let n={track:e,type:"video",info:{width:t.decoderConfig.codedWidth,height:t.decoderConfig.codedHeight,decoderConfig:t.decoderConfig},chunkQueue:[],firstTimestamp:null,lastKeyFrameTimestamp:null,lastWrittenMsTimestamp:null};return this.#s.push(n),this.#s.sort((o,l)=>o.track.id-l.track.id),n}#P(e,t){let i=this.#s.find(o=>o.track===e);if(i)return i;d(t),d(t.decoderConfig);let n={track:e,type:"audio",info:{numberOfChannels:t.decoderConfig.numberOfChannels,sampleRate:t.decoderConfig.sampleRate,decoderConfig:t.decoderConfig},chunkQueue:[],firstTimestamp:null,lastKeyFrameTimestamp:null,lastWrittenMsTimestamp:null};return this.#s.push(n),this.#s.sort((o,l)=>o.track.id-l.track.id),n}#F(e,t){let i=this.#s.find(o=>o.track===e);if(i)return i;d(t),d(t.decoderConfig);let n={track:e,type:"subtitle",info:{decoderConfig:t.decoderConfig},chunkQueue:[],firstTimestamp:null,lastKeyFrameTimestamp:null,lastWrittenMsTimestamp:null};return this.#s.push(n),this.#s.sort((o,l)=>o.track.id-l.track.id),n}addEncodedVideoChunk(e,t,i){let n=this.#M(e,i),o=new Uint8Array(t.byteLength);t.copyTo(o);let l=this.#y(n,o,t.timestamp,t.duration??0,t.type);e.source.codec==="vp9"&&this.#D(n,l),n.chunkQueue.push(l),this.#C()}addEncodedAudioChunk(e,t,i){let n=this.#P(e,i),o=new Uint8Array(t.byteLength);t.copyTo(o);let l=this.#y(n,o,t.timestamp,t.duration??0,t.type);n.chunkQueue.push(l),this.#C()}addEncodedSubtitleChunk(e,t,i){let n=this.#F(e,i),o=this.#y(n,t.body,t.timestamp,t.duration,"key",t.additions);n.chunkQueue.push(o),this.#C()}#C(){let e=0;for(let t of this.#s)t.track.source.closed||e++;if(!(this.#s.length0&&o.chunkQueue[0].timestamp=2&&i++;let c={rgb:7,bt709:2,bt470bg:1,smpte170m:3}[e.info.decoderConfig.colorSpace.matrix];ve(t.data,i+0,i+3,c)}#y(e,t,i,n,o,l=null){let a=this.#B(e,i,o==="key");return{data:t,type:o,timestamp:a,duration:n/1e6,additions:l}}#B(e,t,i){let n=t/1e6;if(n<0)throw new Error(`Timestamps must be non-negative (got ${n}s).`);if(e.firstTimestamp===null&&(e.firstTimestamp=n),n-=e.firstTimestamp,e.lastKeyFrameTimestamp!==null&&n{if(e===g)return t.type==="key";let C=g.chunkQueue[0];return C&&C.type==="key"});(!this.#u||n&&i-this.#m>=1e3)&&this.#I(i);let o=i-this.#m;if(o<0)return;if(o>=he)throw new Error(`Current Matroska cluster exceeded its maximum allowed length of ${he} milliseconds. In order to produce a correct WebM file, you must pass in a key frame at least every ${he} milliseconds.`);let a=new Uint8Array(4),c=new DataView(a.buffer);c.setUint8(0,128|e.track.id),c.setInt16(1,o,!1);let b=Math.floor(1e3*t.duration);if(b===0&&!t.additions){c.setUint8(3,+(t.type==="key")<<7);let g={id:163,data:[a,t.data]};this.writeEBML(g)}else{let g={id:160,data:[{id:161,data:[a,t.data]},t.type==="delta"?{id:251,data:new R(e.lastWrittenMsTimestamp-i)}:null,b>0?{id:155,data:b}:null,t.additions?{id:30113,data:t.additions}:null]};this.writeEBML(g)}this.#c=Math.max(this.#c,i+b),e.lastWrittenMsTimestamp=i,this.#f.add(e)}#I(e){this.#u&&!this.#r.options.streaming&&this.#w(),this.#u={id:524531317,size:this.#r.options.streaming?-1:Ne,data:[{id:231,data:e}]},this.writeEBML(this.#u),this.#m=e,this.#f.clear()}#w(){d(this.#u);let e=this.#e.getPos()-this.dataOffsets.get(this.#u),t=this.#e.getPos();this.#e.seek(this.offsets.get(this.#u)+4),this.writeEBMLVarInt(e,Ne),this.#e.seek(t);let i=this.offsets.get(this.#u)-this.#k;d(this.#d),this.#d.data.push({id:187,data:[{id:179,data:this.#m},...[...this.#f].map(n=>({id:183,data:[{id:247,data:n.track.id},{id:241,data:i}]}))]})}onTrackClose(){this.#C()}finalize(){for(let e of this.#s)for(;e.chunkQueue.length>0;)this.#S(e,e.chunkQueue.shift());if(this.#r.options.streaming||this.#w(),d(this.#d),this.writeEBML(this.#d),!this.#r.options.streaming){let e=this.#e.getPos(),t=this.#e.getPos()-this.#k;this.#e.seek(this.offsets.get(this.#o)+4),this.writeEBMLVarInt(t,We),this.#h.data=new P(this.#c),this.#e.seek(this.offsets.get(this.#h)),this.writeEBML(this.#h),this.#a.data[0].data[1].data=this.offsets.get(this.#d)-this.#k,this.#a.data[1].data[1].data=this.offsets.get(this.#n)-this.#k,this.#a.data[2].data[1].data=this.offsets.get(this.#l)-this.#k,this.#e.seek(this.offsets.get(this.#a)),this.writeEBML(this.#a),this.#e.seek(e)}}};var J=class{},me=class extends J{constructor(e){super();this.options=e}createMuxer(e){return new Y(e,this)}},ee=class extends J{constructor(e={}){super();this.options=e}createMuxer(e){return new Z(e,this)}},F=class extends ee{};var Re=(s,r,e)=>{if(s==="avc"){let t=100;r<=768&&e<=432?t=66:r<=1920&&e<=1080&&(t=77);let i=0,n=r>1920||e>1080?50:41,o=t.toString(16).padStart(2,"0"),l=i.toString(16).padStart(2,"0"),a=n.toString(16).padStart(2,"0");return`avc1.${o}${l}${a}`}else if(s==="hevc"){let t=0,i=1,n=Array(32).fill(0);n[i]=1;let o=parseInt(n.reverse().join(""),2).toString(16).replace(/^0+/,""),l="L",a=120;return r<=1280&&e<=720?a=93:r<=1920&&e<=1080?a=120:r<=3840&&e<=2160?a=150:(l="H",a=180),`hev1.${t===0?"":String.fromCharCode(65+t-1)}${i}.${o}.${l}${a}.B0`}else{if(s==="vp8")return"vp8";if(s==="vp9"){let t="00",i;return r<=854&&e<=480?i="21":r<=1280&&e<=720?i="31":r<=1920&&e<=1080?i="41":r<=3840&&e<=2160?i="51":i="61",`vp09.${t}.${i}.08`}else if(s==="av1"){let i;return r<=854&&e<=480?i="01":r<=1280&&e<=720?i="03":r<=1920&&e<=1080?i="04":r<=3840&&e<=2160?i="07":i="09",`av01.0.${i}M.08`}}throw new Error(`Unhandled codec '${s}'.`)},Qe=(s,r,e)=>{if(s==="aac")return r>=2&&e<=24e3?"mp4a.40.29":e<=24e3?"mp4a.40.5":"mp4a.40.2";if(s==="opus")return"opus";if(s==="vorbis")return"vorbis";throw new Error(`Unhandled codec '${s}'.`)};var Q=/(?:(.+?)\n)?((?:\d{2}:)?\d{2}:\d{2}.\d{3})\s+-->\s+((?:\d{2}:)?\d{2}:\d{2}.\d{3})/g,Bt=/^WEBVTT.*?\n{2}/,It=/(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})/,He=/<(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})>/g,fe=new TextEncoder,te=class{#e;#r=null;#i=null;#t=!1;constructor(r){this.#e=r}configure(r){if(r.codec!=="webvtt")throw new Error("Codec must be 'webvtt'.");this.#r=r}encode(r){if(!this.#r)throw new Error("Encoder not configured.");r=r.replace(`\r +`,` +`).replace("\r",` +`),Q.lastIndex=0;let e;if(!this.#i){if(!Bt.test(r)){let i=new Error("WebVTT preamble incorrect.");throw this.#e.error(i),i}e=Q.exec(r);let t=r.slice(0,e?.index??r.length).trimEnd();if(!t){let i=new Error("No WebVTT preamble provided.");throw this.#e.error(i),i}this.#i=fe.encode(t),e&&(r=r.slice(e.index),Q.lastIndex=0)}for(;e=Q.exec(r);){let t=r.slice(0,e.index),i=e[1]||"",n=e.index+e[0].length,o=r.indexOf(` +`,n)+1,l=r.slice(n,o).trim(),a=r.indexOf(` + +`,n);a===-1&&(a=r.length);let c=this.#s(e[2]),g=this.#s(e[3])-c,C=r.slice(o,a),_=`${l} +${i} +${t}`;He.lastIndex=0,C=C.replace(He,E=>{let $e=this.#s(E.slice(1,-1))-c;return`<${this.#o($e)}>`}),r=r.slice(a).trimStart(),Q.lastIndex=0;let A={body:fe.encode(C),additions:_.trim()===""?null:fe.encode(_),timestamp:c*1e3,duration:g*1e3},T={};this.#t||(T.decoderConfig={description:this.#i},this.#t=!0),this.#e.output(A,T)}}#s(r){let e=It.exec(r);if(!e)throw new Error("Expected match.");return 60*60*1e3*Number(e[1]||"0")+60*1e3*Number(e[2])+1e3*Number(e[3])+Number(e[4])}#o(r){let e=Math.floor(r/36e5),t=Math.floor(r%(60*60*1e3)/(60*1e3)),i=Math.floor(r%(60*1e3)/1e3),n=r%1e3;return e.toString().padStart(2,"0")+":"+t.toString().padStart(2,"0")+":"+i.toString().padStart(2,"0")+"."+n.toString().padStart(3,"0")}};var H=class{constructor(){this.connectedTrack=null;this.closed=!1}ensureValidDigest(){if(!this.connectedTrack)throw new Error("Cannot call digest without connecting the source to an output track.");if(!this.connectedTrack.output.started)throw new Error("Cannot call digest before output has been started.");if(this.connectedTrack.output.finalizing)throw new Error("Cannot call digest after output has started finalizing.");if(this.closed)throw new Error("Cannot call digest after source has been closed.")}start(){}async flush(){}close(){if(this.closed)throw new Error("Source already closed.");if(!this.connectedTrack)throw new Error("Cannot call close without connecting the source to an output track.");if(!this.connectedTrack.output.started)throw new Error("Cannot call close before output has been started.");this.closed=!0,!this.connectedTrack.output.finalizing&&this.connectedTrack.output.muxer.onTrackClose(this.connectedTrack)}},D=class extends H{constructor(e){super();this.connectedTrack=null;this.codec=e}},pe=class extends D{constructor(r){super(r)}digest(r,e){this.ensureValidDigest(),this.connectedTrack?.output.muxer.addEncodedVideoChunk(this.connectedTrack,r,e)}},_t=5,$=class{constructor(r,e){this.source=r;this.codecConfig=e;this.encoder=null;this.lastMultipleOfKeyFrameInterval=-1}digest(r){this.source.ensureValidDigest(),this.ensureEncoder(r),d(this.encoder);let e=Math.floor(r.timestamp/1e6/_t);this.encoder.encode(r,{keyFrame:e!==this.lastMultipleOfKeyFrameInterval}),this.lastMultipleOfKeyFrameInterval=e}ensureEncoder(r){this.encoder||(this.encoder=new VideoEncoder({output:(e,t)=>this.source.connectedTrack?.output.muxer.addEncodedVideoChunk(this.source.connectedTrack,e,t),error:e=>console.error(e)}),this.encoder.configure({codec:Re(this.codecConfig.codec,r.codedWidth,r.codedHeight),width:r.codedWidth,height:r.codedHeight,bitrate:this.codecConfig.bitrate}))}async flush(){return this.encoder?.flush()}},be=class extends D{constructor(r){super(r.codec),this.encoder=new $(this,r)}digest(r){this.encoder.digest(r)}flush(){return this.encoder.flush()}},ke=class extends D{constructor(e,t){super(t.codec);this.canvas=e;this.encoder=new $(this,t)}digest(e,t=0){let i=new VideoFrame(this.canvas,{timestamp:Math.round(1e6*e),duration:Math.round(1e6*t)});this.encoder.digest(i),i.close()}flush(){return this.encoder.flush()}},ge=class extends D{constructor(e,t){super(t.codec);this.track=e;this.abortController=null;this.encoder=new $(this,t)}start(){this.abortController=new AbortController;let e=new MediaStreamTrackProcessor({track:this.track}),t=new WritableStream({write:i=>{this.encoder.digest(i),i.close()}});e.readable.pipeTo(t,{signal:this.abortController.signal}).catch(i=>{i instanceof DOMException&&i.name==="AbortError"||console.error("Pipe error:",i)})}async flush(){this.abortController&&(this.abortController.abort(),this.abortController=null),await this.encoder.flush()}},B=class extends H{constructor(e){super();this.connectedTrack=null;this.codec=e}},Te=class extends B{constructor(r){super(r)}digest(r,e){this.ensureValidDigest(),this.connectedTrack?.output.muxer.addEncodedAudioChunk(this.connectedTrack,r,e)}},K=class{constructor(r,e){this.source=r;this.codecConfig=e;this.encoder=null}digest(r){this.source.ensureValidDigest(),this.ensureEncoder(r),d(this.encoder),this.encoder.encode(r)}ensureEncoder(r){this.encoder||(this.encoder=new AudioEncoder({output:(e,t)=>this.source.connectedTrack?.output.muxer.addEncodedAudioChunk(this.source.connectedTrack,e,t),error:e=>console.error(e)}),this.encoder.configure({codec:Qe(this.codecConfig.codec,r.numberOfChannels,r.sampleRate),numberOfChannels:r.numberOfChannels,sampleRate:r.sampleRate,bitrate:this.codecConfig.bitrate}))}async flush(){return this.encoder?.flush()}},Ce=class extends B{constructor(r){super(r.codec),this.encoder=new K(this,r)}digest(r){this.encoder.digest(r)}flush(){return this.encoder.flush()}},xe=class extends B{constructor(e){super(e.codec);this.accumulatedFrameCount=0;this.encoder=new K(this,e)}digest(e){let t=e.numberOfChannels,i=e.sampleRate,n=e.length,o=new Float32Array(t*n);for(let a=0;a{this.encoder.digest(i),i.close()}});e.readable.pipeTo(t,{signal:this.abortController.signal}).catch(i=>{i instanceof DOMException&&i.name==="AbortError"||console.error("Pipe error:",i)})}async flush(){this.abortController&&(this.abortController.abort(),this.abortController=null),await this.encoder.flush()}},Se=class extends H{constructor(e){super();this.connectedTrack=null;this.codec=e}},we=class extends Se{constructor(r){super(r),this.encoder=new te({output:(e,t)=>this.connectedTrack?.output.muxer.addEncodedSubtitleChunk(this.connectedTrack,e,t),error:e=>console.error(e)}),this.encoder.configure({codec:r})}digest(r){this.ensureValidDigest(),this.encoder.encode(r)}};var G=class{},re=class extends G{#e=0;#r;#i=new ArrayBuffer(2**16);#t=new Uint8Array(this.#i);#s=0;constructor(r){super(),this.#r=r}#o(r){let e=this.#i.byteLength;for(;et.start-i.start);r.push({start:e[0].start,size:e[0].data.byteLength});for(let t=1;ta.start<=e&&eNt){for(let a=0;a=r.written[n+1].start;)r.written[n].end=Math.max(r.written[n].end,r.written[n+1].end),r.written.splice(n+1,1)}#n(r){let t={start:Math.floor(r/this.#i)*this.#i,data:new Uint8Array(this.#i),written:[],shouldFlush:!1};return this.#t.push(t),this.#t.sort((i,n)=>i.start-n.start),this.#t.indexOf(t)}#a(r=!1){for(let e=0;er.stream.write({type:"write",data:e,position:t}),chunkSize:r.options?.chunkSize}))}};var I=class{constructor(){this.output=null}},Ae=class extends I{constructor(){super(...arguments);this.buffer=null}createWriter(){return new re(this)}},q=class extends I{constructor(e){super();this.options=e;if(typeof e!="object")throw new TypeError("StreamTarget requires an options object to be passed to its constructor.");if(e.onData){if(typeof e.onData!="function")throw new TypeError("options.onData, when provided, must be a function.");if(e.onData.length<2)throw new TypeError("options.onData, when provided, must be a function that takes in at least two arguments (data and position). Ignoring the position argument, which specifies the byte offset at which the data is to be written, can lead to broken outputs.")}if(e.chunked!==void 0&&typeof e.chunked!="boolean")throw new TypeError("options.chunked, when provided, must be a boolean.");if(e.chunkSize!==void 0&&(!Number.isInteger(e.chunkSize)||e.chunkSize<=0))throw new TypeError("options.chunkSize, when provided, must be a positive integer.")}createWriter(){return this.options.chunked?new j(this):new ie(this)}},Oe=class extends I{constructor(e,t){super();this.stream=e;this.options=t;if(!(e instanceof FileSystemWritableFileStream))throw new TypeError("FileSystemWritableFileStreamTarget requires a FileSystemWritableFileStream instance.");if(t!==void 0&&typeof t!="object")throw new TypeError("FileSystemWritableFileStreamTarget's options, when provided, must be an object.");if(t&&t.chunkSize!==void 0&&(!Number.isInteger(t.chunkSize)||t.chunkSize<=0))throw new TypeError("options.chunkSize, when provided, must be a positive integer")}createWriter(){return new se(this)}};export{Ae as ArrayBufferTarget,xe as AudioBufferSource,Ce as AudioDataSource,ke as CanvasSource,Te as EncodedAudioChunkSource,pe as EncodedVideoChunkSource,Oe as FileSystemWritableFileStreamTarget,ye as MediaStreamAudioTrackSource,ge as MediaStreamVideoTrackSource,ee as MkvOutputFormat,me as Mp4OutputFormat,ne as Output,q as StreamTarget,I as Target,we as TextSubtitleSource,be as VideoFrameSource,F as WebMOutputFormat}; diff --git a/dist/metamuxer.mjs b/dist/metamuxer.mjs index 0bbe4b3..f4cf278 100644 --- a/dist/metamuxer.mjs +++ b/dist/metamuxer.mjs @@ -1,92 +1,64 @@ -// src/codec.ts -var buildVideoCodecString = (codec, width, height) => { - if (codec === "avc") { - let profileIndication = 100; - if (width <= 768 && height <= 432) { - profileIndication = 66; - } else if (width <= 1920 && height <= 1080) { - profileIndication = 77; +// src/output.ts +var Output = class { + constructor(options) { + this.tracks = []; + this.started = false; + this.finalizing = false; + if (options.target.output) { + throw new Error("Target is already used for another output."); } - const profileCompatibility = 0; - const levelIndication = width > 1920 || height > 1080 ? 50 : 41; - const hexProfileIndication = profileIndication.toString(16).padStart(2, "0"); - const hexProfileCompatibility = profileCompatibility.toString(16).padStart(2, "0"); - const hexLevelIndication = levelIndication.toString(16).padStart(2, "0"); - return `avc1.${hexProfileIndication}${hexProfileCompatibility}${hexLevelIndication}`; - } else if (codec === "hevc") { - let profileSpace = 0; - let profileIdc = 1; - const compatibilityFlags = Array(32).fill(0); - compatibilityFlags[profileIdc] = 1; - const compatibilityHex = parseInt(compatibilityFlags.reverse().join(""), 2).toString(16).replace(/^0+/, ""); - let tier = "L"; - let level = 120; - if (width <= 1280 && height <= 720) { - level = 93; - } else if (width <= 1920 && height <= 1080) { - level = 120; - } else if (width <= 3840 && height <= 2160) { - level = 150; - } else { - tier = "H"; - level = 180; - } - const constraintFlags = "B0"; - const profilePrefix = profileSpace === 0 ? "" : String.fromCharCode(65 + profileSpace - 1); - return `hev1.${profilePrefix}${profileIdc}.${compatibilityHex}.${tier}${level}.${constraintFlags}`; - } else if (codec === "vp8") { - return "vp8"; - } else if (codec === "vp9") { - const profile = "00"; - let level; - if (width <= 854 && height <= 480) { - level = "21"; - } else if (width <= 1280 && height <= 720) { - level = "31"; - } else if (width <= 1920 && height <= 1080) { - level = "41"; - } else if (width <= 3840 && height <= 2160) { - level = "51"; - } else { - level = "61"; - } - const bitDepth = "08"; - return `vp09.${profile}.${level}.${bitDepth}`; - } else if (codec === "av1") { - const profile = 0; - let level; - if (width <= 854 && height <= 480) { - level = "01"; - } else if (width <= 1280 && height <= 720) { - level = "03"; - } else if (width <= 1920 && height <= 1080) { - level = "04"; - } else if (width <= 3840 && height <= 2160) { - level = "07"; - } else { - level = "09"; - } - const tier = "M"; - const bitDepth = "08"; - return `av01.${profile}.${level}${tier}.${bitDepth}`; + options.target.output = this; + this.writer = options.target.createWriter(); + this.muxer = options.format.createMuxer(this); } - throw new Error(`Unhandled codec '${codec}'.`); -}; -var buildAudioCodecString = (codec, numberOfChannels, sampleRate) => { - if (codec === "aac") { - if (numberOfChannels >= 2 && sampleRate <= 24e3) { - return "mp4a.40.29"; - } - if (sampleRate <= 24e3) { - return "mp4a.40.5"; - } - return "mp4a.40.2"; - } else if (codec === "opus") { - return "opus"; - } else if (codec === "vorbis") { - return "vorbis"; + addVideoTrack(source, metadata = {}) { + this.addTrack("video", source, metadata); + } + addAudioTrack(source, metadata = {}) { + this.addTrack("audio", source, metadata); + } + addSubtitleTrack(source, metadata = {}) { + this.addTrack("subtitle", source, metadata); + } + addTrack(type, source, metadata) { + if (this.started) { + throw new Error("Cannot add track after output has started."); + } + if (source.connectedTrack) { + throw new Error("Source is already used for a track."); + } + const track = { + id: this.tracks.length + 1, + output: this, + type, + source, + metadata + }; + this.muxer.beforeTrackAdd(track); + this.tracks.push(track); + source.connectedTrack = track; + } + start() { + if (this.started) { + throw new Error("Output already started."); + } + this.started = true; + this.muxer.start(); + for (const track of this.tracks) { + track.source.start(); + } + } + async finalize() { + if (this.finalizing) { + throw new Error("Cannot call finalize twice."); + } + this.finalizing = true; + const promises = this.tracks.map((x) => x.source.flush()); + await Promise.all(promises); + this.muxer.finalize(); + this.writer.flush(); + this.writer.finalize(); } - throw new Error(`Unhandled codec '${codec}'.`); }; // src/misc.ts @@ -131,318 +103,6 @@ var toUint8Array = (source) => { } }; -// src/source.ts -var VideoSource = class { - 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; - } - ensureNotFinalizing() { - if (this.connectedTrack?.output.finalizing) { - throw new Error("Cannot call digest after output has started finalizing."); - } - } - start() { - } - async flush() { - } -}; -var EncodedVideoChunkSource = class extends VideoSource { - constructor(codec) { - super(codec); - } - // TODO: Ensure that the first chunk is a key frame (same for the audio case) - digest(chunk, meta) { - this.ensureValidDigest(); - this.connectedTrack?.output.muxer.addEncodedVideoChunk(this.connectedTrack, chunk, meta); - } -}; -var KEY_FRAME_INTERVAL = 5; -var VideoEncoderWrapper = class { - constructor(source, codecConfig) { - this.source = source; - this.codecConfig = codecConfig; - this.encoder = null; - this.lastMultipleOfKeyFrameInterval = -1; - } - // TODO: Ensure video frame size remains constant - digest(videoFrame) { - this.source.ensureValidDigest(); - this.ensureEncoder(videoFrame); - assert(this.encoder); - const multipleOfKeyFrameInterval = Math.floor(videoFrame.timestamp / 1e6 / KEY_FRAME_INTERVAL); - this.encoder.encode(videoFrame, { keyFrame: multipleOfKeyFrameInterval !== this.lastMultipleOfKeyFrameInterval }); - this.lastMultipleOfKeyFrameInterval = multipleOfKeyFrameInterval; - } - ensureEncoder(videoFrame) { - if (this.encoder) { - return; - } - this.encoder = new VideoEncoder({ - output: (chunk, meta) => this.source.connectedTrack?.output.muxer.addEncodedVideoChunk(this.source.connectedTrack, chunk, meta), - error: (error) => console.error(error) - // TODO - }); - this.encoder.configure({ - codec: buildVideoCodecString(this.codecConfig.codec, videoFrame.codedWidth, videoFrame.codedHeight), - width: videoFrame.codedWidth, - height: videoFrame.codedHeight, - bitrate: this.codecConfig.bitrate - }); - } - async flush() { - return this.encoder?.flush(); - } -}; -var VideoFrameSource = class extends VideoSource { - constructor(codecConfig) { - super(codecConfig.codec); - this.encoder = new VideoEncoderWrapper(this, codecConfig); - } - digest(videoFrame) { - this.encoder.digest(videoFrame); - } - flush() { - return this.encoder.flush(); - } -}; -var CanvasSource = class extends VideoSource { - constructor(canvas, codecConfig) { - super(codecConfig.codec); - this.canvas = canvas; - this.encoder = new VideoEncoderWrapper(this, codecConfig); - } - digest(timestamp, duration = 0) { - const frame = new VideoFrame(this.canvas, { - timestamp: Math.round(1e6 * timestamp), - duration: Math.round(1e6 * duration) - }); - this.encoder.digest(frame); - frame.close(); - } - flush() { - return this.encoder.flush(); - } -}; -var MediaStreamVideoTrackSource = class extends VideoSource { - constructor(track, codecConfig) { - super(codecConfig.codec); - this.track = track; - this.abortController = null; - this.encoder = new VideoEncoderWrapper(this, codecConfig); - } - start() { - this.abortController = new AbortController(); - const processor = new MediaStreamTrackProcessor({ track: this.track }); - const consumer = new WritableStream({ - write: (videoFrame) => { - this.encoder.digest(videoFrame); - videoFrame.close(); - } - }); - processor.readable.pipeTo(consumer, { - signal: this.abortController.signal - }).catch((err) => { - if (err instanceof DOMException && err.name === "AbortError") return; - console.error("Pipe error:", err); - }); - } - async flush() { - if (this.abortController) { - this.abortController.abort(); - this.abortController = null; - } - 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); - assert(this.encoder); - this.encoder.encode(audioData); - } - ensureEncoder(audioData) { - if (this.encoder) { - return; - } - this.encoder = new AudioEncoder({ - output: (chunk, meta) => this.source.connectedTrack?.output.muxer.addEncodedAudioChunk(this.source.connectedTrack, chunk, meta), - error: (error) => console.error(error) - // TODO - }); - this.encoder.configure({ - codec: buildAudioCodecString(this.codecConfig.codec, audioData.numberOfChannels, audioData.sampleRate), - numberOfChannels: audioData.numberOfChannels, - sampleRate: audioData.sampleRate, - bitrate: this.codecConfig.bitrate - }); - } - async flush() { - return this.encoder?.flush(); - } -}; -var AudioDataSource = class extends AudioSource { - constructor(codecConfig) { - super(codecConfig.codec); - this.encoder = new AudioEncoderWrapper(this, codecConfig); - } - digest(audioData) { - this.encoder.digest(audioData); - } - flush() { - return this.encoder.flush(); - } -}; -var AudioBufferSource = class extends AudioSource { - constructor(codecConfig) { - super(codecConfig.codec); - this.accumulatedFrameCount = 0; - this.encoder = new AudioEncoderWrapper(this, codecConfig); - } - digest(audioBuffer) { - const numberOfChannels = audioBuffer.numberOfChannels; - const sampleRate = audioBuffer.sampleRate; - const numberOfFrames = audioBuffer.length; - const data = new Float32Array(numberOfChannels * numberOfFrames); - for (let channel = 0; channel < numberOfChannels; channel++) { - const channelData = audioBuffer.getChannelData(channel); - data.set(channelData, channel * numberOfFrames); - } - const audioData = new AudioData({ - format: "f32-planar", - sampleRate, - numberOfFrames, - numberOfChannels, - timestamp: Math.round(1e6 * this.accumulatedFrameCount / sampleRate), - data - }); - this.encoder.digest(audioData); - audioData.close(); - this.accumulatedFrameCount += numberOfFrames; - } - flush() { - return this.encoder.flush(); - } -}; -var MediaStreamAudioTrackSource = class extends AudioSource { - constructor(track, codecConfig) { - super(codecConfig.codec); - this.track = track; - this.abortController = null; - this.encoder = new AudioEncoderWrapper(this, codecConfig); - } - start() { - this.abortController = new AbortController(); - const processor = new MediaStreamTrackProcessor({ track: this.track }); - const consumer = new WritableStream({ - write: (audioData) => { - this.encoder.digest(audioData); - audioData.close(); - } - }); - processor.readable.pipeTo(consumer, { - signal: this.abortController.signal - }).catch((err) => { - if (err instanceof DOMException && err.name === "AbortError") return; - console.error("Pipe error:", err); - }); - } - async flush() { - if (this.abortController) { - this.abortController.abort(); - this.abortController = null; - } - await this.encoder.flush(); - } -}; - -// src/output.ts -var Output = class { - constructor(options) { - this.tracks = []; - this.started = false; - this.finalizing = false; - this.writer = options.target.createWriter(); - this.muxer = options.format.createMuxer(this); - } - addTrack(source, metadata = {}) { - if (this.started) { - throw new Error("Cannot add track after output has started."); - } - if (source.connectedTrack) { - throw new Error("Source is already used for a track."); - } - const track = { - id: this.tracks.length + 1, - output: this, - type: source instanceof VideoSource ? "video" : "audio", - source, - metadata - }; - this.muxer.beforeTrackAdd(track); - this.tracks.push(track); - source.connectedTrack = track; - } - start() { - if (this.started) { - throw new Error("Output already started."); - } - this.started = true; - this.muxer.start(); - for (const track of this.tracks) { - track.source.start(); - } - } - async finalize() { - if (this.finalizing) { - throw new Error("Cannot call finalize twice."); - } - this.finalizing = true; - const promises = this.tracks.map((x) => x.source.flush()); - await Promise.all(promises); - this.muxer.finalize(); - this.writer.flush(); - this.writer.finalize(); - } -}; - // src/isobmff/isobmff_boxes.ts var bytes = new Uint8Array(8); var view = new DataView(bytes.buffer); @@ -1187,6 +847,8 @@ var Muxer = class { } beforeTrackAdd(track) { } + onTrackClose(track) { + } }; // src/isobmff/isobmff_muxer.ts @@ -1797,8 +1459,6 @@ var measureEBMLVarInt = (value) => { }; // src/matroska/matroska_muxer.ts -var VIDEO_TRACK_TYPE = 1; -var AUDIO_TRACK_TYPE = 2; var MAX_CHUNK_LENGTH_MS = 2 ** 15; var APP_NAME = "https://github.com/Vanilagy/webm-muxer"; var SEGMENT_SIZE_BYTES = 6; @@ -1811,7 +1471,13 @@ var CODEC_STRING_MAP = { av1: "V_AV1", aac: "A_AAC", opus: "A_OPUS", - vorbis: "A_VORBIS" + vorbis: "A_VORBIS", + webvtt: "S_TEXT/WEBVTT" +}; +var TRACK_TYPE_MAP = { + video: 1, + audio: 2, + subtitle: 17 }; var MatroskaMuxer = class extends Muxer { constructor(output, format) { @@ -1998,10 +1664,16 @@ var MatroskaMuxer = class extends Muxer { if (!["vp8", "vp9", "av1"].includes(track.source.codec)) { throw new Error(`WebM only supports VP8, VP9 and AV1 as video codecs. Switching to MKV removes this restriction.`); } - } else { + } else if (track.type === "audio") { if (!["opus", "vorbis"].includes(track.source.codec)) { throw new Error(`WebM only supports Opus and Vorbis as audio codecs. Switching to MKV removes this restriction.`); } + } else if (track.type === "subtitle") { + if (track.source.codec !== "webvtt") { + throw new Error(`WebM only supports WebVTT as subtitle codec. Switching to MKV removes this restriction.`); + } + } else { + throw new Error("WebM only supports video, audio and subtitle tracks. Switching to MKV removes this restriction."); } } start() { @@ -2067,7 +1739,7 @@ var MatroskaMuxer = class extends Muxer { tracksElement.data.push({ id: 174 /* TrackEntry */, data: [ { id: 215 /* TrackNumber */, data: trackData.track.id }, { id: 29637 /* TrackUID */, data: trackData.track.id }, - { id: 131 /* TrackType */, data: trackData.type === "video" ? VIDEO_TRACK_TYPE : AUDIO_TRACK_TYPE }, + { id: 131 /* TrackType */, data: TRACK_TYPE_MAP[trackData.type] }, // TODO Subtitle case { id: 134 /* CodecID */, data: CODEC_STRING_MAP[trackData.track.source.codec] }, trackData.info.decoderConfig.description ? { id: 25506 /* CodecPrivate */, data: toUint8Array(trackData.info.decoderConfig.description) } : null, @@ -2186,23 +1858,55 @@ var MatroskaMuxer = class extends Muxer { this.#trackDatas.sort((a, b) => a.track.id - b.track.id); return newTrackData; } + #getSubtitleTrackData(track, meta) { + const existingTrackData = this.#trackDatas.find((x) => x.track === track); + if (existingTrackData) { + return existingTrackData; + } + assert(meta); + assert(meta.decoderConfig); + const newTrackData = { + track, + type: "subtitle", + info: { + decoderConfig: meta.decoderConfig + }, + chunkQueue: [], + firstTimestamp: null, + lastKeyFrameTimestamp: null, + lastWrittenMsTimestamp: null + }; + this.#trackDatas.push(newTrackData); + this.#trackDatas.sort((a, b) => a.track.id - b.track.id); + return newTrackData; + } addEncodedVideoChunk(track, chunk, meta) { const trackData = this.#getVideoTrackData(track, meta); - let videoChunk = this.#createInternalChunk(trackData, chunk); + let data = new Uint8Array(chunk.byteLength); + chunk.copyTo(data); + let videoChunk = this.#createInternalChunk(trackData, data, chunk.timestamp, chunk.duration ?? 0, chunk.type); if (track.source.codec === "vp9") this.#fixVP9ColorSpace(trackData, videoChunk); trackData.chunkQueue.push(videoChunk); this.#interleaveChunks(); - this.#writer.flush(); } addEncodedAudioChunk(track, chunk, meta) { const trackData = this.#getAudioTrackData(track, meta); - let audioChunk = this.#createInternalChunk(trackData, chunk); + let data = new Uint8Array(chunk.byteLength); + chunk.copyTo(data); + let audioChunk = this.#createInternalChunk(trackData, data, chunk.timestamp, chunk.duration ?? 0, chunk.type); trackData.chunkQueue.push(audioChunk); this.#interleaveChunks(); - this.#writer.flush(); + } + addEncodedSubtitleChunk(track, chunk, meta) { + const trackData = this.#getSubtitleTrackData(track, meta); + let subtitleChunk = this.#createInternalChunk(trackData, chunk.body, chunk.timestamp, chunk.duration, "key", chunk.additions); + trackData.chunkQueue.push(subtitleChunk); + this.#interleaveChunks(); } #interleaveChunks() { - if (this.#trackDatas.length < this.output.tracks.length) { + let openTrackCount = 0; + for (const trackData of this.#trackDatas) if (!trackData.track.source.closed) openTrackCount++; + if (this.#trackDatas.length < openTrackCount) { return; } outer: @@ -2210,10 +1914,10 @@ var MatroskaMuxer = class extends Muxer { let trackWithMinTimestamp = null; let minTimestamp = Infinity; for (let trackData of this.#trackDatas) { - if (trackData.chunkQueue.length === 0) { + if (trackData.chunkQueue.length === 0 && !trackData.track.source.closed) { break outer; } - if (trackData.chunkQueue[0].timestamp < minTimestamp) { + if (trackData.chunkQueue.length > 0 && trackData.chunkQueue[0].timestamp < minTimestamp) { trackWithMinTimestamp = trackData; minTimestamp = trackData.chunkQueue[0].timestamp; } @@ -2224,6 +1928,7 @@ var MatroskaMuxer = class extends Muxer { let chunk = trackWithMinTimestamp.chunkQueue.shift(); this.#writeBlock(trackWithMinTimestamp, chunk); } + this.#writer.flush(); } /** Due to [a bug in Chromium](https://bugs.chromium.org/p/chromium/issues/detail?id=1377842), VP9 streams often * lack color space information. This method patches in that information. */ @@ -2256,91 +1961,20 @@ var MatroskaMuxer = class extends Muxer { }[trackData.info.decoderConfig.colorSpace.matrix]; writeBits(chunk.data, i + 0, i + 3, colorSpaceID); } - /* - addSubtitleChunk(chunk: EncodedSubtitleChunk, meta: EncodedSubtitleChunkMetadata, timestamp?: number) { - if (typeof chunk !== 'object' || !chunk) { - throw new TypeError("addSubtitleChunk's first argument (chunk) must be an object."); - } else { - // We can't simply do an instanceof check, so let's check the structure itself: - if (!(chunk.body instanceof Uint8Array)) { - throw new TypeError('body must be an instance of Uint8Array.'); - } - if (!Number.isFinite(chunk.timestamp) || chunk.timestamp < 0) { - throw new TypeError('timestamp must be a non-negative real number.'); - } - if (!Number.isFinite(chunk.duration) || chunk.duration < 0) { - throw new TypeError('duration must be a non-negative real number.'); - } - if (chunk.additions && !(chunk.additions instanceof Uint8Array)) { - throw new TypeError('additions, when present, must be an instance of Uint8Array.'); - } - } - - if (typeof meta !== 'object') { - throw new TypeError("addSubtitleChunk's second argument (meta) must be an object."); - } - - this.#ensureNotFinalized(); - if (!this.#options.subtitles) throw new Error('No subtitle track declared.'); - - // Write possible subtitle decoder metadata to the file - if (meta?.decoderConfig) { - if (this.#options.streaming) { - this.#subtitleCodecPrivate = this.#createCodecPrivateElement(meta.decoderConfig.description); - } else { - this.#writeCodecPrivate(this.#subtitleCodecPrivate, meta.decoderConfig.description); - } - } - - let subtitleChunk = this.#createInternalChunk( - chunk.body, - 'key', - timestamp ?? chunk.timestamp, - SUBTITLE_TRACK_NUMBER, - chunk.duration, - chunk.additions - ); - - this.#lastSubtitleTimestamp = subtitleChunk.timestamp; - this.#subtitleChunkQueue.push(subtitleChunk); - - this.#writeSubtitleChunks(); - this.#maybeFlushStreamingTargetWriter(); - } - - #writeSubtitleChunks() { - // Writing subtitle chunks is different from video and audio: A subtitle chunk will be written if it's - // guaranteed that no more media chunks will be written before it, to ensure monotonicity. However, media chunks - // will NOT wait for subtitle chunks to arrive, as they may never arrive, so that's how non-monotonicity can - // arrive. But it should be fine, since it's all still in one cluster. - - let lastWrittenMediaTimestamp = Math.min( - this.#options.video ? this.#lastVideoTimestamp : Infinity, - this.#options.audio ? this.#lastAudioTimestamp : Infinity - ); - - let queue = this.#subtitleChunkQueue; - while (queue.length > 0 && queue[0].timestamp <= lastWrittenMediaTimestamp) { - this.#writeBlock(queue.shift(), !this.#options.video && !this.#options.audio); - } - } - */ /** Converts a read-only external chunk into an internal one for easier use. */ - #createInternalChunk(trackData, chunk) { - let adjustedTimestamp = this.#validateTimestamp(trackData, chunk); - let data = new Uint8Array(chunk.byteLength); - chunk.copyTo(data); + #createInternalChunk(trackData, data, timestamp, duration, type, additions = null) { + let adjustedTimestamp = this.#validateTimestamp(trackData, timestamp, type === "key"); let internalChunk = { data, - type: chunk.type, + type, timestamp: adjustedTimestamp, - duration: (chunk.duration ?? 0) / 1e6, - additions: null + duration: duration / 1e6, + additions }; return internalChunk; } - #validateTimestamp(trackData, chunk) { - let timestampInSeconds = chunk.timestamp / 1e6; + #validateTimestamp(trackData, timestamp, isKeyFrame) { + let timestampInSeconds = timestamp / 1e6; if (timestampInSeconds < 0) { throw new Error(`Timestamps must be non-negative (got ${timestampInSeconds}s).`); } @@ -2351,7 +1985,7 @@ var MatroskaMuxer = class extends Muxer { if (trackData.lastKeyFrameTimestamp !== null && timestampInSeconds < trackData.lastKeyFrameTimestamp) { throw new Error(`Timestamp cannot be before last key frame's timestamp (got ${timestampInSeconds}s, last key frame at ${trackData.lastKeyFrameTimestamp}s).`); } - if (chunk.type === "key") { + if (isKeyFrame) { trackData.lastKeyFrameTimestamp = timestampInSeconds; } return timestampInSeconds; @@ -2447,6 +2081,9 @@ var MatroskaMuxer = class extends Muxer { }) ] }); } + onTrackClose() { + this.#interleaveChunks(); + } /** Finalizes the file, making it ready for use. Must be called after all media chunks have been added. */ finalize() { for (let trackData of this.#trackDatas) { @@ -2501,6 +2138,499 @@ var MkvOutputFormat2 = class extends OutputFormat { var WebMOutputFormat = class extends MkvOutputFormat2 { }; +// src/codec.ts +var buildVideoCodecString = (codec, width, height) => { + if (codec === "avc") { + let profileIndication = 100; + if (width <= 768 && height <= 432) { + profileIndication = 66; + } else if (width <= 1920 && height <= 1080) { + profileIndication = 77; + } + const profileCompatibility = 0; + const levelIndication = width > 1920 || height > 1080 ? 50 : 41; + const hexProfileIndication = profileIndication.toString(16).padStart(2, "0"); + const hexProfileCompatibility = profileCompatibility.toString(16).padStart(2, "0"); + const hexLevelIndication = levelIndication.toString(16).padStart(2, "0"); + return `avc1.${hexProfileIndication}${hexProfileCompatibility}${hexLevelIndication}`; + } else if (codec === "hevc") { + let profileSpace = 0; + let profileIdc = 1; + const compatibilityFlags = Array(32).fill(0); + compatibilityFlags[profileIdc] = 1; + const compatibilityHex = parseInt(compatibilityFlags.reverse().join(""), 2).toString(16).replace(/^0+/, ""); + let tier = "L"; + let level = 120; + if (width <= 1280 && height <= 720) { + level = 93; + } else if (width <= 1920 && height <= 1080) { + level = 120; + } else if (width <= 3840 && height <= 2160) { + level = 150; + } else { + tier = "H"; + level = 180; + } + const constraintFlags = "B0"; + const profilePrefix = profileSpace === 0 ? "" : String.fromCharCode(65 + profileSpace - 1); + return `hev1.${profilePrefix}${profileIdc}.${compatibilityHex}.${tier}${level}.${constraintFlags}`; + } else if (codec === "vp8") { + return "vp8"; + } else if (codec === "vp9") { + const profile = "00"; + let level; + if (width <= 854 && height <= 480) { + level = "21"; + } else if (width <= 1280 && height <= 720) { + level = "31"; + } else if (width <= 1920 && height <= 1080) { + level = "41"; + } else if (width <= 3840 && height <= 2160) { + level = "51"; + } else { + level = "61"; + } + const bitDepth = "08"; + return `vp09.${profile}.${level}.${bitDepth}`; + } else if (codec === "av1") { + const profile = 0; + let level; + if (width <= 854 && height <= 480) { + level = "01"; + } else if (width <= 1280 && height <= 720) { + level = "03"; + } else if (width <= 1920 && height <= 1080) { + level = "04"; + } else if (width <= 3840 && height <= 2160) { + level = "07"; + } else { + level = "09"; + } + const tier = "M"; + const bitDepth = "08"; + return `av01.${profile}.${level}${tier}.${bitDepth}`; + } + throw new Error(`Unhandled codec '${codec}'.`); +}; +var buildAudioCodecString = (codec, numberOfChannels, sampleRate) => { + if (codec === "aac") { + if (numberOfChannels >= 2 && sampleRate <= 24e3) { + return "mp4a.40.29"; + } + if (sampleRate <= 24e3) { + return "mp4a.40.5"; + } + return "mp4a.40.2"; + } else if (codec === "opus") { + return "opus"; + } else if (codec === "vorbis") { + return "vorbis"; + } + throw new Error(`Unhandled codec '${codec}'.`); +}; + +// src/subtitles.ts +var cueBlockHeaderRegex = /(?:(.+?)\n)?((?:\d{2}:)?\d{2}:\d{2}.\d{3})\s+-->\s+((?:\d{2}:)?\d{2}:\d{2}.\d{3})/g; +var preambleStartRegex = /^WEBVTT.*?\n{2}/; +var timestampRegex = /(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})/; +var inlineTimestampRegex = /<(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})>/g; +var textEncoder = new TextEncoder(); +var SubtitleEncoder = class { + #options; + #config = null; + #preambleBytes = null; + #preambleEmitted = false; + constructor(options) { + this.#options = options; + } + configure(config) { + if (config.codec !== "webvtt") { + throw new Error("Codec must be 'webvtt'."); + } + this.#config = config; + } + encode(text) { + if (!this.#config) { + throw new Error("Encoder not configured."); + } + text = text.replace("\r\n", "\n").replace("\r", "\n"); + cueBlockHeaderRegex.lastIndex = 0; + let match; + if (!this.#preambleBytes) { + if (!preambleStartRegex.test(text)) { + let error = new Error("WebVTT preamble incorrect."); + this.#options.error(error); + throw error; + } + match = cueBlockHeaderRegex.exec(text); + let preamble = text.slice(0, match?.index ?? text.length).trimEnd(); + if (!preamble) { + let error = new Error("No WebVTT preamble provided."); + this.#options.error(error); + throw error; + } + this.#preambleBytes = textEncoder.encode(preamble); + if (match) { + text = text.slice(match.index); + cueBlockHeaderRegex.lastIndex = 0; + } + } + while (match = cueBlockHeaderRegex.exec(text)) { + let notes = text.slice(0, match.index); + let cueIdentifier = match[1] || ""; + let matchEnd = match.index + match[0].length; + let bodyStart = text.indexOf("\n", matchEnd) + 1; + let cueSettings = text.slice(matchEnd, bodyStart).trim(); + let bodyEnd = text.indexOf("\n\n", matchEnd); + if (bodyEnd === -1) bodyEnd = text.length; + let startTime = this.#parseTimestamp(match[2]); + let endTime = this.#parseTimestamp(match[3]); + let duration = endTime - startTime; + let body = text.slice(bodyStart, bodyEnd); + let additions = `${cueSettings} +${cueIdentifier} +${notes}`; + inlineTimestampRegex.lastIndex = 0; + body = body.replace(inlineTimestampRegex, (match2) => { + let time = this.#parseTimestamp(match2.slice(1, -1)); + let offsetTime = time - startTime; + return `<${this.#formatTimestamp(offsetTime)}>`; + }); + text = text.slice(bodyEnd).trimStart(); + cueBlockHeaderRegex.lastIndex = 0; + let chunk = { + body: textEncoder.encode(body), + additions: additions.trim() === "" ? null : textEncoder.encode(additions), + timestamp: startTime * 1e3, + duration: duration * 1e3 + }; + let meta = {}; + if (!this.#preambleEmitted) { + meta.decoderConfig = { + description: this.#preambleBytes + }; + this.#preambleEmitted = true; + } + this.#options.output(chunk, meta); + } + } + #parseTimestamp(string) { + let match = timestampRegex.exec(string); + if (!match) throw new Error("Expected match."); + return 60 * 60 * 1e3 * Number(match[1] || "0") + 60 * 1e3 * Number(match[2]) + 1e3 * Number(match[3]) + Number(match[4]); + } + #formatTimestamp(timestamp) { + let hours = Math.floor(timestamp / (60 * 60 * 1e3)); + let minutes = Math.floor(timestamp % (60 * 60 * 1e3) / (60 * 1e3)); + let seconds = Math.floor(timestamp % (60 * 1e3) / 1e3); + let milliseconds = timestamp % 1e3; + return hours.toString().padStart(2, "0") + ":" + minutes.toString().padStart(2, "0") + ":" + seconds.toString().padStart(2, "0") + "." + milliseconds.toString().padStart(3, "0"); + } +}; + +// src/source.ts +var MediaSource = class { + constructor() { + this.connectedTrack = null; + this.closed = false; + } + ensureValidDigest() { + if (!this.connectedTrack) { + throw new Error("Cannot call digest without connecting the source to an output track."); + } + if (!this.connectedTrack.output.started) { + throw new Error("Cannot call digest before output has been started."); + } + if (this.connectedTrack.output.finalizing) { + throw new Error("Cannot call digest after output has started finalizing."); + } + if (this.closed) { + throw new Error("Cannot call digest after source has been closed."); + } + } + // TODO: These are should not be called from the outside lib + start() { + } + async flush() { + } + close() { + if (this.closed) { + throw new Error("Source already closed."); + } + if (!this.connectedTrack) { + throw new Error("Cannot call close without connecting the source to an output track."); + } + if (!this.connectedTrack.output.started) { + throw new Error("Cannot call close before output has been started."); + } + this.closed = true; + if (this.connectedTrack.output.finalizing) { + return; + } + this.connectedTrack.output.muxer.onTrackClose(this.connectedTrack); + } +}; +var VideoSource = class extends MediaSource { + constructor(codec) { + super(); + this.connectedTrack = null; + this.codec = codec; + } +}; +var EncodedVideoChunkSource = class extends VideoSource { + constructor(codec) { + super(codec); + } + // TODO: Ensure that the first chunk is a key frame (same for the audio case) + digest(chunk, meta) { + this.ensureValidDigest(); + this.connectedTrack?.output.muxer.addEncodedVideoChunk(this.connectedTrack, chunk, meta); + } +}; +var KEY_FRAME_INTERVAL = 5; +var VideoEncoderWrapper = class { + constructor(source, codecConfig) { + this.source = source; + this.codecConfig = codecConfig; + this.encoder = null; + this.lastMultipleOfKeyFrameInterval = -1; + } + // TODO: Ensure video frame size remains constant + digest(videoFrame) { + this.source.ensureValidDigest(); + this.ensureEncoder(videoFrame); + assert(this.encoder); + const multipleOfKeyFrameInterval = Math.floor(videoFrame.timestamp / 1e6 / KEY_FRAME_INTERVAL); + this.encoder.encode(videoFrame, { keyFrame: multipleOfKeyFrameInterval !== this.lastMultipleOfKeyFrameInterval }); + this.lastMultipleOfKeyFrameInterval = multipleOfKeyFrameInterval; + } + ensureEncoder(videoFrame) { + if (this.encoder) { + return; + } + this.encoder = new VideoEncoder({ + output: (chunk, meta) => this.source.connectedTrack?.output.muxer.addEncodedVideoChunk(this.source.connectedTrack, chunk, meta), + error: (error) => console.error(error) + // TODO + }); + this.encoder.configure({ + codec: buildVideoCodecString(this.codecConfig.codec, videoFrame.codedWidth, videoFrame.codedHeight), + width: videoFrame.codedWidth, + height: videoFrame.codedHeight, + bitrate: this.codecConfig.bitrate + }); + } + async flush() { + return this.encoder?.flush(); + } +}; +var VideoFrameSource = class extends VideoSource { + constructor(codecConfig) { + super(codecConfig.codec); + this.encoder = new VideoEncoderWrapper(this, codecConfig); + } + digest(videoFrame) { + this.encoder.digest(videoFrame); + } + flush() { + return this.encoder.flush(); + } +}; +var CanvasSource = class extends VideoSource { + constructor(canvas, codecConfig) { + super(codecConfig.codec); + this.canvas = canvas; + this.encoder = new VideoEncoderWrapper(this, codecConfig); + } + digest(timestamp, duration = 0) { + const frame = new VideoFrame(this.canvas, { + timestamp: Math.round(1e6 * timestamp), + duration: Math.round(1e6 * duration) + }); + this.encoder.digest(frame); + frame.close(); + } + flush() { + return this.encoder.flush(); + } +}; +var MediaStreamVideoTrackSource = class extends VideoSource { + constructor(track, codecConfig) { + super(codecConfig.codec); + this.track = track; + this.abortController = null; + this.encoder = new VideoEncoderWrapper(this, codecConfig); + } + start() { + this.abortController = new AbortController(); + const processor = new MediaStreamTrackProcessor({ track: this.track }); + const consumer = new WritableStream({ + write: (videoFrame) => { + this.encoder.digest(videoFrame); + videoFrame.close(); + } + }); + processor.readable.pipeTo(consumer, { + signal: this.abortController.signal + }).catch((err) => { + if (err instanceof DOMException && err.name === "AbortError") return; + console.error("Pipe error:", err); + }); + } + async flush() { + if (this.abortController) { + this.abortController.abort(); + this.abortController = null; + } + await this.encoder.flush(); + } +}; +var AudioSource = class extends MediaSource { + constructor(codec) { + super(); + this.connectedTrack = null; + this.codec = codec; + } +}; +var EncodedAudioChunkSource = class extends AudioSource { + constructor(codec) { + super(codec); + } + digest(chunk, meta) { + this.ensureValidDigest(); + 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.ensureValidDigest(); + this.ensureEncoder(audioData); + assert(this.encoder); + this.encoder.encode(audioData); + } + ensureEncoder(audioData) { + if (this.encoder) { + return; + } + this.encoder = new AudioEncoder({ + output: (chunk, meta) => this.source.connectedTrack?.output.muxer.addEncodedAudioChunk(this.source.connectedTrack, chunk, meta), + error: (error) => console.error(error) + // TODO + }); + this.encoder.configure({ + codec: buildAudioCodecString(this.codecConfig.codec, audioData.numberOfChannels, audioData.sampleRate), + numberOfChannels: audioData.numberOfChannels, + sampleRate: audioData.sampleRate, + bitrate: this.codecConfig.bitrate + }); + } + async flush() { + return this.encoder?.flush(); + } +}; +var AudioDataSource = class extends AudioSource { + constructor(codecConfig) { + super(codecConfig.codec); + this.encoder = new AudioEncoderWrapper(this, codecConfig); + } + digest(audioData) { + this.encoder.digest(audioData); + } + flush() { + return this.encoder.flush(); + } +}; +var AudioBufferSource = class extends AudioSource { + constructor(codecConfig) { + super(codecConfig.codec); + this.accumulatedFrameCount = 0; + this.encoder = new AudioEncoderWrapper(this, codecConfig); + } + digest(audioBuffer) { + const numberOfChannels = audioBuffer.numberOfChannels; + const sampleRate = audioBuffer.sampleRate; + const numberOfFrames = audioBuffer.length; + const data = new Float32Array(numberOfChannels * numberOfFrames); + for (let channel = 0; channel < numberOfChannels; channel++) { + const channelData = audioBuffer.getChannelData(channel); + data.set(channelData, channel * numberOfFrames); + } + const audioData = new AudioData({ + format: "f32-planar", + sampleRate, + numberOfFrames, + numberOfChannels, + timestamp: Math.round(1e6 * this.accumulatedFrameCount / sampleRate), + data + }); + this.encoder.digest(audioData); + audioData.close(); + this.accumulatedFrameCount += numberOfFrames; + } + flush() { + return this.encoder.flush(); + } +}; +var MediaStreamAudioTrackSource = class extends AudioSource { + constructor(track, codecConfig) { + super(codecConfig.codec); + this.track = track; + this.abortController = null; + this.encoder = new AudioEncoderWrapper(this, codecConfig); + } + start() { + this.abortController = new AbortController(); + const processor = new MediaStreamTrackProcessor({ track: this.track }); + const consumer = new WritableStream({ + write: (audioData) => { + this.encoder.digest(audioData); + audioData.close(); + } + }); + processor.readable.pipeTo(consumer, { + signal: this.abortController.signal + }).catch((err) => { + if (err instanceof DOMException && err.name === "AbortError") return; + console.error("Pipe error:", err); + }); + } + async flush() { + if (this.abortController) { + this.abortController.abort(); + this.abortController = null; + } + await this.encoder.flush(); + } +}; +var SubtitleSource = class extends MediaSource { + constructor(codec) { + super(); + this.connectedTrack = null; + this.codec = codec; + } +}; +var TextSubtitleSource = class extends SubtitleSource { + constructor(codec) { + super(codec); + this.encoder = new SubtitleEncoder({ + output: (chunk, metadata) => this.connectedTrack?.output.muxer.addEncodedSubtitleChunk(this.connectedTrack, chunk, metadata), + error: (error) => console.error(error) + // TODO + }); + this.encoder.configure({ codec }); + } + digest(text) { + this.ensureValidDigest(); + this.encoder.encode(text); + } +}; + // src/writer.ts var Writer = class { }; @@ -2718,9 +2848,10 @@ var FileSystemWritableFileStreamTargetWriter = class extends ChunkedStreamTarget }; // src/target.ts -var isTarget = Symbol("isTarget"); -isTarget; var Target = class { + constructor() { + this.output = null; + } }; var ArrayBufferTarget2 = class extends Target { constructor() { @@ -2795,6 +2926,7 @@ export { Output, StreamTarget, Target, + TextSubtitleSource, VideoFrameSource, WebMOutputFormat }; diff --git a/src/index.ts b/src/index.ts index 3c2c242..99a83a9 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 { EncodedVideoChunkSource, VideoFrameSource, CanvasSource, MediaStreamVideoTrackSource, EncodedAudioChunkSource, AudioDataSource, AudioBufferSource, MediaStreamAudioTrackSource } from './source'; +export { EncodedVideoChunkSource, VideoFrameSource, CanvasSource, MediaStreamVideoTrackSource, EncodedAudioChunkSource, AudioDataSource, AudioBufferSource, MediaStreamAudioTrackSource, TextSubtitleSource } from './source'; export { Target, ArrayBufferTarget, StreamTarget, FileSystemWritableFileStreamTarget } from './target'; diff --git a/src/matroska/matroska_muxer.ts b/src/matroska/matroska_muxer.ts index de2d143..7fb4489 100644 --- a/src/matroska/matroska_muxer.ts +++ b/src/matroska/matroska_muxer.ts @@ -1,13 +1,12 @@ import { assert, readBits, toUint8Array, writeBits } from '../misc'; import { Muxer } from '../muxer'; -import { Output, OutputAudioTrack, OutputTrack, OutputVideoTrack } from '../output'; +import { Output, OutputAudioTrack, OutputSubtitleTrack, OutputTrack, OutputVideoTrack } from '../output'; import { MkvOutputFormat, WebMOutputFormat } from '../output_format'; -import { AudioCodec, VideoCodec } from '../source'; +import { AudioCodec, SubtitleCodec, VideoCodec } from '../source'; +import { EncodedSubtitleChunk, EncodedSubtitleChunkMetadata, SubtitleDecoderConfig } from '../subtitles'; import { Writer } from '../writer'; import { EBML, EBMLElement, EBMLFloat32, EBMLFloat64, EBMLId, EBMLSignedInt, measureEBMLVarInt, measureSignedInt, measureUnsignedInt } from './ebml'; -const VIDEO_TRACK_TYPE = 1; -const AUDIO_TRACK_TYPE = 2; const MAX_CHUNK_LENGTH_MS = 2**15; const APP_NAME = 'https://github.com/Vanilagy/webm-muxer'; // TODO const SEGMENT_SIZE_BYTES = 6; @@ -59,12 +58,19 @@ type MatroskaTrackData = { sampleRate: number, decoderConfig: AudioDecoderConfig } +} | { + track: OutputSubtitleTrack, + type: 'subtitle', + info: { + decoderConfig: SubtitleDecoderConfig + } }); type MatroskaVideoTrackData = MatroskaTrackData & { type: 'video' }; type MatroskaAudioTrackData = MatroskaTrackData & { type: 'audio' }; +type MatroskaSubtitleTrackData = MatroskaTrackData & { type: 'subtitle' }; -const CODEC_STRING_MAP: Record = { +const CODEC_STRING_MAP: Record = { avc: 'V_MPEG4/ISO/AVC', hevc: 'V_MPEGH/ISO/HEVC', vp8: 'V_VP8', @@ -73,6 +79,13 @@ const CODEC_STRING_MAP: Record = { aac: 'A_AAC', opus: 'A_OPUS', vorbis: 'A_VORBIS', + webvtt: 'S_TEXT/WEBVTT' +}; + +const TRACK_TYPE_MAP: Record = { + video: 1, + audio: 2, + subtitle: 17 }; // TODO: Perhaps we can make this muxer always be streamable. We can do it similar to the MP4 muxer, where for each @@ -290,10 +303,16 @@ export class MatroskaMuxer extends Muxer { if (!['vp8', 'vp9', 'av1'].includes(track.source.codec)) { throw new Error(`WebM only supports VP8, VP9 and AV1 as video codecs. Switching to MKV removes this restriction.`); } - } else { + } else if (track.type === 'audio') { if (!['opus', 'vorbis'].includes(track.source.codec)) { throw new Error(`WebM only supports Opus and Vorbis as audio codecs. Switching to MKV removes this restriction.`); } + } else if (track.type === 'subtitle') { + if (track.source.codec !== 'webvtt') { + throw new Error(`WebM only supports WebVTT as subtitle codec. Switching to MKV removes this restriction.`); + } + } else { + throw new Error('WebM only supports video, audio and subtitle tracks. Switching to MKV removes this restriction.'); } } @@ -370,7 +389,7 @@ export class MatroskaMuxer extends Muxer { tracksElement.data.push({ id: EBMLId.TrackEntry, data: [ { id: EBMLId.TrackNumber, data: trackData.track.id }, { id: EBMLId.TrackUID, data: trackData.track.id }, - { id: EBMLId.TrackType, data: trackData.type === 'video' ? VIDEO_TRACK_TYPE : AUDIO_TRACK_TYPE }, // TODO Subtitle case + { id: EBMLId.TrackType, data: TRACK_TYPE_MAP[trackData.type] }, // TODO Subtitle case { id: EBMLId.CodecID, data: CODEC_STRING_MAP[trackData.track.source.codec] }, (trackData.info.decoderConfig.description ? { id: EBMLId.CodecPrivate, data: toUint8Array(trackData.info.decoderConfig.description) } : null), ...(trackData.type === 'video' ? [ @@ -419,18 +438,6 @@ export class MatroskaMuxer extends Muxer { ] : []) ] }) } - - /* - if (this.#options.subtitles) { - tracksElement.data.push({ id: EBMLId.TrackEntry, data: [ - { id: EBMLId.TrackNumber, data: SUBTITLE_TRACK_NUMBER }, - { id: EBMLId.TrackUID, data: SUBTITLE_TRACK_NUMBER }, - { id: EBMLId.TrackType, data: SUBTITLE_TRACK_TYPE }, - { id: EBMLId.CodecID, data: this.#options.subtitles.codec }, - this.#subtitleCodecPrivate - ] }); - } - */ } #createSegment() { @@ -525,35 +532,75 @@ export class MatroskaMuxer extends Muxer { return newTrackData; } + + #getSubtitleTrackData(track: OutputSubtitleTrack, meta?: EncodedSubtitleChunkMetadata) { + const existingTrackData = this.#trackDatas.find(x => x.track === track); + if (existingTrackData) { + return existingTrackData as MatroskaAudioTrackData; + } + + // TODO Make proper errors for these + assert(meta); + assert(meta.decoderConfig); + + const newTrackData: MatroskaSubtitleTrackData = { + track, + type: 'subtitle', + info: { + decoderConfig: meta.decoderConfig + }, + chunkQueue: [], + firstTimestamp: null, + lastKeyFrameTimestamp: null, + lastWrittenMsTimestamp: null + }; + + this.#trackDatas.push(newTrackData); + this.#trackDatas.sort((a, b) => a.track.id - b.track.id); + + return newTrackData; + } addEncodedVideoChunk(track: OutputVideoTrack, chunk: EncodedVideoChunk, meta?: EncodedVideoChunkMetadata) { const trackData = this.#getVideoTrackData(track, meta); - let videoChunk = this.#createInternalChunk(trackData, chunk); + let data = new Uint8Array(chunk.byteLength); + chunk.copyTo(data); + + let videoChunk = this.#createInternalChunk(trackData, data, chunk.timestamp, chunk.duration ?? 0, chunk.type); if (track.source.codec === 'vp9') this.#fixVP9ColorSpace(trackData, videoChunk); trackData.chunkQueue.push(videoChunk); this.#interleaveChunks(); - - //this.#writeSubtitleChunks(); - this.#writer.flush(); } addEncodedAudioChunk(track: OutputAudioTrack, chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata) { const trackData = this.#getAudioTrackData(track, meta); - let audioChunk = this.#createInternalChunk(trackData, chunk); + let data = new Uint8Array(chunk.byteLength); + chunk.copyTo(data); + + let audioChunk = this.#createInternalChunk(trackData, data, chunk.timestamp, chunk.duration ?? 0, chunk.type); trackData.chunkQueue.push(audioChunk); this.#interleaveChunks(); - - //this.#writeSubtitleChunks(); - this.#writer.flush(); + } + + addEncodedSubtitleChunk(track: OutputSubtitleTrack, chunk: EncodedSubtitleChunk, meta?: EncodedSubtitleChunkMetadata) { + const trackData = this.#getSubtitleTrackData(track, meta); + + let subtitleChunk = this.#createInternalChunk(trackData, chunk.body, chunk.timestamp, chunk.duration, 'key', chunk.additions); + + trackData.chunkQueue.push(subtitleChunk); + this.#interleaveChunks(); } #interleaveChunks() { - if (this.#trackDatas.length < this.output.tracks.length) { - return; // We haven't seen a sample from each track yet + let openTrackCount = 0; + for (const trackData of this.#trackDatas) if (!trackData.track.source.closed) openTrackCount++; + + if (this.#trackDatas.length < openTrackCount) { + return; // We haven't seen a sample from every open track yet } outer: @@ -562,11 +609,11 @@ export class MatroskaMuxer extends Muxer { let minTimestamp = Infinity; for (let trackData of this.#trackDatas) { - if (trackData.chunkQueue.length === 0) { + if (trackData.chunkQueue.length === 0 && !trackData.track.source.closed) { break outer; } - if (trackData.chunkQueue[0]!.timestamp < minTimestamp) { + if (trackData.chunkQueue.length > 0 && trackData.chunkQueue[0]!.timestamp < minTimestamp) { trackWithMinTimestamp = trackData; minTimestamp = trackData.chunkQueue[0]!.timestamp; } @@ -579,6 +626,8 @@ export class MatroskaMuxer extends Muxer { let chunk = trackWithMinTimestamp.chunkQueue.shift()!; this.#writeBlock(trackWithMinTimestamp, chunk); } + + this.#writer.flush(); } /** Due to [a bug in Chromium](https://bugs.chromium.org/p/chromium/issues/detail?id=1377842), VP9 streams often @@ -617,99 +666,30 @@ export class MatroskaMuxer extends Muxer { writeBits(chunk.data, i+0, i+3, colorSpaceID); } - /* - addSubtitleChunk(chunk: EncodedSubtitleChunk, meta: EncodedSubtitleChunkMetadata, timestamp?: number) { - if (typeof chunk !== 'object' || !chunk) { - throw new TypeError("addSubtitleChunk's first argument (chunk) must be an object."); - } else { - // We can't simply do an instanceof check, so let's check the structure itself: - if (!(chunk.body instanceof Uint8Array)) { - throw new TypeError('body must be an instance of Uint8Array.'); - } - if (!Number.isFinite(chunk.timestamp) || chunk.timestamp < 0) { - throw new TypeError('timestamp must be a non-negative real number.'); - } - if (!Number.isFinite(chunk.duration) || chunk.duration < 0) { - throw new TypeError('duration must be a non-negative real number.'); - } - if (chunk.additions && !(chunk.additions instanceof Uint8Array)) { - throw new TypeError('additions, when present, must be an instance of Uint8Array.'); - } - } - - if (typeof meta !== 'object') { - throw new TypeError("addSubtitleChunk's second argument (meta) must be an object."); - } - - this.#ensureNotFinalized(); - if (!this.#options.subtitles) throw new Error('No subtitle track declared.'); - - // Write possible subtitle decoder metadata to the file - if (meta?.decoderConfig) { - if (this.#options.streaming) { - this.#subtitleCodecPrivate = this.#createCodecPrivateElement(meta.decoderConfig.description); - } else { - this.#writeCodecPrivate(this.#subtitleCodecPrivate, meta.decoderConfig.description); - } - } - - let subtitleChunk = this.#createInternalChunk( - chunk.body, - 'key', - timestamp ?? chunk.timestamp, - SUBTITLE_TRACK_NUMBER, - chunk.duration, - chunk.additions - ); - - this.#lastSubtitleTimestamp = subtitleChunk.timestamp; - this.#subtitleChunkQueue.push(subtitleChunk); - - this.#writeSubtitleChunks(); - this.#maybeFlushStreamingTargetWriter(); - } - - #writeSubtitleChunks() { - // Writing subtitle chunks is different from video and audio: A subtitle chunk will be written if it's - // guaranteed that no more media chunks will be written before it, to ensure monotonicity. However, media chunks - // will NOT wait for subtitle chunks to arrive, as they may never arrive, so that's how non-monotonicity can - // arrive. But it should be fine, since it's all still in one cluster. - - let lastWrittenMediaTimestamp = Math.min( - this.#options.video ? this.#lastVideoTimestamp : Infinity, - this.#options.audio ? this.#lastAudioTimestamp : Infinity - ); - - let queue = this.#subtitleChunkQueue; - while (queue.length > 0 && queue[0].timestamp <= lastWrittenMediaTimestamp) { - this.#writeBlock(queue.shift(), !this.#options.video && !this.#options.audio); - } - } - */ - /** Converts a read-only external chunk into an internal one for easier use. */ #createInternalChunk( trackData: MatroskaTrackData, - chunk: EncodedVideoChunk | EncodedAudioChunk + data: Uint8Array, + timestamp: number, + duration: number, + type: 'key' | 'delta', + additions: Uint8Array | null = null ) { - let adjustedTimestamp = this.#validateTimestamp(trackData, chunk); - - let data = new Uint8Array(chunk.byteLength); - chunk.copyTo(data); + let adjustedTimestamp = this.#validateTimestamp(trackData, timestamp, type === 'key'); let internalChunk: InternalMediaChunk = { data, - type: chunk.type, + type, timestamp: adjustedTimestamp, - duration: (chunk.duration ?? 0) / 1e6, - additions: null + duration: duration / 1e6, + additions }; return internalChunk; } - #validateTimestamp(trackData: MatroskaTrackData, chunk: EncodedVideoChunk | EncodedAudioChunk) { - let timestampInSeconds = chunk.timestamp / 1e6; + #validateTimestamp(trackData: MatroskaTrackData, timestamp: number, isKeyFrame: boolean) { + let timestampInSeconds = timestamp / 1e6; if (timestampInSeconds < 0) { throw new Error(`Timestamps must be non-negative (got ${timestampInSeconds}s).`); @@ -725,7 +705,7 @@ export class MatroskaMuxer extends Muxer { throw new Error(`Timestamp cannot be before last key frame's timestamp (got ${timestampInSeconds}s, last key frame at ${trackData.lastKeyFrameTimestamp}s).`); } - if (chunk.type === 'key') { + if (isKeyFrame) { trackData.lastKeyFrameTimestamp = timestampInSeconds; } @@ -860,7 +840,6 @@ export class MatroskaMuxer extends Muxer { assert(this.#cues); // Add a CuePoint to the Cues element for better seeking - // TODO: Should this include subtitle tracks? (this.#cues.data as EBML[]).push({ id: EBMLId.CuePoint, data: [ { id: EBMLId.CueTime, data: this.#currentClusterMsTimestamp! }, // We only write out cues for tracks that have at least one chunk in this cluster @@ -873,6 +852,11 @@ export class MatroskaMuxer extends Muxer { ] }); } + override onTrackClose() { + // Since a track is now closed, we may be able to write out chunks that were previously waiting + this.#interleaveChunks(); + } + /** Finalizes the file, making it ready for use. Must be called after all media chunks have been added. */ finalize() { // Flush any remaining queued chunks to the file @@ -886,14 +870,6 @@ export class MatroskaMuxer extends Muxer { this.#finalizeCurrentCluster(); } - /* - while (this.#videoChunkQueue.length > 0) this.#writeBlock(this.#videoChunkQueue.shift(), true); - while (this.#audioChunkQueue.length > 0) this.#writeBlock(this.#audioChunkQueue.shift(), true); - while (this.#subtitleChunkQueue.length > 0 && this.#subtitleChunkQueue[0].timestamp <= this.#duration) { - this.#writeBlock(this.#subtitleChunkQueue.shift(), false); - } - */ - assert(this.#cues); this.writeEBML(this.#cues); diff --git a/src/muxer.ts b/src/muxer.ts index 0b4b25d..1bcf9d0 100644 --- a/src/muxer.ts +++ b/src/muxer.ts @@ -1,4 +1,5 @@ -import { Output, OutputAudioTrack, OutputTrack, OutputVideoTrack } from "./output"; +import { Output, OutputAudioTrack, OutputSubtitleTrack, OutputTrack, OutputVideoTrack } from "./output"; +import { EncodedSubtitleChunk, EncodedSubtitleChunkMetadata } from "./subtitles"; export abstract class Muxer { output: Output; @@ -10,7 +11,9 @@ export abstract class Muxer { abstract start(): void; abstract addEncodedVideoChunk(track: OutputVideoTrack, chunk: EncodedVideoChunk, meta?: EncodedVideoChunkMetadata): void; abstract addEncodedAudioChunk(track: OutputAudioTrack, chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata): void; + abstract addEncodedSubtitleChunk(track: OutputSubtitleTrack, chunk: EncodedSubtitleChunk, meta?: EncodedSubtitleChunkMetadata): void; abstract finalize(): void; beforeTrackAdd(track: OutputTrack) {} + onTrackClose(track: OutputTrack) {} } \ No newline at end of file diff --git a/src/output.ts b/src/output.ts index c5b4e38..475215b 100644 --- a/src/output.ts +++ b/src/output.ts @@ -1,7 +1,7 @@ import { TransformationMatrix } from "./misc"; import { Muxer } from "./muxer"; import { OutputFormat } from "./output_format"; -import { AudioSource, VideoSource } from "./source"; +import { AudioSource, MediaSource, SubtitleSource, VideoSource } from "./source"; import { Target } from "./target"; import { Writer } from "./writer"; @@ -21,16 +21,22 @@ export type OutputTrack = { type: 'audio', source: AudioSource, metadata: AudioTrackMetadata +} | { + type: 'subtitle', + source: SubtitleSource, + metadata: SubtitleTrackMetadata }); export type OutputVideoTrack = OutputTrack & { type: 'video' }; export type OutputAudioTrack = OutputTrack & { type: 'audio' }; +export type OutputSubtitleTrack = OutputTrack & { type: 'subtitle' }; type VideoTrackMetadata = { rotation?: 0 | 90 | 180 | 270 | TransformationMatrix, frameRate?: number }; type AudioTrackMetadata = {}; +type SubtitleTrackMetadata = {}; export class Output { muxer: Muxer; @@ -40,11 +46,28 @@ export class Output { finalizing = false; constructor(options: OutputOptions) { + if (options.target.output) { + throw new Error('Target is already used for another output.'); + } + options.target.output = this; + this.writer = options.target.createWriter(); this.muxer = options.format.createMuxer(this); } - addTrack(source: VideoSource | AudioSource, metadata: VideoTrackMetadata | AudioTrackMetadata = {}) { + addVideoTrack(source: VideoSource, metadata: VideoTrackMetadata = {}) { + this.addTrack('video', source, metadata); + } + + addAudioTrack(source: AudioSource, metadata: AudioTrackMetadata = {}) { + this.addTrack('audio', source, metadata); + } + + addSubtitleTrack(source: SubtitleSource, metadata: SubtitleTrackMetadata = {}) { + this.addTrack('subtitle', source, metadata); + } + + private addTrack(type: OutputTrack['type'], source: MediaSource, metadata: object) { if (this.started) { throw new Error('Cannot add track after output has started.'); } @@ -55,8 +78,8 @@ export class Output { const track = { id: this.tracks.length + 1, output: this, - type: source instanceof VideoSource ? 'video' : 'audio', - source, + type, + source: source as any, metadata } as OutputTrack; diff --git a/src/source.ts b/src/source.ts index bced3c0..c7dd732 100644 --- a/src/source.ts +++ b/src/source.ts @@ -1,17 +1,15 @@ import { buildAudioCodecString, buildVideoCodecString } from "./codec"; import { assert, TransformationMatrix } from "./misc"; -import { OutputAudioTrack, OutputTrack, OutputVideoTrack } from "./output"; +import { OutputAudioTrack, OutputSubtitleTrack, OutputTrack, OutputVideoTrack } from "./output"; +import { SubtitleEncoder } from "./subtitles"; export type VideoCodec = 'avc' | 'hevc' | 'vp8' | 'vp9' | 'av1'; export type AudioCodec = 'aac' | 'opus' | 'vorbis'; // TODO add the rest +export type SubtitleCodec = 'webvtt'; -export abstract class VideoSource { - connectedTrack: OutputVideoTrack | null = null; - codec: VideoCodec; - - constructor(codec: VideoCodec) { - this.codec = codec; - } +export abstract class MediaSource { + connectedTrack: OutputTrack | null = null; + closed = false; ensureValidDigest() { if (!this.connectedTrack) { @@ -25,39 +23,49 @@ export abstract class VideoSource { if (this.connectedTrack.output.finalizing) { throw new Error('Cannot call digest after output has started finalizing.'); } - } - start() {} - async flush() {} -} - -export abstract class AudioSource { - connectedTrack: OutputAudioTrack | null = null; - codec: AudioCodec; - - constructor(codec: AudioCodec) { - this.codec = codec; - } - - ensureNotFinalizing() { - if (this.connectedTrack?.output.finalizing) { - throw new Error('Cannot call digest after output has started finalizing.'); + if (this.closed) { + throw new Error('Cannot call digest after source has been closed.'); } } + // TODO: These are should not be called from the outside lib start() {} async flush() {} + + close() { + if (this.closed) { + throw new Error('Source already closed.'); + } + + if (!this.connectedTrack) { + throw new Error('Cannot call close without connecting the source to an output track.'); + } + + if (!this.connectedTrack.output.started) { + throw new Error('Cannot call close before output has been started.'); + } + + this.closed = true; + + if (this.connectedTrack.output.finalizing) { + return; + } + + this.connectedTrack.output.muxer.onTrackClose(this.connectedTrack); + } } -export type VideoCodecConfig = { - codec: 'avc' | 'hevc' | 'vp8' | 'vp9' | 'av1', - bitrate: number -}; +export abstract class VideoSource extends MediaSource { + override connectedTrack: OutputVideoTrack | null = null; + codec: VideoCodec; -export type AudioCodecConfig = { - codec: 'aac' | 'opus' | 'vorbis', - bitrate: number -}; + constructor(codec: VideoCodec) { + super(); + + this.codec = codec; + } +} export class EncodedVideoChunkSource extends VideoSource { constructor(codec: VideoCodec) { @@ -74,6 +82,11 @@ export class EncodedVideoChunkSource extends VideoSource { const KEY_FRAME_INTERVAL = 5; +type VideoCodecConfig = { + codec: 'avc' | 'hevc' | 'vp8' | 'vp9' | 'av1', + bitrate: number +}; + class VideoEncoderWrapper { private encoder: VideoEncoder | null = null; private lastMultipleOfKeyFrameInterval = -1; @@ -200,17 +213,33 @@ export class MediaStreamVideoTrackSource extends VideoSource { } } +export abstract class AudioSource extends MediaSource { + override connectedTrack: OutputAudioTrack | null = null; + codec: AudioCodec; + + constructor(codec: AudioCodec) { + super(); + + this.codec = codec; + } +} + export class EncodedAudioChunkSource extends AudioSource { constructor(codec: AudioCodec) { super(codec); } digest(chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata) { - this.ensureNotFinalizing(); + this.ensureValidDigest(); this.connectedTrack?.output.muxer.addEncodedAudioChunk(this.connectedTrack, chunk, meta); } } +type AudioCodecConfig = { + codec: 'aac' | 'opus' | 'vorbis', + bitrate: number +}; + class AudioEncoderWrapper { private encoder: AudioEncoder | null = null; @@ -218,7 +247,7 @@ class AudioEncoderWrapper { // TODO: Ensure audio parameters remain constant digest(audioData: AudioData) { - this.source.ensureNotFinalizing(); + this.source.ensureValidDigest(); this.ensureEncoder(audioData); assert(this.encoder); @@ -345,4 +374,35 @@ export class MediaStreamAudioTrackSource extends AudioSource { await this.encoder.flush(); } +} + +export abstract class SubtitleSource extends MediaSource { + override connectedTrack: OutputSubtitleTrack | null = null; + codec: SubtitleCodec; + + constructor(codec: SubtitleCodec) { + super(); + + this.codec = codec; + } +} + +export class TextSubtitleSource extends SubtitleSource { + private encoder: SubtitleEncoder; + + constructor(codec: SubtitleCodec) { + super(codec); + + this.encoder = new SubtitleEncoder({ + output: (chunk, metadata) => this.connectedTrack?.output.muxer.addEncodedSubtitleChunk(this.connectedTrack, chunk, metadata), + error: (error) => console.error(error) // TODO + }); + + this.encoder.configure({ codec }); + } + + digest(text: string) { + this.ensureValidDigest(); + this.encoder.encode(text); + } } \ No newline at end of file diff --git a/src/subtitles.ts b/src/subtitles.ts new file mode 100644 index 0000000..1007aaa --- /dev/null +++ b/src/subtitles.ts @@ -0,0 +1,151 @@ +export type EncodedSubtitleChunk = { + body: Uint8Array, + additions: Uint8Array | null, + timestamp: number, + duration: number +}; + +export type SubtitleDecoderConfig = { + description: Uint8Array +}; + +export type EncodedSubtitleChunkMetadata = { + decoderConfig?: SubtitleDecoderConfig +}; + +interface SubtitleEncoderOptions { + output: (chunk: EncodedSubtitleChunk, metadata: EncodedSubtitleChunkMetadata) => unknown, + error: (error: Error) => unknown +} + +interface SubtitleEncoderConfig { + codec: 'webvtt' +} + +const cueBlockHeaderRegex = /(?:(.+?)\n)?((?:\d{2}:)?\d{2}:\d{2}.\d{3})\s+-->\s+((?:\d{2}:)?\d{2}:\d{2}.\d{3})/g; +const preambleStartRegex = /^WEBVTT.*?\n{2}/; +const timestampRegex = /(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})/; +const inlineTimestampRegex = /<(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})>/g; +const textEncoder = new TextEncoder(); + +export class SubtitleEncoder { + #options: SubtitleEncoderOptions; + #config: SubtitleEncoderConfig | null = null; + #preambleBytes: Uint8Array | null = null; + #preambleEmitted = false; + + constructor(options: SubtitleEncoderOptions) { + this.#options = options; + } + + configure(config: SubtitleEncoderConfig) { + if (config.codec !== 'webvtt') { + throw new Error("Codec must be 'webvtt'."); + } + + this.#config = config; + } + + encode(text: string) { + if (!this.#config) { + throw new Error('Encoder not configured.'); + } + + text = text.replace('\r\n', '\n').replace('\r', '\n'); + + cueBlockHeaderRegex.lastIndex = 0; + let match: RegExpMatchArray | null; + + if (!this.#preambleBytes) { + if (!preambleStartRegex.test(text)) { + let error = new Error('WebVTT preamble incorrect.'); + this.#options.error(error); + throw error; + } + + match = cueBlockHeaderRegex.exec(text); + let preamble = text.slice(0, match?.index ?? text.length).trimEnd(); + + if (!preamble) { + let error = new Error('No WebVTT preamble provided.'); + this.#options.error(error); + throw error; + } + + this.#preambleBytes = textEncoder.encode(preamble); + + if (match) { + text = text.slice(match.index); + cueBlockHeaderRegex.lastIndex = 0; + } + } + + while (match = cueBlockHeaderRegex.exec(text)) { + let notes = text.slice(0, match.index); + let cueIdentifier = match[1] || ''; + let matchEnd = match.index! + match[0].length; + let bodyStart = text.indexOf('\n', matchEnd) + 1; + let cueSettings = text.slice(matchEnd, bodyStart).trim(); + let bodyEnd = text.indexOf('\n\n', matchEnd); + if (bodyEnd === -1) bodyEnd = text.length; + + let startTime = this.#parseTimestamp(match[2]!); + let endTime = this.#parseTimestamp(match[3]!); + let duration = endTime - startTime; + + let body = text.slice(bodyStart, bodyEnd); + let additions = `${cueSettings}\n${cueIdentifier}\n${notes}`; + + // Replace in-body timestamps so that they're relative to the cue start time + inlineTimestampRegex.lastIndex = 0; + body = body.replace(inlineTimestampRegex, (match) => { + let time = this.#parseTimestamp(match.slice(1, -1)); + let offsetTime = time - startTime; + + return `<${this.#formatTimestamp(offsetTime)}>`; + }); + + text = text.slice(bodyEnd).trimStart(); + cueBlockHeaderRegex.lastIndex = 0; + + let chunk: EncodedSubtitleChunk = { + body: textEncoder.encode(body), + additions: additions.trim() === '' ? null : textEncoder.encode(additions), + timestamp: startTime * 1000, + duration: duration * 1000 + }; + + let meta: EncodedSubtitleChunkMetadata = {}; + if (!this.#preambleEmitted) { + meta.decoderConfig = { + description: this.#preambleBytes + }; + this.#preambleEmitted = true; + } + + this.#options.output(chunk, meta); + } + } + + #parseTimestamp(string: string) { + let match = timestampRegex.exec(string); + if (!match) throw new Error('Expected match.'); + + return 60 * 60 * 1000 * Number(match[1] || '0') + + 60 * 1000 * Number(match[2]) + + 1000 * Number(match[3]) + + Number(match[4]); + } + + #formatTimestamp(timestamp: number) { + let hours = Math.floor(timestamp / (60 * 60 * 1000)); + let minutes = Math.floor((timestamp % (60 * 60 * 1000)) / (60 * 1000)); + let seconds = Math.floor((timestamp % (60 * 1000)) / 1000); + let milliseconds = timestamp % 1000; + + return hours.toString().padStart(2, '0') + ':' + + minutes.toString().padStart(2, '0') + ':' + + seconds.toString().padStart(2, '0') + '.' + + milliseconds.toString().padStart(3, '0'); + } +} \ No newline at end of file diff --git a/src/target.ts b/src/target.ts index c1ad771..54175c3 100644 --- a/src/target.ts +++ b/src/target.ts @@ -1,9 +1,8 @@ +import { Output } from "./output"; import { ArrayBufferTargetWriter, ChunkedStreamTargetWriter, FileSystemWritableFileStreamTargetWriter, StreamTargetWriter, Writer } from "./writer"; -const isTarget = Symbol('isTarget'); export abstract class Target { - // If we didn't add this symbol, then {} would be assignable to Target - [isTarget]!: true; + output: Output | null = null; abstract createWriter(): Writer; }