├── .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 |
--------------------------------------------------------------------------------