From 3db4aa8b914c8439dfc54c1a97f5dd2a3f8f79cd Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Tue, 11 Nov 2025 11:05:15 +0100 Subject: [PATCH] Fixed doubly stacked rotation correction when converting rotated video with a custom process function (fixes #218) --- dev/convert.html | 15 +++++++++------ src/conversion.ts | 8 ++++++-- src/sample.ts | 5 ++++- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/dev/convert.html b/dev/convert.html index bc1f9d1..79b7ae3 100644 --- a/dev/convert.html +++ b/dev/convert.html @@ -48,6 +48,7 @@ target }); + let ctx = null; const conversion = await Mediabunny.Conversion.init({ input: new Mediabunny.Input({ formats: Mediabunny.ALL_FORMATS, @@ -56,6 +57,7 @@ output, audio: (_, n) => ({ discard: n > 1, + //codec: 'opus', /* process: (sample) => { return sample; @@ -97,7 +99,7 @@ }, */ video: () => ({ - /* + //discard: true, process: (sample) => { if (!ctx) { // Create a canvas for image compositing @@ -108,13 +110,14 @@ ctx = canvas.getContext('2d'); } + console.log(ctx.canvas.width, ctx.canvas.height); + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); - sample.draw(ctx, 0, 0); - ctx.drawImage(watermark, 32, 32); + sample.drawWithFit(ctx, { fit: 'fill' }); + //ctx.drawImage(watermark, 32, 32); return ctx.canvas; }, - */ //width: 300, //alpha: 'keep', //width: 320, @@ -170,8 +173,8 @@ } }, trim: { - start: 0, - end: 10 + //start: 0, + //end: 10 }, }); console.log(conversion); diff --git a/src/conversion.ts b/src/conversion.ts index abf820a..0cabe1a 100644 --- a/src/conversion.ts +++ b/src/conversion.ts @@ -182,7 +182,8 @@ export type ConversionVideoOptions = { * corrections. * * Must return a {@link VideoSample} or a `CanvasImageSource`, an array of them, or `null` for dropping the frame. - * When non-timestamped data is returned, the timestamp and duration from the source sample will be used. + * When non-timestamped data is returned, the timestamp and duration from the source sample will be used. Rotation + * metadata of the returned sample will be ignored. * * This function can also be used to manually resize frames. When doing so, you should signal the post-process * dimensions using the `processedWidth` and `processedHeight` fields, which enables the encoder to better know what @@ -873,7 +874,10 @@ export class Conversion { || trackOptions.process !== undefined; let needsRerender = width !== originalWidth || height !== originalHeight - || (totalRotation !== 0 && !outputSupportsRotation) + // TODO This is suboptimal: Forcing a rerender when both rotation and process are set is not + // performance-optimal, but right now there's no other way because we can't change the track rotation + // metadata after the output has already started. Should be possible with API changes in v2, though! + || (totalRotation !== 0 && (!outputSupportsRotation || trackOptions.process !== undefined)) || !!crop; const alpha = trackOptions.alpha ?? 'discard'; diff --git a/src/sample.ts b/src/sample.ts index 805aebe..35d7800 100644 --- a/src/sample.ts +++ b/src/sample.ts @@ -552,7 +552,6 @@ export class VideoSample implements Disposable { dHeight, ); - // Restore the previous transformation state context.restore(); } @@ -644,6 +643,8 @@ export class VideoSample implements Disposable { dy = (canvasHeight - newHeight) / 2; } + context.save(); + const aspectRatioChange = rotation % 180 === 0 ? 1 : newWidth / newHeight; context.translate(canvasWidth / 2, canvasHeight / 2); context.rotate(rotation * Math.PI / 180); @@ -655,6 +656,8 @@ export class VideoSample implements Disposable { // Important that we don't use .draw() here since that would take rotation into account, but we wanna handle it // ourselves here context.drawImage(this.toCanvasImageSource(), sx, sy, sWidth, sHeight, dx, dy, newWidth, newHeight); + + context.restore(); } /** @internal */