new vod endpoint for clips to set discontinuity

ensure tracking-detail playlists emit #EXT-X-DISCONTINUITY (avoids fMP4 timestamp rewrites and playback stalls) while leaving standard recordings behavior unchanged
This commit is contained in:
Josh Hawkins 2025-12-07 12:41:52 -06:00
parent d0febd0460
commit bccd45c052

View File

@ -837,7 +837,19 @@ async def recording_clip(
dependencies=[Depends(require_camera_access)], dependencies=[Depends(require_camera_access)],
description="Returns an HLS playlist for the specified timestamp-range on the specified camera. Append /master.m3u8 or /index.m3u8 for HLS playback.", description="Returns an HLS playlist for the specified timestamp-range on the specified camera. Append /master.m3u8 or /index.m3u8 for HLS playback.",
) )
async def vod_ts(camera_name: str, start_ts: float, end_ts: float): async def vod_ts(
camera_name: str,
start_ts: float,
end_ts: float,
force_discontinuity: bool = False,
):
logger.debug(
"VOD: Generating VOD for %s from %s to %s with force_discontinuity=%s",
camera_name,
start_ts,
end_ts,
force_discontinuity,
)
recordings = ( recordings = (
Recordings.select( Recordings.select(
Recordings.path, Recordings.path,
@ -862,6 +874,14 @@ async def vod_ts(camera_name: str, start_ts: float, end_ts: float):
recording: Recordings recording: Recordings
for recording in recordings: for recording in recordings:
logger.debug(
"VOD: processing recording: %s start=%s end=%s duration=%s",
recording.path,
recording.start_time,
recording.end_time,
recording.duration,
)
clip = {"type": "source", "path": recording.path} clip = {"type": "source", "path": recording.path}
duration = int(recording.duration * 1000) duration = int(recording.duration * 1000)
@ -870,6 +890,11 @@ async def vod_ts(camera_name: str, start_ts: float, end_ts: float):
inpoint = int((start_ts - recording.start_time) * 1000) inpoint = int((start_ts - recording.start_time) * 1000)
clip["clipFrom"] = inpoint clip["clipFrom"] = inpoint
duration -= inpoint duration -= inpoint
logger.debug(
"VOD: applied clipFrom %sms to %s",
inpoint,
recording.path,
)
# adjust end if recording.end_time is after end_ts # adjust end if recording.end_time is after end_ts
if recording.end_time > end_ts: if recording.end_time > end_ts:
@ -877,12 +902,23 @@ async def vod_ts(camera_name: str, start_ts: float, end_ts: float):
if duration < min_duration_ms: if duration < min_duration_ms:
# skip if the clip has no valid duration (too short to contain frames) # skip if the clip has no valid duration (too short to contain frames)
logger.debug(
"VOD: skipping recording %s - resulting duration %sms too short",
recording.path,
duration,
)
continue continue
if min_duration_ms <= duration < max_duration_ms: if min_duration_ms <= duration < max_duration_ms:
clip["keyFrameDurations"] = [duration] clip["keyFrameDurations"] = [duration]
clips.append(clip) clips.append(clip)
durations.append(duration) durations.append(duration)
logger.debug(
"VOD: added clip %s duration_ms=%s clipFrom=%s",
recording.path,
duration,
clip.get("clipFrom"),
)
else: else:
logger.warning(f"Recording clip is missing or empty: {recording.path}") logger.warning(f"Recording clip is missing or empty: {recording.path}")
@ -902,7 +938,7 @@ async def vod_ts(camera_name: str, start_ts: float, end_ts: float):
return JSONResponse( return JSONResponse(
content={ content={
"cache": hour_ago.timestamp() > start_ts, "cache": hour_ago.timestamp() > start_ts,
"discontinuity": False, "discontinuity": force_discontinuity,
"consistentSequenceMediaInfo": True, "consistentSequenceMediaInfo": True,
"durations": durations, "durations": durations,
"segment_duration": max(durations), "segment_duration": max(durations),
@ -986,6 +1022,19 @@ async def vod_event(
return vod_response return vod_response
@router.get(
"/vod/clip/{camera_name}/start/{start_ts}/end/{end_ts}",
dependencies=[Depends(require_camera_access)],
description="Returns an HLS playlist for a timestamp range with HLS discontinuity enabled. Append /master.m3u8 or /index.m3u8 for HLS playback.",
)
async def vod_clip(
camera_name: str,
start_ts: float,
end_ts: float,
):
return await vod_ts(camera_name, start_ts, end_ts, force_discontinuity=True)
@router.get( @router.get(
"/events/{event_id}/snapshot.jpg", "/events/{event_id}/snapshot.jpg",
description="Returns a snapshot image for the specified object id. NOTE: The query params only take affect while the event is in-progress. Once the event has ended the snapshot configuration is used.", description="Returns a snapshot image for the specified object id. NOTE: The query params only take affect while the event is in-progress. Once the event has ended the snapshot configuration is used.",