Fix duplicate React keys and the MP4 encoder probe
- MatchCard and IqdbCard are siblings in the library detail aside and
both used key={item.j_id}, so React warned about duplicate children
(J-81 twice). Their keys are now unique per card.
- The video pipeline no longer decides "this browser can't encode" from
a single getFirstEncodableVideoCodec probe with source dimensions.
It now probes Conversion.init with the real (even) output size across
codec candidates and hardware preferences, uses the first valid
configuration, and reports exactly what was tried when nothing works.
It also fails fast with a clear message if VideoEncoder is missing in
the worker.
This commit is contained in:
@@ -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<OptimizeResult> {
|
||||
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<typeof createStreamOutput>;
|
||||
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}`,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user