背景:
之前项目中有个需求,需要在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 就是视频数据 格式是ArrayBuffer
console.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>
看看功能效果