Files
CategoryResourceRepost/极客时间专栏/从0打造音视频直播系统/WebRTC 1对1通话/01 | 原来通过浏览器访问摄像头这么容易.md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

242 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<audio id="audio" title="01 | 原来通过浏览器访问摄像头这么容易" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/20/5a/20a3b85bed43e9bbec6ca0bf1da5b05a.mp3"></audio>
对于很多从事 JavaScript 开发的同学来说,基本都认为 JavaScript 是专门做页面控制的。如果用 JavaScript 做音视频处理那真是很难想象的事儿。你可能首先想到的问题是JavaScript或者浏览器的性能跟得上吗
而 Google 却不这么认为。 Google 就是要做一些常人无法想象,又难以理解的事情,否则它就不是 Google 了。
“**浏览器 + WebRTC**”就是 Google 给出的答案。2011年Google 创立了 WebRTC 项目,其愿景就是可以在浏览器之间快速地实现音视频通信。
随着 WebRTC 1.0 规范的推出现在主流浏览器Chrome、Firefox、Safari以及 Edge 都已经支持了WebRTC库。换句话说**在这些浏览器之间进行实时音视频通信已经很成熟了**。
下面我就通过讲解JavaScript/浏览器访问电脑上的音视频设备,向你展示通过现代浏览器访问音视频设备是何其简单。
## WebRTC处理过程
在正式讲解如何通过浏览器采集音视频数据之前,我先向你介绍一下 WebRTC 实现一对一音视频实时通话的整个处理过程。对这个过程的了解,可以帮助你在阅读文章时,能清楚明了地知道所阅读的这篇文章、所要学习的知识点在整个处理过程中的位置。
<img src="https://static001.geekbang.org/resource/image/c5/a0/c536a1dd0ed50008d2ada594e052d6a0.png" alt="">
上面这幅图是整个WebRTC 1对1音视频实时通话的过程图。通过这幅图你可以看出要实现 1对1音视频实时通话其过程还是蛮复杂的。
这幅图从大的方面可以分为4部分即**两个 WebRTC 终端**(上图中的两个大方框)、**一个 Signal信令服务器**和**一个 STUN/TURN 服务器**。
- WebRTC 终端负责音视频采集、编解码、NAT 穿越、音视频数据传输。
- Signal 服务器,负责信令处理,如加入房间、离开房间、媒体协商消息的传递等。
- STUN/TURN服务器负责获取WebRTC终端在公网的IP地址以及NAT穿越失败后的数据中转。
接下来,我就向你描述一下**WebRTC进行音视频通话的大体过程**。
当一端WebRTC终端进入房间之前它首先会检测自己的设备是否可用。如果此时设备可用则进行**音视频数据采集**,这也是本篇我们要介绍的重点内容。
采集到的数据一方面可以做预览,也就是让自己可以看到自己的视频;另一方面,可以将其录制下来保存成文件,等到视频通话结束后,上传到服务器让用户回看之前的内容。
在获取音视频数据就绪后WebRTC终端要发送 “**加入**” 信令到 Signal 服务器。Signal 服务器收到该消息后会创建房间。在另外一端,也要做同样的事情,只不过它不是创建房间,而是加入房间了。待第二个终端成功加入房间后,第一个用户会收到 “**另一个用户已经加入成功**” 的消息。
此时,第一个终端将创建 **“媒体连接” 对象**,即**RTCPeerConnection**(该对象会在后面的文章中做详细介绍),并将采集到的音视频数据通过 RTCPeerConnection 对象进行编码,最终通过 P2P 传送给对端。
当然,在进行 P2P 穿越时很有可能失败。所以当P2P穿越失败时为了保障音视频数据仍然可以互通则需要通过 TURN 服务器TURN服务会在后面文章中专门介绍进行音视频数据中转。
这样,当音视频数据 “历尽千辛万苦” 来到对端后,对端首先将收到的音视频数据进行解码,最后再将其展示出来,这样就完成了一端到另一端的单通。如果双方要互通,那么,两方都要通过 RTCPeerConnection 对象传输自己一端的数据,并从另一端接收数据。
以上,就是这幅图大体所描述的含义。而本文要重点介绍的内容就是 WebRTC 终端中的音视频采集部分。
## 音视频采集基本概念
在正式介绍 JavaScript 采集音视频数据的 API 之前你还需要了解一些基本概念。这些概念虽然都不难理解但在后面讲解API 时都会用到它们,很是重要,所以在这里我还是给你着重汇总和强调下。
- **摄像头**。用于捕捉(采集)图像和视频。
- **帧率**。现在的摄像头功能已非常强大,一般情况下,一秒钟可以采集 30 张以上的图像,一些好的摄像头甚至可以采集 100 张以上。我们把**摄像头一秒钟采集图像的次数称为帧率**。帧率越高,视频就越平滑流畅。然而,在直播系统中一般不会设置太高的帧率,因为帧率越高,占的网络带宽就越多。
- **分辨率**。摄像头除了可以设置帧率之外,还可以调整分辨率。我们常见的分辨率有 2K、1080P、720P、420P等。分辨率越高图像就越清晰但同时也带来一个问题即占用的带宽也就越多。所以在直播系统中分辨率的高低与网络带宽有紧密的联系。也就是说分辨率会跟据你的网络带宽进行动态调整。
- **宽高比**。分辨率一般分为两种宽高比,即 16:9 或 4:3。4:3的宽高比是从黑白电视而来而 16:9的宽高比是从显示器而来。现在一般情况下都采用 16:9的比例。
- **麦克风**。用于采集音频数据。它与视频一样,可以指定一秒内采样的次数,称为**采样率**。每个采样用几个bit表示称为**采样位深或采样大小**。
- **轨Track**。WebRTC 中的“轨”借鉴了多媒体的概念。火车轨道的特性你应该非常清楚,两条轨永远不会相交。“轨”在多媒体中表达的就是**每条轨数据都是独立的,不会与其他轨相交**,如 MP4 中的音频轨、视频轨,它们在 MP4 文件中是被分别存储的。
- **流Stream**。可以理解为容器。在 WebRTC 中“流”可以分为媒体流MediaStream和数据流DataStream。其中[媒体流](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia)可以存放0个或多个音频轨或视频轨数据流可以存0个或多个数据轨。
## 音视频采集
有了上面这些基本概念,你就可以很容易理解后面所要讲的内容了。接下来,就让我们来具体看看在浏览器下采集音视频的 API 格式以及如何控制音视频的采集吧。
### 1. getUserMedia 方法
在浏览器中访问音视频设备非常简单,只要调用**getUserMedia**这个 API 即可。该 API 的基本格式如下:
```
var promise = navigator.mediaDevices.getUserMedia(constraints);
```
它返回一个**Promise**对象。
- 如果**getUserMedia**调用成功,则可以通过 Promise 获得**MediaStream**对象,也就是说现在我们已经从音视频设备中获取到音视频数据了。
- 如果调用失败,比如用户拒绝该 API 访问媒体设备(音频设备、视频设备),或者要访问的媒体设备不可用,则返回的 Promise 会得到 PermissionDeniedError 或 NotFoundError 等错误对象。
### 2. MediaStreamConstraints 参数
从上面的调用格式中可以看到,**getUserMedia**方法有一个输入参数**constraints**,其类型为 **MediaStreamConstraints**。它可以指定**MediaStream**中包含哪些类型的媒体轨(音频轨、视频轨),并且可为这些媒体轨设置一些限制。
下面我们就来详细看一下它包括哪些限制这里我引用一下WebRTC 1.0 规范对 [MediaStreamConstraints](https://w3c.github.io/mediacapture-main/getusermedia.html#mediastreamconstraints)的定义,其格式如下:
```
dictionary MediaStreamConstraints {
(boolean or MediaTrackConstraints) video = false,
(boolean or MediaTrackConstraints) audio = false
};
```
从上面的代码中可以看出,该结构可以指定采集音频还是视频,或是同时对两者进行采集。
举个例子,比如你只想采集视频,则可以像下面这样定义 constraints
```
const mediaStreamContrains = {
video: true
};
```
或者,同时采集音视和视频:
```
const mediaStreamContrains = {
video: true,
audio: true
};
```
其实,你还可以通过 MediaTrackConstraints 进一步对每一条媒体轨进行限制,比如下面的代码示例:
```
const mediaStreamContrains = {
video: {
frameRate: {min: 20},
width: {min: 640, ideal: 1280},
height: {min: 360, ideal: 720},
aspectRatio: 16/9
},
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
};
```
上面这个例子表示:视频的帧率最小 20 帧每秒;宽度最小是 640理想的宽度是 1280同样的高度最小是 360最理想高度是 720此外宽高比是 16:9对于音频则是开启回音消除、降噪以及自动增益功能。
除了上面介绍的这些参数来控制摄像头和麦克风外,当然还有其他一些参数可以设置,更详细的参数信息,可以跳到下面的参考部分。
通过上面的这些方式就可以很方便地控制音视频的设备了,是不是非常简单?
## 如何使用 getUserMedia API
接下来,我们看一下如何使用上面介绍的 API 来采集视频数据吧。
下面的HTML代码非常简单它引入一段JavaScript代码用于捕获音视频数据然后将采集到的音视频数据通过 video 标签播放出来。
```
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;title&gt;Realtime communication with WebRTC&lt;/title&gt;
&lt;link rel=&quot;stylesheet&quot;, href=&quot;css/client.css&quot; /&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h1&gt;Realtime communication with WebRTC &lt;/h1&gt;
&lt;video autoplay playsinline&gt;&lt;/video&gt;
&lt;script src=&quot;js/client.js&quot;&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
```
为便于你更好地理解该部分的知识,上面这段代码中有两条代码我需要解释一下,一句是:
```
&lt;video autoplay playsinline&gt;&lt;/video&gt;
```
它是HTML5的视频标签不仅可以播放多媒体文件还可以用于播放采集到的数据。其参数含义如下
- **autoplay**,表示当页面加载时可以自动播放视频;
- **playsinline**表示在HTML5页面内播放视频而不是使用系统播放器播放视频。
另一句是:
```
&lt;script src=&quot;js/client.js&quot;&gt;&lt;/script&gt;
```
它引入了外部的 JavaScript 代码,起到的作用就是获取视频数据。具体代码如下:
```
'use strict';
const mediaStreamContrains = {
video: true
};
const localVideo = document.querySelector('video');
function gotLocalMediaStream(mediaStream){
localVideo.srcObject = mediaStream;
}
function handleLocalMediaStreamError(error){
console.log('navigator.getUserMedia error: ', error);
}
navigator.mediaDevices.getUserMedia(mediaStreamContrains).then(
gotLocalMediaStream
).catch(
handleLocalMediaStreamError
);
```
通过上面的代码,我们就可以采集到视频数据并将它展示在页面上了,很简单吧!接下来,我们来大体看一下它的逻辑。
JavaScript 代码中首先执行**getUserMedia()**方法,该方法会请求访问 Camera。如果是第一次请求 Camera浏览器会向用户弹出提示窗口让用户决定是否可以访问摄像头。如果用户允许访问且设备可用则调用 gotLocalMediaStream 方法。
在gotLocalMediaStream方法中其输入参数为**MediaStream**对象,该对象中存放着**getUserMedia**方法采集到的音视频**轨**。我们将它作为视频源赋值给 HTML5 的 video 标签的 srcObject 属性。这样在 HTML 页面加载之后,就可以在该页面中看到摄像头采集到的视频数据了。
在这个例子中,**getUserMedia**方法的输入参数**mediaStreamContraints**限定了只采集视频数据。同样的,你也可以采集音频数据或同时采集音频和视频数据。
## 小结
在 WebRTC 中,**MediaTrack**和**MediaStream**这两个概念特别重要,后续学习 WebRTC的过程中我们会反复用到所以在这最开始你就要理解透这两个概念。举个例子如果你想在一个房间里同时共享视频、共享音频、共享桌面该怎么做呢如果你对 MediaTrack 和 MediaStream 真正理解了就会觉得WebRTC处理这种情况太简单了。
另外,在本文中我重点介绍了**getUserMedia**这个API**它是 WebRTC 几个核心API之一你必须熟练掌握它**。因为通过它,你可以对音视频设备做各种各样的控制,例如,是采集音频,还是采集视频?视频的分辨率是多少?帧率是多少?音频的采样率是多少?
当然特别关键的一点是可以通过该API开启**回音消除**。回音消除问题是所有做实时互动直播系统最难解决的问题之一。对于 JavaScript 开发同学来说现在只需要调用该API时将回音消除选项打开就可以了一下子解决了世界难题。
最后,我还通过一个例子向你具体展示了视频采集后的效果。相信通过这些讲解和展示,你应该已经感受到目前浏览器的强大,以及它可以做更多、更有意思的音视频相关的事情了。
这里你也可以看一下我做出来的效果图(没有美颜):
<img src="https://static001.geekbang.org/resource/image/2a/4f/2a063c7520647f37e3a97a3558fff34f.png" alt="">
## 思考时间
上面我们一起学习了如何通过**getUserMedia**获取到音视频数据。而在真实的场景中,我们往往不但要获取到默认设备的音视频数据,还要能获取到**某个指定的设备**的音视频数据。比如,手机上一般都有两个摄像头——前置摄像头和后置摄像头。那么,你有没有办法采集到指定摄像头的视频数据呢?
欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。
另外为便于你更好地学习我还做了个Demo放在了[GitHub上有需要可点这里](https://github.com/avdance/webrtc_web/tree/master/01_mediastream)。
## 参考
getUserMedia API 控制设备的参数及其含义如下:
<img src="https://static001.geekbang.org/resource/image/f3/8a/f3d578d13b4c21c83b161dae348b8c8a.png" alt="">