[
  {
    "path": ".gitignore",
    "content": "lib-cov\nlcov.info\n*.seed\n*.log\n*.csv\n*.dat\n*.out\n*.pid\n*.gz\n*.pcap\n*.pcm\n\npids\nlogs\nresults\nbuild\n.grunt\n\nnode_modules\n.DS_STORE"
  },
  {
    "path": ".jscsrc",
    "content": "{\n  \"preset\": \"airbnb\"\n}\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: node_js\nbefore_install:\n  - sudo apt-get update -qq\n  - sudo apt-get install -y libavahi-compat-libdnssd-dev libasound2-dev\nnode_js:\n  - \"0.10\"\n  - \"0.12\"\ncache:\n  directories:\n    - node_modules\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "Changelog\n---------\n\n##### 0.3.0\n- Added support for ALAC encoding (iOS 9, OS X El Capitan)\n\n##### 0.2.1\n- Bumped mdns version\n- Added support for node 4.0.0\n\n##### 0.2.0\n- Added support for node 0.12.x\n- Added jscs airbnb checking to build process\n\n##### 0.1.2\n- Updated to mdns 2.2.2 (completes 0.11.x support)\n- Added warning/disconnect for unsupported codecs (non-PCM)\n\n##### 0.1.1\n- Added verbose output for Apple Challenge creation\n\n##### 0.1.0\n- Upgraded to httplike 1.0.1\n- Added option for verbose output\n- Added disconnect timeout (see #8)\n\n##### 0.0.19\n- Removed dependency on ursa package.\n\n##### 0.0.18\n- Locked down dependencies, moved to mdns package\n\n##### 0.0.17\n- Fixed bug where unknown request method (e.g. `GET`) would crash session\n- Removed legacy `MessageBuilder` responses\n- Fixed buggy response errors to use `httplike` errors\n\n##### 0.0.16\n- Added support for fetching human-readable client name\n\n##### 0.0.14\n- Solved issues created in 0.0.12\n- Fixed clientConnected/clientDisconnected issues\n- Correct cleanup for RTP binding\n\n##### 0.0.12\n- EXPERIMENTAL - added testing infrastructure, refactoring.\n- Moved to httplike v0.0.7 (trim on method support)\n\n##### 0.0.11\n- Fixed 'undefined' bug in RTSP replies\n\n##### 0.0.10\n- Fixed scope leakage issues\n- Fixed file naming issues\n\n##### 0.0.7\n- Changed output audio stream pattern to better match callback pattern. See new ```examples/server.js```\n\n"
  },
  {
    "path": "README.md",
    "content": "NodeTunes\n=========\n[![build status](https://secure.travis-ci.org/stephen/nodetunes.png)](http://travis-ci.org/stephen/nodetunes)\n\nnodetunes is an implementation of the Apple AirTunes v2 (audio AirPlay) protocol written in node.js.\n\n```\nnpm install nodetunes\n```\n\nSee ```examples/server.js``` for example usage.\n"
  },
  {
    "path": "examples/server.js",
    "content": "'use strict';\n\nvar AirTunesServer = require('../index');\nvar Speaker = require('speaker');\n\nvar speaker = new Speaker({\n  channels: 2,\n  bitDepth: 16,\n  sampleRate: 44100,\n});\nvar server = new AirTunesServer({ serverName: 'NodeTunes Speaker' });\n\nserver.on('clientConnected', function(stream) {\n  stream.pipe(speaker);\n});\n\nserver.start();\n"
  },
  {
    "path": "examples/server_multiple.js",
    "content": "'use strict';\n\nvar AirTunesServer = require('../index');\nvar Speaker = require('speaker');\n\nvar server1 = new AirTunesServer({ serverName: 'NodeTunes 1' });\nvar server2 = new AirTunesServer({ serverName: 'NodeTunes 2' });\n\nserver1.on('clientConnected', function(stream) {\n  stream.on('data', function(d) {\n    process.stdout.write('\\rWriting for Server 1: ' + d.length + ' bytes @ ' + new Date().getTime() + '\\t');\n  });\n});\n\nserver2.on('clientConnected', function(stream) {\n  stream.on('data', function(d) {\n    process.stdout.write('\\rWriting for Server 2: ' + d.length + ' bytes @ ' + new Date().getTime() + '\\t');\n  });\n});\n\nserver1.start();\nserver2.start();\n"
  },
  {
    "path": "examples/server_stdout.js",
    "content": "'use strict';\n\nvar AirTunesServer = require('../index');\nvar Speaker = require('speaker');\n\nvar server = new AirTunesServer({ serverName: 'NodeTunes Stdout' });\n\nserver.on('clientConnected', function(stream) {\n  stream.pipe(process.stdout);\n});\n\nserver.start();\n"
  },
  {
    "path": "gulpfile.js",
    "content": "var gulp = require('gulp');\nvar mocha = require('gulp-mocha');\nvar jscs = require('gulp-jscs');\n\ngulp.task('test', function() {\n  gulp.src('./test/*.js')\n    .pipe(mocha({ reporter: 'spec' }));\n});\n\ngulp.task('lint', function() {\n  gulp.src('./**/*.js')\n    .pipe(jscs());\n});\n\ngulp.task('default', ['test', 'lint']);\n"
  },
  {
    "path": "index.js",
    "content": "'use strict';\n\nmodule.exports = require(__dirname + '/lib/server');\n"
  },
  {
    "path": "lib/helper.js",
    "content": "'use strict';\n\nvar forge = require('node-forge');\nvar rsa = forge.pki.rsa;\n\nvar fs = require('fs');\nvar crypto = require('crypto');\nvar debug = require('debug')('nodetunes:helper');\n\nvar parseSdp = function(msg) {\n  var multi = ['a', 'p', 'b'];\n\n  var lines = msg.split('\\r\\n');\n  var output = {};\n  for (var i = 0; i < lines.length; i++) {\n\n    var sp = lines[i].split(/=(.+)?/);\n    if (sp.length == 3) { // for some reason there's an empty item?\n      if (multi.indexOf(sp[0]) != -1) { // some attributes are multiline...\n        if (!output[sp[0]])\n          output[sp[0]] = [];\n\n        output[sp[0]].push(sp[1]);\n      } else {\n        output[sp[0]] = sp[1];\n      }\n    }\n  }\n\n  return output;\n};\n\nvar dmapTypes = {\n  mper: 8,\n  asal: 'str',\n  asar: 'str',\n  ascp: 'str',\n  asgn: 'str',\n  minm: 'str',\n  astn: 2,\n  asdk: 1,\n  caps: 1,\n  astm: 4,\n};\n\nvar parseDmap = function(buffer) {\n  var output = {};\n\n  for (var i = 8; i < buffer.length;) {\n    var itemType = buffer.slice(i, i + 4);\n    var itemLength = buffer.slice(i + 4, i + 8).readUInt32BE(0);\n    if (itemLength !== 0) {\n      var data = buffer.slice(i + 8, i + 8 + itemLength);\n      if (dmapTypes[itemType] == 'str') {\n        output[itemType.toString()] = data.toString();\n      } else if (dmapTypes[itemType] == 1) {\n        output[itemType.toString()] = data.readUInt8(0);\n      } else if (dmapTypes[itemType] == 2) {\n        output[itemType.toString()] = data.readUInt16BE(0);\n      } else if (dmapTypes[itemType] == 4) {\n        output[itemType.toString()] = data.readUInt32BE(0);\n      } else if (dmapTypes[itemType] == 8) {\n        output[itemType.toString()] = (data.readUInt32BE(0) << 8) + data.readUInt32BE(4);\n      }\n    }\n\n    i += 8 + itemLength;\n  }\n\n  return output;\n};\n\nvar getPrivateKey = function() {\n\n  var keyFile = fs.readFileSync(__dirname + '/../private.key');\n  var privkey = forge.pki.privateKeyFromPem(keyFile);\n\n  return privkey;\n};\n\nvar privateKey = getPrivateKey();\n\nvar generateAppleResponse = function(challengeBuf, ipAddr, macAddr) {\n  debug = require('debug')('nodetunes:helper'); // HACK: need to reload debug here (https://github.com/visionmedia/debug/issues/150)\n  debug('building challenge for %s (ip: %s, mac: %s)', challengeBuf.toString('base64'), ipAddr.toString('hex'), macAddr.toString('hex'));\n\n  var fullChallenge = Buffer.concat([challengeBuf, ipAddr, macAddr]);\n\n  // im sure there's an easier way to pad this buffer\n  var padding = [];\n  for (var i = fullChallenge.length; i < 32; i++) {\n    padding.push(0);\n  }\n\n  fullChallenge = Buffer.concat([fullChallenge, new Buffer(padding)]).toString('binary');\n  var response = forge.pki.rsa.encrypt(fullChallenge, privateKey, 0x01);\n  debug('computed challenge: %s', forge.util.encode64(response));\n\n  return forge.util.encode64(response);\n};\n\nvar generateRfc2617Response = function(username, realm, password, nonce, uri, method) {\n\n  var md5 = function(content) {\n    return crypto.createHash('md5').update(content).digest().toString('hex');\n  };\n\n  var ha1 = md5(username + ':' + realm + ':' + password);\n  var ha2 = md5(method + ':' + uri);\n  var response = md5(ha1 + ':' + nonce + ':' + ha2);\n\n  return response;\n};\n\nvar getDecoderOptions = function(audioOptions) {\n  if (!audioOptions) return {}\n  var decoderOptions = {\n      frameLength: parseInt(audioOptions[1], 10),\n      compatibleVersion: parseInt(audioOptions[2], 10),\n      bitDepth: parseInt(audioOptions[3], 10),\n      pb: parseInt(audioOptions[4], 10),\n      mb: parseInt(audioOptions[5], 10),\n      kb: parseInt(audioOptions[6], 10),\n      channels: parseInt(audioOptions[7], 10),\n      maxRun: parseInt(audioOptions[8], 10),\n      maxFrameBytes: parseInt(audioOptions[9], 10),\n      avgBitRate: parseInt(audioOptions[10], 10),\n      sampleRate: parseInt(audioOptions[11], 10)\n    };\n\n  return decoderOptions;\n};\n\nvar decryptAudioData = function(data, audioAesKey, audioAesIv, headerSize) {\n  var tmp = new Buffer(16);\n  if (!headerSize) headerSize = 12;\n\n  var remainder = (data.length - 12) % 16;\n  var endOfEncodedData = data.length - remainder;\n\n  var decipher = crypto.createDecipheriv('aes-128-cbc', audioAesKey, audioAesIv);\n  decipher.setAutoPadding(false);\n\n  for (var i = headerSize, l = endOfEncodedData - 16; i <= l; i += 16) {\n    data.copy(tmp, 0, i, i + 16);\n    decipher.update(tmp).copy(data, i, 0, 16);\n  }\n\n  return data.slice(headerSize);\n};\n\nmodule.exports.decryptAudioData = decryptAudioData;\nmodule.exports.getDecoderOptions = getDecoderOptions;\nmodule.exports.parseSdp = parseSdp;\nmodule.exports.parseDmap = parseDmap;\nmodule.exports.generateAppleResponse = generateAppleResponse;\nmodule.exports.generateRfc2617Response = generateRfc2617Response;\nmodule.exports.rsaPrivateKey = privateKey;\n"
  },
  {
    "path": "lib/rtp.js",
    "content": "\n'use strict';\n\nvar dgram = require('dgram');\nvar tools = require('./helper');\nvar crypto = require('crypto');\nvar debug = require('debug')('nodetunes:rtp');\n\nfunction RtpServer(rtspServer) {\n  this.rtspServer = rtspServer;\n  debug = require('debug')('nodetunes:rtp'); // HACK: need to reload debug here (https://github.com/visionmedia/debug/issues/150)\n}\n\nRtpServer.prototype.start = function() {\n  debug('starting rtp servers');\n\n  var socketType = this.rtspServer.ipv6 ? 'udp6' : 'udp4';\n\n  this.baseServer = dgram.createSocket(socketType);\n  this.controlServer = dgram.createSocket(socketType);\n  this.timingServer = dgram.createSocket(socketType);\n\n  this.baseServer.bind(this.rtspServer.ports[0]);\n  this.controlServer.bind(this.rtspServer.ports[1]);\n  this.timingServer.bind(this.rtspServer.ports[2]);\n\n  this.timeoutCounter = -1;\n  this.timeoutChecker = null;\n\n  this.baseServer.on('message', function(msg) {\n    var seq = msg.readUInt16BE(2);\n    var audio = tools.decryptAudioData(msg, this.rtspServer.audioAesKey, this.rtspServer.audioAesIv);\n    this.rtspServer.outputStream.add(audio, seq);\n\n  }.bind(this));\n\n  this.controlServer.on('message', function(msg) {\n\n    // timeout logic for socket disconnects\n    if (this.timeoutCounter === -1 && this.rtspServer.controlTimeout) {\n\n      this.timeoutChecker = setInterval(function() {\n        this.timeoutCounter++;\n\n        if (this.timeoutCounter >= this.rtspServer.controlTimeout) {\n          this.rtspServer.timeoutHandler();\n        }\n\n      }.bind(this), 1000);\n\n    }\n\n    this.timeoutCounter = 0;\n\n  }.bind(this));\n\n  this.timingServer.on('message', function(msg) {\n\n  }.bind(this));\n\n};\n\nRtpServer.prototype.stop = function() {\n  if (this.baseServer) {\n\n    debug('stopping rtp servers');\n\n    try {\n      if (this.timeoutChecker) clearInterval(this.timeoutChecker);\n      this.baseServer.close();\n      this.controlServer.close();\n      this.timingServer.close();\n    } catch (err) {\n\n    }\n  }\n};\n\nmodule.exports = RtpServer;\n"
  },
  {
    "path": "lib/rtsp.js",
    "content": "'use strict';\n\nvar net = require('net');\nvar ServerParser = require('httplike');\nvar tools = require('./helper');\nvar RtpServer = require('./rtp');\nvar httplike = require('httplike');\nvar debug = require('debug')('nodetunes:rtsp');\nvar error = require('debug')('nodetunes:error');\nvar util = require('util');\n\nfunction RtspServer(options, external) {\n  debug = require('debug')('nodetunes:rtsp'); // HACK: need to reload debug here (https://github.com/visionmedia/debug/issues/150)\n\n  this.external = external;\n  this.options = options;\n\n  this.ports = [];\n\n  this.rtp = new RtpServer(this);\n  this.macAddress = options.macAddress;\n  this.metadata = {};\n  this.outputStream = null;\n\n  this.handling = null;\n  this.clientConnected = null;\n  this.controlTimeout = options.controlTimeout;\n\n  this.methodMapping = require('./rtspmethods')(this);\n}\n\nRtspServer.prototype.connectHandler = function(socket) {\n\n  if (this.handling && !this.clientConnected) {\n    socket.end();\n    return;\n  }\n\n  this.socket = socket;\n  this.handling = this.socket;\n\n  socket.id = new Date().getTime();\n\n  var parser = new ServerParser(socket, {\n    protocol: 'RTSP/1.0',\n    statusMessages: {\n      453: 'NOT ENOUGH BANDWIDTH',\n    },\n  });\n\n  parser.on('message', function(req, res) {\n\n    res.set('CSeq', req.getHeader('CSeq'));\n    res.set('Server', 'AirTunes/105.1');\n\n    var method = this.methodMapping[req.method];\n\n    if (method) {\n      debug('received method %s (CSeq: %s)\\n%s', req.method, req.getHeader('CSeq'), util.inspect(req.headers));\n      method(req, res);\n    } else {\n      error('received unknown method:', req.method);\n      res.send(400);\n      socket.end();\n    }\n\n  }.bind(this));\n\n  socket.on('close', this.disconnectHandler.bind({ self: this, socket: socket }));\n};\n\nRtspServer.prototype.timeoutHandler = function() {\n  debug('client timeout detected (no ping in %s seconds)', this.controlTimeout);\n  if (this.clientConnected)\n    this.clientConnected.destroy();\n};\n\nRtspServer.prototype.disconnectHandler = function() {\n  // keep in mind 'this' is bound to an object that looks like { self: this, socket: socket }\n  // (see above)\n\n  // handle case where multiple connections are being sought,\n  // but none have fully connected yet\n  if (this.socket === this.self.handling) {\n    this.self.handling = null;\n  }\n\n  // handle case where \"connected\" client has been disconnected\n  if (this.socket === this.self.clientConnected) {\n    debug('client disconnected');\n\n    this.self.clientConnected = null;\n    this.self.outputStream = null;\n    this.self.rtp.stop();\n    this.self.external.emit('clientDisconnected');\n  }\n\n};\n\nmodule.exports = RtspServer;\n"
  },
  {
    "path": "lib/rtspmethods.js",
    "content": "'use strict';\n\nvar tools = require('./helper');\nvar ipaddr = require('ipaddr.js');\nvar randomstring = require('randomstring');\nvar crypto = require('crypto');\nvar debug = require('debug')('nodetunes:rtspmethods');\nvar OutputStream = require('./streams/output');\nvar AlacDecoderStream = require('alac2pcm');\nvar PcmDecoderStream = require('./streams/pcm');\nvar decoderStreams = { '96 AppleLossless': AlacDecoderStream, '96 L16/44100/2': PcmDecoderStream };\n\nmodule.exports = function(rtspServer) {\n\n  var nonce = '';\n\n  var options = function(req, res) {\n\n    res.set('Public', 'ANNOUNCE, SETUP, RECORD, PAUSE, FLUSH, TEARDOWN, OPTIONS, GET_PARAMETER, SET_PARAMETER, POST, GET');\n\n    if (req.getHeader('Apple-Challenge')) {\n\n      // challenge response consists of challenge + ip address + mac address + padding to 32 bytes,\n      // encrypted with the ApEx private key (private encryption mode w/ PKCS1 padding)\n\n      var challengeBuf = new Buffer(req.getHeader('Apple-Challenge'), 'base64');\n\n      var ipAddrRepr = ipaddr.parse(rtspServer.socket.address().address);\n      if (ipAddrRepr.kind() === 'ipv6' && ipAddrRepr.isIPv4MappedAddress()) {\n        ipAddrRepr = ipAddrRepr.toIPv4Address();\n      }\n\n      var ipAddr = new Buffer(ipAddrRepr.toByteArray());\n\n      var macAddr = new Buffer(rtspServer.macAddress.replace(/:/g, ''), 'hex');\n      res.set('Apple-Response', tools.generateAppleResponse(challengeBuf, ipAddr, macAddr));\n    }\n\n    res.send();\n  };\n\n  var announce = function(req, res) {\n    debug(req.content.toString());\n\n    if (rtspServer.clientConnected) {\n\n      debug('already streaming; rejecting new client');\n      res.status(453).send();\n\n    } else if (rtspServer.options.password && !req.getHeader('Authorization')) {\n\n      var md5sum = crypto.createHash('md5');\n      md5sum.update = randomstring.generate();\n      res.status(401);\n      nonce = md5sum.digest('hex').toString('hex');\n\n      res.set('WWW-Authenticate', 'Digest realm=\"roap\", nonce=\"' + nonce + '\"');\n      res.send();\n\n    } else if (rtspServer.options.password && req.getHeader('Authorization')) {\n\n      var auth = req.getHeader('Authorization');\n\n      var params = auth.split(/, /g);\n      var map = {};\n      params.forEach(function(param) {\n        var pair = param.replace(/[\"]/g, '').split('=');\n        map[pair[0]] = pair[1];\n      });\n\n      var expectedResponse = tools.generateRfc2617Response('iTunes', 'roap', rtspServer.options.password, nonce, map.uri, 'ANNOUNCE');\n      var receivedResponse = map.response;\n\n      if (expectedResponse === receivedResponse) {\n        announceParse(req, res);\n      } else {\n        res.send(401);\n      }\n\n    } else {\n      announceParse(req, res);\n    }\n  };\n\n  var announceParse = function(req, res) {\n\n    var sdp = tools.parseSdp(req.content.toString());\n\n    for (var i = 0; i < sdp.a.length; i++) {\n      var spIndex = sdp.a[i].indexOf(':');\n      var aKey = sdp.a[i].substring(0, spIndex);\n      var aValue = sdp.a[i].substring(spIndex + 1);\n\n      if (aKey == 'rsaaeskey') {\n\n        rtspServer.audioAesKey = tools.rsaPrivateKey.decrypt(new Buffer(aValue, 'base64').toString('binary'), 'RSA-OAEP');\n\n      } else if (aKey == 'aesiv') {\n\n        rtspServer.audioAesIv = new Buffer(aValue, 'base64');\n\n      } else if (aKey == 'rtpmap') {\n\n        rtspServer.audioCodec = aValue;\n\n        if (aValue.indexOf('L16') === -1 && aValue.indexOf('AppleLossless') === -1) {\n          //PCM: L16/(...)\n          //ALAC: 96 AppleLossless\n          rtspServer.external.emit('error', { code: 415, message: 'Codec not supported (' + aValue + ')' });\n          res.status(415).send();\n        }\n\n      } else if (aKey == 'fmtp') {\n\n        rtspServer.audioOptions = aValue.split(' ');\n\n      }\n\n    }\n\n    if (sdp.i) {\n      rtspServer.metadata.clientName = sdp.i;\n      debug('client name reported (%s)', rtspServer.metadata.clientName);\n      rtspServer.external.emit('clientNameChange', sdp.i);\n    }\n\n    if (sdp.c) {\n      if (sdp.c.indexOf('IP6') !== -1) {\n        debug('ipv6 usage detected');\n        rtspServer.ipv6 = true;\n      }\n    }\n\n    var decoderOptions = tools.getDecoderOptions(rtspServer.audioOptions);\n    var decoderStream = new decoderStreams[rtspServer.audioCodec](decoderOptions);\n\n    rtspServer.clientConnected = res.socket;\n    rtspServer.outputStream = new OutputStream();\n    debug('client considered connected');\n    rtspServer.outputStream.setDecoder(decoderStream);\n    rtspServer.external.emit('clientConnected', rtspServer.outputStream);\n\n    res.send();\n  };\n\n  var setup = function(req, res) {\n    rtspServer.ports = [];\n\n    var getRandomPort = function() {\n      var min = 5000;\n      var max = 9999;\n      return Math.floor(Math.random() * (max - min + 1)) + min;\n    };\n\n    rtspServer.ports = [getRandomPort(), getRandomPort(), getRandomPort()];\n\n    if (rtspServer.ports.length >= 3) {\n\n      rtspServer.rtp.start();\n\n      debug('setting udp ports (audio: %s, control: %s, timing: %s)', rtspServer.ports[0], rtspServer.ports[1], rtspServer.ports[2]);\n\n      res.set('Transport', 'RTP/AVP/UDP;unicast;mode=record;server_port=' + rtspServer.ports[0] + ';control_port=' + rtspServer.ports[1] + ';timing_port=' + rtspServer.ports[2]);\n      res.set('Session', '1');\n      res.set('Audio-Jack-Status', 'connected');\n      res.send();\n\n    }\n  };\n\n  var record = function(req, res) {\n    if (!req.getHeader('RTP-Info')) {\n      // jscs:disable\n      // it seems like iOS airplay does something else\n    } else {\n      var rtpInfo = req.getHeader('RTP-Info').split(';');\n      var initSeq = rtpInfo[0].split('=')[1];\n      var initRtpTime = rtpInfo[1].split('=')[1];\n      if (!initSeq || !initRtpTime) {\n        res.send(400);\n      } else {\n        res.set('Audio-Latency', '0'); // FIXME\n      }\n    }\n\n    res.send();\n  };\n\n  var flush = function(req, res) {\n    res.set('RTP-Info', 'rtptime=1147914212'); // FIXME\n    res.send();\n  };\n\n  var teardown = function(req, res) {\n    rtspServer.rtp.stop();\n    res.send();\n  };\n\n  var setParameter = function(req, res) {\n    if (req.getHeader('Content-Type') == 'application/x-dmap-tagged') {\n\n      // metadata dmap/daap format\n      var dmapData = tools.parseDmap(req.content);\n      rtspServer.metadata = dmapData;\n      rtspServer.external.emit('metadataChange', rtspServer.metadata);\n      debug('received metadata (%s)', JSON.stringify(rtspServer.metadata));\n\n    } else if (req.getHeader('Content-Type') == 'image/jpeg') {\n\n      rtspServer.metadata.artwork = req.content;\n      rtspServer.external.emit('artworkChange', req.content);\n      debug('received artwork (length: %s)', rtspServer.metadata.artwork.length);\n\n    } else if (req.getHeader('Content-Type') == 'text/parameters') {\n\n      var data = req.content.toString().split(': ');\n      rtspServer.metadata = rtspServer.metadata || {};\n\n      debug('received text metadata (%s: %s)', data[0], data[1].trim());\n\n      if (data[0] == 'volume') {\n        rtspServer.metadata.volume = parseFloat(data[1]);\n        rtspServer.external.emit('volumeChange', rtspServer.metadata.volume);\n\n      } else if (data[0] == 'progress') {\n\n        rtspServer.metadata.progress = data[1];\n        rtspServer.external.emit('progressChange', rtspServer.metadata.progress);\n\n      }\n\n    } else {\n      debug('uncaptured SET_PARAMETER method: %s', req.content.toString().trim());\n    }\n\n    res.send();\n  };\n\n  var getParameter = function(req, res) {\n    debug('uncaptured GET_PARAMETER method: %s', req.content.toString().trim());\n    res.send();\n  };\n\n  return {\n    OPTIONS: options,\n    ANNOUNCE: announce,\n    SETUP: setup,\n    RECORD: record,\n    FLUSH: flush,\n    TEARDOWN: teardown,\n    SET_PARAMETER: setParameter, // metadata, volume control\n    GET_PARAMETER: getParameter, // asked for by iOS?\n  };\n};\n"
  },
  {
    "path": "lib/server.js",
    "content": "'use strict';\n\nvar mdns = require('mdns');\nvar net = require('net');\nvar portastic = require('portastic');\nvar randomMac = require('random-mac');\nvar RtspServer = require('./rtsp');\nvar EventEmitter = require('events').EventEmitter;\nvar util = require('util');\nvar debug = require('debug')('nodetunes:server');\n\nfunction NodeTunes(options) {\n  options = options || {};\n  this.options = options;\n\n  options.serverName = options.serverName || 'NodeTunes';\n  options.macAddress = options.macAddress || randomMac().toUpperCase().replace(/:/g, '');\n  options.recordDumps = options.recordDumps || false;\n  options.recordMetrics = options.recordMetrics || false;\n  options.controlTimeout = (options.controlTimeout !== undefined && options.controlTimeout !== null ? options.controlTimeout : 5);\n\n  if (options.verbose) {\n    require('debug').enable('nodetunes:*');\n    debug = require('debug')('nodetunes:server');  // HACK: need to reload debug here (https://github.com/visionmedia/debug/issues/150)\n  }\n\n  this.txtSetup = {\n    txtvers: '1',     // txt record version?\n    ch: '2',          // # channels\n    cn: '0,1',          // codec; 0=pcm, 1=alac, 2=aac, 3=aac elc; fwiw Sonos supports aac; pcm required for iPad+Spotify; OS X works with pcm\n    et: '0,1',        // encryption; 0=none, 1=rsa, 3=fairplay, 4=mfisap, 5=fairplay2.5; need rsa for os x\n    md: '0',          // metadata; 0=text, 1=artwork, 2=progress\n    pw: (options.password ? 'true' : 'false'),    // password enabled\n    sr: '44100',      // sampling rate (e.g. 44.1KHz)\n    ss: '16',         // sample size (e.g. 16 bit?)\n    tp: 'TCP,UDP',    // transport protocol\n    vs: '105.1',     // server version?\n    am: 'AirPort4,107',   // device model\n    ek: '1',          // ? from ApEx; setting to 1 enables iTunes; seems to use ALAC regardless of 'cn' setting\n    sv: 'false',    // ? from ApEx\n    da: 'true',     // ? from ApEx\n    vn: '65537',    // ? from ApEx; maybe rsa key modulus? happens to be the same value\n    fv: '76400.10', // ? from ApEx; maybe AirPort software version (7.6.4)\n    sf: '0x5'       // ? from ApEx\n  };\n\n  this.netServer = null;\n  this.rtspServer = new RtspServer(this.options, this);\n\n}\n\nutil.inherits(NodeTunes, EventEmitter);\n\nNodeTunes.prototype.start = function(callback) {\n\n  debug('starting nodetunes server (%s)', this.options.serverName);\n\n  portastic.find({\n    min: 5000,\n    max: 5050,\n    retrieve: 1,\n  }, function(err, port) {\n    if (err) {\n      if (callback) {\n        callback(err);\n      } else {\n        throw err;\n      }\n    }\n\n    this.netServer = net.createServer(this.rtspServer.connectHandler.bind(this.rtspServer)).listen(port, function() {\n\n      var ad = mdns.createAdvertisement(mdns.tcp('raop'), port, {\n        name: this.options.macAddress + '@' + this.options.serverName,\n        txtRecord: this.txtSetup,\n      });\n\n      if (callback) {\n        callback(null, {\n          port: port,\n          macAddress: this.options.macAddress,\n        });\n\n      }\n\n      debug('broadcasting mdns advertisement (for port %s)', port);\n\n    }.bind(this));\n  }.bind(this));\n\n};\n\nNodeTunes.prototype.stop = function() {\n  debug('stopping nodetunes server');\n  this.netServer.close();\n};\n\nmodule.exports = NodeTunes;\n"
  },
  {
    "path": "lib/streams/base.js",
    "content": "'use strict';\n\nmodule.exports = BaseDecoderStream;\n\nvar Readable = require('readable-stream').Readable;\nvar PriorityQueue = require('priorityqueuejs');\nvar util = require('util');\n\nfunction BaseDecoderStream() {\n  Readable.call(this);\n\n  this.isFlowing = true;\n  this.bufferQueue = new PriorityQueue(function(a, b) {\n    return b.sequenceNumber - a.sequenceNumber;\n  });\n};\n\nutil.inherits(BaseDecoderStream, Readable);\n\nBaseDecoderStream.prototype.add = function(chunk, sequenceNumber, isRetransmit) {\n  this._push({ chunk: chunk, sequenceNumber: sequenceNumber });\n};\n\nBaseDecoderStream.prototype._push = function(data) {\n  if (this.isFlowing) {\n    var result = this.push(data.chunk);\n    if (!result) {\n      this.isFlowing = false;\n    }\n    return result;\n  } else {\n    this.bufferQueue.enq(data);\n  }\n};\n\nBaseDecoderStream.prototype._read = function() {\n  this.isFlowing = true;\n  if (this.bufferQueue.size() === 0) return;\n  while (this.bufferQueue.size() > 0) {\n    if (!this._push(this.bufferQueue.deq())) return;\n  }\n};\n"
  },
  {
    "path": "lib/streams/output.js",
    "content": "'use strict';\n\nmodule.exports = OutputStream;\n\nvar PassThrough = require('readable-stream').PassThrough;\nvar BaseStream = require('./base');\nvar util = require('util');\n\nfunction OutputStream() {\n  PassThrough.call(this);\n\n  this.baseStream = new BaseStream();\n  this.decoder = null;\n};\n\nutil.inherits(OutputStream, PassThrough);\n\nOutputStream.prototype.setDecoder = function(decoder) {\n  this.decoder = decoder;\n  this.baseStream.pipe(decoder).pipe(this);\n};\n\nOutputStream.prototype.add = function(chunk, sequenceNumber) {\n  this.baseStream.add(chunk, sequenceNumber);\n};\n"
  },
  {
    "path": "lib/streams/pcm.js",
    "content": "'use strict';\n\nmodule.exports = PcmDecoderStream;\n\nvar Transform = require('readable-stream').Transform;\nvar util = require('util');\n\nfunction PcmDecoderStream() {\n  Transform.apply(this, arguments);\n};\n\nutil.inherits(PcmDecoderStream, Transform);\n\nPcmDecoderStream.prototype._transform = function(pcmData, enc, cb) {\n  var swapBuf = new Buffer(pcmData.length);\n\n  // endian hack\n  for (var i = 0; i < pcmData.length; i += 2) {\n    swapBuf[i] = pcmData[i + 1];\n    swapBuf[i + 1] = pcmData[i];\n  };\n\n  cb(null, swapBuf);\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"nodetunes\",\n  \"version\": \"0.3.0\",\n  \"author\": \"Stephen Wan <stephen@stephenwan.net>\",\n  \"description\": \"AirTunes v2 Server Implementation\",\n  \"contributors\": [\n    {\n      \"name\": \"Stephen Wan\",\n      \"email\": \"stephen@stephenwan.net\"\n    }\n  ],\n  \"engines\": {\n    \"node\": \"0.12.x\"\n  },\n  \"scripts\": {\n    \"start\": \"node ./examples/server.js\",\n    \"test\": \"gulp test\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/stephen/nodetunes.git\"\n  },\n  \"dependencies\": {\n    \"alac2pcm\": \"^1.1.0\",\n    \"debug\": \"^2.1.0\",\n    \"forge\": \"^2.2.0\",\n    \"httplike\": \"^1.0.2\",\n    \"ipaddr.js\": \"^1.0.1\",\n    \"mdns\": \"^2.2.10\",\n    \"metricstream\": \"0.0.0\",\n    \"node-forge\": \"^0.6.16\",\n    \"portastic\": \"0.0.1\",\n    \"priorityqueuejs\": \"0.2.0\",\n    \"random-mac\": \"0.0.4\",\n    \"randomstring\": \"1.0.3\",\n    \"readable-stream\": \"^2.0.2\"\n  },\n  \"devDependencies\": {\n    \"gulp\": \"~3.6.2\",\n    \"gulp-jscs\": \"^2.0.0\",\n    \"gulp-mocha\": \"~0.4.1\",\n    \"mocha\": \"~1.18.2\",\n    \"request\": \"~2.34.0\",\n    \"speaker\": \"0.2.6\"\n  },\n  \"license\": \"MIT\"\n}\n"
  },
  {
    "path": "private.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEA59dE8qLieItsH1WgjrcFRKj6eUWqi+bGLOX1HL3U3GhC/j0Qg90u3sG/1CUt\nwC5vOYvfDmFI6oSFXi5ELabWJmT2dKHzBJKa3k9ok+8t9ucRqMd6DZHJ2YCCLlDRKSKv6kDqnw4U\nwPdpOMXziC/AMj3Z/lUVX1G7WSHCAWKf1zNS1eLvqr+boEjXuBOitnZ/bDzPHrTOZz0Dew0uowxf\n/+sG+NCK3eQJVxqcaJ/vEHKIVd2M+5qL71yJQ+87X6oV3eaYvt3zWZYD6z5vYTcrtij2VZ9Zmni/\nUAaHqn9JdsBWLUEpVviYnhimNVvYFZeCXg/IdTQ+x4IRdiXNv5hEewIDAQABAoIBAQDl8Axy9XfW\nBLmkzkEiqoSwF0PsmVrPzH9KsnwLGH+QZlvjWd8SWYGN7u1507HvhF5N3drJoVU3O14nDY4TFQAa\nLlJ9VM35AApXaLyY1ERrN7u9ALKd2LUwYhM7Km539O4yUFYikE2nIPscEsA5ltpxOgUGCY7b7ez5\nNtD6nL1ZKauw7aNXmVAvmJTcuPxWmoktF3gDJKK2wxZuNGcJE0uFQEG4Z3BrWP7yoNuSK3dii2jm\nlpPHr0O/KnPQtzI3eguhe0TwUem/eYSdyzMyVx/YpwkzwtYL3sR5k0o9rKQLtvLzfAqdBxBurciz\naaA/L0HIgAmOit1GJA2saMxTVPNhAoGBAPfgv1oeZxgxmotiCcMXFEQEWflzhWYTsXrhUIuz5jFu\na39GLS99ZEErhLdrwj8rDDViRVJ5skOp9zFvlYAHs0xh92ji1E7V/ysnKBfsMrPkk5KSKPrnjndM\noPdevWnVkgJ5jxFuNgxkOLMuG9i53B4yMvDTCRiIPMQ++N2iLDaRAoGBAO9v//mU8eVkQaoANf0Z\noMjW8CN4xwWA2cSEIHkd9AfFkftuv8oyLDCG3ZAf0vrhrrtkrfa7ef+AUb69DNggq4mHQAYBp7L+\nk5DKzJrKuO0r+R0YbY9pZD1+/g9dVt91d6LQNepUE/yY2PP5CNoFmjedpLHMOPFdVgqDzDFxU8hL\nAoGBANDrr7xAJbqBjHVwIzQ4To9pb4BNeqDndk5Qe7fT3+/H1njGaC0/rXE0Qb7q5ySgnsCb3DvA\ncJyRM9SJ7OKlGt0FMSdJD5KG0XPIpAVNwgpXXH5MDJg09KHeh0kXo+QA6viFBi21y340NonnEfdf\n54PX4ZGS/Xac1UK+pLkBB+zRAoGAf0AY3H3qKS2lMEI4bzEFoHeK3G895pDaK3TFBVmD7fV0Zhov\n17fegFPMwOII8MisYm9ZfT2Z0s5Ro3s5rkt+nvLAdfC/PYPKzTLalpGSwomSNYJcB9HNMlmhkGzc\n1JnLYT4iyUyx6pcZBmCd8bD0iwY/FzcgNDaUmbX9+XDvRA0CgYEAkE7pIPlE71qvfJQgoA9em0gI\nLAuE4Pu13aKiJnfft7hIjbK+5kyb3TysZvoyDnb3HOKvInK7vXbKuU4ISgxB2bB3HcYzQMGsz1qJ\n2gG0N5hvJpzwwhbhXqFKA4zaaSrw622wDniAK5MlIE0tIAKKP4yxNGjoD2QYjhBGuhvkWKY=\n-----END RSA PRIVATE KEY-----"
  },
  {
    "path": "test/rtspmethods.test.js",
    "content": "var net = require('net');\nvar assert = require('assert');\nvar Nodetunes = require('../index');\nvar Parser = require('httplike').ClientParser;\nvar helper = require('../lib/helper');\n\ndescribe('RTSP Methods', function() {\n\n  // test constants\n\n  var rsaAesKey = 'ldAdTcI8b2okzDhz3bCnFPwwMVwwGCVt8+0bqURzomwUVWh5gwuee14E8FszGvrJvl5+3lfXMMDw3MRTO4arG380WNq3hl7H+ck' +\n    'wgID2ZiV3YgSwh/oVA5QieD65m5vtYyNqe1dypQHOE0Fz/fOXb5ySpmzVvbJbMKP7H7DucpoXTWvk9CHMLZU8z9vWUVxMi862FPNLFWfrCE9NBM' +\n    'bwFk2r40QdbYC5fd+6d/ynrDLit6V5T/l8ESi6tcC4vRFrM8j2gQkGwLilpbKL+k38rBvZK+zTs8k/k25zOb7xtfrKoWJ7soIska+unVnEF5ILE' +\n    'XyE3eg0NsB/IrmqKIrV9Q==';\n  var rsaAesIv = 'VkH+lhtE7jGkV5rUPM64aQ==';\n  var codec = '96 L16/44100/2';\n  var macAddress = '5F513885F785';\n\n  var announceContent = 'v=0\\r\\no=AirTunes 7709564614789383330 0 IN IP4 172.17.104.138\\r\\ns=AirTunes\\r\\n' +\n    'i=Stephen\\'s iPad\\r\\nc=IN IP4 172.17.104.138\\r\\nt=0 0\\r\\nm=audio 0 RTP/AVP 96\\r\\na=rtpmap:' + codec + '\\r\\n' +\n    'a=rsaaeskey:' + rsaAesKey + '\\r\\na=aesiv:' + rsaAesIv + '\\r\\na=min-latency:11025\\r\\na=max-latency:88200';\n\n  var server = null;\n  var port = -1;\n  var client = new net.Socket();\n  var parser = new Parser(client);\n\n  beforeEach(function(done) {\n    client = new net.Socket();\n    parser = new Parser(client);\n    server = new Nodetunes({ macAddress: macAddress });\n    server.start(function(err, d) {\n      port = d.port;\n      done();\n    });\n\n  });\n\n  afterEach(function(done) {\n    server.stop();\n    client.end();\n    done();\n  });\n\n  // tests\n\n  describe('General', function() {\n\n    it('should report CSeq correctly', function(done) {\n\n      var x = 0;\n      parser.on('message', function(m) {\n        assert(m.getHeader('CSeq') === '' + x);\n        x++;\n        if (x === 100) {\n          done();\n        }\n      });\n\n      client.connect(port, 'localhost', function() {\n        for (var i = 0; i < 100; i++) {\n          client.write('OPTIONS * RTSP/1.0\\r\\nCSeq:' + i + '\\r\\nUser-Agent: AirPlay/190.9\\r\\n\\r\\n');\n        }\n      });\n\n    });\n\n    it('should only allow one client', function(done) {\n\n      var secondClient = new net.Socket();\n      var secondParser = new Parser(secondClient);\n\n      secondParser.on('message', function(m) {\n        assert.equal(m.statusCode, 453);\n        assert.equal(m.statusMessage.toUpperCase(), 'NOT ENOUGH BANDWIDTH');\n        secondClient.end();\n        done();\n      });\n\n      client.connect(port, 'localhost', function() {\n\n        client.write('ANNOUNCE * RTSP/1.0\\r\\nCSeq:0\\r\\nUser-Agent: AirPlay/190.9\\r\\nContent-Length:' + announceContent.length + '\\r\\n\\r\\n' + announceContent);\n\n        parser.on('message', function(m) {\n          secondClient.connect(port, 'localhost', function() {\n            secondClient.write('ANNOUNCE * RTSP/1.0\\r\\nCSeq:0\\r\\nUser-Agent: AirPlay/190.9\\r\\nContent-Length:' + announceContent.length + '\\r\\n\\r\\n' + announceContent);\n          });\n        });\n      });\n\n    });\n  });\n\n  describe('OPTIONS', function() {\n\n    it('should respond with available method options', function(done) {\n\n      parser.on('message', function(m) {\n        assert(m.protocol === 'RTSP/1.0');\n        assert(m.statusCode === 200);\n        assert(m.statusMessage === 'OK');\n        assert(m.getHeader('Server') === 'AirTunes/105.1');\n        assert(m.getHeader('CSeq') === '0');\n        assert(m.getHeader('Public') === 'ANNOUNCE, SETUP, RECORD, PAUSE, FLUSH, TEARDOWN, OPTIONS, GET_PARAMETER, SET_PARAMETER, POST, GET');\n        done();\n      });\n\n      client.connect(port, 'localhost', function() {\n        client.write('OPTIONS * RTSP/1.0\\r\\nCSeq:0\\r\\nUser-Agent: AirPlay/190.9\\r\\n\\r\\n');\n      });\n\n    });\n\n    it('should respond with to options with apple challenge response (TODO)', function(done) {\n\n      parser.on('message', function(m) {\n        assert(m.statusCode === 200);\n        done();\n      });\n\n      client.connect(port, 'localhost', function() {\n        client.write('OPTIONS * RTSP/1.0\\r\\nCSeq:0\\r\\nUser-Agent: AirPlay/190.9\\r\\nApple-Challenge: WkfiX/5gzeKemPDHyBwww==\\r\\n\\r\\n');\n      });\n\n    });\n  });\n\n  describe('ANNOUNCE', function() {\n\n    it('should respond with announce acknowledgement', function(done) {\n\n      parser.on('message', function(m) {\n        assert(m.statusCode === 200);\n        assert(server.rtspServer.audioCodec === codec);\n        assert(server.rtspServer.audioAesKey.toString('base64') === helper.rsaPrivateKey.decrypt(new Buffer(rsaAesKey, 'base64').toString('binary'), 'RSA-OAEP').toString('base64'));\n        assert(server.rtspServer.audioAesIv.toString('base64') === rsaAesIv);\n        assert(server.rtspServer.metadata.clientName === 'Stephen\\'s iPad');\n        done();\n      });\n\n      client.connect(port, 'localhost', function() {\n\n        client.write('ANNOUNCE * RTSP/1.0\\r\\nCSeq:0\\r\\nUser-Agent: AirPlay/190.9\\r\\nContent-Length:' + announceContent.length + '\\r\\n\\r\\n' + announceContent);\n      });\n    });\n\n    it('should respond with password required (TODO)', function(done) {\n\n      parser.on('message', function(m) {\n        assert(m.statusCode === 200);\n        assert(server.rtspServer.audioCodec === codec);\n        assert(server.rtspServer.audioAesKey.toString('base64') === helper.rsaPrivateKey.decrypt(new Buffer(rsaAesKey, 'base64').toString('binary'), 'RSA-OAEP').toString('base64'));\n        assert(server.rtspServer.audioAesIv.toString('base64') === rsaAesIv);\n        done();\n      });\n\n      client.connect(port, 'localhost', function() {\n\n        client.write('ANNOUNCE * RTSP/1.0\\r\\nCSeq:0\\r\\nUser-Agent: AirPlay/190.9\\r\\nContent-Length:' + announceContent.length + '\\r\\n\\r\\n' + announceContent);\n      });\n\n    });\n\n    it('should respond with password validation (TODO)', function(done) {\n\n      parser.on('message', function(m) {\n        assert(m.statusCode === 200);\n        assert(server.rtspServer.audioCodec === codec);\n        assert(server.rtspServer.audioAesKey.toString('base64') === helper.rsaPrivateKey.decrypt(new Buffer(rsaAesKey, 'base64').toString('binary'), 'RSA-OAEP').toString('base64'));\n        assert(server.rtspServer.audioAesIv.toString('base64') === rsaAesIv);\n        done();\n      });\n\n      client.connect(port, 'localhost', function() {\n\n        client.write('ANNOUNCE * RTSP/1.0\\r\\nCSeq:0\\r\\nUser-Agent: AirPlay/190.9\\r\\nContent-Length:' + announceContent.length + '\\r\\n\\r\\n' + announceContent);\n      });\n\n    });\n  });\n\n  describe('SETUP', function() {\n\n    it('should respond with setup acknowledgement (TODO)', function(done) {\n      var counter = 0;\n\n      parser.on('message', function(m) {\n        counter++;\n        assert(m.statusCode === 200);\n\n        // TODO: check client ports saved on server\n        if (counter == 1) {\n          client.write('SETUP * RTSP/1.0\\r\\nCSeq:0\\r\\nUser-Agent: AirPlay/190.9\\r\\nTransport:RTP/AVP/UDP;unicast;mode=record;timing_port=56631;x-events;control_port=62727\\r\\n\\r\\n');\n\n        } else if (counter == 2) {\n          assert(server.rtspServer.ports.length === 3);\n          assert(typeof server.rtspServer.ports[0] === 'number');\n          assert(typeof server.rtspServer.ports[1] === 'number');\n          assert(typeof server.rtspServer.ports[2] === 'number');\n          client.destroy();\n          done();\n        }\n      });\n\n      client.connect(port, 'localhost', function() {\n\n        client.write('ANNOUNCE * RTSP/1.0\\r\\nCSeq:0\\r\\nUser-Agent: AirPlay/190.9\\r\\nContent-Length:' + announceContent.length + '\\r\\n\\r\\n' + announceContent);\n      });\n\n    });\n  });\n\n  describe('RECORD', function() {\n\n    it('should respond with record acknowledgement (TODO)', function(done) {\n\n      parser.on('message', function(m) {\n        assert(m.statusCode === 200);\n        done();\n      });\n\n      client.connect(port, 'localhost', function() {\n\n        client.write('RECORD * RTSP/1.0\\r\\nCSeq:0\\r\\nUser-Agent: AirPlay/190.9\\r\\n\\r\\n');\n      });\n\n    });\n  });\n\n  describe('FLUSH', function() {\n\n    it('should respond with flush acknowledgement (IMPL TODO)', function(done) {\n\n      parser.on('message', function(m) {\n        assert(m.statusCode === 200);\n        done();\n      });\n\n      client.connect(port, 'localhost', function() {\n\n        client.write('FLUSH * RTSP/1.0\\r\\nCSeq:0\\r\\nUser-Agent: AirPlay/190.9\\r\\n\\r\\n');\n      });\n\n    });\n  });\n\n  describe('TEARDOWN', function() {\n\n    it('should respond with teardown acknowledgement (TODO)', function(done) {\n\n      parser.on('message', function(m) {\n        assert(m.statusCode === 200);\n        done();\n      });\n\n      client.connect(port, 'localhost', function() {\n        client.write('OPTIONS * RTSP/1.0\\r\\nCSeq:2\\r\\nUser-Agent: AirPlay/190.9\\r\\n\\r\\n');\n      });\n\n    });\n  });\n\n  describe('GET_PARAMETER', function() {\n\n    it('should respond with volume parameter (IMPL TODO)', function(done) {\n\n      parser.on('message', function(m) {\n        assert(m.statusCode === 200);\n        done();\n      });\n\n      var content = 'volume';\n\n      client.connect(port, 'localhost', function() {\n        client.write('GET_PARAMETER * RTSP/1.0\\r\\nCSeq:2\\r\\nUser-Agent: AirPlay/190.9\\r\\nContent-Length:' + content.length + '\\r\\n\\r\\n' + content);\n      });\n\n    });\n  });\n\n  describe('SET_PARAMETER', function() {\n\n    it('should set and acknowledge volume', function(done) {\n\n      server.on('volumeChange', function(volume) {\n        assert(volume === -2.25);\n        done();\n      });\n\n      var content = 'volume: -2.250000';\n\n      client.connect(port, 'localhost', function() {\n        client.write('SET_PARAMETER * RTSP/1.0\\r\\nCSeq:2\\r\\nUser-Agent: AirPlay/190.9\\r\\nContent-Type:text/parameters\\r\\nContent-Length:' + content.length + '\\r\\n\\r\\n' + content);\n      });\n\n    });\n\n    it('should set and acknowedge text metadata (TODO)', function(done) {\n\n      parser.on('message', function(m) {\n        assert(m.statusCode === 200);\n        done();\n      });\n\n      var content = 'bawkbawk';\n\n      client.connect(port, 'localhost', function() {\n        client.write('SET_PARAMETER * RTSP/1.0\\r\\nCSeq:2\\r\\nUser-Agent: AirPlay/190.9\\r\\nContent-Type:application/x-dmap-tagged\\r\\nContent-Length:' + content.length + '\\r\\n\\r\\n' + content);\n      });\n\n    });\n\n    it('should set and acknowedge album metadata (TODO)', function(done) {\n\n      parser.on('message', function(m) {\n        assert(m.statusCode === 200);\n        done();\n      });\n\n      var content = 'bawkbawk';\n\n      client.connect(port, 'localhost', function() {\n        client.write('SET_PARAMETER * RTSP/1.0\\r\\nCSeq:2\\r\\nUser-Agent: AirPlay/190.9\\r\\nContent-Type:application/x-dmap-tagged\\r\\nContent-Length:' + content.length + '\\r\\n\\r\\n' + content);\n      });\n\n    });\n  });\n\n});\n"
  }
]