Hi,大家好,我是姜友华。
上一节我们通过WebSocket协议实现了一个聊天室。这一节我们将用WebRTC协议实现另一个聊天室,视频聊天室。
在开始之前,我们需要架设一个远程服务器,同时需要为它添加SSL支持。SSL我们可以使用Certbot来生成,Certbot的安装与使用在这里。
WebRTC协议实现的是终端间的连接,连接后端对端传输数据而不需要经过服务器。在建立连接时,我们可以使用WebSocket作为它们的信令服务器,用于传递它们之间建立连接所需要的数据。
主要内容:
- 使用WebRTC连接两端的步骤。
- 按步骤实现页面端的视频聊天室。
信令服务器
使用WebSocket作为它们的信令服务器,就拿上节一节我们实现了WebSocket来用。为了适配WebRTC,我们要作稍微的调整,即让发送端不接收自己的信息,以简化WebRTC连接的建立。
为此,我们需要处理的地有5个。
- 建立Message结构体。
/// client.go
......
type Message struct {
Client *Client
Content []byte
}
......
- 将Message结构体传hub。
/// client.go
......
func (c *Client) readPump() {
......
for {
......
message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1))
c.hub.broadcast <- &Message{c, message}
}
}
......
- 更改hub接收Message结构。
/// hub.go
type Hub struct {
......
broadcast chan *Message
......
}
func NewHub() *Hub {
return &Hub{
broadcast: make(chan *Message),
......
}
}
- 不发送给自己。
/// hub.go
func (h *Hub) Run() {
for {
......
case message := <-h.broadcast:
for client := range h.clients {
if message.Client == client {
continue
}
select {
case client.send <- message.Content:
default:
close(client.send)
delete(h.clients, client)
}
}
}
}
}
- 更改发送内容的最大限度。
实现WebRTC连接
一、 WebRTC 协议介绍。
MDN上WebRTC 协议介绍,你可以看看。其中主要涉及下面这5个协议,点击后进入到百度词条:
- ICE(Interactive Connectivity Establishment)
- NAT(Network Address Translation)
- STUN(Session Traversal Utilities for NAT)
- TURN(Traversal Using Relays around NAT)
- SDP(Session Description Protocol)
前4个协议的作用是,建立点对点的网络连接;第5个协议的作用是,确定连接之后的传播内容。
二、WebRTC建立点对点连接的步骤。
使用WebRTC可以建立点(Peer A发起者)对点(Beer B接收者)的连接。对于每一个单向联系,它的具体流程如下图所示: webRTC_画板 1.png本示例只演示两个客户端之间的视频通信。
由于每个客户端都需要将自己的视频发送出去,同时接收其它客户端发来的视频。所以每个客户端都需要扮演两个不同的角色:发送者和接收者。在这里,我们分别将它们命名为Local Connection
和Remote Conneciton
。 好,我们来对上图进行说明:
- Peer A创建一个
Local RTCPeerConnection
,我们称为ALC。 - Peer B创建一个
Remote RTCPeerConnection
,我们称为BRC。
Peer A.
- GetStream: Peer A通过
navigator.mediaDevices.getUserMedia()
捕捉本地媒体Stream。 - ALC调用
addTrack()
,添加Stream到发送轨道上。 - ALC调用
createOffer()
,来创建一个offer
(提议)。 - ALC调用
setLocalDescription()
将offer
设置为本地描述。 - 到了这里,ALC会引发onCandidate事件。
- onCandidate事件里,我们通过信令服务器发送
candidate
出门,信令服务器将它派送到Peer B 。 - ALC接着通过信令服务器将
offer
发送出门,以同样的方式派送到Peer B。 - ALC等待Peer B的回应,等Peer B的Answer。
- ALC调用
setRemoteDescription()
将answer
设置为远地描述。
Peer B.
- BRC的OnTrack在接收到媒体时被触发,事件带有媒体信息。
- BRC接收到Candidate时,添加到本地的IceCandidatek里。
- BRC接收到offer时,调用
setRemoteDescription()
将offer
设置为远地描述。 - BRC调用
createAnswer()
,创建一个answer
(应答) - BRC调用
setLocalDescription()
将answer
设置为本地描述. - BRC接着通过信令服务器将
answer
发送出门,信令服务器将它派送到Peer A 。
看完流程图我们再来看看实现的代码。我们将Peer A的LocalConnection与Peer B的RemoteConnection合在一起,即当前终端可以接发信息。
三、网页端的代码。
1. 静态页面的设计。
index.html
/// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebRTC</title>
<script src="./chat.js"></script>
<style>
video {
border: 5px solid black;
width: 320px;
height: 240px;
transform: rotateY(180deg);
}
button {
width: 150px
}
</style>
</head>
<body>
<h1>WebRTC</h1>
<div id="connectionInfo"></div>
<video id="localClient" playsinline autoplay muted></video>
<video id="remoteClient" playsinline autoplay></video>
<div>
<button id="testButton">Test WebSocket</button>
<button id="startButton">WebRTC Offer</button>
<button id="endButton">WebRTC Exit</button>
</div>
</body>
</html>
就是并排着两个视频显示区:左边为本地的,右边为远地的。
2. 页面里WebRTC的实现。
- 先看代码
/// chat.js
window.onload = function () {
// 判断是否支持WebSocket,不支持则退。
if (!window["WebSocket"]) {
console.log('Does not support websocket.')
return
}
let configuration = {} // { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }
let startButton = document.getElementById("startButton")
let localClient = document.getElementById("localClient")
let remoteClient = document.getElementById("remoteClient")
/** WebSocket **/
// 建立WebSocket连接。
let ws = new WebSocket("wss://" + document.location.host + "/ws")
// 关闭连接。
ws.onclose = function (event) {
console.log('Connection closed.')
}
// 接收信息。
ws.onmessage = function (event) {
let msg = JSON.parse(event.data)
if (!msg) {
return console.log('WebSocket.onmessage is error')
}
switch (msg.key) {
case 'offer':
return receivedOffer(msg.data)
case 'answer':
return receivedAnswer(msg.data)
case 'candidate':
return receivedCandidate(msg.data)
default:
connectionInfo.innerText = msg.data
}
}
// 发送信息。
function wsSend(key, data) {
ws.send(JSON.stringify({ key: key, data: data }))
}
/** WebRTC **/
let localConnection = new RTCPeerConnection(configuration)
let remoteConnection = new RTCPeerConnection(configuration)
/** Get Stream **/
navigator.mediaDevices.getUserMedia({ video: true, audio: false }).then(stream => {
localClient.srcObject = stream
stream.getTracks().forEach(track => { localConnection.addTrack(track, stream) })
}).catch(error => {
console
})
// 开始 WebRTC。
startButton.addEventListener('click', function (e) {
localConnection.createOffer().then(offer => {
localConnection.setLocalDescription(offer)
wsSend('offer', offer)
}).catch(error => {
console.log("startButton.click pc.createOffer: " + error)
})
})
/** RTCPeerConnection Event **/
localConnection.onicecandidate = event => {
wsSend('candidate', event.candidate)
}
remoteConnection.ontrack = event => {
if (remoteClient.srcObject === event.streams[0]) {
return
}
remoteClient.srcObject = event.streams[0]
}
/** Received From WebSocket */
function receivedCandidate(data) {
remoteConnection.addIceCandidate(new RTCIceCandidate(data))
}
function receivedOffer(data) {
remoteConnection.setRemoteDescription(data)
remoteConnection.createAnswer().then(answer => {
remoteConnection.setLocalDescription(answer)
wsSend('answer', answer)
}).catch(error => {
console.log("ReceivedOffer pc.createAnswer: " + error)
})
}
function receivedAnswer(data) {
localConnection.setRemoteDescription(data)
}
}
- 一开始是定义了两个用来接收本地、远地视频的元素:localClient、remoteClient。
- 然后是实现WebSocket,作为WebRTC的信令服务器。
- WebSocket接收信息分四类处理:offer, answer, candidate, 其它。
- 能发送的信息也是这四类。
- 定义了两个连接:localConnection、remoteConnection。
- 获取本地视频并添加到localConnection中,同时显示在本地元素localClient里。
- 用户决定开始创建并发送offer,并设置为本地描述。
- localConnection有两个响事件:onicecandidate、ontrack;ontrack收到视频后显示在远地元素remoteClient里。
- 对信令服务信息的处理。
这是iPhone Chrome的显示效果,在macOS Chrome显示出错,所以本代码未完成浏览器兼容,请你留意。
好,就到这里。我是姜友华,下一次,再见。
网友评论