From fed1e8bb5a4b6a53a6fde599ff9c209e1555ec51 Mon Sep 17 00:00:00 2001 From: JakeBreath Date: Thu, 17 Sep 2026 20:45:12 -0500 Subject: [PATCH] Fix chunk assembly order and surface video duration in the optimizer - The streaming assembler applied chunks sorted by position, so header patches written last could be overwritten by earlier data. Chunks now apply in arrival order (newest write wins per byte range) and are only laid out by position afterwards. - The Optimize modal now reads the media metadata itself: both previews show resolution and duration, and the processed video is flagged in red when it comes out shorter than the original (a truncated encode is no longer something you have to guess by file size). --- .../src/features/optimize/OptimizeModal.tsx | 68 ++++++++++++++++--- frontend/src/features/optimize/video.ts | 10 +-- 2 files changed, 63 insertions(+), 15 deletions(-) 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[] = [];