refactor: extract duplicated code into shared modules (videoWidgetUtils, uploadUtils, fileUtils)
This commit is contained in:
+16
-134
@@ -12,136 +12,22 @@ import {
|
||||
loadEncryptedVideoFromParams,
|
||||
downloadDecryptedVideo
|
||||
} from "./utils/videoLoader.js";
|
||||
import {
|
||||
createVideoWidget,
|
||||
renderVideoInWidget,
|
||||
clearVideoWidget,
|
||||
} from "./utils/videoWidgetUtils.js";
|
||||
|
||||
const app: ComfyApp = rawApp;
|
||||
const api: ComfyApi = rawApi;
|
||||
|
||||
const DEFAULT_VIDEO_SIZE = 256;
|
||||
const MIN_WIDGET_HEIGHT = 64;
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create video preview widget
|
||||
* Load encrypted video from params and render in widget
|
||||
*/
|
||||
function createVideoWidget(node: any, videoName: string, videoInfo: any) {
|
||||
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(
|
||||
videoName,
|
||||
"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;
|
||||
};
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
widget.element.addEventListener('wheel', (event: WheelEvent) => {
|
||||
|
||||
if (event.ctrlKey) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
app.canvas.processMouseWheel(event)
|
||||
})
|
||||
|
||||
widget.videoInfo = videoInfo;
|
||||
widget.value = { hidden: false, videoInfo };
|
||||
|
||||
return widget;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update video preview widget with decrypted video
|
||||
*/
|
||||
async function updateVideoWidget(widget: any, videoInfo: any) {
|
||||
try {
|
||||
const videoUrl = await loadEncryptedVideoFromParams(api, videoInfo);
|
||||
|
||||
// Create video element
|
||||
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;
|
||||
|
||||
app.rootGraph?.setDirtyCanvas(true, false);
|
||||
};
|
||||
|
||||
video.src = videoUrl;
|
||||
|
||||
return videoUrl;
|
||||
} catch (error) {
|
||||
console.error("Failed to decrypt video for preview:", error);
|
||||
throw error;
|
||||
}
|
||||
async function updateVideoWidget(widget: any, videoInfo: any): Promise<string> {
|
||||
const videoUrl = await loadEncryptedVideoFromParams(api, videoInfo);
|
||||
await renderVideoInWidget(widget, videoUrl);
|
||||
return videoUrl;
|
||||
}
|
||||
|
||||
// Register extension
|
||||
@@ -173,6 +59,7 @@ app.registerExtension({
|
||||
w.type === "video-preview"
|
||||
);
|
||||
for (const widget of existingWidgets) {
|
||||
clearVideoWidget(widget);
|
||||
this.removeWidget(widget);
|
||||
}
|
||||
|
||||
@@ -181,8 +68,10 @@ app.registerExtension({
|
||||
for (let i = 0; i < message.cryptio_images.length; i++) {
|
||||
const videoInfo = message.cryptio_images[i];
|
||||
if (videoInfo.filename && videoInfo.filename.endsWith(".encrypted")) {
|
||||
const videoName = `video_${i}`;
|
||||
const widget = createVideoWidget(this, videoName, videoInfo);
|
||||
const widget = createVideoWidget(this, {
|
||||
widgetName: `video_${i}`,
|
||||
videoInfo,
|
||||
});
|
||||
|
||||
updatePromises.push(
|
||||
updateVideoWidget(widget, videoInfo)
|
||||
@@ -231,19 +120,12 @@ app.registerExtension({
|
||||
// Cleanup on node removal
|
||||
const onRemoved = nodeType.prototype.onRemoved;
|
||||
nodeType.prototype.onRemoved = function () {
|
||||
// Clean up video widgets
|
||||
const videoWidgets = this.widgets?.filter((w: any) =>
|
||||
w.type === "video-preview"
|
||||
) || [];
|
||||
|
||||
for (const widget of videoWidgets) {
|
||||
if (widget.videoElement) {
|
||||
widget.videoElement.pause();
|
||||
widget.videoElement.src = "";
|
||||
}
|
||||
if (widget.videoUrl) {
|
||||
URL.revokeObjectURL(widget.videoUrl);
|
||||
}
|
||||
clearVideoWidget(widget);
|
||||
}
|
||||
|
||||
if (onRemoved) {
|
||||
|
||||
+11
-181
@@ -9,202 +9,32 @@ import type {
|
||||
} from "@comfyorg/comfyui-frontend-types";
|
||||
|
||||
import { exchangeKeys } from "./utils/cryptoKeys.js";
|
||||
import { encryptFileWithServerKey } from "./utils/cryptoUtils.js";
|
||||
import { loadEncryptedVideoFromFilename } from "./utils/videoLoader.js";
|
||||
import { uploadEncryptedFile } from "./utils/uploadUtils.js";
|
||||
import {
|
||||
createVideoWidget,
|
||||
renderVideoInWidget,
|
||||
clearVideoWidget,
|
||||
} from "./utils/videoWidgetUtils.js";
|
||||
|
||||
const app: ComfyApp = rawApp;
|
||||
const api: ComfyApi = rawApi;
|
||||
|
||||
const DEFAULT_VIDEO_SIZE = 256;
|
||||
const MIN_WIDGET_HEIGHT = 64;
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create video preview widget
|
||||
*/
|
||||
function createVideoWidget(node: any) {
|
||||
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(
|
||||
"video_preview",
|
||||
"video-preview",
|
||||
container,
|
||||
{
|
||||
serialize: false,
|
||||
hideOnZoom: false,
|
||||
}
|
||||
);
|
||||
|
||||
widget.computeLayoutSize = () => ({
|
||||
minHeight,
|
||||
minWidth,
|
||||
});
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
widget.element.addEventListener('wheel', (event: WheelEvent) => {
|
||||
|
||||
if (event.ctrlKey) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
app.canvas.processMouseWheel(event)
|
||||
})
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
widget.value = { hidden: true };
|
||||
widget.element.hidden = true;
|
||||
|
||||
return widget;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update video preview widget with decrypted video
|
||||
* Load encrypted video from filename and render in widget
|
||||
*/
|
||||
async function updateVideoWidget(widget: any, filename: string) {
|
||||
try {
|
||||
const videoUrl = await loadEncryptedVideoFromFilename(filename);
|
||||
|
||||
// Create video element
|
||||
const video = document.createElement("video");
|
||||
video.controls = true;
|
||||
video.loop = true;
|
||||
video.muted = true;
|
||||
video.autoplay = false;
|
||||
video.style.width = "100%";
|
||||
video.style.height = "auto";
|
||||
|
||||
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 = { hidden: false };
|
||||
|
||||
app.rootGraph?.setDirtyCanvas(true, false);
|
||||
};
|
||||
|
||||
video.src = videoUrl;
|
||||
} catch (error) {
|
||||
console.error("Failed to decrypt video for preview:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear video preview widget
|
||||
*/
|
||||
function clearVideoWidget(widget: any) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload encrypted video to server (encrypted with server public key)
|
||||
* @param file File to upload
|
||||
* @returns Server response
|
||||
*/
|
||||
async function uploadEncryptedVideo(file: File): Promise<any> {
|
||||
console.log("Encrypting and uploading video:", file.name);
|
||||
|
||||
// Encrypt file (use server public key, or directly read if already encrypted with server public key)
|
||||
const encryptedData = file?.name?.endsWith(".encrypted")
|
||||
? new Uint8Array(await file.arrayBuffer())
|
||||
: await encryptFileWithServerKey(file);
|
||||
|
||||
// Create FormData
|
||||
const formData = new FormData();
|
||||
const encryptedBlob = new Blob([encryptedData as BlobPart], {
|
||||
type: "application/octet-stream",
|
||||
});
|
||||
formData.append("image", encryptedBlob, file.name);
|
||||
|
||||
// Upload to server
|
||||
const response = await fetch("/cryptio/upload_encrypted", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Upload failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
const videoUrl = await loadEncryptedVideoFromFilename(filename);
|
||||
await renderVideoInWidget(widget, videoUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle video file upload to node
|
||||
* @param node Node instance
|
||||
* @param file File to upload
|
||||
* @param app ComfyUI App instance
|
||||
*/
|
||||
async function handleVideoUpload(node: any, file: File, app: ComfyApp) {
|
||||
try {
|
||||
// Upload encrypted video
|
||||
const result = await uploadEncryptedVideo(file);
|
||||
const result = await uploadEncryptedFile(file);
|
||||
|
||||
// Update video widget value
|
||||
const videoWidget = node.widgets.find(
|
||||
@@ -281,7 +111,7 @@ app.registerExtension({
|
||||
uploadWidget.label = "choose video to upload";
|
||||
|
||||
// Create video preview widget
|
||||
const previewWidget = createVideoWidget(this);
|
||||
const previewWidget = createVideoWidget(this, { hidden: true });
|
||||
|
||||
// Update preview when video value changes
|
||||
const videoWidget = this.widgets.find((w: any) => w.name === "video");
|
||||
|
||||
@@ -46,3 +46,19 @@ export function getMimeTypeFromFilename(filename: string): string {
|
||||
|
||||
return mimeTypes[extension] || 'image/png'; // 默认返回image/png
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a decrypted file (image or video) from a blob URL.
|
||||
* Strips the .encrypted suffix from the filename.
|
||||
* @param blobUrl Blob URL of the decrypted data
|
||||
* @param filename Original filename (with .encrypted suffix)
|
||||
*/
|
||||
export function downloadDecryptedFile(blobUrl: string, filename: string): void {
|
||||
const a = document.createElement("a");
|
||||
a.href = blobUrl;
|
||||
const originalFilename = filename.replace(/\.encrypted$/, "");
|
||||
a.download = originalFilename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
|
||||
+3
-16
@@ -5,7 +5,7 @@
|
||||
|
||||
import type { ComfyApi } from "@comfyorg/comfyui-frontend-types";
|
||||
import { decryptDataWithServerKey, decryptFileWithClientKey } from "./cryptoUtils.js";
|
||||
import { getMimeTypeFromFilename } from "./fileUtils.js";
|
||||
import { getMimeTypeFromFilename, downloadDecryptedFile } from "./fileUtils.js";
|
||||
|
||||
export interface ImageLoadParams {
|
||||
filename: string;
|
||||
@@ -77,18 +77,5 @@ export async function loadEncryptedImageFromParams(api: ComfyApi, params: ImageL
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载解密后的图片
|
||||
* @param blobUrl Blob URL
|
||||
* @param filename 原始文件名(带 .encrypted 后缀)
|
||||
*/
|
||||
export function downloadDecryptedImage(blobUrl: string, filename: string): void {
|
||||
const a = document.createElement("a");
|
||||
a.href = blobUrl;
|
||||
// 移除 .encrypted 后缀
|
||||
const originalFilename = filename.replace(/\.encrypted$/, "");
|
||||
a.download = originalFilename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
/** @deprecated Use downloadDecryptedFile from fileUtils.ts instead */
|
||||
export const downloadDecryptedImage = downloadDecryptedFile;
|
||||
|
||||
+12
-8
@@ -8,26 +8,27 @@ import { encryptFileWithServerKey } from "./cryptoUtils.js";
|
||||
import { loadEncryptedImageFromFilename } from "./imageLoader.js";
|
||||
|
||||
/**
|
||||
* 上传加密图片到服务器(使用服务端公钥加密)
|
||||
* @param file 要上传的文件
|
||||
* @returns 服务器响应
|
||||
* Upload an encrypted file to the server (encrypted with server public key).
|
||||
* Works for both images and videos.
|
||||
* @param file File to upload
|
||||
* @returns Server response
|
||||
*/
|
||||
export async function uploadEncryptedImage(file: File): Promise<any> {
|
||||
console.log("Encrypting and uploading image:", file.name);
|
||||
export async function uploadEncryptedFile(file: File): Promise<any> {
|
||||
console.log("Encrypting and uploading file:", file.name);
|
||||
|
||||
// 加密文件(使用服务端公钥,如果文件已经使用服务端公钥加密则直接读取)
|
||||
// Encrypt file (use server public key, or directly read if already encrypted)
|
||||
const encryptedData = file?.name?.endsWith(".encrypted")
|
||||
? new Uint8Array(await file.arrayBuffer())
|
||||
: await encryptFileWithServerKey(file);
|
||||
|
||||
// 创建FormData
|
||||
// Create FormData
|
||||
const formData = new FormData();
|
||||
const encryptedBlob = new Blob([encryptedData as BlobPart], {
|
||||
type: "application/octet-stream",
|
||||
});
|
||||
formData.append("image", encryptedBlob, file.name);
|
||||
|
||||
// 上传到服务器
|
||||
// Upload to server
|
||||
const response = await fetch("/cryptio/upload_encrypted", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
@@ -40,6 +41,9 @@ export async function uploadEncryptedImage(file: File): Promise<any> {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/** @deprecated Use uploadEncryptedFile instead */
|
||||
export const uploadEncryptedImage = uploadEncryptedFile;
|
||||
|
||||
/**
|
||||
* 处理文件上传到节点的通用方法
|
||||
* @param node 节点实例
|
||||
|
||||
+3
-16
@@ -5,7 +5,7 @@
|
||||
|
||||
import type { ComfyApi } from "@comfyorg/comfyui-frontend-types";
|
||||
import { decryptDataWithServerKey, decryptFileWithClientKey } from "./cryptoUtils.js";
|
||||
import { getMimeTypeFromFilename } from "./fileUtils.js";
|
||||
import { getMimeTypeFromFilename, downloadDecryptedFile } from "./fileUtils.js";
|
||||
|
||||
export interface VideoLoadParams {
|
||||
filename: string;
|
||||
@@ -77,19 +77,6 @@ export async function loadEncryptedVideoFromParams(api: ComfyApi, params: VideoL
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载解密后的视频
|
||||
* @param blobUrl Blob URL
|
||||
* @param filename 原始文件名(带 .encrypted 后缀)
|
||||
*/
|
||||
export function downloadDecryptedVideo(blobUrl: string, filename: string): void {
|
||||
const a = document.createElement("a");
|
||||
a.href = blobUrl;
|
||||
// 移除 .encrypted 后缀
|
||||
const originalFilename = filename.replace(/\.encrypted$/, "");
|
||||
a.download = originalFilename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
/** @deprecated Use downloadDecryptedFile from fileUtils.ts instead */
|
||||
export const downloadDecryptedVideo = downloadDecryptedFile;
|
||||
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* 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";
|
||||
|
||||
const app: ComfyApp = rawApp;
|
||||
|
||||
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: any, options: CreateVideoWidgetOptions = {}) {
|
||||
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: any, 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: any) {
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user