Files

244 lines
8.1 KiB
JavaScript

import { app } from "../../../scripts/app.js"
import { api } from "../../../scripts/api.js"
app.registerExtension({
name: "be.aboutme.comfyui.live_input_stream",
async setup() {
},
getCustomWidgets() {
return {
WEBCAM_ADVANCED(node, inputName) {
const container = document.createElement('div')
container.style.background = 'rgba(0,0,0,0.25)'
container.style.textAlign = 'center'
return { widget: node.addDOMWidget(inputName, 'WEBCAM_ADVANCED', container, {
getMinHeight: () => 200
}) }
}
}
},
async nodeCreated(node) {
if (node.constructor.comfyClass !== "be.aboutme.comfyui.live_input_stream.webcam_capture_advanced") {
return
}
let deviceWidget = node.widgets.find((w) => w.name === 'device')
const widthWidget = node.widgets.find((w) => w.name === 'desired_width')
const heightWidget = node.widgets.find((w) => w.name === 'desired_height')
const frameRateWidget = node.widgets.find((w) => w.name === 'frame_rate')
const cameraWidget = node.widgets.find((w) => w.name === 'image')
const trimLeftWidget = node.widgets.find((w) => w.name === 'trim_left')
const trimRightWidget = node.widgets.find((w) => w.name === 'trim_right')
const trimTopWidget = node.widgets.find((w) => w.name === 'trim_top')
const trimBottomWidget = node.widgets.find((w) => w.name === 'trim_bottom')
const container = cameraWidget.element;
const videoCanvas = document.createElement('canvas')
const videoCtx = videoCanvas.getContext('2d')
videoCanvas.style.width = '100%'
videoCanvas.style.height = '100%'
videoCanvas.style.objectFit = 'contain'
const video = document.createElement('video')
video.style.height = video.style.width = '100%'
const listVideoDevices = async () => {
const devices = await navigator.mediaDevices.enumerateDevices()
return devices.filter((device) => device.kind === 'videoinput')
}
const drawFrame = () => {
videoCanvas.width = video.videoWidth - trimLeftWidget.value - trimRightWidget.value
videoCanvas.height = video.videoHeight - trimTopWidget.value - trimBottomWidget.value
videoCtx.drawImage(
video,
trimLeftWidget.value,
trimTopWidget.value,
video.videoWidth - trimLeftWidget.value - trimRightWidget.value,
video.videoHeight - trimTopWidget.value - trimBottomWidget.value,
0,
0,
videoCanvas.width,
videoCanvas.height
)
}
const requestFrame = requestAnimationFrame;
const step = () => {
// check if our container is still attached to DOM
if (!container.isConnected) {
// stop the video stream
stream.getTracks().forEach((track) => track.stop())
return
}
drawFrame()
requestFrame(step)
}
const loadVideo = async () => {
try {
// stop existing stream if any
if (video.srcObject) {
const tracks = video.srcObject.getTracks()
tracks.forEach((track) => track.stop())
}
// Request resolution and frame rate from widget values
const videoConstraints = {
deviceId: deviceWidget.value ? { exact: deviceWidget.value } : undefined,
width: { ideal: widthWidget.value },
height: { ideal: heightWidget.value },
frameRate: { ideal: frameRateWidget.value }
}
const stream = await navigator.mediaDevices.getUserMedia({
video: videoConstraints,
audio: false
})
// Update the device widget with the actual device ID being used
const videoTrack = stream.getVideoTracks()[0]
const settings = videoTrack.getSettings()
if (settings.deviceId && !deviceWidget.value) {
deviceWidget.value = settings.deviceId
}
container.replaceChildren(videoCanvas)
// setTimeout(() => res(container), 500)
// video.addEventListener('loadedmetadata', () => res(container), false)
video.srcObject = stream
video.play()
} catch (error) {
const label = document.createElement('div')
label.style.color = 'red'
label.style.overflow = 'auto'
label.style.maxHeight = '100%'
label.style.whiteSpace = 'pre-wrap'
if (window.isSecureContext) {
label.textContent =
'Unable to load webcam, please ensure access is granted:\n' +
error.message
} else {
label.textContent =
'Unable to load webcam. A secure context is required, if you are not accessing ComfyUI on localhost (127.0.0.1) you will have to enable TLS (https)\n\n' +
error.message
}
container.replaceChildren(label)
}
}
const capture = () => {
const data = videoCanvas.toDataURL('image/png')
const img = new Image()
img.onload = () => {
// node.imgs = [img]
app.graph.setDirtyCanvas(true)
}
img.src = data
}
cameraWidget.serializeValue = async () => {
capture()
// Upload image to temp storage
const blob = await new Promise((r) => videoCanvas.toBlob(r))
const name = `webcam_${node.id}.png`
const file = new File([blob], name)
const body = new FormData()
body.append('image', file)
body.append('subfolder', 'webcam')
body.append('type', 'temp')
body.append('overwrite', 'true')
const resp = await api.fetchApi('/upload/image', {
method: 'POST',
body
})
if (resp.status !== 200) {
const err = `Error uploading container image: ${resp.status} - ${resp.statusText}`
throw new Error(err)
}
return `webcam/${name} [temp]`
}
const init = async () => {
// safari needs a call to getUserMedia before being able to list devices
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false })
stream.getTracks().forEach((track) => track.stop())
const devices = await listVideoDevices()
// Build mappings between device IDs and labels
const deviceIdToLabel = {}
const deviceLabelToId = {}
const deviceLabels = []
devices.forEach(device => {
const label = device.label || device.deviceId
deviceIdToLabel[device.deviceId] = label
deviceLabelToId[label] = device.deviceId
deviceLabels.push(label)
})
// Store current value (device ID) and convert to label for display
const currentDeviceId = deviceWidget.value
const currentLabel = deviceIdToLabel[currentDeviceId] || deviceLabels[0] || ''
const widgetIndex = node.widgets.indexOf(deviceWidget)
// Remove the old string widget
node.widgets.splice(widgetIndex, 1)
// Create a new combo widget that displays labels
const newDeviceWidget = node.addWidget(
"combo",
"device",
currentLabel,
async (value) => {
await loadVideo()
},
{
values: deviceLabels,
serialize: true
}
)
// Move the new widget to the original position
node.widgets.splice(node.widgets.indexOf(newDeviceWidget), 1)
node.widgets.splice(widgetIndex, 0, newDeviceWidget)
// Override serializeValue to return the device ID instead of the label
newDeviceWidget.serializeValue = () => {
return deviceLabelToId[newDeviceWidget.value] || newDeviceWidget.value
}
// Create a proxy object so loadVideo gets the device ID, not the label
deviceWidget = {
get value() {
return deviceLabelToId[newDeviceWidget.value] || newDeviceWidget.value
},
set value(v) {
// If setting by device ID, convert to label for display
newDeviceWidget.value = deviceIdToLabel[v] || v
}
}
const reloadOnChange = async () => {
await loadVideo()
}
widthWidget.callback = reloadOnChange
heightWidget.callback = reloadOnChange
frameRateWidget.callback = reloadOnChange
await loadVideo()
step()
}
init()
}
})