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

71 lines
2.3 KiB
Swift

import AVFoundation
import CoreVideo
#if canImport(UIKit)
import Flutter
#else
import FlutterMacOS
#endif
/// Single-slot latest-pixel-buffer sink that feeds a `FlutterTexture`.
///
/// AVCaptureVideoDataOutput's sample-buffer delegate fires on
/// `videoBufferQueue`; we extract the `CVPixelBuffer`, store it as
/// `latestPixelBuffer`, and notify the texture registry on `main` so
/// Flutter pulls the new frame via `copyPixelBuffer()` on the engine
/// thread.
///
/// We retain *only the most recent* buffer the previous one is
/// released the moment a new sample arrives. This matches the
/// `camera_avfoundation` lifetime invariant and bounds memory at one
/// frame regardless of how fast we produce vs. consume.
final class PreviewSink: NSObject, FlutterTexture {
private weak var registry: FlutterTextureRegistry?
private var textureId: Int64 = -1
/// Serial queue for the `latestPixelBuffer` swap. Sample-buffer
/// delegate writes; `copyPixelBuffer()` reads. Without this the
/// pointer could be freed mid-read on a different thread.
private let bufferQueue = DispatchQueue(
label: "ux.camera.preview.buffer",
qos: .userInitiated
)
private var latestPixelBuffer: CVPixelBuffer?
func register(with registry: FlutterTextureRegistry) -> Int64 {
self.registry = registry
textureId = registry.register(self)
return textureId
}
func unregister() {
registry?.unregisterTexture(textureId)
bufferQueue.sync { latestPixelBuffer = nil }
}
// MARK: - FlutterTexture
func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {
var pb: CVPixelBuffer?
bufferQueue.sync {
pb = latestPixelBuffer
latestPixelBuffer = nil
}
if let pb = pb { return Unmanaged.passRetained(pb) }
return nil
}
/// Receives a new frame. Called from the
/// `AVCaptureVideoDataOutputSampleBufferDelegate` on
/// `videoBufferQueue`. Cheap just swaps the pointer + notifies
/// the registry.
func receive(sampleBuffer: CMSampleBuffer) {
guard let pb = CMSampleBufferGetImageBuffer(sampleBuffer) else {
return
}
bufferQueue.sync { latestPixelBuffer = pb }
if let registry = registry {
registry.textureFrameAvailable(textureId)
}
}
}