├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── npm-shrinkwrap.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dlnacast 2 | 3 | Cast local media to your TV through UPnP/DLNA. 4 | Based on thibauts [node-upnp-mediarenderer-client](https://github.com/thibauts/node-upnp-mediarenderer-client). 5 | 6 | ### usage 7 | 8 | ``` 9 | dlnacast [--type ] [--address ] [--subtitle ] 10 | dlnacast --listRenderer 11 | ``` 12 | 13 | ### installation 14 | 15 | `npm install dlnacast -g` 16 | 17 | 18 | ## License 19 | MIT 20 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var minimist = require('minimist') 4 | var RendererFinder = require('renderer-finder') 5 | var fs = require('fs') 6 | var keypress = require('keypress') 7 | var mime = require('mime') 8 | var opts = minimist(process.argv.slice(2)) 9 | var MediaRendererClient = require('upnp-mediarenderer-client') 10 | var smfs = require('static-file-server') 11 | var path = require('path') 12 | 13 | function DIDLMetadata (url, type, title, subtitle) { 14 | var DIDL = '' 15 | DIDL = DIDL + '' 16 | DIDL = DIDL + ' ' 17 | DIDL = DIDL + ' ' + title + '' 18 | if (subtitle) { 19 | DIDL = DIDL + ' ' + subtitle + '' 20 | DIDL = DIDL + ' ' + subtitle + '' 21 | DIDL = DIDL + ' ' + subtitle + '' 22 | } 23 | DIDL = DIDL + ' object.item.videoItem' 24 | DIDL = DIDL + ' ' + url + '' 25 | DIDL = DIDL + ' ' 26 | DIDL = DIDL + '' 27 | return DIDL 28 | } 29 | 30 | var discover = function (cb) { 31 | var finder = new RendererFinder() 32 | 33 | finder.findOne(function (err, info, msg) { 34 | clearTimeout(to) 35 | cb(err, msg.Location) 36 | }) 37 | 38 | var to = setTimeout(function () { 39 | finder.stop() 40 | clearTimeout(to) 41 | cb(new Error('device not found')) 42 | }, 5000) 43 | } 44 | 45 | module.exports = { 46 | listRenderer: function (cb) { 47 | var finder = new RendererFinder() 48 | 49 | finder.on('found', function (info, msg, desc) { 50 | cb(undefined, info, msg, desc) 51 | }) 52 | 53 | finder.on('error', function (err) { 54 | cb(err) 55 | }) 56 | 57 | return finder.start(true) 58 | }, 59 | renderMedia: function (file, type, address, subtitle) { 60 | var cli = null 61 | 62 | if (address) { 63 | startSender(null, address) 64 | } else { 65 | discover(startSender) 66 | } 67 | 68 | function startSender (err, loc) { 69 | if (err) { 70 | console.log(err) 71 | if (require.main === module) { 72 | process.exit() 73 | } 74 | } 75 | cli = new MediaRendererClient(loc) 76 | var subtitlePath = subtitle 77 | var filePath = file 78 | var stat = fs.statSync(filePath) 79 | stat.type = stat.type || mime.lookup(filePath) 80 | var firstHeaders = { 81 | 'Access-Control-Allow-Origin': '*', 82 | 'transferMode.dlna.org': 'Streaming', 83 | 'contentFeatures.dlna.org': 'DLNA.ORG_PN=AVC_MP4_HP_HD_AAC;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000' 84 | } 85 | 86 | // If the is a subtitle to load, load that first and the the media 87 | // This is requires to provide the subtitle headers 88 | smfs.serve(subtitlePath ? subtitlePath : filePath, { 89 | headers: firstHeaders 90 | }, function (err, firstUrl) { 91 | if (err) { 92 | console.log(err) 93 | if (require.main === module) { 94 | process.exit() 95 | } 96 | } 97 | if (subtitlePath) { 98 | firstHeaders['CaptionInfo.sec'] = firstUrl 99 | smfs.serve(filePath, { 100 | headers: firstHeaders 101 | }, function (err, secondUrl) { 102 | if (err) { 103 | console.log(err) 104 | if (require.main === module) { 105 | process.exit() 106 | } 107 | } 108 | runDLNA(secondUrl, firstUrl, stat) 109 | }, firstUrl) 110 | } else { 111 | runDLNA(firstUrl, null, stat) 112 | } 113 | }) 114 | 115 | function runDLNA (fileUrl, subUrl, stat) { 116 | if (require.main === module) { 117 | keypress(process.stdin) 118 | process.stdin.setRawMode(true) 119 | process.stdin.resume() 120 | } 121 | var isPlaying = false 122 | 123 | cli.load(fileUrl, { 124 | autoplay: true, 125 | contentType: stat.type, 126 | metadata: DIDLMetadata(fileUrl, stat.type, path.basename(filePath), subUrl) 127 | }, function (err, result) { 128 | if (err) { 129 | console.log(err.message) 130 | // process.exit() 131 | } 132 | console.log('playing: ', path.basename(filePath)) 133 | console.log('use your space-key to toggle between play and pause') 134 | }) 135 | 136 | cli.on('playing', function () { 137 | isPlaying = true 138 | }) 139 | 140 | cli.on('paused', function () { 141 | isPlaying = false 142 | }) 143 | 144 | cli.on('stopped', function () { 145 | if (require.main === module) { 146 | process.exit() 147 | } 148 | }) 149 | 150 | if (require.main === module) { 151 | process.stdin.on('keypress', function (ch, key) { 152 | if (key && key.name && key.name === 'space') { 153 | if (isPlaying) { 154 | cli.pause() 155 | } else { 156 | cli.play() 157 | } 158 | } 159 | 160 | if (key && key.ctrl && key.name === 'c') { 161 | process.exit() 162 | } 163 | }) 164 | } 165 | } 166 | return cli 167 | } 168 | } 169 | } 170 | 171 | // check if the module is called from a terminal of required from anothe module 172 | if (require.main === module) { 173 | if (!opts._.length && !opts.listRenderer) { 174 | console.log('Usage: dlnacast [--type ] [--address ] [--subtitle ] ') 175 | console.log('Usage: dlnacast --listRenderer') 176 | process.exit() 177 | } 178 | 179 | if (opts.listRenderer) { 180 | module.exports.listRenderer(function (err, info, msg, desc) { 181 | if (err) { 182 | console.log(err) 183 | process.exit() 184 | } 185 | console.log(desc.device.friendlyName + ': ' + msg.Location) 186 | }) 187 | } else { 188 | module.exports.renderMedia(opts._[0], opts.type, opts.address, opts.subtitle) 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /npm-shrinkwrap.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dlnacast", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "get-port": { 6 | "version": "1.0.0", 7 | "from": "get-port@>=1.0.0 <2.0.0", 8 | "resolved": "https://registry.npmjs.org/get-port/-/get-port-1.0.0.tgz" 9 | }, 10 | "internal-ip": { 11 | "version": "1.0.0", 12 | "from": "internal-ip@>=1.0.0 <2.0.0", 13 | "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-1.0.0.tgz" 14 | }, 15 | "keypress": { 16 | "version": "0.2.1", 17 | "from": "keypress@>=0.2.1 <0.3.0", 18 | "resolved": "https://registry.npmjs.org/keypress/-/keypress-0.2.1.tgz" 19 | }, 20 | "mime": { 21 | "version": "1.3.4", 22 | "from": "mime@>=1.3.4 <2.0.0", 23 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz" 24 | }, 25 | "minimist": { 26 | "version": "1.1.1", 27 | "from": "minimist@>=1.1.1 <2.0.0", 28 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.1.1.tgz" 29 | }, 30 | "node-ssdp": { 31 | "version": "2.0.1", 32 | "from": "node-ssdp@>=2.0.1 <3.0.0", 33 | "resolved": "https://registry.npmjs.org/node-ssdp/-/node-ssdp-2.0.1.tgz", 34 | "dependencies": { 35 | "ip": { 36 | "version": "0.3.2", 37 | "from": "ip@>=0.3.0 <0.4.0", 38 | "resolved": "https://registry.npmjs.org/ip/-/ip-0.3.2.tgz" 39 | } 40 | } 41 | }, 42 | "range-parser": { 43 | "version": "1.0.2", 44 | "from": "range-parser@>=1.0.2 <2.0.0", 45 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.0.2.tgz" 46 | }, 47 | "upnp-mediarenderer-client": { 48 | "version": "1.0.0", 49 | "from": "upnp-mediarenderer-client@>=1.0.0 <2.0.0", 50 | "resolved": "https://registry.npmjs.org/upnp-mediarenderer-client/-/upnp-mediarenderer-client-1.0.0.tgz", 51 | "dependencies": { 52 | "debug": { 53 | "version": "2.1.3", 54 | "from": "debug@>=2.1.3 <3.0.0", 55 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz", 56 | "dependencies": { 57 | "ms": { 58 | "version": "0.7.0", 59 | "from": "ms@0.7.0", 60 | "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.0.tgz" 61 | } 62 | } 63 | }, 64 | "upnp-device-client": { 65 | "version": "1.0.0", 66 | "from": "upnp-device-client@>=1.0.0 <2.0.0", 67 | "resolved": "https://registry.npmjs.org/upnp-device-client/-/upnp-device-client-1.0.0.tgz", 68 | "dependencies": { 69 | "concat-stream": { 70 | "version": "1.4.8", 71 | "from": "concat-stream@>=1.4.8 <2.0.0", 72 | "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.4.8.tgz", 73 | "dependencies": { 74 | "inherits": { 75 | "version": "2.0.1", 76 | "from": "inherits@>=2.0.1 <2.1.0", 77 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" 78 | }, 79 | "typedarray": { 80 | "version": "0.0.6", 81 | "from": "typedarray@>=0.0.5 <0.1.0", 82 | "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz" 83 | }, 84 | "readable-stream": { 85 | "version": "1.1.13", 86 | "from": "readable-stream@>=1.1.9 <1.2.0", 87 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.13.tgz", 88 | "dependencies": { 89 | "core-util-is": { 90 | "version": "1.0.1", 91 | "from": "core-util-is@>=1.0.0 <1.1.0", 92 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.1.tgz" 93 | }, 94 | "isarray": { 95 | "version": "0.0.1", 96 | "from": "isarray@0.0.1", 97 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" 98 | }, 99 | "string_decoder": { 100 | "version": "0.10.31", 101 | "from": "string_decoder@>=0.10.0 <0.11.0", 102 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" 103 | } 104 | } 105 | } 106 | } 107 | }, 108 | "elementtree": { 109 | "version": "0.1.6", 110 | "from": "elementtree@>=0.1.6 <0.2.0", 111 | "resolved": "https://registry.npmjs.org/elementtree/-/elementtree-0.1.6.tgz", 112 | "dependencies": { 113 | "sax": { 114 | "version": "0.3.5", 115 | "from": "sax@0.3.5", 116 | "resolved": "https://registry.npmjs.org/sax/-/sax-0.3.5.tgz" 117 | } 118 | } 119 | }, 120 | "network-address": { 121 | "version": "1.0.0", 122 | "from": "network-address@>=1.0.0 <2.0.0", 123 | "resolved": "https://registry.npmjs.org/network-address/-/network-address-1.0.0.tgz" 124 | } 125 | } 126 | } 127 | } 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dlnacast", 3 | "version": "0.0.3", 4 | "description": "cast local media to your TV using UPnP/DLNA", 5 | "main": "index.js", 6 | "bin": { 7 | "dlnacast": "./index.js" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/xat/dlnacast" 15 | }, 16 | "author": "Simon Kusterer", 17 | "contributors": [ 18 | "Manuel Rueda " 19 | ], 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/xat/dlnacast/issues" 23 | }, 24 | "homepage": "https://github.com/xat/dlnacast", 25 | "dependencies": { 26 | "keypress": "^0.2.1", 27 | "mime": "^1.3.4", 28 | "minimist": "^1.1.1", 29 | "renderer-finder": "^0.1.0", 30 | "static-file-server": "0.0.4", 31 | "upnp-mediarenderer-client": "https://registry.npmjs.org/upnp-mediarenderer-client/-/upnp-mediarenderer-client-1.0.0.tgz" 32 | } 33 | } 34 | --------------------------------------------------------------------------------