Реалізація автоматичного переключення аудіо між пристроями
Підключив AirPods — аудіо повинно перейти на них. Відсоединив — повернутися на динамік. Підключив Bluetooth-гарнітуру під час дзвінка — дзвінок не повинен переривуватися. Звучит як базовий OS-функціонал, але в продакшні без явної обробки подій AVAudioSession все це працює непередбачувано.
Як iOS управляє аудіо-маршрутизацією
AVAudioSession — центральний об'єкт. За замовчуванням iOS сама переключає вихідний пристрій при зміні маршруту. Проблема в тому, що додаток може не знати про це, й поточний AVAudioPlayer або AVAudioEngine продовжує працювати на «старому» маршруті до наступної операції відтворення.
Для явного управління — AVAudioSession.routeChangeNotification:
NotificationCenter.default.addObserver(
self,
selector: #selector(handleRouteChange(_:)),
name: AVAudioSession.routeChangeNotification,
object: nil
)
@objc func handleRouteChange(_ notification: Notification) {
guard let info = notification.userInfo,
let reasonValue = info[AVAudioSessionRouteChangeReasonKey] as? UInt,
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue)
else { return }
switch reason {
case .newDeviceAvailable:
// AirPods підключилися — переключаємось на них
resumePlaybackIfNeeded()
case .oldDeviceUnavailable:
// Навушниці відключилися — пауза або перехід на динамік
if let previousRoute = info[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
let wasHeadphones = previousRoute.outputs.contains {
$0.portType == .headphones || $0.portType == .bluetoothA2DP
}
if wasHeadphones { pausePlayback() }
}
case .categoryChange:
reconfigureEngine()
default: break
}
}
oldDeviceUnavailable з паузою — стандартне поведення, якого очікують користувачі (Spotify, Apple Music). Без паузи аудіо продовжує грати в динамік після випадкового відключення навушників.
AirPods й автоматичне переключення між пристроями
AirPods Pro/Max підтримують Automatic Switching — перехід між iPhone, iPad, Mac. Додаток не може управляти цим переключенням, але може реагувати на його наслідки. При переключенні AirPods між пристроями додаток отримує routeChangeNotification з reason override або categoryChange.
Тонкість: після смени маршруту AVAudioSession.currentRoute оновлюється не мгновенно. На oldDeviceUnavailable ще 50–100ms маршрут показує старий пристрій. Потрібен короткий Task.sleep(nanoseconds: 100_000_000) або перевірка на наступному runloop-цикліку.
AVAudioEngine: пересборка графа при смені маршруту
Якщо додаток використовує AVAudioEngine з ефектами (еквалайзер, реверб), смена маршруту може скинути сесію. Ознака — AVAudioEngine.isRunning повертає false після routeChangeNotification.
Правильний паттерн: підписуємось на AVAudioEngineConfigurationChange й переключаємо граф:
NotificationCenter.default.addObserver(
forName: .AVAudioEngineConfigurationChange,
object: audioEngine, queue: .main
) { [weak self] _ in
self?.rebuildAudioGraph()
try? self?.audioEngine.start()
}
rebuildAudioGraph() — відсоединяємо всі ноди, міняємо outputNode (який тепер вказує на новий пристрій), підключаємо заново. Без цього кроку AVAudioPlayerNode продовжує відтворювати, але без аудіо — тихо, без помилок у логах.
Android: AudioManager й AudioDeviceCallback
На Android управління маршрутизацією через AudioManager.AudioDeviceCallback:
val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
audioManager.registerAudioDeviceCallback(object : AudioDeviceCallback() {
override fun onAudioDevicesAdded(addedDevices: Array<AudioDeviceInfo>) {
val bluetooth = addedDevices.firstOrNull {
it.type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP ||
it.type == AudioDeviceInfo.TYPE_BLE_HEADSET
}
bluetooth?.let { switchToDevice(it) }
}
override fun onAudioDevicesRemoved(removedDevices: Array<AudioDeviceInfo>) {
pauseIfHeadphonesRemoved(removedDevices)
}
}, Handler(Looper.getMainLooper()))
AudioManager.setPreferredDevice() (API 28+) дозволяє принудово вибрати пристрій. На Android 12+ з'явився setCommunicationDevice() спеціально для звонків — не плутайте з звичайним відтворенням.
Типічні помилки
- Не викликайте
AVAudioSession.setActive(true)після reconfig — отримаєте тихе відтворення без помилки - Не обробляйте
categoryChangeпри вхідному дзвінку — телефонний дзвінок міняє категорію сесії, після завершення потрібно її відновити - Міняйте маршрут на фоновому потоці —
AVAudioSession-операції повинні бути на main thread або явно синхронізовані
Терміни
Базова обробка смени маршруту (iOS): 3–5 днів. Повна реалізація з AVAudioEngine, підтримкою Android, обробкою всіх сценаріїв (дзвінки, BT-гарнітури, AirPods): 2–3 тижні. Вартість розраховується індивідуально.







