Add Matroska muxer

This commit is contained in:
Vanilagy
2024-11-10 17:08:04 +01:00
parent dd6526a070
commit d01d32f08c
14 changed files with 2655 additions and 36 deletions
+770 -9
View File
@@ -28,11 +28,13 @@ var Metamuxer = (() => {
FileSystemWritableFileStreamTarget: () => FileSystemWritableFileStreamTarget2,
MediaStreamAudioTrackSource: () => MediaStreamAudioTrackSource,
MediaStreamVideoTrackSource: () => MediaStreamVideoTrackSource,
MkvOutputFormat: () => MkvOutputFormat2,
Mp4OutputFormat: () => Mp4OutputFormat,
Output: () => Output,
StreamTarget: () => StreamTarget,
Target: () => Target,
VideoFrameSource: () => VideoFrameSource
VideoFrameSource: () => VideoFrameSource,
WebMOutputFormat: () => WebMOutputFormat
});
// src/codec.ts
@@ -138,6 +140,35 @@ var Metamuxer = (() => {
var isU32 = (value) => {
return value >= 0 && value < 2 ** 32;
};
var readBits = (bytes2, start, end) => {
let result = 0;
for (let i = start; i < end; i++) {
let byteIndex = Math.floor(i / 8);
let byte = bytes2[byteIndex];
let bitIndex = 7 - (i & 7);
let bit = (byte & 1 << bitIndex) >> bitIndex;
result <<= 1;
result |= bit;
}
return result;
};
var writeBits = (bytes2, start, end, value) => {
for (let i = start; i < end; i++) {
let byteIndex = Math.floor(i / 8);
let byte = bytes2[byteIndex];
let bitIndex = 7 - (i & 7);
byte &= ~(1 << bitIndex);
byte |= (value & 1 << end - i - 1) >> end - i - 1 << bitIndex;
bytes2[byteIndex] = byte;
}
};
var toUint8Array = (source) => {
if (source instanceof ArrayBuffer) {
return new Uint8Array(source);
} else {
return new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
}
};
// src/source.ts
var VideoSource = class {
@@ -398,6 +429,7 @@ var Metamuxer = (() => {
type: source instanceof VideoSource ? "video" : "audio",
source
};
this.muxer.beforeTrackAdd(track);
this.tracks.push(track);
source.connectedTrack = track;
}
@@ -672,6 +704,7 @@ var Metamuxer = (() => {
// Component flags
u32(0),
// Component flags mask
// TODO:
ascii("mp4-muxer-hdlr", true)
// Component name
]);
@@ -764,11 +797,11 @@ var Metamuxer = (() => {
]);
var avcC = (trackData) => trackData.info.decoderConfig && box("avcC", [
// For AVC, description is an AVCDecoderConfigurationRecord, so nothing else to do here
...new Uint8Array(trackData.info.decoderConfig.description)
...toUint8Array(trackData.info.decoderConfig.description)
]);
var hvcC = (trackData) => trackData.info.decoderConfig && box("hvcC", [
// For HEVC, description is a HEVCDecoderConfigurationRecord, so nothing else to do here
...new Uint8Array(trackData.info.decoderConfig.description)
...toUint8Array(trackData.info.decoderConfig.description)
]);
var vpcC = (trackData) => {
if (!trackData.info.decoderConfig) {
@@ -840,7 +873,7 @@ var Metamuxer = (() => {
AUDIO_CODEC_TO_CONFIGURATION_BOX[trackData.track.source.codec](trackData)
]);
var esds = (trackData) => {
let description = new Uint8Array(trackData.info.decoderConfig.description);
let description = toUint8Array(trackData.info.decoderConfig.description ?? new ArrayBuffer(0));
return fullBox("esds", 0, 0, [
// https://stackoverflow.com/a/54803118
u32(58753152),
@@ -1165,6 +1198,8 @@ var Metamuxer = (() => {
constructor(output) {
this.output = output;
}
beforeTrackAdd(track) {
}
};
// src/isobmff/isobmff_muxer.ts
@@ -1270,9 +1305,6 @@ var Metamuxer = (() => {
}
}
start() {
this.#writeHeader();
}
#writeHeader() {
const holdsAvc = this.output.tracks.some((x) => x.type === "video" && x.source.codec === "avc");
this.writeBox(ftyp({
holdsAvc,
@@ -1317,8 +1349,8 @@ var Metamuxer = (() => {
}
assert(meta);
assert(meta.decoderConfig);
assert(meta.decoderConfig.codedWidth);
assert(meta.decoderConfig.codedHeight);
assert(meta.decoderConfig.codedWidth !== void 0);
assert(meta.decoderConfig.codedHeight !== void 0);
const newTrackData = {
track,
type: "video",
@@ -1510,6 +1542,9 @@ var Metamuxer = (() => {
trackData.currentChunk.samples.push(sample);
}
#validateTimestamp(trackData, presentationTimestamp, decodeTimestamp) {
if (decodeTimestamp < 0) {
throw new Error(`Timestamps must be non-negative (got ${decodeTimestamp}s).`);
}
if (trackData.firstDecodeTimestamp === null) {
trackData.firstDecodeTimestamp = decodeTimestamp;
}
@@ -1694,6 +1729,721 @@ var Metamuxer = (() => {
}
};
// src/matroska/ebml.ts
var EBMLFloat32 = class {
constructor(value) {
this.value = value;
}
};
var EBMLFloat64 = class {
constructor(value) {
this.value = value;
}
};
var measureUnsignedInt = (value) => {
if (value < 1 << 8) {
return 1;
} else if (value < 1 << 16) {
return 2;
} else if (value < 1 << 24) {
return 3;
} else if (value < 2 ** 32) {
return 4;
} else if (value < 2 ** 40) {
return 5;
} else {
return 6;
}
};
var measureEBMLVarInt = (value) => {
if (value < (1 << 7) - 1) {
return 1;
} else if (value < (1 << 14) - 1) {
return 2;
} else if (value < (1 << 21) - 1) {
return 3;
} else if (value < (1 << 28) - 1) {
return 4;
} else if (value < 2 ** 35 - 1) {
return 5;
} else if (value < 2 ** 42 - 1) {
return 6;
} else {
throw new Error("EBML VINT size not supported " + value);
}
};
// src/matroska/matroska_muxer.ts
var VIDEO_TRACK_TYPE = 1;
var AUDIO_TRACK_TYPE = 2;
var MAX_CHUNK_LENGTH_MS = 2 ** 15;
var APP_NAME = "https://github.com/Vanilagy/webm-muxer";
var SEGMENT_SIZE_BYTES = 6;
var CLUSTER_SIZE_BYTES = 5;
var CODEC_STRING_MAP = {
avc: "V_MPEG4/ISO/AVC",
hevc: "V_MPEGH/ISO/HEVC",
vp8: "V_VP8",
vp9: "V_VP9",
av1: "V_AV1",
aac: "A_AAC",
opus: "A_OPUS",
vorbis: "A_VORBIS"
};
var MatroskaMuxer = class extends Muxer {
constructor(output, format) {
super(output);
this.#helper = new Uint8Array(8);
this.#helperView = new DataView(this.#helper.buffer);
/**
* Stores the position from the start of the file to where EBML elements have been written. This is used to
* rewrite/edit elements that were already added before, and to measure sizes of things.
*/
this.offsets = /* @__PURE__ */ new WeakMap();
/** Same as offsets, but stores position where the element's data starts (after ID and size fields). */
this.dataOffsets = /* @__PURE__ */ new WeakMap();
this.#trackDatas = [];
this.#segment = null;
this.#segmentInfo = null;
this.#seekHead = null;
this.#tracksElement = null;
this.#segmentDuration = null;
this.#cues = null;
this.#currentCluster = null;
this.#currentClusterTimestamp = null;
this.#trackDatasInCurrentCluster = /* @__PURE__ */ new Set();
this.#duration = 0;
this.#writer = output.writer;
this.#format = format;
}
#writer;
#format;
#helper;
#helperView;
#trackDatas;
#segment;
#segmentInfo;
#seekHead;
#tracksElement;
#segmentDuration;
#cues;
#currentCluster;
#currentClusterTimestamp;
#trackDatasInCurrentCluster;
#duration;
#writeByte(value) {
this.#helperView.setUint8(0, value);
this.#writer.write(this.#helper.subarray(0, 1));
}
#writeFloat32(value) {
this.#helperView.setFloat32(0, value, false);
this.#writer.write(this.#helper.subarray(0, 4));
}
#writeFloat64(value) {
this.#helperView.setFloat64(0, value, false);
this.#writer.write(this.#helper);
}
#writeUnsignedInt(value, width = measureUnsignedInt(value)) {
let pos = 0;
switch (width) {
case 6:
this.#helperView.setUint8(pos++, value / 2 ** 40 | 0);
case 5:
this.#helperView.setUint8(pos++, value / 2 ** 32 | 0);
case 4:
this.#helperView.setUint8(pos++, value >> 24);
case 3:
this.#helperView.setUint8(pos++, value >> 16);
case 2:
this.#helperView.setUint8(pos++, value >> 8);
case 1:
this.#helperView.setUint8(pos++, value);
break;
default:
throw new Error("Bad UINT size " + width);
}
this.#writer.write(this.#helper.subarray(0, pos));
}
writeEBMLVarInt(value, width = measureEBMLVarInt(value)) {
let pos = 0;
switch (width) {
case 1:
this.#helperView.setUint8(pos++, 1 << 7 | value);
break;
case 2:
this.#helperView.setUint8(pos++, 1 << 6 | value >> 8);
this.#helperView.setUint8(pos++, value);
break;
case 3:
this.#helperView.setUint8(pos++, 1 << 5 | value >> 16);
this.#helperView.setUint8(pos++, value >> 8);
this.#helperView.setUint8(pos++, value);
break;
case 4:
this.#helperView.setUint8(pos++, 1 << 4 | value >> 24);
this.#helperView.setUint8(pos++, value >> 16);
this.#helperView.setUint8(pos++, value >> 8);
this.#helperView.setUint8(pos++, value);
break;
case 5:
this.#helperView.setUint8(pos++, 1 << 3 | value / 2 ** 32 & 7);
this.#helperView.setUint8(pos++, value >> 24);
this.#helperView.setUint8(pos++, value >> 16);
this.#helperView.setUint8(pos++, value >> 8);
this.#helperView.setUint8(pos++, value);
break;
case 6:
this.#helperView.setUint8(pos++, 1 << 2 | value / 2 ** 40 & 3);
this.#helperView.setUint8(pos++, value / 2 ** 32 | 0);
this.#helperView.setUint8(pos++, value >> 24);
this.#helperView.setUint8(pos++, value >> 16);
this.#helperView.setUint8(pos++, value >> 8);
this.#helperView.setUint8(pos++, value);
break;
default:
throw new Error("Bad EBML VINT size " + width);
}
this.#writer.write(this.#helper.subarray(0, pos));
}
// Assumes the string is ASCII
#writeString(str) {
this.#writer.write(new Uint8Array(str.split("").map((x) => x.charCodeAt(0))));
}
writeEBML(data) {
if (data === null) return;
if (data instanceof Uint8Array) {
this.#writer.write(data);
} else if (Array.isArray(data)) {
for (let elem of data) {
this.writeEBML(elem);
}
} else {
this.offsets.set(data, this.#writer.getPos());
this.#writeUnsignedInt(data.id);
if (Array.isArray(data.data)) {
let sizePos = this.#writer.getPos();
let sizeSize = data.size === -1 ? 1 : data.size ?? 4;
if (data.size === -1) {
this.#writeByte(255);
} else {
this.#writer.seek(this.#writer.getPos() + sizeSize);
}
let startPos = this.#writer.getPos();
this.dataOffsets.set(data, startPos);
this.writeEBML(data.data);
if (data.size !== -1) {
let size = this.#writer.getPos() - startPos;
let endPos = this.#writer.getPos();
this.#writer.seek(sizePos);
this.writeEBMLVarInt(size, sizeSize);
this.#writer.seek(endPos);
}
} else if (typeof data.data === "number") {
let size = data.size ?? measureUnsignedInt(data.data);
this.writeEBMLVarInt(size);
this.#writeUnsignedInt(data.data, size);
} else if (typeof data.data === "string") {
this.writeEBMLVarInt(data.data.length);
this.#writeString(data.data);
} else if (data.data instanceof Uint8Array) {
this.writeEBMLVarInt(data.data.byteLength, data.size);
this.#writer.write(data.data);
} else if (data.data instanceof EBMLFloat32) {
this.writeEBMLVarInt(4);
this.#writeFloat32(data.data.value);
} else if (data.data instanceof EBMLFloat64) {
this.writeEBMLVarInt(8);
this.#writeFloat64(data.data.value);
}
}
}
beforeTrackAdd(track) {
if (!(this.#format instanceof WebMOutputFormat)) {
return;
}
if (track.type === "video") {
if (!["vp8", "vp9", "av1"].includes(track.source.codec)) {
throw new Error(`WebM only supports VP8, VP9 and AV1 as video codecs. Switching to MKV removes this restriction.`);
}
} else {
if (!["opus", "vorbis"].includes(track.source.codec)) {
throw new Error(`WebM only supports Opus and Vorbis as audio codecs. Switching to MKV removes this restriction.`);
}
}
}
start() {
this.#writeEBMLHeader();
if (!this.#format.options.streaming) {
this.#createSeekHead();
}
this.#createSegmentInfo();
this.#createCues();
this.#writer.flush();
}
#writeEBMLHeader() {
let ebmlHeader = { id: 440786851 /* EBML */, data: [
{ id: 17030 /* EBMLVersion */, data: 1 },
{ id: 17143 /* EBMLReadVersion */, data: 1 },
{ id: 17138 /* EBMLMaxIDLength */, data: 4 },
{ id: 17139 /* EBMLMaxSizeLength */, data: 8 },
{ id: 17026 /* DocType */, data: this.#format instanceof WebMOutputFormat ? "webm" : "matroska" },
{ id: 17031 /* DocTypeVersion */, data: 2 },
{ id: 17029 /* DocTypeReadVersion */, data: 2 }
] };
this.writeEBML(ebmlHeader);
}
/**
* Creates a SeekHead element which is positioned near the start of the file and allows the media player to seek to
* relevant sections more easily. Since we don't know the positions of those sections yet, we'll set them later.
*/
#createSeekHead() {
const kaxCues = new Uint8Array([28, 83, 187, 107]);
const kaxInfo = new Uint8Array([21, 73, 169, 102]);
const kaxTracks = new Uint8Array([22, 84, 174, 107]);
let seekHead = { id: 290298740 /* SeekHead */, data: [
{ id: 19899 /* Seek */, data: [
{ id: 21419 /* SeekID */, data: kaxCues },
{ id: 21420 /* SeekPosition */, size: 5, data: 0 }
] },
{ id: 19899 /* Seek */, data: [
{ id: 21419 /* SeekID */, data: kaxInfo },
{ id: 21420 /* SeekPosition */, size: 5, data: 0 }
] },
{ id: 19899 /* Seek */, data: [
{ id: 21419 /* SeekID */, data: kaxTracks },
{ id: 21420 /* SeekPosition */, size: 5, data: 0 }
] }
] };
this.#seekHead = seekHead;
}
#createSegmentInfo() {
let segmentDuration = { id: 17545 /* Duration */, data: new EBMLFloat64(0) };
this.#segmentDuration = segmentDuration;
let segmentInfo = { id: 357149030 /* Info */, data: [
{ id: 2807729 /* TimestampScale */, data: 1e6 },
{ id: 19840 /* MuxingApp */, data: APP_NAME },
{ id: 22337 /* WritingApp */, data: APP_NAME },
!this.#format.options.streaming ? segmentDuration : null
] };
this.#segmentInfo = segmentInfo;
}
#createTracks() {
let tracksElement = { id: 374648427 /* Tracks */, data: [] };
this.#tracksElement = tracksElement;
for (let trackData of this.#trackDatas) {
tracksElement.data.push({ id: 174 /* TrackEntry */, data: [
{ id: 215 /* TrackNumber */, data: trackData.track.id },
{ id: 29637 /* TrackUID */, data: trackData.track.id },
{ id: 131 /* TrackType */, data: trackData.type === "video" ? VIDEO_TRACK_TYPE : AUDIO_TRACK_TYPE },
// TODO Subtitle case
{ id: 134 /* CodecID */, data: CODEC_STRING_MAP[trackData.track.source.codec] },
trackData.info.decoderConfig.description ? { id: 25506 /* CodecPrivate */, data: toUint8Array(trackData.info.decoderConfig.description) } : null,
...trackData.type === "video" ? [
trackData.track.source.metadata.frameRate ? { id: 2352003 /* DefaultDuration */, data: 1e9 / trackData.track.source.metadata.frameRate } : null,
{ id: 224 /* Video */, data: [
{ id: 176 /* PixelWidth */, data: trackData.info.width },
{ id: 186 /* PixelHeight */, data: trackData.info.height },
(() => {
if (trackData.info.decoderConfig.colorSpace) {
let colorSpace = trackData.info.decoderConfig.colorSpace;
if (!colorSpace.matrix || !colorSpace.transfer || !colorSpace.primaries || colorSpace.fullRange == null) {
return null;
}
return { id: 21936 /* Colour */, data: [
{ id: 21937 /* MatrixCoefficients */, data: {
"rgb": 1,
"bt709": 1,
"bt470bg": 5,
"smpte170m": 6
}[colorSpace.matrix] },
{ id: 21946 /* TransferCharacteristics */, data: {
"bt709": 1,
"smpte170m": 6,
"iec61966-2-1": 13
}[colorSpace.transfer] },
{ id: 21947 /* Primaries */, data: {
"bt709": 1,
"bt470bg": 5,
"smpte170m": 6
}[colorSpace.primaries] },
{ id: 21945 /* Range */, data: [1, 2][Number(colorSpace.fullRange)] }
] };
}
return null;
})()
] }
] : [],
...trackData.type === "audio" ? [
{ id: 225 /* Audio */, data: [
{ id: 181 /* SamplingFrequency */, data: new EBMLFloat32(trackData.info.sampleRate) },
{ id: 159 /* Channels */, data: trackData.info.numberOfChannels }
// Bit depth for when PCM is a thing
] }
] : []
] });
}
}
#createSegment() {
let segment = {
id: 408125543 /* Segment */,
size: this.#format.options.streaming ? -1 : SEGMENT_SIZE_BYTES,
data: [
!this.#format.options.streaming ? this.#seekHead : null,
this.#segmentInfo,
this.#tracksElement
]
};
this.#segment = segment;
this.writeEBML(segment);
}
#createCues() {
this.#cues = { id: 475249515 /* Cues */, data: [] };
}
get #segmentDataOffset() {
assert(this.#segment);
return this.dataOffsets.get(this.#segment);
}
#getVideoTrackData(track, chunk, meta) {
const existingTrackData = this.#trackDatas.find((x) => x.track === track);
if (existingTrackData) {
return existingTrackData;
}
assert(meta);
assert(meta.decoderConfig);
assert(meta.decoderConfig.codedWidth !== void 0);
assert(meta.decoderConfig.codedHeight !== void 0);
const newTrackData = {
track,
type: "video",
info: {
width: meta.decoderConfig.codedWidth,
height: meta.decoderConfig.codedHeight,
decoderConfig: meta.decoderConfig
},
chunkQueue: [],
firstTimestamp: null,
lastTimestamp: null,
lastWrittenTimestamp: null
};
this.#trackDatas.push(newTrackData);
this.#trackDatas.sort((a, b) => a.track.id - b.track.id);
return newTrackData;
}
#getAudioTrackData(track, chunk, meta) {
const existingTrackData = this.#trackDatas.find((x) => x.track === track);
if (existingTrackData) {
return existingTrackData;
}
assert(meta);
assert(meta.decoderConfig);
const newTrackData = {
track,
type: "audio",
info: {
numberOfChannels: meta.decoderConfig.numberOfChannels,
sampleRate: meta.decoderConfig.sampleRate,
decoderConfig: meta.decoderConfig
},
chunkQueue: [],
firstTimestamp: null,
lastTimestamp: null,
lastWrittenTimestamp: null
};
this.#trackDatas.push(newTrackData);
this.#trackDatas.sort((a, b) => a.track.id - b.track.id);
return newTrackData;
}
addEncodedVideoChunk(track, chunk, meta, compositionTimeOffset) {
const trackData = this.#getVideoTrackData(track, chunk, meta);
let videoChunk = this.#createInternalChunk(trackData, chunk);
if (track.source.codec === "vp9") this.#fixVP9ColorSpace(trackData, videoChunk);
trackData.lastTimestamp = videoChunk.timestamp;
trackData.chunkQueue.push(videoChunk);
this.#interleaveChunks();
this.#writer.flush();
}
addEncodedAudioChunk(track, chunk, meta) {
const trackData = this.#getAudioTrackData(track, chunk, meta);
let audioChunk = this.#createInternalChunk(trackData, chunk);
trackData.lastTimestamp = audioChunk.timestamp;
trackData.chunkQueue.push(audioChunk);
this.#interleaveChunks();
this.#writer.flush();
}
#interleaveChunks() {
if (this.#trackDatas.length < this.output.tracks.length) {
return;
}
outer:
while (true) {
let trackWithMinTimestamp = null;
let minTimestamp = Infinity;
for (let trackData of this.#trackDatas) {
if (trackData.chunkQueue.length === 0) {
break outer;
}
if (trackData.chunkQueue[0].timestamp < minTimestamp) {
trackWithMinTimestamp = trackData;
minTimestamp = trackData.chunkQueue[0].timestamp;
}
}
if (!trackWithMinTimestamp) {
break;
}
let chunk = trackWithMinTimestamp.chunkQueue.shift();
this.#writeBlock(trackWithMinTimestamp, chunk);
}
}
/** Due to [a bug in Chromium](https://bugs.chromium.org/p/chromium/issues/detail?id=1377842), VP9 streams often
* lack color space information. This method patches in that information. */
// http://downloads.webmproject.org/docs/vp9/vp9-bitstream_superframe-and-uncompressed-header_v1.0.pdf
#fixVP9ColorSpace(trackData, chunk) {
if (chunk.type !== "key") return;
if (!trackData.info.decoderConfig.colorSpace || !trackData.info.decoderConfig.colorSpace.matrix) return;
let i = 0;
if (readBits(chunk.data, 0, 2) !== 2) return;
i += 2;
let profile = (readBits(chunk.data, i + 1, i + 2) << 1) + readBits(chunk.data, i + 0, i + 1);
i += 2;
if (profile === 3) i++;
let showExistingFrame = readBits(chunk.data, i + 0, i + 1);
i++;
if (showExistingFrame) return;
let frameType = readBits(chunk.data, i + 0, i + 1);
i++;
if (frameType !== 0) return;
i += 2;
let syncCode = readBits(chunk.data, i + 0, i + 24);
i += 24;
if (syncCode !== 4817730) return;
if (profile >= 2) i++;
let colorSpaceID = {
"rgb": 7,
"bt709": 2,
"bt470bg": 1,
"smpte170m": 3
}[trackData.info.decoderConfig.colorSpace.matrix];
writeBits(chunk.data, i + 0, i + 3, colorSpaceID);
}
/*
addSubtitleChunk(chunk: EncodedSubtitleChunk, meta: EncodedSubtitleChunkMetadata, timestamp?: number) {
if (typeof chunk !== 'object' || !chunk) {
throw new TypeError("addSubtitleChunk's first argument (chunk) must be an object.");
} else {
// We can't simply do an instanceof check, so let's check the structure itself:
if (!(chunk.body instanceof Uint8Array)) {
throw new TypeError('body must be an instance of Uint8Array.');
}
if (!Number.isFinite(chunk.timestamp) || chunk.timestamp < 0) {
throw new TypeError('timestamp must be a non-negative real number.');
}
if (!Number.isFinite(chunk.duration) || chunk.duration < 0) {
throw new TypeError('duration must be a non-negative real number.');
}
if (chunk.additions && !(chunk.additions instanceof Uint8Array)) {
throw new TypeError('additions, when present, must be an instance of Uint8Array.');
}
}
if (typeof meta !== 'object') {
throw new TypeError("addSubtitleChunk's second argument (meta) must be an object.");
}
this.#ensureNotFinalized();
if (!this.#options.subtitles) throw new Error('No subtitle track declared.');
// Write possible subtitle decoder metadata to the file
if (meta?.decoderConfig) {
if (this.#options.streaming) {
this.#subtitleCodecPrivate = this.#createCodecPrivateElement(meta.decoderConfig.description);
} else {
this.#writeCodecPrivate(this.#subtitleCodecPrivate, meta.decoderConfig.description);
}
}
let subtitleChunk = this.#createInternalChunk(
chunk.body,
'key',
timestamp ?? chunk.timestamp,
SUBTITLE_TRACK_NUMBER,
chunk.duration,
chunk.additions
);
this.#lastSubtitleTimestamp = subtitleChunk.timestamp;
this.#subtitleChunkQueue.push(subtitleChunk);
this.#writeSubtitleChunks();
this.#maybeFlushStreamingTargetWriter();
}
#writeSubtitleChunks() {
// Writing subtitle chunks is different from video and audio: A subtitle chunk will be written if it's
// guaranteed that no more media chunks will be written before it, to ensure monotonicity. However, media chunks
// will NOT wait for subtitle chunks to arrive, as they may never arrive, so that's how non-monotonicity can
// arrive. But it should be fine, since it's all still in one cluster.
let lastWrittenMediaTimestamp = Math.min(
this.#options.video ? this.#lastVideoTimestamp : Infinity,
this.#options.audio ? this.#lastAudioTimestamp : Infinity
);
let queue = this.#subtitleChunkQueue;
while (queue.length > 0 && queue[0].timestamp <= lastWrittenMediaTimestamp) {
this.#writeBlock(queue.shift(), !this.#options.video && !this.#options.audio);
}
}
*/
/** Converts a read-only external chunk into an internal one for easier use. */
#createInternalChunk(trackData, chunk) {
let adjustedTimestamp = this.#validateTimestamp(trackData, chunk.timestamp);
let data = new Uint8Array(chunk.byteLength);
chunk.copyTo(data);
let internalChunk = {
data,
type: chunk.type,
timestamp: adjustedTimestamp,
duration: chunk.duration,
additions: null
};
return internalChunk;
}
#validateTimestamp(trackData, timestamp) {
if (timestamp < 0) {
throw new Error(`Timestamps must be non-negative (got ${timestamp}s).`);
}
if (trackData.firstTimestamp === null) {
trackData.firstTimestamp = timestamp;
}
timestamp -= trackData.firstTimestamp;
if (trackData.lastTimestamp !== null && timestamp < trackData.lastTimestamp) {
throw new Error(
`Timestamps must be monotonically increasing (timestamp went from ${trackData.lastTimestamp}s to ${timestamp}s).`
);
}
return timestamp;
}
/** Writes a block containing media data to the file. */
#writeBlock(trackData, chunk) {
if (!this.#segment) {
this.#createTracks();
this.#createSegment();
}
let msTimestamp = Math.floor(chunk.timestamp / 1e3);
const keyFrameQueuedEverywhere = this.#trackDatas.every((otherTrackData) => {
if (trackData === otherTrackData) {
return chunk.type === "key";
}
const firstQueuedSample = otherTrackData.chunkQueue[0];
return firstQueuedSample && firstQueuedSample.type === "key";
});
if (!this.#currentCluster || keyFrameQueuedEverywhere && msTimestamp - this.#currentClusterTimestamp >= 1e3) {
this.#createNewCluster(msTimestamp);
}
let relativeTimestamp = msTimestamp - this.#currentClusterTimestamp;
if (relativeTimestamp < 0) {
return;
}
let clusterIsTooLong = relativeTimestamp >= MAX_CHUNK_LENGTH_MS;
if (clusterIsTooLong) {
throw new Error(
`Current Matroska cluster exceeded its maximum allowed length of ${MAX_CHUNK_LENGTH_MS} milliseconds. In order to produce a correct WebM file, you must pass in a key frame at least every ${MAX_CHUNK_LENGTH_MS} milliseconds.`
);
}
let prelude = new Uint8Array(4);
let view2 = new DataView(prelude.buffer);
view2.setUint8(0, 128 | trackData.track.id);
view2.setInt16(1, relativeTimestamp, false);
let msDuration = Math.floor((chunk.duration ?? 0) / 1e3);
if (msDuration === 0 && !chunk.additions) {
view2.setUint8(3, Number(chunk.type === "key") << 7);
let simpleBlock = { id: 163 /* SimpleBlock */, data: [
prelude,
chunk.data
] };
this.writeEBML(simpleBlock);
} else {
let blockGroup = { id: 160 /* BlockGroup */, data: [
{ id: 161 /* Block */, data: [
prelude,
chunk.data
] },
chunk.type === "delta" ? { id: 251 /* ReferenceBlock */, data: trackData.lastWrittenTimestamp - msTimestamp } : null,
chunk.duration !== null ? { id: 155 /* BlockDuration */, data: msDuration } : null,
chunk.additions ? { id: 30113 /* BlockAdditions */, data: chunk.additions } : null
] };
this.writeEBML(blockGroup);
}
this.#duration = Math.max(this.#duration, msTimestamp + msDuration);
trackData.lastWrittenTimestamp = msTimestamp;
this.#trackDatasInCurrentCluster.add(trackData);
}
/** Creates a new Cluster element to contain media chunks. */
#createNewCluster(timestamp) {
if (this.#currentCluster && !this.#format.options.streaming) {
this.#finalizeCurrentCluster();
}
this.#currentCluster = {
id: 524531317 /* Cluster */,
size: this.#format.options.streaming ? -1 : CLUSTER_SIZE_BYTES,
data: [
{ id: 231 /* Timestamp */, data: timestamp }
]
};
this.writeEBML(this.#currentCluster);
this.#currentClusterTimestamp = timestamp;
this.#trackDatasInCurrentCluster.clear();
}
#finalizeCurrentCluster() {
assert(this.#currentCluster);
let clusterSize = this.#writer.getPos() - this.dataOffsets.get(this.#currentCluster);
let endPos = this.#writer.getPos();
this.#writer.seek(this.offsets.get(this.#currentCluster) + 4);
this.writeEBMLVarInt(clusterSize, CLUSTER_SIZE_BYTES);
this.#writer.seek(endPos);
let clusterOffsetFromSegment = this.offsets.get(this.#currentCluster) - this.#segmentDataOffset;
assert(this.#cues);
this.#cues.data.push({ id: 187 /* CuePoint */, data: [
{ id: 179 /* CueTime */, data: this.#currentClusterTimestamp },
// We only write out cues for tracks that have at least one chunk in this cluster
...[...this.#trackDatasInCurrentCluster].map((trackData) => {
return { id: 183 /* CueTrackPositions */, data: [
{ id: 247 /* CueTrack */, data: trackData.track.id },
{ id: 241 /* CueClusterPosition */, data: clusterOffsetFromSegment }
] };
})
] });
}
/** Finalizes the file, making it ready for use. Must be called after all media chunks have been added. */
finalize() {
for (let trackData of this.#trackDatas) {
while (trackData.chunkQueue.length > 0) {
this.#writeBlock(trackData, trackData.chunkQueue.shift());
}
}
if (!this.#format.options.streaming) {
this.#finalizeCurrentCluster();
}
assert(this.#cues);
this.writeEBML(this.#cues);
if (!this.#format.options.streaming) {
let endPos = this.#writer.getPos();
let segmentSize = this.#writer.getPos() - this.#segmentDataOffset;
this.#writer.seek(this.offsets.get(this.#segment) + 4);
this.writeEBMLVarInt(segmentSize, SEGMENT_SIZE_BYTES);
this.#segmentDuration.data = new EBMLFloat64(this.#duration);
this.#writer.seek(this.offsets.get(this.#segmentDuration));
this.writeEBML(this.#segmentDuration);
this.#seekHead.data[0].data[1].data = this.offsets.get(this.#cues) - this.#segmentDataOffset;
this.#seekHead.data[1].data[1].data = this.offsets.get(this.#segmentInfo) - this.#segmentDataOffset;
this.#seekHead.data[2].data[1].data = this.offsets.get(this.#tracksElement) - this.#segmentDataOffset;
this.#writer.seek(this.offsets.get(this.#seekHead));
this.writeEBML(this.#seekHead);
this.#writer.seek(endPos);
}
}
};
// src/output_format.ts
var OutputFormat = class {
};
@@ -1706,6 +2456,17 @@ var Metamuxer = (() => {
return new IsobmffMuxer(output, this);
}
};
var MkvOutputFormat2 = class extends OutputFormat {
constructor(options = {}) {
super();
this.options = options;
}
createMuxer(output) {
return new MatroskaMuxer(output, this);
}
};
var WebMOutputFormat = class extends MkvOutputFormat2 {
};
// src/writer.ts
var Writer = class {