Remote VideoStream not working with WebRTC

Felix Hagspiel picture Felix Hagspiel · Jun 30, 2013 · Viewed 13.3k times · Source

EDIT: I wrote a detailed tutorial explaining how to build an simple Videochat-application including a signaling server:

Tutorial: Create your own Videochat-Application with HTML and JavaScript

Please tell me if you find it helpful & understandable. Thanks!


i am trying to get Streams to work via WebRTC and Websocket (nodejs-server). As far as i can see the handshake via SDP works and the Peerconnection is established. The problem is - the Remote-Video is not playing. The src-Attribute gets the Blob and autoplay is set, but it just won`t play. Maybe i am doing something wrong with the ICE-candidates (they are used for media-streaming, right?). Is there any way to check if the PeerConnection is set up correctly?

EDIT: Maybe i should explain how the code works

  1. At load of website a connection to the websocket-server is established, an PeerConnection using googles STUN-server is created and Video and Audio-Streams are collected & added to the PeerConnection

  2. When one user clicks on "create offer"-button a message containing its Session-Description (SDP) is send to the server (client func sendOffer()), which broadcasts it to the other user

  3. The other user gets the message and saves the SDP he received

  4. If the user clicks "accept offer", the SDP is added to the RemoteDescription (func createAnswer()) which then sends an answer-message (containing the SDP of the answering-user) to the offering-user

  5. At the offering-user`s side the func offerAccepted() is executed, which adds the SDP of the other user to his RemoteDesription.

I am not sure at what point exactly the icecandidate-handlers are called, but i think they should work because i get both logs on both sides.

Here`s my Code (this is just for testing, so even if there is a function called broadcast it means that only 2 users can be on the same website at a time):

Markup of index.html:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <style>
            #acceptOffer  {
                display: none;
            }
        </style>
    </head>
    <body>
        <h2>Chat</h2>
        <div>
            <textarea class="output" name="" id="" cols="30" rows="10"></textarea>
        </div>
        <button id="createOffer">create Offer</button>
        <button id="acceptOffer">accept Offer</button>

        <h2>My Stream</h2>
        <video id="myStream" autoplay src=""></video>
        <h2>Remote Stream</h2>
        <video id="remoteStream" autoplay src=""></video>

        <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
        <script src="websocketClient.js"></script>
</body>
</html>

Here is the Server-Code:

"use strict";

var webSocketsServerPort = 61122;

var webSocketServer = require('websocket').server,
http = require('http'),
clients = [];


var server = http.createServer(function(request, response) {
    // Not important for us. We're writing WebSocket server, not HTTP server
});
server.listen(webSocketsServerPort, function() {
    console.log((new Date()) + " Server is listening on port " + webSocketsServerPort);
});

var wsServer = new webSocketServer({
    httpServer: server
});

wsServer.on('request', function(request) {
    console.log((new Date()) + ' Connection from origin ' + request.origin + '.');

    var connection = request.accept(null, request.origin),
    index = clients.push(connection) - 1,
    userName=false;
    console.log((new Date()) + ' Connection accepted from '+connection.remoteAddress);

    // user sent some message
    connection.on('message', function(message) {
        var json = JSON.parse(message.utf8Data);

        console.log(json.type);
        switch (json.type) {
            case 'broadcast':
                broadcast(json);
            break;

            case 'emit':
                emit({type:'offer', data:json.data.data});
            break;

            case 'client':
                respondToClient(json, clients[index]);
            break;

            default:
                respondToClient({type:'error', data:'Sorry, i dont understand that.'}, clients[index]);
            break;

        }

    });

    connection.on('close', function(connection) {
        clients.splice(index,1);
        console.log((new Date()) + " Peer " + connection.remoteAddress + " disconnected.");
        broadcast({type:'text', data: userName+' has left the channel.'});
    });

    var respondToClient = function(data, client){
        client.sendUTF(JSON.stringify( data ));
    };

    var broadcast = function(data){
        for(var i = 0; i < clients.length; i++ ) {
            if(i != index ) {
                clients[i].sendUTF(JSON.stringify( data ));
            }
        }
    };
    var emit = function(){
        // TBD
    };
});

And here the Client-Code:

$(function () {
    "use strict";

    /**
    * Websocket Stuff
    **/

    window.WebSocket = window.WebSocket || window.MozWebSocket;

    // open connection
    var connection = new WebSocket('ws://url-to-node-server:61122'),
    myName = false,
    mySDP = false,
    otherSDP = false;

    connection.onopen = function () {
        console.log("connection to WebSocketServer successfull");
    };

    connection.onerror = function (error) {
        console.log("WebSocket connection error");
    };

    connection.onmessage = function (message) {
        try {
            var json = JSON.parse(message.data),
            output = document.getElementsByClassName('output')[0];

            switch(json.callback) {
                case 'offer':
                    otherSDP = json.data;
                    document.getElementById('acceptOffer').style.display = 'block';
                break;

                case 'setIceCandidate':
                console.log('ICE CANDITATE ADDED');
                    peerConnection.addIceCandidate(json.data);
                break;

                case 'text':
                    var text = output.value;
                    output.value = json.data+'\n'+output.value;
                break;

                case 'answer':
                    otherSDP = json.data;
                    offerAccepted();
                break;

            }

        } catch (e) {
            console.log('This doesn\'t look like a valid JSON or something else went wrong.');
            return;
        }
    };
    /**
    * P2P Stuff
    **/
    navigator.getMedia = ( navigator.getUserMedia ||
       navigator.webkitGetUserMedia ||
       navigator.mozGetUserMedia ||
       navigator.msGetUserMedia);

    // create Connection
    var peerConnection = new webkitRTCPeerConnection(
        { "iceServers": [{ "url": "stun:stun.l.google.com:19302" }] }
    );


    var remoteVideo = document.getElementById('remoteStream'),
        myVideo = document.getElementById('myStream'),

        // get local video-Stream and add to Peerconnection
        stream = navigator.webkitGetUserMedia({ audio: false, video: true }, function (stream) {
            myVideo.src = webkitURL.createObjectURL(stream);
            console.log(stream);
            peerConnection.addStream(stream);
    });

    // executes if other side adds stream
    peerConnection.onaddstream = function(e){
        console.log("stream added");
        if (!e)
        {
            return;
        }
        remoteVideo.setAttribute("src",URL.createObjectURL(e.stream));
        console.log(e.stream);
    };

    // executes if my icecandidate is received, then send it to other side
    peerConnection.onicecandidate  = function(candidate){
        console.log('ICE CANDITATE RECEIVED');
        var json = JSON.stringify( { type: 'broadcast', callback:'setIceCandidate', data:candidate});
        connection.send(json);
    };

    // send offer via Websocket
    var sendOffer = function(){
        peerConnection.createOffer(function (sessionDescription) {
            peerConnection.setLocalDescription(sessionDescription);
            // POST-Offer-SDP-For-Other-Peer(sessionDescription.sdp, sessionDescription.type);
            var json = JSON.stringify( { type: 'broadcast', callback:'offer',data:{sdp:sessionDescription.sdp,type:'offer'}});
            connection.send(json);

        }, null, { 'mandatory': { 'OfferToReceiveAudio': true, 'OfferToReceiveVideo': true } });
    };

    // executes if offer is received and has been accepted
    var createAnswer = function(){

        peerConnection.setRemoteDescription(new RTCSessionDescription(otherSDP));

        peerConnection.createAnswer(function (sessionDescription) {
            peerConnection.setLocalDescription(sessionDescription);
            // POST-answer-SDP-back-to-Offerer(sessionDescription.sdp, sessionDescription.type);
            var json = JSON.stringify( { type: 'broadcast', callback:'answer',data:{sdp:sessionDescription.sdp,type:'answer'}});
            connection.send(json);
        }, null, { 'mandatory': { 'OfferToReceiveAudio': true, 'OfferToReceiveVideo': true } });

    };

    // executes if other side accepted my offer
    var offerAccepted = function(){
        peerConnection.setRemoteDescription(new RTCSessionDescription(otherSDP));
        console.log('it should work now');
    };

    $('#acceptOffer').on('click',function(){
        createAnswer();
    });

    $('#createOffer').on('click',function(){
        sendOffer();
    });
});

I also read that the local-media-stream has to be collected before any offer is send. Does it mean i have to add it when the PeerConnection is created? I.e. something like this:

// create Connection
var peerConnection = new webkitRTCPeerConnection(
    { 
        "iceServers": [{ "url": "stun:stun.l.google.com:19302" }],
        "mediaStream": stream // attach media stream here?
    }
);

Thanks in advance, i appreciate any help!

EDIT2: i am a bit further now. it seems that adding the remote ice-candidates (switch-case setIceCandidate in client-code) is not working because of "An invalid or illegal string was specified. ". the json.data.candidate-object looks like this:

candidate: "a=candidate:1663431597 2 udp 1845501695 141.84.69.86 57538 typ srflx raddr 10.150.16.92 rport 57538 generation 0
↵"
sdpMLineIndex: 1
sdpMid: "video"

i tried creating an new candidate like this

 var remoteCandidate = new RTCIceCandidate(json.data.candidate);
 peerConnection.addIceCandidate(remoteCandidate);

but i still got an syntax error

Answer

HartleySan picture HartleySan · Jul 2, 2013

I was having trouble with essentially the same thing recently, and the best advice I got from someone else on here was to create a version of my program in which I manually copied and pasted the SDP and ICE info from one "peer" (i.e., browser tab) to another and vice versa.

By doing this, I realized several things:

  1. You must call the addStream method of the peer connection object before you attempt to create any offers/answers.

  2. Upon calling the createOffer or createAnswer method, the ICE candidates for that client are instantly generated. However, once you've sent the ICE info to the other peer, you cannot actually set the ICE info until after a remote description is set (by using the received offer/answer).

  3. Make sure that you are properly encoding all info about to be sent on the wire. In JS, this means that you should use the encodeURIComponent function on all data about to be sent on the wire. I had an issue in which SDP and ICE info would sometimes be set properly and sometimes not. It had to do with the fact that I was not URI-encoding the data, which led to any plus signs in the data being turned into spaces, which messed everything up.

Anyway, like I said, I recommend creating a version of your program in which you have a bunch of text areas for spitting out all the data to the screen, and then have other text areas that you can paste copied data into for setting it for the other peer.
Doing this really clarified the whole WebRTC process, which honestly, is not well explained in any documents/tutorials that I've seen yet.

Good luck, and let me know if I can help anymore.