前段时间一直在忙一个基于WebRTC的PC和移动端双向视频的项目。第一次接触webRTC,不免遇到了许多问题,好比:webRTC移动端兼容性检测,如何配置MediaStreamConstraints, 信令(iceCandidate, sessionDescription)传输方式的选择,iceCandidate和sessionDescription设置的前后顺序,STUN和TURN的概念,如何实现截图及录制视频及上传图片和视频功能,如何高效跟踪错误等等。好记性不如烂笔头,特写此文以记之。web
对PC端来讲,webRTC早已被各大浏览器支持了,Chrome 28,FF22,Edge...随着不久以前发布的IOS11也宣布支持webRTC及getUserMedia,webRTC在移动端的应用前景也使人憧憬。canvas
具体到实际项目中,通过测试,各大国产安卓手机自带的浏览器基本不支持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,就是navigator.mediaDevices.getUserMedia(constraints)传入的constraints,至于它的写法及功能,参考MDN,本文不作赘述。我在这里想要强调的是,对于移动端来讲控制好视频图像的大小是很重要的,例如本项目中想要对方的图像占据全屏,这不只是改变video元素的样式或者属性能作到的,首先要作的是改变MediaStreamConstraints中的视频分辨率(width, height),使其长宽比例大体与移动端屏幕的相似,而后再将video元素的长和宽设置为容器的长和宽(例如100%)。服务器
另外对于getUserMedia必定要捕获可能出现的错误,若是是老的API,设置onErr回调,若是是新的(navigator.mediaDevices.getUserMedia),则catch异常。这样作的缘由:getUserMedia每每不会彻底符合咱们的预期,有时即便设置的是ideal的约束,仍然会报错,若是不追踪错误,每每一脸懵逼。这也是后文要提到的高效追踪错误的方法之一。微信
要传输的信令包括两个部分:sessionDescription和iceCandidate。为了便于传输可将其处理成字符串,另外一端接收时还原并用对应的构造函数构造对应的实例便可。websocket
webRTC并无规定信令的传输方式,而是彻底由开发者自定义。常见的方式有短轮询、webSocket(socket.io等),短轮询的优势无非是简单,兼容性强,但在并发量较大时,服务器负荷会很重。而webSocket就不存在这个问题,但webSocket搭建起来较为复杂,并非全部的浏览器都支持websocket。综合来讲socket.io是个不错的解决方案,事件机制和自带的房间概念对撮合视频会话都是自然有利的,而且当浏览器不支持websocket时能够切换为轮询,也解决了兼容性的问题。网络
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指定要传输的视频流session
stream.getTracks().forEach((track) => { pc.addTrack(track, stream); });
复制代码
发起方经过createOffer生成localDescription并传给pc.setLocalDescription(),pc获取了本地的sdp后开始获取candidate,这里的candidate指的是网络信息(ip、端口、协议),根据优先级从高到低分为三类:
三者之中只须要有一类链接成功便可,因此若是通讯双方在同一内网,不配置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
};
复制代码
由于webRTC标准还在不断地更新中,因此相关的API常常会有改动。
另外,对video元素也要特殊处理。设置autoPlay属性,对播放本地视频源的video还要设置muted属性以去除回音。针对IOS播放视频自动全屏的特性,还要设置playsinline属性的值为true。