fix(export): support native background blur

This commit is contained in:
wiiiii123
2026-05-06 20:53:44 +07:00
parent 007739d865
commit eb1ea68f7e
13 changed files with 196 additions and 88 deletions
+1
View File
@@ -318,6 +318,7 @@ interface Window {
offsetY: number;
backgroundColor: string;
backgroundImagePath?: string | null;
backgroundBlurPx?: number;
borderRadius?: number;
shadowIntensity?: number;
webcamInputPath?: string | null;
+29 -3
View File
@@ -49,6 +49,7 @@ vi.mock("node:child_process", () => ({
import { app } from "electron";
import {
buildExperimentalNvidiaCudaStaticLayoutArgs,
buildExperimentalWindowsGpuStaticLayoutArgs,
buildNativeStaticLayoutTimelineSegments,
buildNativeVideoAudioMuxArgs,
getExperimentalNvidiaCudaExportSkipReason,
@@ -348,9 +349,7 @@ describe("resolveExperimentalNvidiaCudaExportScriptPath", () => {
throw new Error(`missing ${candidate}`);
});
expect(await resolveExperimentalNvidiaCudaExportScriptPath()).toBe(
unpackedScriptPath,
);
expect(await resolveExperimentalNvidiaCudaExportScriptPath()).toBe(unpackedScriptPath);
} finally {
if (originalEnv === undefined) {
delete process.env[envName];
@@ -416,6 +415,19 @@ describe("buildExperimentalNvidiaCudaStaticLayoutArgs", () => {
expect(args).toEqual(expect.arrayContaining(["--timeline-map", "timeline-map.csv"]));
});
it("passes background blur to the CUDA wrapper", () => {
const args = buildExperimentalNvidiaCudaStaticLayoutArgs(
createNvidiaCudaSkipOptions({
backgroundImagePath: "wallpaper.jpg",
backgroundBlurPx: 36,
}),
"output.mp4",
"work",
);
expect(args).toEqual(expect.arrayContaining(["--background-blur", "36"]));
});
it("passes webcam source-time controls to the CUDA wrapper", () => {
const args = buildExperimentalNvidiaCudaStaticLayoutArgs(
createNvidiaCudaSkipOptions({
@@ -470,6 +482,20 @@ describe("buildExperimentalNvidiaCudaStaticLayoutArgs", () => {
});
});
describe("buildExperimentalWindowsGpuStaticLayoutArgs", () => {
it("passes background blur to the D3D11 compositor", () => {
const args = buildExperimentalWindowsGpuStaticLayoutArgs(
createNvidiaCudaSkipOptions({
backgroundImagePath: "wallpaper.jpg",
backgroundBlurPx: 36,
}),
"output.mp4",
);
expect(args).toEqual(expect.arrayContaining(["--background-blur", "36"]));
});
});
describe("buildNativeStaticLayoutTimelineSegments", () => {
it("derives contiguous output timeline ranges from edited-track source segments", () => {
expect(
+11 -1
View File
@@ -99,6 +99,7 @@ export interface NativeStaticLayoutExportOptions {
offsetY: number;
backgroundColor: string;
backgroundImagePath?: string | null;
backgroundBlurPx?: number;
borderRadius?: number;
shadowIntensity?: number;
webcamInputPath?: string | null;
@@ -1699,11 +1700,12 @@ function getNvidiaCudaBitrateMbps(options: NativeStaticLayoutExportOptions) {
return Math.max(1, Math.round(options.bitrate / 1_000_000));
}
function buildExperimentalWindowsGpuStaticLayoutArgs(
export function buildExperimentalWindowsGpuStaticLayoutArgs(
options: NativeStaticLayoutExportOptions,
outputPath: string,
) {
const shadowPixels = Math.round(clampUnit(options.shadowIntensity ?? 0) * 64);
const backgroundBlurPx = Math.max(0, options.backgroundBlurPx ?? 0);
const pixelCount = options.width * options.height;
const surfacePoolSize = pixelCount <= 1920 * 1080 ? 12 : 8;
const args = [
@@ -1743,6 +1745,9 @@ function buildExperimentalWindowsGpuStaticLayoutArgs(
if (options.backgroundImagePath) {
args.push("--background-image", options.backgroundImagePath);
}
if (backgroundBlurPx > 0) {
args.push("--background-blur", formatCliNumber(backgroundBlurPx));
}
if (options.webcamInputPath) {
const webcamShadowPixels = Math.round(clampUnit(options.webcamShadowIntensity ?? 0) * 64);
args.push(
@@ -2085,6 +2090,7 @@ export function buildExperimentalNvidiaCudaStaticLayoutArgs(
workDir: string,
) {
const background = convertHexColorToNv12(options.backgroundColor);
const backgroundBlurPx = Math.max(0, options.backgroundBlurPx ?? 0);
const shadowIntensityPct = Math.round(clampUnit(options.shadowIntensity ?? 0) * 100);
const shadowOffsetY =
shadowIntensityPct > 0 ? Math.max(1, Math.round(options.height * 0.012)) : 0;
@@ -2128,6 +2134,9 @@ export function buildExperimentalNvidiaCudaStaticLayoutArgs(
if (options.backgroundImagePath) {
args.push("--background-image", options.backgroundImagePath);
}
if (backgroundBlurPx > 0) {
args.push("--background-blur", formatCliNumber(backgroundBlurPx));
}
if (shadowOffsetY > 0 && shadowIntensityPct > 0) {
args.push(
"--shadow-offset-y",
@@ -2572,6 +2581,7 @@ export async function exportNativeStaticLayoutVideo(
offsetY: options.offsetY,
backgroundColor: options.backgroundColor,
backgroundImagePath: options.backgroundImagePath,
backgroundBlurPx: options.backgroundBlurPx,
borderRadius: options.borderRadius,
shadowIntensity: options.shadowIntensity,
durationSec: options.durationSec,
+13
View File
@@ -205,6 +205,19 @@ describe("native static layout command builders", () => {
expect(args).toEqual(expect.arrayContaining(["-frames:v", "1", "background.png"]));
});
it("pre-blurs image wallpapers for native fallback static backgrounds", () => {
const args = buildNativeStaticBackgroundRenderArgs({
...baseConfig,
outputPath: "background.png",
backgroundImagePath: "wallpaper.jpg",
backgroundBlurPx: 36,
});
const filterComplex = args[args.indexOf("-filter_complex") + 1];
expect(filterComplex).toContain("[bg0]gblur=sigma=36:steps=2[bg_blur]");
expect(filterComplex).toContain("[bg_blur]format=rgba[out]");
});
it("builds a precomposited static layout command with a squircle alpha mask", () => {
const args = buildNativePrecompositedStaticLayoutArgs({
...baseConfig,
+26 -11
View File
@@ -77,6 +77,7 @@ export interface NativeStaticLayoutExportArgsConfig {
offsetY: number;
backgroundColor: string;
backgroundImagePath?: string | null;
backgroundBlurPx?: number;
staticBackgroundPath?: string | null;
maskPath?: string | null;
borderRadius?: number;
@@ -185,16 +186,22 @@ function clampUnitInterval(value: number): number {
}
function formatFfmpegNumber(value: number): string {
return Number.isInteger(value) ? String(value) : value.toFixed(6).replace(/0+$/, "").replace(/\.$/, "");
return Number.isInteger(value)
? String(value)
: value.toFixed(6).replace(/0+$/, "").replace(/\.$/, "");
}
function isPointInsidePolygon(x: number, y: number, points: Array<{ x: number; y: number }>) {
let inside = false;
for (let index = 0, previousIndex = points.length - 1; index < points.length; previousIndex = index++) {
for (
let index = 0, previousIndex = points.length - 1;
index < points.length;
previousIndex = index++
) {
const current = points[index];
const previous = points[previousIndex];
const intersects =
(current.y > y) !== (previous.y > y) &&
current.y > y !== previous.y > y &&
x < ((previous.x - current.x) * (y - current.y)) / (previous.y - current.y) + current.x;
if (intersects) {
@@ -370,6 +377,7 @@ export function buildNativeStaticBackgroundRenderArgs(
config: NativeStaticLayoutExportArgsConfig,
): string[] {
const backgroundColor = formatFfmpegColor(config.backgroundColor);
const backgroundBlurPx = Math.max(0, config.backgroundBlurPx ?? 0);
const args = ["-y", "-hide_banner", "-loglevel", "error"];
if (config.backgroundImagePath) {
args.push("-i", config.backgroundImagePath);
@@ -402,6 +410,14 @@ export function buildNativeStaticBackgroundRenderArgs(
: "[0:v]format=rgba[bg0]",
];
let currentBackgroundLabel = "bg0";
if (backgroundBlurPx > 0 && config.backgroundImagePath) {
filterParts.push(
`[${currentBackgroundLabel}]gblur=sigma=${formatFfmpegNumber(
Math.min(96, backgroundBlurPx),
)}:steps=2[bg_blur]`,
);
currentBackgroundLabel = "bg_blur";
}
if (shadowLayers.length > 0) {
filterParts.push(
@@ -494,9 +510,7 @@ export function buildNativePrecompositedStaticLayoutArgs(
}
const foregroundFilter = `[0:v]scale_cuda=w=${config.contentWidth}:h=${config.contentHeight}:format=nv12:passthrough=0,hwdownload,format=nv12,fps=${config.frameRate},format=rgba[fgbase]`;
const maskFilter = useMask
? ";[2:v]format=gray[mask];[fgbase][mask]alphamerge[fg]"
: "";
const maskFilter = useMask ? ";[2:v]format=gray[mask];[fgbase][mask]alphamerge[fg]" : "";
const foregroundLabel = useMask ? "fg" : "fgbase";
const filterComplex = `${foregroundFilter}${maskFilter};[1:v]format=rgba[bg];[bg][${foregroundLabel}]overlay=x=${config.offsetX}:y=${config.offsetY}:format=auto,trim=duration=${durationSec},setpts=PTS-STARTPTS,format=yuv420p[out]`;
@@ -521,10 +535,7 @@ export function buildNativePrecompositedStaticLayoutArgs(
return args;
}
export function buildNativeConcatArgs(config: {
listPath: string;
outputPath: string;
}): string[] {
export function buildNativeConcatArgs(config: { listPath: string; outputPath: string }): string[] {
return [
"-y",
"-hide_banner",
@@ -554,7 +565,11 @@ export function buildNativeStaticLayoutChunks(
const safeChunkDuration = Math.max(1, Math.min(300, Math.floor(chunkDurationSec)));
const chunks: NativeStaticLayoutChunk[] = [];
for (let startSec = 0, index = 0; startSec < durationSec; startSec += safeChunkDuration, index++) {
for (
let startSec = 0, index = 0;
startSec < durationSec;
startSec += safeChunkDuration, index++
) {
chunks.push({
index,
startSec,
@@ -1,35 +1,35 @@
{
"version": 1,
"platform": "win32",
"arch": "x64",
"helpers": {
"wgc-capture": {
"binaryName": "wgc-capture.exe",
"binarySha256": "a0e2f0b98a903f89ab4706a56623f141604c3a311102e8914c2780745e94c635",
"sourceDir": "electron/native/wgc-capture",
"sourceFingerprint": "820d13741c659a0357d4d7944f3dbf4ce778b9716725b47ec088f573db0e7311",
"updatedAt": "2026-05-06T13:15:19.804Z"
},
"cursor-monitor": {
"binaryName": "cursor-monitor.exe",
"binarySha256": "6ae6d91103b6e891a851e8ea5791e1c1f9aaab700134c18bc4c46cfffd7fdd12",
"sourceDir": "electron/native/cursor-monitor",
"sourceFingerprint": "6ad1b8b50bb336f2a48937b06f5ec56d90b6ab4a3e56a4bca278cf67a5d3e52e",
"updatedAt": "2026-05-05T18:42:26.007Z"
},
"recordly-gpu-export": {
"binaryName": "recordly-gpu-export.exe",
"binarySha256": "e79cb823dd2cfade857db67696ae6456ae0ba3ae9381b6792c948ac648521bd4",
"sourceDir": "electron/native/gpu-export-probe",
"sourceFingerprint": "45aaa4c0cbeb45ecaa9ff0a87b870c33bd0d5b09fb5b3acef45b246970a12e7f",
"updatedAt": "2026-05-05T18:41:54.966Z"
},
"recordly-nvidia-cuda-compositor": {
"binaryName": "recordly-nvidia-cuda-compositor.exe",
"binarySha256": "2913d78e2c1114d32c92fb4238749f7ff46131ec92cf3ad0b55af910c7cdbb07",
"sourceDir": "electron/native/nvidia-cuda-compositor",
"sourceFingerprint": "a60538981da7dd3d9645f59926d42f4f2f0f1f0663d3905e0416fdff8becb314",
"updatedAt": "2026-05-05T18:42:20.123Z"
}
}
"version": 1,
"platform": "win32",
"arch": "x64",
"helpers": {
"wgc-capture": {
"binaryName": "wgc-capture.exe",
"binarySha256": "a0e2f0b98a903f89ab4706a56623f141604c3a311102e8914c2780745e94c635",
"sourceDir": "electron/native/wgc-capture",
"sourceFingerprint": "820d13741c659a0357d4d7944f3dbf4ce778b9716725b47ec088f573db0e7311",
"updatedAt": "2026-05-06T13:15:19.804Z"
},
"cursor-monitor": {
"binaryName": "cursor-monitor.exe",
"binarySha256": "6ae6d91103b6e891a851e8ea5791e1c1f9aaab700134c18bc4c46cfffd7fdd12",
"sourceDir": "electron/native/cursor-monitor",
"sourceFingerprint": "6ad1b8b50bb336f2a48937b06f5ec56d90b6ab4a3e56a4bca278cf67a5d3e52e",
"updatedAt": "2026-05-05T18:42:26.007Z"
},
"recordly-gpu-export": {
"binaryName": "recordly-gpu-export.exe",
"binarySha256": "f147901f01dd5b410f67a5c0feae5064ea7556a878b333aa493e799498abb5e5",
"sourceDir": "electron/native/gpu-export-probe",
"sourceFingerprint": "056a4c41113c471379d0ec6095249c46a79b05fac5fc0e52ce0bfdf6de4d91e1",
"updatedAt": "2026-05-06T13:51:01.756Z"
},
"recordly-nvidia-cuda-compositor": {
"binaryName": "recordly-nvidia-cuda-compositor.exe",
"binarySha256": "ec594808577011407fe40ea4e610a6b6fd2cbc7c5dde4627f48351a8a5a980c3",
"sourceDir": "electron/native/nvidia-cuda-compositor",
"sourceFingerprint": "6dd2a015326ac984c0a88581c809d628592f174e02a6813d24356c22e7384f1a",
"updatedAt": "2026-05-06T13:51:30.019Z"
}
}
}
+54 -19
View File
@@ -60,6 +60,7 @@ struct Options {
float backgroundR = 0.035f;
float backgroundG = 0.035f;
float backgroundB = 0.045f;
float backgroundBlurPx = 0.0f;
std::wstring backgroundImagePath;
std::wstring webcamInputPath;
LONG webcamLeft = -1;
@@ -124,9 +125,9 @@ struct ShaderConstants {
float cursorAtlasAnchorY;
float cursorAtlasAspect;
float cursorBounceScale;
float cursorPadding0;
float cursorPadding1;
float cursorPadding2;
float backgroundBlurPx;
float backgroundBlurPadding0;
float backgroundBlurPadding1;
float zoomEnabled;
float zoomScale;
float zoomX;
@@ -380,6 +381,9 @@ Options parseOptions(int argc, wchar_t** argv) {
options.radius = parseFloatArg(args, L"--radius", options.radius);
options.shadow = parseFloatArg(args, L"--shadow", options.shadow);
options.padding = parseFloatArg(args, L"--padding", options.padding);
options.backgroundBlurPx = std::max(
0.0f,
parseFloatArg(args, L"--background-blur", options.backgroundBlurPx));
options.contentLeft = parseLongArg(args, L"--content-left", options.contentLeft);
options.contentTop = parseLongArg(args, L"--content-top", options.contentTop);
options.contentWidth = parseLongArg(args, L"--content-width", options.contentWidth);
@@ -1498,9 +1502,9 @@ cbuffer CompositorConstants : register(b0) {
float cursorAtlasAnchorY;
float cursorAtlasAspect;
float cursorBounceScale;
float cursorPadding0;
float cursorPadding1;
float cursorPadding2;
float backgroundBlurPx;
float backgroundBlurPadding0;
float backgroundBlurPadding1;
float zoomEnabled;
float zoomScale;
float zoomX;
@@ -1583,6 +1587,48 @@ float sampleCursorAtlasShadow(float2 cursorLocal, float cursorWidth, float curso
return saturate(alpha);
}
float2 getBackgroundCoverUv(float2 uv) {
float2 backgroundUv = uv;
float outputAspect = outputWidth / outputHeight;
float backgroundAspect = backgroundImageWidth / backgroundImageHeight;
if (backgroundAspect > outputAspect) {
backgroundUv.x = 0.5 + ((backgroundUv.x - 0.5) * (outputAspect / backgroundAspect));
} else {
backgroundUv.y = 0.5 + ((backgroundUv.y - 0.5) * (backgroundAspect / outputAspect));
}
return saturate(backgroundUv);
}
float4 sampleBackground(float2 uv) {
if (backgroundImageEnabled <= 0.5) {
return float4(backgroundR, backgroundG, backgroundB, backgroundA);
}
float2 backgroundUv = getBackgroundCoverUv(uv);
float safeBlur = min(max(backgroundBlurPx, 0.0), 96.0);
if (safeBlur <= 0.001) {
return backgroundTexture.Sample(linearSampler, backgroundUv);
}
float2 texel = float2(1.0 / max(outputWidth, 1.0), 1.0 / max(outputHeight, 1.0));
float2 r1 = texel * safeBlur * 0.35;
float2 r2 = texel * safeBlur * 0.70;
float4 color = backgroundTexture.Sample(linearSampler, backgroundUv) * 0.20;
color += backgroundTexture.Sample(linearSampler, saturate(backgroundUv + float2( r1.x, 0.0))) * 0.10;
color += backgroundTexture.Sample(linearSampler, saturate(backgroundUv + float2(-r1.x, 0.0))) * 0.10;
color += backgroundTexture.Sample(linearSampler, saturate(backgroundUv + float2(0.0, r1.y))) * 0.10;
color += backgroundTexture.Sample(linearSampler, saturate(backgroundUv + float2(0.0, -r1.y))) * 0.10;
color += backgroundTexture.Sample(linearSampler, saturate(backgroundUv + float2( r2.x, r2.y))) * 0.05;
color += backgroundTexture.Sample(linearSampler, saturate(backgroundUv + float2(-r2.x, r2.y))) * 0.05;
color += backgroundTexture.Sample(linearSampler, saturate(backgroundUv + float2( r2.x, -r2.y))) * 0.05;
color += backgroundTexture.Sample(linearSampler, saturate(backgroundUv + float2(-r2.x, -r2.y))) * 0.05;
color += backgroundTexture.Sample(linearSampler, saturate(backgroundUv + float2( r2.x, 0.0))) * 0.05;
color += backgroundTexture.Sample(linearSampler, saturate(backgroundUv + float2(-r2.x, 0.0))) * 0.05;
color += backgroundTexture.Sample(linearSampler, saturate(backgroundUv + float2(0.0, r2.y))) * 0.05;
color += backgroundTexture.Sample(linearSampler, saturate(backgroundUv + float2(0.0, -r2.y))) * 0.05;
return color;
}
float4 main(PSIn input) : SV_Target {
float2 outputSize = float2(outputWidth, outputHeight);
float2 pixel = input.uv * outputSize;
@@ -1604,18 +1650,7 @@ float4 main(PSIn input) : SV_Target {
outsideAlpha *
shadowA;
float4 background = float4(backgroundR, backgroundG, backgroundB, backgroundA);
if (backgroundImageEnabled > 0.5) {
float2 backgroundUv = input.uv;
float outputAspect = outputWidth / outputHeight;
float backgroundAspect = backgroundImageWidth / backgroundImageHeight;
if (backgroundAspect > outputAspect) {
backgroundUv.x = 0.5 + ((backgroundUv.x - 0.5) * (outputAspect / backgroundAspect));
} else {
backgroundUv.y = 0.5 + ((backgroundUv.y - 0.5) * (backgroundAspect / outputAspect));
}
background = backgroundTexture.Sample(linearSampler, saturate(backgroundUv));
}
float4 background = sampleBackground(input.uv);
float4 shadow = float4(shadowR, shadowG, shadowB, shadowAlpha);
float4 content = contentTexture.Sample(linearSampler, saturate(contentPixel / outputSize));
@@ -2761,7 +2796,7 @@ float4 main(PSIn input) : SV_Target {
cursorAtlasEnabled ? cursorAtlasEntry->anchorY : 0.0f,
cursorAtlasEnabled ? cursorAtlasEntry->aspectRatio : 1.0f,
cursorEnabled ? cursor.bounceScale : 1.0f,
0.0f,
hasBackgroundImage_ ? options_.backgroundBlurPx : 0.0f,
0.0f,
0.0f,
zoomEnabled ? 1.0f : 0.0f,
@@ -680,13 +680,7 @@ function ffprobeJson(args) {
}
async function ffprobeCsvAsync(args) {
const result = await runAsync(ffprobeCommand, [
"-v",
"error",
...args,
"-of",
"csv=p=0",
]);
const result = await runAsync(ffprobeCommand, ["-v", "error", ...args, "-of", "csv=p=0"]);
return result.stdout;
}
@@ -865,15 +859,23 @@ function roundedRectMaskExpression({ x, y, width, height, radius }) {
return `${centerBand}+${middleBand}+${topLeft}+${topRight}+${bottomLeft}+${bottomRight}`;
}
function createBackgroundFilter(videoInfo, shadowOptions) {
const base = `[0:v]scale=${videoInfo.width}:${videoInfo.height}:force_original_aspect_ratio=increase,crop=${videoInfo.width}:${videoInfo.height},format=rgba[bg]`;
function createBackgroundFilter(videoInfo, shadowOptions, blurPx = 0) {
const safeBlurPx = Math.max(0, Math.min(96, Math.round(Number.isFinite(blurPx) ? blurPx : 0)));
const scaled = `[0:v]scale=${videoInfo.width}:${videoInfo.height}:force_original_aspect_ratio=increase,crop=${videoInfo.width}:${videoInfo.height},format=rgba[bg_scaled]`;
const blurFilter =
safeBlurPx > 0
? `;[bg_scaled]boxblur=luma_radius=${safeBlurPx}:luma_power=1:chroma_radius=${safeBlurPx}:chroma_power=1:alpha_radius=${safeBlurPx}:alpha_power=1[bg]`
: ";[bg_scaled]null[bg]";
if (!shadowOptions) {
return {
filterArgs: [
"-vf",
`scale=${videoInfo.width}:${videoInfo.height}:force_original_aspect_ratio=increase,crop=${videoInfo.width}:${videoInfo.height},format=nv12`,
"-filter_complex",
`${scaled}${blurFilter};[bg]format=nv12[out]`,
"-map",
"[out]",
],
bakedShadow: false,
backgroundBlur: safeBlurPx,
};
}
@@ -894,13 +896,14 @@ function createBackgroundFilter(videoInfo, shadowOptions) {
"-i",
`color=c=black@0.0:s=${videoInfo.width}x${videoInfo.height}:d=1`,
"-filter_complex",
`${base};${shadow};[bg][shadow]overlay=format=auto,format=nv12[out]`,
`${scaled}${blurFilter};${shadow};[bg][shadow]overlay=format=auto,format=nv12[out]`,
"-map",
"[out]",
],
bakedShadow: true,
shadowAlpha,
shadowBlur,
backgroundBlur: safeBlurPx,
};
}
@@ -949,6 +952,7 @@ const backgroundY = Math.round(getNonNegativeNumberArg("--background-y", 16));
const backgroundU = Math.round(getNonNegativeNumberArg("--background-u", 128));
const backgroundV = Math.round(getNonNegativeNumberArg("--background-v", 128));
const backgroundImage = getArg("--background-image", "");
const backgroundBlurPx = getNonNegativeNumberArg("--background-blur", 0);
const backgroundNv12 = getArg("--background-nv12", "");
const shadowOffsetY = Math.round(getNonNegativeNumberArg("--shadow-offset-y", 0));
const shadowIntensityPct = Math.round(getNonNegativeNumberArg("--shadow-intensity-pct", 0));
@@ -1020,9 +1024,14 @@ const shouldBakeStaticShadow =
contentHeight > 0 &&
shadowOffsetY > 0 &&
shadowIntensityPct > 0;
const backgroundSuffix = shouldBakeStaticShadow
? `.shadow-${shadowOffsetY}-${shadowIntensityPct}`
: "";
const backgroundSuffixParts = [];
if (shouldBakeStaticShadow) {
backgroundSuffixParts.push(`shadow-${shadowOffsetY}-${shadowIntensityPct}`);
}
if (backgroundBlurPx > 0) {
backgroundSuffixParts.push(`blur-${Math.round(backgroundBlurPx)}`);
}
const backgroundSuffix = backgroundSuffixParts.length ? `.${backgroundSuffixParts.join(".")}` : "";
const generatedBackgroundNv12Path = join(workDir, `${baseName}${backgroundSuffix}.background.nv12`);
const generatedWebcamNv12Path = join(
workDir,
@@ -1098,6 +1107,7 @@ const backgroundFilter = createBackgroundFilter(
intensityPct: shadowIntensityPct,
}
: null,
backgroundBlurPx,
);
const backgroundConvertPromise =
+1
View File
@@ -215,6 +215,7 @@ contextBridge.exposeInMainWorld("electronAPI", {
offsetY: number;
backgroundColor: string;
backgroundImagePath?: string | null;
backgroundBlurPx?: number;
borderRadius?: number;
shadowIntensity?: number;
webcamInputPath?: string | null;
@@ -175,7 +175,7 @@ describe("ModernVideoExporter native static-layout eligibility", () => {
).toBe("unsupported-frame-overlay");
});
it("reports background blur as the remaining native overlay blocker", () => {
it("allows native static-layout with background blur", () => {
const exporter = createExporter({ backgroundBlur: 12 });
expect(
@@ -187,7 +187,7 @@ describe("ModernVideoExporter native static-layout eligibility", () => {
videoInfo,
60,
),
).toBe("unsupported-background-blur");
).toBeNull();
});
it("allows non-tail trim timelines with native static-layout", () => {
+1 -4
View File
@@ -1257,10 +1257,6 @@ export class ModernVideoExporter {
return "unsupported-frame-overlay";
}
if (this.config.backgroundBlur > 0) {
return "unsupported-background-blur";
}
return this.isDefaultCropRegion() ? null : "non-default-crop";
}
@@ -1895,6 +1891,7 @@ export class ModernVideoExporter {
offsetY,
backgroundColor: background.backgroundColor,
backgroundImagePath: background.backgroundImagePath ?? null,
backgroundBlurPx: Math.max(0, (this.config.backgroundBlur ?? 0) * 3),
borderRadius,
shadowIntensity,
webcamInputPath: webcamOverlay?.inputPath ?? null,