21

I'm using webrtc to communicate between to peers. I wan't to add new track to old generated stream, as I wan't to give functionality to users to switch their microphones during audio communications. The code I'm using is,

Let "pc" be the peerConnection object through which audio communication takes place & "newStream" be the new generated MediaStream got from getUserMedia function with new selected microphone device.

            var localStreams = pc.getLocalStreams()[0];
            localStreams.removeTrack(localStreams.getAudioTracks()[0]);


            var audioTrack = newStream.getAudioTracks()[0];
            localStreams.addTrack(audioTrack);

Is their any way that the newly added track starts reaching to the other previously connected peer without offering him again the whole SDP?

What would be the optimized way to use in such case of switch media device, i.e., microphones when the connections is already established between peers?

Akshay Rathore
  • 819
  • 1
  • 9
  • 23
  • I believe a renegotiation is always necessary when you change anything about the media streams. – deceze Feb 19 '16 at 11:49
  • Is there any other way rather than renegotiation? If not, what is the correct process to perform renegotiation – Akshay Rathore Feb 19 '16 at 12:38
  • I cannot authoritatively say that this is true for just *tracks* as well, but it certainly is for any *streams*. To renegotiate, you just need to create another offer, send it, `setRemoteDescription` on the receiver, create the answer, send it back and set it as remote description. That's pretty much it. No disconnect or ICE negotiation needs to happen, just an updated SDP needs to be exchanged. – deceze Feb 19 '16 at 12:52

1 Answers1

17

Update: working example near bottom.

This depends greatly on which browser you are using at the moment, due to an evolving spec.

In the specification and Firefox, peer connections are now fundamentally track-based, and do not depend on local stream associations. You have var sender = pc.addTrack(track, stream), pc.removeTrack(sender), and even sender.replaceTrack(track), the latter involving no renegotiation at all.

In Chrome you still have just pc.addStream and pc.removeStream, and removing a track from a local stream causes sending of it to cease, but adding it back didn't work. I had luck removing and re-adding the entire stream to the peer connection, followed by renegotiation.

Unfortunately, using adapter.js does not help here, as addTrack is tricky to polyfill.

Renegotiation

Renegotiation is not starting over. All you need is:

pc.onnegotiationneeded = e => pc.createOffer()
  .then(offer => pc.setLocalDescription(offer))
  .then(() => signalingChannel.send(JSON.stringify({sdp: pc.localDescription})));
  .catch(failed);

Once you add this, the peer connection automatically renegotiates when needed using your signaling channel. This even replaces the calls to createOffer and friends you're doing now, a net win.

With this in place, you can add/remove tracks during a live connection, and it should "just work".

If that's not smooth enough, you can even pc.createDataChannel("yourOwnSignalingChannel")

Example

Here's an example of all of that (use https fiddle in Chrome):

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

var dc, sc, 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);

var streams = [];
var haveGum = navigator.mediaDevices.getUserMedia({fake:true, video:true})
.then(stream => streams[1] = stream)
.then(() => navigator.mediaDevices.getUserMedia({ video: true }))
.then(stream => v1.srcObject = streams[0] = stream);

pc.oniceconnectionstatechange = () => update(pc.iceConnectionState);

var negotiating; // Chrome workaround
pc.onnegotiationneeded = () => {
  if (negotiating) return;
  negotiating = true;
  pc.createOffer().then(d => pc.setLocalDescription(d))
  .then(() => live && sc.send(JSON.stringify({ sdp: pc.localDescription })))
  .catch(log);
};
pc.onsignalingstatechange = () => negotiating = pc.signalingState != "stable";

function scInit() {
  sc.onmessage = e => wait(signalingDelayMs).then(() => { 
    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(log);
      } else {
        pc.setRemoteDescription(desc).catch(log);
      }
    } else if (msg.candidate) {
      pc.addIceCandidate(new RTCIceCandidate(msg.candidate)).catch(log);
    }
  }).catch(log);
}

function dcInit() {
  dc.onopen = () => {
    live = true; update("Chat:"); chat.disabled = false; chat.select();
  };
  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) {
      offer.value = pc.localDescription.sdp;
      offer.select();
      answer.placeholder = "Paste answer here";
    }
  };
  dcInit(dc = pc.createDataChannel("chat"));
  scInit(sc = pc.createDataChannel("signaling"));
};

offer.onkeypress = e => {
  if (e.keyCode != 13 || pc.signalingState != "stable") return;
  button.disabled = offer.disabled = true;
  var obj = { type:"offer", sdp:offer.value };
  pc.setRemoteDescription(new RTCSessionDescription(obj))
  .then(() => pc.createAnswer()).then(d => pc.setLocalDescription(d))
  .catch(log);
  pc.onicecandidate = e => {
    if (e.candidate) return;
    if (!live) {
      answer.focus();
      answer.value = pc.localDescription.sdp;
      answer.select();
    } else {
      sc.send(JSON.stringify({ "candidate": e.candidate }));
    }
  };
};

answer.onkeypress = e => {
  if (e.keyCode != 13 || pc.signalingState != "have-local-offer") return;
  answer.disabled = true;
  var obj = { type:"answer", sdp:answer.value };
  pc.setRemoteDescription(new RTCSessionDescription(obj)).catch(log);
};

chat.onkeypress = e => {
  if (e.keyCode != 13) return;
  dc.send(chat.value);
  log("> " + chat.value);
  chat.value = "";
};

function addTrack() {
  pc.addStream(streams[0]);
  flipButton.disabled = false;
  removeAddButton.disabled = false;
}

var flipped = 0;
function flip() {
  pc.getSenders()[0].replaceTrack(streams[flipped = 1 - flipped].getVideoTracks()[0])
  .catch(log);
}

function removeAdd() {
  if ("removeTrack" in pc) {
    pc.removeTrack(pc.getSenders()[0]);
    pc.addStream(streams[flipped = 1 - flipped]);
  } else {
    pc.removeStream(streams[flipped]);
    pc.addStream(streams[flipped = 1 - flipped]);
  }
}

var wait = ms => new Promise(resolve => setTimeout(resolve, ms));
var update = msg => div2.innerHTML = msg;
var log = msg => div.innerHTML += msg + "<br>";
<video id="v1" width="120" height="90" autoplay muted></video>
<video id="v2" width="120" height="90" 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>
<button id="removeAddButton" onclick="removeAdd()" disabled>Remove+Add</button>
<button id="flipButton" onclick="flip()" disabled>ReplaceTrack (FF only)</button>
<div id="div"><p></div><br>
<table><tr><td><div id="div2">Not connected</div></td>
  <td><input id="chat" disabled></input></td></tr></table><br>
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>

Instructions:

No server is involved, so hit Offer, then cut'n'paste offer and answer manually between two tabs (hit the ENTER key after pasting).

Once done, you can chat over the data-channel, and hit addTrack to add video to the other side.

You can then switch out video shown remotely with Remove + Add or replaceTrack (FF only) (modify fiddle in Chrome if you have a secondary camera you want to use.)

Renegotiation is all happening over the data channel now (no more cut'n'paste).

jib
  • 40,579
  • 17
  • 100
  • 158
  • Is this pc.onnegotiationneeded working with all browsers that supports webtrc? – Akshay Rathore Feb 29 '16 at 08:27
  • Yes, this is a core feature. – jib Feb 29 '16 at 13:07
  • getting error with above fiddle "TypeError: Argument 1 of RTCPeerConnection.addStream is not an object." when onclick addTrack – user969068 May 25 '17 at 08:19
  • @user969068 Thanks for letting me know! Are you on Firefox Beta by any change? It's a code snippet problem there. I've filed a [bug](https://bugzil.la/1367805) on it. Please use the [https fiddle](https://jsfiddle.net/7vxzbybp/) instead for now. – jib May 25 '17 at 17:07
  • @jib linux chrome Version 58.0.3029.110 (64-bit), I tried to send/receive offer with FireFox53.0.2 (64 bit). Actually I'm struggling hear from last couple of days, I am learning webrtc, I managed to accomplish text messaging but.. What I am trying to do is connect two peers, on page load they can do text chat, there are two buttons "Audio Call","Video Call",They can connect/disconnect audio/video calls whenever they like but text chat should remain open. your snippet helped me to understand a bit about Renegotiation but still stuck, do you have any snippet? it would be a great help, thanks – user969068 May 25 '17 at 19:14
  • @jib One more thing in what I am trying to develop, when page loads , text chat is available and it doesn't call getUserMedia(), it should call getUserMedia when click on "Audio Call" or "Video Call", That time it should ask the user permission to show the popup for allowing mic/webcam. , I went through all your webRTC replies, you seems very knowledgeable on this subject, kindly if you can guide me with a snippet, , here you will find an example I am using to make text chat work and trying to extend with functionality – user969068 May 25 '17 at 20:55
  • I described above https://stackoverflow.com/questions/44167006/webrtc-datachannel-is-undefined-for-second-peer, thanks again – user969068 May 25 '17 at 20:55
  • @jib , negotiation is not working b/w chrome and firefox , have any idea about that ? – Vivek Doshi May 30 '18 at 03:58
  • @VivekDoshi Sounds like a new question. [wfm](https://jsfiddle.net/jib1/uLw7c3jn/). – jib May 30 '18 at 18:58
  • @wfm , https://stackoverflow.com/questions/50562768/webrtc-renegotiation-firefox-to-chrome-vice-versa-not-updating-streams – Vivek Doshi May 31 '18 at 04:17
  • Missing a closing parenthesis @ '.then(() => signalingChannel.send(JSON.stringify({ "sdp": pc.localDescription }))' .stringify is not being closed. – danefondo Sep 27 '20 at 14:30
  • 2
    @robertfoenix fixed, thanks! At least I had it right in the working examples. ;) – jib Sep 29 '20 at 01:48
  • Yes, that helps! haha – danefondo Sep 29 '20 at 10:34