feat: archive pro (player, timeline, calendar, clip export)#24
Merged
Conversation
- IPlaybackSession/IPlaybackEngine/PlaybackOptions + FfmpegPlaybackSession (PTS-paced, keyframe seek, software decode) - RecordingPlayerPage VM/View, Play from list, nav + DI wiring - IMediaProbe/FfmpegMediaProbe: exact avformat duration, pre-probed into player
- TimelineViewport/TimelineTicks pure mapping (cursor-anchored zoom, adaptive labels) - TimelineControl: DrawingContext render, segments + motion/detection markers, drag-pan, click-to-seek, marker tooltips, per-column bucketing - player VM loads markers from events, playhead + seek-by-time wired into page
- CalendarActivity aggregation (per-day, DST-aware) + ArchiveCalendar VM/View - recordings list filters by selected day; intensity-shaded month grid - player side panel lists motion/detection events, type filter, click-to-seek
- IClipExporter + FfmpegSubprocessClipExporter (-c copy fast, precise re-encode), ClipBounds keyframe math - timeline in/out selection with draggable handles - player export bar: set in/out, precise toggle, progress, save dialog
…hase 16 G,H) - recorder drops +faststart (defeats crash-survivable fragmented MP4); aligns to frag_keyframe+empty_moov+default_base_moof - unit tests: TimelineViewport mapping/zoom/clamp, TimelineTicks, CalendarActivity aggregation, ClipBounds keyframe math
- LibavformatClipExporter: in-process libav stream-copy cut (seek-to-keyframe + trim, muxer rebases ts to zero). Android/iOS can't spawn ffmpeg, so the subprocess exporter only ran on desktop — export was broken on mobile. - IClipExporter registered per head (subprocess on desktop, libav on Android/iOS), same split as IRecorder; removed from SharedComposition. - TimelineControl: pinch-to-zoom (cursor/midpoint-anchored) so touch devices can zoom the timeline — the wheel handler had no touch equivalent.
- Mobile clip export hands the file to the native share sheet (no visible save path on phone); desktop unchanged (reveal-in-folder via IShareService) - Calendar caches per-month aggregates; month navigation reuses them, page re-entry (LoadAsync) clears so new recordings show - Integration test (Skippable on MediaMTX): record -> probe -> export -> probe, asserts real duration + clip length ~ selection - ROADMAP: Phase 16 -> Done
- 86400s over 1000px @80px target = 6912s; smallest nice step >= that is 7200s (2h) - ChooseStep was correct; the test's expected value/comment were off by one rung
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
Turns recordings into a real archive workflow. Phase 6 shipped
recording but no playback at all, so this PR first adds the missing recordings
player (file decode + seek + duration probe), then the archive-pro layer on
top: a canvas timeline with cursor/pinch zoom and event markers, an activity
calendar, a day event list, and in/out clip export. Recording is switched to
fragmented MP4 so files survive a crash. Cross-platform (Win/Lin/Mac/Android/iOS).
Related
What's included
+frag_keyframe+empty_moov+default_base_moof(
FfmpegRecordingSession,LibavformatRecordingSession), so a file is playablemid-record and survives a power loss / crash with no "indexing".
IPlaybackSession/FfmpegPlaybackSession(software decode, PTS-paced, keyframe seek),
IMediaProbe/FfmpegMediaProbefor anexact duration before the first frame. New
RecordingPlayerPage(transport bar +seek), opened from a recording row.
CalendarActivityaggregates recordings + events perlocal day (DST-aware), intensity-shaded month grid; clicking a day filters the
recordings list. Per-month aggregates are cached.
TimelineControl(DrawingContext, markers bucketed perpixel column), cursor-anchored wheel + pinch zoom, drag-pan, click/marker → seek;
adaptive time labels (hours → minutes → seconds). Motion + AI-detection markers.
stream-copy (
-c copy, GOP-accurate) with an optional precise re-encode.type filter (All / Motion / AI), click = seek.
Cross-platform / mobile parity
heads — same stack as live decode.
IRecorder: ffmpeg subprocess on desktop,in-process
LibavformatClipExporteron Android/iOS (the sandbox can't exec abinary). On mobile the exported clip is handed to the native share sheet.
Tests
TimelineViewport(time↔pixel, cursor-anchored zoom, clamping),TimelineTicks(adaptive labels),
CalendarActivity(per-day aggregation, intensity),ClipBounds(keyframe snap, duration).
SkippableFact, gated on the MediaMTX fixture): record → probe → export→ probe; asserts a real probed duration and clip length ≈ the selection.
How to try
Known limitations / deferred
(no in-process re-encode yet).
LibavformatRecordingSessionhas no segment rotation (single file per session) —limits practical Android recording length; a follow-up.
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