原生Js基于WebRTC实现Web端屏幕录制、回放、下载功能

引言

一切的一切都开始于那个夜黑风高的夜晚……
那晚,我急需要一款录屏软件,找来找去,可是:
付费去广告、付费去水印、付费高清、付费下载😅……
等等,突然我灵光乍现,我还有一条路:
自己做!
于是原生Js基于WebRTC实现的Web端实时录屏功能就出来了!文末附开源地址😎

技术选型

  • WebRTC
  • 原生Js

核心代码实现

html

整个的录屏功能区域都放在一个类名为main的div里面
类名为btn的div放上开始录屏button,并给button绑定start()函数,类名为rec

1
2
3
4
5
<div class="main">
<div class="btn">
<button onclick="start()" class="rec">点击录屏</button>
</div>
</div>

Js

js这里大体分为四部分:开始、结束、播放、下载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function start() {
......
/* 开始 */
}
function stop() {
......
/* 结束 */
}
function replay() {
......
/* 播放 */
}
function download() {
......
/* 下载 */
}

start()

我们使用Web API中的navigator.mediaDevices.getDisplayMedia方法来获取屏幕分享(包括音频和视频)的流

1
2
3
4
function start() {
navigator.mediaDevices.getDisplayMedia({ audio:true,video: true })
}

navigator.mediaDevices是Web API的一部分,它提供了访问用户的媒体输入设备(如摄像头、麦克风)和输出设备(如显示器、扬声器)的接口。

但我们只需要获取表示屏幕或应用程序窗口的媒体流,所以用到了getDisplayMedia
{ audio:true, video: true } 是一个约束对象,它指定了我们想要的媒体流的类型。我们需要获取的是包含音频(audio: true)和视频(video: true)的媒体流。

这个方法会返回一个Promise,该Promise在成功时解析会为一个MediaStream对象,该对象表示屏幕和音频的媒体流。随后我们将这个流传递给<video>元素

Promise成功后,会执行一个then异步操作方法

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
.then(function (s) {
stream = s;
mediaRecorder = new MediaRecorder(stream, {
mimeType: 'video/webm'
});
blobs = [], mediaRecorder;
videoTracks = stream.getVideoTracks();
if (videoTracks.length > 0) {
mediaRecorder.start(100);
mediaRecorder.ondataavailable = (e) => {
blobs.push(e.data);
};
document.body.innerHTML = `
<div class="main">
<div class="container">
<span style="--d: 7"></span>
<span style="--d: 6"></span>
<span style="--d: 5"></span>
<span style="--d: 4"></span>
<span style="--d: 3"></span>
<span style="--d: 2"></span>
<span style="--d: 1"></span>
<span style="--d: 0"></span>
<span style="--d: 1"></span>
<span style="--d: 2"></span>
<span style="--d: 3"></span>
<span style="--d: 4"></span>
<span style="--d: 5"></span>
<span style="--d: 6"></span>
<span style="--d: 7"></span>
</div>
</div> `;
}
})

s 是Promise的结果。

然后我们将Promise的结果赋值给 stream 变量: stream = s;

**mediaRecorder = new MediaRecorder(stream, {mimeType: ‘video/webm’});**:创建一个新的 MediaRecorder 对象,它将用于记录 stream 中的媒体内容。在这里我们设置了mimeType为 ‘video/webm’,因此记录的媒体内容将是 webm 格式的视频。

大家是不是会认为 blobs = [], mediaRecorder; 这行代码有些问题?它试图将两个不同的对象赋值给 blobs ,这在 JavaScript 中是不合法的。你认为应该这样写:blobs = [mediaRecorder]; ?然而并不是,这行代码的意思是创建一个新的数组,其中包含两个元素:一个空数组(用来存放 mediaRecorder 的可用数据)和 mediaRecorder 对象,然后将这个新数组赋值给 blobs 变量。
**videoTracks = stream.getVideoTracks();**是从 stream 中获取所有的视频轨道,并将它们存储在 videoTracks 变量中。

**if (videoTracks.length > 0) {…}**这行代码是在检查 videoTracks 中是否有视频轨道。如果有,我们才能将我们的项目继续下去

mediaRecorder.start(100); 这行代码将开始 mediaRecorder 的录制,参数100意味着每100毫秒,mediaRecorder 会生成一个新的数据片段。

mediaRecorder.ondataavailable = (e) => {blobs.push(e.data);}; 这行代码是在当 mediaRecorder 开始录制后产生可用数据时,将数据添加到 blobs 数组中。

document.body.innerHTML = “…”; 这行代码是在将HTML代码赋值给 document.body.innerHTML ,以动态改变html内容的方式来营造一个完美的用户体验,添加的HTML代码可以是一个动画来表示正在录屏,如下所示:
image.png
这个动画对应的css样式为

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
:root {
--h: 280px;
}
.container {
display: flex;
margin: 0 auto;
align-items: center;
height: var(--h);
}
.container span {
background: linear-gradient(to top, #d299c2 0%, #fef9d7 100%);
width: 50px;
height: 20%;
border-radius: calc(var(--h) * 0.2 * 0.5);
margin-right: 4px;
animation: loading 2.5s infinite linear;
animation-delay: calc(0.2s * var(--d));
}
.container span:last-child {
margin-right: 0px;
}
@keyframes loading {
0% {
background-image: linear-gradient(to right, #fa709a 0%, #fee140 100%);
height: 20%;
border-radius: calc(var(--h) * 0.2 * 0.5);
}
50% {
background-image: linear-gradient(to top, #d299c2 0%, #fef9d7 100%);
height: 100%;
border-radius: calc(var(--h) * 1 * 0.5);
}
100% {
background-image: linear-gradient(to top, #a8edea 0%, #fed6e3 100%);
height: 20%;
border-radius: calc(var(--h) * 0.2 * 0.5);
}
}

同时呢用户可能会拒绝给你权限来录制屏幕或发生一些其他的错误,那么就需要针对Promise的错误情况来做一些处理

1
2
3
.catch(function (error) {
console.log(error);
});

.catch(function (error) {…}) 是Promise的catch方法,它将在Promise发生错误时执行。error 是发生的错误对象。

然后我们用console.log(error); 在控制台中打印出错误信息,让我们可以在控制台中看到错误的详细信息,从而更容易地调试和解决问题。

stop()

开始录制当然也要停止录制了,通过创建一个stop函数来控制录制

1
2
3
4
5
6
7
8
9
10
11
function stop() {
if (stream) {
stream.getTracks().forEach(track => track.stop());
}
document.body.innerHTML = `
<div class="main">
<div class="scr">
<span class="stopLog">录制完毕!</span>
</div>
</div> `;
}

我们首先检查是否存在一个名为stream的变量。如果stream存在(也就是说,它不是null或者undefined),那么代码就会执行花括号里面的语句来停止录制。

如果stream不是null或者undefined那么**stream.getTracks()**方法就会被调用。这个方法返回一个包含流中所有轨道的数组。

然后,用 forEach() 方法被用于遍历这个数组。对于数组中的每一个轨道(track),都会调用它的**stop()**方法,以停止该轨道。

**document.body.innerHTML = “…”;**这行代码是在将HTML代码赋值给 document.body.innerHTML,以动态改变html内容的方式来提示用户录制结束,如下图所示:
image.png

replay()

当录制完毕后我们需要将录制好的视频供用户播放。

1
2
3
4
5
6
7
8
9
10
11
12
13
function replay() {
blob = new Blob(blobs, {
type: 'video/webm'
});
document.body.innerHTML = `
<div class="main">
<div class="scr">
<video class="vid" autoplay controls width="800px" height="280px"></video>
</div>
</div> `;
video = document.querySelector(".vid");
video.src = URL.createObjectURL(blob);
}

首先创建一个新的Blob对象,并使用先前存储的blobs数组和媒体类型video/webm
Blob对象表示一段二进制数据,在这里,它表示的是视频文件。

**document.body.innerHTML = “…”;**这行代码和之前的是一个道理,将HTML代码赋值给 document.body.innerHTML,以动态改变html内容的方式来显示录制好的视频

接下来,获取这个新的video元素,并将其src属性设置为新创建的Blob对象的URL。这是通过调用 URL.createObjectURL(blob) 实现的,这个方法会返回一个新的URL,代表blob对象。这样,video元素就可以加载并播放这个视频了。
video元素具有autoplaycontrols属性,意味着视频将在加载后自动播放,并且用户可以控制视频的播放。

download()

使用录屏功能的根本目的还是为了下载到本地,所以我们再创建一个下载函数

1
2
3
4
5
6
7
8
9
10
11
function download() {
blob = new Blob(blobs, {
type: 'video/webm'
});
url = URL.createObjectURL(blob);
a = document.createElement('a');
a.href = url;
a.download = 'record.webm';
a.style.display = 'none';
a.click();
}

这里和replay()函数的原理大体一致,通过调用 URL.createObjectURL(blob) 返回一个新的URL,代表blob对象。

接下来创建一个新的<a>元素,并将其href属性设置为新创建的URL

然后将<a>元素的download属性设置为record.webm,也就是将视频文件将被命名为record.webm

接着将<a>元素的style.display属性设置为none,这样用户在页面上就看不到这个链接了

最后调用<a>元素的**click()**方法,这会模拟用户点击链接的行为,从而自动下载视频文件。

结语

看到到这里,实现屏幕的录制、回放、下载这个功能实现起来是不是也没有很难了?
下面是GitHub开源地址,如果这篇文章对你有所启发,欢迎点赞收藏+转发😁

Github已开源,欢迎star⭐