转载请标明出处与作者:https://blog.csdn.net/u011133887/article/details/83654724
项目中原本就有录制短视频的功能,使用的是 # qdrzwd /VideoRecorder 这个项目,但是该项目不支持 targetSdkVersion 22以上的版本,而现在各大市场都要求 targetSdkVersion 必须要26以上了,所以急需找到替代的方案。
完整工程请移步 # junerver /**VideoRecorder **,如果对您有帮助,请 star ,欢迎反馈问题,我会尽量维护更新。
更新日志 1.2.0:仿照微信,短按拍照长按拍摄 ——19.06.21 1.1.5:增加进度条,修改依赖为 androidx ——19.05.17 1.1.4:修复录制时切出后无法再次播放的问题 ——18.11.12 1.1.3:修复录制时间过短导致崩溃的问题 ——18.11.08 1.1.2:修复在华为设备不兼容的问题 ——18.11.03 1.1:修复录制视频无法在 ios 设备播放的问题 ——18.11.02
分析 解决方法大致上有如下四种:
使用 FFmpeg
使用系统摄像头
使用 MediaRecorder
使用阿里云、腾讯云、七牛云等短视频服务
其中方案一 可以参考:利用FFmpeg玩转Android视频录制与压缩(一) 利用FFmpeg玩转Android视频录制与压缩(二) 利用FFmpeg玩转Android视频录制与压缩(三) 编译Android下可执行命令的FFmpeg 编译Android下可用的全平台FFmpeg(包含libx264与libfdk-aac) Android下玩JNI的新老三种姿势
Github 上项目地址为:# mabeijianxi /small-video-record
该项目存在一些问题,我在使用小米6测试其 Demo 时,既不能录像也不能选取本地视频进行压缩。另外引入 FFmpeg 对于本需求而言,时间成本、学习成本、APK 最终体积增量都是不划算的选择。
方案二 大概是最简单与稳定可靠(机型适配方面)的了:
1 2 3 4 5 6 var intent = Intent(MediaStore.ACTION_VIDEO_CAPTURE)intent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, 10 ) intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1 ) startActivityForResult(intent, VIDEO_WITH_CAMERA)
但是存在着一个致命的缺点,录制完的视频体积非常大,对画质配置只有 1、0 这两种选择。其中 1 最终成片体积太大,0 画质太渣,基本不可用。
方案四 就不多提了,我们的项目并不是专门的短视频 APP,使用这些付费 SDK 完全是杀鸡用牛刀。
最终决定通过方案三 ,使用 MediaRecorder 来完成了该功能,该方案具有以下优势:
无需引入任何第三方库,不会增加 APK 体积
系统自带功能,几乎不存在机型设配问题
最终成片参数可控(分辨率、帧数、编码比特率)
致谢:本文代码大量参考了胖子爱你520
所写 Android使用MediaRecorder和Camera实现视频录制及播放功能整理 一文,并对其代码进行了功能上的优化与 UI 上的美化。
成品预览:
测试手机为 小米6,最终 10s 短视频成片体积在3M左右,处于可接受范围。
功能实现
警告⚠️:以下内容还有大量 Kotlin 代码,可能会引起不适。
此处我们只谈及一些关键的代码片段,完整工程请移步 # junerver /VideoRecorder ,如果对您有帮助,请 star ,欢迎反馈问题,我会尽量维护更新。
录像是如何实现的? 1.启动录制⏺
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 mRecorder = MediaRecorder().apply { reset() setCamera(mCamera) setAudioSource(MediaRecorder.AudioSource.CAMCORDER) setVideoSource(MediaRecorder.VideoSource.CAMERA) setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) setAudioEncoder(MediaRecorder.AudioEncoder.AAC) setVideoEncoder(MediaRecorder.VideoEncoder.H264) setVideoSize(640 , 480 ) setVideoFrameRate(30 ) setVideoEncodingBitRate(3 * 1024 * 1024 ) setOrientationHint(90 ) setMaxDuration(30 * 1000 ) } path = Environment.getExternalStorageDirectory().path + File.separator + "VideoRecorder" if (path != null ) { var dir = File(path) if (!dir.exists()) { dir.mkdir() } dirPath = dir.absolutePath path = dir.absolutePath + "/" + getDate() + ".mp4" Log.d(TAG, "文件路径: $path " ) mRecorder.apply { setOutputFile(path) prepare() start() } startTime = System.currentTimeMillis() }
2.结束录制⏺
1 2 3 4 5 mRecorder.apply { stop() reset() release() }
以上就是录制视频的最核心的代码了,可见,首先我们需要为 MediaRecorder 分配一个摄像头,然后配置相关属性,在最后结束时调用 stop()
方法即可。
重要 :在分配摄像头资源(MediaRecorder.setCamera(mCamera)
)之前,必须先解锁摄像头(mCamera.unlock()
),否则会提示 MediaRecorder: start failed: -19
3. 拍摄完成成片预览⏯ 此处没什么可说的,直接调用系统提供的 MediaPlayer 即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 mMediaPlayer.reset() var uri = Uri.parse(path)mMediaPlayer = MediaPlayer.create(VideoRecordActivity@ this , uri) mMediaPlayer.apply { setAudioStreamType(AudioManager.STREAM_MUSIC) setDisplay(mSurfaceHolder) setOnCompletionListener { mBtnPlay.visibility = View.VISIBLE } } try { mMediaPlayer.prepare() } catch (e: Exception) { e.printStackTrace() } mMediaPlayer.start()
优化体验 1. 录制前的预览 如果你看过上文我们所提到的那篇文章,会发现按照他的代码实现的话,在开始录制视频之前是没有画面的(关键代码 mRecorder.setPreviewDisplay(mSurfaceHolder.getSurface());
),只有用户点击了录制按钮,开始录制之后才会有摄像头的预览画面,这无疑是不合理的。
而 MediaRecorder 在录制视频的过程中该操作并不是必要操作,那么我们完全可以使用摄像头的预览画面来填充到我们的 SurfacerView 中来,这样整个体验就非常流畅了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 var holder = mSurfaceview.holderholder.addCallback(object : SurfaceHolder.Callback { override fun surfaceChanged (holder: SurfaceHolder ?, format: Int , width: Int , height: Int ) { mSurfaceHolder = holder!! mCamera.apply { startPreview() cancelAutoFocus() unlock() } cameraReleaseEnable = true } override fun surfaceDestroyed (holder: SurfaceHolder ?) { handler.removeCallbacks(runnable) } override fun surfaceCreated (holder: SurfaceHolder ?) { try { mSurfaceHolder = holder!! mCamera = Camera.open (Camera.CameraInfo.CAMERA_FACING_BACK) mCamera.apply { setDisplayOrientation(90 ) setPreviewDisplay(holder) val params = mCamera.parameters val size = getPreviewSize() params.apply { setPictureSize(size.first, size.second) jpegQuality = 100 pictureFormat = PixelFormat.JPEG focusMode = Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE } parameters = params } } catch (e: RuntimeException) { finish() } } })
2. 录制时的进度条 拍摄过程中进度条显示是一个非常优秀友好的设计,要实现这一点也非常简单,我们只需要在拍摄开始时加入一个 Handler 来做计时,在结束拍摄时 remove 掉即可! 代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 var handler = Handler() var runnable = object : Runnable { override fun run () { timer++ if (timer < 100 ) { mProgress.progress = timer handler.postDelayed(this , maxSec * 10L ) } else { Log.d("到最大拍摄时间" ,"" ) stopRecord() System.currentTimeMillis() } } }
在开始录制时调用 handler.postDelayed(runnable, maxSec * 10L)
在结束录制时调用 handler.removeCallbacks(runnable)
可能出现的异常
java.lang.RuntimeException: stop failed. at android.media.MediaRecorder.stop(Native Method)
该异常会在部分机型出现,当录制完毕后,执行 stop()
函数时出现异常。 解决思路:
检查 MediaRecorder 的视频输出尺寸是否是系统所支持的尺寸
修改 Camera 的预览尺寸与 MediaRecorder 的视频输出尺寸一致
修改 MPEG_4_SP 为 H264mRecorder?.setVideoEncoder(MediaRecorder.VideoEncoder.MPEG_4_SP)
-> mRecorder?.setVideoEncoder(MediaRecorder.VideoEncoder.H264)
stop failed 的另一种情况 读者反馈当录制视频时间过短时或出现崩溃的情况,就此我们可以参考 stop()
方法的备注:
1 2 3 4 5 6 7 8 9 10 11 12 13 /** * Stops recording. Call this after start(). Once recording is stopped, * you will have to configure it again as if it has just been constructed. * Note that a RuntimeException is intentionally thrown to the * application, if no valid audio/video data has been received when stop() * is called. This happens if stop() is called immediately after * start(). The failure lets the application take action accordingly to * clean up the output file (delete the output file, for instance), since * the output file is not properly constructed when this happens. * * @throws IllegalStateException if it is called before start() */ public native void stop() throws IllegalStateException;
如果在调用stop()
时未收到有效的音频/视频数据,则会抛出RuntimeException
。如果在start()
之后立即调用stop()
,则会发生这种情况。
原因就是在我们结束录制时,整个过程总时长太短,还没有接收到有效地数据。对于这种情况,我们有两种解决方法:
人为延时,在 stop()
方法调用前确定总时长,当总时长小于 1s 时,Thread.sleep(xxx)
try{}catch{}
捕获改异常,并提示用户录制时间太短,然后允许用户重新录制;
修复后的代码:
1 2 3 4 5 6 7 8 9 10 11 if (stopTime-startTime<1100 ) { Thread.sleep(1100 +startTime-stopTime) } mRecorder.stop() mRecorder.reset() mRecorder.release() recorderReleaseEnable = false mCamera.lock() mCamera.stopPreview() mCamera.release()
补充实现 - 短按拍照,长按拍摄 实现仿微信的短按拍照长按拍摄其实非常简单,思路来自上一节中的 stop failed 。既然我们知道录制时间过短会抛出 RuntimeException ,那么我们只需要 try{}catch{}
捕获改异常,在捕获到异常之后使用 Camera 提供的拍摄接口,惊醒拍照即可!
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 try { mRecorder.stop() mRecorder.reset() mRecorder.release() recorderReleaseEnable = false mCamera.lock() mCamera.stopPreview() mCamera.release() cameraReleaseEnable = false mBtnPlay.visibility = View.VISIBLE MediaUtils.getImageForVideo(path) { Log.d(TAG, "获取到了第一帧" ) imgPath = it.absolutePath mLlRecordOp.visibility = View.VISIBLE } } catch (e: java.lang.RuntimeException) { mType = TYPE_IMAGE Log.e("拍摄时间过短" , e.message) mRecorder.reset() mRecorder.release() recorderReleaseEnable = false mCamera.takePicture(null , null , Camera.PictureCallback { data , data ?.let { saveImage(it) { imagepath -> Log.d(TAG, "转为拍照,获取到图片数据 $imagepath " ) imgPath = imagepath mCamera.lock() mCamera.stopPreview() mCamera.release() cameraReleaseEnable = false runOnUiThread { mBtnPlay.visibility = View.INVISIBLE mLlRecordOp.visibility = View.VISIBLE } } } }) }
案例下载地址:Android 仿微信录制短视频