浅聊WebRTC视频通话

Posted 音视频开发老舅

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅聊WebRTC视频通话相关的知识,希望对你有一定的参考价值。

WebRTC 提供了一套标准 API,使 Web 应用可以直接提供实时音视频通信功能。大部分浏览器及操作系统都支持 WebRTC,直接可以在浏览器端发起实时音视频通话,本文以 WebRTC 初学者的视角去完成一个 1V1 网页版实时音视频通话。

完成音视频通话需要了解四个模块:音视频采集、STUN/TURN 服务器、信令服务器、端与端之间 P2P 连接。使用 WebRTC 的 API 完成音视频采集,配合信令服务器和 WebRTC 的RTCPeerConnection方法能实现 1V1 通话,简易流程如下图:

接下来依次讲解它们的作用和核心 API。

音视频采集

通话的人像和声音采集和播放

WebRTC 使用getUserMedia获取摄像头与话筒对应的媒体流对象MediaStream,媒体流可以通过 WebRTC 进行传输,并在多个对等端之间共享。将流对象赋值给视频元素的 srcObject,实现本地播放音视频

API:navigator.mediaDevices.getUserMedia
参数:constraints
返回:promise,方法调用成功得到MediaStream对象。
 
const localVideo = document.querySelector("video");
 
function gotLocalMediaStream(mediaStream) 
  localVideo.srcObject = mediaStream;

 
navigator.mediaDevices
  .getUserMedia(
      video: 
        width: 640,
        height: 480,
        frameRate:15,
        facingMode: 'enviroment', // 设置为后置摄像头
        deviceId : deviceId ? exact:deviceId : undefined
      ,
      audio: false
   )
  .then(gotLocalMediaStream)
  .catch((error) => console.log("navigator.getUserMedia error: ", error));

连接管理

知道怎么捕获本地音视频,接下来了解怎么与另一端建立连接传输音视频数据。

RTCPeerConnection是 WebRTC 实现网络连接、媒体管理、数据管理的统一接口。建立 P2P 连接需要用到RTCPeerConnection中的几个重要类:SDP、ICE、STUN/TURN。

会话描述信息 RTCSessionDescription(SDP)
SDP 是各端的能力,包括音频编解码器类型、传输协议等。这些信息是建立连接是必须传递的,双方知道视频是否支持音频、编码方式是什么,都能通过 SDP 获得。

比如进行视频传输,我的编码是 H264 对方只能解 H265,就没法进行通信了。

SDP 描述分为两部分,分别是会话级别的描述(session level)和媒体级别的描述(media level),其具体的组成可参考 RFC4566[1],带星号 (*) 的是可选的。常见的内容如下:

Session description(会话级别描述)
    v= (protocol version)
    o= (originator and session identifier)
    s= (session name)
    c=* (connection information -- not required if included in all media) One or more Time descriptions ("t=" and "r=" lines; see below)
    a=* (zero or more session attribute lines) Zero or more Media descriptions
Time description
    t= (time the session is active)
 
Media description(媒体级别描述), if present
    m= (media name and transport address)
    c=* (connection information -- optional if included at session level)
    a=* (zero or more media attribute lines)

SDP 解析时,每个 SDP Line 都是以 key=... 形式,解析出 key 是 a 后,可能有两种方式,可参考 RFC4566[2]:

a=<attribute>
a=<attribute>:<value>

有时候并非冒号 (:) 就一定是 <attribute>:<value>,实际上 value 里面也会有冒号,比如:

a=fingerprint:sha-256 7C:93:85:40:01:07:91:BE
a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=ssrc:2527104241 msid:gLzQPGuagv3xXolwPiiGAULOwOLNItvl8LyS

看一下具体例子:

alert(pc.remoteDescription.sdp);
 
 v=0
 o=alice 2890844526 2890844526 IN IP4 host.anywhere.com
 s=
 c=IN IP4 host.anywhere.com
 t=0 0
 //下面的媒体描述,在媒体描述部分包括音频和视频两路媒体
 m=audio 49170 RTP/AVP 0
 a=fmtp:111 minptime=10;useinbandfec=1 //对格式参数的描述
 a=rtpmap:0 PCMU/8000 //对RTP数据的描述
...
 //上面是音频媒体描述,下面是视频媒体描述
 m=video 51372 RTP/AVP 31
 a=rtpmap:31 H261/90000
 ...
 m=video 53000 RTP/AVP 32
 a=rtpmap:32 MPV/90000

ICE 候选者 RTCIceCandidate
WebRTC 点对点连接最方便的方法是双方 IP 直连,但是在实际的应用中,双方会隔着NAT设备给获取地址造成了麻烦。

WebRTC 通过ICE框架确定两端建立网络连接的最佳路径,为开发者者屏蔽了复杂的技术细节。

(NAT 及ICE框架对于使用 WebRTC 的开发者是一个黑盒,为了优化阅读体验将这部分放在最后作为补充知识)

开发者需要了解

原理

两个节点交换 ICE 候选来协商他们自己具体如何连接,一旦两端同意了一个互相兼容的候选,该候选的 SDP 就被用来创建并打开一个连接,通过该连接媒体流就开始运转。

两个 API

onicecandidate:本地代理创建 SDP Offer 并调用 setLocalDescription(offer) 后触发,在 eventHandler 中通过信令服务器将候选信息传递给远端。

addIceCandidate:接收到信令服务器发送过来的候选信息后调用,为本机添加 ICE 代理。

API:pc.onicecandidate = eventHandler
pc.onicecandidate = function(event) 
  if (event.candidate) 
    // Send the candidate to the remote peer
   else 
    // All ICE candidates have been sent
  

 
 
API:pc.addIceCandidate
pc.addIceCandidate(candidate).then(_=>
  // Do stuff when the candidate is successfully passed to the ICE agent
).catch(e=>
  console.log("Error: Failure during addIceCandidate()");
);

本文福利, 免费领取C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

信令服务器
WebRTC 的 SDP 和 ICE 信息需要依赖信令服务器进行消息传输与交换、建立 P2P 连接,之后才能进行音视频通话、传输文本信息。如果没有信令服务器,WebRTC 无法进行通信。

通常使用socket.io实时通信的能力来构建信令服务器。socket.io跨平台、跨终端、跨语言,方便我们在各个端上去实现信令的各个端,去与我们的服务端进行连接。

这张图就表达了信令服务器在整个通话过程中它起到的作用。

用代码看一下如何建立 socket.io 信令服务器

var express = require("express");
var app = express();
var http = require("http");
const  Server  = require("socket.io");
const httpServer = http.createServer(app);
const io = new Server(httpServer);
 
io.on("connection", (socket) => 
    console.log("a user connected");
    socket.on("message", (room, data) => 
      logger.debug("message, room: " + room + ", data, type:" + data.type);
      socket.to(room).emit("message", room, data);
    )
    socket.on("join", (room) => 
      socket.join(room);
    )
);

端与端之间 P2P 连接

  1. 连接过程

A 和 B 建立网络连接的过程如图:

  • A 向 B 发起 WebRTC 呼叫

  • 创建 peerConnection 对象,在参数中指定 Turn/Stun 的地址

var pcConfig = 
  iceServers: [
    
      urls: "turn:stun.al.learningrtc.cn:3478",
      credential: "mypasswd",
      username: "garrylea",
    ,
    
      urls:[
        "stun:stun.example.com",
        "stun:stun-1.example.com"
      ]
    
  ],
;
 
pc = new RTCPeerConnection(pcConfig);

A 调用createOffer方法创建本地会话描述(SDP offer),SDP offer 包含有关已附加到 WebRTC 会话,浏览器支持的编解码器和选项的所有MediaStreamTrack信息,以及ICE代理,目的是通过信令信道发送给潜在远程端点,以请求连接或更新现有连接的配置。

A 调用setLocalDescription方法将提案设置为本地会话描述,并传递给 ICE 层。之后通过信令服务器将会话描述发送给 B

API:pc.createOffer
参数:无
返回:SDP Offer
 
API:pc. setLocalDescription
参数:offer
返回:Promise<null>
 
function sendMessage(roomid, data) 
  if (!socket) 
    console.log("socket is null");
  
  socket.emit("message", roomid, data);

 
const offer = await pc.createOffer()
await pc.setLocalDescription(offer).catch(handleOfferError);
message.log(`传输发起方本地SDP`);
sendMessage(roomid, offer);

A 端 pc.setLocalDescription(offer)创建后,一个 icecandidate 事件就被发送到RTCPeerConnection,onicecandidate事件会被触发。B 端接收到一个从远端页面通过信令发来的新的 ICE 候选地址信息,本机可以通过调用RTCPeerConnection.addIceCandidate() 来添加一个 ICE 代理。

//A端
pc.onicecandidate = (event) => 
  if (!event.candidate) return;
  sendMessage(roomid, 
    type: "candidate",
    label: event.candidate.sdpMLineIndex,
    id: event.candidate.sdpMid,
    candidate: event.candidate.candidate,
  );
;
 
 
//B端
socket.onmessage = e => 
 if (e.data.hasOwnProperty("type") && e.data.type === "candidate") 
  var candidate = new RTCIceCandidate(
    sdpMLineIndex: data.label,
    candidate: data.candidate,
  );
  pc.addIceCandidate(candidate)
    .then(() => 
      console.log("Successed to add ice candidate");
    )
    .catch((err) => 
      console.error(err);
    );
 
  • A 作为呼叫方获取本地媒体流,调用addtrack方法将音视频流流加入RTCPeerConnection对象中传输给另一端,加入时另一端触发ontrack事件。

媒体流加入媒体轨道
 
API:stream.getTracks
参数:无
返回:媒体轨道对象数组
 
const pc = new RTCPeerConnection();
stream.getTracks().forEach((track) => 
  pc.addTrack(track, stream);
);
 
const remoteVideo = document.querySelector("#remote-video");
pc.ontrack = (e) => 
  if (e && e.streams) 
    message.log("收到对方音频/视频流数据...");
    remoteVideo.srcObject = e.streams[0];
  
;

B 作为呼叫方,从信令服务器收到 A 发过来的会话信息,调用setRemoteDescription方法将提案传递到 ICE 层,调用 addTrack 方法加入 RTCPeerConnction

B 调用 createAnswer 方法创建应答,调用setLocalDeacription方法应答设置为本地会话并传递给 ICE 层。

socket.onmessage = e => 
    message.log("接收到发送方SDP");
    await pc.setRemoteDescription(new RTCSessionDescription(e.data));
    message.log("创建接收方(应答)SDP");
    const answer = await pc.createAnswer();
    message.log(`传输接收方(应答)SDP`);
    sendMessage(roomid, answer);
    await pc.setLocalDescription(answer);

 
  • AB 都有了自己和对方的 SDP,媒体交换方面达成一致,收集的 ICE 完成连通性检测建立最连接方式,P2P 连接建立,获得对方的音视频媒体流。

  • pc.ontrack = (e) => 
      if (e && e.streams) 
        message.log("收到对方音频/视频流数据...");
        remoteVideo.srcObject = e.streams[0];
      
    ;

  • 双向数据通道连接

  • RTCDataChannelton 通过 RTCPeerConnection API 可以建立点对点 P2P 互联,不需要中介服务器,延迟更低。

    一端建立 datachannel, 另一端通过 ondatachannel 获取 datachannel 对象

  • API:pc.createDataChannel
    参数:label  通道名
          options?  通道参数
    返回:RTCDataChannel
     
     
    function receivemsg(e) 
      var msg = e.data;
      if (msg) 
        message.log("-> " + msg + "\\r\\n");
       else 
        console.error("received msg is null");
      
    
     
    const dc = pc.createDataChannel("chat");
    dc.onmessage = receivemsg;
    dc.onopen = function () 
      console.log("datachannel open");
    ;
    dc.onclose = function () 
      console.log("datachannel close");
    ;
     
     
    pc.ondatachannel = e => 
      if(!dc)
        dc = e.channel;
        dc.onmessage = receivemsg;
        dc.onopen = dataChannelStateChange;
        dc.opclose = dataChannelStateChange;
      
    ; //当对接创建数据通道时会回调该方法。

    NAT 及 ICE 框架(补充)
    上文提到ICE集成了多种 NAT 穿越技术,比如 STUN、TURN,可以实现NAT穿透,在主机之间发现 P2P 传输路径机制。接下来简单介绍下 NAT、STUN、TURN 是什么。

    网络地址转换( NAT)
    NAT常部署在一个组织的网络出口位置。网络分为私网和公网两个部分,NAT 网关设置在私网到公网的路由出口位置,私网与公网间的双向数据必须都要经过 NAT 网关。组织内部的大量设备,通过 NAT 就可以共享一个公网 IP 地址,解决了 IPv4 地址不足的问题。

    如下图所示,有两个组织,每个组织的 NAT 分配一个公网 IP,分别是 1.2.3.4 以及 1.2.3.5。每个组织私网设备通过 NAT 将内网地址转换为公网地址,然后加入互联网

    NAT 及 ICE 框架(补充)
    上文提到ICE集成了多种 NAT 穿越技术,比如 STUN、TURN,可以实现NAT穿透,在主机之间发现 P2P 传输路径机制。接下来简单介绍下 NAT、STUN、TURN 是什么。

    网络地址转换( NAT)
    NAT常部署在一个组织的网络出口位置。网络分为私网和公网两个部分,NAT 网关设置在私网到公网的路由出口位置,私网与公网间的双向数据必须都要经过 NAT 网关。组织内部的大量设备,通过 NAT 就可以共享一个公网 IP 地址,解决了 IPv4 地址不足的问题。

    如下图所示,有两个组织,每个组织的 NAT 分配一个公网 IP,分别是 1.2.3.4 以及 1.2.3.5。每个组织私网设备通过 NAT 将内网地址转换为公网地址,然后加入互联网

  • NAT 对待 UDP 的实现方式有 4 种,分别为:完全圆锥型、地址受限锥型、端口受限锥型、对称型。

    Session Traversal Utilities for NAT (STUN)
    STUN允许位于 NAT(或多重 NAT)后的客户端找出自己的公网地址,查出自己位于哪种类型的 NAT 之后以及 NAT 为某一个本地端口所绑定的公网端端口。

  • STUN是 C/S 模式的协议,由客户端发送 STUN 请求、STUN 服务响应告知由NAT分配给主机的 IP 地址和端口号,也是一种 Request/Response 的协议,默认端口号是 3478。

    想让内网主机知道它的外网 IP,需要在公网上架设一台 STUN server,并向这台服务器发送 Request,服务器就会返回它的公网 IP 了。

    下面是抓取的一对 STUN 绑定请求和响应。首先客户端向地址为 216.93.246.18 的 STUN 服务器发送 Binding Request(STUN 绑定请求)。

  • 服务器回了 Binding Response,返回公网 IP:

TURN是一种数据传输协议。允许通过 TCP 或 UDP 方式穿透 NAT 或防火墙。TURN 是一个 Client/Server 协议。TURN 的 NAT 穿透方法与 STUN 类似,都是通过取得应用层中的公网地址达到 NAT 穿透

ICE 收集
ICE两端并不知道所处的网络的位置和 NAT 类型,通过ICE能够动态的发现最优的传输路径。ICE 端收集本地地址、通过STUN服务收集 NAT 外网地址、通过TURN收集中继地址,所以会有三种候选地址:

host 类型,即本机内网的 IP 和端口;

srflx 类型, 即本机 NAT 映射后的外网的 IP 和端口;

relay 类型,即中继服务器的 IP 和端口。


    IP: xxx.xxx.xxx.xxx,
    port: number,
    type: host/srflx/relay,
    priority: number,
    protocol: UDP/TCP,
    usernameFragment: string
    ...
 

下图中,Alice 与 Bob 通过 STUN 以及 TURN 服务器收集了三种类型的 candidate。

ICE 收集 candidate 后进行连通性检测,确定主机之间 P2P 最佳传输路径。

本文福利, 免费领取C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

如何实现 iOS开发webrtc 视频通话时录像,截屏。

参考技术A 实现 iOS开发webrtc 视频通话时录像,截屏推荐ZEGO即构科技,4行代码,30分钟在APP、Web和小程序等应用内实现视频通话、语音通话,互动直播功能。【点击免费试用,0成本启动】

实现 iOS开发webrtc 视频通话时录像,截屏的具体步骤如下:
1.权限申请。
2.引入 WebRTC 库。
3.采集并显示本地视频。
4.信令驱动。
5.创建音视频数据通道。
6.媒体协商。
7.渲染远端视频。

想要了解更多关于webrtc的相关信息,推荐咨询ZEGO即构科技。ZEGO即构科技自主研发的高音质语音视频引擎,能够提供实时清晰的多人语音通话独立自研的语音前处理模块(AEC, NS, AGC)能够提供优于同类竞品的处理效果,支持全带语音处理。良好的抖动缓冲、前向纠错和丢帧补偿技术使引擎适应复杂的网络环境,提供低延时清晰流畅的语音视频通话,在较差网络环境中自适应的找到延时与流畅的最佳契合点。官网上海风华峻极智能科技有限公司
2022-05-05广告上海风华峻极智能科技有限公司【远程会议视频系统】一体化解决方案,如同面对面开会一般,让远程会议更简单,多种会议场景布局,舞台设备厂家,高保真语音的传输,极速快捷远程会议体验,只为高效沟通,支持万人及多房间同时在线.点击进入详情页本回答由上海风华峻极智能科技有限公司提供护肤达人IT宅族
2016-06-16·护肤达人,宅租,科技达人护肤达人IT宅族采纳数:5637获赞数:17423
向TA提问私信TAWEBRTC支持点对点通讯,但是WEBRTC仍然需要服务端,因为:
1,为了协调通讯过程客户端之间需要交换元数据,如一个客户端找到另一个客户端以及通知另一个客户端开始通讯.
2,需要处理NAT或防火墙,这是公网上通讯首要处理的问题.
在这篇文章里我们将告诉您怎么创建一个信令服务,怎么处理现实世界中两个客户端的连接,以及怎么处理多方通话和怎么与VOIP,PSTN的交互.

什么是信令?
信令就是协调通讯的过程,为了建立一个webrtc的通讯过程,客户端需要交换如下信息:
1,会话控制消息:用来开始和结束通话(即开始视频,结束视频这些操作指令)
2,处理错误的消息.
3,元数据:如各自的音视频编解码方式,带宽.
4,网络数据:对方的公网IP,端口,内网IP,端口.
5,......

以上是关于浅聊WebRTC视频通话的主要内容,如果未能解决你的问题,请参考以下文章

如何实现 iOS开发webrtc 视频通话时录像,截屏。

WebRTC iOS,如何静音视频通话

Android端WebRTC音视频通话录音-获取音频输出数据

React 原生 firebase+webrtc 进行视频通话

一文带你了解webrtc基本原理(动手实现1v1视频通话)

是否可以通过 WebRTC 或 Web 套接字保存视频通话