text: function TraceablePeerConnection(ice_config, constraints) {
      var self = this;
      var RTCPeerconnection = navigator.mozGetUserMedia ? mozRTCPeerConnection : webkitRTCPeerConnection;
      this.peerconnection = new RTCPeerconnection(ice_config, constraints);
      this.updateLog = [];
      this.stats = {};
      this.statsinterval = null;
      this.maxstats = 0; // limit to 300 values, i.e. 5 minutes; set to 0 to disable
  
      // override as desired
      this.trace = function (what, info) {
          //console.warn('WTRACE', what, info);
          self.updateLog.push({
              time: new Date(),
              type: what,
              value: info || ""
          });
      };
      this.onicecandidate = null;
      this.peerconnection.onicecandidate = function (event) {
          self.trace('onicecandidate', JSON.stringify(event.candidate, null, ' '));
          if (self.onicecandidate !== null) {
              self.onicecandidate(event);
          }
      };
      this.onaddstream = null;
      this.peerconnection.onaddstream = function (event) {
          self.trace('onaddstream', event.stream.id);
          if (self.onaddstream !== null) {
              self.onaddstream(event);
          }
      };
      this.onremovestream = null;
      this.peerconnection.onremovestream = function (event) {
          self.trace('onremovestream', event.stream.id);
          if (self.onremovestream !== null) {
              self.onremovestream(event);
          }
      };
      this.onsignalingstatechange = null;
      this.peerconnection.onsignalingstatechange = function (event) {
          self.trace('onsignalingstatechange', self.signalingState);
          if (self.onsignalingstatechange !== null) {
              self.onsignalingstatechange(event);
          }
      };
      this.oniceconnectionstatechange = null;
      this.peerconnection.oniceconnectionstatechange = function (event) {
          self.trace('oniceconnectionstatechange', self.iceConnectionState);
          if (self.oniceconnectionstatechange !== null) {
              self.oniceconnectionstatechange(event);
          }
      };
      this.onnegotiationneeded = null;
      this.peerconnection.onnegotiationneeded = function (event) {
          self.trace('onnegotiationneeded');
          if (self.onnegotiationneeded !== null) {
              self.onnegotiationneeded(event);
          }
      };
      self.ondatachannel = null;
      this.peerconnection.ondatachannel = function (event) {
          self.trace('ondatachannel', event);
          if (self.ondatachannel !== null) {
              self.ondatachannel(event);
          }
      };
      if (!navigator.mozGetUserMedia && this.maxstats) {
          this.statsinterval = window.setInterval(function() {
              self.peerconnection.getStats(function(stats) {
                  var results = stats.result();
                  for (var i = 0; i < results.length; ++i) {
                      //console.log(results[i].type, results[i].id, results[i].names())
                      var now = new Date();
                      results[i].names().forEach(function (name) {
                          var id = results[i].id + '-' + name;
                          if (!self.stats[id]) {
                              self.stats[id] = {
                                  startTime: now,
                                  endTime: now,
                                  values: [],
                                  times: []
                              };
                          }
                          self.stats[id].values.push(results[i].stat(name));
                          self.stats[id].times.push(now.getTime());
                          if (self.stats[id].values.length > self.maxstats) {
                              self.stats[id].values.shift();
                              self.stats[id].times.shift();
                          }
                          self.stats[id].endTime = now;
                      });
                  }
              });
  
          }, 1000);
      }
  };
  
  dumpSDP = function(description) {
      return 'type: ' + description.type + '\r\n' + description.sdp;
  }
  
  if (TraceablePeerConnection.prototype.__defineGetter__ !== undefined) {
      TraceablePeerConnection.prototype.__defineGetter__('signalingState', function() { return this.peerconnection.signalingState; });
      TraceablePeerConnection.prototype.__defineGetter__('iceConnectionState', function() { return this.peerconnection.iceConnectionState; });
      TraceablePeerConnection.prototype.__defineGetter__('localDescription', function() {
          var publicLocalDescription = simulcast.reverseTransformLocalDescription(this.peerconnection.localDescription);
          return publicLocalDescription;
      });
      TraceablePeerConnection.prototype.__defineGetter__('remoteDescription', function() {
          var publicRemoteDescription = simulcast.reverseTransformRemoteDescription(this.peerconnection.remoteDescription);
          return publicRemoteDescription;
      });
  }
  
  TraceablePeerConnection.prototype.addStream = function (stream) {
      this.trace('addStream', stream.id);
      simulcast.resetSender();
      try
      {
          this.peerconnection.addStream(stream);
      }
      catch (e)
      {
          console.error(e);
          return;
      }
  };
  
  TraceablePeerConnection.prototype.removeStream = function (stream, stopStreams) {
      this.trace('removeStream', stream.id);
      simulcast.resetSender();
      if(stopStreams) {
          stream.getAudioTracks().forEach(function (track) {
              track.stop();
          });
          stream.getVideoTracks().forEach(function (track) {
              track.stop();
          });
      }
      this.peerconnection.removeStream(stream);
  };
  
  TraceablePeerConnection.prototype.createDataChannel = function (label, opts) {
      this.trace('createDataChannel', label, opts);
      return this.peerconnection.createDataChannel(label, opts);
  };
  
  TraceablePeerConnection.prototype.setLocalDescription = function (description, successCallback, failureCallback) {
      var self = this;
      description = simulcast.transformLocalDescription(description);
      this.trace('setLocalDescription', dumpSDP(description));
      this.peerconnection.setLocalDescription(description,
          function () {
              self.trace('setLocalDescriptionOnSuccess');
              successCallback();
          },
          function (err) {
              self.trace('setLocalDescriptionOnFailure', err);
              failureCallback(err);
          }
      );
      /*
       if (this.statsinterval === null && this.maxstats > 0) {
       // start gathering stats
       }
       */
  };
  
  TraceablePeerConnection.prototype.setRemoteDescription = function (description, successCallback, failureCallback) {
      var self = this;
      description = simulcast.transformRemoteDescription(description);
      this.trace('setRemoteDescription', dumpSDP(description));
      this.peerconnection.setRemoteDescription(description,
          function () {
              self.trace('setRemoteDescriptionOnSuccess');
              successCallback();
          },
          function (err) {
              self.trace('setRemoteDescriptionOnFailure', err);
              failureCallback(err);
          }
      );
      /*
       if (this.statsinterval === null && this.maxstats > 0) {
       // start gathering stats
       }
       */
  };
  
  TraceablePeerConnection.prototype.close = function () {
      this.trace('stop');
      if (this.statsinterval !== null) {
          window.clearInterval(this.statsinterval);
          this.statsinterval = null;
      }
      this.peerconnection.close();
  };
  
  TraceablePeerConnection.prototype.createOffer = function (successCallback, failureCallback, constraints) {
      var self = this;
      this.trace('createOffer', JSON.stringify(constraints, null, ' '));
      this.peerconnection.createOffer(
          function (offer) {
              self.trace('createOfferOnSuccess', dumpSDP(offer));
              successCallback(offer);
          },
          function(err) {
              self.trace('createOfferOnFailure', err);
              failureCallback(err);
          },
          constraints
      );
  };
  
  TraceablePeerConnection.prototype.createAnswer = function (successCallback, failureCallback, constraints) {
      var self = this;
      this.trace('createAnswer', JSON.stringify(constraints, null, ' '));
      this.peerconnection.createAnswer(
          function (answer) {
              answer = simulcast.transformAnswer(answer);
              self.trace('createAnswerOnSuccess', dumpSDP(answer));
              successCallback(answer);
          },
          function(err) {
              self.trace('createAnswerOnFailure', err);
              failureCallback(err);
          },
          constraints
      );
  };
  
  TraceablePeerConnection.prototype.addIceCandidate = function (candidate, successCallback, failureCallback) {
      var self = this;
      this.trace('addIceCandidate', JSON.stringify(candidate, null, ' '));
      this.peerconnection.addIceCandidate(candidate);
      /* maybe later
       this.peerconnection.addIceCandidate(candidate,
       function () {
       self.trace('addIceCandidateOnSuccess');
       successCallback();
       },
       function (err) {
       self.trace('addIceCandidateOnFailure', err);
       failureCallback(err);
       }
       );
       */
  };
  
  TraceablePeerConnection.prototype.getStats = function(callback, errback) {
      if (navigator.mozGetUserMedia) {
          // ignore for now...
          if(!errback)
              errback = function () {
  
              }
          this.peerconnection.getStats(null,callback,errback);
      } else {
          this.peerconnection.getStats(callback);
      }
  };
  
  