webrtc基础知识

1,020 阅读11分钟

什么是 WebRTC ?

WebRTC是“网络实时通信”(Web Real Time Communication)的缩写。它最初是为了解决浏览器上视频通话而提出的,即两个浏览器之间直接进行视频和音频的通信,不经过服务器。后来发展到除了音频和视频,还可以传输文字和其他数据,是一种免费开源的实时通信技术,集成了音视频采集、编解码、流媒体传输、渲染等功能,并在Native代码基础上,封装了简单的JavaScript api,仅通过几行代码即可实现点对点通信,且具有良好的跨平台特性,目前主流的浏览器都已支持。

WebRTC主要让浏览器具备三个作用。

  • 获取音频和视频流 MediaStream
  • 进行音频和视频通信 RTCPeerConnection
  • 进行任意数据的通信 RTCDataChannel

MediaStream浏览器兼容问题可通过引用 webrtc.github.io/adapter/ada… 解决

WebRTC 的通信至少需要两种服务配合:

信令阶段需要双向通信服务辅助信息交换。 STUN,TURN辅助实现 NAT 穿越。

基本概念

  • SDP: 即会话描述协议,主要保存当前会话的媒体和传输信息,其中媒体信息包括媒体类型、传输协议、媒体格式等,传输信息包括媒体的远程地址信息、带宽等;它由多行kv格式的文本信息组成,具体可参考这里。(tools.ietf.org/pdf/draft-n…
  • Offer: 通信的发起方对自己的sdp描述
  • Answer: 通信的接收方对自己的sdp描述 信令: 协调发起方、接收方通信的数据信息,其中包括sdp描述信息、会话控制信息(节点加入、退出及各类的业务控制信息等)、网络信息、错误信息等。

webrtc通信

基于webrtc的点对点音视频通信示意图如下图所示:

其具体的流程如下:

  • 客户端A初始化本地音视频设备,创建一个用于offer的SDP对象,其中SDP对象中保存当前音视频的相关信息;
  • 客户端A通过信令服务器将SDP信息发送给客户端B;
  • 客户端B在接收到客户端A发送的SDP信息并保存后,初始化本地音视频设备并创建用于answer的SDP对象;
  • 客户端B通过信令服务器将SDP信息发送给客户端A;
  • 客户端A、B通过交换SDP等信息,建立P2P通道进行音视频传输;

Webrtc ICE(交互式连接建立)

在现实世界中,客户端A、B大部分位于NAT之后,只具有一个内网访问的私有ip,不足以提供足够的信息来建立一条端对端的连接。为了克服由NAT产生的网络问题,一般情况下,webrtc采用ICE框架,通过ICE来找到一条对等连接的最佳道路。 举个栗子,小A和小B是好朋友,某天他们都换了手机号,而且都绑定了一个手机短号(短号不在一个集团网中),然而都忘记新的号码是多少。为了两个人通话联系,小A尝试用短号拨号不通后,小A拨打114询问自己的电话号码,114看到来电显示后将手机号告诉小A;小B以同样的方式获得了自己的手机号;这样两人就可以相互通话了。 类似于上边的例子,客户端A和客户端B处在不同的内网环境中,首先ICE尝试采用从操作系统和网卡获得的主机地址建立连接,如果连接建立失败,ICE会发送binding request给STUN服务器,服务器探测到客户端A的公网地址后将信息加在binding request中,并返回给客户端A,这样客户端A获取到了自己的公网地址。以同样的方式客户端B获取到自己的公网地址。这样,客户端A、B就可以将SDP中的地址信息替换为公网地址进行通信了。

然而,有些情况并不是那么顺利,比如114显示小A、小B电话未知来电或者小A、小B通话线路故障等原因,导致小A、小B不能通话。这个时候114说,要不这样吧,我给你们各发一个临时号,如果你们要通信,我直接按照你们的临时号给中转一下通话信息。同理,webrtc中,如果STUN建连失败,可以采用TURN服务器的方式。TRUN可以担任中间人的角色,将客户端A、B的数据进行中继转发,实现不同内网的客户端通信。 Stun/turn服务器可以采用coturn(github.com/coturn/cotu…

Webrtc (多对对) Webrtc (1对多)

getUserMedia

navigator.mediaDevices或者navigator.mediaDevices.getUserMedia方法目前主要用于在浏览器中获取音频(通过麦克风)和视频(通过摄像头),将来可以用于获取任意数据流,比如光盘和传感器。

let constraints={
      video: {
        // 设定理想值、最大值、最小值
        width: { min: 960, ideal: 1280, max: 1920 },
        height: { min: 480, ideal: 720, max: 1080 },
        //设置采集帧率
        frameRate: {min:30,max:120},
        //前置
        facingMode: "user",
        //后置
        //facingMode: { exact: "environment" } }
        // 也可以指定设备 id,
        // 通过 navigator.mediaDevices.enumerateDevices() 可以获取到支持的设备
        //{ video: { deviceId: myCameraDeviceId } }
        //还有一个比较有意思的就是设置视频源为屏幕,但是目前只有火狐支持了这个属性。
        mediaSource: 'screen'
    },
    audio:false,
};
navigator.mediaDevices.getUserMedia(constraints).then(onSuccess()).catch(onError());
function onSuccess(stream) {
let video = document.getElementById("localVideo");
     video.srcObject=stream;
}
function onError(error) {
     console.log("navigator.mediaDevices.getUserMedia error: ", error);
}
<body>
     <video id="localVideo" autoplay playsinline></video>
</body>

第一个参数(constraints)是个json对象,表示需要获取的媒体设备,以及媒体信息的相关参数设置 详细可见 MediaTrackSettings

  interface MediaTrackSettings {
        //屏幕高宽比
        aspectRatio?: number;
        //自动增益控制
        autoGainControl?: boolean;
        // 声道数,是指支持能不同发声(注意是不同声音)的音响的个数。 单声道:1个声道 双声道:2个声道 立体声道:默认为2个声道 立体声道(4声道):4个声道
        channelCount?: number;
        //设备id
        deviceId?: string;
        //回波消除
        echoCancellation?: boolean;
        //摄像头类型 前置or后置
        facingMode?: string;
        //帧率 即单位时间内帧的数量,单位为:帧/秒 或fps(frames per second)。如动画书中, 一秒内包含多少张图片,图片越多,画面越顺滑,过渡越自然。 帧率的一般以下几个典型值:
        24/25 fps:1秒 24/25 帧,一般的电影帧率。
        30/60 fps:1秒 30/60 帧,游戏的帧率,30帧可以接受,60帧会感觉更加流畅逼真。
        85 fps以上人眼基本无法察觉出来了,所以更高的帧率在视频里没有太大意义。
        frameRate?: number;
        //设备组id
        groupId?: string;
        //视频高度
        height?: number;
        //延迟
        latency?: number;
        //噪声抑制
        noiseSuppression?: boolean;
        //尺寸调整模式
        resizeMode?: string;
        //采样率 采样率即采样的频率。采样率要大于原声波频率的2倍,人耳能听到的最高频率为20kHz,所以为了满足人耳的听觉要求,采样率至少为40kHz,通常为44.1kHz,更高的通常为48kHz。
        sampleRate?: number;
        //样本量 采样位数涉及到声波的振幅量化。波形振幅在模拟信号上也是连续的样本值,而在数字信号中,信号一般是不连续的,所以模拟信号量化以后,只能取一个近似的整数值,为了记录这些振幅值,采样器会采用一个固定的位数来记录这些振幅值,通常有8位、16位、32位。位数越多,记录的值越准确,还原度越高。 由于数字信号是由0,1组成的,因此,需要将幅度值转换为一系列0和1进行存储,也就是编码,最后得到的数据就是数字信号:一串0和1组成的数据。
        sampleSize?: number;
        //视频宽度
        width?: number;
}

onSuccess是一个回调函数,在获取多媒体设备成功时调用; onError也是一个回调函数,在取多媒体设备失败时调用。 如果网页使用了getUserMedia方法,浏览器就会询问用户,是否同意浏览器调用麦克风或摄像头。如果用户同意,就调用回调函数onSuccess;如果用户拒绝,就调用回调函数onError。 onSuccess回调函数的参数是一个数据流对象stream。stream.getAudioTracks方法和stream.getVideoTracks方法,分别返回一个数组,其成员是数据流包含的音轨和视轨(track)。使用的声音源和摄影头的数量,决定音轨和视轨的数量。比如,如果只使用一个摄像头获取视频,且不获取音频,那么视轨的数量为1,音轨的数量为0。每个音轨和视轨,有一个kind属性,表示种类(video或者audio),和一个label属性(比如FaceTime HD Camera (Built-in))。 onError回调函数接受一个Error对象作为参数。Error对象的code属性有如下取值,说明错误的类型。 PERMISSION_DENIED:用户拒绝提供信息。 NOT_SUPPORTED_ERROR:浏览器不支持硬件设备。 MANDATORY_UNSATISFIED_ERROR:无法发现指定的硬件设备。 截屏 Canvas API有一个ctx.drawImage(video, 0, 0)方法,可以将视频的一个帧转为canvas元素。这使得截屏变得非常容易。 捕获的限定条件 比如限定只能录制高清(或者VGA标准)的视频。

var hdConstraints = {
    video: {
        mandatory: {
            minWidth: 1280,
            minHeight: 720
          }
       }
    };
    var vgaConstraints = {
        video: {
            mandatory: {
            maxWidth: 640,
            maxHeight: 360
           }
        }
};

MediaStreamTrack.getSources() 如果本机有多个摄像头/麦克风,这时就需要使用MediaStreamTrack.getSources方法指定,到底使用哪一个摄像头/麦克风。

MediaStreamTrack.getSources(function(sourceInfos) {
        var audioSource = null;
        var videoSource = null;
        for (var i = 0; i != sourceInfos.length; ++i) {
        var sourceInfo = sourceInfos[i];
        if (sourceInfo.kind === 'audio') {
            console.log(sourceInfo.id, sourceInfo.label || 'microphone');
            audioSource = sourceInfo.id;
        } else if (sourceInfo.kind === 'video') {
            console.log(sourceInfo.id, sourceInfo.label || 'camera');
            videoSource = sourceInfo.id;
        } else {
             console.log('Some other kind of source: ', sourceInfo);
          }
        }
             sourceSelected(audioSource, videoSource);
        });
        function sourceSelected(audioSource, videoSource) {
        var constraints = {
        audio: {
              optional: [{sourceId: audioSource}]
        },
        video: {
              optional: [{sourceId: videoSource}]
           }
        };
        navigator.mediaDevices.getUserMedia(constraints, onSuccess, onError);
}

MediaStreamTrack.getSources方法的回调函数,可以得到一个本机的摄像头和麦克风的列表,然后指定使用最后一个摄像头和麦克风。 RTCPeerConnectionl RTCPeerConnection的作用是在浏览器之间建立数据的“点对点”p2p(peer to peer)通信,也就是将浏览器获取的麦克风或摄像头数据,传播给另一个浏览器。这里面包含了很多复杂的工作,比如信号处理、多媒体编码/解码、点对点通信、数据安全、带宽管理等等。 不同客户端之间的音频/视频传递,是不用通过服务器的。但是,两个客户端之间建立联系,需要通过服务器。服务器主要转递两种数据。 通信内容的元数据:打开/关闭对话(session)的命令、媒体文件的元数据(编码格式、媒体类型和带宽)等。 网络通信的元数据:IP地址、NAT网络地址翻译和防火墙等。 WebRTC协议没有规定与服务器的通信方式,因此可以采用各种方式,比如WebSocket。通过服务器,两个客户端按照Session Description Protocol(SDP协议)交换双方的元数据。

    var signalingChannel = createSignalingChannel();
    var pc;
    var configuration ={};
    // run start(true) to initiate a call
    function start(isCaller) {
    pc = new RTCPeerConnection(configuration);
    // send any ice candidates to the other peer
    pc.onicecandidate = function (evt) {
         signalingChannel.send(JSON.stringify({ "candidate": evt.candidate }));
    };
    // once remote stream arrives, show it in the remote video element
    pc.onaddstream = function (evt) {
         remoteView.src = URL.createObjectURL(evt.stream);
    };
    // get the local stream, show it in the local video element and send it
    navigator.getUserMedia({ "audio": true, "video": true }, function (stream) {
    selfView.src = URL.createObjectURL(stream);
    pc.addStream(stream);
    if (isCaller)
         pc.createOffer(gotDescription);
    else
    pc.createAnswer(pc.remoteDescription, gotDescription);
    function gotDescription(desc) {
        pc.setLocalDescription(desc);
        signalingChannel.send(JSON.stringify({ "sdp": desc }));
      }
    });
    }
    signalingChannel.onmessage = function (evt) {
    if (!pc)
    start(false);
    var signal = JSON.parse(evt.data);
    if (signal.sdp)
      pc.setRemoteDescription(new RTCSessionDescription(signal.sdp));
    else
      pc.addIceCandidate(new RTCIceCandidate(signal.candidate));
    };
    RTCDataChannel
    RTCDataChannel的作用是在点对点之间,传播任意数据。它的API与WebSockets的API相同。
    var pc = new RTCPeerConnection(servers,
    {optional: [{RtpDataChannels: true}]});
    pc.ondatachannel = function(event) {
        receiveChannel = event.channel;
        receiveChannel.onmessage = function(event){
        document.querySelector("div#receive").innerHTML = event.data;
      };
    };
    sendChannel = pc.createDataChannel("sendDataChannel", {reliable: false});
    document.querySelector("button#send").onclick = function (){
    var data = document.querySelector("textarea#send").value;
    sendChannel.send(data);
};

外部函数库 由于这两个API比较复杂,一般采用外部函数库进行操作。目前,视频聊天的函数库有SimpleWebRTC、easyRTC、webRTC.io,点对点通信的函数库有PeerJS、Sharefest。

IceServers

我们使用coturn

docker
docker run -d -p 3478:3478 -p 49152-65535:49152-65535/udp --name coturn instrumentisto/coturn 

coturn docker资源地址:

hub.docker.com/r/instrumen…

centos7
  • 安装相关依赖

yum install -y make gcc cc gcc-c++ wget openssl-devel libevent libevent-devel

  • 下载可以编译的源码包

wget https://coturn.net/turnserver/v4.5.1.2/turnserver-4.5.1.2.tar.gz

  • 解压

tar -zxvf turnserver-4.5.1.2.tar.gz

  • 编译安装
./configure --prefix=/home/webrtc/coturn```
make && make install
  • 设置环境变量
vim ~/.bashrc
export turnserver_home=/home/webrtc/coturn export PATH=$PATH:$turnserver_home/bin ```
source ~/.bashrc
  • 编辑配置文件
find /usr -name turnserver.conf 
vim /home/webrtc/coturn/share/examples/turnserver/etc/turnserver.conf
监听的网卡
listening-device=eth0
监听的端口
listening-port=3478
监听的内网id
listening-ip=172.17.19.101
#监听的外网ip
external-ip=122.51.255.169
# 设置账号密码
user=snjx:suonanjiexi
#cli密码
cli-password=suonanjiexi
指定配置文件启动服务
turnserver -v -r 外网ip -a -o -c /usr/local/turnserver/share/examples/turnserver/etc/turnserver.conf
turnserver -v -r 122.51.255.169 -a -o -c /home/webrtc/coturn/share/examples/turnserver/etc/turnserver.conf```
* 测试 ICEServer
https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/
turn:122.51.255.169:3478
stun:122.51.255.169:3478 

const iceConfiguration = {
    "iceServers": [
       {
             url: 'stun:122.51.255.169:3478'
       },
       { // 谷歌的公共服务
            url: "stun:stun.l.google.com:19302"
       },
       {
            url: 'turn:122.51.255.169:3478',
            username: snjx, // 用户名
            credential: suonanjiexi // 密码
       }
       ]
    }
    let localPc = new RTCPeerConnection(iceServers);
    let remotePc = new RTCPeerConnection(iceServers);

参数配置了两个 url,分别是 STUN 和 TURN,这便是 WebRTC 实现点对点通信的关键,也是一般 P2P 连接都需要解决的问题:NAT穿越。