Как получить кадр за кадром из MP4? (MediaCodec) - PullRequest
2 голосов
/ 27 июня 2019

На самом деле я работаю с OpenGL, и я хотел бы поместить все свои текстуры в MP4 для их сжатия.

Затем мне нужно получить его из MP4 на моем Android

Мне нужно как-то декодировать MP4 и получать кадр за кадром по запросу.

Я нашел это MediaCodec

https://developer.android.com/reference/android/media/MediaCodec

и это MediaMetadataRetriever

https://developer.android.com/reference/android/media/MediaMetadataRetriever

Но я не видел подход, как запросить кадр за кадром ...

Если есть кто-то, кто работал с MP4, пожалуйста, дайте мне способ, куда идти.

PS Я работаю с собственным способом (JNI), поэтому не имеет значения, как это сделать .. Java или нативный, но мне нужно найти способ.

EDIT1

Я делаю какой-то фильм (только одну 3d модель), поэтому я меняю свою геометрию, а также текстуры каждые 32 миллисекунды.Поэтому мне кажется разумным использовать mp4 для tex, потому что каждый новый кадр (32 миллисекунды) очень похож на предыдущий ...

Теперь я использую 400 кадров для одной модели.Для геометрии я использую .mtr, а для tex я использую .pkm (потому что он оптимизирован для Android), поэтому у меня есть около 350 файлов .mtr (потому что некоторые файлы содержат субиндекс) и 400 .pkm файлов ...

Это причина, почему я собираюсь использовать mp4 для tex.Потому что один mp4 намного меньше, чем 400 .pkm

EDIT2

Пожалуйста, посмотрите на Edit1

На самом делевсе, что мне нужно знать, это API Android, который может читать MP4 по кадрам?Может быть, какой-то getNextFrame() метод?

Примерно так

MP4Player player = new MP4Player(PATH_TO_MY_MP4_FILE);

void readMP4(){
   Bitmap b;

   while(player.hasNext()){
      b = player.getNextFrame();

      ///.... my code here ...///
   }
}

EDIT3

Я сделал такую ​​реализацию на Java

public static void read(@NonNull final Context iC, @NonNull final String iPath)
{
    long time;

    int fileCount = 0;

    //Create a new Media Player
    MediaPlayer mp = MediaPlayer.create(iC, Uri.parse(iPath));
    time = mp.getDuration() * 1000;

    Log.e("TAG", String.format("TIME :: %s", time));

    MediaMetadataRetriever mRetriever = new MediaMetadataRetriever();
    mRetriever.setDataSource(iPath);

    long a = System.nanoTime();

    //frame rate 10.03/sec, 1/10.03 = in microseconds 99700
    for (int i = 99700 ; i <= time ; i = i + 99700)
    {
        Bitmap b = mRetriever.getFrameAtTime(i, MediaMetadataRetriever.OPTION_CLOSEST_SYNC);

        if (b == null)
        {
            Log.e("TAG", String.format("BITMAP STATE :: %s", "null"));
        }
        else
        {
            fileCount++;
        }

        long curTime = System.nanoTime();
        Log.e("TAG", String.format("EXECUTION TIME :: %s", curTime - a));
        a = curTime;
    }

    Log.e("TAG", String.format("COUNT :: %s", fileCount));
}

и здесь время выполнения

  E/TAG: EXECUTION TIME :: 267982039
  E/TAG: EXECUTION TIME :: 222928769
  E/TAG: EXECUTION TIME :: 289899461
  E/TAG: EXECUTION TIME :: 138265423
  E/TAG: EXECUTION TIME :: 127312577
  E/TAG: EXECUTION TIME :: 251179654
  E/TAG: EXECUTION TIME :: 133996500
  E/TAG: EXECUTION TIME :: 289730345
  E/TAG: EXECUTION TIME :: 132158270
  E/TAG: EXECUTION TIME :: 270951461
  E/TAG: EXECUTION TIME :: 116520808
  E/TAG: EXECUTION TIME :: 209071269
  E/TAG: EXECUTION TIME :: 149697230
  E/TAG: EXECUTION TIME :: 138347269

На этот раз в наносекундах == +/- 200 миллисекунд ... Это очень медленно ... Мне нужно около 30 миллисекунд на кадр.

Итак, я думаю, что этот метод выполняется на процессоре, поэтому спросите, есть ли метод, который выполняется на GPU?

EDIT4

Я обнаружил, что существует MediaCodec класс

https://developer.android.com/reference/android/media/MediaCodec

Также я нашел подобный вопрос здесь MediaCodec получает все кадры из видео

Я понял, что есть способ чтения по байтам, но не по кадрам ...

Итак, еще вопрос - есть ли способ прочитать mp4 видео по кадрам?

Ответы [ 4 ]

2 голосов
/ 30 июня 2019

Решение будет выглядеть примерно так: ExtractMpegFramesTest , в котором MediaCodec используется для генерации «внешних» текстур из видеокадров. В тестовом коде кадры визуализируются в закадровый буфер, а затем сохраняются в формате PNG. Вы бы просто сделали их напрямую.

Есть несколько проблем с этим:

  1. Видео MPEG не предназначено для работы в качестве базы данных с произвольным доступом. Общая структура GOP (группы изображений) имеет один «ключевой кадр» (по существу, изображение JPEG), за которым следуют 14 дельта-кадров, которые просто содержат отличие от предыдущего декодированного кадра. Поэтому, если вы хотите кадр N, вам, возможно, придется сначала декодировать кадры с N-14 по N-1. Не проблема, если вы всегда движетесь вперед (воспроизводите фильм на текстуре) или сохраняете только ключевые кадры (в этот момент вы изобрели неуклюжую базу данных изображений JPEG).
  2. Как уже упоминалось в комментариях и ответах, вы, вероятно, получите некоторые визуальные артефакты. Насколько они плохи, зависит от материала и степени сжатия. Поскольку вы генерируете кадры, вы можете уменьшить это, обеспечив, чтобы при больших изменениях первый кадр всегда был ключевым кадром.
  3. Микропрограмме, с которой взаимодействует MediaCodec, может потребоваться несколько кадров, прежде чем он начнет производить вывод, даже если вы начинаете с ключевого кадра. Поиск в потоке имеет латентную стоимость. Смотрите, например этот пост . (Вы когда-нибудь задумывались, почему цифровые видеорегистраторы имеют плавную быструю перемотку вперед, но не гладкую быструю перемотку назад?)
  4. Кадры MediaCodec, прошедшие через SurfaceTexture, становятся «внешними» текстурами. У них есть некоторые ограничения по сравнению с обычными текстурами - производительность может быть хуже, нельзя использовать в качестве цветового буфера в FBO и т. Д. Если вы просто рендерите его один раз за кадр со скоростью 30 кадров в секунду, это не должно вопрос.
  5. Метод MediaMetadataRetriever getFrameAtTime() имеет менее желаемую производительность по причинам, указанным выше. Вы вряд ли получите лучшие результаты, написав его самостоятельно, хотя вы можете сэкономить немного времени, пропустив шаг, на котором он создает объект Bitmap. Кроме того, вы передали OPTION_CLOSEST_SYNC in, но это даст только те результаты, которые вы хотите получить, если все ваши кадры являются синхронизирующими кадрами (опять же, неуклюжая база данных изображений JPEG). Вам нужно использовать OPTION_CLOSEST.

Если вы просто пытаетесь воспроизвести фильм на текстуре (или ваша проблема может быть уменьшена до этого), Графика имеет несколько примеров. Одним из них может быть релевантный объект TextureFromCamera, который отображает видеопоток с камеры на прямоугольнике GLES, который можно увеличивать и поворачивать. Вы можете заменить вход камеры на код воспроизведения MP4 из одной из других демонстраций. Это будет хорошо работать, если вы играете только вперед, но если вы хотите пропустить или вернуться назад, у вас будут проблемы.

Проблема, которую вы описываете, очень похожа на ту, с которой сталкиваются разработчики 2D-игр. Делать то, что они делают, вероятно, лучший подход.

2 голосов
/ 03 июля 2019

Да, есть способ извлечь отдельные кадры из видео mp4.

В принципе, вы, похоже, ищете альтернативный способ загрузки текстур, где обычным способом является GLUtils.texImage2D (который заполняет текстуру из Bitmap).

Сначала вы должны рассмотреть, чтодругие советуют, и ожидают визуальные артефакты от сжатия.Но при условии, что ваши текстуры образуют связанные текстуры (например, взрыв), имеет смысл получать их из видеопотока.Для несвязанных изображений вы получите лучшие результаты, используя JPG или PNG.И обратите внимание, что у mp4-видео нет альфа-канала, часто используемого в текстурах.

Для задачи вы не можете использовать MediaMetadataRetriever, это не даст вам необходимой точности для извлечения всех кадров.

Вам придется работать с MediaCodec и MediaExtractor классами.Подробная документация по Android для MediaCodec.

На самом деле вам нужно реализовать настраиваемый видеоплеер и добавить одну ключевую функцию: шаг кадра.

Ближе всего к этому относится Android MediaPlayer , который является полноценным проигрывателем, но 1) не хватает frame-step, и 2) довольно закрытым исходным кодом, потому что он реализован множеством собственных библиотек C ++которые невозможно расширить и трудно изучить.

Я советую это с опытом создания покадрового видеоплеера, и я сделал это, приняв MediaPlayer-Extended , которыйнаписанный на простом Java (без собственного кода), так что вы можете включить это в свой проект и добавить нужную вам функцию.Он работает с Android MediaCodec и MediaExtractor.
Где-то в классе MediaPlayer вы добавите функцию для frameStep и добавите еще одну функцию + сигнал в PlaybackThread для декодирования только одного следующего кадра (в режиме паузы).Однако реализация этого будет зависеть от вас.В результате вы позволите декодеру получать и обрабатывать один кадр, использовать кадр, а затем повторять со следующим кадром.Я сделал это, так что я знаю, что этот подход работает.

Другая половина задачи связана с получением результата.Видеоплеер (с MediaCodec) выводит кадры в Surface.Ваша задача будет получить пиксели.Я знаю, как читать растровое изображение RGB с такой поверхности: вам нужно создать OpenGL Pbuffer EGLSurface, позволить MediaCodec рендерить на эту поверхность (Android SurfaceTexture), а затем читать пиксели с этой поверхности.Это еще одна нетривиальная задача: вам нужно создать шейдер для рендеринга текстуры EOS (поверхности) и использовать GLES20.glReadPixels для получения пикселей RGB в ByteBuffer.Затем вы загружаете эти растровые изображения RGB в свои текстуры.
Однако, поскольку вы хотите загружать текстуры, вы можете найти оптимизированный способ рендеринга видеокадра непосредственно в ваши текстуры и избежать перемещения пикселей вокруг.

Надеюсь, это поможет, и удачи в реализации.

2 голосов
/ 27 июня 2019

Я понимаю, почему может показаться, что все ваши текстуры в одном файле кажутся простыми, но это действительно очень плохая идея.

MP4 - это видеокодек, который сильно оптимизирован для списка кадровкоторые имеют высокий уровень сходства с соседними кадрами, то есть движением.Он также оптимизирован для декомпрессии в последовательном порядке, поэтому использование подхода «произвольного доступа» будет очень неэффективным.

Чтобы дать немного больше подробностей, видеокодеки хранят ключевые кадры (один в секунду, но скорость меняется)) и дельта кадры остальное время.Ключевые кадры независимо сжимаются, как отдельные изображения, но дельта-кадры сохраняются как отличие от одного или нескольких других кадров.Алгоритм предполагает, что эта разница будет довольно минимальной после выполнения компенсации движения.

Поэтому, если вы хотите получить доступ к одному дельта-кадру, ваш код должен будет распаковать соседний ключевой кадр и все дельта-кадры, которые соединяются.это к кадру, который вы хотите, это будет намного медленнее, чем просто использование одного кадра JPEG.

Короче говоря, используйте JPEG или PNG для сжатия текстур и добавьте их все в один архивный файл, чтобы сохранить его в чистоте.

0 голосов
/ 11 июля 2019

На самом деле я хочу опубликовать свою реализацию на текущее время.

Здесь h файл

#include <jni.h>
#include <memory>

#include <opencv2/opencv.hpp>

#include "looper.h"
#include "media/NdkMediaCodec.h"
#include "media/NdkMediaExtractor.h"

#ifndef NATIVE_CODEC_NATIVECODECC_H
#define NATIVE_CODEC_NATIVECODECC_H

//Originally took from here https://github.com/googlesamples/android- 
ndk/tree/master/native-codec
//Convert took from here 
https://github.com/kueblert/AndroidMediaCodec/blob/master/nativecodecvideo.cpp

class NativeCodec
{
public:
NativeCodec() = default;

~NativeCodec() = default;

void DecodeDone();

void Pause();

void Resume();

bool createStreamingMediaPlayer(const std::string &filename);

void setPlayingStreamingMediaPlayer(bool isPlaying);

void shutdown();

void rewindStreamingMediaPlayer();

int getFrameWidth() const
{
    return m_frameWidth;
}

int getFrameHeight() const
{
    return m_frameHeight;
}

void getNextFrame(std::vector<unsigned char> &imageData);

private:
struct Workerdata
{
    AMediaExtractor *ex;
    AMediaCodec *codec;
    bool sawInputEOS;
    bool sawOutputEOS;
    bool isPlaying;
    bool renderonce;
};

void Seek();

ssize_t m_bufidx = -1;
int m_frameWidth = -1;
int m_frameHeight = -1;
cv::Size m_frameSize;

Workerdata m_data = {nullptr, nullptr, false, false, false, false};
};

#endif //NATIVE_CODEC_NATIVECODECC_H

Здесь файл cc

#include "native_codec.h"

#include <cassert>
#include "native_codec.h"
#include <jni.h>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cerrno>
#include <climits>
#include "util.h"
#include <android/log.h>
#include <string>
#include <chrono>
#include <android/asset_manager.h>
#include <android/asset_manager_jni.h>

#include <android/log.h>
#include <string>
#include <chrono>

// for native window JNI
#include <android/native_window_jni.h>
#include <android/asset_manager.h>
#include <android/asset_manager_jni.h>

using namespace std;
using namespace std::chrono;

bool NativeCodec::createStreamingMediaPlayer(const std::string &filename)
{
AMediaExtractor *ex = AMediaExtractor_new();
media_status_t err = AMediaExtractor_setDataSource(ex, filename.c_str());;

if (err != AMEDIA_OK)
{
    return false;
}

size_t numtracks = AMediaExtractor_getTrackCount(ex);

AMediaCodec *codec = nullptr;

for (int i = 0; i < numtracks; i++)
{
    AMediaFormat *format = AMediaExtractor_getTrackFormat(ex, i);

    int format_color;

    AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_COLOR_FORMAT, &format_color);
    bool ok = AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_WIDTH, &m_frameWidth);
    ok = ok && AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_HEIGHT, 
 &m_frameHeight);

    if (ok)
    {
        m_frameSize = cv::Size(m_frameWidth, m_frameHeight);
    } else
    {
        //Asking format for frame width / height failed.
    }

    const char *mime;

    if (!AMediaFormat_getString(format, AMEDIAFORMAT_KEY_MIME, &mime))
    {
        return false;
    } else if (!strncmp(mime, "video/", 6))
    {
        // Omitting most error handling for clarity.
        // Production code should check for errors.
        AMediaExtractor_selectTrack(ex, i);
        codec = AMediaCodec_createDecoderByType(mime);
        AMediaCodec_configure(codec, format, nullptr, nullptr, 0);
        m_data.ex = ex;
        m_data.codec = codec;
        m_data.sawInputEOS = false;
        m_data.sawOutputEOS = false;
        m_data.isPlaying = false;
        m_data.renderonce = true;
        AMediaCodec_start(codec);
    }

    AMediaFormat_delete(format);
}

return true;
}

void NativeCodec::getNextFrame(std::vector<unsigned char> &imageData)
{
if (!m_data.sawInputEOS)
{
    m_bufidx = AMediaCodec_dequeueInputBuffer(m_data.codec, 2000);

    if (m_bufidx >= 0)
    {
        size_t bufsize;
        auto buf = AMediaCodec_getInputBuffer(m_data.codec, m_bufidx, &bufsize);
        auto sampleSize = AMediaExtractor_readSampleData(m_data.ex, buf, bufsize);

        if (sampleSize < 0)
        {
            sampleSize = 0;
            m_data.sawInputEOS = true;
        }

        auto presentationTimeUs = AMediaExtractor_getSampleTime(m_data.ex);

        AMediaCodec_queueInputBuffer(m_data.codec, m_bufidx, 0, sampleSize, 
presentationTimeUs,
                                     m_data.sawInputEOS ? 
AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM : 0);

        AMediaExtractor_advance(m_data.ex);
    }
}

if (!m_data.sawOutputEOS)
{
    AMediaCodecBufferInfo info;
    auto status = AMediaCodec_dequeueOutputBuffer(m_data.codec, &info, 0);

    if (status >= 0)
    {
        if (info.flags & AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM)
        {
            __android_log_print(ANDROID_LOG_ERROR, 
 "AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM", "AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM :: %s", 
//
                                "output EOS");

            m_data.sawOutputEOS = true;
        }

        if (info.size > 0)
        {
//                size_t bufsize;
            uint8_t *buf = AMediaCodec_getOutputBuffer(m_data.codec, 
  static_cast<size_t>(status), /*bufsize*/nullptr);
            cv::Mat YUVframe(cv::Size(m_frameSize.width, static_cast<int> 
  (m_frameSize.height * 1.5)), CV_8UC1, buf);

            cv::Mat colImg(m_frameSize, CV_8UC3);
            cv::cvtColor(YUVframe, colImg, CV_YUV420sp2BGR, 3);
            auto dataSize = colImg.rows * colImg.cols * colImg.channels();
            imageData.assign(colImg.data, colImg.data + dataSize);
        }

        AMediaCodec_releaseOutputBuffer(m_data.codec, static_cast<size_t>(status), 
 info.size != 0);

        if (m_data.renderonce)
        {
            m_data.renderonce = false;
            return;
        }
    } else if (status < 0)
    {
        getNextFrame(imageData);
    } else if (status == AMEDIACODEC_INFO_OUTPUT_BUFFERS_CHANGED)
    {
        __android_log_print(ANDROID_LOG_ERROR, 
"AMEDIACODEC_INFO_OUTPUT_BUFFERS_CHANGED", "AMEDIACODEC_INFO_OUTPUT_BUFFERS_CHANGED :: %s", //
                            "output buffers changed");
    } else if (status == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED)
    {
        auto format = AMediaCodec_getOutputFormat(m_data.codec);

        __android_log_print(ANDROID_LOG_ERROR, 
"AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED", "AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED :: %s", 
 //
                            AMediaFormat_toString(format));

        AMediaFormat_delete(format);
    } else if (status == AMEDIACODEC_INFO_TRY_AGAIN_LATER)
    {
        __android_log_print(ANDROID_LOG_ERROR, "AMEDIACODEC_INFO_TRY_AGAIN_LATER", 
  "AMEDIACODEC_INFO_TRY_AGAIN_LATER :: %s", //
                            "no output buffer right now");
    } else
    {
        __android_log_print(ANDROID_LOG_ERROR, "UNEXPECTED INFO CODE", "UNEXPECTED 
 INFO CODE :: %zd", //
                            status);
    }
}
}

void NativeCodec::DecodeDone()
{
if (m_data.codec != nullptr)
{
    AMediaCodec_stop(m_data.codec);
    AMediaCodec_delete(m_data.codec);
    AMediaExtractor_delete(m_data.ex);
    m_data.sawInputEOS = true;
    m_data.sawOutputEOS = true;
}
}

void NativeCodec::Seek()
{
AMediaExtractor_seekTo(m_data.ex, 0, AMEDIAEXTRACTOR_SEEK_CLOSEST_SYNC);
AMediaCodec_flush(m_data.codec);
m_data.sawInputEOS = false;
m_data.sawOutputEOS = false;

if (!m_data.isPlaying)
{
    m_data.renderonce = true;
}
}

void NativeCodec::Pause()
{
if (m_data.isPlaying)
{
    // flush all outstanding codecbuffer messages with a no-op message
    m_data.isPlaying = false;
}
}

void NativeCodec::Resume()
{
if (!m_data.isPlaying)
{
    m_data.isPlaying = true;
}
}

void NativeCodec::setPlayingStreamingMediaPlayer(bool isPlaying)
{
if (isPlaying)
{
    Resume();
} else
{
    Pause();
}
}

void NativeCodec::shutdown()
{
m_bufidx = -1;
DecodeDone();
}

void NativeCodec::rewindStreamingMediaPlayer()
{
Seek();
}

Итак, в соответствии с этой реализацией для преобразования формата (в моем случае из YUV в BGR) вам необходимо установить OpenCV, чтобы понять, как это сделать, проверьте эти два источника

https://www.youtube.com/watch?v=jN9Bv5LHXMk

https://www.youtube.com/watch?v=0fdIiOqCz3o

А также для примера оставляю здесь свой CMakeLists.txt файл

#For add OpenCV take a look at this video
#https://www.youtube.com/watch?v=jN9Bv5LHXMk
#https://www.youtube.com/watch?v=0fdIiOqCz3o
#Look at the video than compare with this file and make the same

set(pathToProject
    C:/Users/tetavi/Downloads/Buffer/OneMoreArNew/arcore-android- 
sdk/samples/hello_ar_c)
set(pathToOpenCv C:/OpenCV-android-sdk)

cmake_minimum_required(VERSION 3.4.1)

set(CMAKE VERBOSE MAKEFILE on)
set(CMAKE CXX FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11")

include_directories(${pathToOpenCv}/sdk/native/jni/include)

# Import the ARCore library.
add_library(arcore SHARED IMPORTED)
set_target_properties(arcore PROPERTIES IMPORTED_LOCATION
    ${ARCORE_LIBPATH}/${ANDROID_ABI}/libarcore_sdk_c.so
    INTERFACE_INCLUDE_DIRECTORIES ${ARCORE_INCLUDE}
    )

# Import the glm header file from the NDK.
add_library(glm INTERFACE)
set_target_properties(glm PROPERTIES
    INTERFACE_INCLUDE_DIRECTORIES 
${ANDROID_NDK}/sources/third_party/vulkan/src/libs/glm
    )

# This is the main app library.
add_library(hello_ar_native SHARED
     src/main/cpp/background_renderer.cc
    src/main/cpp/hello_ar_application.cc
    src/main/cpp/jni_interface.cc
    src/main/cpp/video_render.cc
    src/main/cpp/geometry_loader.cc
    src/main/cpp/plane_renderer.cc
    src/main/cpp/native_codec.cc
    src/main/cpp/point_cloud_renderer.cc
    src/main/cpp/frame_manager.cc
    src/main/cpp/safe_queue.cc
    src/main/cpp/stb_image.h
    src/main/cpp/util.cc)

add_library(lib_opencv SHARED IMPORTED)
set_target_properties(lib_opencv PROPERTIES IMPORTED_LOCATION

${pathToProject}/app/src/main/jniLibs/${CMAKE_ANDROID_ARCH_ABI}/libopencv_java3.so)

target_include_directories(hello_ar_native PRIVATE
    src/main/cpp)

target_link_libraries(hello_ar_native $\{log-lib} lib_opencv
    android
    log
    GLESv2
    glm
    mediandk
    arcore)

Использование:

С помощью этого метода необходимо создать потоковый медиаплеер

NaviteCodec::createStreamingMediaPlayer(pathToYourMP4file);

, а затем просто используйте

NativeCodec::getNextFrame(imageData);

Не стесняйтесь спрашивать

...