Fix headless video playback: prevent infinite looping and enable proper video transitions

- Modified video input to not loop intro/match/result videos (only black screen loops)
- Added automatic transition to black screen when non-looping videos end
- Restart HLS process during video switches for clean segment generation
- Added robust error handling for connection resets during transitions
- Increased HLS playlist size and disabled caching for smoother transitions
- Fixed video switching logic to ensure proper HLS content updates
parent a538f323
"""
RTSP Streamer component for headless video playback and overlay streaming
"""
import sys
import time
import logging
import threading
import subprocess
import json
import http.server
import socketserver
import os
import shutil
import tempfile
from pathlib import Path
from typing import Optional, Dict, Any, List
from urllib.parse import urlparse
try:
import ffmpeg
FFMPEG_PYTHON_AVAILABLE = True
except ImportError:
FFMPEG_PYTHON_AVAILABLE = False
logging.warning("ffmpeg-python not available, falling back to subprocess")
from .thread_manager import ThreadedComponent
from .message_bus import MessageBus, Message, MessageType, MessageBuilder
logger = logging.getLogger(__name__)
class RTSPStreamer(ThreadedComponent):
"""HLS streamer that implements qt-player functionality in headless mode"""
def __init__(self, message_bus: MessageBus, settings, debug_player: bool = False, test_mode: bool = False):
super().__init__(name="headless_player", message_bus=message_bus)
self.settings = settings
self.debug_player = debug_player
self.test_mode = test_mode
# RTSP streaming settings (kept for compatibility)
self.rtsp_port = 8554
self.stream_name = "mbetterc_stream"
self.rtsp_url = f"rtsp://127.0.0.1:{self.rtsp_port}/{self.stream_name}"
# HTTP streaming settings (configurable)
self.http_port = getattr(settings, 'streamer_port', 5884) # Default to 5884
self.http_url = f"http://127.0.0.1:{self.http_port}/{self.stream_name}.m3u8"
# Video processing
self.current_video_path = None
self.ffmpeg_hls_process = None
self.is_streaming = False
# Continuous streaming components
self.black_screen_file = None
self.continuous_mode = True
# Seamless switching using FIFO pipe
self.fifo_pipe = None
self.fifo_path = None
self.current_input_process = None
# Overlay data (simulated for headless mode)
self.overlay_data = {
'title': 'MbetterClient Headless Stream',
'subtitle': 'RTSP Streaming Active',
'ticker': 'Headless mode - no GUI display'
}
# Match tracking (similar to Qt player)
self.is_playing_match_video = False
self.current_match_id = None
self.current_match_video_filename = None
self.current_fixture_id = None
# Video playback state management
self.current_video_type = 'black_screen' # 'black_screen', 'intro', 'match', 'result'
self.video_loop_enabled = True # Videos loop by default
self.pending_video_request = None # Store pending video requests
self.video_end_timer = None # Timer for video completion
# HTTP server for HLS streaming
self.http_server = None
self.hls_dir = None
logger.info("RTSPStreamer initialized")
def initialize(self) -> bool:
"""Initialize the RTSP streamer"""
try:
logger.info("Initializing RTSP streamer...")
# Register with message bus
self.message_bus.register_component(self.name)
# Check if ffmpeg is available
if not self._check_ffmpeg():
logger.error("FFmpeg not found - required for RTSP streaming")
return False
# Subscribe to messages
self.message_bus.subscribe(self.name, MessageType.VIDEO_PLAY, self._handle_video_play)
self.message_bus.subscribe(self.name, MessageType.VIDEO_PAUSE, self._handle_video_pause)
self.message_bus.subscribe(self.name, MessageType.VIDEO_STOP, self._handle_video_stop)
self.message_bus.subscribe(self.name, MessageType.VIDEO_SEEK, self._handle_video_seek)
self.message_bus.subscribe(self.name, MessageType.VIDEO_VOLUME, self._handle_video_volume)
self.message_bus.subscribe(self.name, MessageType.TEMPLATE_CHANGE, self._handle_template_change)
self.message_bus.subscribe(self.name, MessageType.OVERLAY_UPDATE, self._handle_overlay_update)
self.message_bus.subscribe(self.name, MessageType.START_INTRO, self._handle_start_intro)
self.message_bus.subscribe(self.name, MessageType.PLAY_VIDEO_MATCH, self._handle_play_video_match)
self.message_bus.subscribe(self.name, MessageType.PLAY_VIDEO_RESULT, self._handle_play_video_result)
self.message_bus.subscribe(self.name, MessageType.GAME_STATUS, self._handle_game_status)
# Start continuous streaming immediately
logger.info("Starting continuous HLS streaming...")
if not self._start_continuous_streaming():
logger.error("Failed to start continuous streaming")
return False
logger.info(f"HLS streamer initialized - continuous streaming at {self.http_url}")
return True
except Exception as e:
logger.error(f"RTSP streamer initialization failed: {e}")
return False
def run(self):
"""Main run loop for RTSP streamer"""
logger.info("RTSP streamer main loop started")
# In continuous mode, streaming is already started in initialize()
# with black screen as default
logger.info("Continuous streaming is active with black screen as default")
# If test mode is enabled, automatically start intro video after a short delay
if self.test_mode:
logger.info("Test mode enabled - will automatically start intro video")
threading.Timer(3.0, self._start_test_intro).start()
last_heartbeat = time.time()
while self.running:
try:
# Process messages
message = self.message_bus.get_message(self.name, timeout=1.0)
if message:
self._process_message(message)
# Check streaming status
if self.is_streaming:
hls_terminated = self.ffmpeg_hls_process and self.ffmpeg_hls_process.poll() is not None
input_terminated = self.current_input_process and self.current_input_process.poll() is not None
if hls_terminated or input_terminated:
logger.warning("FFmpeg streaming process terminated unexpectedly")
if hls_terminated:
logger.warning("HLS streaming process terminated")
if input_terminated:
logger.warning("Video input process terminated")
# If a non-black-screen video ended, switch back to black screen
if self.current_video_type != 'black_screen':
logger.info(f"Non-looping video ({self.current_video_type}) ended, switching to black screen")
self._switch_to_black_screen()
else:
self.is_streaming = False
else:
self.is_streaming = False
# Send heartbeat every 10 seconds
current_time = time.time()
if current_time - last_heartbeat >= 10.0:
self.heartbeat()
last_heartbeat = current_time
time.sleep(0.1)
except Exception as e:
logger.error(f"RTSP streamer error: {e}")
time.sleep(1)
logger.info("RTSP streamer main loop ended")
def shutdown(self):
"""Shutdown the RTSP streamer"""
logger.info("Shutting down RTSP streamer...")
# Stop continuous streaming
self._stop_continuous_streaming()
# Stop HTTP server
self._stop_http_server()
super().shutdown()
def _check_ffmpeg(self) -> bool:
"""Check if ffmpeg is available"""
try:
result = subprocess.run(['ffmpeg', '-version'],
capture_output=True, text=True, timeout=5)
if result.returncode == 0:
logger.info("FFmpeg found and available")
return True
else:
logger.error("FFmpeg not working properly")
return False
except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError):
logger.error("FFmpeg not found in PATH")
logger.debug("To install FFmpeg on Ubuntu/Debian, run: sudo apt update && sudo apt install ffmpeg")
return False
def _start_continuous_streaming(self) -> bool:
"""Start continuous HLS streaming with black screen fallback"""
try:
logger.info("Starting continuous HLS streaming with black screen fallback...")
# Create black screen video file
if not self._create_black_screen_file():
logger.error("Failed to create black screen file")
return False
# Create FIFO pipe for seamless switching
if not self._create_fifo_pipe():
logger.error("Failed to create FIFO pipe")
return False
# Start with black screen input process first
if not self._start_video_input(self.black_screen_file, loop=True):
logger.error("Failed to start initial black screen input")
return False
# Now start FFmpeg reading from FIFO pipe
if not self._start_ffmpeg_hls_stream():
logger.error("Failed to start FFmpeg HLS stream")
return False
# Mark as streaming successfully
self.is_streaming = True
logger.info("Continuous HLS streaming started successfully")
return True
except Exception as e:
logger.error(f"Failed to start continuous HLS streaming: {e}")
return False
def _create_fifo_pipe(self) -> bool:
"""Create named FIFO pipe for seamless video switching"""
try:
import tempfile
import os
# Create FIFO pipe in temp directory
temp_dir = tempfile.gettempdir()
self.fifo_path = os.path.join(temp_dir, f"mbetterc_fifo_{os.getpid()}")
# Remove existing FIFO if it exists
if os.path.exists(self.fifo_path):
os.unlink(self.fifo_path)
# Create FIFO pipe
os.mkfifo(self.fifo_path)
logger.info(f"Created FIFO pipe: {self.fifo_path}")
return True
except Exception as e:
logger.error(f"Failed to create FIFO pipe: {e}")
return False
def _start_ffmpeg_hls_stream(self) -> bool:
"""Start FFmpeg HLS streaming that reads from FIFO pipe continuously"""
try:
logger.info("Starting FFmpeg HLS streaming from FIFO pipe...")
# Create HLS output directory
self.hls_dir = tempfile.mkdtemp(prefix="mbetterc_hls_")
hls_playlist = os.path.join(self.hls_dir, f"{self.stream_name}.m3u8")
hls_segment_pattern = os.path.join(self.hls_dir, f"{self.stream_name}_%04d.ts")
# FFmpeg command optimized for HLS streaming and VLC compatibility
cmd = [
'ffmpeg',
'-y', # Overwrite output files
'-f', 'mpegts', # Input format (MPEG-TS from FIFO)
'-i', self.fifo_path, # Read from FIFO pipe
'-c:v', 'libx264', # Video codec
'-preset', 'veryfast', # Encoding preset
'-tune', 'zerolatency', # Tune for low latency
'-g', '48', # GOP size (2 seconds at 24fps, aligned with 4-second segments)
'-keyint_min', '48', # Minimum keyframe interval
'-sc_threshold', '0', # Disable scene change detection for consistent GOPs
'-c:a', 'aac', # Audio codec
'-b:a', '128k', # Audio bitrate
'-ac', '2', # Stereo audio
'-ar', '44100', # Audio sample rate
'-f', 'hls', # Output format (HLS)
'-hls_time', '4', # Segment duration (4 seconds - better for VLC)
'-hls_list_size', '15', # Keep 15 segments in playlist (increased for smoother transitions)
'-hls_flags', 'independent_segments+delete_segments', # Better HLS compatibility
'-hls_segment_filename', hls_segment_pattern, # Segment filename pattern
'-start_number', '0', # Start segment numbering from 0
'-hls_allow_cache', '0', # Disable caching to ensure fresh segments
hls_playlist # HLS playlist file
]
logger.info(f"Starting FFmpeg HLS streaming with command: {' '.join(cmd)}")
self.ffmpeg_hls_process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
logger.info(f"FFmpeg HLS subprocess created, PID: {self.ffmpeg_hls_process.pid}")
# Start HTTP server
self._start_http_server()
# Wait for HLS playlist to be created (input process should already be running)
time.sleep(3)
# Check if HLS process is still running
hls_process_running = self.ffmpeg_hls_process.poll() is None
if hls_process_running:
logger.info(f"HLS streaming started - playlist at {self.http_url}")
return True
else:
logger.error("FFmpeg HLS process failed to start")
self._stop_continuous_streaming()
return False
except Exception as e:
logger.error(f"Failed to start FFmpeg HLS stream: {e}")
return False
def _switch_to_video(self, video_path: str):
"""Switch streaming to video file by changing FIFO input"""
try:
logger.info(f"Switching streaming to video: {video_path}")
# Stop current input process
if self.current_input_process:
logger.info("Stopping current input process...")
self.current_input_process.terminate()
try:
self.current_input_process.wait(timeout=2)
except subprocess.TimeoutExpired:
logger.warning("Input process didn't terminate gracefully, killing...")
self.current_input_process.kill()
self.current_input_process.wait()
self.current_input_process = None
# Brief pause to ensure clean transition
time.sleep(0.5)
# Set new video path
self.current_video_path = video_path
# Start new input process feeding into FIFO
success = self._start_video_input(video_path, loop=False)
if success:
# Always restart HLS process for clean segment generation during video switches
logger.info("Restarting HLS process for clean video transition...")
if self.ffmpeg_hls_process:
logger.info("Stopping current HLS process...")
self.ffmpeg_hls_process.terminate()
try:
self.ffmpeg_hls_process.wait(timeout=2)
except subprocess.TimeoutExpired:
logger.warning("HLS process didn't terminate gracefully, killing...")
self.ffmpeg_hls_process.kill()
self.ffmpeg_hls_process.wait()
self.ffmpeg_hls_process = None
# Clean up old HLS directory
if self.hls_dir and os.path.exists(self.hls_dir):
import shutil
shutil.rmtree(self.hls_dir)
self.hls_dir = None
# Restart HLS streaming with fresh directory
if not self._start_ffmpeg_hls_stream():
logger.error("Failed to restart HLS streaming after video switch")
return False
logger.info("Video switch completed successfully - HLS process restarted")
return True
else:
logger.error("Failed to start new video input process")
return False
except Exception as e:
logger.error(f"Failed to switch to video: {e}")
return False
def _start_video_input(self, video_path: str, loop: bool = False) -> bool:
"""Start FFmpeg process to feed video into FIFO pipe"""
try:
logger.info(f"Starting video input process for: {video_path} (loop={loop})")
# FFmpeg command to read video and output to FIFO pipe
cmd = [
'ffmpeg',
'-re', # Read input at native frame rate
'-i', video_path, # Input video file
'-c', 'copy', # Copy streams without re-encoding (faster)
'-f', 'mpegts', # Output format (MPEG-TS for FIFO)
'-y', # Overwrite output
self.fifo_path # Output to FIFO pipe
]
# Add loop option only for black screen
if loop:
cmd.insert(2, '-stream_loop')
cmd.insert(3, '-1')
logger.info(f"Starting video input with command: {' '.join(cmd)}")
self.current_input_process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
logger.info(f"Video input process started, PID: {self.current_input_process.pid}")
# Wait a moment for the process to start
time.sleep(1)
# Check if process is still running
if self.current_input_process.poll() is None:
logger.info("Video input process is running successfully")
return True
else:
logger.error("Video input process failed to start")
return False
except Exception as e:
logger.error(f"Failed to start video input: {e}")
return False
def _switch_to_black_screen(self):
"""Switch streaming to black screen by changing FIFO input"""
try:
logger.info("Switching streaming to black screen")
# Stop current input process
if self.current_input_process:
logger.info("Stopping current input process...")
self.current_input_process.terminate()
try:
self.current_input_process.wait(timeout=2)
except subprocess.TimeoutExpired:
logger.warning("Input process didn't terminate gracefully, killing...")
self.current_input_process.kill()
self.current_input_process.wait()
self.current_input_process = None
# Brief pause to ensure clean transition
time.sleep(0.5)
# Set black screen as current video
self.current_video_path = self.black_screen_file
self.current_video_type = 'black_screen'
# Start black screen input
success = self._start_video_input(self.black_screen_file, loop=True)
if success:
# Always restart HLS process for clean segment generation during switches
logger.info("Restarting HLS process for clean black screen transition...")
if self.ffmpeg_hls_process:
logger.info("Stopping current HLS process...")
self.ffmpeg_hls_process.terminate()
try:
self.ffmpeg_hls_process.wait(timeout=2)
except subprocess.TimeoutExpired:
logger.warning("HLS process didn't terminate gracefully, killing...")
self.ffmpeg_hls_process.kill()
self.ffmpeg_hls_process.wait()
self.ffmpeg_hls_process = None
# Clean up old HLS directory
if self.hls_dir and os.path.exists(self.hls_dir):
import shutil
shutil.rmtree(self.hls_dir)
self.hls_dir = None
# Restart HLS streaming with fresh directory
if not self._start_ffmpeg_hls_stream():
logger.error("Failed to restart HLS streaming after black screen switch")
return False
logger.info("Black screen switch completed successfully - HLS process restarted")
return True
else:
logger.error("Failed to start black screen input process")
return False
except Exception as e:
logger.error(f"Failed to switch to black screen: {e}")
return False
def _create_black_screen_file(self) -> bool:
"""Create a looping black screen video file"""
try:
# Create black screen video file in temp directory
temp_dir = tempfile.gettempdir()
self.black_screen_file = os.path.join(temp_dir, f"mbetterc_black_{os.getpid()}.mp4")
# Remove existing file if it exists
if os.path.exists(self.black_screen_file):
os.unlink(self.black_screen_file)
# Create a 10-second looping black screen video
cmd = [
'ffmpeg',
'-f', 'lavfi', # Use libavfilter input
'-i', 'color=c=black:s=1920x1080:r=30:d=10', # Black color, 10 seconds
'-f', 'lavfi', # Audio input
'-i', 'anullsrc=r=44100:cl=stereo', # Silent audio
'-c:v', 'libx264', # Video codec
'-preset', 'ultrafast', # Fast encoding
'-c:a', 'aac', # Audio codec
'-shortest', # End when shortest input ends
'-y', # Overwrite output
self.black_screen_file # Output file
]
logger.info("Creating black screen video file...")
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=30
)
if result.returncode == 0 and os.path.exists(self.black_screen_file):
logger.info(f"Created black screen video file: {self.black_screen_file}")
return True
else:
logger.error(f"Failed to create black screen file: {result.stderr}")
return False
except Exception as e:
logger.error(f"Failed to create black screen file: {e}")
return False
def _stop_continuous_streaming(self):
"""Stop continuous HLS streaming"""
try:
logger.info("Stopping continuous streaming...")
# Stop input process first
if self.current_input_process:
logger.info("Stopping video input process...")
self.current_input_process.terminate()
try:
self.current_input_process.wait(timeout=2)
except subprocess.TimeoutExpired:
logger.warning("Input process didn't terminate gracefully, killing...")
self.current_input_process.kill()
self.current_input_process.wait()
self.current_input_process = None
# Stop HLS process
if self.ffmpeg_hls_process:
logger.info("Stopping HLS streaming...")
self.ffmpeg_hls_process.terminate()
try:
self.ffmpeg_hls_process.wait(timeout=5)
except subprocess.TimeoutExpired:
logger.warning("FFmpeg HLS process didn't terminate gracefully, killing...")
self.ffmpeg_hls_process.kill()
self.ffmpeg_hls_process.wait()
self.ffmpeg_hls_process = None
# Clean up FIFO pipe
if self.fifo_path and os.path.exists(self.fifo_path):
try:
os.unlink(self.fifo_path)
logger.info(f"Removed FIFO pipe: {self.fifo_path}")
except Exception as e:
logger.warning(f"Failed to remove FIFO pipe: {e}")
self.fifo_path = None
# Clean up black screen file
if self.black_screen_file and os.path.exists(self.black_screen_file):
try:
os.unlink(self.black_screen_file)
logger.info(f"Removed black screen file: {self.black_screen_file}")
except Exception as e:
logger.warning(f"Failed to remove black screen file: {e}")
self.black_screen_file = None
self.is_streaming = False
self.current_video_path = None
# Clean up HLS directory
if self.hls_dir and os.path.exists(self.hls_dir):
shutil.rmtree(self.hls_dir)
self.hls_dir = None
logger.info("Continuous streaming stopped")
except Exception as e:
logger.error(f"Error stopping continuous streaming: {e}")
def _start_http_server(self):
"""Start HTTP server to serve HLS files"""
try:
if self.http_server:
logger.warning("HTTP server already running")
return
# Create a custom request handler that serves files from the HLS directory
class HLSRequestHandler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, streamer=None, **kwargs):
self.streamer = streamer
super().__init__(*args, **kwargs)
def translate_path(self, path):
# Serve files from the current HLS directory
if self.streamer and self.streamer.hls_dir:
# Remove leading slash and join with HLS directory
path = path.lstrip('/')
file_path = os.path.join(self.streamer.hls_dir, path)
if os.path.exists(file_path) and os.path.isfile(file_path):
return file_path
else:
# File doesn't exist, return a non-existent path to trigger 404
return "/nonexistent_file_404"
else:
# No active stream, return 404
return "/nonexistent_file_404"
def do_GET(self):
try:
# Check if we have an active stream
if not self.streamer or not self.streamer.hls_dir:
self.send_error(404, "No active stream. Start streaming first.")
return
# Check if the requested file exists
file_path = self.translate_path(self.path)
if not os.path.exists(file_path):
self.send_error(404, "File not found")
return
# Serve the file
super().do_GET()
except (ConnectionResetError, BrokenPipeError, OSError) as e:
# Handle connection errors gracefully during video transitions
logger.debug(f"Connection error during HLS request: {e}")
# Don't send error response as connection is already broken
return
except Exception as e:
logger.warning(f"Unexpected error in HLS request handler: {e}")
try:
self.send_error(500, "Internal server error")
except (ConnectionResetError, BrokenPipeError, OSError):
# Connection already broken, can't send error
pass
def log_message(self, format, *args):
# Suppress HTTP server logs
pass
# Create HTTP server
self.http_server = socketserver.TCPServer(("", self.http_port), lambda *args, **kwargs: HLSRequestHandler(*args, streamer=self, **kwargs))
logger.info(f"HTTP server created on port {self.http_port}")
# Run server in a separate thread
server_thread = threading.Thread(target=self.http_server.serve_forever, daemon=True)
server_thread.start()
logger.info(f"HTTP server started on port {self.http_port}")
except Exception as e:
logger.error(f"Failed to start HTTP server: {e}")
def _stop_http_server(self):
"""Stop HTTP server"""
try:
if self.http_server:
logger.info("Stopping HTTP server...")
self.http_server.shutdown()
self.http_server.server_close()
self.http_server = None
logger.info("HTTP server stopped")
except Exception as e:
logger.error(f"Error stopping HTTP server: {e}")
def _update_overlay(self, data: Dict[str, Any]):
"""Update overlay data for headless mode"""
try:
self.overlay_data.update(data)
logger.debug(f"Overlay updated: {self.overlay_data}")
# In headless mode, we could potentially overlay text on the video stream
# using ffmpeg filters, but for now we'll just log the overlay data
except Exception as e:
logger.error(f"Failed to update overlay: {e}")
def _process_message(self, message: Message):
"""Process incoming messages"""
try:
if self.debug_player:
logger.info(f"RTSPStreamer processing message: {message.type.value}")
# Route to appropriate handlers
if message.type == MessageType.VIDEO_PLAY:
self._handle_video_play(message)
elif message.type == MessageType.VIDEO_PAUSE:
self._handle_video_pause(message)
elif message.type == MessageType.VIDEO_STOP:
self._handle_video_stop(message)
elif message.type == MessageType.VIDEO_SEEK:
self._handle_video_seek(message)
elif message.type == MessageType.VIDEO_VOLUME:
self._handle_video_volume(message)
elif message.type == MessageType.TEMPLATE_CHANGE:
self._handle_template_change(message)
elif message.type == MessageType.OVERLAY_UPDATE:
self._handle_overlay_update(message)
elif message.type == MessageType.START_INTRO:
self._handle_start_intro(message)
elif message.type == MessageType.PLAY_VIDEO_MATCH:
self._handle_play_video_match(message)
elif message.type == MessageType.PLAY_VIDEO_RESULT:
self._handle_play_video_result(message)
elif message.type == MessageType.GAME_STATUS:
self._handle_game_status(message)
else:
logger.debug(f"Unhandled message type in RTSPStreamer: {message.type}")
except Exception as e:
logger.error(f"Failed to process message: {e}")
# Message handlers (similar to Qt player but for RTSP streaming)
def _handle_video_play(self, message: Message):
"""Handle video play message"""
try:
file_path = message.data.get("file_path")
template_data = message.data.get("overlay_data", {})
template_name = message.data.get("template")
if not file_path:
logger.error("No file path provided for video play")
return
logger.info(f"RTSPStreamer: Switching to video {file_path}")
# Update overlay data
if template_data:
self._update_overlay(template_data)
# Switch to video streaming
self._switch_to_video(file_path)
except Exception as e:
logger.error(f"Failed to handle video play: {e}")
def _handle_video_pause(self, message: Message):
"""Handle video pause message"""
# For continuous streaming, pause means switch to black screen
logger.info("RTSPStreamer: Pausing (switching to black screen)")
self._switch_to_black_screen()
def _handle_video_stop(self, message: Message):
"""Handle video stop message"""
logger.info("RTSPStreamer: Stopping video (switching to black screen)")
self._switch_to_black_screen()
def _handle_video_seek(self, message: Message):
"""Handle video seek message"""
# RTSP streaming doesn't support seeking in the same way
# Would need to restart stream at different position
position = message.data.get("position", 0)
logger.info(f"RTSPStreamer: Seek to position {position} (not implemented for RTSP)")
def _handle_video_volume(self, message: Message):
"""Handle video volume message"""
# Volume control in RTSP stream would require restarting ffmpeg with different audio settings
volume = message.data.get("volume", 100)
logger.info(f"RTSPStreamer: Volume change to {volume}% (not implemented for RTSP)")
def _handle_template_change(self, message: Message):
"""Handle template change message"""
template_name = message.data.get("template_name", "")
template_data = message.data.get("template_data", {})
logger.info(f"RTSPStreamer: Template change to {template_name}")
# Update overlay data
if template_data:
self._update_overlay(template_data)
def _handle_overlay_update(self, message: Message):
"""Handle overlay update message"""
overlay_data = message.data.get("overlay_data", {})
self._update_overlay(overlay_data)
def _handle_start_intro(self, message: Message):
"""Handle START_INTRO message"""
logger.info("RTSPStreamer: Handling START_INTRO message")
# Similar logic to Qt player but for RTSP streaming
match_id = message.data.get("match_id")
fixture_id = message.data.get("fixture_id")
# Find intro video
intro_path = self._find_intro_video_file(match_id)
if intro_path:
logger.info(f"RTSPStreamer: Starting intro video: {intro_path}")
# Set up loop control for intro video
loop_data = {
'infinite_loop': True,
'continuous_playback': True
}
# Play intro video with RTSP streaming
overlay_data = {
"title": "Intro Video",
"subtitle": "Preparing for match...",
"match_id": match_id,
"fixture_id": fixture_id
}
self._update_overlay(overlay_data)
self._switch_to_video(str(intro_path))
self.current_video_type = 'intro'
else:
logger.warning("RTSPStreamer: No intro video found")
def _handle_play_video_match(self, message: Message):
"""Handle PLAY_VIDEO_MATCH message"""
try:
logger.info("RTSPStreamer: Handling PLAY_VIDEO_MATCH message")
match_id = message.data.get("match_id")
video_filename = message.data.get("video_filename")
fixture_id = message.data.get("fixture_id")
result = message.data.get("result")
if not match_id or not video_filename:
logger.error("RTSPStreamer: Missing match_id or video_filename")
return
# Switch to match video (continuous streaming)
match_video_path = self._find_match_video_file(match_id, video_filename)
if match_video_path:
logger.info(f"RTSPStreamer: Switching to match video: {match_video_path}")
# Set match tracking
self.is_playing_match_video = True
self.current_match_id = match_id
self.current_match_video_filename = video_filename
self.current_fixture_id = fixture_id
# Update overlay
overlay_data = {
"title": f"Match {match_id}",
"subtitle": f"Result: {result}" if result else "Live Action",
"result": result,
"match_id": match_id,
"fixture_id": fixture_id
}
self._update_overlay(overlay_data)
self._switch_to_video(str(match_video_path))
self.current_video_type = 'match'
else:
logger.error(f"RTSPStreamer: Match video not found: {video_filename}")
except Exception as e:
logger.error(f"RTSPStreamer: Failed to handle PLAY_VIDEO_MATCH: {e}")
def _handle_play_video_result(self, message: Message):
"""Handle PLAY_VIDEO_RESULTS message"""
try:
logger.info("RTSPStreamer: Handling PLAY_VIDEO_RESULTS message")
fixture_id = message.data.get("fixture_id")
match_id = message.data.get("match_id")
result = message.data.get("result")
winning_outcomes = message.data.get("winning_outcomes", [])
# Find result video
result_video_path = self._find_result_video_file(match_id, result)
if result_video_path:
logger.info(f"RTSPStreamer: Switching to result video: {result_video_path}")
# Update match tracking
if self.is_playing_match_video:
self.is_playing_match_video = False
# Update overlay with results
overlay_data = {
'outcome': result,
'result': result,
'winningOutcomes': [{'outcome': outcome} for outcome in winning_outcomes],
'match_id': match_id,
'fixture_id': fixture_id,
'is_result_video': True
}
self._update_overlay(overlay_data)
self._switch_to_video(str(result_video_path))
self.current_video_type = 'result'
else:
logger.error(f"RTSPStreamer: Result video not found for {result}")
except Exception as e:
logger.error(f"RTSPStreamer: Failed to handle PLAY_VIDEO_RESULTS: {e}")
def _handle_game_status(self, message: Message):
"""Handle GAME_STATUS messages"""
status = message.data.get("status")
logger.info(f"RTSPStreamer: Game status: {status}")
# Similar logic to Qt player for determining when to play intro
def _start_test_intro(self):
"""Automatically start intro video for testing in headless mode"""
try:
logger.info("RTSPStreamer: Starting test intro video")
# Create a START_INTRO message and handle it
intro_message = Message(
type=MessageType.START_INTRO,
sender="test_mode",
data={
"match_id": None,
"fixture_id": None
}
)
self._handle_start_intro(intro_message)
logger.info("RTSPStreamer: Test intro video started successfully")
except Exception as e:
logger.error(f"RTSPStreamer: Failed to start test intro: {e}")
# Helper methods (similar to Qt player)
def _find_intro_video_file(self, match_id: int) -> Optional[Path]:
"""Find intro video file"""
try:
# Go up to project root: mbetterclient/core/rtsp_streamer.py -> mbetterclient/core -> mbetterclient -> project_root
assets_dir = Path(__file__).parent.parent.parent / "assets"
intro_file = assets_dir / "INTRO.mp4"
if intro_file.exists():
logger.info(f"RTSPStreamer: Found intro video: {intro_file}")
return intro_file
else:
logger.warning(f"RTSPStreamer: Intro video not found: {intro_file}")
return None
except Exception as e:
logger.error(f"RTSPStreamer: Failed to find intro video: {e}")
return None
def _find_match_video_file(self, match_id: int, video_filename: str) -> Optional[Path]:
"""Find match video file from extracted ZIP"""
try:
import tempfile
temp_base = Path(tempfile.gettempdir())
temp_dir_pattern = f"match_{match_id}_"
for temp_dir in temp_base.glob(f"{temp_dir_pattern}*"):
if temp_dir.is_dir():
video_file = temp_dir / video_filename
if video_file.exists():
logger.info(f"RTSPStreamer: Found match video: {video_file}")
return video_file
logger.warning(f"RTSPStreamer: Match video not found: {video_filename}")
return None
except Exception as e:
logger.error(f"RTSPStreamer: Failed to find match video: {e}")
return None
def _find_result_video_file(self, match_id: int, result: str) -> Optional[Path]:
"""Find result video file"""
try:
import tempfile
temp_base = Path(tempfile.gettempdir())
temp_dir_pattern = f"match_{match_id}_"
result_filename = f"{result}.mp4"
for temp_dir in temp_base.glob(f"{temp_dir_pattern}*"):
if temp_dir.is_dir():
result_file = temp_dir / result_filename
if result_file.exists():
logger.info(f"RTSPStreamer: Found result video: {result_file}")
return result_file
logger.warning(f"RTSPStreamer: Result video not found: {result_filename}")
return None
except Exception as e:
logger.error(f"RTSPStreamer: Failed to find result video: {e}")
return None
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment