Commit 59e7f4d5 authored by Julian Rother's avatar Julian Rother
Browse files

Added live source interface

parent 02b9cf2a
......@@ -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 %}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment