include zombstream 0.4 frontend
This commit is contained in:
parent
0bc61708f3
commit
8e53bc8014
9 changed files with 494 additions and 0 deletions
11
frontend/Dockerfile
Normal file
11
frontend/Dockerfile
Normal 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
31
frontend/app/api.py
Normal 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
11
frontend/app/app.py
Normal 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
33
frontend/app/frontend.py
Normal 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
|
26
frontend/app/locust-tests.py
Normal file
26
frontend/app/locust-tests.py
Normal 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
|
222
frontend/app/static/style.default.css
Normal file
222
frontend/app/static/style.default.css
Normal 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;
|
||||
}
|
||||
}
|
52
frontend/app/templates/default/main.html.j2
Normal file
52
frontend/app/templates/default/main.html.j2
Normal 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">▶<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">▶<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">
|
||||
▷ Web Player
|
||||
</a>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</main>
|
||||
<footer>
|
||||
{{ configuration["footer"] }}
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
32
frontend/app/templates/default/player.html.j2
Normal file
32
frontend/app/templates/default/player.html.j2
Normal 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
76
frontend/app/zomstream.py
Normal 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)
|
Loading…
Reference in a new issue