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

RTCPeerConnection的作用是在浏览器之间建立数据的“点对点”(peer to peer)通信.

WebRTC architecture diagram

 

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

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

  • 丢包隐藏
  • 回声抵消
  • 带宽自适应
  • 动态抖动缓冲
  • 自动增益控制
  • 噪声抑制与抑制
  • 图像清洗

 

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

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

  • 通信内容的元数据:打开/关闭对话(session)的命令、媒体文件的元数据(编码格式、媒体类型和带宽)等。
  • 网络通信的元数据:IP地址、NAT网络地址翻译和防火墙等。

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

 

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

  1. 张三创造了一个RTCPeerConnection 对象。
  2. 张三通过RTCPeerConnection createOffer()方法创造了一个offer(SDP会话描述) 。
  3. 张三通过他创建的offer调用setLocalDescription(),保存本地会话描述。
  4. 张三发送信令给李四。
  5. 李四接通带有李四offer的电话,调用setRemoteDescription() ,李四的RTCPeerConnection知道张三的设置(张三的本地描述到了李四这里,就成了李四的远程会话描述)。
  6. 李四调用createAnswer(),将李四的本地会话描述(local session description)成功回调。
  7. 李四调用setLocalDescription()设置他自己的本地局部描述。
  8. 李四发送应答信令answer给张三。
  9. 张三将李四的应答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后面找到它的公共地址和端口。

 

Finding connection candidates

 

ICE顺序:

  1. 先UDP,
  2. 如果UDP失败 则TCP,
  3. 如果TCP失败 则HTTP,
  4. 最后HTTPS

 

具体实现:

  1. ICE 先使用 STUN 通过UDP直连
  2. 如果UDP、TCP、http等失败 则使用TURN 中继服务(Relay server)

WebRTC data pathways

 

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/

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值