Android音视频——Libyuv使用实战

作者: GitLqr | 来源:发表于2019-09-20 15:38 被阅读0次

    近期换部门,从事之前从未接触过的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_data, 0);
    }
    
    extern "C"
    JNIEXPORT void JNICALL
    Java_com_libyuv_util_YuvUtil_yuvMirrorI420(JNIEnv *env, jclass type, jbyteArray i420Src,
                                              jint width, jint height, jbyteArray i420Dst) {
        jbyte *src_i420_data = env->GetByteArrayElements(i420Src, NULL);
        jbyte *dst_i420_data = env->GetByteArrayElements(i420Dst, NULL);
        // i420数据镜像
        mirrorI420(src_i420_data, width, height, dst_i420_data);
        env->ReleaseByteArrayElements(i420Dst, dst_i420_data, 0);
    }
    
    extern "C"
    JNIEXPORT void JNICALL
    Java_com_libyuv_util_YuvUtil_yuvScaleI420(JNIEnv *env, jclass type, jbyteArray i420Src,
                                              jint width, jint height, jbyteArray i420Dst,
                                              jint dstWidth, jint dstHeight, jint mode) {
        jbyte *src_i420_data = env->GetByteArrayElements(i420Src, NULL);
        jbyte *dst_i420_data = env->GetByteArrayElements(i420Dst, NULL);
        // i420数据缩放
        scaleI420(src_i420_data, width, height, dst_i420_data, dstWidth, dstHeight, mode);
        env->ReleaseByteArrayElements(i420Dst, dst_i420_data, 0);
    }
    
    extern "C"
    JNIEXPORT void JNICALL
    Java_com_libyuv_util_YuvUtil_yuvRotateI420(JNIEnv *env, jclass type, jbyteArray i420Src,
                                               jint width, jint height, jbyteArray i420Dst, jint degree) {
        jbyte *src_i420_data = env->GetByteArrayElements(i420Src, NULL);
        jbyte *dst_i420_data = env->GetByteArrayElements(i420Dst, NULL);
        // i420数据旋转
        rotateI420(src_i420_data, width, height, dst_i420_data, degree);
        env->ReleaseByteArrayElements(i420Dst, dst_i420_data, 0);
    }
    
    extern "C"
    JNIEXPORT void JNICALL
    Java_com_libyuv_util_YuvUtil_yuvNV21ToI420(JNIEnv *env, jclass type, jbyteArray nv21Src,
                                               jint width, jint height, jbyteArray i420Dst) {
        jbyte *src_nv21_data = env->GetByteArrayElements(nv21Src, NULL);
        jbyte *dst_i420_data = env->GetByteArrayElements(i420Dst, NULL);
        // nv21转化为i420
        nv21ToI420(src_nv21_data, width, height, dst_i420_data);
        env->ReleaseByteArrayElements(i420Dst, dst_i420_data, 0);
    }
    
    extern "C"
    JNIEXPORT void JNICALL
    Java_com_libyuv_util_YuvUtil_yuvI420ToNV21(JNIEnv *env, jclass type, jbyteArray i420Src,
                                               jint width, jint height, jbyteArray nv21Dst) {
    
        jbyte *src_i420_data = env->GetByteArrayElements(i420Src, NULL);
        jbyte *dst_nv21_data = env->GetByteArrayElements(nv21Dst, NULL);
        // i420转化为nv21
        i420ToNv21(src_i420_data, width, height, dst_nv21_data);
        env->ReleaseByteArrayElements(nv21Dst, dst_nv21_data, 0);
    }
    

    2)YuvUtil.java

    以下是YuvUtil.java全部代码,与开源库中的有所不同,修复个别bug,并增加多个图像处理方法及注释。

    提示:原Demo中的YuvUtil#compressYUV()在处理镜像时,会导致图像花屏、app闪退等问题,使用本文中修复后的代码,亲测可稳定处理yuv图像流数据。这里改名为yuvCompress()。

    package com.libyuv.util;
    
    public class YuvUtil {
    
        static {
            System.loadLibrary("yuvutil");
        }
    
        /**
         * YUV数据的基本的处理(nv21-->i420-->mirror-->scale-->rotate)
         *
         * @param nv21Src    原始数据
         * @param width      原始的宽
         * @param height     原始的高
         * @param dst_width  缩放的宽
         * @param i420Dst    目标数据
         * @param dst_height 缩放的高
         * @param mode       压缩模式。这里为0,1,2,3 速度由快到慢,质量由低到高,一般用0就好了,因为0的速度最快
         * @param degree     旋转的角度,90,180和270三种。切记,如果角度是90或270,则最终i420Dst数据的宽高会调换。
         * @param isMirror   是否镜像,一般只有270的时候才需要镜像
         */
        public static native void yuvCompress(byte[] nv21Src, int width, int height, byte[] i420Dst, int dst_width, int dst_height, int mode, int degree, boolean isMirror);
    
        /**
         * yuv数据的裁剪操作
         *
         * @param i420Src    原始数据
         * @param width      原始的宽
         * @param height     原始的高
         * @param i420Dst    输出数据
         * @param dst_width  输出的宽
         * @param dst_height 输出的高
         * @param left       裁剪的x的开始位置,必须为偶数,否则显示会有问题
         * @param top        裁剪的y的开始位置,必须为偶数,否则显示会有问题
         **/
        public static native void yuvCropI420(byte[] i420Src, int width, int height, byte[] i420Dst, int dst_width, int dst_height, int left, int top);
    
        /**
         * yuv数据的镜像操作
         *
         * @param i420Src i420原始数据
         * @param width
         * @param height
         * @param i420Dst i420目标数据
         */
        public static native void yuvMirrorI420(byte[] i420Src, int width, int height, byte[] i420Dst);
    
        /**
         * yuv数据的缩放操作
         *
         * @param i420Src   i420原始数据
         * @param width     原始宽度
         * @param height    原始高度
         * @param i420Dst   i420目标数据
         * @param dstWidth  目标宽度
         * @param dstHeight 目标高度
         * @param mode      压缩模式 ,0~3,质量由低到高,一般传入0
         */
        public static native void yuvScaleI420(byte[] i420Src, int width, int height, byte[] i420Dst, int dstWidth, int dstHeight, int mode);
    
        /**
         * yuv数据的旋转操作
         *
         * @param i420Src i420原始数据
         * @param width
         * @param height
         * @param i420Dst i420目标数据
         * @param degree  旋转角度
         */
        public static native void yuvRotateI420(byte[] i420Src, int width, int height, byte[] i420Dst, int degree);
    
        /**
         * 将NV21转化为I420
         *
         * @param nv21Src 原始I420数据
         * @param width   原始的宽
         * @param width   原始的高
         * @param i420Dst 转化后的NV21数据
         */
        public static native void yuvNV21ToI420(byte[] nv21Src, int width, int height, byte[] i420Dst);
    
        /**
         * 将I420转化为NV21
         *
         * @param i420Src 原始I420数据
         * @param width   原始的宽
         * @param width   原始的高
         * @param nv21Src 转化后的NV21数据
         **/
        public static native void yuvI420ToNV21(byte[] i420Src, int width, int height, byte[] nv21Src);
    }
    
    

    3)CMakeLists.txt

    CMakeLists.txt全部内容如下:

    cmake_minimum_required(VERSION 3.4.1)
    include_directories(src/main/cpp/libyuv/include)
    add_subdirectory(src/main/cpp/libyuv ./build)
    aux_source_directory(src/main/cpp SRC_FILE)
    add_library(yuvutil SHARED ${SRC_FILE})
    find_library(log-lib log)
    target_link_libraries(yuvutil ${log-lib} yuv)
    

    4)build.gradle

    需要在module的build.gradle中指定下NDK的相关配置:

    android {
        defaultConfig {
            ...
            externalNativeBuild {
                cmake {
                    cppFlags ""
                }
            }
        }
        externalNativeBuild {
            cmake {
                path 'CMakeLists.txt'
            }
        }
    }
    

    5)编译so动态库

    通过点击执行 Build->Mark Module 'libyuv' ,编译完成后,在build/intermediates/cmake目录下,可以得到各平台的so库文件了。

    注意,如果你想生成包含armeabi平台的so动态库,那么需要在local.properties中指定低版本的NDK,比如:r14b。
    点击【旧版NDK下载页面】,找到你想使用的NDK版本下载后配置下即可,我建议用r14b。

    三、libyuv实战

    需求:

    1. 使用UVCCamera(免驱摄像头)充当Android设备前置摄像头,获取实时视频图像数据。
    2. APP需要显示2个图像窗口,窗口1显示UVCCamera实时图像,窗口2显示使用YuvUtil处理过后的yuv数据图像。

    实现:

    1. 使用 saki4510t的【UVCCamera】 实现USB摄像头的启动和预览。
    2. 使用 YuvUtil对yuv数据进行各种处理后,再利用YuvImage将yuv转成Bitmap。
    3. 最后,通过SurfaceView将转换后的Bitmap绘制并显示出来。

    1、界面布局

    根据上述需求,在布局中放置2个图像窗口控件,分别是

    1. UVCCameraTextureView:用于UVCCamera直接显示摄像头的预览图像。
    2. BitmapSurfaceView:用于绘制Bitmap的SurfaceView。
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent"
                  android:background="@android:color/black"
                  android:orientation="horizontal">
    
        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:orientation="vertical">
    
            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center"
                android:text="本地镜像图像"
                android:textColor="@android:color/white"/>
    
            <com.serenegiant.usb.widget.UVCCameraTextureView
                android:id="@+id/camera_view_L"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_gravity="center"/>
    
        </LinearLayout>
    
        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:orientation="vertical">
    
            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center"
                android:text="LibYUV处理图像"
                android:textColor="@android:color/white"/>
    
            <com.lqr.demo.widget.BitmapSurfaceView
                android:id="@+id/camera_view_R"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_gravity="center"/>
    
        </LinearLayout>
    
    </LinearLayout>
    

    2、BitmapSurfaceView

    很简单,在子线程中,不断使用SurfaceHolder+Canvas绘制Bitmap而已。
    要绘制的Bitmap由外界通过 BitmapSurfaceView#drawBitmap(Bitmap bitmap) 方法传入。

    /**
     * @创建者 LQR
     * @时间 2019/9/18
     * @描述 专门绘制Bitmap的SurfaceView
     */
    public class BitmapSurfaceView extends SurfaceView implements SurfaceHolder.Callback, Runnable {
    
        private SurfaceHolder mHolder;
        private Thread mThread;
        private boolean mIsDrawing;
        private Bitmap mBitmap;
        private Paint mPaint;
    
        public BitmapSurfaceView(Context context) {
            this(context, null);
        }
    
        public BitmapSurfaceView(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public BitmapSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            mHolder = getHolder();
            mHolder.addCallback(this);
            mPaint = new Paint();
        }
    
        @Override
        public void surfaceCreated(SurfaceHolder holder) {
            mThread = new Thread(this);
            mThread.start();
            mIsDrawing = true;
        }
    
        @Override
        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        }
    
        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {
            mIsDrawing = false;
        }
    
        @Override
        public void run() {
            while (mIsDrawing) {
                try {
                    if (mHolder != null && mBitmap != null) {
                        Canvas canvas = mHolder.lockCanvas();
                        canvas.drawBitmap(mBitmap, 0, 0, mPaint);
                        mHolder.unlockCanvasAndPost(canvas);
                        Thread.sleep(10);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    
        public void drawBitmap(final Bitmap bitmap) {
            post(new Runnable() {
                @Override
                public void run() {
                    mBitmap = bitmap;
                }
            });
        }
    }
    

    至此,布局完成,下面是逻辑代码。

    3、UVCCamera本地预览镜像

    需求是将USB摄像头充当设备的前置摄像头,所以需要将摄像头捕获到的图像进行镜像处理,需要自定义一个UVCCameraHandler,具体看代码注释。

    镜像:就是将图像左右像素对调,从而看起来的效果像照镜子一样。
    UVCCameraHandler:是UVCCamera开源库中的摄像头控制类,用于控制摄像头的开启、预览、监听等功能。

    /**
     * @创建者 LQR
     * @时间 2019/9/18
     * @描述 自定义的UVCCameraHandler
     * <p>
     * 参照{@link com.serenegiant.usb.common.UVCCameraHandlerMultiSurface},对RendererHolder进行设置,
     * 实现SurfaceView或TextureView图像本地镜像功能,关键API:
     * mRendererHolder = new RendererHolder(thread.getWidth(), thread.getHeight(), null);
     * mRendererHolder.setMirror(IRendererCommon.MIRROR_HORIZONTAL);
     */
    public class MyUVCCameraHandler extends AbstractUVCCameraHandler {
    
        public static final MyUVCCameraHandler createHandler(
                final Activity parent, final CameraViewInterface cameraView,
                final int width, final int height) {
            return createHandler(parent, cameraView, 1, width, height, UVCCamera.FRAME_FORMAT_MJPEG, UVCCamera.DEFAULT_BANDWIDTH);
        }
    
        public static final MyUVCCameraHandler createHandler(
                final Activity parent, final CameraViewInterface cameraView,
                final int encoderType, final int width, final int height, final float bandwidthFactor) {
            return createHandler(parent, cameraView, encoderType, width, height, UVCCamera.FRAME_FORMAT_MJPEG, bandwidthFactor);
        }
    
        public static final MyUVCCameraHandler createHandler(
                final Activity parent, final CameraViewInterface cameraView,
                final int encoderType, final int width, final int height) {
            return createHandler(parent, cameraView, encoderType, width, height, UVCCamera.FRAME_FORMAT_MJPEG, UVCCamera.DEFAULT_BANDWIDTH);
        }
    
        public static final MyUVCCameraHandler createHandler(
                final Activity parent, final CameraViewInterface cameraView,
                final int encoderType, final int width, final int height, final int format) {
            return createHandler(parent, cameraView, encoderType, width, height, format, UVCCamera.DEFAULT_BANDWIDTH);
        }
    
        public static final MyUVCCameraHandler createHandler(
                final Activity parent, final CameraViewInterface cameraView,
                final int encoderType, final int width, final int height, final int format, final float bandwidthFactor) {
            final CameraThread thread = new CameraThread(MyUVCCameraHandler.class, parent, cameraView, encoderType, width, height, format, bandwidthFactor);
            thread.start();
            return (MyUVCCameraHandler) thread.getHandler();
        }
    
        private RendererHolder mRendererHolder;
    
        protected MyUVCCameraHandler(CameraThread thread) {
            super(thread);
            mRendererHolder = new RendererHolder(thread.getWidth(), thread.getHeight(), null);
            mRendererHolder.setMirror(IRendererCommon.MIRROR_HORIZONTAL);
        }
    
        public synchronized void release() {
            if (mRendererHolder != null) {
                mRendererHolder.release();
                mRendererHolder = null;
            }
            super.release();
        }
    
        public synchronized void resize(int width, int height) {
            super.resize(width, height);
            if (mRendererHolder != null) {
                mRendererHolder.resize(width, height);
            }
        }
    
        public synchronized void startPreview() {
            checkReleased();
            if (mRendererHolder != null) {
                super.startPreview(mRendererHolder.getSurface());
            } else {
                throw new IllegalStateException();
            }
        }
    
        public synchronized void addSurface(int surfaceId, Surface surface, boolean isRecordable) {
            checkReleased();
            mRendererHolder.addSurface(surfaceId, surface, isRecordable);
        }
    
        public synchronized void removeSurface(int surfaceId) {
            if (mRendererHolder != null) {
                mRendererHolder.removeSurface(surfaceId);
            }
        }
    
        @Override
        public void captureStill(String path, OnCaptureListener listener) {
            checkReleased();
            post(new Runnable() {
                @Override
                public void run() {
                    synchronized (MyUVCCameraHandler.this) {
                        if (mRendererHolder != null) {
                            mRendererHolder.captureStill(path);
                            updateMedia(path);
                        }
                    }
                }
            });
        }
    }
    
    

    4、UVCCamera开启图像预览

    这一部分的代码,借鉴【USBCameraTest6】使用多个Surface显示图像的案例,主要的类说明一下:

    1. USBMonitor:与CameraDialog搭配使用,用于检测USB摄像头状态,包括连接、断开等。
    2. MyUVCCameraHandler:前面自定义的UVCCameraHandler,用于多个Surface显示图像,同时可以控制图像是否镜像。
    3. UVCCameraTextureView:USB摄像头的图像预览窗口,该控件可以根据摄像头分辨率调整窗口大小。
    4. BitmapSurfaceView:用于绘制Bitmap的图像窗口(即:专门显示经过YuvUtil处理后的yuv数据图像)。
    /**
     * @创建者 LQR
     * @时间 2019/9/18
     * @描述 UVCCamera + YuvUtil 处理USB摄像头图像数据
     * 
     * 1、使用UVCCamera实现Usb摄像头图像预览。
     * 2、使用YuvUtil进行图像预处理:旋转、裁剪、缩放、镜像。
     */
    public class PreprocessActivity extends BaseActivity implements CameraDialog.CameraDialogParent {
    
        private int WIDTH = UVCCamera.DEFAULT_PREVIEW_WIDTH;
        private int HEIGHT = UVCCamera.DEFAULT_PREVIEW_HEIGHT;
    
        private Object mSync = new Object();
        private USBMonitor mUSBMonitor;
        private MyUVCCameraHandler mCameraHandler;
    
        private UVCCameraTextureView mCameraViewL;
        private BitmapSurfaceView mCameraViewR;
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_preprocess_test);
            mCameraViewL = findViewById(R.id.camera_view_L);
            mCameraViewL.setAspectRatio(WIDTH / (float) HEIGHT);
            mCameraViewL.setCallback(mCallback);
            mCameraViewR = findViewById(R.id.camera_view_R);
    
            synchronized (mSync) {
                mUSBMonitor = new USBMonitor(this, mOnDeviceConnectListener);
                mCameraHandler = MyUVCCameraHandler.createHandler(this, mCameraViewL, WIDTH, HEIGHT);
            }
    
            // 开启UVCCamera授权提示对话框
            CameraDialog.showDialog(this);
        }
    
        @Override
        protected void onStart() {
            super.onStart();
            synchronized (mSync) {
                mUSBMonitor.register();
            }
            if (mCameraViewL != null) {
                mCameraViewL.onResume();
            }
        }
    
        @Override
        protected void onStop() {
            synchronized (mSync) {
                mCameraHandler.close();
                mUSBMonitor.unregister();
            }
            if (mCameraViewL != null) {
                mCameraViewL.onPause();
            }
            super.onStop();
        }
    
        @Override
        protected void onDestroy() {
            synchronized (mSync) {
                if (mCameraHandler != null) {
                    mCameraHandler.release();
                    mCameraHandler = null;
                }
                if (mUSBMonitor != null) {
                    mUSBMonitor.destroy();
                    mUSBMonitor = null;
                }
            }
            mCameraViewL = null;
            super.onDestroy();
        }
    
        @Override
        public USBMonitor getUSBMonitor() {
            return mUSBMonitor;
        }
    
        @Override
        public void onDialogResult(boolean canceled) {
    
        }
    
        private CameraViewInterface.Callback mCallback = new CameraViewInterface.Callback() {
            @Override
            public void onSurfaceCreated(CameraViewInterface view, Surface surface) {
                // 当TextureView的Surface被创建时,将其添加至CameraHandler中保存并管理。
                mCameraHandler.addSurface(surface.hashCode(), surface, false);
            }
    
            @Override
            public void onSurfaceChanged(CameraViewInterface view, Surface surface, int width, int height) {
    
            }
    
            @Override
            public void onSurfaceDestroy(CameraViewInterface view, Surface surface) {
                // 当TextureView的Surface销毁时,将其从CameraHandler中移除。
                mCameraHandler.removeSurface(surface.hashCode());
            }
        };
    
        private USBMonitor.OnDeviceConnectListener mOnDeviceConnectListener = new USBMonitor.OnDeviceConnectListener() {
            @Override
            public void onAttach(UsbDevice device) {
    
            }
    
            @Override
            public void onDettach(UsbDevice device) {
    
            }
    
            @Override
            public void onConnect(UsbDevice device, USBMonitor.UsbControlBlock ctrlBlock, boolean createNew) {
                synchronized (mSync) {
                    // 当检测到USB连接时
                    if (mCameraHandler != null) {
                        // 开启摄像头
                        mCameraHandler.open(ctrlBlock);
                        // 开启预览,CameraHandler会将图像绘制至关联的Surface上
                        mCameraHandler.startPreview();
                        // 开启YUV数据转视频流(H.264编码)
                        mCameraHandler.startRecording(null, onEncodeResultListener);
                        // 设置YUV帧数据监听
                        mCameraHandler.setOnPreViewResultListener(mOnPreViewResultListener);
                    }
                }
            }
    
            @Override
            public void onDisconnect(UsbDevice device, USBMonitor.UsbControlBlock ctrlBlock) {
                synchronized (mSync) {
                    // 当检测到USB断开时,关闭CameraHandler
                    if (mCameraHandler != null) {
                        queueEvent(new Runnable() {
                            @Override
                            public void run() {
                                if (mCameraHandler != null) {
                                    mCameraHandler.close();
                                }
                            }
                        }, 0);
                    }
                }
            }
    
            @Override
            public void onCancel(UsbDevice device) {
    
            }
        };
    
        /**
         * H.264视频编码数据流
         */
        AbstractUVCCameraHandler.OnEncodeResultListener onEncodeResultListener = new AbstractUVCCameraHandler.OnEncodeResultListener() {
            @Override
            public void onEncodeResult(byte[] data, int offset, int length, long timestamp, int type) {
                // 这里可以使用rtmp进行推流...
            }
    
            @Override
            public void onRecordResult(String videoPath) {
                
            }
        };
    
        /**
         * 摄像头YUV数据流
         */
        AbstractUVCCameraHandler.OnPreViewResultListener mOnPreViewResultListener = new AbstractUVCCameraHandler.OnPreViewResultListener() {
            @Override
            public void onPreviewResult(byte[] data) { // data就是摄像头获取到的nv21格式的yuv数据
                try {
                    ...
                    ---------- 1、使用YuvUtil进行yuv数据处理 ----------
                    ---------- 2、将处理后的yuv数据转成Bitmap传给SurfaceView绘制 ----------
                    ...
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        };
    
    }
    
    

    5、YuvUtil对yuv图像进行处理

    这里针对yuv图像处理提供了2个方法,分别是:

    1. yuvProcessAndDraw1():完完全全手动处理好每一步的图像处理,自由度较高,相对的,也麻烦。
    2. yuvProcessAndDraw2():使用YuvUtil.yuvCompress()一步完成除裁剪以外的图像处理操作,比较便捷。

    在上面AbstractUVCCameraHandler.OnPreViewResultListener的onPreviewResult(byte[] data)回调中,可以任意选择这2个方法中的1个进行处理,效果是一样的。

    /**
     * 使用YuvUtil完全手动处理YUV图像数据,要求理解byte[]的创建长度:
     * yuvNV21ToI420():nv21转i420
     * yuvMirrorI420():镜像
     * yuvScaleI420():缩放
     * yuvCropI420():裁剪
     * yuvRotateI420():旋转
     * yuvI420ToNV21():i420转nv21
     *
     * @param data 摄像头获取到的nv21数据
     */
    private void yuvProcessAndDraw1(byte[] data) {
        int width = WIDTH;
        int height = HEIGHT;
    
        // nv21 --> i420
        byte[] nv21Data = data;
        byte[] i420Data = new byte[width * height * 3 / 2];
        YuvUtil.yuvNV21ToI420(nv21Data, width, height, i420Data);
    
        // 镜像
        byte[] i420MirrorData = new byte[width * height * 3 / 2];
        YuvUtil.yuvMirrorI420(i420Data, width, height, i420MirrorData);
        i420Data = i420MirrorData;
    
        // 缩放
        byte[] i420ScaleData = new byte[width * height * 3 / 2];
        int scaleWidth = 320;
        int scaleHeight = 240;
        YuvUtil.yuvScaleI420(i420Data, width, height, i420ScaleData, scaleWidth, scaleHeight, 0);
        i420Data = i420ScaleData;
        width = scaleWidth;
        height = scaleHeight;
    
        // 裁剪
        byte[] i420CropData = new byte[width * height * 3 / 2];
        int cropWidth = 240;
        int cropHeight = 240;
        YuvUtil.yuvCropI420(i420Data, width, height, i420CropData, cropWidth, cropHeight, 0, 0);
        i420Data = i420CropData;
        width = cropWidth;
        height = cropHeight;
    
        // 旋转
        byte[] i420RotateData = new byte[width * height * 3 / 2];
        int degree = 90;
        YuvUtil.yuvRotateI420(i420Data, width, height, i420RotateData, degree);
        i420Data = i420RotateData;
        if (degree == 90 || degree == 270) {
            int temp = width;
            width = height;
            height = temp;
        }
    
        // i420 --> nv21
        YuvUtil.yuvI420ToNV21(i420Data, width, height, nv21Data);
    
        // 绘制图像
        drawSurfaceView(data, width, height);
    }
    
    /**
     * 使用YuvUtil半自动处理YUV图像数据:
     * yuvCompress():nv21转i420、镜像、缩放、旋转
     * yuvCropI420():裁剪
     * yuvI420ToNV21():i420转nv21
     *
     * @param data 摄像头获取到的nv21数据
     */
    private void yuvProcessAndDraw2(byte[] data) {
        int width = WIDTH;
        int height = HEIGHT;
        int dstWidth = 320;
        int dstHeight = 240;
    
        // nv21 --> i420 --> 镜像 --> 缩放 --> 旋转
        byte[] nv21Data = data;
        byte[] i420Data = new byte[dstWidth * dstHeight * 3 / 2];
        int degree = 90;
        YuvUtil.yuvCompress(nv21Data, width, height, i420Data, dstWidth, dstHeight, 0, 90, true);
        // 旋转过后,需要手动校正宽高
        if (degree == 90 || degree == 270) {
            width = dstHeight;
            height = dstWidth;
        } else {
            width = dstWidth;
            height = dstHeight;
        }
    
        // 裁剪
        byte[] i420CropData = new byte[width * height * 3 / 2];
        int cropWidth = 240;
        int cropHeight = 240;
        YuvUtil.yuvCropI420(i420Data, width, height, i420CropData, cropWidth, cropHeight, 0, 0);
        i420Data = i420CropData;
        width = cropWidth;
        height = cropHeight;
    
        // i420 --> nv21
        YuvUtil.yuvI420ToNV21(i420Data, width, height, nv21Data);
    
        // 绘制图像
        drawSurfaceView(data, width, height);
    }
    
    /**
     * 使用SurfaceView绘制Bitmap图像
     * @param data nv21数据
     * @param width 图像宽
     * @param height 图像高
     */
    private void drawSurfaceView(byte[] data, int width, int height) {
        YuvImage yuvImage = new YuvImage(data, ImageFormat.NV21, width, height, null);
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        yuvImage.compressToJpeg(new Rect(0, 0, width, height), 100, out);
        byte[] bytes = out.toByteArray();
        Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
        mCameraViewR.drawBitmap(bitmap);
    }
    

    要注意的点有2个:

    1. 要明白每次创建byte[]时的长度是多少。
    2. 要知道旋转如果是90或270,则宽高需要对调。

    6、效果

    录制了一小段屏幕,左边是使用UVCCameraTextureView预览USB摄像头镜像后图像,右边是使用YuvUtil对yuv数据进行 镜像、旋转、缩放、裁剪 后的图像,分辨率640*480,流畅度还可以,是镜面效果,完美,撒花。

    相关文章

      网友评论

        本文标题:Android音视频——Libyuv使用实战

        本文链接:https://www.haomeiwen.com/subject/iysjuctx.html