近期换部门,从事之前从未接触过的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的相关知识,相关的文章推荐如下:

  • 《音视频基础知识—像素格式YUV》
  • 《Android平台上基于OpenGl渲染yuv视频》
  • 《图文详解YUV420数据格式》
  • 《视频存储格式YUV420 NV12 NV21 i420 YV1》

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 sizejint NV12_Size = src_width * src_height * 3 / 2;jint NV12_Y_Size = src_width * src_height;// YUV420 video sizejint 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 channeljbyte *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 channeljbyte *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转化为i420jbyte *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转化为i420nv21ToI420(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转化为nv21i420ToNv21(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"><LinearLayoutandroid:layout_width="0dp"android:layout_height="match_parent"android:layout_weight="1"android:orientation="vertical"><TextViewandroid:layout_width="match_parent"android:layout_height="wrap_content"android:gravity="center"android:text="本地镜像图像"android:textColor="@android:color/white"/><com.serenegiant.usb.widget.UVCCameraTextureViewandroid:id="@+id/camera_view_L"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_gravity="center"/></LinearLayout><LinearLayoutandroid:layout_width="0dp"android:layout_height="match_parent"android:layout_weight="1"android:orientation="vertical"><TextViewandroid:layout_width="match_parent"android:layout_height="wrap_content"android:gravity="center"android:text="LibYUV处理图像"android:textColor="@android:color/white"/><com.lqr.demo.widget.BitmapSurfaceViewandroid: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();}@Overridepublic void surfaceCreated(SurfaceHolder holder) {mThread = new Thread(this);mThread.start();mIsDrawing = true;}@Overridepublic void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}@Overridepublic void surfaceDestroyed(SurfaceHolder holder) {mIsDrawing = false;}@Overridepublic 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() {@Overridepublic 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);}}@Overridepublic void captureStill(String path, OnCaptureListener listener) {checkReleased();post(new Runnable() {@Overridepublic 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;@Overrideprotected 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);}@Overrideprotected void onStart() {super.onStart();synchronized (mSync) {mUSBMonitor.register();}if (mCameraViewL != null) {mCameraViewL.onResume();}}@Overrideprotected void onStop() {synchronized (mSync) {mCameraHandler.close();mUSBMonitor.unregister();}if (mCameraViewL != null) {mCameraViewL.onPause();}super.onStop();}@Overrideprotected void onDestroy() {synchronized (mSync) {if (mCameraHandler != null) {mCameraHandler.release();mCameraHandler = null;}if (mUSBMonitor != null) {mUSBMonitor.destroy();mUSBMonitor = null;}}mCameraViewL = null;super.onDestroy();}@Overridepublic USBMonitor getUSBMonitor() {return mUSBMonitor;}@Overridepublic void onDialogResult(boolean canceled) {}private CameraViewInterface.Callback mCallback = new CameraViewInterface.Callback() {@Overridepublic void onSurfaceCreated(CameraViewInterface view, Surface surface) {// 当TextureView的Surface被创建时,将其添加至CameraHandler中保存并管理。mCameraHandler.addSurface(surface.hashCode(), surface, false);}@Overridepublic void onSurfaceChanged(CameraViewInterface view, Surface surface, int width, int height) {}@Overridepublic void onSurfaceDestroy(CameraViewInterface view, Surface surface) {// 当TextureView的Surface销毁时,将其从CameraHandler中移除。mCameraHandler.removeSurface(surface.hashCode());}};private USBMonitor.OnDeviceConnectListener mOnDeviceConnectListener = new USBMonitor.OnDeviceConnectListener() {@Overridepublic void onAttach(UsbDevice device) {}@Overridepublic void onDettach(UsbDevice device) {}@Overridepublic 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);}}}@Overridepublic void onDisconnect(UsbDevice device, USBMonitor.UsbControlBlock ctrlBlock) {synchronized (mSync) {// 当检测到USB断开时,关闭CameraHandlerif (mCameraHandler != null) {queueEvent(new Runnable() {@Overridepublic void run() {if (mCameraHandler != null) {mCameraHandler.close();}}}, 0);}}}@Overridepublic void onCancel(UsbDevice device) {}};/*** H.264视频编码数据流*/AbstractUVCCameraHandler.OnEncodeResultListener onEncodeResultListener = new AbstractUVCCameraHandler.OnEncodeResultListener() {@Overridepublic void onEncodeResult(byte[] data, int offset, int length, long timestamp, int type) {// 这里可以使用rtmp进行推流...}@Overridepublic void onRecordResult(String videoPath) {}};/*** 摄像头YUV数据流*/AbstractUVCCameraHandler.OnPreViewResultListener mOnPreViewResultListener = new AbstractUVCCameraHandler.OnPreViewResultListener() {@Overridepublic 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 --> i420byte[] 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 --> nv21YuvUtil.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 --> nv21YuvUtil.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,流畅度还可以,是镜面效果,完美,撒花。

7、Github

https://github.com/GitLqr/LQRLibyuv

Android音视频——Libyuv使用实战相关推荐

  1. Android 音视频开发学习思路

    Android 音视频开发这块目前的确没有比较系统的教程或者书籍,网上的博客文章也都是比较零散的.只能通过一点点的学习和积累把这块的知识串联积累起来. 初级入门篇: Android 音视频开发(一) ...

  2. 谈谈对Android音视频开发的探究

    ​ 在日常生活中,视频类应用占据了我们越来越多的时间,各大公司也纷纷杀入这个战场,不管是抖音.快手等短视频类型,虎牙.斗鱼等直播类型,腾讯视频.爱奇艺.优酷等长视频类型,还是Vue.美拍等视频编辑美颜 ...

  3. 那些年,Android音视频开发那些事儿

    音视频开发的主要应用有哪些? 音频播放器,录音机,语音电话,音视频监控应用,音视频直播应用,音频编辑/处理软件,蓝牙耳机/音箱,等等 1.视频监控类 (JNI+应用层开发) 从硬件到嵌入式再到软件,涉 ...

  4. 《Android 音视频开发》《 Android 进阶解密》 书籍赠送

    今年听了一年的 "Android不行了..." 之类的谣言,这不都年底了嘛,也没看见哪个公司停止了安卓开发需求. 最近又出现了很多关于"互联网寒冬..."之类的 ...

  5. Android 音视频开发入门指南

    最近收到很多网友通过邮件或者留言说想学习音视频开发,该如何入门,我今天专门写篇文章统一回复下吧. 音视频这块,目前的确没有比较系统的教程或者书籍,网上的博客文章也都是比较零散的,希望我后面能挤出时间整 ...

  6. android音频开发6,Android 音视频开发(一) : 通过三种方式绘制图片

    想要逐步入门音视频开发,就需要一步步的去学习整理,并积累.本文是音视频开发积累的第一篇. 对应的要学习的内容是:在 Android 平台绘制一张图片,使用至少 3 种不同的 API,ImageView ...

  7. android硬编码封装mp4,【Android 音视频开发打怪升级:音视频硬解码篇】四、音视频解封和封装:生成一个MP4...

    [声 明] 首先,这一系列文章均基于自己的理解和实践,可能有不对的地方,欢迎大家指正. 其次,这是一个入门系列,涉及的知识也仅限于够用,深入的知识网上也有许许多多的博文供大家学习了. 最后,写文章过程 ...

  8. Android音视频之AudioRecord录音(一)

    Android音视频之AudioRecord录音(一) 在音视频开发中,录音当然是必不可少的.首先我们要学会单独的录音功能,当然这里说的录音是指用AudioRecord来录音,读取录音原始数据,读到的 ...

  9. android音视频指南-支持的媒体格式

    android音视频指南-支持的媒体格式 本文描述了Android平台提供的媒体编解码器.容器和网络协议支持. 作为应用程序开发人员,您可以使用任何可以在任何Android设备上使用的媒体编解码器,包 ...

  10. “音”你而来,“视”而可见 腾讯云+社区音视频技术开发实战沙龙圆满结束...

    欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由腾讯云技术沙龙团队发布于腾讯云云+社区 近年来,得益于移动互联网的普及和智能终端设备的广泛应用,短视频.直播.在线教学等音视频通信模式 ...

最新文章

  1. 随机生成十六进制颜色
  2. Thirft框架介绍
  3. Androidstudio坑
  4. IPM analysis request DB table
  5. 真的,太多人辜负了.NET5!
  6. oracle单节点加入rac,实验:Oracle单节点RAC添加节点
  7. 2.Mysql数据库的优化技术(1)
  8. 4x对角线之和c语言,如图,▱ABCD的对角线AC、BD交于点O,EF过点O且与BC、AD分别交于点E、F.试猜想线段AE、CF的关系,并说明理由.——青夏教育精英家教网——...
  9. 微信开发之微信公众平台基本配置--服务器token验证(Java实现)
  10. 9008刷机 小米max2_小米max2线刷包_小米max2刷机包_小米max2固件包_小米max2救砖包 - 线刷宝ROM中心...
  11. 玩客云 装java_玩客云内置 eMMC 存储刷入 armbian
  12. word pdf 互转
  13. php strtotime 2099,PHP的strtotime()函数2039年bug问题
  14. CWnd与HWND的区别与联系(相互转换)完全解析
  15. STM32F105 UART4 发送异常及解决
  16. 现在做硬件工程师还有前途吗?
  17. 练习:银行复利计算(用 for 循环解一道初中小题)
  18. Gensim官方介绍翻译
  19. 从材料硕士到算法工程师的转行之路,有三不建议
  20. Echart饼图-圆形图修改重新绘制

热门文章

  1. TCL嵌入式测试技术在Comware V7系统中的应用
  2. Windows文件传输小工具,网络传输文件,内网传输
  3. 【玩儿法】真不是装X 为了不加班我一直用这些小众软件
  4. windows捕获串口数据_如何下载和安装用于Windows数据包捕获的Npcap库?
  5. 2017 ACM-ICPC 亚洲区(青岛赛区)网络赛 HDU 6206 1001 Apple(三角形外接圆圆心和半径)
  6. 使用Python自动下载Himawari-8(葵花8)数据产品——以AOD L3级日均数据和小时数据为例
  7. win10蓝牙无法连接
  8. 当数据库查询速度过慢时,从哪几个方面排查解决?
  9. java 正则表达式 tab_JAVA 正则表达式 (超详细)
  10. android 4.4.2海信电视,ROOT海信电视Android4.0的详细步骤