上一章简单介绍了代码的编码思路和整体结构,并基本完成了Java层面的逻辑。
接下来我们顺着GpuFilterRender.java->GpuFilterRender.cpp的JNI层过度接口,分析两个注意要点。
JNIEXPORT void JNICALL
Java_org_zzrblog_gpufilter_GpuFilterRender_setRotationCamera(JNIEnv *env, jobject instance,
jint rotation, jboolean flipHorizontal,
jboolean flipVertical) {
// 注意这里flipVertical对应render->setRotationCamera.flipHorizontal
// 注意这里flipHorizontal对应render->setRotationCamera.flipVertical
// 因为Android的预览帧数据是横着的,仿照GPUImage的处理方式。
if (render == NULL) {
render = new GpuFilterRender();
}
render->setRotationCamera(rotation, flipVertical, flipHorizontal);
}
注意setRotationCamera这个接口(调用栈是JClass Activity->JClass CFEScheduler->JMethod setUpCamera->JNIMethod setRotationCamera),这是仿照GPUImage开源工程的处理方式,因为在Android系统当中,onPreviewFrame回调出来的data默认是横向的,所以当横向的数据遇上垂直屏幕的开发需求的时候,水平和垂直的翻转刚好就要互换处理。
继续进入GpuFilterRender->setRotationCamera
void GpuFilterRender::setRotationCamera(int rotation, bool flipHorizontal, bool flipVertical)
{
this->mRotation = rotation;
this->mFlipHorizontal = flipHorizontal;
this->mFlipVertical = flipVertical;
adjustFrameScaling();
}
mViewWidth和mViewHeight是在GLThread 的三大生命周期回调当中传入进来的。由于篇幅的关系,代码就不粘贴上来了,有关自定义GLThread 和 GLRender的知识请查阅以前所写的文章。 (https://blog.csdn.net/a360940265a/article/details/88600962)
接下来调用adjustFrameScaling,从方法名字可以了解这是 根据参数调整帧图缩放比例的,但现在放一下,稍后再仔细分析。
JNI层还有一个函数和比较重要的,就是feedVideoData(调用栈是JClass Activity->JClass CFEScheduler->JCallback Camera.onPreviewFrame->JNIMethod feedVideoData),其作用是缓存Camera预览回调接口的帧数据,用于视频渲染。
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
if( mGpuFilterRender!=null){
final Camera.Size previewSize = camera.getParameters().getPreviewSize();
mGpuFilterRender.feedVideoData(data.clone(), previewSize.width, previewSize.height);
}
}
我们先来看看Java层的回调接口,参数传入previewSize.width,previewSize.height ,但是这里的previewSize是横屏的呢,还是竖屏的呢?data数据都是横向的,显然previewSize也是横向的!(即width>height)
JNIEXPORT void JNICALL
Java_org_zzrblog_gpufilter_GpuFilterRender_feedVideoData(JNIEnv *env, jobject instance,
jbyteArray array, jint width, jint height) {
if (render == NULL) return;
jbyte *nv21_buffer = env->GetByteArrayElements(array, NULL);
jsize array_len = env->GetArrayLength(array);
render->feedVideoData(nv21_buffer, array_len, width, height);
env->ReleaseByteArrayElements(array, nv21_buffer, 0);
}
继续进入GpuFilterRender->feedVideoData内部
void GpuFilterRender::feedVideoData(int8_t *data, int data_len, int previewWidth, int previewHeight)
{
if( mFrameWidth != previewWidth){
mFrameWidth = previewWidth;
mFrameHeight = previewHeight;
adjustFrameScaling();
}
int size = previewWidth * previewHeight;
int y_len = size; // mWidth*mHeight
int u_len = size / 4; // mWidth*mHeight / 4
int v_len = size / 4; // mWidth*mHeight / 4
// nv21数据中 y占1个width*height,uv各占0.25个width*mHeight 共 1.5个width*height
if(data_len < y_len+u_len+v_len)
return;
pthread_mutex_lock(&mutex);
ByteBuffer* p = new ByteBuffer(data_len);
p->param1 = y_len;
p->param2 = u_len;
p->param3 = v_len;
p->wrap(data, data_len);
mNV21Pool.put(p);
pthread_mutex_unlock(&mutex);
}
代码不复杂,思路很清晰,根据YUV的NV21格式,计算其数据长度,然后把数据和长度分别寄存到ByteBuffer对象当中,然后把ByteBuffer的指针压栈到NV21的缓存池当中。(PS:只是指针,并不是对象)(由于篇幅关系ByteBuffer 和 NV21BufferPool 的实现代码就不粘贴上来,同学可以通过传送门到github查看);还有需要提醒的就是,有put就有get,生产者消费者模式,所以要利用线程锁做同步操作;
回头再看看:当第一帧图数据输入时,利用mFrameWidth!=previewWidth作为初始化条件,记录当前的previewWidth和previewHeight,并监测其previewFrameSize有改变的情况下,都将触发一次adjustFrameScaling方法;
那么这个adjustFrameScaling究竟是干啥的呢?从方法名字可以了解这是 根据参数调整帧图缩放比例的。显然与预览图的大小,方向等有关,那么现在就看看这方法的内容实现。
void GpuFilterRender::adjustFrameScaling()
{
//第一步、获取surfaceview的宽高,一般是竖屏的,所以width < height,例如720:1280
float outputWidth = mViewWidth;
float outputHeight = mViewHeight;
//第二步、根据摄像头角度,调整横竖屏的参数值
//默认情况下都会执行width/height互换的代码,如果调用Camera.setDisplayOrientation方法那就看情况而定了
if (mRotation == ROTATION_270 || mRotation == ROTATION_90) {
outputWidth = mViewHeight;
outputHeight = mViewWidth;
}
//互换之后,output变成1280:720,呈现的是一张横屏的画布
//FrameSize = previewSize,默认是横向,例如1024:768
float ratio1 = outputWidth / mFrameWidth;
float ratio2 = outputHeight / mFrameHeight;
float ratioMax = std::max(ratio1, ratio2);
//第三步、根据变换比值,求出“能适配输出载体的”预览图像尺寸
//imageSizeNew相等于outputSize*ratioMax
int imageWidthNew = static_cast(mFrameWidth * ratioMax);
int imageHeightNew = static_cast(mFrameHeight * ratioMax);
//第四步、重新计算图像比例值。新的预览图像尺寸/输出载体(有一项肯定是ratioMax,另外一项非ratioMax)
float ratioWidth = imageWidthNew / outputWidth;
float ratioHeight = imageHeightNew / outputHeight;
//第五步、生成对应的顶点坐标数据 和 纹理坐标数据(关键点)
generateFramePositionCords();
generateFrameTextureCords(mRotation, mFlipHorizontal, mFlipVertical);
//第六步、根据效果调整位置坐标or纹理坐标(难点)
float distHorizontal = (1 - 1 / ratioWidth) / 2;
float distVertical = (1 - 1 / ratioHeight) / 2;
textureCords[0] = addDistance(textureCords[0], distHorizontal); // x
textureCords[1] = addDistance(textureCords[1], distVertical); // y
textureCords[2] = addDistance(textureCords[2], distHorizontal);
textureCords[3] = addDistance(textureCords[3], distVertical);
textureCords[4] = addDistance(textureCords[4], distHorizontal);
textureCords[5] = addDistance(textureCords[5], distVertical);
textureCords[6] = addDistance(textureCords[6], distHorizontal);
textureCords[7] = addDistance(textureCords[7], distVertical);
}
函数的内容分6个步骤,每一步骤都写有关键的注释,这里直接挑重点来说明。第一第二步的处理是要把数据调整一致默认横向。第三第四步是根据输出屏幕和预览图的宽高,找到合适的适配比例,尽量满足一项变换另一项。之后是生成顶点坐标数据和纹理坐标数据,最后一步就是调整这些坐标点,我们先看看generateFramePositionCords 和 generateFrameTextureCords
void GpuFilterRender::generateFramePositionCords()
{
float cube[8] = {
// position x, y
-1.0f, -1.0f, //左下
1.0f, -1.0f, //右下
-1.0f, 1.0f, //左上
1.0f, 1.0f, //右上
};
memset(positionCords, 0, sizeof(positionCords));
memcpy(positionCords, cube, sizeof(cube));
}
void GpuFilterRender::generateFrameTextureCords(int rotation, bool flipHorizontal, bool flipVertical)
{
float tempTex[8]={0};
switch (rotation)
{
case ROTATION_90:{
float rotatedTex[8] = {
1.0f, 1.0f,
1.0f, 0.0f,
0.0f, 1.0f,
0.0f, 0.0f,
};
memcpy(tempTex, rotatedTex, sizeof(rotatedTex));
}break;
case ROTATION_180:{
float rotatedTex[8] = {
1.0f, 0.0f,
0.0f, 0.0f,
1.0f, 1.0f,
0.0f, 1.0f,
};
memcpy(tempTex, rotatedTex, sizeof(rotatedTex));
}break;
case ROTATION_270:{
float rotatedTex[8] = {
0.0f, 0.0f,
0.0f, 1.0f,
1.0f, 0.0f,
1.0f, 1.0f,
};
memcpy(tempTex, rotatedTex, sizeof(rotatedTex));
}break;
default:
case ROTATION_0:{
float rotatedTex[8] = {
0.0f, 1.0f,
1.0f, 1.0f,
0.0f, 0.0f,
1.0f, 0.0f,
};
memcpy(tempTex, rotatedTex, sizeof(rotatedTex));
}break;
}
if (flipHorizontal) {
tempTex[0] = flip(tempTex[0]);
tempTex[2] = flip(tempTex[2]);
tempTex[4] = flip(tempTex[4]);
tempTex[6] = flip(tempTex[6]);
}
if (flipVertical) {
tempTex[1] = flip(tempTex[1]);
tempTex[3] = flip(tempTex[3]);
tempTex[5] = flip(tempTex[5]);
tempTex[7] = flip(tempTex[7]);
}
memset(textureCords, 0, sizeof(textureCords));
memcpy(textureCords, tempTex, sizeof(tempTex));
}
positionCords和textureCords是GpuFilterRender类的私有变量,是一组float的数组。
位置坐标比较简单,就是四个位置点的x,y值;但要注意一点的是,这是竖屏的位置坐标哦。
纹理坐标可能就搞懵很多人了,首先我们看ROTATION_0的数据,这组数据就是标准的Android纹理坐标系顶点。(传统OpenGL纹理坐标系和Android中的纹理坐标系的区别,有疑问的同学请查看以前的文章,https://blog.csdn.net/a360940265a/article/details/79169497)但这组数据是没有旋转角度的纹理坐标,实际情况我们是有偏转角度的,(因为预览图像数据默认是横向的!)这下回头再看看ROTATION_270 / ROTATION_90对应的数据,把头顺时针 / 逆时针各转90°,再看看纹理坐标是否对齐位置了? o(* ̄▽ ̄*)ブ
到这里还没结束哦,生成关键的数据之后,要做适配处理。对应的关键代码如下:
int imageWidthNew = static_cast
(mFrameWidth * ratioMax); int imageHeightNew = static_cast (mFrameHeight * ratioMax); float ratioWidth = imageWidthNew / outputWidth; float ratioHeight = imageHeightNew / outputHeight; float distHorizontal = (1 - 1 / ratioWidth) / 2; float distVertical = (1 - 1 / ratioHeight) / 2; textureCords[0] = addDistance(textureCords[0], distHorizontal); // x textureCords[1] = addDistance(textureCords[1], distVertical); // y textureCords[2] = addDistance(textureCords[2], distHorizontal); textureCords[3] = addDistance(textureCords[3], distVertical); textureCords[4] = addDistance(textureCords[4], distHorizontal); textureCords[5] = addDistance(textureCords[5], distVertical); textureCords[6] = addDistance(textureCords[6], distHorizontal); textureCords[7] = addDistance(textureCords[7], distVertical); / 这里写下dist的推演过程,我们可以反推: float distHorizontal * 2 = 1 - 1 / ratioWidth; --------->distHorizontal*2可以理解为整个水平间距 ratioWidth其实等于imageWidthNew / outputWidth,等价替换以上公式: float distHorizontal*2 = (imageWidthNew-outputWidth)/imageWidthNew; ---->右边通分一下 float distHorizontal*2*imageWidthNew = (imageWidthNew-outputWidth); ---->把分母imageWidthNew移至左方 推算到这其实应该能看出个眉目了,imageSizeNew-outputSize,显然就是计算预览帧图与输出载体的偏差值,左方*2是对半平分的意义,imageSizeNew放回右方其实就是归一化处理。最终distHorizontal其实就是归一化后的预览帧图与输出载体的偏差值,把这个偏差值计算到纹理坐标上,就可以把预览帧图不变形的贴到输出载体上。(但会裁剪掉部分内容)
以上的推算过程,我感觉已经写得很明白了。addDistance是GpuFilterRender的内联函数,是针对纹理坐标进行差值计算的,内容如下:
__inline float addDistance(float coordinate, float distance)
{
return coordinate == 0.0f ? distance : 1 - distance;
};
经过 adjustFrameScaling 之后,顶点坐标和纹理坐标就已经准备就绪,下一章介绍如何利用NV21的视频数据进行高效的渲染。
工程地址:https://github.com/MrZhaozhirong/NativeCppApp 入口文件CameraFilterEncoderActivity