基于Camera2和MediaRecorder实现视频录制

一、概述

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

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

  //跳转系统的录制视频页面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)}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/143210.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

html 手机录视频,手机怎么录制视频 怎么用手机录视频?

问&#xff1a;手机怎么录制视频 怎么用手机录视频&#xff1f; 答&#xff1a;这里要分两种情况&#xff0c;一、安卓手机录视频&#xff0c;二、苹果手机录视频。其实录制视频是最简单的事了&#xff0c;下载一个好用的视频录制软件就可以搞定了&#xff0c;现在的视频录制软…

Hololens录制视频

说明&#xff1a; 最近老有兄弟伙在问&#xff0c;如何录制视频&#xff0c;讲道理的话这些东西Hololens官网上面都有详细的说明&#xff0c;不需要过于累赘说明&#xff1b;这里还是简要的讲讲如何录制&#xff0c;对Hololens新朋友们一点说明. 这里只对录制视频的步骤做简要说…

腾讯会议录制视频下载

背景解决方法 默认播放页面下全屏播放页面下 小总结 背景 最近在腾讯会议上参加了一场培训&#xff0c;我觉得特别棒&#xff0c;里面干货很多。老师使用了腾讯会议的云录制功能&#xff0c;然后把录制好的视频链接发给了我们。 如此优秀的培训课程&#xff0c;我想自己下载…

html5视频页面的源码,html5 网页录制视频示例源码

【实例简介】该示例必须在 https环境下运行,否则会遇到意想不到的问题 【实例截图】 【核心代码】MeidaRecorder *{box-sizing: border-box; margin: 0; padding: 0; font-family:monospace; } html,body{height: 100%; } html{-webkit-font-smoothing: antialiased; -moz-osx…

前端 视频录制剖析

前端 视频录制剖析 作者&#xff1a; 很菜的小白在分享 时间&#xff1a;2021年12月7日 音视频三部曲 前端 音频录制剖析 前端 视频录制剖析 前端 桌面共享剖析 介绍 身为一个优秀的前端 coder 我们可能会遇到各种各样的需求&#xff0c;昨天我接到了一个新的需求&#xff0…

python实现屏幕视频录制_用Python来做一个屏幕录制工具

一、写在前面 作为一名测试,有时候经常会遇到需要录屏记录自己操作,方便后续开发同学定位。以前都是用screentogif来录屏制作成动态图,偶尔的机会看到python也能实现。那就赶紧学习下。 二、效果展示 三、知识串讲 这次要讲的东西可能比较多了,涉及到pyqt5 gui软件的制作、…

ffmpeg视频录制

录制视频的基本步骤是&#xff1a; 1. 初始化ffmpeg的基本对象&#xff0c;并将这些对象关联起来&#xff0c;然后打开文件并写入文件头。 2. 编码视频&#xff0c;并将编码后数据存写到文件中。 3. 写入文件尾&#xff0c;并清理ffmpeg对象。 首先&#xff0c;需要初始化f…

html 视频录制插件,网页视频录制插件工具(浏览器插件 Recorder)

网页视频录制插件工具(浏览器插件 Recorder)Loom Video Recorder是一款视频录制工具扩展插件&#xff0c;可以一键录制浏览器的单个标签页,&#xff0c;也支持摄像头录制&#xff0c;能够将录制的视频内容下载道本地&#xff0c;有需要的用户不要错过了&#xff0c;欢迎下载使用…

如何录制一个高品质的网页直播视频,录制在线直播视频的快速教程

怎么录制网页上的直播视频&#xff1f;怎么录制网页上正在播放的视频&#xff1f;怎么把别人在线直播的视频快速录制下来&#xff1f; 今天就教大家用超级捕快来快速录制一个高品质的网页直播视频&#xff0c;高清无损&#xff0c;没有水印。不仅能录制在线直播视频&#xff0…

浏览器怎么录制网页视频?3种网页视频录制方法

我们每天都会在浏览器上观看大量的视频&#xff0c;尤其是在爱奇艺、腾讯、哔哩哔哩等网页上。有时候就会观看到一些精彩的视频画面&#xff0c;就想要将这些画面给下载。 那怎么把网页视频录制下来&#xff1f;今天本文就给大家分享3种有效的网页视频录制方法&#xff0c;有需…

k8s进阶5——AppArmor、Seccomp、ImagePolicyWebhook

文章目录 一、AppArmor限制容器对资源访问1.1 实现步骤1.1.1 定义策略1.1.2 加载策略1.1.3 引用策略 2.2 案例 二、Seccomp 限制容器进程系统调用案例一&#xff1a;使用自定义策略案例二&#xff1a;使用容器运行时默认策略 三、动态准入控制Webhook3.1 ImagePolicyWebhook控制…

数组如何转对象

最近做项目的时候需要把拿到的数组转为对象使用 let arr [{id:1,value:正式},{id:2,value:非正式}]//首先定义一个空对象let obj{}//循环遍历数组arr.forEach(item>{//把id作为对象的键 把value作为对象的值值obj[item.id]item.value})console.log(obj);运行结果如下 这样…

对象转为数组

对象转成数组 在开发中&#xff0c;前端处理数据&#xff0c;很多时候都会用到把对象转成数组&#xff0c;话不多说&#xff0c;咱们直接上代码 // 对象转数组&#xff0c;想要转成什么字段根据需求 function fmObjToArr (data) {let arr []for (let key in data) {arr.push…

js数组对象转对象

将数组对象的数据转换为普通对象键值对key:value的形式 let arr [{id:1,employeeNo:110,name:张三},{id:2,employeeNo:111,name:李四},{id:3,employeeNo:112,name:王五},{id:4,employeeNo:113,name:章雨},{id:5,employeeNo:114,name:小红}, ] let newObj {} arr.map(item >…

JS对象转数组

一、JS对象转数组方法如下(不考虑for相关遍历): 1.[...res] 2.Array.from(res) 3.Object.values(res) 4. Array.prototype.slice.call(res) let obj {name:李四, age:22, gender:男} //可枚举 let obj2 {0:李四, 1:22, 2:男, length:3} //可枚举 伪数组 let set new Set([…

3种JavaScript 对象转数组的方法

来源 | https://www.fly63.com 我们在项目开发的时候&#xff0c;有时需要将js对象转换为数组&#xff0c;下面小编给大家具体演示一下怎么转换&#xff0c;主要是介绍一些常用、简洁的转换方法。 比如JavaScript对象如下&#xff1a; let obj {name: 前端,url: https://www.w…

对象数据如何转化成数组

文章目录 前言一、后端给的数据二、如何转换三、最终结果 前言 项目中后端给的数据有时候不可能完全符合前端的数据要求&#xff0c;比如element-uivue项目组合中&#xff0c;下拉框数据是数组的形式。然而后端同事给的确是对象数据&#xff0c;当然你可以和后端协商改成你想要…

json对象转对象数组

对象的两种取值方式 let obj {name: wan}; console.log(obj.name); //wan这是最普通的一种方式&#xff0c;还有一种方式我们用的不太多&#xff0c;就是使用[]包住属性名取值 let obj {name: wan}; console.log(obj[name]); //wan将对象转化为数组 let obj {未完成:5, 已…

将数组转换为对象

本文翻译自&#xff1a;Convert Array to Object What is the best way to convert: 转换的最佳方式是什么&#xff1a; [a,b,c]to: 至&#xff1a; {0: a,1: b,2: c }#1楼 参考&#xff1a;https://stackoom.com/question/Hghl/将数组转换为对象 #2楼 Quick and dirty #2…

js把对象转换成数组

1.把类似数组的对象转换成数组 【1】什么是类似数组的对象 比如&#xff1a; let arrayLike {0:"z",1:"y",2:"k",length:3 }; 本质是有length属性&#xff0c;可以类似数组的获取元素的方式arrayLike[0]、arrayLike[1]去获取元素&#xff0…