diff --git a/frontend/src/features/optimize/OptimizeModal.tsx b/frontend/src/features/optimize/OptimizeModal.tsx index 0d38881..86e1088 100644 --- a/frontend/src/features/optimize/OptimizeModal.tsx +++ b/frontend/src/features/optimize/OptimizeModal.tsx @@ -68,23 +68,36 @@ function formatEta(ms: number): string { return `${minutes} min left`; } +interface MediaInfo { + width: number; + height: number; + duration: number | null; +} + function PreviewPanel({ title, url, isVideo, size, - dimensions, note, state, + onMetadata, }: { title: string; url: string | null; isVideo: boolean; size: number | null; - dimensions: { width: number | null; height: number | null } | null; note?: string | null; state: "original" | "result" | "empty"; + onMetadata?: (info: MediaInfo) => void; }) { + const [info, setInfo] = useState(null); + + function report(next: MediaInfo) { + setInfo(next); + onMetadata?.(next); + } + return (
@@ -99,18 +112,38 @@ function PreviewPanel({ {url && state !== "empty" ? ( isVideo ? (
- {dimensions?.width && dimensions?.height - ? `${dimensions.width}×${dimensions.height}` + {info + ? `${info.width}×${info.height}${ + info.duration !== null ? ` · ${info.duration.toFixed(1)} s` : "" + }` : "…"} {note ? ` · ${note}` : ""}
@@ -150,6 +185,8 @@ export function OptimizeModal({ const [uploadPercent, setUploadPercent] = useState(0); const [applyError, setApplyError] = useState(null); const [applied, setApplied] = useState(false); + const [originalInfo, setOriginalInfo] = useState(null); + const [resultInfo, setResultInfo] = useState(null); const codecQuery = useQuery({ queryKey: ["optimize-codecs"], @@ -204,10 +241,16 @@ export function OptimizeModal({ 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; function process() { setApplied(false); setApplyError(null); + setResultInfo(null); run({ url: item.raw_url, filename: item.filename, @@ -257,24 +300,29 @@ export function OptimizeModal({ url={item.raw_url} isVideo={isVideo} size={originalSize} - dimensions={{ width: null, height: null }} state="original" + onMetadata={setOriginalInfo} /> + {truncated ? ( +

+ The processed video is shorter than the original ( + {resultInfo?.duration?.toFixed(1)} s vs{" "} + {originalInfo?.duration?.toFixed(1)} s) — encoding may have + stopped early. Don't apply it. +

+ ) : null} + {status === "running" ? (
diff --git a/frontend/src/features/optimize/video.ts b/frontend/src/features/optimize/video.ts index 820c0ef..d1587f5 100644 --- a/frontend/src/features/optimize/video.ts +++ b/frontend/src/features/optimize/video.ts @@ -68,11 +68,11 @@ function assembleBlob(chunks: CollectedChunk[], type: string): Blob { if (chunks.length === 0) { throw new Error("Encoding produced no output."); } - const sorted = [...chunks].sort( - (a, b) => a.position - b.position || a.sequence - b.sequence, - ); + // The muxer may rewrite earlier ranges (header patching), so apply writes + // in arrival order — the newest write wins per byte range — and only then + // lay the segments out by position. + const ordered = [...chunks].sort((a, b) => a.sequence - b.sequence); - // Overwrite ranges so the newest write for each byte range survives. interface Segment { start: number; end: number; @@ -80,7 +80,7 @@ function assembleBlob(chunks: CollectedChunk[], type: string): Blob { blob: Blob; } let segments: Segment[] = []; - for (const chunk of sorted) { + for (const chunk of ordered) { const start = chunk.position; const end = start + chunk.blob.size; const next: Segment[] = [];