В современных мобильных приложениях просмотр видео происходит повсеместно — от социальных сетей до игр. Но иногда разработчики хотят не просто воспроизводить видео, а захватывать из него определенные кадры. Это может понадобиться для создания миниатюр, поочередного анализа кадров или добавления расширенных возможностей редактирования. В данном руководстве будет представлен пошаговый процесс разработки именно этой функциональности. Она позволит пользователям приложений легко и плавно захватывать кадры из воспроизводимых видео.

Для демонстрации будем использовать 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-проектах.

Читайте также:

Читайте нас в Telegram, VK и Дзен


 Перевод статьи Oğuzhan Aslan: Dynamically Retrieving Frames in Android

Предыдущая статьяOutSystems: взаимодействие в реальном времени