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:
Johnpaul
2025-11-27 04:27:22 +01:00
parent 1e384c6735
commit 1c37a3633d
2 changed files with 38 additions and 20 deletions

View File

@@ -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",

View File

@@ -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>