15

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

Felix Hagspiel
  • 2,634
  • 2
  • 30
  • 43
  • Maybe this helps: http://stackoverflow.com/questions/17346616/peerconnection-addicecandidate-giving-error-invalid-string/17424224#17424224 – Mert Mertce Jul 04 '13 at 12:15

2 Answers2

24

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.

HartleySan
  • 7,404
  • 14
  • 66
  • 119
  • Hey @HartleySan, thanks alot for taking time! I did as you said and copy&pasted the SDP-Data. It seems my Ice-candidates are the problem. Also, i do not really understand how the procedure should be. do they need to exchange candidates continously,so should every "onicecandidate"-event send the candidate over to the other user? Or do i dont`t do anything at all on "onicecandidate"? – Felix Hagspiel Jul 02 '13 at 19:19
  • right now i do: 1. create an offer which automatically leads to approx. 20 calls of "onicecandidate". i guess these candidates are the ones of the offering user then,right? 2. send the SDP to the other user 3. The other user then takes the SDP, 4. adds it to its remoteConnection and THEN i should 5. do an "addIceCandidate(offeringUsersCandidate)", also before i do 6. setLocalDescription() of the answering user is called, as i read out of your text, or am i getting that wrong? the problem then is that i do not have the candidate of the offering user at this point. i am really confused :) – Felix Hagspiel Jul 02 '13 at 19:23
  • Yeah, it is confusing, I know. And like I said, there's no good documentation out there to explain it. I played around with it, and here's what I found: 1) ICE candidates from the other peer can be set any time after the remote description (i.e., the description from the other peer) is set. That means that you can set the ICE candidates before or after you set the local description (for the answerer, of course). 2) I tend to continually forward and check for ICE candidates until the two videos pop up on both ends. At that point, additional ICE candidates can be generated, but I don't use them. – HartleySan Jul 02 '13 at 21:54
  • 8
    To sum up, here's what I would do: 1) Whenever onicecandidate occurs, immediately send that data to the other peer. However, when the other peer receives that info, it shouldn't set it until the remote description is set. Any ICE candidate info received before that should be thrown into an array or something, and when it's time, loop through the array, setting all the ICE candidate info. 2) For the answerer, as soon as you get the SDP info (offer) from the other peer, set it as the remote description, immediately set the local description and send it on, and then set any ICE candidate info. – HartleySan Jul 02 '13 at 21:57
  • THANKS!!!! It worked now! I will put up an demo and an HOWTO as soon as i am finished, and then i will post the link here. Thanks again for your patience & good answers! – Felix Hagspiel Jul 04 '13 at 15:17
  • Yeah, that's always a good feeling the first time that remote peer video pops up. Congrats. Best of luck with your project and write-up. – HartleySan Jul 04 '13 at 19:26
  • @HartleySan Thanks for making this clear. I read through a lot of documentation and didn't see this being mentioned. I got the video stream to work, finally! Thanks again. – Jay Jul 08 '13 at 13:10
  • Guys, I can't tell you how much I thank you for this conversation. 2 HOURS trying to figure out the same issue until I read this thread! Forever in debt. – Icarus Jan 31 '14 at 06:55
0
function sharescreen(){
getScreenStream(function(screenStream) {
localpearconnection.removeTrack(localStream); 
localpearconnection.addStream(screenStream);
localpearconnection.createOffer().then(description => createdLocalDescription(description)).catch(errorHandler);
document.getElementById('localVideo').srcObject = screenStream;});}

function getScreenStream(callback) {
if (navigator.getDisplayMedia) {
    navigator.getDisplayMedia({
        video: true
    }).then(screenStream => {
        callback(screenStream);
    });
} else if (navigator.mediaDevices.getDisplayMedia) {
    navigator.mediaDevices.getDisplayMedia({
        video: true
    }).then(screenStream => {
        callback(screenStream);
    });
} else {
    getScreenId(function(error, sourceId, screen_constraints) {
        navigator.mediaDevices.getUserMedia(screen_constraints).then(function(screenStream) {
            callback(screenStream);
        });
    });
}}

The above code working for me.