Реализация автоматического переключения аудио между устройствами
Подключил 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)после smConfigReconfiguration — получаете тихое воспроизведение без ошибки - Не обрабатывать
categoryChangeпри входящем звонке — телефонный звонок меняет категорию сессии, после завершения нужно её восстановить - Менять маршрут на фоновом потоке —
AVAudioSession-операции должны быть на main thread или явно синхронизированы
Сроки
Базовая обработка смены маршрута (iOS): 3–5 дней. Полная реализация с AVAudioEngine, поддержкой Android, обработкой всех сценариев (звонки, BT-гарнитуры, AirPods): 2–3 недели. Стоимость рассчитывается индивидуально.







