Files

174 lines
4.8 KiB
TypeScript

/**
* Shared video widget utilities
* Used by both cryptioSaveVideo.ts and cryptioUploadVideo.ts
*/
//@ts-ignore
import { app as rawApp } from "../../../scripts/app.js";
import type { ComfyApp } from "@comfyorg/comfyui-frontend-types";
import type { CryptIONode, VideoPreviewWidget, CryptIOApp } from "../types.js";
const app: CryptIOApp = rawApp as any;
export const DEFAULT_VIDEO_SIZE = 256;
const MIN_WIDGET_HEIGHT = 64;
export function fitDimensionsToNodeWidth(
width: number,
height: number,
nodeWidth: number
) {
const aspectRatio = width / height;
if (!aspectRatio || Number.isNaN(aspectRatio)) {
return {
minHeight: DEFAULT_VIDEO_SIZE,
minWidth: nodeWidth || DEFAULT_VIDEO_SIZE,
};
}
const minWidth = nodeWidth || DEFAULT_VIDEO_SIZE;
const minHeight = Math.max(minWidth / aspectRatio, MIN_WIDGET_HEIGHT);
return { minHeight, minWidth };
}
export interface CreateVideoWidgetOptions {
widgetName?: string;
hidden?: boolean;
videoInfo?: any;
}
/**
* Create a video preview DOM widget on a node.
*/
export function createVideoWidget(node: CryptIONode, options: CreateVideoWidgetOptions = {}): VideoPreviewWidget {
const {
widgetName = "video_preview",
hidden = false,
videoInfo,
} = options;
let minHeight = DEFAULT_VIDEO_SIZE;
let minWidth = node.size?.[0] || DEFAULT_VIDEO_SIZE;
const container = document.createElement("div");
container.classList.add("comfy-img-preview");
const widget = node.addDOMWidget(
widgetName,
"video-preview",
container,
{
serialize: false,
hideOnZoom: false,
}
);
widget.computeLayoutSize = () => ({
minHeight,
minWidth,
});
widget.updateLayout = (videoWidth: number, videoHeight: number) => {
const nodeWidth = node.size?.[0] || DEFAULT_VIDEO_SIZE;
const dimensions = fitDimensionsToNodeWidth(
videoWidth,
videoHeight,
nodeWidth
);
minHeight = dimensions.minHeight;
minWidth = dimensions.minWidth;
};
// Forward middle-button events to the canvas for panning
widget.element.addEventListener('pointermove', (event: PointerEvent) => {
if ((event.buttons & 4) === 4) {
app.canvas.processMouseMove(event);
}
});
widget.element.addEventListener('pointerdown', (event: PointerEvent) => {
if ((event.buttons & 4) === 4) {
app.canvas.processMouseDown(event);
}
});
// Forward wheel events to canvas (block ctrl+wheel for browser zoom)
widget.element.addEventListener('wheel', (event: WheelEvent) => {
if (event.ctrlKey) {
event.preventDefault();
event.stopPropagation();
return;
}
event.preventDefault();
app.canvas.processMouseWheel(event);
});
if (videoInfo) {
widget.videoInfo = videoInfo;
widget.value = { hidden: false, videoInfo };
} else {
widget.value = { hidden };
}
if (hidden) {
widget.element.hidden = true;
}
return widget;
}
/**
* Update a video widget with a decrypted video URL.
* @param widget The video widget to update
* @param videoUrl The blob URL of the decrypted video
*/
export function renderVideoInWidget(widget: VideoPreviewWidget, videoUrl: string): Promise<void> {
return new Promise((resolve, reject) => {
const video = document.createElement("video");
video.controls = true;
video.loop = true;
video.muted = true;
video.autoplay = false;
video.playsInline = true;
video.onloadedmetadata = () => {
widget.updateLayout?.(video.videoWidth, video.videoHeight);
widget.element.hidden = false;
widget.element.style.overflow = "hidden";
// Clear previous content
widget.element.replaceChildren();
// Add new video
widget.element.appendChild(video);
widget.videoElement = video;
widget.videoUrl = videoUrl;
widget.value = { ...widget.value, hidden: false };
app.rootGraph?.setDirtyCanvas(true, false);
resolve();
};
video.onerror = () => reject(new Error("Failed to load video element"));
video.src = videoUrl;
});
}
/**
* Clean up a video widget (pause, revoke URL, clear DOM).
*/
export function clearVideoWidget(widget: VideoPreviewWidget) {
if (widget.videoElement) {
widget.videoElement.pause();
widget.videoElement.src = "";
}
if (widget.videoUrl) {
URL.revokeObjectURL(widget.videoUrl);
}
widget.element.hidden = true;
widget.value = { hidden: true };
widget.element.replaceChildren();
app.rootGraph?.setDirtyCanvas(true, false);
}