5

Is there a way to send images, videos, and audio to an AirPlay server using JavaScript in the browser?

Nightfirecat
  • 11,432
  • 6
  • 35
  • 51
themirror
  • 9,963
  • 7
  • 46
  • 79
  • [RAOP](http://en.wikipedia.org/wiki/Remote_Audio_Output_Protocol) *has* been reverse engineered, but the protocol is definitely not feasible with HTML/JS only. Perhaps with some native helpers (e.g. a local server). – ephemient Nov 09 '11 at 00:23

2 Answers2

3

It is not possible to achieve this in JavaScript. However, you might be able to run it from the browser with an NPAPI plugin (with great pain).

If you can run a local server, there are several node.js modules which make this much easier. The following example will stream any audio file that is posted to a nearby AirPlay device.

  • It requires the airtunes module from NPM, which I maintain.
  • It uses FFmpeg to transcode the file.

You can test it with:

curl -X POST --data-binary @sample.mp3 http://localhost:8080/audio

It assumes that FFmpeg is located in /usr/local/bin/ffmpeg, and that an AirPlay device is available on localhost:5000 (you can try with Airfoil Speakers).

var airtunes = require('airtunes'),
    express = require('express'),
    app = express(),
    device = airtunes.add('localhost'),
    spawn = require('child_process').spawn;

app.post('/audio', function(req, res) {
  // use ffmpeg to reencode data on the fly
  var ffmpeg = spawn('/usr/local/bin/ffmpeg', [
    '-i', 'pipe:0',       // Read from stdin
    '-f', 's16le',        // PCM 16bits, little-endian
    '-ar', '44100',       // Sampling rate
    '-ac', 2,             // Stereo
    'pipe:1'              // Output to stdout
  ]);

  // pipe data to AirTunes
  ffmpeg.stdout.pipe(airtunes, { end: false });

  // detect if ffmpeg was not spawned correctly
  ffmpeg.stderr.setEncoding('utf8');
  ffmpeg.stderr.on('data', function(data) {
    if(/^execvp\(\)/.test(data)) {
      console.log('failed to start ' + argv.ffmpeg);
      process.exit(1);
    }
  });

  req.pipe(ffmpeg.stdin);

  req.on('end', function() {
    res.end();
 });
});

device.on('status', function(status) {
  console.log('status: ' + status);
});

console.log('listening on port 8080');
app.listen(8080);

Laurent Perrin
  • 14,671
  • 5
  • 50
  • 49
1

This Works for me

    var xhr = new XMLHttpRequest(),
        xhr_stop = new XMLHttpRequest(),
        hostname = "apple-tv.local",
        port =":7000",
        position = "0";


    xhr_stop.open("POST", "http://" + hostname + port + "/stop", true, "AirPlay", null);
    xhr_stop.send(null);

    xhr.open("POST", "http://" + hostname + port + "/play", true, "AirPlay", null);
    xhr.setRequestHeader("Content-Type", "text/parameters");
    xhr.send("Content-Location: " + url + "\nStart-Position: " + position + "\n");

    // set timer to prevent playback from aborting
    xhr.addEventListener("load", function() { 

        var timer = setInterval(function() {

            var xhr = new XMLHttpRequest(),
                // 0 something wrong; 2 ready to play; >2 playing
                playback_info_keys_count = 0,
                terminate_loop, playback_started;

            xhr.open("GET", "http://" + hostname + port + "/playback-info", true, "AirPlay", null);

            xhr.addEventListener("load", function() {

                playback_info_keys_count = xhr.responseXML.getElementsByTagName("key").length;
                console.log("playback: " + playback_started + "; keys: " + playback_info_keys_count)

                // if we're getting some actual playback info
                if (!playback_started && playback_info_keys_count > 2) {
                    playback_started = true;
                    console.log("setting playback_started = true")
                    terminate_loop = false;
                }

                // playback terminated 
                if (terminate_loop && playback_info_keys_count <= 2) {
                    console.log("stopping loop & setting playback_started = false")
                    clearInterval(timer);
                    var xhr_stop = new XMLHttpRequest();
                    xhr_stop.open("POST", "http://" + hostname + port + "/stop", true, "AirPlay", null);
                    xhr_stop.send(null);                    
                    playback_started = false;
                }

                // playback stopped, AppleTV is "readyToPlay"
                if (playback_started && playback_info_keys_count == 2) {
                    console.log("sending /stop signal, setting playback_started = false")
                    var xhr_stop = new XMLHttpRequest();
                    xhr_stop.open("POST", "http://" + hostname + port + "/stop", true, "AirPlay", null);
                    xhr_stop.send(null);
                    playback_started = false;
                    terminate_loop = true;
                }

            }, false);

            xhr.addEventListener("error", function() {
                clearInterval(timer);
            }, false);
            xhr.send(null);

        }, 5000);

    }, false);
Alexey Sh.
  • 1,734
  • 2
  • 22
  • 34
amay0048
  • 961
  • 9
  • 11