diff --git a/build/webm-muxer.js b/build/webm-muxer.js index 405f804..3caf24b 100644 --- a/build/webm-muxer.js +++ b/build/webm-muxer.js @@ -61,7 +61,8 @@ var WebMMuxer = (() => { ArrayBufferTarget: () => ArrayBufferTarget, FileSystemWritableFileStreamTarget: () => FileSystemWritableFileStreamTarget, Muxer: () => Muxer, - StreamTarget: () => StreamTarget + StreamTarget: () => StreamTarget, + SubtitleEncoder: () => SubtitleEncoder }); // src/ebml.ts @@ -542,15 +543,17 @@ var WebMMuxer = (() => { // src/muxer.ts var VIDEO_TRACK_NUMBER = 1; var AUDIO_TRACK_NUMBER = 2; + var SUBTITLE_TRACK_NUMBER = 3; var VIDEO_TRACK_TYPE = 1; var AUDIO_TRACK_TYPE = 2; + var SUBTITLE_TRACK_TYPE = 17; var MAX_CHUNK_LENGTH_MS = __pow(2, 15); var CODEC_PRIVATE_MAX_SIZE = __pow(2, 12); var APP_NAME = "https://github.com/Vanilagy/webm-muxer"; var SEGMENT_SIZE_BYTES = 6; var CLUSTER_SIZE_BYTES = 5; var FIRST_TIMESTAMP_BEHAVIORS = ["strict", "offset", "permissive"]; - var _options, _writer, _segment, _segmentInfo, _seekHead, _tracksElement, _segmentDuration, _colourElement, _videoCodecPrivate, _audioCodecPrivate, _cues, _currentCluster, _currentClusterTimestamp, _duration, _videoChunkQueue, _audioChunkQueue, _firstVideoTimestamp, _firstAudioTimestamp, _lastVideoTimestamp, _lastAudioTimestamp, _colorSpace, _finalized, _validateOptions, validateOptions_fn, _createFileHeader, createFileHeader_fn, _writeEBMLHeader, writeEBMLHeader_fn, _createCodecPrivatePlaceholders, createCodecPrivatePlaceholders_fn, _createColourElement, createColourElement_fn, _createSeekHead, createSeekHead_fn, _createSegmentInfo, createSegmentInfo_fn, _createTracks, createTracks_fn, _createSegment, createSegment_fn, _createCues, createCues_fn, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn, _segmentDataOffset, segmentDataOffset_get, _writeVideoDecoderConfig, writeVideoDecoderConfig_fn, _fixVP9ColorSpace, fixVP9ColorSpace_fn, _createInternalChunk, createInternalChunk_fn, _validateTimestamp, validateTimestamp_fn, _writeSimpleBlock, writeSimpleBlock_fn, _createCodecPrivateElement, createCodecPrivateElement_fn, _writeCodecPrivate, writeCodecPrivate_fn, _createNewCluster, createNewCluster_fn, _finalizeCurrentCluster, finalizeCurrentCluster_fn, _ensureNotFinalized, ensureNotFinalized_fn; + var _options, _writer, _segment, _segmentInfo, _seekHead, _tracksElement, _segmentDuration, _colourElement, _videoCodecPrivate, _audioCodecPrivate, _subtitleCodecPrivate, _cues, _currentCluster, _currentClusterTimestamp, _duration, _videoChunkQueue, _audioChunkQueue, _subtitleChunkQueue, _firstVideoTimestamp, _firstAudioTimestamp, _lastVideoTimestamp, _lastAudioTimestamp, _lastSubtitleTimestamp, _colorSpace, _finalized, _validateOptions, validateOptions_fn, _createFileHeader, createFileHeader_fn, _writeEBMLHeader, writeEBMLHeader_fn, _createCodecPrivatePlaceholders, createCodecPrivatePlaceholders_fn, _createColourElement, createColourElement_fn, _createSeekHead, createSeekHead_fn, _createSegmentInfo, createSegmentInfo_fn, _createTracks, createTracks_fn, _createSegment, createSegment_fn, _createCues, createCues_fn, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn, _segmentDataOffset, segmentDataOffset_get, _writeVideoDecoderConfig, writeVideoDecoderConfig_fn, _fixVP9ColorSpace, fixVP9ColorSpace_fn, _writeSubtitleChunks, writeSubtitleChunks_fn, _createInternalChunk, createInternalChunk_fn, _validateTimestamp, validateTimestamp_fn, _writeBlock, writeBlock_fn, _createCodecPrivateElement, createCodecPrivateElement_fn, _writeCodecPrivate, writeCodecPrivate_fn, _createNewCluster, createNewCluster_fn, _finalizeCurrentCluster, finalizeCurrentCluster_fn, _ensureNotFinalized, ensureNotFinalized_fn; var Muxer = class { constructor(options) { __privateAdd(this, _validateOptions); @@ -567,9 +570,10 @@ var WebMMuxer = (() => { __privateAdd(this, _segmentDataOffset); __privateAdd(this, _writeVideoDecoderConfig); __privateAdd(this, _fixVP9ColorSpace); + __privateAdd(this, _writeSubtitleChunks); __privateAdd(this, _createInternalChunk); __privateAdd(this, _validateTimestamp); - __privateAdd(this, _writeSimpleBlock); + __privateAdd(this, _writeBlock); __privateAdd(this, _createCodecPrivateElement); __privateAdd(this, _writeCodecPrivate); __privateAdd(this, _createNewCluster); @@ -585,16 +589,19 @@ var WebMMuxer = (() => { __privateAdd(this, _colourElement, void 0); __privateAdd(this, _videoCodecPrivate, void 0); __privateAdd(this, _audioCodecPrivate, void 0); + __privateAdd(this, _subtitleCodecPrivate, void 0); __privateAdd(this, _cues, void 0); __privateAdd(this, _currentCluster, void 0); __privateAdd(this, _currentClusterTimestamp, void 0); __privateAdd(this, _duration, 0); __privateAdd(this, _videoChunkQueue, []); __privateAdd(this, _audioChunkQueue, []); + __privateAdd(this, _subtitleChunkQueue, []); __privateAdd(this, _firstVideoTimestamp, void 0); __privateAdd(this, _firstAudioTimestamp, void 0); __privateAdd(this, _lastVideoTimestamp, -1); __privateAdd(this, _lastAudioTimestamp, -1); + __privateAdd(this, _lastSubtitleTimestamp, -1); __privateAdd(this, _colorSpace, void 0); __privateAdd(this, _finalized, false); var _a; @@ -629,19 +636,20 @@ var WebMMuxer = (() => { __privateSet(this, _firstVideoTimestamp, timestamp); if (meta) __privateMethod(this, _writeVideoDecoderConfig, writeVideoDecoderConfig_fn).call(this, meta); - let internalChunk = __privateMethod(this, _createInternalChunk, createInternalChunk_fn).call(this, data, type, timestamp, VIDEO_TRACK_NUMBER); + let videoChunk = __privateMethod(this, _createInternalChunk, createInternalChunk_fn).call(this, data, type, timestamp, VIDEO_TRACK_NUMBER); if (__privateGet(this, _options).video.codec === "V_VP9") - __privateMethod(this, _fixVP9ColorSpace, fixVP9ColorSpace_fn).call(this, internalChunk); - __privateSet(this, _lastVideoTimestamp, internalChunk.timestamp); - while (__privateGet(this, _audioChunkQueue).length > 0 && __privateGet(this, _audioChunkQueue)[0].timestamp <= internalChunk.timestamp) { + __privateMethod(this, _fixVP9ColorSpace, fixVP9ColorSpace_fn).call(this, videoChunk); + __privateSet(this, _lastVideoTimestamp, videoChunk.timestamp); + while (__privateGet(this, _audioChunkQueue).length > 0 && __privateGet(this, _audioChunkQueue)[0].timestamp <= videoChunk.timestamp) { let audioChunk = __privateGet(this, _audioChunkQueue).shift(); - __privateMethod(this, _writeSimpleBlock, writeSimpleBlock_fn).call(this, audioChunk); + __privateMethod(this, _writeBlock, writeBlock_fn).call(this, audioChunk, false); } - if (!__privateGet(this, _options).audio || internalChunk.timestamp <= __privateGet(this, _lastAudioTimestamp)) { - __privateMethod(this, _writeSimpleBlock, writeSimpleBlock_fn).call(this, internalChunk); + if (!__privateGet(this, _options).audio || videoChunk.timestamp <= __privateGet(this, _lastAudioTimestamp)) { + __privateMethod(this, _writeBlock, writeBlock_fn).call(this, videoChunk, true); } else { - __privateGet(this, _videoChunkQueue).push(internalChunk); + __privateGet(this, _videoChunkQueue).push(videoChunk); } + __privateMethod(this, _writeSubtitleChunks, writeSubtitleChunks_fn).call(this); __privateMethod(this, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn).call(this); } addAudioChunk(chunk, meta, timestamp) { @@ -662,24 +670,45 @@ var WebMMuxer = (() => { __privateMethod(this, _writeCodecPrivate, writeCodecPrivate_fn).call(this, __privateGet(this, _audioCodecPrivate), meta.decoderConfig.description); } } - let internalChunk = __privateMethod(this, _createInternalChunk, createInternalChunk_fn).call(this, data, type, timestamp, AUDIO_TRACK_NUMBER); - __privateSet(this, _lastAudioTimestamp, internalChunk.timestamp); - while (__privateGet(this, _videoChunkQueue).length > 0 && __privateGet(this, _videoChunkQueue)[0].timestamp <= internalChunk.timestamp) { + let audioChunk = __privateMethod(this, _createInternalChunk, createInternalChunk_fn).call(this, data, type, timestamp, AUDIO_TRACK_NUMBER); + __privateSet(this, _lastAudioTimestamp, audioChunk.timestamp); + while (__privateGet(this, _videoChunkQueue).length > 0 && __privateGet(this, _videoChunkQueue)[0].timestamp <= audioChunk.timestamp) { let videoChunk = __privateGet(this, _videoChunkQueue).shift(); - __privateMethod(this, _writeSimpleBlock, writeSimpleBlock_fn).call(this, videoChunk); + __privateMethod(this, _writeBlock, writeBlock_fn).call(this, videoChunk, true); } - if (!__privateGet(this, _options).video || internalChunk.timestamp <= __privateGet(this, _lastVideoTimestamp)) { - __privateMethod(this, _writeSimpleBlock, writeSimpleBlock_fn).call(this, internalChunk); + if (!__privateGet(this, _options).video || audioChunk.timestamp <= __privateGet(this, _lastVideoTimestamp)) { + __privateMethod(this, _writeBlock, writeBlock_fn).call(this, audioChunk, !__privateGet(this, _options).video); } else { - __privateGet(this, _audioChunkQueue).push(internalChunk); + __privateGet(this, _audioChunkQueue).push(audioChunk); } + __privateMethod(this, _writeSubtitleChunks, writeSubtitleChunks_fn).call(this); + __privateMethod(this, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn).call(this); + } + addSubtitleChunk(chunk, meta) { + __privateMethod(this, _ensureNotFinalized, ensureNotFinalized_fn).call(this); + if (!__privateGet(this, _options).subtitles) + throw new Error("No subtitle track declared."); + if (meta == null ? void 0 : meta.decoderConfig) { + if (__privateGet(this, _options).streaming) { + __privateSet(this, _subtitleCodecPrivate, __privateMethod(this, _createCodecPrivateElement, createCodecPrivateElement_fn).call(this, meta.decoderConfig.description)); + } else { + __privateMethod(this, _writeCodecPrivate, writeCodecPrivate_fn).call(this, __privateGet(this, _subtitleCodecPrivate), meta.decoderConfig.description); + } + } + let subtitleChunk = __privateMethod(this, _createInternalChunk, createInternalChunk_fn).call(this, chunk.body, "key", chunk.timestamp, SUBTITLE_TRACK_NUMBER, chunk.duration, chunk.additions); + __privateSet(this, _lastSubtitleTimestamp, subtitleChunk.timestamp); + __privateGet(this, _subtitleChunkQueue).push(subtitleChunk); + __privateMethod(this, _writeSubtitleChunks, writeSubtitleChunks_fn).call(this); __privateMethod(this, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn).call(this); } finalize() { while (__privateGet(this, _videoChunkQueue).length > 0) - __privateMethod(this, _writeSimpleBlock, writeSimpleBlock_fn).call(this, __privateGet(this, _videoChunkQueue).shift()); + __privateMethod(this, _writeBlock, writeBlock_fn).call(this, __privateGet(this, _videoChunkQueue).shift(), true); while (__privateGet(this, _audioChunkQueue).length > 0) - __privateMethod(this, _writeSimpleBlock, writeSimpleBlock_fn).call(this, __privateGet(this, _audioChunkQueue).shift()); + __privateMethod(this, _writeBlock, writeBlock_fn).call(this, __privateGet(this, _audioChunkQueue).shift(), true); + while (__privateGet(this, _subtitleChunkQueue).length > 0 && __privateGet(this, _subtitleChunkQueue)[0].timestamp <= __privateGet(this, _duration)) { + __privateMethod(this, _writeBlock, writeBlock_fn).call(this, __privateGet(this, _subtitleChunkQueue).shift(), false); + } if (!__privateGet(this, _options).streaming) { __privateMethod(this, _finalizeCurrentCluster, finalizeCurrentCluster_fn).call(this); } @@ -714,16 +743,19 @@ var WebMMuxer = (() => { _colourElement = new WeakMap(); _videoCodecPrivate = new WeakMap(); _audioCodecPrivate = new WeakMap(); + _subtitleCodecPrivate = new WeakMap(); _cues = new WeakMap(); _currentCluster = new WeakMap(); _currentClusterTimestamp = new WeakMap(); _duration = new WeakMap(); _videoChunkQueue = new WeakMap(); _audioChunkQueue = new WeakMap(); + _subtitleChunkQueue = new WeakMap(); _firstVideoTimestamp = new WeakMap(); _firstAudioTimestamp = new WeakMap(); _lastVideoTimestamp = new WeakMap(); _lastAudioTimestamp = new WeakMap(); + _lastSubtitleTimestamp = new WeakMap(); _colorSpace = new WeakMap(); _finalized = new WeakMap(); _validateOptions = new WeakSet(); @@ -770,6 +802,7 @@ var WebMMuxer = (() => { createCodecPrivatePlaceholders_fn = function() { __privateSet(this, _videoCodecPrivate, { id: 236 /* Void */, size: 4, data: new Uint8Array(CODEC_PRIVATE_MAX_SIZE) }); __privateSet(this, _audioCodecPrivate, { id: 236 /* Void */, size: 4, data: new Uint8Array(CODEC_PRIVATE_MAX_SIZE) }); + __privateSet(this, _subtitleCodecPrivate, { id: 236 /* Void */, size: 4, data: new Uint8Array(CODEC_PRIVATE_MAX_SIZE) }); }; _createColourElement = new WeakSet(); createColourElement_fn = function() { @@ -848,6 +881,15 @@ var WebMMuxer = (() => { ] } ] }); } + if (__privateGet(this, _options).subtitles) { + tracksElement.data.push({ id: 174 /* TrackEntry */, data: [ + { id: 215 /* TrackNumber */, data: SUBTITLE_TRACK_NUMBER }, + { id: 29637 /* TrackUID */, data: SUBTITLE_TRACK_NUMBER }, + { id: 131 /* TrackType */, data: SUBTITLE_TRACK_TYPE }, + { id: 134 /* CodecID */, data: __privateGet(this, _options).subtitles.codec }, + __privateGet(this, _subtitleCodecPrivate) + ] }); + } }; _createSegment = new WeakSet(); createSegment_fn = function() { @@ -955,67 +997,102 @@ var WebMMuxer = (() => { }[__privateGet(this, _colorSpace).matrix]; writeBits(chunk.data, i + 0, i + 3, colorSpaceID); }; + _writeSubtitleChunks = new WeakSet(); + writeSubtitleChunks_fn = function() { + let lastWrittenMediaTimestamp = Math.min( + __privateGet(this, _options).video ? __privateGet(this, _lastVideoTimestamp) : Infinity, + __privateGet(this, _options).audio ? __privateGet(this, _lastAudioTimestamp) : Infinity + ); + let queue = __privateGet(this, _subtitleChunkQueue); + while (queue.length > 0 && queue[0].timestamp <= lastWrittenMediaTimestamp) { + __privateMethod(this, _writeBlock, writeBlock_fn).call(this, queue.shift(), !__privateGet(this, _options).video && !__privateGet(this, _options).audio); + } + }; _createInternalChunk = new WeakSet(); - createInternalChunk_fn = function(data, type, timestamp, trackNumber) { + createInternalChunk_fn = function(data, type, timestamp, trackNumber, duration, additions) { let adjustedTimestamp = __privateMethod(this, _validateTimestamp, validateTimestamp_fn).call(this, timestamp, trackNumber); let internalChunk = { data, + additions, type, timestamp: adjustedTimestamp, + duration, trackNumber }; return internalChunk; }; _validateTimestamp = new WeakSet(); validateTimestamp_fn = function(timestamp, trackNumber) { - let firstTimestamp = trackNumber === VIDEO_TRACK_NUMBER ? __privateGet(this, _firstVideoTimestamp) : __privateGet(this, _firstAudioTimestamp); - let lastTimestamp = trackNumber === VIDEO_TRACK_NUMBER ? __privateGet(this, _lastVideoTimestamp) : __privateGet(this, _lastAudioTimestamp); - if (__privateGet(this, _options).firstTimestampBehavior === "strict" && lastTimestamp === -1 && timestamp !== 0) { - throw new Error( - `The first chunk for your media track must have a timestamp of 0 (received ${timestamp}). Non-zero first timestamps are often caused by directly piping frames or audio data from a MediaStreamTrack into the encoder. Their timestamps are typically relative to the age of the document, which is probably what you want. + let lastTimestamp = trackNumber === VIDEO_TRACK_NUMBER ? __privateGet(this, _lastVideoTimestamp) : trackNumber === AUDIO_TRACK_NUMBER ? __privateGet(this, _lastAudioTimestamp) : __privateGet(this, _lastSubtitleTimestamp); + if (trackNumber !== SUBTITLE_TRACK_NUMBER) { + let firstTimestamp = trackNumber === VIDEO_TRACK_NUMBER ? __privateGet(this, _firstVideoTimestamp) : __privateGet(this, _firstAudioTimestamp); + if (__privateGet(this, _options).firstTimestampBehavior === "strict" && lastTimestamp === -1 && timestamp !== 0) { + throw new Error( + `The first chunk for your media track must have a timestamp of 0 (received ${timestamp}). Non-zero first timestamps are often caused by directly piping frames or audio data from a MediaStreamTrack into the encoder. Their timestamps are typically relative to the age of the document, which is probably what you want. If you want to offset all timestamps of a track such that the first one is zero, set firstTimestampBehavior: 'offset' in the options. If you want to allow non-zero first timestamps, set firstTimestampBehavior: 'permissive'. ` - ); - } else if (__privateGet(this, _options).firstTimestampBehavior === "offset") { - timestamp -= firstTimestamp; + ); + } else if (__privateGet(this, _options).firstTimestampBehavior === "offset") { + timestamp -= firstTimestamp; + } } if (timestamp < lastTimestamp) { throw new Error( `Timestamps must be monotonically increasing (went from ${lastTimestamp} to ${timestamp}).` ); } + if (timestamp < 0) { + throw new Error(`Timestamps must be non-negative (received ${timestamp}).`); + } return timestamp; }; - _writeSimpleBlock = new WeakSet(); - writeSimpleBlock_fn = function(chunk) { + _writeBlock = new WeakSet(); + writeBlock_fn = function(chunk, canCreateNewCluster) { if (__privateGet(this, _options).streaming && !__privateGet(this, _tracksElement)) { __privateMethod(this, _createTracks, createTracks_fn).call(this); __privateMethod(this, _createSegment, createSegment_fn).call(this); } - let msTime = Math.floor(chunk.timestamp / 1e3); - let clusterIsTooLong = chunk.type !== "key" && msTime - __privateGet(this, _currentClusterTimestamp) >= MAX_CHUNK_LENGTH_MS; + let msTimestamp = Math.floor(chunk.timestamp / 1e3); + let shouldCreateNewClusterFromKeyFrame = canCreateNewCluster && chunk.type === "key" && msTimestamp - __privateGet(this, _currentClusterTimestamp) >= 1e3; + if (!__privateGet(this, _currentCluster) || shouldCreateNewClusterFromKeyFrame) { + __privateMethod(this, _createNewCluster, createNewCluster_fn).call(this, msTimestamp); + } + let relativeTimestamp = msTimestamp - __privateGet(this, _currentClusterTimestamp); + if (relativeTimestamp < 0) { + return; + } + let clusterIsTooLong = relativeTimestamp >= MAX_CHUNK_LENGTH_MS; if (clusterIsTooLong) { throw new Error( - `Current Matroska cluster exceeded its maximum allowed length of ${MAX_CHUNK_LENGTH_MS} milliseconds. In order to produce a correct WebM file, you must pass in a video key frame at least every ${MAX_CHUNK_LENGTH_MS} milliseconds.` + `Current Matroska cluster exceeded its maximum allowed length of ${MAX_CHUNK_LENGTH_MS} milliseconds. In order to produce a correct WebM file, you must pass in a key frame at least every ${MAX_CHUNK_LENGTH_MS} milliseconds.` ); } - let shouldCreateNewClusterFromKeyFrame = (chunk.trackNumber === VIDEO_TRACK_NUMBER || !__privateGet(this, _options).video) && chunk.type === "key" && msTime - __privateGet(this, _currentClusterTimestamp) >= 1e3; - if (!__privateGet(this, _currentCluster) || shouldCreateNewClusterFromKeyFrame) { - __privateMethod(this, _createNewCluster, createNewCluster_fn).call(this, msTime); - } let prelude = new Uint8Array(4); let view = new DataView(prelude.buffer); view.setUint8(0, 128 | chunk.trackNumber); - view.setUint16(1, msTime - __privateGet(this, _currentClusterTimestamp), false); - view.setUint8(3, Number(chunk.type === "key") << 7); - let simpleBlock = { id: 163 /* SimpleBlock */, data: [ - prelude, - chunk.data - ] }; - __privateGet(this, _writer).writeEBML(simpleBlock); - __privateSet(this, _duration, Math.max(__privateGet(this, _duration), msTime)); + view.setInt16(1, relativeTimestamp, false); + if (chunk.duration === void 0 && !chunk.additions) { + view.setUint8(3, Number(chunk.type === "key") << 7); + let simpleBlock = { id: 163 /* SimpleBlock */, data: [ + prelude, + chunk.data + ] }; + __privateGet(this, _writer).writeEBML(simpleBlock); + } else { + let msDuration = Math.floor(chunk.duration / 1e3); + let blockGroup = { id: 160 /* BlockGroup */, data: [ + { id: 161 /* Block */, data: [ + prelude, + chunk.data + ] }, + chunk.duration !== void 0 ? { id: 155 /* BlockDuration */, data: msDuration } : null, + chunk.additions ? { id: 30113 /* BlockAdditions */, data: chunk.additions } : null + ] }; + __privateGet(this, _writer).writeEBML(blockGroup); + } + __privateSet(this, _duration, Math.max(__privateGet(this, _duration), msTimestamp)); }; _createCodecPrivateElement = new WeakSet(); createCodecPrivateElement_fn = function(data) { @@ -1025,9 +1102,20 @@ If you want to allow non-zero first timestamps, set firstTimestampBehavior: 'per writeCodecPrivate_fn = function(element, data) { let endPos = __privateGet(this, _writer).pos; __privateGet(this, _writer).seek(__privateGet(this, _writer).offsets.get(element)); + let codecPrivateElementSize = 2 + 4 + data.byteLength; + let voidDataSize = CODEC_PRIVATE_MAX_SIZE - codecPrivateElementSize; + if (voidDataSize < 0) { + let newByteLength = data.byteLength + voidDataSize; + if (data instanceof ArrayBuffer) { + data = data.slice(0, newByteLength); + } else { + data = data.buffer.slice(0, newByteLength); + } + voidDataSize = 0; + } element = [ __privateMethod(this, _createCodecPrivateElement, createCodecPrivateElement_fn).call(this, data), - { id: 236 /* Void */, size: 4, data: new Uint8Array(CODEC_PRIVATE_MAX_SIZE - 2 - 4 - data.byteLength) } + { id: 236 /* Void */, size: 4, data: new Uint8Array(voidDataSize) } ]; __privateGet(this, _writer).writeEBML(element); __privateGet(this, _writer).seek(endPos); @@ -1073,6 +1161,120 @@ If you want to allow non-zero first timestamps, set firstTimestampBehavior: 'per throw new Error("Cannot add new video or audio chunks after the file has been finalized."); } }; + + // src/subtitles.ts + var cueBlockHeaderRegex = /(?:(.+?)\n)?((?:\d{2}:)?\d{2}:\d{2}.\d{3})\s+-->\s+((?:\d{2}:)?\d{2}:\d{2}.\d{3})/g; + var preambleStartRegex = /^WEBVTT.*?\n{2}/; + var timestampRegex = /(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})/; + var inlineTimestampRegex = /<(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})>/g; + var textEncoder = new TextEncoder(); + var _options2, _config, _preambleSeen, _preambleBytes, _preambleEmitted, _parseTimestamp, parseTimestamp_fn, _formatTimestamp, formatTimestamp_fn; + var SubtitleEncoder = class { + constructor(options) { + __privateAdd(this, _parseTimestamp); + __privateAdd(this, _formatTimestamp); + __privateAdd(this, _options2, void 0); + __privateAdd(this, _config, void 0); + __privateAdd(this, _preambleSeen, false); + __privateAdd(this, _preambleBytes, void 0); + __privateAdd(this, _preambleEmitted, false); + __privateSet(this, _options2, options); + } + configure(config) { + if (config.codec !== "webvtt") { + throw new Error("Codec must be 'webvtt'."); + } + __privateSet(this, _config, config); + } + encode(text) { + var _a; + if (!__privateGet(this, _config)) { + throw new Error("Encoder not configured."); + } + text = text.replace("\r\n", "\n").replace("\r", "\n"); + cueBlockHeaderRegex.lastIndex = 0; + let match; + if (!__privateGet(this, _preambleSeen)) { + if (!preambleStartRegex.test(text)) { + let error = new Error("WebVTT preamble incorrect."); + __privateGet(this, _options2).error(error); + throw error; + } + match = cueBlockHeaderRegex.exec(text); + let preamble = text.slice(0, (_a = match == null ? void 0 : match.index) != null ? _a : text.length).trimEnd(); + if (!preamble) { + let error = new Error("No WebVTT preamble provided."); + __privateGet(this, _options2).error(error); + throw error; + } + __privateSet(this, _preambleBytes, textEncoder.encode(preamble)); + __privateSet(this, _preambleSeen, true); + if (match) { + text = text.slice(match.index); + cueBlockHeaderRegex.lastIndex = 0; + } + } + while (match = cueBlockHeaderRegex.exec(text)) { + let notes = text.slice(0, match.index); + let cueIdentifier = match[1] || ""; + let matchEnd = match.index + match[0].length; + let bodyStart = text.indexOf("\n", matchEnd) + 1; + let cueSettings = text.slice(matchEnd, bodyStart).trim(); + let bodyEnd = text.indexOf("\n\n", matchEnd); + if (bodyEnd === -1) + bodyEnd = text.length; + let startTime = __privateMethod(this, _parseTimestamp, parseTimestamp_fn).call(this, match[2]); + let endTime = __privateMethod(this, _parseTimestamp, parseTimestamp_fn).call(this, match[3]); + let duration = endTime - startTime; + let body = text.slice(bodyStart, bodyEnd); + let additions = `${cueSettings} +${cueIdentifier} +${notes}`; + inlineTimestampRegex.lastIndex = 0; + body = body.replace(inlineTimestampRegex, (match2) => { + let time = __privateMethod(this, _parseTimestamp, parseTimestamp_fn).call(this, match2.slice(1, -1)); + let offsetTime = time - startTime; + return `<${__privateMethod(this, _formatTimestamp, formatTimestamp_fn).call(this, offsetTime)}>`; + }); + text = text.slice(bodyEnd).trimStart(); + cueBlockHeaderRegex.lastIndex = 0; + let chunk = { + body: textEncoder.encode(body), + additions: additions === "\n\n" ? void 0 : textEncoder.encode(additions), + timestamp: startTime * 1e3, + duration: duration * 1e3 + }; + let meta = {}; + if (!__privateGet(this, _preambleEmitted)) { + meta.decoderConfig = { + description: __privateGet(this, _preambleBytes) + }; + __privateSet(this, _preambleEmitted, true); + } + __privateGet(this, _options2).output(chunk, meta); + } + } + }; + _options2 = new WeakMap(); + _config = new WeakMap(); + _preambleSeen = new WeakMap(); + _preambleBytes = new WeakMap(); + _preambleEmitted = new WeakMap(); + _parseTimestamp = new WeakSet(); + parseTimestamp_fn = function(string) { + let match = timestampRegex.exec(string); + if (!match) + throw new Error("Expected match."); + return 60 * 60 * 1e3 * Number(match[1] || "0") + 60 * 1e3 * Number(match[2]) + 1e3 * Number(match[3]) + Number(match[4]); + }; + _formatTimestamp = new WeakSet(); + formatTimestamp_fn = function(timestamp) { + let hours = Math.floor(timestamp / (60 * 60 * 1e3)); + let minutes = Math.floor(timestamp % (60 * 60 * 1e3) / (60 * 1e3)); + let seconds = Math.floor(timestamp % (60 * 1e3) / 1e3); + let milliseconds = timestamp % 1e3; + return hours.toString().padStart(2, "0") + ":" + minutes.toString().padStart(2, "0") + ":" + seconds.toString().padStart(2, "0") + "." + milliseconds.toString().padStart(3, "0"); + }; return __toCommonJS(src_exports); })(); if (typeof module === "object" && typeof module.exports === "object") Object.assign(module.exports, WebMMuxer) diff --git a/build/webm-muxer.min.js b/build/webm-muxer.min.js index 9664a0c..325dfeb 100644 --- a/build/webm-muxer.min.js +++ b/build/webm-muxer.min.js @@ -1,6 +1,16 @@ -"use strict";var WebMMuxer=(()=>{var St=Object.defineProperty;var Me=Object.getOwnPropertyDescriptor;var ve=Object.getOwnPropertyNames,oe=Object.getOwnPropertySymbols;var de=Object.prototype.hasOwnProperty,Ne=Object.prototype.propertyIsEnumerable;var b=Math.pow,le=(h,e,i)=>e in h?St(h,e,{enumerable:!0,configurable:!0,writable:!0,value:i}):h[e]=i,ue=(h,e)=>{for(var i in e||={})de.call(e,i)&&le(h,i,e[i]);if(oe)for(var i of oe(e))Ne.call(e,i)&&le(h,i,e[i]);return h};var Oe=(h,e)=>{for(var i in e)St(h,i,{get:e[i],enumerable:!0})},He=(h,e,i,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of ve(e))!de.call(h,r)&&r!==i&&St(h,r,{get:()=>e[r],enumerable:!(s=Me(e,r))||s.enumerable});return h};var We=h=>He(St({},"__esModule",{value:!0}),h);var jt=(h,e,i)=>{if(!e.has(h))throw TypeError("Cannot "+i)};var t=(h,e,i)=>(jt(h,e,"read from private field"),i?i.call(h):e.get(h)),a=(h,e,i)=>{if(e.has(h))throw TypeError("Cannot add the same private member more than once");e instanceof WeakSet?e.add(h):e.set(h,i)},f=(h,e,i,s)=>(jt(h,e,"write to private field"),s?s.call(h,i):e.set(h,i),i);var o=(h,e,i)=>(jt(h,e,"access private method"),i);var Xe={};Oe(Xe,{ArrayBufferTarget:()=>st,FileSystemWritableFileStreamTarget:()=>rt,Muxer:()=>Nt,StreamTarget:()=>E});var B=class{constructor(e){this.value=e}},D=class{constructor(e){this.value=e}};var Lt=h=>h<1<<8?1:h<1<<16?2:h<1<<24?3:h{if(h<(1<<7)-1)return 1;if(h<(1<<14)-1)return 2;if(h<(1<<21)-1)return 3;if(h<(1<<28)-1)return 4;if(h{let s=0;for(let r=e;r>c;s<<=1,s|=it}return s},me=(h,e,i,s)=>{for(let r=e;r>i-r-1<>8),t(this,m).setUint8(s++,e);break;case 3:t(this,m).setUint8(s++,1<<5|e>>16),t(this,m).setUint8(s++,e>>8),t(this,m).setUint8(s++,e);break;case 4:t(this,m).setUint8(s++,1<<4|e>>24),t(this,m).setUint8(s++,e>>16),t(this,m).setUint8(s++,e>>8),t(this,m).setUint8(s++,e);break;case 5:t(this,m).setUint8(s++,1<<3|e/b(2,32)&7),t(this,m).setUint8(s++,e>>24),t(this,m).setUint8(s++,e>>16),t(this,m).setUint8(s++,e>>8),t(this,m).setUint8(s++,e);break;case 6:t(this,m).setUint8(s++,1<<2|e/b(2,40)&3),t(this,m).setUint8(s++,e/b(2,32)|0),t(this,m).setUint8(s++,e>>24),t(this,m).setUint8(s++,e>>16),t(this,m).setUint8(s++,e>>8),t(this,m).setUint8(s++,e);break;default:throw new Error("Bad EBML VINT size "+i)}this.write(t(this,x).subarray(0,s))}writeEBML(e){var i,s;if(e!==null)if(e instanceof Uint8Array)this.write(e);else if(Array.isArray(e))for(let r of e)this.writeEBML(r);else if(this.offsets.set(e,this.pos),o(this,ot,Jt).call(this,e.id),Array.isArray(e.data)){let r=this.pos,n=e.size===-1?1:(i=e.size)!=null?i:4;e.size===-1?o(this,Vt,ce).call(this,255):this.seek(this.pos+n);let d=this.pos;if(this.dataOffsets.set(e,d),this.writeEBML(e.data),e.size!==-1){let c=this.pos-d,it=this.pos;this.seek(r),this.writeEBMLVarInt(c,n),this.seek(it)}}else if(typeof e.data=="number"){let r=(s=e.size)!=null?s:Lt(e.data);this.writeEBMLVarInt(r),o(this,ot,Jt).call(this,e.data,r)}else typeof e.data=="string"?(this.writeEBMLVarInt(e.data.length),o(this,Et,ge).call(this,e.data)):e.data instanceof Uint8Array?(this.writeEBMLVarInt(e.data.byteLength,e.size),this.write(e.data)):e.data instanceof B?(this.writeEBMLVarInt(4),o(this,Dt,be).call(this,e.data.value)):e.data instanceof D&&(this.writeEBMLVarInt(8),o(this,Pt,pe).call(this,e.data.value))}};x=new WeakMap,m=new WeakMap,Vt=new WeakSet,ce=function(e){t(this,m).setUint8(0,e),this.write(t(this,x).subarray(0,1))},Dt=new WeakSet,be=function(e){t(this,m).setFloat32(0,e,!1),this.write(t(this,x).subarray(0,4))},Pt=new WeakSet,pe=function(e){t(this,m).setFloat64(0,e,!1),this.write(t(this,x))},ot=new WeakSet,Jt=function(e,i=Lt(e)){let s=0;switch(i){case 6:t(this,m).setUint8(s++,e/b(2,40)|0);case 5:t(this,m).setUint8(s++,e/b(2,32)|0);case 4:t(this,m).setUint8(s++,e>>24);case 3:t(this,m).setUint8(s++,e>>16);case 2:t(this,m).setUint8(s++,e>>8);case 1:t(this,m).setUint8(s++,e);break;default:throw new Error("Bad UINT size "+i)}this.write(t(this,x).subarray(0,s))},Et=new WeakSet,ge=function(e){this.write(new Uint8Array(e.split("").map(i=>i.charCodeAt(0))))};var lt,A,K,dt,It,Ut=class extends at{constructor(i){super();a(this,dt);a(this,lt,void 0);a(this,A,new ArrayBuffer(b(2,16)));a(this,K,new Uint8Array(t(this,A)));f(this,lt,i)}write(i){o(this,dt,It).call(this,this.pos+i.byteLength),t(this,K).set(i,this.pos),this.pos+=i.byteLength}finalize(){o(this,dt,It).call(this,this.pos),t(this,lt).buffer=t(this,A).slice(0,this.pos)}};lt=new WeakMap,A=new WeakMap,K=new WeakMap,dt=new WeakSet,It=function(i){let s=t(this,A).byteLength;for(;sr.start-n.start);i.push({start:s[0].start,size:s[0].data.byteLength});for(let r=1;rW.start<=s&&sKe){for(let W=0;W=i.written[d+1].start;)i.written[d].end=Math.max(i.written[d].end,i.written[d+1].end),i.written.splice(d+1,1)},Rt=new WeakSet,ye=function(i){let r={start:Math.floor(i/t(this,g))*t(this,g),data:new Uint8Array(t(this,g)),written:[],shouldFlush:!1};return t(this,p).push(r),t(this,p).sort((n,d)=>n.start-d.start),t(this,p).indexOf(r)},Z=new WeakSet,At=function(i=!1){for(let s=0;se.stream.write({type:"write",data:r,position:n}),void 0,{chunkSize:(s=e.options)==null?void 0:s.chunkSize}),i)}};var F=1,_t=2,$e=1,Ye=2,ee=b(2,15),Mt=b(2,12),xe="https://github.com/Vanilagy/webm-muxer",ke=6,Ce=5,Ze=["strict","offset","permissive"],u,l,q,G,w,_,M,z,v,k,N,y,O,Q,C,T,j,L,J,I,tt,gt,Ot,Te,Ht,Se,Wt,Ae,Bt,Ue,Kt,ze,$t,Ve,Yt,De,wt,ie,yt,se,Zt,Pe,H,pt,V,X,Xt,Ee,qt,Fe,xt,re,Gt,Re,S,R,et,vt,kt,ae,Qt,_e,Ct,ne,Tt,he,Nt=class{constructor(e){a(this,Ot);a(this,Ht);a(this,Wt);a(this,Bt);a(this,Kt);a(this,$t);a(this,Yt);a(this,wt);a(this,yt);a(this,Zt);a(this,H);a(this,V);a(this,Xt);a(this,qt);a(this,xt);a(this,Gt);a(this,S);a(this,et);a(this,kt);a(this,Qt);a(this,Ct);a(this,Tt);a(this,u,void 0);a(this,l,void 0);a(this,q,void 0);a(this,G,void 0);a(this,w,void 0);a(this,_,void 0);a(this,M,void 0);a(this,z,void 0);a(this,v,void 0);a(this,k,void 0);a(this,N,void 0);a(this,y,void 0);a(this,O,void 0);a(this,Q,0);a(this,C,[]);a(this,T,[]);a(this,j,void 0);a(this,L,void 0);a(this,J,-1);a(this,I,-1);a(this,tt,void 0);a(this,gt,!1);var s;o(this,Ot,Te).call(this,e),f(this,u,ue({type:"webm",firstTimestampBehavior:"strict"},e)),this.target=e.target;let i=!!t(this,u).streaming;if(e.target instanceof st)f(this,l,new Ut(e.target));else if(e.target instanceof E)f(this,l,(s=e.target.options)!=null&&s.chunked?new ht(e.target,i):new nt(e.target,i));else if(e.target instanceof rt)f(this,l,new zt(e.target,i));else throw new Error(`Invalid target: ${e.target}`);o(this,Ht,Se).call(this)}addVideoChunk(e,i,s){let r=new Uint8Array(e.byteLength);e.copyTo(r),this.addVideoChunkRaw(r,e.type,s!=null?s:e.timestamp,i)}addVideoChunkRaw(e,i,s,r){if(o(this,Tt,he).call(this),!t(this,u).video)throw new Error("No video track declared.");t(this,j)===void 0&&f(this,j,s),r&&o(this,Xt,Ee).call(this,r);let n=o(this,xt,re).call(this,e,i,s,F);for(t(this,u).video.codec==="V_VP9"&&o(this,qt,Fe).call(this,n),f(this,J,n.timestamp);t(this,T).length>0&&t(this,T)[0].timestamp<=n.timestamp;){let d=t(this,T).shift();o(this,S,R).call(this,d)}!t(this,u).audio||n.timestamp<=t(this,I)?o(this,S,R).call(this,n):t(this,C).push(n),o(this,H,pt).call(this)}addAudioChunk(e,i,s){let r=new Uint8Array(e.byteLength);e.copyTo(r),this.addAudioChunkRaw(r,e.type,s!=null?s:e.timestamp,i)}addAudioChunkRaw(e,i,s,r){if(o(this,Tt,he).call(this),!t(this,u).audio)throw new Error("No audio track declared.");t(this,L)===void 0&&f(this,L,s),r!=null&&r.decoderConfig&&(t(this,u).streaming?f(this,k,o(this,et,vt).call(this,r.decoderConfig.description)):o(this,kt,ae).call(this,t(this,k),r.decoderConfig.description));let n=o(this,xt,re).call(this,e,i,s,_t);for(f(this,I,n.timestamp);t(this,C).length>0&&t(this,C)[0].timestamp<=n.timestamp;){let d=t(this,C).shift();o(this,S,R).call(this,d)}!t(this,u).video||n.timestamp<=t(this,J)?o(this,S,R).call(this,n):t(this,T).push(n),o(this,H,pt).call(this)}finalize(){for(;t(this,C).length>0;)o(this,S,R).call(this,t(this,C).shift());for(;t(this,T).length>0;)o(this,S,R).call(this,t(this,T).shift());if(t(this,u).streaming||o(this,Ct,ne).call(this),t(this,l).writeEBML(t(this,N)),!t(this,u).streaming){let e=t(this,l).pos,i=t(this,l).pos-t(this,V,X);t(this,l).seek(t(this,l).offsets.get(t(this,q))+4),t(this,l).writeEBMLVarInt(i,ke),t(this,M).data=new D(t(this,Q)),t(this,l).seek(t(this,l).offsets.get(t(this,M))),t(this,l).writeEBML(t(this,M)),t(this,w).data[0].data[1].data=t(this,l).offsets.get(t(this,N))-t(this,V,X),t(this,w).data[1].data[1].data=t(this,l).offsets.get(t(this,G))-t(this,V,X),t(this,w).data[2].data[1].data=t(this,l).offsets.get(t(this,_))-t(this,V,X),t(this,l).seek(t(this,l).offsets.get(t(this,w))),t(this,l).writeEBML(t(this,w)),t(this,l).seek(e)}o(this,H,pt).call(this),t(this,l).finalize(),f(this,gt,!0)}};u=new WeakMap,l=new WeakMap,q=new WeakMap,G=new WeakMap,w=new WeakMap,_=new WeakMap,M=new WeakMap,z=new WeakMap,v=new WeakMap,k=new WeakMap,N=new WeakMap,y=new WeakMap,O=new WeakMap,Q=new WeakMap,C=new WeakMap,T=new WeakMap,j=new WeakMap,L=new WeakMap,J=new WeakMap,I=new WeakMap,tt=new WeakMap,gt=new WeakMap,Ot=new WeakSet,Te=function(e){if(e.type&&e.type!=="webm"&&e.type!=="matroska")throw new Error(`Invalid type: ${e.type}`);if(e.firstTimestampBehavior&&!Ze.includes(e.firstTimestampBehavior))throw new Error(`Invalid first timestamp behavior: ${e.firstTimestampBehavior}`)},Ht=new WeakSet,Se=function(){o(this,Wt,Ae).call(this),t(this,u).streaming||o(this,$t,Ve).call(this),o(this,Yt,De).call(this),o(this,Bt,Ue).call(this),o(this,Kt,ze).call(this),t(this,u).streaming||(o(this,wt,ie).call(this),o(this,yt,se).call(this)),o(this,Zt,Pe).call(this),o(this,H,pt).call(this)},Wt=new WeakSet,Ae=function(){var i;let e={id:440786851,data:[{id:17030,data:1},{id:17143,data:1},{id:17138,data:4},{id:17139,data:8},{id:17026,data:(i=t(this,u).type)!=null?i:"webm"},{id:17031,data:2},{id:17029,data:2}]};t(this,l).writeEBML(e)},Bt=new WeakSet,Ue=function(){f(this,v,{id:236,size:4,data:new Uint8Array(Mt)}),f(this,k,{id:236,size:4,data:new Uint8Array(Mt)})},Kt=new WeakSet,ze=function(){f(this,z,{id:21936,data:[{id:21937,data:2},{id:21946,data:2},{id:21947,data:2},{id:21945,data:0}]})},$t=new WeakSet,Ve=function(){let e=new Uint8Array([28,83,187,107]),i=new Uint8Array([21,73,169,102]),s=new Uint8Array([22,84,174,107]),r={id:290298740,data:[{id:19899,data:[{id:21419,data:e},{id:21420,size:5,data:0}]},{id:19899,data:[{id:21419,data:i},{id:21420,size:5,data:0}]},{id:19899,data:[{id:21419,data:s},{id:21420,size:5,data:0}]}]};f(this,w,r)},Yt=new WeakSet,De=function(){let e={id:17545,data:new D(0)};f(this,M,e);let i={id:357149030,data:[{id:2807729,data:1e6},{id:19840,data:xe},{id:22337,data:xe},t(this,u).streaming?null:e]};f(this,G,i)},wt=new WeakSet,ie=function(){let e={id:374648427,data:[]};f(this,_,e),t(this,u).video&&e.data.push({id:174,data:[{id:215,data:F},{id:29637,data:F},{id:131,data:$e},{id:134,data:t(this,u).video.codec},t(this,v),t(this,u).video.frameRate?{id:2352003,data:1e9/t(this,u).video.frameRate}:null,{id:224,data:[{id:176,data:t(this,u).video.width},{id:186,data:t(this,u).video.height},t(this,u).video.alpha?{id:21440,data:1}:null,t(this,z)]}]}),t(this,u).audio&&(f(this,k,t(this,u).streaming?t(this,k)||null:{id:236,size:4,data:new Uint8Array(Mt)}),e.data.push({id:174,data:[{id:215,data:_t},{id:29637,data:_t},{id:131,data:Ye},{id:134,data:t(this,u).audio.codec},t(this,k),{id:225,data:[{id:181,data:new B(t(this,u).audio.sampleRate)},{id:159,data:t(this,u).audio.numberOfChannels},t(this,u).audio.bitDepth?{id:25188,data:t(this,u).audio.bitDepth}:null]}]}))},yt=new WeakSet,se=function(){let e={id:408125543,size:t(this,u).streaming?-1:ke,data:[t(this,u).streaming?null:t(this,w),t(this,G),t(this,_)]};f(this,q,e),t(this,l).writeEBML(e)},Zt=new WeakSet,Pe=function(){f(this,N,{id:475249515,data:[]})},H=new WeakSet,pt=function(){t(this,l)instanceof nt&&t(this,l).flush()},V=new WeakSet,X=function(){return t(this,l).dataOffsets.get(t(this,q))},Xt=new WeakSet,Ee=function(e){if(!!e.decoderConfig){if(e.decoderConfig.colorSpace){let i=e.decoderConfig.colorSpace;if(f(this,tt,i),t(this,z).data=[{id:21937,data:{rgb:1,bt709:1,bt470bg:5,smpte170m:6}[i.matrix]},{id:21946,data:{bt709:1,smpte170m:6,"iec61966-2-1":13}[i.transfer]},{id:21947,data:{bt709:1,bt470bg:5,smpte170m:6}[i.primaries]},{id:21945,data:[1,2][Number(i.fullRange)]}],!t(this,u).streaming){let s=t(this,l).pos;t(this,l).seek(t(this,l).offsets.get(t(this,z))),t(this,l).writeEBML(t(this,z)),t(this,l).seek(s)}}e.decoderConfig.description&&(t(this,u).streaming?f(this,v,o(this,et,vt).call(this,e.decoderConfig.description)):o(this,kt,ae).call(this,t(this,v),e.decoderConfig.description))}},qt=new WeakSet,Fe=function(e){if(e.type!=="key"||!t(this,tt))return;let i=0;if(P(e.data,0,2)!==2)return;i+=2;let s=(P(e.data,i+1,i+2)<<1)+P(e.data,i+0,i+1);i+=2,s===3&&i++;let r=P(e.data,i+0,i+1);if(i++,r)return;let n=P(e.data,i+0,i+1);if(i++,n!==0)return;i+=2;let d=P(e.data,i+0,i+24);if(i+=24,d!==4817730)return;s>=2&&i++;let c={rgb:7,bt709:2,bt470bg:1,smpte170m:3}[t(this,tt).matrix];me(e.data,i+0,i+3,c)},xt=new WeakSet,re=function(e,i,s,r){let n=o(this,Gt,Re).call(this,s,r);return{data:e,type:i,timestamp:n,trackNumber:r}},Gt=new WeakSet,Re=function(e,i){let s=i===F?t(this,j):t(this,L),r=i===F?t(this,J):t(this,I);if(t(this,u).firstTimestampBehavior==="strict"&&r===-1&&e!==0)throw new Error(`The first chunk for your media track must have a timestamp of 0 (received ${e}). Non-zero first timestamps are often caused by directly piping frames or audio data from a MediaStreamTrack into the encoder. Their timestamps are typically relative to the age of the document, which is probably what you want. +"use strict";var WebMMuxer=(()=>{var Bt=Object.defineProperty;var hi=Object.getOwnPropertyDescriptor;var di=Object.getOwnPropertyNames,De=Object.getOwnPropertySymbols;var Pe=Object.prototype.hasOwnProperty,li=Object.prototype.propertyIsEnumerable;var b=Math.pow,Ve=(h,t,i)=>t in h?Bt(h,t,{enumerable:!0,configurable:!0,writable:!0,value:i}):h[t]=i,ve=(h,t)=>{for(var i in t||={})Pe.call(t,i)&&Ve(h,i,t[i]);if(De)for(var i of De(t))li.call(t,i)&&Ve(h,i,t[i]);return h};var ui=(h,t)=>{for(var i in t)Bt(h,i,{get:t[i],enumerable:!0})},fi=(h,t,i,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of di(t))!Pe.call(h,r)&&r!==i&&Bt(h,r,{get:()=>t[r],enumerable:!(s=hi(t,r))||s.enumerable});return h};var ci=h=>fi(Bt({},"__esModule",{value:!0}),h);var ge=(h,t,i)=>{if(!t.has(h))throw TypeError("Cannot "+i)};var e=(h,t,i)=>(ge(h,t,"read from private field"),i?i.call(h):t.get(h)),n=(h,t,i)=>{if(t.has(h))throw TypeError("Cannot add the same private member more than once");t instanceof WeakSet?t.add(h):t.set(h,i)},f=(h,t,i,s)=>(ge(h,t,"write to private field"),s?s.call(h,i):t.set(h,i),i);var o=(h,t,i)=>(ge(h,t,"access private method"),i);var ki={};ui(ki,{ArrayBufferTarget:()=>ct,FileSystemWritableFileStreamTarget:()=>mt,Muxer:()=>te,StreamTarget:()=>_,SubtitleEncoder:()=>me});var q=class{constructor(t){this.value=t}},M=class{constructor(t){this.value=t}};var we=h=>h<1<<8?1:h<1<<16?2:h<1<<24?3:h{if(h<(1<<7)-1)return 1;if(h<(1<<14)-1)return 2;if(h<(1<<21)-1)return 3;if(h<(1<<28)-1)return 4;if(h{let s=0;for(let r=t;r>m;s<<=1,s|=w}return s},Me=(h,t,i,s)=>{for(let r=t;r>i-r-1<>8),e(this,c).setUint8(s++,t);break;case 3:e(this,c).setUint8(s++,1<<5|t>>16),e(this,c).setUint8(s++,t>>8),e(this,c).setUint8(s++,t);break;case 4:e(this,c).setUint8(s++,1<<4|t>>24),e(this,c).setUint8(s++,t>>16),e(this,c).setUint8(s++,t>>8),e(this,c).setUint8(s++,t);break;case 5:e(this,c).setUint8(s++,1<<3|t/b(2,32)&7),e(this,c).setUint8(s++,t>>24),e(this,c).setUint8(s++,t>>16),e(this,c).setUint8(s++,t>>8),e(this,c).setUint8(s++,t);break;case 6:e(this,c).setUint8(s++,1<<2|t/b(2,40)&3),e(this,c).setUint8(s++,t/b(2,32)|0),e(this,c).setUint8(s++,t>>24),e(this,c).setUint8(s++,t>>16),e(this,c).setUint8(s++,t>>8),e(this,c).setUint8(s++,t);break;default:throw new Error("Bad EBML VINT size "+i)}this.write(e(this,x).subarray(0,s))}writeEBML(t){var i,s;if(t!==null)if(t instanceof Uint8Array)this.write(t);else if(Array.isArray(t))for(let r of t)this.writeEBML(r);else if(this.offsets.set(t,this.pos),o(this,wt,ye).call(this,t.id),Array.isArray(t.data)){let r=this.pos,a=t.size===-1?1:(i=t.size)!=null?i:4;t.size===-1?o(this,Kt,Fe).call(this,255):this.seek(this.pos+a);let l=this.pos;if(this.dataOffsets.set(t,l),this.writeEBML(t.data),t.size!==-1){let m=this.pos-l,w=this.pos;this.seek(r),this.writeEBMLVarInt(m,a),this.seek(w)}}else if(typeof t.data=="number"){let r=(s=t.size)!=null?s:we(t.data);this.writeEBMLVarInt(r),o(this,wt,ye).call(this,t.data,r)}else typeof t.data=="string"?(this.writeEBMLVarInt(t.data.length),o(this,Zt,Oe).call(this,t.data)):t.data instanceof Uint8Array?(this.writeEBMLVarInt(t.data.byteLength,t.size),this.write(t.data)):t.data instanceof q?(this.writeEBMLVarInt(4),o(this,Gt,_e).call(this,t.data.value)):t.data instanceof M&&(this.writeEBMLVarInt(8),o(this,Yt,Ne).call(this,t.data.value))}};x=new WeakMap,c=new WeakMap,Kt=new WeakSet,Fe=function(t){e(this,c).setUint8(0,t),this.write(e(this,x).subarray(0,1))},Gt=new WeakSet,_e=function(t){e(this,c).setFloat32(0,t,!1),this.write(e(this,x).subarray(0,4))},Yt=new WeakSet,Ne=function(t){e(this,c).setFloat64(0,t,!1),this.write(e(this,x))},wt=new WeakSet,ye=function(t,i=we(t)){let s=0;switch(i){case 6:e(this,c).setUint8(s++,t/b(2,40)|0);case 5:e(this,c).setUint8(s++,t/b(2,32)|0);case 4:e(this,c).setUint8(s++,t>>24);case 3:e(this,c).setUint8(s++,t>>16);case 2:e(this,c).setUint8(s++,t>>8);case 1:e(this,c).setUint8(s++,t);break;default:throw new Error("Bad UINT size "+i)}this.write(e(this,x).subarray(0,s))},Zt=new WeakSet,Oe=function(t){this.write(new Uint8Array(t.split("").map(i=>i.charCodeAt(0))))};var yt,z,Q,Ct,Ce,Wt=class extends bt{constructor(i){super();n(this,Ct);n(this,yt,void 0);n(this,z,new ArrayBuffer(b(2,16)));n(this,Q,new Uint8Array(e(this,z)));f(this,yt,i)}write(i){o(this,Ct,Ce).call(this,this.pos+i.byteLength),e(this,Q).set(i,this.pos),this.pos+=i.byteLength}finalize(){o(this,Ct,Ce).call(this,this.pos),e(this,yt).buffer=e(this,z).slice(0,this.pos)}};yt=new WeakMap,z=new WeakMap,Q=new WeakMap,Ct=new WeakSet,Ce=function(i){let s=e(this,z).byteLength;for(;sr.start-a.start);i.push({start:s[0].start,size:s[0].data.byteLength});for(let r=1;rp.start<=s&&sbi){for(let p=0;p=i.written[l+1].start;)i.written[l].end=Math.max(i.written[l].end,i.written[l+1].end),i.written.splice(l+1,1)},Qt=new WeakSet,He=function(i){let r={start:Math.floor(i/e(this,y))*e(this,y),data:new Uint8Array(e(this,y)),written:[],shouldFlush:!1};return e(this,g).push(r),e(this,g).sort((a,l)=>a.start-l.start),e(this,g).indexOf(r)},J=new WeakSet,Ht=function(i=!1){for(let s=0;st.stream.write({type:"write",data:r,position:a}),void 0,{chunkSize:(s=t.options)==null?void 0:s.chunkSize}),i)}};var L=1,Ut=2,Xt=3,pi=1,gi=2,wi=17,ke=b(2,15),Et=b(2,12),We="https://github.com/Vanilagy/webm-muxer",$e=6,Ke=5,yi=["strict","offset","permissive"],d,u,et,it,T,N,O,V,B,A,H,W,k,st,$,U,E,P,rt,at,K,G,Dt,nt,Vt,ee,Ge,ie,Ye,se,Ze,re,qe,ae,Qe,ne,Xe,oe,je,Pt,xe,vt,Se,he,Je,v,I,R,tt,de,Le,le,Ie,ot,jt,ht,Jt,ue,ti,C,S,Y,zt,dt,Lt,fe,ei,Rt,Ae,lt,It,te=class{constructor(t){n(this,ee);n(this,ie);n(this,se);n(this,re);n(this,ae);n(this,ne);n(this,oe);n(this,Pt);n(this,vt);n(this,he);n(this,v);n(this,R);n(this,de);n(this,le);n(this,ot);n(this,ht);n(this,ue);n(this,C);n(this,Y);n(this,dt);n(this,fe);n(this,Rt);n(this,lt);n(this,d,void 0);n(this,u,void 0);n(this,et,void 0);n(this,it,void 0);n(this,T,void 0);n(this,N,void 0);n(this,O,void 0);n(this,V,void 0);n(this,B,void 0);n(this,A,void 0);n(this,H,void 0);n(this,W,void 0);n(this,k,void 0);n(this,st,void 0);n(this,$,0);n(this,U,[]);n(this,E,[]);n(this,P,[]);n(this,rt,void 0);n(this,at,void 0);n(this,K,-1);n(this,G,-1);n(this,Dt,-1);n(this,nt,void 0);n(this,Vt,!1);var s;o(this,ee,Ge).call(this,t),f(this,d,ve({type:"webm",firstTimestampBehavior:"strict"},t)),this.target=t.target;let i=!!e(this,d).streaming;if(t.target instanceof ct)f(this,u,new Wt(t.target));else if(t.target instanceof _)f(this,u,(s=t.target.options)!=null&&s.chunked?new gt(t.target,i):new pt(t.target,i));else if(t.target instanceof mt)f(this,u,new $t(t.target,i));else throw new Error(`Invalid target: ${t.target}`);o(this,ie,Ye).call(this)}addVideoChunk(t,i,s){let r=new Uint8Array(t.byteLength);t.copyTo(r),this.addVideoChunkRaw(r,t.type,s!=null?s:t.timestamp,i)}addVideoChunkRaw(t,i,s,r){if(o(this,lt,It).call(this),!e(this,d).video)throw new Error("No video track declared.");e(this,rt)===void 0&&f(this,rt,s),r&&o(this,de,Le).call(this,r);let a=o(this,ht,Jt).call(this,t,i,s,L);for(e(this,d).video.codec==="V_VP9"&&o(this,le,Ie).call(this,a),f(this,K,a.timestamp);e(this,E).length>0&&e(this,E)[0].timestamp<=a.timestamp;){let l=e(this,E).shift();o(this,C,S).call(this,l,!1)}!e(this,d).audio||a.timestamp<=e(this,G)?o(this,C,S).call(this,a,!0):e(this,U).push(a),o(this,ot,jt).call(this),o(this,v,I).call(this)}addAudioChunk(t,i,s){let r=new Uint8Array(t.byteLength);t.copyTo(r),this.addAudioChunkRaw(r,t.type,s!=null?s:t.timestamp,i)}addAudioChunkRaw(t,i,s,r){if(o(this,lt,It).call(this),!e(this,d).audio)throw new Error("No audio track declared.");e(this,at)===void 0&&f(this,at,s),r!=null&&r.decoderConfig&&(e(this,d).streaming?f(this,A,o(this,Y,zt).call(this,r.decoderConfig.description)):o(this,dt,Lt).call(this,e(this,A),r.decoderConfig.description));let a=o(this,ht,Jt).call(this,t,i,s,Ut);for(f(this,G,a.timestamp);e(this,U).length>0&&e(this,U)[0].timestamp<=a.timestamp;){let l=e(this,U).shift();o(this,C,S).call(this,l,!0)}!e(this,d).video||a.timestamp<=e(this,K)?o(this,C,S).call(this,a,!e(this,d).video):e(this,E).push(a),o(this,ot,jt).call(this),o(this,v,I).call(this)}addSubtitleChunk(t,i){if(o(this,lt,It).call(this),!e(this,d).subtitles)throw new Error("No subtitle track declared.");i!=null&&i.decoderConfig&&(e(this,d).streaming?f(this,H,o(this,Y,zt).call(this,i.decoderConfig.description)):o(this,dt,Lt).call(this,e(this,H),i.decoderConfig.description));let s=o(this,ht,Jt).call(this,t.body,"key",t.timestamp,Xt,t.duration,t.additions);f(this,Dt,s.timestamp),e(this,P).push(s),o(this,ot,jt).call(this),o(this,v,I).call(this)}finalize(){for(;e(this,U).length>0;)o(this,C,S).call(this,e(this,U).shift(),!0);for(;e(this,E).length>0;)o(this,C,S).call(this,e(this,E).shift(),!0);for(;e(this,P).length>0&&e(this,P)[0].timestamp<=e(this,$);)o(this,C,S).call(this,e(this,P).shift(),!1);if(e(this,d).streaming||o(this,Rt,Ae).call(this),e(this,u).writeEBML(e(this,W)),!e(this,d).streaming){let t=e(this,u).pos,i=e(this,u).pos-e(this,R,tt);e(this,u).seek(e(this,u).offsets.get(e(this,et))+4),e(this,u).writeEBMLVarInt(i,$e),e(this,O).data=new M(e(this,$)),e(this,u).seek(e(this,u).offsets.get(e(this,O))),e(this,u).writeEBML(e(this,O)),e(this,T).data[0].data[1].data=e(this,u).offsets.get(e(this,W))-e(this,R,tt),e(this,T).data[1].data[1].data=e(this,u).offsets.get(e(this,it))-e(this,R,tt),e(this,T).data[2].data[1].data=e(this,u).offsets.get(e(this,N))-e(this,R,tt),e(this,u).seek(e(this,u).offsets.get(e(this,T))),e(this,u).writeEBML(e(this,T)),e(this,u).seek(t)}o(this,v,I).call(this),e(this,u).finalize(),f(this,Vt,!0)}};d=new WeakMap,u=new WeakMap,et=new WeakMap,it=new WeakMap,T=new WeakMap,N=new WeakMap,O=new WeakMap,V=new WeakMap,B=new WeakMap,A=new WeakMap,H=new WeakMap,W=new WeakMap,k=new WeakMap,st=new WeakMap,$=new WeakMap,U=new WeakMap,E=new WeakMap,P=new WeakMap,rt=new WeakMap,at=new WeakMap,K=new WeakMap,G=new WeakMap,Dt=new WeakMap,nt=new WeakMap,Vt=new WeakMap,ee=new WeakSet,Ge=function(t){if(t.type&&t.type!=="webm"&&t.type!=="matroska")throw new Error(`Invalid type: ${t.type}`);if(t.firstTimestampBehavior&&!yi.includes(t.firstTimestampBehavior))throw new Error(`Invalid first timestamp behavior: ${t.firstTimestampBehavior}`)},ie=new WeakSet,Ye=function(){o(this,se,Ze).call(this),e(this,d).streaming||o(this,ne,Xe).call(this),o(this,oe,je).call(this),o(this,re,qe).call(this),o(this,ae,Qe).call(this),e(this,d).streaming||(o(this,Pt,xe).call(this),o(this,vt,Se).call(this)),o(this,he,Je).call(this),o(this,v,I).call(this)},se=new WeakSet,Ze=function(){var i;let t={id:440786851,data:[{id:17030,data:1},{id:17143,data:1},{id:17138,data:4},{id:17139,data:8},{id:17026,data:(i=e(this,d).type)!=null?i:"webm"},{id:17031,data:2},{id:17029,data:2}]};e(this,u).writeEBML(t)},re=new WeakSet,qe=function(){f(this,B,{id:236,size:4,data:new Uint8Array(Et)}),f(this,A,{id:236,size:4,data:new Uint8Array(Et)}),f(this,H,{id:236,size:4,data:new Uint8Array(Et)})},ae=new WeakSet,Qe=function(){f(this,V,{id:21936,data:[{id:21937,data:2},{id:21946,data:2},{id:21947,data:2},{id:21945,data:0}]})},ne=new WeakSet,Xe=function(){let t=new Uint8Array([28,83,187,107]),i=new Uint8Array([21,73,169,102]),s=new Uint8Array([22,84,174,107]),r={id:290298740,data:[{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}]},{id:19899,data:[{id:21419,data:s},{id:21420,size:5,data:0}]}]};f(this,T,r)},oe=new WeakSet,je=function(){let t={id:17545,data:new M(0)};f(this,O,t);let i={id:357149030,data:[{id:2807729,data:1e6},{id:19840,data:We},{id:22337,data:We},e(this,d).streaming?null:t]};f(this,it,i)},Pt=new WeakSet,xe=function(){let t={id:374648427,data:[]};f(this,N,t),e(this,d).video&&t.data.push({id:174,data:[{id:215,data:L},{id:29637,data:L},{id:131,data:pi},{id:134,data:e(this,d).video.codec},e(this,B),e(this,d).video.frameRate?{id:2352003,data:1e9/e(this,d).video.frameRate}:null,{id:224,data:[{id:176,data:e(this,d).video.width},{id:186,data:e(this,d).video.height},e(this,d).video.alpha?{id:21440,data:1}:null,e(this,V)]}]}),e(this,d).audio&&(f(this,A,e(this,d).streaming?e(this,A)||null:{id:236,size:4,data:new Uint8Array(Et)}),t.data.push({id:174,data:[{id:215,data:Ut},{id:29637,data:Ut},{id:131,data:gi},{id:134,data:e(this,d).audio.codec},e(this,A),{id:225,data:[{id:181,data:new q(e(this,d).audio.sampleRate)},{id:159,data:e(this,d).audio.numberOfChannels},e(this,d).audio.bitDepth?{id:25188,data:e(this,d).audio.bitDepth}:null]}]})),e(this,d).subtitles&&t.data.push({id:174,data:[{id:215,data:Xt},{id:29637,data:Xt},{id:131,data:wi},{id:134,data:e(this,d).subtitles.codec},e(this,H)]})},vt=new WeakSet,Se=function(){let t={id:408125543,size:e(this,d).streaming?-1:$e,data:[e(this,d).streaming?null:e(this,T),e(this,it),e(this,N)]};f(this,et,t),e(this,u).writeEBML(t)},he=new WeakSet,Je=function(){f(this,W,{id:475249515,data:[]})},v=new WeakSet,I=function(){e(this,u)instanceof pt&&e(this,u).flush()},R=new WeakSet,tt=function(){return e(this,u).dataOffsets.get(e(this,et))},de=new WeakSet,Le=function(t){if(!!t.decoderConfig){if(t.decoderConfig.colorSpace){let i=t.decoderConfig.colorSpace;if(f(this,nt,i),e(this,V).data=[{id:21937,data:{rgb:1,bt709:1,bt470bg:5,smpte170m:6}[i.matrix]},{id:21946,data:{bt709:1,smpte170m:6,"iec61966-2-1":13}[i.transfer]},{id:21947,data:{bt709:1,bt470bg:5,smpte170m:6}[i.primaries]},{id:21945,data:[1,2][Number(i.fullRange)]}],!e(this,d).streaming){let s=e(this,u).pos;e(this,u).seek(e(this,u).offsets.get(e(this,V))),e(this,u).writeEBML(e(this,V)),e(this,u).seek(s)}}t.decoderConfig.description&&(e(this,d).streaming?f(this,B,o(this,Y,zt).call(this,t.decoderConfig.description)):o(this,dt,Lt).call(this,e(this,B),t.decoderConfig.description))}},le=new WeakSet,Ie=function(t){if(t.type!=="key"||!e(this,nt))return;let i=0;if(F(t.data,0,2)!==2)return;i+=2;let s=(F(t.data,i+1,i+2)<<1)+F(t.data,i+0,i+1);i+=2,s===3&&i++;let r=F(t.data,i+0,i+1);if(i++,r)return;let a=F(t.data,i+0,i+1);if(i++,a!==0)return;i+=2;let l=F(t.data,i+0,i+24);if(i+=24,l!==4817730)return;s>=2&&i++;let m={rgb:7,bt709:2,bt470bg:1,smpte170m:3}[e(this,nt).matrix];Me(t.data,i+0,i+3,m)},ot=new WeakSet,jt=function(){let t=Math.min(e(this,d).video?e(this,K):1/0,e(this,d).audio?e(this,G):1/0),i=e(this,P);for(;i.length>0&&i[0].timestamp<=t;)o(this,C,S).call(this,i.shift(),!e(this,d).video&&!e(this,d).audio)},ht=new WeakSet,Jt=function(t,i,s,r,a,l){let m=o(this,ue,ti).call(this,s,r);return{data:t,additions:l,type:i,timestamp:m,duration:a,trackNumber:r}},ue=new WeakSet,ti=function(t,i){let s=i===L?e(this,K):i===Ut?e(this,G):e(this,Dt);if(i!==Xt){let r=i===L?e(this,rt):e(this,at);if(e(this,d).firstTimestampBehavior==="strict"&&s===-1&&t!==0)throw new Error(`The first chunk for your media track must have a timestamp of 0 (received ${t}). Non-zero first timestamps are often caused by directly piping frames or audio data from a MediaStreamTrack into the encoder. Their timestamps are typically relative to the age of the document, which is probably what you want. If you want to offset all timestamps of a track such that the first one is zero, set firstTimestampBehavior: 'offset' in the options. If you want to allow non-zero first timestamps, set firstTimestampBehavior: 'permissive'. -`);if(t(this,u).firstTimestampBehavior==="offset"&&(e-=s),e=ee)throw new Error(`Current Matroska cluster exceeded its maximum allowed length of ${ee} milliseconds. In order to produce a correct WebM file, you must pass in a video key frame at least every ${ee} milliseconds.`);let r=(e.trackNumber===F||!t(this,u).video)&&e.type==="key"&&i-t(this,O)>=1e3;(!t(this,y)||r)&&o(this,Qt,_e).call(this,i);let n=new Uint8Array(4),d=new DataView(n.buffer);d.setUint8(0,128|e.trackNumber),d.setUint16(1,i-t(this,O),!1),d.setUint8(3,Number(e.type==="key")<<7);let c={id:163,data:[n,e.data]};t(this,l).writeEBML(c),f(this,Q,Math.max(t(this,Q),i))},et=new WeakSet,vt=function(e){return{id:25506,size:4,data:new Uint8Array(e)}},kt=new WeakSet,ae=function(e,i){let s=t(this,l).pos;t(this,l).seek(t(this,l).offsets.get(e)),e=[o(this,et,vt).call(this,i),{id:236,size:4,data:new Uint8Array(Mt-2-4-i.byteLength)}],t(this,l).writeEBML(e),t(this,l).seek(s)},Qt=new WeakSet,_e=function(e){t(this,y)&&!t(this,u).streaming&&o(this,Ct,ne).call(this),f(this,y,{id:524531317,size:t(this,u).streaming?-1:Ce,data:[{id:231,data:e}]}),t(this,l).writeEBML(t(this,y)),f(this,O,e);let i=t(this,l).offsets.get(t(this,y))-t(this,V,X);t(this,N).data.push({id:187,data:[{id:179,data:e},t(this,u).video?{id:183,data:[{id:247,data:F},{id:241,data:i}]}:null,t(this,u).audio?{id:183,data:[{id:247,data:_t},{id:241,data:i}]}:null]})},Ct=new WeakSet,ne=function(){let e=t(this,l).pos-t(this,l).dataOffsets.get(t(this,y)),i=t(this,l).pos;t(this,l).seek(t(this,l).offsets.get(t(this,y))+4),t(this,l).writeEBMLVarInt(e,Ce),t(this,l).seek(i)},Tt=new WeakSet,he=function(){if(t(this,gt))throw new Error("Cannot add new video or audio chunks after the file has been finalized.")};return We(Xe);})(); +`);e(this,d).firstTimestampBehavior==="offset"&&(t-=r)}if(t=1e3;(!e(this,k)||r)&&o(this,fe,ei).call(this,s);let a=s-e(this,st);if(a<0)return;if(a>=ke)throw new Error(`Current Matroska cluster exceeded its maximum allowed length of ${ke} milliseconds. In order to produce a correct WebM file, you must pass in a key frame at least every ${ke} milliseconds.`);let m=new Uint8Array(4),w=new DataView(m.buffer);if(w.setUint8(0,128|t.trackNumber),w.setInt16(1,a,!1),t.duration===void 0&&!t.additions){w.setUint8(3,Number(t.type==="key")<<7);let p={id:163,data:[m,t.data]};e(this,u).writeEBML(p)}else{let p=Math.floor(t.duration/1e3),ft={id:160,data:[{id:161,data:[m,t.data]},t.duration!==void 0?{id:155,data:p}:null,t.additions?{id:30113,data:t.additions}:null]};e(this,u).writeEBML(ft)}f(this,$,Math.max(e(this,$),s))},Y=new WeakSet,zt=function(t){return{id:25506,size:4,data:new Uint8Array(t)}},dt=new WeakSet,Lt=function(t,i){let s=e(this,u).pos;e(this,u).seek(e(this,u).offsets.get(t));let r=2+4+i.byteLength,a=Et-r;if(a<0){let l=i.byteLength+a;i instanceof ArrayBuffer?i=i.slice(0,l):i=i.buffer.slice(0,l),a=0}t=[o(this,Y,zt).call(this,i),{id:236,size:4,data:new Uint8Array(a)}],e(this,u).writeEBML(t),e(this,u).seek(s)},fe=new WeakSet,ei=function(t){e(this,k)&&!e(this,d).streaming&&o(this,Rt,Ae).call(this),f(this,k,{id:524531317,size:e(this,d).streaming?-1:Ke,data:[{id:231,data:t}]}),e(this,u).writeEBML(e(this,k)),f(this,st,t);let i=e(this,u).offsets.get(e(this,k))-e(this,R,tt);e(this,W).data.push({id:187,data:[{id:179,data:t},e(this,d).video?{id:183,data:[{id:247,data:L},{id:241,data:i}]}:null,e(this,d).audio?{id:183,data:[{id:247,data:Ut},{id:241,data:i}]}:null]})},Rt=new WeakSet,Ae=function(){let t=e(this,u).pos-e(this,u).dataOffsets.get(e(this,k)),i=e(this,u).pos;e(this,u).seek(e(this,u).offsets.get(e(this,k))+4),e(this,u).writeEBMLVarInt(t,Ke),e(this,u).seek(i)},lt=new WeakSet,It=function(){if(e(this,Vt))throw new Error("Cannot add new video or audio chunks after the file has been finalized.")};var Mt=/(?:(.+?)\n)?((?:\d{2}:)?\d{2}:\d{2}.\d{3})\s+-->\s+((?:\d{2}:)?\d{2}:\d{2}.\d{3})/g,Ci=/^WEBVTT.*?\n{2}/,Ti=/(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})/,ii=/<(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})>/g,Ue=new TextEncoder,Z,Ft,_t,Nt,Ot,ut,ce,be,si,me=class{constructor(t){n(this,ut);n(this,be);n(this,Z,void 0);n(this,Ft,void 0);n(this,_t,!1);n(this,Nt,void 0);n(this,Ot,!1);f(this,Z,t)}configure(t){if(t.codec!=="webvtt")throw new Error("Codec must be 'webvtt'.");f(this,Ft,t)}encode(t){var s;if(!e(this,Ft))throw new Error("Encoder not configured.");t=t.replace(`\r +`,` +`).replace("\r",` +`),Mt.lastIndex=0;let i;if(!e(this,_t)){if(!Ci.test(t)){let a=new Error("WebVTT preamble incorrect.");throw e(this,Z).error(a),a}i=Mt.exec(t);let r=t.slice(0,(s=i==null?void 0:i.index)!=null?s:t.length).trimEnd();if(!r){let a=new Error("No WebVTT preamble provided.");throw e(this,Z).error(a),a}f(this,Nt,Ue.encode(r)),f(this,_t,!0),i&&(t=t.slice(i.index),Mt.lastIndex=0)}for(;i=Mt.exec(t);){let r=t.slice(0,i.index),a=i[1]||"",l=i.index+i[0].length,m=t.indexOf(` +`,l)+1,w=t.slice(l,m).trim(),p=t.indexOf(` + +`,l);p===-1&&(p=t.length);let ft=o(this,ut,ce).call(this,i[2]),ri=o(this,ut,ce).call(this,i[3])-ft,pe=t.slice(m,p),Ee=`${w} +${a} +${r}`;ii.lastIndex=0,pe=pe.replace(ii,ni=>{let oi=o(this,ut,ce).call(this,ni.slice(1,-1))-ft;return`<${o(this,be,si).call(this,oi)}>`}),t=t.slice(p).trimStart(),Mt.lastIndex=0;let ai={body:Ue.encode(pe),additions:Ee===` + +`?void 0:Ue.encode(Ee),timestamp:ft*1e3,duration:ri*1e3},ze={};e(this,Ot)||(ze.decoderConfig={description:e(this,Nt)},f(this,Ot,!0)),e(this,Z).output(ai,ze)}}};Z=new WeakMap,Ft=new WeakMap,_t=new WeakMap,Nt=new WeakMap,Ot=new WeakMap,ut=new WeakSet,ce=function(t){let i=Ti.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])},be=new WeakSet,si=function(t){let i=Math.floor(t/36e5),s=Math.floor(t%(60*60*1e3)/(60*1e3)),r=Math.floor(t%(60*1e3)/1e3),a=t%1e3;return i.toString().padStart(2,"0")+":"+s.toString().padStart(2,"0")+":"+r.toString().padStart(2,"0")+"."+a.toString().padStart(3,"0")};return ci(ki);})(); if (typeof module === "object" && typeof module.exports === "object") Object.assign(module.exports, WebMMuxer) diff --git a/build/webm-muxer.min.mjs b/build/webm-muxer.min.mjs index d657391..5f43d78 100644 --- a/build/webm-muxer.min.mjs +++ b/build/webm-muxer.min.mjs @@ -1,5 +1,15 @@ -var Re=Object.defineProperty;var he=Object.getOwnPropertySymbols;var _e=Object.prototype.hasOwnProperty,Me=Object.prototype.propertyIsEnumerable;var b=Math.pow,oe=(o,e,i)=>e in o?Re(o,e,{enumerable:!0,configurable:!0,writable:!0,value:i}):o[e]=i,le=(o,e)=>{for(var i in e||={})_e.call(e,i)&&oe(o,i,e[i]);if(he)for(var i of he(e))Me.call(e,i)&&oe(o,i,e[i]);return o};var Gt=(o,e,i)=>{if(!e.has(o))throw TypeError("Cannot "+i)};var t=(o,e,i)=>(Gt(o,e,"read from private field"),i?i.call(o):e.get(o)),a=(o,e,i)=>{if(e.has(o))throw TypeError("Cannot add the same private member more than once");e instanceof WeakSet?e.add(o):e.set(o,i)},f=(o,e,i,s)=>(Gt(o,e,"write to private field"),s?s.call(o,i):e.set(o,i),i);var h=(o,e,i)=>(Gt(o,e,"access private method"),i);var W=class{constructor(e){this.value=e}},D=class{constructor(e){this.value=e}};var Qt=o=>o<1<<8?1:o<1<<16?2:o<1<<24?3:o{if(o<(1<<7)-1)return 1;if(o<(1<<14)-1)return 2;if(o<(1<<21)-1)return 3;if(o<(1<<28)-1)return 4;if(o{let s=0;for(let r=e;r>c;s<<=1,s|=it}return s},ue=(o,e,i,s)=>{for(let r=e;r>i-r-1<>8),t(this,m).setUint8(s++,e);break;case 3:t(this,m).setUint8(s++,1<<5|e>>16),t(this,m).setUint8(s++,e>>8),t(this,m).setUint8(s++,e);break;case 4:t(this,m).setUint8(s++,1<<4|e>>24),t(this,m).setUint8(s++,e>>16),t(this,m).setUint8(s++,e>>8),t(this,m).setUint8(s++,e);break;case 5:t(this,m).setUint8(s++,1<<3|e/b(2,32)&7),t(this,m).setUint8(s++,e>>24),t(this,m).setUint8(s++,e>>16),t(this,m).setUint8(s++,e>>8),t(this,m).setUint8(s++,e);break;case 6:t(this,m).setUint8(s++,1<<2|e/b(2,40)&3),t(this,m).setUint8(s++,e/b(2,32)|0),t(this,m).setUint8(s++,e>>24),t(this,m).setUint8(s++,e>>16),t(this,m).setUint8(s++,e>>8),t(this,m).setUint8(s++,e);break;default:throw new Error("Bad EBML VINT size "+i)}this.write(t(this,x).subarray(0,s))}writeEBML(e){var i,s;if(e!==null)if(e instanceof Uint8Array)this.write(e);else if(Array.isArray(e))for(let r of e)this.writeEBML(r);else if(this.offsets.set(e,this.pos),h(this,nt,jt).call(this,e.id),Array.isArray(e.data)){let r=this.pos,n=e.size===-1?1:(i=e.size)!=null?i:4;e.size===-1?h(this,zt,fe).call(this,255):this.seek(this.pos+n);let d=this.pos;if(this.dataOffsets.set(e,d),this.writeEBML(e.data),e.size!==-1){let c=this.pos-d,it=this.pos;this.seek(r),this.writeEBMLVarInt(c,n),this.seek(it)}}else if(typeof e.data=="number"){let r=(s=e.size)!=null?s:Qt(e.data);this.writeEBMLVarInt(r),h(this,nt,jt).call(this,e.data,r)}else typeof e.data=="string"?(this.writeEBMLVarInt(e.data.length),h(this,Pt,be).call(this,e.data)):e.data instanceof Uint8Array?(this.writeEBMLVarInt(e.data.byteLength,e.size),this.write(e.data)):e.data instanceof W?(this.writeEBMLVarInt(4),h(this,Vt,me).call(this,e.data.value)):e.data instanceof D&&(this.writeEBMLVarInt(8),h(this,Dt,ce).call(this,e.data.value))}};x=new WeakMap,m=new WeakMap,zt=new WeakSet,fe=function(e){t(this,m).setUint8(0,e),this.write(t(this,x).subarray(0,1))},Vt=new WeakSet,me=function(e){t(this,m).setFloat32(0,e,!1),this.write(t(this,x).subarray(0,4))},Dt=new WeakSet,ce=function(e){t(this,m).setFloat64(0,e,!1),this.write(t(this,x))},nt=new WeakSet,jt=function(e,i=Qt(e)){let s=0;switch(i){case 6:t(this,m).setUint8(s++,e/b(2,40)|0);case 5:t(this,m).setUint8(s++,e/b(2,32)|0);case 4:t(this,m).setUint8(s++,e>>24);case 3:t(this,m).setUint8(s++,e>>16);case 2:t(this,m).setUint8(s++,e>>8);case 1:t(this,m).setUint8(s++,e);break;default:throw new Error("Bad UINT size "+i)}this.write(t(this,x).subarray(0,s))},Pt=new WeakSet,be=function(e){this.write(new Uint8Array(e.split("").map(i=>i.charCodeAt(0))))};var ht,A,K,ot,Lt,At=class extends st{constructor(i){super();a(this,ot);a(this,ht,void 0);a(this,A,new ArrayBuffer(b(2,16)));a(this,K,new Uint8Array(t(this,A)));f(this,ht,i)}write(i){h(this,ot,Lt).call(this,this.pos+i.byteLength),t(this,K).set(i,this.pos),this.pos+=i.byteLength}finalize(){h(this,ot,Lt).call(this,this.pos),t(this,ht).buffer=t(this,A).slice(0,this.pos)}};ht=new WeakMap,A=new WeakMap,K=new WeakMap,ot=new WeakSet,Lt=function(i){let s=t(this,A).byteLength;for(;sr.start-n.start);i.push({start:s[0].start,size:s[0].data.byteLength});for(let r=1;rH.start<=s&&sNe){for(let H=0;H=i.written[d+1].start;)i.written[d].end=Math.max(i.written[d].end,i.written[d+1].end),i.written.splice(d+1,1)},Ft=new WeakSet,ge=function(i){let r={start:Math.floor(i/t(this,g))*t(this,g),data:new Uint8Array(t(this,g)),written:[],shouldFlush:!1};return t(this,p).push(r),t(this,p).sort((n,d)=>n.start-d.start),t(this,p).indexOf(r)},Z=new WeakSet,St=function(i=!1){for(let s=0;se.stream.write({type:"write",data:r,position:n}),void 0,{chunkSize:(s=e.options)==null?void 0:s.chunkSize}),i)}};var E=1,Rt=2,Oe=1,He=2,It=b(2,15),_t=b(2,12),we="https://github.com/Vanilagy/webm-muxer",ye=6,xe=5,We=["strict","offset","permissive"],u,l,q,G,w,R,_,z,M,k,v,y,N,Q,C,T,j,L,J,I,tt,bt,vt,ke,Nt,Ce,Ot,Te,Ht,Se,Wt,Ae,Bt,Ue,Kt,ze,pt,ee,gt,ie,$t,Ve,O,ct,V,X,Yt,De,Zt,Pe,wt,se,Xt,Ee,S,F,et,Mt,yt,re,qt,Fe,xt,ae,kt,ne,te=class{constructor(e){a(this,vt);a(this,Nt);a(this,Ot);a(this,Ht);a(this,Wt);a(this,Bt);a(this,Kt);a(this,pt);a(this,gt);a(this,$t);a(this,O);a(this,V);a(this,Yt);a(this,Zt);a(this,wt);a(this,Xt);a(this,S);a(this,et);a(this,yt);a(this,qt);a(this,xt);a(this,kt);a(this,u,void 0);a(this,l,void 0);a(this,q,void 0);a(this,G,void 0);a(this,w,void 0);a(this,R,void 0);a(this,_,void 0);a(this,z,void 0);a(this,M,void 0);a(this,k,void 0);a(this,v,void 0);a(this,y,void 0);a(this,N,void 0);a(this,Q,0);a(this,C,[]);a(this,T,[]);a(this,j,void 0);a(this,L,void 0);a(this,J,-1);a(this,I,-1);a(this,tt,void 0);a(this,bt,!1);var s;h(this,vt,ke).call(this,e),f(this,u,le({type:"webm",firstTimestampBehavior:"strict"},e)),this.target=e.target;let i=!!t(this,u).streaming;if(e.target instanceof Ct)f(this,l,new At(e.target));else if(e.target instanceof B)f(this,l,(s=e.target.options)!=null&&s.chunked?new at(e.target,i):new rt(e.target,i));else if(e.target instanceof Tt)f(this,l,new Ut(e.target,i));else throw new Error(`Invalid target: ${e.target}`);h(this,Nt,Ce).call(this)}addVideoChunk(e,i,s){let r=new Uint8Array(e.byteLength);e.copyTo(r),this.addVideoChunkRaw(r,e.type,s!=null?s:e.timestamp,i)}addVideoChunkRaw(e,i,s,r){if(h(this,kt,ne).call(this),!t(this,u).video)throw new Error("No video track declared.");t(this,j)===void 0&&f(this,j,s),r&&h(this,Yt,De).call(this,r);let n=h(this,wt,se).call(this,e,i,s,E);for(t(this,u).video.codec==="V_VP9"&&h(this,Zt,Pe).call(this,n),f(this,J,n.timestamp);t(this,T).length>0&&t(this,T)[0].timestamp<=n.timestamp;){let d=t(this,T).shift();h(this,S,F).call(this,d)}!t(this,u).audio||n.timestamp<=t(this,I)?h(this,S,F).call(this,n):t(this,C).push(n),h(this,O,ct).call(this)}addAudioChunk(e,i,s){let r=new Uint8Array(e.byteLength);e.copyTo(r),this.addAudioChunkRaw(r,e.type,s!=null?s:e.timestamp,i)}addAudioChunkRaw(e,i,s,r){if(h(this,kt,ne).call(this),!t(this,u).audio)throw new Error("No audio track declared.");t(this,L)===void 0&&f(this,L,s),r!=null&&r.decoderConfig&&(t(this,u).streaming?f(this,k,h(this,et,Mt).call(this,r.decoderConfig.description)):h(this,yt,re).call(this,t(this,k),r.decoderConfig.description));let n=h(this,wt,se).call(this,e,i,s,Rt);for(f(this,I,n.timestamp);t(this,C).length>0&&t(this,C)[0].timestamp<=n.timestamp;){let d=t(this,C).shift();h(this,S,F).call(this,d)}!t(this,u).video||n.timestamp<=t(this,J)?h(this,S,F).call(this,n):t(this,T).push(n),h(this,O,ct).call(this)}finalize(){for(;t(this,C).length>0;)h(this,S,F).call(this,t(this,C).shift());for(;t(this,T).length>0;)h(this,S,F).call(this,t(this,T).shift());if(t(this,u).streaming||h(this,xt,ae).call(this),t(this,l).writeEBML(t(this,v)),!t(this,u).streaming){let e=t(this,l).pos,i=t(this,l).pos-t(this,V,X);t(this,l).seek(t(this,l).offsets.get(t(this,q))+4),t(this,l).writeEBMLVarInt(i,ye),t(this,_).data=new D(t(this,Q)),t(this,l).seek(t(this,l).offsets.get(t(this,_))),t(this,l).writeEBML(t(this,_)),t(this,w).data[0].data[1].data=t(this,l).offsets.get(t(this,v))-t(this,V,X),t(this,w).data[1].data[1].data=t(this,l).offsets.get(t(this,G))-t(this,V,X),t(this,w).data[2].data[1].data=t(this,l).offsets.get(t(this,R))-t(this,V,X),t(this,l).seek(t(this,l).offsets.get(t(this,w))),t(this,l).writeEBML(t(this,w)),t(this,l).seek(e)}h(this,O,ct).call(this),t(this,l).finalize(),f(this,bt,!0)}};u=new WeakMap,l=new WeakMap,q=new WeakMap,G=new WeakMap,w=new WeakMap,R=new WeakMap,_=new WeakMap,z=new WeakMap,M=new WeakMap,k=new WeakMap,v=new WeakMap,y=new WeakMap,N=new WeakMap,Q=new WeakMap,C=new WeakMap,T=new WeakMap,j=new WeakMap,L=new WeakMap,J=new WeakMap,I=new WeakMap,tt=new WeakMap,bt=new WeakMap,vt=new WeakSet,ke=function(e){if(e.type&&e.type!=="webm"&&e.type!=="matroska")throw new Error(`Invalid type: ${e.type}`);if(e.firstTimestampBehavior&&!We.includes(e.firstTimestampBehavior))throw new Error(`Invalid first timestamp behavior: ${e.firstTimestampBehavior}`)},Nt=new WeakSet,Ce=function(){h(this,Ot,Te).call(this),t(this,u).streaming||h(this,Bt,Ue).call(this),h(this,Kt,ze).call(this),h(this,Ht,Se).call(this),h(this,Wt,Ae).call(this),t(this,u).streaming||(h(this,pt,ee).call(this),h(this,gt,ie).call(this)),h(this,$t,Ve).call(this),h(this,O,ct).call(this)},Ot=new WeakSet,Te=function(){var i;let e={id:440786851,data:[{id:17030,data:1},{id:17143,data:1},{id:17138,data:4},{id:17139,data:8},{id:17026,data:(i=t(this,u).type)!=null?i:"webm"},{id:17031,data:2},{id:17029,data:2}]};t(this,l).writeEBML(e)},Ht=new WeakSet,Se=function(){f(this,M,{id:236,size:4,data:new Uint8Array(_t)}),f(this,k,{id:236,size:4,data:new Uint8Array(_t)})},Wt=new WeakSet,Ae=function(){f(this,z,{id:21936,data:[{id:21937,data:2},{id:21946,data:2},{id:21947,data:2},{id:21945,data:0}]})},Bt=new WeakSet,Ue=function(){let e=new Uint8Array([28,83,187,107]),i=new Uint8Array([21,73,169,102]),s=new Uint8Array([22,84,174,107]),r={id:290298740,data:[{id:19899,data:[{id:21419,data:e},{id:21420,size:5,data:0}]},{id:19899,data:[{id:21419,data:i},{id:21420,size:5,data:0}]},{id:19899,data:[{id:21419,data:s},{id:21420,size:5,data:0}]}]};f(this,w,r)},Kt=new WeakSet,ze=function(){let e={id:17545,data:new D(0)};f(this,_,e);let i={id:357149030,data:[{id:2807729,data:1e6},{id:19840,data:we},{id:22337,data:we},t(this,u).streaming?null:e]};f(this,G,i)},pt=new WeakSet,ee=function(){let e={id:374648427,data:[]};f(this,R,e),t(this,u).video&&e.data.push({id:174,data:[{id:215,data:E},{id:29637,data:E},{id:131,data:Oe},{id:134,data:t(this,u).video.codec},t(this,M),t(this,u).video.frameRate?{id:2352003,data:1e9/t(this,u).video.frameRate}:null,{id:224,data:[{id:176,data:t(this,u).video.width},{id:186,data:t(this,u).video.height},t(this,u).video.alpha?{id:21440,data:1}:null,t(this,z)]}]}),t(this,u).audio&&(f(this,k,t(this,u).streaming?t(this,k)||null:{id:236,size:4,data:new Uint8Array(_t)}),e.data.push({id:174,data:[{id:215,data:Rt},{id:29637,data:Rt},{id:131,data:He},{id:134,data:t(this,u).audio.codec},t(this,k),{id:225,data:[{id:181,data:new W(t(this,u).audio.sampleRate)},{id:159,data:t(this,u).audio.numberOfChannels},t(this,u).audio.bitDepth?{id:25188,data:t(this,u).audio.bitDepth}:null]}]}))},gt=new WeakSet,ie=function(){let e={id:408125543,size:t(this,u).streaming?-1:ye,data:[t(this,u).streaming?null:t(this,w),t(this,G),t(this,R)]};f(this,q,e),t(this,l).writeEBML(e)},$t=new WeakSet,Ve=function(){f(this,v,{id:475249515,data:[]})},O=new WeakSet,ct=function(){t(this,l)instanceof rt&&t(this,l).flush()},V=new WeakSet,X=function(){return t(this,l).dataOffsets.get(t(this,q))},Yt=new WeakSet,De=function(e){if(!!e.decoderConfig){if(e.decoderConfig.colorSpace){let i=e.decoderConfig.colorSpace;if(f(this,tt,i),t(this,z).data=[{id:21937,data:{rgb:1,bt709:1,bt470bg:5,smpte170m:6}[i.matrix]},{id:21946,data:{bt709:1,smpte170m:6,"iec61966-2-1":13}[i.transfer]},{id:21947,data:{bt709:1,bt470bg:5,smpte170m:6}[i.primaries]},{id:21945,data:[1,2][Number(i.fullRange)]}],!t(this,u).streaming){let s=t(this,l).pos;t(this,l).seek(t(this,l).offsets.get(t(this,z))),t(this,l).writeEBML(t(this,z)),t(this,l).seek(s)}}e.decoderConfig.description&&(t(this,u).streaming?f(this,M,h(this,et,Mt).call(this,e.decoderConfig.description)):h(this,yt,re).call(this,t(this,M),e.decoderConfig.description))}},Zt=new WeakSet,Pe=function(e){if(e.type!=="key"||!t(this,tt))return;let i=0;if(P(e.data,0,2)!==2)return;i+=2;let s=(P(e.data,i+1,i+2)<<1)+P(e.data,i+0,i+1);i+=2,s===3&&i++;let r=P(e.data,i+0,i+1);if(i++,r)return;let n=P(e.data,i+0,i+1);if(i++,n!==0)return;i+=2;let d=P(e.data,i+0,i+24);if(i+=24,d!==4817730)return;s>=2&&i++;let c={rgb:7,bt709:2,bt470bg:1,smpte170m:3}[t(this,tt).matrix];ue(e.data,i+0,i+3,c)},wt=new WeakSet,se=function(e,i,s,r){let n=h(this,Xt,Ee).call(this,s,r);return{data:e,type:i,timestamp:n,trackNumber:r}},Xt=new WeakSet,Ee=function(e,i){let s=i===E?t(this,j):t(this,L),r=i===E?t(this,J):t(this,I);if(t(this,u).firstTimestampBehavior==="strict"&&r===-1&&e!==0)throw new Error(`The first chunk for your media track must have a timestamp of 0 (received ${e}). Non-zero first timestamps are often caused by directly piping frames or audio data from a MediaStreamTrack into the encoder. Their timestamps are typically relative to the age of the document, which is probably what you want. +var ni=Object.defineProperty;var ze=Object.getOwnPropertySymbols;var oi=Object.prototype.hasOwnProperty,hi=Object.prototype.propertyIsEnumerable;var b=Math.pow,De=(h,t,i)=>t in h?ni(h,t,{enumerable:!0,configurable:!0,writable:!0,value:i}):h[t]=i,Ve=(h,t)=>{for(var i in t||={})oi.call(t,i)&&De(h,i,t[i]);if(ze)for(var i of ze(t))hi.call(t,i)&&De(h,i,t[i]);return h};var me=(h,t,i)=>{if(!t.has(h))throw TypeError("Cannot "+i)};var e=(h,t,i)=>(me(h,t,"read from private field"),i?i.call(h):t.get(h)),n=(h,t,i)=>{if(t.has(h))throw TypeError("Cannot add the same private member more than once");t instanceof WeakSet?t.add(h):t.set(h,i)},f=(h,t,i,s)=>(me(h,t,"write to private field"),s?s.call(h,i):t.set(h,i),i);var o=(h,t,i)=>(me(h,t,"access private method"),i);var Z=class{constructor(t){this.value=t}},M=class{constructor(t){this.value=t}};var be=h=>h<1<<8?1:h<1<<16?2:h<1<<24?3:h{if(h<(1<<7)-1)return 1;if(h<(1<<14)-1)return 2;if(h<(1<<21)-1)return 3;if(h<(1<<28)-1)return 4;if(h{let s=0;for(let r=t;r>m;s<<=1,s|=w}return s},ve=(h,t,i,s)=>{for(let r=t;r>i-r-1<>8),e(this,c).setUint8(s++,t);break;case 3:e(this,c).setUint8(s++,1<<5|t>>16),e(this,c).setUint8(s++,t>>8),e(this,c).setUint8(s++,t);break;case 4:e(this,c).setUint8(s++,1<<4|t>>24),e(this,c).setUint8(s++,t>>16),e(this,c).setUint8(s++,t>>8),e(this,c).setUint8(s++,t);break;case 5:e(this,c).setUint8(s++,1<<3|t/b(2,32)&7),e(this,c).setUint8(s++,t>>24),e(this,c).setUint8(s++,t>>16),e(this,c).setUint8(s++,t>>8),e(this,c).setUint8(s++,t);break;case 6:e(this,c).setUint8(s++,1<<2|t/b(2,40)&3),e(this,c).setUint8(s++,t/b(2,32)|0),e(this,c).setUint8(s++,t>>24),e(this,c).setUint8(s++,t>>16),e(this,c).setUint8(s++,t>>8),e(this,c).setUint8(s++,t);break;default:throw new Error("Bad EBML VINT size "+i)}this.write(e(this,x).subarray(0,s))}writeEBML(t){var i,s;if(t!==null)if(t instanceof Uint8Array)this.write(t);else if(Array.isArray(t))for(let r of t)this.writeEBML(r);else if(this.offsets.set(t,this.pos),o(this,pt,pe).call(this,t.id),Array.isArray(t.data)){let r=this.pos,a=t.size===-1?1:(i=t.size)!=null?i:4;t.size===-1?o(this,$t,Re).call(this,255):this.seek(this.pos+a);let l=this.pos;if(this.dataOffsets.set(t,l),this.writeEBML(t.data),t.size!==-1){let m=this.pos-l,w=this.pos;this.seek(r),this.writeEBMLVarInt(m,a),this.seek(w)}}else if(typeof t.data=="number"){let r=(s=t.size)!=null?s:be(t.data);this.writeEBMLVarInt(r),o(this,pt,pe).call(this,t.data,r)}else typeof t.data=="string"?(this.writeEBMLVarInt(t.data.length),o(this,Yt,_e).call(this,t.data)):t.data instanceof Uint8Array?(this.writeEBMLVarInt(t.data.byteLength,t.size),this.write(t.data)):t.data instanceof Z?(this.writeEBMLVarInt(4),o(this,Kt,Me).call(this,t.data.value)):t.data instanceof M&&(this.writeEBMLVarInt(8),o(this,Gt,Fe).call(this,t.data.value))}};x=new WeakMap,c=new WeakMap,$t=new WeakSet,Re=function(t){e(this,c).setUint8(0,t),this.write(e(this,x).subarray(0,1))},Kt=new WeakSet,Me=function(t){e(this,c).setFloat32(0,t,!1),this.write(e(this,x).subarray(0,4))},Gt=new WeakSet,Fe=function(t){e(this,c).setFloat64(0,t,!1),this.write(e(this,x))},pt=new WeakSet,pe=function(t,i=be(t)){let s=0;switch(i){case 6:e(this,c).setUint8(s++,t/b(2,40)|0);case 5:e(this,c).setUint8(s++,t/b(2,32)|0);case 4:e(this,c).setUint8(s++,t>>24);case 3:e(this,c).setUint8(s++,t>>16);case 2:e(this,c).setUint8(s++,t>>8);case 1:e(this,c).setUint8(s++,t);break;default:throw new Error("Bad UINT size "+i)}this.write(e(this,x).subarray(0,s))},Yt=new WeakSet,_e=function(t){this.write(new Uint8Array(t.split("").map(i=>i.charCodeAt(0))))};var gt,z,Q,wt,ge,Ht=class extends ct{constructor(i){super();n(this,wt);n(this,gt,void 0);n(this,z,new ArrayBuffer(b(2,16)));n(this,Q,new Uint8Array(e(this,z)));f(this,gt,i)}write(i){o(this,wt,ge).call(this,this.pos+i.byteLength),e(this,Q).set(i,this.pos),this.pos+=i.byteLength}finalize(){o(this,wt,ge).call(this,this.pos),e(this,gt).buffer=e(this,z).slice(0,this.pos)}};gt=new WeakMap,z=new WeakMap,Q=new WeakMap,wt=new WeakSet,ge=function(i){let s=e(this,z).byteLength;for(;sr.start-a.start);i.push({start:s[0].start,size:s[0].data.byteLength});for(let r=1;rp.start<=s&&sli){for(let p=0;p=i.written[l+1].start;)i.written[l].end=Math.max(i.written[l].end,i.written[l+1].end),i.written.splice(l+1,1)},qt=new WeakSet,Oe=function(i){let r={start:Math.floor(i/e(this,y))*e(this,y),data:new Uint8Array(e(this,y)),written:[],shouldFlush:!1};return e(this,g).push(r),e(this,g).sort((a,l)=>a.start-l.start),e(this,g).indexOf(r)},J=new WeakSet,Bt=function(i=!1){for(let s=0;st.stream.write({type:"write",data:r,position:a}),void 0,{chunkSize:(s=t.options)==null?void 0:s.chunkSize}),i)}};var L=1,St=2,Qt=3,ui=1,fi=2,ci=17,ye=b(2,15),At=b(2,12),Be="https://github.com/Vanilagy/webm-muxer",He=6,We=5,mi=["strict","offset","permissive"],d,u,et,it,T,_,N,V,O,A,B,H,k,st,W,U,E,P,rt,at,$,K,Et,nt,zt,It,$e,te,Ke,ee,Ge,ie,Ye,se,Ze,re,qe,ae,Qe,Dt,Te,Vt,ke,ne,Xe,v,I,R,tt,oe,je,he,Je,ot,Xt,ht,jt,de,Le,C,S,G,Ut,dt,Jt,le,Ie,Pt,xe,lt,Lt,Ce=class{constructor(t){n(this,It);n(this,te);n(this,ee);n(this,ie);n(this,se);n(this,re);n(this,ae);n(this,Dt);n(this,Vt);n(this,ne);n(this,v);n(this,R);n(this,oe);n(this,he);n(this,ot);n(this,ht);n(this,de);n(this,C);n(this,G);n(this,dt);n(this,le);n(this,Pt);n(this,lt);n(this,d,void 0);n(this,u,void 0);n(this,et,void 0);n(this,it,void 0);n(this,T,void 0);n(this,_,void 0);n(this,N,void 0);n(this,V,void 0);n(this,O,void 0);n(this,A,void 0);n(this,B,void 0);n(this,H,void 0);n(this,k,void 0);n(this,st,void 0);n(this,W,0);n(this,U,[]);n(this,E,[]);n(this,P,[]);n(this,rt,void 0);n(this,at,void 0);n(this,$,-1);n(this,K,-1);n(this,Et,-1);n(this,nt,void 0);n(this,zt,!1);var s;o(this,It,$e).call(this,t),f(this,d,Ve({type:"webm",firstTimestampBehavior:"strict"},t)),this.target=t.target;let i=!!e(this,d).streaming;if(t.target instanceof Nt)f(this,u,new Ht(t.target));else if(t.target instanceof q)f(this,u,(s=t.target.options)!=null&&s.chunked?new bt(t.target,i):new mt(t.target,i));else if(t.target instanceof Ot)f(this,u,new Wt(t.target,i));else throw new Error(`Invalid target: ${t.target}`);o(this,te,Ke).call(this)}addVideoChunk(t,i,s){let r=new Uint8Array(t.byteLength);t.copyTo(r),this.addVideoChunkRaw(r,t.type,s!=null?s:t.timestamp,i)}addVideoChunkRaw(t,i,s,r){if(o(this,lt,Lt).call(this),!e(this,d).video)throw new Error("No video track declared.");e(this,rt)===void 0&&f(this,rt,s),r&&o(this,oe,je).call(this,r);let a=o(this,ht,jt).call(this,t,i,s,L);for(e(this,d).video.codec==="V_VP9"&&o(this,he,Je).call(this,a),f(this,$,a.timestamp);e(this,E).length>0&&e(this,E)[0].timestamp<=a.timestamp;){let l=e(this,E).shift();o(this,C,S).call(this,l,!1)}!e(this,d).audio||a.timestamp<=e(this,K)?o(this,C,S).call(this,a,!0):e(this,U).push(a),o(this,ot,Xt).call(this),o(this,v,I).call(this)}addAudioChunk(t,i,s){let r=new Uint8Array(t.byteLength);t.copyTo(r),this.addAudioChunkRaw(r,t.type,s!=null?s:t.timestamp,i)}addAudioChunkRaw(t,i,s,r){if(o(this,lt,Lt).call(this),!e(this,d).audio)throw new Error("No audio track declared.");e(this,at)===void 0&&f(this,at,s),r!=null&&r.decoderConfig&&(e(this,d).streaming?f(this,A,o(this,G,Ut).call(this,r.decoderConfig.description)):o(this,dt,Jt).call(this,e(this,A),r.decoderConfig.description));let a=o(this,ht,jt).call(this,t,i,s,St);for(f(this,K,a.timestamp);e(this,U).length>0&&e(this,U)[0].timestamp<=a.timestamp;){let l=e(this,U).shift();o(this,C,S).call(this,l,!0)}!e(this,d).video||a.timestamp<=e(this,$)?o(this,C,S).call(this,a,!e(this,d).video):e(this,E).push(a),o(this,ot,Xt).call(this),o(this,v,I).call(this)}addSubtitleChunk(t,i){if(o(this,lt,Lt).call(this),!e(this,d).subtitles)throw new Error("No subtitle track declared.");i!=null&&i.decoderConfig&&(e(this,d).streaming?f(this,B,o(this,G,Ut).call(this,i.decoderConfig.description)):o(this,dt,Jt).call(this,e(this,B),i.decoderConfig.description));let s=o(this,ht,jt).call(this,t.body,"key",t.timestamp,Qt,t.duration,t.additions);f(this,Et,s.timestamp),e(this,P).push(s),o(this,ot,Xt).call(this),o(this,v,I).call(this)}finalize(){for(;e(this,U).length>0;)o(this,C,S).call(this,e(this,U).shift(),!0);for(;e(this,E).length>0;)o(this,C,S).call(this,e(this,E).shift(),!0);for(;e(this,P).length>0&&e(this,P)[0].timestamp<=e(this,W);)o(this,C,S).call(this,e(this,P).shift(),!1);if(e(this,d).streaming||o(this,Pt,xe).call(this),e(this,u).writeEBML(e(this,H)),!e(this,d).streaming){let t=e(this,u).pos,i=e(this,u).pos-e(this,R,tt);e(this,u).seek(e(this,u).offsets.get(e(this,et))+4),e(this,u).writeEBMLVarInt(i,He),e(this,N).data=new M(e(this,W)),e(this,u).seek(e(this,u).offsets.get(e(this,N))),e(this,u).writeEBML(e(this,N)),e(this,T).data[0].data[1].data=e(this,u).offsets.get(e(this,H))-e(this,R,tt),e(this,T).data[1].data[1].data=e(this,u).offsets.get(e(this,it))-e(this,R,tt),e(this,T).data[2].data[1].data=e(this,u).offsets.get(e(this,_))-e(this,R,tt),e(this,u).seek(e(this,u).offsets.get(e(this,T))),e(this,u).writeEBML(e(this,T)),e(this,u).seek(t)}o(this,v,I).call(this),e(this,u).finalize(),f(this,zt,!0)}};d=new WeakMap,u=new WeakMap,et=new WeakMap,it=new WeakMap,T=new WeakMap,_=new WeakMap,N=new WeakMap,V=new WeakMap,O=new WeakMap,A=new WeakMap,B=new WeakMap,H=new WeakMap,k=new WeakMap,st=new WeakMap,W=new WeakMap,U=new WeakMap,E=new WeakMap,P=new WeakMap,rt=new WeakMap,at=new WeakMap,$=new WeakMap,K=new WeakMap,Et=new WeakMap,nt=new WeakMap,zt=new WeakMap,It=new WeakSet,$e=function(t){if(t.type&&t.type!=="webm"&&t.type!=="matroska")throw new Error(`Invalid type: ${t.type}`);if(t.firstTimestampBehavior&&!mi.includes(t.firstTimestampBehavior))throw new Error(`Invalid first timestamp behavior: ${t.firstTimestampBehavior}`)},te=new WeakSet,Ke=function(){o(this,ee,Ge).call(this),e(this,d).streaming||o(this,re,qe).call(this),o(this,ae,Qe).call(this),o(this,ie,Ye).call(this),o(this,se,Ze).call(this),e(this,d).streaming||(o(this,Dt,Te).call(this),o(this,Vt,ke).call(this)),o(this,ne,Xe).call(this),o(this,v,I).call(this)},ee=new WeakSet,Ge=function(){var i;let t={id:440786851,data:[{id:17030,data:1},{id:17143,data:1},{id:17138,data:4},{id:17139,data:8},{id:17026,data:(i=e(this,d).type)!=null?i:"webm"},{id:17031,data:2},{id:17029,data:2}]};e(this,u).writeEBML(t)},ie=new WeakSet,Ye=function(){f(this,O,{id:236,size:4,data:new Uint8Array(At)}),f(this,A,{id:236,size:4,data:new Uint8Array(At)}),f(this,B,{id:236,size:4,data:new Uint8Array(At)})},se=new WeakSet,Ze=function(){f(this,V,{id:21936,data:[{id:21937,data:2},{id:21946,data:2},{id:21947,data:2},{id:21945,data:0}]})},re=new WeakSet,qe=function(){let t=new Uint8Array([28,83,187,107]),i=new Uint8Array([21,73,169,102]),s=new Uint8Array([22,84,174,107]),r={id:290298740,data:[{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}]},{id:19899,data:[{id:21419,data:s},{id:21420,size:5,data:0}]}]};f(this,T,r)},ae=new WeakSet,Qe=function(){let t={id:17545,data:new M(0)};f(this,N,t);let i={id:357149030,data:[{id:2807729,data:1e6},{id:19840,data:Be},{id:22337,data:Be},e(this,d).streaming?null:t]};f(this,it,i)},Dt=new WeakSet,Te=function(){let t={id:374648427,data:[]};f(this,_,t),e(this,d).video&&t.data.push({id:174,data:[{id:215,data:L},{id:29637,data:L},{id:131,data:ui},{id:134,data:e(this,d).video.codec},e(this,O),e(this,d).video.frameRate?{id:2352003,data:1e9/e(this,d).video.frameRate}:null,{id:224,data:[{id:176,data:e(this,d).video.width},{id:186,data:e(this,d).video.height},e(this,d).video.alpha?{id:21440,data:1}:null,e(this,V)]}]}),e(this,d).audio&&(f(this,A,e(this,d).streaming?e(this,A)||null:{id:236,size:4,data:new Uint8Array(At)}),t.data.push({id:174,data:[{id:215,data:St},{id:29637,data:St},{id:131,data:fi},{id:134,data:e(this,d).audio.codec},e(this,A),{id:225,data:[{id:181,data:new Z(e(this,d).audio.sampleRate)},{id:159,data:e(this,d).audio.numberOfChannels},e(this,d).audio.bitDepth?{id:25188,data:e(this,d).audio.bitDepth}:null]}]})),e(this,d).subtitles&&t.data.push({id:174,data:[{id:215,data:Qt},{id:29637,data:Qt},{id:131,data:ci},{id:134,data:e(this,d).subtitles.codec},e(this,B)]})},Vt=new WeakSet,ke=function(){let t={id:408125543,size:e(this,d).streaming?-1:He,data:[e(this,d).streaming?null:e(this,T),e(this,it),e(this,_)]};f(this,et,t),e(this,u).writeEBML(t)},ne=new WeakSet,Xe=function(){f(this,H,{id:475249515,data:[]})},v=new WeakSet,I=function(){e(this,u)instanceof mt&&e(this,u).flush()},R=new WeakSet,tt=function(){return e(this,u).dataOffsets.get(e(this,et))},oe=new WeakSet,je=function(t){if(!!t.decoderConfig){if(t.decoderConfig.colorSpace){let i=t.decoderConfig.colorSpace;if(f(this,nt,i),e(this,V).data=[{id:21937,data:{rgb:1,bt709:1,bt470bg:5,smpte170m:6}[i.matrix]},{id:21946,data:{bt709:1,smpte170m:6,"iec61966-2-1":13}[i.transfer]},{id:21947,data:{bt709:1,bt470bg:5,smpte170m:6}[i.primaries]},{id:21945,data:[1,2][Number(i.fullRange)]}],!e(this,d).streaming){let s=e(this,u).pos;e(this,u).seek(e(this,u).offsets.get(e(this,V))),e(this,u).writeEBML(e(this,V)),e(this,u).seek(s)}}t.decoderConfig.description&&(e(this,d).streaming?f(this,O,o(this,G,Ut).call(this,t.decoderConfig.description)):o(this,dt,Jt).call(this,e(this,O),t.decoderConfig.description))}},he=new WeakSet,Je=function(t){if(t.type!=="key"||!e(this,nt))return;let i=0;if(F(t.data,0,2)!==2)return;i+=2;let s=(F(t.data,i+1,i+2)<<1)+F(t.data,i+0,i+1);i+=2,s===3&&i++;let r=F(t.data,i+0,i+1);if(i++,r)return;let a=F(t.data,i+0,i+1);if(i++,a!==0)return;i+=2;let l=F(t.data,i+0,i+24);if(i+=24,l!==4817730)return;s>=2&&i++;let m={rgb:7,bt709:2,bt470bg:1,smpte170m:3}[e(this,nt).matrix];ve(t.data,i+0,i+3,m)},ot=new WeakSet,Xt=function(){let t=Math.min(e(this,d).video?e(this,$):1/0,e(this,d).audio?e(this,K):1/0),i=e(this,P);for(;i.length>0&&i[0].timestamp<=t;)o(this,C,S).call(this,i.shift(),!e(this,d).video&&!e(this,d).audio)},ht=new WeakSet,jt=function(t,i,s,r,a,l){let m=o(this,de,Le).call(this,s,r);return{data:t,additions:l,type:i,timestamp:m,duration:a,trackNumber:r}},de=new WeakSet,Le=function(t,i){let s=i===L?e(this,$):i===St?e(this,K):e(this,Et);if(i!==Qt){let r=i===L?e(this,rt):e(this,at);if(e(this,d).firstTimestampBehavior==="strict"&&s===-1&&t!==0)throw new Error(`The first chunk for your media track must have a timestamp of 0 (received ${t}). Non-zero first timestamps are often caused by directly piping frames or audio data from a MediaStreamTrack into the encoder. Their timestamps are typically relative to the age of the document, which is probably what you want. If you want to offset all timestamps of a track such that the first one is zero, set firstTimestampBehavior: 'offset' in the options. If you want to allow non-zero first timestamps, set firstTimestampBehavior: 'permissive'. -`);if(t(this,u).firstTimestampBehavior==="offset"&&(e-=s),e=It)throw new Error(`Current Matroska cluster exceeded its maximum allowed length of ${It} milliseconds. In order to produce a correct WebM file, you must pass in a video key frame at least every ${It} milliseconds.`);let r=(e.trackNumber===E||!t(this,u).video)&&e.type==="key"&&i-t(this,N)>=1e3;(!t(this,y)||r)&&h(this,qt,Fe).call(this,i);let n=new Uint8Array(4),d=new DataView(n.buffer);d.setUint8(0,128|e.trackNumber),d.setUint16(1,i-t(this,N),!1),d.setUint8(3,Number(e.type==="key")<<7);let c={id:163,data:[n,e.data]};t(this,l).writeEBML(c),f(this,Q,Math.max(t(this,Q),i))},et=new WeakSet,Mt=function(e){return{id:25506,size:4,data:new Uint8Array(e)}},yt=new WeakSet,re=function(e,i){let s=t(this,l).pos;t(this,l).seek(t(this,l).offsets.get(e)),e=[h(this,et,Mt).call(this,i),{id:236,size:4,data:new Uint8Array(_t-2-4-i.byteLength)}],t(this,l).writeEBML(e),t(this,l).seek(s)},qt=new WeakSet,Fe=function(e){t(this,y)&&!t(this,u).streaming&&h(this,xt,ae).call(this),f(this,y,{id:524531317,size:t(this,u).streaming?-1:xe,data:[{id:231,data:e}]}),t(this,l).writeEBML(t(this,y)),f(this,N,e);let i=t(this,l).offsets.get(t(this,y))-t(this,V,X);t(this,v).data.push({id:187,data:[{id:179,data:e},t(this,u).video?{id:183,data:[{id:247,data:E},{id:241,data:i}]}:null,t(this,u).audio?{id:183,data:[{id:247,data:Rt},{id:241,data:i}]}:null]})},xt=new WeakSet,ae=function(){let e=t(this,l).pos-t(this,l).dataOffsets.get(t(this,y)),i=t(this,l).pos;t(this,l).seek(t(this,l).offsets.get(t(this,y))+4),t(this,l).writeEBMLVarInt(e,xe),t(this,l).seek(i)},kt=new WeakSet,ne=function(){if(t(this,bt))throw new Error("Cannot add new video or audio chunks after the file has been finalized.")};export{Ct as ArrayBufferTarget,Tt as FileSystemWritableFileStreamTarget,te as Muxer,B as StreamTarget}; +`);e(this,d).firstTimestampBehavior==="offset"&&(t-=r)}if(t=1e3;(!e(this,k)||r)&&o(this,le,Ie).call(this,s);let a=s-e(this,st);if(a<0)return;if(a>=ye)throw new Error(`Current Matroska cluster exceeded its maximum allowed length of ${ye} milliseconds. In order to produce a correct WebM file, you must pass in a key frame at least every ${ye} milliseconds.`);let m=new Uint8Array(4),w=new DataView(m.buffer);if(w.setUint8(0,128|t.trackNumber),w.setInt16(1,a,!1),t.duration===void 0&&!t.additions){w.setUint8(3,Number(t.type==="key")<<7);let p={id:163,data:[m,t.data]};e(this,u).writeEBML(p)}else{let p=Math.floor(t.duration/1e3),ft={id:160,data:[{id:161,data:[m,t.data]},t.duration!==void 0?{id:155,data:p}:null,t.additions?{id:30113,data:t.additions}:null]};e(this,u).writeEBML(ft)}f(this,W,Math.max(e(this,W),s))},G=new WeakSet,Ut=function(t){return{id:25506,size:4,data:new Uint8Array(t)}},dt=new WeakSet,Jt=function(t,i){let s=e(this,u).pos;e(this,u).seek(e(this,u).offsets.get(t));let r=2+4+i.byteLength,a=At-r;if(a<0){let l=i.byteLength+a;i instanceof ArrayBuffer?i=i.slice(0,l):i=i.buffer.slice(0,l),a=0}t=[o(this,G,Ut).call(this,i),{id:236,size:4,data:new Uint8Array(a)}],e(this,u).writeEBML(t),e(this,u).seek(s)},le=new WeakSet,Ie=function(t){e(this,k)&&!e(this,d).streaming&&o(this,Pt,xe).call(this),f(this,k,{id:524531317,size:e(this,d).streaming?-1:We,data:[{id:231,data:t}]}),e(this,u).writeEBML(e(this,k)),f(this,st,t);let i=e(this,u).offsets.get(e(this,k))-e(this,R,tt);e(this,H).data.push({id:187,data:[{id:179,data:t},e(this,d).video?{id:183,data:[{id:247,data:L},{id:241,data:i}]}:null,e(this,d).audio?{id:183,data:[{id:247,data:St},{id:241,data:i}]}:null]})},Pt=new WeakSet,xe=function(){let t=e(this,u).pos-e(this,u).dataOffsets.get(e(this,k)),i=e(this,u).pos;e(this,u).seek(e(this,u).offsets.get(e(this,k))+4),e(this,u).writeEBMLVarInt(t,We),e(this,u).seek(i)},lt=new WeakSet,Lt=function(){if(e(this,zt))throw new Error("Cannot add new video or audio chunks after the file has been finalized.")};var vt=/(?:(.+?)\n)?((?:\d{2}:)?\d{2}:\d{2}.\d{3})\s+-->\s+((?:\d{2}:)?\d{2}:\d{2}.\d{3})/g,bi=/^WEBVTT.*?\n{2}/,pi=/(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})/,ti=/<(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})>/g,Se=new TextEncoder,Y,Rt,Mt,Ft,_t,ut,ue,fe,ei,Ae=class{constructor(t){n(this,ut);n(this,fe);n(this,Y,void 0);n(this,Rt,void 0);n(this,Mt,!1);n(this,Ft,void 0);n(this,_t,!1);f(this,Y,t)}configure(t){if(t.codec!=="webvtt")throw new Error("Codec must be 'webvtt'.");f(this,Rt,t)}encode(t){var s;if(!e(this,Rt))throw new Error("Encoder not configured.");t=t.replace(`\r +`,` +`).replace("\r",` +`),vt.lastIndex=0;let i;if(!e(this,Mt)){if(!bi.test(t)){let a=new Error("WebVTT preamble incorrect.");throw e(this,Y).error(a),a}i=vt.exec(t);let r=t.slice(0,(s=i==null?void 0:i.index)!=null?s:t.length).trimEnd();if(!r){let a=new Error("No WebVTT preamble provided.");throw e(this,Y).error(a),a}f(this,Ft,Se.encode(r)),f(this,Mt,!0),i&&(t=t.slice(i.index),vt.lastIndex=0)}for(;i=vt.exec(t);){let r=t.slice(0,i.index),a=i[1]||"",l=i.index+i[0].length,m=t.indexOf(` +`,l)+1,w=t.slice(l,m).trim(),p=t.indexOf(` + +`,l);p===-1&&(p=t.length);let ft=o(this,ut,ue).call(this,i[2]),ii=o(this,ut,ue).call(this,i[3])-ft,ce=t.slice(m,p),Ue=`${w} +${a} +${r}`;ti.lastIndex=0,ce=ce.replace(ti,ri=>{let ai=o(this,ut,ue).call(this,ri.slice(1,-1))-ft;return`<${o(this,fe,ei).call(this,ai)}>`}),t=t.slice(p).trimStart(),vt.lastIndex=0;let si={body:Se.encode(ce),additions:Ue===` + +`?void 0:Se.encode(Ue),timestamp:ft*1e3,duration:ii*1e3},Ee={};e(this,_t)||(Ee.decoderConfig={description:e(this,Ft)},f(this,_t,!0)),e(this,Y).output(si,Ee)}}};Y=new WeakMap,Rt=new WeakMap,Mt=new WeakMap,Ft=new WeakMap,_t=new WeakMap,ut=new WeakSet,ue=function(t){let i=pi.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])},fe=new WeakSet,ei=function(t){let i=Math.floor(t/36e5),s=Math.floor(t%(60*60*1e3)/(60*1e3)),r=Math.floor(t%(60*1e3)/1e3),a=t%1e3;return i.toString().padStart(2,"0")+":"+s.toString().padStart(2,"0")+":"+r.toString().padStart(2,"0")+"."+a.toString().padStart(3,"0")};export{Nt as ArrayBufferTarget,Ot as FileSystemWritableFileStreamTarget,Ce as Muxer,q as StreamTarget,Ae as SubtitleEncoder}; diff --git a/build/webm-muxer.mjs b/build/webm-muxer.mjs index 4b74968..fef36d7 100644 --- a/build/webm-muxer.mjs +++ b/build/webm-muxer.mjs @@ -516,15 +516,17 @@ var FileSystemWritableFileStreamTargetWriter = class extends ChunkedStreamTarget // src/muxer.ts var VIDEO_TRACK_NUMBER = 1; var AUDIO_TRACK_NUMBER = 2; +var SUBTITLE_TRACK_NUMBER = 3; var VIDEO_TRACK_TYPE = 1; var AUDIO_TRACK_TYPE = 2; +var SUBTITLE_TRACK_TYPE = 17; var MAX_CHUNK_LENGTH_MS = __pow(2, 15); var CODEC_PRIVATE_MAX_SIZE = __pow(2, 12); var APP_NAME = "https://github.com/Vanilagy/webm-muxer"; var SEGMENT_SIZE_BYTES = 6; var CLUSTER_SIZE_BYTES = 5; var FIRST_TIMESTAMP_BEHAVIORS = ["strict", "offset", "permissive"]; -var _options, _writer, _segment, _segmentInfo, _seekHead, _tracksElement, _segmentDuration, _colourElement, _videoCodecPrivate, _audioCodecPrivate, _cues, _currentCluster, _currentClusterTimestamp, _duration, _videoChunkQueue, _audioChunkQueue, _firstVideoTimestamp, _firstAudioTimestamp, _lastVideoTimestamp, _lastAudioTimestamp, _colorSpace, _finalized, _validateOptions, validateOptions_fn, _createFileHeader, createFileHeader_fn, _writeEBMLHeader, writeEBMLHeader_fn, _createCodecPrivatePlaceholders, createCodecPrivatePlaceholders_fn, _createColourElement, createColourElement_fn, _createSeekHead, createSeekHead_fn, _createSegmentInfo, createSegmentInfo_fn, _createTracks, createTracks_fn, _createSegment, createSegment_fn, _createCues, createCues_fn, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn, _segmentDataOffset, segmentDataOffset_get, _writeVideoDecoderConfig, writeVideoDecoderConfig_fn, _fixVP9ColorSpace, fixVP9ColorSpace_fn, _createInternalChunk, createInternalChunk_fn, _validateTimestamp, validateTimestamp_fn, _writeSimpleBlock, writeSimpleBlock_fn, _createCodecPrivateElement, createCodecPrivateElement_fn, _writeCodecPrivate, writeCodecPrivate_fn, _createNewCluster, createNewCluster_fn, _finalizeCurrentCluster, finalizeCurrentCluster_fn, _ensureNotFinalized, ensureNotFinalized_fn; +var _options, _writer, _segment, _segmentInfo, _seekHead, _tracksElement, _segmentDuration, _colourElement, _videoCodecPrivate, _audioCodecPrivate, _subtitleCodecPrivate, _cues, _currentCluster, _currentClusterTimestamp, _duration, _videoChunkQueue, _audioChunkQueue, _subtitleChunkQueue, _firstVideoTimestamp, _firstAudioTimestamp, _lastVideoTimestamp, _lastAudioTimestamp, _lastSubtitleTimestamp, _colorSpace, _finalized, _validateOptions, validateOptions_fn, _createFileHeader, createFileHeader_fn, _writeEBMLHeader, writeEBMLHeader_fn, _createCodecPrivatePlaceholders, createCodecPrivatePlaceholders_fn, _createColourElement, createColourElement_fn, _createSeekHead, createSeekHead_fn, _createSegmentInfo, createSegmentInfo_fn, _createTracks, createTracks_fn, _createSegment, createSegment_fn, _createCues, createCues_fn, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn, _segmentDataOffset, segmentDataOffset_get, _writeVideoDecoderConfig, writeVideoDecoderConfig_fn, _fixVP9ColorSpace, fixVP9ColorSpace_fn, _writeSubtitleChunks, writeSubtitleChunks_fn, _createInternalChunk, createInternalChunk_fn, _validateTimestamp, validateTimestamp_fn, _writeBlock, writeBlock_fn, _createCodecPrivateElement, createCodecPrivateElement_fn, _writeCodecPrivate, writeCodecPrivate_fn, _createNewCluster, createNewCluster_fn, _finalizeCurrentCluster, finalizeCurrentCluster_fn, _ensureNotFinalized, ensureNotFinalized_fn; var Muxer = class { constructor(options) { __privateAdd(this, _validateOptions); @@ -541,9 +543,10 @@ var Muxer = class { __privateAdd(this, _segmentDataOffset); __privateAdd(this, _writeVideoDecoderConfig); __privateAdd(this, _fixVP9ColorSpace); + __privateAdd(this, _writeSubtitleChunks); __privateAdd(this, _createInternalChunk); __privateAdd(this, _validateTimestamp); - __privateAdd(this, _writeSimpleBlock); + __privateAdd(this, _writeBlock); __privateAdd(this, _createCodecPrivateElement); __privateAdd(this, _writeCodecPrivate); __privateAdd(this, _createNewCluster); @@ -559,16 +562,19 @@ var Muxer = class { __privateAdd(this, _colourElement, void 0); __privateAdd(this, _videoCodecPrivate, void 0); __privateAdd(this, _audioCodecPrivate, void 0); + __privateAdd(this, _subtitleCodecPrivate, void 0); __privateAdd(this, _cues, void 0); __privateAdd(this, _currentCluster, void 0); __privateAdd(this, _currentClusterTimestamp, void 0); __privateAdd(this, _duration, 0); __privateAdd(this, _videoChunkQueue, []); __privateAdd(this, _audioChunkQueue, []); + __privateAdd(this, _subtitleChunkQueue, []); __privateAdd(this, _firstVideoTimestamp, void 0); __privateAdd(this, _firstAudioTimestamp, void 0); __privateAdd(this, _lastVideoTimestamp, -1); __privateAdd(this, _lastAudioTimestamp, -1); + __privateAdd(this, _lastSubtitleTimestamp, -1); __privateAdd(this, _colorSpace, void 0); __privateAdd(this, _finalized, false); var _a; @@ -603,19 +609,20 @@ var Muxer = class { __privateSet(this, _firstVideoTimestamp, timestamp); if (meta) __privateMethod(this, _writeVideoDecoderConfig, writeVideoDecoderConfig_fn).call(this, meta); - let internalChunk = __privateMethod(this, _createInternalChunk, createInternalChunk_fn).call(this, data, type, timestamp, VIDEO_TRACK_NUMBER); + let videoChunk = __privateMethod(this, _createInternalChunk, createInternalChunk_fn).call(this, data, type, timestamp, VIDEO_TRACK_NUMBER); if (__privateGet(this, _options).video.codec === "V_VP9") - __privateMethod(this, _fixVP9ColorSpace, fixVP9ColorSpace_fn).call(this, internalChunk); - __privateSet(this, _lastVideoTimestamp, internalChunk.timestamp); - while (__privateGet(this, _audioChunkQueue).length > 0 && __privateGet(this, _audioChunkQueue)[0].timestamp <= internalChunk.timestamp) { + __privateMethod(this, _fixVP9ColorSpace, fixVP9ColorSpace_fn).call(this, videoChunk); + __privateSet(this, _lastVideoTimestamp, videoChunk.timestamp); + while (__privateGet(this, _audioChunkQueue).length > 0 && __privateGet(this, _audioChunkQueue)[0].timestamp <= videoChunk.timestamp) { let audioChunk = __privateGet(this, _audioChunkQueue).shift(); - __privateMethod(this, _writeSimpleBlock, writeSimpleBlock_fn).call(this, audioChunk); + __privateMethod(this, _writeBlock, writeBlock_fn).call(this, audioChunk, false); } - if (!__privateGet(this, _options).audio || internalChunk.timestamp <= __privateGet(this, _lastAudioTimestamp)) { - __privateMethod(this, _writeSimpleBlock, writeSimpleBlock_fn).call(this, internalChunk); + if (!__privateGet(this, _options).audio || videoChunk.timestamp <= __privateGet(this, _lastAudioTimestamp)) { + __privateMethod(this, _writeBlock, writeBlock_fn).call(this, videoChunk, true); } else { - __privateGet(this, _videoChunkQueue).push(internalChunk); + __privateGet(this, _videoChunkQueue).push(videoChunk); } + __privateMethod(this, _writeSubtitleChunks, writeSubtitleChunks_fn).call(this); __privateMethod(this, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn).call(this); } addAudioChunk(chunk, meta, timestamp) { @@ -636,24 +643,45 @@ var Muxer = class { __privateMethod(this, _writeCodecPrivate, writeCodecPrivate_fn).call(this, __privateGet(this, _audioCodecPrivate), meta.decoderConfig.description); } } - let internalChunk = __privateMethod(this, _createInternalChunk, createInternalChunk_fn).call(this, data, type, timestamp, AUDIO_TRACK_NUMBER); - __privateSet(this, _lastAudioTimestamp, internalChunk.timestamp); - while (__privateGet(this, _videoChunkQueue).length > 0 && __privateGet(this, _videoChunkQueue)[0].timestamp <= internalChunk.timestamp) { + let audioChunk = __privateMethod(this, _createInternalChunk, createInternalChunk_fn).call(this, data, type, timestamp, AUDIO_TRACK_NUMBER); + __privateSet(this, _lastAudioTimestamp, audioChunk.timestamp); + while (__privateGet(this, _videoChunkQueue).length > 0 && __privateGet(this, _videoChunkQueue)[0].timestamp <= audioChunk.timestamp) { let videoChunk = __privateGet(this, _videoChunkQueue).shift(); - __privateMethod(this, _writeSimpleBlock, writeSimpleBlock_fn).call(this, videoChunk); + __privateMethod(this, _writeBlock, writeBlock_fn).call(this, videoChunk, true); } - if (!__privateGet(this, _options).video || internalChunk.timestamp <= __privateGet(this, _lastVideoTimestamp)) { - __privateMethod(this, _writeSimpleBlock, writeSimpleBlock_fn).call(this, internalChunk); + if (!__privateGet(this, _options).video || audioChunk.timestamp <= __privateGet(this, _lastVideoTimestamp)) { + __privateMethod(this, _writeBlock, writeBlock_fn).call(this, audioChunk, !__privateGet(this, _options).video); } else { - __privateGet(this, _audioChunkQueue).push(internalChunk); + __privateGet(this, _audioChunkQueue).push(audioChunk); } + __privateMethod(this, _writeSubtitleChunks, writeSubtitleChunks_fn).call(this); + __privateMethod(this, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn).call(this); + } + addSubtitleChunk(chunk, meta) { + __privateMethod(this, _ensureNotFinalized, ensureNotFinalized_fn).call(this); + if (!__privateGet(this, _options).subtitles) + throw new Error("No subtitle track declared."); + if (meta == null ? void 0 : meta.decoderConfig) { + if (__privateGet(this, _options).streaming) { + __privateSet(this, _subtitleCodecPrivate, __privateMethod(this, _createCodecPrivateElement, createCodecPrivateElement_fn).call(this, meta.decoderConfig.description)); + } else { + __privateMethod(this, _writeCodecPrivate, writeCodecPrivate_fn).call(this, __privateGet(this, _subtitleCodecPrivate), meta.decoderConfig.description); + } + } + let subtitleChunk = __privateMethod(this, _createInternalChunk, createInternalChunk_fn).call(this, chunk.body, "key", chunk.timestamp, SUBTITLE_TRACK_NUMBER, chunk.duration, chunk.additions); + __privateSet(this, _lastSubtitleTimestamp, subtitleChunk.timestamp); + __privateGet(this, _subtitleChunkQueue).push(subtitleChunk); + __privateMethod(this, _writeSubtitleChunks, writeSubtitleChunks_fn).call(this); __privateMethod(this, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn).call(this); } finalize() { while (__privateGet(this, _videoChunkQueue).length > 0) - __privateMethod(this, _writeSimpleBlock, writeSimpleBlock_fn).call(this, __privateGet(this, _videoChunkQueue).shift()); + __privateMethod(this, _writeBlock, writeBlock_fn).call(this, __privateGet(this, _videoChunkQueue).shift(), true); while (__privateGet(this, _audioChunkQueue).length > 0) - __privateMethod(this, _writeSimpleBlock, writeSimpleBlock_fn).call(this, __privateGet(this, _audioChunkQueue).shift()); + __privateMethod(this, _writeBlock, writeBlock_fn).call(this, __privateGet(this, _audioChunkQueue).shift(), true); + while (__privateGet(this, _subtitleChunkQueue).length > 0 && __privateGet(this, _subtitleChunkQueue)[0].timestamp <= __privateGet(this, _duration)) { + __privateMethod(this, _writeBlock, writeBlock_fn).call(this, __privateGet(this, _subtitleChunkQueue).shift(), false); + } if (!__privateGet(this, _options).streaming) { __privateMethod(this, _finalizeCurrentCluster, finalizeCurrentCluster_fn).call(this); } @@ -688,16 +716,19 @@ _segmentDuration = new WeakMap(); _colourElement = new WeakMap(); _videoCodecPrivate = new WeakMap(); _audioCodecPrivate = new WeakMap(); +_subtitleCodecPrivate = new WeakMap(); _cues = new WeakMap(); _currentCluster = new WeakMap(); _currentClusterTimestamp = new WeakMap(); _duration = new WeakMap(); _videoChunkQueue = new WeakMap(); _audioChunkQueue = new WeakMap(); +_subtitleChunkQueue = new WeakMap(); _firstVideoTimestamp = new WeakMap(); _firstAudioTimestamp = new WeakMap(); _lastVideoTimestamp = new WeakMap(); _lastAudioTimestamp = new WeakMap(); +_lastSubtitleTimestamp = new WeakMap(); _colorSpace = new WeakMap(); _finalized = new WeakMap(); _validateOptions = new WeakSet(); @@ -744,6 +775,7 @@ _createCodecPrivatePlaceholders = new WeakSet(); createCodecPrivatePlaceholders_fn = function() { __privateSet(this, _videoCodecPrivate, { id: 236 /* Void */, size: 4, data: new Uint8Array(CODEC_PRIVATE_MAX_SIZE) }); __privateSet(this, _audioCodecPrivate, { id: 236 /* Void */, size: 4, data: new Uint8Array(CODEC_PRIVATE_MAX_SIZE) }); + __privateSet(this, _subtitleCodecPrivate, { id: 236 /* Void */, size: 4, data: new Uint8Array(CODEC_PRIVATE_MAX_SIZE) }); }; _createColourElement = new WeakSet(); createColourElement_fn = function() { @@ -822,6 +854,15 @@ createTracks_fn = function() { ] } ] }); } + if (__privateGet(this, _options).subtitles) { + tracksElement.data.push({ id: 174 /* TrackEntry */, data: [ + { id: 215 /* TrackNumber */, data: SUBTITLE_TRACK_NUMBER }, + { id: 29637 /* TrackUID */, data: SUBTITLE_TRACK_NUMBER }, + { id: 131 /* TrackType */, data: SUBTITLE_TRACK_TYPE }, + { id: 134 /* CodecID */, data: __privateGet(this, _options).subtitles.codec }, + __privateGet(this, _subtitleCodecPrivate) + ] }); + } }; _createSegment = new WeakSet(); createSegment_fn = function() { @@ -929,67 +970,102 @@ fixVP9ColorSpace_fn = function(chunk) { }[__privateGet(this, _colorSpace).matrix]; writeBits(chunk.data, i + 0, i + 3, colorSpaceID); }; +_writeSubtitleChunks = new WeakSet(); +writeSubtitleChunks_fn = function() { + let lastWrittenMediaTimestamp = Math.min( + __privateGet(this, _options).video ? __privateGet(this, _lastVideoTimestamp) : Infinity, + __privateGet(this, _options).audio ? __privateGet(this, _lastAudioTimestamp) : Infinity + ); + let queue = __privateGet(this, _subtitleChunkQueue); + while (queue.length > 0 && queue[0].timestamp <= lastWrittenMediaTimestamp) { + __privateMethod(this, _writeBlock, writeBlock_fn).call(this, queue.shift(), !__privateGet(this, _options).video && !__privateGet(this, _options).audio); + } +}; _createInternalChunk = new WeakSet(); -createInternalChunk_fn = function(data, type, timestamp, trackNumber) { +createInternalChunk_fn = function(data, type, timestamp, trackNumber, duration, additions) { let adjustedTimestamp = __privateMethod(this, _validateTimestamp, validateTimestamp_fn).call(this, timestamp, trackNumber); let internalChunk = { data, + additions, type, timestamp: adjustedTimestamp, + duration, trackNumber }; return internalChunk; }; _validateTimestamp = new WeakSet(); validateTimestamp_fn = function(timestamp, trackNumber) { - let firstTimestamp = trackNumber === VIDEO_TRACK_NUMBER ? __privateGet(this, _firstVideoTimestamp) : __privateGet(this, _firstAudioTimestamp); - let lastTimestamp = trackNumber === VIDEO_TRACK_NUMBER ? __privateGet(this, _lastVideoTimestamp) : __privateGet(this, _lastAudioTimestamp); - if (__privateGet(this, _options).firstTimestampBehavior === "strict" && lastTimestamp === -1 && timestamp !== 0) { - throw new Error( - `The first chunk for your media track must have a timestamp of 0 (received ${timestamp}). Non-zero first timestamps are often caused by directly piping frames or audio data from a MediaStreamTrack into the encoder. Their timestamps are typically relative to the age of the document, which is probably what you want. + let lastTimestamp = trackNumber === VIDEO_TRACK_NUMBER ? __privateGet(this, _lastVideoTimestamp) : trackNumber === AUDIO_TRACK_NUMBER ? __privateGet(this, _lastAudioTimestamp) : __privateGet(this, _lastSubtitleTimestamp); + if (trackNumber !== SUBTITLE_TRACK_NUMBER) { + let firstTimestamp = trackNumber === VIDEO_TRACK_NUMBER ? __privateGet(this, _firstVideoTimestamp) : __privateGet(this, _firstAudioTimestamp); + if (__privateGet(this, _options).firstTimestampBehavior === "strict" && lastTimestamp === -1 && timestamp !== 0) { + throw new Error( + `The first chunk for your media track must have a timestamp of 0 (received ${timestamp}). Non-zero first timestamps are often caused by directly piping frames or audio data from a MediaStreamTrack into the encoder. Their timestamps are typically relative to the age of the document, which is probably what you want. If you want to offset all timestamps of a track such that the first one is zero, set firstTimestampBehavior: 'offset' in the options. If you want to allow non-zero first timestamps, set firstTimestampBehavior: 'permissive'. ` - ); - } else if (__privateGet(this, _options).firstTimestampBehavior === "offset") { - timestamp -= firstTimestamp; + ); + } else if (__privateGet(this, _options).firstTimestampBehavior === "offset") { + timestamp -= firstTimestamp; + } } if (timestamp < lastTimestamp) { throw new Error( `Timestamps must be monotonically increasing (went from ${lastTimestamp} to ${timestamp}).` ); } + if (timestamp < 0) { + throw new Error(`Timestamps must be non-negative (received ${timestamp}).`); + } return timestamp; }; -_writeSimpleBlock = new WeakSet(); -writeSimpleBlock_fn = function(chunk) { +_writeBlock = new WeakSet(); +writeBlock_fn = function(chunk, canCreateNewCluster) { if (__privateGet(this, _options).streaming && !__privateGet(this, _tracksElement)) { __privateMethod(this, _createTracks, createTracks_fn).call(this); __privateMethod(this, _createSegment, createSegment_fn).call(this); } - let msTime = Math.floor(chunk.timestamp / 1e3); - let clusterIsTooLong = chunk.type !== "key" && msTime - __privateGet(this, _currentClusterTimestamp) >= MAX_CHUNK_LENGTH_MS; + let msTimestamp = Math.floor(chunk.timestamp / 1e3); + let shouldCreateNewClusterFromKeyFrame = canCreateNewCluster && chunk.type === "key" && msTimestamp - __privateGet(this, _currentClusterTimestamp) >= 1e3; + if (!__privateGet(this, _currentCluster) || shouldCreateNewClusterFromKeyFrame) { + __privateMethod(this, _createNewCluster, createNewCluster_fn).call(this, msTimestamp); + } + let relativeTimestamp = msTimestamp - __privateGet(this, _currentClusterTimestamp); + if (relativeTimestamp < 0) { + return; + } + let clusterIsTooLong = relativeTimestamp >= MAX_CHUNK_LENGTH_MS; if (clusterIsTooLong) { throw new Error( - `Current Matroska cluster exceeded its maximum allowed length of ${MAX_CHUNK_LENGTH_MS} milliseconds. In order to produce a correct WebM file, you must pass in a video key frame at least every ${MAX_CHUNK_LENGTH_MS} milliseconds.` + `Current Matroska cluster exceeded its maximum allowed length of ${MAX_CHUNK_LENGTH_MS} milliseconds. In order to produce a correct WebM file, you must pass in a key frame at least every ${MAX_CHUNK_LENGTH_MS} milliseconds.` ); } - let shouldCreateNewClusterFromKeyFrame = (chunk.trackNumber === VIDEO_TRACK_NUMBER || !__privateGet(this, _options).video) && chunk.type === "key" && msTime - __privateGet(this, _currentClusterTimestamp) >= 1e3; - if (!__privateGet(this, _currentCluster) || shouldCreateNewClusterFromKeyFrame) { - __privateMethod(this, _createNewCluster, createNewCluster_fn).call(this, msTime); - } let prelude = new Uint8Array(4); let view = new DataView(prelude.buffer); view.setUint8(0, 128 | chunk.trackNumber); - view.setUint16(1, msTime - __privateGet(this, _currentClusterTimestamp), false); - view.setUint8(3, Number(chunk.type === "key") << 7); - let simpleBlock = { id: 163 /* SimpleBlock */, data: [ - prelude, - chunk.data - ] }; - __privateGet(this, _writer).writeEBML(simpleBlock); - __privateSet(this, _duration, Math.max(__privateGet(this, _duration), msTime)); + view.setInt16(1, relativeTimestamp, false); + if (chunk.duration === void 0 && !chunk.additions) { + view.setUint8(3, Number(chunk.type === "key") << 7); + let simpleBlock = { id: 163 /* SimpleBlock */, data: [ + prelude, + chunk.data + ] }; + __privateGet(this, _writer).writeEBML(simpleBlock); + } else { + let msDuration = Math.floor(chunk.duration / 1e3); + let blockGroup = { id: 160 /* BlockGroup */, data: [ + { id: 161 /* Block */, data: [ + prelude, + chunk.data + ] }, + chunk.duration !== void 0 ? { id: 155 /* BlockDuration */, data: msDuration } : null, + chunk.additions ? { id: 30113 /* BlockAdditions */, data: chunk.additions } : null + ] }; + __privateGet(this, _writer).writeEBML(blockGroup); + } + __privateSet(this, _duration, Math.max(__privateGet(this, _duration), msTimestamp)); }; _createCodecPrivateElement = new WeakSet(); createCodecPrivateElement_fn = function(data) { @@ -999,9 +1075,20 @@ _writeCodecPrivate = new WeakSet(); writeCodecPrivate_fn = function(element, data) { let endPos = __privateGet(this, _writer).pos; __privateGet(this, _writer).seek(__privateGet(this, _writer).offsets.get(element)); + let codecPrivateElementSize = 2 + 4 + data.byteLength; + let voidDataSize = CODEC_PRIVATE_MAX_SIZE - codecPrivateElementSize; + if (voidDataSize < 0) { + let newByteLength = data.byteLength + voidDataSize; + if (data instanceof ArrayBuffer) { + data = data.slice(0, newByteLength); + } else { + data = data.buffer.slice(0, newByteLength); + } + voidDataSize = 0; + } element = [ __privateMethod(this, _createCodecPrivateElement, createCodecPrivateElement_fn).call(this, data), - { id: 236 /* Void */, size: 4, data: new Uint8Array(CODEC_PRIVATE_MAX_SIZE - 2 - 4 - data.byteLength) } + { id: 236 /* Void */, size: 4, data: new Uint8Array(voidDataSize) } ]; __privateGet(this, _writer).writeEBML(element); __privateGet(this, _writer).seek(endPos); @@ -1047,9 +1134,124 @@ ensureNotFinalized_fn = function() { throw new Error("Cannot add new video or audio chunks after the file has been finalized."); } }; + +// src/subtitles.ts +var cueBlockHeaderRegex = /(?:(.+?)\n)?((?:\d{2}:)?\d{2}:\d{2}.\d{3})\s+-->\s+((?:\d{2}:)?\d{2}:\d{2}.\d{3})/g; +var preambleStartRegex = /^WEBVTT.*?\n{2}/; +var timestampRegex = /(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})/; +var inlineTimestampRegex = /<(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})>/g; +var textEncoder = new TextEncoder(); +var _options2, _config, _preambleSeen, _preambleBytes, _preambleEmitted, _parseTimestamp, parseTimestamp_fn, _formatTimestamp, formatTimestamp_fn; +var SubtitleEncoder = class { + constructor(options) { + __privateAdd(this, _parseTimestamp); + __privateAdd(this, _formatTimestamp); + __privateAdd(this, _options2, void 0); + __privateAdd(this, _config, void 0); + __privateAdd(this, _preambleSeen, false); + __privateAdd(this, _preambleBytes, void 0); + __privateAdd(this, _preambleEmitted, false); + __privateSet(this, _options2, options); + } + configure(config) { + if (config.codec !== "webvtt") { + throw new Error("Codec must be 'webvtt'."); + } + __privateSet(this, _config, config); + } + encode(text) { + var _a; + if (!__privateGet(this, _config)) { + throw new Error("Encoder not configured."); + } + text = text.replace("\r\n", "\n").replace("\r", "\n"); + cueBlockHeaderRegex.lastIndex = 0; + let match; + if (!__privateGet(this, _preambleSeen)) { + if (!preambleStartRegex.test(text)) { + let error = new Error("WebVTT preamble incorrect."); + __privateGet(this, _options2).error(error); + throw error; + } + match = cueBlockHeaderRegex.exec(text); + let preamble = text.slice(0, (_a = match == null ? void 0 : match.index) != null ? _a : text.length).trimEnd(); + if (!preamble) { + let error = new Error("No WebVTT preamble provided."); + __privateGet(this, _options2).error(error); + throw error; + } + __privateSet(this, _preambleBytes, textEncoder.encode(preamble)); + __privateSet(this, _preambleSeen, true); + if (match) { + text = text.slice(match.index); + cueBlockHeaderRegex.lastIndex = 0; + } + } + while (match = cueBlockHeaderRegex.exec(text)) { + let notes = text.slice(0, match.index); + let cueIdentifier = match[1] || ""; + let matchEnd = match.index + match[0].length; + let bodyStart = text.indexOf("\n", matchEnd) + 1; + let cueSettings = text.slice(matchEnd, bodyStart).trim(); + let bodyEnd = text.indexOf("\n\n", matchEnd); + if (bodyEnd === -1) + bodyEnd = text.length; + let startTime = __privateMethod(this, _parseTimestamp, parseTimestamp_fn).call(this, match[2]); + let endTime = __privateMethod(this, _parseTimestamp, parseTimestamp_fn).call(this, match[3]); + let duration = endTime - startTime; + let body = text.slice(bodyStart, bodyEnd); + let additions = `${cueSettings} +${cueIdentifier} +${notes}`; + inlineTimestampRegex.lastIndex = 0; + body = body.replace(inlineTimestampRegex, (match2) => { + let time = __privateMethod(this, _parseTimestamp, parseTimestamp_fn).call(this, match2.slice(1, -1)); + let offsetTime = time - startTime; + return `<${__privateMethod(this, _formatTimestamp, formatTimestamp_fn).call(this, offsetTime)}>`; + }); + text = text.slice(bodyEnd).trimStart(); + cueBlockHeaderRegex.lastIndex = 0; + let chunk = { + body: textEncoder.encode(body), + additions: additions === "\n\n" ? void 0 : textEncoder.encode(additions), + timestamp: startTime * 1e3, + duration: duration * 1e3 + }; + let meta = {}; + if (!__privateGet(this, _preambleEmitted)) { + meta.decoderConfig = { + description: __privateGet(this, _preambleBytes) + }; + __privateSet(this, _preambleEmitted, true); + } + __privateGet(this, _options2).output(chunk, meta); + } + } +}; +_options2 = new WeakMap(); +_config = new WeakMap(); +_preambleSeen = new WeakMap(); +_preambleBytes = new WeakMap(); +_preambleEmitted = new WeakMap(); +_parseTimestamp = new WeakSet(); +parseTimestamp_fn = function(string) { + let match = timestampRegex.exec(string); + if (!match) + throw new Error("Expected match."); + return 60 * 60 * 1e3 * Number(match[1] || "0") + 60 * 1e3 * Number(match[2]) + 1e3 * Number(match[3]) + Number(match[4]); +}; +_formatTimestamp = new WeakSet(); +formatTimestamp_fn = function(timestamp) { + let hours = Math.floor(timestamp / (60 * 60 * 1e3)); + let minutes = Math.floor(timestamp % (60 * 60 * 1e3) / (60 * 1e3)); + let seconds = Math.floor(timestamp % (60 * 1e3) / 1e3); + let milliseconds = timestamp % 1e3; + return hours.toString().padStart(2, "0") + ":" + minutes.toString().padStart(2, "0") + ":" + seconds.toString().padStart(2, "0") + "." + milliseconds.toString().padStart(3, "0"); +}; export { ArrayBufferTarget, FileSystemWritableFileStreamTarget, Muxer, - StreamTarget + StreamTarget, + SubtitleEncoder }; diff --git a/src/ebml.ts b/src/ebml.ts index 558a798..4502dee 100644 --- a/src/ebml.ts +++ b/src/ebml.ts @@ -61,6 +61,10 @@ export enum EBMLId { BitDepth = 0x6264, Segment = 0x18538067, SimpleBlock = 0xa3, + BlockGroup = 0xa0, + Block = 0xa1, + BlockAdditions = 0x75a1, + BlockDuration = 0x9b, Cluster = 0x1f43b675, Timestamp = 0xe7, Cues = 0x1c53bb6b, diff --git a/src/index.ts b/src/index.ts index 0c1811b..2caf1cd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,3 @@ export { Muxer } from './muxer'; +export { SubtitleEncoder } from './subtitles'; export * from './target'; \ No newline at end of file diff --git a/src/muxer.ts b/src/muxer.ts index eb4c94a..fb70047 100644 --- a/src/muxer.ts +++ b/src/muxer.ts @@ -1,6 +1,7 @@ import { EBML, EBMLElement, EBMLFloat32, EBMLFloat64, EBMLId } from './ebml'; import { readBits, writeBits } from './misc'; import { ArrayBufferTarget, FileSystemWritableFileStreamTarget, StreamTarget, Target } from './target'; +import { EncodedSubtitleChunk, EncodedSubtitleChunkMetadata } from './subtitles'; import { ArrayBufferTargetWriter, ChunkedStreamTargetWriter, @@ -11,8 +12,10 @@ import { const VIDEO_TRACK_NUMBER = 1; const AUDIO_TRACK_NUMBER = 2; +const SUBTITLE_TRACK_NUMBER = 3; const VIDEO_TRACK_TYPE = 1; const AUDIO_TRACK_TYPE = 2; +const SUBTITLE_TRACK_TYPE = 17; const MAX_CHUNK_LENGTH_MS = 2**15; const CODEC_PRIVATE_MAX_SIZE = 2**12; const APP_NAME = 'https://github.com/Vanilagy/webm-muxer'; @@ -35,6 +38,9 @@ interface MuxerOptions { sampleRate: number, bitDepth?: number }, + subtitles?: { + codec: string + }, type?: 'webm' | 'matroska', firstTimestampBehavior?: typeof FIRST_TIMESTAMP_BEHAVIORS[number], streaming?: boolean @@ -42,7 +48,9 @@ interface MuxerOptions { interface InternalMediaChunk { data: Uint8Array, + additions?: Uint8Array, timestamp: number, + duration?: number, type: 'key' | 'delta', trackNumber: number } @@ -77,6 +85,7 @@ export class Muxer { #colourElement: EBMLElement; #videoCodecPrivate: EBML; #audioCodecPrivate: EBML; + #subtitleCodecPrivate: EBML; #cues: EBMLElement; #currentCluster: EBMLElement; @@ -85,10 +94,12 @@ export class Muxer { #duration = 0; #videoChunkQueue: InternalMediaChunk[] = []; #audioChunkQueue: InternalMediaChunk[] = []; + #subtitleChunkQueue: InternalMediaChunk[] = []; #firstVideoTimestamp: number; #firstAudioTimestamp: number; #lastVideoTimestamp = -1; #lastAudioTimestamp = -1; + #lastSubtitleTimestamp = -1; #colorSpace: VideoColorSpaceInit; #finalized = false; @@ -169,6 +180,7 @@ export class Muxer { #createCodecPrivatePlaceholders() { this.#videoCodecPrivate = { id: EBMLId.Void, size: 4, data: new Uint8Array(CODEC_PRIVATE_MAX_SIZE) }; this.#audioCodecPrivate = { id: EBMLId.Void, size: 4, data: new Uint8Array(CODEC_PRIVATE_MAX_SIZE) }; + this.#subtitleCodecPrivate = { id: EBMLId.Void, size: 4, data: new Uint8Array(CODEC_PRIVATE_MAX_SIZE) }; } #createColourElement() { @@ -264,6 +276,16 @@ export class Muxer { ] } ] }); } + + if (this.#options.subtitles) { + tracksElement.data.push({ id: EBMLId.TrackEntry, data: [ + { id: EBMLId.TrackNumber, data: SUBTITLE_TRACK_NUMBER }, + { id: EBMLId.TrackUID, data: SUBTITLE_TRACK_NUMBER }, + { id: EBMLId.TrackType, data: SUBTITLE_TRACK_TYPE }, + { id: EBMLId.CodecID, data: this.#options.subtitles.codec }, + this.#subtitleCodecPrivate + ] }); + } } #createSegment() { @@ -309,11 +331,11 @@ export class Muxer { if (this.#firstVideoTimestamp === undefined) this.#firstVideoTimestamp = timestamp; if (meta) this.#writeVideoDecoderConfig(meta); - let internalChunk = this.#createInternalChunk(data, type, timestamp, VIDEO_TRACK_NUMBER); - if (this.#options.video.codec === 'V_VP9') this.#fixVP9ColorSpace(internalChunk); + let videoChunk = this.#createInternalChunk(data, type, timestamp, VIDEO_TRACK_NUMBER); + if (this.#options.video.codec === 'V_VP9') this.#fixVP9ColorSpace(videoChunk); /** - * Ok, so the algorithm used to insert video and audio blocks (if both are present) is one where we want to + * Okay, so the algorithm used to insert video and audio blocks (if both are present) is one where we want to * insert the blocks sorted, i.e. always monotonically increasing in timestamp. This means that we can write * an audio chunk of timestamp t_a only when we have a video chunk of timestamp t_v >= t_a, and vice versa. * This means that we need to often queue up a lot of video/audio chunks and wait for their counterpart to @@ -321,21 +343,22 @@ export class Muxer { * chunks remaining in the queues also be flushed to the file. */ - this.#lastVideoTimestamp = internalChunk.timestamp; + this.#lastVideoTimestamp = videoChunk.timestamp; // Write all audio chunks with a timestamp smaller than the incoming video chunk - while (this.#audioChunkQueue.length > 0 && this.#audioChunkQueue[0].timestamp <= internalChunk.timestamp) { + while (this.#audioChunkQueue.length > 0 && this.#audioChunkQueue[0].timestamp <= videoChunk.timestamp) { let audioChunk = this.#audioChunkQueue.shift(); - this.#writeSimpleBlock(audioChunk); + this.#writeBlock(audioChunk, false); } // Depending on the last audio chunk, either write the video chunk to the file or enqueue it - if (!this.#options.audio || internalChunk.timestamp <= this.#lastAudioTimestamp) { - this.#writeSimpleBlock(internalChunk); + if (!this.#options.audio || videoChunk.timestamp <= this.#lastAudioTimestamp) { + this.#writeBlock(videoChunk, true); } else { - this.#videoChunkQueue.push(internalChunk); + this.#videoChunkQueue.push(videoChunk); } + this.#writeSubtitleChunks(); this.#maybeFlushStreamingTargetWriter(); } @@ -442,33 +465,89 @@ export class Muxer { } } - let internalChunk = this.#createInternalChunk(data, type, timestamp, AUDIO_TRACK_NUMBER); + let audioChunk = this.#createInternalChunk(data, type, timestamp, AUDIO_TRACK_NUMBER); // Algorithm explained in `addVideoChunkRaw` - this.#lastAudioTimestamp = internalChunk.timestamp; + this.#lastAudioTimestamp = audioChunk.timestamp; - while (this.#videoChunkQueue.length > 0 && this.#videoChunkQueue[0].timestamp <= internalChunk.timestamp) { + while (this.#videoChunkQueue.length > 0 && this.#videoChunkQueue[0].timestamp <= audioChunk.timestamp) { let videoChunk = this.#videoChunkQueue.shift(); - this.#writeSimpleBlock(videoChunk); + this.#writeBlock(videoChunk, true); } - if (!this.#options.video || internalChunk.timestamp <= this.#lastVideoTimestamp) { - this.#writeSimpleBlock(internalChunk); + if (!this.#options.video || audioChunk.timestamp <= this.#lastVideoTimestamp) { + this.#writeBlock(audioChunk, !this.#options.video); } else { - this.#audioChunkQueue.push(internalChunk); + this.#audioChunkQueue.push(audioChunk); } + this.#writeSubtitleChunks(); this.#maybeFlushStreamingTargetWriter(); } + addSubtitleChunk(chunk: EncodedSubtitleChunk, meta: EncodedSubtitleChunkMetadata) { + this.#ensureNotFinalized(); + if (!this.#options.subtitles) throw new Error('No subtitle track declared.'); + + // Write possible subtitle decoder metadata to the file + if (meta?.decoderConfig) { + if (this.#options.streaming) { + this.#subtitleCodecPrivate = this.#createCodecPrivateElement(meta.decoderConfig.description); + } else { + this.#writeCodecPrivate(this.#subtitleCodecPrivate, meta.decoderConfig.description); + } + } + + let subtitleChunk = this.#createInternalChunk( + chunk.body, + 'key', + chunk.timestamp, + SUBTITLE_TRACK_NUMBER, + chunk.duration, + chunk.additions + ); + + this.#lastSubtitleTimestamp = subtitleChunk.timestamp; + this.#subtitleChunkQueue.push(subtitleChunk); + + this.#writeSubtitleChunks(); + this.#maybeFlushStreamingTargetWriter(); + } + + #writeSubtitleChunks() { + // Writing subtitle chunks is different from video and audio: A subtitle chunk will be written if it's + // guaranteed that no more media chunks will be written before it, to ensure monotonicity. However, media chunks + // will NOT wait for subtitle chunks to arrive, as they may never arrive, so that's how non-monotonicity can + // arrive. But it should be fine, since it's all still in one cluster. + + let lastWrittenMediaTimestamp = Math.min( + this.#options.video ? this.#lastVideoTimestamp : Infinity, + this.#options.audio ? this.#lastAudioTimestamp : Infinity + ); + + let queue = this.#subtitleChunkQueue; + while (queue.length > 0 && queue[0].timestamp <= lastWrittenMediaTimestamp) { + this.#writeBlock(queue.shift(), !this.#options.video && !this.#options.audio); + } + } + /** Converts a read-only external chunk into an internal one for easier use. */ - #createInternalChunk(data: Uint8Array, type: 'key' | 'delta', timestamp: number, trackNumber: number) { + #createInternalChunk( + data: Uint8Array, + type: 'key' | 'delta', + timestamp: number, + trackNumber: number, + duration?: number, + additions?: Uint8Array + ) { let adjustedTimestamp = this.#validateTimestamp(timestamp, trackNumber); let internalChunk: InternalMediaChunk = { data, + additions, type, timestamp: adjustedTimestamp, + duration, trackNumber }; @@ -476,21 +555,29 @@ export class Muxer { } #validateTimestamp(timestamp: number, trackNumber: number) { - let firstTimestamp = trackNumber === VIDEO_TRACK_NUMBER ? this.#firstVideoTimestamp : this.#firstAudioTimestamp; - let lastTimestamp = trackNumber === VIDEO_TRACK_NUMBER ? this.#lastVideoTimestamp : this.#lastAudioTimestamp; - - // Check first timestamp behavior - if (this.#options.firstTimestampBehavior === 'strict' && lastTimestamp === -1 && timestamp !== 0) { - throw new Error( - `The first chunk for your media track must have a timestamp of 0 (received ${timestamp}). Non-zero ` + - `first timestamps are often caused by directly piping frames or audio data from a MediaStreamTrack ` + - `into the encoder. Their timestamps are typically relative to the age of the document, which is ` + - `probably what you want.\n\nIf you want to offset all timestamps of a track such that the first one ` + - `is zero, set firstTimestampBehavior: 'offset' in the options.\nIf you want to allow non-zero first ` + - `timestamps, set firstTimestampBehavior: 'permissive'.\n` - ); - } else if (this.#options.firstTimestampBehavior === 'offset') { - timestamp -= firstTimestamp; + let lastTimestamp = trackNumber === VIDEO_TRACK_NUMBER ? this.#lastVideoTimestamp : + trackNumber === AUDIO_TRACK_NUMBER ? this.#lastAudioTimestamp : + this.#lastSubtitleTimestamp; + + if (trackNumber !== SUBTITLE_TRACK_NUMBER) { + let firstTimestamp = trackNumber === VIDEO_TRACK_NUMBER + ? this.#firstVideoTimestamp + : this.#firstAudioTimestamp; + + // Check first timestamp behavior + if (this.#options.firstTimestampBehavior === 'strict' && lastTimestamp === -1 && timestamp !== 0) { + throw new Error( + `The first chunk for your media track must have a timestamp of 0 (received ${timestamp}). ` + + `Non-zero first timestamps are often caused by directly piping frames or audio data ` + + `from a MediaStreamTrack into the encoder. Their timestamps are typically relative to ` + + `the age of the document, which is probably what you want.\n\nIf you want to offset all ` + + `timestamps of a track such that the first one is zero, set firstTimestampBehavior: ` + + `'offset' in the options.\nIf you want to allow non-zero first timestamps, set ` + + `firstTimestampBehavior: 'permissive'.\n` + ); + } else if (this.#options.firstTimestampBehavior === 'offset') { + timestamp -= firstTimestamp; + } } if (timestamp < lastTimestamp) { @@ -499,11 +586,15 @@ export class Muxer { ); } + if (timestamp < 0) { + throw new Error(`Timestamps must be non-negative (received ${timestamp}).`); + } + return timestamp; } - /** Writes an EBML SimpleBlock containing video or audio data to the file. */ - #writeSimpleBlock(chunk: InternalMediaChunk) { + /** Writes a block containing media data to the file. */ + #writeBlock(chunk: InternalMediaChunk, canCreateNewCluster: boolean) { // When streaming, we create the tracks and segment after we've received the first media chunks. // Due to the interlacing algorithm, this code will be run once we've seen one chunk from every media track. if (this.#options.streaming && !this.#tracksElement) { @@ -511,43 +602,63 @@ export class Muxer { this.#createSegment(); } - let msTime = Math.floor(chunk.timestamp / 1000); - let clusterIsTooLong = chunk.type !== 'key' && msTime - this.#currentClusterTimestamp >= MAX_CHUNK_LENGTH_MS; - - if (clusterIsTooLong) { - throw new Error( - `Current Matroska cluster exceeded its maximum allowed length of ${MAX_CHUNK_LENGTH_MS} ` + - `milliseconds. In order to produce a correct WebM file, you must pass in a video key frame at least ` + - `every ${MAX_CHUNK_LENGTH_MS} milliseconds.` - ); - } - + let msTimestamp = Math.floor(chunk.timestamp / 1000); let shouldCreateNewClusterFromKeyFrame = - (chunk.trackNumber === VIDEO_TRACK_NUMBER || !this.#options.video) && + canCreateNewCluster && chunk.type === 'key' && - msTime - this.#currentClusterTimestamp >= 1000; + msTimestamp - this.#currentClusterTimestamp >= 1000; if ( !this.#currentCluster || shouldCreateNewClusterFromKeyFrame ) { - this.#createNewCluster(msTime); + this.#createNewCluster(msTimestamp); + } + + let relativeTimestamp = msTimestamp - this.#currentClusterTimestamp; + if (relativeTimestamp < 0) { + // The chunk lies out of the current cluster + return; + } + + let clusterIsTooLong = relativeTimestamp >= MAX_CHUNK_LENGTH_MS; + if (clusterIsTooLong) { + throw new Error( + `Current Matroska cluster exceeded its maximum allowed length of ${MAX_CHUNK_LENGTH_MS} ` + + `milliseconds. In order to produce a correct WebM file, you must pass in a key frame at least every ` + + `${MAX_CHUNK_LENGTH_MS} milliseconds.` + ); } let prelude = new Uint8Array(4); let view = new DataView(prelude.buffer); // 0x80 to indicate it's the last byte of a multi-byte number view.setUint8(0, 0x80 | chunk.trackNumber); - view.setUint16(1, msTime - this.#currentClusterTimestamp, false); - view.setUint8(3, Number(chunk.type === 'key') << 7); // Flags + view.setInt16(1, relativeTimestamp, false); - let simpleBlock = { id: EBMLId.SimpleBlock, data: [ - prelude, - chunk.data - ] }; - this.#writer.writeEBML(simpleBlock); + if (chunk.duration === undefined && !chunk.additions) { + // No duration or additions, we can write out a SimpleBlock + view.setUint8(3, Number(chunk.type === 'key') << 7); // Flags (keyframe flag only present for SimpleBlock) + + let simpleBlock = { id: EBMLId.SimpleBlock, data: [ + prelude, + chunk.data + ] }; + this.#writer.writeEBML(simpleBlock); + } else { + let msDuration = Math.floor(chunk.duration / 1000); + let blockGroup = { id: EBMLId.BlockGroup, data: [ + { id: EBMLId.Block, data: [ + prelude, + chunk.data + ] }, + chunk.duration !== undefined ? { id: EBMLId.BlockDuration, data: msDuration } : null, + chunk.additions ? { id: EBMLId.BlockAdditions, data: chunk.additions } : null + ] }; + this.#writer.writeEBML(blockGroup); + } - this.#duration = Math.max(this.#duration, msTime); + this.#duration = Math.max(this.#duration, msTimestamp); } #createCodecPrivateElement(data: AllowSharedBufferSource) { @@ -562,16 +673,30 @@ export class Muxer { let endPos = this.#writer.pos; this.#writer.seek(this.#writer.offsets.get(element)); + let codecPrivateElementSize = 2 + 4 + data.byteLength; + let voidDataSize = CODEC_PRIVATE_MAX_SIZE - codecPrivateElementSize; + + if (voidDataSize < 0) { + // Truncate the CodecPrivate data. This way, the file will at least still be valid. + let newByteLength = data.byteLength + voidDataSize; + if (data instanceof ArrayBuffer) { + data = data.slice(0, newByteLength); + } else { + data = data.buffer.slice(0, newByteLength); + } + voidDataSize = 0; + } + element = [ this.#createCodecPrivateElement(data), - { id: EBMLId.Void, size: 4, data: new Uint8Array(CODEC_PRIVATE_MAX_SIZE - 2 - 4 - data.byteLength) } + { id: EBMLId.Void, size: 4, data: new Uint8Array(voidDataSize) } ]; this.#writer.writeEBML(element); this.#writer.seek(endPos); } - /** Creates a new Cluster element to contain video and audio chunks. */ + /** Creates a new Cluster element to contain media chunks. */ #createNewCluster(timestamp: number) { if (this.#currentCluster && !this.#options.streaming) { this.#finalizeCurrentCluster(); @@ -615,11 +740,14 @@ export class Muxer { this.#writer.seek(endPos); } - /** Finalizes the file, making it ready for use. Must be called after all video and audio chunks have been added. */ + /** Finalizes the file, making it ready for use. Must be called after all media chunks have been added. */ finalize() { // Flush any remaining queued chunks to the file - while (this.#videoChunkQueue.length > 0) this.#writeSimpleBlock(this.#videoChunkQueue.shift()); - while (this.#audioChunkQueue.length > 0) this.#writeSimpleBlock(this.#audioChunkQueue.shift()); + while (this.#videoChunkQueue.length > 0) this.#writeBlock(this.#videoChunkQueue.shift(), true); + while (this.#audioChunkQueue.length > 0) this.#writeBlock(this.#audioChunkQueue.shift(), true); + while (this.#subtitleChunkQueue.length > 0 && this.#subtitleChunkQueue[0].timestamp <= this.#duration) { + this.#writeBlock(this.#subtitleChunkQueue.shift(), false); + } if (!this.#options.streaming) { this.#finalizeCurrentCluster(); diff --git a/src/subtitles.ts b/src/subtitles.ts new file mode 100644 index 0000000..cecbfaa --- /dev/null +++ b/src/subtitles.ts @@ -0,0 +1,151 @@ +export interface EncodedSubtitleChunk { + body: Uint8Array, + additions?: Uint8Array, + timestamp: number, + duration: number +} + +export interface EncodedSubtitleChunkMetadata { + decoderConfig?: { + description: Uint8Array + } +} + +interface SubtitleEncoderOptions { + output: (chunk: EncodedSubtitleChunk, metadata: EncodedSubtitleChunkMetadata) => unknown, + error: (error: Error) => unknown +} + +interface SubtitleEncoderConfig { + codec: 'webvtt' +} + +const cueBlockHeaderRegex = /(?:(.+?)\n)?((?:\d{2}:)?\d{2}:\d{2}.\d{3})\s+-->\s+((?:\d{2}:)?\d{2}:\d{2}.\d{3})/g; +const preambleStartRegex = /^WEBVTT.*?\n{2}/; +const timestampRegex = /(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})/; +const inlineTimestampRegex = /<(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})>/g; +const textEncoder = new TextEncoder(); + +export class SubtitleEncoder { + #options: SubtitleEncoderOptions; + #config: SubtitleEncoderConfig; + #preambleSeen = false; + #preambleBytes: Uint8Array; + #preambleEmitted = false; + + constructor(options: SubtitleEncoderOptions) { + this.#options = options; + } + + configure(config: SubtitleEncoderConfig) { + if (config.codec !== 'webvtt') { + throw new Error("Codec must be 'webvtt'."); + } + + this.#config = config; + } + + encode(text: string) { + if (!this.#config) { + throw new Error('Encoder not configured.'); + } + + text = text.replace('\r\n', '\n').replace('\r', '\n'); + + cueBlockHeaderRegex.lastIndex = 0; + let match: RegExpMatchArray; + + if (!this.#preambleSeen) { + if (!preambleStartRegex.test(text)) { + let error = new Error('WebVTT preamble incorrect.'); + this.#options.error(error); + throw error; + } + + match = cueBlockHeaderRegex.exec(text); + let preamble = text.slice(0, match?.index ?? text.length).trimEnd(); + + if (!preamble) { + let error = new Error('No WebVTT preamble provided.'); + this.#options.error(error); + throw error; + } + + this.#preambleBytes = textEncoder.encode(preamble); + this.#preambleSeen = true; + + if (match) { + text = text.slice(match.index); + cueBlockHeaderRegex.lastIndex = 0; + } + } + + while (match = cueBlockHeaderRegex.exec(text)) { + let notes = text.slice(0, match.index); + let cueIdentifier = match[1] || ''; + let matchEnd = match.index + match[0].length; + let bodyStart = text.indexOf('\n', matchEnd) + 1; + let cueSettings = text.slice(matchEnd, bodyStart).trim(); + let bodyEnd = text.indexOf('\n\n', matchEnd); + if (bodyEnd === -1) bodyEnd = text.length; + + let startTime = this.#parseTimestamp(match[2]); + let endTime = this.#parseTimestamp(match[3]); + let duration = endTime - startTime; + + let body = text.slice(bodyStart, bodyEnd); + let additions = `${cueSettings}\n${cueIdentifier}\n${notes}`; + + // Replace in-body timestamps so that they're relative to the cue start time + inlineTimestampRegex.lastIndex = 0; + body = body.replace(inlineTimestampRegex, (match) => { + let time = this.#parseTimestamp(match.slice(1, -1)); + let offsetTime = time - startTime; + + return `<${this.#formatTimestamp(offsetTime)}>`; + }); + + text = text.slice(bodyEnd).trimStart(); + cueBlockHeaderRegex.lastIndex = 0; + + let chunk: EncodedSubtitleChunk = { + body: textEncoder.encode(body), + additions: additions === '\n\n' ? undefined : textEncoder.encode(additions), + timestamp: startTime * 1000, + duration: duration * 1000 + }; + + let meta: EncodedSubtitleChunkMetadata = {}; + if (!this.#preambleEmitted) { + meta.decoderConfig = { + description: this.#preambleBytes + }; + this.#preambleEmitted = true; + } + + this.#options.output(chunk, meta); + } + } + + #parseTimestamp(string: string) { + let match = timestampRegex.exec(string); + if (!match) throw new Error('Expected match.'); + + return 60 * 60 * 1000 * Number(match[1] || '0') + + 60 * 1000 * Number(match[2]) + + 1000 * Number(match[3]) + + Number(match[4]); + } + + #formatTimestamp(timestamp: number) { + let hours = Math.floor(timestamp / (60 * 60 * 1000)); + let minutes = Math.floor((timestamp % (60 * 60 * 1000)) / (60 * 1000)); + let seconds = Math.floor((timestamp % (60 * 1000)) / 1000); + let milliseconds = timestamp % 1000; + + return hours.toString().padStart(2, '0') + ':' + + minutes.toString().padStart(2, '0') + ':' + + seconds.toString().padStart(2, '0') + '.' + + milliseconds.toString().padStart(3, '0'); + } +} \ No newline at end of file