├── .eslintrc ├── .gitignore ├── README.md ├── device.js ├── index.js ├── package.json └── tests ├── test.js └── test_reconnect.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { // http://eslint.org/docs/user-guide/configuring.html#specifying-environments 3 | "browser": true, // browser global variables 4 | "jasmine": true, // Jasmine global variables 5 | "node": true, // Node.js global variables and Node.js-specific rules 6 | "amd": true, // define global 7 | "phantomjs": true // Phantomjs global variables 8 | }, 9 | "rules": { 10 | /** 11 | * Strict mode 12 | */ 13 | "strict": [2, "global"], // http://eslint.org/docs/rules/strict 14 | 15 | /** 16 | * Variables 17 | */ 18 | "no-shadow": 2, // http://eslint.org/docs/rules/no-shadow 19 | "no-shadow-restricted-names": 2, // http://eslint.org/docs/rules/no-shadow-restricted-names 20 | "no-unused-vars": [2, { // http://eslint.org/docs/rules/no-unused-vars 21 | "vars": "all", 22 | "args": "all" 23 | }], 24 | "no-use-before-define": 2, // http://eslint.org/docs/rules/no-use-before-define 25 | 26 | /** 27 | * Possible errors 28 | */ 29 | "comma-dangle": [1, "always-multiline"], // http://eslint.org/docs/rules/comma-dangle 30 | "no-cond-assign": [2, "always"], // http://eslint.org/docs/rules/no-cond-assign 31 | "no-console": 1, // http://eslint.org/docs/rules/no-console 32 | "no-debugger": 1, // http://eslint.org/docs/rules/no-debugger 33 | "no-alert": 1, // http://eslint.org/docs/rules/no-alert 34 | "no-constant-condition": 1, // http://eslint.org/docs/rules/no-constant-condition 35 | "no-dupe-keys": 2, // http://eslint.org/docs/rules/no-dupe-keys 36 | "no-duplicate-case": 2, // http://eslint.org/docs/rules/no-duplicate-case 37 | "no-empty": 2, // http://eslint.org/docs/rules/no-empty 38 | "no-ex-assign": 2, // http://eslint.org/docs/rules/no-ex-assign 39 | "no-extra-boolean-cast": 0, // http://eslint.org/docs/rules/no-extra-boolean-cast 40 | "no-extra-semi": 2, // http://eslint.org/docs/rules/no-extra-semi 41 | "no-func-assign": 2, // http://eslint.org/docs/rules/no-func-assign 42 | "no-inner-declarations": 2, // http://eslint.org/docs/rules/no-inner-declarations 43 | "no-invalid-regexp": 2, // http://eslint.org/docs/rules/no-invalid-regexp 44 | "no-irregular-whitespace": 2, // http://eslint.org/docs/rules/no-irregular-whitespace 45 | "no-obj-calls": 2, // http://eslint.org/docs/rules/no-obj-calls 46 | "no-reserved-keys": 0, // http://eslint.org/docs/rules/no-reserved-keys 47 | "no-sparse-arrays": 2, // http://eslint.org/docs/rules/no-sparse-arrays 48 | "no-unreachable": 2, // http://eslint.org/docs/rules/no-unreachable 49 | "use-isnan": 2, // http://eslint.org/docs/rules/use-isnan 50 | "block-scoped-var": 2, // http://eslint.org/docs/rules/block-scoped-var 51 | 52 | /** 53 | * Best practices 54 | */ 55 | "consistent-return": 2, // http://eslint.org/docs/rules/consistent-return 56 | "curly": [2, "multi-line"], // http://eslint.org/docs/rules/curly 57 | "default-case": 2, // http://eslint.org/docs/rules/default-case 58 | "dot-notation": [2, { // http://eslint.org/docs/rules/dot-notation 59 | "allowKeywords": true 60 | }], 61 | "eqeqeq": 2, // http://eslint.org/docs/rules/eqeqeq 62 | "no-bitwise": 2, // http://eslint.org/docs/rules/no-bitwise 63 | "guard-for-in": 2, // http://eslint.org/docs/rules/guard-for-in 64 | "linebreak-style": 2, // http://eslint.org/docs/rules/linebreak-style 65 | "no-caller": 2, // http://eslint.org/docs/rules/no-caller 66 | "no-else-return": 2, // http://eslint.org/docs/rules/no-else-return 67 | "no-eq-null": 2, // http://eslint.org/docs/rules/no-eq-null 68 | "no-eval": 2, // http://eslint.org/docs/rules/no-eval 69 | "no-extend-native": 2, // http://eslint.org/docs/rules/no-extend-native 70 | "no-extra-bind": 2, // http://eslint.org/docs/rules/no-extra-bind 71 | "no-fallthrough": 2, // http://eslint.org/docs/rules/no-fallthrough 72 | "no-floating-decimal": 2, // http://eslint.org/docs/rules/no-floating-decimal 73 | "no-implied-eval": 2, // http://eslint.org/docs/rules/no-implied-eval 74 | "no-lone-blocks": 2, // http://eslint.org/docs/rules/no-lone-blocks 75 | "no-loop-func": 2, // http://eslint.org/docs/rules/no-loop-func 76 | "no-multi-str": 2, // http://eslint.org/docs/rules/no-multi-str 77 | "no-native-reassign": 2, // http://eslint.org/docs/rules/no-native-reassign 78 | "no-new": 2, // http://eslint.org/docs/rules/no-new 79 | "no-new-func": 2, // http://eslint.org/docs/rules/no-new-func 80 | "no-new-wrappers": 2, // http://eslint.org/docs/rules/no-new-wrappers 81 | "no-octal": 2, // http://eslint.org/docs/rules/no-octal 82 | "no-octal-escape": 2, // http://eslint.org/docs/rules/no-octal-escape 83 | "no-param-reassign": 2, // http://eslint.org/docs/rules/no-param-reassign 84 | "no-plusplus": 2, // http://eslint.org/docs/rules/no-plusplus 85 | "no-proto": 2, // http://eslint.org/docs/rules/no-proto 86 | "no-redeclare": 2, // http://eslint.org/docs/rules/no-redeclare 87 | "no-return-assign": 2, // http://eslint.org/docs/rules/no-return-assign 88 | "no-script-url": 2, // http://eslint.org/docs/rules/no-script-url 89 | "no-self-compare": 2, // http://eslint.org/docs/rules/no-self-compare 90 | "no-sequences": 2, // http://eslint.org/docs/rules/no-sequences 91 | "no-throw-literal": 2, // http://eslint.org/docs/rules/no-throw-literal 92 | "no-undef": 2, // http://eslint.org/docs/rules/no-undef 93 | "no-with": 2, // http://eslint.org/docs/rules/no-with 94 | "radix": 2, // http://eslint.org/docs/rules/radix 95 | "vars-on-top": 0, // http://eslint.org/docs/rules/vars-on-top 96 | "wrap-iife": [2, "any"], // http://eslint.org/docs/rules/wrap-iife 97 | "yoda": 2, // http://eslint.org/docs/rules/yoda 98 | 99 | /** 100 | * Style 101 | */ 102 | "indent": [2, 2], // http://eslint.org/docs/rules/indent 103 | "brace-style": [2, // http://eslint.org/docs/rules/brace-style 104 | "1tbs", { 105 | "allowSingleLine": true 106 | }], 107 | "func-style": [2, "declaration"], // http://eslint.org/docs/rules/func-style 108 | "quotes": [ // http://eslint.org/docs/rules/quotes 109 | 2, "single", "avoid-escape" 110 | ], 111 | "camelcase": [2, { // http://eslint.org/docs/rules/camelcase 112 | "properties": "never" 113 | }], 114 | "comma-spacing": [2, { // http://eslint.org/docs/rules/comma-spacing 115 | "before": false, 116 | "after": true 117 | }], 118 | "comma-style": [2, "last"], // http://eslint.org/docs/rules/comma-style 119 | "eol-last": 2, // http://eslint.org/docs/rules/eol-last 120 | "func-names": 0, // http://eslint.org/docs/rules/func-names 121 | "key-spacing": [2, { // http://eslint.org/docs/rules/key-spacing 122 | "beforeColon": false, 123 | "afterColon": true 124 | }], 125 | "max-len": [2, 120, 2], // http://eslint.org/docs/rules/max-len 126 | "new-cap": [2, { // http://eslint.org/docs/rules/new-cap 127 | "newIsCap": true, 128 | "capIsNew": false 129 | }], 130 | "no-multiple-empty-lines": [2, { // http://eslint.org/docs/rules/no-multiple-empty-lines 131 | "max": 2 132 | }], 133 | "no-nested-ternary": 2, // http://eslint.org/docs/rules/no-nested-ternary 134 | "no-new-object": 2, // http://eslint.org/docs/rules/no-new-object 135 | "no-spaced-func": 2, // http://eslint.org/docs/rules/no-spaced-func 136 | "no-trailing-spaces": 2, // http://eslint.org/docs/rules/no-trailing-spaces 137 | "no-extra-parens": 2, // http://eslint.org/docs/rules/no-wrap-func 138 | "no-underscore-dangle": 0, // http://eslint.org/docs/rules/no-underscore-dangle 139 | "one-var": [2, "never"], // http://eslint.org/docs/rules/one-var 140 | "operator-linebreak": [2, "before"], // http://eslint.org/docs/rules/operator-linebreak 141 | "padded-blocks": 0, // http://eslint.org/docs/rules/padded-blocks 142 | "semi": [2, "always"], // http://eslint.org/docs/rules/semi 143 | "semi-spacing": [2, { // http://eslint.org/docs/rules/semi-spacing 144 | "before": false, 145 | "after": true 146 | }], 147 | "space-after-keywords": 2, // http://eslint.org/docs/rules/space-after-keywords 148 | "space-before-blocks": 2, // http://eslint.org/docs/rules/space-before-blocks 149 | "space-before-function-paren": [2, "never"], // http://eslint.org/docs/rules/space-before-function-paren 150 | "space-infix-ops": 2, // http://eslint.org/docs/rules/space-infix-ops 151 | "space-return-throw-case": 2, // http://eslint.org/docs/rules/space-return-throw-case 152 | "spaced-comment": 2, // http://eslint.org/docs/rules/spaced-line-comment 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | chromecast-js 2 | ================= 3 | 4 | chromecast-js is a javascript client library for googlecast's remote playback protocol that uses DefaultMediaReceiver to play any (compatible) content in the Chromecast, it works by wrapping the [node-castv2-client](https://github.com/thibauts/node-castv2-client) module. 5 | 6 | ## Installation 7 | 8 | From npm: 9 | 10 | npm install chromecast-js 11 | 12 | ## Usage 13 | 14 | ``` javascript 15 | chromecastjs = require('chromecast-js') 16 | 17 | var browser = new chromecastjs.Browser() 18 | 19 | browser.on('deviceOn', function(device){ 20 | device.connect() 21 | device.on('connected', function(){ 22 | 23 | device.play('http://commondatastorage.googleapis.com/gtv-videos-bucket/big_buck_bunny_1080p.mp4', 60, function(){ 24 | console.log('Playing in your chromecast!') 25 | }); 26 | 27 | setTimeout(function(){ 28 | device.pause(function(){ 29 | console.log('Paused!') 30 | }); 31 | }, 30000); 32 | 33 | setTimeout(function(){ 34 | device.stop(function(){ 35 | console.log('Stoped!') 36 | }); 37 | }, 40000); 38 | 39 | }) 40 | }) 41 | 42 | ``` 43 | 44 | ## Subtitles and Cover 45 | 46 | To include subtitles and a cover image with the media title, use an Object instead of a string in the *play method*: 47 | 48 | ``` javascript 49 | 50 | chromecastjs = require('../') 51 | 52 | var browser = new chromecastjs.Browser() 53 | 54 | var media = { 55 | url : 'http://commondatastorage.googleapis.com/gtv-videos-bucket/big_buck_bunny_1080p.mp4', 56 | subtitles: [{ 57 | language: 'en-US', 58 | url: 'http://carlosguerrero.com/captions_styled.vtt', 59 | name: 'English', 60 | }, 61 | { 62 | language: 'es-ES', 63 | url: 'http://carlosguerrero.com/captions_styled_es.vtt', 64 | name: 'Spanish', 65 | } 66 | ], 67 | cover: { 68 | title: 'Big Bug Bunny', 69 | url: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg' 70 | }, 71 | subtitles_style: { 72 | backgroundColor: '#FFFFFFFF', // see http://dev.w3.org/csswg/css-color/#hex-notation 73 | foregroundColor: '#000FFFF', // see http://dev.w3.org/csswg/css-color/#hex-notation 74 | edgeType: 'DROP_SHADOW', // can be: "NONE", "OUTLINE", "DROP_SHADOW", "RAISED", "DEPRESSED" 75 | edgeColor: '#AA00FFFF', // see http://dev.w3.org/csswg/css-color/#hex-notation 76 | fontScale: 1.5, // transforms into "font-size: " + (fontScale*100) +"%" 77 | fontStyle: 'BOLD_ITALIC', // can be: "NORMAL", "BOLD", "BOLD_ITALIC", "ITALIC", 78 | fontFamily: 'Droid Sans', 79 | fontGenericFamily: 'CURSIVE', // can be: "SANS_SERIF", "MONOSPACED_SANS_SERIF", "SERIF", "MONOSPACED_SERIF", "CASUAL", "CURSIVE", "SMALL_CAPITALS", 80 | windowColor: '#AA00FFFF', // see http://dev.w3.org/csswg/css-color/#hex-notation 81 | windowRoundedCornerRadius: 10, // radius in px 82 | windowType: 'ROUNDED_CORNERS' // can be: "NONE", "NORMAL", "ROUNDED_CORNERS" 83 | } 84 | } 85 | 86 | 87 | browser.on('deviceOn', function(device){ 88 | device.connect() 89 | device.on('connected', function(){ 90 | 91 | // Starting to play Big Buck Bunny (made in Blender) exactly in the first minute without subtitles or cover. 92 | //device.play('http://commondatastorage.googleapis.com/gtv-videos-bucket/big_buck_bunny_1080p.mp4', 60, function(){ 93 | // console.log('Playing in your chromecast!') 94 | //}); 95 | 96 | // Starting to play Big Buck Bunny (made in Blender) exactly in the first minute with example subtitles and cover. 97 | device.play(media, 0, function(){ 98 | console.log('Playing in your chromecast!') 99 | 100 | setTimeout(function(){ 101 | console.log('subtitles off!') 102 | device.subtitlesOff(function(err,status){ 103 | if(err) console.log("error setting subtitles off...") 104 | console.log("subtitles removed.") 105 | }); 106 | }, 20000); 107 | 108 | setTimeout(function(){ 109 | console.log('subtitles on!') 110 | device.changeSubtitles(1, function(err, status){ 111 | if(err) console.log("error restoring subtitles...") 112 | console.log("subtitles restored.") 113 | }); 114 | }, 25000); 115 | 116 | setTimeout(function(){ 117 | console.log('subtitles on!') 118 | device.changeSubtitles(1, function(err, status){ 119 | if(err) console.log("error restoring subtitles...") 120 | console.log("subtitles restored.") 121 | }); 122 | }, 25000); 123 | 124 | setTimeout(function(){ 125 | device.pause(function(){ 126 | console.log('Paused!') 127 | }); 128 | }, 30000); 129 | 130 | setTimeout(function(){ 131 | device.unpause(function(){ 132 | console.log('unpaused!') 133 | }); 134 | }, 40000); 135 | 136 | setTimeout(function(){ 137 | console.log('I ment English subtitles!') 138 | device.changeSubtitles(0, function(err, status){ 139 | if(err) console.log("error restoring subtitles...") 140 | console.log("English subtitles restored.") 141 | }); 142 | }, 45000); 143 | 144 | setTimeout(function(){ 145 | console.log('Increasing subtitles size...') 146 | device.changeSubtitlesSize(10, function(err, status){ 147 | if(err) console.log("error increasing subtitles size...") 148 | console.log("subtitles size increased.") 149 | }); 150 | }, 46000); 151 | 152 | setTimeout(function(){ 153 | device.seek(30,function(){ 154 | console.log('seeking forward!') 155 | }); 156 | }, 50000); 157 | 158 | setTimeout(function(){ 159 | console.log('decreasing subtitles size...') 160 | device.changeSubtitlesSize(1, function(err, status){ 161 | if(err) console.log("error...") 162 | console.log("subtitles size decreased.") 163 | }); 164 | }, 60000); 165 | 166 | setTimeout(function(){ 167 | device.pause(function(){ 168 | console.log('Paused!') 169 | }); 170 | }, 70000); 171 | 172 | setTimeout(function(){ 173 | device.seek(30,function(){ 174 | console.log('seeking forward!') 175 | }); 176 | }, 80000); 177 | 178 | setTimeout(function(){ 179 | device.seek(30,function(){ 180 | console.log('seeking forward!') 181 | }); 182 | }, 85000); 183 | 184 | setTimeout(function(){ 185 | device.unpause(function(){ 186 | console.log('unpaused!') 187 | }); 188 | }, 90000); 189 | 190 | 191 | setTimeout(function(){ 192 | device.seek(-30,function(){ 193 | console.log('seeking backwards!') 194 | }); 195 | }, 100000); 196 | 197 | 198 | setTimeout(function(){ 199 | device.stop(function(){ 200 | console.log('Stoped!') 201 | }); 202 | }, 200000); 203 | 204 | }) 205 | }) 206 | } 207 | 208 | ``` 209 | 210 | -------------------------------------------------------------------------------- /device.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Client = require('castv2-client').Client; 4 | var DefaultMediaReceiver = require('castv2-client').DefaultMediaReceiver; 5 | var events = require('events'); 6 | var util = require('util'); 7 | var debug = require('debug')('Device'); 8 | 9 | function Device(options) { 10 | events.EventEmitter.call(this); 11 | this.config = options; 12 | this.init(); 13 | } 14 | 15 | Device.prototype.connect = function(callback) { 16 | var self = this; 17 | 18 | // Always use a fresh client when connecting 19 | if (self.client) { 20 | self.client.close(); 21 | } 22 | 23 | self.client = new Client(); 24 | self.client.connect(self.host, function() { 25 | debug('connected, launching app ...'); 26 | self.client.launch(DefaultMediaReceiver, function(err, player) { 27 | if (err) { 28 | debug(err); 29 | } else { 30 | self.player = player; 31 | self.emit('connected'); 32 | if (callback) { 33 | callback(); 34 | } 35 | } 36 | 37 | player.on('status', function(status) { 38 | if (status) { 39 | debug('status broadcast playerState=%s', status.playerState); 40 | } else { 41 | debug('-'); 42 | } 43 | }); 44 | }); 45 | }); 46 | 47 | this.client.on('error', function(err) { 48 | debug('Error: %s', err.message); 49 | self.connect(self.host); 50 | self.client.close(); 51 | }); 52 | }; 53 | 54 | Device.prototype.init = function() { 55 | this.host = this.config.addresses[0]; 56 | this.playing = false; 57 | }; 58 | 59 | Device.prototype.play = function(resource, n, callback) { 60 | var self = this; 61 | 62 | var media = null; 63 | var options = { autoplay: true }; 64 | 65 | if (typeof resource !== 'string') { 66 | media = { 67 | contentId: resource.url, 68 | contentType: 'video/mp4', 69 | }; 70 | if (resource.subtitles) { 71 | var i = 0; 72 | var tracks = []; 73 | resource.subtitles.forEach(function(subtitle) { 74 | var track = { 75 | trackId: i, 76 | type: 'TEXT', 77 | trackContentId: subtitle.url, 78 | trackContentType: 'text/vtt', 79 | name: subtitle.name, 80 | language: subtitle.language, 81 | subtype: 'SUBTITLES', 82 | }; 83 | tracks.push(track); 84 | i = i + 1; 85 | }); 86 | 87 | media.tracks = tracks; 88 | options.activeTrackIds = [0]; 89 | } 90 | if (resource.subtitles_style) { 91 | media.textTrackStyle = resource.subtitles_style; 92 | self.subtitles_style = resource.subtitles_style; 93 | } 94 | if (resource.cover) { 95 | media.metadata = { 96 | type: 0, 97 | metadataType: 0, 98 | title: resource.cover.title, 99 | images: [{ 100 | url: resource.cover.url, 101 | }], 102 | }; 103 | } 104 | } else { 105 | media = { 106 | contentId: resource, 107 | contentType: 'video/mp4', 108 | }; 109 | } 110 | 111 | options.currentTime = n || 0; 112 | 113 | self.player.load(media, options, function(err, status) { 114 | self.playing = true; 115 | if (callback) { 116 | callback(err, status); 117 | } 118 | }); 119 | }; 120 | 121 | Device.prototype.getStatus = function(callback) { 122 | var self = this; 123 | 124 | self.player.getStatus(function(err, status) { 125 | if (err) { 126 | debug('getStatus error: %s', err.message); 127 | } else { 128 | callback(status); 129 | } 130 | }); 131 | }; 132 | 133 | // Seeks to specific offset in seconds into the media 134 | Device.prototype.seekTo = function(newCurrentTime, callback) { 135 | this.player.seek(newCurrentTime, callback); 136 | }; 137 | 138 | // Seeks in seconds relative to currentTime 139 | Device.prototype.seek = function(seconds, callback) { 140 | var self = this; 141 | 142 | // Retrieve updated status just before seek 143 | self.getStatus(function(newStatus) { 144 | var newCurrentTime = newStatus.currentTime + seconds; 145 | self.seekTo(newCurrentTime, callback); 146 | }); 147 | }; 148 | 149 | Device.prototype.pause = function(callback) { 150 | var self = this; 151 | 152 | self.playing = false; 153 | self.player.pause(callback); 154 | }; 155 | 156 | Device.prototype.setVolume = function(volume, callback) { 157 | var self = this; 158 | 159 | self.client.setVolume({ level: volume }, callback); 160 | }; 161 | 162 | Device.prototype.setVolumeMuted = function(muted, callback) { 163 | var self = this; 164 | 165 | self.client.setVolume({ 'muted': muted }, callback); 166 | }; 167 | 168 | Device.prototype.unpause = function(callback) { 169 | var self = this; 170 | 171 | self.playing = true; 172 | self.player.play(callback); 173 | }; 174 | 175 | Device.prototype.stop = function(callback) { 176 | var self = this; 177 | 178 | self.playing = false; 179 | self.player.stop(callback); 180 | }; 181 | 182 | Device.prototype.subtitlesOff = function(callback) { 183 | var self = this; 184 | 185 | self.player.media.sessionRequest({ 186 | type: 'EDIT_TRACKS_INFO', 187 | activeTrackIds: [], // turn off subtitles. 188 | }, function(err, status) { 189 | if (err) callback(err); 190 | callback(null, status); 191 | }); 192 | }; 193 | 194 | Device.prototype.changeSubtitles = function(num, callback) { 195 | var self = this; 196 | 197 | self.player.media.sessionRequest({ 198 | type: 'EDIT_TRACKS_INFO', 199 | activeTrackIds: [num], // turn off subtitles. 200 | }, function(err, status) { 201 | if (err) callback(err); 202 | callback(null, status); 203 | }); 204 | }; 205 | 206 | Device.prototype.changeSubtitlesSize = function(num, callback) { 207 | var self = this; 208 | 209 | var newStyle = self.subtitles_style; 210 | newStyle.fontScale = num; 211 | self.player.media.sessionRequest({ 212 | type: 'EDIT_TRACKS_INFO', 213 | textTrackStyle: newStyle, 214 | }, function(err, status) { 215 | if (err) { 216 | callback(err); 217 | } 218 | callback(null, status); 219 | }); 220 | }; 221 | 222 | Device.prototype.close = function(callback) { 223 | if (this.client) { 224 | this.client.close(); 225 | this.client = null; 226 | } 227 | if (callback) { 228 | callback(); 229 | } 230 | }; 231 | 232 | exports.Device = Device; 233 | util.inherits(Device, events.EventEmitter); 234 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var events = require('events'); 4 | var http = require('http'); 5 | var util = require('util'); 6 | 7 | var Device = require('./device').Device; 8 | var SsdpClient = require('node-ssdp').Client; 9 | 10 | function Browser(options) { 11 | events.EventEmitter.call(this); 12 | this.init(options); 13 | } 14 | 15 | Browser.prototype.update = function(device) { 16 | var devConfig = {addresses: device.addresses, name: device.name}; 17 | this.device = new Device(devConfig); 18 | this.emit('deviceOn', this.device); 19 | }; 20 | 21 | Browser.prototype.init = function() { 22 | var _this = this; 23 | 24 | var ssdpBrowser = new SsdpClient(); 25 | ssdpBrowser.on('response', function(headers, statusCode, rinfo) { 26 | if (statusCode !== 200 || !headers.LOCATION) { 27 | return; 28 | } 29 | http.get(headers.LOCATION, function(res) { 30 | var body = ''; 31 | res.on('data', function(chunk) { 32 | body += chunk; 33 | }); 34 | res.on('end', function() { 35 | var match = body.match(/(.+?)<\/friendlyName>/); 36 | if (!match || match.length !== 2) { 37 | return; 38 | } 39 | _this.update({addresses: [rinfo.address], name: match[1]}); 40 | }); 41 | }); 42 | }); 43 | ssdpBrowser.search('urn:dial-multiscreen-org:service:dial:1'); 44 | }; 45 | 46 | util.inherits(Browser, events.EventEmitter); 47 | 48 | exports.Browser = Browser; 49 | exports.Device = Device; 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chromecast-js", 3 | "version": "0.1.5", 4 | "description": "Chromecast/Googlecast streaming module all in JS", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/guerrerocarlos/chromecast-js.git" 9 | }, 10 | "keywords": [ 11 | "chromecast", 12 | "googlecast", 13 | "stream", 14 | "streaming", 15 | "torrentv" 16 | ], 17 | "author": { 18 | "name": "Carlos Guerrero", 19 | "email": "guerrerocarlos@gmail.com" 20 | }, 21 | "license": "ISC", 22 | "bugs": { 23 | "url": "https://github.com/guerrerocarlos/chromecast-js/issues" 24 | }, 25 | "homepage": "https://github.com/guerrerocarlos/chromecast-js", 26 | "dependencies": { 27 | "castv2-client": "0.0.8", 28 | "debug": "^1.0.4", 29 | "node-ssdp": "^2.0.1" 30 | }, 31 | "devDependencies": { 32 | "eslint": "^1.1.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test for all device calls. 3 | * Recommended to be run with DEBUG=castv2 to see underlying protocol communication. 4 | */ 5 | 6 | chromecastjs = require('../'); 7 | 8 | var browser = new chromecastjs.Browser(); 9 | 10 | var media = { 11 | url : 'http://commondatastorage.googleapis.com/gtv-videos-bucket/big_buck_bunny_1080p.mp4', 12 | subtitles: [{ 13 | language: 'en-US', 14 | url: 'http://carlosguerrero.com/captions_styled.vtt', 15 | name: 'English' 16 | }, 17 | { 18 | language: 'es-ES', 19 | url: 'http://carlosguerrero.com/captions_styled_es.vtt', 20 | name: 'Spanish' 21 | } 22 | ], 23 | cover: { 24 | title: 'Big Bug Bunny', 25 | url: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg' 26 | }, 27 | subtitles_style: { 28 | backgroundColor: '#FFFFFFFF', // see http://dev.w3.org/csswg/css-color/#hex-notation 29 | foregroundColor: '#000FFFF', // see http://dev.w3.org/csswg/css-color/#hex-notation 30 | edgeType: 'DROP_SHADOW', // can be: "NONE", "OUTLINE", "DROP_SHADOW", "RAISED", "DEPRESSED" 31 | edgeColor: '#AA00FFFF', // see http://dev.w3.org/csswg/css-color/#hex-notation 32 | fontScale: 1.5, // transforms into "font-size: " + (fontScale*100) +"%" 33 | fontStyle: 'BOLD_ITALIC', // can be: "NORMAL", "BOLD", "BOLD_ITALIC", "ITALIC", 34 | fontFamily: 'Droid Sans', 35 | fontGenericFamily: 'CURSIVE', // can be: "SANS_SERIF", "MONOSPACED_SANS_SERIF", "SERIF", "MONOSPACED_SERIF", "CASUAL", "CURSIVE", "SMALL_CAPITALS", 36 | windowColor: '#AA00FFFF', // see http://dev.w3.org/csswg/css-color/#hex-notation 37 | windowRoundedCornerRadius: 10, // radius in px 38 | windowType: 'ROUNDED_CORNERS' // can be: "NONE", "NORMAL", "ROUNDED_CORNERS" 39 | } 40 | }; 41 | 42 | /* 43 | var media = { 44 | //"url":"http://192.168.0.100:4009/5%20-%203%20-%20Lecture%205.3%20-%20Opportunity%20identification%20(29-59).mp4", 45 | //url : 'http://commondatastorage.googleapis.com/gtv-videos-bucket/big_buck_bunny_1080p.mp4', 46 | url : "http://192.168.0.100:8899/6.11.mp4", 47 | "subtitles":[{ 48 | "language":"en-US", 49 | //"url":"http://192.168.0.100:9999/subtitles.vtt", 50 | 51 | url: 'http://carlosguerrero.com/captions_styled.vtt', 52 | "name":"Spanish" 53 | }] 54 | }; 55 | 56 | var media = {"url":"http://192.168.0.100:8899/6.11.mp4","subtitles":[{"language":"en-US","url":"http://carlosguerrero.com/captions_styled.vtt","name":"Subtitle"}]}; 57 | */ 58 | 59 | browser.on('deviceOn', function(device){ 60 | device.connect(); 61 | device.on('connected', function(){ 62 | 63 | // Starting to play Big Buck Bunny (made in Blender) exactly in the first minute without subtitles or cover. 64 | //device.play('http://commondatastorage.googleapis.com/gtv-videos-bucket/big_buck_bunny_1080p.mp4', 60, function(){ 65 | // console.log('Playing in your chromecast!') 66 | //}); 67 | 68 | // Starting to play Big Buck Bunny (made in Blender) exactly in the first minute with example subtitles and cover. 69 | device.play(media, 0, function(){ 70 | console.log('Playing in your chromecast!'); 71 | 72 | setTimeout(function(){ 73 | console.log('lowering volume'); 74 | device.setVolume( 0.25, function( err, newVol){ 75 | if(err) console.log("there was an error changing the volume."); 76 | else console.log('Volume Changed to: '+newVol.level); 77 | }); 78 | }, 15000); 79 | 80 | setTimeout(function() { 81 | console.log('muting audio'); 82 | device.setVolumeMuted(true, function(err, newVol) { 83 | if(err) console.log("there was an error muting the volume."); 84 | else console.log('newVol', newVol); 85 | }); 86 | }, 18000); 87 | 88 | setTimeout(function() { 89 | console.log('unmuting audio'); 90 | device.setVolumeMuted(false, function(err, newVol) { 91 | if(err) console.log("there was an error muting the volume."); 92 | else console.log('newVol', newVol); 93 | }); 94 | }, 19500); 95 | 96 | setTimeout(function(){ 97 | console.log('subtitles off!'); 98 | device.subtitlesOff(function(err,status){ 99 | if(err) console.log("error setting subtitles off..."); 100 | else console.log("subtitles removed."); 101 | }); 102 | }, 20000); 103 | 104 | setTimeout(function(){ 105 | console.log("restoring audio!"); 106 | device.setVolume( 0.5, function( err, newVol){ 107 | if(err) console.log("there was an error changing the volume."); 108 | else console.log('Volume Changed to: '+newVol.level); 109 | }); 110 | }, 21000); 111 | 112 | 113 | setTimeout(function(){ 114 | console.log('subtitles on!'); 115 | device.changeSubtitles(1, function(err, status){ 116 | if(err) console.log("error restoring subtitles..."); 117 | else console.log("subtitles restored."); 118 | }); 119 | }, 25000); 120 | 121 | setTimeout(function(){ 122 | console.log('subtitles on!'); 123 | device.changeSubtitles(1, function(err, status){ 124 | if(err) console.log("error restoring subtitles..."); 125 | else console.log("subtitles restored."); 126 | }); 127 | }, 25000); 128 | 129 | setTimeout(function(){ 130 | device.pause(function(){ 131 | console.log('Paused!'); 132 | }); 133 | }, 30000); 134 | 135 | setTimeout(function(){ 136 | device.unpause(function(){ 137 | console.log('unpaused!'); 138 | }); 139 | }, 40000); 140 | 141 | setTimeout(function(){ 142 | console.log('I ment English subtitles!'); 143 | device.changeSubtitles(0, function(err, status){ 144 | if(err) console.log("error restoring subtitles..."); 145 | else console.log("English subtitles restored."); 146 | }); 147 | }, 45000); 148 | 149 | setTimeout(function(){ 150 | console.log('Increasing subtitles size...'); 151 | device.changeSubtitlesSize(10, function(err, status){ 152 | if(err) console.log("error increasing subtitles size..."); 153 | else console.log("subtitles size increased."); 154 | }); 155 | }, 46000); 156 | 157 | setTimeout(function(){ 158 | device.seek(30,function(){ 159 | console.log('seeking forward!'); 160 | }); 161 | }, 50000); 162 | 163 | setTimeout(function(){ 164 | console.log('decreasing subtitles size...'); 165 | device.changeSubtitlesSize(1, function(err, status){ 166 | if(err) console.log("error..."); 167 | else console.log("subtitles size decreased."); 168 | }); 169 | }, 60000); 170 | 171 | setTimeout(function(){ 172 | device.pause(function(){ 173 | console.log('Paused!'); 174 | }); 175 | }, 70000); 176 | 177 | setTimeout(function(){ 178 | device.seek(30,function(){ 179 | console.log('seeking forward!'); 180 | }); 181 | }, 80000); 182 | 183 | setTimeout(function(){ 184 | device.seek(30,function(){ 185 | console.log('seeking forward!'); 186 | }); 187 | }, 85000); 188 | 189 | setTimeout(function(){ 190 | device.unpause(function(){ 191 | console.log('unpaused!'); 192 | }); 193 | }, 90000); 194 | 195 | 196 | setTimeout(function(){ 197 | device.seek(-30,function(){ 198 | console.log('seeking backwards!'); 199 | }); 200 | }, 100000); 201 | 202 | setTimeout(function(){ 203 | device.seekTo(0,function(){ 204 | console.log('seeking back to start!'); 205 | }); 206 | }, 110000); 207 | 208 | setTimeout(function(){ 209 | device.seekTo(300,function(){ 210 | console.log('seeking to exactly 5 mins!'); 211 | }); 212 | }, 120000); 213 | 214 | setTimeout(function(){ 215 | device.getStatus(function(status) { 216 | device.seekTo( status.media.duration - 100 ,function(){ 217 | console.log('seeking to 100 sec before end!'); 218 | }); 219 | }); 220 | }, 130000); 221 | 222 | setTimeout(function(){ 223 | device.stop(function(){ 224 | console.log('Stoped!'); 225 | }); 226 | }, 150000); 227 | 228 | //Connection still alive and heartbeats being sent between these two. 229 | 230 | setTimeout(function(){ 231 | device.close(function(){ 232 | console.log('Closed!'); 233 | }); 234 | }, 170000); 235 | 236 | }); 237 | }); 238 | }); -------------------------------------------------------------------------------- /tests/test_reconnect.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test for device and connection reuse. 3 | * Recommended to be run with DEBUG=castv2 to see underlying protocol communication. 4 | */ 5 | 6 | chromecastjs = require('../') 7 | 8 | var browser = new chromecastjs.Browser() 9 | 10 | var media = { 11 | url : 'http://commondatastorage.googleapis.com/gtv-videos-bucket/big_buck_bunny_1080p.mp4', 12 | subtitles: [{ 13 | language: 'en-US', 14 | url: 'http://carlosguerrero.com/captions_styled.vtt', 15 | name: 'English', 16 | }, 17 | { 18 | language: 'es-ES', 19 | url: 'http://carlosguerrero.com/captions_styled_es.vtt', 20 | name: 'Spanish', 21 | } 22 | ], 23 | cover: { 24 | title: 'Big Bug Bunny', 25 | url: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg' 26 | }, 27 | subtitles_style: { 28 | backgroundColor: '#FFFFFFFF', // see http://dev.w3.org/csswg/css-color/#hex-notation 29 | foregroundColor: '#000FFFF', // see http://dev.w3.org/csswg/css-color/#hex-notation 30 | edgeType: 'DROP_SHADOW', // can be: "NONE", "OUTLINE", "DROP_SHADOW", "RAISED", "DEPRESSED" 31 | edgeColor: '#AA00FFFF', // see http://dev.w3.org/csswg/css-color/#hex-notation 32 | fontScale: 1.5, // transforms into "font-size: " + (fontScale*100) +"%" 33 | fontStyle: 'BOLD_ITALIC', // can be: "NORMAL", "BOLD", "BOLD_ITALIC", "ITALIC", 34 | fontFamily: 'Droid Sans', 35 | fontGenericFamily: 'CURSIVE', // can be: "SANS_SERIF", "MONOSPACED_SANS_SERIF", "SERIF", "MONOSPACED_SERIF", "CASUAL", "CURSIVE", "SMALL_CAPITALS", 36 | windowColor: '#AA00FFFF', // see http://dev.w3.org/csswg/css-color/#hex-notation 37 | windowRoundedCornerRadius: 10, // radius in px 38 | windowType: 'ROUNDED_CORNERS' // can be: "NONE", "NORMAL", "ROUNDED_CORNERS" 39 | } 40 | } 41 | 42 | function testCast(device, media) { 43 | device.connect(function(err, status) { 44 | console.log('Connected!'); 45 | device.play(media, 0, function(){ 46 | console.log('Playing in your chromecast!'); 47 | 48 | setTimeout(function(){ 49 | device.stop(function (err, status) { 50 | console.log('device stopped'); 51 | 52 | setTimeout(function(){ 53 | testCast(device, media); //infinite loop 54 | // device.play(media, 0, function(){ 55 | // console.log('playing again'); 56 | // }); 57 | }, 8000); 58 | }); 59 | }, 8000); 60 | }); 61 | }); 62 | } 63 | 64 | browser.on('deviceOn', function(device){ 65 | testCast(device, media); 66 | }); 67 | 68 | 69 | --------------------------------------------------------------------------------