Streaming Audio Goodness from Amazon S3 to the Client's Ears

Built up this cool little feature the other day for a project of mine. I'm going to go through how I got audio files stored on Amazon's S3 to stream down from on high and play in the browser.

If you're only here for the code, here you go.

If you don't have an Amazon Web Service account, you're going to need to head over there to get some creds.

This S3 streamer will be split up into three files along with a loading GIF so users don't get impatient.

- index.html
- server.js
- config.js

Server Set Up

First, we'll whip up an express server to handle two routes. One route will serve up index.html and the other will stream our S3 sounds.

var express = require('express');  
var app = express();

// Serve up index.html
app.use('/', express.static(__dirname));

app.get('/audio', function(req, res) {  
  // Add s3 streaming in here
});

// Start Listenin
app.listen(3000, function() {  
  console.log('makin music on 3000');
});

Next, we'll have to configure an S3 client. I found the s3 npm module to be really easy to use. It lets you set up a client, upload and download files without too much extra work. That said, it's a pretty high level module and if you need finer control over what's going on up there, check out (knox)[https://www.npmjs.com/package/knox] or the official Amazon module aws-sdk.

Make sure and install s3 then require it in server.js.

npm install s3

S3 will need to check out credentials first.

var s3 = require('s3');

//...

// Set up s3 credentials
var client = s3.createClient({  
  s3Options: {
    accessKeyId: <YOUR ACCESS KEY>,
    secretAccessKey: <YOUR SECRET KEY>
  }
});

That done, we'll fill in our route. To stream audio, s3 includes a downloadStream method which will create a read stream pointed at a file stored on S3 and then pipe it down to the client response.

To tell s3 which file we want to download, we need to pass it the name of the S3 Bucket it is stored in as well as its Key value. I have an mp3 file cleverly named test.mp3 stored in a bucket named New-Bucket-1020. Of course, yours can be named whatever your heart desires. We'll save the bucket and key information in a params object and pass that to the downloadStream method.

We'll attach two event listeners onto the download stream. One to check for errors and send a 404 if the files doesn't exist, and the other to listen for incoming headers. When the headers come in, we'll send that information down to the client so it knows what the hell is going on.

Finally, we'll pipe downloadStream to the response to get the tunes flowing.

app.get('/audio', function(req, res) {

  var params = {
    Bucket: 'New-Bucket-1020',
    Key: 'test.mp3'
  };

  var downloadStream = client.downloadStream(params);

  downloadStream.on('error', function() {
    res.status(404).send('Not Found');
  });
  downloadStream.on('httpHeaders', function(statusCode, headers, resp) {
    // Set Headers
    res.set({
      'Content-Type': headers['content-type']
    });
  });

  // Pipe download stream to response
  downloadStream.pipe(res);
});

Set Up the Client

On index.html we'll render two buttons, play and stop, which will do exactly what you hope they would.

We'll include a script that will start by grabbing the browser's AudioContext so we can get access to the speakers.

The play button will call the playTunes function which will trigger an ajax request to our server which will start the audio stream. When the data comes through, we'll put it through a helper function, process which will do most of our work. The process function will create a buffer source node, decode the Data from our stream, plug it into the user's speakers, and tell the source to start playing.

    window.AudioContext = window.AudioContext ||     window.webkitAudioContext;
    var context = new AudioContext();

// ...

    function playTunes() {
      var request = new XMLHttpRequest();
      request.open("GET", "http://localhost:3000/audio/", true);
      request.responseType = "arraybuffer";

      spinner.show();

      request.onload = function() {
          spinner.hide();
        var Data = request.response;
        process(Data);
      };

      request.send();
    }

    function process(Data) {
      source = context.createBufferSource(); // Create Sound Source
      context.decodeAudioData(Data, function(buffer) {
        source.buffer = buffer;
        source.connect(context.destination);
        source.start(context.currentTime);
      });
    }

Lastly, we'll add a function to stop the music, and some logic to show/hide a spinner gif while we're waiting for the stream to come on down from the server.

    var spinner = document.getElementById('spinner');

    spinner.hide = function(){
        this.style.display = 'none';
    };

    spinner.show = function(){
        this.style.display = 'block';
    }

    function stopTunes(){
        if(source.stop){
            source.stop();
        }
    }

Our HTML will look like this.

<!DOCTYPE html>  
<html>  
<head>  
    <title>S3 Audio</title>
</head>  
<body>  
    <button onclick="playTunes()">Play</button>
    <button onclick="stopTunes()">Stop</button>
    <div>
        <img id="spinner" style="display:none" src="ajax-loader.gif" alt="">
    </div>
    <script> // app logic </script>
</body>  
</html>  

So there it is! A quick way to stream audio samples down from S3. Full code is right here.