Files
ux/ios/Classes/Camera/DeviceOrientationBridge.swift
agra 14565ebd7a camera: macOS port via darwin/ split (no shared-file pragmas)
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.
2026-05-13 18:53:46 +03:00

94 lines
3.4 KiB
Swift

import AVFoundation
import UIKit
/// Bridges physical-device rotation notifications to a closure callback.
/// Observed source is `UIDevice.current.orientation` independent of
/// any UI orientation lock, so this fires even while the app's window
/// is portrait-locked. `.faceUp` / `.faceDown` are ignored (no useful
/// direction).
///
/// `UIDevice.beginGeneratingDeviceOrientationNotifications()` must be
/// called on main, balanced with `end()`; this class enforces both.
///
/// iOS-only. The macOS counterpart in
/// `macos/Classes/Camera/DeviceOrientationBridge.swift` is a no-op
/// desktops don't rotate, so `current` stays at the initial
/// `portraitUp` and the listener never fires.
final class DeviceOrientationBridge {
typealias Listener = (DeviceOrientationFlutter) -> Void
private var listener: Listener?
private var observer: NSObjectProtocol?
/// Most recent valid orientation observed. Initialised to
/// `portraitUp` so callers have a starting value before the first
/// rotation event.
private(set) var current: DeviceOrientationFlutter = .portraitUp
/// Starts observing. Safe to call multiple times; subsequent calls
/// replace the listener but don't re-register.
func start(listener: @escaping Listener) {
self.listener = listener
guard observer == nil else { return }
// beginGeneratingDeviceOrientationNotifications is main-only.
if Thread.isMainThread {
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
} else {
DispatchQueue.main.sync {
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
}
}
// Seed `current` from the device's reported orientation if it
// is already a valid one.
if let seed = DeviceOrientationFlutter(uiDevice: UIDevice.current.orientation) {
current = seed
}
observer = NotificationCenter.default.addObserver(
forName: UIDevice.orientationDidChangeNotification,
object: UIDevice.current,
queue: .main
) { [weak self] _ in
guard let self = self else { return }
guard let next = DeviceOrientationFlutter(uiDevice: UIDevice.current.orientation) else {
return
}
guard next != self.current else { return }
self.current = next
self.listener?(next)
}
}
func stop() {
listener = nil
if let observer = observer {
NotificationCenter.default.removeObserver(observer)
self.observer = nil
DispatchQueue.main.async {
UIDevice.current.endGeneratingDeviceOrientationNotifications()
}
}
}
deinit { stop() }
}
extension DeviceOrientationFlutter {
/// `UIDeviceOrientation` Flutter convention is a direct 1:1 by
/// name. Despite the AV-side flip, `UIDeviceOrientation.landscapeLeft`
/// and Flutter's `landscapeLeft` describe the same physical pose
/// (home button on the right). The flip lives in
/// [avVideoOrientation], not here.
public init?(uiDevice: UIDeviceOrientation) {
switch uiDevice {
case .portrait: self = .portraitUp
case .portraitUpsideDown: self = .portraitDown
case .landscapeLeft: self = .landscapeLeft
case .landscapeRight: self = .landscapeRight
default: return nil
}
}
}