From a67b5b62ae84f526f13f4566da491c003650602b Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Thu, 12 Feb 2026 23:54:29 +0100 Subject: [PATCH] Add frontend JS for TweenConcatVideos video preview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gifs ui dict requires custom JS to render — ComfyUI doesn't handle it natively. Adds a minimal video preview widget that uses ComfyUI's /view endpoint to play the concatenated video on the node. Co-Authored-By: Claude Opus 4.6 --- __init__.py | 2 ++ web/js/tween_preview.js | 72 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 web/js/tween_preview.js diff --git a/__init__.py b/__init__.py index 3293d11..faf41d6 100644 --- a/__init__.py +++ b/__init__.py @@ -43,6 +43,8 @@ from .nodes import ( LoadSGMVFIModel, SGMVFIInterpolate, SGMVFISegmentInterpolate, ) +WEB_DIRECTORY = "./web" + NODE_CLASS_MAPPINGS = { "LoadBIMVFIModel": LoadBIMVFIModel, "BIMVFIInterpolate": BIMVFIInterpolate, diff --git a/web/js/tween_preview.js b/web/js/tween_preview.js new file mode 100644 index 0000000..fe91217 --- /dev/null +++ b/web/js/tween_preview.js @@ -0,0 +1,72 @@ +import { app } from "../../scripts/app.js"; +import { api } from "../../scripts/api.js"; + +function fitHeight(node) { + node.setSize([node.size[0], node.computeSize([node.size[0], node.size[1]])[1]]); + node?.graph?.setDirtyCanvas(true); +} + +app.registerExtension({ + name: "Tween.VideoPreview", + async beforeRegisterNodeDef(nodeType, nodeData) { + if (nodeData?.name !== "TweenConcatVideos") return; + + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + onNodeCreated?.apply(this, arguments); + + const container = document.createElement("div"); + const previewWidget = this.addDOMWidget("videopreview", "preview", container, { + serialize: false, + hideOnZoom: false, + getValue() { return container.value; }, + setValue(v) { container.value = v; }, + }); + + previewWidget.computeSize = function (width) { + if (this.aspectRatio && !this.videoEl.hidden) { + const height = (previewNode.size[0] - 20) / this.aspectRatio + 10; + return [width, height > 0 ? height : -4]; + } + return [width, -4]; + }; + + const previewNode = this; + + previewWidget.videoEl = document.createElement("video"); + previewWidget.videoEl.controls = true; + previewWidget.videoEl.loop = true; + previewWidget.videoEl.muted = true; + previewWidget.videoEl.style.width = "100%"; + previewWidget.videoEl.hidden = true; + + previewWidget.videoEl.addEventListener("loadedmetadata", () => { + previewWidget.aspectRatio = previewWidget.videoEl.videoWidth / previewWidget.videoEl.videoHeight; + fitHeight(previewNode); + }); + previewWidget.videoEl.addEventListener("error", () => { + previewWidget.videoEl.hidden = true; + fitHeight(previewNode); + }); + + container.appendChild(previewWidget.videoEl); + }; + + const onExecuted = nodeType.prototype.onExecuted; + nodeType.prototype.onExecuted = function (message) { + onExecuted?.apply(this, arguments); + + if (!message?.gifs?.length) return; + + const params = message.gifs[0]; + const previewWidget = this.widgets?.find((w) => w.name === "videopreview"); + if (!previewWidget) return; + + const query = new URLSearchParams(params); + query.set("timestamp", Date.now()); + previewWidget.videoEl.src = api.apiURL("/view?" + query); + previewWidget.videoEl.hidden = false; + previewWidget.videoEl.autoplay = true; + }; + }, +});