Files
MajoorWaldi-ComfyUI-Majoor-…/js/features/viewer/LiveStreamTracker.js
T
Ewald 2b10814b2e feat(mfv): ImageOps live preview crop + node panel selection sync
- ImageOps preview bridge: stream cropped contain-fit rect of __imageops_state.canvas (removes square scope/letterbox); fall back to full canvas when user pan/zoom is active. Signature-cached encode (WeakMap) avoids re-encoding on every poll tick.
- Floating viewer: skip metadata enrichment for imageops-live-preview source; preserve user pan/zoom across stream frames of the same node; remove inline CSS overrides that blocked the MFV transform.
- Workflow Nodes panel: graph-selected node is auto-expanded, promoted to first position in its container, scrolled into view, and gets a subtle blue highlight (is-selected-from-graph).
2026-04-24 23:47:27 +02:00

157 lines
5.0 KiB
JavaScript

/**
* LiveStreamTracker - bridges ComfyUI generation events to the Floating Viewer.
*
* Two sources feed the MFV:
* 1. NEW_GENERATION_OUTPUT: when Live Stream is active, shows the latest output
* file after workflow execution.
* 2. b_preview: when KSampler Preview is active, streams denoising-step preview
* blobs from the ComfyUI WebSocket API.
*
* Canvas node selection is intentionally owned by Node Stream, not Live Stream.
*/
import { EVENTS } from "../../app/events.js";
import { floatingViewerManager } from "./floatingViewerManager.js";
import { waitForComfyApi } from "../../app/comfyApiBridge.js";
let _initialized = false;
let _genOutputHandler = null;
let _previewHandler = null;
let _previewWithMetaHandler = null;
let _previewWithMetaFired = false;
let _apiRef = null;
let _currentJobId = null;
let _previewHookGeneration = 0;
async function _hookPreviewApi(app) {
const hookGeneration = ++_previewHookGeneration;
try {
_detachPreviewApiListeners();
const api = await waitForComfyApi({ app, timeoutMs: 8000 });
if (hookGeneration !== _previewHookGeneration) return;
if (!api) {
console.debug("[Majoor] MFV: ComfyUI API not found - preview streaming disabled");
return;
}
_apiRef = api;
_previewWithMetaHandler = (e) => {
_previewWithMetaFired = true;
try {
const { blob, nodeId, jobId } = e.detail || {};
if (!blob || !(blob instanceof Blob)) return;
if (_currentJobId && jobId && jobId !== _currentJobId) return;
floatingViewerManager.feedPreviewBlob(blob, {
sourceLabel: nodeId ? `Node ${nodeId}` : null,
});
} catch (err) {
console.debug?.("[MFV] b_preview_with_metadata error", err);
}
};
api.addEventListener("b_preview_with_metadata", _previewWithMetaHandler);
_previewHandler = (e) => {
try {
if (_previewWithMetaFired) return;
const blob = e.detail;
if (!blob || !(blob instanceof Blob)) return;
floatingViewerManager.feedPreviewBlob(blob);
} catch (err) {
console.debug?.("[MFV] preview blob error", err);
}
};
api.addEventListener("b_preview", _previewHandler);
console.debug(
"[Majoor] MFV preview stream hooked to ComfyUI API (b_preview_with_metadata + b_preview fallback)",
);
} catch (e) {
console.debug?.("[Majoor] MFV preview hook failed - preview streaming disabled", e);
}
}
function _detachPreviewApiListeners() {
if (_apiRef) {
if (_previewHandler) {
try {
_apiRef.removeEventListener("b_preview", _previewHandler);
} catch (e) {
console.debug?.(e);
}
}
if (_previewWithMetaHandler) {
try {
_apiRef.removeEventListener("b_preview_with_metadata", _previewWithMetaHandler);
} catch (e) {
console.debug?.(e);
}
}
}
_previewHandler = null;
_previewWithMetaHandler = null;
_previewWithMetaFired = false;
_apiRef = null;
}
export function setCurrentJobId(jobId) {
_currentJobId = jobId || null;
}
function _pickLatest(files) {
if (!Array.isArray(files) || !files.length) return null;
const images = files.filter((f) => {
const name = String(f?.filename || "").toLowerCase();
return (
name.endsWith(".png") ||
name.endsWith(".jpg") ||
name.endsWith(".jpeg") ||
name.endsWith(".webp") ||
name.endsWith(".avif")
);
});
return images[images.length - 1] ?? files[files.length - 1];
}
export function initLiveStreamTracker(app) {
if (_genOutputHandler) return;
_initialized = true;
_genOutputHandler = (e) => {
try {
if (!floatingViewerManager.getLiveActive()) return;
const latest = _pickLatest(e.detail?.files);
if (!latest) return;
floatingViewerManager.upsertWithContent(latest);
} catch (err) {
console.debug?.("[MFV] generation output error", err);
}
};
if (typeof window !== "undefined") {
window.addEventListener(EVENTS.NEW_GENERATION_OUTPUT, _genOutputHandler);
}
_hookPreviewApi(app);
console.debug("[Majoor] LiveStreamTracker initialized");
}
export function teardownLiveStreamTracker(app) {
void app;
if (_genOutputHandler) {
if (typeof window !== "undefined") {
window.removeEventListener(EVENTS.NEW_GENERATION_OUTPUT, _genOutputHandler);
}
_genOutputHandler = null;
}
_previewHookGeneration += 1;
_detachPreviewApiListeners();
_currentJobId = null;
_initialized = false;
console.debug("[Majoor] LiveStreamTracker torn down");
}
export function isLiveStreamTrackerInitialized() {
return _initialized;
}