mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-02 13:23:51 +02:00
Fix error when aborting conversion, expose Conversion class for more control, refactor Output state
This commit is contained in:
+9
-9
@@ -18,14 +18,12 @@
|
|||||||
const target = new Metamuxer.BufferTarget();
|
const target = new Metamuxer.BufferTarget();
|
||||||
const outputFormat = new Metamuxer.WebMOutputFormat()
|
const outputFormat = new Metamuxer.WebMOutputFormat()
|
||||||
|
|
||||||
const abortController = new AbortController();
|
|
||||||
const button = document.createElement('button');
|
const button = document.createElement('button');
|
||||||
button.textContent = 'Cancel';
|
button.textContent = 'Cancel';
|
||||||
button.onclick = () => abortController.abort();
|
button.onclick = () => conversion.cancel();
|
||||||
document.body.append(button);
|
document.body.append(button);
|
||||||
|
|
||||||
console.time()
|
const conversion = await Metamuxer.Conversion.init({
|
||||||
const res = await Metamuxer.convert({
|
|
||||||
input: new Metamuxer.Input({
|
input: new Metamuxer.Input({
|
||||||
formats: Metamuxer.ALL_FORMATS,
|
formats: Metamuxer.ALL_FORMATS,
|
||||||
source
|
source
|
||||||
@@ -77,13 +75,15 @@
|
|||||||
start: 0,
|
start: 0,
|
||||||
end: 10
|
end: 10
|
||||||
},
|
},
|
||||||
onProgress: (event) => {
|
computeProgress: true
|
||||||
progress.value = event.completion;
|
|
||||||
},
|
|
||||||
abortSignal: abortController.signal
|
|
||||||
});
|
});
|
||||||
|
console.log(conversion);
|
||||||
|
|
||||||
|
conversion.onProgress = () => progress.value = conversion.progress;
|
||||||
|
|
||||||
|
console.time();
|
||||||
|
await conversion.execute();
|
||||||
console.timeEnd()
|
console.timeEnd()
|
||||||
console.log(res)
|
|
||||||
|
|
||||||
console.log("Done", target.buffer);
|
console.log("Done", target.buffer);
|
||||||
|
|
||||||
|
|||||||
+242
-198
@@ -62,13 +62,66 @@ export type ConversionOptions = {
|
|||||||
end: number;
|
end: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
onProgress?: (event: { completion: number }) => unknown;
|
computeProgress?: boolean;
|
||||||
abortSignal?: AbortSignal;
|
};
|
||||||
|
|
||||||
|
const FALLBACK_NUMBER_OF_CHANNELS = 2;
|
||||||
|
const FALLBACK_SAMPLE_RATE = 48000;
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export const convert = async (options: ConversionOptions) => {
|
||||||
|
const conversion = await Conversion.init(options);
|
||||||
|
await conversion.execute();
|
||||||
|
return conversion;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type ConversionInfo = {
|
export class Conversion {
|
||||||
utilizedTracks: InputTrack[];
|
/** @internal */
|
||||||
|
_options: ConversionOptions;
|
||||||
|
/** @internal */
|
||||||
|
_input: Input;
|
||||||
|
/** @internal */
|
||||||
|
_output: Output;
|
||||||
|
/** @internal */
|
||||||
|
_startTimestamp: number;
|
||||||
|
/** @internal */
|
||||||
|
_endTimestamp: number;
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
_addedCounts: Record<TrackType, number> = {
|
||||||
|
video: 0,
|
||||||
|
audio: 0,
|
||||||
|
subtitle: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
_totalTrackCount = 0;
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
_trackPromises: Promise<void>[] = [];
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
_started: Promise<void>;
|
||||||
|
/** @internal */
|
||||||
|
_start: () => void;
|
||||||
|
/** @internal */
|
||||||
|
_executed = false;
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
_synchronizer = new TrackSynchronizer();
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
_totalDuration: number | null = null;
|
||||||
|
/** @internal */
|
||||||
|
_maxTimestamps = new Map<number, number>(); // Track ID -> timestamp
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
_canceled = false;
|
||||||
|
|
||||||
|
progress = 0;
|
||||||
|
onProgress?: () => unknown = undefined;
|
||||||
|
utilizedTracks: InputTrack[] = [];
|
||||||
discardedTracks: {
|
discardedTracks: {
|
||||||
track: InputTrack;
|
track: InputTrack;
|
||||||
reason:
|
reason:
|
||||||
@@ -78,45 +131,16 @@ export type ConversionInfo = {
|
|||||||
| 'unknownSourceCodec'
|
| 'unknownSourceCodec'
|
||||||
| 'undecodableSourceCodec'
|
| 'undecodableSourceCodec'
|
||||||
| 'noEncodableTargetCodec';
|
| 'noEncodableTargetCodec';
|
||||||
}[];
|
}[] = [];
|
||||||
};
|
|
||||||
|
|
||||||
const FALLBACK_NUMBER_OF_CHANNELS = 2;
|
static async init(options: ConversionOptions) {
|
||||||
const FALLBACK_SAMPLE_RATE = 48000;
|
const conversion = new Conversion(options);
|
||||||
|
await conversion._init();
|
||||||
|
|
||||||
/** @public */
|
return conversion;
|
||||||
export const convert = (options: ConversionOptions) => {
|
}
|
||||||
const conversion = new Conversion(options);
|
|
||||||
return conversion.execute();
|
|
||||||
};
|
|
||||||
|
|
||||||
class Conversion {
|
private constructor(options: ConversionOptions) {
|
||||||
input: Input;
|
|
||||||
output: Output;
|
|
||||||
startTimestamp: number;
|
|
||||||
endTimestamp: number;
|
|
||||||
|
|
||||||
addedCounts: Record<TrackType, number> = {
|
|
||||||
video: 0,
|
|
||||||
audio: 0,
|
|
||||||
subtitle: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
totalTrackCount = 0;
|
|
||||||
|
|
||||||
trackPromises: Promise<void>[] = [];
|
|
||||||
|
|
||||||
started: Promise<void>;
|
|
||||||
start: () => void;
|
|
||||||
|
|
||||||
synchronizer = new TrackSynchronizer();
|
|
||||||
|
|
||||||
totalDuration: number | null = null;
|
|
||||||
maxTimestamps = new Map<number, number>(); // Track ID -> timestamp
|
|
||||||
|
|
||||||
result: ConversionInfo;
|
|
||||||
|
|
||||||
constructor(public options: ConversionOptions) {
|
|
||||||
if (!options || typeof options !== 'object') {
|
if (!options || typeof options !== 'object') {
|
||||||
throw new TypeError('options must be an object.');
|
throw new TypeError('options must be an object.');
|
||||||
}
|
}
|
||||||
@@ -223,50 +247,38 @@ class Conversion {
|
|||||||
&& options.trim.start >= options.trim.end) {
|
&& options.trim.start >= options.trim.end) {
|
||||||
throw new TypeError('options.trim.start must be less than options.trim.end.');
|
throw new TypeError('options.trim.start must be less than options.trim.end.');
|
||||||
}
|
}
|
||||||
if (options.onProgress !== undefined && typeof options.onProgress !== 'function') {
|
if (options.computeProgress !== undefined && typeof options.computeProgress !== 'boolean') {
|
||||||
throw new TypeError('options.onProgress, when provided, must be a function.');
|
throw new TypeError('options.computeProgress, when provided, must be a boolean.');
|
||||||
}
|
|
||||||
if (options.abortSignal !== undefined && !(options.abortSignal instanceof AbortSignal)) {
|
|
||||||
throw new TypeError('options.abortSignal, when provided, must be an AbortSignal.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.input = options.input;
|
this._options = options;
|
||||||
this.output = options.output;
|
this._input = options.input;
|
||||||
|
this._output = options.output;
|
||||||
|
|
||||||
this.startTimestamp = options.trim?.start ?? 0;
|
this._startTimestamp = options.trim?.start ?? 0;
|
||||||
this.endTimestamp = options.trim?.end ?? Infinity;
|
this._endTimestamp = options.trim?.end ?? Infinity;
|
||||||
|
|
||||||
const { promise: started, resolve: start } = promiseWithResolvers();
|
const { promise: started, resolve: start } = promiseWithResolvers();
|
||||||
this.started = started;
|
this._started = started;
|
||||||
this.start = start;
|
this._start = start;
|
||||||
|
|
||||||
this.result = {
|
|
||||||
utilizedTracks: [],
|
|
||||||
discardedTracks: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
options.abortSignal?.addEventListener('abort', () => {
|
|
||||||
if (!this.output.isFinalizing) {
|
|
||||||
void this.output.cancel();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute() {
|
/** @internal */
|
||||||
const inputTracks = await this.input.getTracks();
|
async _init() {
|
||||||
const outputTrackCounts = this.output.format.getSupportedTrackCounts();
|
const inputTracks = await this._input.getTracks();
|
||||||
|
const outputTrackCounts = this._output.format.getSupportedTrackCounts();
|
||||||
|
|
||||||
for (const track of inputTracks) {
|
for (const track of inputTracks) {
|
||||||
if (this.totalTrackCount === outputTrackCounts.total.max) {
|
if (this._totalTrackCount === outputTrackCounts.total.max) {
|
||||||
this.result.discardedTracks.push({
|
this.discardedTracks.push({
|
||||||
track,
|
track,
|
||||||
reason: 'maxTrackCountReached',
|
reason: 'maxTrackCountReached',
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.addedCounts[track.type] === outputTrackCounts[track.type].max) {
|
if (this._addedCounts[track.type] === outputTrackCounts[track.type].max) {
|
||||||
this.result.discardedTracks.push({
|
this.discardedTracks.push({
|
||||||
track,
|
track,
|
||||||
reason: 'maxTrackCountOfTypeReached',
|
reason: 'maxTrackCountOfTypeReached',
|
||||||
});
|
});
|
||||||
@@ -274,52 +286,80 @@ class Conversion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (track.isVideoTrack()) {
|
if (track.isVideoTrack()) {
|
||||||
if (this.options.video?.discard) {
|
if (this._options.video?.discard) {
|
||||||
this.result.discardedTracks.push({
|
this.discardedTracks.push({
|
||||||
track,
|
track,
|
||||||
reason: 'discardedByUser',
|
reason: 'discardedByUser',
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.processVideoTrack(track);
|
await this._processVideoTrack(track);
|
||||||
} else if (track.isAudioTrack()) {
|
} else if (track.isAudioTrack()) {
|
||||||
if (this.options.audio?.discard) {
|
if (this._options.audio?.discard) {
|
||||||
this.result.discardedTracks.push({
|
this.discardedTracks.push({
|
||||||
track,
|
track,
|
||||||
reason: 'discardedByUser',
|
reason: 'discardedByUser',
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.processAudioTrack(track);
|
await this._processAudioTrack(track);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.options.onProgress) {
|
if (this._options.computeProgress) {
|
||||||
this.totalDuration = Math.min(
|
this._totalDuration = Math.min(
|
||||||
await this.input.computeDuration() - this.startTimestamp,
|
await this._input.computeDuration() - this._startTimestamp,
|
||||||
this.endTimestamp - this.startTimestamp,
|
this._endTimestamp - this._startTimestamp,
|
||||||
);
|
);
|
||||||
this.options.onProgress({ completion: 0 });
|
this.onProgress?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.output.start();
|
|
||||||
this.start();
|
|
||||||
|
|
||||||
await Promise.all(this.trackPromises);
|
|
||||||
|
|
||||||
await this.output.finalize();
|
|
||||||
|
|
||||||
this.options.onProgress?.({ completion: 1 });
|
|
||||||
|
|
||||||
return this.result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async processVideoTrack(track: InputVideoTrack) {
|
async execute() {
|
||||||
|
if (this._executed) {
|
||||||
|
throw new Error('Conversion cannot be executed twice.');
|
||||||
|
}
|
||||||
|
|
||||||
|
this._executed = true;
|
||||||
|
|
||||||
|
await this._output.start();
|
||||||
|
this._start();
|
||||||
|
|
||||||
|
await Promise.all(this._trackPromises);
|
||||||
|
|
||||||
|
if (this._canceled) {
|
||||||
|
await new Promise(() => {}); // Never resolve
|
||||||
|
}
|
||||||
|
|
||||||
|
await this._output.finalize();
|
||||||
|
|
||||||
|
if (this._options.computeProgress) {
|
||||||
|
this.progress = 1;
|
||||||
|
this.onProgress?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async cancel() {
|
||||||
|
if (this._output.state === 'finalizing' || this._output.state === 'finalized') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._canceled) {
|
||||||
|
console.warn('Conversion already canceled.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._canceled = true;
|
||||||
|
await this._output.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
async _processVideoTrack(track: InputVideoTrack) {
|
||||||
const sourceCodec = track.codec;
|
const sourceCodec = track.codec;
|
||||||
if (!sourceCodec) {
|
if (!sourceCodec) {
|
||||||
this.result.discardedTracks.push({
|
this.discardedTracks.push({
|
||||||
track,
|
track,
|
||||||
reason: 'unknownSourceCodec',
|
reason: 'unknownSourceCodec',
|
||||||
});
|
});
|
||||||
@@ -328,8 +368,8 @@ class Conversion {
|
|||||||
|
|
||||||
let videoSource: VideoSource;
|
let videoSource: VideoSource;
|
||||||
|
|
||||||
const totalRotation = normalizeRotation(track.rotation + (this.options.video?.rotate ?? 0));
|
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
|
const [originalWidth, originalHeight] = totalRotation % 180 === 0
|
||||||
? [track.codedWidth, track.codedHeight]
|
? [track.codedWidth, track.codedHeight]
|
||||||
@@ -342,78 +382,78 @@ class Conversion {
|
|||||||
// A lot of video encoders require that the dimensions be multiples of 2
|
// A lot of video encoders require that the dimensions be multiples of 2
|
||||||
const ceilToMultipleOfTwo = (value: number) => Math.ceil(value / 2) * 2;
|
const ceilToMultipleOfTwo = (value: number) => Math.ceil(value / 2) * 2;
|
||||||
|
|
||||||
if (this.options.video?.width !== undefined && this.options.video.height === undefined) {
|
if (this._options.video?.width !== undefined && this._options.video.height === undefined) {
|
||||||
width = ceilToMultipleOfTwo(this.options.video.width);
|
width = ceilToMultipleOfTwo(this._options.video.width);
|
||||||
height = ceilToMultipleOfTwo(Math.round(width / aspectRatio));
|
height = ceilToMultipleOfTwo(Math.round(width / aspectRatio));
|
||||||
} else if (this.options.video?.width === undefined && this.options.video?.height !== undefined) {
|
} else if (this._options.video?.width === undefined && this._options.video?.height !== undefined) {
|
||||||
height = ceilToMultipleOfTwo(this.options.video.height);
|
height = ceilToMultipleOfTwo(this._options.video.height);
|
||||||
width = ceilToMultipleOfTwo(Math.round(height * aspectRatio));
|
width = ceilToMultipleOfTwo(Math.round(height * aspectRatio));
|
||||||
} else if (this.options.video?.width !== undefined && this.options.video.height !== undefined) {
|
} else if (this._options.video?.width !== undefined && this._options.video.height !== undefined) {
|
||||||
width = ceilToMultipleOfTwo(this.options.video.width);
|
width = ceilToMultipleOfTwo(this._options.video.width);
|
||||||
height = ceilToMultipleOfTwo(this.options.video.height);
|
height = ceilToMultipleOfTwo(this._options.video.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
const firstTimestamp = await track.getFirstTimestamp();
|
const firstTimestamp = await track.getFirstTimestamp();
|
||||||
const needsReencode = !!this.options.video?.forceReencode || this.startTimestamp > 0 || firstTimestamp < 0;
|
const needsReencode = !!this._options.video?.forceReencode || this._startTimestamp > 0 || firstTimestamp < 0;
|
||||||
const needsRerender = width !== originalWidth
|
const needsRerender = width !== originalWidth
|
||||||
|| height !== originalHeight
|
|| height !== originalHeight
|
||||||
|| (totalRotation !== 0 && !outputSupportsRotation);
|
|| (totalRotation !== 0 && !outputSupportsRotation);
|
||||||
|
|
||||||
let videoCodecs = this.output.format.getSupportedVideoCodecs();
|
let videoCodecs = this._output.format.getSupportedVideoCodecs();
|
||||||
if (
|
if (
|
||||||
!needsReencode
|
!needsReencode
|
||||||
&& !this.options.video?.bitrate
|
&& !this._options.video?.bitrate
|
||||||
&& !needsRerender
|
&& !needsRerender
|
||||||
&& videoCodecs.includes(sourceCodec)
|
&& videoCodecs.includes(sourceCodec)
|
||||||
&& (!this.options.video?.codec || this.options.video?.codec === sourceCodec)
|
&& (!this._options.video?.codec || this._options.video?.codec === sourceCodec)
|
||||||
) {
|
) {
|
||||||
// Fast path, we can simply copy over the encoded packets
|
// Fast path, we can simply copy over the encoded packets
|
||||||
|
|
||||||
const source = new EncodedVideoPacketSource(sourceCodec);
|
const source = new EncodedVideoPacketSource(sourceCodec);
|
||||||
videoSource = source;
|
videoSource = source;
|
||||||
|
|
||||||
this.trackPromises.push((async () => {
|
this._trackPromises.push((async () => {
|
||||||
await this.started;
|
await this._started;
|
||||||
|
|
||||||
const sink = new EncodedPacketSink(track);
|
const sink = new EncodedPacketSink(track);
|
||||||
const decoderConfig = await track.getDecoderConfig();
|
const decoderConfig = await track.getDecoderConfig();
|
||||||
const meta: EncodedVideoChunkMetadata = { decoderConfig: decoderConfig ?? undefined };
|
const meta: EncodedVideoChunkMetadata = { decoderConfig: decoderConfig ?? undefined };
|
||||||
|
|
||||||
for await (const packet of sink.packets(undefined, this.endTimestamp)) {
|
for await (const packet of sink.packets(undefined, this._endTimestamp)) {
|
||||||
if (this.synchronizer.shouldWait(track.id, packet.timestamp)) {
|
if (this._synchronizer.shouldWait(track.id, packet.timestamp)) {
|
||||||
await this.synchronizer.wait(packet.timestamp);
|
await this._synchronizer.wait(packet.timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.options.abortSignal?.aborted) {
|
if (this._canceled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await source.add(packet, meta);
|
await source.add(packet, meta);
|
||||||
this.reportProgress(track.id, packet.timestamp + packet.duration);
|
this._reportProgress(track.id, packet.timestamp + packet.duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
await source.close();
|
await source.close();
|
||||||
this.synchronizer.closeTrack(track.id);
|
this._synchronizer.closeTrack(track.id);
|
||||||
})());
|
})());
|
||||||
} else {
|
} else {
|
||||||
// We need to decode & reencode the video
|
// We need to decode & reencode the video
|
||||||
|
|
||||||
const canDecode = await track.canDecode();
|
const canDecode = await track.canDecode();
|
||||||
if (!canDecode) {
|
if (!canDecode) {
|
||||||
this.result.discardedTracks.push({
|
this.discardedTracks.push({
|
||||||
track,
|
track,
|
||||||
reason: 'undecodableSourceCodec',
|
reason: 'undecodableSourceCodec',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.options.video?.codec) {
|
if (this._options.video?.codec) {
|
||||||
videoCodecs = videoCodecs.filter(codec => codec === this.options.video?.codec);
|
videoCodecs = videoCodecs.filter(codec => codec === this._options.video?.codec);
|
||||||
}
|
}
|
||||||
|
|
||||||
const encodableCodecs = await getEncodableVideoCodecs(videoCodecs, { width, height });
|
const encodableCodecs = await getEncodableVideoCodecs(videoCodecs, { width, height });
|
||||||
if (encodableCodecs.length === 0) {
|
if (encodableCodecs.length === 0) {
|
||||||
this.result.discardedTracks.push({
|
this.discardedTracks.push({
|
||||||
track,
|
track,
|
||||||
reason: 'noEncodableTargetCodec',
|
reason: 'noEncodableTargetCodec',
|
||||||
});
|
});
|
||||||
@@ -422,37 +462,37 @@ class Conversion {
|
|||||||
|
|
||||||
const encodingConfig: VideoEncodingConfig = {
|
const encodingConfig: VideoEncodingConfig = {
|
||||||
codec: encodableCodecs[0]!,
|
codec: encodableCodecs[0]!,
|
||||||
bitrate: this.options.video?.bitrate ?? QUALITY_HIGH,
|
bitrate: this._options.video?.bitrate ?? QUALITY_HIGH,
|
||||||
onEncodedPacket: sample => this.reportProgress(track.id, sample.timestamp + sample.duration),
|
onEncodedPacket: sample => this._reportProgress(track.id, sample.timestamp + sample.duration),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (needsRerender) {
|
if (needsRerender) {
|
||||||
const source = new VideoFrameSource(encodingConfig);
|
const source = new VideoFrameSource(encodingConfig);
|
||||||
videoSource = source;
|
videoSource = source;
|
||||||
|
|
||||||
this.trackPromises.push((async () => {
|
this._trackPromises.push((async () => {
|
||||||
await this.started;
|
await this._started;
|
||||||
|
|
||||||
const sink = new CanvasSink(track, {
|
const sink = new CanvasSink(track, {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
fit: this.options.video?.fit ?? 'fill',
|
fit: this._options.video?.fit ?? 'fill',
|
||||||
rotation: totalRotation, // Bake the rotation into the output
|
rotation: totalRotation, // Bake the rotation into the output
|
||||||
poolSize: 1,
|
poolSize: 1,
|
||||||
});
|
});
|
||||||
const iterator = sink.canvases(this.startTimestamp, this.endTimestamp);
|
const iterator = sink.canvases(this._startTimestamp, this._endTimestamp);
|
||||||
|
|
||||||
for await (const { canvas, timestamp, duration } of iterator) {
|
for await (const { canvas, timestamp, duration } of iterator) {
|
||||||
if (this.synchronizer.shouldWait(track.id, timestamp)) {
|
if (this._synchronizer.shouldWait(track.id, timestamp)) {
|
||||||
await this.synchronizer.wait(timestamp);
|
await this._synchronizer.wait(timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.options.abortSignal?.aborted) {
|
if (this._canceled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await source.add(new VideoFrame(canvas, {
|
await source.add(new VideoFrame(canvas, {
|
||||||
timestamp: 1e6 * Math.max(timestamp - this.startTimestamp, 0),
|
timestamp: 1e6 * Math.max(timestamp - this._startTimestamp, 0),
|
||||||
duration: 1e6 * duration,
|
duration: 1e6 * duration,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -461,21 +501,21 @@ class Conversion {
|
|||||||
const source = new VideoFrameSource(encodingConfig);
|
const source = new VideoFrameSource(encodingConfig);
|
||||||
videoSource = source;
|
videoSource = source;
|
||||||
|
|
||||||
this.trackPromises.push((async () => {
|
this._trackPromises.push((async () => {
|
||||||
await this.started;
|
await this._started;
|
||||||
|
|
||||||
const sink = new VideoFrameSink(track);
|
const sink = new VideoFrameSink(track);
|
||||||
|
|
||||||
for await (const { frame, timestamp } of sink.frames(this.startTimestamp, this.endTimestamp)) {
|
for await (const { frame, timestamp } of sink.frames(this._startTimestamp, this._endTimestamp)) {
|
||||||
if (this.synchronizer.shouldWait(track.id, timestamp)) {
|
if (this._synchronizer.shouldWait(track.id, timestamp)) {
|
||||||
await this.synchronizer.wait(timestamp);
|
await this._synchronizer.wait(timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
const clone = setVideoFrameTiming(frame, {
|
const clone = setVideoFrameTiming(frame, {
|
||||||
timestamp: Math.max(timestamp - this.startTimestamp, 0),
|
timestamp: Math.max(timestamp - this._startTimestamp, 0),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.options.abortSignal?.aborted) {
|
if (this._canceled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,25 +524,26 @@ class Conversion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await source.close();
|
await source.close();
|
||||||
this.synchronizer.closeTrack(track.id);
|
this._synchronizer.closeTrack(track.id);
|
||||||
})());
|
})());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.output.addVideoTrack(videoSource, {
|
this._output.addVideoTrack(videoSource, {
|
||||||
languageCode: track.languageCode,
|
languageCode: track.languageCode,
|
||||||
rotation: needsRerender ? 0 : totalRotation, // Rerendering will bake the rotation into the output
|
rotation: needsRerender ? 0 : totalRotation, // Rerendering will bake the rotation into the output
|
||||||
});
|
});
|
||||||
this.addedCounts.video++;
|
this._addedCounts.video++;
|
||||||
this.totalTrackCount++;
|
this._totalTrackCount++;
|
||||||
|
|
||||||
this.result.utilizedTracks.push(track);
|
this.utilizedTracks.push(track);
|
||||||
}
|
}
|
||||||
|
|
||||||
async processAudioTrack(track: InputAudioTrack) {
|
/** @internal */
|
||||||
|
async _processAudioTrack(track: InputAudioTrack) {
|
||||||
const sourceCodec = track.codec;
|
const sourceCodec = track.codec;
|
||||||
if (!sourceCodec) {
|
if (!sourceCodec) {
|
||||||
this.result.discardedTracks.push({
|
this.discardedTracks.push({
|
||||||
track,
|
track,
|
||||||
reason: 'unknownSourceCodec',
|
reason: 'unknownSourceCodec',
|
||||||
});
|
});
|
||||||
@@ -516,55 +557,55 @@ class Conversion {
|
|||||||
|
|
||||||
const firstTimestamp = await track.getFirstTimestamp();
|
const firstTimestamp = await track.getFirstTimestamp();
|
||||||
|
|
||||||
let numberOfChannels = this.options.audio?.numberOfChannels ?? originalNumberOfChannels;
|
let numberOfChannels = this._options.audio?.numberOfChannels ?? originalNumberOfChannels;
|
||||||
let sampleRate = this.options.audio?.sampleRate ?? originalSampleRate;
|
let sampleRate = this._options.audio?.sampleRate ?? originalSampleRate;
|
||||||
let needsResample = numberOfChannels !== originalNumberOfChannels
|
let needsResample = numberOfChannels !== originalNumberOfChannels
|
||||||
|| sampleRate !== originalSampleRate
|
|| sampleRate !== originalSampleRate
|
||||||
|| this.startTimestamp > 0
|
|| this._startTimestamp > 0
|
||||||
|| firstTimestamp < 0;
|
|| firstTimestamp < 0;
|
||||||
|
|
||||||
let audioCodecs = this.output.format.getSupportedAudioCodecs();
|
let audioCodecs = this._output.format.getSupportedAudioCodecs();
|
||||||
if (
|
if (
|
||||||
!this.options.audio?.forceReencode
|
!this._options.audio?.forceReencode
|
||||||
&& !this.options.audio?.bitrate
|
&& !this._options.audio?.bitrate
|
||||||
&& !needsResample
|
&& !needsResample
|
||||||
&& audioCodecs.includes(sourceCodec)
|
&& audioCodecs.includes(sourceCodec)
|
||||||
&& (!this.options.audio?.codec || this.options.audio.codec === sourceCodec)
|
&& (!this._options.audio?.codec || this._options.audio.codec === sourceCodec)
|
||||||
) {
|
) {
|
||||||
// Fast path, we can simply copy over the encoded packets
|
// Fast path, we can simply copy over the encoded packets
|
||||||
|
|
||||||
const source = new EncodedAudioPacketSource(sourceCodec);
|
const source = new EncodedAudioPacketSource(sourceCodec);
|
||||||
audioSource = source;
|
audioSource = source;
|
||||||
|
|
||||||
this.trackPromises.push((async () => {
|
this._trackPromises.push((async () => {
|
||||||
await this.started;
|
await this._started;
|
||||||
|
|
||||||
const sink = new EncodedPacketSink(track);
|
const sink = new EncodedPacketSink(track);
|
||||||
const decoderConfig = await track.getDecoderConfig();
|
const decoderConfig = await track.getDecoderConfig();
|
||||||
const meta: EncodedAudioChunkMetadata = { decoderConfig: decoderConfig ?? undefined };
|
const meta: EncodedAudioChunkMetadata = { decoderConfig: decoderConfig ?? undefined };
|
||||||
|
|
||||||
for await (const packet of sink.packets(undefined, this.endTimestamp)) {
|
for await (const packet of sink.packets(undefined, this._endTimestamp)) {
|
||||||
if (this.synchronizer.shouldWait(track.id, packet.timestamp)) {
|
if (this._synchronizer.shouldWait(track.id, packet.timestamp)) {
|
||||||
await this.synchronizer.wait(packet.timestamp);
|
await this._synchronizer.wait(packet.timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.options.abortSignal?.aborted) {
|
if (this._canceled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await source.add(packet, meta);
|
await source.add(packet, meta);
|
||||||
this.reportProgress(track.id, packet.timestamp + packet.duration);
|
this._reportProgress(track.id, packet.timestamp + packet.duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
await source.close();
|
await source.close();
|
||||||
this.synchronizer.closeTrack(track.id);
|
this._synchronizer.closeTrack(track.id);
|
||||||
})());
|
})());
|
||||||
} else {
|
} else {
|
||||||
// We need to decode & reencode the audio
|
// We need to decode & reencode the audio
|
||||||
|
|
||||||
const canDecode = await track.canDecode();
|
const canDecode = await track.canDecode();
|
||||||
if (!canDecode) {
|
if (!canDecode) {
|
||||||
this.result.discardedTracks.push({
|
this.discardedTracks.push({
|
||||||
track,
|
track,
|
||||||
reason: 'undecodableSourceCodec',
|
reason: 'undecodableSourceCodec',
|
||||||
});
|
});
|
||||||
@@ -573,8 +614,8 @@ class Conversion {
|
|||||||
|
|
||||||
let codecOfChoice: AudioCodec | null = null;
|
let codecOfChoice: AudioCodec | null = null;
|
||||||
|
|
||||||
if (this.options.audio?.codec) {
|
if (this._options.audio?.codec) {
|
||||||
audioCodecs = audioCodecs.filter(codec => codec === this.options.audio!.codec);
|
audioCodecs = audioCodecs.filter(codec => codec === this._options.audio!.codec);
|
||||||
}
|
}
|
||||||
|
|
||||||
const encodableCodecs = await getEncodableAudioCodecs(audioCodecs, {
|
const encodableCodecs = await getEncodableAudioCodecs(audioCodecs, {
|
||||||
@@ -611,7 +652,7 @@ class Conversion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (codecOfChoice === null) {
|
if (codecOfChoice === null) {
|
||||||
this.result.discardedTracks.push({
|
this.discardedTracks.push({
|
||||||
track,
|
track,
|
||||||
reason: 'noEncodableTargetCodec',
|
reason: 'noEncodableTargetCodec',
|
||||||
});
|
});
|
||||||
@@ -619,25 +660,25 @@ class Conversion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (needsResample) {
|
if (needsResample) {
|
||||||
audioSource = this.resampleAudio(track, codecOfChoice, numberOfChannels, sampleRate);
|
audioSource = this._resampleAudio(track, codecOfChoice, numberOfChannels, sampleRate);
|
||||||
} else {
|
} else {
|
||||||
const source = new AudioDataSource({
|
const source = new AudioDataSource({
|
||||||
codec: codecOfChoice,
|
codec: codecOfChoice,
|
||||||
bitrate: this.options.audio?.bitrate ?? QUALITY_HIGH,
|
bitrate: this._options.audio?.bitrate ?? QUALITY_HIGH,
|
||||||
onEncodedPacket: packet => this.reportProgress(track.id, packet.timestamp + packet.duration),
|
onEncodedPacket: packet => this._reportProgress(track.id, packet.timestamp + packet.duration),
|
||||||
});
|
});
|
||||||
audioSource = source;
|
audioSource = source;
|
||||||
|
|
||||||
this.trackPromises.push((async () => {
|
this._trackPromises.push((async () => {
|
||||||
await this.started;
|
await this._started;
|
||||||
|
|
||||||
const sink = new AudioDataSink(track);
|
const sink = new AudioDataSink(track);
|
||||||
for await (const { data, timestamp } of sink.data(undefined, this.endTimestamp)) {
|
for await (const { data, timestamp } of sink.data(undefined, this._endTimestamp)) {
|
||||||
if (this.synchronizer.shouldWait(track.id, timestamp)) {
|
if (this._synchronizer.shouldWait(track.id, timestamp)) {
|
||||||
await this.synchronizer.wait(timestamp);
|
await this._synchronizer.wait(timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.options.abortSignal?.aborted) {
|
if (this._canceled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -646,25 +687,26 @@ class Conversion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await source.close();
|
await source.close();
|
||||||
this.synchronizer.closeTrack(track.id);
|
this._synchronizer.closeTrack(track.id);
|
||||||
})());
|
})());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.output.addAudioTrack(audioSource, {
|
this._output.addAudioTrack(audioSource, {
|
||||||
languageCode: track.languageCode,
|
languageCode: track.languageCode,
|
||||||
});
|
});
|
||||||
this.addedCounts.audio++;
|
this._addedCounts.audio++;
|
||||||
this.totalTrackCount++;
|
this._totalTrackCount++;
|
||||||
|
|
||||||
this.result.utilizedTracks.push(track);
|
this.utilizedTracks.push(track);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resamples the audio by decoding it, playing it onto an OfflineAudioContext and encoding the
|
* Resamples the audio by decoding it, playing it onto an OfflineAudioContext and encoding the
|
||||||
* resulting AudioBuffer.
|
* resulting AudioBuffer.
|
||||||
|
* @internal
|
||||||
*/
|
*/
|
||||||
resampleAudio(
|
_resampleAudio(
|
||||||
track: InputAudioTrack,
|
track: InputAudioTrack,
|
||||||
codec: AudioCodec,
|
codec: AudioCodec,
|
||||||
targetNumberOfChannels: number,
|
targetNumberOfChannels: number,
|
||||||
@@ -672,16 +714,16 @@ class Conversion {
|
|||||||
) {
|
) {
|
||||||
const source = new AudioBufferSource({
|
const source = new AudioBufferSource({
|
||||||
codec,
|
codec,
|
||||||
bitrate: this.options.audio?.bitrate ?? QUALITY_HIGH,
|
bitrate: this._options.audio?.bitrate ?? QUALITY_HIGH,
|
||||||
onEncodedPacket: packet => this.reportProgress(track.id, packet.timestamp + packet.duration),
|
onEncodedPacket: packet => this._reportProgress(track.id, packet.timestamp + packet.duration),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.trackPromises.push((async () => {
|
this._trackPromises.push((async () => {
|
||||||
await this.started;
|
await this._started;
|
||||||
|
|
||||||
const trackDuration = Math.min(
|
const trackDuration = Math.min(
|
||||||
await track.computeDuration() - this.startTimestamp,
|
await track.computeDuration() - this._startTimestamp,
|
||||||
this.endTimestamp - this.startTimestamp,
|
this._endTimestamp - this._startTimestamp,
|
||||||
);
|
);
|
||||||
const totalFrameCount = Math.round(trackDuration * targetSampleRate);
|
const totalFrameCount = Math.round(trackDuration * targetSampleRate);
|
||||||
const maxChunkLength = 5 * targetSampleRate;
|
const maxChunkLength = 5 * targetSampleRate;
|
||||||
@@ -694,12 +736,14 @@ class Conversion {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const sink = new AudioBufferSink(track);
|
const sink = new AudioBufferSink(track);
|
||||||
for await (const { buffer, timestamp, duration } of sink.buffers(this.startTimestamp, this.endTimestamp)) {
|
const iterator = sink.buffers(this._startTimestamp, this._endTimestamp);
|
||||||
if (this.synchronizer.shouldWait(track.id, timestamp)) {
|
|
||||||
await this.synchronizer.wait(timestamp);
|
for await (const { buffer, timestamp, duration } of iterator) {
|
||||||
|
if (this._synchronizer.shouldWait(track.id, timestamp)) {
|
||||||
|
await this._synchronizer.wait(timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
const offsetTimestamp = timestamp - this.startTimestamp;
|
const offsetTimestamp = timestamp - this._startTimestamp;
|
||||||
const endTimestamp = offsetTimestamp + duration;
|
const endTimestamp = offsetTimestamp + duration;
|
||||||
|
|
||||||
// while loop, as a single source buffer may span multiple audio contexts
|
// while loop, as a single source buffer may span multiple audio contexts
|
||||||
@@ -725,7 +769,7 @@ class Conversion {
|
|||||||
// Render the audio
|
// Render the audio
|
||||||
const renderedBuffer = await currentContext.startRendering();
|
const renderedBuffer = await currentContext.startRendering();
|
||||||
|
|
||||||
if (this.options.abortSignal?.aborted) {
|
if (this._canceled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -753,7 +797,7 @@ class Conversion {
|
|||||||
if (currentContext) {
|
if (currentContext) {
|
||||||
const renderedBuffer = await currentContext.startRendering();
|
const renderedBuffer = await currentContext.startRendering();
|
||||||
|
|
||||||
if (this.options.abortSignal?.aborted) {
|
if (this._canceled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -761,30 +805,30 @@ class Conversion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await source.close();
|
await source.close();
|
||||||
this.synchronizer.closeTrack(track.id);
|
this._synchronizer.closeTrack(track.id);
|
||||||
})());
|
})());
|
||||||
|
|
||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
|
|
||||||
reportProgress(trackId: number, endTimestamp: number) {
|
/** @internal */
|
||||||
if (!this.options.onProgress) {
|
_reportProgress(trackId: number, endTimestamp: number) {
|
||||||
|
if (!this._options.computeProgress) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
assert(this.totalDuration !== null);
|
assert(this._totalDuration !== null);
|
||||||
|
|
||||||
this.maxTimestamps.set(trackId, Math.max(endTimestamp, this.maxTimestamps.get(trackId) ?? -Infinity));
|
this._maxTimestamps.set(trackId, Math.max(endTimestamp, this._maxTimestamps.get(trackId) ?? -Infinity));
|
||||||
|
|
||||||
let totalTimestamps = 0;
|
let totalTimestamps = 0;
|
||||||
for (const [, timestamp] of this.maxTimestamps) {
|
for (const [, timestamp] of this._maxTimestamps) {
|
||||||
totalTimestamps += timestamp;
|
totalTimestamps += timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
const averageTimestamp = totalTimestamps / this.totalTrackCount;
|
const averageTimestamp = totalTimestamps / this._totalTrackCount;
|
||||||
|
|
||||||
this.options.onProgress({
|
this.progress = 0.99 * clamp(averageTimestamp / this._totalDuration, 0, 1);
|
||||||
completion: 0.99 * clamp(averageTimestamp / this.totalDuration, 0, 1),
|
this.onProgress?.();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -109,7 +109,7 @@ export {
|
|||||||
AudioBufferSink,
|
AudioBufferSink,
|
||||||
WrappedAudioBuffer,
|
WrappedAudioBuffer,
|
||||||
} from './media-sink';
|
} from './media-sink';
|
||||||
export { convert, ConversionOptions, ConversionInfo } from './conversion';
|
export { convert, ConversionOptions, Conversion } from './conversion';
|
||||||
export {
|
export {
|
||||||
CustomVideoDecoder,
|
CustomVideoDecoder,
|
||||||
CustomAudioDecoder,
|
CustomAudioDecoder,
|
||||||
|
|||||||
+7
-7
@@ -44,16 +44,16 @@ export abstract class MediaSource {
|
|||||||
throw new Error('Source is not connected to an output track.');
|
throw new Error('Source is not connected to an output track.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._connectedTrack.output._canceled) {
|
if (this._connectedTrack.output.state === 'canceled') {
|
||||||
throw new Error('Output has been canceled.');
|
throw new Error('Output has been canceled.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this._connectedTrack.output._started) {
|
if (this._connectedTrack.output.state === 'finalizing' || this._connectedTrack.output.state === 'finalized') {
|
||||||
throw new Error('Output has not started.');
|
throw new Error('Output has been finalized.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._connectedTrack.output._finalizing) {
|
if (this._connectedTrack.output.state === 'pending') {
|
||||||
throw new Error('Output is finalizing.');
|
throw new Error('Output has not started.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._closed) {
|
if (this._closed) {
|
||||||
@@ -77,7 +77,7 @@ export abstract class MediaSource {
|
|||||||
throw new Error('Cannot call close without connecting the source to an output track.');
|
throw new Error('Cannot call close without connecting the source to an output track.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!connectedTrack.output._started) {
|
if (connectedTrack.output.state === 'pending') {
|
||||||
throw new Error('Cannot call close before output has been started.');
|
throw new Error('Cannot call close before output has been started.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ export abstract class MediaSource {
|
|||||||
|
|
||||||
this._closed = true;
|
this._closed = true;
|
||||||
|
|
||||||
if (connectedTrack.output._finalizing) {
|
if (connectedTrack.output.state === 'finalizing' || connectedTrack.output.state === 'finalized') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+61
-63
@@ -72,6 +72,7 @@ export class Output<
|
|||||||
> {
|
> {
|
||||||
format: F;
|
format: F;
|
||||||
target: T;
|
target: T;
|
||||||
|
state: 'pending' | 'started' | 'canceled' | 'finalizing' | 'finalized' = 'pending';
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_muxer: Muxer;
|
_muxer: Muxer;
|
||||||
@@ -80,32 +81,14 @@ export class Output<
|
|||||||
/** @internal */
|
/** @internal */
|
||||||
_tracks: OutputTrack[] = [];
|
_tracks: OutputTrack[] = [];
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_started = false;
|
_startPromise: Promise<void> | null = null;
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_canceled = false;
|
_cancelPromise: Promise<void> | null = null;
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_finalizing = false;
|
_finalizePromise: Promise<void> | null = null;
|
||||||
/** @internal */
|
|
||||||
_finalized = false;
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_mutex = new AsyncMutex();
|
_mutex = new AsyncMutex();
|
||||||
|
|
||||||
get isStarted() {
|
|
||||||
return this._started;
|
|
||||||
}
|
|
||||||
|
|
||||||
get isCanceled() {
|
|
||||||
return this._canceled;
|
|
||||||
}
|
|
||||||
|
|
||||||
get isFinalizing() {
|
|
||||||
return this._finalizing;
|
|
||||||
}
|
|
||||||
|
|
||||||
get isFinalized() {
|
|
||||||
return this._finalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(options: OutputOptions<F, T>) {
|
constructor(options: OutputOptions<F, T>) {
|
||||||
if (!options || typeof options !== 'object') {
|
if (!options || typeof options !== 'object') {
|
||||||
throw new TypeError('options must be an object.');
|
throw new TypeError('options must be an object.');
|
||||||
@@ -172,8 +155,8 @@ export class Output<
|
|||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
private _addTrack(type: OutputTrack['type'], source: MediaSource, metadata: object) {
|
private _addTrack(type: OutputTrack['type'], source: MediaSource, metadata: object) {
|
||||||
if (this._started) {
|
if (this.state !== 'pending') {
|
||||||
throw new Error('Cannot add track after output has started.');
|
throw new Error('Cannot add track after output has been started or canceled.');
|
||||||
}
|
}
|
||||||
if (source._connectedTrack) {
|
if (source._connectedTrack) {
|
||||||
throw new Error('Source is already used for a track.');
|
throw new Error('Source is already used for a track.');
|
||||||
@@ -262,13 +245,6 @@ export class Output<
|
|||||||
}
|
}
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
if (this._canceled) {
|
|
||||||
throw new Error('Output has been canceled.');
|
|
||||||
}
|
|
||||||
if (this._started) {
|
|
||||||
throw new Error('Output already started.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify minimum track count constraints
|
// Verify minimum track count constraints
|
||||||
const supportedTrackCounts = this.format.getSupportedTrackCounts();
|
const supportedTrackCounts = this.format.getSupportedTrackCounts();
|
||||||
for (const trackType of ALL_TRACK_TYPES) {
|
for (const trackType of ALL_TRACK_TYPES) {
|
||||||
@@ -298,60 +274,82 @@ export class Output<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._started = true;
|
if (this.state === 'canceled') {
|
||||||
this._writer.start();
|
throw new Error('Output has been canceled.');
|
||||||
|
|
||||||
const release = await this._mutex.acquire();
|
|
||||||
|
|
||||||
await this._muxer.start();
|
|
||||||
|
|
||||||
for (const track of this._tracks) {
|
|
||||||
track.source._start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
release();
|
if (this._startPromise) {
|
||||||
|
console.warn('Output has already been started.');
|
||||||
|
return this._startPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._startPromise = (async () => {
|
||||||
|
this.state = 'started';
|
||||||
|
this._writer.start();
|
||||||
|
|
||||||
|
const release = await this._mutex.acquire();
|
||||||
|
|
||||||
|
await this._muxer.start();
|
||||||
|
|
||||||
|
for (const track of this._tracks) {
|
||||||
|
track.source._start();
|
||||||
|
}
|
||||||
|
|
||||||
|
release();
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancel() {
|
async cancel() {
|
||||||
if (this._finalizing) {
|
if (this._cancelPromise) {
|
||||||
throw new Error('Cannot cancel after calling finalize.');
|
console.warn('Output has already been canceled.');
|
||||||
|
return this._cancelPromise;
|
||||||
|
} else if (this.state === 'finalizing' || this.state === 'finalized') {
|
||||||
|
console.warn('Output has already been finalized.');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (this._canceled) {
|
|
||||||
throw new Error('Output already canceled.');
|
|
||||||
}
|
|
||||||
this._canceled = true;
|
|
||||||
|
|
||||||
const release = await this._mutex.acquire();
|
return this._cancelPromise = (async () => {
|
||||||
|
this.state = 'canceled';
|
||||||
|
|
||||||
const promises = this._tracks.map(x => x.source._flushOrWaitForClose());
|
const release = await this._mutex.acquire();
|
||||||
await Promise.all(promises);
|
|
||||||
|
|
||||||
await this._writer.close();
|
const promises = this._tracks.map(x => x.source._flushOrWaitForClose());
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
release();
|
await this._writer.close();
|
||||||
|
|
||||||
|
release();
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
async finalize() {
|
async finalize() {
|
||||||
if (!this._started) {
|
if (this.state === 'pending') {
|
||||||
throw new Error('Cannot finalize before starting.');
|
throw new Error('Cannot finalize before starting.');
|
||||||
}
|
}
|
||||||
if (this._finalizing) {
|
if (this.state === 'canceled') {
|
||||||
throw new Error('Cannot call finalize twice.');
|
throw new Error('Cannot finalize after canceling.');
|
||||||
|
}
|
||||||
|
if (this._finalizePromise) {
|
||||||
|
console.warn('Output has already been finalized.');
|
||||||
|
return this._finalizePromise;
|
||||||
}
|
}
|
||||||
this._finalizing = true;
|
|
||||||
|
|
||||||
const release = await this._mutex.acquire();
|
return this._finalizePromise = (async () => {
|
||||||
|
this.state = 'finalizing';
|
||||||
|
|
||||||
const promises = this._tracks.map(x => x.source._flushOrWaitForClose());
|
const release = await this._mutex.acquire();
|
||||||
await Promise.all(promises);
|
|
||||||
|
|
||||||
await this._muxer.finalize();
|
const promises = this._tracks.map(x => x.source._flushOrWaitForClose());
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
await this._writer.flush();
|
await this._muxer.finalize();
|
||||||
await this._writer.finalize();
|
|
||||||
|
|
||||||
this._finalized = true;
|
await this._writer.flush();
|
||||||
|
await this._writer.finalize();
|
||||||
|
|
||||||
release();
|
this.state = 'finalized';
|
||||||
|
|
||||||
|
release();
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,4 @@
|
|||||||
- https://github.com/Vanilagy/mp4-muxer/issues/83 tell him it's possible now
|
- 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
|
- is this fixed? https://github.com/Vanilagy/webm-muxer/issues/50
|
||||||
- cross-track offset for streaming sources
|
- cross-track offset for streaming sources
|
||||||
- configurable fragmented mp4 fragment size, like the mp4-muxer PR
|
- configurable fragmented mp4 fragment size, like the mp4-muxer PR
|
||||||
- cancel in convert causes error
|
|
||||||
Reference in New Issue
Block a user