diff --git a/src/components/VideoEditor.tsx b/src/components/VideoEditor.tsx index 1e4e9f0d..9c015719 100644 --- a/src/components/VideoEditor.tsx +++ b/src/components/VideoEditor.tsx @@ -24,6 +24,7 @@ import { Layers, Crop, Scissors, RotateCw, Volume2, Type, SlidersHorizontal, Zap, AlertTriangle, Github, Copy } from "lucide-react"; +import { formatBytes } from "@/lib/fileSizeRisk"; import OnboardingTour from "./OnboardingTour"; import { useKeyboardShortcuts } from "@/hooks/useKeyboardShortcuts"; @@ -210,6 +211,7 @@ export default function VideoEditor() { recommendedPreset, currentTime, toggleSound, + fileRisk, } = useVideoEditor(); useKeyboardShortcuts({ @@ -237,6 +239,12 @@ export default function VideoEditor() { const toggleSection = (key: keyof typeof openSections) => setOpenSections((prev) => ({ ...prev, [key]: !prev[key] })); + const [warningDismissed, setWarningDismissed] = useState(false); + + // Reset dismissal when a new file is loaded + useEffect(() => { + setWarningDismissed(false); + }, [file?.name]); const downloadRef = useRef(null); /** @@ -405,10 +413,33 @@ export default function VideoEditor() { )} - {file && file.size > 100 * 1024 * 1024 && ( -

- ⚠️ Large file - processing may take several minutes -

+ {file && fileRisk !== "safe" && !warningDismissed && ( +
+ +
+

+ {fileRisk === "very-large" ? "Very large file detected" : "Large file detected"} + {file ? ` (${formatBytes(file.size)})` : ""} +

+

+ Browser-based FFmpeg processing may be slow or fail due to memory limits. For best results, use files under 500 MB. Export is still available but may take longer or fail. +

+
+
+ +
+
)} {file && (
(null); const [error, setError] = useState(null); const [fileError, setFileError] = useState(""); + const [fileRisk, setFileRisk] = useState("safe"); const [exportStartedAt, setExportStartedAt] = useState(null); const exportAbortControllerRef = useRef(null); const exportCancelledRef = useRef(false); @@ -396,6 +398,17 @@ export function useVideoEditor() { try { const { width, height, duration: dur } = await extractMetadata(selectedFile); + // Analyze file risk (optional device memory if available) + try { + const deviceMemory = typeof navigator !== "undefined" && (navigator as any).deviceMemory; + const deviceMemoryGb = typeof deviceMemory === "number" ? deviceMemory : undefined; + const risk = analyzeFileRisk(selectedFile, { deviceMemoryGb }); + setFileRisk(risk); + } catch (e) { + // Non-fatal: keep default 'safe' on analysis failure + setFileRisk("safe"); + } + // Layer 5: Resolution check const dimensionCheck = validateDimensions(width, height); if (dimensionCheck === "blocked") { @@ -715,5 +728,6 @@ export function useVideoEditor() { recommendedPreset, currentTime, toggleSound, + fileRisk, }; } diff --git a/src/lib/fileSizeRisk.ts b/src/lib/fileSizeRisk.ts new file mode 100644 index 00000000..b9e1fee0 --- /dev/null +++ b/src/lib/fileSizeRisk.ts @@ -0,0 +1,52 @@ +export type FileRiskLevel = "safe" | "large" | "very-large"; + +export interface FileRiskThresholds { + large?: number; // bytes + veryLarge?: number; // bytes +} + +const DEFAULT_THRESHOLDS: Required = { + large: 500 * 1024 * 1024, // 500MB + veryLarge: 1024 * 1024 * 1024, // 1GB +}; + +/** + * Analyze a File and return a simple risk level. + * Optionally adjusts thresholds based on `deviceMemoryGb` (if provided). + */ +export function analyzeFileRisk( + file: File, + opts?: FileRiskThresholds & { deviceMemoryGb?: number } +): FileRiskLevel { + const deviceMemoryGb = opts?.deviceMemoryGb; + const thresholds: Required = { + large: opts?.large ?? DEFAULT_THRESHOLDS.large, + veryLarge: opts?.veryLarge ?? DEFAULT_THRESHOLDS.veryLarge, + }; + + // If device memory is known, scale thresholds down for low-memory devices. + // This keeps the logic lightweight and conservative: less memory -> stricter thresholds. + if (typeof deviceMemoryGb === "number" && isFinite(deviceMemoryGb) && deviceMemoryGb > 0) { + // Scale factor ranges [0.25, 1]. 8GB and above => 1 (no change). + const factor = Math.max(0.25, Math.min(1, deviceMemoryGb / 8)); + thresholds.large = Math.round(thresholds.large * factor); + thresholds.veryLarge = Math.round(thresholds.veryLarge * factor); + } + + const size = file.size; + if (size >= thresholds.veryLarge) return "very-large"; + if (size >= thresholds.large) return "large"; + return "safe"; +} + +export function formatBytes(bytes: number): string { + if (bytes < 1024) return `${bytes} B`; + const units = ["KB", "MB", "GB", "TB"]; + let v = bytes / 1024; + let i = 0; + while (v >= 1024 && i < units.length - 1) { + v /= 1024; + i++; + } + return `${v.toFixed(i === 0 ? 0 : 1)} ${units[i]}`; +} diff --git a/src/lib/tests/fileSizeRisk.test.ts b/src/lib/tests/fileSizeRisk.test.ts new file mode 100644 index 00000000..54e4d37d --- /dev/null +++ b/src/lib/tests/fileSizeRisk.test.ts @@ -0,0 +1,64 @@ +import { describe, it, expect } from 'vitest'; +import { analyzeFileRisk, formatBytes } from '../fileSizeRisk'; + +// Helper to create a fake File-like object with a size property. +function fakeFileWithSize(size: number): File { + // Cast to File because tests run in jsdom; we only need `.size`. + return { size } as unknown as File; +} + +describe('fileSizeRisk', () => { + it('detects safe files < 500MB', () => { + const size = 499 * 1024 * 1024; // 499 MB + const f = fakeFileWithSize(size); + const r = analyzeFileRisk(f); + expect(r).toBe('safe'); + }); + + it('detects large files >= 500MB', () => { + const size = 600 * 1024 * 1024; // 600 MB + const f = fakeFileWithSize(size); + const r = analyzeFileRisk(f); + expect(r).toBe('large'); + }); + + it('detects very-large files >= 1GB', () => { + const size = 1.1 * 1024 * 1024 * 1024; // 1.1 GB + const f = fakeFileWithSize(Math.floor(size)); + const r = analyzeFileRisk(f); + expect(r).toBe('very-large'); + }); + + it('respects custom threshold overrides', () => { + const size = 350 * 1024 * 1024; // 350 MB + const f = fakeFileWithSize(size); + // Override thresholds so this file becomes 'large' + const r1 = analyzeFileRisk(f, { large: 300 * 1024 * 1024, veryLarge: 900 * 1024 * 1024 }); + expect(r1).toBe('large'); + + // With a higher large threshold, it should be safe + const r2 = analyzeFileRisk(f, { large: 400 * 1024 * 1024, veryLarge: 900 * 1024 * 1024 }); + expect(r2).toBe('safe'); + }); + + it('formats bytes into KB, MB, GB correctly', () => { + expect(formatBytes(800)).toBe('800 B'); + expect(formatBytes(2048)).toBe('2 KB'); + expect(formatBytes(5 * 1024 * 1024)).toBe('5.0 MB'); + expect(formatBytes(3 * 1024 * 1024 * 1024)).toBe('3.0 GB'); + }); + + it('scales thresholds for low-memory devices (more conservative)', () => { + const size = 400 * 1024 * 1024; // 400 MB + const f = fakeFileWithSize(size); + + // Default without device memory: 400MB is 'safe' (default large ~500MB) + expect(analyzeFileRisk(f)).toBe('safe'); + + // On a 4GB device thresholds scale down (factor 0.5) => large threshold becomes ~250MB + expect(analyzeFileRisk(f, { deviceMemoryGb: 4 })).toBe('large'); + + // On a high-memory device (16GB) thresholds shouldn't be stricter than default + expect(analyzeFileRisk(f, { deviceMemoryGb: 16 })).toBe('safe'); + }); +});