diff --git a/design/api.md b/design/api.md index 52fbf94..39bbd70 100644 --- a/design/api.md +++ b/design/api.md @@ -296,7 +296,7 @@ arbitrary order. Each recording object has the following properties: Note this may be greater than the requested `endTime90k` if this recording was ongoing at the requested time. * `videoSampleEntryId`: a reference to an entry in the `videoSampleEntries` - map.mp4` URL. + object. * `videoSamples`: the number of samples (aka frames) of video in this recording. * `sampleFileBytes`: the number of bytes of video in this recording. diff --git a/ui/src/List/VideoList.tsx b/ui/src/List/VideoList.tsx index 7c2b1f8..6b45028 100644 --- a/ui/src/List/VideoList.tsx +++ b/ui/src/List/VideoList.tsx @@ -94,8 +94,12 @@ const VideoList = ({ key={r.startId} onClick={() => setActiveRecording([stream, r])} > - {formatTime(r.startTime90k)} - {formatTime(r.endTime90k)} + + {formatTime(Math.max(r.startTime90k, range90k![0]))} + + + {formatTime(Math.min(r.endTime90k, range90k![1]))} + {vse.width}x{vse.height} diff --git a/ui/src/List/index.tsx b/ui/src/List/index.tsx index 3385879..7dce6f9 100644 --- a/ui/src/List/index.tsx +++ b/ui/src/List/index.tsx @@ -153,7 +153,8 @@ const Main = ({ cameras, timeZoneName }: Props) => { src={api.recordingUrl( activeRecording[0].camera.uuid, activeRecording[0].streamType, - activeRecording[1] + activeRecording[1], + range90k! )} /> diff --git a/ui/src/api.ts b/ui/src/api.ts index 80769e7..725bda4 100644 --- a/ui/src/api.ts +++ b/ui/src/api.ts @@ -5,7 +5,9 @@ /** * @file Convenience wrapper around the Moonfire NVR API layer. * - * See design/api.md for a description of the API. + * See design/api.md for a description of the API. Some of the + * documentation is copied into the docstrings here for convenience, but + * that doc is authoritative. * * The functions here return a Typescript discriminating union of status. * This seems convenient for ensuring the caller handles all possibilities. @@ -180,16 +182,72 @@ export async function logout(req: LogoutRequest, init: RequestInit) { }); } +/** + * Represents a range of one or more recordings as in a single array entry of + * GET /api/cameras/<uuid>/<stream>/<recordings>. + */ export interface Recording { + /** id of the first recording in this range. */ startId: number; + + /** + * If present, indicates that recordings startId, endId (inclusive) + * are described here. + */ endId?: number; + + /** + * If this range is not fully committed to the database, the first id that is + * uncommitted. This is significant because it's possible that after a crash + * and restart, this id will refer to a completely different recording. That + * recording will have a different openId. + */ firstUncommitted?: number; + + /** + * If this boolean is true, the recording endId is still being written to. + * Accesses to this id (such as view.mp4) may retrieve more data than + * described here if not bounded by duration. Additionally, if startId == + * endId, the start time of the recording is "unanchored" and may change in + * subsequent accesses. + */ growing?: boolean; + + /** + * Each time Moonfire NVR starts in read-write mode, it is assigned an + * increasing "open id". This field is the open id as of when these + * recordings were written. This can be used to disambiguate ids referring to + * uncommitted recordings. + */ openId: number; + + /** + * start time of the given recording, in the wall time scale. Note this + * may be less than the requested startTime90k if this recording was ongoing + * at the requested time. + */ startTime90k: number; + + /** + * end time of the given recording, in the wall time scale. Note this may be + * greater than the requested endTime90k if this recording was ongoing at the + * requested time. + */ endTime90k: number; + + /** + * a reference to an entry in the videoSampleEntries object. + */ videoSampleEntryId: number; + + /** + * the number of samples (aka frames) of video in this recording. + */ videoSamples: number; + + /** + * the number of bytes of video in this recording. + */ sampleFileBytes: number; } @@ -247,17 +305,38 @@ export async function recordings(req: RecordingsRequest, init: RequestInit) { return await json(url, init); } +/** + * Returns a URL to a .mp4 of the given recording. + * If trimToRange90k is supplied, the .mp4 will include + * only the portion of the recording which overlaps with the given half-open + * interval. + */ export function recordingUrl( cameraUuid: string, stream: StreamType, - r: Recording + r: Recording, + trimToRange90k?: [number, number] ): string { let s = `${r.startId}`; if (r.endId !== undefined) { s += `-${r.endId}`; } if (r.firstUncommitted !== undefined) { - s += `@${r.openId}`; + s += `@${r.openId}`; // disambiguate. + } + let rel = ""; + if (trimToRange90k !== undefined && r.startTime90k < trimToRange90k[0]) { + rel += trimToRange90k[0] - r.startTime90k; + } + rel += "-"; + if (trimToRange90k !== undefined && r.endTime90k > trimToRange90k[1]) { + rel += trimToRange90k[1] - r.startTime90k; + } else if (r.growing) { + // View just the portion described by recording, not anything added later. + rel += r.endTime90k - r.startTime90k; + } + if (rel !== "-") { + s += "." + rel; } return withQuery(`/api/cameras/${cameraUuid}/${stream}/view.mp4`, { s,