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.
99 lines
3.5 KiB
Swift
99 lines
3.5 KiB
Swift
import AVFoundation
|
|
|
|
/// Static helpers for camera-device discovery and per-device init.
|
|
///
|
|
/// Mirrors what telegram-ios's `CameraDevice` does for the
|
|
/// session-priority + format-negotiation cases we actually care
|
|
/// about — front / back wide-angle only. Telegram's preference for
|
|
/// `TripleCamera` / `DualCamera` is for multi-cam zoom UX we don't
|
|
/// build today; if we ever need it, this is where it goes.
|
|
enum CaptureDevice {
|
|
/// Enumerate the front + back wide-angle cameras the system
|
|
/// exposes. Order: back first, front second. Stable for the
|
|
/// lifetime of the process — iOS doesn't hot-swap cameras.
|
|
static func discover() -> [DiscoveredCamera] {
|
|
let session = AVCaptureDevice.DiscoverySession(
|
|
deviceTypes: [.builtInWideAngleCamera],
|
|
mediaType: .video,
|
|
position: .unspecified
|
|
)
|
|
// Sort so back devices come first; the chat composer opens
|
|
// the back camera by default elsewhere, so this matches the
|
|
// common "first available" pick.
|
|
return session.devices
|
|
.sorted { positionRank($0.position) < positionRank($1.position) }
|
|
.map { device in
|
|
DiscoveredCamera(
|
|
device: device,
|
|
lens: lensName(for: device.position),
|
|
// iOS doesn't expose sensor orientation directly;
|
|
// 90° matches what `camera_avfoundation` reports
|
|
// and what banlu's `normalizeCameraCapture` math
|
|
// assumes for iOS sensors.
|
|
sensorOrientation: 90
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Apply our default per-device config: continuous autofocus,
|
|
/// continuous auto-exposure, torch off. Idempotent; safe to call
|
|
/// repeatedly. The block is wrapped in
|
|
/// `lockForConfiguration` / `unlockForConfiguration`.
|
|
static func applyDefaults(_ device: AVCaptureDevice) {
|
|
do {
|
|
try device.lockForConfiguration()
|
|
defer { device.unlockForConfiguration() }
|
|
|
|
if device.isFocusModeSupported(.continuousAutoFocus) {
|
|
device.focusMode = .continuousAutoFocus
|
|
}
|
|
if device.isExposureModeSupported(.continuousAutoExposure) {
|
|
device.exposureMode = .continuousAutoExposure
|
|
}
|
|
if device.hasTorch && device.isTorchModeSupported(.off) {
|
|
device.torchMode = .off
|
|
}
|
|
} catch {
|
|
// Best-effort — a device that refuses lockForConfiguration
|
|
// will still capture frames; we just can't tweak focus.
|
|
}
|
|
}
|
|
|
|
// MARK: - private
|
|
|
|
private static func positionRank(_ position: AVCaptureDevice.Position) -> Int {
|
|
switch position {
|
|
case .back: return 0
|
|
case .front: return 1
|
|
default: return 2
|
|
}
|
|
}
|
|
|
|
private static func lensName(for position: AVCaptureDevice.Position) -> String {
|
|
switch position {
|
|
case .front: return "front"
|
|
case .back: return "back"
|
|
default: return "back"
|
|
}
|
|
}
|
|
}
|
|
|
|
/// One row of the discovery result. `lens` and `sensorOrientation`
|
|
/// match the wire shape expected by Dart's
|
|
/// `MethodChannelUxCameraBackend.availableCameras()`.
|
|
struct DiscoveredCamera {
|
|
let device: AVCaptureDevice
|
|
let lens: String
|
|
let sensorOrientation: Int
|
|
|
|
var uniqueID: String { device.uniqueID }
|
|
|
|
func toWire() -> [String: Any] {
|
|
return [
|
|
"id": uniqueID,
|
|
"lens": lens,
|
|
"sensorOrientation": sensorOrientation,
|
|
]
|
|
}
|
|
}
|