Android 使用Camera1实现相机预览、拍照、录像
1. 前言
本文介绍如何从零开始,在Android中实现Camera1的接入,并在文末提供Camera1Manager工具类,可以用于快速接入Camera1。
Android Camera1 API虽然已经被Google废弃,但有些场景下不得不使用。
并且Camera1返回的帧数据是NV21,不像Camera2、CameraX那样,需要自己再转一层,才能得到NV21。
Camera1的API调用也比Camera2简单不少,和CameraX的简单程度差不多,所以在一定的场景下,Camera1还是有其用途的。
2. 前置操作
2.1 添加权限
在AndroidManifest中添加如下权限
2.2 申请权限
别忘了申请权限
ActivityCompat.requestPermissions( this@WelComeActivity, arrayOf( android.Manifest.permission.WRITE_EXTERNAL_STORAGE, android.Manifest.permission.RECORD_AUDIO, android.Manifest.permission.CAMERA ), 123 )
2.3 声明XML布局
新建一个Activity,在其XML中声明SurfaceView
3. 实现预览功能
3.1 添加SurfaceView的回调
binding.surfaceView.holder.addCallback(surfaceCallback) private var surfaceCallback: SurfaceHolder.Callback = object : SurfaceHolder.Callback { // Surface创建时 override fun surfaceCreated(holder: SurfaceHolder) { } // Surface改变时 override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { } // Surface销毁时 override fun surfaceDestroyed(holder: SurfaceHolder) { } }
3.2 打开相机
当Surface创建时,也就是在surfaceCreated的时候,打开相机
private var camera: Camera? = null private fun openCamera(holder: SurfaceHolder) { try { camera = Camera.open(cameraId) } catch (e: Exception) { e.printStackTrace() } }
3.3 开始预览
当我们打开相机后,就可以开始预览了
这里首先将设置camera1预览的尺寸,一般来说,通过camera!!.parameters.supportedPreviewSizes获取到的列表中,第一项就是最推荐的尺寸了。
private fun setPreviewSize() { //获取摄像头支持的宽、高 val supportedPreviewSizes: List = camera!!.parameters.supportedPreviewSizes supportedPreviewSizes.forEach { Log.i("ZZZZ", "${it.width}*${it.height}") } val parameters = camera?.parameters val size = supportedPreviewSizes[0] parameters?.setPreviewSize(size.width, size.height) camera?.setParameters(parameters) }
接着,将SurfaceHolder设置到camera中。setPreviewDisplay接受一个SurfaceHolder对象作为参数,该对象表示预览显示的表面。通过调用setPreviewDisplay方法,可以将相机的预览数据输出到指定的表面对象上,从而在预览界面中显示出相机的拍摄画面。
camera?.setPreviewDisplay(holder)
接着调用setDisplayOrientation方法来设置相机的预览方向。该方法接受一个参数,即预览方向的度数。例如,如果要在竖直模式下使用相机,而默认的预览方向是水平的,那么就可以通过调用setDisplayOrientation方法将预览方向顺时针旋转90度。
camera?.setDisplayOrientation(90)
最后,调用startPreview()就可以启动相机的预览了
camera?.startPreview()
来看一下完整代码
private fun startPreview(holder: SurfaceHolder) { try { setPreviewSize() camera?.setPreviewDisplay(holder) camera?.setDisplayOrientation(90) camera?.startPreview() } catch (e: IOException) { e.printStackTrace() } }
3.4 效果如下
4. 实现拍照功能
4.1 调用拍照接口
要进行拍照,调用camera.takePicture即可,它共有3个回调参数
- ShutterCallback shutter(捕获图片瞬间的回调):快门回调是在拍照时快门按下的瞬间调用的回调。它允许您在拍照时执行一些自定义操作,例如触发闪光灯或显示自定义的拍照界面。
- PictureCallback raw(原始图像数据回调):原始图像数据回调是在拍照后,获取到原始未压缩的数据时调用的回调。您可以在这个回调中对图像数据进行处理或保存。
- PictureCallback jpeg(JPEG图像数据回调):JPEG图像数据回调是在拍照后,获取到图像的JPEG格式数据时调用的回调。您可以在这个回调中对JPEG图像数据进行处理或保存。
这里我们只需要用到jpeg回调
private val threadPool = Executors.newCachedThreadPool() binding.btnTakePicture.setOnClickListener { camera?.takePicture( null,null,{ data, camera -> //jpeg回调 }) }
4.2 在jpeg回调中保存图片
//MediaFileUtils类详见本文附录 val pictureFile: File = MediaFileUtils.getOutputMediaFile(MEDIA_TYPE_IMAGE)!! try { val fos = FileOutputStream(pictureFile) fos.write(data) fos.close() } 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) }
来查看下效果,可以看到图片已经被保存了,但是图片的方向目前是有问题的。
4.3 解决图片保存的方向问题
所以,我们需要先将图片转成bitmap,旋转角度后,再保存
修改代码为如下代码
//路径示例 : /storage/emulated/0/Pictures/MyCameraApp/IMG_20230726_135652.jpg val pictureFile: File = MediaFileUtils.getOutputMediaFile(MediaFileUtils.MEDIA_TYPE_IMAGE)!! val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size) val matrix = Matrix() matrix.postRotate(270F) val rotatedBitmap: Bitmap = Bitmap.createBitmap( bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true ) //ImageUtils需要依赖 implementation 'com.blankj:utilcodex:1.31.1' ImageUtils.save(rotatedBitmap, pictureFile, Bitmap.CompressFormat.JPEG)
来看一下效果,可以看到现在图片方向是对了,但是图片左右的内容是相反的
4.4 解决图片保存镜像问题
要解决图片的镜像问题,就调用一下matrix.postScale左右水平变换就好了
matrix.postScale(-1F, 1F, bitmap.width / 2F, bitmap.height / 2F)
完整代码如下
val pictureFile: File = MediaFileUtils.getOutputMediaFile(MediaFileUtils.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(270F) 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需要依赖 implementation 'com.blankj:utilcodex:1.31.1' ImageUtils.save(rotatedBitmap, pictureFile, Bitmap.CompressFormat.JPEG)
5. 实现录像功能
要录制视频,需要使用MediaRecorder,若要使用 Camera1 拍摄视频,需要谨慎管理 Camera 和 MediaRecorder,并且必须按特定顺序调用相应方法。您必须遵循以下顺序,才能使您的应用正常工作:
- 打开相机。
- 做好准备,并开始预览(如果您的应用会显示正在录制的视频,而通常情况下都是如此)。
- 通过调用 Camera.unlock() 解锁相机,以供 MediaRecorder 使用。
- 通过在 MediaRecorder 上调用以下方法来配置录制:
- 通过 setCamera(camera) 关联您的 Camera 实例。
- 调用 setAudioSource(MediaRecorder.AudioSource.CAMCORDER)。
- 调用 setVideoSource(MediaRecorder.VideoSource.CAMERA)。
- 调用 setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P)) 以设置质量。
- 调用 setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString())。
- 如果您的应用提供视频预览,请调用 setPreviewDisplay(preview?.holder?.surface)。
- 调用 setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)。
- 调用 setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT)。
- 调用 setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT)。
- 调用 prepare() 以完成 MediaRecorder 配置。
- 如需开始录制,请调用 MediaRecorder.start()。
- 如需停止录制,请按以下顺序调用以下方法:
- 调用 MediaRecorder.stop()。
- (可选)通过调用 MediaRecorder.reset() 移除当前的 MediaRecorder 配置。
- 调用 MediaRecorder.release()。
- 通过调用 Camera.lock() 锁定相机,以便将来的 MediaRecorder 会话可以使用它。
- 如需停止预览,请调用 Camera.stopPreview()。
- 最后,如需释放 Camera 以供其他进程使用,请调用 Camera.release()。
具体可以见 Camera1 录制视频
下面直接附上代码,直接如下代码就好了
5.1 开始录制
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) } fun getVideoFilePath(context: Context?): String { val filename = "VIDEO_${System.currentTimeMillis()}.mp4" val dir = context?.getExternalFilesDir("video") return "${dir!!.path}/$filename" }
5.2 停止播放
fun stopVideo() { mediaRecorder.stop() mediaRecorder.release() camera?.lock() }
5.3 释放资源
fun releaseMediaRecorder() { if (mediaRecorder != null) { mediaRecorder.reset() // 清除配置 mediaRecorder.release() //mediaRecorder = null camera?.lock() } }
6. CameraHelper工具类
可以直接使用这个工具类,来快速接入Camera1
class CameraHelper( private val activity: AppCompatActivity, private var cameraId: Int, private var width: Int = 720, private var height: Int = 1280, ) : Camera.PreviewCallback { private var surfaceHolder: SurfaceHolder? = null private var surfaceTexture: SurfaceTexture? = null private var mCamera: Camera? = null private var buffer: ByteArray? = null private var bytes: ByteArray? = null /** * 打开相机 * * @param cameraId 后摄 Camera.CameraInfo.CAMERA_FACING_BACK * 前摄 Camera.CameraInfo.CAMERA_FACING_FRONT */ 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) } /** * 切换摄像头 * @param cameraId 指定摄像头ID */ fun switchCamera(cameraId: Int) { this.cameraId = cameraId previewAlign() } 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() } private val threadPool = Executors.newCachedThreadPool() /** * 拍摄照片 */ fun takePicture(completedCallBack: () -> Unit, errorCallBack: (Exception) -> Unit) { mCamera?.takePicture(null, null, object : Camera.PictureCallback { override fun onPictureTaken(data: ByteArray?, camera: Camera?) { previewAlign() threadPool.execute { val pictureFile: File = getOutputMediaFile(MEDIA_TYPE_IMAGE)!! val bitmap : Bitmap try { //路径示例 : /storage/emulated/0/Pictures/MyCameraApp/IMG_20230726_135652.jpg bitmap = BitmapFactory.decodeByteArray(data, 0, data!!.size) }catch (e:Exception){ errorCallBack.invoke(e) return@execute } 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 ) //需要依赖"com.blankj:utilcodex:1.31.1" ImageUtils.save(rotatedBitmap, pictureFile, Bitmap.CompressFormat.JPEG) completedCallBack.invoke() } } }) } override fun onPreviewFrame(data: ByteArray, camera: Camera?) { onPreviewListener?.onPreviewFrame(data, camera) camera!!.addCallbackBuffer(data) } 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 = 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 { degrees = 0 mOnChangedSizeListener?.onChanged(height, width) } Surface.ROTATION_90 -> { degrees = 90 mOnChangedSizeListener?.onChanged(width, height) } Surface.ROTATION_180 -> { degrees = 180 mOnChangedSizeListener?.onChanged(height, width) } Surface.ROTATION_270 -> { degrees = 270 mOnChangedSizeListener?.onChanged(width, height) } } var result: Int if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { result = (info.orientation + degrees) % 360 result = (360 - result) % 360 // compensate the mirror } else { // back-facing result = (info.orientation - degrees + 360) % 360 } //设置角度, 参考源码注释 mCamera!!.setDisplayOrientation(result) } private lateinit var mediaRecorder: MediaRecorder private val handle = Handler(Looper.getMainLooper()) /** * 开始录像 */ fun startVideo(path: String) { mediaRecorder = MediaRecorder() //解锁相机,以供 MediaRecorder 使用 mCamera?.unlock() //设置要用于视频捕获的相机 mediaRecorder.setCamera(mCamera) //设置音频源 mediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER) //设置视频源 mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA) //设置视频的输出格式和编码 mediaRecorder.setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH)) //设置输出视频播放的方向,这里只是示例,需要根据实际手机方位来决定角度 mediaRecorder.setOrientationHint(if (cameraId == 1) 270 else 90) //设置输出文件 mediaRecorder.setOutputFile(path) //指定 SurfaceView 预览布局元素 mediaRecorder.setPreviewDisplay(surfaceHolder!!.surface) try { mediaRecorder.prepare() } catch (e: IOException) { e.printStackTrace() releaseMediaRecorder() } handle.postDelayed({ try { mediaRecorder.start() } catch (e: IOException) { e.printStackTrace() releaseMediaRecorder() } }, 10) } /** * 释放资源 */ fun releaseMediaRecorder() { if (mediaRecorder != null) { mediaRecorder.reset() // 清除配置 mediaRecorder.release() //mediaRecorder = null mCamera?.lock() } } /** * 停止录像 */ fun stopVideo() { mediaRecorder.stop() mediaRecorder.release() mCamera?.lock() } interface OnChangedSizeListener { fun onChanged(width: Int, height: Int) } interface OnPreviewListener { fun onPreviewFrame(data: ByteArray, camera: Camera?) } private var onPreviewListener: OnPreviewListener? = null /** * 设置预览监听 */ fun setOnPreviewListener(listener: OnPreviewListener) { this.onPreviewListener = listener } companion object { private const val TAG = "CAMERA_HELPER" } }
进行使用
//这里的Activity是横屏的 class MainActivity : AppCompatActivity(), SurfaceHolder.Callback { private lateinit var binding: ActivityMainBinding private lateinit var cameraHelper: CameraHelper private val cameraId = 1 private val nativeLib = NativeLib() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) ActivityCompat.requestPermissions( this, arrayOf( Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO, Manifest.permission.WRITE_EXTERNAL_STORAGE ), 1 ) nativeLib.load(assets, 0, 0) cameraHelper = CameraHelper(this, cameraId,1920,1080) cameraHelper.setOnPreviewListener(object : CameraHelper.OnPreviewListener { override fun onPreviewFrame(data: ByteArray, camera: Camera?) { //预览回调 } }) binding.surfaceView.holder.addCallback(this) binding.btnTakePicture.setOnClickListener { cameraHelper.takePicture({ //拍照成功 },{ //拍照失败 }) } binding.btnVideoCapture.setOnClickListener { //开始录制 val path = MediaFileUtils.getOutputMediaFile(MediaFileUtils.MEDIA_TYPE_VIDEO)!!.path cameraHelper.startVideo(path) //cameraHelper.stopVideo() //结束录制 } } override fun surfaceCreated(holder: SurfaceHolder) { //开始预览 cameraHelper.startPreview(holder) } override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { } override fun surfaceDestroyed(holder: SurfaceHolder) { //停止预览 cameraHelper.stopPreview() } }
7. 附录
7.1 MediaFileUtils
获取媒体文件路径的工具类
object MediaFileUtils { val MEDIA_TYPE_IMAGE = 1 val MEDIA_TYPE_VIDEO = 2 /** Create a file Uri for saving an image or video */ fun getOutputMediaFileUri(type: Int): Uri { return Uri.fromFile(getOutputMediaFile(type)) } /** Create a File for saving an image or video */ fun getOutputMediaFile(type: Int): File? { // To be safe, you should check that the SDCard is mounted // using Environment.getExternalStorageState() before doing this. val mediaStorageDir = File( Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "MyCameraApp" ) // This location works best if you want the created images to be shared // between applications and persist after your app has been uninstalled. // Create the storage directory if it does not exist mediaStorageDir.apply { if (!exists()) { if (!mkdirs()) { Log.d("MyCameraApp", "failed to create directory") return null } } } // Create a media file name val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date()) return when (type) { MEDIA_TYPE_IMAGE -> { File("${mediaStorageDir.path}${File.separator}IMG_$timeStamp.jpg") } MEDIA_TYPE_VIDEO -> { File("${mediaStorageDir.path}${File.separator}VID_$timeStamp.mp4") } else -> null } } }
7.2. 本文源码下载
Android Camera1 Demo - 实现预览、拍照、录制视频功能