From 8fad1c053612366ea443548628eeef67b9120b65 Mon Sep 17 00:00:00 2001 From: kiki Date: Tue, 19 May 2026 03:33:12 +0530 Subject: [PATCH 1/3] feat: add Pillarbox Blur background option for Fit mode --- src/components/FramingControl.tsx | 81 +++++++++++++++++++------------ src/lib/ffmpeg.ts | 18 +++++-- src/lib/types.ts | 2 + 3 files changed, 65 insertions(+), 36 deletions(-) diff --git a/src/components/FramingControl.tsx b/src/components/FramingControl.tsx index 75d93bc0..710f2e05 100644 --- a/src/components/FramingControl.tsx +++ b/src/components/FramingControl.tsx @@ -11,38 +11,55 @@ interface Props { export default function FramingControl({ recipe, onChange }: Props) { return ( -
- {(["fit", "fill"] as const).map((mode) => { - const Icon = mode === "fit" ? Maximize2 : Crop; - const active = recipe.framing === mode; - return ( - - ); - })} +
+
+ {(["fit", "fill"] as const).map((mode) => { + const Icon = mode === "fit" ? Maximize2 : Crop; + const active = recipe.framing === mode; + return ( + + ); + })} +
+ + {recipe.framing === "fit" && ( + + )}
); } \ No newline at end of file diff --git a/src/lib/ffmpeg.ts b/src/lib/ffmpeg.ts index 00b8fa73..f92f0a85 100644 --- a/src/lib/ffmpeg.ts +++ b/src/lib/ffmpeg.ts @@ -110,10 +110,20 @@ function buildVideoFilter(recipe: EditRecipe, targetW: number, targetH: number): } if (recipe.framing === "fit") { - filters.push( - `scale=${targetW}:${targetH}:force_original_aspect_ratio=decrease`, - `pad=${targetW}:${targetH}:(ow-iw)/2:(oh-ih)/2:color=black` - ); + if (recipe.blurBackground) { + const preStr = filters.length > 0 ? filters.join(",") + "," : ""; + filters.length = 0; // clear existing filters, we will push the complex chain + const bgScale = `scale=${targetW}:${targetH}:force_original_aspect_ratio=increase,crop=${targetW}:${targetH},boxblur=20:20`; + const fgScale = `scale=${targetW}:${targetH}:force_original_aspect_ratio=decrease`; + filters.push( + `${preStr}split=2[blur][main];[blur]${bgScale}[bg];[main]${fgScale}[fg];[bg][fg]overlay=(W-w)/2:(H-h)/2` + ); + } else { + filters.push( + `scale=${targetW}:${targetH}:force_original_aspect_ratio=decrease`, + `pad=${targetW}:${targetH}:(ow-iw)/2:(oh-ih)/2:color=black` + ); + } } else { filters.push( `scale=${targetW}:${targetH}:force_original_aspect_ratio=increase`, diff --git a/src/lib/types.ts b/src/lib/types.ts index bf167094..7438f066 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -15,6 +15,7 @@ export interface EditRecipe { contrast: number; saturation: number; soundOnCompletion: boolean; + blurBackground: boolean; } export type OverlayPosition = @@ -80,6 +81,7 @@ export const DEFAULT_RECIPE: EditRecipe = { contrast: 0, saturation: 0, soundOnCompletion: false, + blurBackground: false, }; export const MAX_FILE_SIZE = From f240da026730d3a0932c2b845f840c461a3efaba Mon Sep 17 00:00:00 2001 From: kiki Date: Wed, 20 May 2026 15:07:55 +0530 Subject: [PATCH 2/3] test: add unit tests for buildVideoFilter and export it --- src/lib/ffmpeg.ts | 2 +- src/lib/tests/ffmpeg.test.ts | 40 +++++++++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/lib/ffmpeg.ts b/src/lib/ffmpeg.ts index f92f0a85..b96d65b1 100644 --- a/src/lib/ffmpeg.ts +++ b/src/lib/ffmpeg.ts @@ -87,7 +87,7 @@ function buildSessionId(): string { return `${Date.now()}-${Math.random().toString(16).slice(2)}`; } -function buildVideoFilter(recipe: EditRecipe, targetW: number, targetH: number): string { +export function buildVideoFilter(recipe: EditRecipe, targetW: number, targetH: number): string { const filters: string[] = []; if (recipe.trimStart > 0 || recipe.trimEnd !== null) { diff --git a/src/lib/tests/ffmpeg.test.ts b/src/lib/tests/ffmpeg.test.ts index bdde5d41..76c22c5c 100644 --- a/src/lib/tests/ffmpeg.test.ts +++ b/src/lib/tests/ffmpeg.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from "vitest"; -import { buildAudioFilter } from "../ffmpeg"; +import { buildAudioFilter, buildVideoFilter } from "../ffmpeg"; +import { DEFAULT_RECIPE } from "../types"; describe("buildAudioFilter", () => { it("should return an empty string for 1.0x speed", () => { @@ -41,3 +42,40 @@ describe("buildAudioFilter", () => { expect(buildAudioFilter(10)).toBe("atempo=2.0,atempo=2.0,atempo=2.0,atempo=1.25"); }); }); + +describe("buildVideoFilter", () => { + it("should generate standard letterbox filters when blurBackground is false", () => { + const recipe = { + ...DEFAULT_RECIPE, + framing: "fit" as const, + blurBackground: false, + }; + const filter = buildVideoFilter(recipe, 1080, 1920); + expect(filter).toContain("scale=1080:1920:force_original_aspect_ratio=decrease"); + expect(filter).toContain("pad=1080:1920:(ow-iw)/2:(oh-ih)/2:color=black"); + }); + + it("should generate complex split and blur filters when blurBackground is true", () => { + const recipe = { + ...DEFAULT_RECIPE, + framing: "fit" as const, + blurBackground: true, + }; + const filter = buildVideoFilter(recipe, 1080, 1920); + expect(filter).toContain("split=2[blur][main]"); + expect(filter).toContain("scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,boxblur=20:20[bg]"); + expect(filter).toContain("scale=1080:1920:force_original_aspect_ratio=decrease[fg]"); + expect(filter).toContain("[bg][fg]overlay=(W-w)/2:(H-h)/2"); + }); + + it("should generate crop filters when framing is fill", () => { + const recipe = { + ...DEFAULT_RECIPE, + framing: "fill" as const, + }; + const filter = buildVideoFilter(recipe, 1080, 1920); + expect(filter).toContain("scale=1080:1920:force_original_aspect_ratio=increase"); + expect(filter).toContain("crop=1080:1920"); + }); +}); + From 38d3b2f22329d1d3939b82fa3d6ae9926e5feea5 Mon Sep 17 00:00:00 2001 From: kiki Date: Wed, 20 May 2026 22:34:28 +0530 Subject: [PATCH 3/3] fix: resolve linter form label error and add blurBackground default preset --- src/components/FramingControl.tsx | 15 ++++++++++++--- src/lib/constants.ts | 1 + 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/components/FramingControl.tsx b/src/components/FramingControl.tsx index 710f2e05..49f07905 100644 --- a/src/components/FramingControl.tsx +++ b/src/components/FramingControl.tsx @@ -47,16 +47,25 @@ export default function FramingControl({ recipe, onChange }: Props) {
{recipe.framing === "fit" && ( -