5

我的目标是使用 Android MediaCodec 解码视频流,然后使用输出图像在本机代码中进行进一步的图像处理。

平台:华硕tf700t android 4.1.1。测试码流:H.264 全高清 @ 24 frm/s

内置 Tegra-3 SoC,我指望硬件支持视频解码。从功能上讲,我的应用程序的行为符合预期:我确实可以访问解码器图像并正确处理它们。但是,我遇到了非常高的解码器 CPU 负载。

在以下实验中,进程/线程负载由 adb shell 中的“top -m 32 -t”测量。为了从“顶部”获得可靠的输出,所有 4 个 cpu 内核都通过运行几个以最低优先级永远循环的线程来强制激活。这可以通过重复执行“cat /sys/devices/system/cpu/cpu[0-3]/online”来确认。为简单起见,只有视频解码,没有音频;并且没有时序控制,因此解码器尽可能快地运行。

第一个实验:运行应用程序,调用 JNI 处理函数,但所有进一步的处理调用都被注释掉了。结果:

  • 吞吐量:25 frm/s
  • 应用程序的 VideoDecoder 线程负载 1%
  • 进程/system/bin/mediaserver的线程Binder_3负载24%

解码速度似乎受 CPU 限制(四核 CPU 的 25%)...启用输出处理时,解码图像正确且应用程序正常工作。唯一的问题:解码的 CPU 负载太高。

经过大量的实验,我考虑给 MediaCodec 一个表面来绘制它的结果。在所有其他方面,代码是相同的。结果:

  • 吞吐量 55 frm/s(很好!!)
  • 应用程序的 VideoDecoder 线程负载 2%
  • 进程 /system/bin/mediaserver 的线程 mediaserver 负载 1%

事实上,视频显示在提供的 Surface 上。由于几乎没有任何 cpu 负载,这必须是硬件加速的......

似乎 de MediaCodec 仅在提供 Surface 时才使用硬件加速?

到现在为止还挺好。我已经倾向于使用 Surface 作为解决方法(不是必需的,但在某些情况下甚至是不错的选择)。但是,如果提供了表面,我将无法访问输出图像!结果是本机代码中的访问冲突。

这真的让我很困惑!我没有在文档http://developer.android.com/reference/android/media/MediaCodec.html中看到任何访问限制的概念。在 google I/O 演示http://www.youtube.com/watch?v=RQws6vsoav8中也没有提到这个方向。

那么:如何使用硬件加速的 Android MediaCodec 解码器并访问原生代码中的图像?如何避免访问冲突?任何帮助表示赞赏!还有任何解释或提示。

我很确定 MediaExtractor 和 MediaCodec 使用正确,因为应用程序功能正常(只要我不提供 Surface)。它仍然是相当实验性的,一个好的 API 设计在待办事项列表中;-)

请注意,两个实验之间的唯一区别是变量 mSurface: null 或 "mDecoder.configure(mediaFormat, mSurface, null, 0);" 中的实际 Surface

初始化代码:

mExtractor = new MediaExtractor();
mExtractor.setDataSource(mPath);

// Locate first video stream
for (int i = 0; i < mExtractor.getTrackCount(); i++) {
    mediaFormat = mExtractor.getTrackFormat(i);
    String mime = mediaFormat.getString(MediaFormat.KEY_MIME);
    Log.i(TAG, String.format("Stream %d/%d %s", i, mExtractor.getTrackCount(), mime));
    if (streamId == -1 && mime.startsWith("video/")) {
        streamId = i;
    }
}

if (streamId == -1) {
    Log.e(TAG, "Can't find video info in " + mPath);
    return;
}

mExtractor.selectTrack(streamId);
mediaFormat = mExtractor.getTrackFormat(streamId);

mDecoder = MediaCodec.createDecoderByType(mediaFormat.getString(MediaFormat.KEY_MIME));
mDecoder.configure(mediaFormat, mSurface, null, 0);

width = mediaFormat.getInteger(MediaFormat.KEY_WIDTH);
height = mediaFormat.getInteger(MediaFormat.KEY_HEIGHT);
Log.i(TAG, String.format("Image size: %dx%d format: %s", width, height, mediaFormat.toString()));
JniGlue.decoutStart(width, height);

解码器循环(在单独的线程中运行):

ByteBuffer[] inputBuffers = mDecoder.getInputBuffers();
ByteBuffer[] outputBuffers = mDecoder.getOutputBuffers();

while (!isEOS && !Thread.interrupted()) {
    int inIndex = mDecoder.dequeueInputBuffer(10000);
    if (inIndex >= 0) {
        // Valid buffer returned
        int sampleSize = mExtractor.readSampleData(inputBuffers[inIndex], 0);
        if (sampleSize < 0) {
            Log.i(TAG, "InputBuffer BUFFER_FLAG_END_OF_STREAM");
            mDecoder.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
            isEOS = true;
        } else {
            mDecoder.queueInputBuffer(inIndex, 0, sampleSize, mExtractor.getSampleTime(), 0);
            mExtractor.advance();
        }
    }

    int outIndex = mDecoder.dequeueOutputBuffer(info, 10000);
    if (outIndex >= 0) {
        // Valid buffer returned
        ByteBuffer buffer = outputBuffers[outIndex];
        JniGlue.decoutFrame(buffer, info.offset, info.size);
        mDecoder.releaseOutputBuffer(outIndex, true);
    } else {
        // Some INFO_* value returned
        switch (outIndex) {
        case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
            Log.i(TAG, "RunDecoder: INFO_OUTPUT_BUFFERS_CHANGED");
            outputBuffers = mDecoder.getOutputBuffers();
            break;
        case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
            Log.i(TAG, "RunDecoder: New format " + mDecoder.getOutputFormat());
            break;
        case MediaCodec.INFO_TRY_AGAIN_LATER:
            // Timeout - simply ignore
            break;
        default:
            // Some other value, simply ignore
            break;
        }
    }

    if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
        Log.d(TAG, "RunDecoder: OutputBuffer BUFFER_FLAG_END_OF_STREAM");
        isEOS = true;
    }
}
4

2 回答 2

4

如果您配置输出 Surface,则解码后的数据将写入可用作 OpenGL ES 纹理的图形缓冲区(通过“外部纹理”扩展)。各种硬件以他们喜欢的格式传递数据,CPU 不必复制数据。

如果您不配置 Surface,则输出将进入java.nio.ByteBuffer. 至少有一个缓冲区副本可以将数据从 MediaCodec 分配的缓冲区ByteByffer获取到您的 . 我希望您看到的是间接成本而不是软件解码成本。

可以通过将输出发送到 a SurfaceTexture,渲染到 FBO 或 pbuffer,然后使用glReadPixels来提取数据来改善问题。如果您从本机代码中读取“直接”ByteBuffer或调用glReadPixels,则可以减少 JNI 开销。这种方法的缺点是您的数据将采用 RGB 而不是 YCbCr。(OTOH,如果您想要的转换可以在 GLES 2.0 片段着色器中表达,您可以让 GPU 代替 CPU 来完成这项工作。)

如另一个答案中所述,不同设备上的解码器ByteBuffer以不同格式输出数据,因此如果可移植性对您很重要,则在软件中解释数据可能不可行。

编辑: Grafika现在有一个使用 GPU 进行图像处理的示例。您可以在此处观看演示视频。

于 2013-03-30T00:12:33.863 回答
0

我在nexus 4上使用mediacodec api并获得QOMX_COLOR_FormatYUV420PackedSemiPlanar64x32Tile2m8ka的输出颜色格式。我认为这种格式是一种硬件格式,只能通过硬件渲染来渲染。有趣的是,我发现当我使用 null 和实际 Surface 为 MediaCodec 配置表面时,输出缓冲区长度将分别更改为实际值和 0。我不知道为什么。我认为您可以在不同的设备上进行一些实验以获得更多结果。关于硬件加速,您可以查看 http://www.saschahlusiak.de/2012/10/hardware-acceleration-on-sgs2-with-android-4-0/

于 2013-03-29T20:50:03.207 回答