mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 10:53:50 +02:00
287 lines
8.7 KiB
TypeScript
287 lines
8.7 KiB
TypeScript
import { expect, test } from 'vitest';
|
|
import path from 'node:path';
|
|
import { assert, toUint8Array } from '../../src/misc.js';
|
|
import { Input } from '../../src/input.js';
|
|
import { BufferSource, FilePathSource } from '../../src/source.js';
|
|
import { ALL_FORMATS, FLAC } from '../../src/input-format.js';
|
|
import { EncodedPacketSink } from '../../src/media-sink.js';
|
|
import { Output } from '../../src/output.js';
|
|
import { BufferTarget } from '../../src/target.js';
|
|
import { FlacOutputFormat } from '../../src/output-format.js';
|
|
import { Conversion } from '../../src/conversion.js';
|
|
|
|
const __dirname = new URL('.', import.meta.url).pathname;
|
|
|
|
test('can loop over all samples', async () => {
|
|
const filePath = path.join(__dirname, '..', 'public/sample.flac');
|
|
using input = new Input({
|
|
source: new FilePathSource(filePath),
|
|
formats: ALL_FORMATS,
|
|
});
|
|
|
|
const track = await input.getPrimaryAudioTrack();
|
|
assert(track);
|
|
expect(await track.computeDuration()).toEqual(19.714285714285715);
|
|
expect(await track.getDecoderConfig()).toEqual({
|
|
codec: 'flac',
|
|
numberOfChannels: 2,
|
|
sampleRate: 44100,
|
|
description: new Uint8Array([
|
|
102, 76, 97, 67, 128, 0, 0, 34, 16, 0, 16, 0, 0, 6, 45, 0, 37, 173, 10,
|
|
196, 66, 240, 0, 13, 68, 24, 85, 22, 231, 0, 113, 139, 185, 1, 33, 54,
|
|
155, 80, 241, 191, 203, 112,
|
|
]),
|
|
});
|
|
expect(await track.getCodecParameterString()).toEqual('flac');
|
|
expect(await track.getTimeResolution()).toEqual(44100);
|
|
expect(await input.getMimeType()).toEqual('audio/flac');
|
|
|
|
const sink = new EncodedPacketSink(track);
|
|
let samples = 0;
|
|
let lastSampleTimestamp = 0;
|
|
for await (const sample of sink.packets()) {
|
|
samples++;
|
|
lastSampleTimestamp = sample.timestamp;
|
|
if (sample.sequenceNumber === 212) {
|
|
// Last frame is a bit shorter
|
|
// due it having a custom block size and the duration not being a multiple of the frame size
|
|
expect(sample.duration).toEqual(0.023764172335600908);
|
|
} else {
|
|
expect(sample.duration).toEqual(0.09287981859410431);
|
|
}
|
|
}
|
|
expect(samples).toBe(213);
|
|
expect(lastSampleTimestamp).toBe(19.690521541950112);
|
|
});
|
|
|
|
test('can do random access', async () => {
|
|
const filePath = path.join(__dirname, '..', 'public/sample.flac');
|
|
using input = new Input({
|
|
source: new FilePathSource(filePath),
|
|
formats: ALL_FORMATS,
|
|
});
|
|
|
|
const track = await input.getPrimaryAudioTrack();
|
|
assert(track);
|
|
const packetSink = new EncodedPacketSink(track);
|
|
|
|
const packet = await packetSink.getPacket(10);
|
|
assert(packet);
|
|
expect(packet.timestamp).toBe(9.93814058956916);
|
|
expect(packet.data.byteLength).toBe(8345);
|
|
expect(packet.sequenceNumber).toBe(107);
|
|
expect(packet.duration).toBe(0.09287981859410431);
|
|
|
|
const nextPacket = await packetSink.getNextPacket(packet);
|
|
assert(nextPacket);
|
|
expect(nextPacket.timestamp).toBe(10.031020408163265);
|
|
expect(nextPacket.data.byteLength).toBe(8988);
|
|
expect(nextPacket.sequenceNumber).toBe(108);
|
|
expect(nextPacket.duration).toBe(0.09287981859410431);
|
|
|
|
const priorPacket = await packetSink.getPacket(3);
|
|
assert(priorPacket);
|
|
expect(priorPacket.timestamp).toBe(2.972154195011338);
|
|
expect(priorPacket.data.byteLength).toBe(6877);
|
|
expect(priorPacket.sequenceNumber).toBe(32);
|
|
expect(priorPacket.duration).toBe(0.09287981859410431);
|
|
});
|
|
|
|
test('can get metadata-only packets', async () => {
|
|
const filePath = path.join(__dirname, '..', 'public/sample.flac');
|
|
using input = new Input({
|
|
source: new FilePathSource(filePath),
|
|
formats: ALL_FORMATS,
|
|
});
|
|
|
|
const track = await input.getPrimaryAudioTrack();
|
|
assert(track);
|
|
const packetSink = new EncodedPacketSink(track);
|
|
|
|
const packet = await packetSink.getPacket(10, { metadataOnly: true });
|
|
assert(packet);
|
|
expect(packet.timestamp).toBe(9.93814058956916);
|
|
expect(packet.isMetadataOnly).toBe(true);
|
|
expect(packet.sequenceNumber).toBe(107);
|
|
expect(packet.duration).toBe(0.09287981859410431);
|
|
});
|
|
|
|
test('can get metadata', async () => {
|
|
const filePath = path.join(__dirname, '..', 'public/sample.flac');
|
|
using input = new Input({
|
|
source: new FilePathSource(filePath),
|
|
formats: ALL_FORMATS,
|
|
});
|
|
|
|
const { images: inputImages, ...descriptiveMetadata } = await input.getMetadataTags();
|
|
|
|
expect(inputImages![0]!.data).toHaveLength(4852);
|
|
expect(inputImages![0]!.data.slice(0, 8)).toEqual(new Uint8Array([
|
|
137, 80, 78, 71, 13, 10, 26, 10,
|
|
]));
|
|
expect(inputImages![0]!.mimeType).toBe('image/png');
|
|
expect(inputImages![0]!.description).toBe('Album cover');
|
|
expect(inputImages![0]!.kind).toBe('coverFront');
|
|
|
|
expect(descriptiveMetadata).toEqual({
|
|
title: 'The Happy Meeting',
|
|
date: new Date('2020'),
|
|
album: 'Samples files',
|
|
artist: 'Samples Files',
|
|
trackNumber: 4,
|
|
genre: 'Ambient',
|
|
raw: {
|
|
ALBUM: 'Samples files',
|
|
ARTIST: 'Samples Files',
|
|
DATE: '2020',
|
|
ENCODER: 'Lavf58.76.100',
|
|
GENRE: 'Ambient',
|
|
TITLE: 'The Happy Meeting',
|
|
TRACKNUMBER: '4',
|
|
vendor: 'Lavf58.76.100',
|
|
},
|
|
});
|
|
});
|
|
|
|
test('can re-mux a .flac', async () => {
|
|
const filePath = path.join(__dirname, '..', 'public/sample.flac');
|
|
using input = new Input({
|
|
source: new FilePathSource(filePath),
|
|
formats: ALL_FORMATS,
|
|
});
|
|
|
|
let framesWritten = 0;
|
|
|
|
const output = new Output({
|
|
format: new FlacOutputFormat({
|
|
onFrame() {
|
|
framesWritten++;
|
|
},
|
|
}),
|
|
target: new BufferTarget(),
|
|
});
|
|
|
|
const conversion = await Conversion.init({ input, output });
|
|
await conversion.execute();
|
|
|
|
expect(framesWritten).toBe(213);
|
|
|
|
const buffer = output.target.buffer;
|
|
assert(buffer);
|
|
|
|
const outputAsInput = new Input({
|
|
source: new BufferSource(buffer),
|
|
formats: [FLAC],
|
|
});
|
|
|
|
const outputTrack = await outputAsInput.getPrimaryAudioTrack();
|
|
assert(outputTrack);
|
|
|
|
const inputTrack = await input.getPrimaryAudioTrack();
|
|
assert(inputTrack);
|
|
expect(await inputTrack.getSampleRate()).toBe(await outputTrack.getSampleRate());
|
|
expect(await inputTrack.getNumberOfChannels()).toBe(await outputTrack.getNumberOfChannels());
|
|
expect(await inputTrack.getTimeResolution()).toBe(await outputTrack.getTimeResolution());
|
|
|
|
const outputMetadataTags = await outputAsInput.getMetadataTags();
|
|
|
|
const inputMetadataTags = await input.getMetadataTags();
|
|
expect(inputMetadataTags.images).toHaveLength(1);
|
|
expect(Object.keys(outputMetadataTags)).toEqual([
|
|
'images',
|
|
'raw',
|
|
'title',
|
|
'date',
|
|
'album',
|
|
'artist',
|
|
'trackNumber',
|
|
'genre',
|
|
]);
|
|
|
|
const { images: inputImages, ...otherInputMetadataTags } = inputMetadataTags;
|
|
|
|
expect(outputMetadataTags).toEqual({
|
|
...otherInputMetadataTags,
|
|
raw: {
|
|
...otherInputMetadataTags.raw,
|
|
vendor: 'Mediabunny',
|
|
},
|
|
images: inputImages,
|
|
});
|
|
|
|
const inputPacketSink = new EncodedPacketSink(inputTrack);
|
|
const outputPacketSink = new EncodedPacketSink(outputTrack);
|
|
let packets = 0;
|
|
let timestamp = 0;
|
|
for await (const packet of outputPacketSink.packets()) {
|
|
packets++;
|
|
timestamp = packet.timestamp;
|
|
}
|
|
|
|
expect(packets).toBe(213);
|
|
expect(timestamp).toBe(19.690521541950112);
|
|
|
|
// Test that packets are byte-identical
|
|
const inputPacket = await inputPacketSink.getPacket(10);
|
|
const outputPacket = await outputPacketSink.getPacket(10);
|
|
|
|
assert(inputPacket);
|
|
assert(outputPacket);
|
|
|
|
const { data: inputPacketData, ...otherInputPacket } = inputPacket;
|
|
const { data: outputPacketData, ...otherOutputPacket } = outputPacket;
|
|
|
|
expect(otherInputPacket).toEqual(otherOutputPacket);
|
|
expect(inputPacketData).toEqual(outputPacketData);
|
|
|
|
const inputDecoderConfig = await inputTrack.getDecoderConfig();
|
|
const outputDecoderConfig = await outputTrack.getDecoderConfig();
|
|
assert(inputDecoderConfig);
|
|
assert(outputDecoderConfig);
|
|
|
|
const { description: inputDescription, ...otherInputDecoderConfig } = inputDecoderConfig;
|
|
const { description: outputDescription, ...otherOutputDecoderConfig } = outputDecoderConfig;
|
|
|
|
assert(inputDescription);
|
|
assert(outputDescription);
|
|
|
|
const inputArray = toUint8Array(inputDescription);
|
|
const outputArray = toUint8Array(outputDescription);
|
|
|
|
const inputWithoutCrc = inputArray.slice(0, -16);
|
|
const outputWithoutCrc = outputArray.slice(0, -16);
|
|
expect(inputWithoutCrc).toEqual(outputWithoutCrc);
|
|
|
|
expect(otherInputDecoderConfig).toEqual(otherOutputDecoderConfig);
|
|
});
|
|
|
|
test('appendOnly writes correct STREAMINFO header', async () => {
|
|
const filePath = path.join(__dirname, '..', 'public/sample.flac');
|
|
using input = new Input({
|
|
source: new FilePathSource(filePath),
|
|
formats: ALL_FORMATS,
|
|
});
|
|
|
|
const target = new BufferTarget();
|
|
const output = new Output({
|
|
format: new FlacOutputFormat({ appendOnly: true }),
|
|
target,
|
|
});
|
|
|
|
const conversion = await Conversion.init({ input, output });
|
|
await conversion.execute();
|
|
|
|
assert(target.buffer);
|
|
const bytes = new Uint8Array(target.buffer);
|
|
|
|
// STREAMINFO: min_block=16, max_block=65535, min_frame=0, max_frame=0
|
|
expect(bytes.slice(8, 18)).toEqual(new Uint8Array([
|
|
// minimum_block_size=16
|
|
0x00, 0x10,
|
|
// maximum_block_size=65535
|
|
0xFF, 0xFF,
|
|
// minimum_frame_size=0
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
]));
|
|
});
|