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

5

实际上我正在使用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来进行纹理处理,因为每个新帧(32毫秒)与上一个非常相似...

现在我为一个模型使用400帧。对于几何形状,我使用.mtr文件,对于纹理,我使用.pkm文件(因为它针对Android进行了优化),因此我大约有350个.mtr文件(因为某些文件包括子索引)和400个.pkm文件...

这就是我要使用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 ...///
   }
}

编辑3

我在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上执行的方法?
编辑4
我发现了一个叫做MediaCodec的类。

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

我在这里找到了类似的问题MediaCodec从视频中获取所有帧

我明白有一种按字节读取的方法,但不是按帧...

所以,问题仍然存在 - 是否有一种按帧读取mp4视频的方法?


1
“我想把所有的纹理都放在MP4中以进行压缩。”这样做会导致非常糟糕的结果。MPEG压缩依赖于帧与帧之间的相似性;一系列没有相似性或相似性很小的帧将会产生可怕的视觉效果。 - Nicol Bolas
请查看以下内容以了解更多有关Android开发的信息:Android Developer Blog上的游戏纹理文章OpenGL纹理压缩支持面向Android开发人员的图像压缩 - Google I/O 2016 - Morrison Chang
@NicolBolas 是的,我明白了,但是我使用的是同样的模型。我编辑了我的问题,请看一下。 - Sirop4ik
4个回答

13
解决方案类似于ExtractMpegFramesTest,其中使用MediaCodec从视频帧生成“外部”纹理。在测试代码中,帧被渲染到离屏pbuffer中,然后保存为PNG。你只需直接渲染它们。
这里有几个问题:
  1. MPEG视频并不适合作为随机访问数据库。常见的GOP(组图片)结构包含一个“关键帧”(实际上是一个JPEG图像),后面跟着14个增量帧,仅保存与先前解码帧的差异。因此,如果您想要第N帧,则首先必须解码第N-14到N-1帧。如果您始终向前移动(将电影播放到纹理上),或者仅存储关键帧(此时您已经发明了一种笨拙的JPEG图像数据库),这不是问题。
  2. 如评论和答案所述,您可能会得到一些视觉伪影。这看起来很糟糕取决于材料和压缩率。由于您正在生成帧,因此可以通过确保在出现较大更改时,第一帧始终是关键帧来减少此问题。
  3. 与MediaCodec接口交互的固件可能需要多个帧才能开始生成输出,即使您从关键帧开始。在流中进行搜索具有延迟成本。参见例如此帖子。(曾经想过为什么DVR具有平稳的快进,但没有平稳的快退吗?)
  4. 传递到SurfaceTexture的MediaCodec帧变成“外部”纹理。这些与普通纹理相比具有一些限制--性能可能更差,在FBO中不能用作颜色缓冲区等。如果您每秒只渲染一次,那么这并不重要。
  5. 由于上述原因,MediaMetadataRetriever的getFrameAtTime()方法性能不佳。您不太可能通过自己编写它来获得更好的结果,尽管您可以跳过创建Bitmap对象的步骤以节省一些时间。此外,您传入了OPTION_CLOSEST_SYNC,但如果所有帧都是同步帧(再次指出,这是笨拙的JPEG图像数据库),那么它只会产生您想要的结果。您需要使用OPTION_CLOSEST
如果你只是想将电影播放在纹理上(或者你的问题可以简化为此),Grafika 有一些例子。其中一个可能相关的例子是TextureFromCamera,它将相机视频流渲染到可以缩放和旋转的GLES矩形上。您可以使用其他演示中的MP4播放代码替换相机输入。如果您只播放正向,则可以正常工作,但如果要跳过或倒回,则会遇到麻烦。
你描述的问题听起来非常类似于2D游戏开发人员所处理的问题。做他们做的事情可能是最好的方法。

那么,据我所见,没有办法通过索引获取帧?有一种decoder.getFrameByIndex(index)的方法吗?我看到我必须进行所有这些操作decoder post image to texture surface then openGL draw it then I need to read pixels glReadPixels() and then create Bitmap and just now I can use it...所以,问题是 - 没有办法通过这种方式获取帧decoder.setMP4Path(path) decoder.getFrameByIndex(index)吗?抱歉,但我对此还不熟悉... - Sirop4ik
我认为我理解了这个概念。只有一个问题,我正在使用这个cpp示例 https://gist.github.com/alekseytimoshchenko/ad7c1efe7871395b885babaee495a333 ,问题是 - 解码器如何知道继续下一帧?这行代码是否让它前进 AMediaExtractor_advance(d->ex); - Sirop4ik
还有,如何知道我有哪种输出格式?我知道YUV,但根据这些方法https://github.com/latelee/yuv2rgb/blob/master/yuv2rgb.c,有几种`YUV`格式,`YUV422`,`YUV420`... - Sirop4ik
处理帧的高效方法是让视频解码器将它们作为外部纹理传递给GPU。将它们复制到应用程序空间,通过软件将它们转换为RGB,并上传到GPU将增加不必要的开销。MediaCodec解码器可以选择自己的YUV格式,通常为NV12或I420,但在某些设备上(特别是那些使用高通芯片组的设备)可能是专有格式。您需要获取媒体格式对象并查询“AMEDIAFORMAT_KEY_COLOR_FORMAT”键,然后针对特定格式使用YUV转换器。(您的kueblert示例似乎是特定于设备的。) - fadden
@fadden,使用GL纹理对大多数视频都有效,但有时我只会得到一些随机的颜色,就像这个问题所述 https://stackoverflow.com/questions/57504443/mediacodec-render-on-offscreen-gl-texture-notworking-on-some-specific-video 请帮忙看一下。 - Robin
显示剩余4条评论

2
是的,有一种方法可以从mp4视频中提取单帧。
原则上,您似乎正在寻找替代加载纹理的方式,其中通常的方式是使用GLUtils.texImage2D(它从Bitmap填充纹理)。
首先,您应该考虑其他人的建议,并期望压缩会产生视觉伪影。但是假设您的纹理形成相关纹理(例如爆炸),从视频流中获取这些纹理是有意义的。对于不相关的图像,使用JPG或PNG将获得更好的结果。请注意,mp4视频没有alpha通道,通常用于纹理。
对于此任务,您不能使用MediaMetadataRetriever,它无法为您提供提取所有帧所需的准确性。
您需要使用{{link1:MediaCodec}}和{{link2:MediaExtractor}}类。 Android文档中对MediaCodec的介绍非常详细。
实际上,您需要实现一种自定义视频播放器,并添加一个关键功能:帧步进。
与此相似的是 Android 的 MediaPlayer,它是一个完整的播放器,但是它缺少帧步进功能,并且由于它是由许多本地的 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位图上传到你的纹理中。
然而,如果你想要加载纹理,你可能会发现直接将视频帧渲染到你的纹理中,并避免移动像素的优化方法。

希望这有所帮助,祝你实现好运。


看看这个样例github.com/kueblert/AndroidMediaCodec/blob/master/…,我理解这正是我需要的,这个人在这里获取输出缓冲区AMediaCodec_getOutputBuffer(mCodec, status, &bufsize),然后我有了缓冲区,但我不想使用OpenCV,所以我找到了这种方法github.com/latelee/yuv2rgb/blob/master/yuv2rgb.c将YUV转换为RGB...就是这样,我欢迎使用帧的RGB缓冲区。你觉得这很有趣吗? - Sirop4ik
是的,你走对了方向。请注意,你在C++/本地环境中,而我主要使用Java/Kotlin。所以我没有使用C++媒体的经验。 然而,以下是需要学习的内容:https://developer.android.com/ndk/guides/stable_apis 包含本地API,你可能会对libmediandk感兴趣(这就是你上面链接中的那个人使用的),还有SurfaceTexture的jni版本、硬件缓冲区API等等。 重要的一步还包括YUV2RGB,你已经有了线索,这是一个简单的转换,但你必须从C++中获取硬件缓冲区的访问权限,也许你需要了解YUV格式。 - Pointer Null

2
我能理解为什么把所有贴图都放在一个文件中似乎很容易,但这是一个非常糟糕的想法。
MP4是一种视频编解码器,它对具有高度相似性的相邻帧(即运动)进行了高度优化。 它还经过优化以按顺序解压缩,因此使用“随机访问”方法将非常低效。
更详细地说,视频编解码器存储关键帧(每秒钟一个,但速率会改变)和其余时间的增量帧。 关键帧就像单独的图像一样被独立压缩,但增量帧存储为与一个或多个其他帧的差异。 在执行运动补偿之后,算法假定此差异将相当小。
因此,如果您想访问单个增量帧,则代码将必须解压缩附近的关键帧以及连接它与所需帧的所有增量帧,这比仅使用单个帧JPEG要慢得多。
简而言之,请使用JPEG或PNG来压缩贴图,并将它们全部添加到单个归档文件中以保持整洁。

2
不要使用JPEG或PNG格式。请使用适合GPU的实际压缩纹理格式,例如ETC2或ASTC。 - solidpixel
@solidpixel 我明白了,但我使用的是同样的模型。我编辑了我的问题,请看一下。 - Sirop4ik
你好!能否请您看一下这个问题:https://stackoverflow.com/questions/56935547/how-to-get-decode-format-from-mediacodec - Sirop4ik

1
实际上,我想发布我的当前时间的实现。
这是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);

随意提问


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接