Catch-all commit for outstanding pre-existing local changes. Mixes
several themes that would normally be split:
- Rename: UxPlugin → XPlugin across iOS, macOS, Android registrants.
- New top-level packages under lib/src/: anim/ (animated values,
panes, sheets, dock, measured), core/ (Emitter, ReactiveBuilder
scaffolding, presenter/widget/value/dispose primitives), navi/
(Screen/ScreenStack/Router/hero/transitions), reactive/.
- Edits across existing plugins (clipboard, crash, file, gallery,
keyboard, scanner, sensor, url) to align with the new core.
- Test updates and CHANGELOG/README touches accompanying the above.
Camera page kept the session running while the host app was
backgrounded — wastes battery, holds the hardware, and blocks
other apps from grabbing the camera. Add per-platform observers
that pause/resume the session on app foreground/background, with
a uniform `pauseForBackground` / `resumeForForeground` pair on the
shared CameraInstance.
Behaviour:
- On background: any in-flight recording is hard-cancelled
(matches every messaging app — the take ends with the app
switch). The session stops so the OS can release the camera.
- On foreground: session restarts iff it had been running.
Emits `sessionInterrupted` (`reason: appBackgrounded`) and
`sessionResumed` events so the Dart side can surface UX
affordances if needed.
iOS — `ios/Classes/Camera/CameraInstance+iOS.swift`:
Subscribes to UIApplication.{willResignActive, didBecomeActive}
notifications. Work hops onto sessionQueue so AV mutations stay
serialised. Storage uses the shared
`CameraInstance.lifecycleCleanup` closure slot — extension
doesn't need to add stored properties.
Android — added `androidx.lifecycle:lifecycle-process:2.7.0`,
observes `ProcessLifecycleOwner.get().lifecycle`. ON_STOP →
`pauseForBackground` (cancels recording + drops
CustomLifecycleOwner to CREATED → CameraX releases camera).
ON_START → `resumeForForeground`. Observer add/remove on main
thread per `ProcessLifecycleOwner` contract.
macOS — `macos/Classes/Camera/CameraInstance+macOS.swift`:
Intentional no-op. macOS desktop background semantics are
softer; the chat composer's Card dialog typically stays
foregrounded. Slot is wired so the shared
`observeLifecycle()` call still compiles.
Verified: all four platforms (iOS / Android / macOS / app tests)
build clean. Pod install picks up the new iOS extension file
once Pods/ is fresh — `flutter clean` if mid-iteration.
Diagnostic from a fresh build with NSLog confirmed the rotation
behaviour on macOS: with `videoRotationAngle = 90`
(`.portrait`), `AVCapturePhoto.cgImageRepresentation()` returned a
720x1280 CGImage — *physical* portrait pixels, not just an EXIF
tag. So Apple's AVCaptureSession.h docs claiming
"AVCapturePhotoOutput uses EXIF only" don't hold on macOS. The
data-output connection on macOS also honors the same setter, which
is why preview + video flipped to rotated as soon as the photo fix
landed.
Pin macOS to 0° rotation (`.landscapeRight`):
- Photo: 1280x720 (sensor-native landscape, matches what the user
sees on screen).
- Preview: 1280x720 (matches the new desktop 4:3 page aspect).
- Video: 1280x720 (was already correct before the .portrait
change; back to that state).
The snapshot orientation argument is still ignored on macOS —
desktop cameras don't rotate. The argument carries through for iOS
where the camera page passes the device orientation.
`.landscapeRight` / `videoRotationAngle = 0` both still produced a
JPEG rotated 90° CW on macOS. User verified that `.portrait` /
`videoRotationAngle = 90` is the value that DOES NOT rotate the
captured frame on the photo output's connection — counter to the
iOS convention where `.portrait` rotates the landscape sensor
frame into portrait pixels.
I'd expect this to be macOS-specific photo-pipeline plumbing; the
data output's connection still doesn't honor the setter (isSupported
returns false or the setter no-ops), so preview + video stay
unaffected. Verified empirically; not chasing the AVFoundation
source for the why.
Setting `videoOrientation = .landscapeRight` had no effect on
macOS 14+ — Apple deprecated it in favour of `videoRotationAngle`
(a `CGFloat` in degrees) and the old setter is silently ignored
in newer versions. The captured JPEG stayed rotated 90° CW even
with our previous fix.
Try `videoRotationAngle = 0` first (macOS 14+) — that's "no
rotation from the sensor's natural orientation", which is landscape
on desktop cameras. Fall back to `videoOrientation = .landscapeRight`
for macOS 13 and older.
Same `applyUxCaptureOrientation` entry point — no caller changes.
iOS extension untouched; iOS still uses the per-snapshot
`videoOrientation` set (deprecated on iOS 17+ too, but still
functions there).
`AVCaptureVideoDataOutput`'s connection on macOS doesn't honor
`videoOrientation` (or its `isVideoOrientationSupported` is false) —
which is why the preview + recorded video were landscape and looked
fine even with our previously-no-op extension. `AVCapturePhotoOutput`'s
connection on macOS *does* honor it, and its default is `.portrait`
— same as iOS — so leaving it untouched rotated the captured JPEG 90°
CW relative to the landscape sensor.
The extension now sets `.landscapeRight` unconditionally (guarded by
`isVideoOrientationSupported`, so on the data output it's a no-op):
photo connection pins to landscape, JPEG EXIF orientation = 1 (no
rotation), captured image matches the preview.
Video + preview already correct → unaffected.
macOS preview was stretching (aspect wrong) and macOS photo capture
was rotating the landscape sensor 90° because the shared
PhotoOutput / CameraInstance code was setting
`AVCaptureConnection.videoOrientation` from the orientation snapshot
unconditionally. iOS needs that to rotate sample buffers to portrait;
macOS desktop cams are physically landscape and any rotation just
skews the result.
Moved the rotation call behind a per-platform extension on
`AVCaptureConnection`:
- `ios/Classes/Camera/AVCaptureConnection+iOS.swift` applies the
snapshot orientation (current behavior).
- `macos/Classes/Camera/AVCaptureConnection+macOS.swift` is a
no-op. macOS-flavoured photos / preview frames now flow at
native landscape orientation.
`CaptureDevice` reports sensorOrientation=0 on macOS (was hardcoded
90 for iOS); on macOS the page's `normalizeCameraCapture` math then
collapses to identity and the saved JPEG stays the landscape the
sensor produced. iOS keeps sensorOrientation=90 (matches
camera_avfoundation's reported value and the existing capture-
transform math).
Photo and video paths now both produce upright content on macOS
(video already worked because VideoRecorder's transform table maps
the always-portraitUp macOS snapshot to `.identity`).
Reuse the AVFoundation Swift files between iOS and macOS without
sprinkling `#if canImport(UIKit)` through them. The split is:
darwin/Camera/ platform-shared (AVFoundation only)
CameraPlugin channel + instance map
CameraInstance session + outputs + texture
CameraSession AVCaptureSession + runtime-error obs
CaptureDevice front/back discovery
PhotoOutput AVCapturePhotoOutput
PreviewSink CVPixelBuffer → FlutterTexture
VideoRecorder AVAssetWriter
DeviceOrientation wire-string enum
ios/Classes/Camera/ iOS-only impls + extensions
AudioSession AVAudioSession.upgradeForRecording
DeviceOrientationBridge UIDevice.orientation listener
CameraSession+iOS AVCaptureSessionWasInterrupted obs
+ InterruptionReason decode + the
application-audio-session flags
(all iOS-only on AVCaptureSession)
CameraSettings UIApplication.openSettingsURLString
FlutterRegistrar+iOS method-form of textures/messenger
macos/Classes/Camera/ macOS no-op stubs (same surface)
AudioSession no-op (no AVAudioSession on macOS)
DeviceOrientationBridge no-op (desktops don't rotate)
CameraSession+macOS no-op setupPlatform()
CameraSettings NSWorkspace → System Settings'
Privacy_Camera pane
FlutterRegistrar+macOS property-form of textures/messenger
`CameraSession.init` now calls `setupPlatform()` which each platform
provides via an extension — keeps the iOS-only interruption observer
and the `automaticallyConfiguresApplicationAudioSession` /
`usesApplicationAudioSession` flags (both iOS-only on AVCaptureSession)
out of the shared file. Flash-mode in PhotoOutput uses
`if #available(macOS 11/13, *)` rather than `#if`, since those are
plain version gates not platform splits.
The shared files compile into the iOS pod from `ios/Classes/Camera-shared/`
and into the macOS pod from `macos/Classes/Camera-shared/`, each a
mirror populated by a `prepare_command` in the podspec:
rm -rf Classes/Camera-shared && cp -R ../darwin/Camera Classes/Camera-shared
Symlinks and `../` source globs both fail — Pathname.glob bails on
symlinks, and CocoaPods silently drops paths that escape the pod
directory. The mirror destinations are .gitignore'd.
macOS UxPlugin now registers CameraPlugin alongside the others.