Mobile
Article

Android Video Calling with CrossWalk and PeerJS

By Wern Ancheta

Creating a Video Calling App in Android with CrossWalk and PeerJS

In this tutorial I'm going to show how to create a video calling app in Android. It will be implemented using Ionic, Cordova, Crosswalk and PeerJS.

What We'll Build

Here are a few screenshots on what we're going to build in this tutorial. The default page is the login page.

Login Screen

And once the call has started, the app will display a live video feed of the other user.

App Screenshot

Set Up

I'm going to assume that you have already developed hybrid mobile apps using Cordova before. If you're new to Cordova, I suggest you read the Cordova Platform Guide first. Return here once you've setup the Android SDK and Cordova on your machine.

If you haven't used Ionic for projects before, install it with npm.

npm install -g ionic

Create a new Ionic app.

ionic start VideoKoler blank

Installing Front-end Assets

Install bower if you don't already have it on your machine. It will be used for installing front-end packages needed for this project.

npm install -g bower

Next, navigate to the VideoKoler directory and install Angular Localstorage and PeerJS.

bower install angular-local-storage peerjs --save

Angular Localstorage is used for saving data into the the browser or WebView local storage. PeerJS is a JavaScript library that makes implementation of WebRTC easier.

Installing Crosswalk

By default Ionic uses the WebView of the default Android browser. WebRTC isn't supported in that browser so we're using Crosswalk to give it superpowers. Crosswalk installs a more recent version of Chrome into your app so that you can use JavaScript APIs that are not commonly available in the default Android browser. This makes the WebView in which the app is rendered the same for all devices. This means you won't have to lose sleep worrying if a specific feature you've used in your app isn't implemented.

You can list available browsers using the following command.

ionic browser list

This should result in an output like the following.

ionic browser list

I always stick with the latest version that is not Canary. This is because Canary is the bleeding edge version of Chrome and it's not stable yet. You can then add the version by using ionic browser add.

ionic browser add crosswalk@12.41.296.5

Finally, add Android as a platform.

ionic platform add android

PeerServer

The app is going to use the PeerServer Cloud Service. At the time of writing, it's free for up to 50 concurrent connections.

peerserver cloud service

Click on Developer – Free, this will open a modal that allows you to sign up to the service.

peerserver sign up

Once your account is created, you will be redirected to the dashboard. Click the Create new API key button to generate a new key.

create new api key

You will then be asked for the 'Max concurrent peers' and 'Max concurrent peers per IP', the defaults are fine, you can update the values later if need be.

key settings

If you want to run the PeerServer on your own server, you can follow the following steps. Note that the limitations of the PeerServer Cloud Service doesn't apply when you run your own server.

First, create a new folder outside the www directory and name it videokoler-server. Navigate to that directory and install the PeerServer and Express.

npm install peer express --save

Create a server.js file and add the following.

var express = require('express');
var express_peer_server = require('peer').ExpressPeerServer;
var peer_options = {
    debug: true
};

var app = express();
var port = 3000;
var server = app.listen(port);

app.use('/peerjs', express_peer_server(server, peer_options));

The code above creates a new PeerServer from express server. You can run the server via node.

node server.js

To check if the peerserver is running properly, access the following URL in your browser, replacing localhost with the domain name.

http://localhost:3000/peerjs

The output should be something like:

{"name":"PeerJS Server","description":"A server side element to broker connections between PeerJS clients.","website":"http://peerjs.com/"}

If you don't have a server to test on, you can use ngrok to expose localhost to the internet. Download ngrok and execute it using the following command.

ngrok http 3000

Ngrok will assign a URL which we can use for the peer configuration later.

Building the App

You can find all the code used in this app on Github repo.

Navigate to the VideoKoler directory and open index.html. Add the following after the script tag that includes the cordova.js file. These are the paths to Angular Localstorage and PeerJS library installed earlier.

<script src="lib/angular-local-storage/dist/angular-local-storage.js"></script>
<script src="lib/peerjs/peer.min.js"></script>

Add the controllers we will need after the script tag that links to the app.js file. There are 2 controllers, one for handling login and one for calls.

<script src="js/controllers/IndexController.js"></script>
<script src="js/controllers/CallController.js"></script>

Inside the body tag, add the ion-nav-view directive. This allows the showing of different templates based on the current state. Replace the current content of the body tag with the below.

<body ng-app="starter">

    <ion-nav-view></ion-nav-view>

</body>

Create a templates folder inside of www and add an index.html file and videocall.html file.

The index.html file contains the template for the home page of the app. It has a text field for inputting a username and a login button. Take note of the name of the controller, the name of the model that's assigned to the text field, and the function called when the login button is clicked. These will be declared and used later in the controller.

<ion-header-bar class="bar-stable">
  <h1 class="title">VideoKoler</h1>
</ion-header-bar>
<ion-content class="padding has-header" ng-controller="IndexController">

  <div class="list">
    <label class="item item-input">
      <span class="input-label">Your Username</span>
      <input type="text" ng-model="username">
    </label>
  </div>

  <button class="button button-positive button-block" ng-click="login()">
    Login
  </button>
</ion-content>

videocall.html contains the template for calling another user. It has the video container at the top of the layout, which is empty by default. The video will be added once a call begins. It displays the username selected by the user and asks for the username of the user to call.

At the bottom of the layout is a button for initiating a call. Just like the previous template, take note of the values given to the ng-controller, ng-model and ng-click attributes.

<ion-header-bar class="bar-stable">
  <h1 class="title">VideoKoler</h1>
</ion-header-bar>
<ion-content class="padding has-header" ng-controller="CallController">

  <div class="card">
    <video id="contact-video" autoplay></video>
  </div>

  <div class="card">
    <div class="item item-text-wrap">
      Your Username: <strong>{{ username }}</strong>
    </div>
  </div>

  <div class="list">
    <label class="item item-input">
      <span class="input-label">Contact Username</span>
      <input type="text" ng-model="contact_username">
    </label>
  </div>

  <button class="button button-positive button-block" ng-click="startCall()">
    Call
  </button>
</ion-content>

Moving to the JavaScript side of the app. Open js/app.js and add Angular Localstorage as a dependency. ionic is added by default so add LocalStorageModule after it.

angular.module('starter', ['ionic', 'LocalStorageModule'])

Add the state configuration. For this app there are 2 states. login which is the default page and app which is the main app page where video calls take place.

A state is set using the state method provided by $stateProvider. This method accepts the name of the state as its first argument and an object containing the options as its second.

For the options, url and templateUrl are all that's needed. Where url is the url accessed for the state activated. templateUrl is the filesystem path of the template used by this state.

.config(function ($stateProvider, $urlRouterProvider) {
  $stateProvider
    .state('login', {
      url: '/login',
      templateUrl: 'templates/index.html'
    })

    .state('app', {
      url: '/app',
      templateUrl: 'templates/videocall.html'
    });

    $urlRouterProvider.otherwise('/login'); //set default page
});

Next create an IndexController.js file inside a js/controllers directory and add the following.

(function(){
    angular.module('starter')
    .controller('IndexController', ['localStorageService', '$scope', '$state', IndexController]);

    function IndexController(localStorageService, $scope, $state){

        $scope.login = function(){

            var username = $scope.username;
            localStorageService.set('username', username);
            $state.go('app');

        };

    }

})();

Breaking this code down. Everything is wrapped in an immediately invoked function expression.

(function(){

})()

Attach a controller to the starter module. This module was created earlier in the js/app.js file. The controller uses localStorageService which is made available by the Angular Localstorage module. Aside from that, there's $scope which is used in this controller to set and get variables and functions into the current scope. Finally there is $state which is used to navigate to a different state.

 angular.module('starter')
    .controller('IndexController', ['localStorageService', '$scope', '$state', IndexController]);

The controller does one thing. It's where the login function is attached to the $scope. Once the login button is clicked, this function is executed.

It gets the current value of the username variable in the $scope, saves it in localstorage and redirects to the main app page.

function IndexController(localStorageService, $scope, $state){

    $scope.login = function(){

        var username = $scope.username;
        if(username){
            localStorageService.set('username', username);
            $state.go('app');
        }

    };

}

While still inside the js/controllers directory, create a CallController.js file and add the following.

(function(){
    angular.module('starter')
    .controller('CallController', ['localStorageService', '$scope', '$ionicPopup', CallController]);

    function CallController(localStorageService, $scope, $ionicPopup){

            $scope.username = localStorageService.get('username');

            var peer = new Peer($scope.username, {
              key: 'your peerserver cloud key',
              config: {'iceServers': [
                { url: 'stun:stun1.l.google.com:19302' },
                { url: 'turn:numb.viagenie.ca', credential: 'muazkh', username: 'webrtc@live.com' }
              ]}
            });

            /* if you run your own peerserver
            var peer = new Peer($scope.username, {
              host: 'your-peerjs-server.com', port: 3000, path: '/peerjs',
              config: {'iceServers': [
                { url: 'stun:stun1.l.google.com:19302' },
                { url: 'turn:numb.viagenie.ca', credential: 'muazkh', username: 'webrtc@live.com' }
              ]}
            });
          */

            function getVideo(successCallback, errorCallback){
                navigator.webkitGetUserMedia({audio: true, video: true}, successCallback, errorCallback);
            }


            function onReceiveCall(call){

                $ionicPopup.alert({
                    title: 'Incoming Call',
                    template: 'Someone is calling you. Connecting now..'
                });

                getVideo(
                    function(MediaStream){
                        call.answer(MediaStream);
                    },
                    function(err){
                        $ionicPopup.alert({
                            title: 'Error',
                            template: 'An error occurred while try to connect to the device mic and camera'
                        });
                    }
                );

                call.on('stream', onReceiveStream);
            }


            function onReceiveStream(stream){
                var video = document.getElementById('contact-video');
                video.src = window.URL.createObjectURL(stream);
                video.onloadedmetadata = function(){
                    $ionicPopup.alert({
                        title: 'Call Ongoing',
                        template: 'Call has started. You can speak now'
                    });
                };

            }

            $scope.startCall = function(){
                var contact_username = $scope.contact_username;

                getVideo(
                    function(MediaStream){

                        var call = peer.call(contact_username, MediaStream);
                        call.on('stream', onReceiveStream);
                    },
                    function(err){
                        $ionicPopup.alert({
                            title: 'Error',
                            template: 'An error occurred while try to connect to the device mic and camera'
                        });
                    }
                );

            };

            peer.on('call', onReceiveCall);



    }

})();

Breaking the code down. We first retrieve the username from the localstorage.

$scope.username = localStorageService.get('username');

Create a new peer that uses the username as the peer ID. Then supply the peerserver cloud key as the value for the key option. (You can find your key on the PeerJS Dashboard.)

Next, add the ICE (Interactive Connectivity Establishment) server configuration using the config option. This is important since peerserver cloud only serves as a signaling server. This means it's only used for exchanging data between peers. An ICE server is needed to traverse NATs (Network) and firewalls. You can find a list of freely available ICE servers here.

var peer = new Peer($scope.username, {
    key: 'your peerserver cloud key',
    config: {'iceServers': [
        { url: 'stun:stun1.l.google.com:19302' },
        { url: 'turn:numb.viagenie.ca', credential: 'muazkh', username: 'webrtc@live.com' }
    ]}
});

If you're using your own server, you have to supply the host, port and path instead of the key. Then you can use the same values for the config.

var peer = new Peer($scope.username, {
        host: 'your-peerjs-server.com', port: 3000, path: '/peerjs',
        config: {'iceServers': [
            { url: 'stun:stun1.l.google.com:19302' },
            { url: 'turn:numb.viagenie.ca', credential: 'muazkh', username: 'webrtc@live.com' }
        ]}
    }
);

Create the function for requesting the mic and camera from the browser. This uses the Media Capture API. It can be used in Chrome using navigator.webkitGetUserMedia. This accepts an object containing the constraints as it's first argument. In the example below, both audio and video are set to true. This tells the browser to request access to both the mic and camera. The second and third arguments are the success and error callbacks. These are passed to the function once it's called.

function getVideo(successCallback, errorCallback){
    navigator.webkitGetUserMedia({audio: true, video: true}, successCallback, errorCallback);
}

Create the function to be executed when a call is received from another peer. An object containing the current call is passed as an argument. Inside the function, it informs the user that a call is received by using an ionic alert. Then it proceeds with calling the getVideo function. When the success callback is executed, it returns a MediaStream which can then be used to give an answer to the call. If the error callback is executed, it informs the user that there was an error. Finally, listen for the stream event on the call. When this event happens, the onReceiveStream function is called.

function onReceiveCall(call){

    $ionicPopup.alert({
        title: 'Incoming Call',
        template: 'Someone is calling you. Connecting now..'
    });

    getVideo(
        function(MediaStream){
            call.answer(MediaStream);
        },
        function(err){
            $ionicPopup.alert({
                title: 'Error',
                template: 'An error occurred while try to connect to the device mic and camera'
            });
        }
    );

    call.on('stream', onReceiveStream);
}

The onReceiveStream function is executed when the media stream is received from the other peer. The stream can then be used as a source to the video element. Once all the meta data has loaded, inform the user that the call has started.

function onReceiveStream(stream){
    var video = document.getElementById('contact-video');
    video.src = window.URL.createObjectURL(stream);
    video.onloadedmetadata = function(){
        $ionicPopup.alert({
            title: 'Call Ongoing',
            template: 'Call has started. You can speak now'
        });
    };

}

Declare the function for initiating the call. This takes the current value of the contact_username in the $scope. This is the username used by the other user when they logged in. Then the getVideo function is executed and it returns the media stream which is used to initiate the call to the other peer. This is done via the call method in the peer. This accepts the username of the other peer as the first argument and the media stream as the second. It then returns an object which you can use for listening for the stream event. Just like on the onReceiveCall function, the onReceiveStream is called once this event happens. This allows the initiator of the call to receive the stream from the other peer.

$scope.startCall = function(){
    var contact_username = $scope.contact_username;

    getVideo(
        function(MediaStream){

            var call = peer.call(contact_username, MediaStream);
            call.on('stream', onReceiveStream);
        },
        function(err){
            $ionicPopup.alert({
                title: 'Error',
                template: 'An error occurred while try to connect to the device mic and camera'
            });
        }
    );

};

Deployment

Now we're ready to deploy the app on an Android device. Navigate to the platforms/android directory and open the AndroidManifest.xml file. Add the following as children of the manifest tag if they don't already exist.

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />

Execute the following to build the app.

ionic build android

Once building is complete, you can get the apk file inside the platforms/android/build/outputs/apk directory. Since Crosswalk is used, separate apk's are generated for ARM and x86 processors. You already know that Crosswalk installs the web view of a recent version of Chrome. This means that the apk becomes larger in size than the usual (default Android WebView). This is the reason why a separate apk is generated for each architecture. Because if it's merged into a single apk file which works on both processors, it will be even larger. You can pick either of the following files depending on your device.
– android-armv7-debug.apk
– android-x86-debug.apk

Conclusion

That's it! In this tutorial, you have learned how to build a video calling app for Android using Ionic, Crosswalk and PeerJS. Please let me know if you have any questions, comments or problems in the comments below.

  • http://careersreport.com Margaret Lan

    I will show excellent internet job opportunity… three-five hrs of work /a day… Payment at the end of each week… Bonuses…Payment of 6-9 thousand dollars /a month… Merely several hrs of spare time, desktop or laptop, most basic knowing% of web and dependable internet connection is what is needed…Get more information by visiting my page

  • Hemant Jindal

    sir there is no call ending button in this app

    • Wern_Ancheta

      please see my comment above

  • Hemant Insan Insan

    sir there is no call ending button in this app

    • Wern_Ancheta

      You can end a call by using the stop method on a call:

      var call = peer.call(contact_username, MediaStream);

      call.stop();

      But you first have to create a new data connection. This will allow you to send a message to the other peer that will signal the app to end the call.
      You can check out the peerJS docs for that. http://peerjs.com/docs/

    • Wern_Ancheta

      You can end a call by using the stop method on a call:

      var call = peer.call(contact_username, MediaStream);

      call.stop();

      But you first have to create a new data connection. This will allow you to send a message to the other peer that will signal the app to end the call.
      You can check out the peerJS docs for that. http://peerjs.com/docs/

  • SocialChooozy

    Hey buddy thank you for tutorial, but this lib seems to be did not work without calling peer.connect function and you forgot to mention it

    • Wern_Ancheta

      peer.connect is only for data connections (e.g. sending files or messages between 2 peers). Its not required if you only intend to use peerJS for video calls.

  • Wern_Ancheta

    Hi Rudi, did you register on peerjs.com for a peerserver cloud key?

  • Nora Sunshine

    Hi, it works fine with the peerserver cloud key but when running on my own server I can’t get a connection. (The server is reachable on port 3000 over the internet) Any pointers?

    • Moinul Alam

      you must be on https on client side (newer browsers dont allow use of cam and stuff so peerjs just fails). You must also have https (SSL) set up on the node server or it wont work.

  • bill

    @Wern_Ancheta:disqus Thank you! this post helped me a lot!
    Can you kindly help me to solve some problems?

    – how can i switch camera on Ionic? i want to use back camera
    – i can’t receive calls when i call from browser, the connection doesn’t ope. if i call from the app is working.

    it’s not shown neither the ionicpopup

    peer.on(‘call’, function(call){
    $ionicPopup.alert({
    title: ‘Info’,
    template: ‘Call incoming.’
    });

    // call.answer(MediaStream); // commented to see if i see at least the popup
    });
    thank you

    • bill

      tried videocall smartphone-smartphone and does’nt work :(
      smartphone to browser works

  • Amit Patankar

    Hello,
    Thanks @Wern_Ancheta for the this excellent post.

    I have a few questions for which I need your help and guidance:
    1. How to switch camera – front / back
    2. How to test between browser and android (I am using local peerjs server) – What should be the browser URL ?
    3. How can I use this application (or what changes do I need to do) to test it on iOS

    Thanks !

    • http://www.agt.co.id/ Maulana yusuf

      Did you success? i have tried according to this tutorial. but ist not work.

  • Jagat Iyer

    Hi Wern. Am trying to authenticate users using Auth0. While I am able to authenticate the user, after Call, nothing happens. The other user is being accessed using ‘contact_username’. Pls assist.

  • victor garcia

    Hey, nice work. but it does not work between android device’s. How can i do that?

  • RD

    thanks for the tutorial! but i cannot see the video stream in my android device when i call from my pc browser to the app.Is it due to webkitgetusermedia() function ?

  • Mohit Upadhyaya

    Request was denied for security when try to connect through android app? Any help?

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Mobile, once a week, for free.