RTSP Fully Implemented

This commit is contained in:
salirezav
2025-11-01 14:58:25 -04:00
parent 43e1dace8c
commit 1a8aa8a027
5 changed files with 204 additions and 23 deletions

View File

@@ -12,6 +12,7 @@ import logging
import cv2
import numpy as np
import contextlib
import queue
from typing import Optional, Dict, Any
from datetime import datetime
from pathlib import Path
@@ -627,13 +628,41 @@ class CameraRecorder:
use_streamer_frames: If True, read frames from streamer's frame queue instead of capturing directly
"""
try:
# Initialize video writer
if not self._initialize_video_writer():
# For streamer frames, we need to get a frame first to determine dimensions
initial_frame = None
if use_streamer_frames and self.streamer:
self.logger.info("Waiting for first frame from streamer to determine video dimensions...")
# Wait for first frame (with timeout)
timeout_start = time.time()
while initial_frame is None and time.time() - timeout_start < 5.0:
if self._stop_recording_event.is_set():
self.logger.error("Stop event set before getting initial frame")
return
if not self.streamer.streaming:
self.logger.error("Streamer stopped before getting initial frame")
return
try:
initial_frame = self.streamer._recording_frame_queue.get(timeout=0.5)
self.logger.info(f"Got initial frame from streamer: {initial_frame.shape if initial_frame is not None else 'None'}")
except Exception:
continue
if initial_frame is None:
self.logger.error("Failed to get initial frame from streamer for video writer initialization")
return
# Initialize video writer (with initial frame dimensions if using streamer frames)
if not self._initialize_video_writer(use_streamer_frames=use_streamer_frames, initial_frame=initial_frame):
self.logger.error("Failed to initialize video writer")
return
self.logger.info(f"Recording loop started (using {'streamer frames' if use_streamer_frames else 'direct capture'})")
# Write the initial frame if we got one from streamer
if initial_frame is not None and self.video_writer:
self.video_writer.write(initial_frame)
self.frame_count += 1
while not self._stop_recording_event.is_set():
try:
if use_streamer_frames and self.streamer:
@@ -672,6 +701,33 @@ class CameraRecorder:
if frame is not None and self.video_writer:
self.video_writer.write(frame)
self.frame_count += 1
# If streamer is active and using our shared camera, populate its queues
if self.streamer and self.streamer.streaming and self.streamer._using_shared_camera:
try:
# Populate streamer's MJPEG queue
try:
self.streamer._frame_queue.put_nowait(frame.copy())
except queue.Full:
try:
self.streamer._frame_queue.get_nowait()
self.streamer._frame_queue.put_nowait(frame.copy())
except queue.Empty:
pass
# Populate streamer's RTSP queue if RTSP is active
if self.streamer.rtsp_streaming:
try:
self.streamer._rtsp_frame_queue.put_nowait(frame.copy())
except queue.Full:
try:
self.streamer._rtsp_frame_queue.get_nowait()
self.streamer._rtsp_frame_queue.put_nowait(frame.copy())
except queue.Empty:
pass
except Exception as e:
# Non-critical error - logging is optional to avoid spam
pass
# Control frame rate (skip sleep if target_fps is 0 for maximum speed)
if self.camera_config.target_fps > 0:
@@ -698,17 +754,49 @@ class CameraRecorder:
# Note: Don't set self.recording = False here - let stop_recording() handle it
# to avoid race conditions where stop_recording thinks recording already stopped
def _initialize_video_writer(self) -> bool:
"""Initialize OpenCV video writer"""
def _initialize_video_writer(self, use_streamer_frames: bool = False, initial_frame: Optional[np.ndarray] = None) -> bool:
"""Initialize OpenCV video writer
Args:
use_streamer_frames: If True, using frames from streamer (camera handle may be None)
initial_frame: Optional initial frame to get dimensions from (used when use_streamer_frames=True)
"""
try:
# Get frame dimensions by capturing a test frame
pRawData, FrameHead = mvsdk.CameraGetImageBuffer(self.hCamera, 1000)
mvsdk.CameraImageProcess(self.hCamera, pRawData, self.frame_buffer, FrameHead)
mvsdk.CameraReleaseImageBuffer(self.hCamera, pRawData)
# Get frame dimensions
if use_streamer_frames and initial_frame is not None:
# Get dimensions from initial frame
frame_height, frame_width = initial_frame.shape[:2]
frame_size = (frame_width, frame_height)
self.logger.info(f"Using frame dimensions from streamer frame: {frame_size}")
elif self.hCamera:
# Get frame dimensions by capturing a test frame from camera
pRawData, FrameHead = mvsdk.CameraGetImageBuffer(self.hCamera, 1000)
mvsdk.CameraImageProcess(self.hCamera, pRawData, self.frame_buffer, FrameHead)
mvsdk.CameraReleaseImageBuffer(self.hCamera, pRawData)
frame_size = (FrameHead.iWidth, FrameHead.iHeight)
else:
# Fallback: try to get dimensions from streamer's camera if available
if self.streamer and self.streamer.hCamera:
try:
with suppress_camera_errors():
pRawData, FrameHead = mvsdk.CameraGetImageBuffer(self.streamer.hCamera, 1000)
mvsdk.CameraReleaseImageBuffer(self.streamer.hCamera, pRawData)
frame_size = (FrameHead.iWidth, FrameHead.iHeight)
self.logger.info(f"Got frame dimensions from streamer's camera: {frame_size}")
except Exception as e:
self.logger.error(f"Failed to get frame dimensions from streamer camera: {e}")
# Use camera config defaults as last resort
camera_config = self.camera_config
frame_size = (camera_config.resolution_width or 1280, camera_config.resolution_height or 1024)
self.logger.warning(f"Using default frame size from config: {frame_size}")
else:
# Use camera config defaults as last resort
camera_config = self.camera_config
frame_size = (camera_config.resolution_width or 1280, camera_config.resolution_height or 1024)
self.logger.warning(f"Using default frame size from config: {frame_size}")
# Set up video writer with configured codec
fourcc = cv2.VideoWriter_fourcc(*self.camera_config.video_codec)
frame_size = (FrameHead.iWidth, FrameHead.iHeight)
# Use 30 FPS for video writer if target_fps is 0 (unlimited)
video_fps = self.camera_config.target_fps if self.camera_config.target_fps > 0 else 30.0
@@ -779,15 +867,60 @@ class CameraRecorder:
"""Clean up recording resources"""
try:
if self.video_writer:
# CRITICAL: Must release video writer properly to finalize MP4 file (write moov atom)
# OpenCV VideoWriter writes the moov atom only when release() is called
try:
# Ensure all frames are flushed
if hasattr(self.video_writer, 'flush'):
self.video_writer.flush()
except:
pass
# Release writer - this writes the moov atom and finalizes the file
self.video_writer.release()
self.video_writer = None
self.logger.debug("Video writer released")
self.logger.info(f"Video writer released and file closed (recorded {self.frame_count} frames)")
# Small delay to ensure file system sync
import time
time.sleep(0.1)
# Verify file exists and has content
if self.output_filename and os.path.exists(self.output_filename):
file_size = os.path.getsize(self.output_filename)
self.logger.info(f"Video file size: {file_size / (1024*1024):.2f} MB ({file_size} bytes)")
if file_size == 0:
self.logger.error("ERROR: Video file is empty (0 bytes)!")
elif file_size < 1024: # Less than 1KB is suspicious
self.logger.warning(f"WARNING: Video file is very small ({file_size} bytes) - may be corrupted")
else:
# Verify file has moov atom by checking if it's readable
try:
import subprocess
result = subprocess.run(
['ffprobe', '-v', 'error', '-show_format', self.output_filename],
capture_output=True,
timeout=5,
stderr=subprocess.PIPE
)
if result.returncode != 0:
stderr = result.stderr.decode('utf-8', errors='ignore')
if 'moov atom not found' in stderr:
self.logger.error("ERROR: Video file is missing moov atom (metadata) - file is corrupted/incomplete!")
else:
self.logger.warning(f"WARNING: ffprobe check failed: {stderr[:200]}")
else:
self.logger.info("Video file validated: moov atom present, file is readable")
except Exception as e:
self.logger.debug(f"Could not validate video file with ffprobe: {e}")
# Note: Don't set self.recording = False here - let stop_recording() control the flag
# to maintain proper state synchronization
except Exception as e:
self.logger.error(f"Error during recording cleanup: {e}")
import traceback
self.logger.error(f"Traceback: {traceback.format_exc()}")
def test_connection(self) -> bool:
"""Test camera connection"""