首页 > 解决方案 > 如何从 MP4 逐帧获取?(媒体编解码器)

问题描述

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

然后我需要从我的 Android 上的 MP4 获取它

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

我找到了这个MediaCodec

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

和这个MediaMetadataRetriever

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

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

如果有人使用过 MP4,请告诉我去哪里。

PS我正在使用本机方式(JNI),所以不管怎么做.. Java或本机,但我需要找到方法。

编辑1

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

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

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

编辑2

请看一下Edit1

实际上,我只需要知道可以MP4按帧读取的 Android API 吗?也许某种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 get all frames from video

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

所以,还有一个问题——是否有办法mp4按帧读取视频?

标签: androidopengl-esmp4android-mediacodec

解决方案


该解决方案类似于ExtractMpegFramesTest,其中 MediaCodec 用于从视频帧生成“外部”纹理。在测试代​​码中,帧被渲染到屏幕外 pbuffer,然后保存为 PNG。您只需直接渲染它们。

这样做有几个问题:

  1. MPEG 视频不能很好地用作随机访问数据库。一个常见的 GOP(图片组)结构具有一个“关键帧”(本质上是 JPEG 图像),后跟 14 个增量帧,它们仅保存与前一个解码帧的差异。因此,如果您想要帧 N,您可能必须首先解码帧 N-14 到 N-1。如果您一直在前进(在纹理上播放电影)或者您只存储关键帧(此时您已经发明了一个笨拙的 JPEG 图像数据库),这不是问题。
  2. 如评论和答案中所述,您可能会得到一些视觉伪影。这些看起来有多糟糕取决于材料和您的压缩率。由于您正在生成帧,因此您可以通过确保在发生重大变化时第一帧始终是关键帧来减少这种情况。
  3. MediaCodec 接口的固件在开始产生输出之前可能需要几个帧,即使您从关键帧开始。在流中四处寻找有延迟成本。参见例如这篇文章。(有没有想过为什么 DVR 有平滑的快进,但没有平滑的快退?)
  4. 通过 SurfaceTexture 传递的 MediaCodec 帧成为“外部”纹理。这些与普通纹理相比有一些限制——性能可能更差,不能用作FBO 中的颜色缓冲区等。如果你只是以 30fps 的速度每帧渲染一次,这无关紧要。
  5. getFrameAtTime()由于上述原因,MediaMetadataRetriever 的方法的性能不太理想。通过自己编写它不太可能获得更好的结果,尽管您可以通过跳过创建 Bitmap 对象的步骤来节省一些时间。此外,您通过OPTION_CLOSEST_SYNC了,但只有当您的所有帧都是同步帧时才会产生您想要的结果(同样,笨拙的 JPEG 图像数据库)。你需要使用OPTION_CLOSEST.

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

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


推荐阅读