Files
ux/darwin/Camera/CameraSession.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

82 lines
3.1 KiB
Swift

import AVFoundation
import Foundation
/// Thin wrapper around `AVCaptureSession` that owns the lifecycle
/// helpers and observes the runtime-error / interruption
/// notifications (iOS only macOS has no equivalent), surfacing them
/// through closures so [CameraInstance] doesn't repeat the boilerplate.
///
/// All `AVCaptureSession` mutations (input/output add/remove, start,
/// stop) must run on the caller's `sessionQueue`. This class doesn't
/// enforce that; the contract is that
/// [CameraInstance.sessionQueue.async { }] wraps every call site.
///
/// Platform-specific init bits the application-audio-session flags
/// and the interruption observers live in `setupPlatform()` whose
/// real implementation is in `ios/Classes/Camera/CameraSession+iOS.swift`.
/// The macOS counterpart (`macos/Classes/Camera/CameraSession+macOS.swift`)
/// is a no-op: macOS has no `AVAudioSession` and no
/// `AVCaptureSessionWasInterrupted` notification.
final class CameraSession {
let av: AVCaptureSession
/// Called on `.main` when the session reports an unrecoverable
/// runtime error. Notable case: `.mediaServicesWereReset` the
/// caller typically tears down and recreates the session.
var onRuntimeError: ((NSError) -> Void)?
/// Called on `.main` when the session is interrupted (e.g. video
/// device taken by another foreground client, audio session
/// interruption, or app backgrounded with `usesApplicationAudioSession`).
/// iOS only never fires on macOS.
var onInterrupted: ((String) -> Void)?
/// Called on `.main` when an earlier interruption ends. iOS only.
var onResumed: (() -> Void)?
var runtimeErrorObserver: NSObjectProtocol?
var interruptedObserver: NSObjectProtocol?
var resumedObserver: NSObjectProtocol?
init() {
av = AVCaptureSession()
runtimeErrorObserver = NotificationCenter.default.addObserver(
forName: .AVCaptureSessionRuntimeError,
object: av,
queue: .main
) { [weak self] note in
let error = note.userInfo?[AVCaptureSessionErrorKey] as? NSError
?? NSError(domain: "ux.camera", code: -1)
self?.onRuntimeError?(error)
}
setupPlatform()
}
deinit {
if let o = runtimeErrorObserver { NotificationCenter.default.removeObserver(o) }
if let o = interruptedObserver { NotificationCenter.default.removeObserver(o) }
if let o = resumedObserver { NotificationCenter.default.removeObserver(o) }
}
/// Configure block; pairs `beginConfiguration` /
/// `commitConfiguration` so every add/remove batch lands as one
/// session update. Caller must be on sessionQueue.
func configure(_ block: () -> Void) {
av.beginConfiguration()
block()
av.commitConfiguration()
}
/// Start the session if it isn't already running.
/// Caller must be on sessionQueue.
func start() {
if !av.isRunning { av.startRunning() }
}
/// Stop the session if it's running.
/// Caller must be on sessionQueue.
func stop() {
if av.isRunning { av.stopRunning() }
}
}