Full Code of stephen/nodetunes for AI

master 5e04b3b3ae7d cached
21 files
38.7 KB
12.1k tokens
6 symbols
1 requests
Download .txt
Repository: stephen/nodetunes
Branch: master
Commit: 5e04b3b3ae7d
Files: 21
Total size: 38.7 KB

Directory structure:
gitextract_r8tg7z38/

├── .gitignore
├── .jscsrc
├── .travis.yml
├── CHANGELOG.md
├── README.md
├── examples/
│   ├── server.js
│   ├── server_multiple.js
│   └── server_stdout.js
├── gulpfile.js
├── index.js
├── lib/
│   ├── helper.js
│   ├── rtp.js
│   ├── rtsp.js
│   ├── rtspmethods.js
│   ├── server.js
│   └── streams/
│       ├── base.js
│       ├── output.js
│       └── pcm.js
├── package.json
├── private.key
└── test/
    └── rtspmethods.test.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
lib-cov
lcov.info
*.seed
*.log
*.csv
*.dat
*.out
*.pid
*.gz
*.pcap
*.pcm

pids
logs
results
build
.grunt

node_modules
.DS_STORE

================================================
FILE: .jscsrc
================================================
{
  "preset": "airbnb"
}


================================================
FILE: .travis.yml
================================================
language: node_js
before_install:
  - sudo apt-get update -qq
  - sudo apt-get install -y libavahi-compat-libdnssd-dev libasound2-dev
node_js:
  - "0.10"
  - "0.12"
cache:
  directories:
    - node_modules


================================================
FILE: CHANGELOG.md
================================================
Changelog
---------

##### 0.3.0
- Added support for ALAC encoding (iOS 9, OS X El Capitan)

##### 0.2.1
- Bumped mdns version
- Added support for node 4.0.0

##### 0.2.0
- Added support for node 0.12.x
- Added jscs airbnb checking to build process

##### 0.1.2
- Updated to mdns 2.2.2 (completes 0.11.x support)
- Added warning/disconnect for unsupported codecs (non-PCM)

##### 0.1.1
- Added verbose output for Apple Challenge creation

##### 0.1.0
- Upgraded to httplike 1.0.1
- Added option for verbose output
- Added disconnect timeout (see #8)

##### 0.0.19
- Removed dependency on ursa package.

##### 0.0.18
- Locked down dependencies, moved to mdns package

##### 0.0.17
- Fixed bug where unknown request method (e.g. `GET`) would crash session
- Removed legacy `MessageBuilder` responses
- Fixed buggy response errors to use `httplike` errors

##### 0.0.16
- Added support for fetching human-readable client name

##### 0.0.14
- Solved issues created in 0.0.12
- Fixed clientConnected/clientDisconnected issues
- Correct cleanup for RTP binding

##### 0.0.12
- EXPERIMENTAL - added testing infrastructure, refactoring.
- Moved to httplike v0.0.7 (trim on method support)

##### 0.0.11
- Fixed 'undefined' bug in RTSP replies

##### 0.0.10
- Fixed scope leakage issues
- Fixed file naming issues

##### 0.0.7
- Changed output audio stream pattern to better match callback pattern. See new ```examples/server.js```



================================================
FILE: README.md
================================================
NodeTunes
=========
[![build status](https://secure.travis-ci.org/stephen/nodetunes.png)](http://travis-ci.org/stephen/nodetunes)

nodetunes is an implementation of the Apple AirTunes v2 (audio AirPlay) protocol written in node.js.

```
npm install nodetunes
```

See ```examples/server.js``` for example usage.


================================================
FILE: examples/server.js
================================================
'use strict';

var AirTunesServer = require('../index');
var Speaker = require('speaker');

var speaker = new Speaker({
  channels: 2,
  bitDepth: 16,
  sampleRate: 44100,
});
var server = new AirTunesServer({ serverName: 'NodeTunes Speaker' });

server.on('clientConnected', function(stream) {
  stream.pipe(speaker);
});

server.start();


================================================
FILE: examples/server_multiple.js
================================================
'use strict';

var AirTunesServer = require('../index');
var Speaker = require('speaker');

var server1 = new AirTunesServer({ serverName: 'NodeTunes 1' });
var server2 = new AirTunesServer({ serverName: 'NodeTunes 2' });

server1.on('clientConnected', function(stream) {
  stream.on('data', function(d) {
    process.stdout.write('\rWriting for Server 1: ' + d.length + ' bytes @ ' + new Date().getTime() + '\t');
  });
});

server2.on('clientConnected', function(stream) {
  stream.on('data', function(d) {
    process.stdout.write('\rWriting for Server 2: ' + d.length + ' bytes @ ' + new Date().getTime() + '\t');
  });
});

server1.start();
server2.start();


================================================
FILE: examples/server_stdout.js
================================================
'use strict';

var AirTunesServer = require('../index');
var Speaker = require('speaker');

var server = new AirTunesServer({ serverName: 'NodeTunes Stdout' });

server.on('clientConnected', function(stream) {
  stream.pipe(process.stdout);
});

server.start();


================================================
FILE: gulpfile.js
================================================
var gulp = require('gulp');
var mocha = require('gulp-mocha');
var jscs = require('gulp-jscs');

gulp.task('test', function() {
  gulp.src('./test/*.js')
    .pipe(mocha({ reporter: 'spec' }));
});

gulp.task('lint', function() {
  gulp.src('./**/*.js')
    .pipe(jscs());
});

gulp.task('default', ['test', 'lint']);


================================================
FILE: index.js
================================================
'use strict';

module.exports = require(__dirname + '/lib/server');


================================================
FILE: lib/helper.js
================================================
'use strict';

var forge = require('node-forge');
var rsa = forge.pki.rsa;

var fs = require('fs');
var crypto = require('crypto');
var debug = require('debug')('nodetunes:helper');

var parseSdp = function(msg) {
  var multi = ['a', 'p', 'b'];

  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]] = [];

        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,
};

var parseDmap = function(buffer) {
  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;
  }

  return output;
};

var getPrivateKey = function() {

  var keyFile = fs.readFileSync(__dirname + '/../private.key');
  var privkey = forge.pki.privateKeyFromPem(keyFile);

  return privkey;
};

var privateKey = getPrivateKey();

var generateAppleResponse = function(challengeBuf, ipAddr, macAddr) {
  debug = require('debug')('nodetunes:helper'); // HACK: need to reload debug here (https://github.com/visionmedia/debug/issues/150)
  debug('building challenge for %s (ip: %s, mac: %s)', challengeBuf.toString('base64'), ipAddr.toString('hex'), macAddr.toString('hex'));

  var fullChallenge = Buffer.concat([challengeBuf, ipAddr, macAddr]);

  // im sure there's an easier way to pad this buffer
  var padding = [];
  for (var i = fullChallenge.length; i < 32; i++) {
    padding.push(0);
  }

  fullChallenge = Buffer.concat([fullChallenge, new Buffer(padding)]).toString('binary');
  var response = forge.pki.rsa.encrypt(fullChallenge, privateKey, 0x01);
  debug('computed challenge: %s', forge.util.encode64(response));

  return forge.util.encode64(response);
};

var generateRfc2617Response = function(username, realm, password, nonce, uri, method) {

  var md5 = function(content) {
    return crypto.createHash('md5').update(content).digest().toString('hex');
  };

  var ha1 = md5(username + ':' + realm + ':' + password);
  var ha2 = md5(method + ':' + uri);
  var response = md5(ha1 + ':' + nonce + ':' + ha2);

  return response;
};

var getDecoderOptions = function(audioOptions) {
  if (!audioOptions) return {}
  var decoderOptions = {
      frameLength: parseInt(audioOptions[1], 10),
      compatibleVersion: parseInt(audioOptions[2], 10),
      bitDepth: parseInt(audioOptions[3], 10),
      pb: parseInt(audioOptions[4], 10),
      mb: parseInt(audioOptions[5], 10),
      kb: parseInt(audioOptions[6], 10),
      channels: parseInt(audioOptions[7], 10),
      maxRun: parseInt(audioOptions[8], 10),
      maxFrameBytes: parseInt(audioOptions[9], 10),
      avgBitRate: parseInt(audioOptions[10], 10),
      sampleRate: parseInt(audioOptions[11], 10)
    };

  return decoderOptions;
};

var decryptAudioData = function(data, audioAesKey, audioAesIv, headerSize) {
  var tmp = new Buffer(16);
  if (!headerSize) headerSize = 12;

  var remainder = (data.length - 12) % 16;
  var endOfEncodedData = data.length - remainder;

  var decipher = crypto.createDecipheriv('aes-128-cbc', audioAesKey, audioAesIv);
  decipher.setAutoPadding(false);

  for (var i = headerSize, l = endOfEncodedData - 16; i <= l; i += 16) {
    data.copy(tmp, 0, i, i + 16);
    decipher.update(tmp).copy(data, i, 0, 16);
  }

  return data.slice(headerSize);
};

module.exports.decryptAudioData = decryptAudioData;
module.exports.getDecoderOptions = getDecoderOptions;
module.exports.parseSdp = parseSdp;
module.exports.parseDmap = parseDmap;
module.exports.generateAppleResponse = generateAppleResponse;
module.exports.generateRfc2617Response = generateRfc2617Response;
module.exports.rsaPrivateKey = privateKey;


================================================
FILE: lib/rtp.js
================================================

'use strict';

var dgram = require('dgram');
var tools = require('./helper');
var crypto = require('crypto');
var debug = require('debug')('nodetunes:rtp');

function RtpServer(rtspServer) {
  this.rtspServer = rtspServer;
  debug = require('debug')('nodetunes:rtp'); // HACK: need to reload debug here (https://github.com/visionmedia/debug/issues/150)
}

RtpServer.prototype.start = function() {
  debug('starting rtp servers');

  var socketType = this.rtspServer.ipv6 ? 'udp6' : 'udp4';

  this.baseServer = dgram.createSocket(socketType);
  this.controlServer = dgram.createSocket(socketType);
  this.timingServer = dgram.createSocket(socketType);

  this.baseServer.bind(this.rtspServer.ports[0]);
  this.controlServer.bind(this.rtspServer.ports[1]);
  this.timingServer.bind(this.rtspServer.ports[2]);

  this.timeoutCounter = -1;
  this.timeoutChecker = null;

  this.baseServer.on('message', function(msg) {
    var seq = msg.readUInt16BE(2);
    var audio = tools.decryptAudioData(msg, this.rtspServer.audioAesKey, this.rtspServer.audioAesIv);
    this.rtspServer.outputStream.add(audio, seq);

  }.bind(this));

  this.controlServer.on('message', function(msg) {

    // timeout logic for socket disconnects
    if (this.timeoutCounter === -1 && this.rtspServer.controlTimeout) {

      this.timeoutChecker = setInterval(function() {
        this.timeoutCounter++;

        if (this.timeoutCounter >= this.rtspServer.controlTimeout) {
          this.rtspServer.timeoutHandler();
        }

      }.bind(this), 1000);

    }

    this.timeoutCounter = 0;

  }.bind(this));

  this.timingServer.on('message', function(msg) {

  }.bind(this));

};

RtpServer.prototype.stop = function() {
  if (this.baseServer) {

    debug('stopping rtp servers');

    try {
      if (this.timeoutChecker) clearInterval(this.timeoutChecker);
      this.baseServer.close();
      this.controlServer.close();
      this.timingServer.close();
    } catch (err) {

    }
  }
};

module.exports = RtpServer;


================================================
FILE: lib/rtsp.js
================================================
'use strict';

var net = require('net');
var ServerParser = require('httplike');
var tools = require('./helper');
var RtpServer = require('./rtp');
var httplike = require('httplike');
var debug = require('debug')('nodetunes:rtsp');
var error = require('debug')('nodetunes:error');
var util = require('util');

function RtspServer(options, external) {
  debug = require('debug')('nodetunes:rtsp'); // HACK: need to reload debug here (https://github.com/visionmedia/debug/issues/150)

  this.external = external;
  this.options = options;

  this.ports = [];

  this.rtp = new RtpServer(this);
  this.macAddress = options.macAddress;
  this.metadata = {};
  this.outputStream = null;

  this.handling = null;
  this.clientConnected = null;
  this.controlTimeout = options.controlTimeout;

  this.methodMapping = require('./rtspmethods')(this);
}

RtspServer.prototype.connectHandler = function(socket) {

  if (this.handling && !this.clientConnected) {
    socket.end();
    return;
  }

  this.socket = socket;
  this.handling = this.socket;

  socket.id = new Date().getTime();

  var parser = new ServerParser(socket, {
    protocol: 'RTSP/1.0',
    statusMessages: {
      453: 'NOT ENOUGH BANDWIDTH',
    },
  });

  parser.on('message', function(req, res) {

    res.set('CSeq', req.getHeader('CSeq'));
    res.set('Server', 'AirTunes/105.1');

    var method = this.methodMapping[req.method];

    if (method) {
      debug('received method %s (CSeq: %s)\n%s', req.method, req.getHeader('CSeq'), util.inspect(req.headers));
      method(req, res);
    } else {
      error('received unknown method:', req.method);
      res.send(400);
      socket.end();
    }

  }.bind(this));

  socket.on('close', this.disconnectHandler.bind({ self: this, socket: socket }));
};

RtspServer.prototype.timeoutHandler = function() {
  debug('client timeout detected (no ping in %s seconds)', this.controlTimeout);
  if (this.clientConnected)
    this.clientConnected.destroy();
};

RtspServer.prototype.disconnectHandler = function() {
  // keep in mind 'this' is bound to an object that looks like { self: this, socket: socket }
  // (see above)

  // handle case where multiple connections are being sought,
  // but none have fully connected yet
  if (this.socket === this.self.handling) {
    this.self.handling = null;
  }

  // handle case where "connected" client has been disconnected
  if (this.socket === this.self.clientConnected) {
    debug('client disconnected');

    this.self.clientConnected = null;
    this.self.outputStream = null;
    this.self.rtp.stop();
    this.self.external.emit('clientDisconnected');
  }

};

module.exports = RtspServer;


================================================
FILE: lib/rtspmethods.js
================================================
'use strict';

var tools = require('./helper');
var ipaddr = require('ipaddr.js');
var randomstring = require('randomstring');
var crypto = require('crypto');
var debug = require('debug')('nodetunes:rtspmethods');
var OutputStream = require('./streams/output');
var AlacDecoderStream = require('alac2pcm');
var PcmDecoderStream = require('./streams/pcm');
var decoderStreams = { '96 AppleLossless': AlacDecoderStream, '96 L16/44100/2': PcmDecoderStream };

module.exports = function(rtspServer) {

  var nonce = '';

  var options = function(req, res) {

    res.set('Public', 'ANNOUNCE, SETUP, RECORD, PAUSE, FLUSH, TEARDOWN, OPTIONS, GET_PARAMETER, SET_PARAMETER, POST, GET');

    if (req.getHeader('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)

      var challengeBuf = new Buffer(req.getHeader('Apple-Challenge'), 'base64');

      var ipAddrRepr = ipaddr.parse(rtspServer.socket.address().address);
      if (ipAddrRepr.kind() === 'ipv6' && ipAddrRepr.isIPv4MappedAddress()) {
        ipAddrRepr = ipAddrRepr.toIPv4Address();
      }

      var ipAddr = new Buffer(ipAddrRepr.toByteArray());

      var macAddr = new Buffer(rtspServer.macAddress.replace(/:/g, ''), 'hex');
      res.set('Apple-Response', tools.generateAppleResponse(challengeBuf, ipAddr, macAddr));
    }

    res.send();
  };

  var announce = function(req, res) {
    debug(req.content.toString());

    if (rtspServer.clientConnected) {

      debug('already streaming; rejecting new client');
      res.status(453).send();

    } else if (rtspServer.options.password && !req.getHeader('Authorization')) {

      var md5sum = crypto.createHash('md5');
      md5sum.update = randomstring.generate();
      res.status(401);
      nonce = md5sum.digest('hex').toString('hex');

      res.set('WWW-Authenticate', 'Digest realm="roap", nonce="' + nonce + '"');
      res.send();

    } else if (rtspServer.options.password && req.getHeader('Authorization')) {

      var auth = req.getHeader('Authorization');

      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;

      if (expectedResponse === receivedResponse) {
        announceParse(req, res);
      } else {
        res.send(401);
      }

    } else {
      announceParse(req, res);
    }
  };

  var announceParse = function(req, res) {

    var sdp = tools.parseSdp(req.content.toString());

    for (var i = 0; i < sdp.a.length; i++) {
      var spIndex = sdp.a[i].indexOf(':');
      var aKey = sdp.a[i].substring(0, spIndex);
      var aValue = sdp.a[i].substring(spIndex + 1);

      if (aKey == 'rsaaeskey') {

        rtspServer.audioAesKey = tools.rsaPrivateKey.decrypt(new Buffer(aValue, 'base64').toString('binary'), 'RSA-OAEP');

      } else if (aKey == 'aesiv') {

        rtspServer.audioAesIv = new Buffer(aValue, 'base64');

      } else if (aKey == 'rtpmap') {

        rtspServer.audioCodec = aValue;

        if (aValue.indexOf('L16') === -1 && aValue.indexOf('AppleLossless') === -1) {
          //PCM: L16/(...)
          //ALAC: 96 AppleLossless
          rtspServer.external.emit('error', { code: 415, message: 'Codec not supported (' + aValue + ')' });
          res.status(415).send();
        }

      } else if (aKey == 'fmtp') {

        rtspServer.audioOptions = aValue.split(' ');

      }

    }

    if (sdp.i) {
      rtspServer.metadata.clientName = sdp.i;
      debug('client name reported (%s)', rtspServer.metadata.clientName);
      rtspServer.external.emit('clientNameChange', sdp.i);
    }

    if (sdp.c) {
      if (sdp.c.indexOf('IP6') !== -1) {
        debug('ipv6 usage detected');
        rtspServer.ipv6 = true;
      }
    }

    var decoderOptions = tools.getDecoderOptions(rtspServer.audioOptions);
    var decoderStream = new decoderStreams[rtspServer.audioCodec](decoderOptions);

    rtspServer.clientConnected = res.socket;
    rtspServer.outputStream = new OutputStream();
    debug('client considered connected');
    rtspServer.outputStream.setDecoder(decoderStream);
    rtspServer.external.emit('clientConnected', rtspServer.outputStream);

    res.send();
  };

  var setup = function(req, res) {
    rtspServer.ports = [];

    var getRandomPort = function() {
      var min = 5000;
      var max = 9999;
      return Math.floor(Math.random() * (max - min + 1)) + min;
    };

    rtspServer.ports = [getRandomPort(), getRandomPort(), getRandomPort()];

    if (rtspServer.ports.length >= 3) {

      rtspServer.rtp.start();

      debug('setting udp ports (audio: %s, control: %s, timing: %s)', rtspServer.ports[0], rtspServer.ports[1], rtspServer.ports[2]);

      res.set('Transport', 'RTP/AVP/UDP;unicast;mode=record;server_port=' + rtspServer.ports[0] + ';control_port=' + rtspServer.ports[1] + ';timing_port=' + rtspServer.ports[2]);
      res.set('Session', '1');
      res.set('Audio-Jack-Status', 'connected');
      res.send();

    }
  };

  var record = function(req, res) {
    if (!req.getHeader('RTP-Info')) {
      // jscs:disable
      // it seems like iOS airplay does something else
    } else {
      var rtpInfo = req.getHeader('RTP-Info').split(';');
      var initSeq = rtpInfo[0].split('=')[1];
      var initRtpTime = rtpInfo[1].split('=')[1];
      if (!initSeq || !initRtpTime) {
        res.send(400);
      } else {
        res.set('Audio-Latency', '0'); // FIXME
      }
    }

    res.send();
  };

  var flush = function(req, res) {
    res.set('RTP-Info', 'rtptime=1147914212'); // FIXME
    res.send();
  };

  var teardown = function(req, res) {
    rtspServer.rtp.stop();
    res.send();
  };

  var setParameter = function(req, res) {
    if (req.getHeader('Content-Type') == 'application/x-dmap-tagged') {

      // metadata dmap/daap format
      var dmapData = tools.parseDmap(req.content);
      rtspServer.metadata = dmapData;
      rtspServer.external.emit('metadataChange', rtspServer.metadata);
      debug('received metadata (%s)', JSON.stringify(rtspServer.metadata));

    } else if (req.getHeader('Content-Type') == 'image/jpeg') {

      rtspServer.metadata.artwork = req.content;
      rtspServer.external.emit('artworkChange', req.content);
      debug('received artwork (length: %s)', rtspServer.metadata.artwork.length);

    } else if (req.getHeader('Content-Type') == 'text/parameters') {

      var data = req.content.toString().split(': ');
      rtspServer.metadata = rtspServer.metadata || {};

      debug('received text metadata (%s: %s)', data[0], data[1].trim());

      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 {
      debug('uncaptured SET_PARAMETER method: %s', req.content.toString().trim());
    }

    res.send();
  };

  var getParameter = function(req, res) {
    debug('uncaptured GET_PARAMETER method: %s', req.content.toString().trim());
    res.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?
  };
};


================================================
FILE: lib/server.js
================================================
'use strict';

var mdns = require('mdns');
var net = require('net');
var portastic = require('portastic');
var randomMac = require('random-mac');
var RtspServer = require('./rtsp');
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var debug = require('debug')('nodetunes:server');

function NodeTunes(options) {
  options = options || {};
  this.options = options;

  options.serverName = options.serverName || 'NodeTunes';
  options.macAddress = options.macAddress || randomMac().toUpperCase().replace(/:/g, '');
  options.recordDumps = options.recordDumps || false;
  options.recordMetrics = options.recordMetrics || false;
  options.controlTimeout = (options.controlTimeout !== undefined && options.controlTimeout !== null ? options.controlTimeout : 5);

  if (options.verbose) {
    require('debug').enable('nodetunes:*');
    debug = require('debug')('nodetunes:server');  // HACK: need to reload debug here (https://github.com/visionmedia/debug/issues/150)
  }

  this.txtSetup = {
    txtvers: '1',     // txt record version?
    ch: '2',          // # channels
    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
    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: '105.1',     // server version?
    am: 'AirPort4,107',   // 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
  };

  this.netServer = null;
  this.rtspServer = new RtspServer(this.options, this);

}

util.inherits(NodeTunes, EventEmitter);

NodeTunes.prototype.start = function(callback) {

  debug('starting nodetunes server (%s)', this.options.serverName);

  portastic.find({
    min: 5000,
    max: 5050,
    retrieve: 1,
  }, function(err, port) {
    if (err) {
      if (callback) {
        callback(err);
      } else {
        throw err;
      }
    }

    this.netServer = net.createServer(this.rtspServer.connectHandler.bind(this.rtspServer)).listen(port, function() {

      var ad = mdns.createAdvertisement(mdns.tcp('raop'), port, {
        name: this.options.macAddress + '@' + this.options.serverName,
        txtRecord: this.txtSetup,
      });

      if (callback) {
        callback(null, {
          port: port,
          macAddress: this.options.macAddress,
        });

      }

      debug('broadcasting mdns advertisement (for port %s)', port);

    }.bind(this));
  }.bind(this));

};

NodeTunes.prototype.stop = function() {
  debug('stopping nodetunes server');
  this.netServer.close();
};

module.exports = NodeTunes;


================================================
FILE: lib/streams/base.js
================================================
'use strict';

module.exports = BaseDecoderStream;

var Readable = require('readable-stream').Readable;
var PriorityQueue = require('priorityqueuejs');
var util = require('util');

function BaseDecoderStream() {
  Readable.call(this);

  this.isFlowing = true;
  this.bufferQueue = new PriorityQueue(function(a, b) {
    return b.sequenceNumber - a.sequenceNumber;
  });
};

util.inherits(BaseDecoderStream, Readable);

BaseDecoderStream.prototype.add = function(chunk, sequenceNumber, isRetransmit) {
  this._push({ chunk: chunk, sequenceNumber: sequenceNumber });
};

BaseDecoderStream.prototype._push = function(data) {
  if (this.isFlowing) {
    var result = this.push(data.chunk);
    if (!result) {
      this.isFlowing = false;
    }
    return result;
  } else {
    this.bufferQueue.enq(data);
  }
};

BaseDecoderStream.prototype._read = function() {
  this.isFlowing = true;
  if (this.bufferQueue.size() === 0) return;
  while (this.bufferQueue.size() > 0) {
    if (!this._push(this.bufferQueue.deq())) return;
  }
};


================================================
FILE: lib/streams/output.js
================================================
'use strict';

module.exports = OutputStream;

var PassThrough = require('readable-stream').PassThrough;
var BaseStream = require('./base');
var util = require('util');

function OutputStream() {
  PassThrough.call(this);

  this.baseStream = new BaseStream();
  this.decoder = null;
};

util.inherits(OutputStream, PassThrough);

OutputStream.prototype.setDecoder = function(decoder) {
  this.decoder = decoder;
  this.baseStream.pipe(decoder).pipe(this);
};

OutputStream.prototype.add = function(chunk, sequenceNumber) {
  this.baseStream.add(chunk, sequenceNumber);
};


================================================
FILE: lib/streams/pcm.js
================================================
'use strict';

module.exports = PcmDecoderStream;

var Transform = require('readable-stream').Transform;
var util = require('util');

function PcmDecoderStream() {
  Transform.apply(this, arguments);
};

util.inherits(PcmDecoderStream, Transform);

PcmDecoderStream.prototype._transform = function(pcmData, enc, cb) {
  var swapBuf = new Buffer(pcmData.length);

  // endian hack
  for (var i = 0; i < pcmData.length; i += 2) {
    swapBuf[i] = pcmData[i + 1];
    swapBuf[i + 1] = pcmData[i];
  };

  cb(null, swapBuf);
};


================================================
FILE: package.json
================================================
{
  "name": "nodetunes",
  "version": "0.3.0",
  "author": "Stephen Wan <stephen@stephenwan.net>",
  "description": "AirTunes v2 Server Implementation",
  "contributors": [
    {
      "name": "Stephen Wan",
      "email": "stephen@stephenwan.net"
    }
  ],
  "engines": {
    "node": "0.12.x"
  },
  "scripts": {
    "start": "node ./examples/server.js",
    "test": "gulp test"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/stephen/nodetunes.git"
  },
  "dependencies": {
    "alac2pcm": "^1.1.0",
    "debug": "^2.1.0",
    "forge": "^2.2.0",
    "httplike": "^1.0.2",
    "ipaddr.js": "^1.0.1",
    "mdns": "^2.2.10",
    "metricstream": "0.0.0",
    "node-forge": "^0.6.16",
    "portastic": "0.0.1",
    "priorityqueuejs": "0.2.0",
    "random-mac": "0.0.4",
    "randomstring": "1.0.3",
    "readable-stream": "^2.0.2"
  },
  "devDependencies": {
    "gulp": "~3.6.2",
    "gulp-jscs": "^2.0.0",
    "gulp-mocha": "~0.4.1",
    "mocha": "~1.18.2",
    "request": "~2.34.0",
    "speaker": "0.2.6"
  },
  "license": "MIT"
}


================================================
FILE: private.key
================================================
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEA59dE8qLieItsH1WgjrcFRKj6eUWqi+bGLOX1HL3U3GhC/j0Qg90u3sG/1CUt
wC5vOYvfDmFI6oSFXi5ELabWJmT2dKHzBJKa3k9ok+8t9ucRqMd6DZHJ2YCCLlDRKSKv6kDqnw4U
wPdpOMXziC/AMj3Z/lUVX1G7WSHCAWKf1zNS1eLvqr+boEjXuBOitnZ/bDzPHrTOZz0Dew0uowxf
/+sG+NCK3eQJVxqcaJ/vEHKIVd2M+5qL71yJQ+87X6oV3eaYvt3zWZYD6z5vYTcrtij2VZ9Zmni/
UAaHqn9JdsBWLUEpVviYnhimNVvYFZeCXg/IdTQ+x4IRdiXNv5hEewIDAQABAoIBAQDl8Axy9XfW
BLmkzkEiqoSwF0PsmVrPzH9KsnwLGH+QZlvjWd8SWYGN7u1507HvhF5N3drJoVU3O14nDY4TFQAa
LlJ9VM35AApXaLyY1ERrN7u9ALKd2LUwYhM7Km539O4yUFYikE2nIPscEsA5ltpxOgUGCY7b7ez5
NtD6nL1ZKauw7aNXmVAvmJTcuPxWmoktF3gDJKK2wxZuNGcJE0uFQEG4Z3BrWP7yoNuSK3dii2jm
lpPHr0O/KnPQtzI3eguhe0TwUem/eYSdyzMyVx/YpwkzwtYL3sR5k0o9rKQLtvLzfAqdBxBurciz
aaA/L0HIgAmOit1GJA2saMxTVPNhAoGBAPfgv1oeZxgxmotiCcMXFEQEWflzhWYTsXrhUIuz5jFu
a39GLS99ZEErhLdrwj8rDDViRVJ5skOp9zFvlYAHs0xh92ji1E7V/ysnKBfsMrPkk5KSKPrnjndM
oPdevWnVkgJ5jxFuNgxkOLMuG9i53B4yMvDTCRiIPMQ++N2iLDaRAoGBAO9v//mU8eVkQaoANf0Z
oMjW8CN4xwWA2cSEIHkd9AfFkftuv8oyLDCG3ZAf0vrhrrtkrfa7ef+AUb69DNggq4mHQAYBp7L+
k5DKzJrKuO0r+R0YbY9pZD1+/g9dVt91d6LQNepUE/yY2PP5CNoFmjedpLHMOPFdVgqDzDFxU8hL
AoGBANDrr7xAJbqBjHVwIzQ4To9pb4BNeqDndk5Qe7fT3+/H1njGaC0/rXE0Qb7q5ySgnsCb3DvA
cJyRM9SJ7OKlGt0FMSdJD5KG0XPIpAVNwgpXXH5MDJg09KHeh0kXo+QA6viFBi21y340NonnEfdf
54PX4ZGS/Xac1UK+pLkBB+zRAoGAf0AY3H3qKS2lMEI4bzEFoHeK3G895pDaK3TFBVmD7fV0Zhov
17fegFPMwOII8MisYm9ZfT2Z0s5Ro3s5rkt+nvLAdfC/PYPKzTLalpGSwomSNYJcB9HNMlmhkGzc
1JnLYT4iyUyx6pcZBmCd8bD0iwY/FzcgNDaUmbX9+XDvRA0CgYEAkE7pIPlE71qvfJQgoA9em0gI
LAuE4Pu13aKiJnfft7hIjbK+5kyb3TysZvoyDnb3HOKvInK7vXbKuU4ISgxB2bB3HcYzQMGsz1qJ
2gG0N5hvJpzwwhbhXqFKA4zaaSrw622wDniAK5MlIE0tIAKKP4yxNGjoD2QYjhBGuhvkWKY=
-----END RSA PRIVATE KEY-----

================================================
FILE: test/rtspmethods.test.js
================================================
var net = require('net');
var assert = require('assert');
var Nodetunes = require('../index');
var Parser = require('httplike').ClientParser;
var helper = require('../lib/helper');

describe('RTSP Methods', function() {

  // test constants

  var rsaAesKey = 'ldAdTcI8b2okzDhz3bCnFPwwMVwwGCVt8+0bqURzomwUVWh5gwuee14E8FszGvrJvl5+3lfXMMDw3MRTO4arG380WNq3hl7H+ck' +
    'wgID2ZiV3YgSwh/oVA5QieD65m5vtYyNqe1dypQHOE0Fz/fOXb5ySpmzVvbJbMKP7H7DucpoXTWvk9CHMLZU8z9vWUVxMi862FPNLFWfrCE9NBM' +
    'bwFk2r40QdbYC5fd+6d/ynrDLit6V5T/l8ESi6tcC4vRFrM8j2gQkGwLilpbKL+k38rBvZK+zTs8k/k25zOb7xtfrKoWJ7soIska+unVnEF5ILE' +
    'XyE3eg0NsB/IrmqKIrV9Q==';
  var rsaAesIv = 'VkH+lhtE7jGkV5rUPM64aQ==';
  var codec = '96 L16/44100/2';
  var macAddress = '5F513885F785';

  var announceContent = 'v=0\r\no=AirTunes 7709564614789383330 0 IN IP4 172.17.104.138\r\ns=AirTunes\r\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' +
    'a=rsaaeskey:' + rsaAesKey + '\r\na=aesiv:' + rsaAesIv + '\r\na=min-latency:11025\r\na=max-latency:88200';

  var server = null;
  var port = -1;
  var client = new net.Socket();
  var parser = new Parser(client);

  beforeEach(function(done) {
    client = new net.Socket();
    parser = new Parser(client);
    server = new Nodetunes({ macAddress: macAddress });
    server.start(function(err, d) {
      port = d.port;
      done();
    });

  });

  afterEach(function(done) {
    server.stop();
    client.end();
    done();
  });

  // tests

  describe('General', function() {

    it('should report CSeq correctly', function(done) {

      var x = 0;
      parser.on('message', function(m) {
        assert(m.getHeader('CSeq') === '' + x);
        x++;
        if (x === 100) {
          done();
        }
      });

      client.connect(port, 'localhost', function() {
        for (var i = 0; i < 100; i++) {
          client.write('OPTIONS * RTSP/1.0\r\nCSeq:' + i + '\r\nUser-Agent: AirPlay/190.9\r\n\r\n');
        }
      });

    });

    it('should only allow one client', function(done) {

      var secondClient = new net.Socket();
      var secondParser = new Parser(secondClient);

      secondParser.on('message', function(m) {
        assert.equal(m.statusCode, 453);
        assert.equal(m.statusMessage.toUpperCase(), 'NOT ENOUGH BANDWIDTH');
        secondClient.end();
        done();
      });

      client.connect(port, 'localhost', function() {

        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);

        parser.on('message', function(m) {
          secondClient.connect(port, 'localhost', function() {
            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);
          });
        });
      });

    });
  });

  describe('OPTIONS', function() {

    it('should respond with available method options', function(done) {

      parser.on('message', function(m) {
        assert(m.protocol === 'RTSP/1.0');
        assert(m.statusCode === 200);
        assert(m.statusMessage === 'OK');
        assert(m.getHeader('Server') === 'AirTunes/105.1');
        assert(m.getHeader('CSeq') === '0');
        assert(m.getHeader('Public') === 'ANNOUNCE, SETUP, RECORD, PAUSE, FLUSH, TEARDOWN, OPTIONS, GET_PARAMETER, SET_PARAMETER, POST, GET');
        done();
      });

      client.connect(port, 'localhost', function() {
        client.write('OPTIONS * RTSP/1.0\r\nCSeq:0\r\nUser-Agent: AirPlay/190.9\r\n\r\n');
      });

    });

    it('should respond with to options with apple challenge response (TODO)', function(done) {

      parser.on('message', function(m) {
        assert(m.statusCode === 200);
        done();
      });

      client.connect(port, 'localhost', function() {
        client.write('OPTIONS * RTSP/1.0\r\nCSeq:0\r\nUser-Agent: AirPlay/190.9\r\nApple-Challenge: WkfiX/5gzeKemPDHyBwww==\r\n\r\n');
      });

    });
  });

  describe('ANNOUNCE', function() {

    it('should respond with announce acknowledgement', function(done) {

      parser.on('message', function(m) {
        assert(m.statusCode === 200);
        assert(server.rtspServer.audioCodec === codec);
        assert(server.rtspServer.audioAesKey.toString('base64') === helper.rsaPrivateKey.decrypt(new Buffer(rsaAesKey, 'base64').toString('binary'), 'RSA-OAEP').toString('base64'));
        assert(server.rtspServer.audioAesIv.toString('base64') === rsaAesIv);
        assert(server.rtspServer.metadata.clientName === 'Stephen\'s iPad');
        done();
      });

      client.connect(port, 'localhost', function() {

        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);
      });
    });

    it('should respond with password required (TODO)', function(done) {

      parser.on('message', function(m) {
        assert(m.statusCode === 200);
        assert(server.rtspServer.audioCodec === codec);
        assert(server.rtspServer.audioAesKey.toString('base64') === helper.rsaPrivateKey.decrypt(new Buffer(rsaAesKey, 'base64').toString('binary'), 'RSA-OAEP').toString('base64'));
        assert(server.rtspServer.audioAesIv.toString('base64') === rsaAesIv);
        done();
      });

      client.connect(port, 'localhost', function() {

        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);
      });

    });

    it('should respond with password validation (TODO)', function(done) {

      parser.on('message', function(m) {
        assert(m.statusCode === 200);
        assert(server.rtspServer.audioCodec === codec);
        assert(server.rtspServer.audioAesKey.toString('base64') === helper.rsaPrivateKey.decrypt(new Buffer(rsaAesKey, 'base64').toString('binary'), 'RSA-OAEP').toString('base64'));
        assert(server.rtspServer.audioAesIv.toString('base64') === rsaAesIv);
        done();
      });

      client.connect(port, 'localhost', function() {

        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);
      });

    });
  });

  describe('SETUP', function() {

    it('should respond with setup acknowledgement (TODO)', function(done) {
      var counter = 0;

      parser.on('message', function(m) {
        counter++;
        assert(m.statusCode === 200);

        // TODO: check client ports saved on server
        if (counter == 1) {
          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');

        } else if (counter == 2) {
          assert(server.rtspServer.ports.length === 3);
          assert(typeof server.rtspServer.ports[0] === 'number');
          assert(typeof server.rtspServer.ports[1] === 'number');
          assert(typeof server.rtspServer.ports[2] === 'number');
          client.destroy();
          done();
        }
      });

      client.connect(port, 'localhost', function() {

        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);
      });

    });
  });

  describe('RECORD', function() {

    it('should respond with record acknowledgement (TODO)', function(done) {

      parser.on('message', function(m) {
        assert(m.statusCode === 200);
        done();
      });

      client.connect(port, 'localhost', function() {

        client.write('RECORD * RTSP/1.0\r\nCSeq:0\r\nUser-Agent: AirPlay/190.9\r\n\r\n');
      });

    });
  });

  describe('FLUSH', function() {

    it('should respond with flush acknowledgement (IMPL TODO)', function(done) {

      parser.on('message', function(m) {
        assert(m.statusCode === 200);
        done();
      });

      client.connect(port, 'localhost', function() {

        client.write('FLUSH * RTSP/1.0\r\nCSeq:0\r\nUser-Agent: AirPlay/190.9\r\n\r\n');
      });

    });
  });

  describe('TEARDOWN', function() {

    it('should respond with teardown acknowledgement (TODO)', function(done) {

      parser.on('message', function(m) {
        assert(m.statusCode === 200);
        done();
      });

      client.connect(port, 'localhost', function() {
        client.write('OPTIONS * RTSP/1.0\r\nCSeq:2\r\nUser-Agent: AirPlay/190.9\r\n\r\n');
      });

    });
  });

  describe('GET_PARAMETER', function() {

    it('should respond with volume parameter (IMPL TODO)', function(done) {

      parser.on('message', function(m) {
        assert(m.statusCode === 200);
        done();
      });

      var content = 'volume';

      client.connect(port, 'localhost', function() {
        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);
      });

    });
  });

  describe('SET_PARAMETER', function() {

    it('should set and acknowledge volume', function(done) {

      server.on('volumeChange', function(volume) {
        assert(volume === -2.25);
        done();
      });

      var content = 'volume: -2.250000';

      client.connect(port, 'localhost', function() {
        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);
      });

    });

    it('should set and acknowedge text metadata (TODO)', function(done) {

      parser.on('message', function(m) {
        assert(m.statusCode === 200);
        done();
      });

      var content = 'bawkbawk';

      client.connect(port, 'localhost', function() {
        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);
      });

    });

    it('should set and acknowedge album metadata (TODO)', function(done) {

      parser.on('message', function(m) {
        assert(m.statusCode === 200);
        done();
      });

      var content = 'bawkbawk';

      client.connect(port, 'localhost', function() {
        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);
      });

    });
  });

});
Download .txt
gitextract_r8tg7z38/

├── .gitignore
├── .jscsrc
├── .travis.yml
├── CHANGELOG.md
├── README.md
├── examples/
│   ├── server.js
│   ├── server_multiple.js
│   └── server_stdout.js
├── gulpfile.js
├── index.js
├── lib/
│   ├── helper.js
│   ├── rtp.js
│   ├── rtsp.js
│   ├── rtspmethods.js
│   ├── server.js
│   └── streams/
│       ├── base.js
│       ├── output.js
│       └── pcm.js
├── package.json
├── private.key
└── test/
    └── rtspmethods.test.js
Download .txt
SYMBOL INDEX (6 symbols across 6 files)

FILE: lib/rtp.js
  function RtpServer (line 9) | function RtpServer(rtspServer) {

FILE: lib/rtsp.js
  function RtspServer (line 12) | function RtspServer(options, external) {

FILE: lib/server.js
  function NodeTunes (line 12) | function NodeTunes(options) {

FILE: lib/streams/base.js
  function BaseDecoderStream (line 9) | function BaseDecoderStream() {

FILE: lib/streams/output.js
  function OutputStream (line 9) | function OutputStream() {

FILE: lib/streams/pcm.js
  function PcmDecoderStream (line 8) | function PcmDecoderStream() {
Condensed preview — 21 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (42K chars).
[
  {
    "path": ".gitignore",
    "chars": 128,
    "preview": "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."
  },
  {
    "path": ".jscsrc",
    "chars": 25,
    "preview": "{\n  \"preset\": \"airbnb\"\n}\n"
  },
  {
    "path": ".travis.yml",
    "chars": 206,
    "preview": "language: node_js\nbefore_install:\n  - sudo apt-get update -qq\n  - sudo apt-get install -y libavahi-compat-libdnssd-dev l"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 1424,
    "preview": "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 v"
  },
  {
    "path": "README.md",
    "chars": 312,
    "preview": "NodeTunes\n=========\n[![build status](https://secure.travis-ci.org/stephen/nodetunes.png)](http://travis-ci.org/stephen/n"
  },
  {
    "path": "examples/server.js",
    "chars": 340,
    "preview": "'use strict';\n\nvar AirTunesServer = require('../index');\nvar Speaker = require('speaker');\n\nvar speaker = new Speaker({\n"
  },
  {
    "path": "examples/server_multiple.js",
    "chars": 663,
    "preview": "'use strict';\n\nvar AirTunesServer = require('../index');\nvar Speaker = require('speaker');\n\nvar server1 = new AirTunesSe"
  },
  {
    "path": "examples/server_stdout.js",
    "chars": 262,
    "preview": "'use strict';\n\nvar AirTunesServer = require('../index');\nvar Speaker = require('speaker');\n\nvar server = new AirTunesSer"
  },
  {
    "path": "gulpfile.js",
    "chars": 318,
    "preview": "var gulp = require('gulp');\nvar mocha = require('gulp-mocha');\nvar jscs = require('gulp-jscs');\n\ngulp.task('test', funct"
  },
  {
    "path": "index.js",
    "chars": 68,
    "preview": "'use strict';\n\nmodule.exports = require(__dirname + '/lib/server');\n"
  },
  {
    "path": "lib/helper.js",
    "chars": 4743,
    "preview": "'use strict';\n\nvar forge = require('node-forge');\nvar rsa = forge.pki.rsa;\n\nvar fs = require('fs');\nvar crypto = require"
  },
  {
    "path": "lib/rtp.js",
    "chars": 1997,
    "preview": "\n'use strict';\n\nvar dgram = require('dgram');\nvar tools = require('./helper');\nvar crypto = require('crypto');\nvar debug"
  },
  {
    "path": "lib/rtsp.js",
    "chars": 2657,
    "preview": "'use strict';\n\nvar net = require('net');\nvar ServerParser = require('httplike');\nvar tools = require('./helper');\nvar Rt"
  },
  {
    "path": "lib/rtspmethods.js",
    "chars": 7789,
    "preview": "'use strict';\n\nvar tools = require('./helper');\nvar ipaddr = require('ipaddr.js');\nvar randomstring = require('randomstr"
  },
  {
    "path": "lib/server.js",
    "chars": 3234,
    "preview": "'use strict';\n\nvar mdns = require('mdns');\nvar net = require('net');\nvar portastic = require('portastic');\nvar randomMac"
  },
  {
    "path": "lib/streams/base.js",
    "chars": 1031,
    "preview": "'use strict';\n\nmodule.exports = BaseDecoderStream;\n\nvar Readable = require('readable-stream').Readable;\nvar PriorityQueu"
  },
  {
    "path": "lib/streams/output.js",
    "chars": 573,
    "preview": "'use strict';\n\nmodule.exports = OutputStream;\n\nvar PassThrough = require('readable-stream').PassThrough;\nvar BaseStream "
  },
  {
    "path": "lib/streams/pcm.js",
    "chars": 524,
    "preview": "'use strict';\n\nmodule.exports = PcmDecoderStream;\n\nvar Transform = require('readable-stream').Transform;\nvar util = requ"
  },
  {
    "path": "package.json",
    "chars": 1058,
    "preview": "{\n  \"name\": \"nodetunes\",\n  \"version\": \"0.3.0\",\n  \"author\": \"Stephen Wan <stephen@stephenwan.net>\",\n  \"description\": \"Air"
  },
  {
    "path": "private.key",
    "chars": 1674,
    "preview": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEA59dE8qLieItsH1WgjrcFRKj6eUWqi+bGLOX1HL3U3GhC/j0Qg90u3sG/1CUt\nwC5vOYvfDmF"
  },
  {
    "path": "test/rtspmethods.test.js",
    "chars": 10594,
    "preview": "var net = require('net');\nvar assert = require('assert');\nvar Nodetunes = require('../index');\nvar Parser = require('htt"
  }
]

About this extraction

This page contains the full source code of the stephen/nodetunes GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 21 files (38.7 KB), approximately 12.1k tokens, and a symbol index with 6 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!