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.
127 lines
4.6 KiB
Swift
127 lines
4.6 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 cameras the system exposes. On iOS that's the
|
|
/// front + back wide-angle cameras (in that order, back first).
|
|
/// On macOS it's the built-in FaceTime HD plus any external /
|
|
/// Continuity cameras the OS reports.
|
|
static func discover() -> [DiscoveredCamera] {
|
|
let session = AVCaptureDevice.DiscoverySession(
|
|
deviceTypes: deviceTypes,
|
|
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. On macOS positions are all
|
|
// `.unspecified`, so the sort is a no-op there.
|
|
return session.devices
|
|
.sorted { positionRank($0.position) < positionRank($1.position) }
|
|
.map { device in
|
|
DiscoveredCamera(
|
|
device: device,
|
|
lens: lensName(for: device.position),
|
|
sensorOrientation: defaultSensorOrientation
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Hardcoded per platform: 90° on iOS (matches the value
|
|
/// `camera_avfoundation` reports and that banlu's
|
|
/// `normalizeCameraCapture` math assumes), 0° on macOS (desktop
|
|
/// cams are already physically landscape — any non-zero value
|
|
/// would make `normalizeCameraCapture` rotate the saved JPEG).
|
|
#if os(iOS)
|
|
private static let defaultSensorOrientation = 90
|
|
#else
|
|
private static let defaultSensorOrientation = 0
|
|
#endif
|
|
|
|
/// Per-platform set of device types the discovery session asks
|
|
/// for. iOS only has the built-in cameras; macOS additionally
|
|
/// surfaces externals + Continuity Camera (iPhone-as-webcam).
|
|
/// `.external` / `.externalUnknown` / `.continuityCamera` are
|
|
/// macOS-only enum cases — referencing them on iOS would not
|
|
/// compile, hence the `#if os(macOS)` block.
|
|
private static var deviceTypes: [AVCaptureDevice.DeviceType] {
|
|
var types: [AVCaptureDevice.DeviceType] = [.builtInWideAngleCamera]
|
|
#if os(macOS)
|
|
if #available(macOS 14.0, *) {
|
|
types.append(.external)
|
|
types.append(.continuityCamera)
|
|
} else {
|
|
types.append(.externalUnknown)
|
|
}
|
|
#endif
|
|
return types
|
|
}
|
|
|
|
/// 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
|
|
/// `MethodChannelXCameraBackend.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,
|
|
]
|
|
}
|
|
}
|