Add specific, per-output-format data callbacks & rewrite some logic in the process

This commit is contained in:
Vanilagy
2025-03-22 20:30:24 +01:00
parent bbc2e088ca
commit ceb2528014
14 changed files with 458 additions and 100 deletions
+14 -5
View File
@@ -16,9 +16,9 @@
const source = new Metamuxer.BlobSource(file);
const target = new Metamuxer.BufferTarget();
const outputFormat = new Metamuxer.Mp4OutputFormat({
fastStart: 'fragmented'
})
const outputFormat = new Metamuxer.OggOutputFormat({
onPage: console.log
});
const button = document.createElement('button');
button.textContent = 'Cancel';
@@ -75,13 +75,22 @@
},
trim: {
start: 0,
end: 10
end: 60
},
computeProgress: true
});
console.log(conversion);
conversion.onProgress = () => progress.value = conversion.progress;
function updateProgress() {
progress.value = conversion.progress;
if (conversion.progress === 1) {
return;
}
setTimeout(updateProgress, 1000/60);
}
updateProgress();
console.time();
await conversion.execute();
+6
View File
@@ -374,6 +374,12 @@ export class Conversion {
}
}
const unintentionallyDiscardedTracks = this.discardedTracks.filter(x => x.reason !== 'discardedByUser');
if (unintentionallyDiscardedTracks.length > 0) {
// Let's give the user a notice/warning about discarded tracks so they aren't confused
console.warn('Some tracks had to be discarded from the conversion:', unintentionallyDiscardedTracks);
}
if (this._options.computeProgress) {
this._totalDuration = Math.min(
await this._input.computeDuration() - this._startTimestamp,
+3
View File
@@ -19,8 +19,11 @@ export {
WebMOutputFormat,
WebMOutputFormatOptions,
Mp3OutputFormat,
Mp3OutputFormatOptions,
WaveOutputFormat,
WaveOutputFormatOptions,
OggOutputFormat,
OggOutputFormatOptions,
TrackCountLimits,
InclusiveIntegerRange,
} from './output-format';
-3
View File
@@ -317,9 +317,6 @@ export const ftyp = (details: {
/** Movie Sample Data Box. Contains the actual frames/samples of the media. */
export const mdat = (reserveLargeSize: boolean): Box => ({ type: 'mdat', largeSize: reserveLargeSize });
/** Free Space Box: A box that designates unused space in the movie data file. */
export const free = (size: number): Box => ({ type: 'free', size });
/**
* Movie Box: Used to specify the information that defines a movie - that is, the information that allows
* an application to interpret the sample data that is stored elsewhere.
+95 -36
View File
@@ -1,4 +1,4 @@
import { Box, free, ftyp, IsobmffBoxWriter, mdat, mfra, moof, moov, vtta, vttc, vtte } from './isobmff-boxes';
import { Box, ftyp, IsobmffBoxWriter, mdat, mfra, moof, moov, vtta, vttc, vtte } from './isobmff-boxes';
import { Muxer } from '../muxer';
import { Output, OutputAudioTrack, OutputSubtitleTrack, OutputTrack, OutputVideoTrack } from '../output';
import { BufferTargetWriter, Writer } from '../writer';
@@ -97,6 +97,7 @@ export const intoTimescale = (timeInSeconds: number, timescale: number, round =
};
export class IsobmffMuxer extends Muxer {
private format: IsobmffOutputFormat;
private writer: Writer;
private boxWriter: IsobmffBoxWriter;
private isMov: boolean;
@@ -107,7 +108,6 @@ export class IsobmffMuxer extends Muxer {
private auxWriter = this.auxTarget._createWriter();
private auxBoxWriter = new IsobmffBoxWriter(this.auxWriter);
private ftypSize: number | null = null;
private mdat: Box | null = null;
private trackDatas: IsobmffTrackData[] = [];
@@ -123,6 +123,7 @@ export class IsobmffMuxer extends Muxer {
constructor(output: Output, format: IsobmffOutputFormat) {
super(output);
this.format = format;
this.writer = output._writer;
this.boxWriter = new IsobmffBoxWriter(this.writer);
@@ -147,19 +148,32 @@ export class IsobmffMuxer extends Muxer {
const holdsAvc = this.output._tracks.some(x => x.type === 'video' && x.source._codec === 'avc');
// Write the header
{
if (this.format._options.onFtyp) {
this.writer.startTrackingWrites();
}
this.boxWriter.writeBox(ftyp({
isMov: this.isMov,
holdsAvc: holdsAvc,
fragmented: this.isFragmented,
}));
this.ftypSize = this.writer.getPos();
if (this.format._options.onFtyp) {
const { data, start } = this.writer.stopTrackingWrites();
this.format._options.onFtyp(data, start);
}
}
if (this.fastStart === 'in-memory') {
this.mdat = mdat(false);
} else if (this.isFragmented) {
// We write the moov box once we write out the first fragment to make sure we get the decoder configs
} else {
if (this.format._options.onMdat) {
this.writer.startTrackingWrites();
}
this.mdat = mdat(true); // Reserve large size by default, can refine this when finalizing.
this.boxWriter.writeBox(this.mdat);
}
@@ -802,59 +816,82 @@ export class IsobmffMuxer extends Muxer {
const fragmentNumber = this.nextFragmentNumber++;
if (fragmentNumber === 1) {
if (this.format._options.onMoov) {
this.writer.startTrackingWrites();
}
// Write the moov box now that we have all decoder configs
const movieBox = moov(this.trackDatas, this.creationTime, true);
this.boxWriter.writeBox(movieBox);
if (this.format._options.onMoov) {
const { data, start } = this.writer.stopTrackingWrites();
this.format._options.onMoov(data, start);
}
}
// Not all tracks need to be present in every fragment
const tracksInFragment = this.trackDatas.filter(x => x.currentChunk);
// Write out an initial moof box; will be overwritten later once actual chunk offsets are known
const moofOffset = this.writer.getPos();
// Create an initial moof box and measure it; we need this to know where the following mdat box will begin
const moofBox = moof(fragmentNumber, tracksInFragment);
this.boxWriter.writeBox(moofBox);
const moofOffset = this.writer.getPos();
const mdatStartPos = moofOffset + this.boxWriter.measureBox(moofBox);
// Create the mdat box
{
const mdatBox = mdat(false); // Initially assume the fragment is not larger than 4 GiB
let totalTrackSampleSize = 0;
// Header with large size. We always reserve 16 bytes for it even if we don't end up using the large size.
const mdatHeaderSize = 16;
// Compute the size of the mdat box
let currentPos = mdatStartPos + mdatHeaderSize;
let fragmentStartTimestamp = Infinity;
for (const trackData of tracksInFragment) {
trackData.currentChunk!.offset = currentPos;
trackData.currentChunk!.moofOffset = moofOffset;
for (const sample of trackData.currentChunk!.samples) {
totalTrackSampleSize += sample.size;
}
currentPos += sample.size;
}
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;
fragmentStartTimestamp = Math.min(fragmentStartTimestamp, trackData.currentChunk!.startTimestamp);
}
const mdatSize = currentPos - mdatStartPos;
if (this.format._options.onMoof) {
this.writer.startTrackingWrites();
}
const newMoofBox = moof(fragmentNumber, tracksInFragment);
this.boxWriter.writeBox(newMoofBox);
if (this.format._options.onMoof) {
const { data, start } = this.writer.stopTrackingWrites();
this.format._options.onMoof(data, start, fragmentStartTimestamp);
}
assert(this.writer.getPos() === mdatStartPos);
if (this.format._options.onMdat) {
this.writer.startTrackingWrites();
}
const mdatBox = mdat(mdatSize >= 2 ** 32);
mdatBox.size = mdatSize;
this.boxWriter.writeBox(mdatBox);
}
this.writer.seek(mdatStartPos + mdatHeaderSize);
// Write sample data
for (const trackData of tracksInFragment) {
trackData.currentChunk!.offset = this.writer.getPos();
trackData.currentChunk!.moofOffset = moofOffset;
for (const sample of trackData.currentChunk!.samples) {
this.writer.write(sample.data!);
sample.data = null; // Can be GC'd
}
}
// Now that we set the actual chunk offsets, fix the moof box
const endPos = this.writer.getPos();
this.writer.seek(this.boxWriter.offsets.get(moofBox)!);
const newMoofBox = moof(fragmentNumber, tracksInFragment);
this.boxWriter.writeBox(newMoofBox);
this.writer.seek(endPos);
if (this.format._options.onMdat) {
const { data, start } = this.writer.stopTrackingWrites();
this.format._options.onMdat(data, start);
}
for (const trackData of tracksInFragment) {
trackData.finalizedChunks.push(trackData.currentChunk!);
@@ -937,9 +974,22 @@ export class IsobmffMuxer extends Muxer {
if (mdatSize >= 2 ** 32) this.mdat.largeSize = true;
}
if (this.format._options.onMoov) {
this.writer.startTrackingWrites();
}
const movieBox = moov(this.trackDatas, this.creationTime);
this.boxWriter.writeBox(movieBox);
if (this.format._options.onMoov) {
const { data, start } = this.writer.stopTrackingWrites();
this.format._options.onMoov(data, start);
}
if (this.format._options.onMdat) {
this.writer.startTrackingWrites();
}
this.mdat.size = mdatSize!;
this.boxWriter.writeBox(this.mdat);
@@ -950,6 +1000,11 @@ export class IsobmffMuxer extends Muxer {
sample.data = null;
}
}
if (this.format._options.onMdat) {
const { data, start } = this.writer.stopTrackingWrites();
this.format._options.onMdat(data, start);
}
} else if (this.isFragmented) {
// Append the mfra box to the end of the file for better random access
const startPos = this.writer.getPos();
@@ -962,7 +1017,6 @@ export class IsobmffMuxer extends Muxer {
this.boxWriter.writeU32(mfraBoxSize);
} else {
assert(this.mdat);
assert(this.ftypSize !== null);
const mdatPos = this.boxWriter.offsets.get(this.mdat);
assert(mdatPos !== undefined);
@@ -971,16 +1025,21 @@ export class IsobmffMuxer extends Muxer {
this.mdat.largeSize = mdatSize >= 2 ** 32; // Only use the large size if we need it
this.boxWriter.patchBox(this.mdat);
if (this.format._options.onMdat) {
const { data, start } = this.writer.stopTrackingWrites();
this.format._options.onMdat(data, start);
}
if (this.format._options.onMoov) {
this.writer.startTrackingWrites();
}
const movieBox = moov(this.trackDatas, this.creationTime);
if (typeof this.fastStart === 'object') {
this.writer.seek(this.ftypSize);
this.boxWriter.writeBox(movieBox);
const remainingBytes = mdatPos - this.writer.getPos();
this.boxWriter.writeBox(free(remainingBytes));
} else {
this.boxWriter.writeBox(movieBox);
if (this.format._options.onMoov) {
const { data, start } = this.writer.stopTrackingWrites();
this.format._options.onMoov(data, start);
}
}
+29 -17
View File
@@ -168,6 +168,10 @@ export class MatroskaMuxer extends Muxer {
}
private writeEBMLHeader() {
if (this.format._options.onEbmlHeader) {
this.writer.startTrackingWrites();
}
const ebmlHeader: EBML = { id: EBMLId.EBML, data: [
{ id: EBMLId.EBMLVersion, data: 1 },
{ id: EBMLId.EBMLReadVersion, data: 1 },
@@ -178,6 +182,11 @@ export class MatroskaMuxer extends Muxer {
{ id: EBMLId.DocTypeReadVersion, data: 2 },
] };
this.ebmlWriter.writeEBML(ebmlHeader);
if (this.format._options.onEbmlHeader) {
const { data, start } = this.writer.stopTrackingWrites(); // start should be 0
this.format._options.onEbmlHeader(data, start);
}
}
/**
@@ -365,14 +374,16 @@ export class MatroskaMuxer extends Muxer {
};
this.segment = segment;
if (this.format._options.onSegmentHeader) {
this.writer.startTrackingWrites();
}
this.ebmlWriter.writeEBML(segment);
/*
if (this.#writer instanceof BaseStreamTargetWriter && this.#writer.target.options.onHeader) {
let { data, start } = this.#writer.getTrackedWrites(); // start should be 0
this.#writer.target.options.onHeader(data, start);
if (this.format._options.onSegmentHeader) {
const { data, start } = this.writer.stopTrackingWrites();
this.format._options.onSegmentHeader(data, start);
}
*/
}
private createCues() {
@@ -776,15 +787,13 @@ export class MatroskaMuxer extends Muxer {
/** Creates a new Cluster element to contain media chunks. */
private createNewCluster(msTimestamp: number) {
if (this.currentCluster && !this.format._options.streamable) {
if (this.currentCluster) {
this.finalizeCurrentCluster();
}
/*
if (this.#writer instanceof BaseStreamTargetWriter && this.#writer.target.options.onCluster) {
this.#writer.startTrackingWrites();
if (this.format._options.onCluster) {
this.writer.startTrackingWrites();
}
*/
this.currentCluster = {
id: EBMLId.Cluster,
@@ -802,6 +811,8 @@ export class MatroskaMuxer extends Muxer {
private finalizeCurrentCluster() {
assert(this.currentCluster);
if (!this.format._options.streamable) {
const clusterSize = this.writer.getPos() - this.ebmlWriter.dataOffsets.get(this.currentCluster)!;
const endPos = this.writer.getPos();
@@ -809,13 +820,14 @@ export class MatroskaMuxer extends Muxer {
this.writer.seek(this.ebmlWriter.offsets.get(this.currentCluster)! + 4);
this.ebmlWriter.writeVarInt(clusterSize, CLUSTER_SIZE_BYTES);
this.writer.seek(endPos);
/*
if (this.#writer instanceof BaseStreamTargetWriter && this.#writer.target.options.onCluster) {
let { data, start } = this.#writer.getTrackedWrites();
this.#writer.target.options.onCluster(data, start, this.#currentClusterTimestamp);
}
*/
if (this.format._options.onCluster) {
assert(this.currentClusterStartMsTimestamp !== null);
const { data, start } = this.writer.stopTrackingWrites();
this.format._options.onCluster(data, start, this.currentClusterStartMsTimestamp / 1000);
}
const clusterOffsetFromSegment
= this.ebmlWriter.offsets.get(this.currentCluster)! - this.segmentDataOffset;
@@ -869,7 +881,7 @@ export class MatroskaMuxer extends Muxer {
// Flush any remaining queued chunks to the file
await this.interleaveChunks(true);
if (!this.format._options.streamable && this.currentCluster) {
if (this.currentCluster) {
this.finalizeCurrentCluster();
}
+13 -1
View File
@@ -1,21 +1,24 @@
import { assert, toDataView } from '../misc';
import { Muxer } from '../muxer';
import { Output, OutputAudioTrack } from '../output';
import { Mp3OutputFormat } from '../output-format';
import { EncodedPacket } from '../packet';
import { Writer } from '../writer';
import { getXingOffset, INFO, readFrameHeader, XING } from './mp3-misc';
import { Mp3Writer, XingFrameData } from './mp3-writer';
export class Mp3Muxer extends Muxer {
private format: Mp3OutputFormat;
private writer: Writer;
private mp3Writer: Mp3Writer;
private xingFrameData: XingFrameData | null = null;
private frameCount = 0;
private framePositions: number[] = [];
constructor(output: Output) {
constructor(output: Output, format: Mp3OutputFormat) {
super(output);
this.format = format;
this.writer = output._writer;
this.mp3Writer = new Mp3Writer(output._writer);
}
@@ -120,8 +123,17 @@ export class Mp3Muxer extends Muxer {
this.xingFrameData.fileSize = endPos;
this.xingFrameData.toc = toc;
if (this.format._options.onXingFrame) {
this.writer.startTrackingWrites();
}
this.mp3Writer.writeXingFrame(this.xingFrameData);
if (this.format._options.onXingFrame) {
const { data, start } = this.writer.stopTrackingWrites();
this.format._options.onXingFrame(data, start);
}
this.writer.seek(endPos);
release();
+13 -1
View File
@@ -2,6 +2,7 @@ import { OPUS_INTERNAL_SAMPLE_RATE, parseOpusIdentificationHeader } from '../cod
import { assert, setInt64, toDataView, toUint8Array } from '../misc';
import { Muxer } from '../muxer';
import { Output, OutputAudioTrack } from '../output';
import { OggOutputFormat } from '../output-format';
import { EncodedPacket } from '../packet';
import { Writer } from '../writer';
import {
@@ -40,6 +41,7 @@ type Packet = {
};
export class OggMuxer extends Muxer {
private format: OggOutputFormat;
private writer: Writer;
private trackDatas: OggTrackData[] = [];
@@ -48,9 +50,10 @@ export class OggMuxer extends Muxer {
private pageBytes = new Uint8Array(MAX_PAGE_SIZE);
private pageView = new DataView(this.pageBytes.buffer);
constructor(output: Output) {
constructor(output: Output, format: OggOutputFormat) {
super(output);
this.format = format;
this.writer = output._writer;
}
@@ -399,7 +402,16 @@ export class OggMuxer extends Muxer {
trackData.currentPageSize = 27;
trackData.currentPageStartsWithFreshPacket = true;
if (this.format._options.onPage) {
this.writer.startTrackingWrites();
}
this.writer.write(slice);
if (this.format._options.onPage) {
const { data, start } = this.writer.stopTrackingWrites();
this.format._options.onPage(data, start, trackData.track.source);
}
}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
+178 -6
View File
@@ -11,6 +11,7 @@ import {
} from './codec';
import { IsobmffMuxer } from './isobmff/isobmff-muxer';
import { MatroskaMuxer } from './matroska/matroska-muxer';
import { MediaSource } from './media-source';
import { Mp3Muxer } from './mp3/mp3-muxer';
import { Muxer } from './muxer';
import { OggMuxer } from './ogg/ogg-muxer';
@@ -84,7 +85,7 @@ export abstract class OutputFormat {
}
/**
* Options controlling the format of an output ISOBMFF file.
* ISOBMFF-specific output options.
* @public
*/
export type IsobmffOutputFormatOptions = {
@@ -116,6 +117,41 @@ export type IsobmffOutputFormatOptions = {
* New fragments will only be created when the current fragment is longer than this value. Defaults to 1 second.
*/
minimumFragmentDuration?: number;
/**
* Will be called once the ftyp (File Type) box of the output file has been written.
*
* @param data - The raw bytes.
* @param position - The byte offset of the data in the file.
*/
onFtyp?: (data: Uint8Array, position: number) => unknown;
/**
* Will be called once the moov (Movie) box of the output file has been written.
*
* @param data - The raw bytes.
* @param position - The byte offset of the data in the file.
*/
onMoov?: (data: Uint8Array, position: number) => unknown;
/**
* Will be called for each finalized mdat (Media Data) box of the output file. Usage of this callback is not
* recommended when not using `fastStart: 'fragmented'`, as there will be one monolithic mdat box which might
* require large amounts of memory.
*
* @param data - The raw bytes.
* @param position - The byte offset of the data in the file.
*/
onMdat?: (data: Uint8Array, position: number) => unknown;
/**
* Will be called for each finalized moof (Movie Fragment) box of the output file.
*
* @param data - The raw bytes.
* @param position - The byte offset of the data in the file.
* @param timestamp - The start timestamp of the fragment in seconds.
*/
onMoof?: (data: Uint8Array, position: number, timestamp: number) => unknown;
};
/**
@@ -139,6 +175,18 @@ export abstract class IsobmffOutputFormat extends OutputFormat {
) {
throw new TypeError('options.minimumFragmentDuration, when provided, must be a non-negative number.');
}
if (options.onFtyp !== undefined && typeof options.onFtyp !== 'function') {
throw new TypeError('options.onFtyp, when provided, must be a function.');
}
if (options.onMoov !== undefined && typeof options.onMoov !== 'function') {
throw new TypeError('options.onMoov, when provided, must be a function.');
}
if (options.onMdat !== undefined && typeof options.onMdat !== 'function') {
throw new TypeError('options.onMdat, when provided, must be a function.');
}
if (options.onMoof !== undefined && typeof options.onMoof !== 'function') {
throw new TypeError('options.onMoof, when provided, must be a function.');
}
super();
@@ -228,7 +276,7 @@ export class MovOutputFormat extends IsobmffOutputFormat {
}
/**
* Options controlling the format of an output Matroska file.
* Matroska-specific output options.
* @public
*/
export type MkvOutputFormatOptions = {
@@ -244,6 +292,32 @@ export type MkvOutputFormatOptions = {
* when the current cluster is longer than this value. Defaults to 1 second.
*/
minimumClusterDuration?: number;
/**
* Will be called once the EBML header of the output file has been written.
*
* @param data - The raw bytes.
* @param position - The byte offset of the data in the file.
*/
onEbmlHeader?: (data: Uint8Array, position: number) => void;
/**
* Will be called once the header part of the Matroska Segment element has been written. The header data includes
* the Segment element and everything inside it, up to (but excluding) the first Matroska Cluster.
*
* @param data - The raw bytes.
* @param position - The byte offset of the data in the file.
*/
onSegmentHeader?: (data: Uint8Array, position: number) => unknown;
/**
* Will be called for each finalized Matroska Cluster of the output file.
*
* @param data - The raw bytes.
* @param position - The byte offset of the data in the file.
* @param timestamp - The start timestamp of the cluster in seconds.
*/
onCluster?: (data: Uint8Array, position: number, timestamp: number) => unknown;
};
/**
@@ -267,6 +341,15 @@ export class MkvOutputFormat extends OutputFormat {
) {
throw new TypeError('options.minimumClusterDuration, when provided, must be a non-negative number.');
}
if (options.onEbmlHeader !== undefined && typeof options.onEbmlHeader !== 'function') {
throw new TypeError('options.onEbmlHeader, when provided, must be a function.');
}
if (options.onSegmentHeader !== undefined && typeof options.onSegmentHeader !== 'function') {
throw new TypeError('options.onHeader, when provided, must be a function.');
}
if (options.onCluster !== undefined && typeof options.onCluster !== 'function') {
throw new TypeError('options.onCluster, when provided, must be a function.');
}
super();
@@ -312,7 +395,7 @@ export class MkvOutputFormat extends OutputFormat {
}
/**
* Options controlling the format of an output WebM file.
* WebM-specific output options.
* @public
*/
export type WebMOutputFormatOptions = MkvOutputFormatOptions;
@@ -349,14 +432,44 @@ export class WebMOutputFormat extends MkvOutputFormat {
}
}
/**
* MP3-specific output options.
* @public
*/
export type Mp3OutputFormatOptions = {
/**
* Will be called once the Xing metadata frame is finalized.
*
* @param data - The raw bytes.
* @param position - The byte offset of the data in the file.
*/
onXingFrame?: (data: Uint8Array, position: number) => unknown;
};
/**
* MP3 file format.
* @public
*/
export class Mp3OutputFormat extends OutputFormat {
/** @internal */
_options: Mp3OutputFormatOptions;
constructor(options: Mp3OutputFormatOptions = {}) {
if (!options || typeof options !== 'object') {
throw new TypeError('options must be an object.');
}
if (options.onXingFrame !== undefined && typeof options.onXingFrame !== 'function') {
throw new TypeError('options.onXingFrame, when provided, must be a function.');
}
super();
this._options = options;
}
/** @internal */
_createMuxer(output: Output) {
return new Mp3Muxer(output);
return new Mp3Muxer(output, this);
}
/** @internal */
@@ -386,14 +499,42 @@ export class Mp3OutputFormat extends OutputFormat {
}
}
/**
* WAVE-specific output options.
* @public
*/
export type WaveOutputFormatOptions = {
/**
* Will be called once the file header is written. The header consists of the RIFF header, the format chunk, and the
* start of the data chunk (with a placeholder size of 0).
*/
onHeader?: (data: Uint8Array, position: number) => unknown;
};
/**
* WAVE file format, based on RIFF.
* @public
*/
export class WaveOutputFormat extends OutputFormat {
/** @internal */
_options: WaveOutputFormatOptions;
constructor(options: WaveOutputFormatOptions = {}) {
if (!options || typeof options !== 'object') {
throw new TypeError('options must be an object.');
}
if (options.onHeader !== undefined && typeof options.onHeader !== 'function') {
throw new TypeError('options.onHeader, when provided, must be a function.');
}
super();
this._options = options;
}
/** @internal */
_createMuxer(output: Output) {
return new WaveMuxer(output);
return new WaveMuxer(output, this);
}
/** @internal */
@@ -427,14 +568,45 @@ export class WaveOutputFormat extends OutputFormat {
}
}
/**
* Ogg-specific output options.
* @public
*/
export type OggOutputFormatOptions = {
/**
* Will be called for each Ogg page that is written.
*
* @param data - The raw bytes.
* @param position - The byte offset of the data in the file.
* @param source - The media source backing the page's logical bitstream (track).
*/
onPage?: (data: Uint8Array, position: number, source: MediaSource) => unknown;
};
/**
* Ogg file format.
* @public
*/
export class OggOutputFormat extends OutputFormat {
/** @internal */
_options: OggOutputFormatOptions;
constructor(options: OggOutputFormatOptions = {}) {
if (!options || typeof options !== 'object') {
throw new TypeError('options must be an object.');
}
if (options.onPage !== undefined && typeof options.onPage !== 'function') {
throw new TypeError('options.onPage, when provided, must be a function.');
}
super();
this._options = options;
}
/** @internal */
_createMuxer(output: Output) {
return new OggMuxer(output);
return new OggMuxer(output, this);
}
/** @internal */
+2 -2
View File
@@ -21,8 +21,8 @@ export class EncodedPacket {
/** The type of this packet. */
public readonly type: PacketType,
/**
* The timestamp of this packet in seconds. May be negative. Samples with negative end timestamps should not
* be presented.
* The presentation timestamp of this packet in seconds. May be negative. Samples with negative end timestamps
* should not be presented.
*/
public readonly timestamp: number,
/** The duration of this packet in seconds. */
+10 -10
View File
@@ -22,7 +22,7 @@ export type VideoSampleInit = {
codedHeight?: number;
/** The rotation of the frame in degrees, clockwise. */
rotation?: Rotation;
/** The timestamp of the frame in seconds. */
/** The presentation timestamp of the frame in seconds. */
timestamp?: number;
/** The duration of the frame in seconds. */
duration?: number;
@@ -50,8 +50,8 @@ export class VideoSample {
/** The rotation of the frame in degrees, clockwise. */
readonly rotation!: Rotation;
/**
* The timestamp of the frame in seconds. May be negative. Frames with negative end timestamps should not
* be presented.
* The presentation timestamp of the frame in seconds. May be negative. Frames with negative end timestamps should
* not be presented.
*/
readonly timestamp!: number;
/** The duration of the frame in seconds. */
@@ -69,7 +69,7 @@ export class VideoSample {
return this.rotation % 180 === 0 ? this.codedHeight : this.codedWidth;
}
/** The timestamp of the frame in microseconds. */
/** The presentation timestamp of the frame in microseconds. */
get microsecondTimestamp() {
return Math.trunc(SECOND_TO_MICROSECOND_FACTOR * this.timestamp);
}
@@ -437,7 +437,7 @@ export class VideoSample {
(this.rotation as Rotation) = newRotation;
}
/** Sets the timestamp of this video sample, in seconds. */
/** Sets the presentation timestamp of this video sample, in seconds. */
setTimestamp(newTimestamp: number) {
if (!Number.isFinite(newTimestamp)) {
throw new TypeError('newTimestamp must be a number.');
@@ -479,7 +479,7 @@ export type AudioSampleInit = {
numberOfChannels: number;
/** The audio sample rate in hertz. */
sampleRate: number;
/** The timestamp of the sample in seconds. */
/** The presentation timestamp of the sample in seconds. */
timestamp: number;
};
@@ -527,12 +527,12 @@ export class AudioSample {
/** The timestamp of the sample in seconds. */
readonly duration: number;
/**
* The timestamp of the sample in seconds. May be negative. Samples with negative end timestamps should not
* be presented.
* The presentation timestamp of the sample in seconds. May be negative. Samples with negative end timestamps should
* not be presented.
*/
readonly timestamp: number;
/** The timestamp of the sample in microseconds. */
/** The presentation timestamp of the sample in microseconds. */
get microsecondTimestamp() {
return Math.trunc(SECOND_TO_MICROSECOND_FACTOR * this.timestamp);
}
@@ -928,7 +928,7 @@ export class AudioSample {
return audioBuffer;
}
/** Sets the timestamp of this audio sample, in seconds. */
/** Sets the presentation timestamp of this audio sample, in seconds. */
setTimestamp(newTimestamp: number) {
if (!Number.isFinite(newTimestamp)) {
throw new TypeError('newTimestamp must be a number.');
+13 -1
View File
@@ -5,16 +5,19 @@ import { WaveFormat } from './wave-demuxer';
import { RiffWriter } from './riff-writer';
import { Writer } from '../writer';
import { EncodedPacket } from '../packet';
import { WaveOutputFormat } from '../output-format';
export class WaveMuxer extends Muxer {
private format: WaveOutputFormat;
private writer: Writer;
private riffWriter: RiffWriter;
private headerWritten = false;
private dataSize = 0;
constructor(output: Output) {
constructor(output: Output, format: WaveOutputFormat) {
super(output);
this.format = format;
this.writer = output._writer;
this.riffWriter = new RiffWriter(output._writer);
}
@@ -60,6 +63,10 @@ export class WaveMuxer extends Muxer {
}
private writeHeader(track: OutputAudioTrack, config: AudioDecoderConfig) {
if (this.format._options.onHeader) {
this.writer.startTrackingWrites();
}
let format: WaveFormat;
const codec = track.source._codec;
@@ -97,6 +104,11 @@ export class WaveMuxer extends Muxer {
// data chunk
this.riffWriter.writeAscii('data');
this.riffWriter.writeU32(0); // Data size placeholder
if (this.format._options.onHeader) {
const { data, start } = this.writer.stopTrackingWrites();
this.format._options.onHeader(data, start);
}
}
async finalize() {
+66
View File
@@ -19,6 +19,66 @@ export abstract class Writer {
abstract finalize(): Promise<void>;
/** Closes the writer. */
abstract close(): Promise<void>;
private trackedWrites: Uint8Array | null = null;
private trackedStart = -1;
private trackedEnd = -1;
protected maybeTrackWrites(data: Uint8Array) {
if (!this.trackedWrites) {
return;
}
// Handle negative relative write positions
let pos = this.getPos();
if (pos < this.trackedStart) {
if (pos + data.byteLength <= this.trackedStart) {
return;
}
data = data.subarray(this.trackedStart - pos);
pos = 0;
}
const neededSize = pos + data.byteLength - this.trackedStart;
let newLength = this.trackedWrites.byteLength;
while (newLength < neededSize) {
newLength *= 2;
}
// Check if we need to resize the buffer
if (newLength !== this.trackedWrites.byteLength) {
const copy = new Uint8Array(newLength);
copy.set(this.trackedWrites, 0);
this.trackedWrites = copy;
}
this.trackedWrites.set(data, pos - this.trackedStart);
this.trackedEnd = Math.max(this.trackedEnd, pos + data.byteLength);
}
startTrackingWrites() {
this.trackedWrites = new Uint8Array(2 ** 10);
this.trackedStart = this.getPos();
this.trackedEnd = this.trackedStart;
}
stopTrackingWrites() {
if (!this.trackedWrites) {
throw new Error('Internal error: Can\'t get tracked writes since nothing was tracked.');
}
const slice = this.trackedWrites.subarray(0, this.trackedEnd - this.trackedStart);
const result = {
data: slice,
start: this.trackedStart,
end: this.trackedEnd,
};
this.trackedWrites = null;
return result;
}
}
const ARRAY_BUFFER_INITIAL_SIZE = 2 ** 16;
@@ -83,6 +143,8 @@ export class BufferTargetWriter extends Writer {
}
write(data: Uint8Array) {
this.maybeTrackWrites(data);
this.ensureSize(this.pos + data.byteLength);
this.bytes.set(data, this.pos);
@@ -139,6 +201,8 @@ export class StreamTargetWriter extends Writer {
}
write(data: Uint8Array) {
this.maybeTrackWrites(data);
this.sections.push({
data: data.slice(),
start: this.pos,
@@ -273,6 +337,8 @@ export class ChunkedStreamTargetWriter extends Writer {
}
write(data: Uint8Array) {
this.maybeTrackWrites(data);
this.writeDataIntoChunks(data, this.pos);
this.queueChunksForFlush();
-2
View File
@@ -1,6 +1,4 @@
- onHeader, etc callbacks for Matroska
- https://github.com/Vanilagy/mp4-muxer/issues/83 tell him it's possible now
- is this fixed? https://github.com/Vanilagy/webm-muxer/issues/50
- cross-track offset for streaming sources
- textsubtitlesource, chunked piping
- More efficient MP3 loading when reading sequentially