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:
Vanilagy
2025-11-27 16:26:46 +01:00
parent 52d7ba07b0
commit f86294b802
18 changed files with 2160 additions and 425 deletions
+58
View File
@@ -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);
});
+334
View File
@@ -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);
});