Встроенный видеоплеер в Android: Media3/ExoPlayer — быстрый старт
Короткий ответ: используйте Media3 (официальная эволюция ExoPlayer): подключите зависимости, добавьте PlayerView, создайте ExoPlayer, привяжите к жизненному циклу Activity/Fragment, настройте LoadControl и TrackSelection, обработайте ошибки и сохраните позицию воспроизведения.
Оглавление {{TOC_AUTOMATIC}}
Подготовка: зависимости, разрешения и layout
- В build.gradle (:app) подключите Media3:
dependencies {
implementation("androidx.media3:media3-exoplayer:1.4.1")
implementation("androidx.media3:media3-ui:1.4.1")
// опционально: HLS/DASH модули, DRM и т.п.
}
- Манифест: не забудьте право на интернет и (если нужно) поддержку PiP:
<uses-permission android:name="android.permission.INTERNET" />
<activity android:name=".VideoActivity" android:supportsPictureInPicture="true" />
- Разметка (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 до тонкой настройки буферизации, качества и интеграции с системой.