Create snapshot functionality from History view#22639
Create snapshot functionality from History view#22639nrlcode wants to merge 4 commits intoblakeblackshear:devfrom
Conversation
| setFullResolution: React.Dispatch<React.SetStateAction<VideoResolutionType>>; | ||
| toggleFullscreen: () => void; | ||
| containerRef?: React.MutableRefObject<HTMLDivElement | null>; | ||
| supportsSnapshot?: boolean; |
There was a problem hiding this comment.
supportsSnapshot is a pure pass-through prop that adds noise here. Since it ends up as snapshot: supportsSnapshot in the features object inside HlsVideoPlayer, the cleaner approach is just to infer it from whether onSnapshot is provided - the same way plusUpload is already gated on onUploadFrame existing. That would eliminate this prop chain entirely.
I would just mirror what we already do with onUploadFrame.
There was a problem hiding this comment.
I removed the supportsSnapshot prop chain entirely and now infer snapshot support directly from onSnapshot presence in HlsVideoPlayer (same pattern as other callback-gated features like upload).
| const currentTime = videoRef.current?.currentTime; | ||
|
|
||
| if (!currentTime) { | ||
| if (currentTime == undefined) { |
There was a problem hiding this comment.
This change here seems unrelated to snapshots. Am I missing something?
There was a problem hiding this comment.
Agreed. That change was unrelated to snapshot scope. I reverted the logic back to its previous behavior so this PR only carries snapshot-specific changes.
| const frameTime = getVideoTime(); | ||
|
|
||
| if (frameTime && onUploadFrame) { | ||
| if (frameTime != undefined && onUploadFrame) { |
There was a problem hiding this comment.
This change also seems unrelated to snapshots.
There was a problem hiding this comment.
Reverted this to the previous check to keep scope tight and avoid unrelated behavioral changes in this snapshot PR.
| const frameTime = getVideoTime(); | ||
|
|
||
| if (frameTime) { | ||
| if (frameTime != undefined) { |
There was a problem hiding this comment.
This change also seems unrelated to snapshots.
There was a problem hiding this comment.
Also reverted for the same reason
web/src/utils/snapshotUtil.ts
Outdated
| }); | ||
|
|
||
| const safeTimestamp = | ||
| timestamp === "Invalid time" |
There was a problem hiding this comment.
formatUnixTimestampToDateTime returns the string "Invalid time" on bad input, and this is meant for UI display. But you are pattern-matching on that magic string as error handling. If that return value ever changes, this silently breaks. I would just validate the input before calling the formatter instead, or don't use a formatter at all (since Live view's snapshot download has no formatting, either).
There was a problem hiding this comment.
Sounds good. I removed the check and now validate the input timestamp before calling the formatter (finite-number validation). If invalid/missing, it falls back to current time; if valid, it uses the playback timestamp. This avoids coupling to formatter UI text.
|
I pushed a cleanup commit that narrows this PR to snapshot functionality only: removed supportsSnapshot pass-through, restored unrelated time guard changes, and replaced "Invalid time" string matching with explicit input validation in snapshot filename generation. |
| return currentTime + inpointOffset; | ||
| }, [videoRef, inpointOffset]); | ||
|
|
||
| const handleSnapshot = useCallback(async () => { |
There was a problem hiding this comment.
We'll need some kind of loading guard that disables the button while the snapshot is being downloaded (like is done in LiveCameraView's snapshot handler), because a user could spam click the snapshot button, which in most cases will cause the browser to just download a bunch of black frames.
There was a problem hiding this comment.
Added in the guarding features!
| toast.error(t("snapshot.captureFailed", { ns: "views/live" }), { | ||
| position: "top-center", | ||
| }); | ||
| if (isSnapshotLoading) { |
There was a problem hiding this comment.
There's some unnecessary redundancy here, I would remove this guard from the callback entirely and let VideoControls handle it. The onClick in VideoControls already has if (snapshotLoading) return, so the callback never fires while loading. That would match the existing pattern of other things here where the component prevents re-triggering, not the handler.
| ? "cursor-not-allowed opacity-50" | ||
| : "cursor-pointer", | ||
| )} | ||
| title={snapshotTitle} |
There was a problem hiding this comment.
I'd remove this. None of the other buttons here have a title.
Please read the contributing guidelines before submitting a PR.
Proposed change
This PR adds snapshot support to the History/Recording player flow.
videoelement), not from the live snapshot endpoint.This improves user workflows when reviewing recordings and exporting a frame from a specific playback moment.
I tried to re-use the functionality from Frigate+ and keep the scope of changes minimal.
VideoControls.tsx: adds snapshot button API/UI.HlsVideoPlayer.tsx: wires button to capture/download in History player.snapshotUtil.ts: playback-timestamp + timezone filename and capture-from-current-video support.DynamicVideoPlayer.tsx: passessupportsSnapshotdown.RecordingView.tsx: enables snapshot for History.Type of change
Additional information
For new features
AI disclosure
AI tool(s) used (e.g., Claude, Copilot, ChatGPT, Cursor):
ChatGPT/Codex
How AI was used (e.g., code generation, code review, debugging, documentation):
Used for implementation assistance, debugging, and refinement of targeted frontend changes.
Extent of AI involvement (e.g., generated entire implementation, assisted with specific functions, suggested fixes):
Assisted with specific functions and targeted fixes. Final scope and selected commits were manually controlled.
Human oversight: Describe what manual review, testing, and validation you performed on the AI-generated portions.
I manually reviewed all changed files, validated commit scope against
origin/dev, and tested snapshot behavior in the History viewer. I also ran frontend checks used during iteration (lint/build flow) and validated behavior in containerized test deployment.Checklist
enlocale.ruff format frigate) [N/A]