Implement RTSP streaming functionality for cameras
- Added endpoints to start and stop RTSP streaming for cameras in the API. - Enhanced CameraManager and CameraStreamer classes to manage RTSP streaming state and processes. - Updated API documentation to include new RTSP streaming commands. - Modified Docker configurations to include FFmpeg for RTSP streaming support. - Adjusted MediaMTX settings for improved stream handling and timeout configurations.
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
72
check_rtsp_status.sh
Executable file
72
check_rtsp_status.sh
Executable file
@@ -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 ""
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
72
rtsp_access_guide.md
Normal file
72
rtsp_access_guide.md
Normal file
@@ -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`
|
||||
|
||||
113
test_rtsp.py
Normal file
113
test_rtsp.py
Normal file
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user