WebRTC音视频通话简单音视频通话

Posted 音视频开发老舅

tags:

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

本篇不详细介绍websocket,只针对websocket整合rtc。

一、简单说下webrtc的流程

webrtc是P2P通信,也就是实际交流的只有两个人,而要建立通信,这两个人需要交换一些信息来保证通信安全。而且,webrtc必须通过ssh加密,也就是使用https协议、wss协议。

借用一幅图

1.1 创建端点的解析

以下解析不包括websockt,只针对stun做解析。与上图略有不同

  1. 首先,Client A创建端点(Create PeerConnection),并添加音视频流(Add Streams)。接下来通知Client B,让Client B也创建一个端点。

  2. Client B收到通知后,Client B创建端点(Create PeerConnection),并添加音视频流(Add Streams),

  3. 接下来,Client B创建一个用于answer的SDP对象(Create Answer),保存并发送给Client A。

  4. Client A收到用于answer的SDP后,保存下来。

  5. 然后, Client A创建一个用于offer的SDP对象(Create Office),保存并发送给Client B。

  6. 最后,Client B保存收到的用于offer的SDP对象

以上步骤完成之后:

1、rtc会自动收集Candidate信息,并通过回调函数通知你,用于交换Candidate信息。

2、交换完Candidate信息后,P2P连接就建立好了。并通过回调函数,将远程视频流给你

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

1.2 交换Candidate信息

Candidate信息是交换完SDP对象之后自动收集的信息。在创建端点(PeerConnection)的时候,添加回调函数(onIceCandidate

创建回调函数(onIceCandidate)

将Candidate信息发送给另一端(a发给b,b发给a)

保存发过来的 Candidate信息(addIceCandidate)。注意是保存发过来的,不是保存自己的!!!

交换完Candidate信息后,P2P连接就建立好了。

二、新建springboot项目,并开启ssh

因为rtc必须使用ssh,所以springboot需要使用https协议才可以

2.1 生成ssh自签名文件

在终端中执行

keytool -genkey -alias webrtc -dname "CN=Andy,OU=kfit,O=kfit,L=HaiDian,ST=BeiJing,C=CN" -storetype PKCS12 -keyalg RSA -keysize 2048 -keystore webrtc.keystore -validity 36500

执行时,会要求输入密码;
执行后,会在根目录下生成一个webrtc.keystore的文件

 

2.2 配置ssh信息

webrtc.keystore文件放在resource目录下

application.yaml中填写配置信息

server:
  ssl:
    #证书的路径
    key-store: classpath:webrtc.keystore
    #证书密码
    key-store-password: 123456
    #秘钥库类型
    key-store-type: JKS

2.3 检测一下能不能跑起来

运行就行,能跑起来就OK。

三、编写websocket服务类

这个简单的demo只考虑一个房间,没有房间控制,所以websocket代码很少,主要代码都在js里面。

3.1 先放一下Message实体类

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Message

    private String operation;
    private Object msg;

    public Message setOperation(String operation)
    
        this.operation = operation;
        return this;
    

    public Message setMsg(Object msg)
    
        this.msg = msg;
        return this;
    

    public String getMsgStr()
        return msg == null ? "" : msg.toString();
    


3.2 服务类

主要有以下信息:

  • 加入房间(into)
  • 发送 sdp 对象(send-sdp)
  • 交换 candidate 信息(send-candidate)
package com.websocket.controller;

import com.alibaba.fastjson.JSONObject;
import com.websocket.pojo.Message;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;

import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;

@Log4j2
@Controller
@ServerEndpoint("/webrtc")
public class WebrtcController

    /**
     * 这里只做一个最简单的, 只有一个房间, 后面有需要自己可以改
     */
    private static Session offer;
    private static Session answer;

    @OnMessage
    public void onMessage(Session session, String message)
    
        final Message data = JSONObject.parseObject(message, Message.class);
        final Message response = Message.builder()
                .operation(data.getOperation())
                .build();
        switch (data.getOperation()) 
            //加入房间
            case "into": 
                if (offer == null) 
                    offer = session;
                    response.setMsg("offer");
                
                else if (answer == null) 
                    answer = session;
                    response.setMsg("answer");
                
                else 
                    response.setMsg("none");
                
                sendMessage(session, response);
                break;
            
            case "start":
                sendMessage(offer, response);
                break;
            //发送 offer 的 SDP 对象
            case "send-offer":
                //发送 answer 的 SDP 对象
            case "send-answer":
                //交换 candidate 信息
            case "send-candidate": 
                sendOther(session, response.setMsg(data.getMsg()));
                break;
            
        
    

    @OnClose
    public void onClose(Session session, CloseReason closeReason)
    
        if (offer != null && session.getId().equals(offer.getId())) 
            offer = null;
        
        else if (answer != null && session.getId().equals(answer.getId())) 
            answer = null;
        
    

    public static void sendOther(Session session, Object msg)
    
        if (offer != null && session.getId().equals(offer.getId())) 
            sendMessage(answer, msg);
        
        else if (answer != null && session.getId().equals(answer.getId())) 
            sendMessage(offer, msg);
        
    

    public static void sendMessage(Session session, Object msg)
    
        sendMessage(session, JSONObject.toJSONString(msg));
    


    @SneakyThrows
    private static void sendMessage(Session session, String msg)
    
        final RemoteEndpoint.Basic basic = session.getBasicRemote();
        basic.sendText(msg);
    



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

四、页面

4.1 html

这部分不太重要,就直接放上来了

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
  <meta charset="UTF-8">
  <title>websocket-demo</title>
  
  <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.2.1/css/bootstrap.min.css">
</head>
<body>
  <div class="container py-3">
    <div class="row">
      
      <div class="col-12">
        <div id="addRoom" class="btn btn-primary">加入房间</div>
      </div>
      
      <div class="col-12 col-lg-6">
        <p>本地视频:</p>
        <video id="localVideo" width="500px" height="300px" autoplay style="border: 1px solid black;"></video>
      </div>
      
      <div class="col-12 col-lg-6">
        <p>远程视频:</p>
        <video id="remoteVideo" width="500px" height="300px" autoplay style="border: 1px solid black;"></video>
      </div>
    
    </div>
  
  </div>
  
  <script src="https://s3.pstatp.com/cdn/expire-1-M/jquery/3.3.1/jquery.min.js" type="text/javascript"></script>

</body>
</html>

4.2 webrtc工具类 webrtc-util.js

包括以下方法:

打开本地音视频流

创建PeerConnection对象

创建用于office的SDP对象

创建用于answer的SDP对象

保存SDP对象

保存Candidate信息

收集 candidate 的回调

获得远程视频流的回调

需要注意的是:最后的两个回调,需要在创建PeerConnection对象之后,打开本地音视频流之前执行。

4.2.1 本地变量

其中的 ice对象,根据上一篇测试通过的stun服务器信息填写。

//端点对象
let rtcPeerConnection;

//本地视频流
let localMediaStream = null;

//ice服务器信息, 用于创建 SDP 对象
let iceServers = 
  "iceServers": [
    // "url": "stun:stun.l.google.com:19302",
    "urls": ["stun:159.75.239.36:3478"],
    "urls": ["turn:159.75.239.36:3478"], "username": "chr", "credential": "123456",
  ]
;

// 本地音视频信息, 用于 打开本地音视频流
const mediaConstraints = 
  video: width: 500, height: 300,
  audio: true //由于没有麦克风,所有如果请求音频,会报错,不过不会影响视频流播放
;

// 创建 offer 的信息
const offerOptions = 
  iceRestart: true,
  offerToReceiveAudio: true, //由于没有麦克风,所有如果请求音频,会报错,不过不会影响视频流播放
;

4.2.2 打开本地音视频流 

// 1、打开本地音视频流
const openLocalMedia = (callback) => 
  console.log('打开本地视频流');
  navigator.mediaDevices.getUserMedia(mediaConstraints)
    .then(stream => 
      localMediaStream = stream;
      //将 音视频流 添加到 端点 中
      for (const track of localMediaStream.getTracks()) 
        rtcPeerConnection.addTrack(track, localMediaStream);
      
      callback(localMediaStream);
    )

4.2.3 创建 PeerConnection 对象 

// 2、创建 PeerConnection 对象
const createPeerConnection = () => 
  rtcPeerConnection = new RTCPeerConnection(iceServers);

4.2.4 创建用于 offer 的 SDP 对象 

// 3、创建用于 offer 的 SDP 对象
const createOffer = (callback) => 
  // 调用PeerConnection的 CreateOffer 方法创建一个用于 offer的SDP对象,SDP对象中保存当前音视频的相关参数。
  rtcPeerConnection.createOffer(offerOptions)
    .then(sdp => 
      // 保存自己的 SDP 对象
      rtcPeerConnection.setLocalDescription(sdp)
        .then(() => callback(sdp));
    )
    .catch(() => console.log('createOffer 失败'));

 4.2.5 创建用于 answer 的 SDP 对象

// 4、创建用于 answer 的 SDP 对象
const createAnswer = (callback) => 
  // 调用PeerConnection的 CreateAnswer 方法创建一个 answer的SDP对象
  rtcPeerConnection.createAnswer(offerOptions)
    .then(sdp => 
      // 保存自己的 SDP 对象
      rtcPeerConnection.setLocalDescription(sdp)
        .then(() => callback(sdp));
    )
    .catch(() => console.log('createAnswer 失败'))

4.2.6 保存远程的 SDP 对象 

// 5、保存远程的 SDP 对象
const saveSdp = (answerSdp, callback) => 
  rtcPeerConnection.setRemoteDescription(new RTCSessionDescription(answerSdp))
    .then(callback);

4.2.7 保存 candidate 信息 

// 6、保存 candidate 信息
const saveIceCandidate = (candidate) => 
  let iceCandidate = new RTCIceCandidate(candidate);
  rtcPeerConnection.addIceCandidate(iceCandidate)
    .then(() => console.log('addIceCandidate 成功'));

4.2.8 收集 candidate 的回调

// 7、收集 candidate 的回调
const bindOnIceCandidate = (callback) => 
  // 绑定 收集 candidate 的回调
  rtcPeerConnection.onicecandidate = (event) => 
    if (event.candidate) 
      callback(event.candidate);
    
  ;
;

4.2.9 获得 远程视频流 的回调 

// 8、获得 远程视频流 的回调
const bindOnTrack = (callback) => 
  rtcPeerConnection.ontrack = (event) => callback(event.streams[0]);
;

以上代码都写在 webrtc-util.js 中,是可以复用滴

接下来,就是在html中引入这个js,然后和websocket整合一下,把通知、candidate 信息等等,通过websocket发送给另一端

End

如果你对音视频开发感兴趣,觉得文章对您有帮助,别忘了点赞、收藏哦!或者对本文的一些阐述有自己的看法,有任何问题,欢迎在下方评论区讨论!

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

浅聊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)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

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

使用 WebRTC 的 Web 应用程序中的视频通话功能

如何构建一个可以进行基本视频通话的简单 Native WebRTC Android 应用程序?

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

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

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

WebRTC iOS,如何静音视频通话