import { type Dispatch, type SetStateAction } from "react"; import type { VideoMetadata } from "../../utils/types.ts"; import Selector from "../Selector.tsx"; import clsx from "clsx"; import { inputClass, monoInputClass, kickerClass } from "../../utils/styles.ts"; type Props = { metadata: VideoMetadata; setMetadata: Dispatch>; className?: string; }; // Mirrors CompressionService.buildBitrate so the number shown is the number used: // 90% of the raw budget, audio capped at 128 kbit/s, video takes the rest. const BUDGET_MULTIPLIER = 0.9; const AUDIO_RATIO = 0.15; const MAX_AUDIO_BITRATE = 128; type Estimate = { total: number; audio: number; video: number; bpp: number | null }; const estimateBitrate = (metadata: VideoMetadata): Estimate | null => { const { fileSize, duration, width, height, fps } = metadata; if (!fileSize || !duration || duration <= 0) return null; const total = ((fileSize * 8) / duration) * BUDGET_MULTIPLIER; const audio = Math.min(total * AUDIO_RATIO, MAX_AUDIO_BITRATE); const video = total - audio; // Bits per pixel per frame — the usual way to judge whether a bitrate suits a resolution. const bpp = width && height && fps ? (video * 1000) / (width * height * fps) : null; return { total, audio, video, bpp }; }; const formatBitrate = (kbps: number): string => kbps >= 1000 ? `${(kbps / 1000).toFixed(1)} Mbit/s` : `${Math.round(kbps)} kbit/s`; const verdictOf = (estimate: Estimate): { word: string; tone: string } => { if (estimate.video <= 0) return { word: "not enough room", tone: "text-live-red" }; if (estimate.bpp === null) return { word: "—", tone: "text-muted" }; if (estimate.bpp >= 0.15) return { word: "crisp", tone: "text-olive" }; if (estimate.bpp >= 0.09) return { word: "clean", tone: "text-olive" }; if (estimate.bpp >= 0.05) return { word: "soft", tone: "text-muted" }; return { word: "very soft", tone: "text-live-red" }; }; export default function ConfigBox({ metadata, setMetadata, className }: Props) { const updateRes = (e: React.ChangeEvent) => { const [width, height] = e.target.value.split(",").map(Number); setMetadata(prev => ({ ...prev, width, height })); }; const updateFps = (e: React.ChangeEvent) => { setMetadata(prev => ({ ...prev, fps: parseInt(e.target.value) })); }; const updateFileSize = (e: React.ChangeEvent) => { setMetadata(prev => ({ ...prev, fileSize: Math.max(1, parseInt(e.target.value) || 1) * 1000 })); }; const sizeMb = Math.round(metadata.fileSize / 1000); const estimate = estimateBitrate(metadata); const verdict = estimate && verdictOf(estimate); return (

Export settings

{/* What that budget actually buys, at this length and resolution. */}

Estimated bitrate

{verdict &&

{verdict.word}

}
{estimate ? ( <>

{formatBitrate(Math.max(0, estimate.video))}

video, plus {formatBitrate(estimate.audio)} audio — {sizeMb} MB across {metadata.duration.toFixed(1)}s

) : (

Pick a trim range to see what the size limit buys you.

)}
); }