diff --git a/api-tests.http b/api-tests.http index d3fa7f4..1cd62ba 100644 --- a/api-tests.http +++ b/api-tests.http @@ -37,6 +37,15 @@ POST {{API}}/cameras/camera1/stop-stream ### View live MJPEG stream in browser (open URL) # {{API}}/cameras/camera1/stream +### Start RTSP streaming for camera1 (publishes to MediaMTX) +POST {{API}}/cameras/camera1/start-rtsp + +### Stop RTSP streaming for camera1 +POST {{API}}/cameras/camera1/stop-rtsp + +### RTSP stream URL (use with VLC/ffplay): +# rtsp://{{host}}:{{rtsp_port}}/camera1 + ### getting a list of all videos GET {{MEDIA}}/videos/?page=10&limit=1 @@ -78,6 +87,20 @@ Range: bytes=0-1048575 # ffplay -rtsp_transport tcp rtsp://{{host}}:{{rtsp_port}}/vod # vlc rtsp://{{host}}:{{rtsp_port}}/vod +# MediaMTX HTTP API (for checking streams) +@MEDIAMTX_API = http://{{host}}:8889 +### Check MediaMTX version/info +GET {{MEDIAMTX_API}}/v2/config/get + +### List all available RTSP paths/streams +GET {{MEDIAMTX_API}}/v2/paths/list + +### Get info about a specific path (e.g., camera1) +GET {{MEDIAMTX_API}}/v2/paths/get/camera1 + +# MediaMTX Web Interface (open in browser) +# http://{{host}}:8889/static/ + # If you enable WebRTC in MediaMTX, you can open its embedded player page (for testing): # See MediaMTX docs; by default we exposed RTSP and WebRTC ports in compose. diff --git a/camera-management-api/Dockerfile b/camera-management-api/Dockerfile index 5b36c69..ba73974 100644 --- a/camera-management-api/Dockerfile +++ b/camera-management-api/Dockerfile @@ -6,6 +6,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ bash ca-certificates tar sudo \ libusb-1.0-0 libgl1 libx11-6 libxext6 libxcb1 libgtk-3-0 \ libusb-1.0-0-dev udev \ + ffmpeg \ && rm -rf /var/lib/apt/lists/* # Set working directory diff --git a/camera-management-api/usda_vision_system/api/server.py b/camera-management-api/usda_vision_system/api/server.py index 7adcc26..d744cdf 100644 --- a/camera-management-api/usda_vision_system/api/server.py +++ b/camera-management-api/usda_vision_system/api/server.py @@ -7,6 +7,7 @@ This module provides REST API endpoints and WebSocket support for dashboard inte import asyncio import logging import json +import os from typing import Dict, List, Optional, Any from datetime import datetime, timedelta import threading @@ -347,6 +348,45 @@ class APIServer: self.logger.error(f"Error stopping camera stream: {e}") raise HTTPException(status_code=500, detail=str(e)) + @self.app.post("/cameras/{camera_name}/start-rtsp") + async def start_camera_rtsp_stream(camera_name: str): + """Start RTSP streaming for a camera to MediaMTX""" + try: + if not self.camera_manager: + raise HTTPException(status_code=503, detail="Camera manager not available") + + success = self.camera_manager.start_camera_rtsp_streaming(camera_name) + if success: + rtsp_url = f"rtsp://{os.getenv('MEDIAMTX_HOST', 'localhost')}:{os.getenv('MEDIAMTX_RTSP_PORT', '8554')}/{camera_name}" + return { + "success": True, + "message": f"Started RTSP streaming for camera {camera_name}", + "rtsp_url": rtsp_url + } + else: + return {"success": False, "message": f"Failed to start RTSP streaming for camera {camera_name}"} + + except Exception as e: + self.logger.error(f"Error starting RTSP stream: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @self.app.post("/cameras/{camera_name}/stop-rtsp") + async def stop_camera_rtsp_stream(camera_name: str): + """Stop RTSP streaming for a camera""" + try: + if not self.camera_manager: + raise HTTPException(status_code=503, detail="Camera manager not available") + + success = self.camera_manager.stop_camera_rtsp_streaming(camera_name) + if success: + return {"success": True, "message": f"Stopped RTSP streaming for camera {camera_name}"} + else: + return {"success": False, "message": f"Failed to stop RTSP streaming for camera {camera_name}"} + + except Exception as e: + self.logger.error(f"Error stopping RTSP stream: {e}") + raise HTTPException(status_code=500, detail=str(e)) + @self.app.get("/cameras/{camera_name}/config", response_model=CameraConfigResponse) async def get_camera_config(camera_name: str): """Get camera configuration""" diff --git a/camera-management-api/usda_vision_system/camera/manager.py b/camera-management-api/usda_vision_system/camera/manager.py index 8886493..a39838a 100644 --- a/camera-management-api/usda_vision_system/camera/manager.py +++ b/camera-management-api/usda_vision_system/camera/manager.py @@ -520,6 +520,32 @@ class CameraManager: return streamer.is_streaming() + def start_camera_rtsp_streaming(self, camera_name: str) -> bool: + """Start RTSP streaming for a specific camera""" + streamer = self.camera_streamers.get(camera_name) + if not streamer: + self.logger.error(f"Camera streamer not found: {camera_name}") + return False + + return streamer.start_rtsp_streaming() + + def stop_camera_rtsp_streaming(self, camera_name: str) -> bool: + """Stop RTSP streaming for a specific camera""" + streamer = self.camera_streamers.get(camera_name) + if not streamer: + self.logger.error(f"Camera streamer not found: {camera_name}") + return False + + return streamer.stop_rtsp_streaming() + + def is_camera_rtsp_streaming(self, camera_name: str) -> bool: + """Check if a camera is currently RTSP streaming""" + streamer = self.camera_streamers.get(camera_name) + if not streamer: + return False + + return streamer.is_rtsp_streaming() + def get_camera_config(self, camera_name: str) -> Optional[CameraConfig]: """Get camera configuration""" return self.config.get_camera_by_name(camera_name) diff --git a/camera-management-api/usda_vision_system/camera/streamer.py b/camera-management-api/usda_vision_system/camera/streamer.py index 66782ff..51f25d6 100644 --- a/camera-management-api/usda_vision_system/camera/streamer.py +++ b/camera-management-api/usda_vision_system/camera/streamer.py @@ -13,6 +13,7 @@ import logging import cv2 import numpy as np import contextlib +import subprocess from typing import Optional, Dict, Any, Generator from datetime import datetime import queue @@ -70,14 +71,26 @@ class CameraStreamer: # Streaming state self.streaming = False + self.rtsp_streaming = False # RTSP streaming state self._streaming_thread: Optional[threading.Thread] = None + self._rtsp_thread: Optional[threading.Thread] = None self._stop_streaming_event = threading.Event() + self._stop_rtsp_event = threading.Event() self._frame_queue = queue.Queue(maxsize=5) # Buffer for latest frames + self._rtsp_frame_queue = queue.Queue(maxsize=10) # Buffer for RTSP frames (larger buffer for smoother streaming) self._lock = threading.RLock() # Stream settings (optimized for preview) self.preview_fps = 10.0 # Lower FPS for preview to reduce load self.preview_quality = 70 # JPEG quality for streaming + + # RTSP settings + self.rtsp_fps = 15.0 # RTSP FPS (can be higher than MJPEG preview) + self.rtsp_host = os.getenv("MEDIAMTX_HOST", "localhost") + self.rtsp_port = int(os.getenv("MEDIAMTX_RTSP_PORT", "8554")) + self._rtsp_process: Optional[subprocess.Popen] = None + self._rtsp_frame_width = 0 + self._rtsp_frame_height = 0 def start_streaming(self) -> bool: """Start streaming preview frames""" @@ -130,6 +143,88 @@ class CameraStreamer: self.logger.error(f"Error stopping streaming: {e}") return False + def start_rtsp_streaming(self) -> bool: + """Start RTSP streaming to MediaMTX""" + with self._lock: + if self.rtsp_streaming: + self.logger.warning("RTSP streaming already active") + return True + + # Ensure camera is initialized for streaming + if not self.streaming: + if not self.start_streaming(): + return False + + try: + # Get frame dimensions from the first frame if available + if self._frame_queue.empty(): + self.logger.warning("No frames available yet, will initialize RTSP with default dimensions") + # Will update dimensions when first frame arrives + else: + # Peek at a frame to get dimensions (don't remove it) + try: + test_frame = self._frame_queue.queue[0] + self._rtsp_frame_height, self._rtsp_frame_width = test_frame.shape[:2] + except (IndexError, AttributeError): + pass + + # Start RTSP thread + self._stop_rtsp_event.clear() + self._rtsp_thread = threading.Thread(target=self._rtsp_streaming_loop, daemon=True) + self._rtsp_thread.start() + + self.rtsp_streaming = True + self.logger.info(f"Started RTSP streaming for camera: {self.camera_config.name} -> rtsp://{self.rtsp_host}:{self.rtsp_port}/{self.camera_config.name}") + return True + + except Exception as e: + self.logger.error(f"Error starting RTSP streaming: {e}") + return False + + def stop_rtsp_streaming(self) -> bool: + """Stop RTSP streaming""" + with self._lock: + if not self.rtsp_streaming: + return True + + try: + # Signal RTSP thread to stop + self._stop_rtsp_event.set() + + # Wait for thread to finish + if self._rtsp_thread and self._rtsp_thread.is_alive(): + self._rtsp_thread.join(timeout=5.0) + + # Kill FFmpeg process if still running + if self._rtsp_process: + try: + self._rtsp_process.terminate() + self._rtsp_process.wait(timeout=2.0) + except subprocess.TimeoutExpired: + self._rtsp_process.kill() + except Exception as e: + self.logger.warning(f"Error stopping RTSP process: {e}") + self._rtsp_process = None + + # Clear RTSP frame queue + while not self._rtsp_frame_queue.empty(): + try: + self._rtsp_frame_queue.get_nowait() + except queue.Empty: + break + + self.rtsp_streaming = False + self.logger.info(f"Stopped RTSP streaming for camera: {self.camera_config.name}") + return True + + except Exception as e: + self.logger.error(f"Error stopping RTSP streaming: {e}") + return False + + def is_rtsp_streaming(self) -> bool: + """Check if RTSP streaming is active""" + return self.rtsp_streaming + def get_latest_frame(self) -> Optional[bytes]: """Get the latest frame as JPEG bytes for streaming""" try: @@ -258,7 +353,12 @@ class CameraStreamer: frame = self._convert_frame_to_opencv(FrameHead) if frame is not None: - # Add frame to queue (replace oldest if queue is full) + # Update RTSP frame dimensions if not set + if self.rtsp_streaming and self._rtsp_frame_width == 0: + self._rtsp_frame_height, self._rtsp_frame_width = frame.shape[:2] + self.logger.info(f"RTSP frame dimensions set: {self._rtsp_frame_width}x{self._rtsp_frame_height}") + + # Add frame to MJPEG queue (replace oldest if queue is full) try: self._frame_queue.put_nowait(frame) except queue.Full: @@ -269,6 +369,18 @@ class CameraStreamer: except queue.Empty: pass + # Add frame to RTSP queue if RTSP is active + if self.rtsp_streaming: + try: + self._rtsp_frame_queue.put_nowait(frame) + except queue.Full: + # Remove oldest frame and add new one + try: + self._rtsp_frame_queue.get_nowait() + self._rtsp_frame_queue.put_nowait(frame) + except queue.Empty: + pass + # Release buffer mvsdk.CameraReleaseImageBuffer(self.hCamera, pRawData) @@ -406,7 +518,167 @@ class CameraStreamer: except Exception as e: self.logger.warning(f"Error configuring advanced settings: {e}") + def _rtsp_streaming_loop(self): + """Main RTSP streaming loop that feeds frames to FFmpeg""" + self.logger.info("Starting RTSP streaming loop") + + # Wait for frame dimensions to be set + timeout = 10.0 # Wait up to 10 seconds for first frame + start_time = time.time() + while self._rtsp_frame_width == 0 and (time.time() - start_time) < timeout: + if self._stop_rtsp_event.is_set(): + return + time.sleep(0.1) + + if self._rtsp_frame_width == 0: + self.logger.error("Could not determine frame dimensions for RTSP streaming") + self.rtsp_streaming = False + return + + rtsp_url = f"rtsp://{self.rtsp_host}:{self.rtsp_port}/{self.camera_config.name}" + self.logger.info(f"Publishing RTSP stream to {rtsp_url} with dimensions {self._rtsp_frame_width}x{self._rtsp_frame_height} @ {self.rtsp_fps}fps") + + try: + # FFmpeg command to encode frames from stdin and publish to RTSP + # Input: raw video from stdin (BGR24 format) + # Output: H.264 encoded stream via RTSP + # Using TCP transport for better reliability + cmd = [ + "ffmpeg", + "-f", "rawvideo", + "-pixel_format", "bgr24", + "-video_size", f"{self._rtsp_frame_width}x{self._rtsp_frame_height}", + "-framerate", str(self.rtsp_fps), + "-i", "-", # Read from stdin + "-c:v", "libx264", + "-preset", "ultrafast", # Fast encoding for low latency + "-tune", "zerolatency", # Zero latency tuning + "-g", "30", # GOP size (keyframe interval) + "-crf", "23", # Quality (lower = better, but larger file) + "-f", "rtsp", + "-rtsp_transport", "tcp", # Use TCP instead of UDP for reliability + "-muxdelay", "0.1", # Reduce mux delay + rtsp_url + ] + + # Start FFmpeg process + self._rtsp_process = subprocess.Popen( + cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + bufsize=0 # Unbuffered for low latency + ) + + self.logger.info(f"FFmpeg RTSP process started (PID: {self._rtsp_process.pid})") + + # Start thread to monitor FFmpeg stderr for errors + def monitor_ffmpeg_stderr(): + if self._rtsp_process.stderr: + try: + for line in iter(self._rtsp_process.stderr.readline, b''): + if line: + line_str = line.decode('utf-8', errors='ignore').strip() + if line_str: + self.logger.debug(f"FFmpeg stderr: {line_str}") + # Log errors at warning level + if any(keyword in line_str.lower() for keyword in ['error', 'failed', 'fatal']): + self.logger.warning(f"FFmpeg error: {line_str}") + except Exception as e: + self.logger.debug(f"FFmpeg stderr monitor ended: {e}") + + stderr_thread = threading.Thread(target=monitor_ffmpeg_stderr, daemon=True) + stderr_thread.start() + + frame_interval = 1.0 / self.rtsp_fps + last_frame_time = time.time() + + frames_sent = 0 + frames_skipped = 0 + + while not self._stop_rtsp_event.is_set(): + try: + # Check if FFmpeg process is still alive + if self._rtsp_process.poll() is not None: + self.logger.error(f"FFmpeg process exited with code {self._rtsp_process.returncode}") + # Read stderr to see what went wrong + if self._rtsp_process.stderr: + try: + stderr_output = self._rtsp_process.stderr.read().decode('utf-8', errors='ignore') + if stderr_output: + self.logger.error(f"FFmpeg stderr output: {stderr_output[-500:]}") # Last 500 chars + except: + pass + break + + # Get frame from queue with timeout + try: + frame = self._rtsp_frame_queue.get(timeout=0.1) + except queue.Empty: + frames_skipped += 1 + # Log warning if we've skipped many frames (might indicate queue isn't being filled) + if frames_skipped % 100 == 0 and frames_skipped > 0: + self.logger.warning(f"RTSP frame queue empty ({frames_skipped} skipped, {frames_sent} sent)") + continue + + # Ensure frame dimensions match (resize if necessary) + if frame.shape[1] != self._rtsp_frame_width or frame.shape[0] != self._rtsp_frame_height: + frame = cv2.resize(frame, (self._rtsp_frame_width, self._rtsp_frame_height)) + + # Write frame to FFmpeg stdin + if self._rtsp_process.stdin is None: + self.logger.error("FFmpeg stdin is None") + break + + try: + self._rtsp_process.stdin.write(frame.tobytes()) + self._rtsp_process.stdin.flush() + frames_sent += 1 + if frames_sent % 100 == 0: + self.logger.debug(f"Sent {frames_sent} frames to FFmpeg") + except BrokenPipeError: + self.logger.error(f"FFmpeg process pipe broken (sent {frames_sent} frames before error)") + break + except Exception as e: + self.logger.error(f"Error writing frame to FFmpeg: {e} (sent {frames_sent} frames)") + break + + # Control frame rate + current_time = time.time() + elapsed = current_time - last_frame_time + if elapsed < frame_interval: + time.sleep(frame_interval - elapsed) + last_frame_time = time.time() + + except Exception as e: + if not self._stop_rtsp_event.is_set(): + self.logger.error(f"Error in RTSP streaming loop: {e}") + time.sleep(0.1) + + except Exception as e: + self.logger.error(f"Fatal error in RTSP streaming loop: {e}") + finally: + # Cleanup FFmpeg process + if self._rtsp_process: + try: + if self._rtsp_process.stdin: + self._rtsp_process.stdin.close() + except Exception: + pass + try: + self._rtsp_process.terminate() + self._rtsp_process.wait(timeout=2.0) + except subprocess.TimeoutExpired: + self._rtsp_process.kill() + except Exception as e: + self.logger.warning(f"Error stopping FFmpeg process: {e}") + self._rtsp_process = None + + self.logger.info("RTSP streaming loop ended") + def __del__(self): """Destructor to ensure cleanup""" + if self.rtsp_streaming: + self.stop_rtsp_streaming() if self.streaming: self.stop_streaming() diff --git a/check_rtsp_status.sh b/check_rtsp_status.sh new file mode 100755 index 0000000..dd992e4 --- /dev/null +++ b/check_rtsp_status.sh @@ -0,0 +1,72 @@ +#!/bin/bash +# Quick script to check RTSP streaming status + +echo "=== RTSP Streaming Status Check ===" +echo "" + +echo "1. Starting RTSP stream (if not already running)..." +RESPONSE=$(curl -s -X POST http://exp-dash:8000/cameras/camera1/start-rtsp) +echo "$RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$RESPONSE" +echo "" + +echo "2. Waiting for stream to initialize (5 seconds)..." +sleep 5 + +echo "3. Checking MediaMTX for stream..." +PATH_INFO=$(curl -s http://localhost:8889/v2/paths/get/camera1 2>/dev/null) +if [ -n "$PATH_INFO" ] && [ "$PATH_INFO" != "null" ] && [ "$PATH_INFO" != "{}" ]; then + echo "$PATH_INFO" | python3 -m json.tool 2>/dev/null || echo "$PATH_INFO" + SOURCE_READY=$(echo "$PATH_INFO" | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('sourceReady', False))" 2>/dev/null || echo "false") + if [ "$SOURCE_READY" = "True" ]; then + echo "" + echo "✅ Stream is READY!" + else + echo "" + echo "⚠️ Stream path exists but source not ready yet" + fi +else + echo "❌ Stream not found in MediaMTX" +fi +echo "" + +echo "4. Checking for FFmpeg process..." +FFMPEG_PID=$(docker compose exec api bash -c "ps aux | grep '[f]fmpeg' | awk '{print \$2}'" 2>/dev/null | head -1) +if [ -n "$FFMPEG_PID" ]; then + echo "✅ FFmpeg is running (PID: $FFMPEG_PID)" +else + echo "❌ No FFmpeg process found" +fi +echo "" + +echo "5. Latest RTSP/FFmpeg logs..." +docker compose logs api --tail 50 | grep -E "RTSP|FFmpeg|error|Broken pipe" | tail -10 +echo "" + +echo "6. MediaMTX recent logs..." +docker compose logs mediamtx --tail 10 | grep -E "camera1|RTSP|publishing" +echo "" + +# Get Tailscale IP +TAILSCALE_IP=$(tailscale ip -4 2>/dev/null || echo "") +if [ -z "$TAILSCALE_IP" ]; then + TAILSCALE_IP=$(hostname -I | awk '{print $1}') +fi + +echo "=== Access URLs ===" +echo "" +echo "Your Tailscale IP: $TAILSCALE_IP" +echo "" +echo "📺 MediaMTX Web Interface (BEST - shows all streams with player):" +echo " http://$TAILSCALE_IP:8889/static/" +echo "" +echo "📹 Direct WebRTC player for camera1:" +echo " http://$TAILSCALE_IP:8889/camera1/webrtc" +echo "" +echo "🔗 RTSP URL (for VLC):" +echo " rtsp://$TAILSCALE_IP:8554/camera1" +echo "" +echo "⚠️ IMPORTANT: Stream times out after ~10 seconds without a viewer!" +echo " Start the stream, then QUICKLY open the web interface above" +echo "" + + diff --git a/docker-compose.yml b/docker-compose.yml index f2e935f..96e5f27 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,9 +14,11 @@ services: - LD_LIBRARY_PATH=/usr/local/lib:/lib:/usr/lib - PYTHONPATH=/app:/app/camera_sdk - TZ=America/New_York + - MEDIAMTX_HOST=localhost + - MEDIAMTX_RTSP_PORT=8554 command: > sh -lc " - apt-get update && apt-get install -y libusb-1.0-0-dev; + apt-get update && apt-get install -y libusb-1.0-0-dev ffmpeg; # Install camera SDK if not already installed if [ ! -f /lib/libMVSDK.so ] && [ -f 'camera_sdk/linuxSDK_V2.1.0.49(250108)/install.sh' ]; then diff --git a/mediamtx.yml b/mediamtx.yml index b591529..b5fc1ed 100644 --- a/mediamtx.yml +++ b/mediamtx.yml @@ -9,6 +9,11 @@ paths: # allow any path to be read; publishers can be added on-demand readUser: any sourceOnDemand: no + sourceOnDemandStartTimeout: 10s + sourceOnDemandCloseAfter: 10s + # Keep source alive even without readers (remove timeout) + # sourceCloseAfter: never # Keep stream alive indefinitely + sourceCloseAfter: 30s # Keep stream alive for 30 seconds after last reader disconnects # Example on-demand publisher for a demo VOD (adjust file path): # vod: diff --git a/rtsp_access_guide.md b/rtsp_access_guide.md new file mode 100644 index 0000000..4c347aa --- /dev/null +++ b/rtsp_access_guide.md @@ -0,0 +1,72 @@ +# RTSP Streaming Access Guide (Tailscale) + +## Your Tailscale IP +**100.93.40.84** + +## Access URLs + +Replace `exp-dash` with `100.93.40.84` in all URLs when accessing from outside the VM. + +### 1. MediaMTX Web Interface (Best for Testing) +``` +http://100.93.40.84:8889/static/ +``` +This shows all available streams with a built-in player. + +### 2. Direct WebRTC Player for Camera1 +``` +http://100.93.40.84:8889/camera1/webrtc +``` +This is a browser-based player that works without VLC. + +### 3. RTSP URL (for VLC/ffplay) +``` +rtsp://100.93.40.84:8554/camera1 +``` + +**For VLC:** +- File → Open Network Stream +- URL: `rtsp://100.93.40.84:8554/camera1` +- Or use: `rtsp://100.93.40.84:8554/camera1?transport=tcp` + +**For ffplay:** +```bash +ffplay -rtsp_transport tcp rtsp://100.93.40.84:8554/camera1 +``` + +## Important Notes + +1. **Stream Timeout**: MediaMTX closes streams after ~10 seconds if no one is watching. Make sure to: + - Start RTSP first: `curl -X POST http://exp-dash:8000/cameras/camera1/start-rtsp` + - Then quickly open the viewer within 10 seconds + +2. **Ports Exposed**: + - 8554: RTSP + - 8889: WebRTC/HTTP API + - 8189: WebRTC UDP + +3. **Network**: Tailscale creates a VPN, so you can access these URLs from anywhere, as long as your client device is also on the same Tailscale network. + +## Testing Steps + +1. **Start the stream:** + ```bash + curl -X POST http://exp-dash:8000/cameras/camera1/start-rtsp + ``` + +2. **Quickly open a viewer** (within 10 seconds): + - Open browser: `http://100.93.40.84:8889/static/` + - Or: `http://100.93.40.84:8889/camera1/webrtc` + +3. **Check status:** + ```bash + ./check_rtsp_status.sh + ``` + +## Troubleshooting + +If you see "no stream is available": +- The stream might have timed out (waited >10 seconds without viewer) +- Restart the stream and quickly connect +- Check logs: `docker compose logs api --tail 20 | grep RTSP` + diff --git a/test_rtsp.py b/test_rtsp.py new file mode 100644 index 0000000..7a4903b --- /dev/null +++ b/test_rtsp.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +""" +Quick test script to verify RTSP streaming is working. +Tests API endpoints and MediaMTX status. +""" +import requests +import json +import time +import sys + +API_BASE = "http://exp-dash:8000" +MEDIAMTX_BASE = "http://exp-dash:8889" +CAMERA = "camera1" + +def test_api_rtsp(): + """Test RTSP start endpoint""" + print(f"\n{'='*60}") + print(f"Testing RTSP streaming for {CAMERA}") + print(f"{'='*60}\n") + + # 1. Check if camera exists + print(f"1. Checking camera status...") + try: + resp = requests.get(f"{API_BASE}/cameras/{CAMERA}/status", timeout=5) + if resp.status_code == 200: + print(f" ✅ Camera {CAMERA} found") + print(f" Status: {json.dumps(resp.json(), indent=2)}") + else: + print(f" ❌ Camera {CAMERA} not found (status: {resp.status_code})") + return False + except Exception as e: + print(f" ❌ Error checking camera: {e}") + return False + + # 2. Start RTSP streaming + print(f"\n2. Starting RTSP streaming...") + try: + resp = requests.post(f"{API_BASE}/cameras/{CAMERA}/start-rtsp", timeout=10) + print(f" Response status: {resp.status_code}") + print(f" Response: {json.dumps(resp.json(), indent=2)}") + + if resp.status_code == 200 and resp.json().get("success"): + print(f" ✅ RTSP streaming started successfully") + rtsp_url = resp.json().get("rtsp_url", "N/A") + print(f" RTSP URL: {rtsp_url}") + else: + print(f" ❌ Failed to start RTSP streaming") + return False + except Exception as e: + print(f" ❌ Error starting RTSP: {e}") + return False + + # 3. Wait a moment for FFmpeg to start + print(f"\n3. Waiting for stream to initialize (5 seconds)...") + time.sleep(5) + + # 4. Check MediaMTX for the stream + print(f"\n4. Checking MediaMTX for stream...") + try: + resp = requests.get(f"{MEDIAMTX_BASE}/v2/paths/list", timeout=5) + if resp.status_code == 200: + paths = resp.json() + print(f" MediaMTX paths: {json.dumps(paths, indent=2)}") + + # Check if our camera path exists + if isinstance(paths, dict) and "items" in paths: + items = paths.get("items", {}) + if CAMERA in items: + path_info = items[CAMERA] + source_ready = path_info.get("sourceReady", False) + readers = path_info.get("readers", []) + print(f" ✅ Path '{CAMERA}' found in MediaMTX") + print(f" Source ready: {source_ready}") + print(f" Active readers: {len(readers)}") + if source_ready: + print(f" ✅ Stream is ready!") + else: + print(f" ⚠️ Stream not ready yet (sourceReady=false)") + else: + print(f" ❌ Path '{CAMERA}' not found in MediaMTX") + print(f" Available paths: {list(items.keys())}") + else: + print(f" ⚠️ Unexpected response format from MediaMTX") + else: + print(f" ⚠️ MediaMTX API returned status {resp.status_code}") + except Exception as e: + print(f" ⚠️ Error checking MediaMTX: {e}") + + # 5. Check specific path + print(f"\n5. Getting detailed info for path '{CAMERA}'...") + try: + resp = requests.get(f"{MEDIAMTX_BASE}/v2/paths/get/{CAMERA}", timeout=5) + if resp.status_code == 200: + info = resp.json() + print(f" Path info: {json.dumps(info, indent=2)}") + else: + print(f" Path not found (status: {resp.status_code})") + except Exception as e: + print(f" Error: {e}") + + print(f"\n{'='*60}") + print(f"Test complete!") + print(f"Try viewing the stream with:") + print(f" vlc rtsp://exp-dash:8554/{CAMERA}") + print(f" ffplay -rtsp_transport tcp rtsp://exp-dash:8554/{CAMERA}") + print(f"{'='*60}\n") + + return True + +if __name__ == "__main__": + test_api_rtsp() + +