├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── bin └── webcast.js ├── index.js ├── lib ├── cli.js ├── index.js └── utils.js └── package.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = false 10 | insert_final_newline = true 11 | 12 | [*.js] 13 | indent_style = space 14 | indent_size = 2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_* 2 | lib-cov 3 | *.seed 4 | *.log 5 | *.csv 6 | *.dat 7 | *.out 8 | *.pid 9 | *.gz 10 | .sass-cache 11 | .tmp 12 | .env 13 | 14 | pids 15 | logs 16 | results 17 | 18 | npm-debug.log 19 | node_modules 20 | /working/ 21 | config.json 22 | 23 | bower_components 24 | www 25 | /datasets/ 26 | /build/ 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Nathan Wittstock 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webcast-osx-audio 2 | 3 | Streams Mac OS X audio input an mp3 webcast. 4 | 5 | ## Installation 6 | 7 | To install the module for use in your projects: 8 | 9 | ```bash 10 | npm install -g webcast-osx-audio 11 | ``` 12 | 13 | ## Usage 14 | 15 | Global installation exposes the `webcast-audio` command to your shell. Running 16 | this command will start listening to input, and connect to a local Chromecast 17 | with a stream of that input. 18 | 19 | To direct system audio, use [Soundflower][soundflower]. 20 | 21 | ```bash 22 | $ webcast-audio --help 23 | 24 | Usage: webcast-audio [options] 25 | 26 | Options: 27 | -p, --port The port that the streaming server will listen on. [3000] 28 | -b, --bitrate The bitrate for the mp3 encoded stream. [192] 29 | -m, --mono The stream defaults to stereo. Set to mono with this flag. 30 | -s, --samplerate The sample rate for the mp3 encoded stream. [44100] 31 | -u, --url The relative URL that the stream will be hosted at. [stream.mp3] 32 | -i, --iface The public interface that should be reported. Selects the first interface by default. 33 | --version print version and exit 34 | ``` 35 | 36 | ## Known Issues 37 | 38 | - The interface option is only for reporting the IP address that we're listening 39 | on, although we're actually listening on all interfaces. 40 | 41 | ## Contributing 42 | 43 | Feel free to send pull requests! I'm not picky, but would like the following: 44 | 45 | 1. Write tests for any new features, and do not break existing tests. 46 | 2. Be sure to point out any changes that break API. 47 | 48 | ## History 49 | 50 | - **v1.0.0** 51 | Uses latest [osx-audio][] 52 | 53 | - **v0.1.3** 54 | Rewrites stream handling, fixes issues with a wrong interface being selected, 55 | and fixes the `--iface` option causing an error. 56 | 57 | - **v0.1.2** 58 | Handles piping of lame audio differently, to avoid some skipping. 59 | 60 | - **v0.1.1** 61 | Updates osx-audio dependency. 62 | 63 | - **v0.1.0** 64 | First release. Splits from [chromecast-osx-audio][]. 65 | 66 | ## The MIT License (MIT) 67 | 68 | Copyright (c) 2014 Nathan Wittstock 69 | 70 | Permission is hereby granted, free of charge, to any person obtaining a copy of 71 | this software and associated documentation files (the "Software"), to deal in 72 | the Software without restriction, including without limitation the rights to 73 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 74 | the Software, and to permit persons to whom the Software is furnished to do so, 75 | subject to the following conditions: 76 | 77 | The above copyright notice and this permission notice shall be included in all 78 | copies or substantial portions of the Software. 79 | 80 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 81 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 82 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 83 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 84 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 85 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 86 | 87 | [soundflower]: http://rogueamoeba.com/freebies/soundflower/ 88 | [chromecast-osx-audio]: https://github.com/fardog/node-chromecast-osx-audio 89 | [osx-audio]: https://www.npmjs.com/package/osx-audio 90 | -------------------------------------------------------------------------------- /bin/webcast.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * chromecast commandline utility. 4 | * 5 | * @since 0.0.1 6 | */ 7 | 'use strict'; 8 | 9 | var fs = require('fs'); 10 | var chalk = require('chalk'); 11 | var error = chalk.bold.red; 12 | var Cli = require('../lib/cli.js'); 13 | 14 | var cli = new Cli().parse(process.argv.slice(2), function(err, message, options) { 15 | if (err) { 16 | console.error(error('\nYou had errors in your syntax. Use --help for further information.')); 17 | err.forEach(function (e) { 18 | console.error(e.message); 19 | }); 20 | } 21 | else if (message) { 22 | console.log(message); 23 | } 24 | else { 25 | var chromecast = require('../')(options); 26 | } 27 | }); 28 | 29 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib'); 2 | -------------------------------------------------------------------------------- /lib/cli.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var chalk = require('chalk'); 5 | var parseArgs = require('minimist'); 6 | 7 | // Number.isInteger() polyfill :: 8 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger 9 | if (!Number.isInteger) { 10 | Number.isInteger = function isInteger (nVal) { 11 | return typeof nVal === "number" && isFinite(nVal) && nVal > -9007199254740992 && nVal < 9007199254740992 && Math.floor(nVal) === nVal; 12 | }; 13 | } 14 | 15 | var cli = function(options) { 16 | this.options = { 17 | alias: { 18 | port: 'p', 19 | bitrate: 'b', 20 | mono: 'm', 21 | samplerate: 's', 22 | url: 'u', 23 | iface: 'i', 24 | version: 'v', 25 | help: 'h' 26 | }, 27 | default: { 28 | port: 3000, 29 | bitrate: 192, 30 | samplerate: 44100, 31 | url: 'stream.mp3' 32 | }, 33 | 'boolean': ['version', 'help', 'mono'], 34 | 'string': ['url'], 35 | 'integer': ['port', 'bitrate', 'samplerate'] 36 | }; 37 | 38 | this.errors = []; 39 | this.message = null; 40 | 41 | this.helpMessage = [ 42 | chalk.bold.blue("Usage: webcast-audio [options]"), 43 | "", 44 | "Options:", 45 | " -p, --port The port that the streaming server will listen on. [3000]", 46 | " -b, --bitrate The bitrate for the mp3 encoded stream. [192]", 47 | " -m, --mono The stream defaults to stereo. Set to mono with this flag.", 48 | " -s, --samplerate The sample rate for the mp3 encoded stream. [44100]", 49 | " -u, --url The relative URL that the stream will be hosted at. [stream.mp3]", 50 | " -i, --iface The public interface that should be reported. Selects the first interface by default.", 51 | " --version print version and exit" 52 | ]; 53 | 54 | return this; 55 | }; 56 | 57 | cli.prototype.parse = function(argv, next) { 58 | var options = parseArgs(argv, this.options); 59 | 60 | if (options.version) { 61 | var pkg = require('../package.json'); 62 | this.message = "version " + pkg.version; 63 | } 64 | else if (options.help) { 65 | this.message = this.helpMessage.join('\n'); 66 | } 67 | else { 68 | /* 69 | * Options are processed in a significant order; we only save the last error 70 | * message, so we'll want to make sure the most significant are last 71 | */ 72 | 73 | // ensure that parameter-expecting options have parameters 74 | this.options['string'].forEach(function(i) { 75 | if(typeof options[i] !== 'undefined') { 76 | if (typeof options[i] !== 'string' || options[i].length < 1) { 77 | this.errors.push(new Error(i + " expects a value.")); 78 | } 79 | } 80 | }.bind(this)); 81 | 82 | // ensure that number-expecting options have parameters 83 | this.options['integer'].forEach(function(i) { 84 | if(typeof options[i] !== 'undefined') { 85 | if (!Number.isInteger(options[i])) { 86 | this.errors.push(new Error(i + " expects an integer value.")); 87 | } 88 | } 89 | }.bind(this)); 90 | } 91 | 92 | this.parsedOptions = options; 93 | 94 | if (typeof next === 'function') { 95 | // we return the array of errors if there are any, otherwise null 96 | next(this.errors.length > 0 ? this.errors : null, this.message, options); 97 | } 98 | 99 | return this; 100 | }; 101 | 102 | 103 | module.exports = cli; 104 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var utils = require('./utils'); 3 | var express = require('express') 4 | var lame = require('lame'); 5 | var audio = require('osx-audio'); 6 | var debug = require('debug')('webcast'); 7 | 8 | var Webcast = function(options) { 9 | if (!(this instanceof Webcast)) { 10 | return new Webcast(options); 11 | } 12 | 13 | this.options = options; 14 | this.lameOptions = { 15 | // input 16 | channels: 2, // 2 channels (left and right) 17 | bitDepth: 16, // 16-bit samples 18 | sampleRate: 44100, // 44,100 Hz sample rate 19 | 20 | // output 21 | bitRate: options.bitrate, 22 | outSampleRate: options.samplerate, 23 | mode: (options.mono ? lame.MONO : lame.STEREO) // STEREO (default), JOINTSTEREO, DUALCHANNEL or MONO 24 | }; 25 | 26 | // we need to get the address of the local interface 27 | this.ip = utils.getLocalIp(options.iface); 28 | 29 | // create the Encoder instance 30 | this.input = null; 31 | this.encoder = new lame.Encoder(this.lameOptions); 32 | this.encoder.on('data', this.sendChunk.bind(this)); 33 | 34 | // listeners 35 | this.listeners = []; 36 | 37 | // set up an express app 38 | this.app = express() 39 | 40 | var count = 0; 41 | 42 | var self = this; 43 | this.app.get('/' + options.url, function (req, res) { 44 | res.set({ 45 | 'Content-Type': 'audio/mpeg3', 46 | 'Transfer-Encoding': 'chunked' 47 | }); 48 | 49 | if (!self.input) { 50 | debug("no input exists yet, creating"); 51 | self.input = new audio.Input(); 52 | self.input.pipe(self.encoder); 53 | } 54 | 55 | self.addListener(res); 56 | 57 | var onEnd = function() { 58 | self.removeListener(res); 59 | } 60 | 61 | res.on('close', onEnd); 62 | res.on('finish', onEnd); 63 | }); 64 | 65 | this.server = this.app.listen(options.port); 66 | 67 | console.log("streaming at http://" + this.ip + ":" + options.port + "/" + options.url); 68 | 69 | return this; 70 | }; 71 | 72 | Webcast.prototype.addListener = function(res) { 73 | debug("adding listener"); 74 | this.listeners.push(res); 75 | }; 76 | 77 | Webcast.prototype.removeListener = function(res) { 78 | var idx = this.listeners.indexOf(res); 79 | this.listeners.splice(idx, 1); 80 | debug("removed listener. " + this.listeners.length + " are left."); 81 | }; 82 | 83 | Webcast.prototype.sendChunk = function(chunk) { 84 | var self = this; 85 | self.listeners.forEach(function(listener) { 86 | listener.write(chunk); 87 | }); 88 | }; 89 | 90 | module.exports = Webcast; 91 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | var interfaces = require('os').networkInterfaces(); 2 | 3 | var utils = {}; 4 | 5 | utils.getLocalIp = function(device) { 6 | var ip = null; 7 | if (device) { 8 | var dv = {}; 9 | dv[device] = null; 10 | } 11 | for (dev in (dv ? dv : interfaces)) { 12 | interfaces[dev].forEach(function(a) { 13 | if (a.family === 'IPv4' && a.internal === false && !ip) { 14 | ip = a.address; 15 | } 16 | }); 17 | } 18 | 19 | return ip; 20 | }; 21 | 22 | module.exports = utils; 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webcast-osx-audio", 3 | "version": "1.0.1", 4 | "description": "Stream OS X audio input to an mp3 webcast.", 5 | "preferGlobal": true, 6 | "main": "index.js", 7 | "engines": { 8 | "node": ">=0.10.30" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "bin": { 14 | "webcast-audio": "./bin/webcast.js" 15 | }, 16 | "os": [ 17 | "darwin" 18 | ], 19 | "author": "Nathan Wittstock ", 20 | "license": "MIT", 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/fardog/node-webcast-osx-audio.git" 24 | }, 25 | "keywords": [ 26 | "audio", 27 | "sound", 28 | "streaming", 29 | "osx" 30 | ], 31 | "dependencies": { 32 | "chalk": "^0.5.1", 33 | "debug": "^2.0.0", 34 | "express": "^4.9.3", 35 | "lame": "^1.1.1", 36 | "minimist": "^1.1.0", 37 | "osx-audio": "^1.0.0" 38 | } 39 | } 40 | --------------------------------------------------------------------------------