mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-30 20:33:55 +02:00
Replace # with private
This commit is contained in:
+157
-159
@@ -86,38 +86,36 @@ export const intoTimescale = (timeInSeconds: number, timescale: number, round =
|
||||
export class IsobmffMuxer extends Muxer {
|
||||
override timestampsMustStartAtZero = true;
|
||||
|
||||
#writer: Writer;
|
||||
#boxWriter: IsobmffBoxWriter;
|
||||
#format: Mp4OutputFormat;
|
||||
#fastStart: NonNullable<Mp4OutputFormat['options']['fastStart']>;
|
||||
private writer: Writer;
|
||||
private boxWriter: IsobmffBoxWriter;
|
||||
private fastStart: NonNullable<Mp4OutputFormat['options']['fastStart']>;
|
||||
|
||||
#auxTarget = new ArrayBufferTarget();
|
||||
#auxWriter = this.#auxTarget._createWriter();
|
||||
#auxBoxWriter = new IsobmffBoxWriter(this.#auxWriter);
|
||||
private auxTarget = new ArrayBufferTarget();
|
||||
private auxWriter = this.auxTarget._createWriter();
|
||||
private auxBoxWriter = new IsobmffBoxWriter(this.auxWriter);
|
||||
|
||||
#ftypSize: number | null = null;
|
||||
#mdat: Box | null = null;
|
||||
private ftypSize: number | null = null;
|
||||
private mdat: Box | null = null;
|
||||
|
||||
#trackDatas: IsobmffTrackData[] = [];
|
||||
private trackDatas: IsobmffTrackData[] = [];
|
||||
|
||||
#creationTime = Math.floor(Date.now() / 1000) + TIMESTAMP_OFFSET;
|
||||
#finalizedChunks: Chunk[] = [];
|
||||
private creationTime = Math.floor(Date.now() / 1000) + TIMESTAMP_OFFSET;
|
||||
private finalizedChunks: Chunk[] = [];
|
||||
|
||||
#nextFragmentNumber = 1;
|
||||
private nextFragmentNumber = 1;
|
||||
|
||||
constructor(output: Output, format: Mp4OutputFormat) {
|
||||
super(output);
|
||||
|
||||
this.#writer = output._writer;
|
||||
this.#boxWriter = new IsobmffBoxWriter(this.#writer);
|
||||
this.#format = format;
|
||||
this.writer = output._writer;
|
||||
this.boxWriter = new IsobmffBoxWriter(this.writer);
|
||||
|
||||
// If the fastStart option isn't defined, enable in-memory fast start if the target is an ArrayBuffer, as the
|
||||
// memory usage remains identical
|
||||
this.#fastStart = format.options.fastStart ?? (this.#writer instanceof ArrayBufferTargetWriter ? 'in-memory' : false);
|
||||
this.fastStart = format.options.fastStart ?? (this.writer instanceof ArrayBufferTargetWriter ? 'in-memory' : false);
|
||||
|
||||
if (this.#fastStart === 'in-memory' || this.#fastStart === 'fragmented') {
|
||||
this.#writer.ensureMonotonicity = true;
|
||||
if (this.fastStart === 'in-memory' || this.fastStart === 'fragmented') {
|
||||
this.writer.ensureMonotonicity = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,27 +123,27 @@ export class IsobmffMuxer extends Muxer {
|
||||
const holdsAvc = this.output._tracks.some(x => x.type === 'video' && x.source._codec === 'avc');
|
||||
|
||||
// Write the header
|
||||
this.#boxWriter.writeBox(ftyp({
|
||||
this.boxWriter.writeBox(ftyp({
|
||||
holdsAvc: holdsAvc,
|
||||
fragmented: this.#fastStart === 'fragmented'
|
||||
fragmented: this.fastStart === 'fragmented'
|
||||
}));
|
||||
|
||||
this.#ftypSize = this.#writer.getPos();
|
||||
this.ftypSize = this.writer.getPos();
|
||||
|
||||
if (this.#fastStart === 'in-memory') {
|
||||
this.#mdat = mdat(false);
|
||||
} else if (this.#fastStart === 'fragmented') {
|
||||
if (this.fastStart === 'in-memory') {
|
||||
this.mdat = mdat(false);
|
||||
} else if (this.fastStart === 'fragmented') {
|
||||
// We write the moov box once we write out the first fragment to make sure we get the decoder configs
|
||||
} else {
|
||||
this.#mdat = mdat(true); // Reserve large size by default, can refine this when finalizing.
|
||||
this.#boxWriter.writeBox(this.#mdat);
|
||||
this.mdat = mdat(true); // Reserve large size by default, can refine this when finalizing.
|
||||
this.boxWriter.writeBox(this.mdat);
|
||||
}
|
||||
|
||||
this.#writer.flush();
|
||||
this.writer.flush();
|
||||
}
|
||||
|
||||
#getVideoTrackData(track: OutputVideoTrack, meta?: EncodedVideoChunkMetadata) {
|
||||
const existingTrackData = this.#trackDatas.find(x => x.track === track);
|
||||
private getVideoTrackData(track: OutputVideoTrack, meta?: EncodedVideoChunkMetadata) {
|
||||
const existingTrackData = this.trackDatas.find(x => x.track === track);
|
||||
if (existingTrackData) {
|
||||
return existingTrackData as IsobmffVideoTrackData;
|
||||
}
|
||||
@@ -178,14 +176,14 @@ export class IsobmffMuxer extends Muxer {
|
||||
compactlyCodedChunkTable: []
|
||||
};
|
||||
|
||||
this.#trackDatas.push(newTrackData);
|
||||
this.#trackDatas.sort((a, b) => a.track.id - b.track.id);
|
||||
this.trackDatas.push(newTrackData);
|
||||
this.trackDatas.sort((a, b) => a.track.id - b.track.id);
|
||||
|
||||
return newTrackData;
|
||||
}
|
||||
|
||||
#getAudioTrackData(track: OutputAudioTrack, meta?: EncodedAudioChunkMetadata) {
|
||||
const existingTrackData = this.#trackDatas.find(x => x.track === track);
|
||||
private getAudioTrackData(track: OutputAudioTrack, meta?: EncodedAudioChunkMetadata) {
|
||||
const existingTrackData = this.trackDatas.find(x => x.track === track);
|
||||
if (existingTrackData) {
|
||||
return existingTrackData as IsobmffAudioTrackData;
|
||||
}
|
||||
@@ -216,14 +214,14 @@ export class IsobmffMuxer extends Muxer {
|
||||
compactlyCodedChunkTable: []
|
||||
};
|
||||
|
||||
this.#trackDatas.push(newTrackData);
|
||||
this.#trackDatas.sort((a, b) => a.track.id - b.track.id);
|
||||
this.trackDatas.push(newTrackData);
|
||||
this.trackDatas.sort((a, b) => a.track.id - b.track.id);
|
||||
|
||||
return newTrackData;
|
||||
}
|
||||
|
||||
#getSubtitleTrackData(track: OutputSubtitleTrack, meta?: SubtitleMetadata) {
|
||||
const existingTrackData = this.#trackDatas.find(x => x.track === track);
|
||||
private getSubtitleTrackData(track: OutputSubtitleTrack, meta?: SubtitleMetadata) {
|
||||
const existingTrackData = this.trackDatas.find(x => x.track === track);
|
||||
if (existingTrackData) {
|
||||
return existingTrackData as IsobmffSubtitleTrackData;
|
||||
}
|
||||
@@ -256,8 +254,8 @@ export class IsobmffMuxer extends Muxer {
|
||||
cueToSourceId: new WeakMap()
|
||||
};
|
||||
|
||||
this.#trackDatas.push(newTrackData);
|
||||
this.#trackDatas.sort((a, b) => a.track.id - b.track.id);
|
||||
this.trackDatas.push(newTrackData);
|
||||
this.trackDatas.sort((a, b) => a.track.id - b.track.id);
|
||||
|
||||
// Subtitle cues don't need to start at 0, so let's register a timestamp at 0 to satisfy the
|
||||
// "timestamps must start a zero" constraint
|
||||
@@ -267,43 +265,43 @@ export class IsobmffMuxer extends Muxer {
|
||||
}
|
||||
|
||||
addEncodedVideoChunk(track: OutputVideoTrack, chunk: EncodedVideoChunk, meta?: EncodedVideoChunkMetadata) {
|
||||
const trackData = this.#getVideoTrackData(track, meta);
|
||||
const trackData = this.getVideoTrackData(track, meta);
|
||||
|
||||
let data = new Uint8Array(chunk.byteLength);
|
||||
chunk.copyTo(data);
|
||||
|
||||
let timestamp = this.validateAndNormalizeTimestamp(trackData.track, chunk.timestamp, chunk.type === 'key');
|
||||
let sample = this.#createSampleForTrack(trackData, data, timestamp, (chunk.duration ?? 0) / 1e6, chunk.type);
|
||||
let sample = this.createSampleForTrack(trackData, data, timestamp, (chunk.duration ?? 0) / 1e6, chunk.type);
|
||||
|
||||
this.#registerSample(trackData, sample);
|
||||
this.registerSample(trackData, sample);
|
||||
}
|
||||
|
||||
addEncodedAudioChunk(track: OutputAudioTrack, chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata) {
|
||||
const trackData = this.#getAudioTrackData(track, meta);
|
||||
const trackData = this.getAudioTrackData(track, meta);
|
||||
|
||||
let data = new Uint8Array(chunk.byteLength);
|
||||
chunk.copyTo(data);
|
||||
|
||||
let timestamp = this.validateAndNormalizeTimestamp(trackData.track, chunk.timestamp, chunk.type === 'key');
|
||||
let sample = this.#createSampleForTrack(trackData, data, timestamp, (chunk.duration ?? 0) / 1e6, chunk.type);
|
||||
let sample = this.createSampleForTrack(trackData, data, timestamp, (chunk.duration ?? 0) / 1e6, chunk.type);
|
||||
|
||||
this.#registerSample(trackData, sample);
|
||||
this.registerSample(trackData, sample);
|
||||
}
|
||||
|
||||
addSubtitleCue(track: OutputSubtitleTrack, cue: SubtitleCue, meta?: SubtitleMetadata) {
|
||||
const trackData = this.#getSubtitleTrackData(track, meta);
|
||||
const trackData = this.getSubtitleTrackData(track, meta);
|
||||
|
||||
this.validateAndNormalizeTimestamp(trackData.track, 1e6 * cue.timestamp, true);
|
||||
|
||||
if (track.source._codec === 'webvtt') {
|
||||
trackData.cueQueue.push(cue);
|
||||
this.#processWebVTTCues(trackData, cue.timestamp);
|
||||
this.processWebVTTCues(trackData, cue.timestamp);
|
||||
} else {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
#processWebVTTCues(trackData: IsobmffSubtitleTrackData, until: number) {
|
||||
private processWebVTTCues(trackData: IsobmffSubtitleTrackData, until: number) {
|
||||
// WebVTT cues need to undergo special processing as empty sections need to be padded out with samples, and
|
||||
// overlapping samples require special logic. The algorithm produces the format specified in ISO 14496-30.
|
||||
|
||||
@@ -329,18 +327,18 @@ export class IsobmffMuxer extends Muxer {
|
||||
|
||||
// We may need to pad out empty space with an vtte box
|
||||
if (trackData.lastCueEndTimestamp < sampleStart) {
|
||||
this.#auxWriter.seek(0);
|
||||
this.auxWriter.seek(0);
|
||||
let box = vtte();
|
||||
this.#auxBoxWriter.writeBox(box);
|
||||
this.auxBoxWriter.writeBox(box);
|
||||
|
||||
let body = this.#auxWriter.getSlice(0, this.#auxWriter.getPos());
|
||||
let sample = this.#createSampleForTrack(trackData, body, trackData.lastCueEndTimestamp, sampleStart - trackData.lastCueEndTimestamp, 'key');
|
||||
let body = this.auxWriter.getSlice(0, this.auxWriter.getPos());
|
||||
let sample = this.createSampleForTrack(trackData, body, trackData.lastCueEndTimestamp, sampleStart - trackData.lastCueEndTimestamp, 'key');
|
||||
|
||||
this.#registerSample(trackData, sample);
|
||||
this.registerSample(trackData, sample);
|
||||
trackData.lastCueEndTimestamp = sampleStart;
|
||||
}
|
||||
|
||||
this.#auxWriter.seek(0);
|
||||
this.auxWriter.seek(0);
|
||||
|
||||
for (let i = 0; i < trackData.cueQueue.length; i++) {
|
||||
let cue = trackData.cueQueue[i]!
|
||||
@@ -364,11 +362,11 @@ export class IsobmffMuxer extends Muxer {
|
||||
if (cue.notes) {
|
||||
// Any notes/comments are included in a special vtta box
|
||||
let box = vtta(cue.notes);
|
||||
this.#auxBoxWriter.writeBox(box);
|
||||
this.auxBoxWriter.writeBox(box);
|
||||
}
|
||||
|
||||
let box = vttc(cue.text, containsTimestamp ? sampleStart : null, cue.identifier ?? null, cue.settings ?? null, sourceId ?? null);
|
||||
this.#auxBoxWriter.writeBox(box);
|
||||
this.auxBoxWriter.writeBox(box);
|
||||
|
||||
if (endTimestamp === sampleEnd) {
|
||||
// The cue won't appear in any future sample, so we're done with it
|
||||
@@ -376,15 +374,15 @@ export class IsobmffMuxer extends Muxer {
|
||||
}
|
||||
}
|
||||
|
||||
let body = this.#auxWriter.getSlice(0, this.#auxWriter.getPos());
|
||||
let sample = this.#createSampleForTrack(trackData, body, sampleStart, sampleEnd - sampleStart, 'key');
|
||||
let body = this.auxWriter.getSlice(0, this.auxWriter.getPos());
|
||||
let sample = this.createSampleForTrack(trackData, body, sampleStart, sampleEnd - sampleStart, 'key');
|
||||
|
||||
this.#registerSample(trackData, sample);
|
||||
this.registerSample(trackData, sample);
|
||||
trackData.lastCueEndTimestamp = sampleEnd;
|
||||
}
|
||||
}
|
||||
|
||||
#createSampleForTrack(
|
||||
private createSampleForTrack(
|
||||
trackData: IsobmffTrackData,
|
||||
data: Uint8Array,
|
||||
timestamp: number,
|
||||
@@ -405,7 +403,7 @@ export class IsobmffMuxer extends Muxer {
|
||||
return sample;
|
||||
}
|
||||
|
||||
#processTimestamps(trackData: IsobmffTrackData) {
|
||||
private processTimestamps(trackData: IsobmffTrackData) {
|
||||
if (trackData.timestampProcessingQueue.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -433,7 +431,7 @@ export class IsobmffMuxer extends Muxer {
|
||||
trackData.lastTimescaleUnits += delta;
|
||||
trackData.lastSample.timescaleUnitsToNextSample = delta;
|
||||
|
||||
if (this.#fastStart !== 'fragmented') {
|
||||
if (this.fastStart !== 'fragmented') {
|
||||
let lastTableEntry = last(trackData.timeToSampleTable);
|
||||
assert(lastTableEntry);
|
||||
|
||||
@@ -485,7 +483,7 @@ export class IsobmffMuxer extends Muxer {
|
||||
} else {
|
||||
trackData.lastTimescaleUnits = 0;
|
||||
|
||||
if (this.#fastStart !== 'fragmented') {
|
||||
if (this.fastStart !== 'fragmented') {
|
||||
trackData.timeToSampleTable.push({
|
||||
sampleCount: 1,
|
||||
sampleDelta: durationInTimescale
|
||||
@@ -503,21 +501,21 @@ export class IsobmffMuxer extends Muxer {
|
||||
trackData.timestampProcessingQueue.length = 0;
|
||||
}
|
||||
|
||||
#registerSample(trackData: IsobmffTrackData, sample: Sample) {
|
||||
if (this.#fastStart === 'fragmented') {
|
||||
private registerSample(trackData: IsobmffTrackData, sample: Sample) {
|
||||
if (this.fastStart === 'fragmented') {
|
||||
trackData.sampleQueue.push(sample);
|
||||
this.#interleaveSamples();
|
||||
this.interleaveSamples();
|
||||
} else {
|
||||
this.#addSampleToTrack(trackData, sample);
|
||||
this.addSampleToTrack(trackData, sample);
|
||||
}
|
||||
}
|
||||
|
||||
#addSampleToTrack(trackData: IsobmffTrackData, sample: Sample) {
|
||||
private addSampleToTrack(trackData: IsobmffTrackData, sample: Sample) {
|
||||
if (sample.type === 'key') {
|
||||
this.#processTimestamps(trackData);
|
||||
this.processTimestamps(trackData);
|
||||
}
|
||||
|
||||
if (this.#fastStart !== 'fragmented') {
|
||||
if (this.fastStart !== 'fragmented') {
|
||||
trackData.samples.push(sample);
|
||||
}
|
||||
|
||||
@@ -527,10 +525,10 @@ export class IsobmffMuxer extends Muxer {
|
||||
} else {
|
||||
let currentChunkDuration = sample.timestamp - trackData.currentChunk.startTimestamp;
|
||||
|
||||
if (this.#fastStart === 'fragmented') {
|
||||
if (this.fastStart === 'fragmented') {
|
||||
// We can only finalize this fragment (and begin a new one) if we know that each track will be able to
|
||||
// start the new one with a key frame.
|
||||
const keyFrameQueuedEverywhere = this.#trackDatas.every(otherTrackData => {
|
||||
const keyFrameQueuedEverywhere = this.trackDatas.every(otherTrackData => {
|
||||
if (trackData === otherTrackData) {
|
||||
return sample.type === 'key';
|
||||
}
|
||||
@@ -541,7 +539,7 @@ export class IsobmffMuxer extends Muxer {
|
||||
|
||||
if (currentChunkDuration >= 1.0 && keyFrameQueuedEverywhere) {
|
||||
beginNewChunk = true;
|
||||
this.#finalizeFragment();
|
||||
this.finalizeFragment();
|
||||
}
|
||||
} else {
|
||||
beginNewChunk = currentChunkDuration >= 0.5; // Chunk is long enough, we need a new one
|
||||
@@ -550,7 +548,7 @@ export class IsobmffMuxer extends Muxer {
|
||||
|
||||
if (beginNewChunk) {
|
||||
if (trackData.currentChunk) {
|
||||
this.#finalizeCurrentChunk(trackData);
|
||||
this.finalizeCurrentChunk(trackData);
|
||||
}
|
||||
|
||||
trackData.currentChunk = {
|
||||
@@ -566,13 +564,13 @@ export class IsobmffMuxer extends Muxer {
|
||||
trackData.timestampProcessingQueue.push(sample);
|
||||
}
|
||||
|
||||
#finalizeCurrentChunk(trackData: IsobmffTrackData) {
|
||||
assert(this.#fastStart !== 'fragmented');
|
||||
private finalizeCurrentChunk(trackData: IsobmffTrackData) {
|
||||
assert(this.fastStart !== 'fragmented');
|
||||
|
||||
if (!trackData.currentChunk) return;
|
||||
|
||||
trackData.finalizedChunks.push(trackData.currentChunk);
|
||||
this.#finalizedChunks.push(trackData.currentChunk);
|
||||
this.finalizedChunks.push(trackData.currentChunk);
|
||||
|
||||
if (
|
||||
trackData.compactlyCodedChunkTable.length === 0
|
||||
@@ -584,27 +582,27 @@ export class IsobmffMuxer extends Muxer {
|
||||
});
|
||||
}
|
||||
|
||||
if (this.#fastStart === 'in-memory') {
|
||||
if (this.fastStart === 'in-memory') {
|
||||
trackData.currentChunk.offset = 0; // We'll compute the proper offset when finalizing
|
||||
return;
|
||||
}
|
||||
|
||||
// Write out the data
|
||||
trackData.currentChunk.offset = this.#writer.getPos();
|
||||
trackData.currentChunk.offset = this.writer.getPos();
|
||||
for (let sample of trackData.currentChunk.samples) {
|
||||
assert(sample.data);
|
||||
this.#writer.write(sample.data);
|
||||
this.writer.write(sample.data);
|
||||
sample.data = null; // Can be GC'd
|
||||
}
|
||||
|
||||
this.#writer.flush();
|
||||
this.writer.flush();
|
||||
}
|
||||
|
||||
#interleaveSamples() {
|
||||
assert(this.#fastStart === 'fragmented');
|
||||
private interleaveSamples() {
|
||||
assert(this.fastStart === 'fragmented');
|
||||
|
||||
for (const track of this.output._tracks) {
|
||||
if (!track.source._closed && !this.#trackDatas.some(x => x.track === track)) {
|
||||
if (!track.source._closed && !this.trackDatas.some(x => x.track === track)) {
|
||||
return; // We haven't seen a sample from this open track yet
|
||||
}
|
||||
}
|
||||
@@ -614,7 +612,7 @@ export class IsobmffMuxer extends Muxer {
|
||||
let trackWithMinTimestamp: IsobmffTrackData | null = null;
|
||||
let minTimestamp = Infinity;
|
||||
|
||||
for (let trackData of this.#trackDatas) {
|
||||
for (let trackData of this.trackDatas) {
|
||||
if (trackData.sampleQueue.length === 0 && !trackData.track.source._closed) {
|
||||
break outer;
|
||||
}
|
||||
@@ -630,25 +628,25 @@ export class IsobmffMuxer extends Muxer {
|
||||
}
|
||||
|
||||
let sample = trackWithMinTimestamp.sampleQueue.shift()!;
|
||||
this.#addSampleToTrack(trackWithMinTimestamp, sample);
|
||||
this.addSampleToTrack(trackWithMinTimestamp, sample);
|
||||
}
|
||||
}
|
||||
|
||||
#finalizeFragment(flushWriter = true) {
|
||||
assert(this.#fastStart === 'fragmented');
|
||||
private finalizeFragment(flushWriter = true) {
|
||||
assert(this.fastStart === 'fragmented');
|
||||
|
||||
let fragmentNumber = this.#nextFragmentNumber++;
|
||||
let fragmentNumber = this.nextFragmentNumber++;
|
||||
|
||||
if (fragmentNumber === 1) {
|
||||
// Write the moov box now that we have all decoder configs
|
||||
let movieBox = moov(this.#trackDatas, this.#creationTime, true);
|
||||
this.#boxWriter.writeBox(movieBox);
|
||||
let movieBox = moov(this.trackDatas, this.creationTime, true);
|
||||
this.boxWriter.writeBox(movieBox);
|
||||
}
|
||||
|
||||
// Write out an initial moof box; will be overwritten later once actual chunk offsets are known
|
||||
let moofOffset = this.#writer.getPos();
|
||||
let moofBox = moof(fragmentNumber, this.#trackDatas);
|
||||
this.#boxWriter.writeBox(moofBox);
|
||||
let moofOffset = this.writer.getPos();
|
||||
let moofBox = moof(fragmentNumber, this.trackDatas);
|
||||
this.boxWriter.writeBox(moofBox);
|
||||
|
||||
// Create the mdat box
|
||||
{
|
||||
@@ -656,94 +654,94 @@ export class IsobmffMuxer extends Muxer {
|
||||
let totalTrackSampleSize = 0;
|
||||
|
||||
// Compute the size of the mdat box
|
||||
for (let trackData of this.#trackDatas) {
|
||||
for (let trackData of this.trackDatas) {
|
||||
assert(trackData.currentChunk);
|
||||
for (let sample of trackData.currentChunk.samples) {
|
||||
totalTrackSampleSize += sample.size;
|
||||
}
|
||||
}
|
||||
|
||||
let mdatSize = this.#boxWriter.measureBox(mdatBox) + totalTrackSampleSize;
|
||||
let mdatSize = this.boxWriter.measureBox(mdatBox) + totalTrackSampleSize;
|
||||
if (mdatSize >= 2**32) {
|
||||
// Fragment is larger than 4 GiB, we need to use the large size
|
||||
mdatBox.largeSize = true;
|
||||
mdatSize = this.#boxWriter.measureBox(mdatBox) + totalTrackSampleSize;
|
||||
mdatSize = this.boxWriter.measureBox(mdatBox) + totalTrackSampleSize;
|
||||
}
|
||||
|
||||
mdatBox.size = mdatSize;
|
||||
this.#boxWriter.writeBox(mdatBox);
|
||||
this.boxWriter.writeBox(mdatBox);
|
||||
}
|
||||
|
||||
// Write sample data
|
||||
for (let trackData of this.#trackDatas) {
|
||||
trackData.currentChunk!.offset = this.#writer.getPos();
|
||||
for (let trackData of this.trackDatas) {
|
||||
trackData.currentChunk!.offset = this.writer.getPos();
|
||||
trackData.currentChunk!.moofOffset = moofOffset;
|
||||
|
||||
for (let sample of trackData.currentChunk!.samples) {
|
||||
this.#writer.write(sample.data!);
|
||||
this.writer.write(sample.data!);
|
||||
sample.data = null; // Can be GC'd
|
||||
}
|
||||
}
|
||||
|
||||
// Now that we set the actual chunk offsets, fix the moof box
|
||||
let endPos = this.#writer.getPos();
|
||||
this.#writer.seek(this.#boxWriter.offsets.get(moofBox)!);
|
||||
let newMoofBox = moof(fragmentNumber, this.#trackDatas);
|
||||
this.#boxWriter.writeBox(newMoofBox);
|
||||
this.#writer.seek(endPos);
|
||||
let endPos = this.writer.getPos();
|
||||
this.writer.seek(this.boxWriter.offsets.get(moofBox)!);
|
||||
let newMoofBox = moof(fragmentNumber, this.trackDatas);
|
||||
this.boxWriter.writeBox(newMoofBox);
|
||||
this.writer.seek(endPos);
|
||||
|
||||
for (let trackData of this.#trackDatas) {
|
||||
for (let trackData of this.trackDatas) {
|
||||
trackData.finalizedChunks.push(trackData.currentChunk!);
|
||||
this.#finalizedChunks.push(trackData.currentChunk!);
|
||||
this.finalizedChunks.push(trackData.currentChunk!);
|
||||
trackData.currentChunk = null;
|
||||
}
|
||||
|
||||
if (flushWriter) {
|
||||
this.#writer.flush();
|
||||
this.writer.flush();
|
||||
}
|
||||
}
|
||||
|
||||
override onTrackClose(track: OutputTrack) {
|
||||
if (track.type === 'subtitle' && track.source._codec === 'webvtt') {
|
||||
let trackData = this.#trackDatas.find(x => x.track === track) as IsobmffSubtitleTrackData;
|
||||
let trackData = this.trackDatas.find(x => x.track === track) as IsobmffSubtitleTrackData;
|
||||
if (trackData) {
|
||||
this.#processWebVTTCues(trackData, Infinity);
|
||||
this.processWebVTTCues(trackData, Infinity);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.#fastStart === 'fragmented') {
|
||||
if (this.fastStart === 'fragmented') {
|
||||
// Since a track is now closed, we may be able to write out chunks that were previously waiting
|
||||
this.#interleaveSamples();
|
||||
this.interleaveSamples();
|
||||
}
|
||||
}
|
||||
|
||||
/** Finalizes the file, making it ready for use. Must be called after all video and audio chunks have been added. */
|
||||
finalize() {
|
||||
for (let trackData of this.#trackDatas) {
|
||||
for (let trackData of this.trackDatas) {
|
||||
if (trackData.type === 'subtitle' && trackData.track.source._codec === 'webvtt') {
|
||||
this.#processWebVTTCues(trackData, Infinity);
|
||||
this.processWebVTTCues(trackData, Infinity);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.#fastStart === 'fragmented') {
|
||||
for (let trackData of this.#trackDatas) {
|
||||
if (this.fastStart === 'fragmented') {
|
||||
for (let trackData of this.trackDatas) {
|
||||
for (let sample of trackData.sampleQueue) {
|
||||
this.#addSampleToTrack(trackData, sample);
|
||||
this.addSampleToTrack(trackData, sample);
|
||||
}
|
||||
|
||||
this.#processTimestamps(trackData);
|
||||
this.processTimestamps(trackData);
|
||||
}
|
||||
|
||||
this.#finalizeFragment(false); // Don't flush the last fragment as we will flush it with the mfra box soon
|
||||
this.finalizeFragment(false); // Don't flush the last fragment as we will flush it with the mfra box soon
|
||||
} else {
|
||||
for (let trackData of this.#trackDatas) {
|
||||
this.#processTimestamps(trackData);
|
||||
this.#finalizeCurrentChunk(trackData);
|
||||
for (let trackData of this.trackDatas) {
|
||||
this.processTimestamps(trackData);
|
||||
this.finalizeCurrentChunk(trackData);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.#fastStart === 'in-memory') {
|
||||
assert(this.#mdat);
|
||||
if (this.fastStart === 'in-memory') {
|
||||
assert(this.mdat);
|
||||
let mdatSize: number;
|
||||
|
||||
// We know how many chunks there are, but computing the chunk positions requires an iterative approach:
|
||||
@@ -755,12 +753,12 @@ export class IsobmffMuxer extends Muxer {
|
||||
// size of the moov box and can compute the proper chunk positions.
|
||||
|
||||
for (let i = 0; i < 2; i++) {
|
||||
let movieBox = moov(this.#trackDatas, this.#creationTime);
|
||||
let movieBoxSize = this.#boxWriter.measureBox(movieBox);
|
||||
mdatSize = this.#boxWriter.measureBox(this.#mdat);
|
||||
let currentChunkPos = this.#writer.getPos() + movieBoxSize + mdatSize;
|
||||
let movieBox = moov(this.trackDatas, this.creationTime);
|
||||
let movieBoxSize = this.boxWriter.measureBox(movieBox);
|
||||
mdatSize = this.boxWriter.measureBox(this.mdat);
|
||||
let currentChunkPos = this.writer.getPos() + movieBoxSize + mdatSize;
|
||||
|
||||
for (let chunk of this.#finalizedChunks) {
|
||||
for (let chunk of this.finalizedChunks) {
|
||||
chunk.offset = currentChunkPos;
|
||||
for (let { data } of chunk.samples) {
|
||||
assert(data);
|
||||
@@ -770,53 +768,53 @@ export class IsobmffMuxer extends Muxer {
|
||||
}
|
||||
|
||||
if (currentChunkPos < 2**32) break;
|
||||
if (mdatSize >= 2**32) this.#mdat.largeSize = true;
|
||||
if (mdatSize >= 2**32) this.mdat.largeSize = true;
|
||||
}
|
||||
|
||||
let movieBox = moov(this.#trackDatas, this.#creationTime);
|
||||
this.#boxWriter.writeBox(movieBox);
|
||||
let movieBox = moov(this.trackDatas, this.creationTime);
|
||||
this.boxWriter.writeBox(movieBox);
|
||||
|
||||
this.#mdat.size = mdatSize!;
|
||||
this.#boxWriter.writeBox(this.#mdat);
|
||||
this.mdat.size = mdatSize!;
|
||||
this.boxWriter.writeBox(this.mdat);
|
||||
|
||||
for (let chunk of this.#finalizedChunks) {
|
||||
for (let chunk of this.finalizedChunks) {
|
||||
for (let sample of chunk.samples) {
|
||||
assert(sample.data);
|
||||
this.#writer.write(sample.data);
|
||||
this.writer.write(sample.data);
|
||||
sample.data = null;
|
||||
}
|
||||
}
|
||||
} else if (this.#fastStart === 'fragmented') {
|
||||
} else if (this.fastStart === 'fragmented') {
|
||||
// Append the mfra box to the end of the file for better random access
|
||||
let startPos = this.#writer.getPos();
|
||||
let mfraBox = mfra(this.#trackDatas);
|
||||
this.#boxWriter.writeBox(mfraBox);
|
||||
let startPos = this.writer.getPos();
|
||||
let mfraBox = mfra(this.trackDatas);
|
||||
this.boxWriter.writeBox(mfraBox);
|
||||
|
||||
// Patch the 'size' field of the mfro box at the end of the mfra box now that we know its actual size
|
||||
let mfraBoxSize = this.#writer.getPos() - startPos;
|
||||
this.#writer.seek(this.#writer.getPos() - 4);
|
||||
this.#boxWriter.writeU32(mfraBoxSize);
|
||||
let mfraBoxSize = this.writer.getPos() - startPos;
|
||||
this.writer.seek(this.writer.getPos() - 4);
|
||||
this.boxWriter.writeU32(mfraBoxSize);
|
||||
} else {
|
||||
assert(this.#mdat);
|
||||
assert(this.#ftypSize !== null);
|
||||
assert(this.mdat);
|
||||
assert(this.ftypSize !== null);
|
||||
|
||||
let mdatPos = this.#boxWriter.offsets.get(this.#mdat);
|
||||
let mdatPos = this.boxWriter.offsets.get(this.mdat);
|
||||
assert(mdatPos !== undefined);
|
||||
let mdatSize = this.#writer.getPos() - mdatPos;
|
||||
this.#mdat.size = mdatSize;
|
||||
this.#mdat.largeSize = mdatSize >= 2**32; // Only use the large size if we need it
|
||||
this.#boxWriter.patchBox(this.#mdat);
|
||||
let mdatSize = this.writer.getPos() - mdatPos;
|
||||
this.mdat.size = mdatSize;
|
||||
this.mdat.largeSize = mdatSize >= 2**32; // Only use the large size if we need it
|
||||
this.boxWriter.patchBox(this.mdat);
|
||||
|
||||
let movieBox = moov(this.#trackDatas, this.#creationTime);
|
||||
let movieBox = moov(this.trackDatas, this.creationTime);
|
||||
|
||||
if (typeof this.#fastStart === 'object') {
|
||||
this.#writer.seek(this.#ftypSize);
|
||||
this.#boxWriter.writeBox(movieBox);
|
||||
if (typeof this.fastStart === 'object') {
|
||||
this.writer.seek(this.ftypSize);
|
||||
this.boxWriter.writeBox(movieBox);
|
||||
|
||||
let remainingBytes = mdatPos - this.#writer.getPos();
|
||||
this.#boxWriter.writeBox(free(remainingBytes));
|
||||
let remainingBytes = mdatPos - this.writer.getPos();
|
||||
this.boxWriter.writeBox(free(remainingBytes));
|
||||
} else {
|
||||
this.#boxWriter.writeBox(movieBox);
|
||||
this.boxWriter.writeBox(movieBox);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+196
-196
@@ -88,94 +88,94 @@ const TRACK_TYPE_MAP: Record<OutputTrack['type'], number> = {
|
||||
export class MatroskaMuxer extends Muxer {
|
||||
override timestampsMustStartAtZero = false;
|
||||
|
||||
#writer: Writer;
|
||||
#format: WebMOutputFormat | MkvOutputFormat;
|
||||
private writer: Writer;
|
||||
private format: WebMOutputFormat | MkvOutputFormat;
|
||||
|
||||
#helper = new Uint8Array(8);
|
||||
#helperView = new DataView(this.#helper.buffer);
|
||||
private helper = new Uint8Array(8);
|
||||
private helperView = new DataView(this.helper.buffer);
|
||||
|
||||
/**
|
||||
* Stores the position from the start of the file to where EBML elements have been written. This is used to
|
||||
* rewrite/edit elements that were already added before, and to measure sizes of things.
|
||||
*/
|
||||
offsets = new WeakMap<EBML, number>();
|
||||
private offsets = new WeakMap<EBML, number>();
|
||||
/** Same as offsets, but stores position where the element's data starts (after ID and size fields). */
|
||||
dataOffsets = new WeakMap<EBML, number>();
|
||||
private dataOffsets = new WeakMap<EBML, number>();
|
||||
|
||||
#trackDatas: MatroskaTrackData[] = [];
|
||||
private trackDatas: MatroskaTrackData[] = [];
|
||||
|
||||
#segment: EBMLElement | null = null;
|
||||
#segmentInfo: EBMLElement | null = null;
|
||||
#seekHead: SeekHead | null = null;
|
||||
#tracksElement: EBMLElement | null = null;
|
||||
#segmentDuration: EBMLElement | null = null;
|
||||
#cues: EBMLElement | null = null;
|
||||
private segment: EBMLElement | null = null;
|
||||
private segmentInfo: EBMLElement | null = null;
|
||||
private seekHead: SeekHead | null = null;
|
||||
private tracksElement: EBMLElement | null = null;
|
||||
private segmentDuration: EBMLElement | null = null;
|
||||
private cues: EBMLElement | null = null;
|
||||
|
||||
#currentCluster: EBMLElement | null = null;
|
||||
#currentClusterMsTimestamp: number | null = null;
|
||||
#trackDatasInCurrentCluster = new Set<MatroskaTrackData>();
|
||||
private currentCluster: EBMLElement | null = null;
|
||||
private currentClusterMsTimestamp: number | null = null;
|
||||
private trackDatasInCurrentCluster = new Set<MatroskaTrackData>();
|
||||
|
||||
#duration = 0;
|
||||
private duration = 0;
|
||||
|
||||
constructor(output: Output, format: MkvOutputFormat) {
|
||||
super(output);
|
||||
|
||||
this.#writer = output._writer;
|
||||
this.#format = format;
|
||||
this.writer = output._writer;
|
||||
this.format = format;
|
||||
|
||||
if (this.#format.options.streamable) {
|
||||
this.#writer.ensureMonotonicity = true;
|
||||
if (this.format.options.streamable) {
|
||||
this.writer.ensureMonotonicity = true;
|
||||
}
|
||||
}
|
||||
|
||||
#writeByte(value: number) {
|
||||
this.#helperView.setUint8(0, value);
|
||||
this.#writer.write(this.#helper.subarray(0, 1));
|
||||
private writeByte(value: number) {
|
||||
this.helperView.setUint8(0, value);
|
||||
this.writer.write(this.helper.subarray(0, 1));
|
||||
}
|
||||
|
||||
#writeFloat32(value: number) {
|
||||
this.#helperView.setFloat32(0, value, false);
|
||||
this.#writer.write(this.#helper.subarray(0, 4));
|
||||
private writeFloat32(value: number) {
|
||||
this.helperView.setFloat32(0, value, false);
|
||||
this.writer.write(this.helper.subarray(0, 4));
|
||||
}
|
||||
|
||||
#writeFloat64(value: number) {
|
||||
this.#helperView.setFloat64(0, value, false);
|
||||
this.#writer.write(this.#helper);
|
||||
private writeFloat64(value: number) {
|
||||
this.helperView.setFloat64(0, value, false);
|
||||
this.writer.write(this.helper);
|
||||
}
|
||||
|
||||
#writeUnsignedInt(value: number, width = measureUnsignedInt(value)) {
|
||||
private writeUnsignedInt(value: number, width = measureUnsignedInt(value)) {
|
||||
let pos = 0;
|
||||
|
||||
// Each case falls through:
|
||||
switch (width) {
|
||||
case 6:
|
||||
// Need to use division to access >32 bits of floating point var
|
||||
this.#helperView.setUint8(pos++, (value / 2**40) | 0);
|
||||
this.helperView.setUint8(pos++, (value / 2**40) | 0);
|
||||
case 5:
|
||||
this.#helperView.setUint8(pos++, (value / 2**32) | 0);
|
||||
this.helperView.setUint8(pos++, (value / 2**32) | 0);
|
||||
case 4:
|
||||
this.#helperView.setUint8(pos++, value >> 24);
|
||||
this.helperView.setUint8(pos++, value >> 24);
|
||||
case 3:
|
||||
this.#helperView.setUint8(pos++, value >> 16);
|
||||
this.helperView.setUint8(pos++, value >> 16);
|
||||
case 2:
|
||||
this.#helperView.setUint8(pos++, value >> 8);
|
||||
this.helperView.setUint8(pos++, value >> 8);
|
||||
case 1:
|
||||
this.#helperView.setUint8(pos++, value);
|
||||
this.helperView.setUint8(pos++, value);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Bad UINT size ' + width);
|
||||
}
|
||||
|
||||
this.#writer.write(this.#helper.subarray(0, pos));
|
||||
this.writer.write(this.helper.subarray(0, pos));
|
||||
}
|
||||
|
||||
#writeSignedInt(value: number, width = measureSignedInt(value)) {
|
||||
private writeSignedInt(value: number, width = measureSignedInt(value)) {
|
||||
if (value < 0) {
|
||||
// Two's complement stuff
|
||||
value += 2 ** (width * 8);
|
||||
}
|
||||
|
||||
this.#writeUnsignedInt(value, width);
|
||||
this.writeUnsignedInt(value, width);
|
||||
}
|
||||
|
||||
writeEBMLVarInt(value: number, width = measureEBMLVarInt(value)) {
|
||||
@@ -183,22 +183,22 @@ export class MatroskaMuxer extends Muxer {
|
||||
|
||||
switch (width) {
|
||||
case 1:
|
||||
this.#helperView.setUint8(pos++, (1 << 7) | value);
|
||||
this.helperView.setUint8(pos++, (1 << 7) | value);
|
||||
break;
|
||||
case 2:
|
||||
this.#helperView.setUint8(pos++, (1 << 6) | (value >> 8));
|
||||
this.#helperView.setUint8(pos++, value);
|
||||
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);
|
||||
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);
|
||||
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:
|
||||
/**
|
||||
@@ -206,94 +206,94 @@ export class MatroskaMuxer extends Muxer {
|
||||
* operations, so we need to do a division by 2^32 instead of a
|
||||
* right-shift of 32 to retain those top 3 bits
|
||||
*/
|
||||
this.#helperView.setUint8(pos++, (1 << 3) | ((value / 2**32) & 0x7));
|
||||
this.#helperView.setUint8(pos++, value >> 24);
|
||||
this.#helperView.setUint8(pos++, value >> 16);
|
||||
this.#helperView.setUint8(pos++, value >> 8);
|
||||
this.#helperView.setUint8(pos++, value);
|
||||
this.helperView.setUint8(pos++, (1 << 3) | ((value / 2**32) & 0x7));
|
||||
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) & 0x3));
|
||||
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);
|
||||
this.helperView.setUint8(pos++, (1 << 2) | ((value / 2**40) & 0x3));
|
||||
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));
|
||||
this.writer.write(this.helper.subarray(0, pos));
|
||||
}
|
||||
|
||||
// Assumes the string is ASCII
|
||||
#writeString(str: string) {
|
||||
this.#writer.write(new Uint8Array(str.split('').map(x => x.charCodeAt(0))));
|
||||
private writeString(str: string) {
|
||||
this.writer.write(new Uint8Array(str.split('').map(x => x.charCodeAt(0))));
|
||||
}
|
||||
|
||||
writeEBML(data: EBML | null) {
|
||||
private writeEBML(data: EBML | null) {
|
||||
if (data === null) return;
|
||||
|
||||
if (data instanceof Uint8Array) {
|
||||
this.#writer.write(data);
|
||||
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.offsets.set(data, this.writer.getPos());
|
||||
|
||||
this.#writeUnsignedInt(data.id); // ID field
|
||||
this.writeUnsignedInt(data.id); // ID field
|
||||
|
||||
if (Array.isArray(data.data)) {
|
||||
let sizePos = this.#writer.getPos();
|
||||
let sizePos = this.writer.getPos();
|
||||
let sizeSize = data.size === -1 ? 1 : (data.size ?? 4);
|
||||
|
||||
if (data.size === -1) {
|
||||
// Write the reserved all-one-bits marker for unknown/unbounded size.
|
||||
this.#writeByte(0xff);
|
||||
this.writeByte(0xff);
|
||||
} else {
|
||||
this.#writer.seek(this.#writer.getPos() + sizeSize);
|
||||
this.writer.seek(this.writer.getPos() + sizeSize);
|
||||
}
|
||||
|
||||
let startPos = this.#writer.getPos();
|
||||
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);
|
||||
let size = this.writer.getPos() - startPos;
|
||||
let endPos = this.writer.getPos();
|
||||
this.writer.seek(sizePos);
|
||||
this.writeEBMLVarInt(size, sizeSize);
|
||||
this.#writer.seek(endPos);
|
||||
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);
|
||||
this.writeUnsignedInt(data.data, size);
|
||||
} else if (typeof data.data === 'string') {
|
||||
this.writeEBMLVarInt(data.data.length);
|
||||
this.#writeString(data.data);
|
||||
this.writeString(data.data);
|
||||
} else if (data.data instanceof Uint8Array) {
|
||||
this.writeEBMLVarInt(data.data.byteLength, data.size);
|
||||
this.#writer.write(data.data);
|
||||
this.writer.write(data.data);
|
||||
} else if (data.data instanceof EBMLFloat32) {
|
||||
this.writeEBMLVarInt(4);
|
||||
this.#writeFloat32(data.data.value);
|
||||
this.writeFloat32(data.data.value);
|
||||
} else if (data.data instanceof EBMLFloat64) {
|
||||
this.writeEBMLVarInt(8);
|
||||
this.#writeFloat64(data.data.value);
|
||||
this.writeFloat64(data.data.value);
|
||||
} else if (data.data instanceof EBMLSignedInt) {
|
||||
let size = data.size ?? measureSignedInt(data.data.value);
|
||||
this.writeEBMLVarInt(size);
|
||||
this.#writeSignedInt(data.data.value, size);
|
||||
this.writeSignedInt(data.data.value, size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override beforeTrackAdd(track: OutputTrack) {
|
||||
if (!(this.#format instanceof WebMOutputFormat)) {
|
||||
if (!(this.format instanceof WebMOutputFormat)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -315,25 +315,25 @@ export class MatroskaMuxer extends Muxer {
|
||||
}
|
||||
|
||||
start() {
|
||||
this.#writeEBMLHeader();
|
||||
this.writeEBMLHeader();
|
||||
|
||||
if (!this.#format.options.streamable) {
|
||||
this.#createSeekHead();
|
||||
if (!this.format.options.streamable) {
|
||||
this.createSeekHead();
|
||||
}
|
||||
|
||||
this.#createSegmentInfo();
|
||||
this.#createCues();
|
||||
this.createSegmentInfo();
|
||||
this.createCues();
|
||||
|
||||
this.#writer.flush();
|
||||
this.writer.flush();
|
||||
}
|
||||
|
||||
#writeEBMLHeader() {
|
||||
private writeEBMLHeader() {
|
||||
let ebmlHeader: EBML = { id: EBMLId.EBML, data: [
|
||||
{ id: EBMLId.EBMLVersion, data: 1 },
|
||||
{ id: EBMLId.EBMLReadVersion, data: 1 },
|
||||
{ id: EBMLId.EBMLMaxIDLength, data: 4 },
|
||||
{ id: EBMLId.EBMLMaxSizeLength, data: 8 },
|
||||
{ id: EBMLId.DocType, data: this.#format instanceof WebMOutputFormat ? 'webm' : 'matroska' },
|
||||
{ id: EBMLId.DocType, data: this.format instanceof WebMOutputFormat ? 'webm' : 'matroska' },
|
||||
{ id: EBMLId.DocTypeVersion, data: 2 },
|
||||
{ id: EBMLId.DocTypeReadVersion, data: 2 }
|
||||
] };
|
||||
@@ -344,7 +344,7 @@ export class MatroskaMuxer extends Muxer {
|
||||
* 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() {
|
||||
private createSeekHead() {
|
||||
const kaxCues = new Uint8Array([ 0x1c, 0x53, 0xbb, 0x6b ]);
|
||||
const kaxInfo = new Uint8Array([ 0x15, 0x49, 0xa9, 0x66 ]);
|
||||
const kaxTracks = new Uint8Array([ 0x16, 0x54, 0xae, 0x6b ]);
|
||||
@@ -363,27 +363,27 @@ export class MatroskaMuxer extends Muxer {
|
||||
{ id: EBMLId.SeekPosition, size: 5, data: 0 }
|
||||
] }
|
||||
] };
|
||||
this.#seekHead = seekHead;
|
||||
this.seekHead = seekHead;
|
||||
}
|
||||
|
||||
#createSegmentInfo() {
|
||||
private createSegmentInfo() {
|
||||
let segmentDuration: EBML = { id: EBMLId.Duration, data: new EBMLFloat64(0) };
|
||||
this.#segmentDuration = segmentDuration;
|
||||
this.segmentDuration = segmentDuration;
|
||||
|
||||
let segmentInfo: EBML = { id: EBMLId.Info, data: [
|
||||
{ id: EBMLId.TimestampScale, data: 1e6 },
|
||||
{ id: EBMLId.MuxingApp, data: APP_NAME },
|
||||
{ id: EBMLId.WritingApp, data: APP_NAME },
|
||||
!this.#format.options.streamable ? segmentDuration : null
|
||||
!this.format.options.streamable ? segmentDuration : null
|
||||
] };
|
||||
this.#segmentInfo = segmentInfo;
|
||||
this.segmentInfo = segmentInfo;
|
||||
}
|
||||
|
||||
#createTracks() {
|
||||
private createTracks() {
|
||||
let tracksElement = { id: EBMLId.Tracks, data: [] as EBML[] };
|
||||
this.#tracksElement = tracksElement;
|
||||
this.tracksElement = tracksElement;
|
||||
|
||||
for (let trackData of this.#trackDatas) {
|
||||
for (let trackData of this.trackDatas) {
|
||||
tracksElement.data.push({ id: EBMLId.TrackEntry, data: [
|
||||
{ id: EBMLId.TrackNumber, data: trackData.track.id },
|
||||
{ id: EBMLId.TrackUID, data: trackData.track.id },
|
||||
@@ -429,17 +429,17 @@ export class MatroskaMuxer extends Muxer {
|
||||
}
|
||||
}
|
||||
|
||||
#createSegment() {
|
||||
private createSegment() {
|
||||
let segment: EBML = {
|
||||
id: EBMLId.Segment,
|
||||
size: this.#format.options.streamable ? -1 : SEGMENT_SIZE_BYTES,
|
||||
size: this.format.options.streamable ? -1 : SEGMENT_SIZE_BYTES,
|
||||
data: [
|
||||
!this.#format.options.streamable ? this.#seekHead as EBML : null,
|
||||
this.#segmentInfo,
|
||||
this.#tracksElement
|
||||
!this.format.options.streamable ? this.seekHead as EBML : null,
|
||||
this.segmentInfo,
|
||||
this.tracksElement
|
||||
]
|
||||
};
|
||||
this.#segment = segment;
|
||||
this.segment = segment;
|
||||
|
||||
this.writeEBML(segment);
|
||||
|
||||
@@ -451,17 +451,17 @@ export class MatroskaMuxer extends Muxer {
|
||||
*/
|
||||
}
|
||||
|
||||
#createCues() {
|
||||
this.#cues = { id: EBMLId.Cues, data: [] };
|
||||
private createCues() {
|
||||
this.cues = { id: EBMLId.Cues, data: [] };
|
||||
}
|
||||
|
||||
get #segmentDataOffset() {
|
||||
assert(this.#segment);
|
||||
return this.dataOffsets.get(this.#segment)!;
|
||||
private get segmentDataOffset() {
|
||||
assert(this.segment);
|
||||
return this.dataOffsets.get(this.segment)!;
|
||||
}
|
||||
|
||||
#getVideoTrackData(track: OutputVideoTrack, meta?: EncodedVideoChunkMetadata) {
|
||||
const existingTrackData = this.#trackDatas.find(x => x.track === track);
|
||||
private getVideoTrackData(track: OutputVideoTrack, meta?: EncodedVideoChunkMetadata) {
|
||||
const existingTrackData = this.trackDatas.find(x => x.track === track);
|
||||
if (existingTrackData) {
|
||||
return existingTrackData as MatroskaVideoTrackData;
|
||||
}
|
||||
@@ -485,14 +485,14 @@ export class MatroskaMuxer extends Muxer {
|
||||
lastWrittenMsTimestamp: null
|
||||
};
|
||||
|
||||
this.#trackDatas.push(newTrackData);
|
||||
this.#trackDatas.sort((a, b) => a.track.id - b.track.id);
|
||||
this.trackDatas.push(newTrackData);
|
||||
this.trackDatas.sort((a, b) => a.track.id - b.track.id);
|
||||
|
||||
return newTrackData;
|
||||
}
|
||||
|
||||
#getAudioTrackData(track: OutputAudioTrack, meta?: EncodedAudioChunkMetadata) {
|
||||
const existingTrackData = this.#trackDatas.find(x => x.track === track);
|
||||
private getAudioTrackData(track: OutputAudioTrack, meta?: EncodedAudioChunkMetadata) {
|
||||
const existingTrackData = this.trackDatas.find(x => x.track === track);
|
||||
if (existingTrackData) {
|
||||
return existingTrackData as MatroskaAudioTrackData;
|
||||
}
|
||||
@@ -514,14 +514,14 @@ export class MatroskaMuxer extends Muxer {
|
||||
lastWrittenMsTimestamp: null
|
||||
};
|
||||
|
||||
this.#trackDatas.push(newTrackData);
|
||||
this.#trackDatas.sort((a, b) => a.track.id - b.track.id);
|
||||
this.trackDatas.push(newTrackData);
|
||||
this.trackDatas.sort((a, b) => a.track.id - b.track.id);
|
||||
|
||||
return newTrackData;
|
||||
}
|
||||
|
||||
#getSubtitleTrackData(track: OutputSubtitleTrack, meta?: SubtitleMetadata) {
|
||||
const existingTrackData = this.#trackDatas.find(x => x.track === track);
|
||||
private getSubtitleTrackData(track: OutputSubtitleTrack, meta?: SubtitleMetadata) {
|
||||
const existingTrackData = this.trackDatas.find(x => x.track === track);
|
||||
if (existingTrackData) {
|
||||
return existingTrackData as MatroskaAudioTrackData;
|
||||
}
|
||||
@@ -541,41 +541,41 @@ export class MatroskaMuxer extends Muxer {
|
||||
lastWrittenMsTimestamp: null
|
||||
};
|
||||
|
||||
this.#trackDatas.push(newTrackData);
|
||||
this.#trackDatas.sort((a, b) => a.track.id - b.track.id);
|
||||
this.trackDatas.push(newTrackData);
|
||||
this.trackDatas.sort((a, b) => a.track.id - b.track.id);
|
||||
|
||||
return newTrackData;
|
||||
}
|
||||
|
||||
addEncodedVideoChunk(track: OutputVideoTrack, chunk: EncodedVideoChunk, meta?: EncodedVideoChunkMetadata) {
|
||||
const trackData = this.#getVideoTrackData(track, meta);
|
||||
const trackData = this.getVideoTrackData(track, meta);
|
||||
|
||||
let data = new Uint8Array(chunk.byteLength);
|
||||
chunk.copyTo(data);
|
||||
|
||||
let timestamp = this.validateAndNormalizeTimestamp(trackData.track, chunk.timestamp, chunk.type === 'key');
|
||||
let videoChunk = this.#createInternalChunk(data, timestamp, (chunk.duration ?? 0) / 1e6, chunk.type);
|
||||
if (track.source._codec === 'vp9') this.#fixVP9ColorSpace(trackData, videoChunk);
|
||||
let videoChunk = this.createInternalChunk(data, timestamp, (chunk.duration ?? 0) / 1e6, chunk.type);
|
||||
if (track.source._codec === 'vp9') this.fixVP9ColorSpace(trackData, videoChunk);
|
||||
|
||||
trackData.chunkQueue.push(videoChunk);
|
||||
this.#interleaveChunks();
|
||||
this.interleaveChunks();
|
||||
}
|
||||
|
||||
addEncodedAudioChunk(track: OutputAudioTrack, chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata) {
|
||||
const trackData = this.#getAudioTrackData(track, meta);
|
||||
const trackData = this.getAudioTrackData(track, meta);
|
||||
|
||||
let data = new Uint8Array(chunk.byteLength);
|
||||
chunk.copyTo(data);
|
||||
|
||||
let timestamp = this.validateAndNormalizeTimestamp(trackData.track, chunk.timestamp, chunk.type === 'key');
|
||||
let audioChunk = this.#createInternalChunk(data, timestamp, (chunk.duration ?? 0) / 1e6, chunk.type);
|
||||
let audioChunk = this.createInternalChunk(data, timestamp, (chunk.duration ?? 0) / 1e6, chunk.type);
|
||||
|
||||
trackData.chunkQueue.push(audioChunk);
|
||||
this.#interleaveChunks();
|
||||
this.interleaveChunks();
|
||||
}
|
||||
|
||||
addSubtitleCue(track: OutputSubtitleTrack, cue: SubtitleCue, meta?: SubtitleMetadata) {
|
||||
const trackData = this.#getSubtitleTrackData(track, meta);
|
||||
const trackData = this.getSubtitleTrackData(track, meta);
|
||||
|
||||
const timestamp = this.validateAndNormalizeTimestamp(trackData.track, 1e6 * cue.timestamp, true);
|
||||
|
||||
@@ -594,15 +594,15 @@ export class MatroskaMuxer extends Muxer {
|
||||
const body = textEncoder.encode(bodyText);
|
||||
const additions = `${cue.settings ?? ''}\n${cue.identifier ?? ''}\n${cue.notes ?? ''}`;
|
||||
|
||||
let subtitleChunk = this.#createInternalChunk(body, timestamp, cue.duration, 'key', additions.trim() ? textEncoder.encode(additions) : null);
|
||||
let subtitleChunk = this.createInternalChunk(body, timestamp, cue.duration, 'key', additions.trim() ? textEncoder.encode(additions) : null);
|
||||
|
||||
trackData.chunkQueue.push(subtitleChunk);
|
||||
this.#interleaveChunks();
|
||||
this.interleaveChunks();
|
||||
}
|
||||
|
||||
#interleaveChunks() {
|
||||
private interleaveChunks() {
|
||||
for (const track of this.output._tracks) {
|
||||
if (!track.source._closed && !this.#trackDatas.some(x => x.track === track)) {
|
||||
if (!track.source._closed && !this.trackDatas.some(x => x.track === track)) {
|
||||
return; // We haven't seen a sample from this open track yet
|
||||
}
|
||||
}
|
||||
@@ -612,7 +612,7 @@ export class MatroskaMuxer extends Muxer {
|
||||
let trackWithMinTimestamp: MatroskaTrackData | null = null;
|
||||
let minTimestamp = Infinity;
|
||||
|
||||
for (let trackData of this.#trackDatas) {
|
||||
for (let trackData of this.trackDatas) {
|
||||
if (trackData.chunkQueue.length === 0 && !trackData.track.source._closed) {
|
||||
break outer;
|
||||
}
|
||||
@@ -628,16 +628,16 @@ export class MatroskaMuxer extends Muxer {
|
||||
}
|
||||
|
||||
let chunk = trackWithMinTimestamp.chunkQueue.shift()!;
|
||||
this.#writeBlock(trackWithMinTimestamp, chunk);
|
||||
this.writeBlock(trackWithMinTimestamp, chunk);
|
||||
}
|
||||
|
||||
this.#writer.flush();
|
||||
this.writer.flush();
|
||||
}
|
||||
|
||||
/** Due to [a bug in Chromium](https://bugs.chromium.org/p/chromium/issues/detail?id=1377842), VP9 streams often
|
||||
* lack color space information. This method patches in that information. */
|
||||
// http://downloads.webmproject.org/docs/vp9/vp9-bitstream_superframe-and-uncompressed-header_v1.0.pdf
|
||||
#fixVP9ColorSpace(trackData: MatroskaVideoTrackData, chunk: InternalMediaChunk) {
|
||||
private fixVP9ColorSpace(trackData: MatroskaVideoTrackData, chunk: InternalMediaChunk) {
|
||||
if (chunk.type !== 'key') return;
|
||||
if (!trackData.info.decoderConfig.colorSpace || !trackData.info.decoderConfig.colorSpace.matrix) return;
|
||||
|
||||
@@ -671,7 +671,7 @@ export class MatroskaMuxer extends Muxer {
|
||||
}
|
||||
|
||||
/** Converts a read-only external chunk into an internal one for easier use. */
|
||||
#createInternalChunk(
|
||||
private createInternalChunk(
|
||||
data: Uint8Array,
|
||||
timestamp: number,
|
||||
duration: number,
|
||||
@@ -690,17 +690,17 @@ export class MatroskaMuxer extends Muxer {
|
||||
}
|
||||
|
||||
/** Writes a block containing media data to the file. */
|
||||
#writeBlock(trackData: MatroskaTrackData, chunk: InternalMediaChunk) {
|
||||
private writeBlock(trackData: MatroskaTrackData, chunk: InternalMediaChunk) {
|
||||
// Due to the interlacing algorithm, this code will be run once we've seen one chunk from every media track.
|
||||
if (!this.#segment) {
|
||||
this.#createTracks();
|
||||
this.#createSegment();
|
||||
if (!this.segment) {
|
||||
this.createTracks();
|
||||
this.createSegment();
|
||||
}
|
||||
|
||||
let msTimestamp = Math.floor(1000 * chunk.timestamp);
|
||||
// We can only finalize this fragment (and begin a new one) if we know that each track will be able to
|
||||
// start the new one with a key frame.
|
||||
const keyFrameQueuedEverywhere = this.#trackDatas.every(otherTrackData => {
|
||||
const keyFrameQueuedEverywhere = this.trackDatas.every(otherTrackData => {
|
||||
if (otherTrackData.track.source._closed) {
|
||||
return true;
|
||||
}
|
||||
@@ -714,13 +714,13 @@ export class MatroskaMuxer extends Muxer {
|
||||
});
|
||||
|
||||
if (
|
||||
!this.#currentCluster ||
|
||||
(keyFrameQueuedEverywhere && msTimestamp - this.#currentClusterMsTimestamp! >= 1000)
|
||||
!this.currentCluster ||
|
||||
(keyFrameQueuedEverywhere && msTimestamp - this.currentClusterMsTimestamp! >= 1000)
|
||||
) {
|
||||
this.#createNewCluster(msTimestamp);
|
||||
this.createNewCluster(msTimestamp);
|
||||
}
|
||||
|
||||
let relativeTimestamp = msTimestamp - this.#currentClusterMsTimestamp!;
|
||||
let relativeTimestamp = msTimestamp - this.currentClusterMsTimestamp!;
|
||||
if (relativeTimestamp < 0) {
|
||||
// The chunk lies outside of the current cluster
|
||||
return;
|
||||
@@ -770,16 +770,16 @@ export class MatroskaMuxer extends Muxer {
|
||||
this.writeEBML(blockGroup);
|
||||
}
|
||||
|
||||
this.#duration = Math.max(this.#duration, msTimestamp + msDuration);
|
||||
this.duration = Math.max(this.duration, msTimestamp + msDuration);
|
||||
trackData.lastWrittenMsTimestamp = msTimestamp;
|
||||
|
||||
this.#trackDatasInCurrentCluster.add(trackData);
|
||||
this.trackDatasInCurrentCluster.add(trackData);
|
||||
}
|
||||
|
||||
/** Creates a new Cluster element to contain media chunks. */
|
||||
#createNewCluster(msTimestamp: number) {
|
||||
if (this.#currentCluster && !this.#format.options.streamable) {
|
||||
this.#finalizeCurrentCluster();
|
||||
private createNewCluster(msTimestamp: number) {
|
||||
if (this.currentCluster && !this.format.options.streamable) {
|
||||
this.finalizeCurrentCluster();
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -788,28 +788,28 @@ export class MatroskaMuxer extends Muxer {
|
||||
}
|
||||
*/
|
||||
|
||||
this.#currentCluster = {
|
||||
this.currentCluster = {
|
||||
id: EBMLId.Cluster,
|
||||
size: this.#format.options.streamable ? -1 : CLUSTER_SIZE_BYTES,
|
||||
size: this.format.options.streamable ? -1 : CLUSTER_SIZE_BYTES,
|
||||
data: [
|
||||
{ id: EBMLId.Timestamp, data: msTimestamp }
|
||||
]
|
||||
};
|
||||
this.writeEBML(this.#currentCluster);
|
||||
this.writeEBML(this.currentCluster);
|
||||
|
||||
this.#currentClusterMsTimestamp = msTimestamp;
|
||||
this.#trackDatasInCurrentCluster.clear();
|
||||
this.currentClusterMsTimestamp = msTimestamp;
|
||||
this.trackDatasInCurrentCluster.clear();
|
||||
}
|
||||
|
||||
#finalizeCurrentCluster() {
|
||||
assert(this.#currentCluster);
|
||||
let clusterSize = this.#writer.getPos() - this.dataOffsets.get(this.#currentCluster)!;
|
||||
let endPos = this.#writer.getPos();
|
||||
private finalizeCurrentCluster() {
|
||||
assert(this.currentCluster);
|
||||
let clusterSize = this.writer.getPos() - this.dataOffsets.get(this.currentCluster)!;
|
||||
let endPos = this.writer.getPos();
|
||||
|
||||
// Write the size now that we know it
|
||||
this.#writer.seek(this.offsets.get(this.#currentCluster)! + 4);
|
||||
this.writer.seek(this.offsets.get(this.currentCluster)! + 4);
|
||||
this.writeEBMLVarInt(clusterSize, CLUSTER_SIZE_BYTES);
|
||||
this.#writer.seek(endPos);
|
||||
this.writer.seek(endPos);
|
||||
|
||||
/*
|
||||
if (this.#writer instanceof BaseStreamTargetWriter && this.#writer.target.options.onCluster) {
|
||||
@@ -819,15 +819,15 @@ export class MatroskaMuxer extends Muxer {
|
||||
*/
|
||||
|
||||
let clusterOffsetFromSegment =
|
||||
this.offsets.get(this.#currentCluster)! - this.#segmentDataOffset;
|
||||
this.offsets.get(this.currentCluster)! - this.segmentDataOffset;
|
||||
|
||||
assert(this.#cues);
|
||||
assert(this.cues);
|
||||
|
||||
// Add a CuePoint to the Cues element for better seeking
|
||||
(this.#cues.data as EBML[]).push({ id: EBMLId.CuePoint, data: [
|
||||
{ id: EBMLId.CueTime, data: this.#currentClusterMsTimestamp! },
|
||||
(this.cues.data as EBML[]).push({ id: EBMLId.CuePoint, data: [
|
||||
{ id: EBMLId.CueTime, data: this.currentClusterMsTimestamp! },
|
||||
// We only write out cues for tracks that have at least one chunk in this cluster
|
||||
...[...this.#trackDatasInCurrentCluster].map(trackData => {
|
||||
...[...this.trackDatasInCurrentCluster].map(trackData => {
|
||||
return { id: EBMLId.CueTrackPositions, data: [
|
||||
{ id: EBMLId.CueTrack, data: trackData.track.id },
|
||||
{ id: EBMLId.CueClusterPosition, data: clusterOffsetFromSegment }
|
||||
@@ -838,55 +838,55 @@ export class MatroskaMuxer extends Muxer {
|
||||
|
||||
override onTrackClose() {
|
||||
// Since a track is now closed, we may be able to write out chunks that were previously waiting
|
||||
this.#interleaveChunks();
|
||||
this.interleaveChunks();
|
||||
}
|
||||
|
||||
/** Finalizes the file, making it ready for use. Must be called after all media chunks have been added. */
|
||||
finalize() {
|
||||
if (!this.#segment) {
|
||||
this.#createTracks();
|
||||
this.#createSegment();
|
||||
if (!this.segment) {
|
||||
this.createTracks();
|
||||
this.createSegment();
|
||||
}
|
||||
|
||||
// Flush any remaining queued chunks to the file
|
||||
for (let trackData of this.#trackDatas) {
|
||||
for (let trackData of this.trackDatas) {
|
||||
while (trackData.chunkQueue.length > 0) {
|
||||
this.#writeBlock(trackData, trackData.chunkQueue.shift()!);
|
||||
this.writeBlock(trackData, trackData.chunkQueue.shift()!);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.#format.options.streamable && this.#currentCluster) {
|
||||
this.#finalizeCurrentCluster();
|
||||
if (!this.format.options.streamable && this.currentCluster) {
|
||||
this.finalizeCurrentCluster();
|
||||
}
|
||||
|
||||
assert(this.#cues);
|
||||
this.writeEBML(this.#cues);
|
||||
assert(this.cues);
|
||||
this.writeEBML(this.cues);
|
||||
|
||||
if (!this.#format.options.streamable) {
|
||||
let endPos = this.#writer.getPos();
|
||||
if (!this.format.options.streamable) {
|
||||
let endPos = this.writer.getPos();
|
||||
|
||||
// Write the Segment size
|
||||
let segmentSize = this.#writer.getPos() - this.#segmentDataOffset;
|
||||
this.#writer.seek(this.offsets.get(this.#segment!)! + 4);
|
||||
let segmentSize = this.writer.getPos() - this.segmentDataOffset;
|
||||
this.writer.seek(this.offsets.get(this.segment!)! + 4);
|
||||
this.writeEBMLVarInt(segmentSize, SEGMENT_SIZE_BYTES);
|
||||
|
||||
// Write the duration of the media to the Segment
|
||||
this.#segmentDuration!.data = new EBMLFloat64(this.#duration);
|
||||
this.#writer.seek(this.offsets.get(this.#segmentDuration!)!);
|
||||
this.writeEBML(this.#segmentDuration!);
|
||||
this.segmentDuration!.data = new EBMLFloat64(this.duration);
|
||||
this.writer.seek(this.offsets.get(this.segmentDuration!)!);
|
||||
this.writeEBML(this.segmentDuration!);
|
||||
|
||||
// Fill in SeekHead position data and write it again
|
||||
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.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(this.offsets.get(this.seekHead!)!);
|
||||
this.writeEBML(this.seekHead!);
|
||||
|
||||
this.#writer.seek(endPos);
|
||||
this.writer.seek(endPos);
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
-12
@@ -26,12 +26,12 @@ const preambleStartRegex = /^WEBVTT(.|\n)*?\n{2}/;
|
||||
export const inlineTimestampRegex = /<(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})>/g;
|
||||
|
||||
export class SubtitleParser {
|
||||
#options: SubtitleParserOptions;
|
||||
#preambleText: string | null = null;
|
||||
#preambleEmitted = false;
|
||||
private options: SubtitleParserOptions;
|
||||
private preambleText: string | null = null;
|
||||
private preambleEmitted = false;
|
||||
|
||||
constructor(options: SubtitleParserOptions) {
|
||||
this.#options = options;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
parse(text: string) {
|
||||
@@ -40,10 +40,10 @@ export class SubtitleParser {
|
||||
cueBlockHeaderRegex.lastIndex = 0;
|
||||
let match: RegExpMatchArray | null;
|
||||
|
||||
if (!this.#preambleText) {
|
||||
if (!this.preambleText) {
|
||||
if (!preambleStartRegex.test(text)) {
|
||||
let error = new Error('WebVTT preamble incorrect.');
|
||||
this.#options.error(error);
|
||||
this.options.error(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -52,11 +52,11 @@ export class SubtitleParser {
|
||||
|
||||
if (!preamble) {
|
||||
let error = new Error('No WebVTT preamble provided.');
|
||||
this.#options.error(error);
|
||||
this.options.error(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.#preambleText = preamble;
|
||||
this.preambleText = preamble;
|
||||
|
||||
if (match) {
|
||||
text = text.slice(match.index);
|
||||
@@ -92,14 +92,14 @@ export class SubtitleParser {
|
||||
};
|
||||
|
||||
let meta: SubtitleMetadata = {};
|
||||
if (!this.#preambleEmitted) {
|
||||
if (!this.preambleEmitted) {
|
||||
meta.config = {
|
||||
description: this.#preambleText
|
||||
description: this.preambleText
|
||||
};
|
||||
this.#preambleEmitted = true;
|
||||
this.preambleEmitted = true;
|
||||
}
|
||||
|
||||
this.#options.output(cue, meta);
|
||||
this.options.output(cue, meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+78
-78
@@ -21,58 +21,58 @@ export abstract class Writer {
|
||||
* written to the ArrayBufferTarget once the muxing finishes.
|
||||
*/
|
||||
export class ArrayBufferTargetWriter extends Writer {
|
||||
#pos = 0;
|
||||
#target: ArrayBufferTarget;
|
||||
#buffer = new ArrayBuffer(2**16);
|
||||
#bytes = new Uint8Array(this.#buffer);
|
||||
#maxPos = 0;
|
||||
private pos = 0;
|
||||
private target: ArrayBufferTarget;
|
||||
private buffer = new ArrayBuffer(2**16);
|
||||
private bytes = new Uint8Array(this.buffer);
|
||||
private maxPos = 0;
|
||||
|
||||
constructor(target: ArrayBufferTarget) {
|
||||
super();
|
||||
|
||||
this.#target = target;
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
#ensureSize(size: number) {
|
||||
let newLength = this.#buffer.byteLength;
|
||||
private ensureSize(size: number) {
|
||||
let newLength = this.buffer.byteLength;
|
||||
while (newLength < size) newLength *= 2;
|
||||
|
||||
if (newLength === this.#buffer.byteLength) return;
|
||||
if (newLength === this.buffer.byteLength) return;
|
||||
|
||||
let newBuffer = new ArrayBuffer(newLength);
|
||||
let newBytes = new Uint8Array(newBuffer);
|
||||
newBytes.set(this.#bytes, 0);
|
||||
newBytes.set(this.bytes, 0);
|
||||
|
||||
this.#buffer = newBuffer;
|
||||
this.#bytes = newBytes;
|
||||
this.buffer = newBuffer;
|
||||
this.bytes = newBytes;
|
||||
}
|
||||
|
||||
write(data: Uint8Array) {
|
||||
this.#ensureSize(this.#pos + data.byteLength);
|
||||
this.ensureSize(this.pos + data.byteLength);
|
||||
|
||||
this.#bytes.set(data, this.#pos);
|
||||
this.#pos += data.byteLength;
|
||||
this.bytes.set(data, this.pos);
|
||||
this.pos += data.byteLength;
|
||||
|
||||
this.#maxPos = Math.max(this.#maxPos, this.#pos);
|
||||
this.maxPos = Math.max(this.maxPos, this.pos);
|
||||
}
|
||||
|
||||
seek(newPos: number) {
|
||||
this.#pos = newPos;
|
||||
this.pos = newPos;
|
||||
}
|
||||
|
||||
getPos() {
|
||||
return this.#pos;
|
||||
return this.pos;
|
||||
}
|
||||
|
||||
flush() {}
|
||||
|
||||
finalize() {
|
||||
this.#ensureSize(this.#pos);
|
||||
this.#target.buffer = this.#buffer.slice(0, Math.max(this.#maxPos, this.#pos));
|
||||
this.ensureSize(this.pos);
|
||||
this.target.buffer = this.buffer.slice(0, Math.max(this.maxPos, this.pos));
|
||||
}
|
||||
|
||||
getSlice(start: number, end: number) {
|
||||
return this.#bytes.slice(start, end);
|
||||
return this.bytes.slice(start, end);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,45 +81,45 @@ export class ArrayBufferTargetWriter extends Writer {
|
||||
* last flush. This is useful for streaming applications, like piping the output to disk.
|
||||
*/
|
||||
export class StreamTargetWriter extends Writer {
|
||||
#pos = 0;
|
||||
#target: StreamTarget;
|
||||
#sections: {
|
||||
private pos = 0;
|
||||
private target: StreamTarget;
|
||||
private sections: {
|
||||
data: Uint8Array,
|
||||
start: number
|
||||
}[] = [];
|
||||
#lastFlushEnd = 0;
|
||||
private lastFlushEnd = 0;
|
||||
|
||||
constructor(target: StreamTarget) {
|
||||
super();
|
||||
|
||||
this.#target = target;
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
write(data: Uint8Array) {
|
||||
this.#sections.push({
|
||||
this.sections.push({
|
||||
data: data.slice(),
|
||||
start: this.#pos
|
||||
start: this.pos
|
||||
});
|
||||
this.#pos += data.byteLength;
|
||||
this.pos += data.byteLength;
|
||||
}
|
||||
|
||||
seek(newPos: number) {
|
||||
this.#pos = newPos;
|
||||
this.pos = newPos;
|
||||
}
|
||||
|
||||
getPos() {
|
||||
return this.#pos;
|
||||
return this.pos;
|
||||
}
|
||||
|
||||
flush() {
|
||||
if (this.#sections.length === 0) return;
|
||||
if (this.sections.length === 0) return;
|
||||
|
||||
let chunks: {
|
||||
start: number,
|
||||
size: number,
|
||||
data?: Uint8Array
|
||||
}[] = [];
|
||||
let sorted = [...this.#sections].sort((a, b) => a.start - b.start);
|
||||
let sorted = [...this.sections].sort((a, b) => a.start - b.start);
|
||||
|
||||
chunks.push({
|
||||
start: sorted[0]!.start,
|
||||
@@ -145,22 +145,22 @@ export class StreamTargetWriter extends Writer {
|
||||
chunk.data = new Uint8Array(chunk.size);
|
||||
|
||||
// Make sure to write the data in the correct order for correct overwriting
|
||||
for (let section of this.#sections) {
|
||||
for (let section of this.sections) {
|
||||
// Check if the section is in the chunk
|
||||
if (chunk.start <= section.start && section.start < chunk.start + chunk.size) {
|
||||
chunk.data.set(section.data, section.start - chunk.start);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.ensureMonotonicity && chunk.start !== this.#lastFlushEnd) {
|
||||
if (this.ensureMonotonicity && chunk.start !== this.lastFlushEnd) {
|
||||
throw new Error('Internal error: Monotonicity violation.');
|
||||
}
|
||||
|
||||
this.#target.options.onData?.(chunk.data, chunk.start);
|
||||
this.#lastFlushEnd = chunk.start + chunk.data.byteLength;
|
||||
this.target.options.onData?.(chunk.data, chunk.start);
|
||||
this.lastFlushEnd = chunk.start + chunk.data.byteLength;
|
||||
}
|
||||
|
||||
this.#sections.length = 0;
|
||||
this.sections.length = 0;
|
||||
}
|
||||
|
||||
finalize() {}
|
||||
@@ -186,51 +186,51 @@ interface ChunkSection {
|
||||
* size, which is when it is piped to the StreamTarget. This is helpful for reducing the total amount of writes.
|
||||
*/
|
||||
export class ChunkedStreamTargetWriter extends Writer {
|
||||
#pos = 0;
|
||||
#target: StreamTarget;
|
||||
#chunkSize: number;
|
||||
private pos = 0;
|
||||
private target: StreamTarget;
|
||||
private chunkSize: number;
|
||||
/**
|
||||
* The data is divided up into fixed-size chunks, whose contents are first filled in RAM and then flushed out.
|
||||
* A chunk is flushed if all of its contents have been written.
|
||||
*/
|
||||
#chunks: Chunk[] = [];
|
||||
#lastFlushEnd = 0;
|
||||
private chunks: Chunk[] = [];
|
||||
private lastFlushEnd = 0;
|
||||
|
||||
constructor(target: StreamTarget) {
|
||||
super();
|
||||
|
||||
this.#target = target;
|
||||
this.#chunkSize = target.options?.chunkSize ?? DEFAULT_CHUNK_SIZE;
|
||||
this.target = target;
|
||||
this.chunkSize = target.options?.chunkSize ?? DEFAULT_CHUNK_SIZE;
|
||||
|
||||
if (!Number.isInteger(this.#chunkSize) || this.#chunkSize < 2**10) {
|
||||
if (!Number.isInteger(this.chunkSize) || this.chunkSize < 2**10) {
|
||||
throw new Error('Invalid StreamTarget options: chunkSize must be an integer not smaller than 1024.');
|
||||
}
|
||||
}
|
||||
|
||||
write(data: Uint8Array) {
|
||||
this.#writeDataIntoChunks(data, this.#pos);
|
||||
this.#flushChunks();
|
||||
this.writeDataIntoChunks(data, this.pos);
|
||||
this.flushChunks();
|
||||
|
||||
this.#pos += data.byteLength;
|
||||
this.pos += data.byteLength;
|
||||
}
|
||||
|
||||
seek(newPos: number) {
|
||||
this.#pos = newPos;
|
||||
this.pos = newPos;
|
||||
}
|
||||
|
||||
getPos() {
|
||||
return this.#pos;
|
||||
return this.pos;
|
||||
}
|
||||
|
||||
#writeDataIntoChunks(data: Uint8Array, position: number) {
|
||||
private writeDataIntoChunks(data: Uint8Array, position: number) {
|
||||
// First, find the chunk to write the data into, or create one if none exists
|
||||
let chunkIndex = this.#chunks.findIndex(x => x.start <= position && position < x.start + this.#chunkSize);
|
||||
if (chunkIndex === -1) chunkIndex = this.#createChunk(position);
|
||||
let chunk = this.#chunks[chunkIndex]!;
|
||||
let chunkIndex = this.chunks.findIndex(x => x.start <= position && position < x.start + this.chunkSize);
|
||||
if (chunkIndex === -1) chunkIndex = this.createChunk(position);
|
||||
let chunk = this.chunks[chunkIndex]!;
|
||||
|
||||
// Figure out how much to write to the chunk, and then write to the chunk
|
||||
let relativePosition = position - chunk.start;
|
||||
let toWrite = data.subarray(0, Math.min(this.#chunkSize - relativePosition, data.byteLength));
|
||||
let toWrite = data.subarray(0, Math.min(this.chunkSize - relativePosition, data.byteLength));
|
||||
chunk.data.set(toWrite, relativePosition);
|
||||
|
||||
// Create a section describing the region of data that was just written to
|
||||
@@ -238,29 +238,29 @@ export class ChunkedStreamTargetWriter extends Writer {
|
||||
start: relativePosition,
|
||||
end: relativePosition + toWrite.byteLength
|
||||
};
|
||||
this.#insertSectionIntoChunk(chunk, section);
|
||||
this.insertSectionIntoChunk(chunk, section);
|
||||
|
||||
// Queue chunk for flushing to target if it has been fully written to
|
||||
if (chunk.written[0]!.start === 0 && chunk.written[0]!.end === this.#chunkSize) {
|
||||
if (chunk.written[0]!.start === 0 && chunk.written[0]!.end === this.chunkSize) {
|
||||
chunk.shouldFlush = true;
|
||||
}
|
||||
|
||||
// Make sure we don't hold too many chunks in memory at once to keep memory usage down
|
||||
if (this.#chunks.length > MAX_CHUNKS_AT_ONCE) {
|
||||
if (this.chunks.length > MAX_CHUNKS_AT_ONCE) {
|
||||
// Flush all but the last chunk
|
||||
for (let i = 0; i < this.#chunks.length-1; i++) {
|
||||
this.#chunks[i]!.shouldFlush = true;
|
||||
for (let i = 0; i < this.chunks.length-1; i++) {
|
||||
this.chunks[i]!.shouldFlush = true;
|
||||
}
|
||||
this.#flushChunks();
|
||||
this.flushChunks();
|
||||
}
|
||||
|
||||
// If the data didn't fit in one chunk, recurse with the remaining datas
|
||||
if (toWrite.byteLength < data.byteLength) {
|
||||
this.#writeDataIntoChunks(data.subarray(toWrite.byteLength), position + toWrite.byteLength);
|
||||
this.writeDataIntoChunks(data.subarray(toWrite.byteLength), position + toWrite.byteLength);
|
||||
}
|
||||
}
|
||||
|
||||
#insertSectionIntoChunk(chunk: Chunk, section: ChunkSection) {
|
||||
private insertSectionIntoChunk(chunk: Chunk, section: ChunkSection) {
|
||||
let low = 0;
|
||||
let high = chunk.written.length - 1;
|
||||
let index = -1;
|
||||
@@ -288,37 +288,37 @@ export class ChunkedStreamTargetWriter extends Writer {
|
||||
}
|
||||
}
|
||||
|
||||
#createChunk(includesPosition: number) {
|
||||
let start = Math.floor(includesPosition / this.#chunkSize) * this.#chunkSize;
|
||||
private createChunk(includesPosition: number) {
|
||||
let start = Math.floor(includesPosition / this.chunkSize) * this.chunkSize;
|
||||
let chunk: Chunk = {
|
||||
start,
|
||||
data: new Uint8Array(this.#chunkSize),
|
||||
data: new Uint8Array(this.chunkSize),
|
||||
written: [],
|
||||
shouldFlush: false
|
||||
};
|
||||
this.#chunks.push(chunk);
|
||||
this.#chunks.sort((a, b) => a.start - b.start);
|
||||
this.chunks.push(chunk);
|
||||
this.chunks.sort((a, b) => a.start - b.start);
|
||||
|
||||
return this.#chunks.indexOf(chunk);
|
||||
return this.chunks.indexOf(chunk);
|
||||
}
|
||||
|
||||
#flushChunks(force = false) {
|
||||
for (let i = 0; i < this.#chunks.length; i++) {
|
||||
let chunk = this.#chunks[i]!;
|
||||
private flushChunks(force = false) {
|
||||
for (let i = 0; i < this.chunks.length; i++) {
|
||||
let chunk = this.chunks[i]!;
|
||||
if (!chunk.shouldFlush && !force) continue;
|
||||
|
||||
for (let section of chunk.written) {
|
||||
if (this.ensureMonotonicity && chunk.start + section.start !== this.#lastFlushEnd) {
|
||||
if (this.ensureMonotonicity && chunk.start + section.start !== this.lastFlushEnd) {
|
||||
throw new Error('Internal error: Monotonicity violation.');
|
||||
}
|
||||
|
||||
this.#target.options.onData?.(
|
||||
this.target.options.onData?.(
|
||||
chunk.data.subarray(section.start, section.end),
|
||||
chunk.start + section.start
|
||||
);
|
||||
this.#lastFlushEnd = chunk.start + section.end;
|
||||
this.lastFlushEnd = chunk.start + section.end;
|
||||
}
|
||||
this.#chunks.splice(i--, 1);
|
||||
this.chunks.splice(i--, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,7 +327,7 @@ export class ChunkedStreamTargetWriter extends Writer {
|
||||
}
|
||||
|
||||
finalize() {
|
||||
this.#flushChunks(true);
|
||||
this.flushChunks(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user