Modern WebRTC remote renegotiation

Shows modern WebRTC use with cut'n'paste Offer/Answer exchange in Firefox Dev Edition w/renegotiation through datachannel.

by jib1

HTML

<video id="v1" height="120" width="160" autoplay muted></video>
<video id="v2" height="120" width="160" autoplay></video><br>
<button id="button" onclick="createOffer()">Offer:</button>
<textarea id="offer" placeholder="Paste offer here"></textarea><br>
Answer: <textarea id="answer"></textarea><br>
<button id="button" onclick="addTrack()">AddTrack</button>
<div id="div"></div><br>
Chat: <input id="chat"></input><br>
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>

JavaScript

var config = { iceServers: [{ urls: "stun:stun.l.google.com:19302" }] };

var dc = null, sc = null, pc = new RTCPeerConnection(config), live = false;
pc.onaddstream = e => v2.srcObject = e.stream;
pc.ondatachannel = e => dc? scInit(sc = e.channel) : dcInit(dc = e.channel);
v2.onloadedmetadata = e => { log("Face time!"); };

function addTrack() {
  navigator.mediaDevices.getUserMedia({video:true, audio:true})
  .then(stream => pc.addStream(v1.srcObject = stream));
}

var negotiated = false; // work around FF39- not self-firing negotiationneeded
pc.onnegotiationneeded = e => {
  negotiated = true;
  pc.createOffer().then(d => pc.setLocalDescription(d)).then(() => {
    if (live) sc.send(JSON.stringify({ "sdp": pc.localDescription }));
  }).catch(failed);
};

function scInit() {
  sc.onmessage = e => {
    var msg = JSON.parse(e.data);
    if (msg.sdp) {
      var desc = new RTCSessionDescription(JSON.parse(e.data).sdp);
      if (desc.type == "offer") {
        pc.setRemoteDescription(desc).then(() => pc.createAnswer())
        .then(answer => pc.setLocalDescription(answer)).then(() => {
          sc.send(JSON.stringify({ "sdp": pc.localDescription }));
        }).catch(failed);
      } else {
        pc.setRemoteDescription(desc).catch(failed);
      }
    } else if (msg.candidate) {
      pc.addIceCandidate(new RTCIceCandidate(msg.candidate)).catch(failed);
    }
  };
}

function dcInit() {
  dc.onopen = () => { live = true; log("Chat!"); };
  dc.onmessage = e => log(e.data);
}

function createOffer() {
  button.disabled = true;
  pc.onicecandidate = e => {
    if (live) {
      sc.send(JSON.stringify({ "candidate": e.candidate }));
    } else {
      if (e.candidate) return;
      offer.value = pc.localDescription.sdp;
      offer.select();
      answer.placeholder = "Paste answer here";
    }
  };
  dcInit(dc = pc.createDataChannel("chat"));
  scInit(sc = pc.createDataChannel("signaling"));
  if (!negotiated) {
    pc.onnegotiationneeded();
 ...