Skip to content

Full-sensor stills: photo mode runs the photo format, and the mode toggle pre-warms it - #248

Merged
darioalessandro merged 3 commits into
masterfrom
fix-photos-res
Oct 15, 2026
Merged

darioalessandro merged 3 commits into
masterfrom
fix-photos-res

Conversation

@darioalessandro

Copy link
Copy Markdown
Member

What you get

Photos from the remote stop looking like screenshots of the preview. The camera's session was permanently configured for video, so every still was a 16:9 video frame — upscaled past the sensor (the 14 Oct dinner shot: 4224×2376 from a 4032-wide sensor, digital zoom 1.04×, no Deep Fusion). Now:

  • Photo mode runs the sensor's real photo format. Stills come out at the active format's full photo resolution (12MP on an iPhone 14; 24/48MP ceilings on Pro sensors), with .quality multi-frame processing on every shot — no longer tied to the HDR toggle.
  • Aspect ratios are crops of the full capture. 16:9 (the default) is now ~9MP cut from the 12MP frame instead of a native video frame. The crop runs through ImageIO: HEIC stays HEIC, EXIF — including the rig sync stamp — survives.
  • The PHOTO/VIDEO toggle pre-warms the cameras (new wire action SetCameraMode = 35). The ~0.5s sensor reconfigure happens when you flip the toggle, while you're composing — not at the shutter. First shot after a flip is instant, and the preview shows the real 4:3 photo frame while you frame it.

How it behaves

  • The capture engine follows the rig's existing currentCameraMode — no new seam; capture commands still set the mode at arrival (a synced rig shot arms photo configuration during the countdown), so a missed pre-warm can never desync a shot.
  • SetCameraMode rides the standard control contract: one full-state reply, refused while recording, sent only to cameras that report camera_mode in their state (presence is the capability), reconciled when a camera joins mid-session — same shape as aspect ratio.
  • Mac Catalyst: the .photo preset throws on Mac cameras (measured on hardware) and buys nothing there — a Mac camera's photo ceiling is its video format — so the Mac keeps the video format and gets its full ceiling via maxPhotoDimensions.

What was checked

  • Full simulator suite green: FlatBuffer round-trips for the new action, the coordinator's apply/refuse matrix (applied idle, refused mid-recording with the mode unchanged), director gating + join reconcile, and ImageIO crop codec/metadata preservation.
  • Catalyst hardware suite green on a real camera: native-ratio still measured at exactly the active format's photo ceiling, cropped still matching the cropRect oracle, frames flowing across photo↔video flips (423ms / 53ms).
  • Pending: the same full-res measurement on a physical iPhone 14 (device was offline); expecting 4032×3024.

🤖 Generated with Claude Code

darioalessandro and others added 2 commits October 15, 2026 07:11
…ggle pre-warms it

Stills were frames lifted from the 16:9 video pipeline: the session never
left the video format, so a 12MP sensor delivered ~10MP upscaled, digitally
zoomed frames with no Deep Fusion (quality prioritization was tied to the
HDR toggle). The capture engine now follows the rig's currentCameraMode:
photo mode runs the .photo preset (full sensor, preview-sized buffers to
the streamer), video mode runs the selected video format, and every shot
asks for maxPhotoDimensions at .quality. Aspect crops go through ImageIO,
so HEIC stays HEIC and EXIF (the rig sync stamp included) survives; any
non-native ratio is now a crop of the full capture. On Catalyst the .photo
preset throws (measured on hardware) and buys nothing, so Mac photo mode
keeps the video format and gets its ceiling via maxPhotoDimensions.

New wire action SetCameraMode = 35: the director's PHOTO/VIDEO toggle
pre-warms every capable camera, paying the ~0.5s sensor reconfigure at the
toggle instead of at the shutter. It is a setting push on the standard
control-reply contract (refused while recording), gated on the camera
reporting camera_mode in its state (presence is the capability), and
reconciled on join like aspect. Capture commands still set the mode at
arrival — ScheduledCapture arms photo configuration during the countdown —
so a missed push can never desync a shot.

Verified: full suite green under the simulator (serialization round-trips,
coordinator apply/refuse matrix, director gating + join reconcile, ImageIO
crop codec/metadata); Catalyst hardware suite green with stills measured at
exactly the active format's photo ceiling and crops matching the cropRect
oracle. iPhone hardware run pending (device offline).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…only on real change

Inline narration moved into doc comments on the declarations it described
(cloneCameraSettings, applyPhotoFormatLocked, setVideoQualityLocked, the
rig's currentCameraMode) or deleted. cropPhotoData's dictionary surgery is
now a pure destinationProperties helper built from let + merging. The
platform .photo-preset constant is static. applyCaptureModeLocked announces
the preview-encoder scene cut only when the session actually reconfigured,
so the Catalyst no-op path no longer forces a gratuitous keyframe.

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

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

📊 Test Coverage Report

✅ 755/774 tests passed · ⏭️ 19 skipped

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

🎥 Capture & session core

File Coverage Lines
CaptureEngine.swift ██░░░░░░░░░░░░░░░░░░ 11.5% 233/2020 🔴
RecordingPipeline.swift ████████████████░░░░ 80.6% 697/865 🟢
FrameStreamingCoordinator.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/154 🔴
FrameStreamer.swift ███████████████████░ 93.4% 113/121 🟢
RemoteCmdFlatBuffers.swift ██████████████████░░ 91.6% 1141/1245 🟢
📁 Full report — 101 files
File Coverage Lines
DebugLog.swift █████████████████████ 104.8% 22/21 🟢
JPEGFrameEncoder.swift ████████████████████ 100.0% 12/12 🟢
CameraPreviewMode.swift ████████████████████ 100.0% 13/13 🟢
AppActivityMonitor.swift ████████████████████ 100.0% 31/31 🟢
MonitorViewModel.swift ████████████████████ 100.0% 10/10 🟢
AppTheme.swift ████████████████████ 100.0% 33/33 🟢
Messages.swift ████████████████████ 100.0% 7/7 🟢
ZoomScale.swift ████████████████████ 100.0% 56/56 🟢
ZoomScaleSeed.swift ████████████████████ 100.0% 14/14 🟢
MonitorChrome.swift ████████████████████ 100.0% 12/12 🟢
PeerSessionCore.swift ████████████████████ 100.0% 26/26 🟢
ClockOffsetEstimator.swift ████████████████████ 100.0% 24/24 🟢
ExposurePolicy.swift ████████████████████ 100.0% 42/42 🟢
MultipeerMessages.swift ████████████████████ 100.0% 12/12 🟢
StreamingConfig.swift ████████████████████ 100.0% 34/34 🟢
FocusedCameraControlState.swift ████████████████████ 100.0% 6/6 🟢
Locked.swift ████████████████████ 100.0% 19/19 🟢
FrameCreditWindow.swift ████████████████████ 100.0% 9/9 🟢
AppDelegate.swift ████████████████████ 100.0% 17/17 🟢
CameraLink.swift ████████████████████ 100.0% 59/59 🟢
HEICFrameEncoder.swift ████████████████████ 100.0% 17/17 🟢
PeerCompatibility.swift ████████████████████ 100.0% 42/42 🟢
RigQualityMenu.swift ████████████████████ 99.3% 140/141 🟢
DeviceScannerViewModel.swift ████████████████████ 98.9% 183/185 🟢
FrameSender.swift ████████████████████ 98.4% 187/190 🟢
RemoteCmds.swift ████████████████████ 97.6% 325/333 🟢
FocusPointMapping.swift ████████████████████ 97.6% 40/41 🟢
CaptureSyncMetadata.swift ███████████████████░ 97.0% 65/67 🟢
MultiCamChrome.swift ███████████████████░ 96.9% 31/32 🟢
DiscoveredPeers.swift ███████████████████░ 94.4% 17/18 🟢
WatchSharedTypes.swift ███████████████████░ 94.0% 236/251 🟢
VP9FrameEncoder.swift ███████████████████░ 93.9% 153/163 🟢
WatchPreviewStreamer.swift ███████████████████░ 93.6% 44/47 🟢
MulticamController.swift ███████████████████░ 93.6% 1454/1554 🟢
FrameStreamer.swift ███████████████████░ 93.4% 113/121 🟢
MulticamViewModel.swift ██████████████████░░ 92.1% 139/151 🟢
RemoteCmdFlatBuffers.swift ██████████████████░░ 91.6% 1141/1245 🟢
WelcomeView.swift ██████████████████░░ 89.3% 492/551 🟢
VideoTransferProgressView.swift █████████████████░░░ 84.0% 295/351 🟢
UIImage+gif.swift ████████████████░░░░ 81.6% 129/158 🟢
CameraDeviceDescriptor.swift ████████████████░░░░ 81.0% 34/42 🟢
RecordingPipeline.swift ████████████████░░░░ 80.6% 697/865 🟢
CameraProgressOverlayView.swift ████████████████░░░░ 78.5% 186/237 🟡
SessionCoordinator.swift ████████████████░░░░ 78.0% 1872/2401 🟡
UICmds.swift ███████████████░░░░░ 77.4% 72/93 🟡
FlatBufferSchemas_generated.swift (excluded from app-code metric) ███████████████░░░░░ 77.1% 877/1138 🟡
SettingsView.swift ███████████████░░░░░ 76.2% 593/778 🟡
RolePickerView.swift ███████████████░░░░░ 75.8% 314/414 🟡
StreamLog.swift ███████████████░░░░░ 75.0% 15/20 🟡
WelcomeViewModel.swift ███████████████░░░░░ 74.1% 100/135 🟡
CameraRecordingTimerView.swift ███████████████░░░░░ 73.4% 127/173 🟡
SettingsViewModel.swift ██████████████░░░░░░ 70.6% 89/126 🟡
DeviceScannerView.swift ██████████████░░░░░░ 70.1% 1142/1630 🟡
UIAlertController.swift ██████████████░░░░░░ 69.8% 37/53 🟡
WelcomeViewController.swift █████████████░░░░░░░ 64.1% 41/64 🟡
ScannerLobby.swift ████████████░░░░░░░░ 62.5% 5/8 🟡
CameraViewModel.swift ████████████░░░░░░░░ 59.9% 160/267 🟡
MonitorView.swift ███████████░░░░░░░░░ 56.8% 440/775 🟡
StoreManager.swift ███████████░░░░░░░░░ 54.6% 83/152 🟡
UIViewController+SwiftUIHosting.swift ██████████░░░░░░░░░░ 48.4% 15/31 🔴
FrameCodecs.swift █████████░░░░░░░░░░░ 47.1% 24/51 🔴
SceneDelegate.swift █████████░░░░░░░░░░░ 45.8% 11/24 🔴
FrameStreamReceiver.swift █████████░░░░░░░░░░░ 42.6% 219/514 🔴
PurchaseManaging.swift ████████░░░░░░░░░░░░ 42.0% 21/50 🔴
MulticamViewController.swift ████████░░░░░░░░░░░░ 39.8% 130/327 🔴
MulticamView.swift ████████░░░░░░░░░░░░ 39.5% 527/1333 🔴
CameraScreenView.swift ████████░░░░░░░░░░░░ 39.2% 262/668 🔴
HEVCFrameEncoder.swift ███████░░░░░░░░░░░░░ 36.5% 91/249 🔴
EnumExtensions.swift ███████░░░░░░░░░░░░░ 33.9% 40/118 🔴
ExposureControls.swift ██████░░░░░░░░░░░░░░ 30.8% 94/305 🔴
SwiftConstants.swift █████░░░░░░░░░░░░░░░ 25.7% 18/70 🔴
DeviceScannerViewController.swift █████░░░░░░░░░░░░░░░ 24.3% 117/482 🔴
CameraRig.swift █████░░░░░░░░░░░░░░░ 23.7% 150/632 🔴
RulerPill.swift █████░░░░░░░░░░░░░░░ 23.6% 147/622 🔴
WatchSessionManager.swift ████░░░░░░░░░░░░░░░░ 20.8% 37/178 🔴
MediaProcessors.swift ███░░░░░░░░░░░░░░░░░ 17.5% 11/63 🔴
SessionDebugConsole.swift ███░░░░░░░░░░░░░░░░░ 15.2% 75/495 🔴
CaptureEngine.swift ██░░░░░░░░░░░░░░░░░░ 11.5% 233/2020 🔴
RolePickerController.swift ██░░░░░░░░░░░░░░░░░░ 10.2% 23/226 🔴
PeerLinkStatus.swift █░░░░░░░░░░░░░░░░░░░ 7.1% 10/141 🔴
CameraCountdownTorch.swift █░░░░░░░░░░░░░░░░░░░ 4.9% 3/61 🔴
WatchRemoteCameraController.swift █░░░░░░░░░░░░░░░░░░░ 3.2% 9/279 🔴
SoundManager.swift ░░░░░░░░░░░░░░░░░░░░ 2.1% 1/47 🔴
RecordingTimer.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/52 🔴
ViewfinderGestureLayer.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/168 🔴
CameraPermissionsView.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/676 🔴
CameraHostController.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/205 🔴
RemoteShutterHelpView.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/1373 🔴
CameraPermissionErrorView.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/416 🔴
ZoomPill.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/116 🔴
PhotosAccessDeniedView.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/614 🔴
FrameStreamingCoordinator.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/154 🔴
MicrophonePermissionPromptView.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/347 🔴
Photos.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/12 🔴
LocalNetworkPermissionView.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/747 🔴
PermissionManager.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/185 🔴
OrientationUtils.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/18 🔴
PhotosAccessHelper.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/347 🔴
UIAlertPresenter.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/43 🔴
MultipeerService.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/123 🔴
Data+MD5.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/4 🔴

Generated from 3872838 · workflow run

A fresh session now starts on HEIC when the photo output offers HEVC —
Apple's own default: half the bytes of JPEG at equal quality, which
matters now that full-sensor stills cross the P2P link — and JPEG
otherwise. The policy is one pure function (defaultPhotoFormat), the
director's SetPhotoQuality still overrides it, and the crop path already
preserves the container. Hardware run measured the fallback live: this
MacBook's camera offers no HEVC and correctly starts on JPEG.

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

Copy link
Copy Markdown
Member Author

Added in this PR per review: stills default to HEIC where the hardware encodes HEVC (pure defaultPhotoFormat policy, director override intact). Verified: full sim suite green; Catalyst hardware suite green — this MacBook's camera offers no HEVC and the default measured JPEG, proving the fallback path live. iPhone exercises the HEIC branch.

@darioalessandro
darioalessandro merged commit 595b626 into master Oct 15, 2026
2 checks passed
@darioalessandro
darioalessandro deleted the fix-photos-res branch October 15, 2026 05:33
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