Files
ComfyUI_frontend/src/extensions/core/webcamCapture.ts
pythongosssss ac728b92ae fix: fix webcam node not showing preview in nodes 2.0 (#11549)
## Summary

Adds test coverage for webcam node & fixes issue found in testing where
the captured image does not show in nodes 2.0

## Changes

- **What**: 
- call `setNodePreviewsByNodeId` alongside `node.imgs = [img]`
- add tests for general coverage

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11549-fix-fix-webcam-node-not-showing-preview-in-nodes-2-0-34a6d73d3650810c89eee9c25cd07700)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-04-22 11:29:08 -07:00

171 lines
5.7 KiB
TypeScript

import { t } from '@/i18n'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
import { api } from '../../scripts/api'
import { app } from '../../scripts/app'
const WEBCAM_READY = Symbol()
app.registerExtension({
name: 'Comfy.WebcamCapture',
getCustomWidgets() {
return {
WEBCAM(node, inputName) {
// @ts-expect-error fixme ts strict error
let res
// @ts-expect-error fixme ts strict error
node[WEBCAM_READY] = new Promise((resolve) => (res = resolve))
const container = document.createElement('div')
container.style.background = 'rgba(0,0,0,0.25)'
container.style.textAlign = 'center'
const video = document.createElement('video')
video.style.height = video.style.width = '100%'
const loadVideo = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: false
})
container.replaceChildren(video)
// @ts-expect-error fixme ts strict error
setTimeout(() => res(video), 500) // Fallback as loadedmetadata doesnt fire sometimes?
// @ts-expect-error fixme ts strict error
video.addEventListener('loadedmetadata', () => res(video), 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' +
// @ts-expect-error fixme ts strict error
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' +
// @ts-expect-error fixme ts strict error
error.message
}
container.replaceChildren(label)
}
}
loadVideo()
return { widget: node.addDOMWidget(inputName, 'WEBCAM', container) }
}
}
},
nodeCreated(node: LGraphNode) {
if ((node.type, node.constructor.comfyClass !== 'WebcamCapture')) return
// @ts-expect-error fixme ts strict error
let video
// @ts-expect-error fixme ts strict error
const camera = node.widgets.find((w) => w.name === 'image')
// @ts-expect-error fixme ts strict error
const w = node.widgets.find((w) => w.name === 'width')
// @ts-expect-error fixme ts strict error
const h = node.widgets.find((w) => w.name === 'height')
// @ts-expect-error fixme ts strict error
const captureOnQueue = node.widgets.find(
(w) => w.name === 'capture_on_queue'
)
const canvas = document.createElement('canvas')
const nodeOutputStore = useNodeOutputStore()
const capture = () => {
// @ts-expect-error widget value type narrow down
canvas.width = w.value
// @ts-expect-error widget value type narrow down
canvas.height = h.value
const ctx = canvas.getContext('2d')
// @ts-expect-error widget value type narrow down
ctx.drawImage(video, 0, 0, w.value, h.value)
const data = canvas.toDataURL('image/png')
const img = new Image()
img.onload = () => {
node.imgs = [img]
nodeOutputStore.setNodePreviewsByNodeId(node.id, [data])
app.canvas.setDirty(true)
}
img.src = data
}
const btn = node.addWidget(
'button',
'waiting for camera...',
'capture',
capture,
{}
)
btn.disabled = true
btn.serializeValue = () => undefined
// @ts-expect-error fixme ts strict error
camera.serializeValue = async () => {
// @ts-expect-error fixme ts strict error
if (captureOnQueue.value) {
capture()
} else if (!node.imgs?.length) {
const err = `No webcam image captured`
useToastStore().addAlert(err)
throw new Error(err)
}
// Upload image to temp storage
// @ts-expect-error fixme ts strict error
const blob = await new Promise<Blob>((r) => canvas.toBlob(r))
const name = `${+new Date()}.png`
const file = new File([blob], name)
const body = new FormData()
body.append('image', file)
body.append('subfolder', 'webcam')
body.append('type', 'temp')
const resp = await api.fetchApi('/upload/image', {
method: 'POST',
body
})
if (resp.status !== 200) {
const err = `Error uploading camera image: ${resp.status} - ${resp.statusText}`
useToastStore().addAlert(err)
throw new Error(err)
}
const data = await resp.json()
const serverName = data.name || name
const subfolder = data.subfolder || 'webcam'
const type = data.type || 'temp'
return `${subfolder}/${serverName} [${type}]`
}
// @ts-expect-error fixme ts strict error
node[WEBCAM_READY].then((v) => {
video = v
// If width isn't specified then use video output resolution
// @ts-expect-error fixme ts strict error
if (!w.value) {
// @ts-expect-error fixme ts strict error
w.value = video.videoWidth || 640
// @ts-expect-error fixme ts strict error
h.value = video.videoHeight || 480
}
btn.disabled = false
btn.label = t('g.capture')
})
}
})