Simple

Creating a mobile app from a simple HTML site: Part 4

How to polish your app and prepare it for market

In previous sections of this step-by-step series (Part 1, Part 2, and Part 3) we’ve created an app that loads multiple school plans from the server.

What we have so far is functional, but still has a number of issues, including two which are major: no offline mode and a hard-coded configuration. In this closing post, we’ll work on all of these issues.

If you haven’t built up the examples from the previous parts, use stage7 app and stage7 server as a starting point. To get started, follow the instructions on how to load any stage in this tutorial.

Refactoring

First, let’s do some refactoring. We’ll add a User object type to make it easier to choose which user’s plan to display. This will allow us to remove some more code from the app’s main logic flow, making the code cleaner and more modular. This object will load data from the server to create the plan UI. At the moment, rendering is done in app.onDeviceReady and app.renderData methods. Our new onDeviceReady method creates an app.user object and runs the methods needed to get and render plans. Replace the existing onDeviceReady method with the following:

onDeviceReady: function() {
    app.user = new User('johndoe');
    app.user.getPlans();

    app.activateFingerSwipe();
},

Next, delete the app.renderData method completely. We will store this functionality inside our new User object instead, as you’ll see below.

Next, at the beginning of the index.js file (before the definition of Plan), add the following code to define the User object:

function User(userid) {
    this.id = userid;
    this.plans = [];
}

User.prototype.getPlans = function() {
    var self = this;
    var request = new XMLHttpRequest({
        mozAnon: true,
        mozSystem: true});
    request.onload = function() {
        var plans = JSON.parse(this.responseText);
        for (var i = 0; i < plans.length; i++) {
            self.plans.push(new Plan(plans[i]));
        }
        self.displayPlans();
    };
    request.open("get", app.getPlansURL + this.id, true);
    request.send();
};

User.prototype.displayPlans = function() {
    var self = this;
    navigator.globalization.getDateNames(function(dayOfWeek){
        var deck = document.getElementById('plan-group');
        var tabbar = document.getElementById('plan-group-menu');
        for (var i = 0; i < self.plans.length; i++) {
            self.plans[i].createUI(deck, tabbar, dayOfWeek);
        }
    }, function() {}, {type: 'narrow', item: 'days'});
};

This contains functionality previously found in renderData and onDeviceReady. After the plan’s JSON is loaded from the server we iterate over the list and create a list of Plan objects in user.plans. We then call user.displayPlans, which creates the required DOM environment and invokes plan.createUI for each element of user.plans.

You may notice that the above code uses app.getPlansURL to allow the XHR call to find the JSON. Previously the URL was hardcoded into request.open("get", "http://127.0.0.1:8080/plans/johndoe", true);. Since it’s better to keep settings in one place we’ll add this as a parameter of the app object instead.

Add the following into your code:

var app = {
    getPlansURL: "http://127.0.0.1:8080/plans/",
    // ....
}

Now try preparing your app again with the cordova prepare terminal command, starting your server, and reloading the app in WebIDE (as we’ve done in previous articles). The app should work as before.

Offline mode

Let’s turn our attention to making the app work offline.

There are several technologies available to store data on the device. I’ve decided to use the localStorage API, as the data we need to store is basically just simple strings and doesn’t need any complex structuring. As this is a key/value store, objects need to be stringified (represented as a string). I will use only two functions — setItem() and getItem(). For example, storing a user id would require you to call localStorage.setItem('user_id', user.id);.

There are two values we definitely need to store in the phone memory — the ids of the current user and the plans. To make the app fully open and allow users to use servers not provided by the app creators we also need to store the server address. We’ll start by storing the plans. The user id and server will be still hardcoded. Let’s add a new method — User.loadPlans — and call that from app.onDeviceReady, instead of calling both user.getPlans() and app.user.displayPlans. We will decide if the plans need to be loaded from the server and then display the plans by calling User.displayPlans() from User.loadPlans.

First update onDeviceReady again, to this:

onDeviceReady: function() {
    app.user = new User('johndoe');
    app.user.loadPlans();
    app.activateFingerSwipe();
},

Now add in the following definition for the loadPlans method, up near your other User object-defining code:

User.prototype.loadPlans = function() {
    var plans = localStorage.getItem('plans');
    if (plans === null) {
        return this.getPlans();
    }
    console.log('DEBUG: plans loaded from device');
    this.initiatePlans(plans);
    this.displayPlans();
};

The above method calls another new method — initiatePlans. This parses the JSON representation of the plans and initiates Plan objects inside the User.plans array. In the future we would like to be able to reload the plans from the server, but the existing code always adds new plans instead of replacing them.

Add the following definition just below where you added the loadPlans code:

User.prototype.initiatePlans = function(plansString) {
    var plans = JSON.parse(plansString);
    for (var i = 0; i < plans.length; i++) {
        this.plans.push(new Plan(plans[i]));
    }
}

We also need to store the plans on the client. The best point to do this is right after the plans have been loaded from the server. We will call localStorage.setItem('plans', this.responseText) inside the success response function of getPlans. We must also remember to call the initiatePlans method.

Update your getPlans definition to the following:

User.prototype.getPlans = function() {
    var self = this;
    var request = new XMLHttpRequest({
        mozAnon: true,
        mozSystem: true});
    request.onload = function() {
        console.log('DEBUG: plans loaded from server');
        self.initiatePlans(this.responseText);
        localStorage.setItem('plans', this.responseText);
        self.displayPlans();
    };
    request.onerror = function(error) {
        console.log('DEBUG: Failed to get plans from ``' + app.getPlansURL + '``', error);
    };
    request.open("get", app.getPlansURL + this.id, true);
    request.send();
};

At this moment, successful loading of the user’s plans happens only once. Since school plans change occasionally (usually twice a year) you should be able to reload the plans from the server any time you want. We can use User.getPlans to return updated plans, but first we need to remove existing plans from the UI, otherwise multiple copies will be displayed.

Let’s create User.reloadPlans — add the code below your existing User object-defining code:

User.prototype.reloadPlans = function() {
    // remove UI from plans
    for (var i = 0; i < this.plans.length; i++) {
        this.plans[i].removeUI();
    }
    // clean this.plans
    this.plans = [];
    // clean device storage
    localStorage.setItem('plans', '[]');
    // load plans from server
    this.getPlans();
};

There is a new method to add to the Plan object too — Plan.removeUI — which simply calls Element.remove() on a plan’s card and tab. Add this below your previous Plan object-defining code:

Plan.prototype.removeUI = function() {
    this.card.remove();
    this.tab.remove();
};

This is a good time to test if the code has really reloaded. Run cordova prepare again on your code, and reload it in WebIDE.

We’ve got two console.log statements inside loadPlans and getPlans to let us know if the plans are loaded from device’s storage or the server. To initiate a reload run this code in Console:

app.user.reloadPlans()

Note: Brick reports an error at this point, as the link between each tab and card has been lost inside the internal system. Please ignore this error. Once again, our app still works.

reloadPlans from Console

Let’s make the reload functional by adding a UI element to reload the plans. In index.html, wrap a div#topbar around the brick-tabbar. First, add a reload button:

<div id="reload-button"></div>
<div id="topbar">
    <brick-tabbar id="plan-group-menu">
    </brick-tabbar>
</div>

Now we’ll make the button float to the left and use calc to set the size of the div#topbar. This makes Brick’s tabbar calculate the size when layout is changed (portrait or horizontal). In css/index.css, add the following at the bottom of the file:

#topbar {
	width: calc(100% - 45px);
}

#reload-button {
	float: left;
	/* height of the tabbar */
	width: 45px;
	height: 45px;
	background-image: url('../img/reload.svg');
	background-size: 25px 25px;
	background-repeat: no-repeat;
	background-position: center center;
}

We will download some Gaia icons to use within the application. At this stage, you should save the reload.svg file inside your img folder.

Last detail: Listen to the touchstart event on the #reload-button and call app.user.reloadPlans. Let’s add an app.assignButtons method:

assignButtons: function(){
    var reloadButton = document.getElementById('reload-button');
    reloadButton.addEventListener('touchstart', function() {
        app.user.reloadPlans();
    }, false);
},

We need to call it from app.deviceReady before or after app.activateFingerSwipe().

onDeviceReady: function() {
    // ...
    app.activateFingerSwipe();
    app.assignButtons();
},

At this point, save your code, prepare your cordova app, and reload it in WebIDE. You should see something like this:

reload button

Here you can find current code for app and server.

Settings page

Currently all of the identity information (the address of the server and user id) is hardcoded in the app. If the app is to be used with others, we provide functionality for users to set this data without editing the code. We will implement a settings page, using Brick’s flipbox component to change from content to settings and vice versa.

First we must tell the app to load the component. Let’s change index.html to load the flipbox widget by adding the following line into the HTML <head> section:

<link rel="import" href="app/bower_components/brick-flipbox/dist/brick-flipbox.html">

Some changes must happen inside the HTML <body> as well. brick-topbar and brick-deck need to be placed inside switchable sections of the same flipbox, and the reload button also moves to inside the settings. Replace everything you’ve got in your <body> so far with the following (although you need to leave the <script> elements in place at the bottom):

<brick-flipbox>
    <section id="plans">
        <div id="settings-button"></div>
        <div id="topbar">
            <brick-tabbar id="plan-group-menu">
            </brick-tabbar>
        </div>
        <brick-deck id="plan-group">
        </brick-deck>
    </section>
    <section id="settings">
        <div id="settings-off-button"></div>
        <h2>Settings</h2>
    </section>
</brick-flipbox>

The idea is to switch to the “settings” view on pressing a settings-button, and then back to the “plans” view with the settings-off-button.

Here’s how to wire up this new functionality. First up, inside your app definition code add a toggleSettings() function, just before the definition of assignButtons():

toggleSettings: function() {
    app.flipbox.toggle();
},

Update assignButtons() itself to the following (we are leaving the reloadButton code here, as we’ll need it again soon enough):

assignButtons: function() {
    app.flipbox = document.querySelector('brick-flipbox');
    var settingsButton = document.getElementById('settings-button');
    var settingsOffButton = document.getElementById('settings-off-button');
    settingsButton.addEventListener('click', app.toggleSettings);
    settingsOffButton.addEventListener('click', app.toggleSettings);
        
    var reloadButton = document.getElementById('reload-button');
    reloadButton.addEventListener('touchstart', function() {
        app.user.reloadPlans();
    }, false);
},

Next: we need to retrieve some more icons for the app. Save the settings.svg and back.svg files inside your img folder.

Let’s update our css/index.css file too, to add the additional icons in and style our new features. Add the following at the bottom of that file:

#form-settings {
    font-size: 1.4rem;
    padding: 1rem;
}
#settings-button, #settings-off-button {
    float: left;
    width: 45px;
    height: 45px;
    background-size: 25px 25px;
    background-repeat: no-repeat;
    background-position: center center;
}
#settings-button {
    background-image: url('../img/settings.svg');
}
#settings-off-button {
    background-image: url('../img/back.svg');
    border-right: 1px solid #ccc;
    margin-right: 15px;
}
brick-flipbox {
    width: 100%;
    height: 100%;
}

If you now prepare your cordova app again with cordova prepare, and reload the app inside WebIDE, you should see something like this:

flipbox working

Adding a settings form

To add a form and some data, replace your current <section id="settings"></section> element with the following:

<section id="settings">
    <div id="settings-off-button"></div>
    <h2>Settings</h2>
    <form id="form-settings">
        <p><label for="input-server">Server</label></p>
        <p><input type="text" id="input-server" placeholder="http://example.com/"/></p>
        <p><label for="input-user" id="label-user">User ID</label></p>
        <p><input type="text" id="input-user" placeholder="johndoe"/></p>
        <p><button id="reload-button">RELOAD ALL</button></p>
    </form>
</section>

Making it look good is easy! We’ll use some ready-made CSS available in Firefox OS Building Blocks. Download input_areas.css and buttons.css and save them in your www/css directory.

Then add the following to your HTML <head>, to apply the new CSS to your markup.

<link rel="stylesheet" type="text/css" href="css/input_areas.css">
<link rel="stylesheet" type="text/css" href="css/buttons.css">

Go into the css/index.css file and delete the #reload-button { ... } rule to make the “RELOAD ALL” button display correctly.

Run cordova prepare again and reload the app in WebIDE, and you should see something like this:

settings

We’ve got the view — now to support it. When the app first installs there will be no setting, so the app will have no idea how to load the data. Instead of showing empty plans we immediately toggle the view to the settings. Update your onDeviceReady function as shown:

onDeviceReady: function() {
    app.plansServer = localStorage.getItem('plansServer');
    app.userID = localStorage.getItem('userID');
    app.activateFingerSwipe();
    app.assignButtons();

    if (app.plansServer && app.userID) {
        app.user = new User(app.userID);
        app.user.loadPlans();
    } else {
        app.toggleSettings();
    }
},

The app needs to read the data from our form inputs, so save them into the phone’s storage, and reload after the change. Let’s add this functionality at the end of the app.assignButtons function. First we will stop the form from submitting (otherwise our app would just reload the index.html instead of redrawing the content). Add the following code to the end of the assignButtons function, just before the closing },:

document.getElementById('form-settings').addEventListener('submit', function(e) {
    e.preventDefault();
}, false)

We will read and set the input value for the server. I’ve decided to read the input value on the blur event. It is dispatched when the user touches any other DOM Element on the page. When this happens, the server value is stored in the device’s storage. If app.plansServer exists on app launch, we set the default input value. The same has to happen for the userID, but there is a special step. Either we change the app.user or create a new one if none exists. Add the following just above the previous block of code you added:

var serverInput = document.getElementById('input-server');
serverInput.addEventListener('blur', function() {
    app.plansServer = serverInput.value || null;
    localStorage.setItem('plansServer', app.plansServer);
});
if (app.plansServer) {
    serverInput.value = app.plansServer;    
}

var userInput = document.getElementById('input-user');
userInput.addEventListener('blur', function() {
    app.userID = userInput.value || null;
    if (app.userID) {
        if (app.user) {
            app.user.id = app.userID;
        } else {
            app.user = new User('app.userID');
        }
        localStorage.setItem('userID', app.userID);
    }
});
if (app.userID) {
    userInput.value = app.userID;    
}

After preparing your app and reloading it in WebIDE, you should now see the settings working as expected. The settings screen appears automatically if no plans are found in the localstorage, allowing you to enter your server and userID details.

Test it with the following settings:

  • Server: http://127.0.0.1:8080
  • User ID: johndoe

The only inconvenience is that you need to switch back to the plans view manually once Settings are loaded. In addition, you might get an error if you don’t blur out of both form fields.

settings are working

Improving the settings UX

In fact, switching back to the plans view should be done automatically after plans are successfully loaded. We can implement this by adding a callback to the User.getPlans function, and including it in the call made from User.reloadPlans:

First, update your getPlans() function definition to the following:

User.prototype.getPlans = function(callback) {
    var self = this;
    var url = app.plansServer + '/plans/' + this.id;
    var request = new XMLHttpRequest({
        mozAnon: true,
        mozSystem: true});
    request.onload = function() {
        console.log('DEBUG: plans loaded from server');
        self.initiatePlans(this.responseText);
        localStorage.setItem('plans', this.responseText);
        self.displayPlans();
        if (callback) {
            callback();
        }
    };
    request.open("get", url, true);
    request.send();
};

Next, in User.reloadPlans add the callback function as an argument of the this.getPlans call:

this.getPlans(app.toggleSettings);

Again, try saving, preparing, and reloading your new code. You should now see that the app displays the plans view automatically when correct server and user information is entered and available. (It switches to the Settings view only if there is no error in self.initiatePlans(this.responseText).).

final

Where do we go from here

Congratulations! You’ve reached the end of this 4-part tutorial, and you should now have your own working prototype of a fun school plan application.

There is still plenty to do to improve the app. Here are some ideas to explore:

  • Writing JSON files is not a comfortable task. It would be better to add new functionality that lets you edit plans on the server. Assigning them to the user on the server opens new possibilities, and this would just require an action to sync the user account on both server and client.
  • Displaying hours would also be nice.
  • It would be cool to implement a reminder alarm for some school activities.

Try implementing these yourself, or build out other improvements for your own school scheduling app. Let us know how it goes.

View full post on Mozilla Hacks – the Web developer blog

VN:F [1.9.22_1171]
Rating: 0.0/10 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)

100 super useful web sites allowing one simple task each

I just came across this post on imgur showing lots of cool little helper sites. Sadly there were no links in the description, so using some Sublime Text Magic, I converted them.

Just to find out later that not only do imgur posters either post screenshots of links or unlinked links, no, all of this was once again stolen content.

The original list is maintained by Amit Agarwal so please go on to:

The 101 Most Useful Websites

Excellent work, Amit, shame people don’t respect it.

To make this not a total waste of blogpost, at least here is how I used Sublime Text to turn a list of non-links into a list of links using a bit of regular expression knowledge (go fullscreen).

View full post on Christian Heilmann

VN:F [1.9.22_1171]
Rating: 0.0/10 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)

Two simple syndication tricks to get wrong: have an image and don’t trust iframes

Blogging is simple, but many people forget that there are freaks like me out there who have done this for quite a while who use RSS readers and that there are new folks out there who will share your links on Facebook, Google+ and other social networks.

Beastie Boys - Ill communication

Therefore it is pretty important to do two things in your posts:

  • Do not rely on iframes working
  • Do have an image for social media sites to show as a thumbnail

As to iframes, my good friend Jens Grochtdreis this morning posted about needing a new workflow in web development, proving his point by embedding two of his talks on speakerdeck (which are iframes). To me, his post looked like this in feedly:

Blog post with embeds not showing up - all I got is two headings promising content

Two headings, no content. I reported the problem on Twitter and now he linked the headings to the URLs on speaker deck. Everybody wins.

Most readers will filter out iframe content if it mixes http and https for security reasons. Something we’ll encounter more and more in the future when content security policy becomes a bigger topic (yes, the web is insecure, yes, it is our job to fix that).

The second tip about having an image is a nice-to-have but important. Having an image in your post – a real image element, not a background image – means that Facebook, Google+ and others will show this image next to your blog post title when someone shares the link on these systems. If you don’t pick a thumbnail, Facebook picks the first one. This means your link could show up next to an ad for another product and confuse potential readers. A great example I encountered the other day was Scott Hanselman’s Are you a Phony? post which, with a title like that, showed up next to an ad with an endorsement of his for his hosting service. Confusing message, much?

scott hanselman post shared on Facebook with a wrong thumbnail

Seems to me people click much more on links with contextually relevant images next to them. Thus, providing one makes it nicer for everyone.

Simple, but effective.

View full post on Christian Heilmann

VN:F [1.9.22_1171]
Rating: 0.0/10 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)

Building a simple paint game with HTML5 Canvas and Vanilla JavaScript

When the talk is about HTML5 Canvas you mostly hear about libraries to make it work for legacy browsers, performance tricks like off-screen Canvas and ways to draw and animate sprites and tiles. This is only one part of Canvas, though. On the lowest level, Canvas is a way to manipulate pixels of a portion of the screen. Either via a painting API or by directly manipulating the pixel array (which by the way is a typed array and thus performs admirably).

Using this knowledge, I thought it’d be fun to create a small game I saw in an ad for a tablet: a simple game for kids to paint letters. The result is a demo for FirefoxOS called Letterpaint which will show up soon on the Marketplace. The code is on GitHub.

letterpaint

The fun thing about building Letterpaint was that I took a lot of shortcuts. Painting on a canvas is easy (and gets much easier using Jacob Seidelin’s Canvas cheatsheet), but on the first glance making sure that users stay in a certain shape is tricky. So is finding out how much of the letter has been filled in. However, by going back to seeing a Canvas as a collection of pixels, I found a simple way to make this work:

  • When I paint the letter, I read out the amount of pixels that have the colour of the letter
  • When you click the mouse button or touch the screen I test the colour of the pixel at the current mouse/finger position
  • When the pixel is not transparent, you are inside the letter as the main Canvas by default is transparent
  • When you release the mouse or stop touching the screen I compare the amount of pixels of the paint colour with the ones of the letter.

Simple, isn’t it? And it is all possible with two re-usable functions:

/*
  getpixelcolour(x, y)
  returns the rgba value of the pixel at position x and y
*/
function getpixelcolour(x, y) {
  var pixels = cx.getImageData(0, 0, c.width, c.height);
  var index = ((y * (pixels.width * 4)) + (x * 4));
  return {
    r: pixels.data[index],
    g: pixels.data[index + 1],
    b: pixels.data[index + 2],
    a: pixels.data[index + 3]
  };
}
 
/*
  getpixelamount(r, g, b)
  returns the amount of pixels in the canvas of the colour 
  provided
*/
function getpixelamount(r, g, b) {
  var pixels = cx.getImageData(0, 0, c.width, c.height);
  var all = pixels.data.length;
  var amount = 0;
  for (i = 0; i < all; i += 4) {
    if (pixels.data[i] === r &&
        pixels.data[i + 1] === g &&
        pixels.data[i + 2] === b) {
      amount++;
    }
  }
  return amount;
}

Add some painting functions to that and you have the game done. You can see a step by step guide of this online (and pull the code from GitHub) and there is a screencast describing the tricks and decisions on YouTube.

The main thing to remember here is that it is very tempting to reach for libraries and tools to get things done quickly, but that it could mean that you think too complex. Browsers have very powerful tools built in for us, and in many cases it means you just need to be up-to-date and fearless in trying something “new” that comes out-of-the-box.

View full post on Mozilla Hacks – the Web developer blog

VN:F [1.9.22_1171]
Rating: 0.0/10 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)

Making WebRTC Simple with conversat.io

WebRTC is awesome, but it’s a bit unapproachable. Last week, my colleagues and I at &yet released a couple of tools we hope will help make it more tinkerable and pose a real risk of actually being useful.

As a demo of these tools, we very quickly built a simple product called conversat.io that lets you create free, multi-user video calls with no account and no plugins, just by going to a url in a modern browser. Anyone who visits that same URL joins the call.

conversat.io

The purpose of conversat.io is two fold. First, it’s a useful communication tool. Our team uses And Bang for tasks and group chat, so being able to drop a link to a video conversation “room” into our team chat that people can join is super useful. Second, it’s a demo of the SimpleWebRTC.js library and the little signaling server that runs it, signalmaster.

(Both SimpleWebRTC and signalmaster are open sourced on Github and MIT licensed. Help us make them better!)

Quick note on browser support

WebRTC currently only works in Chrome stable and FireFox Nightlies (with the media.peerconnection.enabled preference enabled in about:config).

Hopefully we’ll see much broader browser support soon. I’m particularly excited about having WebRTC available on smartphones and tablets.

Approachability and adoption

I firmly believe that widespread adoption of new web technologies is directly corellated to how easy they are to play with. When I was a new JS developer, it was jQuery’s approachability that made me feel empowered to build cool stuff.

My falling in love with javascript all started with doing this with jQuery:

$('#demo').slideDown();

And then seeing the element move on my screen. I knew nothing. But as cheesy as it sounds, this simple thing left me feeling empowered to build more interesting things.

Socket.io did the same thing for people wanting to build apps that pushed data from the server to the client:

// server:
client.emit("something", {
    some: "data" 
});
// client:
socket = io.connect();
socket.on("something", function (data) {
    // here's my data!
    console.log(data);
});

Rather than having to figure out how to set up long-polling, BOSH, and XMPP in order to get data pushed out to the browser, I could now just send messages to the browser. In fact, if I didn’t want to, I didn’t even have to think about serializing and de-serializing. I could now just pass simple javascript objects seamlessly back and forth between the client and server.

I’ve heard some “hardcore” devs complain that tools like this lead to too many poorly made tools and too many “wannabe” developers who don’t know what they’re doing. That’s garbage.

Approachable tools that make developers feel empowered to build cool stuff is the reason the web is as successful and vibrant as it is.

Tools like this are the gateway drug for getting us hooked on building things on these types of technologies. They introduce the concept and help us think about what could be built. Whether or not we ultimately end up building the final app with the tool whose simplicity introduced it to us is irrelevant.

The potential of WebRTC

I’m convinced WebRTC has the potential to have a huge impact on how we communicate. It already has for our team at &yet. Sure, we already used stuff like Skype, Facetime, and Google Hangouts. But the simplicity and convenience of just opening a URL in a browser and instantly being in a conversation is powerful.

Once this technology is broadly available and on mobile devices, it’s nothing short of a game changer for communications.

Challenges

There are definitely quite a few hurdles that get in the way of just playing with WebRTC: complexity and browser differences in instantiating peer connections, generating and processing signaling messages, and attaching media streams to video elements.

Even at the point you have those things, you still need a way to let two users find each other and have a mechanism for each user to send the proper signaling messages directly to the other user or users that they want to connect to.

SimpleWebRTC.js is our answer to the clientside complexities. It abstracts away API differences between Firefox and Chrome.

Using SimpleWebRTC

At its simplest, you just need to include the SimpleWebRTC.js script, provide a container for your local video, a container for the remote video(s) like this:

<!DOCTYPE html>
<html>
    <head>
        <script src="http://simplewebrtc.com/latest.js"></script> 
    </head>
    <body>
        <div id="localVideo"></div>
        <div id="remoteVideos"></div>
    </body>
</html>

Then in you just init a webrtc object and tell it which containers to use:

var webrtc = new WebRTC({
    // the id of (or actual element) to hold "our" video
    localVideoEl: 'localVideo',
 
    // the id of or actual element that will hold remote videos
    remoteVideosEl: 'remoteVideos',
 
     // immediately ask for camera access
    autoRequestMedia: true
});

At this point, if you run the code above, you’ll see your video turn on and render in the container you gave it.

The next step is to actually specify who you want to connect to.

For simplicity and maximum “tinkerability” we do this by asking that both users who want to connect to each other join the same “room”, which basically means: call “join” with the same string.

So, for demonstration purposes we’ll just tell our webrtc to join a certain room once it’s ready (meaning it’s connected to the signaling server). We do this like so:

// we have to wait until it's ready
webrtc.on('readyToCall', function () {
    // you can name it anything
    webrtc.joinRoom('your awesome room name');
});

Once a user has done this, he/she is ready and waiting for someone to join.

If you want to test this locally, you can either open it in Firefox and Chrome or in two tabs within Chrome. (Firefox doesn’t yet let two tabs both access local media).

At this point, you should automatically be connected and be having a lively (probably very echo-y!) conversation with yourself.

If you happen to be me, it’d look like this:

henrik in conversat.io

The signaling server

The example above will connect to a sandbox signaling server we keep running to make it easy to mess around with this stuff.

We aim to keep it available for people to use to play with SimpleWebRTC, but it’s definitely not meant for production use and we may kill it or restart it at any time.

If you want to actually build an app that depends on it, you can either run one yourself, or if you’d rather not mess with it, we can host, and keep up to date, and help scale one for you. The code for that server is on github.

You can just pass a URL to a different signaling server as part of your config by passing a “url” option when initiating your webrtc object.

So, what’s it actually doing under the hood?

It’s not too bad, really. You can read the full source of the client library here: https://github.com/HenrikJoreteg/SimpleWebRTC/blob/master/simplewebrtc.js and the signaling server here: https://github.com/andyet/signalmaster/blob/master/server.js

The process of starting a video call in conversat.io looks something like this:

  1. Establish connection to the signaling server. It does this with socket.io and connects to our sandbox signaling server at: http://signaling.simplewebrtc.com:8888

  2. Request access to local video camera by calling browser prefixed getUserMedia.

  3. Create or get local video element and attach the stream that we get from getUserMedia to the video element.

    firefox:

    element.mozSrcObject = stream; element.play();

    webkit:

    element.autoplay = true;
    element.src = webkitURL.createObjectURL(stream);
  4. Call joinRoom which sends a socket.io message to the signaling server telling it the name of the room name it wants to connect to. The signaling server will either create the room if it doesn’t exist or join it if it does. All I mean by “room” is that the particular socket.io session ID is grouped by that room name so we can broadcast messages about people joining/leaving that room to only the clients connected to that room.

  5. Now we play an awesome rocket lander game that @fritzy wrote while we wait for someone to join us:

  6. When someone else joins the same “room” we broadcast that to the other connected users and we create a Conversation object that we’ve defined which wraps the browser’s peerConnection. The peer connection represents, as you’d probably guess, the connection between you and another person.

  7. The signaling server broadcasts the new socket.io session ID to each user in the room and each user’s client creates a Conversation object for every other user in the room.

  8. At this point we have a mechanism of knowing who to connect to and how to send direct messages to each of their sessions.

  9. Now we use the peerConnection to create an “offer” and store our local offer and set it in our peer connection as the local description. This contains information about how another client can reach and talk to our browser.

    peerConnection.createOffer();

    We then send this over our socket.io connection to the other people in the room.

  10. When a client receives and offer we add it to our peer connection:

    var remote = new RTCSessionDescription(message.payload);
    peerConnection.setRemoteDescriptionremoteDescription);

    and generate an answer by calling peerConnection.createAnswer() and send that back to the person we got the offer from.

  11. When the answer is received we set it as the remote description. Then we create and send ICE Candidates much in the same way. This will negotiate our connection and connect us.

  12. If that process is successful we’ll get an onaddstream event from our peer connection and we can then create a video element and attach that stream to it. At this point the video call should be in progress.

If you wish to dig into it further, send pull requests and file issues on the SimpleWebRTC project on github.

The road ahead

This is just a start. Help us make this stuff better!

There’s a lot more we’d like to see with this:

  1. Making the signaling piece more pluggable (so you can use whatever you want).
  2. Adding support for pausing and resuming video/audio.
  3. It’d be great to be able to figure out who’s talking and emit an event to other connected users when that changes.
  4. Better control over handling/rejecting incoming requests.
  5. Setting max connections, perhaps determined based on HTML5 connection APIs?

Hit me up on twitter (@henrikjoreteg) if you do something cool with this stuff or run into issues or just want to talk about it. I’d love to hear from you.

Keep building awesome stuff, you amazing web people! Go go gadget Internet!

View full post on Mozilla Hacks – the Web developer blog

VN:F [1.9.22_1171]
Rating: 0.0/10 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)