diff --git a/dev/demux.html b/dev/demux.html
index 31a4a52..212c601 100644
--- a/dev/demux.html
+++ b/dev/demux.html
@@ -14,7 +14,11 @@
source: new Mediabunny.BlobSource(file),
});
- console.log(await input.getMetadata());
+ const audioTrack = await input.getPrimaryAudioTrack();
+ const sink = new Mediabunny.EncodedPacketSink(audioTrack);
+
+ console.log(await audioTrack.getDecoderConfig())
+ console.log(await sink.getFirstPacket());
/*
diff --git a/src/index.ts b/src/index.ts
index 9bd1991..07a3353 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -188,5 +188,9 @@ export {
registerDecoder,
registerEncoder,
} from './custom-coder';
+export {
+ MetadataTags,
+ RichImageData,
+} from './tags';
// 🐡🦔
diff --git a/src/input.ts b/src/input.ts
index f7fe149..0ababc2 100644
--- a/src/input.ts
+++ b/src/input.ts
@@ -142,7 +142,7 @@ export class Input {
return demuxer.getMimeType();
}
- async getMetadata() {
+ async getMetadataTags() {
const demuxer = await this._getDemuxer();
return demuxer.getMetadataTags();
}
diff --git a/src/matroska/matroska-demuxer.ts b/src/matroska/matroska-demuxer.ts
index fbcfec4..d1a22df 100644
--- a/src/matroska/matroska-demuxer.ts
+++ b/src/matroska/matroska-demuxer.ts
@@ -1520,13 +1520,13 @@ export class MatroskaDemuxer extends Demuxer {
case 'part_number': {
const parts = value.split('/');
const trackNum = Number.parseInt(parts[0]!, 10);
- const trackNumMax = parts[1] && Number.parseInt(parts[1], 10);
+ const tracksTotal = parts[1] && Number.parseInt(parts[1], 10);
if (Number.isInteger(trackNum) && trackNum > 0) {
metadataTags.trackNumber ??= trackNum;
}
- if (trackNumMax && Number.isInteger(trackNumMax) && trackNumMax > 0) {
- metadataTags.tracksTotal ??= trackNumMax;
+ if (tracksTotal && Number.isInteger(tracksTotal) && tracksTotal > 0) {
+ metadataTags.tracksTotal ??= tracksTotal;
}
}; break;
@@ -1534,13 +1534,13 @@ export class MatroskaDemuxer extends Demuxer {
case 'disc': {
const discParts = value.split('/');
const discNum = Number.parseInt(discParts[0]!, 10);
- const discNumMax = discParts[1] && Number.parseInt(discParts[1], 10);
+ const discsTotal = discParts[1] && Number.parseInt(discParts[1], 10);
if (Number.isInteger(discNum) && discNum > 0) {
metadataTags.discNumber ??= discNum;
}
- if (discNumMax && Number.isInteger(discNumMax) && discNumMax > 0) {
- metadataTags.discsTotal ??= discNumMax;
+ if (discsTotal && Number.isInteger(discsTotal) && discsTotal > 0) {
+ metadataTags.discsTotal ??= discsTotal;
}
}; break;
}
diff --git a/src/mp3/mp3-demuxer.ts b/src/mp3/mp3-demuxer.ts
index 742a626..dad16a1 100644
--- a/src/mp3/mp3-demuxer.ts
+++ b/src/mp3/mp3-demuxer.ts
@@ -60,8 +60,9 @@ export class Mp3Demuxer extends Demuxer {
await this.advanceReader();
}
- // There has to be a frame if this demuxer got selected
- assert(this.firstFrameHeader);
+ if (!this.firstFrameHeader) {
+ throw new Error('No valid MP3 frame found.');
+ }
this.tracks = [new InputAudioTrack(new Mp3AudioTrackBacking(this))];
})();
diff --git a/src/mp3/mp3-reader.ts b/src/mp3/mp3-reader.ts
index 869e702..f857528 100644
--- a/src/mp3/mp3-reader.ts
+++ b/src/mp3/mp3-reader.ts
@@ -281,13 +281,13 @@ export const parseId3V2Tag = (slice: FileSlice, header: Id3V2Header, tags: Metad
const trackText = reader.readId3V2EncodingAndText(frameEndPos);
const parts = trackText.split('/');
const trackNum = Number.parseInt(parts[0]!, 10);
- const trackNumMax = parts[1] && Number.parseInt(parts[1], 10);
+ const tracksTotal = parts[1] && Number.parseInt(parts[1], 10);
if (Number.isInteger(trackNum) && trackNum > 0) {
tags.trackNumber ??= trackNum;
}
- if (trackNumMax && Number.isInteger(trackNumMax) && trackNumMax > 0) {
- tags.tracksTotal ??= trackNumMax;
+ if (tracksTotal && Number.isInteger(tracksTotal) && tracksTotal > 0) {
+ tags.tracksTotal ??= tracksTotal;
}
}; break;
@@ -296,13 +296,13 @@ export const parseId3V2Tag = (slice: FileSlice, header: Id3V2Header, tags: Metad
const discText = reader.readId3V2EncodingAndText(frameEndPos);
const parts = discText.split('/');
const discNum = Number.parseInt(parts[0]!, 10);
- const discNumMax = parts[1] && Number.parseInt(parts[1], 10);
+ const discsTotal = parts[1] && Number.parseInt(parts[1], 10);
if (Number.isInteger(discNum) && discNum > 0) {
tags.discNumber ??= discNum;
}
- if (discNumMax && Number.isInteger(discNumMax) && discNumMax > 0) {
- tags.discsTotal ??= discNumMax;
+ if (discsTotal && Number.isInteger(discsTotal) && discsTotal > 0) {
+ tags.discsTotal ??= discsTotal;
}
}; break;
diff --git a/src/ogg/ogg-demuxer.ts b/src/ogg/ogg-demuxer.ts
index 88e248b..940a5d1 100644
--- a/src/ogg/ogg-demuxer.ts
+++ b/src/ogg/ogg-demuxer.ts
@@ -334,40 +334,40 @@ export class OggDemuxer extends Demuxer {
case 'TRACKNUMBER': {
const parts = value.split('/');
const trackNum = Number.parseInt(parts[0]!, 10);
- const trackNumMax = parts[1] && Number.parseInt(parts[1], 10);
+ const tracksTotal = parts[1] && Number.parseInt(parts[1], 10);
if (Number.isInteger(trackNum) && trackNum > 0) {
this.metadataTags.trackNumber ??= trackNum;
}
- if (trackNumMax && Number.isInteger(trackNumMax) && trackNumMax > 0) {
- this.metadataTags.tracksTotal ??= trackNumMax;
+ if (tracksTotal && Number.isInteger(tracksTotal) && tracksTotal > 0) {
+ this.metadataTags.tracksTotal ??= tracksTotal;
}
}; break;
case 'TRACKTOTAL': {
- const trackNumMax = Number.parseInt(value, 10);
- if (Number.isInteger(trackNumMax) && trackNumMax > 0) {
- this.metadataTags.tracksTotal ??= trackNumMax;
+ const tracksTotal = Number.parseInt(value, 10);
+ if (Number.isInteger(tracksTotal) && tracksTotal > 0) {
+ this.metadataTags.tracksTotal ??= tracksTotal;
}
}; break;
case 'DISCNUMBER': {
const parts = value.split('/');
const discNum = Number.parseInt(parts[0]!, 10);
- const discNumMax = parts[1] && Number.parseInt(parts[1], 10);
+ const discsTotal = parts[1] && Number.parseInt(parts[1], 10);
if (Number.isInteger(discNum) && discNum > 0) {
this.metadataTags.discNumber ??= discNum;
}
- if (discNumMax && Number.isInteger(discNumMax) && discNumMax > 0) {
- this.metadataTags.discsTotal ??= discNumMax;
+ if (discsTotal && Number.isInteger(discsTotal) && discsTotal > 0) {
+ this.metadataTags.discsTotal ??= discsTotal;
}
}; break;
case 'DISCTOTAL': {
- const trackNumMax = Number.parseInt(value, 10);
- if (Number.isInteger(trackNumMax) && trackNumMax > 0) {
- this.metadataTags.tracksTotal ??= trackNumMax;
+ const discsTotal = Number.parseInt(value, 10);
+ if (Number.isInteger(discsTotal) && discsTotal > 0) {
+ this.metadataTags.discsTotal ??= discsTotal;
}
}; break;
diff --git a/src/ogg/ogg-muxer.ts b/src/ogg/ogg-muxer.ts
index 8e2cc5e..05f79a7 100644
--- a/src/ogg/ogg-muxer.ts
+++ b/src/ogg/ogg-muxer.ts
@@ -334,21 +334,21 @@ export class OggMuxer extends Muxer {
}; break;
case 'trackNumber': {
- addCommentTag('DISCNUMBER', value.toString());
- }; break;
-
- case 'tracksTotal': {
- addCommentTag('DISCTOTAL', value.toString());
- }; break;
-
- case 'discNumber': {
addCommentTag('TRACKNUMBER', value.toString());
}; break;
- case 'discsTotal': {
+ case 'tracksTotal': {
addCommentTag('TRACKTOTAL', value.toString());
}; break;
+ case 'discNumber': {
+ addCommentTag('DISCNUMBER', value.toString());
+ }; break;
+
+ case 'discsTotal': {
+ addCommentTag('DISCTOTAL', value.toString());
+ }; break;
+
case 'images': {
for (const image of value) {
// https://datatracker.ietf.org/doc/rfc9639/ Section 8.8
diff --git a/src/tags.ts b/src/tags.ts
index 68b6947..340638d 100644
--- a/src/tags.ts
+++ b/src/tags.ts
@@ -1,3 +1,12 @@
+/*!
+ * Copyright (c) 2025-present, Vanilagy and contributors
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+/** @public */
export type MetadataTags = {
title?: string;
description?: string;
@@ -22,6 +31,7 @@ export type MetadataTags = {
raw?: Record;
};
+/** @public */
export class RichImageData {
constructor(public data: Uint8Array, public mimeType: string) {}
}
diff --git a/src/wave/wave-demuxer.ts b/src/wave/wave-demuxer.ts
index 0c7ef97..974cf25 100644
--- a/src/wave/wave-demuxer.ts
+++ b/src/wave/wave-demuxer.ts
@@ -230,13 +230,13 @@ export class WaveDemuxer extends Demuxer {
case 'TRCK': {
const parts = value.split('/');
const trackNum = Number.parseInt(parts[0]!, 10);
- const trackNumMax = parts[1] && Number.parseInt(parts[1], 10);
+ const tracksTotal = parts[1] && Number.parseInt(parts[1], 10);
if (Number.isInteger(trackNum) && trackNum > 0) {
this.metadataTags.trackNumber ??= trackNum;
}
- if (trackNumMax && Number.isInteger(trackNumMax) && trackNumMax > 0) {
- this.metadataTags.tracksTotal ??= trackNumMax;
+ if (tracksTotal && Number.isInteger(tracksTotal) && tracksTotal > 0) {
+ this.metadataTags.tracksTotal ??= tracksTotal;
}
}; break;
diff --git a/test/node/metadata-tags.test.ts b/test/node/metadata-tags.test.ts
new file mode 100644
index 0000000..214322b
--- /dev/null
+++ b/test/node/metadata-tags.test.ts
@@ -0,0 +1,390 @@
+import { expect, test } from 'vitest';
+import { Output } from '../../src/output.js';
+import {
+ MkvOutputFormat,
+ MovOutputFormat,
+ Mp3OutputFormat,
+ Mp4OutputFormat,
+ OggOutputFormat,
+ WavOutputFormat,
+} from '../../src/output-format.js';
+import { BufferTarget } from '../../src/target.js';
+import { EncodedAudioPacketSource } from '../../src/media-source.js';
+import { EncodedPacket } from '../../src/packet.js';
+import { Input } from '../../src/input.js';
+import { BufferSource, FilePathSource } from '../../src/source.js';
+import { ALL_FORMATS } from '../../src/input-format.js';
+import { MetadataTags } from '../../src/tags.js';
+import path from 'node:path';
+import { AudioCodec, buildAudioCodecString } from '../../src/codec.js';
+
+const __dirname = new URL('.', import.meta.url).pathname;
+
+const createDummyAudioTrack = (codec: AudioCodec, output: Output) => {
+ const source = new EncodedAudioPacketSource(codec);
+ output.addAudioTrack(source);
+
+ return {
+ async addPacket() {
+ // Data to make it behave like an MP3 frame
+ const data = new Uint8Array(2000);
+ data[0] = 255;
+ data[1] = 251;
+ data[2] = 224;
+ data[3] = 100;
+
+ // OpusHead description
+ const description = new Uint8Array(64);
+ description[0] = 0x4f;
+ description[1] = 0x70;
+ description[2] = 0x75;
+ description[3] = 0x73;
+ description[4] = 0x48;
+ description[5] = 0x65;
+ description[6] = 0x61;
+ description[7] = 0x64;
+
+ await source.add(
+ new EncodedPacket(data, 'key', 0, 1),
+ {
+ decoderConfig: {
+ codec: buildAudioCodecString(codec, 2, 48000),
+ numberOfChannels: 2,
+ sampleRate: 48000,
+ description,
+ },
+ },
+ );
+ },
+ };
+};
+
+const coverArt = new Uint8Array(1024);
+coverArt[0] = 69;
+const songMetadata: MetadataTags = {
+ title: 'Trying to Feel Alive',
+ description: 'A song',
+ artist: 'Porter Robinson & others',
+ album: 'Nurture',
+ albumArtist: 'Porter Robinson',
+ genre: 'Electronic',
+ comment: 'Some of this info is intentionally incorrect',
+ lyrics: 'Well, do you feel better now?\nI thought I\'d run until the sky came out',
+ trackNumber: 13,
+ tracksTotal: 14,
+ discNumber: 1,
+ discsTotal: 1,
+ date: new Date(2021, 3, 23),
+ images: [{
+ data: coverArt,
+ kind: 'coverFront',
+ mimeType: 'image/jpeg',
+ description: 'This image shows a person laying in a field of grass',
+ }],
+};
+
+test('Read and write metadata, MP4', async () => {
+ const output = new Output({
+ format: new Mp4OutputFormat(),
+ target: new BufferTarget(),
+ });
+
+ output.setMetadataTags({
+ ...songMetadata,
+ raw: {
+ '©hog': 'pish',
+ '©nam': 'Cheerleader', // Test that it doesn't override the main title
+ },
+ });
+
+ const dummyTrack = createDummyAudioTrack('aac', output);
+
+ await output.start();
+ await dummyTrack.addPacket();
+ await output.finalize();
+
+ const input = new Input({
+ source: new BufferSource(output.target.buffer!),
+ formats: ALL_FORMATS,
+ });
+
+ const readTags = await input.getMetadataTags();
+
+ expect(readTags.title).toBe(songMetadata.title);
+ expect(readTags.description).toBe(songMetadata.description);
+ expect(readTags.artist).toBe(songMetadata.artist);
+ expect(readTags.album).toBe(songMetadata.album);
+ expect(readTags.albumArtist).toBe(songMetadata.albumArtist);
+ expect(readTags.comment).toBe(songMetadata.comment);
+ expect(readTags.lyrics).toBe(songMetadata.lyrics);
+ expect(readTags.trackNumber).toBe(songMetadata.trackNumber);
+ expect(readTags.tracksTotal).toBe(songMetadata.tracksTotal);
+ expect(readTags.discNumber).toBe(songMetadata.discNumber);
+ expect(readTags.discsTotal).toBe(songMetadata.discsTotal);
+ expect(readTags.date).toEqual(readTags.date);
+ expect(readTags.images).toHaveLength(1);
+ expect(readTags.images![0]!.data).toEqual(coverArt);
+ expect(readTags.images![0]!.mimeType).toEqual('image/jpeg');
+ expect(readTags.images![0]!.kind).toEqual('coverFront');
+ expect(readTags.images![0]!.description).toBeUndefined(); // Lost in MP4
+
+ expect(readTags.raw!['©nam']).toBe(songMetadata.title);
+ expect(readTags.raw!['trkn']).instanceOf(Uint8Array);
+ expect(readTags.raw!['©hog']).toBe('pish');
+});
+
+test('Read and write metadata, QuickTime', async () => {
+ const output = new Output({
+ format: new MovOutputFormat(),
+ target: new BufferTarget(),
+ });
+
+ output.setMetadataTags({
+ ...songMetadata,
+ raw: {
+ '©sic': 'ko mode',
+ },
+ });
+
+ const dummyTrack = createDummyAudioTrack('aac', output);
+
+ await output.start();
+ await dummyTrack.addPacket();
+ await output.finalize();
+
+ const input = new Input({
+ source: new BufferSource(output.target.buffer!),
+ formats: ALL_FORMATS,
+ });
+
+ const readTags = await input.getMetadataTags();
+
+ expect(readTags.title).toBe(songMetadata.title);
+ expect(readTags.description).toBe(songMetadata.description);
+ expect(readTags.artist).toBe(songMetadata.artist);
+ expect(readTags.album).toBe(songMetadata.album);
+ expect(readTags.albumArtist).toBe(songMetadata.albumArtist);
+ expect(readTags.comment).toBe(songMetadata.comment);
+ expect(readTags.lyrics).toBe(songMetadata.lyrics);
+ expect(readTags.trackNumber).toBeUndefined(); // All of these don't work in MOV
+ expect(readTags.tracksTotal).toBeUndefined();
+ expect(readTags.discNumber).toBeUndefined();
+ expect(readTags.discsTotal).toBeUndefined();
+ expect(readTags.date).toEqual(readTags.date);
+ expect(readTags.images).toBeUndefined();
+
+ expect(readTags.raw!['©nam']).toBe(songMetadata.title);
+ // We don't know what the data type is, so the demuxer just returns Uint8Array
+ expect(readTags.raw!['©sic']).toEqual(new Uint8Array([
+ 0, 7, // String length 7
+ 85, 196, // Language code for 'und'
+ 107, 111, 32, 109, 111, 100, 101, // 'ko mode'
+ ]));
+});
+
+test('Read MOV metadata tags, ilst with keys', async () => {
+ const input = new Input({
+ source: new FilePathSource(path.join(__dirname, '../public/trunc-buck-bunny.mov')),
+ formats: ALL_FORMATS,
+ });
+
+ const tags = await input.getMetadataTags();
+ expect(tags.raw).not.toBeUndefined();
+ expect(tags.raw!['com.apple.quicktime.version']).toBe('7.4.1 (14) 0x7418000 (Mac OS X, 10.5.2, 9C31)');
+ expect(tags.raw!['com.apple.quicktime.player.movie.audio.gain']).toEqual(new Uint8Array([63, 128, 0, 0]));
+});
+
+test('Read and write metadata, Matroska', async () => {
+ const output = new Output({
+ format: new MkvOutputFormat(),
+ target: new BufferTarget(),
+ });
+
+ output.setMetadataTags({
+ ...songMetadata,
+ raw: {
+ CUSTOM: 'Levels',
+ },
+ });
+
+ const dummyTrack = createDummyAudioTrack('opus', output);
+
+ await output.start();
+ await dummyTrack.addPacket();
+ await output.finalize();
+
+ const input = new Input({
+ source: new BufferSource(output.target.buffer!),
+ formats: ALL_FORMATS,
+ });
+
+ const readTags = await input.getMetadataTags();
+
+ expect(readTags.title).toBe(songMetadata.title);
+ expect(readTags.description).toBe(songMetadata.description);
+ expect(readTags.artist).toBe(songMetadata.artist);
+ expect(readTags.album).toBe(songMetadata.album);
+ expect(readTags.albumArtist).toBe(songMetadata.albumArtist);
+ expect(readTags.comment).toBe(songMetadata.comment);
+ expect(readTags.lyrics).toBe(songMetadata.lyrics);
+ expect(readTags.trackNumber).toBe(songMetadata.trackNumber);
+ expect(readTags.tracksTotal).toBe(songMetadata.tracksTotal);
+ expect(readTags.discNumber).toBe(songMetadata.discNumber);
+ expect(readTags.discsTotal).toBe(songMetadata.discsTotal);
+ expect(readTags.date).toEqual(readTags.date);
+ expect(readTags.images).toHaveLength(1);
+ expect(readTags.images![0]!.data).toEqual(coverArt);
+ expect(readTags.images![0]!.mimeType).toEqual('image/jpeg');
+ expect(readTags.images![0]!.kind).toEqual('coverFront');
+ expect(readTags.images![0]!.description).toEqual(songMetadata.images![0]!.description);
+ expect(readTags.images![0]!.name).toEqual('cover.jpg'); // It constructed this name
+
+ expect(readTags.raw!['TITLE']).toBe(songMetadata.title);
+ expect(readTags.raw!['PART_NUMBER']).toBe('13/14');
+ expect(readTags.raw!['CUSTOM']).toBe('Levels');
+});
+
+test('Read and write metadata, MP3', async () => {
+ const output = new Output({
+ format: new Mp3OutputFormat(),
+ target: new BufferTarget(),
+ });
+
+ output.setMetadataTags({
+ ...songMetadata,
+ raw: {
+ TXXY: 'ID3v2 goated',
+ },
+ });
+
+ const dummyTrack = createDummyAudioTrack('mp3', output);
+
+ await output.start();
+ await dummyTrack.addPacket();
+ await output.finalize();
+
+ const input = new Input({
+ source: new BufferSource(output.target.buffer!),
+ formats: ALL_FORMATS,
+ });
+
+ const readTags = await input.getMetadataTags();
+
+ // ID3v2 is goated, so pretty much everything was copied:
+ expect(readTags.title).toBe(songMetadata.title);
+ expect(readTags.description).toBe(songMetadata.description);
+ expect(readTags.artist).toBe(songMetadata.artist);
+ expect(readTags.album).toBe(songMetadata.album);
+ expect(readTags.albumArtist).toBe(songMetadata.albumArtist);
+ expect(readTags.comment).toBe(songMetadata.comment);
+ expect(readTags.lyrics).toBe(songMetadata.lyrics);
+ expect(readTags.trackNumber).toBe(songMetadata.trackNumber);
+ expect(readTags.tracksTotal).toBe(songMetadata.tracksTotal);
+ expect(readTags.discNumber).toBe(songMetadata.discNumber);
+ expect(readTags.discsTotal).toBe(songMetadata.discsTotal);
+ expect(readTags.date).toEqual(readTags.date);
+ expect(readTags.images).toHaveLength(1);
+ expect(readTags.images![0]!.data).toEqual(coverArt);
+ expect(readTags.images![0]!.mimeType).toEqual('image/jpeg');
+ expect(readTags.images![0]!.kind).toEqual('coverFront');
+ expect(readTags.images![0]!.description).toEqual(songMetadata.images![0]!.description);
+ expect(readTags.images![0]!.name).toBeUndefined(); // Can't be contained in ID3v2
+
+ expect(readTags.raw!['TIT2']).toBe(songMetadata.title);
+ expect(readTags.raw!['APIC']).instanceOf(Uint8Array);
+ expect(readTags.raw!['TXXY']).toBe('ID3v2 goated');
+});
+
+test('Read and write metadata, Ogg', async () => {
+ const output = new Output({
+ format: new OggOutputFormat(),
+ target: new BufferTarget(),
+ });
+
+ output.setMetadataTags({
+ ...songMetadata,
+ raw: {
+ vendor: 'mediabunny corp',
+ COMPOSER: 'Hans Zimmer',
+ },
+ });
+
+ const dummyTrack = createDummyAudioTrack('opus', output);
+
+ await output.start();
+ await dummyTrack.addPacket();
+ await output.finalize();
+
+ const input = new Input({
+ source: new BufferSource(output.target.buffer!),
+ formats: ALL_FORMATS,
+ });
+
+ const readTags = await input.getMetadataTags();
+
+ expect(readTags.title).toBe(songMetadata.title);
+ expect(readTags.description).toBe(songMetadata.description);
+ expect(readTags.artist).toBe(songMetadata.artist);
+ expect(readTags.album).toBe(songMetadata.album);
+ expect(readTags.albumArtist).toBe(songMetadata.albumArtist);
+ expect(readTags.comment).toBe(songMetadata.comment);
+ expect(readTags.lyrics).toBe(songMetadata.lyrics);
+ expect(readTags.trackNumber).toBe(songMetadata.trackNumber);
+ expect(readTags.tracksTotal).toBe(songMetadata.tracksTotal);
+ expect(readTags.discNumber).toBe(songMetadata.discNumber);
+ expect(readTags.discsTotal).toBe(songMetadata.discsTotal);
+ expect(readTags.date).toEqual(readTags.date);
+ expect(readTags.images).toHaveLength(1);
+ expect(readTags.images![0]!.data).toEqual(coverArt);
+ expect(readTags.images![0]!.mimeType).toEqual('image/jpeg');
+ expect(readTags.images![0]!.kind).toEqual('coverFront');
+ expect(readTags.images![0]!.description).toEqual(songMetadata.images![0]!.description);
+ expect(readTags.images![0]!.name).toBeUndefined(); // Can't be contained in Vorbis-style metadata
+
+ expect(readTags.raw!['vendor']).toBe('mediabunny corp');
+ expect(readTags.raw!['COMPOSER']).toBe('Hans Zimmer');
+});
+
+test('Read and write metadata, WAVE', async () => {
+ const output = new Output({
+ format: new WavOutputFormat(),
+ target: new BufferTarget(),
+ });
+
+ output.setMetadataTags({
+ ...songMetadata,
+ raw: {
+ IKEK: 'RIFF INFO lowkey mid',
+ },
+ });
+
+ const dummyTrack = createDummyAudioTrack('pcm-s16', output);
+
+ await output.start();
+ await dummyTrack.addPacket();
+ await output.finalize();
+
+ const input = new Input({
+ source: new BufferSource(output.target.buffer!),
+ formats: ALL_FORMATS,
+ });
+
+ const readTags = await input.getMetadataTags();
+
+ expect(readTags.title).toBe(songMetadata.title);
+ expect(readTags.description).toBeUndefined();
+ expect(readTags.artist).toBe(songMetadata.artist);
+ expect(readTags.album).toBe(songMetadata.album);
+ expect(readTags.albumArtist).toBeUndefined();
+ expect(readTags.comment).toBe(songMetadata.comment);
+ expect(readTags.lyrics).toBeUndefined();
+ expect(readTags.trackNumber).toBe(songMetadata.trackNumber);
+ expect(readTags.tracksTotal).toBe(songMetadata.tracksTotal);
+ expect(readTags.discNumber).toBeUndefined();
+ expect(readTags.discsTotal).toBeUndefined();
+ expect(readTags.date).toEqual(readTags.date);
+ expect(readTags.images).toBeUndefined();
+
+ expect(readTags.raw!['INAM']).toBe(songMetadata.title);
+ expect(readTags.raw!['IKEK']).toBe('RIFF INFO lowkey mid');
+});
diff --git a/test/public/trunc-buck-bunny.mov b/test/public/trunc-buck-bunny.mov
new file mode 100644
index 0000000..9192cd7
Binary files /dev/null and b/test/public/trunc-buck-bunny.mov differ