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
+45 -2
View File
@@ -23,12 +23,55 @@
console.log(await cursor.next()); console.log(await cursor.next());
*/ */
//console.log(await cursor.seekTo(4.9))
//console.log(await cursor.seekTo(5.1))
//return;
const mh = [ const mh = [
cursor.seekTo(2.10), cursor.seekTo(2.05),
cursor.seekTo(5), //cursor.seekTo(4.9),
//cursor.seekTo(4.9),
//cursor.seekTo(4.9),
//cursor.seekTo(4.9),
//cursor.next(),
cursor.next(),
cursor.next(),
cursor.next(),
cursor.next(),
cursor.next(),
cursor.next(),
cursor.next(),
cursor.next(),
cursor.next(),
cursor.next(),
cursor.next(),
cursor.next(),
cursor.next(),
cursor.next(),
cursor.next(),
cursor.next(),
cursor.close(),
//cursor.next(),
//cursor.next(),
//cursor.next(),
//cursor.next(),
//cursor.close(),
//cursor.seekTo(2.10),
//cursor.seekTo(5),
//cursor.close(),
//cursor.seekTo(2.00), //cursor.seekTo(2.00),
]; ];
/*
for (const yo of mh) {
const samp = await yo;
console.log(samp)
samp?.close();
}
console.log("done")
*/
console.log(await Promise.all(mh)); console.log(await Promise.all(mh));
//console.log(await cursor.seekTo(2.05)); //console.log(await cursor.seekTo(2.05));
+13
View File
@@ -30,6 +30,19 @@ export default tseslint.config(
'@typescript-eslint/require-await': 'off', '@typescript-eslint/require-await': 'off',
'@stylistic/yield-star-spacing': ['error', { before: false, after: true }], '@stylistic/yield-star-spacing': ['error', { before: false, after: true }],
'@typescript-eslint/no-unsafe-enum-comparison': 'off', '@typescript-eslint/no-unsafe-enum-comparison': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
// From https://typescript-eslint.io/rules/no-unused-vars/
"args": "all",
"argsIgnorePattern": "^_",
"caughtErrors": "all",
"caughtErrorsIgnorePattern": "^_",
"destructuredArrayIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"ignoreRestSiblings": true,
},
],
}, },
}, },
{ {
+3 -2
View File
@@ -134,7 +134,8 @@ export type ConversionVideoOptions = {
fit?: 'fill' | 'contain' | 'cover'; fit?: 'fill' | 'contain' | 'cover';
/** /**
* The angle in degrees to rotate the input video by, clockwise. Rotation is applied before cropping and resizing. * The angle in degrees to rotate the input video by, clockwise. Rotation is applied before cropping and resizing.
* This rotation is _in addition to_ the natural rotation of the input video as specified in input file's metadata. * This rotation is _in addition to_ the natural rotation of the input video as specified in the input file's
* metadata.
*/ */
rotate?: Rotation; rotate?: Rotation;
/** /**
@@ -171,7 +172,7 @@ export type ConversionVideoOptions = {
* frames improve seeking behavior but increase file size. When using multiple video tracks, you should give them * frames improve seeking behavior but increase file size. When using multiple video tracks, you should give them
* all the same key frame interval. * all the same key frame interval.
* *
* Setting this fields forces a transcode. * Setting this field forces a transcode.
*/ */
keyFrameInterval?: number; keyFrameInterval?: number;
/** When `true`, video will always be re-encoded instead of directly copying over the encoded samples. */ /** When `true`, video will always be re-encoded instead of directly copying over the encoded samples. */
+966 -408
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -103,7 +103,7 @@ export const validateVideoEncodingConfig = (config: VideoEncodingConfig) => {
}; };
/** /**
* Additional options that control audio encoding. * Additional options that control video encoding.
* @group Encoding * @group Encoding
* @public * @public
*/ */
+1 -1
View File
@@ -184,7 +184,7 @@ export {
} from './media-sink'; } from './media-sink';
export { export {
PacketCursor, PacketCursor,
VideoSampleCursor2, VideoSampleCursor,
} from './cursors'; } from './cursors';
export { export {
Conversion, Conversion,
+2
View File
@@ -180,6 +180,8 @@ export class Input<S extends Source = Source> implements Disposable {
this._source._disposed = true; this._source._disposed = true;
this._source._dispose(); this._source._dispose();
// TODO this should dispose cursors probably
} }
/** /**
+41 -6
View File
@@ -67,7 +67,7 @@ export type PacketRetrievalOptions = {
verifyKeyPackets?: boolean; verifyKeyPackets?: boolean;
}; };
const validatePacketRetrievalOptions = (options: PacketRetrievalOptions) => { export const validatePacketRetrievalOptions = (options: PacketRetrievalOptions) => {
if (!options || typeof options !== 'object') { if (!options || typeof options !== 'object') {
throw new TypeError('options must be an object.'); throw new TypeError('options must be an object.');
} }
@@ -82,7 +82,7 @@ const validatePacketRetrievalOptions = (options: PacketRetrievalOptions) => {
} }
}; };
const validateTimestamp = (timestamp: number) => { export const validateTimestamp = (timestamp: number) => {
if (!isNumber(timestamp)) { if (!isNumber(timestamp)) {
throw new TypeError('timestamp must be a number.'); // It can be non-finite, that's fine throw new TypeError('timestamp must be a number.'); // It can be non-finite, that's fine
} }
@@ -830,6 +830,7 @@ export class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
customDecoder: CustomVideoDecoder | null = null; customDecoder: CustomVideoDecoder | null = null;
customDecoderCallSerializer = new CallSerializer(); customDecoderCallSerializer = new CallSerializer();
customDecoderQueueSize = 0; customDecoderQueueSize = 0;
customDecoderClosed = false;
inputTimestamps: number[] = []; // Timestamps input into the decoder, sorted. inputTimestamps: number[] = []; // Timestamps input into the decoder, sorted.
sampleQueue: VideoSample[] = []; // Safari-specific thing, check usage. sampleQueue: VideoSample[] = []; // Safari-specific thing, check usage.
@@ -850,6 +851,8 @@ export class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
currentAlphaPacketIndex = 0; currentAlphaPacketIndex = 0;
alphaRaslSkipped = false; // For HEVC stuff alphaRaslSkipped = false; // For HEVC stuff
onDequeue: (() => unknown) | null = null;
constructor( constructor(
onSample: (sample: VideoSample) => unknown, onSample: (sample: VideoSample) => unknown,
onError: (error: Error) => unknown, onError: (error: Error) => unknown,
@@ -918,6 +921,10 @@ export class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
error: onError, error: onError,
}); });
this.decoder.configure(this.decoderConfig); this.decoder.configure(this.decoderConfig);
this.decoder.addEventListener('dequeue', () => {
this.onDequeue?.();
});
} }
} }
@@ -949,7 +956,10 @@ export class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
this.customDecoderQueueSize++; this.customDecoderQueueSize++;
void this.customDecoderCallSerializer void this.customDecoderCallSerializer
.call(() => this.customDecoder!.decode(packet)) .call(() => this.customDecoder!.decode(packet))
.then(() => this.customDecoderQueueSize--); .then(() => {
this.customDecoderQueueSize--;
this.onDequeue?.();
});
} else { } else {
assert(this.decoder); assert(this.decoder);
@@ -1026,6 +1036,10 @@ export class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
error: this.onError, error: this.onError,
}); });
this.alphaDecoder.configure(this.decoderConfig); this.alphaDecoder.configure(this.decoderConfig);
this.alphaDecoder.addEventListener('dequeue', () => {
this.onDequeue?.();
});
} }
const type = determineVideoPacketType(this.codec, this.decoderConfig, packet.sideData.alpha); const type = determineVideoPacketType(this.codec, this.decoderConfig, packet.sideData.alpha);
@@ -1186,11 +1200,19 @@ export class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
close() { close() {
if (this.customDecoder) { if (this.customDecoder) {
void this.customDecoderCallSerializer.call(() => this.customDecoder!.close()); if (!this.customDecoderClosed) {
this.customDecoderClosed = true;
void this.customDecoderCallSerializer.call(() => this.customDecoder!.close());
}
} else { } else {
assert(this.decoder); assert(this.decoder);
this.decoder.close();
this.alphaDecoder?.close(); if (this.decoder.state !== 'closed') {
this.decoder.close();
}
if (this.alphaDecoder && this.alphaDecoder.state !== 'closed') {
this.alphaDecoder.close();
}
this.colorQueue.forEach(x => x.close()); this.colorQueue.forEach(x => x.close());
this.colorQueue.length = 0; this.colorQueue.length = 0;
@@ -1205,6 +1227,19 @@ export class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
} }
this.sampleQueue.length = 0; this.sampleQueue.length = 0;
} }
get closed() {
if (this.customDecoder) {
if (this.customDecoderClosed) {
return true;
}
return !this.customDecoderCallSerializer.errored;
} else {
assert(this.decoder);
return this.decoder.state === 'closed';
}
}
} }
/** Utility class that merges together color and alpha information using simple WebGL 2 shaders. */ /** Utility class that merges together color and alpha information using simple WebGL 2 shaders. */
+1 -1
View File
@@ -72,7 +72,7 @@ export type MetadataTags = {
* - Ogg: The key-value string pairs from the Vorbis-style comment header (see RFC 7845, Section 5.2). * - Ogg: The key-value string pairs from the Vorbis-style comment header (see RFC 7845, Section 5.2).
* Additionally, the `'vendor'` key refers to the vendor string within this header. * 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. * - 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). * - 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.
*/ */
raw?: Record<string, string | Uint8Array | RichImageData | AttachedFile | null>; raw?: Record<string, string | Uint8Array | RichImageData | AttachedFile | null>;
+140 -2
View File
@@ -666,9 +666,15 @@ export const computeRationalApproximation = (x: number, maxDenominator: number)
export class CallSerializer { export class CallSerializer {
currentPromise = Promise.resolve(); currentPromise = Promise.resolve();
errored = false;
call(fn: () => Promise<void> | void) { call(fn: () => Promise<void> | void) {
return this.currentPromise = this.currentPromise.then(fn); return this.currentPromise = this.currentPromise
.then(fn)
.catch((error) => {
this.errored = true;
throw error;
});
} }
} }
@@ -840,11 +846,143 @@ export class AsyncMutex2 {
const { promise, resolve } = promiseWithResolvers(); const { promise, resolve } = promiseWithResolvers();
this.promise = promise; this.promise = promise;
let released = false;
return { return {
[Symbol.dispose]: () => { release: () => {
if (released) {
return;
}
released = true;
resolve(); resolve();
this.locked = false; this.locked = false;
}, },
[Symbol.dispose]() {
this.release();
},
}; };
} }
} }
export type AsyncMutexLock = {
release: () => void;
[Symbol.dispose]: () => void;
};
export class AsyncMutex3 {
locked = false;
resolverQueue: (() => void)[] = [];
lock(): AsyncMutexLock {
if (this.locked) {
throw new Error('Mutex already locked.');
}
this.locked = true;
let released = false;
return {
release: () => {
if (released) {
return;
}
released = true;
this.locked = false;
if (this.resolverQueue.length > 0) {
const resolve = this.resolverQueue.shift()!;
resolve();
}
},
[Symbol.dispose]() {
this.release();
},
};
}
request() {
if (!this.locked) {
return null;
}
const { promise, resolve } = promiseWithResolvers();
this.resolverQueue.push(resolve);
return promise;
}
}
export class CallSerializer2 {
private currentPromise: Promise<unknown> | null = null;
private queuedCalls = 0;
call<T>(fn: () => T) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ReturnType = T extends Promise<any> ? T : T | Promise<T>;
if (this.currentPromise) {
this.queuedCalls++;
return (this.currentPromise = this.currentPromise
.catch(() => {})
.then(() => {
this.queuedCalls--;
return fn();
})
.finally(() => {
if (this.queuedCalls === 0) {
this.currentPromise = null;
}
})) as unknown as ReturnType;
} else {
const result = fn();
if (result instanceof Promise) {
this.currentPromise = result
.finally(() => {
if (this.queuedCalls === 0) {
this.currentPromise = null;
}
});
}
return result as unknown as ReturnType;
}
}
done() {
if (this.currentPromise) {
return this.currentPromise
.catch(() => {})
.then(() => {});
} else {
return null;
}
}
}
export const defer = (callback: () => void) => {
let executed = false;
return {
execute() {
if (executed) {
return;
}
executed = true;
callback();
},
[Symbol.dispose]() {
this.execute();
},
};
};
export const promiseIterateAll = async function* <T>(iterable: Iterable<T>) {
for (const promise of iterable) {
yield await promise;
}
};
+1 -1
View File
@@ -77,7 +77,7 @@ export type BaseTrackMetadata = {
/** The track's disposition, i.e. information about its intended usage. */ /** The track's disposition, i.e. information about its intended usage. */
disposition?: Partial<TrackDisposition>; disposition?: Partial<TrackDisposition>;
/** /**
* The maximum amount of encoded packets that will be added to this track. Setting this field provides the muxer * The maximum number of encoded packets that will be added to this track. Setting this field provides the muxer
* with an additional signal that it can use to preallocate space in the file. * with an additional signal that it can use to preallocate space in the file.
* *
* When this field is set, it is an error to provide more packets than whatever this field specifies. * When this field is set, it is an error to provide more packets than whatever this field specifies.
+1 -1
View File
@@ -68,7 +68,7 @@ export class EncodedPacket {
/** The duration of this packet in seconds. */ /** The duration of this packet in seconds. */
public readonly duration: number, public readonly duration: number,
/** /**
* The sequence number indicates the decode order of the packets. Packet A must be decoded before packet B if A * The sequence number indicates the decode order of the packets. Packet A must be decoded before packet B if A
* has a lower sequence number than B. If two packets have the same sequence number, they are the same packet. * has a lower sequence number than B. If two packets have the same sequence number, they are the same packet.
* Otherwise, sequence numbers are arbitrary and are not guaranteed to have any meaning besides their relative * Otherwise, sequence numbers are arbitrary and are not guaranteed to have any meaning besides their relative
* ordering. Negative sequence numbers mean the sequence number is undefined. * ordering. Negative sequence numbers mean the sequence number is undefined.
+22
View File
@@ -53,6 +53,9 @@ export type VideoSampleInit = {
* @public * @public
*/ */
export class VideoSample implements Disposable { export class VideoSample implements Disposable {
/** @internal */
static _openSampleCount = 0;
/** @internal */ /** @internal */
_data!: VideoFrame | OffscreenCanvas | Uint8Array | null; _data!: VideoFrame | OffscreenCanvas | Uint8Array | null;
/** @internal */ /** @internal */
@@ -107,6 +110,14 @@ export class VideoSample implements Disposable {
return this.format && this.format.includes('A'); return this.format && this.format.includes('A');
} }
/**
* Whether this sample is closed, meaning its underlying data has been discarded. When a sample is closed, most
* operations will fail.
*/
get closed() {
return this._closed;
}
/** /**
* Creates a new {@link VideoSample} from a * Creates a new {@link VideoSample} from a
* [`VideoFrame`](https://developer.mozilla.org/en-US/docs/Web/API/VideoFrame). This is essentially a near zero-cost * [`VideoFrame`](https://developer.mozilla.org/en-US/docs/Web/API/VideoFrame). This is essentially a near zero-cost
@@ -265,6 +276,8 @@ export class VideoSample implements Disposable {
} else { } else {
throw new TypeError('Invalid data type: Must be a BufferSource or CanvasImageSource.'); throw new TypeError('Invalid data type: Must be a BufferSource or CanvasImageSource.');
} }
VideoSample._openSampleCount++;
} }
/** Clones this video sample. */ /** Clones this video sample. */
@@ -320,6 +333,7 @@ export class VideoSample implements Disposable {
} }
this._closed = true; this._closed = true;
VideoSample._openSampleCount--;
} }
/** Returns the number of bytes required to hold this video sample's pixel data. */ /** Returns the number of bytes required to hold this video sample's pixel data. */
@@ -888,6 +902,14 @@ export class AudioSample implements Disposable {
return Math.trunc(SECOND_TO_MICROSECOND_FACTOR * this.duration); return Math.trunc(SECOND_TO_MICROSECOND_FACTOR * this.duration);
} }
/**
* Whether this sample is closed, meaning its underlying data has been discarded. When a sample is closed, most
* operations will fail.
*/
get closed() {
return this._closed;
}
/** /**
* Creates a new {@link AudioSample}, either from an existing * Creates a new {@link AudioSample}, either from an existing
* [`AudioData`](https://developer.mozilla.org/en-US/docs/Web/API/AudioData) or from raw bytes specified in * [`AudioData`](https://developer.mozilla.org/en-US/docs/Web/API/AudioData) or from raw bytes specified in
+531
View File
@@ -0,0 +1,531 @@
import { expect, test } from 'vitest';
import { Input } from '../../src/input.js';
import { UrlSource } from '../../src/source.js';
import { ALL_FORMATS } from '../../src/input-format.js';
import { PacketReader, VideoSampleCursor } from '../../src/cursors.js';
import { VideoSample } from '../../src/sample.js';
import { promiseIterateAll } from '../../src/misc.js';
test('Sample cursor seeking', async () => {
using input = new Input({
source: new UrlSource('/trim-buck-bunny.mov'),
formats: ALL_FORMATS,
});
const videoTrack = (await input.getPrimaryVideoTrack())!;
const reader = new PacketReader(videoTrack);
const cursor = await VideoSampleCursor.init(reader);
cursor.debugInfo.enabled = true;
expect(cursor.current).toBe(null);
expect(cursor.debugInfo.pumpsStarted).toBe(0);
// Seek to start
const seekToResult1 = cursor.seekToFirst();
expect(seekToResult1).instanceOf(Promise);
const sample1 = (await seekToResult1)!;
expect(sample1).not.toBe(null);
expect(sample1).toBe(cursor.current);
expect(sample1.timestamp).toBeLessThanOrEqual(0);
expect(sample1.closed).toBe(false);
expect(cursor.debugInfo.pumpsStarted).toBe(1);
// Seek to a frame in the current GOP
const seekToResult2 = cursor.seekTo(0.5);
expect(seekToResult2).instanceOf(Promise);
const sample2 = (await seekToResult2)!;
expect(sample2).not.toBe(null);
expect(sample2).toBe(cursor.current);
expect(sample2.timestamp).toBeGreaterThan(0);
expect(sample2.timestamp).toBeLessThanOrEqual(0.5);
expect(sample2.closed).toBe(false);
expect(sample1.closed).toBe(true);
// Wait a little bit so the next sample is most definitely decoded and waiting in the queue
await new Promise(resolve => setTimeout(resolve, 200));
const seekToResult3 = cursor.seekTo(0.55);
expect(seekToResult3).instanceOf(VideoSample); // No promise this time
const sample3 = (await seekToResult3)!;
expect(sample3).toBe(cursor.current);
expect(sample3.timestamp).toBeGreaterThan(0.5);
expect(sample3.timestamp).toBeLessThanOrEqual(0.55);
expect(sample3.closed).toBe(false);
expect(sample2.closed).toBe(true);
// Let's get the same sample again
const seekToResult4 = cursor.seekTo(0.55);
expect(seekToResult4).instanceOf(VideoSample);
const sample4 = (await seekToResult4)!;
expect(sample3).toBe(sample4);
expect(cursor.debugInfo.pumpsStarted).toBe(1);
// Seek to a different GOP
const seekToResult5 = cursor.seekTo(2);
expect(seekToResult5).instanceOf(Promise);
const sample5 = (await seekToResult5)!;
expect(sample5).toBe(cursor.current);
expect(sample5.timestamp).toBeGreaterThan(0.55);
expect(sample5.timestamp).toBeLessThanOrEqual(2);
expect(sample5.closed).toBe(false);
expect(sample3.closed).toBe(true);
expect(cursor.debugInfo.pumpsStarted).toBe(2);
// Seek to a frame in the current GOP
const seekToResult6 = cursor.seekTo(2.5);
expect(seekToResult6).instanceOf(Promise);
const sample6 = (await seekToResult6)!;
expect(sample6).toBe(cursor.current);
expect(sample6.timestamp).toBeGreaterThan(2);
expect(sample6.timestamp).toBeLessThanOrEqual(2.5);
expect(sample6.closed).toBe(false);
expect(sample5.closed).toBe(true);
// Seek to a frame in the current GOP, but backwards, requiring decoding to start over
const seekToResult7 = cursor.seekTo(2.4);
expect(seekToResult7).instanceOf(Promise);
const sample7 = (await seekToResult7)!;
expect(sample7).toBe(cursor.current);
expect(sample7.timestamp).toBeGreaterThan(2);
expect(sample7.timestamp).toBeLessThanOrEqual(2.4);
expect(sample7.closed).toBe(false);
expect(sample6.closed).toBe(true);
expect(cursor.debugInfo.pumpsStarted).toBe(3);
// Seek to a previous GOP
const seekToResult8 = cursor.seekTo(1);
expect(seekToResult8).instanceOf(Promise);
const sample8 = (await seekToResult8)!;
expect(sample8).toBe(cursor.current);
expect(sample8.timestamp).toBeLessThanOrEqual(1);
expect(sample8.closed).toBe(false);
expect(sample7.closed).toBe(true);
expect(cursor.debugInfo.pumpsStarted).toBe(4);
// Seek to past the end
const seekToResult9 = cursor.seekTo(Infinity);
expect(seekToResult9).instanceOf(Promise);
const sample9 = (await seekToResult9)!;
expect(sample9).toBe(cursor.current);
expect(sample9.timestamp).toBe(5); // The length of the video
expect(sample9.closed).toBe(false);
expect(sample8.closed).toBe(true);
expect(cursor.debugInfo.pumpsStarted).toBe(5);
// Seek to before the start
const seekToResult10 = cursor.seekTo(-Infinity);
expect(seekToResult10).toBe(null);
expect(sample9.closed).toBe(true);
const seekToResult11 = cursor.seekToKey(2.5);
expect(seekToResult11).toBeInstanceOf(Promise);
const sample11 = (await seekToResult11)!;
expect(sample11).toBe(cursor.current);
expect(sample11.timestamp).toBe(2);
expect(sample11.closed).toBe(false);
await cursor.close();
expect(sample11.closed).toBe(true);
expect(cursor.current).toBe(null);
expect(cursor.debugInfo.pumpsStarted).toBe(6);
await cursor.close();
expect(VideoSample._openSampleCount).toBe(0);
});
test('Sample cursor advancing', async () => {
using input = new Input({
source: new UrlSource('/trim-buck-bunny.mov'),
formats: ALL_FORMATS,
});
const videoTrack = (await input.getPrimaryVideoTrack())!;
const reader = new PacketReader(videoTrack);
const cursor = await VideoSampleCursor.init(reader);
cursor.debugInfo.enabled = true;
expect(cursor.current).toBe(null);
expect(cursor.debugInfo.pumpsStarted).toBe(0);
const firstSample = (await cursor.seekToFirst())!;
const secondSample = (await cursor.next())!;
const thirdSample = (await cursor.next())!;
expect(secondSample.timestamp).toBeGreaterThan(firstSample.timestamp);
expect(thirdSample.timestamp).toBeGreaterThan(secondSample.timestamp);
expect(firstSample.closed).toBe(true);
expect(secondSample.closed).toBe(true);
expect(thirdSample.closed).toBe(false);
expect(cursor.current).toBe(thirdSample);
await new Promise(resolve => setTimeout(resolve, 200));
const fourthSampleResult = cursor.next(); // It's available instantly
expect(fourthSampleResult).not.toBeInstanceOf(Promise);
const fourthSample = fourthSampleResult as VideoSample;
expect(fourthSample.timestamp).toBeGreaterThan(thirdSample.timestamp);
const lastSample = await cursor.seekTo(5);
expect(lastSample).not.toBe(null);
const nextSample = await cursor.next();
expect(nextSample).toBe(null);
const nextNextSample = await cursor.next();
expect(nextNextSample).toBe(null);
const middleSample = (await cursor.seekTo(3))!;
const sampleAfterMiddle = (await cursor.next())!;
expect(sampleAfterMiddle.timestamp).toBeGreaterThan(middleSample.timestamp);
await cursor.seekTo(-Infinity);
const firstSampleAgain = (await cursor.next())!;
expect(firstSampleAgain.timestamp).toBe(firstSample.timestamp);
let total = 0;
let lastTimestamp = -Infinity;
await cursor.iterate((sample) => {
total++;
expect(sample.timestamp).toBeGreaterThan(lastTimestamp);
lastTimestamp = sample.timestamp;
});
expect(total).toBe(121);
expect(cursor.current).toBe(null);
total = 0;
await cursor.iterate(() => total++);
expect(total).toBe(0); // Since we're at the end
await cursor.seekToFirst(); ;
total = 0;
await cursor.iterate((sample, stop) => {
if (sample.timestamp === 1) {
stop();
return;
}
total++;
});
expect(total).toBe(24);
expect(cursor.current!.timestamp).toBe(1);
total = 0;
for await (const sample of cursor) {
if (total === 0) {
expect(sample.timestamp).toBe(1);
}
total++;
}
expect(total).toBe(97);
await cursor.close();
expect(cursor.debugInfo.pumpsStarted).toBe(5);
expect(VideoSample._openSampleCount).toBe(0);
});
test('Sample cursor advancing, cold start', async () => {
using input = new Input({
source: new UrlSource('/trim-buck-bunny.mov'),
formats: ALL_FORMATS,
});
const videoTrack = (await input.getPrimaryVideoTrack())!;
const reader = new PacketReader(videoTrack);
const cursor = await VideoSampleCursor.init(reader);
cursor.debugInfo.enabled = true;
const firstSample = (await cursor.next())!;
expect(firstSample).not.toBe(null);
expect(firstSample.timestamp).toBe(0);
const secondSample = (await cursor.next())!;
expect(secondSample.timestamp).toBeGreaterThan(firstSample.timestamp);
await cursor.seekTo(-Infinity);
void cursor.next();
void cursor.seekTo(2);
await cursor.close();
// Ensure the calls were serialized correctly
expect(cursor.debugInfo.seekPackets.map(x => x?.timestamp ?? null)).toEqual([0, null, 0, 2]);
const cursor2 = await VideoSampleCursor.init(reader);
for await (const sample of cursor2) {
expect(sample.timestamp).toBe(0);
break;
}
await cursor2.iterate((sample, stop) => {
expect(sample.timestamp).toBe(0);
stop();
});
await cursor2.close();
expect(VideoSample._openSampleCount).toBe(0);
});
test('Decoder pump error handling', async () => {
using input = new Input({
source: new UrlSource('/trim-buck-bunny.mov'),
formats: ALL_FORMATS,
});
const videoTrack = (await input.getPrimaryVideoTrack())!;
const reader = new PacketReader(videoTrack);
const cursor = await VideoSampleCursor.init(reader);
cursor.debugInfo.enabled = true;
cursor.debugInfo.throwInPump = true;
await expect(cursor.seekToFirst()).rejects.toThrow();
expect(cursor.current).toBe(null);
cursor.debugInfo.throwInPump = false;
const firstPacket = (await cursor.seekToFirst())!;
expect(firstPacket.timestamp).toBe(0); // It has recovered
await cursor.close();
expect(VideoSample._openSampleCount).toBe(0);
});
test('Use after close', async () => {
using input = new Input({
source: new UrlSource('/trim-buck-bunny.mov'),
formats: ALL_FORMATS,
});
const videoTrack = (await input.getPrimaryVideoTrack())!;
const reader = new PacketReader(videoTrack);
const cursor = await VideoSampleCursor.init(reader);
await cursor.close();
await expect(async () => await cursor.seekToFirst()).rejects.toThrow();
await expect(async () => await cursor.seekTo(0)).rejects.toThrow();
await expect(async () => await cursor.seekToKey(0)).rejects.toThrow();
await expect(async () => await cursor.next()).rejects.toThrow();
});
test('Command queuing', async () => {
using input = new Input({
source: new UrlSource('/trim-buck-bunny.mov'),
formats: ALL_FORMATS,
});
const videoTrack = (await input.getPrimaryVideoTrack())!;
const reader = new PacketReader(videoTrack);
const cursor0 = await VideoSampleCursor.init(reader);
cursor0.debugInfo.enabled = true;
const commands0 = [
cursor0.seekToFirst(),
cursor0.seekToFirst(),
cursor0.close(),
];
expect(commands0.every(x => x instanceof Promise)).toBe(true);
expect(cursor0.debugInfo.pumpsStarted).toBe(1);
const cursor1 = await VideoSampleCursor.init(reader);
cursor1.debugInfo.enabled = true;
const commands1 = [
cursor1.seekToFirst(),
cursor1.close(),
];
expect(commands1.every(x => x instanceof Promise)).toBe(true);
// eslint-disable-next-line @typescript-eslint/await-thenable
const results1 = await Promise.all(commands1);
expect(results1[0]!.timestamp).toBe(0);
expect(cursor1.debugInfo.decodedPackets.map(x => x.timestamp)).toEqual([0]);
const cursor2 = await VideoSampleCursor.init(reader);
cursor2.debugInfo.enabled = true;
const commands2 = [
cursor2.seekTo(0),
cursor2.seekTo(1),
cursor2.seekTo(2),
cursor2.seekTo(3),
cursor2.seekTo(4),
cursor2.seekTo(5),
cursor2.close(),
];
expect(commands2.every(x => x instanceof Promise)).toBe(true);
// eslint-disable-next-line @typescript-eslint/await-thenable
const results2 = await Promise.all(commands2);
expect(results2[0]!.timestamp).toBe(0);
expect(results2[1]!.timestamp).toBe(1);
expect(results2[2]!.timestamp).toBe(2);
expect(results2[3]!.timestamp).toBe(3);
expect(results2[4]!.timestamp).toBe(4);
expect(results2[5]!.timestamp).toBe(5);
expect(cursor2.debugInfo.decodedPackets.map(x => x.timestamp)).toEqual([
0, 1, 2, 3, 4, 5,
]);
const cursor3 = await VideoSampleCursor.init(reader);
cursor3.debugInfo.enabled = true;
const commands3 = [
cursor3.seekTo(0.5),
cursor3.seekTo(0.4),
cursor3.seekTo(0.3),
cursor3.seekTo(0.2),
cursor3.seekTo(0.1),
cursor3.seekTo(0),
cursor3.close(),
];
expect(commands3.every(x => x instanceof Promise)).toBe(true);
// eslint-disable-next-line @typescript-eslint/await-thenable
const results3 = await Promise.all(commands3);
expect(results3[0]!.timestamp).toBeLessThanOrEqual(0.5);
expect(results3[1]!.timestamp).toBeLessThanOrEqual(0.4);
expect(results3[2]!.timestamp).toBeLessThanOrEqual(0.3);
expect(results3[3]!.timestamp).toBeLessThanOrEqual(0.2);
expect(results3[4]!.timestamp).toBeLessThanOrEqual(0.1);
expect(results3[5]!.timestamp).toBe(0);
expect(cursor3.debugInfo.decodedPackets.every(x => x.timestamp <= 0.5)).toBe(true);
expect(cursor3.debugInfo.pumpsStarted).toBe(1);
const cursor4 = await VideoSampleCursor.init(reader);
cursor4.debugInfo.enabled = true;
const commands4 = [
cursor4.seekToFirst(),
cursor4.next(),
cursor4.next(),
cursor4.close(),
];
expect(commands4.every(x => x instanceof Promise)).toBe(true);
// eslint-disable-next-line @typescript-eslint/await-thenable
const results4 = await Promise.all(commands4);
expect(results4[0]!.timestamp).toBe(0);
expect(results4[1]!.timestamp).toBeGreaterThan(results4[0]!.timestamp);
expect(results4[2]!.timestamp).toBeGreaterThan(results4[1]!.timestamp);
expect(cursor4.debugInfo.decodedPackets.length).toBeGreaterThan(3); // Because .next() goes into "sequential mode"
const cursor5 = await VideoSampleCursor.init(reader);
cursor5.debugInfo.enabled = true;
const commands5 = [
cursor5.seekTo(0),
cursor5.next(),
cursor5.next(),
cursor5.seekTo(0.4),
cursor5.next(),
cursor5.next(),
cursor5.seekTo(0.8),
cursor5.next(),
cursor5.next(),
cursor5.seekTo(0),
cursor5.next(),
cursor5.close(),
];
// eslint-disable-next-line @typescript-eslint/await-thenable
const results5 = await Promise.all(commands5);
expect(results5[0]!.timestamp).toBe(0);
expect(results5[1]!.timestamp).toBeGreaterThan(results5[0]!.timestamp);
expect(results5[2]!.timestamp).toBeGreaterThan(results5[1]!.timestamp);
expect(results5[3]!.timestamp).toBeLessThanOrEqual(0.4);
expect(results5[3]!.timestamp).toBeGreaterThan(results5[2]!.timestamp);
expect(results5[4]!.timestamp).toBeGreaterThan(results5[3]!.timestamp);
expect(results5[5]!.timestamp).toBeGreaterThan(results5[4]!.timestamp);
expect(results5[6]!.timestamp).toBeLessThanOrEqual(0.8);
expect(results5[6]!.timestamp).toBeGreaterThan(results5[5]!.timestamp);
expect(results5[7]!.timestamp).toBeGreaterThan(results5[6]!.timestamp);
expect(results5[8]!.timestamp).toBeGreaterThan(results5[7]!.timestamp);
expect(results5[9]!.timestamp).toBe(0);
expect(results5[10]!.timestamp).toBeGreaterThan(results5[9]!.timestamp);
expect(cursor5.debugInfo.pumpsStarted).toBe(1);
const cursor6 = await VideoSampleCursor.init(reader);
cursor6.debugInfo.enabled = true;
const commands6 = [
cursor6.seekTo(0),
cursor6.seekTo(0.4),
cursor6.seekTo(0.8),
cursor6.seekTo(3.4),
cursor6.seekTo(3.8),
cursor6.seekTo(5),
cursor6.close(),
];
// eslint-disable-next-line @typescript-eslint/await-thenable
const results6 = await Promise.all(commands6);
expect(results6[0]!.timestamp).toBe(0);
expect(results6[1]!.timestamp).toBeLessThanOrEqual(0.4);
expect(results6[2]!.timestamp).toBeLessThanOrEqual(0.8);
expect(results6[3]!.timestamp).toBeLessThanOrEqual(3.4);
expect(results6[4]!.timestamp).toBeLessThanOrEqual(3.8);
expect(results6[5]!.timestamp).toBe(5);
expect(cursor6.debugInfo.pumpsStarted).toBe(3);
const cursor7 = await VideoSampleCursor.init(reader);
const commands7 = [
cursor7.close(),
cursor7.close(),
];
expect(commands7[0]).toBe(commands7[1]); // Same Promise
await Promise.all(commands7);
const cursor8 = await VideoSampleCursor.init(reader, { autoClose: false });
const firstSample = await cursor8.seekToFirst();
firstSample!.close();
await new Promise(resolve => setTimeout(resolve, 200));
const commands8 = [
cursor8.next(),
cursor8.next(),
cursor8.next(),
];
expect(commands8.every(x => !(x instanceof Promise))).toBe(true);
for await (using sample of promiseIterateAll(commands8)) {
expect(sample!.closed).toBe(false);
}
await cursor8.close();
expect(VideoSample._openSampleCount).toBe(0);
});
// TODO:
// - Test throwing if cursor isn't closed at the end of the test
// - Sample cursor mapping function
// - AudioSampleCursor
// Test Sylvie video?
+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);
});
Binary file not shown.
Binary file not shown.