Android 仿微信录制短视频(不使用 FFmpeg)

转载请标明出处与作者: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

分析

解决方法大致上有如下四种:

  1. 使用 FFmpeg
  2. 使用系统摄像头
  3. 使用 MediaRecorder
  4. 使用阿里云、腾讯云、七牛云等短视频服务

其中方案一可以参考:
利用FFmpeg玩转Android视频录制与压缩(一)
利用FFmpeg玩转Android视频录制与压缩(二)
利用FFmpeg玩转Android视频录制与压缩(三)
编译Android下可执行命令的FFmpeg
编译Android下可用的全平台FFmpeg(包含libx264与libfdk-aac)
Android下玩JNI的新老三种姿势

Github 上项目地址为:# mabeijianxi/small-video-record

该项目存在一些问题,我在使用小米6测试其 Demo 时,既不能录像也不能选取本地视频进行压缩。另外引入 FFmpeg 对于本需求而言,时间成本、学习成本、APK 最终体积增量都是不划算的选择。

方案二大概是最简单与稳定可靠(机型适配方面)的了:

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 来完成了该功能,该方案具有以下优势:

  1. 无需引入任何第三方库,不会增加 APK 体积
  2. 系统自带功能,几乎不存在机型设配问题
  3. 最终成片参数可控(分辨率、帧数、编码比特率)

致谢:本文代码大量参考了[胖子爱你520
](https://blog.csdn.net/woshizisezise) 所写 Android使用MediaRecorder和Camera实现视频录制及播放功能整理 一文,并对其代码进行了功能上的优化与 UI 上的美化。

成品预览:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U2n9WHBv-1655782213873)(https://github.com/junerver/VideoRecorder/blob/master/art/2.jpg?raw=true)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qvh1ZR69-1655782213874)(https://github.com/junerver/VideoRecorder/blob/master/art/3.png?raw=true)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0JGzf7uY-1655782213874)(https://github.com/junerver/VideoRecorder/blob/master/art/4.jpg?raw=true)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ncFdk7kE-1655782213875)(https://github.com/junerver/VideoRecorder/blob/master/art/1.jpg?raw=true)]

测试手机为 小米6,最终 10s 短视频成片体积在3M左右,处于可接受范围。

功能实现

警告⚠️:以下内容还有大量 Kotlin 代码,可能会引起不适。

此处我们只谈及一些关键的代码片段,完整工程请移步 # junerver/VideoRecorder
,如果对您有帮助,请 star ,欢迎反馈问题,我会尽量维护更新。

录像是如何实现的?

1.启动录制⏺

mRecorder = MediaRecorder().apply {
    reset()
    setCamera(mCamera)
    // 设置音频源与视频源 这两项需要放在setOutputFormat之前
    setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
    setVideoSource(MediaRecorder.VideoSource.CAMERA)
    //设置输出格式
    setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
    //这两项需要放在setOutputFormat之后 IOS必须使用ACC
    setAudioEncoder(MediaRecorder.AudioEncoder.AAC)  //音频编码格式
    //使用MPEG_4_SP格式在华为P20 pro上停止录制时会出现
    //MediaRecorder: stop failed: -1007
    //java.lang.RuntimeException: stop failed.
    // at android.media.MediaRecorder.stop(Native Method)
    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.结束录制⏺

mRecorder.apply {
    stop()  //结束录制
    reset()  //重置
    release() //释放资源
}

以上就是录制视频的最核心的代码了,可见,首先我们需要为 MediaRecorder 分配一个摄像头,然后配置相关属性,在最后结束时调用 stop() 方法即可。

重要:在分配摄像头资源(MediaRecorder.setCamera(mCamera))之前,必须先解锁摄像头(mCamera.unlock()),否则会提示 MediaRecorder: start failed: -19

3. 拍摄完成成片预览⏯
此处没什么可说的,直接调用系统提供的 MediaPlayer 即可

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 中来,这样整个体验就非常流畅了。

var holder = mSurfaceview.holder
holder.addCallback(object : SurfaceHolder.Callback {
    override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) {
        mSurfaceHolder = holder!!
        mCamera.apply {
            startPreview()
            cancelAutoFocus()
            // 关键代码 该操作必须在开启预览之后进行(最后调用),
            // 否则会黑屏,并提示该操作的下一步出错
            // 只有执行该步骤后才可以使用MediaRecorder进行录制
            // 否则会报 MediaRecorder(13280): start failed: -19
            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)//旋转90度
                setPreviewDisplay(holder)
                val params = mCamera.parameters
                //注意此处需要根据摄像头获取最优像素,//如果不设置会按照系统默认配置最低160x120分辨率
                val size = getPreviewSize()
                params.apply {
                    setPictureSize(size.first, size.second)
                    jpegQuality = 100
                    pictureFormat = PixelFormat.JPEG
                    focusMode = Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE//1连续对焦
                }
                parameters = params
            }
        } catch (e: RuntimeException) {
            //Camera.open() 在摄像头服务无法连接时可能会抛出 RuntimeException
            finish()
        }
    }
})

2. 录制时的进度条
拍摄过程中进度条显示是一个非常优秀友好的设计,要实现这一点也非常简单,我们只需要在拍摄开始时加入一个 Handler 来做计时,在结束拍摄时 remove 掉即可!
代码如下:

 //用于记录视频录制时长
    var handler = Handler()
    var runnable = object : Runnable {
        override fun run() {
            timer++
            if (timer < 100) {
                // 之所以这里是100 是为了方便使用进度条
                mProgress.progress = timer
                //总时长的毫秒数 / 100 即每次间隔延时的毫秒数  == 录制总时长 * 10
                handler.postDelayed(this, maxSec * 10L)
            } else {
                //停止录制 保存录制的流、显示供操作的ui
                Log.d("到最大拍摄时间","")
                stopRecord()
                System.currentTimeMillis()
            }
        }

    }

在开始录制时调用 handler.postDelayed(runnable, maxSec * 10L)
在结束录制时调用 handler.removeCallbacks(runnable)

可能出现的异常

MediaRecorder: stop failed: -1007

java.lang.RuntimeException: stop failed.
at android.media.MediaRecorder.stop(Native Method)

该异常会在部分机型出现,当录制完毕后,执行 stop() 函数时出现异常。
解决思路:

  1. 检查 MediaRecorder 的视频输出尺寸是否是系统所支持的尺寸
  2. 修改 Camera 的预览尺寸与 MediaRecorder 的视频输出尺寸一致
  3. 修改 MPEG_4_SP 为 H264
    mRecorder?.setVideoEncoder(MediaRecorder.VideoEncoder.MPEG_4_SP) -> mRecorder?.setVideoEncoder(MediaRecorder.VideoEncoder.H264)

stop failed 的另一种情况

读者反馈当录制视频时间过短时或出现崩溃的情况,就此我们可以参考 stop() 方法的备注:

/**
     * 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(),则会发生这种情况。

原因就是在我们结束录制时,整个过程总时长太短,还没有接收到有效地数据。对于这种情况,我们有两种解决方法:

  1. 人为延时,在 stop() 方法调用前确定总时长,当总时长小于 1s 时,Thread.sleep(xxx)
  2. try{}catch{} 捕获改异常,并提示用户录制时间太短,然后允许用户重新录制;

修复后的代码:

//方法1:分别记录保存起始时间与结束时间,如果时间小于1.1s 则让线程sleep
  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 提供的拍摄接口,惊醒拍照即可!

代码如下:

//方法2 : 捕捉异常改为拍照
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) {
    //当catch到RE时,说明是录制时间过短,此时将由录制改变为拍摄
    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 仿微信录制短视频

评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值