Files
ux/ios/Classes/Camera/AudioSession.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

76 lines
3.0 KiB
Swift

import AVFoundation
/// Idempotent helpers for upgrading the app's shared `AVAudioSession`
/// to the union of categories needed for capture without trampling
/// what other modules (audio player, video_player) had set. Pattern
/// mirrors `camera_avfoundation`'s `upgradeAudioSessionCategory`
/// only ever WIDENS the category, never narrows.
///
/// We pair this with the session's
/// `automaticallyConfiguresApplicationAudioSession = false` +
/// `usesApplicationAudioSession = true` (set in [CameraSession.init])
/// so AVCaptureSession doesn't yank the category back.
///
/// iOS-only. The macOS counterpart lives in
/// `macos/Classes/Camera/AudioSession.swift` and is a no-op
/// `AVCaptureSession` manages its own audio routing on macOS.
enum AudioSession {
/// Upgrade the shared category to include `.playAndRecord` (and
/// the given options union'd with whatever's already set). No-op
/// when the union equals the current state, so this is cheap to
/// call on every recording start.
static func upgradeForRecording() {
upgrade(
requestedCategory: .playAndRecord,
options: [.defaultToSpeaker, .allowBluetoothA2DP, .allowAirPlay]
)
}
private static func upgrade(
requestedCategory: AVAudioSession.Category,
options: AVAudioSession.CategoryOptions
) {
let playCategories: Set<AVAudioSession.Category> = [.playback, .playAndRecord]
let recordCategories: Set<AVAudioSession.Category> = [.record, .playAndRecord]
let currentCategory = AVAudioSession.sharedInstance().category
let requiredCategories: Set<AVAudioSession.Category> = [
requestedCategory, currentCategory,
]
let requiresPlay = !requiredCategories.isDisjoint(with: playCategories)
let requiresRecord = !requiredCategories.isDisjoint(with: recordCategories)
var finalCategory = requestedCategory
if requiresPlay && requiresRecord {
finalCategory = .playAndRecord
} else if requiresPlay {
finalCategory = .playback
} else if requiresRecord {
finalCategory = .record
}
let finalOptions = AVAudioSession.sharedInstance().categoryOptions
.union(options)
if finalCategory == currentCategory
&& finalOptions == AVAudioSession.sharedInstance().categoryOptions
{
return
}
try? AVAudioSession.sharedInstance().setCategory(
finalCategory,
options: finalOptions
)
// With AVCaptureSession.usesApplicationAudioSession = true and
// automaticallyConfiguresApplicationAudioSession = false, the
// app owns activation without this, the mic input never
// delivers sample buffers. Telegram does the same from
// ManagedAudioSession.activate (setActive(true)).
try? AVAudioSession.sharedInstance().setActive(
true,
options: [.notifyOthersOnDeactivation]
)
}
}