Comparar commits
18 Commits
| Autor | SHA1 | Data | |
|---|---|---|---|
| d658a66f15 | |||
| 4eacc487d8 | |||
| 7438113410 | |||
| e5763a7710 | |||
| fbf9141a5a | |||
| c801f04b3b | |||
| 93e4d87b35 | |||
| b601e66a6e | |||
| ca0213f54b | |||
| 7b656d24c2 | |||
| 62120bba13 | |||
| e834a679f5 | |||
| e69cc01f6d | |||
| 89baea91c4 | |||
| 6842dbb87a | |||
| 2a4c6908e2 | |||
| 9d4cf21f00 | |||
| dde6795193 |
@@ -2,3 +2,5 @@
|
||||
node_modules/
|
||||
.idea/
|
||||
samples/
|
||||
npm-debug.log
|
||||
playground.js
|
||||
|
||||
+50
-83
@@ -1,108 +1,75 @@
|
||||
# castnow
|
||||
# castnow v0.5
|
||||
|
||||
castnow is commandline utility which can be used to playback media files on
|
||||
your chromecast device. It supports playback of local video files, youtube
|
||||
clips, videos on the web and torrents. You can also re-attach a running
|
||||
playback session.
|
||||
In software it's often said that a codebase needs at least 2 rewrites until
|
||||
it's considered as "good" code. I hope for castnow one rewrite will do it though :)
|
||||
|
||||
### usage
|
||||
### Why the rewrite?
|
||||
|
||||
```
|
||||
castnow has some pain points at the moment which I think
|
||||
only can be solved nicely through an rewrite.
|
||||
Here are some:
|
||||
|
||||
// start playback of a local video file
|
||||
castnow ./myvideo.mp4
|
||||
* Custom Plugins are not supported
|
||||
* It would be hard to build a Web-Interface based on the current code
|
||||
* The Playlist functionality is more or less a hack since it was not part of the initial idea/concept
|
||||
* When FFmpeg Transcoding is done the player controls are not supported
|
||||
* You can't mix for example YouTube Videos together with MP4 Videos in the Playlist
|
||||
* There are often crashes while playing
|
||||
* Transcoding always converts video+audio although often only transcoding of the audio-stream would be needed
|
||||
|
||||
// start playback of video and mp3 files in the local directory
|
||||
castnow ./mydirectory/
|
||||
|
||||
// playback 3 videos after each other
|
||||
castnow video1.mp4 video2.mp4 video3.mp4
|
||||
### v0.5 goals
|
||||
|
||||
// start playback of some mp4 file over the web
|
||||
castnow http://commondatastorage.googleapis.com/gtv-videos-bucket/ED_1280.mp4
|
||||
#### Simplicity
|
||||
|
||||
// start playback of some youtube clip
|
||||
castnow https://www.youtube.com/watch?v=pcVRrlmpcWk
|
||||
Simplicity will stay the main focus. Someone should just need to type in `castnow <media source>`
|
||||
into his terminal and castnow then should figure out by it self how to get the given media-input
|
||||
running on Chromecast. Options like `--tomp4` or `--myip` should not be needed anymore.
|
||||
|
||||
// playback some youtube playlist
|
||||
castnow https://www.youtube.com/playlist?list=PLrIJmi5XabBPNDJ_YyC-KNa_cZ6SwTOYC
|
||||
#### Playlist first
|
||||
|
||||
// start playback of some video over torrent
|
||||
castnow <url-to-torrent-file OR magnet>
|
||||
Internally a Playlist will be the center of the architecture. The Playlist will communicate
|
||||
with Chromecast through some kind of Engine thing. Items in the Playlist can be moved around and the User can jump forward and backward. The Playlist can contain items
|
||||
of different types (for example youtube and some local mp4).
|
||||
You may ask yourself why this Playlist thing is so important since the Playlist will
|
||||
mostly contain just one item anyway. The answer is "Google Cast for Audio". With
|
||||
a Playlist as center of the architecture it will be easy to support stuff like .m3u
|
||||
files.
|
||||
|
||||
// start playback of some video over torrent, with local subtitles
|
||||
castnow <url-to-torrent-file OR magnet> --subtitles </local/path/to/subtitles.srt>
|
||||
#### More Hackable
|
||||
|
||||
// transcode some other videoformat to mp4 while playback (requires ffmpeg)
|
||||
castnow ./myvideo.avi --tomp4
|
||||
The goal here is that developers can pull in castnow as external library. For example someone could build an node-webkit (NW.js) app on top of castnow. Basicly this means the lib and bin stuff will get separated in the codebase.
|
||||
|
||||
// re-attach to an currently running playback session
|
||||
castnow
|
||||
#### User-Plugin support
|
||||
|
||||
```
|
||||
If someone wants to write his own plugin without modifying the castnow codebase
|
||||
he will be able todo that. The idea here is that the user can dynamicly
|
||||
load plugins with some sort of `--plugin` parameter (e.g. `--plugin ./myplugin.js`).
|
||||
|
||||
### options
|
||||
#### Better/Smarter transcoding
|
||||
|
||||
* `--tomp4` Transcode a video file to mp4 while playback. This option requires
|
||||
ffmpeg to be installed on your computer. The play / pause controls are currently
|
||||
not supported in transcode mode.
|
||||
castnow will detect if ffmpeg or avconv is installed and if it has the minimum
|
||||
required version. The plan is also to auto-detect if transcoding of an file is needed
|
||||
using ffprobe. If only audio needs to be transcoded castnow will not transcode video.
|
||||
Besides that the main goal is to support player controls for transcoding files (maybe
|
||||
even seeking). We will also test if it's better to transcode to .mkv instead of .mp4.
|
||||
|
||||
* `--device "my chromecast"` If you have more than one chromecast in your network
|
||||
use the `--device` option to specify the device on which you want to start casting.
|
||||
Otherwise castnow will just use the first device it finds in the network.
|
||||
#### Robustness
|
||||
|
||||
* `--address <IP>` The IP address of your chromecast. This will skip the MDNS scan.
|
||||
This includes stuff like handling connection losses correctly, prevent the player
|
||||
from going into idle modus if the player was paused too long and stuff like that.
|
||||
|
||||
* `--subtitles <path/URL>` This can be a path or URL to a vtt or srt file which
|
||||
contains subtitles.
|
||||
### What happens after v0.5.x
|
||||
|
||||
* `--myip <IP>` Your main IP address (useful if you have multiple network adapters)
|
||||
The plan is to add an minimalstic web interface with an REST API. This likely
|
||||
will happen within v0.6.x. After that, if nothing goes wrong, v1.0 is ahead :)
|
||||
|
||||
* `--verbose` Hide the player timeline.
|
||||
### Contribution
|
||||
|
||||
* `--peerflix-* <val>` Pass options to peerflix.
|
||||
|
||||
* `--ffmpeg-* <val>` Pass options to ffmpeg.
|
||||
|
||||
* `--type <val>` Explicity set the mime-type of the first item in the playlist (e.g. 'video/mp4').
|
||||
|
||||
* `--seek <val>` Seek to the specified time on start using the format hh:mm:ss or mm:ss.
|
||||
|
||||
* `--bypass-srt-encoding` Disable automatic UTF8 encoding of SRT subtitles.
|
||||
|
||||
* `--help` Display help message.
|
||||
|
||||
### player controls
|
||||
|
||||
```
|
||||
|
||||
space // toggle between play and pause
|
||||
m // toggle between mute and unmute
|
||||
up // volume up
|
||||
down // volume down
|
||||
left // seek backward (keep pressed / multiple press for faster seek)
|
||||
right // seek forward (keep pressed / multiple press for faster seek)
|
||||
n // next item in the playlist (only supported in launch-mode)
|
||||
s // stop playback
|
||||
q // quit
|
||||
|
||||
```
|
||||
|
||||
### reporting bugs/issues
|
||||
|
||||
Please always append the debug output to your issues. You can enable the debug messages by setting the
|
||||
DEBUG ENV variable before running the castnow-command like this: `DEBUG=castnow* castnow ./myvideo.mp4`.
|
||||
Some problems are also already addressed in our wiki https://github.com/xat/castnow/wiki.
|
||||
|
||||
### installation
|
||||
|
||||
`npm install castnow -g`
|
||||
|
||||
### contributers
|
||||
|
||||
* [tooryx](https://github.com/tooryx)
|
||||
* [przemyslawpluta](https://github.com/przemyslawpluta)
|
||||
Contributers are welcome :-)
|
||||
All I ask for is that we all agree on a similar coding style and have the same project goals in mind. I for myself love splitting up stuff in smaller functions and then compose
|
||||
them together. I'm also willing to give direct push access to people who are constantly contributing.
|
||||
|
||||
## License
|
||||
Copyright (c) 2014 Simon Kusterer
|
||||
Copyright (c) 2015 Simon Kusterer
|
||||
Licensed under the MIT license.
|
||||
|
||||
Arquivo executável
+67
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
var castnow = require('./castnow')();
|
||||
var opts = require('minimist')(process.argv.slice(2));
|
||||
var attachMode = !opts._.length;
|
||||
var launchMode = !attachMode;
|
||||
var scanner = require('chromecast-scanner');
|
||||
var debug = require('debug')('castnow:bin');
|
||||
var engine = castnow.getEngine();
|
||||
var pl = castnow.getPlaylist();
|
||||
|
||||
// plugins
|
||||
var urlPlugin = require('./plugins/url');
|
||||
var youtubePlugin = require('./plugins/youtube');
|
||||
var youtubePlaylistPlugin = require('./plugins/youtubeplaylist');
|
||||
var localfilePlugin = require('./plugins/localfile');
|
||||
var directoriesPlugin = require('./plugins/directories');
|
||||
var torrentPlugin = require('./plugins/torrent');
|
||||
|
||||
var abort = function(message) {
|
||||
// stop
|
||||
};
|
||||
|
||||
if (opts.help) {
|
||||
// display help message
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.version) {
|
||||
// display castnow version
|
||||
return;
|
||||
}
|
||||
|
||||
// Load Plugins here...
|
||||
castnow.use(urlPlugin);
|
||||
castnow.use(localfilePlugin);
|
||||
castnow.use(youtubePlugin);
|
||||
castnow.use(youtubePlaylistPlugin);
|
||||
castnow.use(directoriesPlugin());
|
||||
castnow.use(torrentPlugin);
|
||||
|
||||
if (opts.check) {
|
||||
// - list all chromecast devices found in the network
|
||||
// - check if ffmpeg is installed with an supported version
|
||||
return;
|
||||
}
|
||||
|
||||
if (launchMode) {
|
||||
debug('launch mode');
|
||||
castnow.resolve(opts._, function(err, items) {
|
||||
if (err) return debug('error resolving items');
|
||||
pl.append.apply(pl, items);
|
||||
debug('appended items to playlist %s', items.length);
|
||||
scanner(function(err, service) {
|
||||
if (err) return debug('chromecast not found');
|
||||
debug('found chromecast running with address %s', service.address);
|
||||
castnow.connect(service.address, function(err) {
|
||||
if (err) return debug('chromecast connection failed');
|
||||
// load first item in playlist
|
||||
pl.load(0, function(err, item, controls) {
|
||||
if (err) return debug('load failed with error: %s', err.message);
|
||||
debug('load succeeded');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
var playlist = require('./lib/playlist');
|
||||
var engine = require('./lib/engine');
|
||||
var itemBuilder = require('./lib/itembuilder');
|
||||
var utils = require('./lib/utils');
|
||||
var xtend = require('xtend');
|
||||
var isArray = require('util').isArray;
|
||||
var express = require('express');
|
||||
var async = require('async');
|
||||
var hoook = require('hoook');
|
||||
var debug = require('debug')('castnow');
|
||||
var noop = function() {};
|
||||
|
||||
var defaults = {
|
||||
port: 7373
|
||||
};
|
||||
|
||||
var castnow = function(opts) {
|
||||
var eng = engine();
|
||||
var pl = playlist(eng);
|
||||
var router = express.Router();
|
||||
var uniqueId = utils.uniqueId(1);
|
||||
var options = xtend(defaults, opts || {});
|
||||
var app = express();
|
||||
var flatteners = {};
|
||||
var itemCreators = {};
|
||||
var cn;
|
||||
|
||||
var flatten = function(input, cb) {
|
||||
if (!cb) cb = noop;
|
||||
// the flatten-hook can be used to extract
|
||||
// the items of a playlist-like input (e.g. .m3u)
|
||||
cn.fire('flatten', { input: input },
|
||||
function(err, ev) {
|
||||
if (err) return cb(err);
|
||||
cb(null, isArray(ev.input) ? ev.input : [ev.input]);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
var resolver = function(input, cb) {
|
||||
if (!cb) cb = noop;
|
||||
cn.fire('resolve', input, function(err, ev) {
|
||||
if (err) return cb(err);
|
||||
cb(null, ev.item || null);
|
||||
});
|
||||
};
|
||||
|
||||
app.use(router);
|
||||
app.listen(options.port);
|
||||
|
||||
return cn = xtend({
|
||||
|
||||
getEngine: function() {
|
||||
return eng;
|
||||
},
|
||||
|
||||
getPlaylist: function() {
|
||||
return pl;
|
||||
},
|
||||
|
||||
// get an express router which
|
||||
// can be used by all plugins.
|
||||
getRouter: function() {
|
||||
return router;
|
||||
},
|
||||
|
||||
// connect to chromecast
|
||||
connect: function() {
|
||||
eng.connect.apply(null, arguments);
|
||||
},
|
||||
|
||||
// resolve is used to transform some input
|
||||
// into an item that can be added to the
|
||||
// playlist
|
||||
resolve: function(input, cb) {
|
||||
if (!cb) cb = noop;
|
||||
var that = this;
|
||||
var inputs = isArray(input) ? input : [input];
|
||||
|
||||
async.concat(inputs, flatten, function(err, list) {
|
||||
if (err) cb(err);
|
||||
async.mapSeries(list, resolver, function(err, items) {
|
||||
if (err) return cb(err);
|
||||
items = items.filter(function(item) {
|
||||
return !!item;
|
||||
});
|
||||
cb(null, items);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
getOptions: function() {
|
||||
return options;
|
||||
},
|
||||
|
||||
createBlankItem: function() {
|
||||
return itemBuilder(uniqueId());
|
||||
},
|
||||
|
||||
createItem: function(name, options, cb) {
|
||||
if (!itemCreators[name]) return cb(new Error('creator not found'));
|
||||
itemCreators[name](options, cb);
|
||||
},
|
||||
|
||||
flatten: function(name, options, cb) {
|
||||
if (!flatteners[name]) return cb(new Error('flattener not found'));
|
||||
flatteners[name](options, cb);
|
||||
},
|
||||
|
||||
addItemCreator: function(name, fn) {
|
||||
itemCreators[name] = fn;
|
||||
},
|
||||
|
||||
addFlattener: function(name, fn) {
|
||||
flattener[name] = fn;
|
||||
},
|
||||
|
||||
// register a plugin
|
||||
use: function(fn) {
|
||||
fn(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
}, hoook());
|
||||
|
||||
};
|
||||
|
||||
module.exports = castnow;
|
||||
@@ -0,0 +1,64 @@
|
||||
var castnow = require('../castnow')();
|
||||
var scanner = require('chromecast-scanner');
|
||||
var keypress = require('keypress');
|
||||
var engine = castnow.getEngine();
|
||||
var pl = castnow.getPlaylist();
|
||||
|
||||
// plugins
|
||||
var urlPlugin = require('../plugins/url');
|
||||
var torrentPlugin = require('../plugins/torrent');
|
||||
var youtubePlugin = require('../plugins/youtube');
|
||||
var youtubePlaylistPlugin = require('../plugins/youtubeplaylist');
|
||||
|
||||
// register plugins
|
||||
castnow.use(urlPlugin);
|
||||
castnow.use(youtubePlugin);
|
||||
castnow.use(youtubePlaylistPlugin);
|
||||
castnow.use(torrentPlugin);
|
||||
|
||||
var list = [
|
||||
'https://www.youtube.com/watch?v=JskztPPSJwY',
|
||||
'http://commondatastorage.googleapis.com/gtv-videos-bucket/ED_1280.mp4',
|
||||
'https://www.youtube.com/watch?v=pcVRrlmpcWk',
|
||||
'https://www.youtube.com/playlist?list=PLrIJmi5XabBPNDJ_YyC-KNa_cZ6SwTOYC'
|
||||
];
|
||||
|
||||
castnow.resolve(list, function(err, items) {
|
||||
if (err) return console.log('could not resolve items');
|
||||
|
||||
console.log('%s items resolved', items.length);
|
||||
|
||||
pl.append.apply(pl, items);
|
||||
scanner(function(err, service) {
|
||||
if (err) return console.log('chromecast not found');
|
||||
|
||||
castnow.connect(service.address, function(err) {
|
||||
if (err) return console.log('chromecast connection failed');
|
||||
keypress(process.stdin);
|
||||
process.stdin.setRawMode(true);
|
||||
process.stdin.resume();
|
||||
|
||||
pl.load();
|
||||
|
||||
console.log('use the arrow keys to jump back and forward in the playlist');
|
||||
|
||||
process.stdin.on('keypress', function(ch, key) {
|
||||
if (key && key.name) {
|
||||
if (key.name === 'left') {
|
||||
return pl.prev(function(err) {
|
||||
if (!err) console.log('jumped to prev item');
|
||||
});
|
||||
}
|
||||
if (key.name === 'right') {
|
||||
return pl.next(function(err) {
|
||||
if (!err) console.log('jumped to next item');
|
||||
});
|
||||
}
|
||||
}
|
||||
if (key && key.ctrl && key.name === 'c') {
|
||||
process.exit();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
-304
@@ -1,304 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
var player = require('chromecast-player')();
|
||||
var opts = require('minimist')(process.argv.slice(2));
|
||||
var chalk = require('chalk');
|
||||
var keypress = require('keypress');
|
||||
var ui = require('playerui')();
|
||||
var circulate = require('array-loop');
|
||||
var xtend = require('xtend');
|
||||
var unformatTime = require('./utils/unformat-time');
|
||||
var debug = require('debug')('castnow');
|
||||
var debouncedSeeker = require('debounced-seeker');
|
||||
var noop = function() {};
|
||||
|
||||
// plugins
|
||||
var directories = require('./plugins/directories');
|
||||
var localfile = require('./plugins/localfile');
|
||||
var torrent = require('./plugins/torrent');
|
||||
var youtubeplaylist = require('./plugins/youtubeplaylist');
|
||||
var youtube = require('./plugins/youtube');
|
||||
var transcode = require('./plugins/transcode');
|
||||
var subtitles = require('./plugins/subtitles');
|
||||
|
||||
if (opts.help) {
|
||||
return console.log([
|
||||
'',
|
||||
'Usage: castnow [<media>, <media>, ...] [OPTIONS]',
|
||||
'',
|
||||
'Option Meaning',
|
||||
'--tomp4 Convert file to mp4 while playback',
|
||||
'--device <name> The name of the chromecast device that should be used',
|
||||
'--address <ip> The IP address of your chromecast device',
|
||||
'--subtitles <path/url> Path or URL to an SRT or VTT file',
|
||||
'--myip <ip> Your main IP address',
|
||||
'--verbose No output',
|
||||
'--peerflix-* <value> Pass options to peerflix',
|
||||
'--ffmpeg-* <value> Pass options to ffmpeg',
|
||||
'--type <val> Explicity set the mime-type (e.g. "video/mp4")',
|
||||
'--bypass-srt-encoding Disable automatic UTF8 encoding of SRT subtitles',
|
||||
'--seek <value> Seek to the specified time on start using the format hh:mm:ss or mm:ss',
|
||||
|
||||
'--help This help screen',
|
||||
'',
|
||||
'Player controls',
|
||||
'',
|
||||
'Key Meaning',
|
||||
'space Toggle between play and pause',
|
||||
'm Toggle between mute and unmute',
|
||||
'up Volume Up',
|
||||
'down Volume Down',
|
||||
'left Seek backward',
|
||||
'right Seek forward',
|
||||
'n Next in playlist',
|
||||
's Stop playback',
|
||||
'quit Quit',
|
||||
''
|
||||
].join('\n'));
|
||||
}
|
||||
|
||||
if (opts._.length) {
|
||||
opts.playlist = opts._.map(function(item) {
|
||||
return {
|
||||
path: item
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
delete opts._;
|
||||
|
||||
if (opts.verbose || process.env.DEBUG) {
|
||||
ui.hide();
|
||||
}
|
||||
|
||||
ui.showLabels('state');
|
||||
|
||||
var last = function(fn, l) {
|
||||
return function() {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
args.push(l);
|
||||
l = fn.apply(null, args);
|
||||
return l;
|
||||
};
|
||||
};
|
||||
|
||||
var ctrl = function(err, p, ctx) {
|
||||
if (err) {
|
||||
debug('player error: %o', err);
|
||||
console.log(chalk.red(err));
|
||||
process.exit();
|
||||
}
|
||||
|
||||
var playlist = ctx.options.playlist;
|
||||
var volume;
|
||||
|
||||
keypress(process.stdin);
|
||||
process.stdin.setRawMode(true);
|
||||
process.stdin.resume();
|
||||
|
||||
// get initial volume
|
||||
p.getVolume(function(err, status) {
|
||||
volume = status;
|
||||
});
|
||||
|
||||
if (!ctx.options.disableTimeline) {
|
||||
p.on('position', function(pos) {
|
||||
ui.setProgress(pos.percent);
|
||||
ui.render();
|
||||
});
|
||||
}
|
||||
|
||||
var seek = debouncedSeeker(function(offset) {
|
||||
if (ctx.options.disableSeek || offset === 0) return;
|
||||
var seconds = Math.max(0, (p.getPosition() / 1000) + offset);
|
||||
debug('seeking to %s', seconds);
|
||||
p.seek(seconds);
|
||||
}, 500);
|
||||
|
||||
var updateTitle = function() {
|
||||
p.getStatus(function(err, status) {
|
||||
if (!status.media ||
|
||||
!status.media.metadata ||
|
||||
!status.media.metadata.title) return;
|
||||
|
||||
var metadata = status.media.metadata;
|
||||
var title;
|
||||
if (metadata.artist) {
|
||||
title = metadata.artist + ' - ' + metadata.title;
|
||||
} else {
|
||||
title = metadata.title;
|
||||
}
|
||||
ui.setLabel('source', 'Source', title);
|
||||
ui.showLabels('state', 'source');
|
||||
ui.render();
|
||||
});
|
||||
};
|
||||
|
||||
var initialSeek = function() {
|
||||
var seconds = unformatTime(ctx.options.seek);
|
||||
debug('seeking to %s', seconds);
|
||||
p.seek(seconds);
|
||||
};
|
||||
|
||||
p.on('playing', updateTitle);
|
||||
|
||||
if (!ctx.options.disableSeek && ctx.options.seek) {
|
||||
p.once('playing', initialSeek);
|
||||
}
|
||||
|
||||
updateTitle();
|
||||
|
||||
var nextInPlaylist = function() {
|
||||
if (ctx.mode !== 'launch') return;
|
||||
if (!playlist.length) return process.exit();
|
||||
p.stop(function() {
|
||||
ui.showLabels('state');
|
||||
debug('loading next in playlist: %o', playlist[0]);
|
||||
p.load(playlist[0], noop);
|
||||
playlist.shift();
|
||||
});
|
||||
};
|
||||
|
||||
p.on('status', last(function(status, memo) {
|
||||
if (status.playerState !== 'IDLE') return;
|
||||
if (status.idleReason !== 'FINISHED') return;
|
||||
if (memo && memo.playerState === 'IDLE') return;
|
||||
nextInPlaylist();
|
||||
return status;
|
||||
}));
|
||||
|
||||
var keyMappings = {
|
||||
|
||||
// toggle between play / pause
|
||||
space: function() {
|
||||
if (p.currentSession.playerState === 'PLAYING') {
|
||||
p.pause();
|
||||
} else if (p.currentSession.playerState === 'PAUSED') {
|
||||
p.play();
|
||||
}
|
||||
},
|
||||
|
||||
// toggle between mute / unmute
|
||||
m: function() {
|
||||
if (volume.muted) {
|
||||
p.unmute(function(err, status) {
|
||||
if (err) return;
|
||||
volume = status;
|
||||
});
|
||||
} else {
|
||||
p.mute(function(err, status) {
|
||||
if (err) return;
|
||||
volume = status;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// volume up
|
||||
up: function() {
|
||||
if (volume.level >= 1) return;
|
||||
p.setVolume(Math.min(volume.level + 0.05, 1), function(err, status) {
|
||||
if (err) return;
|
||||
volume = status;
|
||||
});
|
||||
},
|
||||
|
||||
// volume down
|
||||
down: function() {
|
||||
if (volume.level <= 0) return;
|
||||
p.setVolume(Math.max(volume.level - 0.05, 0), function(err, status) {
|
||||
if (err) return;
|
||||
volume = status;
|
||||
});
|
||||
},
|
||||
|
||||
// next item in playlist
|
||||
n: function() {
|
||||
nextInPlaylist();
|
||||
},
|
||||
|
||||
// stop playback
|
||||
s: function() {
|
||||
p.stop();
|
||||
},
|
||||
|
||||
// quit
|
||||
q: function() {
|
||||
process.exit();
|
||||
},
|
||||
|
||||
// Rewind, one "seekCount" per press
|
||||
left: function() {
|
||||
seek(-30);
|
||||
},
|
||||
|
||||
// Forward, one "seekCount" per press
|
||||
right: function() {
|
||||
seek(30);
|
||||
}
|
||||
};
|
||||
|
||||
process.stdin.on('keypress', function(ch, key) {
|
||||
if (key && key.name && keyMappings[key.name]) {
|
||||
debug('key pressed: %s', key.name);
|
||||
keyMappings[key.name]();
|
||||
}
|
||||
if (key && key.ctrl && key.name == 'c') {
|
||||
process.exit();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var capitalize = function(str) {
|
||||
return str.substr(0, 1).toUpperCase() + str.substr(1);
|
||||
};
|
||||
|
||||
var logState = (function() {
|
||||
var inter;
|
||||
var dots = circulate(['.', '..', '...', '....']);
|
||||
return function(status) {
|
||||
if (inter) clearInterval(inter);
|
||||
debug('player status: %s', status);
|
||||
inter = setInterval(function() {
|
||||
ui.setLabel('state', 'State', capitalize(status) + dots());
|
||||
ui.render();
|
||||
}, 300);
|
||||
};
|
||||
})();
|
||||
|
||||
player.use(function(ctx, next) {
|
||||
ctx.on('status', logState);
|
||||
next();
|
||||
});
|
||||
|
||||
player.use(directories);
|
||||
player.use(torrent);
|
||||
player.use(localfile);
|
||||
player.use(youtubeplaylist);
|
||||
player.use(youtube);
|
||||
player.use(transcode);
|
||||
player.use(subtitles);
|
||||
|
||||
player.use(function(ctx, next) {
|
||||
if (ctx.mode !== 'launch') return next();
|
||||
ctx.options = xtend(ctx.options, ctx.options.playlist[0]);
|
||||
ctx.options.playlist.shift();
|
||||
next();
|
||||
});
|
||||
|
||||
if (!opts.playlist) {
|
||||
debug('attaching...');
|
||||
player.attach(opts, ctrl);
|
||||
} else {
|
||||
debug('launching...');
|
||||
player.launch(opts, ctrl);
|
||||
}
|
||||
|
||||
process.on('SIGINT', function() {
|
||||
process.exit();
|
||||
});
|
||||
|
||||
process.on('exit', function() {
|
||||
ui.hide();
|
||||
});
|
||||
|
||||
module.exports = player;
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
var hoook = require('hoook');
|
||||
var xtend = require('xtend');
|
||||
var Client = require('castv2-client').Client;
|
||||
var async = require('async');
|
||||
var noop = function() {};
|
||||
|
||||
var STATES = {
|
||||
not_connected: 1,
|
||||
connected: 2,
|
||||
launched: 4
|
||||
};
|
||||
|
||||
var engine = function(opts) {
|
||||
var client = new Client();
|
||||
var state = STATES.not_connected;
|
||||
var loading = false;
|
||||
var currentItem;
|
||||
var currentControls;
|
||||
|
||||
// set the current item and controls
|
||||
var setCurrent = function(item, controls) {
|
||||
eng.fire('set_item', { item: item, controls: controls });
|
||||
currentItem = item;
|
||||
currentControls = controls;
|
||||
};
|
||||
|
||||
// unset the current item and controls
|
||||
var unsetCurrent = function() {
|
||||
eng.fire('unset_item', { item: currentItem, controls: currentControls });
|
||||
currentItem = null;
|
||||
currentControls = null;
|
||||
};
|
||||
|
||||
var setState = function(newState) {
|
||||
eng.fire('state_change', { from: state, to: newState });
|
||||
state = newState;
|
||||
};
|
||||
|
||||
// close the client connection
|
||||
// if an error occurs.
|
||||
var onError = function() {
|
||||
client.close();
|
||||
};
|
||||
|
||||
// onClose get's called if the client
|
||||
// looses connection to chromecast.
|
||||
var onClose = function() {
|
||||
setState(STATES.not_connected);
|
||||
loading = false;
|
||||
if (currentItem) {
|
||||
currentItem.unload(currentControls, unsetCurrent);
|
||||
} else {
|
||||
unsetCurrent();
|
||||
}
|
||||
};
|
||||
|
||||
var onStatus = function(status) {
|
||||
var apps = status.applications;
|
||||
if (!apps || loading) return;
|
||||
if (!currentItem) return;
|
||||
if (currentItem.getAppId() !== apps[0].appId) {
|
||||
// we will reach this point here if
|
||||
// media playback was started with castnow
|
||||
// and while playback the user suddenly casts
|
||||
// something else with an other app.
|
||||
setState(STATES.connected);
|
||||
eng.fire('out_of_sync');
|
||||
currentItem.unload(currentControls, unsetCurrent);
|
||||
}
|
||||
};
|
||||
|
||||
client.on('error', onError);
|
||||
client.client.on('close', onClose);
|
||||
client.on('status', onStatus);
|
||||
|
||||
var eng = xtend({
|
||||
|
||||
// connect to chromecast
|
||||
connect: function(address, cb) {
|
||||
if (!cb) cb = noop;
|
||||
client.connect(address, function() {
|
||||
setState(STATES.connected);
|
||||
cb(null, client);
|
||||
});
|
||||
},
|
||||
|
||||
// close a connection
|
||||
close: function() {
|
||||
if (state === STATES.not_connected) return;
|
||||
client.close();
|
||||
},
|
||||
|
||||
// load an new item
|
||||
load: function(item, attach, cb) {
|
||||
if (arguments.length === 2) {
|
||||
cb = attach;
|
||||
attach = false;
|
||||
}
|
||||
if (!cb) cb = noop;
|
||||
if (state === STATES.not_connected) {
|
||||
return cb(new Error('not connected'));
|
||||
}
|
||||
if (loading) {
|
||||
return cb(new Error('load already in progress'));
|
||||
}
|
||||
loading = true;
|
||||
|
||||
async.waterfall([
|
||||
|
||||
// unload previous item
|
||||
function(next) {
|
||||
if (!currentItem) return next();
|
||||
currentItem.unload(currentControls, function(err) {
|
||||
if (err) return next(err);
|
||||
unsetCurrent();
|
||||
next();
|
||||
});
|
||||
},
|
||||
|
||||
// get the currently running appId
|
||||
function(next) {
|
||||
client.getSessions(function(err, apps) {
|
||||
if (err) return next(err);
|
||||
if (!apps.length) return next(null, null);
|
||||
next(null, apps[0]);
|
||||
});
|
||||
},
|
||||
|
||||
// launch or attach
|
||||
function(session, next) {
|
||||
if (!session || session.appId !== item.getAppId()) {
|
||||
// launch
|
||||
eng.fire('launch', { item: item }, function(err) {
|
||||
if (err) return next(err);
|
||||
client.launch(item.getApi(), next);
|
||||
});
|
||||
} else {
|
||||
// attach
|
||||
eng.fire('join', { item: item }, function(err) {
|
||||
if (err) return next(err);
|
||||
client.join(session, item.getApi(), next);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// load the item
|
||||
function(controls, next) {
|
||||
setState(STATES.launched);
|
||||
setCurrent(item, controls);
|
||||
|
||||
if (attach) return next(null, controls);
|
||||
|
||||
eng.fire('load', { item: item, controls: controls }, function(err) {
|
||||
if (err) return cb(err);
|
||||
item.load(controls, function(err) {
|
||||
if (err) return next(err);
|
||||
next(null, controls);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
], function(err, controls) {
|
||||
loading = false;
|
||||
if (err) return cb(err);
|
||||
cb(null, item, controls);
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
// join a running playback session
|
||||
join: function(item, cb) {
|
||||
return this.load(item, true, cb);
|
||||
},
|
||||
|
||||
// find running apps on chromecast
|
||||
find: function(cb) {
|
||||
if (!cb) cb = noop;
|
||||
if (state === STATES.not_connected) {
|
||||
return cb(new Error('not connected'));
|
||||
}
|
||||
client.getSessions(function(err, apps) {
|
||||
if (err) return cb(err);
|
||||
if (!apps.length) return cb(new Error('app not found'));
|
||||
cb(null, apps[0]);
|
||||
});
|
||||
},
|
||||
|
||||
// get the current controlls
|
||||
getControls: function() {
|
||||
return currentControls;
|
||||
},
|
||||
|
||||
// get the current loaded item
|
||||
getItem: function() {
|
||||
return currentItem;
|
||||
},
|
||||
|
||||
hasItem: function() {
|
||||
return !!currentItem;
|
||||
},
|
||||
|
||||
getState: function() {
|
||||
return state;
|
||||
},
|
||||
|
||||
// if an item is currently getting loaded
|
||||
isLoading: function() {
|
||||
return loading;
|
||||
}
|
||||
|
||||
}, hoook());
|
||||
|
||||
return eng;
|
||||
};
|
||||
|
||||
engine.STATES = STATES;
|
||||
|
||||
module.exports = engine;
|
||||
@@ -0,0 +1,67 @@
|
||||
var player = require('chromecast-player');
|
||||
|
||||
var itemBuilder = function(id) {
|
||||
var data = {};
|
||||
var api = player.api;
|
||||
var appId = api.APP_ID;
|
||||
var args = {};
|
||||
|
||||
return {
|
||||
|
||||
getId: function() {
|
||||
return id;
|
||||
},
|
||||
|
||||
setApi: function(a) {
|
||||
api = a;
|
||||
},
|
||||
|
||||
getApi: function() {
|
||||
return api;
|
||||
},
|
||||
|
||||
setAppId: function(id) {
|
||||
appId = id;
|
||||
},
|
||||
|
||||
getAppId: function() {
|
||||
return appId;
|
||||
},
|
||||
|
||||
setArgs: function(a) {
|
||||
args = a;
|
||||
},
|
||||
|
||||
getArgs: function() {
|
||||
return args;
|
||||
},
|
||||
|
||||
setSource: function(src) {
|
||||
source = src;
|
||||
},
|
||||
|
||||
getSource: function() {
|
||||
return source;
|
||||
},
|
||||
|
||||
set: function(key, val) {
|
||||
data[key] = val;
|
||||
},
|
||||
|
||||
get: function(key) {
|
||||
return data[key];
|
||||
},
|
||||
|
||||
load: function(controls, cb) {
|
||||
controls.load(this.getArgs(), cb);
|
||||
},
|
||||
|
||||
unload: function(controls, cb) {
|
||||
if (controls.destroy) controls.destroy();
|
||||
cb();
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = itemBuilder;
|
||||
@@ -0,0 +1,109 @@
|
||||
var hoook = require('hoook');
|
||||
var xtend = require('xtend');
|
||||
var utils = require('./utils');
|
||||
var find = require('array-find');
|
||||
var noop = function() {};
|
||||
var ENGINE_STATES = require('./engine').STATES;
|
||||
|
||||
var STATES = {
|
||||
not_loaded: 1,
|
||||
loaded: 2
|
||||
};
|
||||
|
||||
var playlist = function(engine) {
|
||||
var items = [];
|
||||
var uniqueId = utils.uniqueId(1);
|
||||
var cursor;
|
||||
var pl;
|
||||
|
||||
return pl = xtend({
|
||||
|
||||
load: function(pos, cb) {
|
||||
if (!cb) cb = noop;
|
||||
if (typeof pos === 'undefined') pos = 0;
|
||||
if (typeof items[pos] === 'undefined') {
|
||||
return cb(new Error('out of range'));
|
||||
}
|
||||
if (engine.getState() === ENGINE_STATES.not_connected) {
|
||||
return cb(new Error('engine not connected'));
|
||||
}
|
||||
|
||||
engine.load(items[pos], function(err, controls, item) {
|
||||
if (err) return cb(err);
|
||||
cursor = pos;
|
||||
cb(null, controls, item);
|
||||
});
|
||||
},
|
||||
|
||||
append: function() {
|
||||
items.push.apply(items, arguments);
|
||||
},
|
||||
|
||||
prepend: function() {
|
||||
items.unshift.apply(items, arguments);
|
||||
},
|
||||
|
||||
move: function(from, to) {
|
||||
|
||||
},
|
||||
|
||||
remove: function(pos) {
|
||||
|
||||
},
|
||||
|
||||
getCurrent: function() {
|
||||
if (typeof cursor === 'undefined') return false;
|
||||
return items[cursor];
|
||||
},
|
||||
|
||||
count: function() {
|
||||
return items.length;
|
||||
},
|
||||
|
||||
// load the next item
|
||||
next: function(cb) {
|
||||
if (!cb) cb = noop;
|
||||
if (!items.length) return cb(new Error('playlist is empty'));
|
||||
if (typeof cursor === 'undefined') {
|
||||
// load first
|
||||
return this.load(0, cb);
|
||||
}
|
||||
if (this.hasNext()) {
|
||||
return this.load(cursor+1, cb);
|
||||
}
|
||||
cb(new Error('next item does not exist'));
|
||||
},
|
||||
|
||||
// load the prev item
|
||||
prev: function(cb) {
|
||||
if (!cb) cb = noop;
|
||||
if (!items.length) return cb(new Error('playlist is empty'));
|
||||
if (this.hasPrev()) {
|
||||
return this.load(cursor-1, cb);
|
||||
}
|
||||
cb(new Error('prev item does not exist'));
|
||||
},
|
||||
|
||||
hasNext: function() {
|
||||
if (typeof cursor === 'undefined') return false;
|
||||
return (cursor+1) < items.length;
|
||||
},
|
||||
|
||||
hasPrev: function() {
|
||||
if (typeof cursor === 'undefined') return false;
|
||||
return cursor > 0;
|
||||
},
|
||||
|
||||
// find an item by its id
|
||||
findItem: function(id) {
|
||||
return find(items, function(item) {
|
||||
return item.getId() === id;
|
||||
});
|
||||
}
|
||||
|
||||
}, hoook());
|
||||
};
|
||||
|
||||
playlist.STATES = STATES;
|
||||
|
||||
module.exports = playlist;
|
||||
@@ -0,0 +1,80 @@
|
||||
var os = require('os');
|
||||
|
||||
// change a time-string in the format 'xx:xx:xx'
|
||||
// or 'xx:xx' to seconds
|
||||
var unformatTime = function(str) {
|
||||
return str.split(':')
|
||||
.reverse()
|
||||
.map(function(val, i) {
|
||||
return parseInt(val, 10) * Math.pow(60, i);
|
||||
})
|
||||
.reduce(function(a, b) {
|
||||
return a + b;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
// create unique ids
|
||||
var uniqueId = function(start) {
|
||||
var c = start || 0;
|
||||
return function() {
|
||||
return c++;
|
||||
};
|
||||
};
|
||||
|
||||
// basic key/val store
|
||||
var store = function() {
|
||||
var memo = {};
|
||||
return {
|
||||
get: function(key) {
|
||||
return memo[key];
|
||||
},
|
||||
set: function(key, val) {
|
||||
memo[key] = val;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// count how many characters in two strings
|
||||
// overlap, starting from the beginning
|
||||
// and ending as soon there is a non-matching
|
||||
// character
|
||||
var overlapCount = function(str1, str2) {
|
||||
var len = Math.min(str1.length, str2.length);
|
||||
var i = 0;
|
||||
for (; i<len; i++) {
|
||||
if (str1[i] !== str2[i]) return i;
|
||||
}
|
||||
return len;
|
||||
};
|
||||
|
||||
// get the local ip which matches
|
||||
// the most with the given compare-ip
|
||||
// Notice: since we don't know the subnetmask
|
||||
// of our local network adapters overlap
|
||||
// checking is better than nothing.
|
||||
var guessIp = function(compare) {
|
||||
var interfaces = os.networkInterfaces();
|
||||
var memo = [];
|
||||
if (!compare) compare = '';
|
||||
|
||||
Object.keys(interfaces).forEach(function(i) {
|
||||
interfaces[i].forEach(function(i2) {
|
||||
if (!i2.internal && i2.family === 'IPv4') {
|
||||
i2.overlap = overlapCount(i2.address, compare);
|
||||
memo.push(i2);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return memo
|
||||
.reduce(function(a, b) {
|
||||
return a.overlap > b.overlap ? a : b;
|
||||
}).address;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
unformatTime: unformatTime,
|
||||
uniqueId: uniqueId,
|
||||
store: store,
|
||||
guessIp: guessIp
|
||||
};
|
||||
+20
-19
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "castnow",
|
||||
"version": "0.4.7",
|
||||
"version": "0.5.0",
|
||||
"description": "commandline chromecast player",
|
||||
"main": "index.js",
|
||||
"main": "castnow.js",
|
||||
"bin": {
|
||||
"castnow": "./index.js"
|
||||
"castnow": "./bin.js"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"test": "node test.js"
|
||||
},
|
||||
"author": "Simon Kusterer",
|
||||
"license": "MIT",
|
||||
@@ -27,28 +27,29 @@
|
||||
"cast"
|
||||
],
|
||||
"dependencies": {
|
||||
"array-loop": "^1.0.0",
|
||||
"array-find": "^0.1.1",
|
||||
"async": "^0.9.0",
|
||||
"castv2-client": "0.0.8",
|
||||
"chalk": "^0.5.1",
|
||||
"chromecast-player": "0.1.7",
|
||||
"debounced-seeker": "^1.0.0",
|
||||
"debug": "^2.1.0",
|
||||
"fs-extended": "^0.2.0",
|
||||
"chromecast-player": "^0.1.7",
|
||||
"chromecast-scanner": "^0.1.0",
|
||||
"debug": "^2.1.1",
|
||||
"express": "^4.11.0",
|
||||
"get-youtube-id": "^0.1.3",
|
||||
"got": "^1.2.2",
|
||||
"got": "^2.3.0",
|
||||
"hoook": "^0.1.0",
|
||||
"internal-ip": "^1.0.0",
|
||||
"is-url": "^1.2.0",
|
||||
"keypress": "^0.2.1",
|
||||
"mime": "^1.2.11",
|
||||
"minimist": "^1.1.0",
|
||||
"peerflix": "^0.19.1",
|
||||
"playerui": "^1.2.0",
|
||||
"query-string": "^1.0.0",
|
||||
"pump": "^1.0.0",
|
||||
"query-extend": "^0.1.4",
|
||||
"range-parser": "^1.0.2",
|
||||
"read-torrent": "^1.0.0",
|
||||
"router": "^0.6.2",
|
||||
"srt2vtt": "^1.2.0",
|
||||
"stream-transcoder": "0.0.5",
|
||||
"read-torrent": "^1.2.0",
|
||||
"torrent-stream": "^0.16.2",
|
||||
"xml2js": "^0.4.4",
|
||||
"xtend": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tape": "^3.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
+25
-35
@@ -1,48 +1,38 @@
|
||||
var fs = require('fs-extended');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var xtend = require('xtend');
|
||||
var join = path.join;
|
||||
var extname = path.extname;
|
||||
var debug = require('debug')('castnow:directories');
|
||||
|
||||
var acceptedExtensions = {
|
||||
'.mp3': true,
|
||||
'.mp4': true
|
||||
var defaults = {
|
||||
extensions: ['.mp3', '.mp4']
|
||||
};
|
||||
|
||||
function filter(filePath) {
|
||||
return !!acceptedExtensions[extname(filePath)];
|
||||
}
|
||||
|
||||
var isDir = function(item) {
|
||||
return fs.existsSync(item.path) && fs.lstatSync(item.path).isDirectory();
|
||||
var isDir = function(path) {
|
||||
return fs.existsSync(path) && fs.lstatSync(path).isDirectory();
|
||||
};
|
||||
|
||||
// check which items in the playlist are
|
||||
// actually directories and get all mp4 and
|
||||
// mp3 files out of those.
|
||||
var flattenFiles = function(playlist) {
|
||||
var items = [];
|
||||
playlist.forEach(function(item) {
|
||||
if (isDir(item)) {
|
||||
debug('directory found: %s', item.path);
|
||||
var mediaFiles = fs.listFilesSync(item.path, { filter: filter });
|
||||
items.push.apply(items, mediaFiles.map(function(file) {
|
||||
debug('added file %s from directory %s', file, item.path);
|
||||
return {
|
||||
path: join(item.path, file)
|
||||
};
|
||||
}));
|
||||
return;
|
||||
}
|
||||
items.push(item);
|
||||
});
|
||||
return items;
|
||||
};
|
||||
var directories = function(opts) {
|
||||
var options = xtend(defaults, opts || {});
|
||||
|
||||
var directories = function(ctx, next) {
|
||||
if (ctx.mode !== 'launch') return next();
|
||||
ctx.options.playlist = flattenFiles(ctx.options.playlist);
|
||||
next();
|
||||
var filterByExt = function(filePath) {
|
||||
return options.extensions.indexOf(extname(filePath).toLowerCase()) > -1;
|
||||
};
|
||||
|
||||
return function(castnow) {
|
||||
castnow.hook('flatten', function(ev, next, stop) {
|
||||
if (!isDir(ev.input)) return next();
|
||||
debug('directory found: %s', ev.input);
|
||||
var files = fs.readdirSync(ev.input)
|
||||
.filter(filterByExt)
|
||||
.map(function(filename) {
|
||||
return join(ev.input, filename);
|
||||
});
|
||||
ev.input = files;
|
||||
stop();
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = directories;
|
||||
|
||||
+49
-33
@@ -1,53 +1,69 @@
|
||||
var http = require('http');
|
||||
var internalIp = require('internal-ip');
|
||||
var router = require('router');
|
||||
var path = require('path');
|
||||
var serveMp4 = require('../utils/serve-mp4');
|
||||
var serveMp4 = require('../lib/serve-mp4');
|
||||
var debug = require('debug')('castnow:localfile');
|
||||
var fs = require('fs');
|
||||
var port = 4100;
|
||||
|
||||
var isFile = function(item) {
|
||||
return fs.existsSync(item.path) && fs.statSync(item.path).isFile();
|
||||
var isFile = function(path) {
|
||||
return fs.existsSync(path) && fs.statSync(path).isFile();
|
||||
};
|
||||
|
||||
var contains = function(arr, cb) {
|
||||
for (var i=0, len=arr.length; i<len; i++) {
|
||||
if (cb(arr[i], i)) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
var localfile = function(castnow) {
|
||||
var options = castnow.getOptions();
|
||||
var router = castnow.getRouter();
|
||||
var playlist = castnow.getPlaylist();
|
||||
var ip = options.ip || internalIp();
|
||||
|
||||
var localfile = function(ctx, next) {
|
||||
if (ctx.mode !== 'launch') return next();
|
||||
if (!contains(ctx.options.playlist, isFile)) return next();
|
||||
router.get('/localfile/:id', function(req, res) {
|
||||
var item = playlist.findItem(parseInt(req.params.id, 10));
|
||||
if (!item) return res.sendStatus(404);
|
||||
debug('incoming request serving %s', item.getSource());
|
||||
serveMp4(req, res, item.getSource());
|
||||
});
|
||||
|
||||
var route = router();
|
||||
var list = ctx.options.playlist.slice(0);
|
||||
var ip = (ctx.options.myip || internalIp());
|
||||
castnow.addItemCreator('localfile', function(o, cb) {
|
||||
var opts = {};
|
||||
var item;
|
||||
var url;
|
||||
|
||||
ctx.options.playlist = list.map(function(item, idx) {
|
||||
if (!isFile(item)) return item;
|
||||
return {
|
||||
path: 'http://' + ip + ':' + port + '/' + idx,
|
||||
type: 'video/mp4',
|
||||
if (typeof o === 'string') {
|
||||
opts.source = o;
|
||||
} else {
|
||||
opts = o;
|
||||
}
|
||||
|
||||
if (!isFile(opts.source)) return cb(new Error('not a valid file'));
|
||||
|
||||
item = castnow.createBlankItem();
|
||||
item.setSource(opts.source);
|
||||
|
||||
url = 'http://' + ip + ':' + options.port + '/localfile/' + item.getId();
|
||||
|
||||
item.setArgs({
|
||||
autoplay: true,
|
||||
currentTime: opts.start || 0,
|
||||
media: {
|
||||
contentId: url,
|
||||
contentType: 'video/mp4',
|
||||
streamType: 'BUFFERED',
|
||||
metadata: {
|
||||
title: path.basename(item.path)
|
||||
title: path.basename(item.getSource())
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
route.all('/{idx}', function(req, res) {
|
||||
debug('incoming request serving %s', list[req.params.idx].path);
|
||||
serveMp4(req, res, list[req.params.idx].path);
|
||||
});
|
||||
|
||||
http.createServer(route).listen(port);
|
||||
debug('started webserver on address %s using port %s', ip, port);
|
||||
next();
|
||||
|
||||
castnow.hook('resolve', function(ev, next, stop) {
|
||||
var input = ev.input;
|
||||
if (!isFile(input.source)) return next();
|
||||
debug('localfile detected: %s', source);
|
||||
castnow.createItem('localfile', input, function(err, item) {
|
||||
if (err) return stop(err);
|
||||
ev.item = item;
|
||||
return stop();
|
||||
});
|
||||
}, 600);
|
||||
};
|
||||
|
||||
module.exports = localfile;
|
||||
|
||||
+92
-30
@@ -1,41 +1,103 @@
|
||||
var readTorrent = require('read-torrent');
|
||||
var peerflix = require('peerflix');
|
||||
var torrentStream = require('torrent-stream');
|
||||
var internalIp = require('internal-ip');
|
||||
var grabOpts = require('../utils/grab-opts');
|
||||
var debug = require('debug')('castnow:torrent');
|
||||
var port = 4102;
|
||||
var pump = require('pump');
|
||||
var rangeParser = require('range-parser');
|
||||
var mime = require('mime');
|
||||
|
||||
var torrent = function(ctx, next) {
|
||||
if (ctx.mode !== 'launch') return next();
|
||||
if (ctx.options.playlist.length > 1) return next();
|
||||
var path = ctx.options.playlist[0].path;
|
||||
// the code here is mostly copied together from peerflix.
|
||||
// all credits go to mafintosh.
|
||||
|
||||
if (!/^magnet:/.test(path) &&
|
||||
!/torrent$/.test(path) &&
|
||||
!ctx.options.torrent) return next();
|
||||
var isTorrent = function(path) {
|
||||
return /^magnet:/.test(path) || /torrent$/.test(path);
|
||||
};
|
||||
|
||||
readTorrent(path, function(err, torrent) {
|
||||
if (err) {
|
||||
debug('error reading torrent: %o', err);
|
||||
return next();
|
||||
var torrent = function(castnow) {
|
||||
var router = castnow.getRouter();
|
||||
var playlist = castnow.getPlaylist();
|
||||
var ip = castnow.getOptions().ip || internalIp();
|
||||
var port = castnow.getOptions().port;
|
||||
|
||||
router.get('/torrent/:id', function(req, res) {
|
||||
var item = playlist.findItem(parseInt(req.params.id, 10));
|
||||
if (!item || !item.get('torrent')) return res.sendStatus(404);
|
||||
var file = item.get('file');
|
||||
var range = req.headers.range;
|
||||
var len = file.length;
|
||||
|
||||
res.setHeader('Content-Type', mime.lookup(file.name));
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
|
||||
if (!range) {
|
||||
res.setHeader('Content-Length', len);
|
||||
res.statusCode = 200;
|
||||
return pump(file.createReadStream(), res);
|
||||
}
|
||||
if (!ctx.options['peerflix-port']) ctx.options['peerflix-port'] = port;
|
||||
var engine = peerflix(torrent, grabOpts(ctx.options, 'peerflix-'));
|
||||
var ip = ctx.options.myip || internalIp();
|
||||
engine.server.once('listening', function() {
|
||||
debug('started webserver on address %s using port %s', ip, engine.server.address().port);
|
||||
ctx.options.playlist[0] = {
|
||||
path: 'http://' + ip + ':' + engine.server.address().port,
|
||||
type: 'video/mp4',
|
||||
media: {
|
||||
metadata: {
|
||||
title: engine.server.index.name
|
||||
}
|
||||
}
|
||||
};
|
||||
next();
|
||||
});
|
||||
|
||||
var part = rangeParser(len, range)[0];
|
||||
var chunksize = (part.end - part.start) + 1;
|
||||
|
||||
res.setHeader('Content-Range', 'bytes ' + part.start + '-' + part.end + '/' + len);
|
||||
res.setHeader('Accept-Ranges', 'bytes');
|
||||
res.setHeader('Content-Length', chunksize);
|
||||
res.statusCode = 206;
|
||||
|
||||
return pump(file.createReadStream(part), res);
|
||||
});
|
||||
|
||||
castnow.hook('resolve', function(ev, next, stop) {
|
||||
var item = ev.item;
|
||||
if (!isTorrent(item.getSource())) return next();
|
||||
|
||||
debug('torrent detected: %s', item.getSource());
|
||||
|
||||
readTorrent(item.getSource(), function(err, torr) {
|
||||
if (err) {
|
||||
debug('error reading torrent: %o', err);
|
||||
return stop(new Error('error reading torrent'));
|
||||
}
|
||||
var url = 'http://' + ip + ':' + port + '/torrent/' + item.getId();
|
||||
item.set('torrent', torr);
|
||||
item.setArgs({
|
||||
autoplay: true,
|
||||
currentTime: 0,
|
||||
media: {
|
||||
contentId: url,
|
||||
contentType: 'video/mp4',
|
||||
streamType: 'BUFFERED'
|
||||
}
|
||||
});
|
||||
item.enable();
|
||||
stop();
|
||||
});
|
||||
}, 800);
|
||||
|
||||
// TODO: Clean up stuff, once torrent is finished.
|
||||
|
||||
playlist.hook('load', function(ev, next) {
|
||||
var item = ev.item;
|
||||
if (!item.get('torrent')) return next();
|
||||
var torrentEngine = torrentStream(item.get('torrent'));
|
||||
|
||||
var onReady = function() {
|
||||
// extract largest file
|
||||
// (this will likely be the video file)
|
||||
var file = torrentEngine.files.reduce(function(a, b) {
|
||||
return a.length > b.length ? a : b;
|
||||
});
|
||||
file.select();
|
||||
item.set('file', file);
|
||||
next();
|
||||
};
|
||||
|
||||
if (torrentEngine.torrent) {
|
||||
onReady();
|
||||
} else {
|
||||
torrentEngine.once('ready', onReady);
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
module.exports = torrent;
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
var isUrl = require('is-url');
|
||||
var debug = require('debug')('castnow:url');
|
||||
|
||||
var url = function(castnow) {
|
||||
|
||||
castnow.addItemCreator('url', function(o, cb) {
|
||||
var opts = {};
|
||||
var item;
|
||||
var url;
|
||||
|
||||
if (typeof o === 'string') {
|
||||
opts.source = o;
|
||||
} else {
|
||||
opts = o;
|
||||
}
|
||||
|
||||
if (!isUrl(opts.source)) return cb(new Error('not a valid url'));
|
||||
|
||||
item = castnow.createBlankItem();
|
||||
item.setSource(opts.source);
|
||||
|
||||
item.setArgs({
|
||||
autoplay: true,
|
||||
currentTime: opts.start || 0,
|
||||
media: {
|
||||
contentId: opts.source,
|
||||
contentType: 'video/mp4',
|
||||
streamType: 'BUFFERED'
|
||||
}
|
||||
});
|
||||
|
||||
return cb(null, item);
|
||||
});
|
||||
|
||||
castnow.hook('resolve', function(ev, done, stop) {
|
||||
var input = ev.input;
|
||||
if (!isUrl(item.source)) return done();
|
||||
|
||||
debug('url detected', item.source);
|
||||
|
||||
castnow.createItem('url', input, function(err, item) {
|
||||
if (err) return stop(err);
|
||||
ev.item = item;
|
||||
return stop();
|
||||
});
|
||||
}, 500);
|
||||
|
||||
};
|
||||
|
||||
module.exports = url;
|
||||
+23
-16
@@ -16,25 +16,32 @@ Yt.APP_ID = '233637DE';
|
||||
inherits(Yt, Api);
|
||||
|
||||
Yt.prototype.load = function(options, cb) {
|
||||
var youtubeId = getYouTubeId(options.path);
|
||||
debug('loading video with id %s', youtubeId);
|
||||
var opts = {
|
||||
type: 'flingVideo',
|
||||
data: {
|
||||
currentTime: 0,
|
||||
videoId: youtubeId
|
||||
}
|
||||
};
|
||||
this.ytreq.request(opts);
|
||||
this.ytreq.request(options);
|
||||
if (cb) cb();
|
||||
};
|
||||
|
||||
var youtube = function(ctx, next) {
|
||||
if (ctx.mode !== 'launch') return next();
|
||||
if (!getYouTubeId(ctx.options.playlist[0].path)) return next();
|
||||
debug('using youtube api');
|
||||
ctx.api = Yt;
|
||||
next();
|
||||
var youtube = function(castnow) {
|
||||
|
||||
castnow.hook('resolve', function(ev, next, stop) {
|
||||
var item = ev.item;
|
||||
var youtubeId = getYouTubeId(item.getSource());
|
||||
if (!youtubeId) return next();
|
||||
debug('youtube url detected %s', item.getSource());
|
||||
|
||||
item.setApi('youtube', Yt);
|
||||
|
||||
item.setArgs({
|
||||
type: 'flingVideo',
|
||||
data: {
|
||||
currentTime: 0,
|
||||
videoId: youtubeId
|
||||
}
|
||||
});
|
||||
|
||||
item.enable();
|
||||
stop();
|
||||
}, 1000);
|
||||
|
||||
};
|
||||
|
||||
module.exports = youtube;
|
||||
|
||||
@@ -1,72 +1,48 @@
|
||||
var url = require('url');
|
||||
var got = require('got');
|
||||
var qs = require('query-string');
|
||||
var parser = require('xml2js').parseString;
|
||||
var debug = require('debug')('castnow:youtubeplaylist');
|
||||
var queryExtend = require('query-extend');
|
||||
|
||||
function getPlaylistItems(id, callback) {
|
||||
var getYoutubeItemUrl = function(id) {
|
||||
return 'https://www.youtube.com/watch?v=' + id;
|
||||
};
|
||||
|
||||
got('https://gdata.youtube.com/feeds/api/playlists/' + id + '?v=2&max-results=50', function get(err, data, res) {
|
||||
var getApiUrl = function(id) {
|
||||
return 'https://gdata.youtube.com/feeds/api/playlists/' + id + '?v=2&max-results=50';
|
||||
};
|
||||
|
||||
var videos = [];
|
||||
var isYoutubePlaylist = function(input) {
|
||||
return /youtube/.test(input) && /playlist\?list/.test(input);
|
||||
};
|
||||
|
||||
if (!err && res.statusCode === 200) {
|
||||
return parser(data, { normalizeTags: true, explicitArray: true }, function parse(err, result) {
|
||||
|
||||
var i;
|
||||
|
||||
for (i = 0; i < result.feed.entry.length; i++) {
|
||||
videos.push({path: 'https://www.youtube.com/watch?v=' + result.feed.entry[i]['media:group'][0]['yt:videoid']});
|
||||
}
|
||||
|
||||
callback(videos);
|
||||
var getListId = function(url) {
|
||||
return queryExtend(url, true).list;
|
||||
};
|
||||
|
||||
var getPlaylistItems = function(id, cb) {
|
||||
got(getApiUrl(id), function get(err, data, res) {
|
||||
if (err || res.statusCode !== 200) return cb(null, [])
|
||||
parser(data, { normalizeTags: true, explicitArray: true }, function(err, result) {
|
||||
if (err) return cb(null, []);
|
||||
var videos = result.feed.entry.map(function(entry) {
|
||||
return getYoutubeItemUrl(entry['media:group'][0]['yt:videoid']);
|
||||
});
|
||||
}
|
||||
|
||||
if (err) { console.log(err.stack); }
|
||||
|
||||
callback(videos);
|
||||
|
||||
cb(null, videos);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
}
|
||||
var youtubePlaylist = function(castnow) {
|
||||
|
||||
function updatePlaylist(stash, ctx, next) {
|
||||
var out = [], i;
|
||||
castnow.hook('flatten', function(ev, next, stop) {
|
||||
var input = ev.input;
|
||||
if (!isYoutubePlaylist(input)) return next();
|
||||
|
||||
for (i = 0; i < ctx.options.playlist.length; i++) {
|
||||
if (!stash[ctx.options.playlist[i]]) {
|
||||
out.push(ctx.options.playlist[i]);
|
||||
} else {
|
||||
out = out.concat(stash[ctx.options.playlist[i]]);
|
||||
}
|
||||
}
|
||||
ctx.options.playlist = out;
|
||||
next();
|
||||
}
|
||||
debug('youtube playlist detected %s', input);
|
||||
|
||||
var youtubePlaylist = function youtubePlaylist(ctx, next) {
|
||||
|
||||
if (ctx.mode !== 'launch') return next();
|
||||
|
||||
var items = [], stash = {}, count = 0, i;
|
||||
|
||||
for (i = 0; i < ctx.options.playlist.length; i++) {
|
||||
if (/youtube/.test(ctx.options.playlist[i].path) && /playlist\?list/.test(ctx.options.playlist[i].path)) {
|
||||
debug('loading youtube playlist %s', ctx.options.playlist[i].path);
|
||||
items.push(qs.parse(url.parse(ctx.options.playlist[i].path).query).list);
|
||||
ctx.options.playlist[i] = items.length;
|
||||
}
|
||||
}
|
||||
|
||||
if (!items.length) return next();
|
||||
|
||||
items.forEach(function grabDetails(item) {
|
||||
getPlaylistItems(item, function get(found) {
|
||||
count = count + 1;
|
||||
stash[count] = found;
|
||||
if (count === items.length) { updatePlaylist(stash, ctx, next); }
|
||||
getPlaylistItems(getListId(input), function(err, videos) {
|
||||
ev.input = videos;
|
||||
stop();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
var test = require('tape');
|
||||
var castnow = require('./castnow')();
|
||||
var ENGINE_STATES = require('./lib/engine').STATES;
|
||||
var scanner = require('chromecast-scanner');
|
||||
var urlPlugin = require('./plugins/url');
|
||||
var async = require('async');
|
||||
var demo = 'http://commondatastorage.googleapis.com/gtv-videos-bucket/ED_1280.mp4';
|
||||
|
||||
castnow.use(urlPlugin);
|
||||
|
||||
test('castnow engine', function(t) {
|
||||
var engine = castnow.getEngine();
|
||||
|
||||
async.waterfall([
|
||||
function(next) {
|
||||
scanner(function(err, service) {
|
||||
if (err) return next(err);
|
||||
return next(null, service.address);
|
||||
});
|
||||
},
|
||||
|
||||
function(address, next) {
|
||||
t.equal(engine.getState(), ENGINE_STATES.not_connected, 'engine should not be connected');
|
||||
engine.connect(address, function() {
|
||||
t.equal(engine.getState(), ENGINE_STATES.connected, 'engine should be connected');
|
||||
next();
|
||||
});
|
||||
},
|
||||
|
||||
function(next) {
|
||||
castnow.createItem('url', demo, function(err, item) {
|
||||
next(null, item);
|
||||
});
|
||||
},
|
||||
|
||||
function(item, next) {
|
||||
engine.load(item, function(err) {
|
||||
t.equal(err, null, 'there should not be any load error');
|
||||
t.equal(engine.getState(), ENGINE_STATES.launched, 'engine should be in launch state')
|
||||
next(null, item);
|
||||
});
|
||||
},
|
||||
|
||||
function(item, next) {
|
||||
setTimeout(function() {
|
||||
engine.load(item, function() {
|
||||
next();
|
||||
});
|
||||
}, 5000);
|
||||
},
|
||||
|
||||
function(next) {
|
||||
setTimeout(function() {
|
||||
engine.close();
|
||||
setTimeout(function() {
|
||||
t.equal(engine.getState(), ENGINE_STATES.not_connected, 'engine should not be connected');
|
||||
next();
|
||||
}, 1000);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
],
|
||||
function(err) {
|
||||
if (err) {
|
||||
t.fail(err.message);
|
||||
}
|
||||
t.end();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
module.exports = function(options, prefix) {
|
||||
var opts = {};
|
||||
var len = prefix.length;
|
||||
for (var key in options) {
|
||||
if (key.substr(0, len) === prefix) {
|
||||
opts[key.substr(len)] = options[key];
|
||||
}
|
||||
}
|
||||
return opts;
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
module.exports = function (string) {
|
||||
var timeArray = string.split(':'),
|
||||
seconds = 0;
|
||||
// turn hours and minutes into seconds and add them all up
|
||||
if (timeArray.length === 3) {
|
||||
// hours
|
||||
seconds = seconds + (parseInt(timeArray[0]) * 60 * 60);
|
||||
// minutes
|
||||
seconds = seconds + (parseInt(timeArray[1]) * 60);
|
||||
// seconds
|
||||
seconds = seconds + parseInt(timeArray[2]);
|
||||
} else if (timeArray.length === 2) {
|
||||
// minutes
|
||||
seconds = seconds + (parseInt(timeArray[0]) * 60);
|
||||
// seconds
|
||||
seconds = seconds + parseInt(timeArray[1]);
|
||||
}
|
||||
return seconds;
|
||||
};
|
||||
Referência em uma Nova Issue
Bloquear um usuário