├── .gitignore ├── README.md ├── index.html ├── package.json ├── server.js ├── startTranscoder.js ├── testSupport.js ├── transcoding-server.js └── utils ├── ffprobe.js ├── list-files.js ├── rar-utils.js └── video-support.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chromecast media server 2 | 3 | Requires ffmpeg and ffprobe 4 | 5 | Can either be used to start standalone media files from rar archives, or run as a web server from which videos can be started from a web site. 6 | 7 | Start web server using 'node server.js /path/to/media'. 8 | 9 | Cast a video using 'node startTranscoder.js /path/to/rar'. 10 | 11 | It will transcode media directly from rar archives to nearest Chromecast. 12 | 13 | (c) Mattias Rost 2015, http://rost.me 14 | 15 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 71 | 72 | 73 |
74 |
play
75 |
pause
76 |
77 |
78 | 212 | 213 | 214 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "transcode-cast", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Mattias Rost", 10 | "license": "ISC", 11 | "dependencies": { 12 | "body-parser": "^1.12.0", 13 | "chromecast-player": "^0.2.1", 14 | "delayed-stream": "0.0.6", 15 | "express": "^4.12.2", 16 | "internal-ip": "^1.0.0", 17 | "stream-transcoder": "0.0.5", 18 | "walk": "^2.3.9" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require('express') 2 | var bodyParser = require('body-parser'); 3 | var app = express() 4 | var walk = require('walk'); 5 | var player = require('chromecast-player')(); 6 | var internalIp = require('internal-ip'); 7 | var transServer = require('./transcoding-server.js'); 8 | var listDir = require('./utils/list-files.js'); 9 | var path = require('path'); 10 | 11 | var streamserver; 12 | 13 | var state = {}; 14 | 15 | var libraryPath = process.argv[2]; 16 | var castport = 1235; 17 | 18 | app.use(bodyParser.urlencoded({extended: false})); 19 | 20 | app.get('/list', function(req, res) { 21 | 22 | var files = []; 23 | var walker = walk.walk(libraryPath, { followLinks: false}); 24 | walker.on('file', function(root, stat, next) { 25 | if (stat.name.indexOf('.rar', stat.name.length-4) !== -1) 26 | files.push( root + "/" + stat.name ); 27 | next(); 28 | }); 29 | 30 | walker.on('end', function() { 31 | res.send( JSON.stringify(files) ); 32 | }); 33 | }); 34 | 35 | app.get('/dir', function(req, res) { 36 | var requestPath = req.query['path'] || ''; 37 | if (requestPath.indexOf('/')==0) requestPath = requestPath.substring(1); 38 | listDir(path.resolve(libraryPath, requestPath), function(files) { 39 | files.forEach(function(file) { file.path = file.path.substring(libraryPath.length); }); 40 | res.send( JSON.stringify(files) ); 41 | }); 42 | }); 43 | 44 | app.use('/index.html', express.static('index.html')); 45 | 46 | app.get('/launch', function(req, res) { 47 | var requestPath = req.param('path'); 48 | if (requestPath.indexOf('/')==0) requestPath = requestPath.substring(1); 49 | var filePath = path.resolve( libraryPath, requestPath ); 50 | 51 | if (streamserver) { 52 | streamserver.close(); 53 | streamserver = undefined; 54 | } 55 | 56 | streamserver = transServer.start(filePath, castport); 57 | 58 | player.launch("http://" + internalIp() + ":" + castport + "/video.mp4?path=" + filePath, function(err, p) { 59 | p.once('playing', function() { 60 | console.log('now playing'); 61 | res.send('playing'); 62 | state.playing = true; 63 | state.path = path; 64 | }); 65 | }); 66 | }); 67 | 68 | app.get('/host', function(req, res) { 69 | var requestPath = req.param('path'); 70 | if (requestPath.indexOf('/')==0) requestPath = requestPath.substring(1); 71 | var filePath = path.resolve( libraryPath, requestPath ); 72 | 73 | if (streamserver) { 74 | streamserver.close(); 75 | streamserver = undefined; 76 | } 77 | 78 | streamserver = transServer.start(filePath, castport); 79 | 80 | res.send('http://' + internalIp() + ':' + castport + "/video.mkv?path=" + filePath); 81 | }); 82 | 83 | app.get('/state', function(req, res) { 84 | res.send(JSON.stringify(state)); 85 | }); 86 | 87 | app.get('/pause', function(req, res) { 88 | player.attach(function(err, p) { 89 | console.log('pausing'); 90 | p.pause(); 91 | res.send('paused'); 92 | }); 93 | }); 94 | 95 | app.get('/play', function(req, res) { 96 | player.attach(function(err, p) { 97 | console.log('playing'); 98 | p.play(); 99 | res.send('playing'); 100 | }); 101 | }); 102 | 103 | var server = app.listen(2000, function() { 104 | console.log("started"); 105 | }); 106 | -------------------------------------------------------------------------------- /startTranscoder.js: -------------------------------------------------------------------------------- 1 | var player = require('chromecast-player')(); 2 | var transServer = require('./transcoding-server.js'); 3 | var internalIp = require('internal-ip'); 4 | 5 | var port = 1234; 6 | 7 | transServer.start(process.argv[2], port); 8 | 9 | /*player.launch("http://" + internalIp() + ":" + port + "/video.mp4", function(err, p) { 10 | p.once('playing', function() { 11 | console.log('now playing'); 12 | }); 13 | });*/ 14 | 15 | console.log('listening on port ' + port); 16 | -------------------------------------------------------------------------------- /testSupport.js: -------------------------------------------------------------------------------- 1 | var getSupport = require('./utils/video-support'); 2 | 3 | try { 4 | getSupport(process.stdin, function(result) { 5 | console.log('audio supported: ' + result.supportsAudio()); 6 | console.log('video supported: ' + result.supportsVideo()); 7 | }); 8 | } catch (e) {} 9 | 10 | process.stdout.on('error', function(err) { 11 | console.log('error!'); 12 | }); 13 | -------------------------------------------------------------------------------- /transcoding-server.js: -------------------------------------------------------------------------------- 1 | var Transcoder = require('stream-transcoder'); 2 | var http = require('http'); 3 | var rar = require('./utils/rar-utils.js'); 4 | var internalIp = require('internal-ip'); 5 | var stream = require('stream'); 6 | var DelayedStream = require('delayed-stream'); 7 | var fs = require('fs'); 8 | var videoSupport = require('./utils/video-support'); 9 | var url = require('url'); 10 | 11 | function startTranscodingServer(path, port) { 12 | 13 | return http.createServer(function(req, res) { 14 | res.writeHead(200, { 15 | 'Access-Control-Allow-Origin': '*' 16 | }); 17 | 18 | console.log('http request'); 19 | 20 | // requestedPath = url.parse(req.url, true).pathname.substring(1); 21 | var requestedPath = url.parse(req.url, true).query.path; 22 | 23 | console.log(requestedPath); 24 | 25 | path = requestedPath; 26 | 27 | // get rarStream, analyse with ffprobe 28 | // get rarStream again, set ffmpeg flags based on analysis from ffprobe 29 | 30 | function openVideoStream() { 31 | if (path.substring(path.length-4)=='.rar') { 32 | return rar.openRarStream(path); 33 | } else { 34 | return fs.createReadStream(path); 35 | } 36 | } 37 | 38 | videoSupport( openVideoStream(), function(support) { 39 | console.log('got support ' + JSON.stringify(support) ); 40 | 41 | var videoStream = openVideoStream(); 42 | var trans = new Transcoder( videoStream ) 43 | .custom('strict', 'experimental') 44 | .format('matroska') 45 | // .format('mp4') 46 | // .custom('ss', '00:20:00') 47 | .on('finish', function() { 48 | console.log('finished transcoding'); 49 | }) 50 | .on('error', function(err) { 51 | console.log('transcoding error: %o', err); 52 | }) 53 | .on('progress', function(progress) { 54 | console.log('Progress: ' + progress.progress); 55 | }); 56 | 57 | if (support.supportsAudio()) { 58 | trans.audioCodec('copy'); 59 | } else { 60 | trans.audioCodec('aac') 61 | .custom('ac', '2'); 62 | } 63 | 64 | if (support.supportsVideo()) { 65 | trans.videoCodec('copy'); 66 | } else { 67 | trans.videoCodec('libx264') 68 | .custom('movflags', 'frag_keyframe'); 69 | } 70 | 71 | var args = trans._compileArguments(); 72 | args = [ '-i', '-' ].concat(args); 73 | args.push('pipe:1'); 74 | console.log('spawning ffmpeg ', args.join(' ')); 75 | 76 | var transStream = trans.stream(); 77 | transStream.pipe(res); 78 | 79 | videoStream.on('end', function() { console.log('video stream ended'); }); 80 | 81 | req.on('close', stopStream); 82 | req.on('end', stopStream); 83 | 84 | function stopStream() { 85 | console.log('Connection closed'); 86 | transStream.unpipe(); 87 | transStream.destroy(); 88 | } 89 | }); 90 | 91 | }).listen(port); 92 | } 93 | 94 | module.exports = { 95 | start : startTranscodingServer 96 | }; 97 | -------------------------------------------------------------------------------- /utils/ffprobe.js: -------------------------------------------------------------------------------- 1 | var os = require('os'); 2 | var spawn = require('child_process').spawn; 3 | 4 | function ffprobe(input, cb) { 5 | var args = []; 6 | if (typeof input == "string") { 7 | args.push('-i', input); 8 | } else { 9 | args.push('-i', '-'); 10 | } 11 | 12 | args.push('-of','json','-show_streams'); 13 | 14 | // console.log('ffprobe ' + args.join(' ')); 15 | 16 | var child = spawn('ffprobe', args); 17 | var infoString = ""; 18 | child.stdout.on('data', function(data) { 19 | infoString += data.toString(); 20 | }); 21 | child.stdout.on('end', function() { 22 | cb(JSON.parse(infoString.trim())); 23 | }); 24 | 25 | child.on('exit', function(code) { 26 | // console.log('ffprobe exited'); 27 | }); 28 | 29 | if (typeof input != "string") { 30 | input.pipe(child.stdin); 31 | 32 | child.stdin.on('error', function( err ) { 33 | // console.log('child error'); 34 | }); 35 | } 36 | } 37 | 38 | process.stdout.on('error', function( err ) { 39 | console.log('pipe error ' + err); 40 | }); 41 | 42 | module.exports = ffprobe; 43 | -------------------------------------------------------------------------------- /utils/list-files.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var rarUtils = require('./rar-utils'); 4 | 5 | if (!String.prototype.endsWith) { 6 | String.prototype.endsWith = function(suffix) { 7 | return this.indexOf(suffix, this.length - suffix.length) !== -1; 8 | }; 9 | } 10 | 11 | function listDir(dirpath, cb) { 12 | fs.readdir(dirpath, function(err, files) { 13 | var infos = files.filter(function(filename) { return filename.indexOf('.')!=0; }).map(function(filename) { 14 | 15 | var file = path.resolve(dirpath, filename); 16 | var stat = fs.statSync(file); 17 | 18 | var info = { 19 | path: file, 20 | filename: filename.toString(), 21 | isDirectory: stat.isDirectory(), 22 | isFile: stat.isFile() 23 | }; 24 | 25 | return info; 26 | }); 27 | 28 | var waitingForRars = 0; 29 | infos.filter(function(f) { return f.filename.endsWith('.rar'); }).forEach(function(rar) { 30 | waitingForRars++; 31 | rarUtils.rarContent(rar.path, function(content) { 32 | rar.rarInfo = { file: content }; 33 | --waitingForRars; 34 | taskComplete(); 35 | }); 36 | }); 37 | 38 | var waitingForInfos = 0; 39 | infos.filter(function(f) { return f.isDirectory; }).forEach(function(dir) { 40 | var infoPath = path.resolve(dir.path, '.si'); 41 | if (fs.existsSync(infoPath)) { 42 | waitingForInfos++; 43 | fs.readFile(infoPath, function(err, data) { 44 | dir.si = JSON.parse(data); 45 | waitingForInfos--; 46 | taskComplete(); 47 | }); 48 | } 49 | }); 50 | 51 | function taskComplete() { 52 | if (waitingForRars==0 && waitingForInfos==0) 53 | cb(infos); 54 | } 55 | 56 | taskComplete(); 57 | }); 58 | } 59 | 60 | module.exports = listDir; 61 | -------------------------------------------------------------------------------- /utils/rar-utils.js: -------------------------------------------------------------------------------- 1 | var os = require('os'); 2 | var spawn = require('child_process').spawn; 3 | 4 | function rarContent(path, cb) { 5 | var child = spawn('unrar', ['lb', path]); 6 | child.stdout.on('data', function(data) { 7 | cb(("" + data).trim()); 8 | }); 9 | child.on('exit', function() { 10 | // done 11 | }); 12 | } 13 | 14 | function openRarStream(path, cb) { 15 | var child = spawn('unrar', ['p','-inul',path], { 16 | cwd: os.tmpdir() 17 | }); 18 | 19 | child.stdin.on('error', function(err) { 20 | console.log('unrar error'); 21 | }); 22 | 23 | child.stdout.on('error', function() { 24 | console.log('rar error'); 25 | }); 26 | 27 | child.stdout.on('end', function() { 28 | console.log('rar ended'); 29 | }); 30 | 31 | child.on('disconnect', function() { 32 | console.log('rar disconnect'); 33 | }); 34 | 35 | child.on('close', function() { 36 | console.log('rar closed'); 37 | }); 38 | 39 | child.on('exit', function(code) { 40 | console.log('unrar exited'); 41 | }); 42 | 43 | return child.stdout; 44 | } 45 | 46 | module.exports = { 47 | rarContent: rarContent, 48 | openRarStream: openRarStream 49 | }; 50 | -------------------------------------------------------------------------------- /utils/video-support.js: -------------------------------------------------------------------------------- 1 | var ffprobe = require('./ffprobe'); 2 | 3 | var supportedVideoCodecs = ['h264']; 4 | var supportedAudioCodecs = ['aac']; 5 | 6 | function getSupport(input, cb) { 7 | ffprobe(input, function(result) { 8 | var audio = "", video = ""; 9 | result.streams.forEach(function(stream) { 10 | if (stream.codec_type == "audio") { 11 | audio = stream.codec_name; 12 | } else if (stream.codec_type == "video") { 13 | video = stream.codec_name; 14 | } 15 | }); 16 | 17 | var support = { 18 | supportsVideo : function() { 19 | return supportedVideoCodecs.indexOf(video)!==-1; 20 | }, 21 | supportsAudio : function() { 22 | return supportedAudioCodecs.indexOf(audio)!==-1; 23 | } 24 | }; 25 | 26 | cb(support); 27 | }); 28 | } 29 | 30 | /*getSupport(process.stdin, function(result) { 31 | 32 | console.log('audio supported: ' + result.supportsAudio()); 33 | console.log('video supported: ' + result.supportsVideo()); 34 | });*/ 35 | 36 | module.exports = getSupport; 37 | 38 | --------------------------------------------------------------------------------