微信小程序录制视频功能实现

 背景:

之前项目中有个需求,需要在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)
  }
})

代码实现

  1. 首先创建 camera 上下文 CameraContext 对象ctx
  2. 通过ctx.onCameraFrame注册一个listener用于获取 Camera 实时帧数据
  3. 调用ctx.startRecord开始录像,调用成功后触发listener.start()开始监听获取实时的视频数据
  4. 调用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>

看看功能效果

猜你喜欢

转载自blog.csdn.net/weixin_45032067/article/details/126174209