Встроенный видеоплеер в Android: Media3/ExoPlayer — быстрый старт

Короткий ответ: используйте Media3 (официальная эволюция ExoPlayer): подключите зависимости, добавьте PlayerView, создайте ExoPlayer, привяжите к жизненному циклу Activity/Fragment, настройте LoadControl и TrackSelection, обработайте ошибки и сохраните позицию воспроизведения.

Оглавление {{TOC_AUTOMATIC}}

Подготовка: зависимости, разрешения и layout

  1. В build.gradle (:app) подключите Media3:
dependencies {
  implementation("androidx.media3:media3-exoplayer:1.4.1")
  implementation("androidx.media3:media3-ui:1.4.1")
  // опционально: HLS/DASH модули, DRM и т.п.
}
  1. Манифест: не забудьте право на интернет и (если нужно) поддержку PiP:
<uses-permission android:name="android.permission.INTERNET" />
<activity android:name=".VideoActivity" android:supportsPictureInPicture="true" />
  1. Разметка (res/layout/activity_video.xml): используйте PlayerView из media3-ui:
<androidx.media3.ui.PlayerView
  android:id="@+id/playerView"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:keepScreenOn="true"
  app:useController="true"
  app:resize_mode="fit"/>

Не размещайте PlayerView внутри ScrollView — это ломает измерения и полноэкранный режим.

Инициализация, жизненный цикл и базовое воспроизведение

Пример Activity на Kotlin (с ключевыми фрагментами):

class VideoActivity : AppCompatActivity() {
  private var player: ExoPlayer? = null
  private lateinit var playerView: PlayerView
  private val videoUrl = "https://example.com/video.m3u8"
  private var playbackPosition = 0L

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_video)
    playerView = findViewById(R.id.playerView)
  }

  private fun initializePlayer() {
    if (player != null) return
    player = ExoPlayer.Builder(this).build().also { exo ->
      playerView.player = exo
      val mediaItem = MediaItem.fromUri(videoUrl)
      exo.setMediaItem(mediaItem)
      exo.playWhenReady = true
      exo.seekTo(playbackPosition)
      exo.addListener(playerListener)
      exo.prepare()
    }
  }

  private fun releasePlayer() {
    playerView.player = null
    player?.run { playbackPosition = currentPosition; release() }
    player = null
  }

  // Жизненный цикл (рекомендованный паттерн)
  override fun onStart() { super.onStart(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) initializePlayer() }
  override fun onResume() { super.onResume(); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || player == null) initializePlayer() }
  override fun onPause() { super.onPause(); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) releasePlayer() }
  override fun onStop() { super.onStop(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) releasePlayer() }
}

Обработчик событий:

private val playerListener = object : Player.Listener {
  override fun onPlaybackStateChanged(state: Int) { /* показать/скрыть лоадер, обработать END */ }
  override fun onPlayerError(error: PlaybackException) { /* показать кнопку "Повторить" */ }
}

Продвинутые настройки: буферизация, качество, fullscreen и PiP

  • Буферизация (DefaultLoadControl): настраивайте min/maxBufferMs, bufferForPlaybackMs в зависимости от сцена — короткие ролики vs фильмы.
val loadControl = DefaultLoadControl.Builder()
  .setBufferDurationsMs(5_000, 50_000, 1_500, 3_000)
  .build()
player = ExoPlayer.Builder(context).setLoadControl(loadControl).build()
  • Управление качеством: используйте trackSelectionParameters для ограничения качества (например, SD для экономии трафика).
val params = player?.trackSelectionParameters?.buildUpon()
  ?.setMaxVideoSizeSd()
  ?.build()
player?.trackSelectionParameters = params!!
  • Full screen: отдельная кнопка переключает ориентацию и системные UI-флаги; не забудьте вернуть флаги при выходе.
  • PiP: вызывайте enterPictureInPictureMode() в onUserLeaveHint и объявите поддержку в манифесте.

Для кастомных контролов задайте app:controller_layout_id и используйте стандартные id (exo_play, exo_pause) или свои View, чтобы привязать логику.

Когда плеер в Activity, ViewModel или как Singleton

ПодходОсобенностьКогда использовать
Activity/FragmentПросто, локальноОдин экран с видео
Singleton/DI (Hilt)Единый плеер между экранамиФоновое воспроизведение, медиаплеер
ViewModelПереживает rotate, удобен с MVVMСохранение позиции/стейта при recreate

Частые ошибки

  • Создание плеера в onCreate без release → утечки.
  • Несохранение позиции при rotate → видео стартует сначала.
  • Несоответствие жизненного цикла для API <24 и >=24 → двойное создание/утечки.
  • Игнорирование ошибок сети → чёрный экран вместо понятного сообщения.
  • Неправильная интеграция с MediaSession → уведомления/PiP не работают.

FAQ

  • Как сохранить позицию при смене ориентации?
    Сохраняйте player.currentPosition в onPause/onStop и seekTo при инициализации.

  • Нужен ли foreground service для фонового воспроизведения?
    Да — если хотите воспроизведение в фоне с уведомлением (особенно для аудио/подкастов).

  • Что выбрать: Media3 или старый ExoPlayer?
    Для новых проектов — Media3; старый пакет поддерживается минимальными фикcами.

  • Как отладить ре‑буферинг?
    Логируйте onPlaybackStateChanged, экспериментируйте с LoadControl и тестируйте на разных сетях.

Этот набор шагов и паттернов даёт рабочую основу для надёжного видеоплеера в Android: от подключения SDK до тонкой настройки буферизации, качества и интеграции с системой.