<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="">


