webRTC实战总结
前言
前段时间一直在忙一个基于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实战总结的更多相关文章
- (译)WebRTC实战: STUN, TURN, Signaling
http://xiaol.me/2014/08/24/webrtc-stun-turn-signaling/ 原文:WebRTC in the real world: STUN, TURN and s ...
- webrtc之TURE、STUN、摄像头打开实战
前言: 大家周末好,今天给 webrtc之TURE.STUN.摄像头打开实战 大家分享的是webrtc第一篇文章,在之前的音视频文章里面没有分享过关于webrtc的内容:在上个周末分享了一篇关于播放器 ...
- 实时音视频互动系列(下):基于 WebRTC 技术的实战解析
在 WebRTC 项目中,又拍云团队做到了覆盖系统全局,保证项目进程流畅.这牵涉到主要三大块技术点: 网络端.服务端的开发和传输算法 WebRTC 协议中牵扯到服务端的应用协议和信令服务 客户端iOS ...
- 12┃音视频直播系统之 WebRTC 实现1对1直播系统实战
一.搭建 Web 服务器 前面我们已经实现过,但是没有详细说HTTPS服务 首先需要引入了 express 库,它的功能非常强大,用它来实现 Web 服务器非常方便 同时还需要引入 HTTPS 服务, ...
- 基于 WebRTC 技术的实时通信服务开发实践
随着直播的发展,直播实时互动性变得日益重要.又拍云在 WebRTC 的基础上,凭借多年的开发经验,结合当下实际情况,开发 UPRTC 系统,解决了网络延时.并发量大.客户端解码能力差等问题. WebR ...
- WebRTC 视频对话
今天聊一下WebRTC.很多开发者,可能会觉得有些陌生,或者直接感觉繁杂.因为WebRTC在iOS上的应用,只是编译都让人很是头痛.这些话,到此为止,以防让了解者失去信心.我们只传播正能量,再多的困难 ...
- 利用peerjs轻松玩转webrtc
随着5G技术的推广,可以预见在不久的将来网速将得到极大提升,实时音视频互动这类对网络传输质量要求较高的应用将是最直接的受益者.而且伴随着webrtc技术的成熟,该领域可能将成为下一个技术热点,但是传统 ...
- 1.2、初识WebRTC
文章导读:本节内容,如标题所讲,“初识webrtc”.读完之后,我需要你能清楚三个问题:第一.真正的搞明白实时音视频在生产环境中的真实应用以及前景分析:第二.开发一个符合商业标准的实时音视频应用需要解 ...
- Kurento实战之一:KMS部署和体验
欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...
随机推荐
- 【前端_js】js中数字字符串之间的比较
js中字符串间的比较是按照位次优先,比较各字符的ASCII大小,包括数字字符串之间的比较. 1.console.log("1"<"3");//true 2 ...
- 安装Tesseract
下载网站 https://digi.bib.uni-mannheim.de/tesseract/
- CSS 实现单行、多行文本溢出显示省略号
单行溢出 width:200px; overflow:hidden; text-overflow:ellipsis; -o-text-overflow:ellipsis; -webkit-text-o ...
- 高并发架构系列:如何从0到1设计一个类Dubbo的RPC框架
在过去持续分享的几十期阿里Java面试题中,几乎每次都会问到Dubbo相关问题,比如:“如何从0到1设计一个Dubbo的RPC框架”,这个问题主要考察以下几个方面: 你对RPC框架的底层原理掌握程度. ...
- Python学习笔记(三):文件和集合操作
python string与list互转 因为python的read和write方法的操作对象都是string.而操作二进制的时候会把string转换成list进行解析,解析后重新写入文件的时候,还得 ...
- Codeforces Round #462 (Div. 2) D. A Determined Cleanup
D. A Determined Cleanup time limit per test1 second memory limit per test256 megabytes Problem Descr ...
- 洛谷 P2205 [USACO13JAN]画栅栏
这题其实没什么,但用到的算法都十分有用.做一个不恰当的比喻,这是一只必须用牛刀杀的鸡,但因为我这个蒟蒻杀不死牛,所以只能找只鸡来练练手. 题目描述 Farmer John 想出了一个给牛棚旁的长围墙涂 ...
- vue时时监听input输入框中 输入内容 写法
Vue input 监听 使用 v-on:input="change" 实现即可 App.vue <template> <div> <md-field ...
- 电脑卡,eclipse Android stadio 卡,什么都卡解决方法
昨天还好好的,今天什么都没有动就很卡.Android stadio 半天,改了东西才编译.什么都慢一拍,你能感觉到,打开网页也好,什么也好. 莫名的问题,总是被莫名的解决.真的,下了个360杀毒,没效 ...
- 浅谈javascript的运行机制
积累一下这几天学的,记录一下: 一.为什么JavaScript是单线程? JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事.那么,为什么JavaScript不能有多个线程 ...