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