前言

前段时间一直在忙一个基于WebRTC的PC和移动端双向视频的项目。第一次接触webRTC,难免遇到了许多问题,比如:webRTC移动端兼容性检测,如何配置MediaStreamConstraints, 信令(iceCandidate, sessionDescription)传输方式的选择,iceCandidate和sessionDescription设置的先后顺序,STUN和TURN的概念,如何实现截图及录制视频及上传图片和视频功能,如何高效跟踪错误等等。好记性不如烂笔头,特写此文以记之。

移动端兼容性

对PC端来说,webRTC早已被各大浏览器支持了,Chrome 28,FF22,Edge…随着不久之前发布的IOS11也宣布支持webRTC及getUserMedia,webRTC在移动端的应用前景也令人憧憬。

具体到实际项目中,经过测试,各大国产安卓手机自带的浏览器基本不支持webRTC,但这些安卓手机的微信内置浏览器均能良好地支持webRTC,虽然Chrome及Firefox的移动端版本也能良好的支持webRTC,但国情决定了微信内置浏览器作为最佳切入点。另一方面。IOS11中微信内置浏览器还不支持webRTC(我坚信不久的将来就会支持),但在Safari中能够完美支持。因此本项目选择了微信公众号为切入点,通过检测userAgent引导IOS11用户在Safari中打开页面。

检测webRTC的可行性,主要从getUserMedia和webRTC本身来入手:

function detectWebRTC() {
const WEBRTC_CONSTANTS = ['RTCPeerConnection', 'webkitRTCPeerConnection', 'mozRTCPeerConnection', 'RTCIceGatherer']; const isWebRTCSupported = WEBRTC_CONSTANTS.find((item) => {
return item in window;
}); const isGetUserMediaSupported = navigator && navigator.mediaDevices && navigator.mediaDevices.getUserMedia; if (!isWebRTCSupported || typeof isGetUserMediaSupported === 'undefined' ) {
return false;
} return true;
}

如果返回false,再去检测userAgent给予用户不支持的具体提示。

配置MediaStreamConstraints

所谓MediaStreamConstraints,就是navigator.mediaDevices.getUserMedia(constraints)传入的constraints,至于它的写法及功能,参考MDN,本文不做赘述。我在这里想要强调的是,对于移动端来说控制好视频图像的大小是很重要的,例如本项目中想要对方的图像占据全屏,这不仅是改变video元素的样式或者属性能做到的,首先要做的是改变MediaStreamConstraints中的视频分辨率(width, height),使其长宽比例大致与移动端屏幕的类似,然后再将video元素的长和宽设置为容器的长和宽(例如100%)。

另外对于getUserMedia一定要捕获可能出现的错误,如果是老的API,设置onErr回调,如果是新的(navigator.mediaDevices.getUserMedia),则catch异常。这样做的原因:getUserMedia往往不会完全符合我们的预期,有时即使设置的是ideal的约束,仍然会报错,如果不追踪错误,往往一脸懵逼。这也是后文要提到的高效追踪错误的方法之一。

搭建信令传输服务

要传输的信令包括两个部分:sessionDescription和iceCandidate。为了便于传输可将其处理成字符串,另一端接收时还原并用对应的构造函数构造对应的实例即可。

webRTC并没有规定信令的传输方式,而是完全由开发者自定义。常见的方式有短轮询、webSocket(socket.io等),短轮询的优点无非是简单,兼容性强,但在并发量较大时,服务器负荷会很重。而webSocket就不存在这个问题,但webSocket搭建起来较为复杂,并不是所有的浏览器都支持websocket。综合来说socket.io是个不错的解决方案,事件机制和自带的房间概念对撮合视频会话都是天然有利的,并且当浏览器不支持websocket时可以切换为轮询,也解决了兼容性的问题。

发起视频会话的流程

可以看到无论是发起方还是接受方,第一步都是getUserMedia获取本地媒体流,然后新建一个RTCPeerConnection实例,并指定好onicecandidate、onaddstream等回调:

// 指定TURN及STUN
const peerConnectionConfig = {
'iceServers': [
{
'urls': 'turn:numb.viagenie.ca',
'username': 'muazkh',
'credential': 'webrtc@live.com'
}, {
'urls': 'stun:stun.l.google.com:19302'
}
],
bundlePolicy: 'max-bundle',
}; const pc = new RTCPeerConnection(peerConnectionConfig);
pc.onicecandidate = ...;
pc.onaddstream = ...;

然后addTrack指定要传输的视频流

stream.getTracks().forEach((track) => { pc.addTrack(track, stream); });

发起方通过createOffer生成localDescription并传给pc.setLocalDescription(),pc获取了本地的sdp后开始获取candidate,这里的candidate指的是网络信息(ip、端口、协议),根据优先级从高到低分为三类:

  • host: 设备的ipv4或ipv6地址,即内网地址,一般会有两个,分别对应udp和tcp,ip相同,端口不同;
  • srflx(server reflexive): STUN返回的外网地址;
  • relay: 当STUN不适用时(某些NAT会为每个连接分配不同的端口,导致获取的端口和视频连接端口并不一致),中继服务器的地址;

三者之中只需要有一类连接成功即可,所以如果通信双方在同一内网,不配置STUN和TURN也可以直接连接。其实这里隐藏着性能优化的点:如上图所示,webRTC通信双方在交换candidate时,首先由发起方先收集所有的candidate,然后在icegatheringstatechange事件中检测iceGatheringState是否为’complete’,再发送给接收方。接收方设置了发送方传来的sdp和candidate后,同样要收集完自己所有的candidate,再发送给对方。如果这些candidate中有一对可以连接成功,则P2P通信建立,否则连接失败。

问题来了,接受端要等待发起方收集完所有的candidate之后才开始收集自己的candidate,这其实是可以同时进行的;另外其实不一定需要所有的candidate才能建立连接,这也是可以省下时间的;最后如果网络,STUN或者TURN出现问题,在上述传输模式下是非常致命的,会让连接的时间变得很长不可接受。

解决方案就是IETF提出的Trickle ICE。即发起方每获取一个candidate便立即发送给接收方,这样做的好处在于第一类candidate即host,会立即发送给接收方,这样接收方收到后可以立刻开始收集candidate,也就是发起方和接收方同时进行收集candidate的工作。另外,接收方每收到一个candidate会立即去检查它的有效性,如果有效直接接通视频,如果无效也不至于浪费时间。详情可以参见ICE always tastes better when it trickles.

至于sessionDescription及iceCandidate的传输,因为JavaScript没有处理sdp格式数据的方法,所以直接将其当做字符串处理,这样做的坏处是难以改变sdp中的信息(如果非要改,通过正则匹配还是能改的)。

在挂断视频时,不仅要关闭peerConnection,也要停止本地及远程的媒体流:


const tracks = localStream.getTracks().concat(remoteStream.getTracks());
tracks.forEach((track) => {
track.stop();
}); peerConnection.close();

截图&录制视频

截图其实并不算什么新鲜的东西,无非是利用canvas的drawImage函数获取video元素在某一帧的图像,得到的是图片的base64格式字符串,但要注意的是这样得到的base64码之前有这样一串文本:

data:image/png;base64,

这是对数据协议,格式,编码方式的声明,是给浏览器看的。所以在将drawImage得到的字符串上传给服务器时,最好将这串文本去掉,防止后端在转换图片时出现错误。

录制视频使用的是MediaRecorder API 详情参考MDN MediaRecorder,目前仅支持录制webm格式的视频。可以在新建MediaRecorder实例的时候,设置mimeType、videoBitsPerSecond、audioBitsPerSecond:

const options = {
mimeType: 'video/webm;codecs=vp8', // 视频格式及编码格式
videoBitsPerSecond: 2500000, // 视频比特率,影响文件大小和质量
audioBitsPerSecond: 128000 // 音频比特率,影响文件大小和质量
}; const recorder = new MediaRecorder(options);

在recorder的ondataavailable事件中拿到数据,将其转换为Blob对象,再通过Formdata异步上传至服务器。

错误追踪

整个双向视频涉及到的步骤较多,做好错误追踪是非常重要的。像getUserMedia时,一定要catch可能出现的异常。因为不同的设备,不同的浏览器或者说不同的用户往往不能完全满足我们设置的constraints。还有在实例化RTCPeerConnection时,往往会出现不可预期的错误,常见的有STUN、TURN格式不对,还有createOffer时传递的offerOptions格式不对,正确的应该为:

const offerOptions = {
'offerToReceiveAudio': true,
'offerToReceiveVideo': true
};

CAVEAT

因为webRTC标准还在不断地更新中,所以相关的API经常会有改动。

  • navigator.getUserMeida(已废弃),现在改为navigator.mediaDevices.getUserMedia;
  • RTCPeerConnection.addStream被RTCPeerConnection.addTrack取代;
  • STUN,TURN配置里的url现被urls取代;

另外,对video元素也要特殊处理。设置autoPlay属性,对播放本地视频源的video还要设置muted属性以去除回音。针对IOS播放视频自动全屏的特性,还要设置playsinline属性的值为true。

 

webRTC实战总结的更多相关文章

  1. (译)WebRTC实战: STUN, TURN, Signaling

    http://xiaol.me/2014/08/24/webrtc-stun-turn-signaling/ 原文:WebRTC in the real world: STUN, TURN and s ...

  2. webrtc之TURE、STUN、摄像头打开实战

    前言: 大家周末好,今天给 webrtc之TURE.STUN.摄像头打开实战 大家分享的是webrtc第一篇文章,在之前的音视频文章里面没有分享过关于webrtc的内容:在上个周末分享了一篇关于播放器 ...

  3. 实时音视频互动系列(下):基于 WebRTC 技术的实战解析

    在 WebRTC 项目中,又拍云团队做到了覆盖系统全局,保证项目进程流畅.这牵涉到主要三大块技术点: 网络端.服务端的开发和传输算法 WebRTC 协议中牵扯到服务端的应用协议和信令服务 客户端iOS ...

  4. 12┃音视频直播系统之 WebRTC 实现1对1直播系统实战

    一.搭建 Web 服务器 前面我们已经实现过,但是没有详细说HTTPS服务 首先需要引入了 express 库,它的功能非常强大,用它来实现 Web 服务器非常方便 同时还需要引入 HTTPS 服务, ...

  5. 基于 WebRTC 技术的实时通信服务开发实践

    随着直播的发展,直播实时互动性变得日益重要.又拍云在 WebRTC 的基础上,凭借多年的开发经验,结合当下实际情况,开发 UPRTC 系统,解决了网络延时.并发量大.客户端解码能力差等问题. WebR ...

  6. WebRTC 视频对话

    今天聊一下WebRTC.很多开发者,可能会觉得有些陌生,或者直接感觉繁杂.因为WebRTC在iOS上的应用,只是编译都让人很是头痛.这些话,到此为止,以防让了解者失去信心.我们只传播正能量,再多的困难 ...

  7. 利用peerjs轻松玩转webrtc

    随着5G技术的推广,可以预见在不久的将来网速将得到极大提升,实时音视频互动这类对网络传输质量要求较高的应用将是最直接的受益者.而且伴随着webrtc技术的成熟,该领域可能将成为下一个技术热点,但是传统 ...

  8. 1.2、初识WebRTC

    文章导读:本节内容,如标题所讲,“初识webrtc”.读完之后,我需要你能清楚三个问题:第一.真正的搞明白实时音视频在生产环境中的真实应用以及前景分析:第二.开发一个符合商业标准的实时音视频应用需要解 ...

  9. Kurento实战之一:KMS部署和体验

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

随机推荐

  1. 自动化运维工具——ansible安装入门(一)

    一.简介 现如今有很多运维自动化的工具,如:Ansible.Puppet.saltStack.Fabric.chef.Cfengine 1. Ansible介绍 Ansible 是由 Cobbler与 ...

  2. GNU汇编 程序状态字访问指令

    .text .global  _start _start: mrs r0,cpsr orr r0,#0b100 msr cpsr,r0

  3. Laravel 打印已执行的sql语句

    打开app\Providers\AppServiceProvider.PHP,在boot方法中添加如下内容 5.2以下版本 // 先引入DB use DB; // 或者直接使用 \DB:: DB::l ...

  4. JZOJ 3509. 【NOIP2013模拟11.5B组】倒霉的小C

    3509. [NOIP2013模拟11.5B组]倒霉的小C(beats) (File IO): input:beats.in output:beats.out Time Limits: 1000 ms ...

  5. Ecshop里添加多个h1标题

    目录 功能: 思路: 效果: pageheader_list.htm里 product_sn_list.htm模板里 控制器里 功能: 点击页面右边的两个按钮,切换下面的<div class=& ...

  6. Java课堂作业详解

    今天的Java课堂留下了一个作业:使用Eclipse编写一个程序,使输入的两个数进行加和,并且输出他们的和.对于这个题目,我们首先可以把它分解成为三个不同的小步骤 第一步就是输入这两个数,因为我们无需 ...

  7. hdu 5667

    Sequence Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/Others)Total Su ...

  8. 笔记-python-standard library-8.10 copy

    笔记-python-standard library-8.10 copy 1.      copy source code:Lib/copy.py python中的赋值语句不复制对象,它创建了对象和目 ...

  9. Redis实现之复制(二)

    PSYNC命令的实现 在Redis实现之复制(一)这一章中,我们介绍了PSYNC命令和它的工作机制,但一直没有说明PSYNC命令的参数以及返回值.现在,我们了解了运行ID.复制偏移量.复制积压缓冲区以 ...

  10. 使用WMI Filter 实现组策略的筛选!

    今天接到一个客户的一个问题,提到需要分系统版本分发相应的MSI程序.比如简体版接受简体版的分发程序,繁体版接受繁体版的分发程序!这个建立组策略的不同版本分发本身不会太难,我们只需要建立两个不同组策略分 ...