WebRTC 学习笔记 (二)

/ 技术文章 / 0 条评论 / 1540浏览

WebRTC 学习笔记 (二)

前言: 距离上一次看WebRTC相关内容已经过去一年,没想到和上次看的内容兼职天差地别。随着WebRTC协议自身的不断更新,原来用的那些工具和接口纷纷停止维护或者不能使用。只能感叹现在的技术发展是在太快了。最近正好又有兴趣,再一次研究一下WebRTC,正好再回顾一下,把原来的信息更新补充一下。

WebRTC 全称是: Web browser Real Time Communication。

最初是谷歌2010年以6820万美元收购Global IP Solutions(GIPS)公司而获得的一项技术,它使得Web中的实时通讯成为可能,是一项能够在浏览器内部进行实时音频和视频通信的技术。

当浏览器实现对应音视频组件后,开发者可以容易地通过JS API 实现他们自己的RTC web 应用。

现在已经被推为W3C的标准,名称为WebRTC,是现阶段Html5无插件多媒体通信的唯一手段。

相关网站

WebRTC相关API介绍

功能划分

  1. 获取音频和视频数据
  2. 传输音频和视频数据
  3. 传输任意二进制数据

API划分:三个JS接口

  1. MediaStream (又叫getUserMedia)
  2. RTCPeerConnection (C++)
  3. RTCDataChannel

好用的库

拓扑结构

虽然WebRTC号称支持点对点通信,当然做的也很好,但是在真实的生产环境中,鉴于多媒体交流的复杂度,仍旧需要一些服务器帮助视频通话能够应付复杂的网络环境、多浏览器间的兼容问题,以及视频编解码问题

topology

** 注意:上述Signaling,STUN, TURN均需安装在公网**

Signaling 信令服务

可以使用一下现成的信令服务器,为了兼容Windows和Linux,可以选择基于NodeJS的信令服务

这里用 rtcmulticonnection-server为例

  1. 安装NodeJS, 当前稳定版本为10.x
    curl -sL https://rpm.nodesource.com/setup_10.x | sudo bash -
    sudo yum install nodejs
    # 安装时,输入y同意安装
    # 安装完成后检查版本
    npm --version
  1. 安装依赖
    npm install rtcmulticonnection-server
  1. 服务端代码 server.js
    // http://127.0.0.1:30010
    // http://localhost:30010

    var server = require('http'),
        url = require('url'),
        path = require('path'),
        fs = require('fs');


    var config = JSON.parse(fs.readFileSync('./config.json', 'utf8'));


    var RTCMultiConnectionServer = require('rtcmulticonnection-server');
    var ioServer = require('socket.io');
    var app = server.createServer();

    RTCMultiConnectionServer.beforeHttpListen(app, config);
    app = app.listen(process.env.PORT || 30010, process.env.IP || "0.0.0.0", function() {
        RTCMultiConnectionServer.afterHttpListen(app, config);
    });

    // --------------------------
    // socket.io codes goes below

    ioServer(app).on('connection', function(socket) {
        RTCMultiConnectionServer.addSocket(socket, config);

        // ----------------------
        // 自定义socket事件

        const params = socket.handshake.query;

        if (!params.socketCustomEvent) {
            params.socketCustomEvent = 'custom-message';
        }

        socket.on(params.socketCustomEvent, function(message) {
            socket.broadcast.emit(params.socketCustomEvent, message);
        });
    });
  1. 配置(可选)config.json
  1. 启动服务
    node server.js
  1. 测试 访问 http://localhost:30010/socket.io/, 返回以下信息表明信令服务搭建成功
    {"code":0,"message":"Transport unknown"}

ICE 框架

多数联网设备都位于局域网中, 并位于防火墙后面, 设备本身只有一个内网的私有IP, 在与外部通信时, 会经过1个或多个NAT路由器, 最终得到一个最外端的一个外部IP, 然后与远端目标机器通讯. 这一网络结构对于web应用, c/s型应用等来说不是问题, 但对于VoIP/P2P等应用就是一个问题了. 通信双方并不知道自己或对方的outermost的外网IP:Port, 如何建立直连呢?

这时就需要NAT穿透, 目前WebRTC采用的是ICE框架 (ICE+STUN+TURN), ICE也适用于非WebRTC应用, 这是目前业界用于穿透NAT的标准方案。

    yum install libevent-devel
    wget http://turnserver.open-sys.org/downloads/v4.5.0.8/turnserver-4.5.0.8.tar.gz
    tar -xzvf turnserver-4.5.0.8.tar.gz
    cd turnserver-4.5.0.8
    ./configure
    make
    sudo make install
    # 添加PATH环境变量
    vi ~/.bashrc
    export PATH=/opt/webrtc/coturn/turnserver-4.5.0.8/bin:$PATH
    :x
    source ~/.bashrc
  1. 创建用户,在默认sqllite中创建用户,用户名webrtcuser,密码d2VicnRjdXNlcg,域(realm)my-domain.com
    turnadmin -a -u webrtcuser -r my-domain.com -p d2VicnRjdXNlcg
    
        openssl生成自签名证书,默认使用 `/usr/local/etc/turn_server_cert.pem` `/usr/local/etc/turn_server_pkey.pem`
    
    openssl req -x509 -newkey rsa:2048 -keyout /usr/local/etc/turn_server_pkey.pem -out /usr/local/etc/turn_server_cert.pem -days 99999 -nodes
  1. 修改配置,在Coturn编译完成好之后会自动生成一个配置文件的模板,在/usr/local/etc/turnserver.conf.default,这里我们在新建一个新的配置文件
    cp /usr/local/etc/turnserver.conf.default /usr/local/etc/turnserver.conf
    vi /usr/local/etc/turnserver.conf

    relay-device=eth0  //外网网卡的设备号
    listening-ip=172.19.149.87 //内网IP,没有填外网IP也可以
    listening-port=3478 
    tls-listening-port=5349
    relay-ip=172.19.149.87  //内网IP,没有填外网IP也可以
    external-ip=101.132.121.80/172.19.149.87  //外网IP/内网IP
    relay-threads=500 
    lt-cred-mech 
    pidfile=”/var/run/turnserver.pid” 
    min-port=49152 
    max-port=65535 
    user=webrtcuser:d2VicnRjdXNlcg
    realm=my-domain.com
  1. 运行服务
    turnserver -o -a -f
  1. 测试

可以使用下面的网址测试coTURN服务器是否生效 https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/

注意:

主要代码

这里只例举视频聊天室的实现,更多如录屏、共享页面等可参考https://rtcmulticonnection.herokuapp.com/demos/ 中更多代码

依赖js库


    <!-- 视频会话处理核心库 -->
    <script src="https://rtcmulticonnection.herokuapp.com/dist/RTCMultiConnection.min.js"></script>
    <!-- socket.io 客户端 -->
    <script src="https://rtcmulticonnection.herokuapp.com/socket.io/socket.io.js"></script>
    <!-- 屏蔽浏览器厂商差异 -->
    <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>


    <!-- 自定义样式布局 -->
    <link rel="stylesheet" href="/static/js/getHTMLMediaElement.css">
    <script src="/static/js/getHTMLMediaElement.js"></script>

初始化connection


    // ......................................................
    // ..................RTCMultiConnection Code.............
    // ......................................................
    var connection = new RTCMultiConnection();

    // 信令服务socket.io URL,默认认为页面和信令服务在同一台服务器上
    connection.socketURL = '/';

    // 如果使用自己搭建的信令服务器,可以这样
    // connection.socketURL = 'https://rtcmulticonnection.herokuapp.com:443/';

    // 点对点寻址建链事件
    connection.socketMessageEvent = 'video-conference-demo';

    // 开启视频会话或音频会话
    connection.session = {
        audio: true,
        video: true
    };

    // SDP协议设置,点对点流媒体是否发送音视频
    connection.sdpConstraints.mandatory = {
        OfferToReceiveAudio: true,
        OfferToReceiveVideo: true
    };

    // 配置 STUN TURN 服务URL
    // https://www.rtcmulticonnection.org/docs/iceServers/
    // use your own TURN-server here!
    connection.iceServers = [];

    // second step, set STUN url
    connection.iceServers.push({
        urls: 'stun:101.132.121.80:3478'
    });

    // last step, set TURN url (recommended)
    connection.iceServers.push({
        urls: 'turns:101.132.121.80:5349',
        credential: 'd2VicnRjdXNlcg',
        username: 'webrtcuser'
    });
    connection.iceServers.push({
        urls: 'turn:101.132.121.80:3478?transport=tcp',
        credential: 'd2VicnRjdXNlcg',
        username: 'webrtcuser'
    });

    // 公共会话查询ID,
    // 如果不设置该参数,所有会话默认为私有会话,无法被第三方查询
    // 设置publicRoomIdentifier后,可通过connection.socket.emit('get-public-rooms', connection.publicRoomIdentifier, callback)查询所有可用会话
    connection.publicRoomIdentifier = 'webrtc-public-sessions'
    connection.videosContainer = document.getElementById('videos-container');

    // 重中之重,链路接通回调函数
    connection.onstream = function(event) {
        var existing = document.getElementById(event.streamid);
        if(existing && existing.parentNode) {
          existing.parentNode.removeChild(existing);
        }

        // 初始化视频元素
        event.mediaElement.removeAttribute('src');
        event.mediaElement.removeAttribute('srcObject');
        event.mediaElement.muted = true;
        event.mediaElement.volume = 0;

        // 创建html 视频元素,控制器,进度条
        var video = document.createElement('video');

        try {
            video.setAttributeNode(document.createAttribute('autoplay'));
            video.setAttributeNode(document.createAttribute('playsinline'));
        } catch (e) {
            video.setAttribute('autoplay', true);
            video.setAttribute('playsinline', true);
        }

        // 本地视频静音,避免哨音
        if(event.type === 'local') {
          video.volume = 0;
          try {
              video.setAttributeNode(document.createAttribute('muted'));
          } catch (e) {
              video.setAttribute('muted', true);
          }
        }
        // 将视频流加入新创建的页面视频元素
        video.srcObject = event.stream;

        // 自定义视频样式
        var width = parseInt(connection.videosContainer.clientWidth / 3) - 20;
        var mediaElement = getHTMLMediaElement(video, {
            title: event.userid,
            buttons: ['full-screen'],
            width: width,
            showOnMouseEnter: false
        });

        // 将视频元素添加至页面指定位置
        connection.videosContainer.appendChild(mediaElement);
        
        // 5棉猴开始播放视频(需等待视频初始化完成)
        setTimeout(function() {
            mediaElement.media.play();
        }, 5000);

        mediaElement.id = event.streamid;

        // to keep room-id in cache
        localStorage.setItem(connection.socketMessageEvent, connection.sessionid);

        // 所有成员全部离开会话后,刷新页面
        if(event.type === 'local') {
          connection.socket.on('disconnect', function() {
            if(!connection.getAllParticipants().length) {
              location.reload();
            }
          });
        }
    };

    // 将离线的会话视频元素从页面移除
    connection.onstreamended = function(event) {
        var mediaElement = document.getElementById(event.streamid);
        if (mediaElement) {
            mediaElement.parentNode.removeChild(mediaElement);
        }
    };

    // 有可能找不到外置麦克风,尝试重新加入会话
    connection.onMediaError = function(e) {
        if (e.message === 'Concurrent mic process limit.') {
            if (DetectRTC.audioInputDevices.length <= 1) {
                alert('Please select external microphone. Check github issue number 483.');
                return;
            }

            var secondaryMic = DetectRTC.audioInputDevices[1].deviceId;
            connection.mediaConstraints.audio = {
                deviceId: secondaryMic
            };

            connection.join(connection.sessionid);
        }
    };

创建会话

        // 获取会话ID,用户识别号(均需全局唯一)
        connection.userid = document.getElementById('user-mobile').value
        connection.open(document.getElementById('room-id').value, function(isRoomOpened, roomid, error) {
            if(isRoomOpened === true) {
              console.log('Open connection success, sessionid: ',connection.sessionid);
            }
            else {
              disableInputButtons(true);
              if(error === 'Room not available') {
                alert('Someone already created this room. Please either join or create a separate room.');
                return;
              }
              alert(error);
            }
        });

加入会话

        connection.userid = document.getElementById('user-mobile').value
        connection.join(document.getElementById('room-id').value, function(isJoinedRoom, roomid, error) {
          if (error) {
                disableInputButtons(true);
                if(error === 'Room not available') {
                  alert('This room does not exist. Please either create it or wait for moderator to enter in the room.');
                  return;
                }
                alert(error);
            }
        });

参考: