diff --git a/dev/convert.html b/dev/convert.html index 3c6e148..f0bfebe 100644 --- a/dev/convert.html +++ b/dev/convert.html @@ -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 }); diff --git a/dev/player.html b/dev/player.html index 1752009..9659ac6 100644 --- a/dev/player.html +++ b/dev/player.html @@ -50,7 +50,7 @@ if (!(await videoTrack?.canDecode())) { videoTrack = null; } if (!(await audioTrack?.canDecode())) { - audioTrack = null; + audioTrack = null; } const canvas = document.querySelector('canvas'); diff --git a/src/codec.ts b/src/codec.ts index 29d22c1..5cb92db 100644 --- a/src/codec.ts +++ b/src/codec.ts @@ -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) { diff --git a/src/conversion.ts b/src/conversion.ts index de07c12..1bf4052 100644 --- a/src/conversion.ts +++ b/src/conversion.ts @@ -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); } diff --git a/src/input-track.ts b/src/input-track.ts index c135f9a..009f45f 100644 --- a/src/input-track.ts +++ b/src/input-track.ts @@ -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(); diff --git a/src/matroska/ebml.ts b/src/matroska/ebml.ts index ff9d0c9..6b95f56 100644 --- a/src/matroska/ebml.ts +++ b/src/matroska/ebml.ts @@ -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; diff --git a/src/media-sink.ts b/src/media-sink.ts index 0cc4b52..d64cb97 100644 --- a/src/media-sink.ts +++ b/src/media-sink.ts @@ -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 { + 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; /** diff --git a/src/media-source.ts b/src/media-source.ts index 41c2fb3..ded3738 100644 --- a/src/media-source.ts +++ b/src/media-source.ts @@ -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; diff --git a/src/reader.ts b/src/reader.ts index 6ac6eb6..88a8ac4 100644 --- a/src/reader.ts +++ b/src/reader.ts @@ -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); diff --git a/src/sample.ts b/src/sample.ts index 634bc18..96d456c 100644 --- a/src/sample.ts +++ b/src/sample.ts @@ -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, diff --git a/src/source.ts b/src/source.ts index bb0bb19..854532a 100644 --- a/src/source.ts +++ b/src/source.ts @@ -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; } /**