《WebRTC实时通信》笔记整理汇总

前言

1、本书脉络

  1. 以高度抽象方式描述与WebRTC相关的完整开发周期;

    • 说明如何编写代码以查询、获得对本地多媒体资源(如音频和视频设备)的访问,并在HTML5中显示

    • 将获得的媒体流与PeerConnection对象相关联(对等方的逻辑连接);

    • 讨论与建立正确的信令通道有关的各种选择

  2. 构建一个完整的WebRTC应用程序

2、章节简介

《WebRTC实时通信》

第一章-简介

1、WebRTC架构

WebRTC梯形
  1. 浏览器内运行Web应用程序(自其它Web服务器下载);
  2. 信令消息用于建立和终止通信,通过 HTTP 或 WebSocket 协议通过 Web 服务器传输;
  3. 数据路径:PeerConnection 允许媒体直接在浏览器之间流动,而无需任何中间服务器

当两种浏览器都运行相同的 Web 应用程序(从相同的网页下载)时,架构变为三角形

WebRTC三角形
服务器-浏览器交互协议栈
rcwr_0103rcwr_0104

1、Datagram Transport Layer Security (DTLS)

数据报传输层安全性协议旨在防止对用户数据报协议(UDP)提供的数据传输进行窃听,篡改或消息伪造。 DTLS 协议基于面向流的传输层安全性(TLS)协议,旨在提供类似的安全性保证。

2、Secure Real-time Transport Protocol (SRTP)

安全实时传输协议用于将媒体数据与用于监视与数据流相关的传输统计信息的 RTP 控制协议 RTP Control Protocol (RTCP)信息一起传输。

3、STUN and TURN

NAT会话遍历实用程序(STUN)协议允许主机应用程序发现网络上网络地址转换器的存在,并且在这种情况下,可以为当前连接获取分配的公共IP和端口元组。 为此,该协议需要已配置的第三方STUN服务器的协助,该服务器必须位于公共网络上。

围绕NAT的遍历使用中继(TURN)协议允许NAT后面的主机从驻留在公用Internet上的中继服务器获取公用IP地址和端口。 由于中继了传输地址,主机可以从任何可以将数据包发送到公共Internet的对等方接收媒体。

3、WebRTC API

主要概念具体解释
MediaStream音视频数据流的抽象表示(本地和远程音频和视频的获取和管理)
PeerConnection允许两个用户在浏览器之间直接通信,表示与远程对等点的关联(连接管理)
DataChannel提供通用传输服务,允许Web浏览器以双向对等方式交换通用数据(管理任意数据)

**WebRTC API接口参考:**https://developer.mozilla.org/zh-CN/docs/Web/API/WebRTC_API

第二章-处理浏览器中的媒体

摘要:利用浏览器提供的API获取本地媒体流并在浏览器内播放。

1、WebRTC的10个步骤

WebRTC的10个步骤

2、媒体捕获及数据流(API)

2.1 MediaStream

W3C Media Capture and Streams 文档定义了一组 JavaScript API,这些API使应用程序能够从平台请求音频和视频流,以及操纵和处理流数据

截屏2020-10-12 下午4.55.10

单个MediaStream 可以包含零个或多个轨道。 每个轨道都有一个对应的 MediaStreamTrack 对象,该对象代表用户代理中的特定媒体源MediaStream 中的所有轨道在渲染时进行同步MediaStreamTrack 表示包含一个或多个通道的内容,其中,通道之间具有定义的已知的关系。

2.2 getUserMedia()/ createObjectUrl()

W3C Media Capture Streams API 定义了两种方法 getUserMedia()createObjectUrl()

方法用法解释
getUserMedia()getUserMedia(constraints, successCallback, errorCallback)通过指定一组(强制或可选)成功和失败的回调函数,Web 开发人员可以访问本地设备媒体(当前是音频和/或视频);
提示用户许可使用其网络摄像头或其他视频或音频输入
createObjectUrl()createObjectURL(stream)指示浏览器创建和管理与本地文件或二进制对象(blob)关联的唯一URL

3、 使用getUserMedia()

<!--
    例2-1 第一个启用WebRTC的html界面
-->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>getUserMedia very simple demo</title>
</head>
<body>
    <div id="mainDiv">
        <h1><code>getUserMedia()</code> very simple demo</h1>
        <p>With this example, we simply call <code>getUserMedia()</code> and display  the received stream inside an HTML5 <video> element</p>
        <p>View page source to access both HTML and JavaScript code...</p>
        <video autoplay></video>
        <script src="js/getUserMedia.js"> </script> 
      </div>
</body>
</html>
// Look after different browser vendors' ways of calling the getUserMedia()
// API method:
// Opera --> getUserMedia
// Chrome --> webkitGetUserMedia
// Firefox --> mozGetUserMedia

navigator.getUserMedia = navigator.webkitGetUserMedia;

// Use constraints to ask for a video-only MediaStream:
var constraints = {audio: false, video: true}; // 只收集视频,不收集音频
var video = document.querySelector("video"); // 返回文档中匹配指定 CSS 选择器的一个元素

// Callback to be called in case of success...
function successCallback(stream) {
  // getUserMedia()成功,网络摄像头的视频流将被设置为视频元素的源
  // Note: make the returned stream available to console for inspection
  window.stream = stream; // 将 MediaStream 提供给控制台以供用户检查

  if (window.URL) {
    // Chrome case: URL.createObjectURL() converts a MediaStream to a blob URL

    // Reference: https://github.com/a-wing/webrtc-book-cn/issues/1
    // video.src = window.URL.createObjectURL(stream);

    video.srcObject = stream;
  } else {
    // Firefox and Opera: the src of the video can be set directly from the stream
    video.src = stream;
  }

  // We're all set. Let's just play the video out!
  video.play();
}

// Callback to be called in case of failures...
function errorCallback(error) {
  console.log("navigator.getUserMedia error: ", error);
}

// Main action: just call getUserMedia() on the navigator object
/** 
 * 方法提醒用户需要使用音频(0或者1)和(0或者1)视频输入设备,比如相机,屏幕共享,或者麦克风。
 * 如果用户给予许可,successCallback回调就会被调用,MediaStream对象作为回调函数的参数。
 * 如果用户拒绝许可或者没有媒体可用,errorCallback就会被调用,
*/
navigator.getUserMedia(constraints, successCallback, errorCallback);

将html文件在浏览器中打开,页面显示如下:

截屏2020-10-18 上午11.11.23

给予许可后,successCallback回调就会被调用,由摄像头收集到的视频将在界面中播放:

截屏2020-10-18 上午11.14.10

在console输入Stream检查:

截屏2020-10-18 上午11.16.16

4、媒体模型

浏览器提供了从源(sources)到接收器(sinks)的媒体管道(pipeline)。在浏览器中,接收器是 <img><video><audio> 标记。

浏览器中的来源可分为静态源和动态源:

特点
静态源物理网络摄像头,麦克风
来自用户硬盘驱动器的本地视频或音频文件,网络资源或静态图像
来源产生的媒体通常不会随时间变化
向用户显示此类源的接收器(实际标签本身)具有用于控制源内容的各种控件。
动态源麦克风和相机getUserMedia() API方法添加;
来源的特性可能会根据应用需求而变化

4.1 媒体约束

约束用于限制MediaStream 轨道源上允许的可变性范围,由对象的属性或特征以及可能的值的集和组成,可以将值指定为范围或枚举。

MediaTrackConstraint对象中定义了特定的约束,一个约束用于音频,另一个约束用于视频。此对象中的属性的类型为ConstraintLongConstraintBooleanConstraintDoubleConstraintDOMString 。这些可以是特定值(例如,数字,布尔值或字符串),范围(具有最小值和最大值的LongRangeDoubleRange )或具有idealexact定义的对象。

  1. 对于特定值,浏览器将尝试选择尽可能接近的东西。
  2. 对于一个范围,将使用该范围内的最佳值。
  3. 如果指定了exact ,则仅返回与该约束完全匹配的媒体流。

getUserMedia()调用允许在首次获取轨道时应用一组初始约束(例如,设置视频分辨率的值)(仅Chrome);

此外,可以在初始化后通过专用的约束API添加约束

4.2 如何使用约束——以设置视频分辨率为例

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>getUserMedia() and constraints</title>
</head>
<body>
    <div id="mainDiv">
        <h1><code>getUserMedia()</code>: playing with video constraints</h1>
        <p>Click one of the below buttons to change video resolution...</p>
        <div id="buttons">
            <button id="qvga">320x240</button>
            <button id="vga">640x480</button>
            <button id="hd">1280x960</button>
        </div>
        <p id="dimensions"></p>
        <video autoplay></video>
        <script src="js/getUserMedia_constraints.js"></script>
    </div>
</body>
</html>
// Define local variables associated with video resolution selection
// buttons in the HTML page
var vgaButton = document.querySelector("button#vga");
var qvgaButton = document.querySelector("button#qvga");
var hdButton = document.querySelector("button#hd");

// Video element in the HTML5 pagevar
video = document.querySelector("video");

// The local MediaStream to play with
var stream;

// Look after different browser vendors' ways of calling the
// getUserMedia() API method:
navigator.getUserMedia = navigator.webkitGetUserMedia ;

// Callback to be called in case of success...
function successCallback(gotStream) {
  // Make the stream available to the console for introspection
  window.stream = gotStream;

  // Attach the returned stream to the <video> element
  // in the HTML page
  // video.src = window.URL.createObjectURL(stream);
  video.srcObject = gotStream;
  // Start playing video
  video.play();
}

// Callback to be called in case of failure...
function errorCallback(error){
  console.log("navigator.getUserMedia error: ", error);
}

/**
 * 定义三种分辨率的视频对象
 */
// Constraints object for low resolution video
var qvgaConstraints = {
  video: {
    mandatory: {
      maxWidth: 320,
      maxHeight: 240
    }
  }
};
// Constraints object for standard resolution video
var vgaConstraints = {
  video: {
    mandatory: {
      maxWidth: 640,
      maxHeight: 480
    }
  }
};
// Constraints object for high resolution video
var hdConstraints = {
  video: {
    mandatory: {
      minWidth: 1280,
      minHeight: 960
    }
  }
};

/**
 * 设置button的行为
 * 点击某一button,将对应分辨率的对象作为参数传入getMedia()
 */
// Associate actions with buttons:
qvgaButton.onclick = function() {
  getMedia(qvgaConstraints)
};
vgaButton.onclick = function() {
  getMedia(vgaConstraints)
};
hdButton.onclick = function() {
  getMedia(hdConstraints)
};

// Simple wrapper for getUserMedia() with constraints object as
// an input parameter
function getMedia(constraints) {
  if (!!stream) {
    video.src = null;
    stream.stop();
  }
  navigator.getUserMedia(constraints, successCallback, errorCallback);
}

界面如下:

320x240680x480
截屏2020-10-18 下午2.09.03截屏2020-10-18 下午2.10.01

点击1280x960按钮时抛出错误,OverConstrainedError,判断是摄像头无法采集1280x960分辨率视频。

截屏2020-10-18 下午2.19.25

OverConstrainedError[无法满足要求错误]

指定的要求无法被设备满足,此异常是一个类型为OverconstrainedError的对象,拥有一个constraint属性,这个属性包含了当前无法被满足的constraint对象,还拥有一个message属性,包含了阅读友好的字符串用来说明情况。

第三章-构建浏览器RTC梯形图:本地视角

摘要:分析 WebRTC 1.0 API,其主要目的是允许向其他浏览器发送和接收媒体(暂不涉及信令通道)。

需要一种机制来适当地协调实时通信,并允许对等方交换控制消息。 在 WebRTC 内部尚未定义这种机制(通常称为信令),因此不属于 RTCPeerConnection API 规范

浏览器之间的互操作性Web服务器使用下载的JavaScript代码确保。由此,开发人员可以使用常用的消息传递协议(SIP、XMPP、Jingle…)来实现信令通道,也可以自行设计专有信令机制。

简而言之,RTCPeerConnection API负责浏览器之间P2P通信,是MediaPath(本章内容);

SIP、XMPP、Jingle等是用于Web服务器之间交换信令,是Signaling Path。

1、传递哪些信息?——Signaling Path

  1. 如何知道彼此存在?如何看到对方?——媒体会话管理 设置和断开通信,并报告潜在的错误情况;
  2. 双方如何连接传输信息?——节点的网络配置,即使存在NAT也可用于交换实时数据的网络地址和端口;
  3. 如何使音视频信息在双方浏览器端均可以编解码?——节点的多媒体功能 支持的媒体,可用的编码器/解码器(编解码器),支持的分辨率和帧速率等。

在正确交换和协商所有上述信息之前,WebRTC 对等方之间无法传输任何数据。

在第3章中,为简化问题,将通过某种方式在单台计算机上模拟对等行为来实现此目标。这意味着我们暂时将绕过信令通道设置阶段,并让上述三个步骤(会话管理,网络配置和多媒体功能交换)在单个计算机上进行。

在第5章中,我们将通过展示本地场景如何在两个启用 WebRTC 的对等点之间引入真正的信令通道,来最终向 WebRTC 建筑中添加最后一块砖。

2、RTCPeerConnection——MediaPath

RTCPeerConnection 接口代表一个由本地计算机到远端的WebRTC连接。该接口提供了创建,保持,监控,关闭连接的方法的实现,是每个浏览器之间点对点连接的核心。

2.1 前序知识

2.1.1 STUN
截屏2020-10-18 下午10.26.08

STUN(用户数据报协议[UDP]简单穿越网络地址转换器[NAT])服务器允许所有的NAT客户终端(如防火墙后边的计算机)与位于局区域网以外的远端实现媒体交流。

通过STUN服务器,客户终端可以了解他们的:

  1. 公共地址
  2. 挡在他们前面的NAT类型
  3. 通过NAT与特定局部端口相连的因特网方端口
2.1.2 TURN

STUN P2P不成功后,使用数据中转。

一种数据传输协议,是STUN/RFC5389的一个拓展。允许在 TCP 或 UDP 的连线上在线跨域 NAT 或防火墙
为了保证通信能够建立,我们可以在没办法的情况下用保证成功的中继方法(Relaying), 虽然使用中继会对服务器负担加重,而且也算不上P2P,但是至少保证了最坏情况下信道的通畅,从而不至于受NAT类型的限制。

2.1.3 SDP

SDP(Session Description Protocol)是一种通用的会话描述协议,主要用来描述多媒体会话,用途包括会话声明、会话邀请、会话初始化等。

WebRTC主要在连接建立阶段用到SDP,连接双方通过信令服务交换会话信息,包括音视频编解码器(codec)、主机候选地址、网络传输协议等。

2.1.4 DTLS

Datagram Transport Layer Security数据报传输层安全性(DTLS)是一种通信协议,旨在保护数据隐私并防止窃听和篡改。 它基于传输层安全性(TLS)协议,该协议为基于计算机的通信网络提供安全性。 DTSL和TLS之间的主要区别在于DTLS使用UDP,而TLS使用TCP。 它可用于Web浏览,邮件,即时消息传递和VoIP。 DTSL是与SRTP一起用于WebRTC技术的安全协议之一。

2.1.5 SRTP

SRTP(SecureReal-time Transport Protocol) 安全实时传输协议,SRTP是在实时传输协议(Real-time Transport Protocol)基础上所定义的一个协议,旨在为单播和多播应用程序中的实时传输协议的数据提供加密、消息认证、完整性保证和重放保护安全实时传输协议。

2.2 将 MediaStream 添加到 PeerConnection

rcwr_0301

调用 new RTCPeerConnection(configuration) 会创建一个 RTCPeerConnection 对象,该对象是两个用户/浏览器之间通信通道的抽象,可以为特定的 MediaStream 输入或输出,如图所示。

2.2.1 程序分析

例3-1 本地 RTCPeerConnection 用法示例

页面设计

  1. start——启动应用程序
  2. call——在本地和(假)远程用户之间进行呼叫
  3. hangup——挂断该呼叫

截屏2020-10-18 下午10.00.56

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
    "http://www.w3.org/TR/html4/loose.dtd">

<html>
  <head>
    <title>Local PeerConnection() example</title>
  </head>
  <body>
    <table border="1" width="100%">
      <tr>
        <th>Local video</th>
        <th>'Remote' video</th>
      </tr>
      <tr>
        <td><video id="localVideo" autoplay></video></td>
        <td><video id="remoteVideo" autoplay></video></td>
      </tr>
      <tr>
        <td align="center">
          <div>
            <button id="startButton">Start</button>
            <button id="callButton">Call</button>
            <button id="hangupButton">Hang Up</button>
          </div>
        </td>
        <td><!-- void --></td>
      </tr>
    </table>
    <script src="js/localPeerConnection.js"></script>
  </body>
</html>

JS代码分析

详见注释

// JavaScript variables holding stream and connection information
// 声明三个变量存储媒体流、本地连接信息、远端连接信息
var localStream, localPeerConnection, remotePeerConnection;

// JavaScript variables associated with HTML5 video elements in the page
var localVideo = document.getElementById("localVideo");
var remoteVideo = document.getElementById("remoteVideo");

// JavaScript variables assciated with call management buttons in the page
var startButton = document.getElementById("startButton");
var callButton = document.getElementById("callButton");
var hangupButton = document.getElementById("hangupButton");

// Just allow the user to click on the Call button at start-up
// 只允许用户在打开页面时点击按钮startButton
// 禁用callButton hangupButton
startButton.disabled = false;
callButton.disabled = true;
hangupButton.disabled = true;

// Associate JavaScript handlers with click events on the buttons
// 将按钮和对应行为联系起来
startButton.onclick = start;
callButton.onclick = call;
hangupButton.onclick = hangup;

// Utility function for logging information to the JavaScript console
// 在命令行显示一些必要信息
function log(text) {
	console.log("At time: " + (performance.now() / 1000).toFixed(3) + " --> " + text);
}

// Callback in case of success of the getUserMedia() call
function successCallback(stream) {
	log("Received local stream");

	// Associate the local video element with the retrieved stream
	if (window.URL) {
        // localVideo.src = URL.createObjectURL(stream);
        localVideo.srcObject=stream;
	} else {
		localVideo.src = stream;
	}

	localStream = stream;

    // We can now enable the Call button
    // 启用 callButton.disabled
	callButton.disabled = false;
}

// Function associated with clicking on the Start button
// This is the event triggering all other actions
// 获取本地音视频流,显示在界面中
function start() {
	log("Requesting local stream");

    // First of all, disable the Start button on the page
    // 禁用 startButton
	startButton.disabled = true;

	// Get ready to deal with different browser vendors...
	navigator.getUserMedia = navigator.getUserMedia ||
		navigator.webkitGetUserMedia || navigator.mozGetUserMedia;

	// Now, call getUserMedia()
    navigator.getUserMedia({audio:true, video:{mandatory: {maxWidth: 320,maxHeight: 240}}}, 
        successCallback, 
        function(error) {
		log("navigator.getUserMedia error: ", error);
	});
}

// Function associated with clicking on the Call button
// This is enabled upon successful completion of the Start button handler
/**
 * 1、调用 RTCPeerConnection API
 * 2、创建本地、远端 PeerConnection对象
 * 3、分别设置事件触发器onicecandidate
 * 4、将本地流添加到本地 PeerConnection
 * 5、调用 createOffer() 方法
 */
function call() {
    // First of all, disable the Call button on the page...
    // 禁用 callButton
	callButton.disabled = true;

    // ...and enable the Hangup button
    // 启用 hangupButton
	hangupButton.disabled = false;
	log("Starting call");

	// Note that getVideoTracks() and getAudioTracks() are not currently
	// supported in Firefox...
	// ...just use them with Chrome
	if (navigator.webkitGetUserMedia) { // 成功get本地Stream
		// Log info about video and audio device in use
		if (localStream.getVideoTracks().length > 0) {  // 视频
			log('Using video device: ' + localStream.getVideoTracks()[0].label);
		} if (localStream.getAudioTracks().length > 0) {    // 音频
			log('Using audio device: ' + localStream.getAudioTracks()[0].label);
		}
	}

	// Chrome
	if (navigator.webkitGetUserMedia) {
		RTCPeerConnection = webkitRTCPeerConnection; // 调用RTCPeerConnection API

		// Firefox
	} else if(navigator.mozGetUserMedia) {
		RTCPeerConnection = mozRTCPeerConnection;
		RTCSessionDescription = mozRTCSessionDescription;
		RTCIceCandidate = mozRTCIceCandidate;
	}  log("RTCPeerConnection object: " + RTCPeerConnection);

	// This is an optional configuration string, associated with
    // NAT traversal setup
    // 和NAT穿越打洞相关的可选的参数
	var servers = null;

    // 创建本地PeerConnection对象
	localPeerConnection = new RTCPeerConnection(servers);
	log("Created local peer connection object localPeerConnection");

    // Add a handler associated with ICE protocol events
    /**
     * RTCPeerConnection 的属性 onicecandidate (是一个事件触发器 EventHandler) 
     * 能够让函数在事件icecandidate发生在实例  RTCPeerConnection 上时被调用。 
     * 只要本地代理ICE 需要通过信令服务器传递信息给其他对等端时就会触发。
     */
	localPeerConnection.onicecandidate = gotLocalIceCandidate;

    // 创建远端PeerConnection对象
	remotePeerConnection = new RTCPeerConnection(servers);
	log("Created remote peer connection object remotePeerConnection");

	// Add a handler associated with ICE protocol events...
	remotePeerConnection.onicecandidate = gotRemoteIceCandidate;

	// ...and a second handler to be activated as soon as the remote
	// stream becomes available.
	remotePeerConnection.onaddstream = gotRemoteStream;

	// Add the local stream (as returned by getUserMedia())
	// to the local PeerConnection.
	localPeerConnection.addStream(localStream);
	log("Added localStream to localPeerConnection");

	// We're all set! Create an Offer to be 'sent' to the callee as soon
    // as the local SDP is ready.
    // createOffer()启动创建一个SDP offer,目的是启动一个新的WebRTC去连接远程端点。
	localPeerConnection.createOffer(gotLocalDescription, onSignalingError);
}

function onSignalingError(error) {
	console.log('Failed to create signaling message : ' + error.name);
}

// Handler to be called when the 'local' SDP becomes available
function gotLocalDescription(description) {
	// Add the local description to the local PeerConnection
	localPeerConnection.setLocalDescription(description);
	log("Offer from localPeerConnection: \n" + description.sdp);

	// ...do the same with the 'pseudoremote' PeerConnection
	// Note: this is the part that will have to be changed if you want
	// the communicating peers to become remote
	// (which calls for the setup of a proper signaling channel)
	remotePeerConnection.setRemoteDescription(description);

	// Create the Answer to the received Offer based on the 'local' description
	remotePeerConnection.createAnswer(gotRemoteDescription, onSignalingError);
}

// Handler to be called when the remote SDP becomes available
function gotRemoteDescription(description){
	// Set the remote description as the local description of the
	// remote PeerConnection.
	remotePeerConnection.setLocalDescription(description);
	log("Answer from remotePeerConnection: \n" + description.sdp);

	// Conversely, set the remote description as the remote description of the
	// local PeerConnection
	localPeerConnection.setRemoteDescription(description);
}

// Handler to be called when hanging up the call
function hangup() {
	log("Ending call");

	// Close PeerConnection(s)
	localPeerConnection.close();
	remotePeerConnection.close();

	// Reset local variables
	localPeerConnection = null;
	remotePeerConnection = null;

	// Disable Hangup button
	hangupButton.disabled = true;

	// Enable Call button to allow for new calls to be established
	callButton.disabled = false;
}

// Handler to be called as soon as the remote stream becomes available
function gotRemoteStream(event){
	// Associate the remote video element with the retrieved stream
	if (window.URL) {
		// Chrome;
        // remoteVideo.src = window.URL.createObjectURL(event.stream);
        remoteVideo.srcObject=event.stream;
	} else {
		// Firefox;
		remoteVideo.src = event.stream;
	}  log("Received remote stream");
}

// Handler to be called whenever a new local ICE candidate becomes available
function gotLocalIceCandidate(event){
	if (event.candidate) {
        // Add candidate to the remote PeerConnection;
        // 向 ICE 代理提供远程候选对象
		remotePeerConnection.addIceCandidate(new RTCIceCandidate(event.candidate));
		log("Local ICE candidate: \n" + event.candidate.candidate);
	}
}

// Handler to be called whenever a new remote ICE candidate becomes available
function gotRemoteIceCandidate(event){
	if (event.candidate) {
		// Add candidate to the local PeerConnection;
		localPeerConnection.addIceCandidate(new RTCIceCandidate(event.candidate));

		log("Remote ICE candidate: \n " + event.candidate.candidate);
	}
}

2.2.2 运行情况
截屏2020-10-19 上午10.41.18 截屏2020-10-19 上午10.41.48 截屏2020-10-19 上午10.42.11 截屏2020-10-19 上午10.42.58

2.3 将数据通道添加到本地 PeerConnection

点对点数据 API 使 Web应用程序可以以点对点方式发送和接收通用应用程序数据。 用于发送和接收数据的 API 汲取了 WebSocket 的启发。

和上文2.2实验的区别是:上文实验直接将本地Stream作为远端Stream,不涉及数据流的传输。本实验添加了用于通用数据流的数据通道。

2.3.1 程序分析

例3-2 本地数据通道用法示例

页面设计

  1. Start——在启动时按下的“开始”按钮;
  2. Send——需要在数据通道上流式传输新数据时使用的发送按钮;
  3. Stop——关闭按钮,可用于重置应用程序并将其恢复到原始状态。

截屏2020-10-19 上午10.59.28

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
    "http://www.w3.org/TR/html4/loose.dtd">
<html>
  <head>
    <title>DataChannel simple example</title>
  </head>
  <body>
    <textarea rows="5" cols="50" id="dataChannelSend" disabled placeholder="1: Press Start; 2: Enter text; 3: Press Send."></textarea>
    <textarea rows="5" cols="50" id="dataChannelReceive" disabled></textarea>
    <div id="buttons">
      <button id="startButton">Start</button>
      <button id="sendButton">Send</button>
      <button id="closeButton">Stop</button>
    </div>
    <script src="js/dataChannel.js"></script>
  </body>
</html>

JS代码分析

//JavaScript variables associated with send and receive channels
var sendChannel, receiveChannel;

//JavaScript variables associated with demo buttons
var startButton = document.getElementById("startButton");
var sendButton = document.getElementById("sendButton");
var closeButton = document.getElementById("closeButton");

//On startup, just the Start button must be enabled
startButton.disabled = false;
sendButton.disabled = true;
closeButton.disabled = true;

//Associate handlers with buttons
startButton.onclick = createConnection;
sendButton.onclick = sendData;
closeButton.onclick = closeDataChannels;

//Utility function for logging information to the JavaScript console
function log(text) {
  console.log("At time: " + (performance.now() / 1000).toFixed(3) +" --> " + text);
}

/**
 * 创建连接,方法与上一个实验类似
 * 调用 RTCPeerConnection
 */
function createConnection() {

  // Chrome
  if (navigator.webkitGetUserMedia) {
    RTCPeerConnection = webkitRTCPeerConnection;

    // Firefox
  } else if(navigator.mozGetUserMedia) {
    RTCPeerConnection = mozRTCPeerConnection;
    RTCSessionDescription = mozRTCSessionDescription;
    RTCIceCandidate = mozRTCIceCandidate;
  }
  log("RTCPeerConnection object: " + RTCPeerConnection);

  // This is an optional configuration string
  // associated with NAT traversal setup
  var servers = null;

  // JavaScript variable associated with proper
  // configuration of an RTCPeerConnection object:
  // use DTLS/SRTP
  var pc_constraints = {
    'optional': [
      {
        'DtlsSrtpKeyAgreement': true
      }
    ]
  };

  // Create the local PeerConnection object...
  // ...with data channels
  
  localPeerConnection = new RTCPeerConnection(servers, pc_constraints);
  log("Created local peer connection object, with Data Channel");
  try {

    // Note: SCTP-based reliable DataChannels supported
    // in Chrome 29+ !
    // use {reliable: false} if you have an older version of Chrome
    //RTCPeerConnection 的 createDataChannel() 方法创建一个可以发送任意数据的数据通道(data channel)
    //常用于后台传输内容, 例如: 图像, 文件传输, 聊天文字, 游戏数据更新包, 等等。
    sendChannel = localPeerConnection.createDataChannel("sendDataChannel", {reliable: true});
    log('Created reliable send data channel');
  } catch (e) {
    alert('Failed to create data channel!');
    log('createDataChannel() failed with following message: ' + e.message);
  }
  // Associate handlers with peer connection ICE events
  localPeerConnection.onicecandidate = gotLocalCandidate;

  // Associate handlers with data channel events
  sendChannel.onopen = handleSendChannelStateChange;
  sendChannel.onclose = handleSendChannelStateChange;

  // Mimic a remote peer connection
  window.remotePeerConnection = new RTCPeerConnection(servers, pc_constraints);
  log('Created remote peer connection object, with DataChannel');

  // Associate handlers with peer connection ICE events...
  remotePeerConnection.onicecandidate = gotRemoteIceCandidate;

  // ...and data channel creation event
  remotePeerConnection.ondatachannel = gotReceiveChannel;

  // We're all set! Let's start negotiating a session...
  localPeerConnection.createOffer(gotLocalDescription, onSignalingError);

  // Disable Start button and enable Close button
  startButton.disabled = true;
  closeButton.disabled = false;
}

function onSignalingError(error) {
  console.log('Failed to create signaling message : ' + error.name);
}

// Handler for sending data to the remote peer
/**
 *一旦有新数据到达接收器,handleMessage() 处理函数就会被调用。 
 *这样的处理程序首先在接收者的文本区域内打印收到的消息,然后重置发送者的编辑框
 */
function sendData() {

  var data = document.getElementById("dataChannelSend").value;
  sendChannel.send(data);
  log('Sent data: ' + data);
}

// Close button handler
function closeDataChannels() {

  // Close channels...
  log('Closing data channels');
  sendChannel.close();
  log('Closed data channel with label: ' + sendChannel.label);
  receiveChannel.close();
  log('Closed data channel with label: ' + receiveChannel.label);

  // Close peer connections
  localPeerConnection.close();
  remotePeerConnection.close();

  // Reset local variables
  localPeerConnection = null;
  remotePeerConnection = null;
  log('Closed peer connections');

  // Rollback to the initial setup of the HTML5 page
  startButton.disabled = false;
  sendButton.disabled = true;
  closeButton.disabled = true;
  dataChannelSend.value = "";
  dataChannelReceive.value = "";
  dataChannelSend.disabled = true;
  dataChannelSend.placeholder = "1: Press Start; 2: Enter text; 3: Press Send.";

}

// Handler to be called as soon as the local SDP is made available to
// the application
function gotLocalDescription(desc) {

  // Set local SDP as the right (local/remote) description for both local
  // and remote parties
  localPeerConnection.setLocalDescription(desc);
   log('localPeerConnection\'s SDP: \n' + desc.sdp);
  remotePeerConnection.setRemoteDescription(desc);

  // Create answer from the remote party, based on the local SDP
  remotePeerConnection.createAnswer(gotRemoteDescription, onSignalingError);
}

// Handler to be called as soon as the remote SDP is made available to
// the application
function gotRemoteDescription(desc) {

  // Set remote SDP as the right (remote/local) description for both local
  // and remote parties
  remotePeerConnection.setLocalDescription(desc);
  log('Answer from remotePeerConnection\'s SDP: \n' + desc.sdp);
  localPeerConnection.setRemoteDescription(desc);
}

// Handler to be called whenever a new local ICE candidate becomes available
function gotLocalCandidate(event) {
  log('local ice callback');
  if (event.candidate) {
    remotePeerConnection.addIceCandidate(event.candidate);
    log('Local ICE candidate: \n' + event.candidate.candidate);
  }
}

// Handler to be called whenever a new remote ICE candidate becomes available
function gotRemoteIceCandidate(event) {
  log('remote ice callback');
  if (event.candidate) {
    localPeerConnection.addIceCandidate(event.candidate);
    log('Remote ICE candidate: \n ' + event.candidate.candidate);
  }
}

// Handler associated with the management of remote peer connection's
// data channel events
function gotReceiveChannel(event) {
  log('Receive Channel Callback: event --> ' + event);

  // Retrieve channel information
  receiveChannel = event.channel;

  // Set handlers for the following events:
  // (i) open; (ii) message; (iii) close
  receiveChannel.onopen = handleReceiveChannelStateChange;
  receiveChannel.onmessage = handleMessage;
  receiveChannel.onclose = handleReceiveChannelStateChange;
}

// Message event handler
function handleMessage(event) {
  log('Received message: ' + event.data);

  // Show message in the HTML5 page
  document.getElementById("dataChannelReceive").value = event.data;

  // Clean 'Send' text area in the HTML page
  document.getElementById("dataChannelSend").value = '';
}

// Handler for either 'open' or 'close' events on sender's data channel
function handleSendChannelStateChange() {
  var readyState = sendChannel.readyState;
  log('Send channel state is: ' + readyState);

  if (readyState == "open") {

    // Enable 'Send' text area and set focus on it
    dataChannelSend.disabled = false;
    dataChannelSend.focus();
    dataChannelSend.placeholder = "";

    // Enable both Send and Close buttons
    sendButton.disabled = false;
    closeButton.disabled = false;
  } else {

    // event MUST be 'close', if we are here...
    // Disable 'Send' text area
    dataChannelSend.disabled = true;

    // Disable both Send and Close buttons
    sendButton.disabled = true;
    closeButton.disabled = true;
  }
}

// Handler for either 'open' or 'close' events on receiver's data channel
function handleReceiveChannelStateChange() {
  var readyState = receiveChannel.readyState;
  log('Receive channel state is: ' + readyState);
}
2.3.2 运行情况
截屏2020-10-19 上午11.07.23 截屏2020-10-19 上午11.07.32 截屏2020-10-19 上午11.07.41

Xnip2020-10-19_16-14-27

2.3.3 API总结
API描述
setLocalDescription()aPromise = RTCPeerConnection.setLocalDescription(sessionDescription);如果setLocalDescription()在连接已经建立时被调用,则表示正在进行重新协商(可能是为了适应不断变化的网络状况)。直到协商完成前,商定的配置都不会生效。
setRemoteDescription()aPromise = RTCPeerConnection.setRemoteDescription(sessionDescription);方法改变与连接相关的描述,该描述主要是描述有些关于连接的属性,例如对端使用的解码器。
createAnswer()aPromise = RTCPeerConnection.createAnswer([options]);方法会创建对从远程对等方收到的要约的SDP应答,包含有关会话中已附加的所有媒体,浏览器支持的编解码器和选项以及已收集的所有ICE候选者的信息。
ondatachannel()RTCPeerConnection.ondatachannel = function;是一个 EventHandler,它指定了一个函数,当datachannel event发生在 RTCPeerConnection 上时,该函数将被调用。

第四章-需要信令通道

摘要:在对等端间建立适当的信令通道(作为第三章的补充)。模拟了以服务器为中继,两个客户端之间交换信令的过程。

正如我们在第3章中预期的那样,在启用 WebRTC 的应用程序中需要一个信令通道,以允许交换会话描述和网络可达性信息。在本章中,我们将描述如何在对成功建立启用 WebRTC 的通信会话感兴趣的任何一对对等端之间创建适当的信令通道。

1 、呼叫流程分析(设计)

角色说明
通道发起者对等方,首先主动与远端建立专用的通信通道
信令服务器管理通道创建并充当消息中继节点
频道加入者远程方加入已存在的频道

这个想法是在接收到启动器发出的特定请求后,服务器根据需要创建通道。 第二个对等方加入频道后,就可以开始对话。 消息交换始终通过服务器进行,该服务器基本上充当透明的中继节点。 当对等方之一决定退出正在进行的对话时,它将在断开连接之前向服务器发出一条临时消息(图中称为Bye)。 服务器将该消息分派给远程方,在将确认发送回服务器后,远程方也将断开连接。 收到确认后,最终会触发服务器端的通道重置过程,从而使总体方案恢复到其原始配置。

2、最终效果

  1. 进入项目文件夹下,进入终端,输入node simpleNodeServer_OK.js
截屏2020-10-23_下午8_10_05
  1. 打开Firefox浏览器,输入http://localhost:8181/(此页面记为Client-A)
  2. 输入“房间号”创建一个新“房间”
截屏2020-10-23 下午8.18.17

此时浏览器界面显示

截屏2020-10-23 下午8.19.45
  1. 再打开一个窗口,输入http://localhost:8181/,输入相同“房间号”加入房间(此页面记为Client-B)

Client-A显示

截屏2020-10-23 下午8.26.47

此时若有第三个页面想要加入,显示

截屏2020-10-23 下午8.29.16
  1. 开始聊天
  2. 输入"Bye"结束聊天
Client-AClient-B
截屏2020-10-23 下午8.32.07截屏2020-10-23 下午8.32.34

命令行输出如下

截屏2020-10-23 下午8.33.44

3、具体实现

客户机端服务器端(最多2个客户端相连)
(1) 允许客户端连接到服务器(通过socket.io库)
(2) 提示用户输入要加入的频道的名称
(3) 将创建或加入请求发送到服务器
(4) 开始初步处理服务器发送的事件。
(1) 创建服务实例监听8181扩展
(2) 要求创建房间的第一个客户端是通道启动器
(3) 允许第二个到达的客户端加入新创建的频道
(4) 所有其他客户端均被拒绝进入会议室(并因此收到该事件的通知)

序列图

rcwr_0401_full

4、Socket.io类库[1]

socket.emit(event,data,[callback])
  1. event表示:参数值为一个用于指定事件名的字符串。
  2. data参数值:代表该事件中携带的数据。这个数据就是要发送给对方的数据。数据可以是字符串,也可以是对象。
  3. callback参数:值为一个参数,用于指定一个当对方确定接收到数据时调用的回调函数
socket.on(event,function(data,fn){})
socket.once(event,function(data,fn){})

一方使用emit发送事件后,另一方可以使用on,或者once方法,对该事件进行监听。once和on不同的地方就是,once只监听一次,会在回调函数执行完毕后,取消监听。

socket.broadcast.send('user connected');
socket.broadcase.emit('login',names)

当我们某个客户端与服务器建立连接以后,用于与该客户端连接的socket对象,就有一个broadcast对象,代表所有与其他Socket.IO客户端建立连接的socket对象。可以利用该对象的send方法和emit方法向所有其他客户端广播消息。

5、代码分解

5.1 创建信令信道

Peer1加载html界面 -> 连接至服务器 -> 输入channel名

//Connect to server
var socket = io.connect('http://localhost:8181');

//Ask channel name from user 
channel = prompt("Enter signalling channel name:");

创建频道,Client向Server发create or join事件,并传递上一步获取到的频道名。

if (channel !== "") {
	console.log('Trying to create or join channel: ', channel);
	// Send 'create or join' to the server
	socket.emit('create or join', channel);
}

Server监听到Client的emit后完成以下操作:

  1. 验证所提及的渠道是一个全新的通道(即其中没有客户)
  2. 将服务器端 room 与通道关联
  3. 允许发出请求的客户端加入通道
  4. 向客户端发送一条名为 created 的通知消息
// Handle 'create or join' messages
        socket.on('create or join', function (channel) {
                var numClients = findClientsSocket(channel);
                console.log('numclients = ' + numClients);
                // First client joining...
                if (numClients == 1){
                        socket.join(channel);
                        socket.emit('created', channel);
                        console.log("First client joining...");
                // Second client joining...
                } else if (numClients == 2) {
                    ...
                } else { // max two clients
                		...
                }
        });

当Client收到Server的答复时,它仅将事件记录在 JavaScript 控制台上和 HTML5 页面中包含的 <div> 元素内:

//Handle 'created' message
socket.on('created', function (channel){	 
	console.log('channel ' + channel + ' has been created!');
	console.log('This peer is the initiator...');

	// Dynamically modify the HTML5 page
	div.insertAdjacentHTML( 'beforeEnd', '<p>Time: ' + 
		(performance.now() / 1000).toFixed(3) +	' --> Channel ' 
		+ channel + ' has been created! </p>');
	
	div.insertAdjacentHTML( 'beforeEnd', '<p>Time: ' + 
		(performance.now() / 1000).toFixed(3) + 
		' --> This peer is the initiator...</p>');
});

如图:

截屏2020-10-23 下午8.19.45

5.2 加入信令信道

和上文类似,Peer2加载html界面 -> 连接至服务器 -> 输入channel名;

Client向Server发create or join事件,并传递上一步获取到的频道名;

Server侦听获取channel后, 由于这次请求方不是发起方,因此Server的行为将由以下代码段驱动:

...								
								  else if (numClients == 2) {
                        // Inform initiator...
                		io.sockets.in(channel).emit('remotePeerJoining', channel);
                		// Let the new peer join channel
                        socket.join(channel);

                        socket.broadcast.to(channel).emit('broadcast: joined', 'S --> \
                                broadcast(): client ' + socket.id + ' joined channel ' + channel);
                        console.log("Second client joining...");
                } else { // max two clients
                		console.log("Channel full!");
                    socket.emit('full', channel);
                }
  1. 通知通道发起者新加入请求的到来。
  2. 允许新客户进入已经存在的房间。
  3. 更新(通过广播消息)频道启动程序有关加入操作成功完成的信息,使其准备开始新的对话。

Peer1侦听到remotePeerJoining后,向浏览器页面打印信息

//Handle 'remotePeerJoining' message
socket.on('remotePeerJoining', function (channel){
	console.log('Request to join ' + channel);
	console.log('You are the initiator!');

	div.insertAdjacentHTML( 'beforeEnd', '<p style="color:red">Time: ' + 
		(performance.now() / 1000).toFixed(3) + 
		' --> Message from server: request to join channel ' + 
		channel + '</p>'); 
});

Peer1侦听到broadcast: joined后,向浏览器页面打印信息,弹出对话框提示发送消息

//Handle 'broadcast: joined' message
socket.on('broadcast: joined', function (msg){

	div.insertAdjacentHTML( 'beforeEnd', '<p style="color:red">Time: ' + 
		(performance.now() / 1000).toFixed(3) + 
		' --> Broadcast message from server: </p>');
	div.insertAdjacentHTML( 'beforeEnd', '<p style="color:red">' + 
		msg + '</p>');

	console.log('Broadcast message from server: ' + msg);	  

	// Start chatting with remote peer:
	// 1. Get user's message
	var myMessage = prompt('Insert message to be sent to your peer:', "");

	// 2. Send to remote peer (through server)
	socket.emit('message', {
		channel: channel,
		message: myMessage});
});

如图

截屏2020-10-23 下午8.26.47 截屏2020-10-25 下午4.54.32

5.3 交换信令

Server侦听到message后,作为中介广播message

		// Handle 'message' messages
        socket.on('message', function (message) {
                log('S --> Got message: ', message);
                socket.broadcast.to(message.channel).emit('message', message.message);
        });

Peer2侦听到Server广播的message后,在界面中打印收到消息,并用response回复(发往Server)

//Handle 'message' message
socket.on('message', function (message){
	console.log('Got message from other peer: ' + message);

	div.insertAdjacentHTML( 'beforeEnd', '<p>Time: ' + 
		(performance.now() / 1000).toFixed(3) +	
		' --> Got message from other peer: </p>');
	div.insertAdjacentHTML( 'beforeEnd', '<p style="color:blue">' + 
		message + '</p>');	  

	// Send back response message:
	// 1. Get response from user
	var myResponse = prompt('Send response to other peer:', "");	  

	// 2. Send it to remote peer (through server)
	socket.emit('response', {
		channel: channel,
		message: myResponse});

});

Server侦听到response后,广播收到的消息

       // Handle 'response' messages
        socket.on('response', function (response) {
            log('S --> Got response: ', response);

            // Just forward message to the other peer
            socket.broadcast.to(response.channel).emit('response', response.message);
        });

Peer1收到Server广播的response后,判断内容是否是“Bye”:若是,则进入关闭信令信道流程;若不是,则继续发送消息。

//Handle 'response' message
socket.on('response', function (response){
	console.log('Got response from other peer: ' + response);

	div.insertAdjacentHTML( 'beforeEnd', '<p>Time: ' + 
		(performance.now() / 1000).toFixed(3) + 
		' --> Got response from other peer: </p>');
	div.insertAdjacentHTML( 'beforeEnd', '<p style="color:blue">' + 
		response + '</p>');

	// Keep on chatting
	var chatMessage = prompt('Keep on chatting. Write "Bye" to quit conversation', "");

	// User wants to quit conversation: send 'Bye' to remote party
	if(chatMessage == "Bye"){
		div.insertAdjacentHTML( 'beforeEnd', '<p>Time: ' + 
			(performance.now() / 1000).toFixed(3) + 
			' --> Sending "Bye" to server...</p>');
		console.log('Sending "Bye" to server');

		socket.emit('Bye', channel);

		div.insertAdjacentHTML( 'beforeEnd', '<p>Time: ' + 
			(performance.now() / 1000).toFixed(3) + 
			' --> Going to disconnect...</p>');
		console.log('Going to disconnect...');

		// Disconnect from server
		socket.disconnect();
	}else{
		// Keep on going: send response back to remote party (through server)
		socket.emit('response', {
			channel: channel,
			message: chatMessage});
	}
});

5.4 关闭信令信道

关闭过程实际上是通过在两个浏览器之一中插入 Bye 消息触发的。收到“Bye”后,先在界面中打印相关内容,然后向Server发送Bye事件,同时执行socket.disconnect()与Server断开。

// User wants to quit conversation: send 'Bye' to remote party
	if(chatMessage == "Bye"){
		div.insertAdjacentHTML( 'beforeEnd', '<p>Time: ' + 
			(performance.now() / 1000).toFixed(3) + 
			' --> Sending "Bye" to server...</p>');
		console.log('Sending "Bye" to server');

		socket.emit('Bye', channel);

		div.insertAdjacentHTML( 'beforeEnd', '<p>Time: ' + 
			(performance.now() / 1000).toFixed(3) + 
			' --> Going to disconnect...</p>');
		console.log('Going to disconnect...');

		// Disconnect from server
		socket.disconnect();
	}

Server侦听到Bye后,广播Bye,同时关闭Server一侧的socket。

// Handle 'Bye' messages
        socket.on('Bye', function(channel){
        	// Notify other peer
        	socket.broadcast.to(channel).emit('Bye');

        	// Close socket from server's side
        	socket.disconnect();
        });

另一侧Client侦听到Bye后,向Server发送Ack,同时执行socket.disconnect(),断开与Server的连接。

//Handle 'Bye' message
socket.on('Bye', function (){
	console.log('Got "Bye" from other peer! Going to disconnect...');

	div.insertAdjacentHTML( 'beforeEnd', '<p>Time: ' + 
		(performance.now() / 1000).toFixed(3) + 
		' --> Got "Bye" from other peer!</p>');

	div.insertAdjacentHTML( 'beforeEnd', '<p>Time: ' + 
		(performance.now() / 1000).toFixed(3) + 
		' --> Sending "Ack" to server</p>');

	// Send 'Ack' back to remote party (through server)
	console.log('Sending "Ack" to server');

	socket.emit('Ack');

	// Disconnect from server
	div.insertAdjacentHTML( 'beforeEnd', '<p>Time: ' + 
		(performance.now() / 1000).toFixed(3) + 
		' --> Going to disconnect...</p>');
	console.log('Going to disconnect...');

	socket.disconnect();
});

Server侦听到Ack后,在控制台上记录了 Ack 消息的接收情况,并且该通道最终被销毁。

        // Handle 'Ack' messages
        socket.on('Ack', function () {
            console.log('Got an Ack!');
            // Close socket from server's side
        	socket.disconnect();
        });

至此,整个信令通道的通信流程完全结束。

第五章-搭建一个WebRTC系统

1. WebRTC会话完整流程

1.1 双方加入房间 + 访问本地媒体流

对等端和信令服务器建立联系

  1. Initiator 连接到服务器,并使其创建信令通道。
  2. Initiator(在获得用户同意后)可以访问用户的媒体。
  3. Joiner 连接到服务器并加入频道。
  4. 当 Joiner 还可以访问本地用户的媒体时,将通过服务器将一条消息发送给 Initiator ,触发协商过程进入下一阶段
完整WebRTC呼叫流程-1-双方加入房间+添加本地媒体流

1.2 Initiator协商过程 + Joined处理offer

完整WebRTC会话过程-2-Initiator加入+Joined处理offer

1.3 ICE候选人交换

信令服务器的主要任务之一是使发起方和连接方之间的网络可达性信息能够交换,从而可以在两者之间建立媒体包流。 交互式连接建立(ICE)RFC5245 技术允许对等方发现有关彼此拓扑的足够信息,从而有可能在彼此之间找到一条或多条通信路径

截屏2020-11-25 下午8.07.21

此类信息由与每个 RTCPeerConnection 对象关联的 ICE 代理在本地收集。 ICE 代理负责:

  • 收集候选传输地址,候选地址是或许可用于接收媒体以建立对等连接的IP地址和端口
截屏2020-11-25 下午8.48.33
  • 优先级排序

    主机候选项的优先级最高,其次是反射地址,最后是中继候选项。

  • 在同级之间执行连接检查

  • 发送连接保持活动


1.3.1 收集传输地址

设置会话描述(本地或远程)后,本地 ICE 代理会自动开始发现本地对等方所有可能候选者的过程

  1. ICE 代理向操作系统查询本地 IP 地址
  2. 如果已配置,它将查询外部 STUN 服务器以检索对等方的公共 IP 地址和端口元组。
  3. 如果已配置,则代理还将 TURN 服务器用作最后的手段。 如果对等连接检查失败,则媒体流将通过 TURN 服务器进行中继

每当发现新的候选对象(即IP,port tuple)时,ICE 代理就会自动将其注册到 RTCPeerConnection 对象,并通过回调函数(onIceCandidate)通知应用程序。 该应用程序可以决定在发现每个候选者之后(Trickle ICE)尽快将其转移到远程方,或者决定等待 ICE 收集阶段完成,然后立即发送所有候选者(标准ICE)。

只要浏览器引发 IceCandidate 事件(因为已经收集了一个新的 ICE 候选对象),就会激活 handleIceCandidate() 处理程序。 此处理程序将检索到的候选者包装在专用候选者消息中,该消息将通过服务器发送给远程方


1.3.2 连接检查

此时,每个 ICE 代理都有其候选人和其同行候选人的完整列表。 将它们配对。 为了查看哪个对有效,每个代理计划安排一系列优先检查:首先检查本地 IP 地址,然后检查公共 IP 地址,最后使用 TURN。

如果一对候选对象中的一个可行,则存在用于点对点连接的路由路径。 相反,如果所有候选项均失败,则 RTCPeerConnection 被标记为失败,或者连接回退到 TURN 中继服务器以建立连接。


1.3.3 连接保持

建立连接后,ICE 代理会继续向其他对等方发出定期的 STUN 请求。 这用作连接保持活动状态。

完整WebRTC会话流程-3-ICE协商

1.4 Joined的answer

完整WebRTC会话流程-4-Joined的answer

1.5 开始点对点通信

两个对等方已成功交换会话描述和网络可达性信息。 借助信令服务器的中介,已经正确设置和配置了两个 PeerConnection 对象。 如 图5-16 所示,双向多媒体通信通道现在可用作两个浏览器之间的直接传输工具。 现在服务器已完成其任务,并且此后将被两个通信对等方完全绕开。

完整WebRTC会话流程-4-P2P通信

2.快速浏览 Chrome WebRTC 内部工具

使用支持 WebRTC 的网络应用程序时,可以通过打开一个新标签页并在该标签页的位置栏中输入 chrome://webrtc-internals/ 来监视其状态。

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值