Реалізація відеоплеєра в мобільному застосунку
Відеоплеєр «з коробки» — AVPlayerViewController на iOS або VideoView з MediaPlayer на Android — працює, але в production майже завжди потрібен користувацький UI: субтитри, вибір якості, користувацькі контролі. Саме тут починаються нюанси.
Користувацькі контролі поверх AVPlayer
На iOS використовуємо AVPlayer + AVPlayerLayer замість AVPlayerViewController. Шар додаємо в viewDidLayoutSubviews:
playerLayer = AVPlayerLayer(player: player)
playerLayer.frame = videoContainerView.bounds
playerLayer.videoGravity = .resizeAspect
videoContainerView.layer.addSublayer(playerLayer)
Прогрес: player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.5, preferredTimescale: 600), queue: .main). Слайдер оновлюється кожні півсекунди, не перевантажуючи UI.
При драгу слайдера: player.pause() на початку drag, player.seek(to:), player.play() наприкінці. Інакше відео заїкається при швидкій перемотці.
Android. StyledPlayerView з media3-ui — налаштовується через XML-атрибути та заміну layout. Для повністю користувацького UI — PlayerView з use_controller="false" та власні кнопки поверх. ExoPlayer + SimpleOnPlaybackStateChangedListener.
Субтитри
iOS. AVMediaCharacteristic.legible — рідні субтитри з HLS/MP4. Для зовнішніх SRT/VTT файлів: конвертуємо SRT → WebVTT, створюємо AVURLAsset з конфігом, додаємо трек через AVMutableComposition. Або — малюємо субтитри своїм UILabel поверх плеєра, парсимо SRT з NSRegularExpression.
Android/ExoPlayer. SubtitleConfiguration у MediaItem.Builder(). Підтримувані формати: WebVTT, SRT, TTML, SSA/ASS. Зовнішній файл: MediaItem.SubtitleConfiguration.Builder(uri).setMimeType(MimeTypes.TEXT_VTT).build().
Вибір якості
Для HLS-потоків вбудований ABR (adaptive bitrate) переключає якість автоматично. Ручний вибір — AVPlayerItem.preferredPeakBitRate на iOS. ExoPlayer: DefaultTrackSelector з parametersBuilder.setMaxVideoBitrate(bitrate).
Для прогресивного MP4 з кількома URL (360p, 720p, 1080p): при смені якості зберігаємо player.currentTime / player.currentPosition, замінюємо джерело, seek(to: savedPosition) після player.ready.
Повноекранний режим
iOS. При переході в landscape — playerLayer.frame = UIScreen.main.bounds, ховаємо navigation bar. Назад — відновлюємо. Підтримка тільки landscape для повного екрана: supportedInterfaceOrientations повертаємо .landscape тільки для VC плеєра.
Android. WindowInsetsControllerCompat(window).hide(WindowInsetsCompat.Type.systemBars()) + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE.
Буферизація та індикатор завантаження
Користувач повинен бачити, що відео буферизується, а не зависло. На iOS підписуємось на KVO-властивість AVPlayerItem.isPlaybackBufferEmpty — при true показуємо UIActivityIndicatorView, при isPlaybackLikelyToKeepUp == true — ховаємо.
На Android Player.Listener.onPlaybackStateChanged з STATE_BUFFERING — стан, коли плеєр чекає даних. STATE_READY — даних достатньо для відтворення.
Важливий нюанс: не показувати спіннер при першій завантаженні до початку відтворення — користувач ще не натиснув play. Спіннер потрібен тільки коли відтворення уже шло та раптово встало на буферизацію.
Flutter: video_player та Chewie
video_player (pub.dev) — базовий плеєр без UI. chewie будує поверх нього стандартні контроли з підтримкою fullscreen та субтитрів. Для користувацького UI — беремо video_player та малюємо своє поверх VideoPlayer віджету. Субтитри через VideoPlayerController з closedCaptionFile.
Орієнтири за часом
Плеєр із користувацькими контролами, субтитрами та повноекранним режимом — 2–3 дні. Додати вибір якості для HLS та збереження позиції при закритті — ще 1 день.







