refactor: half way to process per session
All checks were successful
Build Worker Base and Application Images / check-base-changes (push) Successful in 7s
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 9s

This commit is contained in:
ziesorx 2025-09-25 20:52:26 +07:00
parent 2e5316ca01
commit 34d1982e9e
12 changed files with 2771 additions and 92 deletions

View file

@ -24,6 +24,7 @@ from .state import worker_state, SystemMetrics
from ..models import ModelManager
from ..streaming.manager import shared_stream_manager
from ..tracking.integration import TrackingPipelineIntegration
from .session_integration import SessionWebSocketIntegration
logger = logging.getLogger(__name__)
@ -48,6 +49,9 @@ class WebSocketHandler:
self._heartbeat_count = 0
self._last_processed_models: set = set() # Cache of last processed model IDs
# Initialize session integration
self.session_integration = SessionWebSocketIntegration(self)
async def handle_connection(self) -> None:
"""
Main connection handler that manages the WebSocket lifecycle.
@ -66,14 +70,16 @@ class WebSocketHandler:
# Send immediate heartbeat to show connection is alive
await self._send_immediate_heartbeat()
# Start background tasks (matching original architecture)
stream_task = asyncio.create_task(self._process_streams())
# Start session integration
await self.session_integration.start()
# Start background tasks - stream processing now handled by session workers
heartbeat_task = asyncio.create_task(self._send_heartbeat())
message_task = asyncio.create_task(self._handle_messages())
logger.info(f"WebSocket background tasks started for {client_info} (stream + heartbeat + message handler)")
logger.info(f"WebSocket background tasks started for {client_info} (heartbeat + message handler)")
# Wait for heartbeat and message tasks (stream runs independently)
# Wait for heartbeat and message tasks
await asyncio.gather(heartbeat_task, message_task)
except Exception as e:
@ -87,6 +93,11 @@ class WebSocketHandler:
await stream_task
except asyncio.CancelledError:
logger.debug(f"Stream task cancelled for {client_info}")
# Stop session integration
if hasattr(self, 'session_integration'):
await self.session_integration.stop()
await self._cleanup()
async def _send_immediate_heartbeat(self) -> None:
@ -180,11 +191,11 @@ class WebSocketHandler:
try:
if message_type == MessageTypes.SET_SUBSCRIPTION_LIST:
await self._handle_set_subscription_list(message)
await self.session_integration.handle_set_subscription_list(message)
elif message_type == MessageTypes.SET_SESSION_ID:
await self._handle_set_session_id(message)
await self.session_integration.handle_set_session_id(message)
elif message_type == MessageTypes.SET_PROGRESSION_STAGE:
await self._handle_set_progression_stage(message)
await self.session_integration.handle_progression_stage(message)
elif message_type == MessageTypes.REQUEST_STATE:
await self._handle_request_state(message)
elif message_type == MessageTypes.PATCH_SESSION_RESULT:
@ -619,31 +630,108 @@ class WebSocketHandler:
logger.error(f"Failed to send WebSocket message: {e}")
raise
async def send_message(self, message) -> None:
"""Public method to send messages (used by session integration)."""
await self._send_message(message)
# DEPRECATED: Stream processing is now handled directly by session worker processes
async def _process_streams(self) -> None:
"""
Stream processing task that handles frame processing and detection.
This is a placeholder for Phase 2 - currently just logs that it's running.
DEPRECATED: Stream processing task that handles frame processing and detection.
Stream processing is now integrated directly into session worker processes.
"""
logger.info("DEPRECATED: Stream processing task - now handled by session workers")
return # Exit immediately - no longer needed
# OLD CODE (disabled):
logger.info("Stream processing task started")
try:
while self.connected:
# Get current subscriptions
subscriptions = worker_state.get_all_subscriptions()
# TODO: Phase 2 - Add actual frame processing logic here
# This will include:
# - Frame reading from RTSP/HTTP streams
# - Model inference using loaded pipelines
# - Detection result sending via WebSocket
if not subscriptions:
await asyncio.sleep(0.5)
continue
# Process frames for each subscription
for subscription in subscriptions:
await self._process_subscription_frames(subscription)
# Sleep to prevent excessive CPU usage (similar to old poll_interval)
await asyncio.sleep(0.1) # 100ms polling interval
await asyncio.sleep(0.25) # 250ms polling interval
except asyncio.CancelledError:
logger.info("Stream processing task cancelled")
except Exception as e:
logger.error(f"Error in stream processing: {e}", exc_info=True)
async def _process_subscription_frames(self, subscription) -> None:
"""
Process frames for a single subscription by getting frames from stream manager
and forwarding them to the appropriate session worker.
"""
try:
subscription_id = subscription.subscriptionIdentifier
# Get the latest frame from the stream manager
frame_data = await self._get_frame_from_stream_manager(subscription)
if frame_data and frame_data['frame'] is not None:
# Extract display identifier (format: "test1;Dispenser Camera 1")
display_id = subscription_id.split(';')[-1] if ';' in subscription_id else subscription_id
# Forward frame to session worker via session integration
success = await self.session_integration.process_frame(
subscription_id=subscription_id,
frame=frame_data['frame'],
display_id=display_id,
timestamp=frame_data.get('timestamp', asyncio.get_event_loop().time())
)
if success:
logger.debug(f"[Frame Processing] Sent frame to session worker for {subscription_id}")
else:
logger.warning(f"[Frame Processing] Failed to send frame to session worker for {subscription_id}")
except Exception as e:
logger.error(f"Error processing frames for {subscription.subscriptionIdentifier}: {e}")
async def _get_frame_from_stream_manager(self, subscription) -> dict:
"""
Get the latest frame from the stream manager for a subscription using existing API.
"""
try:
subscription_id = subscription.subscriptionIdentifier
# Use existing stream manager API to check if frame is available
if not shared_stream_manager.has_frame(subscription_id):
# Stream should already be started by session integration
return {'frame': None, 'timestamp': None}
# Get frame using existing API with crop coordinates if available
crop_coords = None
if hasattr(subscription, 'cropX1') and subscription.cropX1 is not None:
crop_coords = (
subscription.cropX1, subscription.cropY1,
subscription.cropX2, subscription.cropY2
)
# Use existing get_frame method
frame = shared_stream_manager.get_frame(subscription_id, crop_coords)
if frame is not None:
return {
'frame': frame,
'timestamp': asyncio.get_event_loop().time()
}
return {'frame': None, 'timestamp': None}
except Exception as e:
logger.error(f"Error getting frame from stream manager for {subscription.subscriptionIdentifier}: {e}")
return {'frame': None, 'timestamp': None}
async def _cleanup(self) -> None:
"""Clean up resources when connection closes."""
logger.info("Cleaning up WebSocket connection")