mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 11:11:53 +00:00
refactor: use instance-specific canvas and video elements
- Move canvas and persistentVideo creation to onMounted - Store as refs for proper lifecycle management - Clean up elements in onUnmounted to prevent memory leaks - Prevents potential conflicts if multiple webcam widgets exist
This commit is contained in:
@@ -147,7 +147,7 @@
|
|||||||
"control_before_generate": "control before generate",
|
"control_before_generate": "control before generate",
|
||||||
"choose_file_to_upload": "choose file to upload",
|
"choose_file_to_upload": "choose file to upload",
|
||||||
"capture": "capture",
|
"capture": "capture",
|
||||||
"capturePhoto": "Capture photo",
|
"capturePhoto": "Capture Photo",
|
||||||
"captureModeOnRun": "On Run",
|
"captureModeOnRun": "On Run",
|
||||||
"captureModeManual": "Manually",
|
"captureModeManual": "Manually",
|
||||||
"capturedImage": "Captured Image",
|
"capturedImage": "Captured Image",
|
||||||
|
|||||||
@@ -103,12 +103,9 @@ const stream = ref<MediaStream | null>(null)
|
|||||||
// Track pending video event listeners for cleanup
|
// Track pending video event listeners for cleanup
|
||||||
const pendingVideoCleanup = ref<(() => void) | null>(null)
|
const pendingVideoCleanup = ref<(() => void) | null>(null)
|
||||||
const isHovered = useElementHover(videoContainerRef)
|
const isHovered = useElementHover(videoContainerRef)
|
||||||
const canvas = document.createElement('canvas')
|
// Instance-specific elements for capture - created per component instance
|
||||||
// Persistent video element for capture - not in DOM template but keeps stream active
|
const canvas = ref<HTMLCanvasElement | null>(null)
|
||||||
const persistentVideo = document.createElement('video')
|
const persistentVideo = ref<HTMLVideoElement | null>(null)
|
||||||
persistentVideo.autoplay = true
|
|
||||||
persistentVideo.muted = true
|
|
||||||
persistentVideo.playsInline = true
|
|
||||||
const capturedImageUrl = ref<string | null>(null)
|
const capturedImageUrl = ref<string | null>(null)
|
||||||
const lastUploadedPath = ref<string | null>(null)
|
const lastUploadedPath = ref<string | null>(null)
|
||||||
|
|
||||||
@@ -422,7 +419,7 @@ function showWidgets() {
|
|||||||
|
|
||||||
const captureWidget = createActionWidget({
|
const captureWidget = createActionWidget({
|
||||||
name: CAPTURE_WIDGET_NAME,
|
name: CAPTURE_WIDGET_NAME,
|
||||||
label: t('g.captureImage', 'Capture Photo'),
|
label: t('g.capturePhoto', 'Capture Photo'),
|
||||||
iconClass: 'icon-[lucide--camera]',
|
iconClass: 'icon-[lucide--camera]',
|
||||||
onClick: () => captureImage(node)
|
onClick: () => captureImage(node)
|
||||||
})
|
})
|
||||||
@@ -451,8 +448,8 @@ function capturePhoto(node: LGraphNode) {
|
|||||||
|
|
||||||
// Use visible video element if available, otherwise use persistent video
|
// Use visible video element if available, otherwise use persistent video
|
||||||
const videoElement =
|
const videoElement =
|
||||||
videoRef.value ?? (stream.value?.active ? persistentVideo : null)
|
videoRef.value ?? (stream.value?.active ? persistentVideo.value : null)
|
||||||
if (!videoElement) return null
|
if (!videoElement || !canvas.value) return null
|
||||||
|
|
||||||
const widthWidget = node.widgets?.find((w) => toRaw(w).name === 'width')
|
const widthWidget = node.widgets?.find((w) => toRaw(w).name === 'width')
|
||||||
const heightWidget = node.widgets?.find((w) => toRaw(w).name === 'height')
|
const heightWidget = node.widgets?.find((w) => toRaw(w).name === 'height')
|
||||||
@@ -460,14 +457,14 @@ function capturePhoto(node: LGraphNode) {
|
|||||||
const width = (widthWidget?.value as number) || DEFAULT_VIDEO_WIDTH
|
const width = (widthWidget?.value as number) || DEFAULT_VIDEO_WIDTH
|
||||||
const height = (heightWidget?.value as number) || DEFAULT_VIDEO_HEIGHT
|
const height = (heightWidget?.value as number) || DEFAULT_VIDEO_HEIGHT
|
||||||
|
|
||||||
canvas.width = width
|
canvas.value.width = width
|
||||||
canvas.height = height
|
canvas.value.height = height
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d')
|
const ctx = canvas.value.getContext('2d')
|
||||||
if (!ctx) return null
|
if (!ctx) return null
|
||||||
|
|
||||||
ctx.drawImage(videoElement, 0, 0, width, height)
|
ctx.drawImage(videoElement, 0, 0, width, height)
|
||||||
return canvas.toDataURL('image/png')
|
return canvas.value.toDataURL('image/png')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadImage(
|
async function uploadImage(
|
||||||
@@ -475,8 +472,10 @@ async function uploadImage(
|
|||||||
node: LGraphNode
|
node: LGraphNode
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
|
if (!canvas.value) throw new Error('Canvas not initialized')
|
||||||
|
|
||||||
const blob = await new Promise<Blob>((resolve, reject) => {
|
const blob = await new Promise<Blob>((resolve, reject) => {
|
||||||
canvas.toBlob((b) => {
|
canvas.value!.toBlob((b) => {
|
||||||
if (b) resolve(b)
|
if (b) resolve(b)
|
||||||
else reject(new Error('Failed to convert canvas to blob'))
|
else reject(new Error('Failed to convert canvas to blob'))
|
||||||
})
|
})
|
||||||
@@ -573,9 +572,12 @@ async function startCameraPreview() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ensure persistent video also has the stream for background capture
|
// Ensure persistent video also has the stream for background capture
|
||||||
if (!persistentVideo.srcObject || persistentVideo.paused) {
|
if (
|
||||||
persistentVideo.srcObject = stream.value
|
persistentVideo.value &&
|
||||||
await persistentVideo.play()
|
(!persistentVideo.value.srcObject || persistentVideo.value.paused)
|
||||||
|
) {
|
||||||
|
persistentVideo.value.srcObject = stream.value
|
||||||
|
await persistentVideo.value.play()
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -588,8 +590,10 @@ async function startCameraPreview() {
|
|||||||
|
|
||||||
stream.value = cameraStream
|
stream.value = cameraStream
|
||||||
// Attach stream to persistent video for capture when UI video is hidden
|
// Attach stream to persistent video for capture when UI video is hidden
|
||||||
persistentVideo.srcObject = cameraStream
|
if (persistentVideo.value) {
|
||||||
await persistentVideo.play()
|
persistentVideo.value.srcObject = cameraStream
|
||||||
|
await persistentVideo.value.play()
|
||||||
|
}
|
||||||
isShowingPreview.value = true
|
isShowingPreview.value = true
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|
||||||
@@ -677,6 +681,13 @@ function stopStreamTracks() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
// Create instance-specific elements for capture
|
||||||
|
canvas.value = document.createElement('canvas')
|
||||||
|
persistentVideo.value = document.createElement('video')
|
||||||
|
persistentVideo.value.autoplay = true
|
||||||
|
persistentVideo.value.muted = true
|
||||||
|
persistentVideo.value.playsInline = true
|
||||||
|
|
||||||
// Order matters: first set defaults via hideWidgets, THEN store original widgets
|
// Order matters: first set defaults via hideWidgets, THEN store original widgets
|
||||||
// This ensures restoreWidgets() will restore the correct default values
|
// This ensures restoreWidgets() will restore the correct default values
|
||||||
hideWidgets()
|
hideWidgets()
|
||||||
@@ -691,5 +702,12 @@ onUnmounted(() => {
|
|||||||
pendingVideoCleanup.value?.()
|
pendingVideoCleanup.value?.()
|
||||||
stopStreamTracks()
|
stopStreamTracks()
|
||||||
restoreWidgets()
|
restoreWidgets()
|
||||||
|
|
||||||
|
// Clean up instance-specific elements
|
||||||
|
if (persistentVideo.value) {
|
||||||
|
persistentVideo.value.srcObject = null
|
||||||
|
persistentVideo.value = null
|
||||||
|
}
|
||||||
|
canvas.value = null
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user