diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..dab7ba1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = tab +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = false +insert_final_newline = true + +[*.js] +indent_style = space +indent_size = 2 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..315c4e4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +.DS_* +lib-cov +*.seed +*.log +*.csv +*.dat +*.out +*.pid +*.gz +.sass-cache +.tmp +.env + +pids +logs +results + +npm-debug.log +node_modules +/working/ +config.json + +bower_components +www +/datasets/ +/build/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..73f4b68 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014 Nathan Wittstock + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..b8b48e2 --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# chromecast-osx-audio v0.0.1 + +Streams Mac OS X audio input to a local Chromecast device. + +## Installation + +To install the module for use in your projects: + +```bash +npm install -g chromecast-osx-audio +``` + +## Usage + +Pipe input to a writeable file stream: + +```js +var fs = require('fs'); +var audio = require('osx-audio'); + +var input = audio.createReadStream(); + +var writable = fs.createWriteStream('output.txt'); +input.pipe(writable); +``` + +### Options + +None yet. + +## Environment Variables + +None yet. + +## Known Issues + +Too early to know. + +## Contributing + +Feel free to send pull requests! I'm not picky, but would like the following: + +1. Write tests for any new features, and do not break existing tests. +2. Be sure to point out any changes that break API. + +## History + +- **v0.0.1** +Initial Release. + +## The MIT License (MIT) + +Copyright (c) 2014 Nathan Wittstock + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/bin/chromecast.js b/bin/chromecast.js new file mode 100755 index 0000000..841104c --- /dev/null +++ b/bin/chromecast.js @@ -0,0 +1,28 @@ +#!/usr/bin/env node +/** + * chromecast commandline utility. + * + * @since 0.0.1 + */ +'use strict'; + +var fs = require('fs'); +var chalk = require('chalk'); +var error = chalk.bold.red; +var Cli = require('../lib/cli.js'); + +var cli = new Cli().parse(process.argv.slice(2), function(err, message, options) { + if (err) { + console.error(error('\nYou had errors in your syntax. Use --help for further information.')); + err.forEach(function (e) { + console.error(e.message); + }); + } + else if (message) { + console.log(message); + } + else { + var chromecast = require('../')(options); + } +}); + diff --git a/index.js b/index.js new file mode 100644 index 0000000..ee32e96 --- /dev/null +++ b/index.js @@ -0,0 +1,111 @@ +var Chromecast = function(options) { + + var lame = require('lame'); + var audio = require('osx-audio'); + var fs = require('fs'); + + // we need to get the address of the local interface + var ip = null; + var interfaces = require('os').networkInterfaces(); + for (dev in interfaces) { + interfaces[dev].forEach(function(a) { + if (a.family === 'IPv4' && a.internal === false) { + ip = a.address; + } + }); + } + + // create the Encoder instance + var encoder = new lame.Encoder({ + // input + channels: 2, // 2 channels (left and right) + bitDepth: 16, // 16-bit samples + sampleRate: 44100, // 44,100 Hz sample rate + + // output + bitRate: options.bitrate, + outSampleRate: options.samplerate, + mode: (options.mono ? lame.MONO : lame.STEREO) // STEREO (default), JOINTSTEREO, DUALCHANNEL or MONO + }); + + var input = audio.createReadStream(); + input.pipe(encoder); + + // set up an express app + var express = require('express') + var app = express() + + app.get('/', function(req, res) { + res.send('Nope.'); + }); + + app.get('/stream.mp3', function (req, res) { + res.set({ + 'Content-Type': 'audio/mpeg3', + 'Transfer-Encoding': 'chunked' + }); + encoder.pipe(res); + }); + + var server = app.listen(options.port); + + + var Client = require('castv2-client').Client; + var DefaultMediaReceiver = require('castv2-client').DefaultMediaReceiver; + var mdns = require('mdns'); + + var browser = mdns.createBrowser(mdns.tcp('googlecast')); + + browser.on('serviceUp', function(service) { + console.log('found device "%s" at %s:%d', service.name, service.addresses[0], service.port); + ondeviceup(service.addresses[0]); + browser.stop(); + }); + + browser.start(); + + function ondeviceup(host) { + + var client = new Client(); + + client.connect(host, function() { + console.log('connected, launching app ...'); + + client.launch(DefaultMediaReceiver, function(err, player) { + var media = { + + // Here you can plug an URL to any mp4, webm, mp3 or jpg file with the proper contentType. + contentId: 'http://' + ip + ':' + server.address().port + '/stream.mp3', + contentType: 'audio/mpeg3', + streamType: 'LIVE', // or LIVE + + // Title and cover displayed while buffering + metadata: { + type: 0, + metadataType: 0, + title: options.name + } + }; + + player.on('status', function(status) { + console.log('status broadcast playerState=%s', status.playerState); + }); + + console.log('app "%s" launched, loading media %s ...', player.session.displayName, media.contentId); + + player.load(media, { autoplay: true }, function(err, status) { + console.log('media loaded playerState=%s', status.playerState); + }); + }); + + }); + + client.on('error', function(err) { + console.log('Error: %s', err.message); + client.close(); + }); + + } +} + +module.exports = Chromecast; diff --git a/lib/cli.js b/lib/cli.js new file mode 100644 index 0000000..09a4e42 --- /dev/null +++ b/lib/cli.js @@ -0,0 +1,101 @@ +'use strict'; + +var fs = require('fs'); +var chalk = require('chalk'); +var parseArgs = require('minimist'); + +// Number.isInteger() polyfill :: +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger +if (!Number.isInteger) { + Number.isInteger = function isInteger (nVal) { + return typeof nVal === "number" && isFinite(nVal) && nVal > -9007199254740992 && nVal < 9007199254740992 && Math.floor(nVal) === nVal; + }; +} + +var cli = function(options) { + this.options = { + alias: { + port: 'p', + bitrate: 'b', + mono: 'm', + samplerate: 's', + name: 'n', + version: 'v', + help: 'h' + }, + default: { + port: 3000, + bitrate: 192, + samplerate: 44100, + name: 'Chrome OSX Audio Stream' + }, + 'boolean': ['version', 'help', 'mono'], + 'string': ['name'], + 'integer': ['port', 'bitrate', 'samplerate'] + }; + + this.errors = []; + this.message = null; + + this.helpMessage = [ + chalk.bold.blue("Usage: chromecast [options]"), + "", + "Options:", + " -p, --port The port that the streaming server will listen on. [3000]", + " -b, --bitrate The bitrate for the mp3 encoded stream. [192]", + " -m, --mono The stream defaults to stereo. Set to mono with this flag.", + " -s, --samplerate The sample rate for the mp3 encoded stream [44100]", + " -n, --name A name for the server to report itself as. [Chrome OSX Audio Stream]", + " --version print version and exit" + ]; + + return this; +}; + +cli.prototype.parse = function(argv, next) { + var options = parseArgs(argv, this.options); + + if (options.version) { + var pkg = require('../package.json'); + this.message = "version " + pkg.version; + } + else if (options.help) { + this.message = this.helpMessage.join('\n'); + } + else { + /* + * Options are processed in a significant order; we only save the last error + * message, so we'll want to make sure the most significant are last + */ + + // ensure that parameter-expecting options have parameters + this.options['string'].forEach(function(i) { + if(typeof options[i] !== 'undefined') { + if (typeof options[i] !== 'string' || options[i].length < 1) { + this.errors.push(new Error(i + " expects a value.")); + } + } + }.bind(this)); + + // ensure that number-expecting options have parameters + this.options['integer'].forEach(function(i) { + if(typeof options[i] !== 'undefined') { + if (!Number.isInteger(options[i])) { + this.errors.push(new Error(i + " expects an integer value.")); + } + } + }.bind(this)); + } + + this.parsedOptions = options; + + if (typeof next === 'function') { + // we return the array of errors if there are any, otherwise null + next(this.errors.length > 0 ? this.errors : null, this.message, options); + } + + return this; +}; + + +module.exports = cli; diff --git a/package.json b/package.json new file mode 100644 index 0000000..5c663c9 --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "chromecast-osx-audio", + "version": "0.0.0", + "description": "Stream OS X audio input to a local Chromecast device.", + "preferGlobal": true, + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "bin": { + "chromecast": "./bin/chromecast.js" + }, + "os": [ + "darwin" + ], + "author": "Nathan Wittstock ", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/fardog/node-chromecast-osx-audio.git" + }, + "keywords": [ + "audio", + "sound", + "streaming", + "osx", + "chromecast" + ], + "dependencies": { + "castv2-client": "0.0.2", + "chalk": "^0.5.1", + "express": "^4.9.3", + "lame": "^1.1.1", + "mdns": "^2.2.0", + "minimist": "^1.1.0", + "osx-audio": "0.0.2" + } +}