Add metadata tag support to Conversion API

This commit is contained in:
Vanilagy
2025-09-05 23:34:24 +02:00
parent c6cb8f7b43
commit dcd99f8930
7 changed files with 230 additions and 13 deletions
+39
View File
@@ -47,6 +47,7 @@ import {
} from './misc';
import { Output, TrackType } from './output';
import { AudioSample, VideoSample } from './sample';
import { MetadataTags, validateMetadataTags } from './tags';
/**
* The options for media file conversion.
@@ -84,6 +85,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>;
};
/**
@@ -397,6 +407,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;
@@ -480,6 +493,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. */
+6 -1
View File
@@ -252,7 +252,12 @@ export class MatroskaDemuxer extends Demuxer {
// Load metadata tags from each segment lazily (only once)
for (const segment of this.segments) {
if (!segment.metadataTagsCollected) {
await this.loadSegmentMetadata(segment);
if (this.reader.fileSize !== null) {
await this.loadSegmentMetadata(segment);
} else {
// The seeking would be too crazy, let's not
}
segment.metadataTagsCollected = true;
}
}
+2
View File
@@ -223,6 +223,8 @@ export class Output<
/**
* Sets descriptive metadata tags about the media file, such as title, author, date, or cover art. When called
* multiple times, only the metadata from the last call will be used.
*
* Must be called before output is started.
*/
setMetadataTags(tags: MetadataTags) {
validateMetadataTags(tags);
+32
View File
@@ -36,6 +36,8 @@ export abstract class Source {
abstract _retrieveSize(): MaybePromise<number | null>;
/** @internal */
abstract _read(start: number, end: number): MaybePromise<ReadResult | null>;
/** @internal */
abstract get _supportsRandomAccess(): boolean;
/** @internal */
private _sizePromise: Promise<number | null> | null = null;
@@ -113,6 +115,11 @@ export class BufferSource extends Source {
offset: 0,
};
}
/** @internal */
get _supportsRandomAccess() {
return true;
}
}
/**
@@ -210,6 +217,11 @@ export class BlobSource extends Source {
worker.running = false;
}
/** @internal */
get _supportsRandomAccess() {
return true;
}
}
const URL_SOURCE_MIN_LOAD_AMOUNT = 0.5 * 2 ** 20; // 0.5 MiB
@@ -488,6 +500,11 @@ export class UrlSource extends Source {
}
}
}
/** @internal */
get _supportsRandomAccess() {
return true;
}
}
/**
@@ -559,6 +576,11 @@ export class FilePathSource extends Source {
_retrieveSize(): MaybePromise<number> {
return this._streamSource._retrieveSize();
}
/** @internal */
get _supportsRandomAccess() {
return true;
}
}
/**
@@ -728,6 +750,11 @@ export class StreamSource extends Source {
worker.running = false;
}
/** @internal */
get _supportsRandomAccess() {
return true;
}
}
type ReadableStreamSourcePendingSlice = {
@@ -987,6 +1014,11 @@ export class ReadableStreamSource extends Source {
this._pulling = false;
}
/** @internal */
get _supportsRandomAccess() {
return false;
}
}
type PrefetchProfile = (start: number, end: number, workers: ReadWorker[]) => {
+8
View File
@@ -11,6 +11,14 @@
* Common tags are normalized by Mediabunny into a uniform format, while the `raw` field can be used to directly read or
* write the underlying metadata tags (which differ by format).
*
* - For MP4/QuickTime files, the metadata refers to the data in `'moov'`-level `'udta'` and `'meta'` atoms.
* - For Matroska files, the metadata refers to the Tags and Attachments elements whose target is 50 (MOVIE).
* - For MP3 files, the metadata refers to the ID3v2 or ID3v1 tags.
* - For Ogg files, there is no global metadata so instead, the metadata refers to the combined metadata of all tracks,
* in Vorbis-style comment headers.
* - For WAVE files, the metadata refers to the chunks within the RIFF INFO chunk.
* - For ADTS files, there is no metadata.
*
* @group Metadata tags
* @public
*/