2019-07-03
关键字:摄像头适配、相机花屏、USB摄像头花屏
1、前言
rk3128 是一套定位于低成本 Android 商用开发板芯片的解决方案。它虽价格低廉、性能一般,但却依然该有的功能都支持。
像摄像头适配在这款芯片中就是有一套完善的支持策略的。
这款芯片支持传统手机形式的前置、后置类型摄像头,也支持市面上常见的USB摄像头。
笔者这次的需求是要适配一款 USB摄像头。笔者手里的 rk3128 开发板运行的是 Android4.4 操作系统。下面是适配简要步骤
2、适配过程
其实整个适配过程很简单,因为主要的接口都有现成的。唯一需要我们做的就是确保一些节点文件的存在与权限而已。
首先来确保 /dev/video* 节点的权限。USB摄像头都会被识别成 /dev/video0 ~ /dev/video* 节点,一个头映射成一个节点。
device/rockchip/rksdk/ueventd.rk30board.rc
在这里要做的也不多,根据自身业务需要修改一下权限即可。
/dev/video0 0666 system camera /dev/video1 0666 system camera /dev/video2 0666 system camera
接下来还可以瞄一眼下面这个文件
systemcore ootdirueventd.rc
这个文件里如果也有关于 /dev/video* 的权限限定操作,建议将它删了。我们一切以上面的 ueventd.rk30board.rc 为准。
其次可以关注一下 Camera 的 HAL 层。这一层是至关重要的。它起到对接驱动与上层服务的之间枢纽的作用。
/hardware/rk29/camera/CameraHal/
还有诸如如下两个目录
.frameworksavservicescameralibcameraservice
.frameworksavcamera
再更上层一点的还有以下目录内的代码文件
.frameworksasecorejavaandroidhardware
最后再往上就到 APK 层了。
其实笔者这一路适配下来都没遇到什么阻碍,rk 原厂 SDK 对这块的功能就已经很完善了。基本上是改了一下权限以后开机插上摄像头就能看到对应节点了。笔者唯一遇到的比较头疼的问题在于点亮摄像头以后。
3、测试摄像头
前面一路的适配工作都异常的顺利。但就在点亮摄像头以后发现了一个大问题:在画面处于 0 度旋转方向上时自带相机的预览画面出现了花屏现象。如下图所示
这是一个很诡异的问题。笔者的开发板是通过系统属性 ro.sf.hwrotation 来控制画面旋转方向的。共可以设置 4 个值:0、90、180、270,分别代表四个画面旋转方向。这四个方向中,仅 0 度会花屏,其它 3 个都正常。而且,花屏状态下拍出来的照片也是正常的。所以这个问题非常诡异。
但这个问题其实要确定原因也不难。我们根据前面的已知条件,可以排除掉摄像头和开发板的硬件问题。所以很快就能将矛头指向软件层面了。
而我们又已确定了花屏状态下拍出来的照片也正常,这也确定了花屏和我们对于相机设定的参数无关。我们的 APK 对于相机的调用也无非就是打开与关闭摄像头以及设置一下图像参数而已。因此这一步我们也可以排除掉是上层 APK 的问题。并且在下载了第三方相机 APK 以后也发现同样会有问题。这更进一步撇除了上层 APK 的原因了。
所以剩下来的就是中间服务层的嫌疑最大了。
中间服务层有 frameworks 层的以及 HAL 层的。笔者这边凭直觉就排除掉 framework 层服务的嫌疑了,直接分析 HAL 层。
那到这,我们来思考一下:一个相机应用从打开到我们能看到图像并能拍照,它背后的实现原理是什么?
肯定得有一个模块专门负责采集数据,从驱动那里将原始画面数据拿上来。也肯定得有一个模块专门负责拍照,在按下快门以后,将当前画面数据截取下来,打包成图片格式并存储起来。必须也少不了有一个模块专门负责画面预览,我们拍照的时机是不确定的,因此屏幕上必须实时预览摄像头当前采集到的画面,这在本质上就是一个实时视频流的播放,说白了就是得有一个播放器来为我们播放实时画面。肯定也还有其它模块,但是一个相机最核心的几个模块估计就这仨了。
捋清楚了相机背后的原理以后,我们再来根据前面的已知条件来分析。花屏状态下拍照功能正常,而且其它旋转角度下显示正常。这就将前面说的模块一和模块二的嫌疑给排除掉了。那就只剩下一个预览模块了,而事实上我们也确实就是预览出现了问题。
那预览模块的核心又是什么呢?是解码!摄像头采集出来的数据是不能直接拿给播放器播放的,这中间需要解一下码。同时还得根据当前画面的旋转角度作一下转换。因此,这个问题拥有最大嫌疑的就是预览模块的解码功能。
rk3128 的 HAL 层里负责预览解码的代码在哪呢?
./hardware/rk29/camera/CameraHal/DisplayAdapter.cpp
在这个代码里有这么一个无限循环函数的定义
void DisplayAdapter::displayThread() { //... }
我们跟踪一下这个函数,可以发现在预览过程中,对于每一画面帧都有一个调用解码函数的操作,如下所示
case CMD_DISPLAY_FRAME: { // ... if((frame->frame_fmt == V4L2_PIX_FMT_YUYV) && (strcmp((mDisplayFormat),CAMERA_DISPLAY_FORMAT_YUV420P)==0)) { if((frame->frame_width == mDisplayWidth) && (frame->frame_height== mDisplayHeight)) arm_yuyv_to_yv12(frame->frame_width, frame->frame_height, (char*)(frame->vir_addr), (char*)mDisplayBufInfo[queue_display_index].vir_addr); } else if((frame->frame_fmt == V4L2_PIX_FMT_YUYV) && (strcmp((mDisplayFormat),CAMERA_DISPLAY_FORMAT_YUV420SP)==0)) { if((frame->frame_width == mDisplayWidth) && (frame->frame_height== mDisplayHeight)) arm_yuyv_to_nv12(frame->frame_width, frame->frame_height, (char*)(frame->vir_addr), (char*)mDisplayBufInfo[queue_display_index].vir_addr); } else if((frame->frame_fmt == V4L2_PIX_FMT_NV12) && (strcmp((mDisplayFormat),CAMERA_DISPLAY_FORMAT_RGB565)==0)) { arm_nv12torgb565(frame->frame_width, frame->frame_height, (char*)(frame->vir_addr), (short int*)mDisplayBufInfo[queue_display_index].vir_addr, mDisplayWidth); } else if((frame->frame_fmt == V4L2_PIX_FMT_NV12) && (strcmp(mDisplayFormat, CAMERA_DISPLAY_FORMAT_YUV420SP)==0)) { #if 1 arm_camera_yuv420_scale_arm(V4L2_PIX_FMT_NV12, V4L2_PIX_FMT_NV12, (char*)(frame->vir_addr), (char*)mDisplayBufInfo[queue_display_index].vir_addr, frame->frame_width, frame->frame_height, mDisplayWidth, mDisplayHeight, false, frame->zoom_value); #else rga_nv12_scale_crop(frame->frame_width, frame->frame_height, (char*)(frame->vir_addr), (short int *)(mDisplayBufInfo[queue_display_index].vir_addr), mDisplayWidth,mDisplayWidth,mDisplayHeight,frame->zoom_value,false,true,false); #endif } // ... }break;
上面标红加粗部分就是不同类型的画面解码函数了。不过很可惜的是,这些代码都是闭源的。它们被封装在下面这个库里
./device/rockchip/common/vpu/lib/libjpeghwenc.so
所以最终的解决办法就是更换一个解码函数即可。如果这上面所有的解码函数都不能解决您的问题,那只能辛苦一点,自己去外部找解码函数来使用了。反正问题的根源就在这。
4、结语
解决问题的时候切忌不可一看到问题立马就扑上去看代码、抓打印,这是属于蛮干型,经常会造成有劳无功的结果的。
正确的解决问题流程应该是像下面这样子的
1、确定问题
2、明确现象与复现方法
3、初步分析,确定该问题背后的代码组成。
4、进一步分析,梳理该问题背后涉及技术的类别,根据现象缩小分析范围
5、开始分析
6、不断思考与总结
我们在解决一个问题的过程中,很可能大多数时候我们接触代码时间只占很少一部分时间,大多数时间都应该用来作前期分析,甚至可以简单粗暴地套用 8020定律 来解释。而且这种方式才是真正高效率的方式。