直接说Camera2,参看文章(本文章是下面文章的学习笔记,建议直接阅读下面文章)
https://www.jianshu.com/p/9a2e66916fcb 概览
https://www.jianshu.com/p/df3c8683bb90 开关
概览
1. 拍照的流程
如下图,Camera2的API模型被设计成一个Pipeline(管道),顺序的处理每一帧的请求并返回请求结果给客户端。
2. Capture
Capture不仅仅是拍照而已,事实上,Camera2中所有的操作都被抽象成Capture(捕获),如对焦等。
Capture又可以细分为单次模式、多次模式和重复模式
- 单次模式(One-shot):指的是只执行一次的Capture操作,例如设置闪光灯模式、对焦模式和拍一张照片等,多次一次性模式的Capture会进入队列按顺序执行
- 多次模式(Burst):指的是连续多次执行指定的Capture操作,注意,多次Capture执行期间不允许插入其他任何的Capture操作,例如连续拍摄100张照片
- 重复模式(Repeating):指的是不断重复执行指定的Capture操作,当有其他模式的Capture提交时会暂停该模式,转而执行其他模式的Capture,当其他模式的Capture执行完毕后会恢复执行。 如预览等
3. CameraManager
CameraManager功能不多,主要用来查询相机信息和执行打开相机的操作
4. CameraCharacteristics
CameraCharacteristics中携带了大量的相机信息,如相机朝向,判断闪光灯是否可用,AE模式等
5. CameraDevice
表示当前连接的摄像头,通过CameraDevice,可以有下面操作
- 创建CameraCaptureSession
- 创建CaptureRequest
- 关闭相机操作(打开是通过CameraManager)
- 监听设备状态
6. Surface
Surface是一块用于填充图像的内存空间,例如你可以使用SurfaceView的Surface接收每一帧预览数据用于显示预览画面,也可以使用ImageReader和Surface接收JPEG或YUV数据,每一个Surface都可以有自己的尺寸和数据格式,你可以从CameraCharacteristics获取某一个数据格式支持的尺寸列表
7. CameraCaptureSession
顾名思义,CameraCaptureSession就是与摄像头建立的一次“会话”,后面的CaptureRequest都会提交该回话
8. CameraRequest
一次相机操作请求,如拍照,预览等
开关相机
1. 相机配置
AndroidManfiest中添加如下代码
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-feature android:name="android.hardware.camera" />
动态申请相机权限(太简单了,略)
2. 获取摄像头信息
private val mCameraManager by lazy {
mContext.getSystemService(CameraManager::class.java) as CameraManager
}
/**
* 初始化得到CameraId
*/
fun initCameraId() {
if (mBackCameraId.isNotEmpty() || mFortCameraId.isNotEmpty()) return
// mCameraManager就是CameraManager实例
for (cameraId in mCameraManager.cameraIdList) {
val cameraCharacter = mCameraManager.getCameraCharacteristics(cameraId)
val facing = cameraCharacter.get(CameraCharacteristics.LENS_FACING)
//相机朝向, 我们这里很简单, 就分别获取第一个摄像头
if (facing == CameraCharacteristics.LENS_FACING_FRONT) {
if (mFortCameraId.isEmpty()) mFortCameraId = cameraId
} else if (facing == CameraCharacteristics.LENS_FACING_BACK) {
if (mBackCameraId.isEmpty()) {
mBackCameraId = cameraId
}
}
}
}
2. 打开相机
代码如下
fun openCamera(surface: Surface) {
if (isOpenCamera()) return
initCameraId()
//检查权限
if (ActivityCompat.checkSelfPermission(mContext, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
return
}
//打开相机
mCameraManager.openCamera(mBackCameraId, mCameraStateCallback, mBackGroundHandler)
mPreviewSurface = surface
}
private val mCameraStateCallback: CameraDevice.StateCallback = object : CameraDevice.StateCallback() {
override fun onOpened(camera: CameraDevice) {
logcat("camera onOpen")
// 获取到CameraDevice实例,后面还需要这个实例进行后续操作
mCameraDevice = camera
openPreviewSession()
}
override fun onDisconnected(camera: CameraDevice) {
logcat("camera on disconnect")
camera.close()
mCameraDevice = null
}
override fun onClosed(camera: CameraDevice) {
logcat("camera onClosed")
mCameraDevice = null
}
override fun onError(camera: CameraDevice, error: Int) {
logcat("camera onError")
camera.close()
mCameraDevice = null
}
}
private val mBackGroundHandler by lazy {
Handler(mBackGroundHandlerThread.looper)
}
private val mBackGroundHandlerThread by lazy {
val thread = HandlerThread("back ground")
thread.start()
thread
}
3. 关闭相机
fun closeCamera() {
mCameraDevice?.close()
mCameraDevice = null
}
预览
1. 获取Surface
我们使用SurfaceTexture来显示预览画面
<TextureView
android:id="@+id/camera_preview"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
我们设置SurfaceTextureListener,成功之后创建Surface
binding.cameraPreview.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureAvailable(surface: SurfaceTexture?, width: Int, height: Int) {
if (surface == null) return
//获取预览尺寸, 这里对主流程来说不重要, 这里根据心情来写
val previewSize = MyCameraManager.instance.getPreviewSize(binding.cameraPreview.height, binding.cameraPreview.width) ?: return
surface.setDefaultBufferSize(previewSize.width, previewSize.height)
val lp = binding.cameraPreview.layoutParams
lp.width = previewSize.height
lp.height = previewSize.width
// 根据预览尺寸来设置预览控件大小
binding.cameraPreview.layoutParams = lp
//获取预览的Surface
mPreviewSurface = Surface(surface)
if (!MyCameraManager.instance.isOpenCamera() && mIsResumed) {
//打开相机
MyCameraManager.instance.openCamera(mPreviewSurface!!)
}
}
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture?, width: Int, height: Int) {
logcat("onSurfaceTextureSizeChanged")
}
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture?): Boolean {
logcat("onSurfaceTextureDestroyed")
return true
}
override fun onSurfaceTextureUpdated(surface: SurfaceTexture?) {
// logcat("onSurfaceTextureUpdated")
}
}
2. 预览
在上面我们已经展示了打开相机的操作,打开相机之后调用openPreviewSession来创建一个会话。具体实现代码如下
private fun openPreviewSession() {
val surface = mPreviewSurface ?: return
val device = mCameraDevice ?: return
val characteristics = mCameraManager.getCameraCharacteristics(mBackCameraId)
//得到预览的尺寸, 这个对本教程不重要, 这里根据自己的心情来
val imageSize = getOptimalSize(characteristics, ImageReader::class.java, maxWidth = 1920, maxHeight = 1080)
if (imageSize != null) {
logcat("ImageSize:${imageSize.width} ${imageSize.height}")
val jpegReader = ImageReader.newInstance(imageSize.width, imageSize.height, ImageFormat.JPEG, 2)
jpegReader.setOnImageAvailableListener(OnJpegImageAvailableListener(), mBackGroundHandler)
mJpegSurface = jpegReader.surface
//这里添加两个Surface, 一个是预览的Surface, 另外一个是拍照的Surface
device.createCaptureSession(listOf(surface, mJpegSurface), mSessionStateCallback, mBackGroundHandler)
}
}
private var mSessionStateCallback: CameraCaptureSession.StateCallback = object : CameraCaptureSession.StateCallback() {
override fun onConfigured(session: CameraCaptureSession) {
logcat("session onConfigured")
val camera = mCameraDevice ?: return
val surface = mPreviewSurface ?: return
mSession = session
//当Session创建成功之后, 发出一个request
val requestBuilder = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
//这里就是我们预览的Surface
requestBuilder.addTarget(surface)
val request = requestBuilder.build()
// 发起一个Repeating Request
session.setRepeatingRequest(request, object : CameraCaptureSession.CaptureCallback() {
override fun onCaptureStarted(session: CameraCaptureSession, request: CaptureRequest, timestamp: Long, frameNumber: Long) {
}
override fun onCaptureProgressed(session: CameraCaptureSession, request: CaptureRequest, partialResult: CaptureResult) {
}
override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) {
}
}, mBackGroundHandler)
}
}
如上,我们就可以将预览的图像显示出来了
拍照
注: 拍照这个部分可能会有兼容性问题,下面代码小米手机拍照异常,华为ok。应该还是有一些细节没有处理好。
创建用于获取拍照图像的Surface
private fun openPreviewSession() {
val surface = mPreviewSurface ?: return
val device = mCameraDevice ?: return
val characteristics = mCameraManager.getCameraCharacteristics(mBackCameraId)
//得到预览的尺寸, 这个对本教程不重要, 这里根据自己的心情来
val imageSize = getOptimalSize(characteristics, ImageReader::class.java, maxWidth = 1920, maxHeight = 1080)
if (imageSize != null) {
logcat("ImageSize:${imageSize.width} ${imageSize.height}")
val jpegReader = ImageReader.newInstance(imageSize.width, imageSize.height, ImageFormat.JPEG, 2)
jpegReader.setOnImageAvailableListener(OnJpegImageAvailableListener(), mBackGroundHandler)
mJpegSurface = jpegReader.surface
//这里添加两个Surface, 一个是预览的Surface, 另外一个是拍照的Surface
device.createCaptureSession(listOf(surface, mJpegSurface), mSessionStateCallback, mBackGroundHandler)
}
}
这里需要设置ImageAvailableListener,用于保存图像
private inner class OnJpegImageAvailableListener : ImageReader.OnImageAvailableListener {
private val dateFormat: DateFormat = SimpleDateFormat("yyyyMMddHHmmssSSS", Locale.CHINA)
private val cameraDir: String = "${Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)}/Camera"
override fun onImageAvailable(reader: ImageReader?) {
logcat("onImageAvailable:$reader")
if (reader == null) return
val image = reader.acquireNextImage()
val captureResult = captureResults.take()
if (image != null && captureResult != null) {
logcat("image != null ")
val jpegByteBuffer = image.planes[0].buffer
val jpegByteArray = ByteArray(jpegByteBuffer.remaining())
jpegByteBuffer.get(jpegByteArray)
val width = image.width
val height = image.height
runIO {
val date = System.currentTimeMillis()
val title = "IMG_${dateFormat.format(date)}"
val displayName = "$title.jpeg"
val path = "$cameraDir/$displayName"
val orientation = captureResult[CaptureResult.JPEG_ORIENTATION]
val location = captureResult[CaptureResult.JPEG_GPS_LOCATION]
val longitude = location?.longitude ?: 0.0
val latition = location?.latitude ?: 0.0
logcat("write to local:$path")
File(path).writeBytes(jpegByteArray)
val values = ContentValues()
values.put(MediaStore.Images.ImageColumns.TITLE, title)
values.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, displayName)
values.put(MediaStore.Images.ImageColumns.DATA, path)
values.put(MediaStore.Images.ImageColumns.DATE_TAKEN, date)
values.put(MediaStore.Images.ImageColumns.WIDTH, width)
values.put(MediaStore.Images.ImageColumns.HEIGHT, height)
values.put(MediaStore.Images.ImageColumns.ORIENTATION, orientation)
values.put(MediaStore.Images.ImageColumns.LONGITUDE, longitude)
values.put(MediaStore.Images.ImageColumns.LATITUDE, latition)
mContext.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
}
}
}
}
创建Request,发起拍照请求
fun takePic() {
val builder = mCameraDevice?.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE) ?: return
builder.addTarget(mPreviewSurface!!)
builder.addTarget(mJpegSurface!!)
mSession?.capture(builder.build(), CaptureImageStateCallback(), mBackGroundHandler)
}
获取CaptureRequest
private inner class CaptureImageStateCallback : CameraCaptureSession.CaptureCallback() {
override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) {
logcat("onCaptureCompleted")
super.onCaptureCompleted(session, request, result)
captureResults.put(result)
}
}
这样就可以了。