| @@ -1 +1,2 @@ | |||
| node_modules | |||
| conf.json | |||
| @@ -0,0 +1,3 @@ | |||
| { | |||
| "tmpdir": "tmp" | |||
| } | |||
| @@ -0,0 +1,61 @@ | |||
| var mime = require("mime"); | |||
| /* | |||
| * Must be set to a function which takes an options argument with | |||
| * 'start' and 'end', and returns a readStream. | |||
| * The readStream must have the additional properties | |||
| * 'filesize' and 'filename' set. | |||
| */ | |||
| exports.readStreamCreator = null; | |||
| exports.httpPath = "/http-stream"; | |||
| exports.init = function(app) { | |||
| app.get("/http-stream", (req, res) => { | |||
| if (exports.readStreamCreator == null) { | |||
| res.writeHead(500); | |||
| throw "readStreamCreator not set!"; | |||
| } | |||
| var rs; | |||
| var parts = []; | |||
| if (req.headers.range) { | |||
| parts = req.headers.range.replace("bytes=", "").split("-"); | |||
| var start = parseInt(parts[0]); | |||
| var end = parseInt(parts[1]); | |||
| var options = {}; | |||
| if (!isNaN(start)) options.start = start; | |||
| if (!isNaN(end)) options.end = end; | |||
| rs = exports.readStreamCreator(options); | |||
| } else { | |||
| rs = exports.readStreamCreator(); | |||
| } | |||
| var start = parseInt(parts[0]) || 0; | |||
| var end = parts[1] ? parseInt(parts[1]) : rs.filesize - 1; | |||
| var chunksize = end - start + 1; | |||
| if (chunksize > rs.filesize || start > end || end >= rs.filesize) { | |||
| res.writeHead(416); | |||
| res.end("Range not satisfiable. Start: "+start+", end: "+end); | |||
| return; | |||
| } | |||
| res.writeHead(req.headers.range ? 206 : 200, { | |||
| "icy-name": rs.filename, | |||
| "content-length": chunksize, | |||
| "content-type": mime.lookup(rs.filename), | |||
| "content-range": "bytes "+start+"-"+end+"/"+rs.filesize, | |||
| "accept-ranges": "bytes" | |||
| }); | |||
| if (req.method === "HEAD") | |||
| return res.end(); | |||
| rs.pipe(res); | |||
| }); | |||
| } | |||
| exports.stop = function() { | |||
| exports.readStreamCreator = null; | |||
| } | |||
| @@ -1,11 +1,34 @@ | |||
| var player = require("./player.js"); | |||
| var player = require("./player"); | |||
| var httpStream = require("./http-stream"); | |||
| var torrentStreamer = require("./torrent-streamer"); | |||
| exports.init = function(app) { | |||
| player.init(app); | |||
| exports.httpPath = player.httpPath; | |||
| var app; | |||
| exports.init = function(_app, conf) { | |||
| app = _app; | |||
| player.init(app, conf); | |||
| httpStream.init(app, conf); | |||
| torrentStreamer.init(app, conf); | |||
| } | |||
| exports.playFile = function(path, cb) { | |||
| player.play(path, cb); | |||
| } | |||
| exports.playTorrent = function(magnet, cb) { | |||
| torrentStreamer.stream(magnet, err => { | |||
| if (err) | |||
| return cb(err); | |||
| player.play(app.getAddress()+httpStream.httpPath, cb); | |||
| }); | |||
| } | |||
| exports.isPlaying = player.isPlaying; | |||
| player.onstop = function() { | |||
| torrentStreamer.stop(); | |||
| httpStream.stop(); | |||
| } | |||
| @@ -3,6 +3,8 @@ var fs = require("fs"); | |||
| var net = require("net"); | |||
| var Queue = require("../queue"); | |||
| exports.httpPath = "/playback"; | |||
| var child = null; | |||
| var ipcServer = process.cwd()+"/mpv-ipc-socket"; | |||
| @@ -86,13 +88,15 @@ exports.play = function(path, cb) { | |||
| var lchild = spawn("mpv", [ | |||
| path, | |||
| "--input-ipc-server", ipcServer | |||
| "--no-cache-pause", | |||
| "--input-unix-socket", ipcServer | |||
| ], { stdio: "inherit" }); | |||
| child = lchild; | |||
| lchild.running = true; | |||
| lchild.once("close", () => { | |||
| console.log("child closed"); | |||
| if (lchild.running) exports.stop(); | |||
| }); | |||
| lchild.on("error", err => console.error(err.toString())); | |||
| @@ -100,9 +104,7 @@ exports.play = function(path, cb) { | |||
| lchild.state = {}; | |||
| lchild.msgqueue = Queue(); | |||
| console.log("lchild is "+lchild); | |||
| lchild.initTimeout = setTimeout(() => { | |||
| console.log("lchild still is "+lchild); | |||
| // Create socket | |||
| lchild.sock = net.connect(ipcServer, () => { | |||
| @@ -118,7 +120,6 @@ exports.play = function(path, cb) { | |||
| lchild.sock.on("error", err => console.trace(err)); | |||
| // Set fullscreen | |||
| cmd(["set_property", "fullscreen", "yes"]); | |||
| cb(); | |||
| @@ -132,6 +133,8 @@ exports.stop = function() { | |||
| child.kill("SIGKILL"); | |||
| clearTimeout(child.initTimeout); | |||
| child = null; | |||
| exports.onstop(); | |||
| } | |||
| try { | |||
| fs.unlinkSync(ipcServer); | |||
| @@ -140,7 +143,7 @@ exports.stop = function() { | |||
| exports.init = function(app) { | |||
| function evt(p, cb) { | |||
| app.post("/playback/"+p, (req, res) => cb(req, res)); | |||
| app.post(exports.httpPath+"/"+p, (req, res) => cb(req, res)); | |||
| } | |||
| evt("exit", (req, res) => { res.end(); exports.stop() }); | |||
| @@ -0,0 +1,56 @@ | |||
| var httpStream = require("./http-stream"); | |||
| var torrentStream = require("torrent-stream"); | |||
| var mediarx = /\.(mp4|mkv|mov|avi|ogv)$/; | |||
| var tmpdir = process.cwd()+"/tmp"; | |||
| var engine; | |||
| var conf; | |||
| exports.init = function(app, _conf) { | |||
| conf = _conf; | |||
| } | |||
| exports.stream = function(magnet, cb) { | |||
| if (engine) | |||
| return engine.destroy(() => | |||
| { engine = null; exports.stream(magnet, cb) }); | |||
| engine = torrentStream(magnet, { | |||
| tmp: conf.tmpdir | |||
| }); | |||
| engine.on("ready", () => { | |||
| var file = null; | |||
| engine.files.forEach(f => { | |||
| if (mediarx.test(f.name) && | |||
| (file == null || f.length > file.length)) { | |||
| file = f; | |||
| } | |||
| }); | |||
| if (file == null) | |||
| return cb("No media file in the torrent"); | |||
| file.select(); | |||
| httpStream.readStreamCreator = function(options) { | |||
| console.log("creating stream with", options); | |||
| var rs = file.createReadStream(options); | |||
| rs.filesize = file.length; | |||
| rs.filename = file.name; | |||
| rs.on("close", () => console.log("stream closing")); | |||
| return rs; | |||
| } | |||
| cb(); | |||
| }); | |||
| } | |||
| exports.stop = function() { | |||
| if (engine != null) | |||
| engine.destroy(); | |||
| } | |||
| @@ -10,6 +10,8 @@ | |||
| "author": "Martin Dørum Nygaard <martid0311@gmail.com> (http://mort.coffee)", | |||
| "license": "ISC", | |||
| "dependencies": { | |||
| "webstuff": "^1.3.0" | |||
| "mime": "^1.3.4", | |||
| "torrent-stream": "^1.0.3", | |||
| "webstuff": "^1.4.0" | |||
| } | |||
| } | |||
| @@ -1,21 +1,41 @@ | |||
| var fs = require("fs"); | |||
| var web = require("webstuff"); | |||
| var play = require("./js/play"); | |||
| var magnet = "magnet:?xt=urn:btih:13241fe16a2797b2a41b7822bde970274d6b687c&dn=Mad+Max%3A+Fury+Road+%282015%29+1080p+BrRip+x264+-+YIFY&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Fzer0day.ch%3A1337&tr=udp%3A%2F%2Fopen.demonii.com%3A1337&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Fexodus.desync.com%3A6969"; | |||
| var conf = JSON.parse(fs.readFileSync("conf.json")); | |||
| var app = web(); | |||
| play.init(app); | |||
| play.init(app, conf); | |||
| app.express.use((req, res, next) => { | |||
| if (req.url === "/" && play.isPlaying()) | |||
| res.redirect("/playback"); | |||
| res.redirect(play.httpPath); | |||
| else | |||
| next(); | |||
| }); | |||
| app.static("web"); | |||
| app.post("/plaything", (req, res) => { | |||
| play.playFile("/home/martin/Documents/Assassination Classroom - E01.mkv", () => { | |||
| res.redirect("/playback"); | |||
| app.post("/play/link", (req, res) => { | |||
| req.parseBody((err, fields) => { | |||
| if (!fields.url) | |||
| return res.redirect("/"); | |||
| play.playFile(fields.url, () => { | |||
| res.redirect(play.httpPath); | |||
| }); | |||
| }); | |||
| }); | |||
| app.post("/play/magnet", (req, res) => { | |||
| req.parseBody((err, fields) => { | |||
| if (!fields.magnet) | |||
| return res.redirect("/"); | |||
| play.playTorrent(fields.magnet, () => { | |||
| res.redirect(play.httpPath); | |||
| }); | |||
| }); | |||
| }); | |||
| @@ -3,12 +3,24 @@ | |||
| <head> | |||
| <meta charset="utf-8"> | |||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |||
| <link rel="stylesheet" href="style.css"> | |||
| <title>Mort's Media PC</title> | |||
| </head> | |||
| <body> | |||
| <form method="post" action="/plaything"> | |||
| <button>Hello There!</button> | |||
| </form> | |||
| <div id="parts"> | |||
| <form class="part" method="post" action="/play/link"> | |||
| <div class="name">URL:</div> | |||
| <input type="url" name="url" autocomplete="off"> | |||
| <button>Play</button> | |||
| </form> | |||
| <form class="part" method="post" action="/play/magnet"> | |||
| <div class="name">Magnet Link:</div> | |||
| <input type="url" name="magnet" autocomplete="off"> | |||
| <button>Play</button> | |||
| </form> | |||
| </div> | |||
| <script src="/webstuff.js"></script> | |||
| </body> | |||
| </html> | |||
| @@ -120,3 +120,13 @@ elems.exit.addEventListener("click", function(evt) { | |||
| 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)); | |||
| }); | |||
| window.addEventListener("keydown", function(evt) { | |||
| console.log(evt.keyCode); | |||
| }); | |||
| @@ -0,0 +1,32 @@ | |||
| * { | |||
| box-sizing: border-box; | |||
| } | |||
| #parts { | |||
| max-width: 600px; | |||
| margin: auto; | |||
| } | |||
| #parts .part { | |||
| border-bottom: 1px solid #eee; | |||
| padding: 20px 10px; | |||
| padding-top: 17px; | |||
| } | |||
| #parts .part:last-child { | |||
| border-bottom: none; | |||
| } | |||
| #parts .part .name { | |||
| margin-bottom: 6px; | |||
| } | |||
| #parts .part input { | |||
| width: calc(100% - 80px); | |||
| } | |||
| #parts .part button { | |||
| width: 75px; | |||
| float: right; | |||
| } | |||
| #parts .part input, | |||
| #parts .part button { | |||
| height: 25px; | |||
| } | |||