From bab2d15d6eabaf43c9b06388e87d17358a07680e Mon Sep 17 00:00:00 2001 From: Mike Lang Date: Sun, 9 Dec 2018 17:27:08 -0800 Subject: [PATCH] Initial implementation of the restreamer Supports serving segments, listing segments for an hour, and generating playlists so it can stream. --- restreamer/restreamer/__main__.py | 14 +++++ restreamer/restreamer/generate_hls.py | 16 +++++ restreamer/restreamer/main.py | 84 +++++++++++++++++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 restreamer/restreamer/generate_hls.py create mode 100644 restreamer/restreamer/main.py diff --git a/restreamer/restreamer/__main__.py b/restreamer/restreamer/__main__.py index e69de29..24935eb 100644 --- a/restreamer/restreamer/__main__.py +++ b/restreamer/restreamer/__main__.py @@ -0,0 +1,14 @@ + +import gevent.monkey +gevent.monkey.patch_all() + +import logging + +import argh + +from restreamer.main import main + +LOG_FORMAT = "[%(asctime)s] %(levelname)8s %(name)s(%(module)s:%(lineno)d): %(message)s" + +logging.basicConfig(level=logging.INFO, format=LOG_FORMAT) +argh.dispatch_command(main) diff --git a/restreamer/restreamer/generate_hls.py b/restreamer/restreamer/generate_hls.py new file mode 100644 index 0000000..242b763 --- /dev/null +++ b/restreamer/restreamer/generate_hls.py @@ -0,0 +1,16 @@ + + +def generate_master(playlists): + """Generate master playlist. Playlists arg should be a map {name: url}. + Little validation or encoding is done - please try to keep the names valid + without escaping. + """ + lines = ["#EXTM3U"] + for name, url in playlists.items(): + lines += [ + # We name each variant with a VIDEO rendition with no url + '#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="{name}",NAME="{name}",AUTOSELECT=YES,DEFAULT=YES'.format(name=name), + '#EXT-X-STREAM-INF:VIDEO="{name}"'.format(name=name), + url, + ] + return "\n".join(lines) + '\n' diff --git a/restreamer/restreamer/main.py b/restreamer/restreamer/main.py new file mode 100644 index 0000000..596c846 --- /dev/null +++ b/restreamer/restreamer/main.py @@ -0,0 +1,84 @@ + +import errno +import json +import os + +from flask import Flask, url_for, request, abort +from gevent.pywsgi import WSGIServer + +import generate_hls + + +app = Flask('restreamer', static_url_path='/segments') + + +""" +The restreamer is a simple http api for listing available segments and generating +HLS playlists for them. + +The segments themselves are ideally to be served by some external webserver +under "/segments////" (ie. with BASE_DIR under "/segments"), +though this server will also serve them if requested. +""" + + +def listdir(path, error=True): + """List files in path, excluding hidden files. + Behaviour when path doesn't exist depends on error arg. + If error is True, raise 404. Otherwise, return []. + """ + try: + return [name for name in os.listdir(path) if not name.startswith('.')] + except OSError as e: + if e.errno != errno.ENOENT: + raise + if error: + abort(404) + return [] + + +@app.route('/files///') +def list_segments(stream, variant, hour): + """Returns a JSON list of segment files for a given stream, variant and hour. + Returns empty list on non-existant streams, etc. + """ + # Check no-one's being sneaky with path traversal or hidden folders + if any(arg.startswith('.') for arg in (stream, variant, hour)): + return "Parts may not start with period", 403 + path = os.path.join( + app.static_folder, + stream, + variant, + hour, + ) + return json.dumps(listdir(path, error=False)) + + +@app.route('/playlist/.m3u8') +def generate_master_playlist(stream): + """Returns a HLS master playlist for the given stream. + Takes optional params: + start, end: The time to begin and end the stream at. + See generate_media_playlist for details. + """ + # path traversal / hidden folders + if stream.startswith('.'): + return "Stream may not start with period", 403 + variants = listdir(os.path.join(app.static_folder, stream)) + playlists = { + variant: url_for('generate_media_playlist', stream=stream, variant=variant, **request.args) + for variant in variants + } + return generate_hls.generate_master(playlists) + + +@app.route('/playlist//.m3u8') +def generate_media_playlist(stream, variant): + # TODO + return "Not Implemented", 501 + + +def main(host='0.0.0.0', port=8000, base_dir='.'): + app.static_folder = base_dir + server = WSGIServer((host, port), app) + server.serve_forever()