Реализация видеоплеера в мобильном приложении
Видеоплеер «из коробки» — AVPlayerViewController на iOS или VideoView с MediaPlayer на Android — работает, но в продакшене почти всегда нужен кастомный 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 день.







