Fix EBML parsers for large integers, make small API and docblock adjustments

This commit is contained in:
Vanilagy
2025-04-25 17:35:03 +02:00
parent 03b9c6c9ae
commit 7fe0593b6c
11 changed files with 68 additions and 55 deletions
+8 -8
View File
@@ -15,7 +15,7 @@
const file = fileInput.files[0]; const file = fileInput.files[0];
const source = new Metamuxer.BlobSource(file); const source = new Metamuxer.BlobSource(file);
const target = new Metamuxer.StreamTarget(new WritableStream({ const target = new Metamuxer.BufferTarget() ?? new Metamuxer.StreamTarget(new WritableStream({
write: console.log write: console.log
}), { }), {
chunked: true, chunked: true,
@@ -41,8 +41,8 @@
target target
}), }),
audio: { audio: {
//discard: true discard: true
forceReencode: true, //forceReencode: true,
}, },
/* /*
video: { video: {
@@ -73,16 +73,16 @@
//rotate: 90 //rotate: 90
//width: 720 ?? 2160, //width: 720 ?? 2160,
//height: 1280 ?? 3840, //height: 1280 ?? 3840,
fit: 'contain', //fit: 'contain',
rotate: 90, //rotate: 90,
width: 512, //width: 512,
height: 512, //height: 512,
//width: 200, //width: 200,
//height: 100, //height: 100,
}, },
trim: { trim: {
start: 0, start: 0,
//end: 60 end: 30
}, },
computeProgress: true computeProgress: true
}); });
+1 -1
View File
@@ -50,7 +50,7 @@ if (!(await videoTrack?.canDecode())) {
videoTrack = null; videoTrack = null;
} }
if (!(await audioTrack?.canDecode())) { if (!(await audioTrack?.canDecode())) {
audioTrack = null; audioTrack = null;
} }
const canvas = document.querySelector('canvas'); const canvas = document.querySelector('canvas');
+2 -2
View File
@@ -1303,7 +1303,7 @@ export const validateVideoChunkMetadata = (metadata: EncodedVideoChunkMetadata |
if (!VALID_VIDEO_CODEC_STRING_PREFIXES.some(prefix => metadata.decoderConfig!.codec.startsWith(prefix))) { if (!VALID_VIDEO_CODEC_STRING_PREFIXES.some(prefix => metadata.decoderConfig!.codec.startsWith(prefix))) {
throw new TypeError( throw new TypeError(
'Video chunk metadata decoder configuration codec string must be a valid video codec string as specified in' 'Video chunk metadata decoder configuration codec string must be a valid video codec string as specified in'
+ ' the WebCodecs codec registry.', + ' the WebCodecs Codec Registry.',
); );
} }
if (!Number.isInteger(metadata.decoderConfig.codedWidth) || metadata.decoderConfig.codedWidth! <= 0) { if (!Number.isInteger(metadata.decoderConfig.codedWidth) || metadata.decoderConfig.codedWidth! <= 0) {
@@ -1444,7 +1444,7 @@ export const validateAudioChunkMetadata = (metadata: EncodedAudioChunkMetadata |
if (!VALID_AUDIO_CODEC_STRING_PREFIXES.some(prefix => metadata.decoderConfig!.codec.startsWith(prefix))) { if (!VALID_AUDIO_CODEC_STRING_PREFIXES.some(prefix => metadata.decoderConfig!.codec.startsWith(prefix))) {
throw new TypeError( throw new TypeError(
'Audio chunk metadata decoder configuration codec string must be a valid audio codec string as specified in' 'Audio chunk metadata decoder configuration codec string must be a valid audio codec string as specified in'
+ ' the WebCodecs codec registry.', + ' the WebCodecs Codec Registry.',
); );
} }
if (!Number.isInteger(metadata.decoderConfig.sampleRate) || metadata.decoderConfig.sampleRate <= 0) { if (!Number.isInteger(metadata.decoderConfig.sampleRate) || metadata.decoderConfig.sampleRate <= 0) {
+12 -6
View File
@@ -52,13 +52,13 @@ export type ConversionOptions = {
/** The desired bitrate of the output video. */ /** The desired bitrate of the output video. */
bitrate?: VideoEncodingConfig['bitrate']; bitrate?: VideoEncodingConfig['bitrate'];
/** /**
* The desired width of the output video, defaulting to the video's natural display width. If height is not set, * The desired width of the output video in pixels, defaulting to the video's natural display width. If height
* it will be deduced automatically based on aspect ratio. * is not set, it will be deduced automatically based on aspect ratio.
*/ */
width?: number; width?: number;
/** /**
* The desired height of the output video, defaulting to the video's natural display height. If width is not * The desired height of the output video in pixels, defaulting to the video's natural display height. If width
* set, it will be deduced automatically based on aspect ratio. * is not set, it will be deduced automatically based on aspect ratio.
*/ */
height?: number; height?: number;
/** /**
@@ -492,8 +492,11 @@ export class Conversion {
const sink = new EncodedPacketSink(track); const sink = new EncodedPacketSink(track);
const decoderConfig = await track.getDecoderConfig(); const decoderConfig = await track.getDecoderConfig();
const meta: EncodedVideoChunkMetadata = { decoderConfig: decoderConfig ?? undefined }; const meta: EncodedVideoChunkMetadata = { decoderConfig: decoderConfig ?? undefined };
const endPacket = Number.isFinite(this._endTimestamp)
? await sink.getPacket(this._endTimestamp, { metadataOnly: true }) ?? undefined
: undefined;
for await (const packet of sink.packets(undefined, this._endTimestamp)) { for await (const packet of sink.packets(undefined, endPacket)) {
if (this._synchronizer.shouldWait(track.id, packet.timestamp)) { if (this._synchronizer.shouldWait(track.id, packet.timestamp)) {
await this._synchronizer.wait(packet.timestamp); await this._synchronizer.wait(packet.timestamp);
} }
@@ -658,8 +661,11 @@ export class Conversion {
const sink = new EncodedPacketSink(track); const sink = new EncodedPacketSink(track);
const decoderConfig = await track.getDecoderConfig(); const decoderConfig = await track.getDecoderConfig();
const meta: EncodedAudioChunkMetadata = { decoderConfig: decoderConfig ?? undefined }; const meta: EncodedAudioChunkMetadata = { decoderConfig: decoderConfig ?? undefined };
const endPacket = Number.isFinite(this._endTimestamp)
? await sink.getPacket(this._endTimestamp, { metadataOnly: true }) ?? undefined
: undefined;
for await (const packet of sink.packets(undefined, this._endTimestamp)) { for await (const packet of sink.packets(undefined, endPacket)) {
if (this._synchronizer.shouldWait(track.id, packet.timestamp)) { if (this._synchronizer.shouldWait(track.id, packet.timestamp)) {
await this._synchronizer.wait(packet.timestamp); await this._synchronizer.wait(packet.timestamp);
} }
+5 -5
View File
@@ -158,12 +158,12 @@ export class InputVideoTrack extends InputTrack {
return this._backing.getCodec(); return this._backing.getCodec();
} }
/** The width of the track's coded samples, before any transformations or rotations. */ /** The width in pixels of the track's coded samples, before any transformations or rotations. */
get codedWidth() { get codedWidth() {
return this._backing.getCodedWidth(); return this._backing.getCodedWidth();
} }
/** The height of the track's coded samples, before any transformations or rotations. */ /** The height in pixels of the track's coded samples, before any transformations or rotations. */
get codedHeight() { get codedHeight() {
return this._backing.getCodedHeight(); return this._backing.getCodedHeight();
} }
@@ -173,13 +173,13 @@ export class InputVideoTrack extends InputTrack {
return this._backing.getRotation(); return this._backing.getRotation();
} }
/** The width of the track's frames after rotation. */ /** The width in pixels of the track's frames after rotation. */
get displayWidth() { get displayWidth() {
const rotation = this._backing.getRotation(); const rotation = this._backing.getRotation();
return rotation % 180 === 0 ? this._backing.getCodedWidth() : this._backing.getCodedHeight(); return rotation % 180 === 0 ? this._backing.getCodedWidth() : this._backing.getCodedHeight();
} }
/** The height of the track's frames after rotation. */ /** The height in pixels of the track's frames after rotation. */
get displayHeight() { get displayHeight() {
const rotation = this._backing.getRotation(); const rotation = this._backing.getRotation();
return rotation % 180 === 0 ? this._backing.getCodedHeight() : this._backing.getCodedWidth(); return rotation % 180 === 0 ? this._backing.getCodedHeight() : this._backing.getCodedWidth();
@@ -190,7 +190,7 @@ export class InputVideoTrack extends InputTrack {
return this._backing.getColorSpace(); return this._backing.getColorSpace();
} }
/** Returns true iff the track's samples use a high dynamic range (HDR). */ /** If this method returns true, the track's samples use a high dynamic range (HDR). */
async hasHighDynamicRange() { async hasHighDynamicRange() {
const colorSpace = await this._backing.getColorSpace(); const colorSpace = await this._backing.getColorSpace();
+6 -5
View File
@@ -393,15 +393,14 @@ export class EBMLReader {
const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 1); const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 1);
const firstByte = view.getUint8(offset); const firstByte = view.getUint8(offset);
// Find the position of the first set bit, which determines the width // Find the position of VINT_MARKER, which determines the width
let width = 1; let width = 1;
let mask = 0x80; let mask = 1 << 7;
while ((firstByte & mask) === 0 && width < MAX_VAR_INT_SIZE) { while ((firstByte & mask) === 0 && width < MAX_VAR_INT_SIZE) {
width++; width++;
mask >>= 1; mask >>= 1;
} }
// Read all bytes
const { view: fullView, offset: fullOffset } = this.reader.getViewAndOffset(this.pos, this.pos + width); const { view: fullView, offset: fullOffset } = this.reader.getViewAndOffset(this.pos, this.pos + width);
// First byte's value needs the marker bit cleared // First byte's value needs the marker bit cleared
@@ -409,7 +408,8 @@ export class EBMLReader {
// Read remaining bytes // Read remaining bytes
for (let i = 1; i < width; i++) { for (let i = 1; i < width; i++) {
value = (value << 8) | fullView.getUint8(fullOffset + i); value *= 1 << 8;
value += fullView.getUint8(fullOffset + i);
} }
this.pos += width; this.pos += width;
@@ -426,7 +426,8 @@ export class EBMLReader {
// Read bytes from most significant to least significant // Read bytes from most significant to least significant
for (let i = 0; i < width; i++) { for (let i = 0; i < width; i++) {
value = (value << 8) | view.getUint8(offset + i); value *= 1 << 8;
value += view.getUint8(offset + i);
} }
this.pos += width; this.pos += width;
+28 -17
View File
@@ -129,13 +129,24 @@ export class EncodedPacketSink {
* method will intelligently preload packets based on the speed of the consumer. * method will intelligently preload packets based on the speed of the consumer.
* *
* @param startPacket - (optional) The packet from which iteration should begin. This packet will also be yielded. * @param startPacket - (optional) The packet from which iteration should begin. This packet will also be yielded.
* @param endTimestamp - The timestamp in seconds at which to stop iteration. This timestamp is exclusive. * @param endTimestamp - (optional) The timestamp at which iteration should end. This packet will _not_ be yielded.
*/ */
packets( packets(
startPacket?: EncodedPacket, startPacket?: EncodedPacket,
endTimestamp = Infinity, endPacket?: EncodedPacket,
options?: PacketRetrievalOptions, options: PacketRetrievalOptions = {},
): AsyncGenerator<EncodedPacket, void, unknown> { ): AsyncGenerator<EncodedPacket, void, unknown> {
if (startPacket !== undefined && !(startPacket instanceof EncodedPacket)) {
throw new TypeError('startPacket must be an EncodedPacket.');
}
if (startPacket !== undefined && startPacket.isMetadataOnly && !options?.metadataOnly) {
throw new TypeError('startPacket can only be metadata-only if options.metadataOnly is enabled.');
}
if (endPacket !== undefined && !(endPacket instanceof EncodedPacket)) {
throw new TypeError('endPacket must be an EncodedPacket.');
}
validatePacketRetrievalOptions(options);
const packetQueue: EncodedPacket[] = []; const packetQueue: EncodedPacket[] = [];
let { promise: queueNotEmpty, resolve: onQueueNotEmpty } = promiseWithResolvers(); let { promise: queueNotEmpty, resolve: onQueueNotEmpty } = promiseWithResolvers();
@@ -157,7 +168,7 @@ export class EncodedPacketSink {
let packet = startPacket ?? await this.getFirstPacket(options); let packet = startPacket ?? await this.getFirstPacket(options);
while (packet && !terminated) { while (packet && !terminated) {
if (packet.timestamp >= endTimestamp) { if (endPacket && packet.sequenceNumber >= endPacket?.sequenceNumber) {
break; break;
} }
@@ -332,25 +343,25 @@ export abstract class BaseMediaSampleSink<
let currentPacket: EncodedPacket | null = keyPacket; let currentPacket: EncodedPacket | null = keyPacket;
let packetsEndTimestamp = Infinity; let endPacket: EncodedPacket | undefined = undefined;
if (endTimestamp < Infinity) { if (endTimestamp < Infinity) {
// When an end timestamp is set, we cannot simply use that for the packet iterator due to out-of-order // When an end timestamp is set, we cannot simply use that for the packet iterator due to out-of-order
// frames (B-frames). Instead, we'll need to keep decoding packets until we get a frame that exceeds // frames (B-frames). Instead, we'll need to keep decoding packets until we get a frame that exceeds
// this end time. However, we can still put a bound on it: Since key frames are by definition never // this end time. However, we can still put a bound on it: Since key frames are by definition never
// out of order, we can stop at the first key frame after the end timestamp. // out of order, we can stop at the first key frame after the end timestamp.
const endPacket = await packetSink.getPacket(endTimestamp); const packet = await packetSink.getPacket(endTimestamp);
const endKeyPacket = !endPacket const keyPacket = !packet
? null ? null
: endPacket.type === 'key' && endPacket.timestamp === endTimestamp : packet.type === 'key' && packet.timestamp === endTimestamp
? endPacket ? packet
: await packetSink.getNextKeyPacket(endPacket); : await packetSink.getNextKeyPacket(packet);
if (endKeyPacket) { if (keyPacket) {
packetsEndTimestamp = endKeyPacket.timestamp; endPacket = keyPacket;
} }
} }
const packets = packetSink.packets(keyPacket, packetsEndTimestamp); const packets = packetSink.packets(keyPacket, endPacket);
await packets.next(); // Skip the start packet as we already have it await packets.next(); // Skip the start packet as we already have it
while (currentPacket && !ended) { while (currentPacket && !ended) {
@@ -847,13 +858,13 @@ export type WrappedCanvas = {
*/ */
export type CanvasSinkOptions = { export type CanvasSinkOptions = {
/** /**
* The width of the output canvas, defaulting to the display width of the video track. If height is not set, it * The width of the output canvas in pixels, defaulting to the display width of the video track. If height is not
* will be deduced automatically based on aspect ratio. * set, it will be deduced automatically based on aspect ratio.
*/ */
width?: number; width?: number;
/** /**
* The height of the output canvas, defaulting to the display height of the video track. If width is not set, it * The height of the output canvas in pixels, defaulting to the display height of the video track. If width is not
* will be deduced automatically based on aspect ratio. * set, it will be deduced automatically based on aspect ratio.
*/ */
height?: number; height?: number;
/** /**
+2 -2
View File
@@ -188,7 +188,7 @@ export type VideoEncodingConfig = {
*/ */
keyFrameInterval?: number; keyFrameInterval?: number;
/** /**
* The full codec string as specified in the WebCodecs API Codec Registry. This string must match the codec * The full codec string as specified in the WebCodecs Codec Registry. This string must match the codec
* specified in `codec`. When not set, a fitting codec string will be constructed automatically by the library. * specified in `codec`. When not set, a fitting codec string will be constructed automatically by the library.
*/ */
fullCodecString?: string; fullCodecString?: string;
@@ -653,7 +653,7 @@ export type AudioEncodingConfig = {
*/ */
bitrate?: number | Quality; bitrate?: number | Quality;
/** /**
* The full codec string as specified in the WebCodecs API Codec Registry. This string must match the codec * The full codec string as specified in the WebCodecs Codec Registry. This string must match the codec
* specified in `codec`. When not set, a fitting codec string will be constructed automatically by the library. * specified in `codec`. When not set, a fitting codec string will be constructed automatically by the library.
*/ */
fullCodecString?: string; fullCodecString?: string;
+1 -1
View File
@@ -58,7 +58,7 @@ export class Reader {
} }
} }
this.source.onread?.({ start, end }); this.source.onread?.(start, end);
const bytesPromise = this.source._read(start, end); const bytesPromise = this.source._read(start, end);
const loadingSegment: LoadingSegment = { start, end, promise: bytesPromise }; const loadingSegment: LoadingSegment = { start, end, promise: bytesPromise };
this.loadingSegments.push(loadingSegment); this.loadingSegments.push(loadingSegment);
+2 -2
View File
@@ -348,8 +348,8 @@ export class VideoSample {
* *
* @param dx - The x-coordinate in the destination canvas at which to place the top-left corner of the source image. * @param dx - The x-coordinate in the destination canvas at which to place the top-left corner of the source image.
* @param dy - The y-coordinate in the destination canvas at which to place the top-left corner of the source image. * @param dy - The y-coordinate in the destination canvas at which to place the top-left corner of the source image.
* @param dWidth - The width to draw the image in the destination canvas. This allows scaling of the drawn image. * @param dWidth - The width in pixels with which to draw the image in the destination canvas.
* @param dHeight - The height to draw the image in the destination canvas. This allows scaling of the drawn image. * @param dHeight - The height in pixels with which to draw the image in the destination canvas.
*/ */
draw( draw(
context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
+1 -6
View File
@@ -19,12 +19,7 @@ export abstract class Source {
} }
/** Called each time data is requested from the source. */ /** Called each time data is requested from the source. */
onread: ((range: { onread: ((start: number, end: number) => unknown) | null = null;
/** The start byte offset (inclusive). */
start: number;
/** The end byte offset (exclusive). */
end: number;
}) => unknown) | null = null;
} }
/** /**