Web console: archive, snapshots, camera settings and two-way audio#60
Merged
Conversation
- GET /api/v1/cameras/{id}/snapshot returns a fresh JPEG, preferring
Majestic's own /image.jpg and falling back to one short ffmpeg pull for
cameras that don't have it
- camera button on every tile and on the single-camera page; the modal
fetches as a blob so a camera that doesn't answer says so instead of
showing a broken image, and the download keeps a dated filename
- guarded like live video: ViewLive plus the caller's camera subset
- nothing is stored server-side yet; the shared, indexed snapshot library
the desktop keeps is a separate slice
- GET /api/v1/recordings lists what the desktop head recorded, scoped to the caller's cameras; /stream serves the file with range support so the browser's own <video> seeking just works - only files indexed in the database can be served: the path comes from the row, so no request can reach an arbitrary file - listing and playback need ViewArchive (the flag finally has a use); deleting is Manage, and drops the row before the file so a failed delete leaves a leftover rather than a broken entry - H.265 recordings are flagged unplayable in the DTO and offered as a download instead of a dead player - recording itself is still started from the desktop app
- ffmpeg no longer drops audio (-an); it transcodes whatever the camera sends to AAC, because G.711/PCM/Opus don't play in MSE - audio rides in the existing shared stream rather than a second session: 64 kbps is about a percent of the video, so carrying it for everyone is cheaper than a second ffmpeg for the one who listens — and nobody's picture restarts when they unmute - the codec string is built from the init segment's actual boxes, so a camera without sound is still declared video-only and keeps working - per-tile listen toggle, shown only when the stream really has audio; tiles stay muted by default, which is also what autoplay requires
- new components/Icon.tsx: inline SVG in the Lucide style the desktop app already uses, inheriting currentColor and sized by the caller - emoji rendered in a system font that differs per platform, ignored the theme colour and didn't scale with their button; the same went for the fullwidth plus and friends - labels in strings.ts no longer carry a glyph — the icon goes in the markup next to the text
- WebRecorder spawns one ffmpeg per camera writing the stream to disk with -c copy, and indexes the row in the same table the desktop head uses; files land in the same <recordings>/<camera>/<date>/ layout, so one archive stays navigable by hand and both heads see each other's clips - stopping sends "q" on ffmpeg's stdin instead of killing it: an MP4 needs its trailer, and a killed process leaves a file nothing will play - a camera that delivered nothing produces no archive entry — the empty file and its row are dropped and the caller gets nothing_recorded, which the camera page reports instead of silently flipping the button - recording is Manage: it writes to the host's disk until someone stops it
- /api/v1/recordings takes from/to; a new /recordings/calendar returns just timestamps and sizes for a range - days are grouped in the BROWSER, not on the server: which day a clip belongs to depends on the viewer's time zone, and a web client may sit in a different one than the machine that recorded it (Core's CalendarActivity solves the same problem for the desktop, where the two are always the same host) - month grid shades each day by how busy it was; clicking one filters the list, clicking again or "whole archive" clears it
- /api/v1/recordings answers with a page ({total, offset, limit, items})
instead of a silently capped array — the old 200-row ceiling hid the
rest of the archive with nothing in the response to say so
- new /recordings/{id}/export cuts start..end with ffmpeg and streams the
result as a download; nothing is stored, since keeping a copy would grow
the archive behind the person's back
- two cut modes, same as the desktop exporter: a fast stream copy that can
only begin on a keyframe, or an exact-frame re-encode on request
- export needs the Export permission, which until now gated nothing
- player gets in/out marks taken from the playhead rather than typed
timecodes
Recording: - ffmpeg now writes through the segment muxer (10 minutes by default, OPENIPC_WEB_SEGMENT_SECONDS to change): a night in one file is painful to download and to seek, and a corrupt tail would take the whole night with it. Each part is closed on its own, so finished segments survive a server that dies mid-recording. - on stop the parts are reconciled into archive rows; segment start times come from the segment index rather than file mtime, which is when the writing finished, not when the footage began. Empty parts (a stop that lands on a boundary, or a camera that gave nothing) are dropped. Navigation: - the sidebar had grown to seven entries, which crowded the mobile tab strip and mixed daily viewing with once-a-month configuration. Now: Grid, Cameras, Archive, Settings — with groups, users and system as tabs under Settings, and the old paths redirecting. - discovery moves next to "Add camera" on the Cameras page, since that is what it is: another way to add one. - typing a URL you may not use (users, discovery) now lands on a page you can, instead of a screen stuck loading behind a 403.
- Grid-wide stills mode: periodic snapshots instead of live streams, so a big wall costs one HTTP GET per camera per interval instead of an ffmpeg session each; interval 2-60s, remembered per browser. - Digital zoom in a tile (wheel, drag, buttons) on the tile's own slot, never on the pooled <video>, so the zoom can't follow a camera into another cell. - Kiosk: chrome-free full-viewport wall with optional page cycling; uses the Fullscreen API when granted and falls back to a fixed overlay when it isn't. - Fix a pre-existing leak: the live WebSocket was never read, so a viewer's close frame went unnoticed and the shared ffmpeg outlived its last viewer. Verified 8 streams -> 0 after the last tile releases.
- Api/MajesticApi.cs: read the parsed config, apply field edits, post raw config.json, set day/night, read /metrics. Schema-driven like the desktop editor (IMajesticConfigSchema), so it renders whatever knobs the live firmware exposes instead of a fixed form. - Manage-gated including reads: config.json carries the camera's own logins and often the Wi-Fi PSK, which is not what ViewLive grants. - Edits are read-modify-write against a fresh copy of the camera's config and travel as (path, value), so a stale tab can't revert someone else's change. - ISP fields get their own panel: Majestic re-reads those live, so tuning the picture doesn't restart the encoder. Restart-y fields are flagged in the UI. - Probe once when the IsMajestic flag is unset, like the desktop does — a hand-added camera would otherwise never show the panel. - A camera timeout no longer answers 204: our own call deadline was being read as "the viewer left", so silence looked like success. Now 504.
- Api/SnapshotLibraryApi.cs: keep a still (capture, write, index), list it, serve it, delete it. Storage matches SnapshotService exactly — same folder layout, same .thumbs naming, same table — so a still taken from a phone shows up in the desktop browser and vice versa. Verified against the two snapshots the desktop wrote in June. - Thumbnails via ffmpeg, which this head already shells out to, instead of pulling the Video layer's Skia thumbnailer into the server. - Pixel dimensions read from the JPEG frame header: the desktop stores them to prove a capture was full-resolution, and no imaging library is needed to get two bytes. - Saving needs ViewArchive (it adds to an archive the caller can already read), deleting needs Manage — same split as recordings. - Archive page grows tabs (recordings / snapshots) rather than a fifth nav entry: the sidebar doubles as the phone tab bar.
- Api/TalkApi.cs: probe the camera's backchannel, then bridge a WebSocket of 16-bit PCM into it — G.711 encoding and RTP packetizing stay server-side, in the same Core code the desktop talks with. - PushToTalkController is not used: it owns a microphone on the machine running it, and here the microphone is three network hops away. Same reason PtzApi skips PtzController. - One talker per camera. A speaker is exclusive; two streams interleaved into one RTP session would come out as noise, so a second attempt is refused. - New Talk permission rather than reusing Ptz/Manage: it is the one action that reaches people standing in front of the camera. - The client follows the negotiated format (an AudioContext at the camera's rate resamples the mic), holds to talk, and releases the camera when the route changes. - Microphones need a secure context, so over plain HTTP the button is replaced by a note saying so instead of failing at the press. - Fix: ViewArchive was missing from the users page, which quietly made the archive ungrantable to anyone created from the web console.
- Latest stable release and total download count, next to the build badge. - Live from the GitHub API rather than written out, so the numbers can't go stale between releases.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Builds on the React SPA web console and closes most of the gap with the desktop head. Everything here is server-side + SPA only — no change to the video pipeline, the desktop UI, or the mobile heads.
Live grid
2–60 s. A 25-cell wall costs one HTTP GET per camera per interval instead of an
ffmpeg session each; the camera encodes the JPEG itself when it runs Majestic.
transform lives on the tile's slot, never on the pooled
<video>, so zoomcan't follow a camera into another cell.
Uses the Fullscreen API when granted and falls back to a fixed overlay when it
isn't (embedded/iframed pages refuse it outright).
watched from a control-room screen and from someone's laptop.
Archive
the desktop head uses (
-c copy, segmented,qon stdin so the MP4 trailer iswritten).
-c copyor precisere-encode), streamed as a download without leaving a copy on the server.
Snapshot library
.thumbs/<id>.jpgnaming and the same table as the desktop'sSnapshotService, so a still taken from a phone shows up in the desktopgallery and vice versa.
rather than pulling the Video layer's Skia thumbnailer into the server.
needed to record what the desktop records.
Camera settings (Majestic)
config.json, edit fields, post raw JSON, set day/night, read/metrics. Schema-driven like the desktop editor (IMajesticConfigSchema), soit renders whatever knobs the firmware actually exposes instead of a fixed form
that drifts between builds.
picture does not restart the encoder. Fields that do restart it are flagged.
(path, value)and are applied against a freshly read copy ofthe camera's config, so a stale tab can't revert someone else's change.
Two-way audio
the video bitrate) rather than a second session, so nobody's picture restarts
when they unmute.
encoding and RTP packetizing happen server-side in the same Core code the
desktop talks with. One talker per camera — a speaker is exclusive.
Permissions
Talkflag, separate fromPtz/Manage: it is the one action thatreaches people standing in front of the camera.
ViewArchivenow gates the archive and the snapshot library;Exportgatesclip export. Both flags previously existed without gating anything.
Manage-only including reads —config.jsoncarriesthe camera's own logins and, on many builds, the Wi-Fi PSK.
Fixes
the shared ffmpeg process outlived its last viewer. Verified: 8 streams → 0
after the last tile releases.
own call deadline surfaced as the same cancellation as an abandoned request.
Now 504.
t=90s with
currentTime=0and sat paused forever. A watchdog now jumps to thelive edge.
ViewArchivewas missing from the users page, which quietly made the archiveungrantable to anyone created from the web console.
Related
Type
Checklist
TreatWarningsAsErrors=true).dotnet test); new Core logic has unit tests.AppreferencesCoreonly (Infrastructure / Video / Devices wired via DI in a head).Platforms tested
Screenshots / notes