diff --git a/copyparty/__main__.py b/copyparty/__main__.py index fb969600..51348e2b 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -324,6 +324,9 @@ def run_argparse(argv, formatter): ap2.add_argument("-mtp", metavar="M=[f,]bin", action="append", type=str, help="read tag M using bin") ap2.add_argument("--srch-time", metavar="SEC", type=int, default=30, help="search deadline") + ap2 = ap.add_argument_group('video streaming options') + ap2.add_argument("--vcr", action="store_true", help="enable video streaming") + ap2 = ap.add_argument_group('appearance options') ap2.add_argument("--css-browser", metavar="L", help="URL to additional CSS to include") diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index b50599c6..8b0f2a77 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -18,6 +18,8 @@ from .util import * # noqa # pylint: disable=unused-wildcard-import from .authsrv import AuthSrv from .szip import StreamZip from .star import StreamTar +from .vcr import VCR_Direct +from .th_srv import FMT_FF if not PY2: unicode = str @@ -229,7 +231,9 @@ class HttpCli(object): def send_headers(self, length, status=200, mime=None, headers={}): response = ["{} {} {}".format(self.http_ver, status, HTTPCODE[status])] - if length is not None: + if length is None: + self.keepalive = False + else: response.append("Content-Length: " + unicode(length)) # close if unknown length, otherwise take client's preference @@ -1563,6 +1567,15 @@ class HttpCli(object): if rem.startswith(".hist/up2k."): raise Pebkac(403) + if "vcr" in self.uparam: + ext = abspath.rsplit(".")[-1] + if not self.args.vcr or ext not in FMT_FF: + raise Pebkac(403) + + vcr = VCR_Direct(self, abspath) + vcr.run() + return False + is_dir = stat.S_ISDIR(st.st_mode) th_fmt = self.uparam.get("th") if th_fmt is not None: diff --git a/copyparty/vcr.py b/copyparty/vcr.py new file mode 100644 index 00000000..ead51e6d --- /dev/null +++ b/copyparty/vcr.py @@ -0,0 +1,80 @@ +# coding: utf-8 +from __future__ import print_function, unicode_literals + +import time +import shlex +import subprocess as sp + +from .__init__ import PY2 +from .util import fsenc + + +class VCR_Direct(object): + def __init__(self, cli, fpath): + self.cli = cli + self.fpath = fpath + + self.log_func = cli.log_func + self.log_src = cli.log_src + + def log(self, msg, c=0): + self.log_func(self.log_src, "vcr: {}".format(msg), c) + + def run(self): + opts = self.cli.uparam + + # fmt: off + cmd = [ + "ffmpeg", + "-nostdin", + "-hide_banner", + "-v", "warning", + "-i", fsenc(self.fpath), + "-vf", "scale=640:-4", + "-c:a", "libopus", + "-b:a", "128k", + "-c:v", "libvpx", + "-deadline", "realtime", + "-row-mt", "1" + ] + # fmt: on + + if "ss" in opts: + cmd.extend(["-ss", opts["ss"]]) + + if "crf" in opts: + cmd.extend(["-b:v", "0", "-crf", opts["crf"]]) + else: + cmd.extend(["-b:v", "{}M".format(opts.get("mbps", 1.2))]) + + cmd.extend(["-f", "webm", "-"]) + + comp = str if not PY2 else unicode + cmd = [x.encode("utf-8") if isinstance(x, comp) else x for x in cmd] + + self.log(" ".join([shlex.quote(x.decode("utf-8", "replace")) for x in cmd])) + + p = sp.Popen(cmd, stdout=sp.PIPE) + + self.cli.send_headers(None, mime="video/webm") + + fails = 0 + while True: + self.log("read") + buf = p.stdout.read(1024 * 4) + if not buf: + fails += 1 + if p.poll() is not None or fails > 30: + self.log("ffmpeg exited") + return + + time.sleep(0.1) + continue + + fails = 0 + try: + self.cli.s.sendall(buf) + except: + self.log("client disconnected") + p.kill() + return diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 2bbfca51..b9c8de99 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -1552,6 +1552,7 @@ var thegrid = (function () { href = this.getAttribute('href'), aplay = ebi('a' + oth.getAttribute('id')), is_img = /\.(gif|jpe?g|png|webp)(\?|$)/i.test(href), + is_vid = /\.(av1|asf|avi|flv|m4v|mkv|mjpeg|mjpg|mpg|mpeg|mpg2|mpeg2|h264|avc|h265|hevc|mov|3gp|mp4|ts|mpegts|nut|ogv|ogm|rm|vob|webm|wmv)(\?|$)/i.test(href), in_tree = null, have_sel = QS('#files tr.sel'), td = oth.closest('td').nextSibling, @@ -1579,6 +1580,9 @@ var thegrid = (function () { else if (in_tree && !have_sel) in_tree.click(); + else if (is_vid) + window.open(href + (href.indexOf('?') === -1 ? '?' : '&') + 'vcr', '_blank'); + else if (!is_img && have_sel) window.open(href, '_blank');