mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 02:43:48 +02:00
* Added ID3v2 support for FLAC files. (Fixes #417) * Fix ID3v2 header size calculation, adjust ID3v2 logic for FLAC files --------- Co-authored-by: Vanilagy <[email protected]>
This commit is contained in:
@@ -37,6 +37,7 @@ import {
|
||||
getSampleRateOrUncommon,
|
||||
} from './flac-misc';
|
||||
import { Bitstream } from '../../shared/bitstream';
|
||||
import { ID3_V2_HEADER_SIZE, parseId3V2Tag, readId3V2Header } from '../id3';
|
||||
|
||||
type FlacAudioInfo = {
|
||||
numberOfChannels: number;
|
||||
@@ -102,9 +103,34 @@ export class FlacDemuxer extends Demuxer {
|
||||
}
|
||||
|
||||
async readMetadata() {
|
||||
let currentPos = 4; // Skip 'fLaC'
|
||||
|
||||
return (this.metadataPromise ??= (async () => {
|
||||
// Read all ID3v2 tags at the start of the file
|
||||
let currentPos = 0;
|
||||
while (true) {
|
||||
let headerSlice = this.reader.requestSlice(currentPos, ID3_V2_HEADER_SIZE);
|
||||
if (headerSlice instanceof Promise) headerSlice = await headerSlice;
|
||||
|
||||
if (!headerSlice) {
|
||||
this.lastSampleLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const id3V2Header = readId3V2Header(headerSlice);
|
||||
if (!id3V2Header) {
|
||||
break;
|
||||
}
|
||||
|
||||
let contentSlice = this.reader.requestSlice(headerSlice.filePos, id3V2Header.size);
|
||||
if (contentSlice instanceof Promise) contentSlice = await contentSlice;
|
||||
assert(contentSlice);
|
||||
|
||||
parseId3V2Tag(contentSlice, id3V2Header, this.metadataTags);
|
||||
|
||||
currentPos = headerSlice.filePos + id3V2Header.size;
|
||||
}
|
||||
|
||||
currentPos += 4; // Skip 'fLaC'
|
||||
|
||||
while (
|
||||
this.reader.fileSize === null
|
||||
|| currentPos < this.reader.fileSize
|
||||
|
||||
+10
-11
@@ -152,7 +152,11 @@ export const readId3V2Header = (slice: FileSlice): Id3V2Header | null => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const size = decodeSynchsafe(sizeRaw);
|
||||
let size = decodeSynchsafe(sizeRaw);
|
||||
|
||||
if (flags & Id3V2HeaderFlags.Footer) {
|
||||
size += ID3_V2_HEADER_SIZE;
|
||||
}
|
||||
|
||||
return { majorVersion, revision, flags, size };
|
||||
};
|
||||
@@ -165,12 +169,12 @@ export const parseId3V2Tag = (slice: FileSlice, header: Id3V2Header, tags: Metad
|
||||
return;
|
||||
}
|
||||
|
||||
const bytes = readBytes(slice, header.size);
|
||||
const reader = new Id3V2Reader(header, bytes);
|
||||
const dataSize = (header.flags & Id3V2HeaderFlags.Footer)
|
||||
? header.size - ID3_V2_HEADER_SIZE
|
||||
: header.size;
|
||||
|
||||
if (header.flags & Id3V2HeaderFlags.Footer) {
|
||||
reader.removeFooter();
|
||||
}
|
||||
const bytes = readBytes(slice, dataSize);
|
||||
const reader = new Id3V2Reader(header, bytes);
|
||||
|
||||
if ((header.flags & Id3V2HeaderFlags.Unsynchronisation) && header.majorVersion === 3) {
|
||||
reader.ununsynchronizeAll();
|
||||
@@ -463,11 +467,6 @@ export class Id3V2Reader {
|
||||
this.view = new DataView(this.bytes.buffer);
|
||||
}
|
||||
|
||||
removeFooter() {
|
||||
this.bytes = this.bytes.subarray(0, this.bytes.length - ID3_V2_HEADER_SIZE);
|
||||
this.view = new DataView(this.bytes.buffer);
|
||||
}
|
||||
|
||||
readBytes(length: number) {
|
||||
const slice = this.bytes.subarray(this.pos, this.pos + length);
|
||||
this.pos += length;
|
||||
|
||||
+17
-1
@@ -446,7 +446,23 @@ export class OggInputFormat extends InputFormat {
|
||||
export class FlacInputFormat extends InputFormat {
|
||||
/** @internal */
|
||||
async _canReadInput(input: Input) {
|
||||
let slice = input._reader.requestSlice(0, 4);
|
||||
let currentPos = 0;
|
||||
|
||||
// There might be ID3v2 headers at the start, skip 'em
|
||||
while (true) {
|
||||
let slice = input._reader.requestSlice(currentPos, ID3_V2_HEADER_SIZE);
|
||||
if (slice instanceof Promise) slice = await slice;
|
||||
if (!slice) break;
|
||||
|
||||
const id3V2Header = readId3V2Header(slice);
|
||||
if (!id3V2Header) {
|
||||
break;
|
||||
}
|
||||
|
||||
currentPos = slice.filePos + id3V2Header.size;
|
||||
}
|
||||
|
||||
let slice = input._reader.requestSlice(currentPos, 4);
|
||||
if (slice instanceof Promise) slice = await slice;
|
||||
if (!slice) return false;
|
||||
|
||||
|
||||
+4
-2
@@ -20,7 +20,8 @@ import { isRecordStringString } from './misc';
|
||||
* in Vorbis-style comment headers.
|
||||
* - For WAVE files, the metadata refers to the chunks within the RIFF INFO chunk.
|
||||
* - For ADTS files, the metadata refers to the ID3v2 tags.
|
||||
* - For FLAC files, the metadata lives in Vorbis style in the Vorbis comment block.
|
||||
* - For FLAC files, the metadata lives in Vorbis style in the Vorbis comment block, or sometimes in ID3v2 tags at the
|
||||
* start of the file.
|
||||
* - For MPEG-TS files, metadata tags are currently not supported.
|
||||
*
|
||||
* @group Metadata tags
|
||||
@@ -78,7 +79,8 @@ export type MetadataTags = {
|
||||
* 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.
|
||||
* - FLAC: The key-value string pairs from the vorbis metadata block (see RFC 9639, Section D.2.3).
|
||||
* Additionally, the `'vendor'` key refers to the vendor string within this header.
|
||||
* Additionally, the `'vendor'` key refers to the vendor string within this header. If ID3v2 tags appear at the
|
||||
* start of the file, their content is stored just like for MP3.
|
||||
* - MPEG-TS: Not supported.
|
||||
*/
|
||||
raw?: Record<string, string | Uint8Array | RichImageData | AttachedFile | Record<string, string> | null>;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs/promises';
|
||||
import { assert, toUint8Array } from '../../src/misc.js';
|
||||
import { Input } from '../../src/input.js';
|
||||
import { BufferSource, FilePathSource } from '../../src/source.js';
|
||||
@@ -284,3 +285,59 @@ test('appendOnly writes correct STREAMINFO header', async () => {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
]));
|
||||
});
|
||||
|
||||
test('can read a FLAC file with leading ID3v2 tags', async () => {
|
||||
const createId3V23TitleTag = (title: string) => {
|
||||
const titleBytes = new TextEncoder().encode(title);
|
||||
const frame = new Uint8Array(11 + titleBytes.length);
|
||||
frame.set([0x54, 0x49, 0x54, 0x32]); // TIT2
|
||||
frame[7] = 1 + titleBytes.length;
|
||||
frame[10] = 0; // ISO-8859-1
|
||||
frame.set(titleBytes, 11);
|
||||
|
||||
const tag = new Uint8Array(10 + frame.length);
|
||||
tag.set([0x49, 0x44, 0x33, 0x03, 0x00, 0x00]); // ID3v2.3
|
||||
tag[9] = frame.length;
|
||||
tag.set(frame, 10);
|
||||
|
||||
return tag;
|
||||
};
|
||||
|
||||
const concatenateBytes = (...chunks: Uint8Array[]) => {
|
||||
const result = new Uint8Array(chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0));
|
||||
let offset = 0;
|
||||
|
||||
for (const chunk of chunks) {
|
||||
result.set(chunk, offset);
|
||||
offset += chunk.byteLength;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
using input = new Input({
|
||||
source: new BufferSource(concatenateBytes(
|
||||
createId3V23TitleTag('First tag'),
|
||||
createId3V23TitleTag('Second tag'),
|
||||
await fs.readFile(new URL('../public/sample.flac', import.meta.url)),
|
||||
)),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
expect(await input.canRead()).toBe(true);
|
||||
expect(await input.getFormat()).toBe(FLAC);
|
||||
|
||||
const track = await input.getPrimaryAudioTrack();
|
||||
assert(track);
|
||||
expect(await track.getDurationFromMetadata()).toEqual(19.714285714285715);
|
||||
|
||||
const firstPacket = await new EncodedPacketSink(track).getPacket(0);
|
||||
assert(firstPacket);
|
||||
expect(firstPacket.sequenceNumber).toBe(0);
|
||||
expect(firstPacket.timestamp).toBe(0);
|
||||
|
||||
const metadataTags = await input.getMetadataTags();
|
||||
expect(metadataTags.title).toBe('First tag');
|
||||
expect(metadataTags.raw!['TIT2']).toBe('First tag');
|
||||
expect(metadataTags.raw!['TITLE']).toBe('The Happy Meeting');
|
||||
});
|
||||
|
||||
@@ -354,7 +354,7 @@ test('MPEG-TS seeking race condition test', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('MPEG-TS video key packets', async () => {
|
||||
test('MPEG-TS video key packets', { timeout: 10_000 }, async () => {
|
||||
for (let i = 0; i < 2; i++) {
|
||||
using input = new Input({
|
||||
source: new FilePathSource(path.join(__dirname, '../public/trim-buck-bunny-ffmpeg.ts')),
|
||||
|
||||
Reference in New Issue
Block a user