Android Camera2 在预览时,录制视频

349 阅读4分钟

写在前面:

  1. 本文中的方法只是实现在 Android 中使用 Camera2 的情况下,实现一个在预览时,实现时评录制。本文中的代码只是一个demo,相机相关的流程需要再细化,
  2. 另外,本文中的代码会存在一个问题,预览过程中会出现拉伸,原因是,本例中使用的是全局预览,需要再做适配,后面再补上.
  3. 本文中的代码,并不规范,只是实现效果,许多地方的异常可能发生的地方,被直接忽略了

因为代码比较简单,所以直接给出代码:

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<Size>,  
        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.StateCallbackonOpened() 中床创建 CameraCaptureSession.StateCallback 后面的过程中需要使用到 用来在 MediaRecorderSurface, 需要提前 设置好MediaRecorder 并调用 prepare 不然预览会失败
    • 所以在第一次会创建一个文件,建议处理好文件的路径,本例目前只有一个文件路径

直接总结:

  • 本文主要参考的是 google官方的demo, 官方的预览和录制视频是分开的,导致我一开始以为需要重新创建 CameraCaptureSession, 所以一开始,是在 预览视频录制 之间进行切换,但是这个方法在 切换 的时候,会有明显的卡顿。
  • 最后选择了在预览的 CameraCaptureSession 中添加 MediaRecorder 用来录制的 Surface. 最后实现的效果还是相对理想的,但是会有一个瑕疵,预览的显示和最后录制得到的视频不一致,但是我需要的是最后的结果,屏幕预览的效果,后面再适配一下,是我可以接受的效果