diff --git a/livestreams.py b/livestreams.py index c89cd8265acbaf80b91d0a2f3c6972aa0a996568..346472021d5417783f6766484392fa3498d0ddc0 100644 --- a/livestreams.py +++ b/livestreams.py @@ -62,7 +62,7 @@ def restart_failed_live_transcode(id, type, data, state, status): restart_job(id) @app.route('/internal/streaming') -#@register_navbar('Streaming', icon='transfer') +@register_navbar('Streaming', icon='broadcast-tower', iconlib='fa') @mod_required def streaming(): sources = query('SELECT * FROM live_sources WHERE NOT deleted') @@ -152,3 +152,65 @@ def streamauth(server): modify('UPDATE live_sources SET server = NULL, clientid = NULL, preview_key = NULL, last_active = ? WHERE server = ? AND clientid = ?', datetime.now(), server, request.values['clientid']) return 'Ok', 200 return 'Bad request', 400 + +def schedule_livestream(lecture_id): + def build_filter(l): + return ','.join(l) if l else None + server = 'rwth.video' + lecture = query('SELECT * FROM lectures WHERE id = ?', lecture_id)[0] + settings = json.loads(lecture['stream_settings']) + data = {'src1': {'afilter': [], 'vfilter': []}, 'src2': {'afilter': [], 'vfilter': []}, 'afilter': [], 'videoag_logo': int(bool(settings.get('video_showlogo'))), 'lecture_id': lecture['id']} + src1 = (query('SELECT * FROM live_sources WHERE NOT deleted AND id = ?', settings.get('source1')) or [{}])[0] + src2 = (query('SELECT * FROM live_sources WHERE NOT deleted AND id = ?', settings.get('source2')) or [{}])[0] + for idx, obj in zip([1,2], [src1, src2]): + if obj: + server = obj['server'] + data['src%i'%idx]['url'] = 'rtmp://%s/src/%i'%(obj['server'], obj['id']) + mode = settings.get('source%i_audiomode'%idx) + leftvol = float(settings.get('source%i_leftvolume'%idx, 100))/100.0 + rightvol = float(settings.get('source%i_rightvolume'%idx, 100))/100.0 + if mode == 'mono': + data['src%i'%idx]['afilter'].append('pan=mono|c0=%f*c0+%f*c1'%(0.5*leftvol, 0.5*rightvol)) + elif mode == 'stereo': + data['src%i'%idx]['afilter'].append('pan=stereo|c0=%f*c0|c1=%f*c1'%(leftvol, rightvol)) + elif mode == 'unchanged': + pass + elif mode == 'off': + data['src%i'%idx]['afilter'].append('pan=mono|c0=0*c0') + else: + raise(Exception()) + mode = settings.get('videomode') + if mode == '1': + data['vmix'] = 'streamselect=map=0' + elif mode == '2': + data['vmix'] = 'streamselect=map=1' + elif vmode == 'lecture4:3': + data['src1']['vfilter'].append('scale=1440:1080') + data['src2']['vfilter'].append('scale=1440:810,pad=1440:1080:0:135,crop=480:1080') + data['vmix'] = 'hstack' + elif vmode == 'lecture16:9': + data['src1']['vfilter'].append('scale=1440:810,pad=1440:1080:0:135') + data['src2']['vfilter'].append('scale=1440:810,pad=1440:1080:0:135,crop=480:1080') + data['vmix'] = 'hstack' + elif vmode == 'sidebyside': + data['src1']['vfilter'].append('scale=960:540') + data['src2']['vfilter'].append('scale=960:540') + data['vmix'] = 'hstack,pad=1920:1080:0:270' + if settings.get('audio_normalize'): + data['afilter'].append('loudnorm') + data['afilter'] = build_filter(data['afilter']) + data['src1']['afilter'] = build_filter(data['src1']['afilter']) + data['src1']['vfilter'] = build_filter(data['src1']['vfilter']) + data['src2']['afilter'] = build_filter(data['src2']['afilter']) + data['src2']['vfilter'] = build_filter(data['src2']['vfilter']) + data['destbase'] = 'rtmp://%s/hls/l_%i'%(server, lecture['id']) + job_id = schedule_job('complex_live_transcode', data, priority=10) + return job_id + +@app.route('/internal/streaming/start', methods=['POST']) +@mod_required +def start_stream(): + lecture_id = int(request.values['lecture_id']) + course = (query('SELECT courses.* FROM courses JOIN lectures ON (courses.id = lectures.course_id) WHERE lectures.id = ?', lecture_id) or [None])[0] + schedule_livestream(lecture_id) + return redirect(url_for('course', handle=course['handle'])) diff --git a/static/smptebars.jpg b/static/smptebars.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2e0e5598fe39d82591d6b3c5f689b9cf6f9236bc Binary files /dev/null and b/static/smptebars.jpg differ diff --git a/templates/course.html b/templates/course.html index c95340e31089a07f58df399f9179b2c8d5ff8c2d..b6677345af7cb1f8142fb7723d2aa871098e668a 100644 --- a/templates/course.html +++ b/templates/course.html @@ -160,12 +160,21 @@ <option value="{{ source.id }}">{{ source.name }}</option> {% endfor %} </select> - <img src="{{ config.VIDEOPREFIX }}/thumbnail/s_none.jpg" style="width: 100%; margin-bottom: 0.5em; margin-top: 0.5em"/> - <select name="source{{ snum }}_audiomode" class="form-control"> - <option selected value="unchanged">Audio unverändert</option> - <option value="left">Nur linke Tonspur</option> - <option value="right">Nur rechte Tonspur</option> - <option value="mix">Monomix aller Tonspuren</option> + <img src="{{ url_for('static', filename='smptebars.jpg') }}" style="width: 100%; margin-bottom: 0.5em; margin-top: 0.5em"/> + <label>Lautstärke</label> + <div class="row"> + <div class="col-xs-6"> + <input type="range" name="source{{ snum }}_leftvolume" value="100"> + </div> + <div class="col-xs-6"> + <input type="range" name="source{{ snum }}_rightvolume" value="100"> + </div> + </div> + <select name="source{{ snum }}_audiomode" class="form-control" style="margin-top: 0.5em"> + <option value="mono" selected>Mono-Mix</option> + <option value="stereo">Stereo</option> + <option value="unchanged">Audio unverändert</option> + <option value="off">Kein Audio</option> </select> </div> {% endfor %} @@ -174,36 +183,27 @@ <select name="videomode" class="form-control"> <option value="1" selected>Nur Quelle 1</option> <option value="2">Nur Quelle 2</option> - <option value="sidebyside">Side-by-Side (Quelle 1 groß, 1/3 von 2 daneben)</option> + <option value="lecture4:3">Quelle 1 (4:3) links, Ausschnitt von 2 rechts</option> + <option value="lecture16:9">Quelle 1 (16:9) links, Ausschnitt von 2 rechts</option> + <option value="sidebyside">Side-by-Side (Quelle 1 links, 2 rechts)</option> </select> <div class="checkbox"><label><input name="video_showlogo" type="checkbox" checked>Video AG-Logo einblenden</label></div> </div> - <div class="col-xs-12" style="margin-top: 1em"> - <label>Audio</label> - <select name="audiomode" class="form-control"> - <option value="1" selected>Quelle 1</option> - <option value="2">Quelle 2</option> - <option value="splitstereo">Quelle 1 links, 2 rechts</option> - <option value="mix">Mix beider Quellen</option> - </select> - <div class="checkbox"><label><input name="audio_normalize" type="checkbox">Lautstärke normalisieren</label></div> - </div> - <div class="col-xs-12" style="margin-top: 1em"> - <label>Ausgabeformat</label> - <select name="outputmode" class="form-control"> - <option selected value="multi">1080p/720p/360p (Standard)</option> - <option value="720p">Nur 720p</option> - </select> - </div> <div class="col-xs-12" style="margin-top: 1em"> <label>Weitere Einstellungen</label> - <div class="checkbox"><label><input name="autostart" type="checkbox" checked>Automatisch starten</label></div> + <div style="margin-top: -1em;"> + <div class="checkbox"><label><input name="audio_normalize" type="checkbox">Lautstärke normalisieren</label></div> + </div> </div> </div> </form> </div> <div class="modal-footer"> + <form class="form-inline" method="post" action="{{ url_for('start_stream') }}"> + <input type="hidden" id="editstream-lectureid" name="lecture_id" value=""> + <button type="submit" id="editstream-start" class="btn btn-danger">Speichern und starten</button> <button type="button" id="editstream-submit" class="btn btn-primary">Speichern</button> + </form> </div> </div> </div> @@ -212,7 +212,10 @@ <script> function editstream_update() { $('#editstream .source-select').each(function () { - $(this).siblings('img').prop('src', '{{ config.VIDEOPREFIX }}/thumbnail/s_'+$(this).val()+'.jpg'); + if ($(this).val()) + $(this).siblings('img').prop('src', '{{ config.VIDEOPREFIX }}/thumbnail/s_'+$(this).val()+'.jpg'); + else + $(this).siblings('img').prop('src', {{ url_for('static', filename='smptebars.jpg')|tojson }}); }); }; function editstream_dump() { @@ -223,6 +226,9 @@ function editstream_dump() { $("#editstream select[name!='']").each(function () { res[$(this).attr('name')] = $(this).val(); }); + $("#editstream input[type='range'][name!='']").each(function () { + res[$(this).attr('name')] = $(this).val(); + }); return res; }; function editstream_load(obj) { @@ -234,15 +240,24 @@ function editstream_load(obj) { if ($(this).attr('name') in obj) $(this).val(obj[$(this).attr('name')]); }); + $("#editstream input[type='range'][name!='']").each(function () { + if ($(this).attr('name') in obj) + $(this).val(obj[$(this).attr('name')]); + }); }; $('#editstream .source-select').on('change', editstream_update); $('#editstream-submit').on('click', function () { moderator.api.set($('#editstream').data('currentpath'), JSON.stringify(editstream_dump()), true); $('#editstream').modal('hide'); }); +$('#editstream-start').on('click', function () { + moderator.api.set($('#editstream').data('currentpath'), JSON.stringify(editstream_dump()), true); + return true; +}); $('#editstream').on('show.bs.modal', function (event) { var button = $(event.relatedTarget); $('#editstream').data('currentpath', button.data('path')); + $('#editstream-lectureid').val(button.data('lectureid')); $("#editstream-form")[0].reset(); if (button.data('value')) editstream_load(button.data('value')); diff --git a/templates/macros.html b/templates/macros.html index 107cef430aa06216e1389efc8131b5e05dd20db6..f0de863e4ee9192180f095daf86de0532b253884 100644 --- a/templates/macros.html +++ b/templates/macros.html @@ -262,7 +262,7 @@ $('#embedcodebtn').popover( </li> {% if ismod() %} <li class="pull-right"> - <button class="btn btn-default" data-toggle="modal" data-target="#editstream" data-path="{{ 'lectures.%i.stream_settings'%lecture.id }}" data-value='{{ lecture.stream_settings|e }}'> + <button class="btn btn-default" data-toggle="modal" data-target="#editstream" data-lectureid="{{ lecture.id }}" data-path="{{ 'lectures.%i.stream_settings'%lecture.id }}" data-value='{{ lecture.stream_settings|e }}'> <span class="fas fa-broadcast-tower"></span> </button> </li>