From 7f88072669caf2351b2ad20f09d3758c5a341a23 Mon Sep 17 00:00:00 2001 From: Julian Rother <julianr@fsmpi.rwth-aachen.de> Date: Fri, 27 Jul 2018 02:35:12 +0200 Subject: [PATCH] Completed livestream model and made live sources page visible --- livestreams.py | 64 +++++++++++++++++++++++++++++++++++++++- static/smptebars.jpg | Bin 0 -> 6508 bytes templates/course.html | 67 ++++++++++++++++++++++++++---------------- templates/macros.html | 2 +- 4 files changed, 105 insertions(+), 28 deletions(-) create mode 100644 static/smptebars.jpg diff --git a/livestreams.py b/livestreams.py index c89cd82..3464720 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 GIT binary patch literal 6508 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 M3MZlIfdBtZ0A_pz5C8xG literal 0 HcmV?d00001 diff --git a/templates/course.html b/templates/course.html index c95340e..b667734 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 107cef4..f0de863 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> -- GitLab