In this part we are going to create a client application which connects two users using signalling server we created in the previous part.

Visual structure: struct_part4

There will be 2 pages in our app: one for login and another for calling a user.

To start, let’s create a basic HTML page index.html:

Login As

Login

Call
Hang Up

Create the client.js file:


//our user
var name;
//user connected to us
var connectedUser;

//setup a connection to websocket server
var connection = new WebSocket('ws://localhost:9090');

connection.onopen = function(){
    console.log("Connected");
};

//handling messages we got from the server
connection.onmessage = function(message){

    console.log("Got message", message.data);

    var data = JSON.parse(message.data);

    switch(data.type){
        //check for login success
        case "login":
            onLogin(data.success);
            break;
        //when a user wants to connect to us
        case "offer":
            onOffer(data.offer, data.name);
            break;
        //when we send offer to a user and he send back an answer to us
        case "answer":
            onAnswer(data.answer);
            break;
        //when remote user sends us an ice candidate
        case "candidate":
            onCandidate(data.candidate);
            break;
        //when remote user leaves us
        case "leave":
            onLeave();
            break;
        default:
            break;
    }

};

connection.onerror = function(err){
    console.log("Got error", err);
};

//alias for sending messages in json format
function send(message){
    if(connectedUser){
        message.name = connectedUser;
    }
    connection.send(JSON.stringify(message));
}

Let’s implement a simple login auth. We will simply send a username to the server which will tell if the username has been taken or not. Add the following code:


//Login implementation
var loginPage = document.querySelector('#login-page'),
    usernameInput = document.querySelector('#username'),
    loginButton = document.querySelector('#login'),
    callPage = document.querySelector('#call-page'),
    theirUsernameInput = document.querySelector('#their-username'),
    callButton = document.querySelector('#call'),
    hangUpButton = document.querySelector('#hang-up');

//initially hide call page in order to show login page first
callPage.style.display = 'none';

//Login when the user clicks the button "login"
loginButton.addEventListener('click', function(event){
    name = usernameInput.value;
    if(name.length > 0){
        send({
            type: "login",
            name:name
        });
    }
});

function onLogin(success){
    if(success === false){
        alert("Login unsuccessful, please try a different name.");
    } else {
        //if login was successful
        loginPage.style.display = "none";
        callPage.style.display = "block";
        //set up requirements for maling a webrtc connection
        startConnection();
    }
}

In the startConnection function we will: 1. Obtain a video stream from the camera 2. Check if the user’s device supports WebRTC 3. Create the RTCPeerConnection object

Add the following code:


//Starting a peer connection
var yourVideo = document.querySelector('#yours'),
    theirVideo = document.querySelector('#theirs'),
    yourConnection, stream;

function startConnection(){

    //if the user's device supports webrtc
    if(hasUserMedia()){
        navigator.getUserMedia({video: true, audio: true}, function(myStream){
            stream = myStream;
            //add local stream in the top right corner
            yourVideo.src = window.URL.createObjectURL(stream);

            if(hasRTCPeerConnection()){
                setupPeerConnection(stream);
            } else {
                alert("Sorry, your browser does not support WebRTC");
            }

        }, function(error){
            console.log(error);
        })
    } else {
        alert("Sorry, your browser does not support WebRTC");
    }

}

function setupPeerConnection(stream){
    var configuration = {
        "iceServers":[{"url":"stun:stun.1.google.com:19302"}]
    };
    yourConnection  = new RTCPeerConnection(configuration);

    //Setup stream listening
    yourConnection.addStream(stream);
    yourConnection.onaddstream = function(e){
        theirVideo.src = window.URL.createObjectURL(e.stream);
    };

    //setup ice handling
    //sending ice candidate to another user
    yourConnection.onicecandidate = function(event){
        console.log("onicecandidate");
        if(event.candidate){
            send({
                type: "candidate",
                candidate: event.candidate
            });
        }
    };

}

//check if the user's device supports webrtc
function hasUserMedia(){
    navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia
        || navigator.mozGetUserMedia || navigator.msGetUserMedia;
    return !!navigator.getUserMedia;
}

//check if the user's device supports rtcpeerconnection object
function hasRTCPeerConnection(){
    window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection
        || window.mozRTCPeerConnection;
    window.RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription
        || window.mozRTCSessionDescription;
    window.RTCIceCandidate = window.RTCIceCandidate || window.webkitRTCIceCandidate
        || window.mozRTCIceCandidate;
    return !!window.RTCPeerConnection;
}

Now if you run the server and open a client app you will see your local stream in the top right corner of the screen.

Now we are ready to implement a calling function. The scheme is: 1. UserA send the offer to UserB 2. Once UserB receives the offer he sends the answer back 3. They both start trading ICE candidates

Add the following code:


//Initiating a call to the other user
callButton.addEventListener("click", function(){
    var theirUsername = theirUsernameInput.value;
    if(theirUsername.length > 0){
        startPeerConnection(theirUsername);
    }
});

//sending offer and starting ice candidates trading
//remember this process is asynchronous
function startPeerConnection(user){

    connectedUser = user;

    //begin the offer
    yourConnection.createOffer(function(offer){
        send({
            type: "offer",
            offer: offer
        });
        yourConnection.setLocalDescription(offer);
    }), function(error){
        alert("An error has occured.");
    };

}

//when we receive an offer from another user
function onOffer(offer, name){
    connectedUser = name;
    yourConnection.setRemoteDescription(new RTCSessionDescription(offer));

    yourConnection.createAnswer(function(answer){
        yourConnection.setLocalDescription(answer);
        send({
            type: "answer",
            answer: answer
        });
    }, function(error){
        alert("An error has occured");
    });

}

//when we got an answer to our offer
function onAnswer(answer){
    yourConnection.setRemoteDescription(new RTCSessionDescription(answer));
}

//when we got ice candidate from a user we save it
function onCandidate(candidate){
    console.log("oncandidate");
    yourConnection.addIceCandidate(new RTCIceCandidate(candidate));
}

Now if you open the client app in two tabs in your browser, login 2 users and try to call to another user you may see local and remote streams on the web page. If something goes wrong you may navigate(in Chrome) to View->Developer->Developer Tools, open the Network tab and inspect the traffic for errors.

The last feature is hanging up an in-progress call. Add the following code:


//Hanging up a call
//send the other user a leave message and destroy a connection
hangUpButton.addEventListener("click", function(){
    send({
        type: "leave"
    });
    onLeave();
});

function onLeave(){
    connectedUser = null;
    theirVideo.src = null;
    //close rtcpeerconnection and stop transmitting our stream to the other user
    yourConnection.close();
    yourConnection.onicecandidate = null;
    yourConnection.onaddstream = null;
    //setup rtcpeerconnection again to accept new calls
    setupPeerConnection(stream);
}

This is out entire client application, client.js file:


//our user
var name;
//user connected to us
var connectedUser;

//setup a connection to websocket server
var connection = new WebSocket('ws://localhost:9090');

connection.onopen = function(){
    console.log("Connected");
};

//handling messages we got from the server
connection.onmessage = function(message){

    console.log("Got message", message.data);

    var data = JSON.parse(message.data);

    switch(data.type){
        //check for login success
        case "login":
            onLogin(data.success);
            break;
        //when a user wants to connect to us
        case "offer":
            onOffer(data.offer, data.name);
            break;
        //when we send offer to a user and he send back an answer to us
        case "answer":
            onAnswer(data.answer);
            break;
        //when remote user sends us an ice candidate
        case "candidate":
            onCandidate(data.candidate);
            break;
        //when remote user leaves us
        case "leave":
            onLeave();
            break;
        default:
            break;
    }

};

connection.onerror = function(err){
    console.log("Got error", err);
};

//alias for sending messages in json format
function send(message){
    if(connectedUser){
        message.name = connectedUser;
    }
    connection.send(JSON.stringify(message));
}

//Login implementation
var loginPage = document.querySelector('#login-page'),
    usernameInput = document.querySelector('#username'),
    loginButton = document.querySelector('#login'),
    callPage = document.querySelector('#call-page'),
    theirUsernameInput = document.querySelector('#their-username'),
    callButton = document.querySelector('#call'),
    hangUpButton = document.querySelector('#hang-up');

//initially hide call page in order to show login page first
callPage.style.display = 'none';

//Login when the user clicks the button "login"
loginButton.addEventListener('click', function(event){
    name = usernameInput.value;
    if(name.length > 0){
        send({
            type: "login",
            name:name
        });
    }
});

function onLogin(success){
    if(success === false){
        alert("Login unsuccessful, please try a different name.");
    } else {
        //if login was successful
        loginPage.style.display = "none";
        callPage.style.display = "block";
        //set up requirements for maling a webrtc connection
        startConnection();
    }
}

//Starting a peer connection
var yourVideo = document.querySelector('#yours'),
    theirVideo = document.querySelector('#theirs'),
    yourConnection, stream;

function startConnection(){

    //if the user's device supports webrtc
    if(hasUserMedia()){
        navigator.getUserMedia({video: true, audio: true}, function(myStream){
            stream = myStream;
            //add local stream in the top right corner
            yourVideo.src = window.URL.createObjectURL(stream);

            if(hasRTCPeerConnection()){
                setupPeerConnection(stream);
            } else {
                alert("Sorry, your browser does not support WebRTC");
            }

        }, function(error){
            console.log(error);
        })
    } else {
        alert("Sorry, your browser does not support WebRTC");
    }

}

function setupPeerConnection(stream){
    var configuration = {
        "iceServers":[{"url":"stun:stun.1.google.com:19302"}]
    };
    yourConnection  = new RTCPeerConnection(configuration);

    //Setup stream listening
    yourConnection.addStream(stream);
    yourConnection.onaddstream = function(e){
        theirVideo.src = window.URL.createObjectURL(e.stream);
    };

    //setup ice handling
    //sending ice candidate to another user
    yourConnection.onicecandidate = function(event){
        console.log("onicecandidate");
        if(event.candidate){
            send({
                type: "candidate",
                candidate: event.candidate
            });
        }
    };

}

//check if the user's device supports webrtc
function hasUserMedia(){
    navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia
        || navigator.mozGetUserMedia || navigator.msGetUserMedia;
    return !!navigator.getUserMedia;
}

//check if the user's device supports rtcpeerconnection object
function hasRTCPeerConnection(){
    window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection
        || window.mozRTCPeerConnection;
    window.RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription
        || window.mozRTCSessionDescription;
    window.RTCIceCandidate = window.RTCIceCandidate || window.webkitRTCIceCandidate
        || window.mozRTCIceCandidate;
    return !!window.RTCPeerConnection;
}

//Initiating a call to the other user
callButton.addEventListener("click", function(){
    var theirUsername = theirUsernameInput.value;
    if(theirUsername.length > 0){
        startPeerConnection(theirUsername);
    }
});

//sending offer and starting ice candidates trading
//remember this process is asynchronous
function startPeerConnection(user){

    connectedUser = user;

    //begin the offer
    yourConnection.createOffer(function(offer){
        send({
            type: "offer",
            offer: offer
        });
        yourConnection.setLocalDescription(offer);
    }), function(error){
        alert("An error has occured.");
    };

}

//when we receive an offer from another user
function onOffer(offer, name){
    connectedUser = name;
    yourConnection.setRemoteDescription(new RTCSessionDescription(offer));

    yourConnection.createAnswer(function(answer){
        yourConnection.setLocalDescription(answer);
        send({
            type: "answer",
            answer: answer
        });
    }, function(error){
        alert("An error has occured");
    });

}

//when we got an answer to our offer
function onAnswer(answer){
    yourConnection.setRemoteDescription(new RTCSessionDescription(answer));
}

//when we got ice candidate from a user we save it
function onCandidate(candidate){
    console.log("oncandidate");
    yourConnection.addIceCandidate(new RTCIceCandidate(candidate));
}

//Hanging up a call
//send the other user a leave message and destroy a connection
hangUpButton.addEventListener("click", function(){
    send({
        type: "leave"
    });
    onLeave();
});

function onLeave(){
    connectedUser = null;
    theirVideo.src = null;
    //close rtcpeerconnection and stop transmitting our stream to the other user
    yourConnection.close();
    yourConnection.onicecandidate = null;
    yourConnection.onaddstream = null;
    //setup rtcpeerconnection again to accept new calls
    setupPeerConnection(stream);
}

Of course there’s a lot to do to make this app better, like checking user input at each part or handling not having enough bandwidth to stream a video call or handling not being able to traverse firewalls or something else. This is just a basic WebRTC application.

That’s all for today :)