fix(cli): show output video length in render summary, not render time (#1812)

The render-complete summary printed `<fileSize> · <time> · completed`
where `<time>` was the wall-clock render duration. Presented as a bare
middle value, users read it as the video length and compared it to
ffprobe, repeatedly reporting a "wrong duration".

Show the actual output video length (from the perf summary's
compositionDurationSeconds, which equals the rendered frame span) as the
primary figure and label the render time explicitly:
`<fileSize> · <videoLength> video · rendered in <renderTime>`.

png-sequence (directory) output has no single muxed video, so it shows a
frame count instead; when neither is known the summary falls back to
render time only. Docker renders run the producer in a child process
with no perf summary threaded back, so they show render time only rather
than a misleading number.
This commit is contained in:
Miguel Ángel
2026-06-30 10:43:27 -07:00
committed by GitHub
parent 466ee08ffa
commit e7939ccd53
3 changed files with 94 additions and 5 deletions
+27 -5
View File
@@ -55,7 +55,7 @@ import { lintProject, shouldBlockRender } from "../utils/lintProject.js";
import { formatLintFindings } from "../utils/lintFormat.js";
import { loadProducer } from "../utils/producer.js";
import { c } from "../ui/colors.js";
import { formatBytes, formatDuration, errorBox } from "../ui/format.js";
import { formatBytes, formatRenderSummaryDetail, errorBox } from "../ui/format.js";
import { renderProgress } from "../ui/progress.js";
import {
trackRenderComplete,
@@ -1227,6 +1227,9 @@ async function renderDocker(
...getMemorySnapshot(),
});
// ponytail: Docker runs the producer in a child process, so no perfSummary is
// threaded back here; the summary shows render time only (never a wrong video
// length). Probe the output with ffprobe if a duration figure is wanted here.
printRenderComplete(outputPath, elapsed, options.quiet);
if (options.exitAfterComplete) scheduleRenderProcessExit();
return { renderTimeMs: elapsed };
@@ -1323,7 +1326,13 @@ export async function renderLocal(
const elapsed = Date.now() - startTime;
trackRenderMetrics(job, elapsed, options, false);
printRenderComplete(outputPath, elapsed, options.quiet);
printRenderComplete(
outputPath,
elapsed,
options.quiet,
job.perfSummary?.compositionDurationSeconds,
job.perfSummary?.totalFrames,
);
if (!options.skipFeedback) {
await maybePromptRenderFeedback({
renderDurationMs: elapsed,
@@ -1546,12 +1555,20 @@ function trackRenderMetrics(
});
}
function printRenderComplete(outputPath: string, elapsedMs: number, quiet: boolean): void {
function printRenderComplete(
outputPath: string,
elapsedMs: number,
quiet: boolean,
outputDurationSeconds?: number,
frameCount?: number,
): void {
if (quiet) return;
let fileSize = "unknown";
let isDirectory = false;
try {
const stat = statSync(outputPath);
isDirectory = stat.isDirectory();
if (stat.isDirectory()) {
// png-sequence output is a directory; sum the contained file sizes so
// the user sees the on-disk footprint of the deliverable rather than
@@ -1573,8 +1590,13 @@ function printRenderComplete(outputPath: string, elapsedMs: number, quiet: boole
// file doesn't exist or is inaccessible
}
const duration = formatDuration(elapsedMs);
const detail = formatRenderSummaryDetail({
elapsedMs,
outputDurationSeconds,
isDirectory,
frameCount,
});
console.log("");
console.log(c.success("\u25C7") + " " + c.accent(outputPath));
console.log(" " + c.bold(fileSize) + c.dim(" \u00B7 " + duration + " \u00B7 completed"));
console.log(" " + c.bold(fileSize) + c.dim(" \u00B7 " + detail));
}
+42
View File
@@ -0,0 +1,42 @@
import { describe, expect, it } from "vitest";
import { formatRenderSummaryDetail } from "./format.js";
describe("formatRenderSummaryDetail", () => {
it("shows the output video length as the primary figure and labels render time", () => {
// Output is 71.7s of video but only took 34.2s of wall-clock to render:
// the two must be distinguishable so users stop comparing render time to ffprobe.
const detail = formatRenderSummaryDetail({
elapsedMs: 34_200,
outputDurationSeconds: 71.7,
isDirectory: false,
});
expect(detail).toBe("1m 11.7s video · rendered in 34.2s");
expect(detail).toContain("video");
expect(detail).toContain("rendered in");
});
it("omits the video figure gracefully when the duration is unknown", () => {
expect(formatRenderSummaryDetail({ elapsedMs: 5_000, isDirectory: false })).toBe(
"rendered in 5.0s",
);
});
it("shows a frame count for png-sequence directory output instead of a video length", () => {
const detail = formatRenderSummaryDetail({
elapsedMs: 12_000,
isDirectory: true,
frameCount: 120,
// a stray duration must not leak into directory output
outputDurationSeconds: 4,
});
expect(detail).toBe("120 frames · rendered in 12.0s");
expect(detail).not.toContain("video");
});
it("does not crash and shows only render time for a directory with no frame count", () => {
expect(formatRenderSummaryDetail({ elapsedMs: 1_000, isDirectory: true })).toBe(
"rendered in 1.0s",
);
});
});
+25
View File
@@ -14,6 +14,31 @@ export function formatDuration(ms: number): string {
return `${minutes}m ${remaining.toFixed(1)}s`;
}
/**
* Build the detail portion of the render-complete summary (everything after the
* file size). The output video length is shown as the primary figure, with the
* wall-clock render time explicitly labeled "rendered in" so the two are never
* confused (users were comparing the render time to ffprobe's media duration).
* Directory (png-sequence) output has no single muxed video, so it shows a frame
* count instead, or just the render time when neither is known.
*/
export function formatRenderSummaryDetail(input: {
elapsedMs: number;
outputDurationSeconds?: number;
isDirectory: boolean;
frameCount?: number;
}): string {
const middle = input.isDirectory
? input.frameCount != null
? `${input.frameCount} frames`
: undefined
: input.outputDurationSeconds != null && input.outputDurationSeconds > 0
? `${formatDuration(input.outputDurationSeconds * 1000)} video`
: undefined;
const renderTime = `rendered in ${formatDuration(input.elapsedMs)}`;
return [middle, renderTime].filter(Boolean).join(" · ");
}
export function label(name: string, value: string): string {
const pad = 14 - name.length;
return ` ${c.dim(name)}${" ".repeat(Math.max(1, pad))}${c.bold(value)}`;