3
votes

I'm new to WebRTC and am trying to create a simple test page that allows for 1-to-1 video conferences.

So far what I have below works for the following:

  • Chrome -> Chrome
  • Firefox -> Chrome

It does not work in the following situations:

  • Firefox -> Firefox
  • Chrome -> Firefox

Basically, Firefox is unable to receive the remote video stream when it is the callee. The onaddstream handler is never called, but I do not know why.

The only difference I have found between Firefox and Chrome is that Firefox does not trickle ICE candidates; they are included in the initial offer/answer creation causing the onicecandidate handler to never be called with a non-null candidate. On the other hand, Chrome does trickle candidates which are sent to the signaling server as separate messages. I'm not sure if this a/the potential problem or not.

I am testing with Firefox Nightly 34 and Chrome 36. The signaling server is hosted in my LAN and both browsers are running on the same computer.

The following code was based off of http://www.html5rocks.com/en/tutorials/webrtc/basics/.

var localVideo;
var remoteVideo;
var peerConnection;
var peerConnectionConfig = {'iceServers': [{'url': 'stun:stun.services.mozilla.com'}, {'url': 'stun:stun.l.google.com:19302'}]};
var isCaller;

navigator.getUserMedia = navigator.getUserMedia || navigator.mozGetUserMedia || navigator.webkitGetUserMedia;
window.RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
window.RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate || window.webkitRTCIceCandidate;
window.RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription || window.webkitRTCSessionDescription;

function pageReady() {
    localVideo = document.getElementById('localVideo');
    remoteVideo = document.getElementById('remoteVideo');

    serverConnection = new WebSocket('ws://myserver:3434');
    serverConnection.onmessage = gotMessageFromServer;
}

function start(_isCaller) {
    isCaller = _isCaller

    peerConnection = new RTCPeerConnection(peerConnectionConfig);
    peerConnection.onicecandidate = gotIceCandidate;
    peerConnection.onaddstream = gotRemoteStream;

    var constraints = {
        video: true,
        audio: true, 
    };

    if(navigator.getUserMedia) {
        navigator.getUserMedia(constraints, getUserMediaSuccess, getUserMediaError);
    } else {
        alert('Your browser does not support getUserMedia API');
    }
}

function getUserMediaSuccess(stream) {
    localStream = stream;
    localVideo.src = window.URL.createObjectURL(stream);

    peerConnection.addStream(stream);

    if(isCaller) {
        peerConnection.createOffer(gotDescription, createOfferError);
    } else {
        peerConnection.createAnswer(gotDescription, createAnswerError);
    }
}

function gotMessageFromServer(message) {
    if(!peerConnection) start(false);

    var signal = JSON.parse(message.data);
    if(signal.sdp) {
        peerConnection.setRemoteDescription(new RTCSessionDescription(signal.sdp));
    } else if(signal.ice) {
        peerConnection.addIceCandidate(new RTCIceCandidate(signal.ice));
    }
}

function gotIceCandidate(event) {
    if(event.candidate != null) {
        serverConnection.send(JSON.stringify({'ice': event.candidate}));
    }
}

function gotDescription(description) {
    peerConnection.setLocalDescription(description);
    serverConnection.send(JSON.stringify({'sdp': description}));
}

function gotMessageFromServer(message) {
    if(!peerConnection) start(false);

    var signal = JSON.parse(message.data);
    if(signal.sdp) {
        peerConnection.setRemoteDescription(new RTCSessionDescription(signal.sdp));
    } else if(signal.ice) {
        peerConnection.addIceCandidate(new RTCIceCandidate(signal.ice));
    }
}

function gotRemoteStream(event) {
    console.log("got remote stream");
    remoteVideo.src = window.URL.createObjectURL(event.stream);
}

// Error functions....

function getUserMediaError(error) {
    console.log(error);
}

function createOfferError(error) {
    console.log(error);
}

function createAnswerError(error) {
    console.log(error);
}

If it's useful, here is my signaling server. It's a very basic Node.js script that uses the ws WebSocket library to simply broadcast received messages to all connected clients which is only one other client in this case.

var WebSocketServer = require('ws').Server;

var wss = new WebSocketServer({port: 3434});

wss.broadcast = function(data) {
    for(var i in this.clients) {
        this.clients[i].send(data);
    }
};

wss.on('connection', function(ws) {
    ws.on('message', function(message) {
        console.log('received: %s', message);
        wss.broadcast(message);
    });
});

Any help is greatly appreciated!

1
make sure you include the DTLS option in your peerconnection optional configuration optional = { optional: [{ DtlsSrtpKeyAgreement: true}] } and peerConnection = new RTCPeerConnection(peerConnectionConfig, optional);. Also, you may have to wait for all icecandidates to be gathered(though, this will only really effect you when you are not behind the same NAT).Benjamin Trent
I tried adding DtlsSrtpKeyAgreement option to the RTCPeerConnection and I'm unfortunately still hitting the same issue. Both the server and clients are on the same LAN so I shouldn't be hitting any NAT issues. Regardless, I have checked the icegatheringstate property and it shows complete yet I'm still not able to get Firefox to receive a stream when it is the callee.shanet

1 Answers

1
votes

Turns out I had a race condition between the call to setRemoteDescription() and createAnswer(). Firefox asks for permission to use the webcam on every reload of the page whereas Chrome only asks on the first page load. This meant that When I went to call createAnswer() in Firefox, the video stream did not exist yet because the user had not allowed access to the webcam yet. The solution was to move the call to getUserMedia() to when the page is ready. This way, when an incoming call occurs the user has already allowed access to the webcam and the video stream can immediately be shared. It's not perfect, but it allows my little test page to work which is all I'm going for for now.

Here's the updated code if it helps anyone in the future:

var localVideo;
var remoteVideo;
var peerConnection;
var peerConnectionConfig = {'iceServers': [{'url': 'stun:stun.services.mozilla.com'}, {'url': 'stun:stun.l.google.com:19302'}]};

navigator.getUserMedia = navigator.getUserMedia || navigator.mozGetUserMedia || navigator.webkitGetUserMedia;
window.RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
window.RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate || window.webkitRTCIceCandidate;
window.RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription || window.webkitRTCSessionDescription;

function pageReady() {
    localVideo = document.getElementById('localVideo');
    remoteVideo = document.getElementById('remoteVideo');

    serverConnection = new WebSocket('ws://sagan:3434');
    serverConnection.onmessage = gotMessageFromServer;

    var constraints = {
        video: true,
        audio: true, 
    };

    if(navigator.getUserMedia) {
        navigator.getUserMedia(constraints, getUserMediaSuccess, getUserMediaError);
    } else {
        alert('Your browser does not support getUserMedia API');
    }
}

function getUserMediaSuccess(stream) {
    localStream = stream;
    localVideo.src = window.URL.createObjectURL(stream);
}

function start(isCaller) {
    peerConnection = new RTCPeerConnection(peerConnectionConfig);
    peerConnection.onicecandidate = gotIceCandidate;
    peerConnection.onaddstream = gotRemoteStream;
    peerConnection.addStream(localStream);

    if(isCaller) {
        peerConnection.createOffer(gotDescription, createOfferError);
    }
}

function gotMessageFromServer(message) {
    if(!peerConnection) start(false);

    var signal = JSON.parse(message.data);
    if(signal.sdp) {
        peerConnection.setRemoteDescription(new RTCSessionDescription(signal.sdp), function() {
            peerConnection.createAnswer(gotDescription, createAnswerError);
        });
    } else if(signal.ice) {
        peerConnection.addIceCandidate(new RTCIceCandidate(signal.ice));
    }
}

function gotIceCandidate(event) {
    if(event.candidate != null) {
        serverConnection.send(JSON.stringify({'ice': event.candidate}));
    }
}

function gotDescription(description) {
    console.log('got description');
    peerConnection.setLocalDescription(description, function () {
        serverConnection.send(JSON.stringify({'sdp': description}));
    }, function() {console.log('set description error')});
}

function gotRemoteStream(event) {
    console.log("got remote stream");
    remoteVideo.src = window.URL.createObjectURL(event.stream);
}

// Error functions....

function getUserMediaError(error) {
    console.log(error);
}

function createOfferError(error) {
    console.log(error);
}

function createAnswerError(error) {
    console.log(error);
}