diff --git a/src/components/ExportSettings.tsx b/src/components/ExportSettings.tsx
index ddc29a85..d961bdde 100644
--- a/src/components/ExportSettings.tsx
+++ b/src/components/ExportSettings.tsx
@@ -186,6 +186,57 @@ export default function ExportSettings({
+
+
+
+
+
+
+ onChange({
+ denoise: e.target.checked,
+ })
+ }
+ aria-label="Enable noise reduction"
+ aria-checked={recipe.denoise}
+ className="w-full accent-film-600 cursor-pointer"
+ />
+
+
+
+
+ Reduce low-light video grain
+
+
+
+
+ May slightly increase export time.
+
+
+
>
);
}
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index fddfe9f2..f838e8a2 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -19,7 +19,8 @@ export const DEFAULT_RECIPE: EditRecipe = {
contrast: 1,
saturation: 1,
stabilization: false,
+ denoise: false,
soundOnCompletion: false,
normalizeAudio: false,
version: RECIPE_VERSION,
-};
\ No newline at end of file
+};
diff --git a/src/lib/ffmpeg.ts b/src/lib/ffmpeg.ts
index 4f215075..b5a6e428 100644
--- a/src/lib/ffmpeg.ts
+++ b/src/lib/ffmpeg.ts
@@ -39,7 +39,7 @@ export class FFmpegLoadError extends Error {
}
export async function loadFFmpeg(
- signal?: AbortSignal,
+ signal?: AbortSignal,
onProgress?: (percent: number) => void
): Promise {
if (ffmpegInstance?.loaded) {
@@ -96,7 +96,7 @@ function buildVideoFilter(recipe: EditRecipe, targetW: number, targetH: number):
filters.push("setpts=PTS-STARTPTS");
}
-
+
if (recipe.stabilization) {
filters.push("deshake");
}
@@ -122,9 +122,14 @@ function buildVideoFilter(recipe: EditRecipe, targetW: number, targetH: number):
}
if (recipe.speed !== 1) {
- const pts = (1 / recipe.speed).toFixed(4);
- filters.push(`setpts=${pts}*PTS`);
+ const pts = (1 / recipe.speed).toFixed(4);
+ filters.push(`setpts=${pts}*PTS`);
+ }
+
+ if (recipe.denoise) {
+ filters.push("hqdn3d=1.5:1.5:6:6");
}
+
filters.push(
`eq=brightness=${recipe.brightness}:contrast=${recipe.contrast}:saturation=${recipe.saturation}`
);
@@ -177,7 +182,7 @@ function buildArguments(
): string[] {
const vf = buildVideoFilter(recipe, targetW, targetH);
const audioTrim = hasOriginalAudio ? buildAudioTrimFilter(recipe) : "";
-const audioSpeed = hasOriginalAudio ? buildAudioFilter(recipe.speed, recipe.normalizeAudio ?? false) : "";
+ const audioSpeed = hasOriginalAudio ? buildAudioFilter(recipe.speed, recipe.normalizeAudio ?? false) : "";
const afParts = [audioTrim, audioSpeed].filter(Boolean);
const af = afParts.join(",");
@@ -328,7 +333,7 @@ export async function exportVideo(
onProgress(Math.min(99, Math.round(progress * 100)));
};
-
+
try {
await ffmpeg.writeFile(inputName, await fetchFile(file), { signal });
@@ -477,4 +482,4 @@ export async function exportVideo(
export function formatBytes(bytes: number): string {
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
-}
\ No newline at end of file
+}
diff --git a/src/lib/types.ts b/src/lib/types.ts
index 91040259..2e12a177 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -14,6 +14,7 @@ export interface EditRecipe {
quality: number;
format: "mp4" | "webm" | "mkv" | "gif";
stabilization: boolean;
+ denoise: boolean;
brightness: number;
contrast: number;
saturation: number;
@@ -81,6 +82,7 @@ export const DEFAULT_RECIPE: EditRecipe = {
quality: 23,
format: "mp4",
stabilization: false,
+ denoise: false,
brightness: 0,
contrast: 0,
saturation: 0,
@@ -118,4 +120,4 @@ export function isValidRecipe(value: unknown): value is EditRecipe {
if (typeof v.soundOnCompletion !== "boolean") return false;
return true;
-}
\ No newline at end of file
+}