diff --git a/docs/api-config.json b/docs/api-config.json index e160b55..1cd9433 100644 --- a/docs/api-config.json +++ b/docs/api-config.json @@ -13,6 +13,7 @@ "Media sinks": "Methods for extracting media data from input files.", "Media sources": "Methods for adding media data to output files.", "Conversion": "A simple API for converting and transforming media files.", + "Metadata tags": "Descriptive metadata tags attached to media files.", "Codecs": "Codecs understood by Mediabunny.", "Encoding": "Encoder configuration and encodability checks.", "Custom coders": "API for adding custom encoders and decoders.", diff --git a/docs/guide/reading-media-files.md b/docs/guide/reading-media-files.md index d9af1c8..85d3afb 100644 --- a/docs/guide/reading-media-files.md +++ b/docs/guide/reading-media-files.md @@ -76,6 +76,12 @@ await input.getPrimaryVideoTrack(); // => InputVideoTrack | null await input.getPrimaryAudioTrack(); // => InputAudioTrack | null ``` +Mediabunny also lets you read descriptive metadata tags from media files, such as title, artist, or cover art: +```ts +await input.getMetadataTags(); // => MetadataTags +``` +For more info, see [`MetadataTags`](../api/MetadataTags). + ::: info Subtitle tracks are currently not supported for reading. ::: diff --git a/docs/guide/writing-media-files.md b/docs/guide/writing-media-files.md index 5e58081..c70a4b2 100644 --- a/docs/guide/writing-media-files.md +++ b/docs/guide/writing-media-files.md @@ -106,6 +106,25 @@ output.addAudioTrack(audioSource); Adding tracks to an `Output` will throw if the track is not compatible with the output format. Be sure to respect the [properties](./output-formats#format-properties) of the output format when adding tracks. ::: +## Setting metadata tags + +Mediabunny lets you write additional descriptive metadata tags to an output file, such as title, artist, or cover art: + +```ts +output.setMetadataTags({ + title: 'Big Buck Bunny', + artist: 'Blender Foundation', + date: new Date('2008-05-20'), + images: [{ + data: new Uint8Array([...]), + mimeType: 'image/jpeg', + kind: 'coverFront', + }], +}); +``` + +For more info on which tags you can write, see [`MetadataTags`](../api/MetadataTags). + ## Starting an output After all tracks have been added to the `Output`, you need to *start* it. Starting an output spins up the writing process, allowing you to now start sending media data to the output file. It also prevents you from adding any new tracks to it. diff --git a/src/index.ts b/src/index.ts index 07a3353..c7bbedd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -190,6 +190,7 @@ export { } from './custom-coder'; export { MetadataTags, + AttachedImage, RichImageData, } from './tags'; diff --git a/src/input.ts b/src/input.ts index 0ababc2..1b9a0c0 100644 --- a/src/input.ts +++ b/src/input.ts @@ -142,6 +142,7 @@ export class Input { return demuxer.getMimeType(); } + /** Returns descriptive metadata tags about the media file, such as title, author, date, or cover art. */ async getMetadataTags() { const demuxer = await this._getDemuxer(); return demuxer.getMetadataTags(); diff --git a/src/output.ts b/src/output.ts index e1ff034..9e47e86 100644 --- a/src/output.ts +++ b/src/output.ts @@ -220,6 +220,10 @@ export class Output< this._addTrack('subtitle', source, metadata); } + /** + * 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. + */ setMetadataTags(tags: MetadataTags) { validateMetadataTags(tags); diff --git a/src/tags.ts b/src/tags.ts index 340638d..d583027 100644 --- a/src/tags.ts +++ b/src/tags.ts @@ -6,34 +6,98 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -/** @public */ +/** + * Represents descriptive (non-technical) metadata about a media file, such as title, author, date, or cover art. + * 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). + * + * @group Metadata tags + * @public + */ export type MetadataTags = { + /** Title of the media (e.g. Gangnam Style, Titanic, etc.) */ title?: string; + /** Short description or subtitle of the media. */ description?: string; + /** Primary artist(s) or creator(s) of the work. */ artist?: string; + /** Album, collection, or compilation the media belongs to. */ album?: string; + /** Main credited artist for the album/collection as a whole. */ albumArtist?: string; + /** Position of this track within its album or collection (1-based). */ trackNumber?: number; + /** Total number of tracks in the album or collection. */ tracksTotal?: number; + /** Disc index if the release spans multiple discs (1-based). */ discNumber?: number; + /** Total number of discs in the release. */ discsTotal?: number; + /** Genre or category describing the media's style or content (e.g. Metal, Horror, etc.) */ genre?: string; + /** Release, recording or creation date of the media. */ date?: Date; + /** Full text lyrics or transcript associated with the media. */ lyrics?: string; + /** Freeform notes, remarks or commentary about the media. */ comment?: string; - images?: { - data: Uint8Array; - mimeType: string; - kind: 'coverFront' | 'coverBack' | 'unknown'; - name?: string; - description?: string; - }[]; + /** Embedded images such as cover art, booklet scans, artwork or preview frames. */ + images?: AttachedImage[]; + /** + * The raw, underlying metadata tags. + * + * This field can be used for both reading and writing. When reading, it represents the original tags that were used + * to derive the normalized fields, and any additional metadata that Mediabunny doesn't understand. When writing, it + * can be used to set arbitrary metadata tags in the output file. + * + * The format of these tags differs per format: + * - MP4/QuickTime: By default, the keys refer to the names of the individual atoms in the `'ilst'` atom inside the + * `'meta'` atom, and the values are derived from the content of the `'data'` atom inside them. When a `'keys'` atom + * is also used, then the keys reflect the keys specified there (such as `'com.apple.quicktime.version'`). + * Additionally, any atoms within the `'udta'` atom are dumped into here, however with unknown internal format + * (`Uint8Array`). + * - Matroska: `SimpleTag` elements whose target is 50 (MOVIE), either containing string or `Uint8Array` values. + * - MP3: The ID3v2 tags, or a single `'TAG'` key with the contents of the ID3v1 tag. + * - Ogg: The key-value string pairs from the Vorbis-style comment header (see RFC 7845, Section 5.2). + * Additionally, the `'vendor'` key refers to the vendor string within this header. + * - WAVE: The individual metadata chunks within the RIFF INFO chunk. Values are always ISO 8859-1 strings. + */ raw?: Record; }; -/** @public */ +/** + * An embedded image such as cover art, booklet scan, artwork or preview frame. + * + * @group Metadata tags + * @public + */ +export type AttachedImage = { + /** The raw image data. */ + data: Uint8Array; + /** An RFC 6838 MIME type (e.g. image/jpeg, image/png, etc.) */ + mimeType: string; + /** The kind or purpose of the image. */ + kind: 'coverFront' | 'coverBack' | 'unknown'; + /** The name of the image file. */ + name?: string; + /** A short description of the image. */ + description?: string; +}; + +/** + * Image data with additional metadata. + * + * @group Metadata tags + * @public + */ export class RichImageData { - constructor(public data: Uint8Array, public mimeType: string) {} + /** Creates a new {@link RichImageData}. */ + constructor( + /** The raw image data. */ + public data: Uint8Array, + /** An RFC 6838 MIME type (e.g. image/jpeg, image/png, etc.) */ + public mimeType: string, + ) {} } export const validateMetadataTags = (tags: MetadataTags) => { @@ -96,8 +160,8 @@ export const validateMetadataTags = (tags: MetadataTags) => { if (typeof image.mimeType !== 'string') { throw new TypeError('Each image.mimeType must be a string.'); } - if (!['coverFront', 'coverBack', 'other'].includes(image.kind)) { - throw new TypeError('Each image.kind must be \'coverFront\', \'coverBack\', or \'other\'.'); + if (!['coverFront', 'coverBack', 'unknown'].includes(image.kind)) { + throw new TypeError('Each image.kind must be \'coverFront\', \'coverBack\', or \'unknown\'.'); } } }