include zombstream 0.4 frontend

This commit is contained in:
deflax 2021-10-10 22:57:54 +00:00
parent 0bc61708f3
commit 8e53bc8014
9 changed files with 494 additions and 0 deletions

11
frontend/Dockerfile Normal file
View file

@ -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

31
frontend/app/api.py Normal file
View file

@ -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/<stream_name>/", 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)

11
frontend/app/app.py Normal file
View file

@ -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)

33
frontend/app/frontend.py Normal file
View file

@ -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/<appname>/<streamname>")
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

View file

@ -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

View file

@ -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;
}
}

View file

@ -0,0 +1,52 @@
<!DOCTYPE html>
<html>
<head>
<meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0' />
<title>{{ configuration['pagetitle'] }} - Stream Overview</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.default.css') }}">
</head>
<body>
<header>
<h1>{{ configuration["pagetitle"] }}</h1>
<h2>{{ configuration["subtitle"] }}</h2>
</header>
<main>
{% if items == [] %}
<p style="margin-top: 20px; margin-bottom: 150px;">
<span style="color: #888; font-size: 14pt;">There are currently no streams running</span>
</p>
{% endif %}
{% for item in items %}
<article>
<h1>{{ item[1] }}</h1>
<table>
<tbody>
<tr>
<th>Protocol</th>
<th>URL</th>
<th></th>
</tr>
<tr>
<td>RTMP</td>
<td><em class="url">rtmp://{{ configuration["rtmp_base"] }}/{{ item[0] }}/{{ item[1] }}</em></td>
<td class="btn"><a href="rtmp://{{ configuration["rtmp_base"] }}/{{ item[0] }}/{{ item[1] }}" class="btn btn-green">&#9654;<br/>RTMP</a></td>
</tr>
<tr>
<td>HTTP-FLV</td>
<td><em class="url">{{ configuration["web_proto"] }}://{{ configuration["base_url"] }}/flv?app={{ item[0] }}&stream={{ item[1] }}</em></td>
<td class="btn"><a href="{{ configuration["web_proto"] }}://{{ configuration["base_url"] }}/flv?app={{ item[0] }}&stream={{ item[1] }}" class="btn btn-green">&#9654;<br/>HTTP-FLV</a></td>
</tr>
</tbody>
</table>
<a href="{{ url_for('frontend.show_player', streamname=item[1], appname=item[0]) }}"
class="btn-large btn-green">
&#9655; Web Player
</a>
</article>
{% endfor %}
</main>
<footer>
{{ configuration["footer"] }}
</footer>
</body>
</html>

View file

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html>
<head>
<meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0' />
<title>{{ configuration['pagetitle'] }} - {{ appname }}: {{ streamname }}</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.default.css') }}">
</head>
<body>
<header>
<a href="{{ url_for('frontend.start') }}"><h1>{{ configuration["pagetitle"] }}</h1></a>
<h2>{{ configuration["subtitle"] }}</h2>
</header>
<div id="videocontainer">
<h1>{{ streamname }}</h1>
<video id="player" controls>
</video>
</div>
<script src="{{ url_for('static', filename='flv.min.js') }}"></script>
<script type="application/javascript">
if (flvjs.isSupported()) {
var videoElement = document.getElementById('player');
var flvPlayer = flvjs.createPlayer({
type: 'flv',
url: '{{ configuration["web_proto"] }}://{{ configuration["base_url"] }}/flv?app={{ appname }}&stream={{ streamname }}'
});
flvPlayer.attachMediaElement(videoElement);
flvPlayer.load();
flvPlayer.play();
}
</script>
</body>
</html>

76
frontend/app/zomstream.py Normal file
View file

@ -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)