软件杯H5端'视频尬舞机'开发记录

背景

自己还没接触过这方面的开发,所以准备写一篇博客长期跟踪这个项目了

技术栈: 原生JavascriptWebRTC

开始

基本结构

这里使用Parcel进行搭建

1
2
3
4
5
6
7
8
src
--css
--index.css
--js
--index.js
--img
index.html
package.json

index.html中引入JsCss然后执行parcel index.html即可运行项目

WebRTC

基于浏览器的安全策略,通过WebRTC(具体为getUserMedia)调用摄像头和麦克风获取音视频数据,只能是在HTTPS下的网页,或者是本地localhost下才能调用,需要先校验

1
2
3
4
5
6
7
8
9
10
11
12
let isAllowed = true

window.onload = validate()

function validate(){
var isSecureOrigin = location.protocol === 'https:' ||
location.hostname === 'localhost';
if (!isSecureOrigin) {
alert('getUserMedia() must be run from a secure origin: HTTPS or localhost.');
isAllowed = false; // 判断是否满足条件
}
}

然后调用相应的接口打开摄像头,进行链式调用

1
2
3
4
5
if(isAllowed){
navigator.mediaDevices.getUserMedia(constraints)
.then(handleSuccess)
.catch(handleError);
}

constraints为打开的video窗口配置,maxmin为最大最小分辨率,ideal为你期望的最佳分辨率,这里目前先简单的做一下适配

1
2
3
4
5
6
7
const constraints = {
audio: false, // 关闭声音
video: {
width: { min: 350, ideal: window.screen.width + 200, max: 800 },
height: { min: 500, ideal: window.screen.height - 200, max: 1500 }
}
};

调用失败的函数

1
2
3
4
function handleError(error) {
console.log('navigator.getUserMedia error: ', error);
alert('navigator.getUserMedia error: ', error);
}

调用成功的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function handleSuccess(stream) {
window.stream = stream;

//显示在页面上
var recordedVideo = document.querySelector('video#recorded');
recordedVideo.srcObject = stream;
recordedVideo.onloadedmetadata = function(e) {
console.log("Label: " + stream.label);
//视频图像
console.log("VideoTracks" , stream.getVideoTracks());
};

videoPause(recordedVideo) // 注册暂停播放事件
imgFall() // 显示下落的图片
}

再修改一下Html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Index</title>
<link rel="stylesheet" href="./src/css/index.css">
</head>
<body>
<video id='recorded' class='recorded' autoplay>
您的环境不支持播放视频。
</video>
</body>
<script src='./src/js/index.js'></script>
</html>

做到这,不用管之前下面那两个函数,你已经能看到窗口里的你了hhhhh

视频暂停

video是绝对定位并且浮在最下面的,所以一些点击事件操作通过mask处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const mask = document.getElementById('mask');

function videoPause(recordedVideo){

mask.addEventListener('click', () => {
if(!recordedVideo.paused){
isPause = true
recordedVideo.pause()
}else{
isPause = false
recordedVideo.play()
}
})
}

图片下落

这里目前只有一个图片来源,在图片onload回调里进行图片处理,这里封装了一个定时器来循环产生下落的图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let track = []

function imgFall(){
let img = new Image()
img.onload = () => {

track[0] = outInterval(() => {
let item = new Image()
item.src = img.src
item.style.left = parseInt((window.screen.width - 100) * Math.random()) + 'px'

mask.appendChild(item)

}, 0, 5000)
}
img.src = require('../img/people.png')
}

定时器封装

外部延时器

接受3个参数:

  • callback: 回调函数
  • begin: 开始时间
  • delay: 定时间隔

用一个延时器来延时内部的定时器(innerInterval),然后返回内部定时器

1
2
3
4
5
6
7
8
9
10
11
12
13
function outTimeout(callback, begin, delay){
let timeout

setTimeout(() => {
callback()

timeout = innerInterval(callback, delay)

}, begin)

return timeout;

}

内部定时器

接受2个参数:

  • callback: 回调函数

  • delay: 定时间隔

内部进行递归的setTimeout来实现定时器功能,避免了setInterval的间隔重叠问题,保证每个callback经过相同的时间后执行

1
2
3
4
5
6
7
8
9
10
11
12
function innerInterval(callback, delay){

const timeout = setTimeout(function(){

if(!isPause) callback() // 播放状态下才执行回调函数(图片下落)

setTimeout(innerInterval(callback, delay), delay)

}, delay)

return timeout;
}

下落处理

此处为callback函数中的内容,每次执行的时候创建新的img然后随机left偏移,之后append到父元素中

1
2
3
4
5
let item = new Image()
item.src = img.src
item.style.left = parseInt((window.screen.width - 100) * Math.random()) + 'px'

mask.appendChild(item)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.mask img{
position: absolute;
width: 100px;
height: 120px;
animation: fall 5s linear forwards;
}

@keyframes fall {
from {
top: -120px; // 减去图片高度
} to {
top: 100vh; // 落到屏幕下方
}

90% {
opacity: 1;
}
100% {
opacity: 0; // 变透明
}
}

下落后要定时清除图片元素避免过多,设置清除为第一次下落后执行,间隔为每一次下落完成的时间

1
2
3
4
5
6
const deleteImg = outTimeout(() => {
if(mask.childNodes.length > 3){
let child = mask.childNodes
mask.removeChild(child[0])
}
}, 5000, 5000)

如果你想要多列图片下落只需要添加track以及相应的延时和删除即可

目前的样式

多种图片下落

在下落前先将图片都缓存好

1
2
3
4
5
6
function cacheImg(){
let img = new Image()
img.src = require('../img/people0.png')
img.src = require('../img/people1.png')
...
}

然后在总loop中根据当前的loop数,switch到对应的img.src即可

1
2
3
4
5
6
7
8
switch(loop){
case 0: img.src = require('../img/people0.png') ; break;
case 1: img.src = require('../img/people1.png') ; break;
...
default: break;
}
loop = (loop + 1) % 11
}, 100, 5000)

分数记录

分数显示

创建一个函数方便进行分数的管理

1
2
3
4
5
6
7
8
9
10
11
12
13
function showScore(isMiss, data){
if(isMiss){
addScore.innerHTML = 'MISS'
addScore.style.color = 'red'
}else{
addScore.innerHTML = '+' + data
addScore.style.color = 'green'
}
addScore.style.display = 'flex'
let msgScore = setTimeout(() => {
addScore.style.display = 'none'
}, 1000)
}

分数记录

要想获得分数首先要将当前帧图片上传到后端,这里创建一个函数socket连接到后端不断上传图片

1
2
3
4
5
6
7
8
9
10
11
function canvasImgWs(){
recordedVideo.addEventListener('loadedmetadata',() => {
canvas.width = recordedVideo.videoWidth;
canvas.height = recordedVideo.videoHeight;

var canvasImgWs = outTimeout(() => {
canvas.getContext('2d').drawImage(recordedVideo, 0, 0, canvas.width, canvas.height);
wsSend(canvas.toDataURL("image/jpeg", 0.8).split(',')[1])
}, 0,800)
})
}

这里应后端要求只发送了base64,使用split进行分割

上传图片成功后监听服务端返回的数据即可,这里同样使用socket

1
2
3
4
5
6
7
8
9
10
11
12
ws.onmessage = (res) => {
let data = JSON.parse(res.data)
console.log(data)

if(data.is_detect){
is_detect = true
tempCount = data.score
showScore(!is_detect, tempCount)
}else{
is_detect = false
}
}

总结

其他一些细节就不贴了,因为博客距离这个项目完成的时间有点长。。许多逻辑忘记了,不过总体来说难度不算太大,主要的还是靠自己的思考吧,没有什么太大的技术难点,完成后还是觉得很有意思的,收获颇多。

Author: AddOneG
Link: http://yoursite.com/2018/06/15/软件杯H5端-视频尬舞机-开发记录/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.