Skip to content
Snippets Groups Projects
Commit 59e7f4d5 authored by Julian Rother's avatar Julian Rother
Browse files

Added live source interface

parent 02b9cf2a
No related branches found
No related tags found
No related merge requests found
......@@ -179,6 +179,21 @@ CREATE TABLE IF NOT EXISTS `streams` (
`poster` text NOT NULL,
`job_id` INTEGER
);
CREATE TABLE IF NOT EXISTS `live_sources` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`key` varchar(32) UNIQUE,
`preview_key` varchar(32),
`name` text NOT NULL,
`description` text NOT NULL DEFAULT '',
`options` text NOT NULL DEFAULT '',
`server` varchar(32),
`clientid` INTEGER,
`last_active` datetime,
`time_created` datetime NOT NULL,
`time_updated` datetime NOT NULL,
`created_by` INTEGER NOT NULL,
`deleted` INTEGER NOT NULL DEFAULT '0'
);
CREATE TABLE IF NOT EXISTS `stream_stats` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`handle` varchar(32) NOT NULL,
......
......@@ -111,7 +111,15 @@ editable_tables = {
'notify_new_video': {'type': 'boolean'},
'notify_edit': {'type': 'boolean'}
},
'creationtime_fields': [] }
'creationtime_fields': [] },
'live_sources': {
'table': 'live_sources',
'idcolumn': 'id',
'editable_fields': {
'name': {'type': 'shortstring'},
'description': {'type': 'text'},
},
'creationtime_fields': ['created_by', 'time_created', 'time_updated'] }
}
#parses the path to a dict, containing the table, id, field and field type
......
from server import *
import requests
from xml.etree import ElementTree
import random
import string
@sched_func(120)
def livestream_thumbnail():
livestreams = query('SELECT streams.lecture_id, streams.handle AS livehandle FROM streams WHERE streams.active')
......@@ -8,7 +13,7 @@ def livestream_thumbnail():
@app.route('/internal/streaming/legacy_auth', methods=['GET', 'POST'])
@app.route('/internal/streaming/legacy_auth/<server>', methods=['GET', 'POST'])
def streamauth(server=None):
def streamauth_legacy(server=None):
internal = False
if 'X-Real-IP' in request.headers:
for net in config.get('FSMPI_IP_RANGES', []):
......@@ -57,3 +62,82 @@ def streamauth(server=None):
def restart_failed_live_transcode(id, type, data, state, status):
restart_job(id)
@app.route('/internal/streaming')
#@register_navbar('Streaming', icon='transfer')
@mod_required
def streaming():
sources = query('SELECT * FROM live_sources WHERE NOT deleted')
for source in sources:
if not source['clientid']:
continue
r = requests.get('http://%s:8080/stats'%source['server'])
if r.status_code != 200:
continue
source['stat'] = {}
tree = ElementTree.fromstring(r.text)
if not tree:
continue
s = tree.find("./server/application/[name='src']/live/stream/[name='%i']"%source['id'])
for e in s.find("client/[publishing='']").getchildren():
source['stat'][e.tag] = e.text
source['video'] = {}
for e in s.find('meta/video').getchildren():
source['video'][e.tag] = e.text
source['audio'] = {}
for e in s.find('meta/audio').getchildren():
source['audio'][e.tag] = e.text
return render_template("streaming.html", sources=sources)
def gentoken():
return ''.join(random.SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(16))
@app.route('/internal/streaming/rekey/<int:id>')
@mod_required
def streamrekey(id):
modify('UPDATE live_sources SET key = ? WHERE id = ? AND NOT deleted', gentoken(), id)
source = query('SELECT * FROM live_sources WHERE NOT deleted AND id = ?', id)[0]
flash('Der Streamkey von <strong>'+source['name']+'</strong> wurde neu generiert: <span><input readonly type="text" style="width: 15em" value="'+source['key']+'"></span>')
return redirect(url_for('streaming'))
@app.route('/internal/streaming/drop/<int:id>')
@mod_required
def streamdrop(id):
source = (query('SELECT * FROM live_sources WHERE NOT deleted AND id = ?', id) or [None])[0]
if not source:
if 'ref' in request.values:
flash('Streamquelle nicht gefunden')
return redirect(request.values['ref'])
else:
return 'Not found', 404
requests.get('http://%s:8080/control/drop/publisher?clientid=%i'%(source['server'], source['clientid']))
if 'ref' in request.values:
return redirect(request.values['ref'])
return 'Ok', 200
@app.route('/internal/streaming/auth/<server>', methods=['GET', 'POST'])
def streamauth(server):
internal = False
for net in config.get('INTERNAL_IP_RANGES', []):
if ip_address(request.headers['X-Real-IP']) in ip_network(net):
internal = True
if not internal:
return 'Forbidden', 403
if request.values['call'] == 'publish':
sources = query('SELECT * FROM live_sources WHERE NOT deleted AND key = ?', request.values['name'])
if not sources:
return 'Not found', 404
modify('UPDATE live_sources SET server = ?, clientid = ?, last_active = ?, preview_key = ? WHERE id = ?', server, request.values['clientid'], datetime.now(), gentoken(), sources[0]['id'])
ret = Response('Redirect', 301, {'Location': '%i'%sources[0]['id']})
ret.autocorrect_location_header = False
return ret
if request.values['call'] == 'play':
source = (query('SELECT * FROM live_sources WHERE NOT deleted AND id = ?', request.values['name']) or [None])[0]
if not source:
return 'Not found', 404
if source['preview_key'] != request.values.get('preview_key'):
return 'Forbidden', 403
return 'Ok', 200
elif request.values['call'] == 'publish_done':
modify('UPDATE live_sources SET server = NULL, clientid = NULL, preview_key = NULL WHERE server = ? AND clientid = ?', server, request.values['clientid'])
return 'Ok', 200
return 'Bad request', 400
{% extends "base.html" %}
{% block content %}
<div class="panel-group">
<div class="panel panel-default">
<div class="panel-heading">
<h1 class="panel-title">Streamquellen</h1>
</div>
<div class="panel-body">
<form action="{{ url_for('create', table='live_sources', ref=request.url) }}" method="post">
<div class="input-group pull-right" style="width: 300px;">
<input type="text" class="form-control" name="name" placeholder="Quellenname" width="100px">
<span class="input-group-btn">
<button class="btn btn-default" type="submit">Quelle anlegen</button>
</span>
</div>
</form>
</div>
<ul class="list-group">
{% for source in sources %}
<li class="list-group-item{% if source.clientid %} list-group-item-danger{% endif %}">
<div class="row">
<div style="background-image: url('{{ config.VIDEOPREFIX }}/hls/preview/{{ source.id }}.jpg')" class="col-sm-2 col-xs-12 thumbnailimg">
{% if source.clientid %}
<a href="#" data-toggle="modal" data-target="#preview-player" data-srcname="{{ source.name }}" data-srcid="{{ source.id }}">
<span class="glyphicon glyphicon-play-circle playpreviewbtn"></span>
</a>
{% endif %}
</div>
<ul class="list-unstyled col-sm-3 col-xs-12">
<li>{{ moderator_editor(['live_sources',source.id,'name'], source.name) }}</li>
<li>{{ moderator_editor(['live_sources',source.id,'description'], source.description) }}</li>
</ul>
<ul class="list-unstyled col-sm-3 col-xs-12">
{% if source.clientid %}
<li><a href="rtmp://{{ source.server }}/src/{{ source.id }}?preview_key={{ source.preview_key }}">rtmp://{{ source.server }}/src/{{ source.id }}</a></li>
{% if source.stat and source.video and source.audio %}
<li>Quelladresse: {{ source.stat.address }}</li>
<li>Framedrops: {{ source.stat.dropped }}</li>
<li>Auflösung: {{ source.video.width }}x{{ source.video.height }}</li>
<li>Framerate: {{ source.video.frame_rate }} fps</li>
<li>Audiokanäle: {{ source.audio.channels }}</li>
<li>Audiorate: {{ source.audio.sample_rate }} Hz</li>
<li>Codecs: {{ source.video.codec }}/{{ source.audio.codec }}</li>
{% endif %}
{% else %}
{% if source.last_active %}
<li>Zuletzt aktiv: {{ source.last_active|date }}, {{ source.last_active|time }} Uhr</li>
{% else %}
<li>Noch nie aktiv</li>
{% endif %}
{% endif %}
</ul>
<ul class="list-unstyled col-sm-4 col-xs-12">
<li class="pull-right">{{ moderator_delete(['live_sources',source.id,'deleted']) }}</li>
<a href="{{ url_for('streamrekey', id=source.id) }}" class="btn btn-{{ 'primary' if not source.key else 'default' }}">Streamkey erneuern</a>
<a href="{{ url_for('streamdrop', id=source.id, ref=request.url) }}" class="btn btn-danger {% if not source.clientid %} disabled{% endif %}">Trennen</a>
</ul>
</div>
</li>
{% endfor %}
</ul>
</div>
</div>
<div id="preview-player" class="modal fade" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">Vorschau von</h4>
</div>
<div class="modal-body">
</div>
</div>
</div>
</div>
<script>
$('#preview-player').on('show.bs.modal', function (e) {
var btn = $(e.relatedTarget);
$(this).find('.modal-title').text('Vorschau von '+btn.data('srcname'));
$(this).find('.modal-body').html('<video id="previewplayer" style="width: 100%" class="video-js vjs-default-skin vjs-big-play-centered" width="640" height="320" controls><source type="application/x-mpegURL" src="{{config.VIDEOPREFIX}}/hls/preview/'+btn.data('srcid')+'.m3u8"/></video>');
var player = videojs('previewplayer');
player.play();
});
$('#preview-player').on('hidden.bs.modal', function (e) {
videojs('previewplayer').dispose();
$(this).find('.modal-body').text('');
});
</script>
{% endblock %}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment