前语
近期触摸项目傍边的视频录制项目,测试人员提出了一个bug:说项目录制的视频,会断断续续,每隔一段时间就会停住,然后再继续播映。当前项目是选用视频,音频分轨录制,然后再用ffmpeg合成视频。通过走代码逻辑,发现出现bug的原因是因为短少视频帧数,定义的合成帧数以及录制帧数是24,但是在yuv数据转化的过程中,耗时间比较长,导致了丢帧的情况。因为现在项目里面所有的转化都是通过 java 方法转化而成。So,现在需求选用google的libyuv进行格式转化,提高速度。
为什么需求转化?
那么有些聪明的小明就要问了,直接拿到yuv数据录制不就好了吗,为什么需求转化呢? 当然不行,转化是有必要的。因为nv21是Android摄像头回来的数据。我们是通过MediaCodec生成H.264文件。MediaCodec运用需求创立并设置好 MediaFormat 对象,而MediaFormat 运用的是 COLOR_FormatYUV420SemiPlanar,也便是 NV12 形式,那么就得做一个转化,把 NV21 转化到 NV12 。
具体运用代码:
MediaFormat mediaFormat;
if (rotation == 90 || rotation == 270) {
//设置视频宽高
mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, videoHeight, videoWidth);
} else {
mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, videoWidth, videoHeight);
}
//图像数据格式 YUV420(nv12)
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);
//码率
Log.d("输出视频", "码率" + videoWidth * videoHeight);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, videoWidth * videoHeight);
//每秒30帧
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, video_frameRate);
//1秒一个要害帧
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
videoMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
videoMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
videoMediaCodec.start();
关于格式的差异以及Android MediaCodec的了解,可以跳转观看,本文不再描绘: 音视频根底知识—像素格式YUV Android音视频—YUV格式深入浅出 Android MediaCodec 硬编码 H264 文件
libyuv简介
libyuv是Google开源的完结各种YUV与RGB之间彼此转化、旋转、缩放的库。它是跨途径的,可在Windows、Linux、Mac、Android等操作系统,x86、x64、arm架构上进行编译运行,支撑SSE、AVX、NEON等SIMD指令加快。
实践运用
关于Android来说,我们不能直接运用libyuv,需求集成到项目傍边或许编译so动态库才华运用。
1.集成
为了方便,我们直接集成现已完结了的项目里面的module。
LibyuvDemo
这个老哥现已把libyuv 集成而且弄成了一个module,我们只需求下载源码,然后再自己的项目里面添加module就可以直接运用啦!!!但是需求留心的是这个项目里面的YuvUtil的 compressYUV 方法有误,后续需求修正下(后面会说)
当我们下载好了源码之后,在我们项目里面通过AndroidStudio添加module。 添加刚刚下载的源码,里面的libyuv途径,直接finish,剩余的就交给AndroidStudio了
添加完毕之后,项目里就多了一个module,到这儿,基本上就完结了集成。
2.修正代码
前面集成的时候说过,compressYUV方法是有过错的,我们需求修正一下,直接把下面的代码替换掉 compressYUV方法里面的代码就 阔以鸟~~~
extern "C"
JNIEXPORT void JNICALL
Java_com_libyuv_util_YuvUtil_compressYUV(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);
}
这个方法会 针对我们传入的nv21数据,进行紧缩,旋转而且回来yuvI420格式的数据,这关于我们下一步转化nv12有重要作用。参数说明定义方法YuvUtil说得很清楚。
3.添加方法
我们集成的libyuv里面,是没有 I420 转 nv12的方法的,这个方法需求我们自己着手完结。但是我们也没有写过C言语的代码哇。而且我们也不熟悉java与C的彼此调用哇。那怎么办呢?俗话说,好记性不如烂笔头。我奉告你不然直接你自己去了解。所以我们只能先去了解一下了。Android的JNI开发全面介绍与最佳实践
篇幅很长,我们简单过一遍就可以,有爱好的朋友可以具体琢磨琢磨。我们只需求知道我们需求在YuvUtils里面定义一个转化的方法。然后运用native要害字,这要害字便是java调用C的。然后在 C代码也便是YuvJni.cpp文件里面添加对应的方法(留心方法名需求指定具体方位,仿照前面的compressYUV方法的命名即可)
接着我们百度一下 I420转nv12的C端代码,然后我在SharryChoo/LibyuvSample里面找到了一段转化代码如下:
void LibyuvUtil::I420ToNV21(jbyte *src, jbyte *dst, int width, int height) {
jint src_y_size = width * height;
jint src_u_size = src_y_size >> 2;
jbyte *src_y = src;
jbyte *src_u = src + src_y_size;
jbyte *src_v = src + src_y_size + src_u_size;
jint dst_y_size = width * height;
jbyte *dst_y = dst;
jbyte *dst_vu = dst + dst_y_size;
libyuv::I420ToNV21(
(uint8_t *) src_y, width,
(uint8_t *) src_u, width >> 1,
(uint8_t *) src_v, width >> 1,
(uint8_t *) dst_y, width,
(uint8_t *) dst_vu, width,
width, height
);
}
但是我们仍是需求修正一下方法的定义类型才华够让我们运用(仿照compressYUV方法)下面贴出修正后的代码:
// i420 --> nv12
extern "C"
JNIEXPORT void JNICALL
Java_com_libyuv_util_YuvUtil_yuvI420ToNV12(JNIEnv *env, jclass type, jbyteArray i420src,
jbyteArray nv12Dst,
jint width, jint height) {
jbyte *src_i420_data = env->GetByteArrayElements(i420src, NULL);
jbyte *src_nv12_data = env->GetByteArrayElements(nv12Dst, NULL);
jint src_y_size = width * height;
jint src_u_size = src_y_size >> 2;
jbyte *src_y = src_i420_data;
jbyte *src_u = src_i420_data + src_y_size;
jbyte *src_v = src_i420_data + src_y_size + src_u_size;
jint dst_y_size = width * height;
jbyte *dst_y = src_nv12_data;
jbyte *dst_uv = src_nv12_data + dst_y_size;
libyuv::I420ToNV12(
(uint8_t *) src_y, width,
(uint8_t *) src_u, width >> 1,
(uint8_t *) src_v, width >> 1,
(uint8_t *) dst_y, width,
(uint8_t *) dst_uv, width,
width, height
);
}
我们将上面的代码,复制到YuvJni.cpp文件里面,然后在YuvUtil里面定义一个方法:
/**
* 将I420转化为NV2
*
* @param i420Src 原始I420数据
* @param nv12Dst 转化后的NV12数据
* @param width 输出的宽
* @param height 输出的高
*/
public static native void yuvI420ToNV12(byte[] i420Src,byte[] nv12Dst,int width,int height);
到这儿,我们的I420转nv12的方法,就现已添加完毕了。当然,假如还需求其他的方法,我们google一下转化代码,举一反三的往里面添加就好了。那么回到主题,nv21转nv12的姿势,终究是什么呢?
4.项目中运用
因为我们添加的是一个module,那么我们有必要在项目的build.gradle里面添加module才华调用libyuv里面的东西。
dependencies {
api project(":leo-libyuv")
}
然后在我需求运用转化的当地,添加要害的两行代码。
/**
* 编码视频
* @param nv21 nv21 byte数组
* @throws IOException 抛出IO反常
*/
private void encodeVideo(byte[] nv21) throws IOException {
//定义i420 byte数组
byte[] yuvI420 = new byte[nv21.length];
//定义我们需求的nv12数组
byte[] nv12Result = new byte[nv21.length];
//初始化(多次运用构成 anr,弃用)
//YuvUtil.init(videoWidth, videoHeight, videoWidth, videoHeight);
//将nv21转化成I420
YuvUtil.yuvCompress(nv21, videoWidth, videoHeight, yuvI420, videoWidth, videoHeight, 0, rotation, isFrontCamera);
//将I420转化成nv12
YuvUtil.yuvI420ToNV12(yuvI420, nv12Result, videoWidth, videoHeight);
//得到编码器的输入和输出流, 输入流写入源数据 输出流读取编码后的数据
//得到要运用的缓存序列角标
int inputIndex = videoMediaCodec.dequeueInputBuffer(TIMEOUT_USEC);
if (inputIndex >= 0) {
ByteBuffer inputBuffer = videoMediaCodec.getInputBuffer(inputIndex);
inputBuffer.clear();
//把要编码的数据添加进去
inputBuffer.put(nv12Result);
//塞到编码序列中, 等候MediaCodec编码
videoMediaCodec.queueInputBuffer(inputIndex, 0, nv12Result.length, System.nanoTime() / 1000, 0);
}
编码h.264等等一系列操作。
}
到这儿,我们的nv21转nv12的所有姿势,就现已 恁 完毕了。
总结
姿势一共分为三步:
- 继承libyuv到我们的项目傍边。
- 修正libyuv里面的方法,添加对应的I420转nv12的方法。
- 将module添加到我们自己的项目里调用相对应的方法。
当然,这种姿势是一个比较投机取巧的。有时间有精力的话,仍是得好好琢磨琢磨关于libyuv、jni开发、yuv等等相关知识。
相关学习材料:
- 《Android音视频——Libyuv运用实战》
- 《音视频根底知识—像素格式YUV》
- 《Android音视频—YUV格式深入浅出》
- 《Android MediaCodec 硬编码 H264 文件》
- 《Android的JNI开发全面介绍与最佳实践》
- 《LibyuvDemo》
- 《SharryChoo/LibyuvSample》