需求
随着国家对个体户税收政策的收紧,公司需要采集客户的“话术视频”来作为规避业务风险的“凭据”。具体来说就是,需要用户在手机端录制一段“我已同意 xxx”的视频,并且录制完成之后,上传至服务器保存。
经过搜集信息,大致有两种实现思路:
基于 input[capture] 的实现
和
基于 WebRTC 的实现
;input 这种不是本文重点,略过。
基于 WebRTC 的实现的基本思路是:
- 调用 MediaDevices 获取设备相机和麦克风权限;
- 调用 getUserMedia 方法创建 MediaStream 数据流;
- 调用 MediaRecorder 捕获媒体流并对数据流捕获和缓存;
- 如果文件较大,可能需要 Blob 等 API 对媒体流进行分包上传;
- 如果产品有需要,可能后端要调用 ffmpeg 等工具对视频编码进行转换。
WebRTC
前端调用设备采集视频流主要采用的是
WebRTC 实时通讯技术
,它是一项可以用于视频聊天、音频聊天或 P2P 文件分享的技术。
整个 WebRTC 主要有以下几个 API 组成:
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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
|
let media = null;
// 请求当前设备可用的媒体输入输出设备
const enumerateDevicesPromise = navigator.mediaDevices.enumerateDevices();
enumerateDevicesPromise
.then(mediaDevices => {
if (!Array.isArray(mediaDevices)) {
throw new Error('未找到媒体设备');
}
mediaDevices.forEach(({ deviceId, groupId, kind, label }) => {
console.group('EnumerateDevices');
console.log('设备 ID', deviceId);
console.log('设备组 ID', groupId);
console.log('设备类型', kind);
console.log('设备名称', label);
console.groupEnd();
});
const mediaDevicesKinds = mediaDevices.map(({ kind }) => kind);
if (!mediaDevicesKinds.includes('videoinput')) {
throw new Error('没有相机权限');
}
if (!mediaDevicesKinds.includes('audioinput')) {
throw new Error('没有麦克风权限');
}
})
.catch(error => {
console.error(error);
});
// 请求当前设备可用的 Constraints 属性
const supportedConstraints = navigator.mediaDevices.getSupportedConstraints();
const supportedConstraintsKey = Object.entries(supportedConstraints)
.filter(item => item[1])
.map(item => item[0]);
// MediaStreamConstraints 对象,用于指定“请求的媒体类型和相对应的参数”
const userMediaConstraints = {
video: {
width: { min: 1024, ideal: 1280, max: 1920 }, // 指定视频宽度
height: { min: 776, ideal: 720, max: 1080 }, // 指定视频高度
frameRate: { min: 15, ideal: 30, max: 60 }, // 指定视频帧率
facingMode: { exact: 'environment' } // 强制使用后置摄像头
// facingMode: 'user' // 优先使用前置摄像头
},
// video: true, // 不指定分辨率
audio: true
};
// 过滤 userMediaConstraints 中可用的属性
Object.keys(userMediaConstraints).forEach(key => {
if (!supportedConstraintsKey.includes(key)) {
delete userMediaConstraints[key];
}
});
// 返回的 promise 可能既不会 resolve 也不会 reject,因为现在的操作系统权限控制不是非黑即白的
const userMediaPromise =
navigator.mediaDevices.getUserMedia(userMediaConstraints);
userMediaPromise
.then(mediaStream => {
// 如果用户授予了媒体权限,userMediaPromise 会 resolve 一个 MediaStream 对象
const monitorEle = document.querySelector('.monitor');
/**
* 使用 srcObject 将不需要借助 URL.createObjectURL(搭配 src 属性)
* 仅有 Safari 完整支持了 MediaStream、MediaSource、Blob、File
* 大部分浏览器仅支持 MediaStream,不过对我们来说足够了
*/
monitorEle.loop = false;
monitorEle.muted = true;
monitorEle.srcObject = mediaStream;
monitorEle.onloadedmetadata = () => {
// 视频元数据加载完毕,开始播放
monitorEle.play();
};
})
.catch(error => {
// 如果用户拒绝了媒体权限,会 reject 一个 PermissionDeniedError/NotFoundError
switch (error.name) {
case 'AbortError':
console.error('中止错误');
break;
case 'NotAllowedError':
console.error('拒绝错误,用户拒绝了媒体访问请求');
break;
case 'NotFoundError':
console.error('找不到错误');
break;
case 'NotReadableError':
console.error('无法读取错误,别的硬件、软件问题');
break;
case 'OverconstrainedError':
console.error('无法满足要求错误,无法满足 constraint 的要求');
break;
case 'SecurityError':
console.error('安全错误,设备媒体被禁止,和用户设置有关');
break;
case 'TypeError':
console.error('类型错误,constraint 设置问题');
break;
default:
// 建议将 error 上报
break;
}
});
// 用来进行屏幕共享,当前业务场景下用不到
let captureStream = null;
const getDisplayMediaPromise =
navigator.mediaDevices.getDisplayMedia(userMediaConstraints);
getDisplayMediaPromise
.then(mediaStream => {
captureStream = mediaStream;
})
.catch(error => {
switch (error.name) {
case 'AbortError':
console.error('中止错误');
break;
case 'InvalidStateError':
console.error('无效状态错误,当前页面处于非激活状态');
break;
case 'NotAllowedError':
console.error(
'拒绝错误,用户拒绝了访问屏幕区域的权限或者不允许当前浏览实例访问屏幕共享'
);
break;
case 'NotFoundError':
console.error('找不到错误,没有可用于捕获的屏幕视频源');
break;
case 'NotReadableError':
console.error(
'无法读取错误,发生了硬件或操作系统级别错误或锁定,从而预先占用了共享所选源'
);
break;
case 'OverconstrainedError':
console.error(
'转换错误,创建流后,由于无法生成兼容的流导致应用指定的 constraints 失效'
);
break;
case 'TypeError':
console.error('类型错误,constraint 设置问题');
break;
default:
// 建议将 error 上报
break;
}
})
.finally(() => {
// 保存 captureStream,保存至服务器
});
//
|
接下来就是使用
MediaRecorder
进行视频流的录制:
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
|
let chunks = [];
const { isTypeSupported } = MediaRecorder;
const mimeType = isTypeSupported('video/mp4')
? 'video/mp4'
: isTypeSupported('video/mpeg')
? 'video/mpeg'
: isTypeSupported('video/webm;codecs=h264')
? 'video/webm;codecs=h264'
: isTypeSupported('video/webm;codecs=h265')
? 'video/webm;codecs=h265'
: isTypeSupported('video/webm;codecs=vp8')
? 'video/webm;codecs=vp8'
: isTypeSupported('video/webm;codecs=vp9')
? 'video/webm;codecs=vp9'
: isTypeSupported('video/webm')
? 'video/webm'
: 'video/*';
const mediaRecorder = new MediaRecorder(mediaStream, {
mimeType
});
mediaRecorder.ondataavailable = ({ data }) => {
if (data.size > 0) {
chunks.push(e.data);
}
};
mediaRecorder.onstart = () => {
console.log('录制开始');
};
mediaRecorder.onpause = () => {
console.log('录制暂停');
};
mediaRecorder.onresume = () => {
console.log('录制继续');
};
mediaRecorder.onstop = () => {
console.log('录制停止');
};
mediaRecorder.onerror = () => {
console.error('录制失败');
};
mediaRecorder.start(); // 开始录制
mediaRecorder.pause(); // 暂停录制
mediaRecorder.resume(); // 继续录制
mediaRecorder.stop(); // 结束录制
// 暂停/继续录制
const pauseBtn = document.querySelector('.pause-btn');
pauseBtn.onclick = () => {
if (mediaRecorder.state === 'recording') {
mediaRecorder.pause();
} else if (mediaRecorder.state === 'paused') {
mediaRecorder.resume();
}
};
|
兼容性
根据
旷视发布的 RTC 兼容性说明
,iOS 和原生 Android 内置的浏览器和 WebView 组件都已经很好的支持了 WebRTC;腾讯的 X5 也对 WebRTC 有不错的支持。
如果出现了运行错误,可以使用
adapter.js
垫片并使用
Agora WebRTC Precall Test
检测当前环境对 WebRTC 的支持情况。
如果 iOS 不能实时预览和回放,请
参阅
。
小米部分机型不识别 MediaStream,并且没有对应的开发文档。
DEMO 在
这里
。