How to addTrack in MediaStream in WebRTC
Asked Answered
S

1

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?

Secessionist answered 19/2, 2016 at 11:28 Comment(3)
I believe a renegotiation is always necessary when you change anything about the media streams.Clothesline
Is there any other way rather than renegotiation? If not, what is the correct process to perform renegotiationSecessionist
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.Clothesline
R
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).

Realism answered 19/2, 2016 at 21:38 Comment(13)
Is this pc.onnegotiationneeded working with all browsers that supports webtrc?Secessionist
Yes, this is a core feature.Realism
getting error with above fiddle "TypeError: Argument 1 of RTCPeerConnection.addStream is not an object." when onclick addTrackNaresh
@Naresh 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 on it. Please use the https fiddle instead for now.Realism
@Realism 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, thanksNaresh
@Realism 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 functionalityNaresh
I described above #44167506, thanks againNaresh
@Realism , negotiation is not working b/w chrome and firefox , have any idea about that ?Farwell
@VivekDoshi Sounds like a new question. wfm.Realism
@wfm , #50563268Farwell
Missing a closing parenthesis @ '.then(() => signalingChannel.send(JSON.stringify({ "sdp": pc.localDescription }))' .stringify is not being closed.Northcliffe
@robertfoenix fixed, thanks! At least I had it right in the working examples. ;)Realism
Yes, that helps! hahaNorthcliffe

© 2022 - 2024 — McMap. All rights reserved.