diff options
-rwxr-xr-x | flask-mediabrowser | 10 | ||||
-rw-r--r-- | mediabrowser/__init__.py | 75 | ||||
-rw-r--r-- | mediabrowser/assets/directory.png | bin | 3167 -> 2227 bytes | |||
-rw-r--r-- | mediabrowser/assets/parent.png | bin | 2556 -> 3205 bytes | |||
-rw-r--r-- | mediabrowser/assets/spinner.gif | bin | 0 -> 915 bytes | |||
-rw-r--r-- | mediabrowser/assets/style.css | 11 | ||||
-rw-r--r-- | mediabrowser/ffmpeg.py | 2 | ||||
-rw-r--r-- | mediabrowser/templates/listdir.html | 42 | ||||
-rw-r--r-- | mediabrowser/templates/watch.html | 4 | ||||
-rw-r--r-- | mediabrowser/wsgi.py | 9 |
10 files changed, 106 insertions, 47 deletions
diff --git a/flask-mediabrowser b/flask-mediabrowser index d1c0f8a..584d7bb 100755 --- a/flask-mediabrowser +++ b/flask-mediabrowser @@ -1,10 +1,11 @@ #!/usr/bin/env python3.4 from flask import Flask -from werkzeug.contrib.cache import SimpleCache +from werkzeug.contrib.cache import FileSystemCache import mediabrowser import os +import tempfile import logging from argparse import ArgumentParser @@ -19,7 +20,12 @@ if __name__ == "__main__": args = parser.parse_args() - cache = SimpleCache(threshold=5000, default_timeout=60*60*5) + cache_dir = os.path.join(tempfile.gettempdir(), + "mediabrowser-{}".format(os.geteuid())) + if not os.path.exists(cache_dir): + os.makedirs(cache_dir) + # default_timeout=0 doesn't work with FileSystemCache + cache = FileSystemCache(cache_dir, default_timeout=9999999999, threshold=5000) app = Flask("mediabrowser-demo") app.register_blueprint(mediabrowser.build(args.root, cache)) diff --git a/mediabrowser/__init__.py b/mediabrowser/__init__.py index 62aeb48..dc7aa16 100644 --- a/mediabrowser/__init__.py +++ b/mediabrowser/__init__.py @@ -1,4 +1,5 @@ import os +import io import logging import mimetypes from datetime import datetime @@ -36,6 +37,58 @@ class cached(object): return wrapped_func +class cached_stream(object): + """decorator to apply SavingIoWrapper""" + def __init__(self, cache, keyfunc): + self.cache = cache + self.keyfunc = keyfunc + def __call__(self, func): + def wrapped_func(*args, **kwargs): + key = self.keyfunc(*args, **kwargs) + cached_value = self.cache.get(key) + if cached_value is not None: + return io.BytesIO(cached_value) + else: + value = func(*args, **kwargs) + return SavingIoWrapper(value, key, self.cache) + + return wrapped_func + + +class SavingIoWrapper(io.RawIOBase): + """Wraps a read-only io stream and buffers all read-ed data. + on close() that data is written to the specified cache""" + def __init__(self, stream, key, cache): + self.stream = stream + self.key = key + self.cache = cache + self.buf = b"" + self.finished = False + + def close(self): + if self.finished: + self.cache.set(self.key, self.buf) + logging.info("Saved iostream after close to key {} with" + " length={}".format(self.key, len(self.buf))) + self.stream.close() + + @property + def closed(self): + return self.stream.closed + + def readable(self): + return self.stream.readable() + + def seekable(self): + return False + + def read(self, size=-1): + b = self.stream.read(size) + self.buf += b + if b == b'': + self.finished = True + return b + def build(root_directory, cache): blueprint = Blueprint('mediabrowser', __name__, static_folder='assets', @@ -85,6 +138,16 @@ def build(root_directory, cache): else: return None + @cached_stream(cache=cache, keyfunc=lambda ospath: "thumb_video_{}".format(ospath)) + def ffmpeg_thumbnail_video(ospath): + process = ffmpeg.thumbnail_video(ospath, 100, 60) + return process.stdout + + @cached_stream(cache=cache, keyfunc=lambda ospath: "thumb_poster_{}".format(ospath)) + def ffmpeg_thumbnail_poster(ospath): + process = ffmpeg.thumbnail(ospath, 852, 480) + return process.stdout + @blueprint.route('/assets/<path:filename>') def assets(filename): return blueprint.send_static_file(filename) @@ -131,8 +194,8 @@ def build(root_directory, cache): buf += '#EXT-X-ENDLIST\n' return Response(buf, mimetype='application/x-mpegurl') - @blueprint.route('/<path:path>/thumbnail') - def thumbnail(path): + @blueprint.route('/<path:path>/poster') + def poster(path): path = os.path.normpath(path) ospath = os.path.join(root_directory, path) client_mtime = request.if_modified_since @@ -140,8 +203,8 @@ def build(root_directory, cache): if client_mtime is not None and mtime <= client_mtime: return Response(status=304) else: - process = ffmpeg.thumbnail(ospath, 90, 50) - r = Response(process.stdout, mimetype="image/jpeg") + stream = ffmpeg_thumbnail_poster(ospath) + r = Response(stream, mimetype="image/jpeg") r.last_modified = mtime return r @@ -154,8 +217,8 @@ def build(root_directory, cache): if client_mtime is not None and mtime <= client_mtime: return Response(status=304) else: - process = ffmpeg.thumbnail_video(ospath, 90, 50) - r = Response(process.stdout, mimetype="video/webm") + stream = ffmpeg_thumbnail_video(ospath) + r = Response(stream, mimetype="video/webm") r.last_modified = mtime return r diff --git a/mediabrowser/assets/directory.png b/mediabrowser/assets/directory.png Binary files differindex 008a956..bd1ffb8 100644 --- a/mediabrowser/assets/directory.png +++ b/mediabrowser/assets/directory.png diff --git a/mediabrowser/assets/parent.png b/mediabrowser/assets/parent.png Binary files differindex c1919b8..7212962 100644 --- a/mediabrowser/assets/parent.png +++ b/mediabrowser/assets/parent.png diff --git a/mediabrowser/assets/spinner.gif b/mediabrowser/assets/spinner.gif Binary files differnew file mode 100644 index 0000000..6347748 --- /dev/null +++ b/mediabrowser/assets/spinner.gif diff --git a/mediabrowser/assets/style.css b/mediabrowser/assets/style.css index 04e56a7..b5d7689 100644 --- a/mediabrowser/assets/style.css +++ b/mediabrowser/assets/style.css @@ -50,10 +50,17 @@ body.list { font-size: 36px; } +body.list a { + vertical-align: super; +} + +body.list video { + display: inline; +} + body.list > div { - vertical-align: middle; overflow: hidden; - height: 64px; + height: 60px; white-space: nowrap; } diff --git a/mediabrowser/ffmpeg.py b/mediabrowser/ffmpeg.py index cd10ae4..d132b24 100644 --- a/mediabrowser/ffmpeg.py +++ b/mediabrowser/ffmpeg.py @@ -116,7 +116,7 @@ def calculate_splittimes(ospath, chunk_duration): def thumbnail(ospath, width, height): process = LoggedPopen(shlex.split("ffmpeg -v fatal -noaccurate_seek -ss 25.0 -i") + [ospath] + - shlex.split("-frames:v 10 -map 0:v" + shlex.split("-frames:v 1 -map 0:v" " -filter:v \"scale='w=trunc(oh*a/2)*2:h={}', crop='min({},iw):min({},ih)'\"" " -f singlejpeg pipe:".format(height+(height/10), width, height)), stdout=PIPE) diff --git a/mediabrowser/templates/listdir.html b/mediabrowser/templates/listdir.html index cfa46f7..024822d 100644 --- a/mediabrowser/templates/listdir.html +++ b/mediabrowser/templates/listdir.html @@ -3,37 +3,13 @@ <head> <title>Directory Browser - {{ path }}</title> <link rel="stylesheet" href="{{ url_for('mediabrowser.assets', filename='style.css') }}" /> - <script> - function convertOneThumbnailToVideo() { - var els = document.getElementsByClassName("thumbnail"); - if (els.length > 0) { - var el = els[0]; - console.log("convert",el); - - var videoEl = document.createElement("video"); - videoEl.addEventListener("error", convertOneThumbnailToVideo, true); - videoEl.addEventListener("canplaythrough", convertOneThumbnailToVideo, true); - videoEl.setAttribute("width", 90); - videoEl.setAttribute("height", 50); - videoEl.setAttribute("autoplay", ""); - videoEl.setAttribute("loop", true); - videoEl.setAttribute("src", el.getAttribute("data-video-src")); - el.parentNode.replaceChild(videoEl, el); - } - } - window.onload = function() { - convertOneThumbnailToVideo(); - } - </script> </head> <body class="list"> - {% if parent != path %} + {% if path != '.' %} <div> + <img src="{{ url_for('mediabrowser.assets', filename='parent.png') }}" /> <a href="{{ url_for('mediabrowser.listdir', path=parent) }}"> - <div style="width: 100%"> - <img src="{{ url_for('mediabrowser.assets', filename='parent.png') }}" /> - .. - </div> + Parent Directory </a> </div> {% endif %} @@ -41,8 +17,10 @@ {% for file in files %} {% if file['type'] == 'file' %} <div> - <img class="thumbnail" src="{{ url_for('mediabrowser.thumbnail', path=file['fullpath']) }}" - data-video-src="{{ url_for('mediabrowser.thumbnail_video', path=file['fullpath']) }}"/> + <video src="{{ url_for('mediabrowser.thumbnail_video', path=file['fullpath']) }}" + poster="{{ url_for('mediabrowser.assets', filename='spinner.gif') }}" + autoplay="" loop="true" width="100" height="60"> + </video> <a href="{{ url_for('mediabrowser.watch', path=file['fullpath']) }}"> {{ file['filename'] }} </a> @@ -51,11 +29,9 @@ {% if file['type'] == 'directory' %} <div> + <img src="{{ url_for('mediabrowser.assets', filename='directory.png') }}" /> <a href="{{ file['link'] }}"> - <div style="width: 100%"> - <img src="{{ url_for('mediabrowser.assets', filename='directory.png') }}" /> - {{ file['filename'] }} - </div> + {{ file['filename'] }} </a> </div> {% endif %} diff --git a/mediabrowser/templates/watch.html b/mediabrowser/templates/watch.html index 555c0e9..7e3c9fd 100644 --- a/mediabrowser/templates/watch.html +++ b/mediabrowser/templates/watch.html @@ -60,7 +60,7 @@ var config = { debug: logger, maxBufferLength: 500, - manifestLoadingTimeOut: 120000, + manifestLoadingTimeOut: 20000, levelLoadingTimeOut: 20000, fragLoadingTimeOut: 50000 }; @@ -69,6 +69,8 @@ hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED, function (event, data) { video.play(); + video.setAttribute('poster', + "{{ url_for('mediabrowser.poster', path=path) }}"); }); hls.on(Hls.Events.ERROR, function (event, data) { if (data.fatal) { diff --git a/mediabrowser/wsgi.py b/mediabrowser/wsgi.py index 038ca5b..4091b7b 100644 --- a/mediabrowser/wsgi.py +++ b/mediabrowser/wsgi.py @@ -1,7 +1,7 @@ import mediabrowser from flask import Flask -from werkzeug.contrib.cache import SimpleCache +from werkzeug.contrib.cache import FileSystemCache import os import logging @@ -10,7 +10,12 @@ logging.basicConfig(level=logging.INFO) root = os.getenv("MEDIABROWSER_ROOT") if not root: raise Exception('Must set MEDIABROWSER_ROOT variable') -cache = SimpleCache() +cache_dir = os.getenv("MEDIABROWSER_CACHEDIR") +if not cache_dir: + raise Exception('Must set MEDIABROWSER_CACHEDIR variable') + +# default_timeout=0 doesn't work with FileSystemCache +cache = FileSystemCache(cache_dir, default_timeout=9999999999, threshold=5000) application = Flask("mediabrowser-demo") application.register_blueprint(mediabrowser.build(root, cache)) |