效果
https://codepen.io/vicksiyi/pen/pMWybg
录制
- 服务端录制
- 客户端录制
服务端录制: 优点是不用担心客户因自身电脑问题造成录制失败(如磁盘空间不足),也不会因录制时抢占资源(CPU 占用率过高)而导致其他应用出现问题等;缺点是实现的复杂度很高。
客户端录制: 优点是方便录制方(如老师)操控,并且所录制的视频清晰度高,实现相对简单。这里可以和服务端录制做个对比,一般客户端摄像头的分辨率都非常高的(如 1280x720),所以客户端录制可以录制出非常清晰的视频;但服务端录制要做到这点就很困难了,本地高清的视频在上传服务端时由于网络带宽不足,视频的分辨率很有可能会被自动缩小到了 640x360,这就导致用户回看时视频特别模糊,用户体验差。不过客户端录制也有很明显的缺点,其中最主要的缺点就是录制失败率高。 因为客户端在进行录制时会开启第二路编码器,这样会特别耗 CPU。而 CPU 占用过高后,就很容易造成应用程序卡死。除此之外,它对内存、硬盘的要求也特别高。
基本原理
在了解基本原理之前首先得搞清楚以下问题:
- 录制后音视频流的存储格式是什么呢?(存储格式的选择对于录制后的回放很重要!!!)
- 录制下来的音视频流如何播放?(普通播放器播放 or 私有播放器 等)
- 怎样停止录制?
在接入正题之前,首先我们得思考一下:JS存储二进制数据类型(ArrayBuffer、ArrayBufferView、Blob)之间关系
1. ArrayBuffer
ArrayBuffer 对象表示通用的、固定长度的二进制数据缓冲区。因此,你可以直接使用它存储图片、视频等内容。
但你并不能直接对 ArrayBuffer 对象进行访问,类似于 Java 语言中的抽象类,在物理内存中并不存在这样一个对象,必须使用其封装类进行实例化后才能进行访问。
也就是说, ArrayBuffer 只是描述有这样一块空间可以用来存放二进制数据,但在计算机的内存中并没有真正地为其分配空间。只有当具体类型化后,它才真正地存在于内存中。如下所示:
let buffer = new ArrayBuffer(16); // 创建一个长度为 16 的 buffer
let view = new Uint32Array(buffer);
或
let buffer = new Uint8Array([255, 255, 255, 255]).buffer;
let dataView = new DataView(buffer);
在上面的例子中,一开始生成的 buffer 是不能被直接访问的。只有将 buffer 做为参数生成一个具体的类型的新对象时(如 Uint32Array 或 DataView),这个新生成的对象才能被访问。
2. ArrayBufferView
ArrayBufferView 并不是一个具体的类型,而是代表不同类型的 Array 的描述。这些类型包括:Int8Array、Uint8Array、DataView 等。也就是说 Int8Array、Uint8Array 等才是 JavaScript 在内存中真正可以分配的对象。
以 Int8Array 为例,当你对其实例化时,计算机就会在内存中为其分配一块空间,在该空间中的每一个元素都是 8 位的整数。再以 Uint8Array 为例,它表达的是在内存中分配一块每个元素大小为 8 位的无符号整数的空间。
通过这上面的描述,你现在应该知道 ArrayBuffer 与 ArrayBufferView 的区别了吧?ArrayBufferView 指的是 Int8Array、Uint8Array、DataView 等类型的总称,而这些类型都是使用 ArrayBuffer 类实现的,因此才统称他们为 ArrayBufferView。
3. Blob
Blob(Binary Large Object)是 JavaScript 的大型二进制对象类型,WebRTC 最终就是使用它将录制好的音视频流保存成多媒体文件的。而它的底层是由上面所讲的 ArrayBuffer 对象的封装类实现的,即 Int8Array、Uint8Array 等类型。
var aBlob = new Blob( array, options );
其中,array 可以是ArrayBuffer、ArrayBufferView、Blob、DOMString等类型 ;option,用于指定存储成的媒体类型。
怎样录制本地音视频?
var mediaRecorder = new MediaRecorder(stream[, options]);
- stream,通过 getUserMedia 获取的本地视频流或通过 RTCPeerConnection 获取的远程视频流。
- options,可选项,指定视频格式、编解码器、码率等相关信息,如
mimeType: 'video/webm;codecs=vp8'
。
MediaRecorder 对象还有一个特别重要的事件,即 ondataavailable 事件。当 MediaRecoder 捕获到数据时就会触发该事件。通过它,我们才能将音视频数据录制下来。
开始录制关键代码:
...
var buffer;
...
var handleDataAvailable = (e) => {
if (e && e.data && e.data.size > 0) {
buffer.push(e.data);
}
console.log(buffer);
}
record.onclick = () => {
buffer = [];
// 设置录制下来的多媒体格式
var options = {
mimeType: 'video/webm;codecs=vp8'
}
// 判断浏览器是否支持录制
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
console.error(`${options.mimeType} is not supported!`);
return;
}
try {
// 创建录制对象
mediaRecorder = new MediaRecorder(window.stream, options);
} catch (e) {
console.error('Failed to create MediaRecorder:', e);
return;
}
// 当有音视频数据来了之后触发该事件
mediaRecorder.ondataavailable = handleDataAvailable;
// 开始录制
mediaRecorder.start(10);
}
...
停止播放关键代码:
mediaRecorder.stop(10);
下载关键代码:
btnDownload.onclick = () => {
var blob = new Blob(buffer, { type: 'video/webm' });
var url = window.URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.style.display = 'none';
a.download = 'newBuffer.webm';
a.click();
}
完整代码:
<!DOCTYPE html>
<html>
<head>
<title>WebRTC Learing</title>
</head>
<body>
<div>
<video autoplay playsinline id="player"></video>
</div>
<div>
<video id="recvideo"></video>
</div>
<button id="record">Start Record</button>
<button id="recplay" disabled>Play</button>
<button id="download" disabled>Download</button>
</body>
<script>
'use strict';
var buffer;
var mediaRecorder
//是否已截图
var isSelectPicture = false;
var videoplay = document.querySelector('video#player');
var record = document.querySelector('button#record');
var recplay = document.querySelector('button#recplay');
var btnDownload = document.querySelector('button#download');
var recvideo = document.querySelector('video#recvideo')
var handleDataAvailable = (e) => {
if (e && e.data && e.data.size > 0) {
buffer.push(e.data);
}
console.log(buffer);
}
record.onclick = () => {
buffer = [];
// 设置录制下来的多媒体格式
var options = {
mimeType: 'video/webm;codecs=vp8'
}
// 判断浏览器是否支持录制
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
console.error(`${options.mimeType} is not supported!`);
return;
}
try {
// 创建录制对象
mediaRecorder = new MediaRecorder(window.stream, options);
} catch (e) {
console.error('Failed to create MediaRecorder:', e);
return;
}
// 当有音视频数据来了之后触发该事件
mediaRecorder.ondataavailable = handleDataAvailable;
// 开始录制
mediaRecorder.start(10);
record.disabled = true;
recplay.disabled = false;
}
recplay.onclick = () => {
mediaRecorder.stop(10);
recplay.disabled = true;
btnDownload.disabled = false;
var blob = new Blob(buffer, { type: 'video/webm' });
recvideo.src = window.URL.createObjectURL(blob);
recvideo.srcObject = null;
recvideo.controls = true;
recvideo.play();
}
btnDownload.onclick = () => {
var blob = new Blob(buffer, { type: 'video/webm' });
var url = window.URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.style.display = 'none';
a.download = 'newBuffer.webm';
a.click();
}
; (
() => {
if (!navigator.mediaDevices ||
!navigator.mediaDevices.getUserMedia) {
console.log('getUserMedia is not supported!');
return;
} else {
var constraints = {
video: {
width: 640,
height: 480,
frameRate: 15,
facingMode: 'enviroment'
},
audio: false
}
navigator.mediaDevices.getUserMedia(constraints)
.then((stream) => {
window.stream = stream
videoplay.srcObject = stream;
})
.catch((err) => {
console.log('getUserMedia error:', err);
});
}
}
)();
</script>
</html>