11

The browser used is Chrome... I have the caller and receiver code to generate SDP and ICE candidates. I get the caller code to generate proper SDP and ICE candidates with sdpMid=video but for the receiver, I am getting ICE candidates generated only for sdpMid=audio.

UPDATE: Here is the localSessionDescription SDP for the receiver after changes, as suggested:

 v=0
 o=- 7912682607537349212 2 IN IP4 127.0.0.1
 s=-
 t=0 0
 a=group:BUNDLE audio video
 a=msid-semantic: WMS 9f0MAtEwYGWY3pdBDI8ZtTu4dVu92R6IpEFd
 m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 126
 c=IN IP4 0.0.0.0
 a=rtcp:9 IN IP4 0.0.0.0
 a=ice-ufrag:0D1hLEwxnqReQosQ
 a=ice-pwd:Nsc4EAtefrfgzTetHjJA5lsg
 a=fingerprint:sha-256 6C:85:D8:33:D8:C6:CB:CE:D4:8E:B4:7A:C2:F5:2F:D0:67:04:25:B2:74:F9:C6:3A:2E:96:E6:56:E7:27:B0:F8
 a=setup:active
 a=mid:audio
 a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
 a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
 a=sendrecv
 a=rtcp-mux
 a=rtpmap:111 opus/48000/2
 a=fmtp:111 minptime=10; useinbandfec=1
 a=rtpmap:103 ISAC/16000
 a=rtpmap:104 ISAC/32000
 a=rtpmap:9 G722/8000
 a=rtpmap:0 PCMU/8000
 a=rtpmap:8 PCMA/8000
 a=rtpmap:106 CN/32000
 a=rtpmap:105 CN/16000
 a=rtpmap:13 CN/8000
 a=rtpmap:126 telephone-event/8000
 a=maxptime:60
 a=ssrc:2958641119 cname:Iu8s16HLxglPDg9k
 a=ssrc:2958641119 msid:9f0MAtEwYGWY3pdBDI8ZtTu4dVu92R6IpEFd bb63739b-cca2-4aa5-90a6-cf4bbaa199af
 a=ssrc:2958641119 mslabel:9f0MAtEwYGWY3pdBDI8ZtTu4dVu92R6IpEFd
 a=ssrc:2958641119 label:bb63739b-cca2-4aa5-90a6-cf4bbaa199af
 m=video 9 UDP/TLS/RTP/SAVPF 100 101 116 117 96
 c=IN IP4 0.0.0.0
 a=rtcp:9 IN IP4 0.0.0.0
 a=ice-ufrag:0D1hLEwxnqReQosQ
 a=ice-pwd:Nsc4EAtefrfgzTetHjJA5lsg
 a=fingerprint:sha-256 6C:85:D8:33:D8:C6:CB:CE:D4:8E:B4:7A:C2:F5:2F:D0:67:04:25:B2:74:F9:C6:3A:2E:96:E6:56:E7:27:B0:F8
 a=setup:active
 a=mid:video
 a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
 a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
 a=extmap:4 urn:3gpp:video-orientation
 a=sendrecv
 a=rtcp-mux
 a=rtpmap:100 VP8/90000
 a=rtcp-fb:100 ccm fir
 a=rtcp-fb:100 nack
 a=rtcp-fb:100 nack pli
 a=rtcp-fb:100 goog-remb
 a=rtcp-fb:100 transport-cc
 a=rtpmap:101 VP9/90000
 a=rtcp-fb:101 ccm fir
 a=rtcp-fb:101 nack
 a=rtcp-fb:101 nack pli
 a=rtcp-fb:101 goog-remb
 a=rtcp-fb:101 transport-cc
 a=rtpmap:116 red/90000
 a=rtpmap:117 ulpfec/90000
 a=rtpmap:96 rtx/90000
 a=fmtp:96 apt=100
 a=ssrc-group:FID 3143004909 4248148453
 a=ssrc:3143004909 cname:Iu8s16HLxglPDg9k
 a=ssrc:3143004909 msid:9f0MAtEwYGWY3pdBDI8ZtTu4dVu92R6IpEFd 778ef702-e7fc-47ea-bb3a-477e0b4262ba
 a=ssrc:3143004909 mslabel:9f0MAtEwYGWY3pdBDI8ZtTu4dVu92R6IpEFd
 a=ssrc:3143004909 label:778ef702-e7fc-47ea-bb3a-477e0b4262ba
 a=ssrc:4248148453 cname:Iu8s16HLxglPDg9k
 a=ssrc:4248148453 msid:9f0MAtEwYGWY3pdBDI8ZtTu4dVu92R6IpEFd 778ef702-e7fc-47ea-bb3a-477e0b4262ba
 a=ssrc:4248148453 mslabel:9f0MAtEwYGWY3pdBDI8ZtTu4dVu92R6IpEFd
 a=ssrc:4248148453 label:778ef702-e7fc-47ea-bb3a-477e0b4262ba

This is generated for the corresponding getUserMedia as in:

 navigator.getUserMedia({ audio: true, video: { width: 1280, height: 720 } },...

The ICE candidate generation code is:

pc.onicecandidate = function (event) {
   console.log("Generated Icecandidate:" );
   console.log(event);
   ...
 };

On console.log, I see ICE candidates, as in:

RTCIceCandidate
candidate: "candidate:211156821 1 udp 2122260223 192.168.1.5 41811 typ host generation 0 ufrag kV5Snl0LQhJlYujt"
sdpMLineIndex:0
sdpMid:"audio"

Needless to say, I am not able to get the remote video displayed. I am trying this on a local network so really even STUN is not required.

I would like to know, why I am not getting any ICE candidates for sdpMid=video. Also, of the four ICE candidates generated,three ICE candidates have a sdpMLineIndex:0 and one ICE candidate has candidate property as null!

UPDATE 1: I got the answer to the last issue... Candidate property as Null. "Note: the RTCPeerConnection.onicecandidate will be called once with an empty candidate property to signal the end of trickle ICE event." This is explained here.

On the caller side, I get more than 10 ICE candidates, some with audio and some with video.

Where am I going wrong?

UPDATE 2: Here is the code for the receiver part which does not generate ICE candidates for video. I have stripped authentication and other sections to focus only on the relevant part. I removed caching of ICE candidate and send it as it comes:

$(document).ready(function () {

  var socket = io.connect();
  var pc = new RTCPeerConnection ({
    "iceServers": [{"url": "stun:stun.l.google.com:19302"}]
  });

  pc.onicecandidate = function (event) {
    socket.emit('candidateFromReceiver',event.candidate);
    console.log("Candidate Generated:");
    console.log(event.candidate);
  }; 

  pc.onaddstream = function(ev) {
    stream = ev.stream;        
    var video = $('#vid2'); 
    video.attr('src', URL.createObjectURL(stream));
    video.onloadedmetadata = function(e) {
      video.play();
    }
  };

  socket.on('connect',function() { console.log("Socket connected"); });
  socket.on('candidateFromCaller', function (data) {
      pc.addIceCandidate(new RTCIceCandidate(data));
  });

  navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia ||
                       navigator.mozGetUserMedia;
  if (navigator.getUserMedia) {
    navigator.getUserMedia({ audio: true, video: { width: 1280, height: 720 } },
      function(stream) {
         var video = $('#vid1'); 
         video.attr('src', URL.createObjectURL(stream));
         video.onloadedmetadata = function(e) {
           video.play();
         }
     pc.addStream(stream);
      },error);

    socket.on('sdpOffer', function(data) {
      var sdpOffer = new RTCSessionDescription(data.sdpOffer);
      pc.setRemoteDescription(sdpOffer, function() {
        pc.createAnswer(function(sdpAnswer) {
          localSessionDescription = new RTCSessionDescription(sdpAnswer);
          pc.setLocalDescription(localSessionDescription, function() {
            socket.emit('sdpAnswer',localSessionDescription);
          },error);
        }, error);
      },error);
    });
  }

  function error(err) {
    console.log("ERROR!!!!");
    console.log(err);
  }

}); // End of document.ready function

If I insert code, just after getting user media, to generate an Offer (as I have in the caller code), the ICE candidates generated include ones for video too. Of course, that was only for testing as the rest of the code bombs after that, as expected.

jib
  • 40,579
  • 17
  • 100
  • 158
Sunny
  • 9,245
  • 10
  • 49
  • 79
  • 1
    Hard to say what's wrong since you haven't posted any code to look at. The sdp (answer) you post seems to have `m=` lines for both audio and video, so that seems fine. Have you tried switching caller and receiver? – jib Apr 25 '16 at 17:55
  • @jib Switching caller and receiver machines did not help. Code is too long to send. Summary: Both SDPs, have m=audio and m=video lines. In CALLER&RECEIVER, I store the ICE-candidates in an array. This code is just after creating PeerConnection. In CALLER, upon receipt of answer-SDP, I set the remoteDescription and send the array over to the receiver. In RECEIVER, upon receipt of offer-SDP, I set the remoteDescription and send the array over to the caller. I suspect that there is something wrong in this sequence. I was reading RFC 5245. If you can provide some pointers, that will help.Thnx – Sunny Apr 25 '16 at 19:02
  • 1
    The whole array-thing seems suspect. Why are you doing that? The whole point of Trickle ICE is to trickle, that is, send candidates as soon as they become available. Don't cache them. On the caller side, waiting until you have an answer back just wastes a lot of time. In contrast, on the receiver side, I suspect the array is empty, as you're too early. ICE gathering starts after set*Local*Description, which on the receiver side happens after setRemoteDescription. – jib Apr 25 '16 at 20:56
  • 1
    This isn't plain JavaScript. JQuery is it? Not familiar with `video.attr('src', URL.createObjectURL(stream));`, have you tried `video.src = URL.createObjectURL(stream);`? Also, this is local video... Where's remote video set up? Are you hearing audio remotely? – jib Apr 26 '16 at 13:44
  • @jib The Jquery part is irrelevant I think and only comes into play for setting attributes of HTML elements and it works fine. There is other code that is not included that relies on JQuery. I think that how the stream, local or remote, is displayed locally has no relevance to seeing ICE candidates? Or does it? In any event, in both caller and receiver, I get to see the local video displayed in the div (locally). I hear the audio from caller to receiver but not the other way around. I did not set the remote video as I am not seeing the ICE candidate yet. Every comment of yours is VERY useful. – Sunny Apr 26 '16 at 13:57
  • @jib Eureka 1! I get to see the remote video from the caller in the receiver! I am not able to see the remote video from the receiver in the caller... so I just need to get the ICE candidate issue fixed in the receiver code above! Getting there! – Sunny Apr 26 '16 at 14:10
  • @jib I set only one `const peerConn` for all sides(caller and callee). I set `onicecandidate` candidate for `peerConn` but as I can see in my console - after `offer-answer` exchange only CALLER send own iceCandidates to callee. Callee - never sended candidates. `peerConn.onicecandidate = function(e){ //send candidates to server}`. – Сергей Mar 29 '19 at 16:24

3 Answers3

14

(It sounds from comments that you're caching ICE candidates. Don't do that. I also suspect timing issues may be behind loss of some candidates.)

The whole point of Trickle ICE is to trickle candidates, that is, send candidates as soon as they become available.

With WebRTC, your app is responsible for signaling between peers, which is time-sensitive. So:

  1. Send pc.localDescription no later than in the setLocalDescription success callback.
  2. Expect pc.onicecandidate to start firing immediately after that callback. Send them.

This is true on both sides (for offer and answer). What you want to see on the wire is:

offer, candidate, candidate, candidate

and the other way:

answer, candidate, candidate, candidate

What not to do:

  • Don't cache ICE candidates.
  • Don't wait until you have an answer back, that just wastes time.
  • Don't delay calling setRemoteDescription when an offer comes in on the receiving end for any reason, or it wont be ready to receive candidates.

Update 2:

Your sdp says a=recvonly and not a=sendrecv, which means the receiver is resigned to receive only, without send anything in return. One of two things can cause this:

  1. Caller set createOffer options like offerToReceiveVideo:false and/or offerToReceiveAudio:false.
  2. Receiver did not called pc.addStream in time for (before) pc.setLocalDescription.

The second can happen if there's a race between getUserMedia and receiving an offer.

Update 3:

If all else fails, compare to working code. I've shared a cross-tab demo before in other answers, but it only sent video, didn't receive any.

Here's a modified version of that demo that only receives video from the remote camera instead. As usual, open it in two tabs in the same browser.

Note that in Firefox, after you hit Call, you have to physically focus the other tab before it allows access to the camera.

jib
  • 40,579
  • 17
  • 100
  • 158
  • I followed your instructions but still no luck... The stripped code is provided... I just want to see proper ICE candidates. This code is intended only to resolve that limited issue. Thanks a LOT. Your comments have been a great help. It would be exciting to see the remote video display... hope to get there soon! – Sunny Apr 26 '16 at 13:15
  • I am marking your answer as accepted. I have updated code to display video from caller. The video from receiver is still not seen on the caller side, as expected, because the local ICE candidates for the receiver do not include one for video. Any pointers from you would be appreciated. – Sunny Apr 26 '16 at 16:16
  • Sorry I missed your update 2 earlier. I did a few changes yesterday based upon your suggestions and now the a=sendrecv for both audio and video for the receiver. I did not check this part yesterday after the changes. Checked them now after reading your update 2. I think the reason for the problem yesterday was point 2 of your update 2. – Sunny Apr 26 '16 at 17:57
  • I have added the latest SDP for the receiver. If you get time to look... I am getting there. You have been a big help. – Sunny Apr 26 '16 at 18:07
  • 1
    @Sam with the latest answer you show with `sendrecv` for both audio & video m-lines, I'd expect ICE candidates to be generated for both audio and video right after you `setLocalDescription(answer)`. I don't see anything wrong, sorry. - The only thing that stuck out was your `pc.onicecandidate` which is usually side-agnostic (same code on both ends), but you have it wired to only work one way ('candidateFromReceiver'), hopefully that's just and artifact of reducing the example for this post. – jib Apr 26 '16 at 22:18
  • "pc.onicecandidate which is usually side-agnostic." I have separate code for receiver & caller. I have posted only the receiver code. Any ICE candidates generated by receiver are sent to the Caller and that event is 'candidateFromReceiver' Similarly, the Caller code emits corresponding 'candidateFromCaller' which is sent from the Caller to the receiver as you can see in the above code socket.on('candidateFromCaller'...). I could have just labeled the event as 'Candidate' on both sides and the code would be reduced in the signaling server.But that is not the problem here... Right? – Sunny Apr 27 '16 at 11:09
  • separate code, as in separate file/url for receiver and caller. HTTP server and signalling server is integrated... based upon Node.js. – Sunny Apr 27 '16 at 13:02
  • @Sam commonly people use the same file on both ends I think, in spite of the inherent asymmetry. I'll update the answer with a fiddle to look at, which may help. – jib Apr 27 '16 at 14:32
  • Thanks for all your help. I have rewritten the code to be in a single file and using promises. I am trying to follow your example, which works, except that I do not know coffeescript. Will see how it goes. I think your example will be a BIG help to MANY PEOPLE, if you can have another pure Javascript version ALSO posted separately outside this clutter here. I mean a self-answered post. IT WILL BE TRULY USEFUL TO MANY WHO STRUGGLE THE FIRST TIME AROUND. I have learned a LOT just with your comments but the example posted in coffeescript was the ultimate help. In pure Jvascript it will b awesome. – Sunny Apr 28 '16 at 04:14
  • Sam, curious, did you get this resolved and if so, what was the issue with the ice candidates only being seen for audio from the receiver? – SBG May 30 '17 at 16:53
  • @jib Question on bundles (see https://tools.ietf.org/html/draft-ietf-mmusic-sdp-bundle-negotiation-37#page-28 ), i am wondering whether this has to do with bundles? The fact that you only see audio and that the audio/video is bundled per the SDP - in the spec it says "11.1.2. Generating the SDP Answer When an answerer generates an answer that contains a BUNDLE group, the answerer MUST associate ICE-related SDP attributes with the "m=" line associated with the answerer BUNDLE-tag, according to [Section 11.1].") - thoughts? – SBG May 30 '17 at 18:39
  • I am also facing same issue in android .not able to get video candidate from receiver side, I tried lot still no result, can anyone has more idea about this? – Richa Shah Apr 17 '20 at 04:56
  • @jib would you please help me to sort out my problem on https://stackoverflow.com/questions/63862778/webrtc-video-streaming-is-working-in-firefox-but-not-in-chrome I will be very much delighted if you help me. – Rakibul Islam Prince Sep 13 '20 at 18:09
3

What you are dealing with is called bundeling. Offerer and answerer agree to bundle all ICE transports into a single ICE transport. Therefore you only get a single ICE candidate for the first m-section.

The offerer is still giving you all these ICE candidates for the video m-section, because it does not know if the answerer is going to agree to use bundle.

As AlexD points out in his answer you can influence this behavior on the offerer side via the "bundlePolicy". "maxBundle" as policy will for example result in the offerer assuming the answerer is going to understand bundleling and therefore only create ICE candidates for a single transport.

But as long as the offerer is offering bundle the answerer is going to use it if it supports it.

Nils Ohlmeier
  • 1,061
  • 7
  • 8
2

I was able to solve this by setting:

rtcConfiguration.bundlePolicy = "max-compat"

See: http://w3c.github.io/webrtc-pc/#dom-rtcbundlepolicy-max-compat

jib
  • 40,579
  • 17
  • 100
  • 158
AlexD
  • 813
  • 9
  • 15