基于Camera2和MediaRecorder实现视频录制

2023-11-08

一、概述

视频录制,在一般开发中很少遇到,大部分开发工作都是写写页面,请求接口,展示数据等等。真要遇到,可能采用第三方库实现,一来实现快速,二来可能觉得别人实现的比较好。特别是在开发周期很紧的情况下,一般都不会自己花时间实现。

其实最好是使用手机系统的录制视频,功能完善,稳定。实现起来最简单,简简单单几句代码:

  //跳转系统的录制视频页面
  val intent = Intent(MediaStore.ACTION_VIDEO_CAPTURE)
  intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY,1)
  intent.putExtra(MediaStore.EXTRA_DURATION_LIMIT,30)//录制时长
  startActivityForResult(intent, 666)

   //打开手机的选择视频页
//  val intent = Intent()
//  intent.action = Intent.ACTION_PICK
//  intent.data = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
//  startActivityForResult(intent,666)

然后在onActiityResult方法中接收录制好的视频路径处理即可。

但如果需求是像微信那样app内的录制视频,就不能使用系统自带录制功能,需要自己实现。

下面将我自己实现的,记录下来。这里也只是实现了一个简单的录制功能,甚至还有问题:前置摄像头录制视频是镜像的。
另外下面的实现不支持在Android6.0以下的手机上使用,因为使用到了API23的方法:MediaCodec.createPersistentInputSurface(),主要是为了能支持横屏录制的视频方向为横屏。

先看看演示效果:
在这里插入图片描述

二、实现方案和细节

使用的Camera2 和 MediaRecorder。
如果使用Camera1的话,可能会更简单一些,Camera2用起来确实相对麻烦一点。不过Camera1毕竟已经被弃用了,且使用Camera1打开相机比Camera2要耗时一些。

Camera2使用

  1. 用CameraManager获取相机Id列表cameraIdList,然后openCamera指定的相机id,打开相机
  2. 打开成功后,使用 CameraDevice.createCaptureSession 创建CameraCaptureSession
  3. 创建成功后,使用CameraCaptureSession.setRepeatingRequest 发起预览请求,它需要传入CaptureRequest,通过CameraDevice.captureRequest创建,CaptureRequest可以设置一些参数,对焦、曝光、闪光灯等等

第2步 createCaptureSession 时需要传入Surface列表。

这里传入了两个Surface,一个是预览使用,由SurfaceView提供。
另一个是录制使用,通过MediaCodec.createPersistentInputSurface() 创建,设置给MediaRecorder。
如果预览时不创建MediaRecorder,只传入预览Surface,等到点击录制时,才创建MediaRecorder,需要重新创建createCaptureSession,传入新的Surface,这样虽然可以,但是点击录制时会很慢,预览画面会断一下。

第2步传入的Surface列表,还需要在第3步中使用CaptureRequest.addTarget 添加,两个地方必须对应,不然发起预览请求时会报错。

MediaRecorder配置

因为使用的是Camera2,所以不能使用MediaRecorder.setCamera()。
替代方法是使用MediaRecorder.surface,前提是需要设置数据源为Surface

mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE)

且需要在mediaRecorder.prepare之后,mediaRecorder.surface 才可用。

后面因为要支持横屏录制,没有采用 mediaRecorder.surface 。
如果进入页面开启相机预览时手机竖屏,点击录制时手机横屏,因为在预览时就创建了mediaRecorder,并且setOrientationHint确定了视频方向,无法再改变(只能prepare之前设置),这时录制的视频方向肯定就不对。

要改变视频方向,只能重新创建 mediaRecorder ,但是重新创建mediaRecorder,同时也重新创建了一个新的Sueface,需要重新createCaptureSession传入新的Sueface。(改成点击录制时,创建mediaRecorder,然后重新createCaptureSession,测试中也发现画面会断一下,效果不好)。

正因如此,最终改为使用 MediaCodec.createPersistentInputSurface() 创建 Surface,然后 setInputSurface 给 mediaRecorder。MediaCodec.createPersistentInputSurface()创建的Surface只有在mediaRecorder.prepare之后才可用。 在点击录制时,重新配置mediaRecorder,设置新的方向。这样虽然mediaRecorder重新配置了,但是Surface还是同一个。

		var mediaRecorder = MediaRecorder()
        recordSurface = MediaCodec.createPersistentInputSurface()
        var recordSurface = mRecordSurface
        if (recordSurface == null) {
            recordSurface = MediaCodec.createPersistentInputSurface()
            mRecordSurface = recordSurface
        }
        mediaRecorder.setInputSurface(recordSurface)
        mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC)
        //数据源来之surface
        mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE)
        //设置QUALITY_HIGH,可以提高视频的录制质量(文件也会变大),但是不能设置编码格式和帧率等参数,否则报错
        val profile = CamcorderProfile.get(CamcorderProfile.QUALITY_480P)
        mediaRecorder.setProfile(profile)
        mediaRecorder.setMaxFileSize(0)
        mediaRecorder.setVideoSize(width, height)
        //视频方向
        mediaRecorder.setOrientationHint(mRecordVideoOrientation)
        val parentFile = externalCacheDir ?: cacheDir
        val fileName = "${System.currentTimeMillis()}.mp4"
        //不设置setOutputFile prepare时会报错
        mediaRecorder.setOutputFile(parentFile.absolutePath + File.separator + fileName)
        //prepare之后recordSurface才能用
        mediaRecorder.prepare()

三、全部代码

/**
 * 录制视频
 * 支持后置、前置摄像头切换(但前置摄像头录制视频是镜像的,需要翻转) TODO
 * Android 6.0以下不支持
 * 支持横屏录制
 * 2023/03/17
 */
@RequiresApi(Build.VERSION_CODES.M)
class VideoRecord23Activity : AppCompatActivity(), SurfaceHolder.Callback,
    View.OnClickListener {

    companion object {
        const val DURATION = "duration"
        const val FILE_NAME = "name"
        const val FILE_PATH = "path"
    }
    private val requestPermissionCode = 52
    private val requestActivityCode = 10

    /**
     * mCountDownMsg 录制进度倒计时msg,一秒钟发送一次
     * mStartRecordMsg 开始录制
     * mCameraOpenFailMsg 相机打开失败
     * mCameraPreviewFailMsg 相机预览失败
     * mRecordErrorMsg 录制出现错误
     * 在 mCountDownHandler(主线程的Handler)中处理
     */
    private val mCountDownMsg = 19
    private val mStartRecordMsg = 20
    private val mCameraOpenFailMsg = 21
    private val mCameraPreviewFailMsg = 22
    private val mRecordErrorMsg = 23
    private lateinit var mSurfaceView :SurfaceView
    private lateinit var mRecordProgressBar: ProgressBar
    private lateinit var mRecordStateIv: ImageView
    private lateinit var mFlashlightIv: ImageView
    private lateinit var mSwitchCameraIv: ImageView
    /**
     * 录制视频文件路径
     */
    @Volatile
    private var mFilePath: String? = null

    /**
     * 录制的视频文件名
     */
    @Volatile
    private var mFileName: String? = null

    /**
     * 预览画面尺寸,和视频录制尺寸
     */
    @Volatile
    private var mRecordSize: Size? = null

    /**
     * 相机方向
     */
    private var mCameraOrientation: Int = 0

    /**
     * 录制视频的方向,随着手机方向的改变而改变
     */
    @Volatile
    private var mRecordVideoOrientation: Int = 0

    /**
     * 默认打开后置相机 LENS_FACING_BACK
     * 可以切换为前置相机 LENS_FACING_FRONT
     */
    private var mFensFacing = CameraCharacteristics.LENS_FACING_BACK

    /**
     * 预览Surface
     */
    @Volatile
    private var mPreviewSurface: Surface? = null

    /**
     * 录制Surface
     */
    @Volatile
    private var mRecordSurface: Surface? = null

    @Volatile
    private var mCameraDevice: CameraDevice? = null

    @Volatile
    private var mCameraCaptureSession: CameraCaptureSession? = null

    @Volatile
    private var mCaptureRequest: CaptureRequest.Builder? = null
    private var mOrientationEventListener: OrientationEventListener? = null

    @Volatile
    private var mMediaRecorder: MediaRecorder? = null
    /**
     * 是否是录制中的状态
     * true:录制中
     */
    @Volatile
    private var mRecordingState = false

    /**
     * 是否录制完成。从手动点击开始录制到手动点击停止录制(或者录制时长倒计时到了),为录制完成,值为true。其他情况为false
     */
    private var mRecordComplete = false

    /**
     * 闪光灯状态
     * true 开启
     * false 关闭
     */
    private var mFlashlightState = false

    /**
     * 是否可以录制
     * 录制完成,跳转播放页面后,返回时点击的完成,不能录制
     * 其他情况都为可以录制
     */
    private var mRecordable = true

    /**
     * 录制最大时长,时间到了之后录制完成
     * 单位:秒
     */
    private var mMaxRecordDuration = 30

    /**
     * 已录制的时长
     */
    private var mCurrentRecordDuration = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_video_record)

        mSurfaceView = findViewById(R.id.surfaceView)
        mSurfaceView.holder.addCallback(this)
        mRecordStateIv = findViewById(R.id.recordStateIv)
        mRecordProgressBar = findViewById(R.id.recordProgressBar)
        mFlashlightIv = findViewById(R.id.flashlightIv)
        mSwitchCameraIv = findViewById(R.id.switchIv)
        mRecordStateIv.setOnClickListener(this)
        mFlashlightIv.setOnClickListener(this)
        mSwitchCameraIv.setOnClickListener(this)

        initOrientationEventListener()
        mMaxRecordDuration = intent.getIntExtra(DURATION, 30)
        mSurfaceView.scaleX = -1f
    }

    override fun surfaceCreated(holder: SurfaceHolder?) {
        if (mRecordable) {
            mRecordComplete = false
            mFlashlightState = false
            mFlashlightIv.setImageResource(R.drawable.flashlight_off)
            checkPermissionAndOpenCamera()
            mOrientationEventListener?.enable()
        }
    }

    override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) {
    }

    override fun surfaceDestroyed(holder: SurfaceHolder?) {
    }

    @SuppressLint("MissingPermission")
    override fun onClick(v: View) {
        when (v.id) {
            R.id.recordStateIv -> {
                if (mRecordingState) {
                    //停止录制
                    stopRecord()
                    mRecordComplete = true
                    //跳转预览页面
                    openPlayActivity()
                } else {
                    startRecord()
                    mOrientationEventListener?.disable()
                }
            }
            R.id.flashlightIv -> {
                val captureRequest = mCaptureRequest ?: return
                val cameraCaptureSession = mCameraCaptureSession ?: return
                if (mFlashlightState) {
                    //闪光灯开启,点击关闭
                    captureRequest[CaptureRequest.FLASH_MODE] = CaptureRequest.FLASH_MODE_OFF
                    mFlashlightIv.setImageResource(R.drawable.flashlight_off)
                } else {
                    //闪关灯关闭,点击开启
                    captureRequest[CaptureRequest.FLASH_MODE] = CaptureRequest.FLASH_MODE_TORCH
                    mFlashlightIv.setImageResource(R.drawable.flashlight_on)
                }
                mFlashlightState = !mFlashlightState
                cameraCaptureSession.stopRepeating()
                mCameraCaptureSession?.setRepeatingRequest(captureRequest.build(), mCameraCaptureSessionCaptureCallback, mRecordThreadHandler)
            }
            R.id.switchIv -> {
                if (mRecordingState) {
                    //正在录制中
                    Toast.makeText(this, "正在录制", Toast.LENGTH_SHORT).show()
                    return
                }
                if (mFensFacing == CameraCharacteristics.LENS_FACING_BACK) {
                    //当前打开的是后置摄像头,切换到前置摄像头
                    mFensFacing = CameraCharacteristics.LENS_FACING_FRONT
                    close()
                    openCamera()
                } else if (mFensFacing == CameraCharacteristics.LENS_FACING_FRONT) {
                    //当前打开的是前置摄像头,切换到后置摄像头
                    mFensFacing = CameraCharacteristics.LENS_FACING_BACK
                    close()
                    openCamera()
                }
            }
        }
    }

    private fun startRecord() {
        mRecordThreadHandler.sendEmptyMessage(mStartRecordMsg)
        mRecordingState = true
        mRecordStateIv.setImageResource(R.drawable.record_start_state_bg)
        mCurrentRecordDuration = 0
        //开始录制倒计时
        mUiHandler.sendEmptyMessageDelayed(mCountDownMsg, 1000)
    }

    private fun stopRecord() {
        //视图变为 停止录制状态
        mRecordingState = false
        mRecordStateIv.setImageResource(R.drawable.record_stop_state_bg)
        mRecordProgressBar.progress = 0
        mUiHandler.removeMessages(mCountDownMsg)
        val mediaRecorder = mMediaRecorder ?: return
        try {
            mediaRecorder.stop()
        } catch (t: Throwable) {
            t.printStackTrace()
        }
    }

    /**
     * 检查相机和录音与权限,并打开相机
     */
    private fun checkPermissionAndOpenCamera(){
        //录制音频权限ok
        val audioOk = ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
        //相机权限ok
        val cameraOk = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)== PackageManager.PERMISSION_GRANTED
        if (audioOk && cameraOk) {
            openCamera()
        } else if (!audioOk && !cameraOk) {
            val array = arrayOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
            ActivityCompat.requestPermissions(this, array, requestPermissionCode)
        } else if (!audioOk && cameraOk) {
            val array = arrayOf(Manifest.permission.RECORD_AUDIO)
            ActivityCompat.requestPermissions(this, array, requestPermissionCode)
        } else if (audioOk && !cameraOk) {
            val array = arrayOf(Manifest.permission.CAMERA)
            ActivityCompat.requestPermissions(this, array, requestPermissionCode)
        }
    }

    @SuppressLint("MissingPermission")
    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == requestPermissionCode) {
            for (i in grantResults) {
                if (i != PackageManager.PERMISSION_GRANTED) {
                    Toast.makeText(this, "请开启相机和录音权限", Toast.LENGTH_SHORT).show()
                    finish()
                    return
                }
            }
            openCamera()
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == requestActivityCode) {
            when (resultCode) {
                //确认
                Activity.RESULT_OK -> {
                    val fileName = mFileName ?: return
                    val filePath = mFilePath ?: return
                    //不能再录制
                    mRecordable = false
                    val intent = Intent()
                    //把录制的视频文件名、文件路径传给外面调用的页面
                    intent.putExtra(FILE_NAME, fileName)
                    intent.putExtra(FILE_PATH, filePath)
                    setResult(Activity.RESULT_OK, intent)
                    finish()
                }
                //重新录制
                Activity.RESULT_CANCELED -> {
                    //删除文件,重新录制
                    deleteRecordFile()
                }
            }
        }
    }

    /**
     * 页面暂停时,关闭相机,停止录制
     */
    override fun onStop() {
        super.onStop()
        close()
        mOrientationEventListener?.disable()
        mRecordThreadHandler.removeMessages(mStartRecordMsg)
    }

    override fun onDestroy() {
        super.onDestroy()
        mRecordThreadHandler.looper.quit()
        mPreviewSurface?.release()
        mRecordSurface?.release()
        mMediaRecorder?.release()
        mMediaRecorder = null
    }

    /**
     * 准备录制相关处理
     */
    @RequiresPermission(Manifest.permission.CAMERA)
    fun openCamera() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            //使用context可能会出现内存泄漏(红米手机上),CameraManager会一直持有context
            val cameraManager = applicationContext.getSystemService(CAMERA_SERVICE) as? CameraManager
            val cameraIdList = cameraManager?.cameraIdList
            if (cameraManager == null || cameraIdList == null || cameraIdList.isEmpty()) {
                Toast.makeText(this, "无法使用设备相机", Toast.LENGTH_SHORT).show()
                finish()
                return
            }
            for (id in cameraIdList) {
                var cameraCharacteristics: CameraCharacteristics? = null
                try {
                    cameraCharacteristics = cameraManager.getCameraCharacteristics(id)
                } catch (t: Throwable) {
                    t.printStackTrace()
                }
                if (cameraCharacteristics == null) {
                    continue
                }
                val fensFacing = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING)
                if (fensFacing != mFensFacing) {
                    continue
                }
                val level = cameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)
                val capabilities = cameraCharacteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)  
                mCameraOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0
                val map = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) ?: continue
//                //获取预览支持的分辨率
//                val outputSizes = map.getOutputSizes(SurfaceHolder::class.java)
//                Log.i("TAG", "prepareRecord: outputSizes ${Arrays.toString(outputSizes)}")
                //获取录制支持的分辨率
                val recorderSizes = map.getOutputSizes(MediaRecorder::class.java)
//                Log.i("TAG", "prepareRecord: recorderSizes ${Arrays.toString(recorderSizes)}")
                val recordSize = getRecordSize(recorderSizes)
                mRecordSize = recordSize
                resizeSurfaceSize(recordSize.width, recordSize.height)
                mSurfaceView.holder.setFixedSize(recordSize.width, recordSize.height)
                try {
                    cameraManager.openCamera(id, mCameraDeviceStateCallback, mRecordThreadHandler)
                } catch (t: Throwable) {
                    t.printStackTrace()
                    Toast.makeText(this, "相机打开失败,请关闭重试", Toast.LENGTH_SHORT).show()
                }
                break
            }
        } else {
            //6.0以下
            Toast.makeText(this, "Android系统版本太低不支持", Toast.LENGTH_SHORT).show()
            finish()
        }
    }

    private val mCameraDeviceStateCallback: CameraDevice.StateCallback by lazy(LazyThreadSafetyMode.NONE) {
        object : CameraDevice.StateCallback() {
            override fun onOpened(camera: CameraDevice) {
                mCameraDevice = camera
                val recordSize = mRecordSize ?: return
                //预览surface
                val previewSurface = mSurfaceView.holder.surface
                mPreviewSurface = previewSurface
                setupMediaRecorder(recordSize.width, recordSize.height, false)
                val recordSurface = mRecordSurface
                mRecordSurface = recordSurface
                val surfaceList = listOf(previewSurface, recordSurface)
                camera.createCaptureSession(surfaceList, mCameraCaptureSessionStateCallback, mRecordThreadHandler)
            }

            override fun onDisconnected(camera: CameraDevice) {
                //相机连接断开
                if (mCameraDevice != null) {
                    close()
                } else {
                    camera.close()
                }
            }

            override fun onError(camera: CameraDevice, error: Int) {
                camera.close()
                mUiHandler.sendEmptyMessage(mCameraOpenFailMsg)
            }
        }
    }

    private val mCameraCaptureSessionStateCallback: CameraCaptureSession.StateCallback by lazy {
        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)
                mCaptureRequest = captureRequest
                captureRequest.addTarget(previewSurface)
                captureRequest.addTarget(recordSurface)
                
                //对焦
                captureRequest.set(CaptureRequest.CONTROL_AF_MODE,CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO)
                //自动曝光
                captureRequest.set(CaptureRequest.CONTROL_AE_MODE,CaptureRequest.CONTROL_AE_MODE_ON)
                //进行重复请求录制预览
                session.setRepeatingRequest(captureRequest.build(), mCameraCaptureSessionCaptureCallback, mRecordThreadHandler)
            }

            override fun onConfigureFailed(session: CameraCaptureSession) {
                session.close()
                mUiHandler.sendEmptyMessage(mCameraPreviewFailMsg)
            }
        }
    }

    private val mCameraCaptureSessionCaptureCallback: CameraCaptureSession.CaptureCallback by lazy(LazyThreadSafetyMode.NONE){
        object :CameraCaptureSession.CaptureCallback(){
            override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) {
                super.onCaptureCompleted(session, request, result)
                //这个方法在预览过长中,会一直被回调
//                Log.i("TAG", "onCaptureCompleted thread ${Thread.currentThread().name}")
            }

            override fun onCaptureFailed(session: CameraCaptureSession, request: CaptureRequest, failure: CaptureFailure) {
                super.onCaptureFailed(session, request, failure)
            }
        }
    }

    /**
     * 录制线程的Handler
     */
    private val mRecordThreadHandler: Handler by lazy(LazyThreadSafetyMode.NONE) {
        val recordThread = HandlerThread("RecordVideoThread")
        recordThread.start()
        object : Handler(recordThread.looper) {
            override fun handleMessage(msg: Message) {
                if (isFinishing || isDestroyed) return
                when (msg.what) {
                    mStartRecordMsg -> {//开始录制
                        if (!mRecordingState) {//不是开始录制状态,return
                            return
                        }
                        val recordSize = mRecordSize ?: return
                        try {
                            //重新配置MediaRecorder,因为用户刚打开页面时的手机方向,和点击录制时的手机方向可能不一样,所以重新配置。注意是为了支持横屏录制的视频为横屏视频,不然都是竖屏视频
                            setupMediaRecorder(recordSize.width, recordSize.height, true)
                            //视图变为录制状态
                            mMediaRecorder?.start()
                        } catch (t: Throwable) {
                            t.printStackTrace()
                            //录制出现错误
                            mUiHandler.sendEmptyMessage(mRecordErrorMsg)
                        }
                    }
                }
            }
        }
    }

    /**
     * ui线程Handler 处理录制倒计时,相机打开失败相关消息
     */
    private val mUiHandler: Handler by lazy(LazyThreadSafetyMode.NONE) {
        object : Handler(Looper.getMainLooper()) {
            override fun handleMessage(msg: Message?) {
                if (isFinishing || isDestroyed) {
                    return
                }
                when(msg?.what){
                    mCountDownMsg -> {
                        mCurrentRecordDuration += 1
                        val progress = (mCurrentRecordDuration * 1f / mMaxRecordDuration * 100 + 0.5f).toInt()
                        mRecordProgressBar.progress = progress
                        if (mCurrentRecordDuration >= mMaxRecordDuration) {
                            //录制时间到了,停止录制
                            stopRecord()
                            mRecordComplete = true
                            //跳转预览页面
                            openPlayActivity()
                        } else {
                            sendEmptyMessageDelayed(mCountDownMsg, 1000)
                        }
                    }
                    mCameraOpenFailMsg -> {
                        Toast.makeText(this@VideoRecord23Activity, "相机打开失败,请关闭重试", Toast.LENGTH_SHORT).show()
                    }
                    mCameraPreviewFailMsg -> {
                        Toast.makeText(this@VideoRecord23Activity, "相机预览失败,请关闭重试", Toast.LENGTH_SHORT).show()
                    }
                }
            }
        }
    }

    /**
     * 创建并配置 MediaRecorder
     * @param width 视频宽度
     * @param height 视频高度
     * @param  outputFileCreated 输出文件是否已经创建;第一次prepare时,文件已经创建了,开始录制时,不用再次创建文件
     */
    private fun setupMediaRecorder(width: Int, height: Int, outputFileCreated: Boolean): MediaRecorder {
        var mediaRecorder = mMediaRecorder
        if (mediaRecorder == null) {
            mediaRecorder = MediaRecorder()
            mMediaRecorder = mediaRecorder
        } else {
            mediaRecorder.reset()
        }
        var recordSurface = mRecordSurface
        if (recordSurface == null) {
            recordSurface = MediaCodec.createPersistentInputSurface()
            mRecordSurface = recordSurface
        }
        mediaRecorder.setInputSurface(recordSurface)
        mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC)
        //数据源来之surface
        mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE)

        //设置QUALITY_HIGH,可以提高视频的录制质量(文件也会变大),但是不能设置编码格式和帧率等参数,否则报错
        val profile = CamcorderProfile.get(CamcorderProfile.QUALITY_480P)
        mediaRecorder.setProfile(profile)
        mediaRecorder.setMaxFileSize(0)
        mediaRecorder.setVideoSize(width, height)
        //视频方向
        mediaRecorder.setOrientationHint(mRecordVideoOrientation)
        //录制文件没有创建,创建文件
        if (!outputFileCreated) {
            val parentFile = externalCacheDir ?: cacheDir
            val fileName = "${System.currentTimeMillis()}.mp4"
            mFileName = fileName
            mFilePath = parentFile.absolutePath + File.separator + fileName
        }
        //不设置setOutputFile prepare时会报错
        mediaRecorder.setOutputFile(mFilePath)

        //prepare之后recordSurface才能用
        mediaRecorder.prepare()
        return mediaRecorder
    }

    /**
     * 页面关闭或不在前台时,停止录制、释放相机
     */
    private fun close() {
        if (mRecordingState) {
            //停止录制
            stopRecord()
        }
        if (!mRecordComplete) {
            //没有录制完成,或者没有开始录制过(MediaRecorder prepare时会创建文件),删除录制的文件
            deleteRecordFile()
        }
        //释放相机
        val previewSurface = mPreviewSurface
        if (previewSurface != null) {
            mCaptureRequest?.removeTarget(previewSurface)
        }
        val recordSurface = mRecordSurface
        if (recordSurface != null) {
            mCaptureRequest?.removeTarget(recordSurface)
        }
        mCameraCaptureSession?.close()
        mCameraDevice?.close()
        mCaptureRequest = null
        mCameraCaptureSession = null
        mCameraDevice = null
    }

    /**
     * 删除录制的文件
     */
    private fun deleteRecordFile() {
        val filePath = mFilePath ?: return
        try {
            val file = File(filePath)
            if (file.exists()) {
                file.delete()
            }
            mFilePath = null
        } catch (t: Throwable) {
            t.printStackTrace()
        }
    }

    /**
     * 获取录制的视频尺寸
     * @param sizes 支持的尺寸列表
     */
    private fun getRecordSize(sizes: Array<Size>): Size {
        //参考尺寸 1280*720
        val compareWidth = 1280
        val compareHeight = 720
        var resultSize = sizes[0]
        var minDiffW = Int.MAX_VALUE
        var minDiffH = Int.MAX_VALUE
        for (size in sizes) {
            if (size.width == compareWidth && size.height == compareHeight) {
                resultSize = size
                break
            }
            //找到最接近 1280*720的size
            val diffW = abs(size.width - compareWidth)
            val diffH = abs(size.height - compareHeight)
            if (diffW < minDiffW && diffH < minDiffH) {
                minDiffW = diffW
                minDiffH = diffH
                resultSize = size
            }
        }
        return resultSize
    }

    /**
     * 根据视频宽高,修改surfaceView的宽高,来适应预览尺寸
     *
     * @param width  预览宽度
     * @param height 预览高度
     */
    private fun resizeSurfaceSize(height: Int, width: Int) {
        val displayW: Int = mSurfaceView.width
        val displayH: Int = mSurfaceView.height
        if (displayW == 0 || displayH == 0) return
        var ratioW = 1f
        var ratioH = 1f
        if (width != displayW) {
            ratioW = width * 1f / displayW
        }
        if (height != displayH) {
            ratioH = height * 1f / displayH
        }
        var finalH = displayH
        var finalW = displayW
        if (ratioW >= ratioH) {
            finalH = (height / ratioW).toInt()
        } else {
            finalW = (width / ratioH).toInt()
        }
        val layoutParams = mSurfaceView.layoutParams
        if (layoutParams.width == finalW && layoutParams.height == finalH) {
            return
        }
        layoutParams.width = finalW
        layoutParams.height = finalH
        mSurfaceView.layoutParams = layoutParams
    }

    /**
     * 监听手机方向改变,计算录制时的视频方向。横屏录制时,视频横屏。竖屏录制时,视频竖屏
     */
    private fun initOrientationEventListener() {
        val orientationEventListener = object : OrientationEventListener(this) {
            override fun onOrientationChanged(orientation: Int) {
                if (orientation == ORIENTATION_UNKNOWN) return
                val rotation = (orientation + 45) / 90 * 90
                if (mFensFacing == CameraCharacteristics.LENS_FACING_BACK) {
                    //后置摄像头
                    mRecordVideoOrientation = (mCameraOrientation + rotation) % 360
                } else if (mFensFacing == CameraCharacteristics.LENS_FACING_FRONT) {
                    //前置摄像头
                    mRecordVideoOrientation = mCameraOrientation - rotation
                }
            }
        }
        mOrientationEventListener = orientationEventListener
    }

    /**
     * 跳转录制视频预览页面
     */
    private fun openPlayActivity() {
        //val intent = Intent(this, VideoPlayActivity::class.java)
        //intent.putExtra(VideoPlayActivity.FILE_PATH, mFilePath)
        //startActivity(intent)
    }
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

基于Camera2和MediaRecorder实现视频录制 的相关文章

  • Espresso - 检查使用按钮按下意图打开哪个活动?

    是否可以跟踪按下某个按钮后打开了哪个 Activity 我有一个测试 其中当单击 按下按钮时 it 向服务器发送请求 直到发送请求时 它打开一个活动 验证是否执行成功在测试中 我需要检查打开的 Activity 是什么 我的测试示例 检查
  • ACTION_VIEW 的 Intent.createChooser 仅显示默认浏览器

    我正在尝试使用 Intent createChooser 显示应用程序选择器对话框 该对话框将列出用户手机中所有可用的网络浏览器 我正在使用下面的代码 Intent browserIntent new Intent Intent ACTIO
  • doInBackground 运行时是否可以停止 asynctask?

    我正在 ActivityB Oncreate 中创建异步任务 在该任务中 我正在运行无限 while 循环doInBackground 当我转到上一个活动并再次回到该活动时 创建了另一个异步任务 我的问题现在是两个无限 while 循环正在
  • 更改工具栏中汉堡图标的大小?

    我有两个问题 可能很奇怪 但无论如何 我有带有应用程序标题的工具栏 如何将其更改为非徽标的图片 下一个问题 是否可以设置 更改工具栏中汉堡图标的大小 我在下面的下一个代码的帮助下制作了经典的导航抽屉 我也使用了 ActionBarDrawe
  • 如何从另一个xml文件动态更新xml文件?

    我想从另一个 xml 文件更新 xml 文件 我使用了一个 xml 文件 如下所示 one xml
  • 无法解析符号 FlutterActivity

    我使用 VCS gt Checkout from Version Control 将 flutter 项目从 github 导入到 Android Studio 中 现在我面临的问题是 Cannot resolve symbol Flutt
  • 无法合并 Dex - Android Studio 3.0

    当我在稳定频道中将 Android Studio 更新到 3 0 并运行该项目时 我开始收到以下错误 Error Execution failed for task app transformDexArchiveWithExternalLi
  • 更改 AChartEngine 中的图例大小

    我想专门更改饼图的图例大小输出 我已经尝试了所有可以找到的 AChartEngine 方法 但没有一个只能更改图例文本大小 我必须重写 onDraw 函数吗 如果是这样 怎么办 要设置图例高度 请使用 renderer setLegendH
  • MI设备中即使应用程序被杀死,如何运行后台服务

    您好 我正在使用 alaram 管理器运行后台服务 它工作正常 但对于某些 mi 设备 后台服务无法工作 我使用了服务 但它无法工作 如何在 mi 中运行我的后台服务 MI UI有自己的安全选项 所以你需要的不仅仅是上面提到的粘性服务 你需
  • Android SDK 中可用的所有“android.intent.action”操作的详尽列表是什么?

    大家好 我想知道标准 Android SDK 中定义的所有 Intent 操作是否有详尽的参考 我正在考虑完整的 android intent action someaction 名称 而不是 Intent 类中定义的方便别名操作的限制列表
  • 如何在照片删除后刷新 Android 的 MediaStore

    问题 如何使媒体存储刷新其已删除文件的条目 从外部存储中删除代码中的照片后 我仍然在图库中看到已删除照片的插槽 空白照片 画廊似乎反映了媒体存储 并且在媒体存储中找到了已删除的照片 直到手机重新启动或通常 直到重新扫描媒体为止 尝试扫描已删
  • 服务器到 Firebase HTTP POST 结果为响应消息 200

    使用 Java 代码 向下滚动查看 我使用 FCM 向我的 Android 发送通知消息 当提供正确的服务器密钥令牌时 我收到如下所示的响应消息 之后从 FCM 收到以下响应消息 Response 200 Success Message m
  • 如何将单选按钮状态保存到已保存/共享首选项?

    我可以在保存的首选项中保存字符串 但很难保存单选按钮 public class PersonalDetailsf extends Activity private SharedPreferences sharedPreferences pr
  • 我们可以在 android studio 中拥有没有 app 文件夹的项目,并将所有内容(java/res/etc)放在根目录中吗

    我想知道在 Android studio 中是否可以有没有应用程序模块 应用程序或任何其他名称 的 android 项目 意味着我可以在项目本身的根目录中创建包和资源 而不是使用应用程序模块 编辑 结构看起来像 MyApp idea gra
  • 有没有办法在Android上创建一个三角形按钮?

    有没有办法创建一个三角形的按钮 我知道我可以将三角形图像作为背景 但这将使三角形之外的区域可单击 有没有办法固定按钮角 X 和 Y 以便我可以将其变成三角形 您可以覆盖OnTouch http developer android com r
  • 带有空白白屏的 WebView

    我在 DialogFragment 中有一个 webview 它使用以下方式显示文档和 PDF它可以进行几次尝试 但如果用户尝试频繁打开和关闭对话框 webview 将显示空白屏幕 我已经尝试了所有的线程link1 https stacko
  • Android:单一活动,多个视图

    我不是 Android 专业人士 尽管我开发了一个包含 50 多个活动的应用程序 这使得该应用程序非常庞大 经过8周的开发 现在出现了一些问题 导致应用程序难以维护和升级 我正在处理的主要问题是 我无法将对象引用传递给活动的构造函数 事实上
  • 按名称查找视图

    是否可以通过名称而不是 id 来查找视图 findViewById R id someView 但我想做这样的事情 findViewByName someView 在处理 xml 时 您必须通过标识符查找视图 但是您可以使用以下方式查找标识
  • OpenGL ES 2.0 屏幕闪烁

    我面临着一个大问题 我正在使用带有 Android 4 0 3 的 Transformer tf101 选项卡 我的应用程序使用自定义 OpenGL ES 2 0 表面 我正在用纹理渲染多个平面 该纹理大约发生变化 每秒 20 次 并通过传
  • 修改 ADW Android 启动器?

    我想更改和修改开源 ADW 启动器 启动器可在此处获取 https github com AnderWeb android packages apps Launcher https github com AnderWeb android p

随机推荐

  • 这些错误你都犯过吗?来看看9大XMind初学者常见错误!

    当我们在思考一个事件时 我们往往会将自己的想法通过思维导图的方式展现出来 XMind就是一款强大的思维导图和头脑风暴工具 数十年来一直是百万用户的首选 gt gt 点击下载Mind ZEN试用版 lt lt 对于初学者来说 常常会犯一些固有
  • TypeC规范--CC连接

    上次在TypeC与PD快充文章内我们在后面放了几张图 如下图 上图位于TypeC规范章节4 5 1 3 3 我们以这个图为例介绍一下 我们可以看到在VBus上会有MOS去切换source和sink的通路 这个是DRP在VBUS上的特点 我们
  • Entity Framework Core系列教程-26-数据迁移

    Entity Framework Core数据迁移 迁移是一种通过保留数据来使数据库架构与EF Core模型保持同步的方法 如上图所示 EF Core API从域 实体 类构建EF Core模型 并且EF Core迁移将基于EF Core模
  • Ubuntu获取串口权限使用CAN分析仪

    Ubuntu获取串口权限使用CAN分析仪 通过以下命令获取usb设备信息 lsusb 可以得到以下信息 Bus 002 Device 001 ID 1d6b 0003 Linux Foundation 3 0 root hub Bus 00
  • Linux Maven-v3.8.6的安装与配置

    Maven v3 8 6的安装与配置 官网下载页地址 maven依赖jdk环境 且需要 jdk1 7 以上版本 安装之前需要先配置好 JAVA HOME 的环境变量 有需要可以看之前的 JDK 11的安装与环境变量配置 1 下载 解压 wg
  • 2023华为OD机试真题【查找单入口空闲区域/回溯法】

    题目描述 给定一个 m xn 的矩阵 由若干字符 和0构成 X表示该处已被占据 0 表示该处空闲 请找到最大的单入口空闲区域 解释 空闲区域是由连通的O组成的区域 位于边界的0可以构成入口 单入口空闲区域即有目只有一个位于边界的0作为入口的
  • 教你从零开始搭建私有网盘及个人博客(云服务器基础使用教程)

    云服务器选择 首先 你必须购买一个的服务器 不过你可以到阿里云的 高校学生 在家实践 计划 http suo im 5sMWSq 中免费领取6个月的云服务器先试试手 选配服务器和选配个人电脑差不多 所以配置方面不多提 我们直接看到 镜像 选
  • Threejs加载OBJ模型

    threejs加载OBJ模型代码
  • 2014年6月23日-6月29日(共11小时,剩4424小时)

    6月23日 上午有事 下午1小时 剩4438小时 6月24日 3小时 剩4435小时 6月25日 5小时 剩4430小时 6月26日 1小时 剩4429小时 6月27日 3小时 剩4426小时 6月29日 2小时 剩4424小时
  • 论文笔记:When Do Contrastive Learning Signals Help Spatio-TemporalGraph Forecasting?

    2022 SigSpatial 1 intro 1 1 背景 论文认为数据稀缺是阻碍时空图 STG 预测的一个关键问题 在这一领域的公共数据集通常只包含几个月的数据 限制了可以构建的训练实例数量 gt 学习模型可能会对训练数据过拟合 导致泛
  • Redis的这些都没有掌握,你还想要拿Offer?

    Redis 简介 Redis 是完全开源免费的 遵守 BSD 协议 是一个高性能的 key value 数据库 Redis 与 其他 key value 缓存产品有以下三个特点 Redis 支持数据持久化 可以将内存中的数据保存在磁盘中 重
  • 反爬虫策略的应对方法汇总

    现在越来越多的工作需要用到爬虫程序 但是同时也有很多人会通过爬虫程序恶意竞争 因此为了能够保护自己的正当权益 各种反爬虫程序被研发利用起来 所以很多时候 在进行爬虫工作的时候首要面对的就是爬虫和反爬虫的拉锯战 这里就说一下常见的防爬虫策略和
  • c语言排序之冒泡排序升序的使用详解

    在c语言中实现冒泡排序 简介 冒泡 这个名字的由来是因为越大的元素会经由交换慢慢 浮 到数列的顶端 故名 冒泡排序应该是最常见的排序方法了 c语言初学者一定要学会使用 冒泡排序 BubbleSort 的基本概念是 依次比较相邻的两个数 将小
  • OpenCV中RotatedRect的角度问题与平行判定

    OpenCV中RotatedRect的角度问题与平行判定 在运用OpenCV的过程中 想对两个细窄长方形进行平行判定 但是因为RotatedRct的角度问题 走了很多弯路 RotatedRect OpenCV中 以左上角角点为O点 竖直向下
  • Nginx 部署 Django Python虚拟环境创建 傻瓜教程

    这里不讨论uwsgi 与 nginx之间的关系 但是建议学习 通俗说 Nginx就是可以让你的网页支持更多请求时保证负载均衡 简单的网页用uwsgi部署配合django runserver也可以达到要求 所以从负载能力是 Nginx gt
  • 浮点数在计算机中存储方式

    C语言和C 语言中 对于浮点类型的数据采用单精度类型 float 和双精度类型 double 来存储 float数据占用32bit double数据占用64bit 我们在声明一个变量float f 2 25f的时候 是如何分配内存的呢 如果
  • 什么是软件测试?零基础入门知识要点总结篇,5分钟带你快速了解

    1 什么是软件测试 软件测试 英语 Software Testing 描述一种用来促进鉴定软件的正确性 完整性 安全性和质量的过程 换句话说 软件测试是一种实际输出与预期输出之间的审核或者比较过程 通俗的来讲 软件测试 就是软件测试人员验证
  • 如何使用 Flask 和 GPT-4 API 创建人工智能驱动的 Python Web 应用程序

    欢迎来到 Web 开发的未来 人工智能 AI 正在彻底改变我们创建在线应用程序并与之交互的方式 在这篇博文中 我们将向您介绍使用 Flask 网络框架和 OpenAI 最先进的 GPT 4 API 创建尖端的人工智能 Python 网络应用
  • Vue06/Vue中this.$nextTick( ) 的用法及详细介绍

    一 nextTick 语法 this nextTick 箭头函数体 作用 this nextTick这个方法作用是当数据被修改后使用这个方法 回调函数获取更新后的dom再渲染出来 注意 1 data改变 更新DOM是异步的 2 获取更新后的
  • 基于Camera2和MediaRecorder实现视频录制

    一 概述 视频录制 在一般开发中很少遇到 大部分开发工作都是写写页面 请求接口 展示数据等等 真要遇到 可能采用第三方库实现 一来实现快速 二来可能觉得别人实现的比较好 特别是在开发周期很紧的情况下 一般都不会自己花时间实现 其实最好是使用