Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
build
build
node_modules
4 changes: 2 additions & 2 deletions examples/play_ffmpeg.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ var airtunes = require('../lib/'),
spawn = require('child_process').spawn,
argv = require('optimist')
.usage('Usage: $0 --host [host] --port [num] --ffmpeg [path] --file [path] --volume [num] --password [string]')
.default('port', 5000)
.default('port', 5002)
.default('volume', 50)
.default('ffmpeg', '/usr/local/bin/ffmpeg')
.default('file', './sample.mp3')
.default('file', './wakeup.mp3')
.demand(['host'])
.argv;

Expand Down
Binary file removed examples/sample.mp3
Binary file not shown.
Binary file added examples/wakeup.mp3
Binary file not shown.
9 changes: 4 additions & 5 deletions lib/audio_out.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
var events = require('events'),
util = require('util'),
config = require('./config.js'),
nu = require('./num_util.js'),
circularBuffer = require('./circular_buffer.js');
nu = require('./num_util.js');

function AudioOut() {
events.EventEmitter.call(this);
Expand All @@ -13,7 +12,7 @@ function AudioOut() {

util.inherits(AudioOut, events.EventEmitter);

AudioOut.prototype.init = function(devices) {
AudioOut.prototype.init = function(devices, circularBuffer) {
var self = this;
config.rtp_time_ref = new Date().getTime();

Expand All @@ -34,7 +33,7 @@ AudioOut.prototype.init = function(devices) {

if(self.hasAirTunes && seq % config.sync_period == 0)
self.emit('need_sync', seq);

self.emit('packet', packet);
packet.release();
}
Expand Down Expand Up @@ -65,4 +64,4 @@ AudioOut.prototype.init = function(devices) {
syncAudio();
}

module.exports = new AudioOut();
module.exports = AudioOut;
8 changes: 4 additions & 4 deletions lib/circular_buffer.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
var Stream = require('stream'),
util = require('util'),
config = require('./config.js'),
packetPool = require('./packet_pool.js');
PacketPool = require('./packet_pool.js');

var WAITING = 0,
FILLING = 1,
Expand All @@ -13,6 +12,7 @@ var WAITING = 0,
function CircularBuffer(packetsInBuffer, packetSize) {
Stream.call(this);

this.packetPool = new PacketPool();
this.maxSize = packetsInBuffer*packetSize;
this.packetSize = packetSize;
this.writable = true;
Expand Down Expand Up @@ -51,7 +51,7 @@ CircularBuffer.prototype.write = function(chunk) {
};

CircularBuffer.prototype.readPacket = function() {
var packet = packetPool.getPacket();
var packet = this.packetPool.getPacket();

// play silence until buffer is filled enough
if(this.status !== ENDING && this.status !== ENDED &&
Expand Down Expand Up @@ -127,4 +127,4 @@ CircularBuffer.prototype.reset = function() {
this.status = WAITING;
};

module.exports = new CircularBuffer(config.packets_in_buffer, config.packet_size);
module.exports = CircularBuffer;
23 changes: 13 additions & 10 deletions lib/device_airtunes.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,25 @@ var dgram = require('dgram'),
config = require('./config.js'),
nu = require('./num_util.js'),
RTSP = require('./rtsp.js'),
udpServers = require('./udp_servers.js'),
audioOut = require('./audio_out.js'),
UdpServers = require('./udp_servers.js'),
bindings = require('../build/Release/airtunes');
udpServers = new UdpServers();

var RTP_HEADER_SIZE = 12;

function AirTunesDevice(host, options) {
function AirTunesDevice(host, audioOut, options) {
events.EventEmitter.call(this);

if(!host)
throw new Error('host is mandatory');

this.udpServers = udpServers;
this.audioOut = audioOut;
this.type = 'airtunes';
this.host = host;
this.port = options.port || 5000;
this.key = this.host + ':' + this.port;
this.rtsp = new RTSP.Client(options.volume || 50, options.password || null);
this.rtsp = new RTSP.Client(options.volume || 50, options.password || null, audioOut);
this.audioCallback = null;
this.encoder = bindings.newEncoder();
}
Expand All @@ -32,7 +34,7 @@ AirTunesDevice.prototype.start = function() {
this.audioSocket = dgram.createSocket('udp4');

// Wait until timing and control ports are chosen. We need them in RTSP handshake.
udpServers.once('ports', function(err) {
this.udpServers.once('ports', function(err) {
if(err) {
self.status = 'stopped';
self.emit('status', 'stopped');
Expand All @@ -44,7 +46,7 @@ AirTunesDevice.prototype.start = function() {
self.doHandshake();
});

udpServers.bind(this.host);
this.udpServers.bind(this.host);
};

AirTunesDevice.prototype.doHandshake = function() {
Expand All @@ -69,7 +71,7 @@ AirTunesDevice.prototype.doHandshake = function() {
self.emit(err);
});

this.rtsp.startHandshake(udpServers, this.host, this.port);
this.rtsp.startHandshake(this.udpServers, this.host, this.port);
};

AirTunesDevice.prototype.relayAudio = function() {
Expand All @@ -85,11 +87,11 @@ AirTunesDevice.prototype.relayAudio = function() {
);
};

audioOut.on('packet', this.audioCallback);
this.audioOut.on('packet', this.audioCallback);
};

AirTunesDevice.prototype.onSyncNeeded = function(seq) {
udpServers.sendControlSync(seq, this);
this.udpServers.sendControlSync(seq, this);
};

AirTunesDevice.prototype.cleanup = function() {
Expand All @@ -98,10 +100,11 @@ AirTunesDevice.prototype.cleanup = function() {
this.emit('status', 'stopped');

if(this.audioCallback) {
audioOut.removeListener('packet', this.audioCallback);
this.audioOut.removeListener('packet', this.audioCallback);
this.audioCallback = null;
}

this.udpServers.close();
this.removeAllListeners();
};

Expand Down
14 changes: 7 additions & 7 deletions lib/device_coreaudio.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
var events = require('events'),
util = require('util'),
config = require('./config.js'),
audioOut = require('./audio_out.js'),
bindings = require('../build/Release/airtunes');

function CoreAudioDevice(hasAirTunes, options) {
function CoreAudioDevice(hasAirTunes, audioOut, options) {
events.EventEmitter.call(this);

this.audioOut = audioOut;
this.type = 'coreaudio';
this.key = 'coreaudio';
this.coreAudio = null;
Expand All @@ -30,9 +30,9 @@ CoreAudioDevice.prototype.start = function(hideStatus) {

var elapsed = new Date().getTime() - config.rtp_time_ref;
var elapsedFrames = Math.floor(elapsed*config.sampling_rate/1000);
var caTimeRef = this.latency + audioOut.lastSeq*config.frames_per_packet - elapsedFrames;
var caTimeRef = this.latency + this.audioOut.lastSeq*config.frames_per_packet - elapsedFrames;
this.coreAudio = bindings.newCoreAudio();

/*
* Since the AudioQueue consumes data as fast as we send it, the internal buffer never
* has a chance to fill. We add this margin to avoid ever draining the buffer.
Expand All @@ -55,14 +55,14 @@ CoreAudioDevice.prototype.start = function(hideStatus) {
this.setVolume(this.volume);

this.status = 'ready';
if(!hideStatus)
if(!hideStatus)
this.emit('status', 'ready');

this.audioCallback = function(packet) {
bindings.enqueuePacket(self.coreAudio, packet.pcm, packet.pcm.length);
};

audioOut.on('packet', this.audioCallback);
this.audioOut.on('packet', this.audioCallback);
}

CoreAudioDevice.prototype.reportStatus = function(){
Expand Down Expand Up @@ -109,7 +109,7 @@ CoreAudioDevice.prototype.cleanup = function() {
this.started = false;

if(this.audioCallback) {
audioOut.removeListener('packet', this.audioCallback);
this.audioOut.removeListener('packet', this.audioCallback);
this.audioCallback = null;
}

Expand Down
12 changes: 6 additions & 6 deletions lib/devices.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,22 @@ var events = require('events'),
async = require('async'),
CoreAudioDevice = require('./device_coreaudio.js'),
AirTunesDevice = require('./device_airtunes.js'),
audioOut = require('./audio_out.js'),
config = require('./config.js');

function Devices() {
function Devices(audioOut) {
events.EventEmitter.call(this);

this.source = null;
this.devices = {};
this.hasAirTunes = false;
this.audioOut = audioOut;
}

util.inherits(Devices, events.EventEmitter);

Devices.prototype.init = function() {
var self = this;
audioOut.on('need_sync', function(seq) {
self.audioOut.on('need_sync', function(seq) {
// relay to all devices
self.forEach(function(dev) {
if(dev.onSyncNeeded && dev.controlPort)
Expand All @@ -41,8 +41,8 @@ Devices.prototype.add = function(type, host, options) {
options = options || {};

var dev = type === 'coreaudio' ?
new CoreAudioDevice(this.hasAirTunes, options) :
new AirTunesDevice(host, options);
new CoreAudioDevice(this.hasAirTunes, this.audioOut, options) :
new AirTunesDevice(host, this.audioOut, options);

var previousDev = this.devices[dev.key];

Expand Down Expand Up @@ -157,4 +157,4 @@ Devices.prototype.checkAirTunesDevices = function() {
this.hasAirTunes = newHasAirTunes;
};

module.exports = new Devices();
module.exports = Devices;
43 changes: 25 additions & 18 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,36 @@
var Stream = require('stream'),
util = require('util'),
devices = require('./devices.js'),
circularBuffer = require('./circular_buffer.js'),
audioOut = require('./audio_out.js');
Devices = require('./devices.js'),
config = require('./config.js'),
CircularBuffer = require('./circular_buffer.js'),
AudioOut = require('./audio_out.js');

function AirTunes() {
var self = this;

Stream.call(this);

devices.init();
devices.on('status', function(key, status, desc) {
var audioOut = new AudioOut();
this.devices = new Devices(audioOut);

this.devices.init();
this.devices.on('status', function(key, status, desc) {
self.emit('device', key, status, desc);
});

circularBuffer.on('status', function(status) {
this.circularBuffer = new CircularBuffer(config.packets_in_buffer, config.packet_size);

this.circularBuffer.on('status', function(status) {
self.emit('buffer', status);
});

audioOut.init(devices);
audioOut.init(this.devices, this.circularBuffer);

circularBuffer.on('drain', function() {
this.circularBuffer.on('drain', function() {
self.emit('drain');
});

circularBuffer.on('error', function(err) {
this.circularBuffer.on('error', function(err) {
self.emit('error', err);
});

Expand All @@ -34,39 +40,40 @@ function AirTunes() {
util.inherits(AirTunes, Stream);

AirTunes.prototype.add = function(host, options) {
return devices.add('airtunes', host, options);
return this.devices.add('airtunes', host, options);
};

AirTunes.prototype.addCoreAudio = function(options) {
return devices.add('coreaudio', null, options);
return this.devices.add('coreaudio', null, options);
};

AirTunes.prototype.stopAll = function(cb) {
devices.stopAll(cb);
this.devices.stopAll(cb);
};

AirTunes.prototype.setVolume = function(deviceKey, volume, callback) {
devices.setVolume(deviceKey, volume, callback);
this.devices.setVolume(deviceKey, volume, callback);
};

AirTunes.prototype.setTrackInfo = function(deviceKey, name, artist, album, callback) {
devices.setTrackInfo(deviceKey, name, artist, album, callback);
this.devices.setTrackInfo(deviceKey, name, artist, album, callback);
};

AirTunes.prototype.reset = function() {
circularBuffer.reset();
this.circularBuffer.reset();
};

AirTunes.prototype.setArtwork = function(deviceKey, art, contentType, callback) {
devices.setArtwork(deviceKey, art, contentType, callback);
this.devices.setArtwork(deviceKey, art, contentType, callback);
};

AirTunes.prototype.write = function(data) {
return circularBuffer.write(data);
return this.circularBuffer.write(data);
};

AirTunes.prototype.end = function() {
circularBuffer.end();
this.circularBuffer.end();
};

module.exports = new AirTunes();
module.exports.AirTunes = AirTunes;
2 changes: 1 addition & 1 deletion lib/packet_pool.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ Packet.prototype.release = function() {
}
};

module.exports = new PacketPool();
module.exports = PacketPool;
Loading