feat: wire HLS manager into routes, add preview start/stop endpoints

This commit is contained in:
Zac Gaetano 2026-04-14 10:12:30 -04:00
parent 36a1e4a80e
commit b936647abb

View file

@ -8,6 +8,7 @@ from pydantic import BaseModel, Field
from ..models import RecorderConfig, PortStatus, SCTE35Marker from ..models import RecorderConfig, PortStatus, SCTE35Marker
from ..recorders.recorder import RecorderManager from ..recorders.recorder import RecorderManager
from ..recorders.scte35 import SCTE35Manager from ..recorders.scte35 import SCTE35Manager
from ..utils.hls import HLSPreviewManager
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -31,22 +32,27 @@ router = APIRouter(prefix="/api")
# These will be set by main.py during startup # These will be set by main.py during startup
recorder_manager: RecorderManager | None = None recorder_manager: RecorderManager | None = None
scte35_manager: SCTE35Manager | None = None scte35_manager: SCTE35Manager | None = None
hls_manager: HLSPreviewManager | None = None
def get_recorder_manager() -> RecorderManager: def get_recorder_manager() -> RecorderManager:
"""Dependency to get the RecorderManager instance."""
if recorder_manager is None: if recorder_manager is None:
raise HTTPException(status_code=503, detail="Recorder not initialized") raise HTTPException(status_code=503, detail="Recorder not initialized")
return recorder_manager return recorder_manager
def get_scte35_manager() -> SCTE35Manager: def get_scte35_manager() -> SCTE35Manager:
"""Dependency to get the SCTE35Manager instance."""
if scte35_manager is None: if scte35_manager is None:
raise HTTPException(status_code=503, detail="SCTE35 manager not initialized") raise HTTPException(status_code=503, detail="SCTE35 manager not initialized")
return scte35_manager 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 # Health and Status Endpoints
# ============================================================================ # ============================================================================
@ -54,16 +60,9 @@ def get_scte35_manager() -> SCTE35Manager:
@router.get("/health") @router.get("/health")
async def health(manager: RecorderManager = Depends(get_recorder_manager)) -> dict[str, Any]: 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: try:
all_status = manager.get_all_status() all_status = manager.get_all_status()
recording_count = sum(1 for status in all_status if status.is_recording) recording_count = sum(1 for status in all_status if status.is_recording)
return { return {
"status": "ok", "status": "ok",
"ports": len(all_status), "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]) @router.get("/ports", response_model=list[PortStatus])
async def list_ports(manager: RecorderManager = Depends(get_recorder_manager)) -> 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: try:
return manager.get_all_status() return manager.get_all_status()
except Exception as e: except Exception as e:
@ -99,22 +92,9 @@ async def get_port(
port_index: int, port_index: int,
manager: RecorderManager = Depends(get_recorder_manager), manager: RecorderManager = Depends(get_recorder_manager),
) -> PortStatus: ) -> 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: try:
return manager.get_status(port_index) return manager.get_status(port_index)
except ValueError as e: 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") raise HTTPException(status_code=404, detail=f"Port {port_index} not found")
except Exception as e: except Exception as e:
logger.error(f"Error getting port {port_index}: {e}") logger.error(f"Error getting port {port_index}: {e}")
@ -132,32 +112,19 @@ async def start_recording(
config: RecorderConfig, config: RecorderConfig,
manager: RecorderManager = Depends(get_recorder_manager), manager: RecorderManager = Depends(get_recorder_manager),
) -> dict[str, Any]: ) -> 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: try:
await manager.start_recording(port_index, config) 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}") logger.info(f"Started recording on port {port_index}")
return { return {"message": "Recording started", "port": port_index}
"message": "Recording started",
"port": port_index,
}
except ValueError as e: except ValueError as e:
logger.warning(f"Invalid port {port_index}: {e}")
raise HTTPException(status_code=404, detail=f"Port {port_index} not found") raise HTTPException(status_code=404, detail=f"Port {port_index} not found")
except RuntimeError as e: except RuntimeError as e:
logger.warning(f"Cannot start recording on port {port_index}: {e}")
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
except Exception as e: except Exception as e:
logger.error(f"Error starting recording on port {port_index}: {e}") logger.error(f"Error starting recording on port {port_index}: {e}")
@ -169,33 +136,71 @@ async def stop_recording(
port_index: int, port_index: int,
manager: RecorderManager = Depends(get_recorder_manager), manager: RecorderManager = Depends(get_recorder_manager),
) -> dict[str, Any]: ) -> 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: try:
await manager.stop_recording(port_index) 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}") logger.info(f"Stopped recording on port {port_index}")
return { return {"message": "Recording stopped", "port": port_index}
"message": "Recording stopped",
"port": port_index,
}
except ValueError as e: except ValueError as e:
logger.warning(f"Invalid port {port_index}: {e}")
raise HTTPException(status_code=404, detail=f"Port {port_index} not found") raise HTTPException(status_code=404, detail=f"Port {port_index} not found")
except Exception as e: except Exception as e:
logger.error(f"Error stopping recording on port {port_index}: {e}") logger.error(f"Error stopping recording on port {port_index}: {e}")
raise HTTPException(status_code=500, detail="Internal server error") 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 # SCTE35 Ad Break Endpoints
# ============================================================================ # ============================================================================
@ -206,18 +211,6 @@ async def inject_scte35_marker(
request: SCTE35InjectionRequest, request: SCTE35InjectionRequest,
manager: SCTE35Manager = Depends(get_scte35_manager), manager: SCTE35Manager = Depends(get_scte35_manager),
) -> SCTE35Marker: ) -> 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: try:
marker = await manager.inject_marker( marker = await manager.inject_marker(
event_id=request.event_id, event_id=request.event_id,
@ -228,12 +221,9 @@ async def inject_scte35_marker(
port_index=None, port_index=None,
srt_destination_url=request.srt_destination_url, srt_destination_url=request.srt_destination_url,
) )
logger.info(f"Injected global SCTE35 marker event_id={request.event_id}") logger.info(f"Injected global SCTE35 marker event_id={request.event_id}")
return marker return marker
except ValueError as e: except ValueError as e:
logger.warning(f"Invalid SCTE35 parameters: {e}")
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
except Exception as e: except Exception as e:
logger.error(f"Error injecting SCTE35 marker: {e}") logger.error(f"Error injecting SCTE35 marker: {e}")
@ -246,16 +236,6 @@ async def inject_scte35_marker_for_port(
request: SCTE35InjectionRequest, request: SCTE35InjectionRequest,
manager: SCTE35Manager = Depends(get_scte35_manager), manager: SCTE35Manager = Depends(get_scte35_manager),
) -> SCTE35Marker: ) -> 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: try:
marker = await manager.inject_marker( marker = await manager.inject_marker(
event_id=request.event_id, event_id=request.event_id,
@ -266,12 +246,9 @@ async def inject_scte35_marker_for_port(
port_index=port_index, port_index=port_index,
srt_destination_url=request.srt_destination_url, srt_destination_url=request.srt_destination_url,
) )
logger.info(f"Injected SCTE35 marker event_id={request.event_id} on port {port_index}") logger.info(f"Injected SCTE35 marker event_id={request.event_id} on port {port_index}")
return marker return marker
except ValueError as e: except ValueError as e:
logger.warning(f"Invalid SCTE35 parameters: {e}")
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
except Exception as e: except Exception as e:
logger.error(f"Error injecting SCTE35 marker on port {port_index}: {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( async def get_scte35_history(
manager: SCTE35Manager = Depends(get_scte35_manager), manager: SCTE35Manager = Depends(get_scte35_manager),
) -> list[SCTE35Marker]: ) -> list[SCTE35Marker]:
"""
Get history of injected SCTE35 markers.
Returns:
List of SCTE35Marker objects in chronological order
"""
try: try:
return manager.get_marker_history() return manager.get_marker_history()
except Exception as e: except Exception as e:
@ -300,9 +271,6 @@ async def get_scte35_history_for_port(
port_index: int, port_index: int,
manager: SCTE35Manager = Depends(get_scte35_manager), manager: SCTE35Manager = Depends(get_scte35_manager),
) -> list[SCTE35Marker]: ) -> list[SCTE35Marker]:
"""
Get SCTE35 marker history for a specific port.
"""
try: try:
return [m for m in manager.get_marker_history() if m.port_index == port_index] return [m for m in manager.get_marker_history() if m.port_index == port_index]
except Exception as e: except Exception as e: