当前位置:   article > 正文

WebRTC直播间搭建记录

WebRTC直播间搭建记录

考虑到后续增加平台直播的可能性,笔记记录一下WebRTC相关.

在这里插入图片描述

让我们分别分析两种情况下的WebRTC连接建立过程:

情况一:AB之间可以直接通信

1.信令交换:
设备A和设备B首先通过信令服务器交换SDP(Session Description Protocol)信息和候选者(candidates)。SDP包含有关会话的信息,包括设备的媒体能力和网络地址。

2.ICE框架协商:
设备A和设备B使用ICE框架收集本地候选者(本地网络地址),然后交换候选者信息,包括通过STUN服务器获取的公共IP地址和端口号。

3.连接建立:
根据ICE框架收集的候选者信息,设备A和设备B尝试直接建立点对点连接。根据候选者优先级排序,选择最佳的候选者进行连接。

4.直接通信:
如果ICE框架成功建立了连接,设备A和设备B之间可以直接进行实时通信,例如音频、视频或数据传输。

情况二:AB之间不能直接通信
1.信令交换:
设备A和设备B通过信令服务器交换SDP信息和候选者。

2.ICE框架协商:
设备A和设备B分别收集本地候选者,并将候选者信息发送给对方。

3.无法直接建立连接:
如果ICE框架无法直接建立连接(例如由于双方都位于NAT环境或防火墙后),则ICE框架会返回无法建立连接的错误或超时。

4.使用TURN服务器:
在无法直接建立连接的情况下,设备A和设备B将使用TURN服务器作为中继器来中转数据流。设备A和设备B分别连接到TURN服务器,并将数据流通过TURN服务器进行中转,从而实现对等通信。

什么情况下AB不能直接建立点对点连接?

双方位于不同的私有网络:如果设备A和设备B都处于不同的私有网络(例如家庭网络),则无法直接访问对方的局域网地址。

AB之间无法直接建立点对点连接的情况通常是由于网络地址转换(NAT)或防火墙的存在,导致设备无法直接接收对方发送的数据包。具体情况包括:

NAT类型限制:如果设备A或设备B位于对称NAT或受限NAT网络中,会导致UDP包的发送和接收出现问题,从而无法建立直接连接。

防火墙限制:防火墙可以阻止对UDP或特定端口的访问,这会影响设备之间的直接通信。

公共IP地址不可用:有些设备可能没有公共IP地址,而是通过NAT路由器共享局域网IP地址。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在这些情况下,WebRTC利用ICE框架和STUN/TURN服务器来实现网络穿透,确保设备之间可以建立可靠的实时通信连接。通过STUN服务器获取公共网络地址,通过TURN服务器进行数据中转,解决了设备间无法直接通信的问题。

附一下个人直播间搭建部分代码

<html>

<head>
    <title>简单直播间</title>
    <style type="text/css">
    body {
        background: #888888 center center no-repeat;
        color: white;
    }

    button {
        cursor: pointer;
        user-select: none;
    }

    .room {
        border: 1px solid black;
        cursor: pointer;
        user-select: none;
        text-align: center;
        background: rgba(0, 0, 0, 0.5);
    }

    video {
        width: 75%;
        border: 2px solid black;
        border-image: linear-gradient(#F80, #2ED) 20 20;
    }

    #chat {
        position: fixed;
        top: 5px;
        right: 5px;
        width: calc(25% - 30px);
        height: calc(75% - 10px);
        background: rgba(0, 0, 0, 0.5);
    }

    .content {
        height: calc(100% - 66px);
        margin-top: 5px;
        margin-bottom: 5px;
        overflow-y: scroll;
    }

    #chatSend {
        white-space: nowrap;
    }

    #chatSend>input {
        width: calc(100% - 90px);
    }

    #chatSend>button {
        width: 80px;
    }

    #chatTag {
        margin-left: 10px;
        line-height: 30px;
    }

    .chatKuang {
        border: 1px solid white;
        margin: 3px;
        border-radius: 5px;
    }

    .hintKuang {
        margin: 3px;
        text-align: center;
    }
    </style>
</head>

<body>
    <div id="create">
        名称:<input id="name" type="text" />
        <button onclick="createRoom()">创建直播间</button>
        <span id="count"></span>
    </div>
    <video id="localVideo" autoplay controls="controls"></video>
    <br />
    <div id="chat">
        <div id="chatTag">
            <button onclick="changeTag(0)">房间列表</button>
            <button onclick="changeTag(1)">房间聊天</button>
        </div>
        <div id="roomContent" class="content"></div>
        <div id="chatContent" class="content" style="display: none"></div>
        <div id="chatSend">
            <input type="text" />
            <button onclick="chatSend()">发送</button>
        </div>
    </div>
    <script type="text/javascript">
    // 背景图片
    (function() {
        var img = new Image();
        img.addEventListener("load", function() {
            document.querySelector("body").style.background =
                "url('" + this.src + "') center center no-repeat";
            let _img = this;
            calculateBackgroundImageScale(_img);
            window.onresize = function(_img) {
                calculateBackgroundImageScale(_img);
            }
        });

        img.src = "https://parva.cool/share/sky043.jpg";
    })();

    //计算背景图片缩放(自适应窗口大小)
    function calculateBackgroundImageScale(img) {
        let w1 = document.body.clientWidth;
        let h1 = document.body.clientHeight;
        let w2 = img.width;
        let h2 = img.height;
        let scale1 = w1 / w2;
        let scale2 = h1 / h2;
        let scale = scale1 > scale2 ? scale1 : scale2;
        document.querySelector("body").style.backgroundSize =
            Math.ceil(w2 * scale) + "px " + Math.ceil(h2 * scale) + "px";
    }

    // 存储本地媒体流
    var localStream;

    // 与服务器的websocket通信
    var socket = new WebSocket("wss://parva.cool/rtc");

    // 判断自己是否正在直播
    var isMe;

    // 当前的标签页(0:房间列表, 1:房间聊天)
    var tag = 0;

    // 发送聊天信息
    function chatSend() {
        let input = document.querySelector("#chatSend input");
        let msg = input.value;
        input.value = "";
        if (Object.keys(pcs).length == 0) return;
        if (isMe) {
            socket.send(JSON.stringify({ event: "chatSend", msg: msg }));
        } else {
            socket.send(JSON.stringify({
                event: "chatSend",
                msg: msg,
                roomName: Object.keys(pcs)[0]
            }));
        }
    }

    // 切换标签
    function changeTag(t) {
        tag = t;
        document.querySelector("#roomContent").style.display = "none";
        document.querySelector("#chatContent").style.display = "none";
        if (tag == 0) {
            document.querySelector("#roomContent").style.display = "block";
        } else if (tag == 1) {
            document.querySelector("#chatContent").style.display = "block";
        }
    }

    // 创建直播间
    function createRoom() {
        let roomName = document.querySelector("#name").value;
        if (!localStream) {
            navigator.mediaDevices.getDisplayMedia({ video: true, audio: true })
                .then((stream) => {
                    localStream = stream;
                    socket.send(JSON.stringify({
                        event: "createRoom",
                        roomName: roomName
                    }));
                });
        } else {
            socket.send(JSON.stringify({
                event: "createRoom",
                roomName: roomName
            }));
        }
    }

    // 关闭直播间
    function closeRoom() {
        socket.send(JSON.stringify({ event: "closeRoom" }));
        document.querySelector("input").disabled = false;
        document.querySelector("button").innerHTML = "创建直播间";
        document.querySelector("button").setAttribute("onclick",
            "createRoom()");
        document.querySelector("video").srcObject = null;
        pcs = {};
        let content = document.querySelector("#chatContent");
        content.innerHTML = "";
        changeTag(0);
        let count = document.querySelector("#count");
        count.innerHTML = "";
        localStream.getTracks()[0].stop();
    }

    // 进入直播间
    var 防止双击;
    var 防止双击setTimeout;

    function joinRoom(roomName) {
        if (防止双击 == roomName) return;
        else 防止双击 = roomName;
        clearTimeout(防止双击setTimeout);
        防止双击setTimeout = setTimeout(function() { 防止双击 = ""; }, 800);
        let name = document.querySelector("#name").value;
        socket.send(JSON.stringify({
            event: "joinRoom",
            name: name,
            roomName: roomName
        }));
    }

    // 接收服务器的消息
    socket.onmessage = function(event) {
        let json = JSON.parse(event.data);

        // 接收聊天信息
        if (json.event === "chat") {
            let chatName = document.createElement("span");
            chatName.innerHTML = json.name + " : ";
            chatName.setAttribute("class", "chatName");
            let chatMessage = document.createElement("span");
            chatMessage.innerHTML = json.msg;
            chatName.setAttribute("class", "chatMessage");
            let chatKuang = document.createElement("div");
            chatKuang.setAttribute("class", "chatKuang");
            chatKuang.appendChild(chatName);
            chatKuang.appendChild(chatMessage);
            let content = document.querySelector("#chatContent");
            content.appendChild(chatKuang);
            content.scrollTop = 9999999;
        }

        // 通知新人加入房间
        if (json.event === "joinHint") {
            let hintKuang = document.createElement("div");
            hintKuang.setAttribute("class", "hintKuang");
            hintKuang.innerHTML = json.name + "加入直播房间!"
            let content = document.querySelector("#chatContent");
            content.appendChild(hintKuang);
            content.scrollTop = 9999999;
        }

        // 有人退出当前房间
        if (json.event === "quitHint") {
            let hintKuang = document.createElement("div");
            hintKuang.setAttribute("class", "hintKuang");
            hintKuang.innerHTML = json.name + "退出房间.."
            let content = document.querySelector("#chatContent");
            content.appendChild(hintKuang);
            content.scrollTop = 9999999;
        }

        // 接收房间人数
        if (json.event === "count") {
            let count = document.querySelector("#count");
            count.innerHTML = "\t在场众神数量 : " + json.count;
        }

        // 所有房间的信息
        if (json.event === "roomsInfo") {
            if (tag != 0) return;
            let content = document.querySelector("#roomContent");
            content.innerHTML = "";
            for (let i = 0; i < json.info.length; i++) {
                let div = document.createElement("div");
                div.innerHTML = json.info[i];
               
                //点击进入房间事件捆绑
                div.setAttribute("onclick", "joinRoom('" + json.info[i] + "')");
                div.setAttribute("class", "room");
                content.appendChild(div);
            }
        }

        // 创建房间失败
        if (json.event === "createRoomFailed") {
            alert("创建房间失败,名称已存在 或 名称格式有误");
            localStream.getTracks()[0].stop();
        }

        // 创建房间成功
        if (json.event === "createRoomOk") {
            document.querySelector("input").disabled = true;
            document.querySelector("button").innerHTML = "关闭直播间";
            document.querySelector("button").setAttribute("onclick",
                "closeRoom()");
           
           //将捕捉的本地媒体流赋值到直播窗口
            document.querySelector("video").srcObject = localStream;
            document.querySelector("video").volume = 0;
            isMe = true;
            changeTag(1);
            let hintKuang = document.createElement("div");
            hintKuang.setAttribute("class", "hintKuang");
            hintKuang.innerHTML = "创建直播房间成功!"
            let content = document.querySelector("#chatContent");
            content.appendChild(hintKuang);
            content.scrollTop = 9999999;
        }

        // 加入房间失败
        if (json.event === "joinRoomFailed") {
            alert("加入房间失败,名称已存在 或 名称格式有误");
        }

        // 主播下播,退出房间
        if (json.event === "roomClosed") {
            document.querySelector("video").srcObject = null;
            pcs = {};
            let content = document.querySelector("#chatContent");
            content.innerHTML = "";
            changeTag(0);
            document.querySelector("input").disabled = false;
            let count = document.querySelector("#count");
            count.innerHTML = "";
        }

        // 有人欲加入我的直播间
        if (json.event === "joinRoom") {
            // 为对方创建一个pc实例,并发送offer给他
            var pc = createRTCPeerConnection(json.name);
            // rtc建立连接:创建一个offer给对方
            pc.createOffer(function(desc) {
                pc.setLocalDescription(desc);
                socket.send(JSON.stringify({
                    event: "_offer",
                    data: {
                        sdp: desc,
                        nameB: json.name
                    }
                }));
            }, function(error) {
                console.log("CreateOffer Failure callback: " + error);
            });
        }

        // rtc建立连接:接收到offer
        if (json.event === "_offer") {
            // 为对方创建一个pc实例,并发送offer给他
            var pc = createRTCPeerConnection(json.data.nameA);
            pc.setRemoteDescription(new RTCSessionDescription(json.data.sdp));
            // rtc建立连接:创建一个answer给对方
            pc.createAnswer(function(desc) {
                pc.setLocalDescription(desc);
                socket.send(JSON.stringify({
                    event: "_answer",
                    data: {
                        sdp: desc,
                        nameA: json.data.nameA
                    }
                }));
            }, function(error) {
                console.log("CreateAnswer Failure callback: " + error);
            });
        }

        // rtc建立连接:接收到answer   --A收到B的answer
        // 将来自对方的 Answer SDP 设置为本地 WebRTC 连接的远程描述,以便双方能够正确地理解和处理对方的媒体数据,从而建立成功的实时通信连接。
        if (json.event === "_answer") {

            pcs[json.data.nameB].setRemoteDescription(
                new RTCSessionDescription(json.data.sdp));
        }

        // rtc建立连接:接收到_ice_candidate
        if (json.event === "_ice_candidate") {
            pcs[json.data.from].addIceCandidate(
                new RTCIceCandidate(json.data.candidate));
        }
    }

    // Q:一对多直播情况下 直播用户A的远程描述需要怎么变化?
    // A:连接都是独立的 分别设置对应的远端描述符 A的描述符不变



    // 存储pc实例
    var pcs = {};

    // stun和turn服务器URL及配置
    var iceServer = {
        iceServers: [
            { urls: "stun:parva.cool:3478" },
            {
                urls: "turn:parva.cool:3478",
                username: "parva",
                credential: "Parva089"
            }
        ]
    };

    // 创建RTCPeerConnection实例
    function createRTCPeerConnection(name) {
        let pc = new RTCPeerConnection(iceServer);
        if (!isMe) {
            for (let n in pcs) pcs[n].close();
            pcs = {};
        }
        // 以{对方的名字:PC实例}键值对形式把PC实例存储起来
        pcs[name] = pc;
        if (localStream) pc.addStream(localStream);
        else pc.addStream(new MediaStream());

        pc.onicecandidate = function(event) {
            if (event.candidate !== null)
                socket.send(JSON.stringify({
                    event: "_ice_candidate",
                    data: {
                        candidate: event.candidate,
                        to: Object.keys(pcs).find(k => pcs[k] == pc)
                    }
                }));
        }

        pc.ontrack = function(event) {
            if (isMe) return;
            changeTag(1);
            document.querySelector("video").srcObject = event.streams[0];
            document.querySelector("input").disabled = true;
            let hintKuang = document.createElement("div");
            hintKuang.setAttribute("class", "hintKuang");
            hintKuang.innerHTML = "成功进入直播间!"
            let content = document.querySelector("#chatContent");
            content.innerHTML = "";
            content.appendChild(hintKuang);
            content.scrollTop = 9999999;
        }

        return pc;
    }
    </script>
</body>

</html>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308
  • 309
  • 310
  • 311
  • 312
  • 313
  • 314
  • 315
  • 316
  • 317
  • 318
  • 319
  • 320
  • 321
  • 322
  • 323
  • 324
  • 325
  • 326
  • 327
  • 328
  • 329
  • 330
  • 331
  • 332
  • 333
  • 334
  • 335
  • 336
  • 337
  • 338
  • 339
  • 340
  • 341
  • 342
  • 343
  • 344
  • 345
  • 346
  • 347
  • 348
  • 349
  • 350
  • 351
  • 352
  • 353
  • 354
  • 355
  • 356
  • 357
  • 358
  • 359
  • 360
  • 361
  • 362
  • 363
  • 364
  • 365
  • 366
  • 367
  • 368
  • 369
  • 370
  • 371
  • 372
  • 373
  • 374
  • 375
  • 376
  • 377
  • 378
  • 379
  • 380
  • 381
  • 382
  • 383
  • 384
  • 385
  • 386
  • 387
  • 388
  • 389
  • 390
  • 391
  • 392
  • 393
  • 394
  • 395
  • 396
  • 397
  • 398
  • 399
  • 400
  • 401
  • 402
  • 403
  • 404
  • 405
  • 406
  • 407
  • 408
  • 409
  • 410
  • 411
  • 412
  • 413
  • 414
  • 415
  • 416
  • 417
  • 418
  • 419
  • 420
  • 421
  • 422
  • 423
  • 424
  • 425
  • 426
  • 427
  • 428
  • 429
  • 430
  • 431
  • 432
  • 433
  • 434
  • 435
  • 436
  • 437
  • 438
  • 439
  • 440
  • 441
  • 442
  • 443

建立 BC 和 A 之间的 WebRTC 连接涉及以下步骤和流程:

前提条件

A 是直播主播,已经在直播间创建了媒体流并开始直播。
B 和 C 是观众,希望加入直播间并观看 A 的直播。
  • 1
  • 2

建立 WebRTC 连接的过程:
1.B 或 C 加入直播间:
B 或 C 在前端页面选择要加入的直播间并点击加入按钮。
前端通过 WebSocket 向服务器发送加入房间的请求,包括用户信息和房间名称。

2.服务器收到加入房间请求:
后端服务器接收到 B 或 C 的加入房间请求,将其添加到对应房间的用户列表中。

3.A 发送 WebRTC offer 给 B 或 C:
当 B 或 C成功加入房间后,后端服务器会通知 A(主播)有新用户加入了直播间。
A 在前端收到加入房间的通知后,使用 WebRTC 创建一个 PeerConnection 对象(pc),并生成一个 SDP offer。
A 将这个 SDP offer 通过 WebSocket 发送给服务器。

4.服务器转发 WebRTC offer 给 B 或 C:
后端服务器收到 A 发送的 SDP offer。
后端服务器将 SDP offer 转发给房间内除 A 之外的其他用户(即 B 或 C)。

5.B 或 C 接收 WebRTC offer:
B 或 C 前端收到 A 发送的 SDP offer。
B 或 C 使用 WebRTC 创建一个 PeerConnection 对象(pc),并设置 A 的 SDP offer 作为远端描述(setRemoteDescription)。

6.B 或 C 创建 WebRTC answer 给 A:
B 或 C 使用自己的本地媒体流(视频和音频)创建一个 SDP answer。
B 或 C 将这个 SDP answer 发送给服务器。

7.服务器转发 WebRTC answer 给 A:
后端服务器收到 B 或 C 发送的 SDP answer。
后端服务器将 SDP answer 转发给 A。

8.A 接收 WebRTC answer:
A 前端收到 B 或 C 发送的 SDP answer。
A 设置 B 或 C 的 SDP answer 作为远端描述(setRemoteDescription)。

9.ICE 候选者交换:
PeerConnection 开始收集和交换 ICE 候选者信息(网络地址、端口等)。

10.B、C 和 A 通过服务器交换 ICE 候选者信息,以便彼此建立直接的通信路径。
建立直播观看连接:

完成以上步骤后,B 或 C 和 A 之间的 WebRTC 连接就建立起来了。
B 或 C 的本地媒体流通过 ICE 候选者的协商直接传输到 A,A 的直播内容会被 B 或 C 观看。

总结:

通过以上步骤,B 或 C 可以加入 A 创建的直播间,并与 A 建立起 WebRTC 连接,实现实时的音视频传输和观看直播内容。整个过程涉及前端的用户交互和媒体流处理,以及后端的 WebRTC 信令传递和 ICE 候选者交换,共同实现了观众与主播之间的实时通信和直播功能。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/453676
推荐阅读
相关标签
  

闽ICP备14008679号