Livereload with Cordova and Gulp

I recently started on an Ionic/Cordova app that uses Gulp as the main build system. While I’ve used Grunt several times in the past I have found Gulp to be much more of an intuitive approach and definitely easier to work with.

In a Cordova project, you develop the application in the $ROOT_DIR/www/ directory. When you build the project for ios, Cordova copies those files from that directory to $ROOT_DIR/platforms/ios/www directory. In order to streamline the development process, I wanted to make use of
livereload via the gulp-livereload and cordova-gapreload plugins. The basic setup is to use Gulp to watch for changes in the www/ directory and when a change is detected, copy the file to the appropriate location in the platforms/ios/www directory before triggering livereload. The main reason for copying files is that the gapreload plugin looks for assets served relative to the platforms/ directory.

As you’d expect with any project, asset files can be numerous and the build system itself should not impose a restriction on nested folders. These requirements make manually listing files and structures an onerous task that should be avoided. Fortunately, file globbing) exists precisely to simplify this task. When specifying files to watch, you can use a glob pattern to match multiple files in many directories with a single expression. For example, to watch all files in the www/ directory, you could specify a route glob as such: www/**

Important: Pretty much every JS library I’ve found that does any sort of route matching with globs uses the minimatch library. Currently this library has what I consider to be unintuitive, undocumented, and even incorrect behavior. The library does not do path normalization before matching which leads to the library not recognizing the equality of paths prefixed with the relative “./“ compared to those that are not. Thus a glob specified as ./www/**/*.js would NOT match the path www/app/test.js . In the Gulp ecosystem at least, it seems that path names are piped between processes without the “./“ so I would recommend that any relative paths or globs you manually specify not have the “./“ prefix.

Once you have Gulp setup to properly watch your asset files and copy them to the appropriate directory, the next step is to configure a basic http server to serve those assets, then trigger a livereload. I fulfilled the first requirement using the simple ecstatic module configured to serve all files from the platforms/ios/www/ directory on port 8000. Make sure your assets are served with a very short cache time . This can be done with ecstatic by specifying cache: 0 in the config object. Once Gulp copies a changed file to the appropriate directory, pipe the new file to livereload to trigger the change.

The final step is to install the the Cordova plugin. From your root directory type: cordova plugin add pro.fing.cordova.gapreload --variable SERVER\_HOST="<host>" where is the is the computer the files are hosted from. If you’re developing with the emulator, this would probably be localhost however you can also specify any IP you want meaning you can use this setup when developing with an actual device

Conclusion That’s pretty much it! Now when you save a file, Gulp should

automatically detect the file has changed, copy it to the platforms directory, then trigger a livereload action. You app running in the emulator should instantly receive the livereload notification, load the changed file from the server, then refresh the webview so the changes take effect.

Caveats This setup has been working pretty well for me however there are

some caveats that are important to mention. First of all, when using the gapreload plugin XMLHttpRequests will be subject to cross origin restrictions meaning you won’t be able to make normal requests to servers that don’t have CORS enabled, even if you whitelist the domain in your config.xml.

Below are the relevant portions of my Gulpfile.js:

Buffering Audio in Parallel on Mobile with Web Audio API

Introduction

The HTML audio element is pretty powerful, unfortunately, mobile browsers tend to restrict it by not letting you play more than one audio stream at once. Furthermore, these browsers will also restrict buffering multiple audio tracks making seamless audio switching difficult at best. While these restrictions are fine for most applications, some apps require many sounds to be played in quick succession. Games with sound effects are the biggest victim of this policy and the most commonly used strategy to circumvent it is to use audio sprites. Below, I will discuss an alternate method that makes use of XMLHttpRequests and the experimental Web Audio API to buffer audio files in parallel then seamless switch between them.

Solution

The first thing we need to do is setup our Web Audio API context. Because the spec has yet to be finalized, it may be necessary to use a vendor prefix:

1
2
3
4
5
if ('webkitAudioContext' in window) {
var myAudioContext = new webkitAudioContext();
} else {
var myAudioContext = new AudioContext();
}

Here, we simple check to see if the prefixed version exists, if not we use the non prefixed version.

The XMLHttpRequest object has received some major updates in the last few years. In addition to being able to send and receive text, it can now do the same with arraybuffers, blobs, and documents. Using the wonderful Async.js library, we simultaneously download multiple audio files as arraybuffers and save them for playback later:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var songUrls = [ /* list of song URLs to buffer */ ];
var songData = [];
async.map(songUrls, function (url, cb) {
request = new XMLHttpRequest();
request.open('GET', url, true);
request.responseType = 'arraybuffer';
request.addEventListener('load', function (event) {
try {
var request = event.target;
var source = myAudioContext.createBufferSource();
source.buffer = myAudioContext.createBuffer(request.response, false);
return cb(null, source);
} catch (err) {
return cb(err);
}
}, false);
request.send();
}, function (err, result) {
if (err) {
$('#message').text("Error Buffering: " + err);
return;
}
songData = result;
});

You’ll notice that once the array buffer is finished downloading we immediately create a new AudioBufferSourceNode and populate it with the downloaded data using the createBuffer method (bonus: the preferred way to do this step is to use the decodeAudioData() method because it’s asynchronous and has better error handling).

At this point, we now have an array of audio buffers so let’s look at how to play them back. The Web Audio API is based on the concept of nodes which allow you to channel data through various transformation functions before finally outputting it. For the sake of this tutorial, all we care about is playback so all we need to do is connect one of the buffers to the output device. We do that using the connect() method:

1
songData[0].connect(myAudioContext.destination);

Although we can start playing the audio now, it is not ideal to do so because audio buffers cannot be restarted. To get around this, we first clone the audio buffer we want to playback:

1
source = myAudioContext.createBufferSource(); source.buffer = songData[0].buffer; source.connect(myAudioContext.destination); source.start(0);

Now, if we want to restart the track or switch to a new track, we can do so by calling source.stop(0) then clone the next audio buffer and start playing it.

A working implementation of this method can be found at http://emgeee.com/projects/web_audio_test

Limitations

While the method described overcome some of the limitations imposed by mobile browsers, it is still constrained by the need for some sort of user interaction before the audio can start playing. On the bright side, we can at least buffer the audio before hand so that when the user does hit play, the audio will start immediately. Normal HTML5

Unlike the

Conclusion

This post describes a method for buffering audio in parallel and playing it back using the experimental Web Audio API. I expect this method will become more robust in the future as the API is further standardized across browsers. I have tested this solution on both Chrome 35 for Android and Mobile Safari running on IOS7 with great results. Unfortunately, the stock Android browser does not support Web Audio, which is particularly unfortunate because that is what is used to run hybrid Cordova apps. For those looking to target IOS exclusively, however, I see this as a viable solution.

Full Sample Code