01 | 原来通过浏览器访问摄像头这么容易

对于很多从事 JavaScript 开发的同学来说,基本都认为 JavaScript 是专门做页面控制的。如果用 JavaScript 做音视频处理,那真是很难想象的事儿。你可能首先想到的问题是:JavaScript或者浏览器的性能跟得上吗?

而 Google 却不这么认为。 Google 就是要做一些常人无法想象,又难以理解的事情,否则它就不是 Google 了。

浏览器 + WebRTC”就是 Google 给出的答案。2011年,Google 创立了 WebRTC 项目,其愿景就是可以在浏览器之间快速地实现音视频通信。

随着 WebRTC 1.0 规范的推出,现在主流浏览器Chrome、Firefox、Safari以及 Edge 都已经支持了WebRTC库。换句话说,在这些浏览器之间进行实时音视频通信已经很成熟了

下面我就通过讲解JavaScript/浏览器访问电脑上的音视频设备,向你展示通过现代浏览器访问音视频设备是何其简单。

WebRTC处理过程

在正式讲解如何通过浏览器采集音视频数据之前,我先向你介绍一下 WebRTC 实现一对一音视频实时通话的整个处理过程。对这个过程的了解,可以帮助你在阅读文章时,能清楚明了地知道所阅读的这篇文章、所要学习的知识点在整个处理过程中的位置。

WebRTC 1对1音视频实时通话过程示意图

上面这幅图是整个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)。其中,媒体流可以存放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的定义,其格式如下:

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 标签播放出来。

<!DOCTYPE html>
<html>
    <head>
        <title>Realtime communication with WebRTC</title>
        <link rel="stylesheet", href="css/client.css" />
    </head>
    <body>
        <h1>Realtime communication with WebRTC </h1>
        <video autoplay playsinline></video>
        <script src="js/client.js"></script>
    </body>
</html>

为便于你更好地理解该部分的知识,上面这段代码中有两条代码我需要解释一下,一句是:

<video autoplay playsinline></video>

它是HTML5的视频标签,不仅可以播放多媒体文件,还可以用于播放采集到的数据。其参数含义如下:

  • autoplay,表示当页面加载时可以自动播放视频;
  • playsinline,表示在HTML5页面内播放视频,而不是使用系统播放器播放视频。

另一句是:

<script src="js/client.js"></script>

它引入了外部的 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 中,MediaTrackMediaStream这两个概念特别重要,后续学习 WebRTC的过程中,我们会反复用到,所以在这最开始你就要理解透这两个概念。举个例子,如果你想在一个房间里,同时共享视频、共享音频、共享桌面,该怎么做呢?如果你对 MediaTrack 和 MediaStream 真正理解了,就会觉得WebRTC处理这种情况太简单了。

另外,在本文中我重点介绍了getUserMedia这个API,它是 WebRTC 几个核心API之一,你必须熟练掌握它。因为通过它,你可以对音视频设备做各种各样的控制,例如,是采集音频,还是采集视频?视频的分辨率是多少?帧率是多少?音频的采样率是多少?

当然,特别关键的一点是可以通过该API开启回音消除。回音消除问题是所有做实时互动直播系统最难解决的问题之一。对于 JavaScript 开发同学来说,现在只需要调用该API时,将回音消除选项打开就可以了,一下子解决了世界难题。

最后,我还通过一个例子向你具体展示了视频采集后的效果。相信通过这些讲解和展示,你应该已经感受到目前浏览器的强大,以及它可以做更多、更有意思的音视频相关的事情了。

这里你也可以看一下我做出来的效果图(没有美颜):

思考时间

上面我们一起学习了如何通过getUserMedia获取到音视频数据。而在真实的场景中,我们往往不但要获取到默认设备的音视频数据,还要能获取到某个指定的设备的音视频数据。比如,手机上一般都有两个摄像头——前置摄像头和后置摄像头。那么,你有没有办法采集到指定摄像头的视频数据呢?

欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。

另外,为便于你更好地学习,我还做了个Demo,放在了GitHub上(有需要可点这里)

参考

getUserMedia API 控制设备的参数及其含义如下:

精选留言

  • 当当

    2019-07-16 12:29:59

    老师,直接在chrome测试成功,声音不正常(回声很想),加了参数消除回音,还是不行,为什么啊?
    作者回复

    自己的音频没有mute 吧?把video 标签里加个muted 试试

    2019-07-16 15:35:09

  • 蓝桥

    2019-07-18 00:14:39

    你好,老师,现有的 rtmp 的直播解决方案和 webRTC 之间优劣势在什么地方?
    作者回复

    Rtmp 底层用的tcp,wenrtc底层主要使用udp,使用tcp 就注定他在极端网络情况下没法实时通信

    2019-07-18 07:35:08

  • 西西弗与卡夫卡

    2019-07-16 07:32:11

    获取前置摄像头
    const mediaStreamContrains = {
    video: { facingMode: "user" },
    audio: true
    };

    获取后置摄像头
    const mediaStreamContrains = {
    video: { facingMode: "environment" },
    audio: true
    };
  • 2019-07-16 12:22:02

    其他平台学过老师的课程,讲的很详细。一直希望老师讲讲Android端相关的知识点,后来想来,音视频重点还是在服务端,Web 目前又比较普及,无论是PC还是移动终端,都越来越多的支持 Web 了。
    作者回复

    谢谢,铁粉呀!

    2019-07-16 15:32:37

  • venn

    2019-10-13 11:47:23

    请问老师,用webrtc做视频直播的话,是不是意味着教师端需要同时和所有学生端直接建立连接,然后音视频不需要经过服务器而直接发到学生端,这样的话对教师端的带宽有要求吗?这种直播稳定吗
    作者回复

    你说的是通过 WebRTC 实现多人实时互动。这个有好几种方案,多终端互联是Mesh方案,还有 SFU, MCU方案。多人互动最好的方案是 SFU,这个在专栏中我已经介绍过了。使用这种方案可以节省多人互动时的带宽。

    2019-10-14 13:27:43

  • Geek_4b4f4a

    2019-07-16 08:37:40

    你好,老师,我是做webrtc的原生Android客户端的,您的课程有这方面的讲解吗?
    作者回复

    这个专栏中没有,其实学完这个专栏你自己可以用android 实现了,很简单

    2019-07-16 09:34:38

  • 李尧|Wonder

    2019-07-26 22:23:26

    老师,我打开audio之后回音特别大最后有很尖锐的声音,是什么原因?我试着在video中加入muted还是不行
    作者回复

    那是产生了啸叫,应该是你其它页面也开着音频,你将 所以音频关掉试试还有吗?没有后再将其它开,保证只有一个页页开着音频就好了。

    2019-07-27 07:55:19

  • 超威丶

    2019-07-16 09:44:14

    请问NAT穿越是啥?
    作者回复

    P2P,端与端直接进行连接,不需要服务器中转数据,这样可以节省服务器带宽,但并不意味着不需要服务器,服务器作为辅助功能

    2019-07-16 10:23:06

  • hh

    2019-08-15 06:53:14

    请问老师,本地打开html可以看到到画面,代码理解,但不知如何给server.js跑起来,如何部署在服务器里呢,配合nginx咋弄呢,对js这方面几乎是小白,望解惑?
    作者回复

    后面的文章会有讲,也会有例子,别着急!

    2019-08-16 01:37:01

  • 1900

    2019-07-16 15:39:02

    数据轨和音频轨视频轨的区别在哪里呢?
    作者回复

    数据轨是存放二进制数据,比如传输文件,就要使用数据轨。

    2019-07-16 21:17:38

  • 不是山谷-13.25

    2020-03-23 10:31:59

    老师,拒绝了摄像头访问之后再次进入怎么才能重新弹出授权弹框啊?
    作者回复

    如果重新刷新不行的话,你就到浏览器的设置中去修改一下;具体位置你在网上找找

    2020-03-24 16:08:11

  • Geek_ualcx9

    2019-12-10 16:20:51

    还没专门搭signal服务器和STUN/TRUN服务器,只是用

    python -m SimpleHTTPServer

    简单的试了一下老师的demo,成功了!一点点正反馈,再前进。
    作者回复

    恭喜!恭喜!

    2020-01-14 23:30:58

  • 一一

    2019-11-18 17:19:54

    1 经测试,通过file:///E:/test/WebRTCTest/day01/WebRTC1.html本地打开Chrome、Edge浏览器,可以正常显示摄像头图像。
    2 将页面部署到 IIS服务器中,通过http://192.168.1.37:81/day01/WebRTC1.html在以上浏览器中,无法显示摄像头图像。
    3 看了网友的留言,老师回复需要https协议,还需要购买个域名,备案,才能解决2中的问题。
    作者回复

    是的!

    2019-11-23 17:27:06

  • 小小小丶盘子

    2019-09-27 17:46:01

    将老师的代码部署到自己站点,手机试了一下可以,电脑没有设备,但是报错是 DOMException,改为alert 的话,就提示 NotFoundError:Requested device not found。
    作者回复

    你是 HTTPS 服务?

    2019-09-29 23:13:04

  • ammo

    2019-09-06 19:26:47

    想知道chrome如何直接播放rtsp直播流。现在后台给我的就是rtsp的播放链接。找了些方案要不太落后,要不就是要收费的。大致知道可以通过websocket转发给前端。有没有开源的直接方案,马上要上项目了。。。。。
    作者回复

    没办法直接播 rtsp 流,因为浏览器不支持 rtsp。一种比较好的方案是做一个代理将 rtsp 转成 浏览器支持的流,不过短时间内也做不出来的

    2019-09-08 07:54:27

  • Shawn·Pen

    2019-08-20 21:27:59

    老师,请问基础开发环境如何搭建?有没有截图什么的?多谢!
    作者回复

    你可以使用任何开发工具进行开发,在布署的时候最后有一个云服务器,并申请一个域名和 https 证书来布署你的页面。

    2019-08-21 00:04:40

  • Geek_leo长沙

    2019-07-30 17:36:32

    老师 我搭了stun服务,基于ubuntu系统搭的,已经放到公网,同nat内的设备可以连接看到视频。但是不同nat的设备无法看到彼此画面。要怎么增加turn服务呢。我找了几天没找到合适资料,就是搭建turn服务的,能帮推荐一下吗。可能需求描述不够到位,我目标就是要实现不同nat下的设备能互联看视频,这流程不同,我没法进行下一步webrtc学习了感觉,麻烦帮推荐或者指导下,多谢啦
    作者回复

    STUN 服务器是用于获主机的外网IP地址的,这样才能进行 NAT 穿越。但在国网 NAT 穿越的成功率还不到 50%,在NAT 穿越不成功的情况下该如何保证通信双方的连通性呢?你可以朝这个方向找想关资料,后面的文章里也会有解决办法,静请期待!

    2019-07-31 00:41:01

  • Geek_leo长沙

    2019-07-26 23:50:56

    我自己搭了一个sturn服务器,放的外网阿里云。手机端的应用。两个设备都连wifi情况下可以视频连接。但是一个连wifi和一个用移动网络的终端互相都看不到对方视频。看信令日志 连接是成功了。不知道哪里出问题了。老师能帮分析下吗
    作者回复

    那当然不行了,在wifi内可以 P2P 连接,在外网无法进行 NAT 穿越,所以通不了。

    2019-07-27 07:52:56

  • 麦晓杰alwaysu

    2019-07-23 17:44:20

    本地调试https,可以使用这个https://github.com/FiloSottile/mkcert。
    作者回复

    非常棒,谢谢!回头偿试一下。

    2019-07-23 18:28:46

  • 曹士远

    2019-07-19 09:03:37

    STUN/TURN 服务器是否有开源的,我之前看到都是使用谷歌的。
    作者回复

    是的coturn就可以,大家都用它!

    2019-07-19 23:51:55