mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 19:03:46 +02:00
Add composable conversions
* Add non-owning conversions via ConversionOptions.ownsOutput A conversion with ownsOutput: false only adds tracks to the output and drives their media data; starting, finalizing, and metadata tags remain the caller's responsibility. This lets multiple conversions and directly-added user tracks compose on a single Output (see upstream issue #436). - ownsOutput: false allows a pre-populated output (state must still be 'pending') and seeds track-capacity accounting from existing tracks - execute() requires the output to be started and never finalizes it - cancel() closes only the conversion's own sources, releasing internal synchronizer waiters, and leaves the output usable - tags cannot be combined with ownsOutput: false - isValid requires at least one contributed track instead of the format's minimum track counts Prototype for API discussion; default (owning) behavior is unchanged. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AJdfnbY9AFh9i9dKgtrj6E * Add external-audio example using a non-owning conversion Demonstrates composing a user-owned audio track (synthesized voiceover via OfflineAudioContext + AudioBufferSource) onto a picked video with Conversion.init({ ownsOutput: false }), including progress reporting and playback/download of the result. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AJdfnbY9AFh9i9dKgtrj6E * Release synchronizer waiters when canceling during output finalization A non-owning conversion's cancel() previously no-oped entirely when the output (owned by someone else) was already finalizing or finalized, leaving pump loops parked in the track synchronizer and hanging execute() forever. Now it still marks the conversion canceled and releases parked waiters in that state, without force-closing sources (finalization owns flushing them at that point). Also adds coverage: non-owning onProgress monotonicity, canceling one of two sibling conversions, capacity seeding across sequential inits, exact metadata exclusivity, and cancel-before-execute. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AJdfnbY9AFh9i9dKgtrj6E * Document non-owning conversions on the converting-media-files guide page Adds the doc section requested in #436: what ownsOutput: false does, the required choreography (add tracks -> output.start() before execute() -> run the conversion concurrently with your own sources -> finalize), the cancellation split (conversion.cancel() leaves the output alive; cancel both for a full abort and tear the output down on error paths), isValid semantics in this mode, and the tags restriction with the setMetadataTags() alternative. Also cross-links the fresh-output rule to the new section. VitePress build passes with dead-link checking on. Co-Authored-By: Claude Opus 4.8 <[email protected]> * Clean up conversion logic, add Output.tracks and .hasEnoughTracks(), move new conversion tests around, remove external audio example * non-owning -> composable, and update docs * Update --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Vanilagy <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
Vanilagy
parent
794b84884f
commit
f41eef0937
+109
-59
@@ -48,7 +48,7 @@ import {
|
||||
promiseWithResolvers,
|
||||
Rotation,
|
||||
} from './misc';
|
||||
import { Output, OutputTrackGroup, TrackType } from './output';
|
||||
import { Output, OutputTrackGroup } from './output';
|
||||
import { Mp4OutputFormat } from './output-format';
|
||||
import {
|
||||
AudioSample,
|
||||
@@ -146,6 +146,18 @@ export type ConversionOptions = {
|
||||
* want to keep the console output clean.
|
||||
*/
|
||||
showWarnings?: boolean;
|
||||
|
||||
/**
|
||||
* Whether this conversion is composable, defaults to `false`. A non-composable conversion takes full ownership of
|
||||
* the output: it requires a fresh output and controls its entire lifecycle, meaning it starts it, writes its
|
||||
* metadata tags, and finalizes it.
|
||||
*
|
||||
* A composable conversion only adds tracks to the output and drives their media data; starting and finalizing
|
||||
* the output is an outside responsibility. This is useful when only some output tracks should be driven by a
|
||||
* conversion, and other are to be driven manually. Additionally, it can be used to have multiple conversions target
|
||||
* the same output.
|
||||
*/
|
||||
composable?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -546,15 +558,6 @@ export class Conversion {
|
||||
/** @internal */
|
||||
_endTimestamp!: number;
|
||||
|
||||
/** @internal */
|
||||
_addedCounts: Record<TrackType, number> = {
|
||||
video: 0,
|
||||
audio: 0,
|
||||
subtitle: 0,
|
||||
};
|
||||
|
||||
/** @internal */
|
||||
_totalTrackCount = 0;
|
||||
/** @internal */
|
||||
_nextOutputTrackId = 0;
|
||||
/** @internal */
|
||||
@@ -564,6 +567,8 @@ export class Conversion {
|
||||
|
||||
/** @internal */
|
||||
_trackPromises: Promise<void>[] = [];
|
||||
/** @internal */
|
||||
_composable = false;
|
||||
|
||||
/** @internal */
|
||||
_started: Promise<void>;
|
||||
@@ -600,7 +605,8 @@ export class Conversion {
|
||||
|
||||
/**
|
||||
* Whether this conversion, as it has been configured, is valid and can be executed. If this field is `false`, check
|
||||
* the `discardedTracks` field for reasons.
|
||||
* the `discardedTracks` field for reasons. Composable conversions are always valid, even if they utilize
|
||||
* zero tracks.
|
||||
*
|
||||
* Note: a conversion having discarded tracks does not automatically mean it is invalid; if the remaining, utilized
|
||||
* tracks make for a valid output file, the conversion is still allowed.
|
||||
@@ -642,12 +648,30 @@ export class Conversion {
|
||||
'options.tracks, when provided, must be either \'all\' or \'primary\'.',
|
||||
);
|
||||
}
|
||||
if (
|
||||
options.output._tracks.length > 0
|
||||
|| Object.keys(options.output._metadataTags).length > 0
|
||||
|| options.output.state !== 'pending'
|
||||
) {
|
||||
throw new TypeError('options.output must be fresh: no tracks or metadata tags added and not started.');
|
||||
if (options.composable !== undefined && typeof options.composable !== 'boolean') {
|
||||
throw new TypeError('options.composable, when provided, must be a boolean.');
|
||||
}
|
||||
|
||||
const composable = options.composable ?? false;
|
||||
if (!composable) {
|
||||
if (
|
||||
options.output.tracks.length > 0
|
||||
|| Object.keys(options.output._metadataTags).length > 0
|
||||
|| options.output.state !== 'pending'
|
||||
) {
|
||||
throw new TypeError('options.output must be fresh: no tracks or metadata tags added and not started.');
|
||||
}
|
||||
} else {
|
||||
if (options.tags !== undefined) {
|
||||
throw new TypeError(
|
||||
'options.tags cannot be set by a composable conversion; set metadata directly on the output'
|
||||
+ ' instead.',
|
||||
);
|
||||
}
|
||||
|
||||
if (options.output.state !== 'pending') {
|
||||
throw new TypeError('options.output must not have been started yet.');
|
||||
}
|
||||
}
|
||||
|
||||
if (options.video !== undefined && typeof options.video !== 'function') {
|
||||
@@ -704,6 +728,7 @@ export class Conversion {
|
||||
}
|
||||
|
||||
this._options = options;
|
||||
this._composable = composable;
|
||||
this.input = options.input;
|
||||
this.output = options.output;
|
||||
|
||||
@@ -857,7 +882,7 @@ export class Conversion {
|
||||
const options = filteredTrackOptions[i]!;
|
||||
|
||||
for (const option of options) {
|
||||
if (this._totalTrackCount === outputTrackCounts.total.max) {
|
||||
if (this.output.tracks.length === outputTrackCounts.total.max) {
|
||||
this.discardedTracks.push({
|
||||
track,
|
||||
reason: 'max_track_count_reached',
|
||||
@@ -866,7 +891,12 @@ export class Conversion {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this._addedCounts[track.type] === outputTrackCounts[track.type].max) {
|
||||
const addedCountOfType = this.output.tracks.reduce(
|
||||
(count, t) => count + (t.type === track.type ? 1 : 0),
|
||||
0,
|
||||
);
|
||||
|
||||
if (addedCountOfType === outputTrackCounts[track.type].max) {
|
||||
this.discardedTracks.push({
|
||||
track,
|
||||
reason: 'max_track_count_of_type_reached',
|
||||
@@ -906,39 +936,44 @@ export class Conversion {
|
||||
}
|
||||
}
|
||||
|
||||
// Now, let's deal with metadata tags
|
||||
// Now, let's deal with metadata tags. A composable conversion does not touch the output's metadata tags; that
|
||||
// remains the responsibility of whoever owns the output.
|
||||
|
||||
const inputTags = await this.input.getMetadataTags();
|
||||
let outputTags: MetadataTags;
|
||||
if (!this._composable) {
|
||||
const inputTags = await this.input.getMetadataTags();
|
||||
let outputTags: MetadataTags;
|
||||
|
||||
if (this._options.tags) {
|
||||
const result = typeof this._options.tags === 'function'
|
||||
? await this._options.tags(inputTags)
|
||||
: this._options.tags;
|
||||
validateMetadataTags(result);
|
||||
if (this._options.tags) {
|
||||
const result = typeof this._options.tags === 'function'
|
||||
? await this._options.tags(inputTags)
|
||||
: this._options.tags;
|
||||
validateMetadataTags(result);
|
||||
|
||||
outputTags = result;
|
||||
} else {
|
||||
outputTags = inputTags;
|
||||
outputTags = result;
|
||||
} else {
|
||||
outputTags = inputTags;
|
||||
}
|
||||
|
||||
// Somewhat dirty but pragmatic
|
||||
const inputAndOutputFormatMatch = inputFormat.mimeType === this.output.format.mimeType;
|
||||
const rawTagsAreUnchanged = inputTags.raw === outputTags.raw;
|
||||
|
||||
if (inputTags.raw && rawTagsAreUnchanged && !inputAndOutputFormatMatch) {
|
||||
// If the input and output formats aren't the same, copying over raw metadata tags makes no sense and
|
||||
// only results in junk tags, so let's cut them out.
|
||||
delete outputTags.raw;
|
||||
}
|
||||
|
||||
this.output.setMetadataTags(outputTags);
|
||||
}
|
||||
|
||||
// Somewhat dirty but pragmatic
|
||||
const inputAndOutputFormatMatch = inputFormat.mimeType === this.output.format.mimeType;
|
||||
const rawTagsAreUnchanged = inputTags.raw === outputTags.raw;
|
||||
|
||||
if (inputTags.raw && rawTagsAreUnchanged && !inputAndOutputFormatMatch) {
|
||||
// If the input and output formats aren't the same, copying over raw metadata tags makes no sense and only
|
||||
// results in junk tags, so let's cut them out.
|
||||
delete outputTags.raw;
|
||||
}
|
||||
|
||||
this.output.setMetadataTags(outputTags);
|
||||
|
||||
// Let's check if the conversion can actually be executed
|
||||
this.isValid = this._totalTrackCount >= outputTrackCounts.total.min
|
||||
&& this._addedCounts.video >= outputTrackCounts.video.min
|
||||
&& this._addedCounts.audio >= outputTrackCounts.audio.min
|
||||
&& this._addedCounts.subtitle >= outputTrackCounts.subtitle.min;
|
||||
if (!this._composable) {
|
||||
this.isValid = this.output.hasEnoughTracks();
|
||||
} else {
|
||||
// Checking Output start validity is not up to us. We consider even zero-track conversions to be valid
|
||||
this.isValid = true;
|
||||
}
|
||||
|
||||
if (this._options.showWarnings ?? true) {
|
||||
const warnElements: unknown[] = [];
|
||||
@@ -1054,6 +1089,13 @@ export class Conversion {
|
||||
);
|
||||
}
|
||||
|
||||
if (this._composable && this.output.state === 'pending') {
|
||||
throw new Error(
|
||||
'A composable conversion requires the output to be started. Call start() on the output before executing'
|
||||
+ ' the conversion.',
|
||||
);
|
||||
}
|
||||
|
||||
if (this._executed) {
|
||||
throw new Error('Conversion cannot be executed twice.');
|
||||
}
|
||||
@@ -1088,7 +1130,10 @@ export class Conversion {
|
||||
this.onProgress?.(0, 0);
|
||||
}
|
||||
|
||||
await this.output.start();
|
||||
if (!this._composable) {
|
||||
await this.output.start();
|
||||
}
|
||||
|
||||
this._start();
|
||||
|
||||
try {
|
||||
@@ -1106,7 +1151,9 @@ export class Conversion {
|
||||
throw new ConversionCanceledError();
|
||||
}
|
||||
|
||||
await this.output.finalize();
|
||||
if (!this._composable) {
|
||||
await this.output.finalize();
|
||||
}
|
||||
|
||||
if (this._computeProgress) {
|
||||
const minTimestamp = Math.min(...this._maxTimestamps.values());
|
||||
@@ -1129,7 +1176,10 @@ export class Conversion {
|
||||
}
|
||||
|
||||
this._canceled = true;
|
||||
await this.output.cancel();
|
||||
|
||||
if (!this._composable) {
|
||||
await this.output.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
@@ -1219,7 +1269,7 @@ export class Conversion {
|
||||
|
||||
for await (const packet of sink.packets(undefined, undefined, { verifyKeyPackets: true })) {
|
||||
if (this._canceled) {
|
||||
return;
|
||||
break;
|
||||
}
|
||||
|
||||
if (packet.timestamp >= this._endTimestamp) {
|
||||
@@ -1377,7 +1427,7 @@ export class Conversion {
|
||||
|
||||
for await (using sample of sink.samples(this._startTimestamp, this._endTimestamp)) {
|
||||
if (this._canceled) {
|
||||
return;
|
||||
break;
|
||||
}
|
||||
|
||||
const adjustedSampleTimestamp = Math.max(sample.timestamp - this._startTimestamp, 0);
|
||||
@@ -1400,7 +1450,9 @@ export class Conversion {
|
||||
}
|
||||
|
||||
let ownGroup: OutputTrackGroup | null = null;
|
||||
if (!trackOptions.group) {
|
||||
if (!trackOptions.group && !this._composable) {
|
||||
// Create per-track groups to replicate the input's pairability graph. Don't do this for composable
|
||||
// conversions.
|
||||
ownGroup = new OutputTrackGroup();
|
||||
}
|
||||
|
||||
@@ -1414,8 +1466,6 @@ export class Conversion {
|
||||
rotation: outputTrackRotation,
|
||||
group: ownGroup ?? trackOptions.group,
|
||||
});
|
||||
this._addedCounts.video++;
|
||||
this._totalTrackCount++;
|
||||
|
||||
this.utilizedTracks.push(track);
|
||||
this._outputTrackIds.push(outputTrackId);
|
||||
@@ -1474,7 +1524,7 @@ export class Conversion {
|
||||
|
||||
for await (const packet of sink.packets()) {
|
||||
if (this._canceled) {
|
||||
return;
|
||||
break;
|
||||
}
|
||||
|
||||
if (packet.timestamp >= this._endTimestamp) {
|
||||
@@ -1595,7 +1645,7 @@ export class Conversion {
|
||||
const sink = new AudioSampleSink(track);
|
||||
for await (using sample of sink.samples(this._startTimestamp, this._endTimestamp)) {
|
||||
if (this._canceled) {
|
||||
return;
|
||||
break;
|
||||
}
|
||||
|
||||
if (needsPadding) {
|
||||
@@ -1663,7 +1713,9 @@ export class Conversion {
|
||||
}
|
||||
|
||||
let ownGroup: OutputTrackGroup | null = null;
|
||||
if (!trackOptions.group) {
|
||||
if (!trackOptions.group && !this._composable) {
|
||||
// Create per-track groups to replicate the input's pairability graph. Don't do this for composable
|
||||
// conversions.
|
||||
ownGroup = new OutputTrackGroup();
|
||||
}
|
||||
|
||||
@@ -1675,8 +1727,6 @@ export class Conversion {
|
||||
disposition: await track.getDisposition(),
|
||||
group: ownGroup ?? trackOptions.group,
|
||||
});
|
||||
this._addedCounts.audio++;
|
||||
this._totalTrackCount++;
|
||||
|
||||
this.utilizedTracks.push(track);
|
||||
this._outputTrackIds.push(outputTrackId);
|
||||
|
||||
@@ -148,8 +148,8 @@ export class HlsMuxer extends Muxer {
|
||||
async start(): Promise<void> {
|
||||
const release = await this.mutex.acquire();
|
||||
|
||||
const someRelative = this.output._tracks.some(t => t.metadata.isRelativeToUnixEpoch);
|
||||
const someNotRelative = this.output._tracks.some(t => !t.metadata.isRelativeToUnixEpoch);
|
||||
const someRelative = this.output.tracks.some(t => t.metadata.isRelativeToUnixEpoch);
|
||||
const someNotRelative = this.output.tracks.some(t => !t.metadata.isRelativeToUnixEpoch);
|
||||
if (someRelative && someNotRelative) {
|
||||
throw new Error(
|
||||
'All tracks must agree on `relativeToUnixEpoch`: some tracks are relative to the Unix epoch and some'
|
||||
@@ -180,14 +180,14 @@ export class HlsMuxer extends Muxer {
|
||||
let keyPacketsOnlyPairingWarned = false;
|
||||
|
||||
// First, let's build the "sibling" groups induced by track pairability
|
||||
for (const track of this.output._tracks) {
|
||||
for (const track of this.output.tracks) {
|
||||
if (track.type === 'video') {
|
||||
hasVideo = true;
|
||||
}
|
||||
|
||||
const pairableGroups = new Map<MediaCodec, OutputTrack[]>();
|
||||
|
||||
for (const otherTrack of this.output._tracks) {
|
||||
for (const otherTrack of this.output.tracks) {
|
||||
if (track === otherTrack) {
|
||||
continue;
|
||||
}
|
||||
@@ -264,7 +264,7 @@ export class HlsMuxer extends Muxer {
|
||||
const unpairedAudioTracks: OutputTrack[] = [];
|
||||
|
||||
// Now, create the top-level variant streams
|
||||
for (const track of this.output._tracks) {
|
||||
for (const track of this.output.tracks) {
|
||||
const assignedGroupKeys = groupAssignment.get(track);
|
||||
if (assignedGroupKeys) {
|
||||
assert(assignedGroupKeys.length > 0);
|
||||
|
||||
@@ -248,7 +248,7 @@ export class IsobmffMuxer extends Muxer {
|
||||
this.initBoxWriter = new IsobmffBoxWriter(initWriter);
|
||||
}
|
||||
|
||||
const holdsAvc = this.output._tracks.some(x => x.isVideoTrack() && x.source._codec === 'avc');
|
||||
const holdsAvc = this.output.tracks.some(x => x.isVideoTrack() && x.source._codec === 'avc');
|
||||
|
||||
// Write the header
|
||||
{
|
||||
@@ -282,7 +282,7 @@ export class IsobmffMuxer extends Muxer {
|
||||
// We're write at finalization
|
||||
} else if (this.fastStart === 'reserve') {
|
||||
// Validate that all tracks have set maximumPacketCount
|
||||
for (const track of this.output._tracks) {
|
||||
for (const track of this.output.tracks) {
|
||||
if (track.metadata.maximumPacketCount === undefined) {
|
||||
throw new Error(
|
||||
'All tracks must specify maximumPacketCount in their metadata when using'
|
||||
@@ -312,7 +312,7 @@ export class IsobmffMuxer extends Muxer {
|
||||
}
|
||||
|
||||
private allTracksAreKnown() {
|
||||
for (const track of this.output._tracks) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -691,7 +691,7 @@ export class MatroskaMuxer extends Muxer {
|
||||
}
|
||||
|
||||
private allTracksAreKnown() {
|
||||
for (const track of this.output._tracks) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -451,7 +451,7 @@ export class MpegTsMuxer extends Muxer {
|
||||
}
|
||||
|
||||
private allTracksAreKnown() {
|
||||
for (const track of this.output._tracks) {
|
||||
for (const track of this.output.tracks) {
|
||||
if (!track.source._closed && !this.trackDatas.some(x => x.track === track)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -297,7 +297,7 @@ export class OggMuxer extends Muxer {
|
||||
}
|
||||
|
||||
allTracksAreKnown() {
|
||||
for (const track of this.output._tracks) {
|
||||
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
|
||||
}
|
||||
|
||||
+41
-13
@@ -372,6 +372,11 @@ export class Output<
|
||||
*/
|
||||
readonly defaultTrackGroup = new OutputTrackGroup();
|
||||
|
||||
/**
|
||||
* The tracks that have been added to this output. Treat it as a readonly field; to add tracks, use the methods.
|
||||
*/
|
||||
readonly tracks: OutputTrack[] = [];
|
||||
|
||||
/** @internal */
|
||||
private _initTarget: T | (() => MaybePromise<T>) | null;
|
||||
/** @internal */
|
||||
@@ -383,8 +388,6 @@ export class Output<
|
||||
/** @internal */
|
||||
_rootWriterPromise: Promise<Writer> | null = null;
|
||||
/** @internal */
|
||||
_tracks: OutputTrack[] = [];
|
||||
/** @internal */
|
||||
_startPromise: Promise<void> | null = null;
|
||||
/** @internal */
|
||||
_cancelPromise: Promise<void> | null = null;
|
||||
@@ -619,7 +622,7 @@ export class Output<
|
||||
metadataCopy.group ??= this.defaultTrackGroup;
|
||||
|
||||
return this._addTrack(new OutputVideoTrack(
|
||||
this._tracks.length + 1, this, source, metadataCopy,
|
||||
this.tracks.length + 1, this, source, metadataCopy,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -634,7 +637,7 @@ export class Output<
|
||||
metadataCopy.group ??= this.defaultTrackGroup;
|
||||
|
||||
return this._addTrack(new OutputAudioTrack(
|
||||
this._tracks.length + 1, this, source, metadataCopy,
|
||||
this.tracks.length + 1, this, source, metadataCopy,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -649,7 +652,7 @@ export class Output<
|
||||
metadataCopy.group ??= this.defaultTrackGroup;
|
||||
|
||||
return this._addTrack(new OutputSubtitleTrack(
|
||||
this._tracks.length + 1, this, source, metadataCopy,
|
||||
this.tracks.length + 1, this, source, metadataCopy,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -680,7 +683,7 @@ export class Output<
|
||||
|
||||
// Verify maximum track count constraints
|
||||
const supportedTrackCounts = this.format.getSupportedTrackCounts();
|
||||
const presentTracksOfThisType = this._tracks.reduce(
|
||||
const presentTracksOfThisType = this.tracks.reduce(
|
||||
(count, t) => count + (t.type === track.type ? 1 : 0),
|
||||
0,
|
||||
);
|
||||
@@ -694,7 +697,7 @@ export class Output<
|
||||
);
|
||||
}
|
||||
const maxTotalCount = supportedTrackCounts.total.max;
|
||||
if (this._tracks.length === maxTotalCount) {
|
||||
if (this.tracks.length === maxTotalCount) {
|
||||
throw new Error(
|
||||
`${this.format._name} does not support more than ${maxTotalCount} tracks`
|
||||
+ `${maxTotalCount === 1 ? '' : 's'} in total.`,
|
||||
@@ -748,12 +751,37 @@ export class Output<
|
||||
}
|
||||
}
|
||||
|
||||
this._tracks.push(track);
|
||||
this.tracks.push(track);
|
||||
track.source._connectedTrack = track;
|
||||
|
||||
return track;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the output has enough tracks (of the correct type) to be started, based on the requirements of the output
|
||||
* format.
|
||||
*/
|
||||
hasEnoughTracks() {
|
||||
const supportedTrackCounts = this.format.getSupportedTrackCounts();
|
||||
for (const trackType of ALL_TRACK_TYPES) {
|
||||
const presentTracksOfThisType = this.tracks.reduce(
|
||||
(count, track) => count + (track.type === trackType ? 1 : 0),
|
||||
0,
|
||||
);
|
||||
const minCount = supportedTrackCounts[trackType].min;
|
||||
if (presentTracksOfThisType < minCount) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const totalMinCount = supportedTrackCounts.total.min;
|
||||
if (this.tracks.length < totalMinCount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the creation of the output file. This method should be called after all tracks have been added. Only after
|
||||
* the output has started can media samples be added to the tracks.
|
||||
@@ -764,7 +792,7 @@ export class Output<
|
||||
// Verify minimum track count constraints
|
||||
const supportedTrackCounts = this.format.getSupportedTrackCounts();
|
||||
for (const trackType of ALL_TRACK_TYPES) {
|
||||
const presentTracksOfThisType = this._tracks.reduce(
|
||||
const presentTracksOfThisType = this.tracks.reduce(
|
||||
(count, track) => count + (track.type === trackType ? 1 : 0),
|
||||
0,
|
||||
);
|
||||
@@ -780,7 +808,7 @@ export class Output<
|
||||
}
|
||||
}
|
||||
const totalMinCount = supportedTrackCounts.total.min;
|
||||
if (this._tracks.length < totalMinCount) {
|
||||
if (this.tracks.length < totalMinCount) {
|
||||
throw new Error(
|
||||
totalMinCount === supportedTrackCounts.total.max
|
||||
? (`${this.format._name} requires exactly ${totalMinCount} track`
|
||||
@@ -807,7 +835,7 @@ export class Output<
|
||||
try {
|
||||
await this._muxer.start();
|
||||
|
||||
const promises = this._tracks.map(track => track.source._start());
|
||||
const promises = this.tracks.map(track => track.source._start());
|
||||
await Promise.all(promises);
|
||||
} finally {
|
||||
release();
|
||||
@@ -850,7 +878,7 @@ export class Output<
|
||||
const release = await this._mutex.acquire();
|
||||
|
||||
try {
|
||||
const promises = this._tracks.map(x => x.source._flushOrWaitForOngoingClose(true)); // Force close
|
||||
const promises = this.tracks.map(x => x.source._flushOrWaitForOngoingClose(true)); // Force close
|
||||
await Promise.all(promises);
|
||||
|
||||
await Promise.all([...this._unfinalizedTargets].map(target => target._close()));
|
||||
@@ -883,7 +911,7 @@ export class Output<
|
||||
const release = await this._mutex.acquire();
|
||||
|
||||
try {
|
||||
const promises = this._tracks.map(x => x.source._flushOrWaitForOngoingClose(false));
|
||||
const promises = this.tracks.map(x => x.source._flushOrWaitForOngoingClose(false));
|
||||
await Promise.all(promises);
|
||||
|
||||
await this._muxer.finalize();
|
||||
|
||||
Reference in New Issue
Block a user