diff --git a/db_schema.sql b/db_schema.sql index 9f5895d11377eba4fd444420e5527de7235e6102..ae064216eb3debe2adc69bfd81f3a518ea130289 100644 --- a/db_schema.sql +++ b/db_schema.sql @@ -108,7 +108,8 @@ CREATE TABLE IF NOT EXISTS `lectures_data` ( `live` INTEGER NOT NULL DEFAULT 0, `norecording` INTEGER NOT NULL DEFAULT 0, `profile` varchar(64), - `stream_settings` text NOT NULL DEFAULT '' + `stream_settings` text NOT NULL DEFAULT '', + `stream_job` INTEGER ); CREATE TABLE IF NOT EXISTS `places` ( `place` varchar(20) NOT NULL PRIMARY KEY, diff --git a/livestreams.py b/livestreams.py index 76a15ca13bbe0c08c628a3bf1369eef540469d4c..063c7fa139e4018ec1e104a90bcde3e4cb49309a 100644 --- a/livestreams.py +++ b/livestreams.py @@ -7,8 +7,9 @@ import string @sched_func(120) def livestream_thumbnail(): livestreams = query('SELECT streams.lecture_id, streams.handle AS livehandle FROM streams WHERE streams.active') - for v in genlive(livestreams): - schedule_job('thumbnail', {'src': v['path'], 'filename': 'l_%i.jpg'%lecture['id']}) + lectures = query('SELECT * FROM lectures WHERE stream_job IS NOT NULL') + for v in genlive(livestreams)+genlive_new(lectures): + schedule_job('thumbnail', {'src': v['path'], 'filename': 'l_%i.jpg'%v['lecture_id']}) @app.route('/internal/streaming/legacy_auth', methods=['GET', 'POST']) @app.route('/internal/streaming/legacy_auth/<server>', methods=['GET', 'POST']) @@ -149,7 +150,14 @@ def streamauth(server): return 'Ok', 200 return 'Forbidden', 403 elif request.values['call'] == 'publish_done': + source = (query('SELECT * FROM live_sources WHERE server = ? AND clientid = ?', server, request.values['clientid']) or [None])[0] modify('UPDATE live_sources SET server = NULL, clientid = NULL, preview_key = NULL, last_active = ? WHERE server = ? AND clientid = ?', datetime.now(), server, request.values['clientid']) + if not source: + return 'Ok', 200 + for lecture in query('SELECT lectures FROM lectures WHERE stream_job IS NOT NULL'): + settings = json.loads(lecture['stream_settings']) + if source['id'] in [settings.get('source1'), settings.get('source2')]: + cancel_job(lecture['stream_job']) return 'Ok', 200 return 'Bad request', 400 @@ -166,6 +174,9 @@ def schedule_livestream(lecture_id): if obj: server = obj['server'] data['src%i'%idx]['url'] = 'rtmp://%s/src/%i'%(obj['server'], obj['id']) + if not obj['clientid']: + flash('Quelle „%s“ ist nicht aktiv!'%obj['name']) + return None 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 @@ -204,17 +215,36 @@ def schedule_livestream(lecture_id): 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']) + if lecture['stream_job']: + flash('Stream läuft bereits!') + return None job_id = schedule_job('complex_live_transcode', data, priority=10) + modify('UPDATE lectures_data SET stream_job = ? WHERE id = ? AND stream_job IS NULL', job_id, lecture_id) + if query('SELECT stream_job FROM lectures WHERE id = ?', lecture_id)[0]['stream_job'] != job_id: + flash('Stream läuft bereits!') + cancel_job(job_id) + return None return job_id @job_handler('complex_live_transcode', state='failed') def restart_failed_complex_live_transcode(id, type, data, state, status): restart_job(id) -@app.route('/internal/streaming/start', methods=['POST']) +@job_handler('complex_live_transcode') +def cleanup_after_complex_live_transcode_ended(id, type, data, state, status): + job = query('SELECT * FROM jobs WHERE id = ?', id, nlfix=False)[0] + if state == 'finished' or (state == 'failed' and job['canceled']): + modify('UPDATE lectures_data SET stream_job = NULL WHERE stream_job = ?', id) + +@app.route('/internal/streaming/control', methods=['POST']) @mod_required -def start_stream(): +def control_stream(): + action = request.values['action'] 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) + if action == 'start': + schedule_livestream(lecture_id) + elif action == 'stop': + lecture = query('SELECT * FROM lectures WHERE id = ?', lecture_id)[0] + cancel_job(lecture['stream_job']) return redirect(url_for('course', handle=course['handle'])) diff --git a/server.py b/server.py index d6e16ebf5ed847978660a7d6c81ca5c2672f786d..3a9a7b06e96630503d9178476c183fa5fa488d57 100644 --- a/server.py +++ b/server.py @@ -135,6 +135,16 @@ def genlive(streams): stream['file_size'] = 0 return streams +def genlive_new(lectures): + hls_format = (query('SELECT * FROM formats WHERE keywords = "hls"') or [{}])[0] + res = [] + for lecture in lectures: + if not lecture['stream_job']: + continue + res.append({'livehandle': 'l_%i'%lecture['id'], 'visible': True, + 'downloadable': False, 'path': 'pub/hls/l_%i.m3u8'%lecture['id'], + 'file_size': 0, 'formats': hls_format, 'lecture_id': lecture['id']}) + return res @app.route('/') @register_navbar('Home', icon='home') @@ -170,6 +180,13 @@ def index(): JOIN courses ON courses.id = lectures.course_id WHERE streams.active AND (? OR (streams.visible AND courses.visible AND courses.listed AND lectures.visible)) ''', ismod()) + livestreams_new = query('''SELECT lectures.*, "course" AS sep, courses.* + FROM lectures + JOIN courses ON courses.id = lectures.course_id + WHERE lectures.stream_job IS NOT NULL AND (? OR (courses.visible AND courses.listed AND lectures.visible)) + ''', ismod()) + for stream in livestreams_new: + stream['livehandle'] = 'l_%i'%stream['id'] featured = query('SELECT * FROM featured WHERE (? OR visible) ORDER BY `order`', ismod()) featured = list(filter(lambda x: not x['deleted'], featured)) for item in featured: @@ -192,7 +209,7 @@ def index(): WHERE videos.lecture_id = ? AND videos.visible ORDER BY formats.prio DESC ''', item['param'])+genlive(streams) - return render_template('index.html', latestvideos=livestreams+latestvideos, upcomming=upcomming, featured=featured) + return render_template('index.html', latestvideos=livestreams_new+livestreams+latestvideos, upcomming=upcomming, featured=featured) @app.route('/courses') @register_navbar('Videos', icon='film') @@ -247,10 +264,11 @@ def course(id=None, handle=None): JOIN formats ON formats.keywords = "hls" WHERE streams.active AND (? OR streams.visible) AND lectures.course_id = ? ''', ismod(), course['id']) + videos += genlive(livestreams) + videos += genlive_new(lectures) chapters = [] if course['coursechapters']: chapters = query('SELECT chapters.* FROM chapters JOIN lectures ON lectures.id = chapters.lecture_id WHERE lectures.course_id = ? AND NOT chapters.deleted AND chapters.visible ORDER BY time ASC', course['id']) - videos += genlive(livestreams) responsible = query('''SELECT users.*, responsible.course_id AS responsible FROM users LEFT JOIN responsible ON (responsible.user_id = users.id AND responsible.course_id = ?) @@ -286,6 +304,7 @@ def lecture(id, course=None, courseid=None): WHERE streams.active AND (? OR streams.visible) AND lectures.id = ? ''', ismod(), id) videos += genlive(livestreams) + videos += genlive_new([lecture]) perms = query('SELECT perm.* FROM perm WHERE ((NOT perm.deleted) AND (perm.lecture_id = ? OR perm.course_id = ?))', lecture['id'], lecture['course_id']) if not videos: @@ -379,15 +398,24 @@ def auth(): # For use with nginx auth_request if url.endswith('jpg') or ismod(): return "OK", 200 if url.startswith('pub/hls/'): - handle = url[len('pub/hls/'):].split('_')[0].split('.')[0] - perms = query('''SELECT lectures.id AS lecture, perm.* - FROM streams - JOIN lectures ON (streams.lecture_id = lectures.id) - JOIN courses ON (lectures.course_id = courses.id) - LEFT JOIN perm ON ((lectures.id = perm.lecture_id OR courses.id = perm.course_id) AND NOT perm.deleted) - WHERE streams.handle = ? - AND (courses.visible AND lectures.visible AND streams.visible) - ORDER BY perm.video_id DESC, perm.lecture_id DESC, perm.course_id DESC''', handle) + handle = url[len('pub/hls/'):].rsplit('_')[0].split('.')[0] + if handle.startswith('l_'): + perms = query('''SELECT lectures.id AS lecture, perm.* + FROM lectures + JOIN courses ON (lectures.course_id = courses.id) + LEFT JOIN perm ON ((lectures.id = perm.lecture_id OR courses.id = perm.course_id) AND NOT perm.deleted) + WHERE lectures.id = ? + AND (courses.visible AND lectures.visible) + ORDER BY perm.video_id DESC, perm.lecture_id DESC, perm.course_id DESC''', int(handle[2:])) + else: + perms = query('''SELECT lectures.id AS lecture, perm.* + FROM streams + JOIN lectures ON (streams.lecture_id = lectures.id) + JOIN courses ON (lectures.course_id = courses.id) + LEFT JOIN perm ON ((lectures.id = perm.lecture_id OR courses.id = perm.course_id) AND NOT perm.deleted) + WHERE streams.handle = ? + AND (courses.visible AND lectures.visible AND streams.visible) + ORDER BY perm.video_id DESC, perm.lecture_id DESC, perm.course_id DESC''', handle) else: perms = query('''SELECT videos.path, videos.id AS vid, perm.* FROM videos diff --git a/templates/course.html b/templates/course.html index b6677345af7cb1f8142fb7723d2aa871098e668a..535afa8c378cd98ad0f04619ea1ffee3568fb05f 100644 --- a/templates/course.html +++ b/templates/course.html @@ -199,8 +199,9 @@ </form> </div> <div class="modal-footer"> - <form class="form-inline" method="post" action="{{ url_for('start_stream') }}"> + <form class="form-inline" method="post" action="{{ url_for('control_stream') }}"> <input type="hidden" id="editstream-lectureid" name="lecture_id" value=""> + <input type="hidden" id="editstream-action" name="action" 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> @@ -257,6 +258,14 @@ $('#editstream-start').on('click', function () { $('#editstream').on('show.bs.modal', function (event) { var button = $(event.relatedTarget); $('#editstream').data('currentpath', button.data('path')); + alert(button.data('active')); + if (button.data('active')) { + $('#editstream-action').val('stop'); + $('#editstream-start').text('Stream stoppen'); + } else { + $('#editstream-action').val('start'); + $('#editstream-start').text('Speichern und starten'); + } $('#editstream-lectureid').val(button.data('lectureid')); $("#editstream-form")[0].reset(); if (button.data('value')) diff --git a/templates/macros.html b/templates/macros.html index f0de863e4ee9192180f095daf86de0532b253884..961f26ab5e6213c2475b27b5f731fcbc7f516792 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-lectureid="{{ lecture.id }}" data-path="{{ 'lectures.%i.stream_settings'%lecture.id }}" data-value='{{ lecture.stream_settings|e }}'> + <button class="btn btn-{{ 'default' if not lecture.stream_settings else 'danger' if lecture.stream_job else 'primary' }}" data-toggle="modal" data-target="#editstream" data-lectureid="{{ lecture.id }}" data-active="{{ 1 if lecture.stream_job else '' }}" data-path="{{ 'lectures.%i.stream_settings'%lecture.id }}" data-value='{{ lecture.stream_settings|e }}'> <span class="fas fa-broadcast-tower"></span> </button> </li>