mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-25 07:16:02 +00:00
fix(export): support native background blur
This commit is contained in:
Vendored
+1
@@ -318,6 +318,7 @@ interface Window {
|
||||
offsetY: number;
|
||||
backgroundColor: string;
|
||||
backgroundImagePath?: string | null;
|
||||
backgroundBlurPx?: number;
|
||||
borderRadius?: number;
|
||||
shadowIntensity?: number;
|
||||
webcamInputPath?: string | null;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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 =
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user