web rtc 学习笔记(一)

创作人 Leo


编辑时间 Wed Nov 23,2022 at 15:03


Web Real Time Communication

webrtc web 实时通讯,可以进行点对点的实时通讯,包括实时数据传输(文字聊天),实时视频(在线视频会议),实时语音(多人语音聊天)

涉及知识:

  1. webrtc API 参考
  2. web 音频 API 参考 WEB Audio
  3. web 获取多媒体设备连接 API
  4. P2P 连接内网穿透,NAT 网络技术
  5. 信令与视频通话
  6. 优化 - SFU 服务器

涉及软件:

  1. P2P 连接打洞服务 coturn 搭建
  2. peerjs WEBRTC 前端(js)+后端(nodejs)库
  3. Golang WEBRTC 库

搭建 coturn 服务

stun 和 turn 协议用来保证两个端能够建立连接
再其中一端无法进行打洞的情况下,turn 服务也会负责数据转发,生产环境建议根据用户量选择带宽和负载均衡策略

  1. 首先安装 Ubuntu 18.04

  2. 安装 coturn

$ sudo apt-get -y update 
$ sudo apt-get -y install coturn

安装后的指令介绍
turnserver 启动 STUN/TURN server instance
turnadmin 界面管理后台
turnutils_peer UDP-only echo server 检测连接使用
turnutils_stunclient 呼叫 STUN server 并获取回应
turnutils_uclient 模拟多人连线 TURN server 并获取回应

  1. 启动 turnserver

首先,服务器需要开放以下端口

3478: UDP+TCP TURN Server 接收 request 的 port
5394: UDP+TCP TURN Server 接收 TLS request 的 port
49152-65536: UDP+TCP 實際連線的 Socket Port range

然后就可以执行命令启动了

sudo turnserver -v -o –lt-cred-mech –user hello:world –realm {turnserver 域名} –external-ip {外网ip}/{需要绑定到的本机网卡ip,内网ip}

例:
sudo turnserver -v -o –lt-cred-mech –user hello:world –realm x.xx.xxx –external-ip 30.1.2.3172.1.2.3

用户名 hello
密码 world
-o 代表后台启动

安装后还需要配置 DNS
将 turnserver 域名绑定到该服务器
A 记录: x.xx.xxx 30.1.2.3

以上都做完,可以通过这个网站测试连通性 测试地址
测试结果能显示 IP 即表式成功了
测试结果

搭建 peerjs server 信令服务

peerjs 提供了一个云 server,也可以自己搭建私有的信令服务

  1. 安装&启动
$ npm install peer -g
$ mkdir peer_js
$ cd peer_js/
$ nohup peerjs --port 10002 --path /peerjs > peerjs-0214.log &
  1. 修改 nginx 配置
map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}

server {
    listen 443 ssl;
    server_name peerjs.xxxxxxxx.xxx;
    access_log /data/logs/nginx/peerjs.xxxxxxxx.xxx;

    ssl_certificate /data/local/nginx/conf/sslkey/xxxxxxxx.xxx.pem;
    ssl_certificate_key /data/local/nginx/conf/sslkey/xxxxxxxx.xxx.key;

    location / {

        proxy_pass http://127.0.0.1:10002 ;

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
}

搭建一个分房间的服务(可选)

这个服务比较简单,主要使用websocket

消息示例:

  1. 加入房间请求,输入一个用户标志 UserFlag 和房间号 RoomID,服务端判断没有房间自动创建,并将用户添加到房间
  2. 房间用户列表响应,当用户加入房间后,将该房间用户列表推送给该用户

基于房间的多人语音聊天前端代码

<!DOCTYPE HTML>
<html>

<head>
    <meta charset=utf-8>
    <title>ws test</title>
</head>

<body>
    <div id="connect">
        <button id="do-connect">连接</button>
    </div>

    <div id="join-room">
        <input id="user-flag" placeholder="随便输入个用户名">
        <input id="room-id" placeholder="输入一个房间ID">
        <button id="do-join">加入房间</button>
    </div>

    <div id="user-list">

    </div>

    <div id="camerabox">
        <audio id="local_video" autoplay muted controls></audio>
        <!-- other connections -->
    </div>

    <script src="jquery.js" type="text/javascript"></script>
    <script src="ConnWs.js" type="text/javascript"></script>
    <script src="https://unpkg.com/peerjs@1.3.1/dist/peerjs.min.js"></script>
    <script>

        var server_host = window.location.hostname;
        var wsObj = null;
        var myWebcamStream = null;
        var mediaConstraints = { audio: true };
        var roomUserList = []; //{"username"=>mediaConnection}
        var myUserFlag = "";

        var myPeer = null;

        function composeUserFlag(v) {
            return v;
        }

        function callStreamFn(uflag, stream) {
            // `stream` is the MediaStream of the remote peer.
            // Here you'd add it to an HTML video/canvas element.
            let audioItem = document.createElement("audio");
            audioItem.autoplay = true;
            audioItem.controls = true;
            audioItem.id = "audio-" + uflag
            document.getElementById("camerabox").appendChild(audioItem);

            audioItem.srcObject = stream;
        }

        function NewLocalPeer(uflag) {
            return new Promise((resole, reject) => {
                myPeer = new Peer(composeUserFlag(uflag), {
                    host: 'peerjs.xxxxxxxx.xxx',
                    path: '/peerjs',
                    config: {
                        'iceServers': [
                            { urls: 'stun:turn.xxxxxxxx.xxx:3478' },
                            {
                                urls: "turn:turn.xxxxxxxx.xxx:3478",  // A TURN server
                                username: "hello",
                                credential: "world"
                            }
                        ]
                    }
                });
                
                myPeer.on('open', function (id) {
                    console.log('My peer ID is: ' + id);
                    resole(true); // waiting until peer open
                });
                
                myPeer.on('call', function (mediaConnection) {
                    mediaConnection.on('stream', function (stream) {
                        // `stream` is the MediaStream of the remote peer.
                        // Here you'd add it to an HTML video/canvas element.
                        console.log(`RECV call.peer ${mediaConnection.peer}`);
                        callStreamFn(mediaConnection.peer, stream);
                    });

                    // Answer the call, providing our mediaStream
                    mediaConnection.answer(myWebcamStream);
                });
            });
        }

        $(document).ready(function () {

            // 加入房间
            $("#do-join").click(async function () {
                if (wsObj == null) {
                    alert("websocket didn't init");
                    return;
                }

                myUserFlag = $("#user-flag").val();
                let roomId = $("#room-id").val();

                // 开启本地语音
                try {
                    myWebcamStream = await navigator.mediaDevices.getUserMedia(mediaConstraints);
                } catch (err) {
                    console.error(err);
                    return;
                }
                document.getElementById("local_video").srcObject = myWebcamStream;

                // 连接到 peer
                await NewLocalPeer(myUserFlag);
                console.log("local peer connected");

                let msg = {
                    user_flag: myUserFlag,
                    room_id: roomId
                };

                wsObj.sendMSG("VC_join_room", msg);
            });

            function respError(data) {
                console.error(`remote resp error msg: code(${data.code}) ${data.msg}`);
            }

            // 更新房间用户列表
            function respRoomUsers(data) {
                let ihtml = "用户列表:<br>";
                for (let i in data.userlist) {
                    let userFlag = data.userlist[i];
                    ihtml += userFlag + "<br>"

                    if (userFlag != myUserFlag) {
                        // 连接
                        console.log(`will add user to room list: ${userFlag}`)

                        roomUserList[userFlag] = myPeer.call(composeUserFlag(userFlag), myWebcamStream); // roomUserList[userFlag] = mediaConnection
                        roomUserList[userFlag].on('stream', function (stream) {
                            // `stream` is the MediaStream of the remote peer.
                            // Here you'd add it to an HTML video/canvas element.
                            callStreamFn(userFlag, stream);
                        });
                    }
                }
                $('#user-list').html(ihtml);
            }

            /**
             * 路由收到的服务器响应数据
             * @param msgid 双字节无符号整数,消息id,0-65535
             * @param content 消息正文
             */
            function dealAction(msgid, content) {
                console.log("deal action data ", msgid, content);

                switch (msgid) {
                    case 'error':
                        respError(content);
                        break;
                    case 'VC_room_userlist':
                        respRoomUsers(content);
                        break;
                    default:
                        console.log("unsupported action:", msgid);
                        break;
                }
            }

            function updateUUID(uuid) {
                $("#ws-host-space").html(`已经连接到:${wsObj.serverHost}`);
            }

            $("#do-connect").click(function (e) {
                wsHost = `wss://${server_host}` // wss://roomser.xxxxxxxx.xxx/connect

                console.log("will connect to ", wsHost);

                wsObj = new ConnWs(wsHost, dealAction, updateUUID);
            });

        });
    </script>
</body>

</html>

设计思路:

  1. 首先用户连接到房间服务器,加入一个房间;同时连接到本地语音设备,连接并将用户标志注册到 peerjs 信令服务器
  2. 收到服务器推送的房间用户列表,通过 peerjs 信令服务器主动连接房间其他人
  3. 收到信令服务器的连接 offer,会触发 peerjs 的 on call 回调,开启一个 audio 给新连接

其他信息:

  1. AudioContext 可以将多个 media stream 进行混合,可以将多个audio标签合并成一个全局audio标签(未验证)

阅读:2355
搜索
  • Linux 高性能网络编程库 Libevent 简介和示例 2578
  • web rtc 学习笔记(一) 2354
  • Mac系统编译PHP7【20190929更新】 2304
  • react 学习笔记(一) 2245
  • zksync 和 layer2 2237
  • Hadoop 高可用集群搭建 (Hadoop HA) 2182
  • Hadoop Map Reduce 案例:好友推荐 2151
  • 小白鼠问题 2085
  • Linux 常用命令 2082
  • 安徽黄山游 2058
简介
不定期分享软件开发经验,生活经验