Files
vod-system/frontend/src/components/video/ConfigBox.tsx
Dylan De Faoite eab3f8be20
All checks were successful
CI / Maven unit tests (push) Successful in 14s
CI / Build and push images (push) Has been skipped
CI / Deploy to server (push) Has been skipped
restyle the rest of the pages
2026-08-02 16:32:00 +01:00

122 lines
5.3 KiB
TypeScript

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<SetStateAction<VideoMetadata>>;
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<HTMLSelectElement>) => {
const [width, height] = e.target.value.split(",").map(Number);
setMetadata(prev => ({ ...prev, width, height }));
};
const updateFps = (e: React.ChangeEvent<HTMLSelectElement>) => {
setMetadata(prev => ({ ...prev, fps: parseInt(e.target.value) }));
};
const updateFileSize = (e: React.ChangeEvent<HTMLInputElement>) => {
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 (
<div className={clsx("flex flex-col gap-4", className)}>
<p className={kickerClass}>Export settings</p>
<Selector label="Resolution">
<select id="resolution" name="resolution" value={`${metadata.width},${metadata.height}`}
onChange={updateRes} className={inputClass}>
<option value="3840,2160">2160p</option>
<option value="2560,1440">1440p</option>
<option value="1920,1080">1080p</option>
<option value="1280,720">720p</option>
<option value="854,480">480p</option>
<option value="640,360">360p</option>
</select>
</Selector>
<Selector label="Frames per second">
<select id="fps" name="fps" value={String(metadata.fps)}
onChange={updateFps} className={inputClass}>
<option value="60">60</option>
<option value="30">30</option>
<option value="15">15</option>
</select>
</Selector>
<Selector label="Size limit (MB)">
<input type="number" min="1" value={sizeMb}
onChange={updateFileSize} className={monoInputClass}
/>
</Selector>
{/* What that budget actually buys, at this length and resolution. */}
<div className="flex flex-col gap-2 bg-fields rounded-field p-3.5">
<div className="flex items-baseline justify-between gap-3">
<p className={kickerClass}>Estimated bitrate</p>
{verdict && <p className={clsx("font-body text-[12px]", verdict.tone)}>{verdict.word}</p>}
</div>
{estimate ? (
<>
<p className="font-data text-[15px] text-text-primary tabular-nums">
{formatBitrate(Math.max(0, estimate.video))}
</p>
<p className="font-body text-[12px] text-muted">
video, plus {formatBitrate(estimate.audio)} audio — {sizeMb} MB
across {metadata.duration.toFixed(1)}s
</p>
</>
) : (
<p className="font-body text-[12px] text-muted">
Pick a trim range to see what the size limit buys you.
</p>
)}
</div>
</div>
);
}