├── .gitignore ├── LICENSE ├── README.md ├── api.js ├── index.js ├── package.json └── timelineHelper.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | examples/ 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Simon Kusterer 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chromecast-player 2 | 3 | A simple chromecast player. 4 | Relies on the [castv2-client](https://github.com/thibauts/node-castv2-client) lib 5 | from thibauts, all credits go to him. 6 | 7 | ### Usage Samples 8 | 9 | Start Playback of some video file: 10 | 11 | ```js 12 | const player = require('chromecast-player')(); 13 | const media = 'http://commondatastorage.googleapis.com/gtv-videos-bucket/ED_1280.mp4'; 14 | 15 | player.launch(media, (err, p) => { 16 | p.once('playing', () => { 17 | console.log('playback has started.'); 18 | }); 19 | }); 20 | ``` 21 | 22 | Attach to a currently playing session: 23 | 24 | ```javascript 25 | const player = require('chromecast-player')(); 26 | 27 | player.attach((err, p) => { 28 | p.pause(); 29 | }); 30 | ``` 31 | 32 | ### Installation 33 | 34 | ```bash 35 | npm install chromecast-player --save 36 | ``` 37 | 38 | ## License 39 | Copyright (c) 2014 Simon Kusterer 40 | Licensed under the MIT license. 41 | -------------------------------------------------------------------------------- /api.js: -------------------------------------------------------------------------------- 1 | var castv2Cli = require('castv2-client'); 2 | var inherits = require('util').inherits; 3 | var Application = castv2Cli.Application; 4 | var RequestResponseController = castv2Cli.RequestResponseController; 5 | var extend = require('xtend'); 6 | var debug = require('debug')('chromecast-player:api'); 7 | var timelineHelper = require('./timelineHelper'); 8 | var noop = function() {}; 9 | var slice = Array.prototype.slice; 10 | 11 | var Api = function(client, session) { 12 | var that = this; 13 | Application.apply(this, arguments); 14 | this.reqres = this.createController(RequestResponseController, 15 | 'urn:x-cast:com.google.cast.media'); 16 | 17 | var onMessage = function(response, broadcast) { 18 | if (response.type !== 'MEDIA_STATUS' || 19 | !broadcast || 20 | !response.status || 21 | !response.status.length || 22 | !response.status[0]) { 23 | return; 24 | } 25 | 26 | var status = response.status[0]; 27 | that.currentSession = status; 28 | that.emit(status.playerState.toLowerCase(), status); 29 | that.emit('status', status); 30 | }; 31 | 32 | var onClose = function() { 33 | debug('API close'); 34 | that.reqres.removeListener('message', onMessage); 35 | that.reqres.removeListener('close', onClose); 36 | that.removeListener('close', onClose); 37 | if (that.client) { 38 | that.client.removeListener('close', onClose); 39 | } 40 | that.tlHelper.removeListener('position', onPosition); 41 | that.emit('closed'); 42 | }; 43 | 44 | this.reqres.on('message', onMessage); 45 | this.reqres.on('close', onClose); 46 | this.client.on('close', onClose); 47 | this.on('close', onClose); 48 | 49 | this.tlHelper = timelineHelper(this); 50 | 51 | var onPosition = function(pos) { 52 | that.emit('position', pos); 53 | }; 54 | 55 | this.tlHelper.on('position', onPosition); 56 | }; 57 | 58 | Api.APP_ID = 'CC1AD845'; 59 | 60 | inherits(Api, Application); 61 | 62 | Api.prototype.getStatus = function(cb) { 63 | var that = this; 64 | this.reqres.request({ type: 'GET_STATUS' }, 65 | function(err, response) { 66 | if(err) return callback(err); 67 | var status = response.status[0]; 68 | that.currentSession = status; 69 | cb(null, status); 70 | } 71 | ); 72 | }; 73 | 74 | Api.prototype.updateStatus = function(cb) { 75 | var that = this; 76 | cb = cb || noop; 77 | this.getStatus(function(err, status) { 78 | if (status) { 79 | that.emit(status.playerState.toLowerCase(), status); 80 | that.emit('status', status); 81 | } 82 | cb(err, status); 83 | }); 84 | }; 85 | 86 | Api.prototype.load = function(opts, cb) { 87 | var options = { 88 | type: 'LOAD', 89 | autoplay: opts.autoplay, 90 | currentTime: opts.startTime, 91 | activeTrackIds: opts.activeTrackIds 92 | }; 93 | var media = extend({ 94 | contentId: opts.path, 95 | contentType: opts.type, 96 | streamType: opts.streamType 97 | }, opts.media); 98 | options.media = media; 99 | 100 | this.reqres.request(options, 101 | function(err, response) { 102 | if(err) return cb(err); 103 | if(response.type === 'LOAD_FAILED') { 104 | return cb(new Error('Load failed')); 105 | } 106 | cb(null, response.status[0]); 107 | } 108 | ); 109 | }; 110 | 111 | Api.prototype.getCurrentSession = function(cb) { 112 | if (this.currentSession) return cb(null, this.currentSession); 113 | this.getStatus(function(err, status) { 114 | if (err) return cb(err); 115 | cb(null, status); 116 | }); 117 | }; 118 | 119 | Api.prototype.sessionRequest = function(data, cb) { 120 | var that = this; 121 | cb = cb || noop; 122 | this.getCurrentSession(function(err, session) { 123 | if (err) return cb(err); 124 | if (!session) return cb(new Error('session not found')); 125 | var sessionId = session.mediaSessionId; 126 | that.reqres.request(extend(data, { mediaSessionId: sessionId } ), 127 | function(err, response) { 128 | if(err) return cb(err); 129 | cb(null, response.status[0]); 130 | } 131 | ); 132 | }); 133 | }; 134 | 135 | // create a back-reference to the platform 136 | // needed for some api methods. 137 | Api.prototype.setPlatform = function(platform) { 138 | this.platform = platform; 139 | }; 140 | 141 | Api.prototype.play = function(cb) { 142 | this.sessionRequest({ type: 'PLAY' }, cb); 143 | }; 144 | 145 | Api.prototype.pause = function(cb) { 146 | this.sessionRequest({ type: 'PAUSE' }, cb); 147 | }; 148 | 149 | Api.prototype.stop = function(cb) { 150 | this.sessionRequest({ type: 'STOP' }, cb); 151 | }; 152 | 153 | Api.prototype.seek = function(currentTime, cb) { 154 | this.sessionRequest({ 155 | type: 'SEEK', 156 | currentTime: currentTime 157 | }, cb); 158 | }; 159 | 160 | // volume can be a number between 0 and 1 161 | Api.prototype.setVolume = function(volume, cb) { 162 | this.platform.setVolume({ level: volume }, cb || noop); 163 | }; 164 | 165 | Api.prototype.getVolume = function(cb) { 166 | this.platform.getVolume(cb || noop); 167 | }; 168 | 169 | Api.prototype.mute = function(cb) { 170 | this.platform.setVolume({ muted: true }, cb || noop); 171 | }; 172 | 173 | Api.prototype.unmute = function(cb) { 174 | this.platform.setVolume({ muted: false }, cb || noop); 175 | }; 176 | 177 | Api.prototype.getPosition = function() { 178 | return this.tlHelper.getPosition(); 179 | }; 180 | 181 | Api.prototype.getProgress = function() { 182 | return this.tlHelper.getProgress(); 183 | }; 184 | 185 | module.exports = Api; 186 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Client = require('castv2-client').Client; 2 | var scanner = require('chromecast-scanner'); 3 | var ware = require('ware'); 4 | var mutate = require('mutate.js'); 5 | var inherits = require('util').inherits; 6 | var ee = require('events').EventEmitter; 7 | var extend = require('xtend'); 8 | var debug = require('debug')('chromecast-player'); 9 | var Promise = require('promiscuous'); 10 | var api = require('./api'); 11 | var noop = function() {}; 12 | var slice = Array.prototype.slice; 13 | 14 | var defaults = { 15 | autoplay: true, 16 | ttl: 10000, 17 | startTime: 0, 18 | streamType: 'BUFFERED', 19 | activeTrackIds: [], 20 | media: {}, 21 | cb: noop 22 | }; 23 | 24 | var apirize = function(fn, ctx) { 25 | return mutate(function(opts) { 26 | opts = opts || {}; 27 | if (opts._opts) { 28 | opts = extend(opts, opts._opts); 29 | delete opts._opts; 30 | } 31 | fn.call(ctx, opts); 32 | }) 33 | .method(['function'], ['cb']) 34 | .method(['object', 'function'], ['_opts', 'cb']) 35 | .method(['string'], ['path']) 36 | .method(['string', 'function'], ['path', 'cb']) 37 | .method(['string', 'object'], ['path', '_opts']) 38 | .method(['string', 'object', 'function'], ['path', '_opts', 'cb']) 39 | .method(['string', 'string'], ['path', 'type']) 40 | .method(['string', 'string', 'function'], ['path', 'type', 'cb']) 41 | .method(['string', 'string', 'object'], ['path', 'type', '_opts']) 42 | .method(['string', 'string', 'object', 'function'], ['path', 'type', '_opts', 'cb']) 43 | .close(); 44 | }; 45 | 46 | var shutdown = function() { 47 | debug('shutdown'); 48 | if (this.client && !this.clientClosed) { 49 | this.client.close(); 50 | this.clientClosed = true; 51 | } 52 | if (this.player && !this.playerClosed) { 53 | this.player.close(); 54 | this.playerClosed = true; 55 | } 56 | this.inst._setStatus(this, 'closed'); 57 | this.emit('closed'); 58 | }; 59 | 60 | var player = function() { 61 | if (!(this instanceof player)) return new player(); 62 | ee.call(this); 63 | this.mw = ware(); 64 | this.use = this.mw.use.bind(this.mw); 65 | 66 | this.launch = apirize(function(opts) { 67 | var that = this; 68 | var ctx = new ee(); 69 | ctx.mode = 'launch'; 70 | ctx.options = opts; 71 | ctx.api = api; 72 | ctx.shutdown = shutdown; 73 | ctx.inst = this; 74 | this.mw.run(ctx, function(err, ctx) { 75 | ctx.options = extend(defaults, ctx.options); 76 | if (err) return ctx.options.cb(err); 77 | that._setStatus(ctx, 'loading plugins'); 78 | that._scan(ctx) 79 | .then(function(ctx) { return that._connect(ctx); }) 80 | .then(function(ctx) { return that._launch(ctx); }) 81 | .then(function(ctx) { return that._load(ctx); }) 82 | .then(function(ctx) { return that._status(ctx); }) 83 | .then(function(ctx) { ctx.options.cb(null, ctx.player, ctx); }, 84 | function(err) { ctx.options.cb(err); }); 85 | }); 86 | }, this); 87 | 88 | this.attach = apirize(function(opts) { 89 | var that = this; 90 | var ctx = new ee(); 91 | ctx.mode = 'attach'; 92 | ctx.options = opts; 93 | ctx.api = api; 94 | ctx.shutdown = shutdown; 95 | ctx.inst = this; 96 | that._setStatus(ctx, 'loading plugins'); 97 | this.mw.run(ctx, function(err, opts) { 98 | ctx.options = extend(defaults, ctx.options); 99 | if (err) return ctx.options.cb(err); 100 | that._scan(ctx) 101 | .then(function(ctx) { return that._connect(ctx); }) 102 | .then(function(ctx) { return that._find(ctx); }) 103 | .then(function(ctx) { return that._join(ctx); }) 104 | .then(function(ctx) { return that._status(ctx); }) 105 | .then(function(ctx) { ctx.options.cb(null, ctx.player, ctx); }, 106 | function(err) { ctx.options.cb(err); }); 107 | }); 108 | }, this); 109 | }; 110 | 111 | inherits(player, ee); 112 | 113 | // find chromecast devices in the network and 114 | // either return the first found or the one 115 | // which matches device. 116 | player.prototype._scan = function(ctx) { 117 | this._setStatus(ctx, 'scanning'); 118 | return new Promise(function(resolve, reject) { 119 | if (ctx.options.address) { 120 | ctx.address = ctx.options.address; 121 | return resolve(ctx); 122 | } 123 | scanner({ 124 | name: ctx.options.device ? ctx.options.device + '.local' : null, 125 | ttl: ctx.options.ttl, 126 | }, 127 | function(err, service) { 128 | if (err) return reject(err); 129 | ctx.address = service.data; 130 | resolve(ctx); 131 | } 132 | ); 133 | }); 134 | }; 135 | 136 | // establish a connection to a chromecast device 137 | player.prototype._connect = function(ctx) { 138 | this._setStatus(ctx, 'connecting'); 139 | return new Promise(function(resolve, reject) { 140 | var client = new Client(); 141 | client.connect(ctx.address, function() { 142 | ctx.client = client; 143 | resolve(ctx); 144 | }); 145 | var onError = function(err) { 146 | debug('client error %o', err); 147 | client.removeListener('error', onError); 148 | ctx.shutdown(); 149 | }; 150 | var onClose = function() { 151 | debug('client onClose'); 152 | client.client.removeListener('close', onClose); 153 | client.removeListener('error', onError); 154 | ctx.clientClosed = true; 155 | }; 156 | client.client.on('close', onClose); 157 | client.on('error', onError); 158 | }); 159 | }; 160 | 161 | // find running app 162 | player.prototype._find = function(ctx) { 163 | this._setStatus(ctx, 'finding'); 164 | return new Promise(function(resolve, reject) { 165 | ctx.client.getSessions(function(err, apps) { 166 | if (err) return reject(err); 167 | if (!apps.length) return reject(new Error('app not found')); 168 | ctx.session = apps[0]; 169 | resolve(ctx); 170 | }); 171 | }); 172 | }; 173 | 174 | // join an existing chromecast session 175 | player.prototype._join = function(ctx) { 176 | var that = this; 177 | this._setStatus(ctx, 'joining'); 178 | return new Promise(function(resolve, reject) { 179 | ctx.client.join(ctx.session, ctx.api, 180 | function(err, p) { 181 | if (err) return reject(err); 182 | if (p.setPlatform) p.setPlatform(ctx.client); 183 | that._setStatus(ctx, 'ready'); 184 | ctx.player = p; 185 | var onStatus = function(status) { 186 | that._setStatus(ctx, status.playerState.toLowerCase()); 187 | }; 188 | var onClosed = function() { 189 | debug('_join player onClosed'); 190 | ctx.player.removeListener('status', onStatus); 191 | ctx.player.removeListener('closed', onClosed); 192 | ctx.playerClosed = true; 193 | ctx.shutdown(); 194 | }; 195 | ctx.player.on('status', onStatus); 196 | ctx.player.on('closed', onClosed); 197 | resolve(ctx); 198 | } 199 | ); 200 | }); 201 | }; 202 | 203 | // fetch the current state of the player 204 | player.prototype._status = function(ctx) { 205 | var that = this; 206 | return new Promise(function(resolve, reject) { 207 | ctx.player.updateStatus(function(err) { 208 | if (err) return reject(err); 209 | resolve(ctx); 210 | }); 211 | }); 212 | }; 213 | 214 | // launch an application 215 | player.prototype._launch = function(ctx) { 216 | var that = this; 217 | this._setStatus(ctx, 'launching'); 218 | return new Promise(function(resolve, reject) { 219 | ctx.client.launch(ctx.api, function(err, p) { 220 | if (err) return reject(err); 221 | if (p.setPlatform) p.setPlatform(ctx.client); 222 | ctx.player = p; 223 | resolve(ctx); 224 | }); 225 | }); 226 | }; 227 | 228 | // load a media file 229 | player.prototype._load = function(ctx) { 230 | var that = this; 231 | this._setStatus(ctx, 'loading'); 232 | return new Promise(function(resolve, reject) { 233 | ctx.player.load(ctx.options, function(err) { 234 | if (err) return reject(err); 235 | that._setStatus(ctx, 'ready'); 236 | var onStatus = function(status) { 237 | that._setStatus(ctx, status.playerState.toLowerCase()); 238 | }; 239 | var onClosed = function() { 240 | debug('_load player onClosed'); 241 | ctx.player.removeListener('status', onStatus); 242 | ctx.player.removeListener('closed', onClosed); 243 | ctx.playerClosed = true; 244 | ctx.shutdown(); 245 | }; 246 | ctx.player.on('status', onStatus); 247 | ctx.player.on('closed', onClosed); 248 | resolve(ctx); 249 | }); 250 | }); 251 | }; 252 | 253 | player.prototype._setStatus = function(ctx, status) { 254 | ctx.status = status; 255 | ctx.emit('status', status); 256 | }; 257 | 258 | player.api = api; 259 | 260 | module.exports = player; 261 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chromecast-player", 3 | "version": "0.2.3", 4 | "description": "simple chromecast player", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Simon Kusterer", 10 | "license": "MIT", 11 | "dependencies": { 12 | "castv2-client": "^1.1.0", 13 | "chromecast-scanner": "^0.5.0", 14 | "debug": "^2.1.1", 15 | "mutate.js": "^0.2.0", 16 | "promiscuous": "^0.6.0", 17 | "time-line": "^1.0.1", 18 | "ware": "^1.2.0", 19 | "xtend": "^4.0.0" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git@github.com:xat/chromecast-player.git" 24 | }, 25 | "keywords": [ 26 | "chromecast", 27 | "media", 28 | "player", 29 | "video" 30 | ], 31 | "bugs": { 32 | "url": "https://github.com/xat/chromecast-player/issues" 33 | }, 34 | "homepage": "https://github.com/xat/chromecast-player" 35 | } 36 | -------------------------------------------------------------------------------- /timelineHelper.js: -------------------------------------------------------------------------------- 1 | var timeline = require('time-line'); 2 | var inherits = require('util').inherits; 3 | var debug = require('debug')('chromecast-player:timelineHelper'); 4 | var EventEmitter = require('events').EventEmitter; 5 | 6 | var TimelineHelper = function(p) { 7 | if (!(this instanceof TimelineHelper)) return new TimelineHelper(p); 8 | this.p = p; 9 | this.len = 0; 10 | this.timelineSupported = false; 11 | this.tl = timeline(this.len, 250); 12 | 13 | var onStatus = this._updatePosition.bind(this); 14 | 15 | var onPosition = function(pos) { 16 | if (isNaN(pos.percent)) return; 17 | this.emit('position', pos); 18 | }.bind(this); 19 | 20 | var onPlaying = this.update.bind(this); 21 | 22 | var onClosed = function() { 23 | debug('timelineHelper closed'); 24 | this.p.removeListener('status', onStatus); 25 | this.tl.removeListener('position', onPosition); 26 | this.p.removeListener('playing', onPlaying); 27 | this.p.removeListener('closed', onClosed); 28 | this.tl._clear(); 29 | }.bind(this); 30 | 31 | this.p.on('status', onStatus); 32 | this.tl.on('position', onPosition); 33 | this.p.on('playing', onPlaying); 34 | this.p.on('closed', onClosed); 35 | }; 36 | 37 | inherits(TimelineHelper, EventEmitter); 38 | 39 | TimelineHelper.prototype._updatePosition = function(status) { 40 | this.tl.jumpTo(status.currentTime * 1000); 41 | if (status.playerState.toLowerCase() !== 'playing') { 42 | this.tl.pause(); 43 | } 44 | }; 45 | 46 | TimelineHelper.prototype._updateLength = function(err, status) { 47 | if (err || !status || !status.media || !status.media.duration) { 48 | this.timelineSupported = false; 49 | return; 50 | }; 51 | 52 | if (this.len !== status.media.duration) { 53 | this.len = status.media.duration; 54 | this.tl.reset(this.len * 1000); 55 | } 56 | 57 | this.timelineSupported = true; 58 | this._updatePosition(status); 59 | }; 60 | 61 | 62 | TimelineHelper.prototype.getPosition = function() { 63 | if (!this.timelineSupported) return false; 64 | return this.tl.getPosition(); 65 | }; 66 | 67 | TimelineHelper.prototype.getProgress = function() { 68 | if (!this.timelineSupported) return false; 69 | return this.tl.getProgress(); 70 | }; 71 | 72 | TimelineHelper.prototype.update = function() { 73 | this.p.getStatus(this._updateLength.bind(this)); 74 | }; 75 | 76 | module.exports = TimelineHelper; 77 | --------------------------------------------------------------------------------