refactor: replace threading with multiprocessing
All checks were successful
Build Worker Base and Application Images / check-base-changes (push) Successful in 10s
Build Worker Base and Application Images / build-base (push) Has been skipped
Build Worker Base and Application Images / build-docker (push) Successful in 2m52s
Build Worker Base and Application Images / deploy-stack (push) Successful in 8s

This commit is contained in:
ziesorx 2025-09-25 12:53:17 +07:00
parent e87ed4c056
commit bfab574058
6 changed files with 682 additions and 58 deletions

View file

@ -1,14 +1,38 @@
"""
Stream coordination and lifecycle management.
Optimized for 1280x720@6fps RTSP and 2560x1440 HTTP snapshots.
Supports both threading and multiprocessing modes for scalability.
"""
import logging
import threading
import time
import os
from typing import Dict, Set, Optional, List, Any
from dataclasses import dataclass
from collections import defaultdict
# Check if multiprocessing is enabled (default enabled with proper initialization)
USE_MULTIPROCESSING = os.environ.get('USE_MULTIPROCESSING', 'true').lower() == 'true'
logger = logging.getLogger(__name__)
if USE_MULTIPROCESSING:
try:
from .process_manager import RTSPProcessManager, ProcessConfig
logger.info("Multiprocessing support enabled")
_mp_loaded = True
except ImportError as e:
logger.warning(f"Failed to load multiprocessing support: {e}")
USE_MULTIPROCESSING = False
_mp_loaded = False
except Exception as e:
logger.warning(f"Multiprocessing initialization failed: {e}")
USE_MULTIPROCESSING = False
_mp_loaded = False
else:
logger.info("Multiprocessing support disabled (using threading mode)")
_mp_loaded = False
from .readers import RTSPReader, HTTPSnapshotReader
from .buffers import shared_cache_buffer, StreamType
from ..tracking.integration import TrackingPipelineIntegration
@ -50,6 +74,42 @@ class StreamManager:
self._camera_subscribers: Dict[str, Set[str]] = defaultdict(set) # camera_id -> set of subscription_ids
self._lock = threading.RLock()
# Initialize multiprocessing manager if enabled (lazy initialization)
self.process_manager = None
self._frame_getter_thread = None
self._multiprocessing_enabled = USE_MULTIPROCESSING and _mp_loaded
if self._multiprocessing_enabled:
logger.info(f"Multiprocessing support enabled, will initialize on first use")
else:
logger.info(f"Multiprocessing support disabled, using threading mode")
def _initialize_multiprocessing(self) -> bool:
"""Lazily initialize multiprocessing manager when first needed."""
if self.process_manager is not None:
return True
if not self._multiprocessing_enabled:
return False
try:
self.process_manager = RTSPProcessManager(max_processes=min(self.max_streams, 15))
# Start monitoring synchronously to ensure it's ready
self.process_manager.start_monitoring()
# Start frame getter thread
self._frame_getter_thread = threading.Thread(
target=self._multiprocess_frame_getter,
daemon=True
)
self._frame_getter_thread.start()
logger.info(f"Initialized multiprocessing manager with max {self.process_manager.max_processes} processes")
return True
except Exception as e:
logger.error(f"Failed to initialize multiprocessing manager: {e}")
self.process_manager = None
self._multiprocessing_enabled = False # Disable for future attempts
return False
def add_subscription(self, subscription_id: str, stream_config: StreamConfig,
crop_coords: Optional[tuple] = None,
model_id: Optional[str] = None,
@ -129,7 +189,24 @@ class StreamManager:
"""Start a stream for the given camera."""
try:
if stream_config.rtsp_url:
# RTSP stream
# Try multiprocessing for RTSP if enabled
if self._multiprocessing_enabled and self._initialize_multiprocessing():
config = ProcessConfig(
camera_id=camera_id,
rtsp_url=stream_config.rtsp_url,
expected_fps=6,
buffer_size=3,
max_retries=stream_config.max_retries
)
success = self.process_manager.add_camera(config)
if success:
self._streams[camera_id] = 'multiprocessing' # Mark as multiprocessing stream
logger.info(f"Started RTSP multiprocessing stream for camera {camera_id}")
return True
else:
logger.warning(f"Failed to start multiprocessing stream for {camera_id}, falling back to threading")
# Fall back to threading mode for RTSP
reader = RTSPReader(
camera_id=camera_id,
rtsp_url=stream_config.rtsp_url,
@ -138,10 +215,10 @@ class StreamManager:
reader.set_frame_callback(self._frame_callback)
reader.start()
self._streams[camera_id] = reader
logger.info(f"Started RTSP stream for camera {camera_id}")
logger.info(f"Started RTSP threading stream for camera {camera_id}")
elif stream_config.snapshot_url:
# HTTP snapshot stream
# HTTP snapshot stream (always use threading)
reader = HTTPSnapshotReader(
camera_id=camera_id,
snapshot_url=stream_config.snapshot_url,
@ -167,10 +244,18 @@ class StreamManager:
"""Stop a stream for the given camera."""
if camera_id in self._streams:
try:
self._streams[camera_id].stop()
stream_obj = self._streams[camera_id]
if stream_obj == 'multiprocessing' and self.process_manager:
# Remove from multiprocessing manager
self.process_manager.remove_camera(camera_id)
logger.info(f"Stopped multiprocessing stream for camera {camera_id}")
else:
# Stop threading stream
stream_obj.stop()
logger.info(f"Stopped threading stream for camera {camera_id}")
del self._streams[camera_id]
shared_cache_buffer.clear_camera(camera_id)
logger.info(f"Stopped stream for camera {camera_id}")
except Exception as e:
logger.error(f"Error stopping stream for camera {camera_id}: {e}")
@ -190,6 +275,38 @@ class StreamManager:
except Exception as e:
logger.error(f"Error in frame callback for camera {camera_id}: {e}")
def _multiprocess_frame_getter(self):
"""Background thread to get frames from multiprocessing manager."""
if not self.process_manager:
return
logger.info("Started multiprocessing frame getter thread")
while self.process_manager:
try:
# Get frames from all multiprocessing cameras
with self._lock:
mp_cameras = [cid for cid, s in self._streams.items() if s == 'multiprocessing']
for camera_id in mp_cameras:
try:
result = self.process_manager.get_frame(camera_id)
if result:
frame, timestamp = result
# Detect stream type and store in cache
stream_type = self._detect_stream_type(frame)
shared_cache_buffer.put_frame(camera_id, frame, stream_type)
# Process tracking
self._process_tracking_for_camera(camera_id, frame)
except Exception as e:
logger.debug(f"Error getting frame for {camera_id}: {e}")
time.sleep(0.05) # 20 FPS polling rate
except Exception as e:
logger.error(f"Error in multiprocess frame getter: {e}")
time.sleep(1.0)
def _process_tracking_for_camera(self, camera_id: str, frame):
"""Process tracking for all subscriptions of a camera."""
try:
@ -362,6 +479,12 @@ class StreamManager:
for camera_id in list(self._streams.keys()):
self._stop_stream(camera_id)
# Stop multiprocessing manager if exists
if self.process_manager:
self.process_manager.stop_all()
self.process_manager = None
logger.info("Stopped multiprocessing manager")
# Clear all tracking
self._subscriptions.clear()
self._camera_subscribers.clear()
@ -434,9 +557,12 @@ class StreamManager:
# Add stream type information
stream_types = {}
for camera_id in self._streams.keys():
if isinstance(self._streams[camera_id], RTSPReader):
stream_types[camera_id] = 'rtsp'
elif isinstance(self._streams[camera_id], HTTPSnapshotReader):
stream_obj = self._streams[camera_id]
if stream_obj == 'multiprocessing':
stream_types[camera_id] = 'rtsp_multiprocessing'
elif isinstance(stream_obj, RTSPReader):
stream_types[camera_id] = 'rtsp_threading'
elif isinstance(stream_obj, HTTPSnapshotReader):
stream_types[camera_id] = 'http'
else:
stream_types[camera_id] = 'unknown'