Android音视频——Libyuv使用实战

Posted GitLqr

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android音视频——Libyuv使用实战相关的知识,希望对你有一定的参考价值。

近期换部门,从事之前从未接触过的android音视频开发,主要涉及到USB摄像头调用、libyuv处理Nv21图像、直播推流等功能,对应的库有【UVCCamera】【libyuv】等,刚接触没经验也没人带挺难搞的,而且网上资料很凌乱,所以,开此篇总结&汇总一下近期的研究,兴许可以帮助到别人,本人亦是新手,文中如有不正确的地方,欢迎指出点评。

一、libyuv入门

先简单说明一下,不管是Android手机的Camera,或是外接的UVCCamera(免驱摄像头),它们获取到的yuv图像格式都是nv21格式的,针对业务,我们可能需要对摄像头获取到的图像进行各种处理,如:镜像、旋转、缩放、裁剪等。

1、yuv概念

总的来说,我们要做的yuv数据处理,无非就是针对各种图像格式下yuv数据(byte[])的转换、调整。举个例子:

  1. NV21:安卓的模式。存储顺序是先存Y,再存U,再VU交替存储,格式为:YYYYVUVUVU。
  2. I420:又叫YU12,安卓的模式。存储顺序是先存Y,再存U,最后存V,格式为YYYYUUUVVV。

可以看到,NV21与I420(都属于YUV420)之间的差别在于U和V的存储位置,所以,NV21要转换成I420,就必须把NV21中的U和V调整为I420的方式存储即可,其他格式之间的转换以此类推。

2、libyuv概念

libyuv是Google开源的yuv图像处理库,实现对各种yuv数据之间的转换,包括数据转换,裁剪,缩放,旋转。尽管libyuv对yuv数据处理的核心进行了封装,但还是要求开发者对各种格式的区别有所了解,这样才能正常调用对应方法,进行转换。在使用这个库之前,如有时间,建议先去了解下yuv的相关知识,相关的文章推荐如下:

3、libyuv核心方法

通过git下载下来的libyuv源码目录,有几个文件需要我们了解下,分别是:

// 格式转换(NV21、NV12、I420等格式互转)
libyuv\\include\\libyuv\\convert_from.h
// 图像处理(镜像、旋转、缩放、裁剪)
libyuv\\include\\libyuv\\planar_functions.h
libyuv\\include\\libyuv\\rotate.h
libyuv\\include\\libyuv\\scale.h
libyuv\\include\\libyuv\\convert.h

以上的几个头文件中声明了libyuv对yuv数据处理的一些函数,我们后续需要使用到这些函数来处理yuv数据的转换和修改。

二、libyuv进阶

通过上面的入门内容与资料,应该对yuv与libyuv有比较表面的理解了,但要完全理解透还是得靠自己再多看看其他资料才行,下面直接使用libyuv这个库,实现一些实际的代码逻辑,完全干货分享,如有错误请不吝赐教。

1、yuv转换格式

因为libyuv对于图像的处理基本上都是针对i420格式的,所以,不管摄像头获取到的图像格式如何,都需要在进行图像处理之前转换成i420格式才行。这里整理了比较常用的nv21与i420、nv12与i420互转的cpp代码实现:

nv21是Android摄像头获取到的图像格式。
nv12是ios摄像头获取到的图像格式。

// nv21 --> i420
void nv21ToI420(jbyte *src_nv21_data, jint width, jint height, jbyte *src_i420_data) 
    jint src_y_size = width * height;
    jint src_u_size = (width >> 1) * (height >> 1);

    jbyte *src_nv21_y_data = src_nv21_data;
    jbyte *src_nv21_vu_data = src_nv21_data + src_y_size;

    jbyte *src_i420_y_data = src_i420_data;
    jbyte *src_i420_u_data = src_i420_data + src_y_size;
    jbyte *src_i420_v_data = src_i420_data + src_y_size + src_u_size;

    libyuv::NV21ToI420((const uint8 *) src_nv21_y_data, width,
                       (const uint8 *) src_nv21_vu_data, width,
                       (uint8 *) src_i420_y_data, width,
                       (uint8 *) src_i420_u_data, width >> 1,
                       (uint8 *) src_i420_v_data, width >> 1,
                       width, height);


// i420 --> nv21
void i420ToNv21(jbyte *src_i420_data, jint width, jint height, jbyte *src_nv21_data) 
    jint src_y_size = width * height;
    jint src_u_size = (width >> 1) * (height >> 1);

    jbyte *src_nv21_y_data = src_nv21_data;
    jbyte *src_nv21_uv_data = src_nv21_data + src_y_size;

    jbyte *src_i420_y_data = src_i420_data;
    jbyte *src_i420_u_data = src_i420_data + src_y_size;
    jbyte *src_i420_v_data = src_i420_data + src_y_size + src_u_size;


    libyuv::I420ToNV21(
            (const uint8 *) src_i420_y_data, width,
            (const uint8 *) src_i420_u_data, width >> 1,
            (const uint8 *) src_i420_v_data, width >> 1,
            (uint8 *) src_nv21_y_data, width,
            (uint8 *) src_nv21_uv_data, width,
            width, height);


// nv12 --> i420 
void nv12ToI420(jbyte *Src_data, jint src_width, jint src_height, jbyte *Dst_data) 
	// NV12 video size
	jint NV12_Size = src_width * src_height * 3 / 2;
	jint NV12_Y_Size = src_width * src_height;

	// YUV420 video size
	jint I420_Size = src_width * src_height * 3 / 2;
	jint I420_Y_Size = src_width * src_height;
	jint I420_U_Size = (src_width >> 1)*(src_height >> 1);
	jint I420_V_Size = I420_U_Size;

    // src: buffer address of Y channel and UV channel
    jbyte *Y_data_Src = Src_data;
    jbyte *UV_data_Src = Src_data + NV12_Y_Size;
    jint src_stride_y = src_width;
    jint src_stride_uv = src_width;

    //dst: buffer address of Y channel、U channel and V channel
    jbyte *Y_data_Dst = Dst_data;
    jbyte *U_data_Dst = Dst_data + I420_Y_Size;
    jbyte *V_data_Dst = Dst_data + I420_Y_Size + I420_U_Size;
    jint Dst_Stride_Y = src_width;
    jint Dst_Stride_U = src_width >> 1;
    jint Dst_Stride_V = Dst_Stride_U;

    libyuv::NV12ToI420((const uint8 *) Y_data_Src, src_stride_y,
                         (const uint8 *) UV_data_Src, src_stride_uv,
                         (uint8 *) Y_data_Dst, Dst_Stride_Y,
                         (uint8 *) U_data_Dst, Dst_Stride_U,
                         (uint8 *) V_data_Dst, Dst_Stride_V,
                         src_width, src_height);


// i420 --> nv12 
void i420ToNv12(jbyte *src_i420_data, jint width, jint height, jbyte *src_nv12_data) 
    jint src_y_size = width * height;
    jint src_u_size = (width >> 1) * (height >> 1);

    jbyte *src_nv12_y_data = src_nv12_data;
    jbyte *src_nv12_uv_data = src_nv12_data + src_y_size;

    jbyte *src_i420_y_data = src_i420_data;
    jbyte *src_i420_u_data = src_i420_data + src_y_size;
    jbyte *src_i420_v_data = src_i420_data + src_y_size + src_u_size;

    libyuv::I420ToNV12(
            (const uint8 *) src_i420_y_data, width,
            (const uint8 *) src_i420_u_data, width >> 1,
            (const uint8 *) src_i420_v_data, width >> 1,
            (uint8 *) src_nv12_y_data, width,
            (uint8 *) src_nv12_uv_data, width,
            width, height);

2、yuv处理图像

针对常见的图像处理,在这里也整理了一些,主要包括 镜像、旋转、缩放、剪裁。
要注意的是,所有的图像处理,都是基于i420数据格式的!

// 镜像
void mirrorI420(jbyte *src_i420_data, jint width, jint height, jbyte *dst_i420_data) 
    jint src_i420_y_size = width * height;
    // jint src_i420_u_size = (width >> 1) * (height >> 1);
    jint src_i420_u_size = src_i420_y_size >> 2;

    jbyte *src_i420_y_data = src_i420_data;
    jbyte *src_i420_u_data = src_i420_data + src_i420_y_size;
    jbyte *src_i420_v_data = src_i420_data + src_i420_y_size + src_i420_u_size;

    jbyte *dst_i420_y_data = dst_i420_data;
    jbyte *dst_i420_u_data = dst_i420_data + src_i420_y_size;
    jbyte *dst_i420_v_data = dst_i420_data + src_i420_y_size + src_i420_u_size;

    libyuv::I420Mirror((const uint8 *) src_i420_y_data, width,
                       (const uint8 *) src_i420_u_data, width >> 1,
                       (const uint8 *) src_i420_v_data, width >> 1,
                       (uint8 *) dst_i420_y_data, width,
                       (uint8 *) dst_i420_u_data, width >> 1,
                       (uint8 *) dst_i420_v_data, width >> 1,
                       width, height);


// 旋转
void rotateI420(jbyte *src_i420_data, jint width, jint height, jbyte *dst_i420_data, jint degree) 
    jint src_i420_y_size = width * height;
    jint src_i420_u_size = (width >> 1) * (height >> 1);

    jbyte *src_i420_y_data = src_i420_data;
    jbyte *src_i420_u_data = src_i420_data + src_i420_y_size;
    jbyte *src_i420_v_data = src_i420_data + src_i420_y_size + src_i420_u_size;

    jbyte *dst_i420_y_data = dst_i420_data;
    jbyte *dst_i420_u_data = dst_i420_data + src_i420_y_size;
    jbyte *dst_i420_v_data = dst_i420_data + src_i420_y_size + src_i420_u_size;

    //要注意这里的width和height在旋转之后是相反的
    if (degree == libyuv::kRotate90 || degree == libyuv::kRotate270) 
        libyuv::I420Rotate((const uint8 *) src_i420_y_data, width,
                           (const uint8 *) src_i420_u_data, width >> 1,
                           (const uint8 *) src_i420_v_data, width >> 1,
                           (uint8 *) dst_i420_y_data, height,
                           (uint8 *) dst_i420_u_data, height >> 1,
                           (uint8 *) dst_i420_v_data, height >> 1,
                           width, height,
                           (libyuv::RotationMode) degree);
    else
        libyuv::I420Rotate((const uint8 *) src_i420_y_data, width,
                           (const uint8 *) src_i420_u_data, width >> 1,
                           (const uint8 *) src_i420_v_data, width >> 1,
                           (uint8 *) dst_i420_y_data, width,
                           (uint8 *) dst_i420_u_data, width >> 1,
                           (uint8 *) dst_i420_v_data, width >> 1,
                           width, height,
                           (libyuv::RotationMode) degree);
    


// 缩放
void scaleI420(jbyte *src_i420_data, jint width, jint height, jbyte *dst_i420_data, jint dst_width,
               jint dst_height, jint mode) 

    jint src_i420_y_size = width * height;
    jint src_i420_u_size = (width >> 1) * (height >> 1);
    jbyte *src_i420_y_data = src_i420_data;
    jbyte *src_i420_u_data = src_i420_data + src_i420_y_size;
    jbyte *src_i420_v_data = src_i420_data + src_i420_y_size + src_i420_u_size;

    jint dst_i420_y_size = dst_width * dst_height;
    jint dst_i420_u_size = (dst_width >> 1) * (dst_height >> 1);
    jbyte *dst_i420_y_data = dst_i420_data;
    jbyte *dst_i420_u_data = dst_i420_data + dst_i420_y_size;
    jbyte *dst_i420_v_data = dst_i420_data + dst_i420_y_size + dst_i420_u_size;

    libyuv::I420Scale((const uint8 *) src_i420_y_data, width,
                      (const uint8 *) src_i420_u_data, width >> 1,
                      (const uint8 *) src_i420_v_data, width >> 1,
                      width, height,
                      (uint8 *) dst_i420_y_data, dst_width,
                      (uint8 *) dst_i420_u_data, dst_width >> 1,
                      (uint8 *) dst_i420_v_data, dst_width >> 1,
                      dst_width, dst_height,
                      (libyuv::FilterMode) mode);


// 裁剪
void cropI420(jbyte *src_i420_data, jint src_length, jint width, jint height, 
				jbyte *dst_i420_data, jint dst_width, jint dst_height, jint left, jint top)
    jint dst_i420_y_size = dst_width * dst_height;
    jint dst_i420_u_size = (dst_width >> 1) * (dst_height >> 1);

    jbyte *dst_i420_y_data = dst_i420_data;
    jbyte *dst_i420_u_data = dst_i420_data + dst_i420_y_size;
    jbyte *dst_i420_v_data = dst_i420_data + dst_i420_y_size + dst_i420_u_size;

    libyuv::ConvertToI420((const uint8 *) src_i420_data, src_length,
                          (uint8 *) dst_i420_y_data, dst_width,
                          (uint8 *) dst_i420_u_data, dst_width >> 1,
                          (uint8 *) dst_i420_v_data, dst_width >> 1,
                          left, top,
                          width, height,
                          dst_width, dst_height,
                          libyuv::kRotate0, libyuv::FOURCC_I420);

3、jni实现YuvUtil

下面编写YuvUtil.java,并通过jni实现上述方法的调用,需要在自己的libyuv module目录下,分别建议3个文件:

  • src/main/cpp/YuvJni.cpp
  • src/main/java/com/libyuv/util/YuvUtil.java
  • CMakeLists.txt

cpp/libyuv就是Google官方的libyuv源码,偷懒的话,可以直接“借鉴”这个开源项目:【LibyuvDemo】,我也是抄这里的,感谢作者~但请注意,【LibyuvDemo】中的代码是有问题的,主要是YuvJni.cpp的代码逻辑没处理好,下面的YuvJni.cpp是我修复后的代码。

1)YuvJni.cpp

以下是YuvJni.cpp代码实现,因为篇幅太长,不利用阅读,故删去上述已贴出代码,这里只贴出YuvJni.cpp中其余核心代码。
注意,这并非是完全代码,需要整合上面代码后(很简单的~),方可使用。

#include <jni.h>
#include <string>
#include "libyuv.h"

...
---------- 因为篇幅太长,这里去掉了上述重复的代码,需要使用者手动修正! ----------
---------- 1、这里需要添加yuv转换格式代码 ----------
---------- 2、这里需要添加yuv处理图像代码 ----------
...

extern "C"
JNIEXPORT void JNICALL
Java_com_libyuv_util_YuvUtil_yuvCompress(JNIEnv *env, jclass type,
                                         jbyteArray nv21Src, jint width,
                                         jint height, jbyteArray i420Dst,
                                         jint dst_width, jint dst_height,
                                         jint mode, jint degree,
                                         jboolean isMirror) 

    jbyte *src_nv21_data = env->GetByteArrayElements(nv21Src, NULL);
    jbyte *dst_i420_data = env->GetByteArrayElements(i420Dst, NULL);
    jbyte *tmp_dst_i420_data = NULL;

    // nv21转化为i420
    jbyte *i420_data = (jbyte *) malloc(sizeof(jbyte) * width * height * 3 / 2);
    nv21ToI420(src_nv21_data, width, height, i420_data);
    tmp_dst_i420_data = i420_data;

    // 镜像
    jbyte *i420_mirror_data = NULL;
    if(isMirror)
        i420_mirror_data = (jbyte *)malloc(sizeof(jbyte) * width * height * 3 / 2);
        mirrorI420(tmp_dst_i420_data, width, height, i420_mirror_data);
        tmp_dst_i420_data = i420_mirror_data;
    

    // 缩放
    jbyte *i420_scale_data = NULL;
    if(width != dst_width || height != dst_height)
        i420_scale_data = (jbyte *)malloc(sizeof(jbyte) * width * height * 3 / 2);
        scaleI420(tmp_dst_i420_data, width, height, i420_scale_data, dst_width, dst_height, mode);
        tmp_dst_i420_data = i420_scale_data;
        width = dst_width;
        height = dst_height;
    

    // 旋转
    jbyte *i420_rotate_data = NULL;
    if (degree == libyuv::kRotate90 || degree == libyuv::kRotate180 || degree == libyuv::kRotate270)
        i420_rotate_data = (jbyte *)malloc(sizeof(jbyte) * width * height * 3 / 2);
        rotateI420(tmp_dst_i420_data, width, height, i420_rotate_data, degree);
        tmp_dst_i420_data = i420_rotate_data;
    

    // 同步数据
    // memcpy(dst_i420_data, tmp_dst_i420_data, sizeof(jbyte) * width * height * 3 / 2);
    jint len = env->GetArrayLength(i420Dst);
    memcpy(dst_i420_data, tmp_dst_i420_data, len);
    tmp_dst_i420_data = NULL;
    env->ReleaseByteArrayElements(i420Dst, dst_i420_data, 0);

    // 释放
    if(i420_data != NULL) free(i420_data);
    if(i420_mirror_data != NULL) free(i420_mirror_data);
    if(i420_scale_data != NULL) free(i420_scale_data);
    if(i420_rotate_data != NULL) free(i420_rotate_data);



extern "C"
JNIEXPORT void JNICALL
Java_com_libyuv_util_YuvUtil_yuvCropI420(JNIEnv *env, jclass type, jbyteArray src_, jint width,
                                     jint height, jbyteArray dst_, jint dst_width, jint dst_height,
                                     jint left, jint top) 
    //裁剪的区域大小不对
    if (left + dst_width > width || top + dst_height > height) 
        return;
    
    //left和top必须为偶数,否则显示会有问题
    if (left % 2 != 0 || top % 2 != 0) 
        return;
    
	// i420数据裁剪
    jint src_length = env->GetArrayLength(src_);
    jbyte *src_i420_data = env->GetByteArrayElements(src_, NULL);
    jbyte *dst_i420_data = env->GetByteArrayElements(dst_, NULL);
    cropI420(src_i420_data, src_length, width, height, dst_i420_data, dst_width, dst_height, left, top);
    env->ReleaseByteArrayElements(dst_, dst_i420_da

以上是关于Android音视频——Libyuv使用实战的主要内容,如果未能解决你的问题,请参考以下文章

RGBAYUV色彩格式及libyuv的使用

ijkplayer编译 Android 版本的 ijkplayer ⑤ ( 执行 init-android-libyuv.sh | 执行 init-android-soundtouch.sh )

错误记录编译 ijkplayer 报错 (fatal error: libyuv.h: No such file or directory #include “libyuv.h“ )

Android音视频——MediaCodec编码mp4踩坑记录

Android音视频——MediaCodec编码mp4踩坑记录

Android开发VR实战二.播放360°全景视频