从零实现直播:又是聊天室

Hi,大家好,我是姜友华。
上一节我们通过WebSocket协议实现了一个聊天室。这一节我们将用WebRTC协议实现另一个聊天室,视频聊天室。

在开始之前,我们需要架设一个远程服务器,同时需要为它添加SSL支持。SSL我们可以使用Certbot来生成,Certbot的安装与使用在这里。

WebRTC协议实现的是终端间的连接,连接后端对端传输数据而不需要经过服务器。在建立连接时,我们可以使用WebSocket作为它们的信令服务器,用于传递它们之间建立连接所需要的数据。

主要内容:

  • 使用WebRTC连接两端的步骤。
  • 按步骤实现页面端的视频聊天室。

信令服务器

使用WebSocket作为它们的信令服务器,就拿上节一节我们实现了WebSocket来用。为了适配WebRTC,我们要作稍微的调整,即让发送端不接收自己的信息,以简化WebRTC连接的建立。
为此,我们需要处理的地有5个。

  1. 建立Message结构体。

/// client.go
......
type Message struct {
    Client  *Client
    Content []byte
}
......

  1. 将Message结构体传hub。

/// client.go
......
func (c *Client) readPump() {
    ......
    for {
        ......
        message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1))
        c.hub.broadcast <- &Message{c, message}
    }
}
......

  1. 更改hub接收Message结构。

/// hub.go
type Hub struct {
        ......
    broadcast chan *Message
        ......
}

func NewHub() *Hub {
    return &Hub{
        broadcast:  make(chan *Message),
                ......
    }
}

  1. 不发送给自己。

/// 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)
                }
            }
        }
    }
}

  1. 更改发送内容的最大限度。

实现WebRTC连接

一、 WebRTC 协议介绍。

MDN上WebRTC 协议介绍,你可以看看。其中主要涉及下面这5个协议,点击后进入到百度词条:

  1. ICE(Interactive Connectivity Establishment)
  2. NAT(Network Address Translation)
  3. STUN(Session Traversal Utilities for NAT)
  4. TURN(Traversal Using Relays around NAT)
  5. SDP(Session Description Protocol)

前4个协议的作用是,建立点对点的网络连接;第5个协议的作用是,确定连接之后的传播内容。

二、WebRTC建立点对点连接的步骤。

本示例只演示两个客户端之间的视频通信。

使用WebRTC可以建立点(Peer A发起者)对点(Beer B接收者)的连接。对于每一个单向联系,它的具体流程如下图所示:

从零实现直播:又是聊天室

由于每个客户端都需要将自己的视频发送出去,同时接收其它客户端发来的视频。所以每个客户端都需要扮演两个不同的角色:发送者和接收者。在这里,我们分别将它们命名为Local ConnectionRemote Conneciton。 好,我们来对上图进行说明:

  • Peer A创建一个Local RTCPeerConnection ,我们称为ALC。
  • Peer B创建一个Remote RTCPeerConnection ,我们称为BRC。

Peer A.

  1. GetStream: Peer A通过 navigator.mediaDevices.getUserMedia() 捕捉本地媒体Stream。
  2. ALC调用 addTrack(),添加Stream到发送轨道上。
  3. ALC调用 createOffer(),来创建一个offer(提议)。
  4. ALC调用 setLocalDescription()offer设置为本地描述。
  5. 到了这里,ALC会引发onCandidate事件。
  6. onCandidate事件里,我们通过信令服务器发送candidate出门,信令服务器将它派送到Peer B 。
  7. ALC接着通过信令服务器将offer发送出门,以同样的方式派送到Peer B。
  8. ALC等待Peer B的回应,等Peer B的Answer。
  9. ALC调用setRemoteDescription()answer设置为远地描述。

Peer B.

  1. BRC的OnTrack在接收到媒体时被触发,事件带有媒体信息。
  2. BRC接收到Candidate时,添加到本地的IceCandidatek里。
  3. BRC接收到offer时,调用 setRemoteDescription()offer设置为远地描述。
  4. BRC调用createAnswer(),创建一个answer(应答)
  5. BRC调用 setLocalDescription()answer设置为本地描述.
  6. 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)
    }
}

  1. 一开始是定义了两个用来接收本地、远地视频的元素:localClient、remoteClient。
  2. 然后是实现WebSocket,作为WebRTC的信令服务器。
  3. WebSocket接收信息分四类处理:offer, answer, candidate, 其它。
  4. 能发送的信息也是这四类。
  5. 定义了两个连接:localConnection、remoteConnection。
  6. 获取本地视频并添加到localConnection中,同时显示在本地元素localClient里。
  7. 用户决定开始创建并发送offer,并设置为本地描述。
  8. localConnection有两个响事件:onicecandidate、ontrack;ontrack收到视频后显示在远地元素remoteClient里。
  9. 对信令服务信息的处理。

运行后显示如下:

从零实现直播:又是聊天室

这是iPhone Chrome的显示效果,在macOS Chrome显示出错,所以本代码未完成浏览器兼容,请你留意。

好,就到这里。我是姜友华,下一次,再见。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容