Merge branch 'main' into fork-branch

This commit is contained in:
Vanilagy
2025-09-12 15:46:12 +02:00
46 changed files with 4355 additions and 335 deletions
+67 -20
View File
@@ -48,6 +48,7 @@ import {
import { Output, TrackType } from './output';
import { Mp4OutputFormat } from './output-format';
import { AudioSample, VideoSample } from './sample';
import { MetadataTags, validateMetadataTags } from './tags';
import { NullTarget } from './target';
/**
@@ -86,6 +87,15 @@ export type ConversionOptions = {
/** The time in the input file in seconds at which the output file should end. Must be greater than `start`. */
end: number;
};
/**
* A callback that returns or resolves to the descriptive metadata tags that should be written to the output file.
* As input, this function will be passed the tags of the input file, allowing you to modify, augment or extend
* them.
*
* If no function is set, the input's metadata tags will be copied to the output.
*/
tags?: (inputTags: MetadataTags) => MaybePromise<MetadataTags>;
};
/**
@@ -433,6 +443,9 @@ export class Conversion {
&& options.trim.start >= options.trim.end) {
throw new TypeError('options.trim.start must be less than options.trim.end.');
}
if (options.tags !== undefined && typeof options.tags !== 'function') {
throw new TypeError('options.tags, when provided, must be a function.');
}
this._options = options;
this.input = options.input;
@@ -516,6 +529,32 @@ export class Conversion {
// Let's give the user a notice/warning about discarded tracks so they aren't confused
console.warn('Some tracks had to be discarded from the conversion:', unintentionallyDiscardedTracks);
}
// Now, let's deal with metadata tags
const inputTags = await this.input.getMetadataTags();
let outputTags: MetadataTags;
if (this._options.tags) {
const result = await this._options.tags(inputTags);
validateMetadataTags(result);
outputTags = result;
} else {
outputTags = inputTags;
}
// Somewhat dirty but pragmatic
const inputAndOutputFormatMatch = (await this.input.getFormat()).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);
}
/** Executes the conversion process. Resolves once conversion is complete. */
@@ -529,9 +568,14 @@ 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,
);
for (const track of this.utilizedTracks) {
this._maxTimestamps.set(track.id, 0);
}
this.onProgress?.(0);
}
@@ -723,7 +767,7 @@ export class Conversion {
await tempOutput.start();
const sink = new VideoSampleSink(track);
const firstSample = await sink.getSample(this._startTimestamp);
const firstSample = await sink.getSample(firstTimestamp); // Let's just use the first sample
if (firstSample) {
try {
@@ -1121,8 +1165,6 @@ export class Conversion {
await this._started;
const resampler = new AudioResampler({
sourceNumberOfChannels: track.numberOfChannels,
sourceSampleRate: track.sampleRate,
targetNumberOfChannels,
targetSampleRate,
startTime: this._startTimestamp,
@@ -1161,7 +1203,10 @@ export class Conversion {
}
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)!),
);
const minTimestamp = Math.min(...this._maxTimestamps.values());
const newProgress = clamp(minTimestamp / this._totalDuration, 0, 1);
@@ -1239,9 +1284,9 @@ class TrackSynchronizer {
* OfflineAudioContext.
*/
export class AudioResampler {
sourceSampleRate: number;
sourceSampleRate: number | null = null;
targetSampleRate: number;
sourceNumberOfChannels: number;
sourceNumberOfChannels: number | null = null;
targetNumberOfChannels: number;
startTime: number;
endTime: number;
@@ -1255,20 +1300,16 @@ export class AudioResampler {
/** The highest index written to in the current buffer */
maxWrittenFrame: number;
channelMixer!: (sourceData: Float32Array, sourceFrameIndex: number, targetChannelIndex: number) => number;
tempSourceBuffer: Float32Array;
tempSourceBuffer!: Float32Array;
constructor(options: {
sourceSampleRate: number;
targetSampleRate: number;
sourceNumberOfChannels: number;
targetNumberOfChannels: number;
startTime: number;
endTime: number;
onSample: (sample: AudioSample) => Promise<void>;
}) {
this.sourceSampleRate = options.sourceSampleRate;
this.targetSampleRate = options.targetSampleRate;
this.sourceNumberOfChannels = options.sourceNumberOfChannels;
this.targetNumberOfChannels = options.targetNumberOfChannels;
this.startTime = options.startTime;
this.endTime = options.endTime;
@@ -1280,17 +1321,14 @@ export class AudioResampler {
this.outputBuffer = new Float32Array(this.bufferSizeInSamples);
this.bufferStartFrame = 0;
this.maxWrittenFrame = -1;
this.setupChannelMixer();
// Pre-allocate temporary buffer for source data
this.tempSourceBuffer = new Float32Array(this.sourceSampleRate * this.sourceNumberOfChannels);
}
/**
* Sets up the channel mixer to handle up/downmixing in the case where input and output channel counts don't match.
*/
setupChannelMixer(): void {
doChannelMixerSetup(): void {
assert(this.sourceNumberOfChannels !== null);
const sourceNum = this.sourceNumberOfChannels;
const targetNum = this.targetNumberOfChannels;
@@ -1408,8 +1446,17 @@ export class AudioResampler {
}
async add(audioSample: AudioSample) {
if (!audioSample || audioSample._closed) {
return;
if (this.sourceSampleRate === null) {
// This is the first sample, so let's init the missing data. Initting the sample rate from the decoded
// sample is more reliable than using the file's metadata, because decoders are free to emit any sample rate
// they see fit.
this.sourceSampleRate = audioSample.sampleRate;
this.sourceNumberOfChannels = audioSample.numberOfChannels;
// Pre-allocate temporary buffer for source data
this.tempSourceBuffer = new Float32Array(this.sourceSampleRate * this.sourceNumberOfChannels);
this.doChannelMixerSetup();
}
const requiredSamples = audioSample.numberOfFrames * audioSample.numberOfChannels;