Skip to content
基于 WebRtc 的简单聊天室
Branch: master
Clone or download
Latest commit 9cf92fa May 28, 2017
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
build INIT May 18, 2017
config build May 18, 2017
dist Build May 19, 2017
src add readme May 19, 2017
static
.babelrc
.editorconfig INIT May 18, 2017
.gitignore .gitignore update May 18, 2017
.postcssrc.js INIT May 18, 2017
PPT.md add readme May 19, 2017
README.md Add url May 28, 2017
index.html INIT May 18, 2017
package.json 简单功能构建 May 18, 2017
yarn.lock 简单功能构建 May 18, 2017

README.md

基于 WebRtc 搭建一个简单的视屏聊天室

github pages 自定义域名的 https 还没配置,chrome 强制要求 WebRtc 上 https 的,这个把栗子挂 coding 了

试一波 https://github.com/kinglisky/echat

只做成了简单的两人视频,输入同样的房间号进入同一个房间就行了,野狗或者网络的原因不成功可以换个房间试试 😁

WebRtc

维基的定义:WebRTC,名称源自网页即时通信(英语:Web Real-Time Communication)的缩写,是一个支持网页浏览器进行实时语音对话或视频对话的API。它于2011年6月1日开源并在Google、Mozilla、Opera支持下被纳入万维网联盟的W3C推荐标准

简单来说让浏览器提供JS的即时通信接口,可通过信令交换建立一个浏览器与浏览器之间(peer-to-peer)的信道,而不经过服务器直接实现浏览器之间的点对点通讯,高效的稳定的数据传输。

主要的三个 API

MediaStream:通过 MediaStream 的 API 能够通过设备的摄像头及话筒获得视频、音频的同步流

RTCPeerConnection:RTCPeerConnection 是 WebRTC 用于构建点对点之间稳定、高效的流传输的组件

RTCDataChannel:RTCDataChannel 使得浏览器之间(点对点)建立一个高吞吐量、低延时的信道,可传输任意数据

MediaStream 提供了调用媒体设备的功能

getUserMedia({
  video: true,
  audio: true
}, localMediaStream => {
  const video = document.getElementById('video')
  video.src = window.URL.createObjectURL(localMediaStream)
  video.onloadedmetadata = function (e) {
    console.log('Label: ' + localMediaStream.label)
    console.log('AudioTracks', localMediaStream.getAudioTracks())
    console.log('VideoTracks', localMediaStream.getVideoTracks())
  }
}, function (e) {
  console.log('Reeeejected!', e)
})

获取配置的配体流数据,可通过 URL.createObjectURL(stream) 将媒体流数据输入到 video 上,P.S view 要加上 autoplay 否则不会自动播放

可通过监听 video.onloadedmetadata 事件,获取数据流的一些信息

getUserMedia 处理可以配置 video audio 还可以配置其他一些东西如

  • 视频流的分辨率 maxWidth minHeight
  • 视频流的最小宽高比 MinAspectRatio
  • 视频流的最大帧速率 MaxFramerate
  • ......

RTCPeerConnection

WebRTC 使用 RTCPeerConnection 来在浏览器之间传递流数据,WebRTC 使用 RTCPeerConnection 来在浏览器之间传递流数据,这个流数据通道是点对点的,不需要经过服务器进行中转。

嗯,此处敲一下黑板,但我们还是不能抛弃能抛弃服务器的,我们仍然需要它来为我们传递信令(signaling)来建立这个信道。WebRTC 没有定义用于建立信道的信令的协议:信令并不是 RTCPeerConnection API的一部分

通过 RTCPeerConnection 建立点对点链接还是有点麻烦的......

看个小栗子

// 使用Google的 stun 服务器
const iceServer = {
  'iceServers': [{
    'url': 'stun:stun.l.google.com:19302'
  }]
}
// 与后台服务器的 WebSocket 连接
const socket = io()
// 创建PeerConnection实例
const pc = new PeerConnection(iceServer)
// 发送ICE候选到其他客户端
pc.onicecandidate = function (event) {
  socket.send(JSON.stringify({
    'event': '__ice_candidate',
    'data': {
      'candidate': event.candidate
    }
  }))
}
// 如果检测到媒体流连接到本地,将其绑定到一个 video 标签上输出
pc.onaddstream = function (event) {
  someVideoElement.src = URL.createObjectURL(event.stream)
}
// 获取本地的媒体流,并绑定到一个video标签上输出,并且发送这个媒体流给其他客户端
getUserMedia({
  'audio': true,
  'video': true
}, stream => {
    // 发送offer和answer的函数,发送本地session描述
  const sendOfferFn = function (desc) {
      pc.setLocalDescription(desc)
      socket.send(JSON.stringify({
        'event': '__offer',
        'data': {
          'sdp': desc
        }
      }))
    }
const sendAnswerFn = function (desc) {
      pc.setLocalDescription(desc)
      socket.send(JSON.stringify({
        'event': '__answer',
        'data': {
          'sdp': desc
        }
      }))
    }
  // 绑定本地媒体流到video标签用于输出
  myselfVideoElement.src = URL.createObjectURL(stream)
  // 向PeerConnection中加入需要发送的流
  pc.addStream(stream)
  // 如果是发送方则发送一个offer信令,否则发送一个answer信令
  if (isCaller) {
    pc.createOffer(sendOfferFn)
  } else {
    pc.createAnswer(sendAnswerFn)
  }
}, errHandler)
// 处理到来的信令
socket.onmessage = function (event) {
  const json = JSON.parse(event.data)
  // 如果是一个ICE的候选,则将其加入到 PeerConnection 中
  // 否则设定对方的 session 描述为传递过来的描述
  if (json.event === '__ice_candidate') {
    pc.addIceCandidate(new RTCIceCandidate(json.data.candidate))
  } else {
    pc.setRemoteDescription(new RTCSessionDescription(json.data.sdp))
  }
}

WebRTC 通过 RTCPeerConnection 建立点对点链接,最主要的是两点:

  • 信令交换
  • NAT/防火墙穿越

信令交换主要是交换三类信息:

  • 连接控制信息:初始化或者关闭连接报告错误。
  • 网络配置:对于外网,我们电脑的 IP 地址和端口?
  • 多媒体数据:使用什么编码解码器,浏览器可以处理什么信息?

信令的主要数据分为两部分:

  • 会话描述协议(Session Description Protocol)确定本机上的媒体流的特性,比如分辨率、编解码能力。
  • 连接两端的主机的网络地址,NAT/防火墙穿越(ICE Candidate)

下面讲一下最最主要的信令交换:

首先是 SDP 的交换:

信息交换

下面有 A 和 B 两个浏览器,它们交换信令的过程大概是:

  • A 和 B 各自创建自己的 RTCPeerConnection 对象,简称PC。
  • A 通过 PC 所提供的 createOffer() 方法建立一个包含 A 的 SDP 描述符的 offer 信令
  • A 通过 PC 所提供的 setLocalDescription() 方法,将 A 的 SDP 描述符交给 A 的 PC 实例
  • A 将信令经过服务器发给B
  • B 将 A 的 offer 信令中所包含的的 SDP 描述符提取出来,通过 PC 所提供的 setRemoteDescription() 方法交给 B 的 PC 实例
  • B 通过 PC 所提供的 createAnswer() 方法建立一个包含 B 的 SDP 描述符 answer 信令
  • B 通过 PC 所提供的 setLocalDescription() 方法,将 B 的 SDP 描述符交给 B 的 PC 实例
  • B 将 answer 信令通过服务器发送给 A
  • A 接收到 B 的 answer 信令后,将其中 B 的 SDP 描述符提取出来,调用 setRemoteDescripttion() 方法交给A自己的 PC 实例

简单来描述就是:

  • A 创建 offer。
  • A ——– 发送offer ——–> B
  • B 接收 A 的 offer 并设置,并由 B 创建 anwer。
  • B ——– 发送 anwer ——–> A
  • A 接收 B 的 anwer 并设置

通过在这一系列的信令交换之后,A 和 B 所创建的 PC 实例都包含 A 和 B 的 SDP 描述符了。我们还需要交换两端主机的网络地址,即 ICE Candidate 的交换

ICE 的交换其实发生在 A 和 B 的 SDP 描述符交换期间。简单来说:

  • A 创建完 PC 实例后并为其添加 onicecandidate 事件回调。
  • 当 A 网络候选可用时,将会调用 onicecandidate 函数,回调函数中包含 A 的 ICE 描述。
  • A 通过中转拂去其发送 ICE 给 B
  • B 接收并调用 PC 的 addIceCandidate() 将 A 的 ICE 描述符加入,从而获取到A的网络地址。

ICE 交换只需要一方含有另一方的 ICE 描述就行,不需要和 SDP 交换一样需要相互交换。

看个栗子吧!

会话描述协议 SDP 大概长这个样子

v=0
o=- 6375483060215944758 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE audio video
a=msid-semantic: WMS d4d74bdc-e8fc-4164-b3ae-3b0297f8753a
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:UFws
a=ice-pwd:z/R+qm/srgbLR4yHEMzewIO/
a=fingerprint:sha-256 C0:0A:70:6A:3C:3A:02:93:23:DD:3E:F3:9F:EC:A3:C9:9F:4C:55:5A:C5:5D:B2:EB:C0:F1:FE:E5:DE:A3:E7:F5
a=setup:actpass
a=mid:audio
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=sendrecv
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:1772214208 cname:wR/OHdsj6KVnPajs
a=ssrc:1772214208 msid:d4d74bdc-e8fc-4164-b3ae-3b0297f8753a 8fdf8579-2db4-4c80-b123-e8ed1e2bedfa
a=ssrc:1772214208 mslabel:d4d74bdc-e8fc-4164-b3ae-3b0297f8753a
a=ssrc:1772214208 label:8fdf8579-2db4-4c80-b123-e8ed1e2bedfa
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:UFws
a=ice-pwd:z/R+qm/srgbLR4yHEMzewIO/
a=fingerprint:sha-256 C0:0A:70:6A:3C:3A:02:93:23:DD:3E:F3:9F:EC:A3:C9:9F:4C:55:5A:C5:5D:B2:EB:C0:F1:FE:E5:DE:A3:E7:F5
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=sendrecv
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 1741418740 3539687233
a=ssrc:1741418740 cname:wR/OHdsj6KVnPajs
a=ssrc:1741418740 msid:d4d74bdc-e8fc-4164-b3ae-3b0297f8753a c9156452-3788-43b4-8d56-f0bf6af8e624
a=ssrc:1741418740 mslabel:d4d74bdc-e8fc-4164-b3ae-3b0297f8753a
a=ssrc:1741418740 label:c9156452-3788-43b4-8d56-f0bf6af8e624
a=ssrc:3539687233 cname:wR/OHdsj6KVnPajs
a=ssrc:3539687233 msid:d4d74bdc-e8fc-4164-b3ae-3b0297f8753a c9156452-3788-43b4-8d56-f0bf6af8e624
a=ssrc:3539687233 mslabel:d4d74bdc-e8fc-4164-b3ae-3b0297f8753a
a=ssrc:3539687233 label:c9156452-3788-43b4-8d56-f0bf6af8e624

一大堆各种描述数据,有兴趣的可以去官方手册查查都什么意思 通过 SDP 可以确定本机上的媒体流的特性,比如分辨率、编解码能力等

另一个就是 ICE Candidate,它大体就是这个样子:

candidate:2134056857 1 udp 2122260223 10.12.36.13 59024 typ host generation 0 ufrag NAac network-id 2

candidate:1184379673 1 tcp 1518214911 10.12.77.20 9 typ host tcptype active generation 0 ufrag YdMb network-id 1 network-cost 10

获取浏览所处的网络环境信息,ICE Candidate 交换其实就是浏览器之间的 NAT 穿越。在处于使用了 NAT 设备的私有TCP/IP网络中的主机之间需要建立连接时需要使用 NAT 穿越技术。

NAT 穿越技术

ICE,全名叫交互式连接建立(Interactive Connectivity Establishment),一种综合性的 NAT 穿越技术,它是一种框架,可以整合各种 NAT 穿越技术如 STUN、TURN(Traversal Using Relay NAT 中继NAT实现的穿透)

这里使用的的是 Google 的 STUN 服务器,国内野狗也有一个同样的,地址是 stun:cn1-stun.wilddog.com:3478

RTCDataChannel

DataChannel 是建立在 PeerConnection 上的,不能单独使用,它主要用于建立一个高吞吐量、低延时的信道,可用于传输任意数据, 例如我们要开发一个浏览器之间传数据的 App 这个就很有用了,它可以用来传输几乎所有客户端获取到所有数据。

DataChannel使用方式几乎和WebSocket一样,有几个事件:

  • onopen
  • onclose
  • onmessage
  • onerror

同时它有几个状态,可以通过 readyState 获取:

  • connecting: 浏览器之间正在试图建立 channel
  • open:建立成功,可以使用 send 方法发送数据了
  • closing:浏览器正在关闭 channel
  • closed:channel已经被关闭了

两个暴露的方法:

  • close(): 用于关闭 channel
  • send():用于通过 channel 向对方发送数据

这次聊天室应用没有用到,但它用处是非常大的。

通过野狗建立后端中转服务

现在 firebase 这么方便,就不用自己在搭建 websocket 服务器转发各种信令数据了,这里用的国内的野狗服务。

数据结构大概是酱紫的:

data0

data1

data2

用户进入的房间号是 room,room 下面随机生成 userid,userid 底下的 mailbox 存放各种交换信息

简单的中转就监听,给同一个 room 下的其他用户(other userid)发送链接信息,然后其他用户响应返回给自己(userid),信心存放在 mailbox 下面,看看野狗文档大概就懂了,和直接用 socket 转发信息有点不一样。

参考资料

You can’t perform that action at this time.