第2章 处理浏览器中的媒体
在本章中,我们将开始研究 WebRTC 框架的细节,该框架基本上指定了一组 JavaScript API,用于开发基于 Web 的应用程序。 这些 API 一开始就被认为是用于实现基本用例的友好工具,例如一对一的音频/视频调用。 它们还应具有足够的灵活性,以保证专家开发人员可以实现各种复杂得多的使用场景。 因此,为程序员提供了一组 API,这些 API 可以大致分为三个逻辑组:
本地和远程音频和视频的获取和管理:
MediaStream界面(以及 HTML5<audio>和<video>标签的相关用法)
连接管理:
RTCPeerConnection接口
管理任意数据:
RTCDataChannel接口
WebRTC 的 10 个步骤
以下 10 个步骤描述了 WebRTC API 的典型使用场景:
- 从本地设备(如麦克风、网络摄像头)创建一个
MediaStream对象。 - 从本地
MediaStream获取 URL Blob - 使用获取的 URL Blob 进行本地预览
- 创建一个
RTCPeerConnection对象 - 将本地流添加到新创建的连接
- 将您自己的会话描述发送到远程对等点 Send your own session description to the remote peer.
- 从您的对等方接收远程会话描述 Receive the remote session description from your peer.
- 处理收到的会话描述,并将远程流添加到您的
RTCPeerConnection - 从远程流获取 URL Blob
- 使用获取的 URL Blob 播放远程对等方的音频和/或视频
我们将逐步完成上述步骤。 在本章的其余部分中,我们将涉及整个基于 WebRTC 的点对点通信生命周期的前三个阶段。 这意味着我们暂时将忘记远程对等方,而只专注于如何从浏览器中访问和使用本地音频和视频资源。 在执行此操作的同时,我们还将研究如何在限制条件下播放(例如,强制视频分辨率)。
::: danger WebRTC 支持的浏览器
在撰写本文时,WebRTC API 在 Chrome,Firefox 和 Opera 中可用。 本书中包含的所有示例均已使用这些浏览器进行了测试。 为了简洁起见(由于 Opera 和 Chrome 在实现 API 方面几乎完全相同),我们从现在开始只将 Chrome 和 Firefox 作为运行客户端平台的示例。
:::
媒体捕获及数据流
W3C Media Capture and Streams 文档定义了一组 JavaScript API,这些API使应用程序能够从平台请求音频和视频流,以及操纵和处理流数据。
流媒体 API
MediaStream 接口用于表示媒体数据流。 流可以是输入或输出,也可以是本地或远程(例如,本地网络摄像头或远程连接)。 必须注意,单个 MediaStream 可以包含零个或多个轨道。 每个轨道都有一个对应的 MediaStreamTrack 对象,该对象代表用户代理中的特定媒体源。 MediaStream 中的所有轨道在渲染时进行同步。MediaStreamTrack 表示包含一个或多个通道的内容,其中,通道之间具有定义的已知的关系。 通道是此 API 规范中考虑的最小单位。 图2-1显示了由单个视频轨道和两个不同的音频(左声道和右声道)轨道组成的 MediaStream

图2-1 由一个视频轨道和两个音频轨道组成的 MediaStream
W3C Media Capture Streams API 定义了两种方法 getUserMedia() 和 createObjectUrl(),以下各节对此进行了简要说明。
获取本地多媒体内容
getUserMedia() API,通过指定一组(强制或可选)成功和失败的回调函数,Web 开发人员可以访问本地设备媒体(当前是音频和/或视频)
getUserMedia(constraints, successCallback, errorCallback)
getUserMedia() 提示用户许可使用其网络摄像头或其他视频或音频输入。
URL
createObjectUrl() 方法指示浏览器创建和管理与本地文件或二进制对象(blob)关联的唯一URL:
createObjectURL(stream)
它在 WebRTC 中的典型用法是从 MediaStream 对象开始创建 Blob URL 。 然后,将在 HTML 页面内使用 Blob URL 。 实际上,本地和远程流都需要此过程。
使用 getUserMedia() API
让我们开始使用 getUserMedia() API 调用及其返回的 MediaStream 对象。 先准备一个带有 JavaScript 代码的简单 HTML 页面,使我们可以访问本地视频资源并将其显示在 HTML5 <video> 标签中。示例 2-1 显示了我们为第一个示例构建的非常简单的页面。
例2-1 我们的第一个启用 WebRTC 的 HTML 页面
<!DOCTYPE html><html><head><title>getUserMedia very simple demo</title></head><body><div id="mainDiv"><h1><code>getUserMedia()</code> very simple demo</h1><p>With this example, we simply call <code>getUserMedia()</code> and display the received stream inside an HTML5 <video> element</p><p>View page source to access both HTML and JavaScript code...</p><video autoplay></video><script src="js/getUserMedia.js"></script></div></body></html>
例2-2 文件 js/getUserMedia.js
// Look after different browser vendors' ways of calling the getUserMedia()// API method:// Opera --> getUserMedia// Chrome --> webkitGetUserMedia// Firefox --> mozGetUserMedianavigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;// Use constraints to ask for a video-only MediaStream:var constraints = {audio: false, video: true};var video = document.querySelector("video");// Callback to be called in case of success...function successCallback(stream) {// Note: make the returned stream available to console for inspectionwindow.stream = stream;if (window.URL) {// Chrome case: URL.createObjectURL() converts a MediaStream to a blob URL// Reference: https://github.com/a-wing/webrtc-book-cn/issues/1// video.src = window.URL.createObjectURL(stream);video.srcObject = stream;} else {// Firefox and Opera: the src of the video can be set directly from the streamvideo.src = stream;}// We're all set. Let's just play the video out!video.play();}// Callback to be called in case of failures...function errorCallback(error) {console.log("navigator.getUserMedia error: ", error);}// Main action: just call getUserMedia() on the navigator objectnavigator.getUserMedia(constraints, successCallback, errorCallback);
以下屏幕截图显示了将页面加载到Chrome(图2-2)或Firefox(图2-3)时的外观。

图2-2 在 Chrome 中打开我们的第一个示例

图2-3 在 Firefox 中打开我们的第一个示例
::: danger 在 Chrome 中打开 JavaScript 文件
如果要在本地计算机上的 Google Chrome 中测试代码,则将面临一些挑战,因为由于安全限制,Chrome 默认不会加载本地文件。 为了解决这些问题,您必须在计算机上本地运行 Web 服务器并使用它来提供应用程序的文件,或者在启动浏览器时使用 --allow-file-access-from-files 选项。
:::
从上图中可以看出,两种浏览器在访问本地设备(在本例中为网络摄像头)之前都需要征得用户的同意。 在征得用户的明确同意后,浏览器最终将获取的 MediaStream 与页面相关联,如图 2-4 和 2-5 所示。
注意:权限授予与网页的域相关,并且此权限不会扩展到网页上的弹出窗口和其他框架。
图2-4 在 Chrome 中显示获取的 MediaStream
图2-5 在 Firefox 中显示获取的 MediaStream
深入研究上面报告的简单代码的一些细节,我们可以重点介绍如何调用 API 方法 getUserMedia(constraints, successCallback, errorCallback),其参数具有以下含义:
constraints对象(请参阅第19页的“媒体约束”),用于指定对只收集本地视频感兴趣的穿戴者(constraints = {audio: false, video: true})success callback如果调用,将传递给MediaStream。在我们的例子中,首先将这样的MediaStream提供给控制台以供用户检查(window.stream = stream;)然后,将其附加到HTML5页面的<video>元素上并最终显示。 参考控制台对返回对象的检查,图 2-6 显示了在 Chrome 开发人员工具窗口中此类活动输出的快照。 每个MediaStream都有一个标签,并包含一个或多个表示音频或视频通道的MediaStreamTracks。

图2-6 在 Chrome 的控制台中检查 MediaStream
关于返回的流如何附加到 <video> 元素,请注意,Chrome 要求转换为所谓的 Blob URL(video.src = window.URL.createObjectURL(stream);),而其他启用 WebRTC 的浏览器允许您按原样使用它(video.src = stream;)。
::: tip 译者 注
Chrome 71 移除了 MediaStream 的 URL.createObjectURL
video.src = window.URL.createObjectURL(stream);
应该改成这种写法:
video.srcObject = stream;
:::
failure callback如果被调用,则传递给错误对象。 在我们的基本示例中,提到的回调仅将返回的错误记录到控制台(console.log("navigator.getUserMedia error: ", error);)
媒体模型
浏览器提供了从源(sources)到接收器(sinks)的媒体管道(pipeline)。 在浏览器中,接收器是 <img>,<video> 和 <audio> 标记。 来源可以是物理网络摄像头,麦克风,来自用户硬盘驱动器的本地视频或音频文件,网络资源或静态图像。 这些来源产生的媒体通常不会随时间变化。 这些源可以被认为是静态的。 向用户显示此类源的接收器(实际标签本身)具有用于控制源内容的各种控件。
getUserMedia() API方法添加了动态源,例如麦克风和相机。 这些来源的特性可能会根据应用需求而变化,这些来源本质上可以被认为是动态的。
媒体约束
约束是一项可选功能,用于限制 MediaStream 轨道源上允许的可变性范围。约束通过 Constrainable 接口在轨道上公开,该接口包括用于动态更改约束的 API
getUserMedia() 调用还允许在首次获取轨道时应用一组初始约束(例如,设置视频分辨率的值)
约束的核心概念是一种功能,它由对象的属性或特征以及可能的值集组成,可以将值指定为范围或枚举。
约束存储在 track 对象上,而不是对象资源。每个轨道都可以选择使用约束进行初始化。另外,可以在初始化之后通过专用的约束 API 添加约束。
约束可以是可选的或强制的。可选约束由有序列表表示,而强制约束与无序集合相关。
这样做是为了在发布 API 的最终版本之前为更多约束提供支持。约束包括高宽比,面向照相机的模式(正面或背面),音频和视频帧率,视频高度和宽度等等。
使用约束
在本节中,我们将快速了解如何使用 getUserMedia() 调用获取轨道时如何应用初始约束集。
::: danger WebRTC 浏览器中的 getUserMedia() 约束支持
目前仅在 Chrome 中支持 getUserMedia() 约束。 本节中的示例将假定您使用此浏览器。
:::
首先,在 例2-3 中构建HTML页面。
例2-3 播放和约束:在 HTML 页面
<!DOCTYPE html><!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN""http://www.w3.org/TR/html4/loose.dtd"><html><head><title>getUserMedia() and constraints</title></head><body><div id="mainDiv"><h1><code>getUserMedia()</code>: playing with video constraints</h1><p>Click one of the below buttons to change video resolution...</p><div id="buttons"><button id="qvga">320x240</button><button id="vga">640x480</button><button id="hd">1280x960</button></div><p id="dimensions"></p><video autoplay></video><script src="js/getUserMedia_constraints.js"></script></div></body></html>
从 例2-3 中的代码片段和 图2-7 中的快照都可以看到,该页面包含三个按钮,每个按钮与以特定分辨率(从低分辨率到高清视频)表示的本地视频流相关联

图2-7 一个简单的 HTML 页面,显示 Chrome 中约束的使用
例2-4 显示了用于获取本地视频流并明确定义的分辨率添加到网页的 JavaScript 代码
例2-4 播放约束: getUserMedia_constraints.js 文件
// Define local variables associated with video resolution selection// buttons in the HTML pagevar vgaButton = document.querySelector("button#vga");var qvgaButton = document.querySelector("button#qvga");var hdButton = document.querySelector("button#hd");// Video element in the HTML5 pagevarvideo = document.querySelector("video");// The local MediaStream to play withvar stream;// Look after different browser vendors' ways of calling the// getUserMedia() API method:navigator.getUserMedia = navigator.getUserMedia ||navigator.webkitGetUserMedia || navigator.mozGetUserMedia;// Callback to be called in case of success...function successCallback(gotStream) {// Make the stream available to the console for introspectionwindow.stream = gotStream;// Attach the returned stream to the <video> element// in the HTML pageif (window.URL) {// Chrome case: URL.createObjectURL() converts a MediaStream to a blob URL// Reference: https://github.com/a-wing/webrtc-book-cn/issues/1// video.src = window.URL.createObjectURL(stream);video.srcObject = stream;} else {// Firefox and Opera: the src of the video can be set directly from the streamvideo.src = stream;}// Start playing videovideo.play();}// Callback to be called in case of failure...function errorCallback(error){console.log("navigator.getUserMedia error: ", error);}// Constraints object for low resolution videovar qvgaConstraints = {video: {mandatory: {maxWidth: 320,maxHeight: 240}}};// Constraints object for standard resolution videovar vgaConstraints = {video: {mandatory: {maxWidth: 640,maxHeight: 480}}};// Constraints object for high resolution videovar hdConstraints = {video: {mandatory: {minWidth: 1280,minHeight: 960}}};// Associate actions with buttons:qvgaButton.onclick = function() {getMedia(qvgaConstraints)};vgaButton.onclick = function() {getMedia(vgaConstraints)};hdButton.onclick = function() {getMedia(hdConstraints)};// Simple wrapper for getUserMedia() with constraints object as// an input parameterfunction getMedia(constraints) {if (!!stream) {video.src = null;stream.getTracks().forEach(function (track) { track.stop(); });}navigator.getUserMedia(constraints, successCallback, errorCallback);}
例2-4 中的代码非常简单。 核心部分与约束对象的正确定义有关,每个约束对象都可以作为输入参数传递给 getUserMedia() 函数。 其中包含的三个样本对象只是简单地指出,视频应视为强制性的,并根据视频的宽度和高度的下限进一步指定分辨率。 为了使读者了解其含义,图2-8 和 图2-9 分别显示了320×240 和 640×480 的分辨率流。
图2-8 在 Chrome 中显示 320×240 分辨率的视频
图2-9 在 Chrome 中显示 640×480 分辨率的视频
