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 ..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: