如何从MP4中逐帧获取? (媒体编解码器)

How to get frame by frame from MP4? (MediaCodec)

实际上我正在使用 OpenGL,我想将我所有的纹理放在 MP4 中以便压缩它们。

然后我需要从 Android

上的 MP4 中获取它

我需要以某种方式解码 MP4 并按请求逐帧获取。

我找到了这个MediaCodec

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

还有这个MediaMetadataRetriever

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

但是我没有看到如何逐帧请求的方法...

如果有谁用过MP4,请指点一下方法。

P.S. 我正在使用本机方式 (JNI),所以如何做并不重要.. Java 或本机,但是我需要找到路。

EDIT1

我制作了某种电影(只有一个 3d 模型),所以我每 32 毫秒更改一次几何形状和纹理。所以,在我看来,使用 mp4 作为 tex 是合理的,因为每个新帧(32 毫秒)与原始帧非常相似......

现在一个模型我用了400帧。对于几何,我使用 .mtr,对于 tex,我使用 .pkm(因为它针对 android 进行了优化),所以我有大约 350 个 .mtr 文件(因为一些文件包含子索引)和 400 个 .pkm 文件...

这就是我要为 tex 使用 mp4 的原因。因为一个mp4比400.pkm小很多

EDIT2

请看Edit1

实际上我需要知道的是 Android 中的 API 可以按帧读取 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 毫秒的帧。

所以,我认为这个方法是在CPU上执行的,所以请问有没有在GPU上执行的方法?

EDIT4

我发现有MediaCodecclass

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

我在这里也发现了类似的问题MediaCodec get all frames from video

我知道有一种方法可以按字节读取,但不能按帧读取...

那么,还有问题 - 是否有一种方法可以逐帧读取 mp4 视频?

我明白为什么将所有纹理放在一个文件中似乎很容易,但这是一个非常糟糕的主意。

MP4 是一种视频编解码器,它针对与相邻帧(即运动)具有高度相似性的帧列表进行了高度优化。它还被优化为按顺序解压缩,因此使用 'random access' 方法将非常低​​效。

为了提供更多细节,视频编解码器存储关键帧(每秒一个,但速率会改变),其余时间存储增量帧。关键帧像单独的图像一样被独立压缩,但增量帧存储为与一个或多个其他帧的差异。该算法假定在执行运动补偿后这种差异将非常小。

因此,如果您想访问单个增量帧,您的代码将不得不解压缩附近的关键帧以及将它连接到您想要的帧的所有增量帧,这将比仅使用单帧 JPEG 慢得多.

简而言之,使用 JPEG 或 PNG 压缩您的纹理并将它们全部添加到一个存档文件以保持整洁。

解决方案看起来类似于 ExtractMpegFramesTest,其中 MediaCodec 用于从视频帧生成 "external" 纹理。在测试代​​码中,帧被渲染到 off-screen pbuffer,然后保存为 PNG。您只需直接渲染它们即可。

这有一些问题:

  1. MPEG 视频不适合用作 random-access 数据库。 一个常见的 GOP(图片组)结构有一个 "key frame"(本质上是一个 JPEG 图像)后跟 14 个增量帧,它只保留与前一个解码帧的差异。因此,如果您想要 N 帧,则可能必须先解码 N-14 到 N-1 帧。如果您总是向前移动(在纹理上播放电影)或您只存储关键帧(此时您已经发明了一个笨拙的 JPEG 图像数据库),这不是问题。
  2. 如评论和答案中所述,您可能会得到一些视觉伪像。这些看起来有多糟糕取决于 material 和您的压缩率。由于您正在生成帧,因此您可以通过确保在发生重大变化时第一帧始终是关键帧来减少这种情况。
  3. 与 MediaCodec 交互的固件在开始生成输出之前可能需要几个帧,即使您从关键帧开始也是如此。在流中四处寻找有延迟成本。参见例如this post。 (有没有想过为什么 DVR 有流畅 fast-forward,但不流畅 fast-backward?)
  4. 通过 SurfaceTexture 传递的 MediaCodec 帧成为 "external" 纹理。这些与普通纹理相比有一些限制——性能可能更差,can't use as color buffer 在 FBO 等中。如果你只是以 30fps 每帧渲染一次,这应该无关紧要。
  5. 由于上述原因,
  6. MediaMetadataRetriever 的 getFrameAtTime() 方法具有 less-than-desirable 性能。虽然您可以通过跳过创建 Bitmap 对象的步骤来节省一些时间,但您不太可能通过自己编写它来获得更好的结果。此外,您传递了 OPTION_CLOSEST_SYNC ,但如果您的所有帧都是同步帧(同样是笨拙的 JPEG 图像数据库),那只会产生您想要的结果。您需要使用 OPTION_CLOSEST.

如果您只是想在纹理上播放电影(或者您的问题可以简化为那样),Grafika 有一些示例。一个可能相关的是 TextureFromCamera,它在可以缩放和旋转的 GLES 矩形上呈现相机视频流。您可以使用来自其他演示之一的 MP4 播放代码替换相机输入。如果你只是向前播放,这会很好用,但如果你想跳过或向后播放,你就会遇到麻烦。

您描述的问题听起来与 2D 游戏开发人员处理的问题非常相似。做他们所做的可能是最好的方法。

是的,有办法从 mp4 视频中提取单帧。

原则上,您似乎在寻找加载纹理的替代方法,通常的方法是 GLUtils.texImage2D(从 Bitmap 填充纹理)。

首先,您应该考虑其他人的建议,并期待压缩后的视觉效果。但假设您的纹理形成相关纹理(例如爆炸),从视频流中获取这些纹理是有意义的。对于不相关的图像,使用 JPG 或 PNG 可以获得更好的结果。并注意 mp4 视频没有 alpha 通道,通常用于纹理。

对于该任务,您不能使用 MediaMetadataRetriever,它不会为您提供提取所有帧所需的准确性。

您必须使用 MediaCodec and MediaExtractor classes。 Android MediaCodec 的文档很详细。

实际上你需要实现一种自定义视频播放器,并添加一个关键功能:帧步。

与此接近的是 Android 的 MediaPlayer,它是完整的播放器,但 1) 缺少 frame-step,并且 2) 更像是 closed-source,因为它是由许多无法扩展且难以研究的本机 C++ 库实现。

我根据创建 frame-by-frame 视频播放器的经验提出这个建议,我通过采用 MediaPlayer-Extended 来做到这一点,它是用纯 java 编写的(没有本机代码),所以您可以将其包含在您的项目中并添加您需要的功能。它适用于 Android 的 MediaCodec 和 MediaExtractor。
在 MediaPlayer class 的某处,您可以为 frameStep 添加函数,并在 PlaybackThread 中添加另一个信号 + 函数以仅解码下一帧(在暂停模式下)。但是,这将取决于您的实施。结果将是您让解码器获取并处理单帧,使用该帧,然后重复下一帧。我做到了,所以我知道这种方法有效。

另一半任务是获取结果。视频播放器(使用 MediaCodec)将帧输出到 Surface。您的任务是获取像素。 我知道如何从这样的表面读取 RGB 位图的方法:您需要创建 OpenGL Pbuffer EGLSurface,让 MediaCodec 渲染到这个表面(Android 的 SurfaceTexture),然后从这个表面读取像素。这是另一个不平凡的任务,您需要创建着色器来渲染 EOS 纹理(表面),并使用 GLES20.glReadPixels 将 RGB 像素获取到 ByteBuffer 中。然后将此 RGB 位图上传到您的纹理中。
但是,当您想要加载纹理时,您可能会发现如何将视频帧直接渲染到纹理中并避免移动像素的优化方法。

希望这对您有所帮助,祝您实施顺利。

实际上我想 post 我当前的实施。

这里是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);

欢迎提问