- 1. 序
- 2. V4L2
- 2.1. ioctls
- VIDIOC_QUERYCAP
- VIDIOC_ENUM_FMT
- VIDIOC_ENUM_FRAMESIZES
- VIDIOC_ENUM_FRAMEINTERVALS
- VIDIOC_TRY_FMT/VIDIOC_S_FMT/VIDIOC_G_FMT
- VIDIOC_S_PARM/VIDIOC_G_PARM
- VIDIOC_S_JPEGCOMP/VIDIOC_G_JPEGCOMP
- VIDIOC_REQBUFS
- VIDIOC_QUERYBUF
- VIDIOC_QBUF/VIDIOC_DQBUF
- VIDIOC_STREAMON/VIDIOC_STREAMOFF
- 2.1. ioctls
- 3. 像素编码格式
- 3.1. RGB
- 3.2. YUV
- 4. Android Camera
- 4.1. Hardware
- CameraFactory
- camera_device
- CameraHardware
- V4L2Camera
- 4.2. Framwork
- JavaSDK层
- class Camera
- mediaserver
- CameraHardwareInterface
- CameraService
- 4.1. Hardware
本文分析的Android源代码来自Android-X86, 对应的版本是5.1, 因此可能与手机上的Android系统有点差异.
V4L2是linux针对摄像头等视频设备的驱动, 应用程序只要对摄像头设备通过open获取文件描述符, 然后就能使用read, write, ioctl等操作对摄像头进行操作了. 在android HAL中同样也是这么做的, 直接对设备的操作相关的代码位于/hardware/libcamera/V4L2Camera.cpp中. 由于我的项目中使用了一个虚拟摄像头设备v4l2loopback, 但是有些android中用到的ioctl该设备的动作不符合预期, 需要进行修改, 所以本小节针对v4l2的做一个介绍, 内容主要来源于v4l2的官方文档.
V4L2是一套大而全的设备驱动, 但是很多功能对于摄像头来说用不到, 在android的HAL中, 用到的ioctl有:
- VIDIOC_QUERYCAP
- VIDIOC_ENUM_FMT
- VIDIOC_ENUM_FRAMESIZES
- VIDIOC_ENUM_FRAMEINTERVALS
- VIDIOC_TRY_FMT/VIDIOC_S_FMT/VIDIOC_G_FMT
- VIDIOC_S_PARM/VIDIOC_G_PARM
- VIDIOC_S_JPEGCOMP/VIDIOC_G_JPEGCOMP
- VIDIOC_REQBUFS
- VIDIOC_QUERYBUF
- VIDIOC_QBUF/VIDIOC_DQBUF
- VIDIOC_STREAMON/VIDIOC_STREAMOFF
接下来就对这些ioctl做一个说明.
2.1 ioctls
VIDIOC_QUERYCAP
用于查询设备的功能和类型, 基本上每个V4L2应用在open设备之后都要用这个ioctl来确定设备的类型. 使用时需要给定一个v4l2_capability类型的数据结构用于传出输出结果, 对于摄像头设备来说, v4l2_capability->capability的V4L2_CAP_VIDEO_CAPTURE位以及V4L2_CAP_STREAMING位需要为1.
V4L2_CAP_VIDEO_CAPTURE表示该设备支持视频捕获, 这也是摄像头的基本功能.
V4L2_CAP_STREAMING表示该设备支持Streaming I/O, 这是一种内存映射的方式来在内核与应用直接传输数据.
VIDIOC_ENUM_FMT
用于查询摄像头支持的图像格式. 使用时需要指定一个v4l2_fmtdesc类型的数据结构作为输出参数. 对于支持多种图像格式的设备来说, 需要设定v4l2_fmtdesc->index然后多次调用这个ioctl, 直到返回EINVAL. ioctl调用成功后, 可以通过v4l2_fmtdesc->pixelformat来获取该设备支持的图像格式, pixelformat可以是:
- V4L2_PIX_FMT_MJPEG
- V4L2_PIX_FMT_JPEG
- V4L2_PIX_FMT_YUYV
- V4L2_PIX_FMT_YVYU等.
VIDIOC_ENUM_FRAMESIZES
获取到图像格式之后, 还要进一步查询该种格式下设备支持的的分辨率, 这个ioctl就是干这个事. 使用是需要指定一个v4l2_frmsizeenum类型的数据结构, 并且设置v4l2_frmsizeenum->pixel_fromat为需要查询的图像格式, v4l2_frmsizeenum->index设置为0.
成功调用后, v4l2_frmivalenum->type可能有三种情况:
- V4L2_FRMSIZE_TYPE_DISCRETE: 可以递增的设置v4l2_frmsizeenum->index来重复调用直到返回EINVAL来获取该种图像格式下所有支持的分辨率. 此时, 可以通过v4l2_frmsizeenum->width和height来获取支持的分辨率的长宽.
- V4L2_FRMSIZE_TYPE_STEPWISE: 此时只有v4l2_frmsizeenum->stepwise是有效的, 并且不能再将index设为其他值重复调用此ioctl.
- V4L2_FRMSIZE_TYPE_CONTINUOUS: STEPWISE的一种特殊情况, 此时同样只有stepwise有效, 并且stepwise.step_width和stepwise.step_height都为1.
上述三种情况中, 第一种很好理解, 就是支持的分辨率. 但是STEPWISE和CONTINUOUS还不知道是什么意思. 可能是任意分辨率都支持的意思?
VIDIOC_ENUM_FRAMEINTERVALS
获取到图像格式以及图像分辨率之后, 还可以查询在这种格式及分辨率下摄像头设备支持的fps. 使用是需要给定v4l2_frmivalenum格式的一个数据结构, 并设置好index=0, pixel_format, width, height.
调用完成后, 同样需要检查v4l2_frmivalenum.type, 同样有DISCRETE, STEPWISE, CONTINUOUS三种情况.
VIDIOC_TRY_FMT/VIDIOC_S_FMT/VIDIOC_G_FMT
这三个ioctl用于设置以及获取图像格式. TRY_FMT和S_FMT的区别在于前者不改变驱动的状态.
需要设置图像格式时一般通过G_FMT先获取当前格式, 再修改参数, 最后S_FMT或者TRY_FMT.
VIDIOC_S_PARM/VIDIOC_G_PARM
用于设置和获取streaming io的参数. 需要指定一个v4l2_streamparm类型的数据结构.
VIDIOC_S_JPEGCOMP/VIDIOC_G_JPEGCOMP
用于设置和获取JPEG格式的相关参数
VIDIOC_REQBUFS
为了在用户程序和内核设备之间交换图像数据, 需要分配一块内存. 这块内存可以在内核设备中分配, 然后用户程序通过mmap映射到用户空间, 也可以在用户空间分配, 然后内核设备进入用户指针IO模式. 这两种情况都可以用这个ioctl来进行初始化. 使用时需要给定一个v4l2_requestbuffers, 并设定好type, memory, count. 可以多次调用这个ioctl来重新设置参数. count设为0表示free 掉所有内存.
VIDIOC_QUERYBUF
VIDIOC_REQBUFS之后, 可以随时通过此ioctl查询buffer的当前状态. 使用时需要给定一个v4l2_buffer类型的数据结构. 并设定好type和index. 其中index的有效值为[0, count-1]. 这里的count是VIDIOC_REQBUFS返回的count.
VIDIOC_QBUF/VIDIOC_DQBUF
VIDIOC_REQBUFS分配了内存后, V4l2设备驱动还不能直接用这些内存, 需要通过VIDIOC_QBUF将分配好的内存中的一帧压入驱动的接收队列中, 然后通过VIDIOC_DQBUF推出一帧数据的内存. 如果是摄像头这样的CAPTURE设备, 那压入的就是一个空的内存区间, 等到摄像头拍摄到数据填充到了这片内存之后, 就能推出一片包含有效图像数据的帧了. 如果摄像头还没完成填充动作, 那么VIDIOC_DQBUF就会阻塞在那, 除非在open时设置了O_NONBLOCK标记位.
VIDIOC_STREAMON/VIDIOC_STREAMOFF
STREAMON表示开始工作, 只有在这个ioctl调用之后, 摄像头才能开始捕捉图像, 填充数据. 相对的, STREAMOFF则表示停止工作, 此时在设备驱动中尚未被DQBUF 的图像会丢失.
由于底层涉及到图像, 摄像头相关的代码中出现了不少像素编码格式, 所以简单了解一下摄像头会遇到的格式.
3.1 RGB
很简单, 一个像素由RGB三种颜色组成, 每种颜色占用8位, 所以一个像素需要3 字节存储. 有些RGB编码格式会用更少的位保存一些颜色, 例如RGB844, 绿色和蓝色分别用4位来保存, 这样一个像素就只需要2字节. 还有RGBA, 增加了一个alpha通道, 这样一个像素就需要32位来保存.
3.2 YUV
YUV同样有三个通道, Y表示亮度, 由RGB三色特定部分叠加到一起, U和V则分别是红色与亮度的差异和蓝色与亮度的差异. Y一般占用8位, 而UV则可以省略, 因此衍生出了YUV444, YUV420, YUV411等一系列编码. 名称中的三位数字表示YUC 三个信道的比例, 例如YUV444表示三种信道1:1:1, 而Y占用8位, 因此一个像素占用24位. YUV420并不是说V就完全省略了, 而是一行4:1:0, 一行4:0:1.
android系统的摄像头预览默认采用YUV420sp编码. YUV420又分为YUV420p和YUV420sp, 两者的区别在于UV数据的存放顺序不一样:
Figure 1: 图片来自http://blog.csdn.net/jefry_xdz/article/details/7931018
4.1 Hardware
在Android-x85 5.1中, HAL层摄像头相关的类之间的关系大概如图所示:
SurfaceSize是对一个Surface的长宽参数的封装. SurfaceDesc是对一个Surface 的长宽参数, fps参数的封装.
V4L2Camera类是对V4L2设备驱动的一层封装, 直接通过ioctl控制V4L2设备.
CameraParameters是对摄像头参数的封装. 其中flatten和unfaltten相当于是对CameraParameters的序列化和反序列化.
camera_device其实是一个类似于抽象类的结构体, 定义了一些列摄像头应该实现的接口, 而CameraHardware继承了这个camera_device, 表示一个摄像头的抽象, 这个类主要实现了android定义的一个摄像头应该实现的动作, 如startPreview. 其底层通过V4L2Camera对象来操作摄像头设备, 每个CameraHardware对象都包含一个V4L2Camera对象. 每个实例还包含了一个CameraParameters对象, 保存摄像头的相关参数.
CameraFactory是一个摄像头的管理类, 整个安卓系统中只有一个实例, 其中通过读配置文件的方式创建了多个CameraHardware实例.
CameraFactory
CameraFactory类扮演着是一个摄像头设备的管理员的角色, 这个类查询手机上有几个摄像头, 这几个摄像头的设备路径分别是什么, 旋转角度是多少, 朝向(前置还是后置)等信息. Android-x86通过读取一个配置文件来获取机器上有多少个摄像头:
hardware/libcamera/CameraFactory.cpp
配置文件的路径位于/etc/camera.cfg, 格式为”front/back path_to_device orientation”, 例如”front /dev/video0 0″
值得一提的另一个函数是 , APP在打开摄像头的过程中会通过这个函数获取摄像头:
从第13行可知, 当android系统启动的时候, 就已经构建好了CameraFactory对象并且通过配置文件读取到机器中有多少摄像头, 对应的设备路径是什么. 但是此时并没有创建CameraHardware对象, 而是直到对应的摄像头第一次被打开的时候才创建.
CameraFactory类整个android系统只有一个实例, 那就是定义在CameraFactory.cpp中的gCameraFactory. camera_module_t定义了几个函数指针, 指向了CameraFactory中的static函数, 当调用这些指针指向的函数的时候, 实质上是在调用gCameraFactory对象中的相应方法:
hardware/libcamera/CameraHal.cpp
上述代码定义了一个camera_module_t, 相应的函数定义如下:
hardware/libcamera/CameraFactory.cpp
camera_device
camera_device同时被定义为了camera_device_t, 此处涉及到HAL的扩展规范. Android HAL定义了三个数据类型, , , , 分别表示模块类型, 模块方法和设备类型. 当需要扩展HAL, 增加一种设备的时候, 就要实现以上这三种数据结构. 例如对于摄像头来说, 就需要定义camera_module_t, camera_device_t, 以及为hw_module_methods_t中的函数指针赋值, 其中只有一个open函数, 相当于初始化模块. 而且HAL还规定camera_module_t的第一个成员必须是hw_module_t, camera_device_t的第一个成员必须是hw_device_t, 接下来的其他成员可以自己定义.
更详细的原理说明参加这里.
在Android-x86中, camera_device_t的定义位于hardware/libhardware/include/hardware/hardware.h
hardware/libhardware/include/hardware/hardware.h
其中的camera_device_ops_t摄像头模块自己定义的一组函数接口, 在同一个文件中, 太长了就不贴出来了.
camera_module_t位于同一个目录下的camera_common.h中
hardware/libhardware/include/hardware/camera_common.h
在framework调用HAL代码时, 通过hw_module_t->methods->open获取hw_device_t, 然后强制转化为camera_device_t, 就能调用camera_device_t->ops中的摄像头相关的函数了. 对于Android-x86的摄像头来说, ops中的函数指针的赋值位于继承了camera_device的CameraHardware类中.
CameraHardware
CameraHardware的众多接口主要是为了完成三个动作: 预览, 录制, 拍照. 其他的函数大多是设置参数等准备工作. 本小节以预览为例对代码流程做一个说明.
首先是对CameraHardware对象的初始化参数, 对应的函数为. 该函数通过调用V4L2Camera的, , , 来分别获取默认预览图像格式, 默认图像格式, 摄像头支持的分辨率和fps:
hardware/libcamera/CameraHardware.cpp
然后将这些参数转为文本形式, 设置到CameraParameters对象中:
hardware/libcamera/CameraHardware.cpp
当准备工作做完之后, 调用 函数, 这个函数只有三行, 首先加个锁, 然后调用 , 所有的预览的工作都是在这个函数中完成.
hardware/libcamera/CameraHardware.cpp
再来看这个 , 这个类很简单, 就是调用了CameraHardware的previewThread方法, 这个方法根据fps计算出一个等待时间, 然后调用V4L2Camera的GrabRawFrame获取摄像头设备的图像, 然后转换成支持的图像格式, 最后放到显示窗口中显示图像.
V4L2Camera
V4L2Camera类主要是对V4L2设备的封装, 下面分析一下常用的几个接口, 如, , , , , , .
- Open
Open接口的逻辑比较简单, 通过 系统调用获取摄像头设备的文件描述符, 然后调用VIDIOC_QUERYCAP ioctl查询设备的能力, 由于是摄像头设备, 这里就要求是设备的V4L2_CAP_VIDEO_CAPTURE位被置为1, 所以有个检查. 最后调用 获取摄像头支持的图像格式.
hardware/libcamera/V4L2Camera.cpp
- EnumFrameFormats
此函数获取摄像头设备的图像格式, 分辨率以及fps. 参见2.1可知, 设备支持的分辨率是对某个图像格式下才有意义, 不说明在什么图像格式下, 是无法获取支持的分辨率的, 同样, fps也是针对某个图像格式和某个分辨率的. 因此摄像头设备支持的图像格式, 分辨率和fps在调用完这个函数之后就全部知道了. 同时, 这个函数还设置好了 和 这两个参数, 这两个参数会被用来设置预览的默认格式.
hardware/libcamera/V4L2Camera.cpp
- EnumFrameSizes
此函数根据给定的pixfmt查询该格式下设备支持的分辨率.
hardware/libcamera/V4L2Camera.cpp
可以看到, Android对于分辨率的类型只认DISCRETE, CONTINUOUS和STEPWISE只输出个日志, 不做任何事.
- EnumFrameIntervals
该函数通过VIDIOC_ENUM_FRAMEINTERVALS ioctl查询指定图像格式和分辨率下设备支持的fps.
hardware/libcamera/V4L2Camera.cpp
- Init
Init函数是V4L2Camera类中最为复杂的一个方法.
hardware/libcamera/V4L2Camera.cpp
总的来说, Init函数做了以下几件事:
- 根据用户要求的长宽和fps, 从设备支持的分辨率和fps中找一个大于用户要求并且最接近的分辨率和fps. 然后设置摄像头设备使用此分辨率和fps. 最后由于实际使用的分辨率比用户请求的大, 还要计算一个裁剪偏差值, 以后使用图片的时候把多出来的那部分裁减掉.
- 如果摄像头设备使用了JPEG或者MJPEG压缩, 则设置图片质量是100%.
- 要求设备分配内存, 并映射到用户空间的videoIn->mem中. 然后压入摄像头设备的输入队列, 至此, 摄像头设备已经做好了捕捉图像的准备, 就等streamon命令了.
- StartStreaming
StartStreaming函数很简单, 除了调用STREAMON ioctl之外只是设置了videoIn 的isStreaming标记:
hardware/libcamera/V4L2Camera.cpp
调用这个函数之后, 摄像头就开始工作了.
- GrabRawFrame
StartStreaming之后, 还需要获取拍摄到的摄像头内容, 于是要调用.
hardware/libcamera/V4L2Camera.cpp
4.2 Framwork
JavaSDK层
Hardware的分析可以自底向上, 首先看V4L2Camera, 再看CameraHardware, 再到CameraFactory. Framework的代码自底向上看东西就太多了, 因此先从SDK中的摄像头部分看起. HAL和Framework说的都是C++的东西, 实现了安卓的底层. 但是实际上在开发app的时候用是SDK是JAVA语言编写的. 我们知道JAVA可以通过JNI来调用C++代码, 接下来就来看看ADK中是如何使用的.
首先考虑一段调用摄像头预览的代码:
第一行打开的是默认摄像头, 也可以换成 打开其他摄像头, 这几个函数的定义在ADK中位于Camera.java中, open函数为:
frameworks/base/core/java/android/hardware/Camera.java
可以看到, 直接open不加任何参数打开的其实是第一个后置摄像头. 总之最后open返回了一个Camera对象. 这里看到了一个熟悉的函数, 在HAL中的camera_module_t中, 除了必须的hw_module_t, 还有两个函数指针 和, 估计这个 最终就是调用了. 于是来看这个函数:
这个函数在Camera.java中只有一个声明, 表明这是一个native函数, 于是就要找其对应的JNI的定义.
frameworks/base/core/jni/android_hardware_Camera.cpp
再来找这个C++中的Camera类, 这个类已经位于android framework中了, 但是getNumberOfCameras的定义其实是在它的父类CameraBase中:
frameworks/av/camera/CameraBase.cpp
可以看到这里只是简单的获取CameraService, 然后调用其getNumberOfCameras 函数, 再来看这个函数:
frameworks/av/camera/CameraBase.cpp
可以看到gCameraService是一个sp<ICameraService>类型的单例, 第一次调用这个函数的时候对gCameraService初始化, 以后每次只是简单地返回这个变量. 在初始化的过程中, 用到了defaultServiceManager获取了一个sm, 并通过sm->getService获取到CameraService. defaultServiceManager这个函数位于frameworks/native/lib/binder/IServiceManager.cpp, 属于binder通信的一部分, 超出了本文的范围, 以后有空再写一篇博客说明.
open函数调用完之后, 就是setPreviewDisplay和startPreiview, 这两个函数同样是native的, 其实现类似, 下面就只看看startPreview:
frameworks/base/core/jni/android_hardware_Camera.cpp
这段代码首先获取了一个Camera对象, 然后对其调用startPreview, get_native_camera的实习如下:
该函数通过env->GetLongField获取了一个JNICameraContext的对象的指针, 然后就能通过getCamera得到Camera对象了, 而这个JNICameraContext的对象的指针是在native_setup中设置的:
注意第13行, 通过Camera::connect获取到了一个Camera对象, 这里终于又从ADK 层进入到了Framework层.
class Camera
在frameworks/av/camera/下有个Camera.cpp, 定义了一个Camera类, 由上一小节可以看到, ADK层通过JNI, 与这个类直接打交道, 进而进入Framework层, 可以说这个类是进入Framework的入口.
Camera类多重继承于CameraBase和BnCameraClient, 而这个CameraBase用到了模板:
frameworks/av/include/camera/CameraBase.h
这里除了用到模板还用到了模板的偏特化, Camera在实际继承CameraBase的时候, TCam就是Camera类型, 而TCamTraits用的是CameraTraits<Camera>, 但是这个模板特化并不是CameraBase.h中的CameraTraits, 而是定义在Camera.h中, 否则的话CameraTraits是空的, 也就没有TCamTraits::TCamListener这些东西了:
frameworks/av/include/camera/Camera.h
mediaserver
mediaserver是一个独立的进程, 有着自己的main函数, 系统启动之后会启动mediaserver作为一个守护进程. mediaserver负责管理android应用需要用到的多媒体相关的服务, 例如音频, 视频播放, 摄像头等. 通过Android的binder机制与app进行通信.
frameworks/av/media/mediaserver/main_mediaserver.cpp
可以看到, 在main函数中分别对几大服务调用了instantiate初始化. 重点关注 这一行, 初始化了摄像头服务. 于是接下来就来看这个CameraService类. 这个类的声明就很长, 约五百行, 内部还定义了, 等内部类. 但是并没有发现main函数中调用的instantiate函数. 考虑到CameraService多继承了BinderService<CameraService>, BnCameraService, DeathRecipient, camera_module_callbacks_t等四个类, 估计这个instantiate就是其中一个类定义的, 果然在BinderService.h中发现了这个函数:
frameworks/native/include/binder/BinderService.h
可以看到 调用了 , 而 首先取得了一个全局唯一的 实例的指针, 并且向其中注册了一个新的服务, 结合CameraService继承了 来看, 注册服务实际上调用的是:
至此, main函数仅仅注册了CameraService, 那么什么时候使用CameraService呢? 这就要看main函数的最后两行:
这里开始就就到了binder通信的部分, 不在本文的范围内, 参见我的另一篇博文.
CameraHardwareInterface