webRTC开发

开发步骤:

  1. 获取本地媒体(getUserMedia(),MediaStream API)
  2. 在浏览器和对等端(其它浏览器或终端)之间建立对等连接(RTCPeerConnection API)
    • RTCPeerConnection 接口代表一个由本地计算机到远端的 WebRTC 连接。该接口提供了创建,保持,监控,关闭连接的方法的实现。
    • RTCSessionDescription 接口描述连接或潜在连接的一端的配置方式。每一个RTCSessionDescription 由一个描述类型组成,该描述类型指示它所描述的请求/应答协商过程的SDP 协议的相关描述。RTCSessionDescription 在两个对等点之间协商连接的过程涉及来回交换对象,每个描述都表示描述的发送者支持的连接配置选项的一个组合。一旦两个对等方就连接的配置达成一致,协商就完成了。
  3. 将媒体和数据通道关联至该连接
  4. 交换会话描述(RTCSessionDescription)
    • 浏览器M从Web服务器请求网页
    • Web服务器向M返回带有WebRTC js的网页
    • 浏览器L从Web服务器请求网页
    • Web服务器向L返回带有WebRTC js的网页
    • M决定与L通信,通过M自身的js将M的会话描述对象(offer,提议)发送至Web服务器
    • Web服务器将M的会话描述对象发送至L上的js
    • L上的js将L的会话描述对象(answer,应答)发送至Web服务器
    • Web服务器转发应答至M上的js
    • M和L开始交互,确定访问对方的最佳方式
    • 完成后,M和L开始协商通信密钥
    • M和L开始交换语音、视频或数据

获取本地媒体(getUserMedia

<p>通过getUserMedia()获取视频</p>
<video id="video" autoplay palysinline></video>
<button id="videoButton" onclick="userOpt('video')">打开摄像头</button>

<p>通过getUserMedia()获取音频</p>
<audio id="audio" autoplay controls></audio>

<button id="audioButton" onclick="userOpt('audio')">打开麦克风</button>

<p>通过getUserMedia()获取音视频</p>
<!-- muted 静音 -->
<video id="allVideo" autoplay palysinline muted></video>
<button id="allButton" onclick="userOpt('all')">全部打开</button>
function userOpt(type) {
    const constraints = {
        audio: false,
        video: false
    }
    switch (type) {
        case 'audio':
            constraints.audio = true
        break
        case 'video':
            constraints.video = true
        case 'all':
            constraints.video = true
            constraints.audio = true
    }
    // 注意getUserMedia()在各浏览器中的区别
    // Opera --> getUserMedia
    // Chrome --> webkitGetUserMedia
    // Firefox --> mozGetUserMedia
    //navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
    navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
        handleSuccess(stream, type)
    }).catch(handleError)
}


function handleSuccess(stream, type) {
    type = type === 'all' ? 'allVideo' : type
    const ele = document.querySelector(`#${type}`)
    try {
        // Chrome浏览器
        ele.srcObject = stream
    } catch {
        // 这里是兼容写法
        let compatibleURL = window.URL || window.webkitURL;
        ele.src = compatibleURL.createObjectURL(stream);
    }
}

function handleError(error) {
    console.log('error ', error)

}

建立点对点的信道

WebRTC使用RTCPeerConnection建立连接传送流数据,在建立RTCPeerConnection实例之后,想要建立点对点的信道,需要做两件事:

1. 确定本机上的媒体流的特性,比如分辨率、编解码能力啥的(SDP描述符)
    - 通过offer和answer交换SDP描述符:
    - 甲和乙各自建立一个RTCPeerConnection实例pc1、pc2
    - 甲通过PC所提供的pc1.createOffer()方法建立一个包含甲的SDP描述符的offer信令
    - 甲通过PC所提供的pc1.setLocalDescription()方法,将甲的SDP描述符交给甲的PC实例
    - 甲将offer信令通过服务器发送给乙
    - 乙将甲的offer信令中所包含的的SDP描述符提取出来,通过PC所提供的pc2.setRemoteDescription()方法交给乙的PC实例
    - 乙通过PC所提供的pc2.createAnswer()方法建立一个包含乙的SDP描述符answer信令
    - 乙通过PC所提供的pc2.setLocalDescription()方法,将乙的SDP描述符交给乙的PC实例
    - 乙将answer信令通过服务器发送给甲
    - 甲接收到乙的answer信令后,将其中乙的SDP描述符提取出来,调用pc1.setRemoteDescripttion()方法交给甲自己的PC实例
    
2. 连接两端的主机的网络地址(ICE Candidate)
    ICE (交互式连接建立) 是一个被WebRTC使用的框架(跟其他技术在一起使用),它被用在两端之间的连接,不管是何种网络类型 (通常用在视频或语音聊天)。这个协议让两端能够互相找到对方并建立一个连接,即便它们都使用了网络地址转译 (NAT) 去跟内网的其他设备共享了一个公网 IP 地址。
    - 通过ICE框架建立NAT/防火墙穿越的连接来获得这个外界可以直接访问的地址,RTCPeerConnection在创立的时候可以将ICE服务器的地址传递进去,如:
const iceServer = {
    "iceServers": [{
        "url": "stun:stun.l.google.com:19302"
    }]
};
const pc = new RTCPeerConnection(iceServer);
  • 甲、乙各创建配置了ICE服务器的PC实例,并为其添加onicecandidate事件回调
  • 当网络候选可用时,将会调用onicecandidate函数
  • 在回调函数内部,甲或乙将网络候选的消息封装在ICE Candidate信令中,通过服务器中转,传递给对方
  • 甲或乙接收到对方通过服务器中转所发送过来ICE Candidate信令时,将其解析并获得网络候选,将其通过PC实例的addIceCandidate()方法加入到PC实例中
  • 连接创立完成,可以向RTCPeerConnection中通过addStream()加入流来传输媒体流数据

提议/应答协商

要在二者之间建立连接,必须在二者之间建立会话。offer/answer是一种“一次性通过”型协商机制。实际中该过程可能会反复多次。
WebRTC使用RTCSessionDescription对象表示提议和应答。每个浏览器都将生成一个该对象。
本地浏览器只关注两个特定的调用:

// 将我的会话描述告知我的浏览器
pc.setLocalDescription(mySessionDescription)
...
// 将对等端的会话描述告知我的浏览器
pc.setRemoteDescription(yourSessionDescription)

生成提议、应答:

// 生成提议
pc.createOffer(gotOffer, didntGetOffer)
 
function gotOffer(aSessionDescription) {
    setLocalDescription(aSessionDescription)
    ...
    // 现在可以将会话描述(提议offer)发送给对等端,以便对等端
    // a)、将提议传递给setRemoteDescription
    // b)、调用createAnswer
}
 
// 生成应答
pc.createAnswer(gotAnswer, didntGetAnswer)
 
function gotAnswer(aSessionDescription) {
    setLocalDescription(aSessionDescription)
    ...
    // 现在将会话描述(应答answer)发送给对等端,以便对等端
    // a)、将应答传递给setRemoteDescription
}

数据通道

RTCDataChannel,数据通道是浏览器之间建立的非媒体的交互连接。即不传递媒体消息,绕过服务器直接传递数据。相比WebSocket、http消息,数据通道支持流量大、延迟低。
注意: 单个对等连接中的多个数据通道底层共享一个流,所以只需一次offer、answer即可建立首个数据通道。之后再建立数据通道无需再次进行offer、answer交换。 典型应用:游戏实时状态更新。

pc = new RTCPeerConnection()
dc = pc.createDataChannel(‘')
// 一端创建完数据通道后,另一端只需要监听ondatachannel事件即可:
pc.ondatachannel = function(e) {
    dc = e.channel
    // 此时,两个对等端已经彼此建立数据通道,可以直接相互发送消息:
    dc.send('i am a text string for sending’)
    dc.onmessage = function(e) {
        console.log('收到消息:', e.data)
    }
}