Simple Slot machine game using HTML5 Part 2: Audio

UPDATE: See also Simple Slot machine game using HTML5 Part 3: Loading.

In part 1 I presented simple slots machine purely in HTML5. This part extends the basic implementation with audio support. The game itself is simple slot machine that has only one winning line and we add effects for roll start, reel stop and win/loss.

Slots with audio

Try audio enabled game here. Note that loading time is significantly longer in audio enabled version. Debug text under button tells what audio system game uses for your browser.
Original non audio version from part 1 is here.

How to support Audio

Web game can implement audio in 3 main ways.

1. Flash player audio. (e.g. use SoundManager2 library)
2. HTML5 Audio (Buzz is easy way to use it)
3. Web Audio API. (See HTML5 Rocks for tutorial)

Flash audio is pretty much deprecated now, as only older browsers will still need it that most of don’t support HTML5 canvas anyway.
HTML5 Audio works very well for desktop browsers, but has only nominal support for mobile (Android & iOS). It’s generally too moody for tablets or mobile.
Web Audio API is supported only by latest browsers, but it works reliably e.g. in Safari iOS 6.0.

Web Audio API has two implementations in wild, the WebKit (iOS, Safari, Chrome) and the Standard (latest Firefox). Fortunately differences are small.

The libraries listed above simplify implementation a lot, but it’s easier to understand how these technologies work with simple examples. So I implemented both methods 2 and 3 from scratch.

Some caveats with audio.

  • Game initial loading time will increase. Audio files can be pretty large and they must be usually preloaded so they can be played on game start
  • iOS (iPad/iPhone) does not allow autoplay for audio. Audio must be enabled by playing some sound in click event handler.

Implementation

Initialization function accepts array of objects that have required audio file name in id property and callback that is called after audio has been initialized and loaded.
First code checks if mp3 or ogg is supported. Firefox requires .ogg and it’s easy to convert at least in OS/X or Linux with ffmpeg. Exact command line depends little on ffmpeg version.

$ ffmpeg -i win.mp3 -strict experimental -acodec vorbis -ac 2 win.ogg

When format is known, the code checks wether to use Web Audio API or normal HTML5 Audio.

function initAudio( audios, callback ) {

    var format = 'mp3';
    var elem = document.createElement('audio');
    if ( elem ) {
        // Check if we can play mp3, if not then fall back to ogg
        if( !elem.canPlayType( 'audio/mpeg;' ) && elem.canPlayType('audio/ogg;')) format = 'ogg';
    }

    var AudioContext = window.webkitAudioContext || window.mozAudioContext || window.MSAudioContext || window.AudioContext;

    if ( AudioContext ) {
        // Browser supports webaudio
        return _initWebAudio( AudioContext, format, audios, callback );
    } else if ( elem ) {
        // HTML5 Audio
        return _initHTML5Audio(format, audios, callback);
    } else {
        // audio not supported
        audios.forEach(function(item) {
            item.play = function() {}; // dummy play
        });
        callback();
    }
}

Both initialization functions attempt to load the desired format of needed audio files and sets play function in objects that is used to play the effect. If audio initialization fails, this play function is set to dummy empty function.

HTML5 Audio initialization function creates Audio objects and sets src to point to the audio file. Downloading is handled automagically by the browser.

function _initHTML5Audio( format, audios, callback ) {

    function _preload( asset ) {
        asset.audio = new Audio( 'audio/' + asset.id + '.' + format);
        asset.audio.preload = 'auto';
        asset.audio.addEventListener("loadeddata", function() {
            // Loaded ok, set play function in object and set default volume
            asset.play = function() {
                asset.audio.play();
            };
            asset.audio.volume = 0.6;
        }, false);

        asset.audio.addEventListener("error", function(err) {
            // Failed to load, set dummy play function
            asset.play = function() {}; // dummy
        }, false);

    }
    audios.forEach(function(asset) {
        _preload( asset );
    });
...

Web Audio initialization is bit more complicated, it needs to download the audio with XHR requests.

function _initWebAudio( AudioContext, format, audios, callback ) {

    var context = new AudioContext();

    function _preload( asset ) {
        var request = new XMLHttpRequest();
        request.open('GET',  'audio/' + asset.id + '.' + format, true);
        request.responseType = 'arraybuffer';

        request.onload = function() {
            context.decodeAudioData(request.response, function(buffer) {

                asset.play = function() {
                    var source = context.createBufferSource(); // creates a sound source
                    source.buffer = buffer;                    // tell the source which sound to play
                    source.connect(context.destination);       // connect the source to the context's destination (the speakers)
                    // support both webkitAudioContext or standard AudioContext
                    source.noteOn ? source.noteOn(0) : source.start(0);
                };
                // default volume
                // support both webkitAudioContext or standard AudioContext
                asset.gain = context.createGain ? context.createGain() : context.createGainNode();
                asset.gain.connect(context.destination);
                asset.gain.gain.value = 0.5;


            }, function(err) {
                asset.play = function() {};
            });
        };
        request.onerror = function(err) {
            asset.play = function() {};
        };
        request.send();
    }

    audios.forEach(function(asset) {
        _preload( asset );
    });

}

NOTE: Chrome supports XMLHttpRequest only when loading pages over HTTP. If you load the HTML file locally you’ll see erros like this in the error console:
XMLHttpRequest cannot load file:///Users/teemuikonen/work/blog/slot2/audio/roll.mp3. Cross origin requests are only supported for HTTP.

After audio has initialized, the game can play any effect simply by calling play function for the effect. If audio initialization or loading failed, the play is simply a dummy function.

$('#play').click(function(e) {
    // start game on play button click
    $('h1').text('Rolling!');
    game.audios[0].play(); // Play start audio
    ...

Code is available in Github.

Continue to Simple Slot machine game using HTML5 Part 3: Loading.

Ad-hoc static file web server for development use with Node.js

There is often need to serve local files through web server so you can access them with browser at http://localhost:8000/ for development and debugging.

Python has good tool, SimpleHTTPServer that can run standalone simply by command python -m SimpleHTTPServer in the directory you want to serve.

Node.js has something similar, http-server that works essentially in the same way.

That said, Today I thought that what internet needs is yet another example of http server with Node.js. Instead of the tools above I tend to use my own one-file implementation as it’s easy to expand and modify for testing purposes, for example to add some mock up Ajax endpoints.
The code also demonstrates some common node.js programming patterns so learning programmers might find it useful.

Code

Server is based on express framework and uses send and async modules to serve index.html and directory listings.

Minimum viable static file server is only few lines in express.

var express = require('express'),
    path = require('path');

var mainapp = express();
mainapp.use(express.static( process.cwd() ));
mainapp.listen(8000);

In case file is not found, we need fallback handler for checking the index.html or if that does not exists then build directory listing. Note that this listens only HTTP GET requests, not POST or HEADs.

mainapp.get('*', function(req, res) {
   var pathname = url.parse(req.url).pathname;
   pathname = path.join(dir, pathname);

    fs.stat(pathname, function(err, stat) {
        // Check if path is directory
        if ( !stat || !stat.isDirectory() ) return res.send(404);

        // check for index.html
        var indexpath = path.join(pathname, 'index.html');
        fs.stat(indexpath, function(err, stat) {
           if ( stat && stat.isFile() ) {
               // index.html was found, serve that
               send(res, indexpath)
                   .pipe(res);
               return;

           } else {
               // No index.html found, build directory listing
               fs.readdir(pathname, function(err, list) {
                  if ( err ) return res.send(404);
                  return directoryHTML( res, req.url, pathname, list );
               });
           }
        });
    });
});

There is to default 404 handler, as express does this automatically.

Function that builds the HTML page from directory listing is surprisingly messy, as it’s not easy to do directory iteration with asynchronous node.js fs api. Directory listing use same HTML layout as python’s SimpleHTTPServer.

function directoryHTML( res, urldir, pathname, list ) {
    var ulist = [];

    function sendHTML( list ) {
        res.setHeader('Content-Type', 'text/html');
        res.send('<!DOCTYPE html>' +
            '<html>\n' +
            '<title>Directory listing for '+urldir+'</title>\n' +
            '<body>\n' +
            '<h2>Directory listing for '+urldir+'</h2>\n' +
            '<hr><ul>\n' +
            list.join('\n') +
            '</ul><hr>\n' +
            '</body>\n' +
            '</html>');
    }

    if ( !list.length ) {
        // Nothing to resolve
        return sendHTML( ulist );
    }

    // Check for each file if it's a directory or a file
    var q = async.queue(function(item, cb) {
        fs.stat(path.join(pathname, item), function(err, stat) {
           if ( !stat ) cb();
           if ( stat.isDirectory() ) {
               ulist.push('<li><a href="'+item+'/">'+item+'/</a></li>')
           } else {
               ulist.push('<li><a href="'+item+'">'+item+'</a></li>')
           }
            cb();
        });
    }, 4); // 4 parallel tasks
    // Push directory listing in workqueue
    list.forEach(function(item) {
        q.push(item);
    });
    // Set drain handler that is called when all tasks are completed
    q.drain = function() {
       sendHTML(ulist);
    };
}

Function uses async librarys queue to control the execution flow.

Running

Running directly with node. Server accepts web root directory as argument.
Download and see full code in Github.

$ git clone git://github.com/tikonen/blog.git
$ cd blog/simplehttpserver
$ node simplehttpserver.js ~/work
Listening port 8000 root dir /Users/teemuikonen/work

Server prints out request access log for debugging purposes.

127.0.0.1 - - [Sun, 2 Apr 2013 12:48:08 GMT] "GET / HTTP/1.1" 200 730 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:20.0) Gecko/20100101 Firefox/20.0"

You also install the server globally either from the checkout or from NPM repository. Install globally with

$ npm install -g simplehttpserver
$ simplehttpserver ~/work
Listening port 8000 root dir /Users/teemuikonen/work

Test server for Apple push notification and feedback integration

Here is a test server that you can use to verify your integration to Apple Push notification service and push notification feedback server. It should be good enough for testing out your application behavior and pdu format. Server helps you to get more debug info than just connecting directly to apple.

See these blog posts on details how to send push notifications

Test server runs under node.js and listens both SSL and plain ports where your application can connect. Get code from Github and generate the SSL keys (see quick howto in cert-howto.txt) and start up server

$ node server.js 
Waiting for connections in ports
Listening feedback port 2296 
Listening feedback port 2196 SSL
Listening push port 2295 
Listening push port 2195 SSL

Note that you may need to install binary module to run the server. Use npm install binary.

Successful push notification sending should look like following. The server dumps the data from your app in binary format for debugging and tries to parse it as Push PDU. Server prints out the fields so you can verify the data.

Accepted push connection 2195 1 SSL
=== RECEIVED DATA (1) ====
00000000: 0100 0000 0151 7261 1d00 206b 4628 de93  .....Qra...kF(^.
00000010: 17c8 0edd 1c79 1640 b58f dfc4 6d21 d0d2  .H.].y.@5._Dm!PR
00000020: d135 1687 239c 44d8 e30a b100 1e7b 2261  Q5..#.DXc.1..{"a
00000030: 7073 223a 7b22 616c 6572 7422 3a22 4865  ps":{"alert":"He
00000040: 6c6c 6f20 5075 7368 227d 7d              llo.Push"}}

=== PDU ====
{ command: 1,
  pduid: 1,
  expiry: Sat Apr 20 2013 17:34:21 GMT+0800 (SGT),
  tokenlength: 32,
  token: '6B4628DE9317C80EDD1C791640B58FDFC46D21D0D2D1351687239C44D8E30AB1',
  payloadlength: 30,
  payload: { aps: { alert: 'Hello Push' } } }

Test server does not validate the data, but it tries to parse JSON message in push notifications and prints error if it fails. Also if command was not set to 1, it sends back error pdu and closes connection. This should be good enough for testing. For example here I made HTTP request to the server to get some error output.

Accepted push connection 2295 1 SSL
=== RECEIVED DATA (1) ====
00000000: 4745 5420 2f20 4854 5450 2f31 2e31 0d0a  GET./.HTTP/1.1..
00000010: 5573 6572 2d41 6765 6e74 3a20 6375 726c  User-Agent:.curl
00000020: 2f37 2e32 392e 300d 0a48 6f73 743a 206c  /7.29.0..Host:.l
00000030: 6f63 616c 686f 7374 3a32 3239 350d 0a41  ocalhost:2295..A
00000040: 6363 6570 743a 202a 2f2a 0d0a 0d0a       ccept:.*/*....

=== PDU ====
{ command: 71,
  pduid: 1163141167,
  expiry: Sun Mar 01 1987 23:31:32 GMT+0800 (SGT),
  tokenlength: 20527,
  token: '312E310D0A557365722D4167656E743A206375726C2F372E32392E300D0A486F73743A206C6F63616C686F73743A323239350D0A4163636570743A202A2F2A0D0A0D0A',
  payloadlength: null,
  payload: 'ERROR: INVALID JSON PAYLOAD [SyntaxError: Unexpected end of input]' }
=== SEND ERROR: 08014554202F
Connection terminated 1

When your app connects successfully to the feedback test service, it sends back few feedback tokens and closes connection after one minute. Edit the actual tokens in the server source code.

Accepted feedback connection 2296 1
SEND: 2696A21000207518B1C2C7686D3B5DCAC8232313D5D0047CF0DC0ED5D753C017FFB64AD25B60
SEND: 2696A21100207518B1C2C7686D3B5DCAC8232313D5D0047CF0DC0ED5D753C017FFB64AD25B60
SEND: 2696A21100207518B1C2C7686D3B5DCAC8232313D5D0047CF0DC0ED5D753C017FFB64AD25B60

Source code is available in Github.

Minesweeper clone in HTML5

In my two previous block entries I wrote about one possible ways to do simple Slots and Car games on HTML5 technologies. This writeup combines some of those methods and introduces new ones to implement Minesweeper game with a twist.

mine

This game is actually “reverse” minesweeper, or should we say Applesweeper. Players mission is to find all the apples without clicking on of the empty tiles. Score is increased when player finds an apple and is decreased when he misses. Game ends when all the apples are found.

Try out the game here: http://ikonen.me/examples/mine/

As in previous games, hud is a html div, and the game grid is drawn on a canvas. Grey slab that hides tiles content is procedurally generated on offscreen canvas at the startup.

this.slab = document.createElement('canvas');
var ctx = this.slab.getContext('2d');
this.slab.width = this.resolution;
this.slab.height = this.resolution;

ctx.fillStyle = 'grey';
ctx.fillRect(0, 0, this.resolution, this.resolution);

ctx.beginPath();
ctx.fillStyle = 'white'
ctx.moveTo(0, 0);
ctx.lineTo(this.resolution, 0);
ctx.lineTo(this.resolution, this.resolution);
ctx.lineTo(0, 0);
ctx.closePath();
ctx.fill();

ctx.fillStyle = 'lightgrey';
ctx.fillRect(4, 4, this.resolution-8, this.resolution-8);

The this.slab holds off-screen canvas that contains the generated slab image.

Game area size and number of apples are function of screen size, to adapt to different screensizes.

var width  = window.innerWidth;
var height = window.innerHeight;

GRID_W = Math.min( 12, ~~(width / GRID_RESOLUTION));
GRID_H = Math.min( 12, ~~(height / GRID_RESOLUTION)) - 1;
var APPLE_COUNT = ~~((GRID_W * GRID_H) / 8);

For example, Here is the game on iPhone 3Gs.

mine_iphone

Game main loop is passive, when user clicks on the screen click handler sets the location on object that holds the clicked tile x and y coordinates

$('#container').click( function( e ){
    var p = $('#canvas').offset();        
    game.click = {
        x:parseInt((e.pageX-p.left)/game.resolution),
        y:parseInt((e.pageY-p.top)/game.resolution)};
    }
);

Main update handler is called on each frame and it checks if button has been clicked and updates the grid and redraws it if required. Grid is simple array where x and y are mapped as position.

Game.prototype.pos = function( x, y ) {
    return y*this.width+x;
}
Game.prototype.xy = function( pos ) {
    return {
        x:parseInt(pos%this.width),
        y:parseInt(pos/this.width)
    }
}

Grid array values are integers where content is bit masked. Higher bits are used to flag if grid location has slab and apple, empty or number.

var SLAB_MASK = Math.pow(2, 16);
var APPLE_MASK = Math.pow(2, 15);

// grid location (5, 6) has slab and number 3
var pos = this.pos(5, 6);
this.grid[pos] = 3;
this.grid[pos] |= SLAB_MASK;

The slab is removed from location just by negating it

this.grid[pos] &= ~SLAB_MASK;

Draw loop just checks for each position and checks with mask what it contains

for ( var y=0; y < this.height; y++ ) {
    for ( var x=0; x < this.width; x++ ) {
        // Draw each tile
        var s = this.pos(x,y);
       if (s & SLAB_MASK) {
          // Still covered tile
         this.ctx.drawImage( this.slab, x * this.resolution, y * this.resolution )

       } else if (s & APPLE_MASK) {
         // Uncovered apple
         this.ctx.drawImage( tile, x * this.resolution + 2, y * this.resolution + 2 )
       } else if (s > 0) {
         // Neighbour number
         this.ctx.fillText( '' + s ,
                            x * this.resolution + this.resolution/2,
                            y * this.resolution + this.resolution/2)
       }
    }
}

When player clicks on empty tile, recursive function walks through the grid clearing slabs from adjacent empty tiles.

function _empty( x, y, force ) {
    if ( x < 0 || x >= that.width || y < 0 || y >= that.height ) return;

    var pos = that.pos(x, y);
    var d = that.grid[pos];

    if (d && (d & SLAB_MASK) && (force || !(d & APPLE_MASK))) {

        that.grid[pos] &= ~SLAB_MASK; // clear out slab

        // Clear next neighbor if this is empty tile
        if (that.grid[pos] == 0) {
            _empty(x, y - 1) // north
            _empty(x, y + 1) // south
            _empty(x - 1, y) // west
            _empty(x - 1, y - 1) // north west
            _empty(x - 1, y + 1) // south east
            _empty(x + 1, y) // east
            _empty(x + 1, y - 1) // north east
            _empty(x + 1, y + 1) // south east
        }
    }
}

Code is available at GitHub.

Car Game on HTML5

This is continuation to my previous blog post, that discussed how to make simple Slots machine with HTML5. The basic principle in this driving game demo is similar, but in addition to that it adds dynamic graphics using canvas.

HTML5 Drive

Try it out here: http://ikonen.me/examples/drive/

Game road is div with background image that is translated down and back up to create illusion of moving road. This same method is used in Slots machine.

Car spritecar2

Cars are drawn on canvas on top of the road so while road moves cars appear moving forward. Each click starts moving the player car from current point to the click point using smooth  Bezier curve. Car acceleration is increased or decreased depending on click location. Player car also has smoke plume animation which intensity depends on acceleration.

function _bezier_quad(t, p0, p1, p2) {
     return {
         x: (1 - t)*( (1 -t) * p0.x + t*p1.x) + t * ( (1 - t) * p1.x + t * p2.x),
         y: (1 - t)*( (1 -t) * p0.y + t*p1.y) + t * ( (1 - t) * p1.y + t * p2.y)
     }
 }

After car has travelled long enough, the update loop starts slowing down road speed and moves in sync  div that holds Finish text and flags.

finish

Collision detection is done on each update. In case player hits on other cars, the other cars may blurb random bubbles that are added as normal div elements with rounded borders. Element location is updated based on car location.

bubble

In collision both cars are bumped so player can make room by hitting to other cars. Game loop is run by animation frames, so it stops if browser window goes in background. This is not optimal but makes implementation simpler. Better option is to run update with setInterval and draw on animation frames.

...
    var that = this;
    (function gameLoop() {
        that.clear(); // clear canvas objects
        that.update(); // update game state
        that.draw(); // draw game
        requestAnimFrame( gameLoop );
    })();
...

Update and draw in separate loops. This is better solution for complex games because browser will always call setInterval callback but requestAnimFrame callback only if browser window is visible and active. Callback is not executed if browser window is minimized or it’s tab is on background. If you rely only on requestAnimFrame the game is paused on background.

    var that = this;
    setInterval(function() {
        that.update(); // update game state
    }, 1000/30); // 30 frames

    (function gameLoop() {
        that.clear(); // clear canvas objects
        that.draw(); // draw game
        requestAnimFrame( gameLoop );
    })();

Game code is available in GitHub.

Simple Slot machine game using HTML5 Part 1: Basics

UPDATE: See also Simple Slot machine game using HTML5 Part 2: Audio.

Here is overview on how to make simple Slot machine with HTML5. This demonstrates the basic structure of HTML5 game and how to use dynamically created graphics.

Slot machine has typically reels with images and player just initiates the action and waits until reels stop. 1 or more in single line usually determine the winning condition. In this game player wins if he or she gets more than one gold bar in row.

Here is view of the game.

Slots machine

You can try it out here http://ikonen.me/examples/slot/.

How it works

Slot machine is a single HTML page that includes the game code, webfont and jQuery. When loaded it runs SlotGame() function that initializes and runs the game.

HTML page has 3 narrow and tall HTML5 canvases, these are the reels. They are located inside div container “reels” that shows only a limited window at any time, hiding the rest of the canvases.

<div id="reels">
   <canvas id="canvas1" width="70" height="300"></canvas>;
   <canvas id="canvas2" width="70" height="300"></canvas>;
   <canvas id="canvas3" width="70" height="300"></canvas>;
</div>

On initialization, game preloads the 6 image assets. Preloading is simply done by creating Image object for each asset and listening its load event. Preloading is required, because otherwise game could not draw the reel canvases on initialization.

img = new Image()
img.src = "img/someimage.png"
img.addEventListener("load", function() {
   // image loaded
});

Game draws the pictures in random order on each canvas reel with shadow and slot separator bars.

ctx.save();
ctx.shadowColor = "rgba(0,0,0,0.5)";
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 5;
ctx.shadowBlur = 5;
ctx.drawImage(asset.img, 3, i * SLOT_HEIGHT + IMAGE_TOP_MARGIN);
ctx.drawImage(asset.img, 3, (i + ITEM_COUNT) * SLOT_HEIGHT + IMAGE_TOP_MARGIN);
ctx.restore();
ctx.fillRect(0, i * SLOT_HEIGHT, 70, SLOT_SEPARATOR_HEIGHT);
ctx.fillRect(0, (i + ITEM_COUNT)  * SLOT_HEIGHT, 70, SLOT_SEPARATOR_HEIGHT);

The reels are not redrawn after this, but when moving they are simply translated with CSS3 transform downwards and when they reach threshold they are moved back to beginning. Threshold and reset offset is selected so that after the reset images are shown on same locations. This creates illusion of constantly rotating wheel. This is why images on borders are twice in the reel, so we avoid showing canvas bottom or top in any situation.
Click ‘Toggle Reels’ button on top left corner of the game page to toggle the reel visibility while it’s spinning, seeing reels in action makes explanation above much easier to understand

Here is image where reel container overflow is set to visible.

Reels

Game loop is simple, it starts when player clicks ‘Play’ and runs on every animation frame updating the reel locations based on game state and “draws” them on screen, or actually as explained earlier just translates their locations. Result is predetermined on each roll start and when each reel stop, its locked on the correct image. Update loop tries to make this when correct image is close to this location, so the jump is not too abrupt (see function _check_slot in slot.js for details).

Each browser has still different name for the transform so initialization code determines the correct CSS name and if browser has hardware accelerated 3d version.

this.vendor = 
  (/webkit/i).test(navigator.appVersion) ? '-webkit' :
  (/firefox/i).test(navigator.userAgent) ? '-moz' :
  (/msie/i).test(navigator.userAgent) ? 'ms' :
   'opera' in window ? '-o' : '';

this.cssTransform = this.vendor + '-transform';
this.has3d = ('WebKitCSSMatrix' in window &amp;&amp; 'm11' in new WebKitCSSMatrix())  
this.trnOpen       = 'translate' + (this.has3d ? '3d(' : '(');
this.trnClose      = this.has3d ? ',0)' : ')';
...
$('#someelement').css(this.cssTransform, this.trnOpen + '0px, ' + '-123px' + this.trnClose);</pre>

Complete code is available on Github.

Continue to Simple Slot machine game using HTML5 Part 2: Audio

Running external worker process in Node.js

This is example how to properly spawn external worker process in Node.js with error checking. This example runs a ImageMagick convert tool to transform .png image files to 8-bit .png file.

Function spawns ‘convert’ with original file as input and temporary destination path as output and checks the result. If everything seems to be in order, it renames the temporary file over original one.

This also prints out all output from the spawned process to stdout for diagnostics.

var fs = require('fs'),
    util = require('util'),
    spawn = require('child_process').spawn;

function package_image( path, next ) {

    // temporary file name. use process PID as part of the name so there
    // wont' be conflicts if two processes run in parallel
    var tmpfile = path + '.tmp.' + process.pid;

    var cmd = 'convert';
    // executes command 'convert path -type Palette png8:path.tmp'
    var convert = spawn(cmd, [
        path,
        '-type', 'Palette',
	'png8:' + tmpfile ]);

    // capture stdout and stderr. Note that convert does not have any output on success
    convert.stdout.setEncoding('utf8');
    convert.stdout.on('data', function (data) {
        console.log( cmd + ': stdout '+ path + ' ' + data.trim() );
    });

    convert.stderr.setEncoding('utf8');
    convert.stderr.on('data', function (data) {
        if (/^execvp\(\)/.test(data)) {
            // we get here if 'convert' command was not found or could
            // not be executed
	    console.log( cmd + ': failed to start: ' + data );
	} else {
	    console.log( cmd + ': stderr '+ path + ' ' + data.trim() );
	}
    });

    // hook on process exit
    convert.on('exit', function( code ) {
        if ( code ) {
	    // 127 means spawn error, command could not be executed
   	    console.log(cmd + ': error '+ path + ' ' + code );
	    return next(code);
	}
        // check if output file exists
        fs.stat( tmpfile, function(err, info) {
	    if ( err ) {
		// no file found? something went wrong. Just ignore
		console.log( cmd + ': output file not found ' + util.inspect(err));
		return next( err );
	    }
	    // check output file size
	    if ( !info.size || !info.isFile() ) {
		console.log( cmd + ': out file 0 bytes or not a file '+ tmpfile);
		fs.unlink( tmpfile ); // remove output file
		return next( true );
	    }

	    // rename temporary file over original one
	    fs.rename( tmpfile, path, function(err) {
	         if ( err ) {
		    fs.unlink( tmpfile );
		    console.log( cmd + ': can not rename '+ tmpfile + ' ' + util.inspect(err));
		 }
                 // done
		 return next( err );
	    });
	});
    });
}

Example of usage

package_image( '/tmp/some.png', function(err) { 
    if ( err ) { 
       console.log('Image conversion failed', err );
    }
});

Apple Push Notifications with Haskell

In series of language evaluation it’s this time Push notifications with Haskell. Haskell is a pure functional language with strong static typing and as such is not ideally suited for IO code (networking) with dynamic data (JSON). Let’s see how it compares with the others. So far in the series

Step 1. Prerequisites

This code uses GHC 7.4.2 (Haskell compiler). On OS/X its easiest to install with port (or homebrew)

    $ sudo port install ghc
    $ ghc --version
    The Glorious Glasgow Haskell Compilation System, version 7.4.2

This installs the compiler and interpreter. You will also need cabal package manager. Base Haskell installation has surprisingly few tricks out of the box and lots of libraries are needed.

    $ sudo port install hs-cabal
    $ cabal-0.14.0 update
    $ cabal-0.14.0 install cabal-install

After this cabal command can be run from ~/.cabal/bin/cabal

  • Read introduction to Apple Push here and get application and private key sandbox certificates as .pem files.
  • And of course you need to have 32 byte push token from your iOS application.

Step 1. The Utilities.

Hexadecimal to binary

Haskell like many similar languages encourage coding style where programs are written as composition of small simple functions. We’ll start with one that encodes hexadecimal string to ByteString. ByteString is Haskell’s practical presentation of binary data.

import qualified Data.ByteString as B
import GHC.Word (Word8)
import Data.Convertible (convert)
import Data.Char (ord, chr, toUpper)
import Data.Bits (shift, (.|.), (.&.)) 

hexToByteString :: String -> B.ByteString
hexToByteString s
  | null s = B.empty
  | otherwise = B.pack . hexToWord8 $ s
  where
    hexToWord8 :: String -> [Word8]
    hexToWord8 [] = []
    hexToWord8 [x] = error "Invalid hex stream"
    hexToWord8 (x:y:xs) = [ hn .|. ln ] ++ hexToWord8 xs
      where
        hn = (shift (decodeNibble x) 4)
        ln = decodeNibble y
        decodeNibble c
          | o >= oA && o <= oF = convert (o - oA + 10) :: Word8
          | o >= o0 && o <= o9 = convert (o - o0) :: Word8
          | otherwise = error $ "Invalid hex: " ++ [c]
          where o = ord . toUpper $ c
                oA = ord 'A'
                oF = ord 'F'
                o0 = ord '0'
                o9 = ord '9'

Note the imports for Word8 and convert. Haskell as statically typed language requires that every single item has to have unambiguous data type. Imported functions are needed to manipulate and convert data so we can get from String (that is list of Char’s) to list of 8bit bytes (list of Word8’s) finally to ByteString

Install these packages to import Data.Convertible and ByteString

~/.cabal/bin/cabal install convertible
~/.cabal/bin/cabal install bytestring

Save file as Hex.hs and try it out

$ ghci
GHCi, version 7.4.2: http://www.haskell.org/ghc/  :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Prelude> :load Hex.hs
[1 of 1] Compiling Util.Hex         ( Hex.hs, interpreted )
Ok, modules loaded: Util.Hex.
*Util.Hex> hexToByteString "AABBCC"
"\170\187\204"
*Util.Hex> :type hexToByteString "AABBCC"
hexToByteString "AABBCC" :: B.ByteString
*Util.Hex> hexToByteString "Something"
"*** Exception: Invalid hex stream
*Util.Hex> hexToByteString "AA"
"\170"

Seems to be working.

JSON encoding

Next step is function that produces a UTF8 encoded JSON object that contains our push notification payload. We use simple object that contains only the alert message.

Save this in file Push.hs

import Text.JSON as JSON
import qualified Data.ByteString.UTF8 as BU

getJSONWithMessage :: String -> JSObject (JSValue)
getJSONWithMessage msg =
  let jmsg = JSString (toJSString msg) in
  toJSObject [("aps",
               JSObject (toJSObject [("alert", jmsg)]))]

Install packages for UTF8 ByteString and Text.JSON

~/.cabal/bin/cabal install json
~/.cabal/bin/cabal install utf8-string

Check that we get correctly formatted JSON string

Prelude> :load Push.hs
[1 of 1] Compiling Main             ( Push.hs, interpreted )
Ok, modules loaded: Main.
*Main> getJSONWithMessage "Hello World"
*Main> getJSONWithMessage "Hello World"
JSONObject {fromJSObject = [("aps",JSObject (JSONObject {fromJSObject = [("alert",JSString (JSONString {fromJSString = "Hello World"}))]}))]}
*Main> JSON.encode . getJSONWithMessage $ "Hello World"
"{\"aps\":{\"alert\":\"Hello World\"}}"

Lets also check that messages with non-ASCII character can be supported

*Main BU> BU.fromString . JSON.encode . getJSONWithMessage $ "Mötörhead"
"{\"aps\":{\"alert\":\"M\195\182t\195\182rhead\"}}"
*Main BU> 

The encoded JSON looks fine.

Building the PDU

For this purpose we use Put that supports building Lazy BinaryString’s with content

import qualified Data.ByteString as B
import qualified Data.ByteString.UTF8 as BU
import qualified Data.ByteString.Lazy as BL
import Data.Binary.Put
import GHC.Word (Word32, Word16)
import Data.Convertible (convert)

buildPDU :: B.ByteString -> BU.ByteString -> Word32 -> Put
buildPDU token payload expiry
  | (B.length token) /= 32 = fail "Invalid token"
  | (B.length payload > 255) = fail "Too long payload"
  | otherwise = do
    putWord8 1 -- command
    putWord32be 1 -- transaction id, can be anything
    putWord32be expiry  -- expiry time as seconds from epoch
    putWord16be ((convert $ B.length token) :: Word16) -- length of token
    putByteString token  -- push token
    putWord16be ((convert $ B.length payload) :: Word16) - payload length
    putByteString payload -- the json encoded as utf-8 string

We need also simple function to compute expiry time as relative to now

import Data.Time.Clock.POSIX (getPOSIXTime)

getExpiryTime :: IO (Word32)
getExpiryTime = do
  pt <- getPOSIXTime
  -- One hour expiry time
  return ( (round pt + 60*60):: Word32)

Install packages for Data.Binary.Put

~/.cabal/bin/cabal install binary

Step 2. Connecting to the Server

Make sure you have the certificate files (cert.pem and key-noenc.pem).

import Network.Socket
import Network.BSD (getHostByName, hostAddress, getProtocolNumber)
import OpenSSL
import OpenSSL.Session as SSL (
  context,
  contextSetPrivateKeyFile,
  contextSetCertificateFile,
  contextSetCiphers,
  contextSetDefaultCiphers,
  contextSetVerificationMode,
  contextSetCAFile,
  connection,
  connect,
  shutdown,
  write,
  read,
  SSL,
  VerificationMode(..),
  ShutdownType(..)
  )

main = withOpenSSL $ do
  -- Prepare SSL context
  ssl <- context
  contextSetPrivateKeyFile ssl "key-noenc.pem"
  contextSetCertificateFile ssl "cert.pem"
  contextSetDefaultCiphers ssl
  contextSetVerificationMode ssl SSL.VerifyNone

  -- Open socket
  proto <- (getProtocolNumber "tcp")
  he <- getHostByName "gateway.sandbox.push.apple.com"
  sock <- socket AF_INET Stream proto
  Network.Socket.connect sock (SockAddrInet 2195 (hostAddress he))

  -- Promote socket to SSL stream
  sslsocket <- connection ssl sock
  SSL.connect sslsocket  -- Handshake

  -- we'll send pdu here

Install OpenSSL package

~/.cabal/bin/cabal install HsOpenSSL

Step 3. Send the PDU

Now we’re finally ready to send the actual push notification message. Replace the push token to your own in following example.


...
  -- Promoto socket to SSL stream
  sslsocket <- connection ssl sock
  SSL.connect sslsocket  -- Handshake

  expiration <- getExpiryTime
  -- we send pdu here
  let token = "6b4628de9317c80edd1c791640b58fdfc46d21d0d2d1351687239c44d8e30ab1"
      message = "Hello World"
      btoken = hexToByteString token
      payload = BU.fromString . JSON.encode . getJSONWithMessage $ message
      lpdu = runPut $ buildPDU btoken payload expiration  -- build binary pdu
      pdu = toStrict lpdu  -- from lazy bytestring to strict
    in do
    SSL.write sslsocket pdu
    SSL.shutdown sslsocket Unidirectional -- Close gracefully
  where
    toStrict = B.concat . BL.toChunks

Then compile and run program

$ ghc -threaded -o Push Push.hs
[1 of 2] Compiling Hex              ( Hex.hs, Hex.o )
[2 of 2] Compiling Main             ( Push.hs, Push.o )
Linking Push ...
$ ./Push

If everything went fine, the program exits within few seconds and you’ll see your push notification appear on your iOS device.

Full source of this example available here: https://github.com/tikonen/blog/tree/master/apn-haskell

Polling Apple Push Notification feedback service with Node.js

Apple assumes that your app polls the Push Notification Feedback Service to get information about App uninstalls so you can stop sending  notifications to those devices. Service returns list of tokens with uninstall timestamps.

This is how you can do it with Node.js. First, check TLS Example and how to create SSL certs to understand basics of how to make secure SSL connection with Node.js.

First simple utility to convert binary data to hex

function bintohex(buf) {

    var hexbuf = new Buffer(buf.length * 2);

    function nibble(b) {
	if (b <= 0x09) return 0x30 + b;
	return 0x41 + (b - 10);
    }

    for(var i=0; i < buf.length; i++) {
        hexbuf[i*2] = nibble(buf[i] >> 4);
        hexbuf[i*2+1] = nibble(buf[i] & 0x0F);
    }
    return hexbuf.toString('ascii', 0, hexbuf.length);
}

Then define function that will be called with push device tokens

function processTokens( pdtokens ) {

    // send or process tokens here
    pdtokens.forEach( function( pd  ){
	console.log('TOKEN ' + pd.token ' INVALIDATED AT ' + new Date( pd.timestamp ) );
    });
}

Then polling function that does it all, note that it assumes function connectAPN that needs simply create SSL connection to ‘feedback.push.apple.com’ port 2196 with your apps client cert and private key.

function pollAPNFeedback() {

    console.log("Connecting APN feedback service");

    connectAPN(function( apnc ) {

	var bufferlist = [];
	apnc.on('data', function(data) {
	    // APN feedback starts sending data immediately on successful connect
	    bufferlist.push(data)
	});

	apnc.on('end', function() {
	    console.log("APN Connection closed");

	    // tokens are parsed to object
	    var pdtokens = []

	    // concatenate all asynchronously collected buffers, and parse the PDU's from them
	    var parsebuf;
	    var totall = 0;
	    for(var i=0; i < bufferlist.length; i++) {
	        totall += bufferlist[i].length;
	    }
	    parsebuf = new Buffer(totall);
	    var offset = 0;
	    for(var i=0; i < bufferlist.length; i++) {
		bufferlist[i].copy(parsebuf, offset, 0);
		offset += bufferlist[i].length;
	    }

	    var count = 0;
	    for(var k = 0; k < parsebuf.length; ) {
		count++;
                 // parse timestamp
		var ts = ((parsebuf[k] << 24) + (parsebuf[k+1] << 16) + (parsebuf[k+2] << 8) + parsebuf[k+3]) >>> 0;
		k += 4;
		var l = ((parsebuf[k] << 8) + parsebuf[k]) >>> 0;
                  k += 2;
                  var pdtokenbuf = new Buffer(l);
		parsebuf.copy(pdtokenbuf, 0, k, k + l);
		k += l;

		var hextoken = bintohex(pdtokenbuf);
		pdtokens.push({timestamp: ts, token:hextoken});

		// process tokens in 100 item batches
	        if( count >= 100 ) {
		    processTokens( pdtokens );
		    count = 0;
		    pdtokens = [];
		}
	}
	processTokens(pdtokens);
         setTimeout(pollAPNFeedback, 1000*60*60*4);
    });
}

pollAPNFeedback();

The function read raw binary data from connection and after Feedback services closes the socket (this happens immediately after it has sent the data) it parses tokens and calls processing function to handle them. Function connects to server every 4 hours.

Note that feedback service host and port is different for sandbox (testing environment) that also needs its own SSL certificates.

Simple popularity algorithm with decay

Most social web sites have user created objects such as pictures, posts, links, etc.. that need to be shown in some dynamic popularity or ranking order on website. The measurement of popularity is typically some user activity such as votes, downvotes, number of clicks or shares per object.

It would be trivial just to calculate total sum of the actions, and sort objects by the sum but this doesn’t take into account the bandwagon effect where people start voting the already top items so the top list freezes quickly and no new items have change to bubble on top. Another problem of this approach is that once object reaches high score it stays high even when no actions would anymore happen. Good popularity measurement needs to decay somehow.

First page of top list (typically 10-20 items)  should always show fresh “hot” items together with long time favorites. We want that older objects fade and need more and more actions to stay on the top list as time passes by.

Simple way to do this is with relative scoring, where base score  grows constantly so that score for younger object is always naturally higher than for old objects with same number of actions. Object creation timestamp can be used trivially as base score. For example:

object_score = created_timestamp + number_of_actions

created_timestamp is object creation time as number of hours or days from some arbitrary fixed reference time . The popularity list is simply all objects sorted by the score. New objects have naturally higher score and old ones need more and more actions to stay on top.

Example in Javascript

Fixed reference time 1st Jan 2000.  Timestamp as hours

Object A created on midnight 1st May 2012, 30 votes

(new Date('2012-5-1') - new Date('2000-1-1'))/1000/60/60 + 30 = 108126

Object B created on midnight 2nd May 2012 2 votes

(new Date('2012-5-2') - new Date('2000-1-1'))/1000/60/60 + 2 = 108122

So, on 2nd May the object A score would still be ahead B but the B needs only 5 more votes to pass A. The A will fall behind quickly unless it gets constantly more votes. In fact with this trivial formula object has to get 24 actions per day just to keep up with the increasing base score.

Major benefit of growing base score is that its one pass, i.e. app  updates score for every new action and thats it. There is no need to do computation on query and no periodic maintenance is required. It’s also compatible with Map-Reduce based views (e.g. CouchDB) as its not dependent of query time.

I assume many high traffic web sites (Digg for sure) use  some derivative of this basic method, but each site needs to tune it based on their actions, traffic and user behavior.

See also Simple Cooldown and Rate Control Algorithm for another perspective to time based scoring.