Android 应用与浏览器之间的 WebRTC 视频聊天
在这里,我们使用 WebRTC 流引擎在原生 Android 应用和 Web 浏览器 Google Chrome 或 Firefox 之间建立 WebRTC 连接。
自 WebRTC 公开以来,视频聊天变得更容易开发。为此涌现了许多 API、服务、服务器和框架。在本文中,我们将详细介绍一种开发 Web 浏览器与原生 Android 应用程序之间视频聊天的方法。
浏览器中的视频聊天
浏览器之间的经典 WebRTC 视频聊天始于 SDP (会话描述协议) 交换。**Alice** 将其 SDP 发送给 **Boris**,Boris 再回复他的 SDP。SDP 是一个类似以下的配置
o=- 1468792194439919690 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE audio video
a=msid-semantic: WMS 9nKsWmxMvOQBYaz9xhRffWeWSUbCbnox6aQ4
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:kSrQ
a=ice-pwd:4uIyZd/mbVSVe2iMFgC1i3qn
a=fingerprint:sha-256 6B:29:2F:47:EB:38:64:F3:25:CE:BD:E6:B0:3F:A6:FA:55:57:A9:EA:44:0B:7C:45:D2:0D:F4:96:8D:B2:9F:BA
a=setup:actpass
a=mid:audio
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=sendonly
a=rtcp-mux
a=rtpmap:111 opus/48000/2
a=rtcp-fb:111 transport-cc
a=fmtp:111 minptime=10;useinbandfec=1
a=rtpmap:103 ISAC/16000
a=rtpmap:104 ISAC/32000
a=rtpmap:9 G722/8000
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:106 CN/32000
a=rtpmap:105 CN/16000
a=rtpmap:13 CN/8000
a=rtpmap:110 telephone-event/48000
a=rtpmap:112 telephone-event/32000
a=rtpmap:113 telephone-event/16000
a=rtpmap:126 telephone-event/8000
a=ssrc:3525514540 cname:drYey7idt605CcEG
a=ssrc:3525514540 msid:9nKsWmxMvOQBYaz9xhRffWeWSUbCbnox6aQ4 09bdb6e7-b4b3-437b-945e-771f535052e3
a=ssrc:3525514540 mslabel:9nKsWmxMvOQBYaz9xhRffWeWSUbCbnox6aQ4
a=ssrc:3525514540 label:09bdb6e7-b4b3-437b-945e-771f535052e3
m=video 9 UDP/TLS/RTP/SAVPF 96 98 100 102 127 97 99 101 125
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:kSrQ
a=ice-pwd:4uIyZd/mbVSVe2iMFgC1i3qn
a=fingerprint:sha-256 6B:29:2F:47:EB:38:64:F3:25:CE:BD:E6:B0:3F:A6:FA:55:57:A9:EA:44:0B:7C:45:D2:0D:F4:96:8D:B2:9F:BA
a=setup:actpass
a=mid:video
a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:4 urn:3gpp:video-orientation
a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=sendonly
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:96 VP8/90000
a=rtcp-fb:96 ccm fir
a=rtcp-fb:96 nack
a=rtcp-fb:96 nack pli
a=rtcp-fb:96 goog-remb
a=rtcp-fb:96 transport-cc
a=rtpmap:98 VP9/90000
a=rtcp-fb:98 ccm fir
a=rtcp-fb:98 nack
a=rtcp-fb:98 nack pli
a=rtcp-fb:98 goog-remb
a=rtcp-fb:98 transport-cc
a=rtpmap:100 H264/90000
a=rtcp-fb:100 ccm fir
a=rtcp-fb:100 nack
a=rtcp-fb:100 nack pli
a=rtcp-fb:100 goog-remb
a=rtcp-fb:100 transport-cc
a=fmtp:100 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=rtpmap:102 red/90000
a=rtpmap:127 ulpfec/90000
a=rtpmap:97 rtx/90000
a=fmtp:97 apt=96
a=rtpmap:99 rtx/90000
a=fmtp:99 apt=98
a=rtpmap:101 rtx/90000
a=fmtp:101 apt=100
a=rtpmap:125 rtx/90000
a=fmtp:125 apt=102
a=ssrc-group:FID 2470936840 2969787502
a=ssrc:2470936840 cname:drYey7idt605CcEG
a=ssrc:2470936840 msid:9nKsWmxMvOQBYaz9xhRffWeWSUbCbnox6aQ4 ce9235c5-f300-466a-aadd-b969dc2f3664
a=ssrc:2470936840 mslabel:9nKsWmxMvOQBYaz9xhRffWeWSUbCbnox6aQ4
a=ssrc:2470936840 label:ce9235c5-f300-466a-aadd-b969dc2f3664
a=ssrc:2969787502 cname:drYey7idt605CcEG
a=ssrc:2969787502 msid:9nKsWmxMvOQBYaz9xhRffWeWSUbCbnox6aQ4 ce9235c5-f300-466a-aadd-b969dc2f3664
a=ssrc:2969787502 mslabel:9nKsWmxMvOQBYaz9xhRffWeWSUbCbnox6aQ4
a=ssrc:2969787502 label:ce9235c5-f300-466a-aadd-b969dc2f3664
从这个 SDP 配置中,我们可以看出,它建议使用 **H.264** 和 **VP8** 编解码器进行视频,以及 **Opus** 进行音频。此外,它还提供了大量其他用于通信的信息:编解码器优先级、**fir**、**nack**、**pli** 反馈的使用、**H.264** 编解码器的配置文件级别为 42e01f - Baseline 3.1 等等。
当您使用原生的 **WebRTC API** 实现视频聊天时,您需要理解 SDP、candidate、codec、ICE、STUN、TURN 等以及其他许多听起来令人生畏的术语。
WebRTC、Websockets 和 SIP
WebRTC 和 Websockets 这两个术语经常被混淆。有时 SIP 也会卷入其中。
好吧,我们可以肯定地说,**WebRTC 与 Websockets 或 SIP 都无关**。
**Websockets** 只是一个方便地将 **SDP** 从 Boris 传输到 Alice 的方法。我们可以使用普通的 **HTTP** 来实现,或者通过电子邮件发送 SDP。SDP 消息交换是信令信息,我们可以使用任何协议来发送它。对于浏览器,默认的发送数据协议是 Websockets 和 HTTP。因此,Websockets 主要被使用,因为它比 HTTP 更接近实时。您无法通过 Websockets 传输视频或音频,只能传输信令信息:文本和命令。
**SIP** 是一个用于交换消息的文本协议。WebRTC 在浏览器中经常被错误地称为 **SIP**,很可能是因为 SIP 消息也使用 SDP 来配置编解码器和建立连接。
另一方面,当我们说诸如 **SIP 电话** 之类的话时,我们指的是一种设备,它除了支持 **SIP (RFC3261)** 协议外,还支持十几种其他网络规范和协议:RTP、SDP、AVPF 等。
确实,其核心方面,WebRTC 使用与 SIP 电话(SRTP、STUN 等)相同的构建块。因此,可以说 WebRTC 和 SIP 设备及软件都使用相同的技术基础。但称浏览器中的 WebRTC 为 SIP 是不正确的,这至少是因为浏览器默认没有 SIP 功能。
**WebRTC** 是一项技术,具有三个主要的音视频传输功能:
- 捕获、编码和发送
- 接收、解码和播放
- 克服 NAT 和防火墙
此外还有许多辅助功能,如抖动补偿、自适应比特率、网络过载控制等。
如上所述,为了通过 WebRTC 成功传输媒体,Alice 和 Boris 应该交换包含视频流格式、打包和其他参数的详细信息的 SDP,这些参数指定了 SDP 发送者将如何接收视频。
除了交换 SDP 之外,可能还需要 **TURN 服务器**。如果无法建立 peer-to-peer 连接(例如,如果 Alice 或 Boris 具有某种不友好的 NAT,例如 **对称** NAT),该服务器将转发视频流量。
现在,假设我们想为聊天添加第三个活跃参与者,或者仅仅是一个观看者。这里有一个很好的例子:辩论。两个参与者交谈,而其他人只是观看。另一个例子是三人或多人聊天。
当第三个参与者加入时,情况会变得更复杂。现在每个参与者都需要**捕获和压缩** **两个视频流** 而不是一个,并**建立相互连接**以克服 **NAT**。在这种情况下,建立连接所需的时间会增加,而连接的稳定性会降低。同时压缩和发送两个或多个视频流会对 CPU 和网络造成严重负载,并影响质量,尤其是在移动设备上。
像这样的任务:
- 连接三个或更多参与者
- 连接视频聊天的附加订阅者
- 录制视频聊天
超出了 peer-to-peer 的范围,需要一个集中的 WebRTC 服务器来管理所有连接。
正如我们之前所说,有一些服务和服务器,以及或多或少方便的 WebRTC API 之上的 API,可以加快视频聊天开发,并允许使用更方便的抽象,即 **Stream**、**Room**、**Publisher**、**Subscriber** 等。
例如,要创建最简单的视频聊天,仅交换流的名称就足够了。Boris 知道 Alice 的流。Alice 知道 Boris 的流。视频聊天就准备好了。
浏览器中视频聊天的示例
在本文中,我们将演示 Streaming API 如何与 **Web Call Server 5** 一起工作,Web Call Server 是一个用于视频聊天和在线广播的 WebRTC 服务器。
视频聊天在操作中的演示如图所示的两个截图。第一个订阅者 **Alice** 看到视频聊天如下:
第二个订阅者 **Edward** 看到视频聊天如下:
在此示例中,发生了几件事:
- Alice 从浏览器向服务器发送名为 Alice 的视频流。
- Edward 从浏览器向服务器发送名为 Edward 的视频流。
- Alice 获取并播放了名为 Edward 的视频流。
- Edward 获取并播放了名为 Alice 的视频流。
从示例中可以看出,我们构建视频聊天的基础是 Alice 和 Edward 都知道对方的流名称。我们没有直接使用 SDP、PeerConnection、NAT、TURN 等。
因此,**通过简单地将流的名称传递给应该播放它们的人,就可以实现视频聊天。**
这个简单的概念允许使用任何前端和后端技术,如 Jquery、Bootstrap、React、Angular、PHP、Java、.Net 等。好消息是,嵌入视频流和视频聊天支持不会影响现有的 Web 应用程序。您可以通过允许(或拒绝)给定订阅者播放特定视频流来控制您的视频聊天。
浏览器中视频聊天的源代码
现在让我们看看相应的代码。带有视频聊天的 HTML 页面有两个主要的 **div 元素**:
- localVideo - 从网络摄像头捕获的视频
- remoteVideo - 从服务器播放的视频
您可以为这些 div 分配任意标识符,例如 id="captureVideo" 或 id="playbackVideo",但页面上必须同时存在这两个 div 元素。
带有 **localVideo** 和 **remoteVideo** 块的 HTML 页面如下所示:
<html>
<head>
<script language="javascript" src="flashphoner.js"></script>
<script language="javascript" src="video-chat.js"></script>
</head>
<body onLoad="init()">
<h1>Video Chat</h1>
<div id="localVideo" style="width:320px;height:240px;border: 1px solid"></div>
<div id="remoteVideo" style="width:320px;height:240px;border: 1px solid"></div>
<input type="button" value="connect" onClick="connect()"/>
<input type="button" value="publish" onClick="publish('Alice')"/>
<input type="button" value="play" onClick="play('Edward')"/>
<p id="status"></p>
</body>
</html>
现在,这是负责发送和播放视频的代码。
从网络摄像头发送流
要发送,我们使用 **session.createStream().publish()** API 方法。对于这个流,我们指定应该显示从网络摄像头捕获的视频的 HTML div 元素 **localVideo**,以及视频流的名称 **Alice**,这样任何连接的客户端知道此名称都可以播放该流。
session.createStream({
name: "Alice",
display: localVideo,
cacheLocalResources: true,
receiveVideo: false,
receiveAudio: false
}).on(Flashphoner.constants.STREAM_STATUS.PUBLISHING, function (publishStream) {
setStatus(Flashphoner.constants.STREAM_STATUS.PUBLISHING);
}).on(Flashphoner.constants.STREAM_STATUS.UNPUBLISHED, function () {
setStatus(Flashphoner.constants.STREAM_STATUS.UNPUBLISHED);
}).on(Flashphoner.constants.STREAM_STATUS.FAILED, function () {
setStatus(Flashphoner.constants.STREAM_STATUS.FAILED);
}).publish();
从服务器播放流
要播放,我们指定要播放的流的名称,以及应该显示从服务器接收的流的 HTML div 元素 **remoteVideo**。我们使用 **session.createStream().play()** API 方法。
session.createStream({
name: "Edward",
display: remoteVideo,
cacheLocalResources: true,
receiveVideo: true,
receiveAudio: true
}).on(Flashphoner.constants.STREAM_STATUS.PLAYING, function (playStream) {
setStatus(Flashphoner.constants.STREAM_STATUS.PLAYING);
}).on(Flashphoner.constants.STREAM_STATUS.STOPPED, function () {
setStatus(Flashphoner.constants.STREAM_STATUS.STOPPED);
}).on(Flashphoner.constants.STREAM_STATUS.FAILED, function () {
setStatus(Flashphoner.constants.STREAM_STATUS.FAILED);
}).play();
在使用服务器时,HTML 页面将从服务器接收各种状态,例如播放的 **PLAYING**、**STOPPED**,以及发布的 **PUBLISHING**、**UNPUBLISHED**。因此,视频聊天工作所需的基本操作是在网页上放置两个 div 块,并包含执行给定流名称的 stream.play() 和 stream.publish() 的相应脚本。**Two Way Streaming** 示例的完整源代码可以在 此处 下载。
Android 应用程序中 WebRTC 视频聊天的示例
Android 的视频聊天工作方式与浏览器中的视频聊天完全相同。该应用程序连接到服务器,并发送来自 Android 设备摄像头的视频流,以及从服务器接收并播放其他视频流。下面是 Android 应用 **Streaming Min**(浏览器中视频聊天 Two Way Streaming 示例的移动版本),它可以实现视频流的交换。 从屏幕截图中可以看出,没有变化。我们有两个视频窗口。左边的窗口显示从网络摄像头捕获的视频,右边的窗口显示从服务器接收的视频。视频流的交换也基于流名称。我们发布一个流并播放另一个流。
Android 应用程序视频聊天的源代码
虽然我们在浏览器中创建视频聊天时使用了包含 **flashphoner.js** API 脚本的 **Web SDK**,但对于功能齐全的 Android 应用程序,我们需要将 Android SDK 的 **aar 文件** 导入到项目中。要理解这一点是如何工作的,我们建议构建并执行基于 Android SDK 的 Streaming Min 示例。所有示例都可以在 github 仓库 中找到。
1. 下载所有示例
git clone https://github.com/flashphoner/wcs-android-sdk-samples.git
2. 下载 SDK
wget https://flashphoner.com/downloads/builds/flashphoner_client/wcs-android-sdk/aar/wcs-android-sdk-1.0.1.25.aar
3. 将 SDK 作为 aar 文件链接到示例。
cd export
./export.sh /tmp/wcs-android-sdk-1.0.1.25.aar
注意,我们将 export.sh 脚本指向下载的文件:**wcs-android-sdk-1.0.1.25.aar** - Android SDK
结果,在 **export/output** 文件夹中,您将找到一个完全配置好的项目,可以在 **Android Studio** 中打开。
现在您只需要使用 **gradle** 构建示例。
1 - 创建新的运行配置
2 - 选择 Gradle 脚本
3 - 构建
结果,我们将获得 **apk 文件**,可以安装到 Android 设备上。在此示例中,我们与浏览器交换了视频流。视频流 **test33** 从 Android 设备发送到服务器并在浏览器中播放。视频流 **8880** 由浏览器发送并在 Android 设备上播放。因此,我们实现了浏览器和 Android 应用之间的双向音视频通信。
在视频聊天的 Web 版本中,我们使用了 HTML div 元素来显示视频。在 Android 上,我们使用渲染器。
private SurfaceViewRenderer localRender;
private SurfaceViewRenderer remoteRender;
**localRenderer** 显示从 Android 设备摄像头捕获的视频。**remoteRenderer** 显示从服务器接收的视频。
1. 连接到服务器并设置渲染器。
sessionOptions = new SessionOptions(mWcsUrlView.getText().toString());
sessionOptions.setLocalRenderer(localRender);
sessionOptions.setRemoteRenderer(remoteRender);
...
session = Flashphoner.createSession(sessionOptions);
…
session.connect(new Connection());
2. 创建一个具有任意名称的流,并将该流发布到服务器。
StreamOptions streamOptions = new StreamOptions(mPublishStreamView.getText().toString());
publishStream = session.createStream(streamOptions);
...
publishStream.publish();
3. 指定要播放的流的名称,并从服务器获取该流。
StreamOptions streamOptions = new StreamOptions(mPlayStreamView.getText().toString());
playStream = session.createStream(streamOptions);
...
playStream.play();
StreamOptions streamOptions = new StreamOptions(mPlayStreamView.getText().toString()); playStream = session.createStream(streamOptions); ... playStream.play(); 完整的 StreamingMinActivity.java 类源代码 在此处 提供。整个 Android 的 Streaming Min 示例可在 此处 的仓库中找到。
Web Call Server
总之,我们演示了如何在浏览器中的 HTML 页面和 Android 应用程序之间创建简单的视频流交换。视频流通过 Web Call Server,它既是信令服务器,也是音频和视频代理。
Web Call Server - 是一款可以安装在 Linux 上、虚拟机或专用服务器上的**服务器软件**。WCS 是一个**流媒体 WebRTC 服务器**,它可以管理来自浏览器、iOS 和 Android 设备的视频流。
参考文献
技术和协议
WebRTC - WebRTC 技术
SDP - 会话描述协议, RFC
Websocket - Websocket 协议, RFC
用于视频聊天开发的服务器和 API
Web Call Server - 用于视频聊天的 WebRTC 流媒体服务器