Вы находитесь на странице: 1из 14

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.

And once the call has started, the app will display a live video feed of the other user.
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.
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.
Click on Developer – Free, this will open a modal that allows you to sign up to the service.
Once your account is created, you will be redirected to the dashboard. Click the Create new API key
button to generate a new 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.

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.

Was this helpful?

Wern Ancheta
Wern is a web developer from the Philippines. He loves building things for the web and
sharing the things he has learned by writing in his blog. When he's not coding or learning
something new, he enjoys watching anime and playing video games.

Вам также может понравиться