mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 10:53:50 +02:00
Fix EBML parsers for large integers, make small API and docblock adjustments
This commit is contained in:
+8
-8
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user