近期換部門,從事以前從未接觸過的Android音視頻開發,主要涉及到USB攝像頭調用、libyuv處理Nv21圖像、直播推流等功能,對應的庫有【UVCCamera】、【libyuv】等,剛接觸沒經驗也沒人帶挺難搞的,並且網上資料很凌亂,因此,開此篇總結&彙總一下近期的研究,興許能夠幫助到別人,本人亦是新手,文中若有不正確的地方,歡迎指出點評。php
先簡單說明一下,不論是Android手機的Camera,或是外接的UVCCamera(免驅攝像頭),它們獲取到的yuv圖像格式都是nv21格式的,針對業務,咱們可能須要對攝像頭獲取到的圖像進行各類處理,如:鏡像、旋轉、縮放、裁剪等。html
總的來講,咱們要作的yuv數據處理,無非就是針對各類圖像格式下yuv數據(byte[])的轉換、調整。舉個例子:java
能夠看到,NV21與I420(都屬於YUV420)之間的差異在於U和V的存儲位置,因此,NV21要轉換成I420,就必須把NV21中的U和V調整爲I420的方式存儲便可,其餘格式之間的轉換以此類推。android
libyuv是Google開源的yuv圖像處理庫,實現對各類yuv數據之間的轉換,包括數據轉換,裁剪,縮放,旋轉。儘管libyuv對yuv數據處理的核心進行了封裝,但仍是要求開發者對各類格式的區別有所瞭解,這樣才能正常調用對應方法,進行轉換。在使用這個庫以前,若有時間,建議先去了解下yuv的相關知識,相關的文章推薦以下:git
經過git下載下來的libyuv源碼目錄,有幾個文件須要咱們瞭解下,分別是:github
// 格式轉換(NV2一、NV十二、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數據的轉換和修改。canvas
經過上面的入門內容與資料,應該對yuv與libyuv有比較表面的理解了,但要徹底理解透仍是得靠本身再多看看其餘資料才行,下面直接使用libyuv這個庫,實現一些實際的代碼邏輯,徹底乾貨分享,若有錯誤請不吝賜教。app
由於libyuv對於圖像的處理基本上都是針對i420格式的,因此,無論攝像頭獲取到的圖像格式如何,都須要在進行圖像處理以前轉換成i420格式才行。這裏整理了比較經常使用的nv21與i420、nv12與i420互轉的cpp代碼實現:ide
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);
}
複製代碼
針對常見的圖像處理,在這裏也整理了一些,主要包括 鏡像、旋轉、縮放、剪裁。 要注意的是,全部的圖像處理,都是基於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);
}
複製代碼
下面編寫YuvUtil.java,並經過jni實現上述方法的調用,須要在本身的libyuv module目錄下,分別建議3個文件:
cpp/libyuv就是Google官方的libyuv源碼,偷懶的話,能夠直接「借鑑」這個開源項目:【LibyuvDemo】,我也是抄這裏的,感謝做者~但請注意,【LibyuvDemo】中的代碼是有問題的,主要是YuvJni.cpp的代碼邏輯沒處理好,下面的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);
}
複製代碼
如下是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);
}
複製代碼
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)
複製代碼
須要在module的build.gradle中指定下NDK的相關配置:
android {
defaultConfig {
...
externalNativeBuild {
cmake {
cppFlags ""
}
}
}
externalNativeBuild {
cmake {
path 'CMakeLists.txt'
}
}
}
複製代碼
經過點擊執行 Build->Mark Module 'libyuv' ,編譯完成後,在build/intermediates/cmake目錄下,能夠獲得各平臺的so庫文件了。
注意,若是你想生成包含armeabi平臺的so動態庫,那麼須要在local.properties中指定低版本的NDK,好比:r14b。 點擊【舊版NDK下載頁面】,找到你想使用的NDK版本下載後配置下便可,我建議用r14b。
需求:
實現:
根據上述需求,在佈局中放置2個圖像窗口控件,分別是
<?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>
複製代碼
很簡單,在子線程中,不斷使用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;
}
});
}
}
複製代碼
至此,佈局完成,下面是邏輯代碼。
需求是將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);
}
}
}
});
}
}
複製代碼
這一部分的代碼,借鑑【USBCameraTest6】使用多個Surface顯示圖像的案例,主要的類說明一下:
/** * @建立者 LQR * @時間 2019/9/18 * @描述 UVCCamera + YuvUtil 處理USB攝像頭圖像數據 * * 一、使用UVCCamera實現Usb攝像頭圖像預覽。 * 二、使用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();
}
}
};
}
複製代碼
這裏針對yuv圖像處理提供了2個方法,分別是:
在上面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個:
錄製了一小段屏幕,左邊是使用UVCCameraTextureView預覽USB攝像頭鏡像後圖像,右邊是使用YuvUtil對yuv數據進行 鏡像、旋轉、縮放、裁剪 後的圖像,分辨率640*480,流暢度還能夠,是鏡面效果,完美,撒花。