diff --git a/backend/app/api/routes.py b/backend/app/api/routes.py index bcd672d..4d2dcf0 100644 --- a/backend/app/api/routes.py +++ b/backend/app/api/routes.py @@ -8,6 +8,7 @@ from pydantic import BaseModel, Field from ..models import RecorderConfig, PortStatus, SCTE35Marker from ..recorders.recorder import RecorderManager from ..recorders.scte35 import SCTE35Manager +from ..utils.hls import HLSPreviewManager logger = logging.getLogger(__name__) @@ -31,22 +32,27 @@ router = APIRouter(prefix="/api") # These will be set by main.py during startup recorder_manager: RecorderManager | None = None scte35_manager: SCTE35Manager | None = None +hls_manager: HLSPreviewManager | None = None def get_recorder_manager() -> RecorderManager: - """Dependency to get the RecorderManager instance.""" if recorder_manager is None: raise HTTPException(status_code=503, detail="Recorder not initialized") return recorder_manager def get_scte35_manager() -> SCTE35Manager: - """Dependency to get the SCTE35Manager instance.""" if scte35_manager is None: raise HTTPException(status_code=503, detail="SCTE35 manager not initialized") return scte35_manager +def get_hls_manager() -> HLSPreviewManager: + if hls_manager is None: + raise HTTPException(status_code=503, detail="HLS manager not initialized") + return hls_manager + + # ============================================================================ # Health and Status Endpoints # ============================================================================ @@ -54,16 +60,9 @@ def get_scte35_manager() -> SCTE35Manager: @router.get("/health") async def health(manager: RecorderManager = Depends(get_recorder_manager)) -> dict[str, Any]: - """ - Health check endpoint. - - Returns: - dict with status, port count, and recording count - """ try: all_status = manager.get_all_status() recording_count = sum(1 for status in all_status if status.is_recording) - return { "status": "ok", "ports": len(all_status), @@ -81,12 +80,6 @@ async def health(manager: RecorderManager = Depends(get_recorder_manager)) -> di @router.get("/ports", response_model=list[PortStatus]) async def list_ports(manager: RecorderManager = Depends(get_recorder_manager)) -> list[PortStatus]: - """ - Get status of all recorder ports. - - Returns: - List of PortStatus objects for all ports - """ try: return manager.get_all_status() except Exception as e: @@ -99,22 +92,9 @@ async def get_port( port_index: int, manager: RecorderManager = Depends(get_recorder_manager), ) -> PortStatus: - """ - Get status of a specific recorder port. - - Args: - port_index: 0-based port index - - Returns: - PortStatus for the specified port - - Raises: - HTTPException(404): If port index is invalid - """ try: return manager.get_status(port_index) except ValueError as e: - logger.warning(f"Invalid port index {port_index}: {e}") raise HTTPException(status_code=404, detail=f"Port {port_index} not found") except Exception as e: logger.error(f"Error getting port {port_index}: {e}") @@ -132,32 +112,19 @@ async def start_recording( config: RecorderConfig, manager: RecorderManager = Depends(get_recorder_manager), ) -> dict[str, Any]: - """ - Start recording on a specific port. - - Args: - port_index: 0-based port index - config: RecorderConfig with codec and output settings - - Returns: - Message confirmation with port number - - Raises: - HTTPException(400): If port is already recording or config is invalid - HTTPException(404): If port index is invalid - """ try: await manager.start_recording(port_index, config) + # Auto-start HLS preview if enabled in config + if config.preview_enabled and hls_manager is not None: + try: + await hls_manager.start_preview(port_index) + except Exception as e: + logger.warning(f"HLS preview failed to start for port {port_index}: {e}") logger.info(f"Started recording on port {port_index}") - return { - "message": "Recording started", - "port": port_index, - } + return {"message": "Recording started", "port": port_index} except ValueError as e: - logger.warning(f"Invalid port {port_index}: {e}") raise HTTPException(status_code=404, detail=f"Port {port_index} not found") except RuntimeError as e: - logger.warning(f"Cannot start recording on port {port_index}: {e}") raise HTTPException(status_code=400, detail=str(e)) except Exception as e: logger.error(f"Error starting recording on port {port_index}: {e}") @@ -169,33 +136,71 @@ async def stop_recording( port_index: int, manager: RecorderManager = Depends(get_recorder_manager), ) -> dict[str, Any]: - """ - Stop recording on a specific port. - - Args: - port_index: 0-based port index - - Returns: - Message confirmation with port number - - Raises: - HTTPException(404): If port index is invalid - """ try: await manager.stop_recording(port_index) + # Stop HLS preview when recording stops + if hls_manager is not None: + try: + await hls_manager.stop_preview(port_index) + except Exception as e: + logger.warning(f"HLS preview failed to stop for port {port_index}: {e}") logger.info(f"Stopped recording on port {port_index}") - return { - "message": "Recording stopped", - "port": port_index, - } + return {"message": "Recording stopped", "port": port_index} except ValueError as e: - logger.warning(f"Invalid port {port_index}: {e}") raise HTTPException(status_code=404, detail=f"Port {port_index} not found") except Exception as e: logger.error(f"Error stopping recording on port {port_index}: {e}") raise HTTPException(status_code=500, detail="Internal server error") +# ============================================================================ +# HLS Preview Endpoints +# ============================================================================ + + +@router.post("/ports/{port_index}/preview/start") +async def start_preview( + port_index: int, + manager: HLSPreviewManager = Depends(get_hls_manager), +) -> dict[str, Any]: + """Manually start HLS preview for a port (independent of recording).""" + try: + await manager.start_preview(port_index) + playlist_url = f"/hls/port_{port_index}.m3u8" + return {"message": "HLS preview started", "port": port_index, "playlist_url": playlist_url} + except Exception as e: + logger.error(f"Error starting HLS preview on port {port_index}: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/ports/{port_index}/preview/stop") +async def stop_preview( + port_index: int, + manager: HLSPreviewManager = Depends(get_hls_manager), +) -> dict[str, Any]: + """Manually stop HLS preview for a port.""" + try: + await manager.stop_preview(port_index) + return {"message": "HLS preview stopped", "port": port_index} + except Exception as e: + logger.error(f"Error stopping HLS preview on port {port_index}: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/ports/{port_index}/preview/status") +async def preview_status( + port_index: int, + manager: HLSPreviewManager = Depends(get_hls_manager), +) -> dict[str, Any]: + """Get HLS preview status for a port.""" + active = manager.is_previewing(port_index) + return { + "port": port_index, + "active": active, + "playlist_url": f"/hls/port_{port_index}.m3u8" if active else None, + } + + # ============================================================================ # SCTE35 Ad Break Endpoints # ============================================================================ @@ -206,18 +211,6 @@ async def inject_scte35_marker( request: SCTE35InjectionRequest, manager: SCTE35Manager = Depends(get_scte35_manager), ) -> SCTE35Marker: - """ - Inject a SCTE35 ad break marker globally (all ports). - - Args: - request: SCTE35InjectionRequest - - Returns: - SCTE35Marker with timestamp and metadata - - Raises: - HTTPException(400): If parameters are invalid - """ try: marker = await manager.inject_marker( event_id=request.event_id, @@ -228,12 +221,9 @@ async def inject_scte35_marker( port_index=None, srt_destination_url=request.srt_destination_url, ) - logger.info(f"Injected global SCTE35 marker event_id={request.event_id}") return marker - except ValueError as e: - logger.warning(f"Invalid SCTE35 parameters: {e}") raise HTTPException(status_code=400, detail=str(e)) except Exception as e: logger.error(f"Error injecting SCTE35 marker: {e}") @@ -246,16 +236,6 @@ async def inject_scte35_marker_for_port( request: SCTE35InjectionRequest, manager: SCTE35Manager = Depends(get_scte35_manager), ) -> SCTE35Marker: - """ - Inject a SCTE35 ad break marker on a specific port's SRT output(s). - - Args: - port_index: 0-based port index - request: SCTE35InjectionRequest - - Returns: - SCTE35Marker with timestamp, port_index, and metadata - """ try: marker = await manager.inject_marker( event_id=request.event_id, @@ -266,12 +246,9 @@ async def inject_scte35_marker_for_port( port_index=port_index, srt_destination_url=request.srt_destination_url, ) - logger.info(f"Injected SCTE35 marker event_id={request.event_id} on port {port_index}") return marker - except ValueError as e: - logger.warning(f"Invalid SCTE35 parameters: {e}") raise HTTPException(status_code=400, detail=str(e)) except Exception as e: logger.error(f"Error injecting SCTE35 marker on port {port_index}: {e}") @@ -282,12 +259,6 @@ async def inject_scte35_marker_for_port( async def get_scte35_history( manager: SCTE35Manager = Depends(get_scte35_manager), ) -> list[SCTE35Marker]: - """ - Get history of injected SCTE35 markers. - - Returns: - List of SCTE35Marker objects in chronological order - """ try: return manager.get_marker_history() except Exception as e: @@ -300,9 +271,6 @@ async def get_scte35_history_for_port( port_index: int, manager: SCTE35Manager = Depends(get_scte35_manager), ) -> list[SCTE35Marker]: - """ - Get SCTE35 marker history for a specific port. - """ try: return [m for m in manager.get_marker_history() if m.port_index == port_index] except Exception as e: