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).
This commit is contained in:
2026-09-17 20:45:12 -05:00
parent 58f030878e
commit fed1e8bb5a
2 changed files with 63 additions and 15 deletions
@@ -68,23 +68,36 @@ function formatEta(ms: number): string {
return `${minutes} min left`; return `${minutes} min left`;
} }
interface MediaInfo {
width: number;
height: number;
duration: number | null;
}
function PreviewPanel({ function PreviewPanel({
title, title,
url, url,
isVideo, isVideo,
size, size,
dimensions,
note, note,
state, state,
onMetadata,
}: { }: {
title: string; title: string;
url: string | null; url: string | null;
isVideo: boolean; isVideo: boolean;
size: number | null; size: number | null;
dimensions: { width: number | null; height: number | null } | null;
note?: string | null; note?: string | null;
state: "original" | "result" | "empty"; state: "original" | "result" | "empty";
onMetadata?: (info: MediaInfo) => void;
}) { }) {
const [info, setInfo] = useState<MediaInfo | null>(null);
function report(next: MediaInfo) {
setInfo(next);
onMetadata?.(next);
}
return ( return (
<section className="flex flex-col overflow-hidden rounded-lg border border-ctp-surface0 bg-ctp-base"> <section className="flex flex-col overflow-hidden rounded-lg border border-ctp-surface0 bg-ctp-base">
<div className="flex items-center justify-between border-b border-ctp-surface0 px-3 py-1.5"> <div className="flex items-center justify-between border-b border-ctp-surface0 px-3 py-1.5">
@@ -99,18 +112,38 @@ function PreviewPanel({
{url && state !== "empty" ? ( {url && state !== "empty" ? (
isVideo ? ( isVideo ? (
<video <video
key={url}
src={url} src={url}
controls controls
loop loop
muted muted
preload="metadata" preload="metadata"
className="max-h-56 w-full object-contain" className="max-h-56 w-full object-contain"
onLoadedMetadata={(event) => {
const element = event.currentTarget;
report({
width: element.videoWidth,
height: element.videoHeight,
duration: Number.isFinite(element.duration)
? element.duration
: null,
});
}}
/> />
) : ( ) : (
<img <img
key={url}
src={url} src={url}
alt={title} alt={title}
className="max-h-56 w-full object-contain" className="max-h-56 w-full object-contain"
onLoad={(event) => {
const element = event.currentTarget;
report({
width: element.naturalWidth,
height: element.naturalHeight,
duration: null,
});
}}
/> />
) )
) : ( ) : (
@@ -122,8 +155,10 @@ function PreviewPanel({
)} )}
</div> </div>
<div className="px-3 py-2 text-[11px] text-ctp-overlay0"> <div className="px-3 py-2 text-[11px] text-ctp-overlay0">
{dimensions?.width && dimensions?.height {info
? `${dimensions.width}×${dimensions.height}` ? `${info.width}×${info.height}${
info.duration !== null ? ` · ${info.duration.toFixed(1)} s` : ""
}`
: "…"} : "…"}
{note ? ` · ${note}` : ""} {note ? ` · ${note}` : ""}
</div> </div>
@@ -150,6 +185,8 @@ export function OptimizeModal({
const [uploadPercent, setUploadPercent] = useState(0); const [uploadPercent, setUploadPercent] = useState(0);
const [applyError, setApplyError] = useState<string | null>(null); const [applyError, setApplyError] = useState<string | null>(null);
const [applied, setApplied] = useState(false); const [applied, setApplied] = useState(false);
const [originalInfo, setOriginalInfo] = useState<MediaInfo | null>(null);
const [resultInfo, setResultInfo] = useState<MediaInfo | null>(null);
const codecQuery = useQuery({ const codecQuery = useQuery({
queryKey: ["optimize-codecs"], queryKey: ["optimize-codecs"],
@@ -204,10 +241,16 @@ export function OptimizeModal({
const hardwareAvailable = const hardwareAvailable =
codecQuery.data?.some((entry) => entry.supported && entry.hardware) ?? codecQuery.data?.some((entry) => entry.supported && entry.hardware) ??
false; false;
const truncated =
isVideo &&
originalInfo?.duration != null &&
resultInfo?.duration != null &&
resultInfo.duration + 1 < originalInfo.duration;
function process() { function process() {
setApplied(false); setApplied(false);
setApplyError(null); setApplyError(null);
setResultInfo(null);
run({ run({
url: item.raw_url, url: item.raw_url,
filename: item.filename, filename: item.filename,
@@ -257,24 +300,29 @@ export function OptimizeModal({
url={item.raw_url} url={item.raw_url}
isVideo={isVideo} isVideo={isVideo}
size={originalSize} size={originalSize}
dimensions={{ width: null, height: null }}
state="original" state="original"
onMetadata={setOriginalInfo}
/> />
<PreviewPanel <PreviewPanel
title="Processed" title="Processed"
url={result?.url ?? null} url={result?.url ?? null}
isVideo={isVideo} isVideo={isVideo}
size={resultSize} size={resultSize}
dimensions={
result
? { width: result.width, height: result.height }
: null
}
note={result?.note ?? null} note={result?.note ?? null}
state={result ? "result" : "empty"} state={result ? "result" : "empty"}
onMetadata={setResultInfo}
/> />
</div> </div>
{truncated ? (
<p className="rounded-md border border-ctp-red/40 bg-ctp-red/10 px-3 py-2 text-xs text-ctp-red">
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.
</p>
) : null}
{status === "running" ? ( {status === "running" ? (
<div className="rounded-md border border-ctp-surface1 bg-ctp-base p-3"> <div className="rounded-md border border-ctp-surface1 bg-ctp-base p-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
+5 -5
View File
@@ -68,11 +68,11 @@ function assembleBlob(chunks: CollectedChunk[], type: string): Blob {
if (chunks.length === 0) { if (chunks.length === 0) {
throw new Error("Encoding produced no output."); throw new Error("Encoding produced no output.");
} }
const sorted = [...chunks].sort( // The muxer may rewrite earlier ranges (header patching), so apply writes
(a, b) => a.position - b.position || a.sequence - b.sequence, // 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 { interface Segment {
start: number; start: number;
end: number; end: number;
@@ -80,7 +80,7 @@ function assembleBlob(chunks: CollectedChunk[], type: string): Blob {
blob: Blob; blob: Blob;
} }
let segments: Segment[] = []; let segments: Segment[] = [];
for (const chunk of sorted) { for (const chunk of ordered) {
const start = chunk.position; const start = chunk.position;
const end = start + chunk.blob.size; const end = start + chunk.blob.size;
const next: Segment[] = []; const next: Segment[] = [];