背景:
之前项目中有个需求,需要在pc端的web页面做一套业务的开户流程,其中在开户流程中需要法人上传本人的开户视频(需要支持扫二维码在手机上录制视频上传)。pc端上传的功能比较好实现,但是扫二维码在手机上录制视频并且同步到pc页面的开户流程中就不太好做了。
一开始有想到两种方案:
a: 做一个h5的视频录制页面,然后生成链接的二维码,手机扫二维码进到h5页面录制视频。
- 优点:视频录制的时长可以控制
- 缺点:无法自定义视频拍摄的界面,只能使用相机原生的界面拍摄,可能还要适配各种机型
b: 在小程序的页面里录制视频,而且有现成的组件和api比较好实现
- 优点:视频拍摄的界面可以调整,比如在界面上添加展示文案,不需要再花精力去适配机型
- 缺点:录制时长比较短,目前有5分钟的时长限制
考虑到这个开户视频需要在录制界面上展示一段朗读的文字,h5可能暂时无法实现,刚好公司目前有一个微信小程序已经上线,所以最终选择了在小程序上开发一个录制视频的功能页面。
实现原理
要在微信小程序实现录制视频的功能要用到两个媒体组件camera和video,其中camera组件用来拍摄视频,video组件用来预览拍摄的视频。
camera
camera组件可以调用系统相机进行拍照或者视频录制,其中onCameraFrame 接口可以根据 属性frame-size 返回不同尺寸的原始帧数据,这个原始帧数据就是我们要拍摄的视频数据。
用法
html
<camera device-position="front" flash="off" binderror="error"></camera>js// 创建 camera 上下文 CameraContext 对象
const context = wx.createCameraContext()
// context.onCameraFrame()返回视频图像的监听器
const listener = context.onCameraFrame((frame) => {// frame.data 就是视频数据 格式是ArrayBufferconsole.log(frame.data instanceof ArrayBuffer, frame.width, frame.height)
})
listener.start() // 开始监听帧数据
setTimeou(() => {listener.start() // 停止监听帧数据
}, 10)
video
通过camera组件的api接口可以拿到实时的视频数据了,但是ArrayBuffer格式的数据是无法预览的。那要预览视频可以用video媒体组件,video组件中可以指定视频的src路径,即播放视频的资源地址,但是只支持网络路径、本地临时路径、云文件ID这些选项。
所以要播放录制的视频还需要拿到视频的本地临时路径,那怎么拿到呢,需要结合下面两个api
开始录像CameraContext.startRecord,可以设置视频的时长
结束录像CameraContext.stopRecord,通过成功的回调函数拿到视频的临时路径
官方给出的使用例子
<view class="page-body"><view class="page-body-wrapper"><camera device-position="back" flash="off" binderror="error" style="width: 100%; height: 300px;"></camera><view class="btn-area"><button type="primary" bindtap="startRecord">开始录像</button></view><view class="btn-area"><button type="primary" bindtap="stopRecord">结束录像</button></view><view class="preview-tips">预览</view><video wx:if="{{videoSrc}}" class="video" src="{{videoSrc}}"></video></view>
</view>
Page({onLoad() {this.ctx = wx.createCameraContext()},startRecord() {this.ctx.startRecord({success: (res) => {console.log('startRecord')}})},stopRecord() {this.ctx.stopRecord({success: (res) => {this.setData({src: res.tempThumbPath,videoSrc: res.tempVideoPath})}})},error(e) {console.log(e.detail)}
})
代码实现
- 首先创建 camera 上下文 CameraContext 对象ctx
- 通过ctx.onCameraFrame注册一个listener用于获取 Camera 实时帧数据
- 调用ctx.startRecord开始录像,调用成功后触发listener.start()开始监听获取实时的视频数据
- 调用ctx.stopRecord结束录像,在成功回调中获取视频的临时路径预览视频,并且触发listener.stop()结束视频侦听保存完整的视频数据
<template><view><view class="page-index" :style="{height: windowHeight+'px', position: 'relative'}"><!-- 录制视频区域 --><camera v-if="!videoSrc.length" device-position="front" flash="off" binderror="error":style="{width:cameraWidth+'px',height: windowHeight+'px'}"><p style="color:red; font-size: 20px;">【本人为我司法定代表人,现声明:我司已充分知晓。。。。】</p><view class="time-clycle" v-if="showTimer">{{timeStamp===0?"开始":timeStamp}}</view><!-- 录制视频时间显示 --><view class="video-time"><span>00:<span v-if="videoTime<10">0</span><span>{{videoTime}}</span></span></view></camera><!-- 查看录制视频 --><video :style="{width:cameraWidth+'px',height: windowHeight+'px'}" v-else :src="videoSrc" controls></video></view><button :disabled="disStartBtn" class="video-operate" v-if="showStartBtn" @click="handleStartCamera()">开始录制</button><button class="video-operate" v-if="showStopBtn" @click="handleStopCamera()">结束录制</button><!-- 上传 --><view v-if="videoSrc.length" class="video-result"><u-button :custom-style="firstButtonStyle" style="width: 35%" type="primary" hover-class="active"@click="removeVedio()">重新录制</u-button><u-button :custom-style="secondButtonStyle" style="width: 65%" type="primary"@click="uploadVedio()">确定上传</u-button></view></view></template><script>import {saveVideoFile} from '@/api/upload-video';import {getUrlParams} from '@/common/utils'export default {components: {},data() {return {}},methods: {/*** 上传视频 */uploadVedio() {this.handleUploadFile(this.videoFile);},/*** 保存录制的视频到后台* @param {Object} params*/saveVideoFileInfo(params) {saveVideoFile(params).then(res => {uni.showModal({title: '提示',content: '录制视频已上传成功,请前往PC端开户流程页面查看视频',showCancel: false,confirmText: '知道了',success: function(res) {if (res.confirm) {// 退出小程序wx.exitMiniProgram();}}})})},/*** 上传文件 */async handleUploadFile(file) {const res = await this.$wxApi.uploadVideo({url: '/dragon/file_extend/upload',name: 'file_data',filePath: this.videoSrc,file,formData: {appId: this.appId,fileName: 'corpIdVideo.mp4'}});this.saveVideoFileInfo({appId: this.appId,fileInfo: JSON.stringify(res)})},/*** 获取系统信息 设置相机的大小适应屏幕*/setCameraSize() {//获取设备信息const res = wx.getSystemInfoSync();//获取屏幕的可使用宽高,设置给相机this.cameraWidth = res.windowWidth;this.windowHeight = res.windowHeight - this.iphoneHeight;},/*** 录制视频计时器*/videoTimeInterval() {this.videoTimer =setInterval(() => {++this.videoTime;}, 1000)},/*** 倒计时录像*/startVideoTimer() {this.timeId = setInterval(() => {if (this.timeStamp > this.timeStampEnd) {this.timeStamp--;} else if (this.timeStamp === this.timeStampEnd) {this.showTimer = false;this.clearTimer()// 开始录像this.startShootVideo();} else {this.clearTimer()}}, 1000)},/*** 倒计时重置*/clearTimer() {this.showTimer = false;clearInterval(this.timeId);this.timeId = null;this.timeStamp = this.timeStampStart;},/*** 重新录制*/removeVedio() {this.videoSrc = '';this.showStartBtn = true;this.disStartBtn = false;this.showStopBtn = false;// 倒计时重置clearInterval(this.timeId);this.timeId = null;this.timeStamp = this.timeStampStart;// 录制时间重置clearInterval(this.videoTimer);this.videoTimer = null;this.videoTime = this.timeStampEnd;this.listener.stop();},/*** 开始录像的方法*/startShootVideo() {this.startTime = new Date().getTime();this.showStopBtn = true;this.listener.start();this.videoSrc = ''let that = this;this.ctx.startRecord({success: (res) => {that.videoTimeInterval()},fail(err) {wx.showToast({title: `开始录像失败${err.errMsg},请重新扫码进入`,icon: 'none',duration: 4000});that.removeVedio();}})},/*** 结束录像的方法*/stopShootVideo() {let that = this;clearInterval(that.videoTimer);this.ctx.stopRecord({compressed: false, //压缩视频success: (res) => {that.videoSrc = res.tempVideoPath;},fail(err) {wx.showToast({title: `结束录像失败${err.errMsg},请重新扫码进入`,icon: 'none',duration: 4000});that.removeVedio();}});},/*** 获取麦克风权限*/getSetting() {return new Promise((resolve, reject) => {wx.getSetting({success(res) {if (!res.authSetting['scope.record']) {wx.authorize({scope: 'scope.record',success() {resolve(true)// 用户已经同意小程序使用录音功能,后续调用 wx.startRecord 接口不会弹窗询问},fail(err) {resolve(false)}})} else {resolve(true)}},fail(err) {resolve(false)}})})},/*** 开始录制*/async handleStartCamera() {this.disStartBtn = true;// 判断是否授权了麦克风const authorizePass = await this.getSetting();if (!authorizePass) {wx.showToast({title: `录像失败,麦克风未授权`,icon: 'none',duration: 4000});this.removeVedio();return}this.showTimer = true;this.timeStamp = this.timeStampStart;// 倒计时3s后,调用开始录像方法this.startVideoTimer();},/*** 结束录制*/handleStopCamera() {this.endTime = new Date().getTime();if (this.endTime - this.startTime < this.minRecordTime) {wx.showToast({title: `录制时间太短,请重新录制`,icon: 'none',duration: 3000});// 停止录像this.ctx.stopRecord();this.removeVedio();} else if (this.endTime - this.startTime > this.maxRecordTime) {wx.showToast({title: `录制时间超过30s,请重新录制`,icon: 'none',duration: 3000});this.removeVedio();} else {this.showStartBtn = false;this.showStopBtn = false;this.stopShootVideo();}},/*** 获取url参数* @param {Object} options*/getCodeQuery(options) {let queryAll = decodeURIComponent(options.q);let appId = getUrlParams('appId', queryAll);this.appId = appId;}},onLoad(options) {if (options.q) {this.getCodeQuery(options);}this.setCameraSize();this.ctx = wx.createCameraContext();// 获取 Camera 实时帧数据this.listener = this.ctx.onCameraFrame((frame) => {this.videoFile = frame.data;})}}
</script><style lang="scss" scoped>.page-index {position: relative;.time-clycle {position: absolute;display: block;top: 50%;left: 50%;transform: translateX(-50%);color: #fff;font-size: 80rpx;text-align: center;text-shadow: 5rpx 5rpx 5rpx #333;}.video-time {position: absolute;color: #fff;right: 0;bottom: 0;widows: 100rpx;heigh: 60rpx;background-color: red;}}.video-operate {position: fixed;text-align: center;left: 0;right: 0;padding-bottom: 68rpx;height: 120rpx;line-height: 120rpx;}.video-result {position: fixed;display: flex;text-align: center;left: 0;right: 0;padding-bottom: 68rpx;height: 120rpx;line-height: 120rpx;}
</style>
看看功能效果




















