webrtc入门

前言:

        webrtc的作用是让两个客户端可以进行点对点的连接,使得双方在传递数据时不需要服务端做转发,提高效率。当然,实际的生产工作中,我们并不能完全脱离服务端,两个客户端想要建立链接,必须交换双方的信息,保证能访问到对方,且发送的内容能被对方正确解析,这个交换信息的工作往往需要服务器来完成。

        另外,webrtc有自己的数据通道(RTCDataChannel),这是一个独立的通道,和媒体流通道不同,它可以传输任何数据,包括媒体流数据。所以也有人会用这个通道去传输h265的视频流,来避开webrtc不支持h265的这个问题。

        webrtc建立连接的过程通常是由一方先发起的,即发起者,另一方作为应答者,类似于打电话,肯定是一方先打电话给另一方的,至于两者哪个是提供视频的,哪个是展示视频的,跟是否为发起者无关。

准备工作:

    1.一个推流客户端,用于创建视频流,并将视频流推送给拉流客户端。

    2.一个拉流客户端,用户接受推流客户端的视频流,并进行展示。

    3.一个信令服务器,用于交换两个客户端的配置信息,包括音视频编解码器和网络情况等,信令服务器往往会开启两个websocket服务,分别用于接受客户端和服务端。

储备知识:

    1.SDP:全名Session Description Protocal,中文名叫会话描述协议,在webrtc中主要用于描述设备的音视频编解码能力和网络情况。在后面的内容中,我会称呼其为offer和answer,分别对应发起者和应答者的sdp。

    2.ICE:全名Interactive Connectivity Establishment,中文名叫交互式连接建立,它会同时使用STUN和TURN两种网络穿透技术,来帮助两个客户端建立端到端的网络连接。这里因为我们是纯内网的教学,所以不介绍STUN和TURN了,因为用不到,想了解的小伙伴自行百度。

    3.WebSocket:一种保持长连接的双向通信的协议。可以做到客户端和服务端的实时数据传输。

    4.webrtc相关api:

        a.RTCPeerConnection,可以通过let pc = new RTCPeerConnection()的方式来创建一个webrtc实例,这是建立webrtc连接的起点,必须先创建webrtc实例,才能调用后续的方法。

        b.RTCSessionDescription,可通过let offer = new RTCSessionDescription(offer)的方式来创建一个sdp,因为经过websocket转发后,sdp会变成json字符串,所以我们需要该方法来复原sdp对象。

        c.RTCIceCandidate,类似于RTCSessionDescription,只不过它是用于复原ice对象的,我们不需要真的创建一个新的sdp或ice对象,我们调用这两个方法只是为了复原。

        d.addTrack,调用方法: pc.addTrack(track, stream),一个属于应答者的监听事件,调用该方法可以将应答者的媒体流存入webrtc实例中,当两个客户端建立连接后,发起者的webrtc实例可以通过监听ontrack事件来获取到对应的媒体流。其中的track和stream可以通过<video>元素获取,或者某些特殊的api,比如getDisplayMedia,相关知识可见MDN。

        e.createOffer,调用方法:let offer = await pc.createOffer()。这里最好用async...await...包裹一下,因为createOffer会返回一个promise。该方法一般由webrtc的发起者调用,返回的offer需要借助websocket服务发给另一个客户端。

        f.createAnswer,调用方法:let answer = await pc.createAnswer()。和createOffer类似(其实本质是一样的,不过是作为应答者才需要调用的方法而已)。

        g.setLocalDescription,调用方法:pc.setLocalDescription(offer)。调用即可,无需返回,一般不会失败的,用于设置本地描述。该方法在两个客户端都需要执行,发起者的参数是offer,应答者的参数是answer。

        h.setRemoteDescription,调用方法:pc.setRemoteDescription(offer)。和setLocalDescription类似,不过是用于设置远程描述的,所以在发起者那边的参数是answer,在应答者这边的参数是offer,恰恰相反。

        i.onicecandidate,调用方法:pc.onicecandidate=function(event){......},一个监听事件,当客户端查询到各自的网络信息后,会触发该事件,并给回调函数传入一个参数event,我们可以通过event.candidate来获取其中的信息,然后通过websocket服务发送给另一方,该事件在发起者和应答者都需要实现,并且不像sdp那样还要区分为offer和answer。

        j.addIceCandidate,调用方法:pc.addIceCandidate(candidate),也是一个异步方法,不过没有返回值,所以调用即可,用于将另一方的candidate信息存到自己的webrtc实例中。

        k.ontrack,调用方法:pc.ontrack=function (event){......},一个属于发起者的监听事件,该事件在获取到应答者的媒体流后触发,我们可以通过event.streams来拿到所有的媒体流,注意这是个数组,数组元素的多少取决于应答者执行了多少的addTrack。

工作流程:

这是一张从网上找的图,我觉得是能比较清晰的说明整个webrtc的工作流程的:

dd1a852f8f735c8608772259ae00c3cd.png

下面给大家讲解一下这张图,同时附上一些代码实现:

1.首先介绍四个角色:

    a.Client A,该客户端用于提供视频流。

    b.Stun Server,网络穿透服务器,用于查询Client A和Client B的网络信息,如果Client A和Client B处于同一内网环境下,一般来讲,是不用这个的。

    c.Signal Server,信令服务器,用于交换两个客户端之间的信令(sdp和ice)

    d.Client B,该客户端用于获取Client A推送过来的视频流,并进行展示。

2.第一步必定是启动信令服务器,因为两个客户端之间建立连接依赖于该服务器,信令服务器一般只需创建两个websocket,等待两个客户端连接自己。而信令服务器的功能也很简单,用两个变量streamer和player分别记录两个客户端的websocket连接,当Client A的服务(即streamer)触发message事件时,streamer会把answer(另一种格式的sdp,其本质也是sdp)和ice通过player的send方法,发送给Client B;当Client B的服务(即player)触发message事件时,player会把offer(也是sdp)和ice通过streamer的send方法,发送给推流端。当然服务器还需监听一些error,close事件,做容错处理,下面的代码有一半是在做这件事的。

    Signal Server:

var clientConfig = { type: 'config', peerConnectionOptions: {} };  //webrtc对象的配置信息
let player;    //拉流端websocket对象
let streamer; // 推流端websocket对象


//创建推流端websocket服务
let WebSocket = require('ws');
let streamerServer = new WebSocket.Server({ port: 5501, backlog: 1 });
streamerServer.on('connection', function (ws, req) {
  ws.on('message', function onStreamerMessage(msg) {
    try {
      msg = JSON.parse(msg);
    } catch(err) {
      streamer.close(1008, 'Cannot parse');
      return;
    }
    try {
      //streamer通过player的send方法进行信息交换
      if (msg.type == 'answer') {
        player.send(JSON.stringify(msg));
      } else if (msg.type == 'iceCandidate') {
        player.send(JSON.stringify(msg));
      } else if (msg.type == 'disconnectPlayer') {
        player.close(1011 /* internal error */, msg.reason);
      } else {
        console.error(`unsupported Streamer message type: ${msg.type}`);
        streamer.close(1008, 'Unsupported message type');
      }
    } catch(err) {
      console.error(`ERROR: ws.on message error: ${err.message}`);
    }
  });
  ws.on('close', function(code, reason) {
    try {
      console.error(`streamer disconnected: ${code} - ${reason}`);
    } catch(err) {
      console.error(`ERROR: ws.on close error: ${err.message}`);
    }
  });
  ws.on('error', function(error) {
    try {
      console.error(`streamer connection error: ${error}`);
      ws.close(1006 /* abnormal closure */, error);
    } catch(err) {
      console.error(`ERROR: ws.on error: ${err.message}`);
    }
  });
  streamer = ws;
  streamer.send(JSON.stringify(clientConfig));
});


//创建拉流端
let playerServer = new WebSocket.Server({ port: 5502, backlog: 1 });


playerServer.on('connection', function (ws, req) {
  if (!streamer || streamer.readyState != 1 /* OPEN */) {
    ws.close(1013 /* Try again later */, 'Streamer is not connected');
    return;
  }
  ws.on('message', function (msg) {
    try {
      msg = JSON.parse(msg);
    } catch (err) {
      console.error(`Cannot parse player message: ${err}`);
      ws.close(1008, 'Cannot parse');
      return;
    }
    //player通过streamer的send方法进行信息交换
    if (msg.type == 'offer') {
      streamer.send(JSON.stringify(msg));
    } else if (msg.type == 'iceCandidate') {
      streamer.send(JSON.stringify(msg));
    } else {
      console.error(`unsupported message type: ${msg.type}`);
      ws.close(1008, 'Unsupported message type');
      return;
    }
  });
  ws.on('close', function(code, reason) {
    console.log(`player connection closed: ${code} - ${reason}`);
  });
  ws.on('error', function(error) {
    console.error(`player ${playerId} connection error: ${error}`);
    ws.close(1006 /* abnormal closure */, error);
  });
  player = ws;
  ws.send(JSON.stringify(clientConfig));
});

3.然后推流端启动,连接websocket服务器,成功连接后,推流端会收到来自服务端发送的webrtc配置信息:

{ type: 'config', peerConnectionOptions: {} },收到该信息后,推流端可以调用浏览器的navigator.mediaDevices.getDisplayMedia方法来获取到桌面应用的视频流数据,具体获取视频流的写法如下:

Client A:

//音视频配置信息,video属性可以是一个对象,用于设置视频的分辨率,比如:
// video:{ width:1280, height:720 }
const CONSTRAINTS={
    audio:false,
    video:true
}
//websocket服务连接成功后调用该函数,传入的peerConnectionOptions是信令服务器发过来的webrtc配置项
async function initMedia(peerConnectionOptions){
    try{
      let stream = await navigator.mediaDevices.getDisplayMedia(CONSTRAINTS);
      //playerRef.current指向一个video元素,用于预览获取到的画面
      playerRef.current.srcObject = stream;    
      //该函数根据peerConnectionOptions创建一个webrtc实例,具体实现见后续步骤
      setupWebRtcPlayer(peerConnectionOptions);    
      stream.getTracks().forEach(track=>{
        // webrtc.current指向webrtc实例,这一步是把获取到的媒体流加入webrtc的媒体流轨道中,
        // 当两个客户端连接成功后,拉流端可以从轨道中获取到推流端的媒体流。
        webrtc.current.addTrack(track, stream);    
      });
    }catch(e){
      console.log("组件"+id+"出错:",e);
    }
  }

4.接下来,推流端将进入待命状态,直到拉流端来获取视频流。对于拉流端而言,它也需要连接websocket服务,在连接websocket服务成功后,它会和推流端一样创建一个webrtc实例(即setupWebRtcPlayer方法)。当然,推流端和拉流端需要实现的webrtc实例是不一样的,从上图中可以看出,在双方都连接websocket服务成功后,推流端执行下面三个方法:

a. Create PeerConnection

b.Add Streams

c.Create Offer

其中Add Streams对应上方代码的 stream.getTracks()...... ,是把媒体流存入webrtc中的过程。另外两个步骤的代码可以简化成下面这样:

 Client A:

async function setupWebRtcPlayer(peerConnectionOptions){
  try{
    let pc1 = new RTCPeerConnection(peerConnectionOptions);    //Create PeerConnection
    let offer = await pc1.createOffer();      //Create Offer
    await pc1.setLocalDescription(offer);    //这里把创建的offer存储为本地描述
    ws.current.send(JSON.stringify(offer));  //通过websocket服务发送offer,ws.current指向一个webscocket实例
    return pc1;
  }catch(e){
    console.log("setupWebRtcPlayer fail:",e);
  }
  return;
}

5.接下来就是把创建的offer通过websocket服务发送给信令服务器,然后信令服务器转发给Client B,Client B在接收到offer后,也会创建一个webrtc实例(即Create PeerConnection),不过后续的操作就不太一样了,我们需要在Client B中将offer设置为远程描述,然后创建answer,将answer设置为Client B的本地描述,然后通过信令服务器将answer发给Client A,设置远程和本地描述的过程没有在途中展现,不过代码页挺简单的,如下:

Client B:

async function setupWebRtcPlayer(peerConnectionOptions,offer){
  try{
    let pc2 = new RTCPeerConnection(peerConnectionOptions);    //Create PeerConnection
    await pc2.setRemoteDescription(offer);    //将Client A的offer设置为远程描述
    let answer = await pc2.createAnswer();    //创建answer
    await pc2.setLocalDescription(answer);    //这里把创建的answer存储为本地描述
    ......    //通过信令服务器发送answer的代码就省略了
    return pc2;
  }catch(e){
    console.log("setupWebRtcPlayer fail:",e);
  }
  return;
}

6.当Client A收到Client B发过来的answer之后,它会将answer设置为Client A的远程描述,注意我们在客户端的setupWebRtcPlayer函数中return 了pc1和pc2,这两个都是webrtc实例,可以调用相关的api,所以我们只需在Client A中执行 pc1.setRemoteDescription(answer) 即可将answer设置为远程描述。

7.到此为止,我们已经将两个客户端之间的offer(也叫sdp)交换完毕,剩下的就简单多了,两个客户端会自动向STUN Server查询自己的公网IP信息(内网不需要),然后得到ice,触发各自的onicecandidate事件,同时通过websocket服务,将各自的ice发送给对方。所以,我们需要再两个客户端上分别实现onicecandidate和执行addIceCandidate方法了。

Client A、Client B:

//这里监听icecandidate事件,然后发送对应的ice信息
pc1.onicecandidate = e=>{
  ws.current.send(JSON.stringify({ type: "iceCandidate", candidate: e.candidate }));
}
//在两个客户端都需要对type = iceCandidate的消息进行监听,先复原收到的ice对象,然后执行addIceCandidate方法
ws.current.onmessage=e=>{
  const msg = JSON.parse(e.data);
  const { type } = msg;
  switch(type){
    ......
    case 'offer':
      //sdp在接受到之后也需要进行复原,然后再执行相关操作
      let offerDesc = new RTCSessionDescription(msg);
      ......
      break;
    case 'answer':
      let answerDesc = new RTCSessionDescription(msg);
    ......
      break;
    case 'iceCandidate':
      let candidate = new RTCIceCandidate(msg.candidate);
      webrtc.current.addIceCandidate(candidate);
      break;
    ......
  }
}

8.当双方的ice交换完毕后,意味着Client A和Client B已经成功连接了,不过我们还需要执行最后一步,在Client B中将媒体流取出来,并在<video>中进行播放。这一步主要通过ontrack事件来完成,代码也很简单:

Client B:

pc2.ontrack=e=>{
  //video.current指向的是一个video标签,e.streams[0]是指第一个媒体流。
  video.current.srcObject = e.streams[0];
}

云渲染:

        UE的云渲染也是基于webrtc去做的,大家可以把启动后的UE程序理解为Client A,我们的网页就是Client B,在启动信令服务器和UE程序后,Client B需要做的就是以下几个步骤:

1.Create PeerConnection

2.Create Offer //这里咱们不需要add stream,因为 add stream是UE来做的。

3.Send Offer SDP

4.Relay Answer SDP

5.On Ice Candidate

6.Send Ice Candidate

7.Relay Ice Candidate

8.Add Ice Candidate

9.on Add Stream //没错,因为我们没做add stream,所以我们就需要去接受视频流,添加ontrack事件。

        看上去步骤挺多的,但其实就是调用一下websocket和webrtc相关的api而已,代码量实际上不大,当然,真正的云渲染不仅仅是传输视频流这么简单,我们还需要进行通信,还要传递鼠标交互甚至是文件等,这些内容就很多了,我们放到后面去讲,入门的话先学到这吧。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
WebRTC 简介 WebRTC,名称源自网页实时通信(Web Real-Time Communication)的缩写,是一个支持网页浏览器进行实时语音通话或视频聊天的技术,是谷歌2010年以6820万美元收购Global IP Solutions公司而获得的一项技术。 WebRTC提供了实时音视频的核心技术,包括音视频的采集、编解码、网络传输、显示等功能,并且还支持跨平台:windows,linux,mac,android。 虽然WebRTC的目标是实现跨平台的Web端实时音视频通讯,但因为核心层代码的Native、高品质和内聚性,开发者很容易进行除Web平台外的移殖和应用。很长一段时间内WebRTC是业界能免费得到的唯一高品质实时音视频通讯技术。 为什么需要 WebRTC 开发者教程? 虽然WebRTC技术已经较为成熟,其集成了最佳的音/视频引擎,十分先进的codec,且包含了使用STUN、ICE、TURN、RTP-over-TCP的关键NAT和防火墙穿透等众多门槛并不低的技术。抛开音视频技术本身的复杂性外,要想找到合适的资料、完整的代码和库、配合合适的IDE和辅助工具能正常地实现编译和安装都非常的不容易,而这还只是个开始。没有靠谱的教程,你该怎么开始?那么地坑等在那,难道你打算一个一个趟过去? 本《WebRTC 零基础开发者教程》主要讲了什么 本文中提供下载的《WebRTC 零基础开发者教程》将以一个初学者的角度,从0开始逐步引导你掌握WebRTC开发的方方面面(当然,教程中更多的是操作性的内容,具体到技术原理和实现,显然不是本教程的讨论范畴)。 《WebRTC 零基础开发者教程》目录 1 工具 1.1 depot_tools 1.1.1 目标 1.1.2 Chromium 1.1.3 使用说明在这儿 1.1.4 下载 1.1.5 使用 1.1.6 具体使用例子 1.2 Gyp工具 1.3 Python工具 1.4 本地集成开发环境(IDE ) 1.4.1 Visual studio 1.4.2 Kdevelop 1.4.3 Eclipse 2 Webrtc 2.1 下载、编译 2.1.1 Windows下 2.1.2 ubuntu下编译 2.1.3 编译Android(只能在 linux 下) 3 webrtc开发 3.1 开发P2P视频软件需要处理的问题 3.1.1 用户列的获取、交换、信令的交换 3.1.2 P2P通信 3.1.3 多媒体处理 3.2 webrtc架构 3.2.1 WebRTC架构组件介绍 3.2.2 WebRTC核心模块API介绍 3.2.3 webRTC核心API详解 4 Libjingle详细介绍 4.1 重要组件 4.1.1 信号 4.1.2 线程和消息 4.1.3 名称转换 4.1.4 SSL支持 4.1.5 连接 4.1.6 传输,通道,连接 4.1.7 候选项 4.1.8 数据包 4.2 如何工作 4.2.1 Application模块 4.2.2 XMPP Messaging Component 模块 4.2.3 Session Logic and management commponent 模块 4.2.4 Peer to peer Component 模块 4.2.5 其他 4.3 建立libjingle应用程序 5 代码分析 5.1 音频通道建立过程 5.2 音频接收播放过程 5.3 视频接收播放过程 6 协议 6.1 XMPP协议 6.1.1 原理介绍 6.1.2 XMPP 协议网络架构 6.1.3 XMPP 协议的组成 6.1.4 Xmpp介绍 6.1.5 协议内容 6.2 Stun协议 6.2.1 P2P实现的原理 6.2.2 P2P的常用实现 6.2.3 Stun URI 6.2.4 内容 6.2.5 中文内容 6.2.6 开源服务器 6.2.7 公开的免费STUN服务器 6.3 Turn协议 6.3.1 概念 6.3.2 Turn uri 6.3.3 开源服务器工程 6.3.4 开源库 6.4 交互式连接建立(Interactive Connectivity Establishment) 6.4.1 IETF规格 6.4.2 开源工程 6.5 XEP-0166 Jingle 6.5.1 绪论 6.5.2 需求 6.6 Sctp协议 6.7 Rtp协议 7 附件 7.1 Gyp工具 7.2 Google test程序 7.3 Webrtc库介绍 7.4 webrtc代码相关基础知识 7.5 STUN和TURN技术浅析 7.6 基于ICE的VoIP穿越NAT改进方案 7.7 ubuntu安装使用stuntman 7.8 一个开源的ICE库——libnice介绍 7.9 4种利用TURN穿越对称型NAT方案的设计与实现 7.10 基于ICE方式SIP信令穿透Symmetric_NAT技术研究

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值