Comparing version
@@ -15,6 +15,6 @@ "use strict"; | ||
var privkey = ursa.createPrivateKey(fs.readFileSync('private.key')); | ||
var response = privkey.publicDecrypt(data, 'base64', 'hex'); | ||
var privkey = ursa.createPrivateKey(fs.readFileSync('private.key')); | ||
var response = privkey.publicDecrypt(data, 'base64', 'hex'); | ||
console.log(response); | ||
console.log(response); | ||
}); |
@@ -6,14 +6,9 @@ "use strict"; | ||
var server = new AirTunesServer(new Speaker({ | ||
var speaker = new Speaker({ | ||
channels: 2, | ||
bitDepth: 16, | ||
sampleRate: 44100 | ||
}), { | ||
}); | ||
var server = new AirTunesServer(speaker); | ||
server.start(); | ||
server.on('volumeChange', function(vol) { | ||
console.log(vol); | ||
}); | ||
server.start(); |
@@ -6,33 +6,34 @@ "use strict"; | ||
var audioProcessor = function(rtspServer) { | ||
var self = this; | ||
var self = this; | ||
var state = 'buffering'; | ||
var state = 'buffering'; | ||
var bufferQueue = new PriorityQueue(function(a, b) { | ||
return b.sequenceNumber - a.sequenceNumber; | ||
}); | ||
var bufferQueue = new PriorityQueue(function(a, b) { | ||
return b.sequenceNumber - a.sequenceNumber; | ||
}); | ||
self.process = function(audio, sequenceNumber) { | ||
var swapBuf = new Buffer(audio.length); | ||
self.process = function(audio, sequenceNumber) { | ||
var swapBuf = new Buffer(audio.length); | ||
for (var i = 0; i < audio.length; i += 2) { | ||
swapBuf[i] = audio[i + 1]; | ||
swapBuf[i + 1] = audio[i]; | ||
} | ||
// endian hack | ||
for (var i = 0; i < audio.length; i += 2) { | ||
swapBuf[i] = audio[i + 1]; | ||
swapBuf[i + 1] = audio[i]; | ||
} | ||
if (bufferQueue.length < 5) { | ||
state = 'buffering'; | ||
} | ||
if (bufferQueue.length < 5) { | ||
state = 'buffering'; | ||
} | ||
bufferQueue.enq({ buffer: swapBuf, sequenceNumber: sequenceNumber }); | ||
bufferQueue.enq({ buffer: swapBuf, sequenceNumber: sequenceNumber }); | ||
if (state == 'active') { | ||
while (bufferQueue.size() >= 5) { | ||
rtspServer.options.outStream.write(bufferQueue.deq().buffer); | ||
} | ||
} else if (bufferQueue.size() >= 100) { | ||
state = 'active'; | ||
} | ||
if (state == 'active') { | ||
while (bufferQueue.size() >= 5) { | ||
rtspServer.options.outStream.write(bufferQueue.deq().buffer); | ||
} | ||
} else if (bufferQueue.size() >= 100) { | ||
state = 'active'; | ||
} | ||
}; | ||
}; | ||
@@ -39,0 +40,0 @@ }; |
@@ -7,46 +7,45 @@ "use strict"; | ||
var rtpServer = function(rtspServer) { | ||
var self = this; | ||
var crypto = require('crypto'); | ||
var RtpServer = function(rtspServer) { | ||
var self = this; | ||
var crypto = require('crypto'); | ||
self.start = function() { | ||
self.baseServer = dgram.createSocket('udp4'); | ||
self.controlServer = dgram.createSocket('udp4'); | ||
self.timingServer = dgram.createSocket('udp4'); | ||
RtpServer.prototype.start = function() { | ||
self.baseServer = dgram.createSocket('udp4'); | ||
self.controlServer = dgram.createSocket('udp4'); | ||
self.timingServer = dgram.createSocket('udp4'); | ||
self.baseServer.bind(rtspServer.ports[0]); | ||
self.controlServer.bind(rtspServer.ports[1]); | ||
self.timingServer.bind(rtspServer.ports[2]); | ||
self.baseServer.bind(rtspServer.ports[0]); | ||
self.controlServer.bind(rtspServer.ports[1]); | ||
self.timingServer.bind(rtspServer.ports[2]); | ||
self.baseServer.on('message', function(msg) { | ||
var meta = msg.slice(0, 12); | ||
var sequenceNumber = meta.slice(2, 4).readUInt16BE(0); | ||
self.baseServer.on('message', function(msg) { | ||
var meta = msg.slice(0, 12); | ||
var sequenceNumber = meta.slice(2, 4).readUInt16BE(0); | ||
var encryptedAudio = msg.slice(12); | ||
var encryptedAudio = msg.slice(12); | ||
var decipher = crypto.createDecipheriv('aes-128-cbc', rtspServer.audioAesKey, rtspServer.audioAesIv); | ||
decipher.setAutoPadding(false); | ||
var decipher = crypto.createDecipheriv('aes-128-cbc', rtspServer.audioAesKey, rtspServer.audioAesIv); | ||
decipher.setAutoPadding(false); | ||
var audio = decipher.update(encryptedAudio); | ||
var audio = decipher.update(encryptedAudio); | ||
rtspServer.audioProcessor.process(audio, sequenceNumber); | ||
rtspServer.audioProcessor.process(audio, sequenceNumber); | ||
}); | ||
}); | ||
self.controlServer.on('message', function(msg) { | ||
var timestamp = msg.readUInt32BE(4); | ||
}); | ||
self.controlServer.on('message', function(msg) { | ||
var timestamp = msg.readUInt32BE(4); | ||
}); | ||
self.timingServer.on('message', function(msg) { | ||
//console.log(msg.length + ' BYTES SENT TO TIMING PORT'); | ||
}); | ||
}; | ||
self.timingServer.on('message', function(msg) { | ||
//console.log(msg.length + ' BYTES SENT TO TIMING PORT'); | ||
}); | ||
}; | ||
self.stop = function() { | ||
self.baseServer.close(); | ||
self.controlServer.close(); | ||
self.timingServer.close(); | ||
} | ||
RtpServer.prototype.stop = function() { | ||
self.baseServer.close(); | ||
self.controlServer.close(); | ||
self.timingServer.close(); | ||
} | ||
} | ||
module.exports = rtpServer; | ||
module.exports = RtpServer; |
@@ -9,33 +9,41 @@ "use strict"; | ||
var server = function(options, external) { | ||
var self = this; | ||
var Server = function(options, external) { | ||
var self = this; | ||
self.external = external; | ||
self.options = options; | ||
self.external = external; | ||
self.options = options; | ||
self.ports = []; | ||
self.ports = []; | ||
self.rtp = new RtpServer(self); | ||
self.audioProcessor = new AudioProcessor(self); | ||
self.macAddress = '5F513885F785'; | ||
self.metadata = {}; | ||
// pull method processors | ||
var methodMapping = require('./rtspmethods')(self); | ||
self.rtp = new RtpServer(self); | ||
self.audioProcessor = new AudioProcessor(self); | ||
self.macAddress = options.macAddress; | ||
self.metadata = {}; | ||
var handler = function(socket) { | ||
self.clientConnected = false; | ||
socket.id = new Date(); | ||
self.external.emit('clientConnected'); | ||
// pull method processors | ||
var methodMapping = require('./rtspmethods')(self); | ||
var parser = new Parser(socket); | ||
parser.on('message', function(m) { | ||
var response = new tools.MessageBuilder(socket); | ||
methodMapping[m.method](response, m.headers, m.content); | ||
}); | ||
Server.prototype.connectHandler = function(socket) { | ||
}; | ||
socket.id = new Date(); | ||
self.external.emit('clientConnected'); | ||
self.handler = handler; | ||
var parser = new Parser(socket); | ||
parser.on('message', function(m) { | ||
var response = new tools.MessageBuilder(socket); | ||
methodMapping[m.method](response, m.headers, m.content); | ||
}); | ||
socket.on('close', self.disconnectHandler); | ||
}; | ||
Server.prototype.disconnectHandler = function() { | ||
self.clientConnected = false; | ||
self.external.emit('clientDisconnected'); | ||
}; | ||
}; | ||
module.exports = server; | ||
module.exports = Server; |
@@ -8,4 +8,5 @@ "use strict"; | ||
var statusMessages = { | ||
200: "OK", | ||
401: "Unauthorized" | ||
200: 'OK', | ||
401: 'Unauthorized', | ||
453: 'Not Enough Bandwidth' | ||
}; | ||
@@ -15,92 +16,92 @@ | ||
var buffer = ''; | ||
var socket = socket; | ||
var self = this; | ||
var buffer = ''; | ||
var socket = socket; | ||
var self = this; | ||
MessageBuilder.prototype.addHeader = function(header, data) { | ||
buffer += header + ": " + data + "\r\n"; | ||
}; | ||
MessageBuilder.prototype.addHeader = function(header, data) { | ||
buffer += header + ": " + data + "\r\n"; | ||
}; | ||
MessageBuilder.prototype.setStatus = function(statusCode, cseq) { | ||
buffer += "RTSP/1.0 " + statusCode + " " + statusMessages[statusCode] + '\r\n'; | ||
self.addHeader('Server', 'AirTunes/105.1'); | ||
self.addHeader('CSeq', cseq); | ||
} | ||
MessageBuilder.prototype.setStatus = function(statusCode, cseq) { | ||
buffer += "RTSP/1.0 " + statusCode + " " + statusMessages[statusCode] + '\r\n'; | ||
self.addHeader('Server', 'AirTunes/105.1'); | ||
self.addHeader('CSeq', cseq); | ||
} | ||
MessageBuilder.prototype.setOK = function(cseq) { | ||
self.setStatus(200, cseq); | ||
}; | ||
MessageBuilder.prototype.setOK = function(cseq) { | ||
self.setStatus(200, cseq); | ||
}; | ||
MessageBuilder.prototype.send = function() { | ||
//console.log('SENDING DATA @ ' + socket.id.getTime()); | ||
//console.log('`--: ' + buffer.replace(/\r\n/g, '\r\n`--: ')); | ||
//console.log('END SEND\n') | ||
socket.write(buffer + '\r\n'); | ||
}; | ||
MessageBuilder.prototype.send = function() { | ||
//console.log('SENDING DATA @ ' + socket.id.getTime()); | ||
//console.log('`--: ' + buffer.replace(/\r\n/g, '\r\n`--: ')); | ||
//console.log('END SEND\n') | ||
socket.write(buffer + '\r\n'); | ||
}; | ||
MessageBuilder.prototype.sendError = function(err) { | ||
//console.log('SENDING ERROR ' + err + ': ' + errorList[err]); | ||
socket.end("RTSP/1.0 " + err + ' ' + statusMessages[err] + '\r\n'); | ||
}; | ||
MessageBuilder.prototype.sendError = function(err) { | ||
//console.log('SENDING ERROR ' + err + ': ' + errorList[err]); | ||
socket.end("RTSP/1.0 " + err + ' ' + statusMessages[err] + '\r\n'); | ||
}; | ||
}; | ||
var parseSdp = function(msg) { | ||
var multi = [ 'a', 'p', 'b' ]; | ||
var multi = [ 'a', 'p', 'b' ]; | ||
var lines = msg.split('\r\n'); | ||
var output = {}; | ||
for (var i = 0; i < lines.length; i++) { | ||
var lines = msg.split('\r\n'); | ||
var output = {}; | ||
for (var i = 0; i < lines.length; i++) { | ||
var sp = lines[i].split(/=(.+)?/); | ||
if (sp.length == 3) { // for some reason there's an empty item? | ||
if (multi.indexOf(sp[0]) != -1) { // some attributes are multiline... | ||
if (!output[sp[0]]) | ||
output[sp[0]] = new Array(); | ||
var sp = lines[i].split(/=(.+)?/); | ||
if (sp.length == 3) { // for some reason there's an empty item? | ||
if (multi.indexOf(sp[0]) != -1) { // some attributes are multiline... | ||
if (!output[sp[0]]) | ||
output[sp[0]] = new Array(); | ||
output[sp[0]].push(sp[1]); | ||
} else { | ||
output[sp[0]] = sp[1]; | ||
} | ||
} | ||
} | ||
return output; | ||
output[sp[0]].push(sp[1]); | ||
} else { | ||
output[sp[0]] = sp[1]; | ||
} | ||
} | ||
} | ||
return output; | ||
}; | ||
var dmapTypes = { | ||
mper: 8, | ||
asal: 'str', | ||
asar: 'str', | ||
ascp: 'str', | ||
asgn: 'str', | ||
minm: 'str', | ||
astn: 2, | ||
asdk: 1, | ||
caps: 1, | ||
astm: 4 | ||
mper: 8, | ||
asal: 'str', | ||
asar: 'str', | ||
ascp: 'str', | ||
asgn: 'str', | ||
minm: 'str', | ||
astn: 2, | ||
asdk: 1, | ||
caps: 1, | ||
astm: 4 | ||
}; | ||
var parseDmap = function(buffer) { | ||
var output = {}; | ||
var output = {}; | ||
for (var i = 8; i < buffer.length;) { | ||
var itemType = buffer.slice(i, i + 4); | ||
var itemLength = buffer.slice(i + 4, i + 8).readUInt32BE(0); | ||
if (itemLength != 0) { | ||
var data = buffer.slice(i + 8, i + 8 + itemLength); | ||
if (dmapTypes[itemType] == 'str') { | ||
output[itemType.toString()] = data.toString(); | ||
} else if (dmapTypes[itemType] == 1) { | ||
output[itemType.toString()] = data.readUInt8(0); | ||
} else if (dmapTypes[itemType] == 2) { | ||
output[itemType.toString()] = data.readUInt16BE(0); | ||
} else if (dmapTypes[itemType] == 4) { | ||
output[itemType.toString()] = data.readUInt32BE(0); | ||
} else if (dmapTypes[itemType] == 8) { | ||
output[itemType.toString()] = (data.readUInt32BE(0) << 8) + data.readUInt32BE(4); | ||
} | ||
} | ||
i += 8 + itemLength; | ||
} | ||
for (var i = 8; i < buffer.length;) { | ||
var itemType = buffer.slice(i, i + 4); | ||
var itemLength = buffer.slice(i + 4, i + 8).readUInt32BE(0); | ||
if (itemLength != 0) { | ||
var data = buffer.slice(i + 8, i + 8 + itemLength); | ||
if (dmapTypes[itemType] == 'str') { | ||
output[itemType.toString()] = data.toString(); | ||
} else if (dmapTypes[itemType] == 1) { | ||
output[itemType.toString()] = data.readUInt8(0); | ||
} else if (dmapTypes[itemType] == 2) { | ||
output[itemType.toString()] = data.readUInt16BE(0); | ||
} else if (dmapTypes[itemType] == 4) { | ||
output[itemType.toString()] = data.readUInt32BE(0); | ||
} else if (dmapTypes[itemType] == 8) { | ||
output[itemType.toString()] = (data.readUInt32BE(0) << 8) + data.readUInt32BE(4); | ||
} | ||
} | ||
i += 8 + itemLength; | ||
} | ||
return output; | ||
return output; | ||
} | ||
@@ -111,14 +112,14 @@ | ||
var generateAppleResponse = function(challengeBuf, ipAddr, macAddr) { | ||
var fullChallenge = Buffer.concat([ challengeBuf, ipAddr, macAddr ]); | ||
var fullChallenge = Buffer.concat([ challengeBuf, ipAddr, macAddr ]); | ||
// im sure there's an easier way to pad this buffer | ||
var padding = new Array(); | ||
for (var i = fullChallenge.length; i < 32; i++) { | ||
padding.push(0); | ||
} | ||
fullChallenge = Buffer.concat([ fullChallenge, new Buffer(padding) ]); | ||
// im sure there's an easier way to pad this buffer | ||
var padding = new Array(); | ||
for (var i = fullChallenge.length; i < 32; i++) { | ||
padding.push(0); | ||
} | ||
fullChallenge = Buffer.concat([ fullChallenge, new Buffer(padding) ]); | ||
var response = privkey.privateEncrypt(fullChallenge, 'base64', 'base64'); | ||
var response = privkey.privateEncrypt(fullChallenge, 'base64', 'base64'); | ||
return response; | ||
return response; | ||
}; | ||
@@ -128,7 +129,7 @@ | ||
var ha1 = crypto.createHash('md5').update(username + ':' + realm + ':' + password).digest().toString('hex'); | ||
var ha2 = crypto.createHash('md5').update(method + ':' + uri).digest().toString('hex'); | ||
var response = crypto.createHash('md5').update(ha1 + ':' + nonce + ':' + ha2).digest().toString('hex'); | ||
var ha1 = crypto.createHash('md5').update(username + ':' + realm + ':' + password).digest().toString('hex'); | ||
var ha2 = crypto.createHash('md5').update(method + ':' + uri).digest().toString('hex'); | ||
var response = crypto.createHash('md5').update(ha1 + ':' + nonce + ':' + ha2).digest().toString('hex'); | ||
return response; | ||
return response; | ||
} | ||
@@ -135,0 +136,0 @@ |
@@ -11,180 +11,187 @@ "use strict"; | ||
var rtspServer = rtspServer; | ||
var rtspServer = rtspServer; | ||
var nonce = ''; | ||
var nonce = ''; | ||
var options = function(response, headers) { | ||
var options = function(response, headers) { | ||
response.setOK(headers['CSeq']); | ||
response.addHeader('Public', 'ANNOUNCE, SETUP, RECORD, PAUSE, FLUSH, TEARDOWN, OPTIONS, GET_PARAMETER, SET_PARAMETER, POST, GET'); | ||
response.setOK(headers['CSeq']); | ||
response.addHeader('Public', 'ANNOUNCE, SETUP, RECORD, PAUSE, FLUSH, TEARDOWN, OPTIONS, GET_PARAMETER, SET_PARAMETER, POST, GET'); | ||
if (headers.hasOwnProperty('Apple-Challenge')) { | ||
if (headers.hasOwnProperty('Apple-Challenge')) { | ||
// challenge response consists of challenge + ip address + mac address + padding to 32 bytes, | ||
// encrypted with the ApEx private key (private encryption mode w/ PKCS1 padding) | ||
// challenge response consists of challenge + ip address + mac address + padding to 32 bytes, | ||
// encrypted with the ApEx private key (private encryption mode w/ PKCS1 padding) | ||
var challengeBuf = new Buffer(headers['Apple-Challenge'], 'base64'); | ||
var ipAddr = new Buffer(ip.toBuffer(ip.address()), 'hex'); | ||
var macAddr = new Buffer(rtspServer.macAddress.replace(/:/g, ''), 'hex'); | ||
response.addHeader('Apple-Response', tools.generateAppleResponse(challengeBuf, ipAddr, macAddr)); | ||
} | ||
var challengeBuf = new Buffer(headers['Apple-Challenge'], 'base64'); | ||
var ipAddr = new Buffer(ip.toBuffer(ip.address()), 'hex'); | ||
var macAddr = new Buffer(rtspServer.macAddress.replace(/:/g, ''), 'hex'); | ||
response.addHeader('Apple-Response', tools.generateAppleResponse(challengeBuf, ipAddr, macAddr)); | ||
} | ||
response.send(); | ||
}; | ||
response.send(); | ||
}; | ||
var announce = function(response, headers, content) { | ||
if (rtspServer.options.password && !headers['Authorization']) { | ||
var announce = function(response, headers, content) { | ||
if (rtspServer.clientConnected) { | ||
response.setStatus(453); | ||
response.send(); | ||
} else if (rtspServer.options.password && !headers['Authorization']) { | ||
var md5sum = crypto.createHash('md5'); | ||
md5sum.update = randomstring.generate(); | ||
response.setStatus(401, headers['CSeq']); | ||
nonce = md5sum.digest('hex').toString('hex'); | ||
var md5sum = crypto.createHash('md5'); | ||
md5sum.update = randomstring.generate(); | ||
response.setStatus(401, headers['CSeq']); | ||
nonce = md5sum.digest('hex').toString('hex'); | ||
response.addHeader('WWW-Authenticate', 'Digest realm="roap", nonce="' + nonce + '"'); | ||
response.send(); | ||
response.addHeader('WWW-Authenticate', 'Digest realm="roap", nonce="' + nonce + '"'); | ||
response.send(); | ||
} else if (rtspServer.options.password && headers['Authorization']) { | ||
} else if (rtspServer.options.password && headers['Authorization']) { | ||
var auth = headers['Authorization']; | ||
var auth = headers['Authorization']; | ||
var params = auth.split(/, /g); | ||
var map = {}; | ||
params.forEach(function(param) { | ||
var pair = param.replace(/["]/g, '').split('='); | ||
map[pair[0]] = pair[1]; | ||
}); | ||
var params = auth.split(/, /g); | ||
var map = {}; | ||
params.forEach(function(param) { | ||
var pair = param.replace(/["]/g, '').split('='); | ||
map[pair[0]] = pair[1]; | ||
}); | ||
var expectedResponse = tools.generateRfc2617Response('iTunes', 'roap', rtspServer.options.password, nonce, map['uri'], 'ANNOUNCE'); | ||
var receivedResponse = map['response']; | ||
var expectedResponse = tools.generateRfc2617Response('iTunes', 'roap', rtspServer.options.password, nonce, map['uri'], 'ANNOUNCE'); | ||
var receivedResponse = map['response']; | ||
if (expectedResponse == receivedResponse) { | ||
announceParse(response, headers, content); | ||
} else { | ||
response.sendError(401); | ||
} | ||
if (expectedResponse == receivedResponse) { | ||
announceParse(response, headers, content); | ||
} else { | ||
response.sendError(401); | ||
} | ||
} else { | ||
announceParse(response, headers, content); | ||
} | ||
}; | ||
var announceParse = function(response, headers, content) { | ||
var sdp = tools.parseSdp(content.toString()); | ||
for (var i = 0; i < sdp.a.length; i++) { | ||
var sp = sdp.a[i].split(':'); | ||
if (sp.length == 2) { | ||
if (sp[0] == 'rsaaeskey') { | ||
rtspServer.audioAesKey = tools.rsaOperations.decrypt(new Buffer(sp[1], 'base64')); | ||
} else if (sp[0] == 'aesiv') { | ||
rtspServer.audioAesIv = new Buffer(sp[1], 'base64'); | ||
} else if (sp[0] == 'rtpmap') { | ||
rtspServer.audioCodec = sp[1]; | ||
} else if (sp[0] == 'fmtp') { | ||
rtspServer.audioOptions = sp[1].split(' '); | ||
} | ||
} | ||
} | ||
rtspServer.clientName = sdp.i; | ||
} else { | ||
announceParse(response, headers, content); | ||
} | ||
}; | ||
response.setOK(headers['CSeq']); | ||
response.send(); | ||
}; | ||
var announceParse = function(response, headers, content) { | ||
var setup = function(response, headers) { | ||
rtspServer.ports = []; | ||
rtspServer.clientConnected = true; | ||
portastic.find({ | ||
min : 50000, | ||
max : 50020, | ||
retrieve: 3 | ||
}, function(err, port){ | ||
if (err) throw err; | ||
var sdp = tools.parseSdp(content.toString()); | ||
for (var i = 0; i < sdp.a.length; i++) { | ||
var sp = sdp.a[i].split(':'); | ||
if (sp.length == 2) { | ||
if (sp[0] == 'rsaaeskey') { | ||
rtspServer.audioAesKey = tools.rsaOperations.decrypt(new Buffer(sp[1], 'base64')); | ||
} else if (sp[0] == 'aesiv') { | ||
rtspServer.audioAesIv = new Buffer(sp[1], 'base64'); | ||
} else if (sp[0] == 'rtpmap') { | ||
rtspServer.audioCodec = sp[1]; | ||
} else if (sp[0] == 'fmtp') { | ||
rtspServer.audioOptions = sp[1].split(' '); | ||
} | ||
} | ||
} | ||
rtspServer.clientName = sdp.i; | ||
rtspServer.ports = port; | ||
response.setOK(headers['CSeq']); | ||
response.send(); | ||
}; | ||
if (rtspServer.ports.length >= 3) { | ||
var setup = function(response, headers) { | ||
rtspServer.ports = []; | ||
rtspServer.rtp.start(); | ||
portastic.find({ | ||
min : 50000, | ||
max : 50020, | ||
retrieve: 3 | ||
}, function(err, port){ | ||
if (err) throw err; | ||
response.setOK(headers['CSeq']); | ||
response.addHeader('Transport', 'RTP/AVP/UDP;unicast;mode=record;server_port=' + rtspServer.ports[0] + ';control_port=' + rtspServer.ports[1] + ';timing_port=' + rtspServer.ports[2]); | ||
response.addHeader('Session', '1'); | ||
response.addHeader('Audio-Jack-Status', 'connected'); | ||
response.send(); | ||
rtspServer.ports = port; | ||
} | ||
}); | ||
}; | ||
if (rtspServer.ports.length >= 3) { | ||
var record = function(response, headers) { | ||
response.setOK(headers['CSeq']); | ||
if (!headers['RTP-Info']) { | ||
// it seems like iOS airplay does something else | ||
} else { | ||
var rtpInfo = headers['RTP-Info'].split(';'); | ||
var initSeq = rtpInfo[0].split('=')[1]; | ||
var initRtpTime = rtpInfo[1].split('=')[1]; | ||
if (!initSeq || !initRtpTime) { | ||
response.sendError(400); | ||
} else { | ||
response.addHeader('Audio-Latency', '1000'); | ||
} | ||
} | ||
response.send(); | ||
}; | ||
rtspServer.rtp.start(); | ||
var flush = function(response, headers) { | ||
response.setOK(headers['CSeq']); | ||
response.addHeader('RTP-Info', 'rtptime=1147914212'); | ||
response.send(); | ||
}; | ||
response.setOK(headers['CSeq']); | ||
response.addHeader('Transport', 'RTP/AVP/UDP;unicast;mode=record;server_port=' + rtspServer.ports[0] + ';control_port=' + rtspServer.ports[1] + ';timing_port=' + rtspServer.ports[2]); | ||
response.addHeader('Session', '1'); | ||
response.addHeader('Audio-Jack-Status', 'connected'); | ||
response.send(); | ||
var teardown = function(response, headers) { | ||
rtspServer.rtp.stop(); | ||
response.setOK(headers['CSeq']); | ||
response.send(); | ||
}; | ||
} | ||
}); | ||
}; | ||
var setParameter = function(response, headers, content) { | ||
if (headers['Content-Type'] == 'application/x-dmap-tagged') { | ||
// metadata dmap/daap format | ||
var record = function(response, headers) { | ||
response.setOK(headers['CSeq']); | ||
if (!headers['RTP-Info']) { | ||
// it seems like iOS airplay does something else | ||
} else { | ||
var rtpInfo = headers['RTP-Info'].split(';'); | ||
var initSeq = rtpInfo[0].split('=')[1]; | ||
var initRtpTime = rtpInfo[1].split('=')[1]; | ||
if (!initSeq || !initRtpTime) { | ||
response.sendError(400); | ||
} else { | ||
response.addHeader('Audio-Latency', '1000'); | ||
} | ||
} | ||
response.send(); | ||
}; | ||
var dmapData = tools.parseDmap(content); | ||
rtspServer.metadata = dmapData; | ||
rtspServer.external.emit('metadataChange', rtspServer.metadata); | ||
} else if (headers['Content-Type'] == 'image/jpeg') { | ||
rtspServer.metadata.artwork = content; | ||
rtspServer.external.emit('artworkChange', content); | ||
} else if (headers['Content-Type'] == 'text/parameters') { | ||
var data = content.toString().split(': '); | ||
rtspServer.metadata = rtspServer.metadata || {}; | ||
var flush = function(response, headers) { | ||
response.setOK(headers['CSeq']); | ||
response.addHeader('RTP-Info', 'rtptime=1147914212'); | ||
response.send(); | ||
}; | ||
if (data[0] == 'volume') { | ||
rtspServer.metadata['volume'] = parseFloat(data[1]); | ||
rtspServer.external.emit('volumeChange', rtspServer.metadata['volume']); | ||
} else if (data[0] == 'progress') { | ||
rtspServer.metadata['progress'] = data[1]; | ||
rtspServer.external.emit('progressChange', rtspServer.metadata['progress']); | ||
} | ||
} else { | ||
var teardown = function(response, headers) { | ||
rtspServer.rtp.stop(); | ||
response.setOK(headers['CSeq']); | ||
response.send(); | ||
}; | ||
} | ||
response.setOK(headers['CSeq']); | ||
response.send(); | ||
}; | ||
var setParameter = function(response, headers, content) { | ||
if (headers['Content-Type'] == 'application/x-dmap-tagged') { | ||
// metadata dmap/daap format | ||
var getParameter = function(response, headers, content) { | ||
response.setOK(headers['CSeq']); | ||
response.send(); | ||
} | ||
var dmapData = tools.parseDmap(content); | ||
rtspServer.metadata = dmapData; | ||
rtspServer.external.emit('metadataChange', rtspServer.metadata); | ||
} else if (headers['Content-Type'] == 'image/jpeg') { | ||
rtspServer.metadata.artwork = content; | ||
rtspServer.external.emit('artworkChange', content); | ||
} else if (headers['Content-Type'] == 'text/parameters') { | ||
var data = content.toString().split(': '); | ||
rtspServer.metadata = rtspServer.metadata || {}; | ||
return { | ||
"OPTIONS" : options, | ||
"ANNOUNCE" : announce, | ||
"SETUP" : setup, | ||
"RECORD" : record, | ||
"FLUSH" : flush, | ||
"TEARDOWN" : teardown, | ||
"SET_PARAMETER" : setParameter, // metadata, volume control | ||
"GET_PARAMETER" : getParameter // asked for by iOS? | ||
}; | ||
if (data[0] == 'volume') { | ||
rtspServer.metadata['volume'] = parseFloat(data[1]); | ||
rtspServer.external.emit('volumeChange', parseInt(rtspServer.metadata['volume'])); | ||
} else if (data[0] == 'progress') { | ||
rtspServer.metadata['progress'] = data[1]; | ||
rtspServer.external.emit('progressChange', rtspServer.metadata['progress']); | ||
} | ||
} else { | ||
} | ||
response.setOK(headers['CSeq']); | ||
response.send(); | ||
}; | ||
var getParameter = function(response, headers, content) { | ||
response.setOK(headers['CSeq']); | ||
response.send(); | ||
} | ||
return { | ||
"OPTIONS" : options, | ||
"ANNOUNCE" : announce, | ||
"SETUP" : setup, | ||
"RECORD" : record, | ||
"FLUSH" : flush, | ||
"TEARDOWN" : teardown, | ||
"SET_PARAMETER" : setParameter, // metadata, volume control | ||
"GET_PARAMETER" : getParameter // asked for by iOS? | ||
}; | ||
}; |
@@ -5,2 +5,4 @@ "use strict"; | ||
var net = require('net'); | ||
var portastic = require('portastic'); | ||
var randomMac = require('random-mac'); | ||
var RtspServer = require('./rtsp'); | ||
@@ -10,53 +12,61 @@ var EventEmitter = require('events').EventEmitter; | ||
var NodeTunes = function(outStream, options) { | ||
var self = this; | ||
var options = options || {}; | ||
var self = this; | ||
var options = options || {}; | ||
options.outStream = outStream; | ||
options.outStream = outStream; | ||
if (!options.serverName) { | ||
options.serverName = 'NodeTunes'; | ||
} | ||
if (!options.serverName) { | ||
options.serverName = 'NodeTunes'; | ||
} | ||
if (!options.macAddress) { | ||
options.macAddress = '5F513885F785'; | ||
} | ||
if (!options.macAddress) { | ||
options.macAddress = randomMac().toUpperCase().replace(/:/g, ''); | ||
} | ||
var txtSetup = { | ||
txtvers: '1', // txt record version? | ||
tx: '1', // ? | ||
ch: '2', // # channels | ||
cn: '0', // codec; 0=pcm, 1=alac, 2=aac, 3=aac elc; fwiw Sonos supports aac; pcm required for iPad+Spotify; OS X works with pcm | ||
et: '0,1', // encryption; 0=none, 1=rsa, 3=fairplay, 4=mfisap, 5=fairplay2.5; need rsa for os x | ||
md: '0', // metadata; 0=text, 1=artwork, 2=progress | ||
pw: (options.password ? 'true' : 'false'), // password enabled | ||
sr: '44100', // sampling rate (e.g. 44.1KHz) | ||
ss: '16', // sample size (e.g. 16 bit?) | ||
tp: 'TCP,UDP', // transport protocol | ||
vs: '130.14', // server version? | ||
am: options.serverName, // device model | ||
ek: '1', // ? from ApEx; setting to 1 enables iTunes; seems to use ALAC regardless of 'cn' setting | ||
//sv: 'false', // ? from ApEx | ||
//da: 'true', // ? from ApEx | ||
//vn: '65537', // ? from ApEx; maybe rsa key modulus? happens to be the same value | ||
//fv: '76400.10', // ? from ApEx; maybe AirPort software version (7.6.4) | ||
//sf: '0x5' // ? from ApEx | ||
}; | ||
var txtSetup = { | ||
txtvers: '1', // txt record version? | ||
tx: '1', // ? | ||
ch: '2', // # channels | ||
cn: '0', // codec; 0=pcm, 1=alac, 2=aac, 3=aac elc; fwiw Sonos supports aac; pcm required for iPad+Spotify; OS X works with pcm | ||
et: '0,1', // encryption; 0=none, 1=rsa, 3=fairplay, 4=mfisap, 5=fairplay2.5; need rsa for os x | ||
md: '0', // metadata; 0=text, 1=artwork, 2=progress | ||
pw: (options.password ? 'true' : 'false'), // password enabled | ||
sr: '44100', // sampling rate (e.g. 44.1KHz) | ||
ss: '16', // sample size (e.g. 16 bit?) | ||
tp: 'TCP,UDP', // transport protocol | ||
vs: '130.14', // server version? | ||
am: options.serverName, // device model | ||
ek: '1', // ? from ApEx; setting to 1 enables iTunes; seems to use ALAC regardless of 'cn' setting | ||
//sv: 'false', // ? from ApEx | ||
//da: 'true', // ? from ApEx | ||
//vn: '65537', // ? from ApEx; maybe rsa key modulus? happens to be the same value | ||
//fv: '76400.10', // ? from ApEx; maybe AirPort software version (7.6.4) | ||
//sf: '0x5' // ? from ApEx | ||
}; | ||
var netServer = null; | ||
var rtspServer = new RtspServer(options, self); | ||
var netServer = null; | ||
var rtspServer = new RtspServer(options, self); | ||
NodeTunes.prototype.start = function() { | ||
NodeTunes.prototype.start = function() { | ||
portastic.find({ | ||
min: 5000, | ||
max: 5050, | ||
retrieve: 1 | ||
}, function(err, port) { | ||
if (err) throw err; | ||
netServer = net.createServer(rtspServer.connectHandler).listen(port, function() { | ||
netServer = net.createServer(rtspServer.handler).listen(5000, function() { | ||
var ad = mdns.createAdvertisement(mdns.tcp('raop'), 5000, { | ||
name: options.macAddress + '@' + options.serverName, | ||
txtRecord: txtSetup | ||
}); | ||
}); | ||
// advertise on bonjour | ||
var ad = mdns.createAdvertisement(mdns.tcp('raop'), port, { | ||
name: options.macAddress + '@' + options.serverName, | ||
txtRecord: txtSetup | ||
}); | ||
}); | ||
}); | ||
}; | ||
}; | ||
NodeTunes.prototype.stop = function() { | ||
netServer.close(); | ||
}; | ||
NodeTunes.prototype.stop = function() { | ||
netServer.close(); | ||
}; | ||
}; | ||
@@ -63,0 +73,0 @@ NodeTunes.prototype.__proto__ = EventEmitter.prototype; |
{ | ||
"name": "nodetunes", | ||
"version": "0.0.4", | ||
"version": "0.0.5", | ||
"author": "Stephen Wan <stephen@stephenwan.net>", | ||
"description": "AirTunes v2 Music Server", | ||
"contributors": [ | ||
"contributors": [ | ||
{ | ||
"name": "Stephen Wan", | ||
"email": "stephen@stephenwan.net" | ||
} | ||
} | ||
], | ||
@@ -19,15 +19,16 @@ "scripts": { | ||
}, | ||
"dependencies" : { | ||
"mdns2" : "*", | ||
"ursa" : "*", | ||
"ip" : "*", | ||
"priorityqueuejs" : "*", | ||
"portastic" : "*", | ||
"randomstring" : "*", | ||
"httplike" : "0.0.1" | ||
"dependencies": { | ||
"mdns2": "*", | ||
"ursa": "*", | ||
"ip": "*", | ||
"priorityqueuejs": "*", | ||
"portastic": "*", | ||
"randomstring": "*", | ||
"httplike": "0.0.1", | ||
"random-mac": "0.0.4" | ||
}, | ||
"devDependencies" : { | ||
"speaker" : "*" | ||
"devDependencies": { | ||
"speaker": "*" | ||
}, | ||
"license": "MIT" | ||
} |
20367
8.01%480
3.67%8
14.29%+ Added
+ Added