-
Notifications
You must be signed in to change notification settings - Fork 40
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
426 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
node_modules | ||
|
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
exports.Browser = require('./airplay/browser').Browser; | ||
exports.createBrowser = function() { | ||
return new exports.Browser(); | ||
}; | ||
|
||
exports.Device = require('./airplay/device').Device; | ||
exports.connectDevice = function(host, port, pass) { | ||
// TODO: connect | ||
return null; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
var events = require('events'); | ||
var mdns = require('mdns'); | ||
var util = require('util'); | ||
|
||
var Device = require('./device').Device; | ||
|
||
var Browser = function() { | ||
var self = this; | ||
|
||
this.devices_ = {}; | ||
this.nextDeviceId_ = 0; | ||
|
||
this.browser_ = mdns.createBrowser(mdns.tcp('airplay')); | ||
this.browser_.on('serviceUp', function(info, flags) { | ||
var device = self.findDeviceByInfo_(info); | ||
if (!device) { | ||
device = new Device(self.nextDeviceId_++, info); | ||
self.devices_[device.id] = device; | ||
self.emit('deviceOnline', device); | ||
} | ||
}); | ||
this.browser_.on('serviceDown', function(info, flags) { | ||
var device = self.findDeviceByInfo_(info); | ||
if (device) { | ||
device.close(); | ||
delete self.devices_[device.id]; | ||
self.emit('deviceOffline', device); | ||
} | ||
}); | ||
}; | ||
util.inherits(Browser, events.EventEmitter); | ||
exports.Browser = Browser; | ||
|
||
Browser.prototype.findDeviceByInfo_ = function(info) { | ||
for (var deviceId in this.devices_) { | ||
var device = this.devices_[deviceId]; | ||
var matching = true; | ||
for (var key in info) { | ||
if (device.info[key] != info[key]) { | ||
matching = false; | ||
break; | ||
} | ||
} | ||
if (matching) { | ||
return device; | ||
} | ||
} | ||
return null; | ||
}; | ||
|
||
Browser.prototype.getDevices = function() { | ||
var devices = []; | ||
for (var deviceId in this.devices_) { | ||
var device = this.devices_[deviceId]; | ||
if (device.isReady()) { | ||
devices.push(device); | ||
} | ||
} | ||
return devices; | ||
}; | ||
|
||
Browser.prototype.getDeviceById = function(deviceId) { | ||
var device = this.devices_[deviceId]; | ||
if (device && device.isReady()) { | ||
return device; | ||
} | ||
return null; | ||
}; | ||
|
||
Browser.prototype.start = function() { | ||
this.browser_.start(); | ||
}; | ||
|
||
Browser.prototype.stop = function() { | ||
this.browser_.stop(); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
var buffer = require('buffer'); | ||
var events = require('events'); | ||
var net = require('net'); | ||
var util = require('util'); | ||
|
||
var Client = function(host, port, user, pass, callback) { | ||
var self = this; | ||
|
||
this.host_ = host; | ||
this.port_ = port; | ||
this.user_ = user; | ||
this.pass_ = pass; | ||
|
||
this.responseWaiters_ = []; | ||
|
||
this.socket_ = net.createConnection(port, host); | ||
this.socket_.on('connect', function() { | ||
self.responseWaiters_.push({ | ||
callback: callback | ||
}); | ||
self.socket_.write( | ||
'GET /playback-info HTTP/1.1\n' + | ||
'User-Agent: MediaControl/1.0\n' + | ||
'Content-Length: 0\n' + | ||
'\n'); | ||
}); | ||
|
||
this.socket_.on('data', function(data) { | ||
var res = self.parseResponse_(data.toString()); | ||
//util.puts(util.inspect(res)); | ||
|
||
var waiter = self.responseWaiters_.shift(); | ||
if (waiter.callback) { | ||
waiter.callback(res); | ||
} | ||
}); | ||
}; | ||
util.inherits(Client, events.EventEmitter); | ||
exports.Client = Client; | ||
|
||
Client.prototype.close = function() { | ||
if (this.socket_) { | ||
this.socket_.close(); | ||
} | ||
this.socket_ = null; | ||
}; | ||
|
||
Client.prototype.parseResponse_ = function(res) { | ||
// Look for HTTP response: | ||
// HTTP/1.1 200 OK | ||
// Some-Header: value | ||
// Content-Length: 427 | ||
// \n | ||
// body (427 bytes) | ||
|
||
var header = res; | ||
var body = ''; | ||
var splitPoint = res.indexOf('\r\n\r\n'); | ||
if (splitPoint != -1) { | ||
header = res.substr(0, splitPoint); | ||
body = res.substr(splitPoint + 4); | ||
} | ||
|
||
// Normalize header \r\n -> \n | ||
header = header.replace(/\r\n/g, '\n'); | ||
|
||
// Peel off status | ||
var status = header.substr(0, header.indexOf('\n')); | ||
var statusMatch = status.match(/HTTP\/1.1 ([0-9]+) (.+)/); | ||
header = header.substr(status.length + 1); | ||
|
||
// Parse headers | ||
var allHeaders = {}; | ||
var headerLines = header.split('\n'); | ||
for (var n = 0; n < headerLines.length; n++) { | ||
var headerLine = headerLines[n]; | ||
var key = headerLine.substr(0, headerLine.indexOf(':')); | ||
var value = headerLine.substr(key.length + 2); | ||
allHeaders[key] = value; | ||
} | ||
|
||
// Trim body? | ||
return { | ||
statusCode: parseInt(statusMatch[1]), | ||
statusReason: statusMatch[2], | ||
headers: allHeaders, | ||
body: body | ||
}; | ||
}; | ||
|
||
Client.prototype.issue_ = function(req, body, callback) { | ||
if (!this.socket_) { | ||
util.puts('client not connected'); | ||
return; | ||
} | ||
|
||
req.headers = req.headers || {}; | ||
req.headers['User-Agent'] = 'MediaControl/1.0'; | ||
req.headers['Content-Length'] = body ? buffer.Buffer.byteLength(body) : 0; | ||
req.headers['Connection'] = 'keep-alive'; | ||
|
||
var allHeaders = ''; | ||
for (var key in req.headers) { | ||
allHeaders += key + ': ' + req.headers[key] + '\n'; | ||
} | ||
|
||
var text = | ||
req.method + ' ' + req.path + ' HTTP/1.1\n' + | ||
allHeaders + '\n'; | ||
if (body) { | ||
text += body; | ||
} | ||
|
||
this.responseWaiters_.push({ | ||
callback: callback | ||
}); | ||
this.socket_.write(text); | ||
}; | ||
|
||
Client.prototype.get = function(path, callback) { | ||
var req = { | ||
method: 'GET', | ||
path: path, | ||
}; | ||
this.issue_(req, null, callback); | ||
}; | ||
|
||
Client.prototype.post = function(path, body, callback) { | ||
var req = { | ||
method: 'POST', | ||
path: path, | ||
}; | ||
this.issue_(req, body, callback); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
var events = require('events'); | ||
var plist = require('plist'); | ||
var util = require('util'); | ||
|
||
var Client = require('./client').Client; | ||
|
||
var Device = function(id, info, opt_readyCallback) { | ||
var self = this; | ||
|
||
this.id = id; | ||
this.info_ = info; | ||
this.serverInfo_ = null; | ||
this.ready_ = false; | ||
|
||
var host = info.host; | ||
var port = info.port; | ||
var user = 'Airplay'; | ||
var pass = ''; | ||
this.client_ = new Client(host, port, user, pass, function() { | ||
// TODO: support passwords | ||
|
||
self.client_.get('/server-info', function(res) { | ||
plist.parseString(res.body, function(err, obj) { | ||
var el = obj[0]; | ||
self.serverInfo_ = { | ||
deviceId: el.deviceid, | ||
features: el.features, | ||
model: el.model, | ||
protocolVersion: el.protovers, | ||
sourceVersion: el.srcvers | ||
}; | ||
}); | ||
|
||
self.makeReady_(opt_readyCallback); | ||
}); | ||
}); | ||
}; | ||
util.inherits(Device, events.EventEmitter); | ||
exports.Device = Device; | ||
|
||
Device.prototype.isReady = function() { | ||
return this.ready_; | ||
}; | ||
|
||
Device.prototype.makeReady_ = function(opt_readyCallback) { | ||
this.ready_ = true; | ||
if (opt_readyCallback) { | ||
opt_readyCallback(this); | ||
} | ||
}; | ||
|
||
Device.prototype.close = function() { | ||
if (this.client_) { | ||
this.client_.close(); | ||
} | ||
this.client_ = null; | ||
this.ready_ = false; | ||
|
||
this.emit('close'); | ||
}; | ||
|
||
Device.prototype.getInfo = function() { | ||
var info = this.info_; | ||
var serverInfo = this.serverInfo_; | ||
return { | ||
id: this.id, | ||
name: info.serviceName, | ||
deviceId: info.host, | ||
features: serverInfo.features, | ||
model: serverInfo.model, | ||
slideshowFeatures: [], | ||
supportedContentTypes: [] | ||
}; | ||
}; | ||
|
||
Device.prototype.default = function(callback) { | ||
if (callback) { | ||
callback(this.getInfo()); | ||
} | ||
}; | ||
|
||
Device.prototype.status = function(callback) { | ||
this.client_.get('/playback-info', function(res) { | ||
if (res) { | ||
plist.parseString(res.body, function(err, obj) { | ||
var el = obj[0]; | ||
var result = { | ||
duration: el.duration, | ||
position: el.position, | ||
rate: el.rate, | ||
playbackBufferEmpty: el.playbackBufferEmpty, | ||
playbackBufferFull: el.playbackBufferFull, | ||
playbackLikelyToKeepUp: el.playbackLikelyToKeepUp, | ||
readyToPlay: el.readyToPlay, | ||
loadedTimeRanges: el.loadedTimeRanges, | ||
seekableTimeRanges: el.seekableTimeRanges | ||
}; | ||
if (callback) { | ||
callback(result); | ||
} | ||
}); | ||
} else { | ||
if (callback) { | ||
callback(null); | ||
} | ||
} | ||
}); | ||
}; | ||
|
||
Device.prototype.authorize = function(req, callback) { | ||
// TODO: implement authorize | ||
if (callback) { | ||
callback(null); | ||
} | ||
}; | ||
|
||
Device.prototype.play = function(content, start, callback) { | ||
var body = | ||
'Content-Location: ' + content + '\n' + | ||
'Start-Position: ' + start + '\n'; | ||
this.client_.post('/play', body, function(res) { | ||
if (callback) { | ||
callback(res ? {} : null); | ||
} | ||
}); | ||
}; | ||
|
||
Device.prototype.stop = function(callback) { | ||
this.client_.post('/stop', null, function(res) { | ||
if (callback) { | ||
callback(res ? {} : null); | ||
} | ||
}); | ||
}; | ||
|
||
Device.prototype.scrub = function(position, callback) { | ||
this.client_.post('/scrub?position=' + position, null, function(res) { | ||
if (callback) { | ||
callback(res ? {} : null); | ||
} | ||
}); | ||
}; | ||
|
||
Device.prototype.reverse = function(callback) { | ||
this.client_.post('/reverse', null, function(res) { | ||
if (callback) { | ||
callback(res ? {} : null); | ||
} | ||
}) | ||
}; | ||
|
||
Device.prototype.rate = function(value, callback) { | ||
this.client_.post('/rate?value=' + value, null, function(res) { | ||
if (callback) { | ||
callback(res ? {} : null); | ||
} | ||
}) | ||
}; | ||
|
||
Device.prototype.volume = function(value, callback) { | ||
this.client_.post('/volume?value=' + value, null, function(res) { | ||
if (callback) { | ||
callback(res ? {} : null); | ||
} | ||
}) | ||
}; | ||
|
||
Device.prototype.photo = function(req, callback) { | ||
// TODO: implement photo | ||
if (callback) { | ||
callback(null); | ||
} | ||
}; |
Oops, something went wrong.