Реалізація потокового аудіовідтворення в мобільному застосунку
Радіо, подкасти, музичний стриминг — аудіо у фоні з буферизацією. Головна складність не в відтворенні, а в стійкості: користувач уходит у інший застосунок, потім повертається — плеєр повинен бути живий, не пересоздан, та синхронізований з тим, що грає.
Архітектура: плеєр у сервісі
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 дні.







