docs: function (iceServers, options, plugin, ready) {
      var Proto = function PeerConnection () {},
          api = new Proto(),
          id = $.uuid(),
          hasLocalDescription = false,
          hasRemoteDescription = false,
          candidates = [],
          inited = false,
          deferMethods = [],
          events;
  
      plugin.addRef(api);
  
      events = {
        addstream: [],
        removestream: [],
        icecandidate: [],
        signalingstatechange: [],
        iceconnectionstatechange: []
      };
  
      var onAddIceCandidate = function onAddIceCandidate () {/* success */},
  
          onAddIceCandidateFailed = function onAddIceCandidateFailed (err) {
            OTPlugin.error('Failed to process candidate');
            OTPlugin.error(err);
          },
  
          processPendingCandidates = function processPendingCandidates () {
            for (var i=0; i<candidates.length; ++i) {
              plugin._.addIceCandidate(id, candidates[i], onAddIceCandidate, onAddIceCandidateFailed);
            }
          },
  
  
          deferMethod = function deferMethod (method) {
            return function() {
              if (inited === true) {
                return method.apply(api, arguments);
              }
  
              deferMethods.push([method, arguments]);
            };
          },
  
          processDeferredMethods = function processDeferredMethods () {
            var m;
            while ( (m = deferMethods.shift()) ) {
              m[0].apply(api, m[1]);
            }
          },
  
          triggerEvent = function triggerEvent (/* eventName [, arg1, arg2, ..., argN] */) {
            var args = Array.prototype.slice.call(arguments),
                eventName = args.shift();
  
            if (!events.hasOwnProperty(eventName)) {
              OTPlugin.error('PeerConnection does not have an event called: ' + eventName);
              return;
            }
  
            $.forEach(events[eventName], function(listener) {
              listener.apply(null, args);
            });
          },
  
          bindAndDelegateEvents = function bindAndDelegateEvents (events) {
            for (var name in events) {
              if (events.hasOwnProperty(name)) {
                events[name] = curryCallAsync(events[name]);
              }
            }
  
            plugin._.on(id, events);
          },
  
          addStream = function addStream (streamJson) {
            setTimeout(function() {
              var stream = MediaStream.fromJson(streamJson, plugin),
                  event = {stream: stream, target: api};
  
              if (api.onaddstream && $.isFunction(api.onaddstream)) {
                $.callAsync(api.onaddstream, event);
              }
  
              triggerEvent('addstream', event);
            }, 3000);
          },
  
          removeStream = function removeStream (streamJson) {
            var stream = MediaStream.fromJson(streamJson, plugin),
                event = {stream: stream, target: api};
  
            if (api.onremovestream && $.isFunction(api.onremovestream)) {
              $.callAsync(api.onremovestream, event);
            }
  
            triggerEvent('removestream', event);
          },
  
          iceCandidate = function iceCandidate (candidateSdp, sdpMid, sdpMLineIndex) {
            var candidate = new OTPlugin.RTCIceCandidate({
              candidate: candidateSdp,
              sdpMid: sdpMid,
              sdpMLineIndex: sdpMLineIndex
            });
  
            var event = {candidate: candidate, target: api};
  
            if (api.onicecandidate && $.isFunction(api.onicecandidate)) {
              $.callAsync(api.onicecandidate, event);
            }
  
            triggerEvent('icecandidate', event);
          },
  
          signalingStateChange = function signalingStateChange (state) {
            api.signalingState = state;
            var event = {state: state, target: api};
  
            if (api.onsignalingstatechange &&
                    $.isFunction(api.onsignalingstatechange)) {
              $.callAsync(api.onsignalingstatechange, event);
            }
  
            triggerEvent('signalingstate', event);
          },
  
          iceConnectionChange = function iceConnectionChange (state) {
            api.iceConnectionState = state;
            var event = {state: state, target: api};
  
            if (api.oniceconnectionstatechange &&
                    $.isFunction(api.oniceconnectionstatechange)) {
              $.callAsync(api.oniceconnectionstatechange, event);
            }
  
            triggerEvent('iceconnectionstatechange', event);
          };
  
      api.createOffer = deferMethod(function (success, error, constraints) {
        OTPlugin.debug('createOffer', constraints);
        plugin._.createOffer(id, function(type, sdp) {
          success(new OTPlugin.RTCSessionDescription({
            type: type,
            sdp: sdp
          }));
        }, error, constraints || {});
      });
  
      api.createAnswer = deferMethod(function (success, error, constraints) {
        OTPlugin.debug('createAnswer', constraints);
        plugin._.createAnswer(id, function(type, sdp) {
          success(new OTPlugin.RTCSessionDescription({
            type: type,
            sdp: sdp
          }));
        }, error, constraints || {});
      });
  
      api.setLocalDescription = deferMethod( function (description, success, error) {
        OTPlugin.debug('setLocalDescription');
  
        plugin._.setLocalDescription(id, description, function() {
          hasLocalDescription = true;
  
          if (hasRemoteDescription) processPendingCandidates();
          if (success) success.call(null);
        }, error);
      });
  
      api.setRemoteDescription = deferMethod( function (description, success, error) {
        OTPlugin.debug('setRemoteDescription');
  
        plugin._.setRemoteDescription(id, description, function() {
          hasRemoteDescription = true;
  
          if (hasLocalDescription) processPendingCandidates();
          if (success) success.call(null);
        }, error);
      });
  
      api.addIceCandidate = deferMethod( function (candidate) {
        OTPlugin.debug('addIceCandidate');
  
        if (hasLocalDescription && hasRemoteDescription) {
          plugin._.addIceCandidate(id, candidate, onAddIceCandidate, onAddIceCandidateFailed);
        }
        else {
          candidates.push(candidate);
        }
      });
  
      api.addStream = deferMethod( function (stream) {
        var constraints = {};
        plugin._.addStream(id, stream, constraints);
      });
  
      api.removeStream = deferMethod( function (stream) {
        plugin._.removeStream(id, stream);
      });
  
  
      api.getRemoteStreams = function () {
        return $.map(plugin._.getRemoteStreams(id), function(stream) {
          return MediaStream.fromJson(stream, plugin);
        });
      };
  
      api.getLocalStreams = function () {
        return $.map(plugin._.getLocalStreams(id), function(stream) {
          return MediaStream.fromJson(stream, plugin);
        });
      };
  
      api.getStreamById = function (streamId) {
        return MediaStream.fromJson(plugin._.getStreamById(id, streamId), plugin);
      };
  
      api.getStats = deferMethod( function (mediaStreamTrack, success, error) {
        plugin._.getStats(id, mediaStreamTrack || null, curryCallAsync(function(statsReportJson) {
          var report = new RTCStatsReport(JSON.parse(statsReportJson));
          success(report);
        }), error);
      });
  
      api.close = function () {
        plugin._.destroyPeerConnection(id);
        plugin.removeRef(this);
      };
  
      api.destroy = function () {
        api.close();
      };
  
      api.addEventListener = function  (event, handler /* [, useCapture] we ignore this */) {
        if (events[event] === void 0) {
          OTPlugin.error('Could not bind invalid event "' + event + '" to PeerConnection. ' +
                          'The valid event types are:');
          OTPlugin.error('\t' + $.keys(events).join(', '));
          return;
        }
  
        events[event].push(handler);
      };
  
      api.removeEventListener = function  (event, handler /* [, useCapture] we ignore this */) {
        if (events[event] === void 0) {
          OTPlugin.error('Could not unbind invalid event "' + event + '" to PeerConnection. ' +
                          'The valid event types are:');
          OTPlugin.error('\t' + $.keys(events).join(', '));
          return;
        }
  
        events[event] = $.filter(events[event], handler);
      };
  
      // These should appear to be null, instead of undefined, if no
      // callbacks are assigned. This more closely matches how the native
      // objects appear and allows 'if (pc.onsignalingstatechange)' type
      // feature detection to work.
      api.onaddstream = null;
      api.onremovestream = null;
      api.onicecandidate = null;
      api.onsignalingstatechange = null;
      api.oniceconnectionstatechange = null;
  
      // Both username and credential must exist, otherwise the plugin throws an error
      $.forEach(iceServers.iceServers, function(iceServer) {
        if (!iceServer.username) iceServer.username = '';
        if (!iceServer.credential) iceServer.credential = '';
      });
  
      if (!plugin._.initPeerConnection(id, iceServers, options)) {
        ready(new $.error('Failed to initialise PeerConnection'));
        return;
      }
  
      // This will make sense once init becomes async
      bindAndDelegateEvents({
        addStream: addStream,
        removeStream: removeStream,
        iceCandidate: iceCandidate,
        signalingStateChange: signalingStateChange,
        iceConnectionChange: iceConnectionChange
      });
  
      inited = true;
      processDeferredMethods();
      ready(void 0, api);
  
      return api;
    }