如何从MP4逐帧获取? (MediaCodec)

时间:2019-06-27 12:59:16

标签: android opengl-es mp4 mediacodec android-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

我发现有MediaCodec

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

我也在这里MediaCodec get all frames from video

找到了类似的问题

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

所以,仍然有一个问题-是否可以通过帧读取mp4视频?

4 个答案:

答案 0 :(得分:2)

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

MP4是一种视频编解码器,它针对与相邻帧(即运动)具有高度相似性的一系列帧进行了高度优化。它还经过优化,可以按顺序进行解压缩,因此使用“随机访问”方法将非常无效率。

要稍微详细一点,视频编解码器会在其余时间存储关键帧(每秒一帧,但速率会发生变化)和增量帧。关键帧像单独的图像一样被独立压缩,但增量帧存储为与一个或多个其他帧的差异。该算法假定在执行运动补偿后,该差异将非常小。

因此,如果要访问单个增量帧,则代码将必须解压缩附近的关键帧以及将其连接到所需帧的所有增量帧,这比仅使用单帧JPEG慢得多。

简而言之,请使用JPEG或PNG压缩纹理并将其全部添加到单个存档文件中,以保持整洁。

答案 1 :(得分:2)

解决方案看起来像ExtractMpegFramesTest,其中MediaCodec用于从视频帧生成“外部”纹理。在测试代​​码中,将帧渲染到屏幕外的pbuffer中,然后另存为PNG。您只需直接渲染它们即可。

这有一些问题:

  1. MPEG视频不能很好地用作随机访问数据库。 常见的GOP(图片组)结构具有一个“关键帧”(本质上是JPEG图像),后跟14个增量帧,它们仅保留与先前解码帧的差异。因此,如果要帧N,则可能必须先解码帧N-14至N-1。如果您一直向前移动(将电影播放到纹理上)或仅存储关键帧(此时您已经创建了笨拙的JPEG图像数据库),这不是问题。
  2. 如评论和答案中所述,您可能会看到一些视觉瑕疵。这些外观的严重程度取决于材料和压缩率。由于正在生成帧,因此可以通过确保每当有较大变化时,第一帧始终是关键帧来减少此情况。
  3. 即使从关键帧开始,MediaCodec与之交互的固件在开始产生输出之前可能需要几个帧。在流中四处寻找具有延迟成本。参见例如this post。 (是否想知道DVR为何具有顺畅的快进功能,却没有顺畅的快退功能?)
  4. 通过表面纹理传递的
  5. MediaCodec帧成为“外部”纹理。这些与普通纹理相比有一些局限性-性能可能更差,在FBO中can't use as color buffer等。如果您仅以30fps的速度每帧渲染一次,那就没关系了。 由于上述原因,
  6. MediaMetadataRetriever的getFrameAtTime()方法的性能不理想。尽管可以通过跳过创建Bitmap对象的步骤来节省一些时间,但是您自己编写它不太可能获得更好的结果。同样,您传入了OPTION_CLOSEST_SYNC,但是只有当所有帧都是同步帧(同样,JPEG图像的笨拙数据库)时,才会产生想要的结果。您需要使用OPTION_CLOSEST

如果您只是想在纹理上播放电影(或者您的问题可以解决),Grafika提供了一些示例。可能相关的一个是TextureFromCamera,它在可缩放和旋转的GLES矩形上渲染摄像机视频流。您可以用其他演示之一的MP4播放代码替换相机输入。如果您只是在播放,这会很好用,但是如果您想跳过或后退,则会遇到麻烦。

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

答案 2 :(得分:2)

是的,有一种方法可以从mp4视频中提取单个帧。

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

首先,您应该考虑其他人的建议,并期望压缩产生视觉效果。但是,假设您的纹理形成了相关的纹理(例如爆炸),那么从视频流中获取它们就很有意义。对于不相关的图像,使用JPG或PNG可获得更好的效果。另外请注意,mp4视频没有alpha通道,通常用于纹理中。

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

您必须使用MediaCodecMediaExtractor类。详细介绍了MediaCodec的Android文档。

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

与此最接近的是Android的MediaPlayer,它是完整的播放器,但1)缺少框架步长,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位图上传到纹理中。
但是,当您要加载纹理时,您可能会找到优化的方法来将视频帧直接渲染到纹理中,并避免移动像素。

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

答案 3 :(得分:0)

实际上,我想发布当前的实施情况。

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

随便问