@@ -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; | |||
} |