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}`,
};
}