mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-02 22:37:32 +00:00
fix: preserve widget object identity for proper reactivity in webcam capture
Mutate widget objects in place instead of creating new instances to fix a reactivity issue where serializeValue closure captured stale widget references. Also adds persistent video element for background capture when UI video is hidden. Fixes capture_on_queue toggle not being read correctly during serialization.
This commit is contained in:
@@ -144,6 +144,16 @@
|
||||
"choose_file_to_upload": "choose file to upload",
|
||||
"capture": "capture",
|
||||
"capturePhoto": "Capture photo",
|
||||
"captureModeOnRun": "On Run",
|
||||
"captureModeManual": "Manually",
|
||||
"captureImage": "Capture",
|
||||
"retakePhoto": "Retake photo",
|
||||
"clickToStopLivePreview": "Click to stop live preview",
|
||||
"failedToCaptureImage": "Failed to capture image",
|
||||
"noWebcamImageCaptured": "No webcam image captured",
|
||||
"errorCapturingImage": "Error capturing image: {error}",
|
||||
"unableToLoadWebcam": "Unable to load webcam: {error}",
|
||||
"webcamRequiresTLS": "Unable to load webcam. TLS is required when not on localhost. Error: {error}",
|
||||
"turnOnCamera": "Turn on Camera",
|
||||
"nodes": "Nodes",
|
||||
"community": "Community",
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex size-12 items-center justify-center rounded-full bg-danger"
|
||||
class="flex size-6 bg-red-600 items-center justify-center bg-danger"
|
||||
>
|
||||
<i class="icon-[lucide--square] size-6 bg-red-400" />
|
||||
</div>
|
||||
@@ -84,6 +84,11 @@ const videoContainerRef = ref<HTMLElement>()
|
||||
const stream = ref<MediaStream | null>(null)
|
||||
const isHovered = useElementHover(videoContainerRef)
|
||||
const canvas = document.createElement('canvas')
|
||||
// Persistent video element for capture - not in DOM template but keeps stream active
|
||||
const persistentVideo = document.createElement('video')
|
||||
persistentVideo.autoplay = true
|
||||
persistentVideo.muted = true
|
||||
persistentVideo.playsInline = true
|
||||
const capturedImageUrl = ref<string | null>(null)
|
||||
const lastUploadedPath = ref<string | null>(null)
|
||||
|
||||
@@ -141,31 +146,27 @@ function applyWidgetVisibility(
|
||||
if (!TOGGLED_WIDGET_NAMES.has(widget.name)) return widget
|
||||
|
||||
if (widget.name === 'capture_on_queue') {
|
||||
return {
|
||||
...widget,
|
||||
type: 'selectToggle',
|
||||
label: 'Capture Image',
|
||||
value: widget.value ?? false,
|
||||
options: {
|
||||
...widget.options,
|
||||
hidden,
|
||||
values: [
|
||||
{ label: 'On Run', value: true },
|
||||
{ label: 'Manually', value: false }
|
||||
]
|
||||
}
|
||||
// Mutate in place to preserve object identity for serializeValue closure
|
||||
widget.type = 'selectToggle'
|
||||
widget.label = 'Capture Image'
|
||||
widget.value = widget.value ?? false
|
||||
widget.options = {
|
||||
...widget.options,
|
||||
hidden,
|
||||
values: [
|
||||
{ label: 'On Run', value: true },
|
||||
{ label: 'Manually', value: false }
|
||||
]
|
||||
}
|
||||
return widget
|
||||
}
|
||||
|
||||
// For width/height, explicitly preserve the value to ensure Vue reactivity works
|
||||
return {
|
||||
...widget,
|
||||
value: widget.value,
|
||||
options: {
|
||||
...widget.options,
|
||||
hidden
|
||||
}
|
||||
// For width/height, mutate options in place
|
||||
widget.options = {
|
||||
...widget.options,
|
||||
hidden
|
||||
}
|
||||
return widget
|
||||
}
|
||||
|
||||
interface ActionWidgetConfig {
|
||||
@@ -216,12 +217,12 @@ function hideWidgets() {
|
||||
if (!node.widgets?.length) return
|
||||
|
||||
// Set default values AND apply visibility in one pass
|
||||
// We must replace node.widgets to trigger Vue reactivity (shallowReactive)
|
||||
// Mutate widgets in place to preserve object identity for serializeValue closure
|
||||
updateNodeWidgets(
|
||||
node,
|
||||
(widgets) =>
|
||||
widgets.map((widget) => {
|
||||
let updatedWidget = applyWidgetVisibility(widget, true)
|
||||
applyWidgetVisibility(widget, true)
|
||||
|
||||
// Set default values for width and height if not already set
|
||||
const needsDefault =
|
||||
@@ -231,13 +232,13 @@ function hideWidgets() {
|
||||
widget.value === ''
|
||||
|
||||
if (widget.name === 'width' && needsDefault) {
|
||||
updatedWidget = { ...updatedWidget, value: 640 }
|
||||
widget.value = 640
|
||||
}
|
||||
if (widget.name === 'height' && needsDefault) {
|
||||
updatedWidget = { ...updatedWidget, value: 480 }
|
||||
widget.value = 480
|
||||
}
|
||||
|
||||
return updatedWidget
|
||||
return widget
|
||||
}),
|
||||
{ dirtyCanvas: false }
|
||||
)
|
||||
@@ -256,14 +257,14 @@ function setupSerializeValue() {
|
||||
|
||||
imageWidget.serializeValue = async () => {
|
||||
const captureOnQueueWidget = node.widgets?.find(
|
||||
(w) => toRaw(w).name === 'capture_on_queue'
|
||||
(w) => w.name === 'capture_on_queue'
|
||||
)
|
||||
|
||||
// Strictly check for boolean true (On Run mode)
|
||||
const shouldCaptureOnQueue = captureOnQueueWidget?.value === true
|
||||
|
||||
if (shouldCaptureOnQueue) {
|
||||
// Auto-capture when queued
|
||||
// Auto-capture when queued - capture and upload immediately
|
||||
const dataUrl = capturePhoto(node)
|
||||
if (!dataUrl) {
|
||||
const err = t('g.failedToCaptureImage', 'Failed to capture image')
|
||||
@@ -309,7 +310,12 @@ function showWidgets() {
|
||||
}
|
||||
|
||||
function capturePhoto(node: LGraphNode) {
|
||||
if (!node || !videoRef.value) return null
|
||||
if (!node) return null
|
||||
|
||||
// Use visible video element if available, otherwise use persistent video
|
||||
const videoElement =
|
||||
videoRef.value ?? (stream.value?.active ? persistentVideo : null)
|
||||
if (!videoElement) return null
|
||||
|
||||
const widthWidget = node.widgets?.find((w) => toRaw(w).name === 'width')
|
||||
const heightWidget = node.widgets?.find((w) => toRaw(w).name === 'height')
|
||||
@@ -323,7 +329,7 @@ function capturePhoto(node: LGraphNode) {
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return null
|
||||
|
||||
ctx.drawImage(videoRef.value, 0, 0, width, height)
|
||||
ctx.drawImage(videoElement, 0, 0, width, height)
|
||||
return canvas.toDataURL('image/png')
|
||||
}
|
||||
|
||||
@@ -422,6 +428,12 @@ async function startCameraPreview() {
|
||||
await videoRef.value.play()
|
||||
}
|
||||
|
||||
// Ensure persistent video also has the stream for background capture
|
||||
if (!persistentVideo.srcObject || persistentVideo.paused) {
|
||||
persistentVideo.srcObject = stream.value
|
||||
await persistentVideo.play()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -431,6 +443,9 @@ async function startCameraPreview() {
|
||||
})
|
||||
|
||||
stream.value = cameraStream
|
||||
// Attach stream to persistent video for capture when UI video is hidden
|
||||
persistentVideo.srcObject = cameraStream
|
||||
await persistentVideo.play()
|
||||
isShowingPreview.value = true
|
||||
await nextTick()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user