Skip to content

Multicam PR4: synced photo capture - #202

Closed
darioalessandro wants to merge 1 commit into
multicam-pr3-directorfrom
multicam-pr4-synced-photo
Closed

Multicam PR4: synced photo capture#202
darioalessandro wants to merge 1 commit into
multicam-pr3-directorfrom
multicam-pr4-synced-photo

Conversation

@darioalessandro

Copy link
Copy Markdown
Member

Stacked on PR3. Schedule-at-timestamp synced stills across the rig, all behind ENABLE_MULTICAM (off).

Wire (additive)

  • ScheduledCapture (action 26): director → camera. Carries the shutter instant in the camera's own SyncClock domain (director already applied the per-camera offset), the shared director-clock anchor (identical across all cameras — the alignment key stamped into each clip), capture id / session id, and the camera's 1-based index.
  • CameraStateResponse gains capture_id_echo for the ack. RemoteCmd.ScheduledCapture / ScheduledCaptureAck, both dispatch switches (incl. the test-local one — the PR2 gotcha), round-trip tests.

Camera side (SessionCoordinator, single-cam untouched)

On ScheduledCapture: ack immediately (or nack if the fire time is > 1s past), then schedule the shutter. The delay runs off the actor and only enqueues FireScheduledCapture, so the capture is pulled by the message pump in order — never racing a state transition (the concern the brief flagged). The photo saves locally as today, additionally stamped with CaptureSyncMetadata: EXIF UserComment JSON + DateTimeOriginal/SubSec, under a shared RS_<sess>_<cap>_cam<k> originalFilename. A stamping failure never costs the user the photo.

Director side (MulticamController)

capturePhoto() picks fireAt = now + 150ms and sends per-camera ScheduledCapture with fireAt + lane.offset to every linked multicam lane; if any offset is missing, it falls back to a plain TakePic fan-out under the same shot id. Aggregate capturingPhoto(captureId, acksRemaining); best-effort — a per-lane 3s ack timeout marks a silent camera failed; returns to monitoring when all lanes resolve. Per-lane outcome (captured/failed) surfaced as a tile badge.

UI

The MulticamView shutter (reusing the 1:1 ShutterButton + activity ring) fires capturePhoto().

Tests

Serialization round-trips; MulticamControllerTests (per-lane offsets → different fire instants, non-multicam lane excluded, ack aggregation, timeout completion, fallback-to-TakePic); camera-side (past-fire nack; valid fire acks immediately then pulls the shutter). Full suite green — 677 tests.

Decisions / deviations

  • Two timestamps on the wire, not one: capture_fire_at_camera_clock_ms (camera domain, for scheduling) and capture_anchor_ms (director domain, the shared alignment key). The camera can't recover the director-domain anchor from the camera-domain fire time (it doesn't know its own offset), so the anchor is sent explicitly. The metadata's per-camera offset/RTT fields are stamped 0 on the camera (diagnostics only; the anchor is the real key).
  • EXIF-level + filename-level stamping, not PHAsset content-metadata threading — matches the brief's "enough for v1" guidance and keeps the save-path churn to one new sibling method.
  • Fallback aggregates via TakePicAck/TakePicResp so the same ack machinery resolves both scheduled and fallback shots.
  • No actor/sleep fight — reported clean; the off-actor-sleep-then-enqueue pattern sidesteps it.

🤖 Generated with Claude Code

Schedule-at-timestamp synced stills across the rig, behind ENABLE_MULTICAM.

Wire (additive, same evolution pattern):
- ScheduledCapture (action 26): director -> camera, carrying the shutter
  instant in the camera's own SyncClock domain (director applied the offset),
  the shared director-clock anchor (the alignment key stamped into each
  clip), capture id / session id, and the camera's 1-based index.
- CameraStateResponse gains capture_id_echo for the ack. RemoteCmd
  ScheduledCapture / ScheduledCaptureAck, both dispatch switches, round-trips.

Camera side (SessionCoordinator, single-cam untouched):
- On ScheduledCapture: ack (or nack if the fire time is > 1s past) immediately,
  then schedule the shutter. The delay runs OFF the actor and only enqueues
  FireScheduledCapture, so the capture is pulled by the message pump in order
  and never races a state transition (the flagged concern). The photo saves
  locally as today, additionally stamped with CaptureSyncMetadata: EXIF
  UserComment JSON + DateTimeOriginal/SubSec, and a shared
  RS_<sess>_<cap>_cam<k> originalFilename so any editor can group and align
  the angles. A stamping failure never costs the user the photo.

Director side (MulticamController):
- capturePhoto(): picks fireAt = now + 150ms, sends per-camera ScheduledCapture
  with fireAt + lane.offset to every linked multicam lane; if any offset is
  missing, falls back to a plain TakePic fan-out under the same shot id.
  Aggregate state capturingPhoto(captureId, acksRemaining); best-effort
  policy — per-lane 3s ack timeout marks a silent camera failed; returns to
  monitoring when all lanes acked/nacked/timed out. Per-lane outcome
  (captured/failed) surfaced to the tile badge.

UI: the MulticamView shutter (reusing the 1:1 ShutterButton + activity ring)
fires capturePhoto(); tiles show a captured/failed badge.

Tests: serialization round-trips; MulticamControllerTests (per-lane offsets
=> different fire instants, non-multicam lane excluded, ack aggregation,
timeout completion, fallback); camera-side (past-fire nack, valid fire acks
immediately then pulls the shutter). Full suite green (677 tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📊 Test Coverage Report

665/677 tests passed · ⏭️ 12 skipped

Metric Coverage
App code (excl. generated + vendored Theater) █████████░░░░░░░░░░░ 44.8% 🔴
Whole target █████████░░░░░░░░░░░ 46.0% 🔴

🎥 Capture & session core

File Coverage Lines
CaptureEngine.swift ██░░░░░░░░░░░░░░░░░░ 10.7% 164/1532 🔴
RecordingPipeline.swift ████████████░░░░░░░░ 59.7% 284/476 🟡
FrameStreamingCoordinator.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/124 🔴
FrameStreamer.swift ██████████████░░░░░░ 69.2% 72/104 🟡
RemoteCmdFlatBuffers.swift ███████████████████░ 95.8% 1267/1322 🟢
📁 Full report — 94 files
File Coverage Lines
CountdownTimer.swift ████████████████████ 100.0% 48/48 🟢
JPEGFrameEncoder.swift ████████████████████ 100.0% 12/12 🟢
CameraPreviewMode.swift ████████████████████ 100.0% 13/13 🟢
AppActivityMonitor.swift ████████████████████ 100.0% 31/31 🟢
AppTheme.swift ████████████████████ 100.0% 33/33 🟢
DebugLog.swift ████████████████████ 100.0% 2/2 🟢
Messages.swift ████████████████████ 100.0% 7/7 🟢
ZoomScale.swift ████████████████████ 100.0% 76/76 🟢
MonitorChrome.swift ████████████████████ 100.0% 56/56 🟢
ClockOffsetEstimator.swift ████████████████████ 100.0% 24/24 🟢
MultipeerMessages.swift ████████████████████ 100.0% 12/12 🟢
StreamingConfig.swift ████████████████████ 100.0% 34/34 🟢
Locked.swift ████████████████████ 100.0% 19/19 🟢
FrameCreditWindow.swift ████████████████████ 100.0% 9/9 🟢
AppDelegate.swift ████████████████████ 100.0% 17/17 🟢
HEICFrameEncoder.swift ████████████████████ 100.0% 17/17 🟢
PeerCompatibility.swift ████████████████████ 100.0% 42/42 🟢
DeviceScannerViewModel.swift ████████████████████ 99.1% 113/114 🟢
FrameSender.swift ████████████████████ 98.4% 187/190 🟢
MulticamViewModel.swift ████████████████████ 98.2% 54/55 🟢
FocusPointMapping.swift ████████████████████ 97.6% 40/41 🟢
RemoteCmds.swift ███████████████████░ 96.1% 324/337 🟢
RemoteCmdFlatBuffers.swift ███████████████████░ 95.8% 1267/1322 🟢
WatchSharedTypes.swift ███████████████████░ 94.0% 236/251 🟢
WatchPreviewStreamer.swift ███████████████████░ 93.6% 44/47 🟢
VP9FrameEncoder.swift ███████████████████░ 92.7% 153/165 🟢
CameraLink.swift ██████████████████░░ 90.9% 10/11 🟢
WelcomeView.swift ██████████████████░░ 89.3% 492/551 🟢
VideoTransferProgressView.swift █████████████████░░░ 84.0% 295/351 🟢
ScannerLobby.swift █████████████████░░░ 83.3% 5/6 🟢
UIImage+gif.swift █████████████████░░░ 83.2% 129/155 🟢
UIAlertController.swift █████████████████░░░ 83.0% 44/53 🟢
MulticamController.swift ████████████████░░░░ 81.2% 381/469 🟢
CameraDeviceDescriptor.swift ████████████████░░░░ 81.0% 34/42 🟢
CameraProgressOverlayView.swift ████████████████░░░░ 78.5% 186/237 🟡
MonitorView.swift ███████████████░░░░░ 76.8% 1178/1534 🟡
FlatBufferSchemas_generated.swift (excluded from app-code metric) ███████████████░░░░░ 76.3% 783/1026 🟡
RolePickerView.swift ███████████████░░░░░ 75.4% 306/406 🟡
RecordingTimer.swift ███████████████░░░░░ 73.5% 61/83 🟡
CameraRecordingTimerView.swift ███████████████░░░░░ 73.4% 127/173 🟡
WelcomeViewModel.swift ███████████████░░░░░ 73.3% 96/131 🟡
SettingsViewModel.swift ██████████████░░░░░░ 70.9% 90/127 🟡
FrameStreamer.swift ██████████████░░░░░░ 69.2% 72/104 🟡
DeviceScannerView.swift ██████████████░░░░░░ 68.4% 1137/1662 🟡
SessionCoordinator.swift ██████████████░░░░░░ 68.0% 1820/2677 🟡
MonitorPresenter.swift █████████████░░░░░░░ 65.3% 154/236 🟡
WelcomeViewController.swift █████████████░░░░░░░ 64.1% 41/64 🟡
UIAlertPresenter.swift ████████████░░░░░░░░ 60.5% 26/43 🟡
RecordingPipeline.swift ████████████░░░░░░░░ 59.7% 284/476 🟡
MediaProcessors.swift ████████████░░░░░░░░ 58.3% 7/12 🟡
StoreManager.swift ███████████░░░░░░░░░ 53.2% 75/141 🟡
CameraViewModel.swift ██████████░░░░░░░░░░ 49.6% 126/254 🔴
ZoomPill.swift ██████████░░░░░░░░░░ 49.4% 168/340 🔴
UICmds.swift ██████████░░░░░░░░░░ 49.0% 96/196 🔴
UIViewController+SwiftUIHosting.swift ██████████░░░░░░░░░░ 48.4% 15/31 🔴
FrameCodecs.swift ██████████░░░░░░░░░░ 48.1% 25/52 🔴
CaptureSyncMetadata.swift ██████████░░░░░░░░░░ 47.8% 32/67 🔴
FrameStreamReceiver.swift █████████░░░░░░░░░░░ 46.3% 238/514 🔴
SceneDelegate.swift █████████░░░░░░░░░░░ 45.8% 11/24 🔴
MonitorViewModel.swift █████████░░░░░░░░░░░ 44.1% 186/422 🔴
PurchaseManaging.swift ████████░░░░░░░░░░░░ 42.0% 21/50 🔴
HEVCFrameEncoder.swift ████████░░░░░░░░░░░░ 40.1% 93/232 🔴
MonitorViewController.swift ███████░░░░░░░░░░░░░ 37.3% 84/225 🔴
EnumExtensions.swift ███████░░░░░░░░░░░░░ 33.9% 40/118 🔴
CameraScreenView.swift ██████░░░░░░░░░░░░░░ 28.5% 148/520 🔴
DeviceScannerViewController.swift █████░░░░░░░░░░░░░░░ 26.2% 113/431 🔴
WatchSessionManager.swift ████░░░░░░░░░░░░░░░░ 20.8% 37/178 🔴
SoundManager.swift ████░░░░░░░░░░░░░░░░ 20.0% 4/20 🔴
MonitorViewController+SwiftUI.swift ███░░░░░░░░░░░░░░░░░ 14.4% 67/465 🔴
CaptureEngine.swift ██░░░░░░░░░░░░░░░░░░ 10.7% 164/1532 🔴
SwiftConstants.swift ██░░░░░░░░░░░░░░░░░░ 10.3% 6/58 🔴
RolePickerController.swift ██░░░░░░░░░░░░░░░░░░ 10.2% 23/226 🔴
PeerLinkStatus.swift █░░░░░░░░░░░░░░░░░░░ 7.1% 10/141 🔴
WatchRemoteCameraController.swift █░░░░░░░░░░░░░░░░░░░ 3.2% 9/279 🔴
CameraPermissionsView.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/640 🔴
CameraHostController.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/197 🔴
RemoteShutterHelpView.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/1373 🔴
CameraPermissionErrorView.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/416 🔴
PhotosAccessDeniedView.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/606 🔴
FrameStreamingCoordinator.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/124 🔴
MulticamView.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/282 🔴
MicrophonePermissionPromptView.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/347 🔴
SettingsView.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/760 🔴
Photos.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/12 🔴
MulticamViewController.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/101 🔴
LocalNetworkPermissionView.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/747 🔴
PermissionManager.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/159 🔴
OrientationUtils.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/18 🔴
PhotosAccessHelper.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/324 🔴
CameraRig.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/574 🔴
StreamLog.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/1 🔴
MultipeerService.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/116 🔴
CameraCountdownTorch.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/61 🔴
Data+MD5.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/4 🔴

Generated from 31f1b4f · workflow run

@darioalessandro

Copy link
Copy Markdown
Member Author

Consolidated into the single feature PR per Dario's request: #203 (branch kept, not deleted).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant