diff --git a/dev/demux.html b/dev/demux.html new file mode 100644 index 0000000..d7a4f71 --- /dev/null +++ b/dev/demux.html @@ -0,0 +1,102 @@ + + + \ No newline at end of file diff --git a/dist/metamuxer.js b/dist/metamuxer.js index 4ae4cce..c523ad2 100644 --- a/dist/metamuxer.js +++ b/dist/metamuxer.js @@ -21,14 +21,24 @@ var Metamuxer = (() => { // src/index.ts var src_exports = {}; __export(src_exports, { + ALL_FORMATS: () => ALL_FORMATS, AUDIO_CODECS: () => AUDIO_CODECS, + ArrayBufferSource: () => ArrayBufferSource, ArrayBufferTarget: () => ArrayBufferTarget, AudioBufferSource: () => AudioBufferSource, AudioDataSource: () => AudioDataSource, AudioSource: () => AudioSource, + BlobSource: () => BlobSource, CanvasSource: () => CanvasSource, EncodedAudioChunkSource: () => EncodedAudioChunkSource, + EncodedVideoChunkDrain: () => EncodedVideoChunkDrain, EncodedVideoChunkSource: () => EncodedVideoChunkSource, + ISOBMFF: () => ISOBMFF, + Input: () => Input, + MATROSKA: () => MATROSKA, + MKV: () => MKV, + MOV: () => MOV, + MP4: () => MP4, MediaSource: () => MediaSource, MediaStreamAudioTrackSource: () => MediaStreamAudioTrackSource, MediaStreamVideoTrackSource: () => MediaStreamVideoTrackSource, @@ -37,13 +47,16 @@ var Metamuxer = (() => { Output: () => Output, OutputFormat: () => OutputFormat, SUBTITLE_CODECS: () => SUBTITLE_CODECS, + Source: () => Source, StreamTarget: () => StreamTarget, SubtitleSource: () => SubtitleSource, Target: () => Target, TextSubtitleSource: () => TextSubtitleSource, VIDEO_CODECS: () => VIDEO_CODECS, + VideoFrameDrain: () => VideoFrameDrain, VideoFrameSource: () => VideoFrameSource, VideoSource: () => VideoSource, + WEBM: () => WEBM, WebMOutputFormat: () => WebMOutputFormat }); @@ -89,6 +102,9 @@ var Metamuxer = (() => { } }; var textEncoder = new TextEncoder(); + var invertObject = (object) => { + return Object.fromEntries(Object.entries(object).map(([key, value]) => [value, key])); + }; var COLOR_PRIMARIES_MAP = { bt709: 1, // ITU-R BT.709 @@ -97,6 +113,7 @@ var Metamuxer = (() => { smpte170m: 6 // ITU-R BT.601 525 - SMPTE 170M }; + var COLOR_PRIMARIES_MAP_INVERSE = invertObject(COLOR_PRIMARIES_MAP); var TRANSFER_CHARACTERISTICS_MAP = { "bt709": 1, // ITU-R BT.709 @@ -105,6 +122,7 @@ var Metamuxer = (() => { "iec61966-2-1": 13 // IEC 61966-2-1 }; + var TRANSFER_CHARACTERISTICS_MAP_INVERSE = invertObject(TRANSFER_CHARACTERISTICS_MAP); var MATRIX_COEFFICIENTS_MAP = { rgb: 0, // Identity @@ -115,6 +133,7 @@ var Metamuxer = (() => { smpte170m: 6 // SMPTE 170M }; + var MATRIX_COEFFICIENTS_MAP_INVERSE = invertObject(MATRIX_COEFFICIENTS_MAP); var colorSpaceIsComplete = (colorSpace) => { return !!colorSpace && !!colorSpace.primaries && !!colorSpace.transfer && !!colorSpace.matrix && colorSpace.fullRange !== void 0; }; @@ -122,9 +141,7 @@ var Metamuxer = (() => { return x instanceof ArrayBuffer || typeof SharedArrayBuffer !== "undefined" && x instanceof SharedArrayBuffer || ArrayBuffer.isView(x) && !(x instanceof DataView); }; var AsyncMutex = class { - constructor() { - this.currentPromise = Promise.resolve(); - } + currentPromise = Promise.resolve(); async acquire() { let resolver; const nextPromise = new Promise((resolve) => { @@ -136,15 +153,87 @@ var Metamuxer = (() => { return resolver; } }; + var rotationMatrix = (rotationInDegrees) => { + const theta = rotationInDegrees * (Math.PI / 180); + const cosTheta = Math.cos(theta); + const sinTheta = Math.sin(theta); + return [ + cosTheta, + sinTheta, + 0, + -sinTheta, + cosTheta, + 0, + 0, + 0, + 1 + ]; + }; + var IDENTITY_MATRIX = rotationMatrix(0); + var bytesToHexString = (bytes2) => { + return [...bytes2].map((x) => x.toString(16).padStart(2, "0")).join(""); + }; + var reverseBitsU32 = (x) => { + x = x >> 1 & 1431655765 | (x & 1431655765) << 1; + x = x >> 2 & 858993459 | (x & 858993459) << 2; + x = x >> 4 & 252645135 | (x & 252645135) << 4; + x = x >> 8 & 16711935 | (x & 16711935) << 8; + x = x >> 16 & 65535 | (x & 65535) << 16; + return x >>> 0; + }; + var binarySearchExact = (arr, key, valueGetter) => { + let low = 0; + let high = arr.length - 1; + let res = -1; + while (low <= high) { + const mid = low + high >> 1; + const midVal = valueGetter(arr[mid]); + if (midVal === key) { + res = mid; + high = mid - 1; + } else if (midVal < key) { + low = mid + 1; + } else { + high = mid - 1; + } + } + return res; + }; + var binarySearchLessOrEqual = (arr, key, valueGetter) => { + let ans = -1; + let low = 0; + let high = arr.length - 1; + while (low <= high) { + const mid = low + (high - low + 1) / 2 | 0; + const midVal = valueGetter(arr[mid]); + if (midVal <= key) { + ans = mid; + low = mid + 1; + } else { + high = mid - 1; + } + } + return ans; + }; + var promiseWithResolvers = () => { + let resolve; + let reject; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; + }; // src/subtitles.ts var cueBlockHeaderRegex = /(?:(.+?)\n)?((?:\d{2}:)?\d{2}:\d{2}.\d{3})\s+-->\s+((?:\d{2}:)?\d{2}:\d{2}.\d{3})/g; var preambleStartRegex = /^WEBVTT(.|\n)*?\n{2}/; var inlineTimestampRegex = /<(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})>/g; var SubtitleParser = class { + options; + preambleText = null; + preambleEmitted = false; constructor(options) { - this.preambleText = null; - this.preambleEmitted = false; this.options = options; } parse(text) { @@ -221,14 +310,14 @@ var Metamuxer = (() => { var IsobmffBoxWriter = class { constructor(writer) { this.writer = writer; - this.helper = new Uint8Array(8); - this.helperView = new DataView(this.helper.buffer); - /** - * Stores the position from the start of the file to where boxes elements have been written. This is used to - * rewrite/edit elements that were already added before, and to measure sizes of things. - */ - this.offsets = /* @__PURE__ */ new WeakMap(); } + helper = new Uint8Array(8); + helperView = new DataView(this.helper.buffer); + /** + * Stores the position from the start of the file to where boxes elements have been written. This is used to + * rewrite/edit elements that were already added before, and to measure sizes of things. + */ + offsets = /* @__PURE__ */ new WeakMap(); writeU32(value) { this.helperView.setUint32(0, value, false); this.writer.write(this.helper.subarray(0, 4)); @@ -368,23 +457,6 @@ var Metamuxer = (() => { } return result; }; - var rotationMatrix = (rotationInDegrees) => { - const theta = rotationInDegrees * (Math.PI / 180); - const cosTheta = Math.cos(theta); - const sinTheta = Math.sin(theta); - return [ - cosTheta, - sinTheta, - 0, - -sinTheta, - cosTheta, - 0, - 0, - 0, - 1 - ]; - }; - var IDENTITY_MATRIX = rotationMatrix(0); var matrixToBytes = (matrix) => { return [ fixed_16_16(matrix[0]), @@ -1120,9 +1192,9 @@ var Metamuxer = (() => { // src/muxer.ts var Muxer = class { + output; + mutex = new AsyncMutex(); constructor(output) { - this.mutex = new AsyncMutex(); - this.trackTimestampInfo = /* @__PURE__ */ new WeakMap(); this.output = output; } // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -1131,6 +1203,7 @@ var Metamuxer = (() => { // eslint-disable-next-line @typescript-eslint/no-unused-vars onTrackClose(track) { } + trackTimestampInfo = /* @__PURE__ */ new WeakMap(); validateAndNormalizeTimestamp(track, rawTimestampInUs, isKeyFrame) { let timestampInSeconds = rawTimestampInUs / 1e6; let timestampInfo = this.trackTimestampInfo.get(track); @@ -1174,20 +1247,19 @@ var Metamuxer = (() => { // src/writer.ts var Writer = class { - constructor() { - /** Setting this to true will cause the writer to ensure data is written in a strictly monotonic, streamable way. */ - this.ensureMonotonicity = false; - } + /** Setting this to true will cause the writer to ensure data is written in a strictly monotonic, streamable way. */ + ensureMonotonicity = false; start() { } }; var ArrayBufferTargetWriter = class extends Writer { + pos = 0; + target; + buffer = new ArrayBuffer(2 ** 16); + bytes = new Uint8Array(this.buffer); + maxPos = 0; constructor(target) { super(); - this.pos = 0; - this.buffer = new ArrayBuffer(2 ** 16); - this.bytes = new Uint8Array(this.buffer); - this.maxPos = 0; this.target = target; } ensureSize(size) { @@ -1214,7 +1286,6 @@ var Metamuxer = (() => { } async flush() { } - // eslint-disable-next-line @typescript-eslint/require-await async finalize() { this.ensureSize(this.pos); this.target.buffer = this.buffer.slice(0, Math.max(this.maxPos, this.pos)); @@ -1224,12 +1295,13 @@ var Metamuxer = (() => { } }; var StreamTargetWriter = class extends Writer { + pos = 0; + target; + sections = []; + lastFlushEnd = 0; + writer = null; constructor(target) { super(); - this.pos = 0; - this.sections = []; - this.lastFlushEnd = 0; - this.writer = null; this.target = target; } start() { @@ -1299,17 +1371,19 @@ var Metamuxer = (() => { var DEFAULT_CHUNK_SIZE = 2 ** 24; var MAX_CHUNKS_AT_ONCE = 2; var ChunkedStreamTargetWriter = class extends Writer { + pos = 0; + target; + chunkSize; + /** + * The data is divided up into fixed-size chunks, whose contents are first filled in RAM and then flushed out. + * A chunk is flushed if all of its contents have been written. + */ + chunks = []; + lastFlushEnd = 0; + writer = null; + flushedChunkQueue = []; constructor(target) { super(); - this.pos = 0; - /** - * The data is divided up into fixed-size chunks, whose contents are first filled in RAM and then flushed out. - * A chunk is flushed if all of its contents have been written. - */ - this.chunks = []; - this.lastFlushEnd = 0; - this.writer = null; - this.flushedChunkQueue = []; this.target = target; this.chunkSize = target._options?.chunkSize ?? DEFAULT_CHUNK_SIZE; if (!Number.isInteger(this.chunkSize) || this.chunkSize < 2 ** 10) { @@ -1427,22 +1501,21 @@ var Metamuxer = (() => { // src/target.ts var Target = class { - constructor() { - /** @internal */ - this._output = null; - } + /** @internal */ + _output = null; }; var ArrayBufferTarget = class extends Target { - constructor() { - super(...arguments); - this.buffer = null; - } + buffer = null; /** @internal */ _createWriter() { return new ArrayBufferTargetWriter(this); } }; var StreamTarget = class extends Target { + /** @internal */ + _writable; + /** @internal */ + _options; constructor(writable, options = {}) { super(); if (!(writable instanceof WritableStream)) { @@ -1467,6 +1540,9 @@ var Metamuxer = (() => { }; // src/codec.ts + var VIDEO_CODECS = ["avc", "hevc", "vp8", "vp9", "av1"]; + var AUDIO_CODECS = ["aac", "opus"]; + var SUBTITLE_CODECS = ["webvtt"]; var AVC_LEVEL_TABLE = [ { maxMacroblocks: 99, maxBitrate: 64e3, level: 10 }, // Level 1 @@ -1674,6 +1750,43 @@ var Metamuxer = (() => { } throw new TypeError(`Unhandled codec '${codec}'.`); }; + var extractVideoCodecString = (codec, description) => { + if (codec === "avc") { + if (!description || description.byteLength < 4) { + throw new TypeError("AVC description must be at least 4 bytes long."); + } + return `avc1.${bytesToHexString(description.subarray(1, 4))}`; + } else if (codec === "hevc") { + if (!description) { + throw new TypeError("HEVC description must be provided."); + } + const view2 = new DataView(description.buffer, description.byteOffset, description.byteLength); + let codecString = "hev1."; + const generalProfileSpace = description[1] >> 6 & 3; + const generalProfileIdc = description[1] & 31; + codecString += ["", "A", "B", "C"][generalProfileSpace] + generalProfileIdc; + codecString += "."; + const compatibilityFlags = reverseBitsU32(view2.getUint32(2)); + codecString += compatibilityFlags.toString(16); + codecString += "."; + const generalTierFlag = description[1] >> 5 & 1; + const generalLevelIdc = description[12]; + codecString += generalTierFlag === 0 ? "L" : "H"; + codecString += generalLevelIdc; + codecString += "."; + const constraintFlags = []; + for (let i = 0; i < 6; i++) { + const byte = description[i + 13]; + constraintFlags.push(byte); + } + while (constraintFlags[constraintFlags.length - 1] === 0) { + constraintFlags.pop(); + } + codecString += constraintFlags.map((x) => x.toString(16)).join("."); + return codecString; + } + throw new TypeError(`Unhandled codec '${codec}'.`); + }; var buildAudioCodecString = (codec, numberOfChannels, sampleRate) => { if (codec === "aac") { if (numberOfChannels >= 2 && sampleRate <= 24e3) { @@ -1690,6 +1803,20 @@ var Metamuxer = (() => { } throw new TypeError(`Unhandled codec '${codec}'.`); }; + var extractAudioCodecString = (codec, description) => { + if (codec === "aac") { + if (!description || description.byteLength < 2) { + throw new TypeError("AAC description must be at least 2 bytes long."); + } + const mpeg4AudioObjectType = description[0] >> 3; + return `mp4a.40.${mpeg4AudioObjectType}`; + } else if (codec === "opus") { + return "opus"; + } else if (codec === "vorbis") { + return "vorbis"; + } + throw new TypeError(`Unhandled codec '${codec}'.`); + }; var getVideoEncoderConfigExtension = (codec) => { if (codec === "avc") { return { @@ -1871,18 +1998,21 @@ var Metamuxer = (() => { return round ? Math.round(value) : value; }; var IsobmffMuxer = class extends Muxer { + timestampsMustStartAtZero = true; + writer; + boxWriter; + fastStart; + auxTarget = new ArrayBufferTarget(); + auxWriter = this.auxTarget._createWriter(); + auxBoxWriter = new IsobmffBoxWriter(this.auxWriter); + ftypSize = null; + mdat = null; + trackDatas = []; + creationTime = Math.floor(Date.now() / 1e3) + TIMESTAMP_OFFSET; + finalizedChunks = []; + nextFragmentNumber = 1; constructor(output, format) { super(output); - this.timestampsMustStartAtZero = true; - this.auxTarget = new ArrayBufferTarget(); - this.auxWriter = this.auxTarget._createWriter(); - this.auxBoxWriter = new IsobmffBoxWriter(this.auxWriter); - this.ftypSize = null; - this.mdat = null; - this.trackDatas = []; - this.creationTime = Math.floor(Date.now() / 1e3) + TIMESTAMP_OFFSET; - this.finalizedChunks = []; - this.nextFragmentNumber = 1; this.writer = output._writer; this.boxWriter = new IsobmffBoxWriter(this.writer); const fastStartDefault = this.writer instanceof ArrayBufferTargetWriter ? "in-memory" : false; @@ -2471,16 +2601,19 @@ var Metamuxer = (() => { // src/matroska/ebml.ts var EBMLFloat32 = class { + value; constructor(value) { this.value = value; } }; var EBMLFloat64 = class { + value; constructor(value) { this.value = value; } }; var EBMLSignedInt = class { + value; constructor(value) { this.value = value; } @@ -2554,29 +2687,31 @@ var Metamuxer = (() => { subtitle: 17 }; var MatroskaMuxer = class extends Muxer { + timestampsMustStartAtZero = false; + writer; + format; + helper = new Uint8Array(8); + helperView = new DataView(this.helper.buffer); + /** + * Stores the position from the start of the file to where EBML elements have been written. This is used to + * rewrite/edit elements that were already added before, and to measure sizes of things. + */ + offsets = /* @__PURE__ */ new WeakMap(); + /** Same as offsets, but stores position where the element's data starts (after ID and size fields). */ + dataOffsets = /* @__PURE__ */ new WeakMap(); + trackDatas = []; + segment = null; + segmentInfo = null; + seekHead = null; + tracksElement = null; + segmentDuration = null; + cues = null; + currentCluster = null; + currentClusterMsTimestamp = null; + trackDatasInCurrentCluster = /* @__PURE__ */ new Set(); + duration = 0; constructor(output, format) { super(output); - this.timestampsMustStartAtZero = false; - this.helper = new Uint8Array(8); - this.helperView = new DataView(this.helper.buffer); - /** - * Stores the position from the start of the file to where EBML elements have been written. This is used to - * rewrite/edit elements that were already added before, and to measure sizes of things. - */ - this.offsets = /* @__PURE__ */ new WeakMap(); - /** Same as offsets, but stores position where the element's data starts (after ID and size fields). */ - this.dataOffsets = /* @__PURE__ */ new WeakMap(); - this.trackDatas = []; - this.segment = null; - this.segmentInfo = null; - this.seekHead = null; - this.tracksElement = null; - this.segmentDuration = null; - this.cues = null; - this.currentCluster = null; - this.currentClusterMsTimestamp = null; - this.trackDatasInCurrentCluster = /* @__PURE__ */ new Set(); - this.duration = 0; this.writer = output._writer; this.format = format; if (this.format._options.streamable) { @@ -3251,6 +3386,8 @@ ${cue.notes ?? ""}`; var OutputFormat = class { }; var Mp4OutputFormat = class extends OutputFormat { + /** @internal */ + _options; constructor(options = {}) { if (!options || typeof options !== "object") { throw new TypeError("options must be an object."); @@ -3267,6 +3404,8 @@ ${cue.notes ?? ""}`; } }; var MkvOutputFormat2 = class extends OutputFormat { + /** @internal */ + _options; constructor(options = {}) { if (!options || typeof options !== "object") { throw new TypeError("options must be an object."); @@ -3285,19 +3424,14 @@ ${cue.notes ?? ""}`; var WebMOutputFormat = class extends MkvOutputFormat2 { }; - // src/source.ts - var VIDEO_CODECS = ["avc", "hevc", "vp8", "vp9", "av1"]; - var AUDIO_CODECS = ["aac", "opus"]; - var SUBTITLE_CODECS = ["webvtt"]; + // src/media-source.ts var MediaSource = class { - constructor() { - /** @internal */ - this._connectedTrack = null; - /** @internal */ - this._closed = false; - /** @internal */ - this._offsetTimestamps = false; - } + /** @internal */ + _connectedTrack = null; + /** @internal */ + _closed = false; + /** @internal */ + _offsetTimestamps = false; /** @internal */ _ensureValidDigest() { if (!this._connectedTrack) { @@ -3337,10 +3471,12 @@ ${cue.notes ?? ""}`; } }; var VideoSource = class extends MediaSource { + /** @internal */ + _connectedTrack = null; + /** @internal */ + _codec; constructor(codec) { super(); - /** @internal */ - this._connectedTrack = null; if (!VIDEO_CODECS.includes(codec)) { throw new TypeError(`Invalid video codec '${codec}'. Must be one of: ${VIDEO_CODECS.join(", ")}.`); } @@ -3378,13 +3514,13 @@ ${cue.notes ?? ""}`; constructor(source, codecConfig) { this.source = source; this.codecConfig = codecConfig; - this.encoder = null; - this.muxer = null; - this.lastMultipleOfKeyFrameInterval = -1; - this.lastWidth = null; - this.lastHeight = null; validateVideoCodecConfig(codecConfig); } + encoder = null; + muxer = null; + lastMultipleOfKeyFrameInterval = -1; + lastWidth = null; + lastHeight = null; async digest(videoFrame) { this.source._ensureValidDigest(); if (this.lastWidth !== null && this.lastHeight !== null) { @@ -3442,6 +3578,8 @@ ${cue.notes ?? ""}`; } }; var VideoFrameSource = class extends VideoSource { + /** @internal */ + _encoder; constructor(codecConfig) { super(codecConfig.codec); this._encoder = new VideoEncoderWrapper(this, codecConfig); @@ -3458,6 +3596,10 @@ ${cue.notes ?? ""}`; } }; var CanvasSource = class extends VideoSource { + /** @internal */ + _encoder; + /** @internal */ + _canvas; constructor(canvas, codecConfig) { if (!(canvas instanceof HTMLCanvasElement)) { throw new TypeError("canvas must be an HTMLCanvasElement."); @@ -3488,6 +3630,14 @@ ${cue.notes ?? ""}`; } }; var MediaStreamVideoTrackSource = class extends VideoSource { + /** @internal */ + _encoder; + /** @internal */ + _abortController = null; + /** @internal */ + _track; + /** @internal */ + _offsetTimestamps = true; constructor(track, codecConfig) { if (!(track instanceof MediaStreamTrack) || track.kind !== "video") { throw new TypeError("track must be a video MediaStreamTrack."); @@ -3497,10 +3647,6 @@ ${cue.notes ?? ""}`; latencyMode: "realtime" }; super(codecConfig.codec); - /** @internal */ - this._abortController = null; - /** @internal */ - this._offsetTimestamps = true; this._encoder = new VideoEncoderWrapper(this, codecConfig); this._track = track; } @@ -3531,10 +3677,12 @@ ${cue.notes ?? ""}`; } }; var AudioSource = class extends MediaSource { + /** @internal */ + _connectedTrack = null; + /** @internal */ + _codec; constructor(codec) { super(); - /** @internal */ - this._connectedTrack = null; if (!AUDIO_CODECS.includes(codec)) { throw new TypeError(`Invalid audio codec '${codec}'. Must be one of: ${AUDIO_CODECS.join(", ")}.`); } @@ -3568,12 +3716,12 @@ ${cue.notes ?? ""}`; constructor(source, codecConfig) { this.source = source; this.codecConfig = codecConfig; - this.encoder = null; - this.muxer = null; - this.lastNumberOfChannels = null; - this.lastSampleRate = null; validateAudioCodecConfig(codecConfig); } + encoder = null; + muxer = null; + lastNumberOfChannels = null; + lastSampleRate = null; async digest(audioData) { this.source._ensureValidDigest(); if (this.lastNumberOfChannels !== null && this.lastSampleRate !== null) { @@ -3620,6 +3768,8 @@ ${cue.notes ?? ""}`; } }; var AudioDataSource = class extends AudioSource { + /** @internal */ + _encoder; constructor(codecConfig) { super(codecConfig.codec); this._encoder = new AudioEncoderWrapper(this, codecConfig); @@ -3636,10 +3786,12 @@ ${cue.notes ?? ""}`; } }; var AudioBufferSource = class extends AudioSource { + /** @internal */ + _encoder; + /** @internal */ + _accumulatedFrameCount = 0; constructor(codecConfig) { super(codecConfig.codec); - /** @internal */ - this._accumulatedFrameCount = 0; this._encoder = new AudioEncoderWrapper(this, codecConfig); } digest(audioBuffer) { @@ -3673,15 +3825,19 @@ ${cue.notes ?? ""}`; } }; var MediaStreamAudioTrackSource = class extends AudioSource { + /** @internal */ + _encoder; + /** @internal */ + _abortController = null; + /** @internal */ + _track; + /** @internal */ + _offsetTimestamps = true; constructor(track, codecConfig) { if (!(track instanceof MediaStreamTrack) || track.kind !== "audio") { throw new TypeError("track must be an audio MediaStreamTrack."); } super(codecConfig.codec); - /** @internal */ - this._abortController = null; - /** @internal */ - this._offsetTimestamps = true; this._encoder = new AudioEncoderWrapper(this, codecConfig); this._track = track; } @@ -3712,10 +3868,12 @@ ${cue.notes ?? ""}`; } }; var SubtitleSource = class extends MediaSource { + /** @internal */ + _connectedTrack = null; + /** @internal */ + _codec; constructor(codec) { super(); - /** @internal */ - this._connectedTrack = null; if (!SUBTITLE_CODECS.includes(codec)) { throw new TypeError(`Invalid subtitle codec '${codec}'. Must be one of: ${SUBTITLE_CODECS.join(", ")}.`); } @@ -3723,6 +3881,8 @@ ${cue.notes ?? ""}`; } }; var TextSubtitleSource = class extends SubtitleSource { + /** @internal */ + _parser; constructor(codec) { super(codec); this._parser = new SubtitleParser({ @@ -3743,15 +3903,19 @@ ${cue.notes ?? ""}`; // src/output.ts var Output = class { + /** @internal */ + _muxer; + /** @internal */ + _writer; + /** @internal */ + _tracks = []; + /** @internal */ + _started = false; + /** @internal */ + _finalizing = false; + /** @internal */ + _mutex = new AsyncMutex(); constructor(options) { - /** @internal */ - this._tracks = []; - /** @internal */ - this._started = false; - /** @internal */ - this._finalizing = false; - /** @internal */ - this._mutex = new AsyncMutex(); if (!options || typeof options !== "object") { throw new TypeError("options must be an object."); } @@ -3854,6 +4018,1302 @@ ${cue.notes ?? ""}`; release(); } }; + + // src/source.ts + var Source = class { + }; + var ArrayBufferSource = class extends Source { + constructor(buffer) { + super(); + this.buffer = buffer; + } + /** @internal */ + async _read(start, end) { + return new Uint8Array(this.buffer, start, end - start); + } + /** @internal */ + async _getSize() { + return this.buffer.byteLength; + } + }; + var BlobSource = class extends Source { + constructor(blob) { + super(); + this.blob = blob; + } + /** @internal */ + async _read(start, end) { + const slice = this.blob.slice(start, end); + const buffer = await slice.arrayBuffer(); + return new Uint8Array(buffer); + } + /** @internal */ + async _getSize() { + return this.blob.size; + } + }; + + // src/demuxer.ts + var Demuxer = class { + input; + constructor(input) { + this.input = input; + } + }; + + // src/input-track.ts + var InputTrack = class { + /** @internal */ + _backing; + /** @internal */ + constructor(backing) { + this._backing = backing; + } + isVideoTrack() { + return this instanceof InputVideoTrack; + } + isAudioTrack() { + return this instanceof InputAudioTrack; + } + getDuration() { + return this._backing.getDuration(); + } + }; + var InputVideoTrack = class extends InputTrack { + /** @internal */ + _backing; + /** @internal */ + constructor(backing) { + super(backing); + this._backing = backing; + } + getCodec() { + return this._backing.getCodec(); + } + getWidth() { + return this._backing.getWidth(); + } + getHeight() { + return this._backing.getHeight(); + } + getRotation() { + return this._backing.getRotation(); + } + getDecoderConfig() { + return this._backing.getDecoderConfig(); + } + async getCodecMimeType() { + const decoderConfig = await this.getDecoderConfig(); + return decoderConfig.codec; + } + }; + var InputAudioTrack = class extends InputTrack { + /** @internal */ + _backing; + /** @internal */ + constructor(backing) { + super(backing); + this._backing = backing; + } + getCodec() { + return this._backing.getCodec(); + } + getNumberOfChannels() { + return this._backing.getNumberOfChannels(); + } + getSampleRate() { + return this._backing.getSampleRate(); + } + getDecoderConfig() { + return this._backing.getDecoderConfig(); + } + async getCodecMimeType() { + const decoderConfig = await this.getDecoderConfig(); + return decoderConfig.codec; + } + }; + + // src/isobmff/isobmff-reader.ts + var IsobmffReader = class { + constructor(reader) { + this.reader = reader; + } + pos = 0; + readRange(start, end) { + const { view: view2, offset } = this.reader.getViewAndOffset(start, end); + return new Uint8Array(view2.buffer, offset, end - start); + } + readU8() { + const { view: view2, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 1); + this.pos++; + return view2.getUint8(offset); + } + readU16() { + const { view: view2, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 2); + this.pos += 2; + return view2.getUint16(offset, false); + } + readU24() { + const { view: view2, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 3); + this.pos += 3; + const high = view2.getUint16(offset, false); + const low = view2.getUint8(offset + 2); + return high * 256 + low; + } + readS32() { + const { view: view2, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 4); + this.pos += 4; + return view2.getInt32(offset, false); + } + readU32() { + const { view: view2, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 4); + this.pos += 4; + return view2.getUint32(offset, false); + } + readI32() { + const { view: view2, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 4); + this.pos += 4; + return view2.getInt32(offset, false); + } + readU64() { + const high = this.readU32(); + const low = this.readU32(); + return high * 4294967296 + low; + } + readF64() { + const { view: view2, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 8); + this.pos += 8; + return view2.getFloat64(offset, false); + } + readFixed_16_16() { + return this.readS32() / 65536; + } + readFixed_2_30() { + return this.readS32() / 1073741824; + } + readAscii(length) { + const { view: view2, offset } = this.reader.getViewAndOffset(this.pos, this.pos + length); + this.pos += length; + let str = ""; + for (let i = 0; i < length; i++) { + str += String.fromCharCode(view2.getUint8(offset + i)); + } + return str; + } + readIsomVariableInteger() { + let result = 0; + for (let i = 0; i < 4; i++) { + result <<= 7; + const nextByte = this.readU8(); + result |= nextByte & 127; + if ((nextByte & 128) === 0) { + break; + } + } + return result; + } + readBoxHeader() { + let totalSize = this.readU32(); + const name = this.readAscii(4); + let headerSize = 8; + const hasLargeSize = totalSize === 1; + if (hasLargeSize) { + totalSize = this.readU64(); + headerSize = 16; + } + return { name, totalSize, headerSize, contentSize: totalSize - headerSize }; + } + }; + + // src/isobmff/isobmff-demuxer.ts + var knownMatrixes = [rotationMatrix(0), rotationMatrix(90), rotationMatrix(180), rotationMatrix(270)]; + var IsobmffDemuxer = class extends Demuxer { + isobmffReader; + currentTrack = null; + tracks = []; + metadataPromise = null; + movieTimescale = -1; + movieDurationInTimescale = -1; + constructor(input) { + super(input); + this.isobmffReader = new IsobmffReader(input._reader); + } + async getDuration() { + await this.readMetadata(); + if (this.movieDurationInTimescale === -1) { + throw new Error("Could not read movie duration."); + } + return this.movieDurationInTimescale / this.movieTimescale; + } + async getTracks() { + await this.readMetadata(); + return this.tracks.map((track) => track.inputTrack); + } + async getMimeType() { + await this.readMetadata(); + let string = "video/mp4"; + if (this.tracks.length > 0) { + const codecMimeTypes = await Promise.all(this.tracks.map((x) => x.inputTrack.getCodecMimeType())); + const uniqueCodecMimeTypes = [...new Set(codecMimeTypes)]; + string += `; codecs="${uniqueCodecMimeTypes.join(", ")}"`; + } + return string; + } + readMetadata() { + return this.metadataPromise ??= (async () => { + const sourceSize = await this.isobmffReader.reader.getSourceSize(); + while (this.isobmffReader.pos < sourceSize) { + await this.isobmffReader.reader.loadRange(this.isobmffReader.pos, this.isobmffReader.pos + 16); + const startPos = this.isobmffReader.pos; + const boxInfo = this.isobmffReader.readBoxHeader(); + if (boxInfo.name === "moov") { + await this.isobmffReader.reader.loadRange( + this.isobmffReader.pos, + this.isobmffReader.pos + boxInfo.contentSize + ); + this.readContiguousBoxes(boxInfo.contentSize); + return; + } + this.isobmffReader.pos = startPos + boxInfo.totalSize; + } + })(); + } + getSampleTableForTrack(internalTrack) { + if (internalTrack.sampleTable) { + return internalTrack.sampleTable; + } + const sampleTable = { + sampleTimingEntries: [], + sampleCompositionTimeOffsets: [], + sampleSizes: [], + keySampleIndices: null, + chunkOffsets: [], + sampleToChunk: [], + presentationTimestamps: [] + }; + internalTrack.sampleTable = sampleTable; + this.isobmffReader.pos = internalTrack.sampleTableOffset; + this.currentTrack = internalTrack; + this.traverseBox(); + this.currentTrack = null; + for (const entry of sampleTable.sampleTimingEntries) { + for (let i = 0; i < entry.count; i++) { + sampleTable.presentationTimestamps.push({ + presentationTimestamp: entry.startDecodeTimestamp + i * entry.delta, + sampleIndex: entry.startIndex + i + }); + } + } + for (const entry of sampleTable.sampleCompositionTimeOffsets) { + for (let i = 0; i < entry.count; i++) { + const sampleIndex = entry.startIndex + i; + const sample = sampleTable.presentationTimestamps[sampleIndex]; + if (!sample) { + continue; + } + sample.presentationTimestamp += entry.offset; + } + } + sampleTable.presentationTimestamps.sort((a, b) => a.presentationTimestamp - b.presentationTimestamp); + return internalTrack.sampleTable; + } + readContiguousBoxes(totalSize) { + const startIndex = this.isobmffReader.pos; + while (this.isobmffReader.pos - startIndex < totalSize) { + this.traverseBox(); + } + } + traverseBox() { + const startPos = this.isobmffReader.pos; + const boxInfo = this.isobmffReader.readBoxHeader(); + const boxEndPos = startPos + boxInfo.totalSize; + switch (boxInfo.name) { + case "mdia": + case "minf": + case "dinf": + { + this.readContiguousBoxes(boxInfo.contentSize); + } + ; + break; + case "mvhd": + { + const version = this.isobmffReader.readU8(); + this.isobmffReader.pos += 3; + if (version === 1) { + this.isobmffReader.pos += 8 + 8; + this.movieTimescale = this.isobmffReader.readU32(); + this.movieDurationInTimescale = this.isobmffReader.readU64(); + } else { + this.isobmffReader.pos += 4 + 4; + this.movieTimescale = this.isobmffReader.readU32(); + this.movieDurationInTimescale = this.isobmffReader.readU32(); + } + } + ; + break; + case "trak": + { + const track = { + id: -1, + demuxer: this, + inputTrack: null, + info: null, + timescale: -1, + durationInTimescale: -1, + rotation: 0, + sampleTableOffset: -1, + sampleTable: null + }; + this.currentTrack = track; + this.readContiguousBoxes(boxInfo.contentSize); + if (track.id !== -1 && track.timescale !== -1 && track.info !== null) { + if (track.info.type === "video" && track.info.codec !== null) { + const videoTrack = track; + track.inputTrack = new InputVideoTrack(new IsobmffVideoTrackBacking(videoTrack)); + this.tracks.push(track); + } else if (track.info.type === "audio" && track.info.codec !== null) { + const audioTrack = track; + track.inputTrack = new InputAudioTrack(new IsobmffAudioTrackBacking(audioTrack)); + this.tracks.push(track); + } + } + this.currentTrack = null; + } + ; + break; + case "tkhd": + { + const track = this.currentTrack; + assert(track); + const version = this.isobmffReader.readU8(); + const flags = this.isobmffReader.readU24(); + const trackEnabled = (flags & 1) !== 0; + if (!trackEnabled) { + break; + } + if (version === 0) { + this.isobmffReader.pos += 8; + track.id = this.isobmffReader.readU32(); + this.isobmffReader.pos += 8; + } else if (version === 1) { + this.isobmffReader.pos += 16; + track.id = this.isobmffReader.readU32(); + this.isobmffReader.pos += 12; + } else { + throw new Error(`Incorrect track header version ${version}.`); + } + this.isobmffReader.pos += 2 * 4 + 2 + 2 + 2 + 2; + const rotationMatrix2 = []; + rotationMatrix2.push(this.isobmffReader.readFixed_16_16(), this.isobmffReader.readFixed_16_16()); + this.isobmffReader.pos += 4; + rotationMatrix2.push(this.isobmffReader.readFixed_16_16(), this.isobmffReader.readFixed_16_16()); + const matrixIndex = knownMatrixes.findIndex((x) => x.every((y, i) => y === rotationMatrix2[i])); + if (matrixIndex === -1) { + track.rotation = 0; + } else { + track.rotation = 90 * matrixIndex; + } + } + ; + break; + case "mdhd": + { + const track = this.currentTrack; + assert(track); + const version = this.isobmffReader.readU8(); + this.isobmffReader.pos += 3; + if (version === 0) { + this.isobmffReader.pos += 8; + track.timescale = this.isobmffReader.readU32(); + track.durationInTimescale = this.isobmffReader.readU32(); + } else if (version === 1) { + this.isobmffReader.pos += 16; + track.timescale = this.isobmffReader.readU32(); + track.durationInTimescale = this.isobmffReader.readU64(); + } + } + ; + break; + case "hdlr": + { + const track = this.currentTrack; + assert(track); + this.isobmffReader.pos += 8; + const handlerType = this.isobmffReader.readAscii(4); + if (handlerType === "vide") { + track.info = { + type: "video", + width: -1, + height: -1, + codec: null, + codecDescription: null, + colorSpace: null + }; + } else if (handlerType === "soun") { + track.info = { + type: "audio", + numberOfChannels: -1, + sampleRate: -1, + codec: null, + codecDescription: null + }; + } + } + ; + break; + case "stbl": + { + const track = this.currentTrack; + assert(track); + track.sampleTableOffset = startPos; + this.readContiguousBoxes(boxInfo.contentSize); + } + ; + break; + case "stsd": + { + const track = this.currentTrack; + assert(track); + if (track.info === null || track.sampleTable) { + break; + } + const stsdVersion = this.isobmffReader.readU8(); + this.isobmffReader.pos += 3; + const entries = this.isobmffReader.readU32(); + for (let i = 0; i < entries; i++) { + const sampleBoxInfo = this.isobmffReader.readBoxHeader(); + if (track.info.type === "video") { + if (sampleBoxInfo.name === "avc1") { + track.info.codec = "avc"; + } else if (sampleBoxInfo.name === "hvc1" || sampleBoxInfo.name === "hev1") { + track.info.codec = "hevc"; + } else { + console.warn(`Unsupported video sample entry type ${sampleBoxInfo.name}.`); + break; + } + this.isobmffReader.pos += 6 * 1 + 2 + 2 + 2 + 3 * 4; + track.info.width = this.isobmffReader.readU16(); + track.info.height = this.isobmffReader.readU16(); + this.isobmffReader.pos += 4 + 4 + 4 + 2 + 32 + 2 + 2; + this.readContiguousBoxes(startPos + sampleBoxInfo.totalSize - this.isobmffReader.pos); + } else { + if (sampleBoxInfo.name === "mp4a") { + track.info.codec = "aac"; + } else if (sampleBoxInfo.name.toLowerCase() === "opus") { + track.info.codec = "opus"; + } else { + console.warn(`Unsupported audio sample entry type ${sampleBoxInfo.name}.`); + break; + } + this.isobmffReader.pos += 6 * 1 + 2; + const version = this.isobmffReader.readU16(); + this.isobmffReader.pos += 3 * 2; + let channelCount = this.isobmffReader.readU16(); + this.isobmffReader.pos += 2 + 2 + 2; + let sampleRate = this.isobmffReader.readU32() / 65536; + if (stsdVersion === 0 && version > 0) { + if (version === 1) { + this.isobmffReader.pos += 4 * 4; + } else if (version === 2) { + this.isobmffReader.pos += 4; + sampleRate = this.isobmffReader.readF64(); + channelCount = this.isobmffReader.readU32(); + this.isobmffReader.pos += 4; + const sampleSize = this.isobmffReader.readU32(); + const flags = this.isobmffReader.readU32(); + const bytesPerFrame = this.isobmffReader.readU32(); + const samplesPerFrame = this.isobmffReader.readU32(); + } + } + track.info.numberOfChannels = channelCount; + track.info.sampleRate = sampleRate; + this.readContiguousBoxes(startPos + sampleBoxInfo.totalSize - this.isobmffReader.pos); + } + } + } + ; + break; + case "avcC": + { + const track = this.currentTrack; + assert(track && track.info); + track.info.codecDescription = this.isobmffReader.readRange( + this.isobmffReader.pos, + this.isobmffReader.pos + boxInfo.contentSize + ); + } + ; + break; + case "hvcC": + { + const track = this.currentTrack; + assert(track && track.info); + track.info.codecDescription = this.isobmffReader.readRange( + this.isobmffReader.pos, + this.isobmffReader.pos + boxInfo.contentSize + ); + } + ; + break; + case "colr": + { + const track = this.currentTrack; + assert(track && track.info?.type === "video"); + const colourType = this.isobmffReader.readAscii(4); + if (colourType !== "nclx") { + break; + } + const colourPrimaries = this.isobmffReader.readU16(); + const transferCharacteristics = this.isobmffReader.readU16(); + const matrixCoefficients = this.isobmffReader.readU16(); + const fullRangeFlag = Boolean(this.isobmffReader.readU8() & 128); + track.info.colorSpace = { + primaries: COLOR_PRIMARIES_MAP_INVERSE[colourPrimaries], + transfer: TRANSFER_CHARACTERISTICS_MAP_INVERSE[transferCharacteristics], + matrix: MATRIX_COEFFICIENTS_MAP_INVERSE[matrixCoefficients], + fullRange: fullRangeFlag + }; + } + ; + break; + case "wave": + { + if (boxInfo.totalSize > 8) { + this.readContiguousBoxes(boxInfo.contentSize); + } + } + ; + break; + case "esds": + { + const track = this.currentTrack; + assert(track && track.info); + this.isobmffReader.pos += 4; + const tag = this.isobmffReader.readU8(); + assert(tag === 3); + this.isobmffReader.readIsomVariableInteger(); + this.isobmffReader.pos += 2; + const mixed = this.isobmffReader.readU8(); + const streamDependenceFlag = (mixed & 128) !== 0; + const urlFlag = (mixed & 64) !== 0; + const ocrStreamFlag = (mixed & 32) !== 0; + if (streamDependenceFlag) { + this.isobmffReader.pos += 2; + } + if (urlFlag) { + const urlLength = this.isobmffReader.readU8(); + this.isobmffReader.pos += urlLength; + } + if (ocrStreamFlag) { + this.isobmffReader.pos += 2; + } + const decoderConfigTag = this.isobmffReader.readU8(); + assert(decoderConfigTag === 4); + this.isobmffReader.readIsomVariableInteger(); + const objectTypeIndication = this.isobmffReader.readU8(); + assert(objectTypeIndication === 64); + this.isobmffReader.pos += 1 + 3 + 4 + 4; + const decoderSpecificInfoTag = this.isobmffReader.readU8(); + assert(decoderSpecificInfoTag === 5); + const decoderSpecificInfoLength = this.isobmffReader.readIsomVariableInteger(); + track.info.codecDescription = this.isobmffReader.readRange( + this.isobmffReader.pos, + this.isobmffReader.pos + decoderSpecificInfoLength + ); + } + ; + break; + case "stts": + { + const track = this.currentTrack; + assert(track); + if (!track.sampleTable) { + break; + } + this.isobmffReader.pos += 4; + const entryCount = this.isobmffReader.readU32(); + let currentIndex = 0; + let currentTimestamp = 0; + for (let i = 0; i < entryCount; i++) { + const sampleCount = this.isobmffReader.readU32(); + const sampleDelta = this.isobmffReader.readU32(); + track.sampleTable.sampleTimingEntries.push({ + startIndex: currentIndex, + startDecodeTimestamp: currentTimestamp, + count: sampleCount, + delta: sampleDelta + }); + currentIndex += sampleCount; + currentTimestamp += sampleCount * sampleDelta; + } + } + ; + break; + case "ctts": + { + const track = this.currentTrack; + assert(track); + if (!track.sampleTable) { + break; + } + this.isobmffReader.pos += 1 + 3; + const entryCount = this.isobmffReader.readU32(); + let sampleIndex = 0; + for (let i = 0; i < entryCount; i++) { + const sampleCount = this.isobmffReader.readU32(); + const sampleOffset = this.isobmffReader.readI32(); + track.sampleTable.sampleCompositionTimeOffsets.push({ + startIndex: sampleIndex, + count: sampleCount, + offset: sampleOffset + }); + sampleIndex += sampleCount; + } + } + ; + break; + case "stsz": + { + const track = this.currentTrack; + assert(track); + if (!track.sampleTable) { + break; + } + this.isobmffReader.pos += 4; + const sampleSize = this.isobmffReader.readU32(); + const sampleCount = this.isobmffReader.readU32(); + if (sampleSize === 0) { + for (let i = 0; i < sampleCount; i++) { + const sampleSize2 = this.isobmffReader.readU32(); + track.sampleTable.sampleSizes.push(sampleSize2); + } + } else { + track.sampleTable.sampleSizes.push(sampleSize); + } + } + ; + break; + case "stz2": + { + throw new Error("Unsupported."); + } + ; + case "stss": + { + const track = this.currentTrack; + assert(track); + if (!track.sampleTable) { + break; + } + this.isobmffReader.pos += 4; + track.sampleTable.keySampleIndices = []; + const entryCount = this.isobmffReader.readU32(); + for (let i = 0; i < entryCount; i++) { + const sampleIndex = this.isobmffReader.readU32() - 1; + track.sampleTable.keySampleIndices.push(sampleIndex); + } + } + ; + break; + case "stsc": + { + const track = this.currentTrack; + assert(track); + if (!track.sampleTable) { + break; + } + this.isobmffReader.pos += 4; + const entryCount = this.isobmffReader.readU32(); + for (let i = 0; i < entryCount; i++) { + const startChunkIndex = this.isobmffReader.readU32() - 1; + const samplesPerChunk = this.isobmffReader.readU32(); + const sampleDescriptionIndex = this.isobmffReader.readU32(); + track.sampleTable.sampleToChunk.push({ + startSampleIndex: -1, + startChunkIndex, + samplesPerChunk, + sampleDescriptionIndex + }); + } + let startSampleIndex = 0; + for (let i = 0; i < track.sampleTable.sampleToChunk.length; i++) { + track.sampleTable.sampleToChunk[i].startSampleIndex = startSampleIndex; + if (i < track.sampleTable.sampleToChunk.length - 1) { + const nextChunk = track.sampleTable.sampleToChunk[i + 1]; + const chunkCount = nextChunk.startChunkIndex - track.sampleTable.sampleToChunk[i].startChunkIndex; + startSampleIndex += chunkCount * track.sampleTable.sampleToChunk[i].samplesPerChunk; + } + } + } + ; + break; + case "stco": + { + const track = this.currentTrack; + assert(track); + if (!track.sampleTable) { + break; + } + this.isobmffReader.pos += 4; + const entryCount = this.isobmffReader.readU32(); + for (let i = 0; i < entryCount; i++) { + const chunkOffset = this.isobmffReader.readU32(); + track.sampleTable.chunkOffsets.push(chunkOffset); + } + } + ; + break; + case "co64": + { + const track = this.currentTrack; + assert(track); + if (!track.sampleTable) { + break; + } + this.isobmffReader.pos += 4; + const entryCount = this.isobmffReader.readU32(); + for (let i = 0; i < entryCount; i++) { + const chunkOffset = this.isobmffReader.readU64(); + track.sampleTable.chunkOffsets.push(chunkOffset); + } + } + ; + break; + } + this.isobmffReader.pos = boxEndPos; + } + }; + var IsobmffTrackBacking = class { + constructor(internalTrack) { + this.internalTrack = internalTrack; + } + chunkToSampleIndex = /* @__PURE__ */ new WeakMap(); + sampleIndexToChunk = /* @__PURE__ */ new Map(); + getCodec() { + throw new Error("Not implemented on base class."); + } + async getDuration() { + return this.internalTrack.durationInTimescale / this.internalTrack.timescale; + } + }; + var IsobmffVideoTrackBacking = class extends IsobmffTrackBacking { + internalTrack; + constructor(internalTrack) { + super(internalTrack); + this.internalTrack = internalTrack; + } + async getCodec() { + return this.internalTrack.info.codec; + } + async getWidth() { + return this.internalTrack.info.width; + } + async getHeight() { + return this.internalTrack.info.height; + } + async getRotation() { + return this.internalTrack.rotation; + } + async getDecoderConfig() { + return { + codec: extractVideoCodecString(this.internalTrack.info.codec, this.internalTrack.info.codecDescription), + codedWidth: this.internalTrack.info.width, + codedHeight: this.internalTrack.info.height, + description: this.internalTrack.info.codecDescription ?? void 0, + colorSpace: this.internalTrack.info.colorSpace ?? void 0 + }; + } + async fetchChunkForSampleIndex(sampleIndex) { + if (sampleIndex === -1) { + return null; + } + const existingChunk = this.sampleIndexToChunk.get(sampleIndex)?.deref(); + if (existingChunk) { + return existingChunk; + } + const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack); + const sampleInfo = getSampleInfo(sampleTable, sampleIndex); + if (!sampleInfo) { + return null; + } + const data = await this.internalTrack.demuxer.isobmffReader.reader.source._read( + sampleInfo.byteOffset, + sampleInfo.byteOffset + sampleInfo.byteSize + ); + const chunk = new EncodedVideoChunk({ + data, + timestamp: 1e6 * sampleInfo.presentationTimestamp / this.internalTrack.timescale, + duration: 1e6 * sampleInfo.duration / this.internalTrack.timescale, + type: sampleInfo.isKeyFrame ? "key" : "delta" + }); + this.chunkToSampleIndex.set(chunk, sampleIndex); + this.sampleIndexToChunk.set(sampleIndex, new WeakRef(chunk)); + return chunk; + } + async getFirstChunk() { + return this.fetchChunkForSampleIndex(0); + } + async getChunk(timestamp) { + const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack); + const sampleIndex = getSampleIndexForTimestamp(sampleTable, timestamp * this.internalTrack.timescale); + return this.fetchChunkForSampleIndex(sampleIndex); + } + async getNextChunk(chunk) { + const sampleIndex = this.chunkToSampleIndex.get(chunk); + if (sampleIndex === void 0) { + throw new Error("Chunk was not created from this track."); + } + return this.fetchChunkForSampleIndex(sampleIndex + 1); + } + async getKeyChunk(timestamp) { + const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack); + const sampleIndex = getSampleIndexForTimestamp(sampleTable, timestamp * this.internalTrack.timescale); + const keyFrameSampleIndex = sampleIndex === -1 ? -1 : getRelevantKeyframeIndexForSample(sampleTable, sampleIndex); + return this.fetchChunkForSampleIndex(keyFrameSampleIndex); + } + async getNextKeyChunk(chunk) { + const sampleIndex = this.chunkToSampleIndex.get(chunk); + if (sampleIndex === void 0) { + throw new Error("Chunk was not created from this track."); + } + const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack); + const nextKeyFrameSampleIndex = getNextKeyframeIndexForSample(sampleTable, sampleIndex); + return this.fetchChunkForSampleIndex(nextKeyFrameSampleIndex); + } + }; + var IsobmffAudioTrackBacking = class extends IsobmffTrackBacking { + internalTrack; + constructor(internalTrack) { + super(internalTrack); + this.internalTrack = internalTrack; + } + async getCodec() { + return this.internalTrack.info.codec; + } + async getNumberOfChannels() { + return this.internalTrack.info.numberOfChannels; + } + async getSampleRate() { + return this.internalTrack.info.sampleRate; + } + async getDecoderConfig() { + return { + codec: extractAudioCodecString(this.internalTrack.info.codec, this.internalTrack.info.codecDescription), + numberOfChannels: this.internalTrack.info.numberOfChannels, + sampleRate: this.internalTrack.info.sampleRate, + description: this.internalTrack.info.codecDescription ?? void 0 + }; + } + }; + var getSampleIndexForTimestamp = (sampleTable, timescaleUnits) => { + const index = binarySearchLessOrEqual( + sampleTable.presentationTimestamps, + timescaleUnits, + (x) => x.presentationTimestamp + ); + if (index === -1) { + return -1; + } + return sampleTable.presentationTimestamps[index].sampleIndex; + }; + var getSampleInfo = (sampleTable, sampleIndex) => { + const timingEntryIndex = binarySearchLessOrEqual(sampleTable.sampleTimingEntries, sampleIndex, (x) => x.startIndex); + const timingEntry = sampleTable.sampleTimingEntries[timingEntryIndex]; + if (!timingEntry || timingEntry.startIndex + timingEntry.count <= sampleIndex) { + return null; + } + const decodeTimestamp = timingEntry.startDecodeTimestamp + (sampleIndex - timingEntry.startIndex) * timingEntry.delta; + let presentationTimestamp = decodeTimestamp; + const offsetEntryIndex = binarySearchLessOrEqual( + sampleTable.sampleCompositionTimeOffsets, + sampleIndex, + (x) => x.startIndex + ); + const offsetEntry = sampleTable.sampleCompositionTimeOffsets[offsetEntryIndex]; + if (offsetEntry) { + presentationTimestamp += offsetEntry.offset; + } + const sampleSize = sampleTable.sampleSizes[Math.min(sampleIndex, sampleTable.sampleSizes.length - 1)]; + const chunkEntryIndex = binarySearchLessOrEqual(sampleTable.sampleToChunk, sampleIndex, (x) => x.startSampleIndex); + const chunkEntry = sampleTable.sampleToChunk[chunkEntryIndex]; + assert(chunkEntry); + const chunkIndex = chunkEntry.startChunkIndex + Math.floor((sampleIndex - chunkEntry.startSampleIndex) / chunkEntry.samplesPerChunk); + const chunkOffset = sampleTable.chunkOffsets[chunkIndex]; + let sampleOffset = chunkOffset; + if (sampleTable.sampleSizes.length === 1) { + sampleOffset += sampleSize * (sampleIndex - chunkEntry.startSampleIndex); + } else { + const startSampleIndex = chunkEntry.startSampleIndex + (chunkIndex - chunkEntry.startChunkIndex) * chunkEntry.samplesPerChunk; + for (let i = startSampleIndex; i < sampleIndex; i++) { + sampleOffset += sampleTable.sampleSizes[i]; + } + } + return { + presentationTimestamp, + duration: timingEntry.delta, + byteOffset: sampleOffset, + byteSize: sampleSize, + isKeyFrame: sampleTable.keySampleIndices ? binarySearchExact(sampleTable.keySampleIndices, sampleIndex, (x) => x) !== -1 : true + }; + }; + var getRelevantKeyframeIndexForSample = (sampleTable, sampleIndex) => { + if (!sampleTable.keySampleIndices) { + return sampleIndex; + } + const index = binarySearchLessOrEqual(sampleTable.keySampleIndices, sampleIndex, (x) => x); + return sampleTable.keySampleIndices[index] ?? -1; + }; + var getNextKeyframeIndexForSample = (sampleTable, sampleIndex) => { + if (!sampleTable.keySampleIndices) { + return sampleIndex + 1; + } + const index = binarySearchLessOrEqual(sampleTable.keySampleIndices, sampleIndex, (x) => x); + return sampleTable.keySampleIndices[index + 1] ?? -1; + }; + + // src/matroska/matroska-demuxer.ts + var MatroskaDemuxer = class extends Demuxer { + }; + + // src/input-format.ts + var InputFormat = class { + }; + var IsobmffInputFormat = class extends InputFormat { + /** @internal */ + async _canReadInput(input) { + const sourceSize = await input._reader.getSourceSize(); + if (sourceSize < 8) { + return false; + } + await input._reader.loadRange(4, 8); + const isobmffReader = new IsobmffReader(input._reader); + isobmffReader.pos = 4; + const fourCc = isobmffReader.readAscii(4); + return fourCc === "ftyp"; + } + _createDemuxer(input) { + return new IsobmffDemuxer(input); + } + }; + var MatroskaInputFormat = class extends InputFormat { + /** @internal */ + async _canReadInput() { + return false; + } + _createDemuxer(input) { + return new MatroskaDemuxer(input); + } + }; + var ISOBMFF = new IsobmffInputFormat(); + var MP4 = ISOBMFF; + var MOV = ISOBMFF; + var MATROSKA = new MatroskaInputFormat(); + var MKV = MATROSKA; + var WEBM = MATROSKA; + var ALL_FORMATS = [ISOBMFF, MKV]; + + // src/reader.ts + var PAGE_SIZE = 4096; + var Reader = class { + constructor(source) { + this.source = source; + } + loadedSegments = []; + sourceSizePromise = null; + getSourceSize() { + if (this.sourceSizePromise) { + return this.sourceSizePromise; + } else { + return this.sourceSizePromise = this.source._getSize(); + } + } + async loadRange(start, end) { + let alignedStart = Math.floor(start / PAGE_SIZE) * PAGE_SIZE; + let alignedEnd = Math.ceil(end / PAGE_SIZE) * PAGE_SIZE; + alignedEnd = Math.min(alignedEnd, await this.getSourceSize()); + const thing = this.loadedSegments.find((x) => x.start <= alignedStart); + if (thing) { + alignedStart = Math.max(alignedStart, thing.end); + } + const thing2 = this.loadedSegments.find((x) => x.end >= alignedEnd); + if (thing2) { + alignedEnd = Math.min(alignedEnd, thing2.start); + } + if (alignedStart >= alignedEnd) { + return; + } + const bytes2 = await this.source._read(alignedStart, alignedEnd); + this.insertIntoLoadedSegments(alignedStart, bytes2); + } + insertIntoLoadedSegments(start, bytes2) { + const segment = { + start, + end: start + bytes2.byteLength, + bytes: bytes2, + view: new DataView(bytes2.buffer) + }; + let index = this.loadedSegments.findLastIndex((x) => x.start <= start); + this.loadedSegments.splice(index + 1, 0, segment); + if (index === -1 || this.loadedSegments[index].end < segment.start) { + index++; + } + const mergeSectionStartIndex = index; + const mergeSectionStart = this.loadedSegments[mergeSectionStartIndex].start; + let mergeSectionEndIndex = index; + let mergeSectionEnd = this.loadedSegments[mergeSectionEndIndex].end; + while (this.loadedSegments.length - 1 > mergeSectionEndIndex && this.loadedSegments[mergeSectionEndIndex + 1].start <= mergeSectionEnd) { + mergeSectionEndIndex++; + mergeSectionEnd = Math.max(mergeSectionEnd, this.loadedSegments[mergeSectionEndIndex].end); + } + if (mergeSectionStartIndex === mergeSectionEndIndex) { + return; + } + for (let i = mergeSectionStartIndex; i <= mergeSectionEndIndex; i++) { + const segment2 = this.loadedSegments[i]; + const coversEntireMergeSection = segment2.start === mergeSectionStart && segment2.end === mergeSectionEnd; + if (coversEntireMergeSection) { + this.loadedSegments.splice(i + 1, mergeSectionEndIndex - i); + this.loadedSegments.splice(mergeSectionStartIndex, i - mergeSectionStartIndex); + return; + } + } + const unifiedBytes = new Uint8Array(mergeSectionEnd - mergeSectionStart); + for (let i = mergeSectionStartIndex; i <= mergeSectionEndIndex; i++) { + const segment2 = this.loadedSegments[i]; + unifiedBytes.set(segment2.bytes, segment2.start - mergeSectionStart); + } + this.loadedSegments.splice(mergeSectionStartIndex + 1, mergeSectionEndIndex - mergeSectionStartIndex); + this.loadedSegments[mergeSectionStartIndex].end = mergeSectionEnd; + this.loadedSegments[mergeSectionStartIndex].bytes = unifiedBytes; + this.loadedSegments[mergeSectionStartIndex].view = new DataView(unifiedBytes.buffer); + } + getViewAndOffset(start, end) { + const segment = this.loadedSegments.find((x) => x.start <= start && end <= x.end); + if (!segment) { + throw new Error(`No segment loaded for range [${start}, ${end}).`); + } + return { + view: segment.view, + offset: segment.bytes.byteOffset + start - segment.start + }; + } + }; + + // src/input.ts + var Input = class { + /** @internal */ + _formats; + /** @internal */ + _reader; + /** @internal */ + _demuxerPromise = null; + /** @internal */ + _format = null; + constructor(options) { + this._formats = options.formats; + this._reader = new Reader(options.source); + } + /** @internal */ + _getDemuxer() { + return this._demuxerPromise ??= (async () => { + for (const format of this._formats) { + const canRead = await format._canReadInput(this); + if (canRead) { + this._format = format; + return format._createDemuxer(this); + } + } + throw new Error("Input has an unrecognizable format."); + })(); + } + async getFormat() { + await this._getDemuxer(); + assert(this._format); + return this._format; + } + async getDuration() { + const demuxer = await this._getDemuxer(); + return demuxer.getDuration(); + } + async getTracks() { + const demuxer = await this._getDemuxer(); + return demuxer.getTracks(); + } + async getVideoTracks() { + const tracks = await this.getTracks(); + return tracks.filter((x) => x.isVideoTrack()); + } + async getPrimaryVideoTrack() { + const tracks = await this.getTracks(); + return tracks.find((x) => x.isVideoTrack()) ?? null; + } + async getAudioTracks() { + const tracks = await this.getTracks(); + return tracks.filter((x) => x.isAudioTrack()); + } + async getPrimaryAudioTrack() { + const tracks = await this.getTracks(); + return tracks.find((x) => x.isAudioTrack()) ?? null; + } + async getMimeType() { + const demuxer = await this._getDemuxer(); + return demuxer.getMimeType(); + } + }; + + // src/media-drain.ts + var EncodedVideoChunkDrain = class { + constructor(videoTrack) { + this.videoTrack = videoTrack; + } + getFirstChunk() { + return this.videoTrack._backing.getFirstChunk(); + } + getChunk(timestamp) { + return this.videoTrack._backing.getChunk(timestamp); + } + getNextChunk(chunk) { + return this.videoTrack._backing.getNextChunk(chunk); + } + getKeyChunk(timestamp) { + return this.videoTrack._backing.getKeyChunk(timestamp); + } + getNextKeyChunk(chunk) { + return this.videoTrack._backing.getNextKeyChunk(chunk); + } + async *chunks(startTimestamp = 0) { + let chunk = await this.getChunk(startTimestamp); + while (chunk) { + yield chunk; + chunk = await this.getNextChunk(chunk); + } + } + }; + var VideoFrameDrain = class { + constructor(videoTrack) { + this.videoTrack = videoTrack; + } + decoderConfig = null; + async createDecoder(onFrame) { + if (!this.decoderConfig) { + this.decoderConfig = await this.videoTrack.getDecoderConfig(); + } + const decoder = new VideoDecoder({ + output: onFrame, + error: (error) => console.error(error) + }); + decoder.configure(this.decoderConfig); + return decoder; + } + async getKeyFrame(timestamp) { + let result = null; + const decoder = await this.createDecoder((frame) => result = frame); + const chunk = await this.videoTrack._backing.getKeyChunk(timestamp); + if (!chunk) { + return null; + } + decoder.decode(chunk); + await decoder.flush(); + decoder.close(); + return result; + } + async getFrame(timestamp) { + let result = null; + const decoder = await this.createDecoder((frame) => { + if (frame.timestamp / 1e6 <= timestamp) { + result?.close(); + result = frame; + } else { + frame.close(); + } + }); + const keyChunk = await this.videoTrack._backing.getKeyChunk(timestamp); + if (!keyChunk) { + return null; + } + const targetChunk = await this.videoTrack._backing.getChunk(timestamp); + assert(targetChunk); + decoder.decode(keyChunk); + let currentChunk = keyChunk; + while (currentChunk !== targetChunk) { + const nextChunk = await this.videoTrack._backing.getNextChunk(currentChunk); + assert(nextChunk); + currentChunk = nextChunk; + decoder.decode(nextChunk); + if (decoder.decodeQueueSize >= 10) { + await new Promise((resolve) => decoder.addEventListener("dequeue", resolve, { once: true })); + } + } + await decoder.flush(); + decoder.close(); + return result; + } + async *frames(startTimestamp = 0) { + const frameQueue = []; + let firstFrameQueued = false; + let lastFrame = null; + let { promise: queueNonEmpty, resolve: onQueueNotEmpty } = promiseWithResolvers(); + let ended = false; + const decoder = await this.createDecoder((frame) => { + if (ended) { + frame.close(); + return; + } + const frameTimestamp = frame.timestamp / 1e6; + if (lastFrame) { + if (frameTimestamp > startTimestamp) { + frameQueue.push(lastFrame); + firstFrameQueued = true; + } else { + lastFrame.close(); + } + } + if (frameTimestamp >= startTimestamp) { + frameQueue.push(frame); + firstFrameQueued = true; + } + lastFrame = firstFrameQueued ? null : frame; + if (frameQueue.length > 0) { + onQueueNotEmpty(); + ({ promise: queueNonEmpty, resolve: onQueueNotEmpty } = promiseWithResolvers()); + } + }); + const keyChunk = await this.videoTrack._backing.getKeyChunk(startTimestamp); + if (!keyChunk) { + return; + } + let decoderIsFlushed = false; + void (async () => { + let currentChunk = keyChunk; + while (currentChunk && !ended) { + decoder.decode(currentChunk); + if (decoder.decodeQueueSize >= 10) { + await new Promise((resolve) => decoder.addEventListener("dequeue", resolve, { once: true })); + } + const nextChunk = await this.videoTrack._backing.getNextChunk(currentChunk); + currentChunk = nextChunk; + } + await decoder.flush(); + decoder.close(); + decoderIsFlushed = true; + onQueueNotEmpty(); + })(); + try { + while (true) { + if (frameQueue.length > 0) { + yield frameQueue.shift(); + } else if (!decoderIsFlushed) { + await queueNonEmpty; + } else { + break; + } + } + } finally { + ended = true; + } + } + }; return __toCommonJS(src_exports); })(); if (typeof module === "object" && typeof module.exports === "object") Object.assign(module.exports, Metamuxer) diff --git a/dist/metamuxer.min.js b/dist/metamuxer.min.js index 9f2561c..67d7f32 100644 --- a/dist/metamuxer.min.js +++ b/dist/metamuxer.min.js @@ -1,10 +1,10 @@ -"use strict";var Metamuxer=(()=>{var Pe=Object.defineProperty;var ht=Object.getOwnPropertyDescriptor;var bt=Object.getOwnPropertyNames;var xt=Object.prototype.hasOwnProperty;var Tt=(t,i)=>{for(var e in i)Pe(t,e,{get:i[e],enumerable:!0})},wt=(t,i,e,r)=>{if(i&&typeof i=="object"||typeof i=="function")for(let s of bt(i))!xt.call(t,s)&&s!==e&&Pe(t,s,{get:()=>i[s],enumerable:!(r=ht(i,s))||r.enumerable});return t};var Ct=t=>wt(Pe({},"__esModule",{value:!0}),t);var _r={};Tt(_r,{AUDIO_CODECS:()=>Y,ArrayBufferTarget:()=>K,AudioBufferSource:()=>Oe,AudioDataSource:()=>Ee,AudioSource:()=>O,CanvasSource:()=>ve,EncodedAudioChunkSource:()=>_e,EncodedVideoChunkSource:()=>Se,MediaSource:()=>W,MediaStreamAudioTrackSource:()=>Me,MediaStreamVideoTrackSource:()=>Ae,MkvOutputFormat:()=>se,Mp4OutputFormat:()=>ke,Output:()=>ze,OutputFormat:()=>z,SUBTITLE_CODECS:()=>ge,StreamTarget:()=>he,SubtitleSource:()=>R,Target:()=>V,TextSubtitleSource:()=>Ve,VIDEO_CODECS:()=>G,VideoFrameSource:()=>ye,VideoSource:()=>E,WebMOutputFormat:()=>D});function d(t){if(!t)throw new Error("Assertion failed.")}var S=t=>t&&t[t.length-1],P=t=>t>=0&&t<2**32,B=(t,i,e)=>{let r=0;for(let s=i;s>a;r<<=1,r|=u}return r},Qe=(t,i,e,r)=>{for(let s=i;s>e-s-1<t instanceof ArrayBuffer?new Uint8Array(t):new Uint8Array(t.buffer,t.byteOffset,t.byteLength),y=new TextEncoder,H={bt709:1,bt470bg:5,smpte170m:6},Q={bt709:1,smpte170m:6,"iec61966-2-1":13},$={rgb:0,bt709:1,bt470bg:5,smpte170m:6},ae=t=>!!t&&!!t.primaries&&!!t.transfer&&!!t.matrix&&t.fullRange!==void 0,Be=t=>t instanceof ArrayBuffer||typeof SharedArrayBuffer<"u"&&t instanceof SharedArrayBuffer||ArrayBuffer.isView(t)&&!(t instanceof DataView),N=class{constructor(){this.currentPromise=Promise.resolve()}async acquire(){let i,e=new Promise(s=>{i=s}),r=this.currentPromise;return this.currentPromise=e,await r,i}};var Z=/(?:(.+?)\n)?((?:\d{2}:)?\d{2}:\d{2}.\d{3})\s+-->\s+((?:\d{2}:)?\d{2}:\d{2}.\d{3})/g,kt=/^WEBVTT(.|\n)*?\n{2}/,j=/<(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})>/g,ue=class{constructor(i){this.preambleText=null;this.preambleEmitted=!1;this.options=i}parse(i){i=i.replaceAll(`\r +"use strict";var Metamuxer=(()=>{var et=Object.defineProperty;var tr=Object.getOwnPropertyDescriptor;var rr=Object.getOwnPropertyNames;var ir=Object.prototype.hasOwnProperty;var sr=(r,e)=>{for(var t in e)et(r,t,{get:e[t],enumerable:!0})},or=(r,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of rr(e))!ir.call(r,s)&&s!==t&&et(r,s,{get:()=>e[s],enumerable:!(i=tr(e,s))||i.enumerable});return r};var nr=r=>or(et({},"__esModule",{value:!0}),r);var ki={};sr(ki,{ALL_FORMATS:()=>er,AUDIO_CODECS:()=>L,ArrayBufferSource:()=>Ne,ArrayBufferTarget:()=>Y,AudioBufferSource:()=>Ue,AudioDataSource:()=>Be,AudioSource:()=>O,BlobSource:()=>He,CanvasSource:()=>Re,EncodedAudioChunkSource:()=>ze,EncodedVideoChunkDrain:()=>Ze,EncodedVideoChunkSource:()=>Ve,ISOBMFF:()=>he,Input:()=>Ye,MATROSKA:()=>qe,MKV:()=>kt,MOV:()=>Zt,MP4:()=>Yt,MediaSource:()=>Q,MediaStreamAudioTrackSource:()=>De,MediaStreamVideoTrackSource:()=>Pe,MkvOutputFormat:()=>ue,Mp4OutputFormat:()=>Oe,Output:()=>We,OutputFormat:()=>R,SUBTITLE_CODECS:()=>ne,Source:()=>J,StreamTarget:()=>ye,SubtitleSource:()=>j,Target:()=>M,TextSubtitleSource:()=>Fe,VIDEO_CODECS:()=>H,VideoFrameDrain:()=>Je,VideoFrameSource:()=>Me,VideoSource:()=>I,WEBM:()=>Jt,WebMOutputFormat:()=>$});function l(r){if(!r)throw new Error("Assertion failed.")}var y=r=>r&&r[r.length-1],P=r=>r>=0&&r<2**32,z=(r,e,t)=>{let i=0;for(let s=e;s>a;i<<=1,i|=c}return i},gt=(r,e,t,i)=>{for(let s=e;s>t-s-1<r instanceof ArrayBuffer?new Uint8Array(r):new Uint8Array(r.buffer,r.byteOffset,r.byteLength),v=new TextEncoder,tt=r=>Object.fromEntries(Object.entries(r).map(([e,t])=>[t,e])),U={bt709:1,bt470bg:5,smpte170m:6},Tt=tt(U),D={bt709:1,smpte170m:6,"iec61966-2-1":13},xt=tt(D),F={rgb:0,bt709:1,bt470bg:5,smpte170m:6},Ct=tt(F),be=r=>!!r&&!!r.primaries&&!!r.transfer&&!!r.matrix&&r.fullRange!==void 0,rt=r=>r instanceof ArrayBuffer||typeof SharedArrayBuffer<"u"&&r instanceof SharedArrayBuffer||ArrayBuffer.isView(r)&&!(r instanceof DataView),K=class{currentPromise=Promise.resolve();async acquire(){let e,t=new Promise(s=>{e=s}),i=this.currentPromise;return this.currentPromise=t,await i,e}},V=r=>{let e=r*(Math.PI/180),t=Math.cos(e),i=Math.sin(e);return[t,i,0,-i,t,0,0,0,1]},it=V(0),wt=r=>[...r].map(e=>e.toString(16).padStart(2,"0")).join(""),St=r=>(r=r>>1&1431655765|(r&1431655765)<<1,r=r>>2&858993459|(r&858993459)<<2,r=r>>4&252645135|(r&252645135)<<4,r=r>>8&16711935|(r&16711935)<<8,r=r>>16&65535|(r&65535)<<16,r>>>0),yt=(r,e,t)=>{let i=0,s=r.length-1,o=-1;for(;i<=s;){let n=i+s>>1,a=t(r[n]);a===e?(o=n,s=n-1):a{let i=-1,s=0,o=r.length-1;for(;s<=o;){let n=s+(o-s+1)/2|0;t(r[n])<=e?(i=n,s=n+1):o=n-1}return i},st=()=>{let r,e;return{promise:new Promise((i,s)=>{r=i,e=s}),resolve:r,reject:e}};var re=/(?:(.+?)\n)?((?:\d{2}:)?\d{2}:\d{2}.\d{3})\s+-->\s+((?:\d{2}:)?\d{2}:\d{2}.\d{3})/g,ar=/^WEBVTT(.|\n)*?\n{2}/,q=/<(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})>/g,ke=class{options;preambleText=null;preambleEmitted=!1;constructor(e){this.options=e}parse(e){e=e.replaceAll(`\r `,` `).replaceAll("\r",` -`),Z.lastIndex=0;let e;if(!this.preambleText){if(!kt.test(i)){let s=new Error("WebVTT preamble incorrect.");throw this.options.error(s),s}e=Z.exec(i);let r=i.slice(0,e?.index??i.length).trimEnd();if(!r){let s=new Error("No WebVTT preamble provided.");throw this.options.error(s),s}this.preambleText=r,e&&(i=i.slice(e.index),Z.lastIndex=0)}for(;e=Z.exec(i);){let r=i.slice(0,e.index),s=e[1],o=e.index+e[0].length,n=i.indexOf(` -`,o)+1,a=i.slice(o,n).trim(),u=i.indexOf(` +`),re.lastIndex=0;let t;if(!this.preambleText){if(!ar.test(e)){let s=new Error("WebVTT preamble incorrect.");throw this.options.error(s),s}t=re.exec(e);let i=e.slice(0,t?.index??e.length).trimEnd();if(!i){let s=new Error("No WebVTT preamble provided.");throw this.options.error(s),s}this.preambleText=i,t&&(e=e.slice(t.index),re.lastIndex=0)}for(;t=re.exec(e);){let i=e.slice(0,t.index),s=t[1],o=t.index+t[0].length,n=e.indexOf(` +`,o)+1,a=e.slice(o,n).trim(),c=e.indexOf(` -`,o);u===-1&&(u=i.length);let m=ce(e[2]),f=ce(e[3])-m,T=i.slice(n,u).trim();i=i.slice(u).trimStart(),Z.lastIndex=0;let M={timestamp:m/1e3,duration:f/1e3,text:T,identifier:s,settings:a,notes:r},C={};this.preambleEmitted||(C.config={description:this.preambleText},this.preambleEmitted=!0),this.options.output(M,C)}}},gt=/(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})/,ce=t=>{let i=gt.exec(t);if(!i)throw new Error("Expected match.");return 60*60*1e3*Number(i[1]||"0")+60*1e3*Number(i[2])+1e3*Number(i[3])+Number(i[4])},le=t=>{let i=Math.floor(t/36e5),e=Math.floor(t%(60*60*1e3)/(60*1e3)),r=Math.floor(t%(60*1e3)/1e3),s=t%1e3;return i.toString().padStart(2,"0")+":"+e.toString().padStart(2,"0")+":"+r.toString().padStart(2,"0")+"."+s.toString().padStart(3,"0")};var J=class{constructor(i){this.writer=i;this.helper=new Uint8Array(8);this.helperView=new DataView(this.helper.buffer);this.offsets=new WeakMap}writeU32(i){this.helperView.setUint32(0,i,!1),this.writer.write(this.helper.subarray(0,4))}writeU64(i){this.helperView.setUint32(0,Math.floor(i/2**32),!1),this.helperView.setUint32(4,i,!1),this.writer.write(this.helper.subarray(0,8))}writeAscii(i){for(let e=0;e[(t%256+256)%256],h=t=>(_.setUint16(0,t,!1),[p[0],p[1]]),St=t=>(_.setInt16(0,t,!1),[p[0],p[1]]),je=t=>(_.setUint32(0,t,!1),[p[1],p[2],p[3]]),c=t=>(_.setUint32(0,t,!1),[p[0],p[1],p[2],p[3]]),Le=t=>(_.setInt32(0,t,!1),[p[0],p[1],p[2],p[3]]),U=t=>(_.setUint32(0,Math.floor(t/2**32),!1),_.setUint32(4,t,!1),[p[0],p[1],p[2],p[3],p[4],p[5],p[6],p[7]]),De=t=>(_.setInt16(0,2**8*t,!1),[p[0],p[1]]),A=t=>(_.setInt32(0,2**16*t,!1),[p[0],p[1],p[2],p[3]]),Ie=t=>(_.setInt32(0,2**30*t,!1),[p[0],p[1],p[2],p[3]]),Ue=(t,i)=>{let e=[],r=t;do{let s=r&127;r>>=7,e.length>0&&(s|=128),e.push(s),i!==void 0&&i--}while(r>0||i);return e.reverse()},g=(t,i=!1)=>{let e=Array(t.length).fill(null).map((r,s)=>t.charCodeAt(s));return i&&e.push(0),e},We=t=>{let i=null;for(let e of t)(!i||e.timestamp>i.timestamp)&&(i=e);return i},qe=t=>{let i=t*(Math.PI/180),e=Math.cos(i),r=Math.sin(i);return[e,r,0,-r,e,0,0,0,1]},Ke=qe(0),Xe=t=>[A(t[0]),A(t[1]),Ie(t[2]),A(t[3]),A(t[4]),Ie(t[5]),A(t[6]),A(t[7]),Ie(t[8])],b=(t,i,e)=>({type:t,contents:i&&new Uint8Array(i.flat(10)),children:e}),x=(t,i,e,r,s)=>b(t,[w(i),je(e),r??[]],s),Ge=t=>t.fragmented?b("ftyp",[g("iso5"),c(512),g("iso5"),g("iso6"),g("mp41")]):b("ftyp",[g("isom"),c(512),g("isom"),t.holdsAvc?g("avc1"):[],g("mp41")]),me=t=>({type:"mdat",largeSize:t}),Ye=t=>({type:"free",size:t}),ee=(t,i,e=!1)=>b("moov",void 0,[yt(i,t),...t.map(r=>vt(r,i)),e?sr(t):null]),yt=(t,i)=>{let e=v(Math.max(0,...i.filter(n=>n.samples.length>0).map(n=>{let a=We(n.samples);return a.timestamp+a.duration})),de),r=Math.max(0,...i.map(n=>n.track.id))+1,s=!P(t)||!P(e),o=s?U:c;return x("mvhd",+s,0,[o(t),o(t),c(de),o(e),A(1),De(1),Array(10).fill(0),Xe(Ke),Array(24).fill(0),c(r)])},vt=(t,i)=>b("trak",void 0,[At(t,i),_t(t,i)]),At=(t,i)=>{let e=We(t.samples),r=v(e?e.timestamp+e.duration:0,de),s=!P(i)||!P(r),o=s?U:c,n;if(t.type==="video"){let a=t.track.metadata.rotation;n=a===void 0||typeof a=="number"?qe(a??0):a}else n=Ke;return x("tkhd",+s,3,[o(i),o(i),c(t.track.id),c(0),o(r),Array(8).fill(0),h(0),h(t.track.id),De(t.type==="audio"?1:0),h(0),Xe(n),A(t.type==="video"?t.info.width:0),A(t.type==="video"?t.info.height:0)])},_t=(t,i)=>b("mdia",void 0,[Et(t,i),Vt(t),zt(t)]),Et=(t,i)=>{let e=We(t.samples),r=v(e?e.timestamp+e.duration:0,t.timescale),s=!P(i)||!P(r),o=s?U:c;return x("mdhd",+s,0,[o(i),o(i),c(t.timescale),o(r),h(21956),h(0)])},Ot={video:"vide",audio:"soun",subtitle:"text"},Mt={video:"VideoHandler",audio:"SoundHandler",subtitle:"TextHandler"},Vt=t=>x("hdlr",0,0,[g("mhlr"),g(Ot[t.type]),c(0),c(0),c(0),g(Mt[t.type],!0)]),zt=t=>b("minf",void 0,[Ut[t.type](),Dt(),Ft(t)]),Pt=()=>x("vmhd",0,1,[h(0),h(0),h(0),h(0)]),Bt=()=>x("smhd",0,0,[h(0),h(0)]),It=()=>x("nmhd",0,0),Ut={video:Pt,audio:Bt,subtitle:It},Dt=()=>b("dinf",void 0,[Wt()]),Wt=()=>x("dref",0,0,[c(1)],[Rt()]),Rt=()=>x("url ",0,1),Ft=t=>{let i=t.compositionTimeOffsetTable.length>1||t.compositionTimeOffsetTable.some(e=>e.sampleCompositionTimeOffset!==0);return b("stbl",void 0,[Nt(t),Zt(t),Jt(t),er(t),tr(t),rr(t),i?ir(t):null])},Nt=t=>{let i;return t.type==="video"?i=Ht(fr[t.track.source._codec],t):t.type==="audio"?i=qt(hr[t.track.source._codec],t):t.type==="subtitle"&&(i=Gt(xr[t.track.source._codec],t)),d(i),x("stsd",0,0,[c(1)],[i])},Ht=(t,i)=>b(t,[Array(6).fill(0),h(1),h(0),h(0),Array(12).fill(0),h(i.info.width),h(i.info.height),c(4718592),c(4718592),c(0),h(1),Array(32).fill(0),h(24),St(65535)],[pr[i.track.source._codec](i),ae(i.info.decoderConfig.colorSpace)?Qt(i):null]),Qt=t=>b("colr",[g("nclx"),h(H[t.info.decoderConfig.colorSpace.primaries]),h(Q[t.info.decoderConfig.colorSpace.transfer]),h($[t.info.decoderConfig.colorSpace.matrix]),w((t.info.decoderConfig.colorSpace.fullRange?1:0)<<7)]),$t=t=>t.info.decoderConfig&&b("avcC",[...I(t.info.decoderConfig.description)]),jt=t=>t.info.decoderConfig&&b("hvcC",[...I(t.info.decoderConfig.description)]),$e=t=>{if(!t.info.decoderConfig)return null;let i=t.info.decoderConfig;d(i.colorSpace);let e=i.codec.split("."),r=Number(e[1]),s=Number(e[2]),a=(Number(e[3])<<4)+(0<<1)+Number(i.colorSpace.fullRange);return x("vpcC",1,0,[w(r),w(s),w(a),w(2),w(2),w(2),h(0)])},Lt=()=>b("av1C",[129,0,0,0]),qt=(t,i)=>b(t,[Array(6).fill(0),h(1),h(0),h(0),c(0),h(i.info.numberOfChannels),h(16),h(0),h(0),A(i.info.sampleRate)],[br[i.track.source._codec](i)]),Kt=t=>{let e=[...I(t.info.decoderConfig.description??new ArrayBuffer(0))];return e=[...w(64),...w(21),...je(0),...c(0),...c(0),...w(5),...Ue(e.length),...e],e=[...h(1),...w(0),...w(4),...Ue(e.length),...e,...w(6),...w(1),...w(2)],e=[...w(3),...Ue(e.length),...e],x("esds",0,0,e)},Xt=t=>{let i=3840,e=0,r=t.info.decoderConfig?.description;if(r){d(r.byteLength>=18);let s=ArrayBuffer.isView(r)?new DataView(r.buffer,r.byteOffset,r.byteLength):new DataView(r);i=s.getUint16(10,!0),e=s.getInt16(14,!0)}return b("dOps",[w(0),w(t.info.numberOfChannels),h(i),c(t.info.sampleRate),De(e),w(0)])},Gt=(t,i)=>b(t,[Array(6).fill(0),h(1)],[Tr[i.track.source._codec](i)]),Yt=t=>b("vttC",[...y.encode(t.info.config.description)]);var Zt=t=>x("stts",0,0,[c(t.timeToSampleTable.length),t.timeToSampleTable.map(i=>[c(i.sampleCount),c(i.sampleDelta)])]),Jt=t=>{if(t.samples.every(e=>e.type==="key"))return null;let i=[...t.samples.entries()].filter(([,e])=>e.type==="key");return x("stss",0,0,[c(i.length),i.map(([e])=>c(e+1))])},er=t=>x("stsc",0,0,[c(t.compactlyCodedChunkTable.length),t.compactlyCodedChunkTable.map(i=>[c(i.firstChunk),c(i.samplesPerChunk),c(1)])]),tr=t=>x("stsz",0,0,[c(0),c(t.samples.length),t.samples.map(i=>c(i.size))]),rr=t=>t.finalizedChunks.length>0&&S(t.finalizedChunks).offset>=2**32?x("co64",0,0,[c(t.finalizedChunks.length),t.finalizedChunks.map(i=>U(i.offset))]):x("stco",0,0,[c(t.finalizedChunks.length),t.finalizedChunks.map(i=>c(i.offset))]),ir=t=>x("ctts",0,0,[c(t.compositionTimeOffsetTable.length),t.compositionTimeOffsetTable.map(i=>[c(i.sampleCount),c(i.sampleCompositionTimeOffset)])]),sr=t=>b("mvex",void 0,t.map(or)),or=t=>x("trex",0,0,[c(t.track.id),c(1),c(0),c(0),c(0)]),Re=(t,i)=>b("moof",void 0,[nr(t),...i.map(ar)]),nr=t=>x("mfhd",0,0,[c(t)]),Ze=t=>{let i=0,e=0,r=0,s=0,o=t.type==="delta";return e|=+o,o?i|=1:i|=2,i<<24|e<<16|r<<8|s},ar=t=>b("traf",void 0,[ur(t),cr(t),lr(t)]),ur=t=>{d(t.currentChunk);let i=0;i|=8,i|=16,i|=32,i|=131072;let e=t.currentChunk.samples[1]??t.currentChunk.samples[0],r={duration:e.timescaleUnitsToNextSample,size:e.size,flags:Ze(e)};return x("tfhd",0,i,[c(t.track.id),c(r.duration),c(r.size),c(r.flags)])},cr=t=>(d(t.currentChunk),x("tfdt",1,0,[U(v(t.currentChunk.startTimestamp,t.timescale))])),lr=t=>{d(t.currentChunk);let i=t.currentChunk.samples.map(k=>k.timescaleUnitsToNextSample),e=t.currentChunk.samples.map(k=>k.size),r=t.currentChunk.samples.map(Ze),s=t.currentChunk.samples.map(k=>v(k.timestamp-k.decodeTimestamp,t.timescale)),o=new Set(i),n=new Set(e),a=new Set(r),u=new Set(s),m=a.size===2&&r[0]!==r[1],l=o.size>1,f=n.size>1,T=!m&&a.size>1,M=u.size>1||[...u].some(k=>k!==0),C=0;return C|=1,C|=4*+m,C|=256*+l,C|=512*+f,C|=1024*+T,C|=2048*+M,x("trun",1,C,[c(t.currentChunk.samples.length),c(t.currentChunk.offset-t.currentChunk.moofOffset||0),m?c(r[0]):[],t.currentChunk.samples.map((k,F)=>[l?c(i[F]):[],f?c(e[F]):[],T?c(r[F]):[],M?Le(s[F]):[]])])},Je=t=>b("mfra",void 0,[...t.map(dr),mr()]),dr=(t,i)=>x("tfra",1,0,[c(t.track.id),c(63),c(t.finalizedChunks.length),t.finalizedChunks.map(r=>[U(v(r.startTimestamp,t.timescale)),U(r.moofOffset),c(i+1),c(1),c(1)])]),mr=()=>x("mfro",0,0,[c(0)]),et=()=>b("vtte"),tt=(t,i,e,r,s)=>b("vttc",void 0,[s!==null?b("vsid",[Le(s)]):null,e!==null?b("iden",[...y.encode(e)]):null,i!==null?b("ctim",[...y.encode(le(i))]):null,r!==null?b("sttg",[...y.encode(r)]):null,b("payl",[...y.encode(t)])]),rt=t=>b("vtta",[...y.encode(t)]),fr={avc:"avc1",hevc:"hvc1",vp8:"vp08",vp9:"vp09",av1:"av01"},pr={avc:$t,hevc:jt,vp8:$e,vp9:$e,av1:Lt},hr={aac:"mp4a",opus:"Opus"},br={aac:Kt,opus:Xt},xr={webvtt:"wvtt"},Tr={webvtt:Yt};var L=class{constructor(i){this.mutex=new N;this.trackTimestampInfo=new WeakMap;this.output=i}beforeTrackAdd(i){}onTrackClose(i){}validateAndNormalizeTimestamp(i,e,r){let s=e/1e6,o=this.trackTimestampInfo.get(i);if(!o){if(!r)throw new Error("First frame must be a key frame.");if(this.timestampsMustStartAtZero&&s>0)throw new Error(`Timestamps must start at zero (got ${s}s).`);o={timestampOffset:s,maxTimestamp:i.source._offsetTimestamps?0:s,lastKeyFrameTimestamp:i.source._offsetTimestamps?0:s},this.trackTimestampInfo.set(i,o)}if(i.source._offsetTimestamps&&(s-=o.timestampOffset),s<0)throw new Error(`Timestamps must be non-negative (got ${s}s).`);if(ss.start-o.start);e.push({start:r[0].start,size:r[0].data.byteLength});for(let s=1;sm.start<=r&&rCr){for(let m=0;m=e.written[n+1].start;)e.written[n].end=Math.max(e.written[n].end,e.written[n+1].end),e.written.splice(n+1,1)}createChunk(e){let s={start:Math.floor(e/this.chunkSize)*this.chunkSize,data:new Uint8Array(this.chunkSize),written:[],shouldFlush:!1};return this.chunks.push(s),this.chunks.sort((o,n)=>o.start-n.start),this.chunks.indexOf(s)}queueChunksForFlush(e=!1){d(this.writer);for(let r=0;r{if(t==="avc"){let o=Math.ceil(i/16)*Math.ceil(e/16),n=it.find(f=>o<=f.maxMacroblocks&&r<=f.maxBitrate)??S(it),a=n?n.level:0,u="64".padStart(2,"0"),m="00",l=a.toString(16).padStart(2,"0");return`avc1.${u}${m}${l}`}else if(t==="hevc"){let s="",n="6",a=i*e,u=st.find(l=>a<=l.maxPictureSize&&r<=l.maxBitrate)??S(st);return`hev1.${s}1.${n}.${u.tier}${u.level}.B0`}else{if(t==="vp8")return"vp8";if(t==="vp9"){let s="00",o=i*e,n=ot.find(u=>o<=u.maxPictureSize&&r<=u.maxBitrate)??S(ot);return`vp09.${s}.${n.level}.08`}else if(t==="av1"){let o=i*e,n=nt.find(u=>o<=u.maxPictureSize&&r<=u.maxBitrate)??S(nt);return`av01.0.${n.level.toString().padStart(2,"0")}${n.tier}.08`}}throw new TypeError(`Unhandled codec '${t}'.`)},ut=(t,i,e)=>{if(t==="aac")return i>=2&&e<=24e3?"mp4a.40.29":e<=24e3?"mp4a.40.5":"mp4a.40.2";if(t==="opus")return"opus";if(t==="vorbis")return"vorbis";throw new TypeError(`Unhandled codec '${t}'.`)},ct=t=>t==="avc"?{avc:{format:"avc"}}:t==="hevc"?{hevc:{format:"hevc"}}:{},lt=t=>t==="aac"?{aac:{format:"aac"}}:t==="opus"?{opus:{format:"opus"}}:{},be=t=>{if(!t)throw new TypeError("Video chunk metadata must be provided.");if(typeof t!="object")throw new TypeError("Video chunk metadata must be an object.");if(!t.decoderConfig)throw new TypeError("Video chunk metadata must include a decoder configuration.");if(typeof t.decoderConfig!="object")throw new TypeError("Video chunk metadata decoder configuration must be an object.");if(typeof t.decoderConfig.codec!="string")throw new TypeError("Video chunk metadata decoder configuration must specify a codec string.");if(!Number.isInteger(t.decoderConfig.codedWidth)||t.decoderConfig.codedWidth<=0)throw new TypeError("Video chunk metadata decoder configuration must specify a valid codedWidth (positive integer).");if(!Number.isInteger(t.decoderConfig.codedHeight)||t.decoderConfig.codedHeight<=0)throw new TypeError("Video chunk metadata decoder configuration must specify a valid codedHeight (positive integer).");if(t.decoderConfig.description!==void 0&&!Be(t.decoderConfig.description))throw new TypeError("Video chunk metadata decoder configuration description, when defined, must be an ArrayBuffer or an ArrayBuffer view.");if(t.decoderConfig.colorSpace!==void 0){let{colorSpace:i}=t.decoderConfig;if(typeof i!="object")throw new TypeError("Video chunk metadata decoder configuration colorSpace, when provided, must be an object.");let e=Object.keys(H);if(i.primaries!=null&&!e.includes(i.primaries))throw new TypeError(`Video chunk metadata decoder configuration colorSpace primaries, when defined, must be one of ${e.join(", ")}.`);let r=Object.keys(Q);if(i.transfer!=null&&!r.includes(i.transfer))throw new TypeError(`Video chunk metadata decoder configuration colorSpace transfer, when defined, must be one of ${r.join(", ")}.`);let s=Object.keys($);if(i.matrix!=null&&!s.includes(i.matrix))throw new TypeError(`Video chunk metadata decoder configuration colorSpace matrix, when defined, must be one of ${s.join(", ")}.`);if(i.fullRange!=null&&typeof i.fullRange!="boolean")throw new TypeError("Video chunk metadata decoder configuration colorSpace fullRange, when defined, must be a boolean.")}if((t.decoderConfig.codec.startsWith("avc1")||t.decoderConfig.codec.startsWith("avc3"))&&!t.decoderConfig.description)throw new TypeError("Video chunk metadata decoder configuration for AVC must include a description, which is expected to be an AVCDecoderConfigurationRecord as specified in ISO 14496-15.");if((t.decoderConfig.codec.startsWith("hev1")||t.decoderConfig.codec.startsWith("hvc1"))&&!t.decoderConfig.description)throw new TypeError("Video chunk metadata decoder configuration for HEVC must include a description, which is expected to be an HEVCDecoderConfigurationRecord as specified in ISO 14496-15.");if((t.decoderConfig.codec==="vp8"||t.decoderConfig.codec.startsWith("vp09"))&&t.decoderConfig.colorSpace===void 0)throw new TypeError("Video chunk metadata decoder configuration for VP8/VP9 must include a colorSpace.")},xe=t=>{if(!t)throw new TypeError("Audio chunk metadata must be provided.");if(typeof t!="object")throw new TypeError("Audio chunk metadata must be an object.");if(!t.decoderConfig)throw new TypeError("Audio chunk metadata must include a decoder configuration.");if(typeof t.decoderConfig!="object")throw new TypeError("Audio chunk metadata decoder configuration must be an object.");if(typeof t.decoderConfig.codec!="string")throw new TypeError("Audio chunk metadata decoder configuration must specify a codec string.");if(!Number.isInteger(t.decoderConfig.sampleRate)||t.decoderConfig.sampleRate<=0)throw new TypeError("Audio chunk metadata decoder configuration must specify a valid sampleRate (positive integer).");if(!Number.isInteger(t.decoderConfig.numberOfChannels)||t.decoderConfig.numberOfChannels<=0)throw new TypeError("Audio chunk metadata decoder configuration must specify a valid numberOfChannels (positive integer).");if(t.decoderConfig.description!==void 0&&!Be(t.decoderConfig.description))throw new TypeError("Audio chunk metadata decoder configuration description, when defined, must be an ArrayBuffer or an ArrayBuffer view.");if(t.decoderConfig.codec.startsWith("mp4a")&&!t.decoderConfig.description)throw new TypeError("Audio chunk metadata decoder configuration for AAC must include a description, which is expected to be an AudioSpecificConfig as specified in ISO 14496-3.");if(t.decoderConfig.codec==="opus"&&t.decoderConfig.description&&t.decoderConfig.description.byteLength<18)throw new TypeError("Invalid decoder description provided for Opus; must be at least 18 bytes long.")},Te=t=>{if(!t)throw new TypeError("Subtitle metadata must be provided.");if(typeof t!="object")throw new TypeError("Subtitle metadata must be an object.");if(!t.config)throw new TypeError("Subtitle metadata must include a config object.");if(typeof t.config!="object")throw new TypeError("Subtitle metadata config must be an object.");if(typeof t.config.description!="string")throw new TypeError("Subtitle metadata config description must be a string.")};var de=1e3,kr=2082844800,v=(t,i,e=!0)=>{let r=t*i;return e?Math.round(r):r},we=class extends L{constructor(e,r){super(e);this.timestampsMustStartAtZero=!0;this.auxTarget=new K;this.auxWriter=this.auxTarget._createWriter();this.auxBoxWriter=new J(this.auxWriter);this.ftypSize=null;this.mdat=null;this.trackDatas=[];this.creationTime=Math.floor(Date.now()/1e3)+kr;this.finalizedChunks=[];this.nextFragmentNumber=1;this.writer=e._writer,this.boxWriter=new J(this.writer);let s=this.writer instanceof q?"in-memory":!1;this.fastStart=r._options.fastStart??s,(this.fastStart==="in-memory"||this.fastStart==="fragmented")&&(this.writer.ensureMonotonicity=!0)}async start(){let e=await this.mutex.acquire(),r=this.output._tracks.some(s=>s.type==="video"&&s.source._codec==="avc");this.boxWriter.writeBox(Ge({holdsAvc:r,fragmented:this.fastStart==="fragmented"})),this.ftypSize=this.writer.getPos(),this.fastStart==="in-memory"?this.mdat=me(!1):this.fastStart==="fragmented"||(this.mdat=me(!0),this.boxWriter.writeBox(this.mdat)),await this.writer.flush(),e()}getVideoTrackData(e,r){let s=this.trackDatas.find(n=>n.track===e);if(s)return s;be(r),d(r),d(r.decoderConfig),d(r.decoderConfig.codedWidth!==void 0),d(r.decoderConfig.codedHeight!==void 0);let o={track:e,type:"video",info:{width:r.decoderConfig.codedWidth,height:r.decoderConfig.codedHeight,decoderConfig:r.decoderConfig},timescale:e.metadata.frameRate??57600,samples:[],sampleQueue:[],timestampProcessingQueue:[],timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.trackDatas.push(o),this.trackDatas.sort((n,a)=>n.track.id-a.track.id),o}getAudioTrackData(e,r){let s=this.trackDatas.find(n=>n.track===e);if(s)return s;xe(r),d(r),d(r.decoderConfig);let o={track:e,type:"audio",info:{numberOfChannels:r.decoderConfig.numberOfChannels,sampleRate:r.decoderConfig.sampleRate,decoderConfig:r.decoderConfig},timescale:r.decoderConfig.sampleRate,samples:[],sampleQueue:[],timestampProcessingQueue:[],timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.trackDatas.push(o),this.trackDatas.sort((n,a)=>n.track.id-a.track.id),o}getSubtitleTrackData(e,r){let s=this.trackDatas.find(n=>n.track===e);if(s)return s;Te(r),d(r),d(r.config);let o={track:e,type:"subtitle",info:{config:r.config},timescale:1e3,samples:[],sampleQueue:[],timestampProcessingQueue:[],timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[],lastCueEndTimestamp:0,cueQueue:[],nextSourceId:0,cueToSourceId:new WeakMap};return this.trackDatas.push(o),this.trackDatas.sort((n,a)=>n.track.id-a.track.id),this.validateAndNormalizeTimestamp(e,0,!0),o}async addEncodedVideoChunk(e,r,s){let o=await this.mutex.acquire();try{let n=this.getVideoTrackData(e,s),a=new Uint8Array(r.byteLength);r.copyTo(a);let u=this.validateAndNormalizeTimestamp(n.track,r.timestamp,r.type==="key"),m=this.createSampleForTrack(n,a,u,(r.duration??0)/1e6,r.type);await this.registerSample(n,m)}finally{o()}}async addEncodedAudioChunk(e,r,s){let o=await this.mutex.acquire();try{let n=this.getAudioTrackData(e,s),a=new Uint8Array(r.byteLength);r.copyTo(a);let u=r.type,m=this.validateAndNormalizeTimestamp(n.track,r.timestamp,u==="key"),l=this.createSampleForTrack(n,a,m,(r.duration??0)/1e6,u);await this.registerSample(n,l)}finally{o()}}async addSubtitleCue(e,r,s){let o=await this.mutex.acquire();try{let n=this.getSubtitleTrackData(e,s);this.validateAndNormalizeTimestamp(n.track,1e6*r.timestamp,!0),e.source._codec==="webvtt"&&(n.cueQueue.push(r),await this.processWebVTTCues(n,r.timestamp))}finally{o()}}async processWebVTTCues(e,r){for(;e.cueQueue.length>0;){let s=new Set([]);for(let l of e.cueQueue)d(l.timestamp<=r),d(e.lastCueEndTimestamp<=l.timestamp+l.duration),s.add(Math.max(l.timestamp,e.lastCueEndTimestamp)),s.add(l.timestamp+l.duration);let o=[...s].sort((l,f)=>l-f),n=o[0],a=o[1]??n;if(r=a)break;j.lastIndex=0;let T=j.test(f.text),M=f.timestamp+f.duration,C=e.cueToSourceId.get(f);if(C===void 0&&as.timestamp).sort((s,o)=>s-o);for(let s=0;s{if(e===a)return r.type==="key";let u=a.sampleQueue[0];return u&&u.type==="key"});o>=1&&n&&(s=!0,await this.finalizeFragment())}else s=o>=.5}s&&(e.currentChunk&&await this.finalizeCurrentChunk(e),e.currentChunk={startTimestamp:r.timestamp,samples:[],offset:null,moofOffset:null}),d(e.currentChunk),e.currentChunk.samples.push(r),e.timestampProcessingQueue.push(r)}async finalizeCurrentChunk(e){if(d(this.fastStart!=="fragmented"),!!e.currentChunk){if(e.finalizedChunks.push(e.currentChunk),this.finalizedChunks.push(e.currentChunk),(e.compactlyCodedChunkTable.length===0||S(e.compactlyCodedChunkTable).samplesPerChunk!==e.currentChunk.samples.length)&&e.compactlyCodedChunkTable.push({firstChunk:e.finalizedChunks.length,samplesPerChunk:e.currentChunk.samples.length}),this.fastStart==="in-memory"){e.currentChunk.offset=0;return}e.currentChunk.offset=this.writer.getPos();for(let r of e.currentChunk.samples)d(r.data),this.writer.write(r.data),r.data=null;await this.writer.flush()}}async interleaveSamples(){d(this.fastStart==="fragmented");for(let e of this.output._tracks)if(!e.source._closed&&!this.trackDatas.some(r=>r.track===e))return;e:for(;;){let e=null,r=1/0;for(let o of this.trackDatas){if(o.sampleQueue.length===0&&!o.track.source._closed)break e;o.sampleQueue.length>0&&o.sampleQueue[0].timestamp=2**32&&(u.largeSize=!0,l=this.boxWriter.measureBox(u)+m),u.size=l,this.boxWriter.writeBox(u)}for(let u of this.trackDatas){u.currentChunk.offset=this.writer.getPos(),u.currentChunk.moofOffset=s;for(let m of u.currentChunk.samples)this.writer.write(m.data),m.data=null}let n=this.writer.getPos();this.writer.seek(this.boxWriter.offsets.get(o));let a=Re(r,this.trackDatas);this.boxWriter.writeBox(a),this.writer.seek(n);for(let u of this.trackDatas)u.finalizedChunks.push(u.currentChunk),this.finalizedChunks.push(u.currentChunk),u.currentChunk=null;e&&await this.writer.flush()}async onTrackClose(e){let r=await this.mutex.acquire();if(e.type==="subtitle"&&e.source._codec==="webvtt"){let s=this.trackDatas.find(o=>o.track===e);s&&await this.processWebVTTCues(s,1/0)}this.fastStart==="fragmented"&&await this.interleaveSamples(),r()}async finalize(){let e=await this.mutex.acquire();for(let r of this.trackDatas)r.type==="subtitle"&&r.track.source._codec==="webvtt"&&await this.processWebVTTCues(r,1/0);if(this.fastStart==="fragmented"){for(let r of this.trackDatas){for(let s of r.sampleQueue)await this.addSampleToTrack(r,s);this.processTimestamps(r)}await this.finalizeFragment(!1)}else for(let r of this.trackDatas)this.processTimestamps(r),await this.finalizeCurrentChunk(r);if(this.fastStart==="in-memory"){d(this.mdat);let r;for(let o=0;o<2;o++){let n=ee(this.trackDatas,this.creationTime),a=this.boxWriter.measureBox(n);r=this.boxWriter.measureBox(this.mdat);let u=this.writer.getPos()+a+r;for(let m of this.finalizedChunks){m.offset=u;for(let{data:l}of m.samples)d(l),u+=l.byteLength,r+=l.byteLength}if(u<2**32)break;r>=2**32&&(this.mdat.largeSize=!0)}let s=ee(this.trackDatas,this.creationTime);this.boxWriter.writeBox(s),this.mdat.size=r,this.boxWriter.writeBox(this.mdat);for(let o of this.finalizedChunks)for(let n of o.samples)d(n.data),this.writer.write(n.data),n.data=null}else if(this.fastStart==="fragmented"){let r=this.writer.getPos(),s=Je(this.trackDatas);this.boxWriter.writeBox(s);let o=this.writer.getPos()-r;this.writer.seek(this.writer.getPos()-4),this.boxWriter.writeU32(o)}else{d(this.mdat),d(this.ftypSize!==null);let r=this.boxWriter.offsets.get(this.mdat);d(r!==void 0);let s=this.writer.getPos()-r;this.mdat.size=s,this.mdat.largeSize=s>=2**32,this.boxWriter.patchBox(this.mdat);let o=ee(this.trackDatas,this.creationTime);if(typeof this.fastStart=="object"){this.writer.seek(this.ftypSize),this.boxWriter.writeBox(o);let n=r-this.writer.getPos();this.boxWriter.writeBox(Ye(n))}else this.boxWriter.writeBox(o)}e()}};var re=class{constructor(i){this.value=i}},X=class{constructor(i){this.value=i}},ie=class{constructor(i){this.value=i}};var Fe=t=>t<256?1:t<65536?2:t<1<<24?3:t<2**32?4:t<2**40?5:6,Ne=t=>t>=-64&&t<64?1:t>=-8192&&t<8192?2:t>=-(1<<20)&&t<1<<20?3:t>=-(1<<27)&&t<1<<27?4:t>=-(2**34)&&t<2**34?5:6,dt=t=>{if(t<127)return 1;if(t<16383)return 2;if(t<(1<<21)-1)return 3;if(t<(1<<28)-1)return 4;if(t<2**35-1)return 5;if(t<2**42-1)return 6;throw new Error("EBML VINT size not supported "+t)};var He=2**15,mt="https://github.com/Vanilagy/webm-muxer",ft=6,pt=5,gr={avc:"V_MPEG4/ISO/AVC",hevc:"V_MPEGH/ISO/HEVC",vp8:"V_VP8",vp9:"V_VP9",av1:"V_AV1",aac:"A_AAC",opus:"A_OPUS",webvtt:"S_TEXT/WEBVTT"},Sr={video:1,audio:2,subtitle:17},Ce=class extends L{constructor(e,r){super(e);this.timestampsMustStartAtZero=!1;this.helper=new Uint8Array(8);this.helperView=new DataView(this.helper.buffer);this.offsets=new WeakMap;this.dataOffsets=new WeakMap;this.trackDatas=[];this.segment=null;this.segmentInfo=null;this.seekHead=null;this.tracksElement=null;this.segmentDuration=null;this.cues=null;this.currentCluster=null;this.currentClusterMsTimestamp=null;this.trackDatasInCurrentCluster=new Set;this.duration=0;this.writer=e._writer,this.format=r,this.format._options.streamable&&(this.writer.ensureMonotonicity=!0)}writeByte(e){this.helperView.setUint8(0,e),this.writer.write(this.helper.subarray(0,1))}writeFloat32(e){this.helperView.setFloat32(0,e,!1),this.writer.write(this.helper.subarray(0,4))}writeFloat64(e){this.helperView.setFloat64(0,e,!1),this.writer.write(this.helper)}writeUnsignedInt(e,r=Fe(e)){let s=0;switch(r){case 6:this.helperView.setUint8(s++,e/2**40|0);case 5:this.helperView.setUint8(s++,e/2**32|0);case 4:this.helperView.setUint8(s++,e>>24);case 3:this.helperView.setUint8(s++,e>>16);case 2:this.helperView.setUint8(s++,e>>8);case 1:this.helperView.setUint8(s++,e);break;default:throw new Error("Bad UINT size "+r)}this.writer.write(this.helper.subarray(0,s))}writeSignedInt(e,r=Ne(e)){e<0&&(e+=2**(r*8)),this.writeUnsignedInt(e,r)}writeEBMLVarInt(e,r=dt(e)){let s=0;switch(r){case 1:this.helperView.setUint8(s++,128|e);break;case 2:this.helperView.setUint8(s++,64|e>>8),this.helperView.setUint8(s++,e);break;case 3:this.helperView.setUint8(s++,32|e>>16),this.helperView.setUint8(s++,e>>8),this.helperView.setUint8(s++,e);break;case 4:this.helperView.setUint8(s++,16|e>>24),this.helperView.setUint8(s++,e>>16),this.helperView.setUint8(s++,e>>8),this.helperView.setUint8(s++,e);break;case 5:this.helperView.setUint8(s++,8|e/2**32&7),this.helperView.setUint8(s++,e>>24),this.helperView.setUint8(s++,e>>16),this.helperView.setUint8(s++,e>>8),this.helperView.setUint8(s++,e);break;case 6:this.helperView.setUint8(s++,4|e/2**40&3),this.helperView.setUint8(s++,e/2**32|0),this.helperView.setUint8(s++,e>>24),this.helperView.setUint8(s++,e>>16),this.helperView.setUint8(s++,e>>8),this.helperView.setUint8(s++,e);break;default:throw new Error("Bad EBML VINT size "+r)}this.writer.write(this.helper.subarray(0,s))}writeString(e){this.writer.write(new Uint8Array(e.split("").map(r=>r.charCodeAt(0))))}writeEBML(e){if(e!==null){if(e instanceof Uint8Array)this.writer.write(e);else if(Array.isArray(e))for(let r of e)this.writeEBML(r);else if(this.offsets.set(e,this.writer.getPos()),this.writeUnsignedInt(e.id),Array.isArray(e.data)){let r=this.writer.getPos(),s=e.size===-1?1:e.size??4;e.size===-1?this.writeByte(255):this.writer.seek(this.writer.getPos()+s);let o=this.writer.getPos();if(this.dataOffsets.set(e,o),this.writeEBML(e.data),e.size!==-1){let n=this.writer.getPos()-o,a=this.writer.getPos();this.writer.seek(r),this.writeEBMLVarInt(n,s),this.writer.seek(a)}}else if(typeof e.data=="number"){let r=e.size??Fe(e.data);this.writeEBMLVarInt(r),this.writeUnsignedInt(e.data,r)}else if(typeof e.data=="string")this.writeEBMLVarInt(e.data.length),this.writeString(e.data);else if(e.data instanceof Uint8Array)this.writeEBMLVarInt(e.data.byteLength,e.size),this.writer.write(e.data);else if(e.data instanceof re)this.writeEBMLVarInt(4),this.writeFloat32(e.data.value);else if(e.data instanceof X)this.writeEBMLVarInt(8),this.writeFloat64(e.data.value);else if(e.data instanceof ie){let r=e.size??Ne(e.data.value);this.writeEBMLVarInt(r),this.writeSignedInt(e.data.value,r)}}}beforeTrackAdd(e){if(this.format instanceof D)if(e.type==="video"){if(!["vp8","vp9","av1"].includes(e.source._codec))throw new Error("WebM only supports VP8, VP9 and AV1 as video codecs. Switching to MKV removes this restriction.")}else if(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.")}async start(){let e=await this.mutex.acquire();this.writeEBMLHeader(),this.format._options.streamable||this.createSeekHead(),this.createSegmentInfo(),this.createCues(),await this.writer.flush(),e()}writeEBMLHeader(){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.format instanceof D?"webm":"matroska"},{id:17031,data:2},{id:17029,data:2}]};this.writeEBML(e)}createSeekHead(){let e=new Uint8Array([28,83,187,107]),r=new Uint8Array([21,73,169,102]),s=new Uint8Array([22,84,174,107]),o={id:290298740,data:[{id:19899,data:[{id:21419,data:e},{id:21420,size:5,data:0}]},{id:19899,data:[{id:21419,data:r},{id:21420,size:5,data:0}]},{id:19899,data:[{id:21419,data:s},{id:21420,size:5,data:0}]}]};this.seekHead=o}createSegmentInfo(){let e={id:17545,data:new X(0)};this.segmentDuration=e;let r={id:357149030,data:[{id:2807729,data:1e6},{id:19840,data:mt},{id:22337,data:mt},this.format._options.streamable?null:e]};this.segmentInfo=r}createTracks(){let e={id:374648427,data:[]};this.tracksElement=e;for(let r of this.trackDatas)e.data.push({id:174,data:[{id:215,data:r.track.id},{id:29637,data:r.track.id},{id:131,data:Sr[r.type]},{id:134,data:gr[r.track.source._codec]},r.type==="video"?this.videoSpecificTrackInfo(r):null,r.type==="audio"?this.audioSpecificTrackInfo(r):null,r.type==="subtitle"?this.subtitleSpecificTrackInfo(r):null]})}videoSpecificTrackInfo(e){let r=[e.info.decoderConfig.description?{id:25506,data:I(e.info.decoderConfig.description)}:null,e.track.metadata.frameRate?{id:2352003,data:1e9/e.track.metadata.frameRate}:null],s=e.info.decoderConfig.colorSpace,o={id:224,data:[{id:176,data:e.info.width},{id:186,data:e.info.height},ae(s)?{id:21936,data:[{id:21937,data:$[s.matrix]},{id:21946,data:Q[s.transfer]},{id:21947,data:H[s.primaries]},{id:21945,data:s.fullRange?2:1}]}:null]};return r.push(o),r}audioSpecificTrackInfo(e){return[e.info.decoderConfig.description?{id:25506,data:I(e.info.decoderConfig.description)}:null,{id:225,data:[{id:181,data:new re(e.info.sampleRate)},{id:159,data:e.info.numberOfChannels}]}]}subtitleSpecificTrackInfo(e){return[{id:25506,data:y.encode(e.info.config.description)}]}createSegment(){let e={id:408125543,size:this.format._options.streamable?-1:ft,data:[this.format._options.streamable?null:this.seekHead,this.segmentInfo,this.tracksElement]};this.segment=e,this.writeEBML(e)}createCues(){this.cues={id:475249515,data:[]}}get segmentDataOffset(){return d(this.segment),this.dataOffsets.get(this.segment)}getVideoTrackData(e,r){let s=this.trackDatas.find(n=>n.track===e);if(s)return s;be(r),d(r),d(r.decoderConfig),d(r.decoderConfig.codedWidth!==void 0),d(r.decoderConfig.codedHeight!==void 0);let o={track:e,type:"video",info:{width:r.decoderConfig.codedWidth,height:r.decoderConfig.codedHeight,decoderConfig:r.decoderConfig},chunkQueue:[],lastWrittenMsTimestamp:null};return this.trackDatas.push(o),this.trackDatas.sort((n,a)=>n.track.id-a.track.id),o}getAudioTrackData(e,r){let s=this.trackDatas.find(n=>n.track===e);if(s)return s;xe(r),d(r),d(r.decoderConfig);let o={track:e,type:"audio",info:{numberOfChannels:r.decoderConfig.numberOfChannels,sampleRate:r.decoderConfig.sampleRate,decoderConfig:r.decoderConfig},chunkQueue:[],lastWrittenMsTimestamp:null};return this.trackDatas.push(o),this.trackDatas.sort((n,a)=>n.track.id-a.track.id),o}getSubtitleTrackData(e,r){let s=this.trackDatas.find(n=>n.track===e);if(s)return s;Te(r),d(r),d(r.config);let o={track:e,type:"subtitle",info:{config:r.config},chunkQueue:[],lastWrittenMsTimestamp:null};return this.trackDatas.push(o),this.trackDatas.sort((n,a)=>n.track.id-a.track.id),o}async addEncodedVideoChunk(e,r,s){let o=await this.mutex.acquire();try{let n=this.getVideoTrackData(e,s),a=new Uint8Array(r.byteLength);r.copyTo(a);let u=r.type==="key",m=this.validateAndNormalizeTimestamp(n.track,r.timestamp,u),l=this.createInternalChunk(a,m,(r.duration??0)/1e6,r.type);e.source._codec==="vp9"&&this.fixVP9ColorSpace(n,l),n.chunkQueue.push(l),await this.interleaveChunks()}finally{o()}}async addEncodedAudioChunk(e,r,s){let o=await this.mutex.acquire();try{let n=this.getAudioTrackData(e,s),a=new Uint8Array(r.byteLength);r.copyTo(a);let u=r.type,m=u==="key",l=this.validateAndNormalizeTimestamp(n.track,r.timestamp,m),f=this.createInternalChunk(a,l,(r.duration??0)/1e6,u);n.chunkQueue.push(f),await this.interleaveChunks()}finally{o()}}async addSubtitleCue(e,r,s){let o=await this.mutex.acquire();try{let n=this.getSubtitleTrackData(e,s),a=this.validateAndNormalizeTimestamp(n.track,1e6*r.timestamp,!0),u=r.text,m=Math.floor(a*1e3);j.lastIndex=0,u=u.replace(j,M=>{let k=ce(M.slice(1,-1))-m;return`<${le(k)}>`});let l=y.encode(u),f=`${r.settings??""} -${r.identifier??""} -${r.notes??""}`,T=this.createInternalChunk(l,a,r.duration,"key",f.trim()?y.encode(f):null);n.chunkQueue.push(T),await this.interleaveChunks()}finally{o()}}async interleaveChunks(){for(let e of this.output._tracks)if(!e.source._closed&&!this.trackDatas.some(r=>r.track===e))return;e:for(;;){let e=null,r=1/0;for(let o of this.trackDatas){if(o.chunkQueue.length===0&&!o.track.source._closed)break e;o.chunkQueue.length>0&&o.chunkQueue[0].timestamp=2&&s++;let m={rgb:7,bt709:2,bt470bg:1,smpte170m:3}[e.info.decoderConfig.colorSpace.matrix];Qe(r.data,s+0,s+3,m)}createInternalChunk(e,r,s,o,n=null){return{data:e,type:o,timestamp:r,duration:s,additions:n}}writeBlock(e,r){this.segment||(this.createTracks(),this.createSegment());let s=Math.floor(1e3*r.timestamp),o=this.trackDatas.every(f=>{if(f.track.source._closed)return!0;if(e===f)return r.type==="key";let T=f.chunkQueue[0];return T&&T.type==="key"});(!this.currentCluster||o&&s-this.currentClusterMsTimestamp>=1e3)&&this.createNewCluster(s);let n=s-this.currentClusterMsTimestamp;if(n<0)return;if(n>=He)throw new Error(`Current Matroska cluster exceeded its maximum allowed length of ${He} milliseconds. In order to produce a correct WebM file, you must pass in a key frame at least every ${He} milliseconds.`);let u=new Uint8Array(4),m=new DataView(u.buffer);m.setUint8(0,128|e.track.id),m.setInt16(1,n,!1);let l=Math.floor(1e3*r.duration);if(l===0&&!r.additions){m.setUint8(3,+(r.type==="key")<<7);let f={id:163,data:[u,r.data]};this.writeEBML(f)}else{let f={id:160,data:[{id:161,data:[u,r.data]},r.type==="delta"?{id:251,data:new ie(e.lastWrittenMsTimestamp-s)}:null,r.additions?{id:30113,data:[{id:166,data:[{id:165,data:r.additions},{id:238,data:1}]}]}:null,l>0?{id:155,data:l}:null]};this.writeEBML(f)}this.duration=Math.max(this.duration,s+l),e.lastWrittenMsTimestamp=s,this.trackDatasInCurrentCluster.add(e)}createNewCluster(e){this.currentCluster&&!this.format._options.streamable&&this.finalizeCurrentCluster(),this.currentCluster={id:524531317,size:this.format._options.streamable?-1:pt,data:[{id:231,data:e}]},this.writeEBML(this.currentCluster),this.currentClusterMsTimestamp=e,this.trackDatasInCurrentCluster.clear()}finalizeCurrentCluster(){d(this.currentCluster);let e=this.writer.getPos()-this.dataOffsets.get(this.currentCluster),r=this.writer.getPos();this.writer.seek(this.offsets.get(this.currentCluster)+4),this.writeEBMLVarInt(e,pt),this.writer.seek(r);let s=this.offsets.get(this.currentCluster)-this.segmentDataOffset;d(this.cues),this.cues.data.push({id:187,data:[{id:179,data:this.currentClusterMsTimestamp},...[...this.trackDatasInCurrentCluster].map(o=>({id:183,data:[{id:247,data:o.track.id},{id:241,data:s}]}))]})}async onTrackClose(){let e=await this.mutex.acquire();await this.interleaveChunks(),e()}async finalize(){let e=await this.mutex.acquire();this.segment||(this.createTracks(),this.createSegment());for(let r of this.trackDatas)for(;r.chunkQueue.length>0;)this.writeBlock(r,r.chunkQueue.shift());if(!this.format._options.streamable&&this.currentCluster&&this.finalizeCurrentCluster(),d(this.cues),this.writeEBML(this.cues),!this.format._options.streamable){let r=this.writer.getPos(),s=this.writer.getPos()-this.segmentDataOffset;this.writer.seek(this.offsets.get(this.segment)+4),this.writeEBMLVarInt(s,ft),this.segmentDuration.data=new X(this.duration),this.writer.seek(this.offsets.get(this.segmentDuration)),this.writeEBML(this.segmentDuration),this.seekHead.data[0].data[1].data=this.offsets.get(this.cues)-this.segmentDataOffset,this.seekHead.data[1].data[1].data=this.offsets.get(this.segmentInfo)-this.segmentDataOffset,this.seekHead.data[2].data[1].data=this.offsets.get(this.tracksElement)-this.segmentDataOffset,this.writer.seek(this.offsets.get(this.seekHead)),this.writeEBML(this.seekHead),this.writer.seek(r)}e()}};var z=class{},ke=class extends z{constructor(i={}){if(!i||typeof i!="object")throw new TypeError("options must be an object.");if(i.fastStart!==void 0&&![!1,"in-memory","fragmented"].includes(i.fastStart))throw new TypeError('options.fastStart, when provided, must be false, "in-memory", or "fragmented".');super(),this._options=i}_createMuxer(i){return new we(i,this)}},se=class extends z{constructor(i={}){if(!i||typeof i!="object")throw new TypeError("options must be an object.");if(i.streamable!==void 0&&typeof i.streamable!="boolean")throw new TypeError("options.streamable, when provided, must be a boolean.");super(),this._options=i}_createMuxer(i){return new Ce(i,this)}},D=class extends se{};var G=["avc","hevc","vp8","vp9","av1"],Y=["aac","opus"],ge=["webvtt"],W=class{constructor(){this._connectedTrack=null;this._closed=!1;this._offsetTimestamps=!1}_ensureValidDigest(){if(!this._connectedTrack)throw new Error("Cannot call digest without connecting the source to an output track.");if(!this._connectedTrack.output._started)throw new Error("Cannot call digest before output has been started.");if(this._connectedTrack.output._finalizing)throw new Error("Cannot call digest after output has started finalizing.");if(this._closed)throw new Error("Cannot call digest after source has been closed.")}_start(){}async _flush(){}close(){if(this._closed)throw new Error("Source already closed.");if(!this._connectedTrack)throw new Error("Cannot call close without connecting the source to an output track.");if(!this._connectedTrack.output._started)throw new Error("Cannot call close before output has been started.");this._closed=!0,!this._connectedTrack.output._finalizing&&this._connectedTrack.output._muxer.onTrackClose(this._connectedTrack)}},E=class extends W{constructor(e){super();this._connectedTrack=null;if(!G.includes(e))throw new TypeError(`Invalid video codec '${e}'. Must be one of: ${G.join(", ")}.`);this._codec=e}},Se=class extends E{constructor(i){super(i)}digest(i,e){if(!(i instanceof EncodedVideoChunk))throw new TypeError("chunk must be an EncodedVideoChunk.");return this._ensureValidDigest(),this._connectedTrack.output._muxer.addEncodedVideoChunk(this._connectedTrack,i,e)}},yr=5,vr=t=>{if(!t||typeof t!="object")throw new TypeError("Codec config must be an object.");if(!G.includes(t.codec))throw new TypeError(`Invalid video codec '${t.codec}'. Must be one of: ${G.join(", ")}.`);if(!Number.isInteger(t.bitrate)||t.bitrate<=0)throw new TypeError("config.bitrate must be a positive integer.");if(t.latencyMode!==void 0&&!["quality","realtime"].includes(t.latencyMode))throw new TypeError("config.latencyMode, when provided, must be 'quality' or 'realtime'.")},oe=class{constructor(i,e){this.source=i;this.codecConfig=e;this.encoder=null;this.muxer=null;this.lastMultipleOfKeyFrameInterval=-1;this.lastWidth=null;this.lastHeight=null;vr(e)}async digest(i){if(this.source._ensureValidDigest(),this.lastWidth!==null&&this.lastHeight!==null){if(i.codedWidth!==this.lastWidth||i.codedHeight!==this.lastHeight)throw new Error(`Video frame size must remain constant. Expected ${this.lastWidth}x${this.lastHeight}, got ${i.codedWidth}x${i.codedHeight}.`)}else this.lastWidth=i.codedWidth,this.lastHeight=i.codedHeight;this.ensureEncoder(i),d(this.encoder);let e=Math.floor(i.timestamp/1e6/yr);this.encoder.encode(i,{keyFrame:e!==this.lastMultipleOfKeyFrameInterval}),this.lastMultipleOfKeyFrameInterval=e,this.encoder.encodeQueueSize>=4&&await new Promise(r=>this.encoder.addEventListener("dequeue",r,{once:!0})),await this.muxer.mutex.currentPromise}ensureEncoder(i){this.encoder||(this.encoder=new VideoEncoder({output:(e,r)=>void this.muxer.addEncodedVideoChunk(this.source._connectedTrack,e,r),error:e=>console.error("Video encode error:",e)}),this.encoder.configure({codec:at(this.codecConfig.codec,i.codedWidth,i.codedHeight,this.codecConfig.bitrate),width:i.codedWidth,height:i.codedHeight,bitrate:this.codecConfig.bitrate,framerate:this.source._connectedTrack?.metadata.frameRate,latencyMode:this.codecConfig.latencyMode,...ct(this.codecConfig.codec)}),d(this.source._connectedTrack),this.muxer=this.source._connectedTrack.output._muxer)}async flush(){this.encoder&&(await this.encoder.flush(),this.encoder.close())}},ye=class extends E{constructor(i){super(i.codec),this._encoder=new oe(this,i)}digest(i){if(!(i instanceof VideoFrame))throw new TypeError("videoFrame must be a VideoFrame.");return this._encoder.digest(i)}_flush(){return this._encoder.flush()}},ve=class extends E{constructor(i,e){if(!(i instanceof HTMLCanvasElement))throw new TypeError("canvas must be an HTMLCanvasElement.");super(e.codec),this._encoder=new oe(this,e),this._canvas=i}digest(i,e=0){if(!Number.isFinite(i)||i<0)throw new TypeError("timestamp must be a non-negative number.");if(!Number.isFinite(e)||e<0)throw new TypeError("duration must be a non-negative number.");let r=new VideoFrame(this._canvas,{timestamp:Math.round(1e6*i),duration:Math.round(1e6*e),alpha:"discard"}),s=this._encoder.digest(r);return r.close(),s}_flush(){return this._encoder.flush()}},Ae=class extends E{constructor(e,r){if(!(e instanceof MediaStreamTrack)||e.kind!=="video")throw new TypeError("track must be a video MediaStreamTrack.");r={...r,latencyMode:"realtime"};super(r.codec);this._abortController=null;this._offsetTimestamps=!0;this._encoder=new oe(this,r),this._track=e}_start(){this._abortController=new AbortController;let e=new MediaStreamTrackProcessor({track:this._track}),r=new WritableStream({write:s=>{this._encoder.digest(s),s.close()}});e.readable.pipeTo(r,{signal:this._abortController.signal}).catch(s=>{s instanceof DOMException&&s.name==="AbortError"||console.error("Pipe error:",s)})}async _flush(){this._abortController&&(this._abortController.abort(),this._abortController=null),await this._encoder.flush()}},O=class extends W{constructor(e){super();this._connectedTrack=null;if(!Y.includes(e))throw new TypeError(`Invalid audio codec '${e}'. Must be one of: ${Y.join(", ")}.`);this._codec=e}},_e=class extends O{constructor(i){super(i)}digest(i,e){if(!(i instanceof EncodedAudioChunk))throw new TypeError("chunk must be an EncodedAudioChunk.");return this._ensureValidDigest(),this._connectedTrack.output._muxer.addEncodedAudioChunk(this._connectedTrack,i,e)}},Ar=t=>{if(!t||typeof t!="object")throw new TypeError("Codec config must be an object.");if(!Y.includes(t.codec))throw new TypeError(`Invalid audio codec '${t.codec}'. Must be one of: ${Y.join(", ")}.`);if(!Number.isInteger(t.bitrate)||t.bitrate<=0)throw new TypeError("config.bitrate must be a positive integer.")},ne=class{constructor(i,e){this.source=i;this.codecConfig=e;this.encoder=null;this.muxer=null;this.lastNumberOfChannels=null;this.lastSampleRate=null;Ar(e)}async digest(i){if(this.source._ensureValidDigest(),this.lastNumberOfChannels!==null&&this.lastSampleRate!==null){if(i.numberOfChannels!==this.lastNumberOfChannels||i.sampleRate!==this.lastSampleRate)throw new Error(`Audio parameters must remain constant. Expected ${this.lastNumberOfChannels} channels at ${this.lastSampleRate} Hz, got ${i.numberOfChannels} channels at ${i.sampleRate} Hz.`)}else this.lastNumberOfChannels=i.numberOfChannels,this.lastSampleRate=i.sampleRate;this.ensureEncoder(i),d(this.encoder),this.encoder.encode(i),this.encoder.encodeQueueSize>=4&&await new Promise(e=>this.encoder.addEventListener("dequeue",e,{once:!0})),await this.muxer.mutex.currentPromise}ensureEncoder(i){this.encoder||(this.encoder=new AudioEncoder({output:(e,r)=>void this.muxer.addEncodedAudioChunk(this.source._connectedTrack,e,r),error:e=>console.error("Audio encode error:",e)}),this.encoder.configure({codec:ut(this.codecConfig.codec,i.numberOfChannels,i.sampleRate),numberOfChannels:i.numberOfChannels,sampleRate:i.sampleRate,bitrate:this.codecConfig.bitrate,...lt(this.codecConfig.codec)}),d(this.source._connectedTrack),this.muxer=this.source._connectedTrack.output._muxer)}async flush(){this.encoder&&(await this.encoder.flush(),this.encoder.close())}},Ee=class extends O{constructor(i){super(i.codec),this._encoder=new ne(this,i)}digest(i){if(!(i instanceof AudioData))throw new TypeError("audioData must be an AudioData.");return this._encoder.digest(i)}_flush(){return this._encoder.flush()}},Oe=class extends O{constructor(e){super(e.codec);this._accumulatedFrameCount=0;this._encoder=new ne(this,e)}digest(e){if(!(e instanceof AudioBuffer))throw new TypeError("audioBuffer must be an AudioBuffer.");let r=e.numberOfChannels,s=e.sampleRate,o=e.length,n=new Float32Array(r*o);for(let m=0;m{this._encoder.digest(s),s.close()}});e.readable.pipeTo(r,{signal:this._abortController.signal}).catch(s=>{s instanceof DOMException&&s.name==="AbortError"||console.error("Pipe error:",s)})}async _flush(){this._abortController&&(this._abortController.abort(),this._abortController=null),await this._encoder.flush()}},R=class extends W{constructor(e){super();this._connectedTrack=null;if(!ge.includes(e))throw new TypeError(`Invalid subtitle codec '${e}'. Must be one of: ${ge.join(", ")}.`);this._codec=e}},Ve=class extends R{constructor(i){super(i),this._parser=new ue({codec:i,output:(e,r)=>this._connectedTrack?.output._muxer.addSubtitleCue(this._connectedTrack,e,r),error:e=>console.error("Subtitle parse error:",e)})}digest(i){if(typeof i!="string")throw new TypeError("text must be a string.");return this._ensureValidDigest(),this._parser.parse(i),this._connectedTrack.output._muxer.mutex.currentPromise}};var ze=class{constructor(i){this._tracks=[];this._started=!1;this._finalizing=!1;this._mutex=new N;if(!i||typeof i!="object")throw new TypeError("options must be an object.");if(!(i.format instanceof z))throw new TypeError("options.format must be an OutputFormat.");if(!(i.target instanceof V))throw new TypeError("options.target must be a Target.");if(i.target._output)throw new Error("Target is already used for another output.");i.target._output=this,this._writer=i.target._createWriter(),this._muxer=i.format._createMuxer(this)}addVideoTrack(i,e={}){if(!(i instanceof E))throw new TypeError("source must be a VideoSource.");if(!e||typeof e!="object")throw new TypeError("metadata must be an object.");if(typeof e.rotation=="number"&&![0,90,180,270].includes(e.rotation))throw new TypeError(`Invalid video rotation: ${e.rotation}. Has to be 0, 90, 180 or 270.`);if(Array.isArray(e.rotation)&&(e.rotation.length!==9||e.rotation.some(r=>!Number.isFinite(r))))throw new TypeError(`Invalid video transformation matrix: ${e.rotation.join()}`);if(e.frameRate!==void 0&&(!Number.isInteger(e.frameRate)||e.frameRate<=0))throw new TypeError(`Invalid video frame rate: ${e.frameRate}. Must be a positive integer.`);this._addTrack("video",i,e)}addAudioTrack(i,e={}){if(!(i instanceof O))throw new TypeError("source must be an AudioSource.");if(!e||typeof e!="object")throw new TypeError("metadata must be an object.");this._addTrack("audio",i,e)}addSubtitleTrack(i,e={}){if(!(i instanceof R))throw new TypeError("source must be a SubtitleSource.");if(!e||typeof e!="object")throw new TypeError("metadata must be an object.");this._addTrack("subtitle",i,e)}_addTrack(i,e,r){if(this._started)throw new Error("Cannot add track after output has started.");if(e._connectedTrack)throw new Error("Source is already used for a track.");let s={id:this._tracks.length+1,output:this,type:i,source:e,metadata:r};this._muxer.beforeTrackAdd(s),this._tracks.push(s),e._connectedTrack=s}async start(){if(this._started)throw new Error("Output already started.");this._started=!0,this._writer.start();let i=await this._mutex.acquire();await this._muxer.start();for(let e of this._tracks)e.source._start();i()}async finalize(){if(!this._started)throw new Error("Cannot finalize before starting.");if(this._finalizing)throw new Error("Cannot call finalize twice.");this._finalizing=!0;let i=await this._mutex.acquire(),e=this._tracks.map(r=>r.source._flush());await Promise.all(e),await this._muxer.finalize(),await this._writer.flush(),await this._writer.finalize(),i()}};return Ct(_r);})(); +`,o);c===-1&&(c=e.length);let u=ge(t[2]),f=ge(t[3])-u,T=e.slice(n,c).trim();e=e.slice(c).trimStart(),re.lastIndex=0;let C={timestamp:u/1e3,duration:f/1e3,text:T,identifier:s,settings:a,notes:i},k={};this.preambleEmitted||(k.config={description:this.preambleText},this.preambleEmitted=!0),this.options.output(C,k)}}},cr=/(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})/,ge=r=>{let e=cr.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])},Te=r=>{let e=Math.floor(r/36e5),t=Math.floor(r%(60*60*1e3)/(60*1e3)),i=Math.floor(r%(60*1e3)/1e3),s=r%1e3;return e.toString().padStart(2,"0")+":"+t.toString().padStart(2,"0")+":"+i.toString().padStart(2,"0")+"."+s.toString().padStart(3,"0")};var ie=class{constructor(e){this.writer=e}helper=new Uint8Array(8);helperView=new DataView(this.helper.buffer);offsets=new WeakMap;writeU32(e){this.helperView.setUint32(0,e,!1),this.writer.write(this.helper.subarray(0,4))}writeU64(e){this.helperView.setUint32(0,Math.floor(e/2**32),!1),this.helperView.setUint32(4,e,!1),this.writer.write(this.helper.subarray(0,8))}writeAscii(e){for(let t=0;t[(r%256+256)%256],p=r=>(E.setUint16(0,r,!1),[h[0],h[1]]),ur=r=>(E.setInt16(0,r,!1),[h[0],h[1]]),_t=r=>(E.setUint32(0,r,!1),[h[1],h[2],h[3]]),m=r=>(E.setUint32(0,r,!1),[h[0],h[1],h[2],h[3]]),At=r=>(E.setInt32(0,r,!1),[h[0],h[1],h[2],h[3]]),N=r=>(E.setUint32(0,Math.floor(r/2**32),!1),E.setUint32(4,r,!1),[h[0],h[1],h[2],h[3],h[4],h[5],h[6],h[7]]),at=r=>(E.setInt16(0,2**8*r,!1),[h[0],h[1]]),A=r=>(E.setInt32(0,2**16*r,!1),[h[0],h[1],h[2],h[3]]),ot=r=>(E.setInt32(0,2**30*r,!1),[h[0],h[1],h[2],h[3]]),nt=(r,e)=>{let t=[],i=r;do{let s=i&127;i>>=7,t.length>0&&(s|=128),t.push(s),e!==void 0&&e--}while(i>0||e);return t.reverse()},S=(r,e=!1)=>{let t=Array(r.length).fill(null).map((i,s)=>r.charCodeAt(s));return e&&t.push(0),t},ct=r=>{let e=null;for(let t of r)(!e||t.timestamp>e.timestamp)&&(e=t);return e},Et=r=>[A(r[0]),A(r[1]),ot(r[2]),A(r[3]),A(r[4]),ot(r[5]),A(r[6]),A(r[7]),ot(r[8])],b=(r,e,t)=>({type:r,contents:e&&new Uint8Array(e.flat(10)),children:t}),g=(r,e,t,i,s)=>b(r,[x(e),_t(t),i??[]],s),It=r=>r.fragmented?b("ftyp",[S("iso5"),m(512),S("iso5"),S("iso6"),S("mp41")]):b("ftyp",[S("isom"),m(512),S("isom"),r.holdsAvc?S("avc1"):[],S("mp41")]),Ce=r=>({type:"mdat",largeSize:r}),Ot=r=>({type:"free",size:r}),se=(r,e,t=!1)=>b("moov",void 0,[dr(e,r),...r.map(i=>lr(i,e)),t?$r(r):null]),dr=(r,e)=>{let t=_(Math.max(0,...e.filter(n=>n.samples.length>0).map(n=>{let a=ct(n.samples);return a.timestamp+a.duration})),xe),i=Math.max(0,...e.map(n=>n.track.id))+1,s=!P(r)||!P(t),o=s?N:m;return g("mvhd",+s,0,[o(r),o(r),m(xe),o(t),A(1),at(1),Array(10).fill(0),Et(it),Array(24).fill(0),m(i)])},lr=(r,e)=>b("trak",void 0,[mr(r,e),fr(r,e)]),mr=(r,e)=>{let t=ct(r.samples),i=_(t?t.timestamp+t.duration:0,xe),s=!P(e)||!P(i),o=s?N:m,n;if(r.type==="video"){let a=r.track.metadata.rotation;n=a===void 0||typeof a=="number"?V(a??0):a}else n=it;return g("tkhd",+s,3,[o(e),o(e),m(r.track.id),m(0),o(i),Array(8).fill(0),p(0),p(r.track.id),at(r.type==="audio"?1:0),p(0),Et(n),A(r.type==="video"?r.info.width:0),A(r.type==="video"?r.info.height:0)])},fr=(r,e)=>b("mdia",void 0,[hr(r,e),kr(r),gr(r)]),hr=(r,e)=>{let t=ct(r.samples),i=_(t?t.timestamp+t.duration:0,r.timescale),s=!P(e)||!P(i),o=s?N:m;return g("mdhd",+s,0,[o(e),o(e),m(r.timescale),o(i),p(21956),p(0)])},pr={video:"vide",audio:"soun",subtitle:"text"},br={video:"VideoHandler",audio:"SoundHandler",subtitle:"TextHandler"},kr=r=>g("hdlr",0,0,[S("mhlr"),S(pr[r.type]),m(0),m(0),m(0),S(br[r.type],!0)]),gr=r=>b("minf",void 0,[wr[r.type](),Sr(),_r(r)]),Tr=()=>g("vmhd",0,1,[p(0),p(0),p(0),p(0)]),xr=()=>g("smhd",0,0,[p(0),p(0)]),Cr=()=>g("nmhd",0,0),wr={video:Tr,audio:xr,subtitle:Cr},Sr=()=>b("dinf",void 0,[yr()]),yr=()=>g("dref",0,0,[m(1)],[vr()]),vr=()=>g("url ",0,1),_r=r=>{let e=r.compositionTimeOffsetTable.length>1||r.compositionTimeOffsetTable.some(t=>t.sampleCompositionTimeOffset!==0);return b("stbl",void 0,[Ar(r),Dr(r),Fr(r),Wr(r),Nr(r),Hr(r),e?Lr(r):null])},Ar=r=>{let e;return r.type==="video"?e=Er(Jr[r.track.source._codec],r):r.type==="audio"?e=Rr(ti[r.track.source._codec],r):r.type==="subtitle"&&(e=Br(ii[r.track.source._codec],r)),l(e),g("stsd",0,0,[m(1)],[e])},Er=(r,e)=>b(r,[Array(6).fill(0),p(1),p(0),p(0),Array(12).fill(0),p(e.info.width),p(e.info.height),m(4718592),m(4718592),m(0),p(1),Array(32).fill(0),p(24),ur(65535)],[ei[e.track.source._codec](e),be(e.info.decoderConfig.colorSpace)?Ir(e):null]),Ir=r=>b("colr",[S("nclx"),p(U[r.info.decoderConfig.colorSpace.primaries]),p(D[r.info.decoderConfig.colorSpace.transfer]),p(F[r.info.decoderConfig.colorSpace.matrix]),x((r.info.decoderConfig.colorSpace.fullRange?1:0)<<7)]),Or=r=>r.info.decoderConfig&&b("avcC",[...B(r.info.decoderConfig.description)]),Vr=r=>r.info.decoderConfig&&b("hvcC",[...B(r.info.decoderConfig.description)]),vt=r=>{if(!r.info.decoderConfig)return null;let e=r.info.decoderConfig;l(e.colorSpace);let t=e.codec.split("."),i=Number(t[1]),s=Number(t[2]),a=(Number(t[3])<<4)+(0<<1)+Number(e.colorSpace.fullRange);return g("vpcC",1,0,[x(i),x(s),x(a),x(2),x(2),x(2),p(0)])},Mr=()=>b("av1C",[129,0,0,0]),Rr=(r,e)=>b(r,[Array(6).fill(0),p(1),p(0),p(0),m(0),p(e.info.numberOfChannels),p(16),p(0),p(0),A(e.info.sampleRate)],[ri[e.track.source._codec](e)]),Pr=r=>{let t=[...B(r.info.decoderConfig.description??new ArrayBuffer(0))];return t=[...x(64),...x(21),..._t(0),...m(0),...m(0),...x(5),...nt(t.length),...t],t=[...p(1),...x(0),...x(4),...nt(t.length),...t,...x(6),...x(1),...x(2)],t=[...x(3),...nt(t.length),...t],g("esds",0,0,t)},zr=r=>{let e=3840,t=0,i=r.info.decoderConfig?.description;if(i){l(i.byteLength>=18);let s=ArrayBuffer.isView(i)?new DataView(i.buffer,i.byteOffset,i.byteLength):new DataView(i);e=s.getUint16(10,!0),t=s.getInt16(14,!0)}return b("dOps",[x(0),x(r.info.numberOfChannels),p(e),m(r.info.sampleRate),at(t),x(0)])},Br=(r,e)=>b(r,[Array(6).fill(0),p(1)],[si[e.track.source._codec](e)]),Ur=r=>b("vttC",[...v.encode(r.info.config.description)]);var Dr=r=>g("stts",0,0,[m(r.timeToSampleTable.length),r.timeToSampleTable.map(e=>[m(e.sampleCount),m(e.sampleDelta)])]),Fr=r=>{if(r.samples.every(t=>t.type==="key"))return null;let e=[...r.samples.entries()].filter(([,t])=>t.type==="key");return g("stss",0,0,[m(e.length),e.map(([t])=>m(t+1))])},Wr=r=>g("stsc",0,0,[m(r.compactlyCodedChunkTable.length),r.compactlyCodedChunkTable.map(e=>[m(e.firstChunk),m(e.samplesPerChunk),m(1)])]),Nr=r=>g("stsz",0,0,[m(0),m(r.samples.length),r.samples.map(e=>m(e.size))]),Hr=r=>r.finalizedChunks.length>0&&y(r.finalizedChunks).offset>=2**32?g("co64",0,0,[m(r.finalizedChunks.length),r.finalizedChunks.map(e=>N(e.offset))]):g("stco",0,0,[m(r.finalizedChunks.length),r.finalizedChunks.map(e=>m(e.offset))]),Lr=r=>g("ctts",0,0,[m(r.compositionTimeOffsetTable.length),r.compositionTimeOffsetTable.map(e=>[m(e.sampleCount),m(e.sampleCompositionTimeOffset)])]),$r=r=>b("mvex",void 0,r.map(Qr)),Qr=r=>g("trex",0,0,[m(r.track.id),m(1),m(0),m(0),m(0)]),ut=(r,e)=>b("moof",void 0,[jr(r),...e.map(Kr)]),jr=r=>g("mfhd",0,0,[m(r)]),Vt=r=>{let e=0,t=0,i=0,s=0,o=r.type==="delta";return t|=+o,o?e|=1:e|=2,e<<24|t<<16|i<<8|s},Kr=r=>b("traf",void 0,[qr(r),Xr(r),Gr(r)]),qr=r=>{l(r.currentChunk);let e=0;e|=8,e|=16,e|=32,e|=131072;let t=r.currentChunk.samples[1]??r.currentChunk.samples[0],i={duration:t.timescaleUnitsToNextSample,size:t.size,flags:Vt(t)};return g("tfhd",0,e,[m(r.track.id),m(i.duration),m(i.size),m(i.flags)])},Xr=r=>(l(r.currentChunk),g("tfdt",1,0,[N(_(r.currentChunk.startTimestamp,r.timescale))])),Gr=r=>{l(r.currentChunk);let e=r.currentChunk.samples.map(w=>w.timescaleUnitsToNextSample),t=r.currentChunk.samples.map(w=>w.size),i=r.currentChunk.samples.map(Vt),s=r.currentChunk.samples.map(w=>_(w.timestamp-w.decodeTimestamp,r.timescale)),o=new Set(e),n=new Set(t),a=new Set(i),c=new Set(s),u=a.size===2&&i[0]!==i[1],d=o.size>1,f=n.size>1,T=!u&&a.size>1,C=c.size>1||[...c].some(w=>w!==0),k=0;return k|=1,k|=4*+u,k|=256*+d,k|=512*+f,k|=1024*+T,k|=2048*+C,g("trun",1,k,[m(r.currentChunk.samples.length),m(r.currentChunk.offset-r.currentChunk.moofOffset||0),u?m(i[0]):[],r.currentChunk.samples.map((w,pe)=>[d?m(e[pe]):[],f?m(t[pe]):[],T?m(i[pe]):[],C?At(s[pe]):[]])])},Mt=r=>b("mfra",void 0,[...r.map(Yr),Zr()]),Yr=(r,e)=>g("tfra",1,0,[m(r.track.id),m(63),m(r.finalizedChunks.length),r.finalizedChunks.map(i=>[N(_(i.startTimestamp,r.timescale)),N(i.moofOffset),m(e+1),m(1),m(1)])]),Zr=()=>g("mfro",0,0,[m(0)]),Rt=()=>b("vtte"),Pt=(r,e,t,i,s)=>b("vttc",void 0,[s!==null?b("vsid",[At(s)]):null,t!==null?b("iden",[...v.encode(t)]):null,e!==null?b("ctim",[...v.encode(Te(e))]):null,i!==null?b("sttg",[...v.encode(i)]):null,b("payl",[...v.encode(r)])]),zt=r=>b("vtta",[...v.encode(r)]),Jr={avc:"avc1",hevc:"hvc1",vp8:"vp08",vp9:"vp09",av1:"av01"},ei={avc:Or,hevc:Vr,vp8:vt,vp9:vt,av1:Mr},ti={aac:"mp4a",opus:"Opus"},ri={aac:Pr,opus:zr},ii={webvtt:"wvtt"},si={webvtt:Ur};var X=class{output;mutex=new K;constructor(e){this.output=e}beforeTrackAdd(e){}onTrackClose(e){}trackTimestampInfo=new WeakMap;validateAndNormalizeTimestamp(e,t,i){let s=t/1e6,o=this.trackTimestampInfo.get(e);if(!o){if(!i)throw new Error("First frame must be a key frame.");if(this.timestampsMustStartAtZero&&s>0)throw new Error(`Timestamps must start at zero (got ${s}s).`);o={timestampOffset:s,maxTimestamp:e.source._offsetTimestamps?0:s,lastKeyFrameTimestamp:e.source._offsetTimestamps?0:s},this.trackTimestampInfo.set(e,o)}if(e.source._offsetTimestamps&&(s-=o.timestampOffset),s<0)throw new Error(`Timestamps must be non-negative (got ${s}s).`);if(si.start-s.start);e.push({start:t[0].start,size:t[0].data.byteLength});for(let i=1;ic.start<=t&&tni){for(let c=0;c=e.written[o+1].start;)e.written[o].end=Math.max(e.written[o].end,e.written[o+1].end),e.written.splice(o+1,1)}createChunk(e){let i={start:Math.floor(e/this.chunkSize)*this.chunkSize,data:new Uint8Array(this.chunkSize),written:[],shouldFlush:!1};return this.chunks.push(i),this.chunks.sort((s,o)=>s.start-o.start),this.chunks.indexOf(i)}queueChunksForFlush(e=!1){l(this.writer);for(let t=0;t{if(r==="avc"){let o=Math.ceil(e/16)*Math.ceil(t/16),n=Bt.find(f=>o<=f.maxMacroblocks&&i<=f.maxBitrate)??y(Bt),a=n?n.level:0,c="64".padStart(2,"0"),u="00",d=a.toString(16).padStart(2,"0");return`avc1.${c}${u}${d}`}else if(r==="hevc"){let s="",n="6",a=e*t,c=Ut.find(d=>a<=d.maxPictureSize&&i<=d.maxBitrate)??y(Ut);return`hev1.${s}1.${n}.${c.tier}${c.level}.B0`}else{if(r==="vp8")return"vp8";if(r==="vp9"){let s="00",o=e*t,n=Dt.find(c=>o<=c.maxPictureSize&&i<=c.maxBitrate)??y(Dt);return`vp09.${s}.${n.level}.08`}else if(r==="av1"){let o=e*t,n=Ft.find(c=>o<=c.maxPictureSize&&i<=c.maxBitrate)??y(Ft);return`av01.0.${n.level.toString().padStart(2,"0")}${n.tier}.08`}}throw new TypeError(`Unhandled codec '${r}'.`)},Nt=(r,e)=>{if(r==="avc"){if(!e||e.byteLength<4)throw new TypeError("AVC description must be at least 4 bytes long.");return`avc1.${wt(e.subarray(1,4))}`}else if(r==="hevc"){if(!e)throw new TypeError("HEVC description must be provided.");let t=new DataView(e.buffer,e.byteOffset,e.byteLength),i="hev1.",s=e[1]>>6&3,o=e[1]&31;i+=["","A","B","C"][s]+o,i+=".";let n=St(t.getUint32(2));i+=n.toString(16),i+=".";let a=e[1]>>5&1,c=e[12];i+=a===0?"L":"H",i+=c,i+=".";let u=[];for(let d=0;d<6;d++){let f=e[d+13];u.push(f)}for(;u[u.length-1]===0;)u.pop();return i+=u.map(d=>d.toString(16)).join("."),i}throw new TypeError(`Unhandled codec '${r}'.`)},Ht=(r,e,t)=>{if(r==="aac")return e>=2&&t<=24e3?"mp4a.40.29":t<=24e3?"mp4a.40.5":"mp4a.40.2";if(r==="opus")return"opus";if(r==="vorbis")return"vorbis";throw new TypeError(`Unhandled codec '${r}'.`)},Lt=(r,e)=>{if(r==="aac"){if(!e||e.byteLength<2)throw new TypeError("AAC description must be at least 2 bytes long.");return`mp4a.40.${e[0]>>3}`}else{if(r==="opus")return"opus";if(r==="vorbis")return"vorbis"}throw new TypeError(`Unhandled codec '${r}'.`)},$t=r=>r==="avc"?{avc:{format:"avc"}}:r==="hevc"?{hevc:{format:"hevc"}}:{},Qt=r=>r==="aac"?{aac:{format:"aac"}}:r==="opus"?{opus:{format:"opus"}}:{},ve=r=>{if(!r)throw new TypeError("Video chunk metadata must be provided.");if(typeof r!="object")throw new TypeError("Video chunk metadata must be an object.");if(!r.decoderConfig)throw new TypeError("Video chunk metadata must include a decoder configuration.");if(typeof r.decoderConfig!="object")throw new TypeError("Video chunk metadata decoder configuration must be an object.");if(typeof r.decoderConfig.codec!="string")throw new TypeError("Video chunk metadata decoder configuration must specify a codec string.");if(!Number.isInteger(r.decoderConfig.codedWidth)||r.decoderConfig.codedWidth<=0)throw new TypeError("Video chunk metadata decoder configuration must specify a valid codedWidth (positive integer).");if(!Number.isInteger(r.decoderConfig.codedHeight)||r.decoderConfig.codedHeight<=0)throw new TypeError("Video chunk metadata decoder configuration must specify a valid codedHeight (positive integer).");if(r.decoderConfig.description!==void 0&&!rt(r.decoderConfig.description))throw new TypeError("Video chunk metadata decoder configuration description, when defined, must be an ArrayBuffer or an ArrayBuffer view.");if(r.decoderConfig.colorSpace!==void 0){let{colorSpace:e}=r.decoderConfig;if(typeof e!="object")throw new TypeError("Video chunk metadata decoder configuration colorSpace, when provided, must be an object.");let t=Object.keys(U);if(e.primaries!=null&&!t.includes(e.primaries))throw new TypeError(`Video chunk metadata decoder configuration colorSpace primaries, when defined, must be one of ${t.join(", ")}.`);let i=Object.keys(D);if(e.transfer!=null&&!i.includes(e.transfer))throw new TypeError(`Video chunk metadata decoder configuration colorSpace transfer, when defined, must be one of ${i.join(", ")}.`);let s=Object.keys(F);if(e.matrix!=null&&!s.includes(e.matrix))throw new TypeError(`Video chunk metadata decoder configuration colorSpace matrix, when defined, must be one of ${s.join(", ")}.`);if(e.fullRange!=null&&typeof e.fullRange!="boolean")throw new TypeError("Video chunk metadata decoder configuration colorSpace fullRange, when defined, must be a boolean.")}if((r.decoderConfig.codec.startsWith("avc1")||r.decoderConfig.codec.startsWith("avc3"))&&!r.decoderConfig.description)throw new TypeError("Video chunk metadata decoder configuration for AVC must include a description, which is expected to be an AVCDecoderConfigurationRecord as specified in ISO 14496-15.");if((r.decoderConfig.codec.startsWith("hev1")||r.decoderConfig.codec.startsWith("hvc1"))&&!r.decoderConfig.description)throw new TypeError("Video chunk metadata decoder configuration for HEVC must include a description, which is expected to be an HEVCDecoderConfigurationRecord as specified in ISO 14496-15.");if((r.decoderConfig.codec==="vp8"||r.decoderConfig.codec.startsWith("vp09"))&&r.decoderConfig.colorSpace===void 0)throw new TypeError("Video chunk metadata decoder configuration for VP8/VP9 must include a colorSpace.")},_e=r=>{if(!r)throw new TypeError("Audio chunk metadata must be provided.");if(typeof r!="object")throw new TypeError("Audio chunk metadata must be an object.");if(!r.decoderConfig)throw new TypeError("Audio chunk metadata must include a decoder configuration.");if(typeof r.decoderConfig!="object")throw new TypeError("Audio chunk metadata decoder configuration must be an object.");if(typeof r.decoderConfig.codec!="string")throw new TypeError("Audio chunk metadata decoder configuration must specify a codec string.");if(!Number.isInteger(r.decoderConfig.sampleRate)||r.decoderConfig.sampleRate<=0)throw new TypeError("Audio chunk metadata decoder configuration must specify a valid sampleRate (positive integer).");if(!Number.isInteger(r.decoderConfig.numberOfChannels)||r.decoderConfig.numberOfChannels<=0)throw new TypeError("Audio chunk metadata decoder configuration must specify a valid numberOfChannels (positive integer).");if(r.decoderConfig.description!==void 0&&!rt(r.decoderConfig.description))throw new TypeError("Audio chunk metadata decoder configuration description, when defined, must be an ArrayBuffer or an ArrayBuffer view.");if(r.decoderConfig.codec.startsWith("mp4a")&&!r.decoderConfig.description)throw new TypeError("Audio chunk metadata decoder configuration for AAC must include a description, which is expected to be an AudioSpecificConfig as specified in ISO 14496-3.");if(r.decoderConfig.codec==="opus"&&r.decoderConfig.description&&r.decoderConfig.description.byteLength<18)throw new TypeError("Invalid decoder description provided for Opus; must be at least 18 bytes long.")},Ae=r=>{if(!r)throw new TypeError("Subtitle metadata must be provided.");if(typeof r!="object")throw new TypeError("Subtitle metadata must be an object.");if(!r.config)throw new TypeError("Subtitle metadata must include a config object.");if(typeof r.config!="object")throw new TypeError("Subtitle metadata config must be an object.");if(typeof r.config.description!="string")throw new TypeError("Subtitle metadata config description must be a string.")};var xe=1e3,ai=2082844800,_=(r,e,t=!0)=>{let i=r*e;return t?Math.round(i):i},Ee=class extends X{timestampsMustStartAtZero=!0;writer;boxWriter;fastStart;auxTarget=new Y;auxWriter=this.auxTarget._createWriter();auxBoxWriter=new ie(this.auxWriter);ftypSize=null;mdat=null;trackDatas=[];creationTime=Math.floor(Date.now()/1e3)+ai;finalizedChunks=[];nextFragmentNumber=1;constructor(e,t){super(e),this.writer=e._writer,this.boxWriter=new ie(this.writer);let i=this.writer instanceof G?"in-memory":!1;this.fastStart=t._options.fastStart??i,(this.fastStart==="in-memory"||this.fastStart==="fragmented")&&(this.writer.ensureMonotonicity=!0)}async start(){let e=await this.mutex.acquire(),t=this.output._tracks.some(i=>i.type==="video"&&i.source._codec==="avc");this.boxWriter.writeBox(It({holdsAvc:t,fragmented:this.fastStart==="fragmented"})),this.ftypSize=this.writer.getPos(),this.fastStart==="in-memory"?this.mdat=Ce(!1):this.fastStart==="fragmented"||(this.mdat=Ce(!0),this.boxWriter.writeBox(this.mdat)),await this.writer.flush(),e()}getVideoTrackData(e,t){let i=this.trackDatas.find(o=>o.track===e);if(i)return i;ve(t),l(t),l(t.decoderConfig),l(t.decoderConfig.codedWidth!==void 0),l(t.decoderConfig.codedHeight!==void 0);let s={track:e,type:"video",info:{width:t.decoderConfig.codedWidth,height:t.decoderConfig.codedHeight,decoderConfig:t.decoderConfig},timescale:e.metadata.frameRate??57600,samples:[],sampleQueue:[],timestampProcessingQueue:[],timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.trackDatas.push(s),this.trackDatas.sort((o,n)=>o.track.id-n.track.id),s}getAudioTrackData(e,t){let i=this.trackDatas.find(o=>o.track===e);if(i)return i;_e(t),l(t),l(t.decoderConfig);let s={track:e,type:"audio",info:{numberOfChannels:t.decoderConfig.numberOfChannels,sampleRate:t.decoderConfig.sampleRate,decoderConfig:t.decoderConfig},timescale:t.decoderConfig.sampleRate,samples:[],sampleQueue:[],timestampProcessingQueue:[],timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.trackDatas.push(s),this.trackDatas.sort((o,n)=>o.track.id-n.track.id),s}getSubtitleTrackData(e,t){let i=this.trackDatas.find(o=>o.track===e);if(i)return i;Ae(t),l(t),l(t.config);let s={track:e,type:"subtitle",info:{config:t.config},timescale:1e3,samples:[],sampleQueue:[],timestampProcessingQueue:[],timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[],lastCueEndTimestamp:0,cueQueue:[],nextSourceId:0,cueToSourceId:new WeakMap};return this.trackDatas.push(s),this.trackDatas.sort((o,n)=>o.track.id-n.track.id),this.validateAndNormalizeTimestamp(e,0,!0),s}async addEncodedVideoChunk(e,t,i){let s=await this.mutex.acquire();try{let o=this.getVideoTrackData(e,i),n=new Uint8Array(t.byteLength);t.copyTo(n);let a=this.validateAndNormalizeTimestamp(o.track,t.timestamp,t.type==="key"),c=this.createSampleForTrack(o,n,a,(t.duration??0)/1e6,t.type);await this.registerSample(o,c)}finally{s()}}async addEncodedAudioChunk(e,t,i){let s=await this.mutex.acquire();try{let o=this.getAudioTrackData(e,i),n=new Uint8Array(t.byteLength);t.copyTo(n);let a=t.type,c=this.validateAndNormalizeTimestamp(o.track,t.timestamp,a==="key"),u=this.createSampleForTrack(o,n,c,(t.duration??0)/1e6,a);await this.registerSample(o,u)}finally{s()}}async addSubtitleCue(e,t,i){let s=await this.mutex.acquire();try{let o=this.getSubtitleTrackData(e,i);this.validateAndNormalizeTimestamp(o.track,1e6*t.timestamp,!0),e.source._codec==="webvtt"&&(o.cueQueue.push(t),await this.processWebVTTCues(o,t.timestamp))}finally{s()}}async processWebVTTCues(e,t){for(;e.cueQueue.length>0;){let i=new Set([]);for(let u of e.cueQueue)l(u.timestamp<=t),l(e.lastCueEndTimestamp<=u.timestamp+u.duration),i.add(Math.max(u.timestamp,e.lastCueEndTimestamp)),i.add(u.timestamp+u.duration);let s=[...i].sort((u,d)=>u-d),o=s[0],n=s[1]??o;if(t=n)break;q.lastIndex=0;let f=q.test(d.text),T=d.timestamp+d.duration,C=e.cueToSourceId.get(d);if(C===void 0&&ni.timestamp).sort((i,s)=>i-s);for(let i=0;i{if(e===n)return t.type==="key";let a=n.sampleQueue[0];return a&&a.type==="key"});s>=1&&o&&(i=!0,await this.finalizeFragment())}else i=s>=.5}i&&(e.currentChunk&&await this.finalizeCurrentChunk(e),e.currentChunk={startTimestamp:t.timestamp,samples:[],offset:null,moofOffset:null}),l(e.currentChunk),e.currentChunk.samples.push(t),e.timestampProcessingQueue.push(t)}async finalizeCurrentChunk(e){if(l(this.fastStart!=="fragmented"),!!e.currentChunk){if(e.finalizedChunks.push(e.currentChunk),this.finalizedChunks.push(e.currentChunk),(e.compactlyCodedChunkTable.length===0||y(e.compactlyCodedChunkTable).samplesPerChunk!==e.currentChunk.samples.length)&&e.compactlyCodedChunkTable.push({firstChunk:e.finalizedChunks.length,samplesPerChunk:e.currentChunk.samples.length}),this.fastStart==="in-memory"){e.currentChunk.offset=0;return}e.currentChunk.offset=this.writer.getPos();for(let t of e.currentChunk.samples)l(t.data),this.writer.write(t.data),t.data=null;await this.writer.flush()}}async interleaveSamples(){l(this.fastStart==="fragmented");for(let e of this.output._tracks)if(!e.source._closed&&!this.trackDatas.some(t=>t.track===e))return;e:for(;;){let e=null,t=1/0;for(let s of this.trackDatas){if(s.sampleQueue.length===0&&!s.track.source._closed)break e;s.sampleQueue.length>0&&s.sampleQueue[0].timestamp=2**32&&(a.largeSize=!0,u=this.boxWriter.measureBox(a)+c),a.size=u,this.boxWriter.writeBox(a)}for(let a of this.trackDatas){a.currentChunk.offset=this.writer.getPos(),a.currentChunk.moofOffset=i;for(let c of a.currentChunk.samples)this.writer.write(c.data),c.data=null}let o=this.writer.getPos();this.writer.seek(this.boxWriter.offsets.get(s));let n=ut(t,this.trackDatas);this.boxWriter.writeBox(n),this.writer.seek(o);for(let a of this.trackDatas)a.finalizedChunks.push(a.currentChunk),this.finalizedChunks.push(a.currentChunk),a.currentChunk=null;e&&await this.writer.flush()}async onTrackClose(e){let t=await this.mutex.acquire();if(e.type==="subtitle"&&e.source._codec==="webvtt"){let i=this.trackDatas.find(s=>s.track===e);i&&await this.processWebVTTCues(i,1/0)}this.fastStart==="fragmented"&&await this.interleaveSamples(),t()}async finalize(){let e=await this.mutex.acquire();for(let t of this.trackDatas)t.type==="subtitle"&&t.track.source._codec==="webvtt"&&await this.processWebVTTCues(t,1/0);if(this.fastStart==="fragmented"){for(let t of this.trackDatas){for(let i of t.sampleQueue)await this.addSampleToTrack(t,i);this.processTimestamps(t)}await this.finalizeFragment(!1)}else for(let t of this.trackDatas)this.processTimestamps(t),await this.finalizeCurrentChunk(t);if(this.fastStart==="in-memory"){l(this.mdat);let t;for(let s=0;s<2;s++){let o=se(this.trackDatas,this.creationTime),n=this.boxWriter.measureBox(o);t=this.boxWriter.measureBox(this.mdat);let a=this.writer.getPos()+n+t;for(let c of this.finalizedChunks){c.offset=a;for(let{data:u}of c.samples)l(u),a+=u.byteLength,t+=u.byteLength}if(a<2**32)break;t>=2**32&&(this.mdat.largeSize=!0)}let i=se(this.trackDatas,this.creationTime);this.boxWriter.writeBox(i),this.mdat.size=t,this.boxWriter.writeBox(this.mdat);for(let s of this.finalizedChunks)for(let o of s.samples)l(o.data),this.writer.write(o.data),o.data=null}else if(this.fastStart==="fragmented"){let t=this.writer.getPos(),i=Mt(this.trackDatas);this.boxWriter.writeBox(i);let s=this.writer.getPos()-t;this.writer.seek(this.writer.getPos()-4),this.boxWriter.writeU32(s)}else{l(this.mdat),l(this.ftypSize!==null);let t=this.boxWriter.offsets.get(this.mdat);l(t!==void 0);let i=this.writer.getPos()-t;this.mdat.size=i,this.mdat.largeSize=i>=2**32,this.boxWriter.patchBox(this.mdat);let s=se(this.trackDatas,this.creationTime);if(typeof this.fastStart=="object"){this.writer.seek(this.ftypSize),this.boxWriter.writeBox(s);let o=t-this.writer.getPos();this.boxWriter.writeBox(Ot(o))}else this.boxWriter.writeBox(s)}e()}};var ae=class{value;constructor(e){this.value=e}},Z=class{value;constructor(e){this.value=e}},ce=class{value;constructor(e){this.value=e}};var dt=r=>r<256?1:r<65536?2:r<1<<24?3:r<2**32?4:r<2**40?5:6,lt=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,jt=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 mt=2**15,Kt="https://github.com/Vanilagy/webm-muxer",qt=6,Xt=5,ci={avc:"V_MPEG4/ISO/AVC",hevc:"V_MPEGH/ISO/HEVC",vp8:"V_VP8",vp9:"V_VP9",av1:"V_AV1",aac:"A_AAC",opus:"A_OPUS",webvtt:"S_TEXT/WEBVTT"},ui={video:1,audio:2,subtitle:17},Ie=class extends X{timestampsMustStartAtZero=!1;writer;format;helper=new Uint8Array(8);helperView=new DataView(this.helper.buffer);offsets=new WeakMap;dataOffsets=new WeakMap;trackDatas=[];segment=null;segmentInfo=null;seekHead=null;tracksElement=null;segmentDuration=null;cues=null;currentCluster=null;currentClusterMsTimestamp=null;trackDatasInCurrentCluster=new Set;duration=0;constructor(e,t){super(e),this.writer=e._writer,this.format=t,this.format._options.streamable&&(this.writer.ensureMonotonicity=!0)}writeByte(e){this.helperView.setUint8(0,e),this.writer.write(this.helper.subarray(0,1))}writeFloat32(e){this.helperView.setFloat32(0,e,!1),this.writer.write(this.helper.subarray(0,4))}writeFloat64(e){this.helperView.setFloat64(0,e,!1),this.writer.write(this.helper)}writeUnsignedInt(e,t=dt(e)){let i=0;switch(t){case 6:this.helperView.setUint8(i++,e/2**40|0);case 5:this.helperView.setUint8(i++,e/2**32|0);case 4:this.helperView.setUint8(i++,e>>24);case 3:this.helperView.setUint8(i++,e>>16);case 2:this.helperView.setUint8(i++,e>>8);case 1:this.helperView.setUint8(i++,e);break;default:throw new Error("Bad UINT size "+t)}this.writer.write(this.helper.subarray(0,i))}writeSignedInt(e,t=lt(e)){e<0&&(e+=2**(t*8)),this.writeUnsignedInt(e,t)}writeEBMLVarInt(e,t=jt(e)){let i=0;switch(t){case 1:this.helperView.setUint8(i++,128|e);break;case 2:this.helperView.setUint8(i++,64|e>>8),this.helperView.setUint8(i++,e);break;case 3:this.helperView.setUint8(i++,32|e>>16),this.helperView.setUint8(i++,e>>8),this.helperView.setUint8(i++,e);break;case 4:this.helperView.setUint8(i++,16|e>>24),this.helperView.setUint8(i++,e>>16),this.helperView.setUint8(i++,e>>8),this.helperView.setUint8(i++,e);break;case 5:this.helperView.setUint8(i++,8|e/2**32&7),this.helperView.setUint8(i++,e>>24),this.helperView.setUint8(i++,e>>16),this.helperView.setUint8(i++,e>>8),this.helperView.setUint8(i++,e);break;case 6:this.helperView.setUint8(i++,4|e/2**40&3),this.helperView.setUint8(i++,e/2**32|0),this.helperView.setUint8(i++,e>>24),this.helperView.setUint8(i++,e>>16),this.helperView.setUint8(i++,e>>8),this.helperView.setUint8(i++,e);break;default:throw new Error("Bad EBML VINT size "+t)}this.writer.write(this.helper.subarray(0,i))}writeString(e){this.writer.write(new Uint8Array(e.split("").map(t=>t.charCodeAt(0))))}writeEBML(e){if(e!==null){if(e instanceof Uint8Array)this.writer.write(e);else if(Array.isArray(e))for(let t of e)this.writeEBML(t);else if(this.offsets.set(e,this.writer.getPos()),this.writeUnsignedInt(e.id),Array.isArray(e.data)){let t=this.writer.getPos(),i=e.size===-1?1:e.size??4;e.size===-1?this.writeByte(255):this.writer.seek(this.writer.getPos()+i);let s=this.writer.getPos();if(this.dataOffsets.set(e,s),this.writeEBML(e.data),e.size!==-1){let o=this.writer.getPos()-s,n=this.writer.getPos();this.writer.seek(t),this.writeEBMLVarInt(o,i),this.writer.seek(n)}}else if(typeof e.data=="number"){let t=e.size??dt(e.data);this.writeEBMLVarInt(t),this.writeUnsignedInt(e.data,t)}else if(typeof e.data=="string")this.writeEBMLVarInt(e.data.length),this.writeString(e.data);else if(e.data instanceof Uint8Array)this.writeEBMLVarInt(e.data.byteLength,e.size),this.writer.write(e.data);else if(e.data instanceof ae)this.writeEBMLVarInt(4),this.writeFloat32(e.data.value);else if(e.data instanceof Z)this.writeEBMLVarInt(8),this.writeFloat64(e.data.value);else if(e.data instanceof ce){let t=e.size??lt(e.data.value);this.writeEBMLVarInt(t),this.writeSignedInt(e.data.value,t)}}}beforeTrackAdd(e){if(this.format instanceof $)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.")}async start(){let e=await this.mutex.acquire();this.writeEBMLHeader(),this.format._options.streamable||this.createSeekHead(),this.createSegmentInfo(),this.createCues(),await this.writer.flush(),e()}writeEBMLHeader(){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.format instanceof $?"webm":"matroska"},{id:17031,data:2},{id:17029,data:2}]};this.writeEBML(e)}createSeekHead(){let e=new Uint8Array([28,83,187,107]),t=new Uint8Array([21,73,169,102]),i=new Uint8Array([22,84,174,107]),s={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.seekHead=s}createSegmentInfo(){let e={id:17545,data:new Z(0)};this.segmentDuration=e;let t={id:357149030,data:[{id:2807729,data:1e6},{id:19840,data:Kt},{id:22337,data:Kt},this.format._options.streamable?null:e]};this.segmentInfo=t}createTracks(){let e={id:374648427,data:[]};this.tracksElement=e;for(let t of this.trackDatas)e.data.push({id:174,data:[{id:215,data:t.track.id},{id:29637,data:t.track.id},{id:131,data:ui[t.type]},{id:134,data:ci[t.track.source._codec]},t.type==="video"?this.videoSpecificTrackInfo(t):null,t.type==="audio"?this.audioSpecificTrackInfo(t):null,t.type==="subtitle"?this.subtitleSpecificTrackInfo(t):null]})}videoSpecificTrackInfo(e){let t=[e.info.decoderConfig.description?{id:25506,data:B(e.info.decoderConfig.description)}:null,e.track.metadata.frameRate?{id:2352003,data:1e9/e.track.metadata.frameRate}:null],i=e.info.decoderConfig.colorSpace,s={id:224,data:[{id:176,data:e.info.width},{id:186,data:e.info.height},be(i)?{id:21936,data:[{id:21937,data:F[i.matrix]},{id:21946,data:D[i.transfer]},{id:21947,data:U[i.primaries]},{id:21945,data:i.fullRange?2:1}]}:null]};return t.push(s),t}audioSpecificTrackInfo(e){return[e.info.decoderConfig.description?{id:25506,data:B(e.info.decoderConfig.description)}:null,{id:225,data:[{id:181,data:new ae(e.info.sampleRate)},{id:159,data:e.info.numberOfChannels}]}]}subtitleSpecificTrackInfo(e){return[{id:25506,data:v.encode(e.info.config.description)}]}createSegment(){let e={id:408125543,size:this.format._options.streamable?-1:qt,data:[this.format._options.streamable?null:this.seekHead,this.segmentInfo,this.tracksElement]};this.segment=e,this.writeEBML(e)}createCues(){this.cues={id:475249515,data:[]}}get segmentDataOffset(){return l(this.segment),this.dataOffsets.get(this.segment)}getVideoTrackData(e,t){let i=this.trackDatas.find(o=>o.track===e);if(i)return i;ve(t),l(t),l(t.decoderConfig),l(t.decoderConfig.codedWidth!==void 0),l(t.decoderConfig.codedHeight!==void 0);let s={track:e,type:"video",info:{width:t.decoderConfig.codedWidth,height:t.decoderConfig.codedHeight,decoderConfig:t.decoderConfig},chunkQueue:[],lastWrittenMsTimestamp:null};return this.trackDatas.push(s),this.trackDatas.sort((o,n)=>o.track.id-n.track.id),s}getAudioTrackData(e,t){let i=this.trackDatas.find(o=>o.track===e);if(i)return i;_e(t),l(t),l(t.decoderConfig);let s={track:e,type:"audio",info:{numberOfChannels:t.decoderConfig.numberOfChannels,sampleRate:t.decoderConfig.sampleRate,decoderConfig:t.decoderConfig},chunkQueue:[],lastWrittenMsTimestamp:null};return this.trackDatas.push(s),this.trackDatas.sort((o,n)=>o.track.id-n.track.id),s}getSubtitleTrackData(e,t){let i=this.trackDatas.find(o=>o.track===e);if(i)return i;Ae(t),l(t),l(t.config);let s={track:e,type:"subtitle",info:{config:t.config},chunkQueue:[],lastWrittenMsTimestamp:null};return this.trackDatas.push(s),this.trackDatas.sort((o,n)=>o.track.id-n.track.id),s}async addEncodedVideoChunk(e,t,i){let s=await this.mutex.acquire();try{let o=this.getVideoTrackData(e,i),n=new Uint8Array(t.byteLength);t.copyTo(n);let a=t.type==="key",c=this.validateAndNormalizeTimestamp(o.track,t.timestamp,a),u=this.createInternalChunk(n,c,(t.duration??0)/1e6,t.type);e.source._codec==="vp9"&&this.fixVP9ColorSpace(o,u),o.chunkQueue.push(u),await this.interleaveChunks()}finally{s()}}async addEncodedAudioChunk(e,t,i){let s=await this.mutex.acquire();try{let o=this.getAudioTrackData(e,i),n=new Uint8Array(t.byteLength);t.copyTo(n);let a=t.type,c=a==="key",u=this.validateAndNormalizeTimestamp(o.track,t.timestamp,c),d=this.createInternalChunk(n,u,(t.duration??0)/1e6,a);o.chunkQueue.push(d),await this.interleaveChunks()}finally{s()}}async addSubtitleCue(e,t,i){let s=await this.mutex.acquire();try{let o=this.getSubtitleTrackData(e,i),n=this.validateAndNormalizeTimestamp(o.track,1e6*t.timestamp,!0),a=t.text,c=Math.floor(n*1e3);q.lastIndex=0,a=a.replace(q,T=>{let k=ge(T.slice(1,-1))-c;return`<${Te(k)}>`});let u=v.encode(a),d=`${t.settings??""} +${t.identifier??""} +${t.notes??""}`,f=this.createInternalChunk(u,n,t.duration,"key",d.trim()?v.encode(d):null);o.chunkQueue.push(f),await this.interleaveChunks()}finally{s()}}async interleaveChunks(){for(let e of this.output._tracks)if(!e.source._closed&&!this.trackDatas.some(t=>t.track===e))return;e:for(;;){let e=null,t=1/0;for(let s of this.trackDatas){if(s.chunkQueue.length===0&&!s.track.source._closed)break e;s.chunkQueue.length>0&&s.chunkQueue[0].timestamp=2&&i++;let c={rgb:7,bt709:2,bt470bg:1,smpte170m:3}[e.info.decoderConfig.colorSpace.matrix];gt(t.data,i+0,i+3,c)}createInternalChunk(e,t,i,s,o=null){return{data:e,type:s,timestamp:t,duration:i,additions:o}}writeBlock(e,t){this.segment||(this.createTracks(),this.createSegment());let i=Math.floor(1e3*t.timestamp),s=this.trackDatas.every(d=>{if(d.track.source._closed)return!0;if(e===d)return t.type==="key";let f=d.chunkQueue[0];return f&&f.type==="key"});(!this.currentCluster||s&&i-this.currentClusterMsTimestamp>=1e3)&&this.createNewCluster(i);let o=i-this.currentClusterMsTimestamp;if(o<0)return;if(o>=mt)throw new Error(`Current Matroska cluster exceeded its maximum allowed length of ${mt} milliseconds. In order to produce a correct WebM file, you must pass in a key frame at least every ${mt} milliseconds.`);let a=new Uint8Array(4),c=new DataView(a.buffer);c.setUint8(0,128|e.track.id),c.setInt16(1,o,!1);let u=Math.floor(1e3*t.duration);if(u===0&&!t.additions){c.setUint8(3,+(t.type==="key")<<7);let d={id:163,data:[a,t.data]};this.writeEBML(d)}else{let d={id:160,data:[{id:161,data:[a,t.data]},t.type==="delta"?{id:251,data:new ce(e.lastWrittenMsTimestamp-i)}:null,t.additions?{id:30113,data:[{id:166,data:[{id:165,data:t.additions},{id:238,data:1}]}]}:null,u>0?{id:155,data:u}:null]};this.writeEBML(d)}this.duration=Math.max(this.duration,i+u),e.lastWrittenMsTimestamp=i,this.trackDatasInCurrentCluster.add(e)}createNewCluster(e){this.currentCluster&&!this.format._options.streamable&&this.finalizeCurrentCluster(),this.currentCluster={id:524531317,size:this.format._options.streamable?-1:Xt,data:[{id:231,data:e}]},this.writeEBML(this.currentCluster),this.currentClusterMsTimestamp=e,this.trackDatasInCurrentCluster.clear()}finalizeCurrentCluster(){l(this.currentCluster);let e=this.writer.getPos()-this.dataOffsets.get(this.currentCluster),t=this.writer.getPos();this.writer.seek(this.offsets.get(this.currentCluster)+4),this.writeEBMLVarInt(e,Xt),this.writer.seek(t);let i=this.offsets.get(this.currentCluster)-this.segmentDataOffset;l(this.cues),this.cues.data.push({id:187,data:[{id:179,data:this.currentClusterMsTimestamp},...[...this.trackDatasInCurrentCluster].map(s=>({id:183,data:[{id:247,data:s.track.id},{id:241,data:i}]}))]})}async onTrackClose(){let e=await this.mutex.acquire();await this.interleaveChunks(),e()}async finalize(){let e=await this.mutex.acquire();this.segment||(this.createTracks(),this.createSegment());for(let t of this.trackDatas)for(;t.chunkQueue.length>0;)this.writeBlock(t,t.chunkQueue.shift());if(!this.format._options.streamable&&this.currentCluster&&this.finalizeCurrentCluster(),l(this.cues),this.writeEBML(this.cues),!this.format._options.streamable){let t=this.writer.getPos(),i=this.writer.getPos()-this.segmentDataOffset;this.writer.seek(this.offsets.get(this.segment)+4),this.writeEBMLVarInt(i,qt),this.segmentDuration.data=new Z(this.duration),this.writer.seek(this.offsets.get(this.segmentDuration)),this.writeEBML(this.segmentDuration),this.seekHead.data[0].data[1].data=this.offsets.get(this.cues)-this.segmentDataOffset,this.seekHead.data[1].data[1].data=this.offsets.get(this.segmentInfo)-this.segmentDataOffset,this.seekHead.data[2].data[1].data=this.offsets.get(this.tracksElement)-this.segmentDataOffset,this.writer.seek(this.offsets.get(this.seekHead)),this.writeEBML(this.seekHead),this.writer.seek(t)}e()}};var R=class{},Oe=class extends R{_options;constructor(e={}){if(!e||typeof e!="object")throw new TypeError("options must be an object.");if(e.fastStart!==void 0&&![!1,"in-memory","fragmented"].includes(e.fastStart))throw new TypeError('options.fastStart, when provided, must be false, "in-memory", or "fragmented".');super(),this._options=e}_createMuxer(e){return new Ee(e,this)}},ue=class extends R{_options;constructor(e={}){if(!e||typeof e!="object")throw new TypeError("options must be an object.");if(e.streamable!==void 0&&typeof e.streamable!="boolean")throw new TypeError("options.streamable, when provided, must be a boolean.");super(),this._options=e}_createMuxer(e){return new Ie(e,this)}},$=class extends ue{};var Q=class{_connectedTrack=null;_closed=!1;_offsetTimestamps=!1;_ensureValidDigest(){if(!this._connectedTrack)throw new Error("Cannot call digest without connecting the source to an output track.");if(!this._connectedTrack.output._started)throw new Error("Cannot call digest before output has been started.");if(this._connectedTrack.output._finalizing)throw new Error("Cannot call digest after output has started finalizing.");if(this._closed)throw new Error("Cannot call digest after source has been closed.")}_start(){}async _flush(){}close(){if(this._closed)throw new Error("Source already closed.");if(!this._connectedTrack)throw new Error("Cannot call close without connecting the source to an output track.");if(!this._connectedTrack.output._started)throw new Error("Cannot call close before output has been started.");this._closed=!0,!this._connectedTrack.output._finalizing&&this._connectedTrack.output._muxer.onTrackClose(this._connectedTrack)}},I=class extends Q{_connectedTrack=null;_codec;constructor(e){if(super(),!H.includes(e))throw new TypeError(`Invalid video codec '${e}'. Must be one of: ${H.join(", ")}.`);this._codec=e}},Ve=class extends I{constructor(e){super(e)}digest(e,t){if(!(e instanceof EncodedVideoChunk))throw new TypeError("chunk must be an EncodedVideoChunk.");return this._ensureValidDigest(),this._connectedTrack.output._muxer.addEncodedVideoChunk(this._connectedTrack,e,t)}},di=5,li=r=>{if(!r||typeof r!="object")throw new TypeError("Codec config must be an object.");if(!H.includes(r.codec))throw new TypeError(`Invalid video codec '${r.codec}'. Must be one of: ${H.join(", ")}.`);if(!Number.isInteger(r.bitrate)||r.bitrate<=0)throw new TypeError("config.bitrate must be a positive integer.");if(r.latencyMode!==void 0&&!["quality","realtime"].includes(r.latencyMode))throw new TypeError("config.latencyMode, when provided, must be 'quality' or 'realtime'.")},de=class{constructor(e,t){this.source=e;this.codecConfig=t;li(t)}encoder=null;muxer=null;lastMultipleOfKeyFrameInterval=-1;lastWidth=null;lastHeight=null;async digest(e){if(this.source._ensureValidDigest(),this.lastWidth!==null&&this.lastHeight!==null){if(e.codedWidth!==this.lastWidth||e.codedHeight!==this.lastHeight)throw new Error(`Video frame size must remain constant. Expected ${this.lastWidth}x${this.lastHeight}, got ${e.codedWidth}x${e.codedHeight}.`)}else this.lastWidth=e.codedWidth,this.lastHeight=e.codedHeight;this.ensureEncoder(e),l(this.encoder);let t=Math.floor(e.timestamp/1e6/di);this.encoder.encode(e,{keyFrame:t!==this.lastMultipleOfKeyFrameInterval}),this.lastMultipleOfKeyFrameInterval=t,this.encoder.encodeQueueSize>=4&&await new Promise(i=>this.encoder.addEventListener("dequeue",i,{once:!0})),await this.muxer.mutex.currentPromise}ensureEncoder(e){this.encoder||(this.encoder=new VideoEncoder({output:(t,i)=>void this.muxer.addEncodedVideoChunk(this.source._connectedTrack,t,i),error:t=>console.error("Video encode error:",t)}),this.encoder.configure({codec:Wt(this.codecConfig.codec,e.codedWidth,e.codedHeight,this.codecConfig.bitrate),width:e.codedWidth,height:e.codedHeight,bitrate:this.codecConfig.bitrate,framerate:this.source._connectedTrack?.metadata.frameRate,latencyMode:this.codecConfig.latencyMode,...$t(this.codecConfig.codec)}),l(this.source._connectedTrack),this.muxer=this.source._connectedTrack.output._muxer)}async flush(){this.encoder&&(await this.encoder.flush(),this.encoder.close())}},Me=class extends I{_encoder;constructor(e){super(e.codec),this._encoder=new de(this,e)}digest(e){if(!(e instanceof VideoFrame))throw new TypeError("videoFrame must be a VideoFrame.");return this._encoder.digest(e)}_flush(){return this._encoder.flush()}},Re=class extends I{_encoder;_canvas;constructor(e,t){if(!(e instanceof HTMLCanvasElement))throw new TypeError("canvas must be an HTMLCanvasElement.");super(t.codec),this._encoder=new de(this,t),this._canvas=e}digest(e,t=0){if(!Number.isFinite(e)||e<0)throw new TypeError("timestamp must be a non-negative number.");if(!Number.isFinite(t)||t<0)throw new TypeError("duration must be a non-negative number.");let i=new VideoFrame(this._canvas,{timestamp:Math.round(1e6*e),duration:Math.round(1e6*t),alpha:"discard"}),s=this._encoder.digest(i);return i.close(),s}_flush(){return this._encoder.flush()}},Pe=class extends I{_encoder;_abortController=null;_track;_offsetTimestamps=!0;constructor(e,t){if(!(e instanceof MediaStreamTrack)||e.kind!=="video")throw new TypeError("track must be a video MediaStreamTrack.");t={...t,latencyMode:"realtime"},super(t.codec),this._encoder=new de(this,t),this._track=e}_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()}},O=class extends Q{_connectedTrack=null;_codec;constructor(e){if(super(),!L.includes(e))throw new TypeError(`Invalid audio codec '${e}'. Must be one of: ${L.join(", ")}.`);this._codec=e}},ze=class extends O{constructor(e){super(e)}digest(e,t){if(!(e instanceof EncodedAudioChunk))throw new TypeError("chunk must be an EncodedAudioChunk.");return this._ensureValidDigest(),this._connectedTrack.output._muxer.addEncodedAudioChunk(this._connectedTrack,e,t)}},mi=r=>{if(!r||typeof r!="object")throw new TypeError("Codec config must be an object.");if(!L.includes(r.codec))throw new TypeError(`Invalid audio codec '${r.codec}'. Must be one of: ${L.join(", ")}.`);if(!Number.isInteger(r.bitrate)||r.bitrate<=0)throw new TypeError("config.bitrate must be a positive integer.")},le=class{constructor(e,t){this.source=e;this.codecConfig=t;mi(t)}encoder=null;muxer=null;lastNumberOfChannels=null;lastSampleRate=null;async digest(e){if(this.source._ensureValidDigest(),this.lastNumberOfChannels!==null&&this.lastSampleRate!==null){if(e.numberOfChannels!==this.lastNumberOfChannels||e.sampleRate!==this.lastSampleRate)throw new Error(`Audio parameters must remain constant. Expected ${this.lastNumberOfChannels} channels at ${this.lastSampleRate} Hz, got ${e.numberOfChannels} channels at ${e.sampleRate} Hz.`)}else this.lastNumberOfChannels=e.numberOfChannels,this.lastSampleRate=e.sampleRate;this.ensureEncoder(e),l(this.encoder),this.encoder.encode(e),this.encoder.encodeQueueSize>=4&&await new Promise(t=>this.encoder.addEventListener("dequeue",t,{once:!0})),await this.muxer.mutex.currentPromise}ensureEncoder(e){this.encoder||(this.encoder=new AudioEncoder({output:(t,i)=>void this.muxer.addEncodedAudioChunk(this.source._connectedTrack,t,i),error:t=>console.error("Audio encode error:",t)}),this.encoder.configure({codec:Ht(this.codecConfig.codec,e.numberOfChannels,e.sampleRate),numberOfChannels:e.numberOfChannels,sampleRate:e.sampleRate,bitrate:this.codecConfig.bitrate,...Qt(this.codecConfig.codec)}),l(this.source._connectedTrack),this.muxer=this.source._connectedTrack.output._muxer)}async flush(){this.encoder&&(await this.encoder.flush(),this.encoder.close())}},Be=class extends O{_encoder;constructor(e){super(e.codec),this._encoder=new le(this,e)}digest(e){if(!(e instanceof AudioData))throw new TypeError("audioData must be an AudioData.");return this._encoder.digest(e)}_flush(){return this._encoder.flush()}},Ue=class extends O{_encoder;_accumulatedFrameCount=0;constructor(e){super(e.codec),this._encoder=new le(this,e)}digest(e){if(!(e instanceof AudioBuffer))throw new TypeError("audioBuffer must be an AudioBuffer.");let t=e.numberOfChannels,i=e.sampleRate,s=e.length,o=new Float32Array(t*s);for(let c=0;c{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()}},j=class extends Q{_connectedTrack=null;_codec;constructor(e){if(super(),!ne.includes(e))throw new TypeError(`Invalid subtitle codec '${e}'. Must be one of: ${ne.join(", ")}.`);this._codec=e}},Fe=class extends j{_parser;constructor(e){super(e),this._parser=new ke({codec:e,output:(t,i)=>this._connectedTrack?.output._muxer.addSubtitleCue(this._connectedTrack,t,i),error:t=>console.error("Subtitle parse error:",t)})}digest(e){if(typeof e!="string")throw new TypeError("text must be a string.");return this._ensureValidDigest(),this._parser.parse(e),this._connectedTrack.output._muxer.mutex.currentPromise}};var We=class{_muxer;_writer;_tracks=[];_started=!1;_finalizing=!1;_mutex=new K;constructor(e){if(!e||typeof e!="object")throw new TypeError("options must be an object.");if(!(e.format instanceof R))throw new TypeError("options.format must be an OutputFormat.");if(!(e.target instanceof M))throw new TypeError("options.target must be a Target.");if(e.target._output)throw new Error("Target is already used for another output.");e.target._output=this,this._writer=e.target._createWriter(),this._muxer=e.format._createMuxer(this)}addVideoTrack(e,t={}){if(!(e instanceof I))throw new TypeError("source must be a VideoSource.");if(!t||typeof t!="object")throw new TypeError("metadata must be an object.");if(typeof t.rotation=="number"&&![0,90,180,270].includes(t.rotation))throw new TypeError(`Invalid video rotation: ${t.rotation}. Has to be 0, 90, 180 or 270.`);if(Array.isArray(t.rotation)&&(t.rotation.length!==9||t.rotation.some(i=>!Number.isFinite(i))))throw new TypeError(`Invalid video transformation matrix: ${t.rotation.join()}`);if(t.frameRate!==void 0&&(!Number.isInteger(t.frameRate)||t.frameRate<=0))throw new TypeError(`Invalid video frame rate: ${t.frameRate}. Must be a positive integer.`);this._addTrack("video",e,t)}addAudioTrack(e,t={}){if(!(e instanceof O))throw new TypeError("source must be an AudioSource.");if(!t||typeof t!="object")throw new TypeError("metadata must be an object.");this._addTrack("audio",e,t)}addSubtitleTrack(e,t={}){if(!(e instanceof j))throw new TypeError("source must be a SubtitleSource.");if(!t||typeof t!="object")throw new TypeError("metadata must be an object.");this._addTrack("subtitle",e,t)}_addTrack(e,t,i){if(this._started)throw new Error("Cannot add track after output has started.");if(t._connectedTrack)throw new Error("Source is already used for a track.");let s={id:this._tracks.length+1,output:this,type:e,source:t,metadata:i};this._muxer.beforeTrackAdd(s),this._tracks.push(s),t._connectedTrack=s}async start(){if(this._started)throw new Error("Output already started.");this._started=!0,this._writer.start();let e=await this._mutex.acquire();await this._muxer.start();for(let t of this._tracks)t.source._start();e()}async finalize(){if(!this._started)throw new Error("Cannot finalize before starting.");if(this._finalizing)throw new Error("Cannot call finalize twice.");this._finalizing=!0;let e=await this._mutex.acquire(),t=this._tracks.map(i=>i.source._flush());await Promise.all(t),await this._muxer.finalize(),await this._writer.flush(),await this._writer.finalize(),e()}};var J=class{},Ne=class extends J{constructor(t){super();this.buffer=t}async _read(t,i){return new Uint8Array(this.buffer,t,i-t)}async _getSize(){return this.buffer.byteLength}},He=class extends J{constructor(t){super();this.blob=t}async _read(t,i){let o=await this.blob.slice(t,i).arrayBuffer();return new Uint8Array(o)}async _getSize(){return this.blob.size}};var ee=class{input;constructor(e){this.input=e}};var Le=class{_backing;constructor(e){this._backing=e}isVideoTrack(){return this instanceof me}isAudioTrack(){return this instanceof fe}getDuration(){return this._backing.getDuration()}},me=class extends Le{_backing;constructor(e){super(e),this._backing=e}getCodec(){return this._backing.getCodec()}getWidth(){return this._backing.getWidth()}getHeight(){return this._backing.getHeight()}getRotation(){return this._backing.getRotation()}getDecoderConfig(){return this._backing.getDecoderConfig()}async getCodecMimeType(){return(await this.getDecoderConfig()).codec}},fe=class extends Le{_backing;constructor(e){super(e),this._backing=e}getCodec(){return this._backing.getCodec()}getNumberOfChannels(){return this._backing.getNumberOfChannels()}getSampleRate(){return this._backing.getSampleRate()}getDecoderConfig(){return this._backing.getDecoderConfig()}async getCodecMimeType(){return(await this.getDecoderConfig()).codec}};var te=class{constructor(e){this.reader=e}pos=0;readRange(e,t){let{view:i,offset:s}=this.reader.getViewAndOffset(e,t);return new Uint8Array(i.buffer,s,t-e)}readU8(){let{view:e,offset:t}=this.reader.getViewAndOffset(this.pos,this.pos+1);return this.pos++,e.getUint8(t)}readU16(){let{view:e,offset:t}=this.reader.getViewAndOffset(this.pos,this.pos+2);return this.pos+=2,e.getUint16(t,!1)}readU24(){let{view:e,offset:t}=this.reader.getViewAndOffset(this.pos,this.pos+3);this.pos+=3;let i=e.getUint16(t,!1),s=e.getUint8(t+2);return i*256+s}readS32(){let{view:e,offset:t}=this.reader.getViewAndOffset(this.pos,this.pos+4);return this.pos+=4,e.getInt32(t,!1)}readU32(){let{view:e,offset:t}=this.reader.getViewAndOffset(this.pos,this.pos+4);return this.pos+=4,e.getUint32(t,!1)}readI32(){let{view:e,offset:t}=this.reader.getViewAndOffset(this.pos,this.pos+4);return this.pos+=4,e.getInt32(t,!1)}readU64(){let e=this.readU32(),t=this.readU32();return e*4294967296+t}readF64(){let{view:e,offset:t}=this.reader.getViewAndOffset(this.pos,this.pos+8);return this.pos+=8,e.getFloat64(t,!1)}readFixed_16_16(){return this.readS32()/65536}readFixed_2_30(){return this.readS32()/1073741824}readAscii(e){let{view:t,offset:i}=this.reader.getViewAndOffset(this.pos,this.pos+e);this.pos+=e;let s="";for(let o=0;oe.inputTrack)}async getMimeType(){await this.readMetadata();let e="video/mp4";if(this.tracks.length>0){let t=await Promise.all(this.tracks.map(s=>s.inputTrack.getCodecMimeType())),i=[...new Set(t)];e+=`; codecs="${i.join(", ")}"`}return e}readMetadata(){return this.metadataPromise??=(async()=>{let e=await this.isobmffReader.reader.getSourceSize();for(;this.isobmffReader.posi.presentationTimestamp-s.presentationTimestamp),e.sampleTable}readContiguousBoxes(e){let t=this.isobmffReader.pos;for(;this.isobmffReader.pos-td.every((f,T)=>f===c[T]));u===-1?s.rotation=0:s.rotation=90*u}break;case"mdhd":{let s=this.currentTrack;l(s);let o=this.isobmffReader.readU8();this.isobmffReader.pos+=3,o===0?(this.isobmffReader.pos+=8,s.timescale=this.isobmffReader.readU32(),s.durationInTimescale=this.isobmffReader.readU32()):o===1&&(this.isobmffReader.pos+=16,s.timescale=this.isobmffReader.readU32(),s.durationInTimescale=this.isobmffReader.readU64())}break;case"hdlr":{let s=this.currentTrack;l(s),this.isobmffReader.pos+=8;let o=this.isobmffReader.readAscii(4);o==="vide"?s.info={type:"video",width:-1,height:-1,codec:null,codecDescription:null,colorSpace:null}:o==="soun"&&(s.info={type:"audio",numberOfChannels:-1,sampleRate:-1,codec:null,codecDescription:null})}break;case"stbl":{let s=this.currentTrack;l(s),s.sampleTableOffset=e,this.readContiguousBoxes(t.contentSize)}break;case"stsd":{let s=this.currentTrack;if(l(s),s.info===null||s.sampleTable)break;let o=this.isobmffReader.readU8();this.isobmffReader.pos+=3;let n=this.isobmffReader.readU32();for(let a=0;a0){if(u===1)this.isobmffReader.pos+=4*4;else if(u===2){this.isobmffReader.pos+=4,f=this.isobmffReader.readF64(),d=this.isobmffReader.readU32(),this.isobmffReader.pos+=4;let T=this.isobmffReader.readU32(),C=this.isobmffReader.readU32(),k=this.isobmffReader.readU32(),w=this.isobmffReader.readU32()}}s.info.numberOfChannels=d,s.info.sampleRate=f,this.readContiguousBoxes(e+c.totalSize-this.isobmffReader.pos)}}}break;case"avcC":{let s=this.currentTrack;l(s&&s.info),s.info.codecDescription=this.isobmffReader.readRange(this.isobmffReader.pos,this.isobmffReader.pos+t.contentSize)}break;case"hvcC":{let s=this.currentTrack;l(s&&s.info),s.info.codecDescription=this.isobmffReader.readRange(this.isobmffReader.pos,this.isobmffReader.pos+t.contentSize)}break;case"colr":{let s=this.currentTrack;if(l(s&&s.info?.type==="video"),this.isobmffReader.readAscii(4)!=="nclx")break;let n=this.isobmffReader.readU16(),a=this.isobmffReader.readU16(),c=this.isobmffReader.readU16(),u=!!(this.isobmffReader.readU8()&128);s.info.colorSpace={primaries:Tt[n],transfer:xt[a],matrix:Ct[c],fullRange:u}}break;case"wave":t.totalSize>8&&this.readContiguousBoxes(t.contentSize);break;case"esds":{let s=this.currentTrack;l(s&&s.info),this.isobmffReader.pos+=4;let o=this.isobmffReader.readU8();l(o===3),this.isobmffReader.readIsomVariableInteger(),this.isobmffReader.pos+=2;let n=this.isobmffReader.readU8(),a=(n&128)!==0,c=(n&64)!==0,u=(n&32)!==0;if(a&&(this.isobmffReader.pos+=2),c){let k=this.isobmffReader.readU8();this.isobmffReader.pos+=k}u&&(this.isobmffReader.pos+=2);let d=this.isobmffReader.readU8();l(d===4),this.isobmffReader.readIsomVariableInteger();let f=this.isobmffReader.readU8();l(f===64),this.isobmffReader.pos+=12;let T=this.isobmffReader.readU8();l(T===5);let C=this.isobmffReader.readIsomVariableInteger();s.info.codecDescription=this.isobmffReader.readRange(this.isobmffReader.pos,this.isobmffReader.pos+C)}break;case"stts":{let s=this.currentTrack;if(l(s),!s.sampleTable)break;this.isobmffReader.pos+=4;let o=this.isobmffReader.readU32(),n=0,a=0;for(let c=0;c{let t=W(r.presentationTimestamps,e,i=>i.presentationTimestamp);return t===-1?-1:r.presentationTimestamps[t].sampleIndex},hi=(r,e)=>{let t=W(r.sampleTimingEntries,e,k=>k.startIndex),i=r.sampleTimingEntries[t];if(!i||i.startIndex+i.count<=e)return null;let o=i.startDecodeTimestamp+(e-i.startIndex)*i.delta,n=W(r.sampleCompositionTimeOffsets,e,k=>k.startIndex),a=r.sampleCompositionTimeOffsets[n];a&&(o+=a.offset);let c=r.sampleSizes[Math.min(e,r.sampleSizes.length-1)],u=W(r.sampleToChunk,e,k=>k.startSampleIndex),d=r.sampleToChunk[u];l(d);let f=d.startChunkIndex+Math.floor((e-d.startSampleIndex)/d.samplesPerChunk),C=r.chunkOffsets[f];if(r.sampleSizes.length===1)C+=c*(e-d.startSampleIndex);else{let k=d.startSampleIndex+(f-d.startChunkIndex)*d.samplesPerChunk;for(let w=k;wk)!==-1:!0}},pi=(r,e)=>{if(!r.keySampleIndices)return e;let t=W(r.keySampleIndices,e,i=>i);return r.keySampleIndices[t]??-1},bi=(r,e)=>{if(!r.keySampleIndices)return e+1;let t=W(r.keySampleIndices,e,i=>i);return r.keySampleIndices[t+1]??-1};var je=class extends ee{};var Ke=class{},pt=class extends Ke{async _canReadInput(e){if(await e._reader.getSourceSize()<8)return!1;await e._reader.loadRange(4,8);let i=new te(e._reader);return i.pos=4,i.readAscii(4)==="ftyp"}_createDemuxer(e){return new $e(e)}},bt=class extends Ke{async _canReadInput(){return!1}_createDemuxer(e){return new je(e)}},he=new pt,Yt=he,Zt=he,qe=new bt,kt=qe,Jt=qe,er=[he,kt];var Xe=4096,Ge=class{constructor(e){this.source=e}loadedSegments=[];sourceSizePromise=null;getSourceSize(){return this.sourceSizePromise?this.sourceSizePromise:this.sourceSizePromise=this.source._getSize()}async loadRange(e,t){let i=Math.floor(e/Xe)*Xe,s=Math.ceil(t/Xe)*Xe;s=Math.min(s,await this.getSourceSize());let o=this.loadedSegments.find(c=>c.start<=i);o&&(i=Math.max(i,o.end));let n=this.loadedSegments.find(c=>c.end>=s);if(n&&(s=Math.min(s,n.start)),i>=s)return;let a=await this.source._read(i,s);this.insertIntoLoadedSegments(i,a)}insertIntoLoadedSegments(e,t){let i={start:e,end:e+t.byteLength,bytes:t,view:new DataView(t.buffer)},s=this.loadedSegments.findLastIndex(d=>d.start<=e);this.loadedSegments.splice(s+1,0,i),(s===-1||this.loadedSegments[s].enda&&this.loadedSegments[a+1].start<=c;)a++,c=Math.max(c,this.loadedSegments[a].end);if(o===a)return;for(let d=o;d<=a;d++){let f=this.loadedSegments[d];if(f.start===n&&f.end===c){this.loadedSegments.splice(d+1,a-d),this.loadedSegments.splice(o,d-o);return}}let u=new Uint8Array(c-n);for(let d=o;d<=a;d++){let f=this.loadedSegments[d];u.set(f.bytes,f.start-n)}this.loadedSegments.splice(o+1,a-o),this.loadedSegments[o].end=c,this.loadedSegments[o].bytes=u,this.loadedSegments[o].view=new DataView(u.buffer)}getViewAndOffset(e,t){let i=this.loadedSegments.find(s=>s.start<=e&&t<=s.end);if(!i)throw new Error(`No segment loaded for range [${e}, ${t}).`);return{view:i.view,offset:i.bytes.byteOffset+e-i.start}}};var Ye=class{_formats;_reader;_demuxerPromise=null;_format=null;constructor(e){this._formats=e.formats,this._reader=new Ge(e.source)}_getDemuxer(){return this._demuxerPromise??=(async()=>{for(let e of this._formats)if(await e._canReadInput(this))return this._format=e,e._createDemuxer(this);throw new Error("Input has an unrecognizable format.")})()}async getFormat(){return await this._getDemuxer(),l(this._format),this._format}async getDuration(){return(await this._getDemuxer()).getDuration()}async getTracks(){return(await this._getDemuxer()).getTracks()}async getVideoTracks(){return(await this.getTracks()).filter(t=>t.isVideoTrack())}async getPrimaryVideoTrack(){return(await this.getTracks()).find(t=>t.isVideoTrack())??null}async getAudioTracks(){return(await this.getTracks()).filter(t=>t.isAudioTrack())}async getPrimaryAudioTrack(){return(await this.getTracks()).find(t=>t.isAudioTrack())??null}async getMimeType(){return(await this._getDemuxer()).getMimeType()}};var Ze=class{constructor(e){this.videoTrack=e}getFirstChunk(){return this.videoTrack._backing.getFirstChunk()}getChunk(e){return this.videoTrack._backing.getChunk(e)}getNextChunk(e){return this.videoTrack._backing.getNextChunk(e)}getKeyChunk(e){return this.videoTrack._backing.getKeyChunk(e)}getNextKeyChunk(e){return this.videoTrack._backing.getNextKeyChunk(e)}async*chunks(e=0){let t=await this.getChunk(e);for(;t;)yield t,t=await this.getNextChunk(t)}},Je=class{constructor(e){this.videoTrack=e}decoderConfig=null;async createDecoder(e){this.decoderConfig||(this.decoderConfig=await this.videoTrack.getDecoderConfig());let t=new VideoDecoder({output:e,error:i=>console.error(i)});return t.configure(this.decoderConfig),t}async getKeyFrame(e){let t=null,i=await this.createDecoder(o=>t=o),s=await this.videoTrack._backing.getKeyChunk(e);return s?(i.decode(s),await i.flush(),i.close(),t):null}async getFrame(e){let t=null,i=await this.createDecoder(a=>{a.timestamp/1e6<=e?(t?.close(),t=a):a.close()}),s=await this.videoTrack._backing.getKeyChunk(e);if(!s)return null;let o=await this.videoTrack._backing.getChunk(e);l(o),i.decode(s);let n=s;for(;n!==o;){let a=await this.videoTrack._backing.getNextChunk(n);l(a),n=a,i.decode(a),i.decodeQueueSize>=10&&await new Promise(c=>i.addEventListener("dequeue",c,{once:!0}))}return await i.flush(),i.close(),t}async*frames(e=0){let t=[],i=!1,s=null,{promise:o,resolve:n}=st(),a=!1,c=await this.createDecoder(f=>{if(a){f.close();return}let T=f.timestamp/1e6;s&&(T>e?(t.push(s),i=!0):s.close()),T>=e&&(t.push(f),i=!0),s=i?null:f,t.length>0&&(n(),{promise:o,resolve:n}=st())}),u=await this.videoTrack._backing.getKeyChunk(e);if(!u)return;let d=!1;(async()=>{let f=u;for(;f&&!a;)c.decode(f),c.decodeQueueSize>=10&&await new Promise(C=>c.addEventListener("dequeue",C,{once:!0})),f=await this.videoTrack._backing.getNextChunk(f);await c.flush(),c.close(),d=!0,n()})();try{for(;;)if(t.length>0)yield t.shift();else if(!d)await o;else break}finally{a=!0}}};return nr(ki);})(); 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 c0a015d..edaf1a2 100644 --- a/dist/metamuxer.min.mjs +++ b/dist/metamuxer.min.mjs @@ -1,9 +1,9 @@ -function d(t){if(!t)throw new Error("Assertion failed.")}var S=t=>t&&t[t.length-1],V=t=>t>=0&&t<2**32,z=(t,i,e)=>{let r=0;for(let s=i;s>a;r<<=1,r|=u}return r},He=(t,i,e,r)=>{for(let s=i;s>e-s-1<t instanceof ArrayBuffer?new Uint8Array(t):new Uint8Array(t.buffer,t.byteOffset,t.byteLength),y=new TextEncoder,R={bt709:1,bt470bg:5,smpte170m:6},F={bt709:1,smpte170m:6,"iec61966-2-1":13},N={rgb:0,bt709:1,bt470bg:5,smpte170m:6},ne=t=>!!t&&!!t.primaries&&!!t.transfer&&!!t.matrix&&t.fullRange!==void 0,Ce=t=>t instanceof ArrayBuffer||typeof SharedArrayBuffer<"u"&&t instanceof SharedArrayBuffer||ArrayBuffer.isView(t)&&!(t instanceof DataView),W=class{constructor(){this.currentPromise=Promise.resolve()}async acquire(){let i,e=new Promise(s=>{i=s}),r=this.currentPromise;return this.currentPromise=e,await r,i}};var X=/(?:(.+?)\n)?((?:\d{2}:)?\d{2}:\d{2}.\d{3})\s+-->\s+((?:\d{2}:)?\d{2}:\d{2}.\d{3})/g,pt=/^WEBVTT(.|\n)*?\n{2}/,H=/<(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})>/g,ae=class{constructor(i){this.preambleText=null;this.preambleEmitted=!1;this.options=i}parse(i){i=i.replaceAll(`\r +function l(r){if(!r)throw new Error("Assertion failed.")}var y=r=>r&&r[r.length-1],M=r=>r>=0&&r<2**32,R=(r,e,t)=>{let i=0;for(let s=e;s>a;i<<=1,i|=c}return i},bt=(r,e,t,i)=>{for(let s=e;s>t-s-1<r instanceof ArrayBuffer?new Uint8Array(r):new Uint8Array(r.buffer,r.byteOffset,r.byteLength),v=new TextEncoder,Be=r=>Object.fromEntries(Object.entries(r).map(([e,t])=>[t,e])),z={bt709:1,bt470bg:5,smpte170m:6},kt=Be(z),B={bt709:1,smpte170m:6,"iec61966-2-1":13},gt=Be(B),U={rgb:0,bt709:1,bt470bg:5,smpte170m:6},Tt=Be(U),fe=r=>!!r&&!!r.primaries&&!!r.transfer&&!!r.matrix&&r.fullRange!==void 0,Ue=r=>r instanceof ArrayBuffer||typeof SharedArrayBuffer<"u"&&r instanceof SharedArrayBuffer||ArrayBuffer.isView(r)&&!(r instanceof DataView),H=class{currentPromise=Promise.resolve();async acquire(){let e,t=new Promise(s=>{e=s}),i=this.currentPromise;return this.currentPromise=t,await i,e}},V=r=>{let e=r*(Math.PI/180),t=Math.cos(e),i=Math.sin(e);return[t,i,0,-i,t,0,0,0,1]},De=V(0),xt=r=>[...r].map(e=>e.toString(16).padStart(2,"0")).join(""),Ct=r=>(r=r>>1&1431655765|(r&1431655765)<<1,r=r>>2&858993459|(r&858993459)<<2,r=r>>4&252645135|(r&252645135)<<4,r=r>>8&16711935|(r&16711935)<<8,r=r>>16&65535|(r&65535)<<16,r>>>0),wt=(r,e,t)=>{let i=0,s=r.length-1,o=-1;for(;i<=s;){let n=i+s>>1,a=t(r[n]);a===e?(o=n,s=n-1):a{let i=-1,s=0,o=r.length-1;for(;s<=o;){let n=s+(o-s+1)/2|0;t(r[n])<=e?(i=n,s=n+1):o=n-1}return i},Fe=()=>{let r,e;return{promise:new Promise((i,s)=>{r=i,e=s}),resolve:r,reject:e}};var ee=/(?:(.+?)\n)?((?:\d{2}:)?\d{2}:\d{2}.\d{3})\s+-->\s+((?:\d{2}:)?\d{2}:\d{2}.\d{3})/g,Gt=/^WEBVTT(.|\n)*?\n{2}/,L=/<(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})>/g,he=class{options;preambleText=null;preambleEmitted=!1;constructor(e){this.options=e}parse(e){e=e.replaceAll(`\r `,` `).replaceAll("\r",` -`),X.lastIndex=0;let e;if(!this.preambleText){if(!pt.test(i)){let s=new Error("WebVTT preamble incorrect.");throw this.options.error(s),s}e=X.exec(i);let r=i.slice(0,e?.index??i.length).trimEnd();if(!r){let s=new Error("No WebVTT preamble provided.");throw this.options.error(s),s}this.preambleText=r,e&&(i=i.slice(e.index),X.lastIndex=0)}for(;e=X.exec(i);){let r=i.slice(0,e.index),s=e[1],o=e.index+e[0].length,n=i.indexOf(` -`,o)+1,a=i.slice(o,n).trim(),u=i.indexOf(` +`),ee.lastIndex=0;let t;if(!this.preambleText){if(!Gt.test(e)){let s=new Error("WebVTT preamble incorrect.");throw this.options.error(s),s}t=ee.exec(e);let i=e.slice(0,t?.index??e.length).trimEnd();if(!i){let s=new Error("No WebVTT preamble provided.");throw this.options.error(s),s}this.preambleText=i,t&&(e=e.slice(t.index),ee.lastIndex=0)}for(;t=ee.exec(e);){let i=e.slice(0,t.index),s=t[1],o=t.index+t[0].length,n=e.indexOf(` +`,o)+1,a=e.slice(o,n).trim(),c=e.indexOf(` -`,o);u===-1&&(u=i.length);let m=ue(e[2]),f=ue(e[3])-m,T=i.slice(n,u).trim();i=i.slice(u).trimStart(),X.lastIndex=0;let E={timestamp:m/1e3,duration:f/1e3,text:T,identifier:s,settings:a,notes:r},C={};this.preambleEmitted||(C.config={description:this.preambleText},this.preambleEmitted=!0),this.options.output(E,C)}}},ht=/(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})/,ue=t=>{let i=ht.exec(t);if(!i)throw new Error("Expected match.");return 60*60*1e3*Number(i[1]||"0")+60*1e3*Number(i[2])+1e3*Number(i[3])+Number(i[4])},ce=t=>{let i=Math.floor(t/36e5),e=Math.floor(t%(60*60*1e3)/(60*1e3)),r=Math.floor(t%(60*1e3)/1e3),s=t%1e3;return i.toString().padStart(2,"0")+":"+e.toString().padStart(2,"0")+":"+r.toString().padStart(2,"0")+"."+s.toString().padStart(3,"0")};var G=class{constructor(i){this.writer=i;this.helper=new Uint8Array(8);this.helperView=new DataView(this.helper.buffer);this.offsets=new WeakMap}writeU32(i){this.helperView.setUint32(0,i,!1),this.writer.write(this.helper.subarray(0,4))}writeU64(i){this.helperView.setUint32(0,Math.floor(i/2**32),!1),this.helperView.setUint32(4,i,!1),this.writer.write(this.helper.subarray(0,8))}writeAscii(i){for(let e=0;e[(t%256+256)%256],h=t=>(_.setUint16(0,t,!1),[p[0],p[1]]),bt=t=>(_.setInt16(0,t,!1),[p[0],p[1]]),$e=t=>(_.setUint32(0,t,!1),[p[1],p[2],p[3]]),c=t=>(_.setUint32(0,t,!1),[p[0],p[1],p[2],p[3]]),je=t=>(_.setInt32(0,t,!1),[p[0],p[1],p[2],p[3]]),B=t=>(_.setUint32(0,Math.floor(t/2**32),!1),_.setUint32(4,t,!1),[p[0],p[1],p[2],p[3],p[4],p[5],p[6],p[7]]),Se=t=>(_.setInt16(0,2**8*t,!1),[p[0],p[1]]),A=t=>(_.setInt32(0,2**16*t,!1),[p[0],p[1],p[2],p[3]]),ke=t=>(_.setInt32(0,2**30*t,!1),[p[0],p[1],p[2],p[3]]),ge=(t,i)=>{let e=[],r=t;do{let s=r&127;r>>=7,e.length>0&&(s|=128),e.push(s),i!==void 0&&i--}while(r>0||i);return e.reverse()},g=(t,i=!1)=>{let e=Array(t.length).fill(null).map((r,s)=>t.charCodeAt(s));return i&&e.push(0),e},ye=t=>{let i=null;for(let e of t)(!i||e.timestamp>i.timestamp)&&(i=e);return i},Le=t=>{let i=t*(Math.PI/180),e=Math.cos(i),r=Math.sin(i);return[e,r,0,-r,e,0,0,0,1]},qe=Le(0),Ke=t=>[A(t[0]),A(t[1]),ke(t[2]),A(t[3]),A(t[4]),ke(t[5]),A(t[6]),A(t[7]),ke(t[8])],b=(t,i,e)=>({type:t,contents:i&&new Uint8Array(i.flat(10)),children:e}),x=(t,i,e,r,s)=>b(t,[w(i),$e(e),r??[]],s),Xe=t=>t.fragmented?b("ftyp",[g("iso5"),c(512),g("iso5"),g("iso6"),g("mp41")]):b("ftyp",[g("isom"),c(512),g("isom"),t.holdsAvc?g("avc1"):[],g("mp41")]),de=t=>({type:"mdat",largeSize:t}),Ge=t=>({type:"free",size:t}),Y=(t,i,e=!1)=>b("moov",void 0,[xt(i,t),...t.map(r=>Tt(r,i)),e?Yt(t):null]),xt=(t,i)=>{let e=v(Math.max(0,...i.filter(n=>n.samples.length>0).map(n=>{let a=ye(n.samples);return a.timestamp+a.duration})),le),r=Math.max(0,...i.map(n=>n.track.id))+1,s=!V(t)||!V(e),o=s?B:c;return x("mvhd",+s,0,[o(t),o(t),c(le),o(e),A(1),Se(1),Array(10).fill(0),Ke(qe),Array(24).fill(0),c(r)])},Tt=(t,i)=>b("trak",void 0,[wt(t,i),Ct(t,i)]),wt=(t,i)=>{let e=ye(t.samples),r=v(e?e.timestamp+e.duration:0,le),s=!V(i)||!V(r),o=s?B:c,n;if(t.type==="video"){let a=t.track.metadata.rotation;n=a===void 0||typeof a=="number"?Le(a??0):a}else n=qe;return x("tkhd",+s,3,[o(i),o(i),c(t.track.id),c(0),o(r),Array(8).fill(0),h(0),h(t.track.id),Se(t.type==="audio"?1:0),h(0),Ke(n),A(t.type==="video"?t.info.width:0),A(t.type==="video"?t.info.height:0)])},Ct=(t,i)=>b("mdia",void 0,[kt(t,i),yt(t),vt(t)]),kt=(t,i)=>{let e=ye(t.samples),r=v(e?e.timestamp+e.duration:0,t.timescale),s=!V(i)||!V(r),o=s?B:c;return x("mdhd",+s,0,[o(i),o(i),c(t.timescale),o(r),h(21956),h(0)])},gt={video:"vide",audio:"soun",subtitle:"text"},St={video:"VideoHandler",audio:"SoundHandler",subtitle:"TextHandler"},yt=t=>x("hdlr",0,0,[g("mhlr"),g(gt[t.type]),c(0),c(0),c(0),g(St[t.type],!0)]),vt=t=>b("minf",void 0,[Ot[t.type](),Mt(),Pt(t)]),At=()=>x("vmhd",0,1,[h(0),h(0),h(0),h(0)]),_t=()=>x("smhd",0,0,[h(0),h(0)]),Et=()=>x("nmhd",0,0),Ot={video:At,audio:_t,subtitle:Et},Mt=()=>b("dinf",void 0,[Vt()]),Vt=()=>x("dref",0,0,[c(1)],[zt()]),zt=()=>x("url ",0,1),Pt=t=>{let i=t.compositionTimeOffsetTable.length>1||t.compositionTimeOffsetTable.some(e=>e.sampleCompositionTimeOffset!==0);return b("stbl",void 0,[Bt(t),jt(t),Lt(t),qt(t),Kt(t),Xt(t),i?Gt(t):null])},Bt=t=>{let i;return t.type==="video"?i=It(nr[t.track.source._codec],t):t.type==="audio"?i=Ft(ur[t.track.source._codec],t):t.type==="subtitle"&&(i=Qt(lr[t.track.source._codec],t)),d(i),x("stsd",0,0,[c(1)],[i])},It=(t,i)=>b(t,[Array(6).fill(0),h(1),h(0),h(0),Array(12).fill(0),h(i.info.width),h(i.info.height),c(4718592),c(4718592),c(0),h(1),Array(32).fill(0),h(24),bt(65535)],[ar[i.track.source._codec](i),ne(i.info.decoderConfig.colorSpace)?Ut(i):null]),Ut=t=>b("colr",[g("nclx"),h(R[t.info.decoderConfig.colorSpace.primaries]),h(F[t.info.decoderConfig.colorSpace.transfer]),h(N[t.info.decoderConfig.colorSpace.matrix]),w((t.info.decoderConfig.colorSpace.fullRange?1:0)<<7)]),Dt=t=>t.info.decoderConfig&&b("avcC",[...P(t.info.decoderConfig.description)]),Wt=t=>t.info.decoderConfig&&b("hvcC",[...P(t.info.decoderConfig.description)]),Qe=t=>{if(!t.info.decoderConfig)return null;let i=t.info.decoderConfig;d(i.colorSpace);let e=i.codec.split("."),r=Number(e[1]),s=Number(e[2]),a=(Number(e[3])<<4)+(0<<1)+Number(i.colorSpace.fullRange);return x("vpcC",1,0,[w(r),w(s),w(a),w(2),w(2),w(2),h(0)])},Rt=()=>b("av1C",[129,0,0,0]),Ft=(t,i)=>b(t,[Array(6).fill(0),h(1),h(0),h(0),c(0),h(i.info.numberOfChannels),h(16),h(0),h(0),A(i.info.sampleRate)],[cr[i.track.source._codec](i)]),Nt=t=>{let e=[...P(t.info.decoderConfig.description??new ArrayBuffer(0))];return e=[...w(64),...w(21),...$e(0),...c(0),...c(0),...w(5),...ge(e.length),...e],e=[...h(1),...w(0),...w(4),...ge(e.length),...e,...w(6),...w(1),...w(2)],e=[...w(3),...ge(e.length),...e],x("esds",0,0,e)},Ht=t=>{let i=3840,e=0,r=t.info.decoderConfig?.description;if(r){d(r.byteLength>=18);let s=ArrayBuffer.isView(r)?new DataView(r.buffer,r.byteOffset,r.byteLength):new DataView(r);i=s.getUint16(10,!0),e=s.getInt16(14,!0)}return b("dOps",[w(0),w(t.info.numberOfChannels),h(i),c(t.info.sampleRate),Se(e),w(0)])},Qt=(t,i)=>b(t,[Array(6).fill(0),h(1)],[dr[i.track.source._codec](i)]),$t=t=>b("vttC",[...y.encode(t.info.config.description)]);var jt=t=>x("stts",0,0,[c(t.timeToSampleTable.length),t.timeToSampleTable.map(i=>[c(i.sampleCount),c(i.sampleDelta)])]),Lt=t=>{if(t.samples.every(e=>e.type==="key"))return null;let i=[...t.samples.entries()].filter(([,e])=>e.type==="key");return x("stss",0,0,[c(i.length),i.map(([e])=>c(e+1))])},qt=t=>x("stsc",0,0,[c(t.compactlyCodedChunkTable.length),t.compactlyCodedChunkTable.map(i=>[c(i.firstChunk),c(i.samplesPerChunk),c(1)])]),Kt=t=>x("stsz",0,0,[c(0),c(t.samples.length),t.samples.map(i=>c(i.size))]),Xt=t=>t.finalizedChunks.length>0&&S(t.finalizedChunks).offset>=2**32?x("co64",0,0,[c(t.finalizedChunks.length),t.finalizedChunks.map(i=>B(i.offset))]):x("stco",0,0,[c(t.finalizedChunks.length),t.finalizedChunks.map(i=>c(i.offset))]),Gt=t=>x("ctts",0,0,[c(t.compositionTimeOffsetTable.length),t.compositionTimeOffsetTable.map(i=>[c(i.sampleCount),c(i.sampleCompositionTimeOffset)])]),Yt=t=>b("mvex",void 0,t.map(Zt)),Zt=t=>x("trex",0,0,[c(t.track.id),c(1),c(0),c(0),c(0)]),ve=(t,i)=>b("moof",void 0,[Jt(t),...i.map(er)]),Jt=t=>x("mfhd",0,0,[c(t)]),Ye=t=>{let i=0,e=0,r=0,s=0,o=t.type==="delta";return e|=+o,o?i|=1:i|=2,i<<24|e<<16|r<<8|s},er=t=>b("traf",void 0,[tr(t),rr(t),ir(t)]),tr=t=>{d(t.currentChunk);let i=0;i|=8,i|=16,i|=32,i|=131072;let e=t.currentChunk.samples[1]??t.currentChunk.samples[0],r={duration:e.timescaleUnitsToNextSample,size:e.size,flags:Ye(e)};return x("tfhd",0,i,[c(t.track.id),c(r.duration),c(r.size),c(r.flags)])},rr=t=>(d(t.currentChunk),x("tfdt",1,0,[B(v(t.currentChunk.startTimestamp,t.timescale))])),ir=t=>{d(t.currentChunk);let i=t.currentChunk.samples.map(k=>k.timescaleUnitsToNextSample),e=t.currentChunk.samples.map(k=>k.size),r=t.currentChunk.samples.map(Ye),s=t.currentChunk.samples.map(k=>v(k.timestamp-k.decodeTimestamp,t.timescale)),o=new Set(i),n=new Set(e),a=new Set(r),u=new Set(s),m=a.size===2&&r[0]!==r[1],l=o.size>1,f=n.size>1,T=!m&&a.size>1,E=u.size>1||[...u].some(k=>k!==0),C=0;return C|=1,C|=4*+m,C|=256*+l,C|=512*+f,C|=1024*+T,C|=2048*+E,x("trun",1,C,[c(t.currentChunk.samples.length),c(t.currentChunk.offset-t.currentChunk.moofOffset||0),m?c(r[0]):[],t.currentChunk.samples.map((k,D)=>[l?c(i[D]):[],f?c(e[D]):[],T?c(r[D]):[],E?je(s[D]):[]])])},Ze=t=>b("mfra",void 0,[...t.map(sr),or()]),sr=(t,i)=>x("tfra",1,0,[c(t.track.id),c(63),c(t.finalizedChunks.length),t.finalizedChunks.map(r=>[B(v(r.startTimestamp,t.timescale)),B(r.moofOffset),c(i+1),c(1),c(1)])]),or=()=>x("mfro",0,0,[c(0)]),Je=()=>b("vtte"),et=(t,i,e,r,s)=>b("vttc",void 0,[s!==null?b("vsid",[je(s)]):null,e!==null?b("iden",[...y.encode(e)]):null,i!==null?b("ctim",[...y.encode(ce(i))]):null,r!==null?b("sttg",[...y.encode(r)]):null,b("payl",[...y.encode(t)])]),tt=t=>b("vtta",[...y.encode(t)]),nr={avc:"avc1",hevc:"hvc1",vp8:"vp08",vp9:"vp09",av1:"av01"},ar={avc:Dt,hevc:Wt,vp8:Qe,vp9:Qe,av1:Rt},ur={aac:"mp4a",opus:"Opus"},cr={aac:Nt,opus:Ht},lr={webvtt:"wvtt"},dr={webvtt:$t};var Q=class{constructor(i){this.mutex=new W;this.trackTimestampInfo=new WeakMap;this.output=i}beforeTrackAdd(i){}onTrackClose(i){}validateAndNormalizeTimestamp(i,e,r){let s=e/1e6,o=this.trackTimestampInfo.get(i);if(!o){if(!r)throw new Error("First frame must be a key frame.");if(this.timestampsMustStartAtZero&&s>0)throw new Error(`Timestamps must start at zero (got ${s}s).`);o={timestampOffset:s,maxTimestamp:i.source._offsetTimestamps?0:s,lastKeyFrameTimestamp:i.source._offsetTimestamps?0:s},this.trackTimestampInfo.set(i,o)}if(i.source._offsetTimestamps&&(s-=o.timestampOffset),s<0)throw new Error(`Timestamps must be non-negative (got ${s}s).`);if(ss.start-o.start);e.push({start:r[0].start,size:r[0].data.byteLength});for(let s=1;sm.start<=r&&rfr){for(let m=0;m=e.written[n+1].start;)e.written[n].end=Math.max(e.written[n].end,e.written[n+1].end),e.written.splice(n+1,1)}createChunk(e){let s={start:Math.floor(e/this.chunkSize)*this.chunkSize,data:new Uint8Array(this.chunkSize),written:[],shouldFlush:!1};return this.chunks.push(s),this.chunks.sort((o,n)=>o.start-n.start),this.chunks.indexOf(s)}queueChunksForFlush(e=!1){d(this.writer);for(let r=0;r{if(t==="avc"){let o=Math.ceil(i/16)*Math.ceil(e/16),n=rt.find(f=>o<=f.maxMacroblocks&&r<=f.maxBitrate)??S(rt),a=n?n.level:0,u="64".padStart(2,"0"),m="00",l=a.toString(16).padStart(2,"0");return`avc1.${u}${m}${l}`}else if(t==="hevc"){let s="",n="6",a=i*e,u=it.find(l=>a<=l.maxPictureSize&&r<=l.maxBitrate)??S(it);return`hev1.${s}1.${n}.${u.tier}${u.level}.B0`}else{if(t==="vp8")return"vp8";if(t==="vp9"){let s="00",o=i*e,n=st.find(u=>o<=u.maxPictureSize&&r<=u.maxBitrate)??S(st);return`vp09.${s}.${n.level}.08`}else if(t==="av1"){let o=i*e,n=ot.find(u=>o<=u.maxPictureSize&&r<=u.maxBitrate)??S(ot);return`av01.0.${n.level.toString().padStart(2,"0")}${n.tier}.08`}}throw new TypeError(`Unhandled codec '${t}'.`)},at=(t,i,e)=>{if(t==="aac")return i>=2&&e<=24e3?"mp4a.40.29":e<=24e3?"mp4a.40.5":"mp4a.40.2";if(t==="opus")return"opus";if(t==="vorbis")return"vorbis";throw new TypeError(`Unhandled codec '${t}'.`)},ut=t=>t==="avc"?{avc:{format:"avc"}}:t==="hevc"?{hevc:{format:"hevc"}}:{},ct=t=>t==="aac"?{aac:{format:"aac"}}:t==="opus"?{opus:{format:"opus"}}:{},pe=t=>{if(!t)throw new TypeError("Video chunk metadata must be provided.");if(typeof t!="object")throw new TypeError("Video chunk metadata must be an object.");if(!t.decoderConfig)throw new TypeError("Video chunk metadata must include a decoder configuration.");if(typeof t.decoderConfig!="object")throw new TypeError("Video chunk metadata decoder configuration must be an object.");if(typeof t.decoderConfig.codec!="string")throw new TypeError("Video chunk metadata decoder configuration must specify a codec string.");if(!Number.isInteger(t.decoderConfig.codedWidth)||t.decoderConfig.codedWidth<=0)throw new TypeError("Video chunk metadata decoder configuration must specify a valid codedWidth (positive integer).");if(!Number.isInteger(t.decoderConfig.codedHeight)||t.decoderConfig.codedHeight<=0)throw new TypeError("Video chunk metadata decoder configuration must specify a valid codedHeight (positive integer).");if(t.decoderConfig.description!==void 0&&!Ce(t.decoderConfig.description))throw new TypeError("Video chunk metadata decoder configuration description, when defined, must be an ArrayBuffer or an ArrayBuffer view.");if(t.decoderConfig.colorSpace!==void 0){let{colorSpace:i}=t.decoderConfig;if(typeof i!="object")throw new TypeError("Video chunk metadata decoder configuration colorSpace, when provided, must be an object.");let e=Object.keys(R);if(i.primaries!=null&&!e.includes(i.primaries))throw new TypeError(`Video chunk metadata decoder configuration colorSpace primaries, when defined, must be one of ${e.join(", ")}.`);let r=Object.keys(F);if(i.transfer!=null&&!r.includes(i.transfer))throw new TypeError(`Video chunk metadata decoder configuration colorSpace transfer, when defined, must be one of ${r.join(", ")}.`);let s=Object.keys(N);if(i.matrix!=null&&!s.includes(i.matrix))throw new TypeError(`Video chunk metadata decoder configuration colorSpace matrix, when defined, must be one of ${s.join(", ")}.`);if(i.fullRange!=null&&typeof i.fullRange!="boolean")throw new TypeError("Video chunk metadata decoder configuration colorSpace fullRange, when defined, must be a boolean.")}if((t.decoderConfig.codec.startsWith("avc1")||t.decoderConfig.codec.startsWith("avc3"))&&!t.decoderConfig.description)throw new TypeError("Video chunk metadata decoder configuration for AVC must include a description, which is expected to be an AVCDecoderConfigurationRecord as specified in ISO 14496-15.");if((t.decoderConfig.codec.startsWith("hev1")||t.decoderConfig.codec.startsWith("hvc1"))&&!t.decoderConfig.description)throw new TypeError("Video chunk metadata decoder configuration for HEVC must include a description, which is expected to be an HEVCDecoderConfigurationRecord as specified in ISO 14496-15.");if((t.decoderConfig.codec==="vp8"||t.decoderConfig.codec.startsWith("vp09"))&&t.decoderConfig.colorSpace===void 0)throw new TypeError("Video chunk metadata decoder configuration for VP8/VP9 must include a colorSpace.")},he=t=>{if(!t)throw new TypeError("Audio chunk metadata must be provided.");if(typeof t!="object")throw new TypeError("Audio chunk metadata must be an object.");if(!t.decoderConfig)throw new TypeError("Audio chunk metadata must include a decoder configuration.");if(typeof t.decoderConfig!="object")throw new TypeError("Audio chunk metadata decoder configuration must be an object.");if(typeof t.decoderConfig.codec!="string")throw new TypeError("Audio chunk metadata decoder configuration must specify a codec string.");if(!Number.isInteger(t.decoderConfig.sampleRate)||t.decoderConfig.sampleRate<=0)throw new TypeError("Audio chunk metadata decoder configuration must specify a valid sampleRate (positive integer).");if(!Number.isInteger(t.decoderConfig.numberOfChannels)||t.decoderConfig.numberOfChannels<=0)throw new TypeError("Audio chunk metadata decoder configuration must specify a valid numberOfChannels (positive integer).");if(t.decoderConfig.description!==void 0&&!Ce(t.decoderConfig.description))throw new TypeError("Audio chunk metadata decoder configuration description, when defined, must be an ArrayBuffer or an ArrayBuffer view.");if(t.decoderConfig.codec.startsWith("mp4a")&&!t.decoderConfig.description)throw new TypeError("Audio chunk metadata decoder configuration for AAC must include a description, which is expected to be an AudioSpecificConfig as specified in ISO 14496-3.");if(t.decoderConfig.codec==="opus"&&t.decoderConfig.description&&t.decoderConfig.description.byteLength<18)throw new TypeError("Invalid decoder description provided for Opus; must be at least 18 bytes long.")},be=t=>{if(!t)throw new TypeError("Subtitle metadata must be provided.");if(typeof t!="object")throw new TypeError("Subtitle metadata must be an object.");if(!t.config)throw new TypeError("Subtitle metadata must include a config object.");if(typeof t.config!="object")throw new TypeError("Subtitle metadata config must be an object.");if(typeof t.config.description!="string")throw new TypeError("Subtitle metadata config description must be a string.")};var le=1e3,pr=2082844800,v=(t,i,e=!0)=>{let r=t*i;return e?Math.round(r):r},xe=class extends Q{constructor(e,r){super(e);this.timestampsMustStartAtZero=!0;this.auxTarget=new J;this.auxWriter=this.auxTarget._createWriter();this.auxBoxWriter=new G(this.auxWriter);this.ftypSize=null;this.mdat=null;this.trackDatas=[];this.creationTime=Math.floor(Date.now()/1e3)+pr;this.finalizedChunks=[];this.nextFragmentNumber=1;this.writer=e._writer,this.boxWriter=new G(this.writer);let s=this.writer instanceof $?"in-memory":!1;this.fastStart=r._options.fastStart??s,(this.fastStart==="in-memory"||this.fastStart==="fragmented")&&(this.writer.ensureMonotonicity=!0)}async start(){let e=await this.mutex.acquire(),r=this.output._tracks.some(s=>s.type==="video"&&s.source._codec==="avc");this.boxWriter.writeBox(Xe({holdsAvc:r,fragmented:this.fastStart==="fragmented"})),this.ftypSize=this.writer.getPos(),this.fastStart==="in-memory"?this.mdat=de(!1):this.fastStart==="fragmented"||(this.mdat=de(!0),this.boxWriter.writeBox(this.mdat)),await this.writer.flush(),e()}getVideoTrackData(e,r){let s=this.trackDatas.find(n=>n.track===e);if(s)return s;pe(r),d(r),d(r.decoderConfig),d(r.decoderConfig.codedWidth!==void 0),d(r.decoderConfig.codedHeight!==void 0);let o={track:e,type:"video",info:{width:r.decoderConfig.codedWidth,height:r.decoderConfig.codedHeight,decoderConfig:r.decoderConfig},timescale:e.metadata.frameRate??57600,samples:[],sampleQueue:[],timestampProcessingQueue:[],timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.trackDatas.push(o),this.trackDatas.sort((n,a)=>n.track.id-a.track.id),o}getAudioTrackData(e,r){let s=this.trackDatas.find(n=>n.track===e);if(s)return s;he(r),d(r),d(r.decoderConfig);let o={track:e,type:"audio",info:{numberOfChannels:r.decoderConfig.numberOfChannels,sampleRate:r.decoderConfig.sampleRate,decoderConfig:r.decoderConfig},timescale:r.decoderConfig.sampleRate,samples:[],sampleQueue:[],timestampProcessingQueue:[],timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.trackDatas.push(o),this.trackDatas.sort((n,a)=>n.track.id-a.track.id),o}getSubtitleTrackData(e,r){let s=this.trackDatas.find(n=>n.track===e);if(s)return s;be(r),d(r),d(r.config);let o={track:e,type:"subtitle",info:{config:r.config},timescale:1e3,samples:[],sampleQueue:[],timestampProcessingQueue:[],timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[],lastCueEndTimestamp:0,cueQueue:[],nextSourceId:0,cueToSourceId:new WeakMap};return this.trackDatas.push(o),this.trackDatas.sort((n,a)=>n.track.id-a.track.id),this.validateAndNormalizeTimestamp(e,0,!0),o}async addEncodedVideoChunk(e,r,s){let o=await this.mutex.acquire();try{let n=this.getVideoTrackData(e,s),a=new Uint8Array(r.byteLength);r.copyTo(a);let u=this.validateAndNormalizeTimestamp(n.track,r.timestamp,r.type==="key"),m=this.createSampleForTrack(n,a,u,(r.duration??0)/1e6,r.type);await this.registerSample(n,m)}finally{o()}}async addEncodedAudioChunk(e,r,s){let o=await this.mutex.acquire();try{let n=this.getAudioTrackData(e,s),a=new Uint8Array(r.byteLength);r.copyTo(a);let u=r.type,m=this.validateAndNormalizeTimestamp(n.track,r.timestamp,u==="key"),l=this.createSampleForTrack(n,a,m,(r.duration??0)/1e6,u);await this.registerSample(n,l)}finally{o()}}async addSubtitleCue(e,r,s){let o=await this.mutex.acquire();try{let n=this.getSubtitleTrackData(e,s);this.validateAndNormalizeTimestamp(n.track,1e6*r.timestamp,!0),e.source._codec==="webvtt"&&(n.cueQueue.push(r),await this.processWebVTTCues(n,r.timestamp))}finally{o()}}async processWebVTTCues(e,r){for(;e.cueQueue.length>0;){let s=new Set([]);for(let l of e.cueQueue)d(l.timestamp<=r),d(e.lastCueEndTimestamp<=l.timestamp+l.duration),s.add(Math.max(l.timestamp,e.lastCueEndTimestamp)),s.add(l.timestamp+l.duration);let o=[...s].sort((l,f)=>l-f),n=o[0],a=o[1]??n;if(r=a)break;H.lastIndex=0;let T=H.test(f.text),E=f.timestamp+f.duration,C=e.cueToSourceId.get(f);if(C===void 0&&as.timestamp).sort((s,o)=>s-o);for(let s=0;s{if(e===a)return r.type==="key";let u=a.sampleQueue[0];return u&&u.type==="key"});o>=1&&n&&(s=!0,await this.finalizeFragment())}else s=o>=.5}s&&(e.currentChunk&&await this.finalizeCurrentChunk(e),e.currentChunk={startTimestamp:r.timestamp,samples:[],offset:null,moofOffset:null}),d(e.currentChunk),e.currentChunk.samples.push(r),e.timestampProcessingQueue.push(r)}async finalizeCurrentChunk(e){if(d(this.fastStart!=="fragmented"),!!e.currentChunk){if(e.finalizedChunks.push(e.currentChunk),this.finalizedChunks.push(e.currentChunk),(e.compactlyCodedChunkTable.length===0||S(e.compactlyCodedChunkTable).samplesPerChunk!==e.currentChunk.samples.length)&&e.compactlyCodedChunkTable.push({firstChunk:e.finalizedChunks.length,samplesPerChunk:e.currentChunk.samples.length}),this.fastStart==="in-memory"){e.currentChunk.offset=0;return}e.currentChunk.offset=this.writer.getPos();for(let r of e.currentChunk.samples)d(r.data),this.writer.write(r.data),r.data=null;await this.writer.flush()}}async interleaveSamples(){d(this.fastStart==="fragmented");for(let e of this.output._tracks)if(!e.source._closed&&!this.trackDatas.some(r=>r.track===e))return;e:for(;;){let e=null,r=1/0;for(let o of this.trackDatas){if(o.sampleQueue.length===0&&!o.track.source._closed)break e;o.sampleQueue.length>0&&o.sampleQueue[0].timestamp=2**32&&(u.largeSize=!0,l=this.boxWriter.measureBox(u)+m),u.size=l,this.boxWriter.writeBox(u)}for(let u of this.trackDatas){u.currentChunk.offset=this.writer.getPos(),u.currentChunk.moofOffset=s;for(let m of u.currentChunk.samples)this.writer.write(m.data),m.data=null}let n=this.writer.getPos();this.writer.seek(this.boxWriter.offsets.get(o));let a=ve(r,this.trackDatas);this.boxWriter.writeBox(a),this.writer.seek(n);for(let u of this.trackDatas)u.finalizedChunks.push(u.currentChunk),this.finalizedChunks.push(u.currentChunk),u.currentChunk=null;e&&await this.writer.flush()}async onTrackClose(e){let r=await this.mutex.acquire();if(e.type==="subtitle"&&e.source._codec==="webvtt"){let s=this.trackDatas.find(o=>o.track===e);s&&await this.processWebVTTCues(s,1/0)}this.fastStart==="fragmented"&&await this.interleaveSamples(),r()}async finalize(){let e=await this.mutex.acquire();for(let r of this.trackDatas)r.type==="subtitle"&&r.track.source._codec==="webvtt"&&await this.processWebVTTCues(r,1/0);if(this.fastStart==="fragmented"){for(let r of this.trackDatas){for(let s of r.sampleQueue)await this.addSampleToTrack(r,s);this.processTimestamps(r)}await this.finalizeFragment(!1)}else for(let r of this.trackDatas)this.processTimestamps(r),await this.finalizeCurrentChunk(r);if(this.fastStart==="in-memory"){d(this.mdat);let r;for(let o=0;o<2;o++){let n=Y(this.trackDatas,this.creationTime),a=this.boxWriter.measureBox(n);r=this.boxWriter.measureBox(this.mdat);let u=this.writer.getPos()+a+r;for(let m of this.finalizedChunks){m.offset=u;for(let{data:l}of m.samples)d(l),u+=l.byteLength,r+=l.byteLength}if(u<2**32)break;r>=2**32&&(this.mdat.largeSize=!0)}let s=Y(this.trackDatas,this.creationTime);this.boxWriter.writeBox(s),this.mdat.size=r,this.boxWriter.writeBox(this.mdat);for(let o of this.finalizedChunks)for(let n of o.samples)d(n.data),this.writer.write(n.data),n.data=null}else if(this.fastStart==="fragmented"){let r=this.writer.getPos(),s=Ze(this.trackDatas);this.boxWriter.writeBox(s);let o=this.writer.getPos()-r;this.writer.seek(this.writer.getPos()-4),this.boxWriter.writeU32(o)}else{d(this.mdat),d(this.ftypSize!==null);let r=this.boxWriter.offsets.get(this.mdat);d(r!==void 0);let s=this.writer.getPos()-r;this.mdat.size=s,this.mdat.largeSize=s>=2**32,this.boxWriter.patchBox(this.mdat);let o=Y(this.trackDatas,this.creationTime);if(typeof this.fastStart=="object"){this.writer.seek(this.ftypSize),this.boxWriter.writeBox(o);let n=r-this.writer.getPos();this.boxWriter.writeBox(Ge(n))}else this.boxWriter.writeBox(o)}e()}};var ee=class{constructor(i){this.value=i}},j=class{constructor(i){this.value=i}},te=class{constructor(i){this.value=i}};var _e=t=>t<256?1:t<65536?2:t<1<<24?3:t<2**32?4:t<2**40?5:6,Ee=t=>t>=-64&&t<64?1:t>=-8192&&t<8192?2:t>=-(1<<20)&&t<1<<20?3:t>=-(1<<27)&&t<1<<27?4:t>=-(2**34)&&t<2**34?5:6,lt=t=>{if(t<127)return 1;if(t<16383)return 2;if(t<(1<<21)-1)return 3;if(t<(1<<28)-1)return 4;if(t<2**35-1)return 5;if(t<2**42-1)return 6;throw new Error("EBML VINT size not supported "+t)};var Oe=2**15,dt="https://github.com/Vanilagy/webm-muxer",mt=6,ft=5,hr={avc:"V_MPEG4/ISO/AVC",hevc:"V_MPEGH/ISO/HEVC",vp8:"V_VP8",vp9:"V_VP9",av1:"V_AV1",aac:"A_AAC",opus:"A_OPUS",webvtt:"S_TEXT/WEBVTT"},br={video:1,audio:2,subtitle:17},Te=class extends Q{constructor(e,r){super(e);this.timestampsMustStartAtZero=!1;this.helper=new Uint8Array(8);this.helperView=new DataView(this.helper.buffer);this.offsets=new WeakMap;this.dataOffsets=new WeakMap;this.trackDatas=[];this.segment=null;this.segmentInfo=null;this.seekHead=null;this.tracksElement=null;this.segmentDuration=null;this.cues=null;this.currentCluster=null;this.currentClusterMsTimestamp=null;this.trackDatasInCurrentCluster=new Set;this.duration=0;this.writer=e._writer,this.format=r,this.format._options.streamable&&(this.writer.ensureMonotonicity=!0)}writeByte(e){this.helperView.setUint8(0,e),this.writer.write(this.helper.subarray(0,1))}writeFloat32(e){this.helperView.setFloat32(0,e,!1),this.writer.write(this.helper.subarray(0,4))}writeFloat64(e){this.helperView.setFloat64(0,e,!1),this.writer.write(this.helper)}writeUnsignedInt(e,r=_e(e)){let s=0;switch(r){case 6:this.helperView.setUint8(s++,e/2**40|0);case 5:this.helperView.setUint8(s++,e/2**32|0);case 4:this.helperView.setUint8(s++,e>>24);case 3:this.helperView.setUint8(s++,e>>16);case 2:this.helperView.setUint8(s++,e>>8);case 1:this.helperView.setUint8(s++,e);break;default:throw new Error("Bad UINT size "+r)}this.writer.write(this.helper.subarray(0,s))}writeSignedInt(e,r=Ee(e)){e<0&&(e+=2**(r*8)),this.writeUnsignedInt(e,r)}writeEBMLVarInt(e,r=lt(e)){let s=0;switch(r){case 1:this.helperView.setUint8(s++,128|e);break;case 2:this.helperView.setUint8(s++,64|e>>8),this.helperView.setUint8(s++,e);break;case 3:this.helperView.setUint8(s++,32|e>>16),this.helperView.setUint8(s++,e>>8),this.helperView.setUint8(s++,e);break;case 4:this.helperView.setUint8(s++,16|e>>24),this.helperView.setUint8(s++,e>>16),this.helperView.setUint8(s++,e>>8),this.helperView.setUint8(s++,e);break;case 5:this.helperView.setUint8(s++,8|e/2**32&7),this.helperView.setUint8(s++,e>>24),this.helperView.setUint8(s++,e>>16),this.helperView.setUint8(s++,e>>8),this.helperView.setUint8(s++,e);break;case 6:this.helperView.setUint8(s++,4|e/2**40&3),this.helperView.setUint8(s++,e/2**32|0),this.helperView.setUint8(s++,e>>24),this.helperView.setUint8(s++,e>>16),this.helperView.setUint8(s++,e>>8),this.helperView.setUint8(s++,e);break;default:throw new Error("Bad EBML VINT size "+r)}this.writer.write(this.helper.subarray(0,s))}writeString(e){this.writer.write(new Uint8Array(e.split("").map(r=>r.charCodeAt(0))))}writeEBML(e){if(e!==null){if(e instanceof Uint8Array)this.writer.write(e);else if(Array.isArray(e))for(let r of e)this.writeEBML(r);else if(this.offsets.set(e,this.writer.getPos()),this.writeUnsignedInt(e.id),Array.isArray(e.data)){let r=this.writer.getPos(),s=e.size===-1?1:e.size??4;e.size===-1?this.writeByte(255):this.writer.seek(this.writer.getPos()+s);let o=this.writer.getPos();if(this.dataOffsets.set(e,o),this.writeEBML(e.data),e.size!==-1){let n=this.writer.getPos()-o,a=this.writer.getPos();this.writer.seek(r),this.writeEBMLVarInt(n,s),this.writer.seek(a)}}else if(typeof e.data=="number"){let r=e.size??_e(e.data);this.writeEBMLVarInt(r),this.writeUnsignedInt(e.data,r)}else if(typeof e.data=="string")this.writeEBMLVarInt(e.data.length),this.writeString(e.data);else if(e.data instanceof Uint8Array)this.writeEBMLVarInt(e.data.byteLength,e.size),this.writer.write(e.data);else if(e.data instanceof ee)this.writeEBMLVarInt(4),this.writeFloat32(e.data.value);else if(e.data instanceof j)this.writeEBMLVarInt(8),this.writeFloat64(e.data.value);else if(e.data instanceof te){let r=e.size??Ee(e.data.value);this.writeEBMLVarInt(r),this.writeSignedInt(e.data.value,r)}}}beforeTrackAdd(e){if(this.format instanceof L)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.")}async start(){let e=await this.mutex.acquire();this.writeEBMLHeader(),this.format._options.streamable||this.createSeekHead(),this.createSegmentInfo(),this.createCues(),await this.writer.flush(),e()}writeEBMLHeader(){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.format instanceof L?"webm":"matroska"},{id:17031,data:2},{id:17029,data:2}]};this.writeEBML(e)}createSeekHead(){let e=new Uint8Array([28,83,187,107]),r=new Uint8Array([21,73,169,102]),s=new Uint8Array([22,84,174,107]),o={id:290298740,data:[{id:19899,data:[{id:21419,data:e},{id:21420,size:5,data:0}]},{id:19899,data:[{id:21419,data:r},{id:21420,size:5,data:0}]},{id:19899,data:[{id:21419,data:s},{id:21420,size:5,data:0}]}]};this.seekHead=o}createSegmentInfo(){let e={id:17545,data:new j(0)};this.segmentDuration=e;let r={id:357149030,data:[{id:2807729,data:1e6},{id:19840,data:dt},{id:22337,data:dt},this.format._options.streamable?null:e]};this.segmentInfo=r}createTracks(){let e={id:374648427,data:[]};this.tracksElement=e;for(let r of this.trackDatas)e.data.push({id:174,data:[{id:215,data:r.track.id},{id:29637,data:r.track.id},{id:131,data:br[r.type]},{id:134,data:hr[r.track.source._codec]},r.type==="video"?this.videoSpecificTrackInfo(r):null,r.type==="audio"?this.audioSpecificTrackInfo(r):null,r.type==="subtitle"?this.subtitleSpecificTrackInfo(r):null]})}videoSpecificTrackInfo(e){let r=[e.info.decoderConfig.description?{id:25506,data:P(e.info.decoderConfig.description)}:null,e.track.metadata.frameRate?{id:2352003,data:1e9/e.track.metadata.frameRate}:null],s=e.info.decoderConfig.colorSpace,o={id:224,data:[{id:176,data:e.info.width},{id:186,data:e.info.height},ne(s)?{id:21936,data:[{id:21937,data:N[s.matrix]},{id:21946,data:F[s.transfer]},{id:21947,data:R[s.primaries]},{id:21945,data:s.fullRange?2:1}]}:null]};return r.push(o),r}audioSpecificTrackInfo(e){return[e.info.decoderConfig.description?{id:25506,data:P(e.info.decoderConfig.description)}:null,{id:225,data:[{id:181,data:new ee(e.info.sampleRate)},{id:159,data:e.info.numberOfChannels}]}]}subtitleSpecificTrackInfo(e){return[{id:25506,data:y.encode(e.info.config.description)}]}createSegment(){let e={id:408125543,size:this.format._options.streamable?-1:mt,data:[this.format._options.streamable?null:this.seekHead,this.segmentInfo,this.tracksElement]};this.segment=e,this.writeEBML(e)}createCues(){this.cues={id:475249515,data:[]}}get segmentDataOffset(){return d(this.segment),this.dataOffsets.get(this.segment)}getVideoTrackData(e,r){let s=this.trackDatas.find(n=>n.track===e);if(s)return s;pe(r),d(r),d(r.decoderConfig),d(r.decoderConfig.codedWidth!==void 0),d(r.decoderConfig.codedHeight!==void 0);let o={track:e,type:"video",info:{width:r.decoderConfig.codedWidth,height:r.decoderConfig.codedHeight,decoderConfig:r.decoderConfig},chunkQueue:[],lastWrittenMsTimestamp:null};return this.trackDatas.push(o),this.trackDatas.sort((n,a)=>n.track.id-a.track.id),o}getAudioTrackData(e,r){let s=this.trackDatas.find(n=>n.track===e);if(s)return s;he(r),d(r),d(r.decoderConfig);let o={track:e,type:"audio",info:{numberOfChannels:r.decoderConfig.numberOfChannels,sampleRate:r.decoderConfig.sampleRate,decoderConfig:r.decoderConfig},chunkQueue:[],lastWrittenMsTimestamp:null};return this.trackDatas.push(o),this.trackDatas.sort((n,a)=>n.track.id-a.track.id),o}getSubtitleTrackData(e,r){let s=this.trackDatas.find(n=>n.track===e);if(s)return s;be(r),d(r),d(r.config);let o={track:e,type:"subtitle",info:{config:r.config},chunkQueue:[],lastWrittenMsTimestamp:null};return this.trackDatas.push(o),this.trackDatas.sort((n,a)=>n.track.id-a.track.id),o}async addEncodedVideoChunk(e,r,s){let o=await this.mutex.acquire();try{let n=this.getVideoTrackData(e,s),a=new Uint8Array(r.byteLength);r.copyTo(a);let u=r.type==="key",m=this.validateAndNormalizeTimestamp(n.track,r.timestamp,u),l=this.createInternalChunk(a,m,(r.duration??0)/1e6,r.type);e.source._codec==="vp9"&&this.fixVP9ColorSpace(n,l),n.chunkQueue.push(l),await this.interleaveChunks()}finally{o()}}async addEncodedAudioChunk(e,r,s){let o=await this.mutex.acquire();try{let n=this.getAudioTrackData(e,s),a=new Uint8Array(r.byteLength);r.copyTo(a);let u=r.type,m=u==="key",l=this.validateAndNormalizeTimestamp(n.track,r.timestamp,m),f=this.createInternalChunk(a,l,(r.duration??0)/1e6,u);n.chunkQueue.push(f),await this.interleaveChunks()}finally{o()}}async addSubtitleCue(e,r,s){let o=await this.mutex.acquire();try{let n=this.getSubtitleTrackData(e,s),a=this.validateAndNormalizeTimestamp(n.track,1e6*r.timestamp,!0),u=r.text,m=Math.floor(a*1e3);H.lastIndex=0,u=u.replace(H,E=>{let k=ue(E.slice(1,-1))-m;return`<${ce(k)}>`});let l=y.encode(u),f=`${r.settings??""} -${r.identifier??""} -${r.notes??""}`,T=this.createInternalChunk(l,a,r.duration,"key",f.trim()?y.encode(f):null);n.chunkQueue.push(T),await this.interleaveChunks()}finally{o()}}async interleaveChunks(){for(let e of this.output._tracks)if(!e.source._closed&&!this.trackDatas.some(r=>r.track===e))return;e:for(;;){let e=null,r=1/0;for(let o of this.trackDatas){if(o.chunkQueue.length===0&&!o.track.source._closed)break e;o.chunkQueue.length>0&&o.chunkQueue[0].timestamp=2&&s++;let m={rgb:7,bt709:2,bt470bg:1,smpte170m:3}[e.info.decoderConfig.colorSpace.matrix];He(r.data,s+0,s+3,m)}createInternalChunk(e,r,s,o,n=null){return{data:e,type:o,timestamp:r,duration:s,additions:n}}writeBlock(e,r){this.segment||(this.createTracks(),this.createSegment());let s=Math.floor(1e3*r.timestamp),o=this.trackDatas.every(f=>{if(f.track.source._closed)return!0;if(e===f)return r.type==="key";let T=f.chunkQueue[0];return T&&T.type==="key"});(!this.currentCluster||o&&s-this.currentClusterMsTimestamp>=1e3)&&this.createNewCluster(s);let n=s-this.currentClusterMsTimestamp;if(n<0)return;if(n>=Oe)throw new Error(`Current Matroska cluster exceeded its maximum allowed length of ${Oe} milliseconds. In order to produce a correct WebM file, you must pass in a key frame at least every ${Oe} milliseconds.`);let u=new Uint8Array(4),m=new DataView(u.buffer);m.setUint8(0,128|e.track.id),m.setInt16(1,n,!1);let l=Math.floor(1e3*r.duration);if(l===0&&!r.additions){m.setUint8(3,+(r.type==="key")<<7);let f={id:163,data:[u,r.data]};this.writeEBML(f)}else{let f={id:160,data:[{id:161,data:[u,r.data]},r.type==="delta"?{id:251,data:new te(e.lastWrittenMsTimestamp-s)}:null,r.additions?{id:30113,data:[{id:166,data:[{id:165,data:r.additions},{id:238,data:1}]}]}:null,l>0?{id:155,data:l}:null]};this.writeEBML(f)}this.duration=Math.max(this.duration,s+l),e.lastWrittenMsTimestamp=s,this.trackDatasInCurrentCluster.add(e)}createNewCluster(e){this.currentCluster&&!this.format._options.streamable&&this.finalizeCurrentCluster(),this.currentCluster={id:524531317,size:this.format._options.streamable?-1:ft,data:[{id:231,data:e}]},this.writeEBML(this.currentCluster),this.currentClusterMsTimestamp=e,this.trackDatasInCurrentCluster.clear()}finalizeCurrentCluster(){d(this.currentCluster);let e=this.writer.getPos()-this.dataOffsets.get(this.currentCluster),r=this.writer.getPos();this.writer.seek(this.offsets.get(this.currentCluster)+4),this.writeEBMLVarInt(e,ft),this.writer.seek(r);let s=this.offsets.get(this.currentCluster)-this.segmentDataOffset;d(this.cues),this.cues.data.push({id:187,data:[{id:179,data:this.currentClusterMsTimestamp},...[...this.trackDatasInCurrentCluster].map(o=>({id:183,data:[{id:247,data:o.track.id},{id:241,data:s}]}))]})}async onTrackClose(){let e=await this.mutex.acquire();await this.interleaveChunks(),e()}async finalize(){let e=await this.mutex.acquire();this.segment||(this.createTracks(),this.createSegment());for(let r of this.trackDatas)for(;r.chunkQueue.length>0;)this.writeBlock(r,r.chunkQueue.shift());if(!this.format._options.streamable&&this.currentCluster&&this.finalizeCurrentCluster(),d(this.cues),this.writeEBML(this.cues),!this.format._options.streamable){let r=this.writer.getPos(),s=this.writer.getPos()-this.segmentDataOffset;this.writer.seek(this.offsets.get(this.segment)+4),this.writeEBMLVarInt(s,mt),this.segmentDuration.data=new j(this.duration),this.writer.seek(this.offsets.get(this.segmentDuration)),this.writeEBML(this.segmentDuration),this.seekHead.data[0].data[1].data=this.offsets.get(this.cues)-this.segmentDataOffset,this.seekHead.data[1].data[1].data=this.offsets.get(this.segmentInfo)-this.segmentDataOffset,this.seekHead.data[2].data[1].data=this.offsets.get(this.tracksElement)-this.segmentDataOffset,this.writer.seek(this.offsets.get(this.seekHead)),this.writeEBML(this.seekHead),this.writer.seek(r)}e()}};var U=class{},Me=class extends U{constructor(i={}){if(!i||typeof i!="object")throw new TypeError("options must be an object.");if(i.fastStart!==void 0&&![!1,"in-memory","fragmented"].includes(i.fastStart))throw new TypeError('options.fastStart, when provided, must be false, "in-memory", or "fragmented".');super(),this._options=i}_createMuxer(i){return new xe(i,this)}},we=class extends U{constructor(i={}){if(!i||typeof i!="object")throw new TypeError("options must be an object.");if(i.streamable!==void 0&&typeof i.streamable!="boolean")throw new TypeError("options.streamable, when provided, must be a boolean.");super(),this._options=i}_createMuxer(i){return new Te(i,this)}},L=class extends we{};var re=["avc","hevc","vp8","vp9","av1"],ie=["aac","opus"],Ve=["webvtt"],q=class{constructor(){this._connectedTrack=null;this._closed=!1;this._offsetTimestamps=!1}_ensureValidDigest(){if(!this._connectedTrack)throw new Error("Cannot call digest without connecting the source to an output track.");if(!this._connectedTrack.output._started)throw new Error("Cannot call digest before output has been started.");if(this._connectedTrack.output._finalizing)throw new Error("Cannot call digest after output has started finalizing.");if(this._closed)throw new Error("Cannot call digest after source has been closed.")}_start(){}async _flush(){}close(){if(this._closed)throw new Error("Source already closed.");if(!this._connectedTrack)throw new Error("Cannot call close without connecting the source to an output track.");if(!this._connectedTrack.output._started)throw new Error("Cannot call close before output has been started.");this._closed=!0,!this._connectedTrack.output._finalizing&&this._connectedTrack.output._muxer.onTrackClose(this._connectedTrack)}},O=class extends q{constructor(e){super();this._connectedTrack=null;if(!re.includes(e))throw new TypeError(`Invalid video codec '${e}'. Must be one of: ${re.join(", ")}.`);this._codec=e}},ze=class extends O{constructor(i){super(i)}digest(i,e){if(!(i instanceof EncodedVideoChunk))throw new TypeError("chunk must be an EncodedVideoChunk.");return this._ensureValidDigest(),this._connectedTrack.output._muxer.addEncodedVideoChunk(this._connectedTrack,i,e)}},xr=5,Tr=t=>{if(!t||typeof t!="object")throw new TypeError("Codec config must be an object.");if(!re.includes(t.codec))throw new TypeError(`Invalid video codec '${t.codec}'. Must be one of: ${re.join(", ")}.`);if(!Number.isInteger(t.bitrate)||t.bitrate<=0)throw new TypeError("config.bitrate must be a positive integer.");if(t.latencyMode!==void 0&&!["quality","realtime"].includes(t.latencyMode))throw new TypeError("config.latencyMode, when provided, must be 'quality' or 'realtime'.")},se=class{constructor(i,e){this.source=i;this.codecConfig=e;this.encoder=null;this.muxer=null;this.lastMultipleOfKeyFrameInterval=-1;this.lastWidth=null;this.lastHeight=null;Tr(e)}async digest(i){if(this.source._ensureValidDigest(),this.lastWidth!==null&&this.lastHeight!==null){if(i.codedWidth!==this.lastWidth||i.codedHeight!==this.lastHeight)throw new Error(`Video frame size must remain constant. Expected ${this.lastWidth}x${this.lastHeight}, got ${i.codedWidth}x${i.codedHeight}.`)}else this.lastWidth=i.codedWidth,this.lastHeight=i.codedHeight;this.ensureEncoder(i),d(this.encoder);let e=Math.floor(i.timestamp/1e6/xr);this.encoder.encode(i,{keyFrame:e!==this.lastMultipleOfKeyFrameInterval}),this.lastMultipleOfKeyFrameInterval=e,this.encoder.encodeQueueSize>=4&&await new Promise(r=>this.encoder.addEventListener("dequeue",r,{once:!0})),await this.muxer.mutex.currentPromise}ensureEncoder(i){this.encoder||(this.encoder=new VideoEncoder({output:(e,r)=>void this.muxer.addEncodedVideoChunk(this.source._connectedTrack,e,r),error:e=>console.error("Video encode error:",e)}),this.encoder.configure({codec:nt(this.codecConfig.codec,i.codedWidth,i.codedHeight,this.codecConfig.bitrate),width:i.codedWidth,height:i.codedHeight,bitrate:this.codecConfig.bitrate,framerate:this.source._connectedTrack?.metadata.frameRate,latencyMode:this.codecConfig.latencyMode,...ut(this.codecConfig.codec)}),d(this.source._connectedTrack),this.muxer=this.source._connectedTrack.output._muxer)}async flush(){this.encoder&&(await this.encoder.flush(),this.encoder.close())}},Pe=class extends O{constructor(i){super(i.codec),this._encoder=new se(this,i)}digest(i){if(!(i instanceof VideoFrame))throw new TypeError("videoFrame must be a VideoFrame.");return this._encoder.digest(i)}_flush(){return this._encoder.flush()}},Be=class extends O{constructor(i,e){if(!(i instanceof HTMLCanvasElement))throw new TypeError("canvas must be an HTMLCanvasElement.");super(e.codec),this._encoder=new se(this,e),this._canvas=i}digest(i,e=0){if(!Number.isFinite(i)||i<0)throw new TypeError("timestamp must be a non-negative number.");if(!Number.isFinite(e)||e<0)throw new TypeError("duration must be a non-negative number.");let r=new VideoFrame(this._canvas,{timestamp:Math.round(1e6*i),duration:Math.round(1e6*e),alpha:"discard"}),s=this._encoder.digest(r);return r.close(),s}_flush(){return this._encoder.flush()}},Ie=class extends O{constructor(e,r){if(!(e instanceof MediaStreamTrack)||e.kind!=="video")throw new TypeError("track must be a video MediaStreamTrack.");r={...r,latencyMode:"realtime"};super(r.codec);this._abortController=null;this._offsetTimestamps=!0;this._encoder=new se(this,r),this._track=e}_start(){this._abortController=new AbortController;let e=new MediaStreamTrackProcessor({track:this._track}),r=new WritableStream({write:s=>{this._encoder.digest(s),s.close()}});e.readable.pipeTo(r,{signal:this._abortController.signal}).catch(s=>{s instanceof DOMException&&s.name==="AbortError"||console.error("Pipe error:",s)})}async _flush(){this._abortController&&(this._abortController.abort(),this._abortController=null),await this._encoder.flush()}},M=class extends q{constructor(e){super();this._connectedTrack=null;if(!ie.includes(e))throw new TypeError(`Invalid audio codec '${e}'. Must be one of: ${ie.join(", ")}.`);this._codec=e}},Ue=class extends M{constructor(i){super(i)}digest(i,e){if(!(i instanceof EncodedAudioChunk))throw new TypeError("chunk must be an EncodedAudioChunk.");return this._ensureValidDigest(),this._connectedTrack.output._muxer.addEncodedAudioChunk(this._connectedTrack,i,e)}},wr=t=>{if(!t||typeof t!="object")throw new TypeError("Codec config must be an object.");if(!ie.includes(t.codec))throw new TypeError(`Invalid audio codec '${t.codec}'. Must be one of: ${ie.join(", ")}.`);if(!Number.isInteger(t.bitrate)||t.bitrate<=0)throw new TypeError("config.bitrate must be a positive integer.")},oe=class{constructor(i,e){this.source=i;this.codecConfig=e;this.encoder=null;this.muxer=null;this.lastNumberOfChannels=null;this.lastSampleRate=null;wr(e)}async digest(i){if(this.source._ensureValidDigest(),this.lastNumberOfChannels!==null&&this.lastSampleRate!==null){if(i.numberOfChannels!==this.lastNumberOfChannels||i.sampleRate!==this.lastSampleRate)throw new Error(`Audio parameters must remain constant. Expected ${this.lastNumberOfChannels} channels at ${this.lastSampleRate} Hz, got ${i.numberOfChannels} channels at ${i.sampleRate} Hz.`)}else this.lastNumberOfChannels=i.numberOfChannels,this.lastSampleRate=i.sampleRate;this.ensureEncoder(i),d(this.encoder),this.encoder.encode(i),this.encoder.encodeQueueSize>=4&&await new Promise(e=>this.encoder.addEventListener("dequeue",e,{once:!0})),await this.muxer.mutex.currentPromise}ensureEncoder(i){this.encoder||(this.encoder=new AudioEncoder({output:(e,r)=>void this.muxer.addEncodedAudioChunk(this.source._connectedTrack,e,r),error:e=>console.error("Audio encode error:",e)}),this.encoder.configure({codec:at(this.codecConfig.codec,i.numberOfChannels,i.sampleRate),numberOfChannels:i.numberOfChannels,sampleRate:i.sampleRate,bitrate:this.codecConfig.bitrate,...ct(this.codecConfig.codec)}),d(this.source._connectedTrack),this.muxer=this.source._connectedTrack.output._muxer)}async flush(){this.encoder&&(await this.encoder.flush(),this.encoder.close())}},De=class extends M{constructor(i){super(i.codec),this._encoder=new oe(this,i)}digest(i){if(!(i instanceof AudioData))throw new TypeError("audioData must be an AudioData.");return this._encoder.digest(i)}_flush(){return this._encoder.flush()}},We=class extends M{constructor(e){super(e.codec);this._accumulatedFrameCount=0;this._encoder=new oe(this,e)}digest(e){if(!(e instanceof AudioBuffer))throw new TypeError("audioBuffer must be an AudioBuffer.");let r=e.numberOfChannels,s=e.sampleRate,o=e.length,n=new Float32Array(r*o);for(let m=0;m{this._encoder.digest(s),s.close()}});e.readable.pipeTo(r,{signal:this._abortController.signal}).catch(s=>{s instanceof DOMException&&s.name==="AbortError"||console.error("Pipe error:",s)})}async _flush(){this._abortController&&(this._abortController.abort(),this._abortController=null),await this._encoder.flush()}},K=class extends q{constructor(e){super();this._connectedTrack=null;if(!Ve.includes(e))throw new TypeError(`Invalid subtitle codec '${e}'. Must be one of: ${Ve.join(", ")}.`);this._codec=e}},Fe=class extends K{constructor(i){super(i),this._parser=new ae({codec:i,output:(e,r)=>this._connectedTrack?.output._muxer.addSubtitleCue(this._connectedTrack,e,r),error:e=>console.error("Subtitle parse error:",e)})}digest(i){if(typeof i!="string")throw new TypeError("text must be a string.");return this._ensureValidDigest(),this._parser.parse(i),this._connectedTrack.output._muxer.mutex.currentPromise}};var Ne=class{constructor(i){this._tracks=[];this._started=!1;this._finalizing=!1;this._mutex=new W;if(!i||typeof i!="object")throw new TypeError("options must be an object.");if(!(i.format instanceof U))throw new TypeError("options.format must be an OutputFormat.");if(!(i.target instanceof I))throw new TypeError("options.target must be a Target.");if(i.target._output)throw new Error("Target is already used for another output.");i.target._output=this,this._writer=i.target._createWriter(),this._muxer=i.format._createMuxer(this)}addVideoTrack(i,e={}){if(!(i instanceof O))throw new TypeError("source must be a VideoSource.");if(!e||typeof e!="object")throw new TypeError("metadata must be an object.");if(typeof e.rotation=="number"&&![0,90,180,270].includes(e.rotation))throw new TypeError(`Invalid video rotation: ${e.rotation}. Has to be 0, 90, 180 or 270.`);if(Array.isArray(e.rotation)&&(e.rotation.length!==9||e.rotation.some(r=>!Number.isFinite(r))))throw new TypeError(`Invalid video transformation matrix: ${e.rotation.join()}`);if(e.frameRate!==void 0&&(!Number.isInteger(e.frameRate)||e.frameRate<=0))throw new TypeError(`Invalid video frame rate: ${e.frameRate}. Must be a positive integer.`);this._addTrack("video",i,e)}addAudioTrack(i,e={}){if(!(i instanceof M))throw new TypeError("source must be an AudioSource.");if(!e||typeof e!="object")throw new TypeError("metadata must be an object.");this._addTrack("audio",i,e)}addSubtitleTrack(i,e={}){if(!(i instanceof K))throw new TypeError("source must be a SubtitleSource.");if(!e||typeof e!="object")throw new TypeError("metadata must be an object.");this._addTrack("subtitle",i,e)}_addTrack(i,e,r){if(this._started)throw new Error("Cannot add track after output has started.");if(e._connectedTrack)throw new Error("Source is already used for a track.");let s={id:this._tracks.length+1,output:this,type:i,source:e,metadata:r};this._muxer.beforeTrackAdd(s),this._tracks.push(s),e._connectedTrack=s}async start(){if(this._started)throw new Error("Output already started.");this._started=!0,this._writer.start();let i=await this._mutex.acquire();await this._muxer.start();for(let e of this._tracks)e.source._start();i()}async finalize(){if(!this._started)throw new Error("Cannot finalize before starting.");if(this._finalizing)throw new Error("Cannot call finalize twice.");this._finalizing=!0;let i=await this._mutex.acquire(),e=this._tracks.map(r=>r.source._flush());await Promise.all(e),await this._muxer.finalize(),await this._writer.flush(),await this._writer.finalize(),i()}};export{ie as AUDIO_CODECS,J as ArrayBufferTarget,We as AudioBufferSource,De as AudioDataSource,M as AudioSource,Be as CanvasSource,Ue as EncodedAudioChunkSource,ze as EncodedVideoChunkSource,q as MediaSource,Re as MediaStreamAudioTrackSource,Ie as MediaStreamVideoTrackSource,we as MkvOutputFormat,Me as Mp4OutputFormat,Ne as Output,U as OutputFormat,Ve as SUBTITLE_CODECS,Ae as StreamTarget,K as SubtitleSource,I as Target,Fe as TextSubtitleSource,re as VIDEO_CODECS,Pe as VideoFrameSource,O as VideoSource,L as WebMOutputFormat}; +`,o);c===-1&&(c=e.length);let u=pe(t[2]),f=pe(t[3])-u,T=e.slice(n,c).trim();e=e.slice(c).trimStart(),ee.lastIndex=0;let C={timestamp:u/1e3,duration:f/1e3,text:T,identifier:s,settings:a,notes:i},k={};this.preambleEmitted||(k.config={description:this.preambleText},this.preambleEmitted=!0),this.options.output(C,k)}}},Yt=/(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})/,pe=r=>{let e=Yt.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])},be=r=>{let e=Math.floor(r/36e5),t=Math.floor(r%(60*60*1e3)/(60*1e3)),i=Math.floor(r%(60*1e3)/1e3),s=r%1e3;return e.toString().padStart(2,"0")+":"+t.toString().padStart(2,"0")+":"+i.toString().padStart(2,"0")+"."+s.toString().padStart(3,"0")};var te=class{constructor(e){this.writer=e}helper=new Uint8Array(8);helperView=new DataView(this.helper.buffer);offsets=new WeakMap;writeU32(e){this.helperView.setUint32(0,e,!1),this.writer.write(this.helper.subarray(0,4))}writeU64(e){this.helperView.setUint32(0,Math.floor(e/2**32),!1),this.helperView.setUint32(4,e,!1),this.writer.write(this.helper.subarray(0,8))}writeAscii(e){for(let t=0;t[(r%256+256)%256],p=r=>(E.setUint16(0,r,!1),[h[0],h[1]]),Zt=r=>(E.setInt16(0,r,!1),[h[0],h[1]]),yt=r=>(E.setUint32(0,r,!1),[h[1],h[2],h[3]]),m=r=>(E.setUint32(0,r,!1),[h[0],h[1],h[2],h[3]]),vt=r=>(E.setInt32(0,r,!1),[h[0],h[1],h[2],h[3]]),F=r=>(E.setUint32(0,Math.floor(r/2**32),!1),E.setUint32(4,r,!1),[h[0],h[1],h[2],h[3],h[4],h[5],h[6],h[7]]),He=r=>(E.setInt16(0,2**8*r,!1),[h[0],h[1]]),A=r=>(E.setInt32(0,2**16*r,!1),[h[0],h[1],h[2],h[3]]),We=r=>(E.setInt32(0,2**30*r,!1),[h[0],h[1],h[2],h[3]]),Ne=(r,e)=>{let t=[],i=r;do{let s=i&127;i>>=7,t.length>0&&(s|=128),t.push(s),e!==void 0&&e--}while(i>0||e);return t.reverse()},S=(r,e=!1)=>{let t=Array(r.length).fill(null).map((i,s)=>r.charCodeAt(s));return e&&t.push(0),t},Le=r=>{let e=null;for(let t of r)(!e||t.timestamp>e.timestamp)&&(e=t);return e},_t=r=>[A(r[0]),A(r[1]),We(r[2]),A(r[3]),A(r[4]),We(r[5]),A(r[6]),A(r[7]),We(r[8])],b=(r,e,t)=>({type:r,contents:e&&new Uint8Array(e.flat(10)),children:t}),g=(r,e,t,i,s)=>b(r,[x(e),yt(t),i??[]],s),At=r=>r.fragmented?b("ftyp",[S("iso5"),m(512),S("iso5"),S("iso6"),S("mp41")]):b("ftyp",[S("isom"),m(512),S("isom"),r.holdsAvc?S("avc1"):[],S("mp41")]),ge=r=>({type:"mdat",largeSize:r}),Et=r=>({type:"free",size:r}),re=(r,e,t=!1)=>b("moov",void 0,[Jt(e,r),...r.map(i=>er(i,e)),t?Rr(r):null]),Jt=(r,e)=>{let t=_(Math.max(0,...e.filter(n=>n.samples.length>0).map(n=>{let a=Le(n.samples);return a.timestamp+a.duration})),ke),i=Math.max(0,...e.map(n=>n.track.id))+1,s=!M(r)||!M(t),o=s?F:m;return g("mvhd",+s,0,[o(r),o(r),m(ke),o(t),A(1),He(1),Array(10).fill(0),_t(De),Array(24).fill(0),m(i)])},er=(r,e)=>b("trak",void 0,[tr(r,e),rr(r,e)]),tr=(r,e)=>{let t=Le(r.samples),i=_(t?t.timestamp+t.duration:0,ke),s=!M(e)||!M(i),o=s?F:m,n;if(r.type==="video"){let a=r.track.metadata.rotation;n=a===void 0||typeof a=="number"?V(a??0):a}else n=De;return g("tkhd",+s,3,[o(e),o(e),m(r.track.id),m(0),o(i),Array(8).fill(0),p(0),p(r.track.id),He(r.type==="audio"?1:0),p(0),_t(n),A(r.type==="video"?r.info.width:0),A(r.type==="video"?r.info.height:0)])},rr=(r,e)=>b("mdia",void 0,[ir(r,e),nr(r),ar(r)]),ir=(r,e)=>{let t=Le(r.samples),i=_(t?t.timestamp+t.duration:0,r.timescale),s=!M(e)||!M(i),o=s?F:m;return g("mdhd",+s,0,[o(e),o(e),m(r.timescale),o(i),p(21956),p(0)])},sr={video:"vide",audio:"soun",subtitle:"text"},or={video:"VideoHandler",audio:"SoundHandler",subtitle:"TextHandler"},nr=r=>g("hdlr",0,0,[S("mhlr"),S(sr[r.type]),m(0),m(0),m(0),S(or[r.type],!0)]),ar=r=>b("minf",void 0,[lr[r.type](),mr(),pr(r)]),cr=()=>g("vmhd",0,1,[p(0),p(0),p(0),p(0)]),ur=()=>g("smhd",0,0,[p(0),p(0)]),dr=()=>g("nmhd",0,0),lr={video:cr,audio:ur,subtitle:dr},mr=()=>b("dinf",void 0,[fr()]),fr=()=>g("dref",0,0,[m(1)],[hr()]),hr=()=>g("url ",0,1),pr=r=>{let e=r.compositionTimeOffsetTable.length>1||r.compositionTimeOffsetTable.some(t=>t.sampleCompositionTimeOffset!==0);return b("stbl",void 0,[br(r),Ar(r),Er(r),Ir(r),Or(r),Vr(r),e?Mr(r):null])},br=r=>{let e;return r.type==="video"?e=kr(Hr[r.track.source._codec],r):r.type==="audio"?e=wr($r[r.track.source._codec],r):r.type==="subtitle"&&(e=vr(jr[r.track.source._codec],r)),l(e),g("stsd",0,0,[m(1)],[e])},kr=(r,e)=>b(r,[Array(6).fill(0),p(1),p(0),p(0),Array(12).fill(0),p(e.info.width),p(e.info.height),m(4718592),m(4718592),m(0),p(1),Array(32).fill(0),p(24),Zt(65535)],[Lr[e.track.source._codec](e),fe(e.info.decoderConfig.colorSpace)?gr(e):null]),gr=r=>b("colr",[S("nclx"),p(z[r.info.decoderConfig.colorSpace.primaries]),p(B[r.info.decoderConfig.colorSpace.transfer]),p(U[r.info.decoderConfig.colorSpace.matrix]),x((r.info.decoderConfig.colorSpace.fullRange?1:0)<<7)]),Tr=r=>r.info.decoderConfig&&b("avcC",[...P(r.info.decoderConfig.description)]),xr=r=>r.info.decoderConfig&&b("hvcC",[...P(r.info.decoderConfig.description)]),St=r=>{if(!r.info.decoderConfig)return null;let e=r.info.decoderConfig;l(e.colorSpace);let t=e.codec.split("."),i=Number(t[1]),s=Number(t[2]),a=(Number(t[3])<<4)+(0<<1)+Number(e.colorSpace.fullRange);return g("vpcC",1,0,[x(i),x(s),x(a),x(2),x(2),x(2),p(0)])},Cr=()=>b("av1C",[129,0,0,0]),wr=(r,e)=>b(r,[Array(6).fill(0),p(1),p(0),p(0),m(0),p(e.info.numberOfChannels),p(16),p(0),p(0),A(e.info.sampleRate)],[Qr[e.track.source._codec](e)]),Sr=r=>{let t=[...P(r.info.decoderConfig.description??new ArrayBuffer(0))];return t=[...x(64),...x(21),...yt(0),...m(0),...m(0),...x(5),...Ne(t.length),...t],t=[...p(1),...x(0),...x(4),...Ne(t.length),...t,...x(6),...x(1),...x(2)],t=[...x(3),...Ne(t.length),...t],g("esds",0,0,t)},yr=r=>{let e=3840,t=0,i=r.info.decoderConfig?.description;if(i){l(i.byteLength>=18);let s=ArrayBuffer.isView(i)?new DataView(i.buffer,i.byteOffset,i.byteLength):new DataView(i);e=s.getUint16(10,!0),t=s.getInt16(14,!0)}return b("dOps",[x(0),x(r.info.numberOfChannels),p(e),m(r.info.sampleRate),He(t),x(0)])},vr=(r,e)=>b(r,[Array(6).fill(0),p(1)],[Kr[e.track.source._codec](e)]),_r=r=>b("vttC",[...v.encode(r.info.config.description)]);var Ar=r=>g("stts",0,0,[m(r.timeToSampleTable.length),r.timeToSampleTable.map(e=>[m(e.sampleCount),m(e.sampleDelta)])]),Er=r=>{if(r.samples.every(t=>t.type==="key"))return null;let e=[...r.samples.entries()].filter(([,t])=>t.type==="key");return g("stss",0,0,[m(e.length),e.map(([t])=>m(t+1))])},Ir=r=>g("stsc",0,0,[m(r.compactlyCodedChunkTable.length),r.compactlyCodedChunkTable.map(e=>[m(e.firstChunk),m(e.samplesPerChunk),m(1)])]),Or=r=>g("stsz",0,0,[m(0),m(r.samples.length),r.samples.map(e=>m(e.size))]),Vr=r=>r.finalizedChunks.length>0&&y(r.finalizedChunks).offset>=2**32?g("co64",0,0,[m(r.finalizedChunks.length),r.finalizedChunks.map(e=>F(e.offset))]):g("stco",0,0,[m(r.finalizedChunks.length),r.finalizedChunks.map(e=>m(e.offset))]),Mr=r=>g("ctts",0,0,[m(r.compositionTimeOffsetTable.length),r.compositionTimeOffsetTable.map(e=>[m(e.sampleCount),m(e.sampleCompositionTimeOffset)])]),Rr=r=>b("mvex",void 0,r.map(Pr)),Pr=r=>g("trex",0,0,[m(r.track.id),m(1),m(0),m(0),m(0)]),$e=(r,e)=>b("moof",void 0,[zr(r),...e.map(Br)]),zr=r=>g("mfhd",0,0,[m(r)]),It=r=>{let e=0,t=0,i=0,s=0,o=r.type==="delta";return t|=+o,o?e|=1:e|=2,e<<24|t<<16|i<<8|s},Br=r=>b("traf",void 0,[Ur(r),Dr(r),Fr(r)]),Ur=r=>{l(r.currentChunk);let e=0;e|=8,e|=16,e|=32,e|=131072;let t=r.currentChunk.samples[1]??r.currentChunk.samples[0],i={duration:t.timescaleUnitsToNextSample,size:t.size,flags:It(t)};return g("tfhd",0,e,[m(r.track.id),m(i.duration),m(i.size),m(i.flags)])},Dr=r=>(l(r.currentChunk),g("tfdt",1,0,[F(_(r.currentChunk.startTimestamp,r.timescale))])),Fr=r=>{l(r.currentChunk);let e=r.currentChunk.samples.map(w=>w.timescaleUnitsToNextSample),t=r.currentChunk.samples.map(w=>w.size),i=r.currentChunk.samples.map(It),s=r.currentChunk.samples.map(w=>_(w.timestamp-w.decodeTimestamp,r.timescale)),o=new Set(e),n=new Set(t),a=new Set(i),c=new Set(s),u=a.size===2&&i[0]!==i[1],d=o.size>1,f=n.size>1,T=!u&&a.size>1,C=c.size>1||[...c].some(w=>w!==0),k=0;return k|=1,k|=4*+u,k|=256*+d,k|=512*+f,k|=1024*+T,k|=2048*+C,g("trun",1,k,[m(r.currentChunk.samples.length),m(r.currentChunk.offset-r.currentChunk.moofOffset||0),u?m(i[0]):[],r.currentChunk.samples.map((w,me)=>[d?m(e[me]):[],f?m(t[me]):[],T?m(i[me]):[],C?vt(s[me]):[]])])},Ot=r=>b("mfra",void 0,[...r.map(Wr),Nr()]),Wr=(r,e)=>g("tfra",1,0,[m(r.track.id),m(63),m(r.finalizedChunks.length),r.finalizedChunks.map(i=>[F(_(i.startTimestamp,r.timescale)),F(i.moofOffset),m(e+1),m(1),m(1)])]),Nr=()=>g("mfro",0,0,[m(0)]),Vt=()=>b("vtte"),Mt=(r,e,t,i,s)=>b("vttc",void 0,[s!==null?b("vsid",[vt(s)]):null,t!==null?b("iden",[...v.encode(t)]):null,e!==null?b("ctim",[...v.encode(be(e))]):null,i!==null?b("sttg",[...v.encode(i)]):null,b("payl",[...v.encode(r)])]),Rt=r=>b("vtta",[...v.encode(r)]),Hr={avc:"avc1",hevc:"hvc1",vp8:"vp08",vp9:"vp09",av1:"av01"},Lr={avc:Tr,hevc:xr,vp8:St,vp9:St,av1:Cr},$r={aac:"mp4a",opus:"Opus"},Qr={aac:Sr,opus:yr},jr={webvtt:"wvtt"},Kr={webvtt:_r};var $=class{output;mutex=new H;constructor(e){this.output=e}beforeTrackAdd(e){}onTrackClose(e){}trackTimestampInfo=new WeakMap;validateAndNormalizeTimestamp(e,t,i){let s=t/1e6,o=this.trackTimestampInfo.get(e);if(!o){if(!i)throw new Error("First frame must be a key frame.");if(this.timestampsMustStartAtZero&&s>0)throw new Error(`Timestamps must start at zero (got ${s}s).`);o={timestampOffset:s,maxTimestamp:e.source._offsetTimestamps?0:s,lastKeyFrameTimestamp:e.source._offsetTimestamps?0:s},this.trackTimestampInfo.set(e,o)}if(e.source._offsetTimestamps&&(s-=o.timestampOffset),s<0)throw new Error(`Timestamps must be non-negative (got ${s}s).`);if(si.start-s.start);e.push({start:t[0].start,size:t[0].data.byteLength});for(let i=1;ic.start<=t&&tXr){for(let c=0;c=e.written[o+1].start;)e.written[o].end=Math.max(e.written[o].end,e.written[o+1].end),e.written.splice(o+1,1)}createChunk(e){let i={start:Math.floor(e/this.chunkSize)*this.chunkSize,data:new Uint8Array(this.chunkSize),written:[],shouldFlush:!1};return this.chunks.push(i),this.chunks.sort((s,o)=>s.start-o.start),this.chunks.indexOf(i)}queueChunksForFlush(e=!1){l(this.writer);for(let t=0;t{if(r==="avc"){let o=Math.ceil(e/16)*Math.ceil(t/16),n=Pt.find(f=>o<=f.maxMacroblocks&&i<=f.maxBitrate)??y(Pt),a=n?n.level:0,c="64".padStart(2,"0"),u="00",d=a.toString(16).padStart(2,"0");return`avc1.${c}${u}${d}`}else if(r==="hevc"){let s="",n="6",a=e*t,c=zt.find(d=>a<=d.maxPictureSize&&i<=d.maxBitrate)??y(zt);return`hev1.${s}1.${n}.${c.tier}${c.level}.B0`}else{if(r==="vp8")return"vp8";if(r==="vp9"){let s="00",o=e*t,n=Bt.find(c=>o<=c.maxPictureSize&&i<=c.maxBitrate)??y(Bt);return`vp09.${s}.${n.level}.08`}else if(r==="av1"){let o=e*t,n=Ut.find(c=>o<=c.maxPictureSize&&i<=c.maxBitrate)??y(Ut);return`av01.0.${n.level.toString().padStart(2,"0")}${n.tier}.08`}}throw new TypeError(`Unhandled codec '${r}'.`)},Ft=(r,e)=>{if(r==="avc"){if(!e||e.byteLength<4)throw new TypeError("AVC description must be at least 4 bytes long.");return`avc1.${xt(e.subarray(1,4))}`}else if(r==="hevc"){if(!e)throw new TypeError("HEVC description must be provided.");let t=new DataView(e.buffer,e.byteOffset,e.byteLength),i="hev1.",s=e[1]>>6&3,o=e[1]&31;i+=["","A","B","C"][s]+o,i+=".";let n=Ct(t.getUint32(2));i+=n.toString(16),i+=".";let a=e[1]>>5&1,c=e[12];i+=a===0?"L":"H",i+=c,i+=".";let u=[];for(let d=0;d<6;d++){let f=e[d+13];u.push(f)}for(;u[u.length-1]===0;)u.pop();return i+=u.map(d=>d.toString(16)).join("."),i}throw new TypeError(`Unhandled codec '${r}'.`)},Wt=(r,e,t)=>{if(r==="aac")return e>=2&&t<=24e3?"mp4a.40.29":t<=24e3?"mp4a.40.5":"mp4a.40.2";if(r==="opus")return"opus";if(r==="vorbis")return"vorbis";throw new TypeError(`Unhandled codec '${r}'.`)},Nt=(r,e)=>{if(r==="aac"){if(!e||e.byteLength<2)throw new TypeError("AAC description must be at least 2 bytes long.");return`mp4a.40.${e[0]>>3}`}else{if(r==="opus")return"opus";if(r==="vorbis")return"vorbis"}throw new TypeError(`Unhandled codec '${r}'.`)},Ht=r=>r==="avc"?{avc:{format:"avc"}}:r==="hevc"?{hevc:{format:"hevc"}}:{},Lt=r=>r==="aac"?{aac:{format:"aac"}}:r==="opus"?{opus:{format:"opus"}}:{},we=r=>{if(!r)throw new TypeError("Video chunk metadata must be provided.");if(typeof r!="object")throw new TypeError("Video chunk metadata must be an object.");if(!r.decoderConfig)throw new TypeError("Video chunk metadata must include a decoder configuration.");if(typeof r.decoderConfig!="object")throw new TypeError("Video chunk metadata decoder configuration must be an object.");if(typeof r.decoderConfig.codec!="string")throw new TypeError("Video chunk metadata decoder configuration must specify a codec string.");if(!Number.isInteger(r.decoderConfig.codedWidth)||r.decoderConfig.codedWidth<=0)throw new TypeError("Video chunk metadata decoder configuration must specify a valid codedWidth (positive integer).");if(!Number.isInteger(r.decoderConfig.codedHeight)||r.decoderConfig.codedHeight<=0)throw new TypeError("Video chunk metadata decoder configuration must specify a valid codedHeight (positive integer).");if(r.decoderConfig.description!==void 0&&!Ue(r.decoderConfig.description))throw new TypeError("Video chunk metadata decoder configuration description, when defined, must be an ArrayBuffer or an ArrayBuffer view.");if(r.decoderConfig.colorSpace!==void 0){let{colorSpace:e}=r.decoderConfig;if(typeof e!="object")throw new TypeError("Video chunk metadata decoder configuration colorSpace, when provided, must be an object.");let t=Object.keys(z);if(e.primaries!=null&&!t.includes(e.primaries))throw new TypeError(`Video chunk metadata decoder configuration colorSpace primaries, when defined, must be one of ${t.join(", ")}.`);let i=Object.keys(B);if(e.transfer!=null&&!i.includes(e.transfer))throw new TypeError(`Video chunk metadata decoder configuration colorSpace transfer, when defined, must be one of ${i.join(", ")}.`);let s=Object.keys(U);if(e.matrix!=null&&!s.includes(e.matrix))throw new TypeError(`Video chunk metadata decoder configuration colorSpace matrix, when defined, must be one of ${s.join(", ")}.`);if(e.fullRange!=null&&typeof e.fullRange!="boolean")throw new TypeError("Video chunk metadata decoder configuration colorSpace fullRange, when defined, must be a boolean.")}if((r.decoderConfig.codec.startsWith("avc1")||r.decoderConfig.codec.startsWith("avc3"))&&!r.decoderConfig.description)throw new TypeError("Video chunk metadata decoder configuration for AVC must include a description, which is expected to be an AVCDecoderConfigurationRecord as specified in ISO 14496-15.");if((r.decoderConfig.codec.startsWith("hev1")||r.decoderConfig.codec.startsWith("hvc1"))&&!r.decoderConfig.description)throw new TypeError("Video chunk metadata decoder configuration for HEVC must include a description, which is expected to be an HEVCDecoderConfigurationRecord as specified in ISO 14496-15.");if((r.decoderConfig.codec==="vp8"||r.decoderConfig.codec.startsWith("vp09"))&&r.decoderConfig.colorSpace===void 0)throw new TypeError("Video chunk metadata decoder configuration for VP8/VP9 must include a colorSpace.")},Se=r=>{if(!r)throw new TypeError("Audio chunk metadata must be provided.");if(typeof r!="object")throw new TypeError("Audio chunk metadata must be an object.");if(!r.decoderConfig)throw new TypeError("Audio chunk metadata must include a decoder configuration.");if(typeof r.decoderConfig!="object")throw new TypeError("Audio chunk metadata decoder configuration must be an object.");if(typeof r.decoderConfig.codec!="string")throw new TypeError("Audio chunk metadata decoder configuration must specify a codec string.");if(!Number.isInteger(r.decoderConfig.sampleRate)||r.decoderConfig.sampleRate<=0)throw new TypeError("Audio chunk metadata decoder configuration must specify a valid sampleRate (positive integer).");if(!Number.isInteger(r.decoderConfig.numberOfChannels)||r.decoderConfig.numberOfChannels<=0)throw new TypeError("Audio chunk metadata decoder configuration must specify a valid numberOfChannels (positive integer).");if(r.decoderConfig.description!==void 0&&!Ue(r.decoderConfig.description))throw new TypeError("Audio chunk metadata decoder configuration description, when defined, must be an ArrayBuffer or an ArrayBuffer view.");if(r.decoderConfig.codec.startsWith("mp4a")&&!r.decoderConfig.description)throw new TypeError("Audio chunk metadata decoder configuration for AAC must include a description, which is expected to be an AudioSpecificConfig as specified in ISO 14496-3.");if(r.decoderConfig.codec==="opus"&&r.decoderConfig.description&&r.decoderConfig.description.byteLength<18)throw new TypeError("Invalid decoder description provided for Opus; must be at least 18 bytes long.")},ye=r=>{if(!r)throw new TypeError("Subtitle metadata must be provided.");if(typeof r!="object")throw new TypeError("Subtitle metadata must be an object.");if(!r.config)throw new TypeError("Subtitle metadata must include a config object.");if(typeof r.config!="object")throw new TypeError("Subtitle metadata config must be an object.");if(typeof r.config.description!="string")throw new TypeError("Subtitle metadata config description must be a string.")};var ke=1e3,Gr=2082844800,_=(r,e,t=!0)=>{let i=r*e;return t?Math.round(i):i},ve=class extends ${timestampsMustStartAtZero=!0;writer;boxWriter;fastStart;auxTarget=new se;auxWriter=this.auxTarget._createWriter();auxBoxWriter=new te(this.auxWriter);ftypSize=null;mdat=null;trackDatas=[];creationTime=Math.floor(Date.now()/1e3)+Gr;finalizedChunks=[];nextFragmentNumber=1;constructor(e,t){super(e),this.writer=e._writer,this.boxWriter=new te(this.writer);let i=this.writer instanceof Q?"in-memory":!1;this.fastStart=t._options.fastStart??i,(this.fastStart==="in-memory"||this.fastStart==="fragmented")&&(this.writer.ensureMonotonicity=!0)}async start(){let e=await this.mutex.acquire(),t=this.output._tracks.some(i=>i.type==="video"&&i.source._codec==="avc");this.boxWriter.writeBox(At({holdsAvc:t,fragmented:this.fastStart==="fragmented"})),this.ftypSize=this.writer.getPos(),this.fastStart==="in-memory"?this.mdat=ge(!1):this.fastStart==="fragmented"||(this.mdat=ge(!0),this.boxWriter.writeBox(this.mdat)),await this.writer.flush(),e()}getVideoTrackData(e,t){let i=this.trackDatas.find(o=>o.track===e);if(i)return i;we(t),l(t),l(t.decoderConfig),l(t.decoderConfig.codedWidth!==void 0),l(t.decoderConfig.codedHeight!==void 0);let s={track:e,type:"video",info:{width:t.decoderConfig.codedWidth,height:t.decoderConfig.codedHeight,decoderConfig:t.decoderConfig},timescale:e.metadata.frameRate??57600,samples:[],sampleQueue:[],timestampProcessingQueue:[],timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.trackDatas.push(s),this.trackDatas.sort((o,n)=>o.track.id-n.track.id),s}getAudioTrackData(e,t){let i=this.trackDatas.find(o=>o.track===e);if(i)return i;Se(t),l(t),l(t.decoderConfig);let s={track:e,type:"audio",info:{numberOfChannels:t.decoderConfig.numberOfChannels,sampleRate:t.decoderConfig.sampleRate,decoderConfig:t.decoderConfig},timescale:t.decoderConfig.sampleRate,samples:[],sampleQueue:[],timestampProcessingQueue:[],timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[]};return this.trackDatas.push(s),this.trackDatas.sort((o,n)=>o.track.id-n.track.id),s}getSubtitleTrackData(e,t){let i=this.trackDatas.find(o=>o.track===e);if(i)return i;ye(t),l(t),l(t.config);let s={track:e,type:"subtitle",info:{config:t.config},timescale:1e3,samples:[],sampleQueue:[],timestampProcessingQueue:[],timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,finalizedChunks:[],currentChunk:null,compactlyCodedChunkTable:[],lastCueEndTimestamp:0,cueQueue:[],nextSourceId:0,cueToSourceId:new WeakMap};return this.trackDatas.push(s),this.trackDatas.sort((o,n)=>o.track.id-n.track.id),this.validateAndNormalizeTimestamp(e,0,!0),s}async addEncodedVideoChunk(e,t,i){let s=await this.mutex.acquire();try{let o=this.getVideoTrackData(e,i),n=new Uint8Array(t.byteLength);t.copyTo(n);let a=this.validateAndNormalizeTimestamp(o.track,t.timestamp,t.type==="key"),c=this.createSampleForTrack(o,n,a,(t.duration??0)/1e6,t.type);await this.registerSample(o,c)}finally{s()}}async addEncodedAudioChunk(e,t,i){let s=await this.mutex.acquire();try{let o=this.getAudioTrackData(e,i),n=new Uint8Array(t.byteLength);t.copyTo(n);let a=t.type,c=this.validateAndNormalizeTimestamp(o.track,t.timestamp,a==="key"),u=this.createSampleForTrack(o,n,c,(t.duration??0)/1e6,a);await this.registerSample(o,u)}finally{s()}}async addSubtitleCue(e,t,i){let s=await this.mutex.acquire();try{let o=this.getSubtitleTrackData(e,i);this.validateAndNormalizeTimestamp(o.track,1e6*t.timestamp,!0),e.source._codec==="webvtt"&&(o.cueQueue.push(t),await this.processWebVTTCues(o,t.timestamp))}finally{s()}}async processWebVTTCues(e,t){for(;e.cueQueue.length>0;){let i=new Set([]);for(let u of e.cueQueue)l(u.timestamp<=t),l(e.lastCueEndTimestamp<=u.timestamp+u.duration),i.add(Math.max(u.timestamp,e.lastCueEndTimestamp)),i.add(u.timestamp+u.duration);let s=[...i].sort((u,d)=>u-d),o=s[0],n=s[1]??o;if(t=n)break;L.lastIndex=0;let f=L.test(d.text),T=d.timestamp+d.duration,C=e.cueToSourceId.get(d);if(C===void 0&&ni.timestamp).sort((i,s)=>i-s);for(let i=0;i{if(e===n)return t.type==="key";let a=n.sampleQueue[0];return a&&a.type==="key"});s>=1&&o&&(i=!0,await this.finalizeFragment())}else i=s>=.5}i&&(e.currentChunk&&await this.finalizeCurrentChunk(e),e.currentChunk={startTimestamp:t.timestamp,samples:[],offset:null,moofOffset:null}),l(e.currentChunk),e.currentChunk.samples.push(t),e.timestampProcessingQueue.push(t)}async finalizeCurrentChunk(e){if(l(this.fastStart!=="fragmented"),!!e.currentChunk){if(e.finalizedChunks.push(e.currentChunk),this.finalizedChunks.push(e.currentChunk),(e.compactlyCodedChunkTable.length===0||y(e.compactlyCodedChunkTable).samplesPerChunk!==e.currentChunk.samples.length)&&e.compactlyCodedChunkTable.push({firstChunk:e.finalizedChunks.length,samplesPerChunk:e.currentChunk.samples.length}),this.fastStart==="in-memory"){e.currentChunk.offset=0;return}e.currentChunk.offset=this.writer.getPos();for(let t of e.currentChunk.samples)l(t.data),this.writer.write(t.data),t.data=null;await this.writer.flush()}}async interleaveSamples(){l(this.fastStart==="fragmented");for(let e of this.output._tracks)if(!e.source._closed&&!this.trackDatas.some(t=>t.track===e))return;e:for(;;){let e=null,t=1/0;for(let s of this.trackDatas){if(s.sampleQueue.length===0&&!s.track.source._closed)break e;s.sampleQueue.length>0&&s.sampleQueue[0].timestamp=2**32&&(a.largeSize=!0,u=this.boxWriter.measureBox(a)+c),a.size=u,this.boxWriter.writeBox(a)}for(let a of this.trackDatas){a.currentChunk.offset=this.writer.getPos(),a.currentChunk.moofOffset=i;for(let c of a.currentChunk.samples)this.writer.write(c.data),c.data=null}let o=this.writer.getPos();this.writer.seek(this.boxWriter.offsets.get(s));let n=$e(t,this.trackDatas);this.boxWriter.writeBox(n),this.writer.seek(o);for(let a of this.trackDatas)a.finalizedChunks.push(a.currentChunk),this.finalizedChunks.push(a.currentChunk),a.currentChunk=null;e&&await this.writer.flush()}async onTrackClose(e){let t=await this.mutex.acquire();if(e.type==="subtitle"&&e.source._codec==="webvtt"){let i=this.trackDatas.find(s=>s.track===e);i&&await this.processWebVTTCues(i,1/0)}this.fastStart==="fragmented"&&await this.interleaveSamples(),t()}async finalize(){let e=await this.mutex.acquire();for(let t of this.trackDatas)t.type==="subtitle"&&t.track.source._codec==="webvtt"&&await this.processWebVTTCues(t,1/0);if(this.fastStart==="fragmented"){for(let t of this.trackDatas){for(let i of t.sampleQueue)await this.addSampleToTrack(t,i);this.processTimestamps(t)}await this.finalizeFragment(!1)}else for(let t of this.trackDatas)this.processTimestamps(t),await this.finalizeCurrentChunk(t);if(this.fastStart==="in-memory"){l(this.mdat);let t;for(let s=0;s<2;s++){let o=re(this.trackDatas,this.creationTime),n=this.boxWriter.measureBox(o);t=this.boxWriter.measureBox(this.mdat);let a=this.writer.getPos()+n+t;for(let c of this.finalizedChunks){c.offset=a;for(let{data:u}of c.samples)l(u),a+=u.byteLength,t+=u.byteLength}if(a<2**32)break;t>=2**32&&(this.mdat.largeSize=!0)}let i=re(this.trackDatas,this.creationTime);this.boxWriter.writeBox(i),this.mdat.size=t,this.boxWriter.writeBox(this.mdat);for(let s of this.finalizedChunks)for(let o of s.samples)l(o.data),this.writer.write(o.data),o.data=null}else if(this.fastStart==="fragmented"){let t=this.writer.getPos(),i=Ot(this.trackDatas);this.boxWriter.writeBox(i);let s=this.writer.getPos()-t;this.writer.seek(this.writer.getPos()-4),this.boxWriter.writeU32(s)}else{l(this.mdat),l(this.ftypSize!==null);let t=this.boxWriter.offsets.get(this.mdat);l(t!==void 0);let i=this.writer.getPos()-t;this.mdat.size=i,this.mdat.largeSize=i>=2**32,this.boxWriter.patchBox(this.mdat);let s=re(this.trackDatas,this.creationTime);if(typeof this.fastStart=="object"){this.writer.seek(this.ftypSize),this.boxWriter.writeBox(s);let o=t-this.writer.getPos();this.boxWriter.writeBox(Et(o))}else this.boxWriter.writeBox(s)}e()}};var oe=class{value;constructor(e){this.value=e}},q=class{value;constructor(e){this.value=e}},ne=class{value;constructor(e){this.value=e}};var je=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,$t=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 qe=2**15,Qt="https://github.com/Vanilagy/webm-muxer",jt=6,Kt=5,Yr={avc:"V_MPEG4/ISO/AVC",hevc:"V_MPEGH/ISO/HEVC",vp8:"V_VP8",vp9:"V_VP9",av1:"V_AV1",aac:"A_AAC",opus:"A_OPUS",webvtt:"S_TEXT/WEBVTT"},Zr={video:1,audio:2,subtitle:17},_e=class extends ${timestampsMustStartAtZero=!1;writer;format;helper=new Uint8Array(8);helperView=new DataView(this.helper.buffer);offsets=new WeakMap;dataOffsets=new WeakMap;trackDatas=[];segment=null;segmentInfo=null;seekHead=null;tracksElement=null;segmentDuration=null;cues=null;currentCluster=null;currentClusterMsTimestamp=null;trackDatasInCurrentCluster=new Set;duration=0;constructor(e,t){super(e),this.writer=e._writer,this.format=t,this.format._options.streamable&&(this.writer.ensureMonotonicity=!0)}writeByte(e){this.helperView.setUint8(0,e),this.writer.write(this.helper.subarray(0,1))}writeFloat32(e){this.helperView.setFloat32(0,e,!1),this.writer.write(this.helper.subarray(0,4))}writeFloat64(e){this.helperView.setFloat64(0,e,!1),this.writer.write(this.helper)}writeUnsignedInt(e,t=je(e)){let i=0;switch(t){case 6:this.helperView.setUint8(i++,e/2**40|0);case 5:this.helperView.setUint8(i++,e/2**32|0);case 4:this.helperView.setUint8(i++,e>>24);case 3:this.helperView.setUint8(i++,e>>16);case 2:this.helperView.setUint8(i++,e>>8);case 1:this.helperView.setUint8(i++,e);break;default:throw new Error("Bad UINT size "+t)}this.writer.write(this.helper.subarray(0,i))}writeSignedInt(e,t=Ke(e)){e<0&&(e+=2**(t*8)),this.writeUnsignedInt(e,t)}writeEBMLVarInt(e,t=$t(e)){let i=0;switch(t){case 1:this.helperView.setUint8(i++,128|e);break;case 2:this.helperView.setUint8(i++,64|e>>8),this.helperView.setUint8(i++,e);break;case 3:this.helperView.setUint8(i++,32|e>>16),this.helperView.setUint8(i++,e>>8),this.helperView.setUint8(i++,e);break;case 4:this.helperView.setUint8(i++,16|e>>24),this.helperView.setUint8(i++,e>>16),this.helperView.setUint8(i++,e>>8),this.helperView.setUint8(i++,e);break;case 5:this.helperView.setUint8(i++,8|e/2**32&7),this.helperView.setUint8(i++,e>>24),this.helperView.setUint8(i++,e>>16),this.helperView.setUint8(i++,e>>8),this.helperView.setUint8(i++,e);break;case 6:this.helperView.setUint8(i++,4|e/2**40&3),this.helperView.setUint8(i++,e/2**32|0),this.helperView.setUint8(i++,e>>24),this.helperView.setUint8(i++,e>>16),this.helperView.setUint8(i++,e>>8),this.helperView.setUint8(i++,e);break;default:throw new Error("Bad EBML VINT size "+t)}this.writer.write(this.helper.subarray(0,i))}writeString(e){this.writer.write(new Uint8Array(e.split("").map(t=>t.charCodeAt(0))))}writeEBML(e){if(e!==null){if(e instanceof Uint8Array)this.writer.write(e);else if(Array.isArray(e))for(let t of e)this.writeEBML(t);else if(this.offsets.set(e,this.writer.getPos()),this.writeUnsignedInt(e.id),Array.isArray(e.data)){let t=this.writer.getPos(),i=e.size===-1?1:e.size??4;e.size===-1?this.writeByte(255):this.writer.seek(this.writer.getPos()+i);let s=this.writer.getPos();if(this.dataOffsets.set(e,s),this.writeEBML(e.data),e.size!==-1){let o=this.writer.getPos()-s,n=this.writer.getPos();this.writer.seek(t),this.writeEBMLVarInt(o,i),this.writer.seek(n)}}else if(typeof e.data=="number"){let t=e.size??je(e.data);this.writeEBMLVarInt(t),this.writeUnsignedInt(e.data,t)}else if(typeof e.data=="string")this.writeEBMLVarInt(e.data.length),this.writeString(e.data);else if(e.data instanceof Uint8Array)this.writeEBMLVarInt(e.data.byteLength,e.size),this.writer.write(e.data);else if(e.data instanceof oe)this.writeEBMLVarInt(4),this.writeFloat32(e.data.value);else if(e.data instanceof q)this.writeEBMLVarInt(8),this.writeFloat64(e.data.value);else if(e.data instanceof ne){let t=e.size??Ke(e.data.value);this.writeEBMLVarInt(t),this.writeSignedInt(e.data.value,t)}}}beforeTrackAdd(e){if(this.format instanceof X)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.")}async start(){let e=await this.mutex.acquire();this.writeEBMLHeader(),this.format._options.streamable||this.createSeekHead(),this.createSegmentInfo(),this.createCues(),await this.writer.flush(),e()}writeEBMLHeader(){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.format instanceof X?"webm":"matroska"},{id:17031,data:2},{id:17029,data:2}]};this.writeEBML(e)}createSeekHead(){let e=new Uint8Array([28,83,187,107]),t=new Uint8Array([21,73,169,102]),i=new Uint8Array([22,84,174,107]),s={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.seekHead=s}createSegmentInfo(){let e={id:17545,data:new q(0)};this.segmentDuration=e;let t={id:357149030,data:[{id:2807729,data:1e6},{id:19840,data:Qt},{id:22337,data:Qt},this.format._options.streamable?null:e]};this.segmentInfo=t}createTracks(){let e={id:374648427,data:[]};this.tracksElement=e;for(let t of this.trackDatas)e.data.push({id:174,data:[{id:215,data:t.track.id},{id:29637,data:t.track.id},{id:131,data:Zr[t.type]},{id:134,data:Yr[t.track.source._codec]},t.type==="video"?this.videoSpecificTrackInfo(t):null,t.type==="audio"?this.audioSpecificTrackInfo(t):null,t.type==="subtitle"?this.subtitleSpecificTrackInfo(t):null]})}videoSpecificTrackInfo(e){let t=[e.info.decoderConfig.description?{id:25506,data:P(e.info.decoderConfig.description)}:null,e.track.metadata.frameRate?{id:2352003,data:1e9/e.track.metadata.frameRate}:null],i=e.info.decoderConfig.colorSpace,s={id:224,data:[{id:176,data:e.info.width},{id:186,data:e.info.height},fe(i)?{id:21936,data:[{id:21937,data:U[i.matrix]},{id:21946,data:B[i.transfer]},{id:21947,data:z[i.primaries]},{id:21945,data:i.fullRange?2:1}]}:null]};return t.push(s),t}audioSpecificTrackInfo(e){return[e.info.decoderConfig.description?{id:25506,data:P(e.info.decoderConfig.description)}:null,{id:225,data:[{id:181,data:new oe(e.info.sampleRate)},{id:159,data:e.info.numberOfChannels}]}]}subtitleSpecificTrackInfo(e){return[{id:25506,data:v.encode(e.info.config.description)}]}createSegment(){let e={id:408125543,size:this.format._options.streamable?-1:jt,data:[this.format._options.streamable?null:this.seekHead,this.segmentInfo,this.tracksElement]};this.segment=e,this.writeEBML(e)}createCues(){this.cues={id:475249515,data:[]}}get segmentDataOffset(){return l(this.segment),this.dataOffsets.get(this.segment)}getVideoTrackData(e,t){let i=this.trackDatas.find(o=>o.track===e);if(i)return i;we(t),l(t),l(t.decoderConfig),l(t.decoderConfig.codedWidth!==void 0),l(t.decoderConfig.codedHeight!==void 0);let s={track:e,type:"video",info:{width:t.decoderConfig.codedWidth,height:t.decoderConfig.codedHeight,decoderConfig:t.decoderConfig},chunkQueue:[],lastWrittenMsTimestamp:null};return this.trackDatas.push(s),this.trackDatas.sort((o,n)=>o.track.id-n.track.id),s}getAudioTrackData(e,t){let i=this.trackDatas.find(o=>o.track===e);if(i)return i;Se(t),l(t),l(t.decoderConfig);let s={track:e,type:"audio",info:{numberOfChannels:t.decoderConfig.numberOfChannels,sampleRate:t.decoderConfig.sampleRate,decoderConfig:t.decoderConfig},chunkQueue:[],lastWrittenMsTimestamp:null};return this.trackDatas.push(s),this.trackDatas.sort((o,n)=>o.track.id-n.track.id),s}getSubtitleTrackData(e,t){let i=this.trackDatas.find(o=>o.track===e);if(i)return i;ye(t),l(t),l(t.config);let s={track:e,type:"subtitle",info:{config:t.config},chunkQueue:[],lastWrittenMsTimestamp:null};return this.trackDatas.push(s),this.trackDatas.sort((o,n)=>o.track.id-n.track.id),s}async addEncodedVideoChunk(e,t,i){let s=await this.mutex.acquire();try{let o=this.getVideoTrackData(e,i),n=new Uint8Array(t.byteLength);t.copyTo(n);let a=t.type==="key",c=this.validateAndNormalizeTimestamp(o.track,t.timestamp,a),u=this.createInternalChunk(n,c,(t.duration??0)/1e6,t.type);e.source._codec==="vp9"&&this.fixVP9ColorSpace(o,u),o.chunkQueue.push(u),await this.interleaveChunks()}finally{s()}}async addEncodedAudioChunk(e,t,i){let s=await this.mutex.acquire();try{let o=this.getAudioTrackData(e,i),n=new Uint8Array(t.byteLength);t.copyTo(n);let a=t.type,c=a==="key",u=this.validateAndNormalizeTimestamp(o.track,t.timestamp,c),d=this.createInternalChunk(n,u,(t.duration??0)/1e6,a);o.chunkQueue.push(d),await this.interleaveChunks()}finally{s()}}async addSubtitleCue(e,t,i){let s=await this.mutex.acquire();try{let o=this.getSubtitleTrackData(e,i),n=this.validateAndNormalizeTimestamp(o.track,1e6*t.timestamp,!0),a=t.text,c=Math.floor(n*1e3);L.lastIndex=0,a=a.replace(L,T=>{let k=pe(T.slice(1,-1))-c;return`<${be(k)}>`});let u=v.encode(a),d=`${t.settings??""} +${t.identifier??""} +${t.notes??""}`,f=this.createInternalChunk(u,n,t.duration,"key",d.trim()?v.encode(d):null);o.chunkQueue.push(f),await this.interleaveChunks()}finally{s()}}async interleaveChunks(){for(let e of this.output._tracks)if(!e.source._closed&&!this.trackDatas.some(t=>t.track===e))return;e:for(;;){let e=null,t=1/0;for(let s of this.trackDatas){if(s.chunkQueue.length===0&&!s.track.source._closed)break e;s.chunkQueue.length>0&&s.chunkQueue[0].timestamp=2&&i++;let c={rgb:7,bt709:2,bt470bg:1,smpte170m:3}[e.info.decoderConfig.colorSpace.matrix];bt(t.data,i+0,i+3,c)}createInternalChunk(e,t,i,s,o=null){return{data:e,type:s,timestamp:t,duration:i,additions:o}}writeBlock(e,t){this.segment||(this.createTracks(),this.createSegment());let i=Math.floor(1e3*t.timestamp),s=this.trackDatas.every(d=>{if(d.track.source._closed)return!0;if(e===d)return t.type==="key";let f=d.chunkQueue[0];return f&&f.type==="key"});(!this.currentCluster||s&&i-this.currentClusterMsTimestamp>=1e3)&&this.createNewCluster(i);let o=i-this.currentClusterMsTimestamp;if(o<0)return;if(o>=qe)throw new Error(`Current Matroska cluster exceeded its maximum allowed length of ${qe} milliseconds. In order to produce a correct WebM file, you must pass in a key frame at least every ${qe} milliseconds.`);let a=new Uint8Array(4),c=new DataView(a.buffer);c.setUint8(0,128|e.track.id),c.setInt16(1,o,!1);let u=Math.floor(1e3*t.duration);if(u===0&&!t.additions){c.setUint8(3,+(t.type==="key")<<7);let d={id:163,data:[a,t.data]};this.writeEBML(d)}else{let d={id:160,data:[{id:161,data:[a,t.data]},t.type==="delta"?{id:251,data:new ne(e.lastWrittenMsTimestamp-i)}:null,t.additions?{id:30113,data:[{id:166,data:[{id:165,data:t.additions},{id:238,data:1}]}]}:null,u>0?{id:155,data:u}:null]};this.writeEBML(d)}this.duration=Math.max(this.duration,i+u),e.lastWrittenMsTimestamp=i,this.trackDatasInCurrentCluster.add(e)}createNewCluster(e){this.currentCluster&&!this.format._options.streamable&&this.finalizeCurrentCluster(),this.currentCluster={id:524531317,size:this.format._options.streamable?-1:Kt,data:[{id:231,data:e}]},this.writeEBML(this.currentCluster),this.currentClusterMsTimestamp=e,this.trackDatasInCurrentCluster.clear()}finalizeCurrentCluster(){l(this.currentCluster);let e=this.writer.getPos()-this.dataOffsets.get(this.currentCluster),t=this.writer.getPos();this.writer.seek(this.offsets.get(this.currentCluster)+4),this.writeEBMLVarInt(e,Kt),this.writer.seek(t);let i=this.offsets.get(this.currentCluster)-this.segmentDataOffset;l(this.cues),this.cues.data.push({id:187,data:[{id:179,data:this.currentClusterMsTimestamp},...[...this.trackDatasInCurrentCluster].map(s=>({id:183,data:[{id:247,data:s.track.id},{id:241,data:i}]}))]})}async onTrackClose(){let e=await this.mutex.acquire();await this.interleaveChunks(),e()}async finalize(){let e=await this.mutex.acquire();this.segment||(this.createTracks(),this.createSegment());for(let t of this.trackDatas)for(;t.chunkQueue.length>0;)this.writeBlock(t,t.chunkQueue.shift());if(!this.format._options.streamable&&this.currentCluster&&this.finalizeCurrentCluster(),l(this.cues),this.writeEBML(this.cues),!this.format._options.streamable){let t=this.writer.getPos(),i=this.writer.getPos()-this.segmentDataOffset;this.writer.seek(this.offsets.get(this.segment)+4),this.writeEBMLVarInt(i,jt),this.segmentDuration.data=new q(this.duration),this.writer.seek(this.offsets.get(this.segmentDuration)),this.writeEBML(this.segmentDuration),this.seekHead.data[0].data[1].data=this.offsets.get(this.cues)-this.segmentDataOffset,this.seekHead.data[1].data[1].data=this.offsets.get(this.segmentInfo)-this.segmentDataOffset,this.seekHead.data[2].data[1].data=this.offsets.get(this.tracksElement)-this.segmentDataOffset,this.writer.seek(this.offsets.get(this.seekHead)),this.writeEBML(this.seekHead),this.writer.seek(t)}e()}};var N=class{},Xe=class extends N{_options;constructor(e={}){if(!e||typeof e!="object")throw new TypeError("options must be an object.");if(e.fastStart!==void 0&&![!1,"in-memory","fragmented"].includes(e.fastStart))throw new TypeError('options.fastStart, when provided, must be false, "in-memory", or "fragmented".');super(),this._options=e}_createMuxer(e){return new ve(e,this)}},Ae=class extends N{_options;constructor(e={}){if(!e||typeof e!="object")throw new TypeError("options must be an object.");if(e.streamable!==void 0&&typeof e.streamable!="boolean")throw new TypeError("options.streamable, when provided, must be a boolean.");super(),this._options=e}_createMuxer(e){return new _e(e,this)}},X=class extends Ae{};var G=class{_connectedTrack=null;_closed=!1;_offsetTimestamps=!1;_ensureValidDigest(){if(!this._connectedTrack)throw new Error("Cannot call digest without connecting the source to an output track.");if(!this._connectedTrack.output._started)throw new Error("Cannot call digest before output has been started.");if(this._connectedTrack.output._finalizing)throw new Error("Cannot call digest after output has started finalizing.");if(this._closed)throw new Error("Cannot call digest after source has been closed.")}_start(){}async _flush(){}close(){if(this._closed)throw new Error("Source already closed.");if(!this._connectedTrack)throw new Error("Cannot call close without connecting the source to an output track.");if(!this._connectedTrack.output._started)throw new Error("Cannot call close before output has been started.");this._closed=!0,!this._connectedTrack.output._finalizing&&this._connectedTrack.output._muxer.onTrackClose(this._connectedTrack)}},I=class extends G{_connectedTrack=null;_codec;constructor(e){if(super(),!j.includes(e))throw new TypeError(`Invalid video codec '${e}'. Must be one of: ${j.join(", ")}.`);this._codec=e}},Ge=class extends I{constructor(e){super(e)}digest(e,t){if(!(e instanceof EncodedVideoChunk))throw new TypeError("chunk must be an EncodedVideoChunk.");return this._ensureValidDigest(),this._connectedTrack.output._muxer.addEncodedVideoChunk(this._connectedTrack,e,t)}},Jr=5,ei=r=>{if(!r||typeof r!="object")throw new TypeError("Codec config must be an object.");if(!j.includes(r.codec))throw new TypeError(`Invalid video codec '${r.codec}'. Must be one of: ${j.join(", ")}.`);if(!Number.isInteger(r.bitrate)||r.bitrate<=0)throw new TypeError("config.bitrate must be a positive integer.");if(r.latencyMode!==void 0&&!["quality","realtime"].includes(r.latencyMode))throw new TypeError("config.latencyMode, when provided, must be 'quality' or 'realtime'.")},ae=class{constructor(e,t){this.source=e;this.codecConfig=t;ei(t)}encoder=null;muxer=null;lastMultipleOfKeyFrameInterval=-1;lastWidth=null;lastHeight=null;async digest(e){if(this.source._ensureValidDigest(),this.lastWidth!==null&&this.lastHeight!==null){if(e.codedWidth!==this.lastWidth||e.codedHeight!==this.lastHeight)throw new Error(`Video frame size must remain constant. Expected ${this.lastWidth}x${this.lastHeight}, got ${e.codedWidth}x${e.codedHeight}.`)}else this.lastWidth=e.codedWidth,this.lastHeight=e.codedHeight;this.ensureEncoder(e),l(this.encoder);let t=Math.floor(e.timestamp/1e6/Jr);this.encoder.encode(e,{keyFrame:t!==this.lastMultipleOfKeyFrameInterval}),this.lastMultipleOfKeyFrameInterval=t,this.encoder.encodeQueueSize>=4&&await new Promise(i=>this.encoder.addEventListener("dequeue",i,{once:!0})),await this.muxer.mutex.currentPromise}ensureEncoder(e){this.encoder||(this.encoder=new VideoEncoder({output:(t,i)=>void this.muxer.addEncodedVideoChunk(this.source._connectedTrack,t,i),error:t=>console.error("Video encode error:",t)}),this.encoder.configure({codec:Dt(this.codecConfig.codec,e.codedWidth,e.codedHeight,this.codecConfig.bitrate),width:e.codedWidth,height:e.codedHeight,bitrate:this.codecConfig.bitrate,framerate:this.source._connectedTrack?.metadata.frameRate,latencyMode:this.codecConfig.latencyMode,...Ht(this.codecConfig.codec)}),l(this.source._connectedTrack),this.muxer=this.source._connectedTrack.output._muxer)}async flush(){this.encoder&&(await this.encoder.flush(),this.encoder.close())}},Ye=class extends I{_encoder;constructor(e){super(e.codec),this._encoder=new ae(this,e)}digest(e){if(!(e instanceof VideoFrame))throw new TypeError("videoFrame must be a VideoFrame.");return this._encoder.digest(e)}_flush(){return this._encoder.flush()}},Ze=class extends I{_encoder;_canvas;constructor(e,t){if(!(e instanceof HTMLCanvasElement))throw new TypeError("canvas must be an HTMLCanvasElement.");super(t.codec),this._encoder=new ae(this,t),this._canvas=e}digest(e,t=0){if(!Number.isFinite(e)||e<0)throw new TypeError("timestamp must be a non-negative number.");if(!Number.isFinite(t)||t<0)throw new TypeError("duration must be a non-negative number.");let i=new VideoFrame(this._canvas,{timestamp:Math.round(1e6*e),duration:Math.round(1e6*t),alpha:"discard"}),s=this._encoder.digest(i);return i.close(),s}_flush(){return this._encoder.flush()}},Je=class extends I{_encoder;_abortController=null;_track;_offsetTimestamps=!0;constructor(e,t){if(!(e instanceof MediaStreamTrack)||e.kind!=="video")throw new TypeError("track must be a video MediaStreamTrack.");t={...t,latencyMode:"realtime"},super(t.codec),this._encoder=new ae(this,t),this._track=e}_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()}},O=class extends G{_connectedTrack=null;_codec;constructor(e){if(super(),!K.includes(e))throw new TypeError(`Invalid audio codec '${e}'. Must be one of: ${K.join(", ")}.`);this._codec=e}},et=class extends O{constructor(e){super(e)}digest(e,t){if(!(e instanceof EncodedAudioChunk))throw new TypeError("chunk must be an EncodedAudioChunk.");return this._ensureValidDigest(),this._connectedTrack.output._muxer.addEncodedAudioChunk(this._connectedTrack,e,t)}},ti=r=>{if(!r||typeof r!="object")throw new TypeError("Codec config must be an object.");if(!K.includes(r.codec))throw new TypeError(`Invalid audio codec '${r.codec}'. Must be one of: ${K.join(", ")}.`);if(!Number.isInteger(r.bitrate)||r.bitrate<=0)throw new TypeError("config.bitrate must be a positive integer.")},ce=class{constructor(e,t){this.source=e;this.codecConfig=t;ti(t)}encoder=null;muxer=null;lastNumberOfChannels=null;lastSampleRate=null;async digest(e){if(this.source._ensureValidDigest(),this.lastNumberOfChannels!==null&&this.lastSampleRate!==null){if(e.numberOfChannels!==this.lastNumberOfChannels||e.sampleRate!==this.lastSampleRate)throw new Error(`Audio parameters must remain constant. Expected ${this.lastNumberOfChannels} channels at ${this.lastSampleRate} Hz, got ${e.numberOfChannels} channels at ${e.sampleRate} Hz.`)}else this.lastNumberOfChannels=e.numberOfChannels,this.lastSampleRate=e.sampleRate;this.ensureEncoder(e),l(this.encoder),this.encoder.encode(e),this.encoder.encodeQueueSize>=4&&await new Promise(t=>this.encoder.addEventListener("dequeue",t,{once:!0})),await this.muxer.mutex.currentPromise}ensureEncoder(e){this.encoder||(this.encoder=new AudioEncoder({output:(t,i)=>void this.muxer.addEncodedAudioChunk(this.source._connectedTrack,t,i),error:t=>console.error("Audio encode error:",t)}),this.encoder.configure({codec:Wt(this.codecConfig.codec,e.numberOfChannels,e.sampleRate),numberOfChannels:e.numberOfChannels,sampleRate:e.sampleRate,bitrate:this.codecConfig.bitrate,...Lt(this.codecConfig.codec)}),l(this.source._connectedTrack),this.muxer=this.source._connectedTrack.output._muxer)}async flush(){this.encoder&&(await this.encoder.flush(),this.encoder.close())}},tt=class extends O{_encoder;constructor(e){super(e.codec),this._encoder=new ce(this,e)}digest(e){if(!(e instanceof AudioData))throw new TypeError("audioData must be an AudioData.");return this._encoder.digest(e)}_flush(){return this._encoder.flush()}},rt=class extends O{_encoder;_accumulatedFrameCount=0;constructor(e){super(e.codec),this._encoder=new ce(this,e)}digest(e){if(!(e instanceof AudioBuffer))throw new TypeError("audioBuffer must be an AudioBuffer.");let t=e.numberOfChannels,i=e.sampleRate,s=e.length,o=new Float32Array(t*s);for(let c=0;c{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()}},Y=class extends G{_connectedTrack=null;_codec;constructor(e){if(super(),!Ce.includes(e))throw new TypeError(`Invalid subtitle codec '${e}'. Must be one of: ${Ce.join(", ")}.`);this._codec=e}},st=class extends Y{_parser;constructor(e){super(e),this._parser=new he({codec:e,output:(t,i)=>this._connectedTrack?.output._muxer.addSubtitleCue(this._connectedTrack,t,i),error:t=>console.error("Subtitle parse error:",t)})}digest(e){if(typeof e!="string")throw new TypeError("text must be a string.");return this._ensureValidDigest(),this._parser.parse(e),this._connectedTrack.output._muxer.mutex.currentPromise}};var ot=class{_muxer;_writer;_tracks=[];_started=!1;_finalizing=!1;_mutex=new H;constructor(e){if(!e||typeof e!="object")throw new TypeError("options must be an object.");if(!(e.format instanceof N))throw new TypeError("options.format must be an OutputFormat.");if(!(e.target instanceof W))throw new TypeError("options.target must be a Target.");if(e.target._output)throw new Error("Target is already used for another output.");e.target._output=this,this._writer=e.target._createWriter(),this._muxer=e.format._createMuxer(this)}addVideoTrack(e,t={}){if(!(e instanceof I))throw new TypeError("source must be a VideoSource.");if(!t||typeof t!="object")throw new TypeError("metadata must be an object.");if(typeof t.rotation=="number"&&![0,90,180,270].includes(t.rotation))throw new TypeError(`Invalid video rotation: ${t.rotation}. Has to be 0, 90, 180 or 270.`);if(Array.isArray(t.rotation)&&(t.rotation.length!==9||t.rotation.some(i=>!Number.isFinite(i))))throw new TypeError(`Invalid video transformation matrix: ${t.rotation.join()}`);if(t.frameRate!==void 0&&(!Number.isInteger(t.frameRate)||t.frameRate<=0))throw new TypeError(`Invalid video frame rate: ${t.frameRate}. Must be a positive integer.`);this._addTrack("video",e,t)}addAudioTrack(e,t={}){if(!(e instanceof O))throw new TypeError("source must be an AudioSource.");if(!t||typeof t!="object")throw new TypeError("metadata must be an object.");this._addTrack("audio",e,t)}addSubtitleTrack(e,t={}){if(!(e instanceof Y))throw new TypeError("source must be a SubtitleSource.");if(!t||typeof t!="object")throw new TypeError("metadata must be an object.");this._addTrack("subtitle",e,t)}_addTrack(e,t,i){if(this._started)throw new Error("Cannot add track after output has started.");if(t._connectedTrack)throw new Error("Source is already used for a track.");let s={id:this._tracks.length+1,output:this,type:e,source:t,metadata:i};this._muxer.beforeTrackAdd(s),this._tracks.push(s),t._connectedTrack=s}async start(){if(this._started)throw new Error("Output already started.");this._started=!0,this._writer.start();let e=await this._mutex.acquire();await this._muxer.start();for(let t of this._tracks)t.source._start();e()}async finalize(){if(!this._started)throw new Error("Cannot finalize before starting.");if(this._finalizing)throw new Error("Cannot call finalize twice.");this._finalizing=!0;let e=await this._mutex.acquire(),t=this._tracks.map(i=>i.source._flush());await Promise.all(t),await this._muxer.finalize(),await this._writer.flush(),await this._writer.finalize(),e()}};var ue=class{},nt=class extends ue{constructor(t){super();this.buffer=t}async _read(t,i){return new Uint8Array(this.buffer,t,i-t)}async _getSize(){return this.buffer.byteLength}},at=class extends ue{constructor(t){super();this.blob=t}async _read(t,i){let o=await this.blob.slice(t,i).arrayBuffer();return new Uint8Array(o)}async _getSize(){return this.blob.size}};var Z=class{input;constructor(e){this.input=e}};var Ee=class{_backing;constructor(e){this._backing=e}isVideoTrack(){return this instanceof de}isAudioTrack(){return this instanceof le}getDuration(){return this._backing.getDuration()}},de=class extends Ee{_backing;constructor(e){super(e),this._backing=e}getCodec(){return this._backing.getCodec()}getWidth(){return this._backing.getWidth()}getHeight(){return this._backing.getHeight()}getRotation(){return this._backing.getRotation()}getDecoderConfig(){return this._backing.getDecoderConfig()}async getCodecMimeType(){return(await this.getDecoderConfig()).codec}},le=class extends Ee{_backing;constructor(e){super(e),this._backing=e}getCodec(){return this._backing.getCodec()}getNumberOfChannels(){return this._backing.getNumberOfChannels()}getSampleRate(){return this._backing.getSampleRate()}getDecoderConfig(){return this._backing.getDecoderConfig()}async getCodecMimeType(){return(await this.getDecoderConfig()).codec}};var J=class{constructor(e){this.reader=e}pos=0;readRange(e,t){let{view:i,offset:s}=this.reader.getViewAndOffset(e,t);return new Uint8Array(i.buffer,s,t-e)}readU8(){let{view:e,offset:t}=this.reader.getViewAndOffset(this.pos,this.pos+1);return this.pos++,e.getUint8(t)}readU16(){let{view:e,offset:t}=this.reader.getViewAndOffset(this.pos,this.pos+2);return this.pos+=2,e.getUint16(t,!1)}readU24(){let{view:e,offset:t}=this.reader.getViewAndOffset(this.pos,this.pos+3);this.pos+=3;let i=e.getUint16(t,!1),s=e.getUint8(t+2);return i*256+s}readS32(){let{view:e,offset:t}=this.reader.getViewAndOffset(this.pos,this.pos+4);return this.pos+=4,e.getInt32(t,!1)}readU32(){let{view:e,offset:t}=this.reader.getViewAndOffset(this.pos,this.pos+4);return this.pos+=4,e.getUint32(t,!1)}readI32(){let{view:e,offset:t}=this.reader.getViewAndOffset(this.pos,this.pos+4);return this.pos+=4,e.getInt32(t,!1)}readU64(){let e=this.readU32(),t=this.readU32();return e*4294967296+t}readF64(){let{view:e,offset:t}=this.reader.getViewAndOffset(this.pos,this.pos+8);return this.pos+=8,e.getFloat64(t,!1)}readFixed_16_16(){return this.readS32()/65536}readFixed_2_30(){return this.readS32()/1073741824}readAscii(e){let{view:t,offset:i}=this.reader.getViewAndOffset(this.pos,this.pos+e);this.pos+=e;let s="";for(let o=0;oe.inputTrack)}async getMimeType(){await this.readMetadata();let e="video/mp4";if(this.tracks.length>0){let t=await Promise.all(this.tracks.map(s=>s.inputTrack.getCodecMimeType())),i=[...new Set(t)];e+=`; codecs="${i.join(", ")}"`}return e}readMetadata(){return this.metadataPromise??=(async()=>{let e=await this.isobmffReader.reader.getSourceSize();for(;this.isobmffReader.posi.presentationTimestamp-s.presentationTimestamp),e.sampleTable}readContiguousBoxes(e){let t=this.isobmffReader.pos;for(;this.isobmffReader.pos-td.every((f,T)=>f===c[T]));u===-1?s.rotation=0:s.rotation=90*u}break;case"mdhd":{let s=this.currentTrack;l(s);let o=this.isobmffReader.readU8();this.isobmffReader.pos+=3,o===0?(this.isobmffReader.pos+=8,s.timescale=this.isobmffReader.readU32(),s.durationInTimescale=this.isobmffReader.readU32()):o===1&&(this.isobmffReader.pos+=16,s.timescale=this.isobmffReader.readU32(),s.durationInTimescale=this.isobmffReader.readU64())}break;case"hdlr":{let s=this.currentTrack;l(s),this.isobmffReader.pos+=8;let o=this.isobmffReader.readAscii(4);o==="vide"?s.info={type:"video",width:-1,height:-1,codec:null,codecDescription:null,colorSpace:null}:o==="soun"&&(s.info={type:"audio",numberOfChannels:-1,sampleRate:-1,codec:null,codecDescription:null})}break;case"stbl":{let s=this.currentTrack;l(s),s.sampleTableOffset=e,this.readContiguousBoxes(t.contentSize)}break;case"stsd":{let s=this.currentTrack;if(l(s),s.info===null||s.sampleTable)break;let o=this.isobmffReader.readU8();this.isobmffReader.pos+=3;let n=this.isobmffReader.readU32();for(let a=0;a0){if(u===1)this.isobmffReader.pos+=4*4;else if(u===2){this.isobmffReader.pos+=4,f=this.isobmffReader.readF64(),d=this.isobmffReader.readU32(),this.isobmffReader.pos+=4;let T=this.isobmffReader.readU32(),C=this.isobmffReader.readU32(),k=this.isobmffReader.readU32(),w=this.isobmffReader.readU32()}}s.info.numberOfChannels=d,s.info.sampleRate=f,this.readContiguousBoxes(e+c.totalSize-this.isobmffReader.pos)}}}break;case"avcC":{let s=this.currentTrack;l(s&&s.info),s.info.codecDescription=this.isobmffReader.readRange(this.isobmffReader.pos,this.isobmffReader.pos+t.contentSize)}break;case"hvcC":{let s=this.currentTrack;l(s&&s.info),s.info.codecDescription=this.isobmffReader.readRange(this.isobmffReader.pos,this.isobmffReader.pos+t.contentSize)}break;case"colr":{let s=this.currentTrack;if(l(s&&s.info?.type==="video"),this.isobmffReader.readAscii(4)!=="nclx")break;let n=this.isobmffReader.readU16(),a=this.isobmffReader.readU16(),c=this.isobmffReader.readU16(),u=!!(this.isobmffReader.readU8()&128);s.info.colorSpace={primaries:kt[n],transfer:gt[a],matrix:Tt[c],fullRange:u}}break;case"wave":t.totalSize>8&&this.readContiguousBoxes(t.contentSize);break;case"esds":{let s=this.currentTrack;l(s&&s.info),this.isobmffReader.pos+=4;let o=this.isobmffReader.readU8();l(o===3),this.isobmffReader.readIsomVariableInteger(),this.isobmffReader.pos+=2;let n=this.isobmffReader.readU8(),a=(n&128)!==0,c=(n&64)!==0,u=(n&32)!==0;if(a&&(this.isobmffReader.pos+=2),c){let k=this.isobmffReader.readU8();this.isobmffReader.pos+=k}u&&(this.isobmffReader.pos+=2);let d=this.isobmffReader.readU8();l(d===4),this.isobmffReader.readIsomVariableInteger();let f=this.isobmffReader.readU8();l(f===64),this.isobmffReader.pos+=12;let T=this.isobmffReader.readU8();l(T===5);let C=this.isobmffReader.readIsomVariableInteger();s.info.codecDescription=this.isobmffReader.readRange(this.isobmffReader.pos,this.isobmffReader.pos+C)}break;case"stts":{let s=this.currentTrack;if(l(s),!s.sampleTable)break;this.isobmffReader.pos+=4;let o=this.isobmffReader.readU32(),n=0,a=0;for(let c=0;c{let t=D(r.presentationTimestamps,e,i=>i.presentationTimestamp);return t===-1?-1:r.presentationTimestamps[t].sampleIndex},ii=(r,e)=>{let t=D(r.sampleTimingEntries,e,k=>k.startIndex),i=r.sampleTimingEntries[t];if(!i||i.startIndex+i.count<=e)return null;let o=i.startDecodeTimestamp+(e-i.startIndex)*i.delta,n=D(r.sampleCompositionTimeOffsets,e,k=>k.startIndex),a=r.sampleCompositionTimeOffsets[n];a&&(o+=a.offset);let c=r.sampleSizes[Math.min(e,r.sampleSizes.length-1)],u=D(r.sampleToChunk,e,k=>k.startSampleIndex),d=r.sampleToChunk[u];l(d);let f=d.startChunkIndex+Math.floor((e-d.startSampleIndex)/d.samplesPerChunk),C=r.chunkOffsets[f];if(r.sampleSizes.length===1)C+=c*(e-d.startSampleIndex);else{let k=d.startSampleIndex+(f-d.startChunkIndex)*d.samplesPerChunk;for(let w=k;wk)!==-1:!0}},si=(r,e)=>{if(!r.keySampleIndices)return e;let t=D(r.keySampleIndices,e,i=>i);return r.keySampleIndices[t]??-1},oi=(r,e)=>{if(!r.keySampleIndices)return e+1;let t=D(r.keySampleIndices,e,i=>i);return r.keySampleIndices[t+1]??-1};var Ve=class extends Z{};var Me=class{},dt=class extends Me{async _canReadInput(e){if(await e._reader.getSourceSize()<8)return!1;await e._reader.loadRange(4,8);let i=new J(e._reader);return i.pos=4,i.readAscii(4)==="ftyp"}_createDemuxer(e){return new Ie(e)}},lt=class extends Me{async _canReadInput(){return!1}_createDemuxer(e){return new Ve(e)}},Re=new dt,ni=Re,ai=Re,mt=new lt,Xt=mt,ci=mt,ui=[Re,Xt];var Pe=4096,ze=class{constructor(e){this.source=e}loadedSegments=[];sourceSizePromise=null;getSourceSize(){return this.sourceSizePromise?this.sourceSizePromise:this.sourceSizePromise=this.source._getSize()}async loadRange(e,t){let i=Math.floor(e/Pe)*Pe,s=Math.ceil(t/Pe)*Pe;s=Math.min(s,await this.getSourceSize());let o=this.loadedSegments.find(c=>c.start<=i);o&&(i=Math.max(i,o.end));let n=this.loadedSegments.find(c=>c.end>=s);if(n&&(s=Math.min(s,n.start)),i>=s)return;let a=await this.source._read(i,s);this.insertIntoLoadedSegments(i,a)}insertIntoLoadedSegments(e,t){let i={start:e,end:e+t.byteLength,bytes:t,view:new DataView(t.buffer)},s=this.loadedSegments.findLastIndex(d=>d.start<=e);this.loadedSegments.splice(s+1,0,i),(s===-1||this.loadedSegments[s].enda&&this.loadedSegments[a+1].start<=c;)a++,c=Math.max(c,this.loadedSegments[a].end);if(o===a)return;for(let d=o;d<=a;d++){let f=this.loadedSegments[d];if(f.start===n&&f.end===c){this.loadedSegments.splice(d+1,a-d),this.loadedSegments.splice(o,d-o);return}}let u=new Uint8Array(c-n);for(let d=o;d<=a;d++){let f=this.loadedSegments[d];u.set(f.bytes,f.start-n)}this.loadedSegments.splice(o+1,a-o),this.loadedSegments[o].end=c,this.loadedSegments[o].bytes=u,this.loadedSegments[o].view=new DataView(u.buffer)}getViewAndOffset(e,t){let i=this.loadedSegments.find(s=>s.start<=e&&t<=s.end);if(!i)throw new Error(`No segment loaded for range [${e}, ${t}).`);return{view:i.view,offset:i.bytes.byteOffset+e-i.start}}};var ft=class{_formats;_reader;_demuxerPromise=null;_format=null;constructor(e){this._formats=e.formats,this._reader=new ze(e.source)}_getDemuxer(){return this._demuxerPromise??=(async()=>{for(let e of this._formats)if(await e._canReadInput(this))return this._format=e,e._createDemuxer(this);throw new Error("Input has an unrecognizable format.")})()}async getFormat(){return await this._getDemuxer(),l(this._format),this._format}async getDuration(){return(await this._getDemuxer()).getDuration()}async getTracks(){return(await this._getDemuxer()).getTracks()}async getVideoTracks(){return(await this.getTracks()).filter(t=>t.isVideoTrack())}async getPrimaryVideoTrack(){return(await this.getTracks()).find(t=>t.isVideoTrack())??null}async getAudioTracks(){return(await this.getTracks()).filter(t=>t.isAudioTrack())}async getPrimaryAudioTrack(){return(await this.getTracks()).find(t=>t.isAudioTrack())??null}async getMimeType(){return(await this._getDemuxer()).getMimeType()}};var ht=class{constructor(e){this.videoTrack=e}getFirstChunk(){return this.videoTrack._backing.getFirstChunk()}getChunk(e){return this.videoTrack._backing.getChunk(e)}getNextChunk(e){return this.videoTrack._backing.getNextChunk(e)}getKeyChunk(e){return this.videoTrack._backing.getKeyChunk(e)}getNextKeyChunk(e){return this.videoTrack._backing.getNextKeyChunk(e)}async*chunks(e=0){let t=await this.getChunk(e);for(;t;)yield t,t=await this.getNextChunk(t)}},pt=class{constructor(e){this.videoTrack=e}decoderConfig=null;async createDecoder(e){this.decoderConfig||(this.decoderConfig=await this.videoTrack.getDecoderConfig());let t=new VideoDecoder({output:e,error:i=>console.error(i)});return t.configure(this.decoderConfig),t}async getKeyFrame(e){let t=null,i=await this.createDecoder(o=>t=o),s=await this.videoTrack._backing.getKeyChunk(e);return s?(i.decode(s),await i.flush(),i.close(),t):null}async getFrame(e){let t=null,i=await this.createDecoder(a=>{a.timestamp/1e6<=e?(t?.close(),t=a):a.close()}),s=await this.videoTrack._backing.getKeyChunk(e);if(!s)return null;let o=await this.videoTrack._backing.getChunk(e);l(o),i.decode(s);let n=s;for(;n!==o;){let a=await this.videoTrack._backing.getNextChunk(n);l(a),n=a,i.decode(a),i.decodeQueueSize>=10&&await new Promise(c=>i.addEventListener("dequeue",c,{once:!0}))}return await i.flush(),i.close(),t}async*frames(e=0){let t=[],i=!1,s=null,{promise:o,resolve:n}=Fe(),a=!1,c=await this.createDecoder(f=>{if(a){f.close();return}let T=f.timestamp/1e6;s&&(T>e?(t.push(s),i=!0):s.close()),T>=e&&(t.push(f),i=!0),s=i?null:f,t.length>0&&(n(),{promise:o,resolve:n}=Fe())}),u=await this.videoTrack._backing.getKeyChunk(e);if(!u)return;let d=!1;(async()=>{let f=u;for(;f&&!a;)c.decode(f),c.decodeQueueSize>=10&&await new Promise(C=>c.addEventListener("dequeue",C,{once:!0})),f=await this.videoTrack._backing.getNextChunk(f);await c.flush(),c.close(),d=!0,n()})();try{for(;;)if(t.length>0)yield t.shift();else if(!d)await o;else break}finally{a=!0}}};export{ui as ALL_FORMATS,K as AUDIO_CODECS,nt as ArrayBufferSource,se as ArrayBufferTarget,rt as AudioBufferSource,tt as AudioDataSource,O as AudioSource,at as BlobSource,Ze as CanvasSource,et as EncodedAudioChunkSource,ht as EncodedVideoChunkDrain,Ge as EncodedVideoChunkSource,Re as ISOBMFF,ft as Input,mt as MATROSKA,Xt as MKV,ai as MOV,ni as MP4,G as MediaSource,it as MediaStreamAudioTrackSource,Je as MediaStreamVideoTrackSource,Ae as MkvOutputFormat,Xe as Mp4OutputFormat,ot as Output,N as OutputFormat,Ce as SUBTITLE_CODECS,ue as Source,Qe as StreamTarget,Y as SubtitleSource,W as Target,st as TextSubtitleSource,j as VIDEO_CODECS,pt as VideoFrameDrain,Ye as VideoFrameSource,I as VideoSource,ci as WEBM,X as WebMOutputFormat}; diff --git a/dist/metamuxer.mjs b/dist/metamuxer.mjs index 13fbb5f..8f131ef 100644 --- a/dist/metamuxer.mjs +++ b/dist/metamuxer.mjs @@ -40,6 +40,9 @@ var toUint8Array = (source) => { } }; var textEncoder = new TextEncoder(); +var invertObject = (object) => { + return Object.fromEntries(Object.entries(object).map(([key, value]) => [value, key])); +}; var COLOR_PRIMARIES_MAP = { bt709: 1, // ITU-R BT.709 @@ -48,6 +51,7 @@ var COLOR_PRIMARIES_MAP = { smpte170m: 6 // ITU-R BT.601 525 - SMPTE 170M }; +var COLOR_PRIMARIES_MAP_INVERSE = invertObject(COLOR_PRIMARIES_MAP); var TRANSFER_CHARACTERISTICS_MAP = { "bt709": 1, // ITU-R BT.709 @@ -56,6 +60,7 @@ var TRANSFER_CHARACTERISTICS_MAP = { "iec61966-2-1": 13 // IEC 61966-2-1 }; +var TRANSFER_CHARACTERISTICS_MAP_INVERSE = invertObject(TRANSFER_CHARACTERISTICS_MAP); var MATRIX_COEFFICIENTS_MAP = { rgb: 0, // Identity @@ -66,6 +71,7 @@ var MATRIX_COEFFICIENTS_MAP = { smpte170m: 6 // SMPTE 170M }; +var MATRIX_COEFFICIENTS_MAP_INVERSE = invertObject(MATRIX_COEFFICIENTS_MAP); var colorSpaceIsComplete = (colorSpace) => { return !!colorSpace && !!colorSpace.primaries && !!colorSpace.transfer && !!colorSpace.matrix && colorSpace.fullRange !== void 0; }; @@ -73,9 +79,7 @@ var isAllowSharedBufferSource = (x) => { return x instanceof ArrayBuffer || typeof SharedArrayBuffer !== "undefined" && x instanceof SharedArrayBuffer || ArrayBuffer.isView(x) && !(x instanceof DataView); }; var AsyncMutex = class { - constructor() { - this.currentPromise = Promise.resolve(); - } + currentPromise = Promise.resolve(); async acquire() { let resolver; const nextPromise = new Promise((resolve) => { @@ -87,15 +91,87 @@ var AsyncMutex = class { return resolver; } }; +var rotationMatrix = (rotationInDegrees) => { + const theta = rotationInDegrees * (Math.PI / 180); + const cosTheta = Math.cos(theta); + const sinTheta = Math.sin(theta); + return [ + cosTheta, + sinTheta, + 0, + -sinTheta, + cosTheta, + 0, + 0, + 0, + 1 + ]; +}; +var IDENTITY_MATRIX = rotationMatrix(0); +var bytesToHexString = (bytes2) => { + return [...bytes2].map((x) => x.toString(16).padStart(2, "0")).join(""); +}; +var reverseBitsU32 = (x) => { + x = x >> 1 & 1431655765 | (x & 1431655765) << 1; + x = x >> 2 & 858993459 | (x & 858993459) << 2; + x = x >> 4 & 252645135 | (x & 252645135) << 4; + x = x >> 8 & 16711935 | (x & 16711935) << 8; + x = x >> 16 & 65535 | (x & 65535) << 16; + return x >>> 0; +}; +var binarySearchExact = (arr, key, valueGetter) => { + let low = 0; + let high = arr.length - 1; + let res = -1; + while (low <= high) { + const mid = low + high >> 1; + const midVal = valueGetter(arr[mid]); + if (midVal === key) { + res = mid; + high = mid - 1; + } else if (midVal < key) { + low = mid + 1; + } else { + high = mid - 1; + } + } + return res; +}; +var binarySearchLessOrEqual = (arr, key, valueGetter) => { + let ans = -1; + let low = 0; + let high = arr.length - 1; + while (low <= high) { + const mid = low + (high - low + 1) / 2 | 0; + const midVal = valueGetter(arr[mid]); + if (midVal <= key) { + ans = mid; + low = mid + 1; + } else { + high = mid - 1; + } + } + return ans; +}; +var promiseWithResolvers = () => { + let resolve; + let reject; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; +}; // src/subtitles.ts var cueBlockHeaderRegex = /(?:(.+?)\n)?((?:\d{2}:)?\d{2}:\d{2}.\d{3})\s+-->\s+((?:\d{2}:)?\d{2}:\d{2}.\d{3})/g; var preambleStartRegex = /^WEBVTT(.|\n)*?\n{2}/; var inlineTimestampRegex = /<(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})>/g; var SubtitleParser = class { + options; + preambleText = null; + preambleEmitted = false; constructor(options) { - this.preambleText = null; - this.preambleEmitted = false; this.options = options; } parse(text) { @@ -172,14 +248,14 @@ var formatSubtitleTimestamp = (timestamp) => { var IsobmffBoxWriter = class { constructor(writer) { this.writer = writer; - this.helper = new Uint8Array(8); - this.helperView = new DataView(this.helper.buffer); - /** - * Stores the position from the start of the file to where boxes elements have been written. This is used to - * rewrite/edit elements that were already added before, and to measure sizes of things. - */ - this.offsets = /* @__PURE__ */ new WeakMap(); } + helper = new Uint8Array(8); + helperView = new DataView(this.helper.buffer); + /** + * Stores the position from the start of the file to where boxes elements have been written. This is used to + * rewrite/edit elements that were already added before, and to measure sizes of things. + */ + offsets = /* @__PURE__ */ new WeakMap(); writeU32(value) { this.helperView.setUint32(0, value, false); this.writer.write(this.helper.subarray(0, 4)); @@ -319,23 +395,6 @@ var lastPresentedSample = (samples) => { } return result; }; -var rotationMatrix = (rotationInDegrees) => { - const theta = rotationInDegrees * (Math.PI / 180); - const cosTheta = Math.cos(theta); - const sinTheta = Math.sin(theta); - return [ - cosTheta, - sinTheta, - 0, - -sinTheta, - cosTheta, - 0, - 0, - 0, - 1 - ]; -}; -var IDENTITY_MATRIX = rotationMatrix(0); var matrixToBytes = (matrix) => { return [ fixed_16_16(matrix[0]), @@ -1071,9 +1130,9 @@ var SUBTITLE_CODEC_TO_CONFIGURATION_BOX = { // src/muxer.ts var Muxer = class { + output; + mutex = new AsyncMutex(); constructor(output) { - this.mutex = new AsyncMutex(); - this.trackTimestampInfo = /* @__PURE__ */ new WeakMap(); this.output = output; } // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -1082,6 +1141,7 @@ var Muxer = class { // eslint-disable-next-line @typescript-eslint/no-unused-vars onTrackClose(track) { } + trackTimestampInfo = /* @__PURE__ */ new WeakMap(); validateAndNormalizeTimestamp(track, rawTimestampInUs, isKeyFrame) { let timestampInSeconds = rawTimestampInUs / 1e6; let timestampInfo = this.trackTimestampInfo.get(track); @@ -1125,20 +1185,19 @@ var Muxer = class { // src/writer.ts var Writer = class { - constructor() { - /** Setting this to true will cause the writer to ensure data is written in a strictly monotonic, streamable way. */ - this.ensureMonotonicity = false; - } + /** Setting this to true will cause the writer to ensure data is written in a strictly monotonic, streamable way. */ + ensureMonotonicity = false; start() { } }; var ArrayBufferTargetWriter = class extends Writer { + pos = 0; + target; + buffer = new ArrayBuffer(2 ** 16); + bytes = new Uint8Array(this.buffer); + maxPos = 0; constructor(target) { super(); - this.pos = 0; - this.buffer = new ArrayBuffer(2 ** 16); - this.bytes = new Uint8Array(this.buffer); - this.maxPos = 0; this.target = target; } ensureSize(size) { @@ -1165,7 +1224,6 @@ var ArrayBufferTargetWriter = class extends Writer { } async flush() { } - // eslint-disable-next-line @typescript-eslint/require-await async finalize() { this.ensureSize(this.pos); this.target.buffer = this.buffer.slice(0, Math.max(this.maxPos, this.pos)); @@ -1175,12 +1233,13 @@ var ArrayBufferTargetWriter = class extends Writer { } }; var StreamTargetWriter = class extends Writer { + pos = 0; + target; + sections = []; + lastFlushEnd = 0; + writer = null; constructor(target) { super(); - this.pos = 0; - this.sections = []; - this.lastFlushEnd = 0; - this.writer = null; this.target = target; } start() { @@ -1250,17 +1309,19 @@ var StreamTargetWriter = class extends Writer { var DEFAULT_CHUNK_SIZE = 2 ** 24; var MAX_CHUNKS_AT_ONCE = 2; var ChunkedStreamTargetWriter = class extends Writer { + pos = 0; + target; + chunkSize; + /** + * The data is divided up into fixed-size chunks, whose contents are first filled in RAM and then flushed out. + * A chunk is flushed if all of its contents have been written. + */ + chunks = []; + lastFlushEnd = 0; + writer = null; + flushedChunkQueue = []; constructor(target) { super(); - this.pos = 0; - /** - * The data is divided up into fixed-size chunks, whose contents are first filled in RAM and then flushed out. - * A chunk is flushed if all of its contents have been written. - */ - this.chunks = []; - this.lastFlushEnd = 0; - this.writer = null; - this.flushedChunkQueue = []; this.target = target; this.chunkSize = target._options?.chunkSize ?? DEFAULT_CHUNK_SIZE; if (!Number.isInteger(this.chunkSize) || this.chunkSize < 2 ** 10) { @@ -1378,22 +1439,21 @@ var ChunkedStreamTargetWriter = class extends Writer { // src/target.ts var Target = class { - constructor() { - /** @internal */ - this._output = null; - } + /** @internal */ + _output = null; }; var ArrayBufferTarget = class extends Target { - constructor() { - super(...arguments); - this.buffer = null; - } + buffer = null; /** @internal */ _createWriter() { return new ArrayBufferTargetWriter(this); } }; var StreamTarget = class extends Target { + /** @internal */ + _writable; + /** @internal */ + _options; constructor(writable, options = {}) { super(); if (!(writable instanceof WritableStream)) { @@ -1418,6 +1478,9 @@ var StreamTarget = class extends Target { }; // src/codec.ts +var VIDEO_CODECS = ["avc", "hevc", "vp8", "vp9", "av1"]; +var AUDIO_CODECS = ["aac", "opus"]; +var SUBTITLE_CODECS = ["webvtt"]; var AVC_LEVEL_TABLE = [ { maxMacroblocks: 99, maxBitrate: 64e3, level: 10 }, // Level 1 @@ -1625,6 +1688,43 @@ var buildVideoCodecString = (codec, width, height, bitrate) => { } throw new TypeError(`Unhandled codec '${codec}'.`); }; +var extractVideoCodecString = (codec, description) => { + if (codec === "avc") { + if (!description || description.byteLength < 4) { + throw new TypeError("AVC description must be at least 4 bytes long."); + } + return `avc1.${bytesToHexString(description.subarray(1, 4))}`; + } else if (codec === "hevc") { + if (!description) { + throw new TypeError("HEVC description must be provided."); + } + const view2 = new DataView(description.buffer, description.byteOffset, description.byteLength); + let codecString = "hev1."; + const generalProfileSpace = description[1] >> 6 & 3; + const generalProfileIdc = description[1] & 31; + codecString += ["", "A", "B", "C"][generalProfileSpace] + generalProfileIdc; + codecString += "."; + const compatibilityFlags = reverseBitsU32(view2.getUint32(2)); + codecString += compatibilityFlags.toString(16); + codecString += "."; + const generalTierFlag = description[1] >> 5 & 1; + const generalLevelIdc = description[12]; + codecString += generalTierFlag === 0 ? "L" : "H"; + codecString += generalLevelIdc; + codecString += "."; + const constraintFlags = []; + for (let i = 0; i < 6; i++) { + const byte = description[i + 13]; + constraintFlags.push(byte); + } + while (constraintFlags[constraintFlags.length - 1] === 0) { + constraintFlags.pop(); + } + codecString += constraintFlags.map((x) => x.toString(16)).join("."); + return codecString; + } + throw new TypeError(`Unhandled codec '${codec}'.`); +}; var buildAudioCodecString = (codec, numberOfChannels, sampleRate) => { if (codec === "aac") { if (numberOfChannels >= 2 && sampleRate <= 24e3) { @@ -1641,6 +1741,20 @@ var buildAudioCodecString = (codec, numberOfChannels, sampleRate) => { } throw new TypeError(`Unhandled codec '${codec}'.`); }; +var extractAudioCodecString = (codec, description) => { + if (codec === "aac") { + if (!description || description.byteLength < 2) { + throw new TypeError("AAC description must be at least 2 bytes long."); + } + const mpeg4AudioObjectType = description[0] >> 3; + return `mp4a.40.${mpeg4AudioObjectType}`; + } else if (codec === "opus") { + return "opus"; + } else if (codec === "vorbis") { + return "vorbis"; + } + throw new TypeError(`Unhandled codec '${codec}'.`); +}; var getVideoEncoderConfigExtension = (codec) => { if (codec === "avc") { return { @@ -1822,18 +1936,21 @@ var intoTimescale = (timeInSeconds, timescale, round = true) => { return round ? Math.round(value) : value; }; var IsobmffMuxer = class extends Muxer { + timestampsMustStartAtZero = true; + writer; + boxWriter; + fastStart; + auxTarget = new ArrayBufferTarget(); + auxWriter = this.auxTarget._createWriter(); + auxBoxWriter = new IsobmffBoxWriter(this.auxWriter); + ftypSize = null; + mdat = null; + trackDatas = []; + creationTime = Math.floor(Date.now() / 1e3) + TIMESTAMP_OFFSET; + finalizedChunks = []; + nextFragmentNumber = 1; constructor(output, format) { super(output); - this.timestampsMustStartAtZero = true; - this.auxTarget = new ArrayBufferTarget(); - this.auxWriter = this.auxTarget._createWriter(); - this.auxBoxWriter = new IsobmffBoxWriter(this.auxWriter); - this.ftypSize = null; - this.mdat = null; - this.trackDatas = []; - this.creationTime = Math.floor(Date.now() / 1e3) + TIMESTAMP_OFFSET; - this.finalizedChunks = []; - this.nextFragmentNumber = 1; this.writer = output._writer; this.boxWriter = new IsobmffBoxWriter(this.writer); const fastStartDefault = this.writer instanceof ArrayBufferTargetWriter ? "in-memory" : false; @@ -2422,16 +2539,19 @@ var IsobmffMuxer = class extends Muxer { // src/matroska/ebml.ts var EBMLFloat32 = class { + value; constructor(value) { this.value = value; } }; var EBMLFloat64 = class { + value; constructor(value) { this.value = value; } }; var EBMLSignedInt = class { + value; constructor(value) { this.value = value; } @@ -2505,29 +2625,31 @@ var TRACK_TYPE_MAP = { subtitle: 17 }; var MatroskaMuxer = class extends Muxer { + timestampsMustStartAtZero = false; + writer; + format; + helper = new Uint8Array(8); + helperView = new DataView(this.helper.buffer); + /** + * Stores the position from the start of the file to where EBML elements have been written. This is used to + * rewrite/edit elements that were already added before, and to measure sizes of things. + */ + offsets = /* @__PURE__ */ new WeakMap(); + /** Same as offsets, but stores position where the element's data starts (after ID and size fields). */ + dataOffsets = /* @__PURE__ */ new WeakMap(); + trackDatas = []; + segment = null; + segmentInfo = null; + seekHead = null; + tracksElement = null; + segmentDuration = null; + cues = null; + currentCluster = null; + currentClusterMsTimestamp = null; + trackDatasInCurrentCluster = /* @__PURE__ */ new Set(); + duration = 0; constructor(output, format) { super(output); - this.timestampsMustStartAtZero = false; - this.helper = new Uint8Array(8); - this.helperView = new DataView(this.helper.buffer); - /** - * Stores the position from the start of the file to where EBML elements have been written. This is used to - * rewrite/edit elements that were already added before, and to measure sizes of things. - */ - this.offsets = /* @__PURE__ */ new WeakMap(); - /** Same as offsets, but stores position where the element's data starts (after ID and size fields). */ - this.dataOffsets = /* @__PURE__ */ new WeakMap(); - this.trackDatas = []; - this.segment = null; - this.segmentInfo = null; - this.seekHead = null; - this.tracksElement = null; - this.segmentDuration = null; - this.cues = null; - this.currentCluster = null; - this.currentClusterMsTimestamp = null; - this.trackDatasInCurrentCluster = /* @__PURE__ */ new Set(); - this.duration = 0; this.writer = output._writer; this.format = format; if (this.format._options.streamable) { @@ -3202,6 +3324,8 @@ ${cue.notes ?? ""}`; var OutputFormat = class { }; var Mp4OutputFormat = class extends OutputFormat { + /** @internal */ + _options; constructor(options = {}) { if (!options || typeof options !== "object") { throw new TypeError("options must be an object."); @@ -3218,6 +3342,8 @@ var Mp4OutputFormat = class extends OutputFormat { } }; var MkvOutputFormat2 = class extends OutputFormat { + /** @internal */ + _options; constructor(options = {}) { if (!options || typeof options !== "object") { throw new TypeError("options must be an object."); @@ -3236,19 +3362,14 @@ var MkvOutputFormat2 = class extends OutputFormat { var WebMOutputFormat = class extends MkvOutputFormat2 { }; -// src/source.ts -var VIDEO_CODECS = ["avc", "hevc", "vp8", "vp9", "av1"]; -var AUDIO_CODECS = ["aac", "opus"]; -var SUBTITLE_CODECS = ["webvtt"]; +// src/media-source.ts var MediaSource = class { - constructor() { - /** @internal */ - this._connectedTrack = null; - /** @internal */ - this._closed = false; - /** @internal */ - this._offsetTimestamps = false; - } + /** @internal */ + _connectedTrack = null; + /** @internal */ + _closed = false; + /** @internal */ + _offsetTimestamps = false; /** @internal */ _ensureValidDigest() { if (!this._connectedTrack) { @@ -3288,10 +3409,12 @@ var MediaSource = class { } }; var VideoSource = class extends MediaSource { + /** @internal */ + _connectedTrack = null; + /** @internal */ + _codec; constructor(codec) { super(); - /** @internal */ - this._connectedTrack = null; if (!VIDEO_CODECS.includes(codec)) { throw new TypeError(`Invalid video codec '${codec}'. Must be one of: ${VIDEO_CODECS.join(", ")}.`); } @@ -3329,13 +3452,13 @@ var VideoEncoderWrapper = class { constructor(source, codecConfig) { this.source = source; this.codecConfig = codecConfig; - this.encoder = null; - this.muxer = null; - this.lastMultipleOfKeyFrameInterval = -1; - this.lastWidth = null; - this.lastHeight = null; validateVideoCodecConfig(codecConfig); } + encoder = null; + muxer = null; + lastMultipleOfKeyFrameInterval = -1; + lastWidth = null; + lastHeight = null; async digest(videoFrame) { this.source._ensureValidDigest(); if (this.lastWidth !== null && this.lastHeight !== null) { @@ -3393,6 +3516,8 @@ var VideoEncoderWrapper = class { } }; var VideoFrameSource = class extends VideoSource { + /** @internal */ + _encoder; constructor(codecConfig) { super(codecConfig.codec); this._encoder = new VideoEncoderWrapper(this, codecConfig); @@ -3409,6 +3534,10 @@ var VideoFrameSource = class extends VideoSource { } }; var CanvasSource = class extends VideoSource { + /** @internal */ + _encoder; + /** @internal */ + _canvas; constructor(canvas, codecConfig) { if (!(canvas instanceof HTMLCanvasElement)) { throw new TypeError("canvas must be an HTMLCanvasElement."); @@ -3439,6 +3568,14 @@ var CanvasSource = class extends VideoSource { } }; var MediaStreamVideoTrackSource = class extends VideoSource { + /** @internal */ + _encoder; + /** @internal */ + _abortController = null; + /** @internal */ + _track; + /** @internal */ + _offsetTimestamps = true; constructor(track, codecConfig) { if (!(track instanceof MediaStreamTrack) || track.kind !== "video") { throw new TypeError("track must be a video MediaStreamTrack."); @@ -3448,10 +3585,6 @@ var MediaStreamVideoTrackSource = class extends VideoSource { latencyMode: "realtime" }; super(codecConfig.codec); - /** @internal */ - this._abortController = null; - /** @internal */ - this._offsetTimestamps = true; this._encoder = new VideoEncoderWrapper(this, codecConfig); this._track = track; } @@ -3482,10 +3615,12 @@ var MediaStreamVideoTrackSource = class extends VideoSource { } }; var AudioSource = class extends MediaSource { + /** @internal */ + _connectedTrack = null; + /** @internal */ + _codec; constructor(codec) { super(); - /** @internal */ - this._connectedTrack = null; if (!AUDIO_CODECS.includes(codec)) { throw new TypeError(`Invalid audio codec '${codec}'. Must be one of: ${AUDIO_CODECS.join(", ")}.`); } @@ -3519,12 +3654,12 @@ var AudioEncoderWrapper = class { constructor(source, codecConfig) { this.source = source; this.codecConfig = codecConfig; - this.encoder = null; - this.muxer = null; - this.lastNumberOfChannels = null; - this.lastSampleRate = null; validateAudioCodecConfig(codecConfig); } + encoder = null; + muxer = null; + lastNumberOfChannels = null; + lastSampleRate = null; async digest(audioData) { this.source._ensureValidDigest(); if (this.lastNumberOfChannels !== null && this.lastSampleRate !== null) { @@ -3571,6 +3706,8 @@ var AudioEncoderWrapper = class { } }; var AudioDataSource = class extends AudioSource { + /** @internal */ + _encoder; constructor(codecConfig) { super(codecConfig.codec); this._encoder = new AudioEncoderWrapper(this, codecConfig); @@ -3587,10 +3724,12 @@ var AudioDataSource = class extends AudioSource { } }; var AudioBufferSource = class extends AudioSource { + /** @internal */ + _encoder; + /** @internal */ + _accumulatedFrameCount = 0; constructor(codecConfig) { super(codecConfig.codec); - /** @internal */ - this._accumulatedFrameCount = 0; this._encoder = new AudioEncoderWrapper(this, codecConfig); } digest(audioBuffer) { @@ -3624,15 +3763,19 @@ var AudioBufferSource = class extends AudioSource { } }; var MediaStreamAudioTrackSource = class extends AudioSource { + /** @internal */ + _encoder; + /** @internal */ + _abortController = null; + /** @internal */ + _track; + /** @internal */ + _offsetTimestamps = true; constructor(track, codecConfig) { if (!(track instanceof MediaStreamTrack) || track.kind !== "audio") { throw new TypeError("track must be an audio MediaStreamTrack."); } super(codecConfig.codec); - /** @internal */ - this._abortController = null; - /** @internal */ - this._offsetTimestamps = true; this._encoder = new AudioEncoderWrapper(this, codecConfig); this._track = track; } @@ -3663,10 +3806,12 @@ var MediaStreamAudioTrackSource = class extends AudioSource { } }; var SubtitleSource = class extends MediaSource { + /** @internal */ + _connectedTrack = null; + /** @internal */ + _codec; constructor(codec) { super(); - /** @internal */ - this._connectedTrack = null; if (!SUBTITLE_CODECS.includes(codec)) { throw new TypeError(`Invalid subtitle codec '${codec}'. Must be one of: ${SUBTITLE_CODECS.join(", ")}.`); } @@ -3674,6 +3819,8 @@ var SubtitleSource = class extends MediaSource { } }; var TextSubtitleSource = class extends SubtitleSource { + /** @internal */ + _parser; constructor(codec) { super(codec); this._parser = new SubtitleParser({ @@ -3694,15 +3841,19 @@ var TextSubtitleSource = class extends SubtitleSource { // src/output.ts var Output = class { + /** @internal */ + _muxer; + /** @internal */ + _writer; + /** @internal */ + _tracks = []; + /** @internal */ + _started = false; + /** @internal */ + _finalizing = false; + /** @internal */ + _mutex = new AsyncMutex(); constructor(options) { - /** @internal */ - this._tracks = []; - /** @internal */ - this._started = false; - /** @internal */ - this._finalizing = false; - /** @internal */ - this._mutex = new AsyncMutex(); if (!options || typeof options !== "object") { throw new TypeError("options must be an object."); } @@ -3805,15 +3956,1321 @@ var Output = class { release(); } }; + +// src/source.ts +var Source = class { +}; +var ArrayBufferSource = class extends Source { + constructor(buffer) { + super(); + this.buffer = buffer; + } + /** @internal */ + async _read(start, end) { + return new Uint8Array(this.buffer, start, end - start); + } + /** @internal */ + async _getSize() { + return this.buffer.byteLength; + } +}; +var BlobSource = class extends Source { + constructor(blob) { + super(); + this.blob = blob; + } + /** @internal */ + async _read(start, end) { + const slice = this.blob.slice(start, end); + const buffer = await slice.arrayBuffer(); + return new Uint8Array(buffer); + } + /** @internal */ + async _getSize() { + return this.blob.size; + } +}; + +// src/demuxer.ts +var Demuxer = class { + input; + constructor(input) { + this.input = input; + } +}; + +// src/input-track.ts +var InputTrack = class { + /** @internal */ + _backing; + /** @internal */ + constructor(backing) { + this._backing = backing; + } + isVideoTrack() { + return this instanceof InputVideoTrack; + } + isAudioTrack() { + return this instanceof InputAudioTrack; + } + getDuration() { + return this._backing.getDuration(); + } +}; +var InputVideoTrack = class extends InputTrack { + /** @internal */ + _backing; + /** @internal */ + constructor(backing) { + super(backing); + this._backing = backing; + } + getCodec() { + return this._backing.getCodec(); + } + getWidth() { + return this._backing.getWidth(); + } + getHeight() { + return this._backing.getHeight(); + } + getRotation() { + return this._backing.getRotation(); + } + getDecoderConfig() { + return this._backing.getDecoderConfig(); + } + async getCodecMimeType() { + const decoderConfig = await this.getDecoderConfig(); + return decoderConfig.codec; + } +}; +var InputAudioTrack = class extends InputTrack { + /** @internal */ + _backing; + /** @internal */ + constructor(backing) { + super(backing); + this._backing = backing; + } + getCodec() { + return this._backing.getCodec(); + } + getNumberOfChannels() { + return this._backing.getNumberOfChannels(); + } + getSampleRate() { + return this._backing.getSampleRate(); + } + getDecoderConfig() { + return this._backing.getDecoderConfig(); + } + async getCodecMimeType() { + const decoderConfig = await this.getDecoderConfig(); + return decoderConfig.codec; + } +}; + +// src/isobmff/isobmff-reader.ts +var IsobmffReader = class { + constructor(reader) { + this.reader = reader; + } + pos = 0; + readRange(start, end) { + const { view: view2, offset } = this.reader.getViewAndOffset(start, end); + return new Uint8Array(view2.buffer, offset, end - start); + } + readU8() { + const { view: view2, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 1); + this.pos++; + return view2.getUint8(offset); + } + readU16() { + const { view: view2, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 2); + this.pos += 2; + return view2.getUint16(offset, false); + } + readU24() { + const { view: view2, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 3); + this.pos += 3; + const high = view2.getUint16(offset, false); + const low = view2.getUint8(offset + 2); + return high * 256 + low; + } + readS32() { + const { view: view2, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 4); + this.pos += 4; + return view2.getInt32(offset, false); + } + readU32() { + const { view: view2, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 4); + this.pos += 4; + return view2.getUint32(offset, false); + } + readI32() { + const { view: view2, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 4); + this.pos += 4; + return view2.getInt32(offset, false); + } + readU64() { + const high = this.readU32(); + const low = this.readU32(); + return high * 4294967296 + low; + } + readF64() { + const { view: view2, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 8); + this.pos += 8; + return view2.getFloat64(offset, false); + } + readFixed_16_16() { + return this.readS32() / 65536; + } + readFixed_2_30() { + return this.readS32() / 1073741824; + } + readAscii(length) { + const { view: view2, offset } = this.reader.getViewAndOffset(this.pos, this.pos + length); + this.pos += length; + let str = ""; + for (let i = 0; i < length; i++) { + str += String.fromCharCode(view2.getUint8(offset + i)); + } + return str; + } + readIsomVariableInteger() { + let result = 0; + for (let i = 0; i < 4; i++) { + result <<= 7; + const nextByte = this.readU8(); + result |= nextByte & 127; + if ((nextByte & 128) === 0) { + break; + } + } + return result; + } + readBoxHeader() { + let totalSize = this.readU32(); + const name = this.readAscii(4); + let headerSize = 8; + const hasLargeSize = totalSize === 1; + if (hasLargeSize) { + totalSize = this.readU64(); + headerSize = 16; + } + return { name, totalSize, headerSize, contentSize: totalSize - headerSize }; + } +}; + +// src/isobmff/isobmff-demuxer.ts +var knownMatrixes = [rotationMatrix(0), rotationMatrix(90), rotationMatrix(180), rotationMatrix(270)]; +var IsobmffDemuxer = class extends Demuxer { + isobmffReader; + currentTrack = null; + tracks = []; + metadataPromise = null; + movieTimescale = -1; + movieDurationInTimescale = -1; + constructor(input) { + super(input); + this.isobmffReader = new IsobmffReader(input._reader); + } + async getDuration() { + await this.readMetadata(); + if (this.movieDurationInTimescale === -1) { + throw new Error("Could not read movie duration."); + } + return this.movieDurationInTimescale / this.movieTimescale; + } + async getTracks() { + await this.readMetadata(); + return this.tracks.map((track) => track.inputTrack); + } + async getMimeType() { + await this.readMetadata(); + let string = "video/mp4"; + if (this.tracks.length > 0) { + const codecMimeTypes = await Promise.all(this.tracks.map((x) => x.inputTrack.getCodecMimeType())); + const uniqueCodecMimeTypes = [...new Set(codecMimeTypes)]; + string += `; codecs="${uniqueCodecMimeTypes.join(", ")}"`; + } + return string; + } + readMetadata() { + return this.metadataPromise ??= (async () => { + const sourceSize = await this.isobmffReader.reader.getSourceSize(); + while (this.isobmffReader.pos < sourceSize) { + await this.isobmffReader.reader.loadRange(this.isobmffReader.pos, this.isobmffReader.pos + 16); + const startPos = this.isobmffReader.pos; + const boxInfo = this.isobmffReader.readBoxHeader(); + if (boxInfo.name === "moov") { + await this.isobmffReader.reader.loadRange( + this.isobmffReader.pos, + this.isobmffReader.pos + boxInfo.contentSize + ); + this.readContiguousBoxes(boxInfo.contentSize); + return; + } + this.isobmffReader.pos = startPos + boxInfo.totalSize; + } + })(); + } + getSampleTableForTrack(internalTrack) { + if (internalTrack.sampleTable) { + return internalTrack.sampleTable; + } + const sampleTable = { + sampleTimingEntries: [], + sampleCompositionTimeOffsets: [], + sampleSizes: [], + keySampleIndices: null, + chunkOffsets: [], + sampleToChunk: [], + presentationTimestamps: [] + }; + internalTrack.sampleTable = sampleTable; + this.isobmffReader.pos = internalTrack.sampleTableOffset; + this.currentTrack = internalTrack; + this.traverseBox(); + this.currentTrack = null; + for (const entry of sampleTable.sampleTimingEntries) { + for (let i = 0; i < entry.count; i++) { + sampleTable.presentationTimestamps.push({ + presentationTimestamp: entry.startDecodeTimestamp + i * entry.delta, + sampleIndex: entry.startIndex + i + }); + } + } + for (const entry of sampleTable.sampleCompositionTimeOffsets) { + for (let i = 0; i < entry.count; i++) { + const sampleIndex = entry.startIndex + i; + const sample = sampleTable.presentationTimestamps[sampleIndex]; + if (!sample) { + continue; + } + sample.presentationTimestamp += entry.offset; + } + } + sampleTable.presentationTimestamps.sort((a, b) => a.presentationTimestamp - b.presentationTimestamp); + return internalTrack.sampleTable; + } + readContiguousBoxes(totalSize) { + const startIndex = this.isobmffReader.pos; + while (this.isobmffReader.pos - startIndex < totalSize) { + this.traverseBox(); + } + } + traverseBox() { + const startPos = this.isobmffReader.pos; + const boxInfo = this.isobmffReader.readBoxHeader(); + const boxEndPos = startPos + boxInfo.totalSize; + switch (boxInfo.name) { + case "mdia": + case "minf": + case "dinf": + { + this.readContiguousBoxes(boxInfo.contentSize); + } + ; + break; + case "mvhd": + { + const version = this.isobmffReader.readU8(); + this.isobmffReader.pos += 3; + if (version === 1) { + this.isobmffReader.pos += 8 + 8; + this.movieTimescale = this.isobmffReader.readU32(); + this.movieDurationInTimescale = this.isobmffReader.readU64(); + } else { + this.isobmffReader.pos += 4 + 4; + this.movieTimescale = this.isobmffReader.readU32(); + this.movieDurationInTimescale = this.isobmffReader.readU32(); + } + } + ; + break; + case "trak": + { + const track = { + id: -1, + demuxer: this, + inputTrack: null, + info: null, + timescale: -1, + durationInTimescale: -1, + rotation: 0, + sampleTableOffset: -1, + sampleTable: null + }; + this.currentTrack = track; + this.readContiguousBoxes(boxInfo.contentSize); + if (track.id !== -1 && track.timescale !== -1 && track.info !== null) { + if (track.info.type === "video" && track.info.codec !== null) { + const videoTrack = track; + track.inputTrack = new InputVideoTrack(new IsobmffVideoTrackBacking(videoTrack)); + this.tracks.push(track); + } else if (track.info.type === "audio" && track.info.codec !== null) { + const audioTrack = track; + track.inputTrack = new InputAudioTrack(new IsobmffAudioTrackBacking(audioTrack)); + this.tracks.push(track); + } + } + this.currentTrack = null; + } + ; + break; + case "tkhd": + { + const track = this.currentTrack; + assert(track); + const version = this.isobmffReader.readU8(); + const flags = this.isobmffReader.readU24(); + const trackEnabled = (flags & 1) !== 0; + if (!trackEnabled) { + break; + } + if (version === 0) { + this.isobmffReader.pos += 8; + track.id = this.isobmffReader.readU32(); + this.isobmffReader.pos += 8; + } else if (version === 1) { + this.isobmffReader.pos += 16; + track.id = this.isobmffReader.readU32(); + this.isobmffReader.pos += 12; + } else { + throw new Error(`Incorrect track header version ${version}.`); + } + this.isobmffReader.pos += 2 * 4 + 2 + 2 + 2 + 2; + const rotationMatrix2 = []; + rotationMatrix2.push(this.isobmffReader.readFixed_16_16(), this.isobmffReader.readFixed_16_16()); + this.isobmffReader.pos += 4; + rotationMatrix2.push(this.isobmffReader.readFixed_16_16(), this.isobmffReader.readFixed_16_16()); + const matrixIndex = knownMatrixes.findIndex((x) => x.every((y, i) => y === rotationMatrix2[i])); + if (matrixIndex === -1) { + track.rotation = 0; + } else { + track.rotation = 90 * matrixIndex; + } + } + ; + break; + case "mdhd": + { + const track = this.currentTrack; + assert(track); + const version = this.isobmffReader.readU8(); + this.isobmffReader.pos += 3; + if (version === 0) { + this.isobmffReader.pos += 8; + track.timescale = this.isobmffReader.readU32(); + track.durationInTimescale = this.isobmffReader.readU32(); + } else if (version === 1) { + this.isobmffReader.pos += 16; + track.timescale = this.isobmffReader.readU32(); + track.durationInTimescale = this.isobmffReader.readU64(); + } + } + ; + break; + case "hdlr": + { + const track = this.currentTrack; + assert(track); + this.isobmffReader.pos += 8; + const handlerType = this.isobmffReader.readAscii(4); + if (handlerType === "vide") { + track.info = { + type: "video", + width: -1, + height: -1, + codec: null, + codecDescription: null, + colorSpace: null + }; + } else if (handlerType === "soun") { + track.info = { + type: "audio", + numberOfChannels: -1, + sampleRate: -1, + codec: null, + codecDescription: null + }; + } + } + ; + break; + case "stbl": + { + const track = this.currentTrack; + assert(track); + track.sampleTableOffset = startPos; + this.readContiguousBoxes(boxInfo.contentSize); + } + ; + break; + case "stsd": + { + const track = this.currentTrack; + assert(track); + if (track.info === null || track.sampleTable) { + break; + } + const stsdVersion = this.isobmffReader.readU8(); + this.isobmffReader.pos += 3; + const entries = this.isobmffReader.readU32(); + for (let i = 0; i < entries; i++) { + const sampleBoxInfo = this.isobmffReader.readBoxHeader(); + if (track.info.type === "video") { + if (sampleBoxInfo.name === "avc1") { + track.info.codec = "avc"; + } else if (sampleBoxInfo.name === "hvc1" || sampleBoxInfo.name === "hev1") { + track.info.codec = "hevc"; + } else { + console.warn(`Unsupported video sample entry type ${sampleBoxInfo.name}.`); + break; + } + this.isobmffReader.pos += 6 * 1 + 2 + 2 + 2 + 3 * 4; + track.info.width = this.isobmffReader.readU16(); + track.info.height = this.isobmffReader.readU16(); + this.isobmffReader.pos += 4 + 4 + 4 + 2 + 32 + 2 + 2; + this.readContiguousBoxes(startPos + sampleBoxInfo.totalSize - this.isobmffReader.pos); + } else { + if (sampleBoxInfo.name === "mp4a") { + track.info.codec = "aac"; + } else if (sampleBoxInfo.name.toLowerCase() === "opus") { + track.info.codec = "opus"; + } else { + console.warn(`Unsupported audio sample entry type ${sampleBoxInfo.name}.`); + break; + } + this.isobmffReader.pos += 6 * 1 + 2; + const version = this.isobmffReader.readU16(); + this.isobmffReader.pos += 3 * 2; + let channelCount = this.isobmffReader.readU16(); + this.isobmffReader.pos += 2 + 2 + 2; + let sampleRate = this.isobmffReader.readU32() / 65536; + if (stsdVersion === 0 && version > 0) { + if (version === 1) { + this.isobmffReader.pos += 4 * 4; + } else if (version === 2) { + this.isobmffReader.pos += 4; + sampleRate = this.isobmffReader.readF64(); + channelCount = this.isobmffReader.readU32(); + this.isobmffReader.pos += 4; + const sampleSize = this.isobmffReader.readU32(); + const flags = this.isobmffReader.readU32(); + const bytesPerFrame = this.isobmffReader.readU32(); + const samplesPerFrame = this.isobmffReader.readU32(); + } + } + track.info.numberOfChannels = channelCount; + track.info.sampleRate = sampleRate; + this.readContiguousBoxes(startPos + sampleBoxInfo.totalSize - this.isobmffReader.pos); + } + } + } + ; + break; + case "avcC": + { + const track = this.currentTrack; + assert(track && track.info); + track.info.codecDescription = this.isobmffReader.readRange( + this.isobmffReader.pos, + this.isobmffReader.pos + boxInfo.contentSize + ); + } + ; + break; + case "hvcC": + { + const track = this.currentTrack; + assert(track && track.info); + track.info.codecDescription = this.isobmffReader.readRange( + this.isobmffReader.pos, + this.isobmffReader.pos + boxInfo.contentSize + ); + } + ; + break; + case "colr": + { + const track = this.currentTrack; + assert(track && track.info?.type === "video"); + const colourType = this.isobmffReader.readAscii(4); + if (colourType !== "nclx") { + break; + } + const colourPrimaries = this.isobmffReader.readU16(); + const transferCharacteristics = this.isobmffReader.readU16(); + const matrixCoefficients = this.isobmffReader.readU16(); + const fullRangeFlag = Boolean(this.isobmffReader.readU8() & 128); + track.info.colorSpace = { + primaries: COLOR_PRIMARIES_MAP_INVERSE[colourPrimaries], + transfer: TRANSFER_CHARACTERISTICS_MAP_INVERSE[transferCharacteristics], + matrix: MATRIX_COEFFICIENTS_MAP_INVERSE[matrixCoefficients], + fullRange: fullRangeFlag + }; + } + ; + break; + case "wave": + { + if (boxInfo.totalSize > 8) { + this.readContiguousBoxes(boxInfo.contentSize); + } + } + ; + break; + case "esds": + { + const track = this.currentTrack; + assert(track && track.info); + this.isobmffReader.pos += 4; + const tag = this.isobmffReader.readU8(); + assert(tag === 3); + this.isobmffReader.readIsomVariableInteger(); + this.isobmffReader.pos += 2; + const mixed = this.isobmffReader.readU8(); + const streamDependenceFlag = (mixed & 128) !== 0; + const urlFlag = (mixed & 64) !== 0; + const ocrStreamFlag = (mixed & 32) !== 0; + if (streamDependenceFlag) { + this.isobmffReader.pos += 2; + } + if (urlFlag) { + const urlLength = this.isobmffReader.readU8(); + this.isobmffReader.pos += urlLength; + } + if (ocrStreamFlag) { + this.isobmffReader.pos += 2; + } + const decoderConfigTag = this.isobmffReader.readU8(); + assert(decoderConfigTag === 4); + this.isobmffReader.readIsomVariableInteger(); + const objectTypeIndication = this.isobmffReader.readU8(); + assert(objectTypeIndication === 64); + this.isobmffReader.pos += 1 + 3 + 4 + 4; + const decoderSpecificInfoTag = this.isobmffReader.readU8(); + assert(decoderSpecificInfoTag === 5); + const decoderSpecificInfoLength = this.isobmffReader.readIsomVariableInteger(); + track.info.codecDescription = this.isobmffReader.readRange( + this.isobmffReader.pos, + this.isobmffReader.pos + decoderSpecificInfoLength + ); + } + ; + break; + case "stts": + { + const track = this.currentTrack; + assert(track); + if (!track.sampleTable) { + break; + } + this.isobmffReader.pos += 4; + const entryCount = this.isobmffReader.readU32(); + let currentIndex = 0; + let currentTimestamp = 0; + for (let i = 0; i < entryCount; i++) { + const sampleCount = this.isobmffReader.readU32(); + const sampleDelta = this.isobmffReader.readU32(); + track.sampleTable.sampleTimingEntries.push({ + startIndex: currentIndex, + startDecodeTimestamp: currentTimestamp, + count: sampleCount, + delta: sampleDelta + }); + currentIndex += sampleCount; + currentTimestamp += sampleCount * sampleDelta; + } + } + ; + break; + case "ctts": + { + const track = this.currentTrack; + assert(track); + if (!track.sampleTable) { + break; + } + this.isobmffReader.pos += 1 + 3; + const entryCount = this.isobmffReader.readU32(); + let sampleIndex = 0; + for (let i = 0; i < entryCount; i++) { + const sampleCount = this.isobmffReader.readU32(); + const sampleOffset = this.isobmffReader.readI32(); + track.sampleTable.sampleCompositionTimeOffsets.push({ + startIndex: sampleIndex, + count: sampleCount, + offset: sampleOffset + }); + sampleIndex += sampleCount; + } + } + ; + break; + case "stsz": + { + const track = this.currentTrack; + assert(track); + if (!track.sampleTable) { + break; + } + this.isobmffReader.pos += 4; + const sampleSize = this.isobmffReader.readU32(); + const sampleCount = this.isobmffReader.readU32(); + if (sampleSize === 0) { + for (let i = 0; i < sampleCount; i++) { + const sampleSize2 = this.isobmffReader.readU32(); + track.sampleTable.sampleSizes.push(sampleSize2); + } + } else { + track.sampleTable.sampleSizes.push(sampleSize); + } + } + ; + break; + case "stz2": + { + throw new Error("Unsupported."); + } + ; + case "stss": + { + const track = this.currentTrack; + assert(track); + if (!track.sampleTable) { + break; + } + this.isobmffReader.pos += 4; + track.sampleTable.keySampleIndices = []; + const entryCount = this.isobmffReader.readU32(); + for (let i = 0; i < entryCount; i++) { + const sampleIndex = this.isobmffReader.readU32() - 1; + track.sampleTable.keySampleIndices.push(sampleIndex); + } + } + ; + break; + case "stsc": + { + const track = this.currentTrack; + assert(track); + if (!track.sampleTable) { + break; + } + this.isobmffReader.pos += 4; + const entryCount = this.isobmffReader.readU32(); + for (let i = 0; i < entryCount; i++) { + const startChunkIndex = this.isobmffReader.readU32() - 1; + const samplesPerChunk = this.isobmffReader.readU32(); + const sampleDescriptionIndex = this.isobmffReader.readU32(); + track.sampleTable.sampleToChunk.push({ + startSampleIndex: -1, + startChunkIndex, + samplesPerChunk, + sampleDescriptionIndex + }); + } + let startSampleIndex = 0; + for (let i = 0; i < track.sampleTable.sampleToChunk.length; i++) { + track.sampleTable.sampleToChunk[i].startSampleIndex = startSampleIndex; + if (i < track.sampleTable.sampleToChunk.length - 1) { + const nextChunk = track.sampleTable.sampleToChunk[i + 1]; + const chunkCount = nextChunk.startChunkIndex - track.sampleTable.sampleToChunk[i].startChunkIndex; + startSampleIndex += chunkCount * track.sampleTable.sampleToChunk[i].samplesPerChunk; + } + } + } + ; + break; + case "stco": + { + const track = this.currentTrack; + assert(track); + if (!track.sampleTable) { + break; + } + this.isobmffReader.pos += 4; + const entryCount = this.isobmffReader.readU32(); + for (let i = 0; i < entryCount; i++) { + const chunkOffset = this.isobmffReader.readU32(); + track.sampleTable.chunkOffsets.push(chunkOffset); + } + } + ; + break; + case "co64": + { + const track = this.currentTrack; + assert(track); + if (!track.sampleTable) { + break; + } + this.isobmffReader.pos += 4; + const entryCount = this.isobmffReader.readU32(); + for (let i = 0; i < entryCount; i++) { + const chunkOffset = this.isobmffReader.readU64(); + track.sampleTable.chunkOffsets.push(chunkOffset); + } + } + ; + break; + } + this.isobmffReader.pos = boxEndPos; + } +}; +var IsobmffTrackBacking = class { + constructor(internalTrack) { + this.internalTrack = internalTrack; + } + chunkToSampleIndex = /* @__PURE__ */ new WeakMap(); + sampleIndexToChunk = /* @__PURE__ */ new Map(); + getCodec() { + throw new Error("Not implemented on base class."); + } + async getDuration() { + return this.internalTrack.durationInTimescale / this.internalTrack.timescale; + } +}; +var IsobmffVideoTrackBacking = class extends IsobmffTrackBacking { + internalTrack; + constructor(internalTrack) { + super(internalTrack); + this.internalTrack = internalTrack; + } + async getCodec() { + return this.internalTrack.info.codec; + } + async getWidth() { + return this.internalTrack.info.width; + } + async getHeight() { + return this.internalTrack.info.height; + } + async getRotation() { + return this.internalTrack.rotation; + } + async getDecoderConfig() { + return { + codec: extractVideoCodecString(this.internalTrack.info.codec, this.internalTrack.info.codecDescription), + codedWidth: this.internalTrack.info.width, + codedHeight: this.internalTrack.info.height, + description: this.internalTrack.info.codecDescription ?? void 0, + colorSpace: this.internalTrack.info.colorSpace ?? void 0 + }; + } + async fetchChunkForSampleIndex(sampleIndex) { + if (sampleIndex === -1) { + return null; + } + const existingChunk = this.sampleIndexToChunk.get(sampleIndex)?.deref(); + if (existingChunk) { + return existingChunk; + } + const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack); + const sampleInfo = getSampleInfo(sampleTable, sampleIndex); + if (!sampleInfo) { + return null; + } + const data = await this.internalTrack.demuxer.isobmffReader.reader.source._read( + sampleInfo.byteOffset, + sampleInfo.byteOffset + sampleInfo.byteSize + ); + const chunk = new EncodedVideoChunk({ + data, + timestamp: 1e6 * sampleInfo.presentationTimestamp / this.internalTrack.timescale, + duration: 1e6 * sampleInfo.duration / this.internalTrack.timescale, + type: sampleInfo.isKeyFrame ? "key" : "delta" + }); + this.chunkToSampleIndex.set(chunk, sampleIndex); + this.sampleIndexToChunk.set(sampleIndex, new WeakRef(chunk)); + return chunk; + } + async getFirstChunk() { + return this.fetchChunkForSampleIndex(0); + } + async getChunk(timestamp) { + const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack); + const sampleIndex = getSampleIndexForTimestamp(sampleTable, timestamp * this.internalTrack.timescale); + return this.fetchChunkForSampleIndex(sampleIndex); + } + async getNextChunk(chunk) { + const sampleIndex = this.chunkToSampleIndex.get(chunk); + if (sampleIndex === void 0) { + throw new Error("Chunk was not created from this track."); + } + return this.fetchChunkForSampleIndex(sampleIndex + 1); + } + async getKeyChunk(timestamp) { + const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack); + const sampleIndex = getSampleIndexForTimestamp(sampleTable, timestamp * this.internalTrack.timescale); + const keyFrameSampleIndex = sampleIndex === -1 ? -1 : getRelevantKeyframeIndexForSample(sampleTable, sampleIndex); + return this.fetchChunkForSampleIndex(keyFrameSampleIndex); + } + async getNextKeyChunk(chunk) { + const sampleIndex = this.chunkToSampleIndex.get(chunk); + if (sampleIndex === void 0) { + throw new Error("Chunk was not created from this track."); + } + const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack); + const nextKeyFrameSampleIndex = getNextKeyframeIndexForSample(sampleTable, sampleIndex); + return this.fetchChunkForSampleIndex(nextKeyFrameSampleIndex); + } +}; +var IsobmffAudioTrackBacking = class extends IsobmffTrackBacking { + internalTrack; + constructor(internalTrack) { + super(internalTrack); + this.internalTrack = internalTrack; + } + async getCodec() { + return this.internalTrack.info.codec; + } + async getNumberOfChannels() { + return this.internalTrack.info.numberOfChannels; + } + async getSampleRate() { + return this.internalTrack.info.sampleRate; + } + async getDecoderConfig() { + return { + codec: extractAudioCodecString(this.internalTrack.info.codec, this.internalTrack.info.codecDescription), + numberOfChannels: this.internalTrack.info.numberOfChannels, + sampleRate: this.internalTrack.info.sampleRate, + description: this.internalTrack.info.codecDescription ?? void 0 + }; + } +}; +var getSampleIndexForTimestamp = (sampleTable, timescaleUnits) => { + const index = binarySearchLessOrEqual( + sampleTable.presentationTimestamps, + timescaleUnits, + (x) => x.presentationTimestamp + ); + if (index === -1) { + return -1; + } + return sampleTable.presentationTimestamps[index].sampleIndex; +}; +var getSampleInfo = (sampleTable, sampleIndex) => { + const timingEntryIndex = binarySearchLessOrEqual(sampleTable.sampleTimingEntries, sampleIndex, (x) => x.startIndex); + const timingEntry = sampleTable.sampleTimingEntries[timingEntryIndex]; + if (!timingEntry || timingEntry.startIndex + timingEntry.count <= sampleIndex) { + return null; + } + const decodeTimestamp = timingEntry.startDecodeTimestamp + (sampleIndex - timingEntry.startIndex) * timingEntry.delta; + let presentationTimestamp = decodeTimestamp; + const offsetEntryIndex = binarySearchLessOrEqual( + sampleTable.sampleCompositionTimeOffsets, + sampleIndex, + (x) => x.startIndex + ); + const offsetEntry = sampleTable.sampleCompositionTimeOffsets[offsetEntryIndex]; + if (offsetEntry) { + presentationTimestamp += offsetEntry.offset; + } + const sampleSize = sampleTable.sampleSizes[Math.min(sampleIndex, sampleTable.sampleSizes.length - 1)]; + const chunkEntryIndex = binarySearchLessOrEqual(sampleTable.sampleToChunk, sampleIndex, (x) => x.startSampleIndex); + const chunkEntry = sampleTable.sampleToChunk[chunkEntryIndex]; + assert(chunkEntry); + const chunkIndex = chunkEntry.startChunkIndex + Math.floor((sampleIndex - chunkEntry.startSampleIndex) / chunkEntry.samplesPerChunk); + const chunkOffset = sampleTable.chunkOffsets[chunkIndex]; + let sampleOffset = chunkOffset; + if (sampleTable.sampleSizes.length === 1) { + sampleOffset += sampleSize * (sampleIndex - chunkEntry.startSampleIndex); + } else { + const startSampleIndex = chunkEntry.startSampleIndex + (chunkIndex - chunkEntry.startChunkIndex) * chunkEntry.samplesPerChunk; + for (let i = startSampleIndex; i < sampleIndex; i++) { + sampleOffset += sampleTable.sampleSizes[i]; + } + } + return { + presentationTimestamp, + duration: timingEntry.delta, + byteOffset: sampleOffset, + byteSize: sampleSize, + isKeyFrame: sampleTable.keySampleIndices ? binarySearchExact(sampleTable.keySampleIndices, sampleIndex, (x) => x) !== -1 : true + }; +}; +var getRelevantKeyframeIndexForSample = (sampleTable, sampleIndex) => { + if (!sampleTable.keySampleIndices) { + return sampleIndex; + } + const index = binarySearchLessOrEqual(sampleTable.keySampleIndices, sampleIndex, (x) => x); + return sampleTable.keySampleIndices[index] ?? -1; +}; +var getNextKeyframeIndexForSample = (sampleTable, sampleIndex) => { + if (!sampleTable.keySampleIndices) { + return sampleIndex + 1; + } + const index = binarySearchLessOrEqual(sampleTable.keySampleIndices, sampleIndex, (x) => x); + return sampleTable.keySampleIndices[index + 1] ?? -1; +}; + +// src/matroska/matroska-demuxer.ts +var MatroskaDemuxer = class extends Demuxer { +}; + +// src/input-format.ts +var InputFormat = class { +}; +var IsobmffInputFormat = class extends InputFormat { + /** @internal */ + async _canReadInput(input) { + const sourceSize = await input._reader.getSourceSize(); + if (sourceSize < 8) { + return false; + } + await input._reader.loadRange(4, 8); + const isobmffReader = new IsobmffReader(input._reader); + isobmffReader.pos = 4; + const fourCc = isobmffReader.readAscii(4); + return fourCc === "ftyp"; + } + _createDemuxer(input) { + return new IsobmffDemuxer(input); + } +}; +var MatroskaInputFormat = class extends InputFormat { + /** @internal */ + async _canReadInput() { + return false; + } + _createDemuxer(input) { + return new MatroskaDemuxer(input); + } +}; +var ISOBMFF = new IsobmffInputFormat(); +var MP4 = ISOBMFF; +var MOV = ISOBMFF; +var MATROSKA = new MatroskaInputFormat(); +var MKV = MATROSKA; +var WEBM = MATROSKA; +var ALL_FORMATS = [ISOBMFF, MKV]; + +// src/reader.ts +var PAGE_SIZE = 4096; +var Reader = class { + constructor(source) { + this.source = source; + } + loadedSegments = []; + sourceSizePromise = null; + getSourceSize() { + if (this.sourceSizePromise) { + return this.sourceSizePromise; + } else { + return this.sourceSizePromise = this.source._getSize(); + } + } + async loadRange(start, end) { + let alignedStart = Math.floor(start / PAGE_SIZE) * PAGE_SIZE; + let alignedEnd = Math.ceil(end / PAGE_SIZE) * PAGE_SIZE; + alignedEnd = Math.min(alignedEnd, await this.getSourceSize()); + const thing = this.loadedSegments.find((x) => x.start <= alignedStart); + if (thing) { + alignedStart = Math.max(alignedStart, thing.end); + } + const thing2 = this.loadedSegments.find((x) => x.end >= alignedEnd); + if (thing2) { + alignedEnd = Math.min(alignedEnd, thing2.start); + } + if (alignedStart >= alignedEnd) { + return; + } + const bytes2 = await this.source._read(alignedStart, alignedEnd); + this.insertIntoLoadedSegments(alignedStart, bytes2); + } + insertIntoLoadedSegments(start, bytes2) { + const segment = { + start, + end: start + bytes2.byteLength, + bytes: bytes2, + view: new DataView(bytes2.buffer) + }; + let index = this.loadedSegments.findLastIndex((x) => x.start <= start); + this.loadedSegments.splice(index + 1, 0, segment); + if (index === -1 || this.loadedSegments[index].end < segment.start) { + index++; + } + const mergeSectionStartIndex = index; + const mergeSectionStart = this.loadedSegments[mergeSectionStartIndex].start; + let mergeSectionEndIndex = index; + let mergeSectionEnd = this.loadedSegments[mergeSectionEndIndex].end; + while (this.loadedSegments.length - 1 > mergeSectionEndIndex && this.loadedSegments[mergeSectionEndIndex + 1].start <= mergeSectionEnd) { + mergeSectionEndIndex++; + mergeSectionEnd = Math.max(mergeSectionEnd, this.loadedSegments[mergeSectionEndIndex].end); + } + if (mergeSectionStartIndex === mergeSectionEndIndex) { + return; + } + for (let i = mergeSectionStartIndex; i <= mergeSectionEndIndex; i++) { + const segment2 = this.loadedSegments[i]; + const coversEntireMergeSection = segment2.start === mergeSectionStart && segment2.end === mergeSectionEnd; + if (coversEntireMergeSection) { + this.loadedSegments.splice(i + 1, mergeSectionEndIndex - i); + this.loadedSegments.splice(mergeSectionStartIndex, i - mergeSectionStartIndex); + return; + } + } + const unifiedBytes = new Uint8Array(mergeSectionEnd - mergeSectionStart); + for (let i = mergeSectionStartIndex; i <= mergeSectionEndIndex; i++) { + const segment2 = this.loadedSegments[i]; + unifiedBytes.set(segment2.bytes, segment2.start - mergeSectionStart); + } + this.loadedSegments.splice(mergeSectionStartIndex + 1, mergeSectionEndIndex - mergeSectionStartIndex); + this.loadedSegments[mergeSectionStartIndex].end = mergeSectionEnd; + this.loadedSegments[mergeSectionStartIndex].bytes = unifiedBytes; + this.loadedSegments[mergeSectionStartIndex].view = new DataView(unifiedBytes.buffer); + } + getViewAndOffset(start, end) { + const segment = this.loadedSegments.find((x) => x.start <= start && end <= x.end); + if (!segment) { + throw new Error(`No segment loaded for range [${start}, ${end}).`); + } + return { + view: segment.view, + offset: segment.bytes.byteOffset + start - segment.start + }; + } +}; + +// src/input.ts +var Input = class { + /** @internal */ + _formats; + /** @internal */ + _reader; + /** @internal */ + _demuxerPromise = null; + /** @internal */ + _format = null; + constructor(options) { + this._formats = options.formats; + this._reader = new Reader(options.source); + } + /** @internal */ + _getDemuxer() { + return this._demuxerPromise ??= (async () => { + for (const format of this._formats) { + const canRead = await format._canReadInput(this); + if (canRead) { + this._format = format; + return format._createDemuxer(this); + } + } + throw new Error("Input has an unrecognizable format."); + })(); + } + async getFormat() { + await this._getDemuxer(); + assert(this._format); + return this._format; + } + async getDuration() { + const demuxer = await this._getDemuxer(); + return demuxer.getDuration(); + } + async getTracks() { + const demuxer = await this._getDemuxer(); + return demuxer.getTracks(); + } + async getVideoTracks() { + const tracks = await this.getTracks(); + return tracks.filter((x) => x.isVideoTrack()); + } + async getPrimaryVideoTrack() { + const tracks = await this.getTracks(); + return tracks.find((x) => x.isVideoTrack()) ?? null; + } + async getAudioTracks() { + const tracks = await this.getTracks(); + return tracks.filter((x) => x.isAudioTrack()); + } + async getPrimaryAudioTrack() { + const tracks = await this.getTracks(); + return tracks.find((x) => x.isAudioTrack()) ?? null; + } + async getMimeType() { + const demuxer = await this._getDemuxer(); + return demuxer.getMimeType(); + } +}; + +// src/media-drain.ts +var EncodedVideoChunkDrain = class { + constructor(videoTrack) { + this.videoTrack = videoTrack; + } + getFirstChunk() { + return this.videoTrack._backing.getFirstChunk(); + } + getChunk(timestamp) { + return this.videoTrack._backing.getChunk(timestamp); + } + getNextChunk(chunk) { + return this.videoTrack._backing.getNextChunk(chunk); + } + getKeyChunk(timestamp) { + return this.videoTrack._backing.getKeyChunk(timestamp); + } + getNextKeyChunk(chunk) { + return this.videoTrack._backing.getNextKeyChunk(chunk); + } + async *chunks(startTimestamp = 0) { + let chunk = await this.getChunk(startTimestamp); + while (chunk) { + yield chunk; + chunk = await this.getNextChunk(chunk); + } + } +}; +var VideoFrameDrain = class { + constructor(videoTrack) { + this.videoTrack = videoTrack; + } + decoderConfig = null; + async createDecoder(onFrame) { + if (!this.decoderConfig) { + this.decoderConfig = await this.videoTrack.getDecoderConfig(); + } + const decoder = new VideoDecoder({ + output: onFrame, + error: (error) => console.error(error) + }); + decoder.configure(this.decoderConfig); + return decoder; + } + async getKeyFrame(timestamp) { + let result = null; + const decoder = await this.createDecoder((frame) => result = frame); + const chunk = await this.videoTrack._backing.getKeyChunk(timestamp); + if (!chunk) { + return null; + } + decoder.decode(chunk); + await decoder.flush(); + decoder.close(); + return result; + } + async getFrame(timestamp) { + let result = null; + const decoder = await this.createDecoder((frame) => { + if (frame.timestamp / 1e6 <= timestamp) { + result?.close(); + result = frame; + } else { + frame.close(); + } + }); + const keyChunk = await this.videoTrack._backing.getKeyChunk(timestamp); + if (!keyChunk) { + return null; + } + const targetChunk = await this.videoTrack._backing.getChunk(timestamp); + assert(targetChunk); + decoder.decode(keyChunk); + let currentChunk = keyChunk; + while (currentChunk !== targetChunk) { + const nextChunk = await this.videoTrack._backing.getNextChunk(currentChunk); + assert(nextChunk); + currentChunk = nextChunk; + decoder.decode(nextChunk); + if (decoder.decodeQueueSize >= 10) { + await new Promise((resolve) => decoder.addEventListener("dequeue", resolve, { once: true })); + } + } + await decoder.flush(); + decoder.close(); + return result; + } + async *frames(startTimestamp = 0) { + const frameQueue = []; + let firstFrameQueued = false; + let lastFrame = null; + let { promise: queueNonEmpty, resolve: onQueueNotEmpty } = promiseWithResolvers(); + let ended = false; + const decoder = await this.createDecoder((frame) => { + if (ended) { + frame.close(); + return; + } + const frameTimestamp = frame.timestamp / 1e6; + if (lastFrame) { + if (frameTimestamp > startTimestamp) { + frameQueue.push(lastFrame); + firstFrameQueued = true; + } else { + lastFrame.close(); + } + } + if (frameTimestamp >= startTimestamp) { + frameQueue.push(frame); + firstFrameQueued = true; + } + lastFrame = firstFrameQueued ? null : frame; + if (frameQueue.length > 0) { + onQueueNotEmpty(); + ({ promise: queueNonEmpty, resolve: onQueueNotEmpty } = promiseWithResolvers()); + } + }); + const keyChunk = await this.videoTrack._backing.getKeyChunk(startTimestamp); + if (!keyChunk) { + return; + } + let decoderIsFlushed = false; + void (async () => { + let currentChunk = keyChunk; + while (currentChunk && !ended) { + decoder.decode(currentChunk); + if (decoder.decodeQueueSize >= 10) { + await new Promise((resolve) => decoder.addEventListener("dequeue", resolve, { once: true })); + } + const nextChunk = await this.videoTrack._backing.getNextChunk(currentChunk); + currentChunk = nextChunk; + } + await decoder.flush(); + decoder.close(); + decoderIsFlushed = true; + onQueueNotEmpty(); + })(); + try { + while (true) { + if (frameQueue.length > 0) { + yield frameQueue.shift(); + } else if (!decoderIsFlushed) { + await queueNonEmpty; + } else { + break; + } + } + } finally { + ended = true; + } + } +}; export { + ALL_FORMATS, AUDIO_CODECS, + ArrayBufferSource, ArrayBufferTarget, AudioBufferSource, AudioDataSource, AudioSource, + BlobSource, CanvasSource, EncodedAudioChunkSource, + EncodedVideoChunkDrain, EncodedVideoChunkSource, + ISOBMFF, + Input, + MATROSKA, + MKV, + MOV, + MP4, MediaSource, MediaStreamAudioTrackSource, MediaStreamVideoTrackSource, @@ -3822,12 +5279,15 @@ export { Output, OutputFormat, SUBTITLE_CODECS, + Source, StreamTarget, SubtitleSource, Target, TextSubtitleSource, VIDEO_CODECS, + VideoFrameDrain, VideoFrameSource, VideoSource, + WEBM, WebMOutputFormat }; diff --git a/eslint.config.mjs b/eslint.config.mjs index 6115d15..7c23432 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -25,6 +25,7 @@ export default tseslint.config( code: 120, }], '@typescript-eslint/no-empty-object-type': 'off', + '@typescript-eslint/require-await': 'off', }, }, { diff --git a/src/codec.ts b/src/codec.ts index cc97e09..a39a786 100644 --- a/src/codec.ts +++ b/src/codec.ts @@ -2,12 +2,29 @@ import { COLOR_PRIMARIES_MAP, MATRIX_COEFFICIENTS_MAP, TRANSFER_CHARACTERISTICS_MAP, + bytesToHexString, isAllowSharedBufferSource, last, + reverseBitsU32, } from './misc'; -import { AudioCodec, VideoCodec } from './source'; import { SubtitleMetadata } from './subtitles'; +/** @public */ +export const VIDEO_CODECS = ['avc', 'hevc', 'vp8', 'vp9', 'av1'] as const; +/** @public */ +export const AUDIO_CODECS = ['aac', 'opus'] as const; // TODO add the rest +/** @public */ +export const SUBTITLE_CODECS = ['webvtt'] as const; // TODO add the rest + +/** @public */ +export type VideoCodec = typeof VIDEO_CODECS[number]; +/** @public */ +export type AudioCodec = typeof AUDIO_CODECS[number]; +/** @public */ +export type SubtitleCodec = typeof SUBTITLE_CODECS[number]; +/** @public */ +export type MediaCodec = VideoCodec | AudioCodec | SubtitleCodec; + // https://en.wikipedia.org/wiki/Advanced_Video_Coding const AVC_LEVEL_TABLE = [ { maxMacroblocks: 99, maxBitrate: 64000, level: 0x0A }, // Level 1 @@ -166,6 +183,65 @@ export const buildVideoCodecString = (codec: VideoCodec, width: number, height: throw new TypeError(`Unhandled codec '${codec}'.`); }; +export const extractVideoCodecString = (codec: VideoCodec, description: Uint8Array | null) => { + if (codec === 'avc') { + if (!description || description.byteLength < 4) { + throw new TypeError('AVC description must be at least 4 bytes long.'); + } + + // TODO: The "temp hack". Something is amiss with the second byte in the hex string, check the specification + + return `avc1.${bytesToHexString(description.subarray(1, 4))}`; + } else if (codec === 'hevc') { + if (!description) { + throw new TypeError('HEVC description must be provided.'); + } + + const view = new DataView(description.buffer, description.byteOffset, description.byteLength); + let codecString = 'hev1.'; + + // general_profile_space and general_profile_idc + const generalProfileSpace = (description[1]! >> 6) & 0x03; + const generalProfileIdc = description[1]! & 0x1F; + codecString += ['', 'A', 'B', 'C'][generalProfileSpace]! + generalProfileIdc; + + codecString += '.'; + + // general_profile_compatibility_flags (in reverse bit order) + const compatibilityFlags = reverseBitsU32(view.getUint32(2)); + codecString += compatibilityFlags.toString(16); + + codecString += '.'; + + // general_tier_flag and general_level_idc + const generalTierFlag = (description[1]! >> 5) & 0x01; + const generalLevelIdc = description[12]!; + codecString += generalTierFlag === 0 ? 'L' : 'H'; + codecString += generalLevelIdc; + + codecString += '.'; + + // constraint_flags (6 bytes) + const constraintFlags: number[] = []; + for (let i = 0; i < 6; i++) { + const byte = description[i + 13]!; + constraintFlags.push(byte); + } + + while (constraintFlags[constraintFlags.length - 1] === 0) { + constraintFlags.pop(); + } + + codecString += constraintFlags.map(x => x.toString(16)).join('.'); + + return codecString; + } + + // TODO + + throw new TypeError(`Unhandled codec '${codec}'.`); +}; + export const buildAudioCodecString = (codec: AudioCodec, numberOfChannels: number, sampleRate: number) => { if (codec === 'aac') { // If stereo or higher channels and lower sample rate, likely using HE-AAC v2 with PS @@ -190,6 +266,25 @@ export const buildAudioCodecString = (codec: AudioCodec, numberOfChannels: numbe throw new TypeError(`Unhandled codec '${codec}'.`); }; +export const extractAudioCodecString = (codec: AudioCodec, description: Uint8Array | null) => { + if (codec === 'aac') { + if (!description || description.byteLength < 2) { + throw new TypeError('AAC description must be at least 2 bytes long.'); + } + + // TODO: Is this correct? Give a source/reason + const mpeg4AudioObjectType = description[0]! >> 3; + return `mp4a.40.${mpeg4AudioObjectType}`; + } else if (codec === 'opus') { + return 'opus'; + } else if (codec === 'vorbis') { + return 'vorbis'; + } + + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + throw new TypeError(`Unhandled codec '${codec}'.`); +}; + export const getVideoEncoderConfigExtension = (codec: VideoCodec) => { if (codec === 'avc') { return { diff --git a/src/demuxer.ts b/src/demuxer.ts new file mode 100644 index 0000000..7d66b09 --- /dev/null +++ b/src/demuxer.ts @@ -0,0 +1,14 @@ +import { Input } from './input'; +import { InputTrack } from './input-track'; + +export abstract class Demuxer { + input: Input; + + constructor(input: Input) { + this.input = input; + } + + abstract getDuration(): Promise; + abstract getTracks(): Promise; + abstract getMimeType(): Promise; +} diff --git a/src/index.ts b/src/index.ts index 5f3642f..694b19f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,14 +9,8 @@ export { WebMOutputFormatOptions, } from './output-format'; export { - VIDEO_CODECS, - VideoCodec, VideoCodecConfig, - AUDIO_CODECS, - AudioCodec, AudioCodecConfig, - SUBTITLE_CODECS, - SubtitleCodec, MediaSource, VideoSource, EncodedVideoChunkSource, @@ -30,6 +24,18 @@ export { MediaStreamAudioTrackSource, SubtitleSource, TextSubtitleSource, -} from './source'; +} from './media-source'; +export { + VIDEO_CODECS, + VideoCodec, + AUDIO_CODECS, + AudioCodec, + SUBTITLE_CODECS, + SubtitleCodec, +} from './codec'; export { Target, ArrayBufferTarget, StreamTarget, StreamTargetChunk, StreamTargetOptions } from './target'; export { TransformationMatrix } from './misc'; +export { Source, ArrayBufferSource, BlobSource } from './source'; +export { ALL_FORMATS, ISOBMFF, MP4, MOV, MATROSKA, MKV, WEBM } from './input-format'; +export { Input, InputOptions } from './input'; +export { EncodedVideoChunkDrain, VideoFrameDrain } from './media-drain'; diff --git a/src/input-format.ts b/src/input-format.ts new file mode 100644 index 0000000..9bd030f --- /dev/null +++ b/src/input-format.ts @@ -0,0 +1,55 @@ +import { Demuxer } from './demuxer'; +import { Input } from './input'; +import { IsobmffDemuxer } from './isobmff/isobmff-demuxer'; +import { IsobmffReader } from './isobmff/isobmff-reader'; +import { MatroskaDemuxer } from './matroska/matroska-demuxer'; + +export abstract class InputFormat { + /** @internal */ + abstract _canReadInput(input: Input): Promise; + + /** @internal */ + abstract _createDemuxer(input: Input): Demuxer; +} + +class IsobmffInputFormat extends InputFormat { + /** @internal */ + override async _canReadInput(input: Input) { + const sourceSize = await input._reader.getSourceSize(); + if (sourceSize < 8) { + return false; + } + + await input._reader.loadRange(4, 8); + + const isobmffReader = new IsobmffReader(input._reader); + isobmffReader.pos = 4; + const fourCc = isobmffReader.readAscii(4); + + return fourCc === 'ftyp'; + } + + override _createDemuxer(input: Input) { + return new IsobmffDemuxer(input); + } +} + +class MatroskaInputFormat extends InputFormat { + /** @internal */ + override async _canReadInput() { + return false; // TODO + } + + override _createDemuxer(input: Input) { + return new MatroskaDemuxer(input); + } +} + +export const ISOBMFF = new IsobmffInputFormat(); +export const MP4 = ISOBMFF; +export const MOV = ISOBMFF; +export const MATROSKA = new MatroskaInputFormat(); +export const MKV = MATROSKA; +export const WEBM = MATROSKA; + +export const ALL_FORMATS: InputFormat[] = [ISOBMFF, MKV]; diff --git a/src/input-track.ts b/src/input-track.ts new file mode 100644 index 0000000..b28aa58 --- /dev/null +++ b/src/input-track.ts @@ -0,0 +1,121 @@ +import { AudioCodec, MediaCodec, VideoCodec } from './codec'; + +export interface InputTrackBacking { + getCodec(): Promise; + getDuration(): Promise; +} + +export abstract class InputTrack { + /** @internal */ + _backing: InputTrackBacking; + + /** @internal */ + constructor(backing: InputTrackBacking) { + this._backing = backing; + } + + abstract getCodec(): Promise; + abstract getCodecMimeType(): Promise; + + isVideoTrack(): this is InputVideoTrack { + return this instanceof InputVideoTrack; + } + + isAudioTrack(): this is InputAudioTrack { + return this instanceof InputAudioTrack; + } + + getDuration() { + return this._backing.getDuration(); + } +} + +export interface InputVideoTrackBacking extends InputTrackBacking { + getCodec(): Promise; + getWidth(): Promise; + getHeight(): Promise; + getRotation(): Promise; + getDecoderConfig(): Promise; + getFirstChunk(): Promise; + getChunk(timestamp: number): Promise; + getNextChunk(chunk: EncodedVideoChunk): Promise; + getKeyChunk(timestamp: number): Promise; + getNextKeyChunk(chunk: EncodedVideoChunk): Promise; +} + +export class InputVideoTrack extends InputTrack { + /** @internal */ + override _backing: InputVideoTrackBacking; + + /** @internal */ + constructor(backing: InputVideoTrackBacking) { + super(backing); + + this._backing = backing; + } + + getCodec() { + return this._backing.getCodec(); + } + + getWidth() { + return this._backing.getWidth(); + } + + getHeight() { + return this._backing.getHeight(); + } + + getRotation() { + return this._backing.getRotation(); + } + + getDecoderConfig() { + return this._backing.getDecoderConfig(); + } + + async getCodecMimeType() { + const decoderConfig = await this.getDecoderConfig(); + return decoderConfig.codec; + } +} + +export interface InputAudioTrackBacking extends InputTrackBacking { + getCodec(): Promise; + getNumberOfChannels(): Promise; + getSampleRate(): Promise; + getDecoderConfig(): Promise; +} + +export class InputAudioTrack extends InputTrack { + /** @internal */ + override _backing: InputAudioTrackBacking; + + /** @internal */ + constructor(backing: InputAudioTrackBacking) { + super(backing); + + this._backing = backing; + } + + getCodec() { + return this._backing.getCodec(); + } + + getNumberOfChannels() { + return this._backing.getNumberOfChannels(); + } + + getSampleRate() { + return this._backing.getSampleRate(); + } + + getDecoderConfig() { + return this._backing.getDecoderConfig(); + } + + async getCodecMimeType() { + const decoderConfig = await this.getDecoderConfig(); + return decoderConfig.codec; + } +} diff --git a/src/input.ts b/src/input.ts new file mode 100644 index 0000000..ea62844 --- /dev/null +++ b/src/input.ts @@ -0,0 +1,82 @@ +import { Demuxer } from './demuxer'; +import { InputFormat } from './input-format'; +import { assert } from './misc'; +import { Reader } from './reader'; +import { Source } from './source'; + +export type InputOptions = { + formats: InputFormat[]; + source: Source; +}; + +export class Input { + /** @internal */ + _formats: InputFormat[]; + /** @internal */ + _reader: Reader; + /** @internal */ + _demuxerPromise: Promise | null = null; + /** @internal */ + _format: InputFormat | null = null; + + constructor(options: InputOptions) { + this._formats = options.formats; + this._reader = new Reader(options.source); + } + + /** @internal */ + _getDemuxer() { + return this._demuxerPromise ??= (async () => { + for (const format of this._formats) { + const canRead = await format._canReadInput(this); + if (canRead) { + this._format = format; + return format._createDemuxer(this); + } + } + + throw new Error('Input has an unrecognizable format.'); + })(); + } + + async getFormat() { + await this._getDemuxer(); + assert(this._format!); + return this._format; + } + + async getDuration() { + const demuxer = await this._getDemuxer(); + return demuxer.getDuration(); + } + + async getTracks() { + const demuxer = await this._getDemuxer(); + return demuxer.getTracks(); + } + + async getVideoTracks() { + const tracks = await this.getTracks(); + return tracks.filter(x => x.isVideoTrack()); + } + + async getPrimaryVideoTrack() { + const tracks = await this.getTracks(); + return tracks.find(x => x.isVideoTrack()) ?? null; + } + + async getAudioTracks() { + const tracks = await this.getTracks(); + return tracks.filter(x => x.isAudioTrack()); + } + + async getPrimaryAudioTrack() { + const tracks = await this.getTracks(); + return tracks.find(x => x.isAudioTrack()) ?? null; + } + + async getMimeType() { + const demuxer = await this._getDemuxer(); + return demuxer.getMimeType(); + } +} diff --git a/src/isobmff/isobmff-boxes.ts b/src/isobmff/isobmff-boxes.ts index 99b1853..b2523ec 100644 --- a/src/isobmff/isobmff-boxes.ts +++ b/src/isobmff/isobmff-boxes.ts @@ -9,8 +9,10 @@ import { TRANSFER_CHARACTERISTICS_MAP, MATRIX_COEFFICIENTS_MAP, colorSpaceIsComplete, + IDENTITY_MATRIX, + rotationMatrix, } from '../misc'; -import { AudioCodec, SubtitleCodec, VideoCodec } from '../source'; +import { AudioCodec, SubtitleCodec, VideoCodec } from '../codec'; import { formatSubtitleTimestamp } from '../subtitles'; import { Writer } from '../writer'; import { @@ -208,21 +210,6 @@ const lastPresentedSample = (samples: Sample[]) => { return result; }; -const rotationMatrix = (rotationInDegrees: number): TransformationMatrix => { - const theta = rotationInDegrees * (Math.PI / 180); - const cosTheta = Math.cos(theta); - const sinTheta = Math.sin(theta); - - // Matrices are post-multiplied in ISOBMFF, meaning this is the transpose of your typical rotation matrix - return [ - cosTheta, sinTheta, 0, - -sinTheta, cosTheta, 0, - 0, 0, 1, - ]; -}; - -const IDENTITY_MATRIX = rotationMatrix(0); - const matrixToBytes = (matrix: TransformationMatrix) => { return [ fixed_16_16(matrix[0]), fixed_16_16(matrix[1]), fixed_2_30(matrix[2]), diff --git a/src/isobmff/isobmff-demuxer.ts b/src/isobmff/isobmff-demuxer.ts new file mode 100644 index 0000000..8baa7d2 --- /dev/null +++ b/src/isobmff/isobmff-demuxer.ts @@ -0,0 +1,1009 @@ +import { AudioCodec, extractAudioCodecString, extractVideoCodecString, MediaCodec, VideoCodec } from '../codec'; +import { Demuxer } from '../demuxer'; +import { Input } from '../input'; +import { + InputAudioTrack, + InputAudioTrackBacking, + InputTrack, + InputTrackBacking, + InputVideoTrack, + InputVideoTrackBacking, +} from '../input-track'; +import { + assert, + COLOR_PRIMARIES_MAP_INVERSE, + MATRIX_COEFFICIENTS_MAP_INVERSE, + TRANSFER_CHARACTERISTICS_MAP_INVERSE, + rotationMatrix, + binarySearchLessOrEqual, + binarySearchExact, +} from '../misc'; +import { IsobmffReader } from './isobmff-reader'; + +type InternalTrack = { + id: number; + demuxer: IsobmffDemuxer; + inputTrack: InputTrack | null; + timescale: number; + durationInTimescale: number; + rotation: number; + sampleTableOffset: number; + sampleTable: SampleTable | null; +} & ({ + info: null; +} | { + info: { + type: 'video'; + width: number; + height: number; + codec: VideoCodec | null; + codecDescription: Uint8Array | null; + colorSpace?: VideoColorSpaceInit | null; + }; +} | { + info: { + type: 'audio'; + numberOfChannels: number; + sampleRate: number; + codec: AudioCodec | null; + codecDescription: Uint8Array | null; + }; +}); + +type InternalVideoTrack = InternalTrack & { info: { type: 'video' } }; +type InternalAudioTrack = InternalTrack & { info: { type: 'audio' } }; + +type SampleTable = { + sampleTimingEntries: SampleTimingEntry[]; + sampleCompositionTimeOffsets: SampleCompositionTimeOffsetEntry[]; + sampleSizes: number[]; + keySampleIndices: number[] | null; // Samples that are keyframes + chunkOffsets: number[]; + sampleToChunk: SampleToChunkEntry[]; + presentationTimestamps: { + presentationTimestamp: number; + sampleIndex: number; + }[]; +}; +type SampleTimingEntry = { + startIndex: number; + startDecodeTimestamp: number; + count: number; + delta: number; +}; +type SampleCompositionTimeOffsetEntry = { + startIndex: number; + count: number; + offset: number; +}; +type SampleToChunkEntry = { + startSampleIndex: number; + startChunkIndex: number; + samplesPerChunk: number; + sampleDescriptionIndex: number; +}; + +const knownMatrixes = [rotationMatrix(0), rotationMatrix(90), rotationMatrix(180), rotationMatrix(270)]; + +export class IsobmffDemuxer extends Demuxer { + isobmffReader: IsobmffReader; + private currentTrack: InternalTrack | null = null; + private tracks: InternalTrack[] = []; + private metadataPromise: Promise | null = null; + private movieTimescale = -1; + private movieDurationInTimescale = -1; + + constructor(input: Input) { + super(input); + + this.isobmffReader = new IsobmffReader(input._reader); + } + + override async getDuration() { + await this.readMetadata(); + + if (this.movieDurationInTimescale === -1) { + throw new Error('Could not read movie duration.'); + } + + return this.movieDurationInTimescale / this.movieTimescale; + } + + override async getTracks() { + await this.readMetadata(); + return this.tracks.map(track => track.inputTrack!); + } + + override async getMimeType() { + await this.readMetadata(); + + let string = 'video/mp4'; + + if (this.tracks.length > 0) { + const codecMimeTypes = await Promise.all(this.tracks.map(x => x.inputTrack!.getCodecMimeType())); + const uniqueCodecMimeTypes = [...new Set(codecMimeTypes)]; + + string += `; codecs="${uniqueCodecMimeTypes.join(', ')}"`; + } + + return string; + } + + readMetadata() { + return this.metadataPromise ??= (async () => { + const sourceSize = await this.isobmffReader.reader.getSourceSize(); + + while (this.isobmffReader.pos < sourceSize) { + await this.isobmffReader.reader.loadRange(this.isobmffReader.pos, this.isobmffReader.pos + 16); + const startPos = this.isobmffReader.pos; + const boxInfo = this.isobmffReader.readBoxHeader(); + + if (boxInfo.name === 'moov') { + // Found moov, load it + await this.isobmffReader.reader.loadRange( + this.isobmffReader.pos, + this.isobmffReader.pos + boxInfo.contentSize, + ); + this.readContiguousBoxes(boxInfo.contentSize); + + return; + } + + this.isobmffReader.pos = startPos + boxInfo.totalSize; + } + })(); + } + + getSampleTableForTrack(internalTrack: InternalTrack) { + if (internalTrack.sampleTable) { + return internalTrack.sampleTable; + } + + const sampleTable: SampleTable = { + sampleTimingEntries: [], + sampleCompositionTimeOffsets: [], + sampleSizes: [], + keySampleIndices: null, + chunkOffsets: [], + sampleToChunk: [], + presentationTimestamps: [], + }; + internalTrack.sampleTable = sampleTable; + + this.isobmffReader.pos = internalTrack.sampleTableOffset; + this.currentTrack = internalTrack; + this.traverseBox(); + this.currentTrack = null; + + for (const entry of sampleTable.sampleTimingEntries) { + for (let i = 0; i < entry.count; i++) { + sampleTable.presentationTimestamps.push({ + presentationTimestamp: entry.startDecodeTimestamp + i * entry.delta, + sampleIndex: entry.startIndex + i, + }); + } + } + + for (const entry of sampleTable.sampleCompositionTimeOffsets) { + for (let i = 0; i < entry.count; i++) { + const sampleIndex = entry.startIndex + i; + const sample = sampleTable.presentationTimestamps[sampleIndex]; + if (!sample) { + continue; + } + + sample.presentationTimestamp += entry.offset; + } + } + + sampleTable.presentationTimestamps.sort((a, b) => a.presentationTimestamp - b.presentationTimestamp); + + return internalTrack.sampleTable; + } + + readContiguousBoxes(totalSize: number) { + const startIndex = this.isobmffReader.pos; + + while (this.isobmffReader.pos - startIndex < totalSize) { + this.traverseBox(); + } + } + + traverseBox() { + const startPos = this.isobmffReader.pos; + const boxInfo = this.isobmffReader.readBoxHeader(); + const boxEndPos = startPos + boxInfo.totalSize; + + switch (boxInfo.name) { + case 'mdia': + case 'minf': + case 'dinf': { + this.readContiguousBoxes(boxInfo.contentSize); + }; break; + + case 'mvhd': { + const version = this.isobmffReader.readU8(); + this.isobmffReader.pos += 3; // Flags + + if (version === 1) { + this.isobmffReader.pos += 8 + 8; + this.movieTimescale = this.isobmffReader.readU32(); + this.movieDurationInTimescale = this.isobmffReader.readU64(); + } else { + this.isobmffReader.pos += 4 + 4; + this.movieTimescale = this.isobmffReader.readU32(); + this.movieDurationInTimescale = this.isobmffReader.readU32(); + } + }; break; + + case 'trak': { + const track = { + id: -1, + demuxer: this, + inputTrack: null, + info: null, + timescale: -1, + durationInTimescale: -1, + rotation: 0, + sampleTableOffset: -1, + sampleTable: null, + } satisfies InternalTrack as InternalTrack; + this.currentTrack = track; + + this.readContiguousBoxes(boxInfo.contentSize); + + if (track.id !== -1 && track.timescale !== -1 && track.info !== null) { + if (track.info.type === 'video' && track.info.codec !== null) { + const videoTrack = track as InternalVideoTrack; + track.inputTrack = new InputVideoTrack(new IsobmffVideoTrackBacking(videoTrack)); + this.tracks.push(track); + } else if (track.info.type === 'audio' && track.info.codec !== null) { + const audioTrack = track as InternalAudioTrack; + track.inputTrack = new InputAudioTrack(new IsobmffAudioTrackBacking(audioTrack)); + this.tracks.push(track); + } + } + + this.currentTrack = null; + }; break; + + case 'tkhd': { + const track = this.currentTrack; + assert(track); + + const version = this.isobmffReader.readU8(); + const flags = this.isobmffReader.readU24(); + + const trackEnabled = (flags & 0x1) !== 0; + if (!trackEnabled) { + break; + } + + // Skip over creation & modification time to reach the track ID + if (version === 0) { + this.isobmffReader.pos += 8; + track.id = this.isobmffReader.readU32(); + this.isobmffReader.pos += 8; + } else if (version === 1) { + this.isobmffReader.pos += 16; + track.id = this.isobmffReader.readU32(); + this.isobmffReader.pos += 12; + } else { + throw new Error(`Incorrect track header version ${version}.`); + } + + this.isobmffReader.pos += 2 * 4 + 2 + 2 + 2 + 2; + const rotationMatrix: number[] = []; + rotationMatrix.push(this.isobmffReader.readFixed_16_16(), this.isobmffReader.readFixed_16_16()); + this.isobmffReader.pos += 4; + rotationMatrix.push(this.isobmffReader.readFixed_16_16(), this.isobmffReader.readFixed_16_16()); + + const matrixIndex = knownMatrixes.findIndex(x => x.every((y, i) => y === rotationMatrix[i])); + if (matrixIndex === -1) { + // console.warn(`Wacky rotation matrix ${rotationMatrix}; sticking with no rotation.`); + track.rotation = 0; + } else { + track.rotation = 90 * matrixIndex; + } + }; break; + + case 'mdhd': { + const track = this.currentTrack; + assert(track); + + const version = this.isobmffReader.readU8(); + this.isobmffReader.pos += 3; // Flags + + if (version === 0) { + this.isobmffReader.pos += 8; + track.timescale = this.isobmffReader.readU32(); + track.durationInTimescale = this.isobmffReader.readU32(); + } else if (version === 1) { + this.isobmffReader.pos += 16; + track.timescale = this.isobmffReader.readU32(); + track.durationInTimescale = this.isobmffReader.readU64(); + } + }; break; + + case 'hdlr': { + const track = this.currentTrack; + assert(track); + + this.isobmffReader.pos += 8; // Version + flags + pre-defined + const handlerType = this.isobmffReader.readAscii(4); + + if (handlerType === 'vide') { + track.info = { + type: 'video', + width: -1, + height: -1, + codec: null, + codecDescription: null, + colorSpace: null, + }; + } else if (handlerType === 'soun') { + track.info = { + type: 'audio', + numberOfChannels: -1, + sampleRate: -1, + codec: null, + codecDescription: null, + }; + } + }; break; + + case 'stbl': { + const track = this.currentTrack; + assert(track); + + track.sampleTableOffset = startPos; + + this.readContiguousBoxes(boxInfo.contentSize); + }; break; + + case 'stsd': { + const track = this.currentTrack; + assert(track); + + if (track.info === null || track.sampleTable) { + break; + } + + const stsdVersion = this.isobmffReader.readU8(); + this.isobmffReader.pos += 3; // Flags + + const entries = this.isobmffReader.readU32(); + + for (let i = 0; i < entries; i++) { + const sampleBoxInfo = this.isobmffReader.readBoxHeader(); + + if (track.info.type === 'video') { + if (sampleBoxInfo.name === 'avc1') { + track.info.codec = 'avc'; + } else if (sampleBoxInfo.name === 'hvc1' || sampleBoxInfo.name === 'hev1') { + track.info.codec = 'hevc'; + } else { + // TODO a more user-friendly message + console.warn(`Unsupported video sample entry type ${sampleBoxInfo.name}.`); + break; + } + + this.isobmffReader.pos += 6 * 1 + 2 + 2 + 2 + 3 * 4; + + track.info.width = this.isobmffReader.readU16(); + track.info.height = this.isobmffReader.readU16(); + + this.isobmffReader.pos += 4 + 4 + 4 + 2 + 32 + 2 + 2; + + this.readContiguousBoxes(startPos + sampleBoxInfo.totalSize - this.isobmffReader.pos); + } else { + if (sampleBoxInfo.name === 'mp4a') { + track.info.codec = 'aac'; + } else if (sampleBoxInfo.name.toLowerCase() === 'opus') { + track.info.codec = 'opus'; + } else { + console.warn(`Unsupported audio sample entry type ${sampleBoxInfo.name}.`); + break; + } + + this.isobmffReader.pos += 6 * 1 + 2; + + const version = this.isobmffReader.readU16(); + this.isobmffReader.pos += 3 * 2; + + let channelCount = this.isobmffReader.readU16(); + + this.isobmffReader.pos += 2 + 2 + 2; + + // Can't use fixed16_16 as that's signed + let sampleRate = this.isobmffReader.readU32() / 0x10000; + + if (stsdVersion === 0 && version > 0) { + // Additional QuickTime fields + if (version === 1) { + this.isobmffReader.pos += 4 * 4; + } else if (version === 2) { + this.isobmffReader.pos += 4; + sampleRate = this.isobmffReader.readF64(); + channelCount = this.isobmffReader.readU32(); + this.isobmffReader.pos += 4; // Always 0x7F000000 + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const sampleSize = this.isobmffReader.readU32(); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const flags = this.isobmffReader.readU32(); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const bytesPerFrame = this.isobmffReader.readU32(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const samplesPerFrame = this.isobmffReader.readU32(); + + /* + if (sampleBoxInfo.name === 'lpcm') { + const bytesPerSample = (sampleSize + 7) >> 3; + const isFloat = Boolean(flags & 1); + const isBigEndian = Boolean(flags & 2); + const sFlags = flags & 4 ? -1 : 0; // I guess it means "signed flags" or something? + + if (sampleSize > 0 && sampleSize <= 64) { + if (isFloat) { + if (sampleSize === 32 && !isBigEndian) { + track.pcmType = 'pcm-f32'; + } + } else { + if (sFlags & (1 << (bytesPerSample - 1))) { + if (bytesPerSample === 2 && !isBigEndian) { + track.pcmType = 'pcm-s16'; + } else if (bytesPerSample === 3 && !isBigEndian) { + track.pcmType = 'pcm-s24'; + } else if (bytesPerSample === 4 && !isBigEndian) { + track.pcmType = 'pcm-s32'; + } + } else { + if (bytesPerSample === 1) { + track.pcmType = 'pcm-u8'; + } + } + } + } + + if (track.pcmType === null) { + throw new Error(`Unsupported linear PCM type.`); + } + } + */ + } + } + + track.info.numberOfChannels = channelCount; + track.info.sampleRate = sampleRate; + + this.readContiguousBoxes(startPos + sampleBoxInfo.totalSize - this.isobmffReader.pos); + } + } + }; break; + + case 'avcC': { + const track = this.currentTrack; + assert(track && track.info); + + track.info.codecDescription = this.isobmffReader.readRange( + this.isobmffReader.pos, + this.isobmffReader.pos + boxInfo.contentSize, + ); + }; break; + + case 'hvcC': { + const track = this.currentTrack; + assert(track && track.info); + + track.info.codecDescription = this.isobmffReader.readRange( + this.isobmffReader.pos, + this.isobmffReader.pos + boxInfo.contentSize, + ); + }; break; + + case 'colr': { + const track = this.currentTrack; + assert(track && track.info?.type === 'video'); + + const colourType = this.isobmffReader.readAscii(4); + if (colourType !== 'nclx') { + break; + } + + const colourPrimaries = this.isobmffReader.readU16(); + const transferCharacteristics = this.isobmffReader.readU16(); + const matrixCoefficients = this.isobmffReader.readU16(); + const fullRangeFlag = Boolean(this.isobmffReader.readU8() & 0x80); + + track.info.colorSpace = { + primaries: COLOR_PRIMARIES_MAP_INVERSE[colourPrimaries], + transfer: TRANSFER_CHARACTERISTICS_MAP_INVERSE[transferCharacteristics], + matrix: MATRIX_COEFFICIENTS_MAP_INVERSE[matrixCoefficients], + fullRange: fullRangeFlag, + }; + }; break; + + case 'wave': { + if (boxInfo.totalSize > 8) { + this.readContiguousBoxes(boxInfo.contentSize); + } + }; break; + + case 'esds': { + const track = this.currentTrack; + assert(track && track.info); + + this.isobmffReader.pos += 4; // Version + flags + + const tag = this.isobmffReader.readU8(); + assert(tag === 0x03); + + this.isobmffReader.readIsomVariableInteger(); // Length + + this.isobmffReader.pos += 2; // ES ID + const mixed = this.isobmffReader.readU8(); + + const streamDependenceFlag = (mixed & 0x80) !== 0; + const urlFlag = (mixed & 0x40) !== 0; + const ocrStreamFlag = (mixed & 0x20) !== 0; + + if (streamDependenceFlag) { + this.isobmffReader.pos += 2; + } + if (urlFlag) { + const urlLength = this.isobmffReader.readU8(); + this.isobmffReader.pos += urlLength; + } + if (ocrStreamFlag) { + this.isobmffReader.pos += 2; + } + + const decoderConfigTag = this.isobmffReader.readU8(); + assert(decoderConfigTag === 0x04); + + this.isobmffReader.readIsomVariableInteger(); // Length + + const objectTypeIndication = this.isobmffReader.readU8(); + assert(objectTypeIndication === 0x40); // Assert it's MPEG-4 audio + + this.isobmffReader.pos += 1 + 3 + 4 + 4; + + const decoderSpecificInfoTag = this.isobmffReader.readU8(); + assert(decoderSpecificInfoTag === 0x05); + + const decoderSpecificInfoLength = this.isobmffReader.readIsomVariableInteger(); + + track.info.codecDescription = this.isobmffReader.readRange( + this.isobmffReader.pos, + this.isobmffReader.pos + decoderSpecificInfoLength, + ); + }; break; + + case 'stts': { + const track = this.currentTrack; + assert(track); + + if (!track.sampleTable) { + break; + } + + this.isobmffReader.pos += 4; // Version + flags + + const entryCount = this.isobmffReader.readU32(); + + let currentIndex = 0; + let currentTimestamp = 0; + + for (let i = 0; i < entryCount; i++) { + const sampleCount = this.isobmffReader.readU32(); + const sampleDelta = this.isobmffReader.readU32(); + + track.sampleTable.sampleTimingEntries.push({ + startIndex: currentIndex, + startDecodeTimestamp: currentTimestamp, + count: sampleCount, + delta: sampleDelta, + }); + + currentIndex += sampleCount; + currentTimestamp += sampleCount * sampleDelta; + } + }; break; + + case 'ctts': { + const track = this.currentTrack; + assert(track); + + if (!track.sampleTable) { + break; + } + + this.isobmffReader.pos += 1 + 3; // Version + flags + + const entryCount = this.isobmffReader.readU32(); + + let sampleIndex = 0; + for (let i = 0; i < entryCount; i++) { + const sampleCount = this.isobmffReader.readU32(); + // version === 0 ? this.isobmffReader.readU32() : this.isobmffReader.readI32(); + const sampleOffset = this.isobmffReader.readI32(); + + track.sampleTable.sampleCompositionTimeOffsets.push({ + startIndex: sampleIndex, + count: sampleCount, + offset: sampleOffset, + }); + + sampleIndex += sampleCount; + } + }; break; + + case 'stsz': { + const track = this.currentTrack; + assert(track); + + if (!track.sampleTable) { + break; + } + + this.isobmffReader.pos += 4; // Version + flags + + const sampleSize = this.isobmffReader.readU32(); + const sampleCount = this.isobmffReader.readU32(); + + if (sampleSize === 0) { + for (let i = 0; i < sampleCount; i++) { + const sampleSize = this.isobmffReader.readU32(); + track.sampleTable.sampleSizes.push(sampleSize); + } + } else { + track.sampleTable.sampleSizes.push(sampleSize); + } + }; break; + + case 'stz2': { + throw new Error('Unsupported.'); + }; + + case 'stss': { + const track = this.currentTrack; + assert(track); + + if (!track.sampleTable) { + break; + } + + this.isobmffReader.pos += 4; // Version + flags + + track.sampleTable.keySampleIndices = []; + + const entryCount = this.isobmffReader.readU32(); + for (let i = 0; i < entryCount; i++) { + const sampleIndex = this.isobmffReader.readU32() - 1; // Convert to 0-indexed + track.sampleTable.keySampleIndices.push(sampleIndex); + } + }; break; + + case 'stsc': { + const track = this.currentTrack; + assert(track); + + if (!track.sampleTable) { + break; + } + + this.isobmffReader.pos += 4; + + const entryCount = this.isobmffReader.readU32(); + + for (let i = 0; i < entryCount; i++) { + const startChunkIndex = this.isobmffReader.readU32() - 1; // Convert to 0-indexed + const samplesPerChunk = this.isobmffReader.readU32(); + const sampleDescriptionIndex = this.isobmffReader.readU32(); + + track.sampleTable.sampleToChunk.push({ + startSampleIndex: -1, + startChunkIndex, + samplesPerChunk, + sampleDescriptionIndex, + }); + } + + let startSampleIndex = 0; + for (let i = 0; i < track.sampleTable.sampleToChunk.length; i++) { + track.sampleTable.sampleToChunk[i]!.startSampleIndex = startSampleIndex; + + if (i < track.sampleTable.sampleToChunk.length - 1) { + const nextChunk = track.sampleTable.sampleToChunk[i + 1]!; + const chunkCount = nextChunk.startChunkIndex + - track.sampleTable.sampleToChunk[i]!.startChunkIndex; + startSampleIndex += chunkCount * track.sampleTable.sampleToChunk[i]!.samplesPerChunk; + } + } + }; break; + + case 'stco': { + const track = this.currentTrack; + assert(track); + + if (!track.sampleTable) { + break; + } + + this.isobmffReader.pos += 4; // Version + flags + + const entryCount = this.isobmffReader.readU32(); + + for (let i = 0; i < entryCount; i++) { + const chunkOffset = this.isobmffReader.readU32(); + track.sampleTable.chunkOffsets.push(chunkOffset); + } + }; break; + + case 'co64': { + const track = this.currentTrack; + assert(track); + + if (!track.sampleTable) { + break; + } + + this.isobmffReader.pos += 4; // Version + flags + + const entryCount = this.isobmffReader.readU32(); + + for (let i = 0; i < entryCount; i++) { + const chunkOffset = this.isobmffReader.readU64(); + track.sampleTable.chunkOffsets.push(chunkOffset); + } + }; break; + } + + this.isobmffReader.pos = boxEndPos; + } +} + +abstract class IsobmffTrackBacking implements InputTrackBacking { + chunkToSampleIndex = new WeakMap(); + sampleIndexToChunk = new Map>(); + + constructor(public internalTrack: InternalTrack) { + + } + + getCodec(): Promise { + throw new Error('Not implemented on base class.'); + } + + async getDuration() { + return this.internalTrack.durationInTimescale / this.internalTrack.timescale; + } +} + +class IsobmffVideoTrackBacking extends IsobmffTrackBacking implements InputVideoTrackBacking { + override internalTrack: InternalVideoTrack; + + constructor(internalTrack: InternalVideoTrack) { + super(internalTrack); + this.internalTrack = internalTrack; + } + + override async getCodec(): Promise { + return this.internalTrack.info.codec!; + } + + async getWidth() { + return this.internalTrack.info.width; + } + + async getHeight() { + return this.internalTrack.info.height; + } + + async getRotation() { + return this.internalTrack.rotation; + } + + async getDecoderConfig(): Promise { + return { + codec: extractVideoCodecString(this.internalTrack.info.codec!, this.internalTrack.info.codecDescription), + codedWidth: this.internalTrack.info.width, + codedHeight: this.internalTrack.info.height, + description: this.internalTrack.info.codecDescription ?? undefined, + colorSpace: this.internalTrack.info.colorSpace ?? undefined, + }; + } + + private async fetchChunkForSampleIndex(sampleIndex: number) { + if (sampleIndex === -1) { + return null; + } + + const existingChunk = this.sampleIndexToChunk.get(sampleIndex)?.deref(); + if (existingChunk) { + return existingChunk; + } + + const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack); + const sampleInfo = getSampleInfo(sampleTable, sampleIndex); + if (!sampleInfo) { + return null; + } + + const data = await this.internalTrack.demuxer.isobmffReader.reader.source._read( + sampleInfo.byteOffset, + sampleInfo.byteOffset + sampleInfo.byteSize, + ); + + const chunk = new EncodedVideoChunk({ + data, + timestamp: (1e6 * sampleInfo.presentationTimestamp) / this.internalTrack.timescale, + duration: (1e6 * sampleInfo.duration) / this.internalTrack.timescale, + type: sampleInfo.isKeyFrame ? 'key' : 'delta', + }); + + this.chunkToSampleIndex.set(chunk, sampleIndex); + this.sampleIndexToChunk.set(sampleIndex, new WeakRef(chunk)); + + return chunk; + } + + async getFirstChunk() { + return this.fetchChunkForSampleIndex(0); + } + + async getChunk(timestamp: number) { + const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack); + const sampleIndex = getSampleIndexForTimestamp(sampleTable, timestamp * this.internalTrack.timescale); + return this.fetchChunkForSampleIndex(sampleIndex); + } + + async getNextChunk(chunk: EncodedVideoChunk) { + const sampleIndex = this.chunkToSampleIndex.get(chunk); + if (sampleIndex === undefined) { + throw new Error('Chunk was not created from this track.'); + } + return this.fetchChunkForSampleIndex(sampleIndex + 1); + } + + async getKeyChunk(timestamp: number) { + const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack); + const sampleIndex = getSampleIndexForTimestamp(sampleTable, timestamp * this.internalTrack.timescale); + const keyFrameSampleIndex = sampleIndex === -1 + ? -1 + : getRelevantKeyframeIndexForSample(sampleTable, sampleIndex); + return this.fetchChunkForSampleIndex(keyFrameSampleIndex); + } + + async getNextKeyChunk(chunk: EncodedVideoChunk) { + const sampleIndex = this.chunkToSampleIndex.get(chunk); + if (sampleIndex === undefined) { + throw new Error('Chunk was not created from this track.'); + } + const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack); + const nextKeyFrameSampleIndex = getNextKeyframeIndexForSample(sampleTable, sampleIndex); + return this.fetchChunkForSampleIndex(nextKeyFrameSampleIndex); + } +} + +class IsobmffAudioTrackBacking extends IsobmffTrackBacking implements InputAudioTrackBacking { + override internalTrack: InternalAudioTrack; + + constructor(internalTrack: InternalAudioTrack) { + super(internalTrack); + this.internalTrack = internalTrack; + } + + override async getCodec(): Promise { + return this.internalTrack.info.codec!; + } + + async getNumberOfChannels() { + return this.internalTrack.info.numberOfChannels; + } + + async getSampleRate() { + return this.internalTrack.info.sampleRate; + } + + async getDecoderConfig(): Promise { + return { + codec: extractAudioCodecString(this.internalTrack.info.codec!, this.internalTrack.info.codecDescription), + numberOfChannels: this.internalTrack.info.numberOfChannels, + sampleRate: this.internalTrack.info.sampleRate, + description: this.internalTrack.info.codecDescription ?? undefined, + }; + } +} + +const getSampleIndexForTimestamp = (sampleTable: SampleTable, timescaleUnits: number) => { + const index = binarySearchLessOrEqual( + sampleTable.presentationTimestamps, + timescaleUnits, + x => x.presentationTimestamp, + ); + if (index === -1) { + return -1; + } + + return sampleTable.presentationTimestamps[index]!.sampleIndex; +}; + +type SampleInfo = { + presentationTimestamp: number; + duration: number; + byteOffset: number; + byteSize: number; + isKeyFrame: boolean; +}; + +const getSampleInfo = (sampleTable: SampleTable, sampleIndex: number): SampleInfo | null => { + const timingEntryIndex = binarySearchLessOrEqual(sampleTable.sampleTimingEntries, sampleIndex, x => x.startIndex); + const timingEntry = sampleTable.sampleTimingEntries[timingEntryIndex]; + if (!timingEntry || timingEntry.startIndex + timingEntry.count <= sampleIndex) { + return null; + } + + const decodeTimestamp = timingEntry.startDecodeTimestamp + + (sampleIndex - timingEntry.startIndex) * timingEntry.delta; + let presentationTimestamp = decodeTimestamp; + const offsetEntryIndex = binarySearchLessOrEqual( + sampleTable.sampleCompositionTimeOffsets, + sampleIndex, + x => x.startIndex, + ); + const offsetEntry = sampleTable.sampleCompositionTimeOffsets[offsetEntryIndex]; + if (offsetEntry) { + presentationTimestamp += offsetEntry.offset; + } + + const sampleSize = sampleTable.sampleSizes[Math.min(sampleIndex, sampleTable.sampleSizes.length - 1)]!; + const chunkEntryIndex = binarySearchLessOrEqual(sampleTable.sampleToChunk, sampleIndex, x => x.startSampleIndex); + const chunkEntry = sampleTable.sampleToChunk[chunkEntryIndex]; + assert(chunkEntry); + + const chunkIndex = chunkEntry.startChunkIndex + + Math.floor((sampleIndex - chunkEntry.startSampleIndex) / chunkEntry.samplesPerChunk); + const chunkOffset = sampleTable.chunkOffsets[chunkIndex]!; + + let sampleOffset = chunkOffset; + if (sampleTable.sampleSizes.length === 1) { + sampleOffset += sampleSize * (sampleIndex - chunkEntry.startSampleIndex); + } else { + const startSampleIndex = chunkEntry.startSampleIndex + + (chunkIndex - chunkEntry.startChunkIndex) * chunkEntry.samplesPerChunk; + for (let i = startSampleIndex; i < sampleIndex; i++) { + sampleOffset += sampleTable.sampleSizes[i]!; + } + } + + return { + presentationTimestamp, + duration: timingEntry.delta, + byteOffset: sampleOffset, + byteSize: sampleSize, + isKeyFrame: sampleTable.keySampleIndices + ? binarySearchExact(sampleTable.keySampleIndices, sampleIndex, x => x) !== -1 + : true, + }; +}; + +const getRelevantKeyframeIndexForSample = (sampleTable: SampleTable, sampleIndex: number) => { + if (!sampleTable.keySampleIndices) { + return sampleIndex; + } + + const index = binarySearchLessOrEqual(sampleTable.keySampleIndices, sampleIndex, x => x); + return sampleTable.keySampleIndices[index] ?? -1; +}; + +const getNextKeyframeIndexForSample = (sampleTable: SampleTable, sampleIndex: number) => { + if (!sampleTable.keySampleIndices) { + return sampleIndex + 1; + } + + const index = binarySearchLessOrEqual(sampleTable.keySampleIndices, sampleIndex, x => x); + return sampleTable.keySampleIndices[index + 1] ?? -1; +}; diff --git a/src/isobmff/isobmff-reader.ts b/src/isobmff/isobmff-reader.ts new file mode 100644 index 0000000..8424d52 --- /dev/null +++ b/src/isobmff/isobmff-reader.ts @@ -0,0 +1,118 @@ +import { Reader } from '../reader'; + +export class IsobmffReader { + pos = 0; + + constructor(public reader: Reader) {} + + readRange(start: number, end: number) { + const { view, offset } = this.reader.getViewAndOffset(start, end); + return new Uint8Array(view.buffer, offset, end - start); + } + + readU8() { + const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 1); + this.pos++; + + return view.getUint8(offset); + } + + readU16() { + const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 2); + this.pos += 2; + + return view.getUint16(offset, false); + } + + readU24() { + const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 3); + this.pos += 3; + + const high = view.getUint16(offset, false); + const low = view.getUint8(offset + 2); + return high * 0x100 + low; + } + + readS32() { + const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 4); + this.pos += 4; + + return view.getInt32(offset, false); + } + + readU32() { + const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 4); + this.pos += 4; + + return view.getUint32(offset, false); + } + + readI32() { + const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 4); + this.pos += 4; + + return view.getInt32(offset, false); + } + + readU64() { + const high = this.readU32(); + const low = this.readU32(); + return high * 0x100000000 + low; + } + + readF64() { + const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 8); + this.pos += 8; + + return view.getFloat64(offset, false); + } + + readFixed_16_16() { + return this.readS32() / 0x10000; + } + + readFixed_2_30() { + return this.readS32() / 0x40000000; + } + + readAscii(length: number) { + const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + length); + this.pos += length; + + let str = ''; + for (let i = 0; i < length; i++) { + str += String.fromCharCode(view.getUint8(offset + i)); + } + return str; + } + + readIsomVariableInteger() { + let result = 0; + + for (let i = 0; i < 4; i++) { + result <<= 7; + const nextByte = this.readU8(); + result |= nextByte & 0x7f; + + if ((nextByte & 0x80) === 0) { + break; + } + } + + return result; + } + + readBoxHeader() { + let totalSize = this.readU32(); + const name = this.readAscii(4); + let headerSize = 8; + + const hasLargeSize = totalSize === 1; + if (hasLargeSize) { + totalSize = this.readU64(); + headerSize = 16; + } + + return { name, totalSize, headerSize, contentSize: totalSize - headerSize }; + } +} diff --git a/src/matroska/matroska-demuxer.ts b/src/matroska/matroska-demuxer.ts new file mode 100644 index 0000000..7dab093 --- /dev/null +++ b/src/matroska/matroska-demuxer.ts @@ -0,0 +1,6 @@ +import { Demuxer } from '../demuxer'; +import { Input } from '../input'; + +export class MatroskaDemuxer extends Demuxer { + +} diff --git a/src/matroska/matroska-muxer.ts b/src/matroska/matroska-muxer.ts index 15453b8..46c9b89 100644 --- a/src/matroska/matroska-muxer.ts +++ b/src/matroska/matroska-muxer.ts @@ -1,4 +1,3 @@ -import { AudioCodec, SubtitleCodec, VideoCodec } from '../source'; import { COLOR_PRIMARIES_MAP, MATRIX_COEFFICIENTS_MAP, @@ -31,7 +30,14 @@ import { inlineTimestampRegex, parseSubtitleTimestamp, } from '../subtitles'; -import { validateAudioChunkMetadata, validateSubtitleMetadata, validateVideoChunkMetadata } from '../codec'; +import { + AudioCodec, + SubtitleCodec, + VideoCodec, + validateAudioChunkMetadata, + validateSubtitleMetadata, + validateVideoChunkMetadata, +} from '../codec'; import { Muxer } from '../muxer'; import { Writer } from '../writer'; diff --git a/src/media-drain.ts b/src/media-drain.ts new file mode 100644 index 0000000..fb9cc4d --- /dev/null +++ b/src/media-drain.ts @@ -0,0 +1,192 @@ +import { InputVideoTrack } from './input-track'; +import { assert, promiseWithResolvers } from './misc'; + +export class EncodedVideoChunkDrain { + constructor(public videoTrack: InputVideoTrack) {} + + getFirstChunk() { + return this.videoTrack._backing.getFirstChunk(); + } + + getChunk(timestamp: number) { + return this.videoTrack._backing.getChunk(timestamp); + } + + getNextChunk(chunk: EncodedVideoChunk) { + return this.videoTrack._backing.getNextChunk(chunk); + } + + getKeyChunk(timestamp: number) { + return this.videoTrack._backing.getKeyChunk(timestamp); + } + + getNextKeyChunk(chunk: EncodedVideoChunk) { + return this.videoTrack._backing.getNextKeyChunk(chunk); + } + + async* chunks(startTimestamp = 0) { + let chunk = await this.getChunk(startTimestamp); // Not necessarily correct if there is no chunk at timestamp 0 + while (chunk) { + yield chunk; + chunk = await this.getNextChunk(chunk); + } + } +} + +export class VideoFrameDrain { + decoderConfig: VideoDecoderConfig | null = null; + + constructor(public videoTrack: InputVideoTrack) {} + + async createDecoder(onFrame: (frame: VideoFrame) => unknown) { + if (!this.decoderConfig) { + this.decoderConfig = await this.videoTrack.getDecoderConfig(); + } + + const decoder = new VideoDecoder({ + output: onFrame, + error: error => console.error(error), + }); + decoder.configure(this.decoderConfig); + + return decoder; + } + + async getKeyFrame(timestamp: number) { + let result: VideoFrame | null = null; + + const decoder = await this.createDecoder(frame => result = frame); + const chunk = await this.videoTrack._backing.getKeyChunk(timestamp); + if (!chunk) { + return null; + } + + decoder.decode(chunk); + + await decoder.flush(); + decoder.close(); + + return result; + } + + async getFrame(timestamp: number) { + let result: VideoFrame | null = null; + + const decoder = await this.createDecoder((frame) => { + if (frame.timestamp / 1e6 <= timestamp) { + result?.close(); + result = frame; + } else { + frame.close(); + } + }); + const keyChunk = await this.videoTrack._backing.getKeyChunk(timestamp); + if (!keyChunk) { + return null; + } + + const targetChunk = await this.videoTrack._backing.getChunk(timestamp); + assert(targetChunk); + + decoder.decode(keyChunk); + + let currentChunk = keyChunk; + while (currentChunk !== targetChunk) { + const nextChunk = await this.videoTrack._backing.getNextChunk(currentChunk); + assert(nextChunk); + + currentChunk = nextChunk; + decoder.decode(nextChunk); + + if (decoder.decodeQueueSize >= 10) { + await new Promise(resolve => decoder.addEventListener('dequeue', resolve, { once: true })); + } + } + + await decoder.flush(); + decoder.close(); + + return result; + } + + async* frames(startTimestamp = 0) { + const frameQueue: VideoFrame[] = []; + let firstFrameQueued = false; + let lastFrame: VideoFrame | null = null; + let { promise: queueNonEmpty, resolve: onQueueNotEmpty } = promiseWithResolvers(); + let ended = false; + + const decoder = await this.createDecoder((frame) => { + if (ended) { + frame.close(); + return; + } + + const frameTimestamp = frame.timestamp / 1e6; + + if (lastFrame) { + if (frameTimestamp > startTimestamp) { + frameQueue.push(lastFrame); + firstFrameQueued = true; + } else { + lastFrame.close(); + } + } + + if (frameTimestamp >= startTimestamp) { + frameQueue.push(frame); + firstFrameQueued = true; + } + + lastFrame = firstFrameQueued ? null : frame; + + if (frameQueue.length > 0) { + onQueueNotEmpty(); + ({ promise: queueNonEmpty, resolve: onQueueNotEmpty } = promiseWithResolvers()); + } + }); + + const keyChunk = await this.videoTrack._backing.getKeyChunk(startTimestamp); + if (!keyChunk) { + return; + } + + let decoderIsFlushed = false; + + // The following is the "pump" process that keeps pumping chunks into the decoder + void (async () => { + let currentChunk: EncodedVideoChunk | null = keyChunk; + + while (currentChunk && !ended) { + decoder.decode(currentChunk); + + if (decoder.decodeQueueSize >= 10) { + await new Promise(resolve => decoder.addEventListener('dequeue', resolve, { once: true })); + } + + const nextChunk = await this.videoTrack._backing.getNextChunk(currentChunk); + currentChunk = nextChunk; + } + + await decoder.flush(); + decoder.close(); + + decoderIsFlushed = true; + onQueueNotEmpty(); // To unstuck the generator + })(); + + try { + while (true) { + if (frameQueue.length > 0) { + yield frameQueue.shift()!; + } else if (!decoderIsFlushed) { + await queueNonEmpty; + } else { + break; + } + } + } finally { + ended = true; + } + } +} diff --git a/src/media-source.ts b/src/media-source.ts new file mode 100644 index 0000000..59d77f5 --- /dev/null +++ b/src/media-source.ts @@ -0,0 +1,646 @@ +import { + AUDIO_CODECS, + AudioCodec, + buildAudioCodecString, + buildVideoCodecString, + getAudioEncoderConfigExtension, + getVideoEncoderConfigExtension, + SUBTITLE_CODECS, + SubtitleCodec, + VIDEO_CODECS, + VideoCodec, +} from './codec'; +import { OutputAudioTrack, OutputSubtitleTrack, OutputTrack, OutputVideoTrack } from './output'; +import { assert } from './misc'; +import { Muxer } from './muxer'; +import { SubtitleParser } from './subtitles'; + +/** @public */ +export abstract class MediaSource { + /** @internal */ + _connectedTrack: OutputTrack | null = null; + /** @internal */ + _closed = false; + /** @internal */ + _offsetTimestamps = false; + + /** @internal */ + _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.'); + } + } + + /** @internal */ + _start() {} + /** @internal */ + 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); + } +} + +/** @public */ +export abstract class VideoSource extends MediaSource { + /** @internal */ + override _connectedTrack: OutputVideoTrack | null = null; + /** @internal */ + _codec: VideoCodec; + + constructor(codec: VideoCodec) { + super(); + + if (!VIDEO_CODECS.includes(codec)) { + throw new TypeError(`Invalid video codec '${codec}'. Must be one of: ${VIDEO_CODECS.join(', ')}.`); + } + + this._codec = codec; + } +} + +/** @public */ +export class EncodedVideoChunkSource extends VideoSource { + constructor(codec: VideoCodec) { + super(codec); + } + + digest(chunk: EncodedVideoChunk, meta?: EncodedVideoChunkMetadata) { + if (!(chunk instanceof EncodedVideoChunk)) { + // TODO add polyfill for browsers that don't have this + throw new TypeError('chunk must be an EncodedVideoChunk.'); + } + + this._ensureValidDigest(); + return this._connectedTrack!.output._muxer.addEncodedVideoChunk(this._connectedTrack!, chunk, meta); + } +} + +const KEY_FRAME_INTERVAL = 5; + +/** @public */ +export type VideoCodecConfig = { + codec: VideoCodec; + bitrate: number; + latencyMode?: VideoEncoderConfig['latencyMode']; +}; + +const validateVideoCodecConfig = (config: VideoCodecConfig) => { + if (!config || typeof config !== 'object') { + throw new TypeError('Codec config must be an object.'); + } + if (!VIDEO_CODECS.includes(config.codec)) { + throw new TypeError(`Invalid video codec '${config.codec}'. Must be one of: ${VIDEO_CODECS.join(', ')}.`); + } + if (!Number.isInteger(config.bitrate) || config.bitrate <= 0) { + throw new TypeError('config.bitrate must be a positive integer.'); + } + if (config.latencyMode !== undefined && !['quality', 'realtime'].includes(config.latencyMode)) { + throw new TypeError('config.latencyMode, when provided, must be \'quality\' or \'realtime\'.'); + } +}; + +class VideoEncoderWrapper { + private encoder: VideoEncoder | null = null; + private muxer: Muxer | null = null; + private lastMultipleOfKeyFrameInterval = -1; + private lastWidth: number | null = null; + private lastHeight: number | null = null; + + constructor(private source: VideoSource, private codecConfig: VideoCodecConfig) { + validateVideoCodecConfig(codecConfig); + } + + async digest(videoFrame: VideoFrame) { + this.source._ensureValidDigest(); + + // Ensure video frame size remains constant + if (this.lastWidth !== null && this.lastHeight !== null) { + if (videoFrame.codedWidth !== this.lastWidth || videoFrame.codedHeight !== this.lastHeight) { + throw new Error( + `Video frame size must remain constant. Expected ${this.lastWidth}x${this.lastHeight},` + + ` got ${videoFrame.codedWidth}x${videoFrame.codedHeight}.`, + ); + } + } else { + this.lastWidth = videoFrame.codedWidth; + this.lastHeight = videoFrame.codedHeight; + } + + this.ensureEncoder(videoFrame); + assert(this.encoder); + + const multipleOfKeyFrameInterval = Math.floor((videoFrame.timestamp / 1e6) / KEY_FRAME_INTERVAL); + + // Ensure a key frame every KEY_FRAME_INTERVAL seconds. It is important that all video tracks follow the same + // "key frame" rhythm, because aligned key frames are required to start new fragments in ISOBMFF or clusters + // in Matroska. + this.encoder.encode(videoFrame, { + keyFrame: multipleOfKeyFrameInterval !== this.lastMultipleOfKeyFrameInterval, + }); + + this.lastMultipleOfKeyFrameInterval = multipleOfKeyFrameInterval; + + // We need to do this after sending the frame to the encoder as the frame otherwise might be closed + if (this.encoder.encodeQueueSize >= 4) { + await new Promise(resolve => this.encoder!.addEventListener('dequeue', resolve, { once: true })); + } + + await this.muxer!.mutex.currentPromise; // Allow the writer to apply backpressure + } + + private ensureEncoder(videoFrame: VideoFrame) { + if (this.encoder) { + return; + } + + this.encoder = new VideoEncoder({ + output: (chunk, meta) => void this.muxer!.addEncodedVideoChunk(this.source._connectedTrack!, chunk, meta), + error: error => console.error('Video encode error:', error), + }); + + this.encoder.configure({ + codec: buildVideoCodecString( + this.codecConfig.codec, + videoFrame.codedWidth, + videoFrame.codedHeight, + this.codecConfig.bitrate, + ), + width: videoFrame.codedWidth, + height: videoFrame.codedHeight, + bitrate: this.codecConfig.bitrate, + framerate: this.source._connectedTrack?.metadata.frameRate, + latencyMode: this.codecConfig.latencyMode, + ...getVideoEncoderConfigExtension(this.codecConfig.codec), + }); + + assert(this.source._connectedTrack); + this.muxer = this.source._connectedTrack.output._muxer; + } + + async flush() { + if (this.encoder) { + await this.encoder.flush(); + this.encoder.close(); + } + } +} + +/** @public */ +export class VideoFrameSource extends VideoSource { + /** @internal */ + private _encoder: VideoEncoderWrapper; + + constructor(codecConfig: VideoCodecConfig) { + super(codecConfig.codec); + this._encoder = new VideoEncoderWrapper(this, codecConfig); + } + + digest(videoFrame: VideoFrame) { + if (!(videoFrame instanceof VideoFrame)) { + throw new TypeError('videoFrame must be a VideoFrame.'); + } + + return this._encoder.digest(videoFrame); + } + + /** @internal */ + override _flush() { + return this._encoder.flush(); + } +} + +/** @public */ +export class CanvasSource extends VideoSource { + /** @internal */ + private _encoder: VideoEncoderWrapper; + /** @internal */ + private _canvas: HTMLCanvasElement | OffscreenCanvas; + + constructor(canvas: HTMLCanvasElement | OffscreenCanvas, codecConfig: VideoCodecConfig) { + if (!(canvas instanceof HTMLCanvasElement)) { + throw new TypeError('canvas must be an HTMLCanvasElement.'); + } + + super(codecConfig.codec); + this._encoder = new VideoEncoderWrapper(this, codecConfig); + this._canvas = canvas; + } + + digest(timestamp: number, duration = 0) { + if (!Number.isFinite(timestamp) || timestamp < 0) { + throw new TypeError('timestamp must be a non-negative number.'); + } + if (!Number.isFinite(duration) || duration < 0) { + throw new TypeError('duration must be a non-negative number.'); + } + + const frame = new VideoFrame(this._canvas, { + timestamp: Math.round(1e6 * timestamp), + duration: Math.round(1e6 * duration), + alpha: 'discard', + }); + + const promise = this._encoder.digest(frame); + frame.close(); + + return promise; + } + + /** @internal */ + override _flush() { + return this._encoder.flush(); + } +} + +/** @public */ +export class MediaStreamVideoTrackSource extends VideoSource { + /** @internal */ + private _encoder: VideoEncoderWrapper; + /** @internal */ + private _abortController: AbortController | null = null; + /** @internal */ + private _track: MediaStreamVideoTrack; + + /** @internal */ + override _offsetTimestamps = true; + + constructor(track: MediaStreamVideoTrack, codecConfig: VideoCodecConfig) { + if (!(track instanceof MediaStreamTrack) || track.kind !== 'video') { + throw new TypeError('track must be a video MediaStreamTrack.'); + } + + codecConfig = { + ...codecConfig, + latencyMode: 'realtime', + }; + + super(codecConfig.codec); + this._encoder = new VideoEncoderWrapper(this, codecConfig); + this._track = track; + } + + /** @internal */ + override _start() { + this._abortController = new AbortController(); + + const processor = new MediaStreamTrackProcessor({ track: this._track }); + const consumer = new WritableStream({ + write: (videoFrame) => { + // TODO: Drop frames if encoder overloaded + void this._encoder.digest(videoFrame); + videoFrame.close(); + }, + }); + + processor.readable.pipeTo(consumer, { + signal: this._abortController.signal, + }).catch((err) => { + // Handle abort error silently + if (err instanceof DOMException && err.name === 'AbortError') return; + // Handle other errors + console.error('Pipe error:', err); + }); + } + + /** @internal */ + override async _flush() { + if (this._abortController) { + this._abortController.abort(); + this._abortController = null; + } + + await this._encoder.flush(); + } +} + +/** @public */ +export abstract class AudioSource extends MediaSource { + /** @internal */ + override _connectedTrack: OutputAudioTrack | null = null; + /** @internal */ + _codec: AudioCodec; + + constructor(codec: AudioCodec) { + super(); + + if (!AUDIO_CODECS.includes(codec)) { + throw new TypeError(`Invalid audio codec '${codec}'. Must be one of: ${AUDIO_CODECS.join(', ')}.`); + } + + this._codec = codec; + } +} + +/** @public */ +export class EncodedAudioChunkSource extends AudioSource { + constructor(codec: AudioCodec) { + super(codec); + } + + digest(chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata) { + if (!(chunk instanceof EncodedAudioChunk)) { + // TODO add polyfill for browsers that don't have this + throw new TypeError('chunk must be an EncodedAudioChunk.'); + } + + this._ensureValidDigest(); + return this._connectedTrack!.output._muxer.addEncodedAudioChunk(this._connectedTrack!, chunk, meta); + } +} +/** @public */ +export type AudioCodecConfig = { + codec: AudioCodec; + bitrate: number; +}; + +const validateAudioCodecConfig = (config: AudioCodecConfig) => { + if (!config || typeof config !== 'object') { + throw new TypeError('Codec config must be an object.'); + } + if (!AUDIO_CODECS.includes(config.codec)) { + throw new TypeError(`Invalid audio codec '${config.codec}'. Must be one of: ${AUDIO_CODECS.join(', ')}.`); + } + if (!Number.isInteger(config.bitrate) || config.bitrate <= 0) { + throw new TypeError('config.bitrate must be a positive integer.'); + } +}; + +class AudioEncoderWrapper { + private encoder: AudioEncoder | null = null; + private muxer: Muxer | null = null; + private lastNumberOfChannels: number | null = null; + private lastSampleRate: number | null = null; + + constructor(private source: AudioSource, private codecConfig: AudioCodecConfig) { + validateAudioCodecConfig(codecConfig); + } + + async digest(audioData: AudioData) { + this.source._ensureValidDigest(); + + // Ensure audio parameters remain constant + if (this.lastNumberOfChannels !== null && this.lastSampleRate !== null) { + if ( + audioData.numberOfChannels !== this.lastNumberOfChannels + || audioData.sampleRate !== this.lastSampleRate + ) { + throw new Error( + `Audio parameters must remain constant. Expected ${this.lastNumberOfChannels} channels at` + + ` ${this.lastSampleRate} Hz, got ${audioData.numberOfChannels} channels at` + + ` ${audioData.sampleRate} Hz.`, + ); + } + } else { + this.lastNumberOfChannels = audioData.numberOfChannels; + this.lastSampleRate = audioData.sampleRate; + } + + this.ensureEncoder(audioData); + assert(this.encoder); + + this.encoder.encode(audioData); + + if (this.encoder.encodeQueueSize >= 4) { + await new Promise(resolve => this.encoder!.addEventListener('dequeue', resolve, { once: true })); + } + + await this.muxer!.mutex.currentPromise; // Allow the writer to apply backpressure + } + + private ensureEncoder(audioData: AudioData) { + if (this.encoder) { + return; + } + + this.encoder = new AudioEncoder({ + output: (chunk, meta) => void this.muxer!.addEncodedAudioChunk(this.source._connectedTrack!, chunk, meta), + error: error => console.error('Audio encode error:', error), + }); + + this.encoder.configure({ + codec: buildAudioCodecString(this.codecConfig.codec, audioData.numberOfChannels, audioData.sampleRate), + numberOfChannels: audioData.numberOfChannels, + sampleRate: audioData.sampleRate, + bitrate: this.codecConfig.bitrate, + ...getAudioEncoderConfigExtension(this.codecConfig.codec), + }); + + assert(this.source._connectedTrack); + this.muxer = this.source._connectedTrack.output._muxer; + } + + async flush() { + if (this.encoder) { + await this.encoder.flush(); + this.encoder.close(); + } + } +} + +/** @public */ +export class AudioDataSource extends AudioSource { + /** @internal */ + private _encoder: AudioEncoderWrapper; + + constructor(codecConfig: AudioCodecConfig) { + super(codecConfig.codec); + this._encoder = new AudioEncoderWrapper(this, codecConfig); + } + + digest(audioData: AudioData) { + if (!(audioData instanceof AudioData)) { + throw new TypeError('audioData must be an AudioData.'); + } + + return this._encoder.digest(audioData); + } + + /** @internal */ + override _flush() { + return this._encoder.flush(); + } +} + +/** @public */ +export class AudioBufferSource extends AudioSource { + /** @internal */ + private _encoder: AudioEncoderWrapper; + /** @internal */ + private _accumulatedFrameCount = 0; + + constructor(codecConfig: AudioCodecConfig) { + super(codecConfig.codec); + this._encoder = new AudioEncoderWrapper(this, codecConfig); + } + + digest(audioBuffer: AudioBuffer) { + if (!(audioBuffer instanceof AudioBuffer)) { + throw new TypeError('audioBuffer must be an AudioBuffer.'); + } + + const numberOfChannels = audioBuffer.numberOfChannels; + const sampleRate = audioBuffer.sampleRate; + const numberOfFrames = audioBuffer.length; + + // Create a planar F32 array containing all channels + const data = new Float32Array(numberOfChannels * numberOfFrames); + for (let channel = 0; channel < numberOfChannels; channel++) { + const channelData = audioBuffer.getChannelData(channel); + data.set(channelData, channel * numberOfFrames); + } + + const audioData = new AudioData({ + format: 'f32-planar', + sampleRate, + numberOfFrames, + numberOfChannels, + timestamp: Math.round(1e6 * this._accumulatedFrameCount / sampleRate), + data: data, + }); + + const promise = this._encoder.digest(audioData); + audioData.close(); + + this._accumulatedFrameCount += numberOfFrames; + + return promise; + } + + /** @internal */ + override _flush() { + return this._encoder.flush(); + } +} + +/** @public */ +export class MediaStreamAudioTrackSource extends AudioSource { + /** @internal */ + private _encoder: AudioEncoderWrapper; + /** @internal */ + private _abortController: AbortController | null = null; + /** @internal */ + private _track: MediaStreamAudioTrack; + + /** @internal */ + override _offsetTimestamps = true; + + constructor(track: MediaStreamAudioTrack, codecConfig: AudioCodecConfig) { + if (!(track instanceof MediaStreamTrack) || track.kind !== 'audio') { + throw new TypeError('track must be an audio MediaStreamTrack.'); + } + + super(codecConfig.codec); + this._encoder = new AudioEncoderWrapper(this, codecConfig); + this._track = track; + } + + /** @internal */ + override _start() { + this._abortController = new AbortController(); + + const processor = new MediaStreamTrackProcessor({ track: this._track }); + const consumer = new WritableStream({ + write: (audioData) => { + // TODO: Drop frames if encoder overloaded + void this._encoder.digest(audioData); + audioData.close(); + }, + }); + + processor.readable.pipeTo(consumer, { + signal: this._abortController.signal, + }).catch((err) => { + // Handle abort error silently + if (err instanceof DOMException && err.name === 'AbortError') return; + // Handle other errors + console.error('Pipe error:', err); + }); + } + + /** @internal */ + override async _flush() { + if (this._abortController) { + this._abortController.abort(); + this._abortController = null; + } + + await this._encoder.flush(); + } +} + +/** @public */ +export abstract class SubtitleSource extends MediaSource { + /** @internal */ + override _connectedTrack: OutputSubtitleTrack | null = null; + /** @internal */ + _codec: SubtitleCodec; + + constructor(codec: SubtitleCodec) { + super(); + + if (!SUBTITLE_CODECS.includes(codec)) { + throw new TypeError(`Invalid subtitle codec '${codec}'. Must be one of: ${SUBTITLE_CODECS.join(', ')}.`); + } + + this._codec = codec; + } +} + +/** @public */ +export class TextSubtitleSource extends SubtitleSource { + /** @internal */ + private _parser: SubtitleParser; + + constructor(codec: SubtitleCodec) { + super(codec); + + this._parser = new SubtitleParser({ + codec, + output: (cue, metadata) => + this._connectedTrack?.output._muxer.addSubtitleCue(this._connectedTrack, cue, metadata), + error: error => console.error('Subtitle parse error:', error), + }); + } + + digest(text: string) { + if (typeof text !== 'string') { + throw new TypeError('text must be a string.'); + } + + this._ensureValidDigest(); + this._parser.parse(text); + + return this._connectedTrack!.output._muxer.mutex.currentPromise; + } +} diff --git a/src/misc.ts b/src/misc.ts index 49abcb5..21a498a 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -53,6 +53,10 @@ export const toUint8Array = (source: AllowSharedBufferSource): Uint8Array => { export const textEncoder = new TextEncoder(); +const invertObject = (object: Record) => { + return Object.fromEntries(Object.entries(object).map(([key, value]) => [value, key])) as Record; +}; + // These maps are taken from https://www.matroska.org/technical/elements.html, // which references the tables in ITU-T H.273 - they should be valid for Matroska and ISOBMFF. export const COLOR_PRIMARIES_MAP: Record = { @@ -60,17 +64,22 @@ export const COLOR_PRIMARIES_MAP: Record = { bt470bg: 5, // ITU-R BT.470BG smpte170m: 6, // ITU-R BT.601 525 - SMPTE 170M }; +export const COLOR_PRIMARIES_MAP_INVERSE = invertObject(COLOR_PRIMARIES_MAP); + export const TRANSFER_CHARACTERISTICS_MAP: Record = { 'bt709': 1, // ITU-R BT.709 'smpte170m': 6, // SMPTE 170M 'iec61966-2-1': 13, // IEC 61966-2-1 }; +export const TRANSFER_CHARACTERISTICS_MAP_INVERSE = invertObject(TRANSFER_CHARACTERISTICS_MAP); + export const MATRIX_COEFFICIENTS_MAP: Record = { rgb: 0, // Identity bt709: 1, // ITU-R BT.709 bt470bg: 5, // ITU-R BT.470BG smpte170m: 6, // SMPTE 170M }; +export const MATRIX_COEFFICIENTS_MAP_INVERSE = invertObject(MATRIX_COEFFICIENTS_MAP); export type RequiredNonNull = { [K in keyof T]-?: NonNullable; @@ -114,3 +123,84 @@ export class AsyncMutex { return resolver!; } } + +export const rotationMatrix = (rotationInDegrees: number): TransformationMatrix => { + const theta = rotationInDegrees * (Math.PI / 180); + const cosTheta = Math.cos(theta); + const sinTheta = Math.sin(theta); + + // Matrices are post-multiplied in ISOBMFF, meaning this is the transpose of your typical rotation matrix + return [ + cosTheta, sinTheta, 0, + -sinTheta, cosTheta, 0, + 0, 0, 1, + ]; +}; + +export const IDENTITY_MATRIX = rotationMatrix(0); + +export const bytesToHexString = (bytes: Uint8Array) => { + return [...bytes].map(x => x.toString(16).padStart(2, '0')).join(''); +}; + +export const reverseBitsU32 = (x: number): number => { + x = ((x >> 1) & 0x55555555) | ((x & 0x55555555) << 1); + x = ((x >> 2) & 0x33333333) | ((x & 0x33333333) << 2); + x = ((x >> 4) & 0x0f0f0f0f) | ((x & 0x0f0f0f0f) << 4); + x = ((x >> 8) & 0x00ff00ff) | ((x & 0x00ff00ff) << 8); + x = ((x >> 16) & 0x0000ffff) | ((x & 0x0000ffff) << 16); + return x >>> 0; // Ensure it's treated as an unsigned 32-bit integer +}; + +export const binarySearchExact = (arr: T[], key: number, valueGetter: (x: T) => number): number => { + let low = 0; + let high = arr.length - 1; + let res = -1; + + while (low <= high) { + const mid = (low + high) >> 1; + const midVal = valueGetter(arr[mid]!); + + if (midVal === key) { + res = mid; + high = mid - 1; // continue searching left to find the lowest index + } else if (midVal < key) { + low = mid + 1; + } else { + high = mid - 1; + } + } + + return res; +}; + +export const binarySearchLessOrEqual = (arr: T[], key: number, valueGetter: (x: T) => number) => { + let ans = -1; + let low = 0; + let high = arr.length - 1; + + while (low <= high) { + const mid = (low + (high - low + 1) / 2) | 0; + const midVal = valueGetter(arr[mid]!); + + if (midVal <= key) { + ans = mid; + low = mid + 1; + } else { + high = mid - 1; + } + } + + return ans; +}; + +export const promiseWithResolvers = () => { + let resolve: (value: T) => void; + let reject: (reason: unknown) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + + return { promise, resolve: resolve!, reject: reject! }; +}; diff --git a/src/output.ts b/src/output.ts index be22d59..77a76d7 100644 --- a/src/output.ts +++ b/src/output.ts @@ -1,7 +1,7 @@ import { AsyncMutex, TransformationMatrix } from './misc'; import { Muxer } from './muxer'; import { OutputFormat } from './output-format'; -import { AudioSource, MediaSource, SubtitleSource, VideoSource } from './source'; +import { AudioSource, MediaSource, SubtitleSource, VideoSource } from './media-source'; import { Target } from './target'; import { Writer } from './writer'; diff --git a/src/reader.ts b/src/reader.ts new file mode 100644 index 0000000..5d3265c --- /dev/null +++ b/src/reader.ts @@ -0,0 +1,135 @@ +import { Source } from './source'; + +const PAGE_SIZE = 4096; + +type ReadSegment = { + start: number; + end: number; + bytes: Uint8Array; + view: DataView; +}; + +export class Reader { + loadedSegments: ReadSegment[] = []; + sourceSizePromise: Promise | null = null; + + constructor(public source: Source) {} + + getSourceSize() { + if (this.sourceSizePromise) { + return this.sourceSizePromise; + } else { + return this.sourceSizePromise = this.source._getSize(); + } + } + + async loadRange(start: number, end: number) { + // Read rounded to the nearest page + let alignedStart = Math.floor(start / PAGE_SIZE) * PAGE_SIZE; + let alignedEnd = Math.ceil(end / PAGE_SIZE) * PAGE_SIZE; + alignedEnd = Math.min(alignedEnd, await this.getSourceSize()); + + const thing = this.loadedSegments.find(x => x.start <= alignedStart); + if (thing) { + alignedStart = Math.max(alignedStart, thing.end); + } + + const thing2 = this.loadedSegments.find(x => x.end >= alignedEnd); + if (thing2) { + alignedEnd = Math.min(alignedEnd, thing2.start); + } + + if (alignedStart >= alignedEnd) { + // Nothing to load + return; + } + + const bytes = await this.source._read(alignedStart, alignedEnd); + this.insertIntoLoadedSegments(alignedStart, bytes); + } + + private insertIntoLoadedSegments(start: number, bytes: Uint8Array) { + /* + let index = -1; + let low = 0; + let high = this.loadedSegments.length - 1; + + while (low <= high) { + let mid = Math.floor(low + (high - low + 1) / 2); + let midVal = this.loadedSegments[mid].start; + + if (midVal >= start) { + index = mid; + high = mid - 1; + } else { + low = mid + 1; + } + } + */ + + const segment: ReadSegment = { + start, + end: start + bytes.byteLength, + bytes, + view: new DataView(bytes.buffer), + }; + let index = this.loadedSegments.findLastIndex(x => x.start <= start); + + this.loadedSegments.splice(index + 1, 0, segment); + if (index === -1 || this.loadedSegments[index]!.end < segment.start) { + index++; + } + + const mergeSectionStartIndex = index; + const mergeSectionStart = this.loadedSegments[mergeSectionStartIndex]!.start; + let mergeSectionEndIndex = index; + let mergeSectionEnd = this.loadedSegments[mergeSectionEndIndex]!.end; + + while ( + this.loadedSegments.length - 1 > mergeSectionEndIndex + && this.loadedSegments[mergeSectionEndIndex + 1]!.start <= mergeSectionEnd + ) { + mergeSectionEndIndex++; + mergeSectionEnd = Math.max(mergeSectionEnd, this.loadedSegments[mergeSectionEndIndex]!.end); + } + + if (mergeSectionStartIndex === mergeSectionEndIndex) { + return; + } + + for (let i = mergeSectionStartIndex; i <= mergeSectionEndIndex; i++) { + const segment = this.loadedSegments[i]!; + const coversEntireMergeSection = segment.start === mergeSectionStart && segment.end === mergeSectionEnd; + + if (coversEntireMergeSection) { + this.loadedSegments.splice(i + 1, mergeSectionEndIndex - i); + this.loadedSegments.splice(mergeSectionStartIndex, i - mergeSectionStartIndex); + + return; + } + } + + const unifiedBytes = new Uint8Array(mergeSectionEnd - mergeSectionStart); + for (let i = mergeSectionStartIndex; i <= mergeSectionEndIndex; i++) { + const segment = this.loadedSegments[i]!; + unifiedBytes.set(segment.bytes, segment.start - mergeSectionStart); + } + + this.loadedSegments.splice(mergeSectionStartIndex + 1, mergeSectionEndIndex - mergeSectionStartIndex); + this.loadedSegments[mergeSectionStartIndex]!.end = mergeSectionEnd; + this.loadedSegments[mergeSectionStartIndex]!.bytes = unifiedBytes; + this.loadedSegments[mergeSectionStartIndex]!.view = new DataView(unifiedBytes.buffer); + } + + getViewAndOffset(start: number, end: number) { + const segment = this.loadedSegments.find(x => x.start <= start && end <= x.end); + if (!segment) { + throw new Error(`No segment loaded for range [${start}, ${end}).`); + } + + return { + view: segment.view, + offset: segment.bytes.byteOffset + start - segment.start, + }; + } +} diff --git a/src/source.ts b/src/source.ts index 8a76705..9cc1010 100644 --- a/src/source.ts +++ b/src/source.ts @@ -1,654 +1,40 @@ -import { - buildAudioCodecString, - buildVideoCodecString, - getAudioEncoderConfigExtension, - getVideoEncoderConfigExtension, -} from './codec'; -import { OutputAudioTrack, OutputSubtitleTrack, OutputTrack, OutputVideoTrack } from './output'; -import { assert } from './misc'; -import { Muxer } from './muxer'; -import { SubtitleParser } from './subtitles'; - -/** @public */ -export const VIDEO_CODECS = ['avc', 'hevc', 'vp8', 'vp9', 'av1'] as const; -/** @public */ -export const AUDIO_CODECS = ['aac', 'opus'] as const; // TODO add the rest -/** @public */ -export const SUBTITLE_CODECS = ['webvtt'] as const; // TODO add the rest - -/** @public */ -export type VideoCodec = typeof VIDEO_CODECS[number]; -/** @public */ -export type AudioCodec = typeof AUDIO_CODECS[number]; -/** @public */ -export type SubtitleCodec = typeof SUBTITLE_CODECS[number]; - -/** @public */ -export abstract class MediaSource { +export abstract class Source { /** @internal */ - _connectedTrack: OutputTrack | null = null; + abstract _read(start: number, end: number): Promise; /** @internal */ - _closed = false; - /** @internal */ - _offsetTimestamps = false; - - /** @internal */ - _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.'); - } - } - - /** @internal */ - _start() {} - /** @internal */ - 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); - } + abstract _getSize(): Promise; } -/** @public */ -export abstract class VideoSource extends MediaSource { - /** @internal */ - override _connectedTrack: OutputVideoTrack | null = null; - /** @internal */ - _codec: VideoCodec; - - constructor(codec: VideoCodec) { +export class ArrayBufferSource extends Source { + constructor(private buffer: ArrayBuffer) { super(); + } - if (!VIDEO_CODECS.includes(codec)) { - throw new TypeError(`Invalid video codec '${codec}'. Must be one of: ${VIDEO_CODECS.join(', ')}.`); - } + /** @internal */ + override async _read(start: number, end: number) { + return new Uint8Array(this.buffer, start, end - start); + } - this._codec = codec; + /** @internal */ + override async _getSize() { + return this.buffer.byteLength; } } -/** @public */ -export class EncodedVideoChunkSource extends VideoSource { - constructor(codec: VideoCodec) { - super(codec); - } - - digest(chunk: EncodedVideoChunk, meta?: EncodedVideoChunkMetadata) { - if (!(chunk instanceof EncodedVideoChunk)) { - // TODO add polyfill for browsers that don't have this - throw new TypeError('chunk must be an EncodedVideoChunk.'); - } - - this._ensureValidDigest(); - return this._connectedTrack!.output._muxer.addEncodedVideoChunk(this._connectedTrack!, chunk, meta); - } -} - -const KEY_FRAME_INTERVAL = 5; - -/** @public */ -export type VideoCodecConfig = { - codec: VideoCodec; - bitrate: number; - latencyMode?: VideoEncoderConfig['latencyMode']; -}; - -const validateVideoCodecConfig = (config: VideoCodecConfig) => { - if (!config || typeof config !== 'object') { - throw new TypeError('Codec config must be an object.'); - } - if (!VIDEO_CODECS.includes(config.codec)) { - throw new TypeError(`Invalid video codec '${config.codec}'. Must be one of: ${VIDEO_CODECS.join(', ')}.`); - } - if (!Number.isInteger(config.bitrate) || config.bitrate <= 0) { - throw new TypeError('config.bitrate must be a positive integer.'); - } - if (config.latencyMode !== undefined && !['quality', 'realtime'].includes(config.latencyMode)) { - throw new TypeError('config.latencyMode, when provided, must be \'quality\' or \'realtime\'.'); - } -}; - -class VideoEncoderWrapper { - private encoder: VideoEncoder | null = null; - private muxer: Muxer | null = null; - private lastMultipleOfKeyFrameInterval = -1; - private lastWidth: number | null = null; - private lastHeight: number | null = null; - - constructor(private source: VideoSource, private codecConfig: VideoCodecConfig) { - validateVideoCodecConfig(codecConfig); - } - - async digest(videoFrame: VideoFrame) { - this.source._ensureValidDigest(); - - // Ensure video frame size remains constant - if (this.lastWidth !== null && this.lastHeight !== null) { - if (videoFrame.codedWidth !== this.lastWidth || videoFrame.codedHeight !== this.lastHeight) { - throw new Error( - `Video frame size must remain constant. Expected ${this.lastWidth}x${this.lastHeight},` - + ` got ${videoFrame.codedWidth}x${videoFrame.codedHeight}.`, - ); - } - } else { - this.lastWidth = videoFrame.codedWidth; - this.lastHeight = videoFrame.codedHeight; - } - - this.ensureEncoder(videoFrame); - assert(this.encoder); - - const multipleOfKeyFrameInterval = Math.floor((videoFrame.timestamp / 1e6) / KEY_FRAME_INTERVAL); - - // Ensure a key frame every KEY_FRAME_INTERVAL seconds. It is important that all video tracks follow the same - // "key frame" rhythm, because aligned key frames are required to start new fragments in ISOBMFF or clusters - // in Matroska. - this.encoder.encode(videoFrame, { - keyFrame: multipleOfKeyFrameInterval !== this.lastMultipleOfKeyFrameInterval, - }); - - this.lastMultipleOfKeyFrameInterval = multipleOfKeyFrameInterval; - - // We need to do this after sending the frame to the encoder as the frame otherwise might be closed - if (this.encoder.encodeQueueSize >= 4) { - await new Promise(resolve => this.encoder!.addEventListener('dequeue', resolve, { once: true })); - } - - await this.muxer!.mutex.currentPromise; // Allow the writer to apply backpressure - } - - private ensureEncoder(videoFrame: VideoFrame) { - if (this.encoder) { - return; - } - - this.encoder = new VideoEncoder({ - output: (chunk, meta) => void this.muxer!.addEncodedVideoChunk(this.source._connectedTrack!, chunk, meta), - error: error => console.error('Video encode error:', error), - }); - - this.encoder.configure({ - codec: buildVideoCodecString( - this.codecConfig.codec, - videoFrame.codedWidth, - videoFrame.codedHeight, - this.codecConfig.bitrate, - ), - width: videoFrame.codedWidth, - height: videoFrame.codedHeight, - bitrate: this.codecConfig.bitrate, - framerate: this.source._connectedTrack?.metadata.frameRate, - latencyMode: this.codecConfig.latencyMode, - ...getVideoEncoderConfigExtension(this.codecConfig.codec), - }); - - assert(this.source._connectedTrack); - this.muxer = this.source._connectedTrack.output._muxer; - } - - async flush() { - if (this.encoder) { - await this.encoder.flush(); - this.encoder.close(); - } - } -} - -/** @public */ -export class VideoFrameSource extends VideoSource { - /** @internal */ - private _encoder: VideoEncoderWrapper; - - constructor(codecConfig: VideoCodecConfig) { - super(codecConfig.codec); - this._encoder = new VideoEncoderWrapper(this, codecConfig); - } - - digest(videoFrame: VideoFrame) { - if (!(videoFrame instanceof VideoFrame)) { - throw new TypeError('videoFrame must be a VideoFrame.'); - } - - return this._encoder.digest(videoFrame); - } - - /** @internal */ - override _flush() { - return this._encoder.flush(); - } -} - -/** @public */ -export class CanvasSource extends VideoSource { - /** @internal */ - private _encoder: VideoEncoderWrapper; - /** @internal */ - private _canvas: HTMLCanvasElement | OffscreenCanvas; - - constructor(canvas: HTMLCanvasElement | OffscreenCanvas, codecConfig: VideoCodecConfig) { - if (!(canvas instanceof HTMLCanvasElement)) { - throw new TypeError('canvas must be an HTMLCanvasElement.'); - } - - super(codecConfig.codec); - this._encoder = new VideoEncoderWrapper(this, codecConfig); - this._canvas = canvas; - } - - digest(timestamp: number, duration = 0) { - if (!Number.isFinite(timestamp) || timestamp < 0) { - throw new TypeError('timestamp must be a non-negative number.'); - } - if (!Number.isFinite(duration) || duration < 0) { - throw new TypeError('duration must be a non-negative number.'); - } - - const frame = new VideoFrame(this._canvas, { - timestamp: Math.round(1e6 * timestamp), - duration: Math.round(1e6 * duration), - alpha: 'discard', - }); - - const promise = this._encoder.digest(frame); - frame.close(); - - return promise; - } - - /** @internal */ - override _flush() { - return this._encoder.flush(); - } -} - -/** @public */ -export class MediaStreamVideoTrackSource extends VideoSource { - /** @internal */ - private _encoder: VideoEncoderWrapper; - /** @internal */ - private _abortController: AbortController | null = null; - /** @internal */ - private _track: MediaStreamVideoTrack; - - /** @internal */ - override _offsetTimestamps = true; - - constructor(track: MediaStreamVideoTrack, codecConfig: VideoCodecConfig) { - if (!(track instanceof MediaStreamTrack) || track.kind !== 'video') { - throw new TypeError('track must be a video MediaStreamTrack.'); - } - - codecConfig = { - ...codecConfig, - latencyMode: 'realtime', - }; - - super(codecConfig.codec); - this._encoder = new VideoEncoderWrapper(this, codecConfig); - this._track = track; - } - - /** @internal */ - override _start() { - this._abortController = new AbortController(); - - const processor = new MediaStreamTrackProcessor({ track: this._track }); - const consumer = new WritableStream({ - write: (videoFrame) => { - // TODO: Drop frames if encoder overloaded - void this._encoder.digest(videoFrame); - videoFrame.close(); - }, - }); - - processor.readable.pipeTo(consumer, { - signal: this._abortController.signal, - }).catch((err) => { - // Handle abort error silently - if (err instanceof DOMException && err.name === 'AbortError') return; - // Handle other errors - console.error('Pipe error:', err); - }); - } - - /** @internal */ - override async _flush() { - if (this._abortController) { - this._abortController.abort(); - this._abortController = null; - } - - await this._encoder.flush(); - } -} - -/** @public */ -export abstract class AudioSource extends MediaSource { - /** @internal */ - override _connectedTrack: OutputAudioTrack | null = null; - /** @internal */ - _codec: AudioCodec; - - constructor(codec: AudioCodec) { +export class BlobSource extends Source { + constructor(private blob: Blob) { super(); + } - if (!AUDIO_CODECS.includes(codec)) { - throw new TypeError(`Invalid audio codec '${codec}'. Must be one of: ${AUDIO_CODECS.join(', ')}.`); - } + /** @internal */ + override async _read(start: number, end: number) { + const slice = this.blob.slice(start, end); + const buffer = await slice.arrayBuffer(); + return new Uint8Array(buffer); + } - this._codec = codec; - } -} - -/** @public */ -export class EncodedAudioChunkSource extends AudioSource { - constructor(codec: AudioCodec) { - super(codec); - } - - digest(chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata) { - if (!(chunk instanceof EncodedAudioChunk)) { - // TODO add polyfill for browsers that don't have this - throw new TypeError('chunk must be an EncodedAudioChunk.'); - } - - this._ensureValidDigest(); - return this._connectedTrack!.output._muxer.addEncodedAudioChunk(this._connectedTrack!, chunk, meta); - } -} -/** @public */ -export type AudioCodecConfig = { - codec: AudioCodec; - bitrate: number; -}; - -const validateAudioCodecConfig = (config: AudioCodecConfig) => { - if (!config || typeof config !== 'object') { - throw new TypeError('Codec config must be an object.'); - } - if (!AUDIO_CODECS.includes(config.codec)) { - throw new TypeError(`Invalid audio codec '${config.codec}'. Must be one of: ${AUDIO_CODECS.join(', ')}.`); - } - if (!Number.isInteger(config.bitrate) || config.bitrate <= 0) { - throw new TypeError('config.bitrate must be a positive integer.'); - } -}; - -class AudioEncoderWrapper { - private encoder: AudioEncoder | null = null; - private muxer: Muxer | null = null; - private lastNumberOfChannels: number | null = null; - private lastSampleRate: number | null = null; - - constructor(private source: AudioSource, private codecConfig: AudioCodecConfig) { - validateAudioCodecConfig(codecConfig); - } - - async digest(audioData: AudioData) { - this.source._ensureValidDigest(); - - // Ensure audio parameters remain constant - if (this.lastNumberOfChannels !== null && this.lastSampleRate !== null) { - if ( - audioData.numberOfChannels !== this.lastNumberOfChannels - || audioData.sampleRate !== this.lastSampleRate - ) { - throw new Error( - `Audio parameters must remain constant. Expected ${this.lastNumberOfChannels} channels at` - + ` ${this.lastSampleRate} Hz, got ${audioData.numberOfChannels} channels at` - + ` ${audioData.sampleRate} Hz.`, - ); - } - } else { - this.lastNumberOfChannels = audioData.numberOfChannels; - this.lastSampleRate = audioData.sampleRate; - } - - this.ensureEncoder(audioData); - assert(this.encoder); - - this.encoder.encode(audioData); - - if (this.encoder.encodeQueueSize >= 4) { - await new Promise(resolve => this.encoder!.addEventListener('dequeue', resolve, { once: true })); - } - - await this.muxer!.mutex.currentPromise; // Allow the writer to apply backpressure - } - - private ensureEncoder(audioData: AudioData) { - if (this.encoder) { - return; - } - - this.encoder = new AudioEncoder({ - output: (chunk, meta) => void this.muxer!.addEncodedAudioChunk(this.source._connectedTrack!, chunk, meta), - error: error => console.error('Audio encode error:', error), - }); - - this.encoder.configure({ - codec: buildAudioCodecString(this.codecConfig.codec, audioData.numberOfChannels, audioData.sampleRate), - numberOfChannels: audioData.numberOfChannels, - sampleRate: audioData.sampleRate, - bitrate: this.codecConfig.bitrate, - ...getAudioEncoderConfigExtension(this.codecConfig.codec), - }); - - assert(this.source._connectedTrack); - this.muxer = this.source._connectedTrack.output._muxer; - } - - async flush() { - if (this.encoder) { - await this.encoder.flush(); - this.encoder.close(); - } - } -} - -/** @public */ -export class AudioDataSource extends AudioSource { /** @internal */ - private _encoder: AudioEncoderWrapper; - - constructor(codecConfig: AudioCodecConfig) { - super(codecConfig.codec); - this._encoder = new AudioEncoderWrapper(this, codecConfig); - } - - digest(audioData: AudioData) { - if (!(audioData instanceof AudioData)) { - throw new TypeError('audioData must be an AudioData.'); - } - - return this._encoder.digest(audioData); - } - - /** @internal */ - override _flush() { - return this._encoder.flush(); - } -} - -/** @public */ -export class AudioBufferSource extends AudioSource { - /** @internal */ - private _encoder: AudioEncoderWrapper; - /** @internal */ - private _accumulatedFrameCount = 0; - - constructor(codecConfig: AudioCodecConfig) { - super(codecConfig.codec); - this._encoder = new AudioEncoderWrapper(this, codecConfig); - } - - digest(audioBuffer: AudioBuffer) { - if (!(audioBuffer instanceof AudioBuffer)) { - throw new TypeError('audioBuffer must be an AudioBuffer.'); - } - - const numberOfChannels = audioBuffer.numberOfChannels; - const sampleRate = audioBuffer.sampleRate; - const numberOfFrames = audioBuffer.length; - - // Create a planar F32 array containing all channels - const data = new Float32Array(numberOfChannels * numberOfFrames); - for (let channel = 0; channel < numberOfChannels; channel++) { - const channelData = audioBuffer.getChannelData(channel); - data.set(channelData, channel * numberOfFrames); - } - - const audioData = new AudioData({ - format: 'f32-planar', - sampleRate, - numberOfFrames, - numberOfChannels, - timestamp: Math.round(1e6 * this._accumulatedFrameCount / sampleRate), - data: data, - }); - - const promise = this._encoder.digest(audioData); - audioData.close(); - - this._accumulatedFrameCount += numberOfFrames; - - return promise; - } - - /** @internal */ - override _flush() { - return this._encoder.flush(); - } -} - -/** @public */ -export class MediaStreamAudioTrackSource extends AudioSource { - /** @internal */ - private _encoder: AudioEncoderWrapper; - /** @internal */ - private _abortController: AbortController | null = null; - /** @internal */ - private _track: MediaStreamAudioTrack; - - /** @internal */ - override _offsetTimestamps = true; - - constructor(track: MediaStreamAudioTrack, codecConfig: AudioCodecConfig) { - if (!(track instanceof MediaStreamTrack) || track.kind !== 'audio') { - throw new TypeError('track must be an audio MediaStreamTrack.'); - } - - super(codecConfig.codec); - this._encoder = new AudioEncoderWrapper(this, codecConfig); - this._track = track; - } - - /** @internal */ - override _start() { - this._abortController = new AbortController(); - - const processor = new MediaStreamTrackProcessor({ track: this._track }); - const consumer = new WritableStream({ - write: (audioData) => { - // TODO: Drop frames if encoder overloaded - void this._encoder.digest(audioData); - audioData.close(); - }, - }); - - processor.readable.pipeTo(consumer, { - signal: this._abortController.signal, - }).catch((err) => { - // Handle abort error silently - if (err instanceof DOMException && err.name === 'AbortError') return; - // Handle other errors - console.error('Pipe error:', err); - }); - } - - /** @internal */ - override async _flush() { - if (this._abortController) { - this._abortController.abort(); - this._abortController = null; - } - - await this._encoder.flush(); - } -} - -/** @public */ -export abstract class SubtitleSource extends MediaSource { - /** @internal */ - override _connectedTrack: OutputSubtitleTrack | null = null; - /** @internal */ - _codec: SubtitleCodec; - - constructor(codec: SubtitleCodec) { - super(); - - if (!SUBTITLE_CODECS.includes(codec)) { - throw new TypeError(`Invalid subtitle codec '${codec}'. Must be one of: ${SUBTITLE_CODECS.join(', ')}.`); - } - - this._codec = codec; - } -} - -/** @public */ -export class TextSubtitleSource extends SubtitleSource { - /** @internal */ - private _parser: SubtitleParser; - - constructor(codec: SubtitleCodec) { - super(codec); - - this._parser = new SubtitleParser({ - codec, - output: (cue, metadata) => - this._connectedTrack?.output._muxer.addSubtitleCue(this._connectedTrack, cue, metadata), - error: error => console.error('Subtitle parse error:', error), - }); - } - - digest(text: string) { - if (typeof text !== 'string') { - throw new TypeError('text must be a string.'); - } - - this._ensureValidDigest(); - this._parser.parse(text); - - return this._connectedTrack!.output._muxer.mutex.currentPromise; + override async _getSize() { + return this.blob.size; } } diff --git a/src/writer.ts b/src/writer.ts index def1f6c..b1056ee 100644 --- a/src/writer.ts +++ b/src/writer.ts @@ -69,7 +69,6 @@ export class ArrayBufferTargetWriter extends Writer { async flush() {} - // eslint-disable-next-line @typescript-eslint/require-await async finalize() { this.ensureSize(this.pos); this.target.buffer = this.buffer.slice(0, Math.max(this.maxPos, this.pos)); diff --git a/tsconfig.json b/tsconfig.json index 40dbf0f..4157a38 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "ES2021", + "target": "ES2023", "strict": true, "noImplicitAny": true, "noImplicitOverride": true,