From 8e53bc80140799f0e4a40dc5216254f7d615290d Mon Sep 17 00:00:00 2001 From: deflax Date: Sun, 10 Oct 2021 22:57:54 +0000 Subject: [PATCH] include zombstream 0.4 frontend --- frontend/Dockerfile | 11 + frontend/app/api.py | 31 +++ frontend/app/app.py | 11 + frontend/app/frontend.py | 33 +++ frontend/app/locust-tests.py | 26 ++ frontend/app/static/style.default.css | 222 ++++++++++++++++++ frontend/app/templates/default/main.html.j2 | 52 ++++ frontend/app/templates/default/player.html.j2 | 32 +++ frontend/app/zomstream.py | 76 ++++++ 9 files changed, 494 insertions(+) create mode 100644 frontend/Dockerfile create mode 100644 frontend/app/api.py create mode 100644 frontend/app/app.py create mode 100644 frontend/app/frontend.py create mode 100644 frontend/app/locust-tests.py create mode 100644 frontend/app/static/style.default.css create mode 100644 frontend/app/templates/default/main.html.j2 create mode 100644 frontend/app/templates/default/player.html.j2 create mode 100644 frontend/app/zomstream.py diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..e9e5a6d --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3 +RUN mkdir /code +RUN pip --no-cache-dir install \ + flask \ + pyyaml \ + uwsgi +COPY ./app /code +ADD https://github.com/bilibili/flv.js/releases/download/v1.5.0/flv.min.js /code/static/flv.min.js +RUN chmod 644 /code/static/flv.min.js +USER nobody +WORKDIR /code diff --git a/frontend/app/api.py b/frontend/app/api.py new file mode 100644 index 0000000..2d8f1c4 --- /dev/null +++ b/frontend/app/api.py @@ -0,0 +1,31 @@ +import flask +import json + +from zomstream import Zomstream + +api_version = "0.2" +api_base = "/api/v" + api_version + +api = flask.Blueprint('api', __name__) +zomstream = Zomstream() + +def construct_response(streams): + # Expecting a JSON-serializable list as an argument + # Returning a JSON string with the API response + + r = {"streams":streams} + return flask.jsonify(r) + +@api.route(api_base + "/streams/", methods = ['GET']) +def api_list_streams(): + streams = [] + for stream in zomstream.getStreamNames(): + streams.append({'app':stream[0],'name':stream[1]}) + return construct_response(streams) + + +@api.route(api_base + "/streams//", methods = ['GET']) +def api_stream( stream_name): + # Filter for streams with 'name' == stream_name + stream = list(filter(lambda stream: stream['name'] == stream_name, zomstream.getStreams())) + return construct_response(stream) diff --git a/frontend/app/app.py b/frontend/app/app.py new file mode 100644 index 0000000..a7829ad --- /dev/null +++ b/frontend/app/app.py @@ -0,0 +1,11 @@ +import flask +import urllib +import frontend +import api + +import logging +logging.basicConfig(level=logging.DEBUG) + +web = flask.Flask(__name__) +web.register_blueprint(api.api) +web.register_blueprint(frontend.frontend) diff --git a/frontend/app/frontend.py b/frontend/app/frontend.py new file mode 100644 index 0000000..22e40df --- /dev/null +++ b/frontend/app/frontend.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 + +# imports +import flask +import urllib +from zomstream import Zomstream + +streamList = [] +zomstream = Zomstream() + +frontend = flask.Blueprint('frontend', __name__) + +@frontend.route("/") +def start(): + mainTemplate = '%s/main.html.j2' % zomstream.configuration['template_folder'] + streamList = zomstream.getStreamNames() + page = flask.render_template( + mainTemplate, + items=streamList, + configuration=zomstream.configuration + ) + return page + +@frontend.route("/player//") +def show_player(appname, streamname): + playerTemplate = '%s/player.html.j2' % zomstream.configuration['template_folder'] + page = flask.render_template( + playerTemplate, + streamname=streamname, + appname=appname, + configuration=zomstream.configuration + ) + return page diff --git a/frontend/app/locust-tests.py b/frontend/app/locust-tests.py new file mode 100644 index 0000000..0a7c2ec --- /dev/null +++ b/frontend/app/locust-tests.py @@ -0,0 +1,26 @@ +from locust import HttpLocust, TaskSet, task +import resource + +resource.setrlimit(resource.RLIMIT_NOFILE, (15000, 15000)) + +class ZomstreamTests(TaskSet): + @task + def index(self): + self.client.get("/") + + @task + def static_css(self): + self.client.get("/static/style.default.css") + + @task + def api_list(self): + self.client.get("/api/streams/") + + @task + def videoplayer(self): + self.client.get("/player/bsod") + +class WebsiteUser(HttpLocust): + task_set = ZomstreamTests + min_wait = 5000 + max_wait = 20000 diff --git a/frontend/app/static/style.default.css b/frontend/app/static/style.default.css new file mode 100644 index 0000000..48c8803 --- /dev/null +++ b/frontend/app/static/style.default.css @@ -0,0 +1,222 @@ +body { + background: #ffffff; + margin: 0px; + padding: 0px; + font-family: sans-serif; + font-size: 10pt; + color: #333333; +} + +header { + background: #eeeeee; + margin: none; + padding: 4px; + box-shadow: 0px 0px 3px rgba(0, 0, 0, .5); +} + +header a { + text-decoration: none; + color: #333333; +} + +header h1 { + margin: 2px; +} + +header h2 { + text-align: right; + font-style: italic; + font-weight: normal; + font-size: 12pt; + color: #888; + margin: 2px; +} + +main { + clear: both; + margin-left: auto; + margin-right: auto; + max-width: 750px; +} + +footer { + text-align: center; + color: #888; + font-size: 8pt; + font-style: italic; +} + +article { + border-radius: 5px; + border: 1px solid #888; + box-shadow: 0px 0px 2px rgba(0, 0, 0, .5); + margin: 15px 0px 15px 0px; + padding: 0px; +} + +article h1 { + font-size: 13pt; + font-weight: normal; + color: #444; + background: #eee; + border-bottom: 1px solid #888; + border-radius: 5px 5px 0px 0px; + padding-top: 4px; + padding-bottom: 4px; + padding-left: 6px; + margin: 0px; +} + +article table { + width: 100%; + border-style: collapse; + padding: 4px; +} + +article table th { + font-weight: bold; + text-align: left; + border-bottom: 1px solid #ccc; + padding-bottom: 6px; +} + +article table td { + border-bottom: 1px solid #eee; +} + +article table td em.url { + font-style: normal; + font-family: monospace; + background: #eee; + border-radius: 5px; + padding: 4px; +} + +article table td.btn { + width: 32px; +} + +article table th.btn { + text-align: right; +} + +a.btn { + color: #eee; + display: block; + border-radius: 5px; + padding: 4px 10px 4px 10px; + box-shadow: 0px 0px 2px rgba(0, 0, 0, .5); + text-shadow: 1px 1px 1px rgba(0, 0, 0, .5); + text-decoration: none; + text-align: center; +} + +a.btn-green { + background-color: rgb(44, 57, 75); + /*background: linear-gradient(#4caf50, #2b622d);*/ +} +a.btn-green:hover { + background-color: rgb(60, 78, 104); + box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.5); +} +a.btn-large { + color: #eee; + font-size: 16pt; + display: block; + padding: 10px; + margin: 10px; + text-shadow: 1px 2px 1px rgba(0, 0, 0, .5); + border-top: 1px solid #888; + text-decoration: none; + text-align: center; + border-radius: 5px; +} + +div#videocontainer { + border-radius: 5px; + border: 1px solid #888; + background: #000; + box-shadow: 0px 0px 2px rgba(0, 0, 0, .5); + margin-top: 15px; + padding: 0px; +} + +@media screen and (min-width: 1501px) { + div#videocontainer { + margin-left: auto; + margin-right: auto; + max-width: 80vw; + } +} + +@media screen and (max-width: 1500px) { + div#videocontainer { + margin-left: 20px; + margin-right: 20px; + } +} + +div#videocontainer h1 { + font-size: 13pt; + font-weight: normal; + color: #444; + background: #eee; + border-bottom: 1px solid #888; + border-radius: 5px 5px 0px 0px; + padding-top: 4px; + padding-bottom: 4px; + padding-left: 6px; + margin: 0px; +} + +video#player { + width: 100%; + max-height: calc(100vh - 150px); + background-color: #000; +} + +/* dark mode */ +@media (prefers-color-scheme: dark) { + body { + background: #202020; + color: #999999; + } + header { + background: #444444; + color: #999999; + } + header a { + color: #e0e0e0; + } + header h2 { + color: #777; + } + footer { + color: #999; + } + article { + border: 1px solid #888; + box-shadow: 0px 0px 5px rgba(150, 150, 150, .5); + } + article h1 { + color: #888; + background: #121212; + } + article table th { + border-bottom: 1px solid #666; + } + article table td { + border-bottom: 1px solid #333; + } + article table td em.url { + color: #eee; + background: #121212; + } + div#videocontainer { + box-shadow: 0px 0px 5px rgba(150, 150, 150, .5); + } + div#videocontainer h1 { + color: #888; + background: #121212; + } +} \ No newline at end of file diff --git a/frontend/app/templates/default/main.html.j2 b/frontend/app/templates/default/main.html.j2 new file mode 100644 index 0000000..e9fb218 --- /dev/null +++ b/frontend/app/templates/default/main.html.j2 @@ -0,0 +1,52 @@ + + + + +{{ configuration['pagetitle'] }} - Stream Overview + + + +
+

{{ configuration["pagetitle"] }}

+

{{ configuration["subtitle"] }}

+
+
+{% if items == [] %} +

+ There are currently no streams running +

+{% endif %} +{% for item in items %} +
+

{{ item[1] }}

+ + + + + + + + + + + + + + + + + + +
ProtocolURL
RTMPrtmp://{{ configuration["rtmp_base"] }}/{{ item[0] }}/{{ item[1] }}
RTMP
HTTP-FLV{{ configuration["web_proto"] }}://{{ configuration["base_url"] }}/flv?app={{ item[0] }}&stream={{ item[1] }}
HTTP-FLV
+ + ▷ Web Player + +
+{% endfor %} +
+
+{{ configuration["footer"] }} +
+ + diff --git a/frontend/app/templates/default/player.html.j2 b/frontend/app/templates/default/player.html.j2 new file mode 100644 index 0000000..281e365 --- /dev/null +++ b/frontend/app/templates/default/player.html.j2 @@ -0,0 +1,32 @@ + + + + +{{ configuration['pagetitle'] }} - {{ appname }}: {{ streamname }} + + + +
+

{{ configuration["pagetitle"] }}

+

{{ configuration["subtitle"] }}

+
+
+

{{ streamname }}

+ +
+ + + + diff --git a/frontend/app/zomstream.py b/frontend/app/zomstream.py new file mode 100644 index 0000000..7e85224 --- /dev/null +++ b/frontend/app/zomstream.py @@ -0,0 +1,76 @@ +import pathlib +import xml.etree.ElementTree as etree +import sys +import yaml +import urllib + + +class Stream: + def __init__(self, app, name, urls): + self.name = name # String + self.app = app # String + self.urls = urls # List of Dictionaries with the keys url and type + + +class Zomstream: + def __init__(self): + # load configuration from config.yml file + if pathlib.Path("config.yml").is_file(): + stream = open('config.yml', 'r') + self.configuration = yaml.load(stream) + stream.close() + else: + print('missing configuration.') + sys.exit(1) + self.streamnames = [] + + def getStreamNames(self): + self.streamnames = [] + # get data from the streaming server + response = urllib.request.urlopen(self.configuration['stat_url']) + content = response.read().decode('utf-8') + # parse the xml / walk the tree + tree = etree.fromstring(content) + server = tree.find('server') + applications = server.findall('application') + for application in applications: + appname = application.find('name') + if appname.text == "live" or appname.text == "rec": + streams = application.find('live').findall('stream') + for stream in streams: + name = stream.find('name') + rate = stream.find('bw_video') + if rate.text != "0": + self.streamnames.append( [appname.text, name.text] ) + + return self.streamnames + + + def getStreams(self): + streams = [] + for streamName in self.getStreamNames(): + urls = [] + app = streamName[0] + name = streamName[1] + + flv_url = self.getFlvUrl (app,name) + rtmp_url = self.getRtmpUrl(app,name) + + urls.append({'url': flv_url, 'type':'http_flv'}) + urls.append({'url': rtmp_url,'type':'rtmp'}) + + stream = Stream(app=app, name=name, urls=urls) + streams.append(stream.__dict__) + return streams + + def getFlvUrl(self,app_name,stream_name): + return '%s://%s/flv?app=%s&stream=%s' % ( + self.configuration['web_proto'], + self.configuration['base_url'], + app_name, + stream_name) + def getRtmpUrl(self,app_name,stream_name): + return "rtmp://%s/%s/%s" % ( + self.configuration['rtmp_base'], + app_name, + stream_name)