美文网首页
从零实现直播:又是聊天室

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

作者: Jiangyouhua | 来源:发表于2021-09-02 20:33 被阅读0次

    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接收者)的连接。对于每一个单向联系,它的具体流程如下图所示: webRTC_画板 1.png

    由于每个客户端都需要将自己的视频发送出去,同时接收其它客户端发来的视频。所以每个客户端都需要扮演两个不同的角色:发送者和接收者。在这里,我们分别将它们命名为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. 对信令服务信息的处理。
    运行后显示如下: IMG_3193.JPG

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

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

    相关文章

      网友评论

          本文标题:从零实现直播:又是聊天室

          本文链接:https://www.haomeiwen.com/subject/vyyuiltx.html