mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 02:43:48 +02:00
Add specific, per-output-format data callbacks & rewrite some logic in the process
This commit is contained in:
+14
-5
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -19,8 +19,11 @@ export {
|
||||
WebMOutputFormat,
|
||||
WebMOutputFormatOptions,
|
||||
Mp3OutputFormat,
|
||||
Mp3OutputFormatOptions,
|
||||
WaveOutputFormat,
|
||||
WaveOutputFormatOptions,
|
||||
OggOutputFormat,
|
||||
OggOutputFormatOptions,
|
||||
TrackCountLimits,
|
||||
InclusiveIntegerRange,
|
||||
} from './output-format';
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user