From 1c37a3633d60c2a4cd28de0edb8f1ebd26674106 Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Thu, 27 Nov 2025 04:27:22 +0100 Subject: [PATCH] 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 --- src/locales/en/main.json | 2 +- .../widgets/components/WidgetWebcam.vue | 56 ++++++++++++------- 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/src/locales/en/main.json b/src/locales/en/main.json index c30efcb5d..e9e013edd 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -147,7 +147,7 @@ "control_before_generate": "control before generate", "choose_file_to_upload": "choose file to upload", "capture": "capture", - "capturePhoto": "Capture photo", + "capturePhoto": "Capture Photo", "captureModeOnRun": "On Run", "captureModeManual": "Manually", "capturedImage": "Captured Image", diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetWebcam.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetWebcam.vue index e15573c42..9541f4360 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetWebcam.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetWebcam.vue @@ -103,12 +103,9 @@ const stream = ref(null) // Track pending video event listeners for cleanup const pendingVideoCleanup = ref<(() => void) | 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 +// Instance-specific elements for capture - created per component instance +const canvas = ref(null) +const persistentVideo = ref(null) const capturedImageUrl = ref(null) const lastUploadedPath = ref(null) @@ -422,7 +419,7 @@ function showWidgets() { const captureWidget = createActionWidget({ name: CAPTURE_WIDGET_NAME, - label: t('g.captureImage', 'Capture Photo'), + label: t('g.capturePhoto', 'Capture Photo'), iconClass: 'icon-[lucide--camera]', onClick: () => captureImage(node) }) @@ -451,8 +448,8 @@ function capturePhoto(node: LGraphNode) { // Use visible video element if available, otherwise use persistent video const videoElement = - videoRef.value ?? (stream.value?.active ? persistentVideo : null) - if (!videoElement) return null + videoRef.value ?? (stream.value?.active ? persistentVideo.value : null) + if (!videoElement || !canvas.value) return null const widthWidget = node.widgets?.find((w) => toRaw(w).name === 'width') 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 height = (heightWidget?.value as number) || DEFAULT_VIDEO_HEIGHT - canvas.width = width - canvas.height = height + canvas.value.width = width + canvas.value.height = height - const ctx = canvas.getContext('2d') + const ctx = canvas.value.getContext('2d') if (!ctx) return null ctx.drawImage(videoElement, 0, 0, width, height) - return canvas.toDataURL('image/png') + return canvas.value.toDataURL('image/png') } async function uploadImage( @@ -475,8 +472,10 @@ async function uploadImage( node: LGraphNode ): Promise { try { + if (!canvas.value) throw new Error('Canvas not initialized') + const blob = await new Promise((resolve, reject) => { - canvas.toBlob((b) => { + canvas.value!.toBlob((b) => { if (b) resolve(b) 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 - if (!persistentVideo.srcObject || persistentVideo.paused) { - persistentVideo.srcObject = stream.value - await persistentVideo.play() + if ( + persistentVideo.value && + (!persistentVideo.value.srcObject || persistentVideo.value.paused) + ) { + persistentVideo.value.srcObject = stream.value + await persistentVideo.value.play() } return @@ -588,8 +590,10 @@ async function startCameraPreview() { stream.value = cameraStream // Attach stream to persistent video for capture when UI video is hidden - persistentVideo.srcObject = cameraStream - await persistentVideo.play() + if (persistentVideo.value) { + persistentVideo.value.srcObject = cameraStream + await persistentVideo.value.play() + } isShowingPreview.value = true await nextTick() @@ -677,6 +681,13 @@ function stopStreamTracks() { } 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 // This ensures restoreWidgets() will restore the correct default values hideWidgets() @@ -691,5 +702,12 @@ onUnmounted(() => { pendingVideoCleanup.value?.() stopStreamTracks() restoreWidgets() + + // Clean up instance-specific elements + if (persistentVideo.value) { + persistentVideo.value.srcObject = null + persistentVideo.value = null + } + canvas.value = null })