Probe real encoder output, order codecs by it, and add a quality slider
Your machine's H.264/HEVC encoders report support and then emit no video samples at all, which no configure-time check can see. The optimizer now test-encodes three frames per codec (cached for 30 days), shows the result as diagnostics in the modal (AVC x / VP9 (hardware) / ...), feeds the working codecs to the worker so Auto tries them first, and tells you to switch to WebM when a container has no usable encoder. Video quality is now a 10-100 slider with a predicted bitrate and size for this clip (mirroring Mediabunny's mapping), which makes the trade-off visible instead of guessing from presets.
This commit is contained in:
@@ -8,25 +8,17 @@ import { cn } from "@/lib/cn";
|
||||
import { formatBytes } from "@/lib/format";
|
||||
import type { MediaItem } from "@/lib/types";
|
||||
|
||||
import { detectVideoCodecs } from "./detect";
|
||||
import { estimateVideoBitrate, VIDEO_CODEC_CANDIDATES } from "./bitrate";
|
||||
import { probeVideoEncoders, workingCodecs } from "./detect";
|
||||
import { deleteStoredFile } from "./opfs";
|
||||
import {
|
||||
DEFAULT_OPTIONS,
|
||||
kindForExtension,
|
||||
type OptimizeKind,
|
||||
type OptimizeOptions,
|
||||
type VideoQualityLevel,
|
||||
} from "./types";
|
||||
import { useOptimizer } from "./useOptimizer";
|
||||
|
||||
const QUALITY_LEVELS: { value: VideoQualityLevel; label: string }[] = [
|
||||
{ value: "very-high", label: "Visually lossless" },
|
||||
{ value: "high", label: "High (recommended)" },
|
||||
{ value: "medium", label: "Balanced" },
|
||||
{ value: "low", label: "Small" },
|
||||
{ value: "very-low", label: "Smallest" },
|
||||
];
|
||||
|
||||
const DIMENSION_CHOICES: { value: number | null; label: string }[] = [
|
||||
{ value: null, label: "Keep original" },
|
||||
{ value: 2560, label: "Max 2560 px" },
|
||||
@@ -190,12 +182,20 @@ export function OptimizeModal({
|
||||
const [resultInfo, setResultInfo] = useState<MediaInfo | null>(null);
|
||||
|
||||
const codecQuery = useQuery({
|
||||
queryKey: ["optimize-codecs"],
|
||||
queryFn: detectVideoCodecs,
|
||||
queryKey: ["optimize-encoders"],
|
||||
queryFn: () => probeVideoEncoders(),
|
||||
enabled: kind === "video",
|
||||
staleTime: Infinity,
|
||||
retry: 0,
|
||||
});
|
||||
const probes = codecQuery.data ?? [];
|
||||
const container: "mp4" | "webm" =
|
||||
options.container === "keep"
|
||||
? item.extension === "webm"
|
||||
? "webm"
|
||||
: "mp4"
|
||||
: options.container;
|
||||
const usableCodecs = workingCodecs(probes, container);
|
||||
|
||||
const applyMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
@@ -240,15 +240,45 @@ export function OptimizeModal({
|
||||
: null;
|
||||
|
||||
const isVideo = kind === "video";
|
||||
const hardwareAvailable =
|
||||
codecQuery.data?.some((entry) => entry.supported && entry.hardware) ??
|
||||
false;
|
||||
const truncated =
|
||||
isVideo &&
|
||||
originalInfo?.duration != null &&
|
||||
resultInfo?.duration != null &&
|
||||
resultInfo.duration + 1 < originalInfo.duration;
|
||||
|
||||
// Predicted output size, mirroring Mediabunny's quality -> bitrate mapping.
|
||||
const estimateCodec =
|
||||
options.codec !== "auto"
|
||||
? options.codec
|
||||
: (usableCodecs[0] ?? VIDEO_CODEC_CANDIDATES[container][0]);
|
||||
const estimate =
|
||||
isVideo &&
|
||||
originalInfo?.duration &&
|
||||
originalInfo.width &&
|
||||
originalInfo.height
|
||||
? (() => {
|
||||
const sourceHeight = originalInfo.height as number;
|
||||
const sourceWidth = originalInfo.width as number;
|
||||
const targetHeight =
|
||||
options.height && options.height < sourceHeight
|
||||
? options.height
|
||||
: sourceHeight;
|
||||
const targetWidth = Math.round(
|
||||
sourceWidth * (targetHeight / sourceHeight),
|
||||
);
|
||||
const bitrate = estimateVideoBitrate(
|
||||
estimateCodec,
|
||||
targetWidth,
|
||||
targetHeight,
|
||||
options.quality,
|
||||
);
|
||||
return {
|
||||
bitrate,
|
||||
size: ((bitrate + 128_000) / 8) * (originalInfo.duration as number),
|
||||
};
|
||||
})()
|
||||
: null;
|
||||
|
||||
function process() {
|
||||
setApplied(false);
|
||||
setApplyError(null);
|
||||
@@ -259,6 +289,7 @@ export function OptimizeModal({
|
||||
filename: item.filename,
|
||||
kind,
|
||||
options,
|
||||
preferredCodecs: usableCodecs,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -531,24 +562,28 @@ export function OptimizeModal({
|
||||
|
||||
{kind === "video" ? (
|
||||
<>
|
||||
<Field label="Quality">
|
||||
<select
|
||||
className={inputClass}
|
||||
value={options.qualityLevel}
|
||||
<Field label={`Quality — ${options.quality}`}>
|
||||
<input
|
||||
type="range"
|
||||
min={10}
|
||||
max={100}
|
||||
value={options.quality}
|
||||
onChange={(event) =>
|
||||
setOptions((current) => ({
|
||||
...current,
|
||||
qualityLevel: event.target.value as VideoQualityLevel,
|
||||
quality: Number(event.target.value),
|
||||
}))
|
||||
}
|
||||
>
|
||||
{QUALITY_LEVELS.map((level) => (
|
||||
<option key={level.value} value={level.value}>
|
||||
{level.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
className="w-full accent-ctp-mauve"
|
||||
/>
|
||||
</Field>
|
||||
{estimate ? (
|
||||
<p className="-mt-2 text-[11px] text-ctp-overlay0">
|
||||
≈ {(estimate.bitrate / 1_000_000).toFixed(1)} Mbps · about{" "}
|
||||
{formatBytes(estimate.size)} for{" "}
|
||||
{originalInfo?.duration?.toFixed(1)} s
|
||||
</p>
|
||||
) : null}
|
||||
<Field label="Resolution">
|
||||
<select
|
||||
className={inputClass}
|
||||
@@ -584,14 +619,15 @@ export function OptimizeModal({
|
||||
}
|
||||
>
|
||||
<option value="auto">Auto (best available)</option>
|
||||
{(codecQuery.data ?? [])
|
||||
.filter((entry) => entry.supported)
|
||||
.map((entry) => (
|
||||
<option key={entry.codec} value={entry.codec}>
|
||||
{entry.codec.toUpperCase()}
|
||||
{entry.hardware ? " (hardware)" : ""}
|
||||
{usableCodecs.map((codec) => {
|
||||
const probe = probes.find((entry) => entry.codec === codec);
|
||||
return (
|
||||
<option key={codec} value={codec}>
|
||||
{codec.toUpperCase()}
|
||||
{probe?.hardware ? " (hardware)" : ""}
|
||||
</option>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</Field>
|
||||
<Field label="Container">
|
||||
@@ -625,13 +661,34 @@ export function OptimizeModal({
|
||||
/>
|
||||
Prefer hardware encoding
|
||||
</label>
|
||||
<p className="text-[11px] leading-relaxed text-ctp-overlay0">
|
||||
{codecQuery.isPending
|
||||
? "Checking the encoders this browser offers…"
|
||||
: hardwareAvailable
|
||||
? "Hardware-accelerated encoders are available on this machine."
|
||||
: "No hardware encoder detected — encoding will use the CPU."}
|
||||
</p>
|
||||
{codecQuery.isPending ? (
|
||||
<p className="text-[11px] leading-relaxed text-ctp-overlay0">
|
||||
Test-encoding a few frames to see which encoders really
|
||||
work here…
|
||||
</p>
|
||||
) : usableCodecs.length === 0 ? (
|
||||
<p className="rounded-md border border-ctp-red/40 bg-ctp-red/10 px-2 py-1.5 text-[11px] leading-relaxed text-ctp-red">
|
||||
This browser cannot produce {container.toUpperCase()} video
|
||||
samples — switch the container to{" "}
|
||||
{container === "mp4" ? "WebM" : "MP4"}.
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-[11px] leading-relaxed text-ctp-overlay0">
|
||||
Encoders:{" "}
|
||||
{probes
|
||||
.map(
|
||||
(probe) =>
|
||||
`${probe.codec.toUpperCase()}${
|
||||
probe.supported && probe.works
|
||||
? probe.hardware
|
||||
? " (hardware)"
|
||||
: " ✓"
|
||||
: " ✗"
|
||||
}`,
|
||||
)
|
||||
.join(" · ")}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user