mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-28 11:23:45 +02:00
Add getMimeType to Output & other small adjustments
This commit is contained in:
+6
-8
@@ -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
@@ -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
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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();
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export abstract class Muxer {
|
||||
}
|
||||
|
||||
abstract start(): Promise<void>;
|
||||
abstract getMimeType(): Promise<string>;
|
||||
abstract addEncodedVideoPacket(
|
||||
track: OutputVideoTrack,
|
||||
packet: EncodedPacket,
|
||||
|
||||
+5
-10
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user