Browse Source

modified version to stream to web page

master
mortie 7 years ago
parent
commit
daf30e4f46
10 changed files with 19 additions and 485 deletions
  1. 1
    52
      README.md
  2. 1
    3
      conf.json.example
  3. 0
    9
      js/notify.js
  4. 3
    51
      js/play/index.js
  5. 0
    54
      js/play/subtitle-finder.js
  6. 8
    28
      server.js
  7. 0
    6
      web/index.html
  8. 4
    33
      web/playback/index.html
  9. 0
    165
      web/playback/script.js
  10. 2
    84
      web/playback/style.css

+ 1
- 52
README.md View File

# MMPC2 # MMPC2


The second version of Mort's Media PC.

## Goal

MMPC2 is meant to be running constantly on a HTPC, letting you stream media to
a TV and control the playback remotely, all through a web interface. It allows
you to stream three kinds of movies:

* URLs - YouTube, Vimeo, Dailymotion, plain HTTP streams, anything supported by
mpv and youtube-dl.
* Magnet Links - Stream any torrent.
* Files - Upload files and have them play.

For magnet links and files, subtitles will automatically be downloaded if you
want and subtitles exist for it in OpenSubtitles.
Modified version of mmpc2.


## Installation ## Installation


With rolling release distros, the version of node in the package manager will With rolling release distros, the version of node in the package manager will
generally be new enough - e.g `sudo pacman -S npm node` in arch is enough. generally be new enough - e.g `sudo pacman -S npm node` in arch is enough.


### Getting a recent version of mpv

MMPC2 requires a relatively new version of mpv, newer than what's currently in
Debian and Ubuntu. For Ubuntu, you could just add this ppa to your system:
https://launchpad.net/~mc3man/+archive/Ubuntu/mpv-tests

sudo add-apt-repository ppa:mc3man/mpv-tests
sudo apt-get update
sudo apt-get install mpv

For Debian, you will probably have to either use the testing repositories to
install mpv, get some binary from somewhere, or compile it from source. I
compiled from source when installing it on the Debian stable (jessie), and the
instructions are a bit too long to include here, but you can get the source
from here: https://github.com/mpv-player/mpv/releases/latest and get the source
there. Make sure to compile with luajit support for youtube-dl, and libpulse
for pulseaudio (if you use that).

### Getting a recent version of youtbe-dl

Debian may ship a version of youtube-dl that's so old it doesn't really work
anymore - I had that problem with my Debian box. To fix that, uninstall
youtube-dl if you installed with apt-get (`apt-get remove youtube-dl`), and
install it through pip (installing pip if necessary)

sudo apt-get install python-pip
sudo pip install youtube-dl

## Configuration ## Configuration


`conf.json` contains a couple of configuration options (assuming you copied `conf.json` contains a couple of configuration options (assuming you copied
{ {
"tmpdir": String. The directory to store temporary files in. "tmpdir": String. The directory to store temporary files in.
Default: "tmp" Default: "tmp"

"subtitles": String (or false). The language code for the subtitles,
or false for no subtitles. Default: "en" (english).

"additional_links": Array of objects which look like this:
{ "name": "some name", "url": "some URL" }
Lets you add more parts to the index page. I use it to link to an
instance of guacamole, a web based VNC client.
( https://guacamole.incubator.apache.org/ )
} }

+ 1
- 3
conf.json.example View File

{ {
"tmpdir": "tmp",
"subtitles": "en",
"additional_links": []
"tmpdir": "tmp"
} }

+ 0
- 9
js/notify.js View File

var spawn = require("child_process").spawn;

module.exports = function(name, msg) {
var args = ["--urgency", "low"];
args.push(name);
if (msg) args.push(msg);

spawn("notify-send", args);
}

+ 3
- 51
js/play/index.js View File

var pathlib = require("path"); var pathlib = require("path");
var urllib = require("url"); var urllib = require("url");
var fsutil = require("../fsutil"); var fsutil = require("../fsutil");
var notify = require("../notify");
var player = require("./player"); var player = require("./player");
var httpStream = require("./http-stream"); var httpStream = require("./http-stream");
var torrentStreamer = require("./torrent-streamer"); var torrentStreamer = require("./torrent-streamer");
var subtitleFinder = require("./subtitle-finder");


exports.httpPath = player.httpPath;
exports.httpPath = "/playback";


exports.cleanupFiles = []; exports.cleanupFiles = [];


player.init(app, conf); player.init(app, conf);
httpStream.init(app, conf); httpStream.init(app, conf);
torrentStreamer.init(app, conf); torrentStreamer.init(app, conf);
subtitleFinder.init(app, conf);
}

/*
* Filename is optional; in case you want to provide a filename for subtitles
* but want a different path
*/
exports.playFile = function(path, cb, filename) {
filename = filename || pathlib.basename(path);

notify("Playing file", filename);

// Find file's length
fs.stat(path, (err, stat) => {
if (err) {
console.trace(err);
return cb();
}

// Find subtitles
subtitleFinder.find(stat.size, filename, subFile => {

// Play!
player.play(path, subFile, cb);
});
});
}

exports.playUrl = function(url, cb) {

notify("Playing url...", url);

// Just play, we won't bother finding subtitles
player.play(url, null, cb);
} }


exports.playTorrent = function(magnet, cb) { exports.playTorrent = function(magnet, cb) {


notify("Playing torrent...");

// Stream torrent // Stream torrent
torrentStreamer.stream(magnet, (err, filesize, filename) => { torrentStreamer.stream(magnet, (err, filesize, filename) => {
if (err) if (err)
return cb(err); return cb(err);


// Find subtitles
subtitleFinder.find(filesize, filename, subFile => {

// Play!
notify("Starting playback.", filename);
player.play(app.getAddress()+httpStream.httpPath, subFile, cb);
});
cb();
}); });
} }


return match[1]; return match[1];
} }


notify("Finding magnet on torrent page...", url);

var urlobj = urllib.parse(url); var urlobj = urllib.parse(url);
var o = urlobj.protocol === "https:" ? https : http; var o = urlobj.protocol === "https:" ? https : http;
o.request(urlobj, res => { o.request(urlobj, res => {
res res
.on("data", d => str += d ) .on("data", d => str += d )
.on("error", err => { .on("error", err => {
notify("Error downloading page!", err.toString());
console.trace(err); console.trace(err);
cb(); cb();
}) })
.on("end", () => { .on("end", () => {
var magnet = findMagnet(str); var magnet = findMagnet(str);
if (!magnet) { if (!magnet) {
notify("No magnet link on page!");
cb();
cb("No magnet link on page!");
return; return;
} }



+ 0
- 54
js/play/subtitle-finder.js View File

var OpenSubtitles = require("opensubtitles-api");
var fs = require("fs");
var http = require("http");
var urllib = require("url");

var subs = new OpenSubtitles({ useragent: "mmpc-media-streamer" });

var conf;

exports.init = function(app, _conf) {
conf = _conf;
}

exports.find = function(filesize, filename, cb) {
if (!conf.subtitles)
return cb();

var subFile = conf.tmpdir+"/subs.srt";
try {
fs.unlinkSync(subFile);
} catch (err) {}

subs.search({
sublanguageid: conf.subtitles,
filesize: filesize,
filename: filename
}).then(subtitles => {
var sub = subtitles[conf.subtitles];
if (!sub || !sub.url)
return cb();

try {
var ws = fs.createWriteStream(subFile);
} catch (err) {
console.trace(err);
cb();
}

http.request(urllib.parse(sub.url), res => {
res.pipe(ws);

res
.on("error", err => {
notify("Error finding subtitles", err.toString());
console.trace(err);
cb();
})
.on("end", () => {
ws.close();
setTimeout(() => cb(subFile), 500);
});
}).end();
});
}

+ 8
- 28
server.js View File

var fs = require("fs"); var fs = require("fs");
var pathlib = require("path"); var pathlib = require("path");
var web = require("webstuff"); var web = require("webstuff");
var notify = require("./js/notify");
var play = require("./js/play"); var play = require("./js/play");
var fsutil = require("./js/fsutil"); var fsutil = require("./js/fsutil");


if (!fields.url) if (!fields.url)
return res.redirect("/"); return res.redirect("/");


function cb() {
res.redirect(play.httpPath);
function cb(err) {
if (err) {
console.log(err);
res.redirect("/");
} else {
res.redirect(play.httpPath);
}
} }


if (fields.url.indexOf("magnet:") === 0) { if (fields.url.indexOf("magnet:") === 0) {
} else if (fields.url.indexOf("/torrent") !== -1) { } else if (fields.url.indexOf("/torrent") !== -1) {
play.playTorrentPage(fields.url, cb); play.playTorrentPage(fields.url, cb);
} else { } else {
play.playUrl(fields.url, cb);
}
});
});

app.post("/play/file", (req, res) => {
notify("Receiving file...");
req.parseBody((err, fields, files) => {
var file = files.file;

if (file == null || !file.name || file.size === 0)
return res.redirect("/"); return res.redirect("/");

var fname = conf.tmpdir+"/uploaded-file"+pathlib.extname(file.name);
fsutil.move(file.path, fname, err => {
if (err) {
console.trace(err);
return res.redirect("/");
}

play.cleanupFiles.push(fname);

play.playFile(fname, () => {
res.redirect(play.httpPath);
}, file.name);
});
}
}); });
}); });



+ 0
- 6
web/index.html View File

<input type="url" name="url" autocomplete="off"> <input type="url" name="url" autocomplete="off">
<button>Play</button> <button>Play</button>
</form> </form>

<form class="part" method="post" enctype="multipart/form-data" action="/play/file">
<div class="name">File:</div>
<input type="file" name="file" autocomplete="off">
<button>Play</button>
</form>
</div> </div>


<script src="/webstuff.js"></script> <script src="/webstuff.js"></script>

+ 4
- 33
web/playback/index.html View File

<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
</head> </head>
<body> <body>
<div id="group-info">
<div id="is-playing">Not Playing</div>
<div id="progress-text"></div>
</div>

<div id="group-bar">
<progress id="progress"></progress>
</div>

<div id="group-buttons">
<button id="mute">Mute</button>
<button id="skip-back">&lt;</button>
<button id="pause">||</button>
<button id="skip-forward">&gt;</button>
<button id="exit">Exit</button>
</div>

<div id="group-volume">
Volume <span id="volume-text">0</span>%<br>
<input id="volume" type="range">
</div>

<div id="group-sub-delay">
Subtitle Delay <span id="sub-delay">0</span>s<br>
<button id="sub-delay-less2">-1</button>
<button id="sub-delay-less">-0.1</button>
<button id="sub-delay-reset">0</button>
<button id="sub-delay-more">+0.1</button>
<button id="sub-delay-more2">+1</button>
</div>

<script src="/webstuff.js"></script>
<script src="script.js"></script>
<video controls autoplay>
<source src="/http-stream">
Your browser does not support the video tag.
</video>
</body> </body>
</html> </html>

+ 0
- 165
web/playback/script.js View File

function timeformat(sec) {
var d = new Date(null);
d.setSeconds(Math.floor(sec))
return d.toISOString().substr(11, 8);
}

var elems = {
is_playing: $$("#is-playing"),
progress_text: $$("#progress-text"),
progress: $$("#progress"),

pause: $$("#pause"),
skip_back: $$("#skip-back"),
skip_forward: $$("#skip-forward"),

mute: $$("#mute"),
exit: $$("#exit"),

volume: $$("#volume"),
volume_text: $$("#volume-text"),

sub_delay: $$("#sub-delay"),
sub_delay_less: $$("#sub-delay-less"),
sub_delay_less2: $$("#sub-delay-less2"),
sub_delay_more: $$("#sub-delay-more"),
sub_delay_more2: $$("#sub-delay-more2"),
sub_delay_reset: $$("#sub-delay-reset")
};

var state = {};

/*
* Update GUI stuff
*/

function update(state) {

// Playing
if (state.playing)
elems.is_playing.innerHTML = "Playing";
else
location.href = "/";

// Progress text
elems.progress_text.innerHTML =
timeformat(state.time_pos)+"/"+
timeformat(state.duration);

// Progress bar
elems.progress.value = state.time_pos;
elems.progress.max = state.duration;

// Buttons
elems.pause.className = state.paused ? "active" : "";
elems.mute.className = state.muted ? "active" : "";

// Volume
elems.volume.min = 0;
elems.volume.max = state.volume_max;
elems.volume.value = state.volume;
elems.volume.step = 5;
elems.volume_text.innerHTML = state.volume;

// Sub Delay
elems.sub_delay.innerHTML = state.sub_delay;
}

function checkState() {
post("/playback/state", null, function(err, res) {
if (err)
return;

state = JSON.parse(res);
update(state);
});
}

checkState();
setInterval(checkState, 500);

/*
* React to input
*/

function playerset(key, val) {
post("/playback/set/"+key+"/"+val, null, function() {
checkState();
});
}

// Set time
elems.progress.addEventListener("click", function(evt) {
var pos = (evt.clientX - evt.target.offsetLeft) / evt.target.clientWidth;
pos *= evt.target.max;
playerset("time-pos", pos);
});

// Toggle pause
elems.pause.addEventListener("click", function() {
if (state.paused) {
playerset("pause", "no");
} else {
playerset("pause", "yes");
playerset("time-pos", state.time_pos - 1);
}
});

// Back 15 seconds
elems.skip_back.addEventListener("click", function() {
playerset("time-pos", state.time_pos - 15);
});

// Forwards 15 seconds
elems.skip_forward.addEventListener("click", function() {
playerset("time-pos", state.time_pos + 15);
});

// Toggle mute
elems.mute.addEventListener("click", function(evt) {
if (state.muted)
playerset("mute", "no");
else
playerset("mute", "yes");
});

// Exit
elems.exit.addEventListener("click", function(evt) {
post("/playback/exit", null, function() {});
});

// Set volume
elems.volume.addEventListener("change", function(evt) {
playerset("volume", evt.target.value);
});
elems.volume.addEventListener("keydown", function(evt) {
if (evt.keyCode === 37 || evt.keyCode === 40)
playerset("volume", parseInt(evt.target.value) - parseInt(evt.target.step));
else if (evt.keyCode === 38 || evt.keyCode === 39)
playerset("volume", parseInt(evt.target.value) + parseInt(evt.target.step));
});

// Less subtitle delay
elems.sub_delay_less.addEventListener("click", function() {
playerset("sub-delay", state.sub_delay - 0.1);
});
elems.sub_delay_less2.addEventListener("click", function() {
playerset("sub-delay", state.sub_delay - 1);
});

// More subtitle delay
elems.sub_delay_more.addEventListener("click", function() {
playerset("sub-delay", state.sub_delay + 0.1);
});
elems.sub_delay_more2.addEventListener("click", function() {
playerset("sub-delay", state.sub_delay + 1);
});

// Set subtitle delay to 0
elems.sub_delay_reset.addEventListener("click", function() {
playerset("sub-delay", 0);
});

window.addEventListener("keydown", function(evt) {
console.log(evt.keyCode);
});

+ 2
- 84
web/playback/style.css View File

* {
box-sizing: border-box;
line-height: 30px;
font-family: Sans-Serif
}

body { body {
margin: 12px;
}

body > div {
margin-bottom: 12px;
}

button {
display: inline-block;
padding: 0px;
border: 1px solid #666;
width: 50px;
height: 32px;
background: #ddd;
margin-left: 2px;
margin-right: 2px;
}

button:active,
button.active {
background: #aaa;
}

/*
* Playback
*/

#is-playing,
#progress-text {
display: inline-block;
}
#progress-text {
float: right;
}
#group-buttons {
text-align: center;
}

progress {
width: 100%;
height: 24px;
}

/*
* Volume
*/

input[type="range"] {
border: 1px solid rgba(0, 0, 0, 0);
box-sizing: border-box;
margin: 0px; margin: 0px;
width: 100%;
}

#group-volume {
text-align: center;
border-top: 1px solid #ccc;
}

#group-volume {
padding-top: 12px;
margin-top: 24px;
} }


/*
* Sub Delay
*/

#group-sub-delay {
text-align: center;
}

#group-sub-delay {
border-top: 1px solid #ccc;
}

#group-sub-delay {
padding-top: 12px;
}
#group-sub-delay button {
margin-top: 6px;
video {
width: 100%;
} }

Loading…
Cancel
Save