mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-03 13:53:53 +02:00
Merge branch 'main' into fork-branch
This commit is contained in:
+67
-20
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user