媒体协商技术
WebRTC通过ICE技术可以实现客户端P2P连接,但是在两端进行P2P连接前还需要先进行媒体协商。WebRTC媒体协商技术主要包括两部分:一部分是SDP(Session Description Protocol)协议,另一部分是JSEP(JavaScript Session Establishment Protocol)协议。 >
SDP协议
1 SDP协议,即会话描述协议,它提供一种描述实时音视频通信所需的各种参数的通用描述,是一个文本信息。简单来说,SDP就是客户端的各端的音视频实时通信的能力,能力指的是各端所支持的音视频编解码能力及相关参数、传输协议和音视频媒体类型等等。
具体信息参见webrtc网络基础
SEP协议
1 2 3 4 5 6 7 8 9 SEP协议,即JavaScript会话建立协议,是一个信令控制协议。在媒体协商的过程中,JSEP协议通过“Offer/Answer”媒体协商机制,承载着遵循SDP协议的信息进行数据交互。JSEP数据一般使用JSON数据结构表示,比如使用“type”字段表示JSEP类型,“sdp”字段承载 SDP 文本数据,结合RTCPeerConnection API 的媒体协商流程如下: 1.客户端A创建RTCPeerConnection对象,然后调用offerForConstraints函数创建 SDP Offer信息,并通过setLocalDescription函数设置 RTCPeerConnection 的本地会话描述; 2.客户端A将SDP Offer信息发送给信令服务器,由信令服务器转发给客户端B; 3.客户端B收到客户端A的SDP Offer 后,通过调用setRemoteDescription函数保存客户端A的SDP; 4.客户端B创建RTCPeerConnection对象,然后调用answerForConstraints函数创建SDP Answer信息,并通过setLocalDescription函数设置 RTCPeerConnection 的本地会话描述; 5.客户端B将SDP Answer信息发送给信令服务器,由信令服务器转发给客户端A; 6.客户端A收到客户端B的SDP Answer后,通过调用setRemoteDescription函数保存客户端A的SDP; 至此,客户端A和客户端B都拿到双方的SDP信息,然后双方就可以通过ICE技术正式进行P2P连接。
一对一通信流程
首先A是发起端也就是呼叫端,呼叫端要与信令服务器建立连接,被呼叫端B端也要与信令服务器建立连接,这样他们就可以经过信令服务器对信令消息进行中转。
A如果想要发起呼叫,首先它要创建一个PeerConnect(对端的连接对象)之后通过getUserMedia拿到本地的音视频流,将这个流添加到连接里去。在进行媒体协商之前,我们需要先将流(本地采集的数据)添加到peerConnection连接中去。这样在媒体协商之前,我们才知道有哪些媒体数据。
如果先做媒体协商的话,知道这是连接中没有数据媒体流,就不会设置相关底层的接收器、发送器,即使后面设置了媒体流,传递给了peerConnection,他也不会进行媒体传输,所以我们要先添加流。
调用PeerConnect的CreateOffer的方法去创建一个Offer的SDP,创建好SDP之后再调用setLocalDescription,把它设置到LocalDescription这个槽里去,那调用完这个方法之后底层会发送一个bind请求给stun和turn服务,此时它就开始收集所有与对方连接的候选者。(还没收集完成,因为stun和turn服务还没有进行响应)
与此同时调用完setLocalDescription之后,之前CreateOffer方法拿到这个SDP也要发送给信令服务器,通过信令服务器的中转,最终转给B,此时B就拿到了offer,即A这端的媒体相关的描述信息。
B端收到这个SDP之后,首先创建一个PeerConnetion。创建好这个对象之后它会调用setRemoteDescription将这个收到的SDP设置进去,设置完成之后它要给一个应答。它要调用Create Answer,这时候它就产生了本机相关的媒体的信息也就是Answer SDP,创建好之后它也要调用setLocalDescription,将这个本地的Answer SDP设置进去,这样对B来说它的协商就OK了。也就是说它有远端的SDP同时它自己这端的SDP也获取到了,这时候在底层就会进行协商。
对于B端,在setLocalDescription的时候它也要向stun和turn服务发送一个bind请求,收集能够与A进行通信的所有的候选者,在调用setLocalDescription之后,B端将自己的Answer SDP发送给信令服务器 ,通过信令服务器转给A,A此时就拿到了B 这一端的媒体描述信息,然后它再设置setRemoteDescription,此时A也可以进行媒体协商了,此时A和B进行媒体协商的动作就算完成了。这是媒体协商这一部分。
那接下来stun和turn服务将这个信息回给A,此时就会触发A端的onIceCandidate事件,因为我们上面是有一个请求(3中出现的),所有此时我们就能收到很多不同的onIceCandidate,A收到候选者之后它将候选者发送给信令服务器,通过信令服务器转给对端,也就是让对端知道我都有哪些通路(让B端知道本机A有哪些通路),对端B收到这个Candidate之后要调用AddIceCandidate这个方法将它添加到对端的连接通路的候选者列表中。
同样的道理,当B收到这个Candidate之后,它也发给信令,通过信令转发给A,此时A也拿到B的所有的候选者,并将它添加到这个候选者列表中,也就是AddIceCandidate,那此时双方就拿到了所有的对方的可以互通的候选者,此时它底层就会做连接检测。
当它找到一个最优的线路之后呢,A与B就进行通讯,首先是A将数据流发送给B,B在收到这个数据流之后,因为它们前面已经做了绑定,就知道是谁来的数据,之后就与它的这个Connection进行对连,B虽然收到数据但是还是显示不出来,要将这个数据进行onAddStream,添加进行之后才能把这个视频数据和音频数据向上抛,才能进行视频的渲染和音频的渲染。
总结:
媒体的协商:看A端有什么媒体能力看B端有什么媒体能力,他们之间所有的媒体取一个交集,取大家都能够识别的支持的能力,包括音频编解码视频编解码,这个采样率是多少,帧率是多少,以及网络的一些信息;
通过ICE对整个可连通的链路地址进行收集,收集完成之后进行排序和连接检测,找出双方可以连接的最优的这条线路;
媒体数据的传输:当从一端传输到另一端之后呢,另一端会收到一个事件,就是onAddStream,当收到这个事件之后就可以将这个媒体流添加到自己的video标签和audio标签中进行音频的播放和视频的渲染,这个就是整个端对端连接的基本流程。
一对一通信示例
代码示例
1. html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta http-equiv ="X-UA-Compatible" content ="IE=edge" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > Document</title > </head > <style > video { width : 400px ; height : 400px ; } </style > <body > <video id ="localvideo" autoplay > </video > <video id ="remotevideo" autoplay > </video > <div > <button id ="start" > Start</button > <button id ="call" > Call</button > <button id ="hangup" > Hangup</button > </div > <script src ="https://webrtc.github.io/adapter/adapter-latest.js" > </script > <script src ="./index.js" > </script > </body > </html >
2. js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 let localVideo = document .querySelector ('#localvideo' );let remoteVideo = document .querySelector ('#remotevideo' );let start = document .querySelector ('#start' );let call = document .querySelector ('#call' );let hangup = document .querySelector ('#hangup' );let localStream,pc1,pc2;function getStream (stream ) { localVideo.srcObject = stream; localStream = stream }function handleError (err ) { console .error (err.name + ':' + err.message ); }function getRemoteStream (e ) { remoteVideo.srcObject = e.streams [0 ] }function getOffer (desc ) { console .log ('pc2' ); console .dir (desc.sdp ); pc1.setLocalDescription (desc) pc2.setRemoteDescription (desc) pc2.createAnswer ().then (getAnswer).catch (handleAnswerError) }function getAnswer (desc ) { pc2.setLocalDescription (desc) pc1.setRemoteDescription (desc) }function handleOfferError (err ) { console .log ('创建offer失败' +err); }function handleAnswerError (err ) { console .log ('创建answer失败' +err); } start.onclick = ()=> { if (navigator.mediaDevices && navigator.mediaDevices .getUserMedia ){ navigator.mediaDevices .getUserMedia ({ video :true }).then (getStream).catch (handleError) }else { console .error ('浏览器不支持getUserMedia' ); } } call.onclick = ()=> { pc1 = new RTCPeerConnection (); pc2 = new RTCPeerConnection (); pc2.addIceCandidate () pc1.onicecandidate = (e )=> { pc2.addIceCandidate (e.candidate ) } pc2.onicecandidate = (e )=> { pc1.addIceCandidate (e.candidate ) } pc2.ontrack = getRemoteStream localStream.getTracks ().forEach (track => { pc1.addTrack (track,localStream) }); pc1.createOffer ({ offerToReceiveVideo :1 , offerToReceiveAudio :0 }).then (getOffer).catch (handleOfferError) } hangup.onclick = ()=> { pc1.close (); pc2.close (); pc1 = pc2 = null ; }
1v1视频通话
1v1视频通话实例