如何正确使用带有 YUV_420_888 和 MediaCodec 的 ImageReader 将视频编码为 h264 格式?

Posted

技术标签:

【中文标题】如何正确使用带有 YUV_420_888 和 MediaCodec 的 ImageReader 将视频编码为 h264 格式?【英文标题】:How to correctly use ImageReader with YUV_420_888 and MediaCodec to encode video to h264 format? 【发布时间】:2018-03-06 08:21:11 【问题描述】:

我正在 android 设备上实现一个相机应用程序。目前,我使用Camera2 API和ImageReader获取YUV_420_888格式的图像数据,但我不知道如何将这些数据准确写入MediaCodec。

这是我的问题:

    什么是YUV_420_888

YUV_420_888 的格式不明确,因为它可以是属于YUV420 系列的任何格式,例如YUV420PYUV420PPYUV420SPYUV420PSP,对吧?

通过访问图像的三个平面(#0、#1、#2),我可以获得该图像的 Y(#0)、U(#1)、V(#2) 值。但是这些值的排列在不同的设备上可能不一样。例如,如果YUV_420_888 真正表示YUV420P,则Plane#1 和Plane#2 的大小都是Plane#0 大小的四分之一。如果YUV_420_888 真的是YUV420SP,那么Plane#1 和Plane#2 的大小都是Plane#0 的一半(Plane#1 和Plane#2 都包含U、V 值)。

如果我想将图像的三个平面的这些数据写入MediaCodec,我需要转换成什么样的格式? YUV420、NV21、NV12、...?

    什么是COLOR_FormatYUV420Flexible

COLOR_FormatYUV420Flexible 的格式也是模棱两可的,因为它可以是属于YUV420 系列的任何格式,对吧?如果我将 MediaCodec 对象的KEY_COLOR_FORMAT 选项设置为COLOR_FormatYUV420Flexible,我应该向 MediaCodec 对象输入什么格式的数据(YUV420P、YUV420SP...?)?

    COLOR_FormatSurface怎么样?

我知道 MediaCodec 有自己的表面,如果我将 MediaCodec 对象的KEY_COLOR_FORMAT 选项设置为COLOR_FormatSurface,就可以使用它。并且使用 Camera2 API,我不需要自己将任何数据写入 MediaCodec 对象。我可以排空输出缓冲区。

但是,我需要更改相机中的图像。例如,绘制其他图片,在其上写一些文字,或者插入另一个视频作为 POP(Picture of Picture)。

我可以使用 ImageReader 从 Camera 读取图像,并在重新绘制之后,将新数据写入 MediaCodec 的表面,然后将其排出吗?该怎么做?

EDIT1

我使用COLOR_FormatSurface 和RenderScript 实现了该功能。这是我的代码:

onImageAvailable方法:

public void onImageAvailable(ImageReader imageReader) 
    try 
        try (Image image = imageReader.acquireLatestImage()) 
            if (image == null) 
                return;
            
            Image.Plane[] planes = image.getPlanes();
            if (planes.length >= 3) 
                ByteBuffer bufferY = planes[0].getBuffer();
                ByteBuffer bufferU = planes[1].getBuffer();
                ByteBuffer bufferV = planes[2].getBuffer();
                int lengthY = bufferY.remaining();
                int lengthU = bufferU.remaining();
                int lengthV = bufferV.remaining();
                byte[] dataYUV = new byte[lengthY + lengthU + lengthV];
                bufferY.get(dataYUV, 0, lengthY);
                bufferU.get(dataYUV, lengthY, lengthU);
                bufferV.get(dataYUV, lengthY + lengthU, lengthV);
                imageYUV = dataYUV;
            
        
     catch (final Exception ex) 

    

将 YUV_420_888 转换为 RGB:

public static Bitmap YUV_420_888_toRGBIntrinsics(Context context, int width, int height, byte[] yuv) 
    RenderScript rs = RenderScript.create(context);
    ScriptIntrinsicYuvToRGB yuvToRgbIntrinsic = ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs));

    Type.Builder yuvType = new Type.Builder(rs, Element.U8(rs)).setX(yuv.length);
    Allocation in = Allocation.createTyped(rs, yuvType.create(), Allocation.USAGE_SCRIPT);

    Type.Builder rgbaType = new Type.Builder(rs, Element.RGBA_8888(rs)).setX(width).setY(height);
    Allocation out = Allocation.createTyped(rs, rgbaType.create(), Allocation.USAGE_SCRIPT);


    Bitmap bmpOut = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);

    in.copyFromUnchecked(yuv);

    yuvToRgbIntrinsic.setInput(in);
    yuvToRgbIntrinsic.forEach(out);
    out.copyTo(bmpOut);
    return bmpOut;

媒体编解码器:

mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
...
mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
...
surface = mediaCodec.createInputSurface(); // This surface is not used in Camera APIv2. Camera APIv2 uses ImageReader's surface.

在另一个线程中:

while (!stop) 
    final byte[] image = imageYUV;

    // Do some yuv computation

    Bitmap bitmap = YUV_420_888_toRGBIntrinsics(getApplicationContext(), width, height, image);
    Canvas canvas = surface.lockHardwareCanvas();
    canvas.drawBitmap(bitmap, matrix, paint);
    surface.unlockCanvasAndPost(canvas);

这种方式可行,但性能不佳。它不能输出 30fps 的视频文件(只有 ~12fps)。也许我不应该使用COLOR_FormatSurface 和表面的画布进行编码。计算出的 YUV 数据应直接写入 mediaCodec,无需任何表面进行任何转换。但我仍然不知道该怎么做。

【问题讨论】:

你有没有解决这个问题。如果是,你能发布答案吗? @ShivamPokhriyal 据我所知,没有标准 SDK 的解决方案。 那么您知道任何解决方法吗? 【参考方案1】:

你是对的,YUV_420_888 是一种可以包装不同 YUV 420 格式的格式。规范中仔细解释了U和V平面的排列方式并没有规定,但是有一定的限制;例如如果 U 平面的像素步长为 2,则同样适用于 V(然后底层字节缓冲区可以是 NV21)。

COLOR_FormatYUV420Flexible 是YUV_420_888 的同义词,但它们分别属于不同的类:MediaCodec 和 ImageFormat。

规范解释:

自LOLLIPOP_MR1以来,所有视频编解码器都支持灵活的 YUV 4:2:0 缓冲区。

COLOR_FormatSurface 是一种不透明格式,可为 MediaCodec 提供最佳性能,但这是有代价的:您无法直接读取或操作其内容。如果您需要处理进入 MediaCodec 的数据,则可以选择使用 ImageReader;它是否会比 ByteBuffer 更有效,取决于你做什么以及如何去做。请注意,对于 API 24+,您可以在 C++ 中使用 camera2 和 MediaCodec。

MediaCodec 的宝贵细节资源是http://www.bigflake.com/mediacodec。它引用了 264 编码的 full example。

【讨论】:

谢谢。我尝试通过 COLOR_FormatSurface 对视频进行编码,但性能不佳(请参阅我的 EDIT1 部分)。有没有使用 COLOR_FormatYUV420Flexible(或其他 YUV 格式)编码视频的例子? 如果我理解正确,您打算在 onImageAvailable() 中添加更多代码,以便 imageYUV 将成为相机的编辑副本框架。因为否则没有理由简单地将平面从 image 复制到另一个 ByteBuffer。 无论如何,即使使用渲染脚本,您的显示过程也不是最理想的。直接使用 OpenGL 可能会快得多,它带有一个着色器,可以将 YUV420 输入动态转换为 RGB。 我知道 imageYUV 很奇怪。但实际上我有多个相机来源。我使用变量同时存储每个相机的帧。并使用不同的线程计算此时来自每个摄像头的帧,并将结果写入视频文件。顺便问一下,如何使用OpenGL将YUV420_888转RGB? 诀窍不是将 YUV '转换'为 RGB,而是使用 OpenGL 着色器,它可以将输入作为 YUV 并将其转换为可以显示的纹理。这样,您不需要读取 RGB 数据,它直接进入屏幕缓冲区。请查看this example 以获得一些提示。【参考方案2】:

创建一个 textureID -> SurfaceTexture -> Surface -> Camera 2 -> onFrameAvaliable -> updateTexImage -> glBindTexture -> 绘制一些东西 -> 交换缓冲区到 Mediacodec 的 inputSurface。

【讨论】:

以上是关于如何正确使用带有 YUV_420_888 和 MediaCodec 的 ImageReader 将视频编码为 h264 格式?的主要内容,如果未能解决你的问题,请参考以下文章

在Android camera2下将YUV_420_888转换为位图的图像不正确

Android Camera App 使用 CameraX 将图像保存为 YUV_420_888 格式

camera2 如何从图像读取器侦听器中的 YUV_420_888 图像中获取 Exif 数据

将 Android camera2 api YUV_420_888 转换为 RGB

将 android.media.Image (YUV_420_888) 转换为位图

Android camera2 输出到 ImageReader 格式 YUV_420_888 仍然很慢