diff --git a/dev/demux.html b/dev/demux.html index f26f80a..b493880 100644 --- a/dev/demux.html +++ b/dev/demux.html @@ -15,14 +15,21 @@ formats: Mediabunny.ALL_FORMATS, source }); + + const canvas = document.createElement('canvas'); + canvas.width = 1920; + canvas.height = 1080; + canvas.style.background = 'ghostwhite'; + const ctx = canvas.getContext('2d'); + document.body.append(canvas); const videoTrack = await input.getPrimaryVideoTrack(); + const sink = new Mediabunny.VideoSampleSink(videoTrack); - const sink = new Mediabunny.EncodedPacketSink(videoTrack); - for await (const packet of sink.packets()) { - if (packet.timestamp >= 2) break; - console.log(packet); - } + const sample = await sink.getSample(3); + console.log(sample); + + sample.draw(ctx, 1500, 500, 50, 50, 0, 0); /* let timestamps = []; diff --git a/docs/guide/packets-and-samples.md b/docs/guide/packets-and-samples.md index 2371bb4..63c5f80 100644 --- a/docs/guide/packets-and-samples.md +++ b/docs/guide/packets-and-samples.md @@ -309,17 +309,29 @@ The `VideoFrame` returned by this method **must** be closed separately from the --- -It's also common to draw video samples to a `` element or an `OffscreenCanvas`. For this, you can use the following method: +It's also common to draw video samples to a `` element or an `OffscreenCanvas`. For this, you can use the following methods: ```ts draw( context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, dx: number, dy: number, - dWidth?: number, - dHeight?: number, + dWidth?: number, // defaults to displayWidth + dHeight?: number, // defaults to displayHeight +): void; + +draw( + context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, + sx: number, + sy: number, + sWidth: number, + sHeight: number, + dx: number, + dy: number, + dWidth?: number, // defaults to sWidth + dHeight?: number, // defaults to sHeight ): void; ``` -This method is similar to [drawImage](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage) and paints the video frame at the given position with the given dimensions. If the dimensions aren't specified, they will default to the display dimensions. This method will automatically draw the frame with the correct rotation based on its `rotation` property. +These methods behave like [drawImage](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage) and paint the video frame at the given position with the given dimensions. This method will automatically draw the frame with the correct rotation based on its `rotation` property. If you want to draw the raw underlying image to a canvas directly (without respecting the rotation metadata), then you can use the following method: ```ts diff --git a/package-lock.json b/package-lock.json index f76592a..fcf4bd7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mediabunny", - "version": "1.0.6", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mediabunny", - "version": "1.0.6", + "version": "1.1.0", "license": "MPL-2.0", "dependencies": { "@types/dom-mediacapture-transform": "^0.1.11", diff --git a/package.json b/package.json index 7626c6f..9165bbf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mediabunny", "author": "Vanilagy", - "version": "1.0.6", + "version": "1.1.0", "description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.", "type": "module", "main": "./dist/bundles/mediabunny.cjs", diff --git a/src/media-sink.ts b/src/media-sink.ts index 816a3f0..66876b1 100644 --- a/src/media-sink.ts +++ b/src/media-sink.ts @@ -737,6 +737,7 @@ class VideoDecoderWrapper extends DecoderWrapper { // Round the timestamps to the time resolution sample.setTimestamp(Math.round(sample.timestamp * this.timeResolution) / this.timeResolution); sample.setDuration(Math.round(sample.duration * this.timeResolution) / this.timeResolution); + sample.setRotation(this.rotation); this.onSample(sample); } diff --git a/src/sample.ts b/src/sample.ts index a1255a0..7f36943 100644 --- a/src/sample.ts +++ b/src/sample.ts @@ -366,9 +366,79 @@ export class VideoSample { context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, dx: number, dy: number, - dWidth: number = this.displayWidth, - dHeight: number = this.displayHeight, + dWidth?: number, + dHeight?: number, + ): void; + /** + * Draws the video sample to a 2D canvas context. Rotation metadata will be taken into account. + * + * @param sx - The x-coordinate of the top left corner of the sub-rectangle of the source image to draw into the + * destination context. + * @param sy - The y-coordinate of the top left corner of the sub-rectangle of the source image to draw into the + * destination context. + * @param sWidth - The width of the sub-rectangle of the source image to draw into the destination context. + * @param sHeight - The height of the sub-rectangle of the source image to draw into the destination context. + * @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 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, + sx: number, + sy: number, + sWidth: number, + sHeight: number, + dx: number, + dy: number, + dWidth?: number, + dHeight?: number, + ): void; + draw( + context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, + arg1: number, + arg2: number, + arg3?: number, + arg4?: number, + arg5?: number, + arg6?: number, + arg7?: number, + arg8?: number, ) { + let sx = 0; + let sy = 0; + let sWidth = this.displayWidth; + let sHeight = this.displayHeight; + let dx = 0; + let dy = 0; + let dWidth = this.displayWidth; + let dHeight = this.displayHeight; + + if (arg5 !== undefined) { + sx = arg1!; + sy = arg2!; + sWidth = arg3!; + sHeight = arg4!; + dx = arg5; + dy = arg6!; + + if (arg7 !== undefined) { + dWidth = arg7; + dHeight = arg8!; + } else { + dWidth = sWidth; + dHeight = sHeight; + } + } else { + dx = arg1; + dy = arg2; + + if (arg3 !== undefined) { + dWidth = arg3; + dHeight = arg4!; + } + } + if (!( (typeof CanvasRenderingContext2D !== 'undefined' && context instanceof CanvasRenderingContext2D) || ( @@ -378,6 +448,18 @@ export class VideoSample { )) { throw new TypeError('context must be a CanvasRenderingContext2D or OffscreenCanvasRenderingContext2D.'); } + if (!Number.isFinite(sx)) { + throw new TypeError('sx must be a number.'); + } + if (!Number.isFinite(sy)) { + throw new TypeError('sy must be a number.'); + } + if (!Number.isFinite(sWidth) || sWidth < 0) { + throw new TypeError('sWidth must be a non-negative number.'); + } + if (!Number.isFinite(sHeight) || sHeight < 0) { + throw new TypeError('sHeight must be a non-negative number.'); + } if (!Number.isFinite(dx)) { throw new TypeError('dx must be a number.'); } @@ -395,6 +477,29 @@ export class VideoSample { throw new Error('VideoSample is closed.'); } + // The provided sx,sy,sWidth,sHeight refer to the final rotated image, but that's not actually how the image is + // stored. Therefore, we must map these back onto the original, pre-rotation image. + if (this.rotation === 90) { + [sx, sy, sWidth, sHeight] = [ + sy, + this.codedHeight - sx - sWidth, + sHeight, + sWidth, + ]; + } else if (this.rotation === 180) { + [sx, sy] = [ + this.codedWidth - sx - sWidth, + this.codedHeight - sy - sHeight, + ]; + } else if (this.rotation === 270) { + [sx, sy, sWidth, sHeight] = [ + this.codedWidth - sy - sHeight, + sx, + sHeight, + sWidth, + ]; + } + const source = this.toCanvasImageSource(); context.save(); @@ -412,6 +517,10 @@ export class VideoSample { context.drawImage( source, + sx, + sy, + sWidth, + sHeight, -dWidth / 2, -dHeight / 2, dWidth,