import { canEncodeVideo, type VideoCodec } from "mediabunny"; 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 CACHE_KEY = "j621.optimize.encoder-probe"; const CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1000; // re-probe monthly const CODEC_STRINGS: Record = { avc: "avc1.42E01E", hevc: "hvc1.1.6.L93.B0", vp9: "vp09.00.10.08", av1: "av01.0.04M.08", }; 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 { 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 { 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 { return false; } finally { try { encoder.close(); } catch { // Already closed by the error path. } } } 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)); } /** Human-readable browser + platform, for the encoder diagnostics line. */ export function browserLabel(): string { const ua = navigator.userAgent; const patterns: [RegExp, string][] = [ [/(Firefox|FxiOS)\/([\d.]+)/, "Firefox"], [/(Edg|EdgA|EdgiOS)\/([\d.]+)/, "Edge"], [/(OPR)\/([\d.]+)/, "Opera"], [/(Chrome|CriOS)\/([\d.]+)/, "Chrome"], [/(Version)\/([\d.]+)[^)]*Safari/, "Safari"], ]; for (const [pattern, name] of patterns) { const match = pattern.exec(ua); if (match) { const version = match[2]?.split(".")[0] ?? ""; return `${name}${version ? ` ${version}` : ""}`; } } return "unknown browser"; }