From 478b2feb875e4ef5a458531c0d24f0b2117f5490 Mon Sep 17 00:00:00 2001 From: Yves Fischer Date: Sat, 2 Jan 2016 15:05:53 +0100 Subject: better thumbs, better caching (with filesystem) --- mediabrowser/__init__.py | 75 +++++++++++++++++++++++++++++++++--- mediabrowser/assets/directory.png | Bin 3167 -> 2227 bytes mediabrowser/assets/parent.png | Bin 2556 -> 3205 bytes mediabrowser/assets/spinner.gif | Bin 0 -> 915 bytes mediabrowser/assets/style.css | 11 +++++- mediabrowser/ffmpeg.py | 2 +- mediabrowser/templates/listdir.html | 42 +++++--------------- mediabrowser/templates/watch.html | 4 +- mediabrowser/wsgi.py | 9 ++++- 9 files changed, 98 insertions(+), 45 deletions(-) create mode 100644 mediabrowser/assets/spinner.gif (limited to 'mediabrowser') 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/') 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('//thumbnail') - def thumbnail(path): + @blueprint.route('//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 index 008a956..bd1ffb8 100644 Binary files a/mediabrowser/assets/directory.png and b/mediabrowser/assets/directory.png differ diff --git a/mediabrowser/assets/parent.png b/mediabrowser/assets/parent.png index c1919b8..7212962 100644 Binary files a/mediabrowser/assets/parent.png and b/mediabrowser/assets/parent.png differ diff --git a/mediabrowser/assets/spinner.gif b/mediabrowser/assets/spinner.gif new file mode 100644 index 0000000..6347748 Binary files /dev/null and b/mediabrowser/assets/spinner.gif differ 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 @@ Directory Browser - {{ path }} - - {% if parent != path %} + {% if path != '.' %}
+ -
- - .. -
+ Parent Directory
{% endif %} @@ -41,8 +17,10 @@ {% for file in files %} {% if file['type'] == 'file' %}
- + {{ file['filename'] }} @@ -51,11 +29,9 @@ {% if file['type'] == 'directory' %} {% 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)) -- cgit v1.2.1