mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-24 23:05:49 +00:00
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:
@@ -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]));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user