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 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
}), {
chunked: true,
@@ -41,8 +41,8 @@
target
}),
audio: {
//discard: true
forceReencode: true,
discard: true
//forceReencode: true,
},
/*
video: {
@@ -73,16 +73,16 @@
//rotate: 90
//width: 720 ?? 2160,
//height: 1280 ?? 3840,
fit: 'contain',
rotate: 90,
width: 512,
height: 512,
//fit: 'contain',
//rotate: 90,
//width: 512,
//height: 512,
//width: 200,
//height: 100,
},
trim: {
start: 0,
//end: 60
end: 30
},
computeProgress: true
});
+1 -1
View File
@@ -50,7 +50,7 @@ if (!(await videoTrack?.canDecode())) {
videoTrack = null;
}
if (!(await audioTrack?.canDecode())) {
audioTrack = null;
audioTrack = null;
}
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))) {
throw new TypeError(
'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) {
@@ -1444,7 +1444,7 @@ export const validateAudioChunkMetadata = (metadata: EncodedAudioChunkMetadata |
if (!VALID_AUDIO_CODEC_STRING_PREFIXES.some(prefix => metadata.decoderConfig!.codec.startsWith(prefix))) {
throw new TypeError(
'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) {
+12 -6
View File
@@ -52,13 +52,13 @@ export type ConversionOptions = {
/** The desired bitrate of the output video. */
bitrate?: VideoEncodingConfig['bitrate'];
/**
* The desired width of the output video, defaulting to the video's natural display width. If height is not set,
* it will be deduced automatically based on aspect ratio.
* The desired width of the output video in pixels, defaulting to the video's natural display width. If height
* is not set, it will be deduced automatically based on aspect ratio.
*/
width?: number;
/**
* The desired height of the output video, defaulting to the video's natural display height. If width is not
* set, it will be deduced automatically based on aspect ratio.
* The desired height of the output video in pixels, defaulting to the video's natural display height. If width
* is not set, it will be deduced automatically based on aspect ratio.
*/
height?: number;
/**
@@ -492,8 +492,11 @@ export class Conversion {
const sink = new EncodedPacketSink(track);
const decoderConfig = await track.getDecoderConfig();
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)) {
await this._synchronizer.wait(packet.timestamp);
}
@@ -658,8 +661,11 @@ export class Conversion {
const sink = new EncodedPacketSink(track);
const decoderConfig = await track.getDecoderConfig();
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)) {
await this._synchronizer.wait(packet.timestamp);
}
+5 -5
View File
@@ -158,12 +158,12 @@ export class InputVideoTrack extends InputTrack {
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() {
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() {
return this._backing.getCodedHeight();
}
@@ -173,13 +173,13 @@ export class InputVideoTrack extends InputTrack {
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() {
const rotation = this._backing.getRotation();
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() {
const rotation = this._backing.getRotation();
return rotation % 180 === 0 ? this._backing.getCodedHeight() : this._backing.getCodedWidth();
@@ -190,7 +190,7 @@ export class InputVideoTrack extends InputTrack {
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() {
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 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 mask = 0x80;
let mask = 1 << 7;
while ((firstByte & mask) === 0 && width < MAX_VAR_INT_SIZE) {
width++;
mask >>= 1;
}
// Read all bytes
const { view: fullView, offset: fullOffset } = this.reader.getViewAndOffset(this.pos, this.pos + width);
// First byte's value needs the marker bit cleared
@@ -409,7 +408,8 @@ export class EBMLReader {
// Read remaining bytes
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;
@@ -426,7 +426,8 @@ export class EBMLReader {
// Read bytes from most significant to least significant
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;
+28 -17
View File
@@ -129,13 +129,24 @@ export class EncodedPacketSink {
* 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 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(
startPacket?: EncodedPacket,
endTimestamp = Infinity,
options?: PacketRetrievalOptions,
endPacket?: EncodedPacket,
options: PacketRetrievalOptions = {},
): 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[] = [];
let { promise: queueNotEmpty, resolve: onQueueNotEmpty } = promiseWithResolvers();
@@ -157,7 +168,7 @@ export class EncodedPacketSink {
let packet = startPacket ?? await this.getFirstPacket(options);
while (packet && !terminated) {
if (packet.timestamp >= endTimestamp) {
if (endPacket && packet.sequenceNumber >= endPacket?.sequenceNumber) {
break;
}
@@ -332,25 +343,25 @@ export abstract class BaseMediaSampleSink<
let currentPacket: EncodedPacket | null = keyPacket;
let packetsEndTimestamp = Infinity;
let endPacket: EncodedPacket | undefined = undefined;
if (endTimestamp < Infinity) {
// 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
// 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.
const endPacket = await packetSink.getPacket(endTimestamp);
const endKeyPacket = !endPacket
const packet = await packetSink.getPacket(endTimestamp);
const keyPacket = !packet
? null
: endPacket.type === 'key' && endPacket.timestamp === endTimestamp
? endPacket
: await packetSink.getNextKeyPacket(endPacket);
: packet.type === 'key' && packet.timestamp === endTimestamp
? packet
: await packetSink.getNextKeyPacket(packet);
if (endKeyPacket) {
packetsEndTimestamp = endKeyPacket.timestamp;
if (keyPacket) {
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
while (currentPacket && !ended) {
@@ -847,13 +858,13 @@ export type WrappedCanvas = {
*/
export type CanvasSinkOptions = {
/**
* The width of the output canvas, defaulting to the display width of the video track. If height is not set, it
* will be deduced automatically based on aspect ratio.
* The width of the output canvas in pixels, defaulting to the display width of the video track. If height is not
* set, it will be deduced automatically based on aspect ratio.
*/
width?: number;
/**
* The height of the output canvas, defaulting to the display height of the video track. If width is not set, it
* will be deduced automatically based on aspect ratio.
* The height of the output canvas in pixels, defaulting to the display height of the video track. If width is not
* set, it will be deduced automatically based on aspect ratio.
*/
height?: number;
/**
+2 -2
View File
@@ -188,7 +188,7 @@ export type VideoEncodingConfig = {
*/
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.
*/
fullCodecString?: string;
@@ -653,7 +653,7 @@ export type AudioEncodingConfig = {
*/
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.
*/
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 loadingSegment: LoadingSegment = { start, end, promise: bytesPromise };
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 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 dHeight - The height 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 in pixels with which to draw the image in the destination canvas.
*/
draw(
context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
+1 -6
View File
@@ -19,12 +19,7 @@ export abstract class Source {
}
/** Called each time data is requested from the source. */
onread: ((range: {
/** The start byte offset (inclusive). */
start: number;
/** The end byte offset (exclusive). */
end: number;
}) => unknown) | null = null;
onread: ((start: number, end: number) => unknown) | null = null;
}
/**