В современных мобильных приложениях просмотр видео происходит повсеместно — от социальных сетей до игр. Но иногда разработчики хотят не просто воспроизводить видео, а захватывать из него определенные кадры. Это может понадобиться для создания миниатюр, поочередного анализа кадров или добавления расширенных возможностей редактирования. В данном руководстве будет представлен пошаговый процесс разработки именно этой функциональности. Она позволит пользователям приложений легко и плавно захватывать кадры из воспроизводимых видео.
Для демонстрации будем использовать ExoPlayer, широко распространенный медиафреймворк для Android. Однако стоит отметить, что представляемый здесь метод извлечения кадров из воспроизводимого видео не привязан к какому-либо конкретному фреймворку. Вы можете выбрать тот медиафреймворк, который максимально соответствует потребностям вашего проекта. Для начала понадобится добавить следующие зависимости в файл build.gradle приложения:
def media3_version = "1.1.1" implementation "androidx.media3:media3-exoplayer:$media3_version" implementation "androidx.media3:media3-exoplayer-dash:$media3_version" implementation "androidx.media3:media3-ui:$media3_version" implementation "androidx.media3:media3-session:$media3_version"
В классе, в котором собираетесь работать с воспроизведением видео (скорее всего, это Activity или Fragment), инициализируйте экземпляр ExoPlayer следующим образом:
private val exoPlayer by lazy { ExoPlayer.Builder(this) .build() }
Не забудьте добавить PlayerView в макет и связать его с экземпляром ExoPlayer:
private fun setPlayer() { binding.playerView.player = exoPlayer }
Выполнение этих шагов позволит приступить к извлечению кадров из воспроизводимого видео в Android-приложении. Углубимся в детали процесса.
Использование MediaMetadataRetriever
Класс MediaMetadataRetriever предлагает простое решение для извлечения кадров и метаданных из медиафайлов в Android. Этот класс предоставляет унифицированный интерфейс для подобных операций, что делает его удобным выбором для разработчиков. Для создания экземпляра MediaMetadataRetriever в его конструкторе не требуется никаких параметров, что упрощает процесс инициализации. Можете запустить его следующим образом:
private val retriever by lazy(::MediaMetadataRetriever)
После инициализации необходимо установить источник данных для ретривера, чтобы начать получать кадры. Если в проекте предполагается использовать локальный видеофайл, хранящийся в ресурсах приложения, можно применить метод setDataSource(FileDescriptor fd, long offset, long length). Вот пример того, как установить источник данных, используя локальный видеофайл:
val assetFileDescriptor = resources.openRawResourceFd(R.raw.sample_video) retriever.setDataSource( assetFileDescriptor.fileDescriptor, assetFileDescriptor.startOffset, assetFileDescriptor.length )
Если же предполагается работать с видеофайлом, расположенным в URI (uniform resource identifier — унифицированный идентификатор ресурса), в качестве альтернативы можно применить метод setDataSource(Context context, Uri uri).
Наконец, интегрируем это в ExoPlayer. В функции loadAndPrepareVideo() устанавливаем источник данных как для MediaMetadataRetriever, так и для ExoPlayer, обеспечивая синхронизацию между получением кадров и воспроизведением видео:
private fun loadAndPrepareVideo() { val assetFileDescriptor = resources.openRawResourceFd(R.raw.sample_video) retriever.setDataSource( assetFileDescriptor.fileDescriptor, assetFileDescriptor.startOffset, assetFileDescriptor.length ) val video = getRawResourceUriString(R.raw.sample_video) val mediaItem = MediaItem.fromUri(video) exoPlayer.setMediaItem(mediaItem) exoPlayer.prepare() } private fun getRawResourceUriString(@RawRes rawResourceId: Int): String { val packageName = packageName return "android.resource://$packageName/raw/" + resources.getResourceEntryName(rawResourceId) }
Выполнение этих шагов позволит получать кадры из воспроизводимого видео с помощью MediaMetadataRetriever в сочетании с ExoPlayer.
Прослушивание кадров в ExoPlayer
Чтобы эффективно извлекать кадры из воспроизводимого видео, необходимо отслеживать изменения в состоянии воспроизведения ExoPlayer. Это позволит синхронизировать извлечение кадров с воспроизведением видео. Для начала зарегистрируйте в ExoPlayer слушателя изменений в состоянии воспроизведения:
exoPlayer.addListener(this)
Теперь реализуйте метод onIsPlayingChanged в Activity или Fragment. Этот метод будет срабатывать при каждом изменении состояния воспроизведения ExoPlayer. Вот пример реализации:
override fun onIsPlayingChanged(isPlaying: Boolean) { super.onIsPlayingChanged(isPlaying) startProcessJobIfNeeded(isPlaying) cancelProcessJobIfNeeded(isPlaying) }
Перейдем к реализации функций cancelProcessJobIfNeeded и startProcessJobIfNeeded. Начнем с cancelProcessJobIfNeeded. Эта функция отвечает за отмену задания корутины, если воспроизведение видео приостановлено:
private var job: Job? = null private fun cancelProcessJobIfNeeded(isPlaying: Boolean) { if (!isPlaying && job != null) { job?.cancel() job = null } }
Переходим к startProcessJobIfNeeded. Эта функция инициирует задание корутины для непрерывного получения кадров во время воспроизведения видео. Настройте параметр delay, чтобы управлять частотой итераций при получении кадров:
private fun startProcessJobIfNeeded(isPlaying: Boolean) { if (job == null && isPlaying) { job = lifecycleScope.launch { while (isActive) { getCurrentFrame() delay(50) // Настройте delay в соответствии с вашими потребностями } } } }
Важно отметить, что этот подход основан на оптимальной практике, найденной в интернете, поскольку ExoPlayer не предоставляет встроенной поддержки для итеративного прослушивания прогресса получения кадров.
Наконец, основной функцией, отвечающей за получение текущего кадра, является getCurrentFrame. Эта функция вычисляет текущую позицию видео, извлекает кадр в этой позиции с помощью MediaMetadataRetriever и масштабирует растровое изображение, чтобы оно соответствовало размерам PlayerView:
private fun getCurrentFrame(): Bitmap? { val currentPosMillis = exoPlayer.currentPosition.toDuration(DurationUnit.MILLISECONDS) val currentPosMicroSec = currentPosMillis.inWholeMicroseconds val currentFrame = retriever.getFrameAtTime(currentPosMicroSec, MediaMetadataRetriever.OPTION_CLOSEST) val bitmap = currentFrame ?: return null return Bitmap.createScaledBitmap(bitmap, binding.playerView.width, binding.playerView.height, false) }
Эти функции позволяют динамически извлекать кадры из воспроизводимого видео в приложении для Android с помощью ExoPlayer.
Заключение
Теперь вы знаете, как извлекать кадры из воспроизводимого видео в Android-приложениях. Используя ExoPlayer и MediaMetadataRetriever, вы сможете легко интегрировать функцию извлечения кадров в свои проекты. Описанные здесь методы предлагают эффективные решения — будь то редактирование видео, анализ кадров или улучшение пользовательского опыта. Важно понимать, что принципы работы с ExoPlayer, продемонстрированные в этом руководстве, применимы к различным медиафреймворкам. Эти инструменты позволят вам использовать возможности видеокадров в своих Android-проектах.
Читайте также:
- Как предотвратить утечки памяти в Android-приложении
- Миграция UI-ориентированной библиотеки Android на Compose Multiplatform (Android/iOS)
- Что нового в системной трассировке Android Studio
Читайте нас в Telegram, VK и Дзен
Перевод статьи Oğuzhan Aslan: Dynamically Retrieving Frames in Android