Files
ausboss-ComfyUI-AusBoss/js/shared/video_preview.mjs
ausboss 0c404b37f3 Save Video: a saved gif or webp shows on the node
With format set to gif or webp the file was saved, but the node's stage
stayed empty and said "Execution finished without video metadata".

The node has shown a gif or webp in an <img> since the two formats were
added, because a <video> cannot play either. The run's result was still
searched with a list of video file endings only, so for these two nothing
was found and the <img> was never filled. The list of picture endings now
sits beside that lookup in js/shared/video_preview.mjs, and both the
lookup and the choice of tag read it. A png is still not taken.

In the classic renderer a saved webp was also drawn a second time under
the stage. ComfyUI shows an animated webp in a widget of its own, neither
its video preview nor its picture preview, and only those two were stood
down. suppressCoreVideoPreview hides that widget as well. The node's
smallest height with a saved webp goes from 469 px back to 415 px.

Tests: the lookup finds a saved gif and webp and sends them to the <img>;
every row of the backend's VIDEO_FORMATS is read from its source and has
to be found and sent to the right tag; the animated widget is hidden on
arrival and when it is already on the node.
2026-10-06 17:35:31 -05:00

98 lines
3.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const DEFAULT_WIDTH = 320;
export function finiteNumber(value, fallback = 0) {
const number = Number(value);
return Number.isFinite(number) ? number : fallback;
}
export function clamp(value, minimum, maximum) {
return Math.max(minimum, Math.min(maximum, value));
}
export function formatTime(secondsValue, precision = 2) {
const seconds = Math.max(0, finiteNumber(secondsValue, 0));
const minutes = Math.floor(seconds / 60);
const remainder = seconds - minutes * 60;
const fixed = remainder.toFixed(precision);
return `${minutes}:${remainder < 10 ? "0" : ""}${fixed}`;
}
export function responsivePreviewHeight(
widthValue,
minimum = 112,
maximum = 420,
) {
const width = Math.max(0, finiteNumber(widthValue, DEFAULT_WIDTH));
const scaled = Math.round(Math.max(0, width - 12) * 9 / 16);
return clamp(scaled, minimum, maximum);
}
export function splitMediaName(nameValue) {
const raw = typeof nameValue === "string" ? nameValue : "";
const annotated = raw.replace(/\s+\[(input|output|temp)\]\s*$/i, "");
const normalized = annotated.replaceAll("\\", "/");
const slash = normalized.lastIndexOf("/");
return slash < 0
? { filename: normalized, subfolder: "" }
: { filename: normalized.slice(slash + 1), subfolder: normalized.slice(0, slash) };
}
export function mediaViewQuery(metaValue, fallbackType = "output") {
const meta = typeof metaValue === "string"
? { ...splitMediaName(metaValue), type: fallbackType }
: (metaValue || {});
return new URLSearchParams({
filename: String(meta.filename || ""),
subfolder: String(meta.subfolder || ""),
type: String(meta.type || fallbackType),
t: String(Date.now()),
}).toString();
}
// Every file Save Video can save, by the tag that shows it. gif and webp are
// animated images, not video: a <video> element cannot play either, so the
// same saved file needs an <img> to preview at all. The two lists once sat
// in two files and only the first was asked whether a run had saved
// anything, so a saved gif or webp was never shown.
// tests/video_preview.test.mjs holds both to the formats the backend writes.
const VIDEO_FILE = /\.(mp4|webm|mov|mkv|avi|m4v)$/i;
const STILL_IMAGE_FILE = /\.(gif|webp)$/i;
export function isStillImage(filename) {
return typeof filename === "string" && STILL_IMAGE_FILE.test(filename);
}
export function findVideoMetadata(value, seen = new Set()) {
if (!value || typeof value !== "object" || seen.has(value)) return null;
seen.add(value);
if (typeof value.filename === "string" && (VIDEO_FILE.test(value.filename) || isStillImage(value.filename))) {
return value;
}
if (Array.isArray(value)) {
for (const item of value) {
const found = findVideoMetadata(item, seen);
if (found) return found;
}
return null;
}
for (const item of Object.values(value)) {
const found = findVideoMetadata(item, seen);
if (found) return found;
}
return null;
}
export function mediaInfo(meta, video) {
const width = finiteNumber(meta?.width, video?.videoWidth || 0);
const height = finiteNumber(meta?.height, video?.videoHeight || 0);
const fps = finiteNumber(meta?.fps ?? meta?.frame_rate, 0);
const frames = finiteNumber(meta?.frame_count, 0);
const duration = finiteNumber(meta?.duration, video?.duration || 0);
const parts = [];
if (width > 0 && height > 0) parts.push(`${Math.round(width)}×${Math.round(height)}`);
if (fps > 0) parts.push(`${Math.round(fps * 100) / 100} fps`);
if (frames > 0) parts.push(`${Math.round(frames)} frames`);
if (duration > 0) parts.push(formatTime(duration, 1));
return parts.join(" · ");
}