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:
2026-09-17 21:11:46 -05:00
parent 33ae790bd0
commit 01b9a6181a
6 changed files with 294 additions and 86 deletions
@@ -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>
{codecQuery.isPending ? (
<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."}
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}
+33
View File
@@ -0,0 +1,33 @@
import type { VideoCodec } from "mediabunny";
export const VIDEO_CODEC_CANDIDATES: Record<"mp4" | "webm", VideoCodec[]> = {
mp4: ["avc", "hevc"],
webm: ["vp9", "av1"],
};
/** Codec efficiency factors, mirroring Mediabunny's bitrate mapping. */
const CODEC_EFFICIENCY: Record<string, number> = {
avc: 1.0,
hevc: 0.6,
vp9: 0.6,
av1: 0.4,
vp8: 1.2,
};
/**
* Mirrors Mediabunny's quality -> bitrate mapping so the UI can predict the
* size of the result before encoding.
*/
export function estimateVideoBitrate(
codec: VideoCodec,
width: number,
height: number,
qualityPercent: number,
): number {
const quality = Math.min(Math.max(qualityPercent / 100, 0.05), 1);
const factor = 0.3 * Math.exp(2.5538 * quality);
const pixels = Math.max(width * height, 1);
const scale = Math.pow(pixels / (1920 * 1080), 0.95);
const base = 3_000_000 * scale * (CODEC_EFFICIENCY[codec] ?? 1);
return Math.ceil((base * factor) / 1000) * 1000;
}
+121 -14
View File
@@ -1,32 +1,139 @@
import { canEncodeVideo, getEncodableVideoCodecs, type VideoCodec } from "mediabunny";
import { canEncodeVideo, type VideoCodec } from "mediabunny";
export interface VideoCodecSupport {
import { VIDEO_CODEC_CANDIDATES } from "./bitrate";
export interface VideoEncoderProbe {
codec: VideoCodec;
supported: boolean;
/** A hardware encoder responds for 1080p (a hint, never a guarantee). */
hardware: boolean;
/** A tiny real encode produced samples — the only reliable check. */
works: boolean;
}
const VIDEO_CODEC_CANDIDATES: VideoCodec[] = ["avc", "hevc", "vp9", "av1"];
const CACHE_KEY = "j621.optimize.encoder-probe";
const CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1000; // re-probe monthly
const CODEC_STRINGS: Record<string, string> = {
avc: "avc1.42E01E",
hevc: "hvc1.1.6.L93.B0",
vp9: "vp09.00.10.08",
av1: "av01.0.04M.08",
};
/** Which codecs this browser can encode, and whether hardware is preferred. */
export async function detectVideoCodecs(): Promise<VideoCodecSupport[]> {
const supported = new Set(await getEncodableVideoCodecs());
const results: VideoCodecSupport[] = [];
for (const codec of VIDEO_CODEC_CANDIDATES) {
const encodable = supported.has(codec);
let hardware = false;
if (encodable) {
const ALL_CODECS: VideoCodec[] = ["avc", "hevc", "vp9", "av1"];
interface CacheEntry {
at: number;
probes: VideoEncoderProbe[];
}
function readCache(): VideoEncoderProbe[] | null {
try {
const raw = window.localStorage.getItem(CACHE_KEY);
if (!raw) return null;
const entry = JSON.parse(raw) as CacheEntry;
if (!Array.isArray(entry.probes)) return null;
if (Date.now() - entry.at > CACHE_TTL_MS) return null;
return entry.probes;
} catch {
return null;
}
}
function writeCache(probes: VideoEncoderProbe[]): void {
try {
const entry: CacheEntry = { at: Date.now(), probes };
window.localStorage.setItem(CACHE_KEY, JSON.stringify(entry));
} catch {
// Storage disabled: probing just runs again next time.
}
}
/** Encoders that this browser can actually produce video samples with. */
export async function probeVideoEncoders(
force = false,
): Promise<VideoEncoderProbe[]> {
if (!force) {
const cached = readCache();
if (cached) return cached;
}
const probes: VideoEncoderProbe[] = [];
for (const codec of ALL_CODECS) {
const supported = await canEncodeVideo(codec, {
width: 640,
height: 360,
}).catch(() => false);
let hardware = false;
let works = false;
if (supported) {
hardware = await canEncodeVideo(codec, {
width: 1920,
height: 1080,
hardwareAcceleration: "prefer-hardware",
}).catch(() => false);
works = await testEncode(codec);
}
probes.push({ codec, supported, hardware, works });
}
writeCache(probes);
return probes;
}
/** Some browsers claim support and then emit nothing; do a real micro-encode. */
async function testEncode(codec: VideoCodec): Promise<boolean> {
const codecString = CODEC_STRINGS[codec];
if (typeof VideoEncoder === "undefined" || !codecString) return false;
let chunks = 0;
let failed = false;
const encoder = new VideoEncoder({
output: () => {
chunks += 1;
},
error: () => {
failed = true;
},
});
try {
const config: VideoEncoderConfig = {
codec: codecString,
width: 640,
height: 360,
bitrate: 600_000,
framerate: 30,
};
const support = await VideoEncoder.isConfigSupported(config);
if (!support.supported) return false;
encoder.configure(config);
const canvas = new OffscreenCanvas(640, 360);
const context = canvas.getContext("2d");
if (!context) return false;
for (let index = 0; index < 3; index += 1) {
context.fillStyle = index % 2 === 0 ? "#224466" : "#cc8844";
context.fillRect(0, 0, 640, 360);
const frame = new VideoFrame(canvas, { timestamp: index * 33_333 });
encoder.encode(frame, { keyFrame: index === 0 });
frame.close();
}
await encoder.flush();
return !failed && chunks > 0;
} catch {
hardware = false;
return false;
} finally {
try {
encoder.close();
} catch {
// Already closed by the error path.
}
}
results.push({ codec, supported: encodable, hardware });
}
return results;
export function workingCodecs(
probes: VideoEncoderProbe[],
container: "mp4" | "webm",
): VideoCodec[] {
const candidates = VIDEO_CODEC_CANDIDATES[container];
return probes
.filter((probe) => probe.supported && probe.works)
.map((probe) => probe.codec)
.filter((codec) => candidates.includes(codec));
}
@@ -20,7 +20,13 @@ scope.onmessage = async (event: MessageEvent<OptimizeRequest>) => {
if (kind === "video") {
// Mediabunny streams the source with range requests — never download a
// whole video into memory.
result = await optimizeVideo(url, filename, options, report);
result = await optimizeVideo(
url,
filename,
options,
report,
event.data.preferredCodecs ?? [],
);
} else {
report(0.01, "Downloading the original…");
const response = await fetch(url);
+7 -12
View File
@@ -1,17 +1,12 @@
/** Shared types for the client-side optimization pipeline. */
import type { VideoCodec } from "mediabunny";
export type OptimizeKind = "image" | "animation" | "video";
export type VideoQualityLevel =
| "very-high"
| "high"
| "medium"
| "low"
| "very-low";
export interface OptimizeOptions {
// Images (jpg / png / webp)
quality: number; // 1-100 for lossy formats
// Images (jpg / png / webp) and video quality (1-100)
quality: number;
pngLevel: number; // 0-6, higher = smaller/slower
maxDimension: number | null; // longest edge in px, null keeps the size
// Animations (gif / apng)
@@ -19,8 +14,7 @@ export interface OptimizeOptions {
fps: number | null; // frame-rate cap
colors: number; // palette size
// Video (mp4 / webm)
codec: "auto" | "avc" | "hevc" | "vp9" | "av1";
qualityLevel: VideoQualityLevel;
codec: "auto" | VideoCodec;
container: "keep" | "mp4" | "webm";
height: number | null; // target height, null keeps the size
preferHardware: boolean;
@@ -34,7 +28,6 @@ export const DEFAULT_OPTIONS: OptimizeOptions = {
fps: null,
colors: 192,
codec: "auto",
qualityLevel: "high",
container: "keep",
height: null,
preferHardware: true,
@@ -45,6 +38,8 @@ export interface OptimizeRequest {
filename: string;
kind: OptimizeKind;
options: OptimizeOptions;
/** Codecs that actually produced samples on this machine, best first. */
preferredCodecs?: VideoCodec[];
}
export interface OptimizeResult {
+19 -9
View File
@@ -11,6 +11,7 @@ import {
} from "mediabunny";
import { assertPlayable } from "./mp4";
import { VIDEO_CODEC_CANDIDATES } from "./bitrate";
import { createStreamOutput, type StreamOutput } from "./stream";
import {
extensionOf,
@@ -19,11 +20,6 @@ import {
type ProgressReporter,
} from "./types";
const CODEC_CANDIDATES: Record<"mp4" | "webm", VideoCodec[]> = {
mp4: ["avc", "hevc"],
webm: ["vp9", "av1"],
};
const PREFERENCE_LABEL: Record<string, string> = {
"prefer-hardware": "hardware",
"prefer-software": "software",
@@ -45,6 +41,7 @@ export async function optimizeVideo(
filename: string,
options: OptimizeOptions,
report: ProgressReporter,
preferredCodecs: VideoCodec[] = [],
): Promise<OptimizeResult> {
if (typeof VideoEncoder === "undefined") {
throw new Error(
@@ -89,11 +86,21 @@ export async function optimizeVideo(
// per-frame quantizer that qualitative presets would otherwise use, which
// made results inconsistent (and sometimes tiny).
const quality = new Quality({
quality: options.qualityLevel,
quality: Math.min(Math.max(options.quality / 100, 0.05), 1),
preferBitrate: true,
});
const containerCodecs = VIDEO_CODEC_CANDIDATES[container];
const codecs: VideoCodec[] =
options.codec === "auto" ? CODEC_CANDIDATES[container] : [options.codec];
options.codec === "auto"
? [
...new Set([
...preferredCodecs.filter((codec) =>
containerCodecs.includes(codec),
),
...containerCodecs,
]),
]
: [options.codec];
const preferences: Attempt["preference"][] = options.preferHardware
? ["prefer-hardware", "prefer-software"]
: ["prefer-software"];
@@ -178,9 +185,12 @@ export async function optimizeVideo(
}
}
const hint =
container === "mp4"
? "This browser's H.264/HEVC encoders produced no video samples — switch the Container to WebM (VP9) instead."
: "Try another codec or a lower resolution.";
throw new Error(
`Video encoding failed on every configuration (${issues.join("; ")}). ` +
"Try WebM output or a lower resolution.",
`Video encoding failed on every configuration (${issues.join("; ")}). ${hint}`,
);
}