Tune export quality for web delivery

Use practical bitrate targets across export backends and add dithering when converting full-range canvas frames to BT.709 video range.
This commit is contained in:
webadderall
2026-08-31 12:25:12 +10:00
parent d27aac7719
commit 621006ff71
4 changed files with 63 additions and 93 deletions
+7 -5
View File
@@ -153,7 +153,7 @@ describe("native static layout command builders", () => {
expect(args).toContain("-filter_complex");
expect(args).toContain(
"color=c=0x101010:s=1920x1080:r=60:d=60.000,format=nv12,setrange=limited,hwupload_cuda[bg];" +
"[0:v]scale_cuda=w=1536:h=864:format=nv12:passthrough=0,hwdownload,format=nv12,scale=in_range=auto:out_range=tv,fps=60,hwupload_cuda[fg];" +
"[0:v]scale_cuda=w=1536:h=864:format=nv12:passthrough=0,hwdownload,format=nv12,scale=in_range=auto:out_range=tv:sws_dither=a_dither,fps=60,hwupload_cuda[fg];" +
"[bg][fg]overlay_cuda=192:108:shortest=0:repeatlast=1:eof_action=repeat,trim=duration=60.000,setpts=PTS-STARTPTS[out]",
);
expect(args).toContain("h264_nvenc");
@@ -179,7 +179,7 @@ describe("native static layout command builders", () => {
expect(args).toEqual(
expect.arrayContaining([
"-vf",
"vflip,scale=in_range=full:out_range=tv",
"vflip,scale=in_range=full:out_range=tv:sws_dither=a_dither",
"-colorspace",
"bt709",
"-color_primaries",
@@ -198,7 +198,7 @@ describe("native static layout command builders", () => {
expect(args).toEqual(
expect.arrayContaining([
"-vf",
"scale_cuda=w=1536:h=864:format=nv12:passthrough=0,hwdownload,format=nv12,scale=in_range=auto:out_range=tv,fps=60,pad=w=1920:h=1080:x=192:y=108:color=0x101010",
"scale_cuda=w=1536:h=864:format=nv12:passthrough=0,hwdownload,format=nv12,scale=in_range=auto:out_range=tv:sws_dither=a_dither,fps=60,pad=w=1920:h=1080:x=192:y=108:color=0x101010",
"-map",
"0:v:0",
"-an",
@@ -214,7 +214,7 @@ describe("native static layout command builders", () => {
});
expect(args).toContain(
"scale_cuda=w=1536:h=864:format=nv12:passthrough=0,hwdownload,format=nv12,scale=in_range=auto:out_range=tv,fps=60,pad=w=1920:h=1080:x=192:y=108:color=0x101010",
"scale_cuda=w=1536:h=864:format=nv12:passthrough=0,hwdownload,format=nv12,scale=in_range=auto:out_range=tv:sws_dither=a_dither,fps=60,pad=w=1920:h=1080:x=192:y=108:color=0x101010",
);
});
@@ -266,7 +266,9 @@ describe("native static layout command builders", () => {
);
expect(filterComplex).toContain("[fgbase][mask]alphamerge[fg]");
expect(filterComplex).toContain("overlay=x=192:y=108:format=auto");
expect(filterComplex).toContain("scale=in_range=full:out_range=tv,format=yuv420p[out]");
expect(filterComplex).toContain(
"scale=in_range=full:out_range=tv:sws_dither=a_dither,format=yuv420p[out]",
);
expect(args).toContain("h264_nvenc");
expect(args).toEqual(expect.arrayContaining(["-pix_fmt", "yuv420p"]));
expect(args).toEqual(expect.arrayContaining([...FFMPEG_BT709_VIDEO_COLOR_ARGS]));
+3 -3
View File
@@ -20,9 +20,9 @@ export const FFMPEG_BT709_VIDEO_COLOR_ARGS = [
"tv",
] as const;
const FFMPEG_AUTO_TO_VIDEO_RANGE_FILTER = "scale=in_range=auto:out_range=tv";
const FFMPEG_AUTO_TO_VIDEO_RANGE_FILTER = "scale=in_range=auto:out_range=tv:sws_dither=a_dither";
const FFMPEG_AUTO_TO_FULL_RANGE_FILTER = "scale=in_range=auto:out_range=full";
const FFMPEG_FULL_TO_VIDEO_RANGE_FILTER = "scale=in_range=full:out_range=tv";
const FFMPEG_FULL_TO_VIDEO_RANGE_FILTER = "scale=in_range=full:out_range=tv:sws_dither=a_dither";
export type NativeExportEncodingMode = "fast" | "balanced" | "quality";
@@ -311,7 +311,7 @@ export function buildNativeVideoExportArgs(
"-i",
"pipe:0",
"-vf",
"vflip,scale=in_range=full:out_range=tv",
`vflip,${FFMPEG_FULL_TO_VIDEO_RANGE_FILTER}`,
"-an",
"-c:v",
encoder,
+14 -12
View File
@@ -2,8 +2,10 @@ import { describe, expect, it } from "vitest";
import { getMp4ExportBitrate, getSourceQualityBitrate } from "./exportBitrate";
describe("export bitrate policy", () => {
it("keeps source-quality exports at a fuller screen-recording bitrate", () => {
expect(getSourceQualityBitrate(1920, 1080)).toBe(30_000_000);
it("uses the web-delivery source-quality caps", () => {
expect(getSourceQualityBitrate(1280, 720)).toBe(8_000_000);
expect(getSourceQualityBitrate(1920, 1080)).toBe(12_000_000);
expect(getSourceQualityBitrate(3840, 2160)).toBe(45_000_000);
expect(
getMp4ExportBitrate({
width: 1920,
@@ -12,7 +14,7 @@ describe("export bitrate policy", () => {
quality: "source",
encodingMode: "quality",
}),
).toBe(30_000_000);
).toBe(12_000_000);
expect(
getMp4ExportBitrate({
width: 1920,
@@ -21,7 +23,7 @@ describe("export bitrate policy", () => {
quality: "source",
encodingMode: "balanced",
}),
).toBe(22_500_000);
).toBe(9_600_000);
});
it("raises high-resolution 60fps source-quality exports above the 30fps budget", () => {
@@ -41,9 +43,9 @@ describe("export bitrate policy", () => {
frameRate: 60,
});
expect(thirtyFpsBitrate).toBe(50_000_000);
expect(thirtyFpsBitrate).toBe(20_555_556);
expect(sixtyFpsBitrate).toBeGreaterThan(thirtyFpsBitrate);
expect(sixtyFpsBitrate).toBe(70_710_678);
expect(sixtyFpsBitrate).toBe(29_069_946);
});
it("keeps modern native static-layout source exports high enough for screen text", () => {
@@ -56,7 +58,7 @@ describe("export bitrate policy", () => {
encodingMode: "balanced",
useModernNativeStaticLayout: true,
}),
).toBe(22_500_000);
).toBe(9_600_000);
expect(
getMp4ExportBitrate({
width: 1920,
@@ -66,7 +68,7 @@ describe("export bitrate policy", () => {
encodingMode: "quality",
useModernNativeStaticLayout: true,
}),
).toBe(30_000_000);
).toBe(12_000_000);
});
it("scales modern native static-layout source exports at 60fps", () => {
@@ -87,9 +89,9 @@ describe("export bitrate policy", () => {
frameRate: 60,
});
expect(thirtyFpsBitrate).toBe(30_000_000);
expect(thirtyFpsBitrate).toBe(12_000_000);
expect(sixtyFpsBitrate).toBeGreaterThan(thirtyFpsBitrate);
expect(sixtyFpsBitrate).toBe(42_426_407);
expect(sixtyFpsBitrate).toBe(16_970_563);
});
it("does not raise fast exports when the requested bitrate is already lower than the cap", () => {
@@ -102,7 +104,7 @@ describe("export bitrate policy", () => {
encodingMode: "fast",
useModernNativeStaticLayout: true,
}),
).toBe(3_000_000);
).toBe(6_000_000);
});
it("scales the modern native cap with output pixel rate", () => {
@@ -115,6 +117,6 @@ describe("export bitrate policy", () => {
encodingMode: "quality",
useModernNativeStaticLayout: true,
}),
).toBe(72_000_000);
).toBe(45_000_000);
});
});
+39 -73
View File
@@ -1,30 +1,49 @@
import type { ExportEncodingMode, ExportMp4FrameRate, ExportQuality } from "./types";
const MIN_MP4_BITRATE = 2_000_000;
const REFERENCE_PIXEL_RATE = 1920 * 1080 * 30;
const REFERENCE_FRAME_RATE = 30;
const HD_PIXELS = 1280 * 720;
const FULL_HD_PIXELS = 1920 * 1080;
const UHD_PIXELS = 3840 * 2160;
function interpolateBitrate(
totalPixels: number,
startPixels: number,
endPixels: number,
startBitrate: number,
endBitrate: number,
): number {
const progress = Math.max(
0,
Math.min(1, (totalPixels - startPixels) / (endPixels - startPixels)),
);
return Math.round(startBitrate + (endBitrate - startBitrate) * progress);
}
export function getEncodingModeBitrateMultiplier(encodingMode: ExportEncodingMode): number {
switch (encodingMode) {
case "fast":
return 0.1;
return 0.5;
case "quality":
return 1;
case "balanced":
default:
return 0.75;
return 0.8;
}
}
export function getSourceQualityBitrate(width: number, height: number): number {
const totalPixels = width * height;
if (totalPixels > 2560 * 1440) {
return 80_000_000;
if (totalPixels <= HD_PIXELS) {
return 8_000_000;
}
if (totalPixels > 1920 * 1080) {
return 50_000_000;
if (totalPixels <= FULL_HD_PIXELS) {
return 12_000_000;
}
return 30_000_000;
if (totalPixels >= UHD_PIXELS) {
return 45_000_000;
}
return interpolateBitrate(totalPixels, FULL_HD_PIXELS, UHD_PIXELS, 12_000_000, 45_000_000);
}
function getBaseMp4ExportBitrate(width: number, height: number, quality: ExportQuality): number {
@@ -33,13 +52,16 @@ function getBaseMp4ExportBitrate(width: number, height: number, quality: ExportQ
}
const totalPixels = width * height;
if (totalPixels <= 1280 * 720) {
return 10_000_000;
if (totalPixels <= HD_PIXELS) {
return 5_000_000;
}
if (totalPixels <= 1920 * 1080) {
return 20_000_000;
if (totalPixels <= FULL_HD_PIXELS) {
return 8_000_000;
}
return 30_000_000;
if (totalPixels >= UHD_PIXELS) {
return 35_000_000;
}
return interpolateBitrate(totalPixels, FULL_HD_PIXELS, UHD_PIXELS, 8_000_000, 35_000_000);
}
function getFrameRateBitrateMultiplier(frameRate: ExportMp4FrameRate): number {
@@ -50,42 +72,6 @@ function getFrameRateBitrateMultiplier(frameRate: ExportMp4FrameRate): number {
return Math.sqrt(Math.max(1, frameRate / REFERENCE_FRAME_RATE));
}
function getModernNativeStaticLayoutBitrateCap(
width: number,
height: number,
frameRate: ExportMp4FrameRate,
quality: ExportQuality,
): number {
const referenceCap =
quality === "source"
? 36_000_000
: quality === "high"
? 28_000_000
: quality === "good"
? 20_000_000
: 14_000_000;
const pixelRateScale = Math.max((width * height * frameRate) / REFERENCE_PIXEL_RATE, 0.1);
return Math.round(referenceCap * Math.sqrt(pixelRateScale));
}
function getModernNativeStaticLayoutBitrateFloor(
width: number,
height: number,
frameRate: ExportMp4FrameRate,
quality: ExportQuality,
): number {
const referenceFloor =
quality === "source"
? 22_000_000
: quality === "high"
? 16_000_000
: quality === "good"
? 12_000_000
: 8_000_000;
const pixelRateScale = Math.max((width * height * frameRate) / REFERENCE_PIXEL_RATE, 0.1);
return Math.round(referenceFloor * Math.sqrt(pixelRateScale));
}
export function getMp4ExportBitrate(options: {
width: number;
height: number;
@@ -99,29 +85,9 @@ export function getMp4ExportBitrate(options: {
getFrameRateBitrateMultiplier(options.frameRate) *
getEncodingModeBitrateMultiplier(options.encodingMode),
);
const nativeStaticLayoutBitrate =
options.useModernNativeStaticLayout && options.encodingMode !== "fast"
? Math.max(
requestedBitrate,
getModernNativeStaticLayoutBitrateFloor(
options.width,
options.height,
options.frameRate,
options.quality,
),
)
: requestedBitrate;
const cappedBitrate = options.useModernNativeStaticLayout
? Math.min(
nativeStaticLayoutBitrate,
getModernNativeStaticLayoutBitrateCap(
options.width,
options.height,
options.frameRate,
options.quality,
),
)
: requestedBitrate;
return Math.max(MIN_MP4_BITRATE, cappedBitrate);
// Keep every backend on the same delivery bitrate policy. Native static-layout
// exports previously applied a second set of floors and caps that could more
// than double the requested web-delivery target.
return Math.max(MIN_MP4_BITRATE, requestedBitrate);
}