您的位置:首页 > 理论基础 > 计算机网络

WebRTC开发基础(WebRTC入门系列2:RTCPeerConnection)

2016-09-18 17:39 399 查看
RTCPeerConnection的作用是在浏览器之间建立数据的“点对点”(peer to peer)通信.




 

使用WebRTC的编解码器和协议做了大量的工作,方便了开发者,使实时通信成为可能,甚至在不可靠的网络,

比如这些如果在voip体系下开发工作量将非常大,而用webRTC的js开发者则不用考虑这些,举几个例子:

丢包隐藏

回声抵消

带宽自适应

动态抖动缓冲

自动增益控制

噪声抑制与抑制

图像清洗

 

不同客户端之间的音频/视频传递,是不用通过服务器的。但是,两个客户端之间建立信令联系,需要通过服务器。这个和XMPP的Jingle会话很类似。

服务器主要转递两种数据。

通信内容的元数据:打开/关闭对话(session)的命令、媒体文件的元数据(编码格式、媒体类型和带宽)等。

网络通信的元数据:IP地址、NAT网络地址翻译和防火墙等。

WebRTC协议没有规定与服务器的信令通信方式,因此可以采用各种方式,比如WebSocket。通过服务器,两个客户端按照Session Description Protocol(SDP协议)交换双方的元数据。

 

本地和远端通讯的过程有些像电话,比如张三正在试着打电话给李四,详细机制:

张三创造了一个RTCPeerConnection 对象。

张三通过RTCPeerConnection createOffer()方法创造了一个offer(SDP会话描述) 。

张三通过他创建的offer调用setLocalDescription(),保存本地会话描述。

张三发送信令给李四。

李四接通带有李四offer的电话,调用setRemoteDescription() ,李四的RTCPeerConnection知道张三的设置(张三的本地描述到了李四这里,就成了李四的远程会话描述)。

李四调用createAnswer(),将李四的本地会话描述(local session description)成功回调。

李四调用setLocalDescription()设置他自己的本地局部描述。

李四发送应答信令answer给张三。

张三将李四的应答answer用setRemoteDescription()保存为远程会话描述(李四的remote session description)。

SDP详解:

https://datatracker.ietf.org/doc/draft-nandakumar-rtcweb-sdp/?include_text=1

bitbucket的一步一步实验,动手代码

https://bitbucket.org/webrtc/codelab

WebSocket简介(用于信令通信)

https://dzone.com/refcardz/html5-websocket

http://www.infoq.com/articles/Web-Sockets-Proxy-Servers

信令服务或开源webRTC框架

https://easyrtc.com/products/easyrtc-opensource

https://github.com/priologic/easyrtc

https://github.com/andyet/signalmaster

如果您一行代码都不想写,可以看看 vLineOpenTok and Asterisk.

 

这里有一个单页应用程序。本地和远程的视频在一个网页,RTCPeerConnection objects 直接交换数据和消息。

https://webrtc.github.io/samples/src/content/peerconnection/pc1/

HTML代码:

<!DOCTYPE html>
<html>
<head>
......
</head>

<body>

<div id="container">

<h1><a href="//webrtc.github.io/samples/" title="WebRTC samples homepage">WebRTC samples</a> <span>Peer connection</span></h1>

<video id="localVideo" autoplay muted></video>
<video id="remoteVideo" autoplay></video>

<div>
<button id="startButton">Start</button>
<button id="callButton">Call</button>
<button id="hangupButton">Hang Up</button>
</div>

</div>

<script src="../../../js/adapter.js"></script>
<script src="../../../js/common.js"></script>
<script src="js/main.js"></script>

<script src="../../../js/lib/ga.js"></script>
</body>
</html>


这里是主要视图:

<video id="localVideo" autoplay muted></video>
<video id="remoteVideo" autoplay></video>

<div>
<button id="startButton">Start</button>
<button id="callButton">Call</button>
<button id="hangupButton">Hang Up</button>
</div>

 

适配器js代码:





/*
*  Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
*
*  Use of this source code is governed by a BSD-style license
*  that can be found in the LICENSE file in the root of the source
*  tree.
*/

/* More information about these options at jshint.com/docs/options */
/* jshint browser: true, camelcase: true, curly: true, devel: true,
eqeqeq: true, forin: false, globalstrict: true, node: true,
quotmark: single, undef: true, unused: strict */
/* global mozRTCIceCandidate, mozRTCPeerConnection, Promise,
mozRTCSessionDescription, webkitRTCPeerConnection, MediaStreamTrack */
/* exported trace,requestUserMedia */

'use strict';

var getUserMedia = null;
var attachMediaStream = null;
var reattachMediaStream = null;
var webrtcDetectedBrowser = null;
var webrtcDetectedVersion = null;
var webrtcMinimumVersion = null;
var webrtcUtils = {
log: function() {
// suppress console.log output when being included as a module.
if (typeof module !== 'undefined' ||
typeof require === 'function' && typeof define === 'function') {
return;
}
console.log.apply(console, arguments);
},
extractVersion: function(uastring, expr, pos) {
var match = uastring.match(expr);
return match && match.length >= pos && parseInt(match[pos]);
}
};

function trace(text) {
// This function is used for logging.
if (text[text.length - 1] === '\n') {
text = text.substring(0, text.length - 1);
}
if (window.performance) {
var now = (window.performance.now() / 1000).toFixed(3);
webrtcUtils.log(now + ': ' + text);
} else {
webrtcUtils.log(text);
}
}

if (typeof window === 'object') {
if (window.HTMLMediaElement &&
!('srcObject' in window.HTMLMediaElement.prototype)) {
// Shim the srcObject property, once, when HTMLMediaElement is found.
Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', {
get: function() {
// If prefixed srcObject property exists, return it.
// Otherwise use the shimmed property, _srcObject
return 'mozSrcObject' in this ? this.mozSrcObject : this._srcObject;
},
set: function(stream) {
if ('mozSrcObject' in this) {
this.mozSrcObject = stream;
} else {
// Use _srcObject as a private property for this shim
this._srcObject = stream;
// TODO: revokeObjectUrl(this.src) when !stream to release resources?
this.src = URL.createObjectURL(stream);
}
}
});
}
// Proxy existing globals
getUserMedia = window.navigator && window.navigator.getUserMedia;
}

// Attach a media stream to an element.
attachMediaStream = function(element, stream) {
element.srcObject = stream;
};

reattachMediaStream = function(to, from) {
to.srcObject = from.srcObject;
};

if (typeof window === 'undefined' || !window.navigator) {
webrtcUtils.log('This does not appear to be a browser');
webrtcDetectedBrowser = 'not a browser';
} else if (navigator.mozGetUserMedia && window.mozRTCPeerConnection) {
webrtcUtils.log('This appears to be Firefox');

webrtcDetectedBrowser = 'firefox';

// the detected firefox version.
webrtcDetectedVersion = webrtcUtils.extractVersion(navigator.userAgent,
/Firefox\/([0-9]+)\./, 1);

// the minimum firefox version still supported by adapter.
webrtcMinimumVersion = 31;

// The RTCPeerConnection object.
window.RTCPeerConnection = function(pcConfig, pcConstraints) {
if (webrtcDetectedVersion < 38) {
// .urls is not supported in FF < 38.
// create RTCIceServers with a single url.
if (pcConfig && pcConfig.iceServers) {
var newIceServers = [];
for (var i = 0; i < pcConfig.iceServers.length; i++) {
var server = pcConfig.iceServers[i];
if (server.hasOwnProperty('urls')) {
for (var j = 0; j < server.urls.length; j++) {
var newServer = {
url: server.urls[j]
};
if (server.urls[j].indexOf('turn') === 0) {
newServer.username = server.username;
newServer.credential = server.credential;
}
newIceServers.push(newServer);
}
} else {
newIceServers.push(pcConfig.iceServers[i]);
}
}
pcConfig.iceServers = newIceServers;
}
}
return new mozRTCPeerConnection(pcConfig, pcConstraints); // jscs:ignore requireCapitalizedConstructors
};

// The RTCSessionDescription object.
if (!window.RTCSessionDescription) {
window.RTCSessionDescription = mozRTCSessionDescription;
}

// The RTCIceCandidate object.
if (!window.RTCIceCandidate) {
window.RTCIceCandidate = mozRTCIceCandidate;
}

// getUserMedia constraints shim.
getUserMedia = function(constraints, onSuccess, onError) {
var constraintsToFF37 = function(c) {
if (typeof c !== 'object' || c.require) {
return c;
}
var require = [];
Object.keys(c).forEach(function(key) {
if (key === 'require' || key === 'advanced' || key === 'mediaSource') {
return;
}
var r = c[key] = (typeof c[key] === 'object') ?
c[key] : {ideal: c[key]};
if (r.min !== undefined ||
r.max !== undefined || r.exact !== undefined) {
require.push(key);
}
if (r.exact !== undefined) {
if (typeof r.exact === 'number') {
r.min = r.max = r.exact;
} else {
c[key] = r.exact;
}
delete r.exact;
}
if (r.ideal !== undefined) {
c.advanced = c.advanced || [];
var oc = {};
if (typeof r.ideal === 'number') {
oc[key] = {min: r.ideal, max: r.ideal};
} else {
oc[key] = r.ideal;
}
c.advanced.push(oc);
delete r.ideal;
if (!Object.keys(r).length) {
delete c[key];
}
}
});
if (require.length) {
c.require = require;
}
return c;
};
if (webrtcDetectedVersion < 38) {
webrtcUtils.log('spec: ' + JSON.stringify(constraints));
if (constraints.audio) {
constraints.audio = constraintsToFF37(constraints.audio);
}
if (constraints.video) {
constraints.video = constraintsToFF37(constraints.video);
}
webrtcUtils.log('ff37: ' + JSON.stringify(constraints));
}
return navigator.mozGetUserMedia(constraints, onSuccess, onError);
};

navigator.getUserMedia = getUserMedia;

// Shim for mediaDevices on older versions.
if (!navigator.mediaDevices) {
navigator.mediaDevices = {getUserMedia: requestUserMedia,
addEventListener: function() { },
removeEventListener: function() { }
};
}
navigator.mediaDevices.enumerateDevices =
navigator.mediaDevices.enumerateDevices || function() {
return new Promise(function(resolve) {
var infos = [
{kind: 'audioinput', deviceId: 'default', label: '', groupId: ''},
{kind: 'videoinput', deviceId: 'default', label: '', groupId: ''}
];
resolve(infos);
});
};

if (webrtcDetectedVersion < 41) {
// Work around http://bugzil.la/1169665 var orgEnumerateDevices =
navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices);
navigator.mediaDevices.enumerateDevices = function() {
return orgEnumerateDevices().then(undefined, function(e) {
if (e.name === 'NotFoundError') {
return [];
}
throw e;
});
};
}
} else if (navigator.webkitGetUserMedia && window.webkitRTCPeerConnection) {
webrtcUtils.log('This appears to be Chrome');

webrtcDetectedBrowser = 'chrome';

// the detected chrome version.
webrtcDetectedVersion = webrtcUtils.extractVersion(navigator.userAgent,
/Chrom(e|ium)\/([0-9]+)\./, 2);

// the minimum chrome version still supported by adapter.
webrtcMinimumVersion = 38;

// The RTCPeerConnection object.
window.RTCPeerConnection = function(pcConfig, pcConstraints) {
// Translate iceTransportPolicy to iceTransports,
// see https://code.google.com/p/webrtc/issues/detail?id=4869 if (pcConfig && pcConfig.iceTransportPolicy) {
pcConfig.iceTransports = pcConfig.iceTransportPolicy;
}

var pc = new webkitRTCPeerConnection(pcConfig, pcConstraints); // jscs:ignore requireCapitalizedConstructors
var origGetStats = pc.getStats.bind(pc);
pc.getStats = function(selector, successCallback, errorCallback) { // jshint ignore: line
var self = this;
var args = arguments;

// If selector is a function then we are in the old style stats so just
// pass back the original getStats format to avoid breaking old users.
if (arguments.length > 0 && typeof selector === 'function') {
return origGetStats(selector, successCallback);
}

var fixChromeStats = function(response) {
var standardReport = {};
var reports = response.result();
reports.forEach(function(report) {
var standardStats = {
id: report.id,
timestamp: report.timestamp,
type: report.type
};
report.names().forEach(function(name) {
standardStats[name] = report.stat(name);
});
standardReport[standardStats.id] = standardStats;
});

return standardReport;
};

if (arguments.length >= 2) {
var successCallbackWrapper = function(response) {
args[1](fixChromeStats(response));
};

return origGetStats.apply(this, [successCallbackWrapper, arguments[0]]);
}

// promise-support
return new Promise(function(resolve, reject) {
if (args.length === 1 && selector === null) {
origGetStats.apply(self, [
function(response) {
resolve.apply(null, [fixChromeStats(response)]);
}, reject]);
} else {
origGetStats.apply(self, [resolve, reject]);
}
});
};

return pc;
};

// add promise support
['createOffer', 'createAnswer'].forEach(function(method) {
var nativeMethod = webkitRTCPeerConnection.prototype[method];
webkitRTCPeerConnection.prototype[method] = function() {
var self = this;
if (arguments.length < 1 || (arguments.length === 1 &&
typeof(arguments[0]) === 'object')) {
var opts = arguments.length === 1 ? arguments[0] : undefined;
return new Promise(function(resolve, reject) {
nativeMethod.apply(self, [resolve, reject, opts]);
});
} else {
return nativeMethod.apply(this, arguments);
}
};
});

['setLocalDescription', 'setRemoteDescription',
'addIceCandidate'].forEach(function(method) {
var nativeMethod = webkitRTCPeerConnection.prototype[method];
webkitRTCPeerConnection.prototype[method] = function() {
var args = arguments;
var self = this;
return new Promise(function(resolve, reject) {
nativeMethod.apply(self, [args[0],
function() {
resolve();
if (args.length >= 2) {
args[1].apply(null, []);
}
},
function(err) {
reject(err);
if (args.length >= 3) {
args[2].apply(null, [err]);
}
}]
);
});
};
});

// getUserMedia constraints shim.
var constraintsToChrome = function(c) {
if (typeof c !== 'object' || c.mandatory || c.optional) {
return c;
}
var cc = {};
Object.keys(c).forEach(function(key) {
if (key === 'require' || key === 'advanced' || key === 'mediaSource') {
return;
}
var r = (typeof c[key] === 'object') ? c[key] : {ideal: c[key]};
if (r.exact !== undefined && typeof r.exact === 'number') {
r.min = r.max = r.exact;
}
var oldname = function(prefix, name) {
if (prefix) {
return prefix + name.charAt(0).toUpperCase() + name.slice(1);
}
return (name === 'deviceId') ? 'sourceId' : name;
};
if (r.ideal !== undefined) {
cc.optional = cc.optional || [];
var oc = {};
if (typeof r.ideal === 'number') {
oc[oldname('min', key)] = r.ideal;
cc.optional.push(oc);
oc = {};
oc[oldname('max', key)] = r.ideal;
cc.optional.push(oc);
} else {
oc[oldname('', key)] = r.ideal;
cc.optional.push(oc);
}
}
if (r.exact !== undefined && typeof r.exact !== 'number') {
cc.mandatory = cc.mandatory || {};
cc.mandatory[oldname('', key)] = r.exact;
} else {
['min', 'max'].forEach(function(mix) {
if (r[mix] !== undefined) {
cc.mandatory = cc.mandatory || {};
cc.mandatory[oldname(mix, key)] = r[mix];
}
});
}
});
if (c.advanced) {
cc.optional = (cc.optional || []).concat(c.advanced);
}
return cc;
};

getUserMedia = function(constraints, onSuccess, onError) {
if (constraints.audio) {
constraints.audio = constraintsToChrome(constraints.audio);
}
if (constraints.video) {
constraints.video = constraintsToChrome(constraints.video);
}
webrtcUtils.log('chrome: ' + JSON.stringify(constraints));
return navigator.webkitGetUserMedia(constraints, onSuccess, onError);
};
navigator.getUserMedia = getUserMedia;

if (!navigator.mediaDevices) {
navigator.mediaDevices = {getUserMedia: requestUserMedia,
enumerateDevices: function() {
return new Promise(function(resolve) {
var kinds = {audio: 'audioinput', video: 'videoinput'};
return MediaStreamTrack.getSources(function(devices) {
resolve(devices.map(function(device) {
return {label: device.label,
kind: kinds[device.kind],
deviceId: device.id,
groupId: ''};
}));
});
});
}};
}

// A shim for getUserMedia method on the mediaDevices object.
// TODO(KaptenJansson) remove once implemented in Chrome stable.
if (!navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia = function(constraints) {
return requestUserMedia(constraints);
};
} else {
// Even though Chrome 45 has navigator.mediaDevices and a getUserMedia
// function which returns a Promise, it does not accept spec-style
// constraints.
var origGetUserMedia = navigator.mediaDevices.getUserMedia.
bind(navigator.mediaDevices);
navigator.mediaDevices.getUserMedia = function(c) {
webrtcUtils.log('spec:   ' + JSON.stringify(c)); // whitespace for alignment
c.audio = constraintsToChrome(c.audio);
c.video = constraintsToChrome(c.video);
webrtcUtils.log('chrome: ' + JSON.stringify(c));
return origGetUserMedia(c);
};
}

// Dummy devicechange event methods.
// TODO(KaptenJansson) remove once implemented in Chrome stable.
if (typeof navigator.mediaDevices.addEventListener === 'undefined') {
navigator.mediaDevices.addEventListener = function() {
webrtcUtils.log('Dummy mediaDevices.addEventListener called.');
};
}
if (typeof navigator.mediaDevices.removeEventListener === 'undefined') {
navigator.mediaDevices.removeEventListener = function() {
webrtcUtils.log('Dummy mediaDevices.removeEventListener called.');
};
}

// Attach a media stream to an element.
attachMediaStream = function(element, stream) {
if (webrtcDetectedVersion >= 43) {
element.srcObject = stream;
} else if (typeof element.src !== 'undefined') {
element.src = URL.createObjectURL(stream);
} else {
webrtcUtils.log('Error attaching stream to element.');
}
};
reattachMediaStream = function(to, from) {
if (webrtcDetectedVersion >= 43) {
to.srcObject = from.srcObject;
} else {
to.src = from.src;
}
};

} else if (navigator.mediaDevices && navigator.userAgent.match(
/Edge\/(\d+).(\d+)$/)) {
webrtcUtils.log('This appears to be Edge');
webrtcDetectedBrowser = 'edge';

webrtcDetectedVersion = webrtcUtils.extractVersion(navigator.userAgent,
/Edge\/(\d+).(\d+)$/, 2);

// the minimum version still supported by adapter.
webrtcMinimumVersion = 12;

if (RTCIceGatherer) {
window.RTCIceCandidate = function(args) {
return args;
};
window.RTCSessionDescription = function(args) {
return args;
};

window.RTCPeerConnection = function(config) {
var self = this;

this.onicecandidate = null;
this.onaddstream = null;
this.onremovestream = null;
this.onsignalingstatechange = null;
this.oniceconnectionstatechange = null;
this.onnegotiationneeded = null;
this.ondatachannel = null;

this.localStreams = [];
this.remoteStreams = [];
this.getLocalStreams = function() { return self.localStreams; };
this.getRemoteStreams = function() { return self.remoteStreams; };

this.localDescription = new RTCSessionDescription({
type: '',
sdp: ''
});
this.remoteDescription = new RTCSessionDescription({
type: '',
sdp: ''
});
this.signalingState = 'stable';
this.iceConnectionState = 'new';

this.iceOptions = {
gatherPolicy: 'all',
iceServers: []
};
if (config && config.iceTransportPolicy) {
switch (config.iceTransportPolicy) {
case 'all':
case 'relay':
this.iceOptions.gatherPolicy = config.iceTransportPolicy;
break;
case 'none':
// FIXME: remove once implementation and spec have added this.
throw new TypeError('iceTransportPolicy "none" not supported');
}
}
if (config && config.iceServers) {
this.iceOptions.iceServers = config.iceServers;
}

// per-track iceGathers etc
this.mLines = [];

this._iceCandidates = [];

this._peerConnectionId = 'PC_' + Math.floor(Math.random() * 65536);

// FIXME: Should be generated according to spec (guid?)
// and be the same for all PCs from the same JS
this._cname = Math.random().toString(36).substr(2, 10);
};

window.RTCPeerConnection.prototype.addStream = function(stream) {
// clone just in case we're working in a local demo
// FIXME: seems to be fixed
this.localStreams.push(stream.clone());

// FIXME: maybe trigger negotiationneeded?
};

window.RTCPeerConnection.prototype.removeStream = function(stream) {
var idx = this.localStreams.indexOf(stream);
if (idx > -1) {
this.localStreams.splice(idx, 1);
}
// FIXME: maybe trigger negotiationneeded?
};

// SDP helper from sdp-jingle-json with modifications.
window.RTCPeerConnection.prototype._toCandidateJSON = function(line) {
var parts;
if (line.indexOf('a=candidate:') === 0) {
parts = line.substring(12).split(' ');
} else { // no a=candidate
parts = line.substring(10).split(' ');
}

var candidate = {
foundation: parts[0],
component: parts[1],
protocol: parts[2].toLowerCase(),
priority: parseInt(parts[3], 10),
ip: parts[4],
port: parseInt(parts[5], 10),
// skip parts[6] == 'typ'
type: parts[7]
//generation: '0'
};

for (var i = 8; i < parts.length; i += 2) {
if (parts[i] === 'raddr') {
candidate.relatedAddress = parts[i + 1]; // was: relAddr
} else if (parts[i] === 'rport') {
candidate.relatedPort = parseInt(parts[i + 1], 10); // was: relPort
} else if (parts[i] === 'generation') {
candidate.generation = parts[i + 1];
} else if (parts[i] === 'tcptype') {
candidate.tcpType = parts[i + 1];
}
}
return candidate;
};

// SDP helper from sdp-jingle-json with modifications.
window.RTCPeerConnection.prototype._toCandidateSDP = function(candidate) {
var sdp = [];
sdp.push(candidate.foundation);
sdp.push(candidate.component);
sdp.push(candidate.protocol.toUpperCase());
sdp.push(candidate.priority);
sdp.push(candidate.ip);
sdp.push(candidate.port);

var type = candidate.type;
sdp.push('typ');
sdp.push(type);
if (type === 'srflx' || type === 'prflx' || type === 'relay') {
if (candidate.relatedAddress && candidate.relatedPort) {
sdp.push('raddr');
sdp.push(candidate.relatedAddress); // was: relAddr
sdp.push('rport');
sdp.push(candidate.relatedPort); // was: relPort
}
}
if (candidate.tcpType && candidate.protocol.toUpperCase() === 'TCP') {
sdp.push('tcptype');
sdp.push(candidate.tcpType);
}
return 'a=candidate:' + sdp.join(' ');
};

// SDP helper from sdp-jingle-json with modifications.
window.RTCPeerConnection.prototype._parseRtpMap = function(line) {
var parts = line.substr(9).split(' ');
var parsed = {
payloadType: parseInt(parts.shift(), 10) // was: id
};

parts = parts[0].split('/');

parsed.name = parts[0];
parsed.clockRate = parseInt(parts[1], 10); // was: clockrate
parsed.numChannels = parts.length === 3 ? parseInt(parts[2], 10) : 1; // was: channels
return parsed;
};

// Parses SDP to determine capabilities.
window.RTCPeerConnection.prototype._getRemoteCapabilities =
function(section) {
var remoteCapabilities = {
codecs: [],
headerExtensions: [],
fecMechanisms: []
};
var i;
var lines = section.split('\r\n');
var mline = lines[0].substr(2).split(' ');
var rtpmapFilter = function(line) {
return line.indexOf('a=rtpmap:' + mline[i]) === 0;
};
var fmtpFilter = function(line) {
return line.indexOf('a=fmtp:' + mline[i]) === 0;
};
var parseFmtp = function(line) {
var parsed = {};
var kv;
var parts = line.substr(('a=fmtp:' + mline[i]).length + 1).split(';');
for (var j = 0; j < parts.length; j++) {
kv = parts[j].split('=');
parsed[kv[0].trim()] = kv[1];
}
console.log('fmtp', mline[i], parsed);
return parsed;
};
var rtcpFbFilter = function(line) {
return line.indexOf('a=rtcp-fb:' + mline[i]) === 0;
};
var parseRtcpFb = function(line) {
var parts = line.substr(('a=rtcp-fb:' + mline[i]).length + 1)
.split(' ');
return {
type: parts.shift(),
parameter: parts.join(' ')
};
};
for (i = 3; i < mline.length; i++) { // find all codecs from mline[3..]
var line = lines.filter(rtpmapFilter)[0];
if (line) {
var codec = this._parseRtpMap(line);

var fmtp = lines.filter(fmtpFilter);
codec.parameters = fmtp.length ? parseFmtp(fmtp[0]) : {};
codec.rtcpFeedback = lines.filter(rtcpFbFilter).map(parseRtcpFb);

remoteCapabilities.codecs.push(codec);
}
}
return remoteCapabilities;
};

// Serializes capabilities to SDP.
window.RTCPeerConnection.prototype._capabilitiesToSDP = function(caps) {
var sdp = '';
caps.codecs.forEach(function(codec) {
var pt = codec.payloadType;
if (codec.preferredPayloadType !== undefined) {
pt = codec.preferredPayloadType;
}
sdp += 'a=rtpmap:' + pt +
' ' + codec.name +
'/' + codec.clockRate +
(codec.numChannels !== 1 ? '/' + codec.numChannels : '') +
'\r\n';
if (codec.parameters && codec.parameters.length) {
sdp += 'a=ftmp:' + pt + ' ';
Object.keys(codec.parameters).forEach(function(param) {
sdp += param + '=' + codec.parameters[param];
});
sdp += '\r\n';
}
if (codec.rtcpFeedback) {
// FIXME: special handling for trr-int?
codec.rtcpFeedback.forEach(function(fb) {
sdp += 'a=rtcp-fb:' + pt + ' ' + fb.type + ' ' +
fb.parameter + '\r\n';
});
}
});
return sdp;
};

// Calculates the intersection of local and remote capabilities.
window.RTCPeerConnection.prototype._getCommonCapabilities =
function(localCapabilities, remoteCapabilities) {
var commonCapabilities = {
codecs: [],
headerExtensions: [],
fecMechanisms: []
};
localCapabilities.codecs.forEach(function(lCodec) {
for (var i = 0; i < remoteCapabilities.codecs.length; i++) {
var rCodec = remoteCapabilities.codecs[i];
if (lCodec.name === rCodec.name &&
lCodec.clockRate === rCodec.clockRate &&
lCodec.numChannels === rCodec.numChannels) {
// push rCodec so we reply with offerer payload type
commonCapabilities.codecs.push(rCodec);

// FIXME: also need to calculate intersection between
// .rtcpFeedback and .parameters
break;
}
}
});

localCapabilities.headerExtensions.forEach(function(lHeaderExtension) {
for (var i = 0; i < remoteCapabilities.headerExtensions.length; i++) {
var rHeaderExtension = remoteCapabilities.headerExtensions[i];
if (lHeaderExtension.uri === rHeaderExtension.uri) {
commonCapabilities.headerExtensions.push(rHeaderExtension);
break;
}
}
});

// FIXME: fecMechanisms
return commonCapabilities;
};

// Parses DTLS parameters from SDP section or sessionpart.
window.RTCPeerConnection.prototype._getDtlsParameters =
function(section, session) {
var lines = section.split('\r\n');
lines = lines.concat(session.split('\r\n')); // Search in session part, too.
var fpLine = lines.filter(function(line) {
return line.indexOf('a=fingerprint:') === 0;
});
fpLine = fpLine[0].substr(14);
var dtlsParameters = {
role: 'auto',
fingerprints: [{
algorithm: fpLine.split(' ')[0],
value: fpLine.split(' ')[1]
}]
};
return dtlsParameters;
};

// Serializes DTLS parameters to SDP.
window.RTCPeerConnection.prototype._dtlsParametersToSDP =
function(params, setupType) {
var sdp = 'a=setup:' + setupType + '\r\n';
params.fingerprints.forEach(function(fp) {
sdp += 'a=fingerprint:' + fp.algorithm + ' ' + fp.value + '\r\n';
});
return sdp;
};

// Parses ICE information from SDP section or sessionpart.
window.RTCPeerConnection.prototype._getIceParameters =
function(section, session) {
var lines = section.split('\r\n');
lines = lines.concat(session.split('\r\n')); // Search in session part, too.
var iceParameters = {
usernameFragment: lines.filter(function(line) {
return line.indexOf('a=ice-ufrag:') === 0;
})[0].substr(12),
password: lines.filter(function(line) {
return line.indexOf('a=ice-pwd:') === 0;
})[0].substr(10),
};
return iceParameters;
};

// Serializes ICE parameters to SDP.
window.RTCPeerConnection.prototype._iceParametersToSDP = function(params) {
return 'a=ice-ufrag:' + params.usernameFragment + '\r\n' +
'a=ice-pwd:' + params.password + '\r\n';
};

window.RTCPeerConnection.prototype._getEncodingParameters = function(ssrc) {
return {
ssrc: ssrc,
codecPayloadType: 0,
fec: 0,
rtx: 0,
priority: 1.0,
maxBitrate: 2000000.0,
minQuality: 0,
framerateBias: 0.5,
resolutionScale: 1.0,
framerateScale: 1.0,
active: true,
dependencyEncodingId: undefined,
encodingId: undefined
};
};

// Create ICE gatherer, ICE transport and DTLS transport.
window.RTCPeerConnection.prototype._createIceAndDtlsTransports =
function(mid, sdpMLineIndex) {
var self = this;
var iceGatherer = new RTCIceGatherer(self.iceOptions);
var iceTransport = new RTCIceTransport(iceGatherer);
iceGatherer.onlocalcandidate = function(evt) {
var event = {};
event.candidate = {sdpMid: mid, sdpMLineIndex: sdpMLineIndex};

var cand = evt.candidate;
var isEndOfCandidates = !(cand && Object.keys(cand).length > 0);
if (isEndOfCandidates) {
event.candidate.candidate =
'candidate:1 1 udp 1 0.0.0.0 9 typ endOfCandidates';
} else {
// RTCIceCandidate doesn't have a component, needs to be added
cand.component = iceTransport.component === 'RTCP' ? 2 : 1;
event.candidate.candidate = self._toCandidateSDP(cand);
}
if (self.onicecandidate !== null) {
if (self.localDescription && self.localDescription.type === '') {
self._iceCandidates.push(event);
} else {
self.onicecandidate(event);
}
}
};
iceTransport.onicestatechange = function() {
/*
console.log(self._peerConnectionId,
'ICE state change', iceTransport.state);
*/
self._updateIceConnectionState(iceTransport.state);
};

var dtlsTransport = new RTCDtlsTransport(iceTransport);
dtlsTransport.ondtlsstatechange = function() {
/*
console.log(self._peerConnectionId, sdpMLineIndex,
'dtls state change', dtlsTransport.state);
*/
};
dtlsTransport.onerror = function(error) {
console.error('dtls error', error);
};
return {
iceGatherer: iceGatherer,
iceTransport: iceTransport,
dtlsTransport: dtlsTransport
};
};

window.RTCPeerConnection.prototype.setLocalDescription =
function(description) {
var self = this;
if (description.type === 'offer') {
if (!description.ortc) {
// FIXME: throw?
} else {
this.mLines = description.ortc;
}
} else if (description.type === 'answer') {
var sections = self.remoteDescription.sdp.split('\r\nm=');
var sessionpart = sections.shift();
sections.forEach(function(section, sdpMLineIndex) {
section = 'm=' + section;

var iceGatherer = self.mLines[sdpMLineIndex].iceGatherer;
var iceTransport = self.mLines[sdpMLineIndex].iceTransport;
var dtlsTransport = self.mLines[sdpMLineIndex].dtlsTransport;
var rtpSender = self.mLines[sdpMLineIndex].rtpSender;
var localCapabilities =
self.mLines[sdpMLineIndex].localCapabilities;
var remoteCapabilities =
self.mLines[sdpMLineIndex].remoteCapabilities;
var sendSSRC = self.mLines[sdpMLineIndex].sendSSRC;
var recvSSRC = self.mLines[sdpMLineIndex].recvSSRC;

var remoteIceParameters = self._getIceParameters(section,
sessionpart);
iceTransport.start(iceGatherer, remoteIceParameters, 'controlled');

var remoteDtlsParameters = self._getDtlsParameters(section,
sessionpart);
dtlsTransport.start(remoteDtlsParameters);

if (rtpSender) {
// calculate intersection of capabilities
var params = self._getCommonCapabilities(localCapabilities,
remoteCapabilities);
params.muxId = sendSSRC;
params.encodings = [self._getEncodingParameters(sendSSRC)];
params.rtcp = {
cname: self._cname,
reducedSize: false,
ssrc: recvSSRC,
mux: true
};
rtpSender.send(params);
}
});
}

this.localDescription = description;
switch (description.type) {
case 'offer':
this._updateSignalingState('have-local-offer');
break;
case 'answer':
this._updateSignalingState('stable');
break;
}

// FIXME: need to _reliably_ execute after args[1] or promise
window.setTimeout(function() {
// FIXME: need to apply ice candidates in a way which is async but in-order
self._iceCandidates.forEach(function(event) {
if (self.onicecandidate !== null) {
self.onicecandidate(event);
}
});
self._iceCandidates = [];
}, 50);
if (arguments.length > 1 && typeof arguments[1] === 'function') {
window.setTimeout(arguments[1], 0);
}
return new Promise(function(resolve) {
resolve();
});
};

window.RTCPeerConnection.prototype.setRemoteDescription =
function(description) {
// FIXME: for type=offer this creates state. which should not
//  happen before SLD with type=answer but... we need the stream
//  here for onaddstream.
var self = this;
var sections = description.sdp.split('\r\nm=');
var sessionpart = sections.shift();
var stream = new MediaStream();
sections.forEach(function(section, sdpMLineIndex) {
section = 'm=' + section;
var lines = section.split('\r\n');
var mline = lines[0].substr(2).split(' ');
var kind = mline[0];
var line;

var iceGatherer;
var iceTransport;
var dtlsTransport;
var rtpSender;
var rtpReceiver;
var sendSSRC;
var recvSSRC;

var mid = lines.filter(function(line) {
return line.indexOf('a=mid:') === 0;
})[0].substr(6);

var cname;

var remoteCapabilities;
var params;

if (description.type === 'offer') {
var transports = self._createIceAndDtlsTransports(mid, sdpMLineIndex);

var localCapabilities = RTCRtpReceiver.getCapabilities(kind);
// determine remote caps from SDP
remoteCapabilities = self._getRemoteCapabilities(section);

line = lines.filter(function(line) {
return line.indexOf('a=ssrc:') === 0 &&
line.split(' ')[1].indexOf('cname:') === 0;
});
sendSSRC = (2 * sdpMLineIndex + 2) * 1001;
if (line) { // FIXME: alot of assumptions here
recvSSRC = line[0].split(' ')[0].split(':')[1];
cname = line[0].split(' ')[1].split(':')[1];
}
rtpReceiver = new RTCRtpReceiver(transports.dtlsTransport, kind);

// calculate intersection so no unknown caps get passed into the RTPReciver
params = self._getCommonCapabilities(localCapabilities,
remoteCapabilities);

params.muxId = recvSSRC;
params.encodings = [self._getEncodingParameters(recvSSRC)];
params.rtcp = {
cname: cname,
reducedSize: false,
ssrc: sendSSRC,
mux: true
};
rtpReceiver.receive(params);
// FIXME: not correct when there are multiple streams but that is
// not currently supported.
stream.addTrack(rtpReceiver.track);

// FIXME: honor a=sendrecv
if (self.localStreams.length > 0 &&
self.localStreams[0].getTracks().length >= sdpMLineIndex) {
// FIXME: actually more complicated, needs to match types etc
var localtrack = self.localStreams[0].getTracks()[sdpMLineIndex];
rtpSender = new RTCRtpSender(localtrack, transports.dtlsTransport);
}

self.mLines[sdpMLineIndex] = {
iceGatherer: transports.iceGatherer,
iceTransport: transports.iceTransport,
dtlsTransport: transports.dtlsTransport,
localCapabilities: localCapabilities,
remoteCapabilities: remoteCapabilities,
rtpSender: rtpSender,
rtpReceiver: rtpReceiver,
kind: kind,
mid: mid,
sendSSRC: sendSSRC,
recvSSRC: recvSSRC
};
} else {
iceGatherer = self.mLines[sdpMLineIndex].iceGatherer;
iceTransport = self.mLines[sdpMLineIndex].iceTransport;
dtlsTransport = self.mLines[sdpMLineIndex].dtlsTransport;
rtpSender = self.mLines[sdpMLineIndex].rtpSender;
rtpReceiver = self.mLines[sdpMLineIndex].rtpReceiver;
sendSSRC = self.mLines[sdpMLineIndex].sendSSRC;
recvSSRC = self.mLines[sdpMLineIndex].recvSSRC;
}

var remoteIceParameters = self._getIceParameters(section, sessionpart);
var remoteDtlsParameters = self._getDtlsParameters(section,
sessionpart);

// for answers we start ice and dtls here, otherwise this is done in SLD
if (description.type === 'answer') {
iceTransport.start(iceGatherer, remoteIceParameters, 'controlling');
dtlsTransport.start(remoteDtlsParameters);

// determine remote caps from SDP
remoteCapabilities = self._getRemoteCapabilities(section);
// FIXME: store remote caps?

if (rtpSender) {
params = remoteCapabilities;
params.muxId = sendSSRC;
params.encodings = [self._getEncodingParameters(sendSSRC)];
params.rtcp = {
cname: self._cname,
reducedSize: false,
ssrc: recvSSRC,
mux: true
};
rtpSender.send(params);
}

// FIXME: only if a=sendrecv
var bidi = lines.filter(function(line) {
return line.indexOf('a=ssrc:') === 0;
}).length > 0;
if (rtpReceiver && bidi) {
line = lines.filter(function(line) {
return line.indexOf('a=ssrc:') === 0 &&
line.split(' ')[1].indexOf('cname:') === 0;
});
if (line) { // FIXME: alot of assumptions here
recvSSRC = line[0].split(' ')[0].split(':')[1];
cname = line[0].split(' ')[1].split(':')[1];
}
params = remoteCapabilities;
params.muxId = recvSSRC;
params.encodings = [self._getEncodingParameters(recvSSRC)];
params.rtcp = {
cname: cname,
reducedSize: false,
ssrc: sendSSRC,
mux: true
};
rtpReceiver.receive(params, kind);
stream.addTrack(rtpReceiver.track);
self.mLines[sdpMLineIndex].recvSSRC = recvSSRC;
}
}
});

this.remoteDescription = description;
switch (description.type) {
case 'offer':
this._updateSignalingState('have-remote-offer');
break;
case 'answer':
this._updateSignalingState('stable');
break;
}
window.setTimeout(function() {
if (self.onaddstream !== null && stream.getTracks().length) {
self.remoteStreams.push(stream);
window.setTimeout(function() {
self.onaddstream({stream: stream});
}, 0);
}
}, 0);
if (arguments.length > 1 && typeof arguments[1] === 'function') {
window.setTimeout(arguments[1], 0);
}
return new Promise(function(resolve) {
resolve();
});
};

window.RTCPeerConnection.prototype.close = function() {
this.mLines.forEach(function(mLine) {
/* not yet
if (mLine.iceGatherer) {
mLine.iceGatherer.close();
}
*/
if (mLine.iceTransport) {
mLine.iceTransport.stop();
}
if (mLine.dtlsTransport) {
mLine.dtlsTransport.stop();
}
if (mLine.rtpSender) {
mLine.rtpSender.stop();
}
if (mLine.rtpReceiver) {
mLine.rtpReceiver.stop();
}
});
// FIXME: clean up tracks, local streams, remote streams, etc
this._updateSignalingState('closed');
this._updateIceConnectionState('closed');
};

// Update the signaling state.
window.RTCPeerConnection.prototype._updateSignalingState =
function(newState) {
this.signalingState = newState;
if (this.onsignalingstatechange !== null) {
this.onsignalingstatechange();
}
};

// Update the ICE connection state.
// FIXME: should be called 'updateConnectionState', also be called for
//  DTLS changes and implement
//  https://lists.w3.org/Archives/Public/public-webrtc/2015Sep/0033.html window.RTCPeerConnection.prototype._updateIceConnectionState =
function(newState) {
var self = this;
if (this.iceConnectionState !== newState) {
var agreement = self.mLines.every(function(mLine) {
return mLine.iceTransport.state === newState;
});
if (agreement) {
self.iceConnectionState = newState;
if (this.oniceconnectionstatechange !== null) {
this.oniceconnectionstatechange();
}
}
}
};

window.RTCPeerConnection.prototype.createOffer = function() {
var self = this;
var offerOptions;
if (arguments.length === 1 && typeof arguments[0] !== 'function') {
offerOptions = arguments[0];
} else if (arguments.length === 3) {
offerOptions = arguments[2];
}

var tracks = [];
var numAudioTracks = 0;
var numVideoTracks = 0;
// Default to sendrecv.
if (this.localStreams.length) {
numAudioTracks = this.localStreams[0].getAudioTracks().length;
numVideoTracks = this.localStreams[0].getAudioTracks().length;
}
// Determine number of audio and video tracks we need to send/recv.
if (offerOptions) {
// Deal with Chrome legacy constraints...
if (offerOptions.mandatory) {
if (offerOptions.mandatory.OfferToReceiveAudio) {
numAudioTracks = 1;
} else if (offerOptions.mandatory.OfferToReceiveAudio === false) {
numAudioTracks = 0;
}
if (offerOptions.mandatory.OfferToReceiveVideo) {
numVideoTracks = 1;
} else if (offerOptions.mandatory.OfferToReceiveVideo === false) {
numVideoTracks = 0;
}
} else {
if (offerOptions.offerToReceiveAudio !== undefined) {
numAudioTracks = offerOptions.offerToReceiveAudio;
}
if (offerOptions.offerToReceiveVideo !== undefined) {
numVideoTracks = offerOptions.offerToReceiveVideo;
}
}
}
if (this.localStreams.length) {
// Push local streams.
this.localStreams[0].getTracks().forEach(function(track) {
tracks.push({
kind: track.kind,
track: track,
wantReceive: track.kind === 'audio' ?
numAudioTracks > 0 : numVideoTracks > 0
});
if (track.kind === 'audio') {
numAudioTracks--;
} else if (track.kind === 'video') {
numVideoTracks--;
}
});
}
// Create M-lines for recvonly streams.
while (numAudioTracks > 0 || numVideoTracks > 0) {
if (numAudioTracks > 0) {
tracks.push({
kind: 'audio',
wantReceive: true
});
numAudioTracks--;
}
if (numVideoTracks > 0) {
tracks.push({
kind: 'video',
wantReceive: true
});
numVideoTracks--;
}
}

var sdp = 'v=0\r\n' +
'o=thisisadapterortc 8169639915646943137 2 IN IP4 127.0.0.1\r\n' +
's=-\r\n' +
't=0 0\r\n';
var mLines = [];
tracks.forEach(function(mline, sdpMLineIndex) {
// For each track, create an ice gatherer, ice transport, dtls transport,
// potentially rtpsender and rtpreceiver.
var track = mline.track;
var kind = mline.kind;
var mid = Math.random().toString(36).substr(2, 10);

var transports = self._createIceAndDtlsTransports(mid, sdpMLineIndex);

var localCapabilities = RTCRtpSender.getCapabilities(kind);
var rtpSender;
// generate an ssrc now, to be used later in rtpSender.send
var sendSSRC = (2 * sdpMLineIndex + 1) * 1001; //Math.floor(Math.random()*4294967295);
var recvSSRC; // don't know yet
if (track) {
rtpSender = new RTCRtpSender(track, transports.dtlsTransport);
}

var rtpReceiver;
if (mline.wantReceive) {
rtpReceiver = new RTCRtpReceiver(transports.dtlsTransport, kind);
}

mLines[sdpMLineIndex] = {
iceGatherer: transports.iceGatherer,
iceTransport: transports.iceTransport,
dtlsTransport: transports.dtlsTransport,
localCapabilities: localCapabilities,
remoteCapabilities: null,
rtpSender: rtpSender,
rtpReceiver: rtpReceiver,
kind: kind,
mid: mid,
sendSSRC: sendSSRC,
recvSSRC: recvSSRC
};

// Map things to SDP.
// Build the mline.
sdp += 'm=' + kind + ' 9 UDP/TLS/RTP/SAVPF ';
sdp += localCapabilities.codecs.map(function(codec) {
return codec.preferredPayloadType;
}).join(' ') + '\r\n';

sdp += 'c=IN IP4 0.0.0.0\r\n';
sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n';

// Map ICE parameters (ufrag, pwd) to SDP.
sdp += self._iceParametersToSDP(
transports.iceGatherer.getLocalParameters());

// Map DTLS parameters to SDP.
sdp += self._dtlsParametersToSDP(
transports.dtlsTransport.getLocalParameters(), 'actpass');

sdp += 'a=mid:' + mid + '\r\n';

if (rtpSender && rtpReceiver) {
sdp += 'a=sendrecv\r\n';
} else if (rtpSender) {
sdp += 'a=sendonly\r\n';
} else if (rtpReceiver) {
sdp += 'a=recvonly\r\n';
} else {
sdp += 'a=inactive\r\n';
}
sdp += 'a=rtcp-mux\r\n';

// Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb.
sdp += self._capabilitiesToSDP(localCapabilities);

if (track) {
sdp += 'a=msid:' + self.localStreams[0].id + ' ' + track.id + '\r\n';
sdp += 'a=ssrc:' + sendSSRC + ' ' + 'msid:' +
self.localStreams[0].id + ' ' + track.id + '\r\n';
}
sdp += 'a=ssrc:' + sendSSRC + ' cname:' + self._cname + '\r\n';
});

var desc = new RTCSessionDescription({
type: 'offer',
sdp: sdp,
ortc: mLines
});
if (arguments.length && typeof arguments[0] === 'function') {
window.setTimeout(arguments[0], 0, desc);
}
return new Promise(function(resolve) {
resolve(desc);
});
};

window.RTCPeerConnection.prototype.createAnswer = function() {
var self = this;
var answerOptions;
if (arguments.length === 1 && typeof arguments[0] !== 'function') {
answerOptions = arguments[0];
} else if (arguments.length === 3) {
answerOptions = arguments[2];
}

var sdp = 'v=0\r\n' +
'o=thisisadapterortc 8169639915646943137 2 IN IP4 127.0.0.1\r\n' +
's=-\r\n' +
't=0 0\r\n';
this.mLines.forEach(function(mLine/*, sdpMLineIndex*/) {
var iceGatherer = mLine.iceGatherer;
//var iceTransport = mLine.iceTransport;
var dtlsTransport = mLine.dtlsTransport;
var localCapabilities = mLine.localCapabilities;
var remoteCapabilities = mLine.remoteCapabilities;
var rtpSender = mLine.rtpSender;
var rtpReceiver = mLine.rtpReceiver;
var kind = mLine.kind;
var sendSSRC = mLine.sendSSRC;
//var recvSSRC = mLine.recvSSRC;

// Calculate intersection of capabilities.
var commonCapabilities = self._getCommonCapabilities(localCapabilities,
remoteCapabilities);

// Map things to SDP.
// Build the mline.
sdp += 'm=' + kind + ' 9 UDP/TLS/RTP/SAVPF ';
sdp += commonCapabilities.codecs.map(function(codec) {
return codec.payloadType;
}).join(' ') + '\r\n';

sdp += 'c=IN IP4 0.0.0.0\r\n';
sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n';

// Map ICE parameters (ufrag, pwd) to SDP.
sdp += self._iceParametersToSDP(iceGatherer.getLocalParameters());

// Map DTLS parameters to SDP.
sdp += self._dtlsParametersToSDP(dtlsTransport.getLocalParameters(),
'active');

sdp += 'a=mid:' + mLine.mid + '\r\n';

if (rtpSender && rtpReceiver) {
sdp += 'a=sendrecv\r\n';
} else if (rtpReceiver) {
sdp += 'a=sendonly\r\n';
} else if (rtpSender) {
sdp += 'a=sendonly\r\n';
} else {
sdp += 'a=inactive\r\n';
}
sdp += 'a=rtcp-mux\r\n';

// Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb.
sdp += self._capabilitiesToSDP(commonCapabilities);

if (rtpSender) {
// add a=ssrc lines from RTPSender
sdp += 'a=msid:' + self.localStreams[0].id + ' ' +
rtpSender.track.id + '\r\n';
sdp += 'a=ssrc:' + sendSSRC + ' ' + 'msid:' +
self.localStreams[0].id + ' ' + rtpSender.track.id + '\r\n';
}
sdp += 'a=ssrc:' + sendSSRC + ' cname:' + self._cname + '\r\n';
});

var desc = new RTCSessionDescription({
type: 'answer',
sdp: sdp
// ortc: tracks -- state is created in SRD already
});
if (arguments.length && typeof arguments[0] === 'function') {
window.setTimeout(arguments[0], 0, desc);
}
return new Promise(function(resolve) {
resolve(desc);
});
};

window.RTCPeerConnection.prototype.addIceCandidate = function(candidate) {
// TODO: lookup by mid
var mLine = this.mLines[candidate.sdpMLineIndex];
if (mLine) {
var cand = Object.keys(candidate.candidate).length > 0 ?
this._toCandidateJSON(candidate.candidate) : {};
// dirty hack to make simplewebrtc work.
// FIXME: need another dirty hack to avoid adding candidates after this
if (cand.type === 'endOfCandidates') {
cand = {};
}
// dirty hack to make chrome work.
if (cand.protocol === 'tcp' && cand.port === 0) {
cand = {};
}
mLine.iceTransport.addRemoteCandidate(cand);
}
if (arguments.length > 1 && typeof arguments[1] === 'function') {
window.setTimeout(arguments[1], 0);
}
return new Promise(function(resolve) {
resolve();
});
};

window.RTCPeerConnection.prototype.getStats = function() {
var promises = [];
this.mLines.forEach(function(mLine) {
['rtpSender', 'rtpReceiver', 'iceGatherer', 'iceTransport',
'dtlsTransport'].forEach(function(thing) {
if (mLine[thing]) {
promises.push(mLine[thing].getStats());
}
});
});
var cb = arguments.length > 1 && typeof arguments[1] === 'function' &&
arguments[1];
return new Promise(function(resolve) {
var results = {};
Promise.all(promises).then(function(res) {
res.forEach(function(result) {
Object.keys(result).forEach(function(id) {
results[id] = result[id];
});
});
if (cb) {
window.setTimeout(cb, 0, results);
}
resolve(results);
});
});
};
}
} else {
webrtcUtils.log('Browser does not appear to be WebRTC-capable');
}

// Returns the result of getUserMedia as a Promise.
function requestUserMedia(constraints) {
return new Promise(function(resolve, reject) {
getUserMedia(constraints, resolve, reject);
});
}

var webrtcTesting = {};
try {
Object.defineProperty(webrtcTesting, 'version', {
set: function(version) {
webrtcDetectedVersion = version;
}
});
} catch (e) {}

if (typeof module !== 'undefined') {
var RTCPeerConnection;
if (typeof window !== 'undefined') {
RTCPeerConnection = window.RTCPeerConnection;
}
module.exports = {
RTCPeerConnection: RTCPeerConnection,
getUserMedia: getUserMedia,
attachMediaStream: attachMediaStream,
reattachMediaStream: reattachMediaStream,
webrtcDetectedBrowser: webrtcDetectedBrowser,
webrtcDetectedVersion: webrtcDetectedVersion,
webrtcMinimumVersion: webrtcMinimumVersion,
webrtcTesting: webrtcTesting,
webrtcUtils: webrtcUtils
//requestUserMedia: not exposed on purpose.
//trace: not exposed on purpose.
};
} else if ((typeof require === 'function') && (typeof define === 'function')) {
// Expose objects and functions when RequireJS is doing the loading.
define([], function() {
return {
RTCPeerConnection: window.RTCPeerConnection,
getUserMedia: getUserMedia,
attachMediaStream: attachMediaStream,
reattachMediaStream: reattachMediaStream,
webrtcDetectedBrowser: webrtcDetectedBrowser,
webrtcDetectedVersion: webrtcDetectedVersion,
webrtcMinimumVersion: webrtcMinimumVersion,
webrtcTesting: webrtcTesting,
webrtcUtils: webrtcUtils
//requestUserMedia: not exposed on purpose.
//trace: not exposed on purpose.
};
});
}


View Code
交互js代码





/*
*  Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
*
*  Use of this source code is governed by a BSD-style license
*  that can be found in the LICENSE file in the root of the source
*  tree.
*/

'use strict';

var startButton = document.getElementById('startButton');
var callButton = document.getElementById('callButton');
var hangupButton = document.getElementById('hangupButton');
callButton.disabled = true;
hangupButton.disabled = true;
startButton.onclick = start;
callButton.onclick = call;
hangupButton.onclick = hangup;

var startTime;
var localVideo = document.getElementById('localVideo');
var remoteVideo = document.getElementById('remoteVideo');

localVideo.addEventListener('loadedmetadata', function() {
trace('Local video videoWidth: ' + this.videoWidth +
'px,  videoHeight: ' + this.videoHeight + 'px');
});

remoteVideo.addEventListener('loadedmetadata', function() {
trace('Remote video videoWidth: ' + this.videoWidth +
'px,  videoHeight: ' + this.videoHeight + 'px');
});

remoteVideo.onresize = function() {
trace('Remote video size changed to ' +
remoteVideo.videoWidth + 'x' + remoteVideo.videoHeight);
// We'll use the first onsize callback as an indication that video has started
// playing out.
if (startTime) {
var elapsedTime = window.performance.now() - startTime;
trace('Setup time: ' + elapsedTime.toFixed(3) + 'ms');
startTime = null;
}
};

var localStream;
var pc1;
var pc2;
var offerOptions = {
offerToReceiveAudio: 1,
offerToReceiveVideo: 1
};

function getName(pc) {
return (pc === pc1) ? 'pc1' : 'pc2';
}

function getOtherPc(pc) {
return (pc === pc1) ? pc2 : pc1;
}

function gotStream(stream) {
trace('Received local stream');
localVideo.srcObject = stream;
localStream = stream;
callButton.disabled = false;
}

function start() {
trace('Requesting local stream');
startButton.disabled = true;
navigator.mediaDevices.getUserMedia({
audio: true,
video: true
})
.then(gotStream)
.catch(function(e) {
alert('getUserMedia() error: ' + e.name);
});
}

function call() {
callButton.disabled = true;
hangupButton.disabled = false;
trace('Starting call');
startTime = window.performance.now();
var videoTracks = localStream.getVideoTracks();
var audioTracks = localStream.getAudioTracks();
if (videoTracks.length > 0) {
trace('Using video device: ' + videoTracks[0].label);
}
if (audioTracks.length > 0) {
trace('Using audio device: ' + audioTracks[0].label);
}
var servers = null;
pc1 = new RTCPeerConnection(servers);
trace('Created local peer connection object pc1');
pc1.onicecandidate = function(e) {
onIceCandidate(pc1, e);
};
pc2 = new RTCPeerConnection(servers);
trace('Created remote peer connection object pc2');
pc2.onicecandidate = function(e) {
onIceCandidate(pc2, e);
};
pc1.oniceconnectionstatechange = function(e) {
onIceStateChange(pc1, e);
};
pc2.oniceconnectionstatechange = function(e) {
onIceStateChange(pc2, e);
};
pc2.onaddstream = gotRemoteStream;

pc1.addStream(localStream);
trace('Added local stream to pc1');

trace('pc1 createOffer start');
pc1.createOffer(onCreateOfferSuccess, onCreateSessionDescriptionError,
offerOptions);
}

function onCreateSessionDescriptionError(error) {
trace('Failed to create session description: ' + error.toString());
}

function onCreateOfferSuccess(desc) {
trace('Offer from pc1\n' + desc.sdp);
trace('pc1 setLocalDescription start');
pc1.setLocalDescription(desc, function() {
onSetLocalSuccess(pc1);
}, onSetSessionDescriptionError);
trace('pc2 setRemoteDescription start');
pc2.setRemoteDescription(desc, function() {
onSetRemoteSuccess(pc2);
}, onSetSessionDescriptionError);
trace('pc2 createAnswer start');
// Since the 'remote' side has no media stream we need
// to pass in the right constraints in order for it to
// accept the incoming offer of audio and video.
pc2.createAnswer(onCreateAnswerSuccess, onCreateSessionDescriptionError);
}

function onSetLocalSuccess(pc) {
trace(getName(pc) + ' setLocalDescription complete');
}

function onSetRemoteSuccess(pc) {
trace(getName(pc) + ' setRemoteDescription complete');
}

function onSetSessionDescriptionError(error) {
trace('Failed to set session description: ' + error.toString());
}

function gotRemoteStream(e) {
remoteVideo.srcObject = e.stream;
trace('pc2 received remote stream');
}

function onCreateAnswerSuccess(desc) {
trace('Answer from pc2:\n' + desc.sdp);
trace('pc2 setLocalDescription start');
pc2.setLocalDescription(desc, function() {
onSetLocalSuccess(pc2);
}, onSetSessionDescriptionError);
trace('pc1 setRemoteDescription start');
pc1.setRemoteDescription(desc, function() {
onSetRemoteSuccess(pc1);
}, onSetSessionDescriptionError);
}

function onIceCandidate(pc, event) {
if (event.candidate) {
getOtherPc(pc).addIceCandidate(new RTCIceCandidate(event.candidate),
function() {
onAddIceCandidateSuccess(pc);
},
function(err) {
onAddIceCandidateError(pc, err);
}
);
trace(getName(pc) + ' ICE candidate: \n' + event.candidate.candidate);
}
}

function onAddIceCandidateSuccess(pc) {
trace(getName(pc) + ' addIceCandidate success');
}

function onAddIceCandidateError(pc, error) {
trace(getName(pc) + ' failed to add ICE Candidate: ' + error.toString());
}

function onIceStateChange(pc, event) {
if (pc) {
trace(getName(pc) + ' ICE state: ' + pc.iceConnectionState);
console.log('ICE state change event: ', event);
}
}

function hangup() {
trace('Ending call');
pc1.close();
pc2.close();
pc1 = null;
pc2 = null;
hangupButton.disabled = true;
callButton.disabled = false;
}


View Code
 

本地,呼叫者

// servers是配置文件(TURN and STUN配置)
pc1 = new webkitRTCPeerConnection(servers);
// ...
pc1.addStream(localStream);


 

远端,被叫着

创建一个offer ,将其设定为PC1的局部描述(local description),PC2远程描述(remote description)。
这样可以不使用信令通讯,因为主叫和被叫都在同一网页上。

pc1.createOffer(gotDescription1);
//...
function gotDescription1(desc){
pc1.setLocalDescription(desc);
trace("Offer from pc1 \n" + desc.sdp);
pc2.setRemoteDescription(desc);
pc2.createAnswer(gotDescription2);
}


创建pc2,当pc1产生视频流,则显示在remoteVideo视频控件(video element):

pc2 = new webkitRTCPeerConnection(servers);
pc2.onaddstream = gotRemoteStream;
//...
function gotRemoteStream(e){
remoteVideo.src = URL.createObjectURL(e.stream);
 trace('pc2 received remote stream');
}


 

运行结果:

Navigated to https://webrtc.github.io/samples/src/content/peerconnection/pc1/ adapter.js:32 This appears to be Chrome
common.js:8 12.639: Requesting local stream
adapter.js:32 chrome: {"audio":true,"video":true}
common.js:8 12.653: Received local stream
common.js:8 14.038: Local video videoWidth: 640px,  videoHeight: 480px
common.js:8 15.183: Starting call
common.js:8 15.183: Using video device: Integrated Camera (04f2:b39a)
common.js:8 15.183: Using audio device: 默认
common.js:8 15.185: Created local peer connection object pc1
common.js:8 15.186: Created remote peer connection object pc2
common.js:8 15.186: Added local stream to pc1
common.js:8 15.187: pc1 createOffer start
common.js:8 15.190: Offer from pc1
v=0
o=- 5740173043645401541 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE audio video
a=msid-semantic: WMS ZWvBmXl2Dax58ugXR3BYDITTKIIV1TYPqViT
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 126
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:MOApAbo/PL8Jl3m9
a=ice-pwd:dxcuXVAFcyVqbgwi0QdQNh0S
a=fingerprint:sha-256 5F:CB:FF:EF:73:09:BC:0A:6F:18:0C:DB:11:A5:AE:AF:37:49:37:71:D0:FE:BA:39:EC:53:6B:10:8C:8A:95:9E
a=setup:actpass
a=mid:audio
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time a=sendrecv
a=rtcp-mux
a=rtpmap:111 opus/48000/2
a=fmtp:111 minptime=10; useinbandfec=1
a=rtpmap:103 ISAC/16000
a=rtpmap:104 ISAC/32000
a=rtpmap:9 G722/8000
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:106 CN/32000
a=rtpmap:105 CN/16000
a=rtpmap:13 CN/8000
a=rtpmap:126 telephone-event/8000
a=maxptime:60
a=ssrc:32244674 cname:anS0gTF+aWAKlwYj
a=ssrc:32244674 msid:ZWvBmXl2Dax58ugXR3BYDITTKIIV1TYPqViT 8ae8dd85-bd5c-49ff-a9bd-f4b88f2663c7
a=ssrc:32244674 mslabel:ZWvBmXl2Dax58ugXR3BYDITTKIIV1TYPqViT
a=ssrc:32244674 label:8ae8dd85-bd5c-49ff-a9bd-f4b88f2663c7
m=video 9 UDP/TLS/RTP/SAVPF 100 116 117 96
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:MOApAbo/PL8Jl3m9
a=ice-pwd:dxcuXVAFcyVqbgwi0QdQNh0S
a=fingerprint:sha-256 5F:CB:FF:EF:73:09:BC:0A:6F:18:0C:DB:11:A5:AE:AF:37:49:37:71:D0:FE:BA:39:EC:53:6B:10:8C:8A:95:9E
a=setup:actpass
a=mid:video
a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time a=extmap:4 urn:3gpp:video-orientation
a=sendrecv
a=rtcp-mux
a=rtpmap:100 VP8/90000
a=rtcp-fb:100 ccm fir
a=rtcp-fb:100 nack
a=rtcp-fb:100 nack pli
a=rtcp-fb:100 goog-remb
a=rtpmap:116 red/90000
a=rtpmap:117 ulpfec/90000
a=rtpmap:96 rtx/90000
a=fmtp:96 apt=100
a=ssrc-group:FID 1099776253 671187929
a=ssrc:1099776253 cname:anS0gTF+aWAKlwYj
a=ssrc:1099776253 msid:ZWvBmXl2Dax58ugXR3BYDITTKIIV1TYPqViT eda73070-3562-4daf-ae0d-143694f294d5
a=ssrc:1099776253 mslabel:ZWvBmXl2Dax58ugXR3BYDITTKIIV1TYPqViT
a=ssrc:1099776253 label:eda73070-3562-4daf-ae0d-143694f294d5
a=ssrc:671187929 cname:anS0gTF+aWAKlwYj
a=ssrc:671187929 msid:ZWvBmXl2Dax58ugXR3BYDITTKIIV1TYPqViT eda73070-3562-4daf-ae0d-143694f294d5
a=ssrc:671187929 mslabel:ZWvBmXl2Dax58ugXR3BYDITTKIIV1TYPqViT
a=ssrc:671187929 label:eda73070-3562-4daf-ae0d-143694f294d5
common.js:8 15.190: pc1 setLocalDescription start
common.js:8 15.191: pc2 setRemoteDescription start
common.js:8 15.192: pc2 createAnswer start
common.js:8 15.202: pc1 setLocalDescription complete
common.js:8 15.203: pc1 ICE candidate:
candidate:2999745851 1 udp 2122260223 192.168.56.1 64106 typ host generation 0
common.js:8 15.204: pc1 ICE candidate:
candidate:1425577752 1 udp 2122194687 172.17.22.106 64107 typ host generation 0
common.js:8 15.204: pc1 ICE candidate:
candidate:2733511545 1 udp 2122129151 192.168.127.1 64108 typ host generation 0
common.js:8 15.204: pc1 ICE candidate:
candidate:1030387485 1 udp 2122063615 192.168.204.1 64109 typ host generation 0
common.js:8 15.205: pc1 ICE candidate:
candidate:3003979406 1 udp 2121998079 172.17.26.47 64110 typ host generation 0
common.js:8 15.206: pc2 setRemoteDescription complete
common.js:8 15.206: Answer from pc2:
v=0
o=- 3554329696104028001 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE audio video
a=msid-semantic: WMS
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 126
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:raDEDz+tkFTWXMn8
a=ice-pwd:RQ8bf7mtOXHIDQ0/vF25IRMf
a=fingerprint:sha-256 5F:CB:FF:EF:73:09:BC:0A:6F:18:0C:DB:11:A5:AE:AF:37:49:37:71:D0:FE:BA:39:EC:53:6B:10:8C:8A:95:9E
a=setup:active
a=mid:audio
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time a=recvonly
a=rtcp-mux
a=rtpmap:111 opus/48000/2
a=fmtp:111 minptime=10; useinbandfec=1
a=rtpmap:103 ISAC/16000
a=rtpmap:104 ISAC/32000
a=rtpmap:9 G722/8000
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:106 CN/32000
a=rtpmap:105 CN/16000
a=rtpmap:13 CN/8000
a=rtpmap:126 telephone-event/8000
a=maxptime:60
m=video 9 UDP/TLS/RTP/SAVPF 100 116 117 96
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:raDEDz+tkFTWXMn8
a=ice-pwd:RQ8bf7mtOXHIDQ0/vF25IRMf
a=fingerprint:sha-256 5F:CB:FF:EF:73:09:BC:0A:6F:18:0C:DB:11:A5:AE:AF:37:49:37:71:D0:FE:BA:39:EC:53:6B:10:8C:8A:95:9E
a=setup:active
a=mid:video
a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time a=extmap:4 urn:3gpp:video-orientation
a=recvonly
a=rtcp-mux
a=rtpmap:100 VP8/90000
a=rtcp-fb:100 ccm fir
a=rtcp-fb:100 nack
a=rtcp-fb:100 nack pli
a=rtcp-fb:100 goog-remb
a=rtpmap:116 red/90000
a=rtpmap:117 ulpfec/90000
a=rtpmap:96 rtx/90000
a=fmtp:96 apt=100
common.js:8 15.207: pc2 setLocalDescription start
common.js:8 15.207: pc1 setRemoteDescription start
common.js:8 15.208: pc2 received remote stream
2common.js:8 15.209: pc1 addIceCandidate success
3common.js:8 15.210: pc1 addIceCandidate success
common.js:8 15.224: pc1 ICE candidate:
candidate:2999745851 2 udp 2122260222 192.168.56.1 64111 typ host generation 0
common.js:8 15.224: pc1 ICE candidate:
candidate:1425577752 2 udp 2122194686 172.17.22.106 64112 typ host generation 0
common.js:8 15.225: pc1 ICE candidate:
candidate:2733511545 2 udp 2122129150 192.168.127.1 64113 typ host generation 0
common.js:8 15.225: pc1 ICE candidate:
candidate:1030387485 2 udp 2122063614 192.168.204.1 64114 typ host generation 0
common.js:8 15.226: pc1 ICE candidate:
candidate:3003979406 2 udp 2121998078 172.17.26.47 64115 typ host generation 0
common.js:8 15.226: pc1 ICE candidate:
candidate:2999745851 1 udp 2122260223 192.168.56.1 64116 typ host generation 0
common.js:8 15.227: pc1 ICE candidate:
candidate:1425577752 1 udp 2122194687 172.17.22.106 64117 typ host generation 0
common.js:8 15.227: pc1 ICE candidate:
candidate:2733511545 1 udp 2122129151 192.168.127.1 64118 typ host generation 0
common.js:8 15.227: pc1 ICE candidate:
candidate:1030387485 1 udp 2122063615 192.168.204.1 64119 typ host generation 0
common.js:8 15.228: pc1 ICE candidate:
candidate:3003979406 1 udp 2121998079 172.17.26.47 64120 typ host generation 0
common.js:8 15.228: pc1 ICE candidate:
candidate:2999745851 2 udp 2122260222 192.168.56.1 64121 typ host generation 0
common.js:8 15.228: pc1 ICE candidate:
candidate:1425577752 2 udp 2122194686 172.17.22.106 64122 typ host generation 0
common.js:8 15.229: pc1 ICE candidate:
candidate:2733511545 2 udp 2122129150 192.168.127.1 64123 typ host generation 0
common.js:8 15.229: pc1 ICE candidate:
candidate:1030387485 2 udp 2122063614 192.168.204.1 64124 typ host generation 0
common.js:8 15.230: pc1 ICE candidate:
candidate:3003979406 2 udp 2121998078 172.17.26.47 64125 typ host generation 0
common.js:8 15.231: pc1 addIceCandidate success
common.js:8 15.231: pc2 setLocalDescription complete
common.js:8 15.231: pc1 setRemoteDescription complete
common.js:8 15.231: pc1 addIceCandidate success
8common.js:8 15.232: pc1 addIceCandidate success
5common.js:8 15.233: pc1 addIceCandidate success
common.js:8 15.233: pc2 ICE state: checking
main.js:197 ICE state change event:  Event {isTrusted: true}
common.js:8 15.243: pc2 ICE candidate:
candidate:2999745851 1 udp 2122260223 192.168.56.1 64126 typ host generation 0
common.js:8 15.246: pc2 ICE candidate:
candidate:1425577752 1 udp 2122194687 172.17.22.106 64127 typ host generation 0
common.js:8 15.247: pc2 ICE candidate:
candidate:2733511545 1 udp 2122129151 192.168.127.1 64128 typ host generation 0
common.js:8 15.248: pc2 ICE candidate:
candidate:1030387485 1 udp 2122063615 192.168.204.1 64129 typ host generation 0
common.js:8 15.249: pc2 ICE candidate:
candidate:3003979406 1 udp 2121998079 172.17.26.47 64130 typ host generation 0
common.js:8 15.250: pc1 ICE state: checking
main.js:197 ICE state change event:  Event {isTrusted: true}
5common.js:8 15.251: pc2 addIceCandidate success
common.js:8 16.271: pc1 ICE state: connected
main.js:197 ICE state change event:  Event {isTrusted: true}
common.js:8 16.272: pc2 ICE state: connected
main.js:197 ICE state change event:  Event {isTrusted: true}
common.js:8 16.326: Remote video size changed to 640x480
common.js:8 16.326: Setup time: 1142.795ms
common.js:8 16.326: Remote video videoWidth: 640px,  videoHeight: 480px
common.js:8 16.326: Remote video size changed to 640x480
common.js:8 18.447: Ending call


 

 

但在真实世界,不可能不通过服务器传送信令,WebRTC 两端必须通过服务器交换信令。

用户相互发现对方和交换“真实世界”的信息,如姓名。

WebRTC客户端应用程序交换网络信息。

视频格式和分辨率等交换数据。

客户端应用穿越NAT网关和防火墙。

所以,你的服务器端需要实现的功能:

用户发现和通信。

信令通信。

NAT和防火墙的穿越。

在点对点通信失败后的中继服务(补救服务)。

STUN协议和它的扩展TURN使用ICE framework。

ICE先试图在节点直接连接,通过最低的延迟,通过UDP协议。在这个过程中:STUN服务器启用NAT后面找到它的公共地址和端口。

 



 

ICE顺序:

先UDP,

如果UDP失败 则TCP,

如果TCP失败 则HTTP,

最后HTTPS

 

具体实现:

ICE 先使用 STUN 通过UDP直连

如果UDP、TCP、http等失败 则使用TURN 中继服务(Relay server)



 

STUN, TURN and signaling 介绍:

http://www.html5rocks.com/en/tutorials/webrtc/infrastructure/

 

许多现有的WebRTC应用只是Web浏览器之间的通信,但网关服务器可以使WebRTC应用在浏览器与设备如电话互动(PSTN和VoIP系统)。
2012五月,Doubango开源了sipml5 SIP客户端,sipml5是通过WebRTC和WebSocket,使视频和语音通话在浏览器和应用程序(iOS或Android)之间进行。

 

网址:

https://www.doubango.org/sipml5/
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: