使用Camera1 API 实现相机基本功能:预览、拍照、录像 同时实现USBCamera 功能
文章目录
一、目的
- 这里核心重点讨论的是实现USBCamera 功能,预览-拍照-录像不作为重点介绍对象
- 目的是为了实现USBCamera 软件,UVC太古老,偏framework层和底层,系统签名等局限性。如何实现Camera 框架下的Camera1 和 Camera2 来实现USBCamera 功能。
二、USBCamera 技术实现方案
- 基于UVC协议 来实现:如之前笔记讨论介绍,强烈建议再熟悉熟悉
- 基于Android 体系下的SDK API来实现,底层就是Camera1 或者 Camera2 框架下来实现
三、误区
基本常识是 USBCamera 是无驱的,那么跟底层系统不相关,USB相机插着就能用,系统本身支持的想法其实是错误的。
大家可以自行查阅相关资料,这里重点说明的是:
1)使用UVC协议来实现USBCamera ,作为一个系统应用 具备系统签名,不用考虑各种权限,是没有问题的。
2)使用Android SDK 框架下的API来实现USBCamera 是不可取的,官方文档明确说明Camera2 是不支持的。但是我们在不同半导体平台下不一样,比如RK上面是支持Camera2 开发USBCamera,
但是在MTK上面 Camera2 是不支持USBCamera,就是打不开相机的。 所以,要想实现Camera2 API来写一个USBCamera 务必驱动层、framework层在芯片平台支持的情况下适配好才行。
四、USBCamera 遇到项目难点
1)使用UVC协议来开发USBCamera 需要系统签名,特别在Android13 及以上版本开发,权限问题绕不过,系统获取节点权限在各个芯片平台上面都绕不过,无解。
2)UVC协议开发遇到困难、了解甚少,那么需要用Android 体系下的Camera API来开发。 Android 5 以后 都是用的Camera2,但是Camera2 在MTK平台上面不支持的 需要驱动层和framework层适配,当然RK平台是支持Camera2 API开发的。那么使用Camera1 API 虽然过时了,拓展性不强,但Camera1 的API 是支持USBCamera 开发的。
3)为什么要自己开发USBCamera,使用市场上写好的 OpenCamera、USBCamera 不好吗? 实际上 客户需求很多样,硬件环境很复杂、功能复杂、 不同平台不同soc 不同USB相机不适配、功能也不可用。
比如:遇到HDMIN 功能作为一个外置摄像头,但又不是真正意义上的Camera,那么不可能正常打开正常使用的。
五、基础补充、资源参考
对于相机开发,还是建议无论是UVC协议开发USBCamera,还是 Android SDK API框架下Camera api 来开发相机,都需要了解最基本的相关资料。
相机整个模块确实太专业、复杂了。 无论从硬件外设、驱动【USBCamera免驱】、相机 都比较专业、覆盖面及广,针对思考中的问题 给出自己认为比较好相关博客,方便了解,助于梳理流程、提升认知。
对于上层应用或者Framework 系统应用开发者,只需要了解基本的架构、API、使用方法,当然这些也不简单的
下面提供部分资源,方便快速了解,充电:
架构图了解
MTKCamera2相机架构
Camera2架构
Android Camera架构简析
Camera相关专栏
Camera Framework 专栏
小驰私房菜系列
小驰私房菜MTK系列
小驰Camera 开发系列
Camera 相机开发
展讯平台 Camera
官方文档:谷歌官方 API 描述
零散知识了解
MTK 相机UI介绍
Camera2 相机认知
Camera2学习笔记
camera2关于拍照预览方向旋转90度和拍照图片镜像功能实现
Camera2 预览集成、简单拍照:熟悉预览步骤流程比较有用
Camera镜像上下左右颠倒问题的解决办法
MTK相机成像质量差
Camera应用分析
部分相机源码参考,学习API使用,梳理流程,偏应用层
极客相机 Camera2 API
Camera2 API详解
极客相机源码
Camera2 相机Demo
Camera2 专业相机Demo
拍照、预览、录像Demo
使用Camera2 拍照
Camera2 系统相关
六、经验参考
备注:
- 极客相机源码在开源OpenCamera 相机修改而来实现的,在切换相机过程中默认Camera1 API 可以正常打开USBCamera相机,切换到Camera2 API 下无法正常打开USBCamera相机;
- 使用Camera2 API,以OpenCamera 作为源码二次开发,发现在MTK平台下获取得到外设USBCamera的CameraId
也打不开USBCamera,但是在RK平台上是可以正常打开的。所以 MTK平台上默认是不支持Camera2来实现USBCamera 功能的,但是RK平台做了适配 是可行的,直接修改RK平台的Camera2 就能实现USBCamera 和 MIPI 相机切换了。 - 基于1 点的实践,那么就只能使用Camera1 API 来实现USBCamera了。 在RK和MTK平台上实际验证都是支持的。
- 自己在MTK平台上面实践发现:在Camera1 API下,打开CameraId 2 就是 外设USBCamera 了。
七、 功能实现
Camera1 API 相对非常简单,简要总结如下
声明cameraID 为前置、后置 或者 外设USBCamera
cameraId = CameraInfo.CAMERA_FACING_FRONT
cameraId = CameraInfo.CAMERA_FACING_BACK
cameraId = 2
选择 绘制组件 SurfaceView 或者 TextureView
打开相机
mCamera = Camera.open(cameraId)
打开后 设置Camera 属性:预览大小、方向、格式
private fun open(cameraId: Int) {
//获得camera对象
mCamera = Camera.open(cameraId)
mCamera?.let { camera ->
//配置camera的属性
val parameters = camera.parameters
//设置预览数据格式为nv21
parameters.previewFormat = ImageFormat.NV21
//这是摄像头宽、高
setPreviewSize(parameters!!)
// 设置摄像头 图像传感器的角度、方向
setPreviewOrientation(cameraId)
camera.parameters = parameters
}
}
切换摄像头
/**
* 切换摄像头
*/
fun switchCamera() {
val cameraId = if (cameraId == Camera.CameraInfo.CAMERA_FACING_BACK) {
Camera.CameraInfo.CAMERA_FACING_FRONT
} else {
Camera.CameraInfo.CAMERA_FACING_BACK
}
switchCamera(cameraId)
}
其实就是把cameraId 换一下,然后重新预览
private fun previewAlign() {
stopPreview()
if (surfaceHolder != null) {
startPreview(surfaceHolder!!)
} else {
startPreview(surfaceTexture!!)
}
}
停止预览
/**
* 停止预览
*/
fun stopPreview() {
if (mCamera != null) {
mCamera?.setPreviewCallback(null)
mCamera?.stopPreview()
mCamera?.release()
mCamera = null
}
}
重新预览
/**
* 开始预览
*/
fun startPreview(surfaceHolder: SurfaceHolder) {
open(cameraId)
this.surfaceHolder = surfaceHolder
buffer = ByteArray(width * height * 3 / 2)
bytes = ByteArray(buffer!!.size)
//数据缓存区
mCamera?.addCallbackBuffer(buffer)
mCamera?.setPreviewCallbackWithBuffer(this)
//设置预览画面
mCamera?.setPreviewDisplay(surfaceHolder)
mCamera?.startPreview()
}
fun startPreview(surfaceTexture: SurfaceTexture) {
open(cameraId)
this.surfaceTexture = surfaceTexture
buffer = ByteArray(width * height * 3 / 2)
bytes = ByteArray(buffer!!.size)
//数据缓存区
mCamera?.addCallbackBuffer(buffer)
mCamera?.setPreviewCallbackWithBuffer(this)
//设置预览画面
mCamera?.setPreviewTexture(surfaceTexture)
mCamera?.startPreview()
}
拍照片
其实就是调用的api,然后对数据进行矩阵变化、缩放、参数设置,最后保存。
/**
* 拍摄照片
*/
fun takePicture(completedCallBack: () -> Unit, errorCallBack: (Exception) -> Unit) {
mCamera?.takePicture(null, null, object : Camera.PictureCallback {
override fun onPictureTaken(data: ByteArray?, camera: Camera?) {
Log.i("ZZZZ", "isMainThread:" + (Looper.myLooper() == Looper.getMainLooper()))
previewAlign()
threadPool.execute {
//路径示例 : /storage/emulated/0/Pictures/MyCameraApp/IMG_20230726_135652.jpg
// val pictureFile: File = getOutputMediaFile(MEDIA_TYPE_IMAGE)!!
// Log.i("ZZZZ", "pictureFile:$pictureFile")
// try {
// val fos = FileOutputStream(pictureFile)
// fos.write(data)
// fos.close()
//
// /*//修正图片方向,这里只是示例,需要根据实际手机方位来决定图片角度
// val exif = ExifInterface(pictureFile.absolutePath)
// exif.setAttribute(
// ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_ROTATE_270.toString()
// )
// exif.saveAttributes()*/
// completedCallBack.invoke()
// } catch (e: FileNotFoundException) {
// Log.d(TAG, "File not found: ${e.message}")
// errorCallBack.invoke(e)
// } catch (e: IOException) {
// Log.d(TAG, "Error accessing file: ${e.message}")
// errorCallBack.invoke(e)
// }
val pictureFile: File = getOutputMediaFile(MEDIA_TYPE_IMAGE)!!
//路径示例 : /storage/emulated/0/Pictures/MyCameraApp/IMG_20230726_135652.jpg
val bitmap = BitmapFactory.decodeByteArray(data, 0, data!!.size)
val matrix = Matrix()
//修正图片方向,这里只是示例,需要根据实际手机方位来决定图片角度
matrix.postRotate(if (cameraId == 1) 270F else 90F)
if (cameraId == 1) {
//postScale在矩阵变换之后进行缩放
matrix.postScale(-1F, 1F, bitmap.width / 2F, bitmap.height / 2F)
}
val rotatedBitmap: Bitmap = Bitmap.createBitmap(
bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true
)
ImageUtils.save(rotatedBitmap, pictureFile, Bitmap.CompressFormat.JPEG)
completedCallBack.invoke()
}
}
})
}
设置预览
private fun setPreviewSize(parameters: Camera.Parameters) {
//获取摄像头支持的宽、高
val supportedPreviewSizes = parameters.supportedPreviewSizes
var size = supportedPreviewSizes[0]
Log.d(TAG, "Camera支持: " + size.width + "x" + size.height)
//选择一个与设置的差距最小的支持分辨率
var m: Int = Math.abs(size.height * size.width - width * height)
supportedPreviewSizes.removeAt(0)
val iterator: Iterator<Camera.Size> = supportedPreviewSizes.iterator()
//遍历
while (iterator.hasNext()) {
val next = iterator.next()
Log.d(TAG, "支持 " + next.width + "x" + next.height)
val n: Int = Math.abs(next.height * next.width - width * height)
if (n < m) {
m = n
size = next
}
}
width = size.width
height = size.height
parameters.setPreviewSize(width, height)
Log.d(TAG, "预览分辨率 width:" + size.width + " height:" + size.height)
}
通过MediaRecorder 录像
fun startVideo(holder: SurfaceHolder) {
mediaRecorder = MediaRecorder()
//解锁相机,以供 MediaRecorder 使用
camera?.unlock()
//设置要用于视频捕获的相机
mediaRecorder.setCamera(camera)
//设置音频源
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
//设置视频源
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA)
//设置视频的输出格式和编码
mediaRecorder.setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P))
//设置输出视频播放的方向
mediaRecorder.setOrientationHint(270)
//设置输出文件
mediaRecorder.setOutputFile(getVideoFilePath(this))
//指定 SurfaceView 预览布局元素
mediaRecorder.setPreviewDisplay(holder.surface)
try {
mediaRecorder.prepare()
} catch (e: IOException) {
e.printStackTrace()
releaseMediaRecorder()
}
Handler().postDelayed({
try {
mediaRecorder.start()
} catch (e: IOException) {
e.printStackTrace()
releaseMediaRecorder()
}
}, 10)
}
Demo效果
USBCamera 开发,和 MIPI开发完全一样的,方法参数设置也一样。 拿到如下Demo 源码后可以仔细需改参数,适配、定制基本功能了。
八、 总结
- 通用平台底层都支持Camera1 的,那么Camera1 开发USBCamera 相机就能在不同平台通用了
- 简单的预览-拍照-录像 需求可以使用Camera1 来处理 复杂的效果 性能 还是用Camera2 API 实现
- Camera1 毕竟是在Android5 及 以前使用,已经废弃了,只是能用。 如果平台支持如RK 平台还是建议用Camera2API, 不支持的情况下可以选择Camera1 API来实现