Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions src/components/video-editor/exportDimensions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import {
calculateMp4ExportDimensions,
calculateMp4SourceDimensions,
capExportCanvasDimensions,
shouldDebounceMp4SupportProbe,
} from "./exportDimensions";

Expand Down Expand Up @@ -57,6 +58,98 @@ describe("calculateMp4SourceDimensions", () => {
height: 1080,
});
});

it("caps a 5K full-screen capture at 4K UHD for native exports", () => {
expect(calculateMp4SourceDimensions(5120, 2880, "native")).toEqual({
width: 3840,
height: 2160,
});
});

it("caps the portrait canvas from a 5K capture at 4K UHD", () => {
expect(calculateMp4SourceDimensions(5120, 2880, "9:16")).toEqual({
width: 2160,
height: 3840,
});
});

it("leaves a native crop of a 5K capture alone once it fits the budget", () => {
expect(
calculateMp4SourceDimensions(5120, 2880, "native", {
width: 0.5,
height: 1,
}),
).toEqual({
width: 2560,
height: 2880,
});
});

it("scales a native crop of a 5K capture that still exceeds the budget", () => {
expect(
calculateMp4SourceDimensions(5120, 2880, "native", {
width: 0.8,
height: 1,
}),
).toEqual({
width: 3434,
height: 2414,
});
});

it("leaves a 4K UHD source untouched", () => {
expect(calculateMp4SourceDimensions(3840, 2160, "native")).toEqual({
width: 3840,
height: 2160,
});
expect(calculateMp4SourceDimensions(3840, 2160, "9:16")).toEqual({
width: 2160,
height: 3840,
});
});
});

describe("capExportCanvasDimensions", () => {
it("returns dimensions inside the ceiling unchanged", () => {
expect(capExportCanvasDimensions(1920, 1080)).toEqual({ width: 1920, height: 1080 });
expect(capExportCanvasDimensions(2160, 3840)).toEqual({ width: 2160, height: 3840 });
});

it("leaves an ultrawide source inside the budget at its own size", () => {
expect(capExportCanvasDimensions(5120, 1440)).toEqual({ width: 5120, height: 1440 });
});

it("scales an ultrawide source above the budget by area, keeping its shape", () => {
expect(capExportCanvasDimensions(5120, 2160)).toEqual({ width: 4434, height: 1870 });
});

it("scales a 5K source down to the 4K UHD pixel budget", () => {
expect(capExportCanvasDimensions(5120, 2880)).toEqual({ width: 3840, height: 2160 });
expect(capExportCanvasDimensions(2880, 5120)).toEqual({ width: 2160, height: 3840 });
});

it("never exceeds the budget after rounding to even dimensions", () => {
for (const [width, height] of [
[5120, 2880],
[6016, 3384],
[5120, 2160],
[7680, 4320],
[2880, 5120],
[3441, 1441],
]) {
const capped = capExportCanvasDimensions(width, height);
expect(capped.width * capped.height).toBeLessThanOrEqual(3840 * 2160);
expect(capped.width % 2).toBe(0);
expect(capped.height % 2).toBe(0);
}
});

it("honours a custom budget", () => {
expect(capExportCanvasDimensions(2880, 5120, 2560 * 1440)).toEqual({
width: 1440,
height: 2560,
});
});
});

describe("calculateMp4ExportDimensions", () => {
Expand All @@ -81,6 +174,29 @@ describe("calculateMp4ExportDimensions", () => {
});
});

it("scales every tier from the capped canvas on a 5K portrait export", () => {
const sourceDimensions = calculateMp4SourceDimensions(5120, 2880, "9:16");

expect(
calculateMp4ExportDimensions(sourceDimensions.width, sourceDimensions.height, "source"),
).toEqual({
width: 2160,
height: 3840,
});
expect(
calculateMp4ExportDimensions(sourceDimensions.width, sourceDimensions.height, "good"),
).toEqual({
width: 1620,
height: 2880,
});
expect(
calculateMp4ExportDimensions(sourceDimensions.width, sourceDimensions.height, "medium"),
).toEqual({
width: 1296,
height: 2304,
});
});

it("scales portrait output dimensions from the aspect target", () => {
const sourceDimensions = calculateMp4SourceDimensions(1920, 1080, "9:16");

Expand Down
36 changes: 34 additions & 2 deletions src/components/video-editor/exportDimensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,37 @@ function normalizeEvenDimension(value: number): number {
return Math.max(2, Math.floor(value / 2) * 2);
}

/**
* Pixel budget for the export canvas that the quality tiers scale from.
*
* "Original" follows the source, and a full-screen capture of a 5K panel at backing
* scale 2 is 5120 x 2880. For a 9:16 export that produced a 2880 x 5120 canvas:
* 14.7 megapixels read back per frame and taller than H.264 hardware encoders accept,
* so the export stalled or failed. Capping the canvas at the area of 4K UHD keeps the
* largest tier encodable and every lower tier scales from the capped canvas, while a
* wide or tall source under that area (an ultrawide, for example) keeps its size.
* Sources at or below 4K UHD are unaffected.
*/
export const MAX_EXPORT_CANVAS_PIXELS = 3840 * 2160;

export function capExportCanvasDimensions(
width: number,
height: number,
maxPixels: number = MAX_EXPORT_CANVAS_PIXELS,
): { width: number; height: number } {
const pixels = width * height;

if (!Number.isFinite(pixels) || pixels <= 0 || pixels <= maxPixels) {
return { width: normalizeEvenDimension(width), height: normalizeEvenDimension(height) };
}

const scale = Math.sqrt(maxPixels / pixels);
return {
width: normalizeEvenDimension(width * scale),
height: normalizeEvenDimension(height * scale),
};
}

function fitAspectRatioWithinBounds(
maxWidth: number,
maxHeight: number,
Expand Down Expand Up @@ -73,15 +104,16 @@ export function calculateMp4SourceDimensions(
const aspectRatioValue = getAspectRatioValue(aspectRatio, sourceAspectRatio);

if (aspectRatio === "native") {
return { width: safeSourceWidth, height: safeSourceHeight };
return capExportCanvasDimensions(safeSourceWidth, safeSourceHeight);
}

const longSide = Math.max(safeSourceWidth, safeSourceHeight);
const shortSide = Math.min(safeSourceWidth, safeSourceHeight);
const maxWidth = aspectRatioValue >= 1 ? longSide : shortSide;
const maxHeight = aspectRatioValue >= 1 ? shortSide : longSide;

return fitAspectRatioWithinBounds(maxWidth, maxHeight, aspectRatioValue);
const fitted = fitAspectRatioWithinBounds(maxWidth, maxHeight, aspectRatioValue);
return capExportCanvasDimensions(fitted.width, fitted.height);
}

export function calculateMp4ExportDimensions(
Expand Down