Add getMimeType to Output & other small adjustments

This commit is contained in:
Vanilagy
2025-06-07 22:51:57 +02:00
parent 85bd7bdcf4
commit c7c57e2132
21 changed files with 330 additions and 101 deletions
+6 -8
View File
@@ -240,7 +240,7 @@ export const buildVideoCodecString = (codec: VideoCodec, width: number, height:
const bitDepth = '08'; // 8-bit
return `vp09.${profile}.${levelInfo.level.toString().padStart(2, '0')}.${bitDepth}${VP9_DEFAULT_SUFFIX}`;
return `vp09.${profile}.${levelInfo.level.toString().padStart(2, '0')}.${bitDepth}`;
} else if (codec === 'av1') {
const profile = 0; // Main Profile, single digit
@@ -252,7 +252,7 @@ export const buildVideoCodecString = (codec: VideoCodec, width: number, height:
const bitDepth = '08'; // 8-bit
return `av01.${profile}.${level}${levelInfo.tier}.${bitDepth}${AV1_DEFAULT_SUFFIX}`;
return `av01.${profile}.${level}${levelInfo.tier}.${bitDepth}`;
}
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
@@ -426,9 +426,8 @@ export const extractVideoCodecString = (trackInfo: {
let string = `vp09.${profile}.${level}.${bitDepth}.${chromaSubsampling}`;
string += `.${colourPrimaries}.${transferCharacteristics}.${matrixCoefficients}.${videoFullRangeFlag}`;
const defaultSuffix = '.01.01.01.01.00';
if (string.endsWith(defaultSuffix)) {
string = string.slice(0, -defaultSuffix.length);
if (string.endsWith(VP9_DEFAULT_SUFFIX)) {
string = string.slice(0, -VP9_DEFAULT_SUFFIX.length);
}
return string;
@@ -476,9 +475,8 @@ export const extractVideoCodecString = (trackInfo: {
string += `.${matrixCoefficients.toString().padStart(2, '0')}`;
string += `.${videoFullRangeFlag}`;
const defaultSuffix = '.0.110.01.01.01.0';
if (string.endsWith(defaultSuffix)) {
string = string.slice(0, -defaultSuffix.length);
if (string.endsWith(AV1_DEFAULT_SUFFIX)) {
string = string.slice(0, -AV1_DEFAULT_SUFFIX.length);
}
return string;
+19 -18
View File
@@ -111,13 +111,14 @@ const FALLBACK_SAMPLE_RATE = 48000;
* @public
*/
export class Conversion {
/** The input file. */
input: Input;
/** The output file. */
output: Output;
/** @internal */
_options: ConversionOptions;
/** @internal */
_input: Input;
/** @internal */
_output: Output;
/** @internal */
_startTimestamp: number;
/** @internal */
_endTimestamp: number;
@@ -299,8 +300,8 @@ export class Conversion {
}
this._options = options;
this._input = options.input;
this._output = options.output;
this.input = options.input;
this.output = options.output;
this._startTimestamp = options.trim?.start ?? 0;
this._endTimestamp = options.trim?.end ?? Infinity;
@@ -312,8 +313,8 @@ export class Conversion {
/** @internal */
async _init() {
const inputTracks = await this._input.getTracks();
const outputTrackCounts = this._output.format.getSupportedTrackCounts();
const inputTracks = await this.input.getTracks();
const outputTrackCounts = this.output.format.getSupportedTrackCounts();
for (const track of inputTracks) {
if (track.isVideoTrack() && this._options.video?.discard) {
@@ -373,13 +374,13 @@ export class Conversion {
if (this.onProgress) {
this._computeProgress = true;
this._totalDuration = Math.min(
await this._input.computeDuration() - this._startTimestamp,
await this.input.computeDuration() - this._startTimestamp,
this._endTimestamp - this._startTimestamp,
);
this.onProgress?.(0);
}
await this._output.start();
await this.output.start();
this._start();
try {
@@ -397,7 +398,7 @@ export class Conversion {
await new Promise(() => {}); // Never resolve
}
await this._output.finalize();
await this.output.finalize();
if (this._computeProgress) {
this.onProgress?.(1);
@@ -406,7 +407,7 @@ export class Conversion {
/** Cancels the conversion process. Does nothing if the conversion is already complete. */
async cancel() {
if (this._output.state === 'finalizing' || this._output.state === 'finalized') {
if (this.output.state === 'finalizing' || this.output.state === 'finalized') {
return;
}
@@ -416,7 +417,7 @@ export class Conversion {
}
this._canceled = true;
await this._output.cancel();
await this.output.cancel();
}
/** @internal */
@@ -433,7 +434,7 @@ export class Conversion {
let videoSource: VideoSource;
const totalRotation = normalizeRotation(track.rotation + (this._options.video?.rotate ?? 0));
const outputSupportsRotation = this._output.format.supportsVideoRotationMetadata;
const outputSupportsRotation = this.output.format.supportsVideoRotationMetadata;
const [originalWidth, originalHeight] = totalRotation % 180 === 0
? [track.codedWidth, track.codedHeight]
@@ -463,7 +464,7 @@ export class Conversion {
|| height !== originalHeight
|| (totalRotation !== 0 && !outputSupportsRotation);
let videoCodecs = this._output.format.getSupportedVideoCodecs();
let videoCodecs = this.output.format.getSupportedVideoCodecs();
if (
!needsReencode
&& !this._options.video?.bitrate
@@ -599,7 +600,7 @@ export class Conversion {
}
}
this._output.addVideoTrack(videoSource, {
this.output.addVideoTrack(videoSource, {
languageCode: track.languageCode,
rotation: needsRerender ? 0 : totalRotation, // Rerendering will bake the rotation into the output
});
@@ -634,7 +635,7 @@ export class Conversion {
|| this._startTimestamp > 0
|| firstTimestamp < 0;
let audioCodecs = this._output.format.getSupportedAudioCodecs();
let audioCodecs = this.output.format.getSupportedAudioCodecs();
if (
!this._options.audio?.forceReencode
&& !this._options.audio?.bitrate
@@ -768,7 +769,7 @@ export class Conversion {
}
}
this._output.addAudioTrack(audioSource, {
this.output.addAudioTrack(audioSource, {
languageCode: track.languageCode,
});
this._addedCounts.audio++;
+16 -16
View File
@@ -23,9 +23,9 @@ export abstract class InputFormat {
abstract _createDemuxer(input: Input): Demuxer;
/** Returns the name of the input format. */
abstract getName(): string;
abstract get name(): string;
/** Returns the typical base MIME type of the input format. */
abstract getMimeType(): string;
abstract get mimeType(): string;
}
/**
@@ -68,11 +68,11 @@ export class Mp4InputFormat extends IsobmffInputFormat {
return !!majorBrand && majorBrand !== 'qt ';
}
getName() {
get name() {
return 'MP4';
}
getMimeType() {
get mimeType() {
return 'video/mp4';
}
}
@@ -88,11 +88,11 @@ export class QuickTimeInputFormat extends IsobmffInputFormat {
return majorBrand === 'qt ';
}
getName() {
get name() {
return 'QuickTime File Format';
}
getMimeType() {
get mimeType() {
return 'video/quicktime';
}
}
@@ -174,11 +174,11 @@ export class MatroskaInputFormat extends InputFormat {
return new MatroskaDemuxer(input);
}
getName() {
get name() {
return 'Matroska';
}
getMimeType() {
get mimeType() {
return 'video/x-matroska';
}
}
@@ -193,11 +193,11 @@ export class WebMInputFormat extends MatroskaInputFormat {
return this.isSupportedEBMLOfDocType(input, 'webm');
}
override getName() {
override get name() {
return 'WebM';
}
override getMimeType() {
override get mimeType() {
return 'video/webm';
}
}
@@ -258,11 +258,11 @@ export class Mp3InputFormat extends InputFormat {
return new Mp3Demuxer(input);
}
getName() {
get name() {
return 'MP3';
}
getMimeType() {
get mimeType() {
return 'audio/mpeg';
}
}
@@ -295,11 +295,11 @@ export class WaveInputFormat extends InputFormat {
return new WaveDemuxer(input);
}
getName() {
get name() {
return 'WAVE';
}
getMimeType() {
get mimeType() {
return 'audio/wav';
}
}
@@ -325,11 +325,11 @@ export class OggInputFormat extends InputFormat {
return new OggDemuxer(input);
}
getName() {
get name() {
return 'Ogg';
}
getMimeType() {
get mimeType() {
return 'application/ogg';
}
}
+8 -15
View File
@@ -49,6 +49,7 @@ import {
} from '../misc';
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
import { Reader } from '../reader';
import { buildIsobmffMimeType } from './isobmff-misc';
import { IsobmffReader, MAX_BOX_HEADER_SIZE } from './isobmff-reader';
type InternalTrack = {
@@ -223,22 +224,14 @@ export class IsobmffDemuxer extends Demuxer {
override async getMimeType() {
await this.readMetadata();
const base = this.tracks.some(x => x.info?.type === 'video')
? 'video/'
: this.tracks.some(x => x.info?.type === 'audio')
? 'audio/'
: 'application/';
const codecStrings = await Promise.all(this.tracks.map(x => x.inputTrack!.getCodecParameterString()));
let string = base + (this.isQuickTime ? 'quicktime' : 'mp4');
if (this.tracks.length > 0) {
const codecMimeTypes = await Promise.all(this.tracks.map(x => x.inputTrack!.getCodecParameterString()));
const uniqueCodecMimeTypes = [...new Set(codecMimeTypes.filter(Boolean))];
string += `; codecs="${uniqueCodecMimeTypes.join(', ')}"`;
}
return string;
return buildIsobmffMimeType({
isQuicktime: this.isQuickTime,
hasVideo: this.tracks.some(x => x.info?.type === 'video'),
hasAudio: this.tracks.some(x => x.info?.type === 'audio'),
codecStrings: codecStrings.filter(Boolean) as string[],
});
}
readMetadata() {
+21
View File
@@ -0,0 +1,21 @@
export const buildIsobmffMimeType = (info: {
isQuicktime: boolean;
hasVideo: boolean;
hasAudio: boolean;
codecStrings: string[];
}) => {
const base = info.hasVideo
? 'video/'
: info.hasAudio
? 'audio/'
: 'application/';
let string = base + (info.isQuicktime ? 'quicktime' : 'mp4');
if (info.codecStrings.length > 0) {
const uniqueCodecMimeTypes = [...new Set(info.codecStrings)];
string += `; codecs="${uniqueCodecMimeTypes.join(', ')}"`;
}
return string;
};
+58 -5
View File
@@ -2,13 +2,14 @@ import { Box, ftyp, IsobmffBoxWriter, mdat, mfra, moof, moov, vtta, vttc, vtte }
import { Muxer } from '../muxer';
import { Output, OutputAudioTrack, OutputSubtitleTrack, OutputTrack, OutputVideoTrack } from '../output';
import { BufferTargetWriter, Writer } from '../writer';
import { assert, computeRationalApproximation, last } from '../misc';
import { assert, computeRationalApproximation, last, promiseWithResolvers } from '../misc';
import { IsobmffOutputFormatOptions, IsobmffOutputFormat, MovOutputFormat } from '../output-format';
import { inlineTimestampRegex, SubtitleConfig, SubtitleCue, SubtitleMetadata } from '../subtitles';
import {
parsePcmCodec,
PCM_AUDIO_CODECS,
PcmAudioCodec,
SubtitleCodec,
validateAudioChunkMetadata,
validateSubtitleMetadata,
validateVideoChunkMetadata,
@@ -22,6 +23,7 @@ import {
serializeHevcDecoderConfigurationRecord,
transformAnnexBToLengthPrefixed,
} from '../codec-data';
import { buildIsobmffMimeType } from './isobmff-misc';
export const GLOBAL_TIMESCALE = 1000;
const TIMESTAMP_OFFSET = 2_082_844_800; // Seconds between Jan 1 1904 and Jan 1 1970
@@ -125,6 +127,7 @@ export class IsobmffMuxer extends Muxer {
private mdat: Box | null = null;
private trackDatas: IsobmffTrackData[] = [];
private allTracksKnown = promiseWithResolvers();
private creationTime = Math.floor(Date.now() / 1000) + TIMESTAMP_OFFSET;
private finalizedChunks: Chunk[] = [];
@@ -197,6 +200,40 @@ export class IsobmffMuxer extends Muxer {
release();
}
private allTracksAreKnown() {
for (const track of this.output._tracks) {
if (!track.source._closed && !this.trackDatas.some(x => x.track === track)) {
return false; // We haven't seen a sample from this open track yet
}
}
return true;
}
async getMimeType() {
await this.allTracksKnown.promise;
const codecStrings = this.trackDatas.map((trackData) => {
if (trackData.type === 'video') {
return trackData.info.decoderConfig.codec;
} else if (trackData.type === 'audio') {
return trackData.info.decoderConfig.codec;
} else {
const map: Record<SubtitleCodec, string> = {
webvtt: 'wvtt',
};
return map[trackData.track.source._codec];
}
});
return buildIsobmffMimeType({
isQuicktime: this.isMov,
hasVideo: this.trackDatas.some(x => x.type === 'video'),
hasAudio: this.trackDatas.some(x => x.type === 'audio'),
codecStrings,
});
}
private getVideoTrackData(track: OutputVideoTrack, packet: EncodedPacket, meta?: EncodedVideoChunkMetadata) {
const existingTrackData = this.trackDatas.find(x => x.track === track);
if (existingTrackData) {
@@ -278,6 +315,10 @@ export class IsobmffMuxer extends Muxer {
this.trackDatas.push(newTrackData);
this.trackDatas.sort((a, b) => a.track.id - b.track.id);
if (this.allTracksAreKnown()) {
this.allTracksKnown.resolve();
}
return newTrackData;
}
@@ -319,6 +360,10 @@ export class IsobmffMuxer extends Muxer {
this.trackDatas.push(newTrackData);
this.trackDatas.sort((a, b) => a.track.id - b.track.id);
if (this.allTracksAreKnown()) {
this.allTracksKnown.resolve();
}
return newTrackData;
}
@@ -360,6 +405,10 @@ export class IsobmffMuxer extends Muxer {
this.trackDatas.push(newTrackData);
this.trackDatas.sort((a, b) => a.track.id - b.track.id);
if (this.allTracksAreKnown()) {
this.allTracksKnown.resolve();
}
return newTrackData;
}
@@ -869,10 +918,8 @@ export class IsobmffMuxer extends Muxer {
assert(this.isFragmented);
if (!isFinalCall) {
for (const track of this.output._tracks) {
if (!track.source._closed && !this.trackDatas.some(x => x.track === track)) {
return; // We haven't seen a sample from this open track yet
}
if (!this.allTracksAreKnown()) {
return; // We can't interleave yet as we don't yet know how many tracks we'll truly have
}
}
@@ -1006,6 +1053,10 @@ export class IsobmffMuxer extends Muxer {
}
}
if (this.allTracksAreKnown()) {
this.allTracksKnown.resolve();
}
if (this.isFragmented) {
// Since a track is now closed, we may be able to write out chunks that were previously waiting
await this.interleaveSamples();
@@ -1018,6 +1069,8 @@ export class IsobmffMuxer extends Muxer {
async finalize() {
const release = await this.mutex.acquire();
this.allTracksKnown.resolve();
for (const trackData of this.trackDatas) {
if (trackData.type === 'subtitle' && trackData.track.source._codec === 'webvtt') {
await this.processWebVTTCues(trackData, Infinity);
+8 -15
View File
@@ -50,6 +50,7 @@ import {
MAX_HEADER_SIZE,
MIN_HEADER_SIZE,
} from './ebml';
import { buildMatroskaMimeType } from './matroska-misc';
type Segment = {
seekHeadSeen: boolean;
@@ -194,23 +195,15 @@ export class MatroskaDemuxer extends Demuxer {
override async getMimeType() {
await this.readMetadata();
const base = this.segments.some(segment => segment.tracks.some(x => x.info?.type === 'video'))
? 'video/'
: this.segments.some(segment => segment.tracks.some(x => x.info?.type === 'audio'))
? 'audio/'
: 'application/';
let string = base + (this.isWebM ? 'webm' : 'x-matroska');
const tracks = await this.getTracks();
if (tracks.length > 0) {
const codecMimeTypes = await Promise.all(tracks.map(x => x.getCodecParameterString()));
const uniqueCodecMimeTypes = [...new Set(codecMimeTypes.filter(Boolean))];
const codecStrings = await Promise.all(tracks.map(x => x.getCodecParameterString()));
string += `; codecs="${uniqueCodecMimeTypes.join(', ')}"`;
}
return string;
return buildMatroskaMimeType({
isWebM: this.isWebM,
hasVideo: this.segments.some(segment => segment.tracks.some(x => x.info?.type === 'video')),
hasAudio: this.segments.some(segment => segment.tracks.some(x => x.info?.type === 'audio')),
codecStrings: codecStrings.filter(Boolean) as string[],
});
}
readMetadata() {
+21
View File
@@ -0,0 +1,21 @@
export const buildMatroskaMimeType = (info: {
isWebM: boolean;
hasVideo: boolean;
hasAudio: boolean;
codecStrings: string[];
}) => {
const base = info.hasVideo
? 'video/'
: info.hasAudio
? 'audio/'
: 'application/';
let string = base + (info.isWebM ? 'webm' : 'x-matroska');
if (info.codecStrings.length > 0) {
const uniqueCodecMimeTypes = [...new Set(info.codecStrings.filter(Boolean))];
string += `; codecs="${uniqueCodecMimeTypes.join(', ')}"`;
}
return string;
};
+58 -4
View File
@@ -7,6 +7,7 @@ import {
assert,
colorSpaceIsComplete,
normalizeRotation,
promiseWithResolvers,
roundToMultiple,
textEncoder,
toUint8Array,
@@ -22,6 +23,7 @@ import {
EBMLSignedInt,
EBMLWriter,
} from './ebml';
import { buildMatroskaMimeType } from './matroska-misc';
import { MkvOutputFormat, WebMOutputFormat } from '../output-format';
import { Output, OutputAudioTrack, OutputSubtitleTrack, OutputTrack, OutputVideoTrack } from '../output';
import {
@@ -36,6 +38,7 @@ import {
OPUS_INTERNAL_SAMPLE_RATE,
PCM_AUDIO_CODECS,
PcmAudioCodec,
SubtitleCodec,
generateAv1CodecConfigurationFromCodecString,
generateVp9CodecConfigurationFromCodecString,
parsePcmCodec,
@@ -121,6 +124,7 @@ export class MatroskaMuxer extends Muxer {
private format: WebMOutputFormat | MkvOutputFormat;
private trackDatas: MatroskaTrackData[] = [];
private allTracksKnown = promiseWithResolvers();
private segment: EBMLElement | null = null;
private segmentInfo: EBMLElement | null = null;
@@ -396,6 +400,40 @@ export class MatroskaMuxer extends Muxer {
return this.ebmlWriter.dataOffsets.get(this.segment)!;
}
private allTracksAreKnown() {
for (const track of this.output._tracks) {
if (!track.source._closed && !this.trackDatas.some(x => x.track === track)) {
return false; // We haven't seen a sample from this open track yet
}
}
return true;
}
async getMimeType() {
await this.allTracksKnown.promise;
const codecStrings = this.trackDatas.map((trackData) => {
if (trackData.type === 'video') {
return trackData.info.decoderConfig.codec;
} else if (trackData.type === 'audio') {
return trackData.info.decoderConfig.codec;
} else {
const map: Record<SubtitleCodec, string> = {
webvtt: 'wvtt',
};
return map[trackData.track.source._codec];
}
});
return buildMatroskaMimeType({
isWebM: this.format instanceof WebMOutputFormat,
hasVideo: this.trackDatas.some(x => x.type === 'video'),
hasAudio: this.trackDatas.some(x => x.type === 'audio'),
codecStrings,
});
}
private getVideoTrackData(track: OutputVideoTrack, meta?: EncodedVideoChunkMetadata) {
const existingTrackData = this.trackDatas.find(x => x.track === track);
if (existingTrackData) {
@@ -445,6 +483,10 @@ export class MatroskaMuxer extends Muxer {
this.trackDatas.push(newTrackData);
this.trackDatas.sort((a, b) => a.track.id - b.track.id);
if (this.allTracksAreKnown()) {
this.allTracksKnown.resolve();
}
return newTrackData;
}
@@ -474,6 +516,10 @@ export class MatroskaMuxer extends Muxer {
this.trackDatas.push(newTrackData);
this.trackDatas.sort((a, b) => a.track.id - b.track.id);
if (this.allTracksAreKnown()) {
this.allTracksKnown.resolve();
}
return newTrackData;
}
@@ -501,6 +547,10 @@ export class MatroskaMuxer extends Muxer {
this.trackDatas.push(newTrackData);
this.trackDatas.sort((a, b) => a.track.id - b.track.id);
if (this.allTracksAreKnown()) {
this.allTracksKnown.resolve();
}
return newTrackData;
}
@@ -587,10 +637,8 @@ export class MatroskaMuxer extends Muxer {
private async interleaveChunks(isFinalCall = false) {
if (!isFinalCall) {
for (const track of this.output._tracks) {
if (!track.source._closed && !this.trackDatas.some(x => x.track === track)) {
return; // We haven't seen a sample from this open track yet
}
if (!this.allTracksAreKnown()) {
return; // We can't interleave yet as we don't yet know how many tracks we'll truly have
}
}
@@ -880,6 +928,10 @@ export class MatroskaMuxer extends Muxer {
override async onTrackClose() {
const release = await this.mutex.acquire();
if (this.allTracksAreKnown()) {
this.allTracksKnown.resolve();
}
// Since a track is now closed, we may be able to write out chunks that were previously waiting
await this.interleaveChunks();
@@ -890,6 +942,8 @@ export class MatroskaMuxer extends Muxer {
async finalize() {
const release = await this.mutex.acquire();
this.allTracksKnown.resolve();
if (!this.segment) {
this.createTracks();
this.createSegment();
+4
View File
@@ -27,6 +27,10 @@ export class Mp3Muxer extends Muxer {
// Nothing needed here
}
async getMimeType() {
return 'audio/mpeg';
}
async addEncodedVideoPacket() {
throw new Error('MP3 does not support video.');
}
+1
View File
@@ -19,6 +19,7 @@ export abstract class Muxer {
}
abstract start(): Promise<void>;
abstract getMimeType(): Promise<string>;
abstract addEncodedVideoPacket(
track: OutputVideoTrack,
packet: EncodedPacket,
+5 -10
View File
@@ -7,7 +7,7 @@ import { PacketRetrievalOptions } from '../media-sink';
import { assert, AsyncMutex, findLast, roundToPrecision, toDataView, UNDETERMINED_LANGUAGE } from '../misc';
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
import { Reader } from '../reader';
import { computeOggPageCrc, extractSampleMetadata, OggCodecInfo } from './ogg-misc';
import { buildOggMimeType, computeOggPageCrc, extractSampleMetadata, OggCodecInfo } from './ogg-misc';
import { MAX_PAGE_HEADER_SIZE, MAX_PAGE_SIZE, MIN_PAGE_HEADER_SIZE, OggReader, Page } from './ogg-reader';
type LogicalBitstream = {
@@ -366,16 +366,11 @@ export class OggDemuxer extends Demuxer {
async getMimeType() {
await this.readMetadata();
let string = 'audio/ogg';
const codecStrings = await Promise.all(this.tracks.map(x => x.getCodecParameterString()));
if (this.tracks.length > 0) {
const codecMimeTypes = await Promise.all(this.tracks.map(x => x.getCodecParameterString()));
const uniqueCodecMimeTypes = [...new Set(codecMimeTypes.filter(Boolean))];
string += `; codecs="${uniqueCodecMimeTypes.join(', ')}"`;
}
return string;
return buildOggMimeType({
codecStrings: codecStrings.filter(Boolean) as string[],
});
}
async getTracks() {
+13
View File
@@ -93,3 +93,16 @@ export const extractSampleMetadata = (
vorbisBlockSize: currentBlocksize,
};
};
export const buildOggMimeType = (info: {
codecStrings: string[];
}) => {
let string = 'audio/ogg';
if (info.codecStrings) {
const uniqueCodecMimeTypes = [...new Set(info.codecStrings)];
string += `; codecs="${uniqueCodecMimeTypes.join(', ')}"`;
}
return string;
};
+34 -5
View File
@@ -1,12 +1,13 @@
import { OPUS_INTERNAL_SAMPLE_RATE, validateAudioChunkMetadata } from '../codec';
import { parseModesFromVorbisSetupPacket, parseOpusIdentificationHeader } from '../codec-data';
import { assert, setInt64, toDataView, toUint8Array } from '../misc';
import { assert, promiseWithResolvers, 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 {
buildOggMimeType,
computeOggPageCrc,
extractSampleMetadata,
OggCodecInfo,
@@ -46,6 +47,7 @@ export class OggMuxer extends Muxer {
private trackDatas: OggTrackData[] = [];
private bosPagesWritten = false;
private allTracksKnown = promiseWithResolvers();
private pageBytes = new Uint8Array(MAX_PAGE_SIZE);
private pageView = new DataView(this.pageBytes.buffer);
@@ -63,6 +65,14 @@ export class OggMuxer extends Muxer {
// Nothin'
}
async getMimeType() {
await this.allTracksKnown.promise;
return buildOggMimeType({
codecStrings: this.trackDatas.map(x => x.codecInfo.codec!),
});
}
addEncodedVideoPacket(): never {
throw new Error('Video tracks are not supported.');
}
@@ -112,6 +122,11 @@ export class OggMuxer extends Muxer {
this.queueHeaderPackets(newTrackData, meta);
this.trackDatas.push(newTrackData);
if (this.allTracksAreKnown()) {
this.allTracksKnown.resolve();
}
return newTrackData;
}
@@ -250,12 +265,20 @@ export class OggMuxer extends Muxer {
throw new Error('Subtitle tracks are not supported.');
}
allTracksAreKnown() {
for (const track of this.output._tracks) {
if (!track.source._closed && !this.trackDatas.some(x => x.track === track)) {
return false; // We haven't seen a sample from this open track yet
}
}
return true;
}
async interleavePages(isFinalCall = false) {
if (!this.bosPagesWritten) {
for (const track of this.output._tracks) {
if (!track.source._closed && !this.trackDatas.some(x => x.track === track)) {
return; // We haven't seen a sample from this open track yet
}
if (!this.allTracksAreKnown()) {
return; // We can't interleave yet as we don't yet know how many tracks we'll truly have
}
// Write the header page for all bitstreams
@@ -421,6 +444,10 @@ export class OggMuxer extends Muxer {
override async onTrackClose() {
const release = await this.mutex.acquire();
if (this.allTracksAreKnown()) {
this.allTracksKnown.resolve();
}
// Since a track is now closed, we may be able to write out chunks that were previously waiting
await this.interleavePages();
@@ -430,6 +457,8 @@ export class OggMuxer extends Muxer {
async finalize() {
const release = await this.mutex.acquire();
this.allTracksKnown.resolve();
await this.interleavePages(true);
for (const trackData of this.trackDatas) {
+9
View File
@@ -344,6 +344,15 @@ export class Output<
})();
}
/**
* Resolves with the full MIME type of the output file, including track codecs.
*
* The returned promise will resolve only once the precise codec strings of all tracks are known.
*/
getMimeType() {
return this._muxer.getMimeType();
}
/**
* Cancels the creation of the output file, releasing internal resources like encoders and preventing further
* samples from being added.
+4
View File
@@ -27,6 +27,10 @@ export class WaveMuxer extends Muxer {
// Nothing needed here - we'll write the header with the first sample
}
async getMimeType() {
return 'audio/wav';
}
async addEncodedVideoPacket() {
throw new Error('WAVE does not support video.');
}