mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-03 22:03:49 +02:00
Move PacketReader elsewhere
This commit is contained in:
@@ -19,11 +19,10 @@ import {
|
|||||||
ResultValue,
|
ResultValue,
|
||||||
UNDETERMINED_LANGUAGE,
|
UNDETERMINED_LANGUAGE,
|
||||||
} from '../misc';
|
} from '../misc';
|
||||||
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
|
import { EncodedPacket, PacketRetrievalOptions, PLACEHOLDER_DATA } from '../packet';
|
||||||
import { readBytes, Reader } from '../reader';
|
import { readBytes, Reader } from '../reader';
|
||||||
import { DEFAULT_TRACK_DISPOSITION } from '../metadata';
|
import { DEFAULT_TRACK_DISPOSITION } from '../metadata';
|
||||||
import { FrameHeader, MAX_FRAME_HEADER_SIZE, MIN_FRAME_HEADER_SIZE, readFrameHeader } from './adts-reader';
|
import { FrameHeader, MAX_FRAME_HEADER_SIZE, MIN_FRAME_HEADER_SIZE, readFrameHeader } from './adts-reader';
|
||||||
import { PacketRetrievalOptions } from '../cursors';
|
|
||||||
|
|
||||||
const SAMPLES_PER_AAC_FRAME = 1024;
|
const SAMPLES_PER_AAC_FRAME = 1024;
|
||||||
|
|
||||||
|
|||||||
+7
-245
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
import { PCM_AUDIO_CODECS } from './codec';
|
import { PCM_AUDIO_CODECS } from './codec';
|
||||||
import { AudioDecoderWrapper, DecoderWrapper, PcmAudioDecoderWrapper, VideoDecoderWrapper } from './decode';
|
import { AudioDecoderWrapper, DecoderWrapper, PcmAudioDecoderWrapper, VideoDecoderWrapper } from './decode';
|
||||||
import { InputDisposedError } from './input';
|
|
||||||
import { InputAudioTrack, InputTrack, InputVideoTrack } from './input-track';
|
import { InputAudioTrack, InputTrack, InputVideoTrack } from './input-track';
|
||||||
import {
|
import {
|
||||||
assert,
|
assert,
|
||||||
@@ -24,230 +23,19 @@ import {
|
|||||||
ResultValue,
|
ResultValue,
|
||||||
Rotation,
|
Rotation,
|
||||||
AsyncGate,
|
AsyncGate,
|
||||||
isNumber,
|
|
||||||
MaybeRelevantPromise,
|
MaybeRelevantPromise,
|
||||||
} from './misc';
|
} from './misc';
|
||||||
import { EncodedPacket } from './packet';
|
import {
|
||||||
|
EncodedPacket,
|
||||||
|
PacketReader,
|
||||||
|
PacketRetrievalOptions,
|
||||||
|
validatePacketRetrievalOptions,
|
||||||
|
validateTimestamp,
|
||||||
|
} from './packet';
|
||||||
import { AudioSample, clampCropRectangle, CropRectangle, validateCropRectangle, VideoSample } from './sample';
|
import { AudioSample, clampCropRectangle, CropRectangle, validateCropRectangle, VideoSample } from './sample';
|
||||||
|
|
||||||
polyfillSymbolDispose();
|
polyfillSymbolDispose();
|
||||||
|
|
||||||
/**
|
|
||||||
* Additional options for controlling packet retrieval.
|
|
||||||
* @group Media sinks
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export type PacketRetrievalOptions = {
|
|
||||||
/**
|
|
||||||
* When set to `true`, only packet metadata (like timestamp) will be retrieved - the actual packet data will not
|
|
||||||
* be loaded.
|
|
||||||
*/
|
|
||||||
metadataOnly?: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When set to true, key packets will be verified upon retrieval by looking into the packet's bitstream.
|
|
||||||
* If not enabled, the packet types will be determined solely by what's stored in the containing file and may be
|
|
||||||
* incorrect, potentially leading to decoder errors. Since determining a packet's actual type requires looking into
|
|
||||||
* its data, this option cannot be enabled together with `metadataOnly`.
|
|
||||||
*/
|
|
||||||
verifyKeyPackets?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const validatePacketRetrievalOptions = (options: PacketRetrievalOptions) => {
|
|
||||||
if (!options || typeof options !== 'object') {
|
|
||||||
throw new TypeError('options must be an object.');
|
|
||||||
}
|
|
||||||
if (options.metadataOnly !== undefined && typeof options.metadataOnly !== 'boolean') {
|
|
||||||
throw new TypeError('options.metadataOnly, when defined, must be a boolean.');
|
|
||||||
}
|
|
||||||
if (options.verifyKeyPackets !== undefined && typeof options.verifyKeyPackets !== 'boolean') {
|
|
||||||
throw new TypeError('options.verifyKeyPackets, when defined, must be a boolean.');
|
|
||||||
}
|
|
||||||
if (options.verifyKeyPackets && options.metadataOnly) {
|
|
||||||
throw new TypeError('options.verifyKeyPackets and options.metadataOnly cannot be enabled together.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const validateTimestamp = (timestamp: number) => {
|
|
||||||
if (!isNumber(timestamp)) {
|
|
||||||
throw new TypeError('timestamp must be a number.'); // It can be non-finite, that's fine
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export class PacketReader<T extends InputTrack = InputTrack> {
|
|
||||||
track: T;
|
|
||||||
|
|
||||||
constructor(track: T) {
|
|
||||||
if (!(track instanceof InputTrack)) {
|
|
||||||
throw new TypeError('track must be an InputTrack.');
|
|
||||||
}
|
|
||||||
this.track = track;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _maybeVerifyPacketType(
|
|
||||||
packet: EncodedPacket | null,
|
|
||||||
options: PacketRetrievalOptions,
|
|
||||||
): MaybePromise<EncodedPacket | null> {
|
|
||||||
if (!options.verifyKeyPackets || !packet || packet.type === 'delta') {
|
|
||||||
return packet;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.track.determinePacketType(packet).then((determinedType) => {
|
|
||||||
if (determinedType) {
|
|
||||||
// @ts-expect-error Technically readonly
|
|
||||||
packet.type = determinedType;
|
|
||||||
}
|
|
||||||
|
|
||||||
return packet;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getFirst(options: PacketRetrievalOptions = {}): MaybePromise<EncodedPacket | null> {
|
|
||||||
validatePacketRetrievalOptions(options);
|
|
||||||
|
|
||||||
if (this.track.input._disposed) {
|
|
||||||
throw new InputDisposedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = new ResultValue<EncodedPacket | null>();
|
|
||||||
const promise = this.track._backing.getFirstPacket(result, options);
|
|
||||||
|
|
||||||
if (result.pending) {
|
|
||||||
return promise.then(() => this._maybeVerifyPacketType(result.value, options));
|
|
||||||
} else {
|
|
||||||
return this._maybeVerifyPacketType(result.value, options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getAt(timestamp: number, options: PacketRetrievalOptions = {}): MaybePromise<EncodedPacket | null> {
|
|
||||||
validateTimestamp(timestamp);
|
|
||||||
validatePacketRetrievalOptions(options);
|
|
||||||
|
|
||||||
if (this.track.input._disposed) {
|
|
||||||
throw new InputDisposedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = new ResultValue<EncodedPacket | null>();
|
|
||||||
const promise = this.track._backing.getPacket(result, timestamp, options);
|
|
||||||
|
|
||||||
if (result.pending) {
|
|
||||||
return promise.then(() => this._maybeVerifyPacketType(result.value, options));
|
|
||||||
} else {
|
|
||||||
return this._maybeVerifyPacketType(result.value, options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getKeyAt(timestamp: number, options: PacketRetrievalOptions = {}): MaybePromise<EncodedPacket | null> {
|
|
||||||
validateTimestamp(timestamp);
|
|
||||||
validatePacketRetrievalOptions(options);
|
|
||||||
|
|
||||||
if (this.track.input._disposed) {
|
|
||||||
throw new InputDisposedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.verifyKeyPackets) {
|
|
||||||
return this._readKeyAtVerified(timestamp, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = new ResultValue<EncodedPacket | null>();
|
|
||||||
const promise = this.track._backing.getKeyPacket(result, timestamp, options);
|
|
||||||
|
|
||||||
if (result.pending) {
|
|
||||||
return promise.then(() => result.value);
|
|
||||||
} else {
|
|
||||||
return result.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _readKeyAtVerified(
|
|
||||||
timestamp: number,
|
|
||||||
options: PacketRetrievalOptions,
|
|
||||||
): Promise<EncodedPacket | null> {
|
|
||||||
const result = new ResultValue<EncodedPacket | null>();
|
|
||||||
const promise = this.track._backing.getKeyPacket(result, timestamp, options);
|
|
||||||
if (result.pending) await promise;
|
|
||||||
|
|
||||||
const packet = result.value;
|
|
||||||
if (!packet) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const determinedType = await this.track.determinePacketType(packet);
|
|
||||||
if (determinedType === 'delta') {
|
|
||||||
// Try returning the previous key packet (in hopes that it's actually a key packet)
|
|
||||||
return this._readKeyAtVerified(packet.timestamp - 1 / this.track.timeResolution, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
return packet;
|
|
||||||
}
|
|
||||||
|
|
||||||
getNext(from: EncodedPacket, options: PacketRetrievalOptions = {}): MaybePromise<EncodedPacket | null> {
|
|
||||||
if (!(from instanceof EncodedPacket)) {
|
|
||||||
throw new TypeError('from must be an EncodedPacket.');
|
|
||||||
}
|
|
||||||
validatePacketRetrievalOptions(options);
|
|
||||||
|
|
||||||
if (this.track.input._disposed) {
|
|
||||||
throw new InputDisposedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = new ResultValue<EncodedPacket | null>();
|
|
||||||
const promise = this.track._backing.getNextPacket(result, from, options);
|
|
||||||
|
|
||||||
if (result.pending) {
|
|
||||||
return promise.then(() => this._maybeVerifyPacketType(result.value, options));
|
|
||||||
} else {
|
|
||||||
return this._maybeVerifyPacketType(result.value, options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getNextKey(from: EncodedPacket, options: PacketRetrievalOptions = {}): MaybePromise<EncodedPacket | null> {
|
|
||||||
if (!(from instanceof EncodedPacket)) {
|
|
||||||
throw new TypeError('from must be an EncodedPacket.');
|
|
||||||
}
|
|
||||||
validatePacketRetrievalOptions(options);
|
|
||||||
|
|
||||||
if (this.track.input._disposed) {
|
|
||||||
throw new InputDisposedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.verifyKeyPackets) {
|
|
||||||
return this._getNextKeyVerified(from, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = new ResultValue<EncodedPacket | null>();
|
|
||||||
const promise = this.track._backing.getNextKeyPacket(result, from, options);
|
|
||||||
|
|
||||||
if (result.pending) {
|
|
||||||
return promise.then(() => result.value);
|
|
||||||
} else {
|
|
||||||
return result.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _getNextKeyVerified(
|
|
||||||
from: EncodedPacket,
|
|
||||||
options: PacketRetrievalOptions,
|
|
||||||
): Promise<EncodedPacket | null> {
|
|
||||||
const result = new ResultValue<EncodedPacket | null>();
|
|
||||||
const promise = this.track._backing.getNextKeyPacket(result, from, options);
|
|
||||||
if (result.pending) await promise;
|
|
||||||
|
|
||||||
const nextPacket = result.value;
|
|
||||||
if (!nextPacket) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const determinedType = await this.track.determinePacketType(nextPacket);
|
|
||||||
if (determinedType === 'delta') {
|
|
||||||
// Try returning the next key packet (in hopes that it's actually a key packet)
|
|
||||||
return this._getNextKeyVerified(nextPacket, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
return nextPacket;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PacketCursor<T extends InputTrack = InputTrack> {
|
export class PacketCursor<T extends InputTrack = InputTrack> {
|
||||||
track: T;
|
track: T;
|
||||||
current: EncodedPacket | null = null;
|
current: EncodedPacket | null = null;
|
||||||
@@ -1640,29 +1428,3 @@ export const canvasTransformer = (
|
|||||||
return new WrappedCanvas(canvas, sample.timestamp, sample.duration);
|
return new WrappedCanvas(canvas, sample.timestamp, sample.duration);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* An AudioBuffer with additional timing information (timestamp & duration).
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export type WrappedAudioBuffer = {
|
|
||||||
/** An AudioBuffer. */
|
|
||||||
buffer: AudioBuffer;
|
|
||||||
/** The timestamp of the corresponding audio sample, in seconds. */
|
|
||||||
timestamp: number;
|
|
||||||
/** The duration of the corresponding audio sample, in seconds. */
|
|
||||||
duration: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const audioBufferTransformer = (): SampleTransformer<AudioSample, WrappedAudioBuffer> => {
|
|
||||||
return (sample) => {
|
|
||||||
const result: WrappedAudioBuffer = {
|
|
||||||
buffer: sample.toAudioBuffer(),
|
|
||||||
timestamp: sample.timestamp,
|
|
||||||
duration: sample.duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
sample.close();
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
textDecoder,
|
textDecoder,
|
||||||
UNDETERMINED_LANGUAGE,
|
UNDETERMINED_LANGUAGE,
|
||||||
} from '../misc';
|
} from '../misc';
|
||||||
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
|
import { EncodedPacket, PacketRetrievalOptions, PLACEHOLDER_DATA } from '../packet';
|
||||||
import {
|
import {
|
||||||
FileSlice,
|
FileSlice,
|
||||||
readBytes,
|
readBytes,
|
||||||
@@ -38,7 +38,6 @@ import {
|
|||||||
readSampleRate,
|
readSampleRate,
|
||||||
getSampleRateOrUncommon,
|
getSampleRateOrUncommon,
|
||||||
} from './flac-misc';
|
} from './flac-misc';
|
||||||
import { PacketRetrievalOptions } from '../cursors';
|
|
||||||
|
|
||||||
type FlacAudioInfo = {
|
type FlacAudioInfo = {
|
||||||
numberOfChannels: number;
|
numberOfChannels: number;
|
||||||
|
|||||||
+1
-1
@@ -161,6 +161,7 @@ export {
|
|||||||
EncodedPacket,
|
EncodedPacket,
|
||||||
EncodedPacketSideData,
|
EncodedPacketSideData,
|
||||||
PacketType,
|
PacketType,
|
||||||
|
PacketReader,
|
||||||
} from './packet';
|
} from './packet';
|
||||||
export {
|
export {
|
||||||
AudioSample,
|
AudioSample,
|
||||||
@@ -174,7 +175,6 @@ export {
|
|||||||
VIDEO_SAMPLE_PIXEL_FORMATS,
|
VIDEO_SAMPLE_PIXEL_FORMATS,
|
||||||
} from './sample';
|
} from './sample';
|
||||||
export {
|
export {
|
||||||
PacketReader,
|
|
||||||
PacketCursor,
|
PacketCursor,
|
||||||
SampleCursor,
|
SampleCursor,
|
||||||
VideoSampleCursor,
|
VideoSampleCursor,
|
||||||
|
|||||||
+2
-2
@@ -12,9 +12,9 @@ import { customAudioDecoders, customVideoDecoders } from './custom-coder';
|
|||||||
import { Input } from './input';
|
import { Input } from './input';
|
||||||
import { assert, MaybeRelevantPromise, ResultValue, Rotation } from './misc';
|
import { assert, MaybeRelevantPromise, ResultValue, Rotation } from './misc';
|
||||||
import { TrackType } from './output';
|
import { TrackType } from './output';
|
||||||
import { EncodedPacket, PacketType } from './packet';
|
import { EncodedPacket, PacketRetrievalOptions, PacketType } from './packet';
|
||||||
import { TrackDisposition } from './metadata';
|
import { TrackDisposition } from './metadata';
|
||||||
import { PacketCursor, PacketRetrievalOptions } from './cursors';
|
import { PacketCursor } from './cursors';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contains aggregate statistics about the encoded packets of a track.
|
* Contains aggregate statistics about the encoded packets of a track.
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ import {
|
|||||||
ResultValue,
|
ResultValue,
|
||||||
MaybeRelevantPromise,
|
MaybeRelevantPromise,
|
||||||
} from '../misc';
|
} from '../misc';
|
||||||
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
|
import { EncodedPacket, PacketRetrievalOptions, PLACEHOLDER_DATA } from '../packet';
|
||||||
import { buildIsobmffMimeType } from './isobmff-misc';
|
import { buildIsobmffMimeType } from './isobmff-misc';
|
||||||
import {
|
import {
|
||||||
MAX_BOX_HEADER_SIZE,
|
MAX_BOX_HEADER_SIZE,
|
||||||
@@ -88,7 +88,6 @@ import {
|
|||||||
readAscii,
|
readAscii,
|
||||||
} from '../reader';
|
} from '../reader';
|
||||||
import { DEFAULT_TRACK_DISPOSITION, MetadataTags, RichImageData, TrackDisposition } from '../metadata';
|
import { DEFAULT_TRACK_DISPOSITION, MetadataTags, RichImageData, TrackDisposition } from '../metadata';
|
||||||
import { PacketRetrievalOptions } from '../cursors';
|
|
||||||
|
|
||||||
type InternalTrack = {
|
type InternalTrack = {
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ import {
|
|||||||
TRANSFER_CHARACTERISTICS_MAP_INVERSE,
|
TRANSFER_CHARACTERISTICS_MAP_INVERSE,
|
||||||
UNDETERMINED_LANGUAGE,
|
UNDETERMINED_LANGUAGE,
|
||||||
} from '../misc';
|
} from '../misc';
|
||||||
import { EncodedPacket, EncodedPacketSideData, PLACEHOLDER_DATA } from '../packet';
|
import { EncodedPacket, EncodedPacketSideData, PacketRetrievalOptions, PLACEHOLDER_DATA } from '../packet';
|
||||||
import {
|
import {
|
||||||
assertDefinedSize,
|
assertDefinedSize,
|
||||||
CODEC_STRING_MAP,
|
CODEC_STRING_MAP,
|
||||||
@@ -70,7 +70,6 @@ import {
|
|||||||
} from './ebml';
|
} from './ebml';
|
||||||
import { buildMatroskaMimeType } from './matroska-misc';
|
import { buildMatroskaMimeType } from './matroska-misc';
|
||||||
import { FileSlice, readBytes, Reader, readI16Be, readU8 } from '../reader';
|
import { FileSlice, readBytes, Reader, readI16Be, readU8 } from '../reader';
|
||||||
import { PacketRetrievalOptions } from '../cursors';
|
|
||||||
|
|
||||||
type Segment = {
|
type Segment = {
|
||||||
seekHeadSeen: boolean;
|
seekHeadSeen: boolean;
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
ResultValue,
|
ResultValue,
|
||||||
UNDETERMINED_LANGUAGE,
|
UNDETERMINED_LANGUAGE,
|
||||||
} from '../misc';
|
} from '../misc';
|
||||||
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
|
import { EncodedPacket, PacketRetrievalOptions, PLACEHOLDER_DATA } from '../packet';
|
||||||
import { FrameHeader, getXingOffset, INFO, XING } from '../../shared/mp3-misc';
|
import { FrameHeader, getXingOffset, INFO, XING } from '../../shared/mp3-misc';
|
||||||
import {
|
import {
|
||||||
ID3_V1_TAG_SIZE,
|
ID3_V1_TAG_SIZE,
|
||||||
@@ -30,7 +30,6 @@ import {
|
|||||||
} from '../id3';
|
} from '../id3';
|
||||||
import { readNextFrameHeader } from './mp3-reader';
|
import { readNextFrameHeader } from './mp3-reader';
|
||||||
import { readAscii, readBytes, Reader, readU32Be } from '../reader';
|
import { readAscii, readBytes, Reader, readU32Be } from '../reader';
|
||||||
import { PacketRetrievalOptions } from '../cursors';
|
|
||||||
|
|
||||||
type Sample = {
|
type Sample = {
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
import { OPUS_SAMPLE_RATE } from '../codec';
|
import { OPUS_SAMPLE_RATE } from '../codec';
|
||||||
import { parseModesFromVorbisSetupPacket, parseOpusIdentificationHeader, readVorbisComments } from '../codec-data';
|
import { parseModesFromVorbisSetupPacket, parseOpusIdentificationHeader, readVorbisComments } from '../codec-data';
|
||||||
import { PacketRetrievalOptions } from '../cursors';
|
|
||||||
import { Demuxer } from '../demuxer';
|
import { Demuxer } from '../demuxer';
|
||||||
import { Input } from '../input';
|
import { Input } from '../input';
|
||||||
import { InputAudioTrack, InputAudioTrackBacking } from '../input-track';
|
import { InputAudioTrack, InputAudioTrackBacking } from '../input-track';
|
||||||
@@ -25,7 +24,7 @@ import {
|
|||||||
toDataView,
|
toDataView,
|
||||||
UNDETERMINED_LANGUAGE,
|
UNDETERMINED_LANGUAGE,
|
||||||
} from '../misc';
|
} from '../misc';
|
||||||
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
|
import { EncodedPacket, PacketRetrievalOptions, PLACEHOLDER_DATA } from '../packet';
|
||||||
import { readBytes, Reader } from '../reader';
|
import { readBytes, Reader } from '../reader';
|
||||||
import { buildOggMimeType, computeOggPageCrc, extractSampleMetadata, OggCodecInfo } from './ogg-misc';
|
import { buildOggMimeType, computeOggPageCrc, extractSampleMetadata, OggCodecInfo } from './ogg-misc';
|
||||||
import {
|
import {
|
||||||
|
|||||||
+219
-1
@@ -6,7 +6,9 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { SECOND_TO_MICROSECOND_FACTOR } from './misc';
|
import { InputDisposedError } from './input';
|
||||||
|
import { InputTrack } from './input-track';
|
||||||
|
import { isNumber, MaybePromise, ResultValue, SECOND_TO_MICROSECOND_FACTOR } from './misc';
|
||||||
|
|
||||||
export const PLACEHOLDER_DATA = /* #__PURE__ */ new Uint8Array(0);
|
export const PLACEHOLDER_DATA = /* #__PURE__ */ new Uint8Array(0);
|
||||||
|
|
||||||
@@ -267,3 +269,219 @@ export class EncodedPacket {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional options for controlling packet retrieval.
|
||||||
|
* @group Media sinks
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export type PacketRetrievalOptions = {
|
||||||
|
/**
|
||||||
|
* When set to `true`, only packet metadata (like timestamp) will be retrieved - the actual packet data will not
|
||||||
|
* be loaded.
|
||||||
|
*/
|
||||||
|
metadataOnly?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When set to true, key packets will be verified upon retrieval by looking into the packet's bitstream.
|
||||||
|
* If not enabled, the packet types will be determined solely by what's stored in the containing file and may be
|
||||||
|
* incorrect, potentially leading to decoder errors. Since determining a packet's actual type requires looking into
|
||||||
|
* its data, this option cannot be enabled together with `metadataOnly`.
|
||||||
|
*/
|
||||||
|
verifyKeyPackets?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validatePacketRetrievalOptions = (options: PacketRetrievalOptions) => {
|
||||||
|
if (!options || typeof options !== 'object') {
|
||||||
|
throw new TypeError('options must be an object.');
|
||||||
|
}
|
||||||
|
if (options.metadataOnly !== undefined && typeof options.metadataOnly !== 'boolean') {
|
||||||
|
throw new TypeError('options.metadataOnly, when defined, must be a boolean.');
|
||||||
|
}
|
||||||
|
if (options.verifyKeyPackets !== undefined && typeof options.verifyKeyPackets !== 'boolean') {
|
||||||
|
throw new TypeError('options.verifyKeyPackets, when defined, must be a boolean.');
|
||||||
|
}
|
||||||
|
if (options.verifyKeyPackets && options.metadataOnly) {
|
||||||
|
throw new TypeError('options.verifyKeyPackets and options.metadataOnly cannot be enabled together.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateTimestamp = (timestamp: number) => {
|
||||||
|
if (!isNumber(timestamp)) {
|
||||||
|
throw new TypeError('timestamp must be a number.'); // It can be non-finite, that's fine
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export class PacketReader<T extends InputTrack = InputTrack> {
|
||||||
|
track: T;
|
||||||
|
|
||||||
|
constructor(track: T) {
|
||||||
|
if (!(track instanceof InputTrack)) {
|
||||||
|
throw new TypeError('track must be an InputTrack.');
|
||||||
|
}
|
||||||
|
this.track = track;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _maybeVerifyPacketType(
|
||||||
|
packet: EncodedPacket | null,
|
||||||
|
options: PacketRetrievalOptions,
|
||||||
|
): MaybePromise<EncodedPacket | null> {
|
||||||
|
if (!options.verifyKeyPackets || !packet || packet.type === 'delta') {
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.track.determinePacketType(packet).then((determinedType) => {
|
||||||
|
if (determinedType) {
|
||||||
|
// @ts-expect-error Technically readonly
|
||||||
|
packet.type = determinedType;
|
||||||
|
}
|
||||||
|
|
||||||
|
return packet;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getFirst(options: PacketRetrievalOptions = {}): MaybePromise<EncodedPacket | null> {
|
||||||
|
validatePacketRetrievalOptions(options);
|
||||||
|
|
||||||
|
if (this.track.input._disposed) {
|
||||||
|
throw new InputDisposedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = new ResultValue<EncodedPacket | null>();
|
||||||
|
const promise = this.track._backing.getFirstPacket(result, options);
|
||||||
|
|
||||||
|
if (result.pending) {
|
||||||
|
return promise.then(() => this._maybeVerifyPacketType(result.value, options));
|
||||||
|
} else {
|
||||||
|
return this._maybeVerifyPacketType(result.value, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAt(timestamp: number, options: PacketRetrievalOptions = {}): MaybePromise<EncodedPacket | null> {
|
||||||
|
validateTimestamp(timestamp);
|
||||||
|
validatePacketRetrievalOptions(options);
|
||||||
|
|
||||||
|
if (this.track.input._disposed) {
|
||||||
|
throw new InputDisposedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = new ResultValue<EncodedPacket | null>();
|
||||||
|
const promise = this.track._backing.getPacket(result, timestamp, options);
|
||||||
|
|
||||||
|
if (result.pending) {
|
||||||
|
return promise.then(() => this._maybeVerifyPacketType(result.value, options));
|
||||||
|
} else {
|
||||||
|
return this._maybeVerifyPacketType(result.value, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getKeyAt(timestamp: number, options: PacketRetrievalOptions = {}): MaybePromise<EncodedPacket | null> {
|
||||||
|
validateTimestamp(timestamp);
|
||||||
|
validatePacketRetrievalOptions(options);
|
||||||
|
|
||||||
|
if (this.track.input._disposed) {
|
||||||
|
throw new InputDisposedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.verifyKeyPackets) {
|
||||||
|
return this._readKeyAtVerified(timestamp, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = new ResultValue<EncodedPacket | null>();
|
||||||
|
const promise = this.track._backing.getKeyPacket(result, timestamp, options);
|
||||||
|
|
||||||
|
if (result.pending) {
|
||||||
|
return promise.then(() => result.value);
|
||||||
|
} else {
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _readKeyAtVerified(
|
||||||
|
timestamp: number,
|
||||||
|
options: PacketRetrievalOptions,
|
||||||
|
): Promise<EncodedPacket | null> {
|
||||||
|
const result = new ResultValue<EncodedPacket | null>();
|
||||||
|
const promise = this.track._backing.getKeyPacket(result, timestamp, options);
|
||||||
|
if (result.pending) await promise;
|
||||||
|
|
||||||
|
const packet = result.value;
|
||||||
|
if (!packet) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const determinedType = await this.track.determinePacketType(packet);
|
||||||
|
if (determinedType === 'delta') {
|
||||||
|
// Try returning the previous key packet (in hopes that it's actually a key packet)
|
||||||
|
return this._readKeyAtVerified(packet.timestamp - 1 / this.track.timeResolution, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
getNext(from: EncodedPacket, options: PacketRetrievalOptions = {}): MaybePromise<EncodedPacket | null> {
|
||||||
|
if (!(from instanceof EncodedPacket)) {
|
||||||
|
throw new TypeError('from must be an EncodedPacket.');
|
||||||
|
}
|
||||||
|
validatePacketRetrievalOptions(options);
|
||||||
|
|
||||||
|
if (this.track.input._disposed) {
|
||||||
|
throw new InputDisposedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = new ResultValue<EncodedPacket | null>();
|
||||||
|
const promise = this.track._backing.getNextPacket(result, from, options);
|
||||||
|
|
||||||
|
if (result.pending) {
|
||||||
|
return promise.then(() => this._maybeVerifyPacketType(result.value, options));
|
||||||
|
} else {
|
||||||
|
return this._maybeVerifyPacketType(result.value, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getNextKey(from: EncodedPacket, options: PacketRetrievalOptions = {}): MaybePromise<EncodedPacket | null> {
|
||||||
|
if (!(from instanceof EncodedPacket)) {
|
||||||
|
throw new TypeError('from must be an EncodedPacket.');
|
||||||
|
}
|
||||||
|
validatePacketRetrievalOptions(options);
|
||||||
|
|
||||||
|
if (this.track.input._disposed) {
|
||||||
|
throw new InputDisposedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.verifyKeyPackets) {
|
||||||
|
return this._getNextKeyVerified(from, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = new ResultValue<EncodedPacket | null>();
|
||||||
|
const promise = this.track._backing.getNextKeyPacket(result, from, options);
|
||||||
|
|
||||||
|
if (result.pending) {
|
||||||
|
return promise.then(() => result.value);
|
||||||
|
} else {
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _getNextKeyVerified(
|
||||||
|
from: EncodedPacket,
|
||||||
|
options: PacketRetrievalOptions,
|
||||||
|
): Promise<EncodedPacket | null> {
|
||||||
|
const result = new ResultValue<EncodedPacket | null>();
|
||||||
|
const promise = this.track._backing.getNextKeyPacket(result, from, options);
|
||||||
|
if (result.pending) await promise;
|
||||||
|
|
||||||
|
const nextPacket = result.value;
|
||||||
|
if (!nextPacket) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const determinedType = await this.track.determinePacketType(nextPacket);
|
||||||
|
if (determinedType === 'delta') {
|
||||||
|
// Try returning the next key packet (in hopes that it's actually a key packet)
|
||||||
|
return this._getNextKeyVerified(nextPacket, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextPacket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,10 +12,9 @@ import { Input } from '../input';
|
|||||||
import { InputAudioTrack, InputAudioTrackBacking } from '../input-track';
|
import { InputAudioTrack, InputAudioTrackBacking } from '../input-track';
|
||||||
import { DEFAULT_TRACK_DISPOSITION, MetadataTags } from '../metadata';
|
import { DEFAULT_TRACK_DISPOSITION, MetadataTags } from '../metadata';
|
||||||
import { assert, MaybeRelevantPromise, ResultValue, UNDETERMINED_LANGUAGE } from '../misc';
|
import { assert, MaybeRelevantPromise, ResultValue, UNDETERMINED_LANGUAGE } from '../misc';
|
||||||
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
|
import { EncodedPacket, PacketRetrievalOptions, PLACEHOLDER_DATA } from '../packet';
|
||||||
import { readAscii, readBytes, Reader, readU16, readU32, readU64 } from '../reader';
|
import { readAscii, readBytes, Reader, readU16, readU32, readU64 } from '../reader';
|
||||||
import { parseId3V2Tag, readId3V2Header } from '../id3';
|
import { parseId3V2Tag, readId3V2Header } from '../id3';
|
||||||
import { PacketRetrievalOptions } from '../cursors';
|
|
||||||
|
|
||||||
export enum WaveFormat {
|
export enum WaveFormat {
|
||||||
PCM = 0x0001,
|
PCM = 0x0001,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Input } from '../../src/input.js';
|
|||||||
import { BufferSource, UrlSource } from '../../src/source.js';
|
import { BufferSource, UrlSource } from '../../src/source.js';
|
||||||
import { ALL_FORMATS } from '../../src/input-format.js';
|
import { ALL_FORMATS } from '../../src/input-format.js';
|
||||||
import {
|
import {
|
||||||
audioBufferTransformer,
|
|
||||||
AudioSampleCursor,
|
AudioSampleCursor,
|
||||||
canvasTransformer,
|
canvasTransformer,
|
||||||
VideoSampleCursor,
|
VideoSampleCursor,
|
||||||
@@ -1115,33 +1114,6 @@ test('Canvas transformer', async () => {
|
|||||||
await cursor2.close();
|
await cursor2.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('AudioBuffer transformer', async () => {
|
|
||||||
using input = new Input({
|
|
||||||
source: new UrlSource('/trim-buck-bunny.mov'),
|
|
||||||
formats: ALL_FORMATS,
|
|
||||||
});
|
|
||||||
|
|
||||||
const audioTrack = (await input.getPrimaryAudioTrack())!;
|
|
||||||
|
|
||||||
const cursor = new AudioSampleCursor(audioTrack, {
|
|
||||||
transform: audioBufferTransformer(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const firstSample = (await cursor.seekToFirst())!;
|
|
||||||
expect(firstSample.buffer).toBeInstanceOf(AudioBuffer);
|
|
||||||
expect(firstSample.timestamp).toBe(0);
|
|
||||||
expect(firstSample.duration).toBeGreaterThan(0);
|
|
||||||
expect(firstSample.buffer.duration).toBe(firstSample.duration);
|
|
||||||
|
|
||||||
const nextSample = (await cursor.next())!;
|
|
||||||
expect(nextSample.buffer).toBeInstanceOf(AudioBuffer);
|
|
||||||
expect(nextSample.timestamp).toBeGreaterThan(firstSample.timestamp);
|
|
||||||
expect(nextSample.buffer).not.toBe(firstSample.buffer);
|
|
||||||
expect(nextSample.buffer.duration).toBe(nextSample.duration);
|
|
||||||
|
|
||||||
await cursor.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Unthrottled decoder', async () => {
|
test('Unthrottled decoder', async () => {
|
||||||
// While the decoder is throttled in practice, these tests simulate what happens when the decoder naturally reaches
|
// While the decoder is throttled in practice, these tests simulate what happens when the decoder naturally reaches
|
||||||
// the end of the encodable packets without having been stopped; in this case, we still expect the cursor to keep
|
// the end of the encodable packets without having been stopped; in this case, we still expect the cursor to keep
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import { CanvasSource, VideoSampleSource } from '../../src/media-source.js';
|
|||||||
import { canEncodeVideo, QUALITY_HIGH } from '../../src/encode.js';
|
import { canEncodeVideo, QUALITY_HIGH } from '../../src/encode.js';
|
||||||
import { VideoSample } from '../../src/sample.js';
|
import { VideoSample } from '../../src/sample.js';
|
||||||
import { Conversion } from '../../src/conversion.js';
|
import { Conversion } from '../../src/conversion.js';
|
||||||
import { canvasTransformer, PacketCursor, PacketReader, VideoSampleCursor } from '../../src/cursors.js';
|
import { canvasTransformer, PacketCursor, VideoSampleCursor } from '../../src/cursors.js';
|
||||||
|
import { PacketReader } from '../../src/packet.js';
|
||||||
|
|
||||||
test('Can decode transparent video', async () => {
|
test('Can decode transparent video', async () => {
|
||||||
using input = new Input({
|
using input = new Input({
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Mp4OutputFormat } from '../../src/output-format.js';
|
|||||||
import { BufferTarget } from '../../src/target.js';
|
import { BufferTarget } from '../../src/target.js';
|
||||||
import { Conversion } from '../../src/conversion.js';
|
import { Conversion } from '../../src/conversion.js';
|
||||||
import { extractAvcNalUnits } from '../../src/codec-data.js';
|
import { extractAvcNalUnits } from '../../src/codec-data.js';
|
||||||
import { PacketReader } from '../../src/cursors.js';
|
import { PacketReader } from '../../src/packet.js';
|
||||||
|
|
||||||
const __dirname = new URL('.', import.meta.url).pathname;
|
const __dirname = new URL('.', import.meta.url).pathname;
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ import { BufferSource, FilePathSource } from '../../src/source.js';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import { ADTS, ALL_FORMATS, FLAC, MP3, MP4, OGG, QTFF, WAVE, WEBM } from '../../src/input-format.js';
|
import { ADTS, ALL_FORMATS, FLAC, MP3, MP4, OGG, QTFF, WAVE, WEBM } from '../../src/input-format.js';
|
||||||
import { PacketReader } from '../../src/cursors.js';
|
|
||||||
import { InputAudioTrack, InputTrack } from '../../src/input-track.js';
|
import { InputAudioTrack, InputTrack } from '../../src/input-track.js';
|
||||||
import { assert } from '../../src/misc.js';
|
import { assert } from '../../src/misc.js';
|
||||||
import { EncodedPacket } from '../../src/packet.js';
|
import { EncodedPacket, PacketReader } from '../../src/packet.js';
|
||||||
|
|
||||||
const __dirname = new URL('.', import.meta.url).pathname;
|
const __dirname = new URL('.', import.meta.url).pathname;
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import { Output } from '../../src/output.js';
|
|||||||
import { BufferTarget } from '../../src/target.js';
|
import { BufferTarget } from '../../src/target.js';
|
||||||
import { FlacOutputFormat } from '../../src/output-format.js';
|
import { FlacOutputFormat } from '../../src/output-format.js';
|
||||||
import { Conversion } from '../../src/conversion.js';
|
import { Conversion } from '../../src/conversion.js';
|
||||||
import { PacketCursor, PacketReader } from '../../src/cursors.js';
|
import { PacketCursor } from '../../src/cursors.js';
|
||||||
|
import { PacketReader } from '../../src/packet.js';
|
||||||
|
|
||||||
const __dirname = new URL('.', import.meta.url).pathname;
|
const __dirname = new URL('.', import.meta.url).pathname;
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import { BufferSource, FilePathSource } from '../../src/source.js';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import { ALL_FORMATS } from '../../src/input-format.js';
|
import { ALL_FORMATS } from '../../src/input-format.js';
|
||||||
import { PacketCursor, PacketReader } from '../../src/cursors.js';
|
import { PacketCursor } from '../../src/cursors.js';
|
||||||
import { promiseAllEnsureOrder } from '../../src/misc.js';
|
import { promiseAllEnsureOrder } from '../../src/misc.js';
|
||||||
|
import { PacketReader } from '../../src/packet.js';
|
||||||
|
|
||||||
const __dirname = new URL('.', import.meta.url).pathname;
|
const __dirname = new URL('.', import.meta.url).pathname;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user