expose replay state on status endpoint and return 202 from start

This commit is contained in:
Josh Hawkins 2026-05-02 23:06:29 -05:00
parent e187446d04
commit 6f43ed41f2
2 changed files with 92 additions and 17 deletions

View File

@ -29,12 +29,16 @@ class DebugReplayStartResponse(BaseModel):
success: bool
replay_camera: str
state: str
class DebugReplayStatusResponse(BaseModel):
"""Response for debug replay status."""
active: bool
state: str
progress_percent: float | None = None
error_message: str | None = None
replay_camera: str | None = None
source_camera: str | None = None
start_time: float | None = None
@ -53,10 +57,12 @@ class DebugReplayStopResponse(BaseModel):
response_model=DebugReplayStartResponse,
dependencies=[Depends(require_role(["admin"]))],
summary="Start debug replay",
description="Start a debug replay session from camera recordings.",
description="Start a debug replay session from camera recordings. Returns "
"immediately while clip generation runs asynchronously; poll "
"/debug_replay/status to track progress.",
)
async def start_debug_replay(request: Request, body: DebugReplayStartBody):
"""Start a debug replay session."""
"""Start a debug replay session asynchronously."""
replay_manager = request.app.replay_manager
if replay_manager.active:
@ -77,28 +83,23 @@ async def start_debug_replay(request: Request, body: DebugReplayStartBody):
frigate_config=request.app.frigate_config,
config_publisher=request.app.config_publisher,
)
except ValueError:
logger.exception("Invalid parameters for debug replay start request")
except ValueError as exc:
logger.info("Rejected debug replay start request: %s", exc)
return JSONResponse(
content={
"success": False,
"message": "Invalid debug replay request parameters",
"message": str(exc),
},
status_code=400,
)
except RuntimeError:
logger.exception("Error while starting debug replay session")
return JSONResponse(
content={
"success": False,
"message": "An internal error occurred while starting debug replay",
},
status_code=500,
)
return DebugReplayStartResponse(
success=True,
replay_camera=replay_camera,
return JSONResponse(
content={
"success": True,
"replay_camera": replay_camera,
"state": replay_manager.state.value,
},
status_code=202,
)
@ -132,6 +133,9 @@ def get_debug_replay_status(request: Request):
return DebugReplayStatusResponse(
active=replay_manager.active,
state=replay_manager.state.value,
progress_percent=replay_manager.progress_percent,
error_message=replay_manager.error_message,
replay_camera=replay_camera,
source_camera=replay_manager.source_camera,
start_time=replay_manager.start_ts,

View File

@ -0,0 +1,71 @@
"""Tests for /debug_replay API endpoints."""
from unittest.mock import patch
from frigate.debug_replay import ReplayState
from frigate.models import Event, Recordings, ReviewSegment
from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp
class TestDebugReplayAPI(BaseTestHttp):
def setUp(self):
super().setUp([Event, Recordings, ReviewSegment])
self.app = self.create_app()
def test_start_returns_202_with_state_preparing_clip(self):
with patch(
"frigate.debug_replay.DebugReplayManager.start",
return_value="_replay_front",
):
with patch.object(
type(self.app.replay_manager),
"state",
new_callable=lambda: property(lambda s: ReplayState.preparing_clip),
):
with AuthTestClient(self.app) as client:
resp = client.post(
"/debug_replay/start",
json={
"camera": "front",
"start_time": 100,
"end_time": 200,
},
headers={"remote-user": "admin", "remote-role": "admin"},
)
self.assertEqual(resp.status_code, 202)
body = resp.json()
self.assertTrue(body["success"])
self.assertEqual(body["replay_camera"], "_replay_front")
self.assertEqual(body["state"], "preparing_clip")
def test_status_returns_state_and_error_message(self):
manager = self.app.replay_manager
manager._set_state(ReplayState.error, error_message="ffmpeg failed: boom")
with AuthTestClient(self.app) as client:
resp = client.get(
"/debug_replay/status",
headers={"remote-user": "admin", "remote-role": "admin"},
)
self.assertEqual(resp.status_code, 200)
body = resp.json()
self.assertEqual(body["state"], "error")
self.assertEqual(body["error_message"], "ffmpeg failed: boom")
self.assertIsNone(body["progress_percent"])
self.assertFalse(body["active"])
def test_status_returns_progress_percent_during_preparing_clip(self):
manager = self.app.replay_manager
manager._set_state(ReplayState.preparing_clip)
manager.progress_percent = 37.5
with AuthTestClient(self.app) as client:
resp = client.get(
"/debug_replay/status",
headers={"remote-user": "admin", "remote-role": "admin"},
)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.json()["progress_percent"], 37.5)