mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-09 16:53:47 +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:
+45
-2
@@ -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));
|
||||||
|
|||||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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?
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user