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:
@@ -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<MediaInfo | null>(null);
|
||||
|
||||
function report(next: MediaInfo) {
|
||||
setInfo(next);
|
||||
onMetadata?.(next);
|
||||
}
|
||||
|
||||
return (
|
||||
<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">
|
||||
@@ -99,18 +112,38 @@ function PreviewPanel({
|
||||
{url && state !== "empty" ? (
|
||||
isVideo ? (
|
||||
<video
|
||||
key={url}
|
||||
src={url}
|
||||
controls
|
||||
loop
|
||||
muted
|
||||
preload="metadata"
|
||||
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
|
||||
key={url}
|
||||
src={url}
|
||||
alt={title}
|
||||
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 className="px-3 py-2 text-[11px] text-ctp-overlay0">
|
||||
{dimensions?.width && dimensions?.height
|
||||
? `${dimensions.width}×${dimensions.height}`
|
||||
{info
|
||||
? `${info.width}×${info.height}${
|
||||
info.duration !== null ? ` · ${info.duration.toFixed(1)} s` : ""
|
||||
}`
|
||||
: "…"}
|
||||
{note ? ` · ${note}` : ""}
|
||||
</div>
|
||||
@@ -150,6 +185,8 @@ export function OptimizeModal({
|
||||
const [uploadPercent, setUploadPercent] = useState(0);
|
||||
const [applyError, setApplyError] = useState<string | null>(null);
|
||||
const [applied, setApplied] = useState(false);
|
||||
const [originalInfo, setOriginalInfo] = useState<MediaInfo | null>(null);
|
||||
const [resultInfo, setResultInfo] = useState<MediaInfo | null>(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}
|
||||
/>
|
||||
<PreviewPanel
|
||||
title="Processed"
|
||||
url={result?.url ?? null}
|
||||
isVideo={isVideo}
|
||||
size={resultSize}
|
||||
dimensions={
|
||||
result
|
||||
? { width: result.width, height: result.height }
|
||||
: null
|
||||
}
|
||||
note={result?.note ?? null}
|
||||
state={result ? "result" : "empty"}
|
||||
onMetadata={setResultInfo}
|
||||
/>
|
||||
</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" ? (
|
||||
<div className="rounded-md border border-ctp-surface1 bg-ctp-base p-3">
|
||||
<div className="flex items-center gap-3">
|
||||
|
||||
Reference in New Issue
Block a user