mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-29 03:43:50 +02:00
BIG cursor progress
- Add extensive tests for packet cursor, sample cursor, and call serialization - Major PacketCursor refactor - Add PacketReader - Many SampleCursor fixes, harden the behavior - Typo fixes and various other small things
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import { CallSerializer2 } from '../../src/misc.js';
|
||||
|
||||
const executeDelayed = async <T>(fn: () => T) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
return fn();
|
||||
};
|
||||
|
||||
test('Call serialization and return values', async () => {
|
||||
const numbers: number[] = [];
|
||||
const serializer = new CallSerializer2();
|
||||
|
||||
const first = serializer.call(() => numbers.push(1));
|
||||
const second = serializer.call(() => executeDelayed(() => numbers.push(2)));
|
||||
const third = serializer.call(() => numbers.push(3));
|
||||
const fourth = serializer.call(() => executeDelayed(() => numbers.push(4)));
|
||||
|
||||
await fourth;
|
||||
|
||||
expect(numbers).toEqual([1, 2, 3, 4]);
|
||||
expect(await first).toBe(1);
|
||||
expect(await second).toBe(2);
|
||||
expect(await third).toBe(3);
|
||||
expect(await fourth).toBe(4);
|
||||
});
|
||||
|
||||
test('Synchronous return value', async () => {
|
||||
const serializer = new CallSerializer2();
|
||||
|
||||
const first = serializer.call(() => {});
|
||||
expect(first).not.toBeInstanceOf(Promise);
|
||||
|
||||
const second = serializer.call(async () => {});
|
||||
const third = serializer.call(() => {});
|
||||
expect(second).toBeInstanceOf(Promise);
|
||||
expect(third).toBeInstanceOf(Promise);
|
||||
|
||||
await third;
|
||||
|
||||
const fourth = serializer.call(() => {});
|
||||
expect(fourth).not.toBeInstanceOf(Promise);
|
||||
});
|
||||
|
||||
test('Error handling', async () => {
|
||||
const serializer = new CallSerializer2();
|
||||
|
||||
expect(() => serializer.call(() => {
|
||||
throw new Error('yo');
|
||||
})).toThrow();
|
||||
|
||||
const second = serializer.call(() => executeDelayed(() => {
|
||||
throw new Error('yo');
|
||||
}));
|
||||
const third = serializer.call(() => executeDelayed(() => 1 + 2));
|
||||
|
||||
await expect(second).rejects.toThrow();
|
||||
expect(await third).toBe(3);
|
||||
});
|
||||
@@ -0,0 +1,334 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import { Input } from '../../src/input.js';
|
||||
import { BufferSource, FilePathSource } from '../../src/source.js';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import { ALL_FORMATS } from '../../src/input-format.js';
|
||||
import { PacketCursor, PacketReader } from '../../src/cursors.js';
|
||||
|
||||
const __dirname = new URL('.', import.meta.url).pathname;
|
||||
|
||||
test('Packet reader', async () => {
|
||||
using input = new Input({
|
||||
source: new FilePathSource(path.join(__dirname, '../public/trim-buck-bunny.mov')),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
const videoTrack = (await input.getPrimaryVideoTrack())!;
|
||||
const reader = new PacketReader(videoTrack);
|
||||
|
||||
const packet1 = (await reader.readFirst())!;
|
||||
expect(packet1.timestamp).toBe(0);
|
||||
|
||||
const packet3 = (await reader.readNext(packet1))!;
|
||||
expect(packet3.sequenceNumber).toBeGreaterThan(packet1.sequenceNumber);
|
||||
|
||||
const packet4 = (await reader.readNextKey(packet1))!;
|
||||
expect(packet4.sequenceNumber).toBeGreaterThan(packet3.sequenceNumber);
|
||||
expect(packet4.type).toBe('key');
|
||||
|
||||
const packet5 = (await reader.readNext(packet3))!;
|
||||
expect(packet5.sequenceNumber).toBeGreaterThan(packet3.sequenceNumber);
|
||||
expect(packet5.sequenceNumber).toBeLessThan(packet4.sequenceNumber);
|
||||
|
||||
const packet6 = (await reader.readAt(2.4))!;
|
||||
expect(packet6.timestamp).toBeGreaterThan(2);
|
||||
expect(packet6.timestamp).toBeLessThanOrEqual(2.4);
|
||||
});
|
||||
|
||||
test('Packet cursor seeking', async () => {
|
||||
using input = new Input({
|
||||
source: new FilePathSource(path.join(__dirname, '../public/trim-buck-bunny.mov')),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
const videoTrack = (await input.getPrimaryVideoTrack())!;
|
||||
const reader = new PacketReader(videoTrack);
|
||||
const cursor = new PacketCursor(reader);
|
||||
|
||||
expect(cursor.current).toBe(null);
|
||||
|
||||
const packet1 = (await cursor.seekToFirst())!;
|
||||
expect(packet1).not.toBe(null);
|
||||
expect(packet1).toBe(cursor.current);
|
||||
expect(packet1.timestamp).toBe(0);
|
||||
|
||||
const packet2 = (await cursor.seekTo(0.01))!;
|
||||
expect(packet1.sequenceNumber).toBe(packet2.sequenceNumber); // Same packet
|
||||
|
||||
const packet3 = (await cursor.seekTo(0.1))!;
|
||||
expect(packet3).toBe(cursor.current);
|
||||
expect(packet3.timestamp).toBeGreaterThan(0);
|
||||
expect(packet3.sequenceNumber).toBeGreaterThan(packet1.sequenceNumber);
|
||||
|
||||
const packet4 = (await cursor.seekToKey(0.1))!;
|
||||
expect(packet4).toBe(cursor.current);
|
||||
expect(packet4.sequenceNumber).toBe(packet1.sequenceNumber);
|
||||
|
||||
const packet5 = (await cursor.seekTo(Infinity))!;
|
||||
expect(packet5).toBe(cursor.current);
|
||||
expect(packet5.timestamp).toBe(5);
|
||||
|
||||
const packet6 = (await cursor.seekTo(-Infinity))!;
|
||||
expect(packet6).toBe(cursor.current);
|
||||
expect(packet6).toBe(null);
|
||||
});
|
||||
|
||||
test('Packet cursor iteration', async () => {
|
||||
using input = new Input({
|
||||
source: new FilePathSource(path.join(__dirname, '../public/trim-buck-bunny.mov')),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
const videoTrack = (await input.getPrimaryVideoTrack())!;
|
||||
const reader = new PacketReader(videoTrack);
|
||||
const cursor = new PacketCursor(reader);
|
||||
|
||||
const packet0 = (await cursor.seekToFirst())!;
|
||||
expect(cursor.current!.timestamp).toBe(0);
|
||||
|
||||
const packet1 = (await cursor.next())!;
|
||||
expect(packet1.sequenceNumber).toBeGreaterThan(packet0.sequenceNumber);
|
||||
expect(packet1).toBe(cursor.current);
|
||||
|
||||
const packet2 = (await cursor.next())!;
|
||||
expect(packet2.sequenceNumber).toBeGreaterThan(packet1.sequenceNumber);
|
||||
expect(packet2).toBe(cursor.current);
|
||||
|
||||
const packet3 = (await cursor.nextKey())!;
|
||||
expect(packet3.sequenceNumber).toBeGreaterThan(packet2.sequenceNumber);
|
||||
expect(packet3.type).toBe('key');
|
||||
expect(packet3).toBe(cursor.current);
|
||||
|
||||
await cursor.seekTo(Infinity);
|
||||
expect(cursor.current).not.toBe(null);
|
||||
|
||||
const packet4 = await cursor.next();
|
||||
expect(packet4).toBe(null);
|
||||
expect(packet4).toBe(cursor.current);
|
||||
|
||||
const packet5 = await cursor.next();
|
||||
expect(packet5).toBe(null);
|
||||
|
||||
await cursor.seekTo(-Infinity);
|
||||
expect(cursor.current).toBe(null);
|
||||
|
||||
const packet6 = (await cursor.next())!;
|
||||
expect(packet6.sequenceNumber).toBe(packet0.sequenceNumber);
|
||||
expect(packet6).toBe(cursor.current);
|
||||
|
||||
await cursor.seekTo(-Infinity);
|
||||
expect(cursor.current).toBe(null);
|
||||
|
||||
const packet7 = (await cursor.next())!;
|
||||
expect(packet7.sequenceNumber).toBe(packet0.sequenceNumber);
|
||||
expect(packet7).toBe(cursor.current);
|
||||
|
||||
const packet8 = (await cursor.next())!;
|
||||
expect(packet8.sequenceNumber).toBeGreaterThan(packet7.sequenceNumber);
|
||||
expect(packet8).toBe(cursor.current);
|
||||
|
||||
await cursor.seekToFirst();
|
||||
|
||||
let total = 0;
|
||||
let lastSeqNum = -Infinity;
|
||||
for await (const packet of cursor) {
|
||||
if (total === 0) {
|
||||
expect(packet.sequenceNumber).toBe(packet0.sequenceNumber);
|
||||
}
|
||||
|
||||
expect(packet.sequenceNumber).toBeGreaterThan(lastSeqNum);
|
||||
|
||||
lastSeqNum = packet.sequenceNumber;
|
||||
total++;
|
||||
}
|
||||
|
||||
expect(total).toBe(121);
|
||||
|
||||
for await (const _ of cursor) {
|
||||
throw new Error('Unreachable');
|
||||
}
|
||||
|
||||
total = 0;
|
||||
await cursor.seekTo(1);
|
||||
for await (const _ of cursor) {
|
||||
total++;
|
||||
}
|
||||
|
||||
expect(total).toBe(97);
|
||||
|
||||
total = 0;
|
||||
await cursor.seekToFirst();
|
||||
await cursor.iterate(() => total++);
|
||||
|
||||
expect(total).toBe(121);
|
||||
expect(cursor.current).toBe(null);
|
||||
|
||||
total = 0;
|
||||
await cursor.seekToFirst();
|
||||
await cursor.iterate((packet, stop) => {
|
||||
if (packet.timestamp === 1) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
|
||||
total++;
|
||||
});
|
||||
|
||||
expect(total).toBe(24);
|
||||
expect(cursor.current!.timestamp).toBe(1);
|
||||
|
||||
await cursor.seekTo(-Infinity);
|
||||
total = 0;
|
||||
for await (const _ of cursor) total++;
|
||||
|
||||
expect(total).toBe(121);
|
||||
|
||||
await cursor.seekTo(-Infinity);
|
||||
total = 0;
|
||||
await cursor.iterate(() => total++);
|
||||
|
||||
expect(total).toBe(121);
|
||||
|
||||
const cursor2 = new PacketCursor(reader);
|
||||
const packet9 = (await cursor2.next())!; // Without any prior seeks
|
||||
expect(packet9.sequenceNumber).toBe(packet0.sequenceNumber);
|
||||
});
|
||||
|
||||
test('Synchronous packet reading', async () => {
|
||||
using input = new Input({
|
||||
source: new BufferSource(fs.readFileSync(path.join(__dirname, '../public/trim-buck-bunny.mov'))),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
const videoTrack = (await input.getPrimaryVideoTrack())!;
|
||||
const reader = new PacketReader(videoTrack);
|
||||
const cursor = new PacketCursor(reader);
|
||||
|
||||
expect(reader.readFirst()).not.toBeInstanceOf(Promise);
|
||||
|
||||
expect(cursor.seekToFirst()).not.toBeInstanceOf(Promise);
|
||||
expect(cursor.seekTo(0.1)).not.toBeInstanceOf(Promise);
|
||||
expect(cursor.seekToKey(0.1)).not.toBeInstanceOf(Promise);
|
||||
expect(cursor.seekTo(2)).not.toBeInstanceOf(Promise);
|
||||
expect(cursor.seekTo(Infinity)).not.toBeInstanceOf(Promise);
|
||||
expect(cursor.seekTo(-Infinity)).not.toBeInstanceOf(Promise);
|
||||
|
||||
void cursor.seekToFirst();
|
||||
|
||||
expect(cursor.next()).not.toBeInstanceOf(Promise);
|
||||
});
|
||||
|
||||
test('Command queuing', async () => {
|
||||
using input = new Input({
|
||||
source: new FilePathSource(path.join(__dirname, '../public/trim-buck-bunny.mov'), {
|
||||
maxCacheSize: 0, // So all commands return promises
|
||||
}),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
const videoTrack = (await input.getPrimaryVideoTrack())!;
|
||||
const reader = new PacketReader(videoTrack);
|
||||
const cursor = new PacketCursor(reader);
|
||||
|
||||
expect(cursor.waitUntilIdle()).toBe(null);
|
||||
|
||||
const commands = [
|
||||
cursor.seekToFirst(),
|
||||
cursor.next(),
|
||||
cursor.next(),
|
||||
cursor.seekTo(2.4),
|
||||
cursor.next(),
|
||||
cursor.nextKey(),
|
||||
cursor.waitUntilIdle()!.then(() => cursor.current),
|
||||
cursor.nextKey(),
|
||||
cursor.nextKey(),
|
||||
cursor.next(),
|
||||
cursor.seekTo(Infinity),
|
||||
cursor.seekTo(-Infinity),
|
||||
cursor.seekToKey(2.4),
|
||||
];
|
||||
|
||||
expect(commands.every(x => x instanceof Promise)).toBe(true);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/await-thenable
|
||||
const resolved = await Promise.all(commands);
|
||||
|
||||
expect(resolved[0]!.timestamp).toBe(0);
|
||||
|
||||
expect(resolved[1]!.sequenceNumber).toBeGreaterThan(resolved[0]!.sequenceNumber);
|
||||
|
||||
expect(resolved[2]!.sequenceNumber).toBeGreaterThan(resolved[1]!.sequenceNumber);
|
||||
|
||||
expect(resolved[3]!.timestamp).toBeGreaterThan(2);
|
||||
expect(resolved[3]!.timestamp).toBeLessThanOrEqual(2.4);
|
||||
|
||||
expect(resolved[4]!.sequenceNumber).toBeGreaterThan(resolved[3]!.sequenceNumber);
|
||||
|
||||
expect(resolved[5]!.timestamp).toBe(3);
|
||||
|
||||
expect(resolved[6]!.sequenceNumber).toBe(resolved[5]!.sequenceNumber);
|
||||
|
||||
expect(resolved[7]!.timestamp).toBe(4);
|
||||
|
||||
expect(resolved[8]!.timestamp).toBe(5);
|
||||
|
||||
expect(resolved[9]).toBe(null);
|
||||
|
||||
expect(resolved[10]!.sequenceNumber).toBe(resolved[8]!.sequenceNumber);
|
||||
|
||||
expect(resolved[11]).toBe(null);
|
||||
|
||||
expect(resolved[12]!.timestamp).toBe(2);
|
||||
expect(resolved[12]!.type).toBe('key');
|
||||
|
||||
void cursor.seekTo(1);
|
||||
await cursor.iterate((packet, stop) => {
|
||||
expect(packet.timestamp).toBe(1);
|
||||
stop();
|
||||
});
|
||||
|
||||
void cursor.seekTo(3);
|
||||
for await (const packet of cursor) {
|
||||
expect(packet.timestamp).toBe(3);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
test('verifyKeyPackets with faultily-labeled key frames', async () => {
|
||||
using input = new Input({
|
||||
source: new FilePathSource(path.join(__dirname, '../public/fake-cod.mp4')),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
const videoTrack = (await input.getPrimaryVideoTrack())!;
|
||||
const reader = new PacketReader(videoTrack);
|
||||
|
||||
const firstPacket = (await reader.readFirst())!;
|
||||
expect(firstPacket.type).toBe('key');
|
||||
|
||||
const fakeKeyPacket = (await reader.readNextKey(firstPacket))!;
|
||||
expect(fakeKeyPacket).not.toBe(null);
|
||||
expect(fakeKeyPacket.type).toBe('key'); // Metadata says it's a key frame
|
||||
expect(fakeKeyPacket.sequenceNumber).toBeGreaterThan(firstPacket.sequenceNumber);
|
||||
|
||||
const verifiedPacket = (await reader.readAt(fakeKeyPacket.timestamp, { verifyKeyPackets: true }))!;
|
||||
expect(verifiedPacket.sequenceNumber).toBe(fakeKeyPacket.sequenceNumber);
|
||||
expect(verifiedPacket.type).toBe('delta'); // After verification, it's actually a delta frame
|
||||
|
||||
const unverifiedKeyAt = (await reader.readKeyAt(fakeKeyPacket.timestamp))!;
|
||||
expect(unverifiedKeyAt.sequenceNumber).toBe(fakeKeyPacket.sequenceNumber);
|
||||
expect(unverifiedKeyAt.type).toBe('key');
|
||||
|
||||
const verifiedKeyAt = (await reader.readKeyAt(fakeKeyPacket.timestamp, { verifyKeyPackets: true }))!;
|
||||
expect(verifiedKeyAt.sequenceNumber).toBe(firstPacket.sequenceNumber);
|
||||
expect(verifiedKeyAt.type).toBe('key');
|
||||
|
||||
const unverifiedNextKey = (await reader.readNextKey(firstPacket))!;
|
||||
expect(unverifiedNextKey).not.toBe(null);
|
||||
expect(unverifiedNextKey.type).toBe('key');
|
||||
expect(unverifiedNextKey.sequenceNumber).toBe(fakeKeyPacket.sequenceNumber);
|
||||
|
||||
const verifiedNextKey = await reader.readNextKey(firstPacket, { verifyKeyPackets: true });
|
||||
expect(verifiedNextKey).toBe(null);
|
||||
});
|
||||
Reference in New Issue
Block a user