From a1e3af7c19a59e451a4f29ef096d1282c0603637 Mon Sep 17 00:00:00 2001 From: RyanOnTheInside <7623207+ryanontheinside@users.noreply.github.com> Date: Sat, 3 May 2025 16:47:53 -0400 Subject: [PATCH 1/6] remove cropping this logic was duplicative. there are native nodes in comfyui for both resizing and cropping. --- node_wrappers/utils/misc_nodes.py | 21 +++++++++++---------- web/js/webcam.js | 12 ++++-------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/node_wrappers/utils/misc_nodes.py b/node_wrappers/utils/misc_nodes.py index d04a02e..cdac6de 100644 --- a/node_wrappers/utils/misc_nodes.py +++ b/node_wrappers/utils/misc_nodes.py @@ -55,9 +55,7 @@ class FastWebcamCapture: return { "required": { "image": ("WEBCAM", {}), - "width": ("INT", {"default": 640, "min": 0, "max": MAX_RESOLUTION, "step": 1}), - "height": ("INT", {"default": 480, "min": 0, "max": MAX_RESOLUTION, "step": 1}), - "capture_on_queue": ("BOOLEAN", {"default": True}), + "capture_on_queue": ("BOOLEAN", {"default": True, "tooltip": "Whether to capture the frame when the node is queued"}), } } @@ -66,7 +64,13 @@ class FastWebcamCapture: CATEGORY = "image" - def process_capture(self, image, width, height, capture_on_queue): + def process_capture(self, image, capture_on_queue): + """ + Process a webcam image at native resolution. + + This node simply captures the webcam feed at its native resolution. + For resizing or cropping, use standard ComfyUI nodes after this one. + """ # Check if we got a data URL if isinstance(image, str) and image.startswith("data:image/"): # Extract the base64 data after the comma @@ -75,16 +79,13 @@ class FastWebcamCapture: # Convert base64 to PIL Image buffer = BytesIO(base64.b64decode(base64_data)) pil_image = Image.open(buffer).convert("RGB") + + # Log the image size we're receiving from the webcam + print(f"Webcam image size: {pil_image.width}x{pil_image.height}") # Convert PIL to numpy array image = np.array(pil_image) - # Handle resize if requested - if width > 0 and height > 0: - import cv2 - - image = cv2.resize(image, (width, height), interpolation=cv2.INTER_AREA) - # Convert to float32 and normalize to 0-1 range image = image.astype(np.float32) / 255.0 diff --git a/web/js/webcam.js b/web/js/webcam.js index aef382e..1076fc4 100644 --- a/web/js/webcam.js +++ b/web/js/webcam.js @@ -66,9 +66,6 @@ app.registerExtension({ const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; const camera = this.widgets.find((w) => w.name === "image"); - const w = this.widgets.find((w) => w.name === "width"); - const h = this.widgets.find((w) => w.name === "height"); - const canvas = document.createElement("canvas"); camera.serializeValue = async () => { @@ -82,20 +79,19 @@ app.registerExtension({ } console.log("Capturing frame from video:", { - paused: webcamVideo.paused, - readyState: webcamVideo.readyState, videoWidth: webcamVideo.videoWidth, videoHeight: webcamVideo.videoHeight }); - canvas.width = w.value || webcamVideo.videoWidth || 640; - canvas.height = h.value || webcamVideo.videoHeight || 480; + // Always use native webcam resolution + canvas.width = webcamVideo.videoWidth; + canvas.height = webcamVideo.videoHeight; const ctx = canvas.getContext("2d"); ctx.drawImage(webcamVideo, 0, 0, canvas.width, canvas.height); const dataUrl = canvas.toDataURL("image/png"); - // console.log("Successfully captured and converted frame to data URL"); + console.log("Captured frame at native resolution:", canvas.width, "x", canvas.height); return dataUrl; }; From 59c59e8232bbbbd4c7ed2f0004d88fb2438ad8a2 Mon Sep 17 00:00:00 2001 From: RyanOnTheInside <7623207+ryanontheinside@users.noreply.github.com> Date: Sat, 3 May 2025 17:11:45 -0400 Subject: [PATCH 2/6] draft video cap --- node_wrappers/utils/misc_nodes.py | 146 +++++++++++++++++++++++++----- web/js/webcam.js | 104 +++++++++++++++++---- 2 files changed, 208 insertions(+), 42 deletions(-) diff --git a/node_wrappers/utils/misc_nodes.py b/node_wrappers/utils/misc_nodes.py index cdac6de..af8291e 100644 --- a/node_wrappers/utils/misc_nodes.py +++ b/node_wrappers/utils/misc_nodes.py @@ -55,47 +55,143 @@ class FastWebcamCapture: return { "required": { "image": ("WEBCAM", {}), - "capture_on_queue": ("BOOLEAN", {"default": True, "tooltip": "Whether to capture the frame when the node is queued"}), + "record_seconds": ( + "FLOAT", + { + "default": 0.0, + "min": 0.0, + "max": 60.0, + "step": 0.5, + "tooltip": "Length of video to record in seconds. 0 means capture a single image (no video)." + } + ), } } - RETURN_TYPES = ("IMAGE",) + RETURN_TYPES = ("IMAGE", "AUDIO") + RETURN_NAMES = ("image", "audio") FUNCTION = "process_capture" CATEGORY = "image" - def process_capture(self, image, capture_on_queue): + def process_capture(self, image, record_seconds): """ - Process a webcam image at native resolution. + Process a webcam image or video at native resolution. - This node simply captures the webcam feed at its native resolution. + This node captures the webcam feed at its native resolution. + If record_seconds > 0, it will record a video for the specified duration. For resizing or cropping, use standard ComfyUI nodes after this one. """ - # Check if we got a data URL - if isinstance(image, str) and image.startswith("data:image/"): - # Extract the base64 data after the comma - base64_data = re.sub("^data:image/.+;base64,", "", image) + import torchaudio + import torch + import numpy as np + import time + import os + import tempfile + import subprocess + from pathlib import Path + + # Create empty audio dict as default + empty_audio = {"waveform": torch.zeros((1, 16000)), "sample_rate": 16000} + + # Check if we're receiving image or video data + if isinstance(image, str): + # Image data URL + if image.startswith("data:image/"): + # Extract the base64 data after the comma + base64_data = re.sub("^data:image/.+;base64,", "", image) - # Convert base64 to PIL Image - buffer = BytesIO(base64.b64decode(base64_data)) - pil_image = Image.open(buffer).convert("RGB") - - # Log the image size we're receiving from the webcam - print(f"Webcam image size: {pil_image.width}x{pil_image.height}") + # Convert base64 to PIL Image + buffer = BytesIO(base64.b64decode(base64_data)) + pil_image = Image.open(buffer).convert("RGB") + + # Log the image size we're receiving from the webcam + print(f"Webcam image size: {pil_image.width}x{pil_image.height}") - # Convert PIL to numpy array - image = np.array(pil_image) + # Convert PIL to numpy array + image_np = np.array(pil_image) - # Convert to float32 and normalize to 0-1 range - image = image.astype(np.float32) / 255.0 + # Convert to float32 and normalize to 0-1 range + image_np = image_np.astype(np.float32) / 255.0 - # Add batch dimension and convert to torch tensor - # ComfyUI expects BHWC format - image = torch.from_numpy(image)[None, ...] - - return (image,) + # Add batch dimension and convert to torch tensor + # ComfyUI expects BHWC format + image_tensor = torch.from_numpy(image_np)[None, ...] + + return (image_tensor, empty_audio) + + # Video data URL + elif image.startswith("data:video/"): + print("Received video data from webcam") + + try: + # Extract the base64 data after the comma + mime_pattern = "data:video/.+;base64," + base64_data = re.sub(f"^{mime_pattern}", "", image) + + # Create a temporary file to store the video + with tempfile.NamedTemporaryFile(suffix='.webm', delete=False) as video_file: + video_path = video_file.name + video_file.write(base64.b64decode(base64_data)) + + # Process video with ffmpeg to extract frames + frames_dir = tempfile.mkdtemp() + frames_path = os.path.join(frames_dir, "frame_%04d.png") + + # Extract frames with ffmpeg + subprocess.run([ + "ffmpeg", "-i", video_path, + "-vf", "fps=30", + frames_path + ], check=True, capture_output=True) + + # Load extracted frames + frames = [] + frame_files = sorted([f for f in os.listdir(frames_dir) if f.startswith("frame_")]) + + if not frame_files: + raise ValueError("No frames extracted from video") + + # Load all frames + for frame_file in frame_files: + frame_path = os.path.join(frames_dir, frame_file) + pil_frame = Image.open(frame_path).convert("RGB") + frame_np = np.array(pil_frame).astype(np.float32) / 255.0 + frames.append(torch.from_numpy(frame_np)) + + # Extract audio with torchaudio + try: + waveform, sample_rate = torchaudio.load(video_path) + audio = {"waveform": waveform, "sample_rate": sample_rate} + except Exception as e: + print(f"Error extracting audio: {e}") + audio = empty_audio + + # Stack frames to create video tensor [B, H, W, C] + video_tensor = torch.stack(frames)[None, ...] + + # Clean up temporary files + try: + os.unlink(video_path) + for frame_file in frame_files: + os.unlink(os.path.join(frames_dir, frame_file)) + os.rmdir(frames_dir) + except Exception as e: + print(f"Error cleaning up temp files: {e}") + + print(f"Processed video with {len(frames)} frames") + return (video_tensor, audio) + + except Exception as e: + print(f"Error processing video: {e}") + # Fall back to empty frame and audio if video processing fails + fallback_tensor = torch.zeros((1, 480, 640, 3)) + return (fallback_tensor, empty_audio) + + else: + raise ValueError(f"Unrecognized data URL format: {image[:30]}...") else: - raise ValueError("Invalid image format received from webcam") + raise ValueError("Invalid data received from webcam") coco_classes = [ diff --git a/web/js/webcam.js b/web/js/webcam.js index 1076fc4..5bb3607 100644 --- a/web/js/webcam.js +++ b/web/js/webcam.js @@ -9,6 +9,8 @@ app.registerExtension({ // Store webcam stream at extension level to avoid reinitializing let webcamStream = null; let webcamVideo = null; + let mediaRecorder = null; + let recordedChunks = []; // Initialize webcam once for all nodes const initWebcam = async () => { @@ -17,7 +19,7 @@ app.registerExtension({ try { webcamStream = await navigator.mediaDevices.getUserMedia({ video: true, - audio: false + audio: true // Now requesting audio too for video recording }); // console.log("Got media stream:", webcamStream); @@ -40,6 +42,60 @@ app.registerExtension({ } return { stream: webcamStream, video: webcamVideo }; }; + + // Function to record video for a specified duration + const recordVideo = async (duration) => { + return new Promise(async (resolve, reject) => { + try { + // Initialize webcam if not already done + const { stream, video } = await initWebcam(); + + // Clear previous recording data + recordedChunks = []; + + // Initialize MediaRecorder + mediaRecorder = new MediaRecorder(stream, { + mimeType: 'video/webm;codecs=vp9,opus' + }); + + // Handle data available event + mediaRecorder.ondataavailable = (event) => { + if (event.data.size > 0) { + recordedChunks.push(event.data); + } + }; + + // Handle recording stop event + mediaRecorder.onstop = () => { + // Create a blob from recorded chunks + const blob = new Blob(recordedChunks, { type: 'video/webm' }); + + // Convert blob to base64 data URL + const reader = new FileReader(); + reader.onloadend = () => { + const base64data = reader.result; + resolve(base64data); + }; + reader.readAsDataURL(blob); + }; + + // Start recording + mediaRecorder.start(); + console.log(`Recording started for ${duration} seconds`); + + // Stop recording after specified duration + setTimeout(() => { + if (mediaRecorder && mediaRecorder.state !== 'inactive') { + mediaRecorder.stop(); + console.log('Recording stopped'); + } + }, duration * 1000); + } catch (error) { + console.error("Error during video recording:", error); + reject(error); + } + }); + }; nodeType.prototype.getCustomWidgets = function() { // console.log("Getting custom widgets"); @@ -69,8 +125,6 @@ app.registerExtension({ const canvas = document.createElement("canvas"); camera.serializeValue = async () => { - // console.log("Serializing camera value"); - // Only initialize webcam when actually needed if (!webcamVideo || webcamVideo.paused || webcamVideo.readyState !== 4) { // console.log("Initializing webcam for first use..."); @@ -78,21 +132,37 @@ app.registerExtension({ webcamVideo = video; } - console.log("Capturing frame from video:", { - videoWidth: webcamVideo.videoWidth, - videoHeight: webcamVideo.videoHeight - }); + // Find record_seconds input value + const recordSecondsWidget = this.widgets.find(w => w.name === "record_seconds"); + const recordSeconds = recordSecondsWidget ? recordSecondsWidget.value : 0; - // Always use native webcam resolution - canvas.width = webcamVideo.videoWidth; - canvas.height = webcamVideo.videoHeight; - - const ctx = canvas.getContext("2d"); - ctx.drawImage(webcamVideo, 0, 0, canvas.width, canvas.height); - - const dataUrl = canvas.toDataURL("image/png"); - console.log("Captured frame at native resolution:", canvas.width, "x", canvas.height); - return dataUrl; + // Single image capture mode + if (recordSeconds <= 0) { + console.log("Capturing single frame from video:", { + videoWidth: webcamVideo.videoWidth, + videoHeight: webcamVideo.videoHeight + }); + + // Always use native webcam resolution + canvas.width = webcamVideo.videoWidth; + canvas.height = webcamVideo.videoHeight; + + const ctx = canvas.getContext("2d"); + ctx.drawImage(webcamVideo, 0, 0, canvas.width, canvas.height); + + const dataUrl = canvas.toDataURL("image/png"); + console.log("Captured frame at native resolution:", canvas.width, "x", canvas.height); + return dataUrl; + } + // Video recording mode + else { + console.log(`Starting video recording for ${recordSeconds} seconds`); + + // Record video and return data URL + const videoDataUrl = await recordVideo(recordSeconds); + console.log("Video recording completed, sending data to backend"); + return videoDataUrl; + } }; return r; From e502120cc1631276c084222daa877ccf9ff64741 Mon Sep 17 00:00:00 2001 From: RyanOnTheInside <7623207+ryanontheinside@users.noreply.github.com> Date: Sat, 3 May 2025 17:45:41 -0400 Subject: [PATCH 3/6] record video. --- node_wrappers/utils/misc_nodes.py | 125 +++++++++++++----------------- web/js/webcam.js | 102 ++++++++++++------------ 2 files changed, 109 insertions(+), 118 deletions(-) diff --git a/node_wrappers/utils/misc_nodes.py b/node_wrappers/utils/misc_nodes.py index af8291e..92de1e3 100644 --- a/node_wrappers/utils/misc_nodes.py +++ b/node_wrappers/utils/misc_nodes.py @@ -49,6 +49,7 @@ class DTypeConverter: return (converted,) + class FastWebcamCapture: @classmethod def INPUT_TYPES(s): @@ -68,8 +69,7 @@ class FastWebcamCapture: } } - RETURN_TYPES = ("IMAGE", "AUDIO") - RETURN_NAMES = ("image", "audio") + RETURN_TYPES = ("IMAGE",) FUNCTION = "process_capture" CATEGORY = "image" @@ -82,21 +82,13 @@ class FastWebcamCapture: If record_seconds > 0, it will record a video for the specified duration. For resizing or cropping, use standard ComfyUI nodes after this one. """ - import torchaudio import torch import numpy as np - import time - import os - import tempfile - import subprocess - from pathlib import Path + import json - # Create empty audio dict as default - empty_audio = {"waveform": torch.zeros((1, 16000)), "sample_rate": 16000} - - # Check if we're receiving image or video data + # Check if we're receiving image or JSON frames data if isinstance(image, str): - # Image data URL + # Single image data URL if image.startswith("data:image/"): # Extract the base64 data after the comma base64_data = re.sub("^data:image/.+;base64,", "", image) @@ -118,78 +110,70 @@ class FastWebcamCapture: # ComfyUI expects BHWC format image_tensor = torch.from_numpy(image_np)[None, ...] - return (image_tensor, empty_audio) + return (image_tensor,) - # Video data URL - elif image.startswith("data:video/"): - print("Received video data from webcam") + # JSON frames data from sequence capture + elif image.startswith("{") and "frames" in image: + print("Received frame sequence data from webcam") try: - # Extract the base64 data after the comma - mime_pattern = "data:video/.+;base64," - base64_data = re.sub(f"^{mime_pattern}", "", image) + # Parse JSON data + frame_data = json.loads(image) + frames = frame_data.get("frames", []) + fps = frame_data.get("fps", 30) + duration = frame_data.get("duration", 1.0) - # Create a temporary file to store the video - with tempfile.NamedTemporaryFile(suffix='.webm', delete=False) as video_file: - video_path = video_file.name - video_file.write(base64.b64decode(base64_data)) + if not frames: + raise ValueError("No frames received in frame data") + + print(f"Processing {len(frames)} frames captured at {fps} FPS over {duration} seconds") - # Process video with ffmpeg to extract frames - frames_dir = tempfile.mkdtemp() - frames_path = os.path.join(frames_dir, "frame_%04d.png") + # Convert frames to tensor + tensor_frames = [] - # Extract frames with ffmpeg - subprocess.run([ - "ffmpeg", "-i", video_path, - "-vf", "fps=30", - frames_path - ], check=True, capture_output=True) - - # Load extracted frames - frames = [] - frame_files = sorted([f for f in os.listdir(frames_dir) if f.startswith("frame_")]) - - if not frame_files: - raise ValueError("No frames extracted from video") - - # Load all frames - for frame_file in frame_files: - frame_path = os.path.join(frames_dir, frame_file) - pil_frame = Image.open(frame_path).convert("RGB") + for i, frame_dataurl in enumerate(frames): + # Extract base64 data + base64_data = re.sub("^data:image/.+;base64,", "", frame_dataurl) + + # Convert to PIL image + buffer = BytesIO(base64.b64decode(base64_data)) + pil_frame = Image.open(buffer).convert("RGB") + + # Convert to numpy and normalize frame_np = np.array(pil_frame).astype(np.float32) / 255.0 - frames.append(torch.from_numpy(frame_np)) + + # Add to frame list + tensor_frames.append(torch.from_numpy(frame_np)) - # Extract audio with torchaudio - try: - waveform, sample_rate = torchaudio.load(video_path) - audio = {"waveform": waveform, "sample_rate": sample_rate} - except Exception as e: - print(f"Error extracting audio: {e}") - audio = empty_audio + # Stack frames to create video tensor + # ComfyUI expects image tensors in BHWC format (batch, height, width, channels) + video_tensor = torch.stack(tensor_frames) # First get [frames, H, W, C] - # Stack frames to create video tensor [B, H, W, C] - video_tensor = torch.stack(frames)[None, ...] + print(f"Stacked tensor shape before corrections: {video_tensor.shape}") - # Clean up temporary files - try: - os.unlink(video_path) - for frame_file in frame_files: - os.unlink(os.path.join(frames_dir, frame_file)) - os.rmdir(frames_dir) - except Exception as e: - print(f"Error cleaning up temp files: {e}") + # Then interpret the frames as the batch dimension for ComfyUI + if len(video_tensor.shape) == 4: + # This is already correct format [frames, H, W, C] + pass + elif len(video_tensor.shape) == 5: + # Remove extra batch dimension if present + video_tensor = video_tensor.squeeze(0) + else: + # Something's wrong with the shape + raise ValueError(f"Unexpected tensor shape: {video_tensor.shape}. Expected 4D [frames, H, W, C]") - print(f"Processed video with {len(frames)} frames") - return (video_tensor, audio) + print(f"Final video tensor shape: {video_tensor.shape}") + + return (video_tensor,) except Exception as e: - print(f"Error processing video: {e}") - # Fall back to empty frame and audio if video processing fails - fallback_tensor = torch.zeros((1, 480, 640, 3)) - return (fallback_tensor, empty_audio) + import traceback + print(f"Error processing frame sequence: {e}") + print(traceback.format_exc()) + raise RuntimeError(f"Failed to process frame sequence: {str(e)}") else: - raise ValueError(f"Unrecognized data URL format: {image[:30]}...") + raise ValueError(f"Unrecognized data format from webcam") else: raise ValueError("Invalid data received from webcam") @@ -1415,3 +1399,4 @@ class RenormalizeInt: result = max(output_max, min(output_min, result)) return result + diff --git a/web/js/webcam.js b/web/js/webcam.js index 5bb3607..7b26b61 100644 --- a/web/js/webcam.js +++ b/web/js/webcam.js @@ -9,8 +9,6 @@ app.registerExtension({ // Store webcam stream at extension level to avoid reinitializing let webcamStream = null; let webcamVideo = null; - let mediaRecorder = null; - let recordedChunks = []; // Initialize webcam once for all nodes const initWebcam = async () => { @@ -19,7 +17,7 @@ app.registerExtension({ try { webcamStream = await navigator.mediaDevices.getUserMedia({ video: true, - audio: true // Now requesting audio too for video recording + audio: false // No need for audio anymore }); // console.log("Got media stream:", webcamStream); @@ -43,55 +41,64 @@ app.registerExtension({ return { stream: webcamStream, video: webcamVideo }; }; - // Function to record video for a specified duration - const recordVideo = async (duration) => { + // Function to capture frames for a given duration + const captureFrames = async (duration, fps = 30) => { return new Promise(async (resolve, reject) => { try { // Initialize webcam if not already done - const { stream, video } = await initWebcam(); + const { video } = await initWebcam(); - // Clear previous recording data - recordedChunks = []; + // Create canvas for frame capture + const canvas = document.createElement('canvas'); + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + const ctx = canvas.getContext('2d'); - // Initialize MediaRecorder - mediaRecorder = new MediaRecorder(stream, { - mimeType: 'video/webm;codecs=vp9,opus' - }); + // Calculate total frames to capture + const totalFrames = Math.ceil(duration * fps); + const frameInterval = 1000 / fps; // ms between frames - // Handle data available event - mediaRecorder.ondataavailable = (event) => { - if (event.data.size > 0) { - recordedChunks.push(event.data); - } - }; + console.log(`Capturing ${totalFrames} frames at ${fps} FPS (${frameInterval}ms intervals)`); - // Handle recording stop event - mediaRecorder.onstop = () => { - // Create a blob from recorded chunks - const blob = new Blob(recordedChunks, { type: 'video/webm' }); + // Store captured frames + const frames = []; + + // Start capturing frames at regular intervals + let frameCount = 0; + const startTime = Date.now(); + + const captureFrame = () => { + // Draw current video frame to canvas + ctx.drawImage(video, 0, 0, canvas.width, canvas.height); + const frameDataUrl = canvas.toDataURL('image/jpeg', 0.95); + frames.push(frameDataUrl); - // Convert blob to base64 data URL - const reader = new FileReader(); - reader.onloadend = () => { - const base64data = reader.result; - resolve(base64data); - }; - reader.readAsDataURL(blob); + frameCount++; + if (frameCount < totalFrames) { + // Calculate next frame time based on start time to avoid drift + const nextFrameTime = startTime + Math.round(frameCount * frameInterval); + const delay = Math.max(0, nextFrameTime - Date.now()); + + // Schedule next frame capture + setTimeout(captureFrame, delay); + } else { + // All frames captured + console.log(`Completed capture of ${frames.length} frames`); + + resolve({ + frames: frames, + width: canvas.width, + height: canvas.height, + duration: duration, + fps: fps + }); + } }; - // Start recording - mediaRecorder.start(); - console.log(`Recording started for ${duration} seconds`); - - // Stop recording after specified duration - setTimeout(() => { - if (mediaRecorder && mediaRecorder.state !== 'inactive') { - mediaRecorder.stop(); - console.log('Recording stopped'); - } - }, duration * 1000); + // Start capture process + captureFrame(); } catch (error) { - console.error("Error during video recording:", error); + console.error("Error during capture:", error); reject(error); } }); @@ -151,17 +158,16 @@ app.registerExtension({ ctx.drawImage(webcamVideo, 0, 0, canvas.width, canvas.height); const dataUrl = canvas.toDataURL("image/png"); - console.log("Captured frame at native resolution:", canvas.width, "x", canvas.height); return dataUrl; } - // Video recording mode + // Video capture mode - capture frames else { - console.log(`Starting video recording for ${recordSeconds} seconds`); - // Record video and return data URL - const videoDataUrl = await recordVideo(recordSeconds); - console.log("Video recording completed, sending data to backend"); - return videoDataUrl; + // Capture frames + const captureResult = await captureFrames(recordSeconds); + + // Convert to JSON and return + return JSON.stringify(captureResult); } }; From ecee3022f668d371848b2ef6384625460d5d2084 Mon Sep 17 00:00:00 2001 From: RyanOnTheInside <7623207+ryanontheinside@users.noreply.github.com> Date: Sat, 3 May 2025 17:51:37 -0400 Subject: [PATCH 4/6] frame count --- node_wrappers/utils/misc_nodes.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/node_wrappers/utils/misc_nodes.py b/node_wrappers/utils/misc_nodes.py index 92de1e3..6dc4451 100644 --- a/node_wrappers/utils/misc_nodes.py +++ b/node_wrappers/utils/misc_nodes.py @@ -69,7 +69,8 @@ class FastWebcamCapture: } } - RETURN_TYPES = ("IMAGE",) + RETURN_TYPES = ("IMAGE", "INT") + RETURN_NAMES = ("image", "frame_count") FUNCTION = "process_capture" CATEGORY = "image" @@ -110,7 +111,7 @@ class FastWebcamCapture: # ComfyUI expects BHWC format image_tensor = torch.from_numpy(image_np)[None, ...] - return (image_tensor,) + return (image_tensor, 1) # JSON frames data from sequence capture elif image.startswith("{") and "frames" in image: @@ -164,7 +165,7 @@ class FastWebcamCapture: print(f"Final video tensor shape: {video_tensor.shape}") - return (video_tensor,) + return (video_tensor, len(frames)) except Exception as e: import traceback From c29d764c69d0a547c136465f8a64739c2038eea6 Mon Sep 17 00:00:00 2001 From: RyanOnTheInside <7623207+ryanontheinside@users.noreply.github.com> Date: Sat, 3 May 2025 17:58:40 -0400 Subject: [PATCH 5/6] capture on queue --- node_wrappers/utils/misc_nodes.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/node_wrappers/utils/misc_nodes.py b/node_wrappers/utils/misc_nodes.py index 6dc4451..3d5860d 100644 --- a/node_wrappers/utils/misc_nodes.py +++ b/node_wrappers/utils/misc_nodes.py @@ -66,6 +66,13 @@ class FastWebcamCapture: "tooltip": "Length of video to record in seconds. 0 means capture a single image (no video)." } ), + "capture_on_queue": ( + "BOOLEAN", + { + "default": True, + "tooltip": "When enabled, capture new frames on each queue. When disabled, reuse previously captured video." + } + ), } } @@ -75,7 +82,11 @@ class FastWebcamCapture: CATEGORY = "image" - def process_capture(self, image, record_seconds): + def __init__(self): + self.last_capture = None + self.last_frame_count = 0 + + def process_capture(self, image, record_seconds, capture_on_queue): """ Process a webcam image or video at native resolution. @@ -83,6 +94,11 @@ class FastWebcamCapture: If record_seconds > 0, it will record a video for the specified duration. For resizing or cropping, use standard ComfyUI nodes after this one. """ + # If capture_on_queue is disabled and we have a previous capture, reuse it + if not capture_on_queue and self.last_capture is not None: + print("Reusing previous capture") + return (self.last_capture, self.last_frame_count) + import torch import numpy as np import json @@ -111,6 +127,10 @@ class FastWebcamCapture: # ComfyUI expects BHWC format image_tensor = torch.from_numpy(image_np)[None, ...] + # Store for potential reuse + self.last_capture = image_tensor + self.last_frame_count = 1 + return (image_tensor, 1) # JSON frames data from sequence capture @@ -165,6 +185,10 @@ class FastWebcamCapture: print(f"Final video tensor shape: {video_tensor.shape}") + # Store for potential reuse + self.last_capture = video_tensor + self.last_frame_count = len(frames) + return (video_tensor, len(frames)) except Exception as e: From 2db37adb140c6e8e193dc860b26afbaa00113c22 Mon Sep 17 00:00:00 2001 From: RyanOnTheInside <7623207+ryanontheinside@users.noreply.github.com> Date: Sat, 3 May 2025 18:00:42 -0400 Subject: [PATCH 6/6] reuse capture --- web/js/webcam.js | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/web/js/webcam.js b/web/js/webcam.js index 7b26b61..63bd8b5 100644 --- a/web/js/webcam.js +++ b/web/js/webcam.js @@ -9,6 +9,7 @@ app.registerExtension({ // Store webcam stream at extension level to avoid reinitializing let webcamStream = null; let webcamVideo = null; + let lastCapture = null; // Initialize webcam once for all nodes const initWebcam = async () => { @@ -85,13 +86,18 @@ app.registerExtension({ // All frames captured console.log(`Completed capture of ${frames.length} frames`); - resolve({ + const result = { frames: frames, width: canvas.width, height: canvas.height, duration: duration, fps: fps - }); + }; + + // Store for potential reuse + lastCapture = result; + + resolve(result); } }; @@ -132,6 +138,13 @@ app.registerExtension({ const canvas = document.createElement("canvas"); camera.serializeValue = async () => { + // Check if we should reuse previous capture + const captureOnQueueWidget = this.widgets.find(w => w.name === "capture_on_queue"); + if (!captureOnQueueWidget.value && lastCapture) { + console.log("Reusing previous capture"); + return JSON.stringify(lastCapture); + } + // Only initialize webcam when actually needed if (!webcamVideo || webcamVideo.paused || webcamVideo.readyState !== 4) { // console.log("Initializing webcam for first use..."); @@ -158,11 +171,20 @@ app.registerExtension({ ctx.drawImage(webcamVideo, 0, 0, canvas.width, canvas.height); const dataUrl = canvas.toDataURL("image/png"); + + // Store for potential reuse + lastCapture = { + frames: [dataUrl], + width: canvas.width, + height: canvas.height, + duration: 0, + fps: 30 + }; + return dataUrl; } // Video capture mode - capture frames else { - // Capture frames const captureResult = await captureFrames(recordSeconds);