以高度抽象方式描述与WebRTC相关的完整开发周期;
说明如何编写代码以查询、获得对本地多媒体资源(如音频和视频设备)的访问,并在HTML5中显示;
将获得的媒体流与PeerConnection对象相关联(对等方的逻辑连接);
讨论与建立正确的信令通道有关的各种选择;
构建一个完整的WebRTC应用程序
PeerConnection
允许媒体直接在浏览器之间流动,而无需任何中间服务器当两种浏览器都运行相同的 Web 应用程序(从相同的网页下载)时,架构变为三角形:
服务器-浏览器交互 | 协议栈 |
---|---|
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的对等方接收媒体。
主要概念 | 具体解释 |
---|---|
MediaStream |
音视频数据流的抽象表示(本地和远程音频和视频的获取和管理) |
PeerConnection |
允许两个用户在浏览器之间直接通信,表示与远程对等点的关联(连接管理) |
DataChannel |
提供通用传输服务,允许Web浏览器以双向对等方式交换通用数据(管理任意数据) |
**WebRTC API接口参考:**https://developer.mozilla.org/zh-CN/docs/Web/API/WebRTC_API
摘要:利用浏览器提供的API获取本地媒体流并在浏览器内播放。
MediaStream
W3C Media Capture and Streams 文档定义了一组 JavaScript API,这些API使应用程序能够从平台请求音频和视频流,以及操纵和处理流数据。
单个MediaStream
可以包含零个或多个轨道。 每个轨道都有一个对应的 MediaStreamTrack
对象,该对象代表用户代理中的特定媒体源。 MediaStream
中的所有轨道在渲染时进行同步。MediaStreamTrack
表示包含一个或多个通道的内容,其中,通道之间具有定义的已知的关系。
getUserMedia()
/ createObjectUrl()
W3C Media Capture Streams API 定义了两种方法 getUserMedia()
和 createObjectUrl()
。
方法 | 用法 | 解释 |
---|---|---|
getUserMedia() |
getUserMedia(constraints, successCallback, errorCallback) | 通过指定一组(强制或可选)成功和失败的回调函数,Web 开发人员可以访问本地设备媒体(当前是音频和/或视频); 提示用户许可使用其网络摄像头或其他视频或音频输入 |
createObjectUrl() |
createObjectURL(stream) | 指示浏览器创建和管理与本地文件或二进制对象(blob)关联的唯一URL |
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>getUserMedia very simple demotitle>
head>
<body>
<div id="mainDiv">
<h1><code>getUserMedia()code> very simple demoh1>
<p>With this example, we simply call <code>getUserMedia()code> and display the received stream inside an HTML5 <video> elementp>
<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文件在浏览器中打开,页面显示如下:
给予许可后,successCallback回调就会被调用,由摄像头收集到的视频将在界面中播放:
在console输入Stream检查:
浏览器提供了从源(sources)到接收器(sinks)的媒体管道(pipeline)。在浏览器中,接收器是 ,
和
标记。
浏览器中的来源可分为静态源和动态源:
源 | 特点 | |
---|---|---|
静态源 | 物理网络摄像头,麦克风 来自用户硬盘驱动器的本地视频或音频文件,网络资源或静态图像 |
来源产生的媒体通常不会随时间变化; 向用户显示此类源的接收器(实际标签本身)具有用于控制源内容的各种控件。 |
动态源 | 麦克风和相机 | 由getUserMedia() API方法添加;来源的特性可能会根据应用需求而变化 |
约束用于限制MediaStream
轨道源上允许的可变性范围,由对象的属性或特征以及可能的值的集和组成,可以将值指定为范围或枚举。
在MediaTrackConstraint
对象中定义了特定的约束,一个约束用于音频,另一个约束用于视频。此对象中的属性的类型为ConstraintLong
, ConstraintBoolean
, ConstraintDouble
或ConstraintDOMString
。这些可以是特定值(例如,数字,布尔值或字符串),范围(具有最小值和最大值的LongRange
或DoubleRange
)或具有ideal
或exact
定义的对象。
exact
,则仅返回与该约束完全匹配的媒体流。 getUserMedia()
调用允许在首次获取轨道时应用一组初始约束(例如,设置视频分辨率的值)(仅Chrome);
此外,可以在初始化后通过专用的约束API添加约束。
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>getUserMedia() and constraintstitle>
head>
<body>
<div id="mainDiv">
<h1><code>getUserMedia()code>: playing with video constraintsh1>
<p>Click one of the below buttons to change video resolution...p>
<div id="buttons">
<button id="qvga">320x240button>
<button id="vga">640x480button>
<button id="hd">1280x960button>
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
// 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);
}
界面如下:
320x240 | 680x480 |
---|---|
点击1280x960按钮时抛出错误,OverConstrainedError
,判断是摄像头无法采集1280x960分辨率视频。
OverConstrainedError
[无法满足要求错误]指定的要求无法被设备满足,此异常是一个类型为
OverconstrainedError
的对象,拥有一个constraint
属性,这个属性包含了当前无法被满足的constraint
对象,还拥有一个message
属性,包含了阅读友好的字符串用来说明情况。
摘要:分析 WebRTC 1.0 API,其主要目的是允许向其他浏览器发送和接收媒体(暂不涉及信令通道)。
需要一种机制来适当地协调实时通信,并允许对等方交换控制消息。 在 WebRTC 内部尚未定义这种机制(通常称为信令),因此不属于 RTCPeerConnection API 规范。
浏览器之间的互操作性由Web服务器使用下载的JavaScript代码确保。由此,开发人员可以使用常用的消息传递协议(SIP、XMPP、Jingle…)来实现信令通道,也可以自行设计专有信令机制。
简而言之,RTCPeerConnection API负责浏览器之间P2P通信,是MediaPath(本章内容);
SIP、XMPP、Jingle等是用于Web服务器之间交换信令,是Signaling Path。
在正确交换和协商所有上述信息之前,WebRTC 对等方之间无法传输任何数据。
在第3章中,为简化问题,将通过某种方式在单台计算机上模拟对等行为来实现此目标。这意味着我们暂时将绕过信令通道设置阶段,并让上述三个步骤(会话管理,网络配置和多媒体功能交换)在单个计算机上进行。
在第5章中,我们将通过展示本地场景如何在两个启用 WebRTC 的对等点之间引入真正的信令通道,来最终向 WebRTC 建筑中添加最后一块砖。
RTCPeerConnection
接口代表一个由本地计算机到远端的WebRTC连接。该接口提供了创建,保持,监控,关闭连接的方法的实现,是每个浏览器之间点对点连接的核心。
STUN(用户数据报协议[UDP]简单穿越网络地址转换器[NAT])服务器允许所有的NAT客户终端(如防火墙后边的计算机)与位于局区域网以外的远端实现媒体交流。
通过STUN服务器,客户终端可以了解他们的:
STUN P2P不成功后,使用数据中转。
一种数据传输协议,是STUN/RFC5389的一个拓展。允许在 TCP 或 UDP 的连线上在线跨域 NAT 或防火墙
为了保证通信能够建立,我们可以在没办法的情况下用保证成功的中继方法(Relaying), 虽然使用中继会对服务器负担加重,而且也算不上P2P,但是至少保证了最坏情况下信道的通畅,从而不至于受NAT类型的限制。
SDP(Session Description Protocol)是一种通用的会话描述协议,主要用来描述多媒体会话,用途包括会话声明、会话邀请、会话初始化等。
WebRTC主要在连接建立阶段用到SDP,连接双方通过信令服务交换会话信息,包括音视频编解码器(codec)、主机候选地址、网络传输协议等。
Datagram Transport Layer Security数据报传输层安全性(DTLS)是一种通信协议,旨在保护数据隐私并防止窃听和篡改。 它基于传输层安全性(TLS)协议,该协议为基于计算机的通信网络提供安全性。 DTSL和TLS之间的主要区别在于DTLS使用UDP,而TLS使用TCP。 它可用于Web浏览,邮件,即时消息传递和VoIP。 DTSL是与SRTP一起用于WebRTC技术的安全协议之一。
SRTP(SecureReal-time Transport Protocol) 安全实时传输协议,SRTP是在实时传输协议(Real-time Transport Protocol)基础上所定义的一个协议,旨在为单播和多播应用程序中的实时传输协议的数据提供加密、消息认证、完整性保证和重放保护安全实时传输协议。
MediaStream
添加到 PeerConnection
调用 new RTCPeerConnection(configuration)
会创建一个 RTCPeerConnection
对象,该对象是两个用户/浏览器之间通信通道的抽象,可以为特定的 MediaStream
输入或输出,如图所示。
例3-1 本地 RTCPeerConnection
用法示例
页面设计
<html>
<head>
<title>Local PeerConnection() exampletitle>
head>
<body>
<table border="1" width="100%">
<tr>
<th>Local videoth>
<th>'Remote' videoth>
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">Startbutton>
<button id="callButton">Callbutton>
<button id="hangupButton">Hang Upbutton>
div>
td>
<td>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);
}
}
PeerConnection
点对点数据 API 使 Web应用程序可以以点对点方式发送和接收通用应用程序数据。 用于发送和接收数据的 API 汲取了 WebSocket 的启发。
和上文2.2实验的区别是:上文实验直接将本地Stream作为远端Stream,不涉及数据流的传输。本实验添加了用于通用数据流的数据通道。
例3-2 本地数据通道用法示例
页面设计
<html>
<head>
<title>DataChannel simple exampletitle>
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">Startbutton>
<button id="sendButton">Sendbutton>
<button id="closeButton">Stopbutton>
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);
}
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 的通信会话感兴趣的任何一对对等端之间创建适当的信令通道。
角色 | 说明 |
---|---|
通道发起者 | 对等方,首先主动与远端建立专用的通信通道 |
信令服务器 | 管理通道创建并充当消息中继节点 |
频道加入者 | 远程方加入已存在的频道 |
这个想法是在接收到启动器发出的特定请求后,服务器根据需要创建通道。 第二个对等方加入频道后,就可以开始对话。 消息交换始终通过服务器进行,该服务器基本上充当透明的中继节点。 当对等方之一决定退出正在进行的对话时,它将在断开连接之前向服务器发出一条临时消息(图中称为Bye)。 服务器将该消息分派给远程方,在将确认发送回服务器后,远程方也将断开连接。 收到确认后,最终会触发服务器端的通道重置过程,从而使总体方案恢复到其原始配置。
node simpleNodeServer_OK.js
此时浏览器界面显示
Client-A显示
此时若有第三个页面想要加入,显示
Client-A | Client-B |
---|---|
命令行输出如下
客户机端 | 服务器端(最多2个客户端相连) |
---|---|
(1) 允许客户端连接到服务器(通过socket.io 库) (2) 提示用户输入要加入的频道的名称 (3) 将创建或加入请求发送到服务器 (4) 开始初步处理服务器发送的事件。 |
(1) 创建服务实例监听8181扩展 (2) 要求创建房间的第一个客户端是通道启动器 (3) 允许第二个到达的客户端加入新创建的频道 (4) 所有其他客户端均被拒绝进入会议室(并因此收到该事件的通知) |
序列图
socket.emit(event,data,[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方法向所有其他客户端广播消息。
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
后完成以下操作:
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 页面中包含的 Time: ' Time: ' 如图: 和上文类似,Peer2加载html界面 -> 连接至服务器 -> 输入channel名; Client向Server发 Server侦听获取channel后, 由于这次请求方不是发起方,因此Server的行为将由以下代码段驱动: Peer1侦听到 Time: ' Peer1侦听到 Time: ' ' 如图 Server侦听到 Peer2侦听到Server广播的 Time: ' ' Server侦听到 Peer1收到Server广播的 Time: ' ' Time: ' Time: ' 关闭过程实际上是通过在两个浏览器之一中插入 Time: ' Time: ' Server侦听到 另一侧Client侦听到 Time: ' Time: ' Time: ' Server侦听到 至此,整个信令通道的通信流程完全结束。 对等端和信令服务器建立联系 信令服务器的主要任务之一是使发起方和连接方之间的网络可达性信息能够交换,从而可以在两者之间建立媒体包流。 交互式连接建立(ICE)RFC5245 技术允许对等方发现有关彼此拓扑的足够信息,从而有可能在彼此之间找到一条或多条通信路径。 此类信息由与每个 优先级排序 主机候选项的优先级最高,其次是反射地址,最后是中继候选项。 在同级之间执行连接检查 发送连接保持活动 设置会话描述(本地或远程)后,本地 ICE 代理会自动开始发现本地对等方所有可能候选者的过程: 每当发现新的候选对象(即IP,port tuple)时,ICE 代理就会自动将其注册到 只要浏览器引发 此时,每个 ICE 代理都有其候选人和其同行候选人的完整列表。 将它们配对。 为了查看哪个对有效,每个代理计划安排一系列优先检查:首先检查本地 IP 地址,然后检查公共 IP 地址,最后使用 TURN。 如果一对候选对象中的一个可行,则存在用于点对点连接的路由路径。 相反,如果所有候选项均失败,则 建立连接后,ICE 代理会继续向其他对等方发出定期的 STUN 请求。 这用作连接保持活动状态。 两个对等方已成功交换会话描述和网络可达性信息。 借助信令服务器的中介,已经正确设置和配置了两个 使用支持 WebRTC 的网络应用程序时,可以通过打开一个新标签页并在该标签页的位置栏中输入 //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', '
5.2 加入信令信道
create or join
事件,并传递上一步获取到的频道名;...
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);
}
remotePeerJoining
后,向浏览器页面打印信息//Handle 'remotePeerJoining' message
socket.on('remotePeerJoining', function (channel){
console.log('Request to join ' + channel);
console.log('You are the initiator!');
div.insertAdjacentHTML( 'beforeEnd', '
broadcast: joined
后,向浏览器页面打印信息,弹出对话框提示发送消息//Handle 'broadcast: joined' message
socket.on('broadcast: joined', function (msg){
div.insertAdjacentHTML( 'beforeEnd', '
5.3 交换信令
message
后,作为中介广播message // Handle 'message' messages
socket.on('message', function (message) {
log('S --> Got message: ', message);
socket.broadcast.to(message.channel).emit('message', message.message);
});
message
后,在界面中打印收到消息,并用response
回复(发往Server)//Handle 'message' message
socket.on('message', function (message){
console.log('Got message from other peer: ' + message);
div.insertAdjacentHTML( 'beforeEnd', '
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);
});
response
后,判断内容是否是“Bye”:若是,则进入关闭信令信道流程;若不是,则继续发送消息。//Handle 'response' message
socket.on('response', function (response){
console.log('Got response from other peer: ' + response);
div.insertAdjacentHTML( 'beforeEnd', '
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', '
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();
});
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', '
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.2 Initiator协商过程 + Joined处理offer
1.3 ICE候选人交换
RTCPeerConnection
对象关联的 ICE 代理在本地收集。 ICE 代理负责:
1.3.1 收集传输地址
RTCPeerConnection
对象,并通过回调函数(onIceCandidate
)通知应用程序。 该应用程序可以决定在发现每个候选者之后(Trickle ICE)尽快将其转移到远程方,或者决定等待 ICE 收集阶段完成,然后立即发送所有候选者(标准ICE)。IceCandidate
事件(因为已经收集了一个新的 ICE 候选对象),就会激活 handleIceCandidate()
处理程序。 此处理程序将检索到的候选者包装在专用候选者消息中,该消息将通过服务器发送给远程方
1.3.2 连接检查
RTCPeerConnection
被标记为失败,或者连接回退到 TURN 中继服务器以建立连接。
1.3.3 连接保持
1.4 Joined的answer
1.5 开始点对点通信
PeerConnection
对象。 如 图5-16 所示,双向多媒体通信通道现在可用作两个浏览器之间的直接传输工具。 现在服务器已完成其任务,并且此后将被两个通信对等方完全绕开。2.快速浏览 Chrome WebRTC 内部工具
chrome://webrtc-internals/
来监视其状态。