Files
VrchStudio-comfyui-web-viewer/web/comfyui/node_utils.js
T

463 lines
16 KiB
JavaScript

// Enable debug logging
export const ENABLE_DEBUG = false;
/**
* Helper function to trigger a new queue generation.
* This function finds the queue button and clicks it to start a new generation.
*/
export function triggerNewGeneration() {
let queueButton = document.querySelector('button[data-testid="queue-button"]');
if (queueButton) {
console.log('Triggering new queue generation.');
queueButton.click();
return;
}
const buttonContainer = document.querySelector('div[data-testid="queue-button"]');
if (buttonContainer) {
queueButton = buttonContainer.querySelector('button[data-pc-name="pcbutton"]');
if (queueButton) {
console.log('Triggering new queue generation.');
queueButton.click();
return; // Do not proceed to default fail condition.
} else {
console.warn("Queue button not found inside container.");
return; // Do not proceed to default fail condition.
}
}
// Default fail condition.
console.error("Queue button not found.");
}
// Helper functions to hide and show widgets
export function hideWidget(node, widget) {
if (!widget) return;
if (widget.type === "hidden" || widget.hidden) return;
widget.origType ??= widget.type;
widget.origComputeSize ??= widget.computeSize;
widget.origHidden = widget.hidden ?? false;
widget.type = "hidden";
widget.hidden = true;
widget.computeSize = widget.computeSize || (() => [0, 0]);
if (widget.element) {
widget.element.style.display = "none";
}
node?.setDirtyCanvas?.(true, true);
}
export function showWidget(node, widget) {
if (!widget) return;
widget.type = widget.origType ?? widget.type;
widget.computeSize = widget.origComputeSize ?? widget.computeSize;
widget.hidden = widget.origHidden ?? false;
if (widget.element) {
widget.element.style.removeProperty("display");
}
node?.setDirtyCanvas?.(true, true);
}
/**
* Enumerate available microphone input devices.
*
* @param {Object} [options]
* @param {boolean} [options.requestAccess=true] - Whether to request `getUserMedia` before enumerating.
* @param {string} [options.logPrefix="[Microphone]"] - Prefix for debug logging.
* @param {boolean} [options.debugEnabled=false] - Enable console logs when true.
* @returns {Promise<MediaDeviceInfo[]>} Resolves with the list of audioinput devices.
*/
export async function listMicrophoneDevices(options = {}) {
const { requestAccess = true, logPrefix = "[Microphone]", debugEnabled = false } = options;
try {
if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
throw new Error("MediaDevices API not supported in this browser");
}
if (requestAccess) {
try {
await navigator.mediaDevices.getUserMedia({ audio: true });
} catch (permissionError) {
if (debugEnabled) {
console.warn(`${logPrefix} Permission request failed:`, permissionError);
}
}
}
const mediaDevices = await navigator.mediaDevices.enumerateDevices();
const audioInputs = mediaDevices.filter(device => device.kind === "audioinput");
if (debugEnabled) {
console.log(`${logPrefix} Enumerated ${audioInputs.length} microphone device(s).`);
}
return audioInputs;
} catch (error) {
if (debugEnabled) {
console.error(`${logPrefix} Failed to enumerate microphones:`, error);
}
throw error;
}
}
/**
* Populate a <select> element with microphone device options.
*
* @param {HTMLSelectElement} selectEl - The select element to populate.
* @param {MediaDeviceInfo[]} devices - List of microphone devices.
* @param {Object} [options]
* @param {string} [options.placeholder="Select Microphone..."] - Placeholder option label.
* @param {string} [options.previousValue] - Previously selected deviceId to restore if available.
* @returns {{selection: string, restored: boolean}} Selected value and whether it was restored.
*/
export function populateMicrophoneSelect(selectEl, devices, options = {}) {
if (!selectEl) {
return { selection: "", restored: false };
}
const { placeholder = "Select Microphone...", previousValue } = options;
const priorValue = previousValue !== undefined ? previousValue : selectEl.value;
while (selectEl.options.length > 0) {
selectEl.remove(0);
}
const defaultOption = document.createElement("option");
defaultOption.value = "";
defaultOption.textContent = placeholder;
selectEl.appendChild(defaultOption);
let restored = false;
devices.forEach(device => {
const option = document.createElement("option");
option.value = device.deviceId;
option.textContent = device.label || `Microphone (${device.deviceId.slice(0, 5)}...)`;
selectEl.appendChild(option);
if (priorValue && device.deviceId === priorValue) {
restored = true;
}
});
if (restored) {
selectEl.value = priorValue;
} else {
selectEl.value = "";
}
return {
selection: selectEl.value,
restored,
};
}
/**
* Create reusable microphone controls UI (select + reload + mute + status).
*
* @param {Object} options
* @param {boolean} [options.debug=false] - Enable console logging.
* @param {Function} [options.onDeviceChange] - Called with deviceId when selection changes.
* @param {Function} [options.onMuteChange] - Called with boolean when mute toggles.
* @param {Function} [options.filterDevices] - Optional filter returning filtered list.
* @returns {Object} { container, selectEl, reloadButton, muteButton, statusLabel, refreshDevices, setMuted, setStatus, getSelection, setSelection }
*/
export function createMicrophoneControls(options = {}) {
const {
debug = false,
onDeviceChange,
onMuteChange,
filterDevices,
} = options;
const log = (...args) => {
if (debug) {
console.log("[MicrophoneControls]", ...args);
}
};
let devicesCache = [];
let isMuted = false;
const container = document.createElement("div");
container.classList.add("mic-controls-container");
const deviceRow = document.createElement("div");
deviceRow.classList.add("mic-device-row");
const selectEl = document.createElement("select");
selectEl.classList.add("mic-device-select");
populateMicrophoneSelect(selectEl, []);
deviceRow.appendChild(selectEl);
const buttonRow = document.createElement("div");
buttonRow.classList.add("mic-button-row");
const reloadButton = document.createElement("button");
reloadButton.textContent = "Reload";
reloadButton.classList.add("vrch-mic-reload-button");
const muteButton = document.createElement("button");
muteButton.classList.add("vrch-mic-mute-button");
const applyMuteStyle = () => {
muteButton.innerHTML = isMuted
? '<span class="vrch-mic-speaker-icon">🔇</span>'
: '<span class="vrch-mic-speaker-icon">🔊</span>';
muteButton.style.backgroundColor = isMuted ? "#9e6a6a" : "#4f9cff";
};
applyMuteStyle();
buttonRow.appendChild(reloadButton);
buttonRow.appendChild(muteButton);
const statusContainer = document.createElement("div");
statusContainer.classList.add("vrch-mic-status-container");
const statusLabel = document.createElement("div");
statusLabel.classList.add("vrch-mic-status-label");
statusLabel.textContent = "Status: Ready";
statusContainer.appendChild(statusLabel);
container.appendChild(deviceRow);
container.appendChild(buttonRow);
container.appendChild(statusContainer);
const setStatus = (status) => {
statusLabel.textContent = `Status: ${status}`;
};
const updateSelection = (deviceId) => {
if (!selectEl) return;
selectEl.value = deviceId || "";
if (onDeviceChange) {
onDeviceChange(selectEl.value);
}
};
const setMuted = (mute) => {
isMuted = !!mute;
applyMuteStyle();
if (onMuteChange) {
onMuteChange(isMuted);
}
};
const getSelection = () => selectEl.value;
const setSelection = (deviceId) => {
const existing = devicesCache.find(device => device.deviceId === deviceId);
if (existing) {
selectEl.value = deviceId;
}
};
const refreshDevices = async ({ requestAccess = false, logPrefix = "[MicrophoneControls]" } = {}) => {
try {
const deviceList = await listMicrophoneDevices({
requestAccess,
logPrefix,
debugEnabled: debug,
});
const filtered = filterDevices ? deviceList.filter(filterDevices) : deviceList;
devicesCache = filtered;
const prior = selectEl.value;
const { selection } = populateMicrophoneSelect(selectEl, devicesCache, {
previousValue: prior,
});
if (selection !== prior && onDeviceChange) {
onDeviceChange(selection);
}
log(`Refreshed devices: ${devicesCache.length}`);
return devicesCache;
} catch (error) {
log("Failed to refresh devices", error);
throw error;
}
};
selectEl.addEventListener("change", () => {
if (onDeviceChange) {
onDeviceChange(selectEl.value);
}
});
muteButton.addEventListener("click", () => {
setMuted(!isMuted);
});
return {
container,
selectEl,
reloadButton,
muteButton,
statusLabel,
refreshDevices,
setMuted,
getMuted: () => isMuted,
setStatus,
getSelection,
setSelection,
devices: () => devicesCache.slice(),
};
}
/**
* Helper function to build the URL based on configuration.
*
* @param {Object} config - The configuration object with the following properties:
* serverWidget: widget for server address
* sslWidget: widget for SSL flag (boolean)
* extraParamsWidget: widget for extra parameters (string)
* mode: string specifying the viewer mode (e.g., "image", "video", etc.)
* path: string specifying the path (default is "web_viewer")
* protocol: (optional) protocol to use (default is "http", also support "websocket")
* channel: (optional) channel number (default is "1", [1-8] based)
* fileGenerator: a function that returns the filename (receives the config as argument)
* additionalParams: (optional) extra query parameters as an object (keys with widget values) or string
*
* @returns {string} The constructed URL.
*/
export function buildUrl(config) {
// Retrieve server address and SSL flag (with default values)
const server = config.serverWidget ? config.serverWidget.value : "127.0.0.1:8188";
const ssl = config.sslWidget ? config.sslWidget.value : false;
const sslStr = ssl ? "true" : "false";
const scheme = ssl ? "https" : "http";
// Process extra parameters; if not empty and not starting with '&', prepend '&'
let extraParams = config.extraParamsWidget ? config.extraParamsWidget.value : "";
if (extraParams && extraParams[0] !== "&") {
extraParams = "&" + extraParams;
}
// Use provided mode and path (with a default for path)
const mode = config.mode || "";
const path = config.path || "web_viewer";
const pathStr = path ? `&path=${path}` : "";
// Generate the filename using the provided fileGenerator callback
const filename = config.fileGenerator ? config.fileGenerator(config) : "";
const filenameStr = filename ? `&filename=${filename}` : "";
// Process additional query parameters if provided
let additionalQuery = "";
if (config.additionalParams && typeof config.additionalParams === "object") {
// Assume each value in the object is a widget; extract its value
additionalQuery = Object.entries(config.additionalParams)
.map(([key, widget]) => `&${key}=${widget.value}`)
.join("");
} else if (typeof config.additionalParams === "string") {
additionalQuery = "&" + config.additionalParams;
}
let protocolStr = "";
let channelStr = "";
// If config.protocol is not http, insert the protocol in the URL before the server
if (config.protocol) {
protocolStr = `&protocol=${config.protocol}`;
}
if (config.channel) {
channelStr = `&channel=${config.channel}`;
}
// Check if dev mode is enabled
const devMode = config.devMode ? config.devMode.value : false;
const viewerPath = devMode ? "dev/viewer" : "viewer";
// Construct the URL using the provided parameters
const url = `${scheme}://vrch.ai/${viewerPath}?mode=${mode}&server=${server}&ssl=${sslStr}${protocolStr}${channelStr}${filenameStr}${pathStr}${extraParams}${additionalQuery}`;
// Return the final URL
return url;
}
/**
* Helper function to initialize the URL and widget visibility after a delay.
*
* @param {Object} node - The current node.
* @param {Object} showUrlWidget - The widget controlling URL visibility.
* @param {Object} urlWidget - The URL widget.
* @param {Function} updateUrl - The updateUrl callback.
*/
export function delayInit(node, showUrlWidget, urlWidget, updateUrl) {
setTimeout(() => {
// Handle URL widget visibility
if (showUrlWidget) {
showUrlWidget.value ? showWidget(node, urlWidget) : hideWidget(node, urlWidget);
}
// Update URL
updateUrl();
}, 1000);
}
/**
* Helper function to create the "Open Web Viewer" button.
*
* @param {Object} node - The current node.
* @param {Object} urlWidget - The widget containing the URL.
* @param {Object} widthWidget - The widget for window width.
* @param {Object} heightWidget - The widget for window height.
*/
export function createOpenWebViewerButton(node, urlWidget, widthWidget, heightWidget) {
// Create a container with fixed height for the button
const container = document.createElement("div");
container.classList.add("vrch-button-container");
const button = document.createElement("button");
button.textContent = "Open Web Viewer";
button.classList.add("vrch-big-button");
button.onclick = () => {
if (urlWidget && urlWidget.value) {
const width = widthWidget ? widthWidget.value : 1280;
const height = heightWidget ? heightWidget.value : 960;
window.open(urlWidget.value, "_blank", `width=${width},height=${height}`);
} else {
console.error("URL widget not found or empty");
}
};
// Add button to container
container.appendChild(button);
// Add the container (with button inside) as widget
const widget = node.addDOMWidget("button_widget", "Open Web Viewer", container);
// Override the computeSize method to force correct sizing
widget.computeSize = function(width) {
return [width, 60]; // Force height to be 48px
};
}
/**
* Helper function to set up widget callbacks for updating the URL and toggling visibility.
*
* @param {Object} node - The current node.
* @param {Function} updateUrl - The updateUrl function to be called on widget change.
* @param {Object} urlWidget - The URL widget to be toggled.
* @param {Object} showUrlWidget - The widget controlling URL visibility.
* @param {Array} widgets - An array of widgets whose callbacks will trigger updateUrl.
* @param {string} [logPrefix] - (Optional) A prefix to log for debugging.
*/
export function setupWidgetCallback(node, updateUrl, urlWidget, showUrlWidget, widgets, logPrefix) {
widgets.forEach(widget => {
if (widget) {
widget.callback = () => {
if (logPrefix && ENABLE_DEBUG) {
console.log(`${logPrefix}:::${widget.name}`);
}
updateUrl();
};
}
});
if (showUrlWidget) {
showUrlWidget.callback = (value) => {
if (value) {
showWidget(node, urlWidget);
} else {
hideWidget(node, urlWidget);
}
};
}
}