音视频通信加餐 —— WebRTC一肝到底
作者:杨成功
简介:专注前端工程与架构产出
来源:SegmentFault 思否社区
最近需要搭建一个在线课堂的直播平台,考虑到清晰度和延迟性,我们一致认为使用 WebRTC 最合适。
原因有两点:首先是“点对点通信”非常吸引我们,不需要中间服务器,客户端直连,通信非常方便;再者是 WebRTC 浏览器原生支持,其他客户端支持也很好,不像传统直播用 flv.js 做兼容,可以实现标准统一。
然而令我非常尴尬的是,社区看了好几篇文章,理论架构写了一堆,但没一个能跑起来。WebRTC 里面概念很新也很多,理解它的通信流程才是最关键,这点恰恰很少有描述。
于是我就自己捣鼓吧。捣鼓了几天,可算是整明白了。下面我结合自己的实践经验,按照我理解的关键步骤,带大家从应用场景的角度认识这个厉害的朋友 —— WebRTC。
线上预览本地通信 Demo:https://example.ruims.top/local/
大纲预览
本文介绍的内容包括以下方面:
什么是 WebRTC? 获取媒体流 对等连接流程 本地模拟通信源码 局域网两端通信 一对多通信 我想学更多
什么是 WebRTC?
较高的延迟 清晰度难以保证
获取媒体流
var stream = await navigator.mediaDevices.getUserMedia()
var stream = await navigator.mediaDevices.getDisplayMedia()
域名是 localhost 协议是 https
let stream = await navigator.mediaDevices.getDisplayMedia({
audio: false,
video: true
})
var stream = await navigator.mediaDevices.getDisplayMedia({
audio: false,
video: {
width: 1920,
height: 1080
}
})
// 视频轨道
let videoTracks = stream.getVideoTracks()
// 音频轨道
let audioTracks = stream.getAudioTracks()
// 全部轨道
stream.getTracks()
const getNewStream = async () => {
var stream = new MediaStream()
let audio_stm = await navigator.mediaDevices.getUserMedia({
audio: true
})
let video_stm = await navigator.mediaDevices.getDisplayMedia({
video: true
})
audio_stm.getAudioTracks().map(row => stream.addTrack(row))
video_stm.getVideoTracks().map(row => stream.addTrack(row))
return stream
}
对等连接流程
第一步:创建连接实例
var peerA = new RTCPeerConnection()
var peerB = new RTCPeerConnection()
var stream = await navigator.mediaDevices.getUserMedia()
stream.getTracks().forEach(track => {
peerA.addTrack(track, stream)
})
peerB.ontrack = async event => {
let [ remoteStream ] = event.streams
console.log(remoteStream)
})
第二步:建立对等连接
var offer = await peerA.createOffer()
var answer = await peerB.createAnswer()
await peerB.setRemoteDescription(offer)
await peerB.setLocalDescription(answer)
await peerA.setRemoteDescription(answer)
await peerA.setLocalDescription(offer)
peerA.onicecandidate = event => {
if (event.candidate) {
peerB.addIceCandidate(event.candidate)
}
}
peerA.onconnectionstatechange = event => {
if (peerA.connectionState === 'connected') {
console.log('对等连接成功!')
}
if (peerA.connectionState === 'disconnected') {
console.log('连接已断开!')
}
}
本地模拟通信源码
上一步我们梳理了点对点通信的流程,其实主要代码也就这么多。这一步我们再把这些知识点串起来,简单实现一个本地模拟通信的 Demo,运行起来让大家看效果。
<div class="local-stream-page">
<video autoplay controls muted id="elA"></video>
<video autoplay controls muted id="elB"></video>
<button onclick="onStart()">播放</button>
</div>
var peerA = null
var peerB = null
var videoElA = document.getElementById('elA')
var videoElB = document.getElementById('elB')
const onStart = async () => {
try {
var stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: true
})
if (videoElA.current) {
videoElA.current.srcObject = stream // 在 video 标签上播放媒体流
}
peerInit(stream) // 初始化连接
} catch (error) {
console.log('error:', error)
}
}
const peerInit = stream => {
// 1. 创建连接实例
var peerA = new RTCPeerConnection()
var peerB = new RTCPeerConnection()
// 2. 添加视频流轨道
stream.getTracks().forEach(track => {
peerA.addTrack(track, stream)
})
// 添加 candidate
peerA.onicecandidate = event => {
if (event.candidate) {
peerB.addIceCandidate(event.candidate)
}
}
// 检测连接状态
peerA.onconnectionstatechange = event => {
if (peerA.connectionState === 'connected') {
console.log('对等连接成功!')
}
}
// 监听数据传来
peerB.ontrack = async event => {
const [remoteStream] = event.streams
videoElB.current.srcObject = remoteStream
}
// 互换sdp认证
transSDP()
}
const transSDP = async () => {
// 1. 创建 offer
let offer = await peerA.createOffer()
await peerB.setRemoteDescription(offer)
// 2. 创建 answer
let answer = await peerB.createAnswer()
await peerB.setLocalDescription(answer)
// 3. 发送端设置 SDP
await peerA.setLocalDescription(offer)
await peerA.setRemoteDescription(answer)
}
局域网两端通信
// peerA 端
let offer = await peerA.createOffer()
await peerA.setLocalDescription(offer)
await peerA.setRemoteDescription(answer)
// peerA 端
const transSDP = async () => {
let offer = await peerA.createOffer()
// 向 peerB 传输 offer
socketA.send({ type: 'offer', data: offer })
// 接收 peerB 传来的 answer
socketA.onmessage = async evt => {
let { type, data } = evt.data
if (type == 'answer') {
await peerA.setLocalDescription(offer)
await peerA.setRemoteDescription(data)
}
}
}
// peerA 端
peerA.onicecandidate = event => {
if (event.candidate) {
socketA.send({ type: 'candid', data: event.candidate })
}
}
// peerB 端,接收 peerA 传来的 offer
socketB.onmessage = async evt => {
let { type, data } = evt.data
if (type == 'offer') {
await peerB.setRemoteDescription(data)
let answer = await peerB.createAnswer()
await peerB.setLocalDescription(answer)
// 向 peerA 传输 answer
socketB.send({ type: 'answer', data: answer })
}
if (type == 'candid') {
peerB.addIceCandidate(data)
}
}
一对多通信
// 发起端
var offer = null
var Peers = [] // 连接实例数组
// 接收端请求连接,传来标识id
const newPeer = async id => {
// 1. 创建连接
let peer = new RTCPeerConnection()
// 2. 添加视频流轨道
stream.getTracks().forEach(track => {
peer.addTrack(track, stream)
})
// 3. 创建并传递 SDP
offer = await peerA.createOffer()
socketA.send({ type: 'offer', data: { id, offer } })
// 5. 保存连接
Peers.push({ id, peer })
}
// 监听接收端的信息
socketA.onmessage = async evt => {
let { type, data } = evt.data
// 接收端请求连接
if (type == 'join') {
newPeer(data)
}
if (type == 'answer') {
let index = Peers.findIndex(row => row.id == data.id)
if (index >= 0) {
await Peers[index].peer.setLocalDescription(offer)
await Peers[index].peer.setRemoteDescription(data.answer)
}
}
}
评论