一、概述

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

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

  //跳转系统的录制视频页面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 = mRecordSurfaceif (recordSurface == null) {recordSurface = MediaCodec.createPersistentInputSurface()mRecordSurface = recordSurface}mediaRecorder.setInputSurface(recordSurface)mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC)//数据源来之surfacemediaRecorder.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 ?: cacheDirval 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 = 52private val requestActivityCode = 10/*** mCountDownMsg 录制进度倒计时msg,一秒钟发送一次* mStartRecordMsg 开始录制* mCameraOpenFailMsg 相机打开失败* mCameraPreviewFailMsg 相机预览失败* mRecordErrorMsg 录制出现错误* 在 mCountDownHandler(主线程的Handler)中处理*/private val mCountDownMsg = 19private val mStartRecordMsg = 20private val mCameraOpenFailMsg = 21private val mCameraPreviewFailMsg = 22private val mRecordErrorMsg = 23private lateinit var mSurfaceView :SurfaceViewprivate lateinit var mRecordProgressBar: ProgressBarprivate lateinit var mRecordStateIv: ImageViewprivate lateinit var mFlashlightIv: ImageViewprivate lateinit var mSwitchCameraIv: ImageView/*** 录制视频文件路径*/@Volatileprivate var mFilePath: String? = null/*** 录制的视频文件名*/@Volatileprivate var mFileName: String? = null/*** 预览画面尺寸,和视频录制尺寸*/@Volatileprivate var mRecordSize: Size? = null/*** 相机方向*/private var mCameraOrientation: Int = 0/*** 录制视频的方向,随着手机方向的改变而改变*/@Volatileprivate var mRecordVideoOrientation: Int = 0/*** 默认打开后置相机 LENS_FACING_BACK* 可以切换为前置相机 LENS_FACING_FRONT*/private var mFensFacing = CameraCharacteristics.LENS_FACING_BACK/*** 预览Surface*/@Volatileprivate var mPreviewSurface: Surface? = null/*** 录制Surface*/@Volatileprivate var mRecordSurface: Surface? = null@Volatileprivate var mCameraDevice: CameraDevice? = null@Volatileprivate var mCameraCaptureSession: CameraCaptureSession? = null@Volatileprivate var mCaptureRequest: CaptureRequest.Builder? = nullprivate var mOrientationEventListener: OrientationEventListener? = null@Volatileprivate var mMediaRecorder: MediaRecorder? = null/*** 是否是录制中的状态* true:录制中*/@Volatileprivate 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 = 0override 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 = falsemFlashlightState = falsemFlashlightIv.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 ?: returnval cameraCaptureSession = mCameraCaptureSession ?: returnif (mFlashlightState) {//闪光灯开启,点击关闭captureRequest[CaptureRequest.FLASH_MODE] = CaptureRequest.FLASH_MODE_OFFmFlashlightIv.setImageResource(R.drawable.flashlight_off)} else {//闪关灯关闭,点击开启captureRequest[CaptureRequest.FLASH_MODE] = CaptureRequest.FLASH_MODE_TORCHmFlashlightIv.setImageResource(R.drawable.flashlight_on)}mFlashlightState = !mFlashlightStatecameraCaptureSession.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_FRONTclose()openCamera()} else if (mFensFacing == CameraCharacteristics.LENS_FACING_FRONT) {//当前打开的是前置摄像头,切换到后置摄像头mFensFacing = CameraCharacteristics.LENS_FACING_BACKclose()openCamera()}}}}private fun startRecord() {mRecordThreadHandler.sendEmptyMessage(mStartRecordMsg)mRecordingState = truemRecordStateIv.setImageResource(R.drawable.record_start_state_bg)mCurrentRecordDuration = 0//开始录制倒计时mUiHandler.sendEmptyMessageDelayed(mCountDownMsg, 1000)}private fun stopRecord() {//视图变为 停止录制状态mRecordingState = falsemRecordStateIv.setImageResource(R.drawable.record_stop_state_bg)mRecordProgressBar.progress = 0mUiHandler.removeMessages(mCountDownMsg)val mediaRecorder = mMediaRecorder ?: returntry {mediaRecorder.stop()} catch (t: Throwable) {t.printStackTrace()}}/*** 检查相机和录音与权限,并打开相机*/private fun checkPermissionAndOpenCamera(){//录制音频权限okval audioOk = ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED//相机权限okval cameraOk = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)== PackageManager.PERMISSION_GRANTEDif (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 ?: returnval filePath = mFilePath ?: return//不能再录制mRecordable = falseval 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会一直持有contextval cameraManager = applicationContext.getSystemService(CAMERA_SERVICE) as? CameraManagerval cameraIdList = cameraManager?.cameraIdListif (cameraManager == null || cameraIdList == null || cameraIdList.isEmpty()) {Toast.makeText(this, "无法使用设备相机", Toast.LENGTH_SHORT).show()finish()return}for (id in cameraIdList) {var cameraCharacteristics: CameraCharacteristics? = nulltry {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) ?: 0val 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 = recordSizeresizeSurfaceSize(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 = cameraval recordSize = mRecordSize ?: return//预览surfaceval previewSurface = mSurfaceView.holder.surfacemPreviewSurface = previewSurfacesetupMediaRecorder(recordSize.width, recordSize.height, false)val recordSurface = mRecordSurfacemRecordSurface = recordSurfaceval 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 = sessionval camera = mCameraDevice ?: returnval previewSurface = mPreviewSurface ?: returnval recordSurface = mRecordSurface ?: returnval captureRequest = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)mCaptureRequest = captureRequestcaptureRequest.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) returnwhen (msg.what) {mStartRecordMsg -> {//开始录制if (!mRecordingState) {//不是开始录制状态,returnreturn}val recordSize = mRecordSize ?: returntry {//重新配置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 += 1val progress = (mCurrentRecordDuration * 1f / mMaxRecordDuration * 100 + 0.5f).toInt()mRecordProgressBar.progress = progressif (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 = mMediaRecorderif (mediaRecorder == null) {mediaRecorder = MediaRecorder()mMediaRecorder = mediaRecorder} else {mediaRecorder.reset()}var recordSurface = mRecordSurfaceif (recordSurface == null) {recordSurface = MediaCodec.createPersistentInputSurface()mRecordSurface = recordSurface}mediaRecorder.setInputSurface(recordSurface)mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC)//数据源来之surfacemediaRecorder.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 ?: cacheDirval fileName = "${System.currentTimeMillis()}.mp4"mFileName = fileNamemFilePath = 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 = mPreviewSurfaceif (previewSurface != null) {mCaptureRequest?.removeTarget(previewSurface)}val recordSurface = mRecordSurfaceif (recordSurface != null) {mCaptureRequest?.removeTarget(recordSurface)}mCameraCaptureSession?.close()mCameraDevice?.close()mCaptureRequest = nullmCameraCaptureSession = nullmCameraDevice = null}/*** 删除录制的文件*/private fun deleteRecordFile() {val filePath = mFilePath ?: returntry {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*720val compareWidth = 1280val compareHeight = 720var resultSize = sizes[0]var minDiffW = Int.MAX_VALUEvar minDiffH = Int.MAX_VALUEfor (size in sizes) {if (size.width == compareWidth && size.height == compareHeight) {resultSize = sizebreak}//找到最接近 1280*720的sizeval diffW = abs(size.width - compareWidth)val diffH = abs(size.height - compareHeight)if (diffW < minDiffW && diffH < minDiffH) {minDiffW = diffWminDiffH = diffHresultSize = size}}return resultSize}/*** 根据视频宽高,修改surfaceView的宽高,来适应预览尺寸** @param width  预览宽度* @param height 预览高度*/private fun resizeSurfaceSize(height: Int, width: Int) {val displayW: Int = mSurfaceView.widthval displayH: Int = mSurfaceView.heightif (displayW == 0 || displayH == 0) returnvar ratioW = 1fvar ratioH = 1fif (width != displayW) {ratioW = width * 1f / displayW}if (height != displayH) {ratioH = height * 1f / displayH}var finalH = displayHvar finalW = displayWif (ratioW >= ratioH) {finalH = (height / ratioW).toInt()} else {finalW = (width / ratioH).toInt()}val layoutParams = mSurfaceView.layoutParamsif (layoutParams.width == finalW && layoutParams.height == finalH) {return}layoutParams.width = finalWlayoutParams.height = finalHmSurfaceView.layoutParams = layoutParams}/*** 监听手机方向改变,计算录制时的视频方向。横屏录制时,视频横屏。竖屏录制时,视频竖屏*/private fun initOrientationEventListener() {val orientationEventListener = object : OrientationEventListener(this) {override fun onOrientationChanged(orientation: Int) {if (orientation == ORIENTATION_UNKNOWN) returnval rotation = (orientation + 45) / 90 * 90if (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)}
}

基于Camera2和MediaRecorder实现视频录制相关推荐

  1. 《android多媒体api》之MediaRecorder音视频录制api

    <android多媒体api>系列是整合梳理android开发中经常用到的媒体相关api:多媒体开发主要内容有音频.视频录制播放.摄像头操作.录制操作.流媒体.直播.推流.拉流等方面:最近 ...

  2. Android 基于MediaCodec+MediaMuxer实现音视频录制合成

    AudioVideoCodec 一款视频录像机,支持AudioRecord录音.MediaCodec输出AAC.MediaMuxer合成音频视频并输出mp4,支持自动对焦.屏幕亮度调节.录制视频时长监 ...

  3. 基于AForge的C#摄像头视频录制

    1. 概述 最近搞华为的CDN(-_-||-_-||-_-||),写东西的时间比较少了.最近由于兴趣学习了下在C#上使用AForge录制摄像头视频并压缩编码.总体上来说这个第三方.net视觉开发库还是 ...

  4. SDVideoCamera:仿抖音(视频录制、视频剪辑、视频合成)

    闲话不多讲,先上项目Github传送门. SDVideoCamera传送门 前言 又是好久没有更新博客了,哈哈哈,由于近来从公司离职,再加上近来要结婚的缘故,所以有大量充足的时间来整理以前写的一个仿写 ...

  5. Android项目小结——可对焦的视频录制(MediaRecorder与TextureView实现)

    一直在做安卓的项目,想着找个时间总结一下,可能太懒了,一直没总结. 代码(尤其是对焦框显示)参考了许多Blog和Github,修修补补改改挺多地方,记录一下,侵删私信或注明出处. 录制 主要的类 Ca ...

  6. 模仿微视视频录制、支持按下录制抬起暂停以及断点进度条(基于javacv)

    apk下载视频录制apk下载, 项目源码地址为https://github.com/qdrzwd/VideoRecorder 补充:感谢雷军辉提供的文档 wiki: http://www.elesos ...

  7. Android 中使用MediaRecorder进行录像详解(视频录制)

    简单的视频录制功能. package com.video; import java.io.IOException; import android.app.Activity; import androi ...

  8. Android使用MediaRecorder和Camera实现视频录制及播放功能整理

    转载请注明出处:http://blog.csdn.net/woshizisezise/article/details/51878566 这两天产品经理向我丢来一个新需求,需要在项目里添加一个视频录制的 ...

  9. 基于kinect + EmguCV 的监控小应用(视频录制保存)

    kinectMonitor 源码位置 家庭监控小应用–https://gitee.com/ellecommander/kinecMonitor 介绍 基于kinect的家庭监控系统,主要是为了监控我爸 ...

最新文章

  1. .net 获取字符串中的第一个逗号的位置_SQLZOO中做错过的题
  2. volatile关键字的作用
  3. ejb能调用另一个ejb吗_异步EJB只是一个Gi头吗?
  4. 每日干货丨C语言数组知识点总结
  5. linux svn 拉取代码_svn快速入门指南
  6. 返回一个不确定的对象_我有一个Android必备知识点,你确定不了解一下?
  7. 关于蛙跳算法的计算机文献,文化蛙跳算法性能分析研究.PDF
  8. 【转】随机函数 rand() srand() 以及seed的原理
  9. sprig aop事务配置
  10. 电力巡检系统登录页面
  11. 《岛上书店》一本被高估的书
  12. 163邮箱怎么申请?手机号怎么申请注册邮箱?
  13. VM虚拟机设置桥接模式
  14. 绕过校园网Web认证
  15. 验证身份证号的存储过程 oracle,Oracle 生成序列号存储过程
  16. android番茄时钟代码,一种实现极简番茄时钟的思路
  17. C# CRC16 CCITT XModem
  18. 让企业报表化繁为简,Smartbi实现报表统一管理
  19. 会计基础-会计科目+会计账户+复式记账+会计分录+会计凭证
  20. 龙之气息服务器维护,【龙之气息:从入坑到肝硬化】

热门文章

  1. C++ STL 基础及应用(2) 模板与操作符重载
  2. Open BMC开发系列(六)增加对GPIO的支持
  3. 三款软件让你学会怎么识别手写文字内容
  4. php move_uploaded_file liunx,PHP move_uploaded_file() 函数(将上传的文件移动到新位置)
  5. 当前4k 32寸的适合办公的显示器有哪些,价位如何
  6. Room DB Error: AppDatabase_Impl does not exist
  7. 英语作文 对软件工程师这一职业的理解
  8. 西安电子科技大学考研833计算机专业基础综合初试备考经验
  9. 不懂电脑出现的误解,你中招了没?
  10. 获取java项目路径_Java获取项目路径