Реализация потокового воспроизведения аудио в мобильном приложении
Радио, подкасты, музыкальный стриминг — аудио в фоне с буферизацией. Главная сложность не в воспроизведении, а в устойчивости: пользователь уходит в другое приложение, потом возвращается — плеер должен быть жив, не пересоздан, и синхронизирован с тем, что играет.
Архитектура: плеер в сервисе
Android. MediaBrowserServiceCompat (устаревший) или media3 MediaSessionService — плеер живёт в отдельном сервисе, Activity только отображает состояние. MediaController связывает UI с сервисом через Binder/IPC. При уничтожении Activity плеер продолжает работать.
// В MediaSessionService
val player = ExoPlayer.Builder(this).build()
val mediaSession = MediaSession.Builder(this, player).build()
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo) = mediaSession
iOS. AVAudioSession.sharedInstance().setCategory(.playback) + UIBackgroundModes: audio в Info.plist. Плеер создаётся в AppDelegate или отдельном синглтоне, переживает пересоздание ViewController.
Буферизация и кэширование чанков
ExoPlayer буферизует вперёд автоматически. Управление через DefaultLoadControl:
val loadControl = DefaultLoadControl.Builder()
.setBufferDurationsMs(
15_000, // minBufferMs
50_000, // maxBufferMs
2_500, // bufferForPlaybackMs
5_000 // bufferForPlaybackAfterRebufferMs
)
.build()
minBufferMs = 15000 — плеер начинает воспроизведение после накопления 2.5 с буфера, держит в памяти до 50 с. При потере сети — продолжает играть из буфера 50 с, потом пауза с индикатором загрузки.
Для кэширования на диск (чтобы не перезагружать при возврате к треку):
val cache = SimpleCache(cacheDir, LeastRecentlyUsedCacheEvictor(100 * 1024 * 1024))
val cacheDataSourceFactory = CacheDataSource.Factory()
.setCache(cache)
.setUpstreamDataSourceFactory(DefaultHttpDataSource.Factory())
iOS. AVURLAsset не кэширует на диск нативно. Для кэширования — AVAssetResourceLoader с кастомным AVAssetResourceLoadingDelegate, пишем данные в файл при загрузке. Или URLCache для HTTP-сегментов при HLS.
Потоковые форматы
| Протокол | Задержка | Применение |
|---|---|---|
| HTTP progressive | нет | подкасты, один файл |
| HLS | 3–30 с | музыкальный стриминг |
| Icecast/Shoutcast (MP3/AAC stream) | < 1 с | интернет-радио |
| OPUS over WebRTC | < 0.2 с | голосовые чаты |
Icecast-стримы (Content-Type: audio/mpeg с бесконечным телом) — ExoPlayer обрабатывает как ProgressiveMediaSource. На iOS — AVPlayer справляется нативно через http:// URL потока.
Метаданные IcyCast
Радиостанции передают метаданные (название трека) прямо в потоке через ICY-заголовки. ExoPlayer IcyDecoder читает их автоматически, получаем через Player.Listener.onMediaMetadataChanged. На iOS нативно не поддерживается — нужен кастомный AVAssetResourceLoadingDelegate с парсингом ICY.
Обработка потери сети
Стриминг — нестабильная среда. При потере соединения плеер должен автоматически попытаться переподключиться, а не просто остановиться.
ExoPlayer: LoadControl.getBackBufferDurationUs() хранит уже воспроизведённые данные в памяти. При переподключении — буфер не теряется, воспроизведение продолжается с того места где остановилось. Для радиострима (live) переподключение означает получение актуального фрагмента, а не того, что было до обрыва.
На iOS: AVPlayer.automaticallyWaitsToMinimizeStalling = true — плеер сам решает, когда накопить достаточно буфера. При обрыве HLS-стрима подписываемся на AVPlayerItem.status KVO, при .failed с NSURLErrorNetworkConnectionLost — replaceCurrentItem(with:) с новым AVPlayerItem от того же URL через 3–5 секунд.
Сроки
Базовый аудио-стриминг с фоновым воспроизведением и медиаконтролами — 2 дня. Кэширование чанков на диск, обработка Icecast-метаданных и офлайн-режим — 3–4 дня.







