H5端获取摄像头并发送流数据给后端

前置知识

上一篇文章是前端调用摄像头,然后把流数据给到video标签,用canvas截取图像。前端调用库解析二维码和条形码。

这篇文章是前端获取到流数据发送给后端,后端解析成功后把结果返回前端。

getUserMedia

获取摄像头/麦克风,回调成功会返回一个MediaStream,里面包含了请求的媒体类型的轨道。此流可以包含一个视频轨道(来自硬件或者虚拟视频源,比如相机、视频采集设备和屏幕共享服务等等)、一个音频轨道(同样来自硬件或虚拟音频源,比如麦克风、A/D转换器等等),也可能是其它轨道类型。MDN

1
2
3
4
5
6
7
navigator.mediaDevices.getUserMedia(constraints)
.then(function(stream) {
/* 使用这个stream stream */
})
.catch(function(err) {
/* 处理error */
});

MediaRecorder

录制MediaStream,产生流数据。MDN

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
navigator.mediaDevices.getUserMedia(constraints)
.then(function(stream) {
/* 使用这个stream */
let mediaRecorder = new MediaRecorder(stream,{ mimeType : 'video/webm' });
// 每3秒调用一次,这个参数必须写
mediaRecorder.start(3000);
mediaRecorder.onstart = function (e) {
console.log('mediaRecorder 开始录制');
};
mediaRecorder.ondataavailable = function (e) {
// e.data是视频的流数据Blob格式
console.log(e.data);
};
})
.catch(function(err) {
/* 处理error */
});

demo实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div id="scanner">
<div class="model">
<div class="scanner-view">
<div class="scanner-view-arrow arrow1"></div>
<div class="scanner-view-arrow arrow2"></div>
<div class="scanner-view-arrow arrow3"></div>
<div class="scanner-view-arrow arrow4"></div>
<div class="scanner-line"></div>
</div>
</div>
<video class="video-view" ref="video" autoplay playsinline="true" webkit-playsinline="true"></video>
</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
<script>
export default {
name: '',
data() {
return {
ws: '',
url: 'wss://192.168.0.110/websocket'
}
},
methods: {
initWebsocket(){
let _this = this;
if (this.ws){
// 已经建立连接
}else {
this.createWebsocket();
this.ws.onopen = function() {
//设置发信息送类型为:ArrayBuffer
_this.ws.binaryType = "arraybuffer";
};
this.ws.onmessage = function(e) {
console.log(e);
};
this.ws.onclose = function(e) {
console.log("onclose: closed");
_this.ws = '';
_this.createWebsocket();
};
this.ws.onerror = function(e) {
console.log("onerror: error");
_this.ws = '';
_this.createWebsocket();
}
}
},
createWebsocket(){
if ('WebSocket' in window){
this.ws = new WebSocket(this.url);
}else {
console.log('浏览器版本太低,请更换浏览器');
}
},
initVideo(constrains){
let _this = this;
if(navigator.mediaDevices.getUserMedia){
//最新标准API
navigator.mediaDevices.getUserMedia(constrains).then(_this.videoSuccess).catch(_this.videoError);
} else if (navigator.webkitGetUserMedia){
//webkit内核浏览器
navigator.webkitGetUserMedia(constrains).then(_this.videoSuccess).catch(_this.videoError);
} else if (navigator.mozGetUserMedia){
//Firefox浏览器
navagator.mozGetUserMedia(constrains).then(_this.videoSuccess).catch(_this.videoError);
} else if (navigator.getUserMedia){
//旧版API
navigator.getUserMedia(constrains).then(_this.videoSuccess).catch(_this.videoError);
}
},
videoSuccess(stream){
let video = this.$refs.video,
_this = this,chunks = [];
//将视频流设置为video元素的源
video.srcObject = stream;
//播放视频
video.play();

// 发送视频流
// 建立视频录制 MediaRecorder目前不支持ios
let mediaRecorder = new MediaRecorder(stream,{ mimeType : 'video/webm' });
// 每..秒调用一次,这个参数必须写
mediaRecorder.start(3000);
mediaRecorder.onstart = function (e) {
console.log('mediaRecorder 开始录制');
};
mediaRecorder.ondataavailable = function (e) {
chunks.push(e.data);
console.log(e.data.type);
let reader = new FileReader();
reader.addEventListener("loadend", function() {
//reader.result是一个含有视频数据流的Blob对象,这里把blob转成ByteBuffer
var videoBlob = new Uint8Array(reader.result);
console.log('视频数据流');
if(reader.result.byteLength > 0){
// websocket发送数据
_this.ws.send(videoBlob);
}
});
reader.readAsArrayBuffer(e.data);
};

},
videoError(error){
console.log("访问用户媒体设备失败:",error.name,error.message);
},
},
mounted(){
// 建立websocket连接
this.initWebsocket();
if (navigator.mediaDevices.getUserMedia || navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia){
//调用用户媒体设备,访问摄像头
this.initVideo({
video:{
height: 800,
facingMode: {
// 强制后置摄像头
exact: "environment"
}
}
});
} else {
alert("你的浏览器不支持访问用户媒体设备");
}
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<style scoped>
#scanner {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
position: relative;
}
.model{
box-sizing: border-box;
width: 100vw;
height: 100vh;
position: relative;
z-index: 88;
border-top: calc((100vh - 60vw)/2) solid rgba(0,0,0,.2);
border-bottom: calc((100vh - 60vw)/2) solid rgba(0,0,0,.2);
border-right: 20vw solid rgba(0,0,0,.2);
border-left: 20vw solid rgba(0,0,0,.2);
}
.scanner-view{
width: 100%;
height: 100%;
position: relative;
border: 1px solid rgba(255,255,255,.3);
z-index: 89;
}
.scanner-line{
position: absolute;
width: 100%;
height: 1px;
background: #49FF46;
border-radius: 20px;
z-index: 90;
animation: myScan 1s infinite alternate;
}
@keyframes myScan{
from {
top: 0;
}
to {
top: 34vh;
}
}
.scanner-view-arrow{
position: absolute;
width: 5vw;
height: 5vw;
border: 2px solid #09bb07;
}
.scanner-view-arrow.arrow1{
top: -1px;
left: 0px;
z-index: 99;
border-right: none;
border-bottom: none;
}
.scanner-view-arrow.arrow2{
top: -1px;
right: 0px;
z-index: 99;
border-left: none;
border-bottom: none;
}
.scanner-view-arrow.arrow3{
bottom: -1px;
left: 0px;
z-index: 99;
border-right: none;
border-top: none;
}
.scanner-view-arrow.arrow4{
bottom: -1px;
right: 0px;
z-index: 99;
border-left: none;
border-top: none;
}
.video-view{
position: absolute;
width: 100vw;
height: 100vh;
object-fit: cover;
top: 0px;
left: 0px;
z-index: 80;
}
</style>

注意事项

  • 截止到现在2019-05-06IOS端不支持MediaRecorder,所以IOS端到目前为止不能发送流数据。具体的支持情况可以查看caniuse