diff --git a/frontend/src/features/library/LibraryDetail.tsx b/frontend/src/features/library/LibraryDetail.tsx index b5f8efa..4a1e2cd 100644 --- a/frontend/src/features/library/LibraryDetail.tsx +++ b/frontend/src/features/library/LibraryDetail.tsx @@ -708,10 +708,10 @@ export function LibraryDetail({ jId }: { jId: string }) {

- + {user ? ( - + ) : null}
diff --git a/frontend/src/features/optimize/video.ts b/frontend/src/features/optimize/video.ts index 3906eca..820c0ef 100644 --- a/frontend/src/features/optimize/video.ts +++ b/frontend/src/features/optimize/video.ts @@ -8,7 +8,6 @@ import { StreamTarget, UrlSource, WebMOutputFormat, - getFirstEncodableVideoCodec, type StreamTargetChunk, type VideoCodec, } from "mediabunny"; @@ -125,18 +124,24 @@ export async function optimizeVideo( options: OptimizeOptions, report: ProgressReporter, ): Promise { + if (typeof VideoEncoder === "undefined") { + throw new Error( + "This browser does not expose WebCodecs (VideoEncoder) inside workers.", + ); + } const extension = extensionOf(filename); report(0.03, "Reading the video…"); - const input = new Input({ + const probe = new Input({ source: new UrlSource(url, { maxCacheSize: 4 * 1024 * 1024 }), formats: ALL_FORMATS, }); - const track = await input.getPrimaryVideoTrack(); + const track = await probe.getPrimaryVideoTrack(); if (!track) { throw new Error("No video track found in this file."); } const sourceWidth = await track.getDisplayWidth(); const sourceHeight = await track.getDisplayHeight(); + probe.dispose?.(); const container: "mp4" | "webm" = options.container === "keep" @@ -147,53 +152,69 @@ export async function optimizeVideo( const format = container === "webm" ? new WebMOutputFormat() : new Mp4OutputFormat(); + const outputHeight = even(options.height ?? sourceHeight); + const outputWidth = even( + Math.round(sourceWidth * (outputHeight / sourceHeight)), + ); const quality = new Quality(options.qualityLevel); - let codec: VideoCodec | undefined = - options.codec === "auto" ? undefined : options.codec; - if (!codec) { - const candidate = await getFirstEncodableVideoCodec( - CODEC_CANDIDATES[container], - { width: sourceWidth, height: sourceHeight, quality }, - ); - if (!candidate) { - throw new Error( - `This browser cannot encode ${container.toUpperCase()} video.`, - ); - } - codec = candidate; - } - - const outputHeight = options.height ?? sourceHeight; - const outputWidth = Math.round(sourceWidth * (outputHeight / sourceHeight)); - - const { target, toBlob } = createStreamOutput(); - const output = new Output({ format, target }); + const candidates: VideoCodec[] = + options.codec === "auto" ? CODEC_CANDIDATES[container] : [options.codec]; + const preferences: ("prefer-hardware" | "no-preference")[] = + options.preferHardware ? ["prefer-hardware", "no-preference"] : ["no-preference"]; report(0.06, "Preparing the encoder…"); - const conversion = await Conversion.init({ - input, - output, - video: { - codec, - quality, - height: even(outputHeight), - fit: "contain", - hardwareAcceleration: options.preferHardware - ? "prefer-hardware" - : "no-preference", - forceTranscode: true, - }, - }); - if (!conversion.isValid) { + let winner: + | { + conversion: Conversion; + input: Input; + stream: ReturnType; + codec: VideoCodec; + } + | null = null; + const tried: string[] = []; + for (const codec of candidates) { + for (const preference of preferences) { + tried.push(`${codec}${preference === "prefer-hardware" ? " (hw)" : ""}`); + const input = new Input({ + source: new UrlSource(url, { maxCacheSize: 4 * 1024 * 1024 }), + formats: ALL_FORMATS, + }); + const stream = createStreamOutput(); + const output = new Output({ format, target: stream.target }); + const attempt = await Conversion.init({ + input, + output, + video: { + codec, + quality, + width: outputWidth, + height: outputHeight, + fit: "contain", + hardwareAcceleration: preference, + forceTranscode: true, + }, + }); + if (attempt.isValid) { + winner = { conversion: attempt, input, stream, codec }; + break; + } + input.dispose?.(); + } + if (winner) break; + } + if (!winner) { throw new Error( - "This conversion is not supported here — try another codec or container.", + `This browser could not configure ${container.toUpperCase()} encoding for ` + + `${outputWidth}×${outputHeight} (tried ${tried.join(", ")}). ` + + "Try another codec, container or resolution.", ); } - conversion.onProgress = (progress: number) => { + + winner.conversion.onProgress = (progress: number) => { report(0.08 + Math.min(Math.max(progress, 0), 1) * 0.85, "Encoding…"); }; try { - await conversion.execute(); + await winner.conversion.execute(); } catch (error) { throw new Error( `Video encoding failed: ${ @@ -201,14 +222,14 @@ export async function optimizeVideo( }`, ); } finally { - input.dispose?.(); + winner.input.dispose?.(); } report(0.97, "Packaging…"); return { - blob: toBlob(container === "webm" ? "video/webm" : "video/mp4"), - width: even(outputWidth), - height: even(outputHeight), - note: `${codec} · ${container}`, + blob: winner.stream.toBlob(container === "webm" ? "video/webm" : "video/mp4"), + width: outputWidth, + height: outputHeight, + note: `${winner.codec} · ${container}`, }; }