自定义Camera2相机实现拍照、录像时预览冻结问题求助
问题根因
你对Camera2的逻辑存在一处关键理解偏差:CameraCaptureSession的输出目标列表只是声明这个会话允许输出到这些Surface,具体每一次捕获请求(包括重复请求)实际输出到哪些Surface,是由你调用addTarget添加的目标决定的。
另外你每次拍照、启动录像都重新创建会话,Camera2的会话创建是耗时操作,过程中旧会话停止、新会话未完全启动,也是预览卡顿冻结的核心原因。你新建会话后只发起了拍照/录像的对应请求,没有把预览Surface加入请求的输出目标,相机不会给预览Surface送帧,自然就会冻住。
具体修改方案
1. 取消频繁重建CaptureSession的逻辑
提前初始化所有需要的输出Surface,一次性创建会话,不需要每次操作都重建:
- 相机启动阶段就初始化好拍照用的
ImageReader,提前拿到它的Surface - 录像相关的MediaRecorder Surface如果不想提前初始化占用资源,也可以先预留位置,兼容度更高的做法是初始化时就把三个核心Surface(预览、拍照、录像)都加入会话目标列表,绝大多数设备都支持同时输出这三类流。
修改后的预览启动逻辑示例:
private fun startCameraPreview(){ if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED){ lifecycleScope.launch(Dispatchers.Main) { // 提前初始化拍照ImageReader,不要每次拍照才创建 selectMostMatchingImageCaptureSize() selectMostMatchingVideoRecordSize() captureImageReader = ImageReader.newInstance( captureSize!!.width, captureSize!!.height, ImageFormat.JPEG, IMAGE_BUFFER_SIZE ) camera = openCamera(cameraManager, cameraId!!, cameraHandler) // 会话目标一次性加全所有需要的输出 val cameraOutputTargets = listOf( viewBinding.cameraSurfaceView.holder.surface, captureImageReader.surface, mediaRecorder.surface ) session = createCaptureSession(camera, cameraOutputTargets, cameraHandler) // 始终保持预览的重复请求运行,这是预览不冻结的核心 val captureBuilder = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW) captureBuilder.set( CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE ) captureBuilder.addTarget(viewBinding.cameraSurfaceView.holder.surface) session.setRepeatingRequest(captureBuilder.build(), null, cameraHandler) } }else{ requestCameraPermission() } }
2. 拍照逻辑简化,不需要重建会话
直接在原有会话上发起单次拍照请求即可,不会打断正在运行的预览重复请求:
private fun captureImage(){ if (captureSize == null) return viewModel.documentPreviewSizeToCaptureSizeScaleFactor = captureSize!!.width / previewSize!!.width.toFloat() lifecycleScope.launch(Dispatchers.IO) { // 直接调用拍照逻辑,不需要重建会话 takePhoto().use { result -> // 原有保存图片、写EXIF的逻辑保持不变 } } }
3. 录像逻辑修改,请求同时添加预览和录像Surface
启动录像时直接替换重复请求即可,不需要重建会话,请求里要同时加两个输出目标:
protected fun recordVideo() { lifecycleScope.launch(Dispatchers.IO) { val recordRequest = session.device .createCaptureRequest(CameraDevice.TEMPLATE_RECORD).apply { // 同时添加预览和录像的Surface,两个目标都能收到帧数据 addTarget(viewBinding.cameraSurfaceView.holder.surface) addTarget(mediaRecorder.surface) } // 替换原有预览请求,现在同时输出到预览和录像 session.setRepeatingRequest(recordRequest.build(), null, cameraHandler) mediaRecorder.start() } }
停止录像时直接切回纯预览请求即可,不需要重建会话:
private fun stopRecording() { mediaRecorder.stop() mediaRecorder.reset() // 切回纯预览的重复请求 val captureBuilder = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW) captureBuilder.set( CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE ) captureBuilder.addTarget(viewBinding.cameraSurfaceView.holder.surface) session.setRepeatingRequest(captureBuilder.build(), null, cameraHandler) }
低端设备兼容优化
如果部分老旧设备不支持同时添加三个输出流到会话,可以做降级处理:仅在启动录像的时候重建一次会话(加入预览+录像Surface),停止录像后再重建会话回到预览+拍照Surface的配置,比你原有每次拍照都重建会话的方案卡顿次数会少很多。另外要确认你选择的预览、拍照、录像分辨率都是设备支持的组合,可以通过CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP查询设备支持的流组合,避免选到不支持的配置导致输出异常。
内容的提问来源于stack exchange,提问作者Chathuranga Shan Jayarathna

