>写在前面: > 1. 本文中的方法只是实现在 `Android` 中使用 `Camera2` 的情况下,实现一个在预览时,实现时评录制。本文中的代码只是一个demo,相机相关的流程需要再细化, > 2. 另外,本文中的代码会存在一个问题,预览过程中会出现拉伸,原因是,本例中使用的是全局预览,需要再做适配,后面再补上. > 3. 本文中的代码,并不规范,只是实现效果,许多地方的异常可能发生的地方,被直接忽略了 ### 因为代码比较简单,所以直接给出代码: ```kotlin class RecordVideoWhilePreviewClient( val context: Context, val mTextureView: AutoFitTextureView, val filePath: String, val stateListener: CameraStateListener ) { companion object { const val TAG = "RecordVideoWhilePreviewClient" } private var state: State = State.NEW val SENSOR_ORIENTATION_DEFAULT_DEGREES = 90 private val SENSOR_ORIENTATION_INVERSE_DEGREES = 270 private val DEAULT_ORIENTATIONS = SparseIntArray().apply { append(Surface.ROTATION_0, 90) append(Surface.ROTATION_90, 0) append(Surface.ROTATION_180, 270) append(Surface.ROTATION_270, 180) } private val INVERSE_ORIENTATIONS = SparseIntArray().apply { append(Surface.ROTATION_0, 270) append(Surface.ROTATION_90, 180) append(Surface.ROTATION_180, 90) append(Surface.ROTATION_270, 0) } private var mBackgroundThread: HandlerThread = HandlerThread(TAG) private lateinit var mBackHandler: Handler @Volatile private var mCameraDevice: CameraDevice? = null @Volatile private var mCameraCaptureSession: CameraCaptureSession? = null @Volatile private var mCaptureRequest: CaptureRequest.Builder? = null @Volatile private var mPreviewSurface: Surface? = null @Volatile private var mRecordSurface: Surface? = null @Volatile private var mMediaRecorder: MediaRecorder? = null @Volatile private var mSensorOrientation: Int = 0 @Volatile private var mCameraOpenCloseLock = Semaphore(1) private lateinit var mPreviewSize: Size private lateinit var mVideoSize: Size private val mSurfaceTextureListener = object : TextureView.SurfaceTextureListener { override fun onSurfaceTextureAvailable(texture: SurfaceTexture, width: Int, height: Int) { openCamera(width, height) } override fun onSurfaceTextureSizeChanged(texture: SurfaceTexture, width: Int, height: Int) { configureTransform(width, height) } override fun onSurfaceTextureDestroyed(texture: SurfaceTexture) = true override fun onSurfaceTextureUpdated(texture: SurfaceTexture) { } } private val mCameraStateCallback = object : CameraDevice.StateCallback() { override fun onOpened(device: CameraDevice) { mCameraOpenCloseLock.release() mCameraDevice = device if (!mTextureView.isAvailable) return try { val texture = mTextureView.surfaceTexture!! // texture.setDefaultBufferSize(, 720) val previewSurface = Surface(texture) mPreviewSurface = previewSurface setupMediaRecorder(1280, 720) val recordSurface = mRecordSurface!! mCaptureRequest = device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply { addTarget(previewSurface) addTarget(recordSurface) } mCameraDevice?.createCaptureSession( listOf(previewSurface, recordSurface), mCaptureSessionStateCallback, mBackHandler ) } catch (e: CameraAccessException) { Log.e(TAG, "onOpened: ") } } override fun onDisconnected(device: CameraDevice) { mCameraOpenCloseLock.release() device.close() mCameraDevice = null } override fun onError(device: CameraDevice, error: Int) { mCameraOpenCloseLock.release() device.close() mCameraDevice = null } } private val mCaptureSessionStateCallback = object : CameraCaptureSession.StateCallback() { override fun onConfigured(session: CameraCaptureSession) { mCameraCaptureSession = session val camera = mCameraDevice ?: return val previewSurface = mPreviewSurface ?: return val recordSurface = mRecordSurface ?: return val captureRequest = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply { addTarget(previewSurface) addTarget(recordSurface) } mCaptureRequest = captureRequest session.setRepeatingRequest(captureRequest.build(), mCaptureCallback, mBackHandler) } override fun onConfigureFailed(session: CameraCaptureSession) { Log.e(TAG, "CameraCaptureSession.StateCallback -- onConfigureFailed: ") } } private val mCaptureCallback = object : CameraCaptureSession.CaptureCallback() { override fun onCaptureCompleted( session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult ) { super.onCaptureCompleted(session, request, result) if (state == State.INITIALIZING) { onStateChange(State.READY) } } } init { mBackgroundThread.start() mBackHandler = Handler(mBackgroundThread.looper) } @SuppressLint("MissingPermission") fun openCamera(width: Int, height: Int) { val manager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager try { if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) { throw RuntimeException("Time out waiting to lock camera opening") } val cameraId = manager.cameraIdList[0] val characteristics = manager.getCameraCharacteristics(cameraId) val range21 = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); val map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) ?: throw RuntimeException("Cannot get available preview/video sizes") mSensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!! mVideoSize = Size(1280, 720) mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture::class.java), width, height, Size(2400, 1080)); mTextureView.setAspectRatio(mPreviewSize.width, mPreviewSize.height) configureTransform(width, height) mMediaRecorder = MediaRecorder() manager.openCamera(cameraId, mCameraStateCallback, null) } catch (e: CameraAccessException) { Log.e(RecordVideoWhilePreview.TAG, "openCamera: Cannot access the camera. \n$e") } catch (e: NullPointerException) { Log.e(RecordVideoWhilePreview.TAG, "onRequestPermissionsResult: 没得相机硬件") } catch (e: InterruptedException) { throw RuntimeException("Interrupted while trying to lock camera opening.") } } private fun setupMediaRecorder(width: Int, height: Int) { var mediaRecorder = mMediaRecorder if (mMediaRecorder == null) { mediaRecorder = MediaRecorder() } else { mediaRecorder?.reset() } val rotation = (context as Activity).windowManager.defaultDisplay.rotation when (mSensorOrientation) { SENSOR_ORIENTATION_DEFAULT_DEGREES -> { mediaRecorder?.setOrientationHint(DEAULT_ORIENTATIONS.get(rotation)) } SENSOR_ORIENTATION_INVERSE_DEGREES -> { mediaRecorder?.setOrientationHint(INVERSE_ORIENTATIONS.get(rotation)) } } var recordSurface = mRecordSurface if (recordSurface == null) { recordSurface = MediaCodec.createPersistentInputSurface() mRecordSurface = recordSurface } mediaRecorder?.apply { setInputSurface(recordSurface!!) setVideoSource(MediaRecorder.VideoSource.SURFACE) setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) setOutputFile(filePath) setVideoEncodingBitRate(1280*720) setVideoSize(width, height) setVideoEncoder(MediaRecorder.VideoEncoder.H264) setCaptureRate(30.0) setVideoFrameRate(30) prepare() } mMediaRecorder = mediaRecorder } private fun chooseOptimalSize( choices: Array, width: Int, height: Int, aspectRatio: Size ): Size { val w = aspectRatio.width val h = aspectRatio.height val bigEnough = choices.filter { it.height == it.width * h / w && it.width >= width && it.height >= height } return if (bigEnough.isNotEmpty()) { Collections.min(bigEnough, CompareSizeByArea()) } else { choices[0] } } private fun configureTransform(viewWidth: Int, viewHeight: Int) { val rotation = (context as Activity).windowManager.defaultDisplay.rotation val matrix = Matrix() val viewRect = RectF(0f, 0f, viewWidth.toFloat(), viewHeight.toFloat()) val bufferRect = RectF(0f, 0f, mPreviewSize.height.toFloat(), mPreviewSize.width.toFloat()) val centerY = viewRect.centerY() val centerX = viewRect.centerX() if (Surface.ROTATION_90 == rotation || Surface.ROTATION_270 == rotation) { bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY()) matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL) val scale = Math.max( viewHeight.toFloat() / mPreviewSize.height, viewWidth.toFloat() / mPreviewSize.width ) with(matrix) { postScale(scale, scale, centerX, centerY) postRotate((90 * (rotation - 2)).toFloat(), centerX, centerY) } } mTextureView.setTransform(matrix) } private fun onStateChange(newState: State) { state = newState stateListener.onStateChange(state) val state = when (state) { State.NEW -> "STATE.NEW" State.INITIALIZING -> "STATE.INITIALIZING" State.READY -> "STATE.READY" State.RECORDING -> "STATE.RECORDING" State.COMPETE -> "STATE.COMPLETE" } context.showToast(state, Toast.LENGTH_SHORT) } fun initialize() { if (mTextureView.isAvailable) { openCamera(mTextureView.width, mTextureView.height) } else { mTextureView.surfaceTextureListener = mSurfaceTextureListener } onStateChange(State.INITIALIZING) } fun close() { mBackgroundThread?.quitSafely() try { mBackgroundThread?.join() } catch (e: InterruptedException) { Log.e(TAG, "close: ") } mPreviewSurface?.release() mRecordSurface?.release() mMediaRecorder?.release() mMediaRecorder = null } fun startRecord() { if (state != State.READY && state != State.COMPETE) return context.showToast("开始录像", Toast.LENGTH_SHORT) setupMediaRecorder(1280, 720) mMediaRecorder?.let { it.start() } onStateChange(State.RECORDING) } fun stopRecord() { if (state != State.RECORDING) return context.showToast("结束录像", Toast.LENGTH_SHORT) mMediaRecorder?.let { it.stop() it.reset() } onStateChange(State.COMPETE) } interface CameraStateListener { fun onStateChange(newState: State) } enum class State { NEW, INITIALIZING, READY, RECORDING, COMPETE, } } ``` ### 需要注意的问题: - 在创建 `MediaRecorder` 的时候,需要设置文件路径,这个时候,会在文件系统中生成一个空文件,等到录制的视频数据写入。 - 但是我们上面的代码中,在 `CameraDevice.StateCallback` 的 `onOpened()` 中床创建 `CameraCaptureSession.StateCallback` 后面的过程中需要使用到 用来在 `MediaRecorder` 的 `Surface`, 需要提前 `设置好MediaRecorder` 并调用 `prepare` 不然预览会失败 - 所以在第一次会创建一个文件,建议处理好文件的路径,本例目前只有一个文件路径 ### 直接总结: - 本文主要参考的是 [google官方的demo](https://github.com/android/camera-samples/tree/main/Camera2Basic), 官方的预览和录制视频是分开的,导致我一开始以为需要重新创建 `CameraCaptureSession`, 所以一开始,是在 *预览* 和 *视频录制* 之间进行切换,但是这个方法在 **切换** 的时候,会有明显的卡顿。 - 最后选择了在预览的 `CameraCaptureSession` 中添加 `MediaRecorder` 用来录制的 `Surface`. 最后实现的效果还是相对理想的,但是会有一个瑕疵,预览的显示和最后录制得到的视频不一致,但是我需要的是最后的结果,屏幕预览的效果,后面再适配一下,是我可以接受的效果