diff --git a/db_schema.sql b/db_schema.sql
index 7086fa1400379d43c491c3868dd56d53ad0c6f3d..67e27877972aede0c3055bf21dcc65753883ffba 100644
--- a/db_schema.sql
+++ b/db_schema.sql
@@ -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,
diff --git a/edit.py b/edit.py
index 6a676cdea3966b8e812a0acf01ca8879e787354c..6cee1ed6d4ff96bf65388abec8adac9a3ae43110 100644
--- a/edit.py
+++ b/edit.py
@@ -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
diff --git a/livestreams.py b/livestreams.py
index 2c8acf6dcfbf8fc524291b014d107fdbc75aac6d..c079dc4fa00f40a5486a3cfeb6346064ec619bab 100644
--- a/livestreams.py
+++ b/livestreams.py
@@ -1,5 +1,10 @@
 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
diff --git a/templates/streaming.html b/templates/streaming.html
new file mode 100644
index 0000000000000000000000000000000000000000..6a385d040c50dc9758ee4a1c494838841cfe914d
--- /dev/null
+++ b/templates/streaming.html
@@ -0,0 +1,89 @@
+{% 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 %}