From 5cbd0fdfe7cea5afa5ed427951010c6690791e0d Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Tue, 12 Feb 2019 01:30:55 +0100 Subject: [PATCH] Added support for VTT subtitles and subtitles toggling both in local and browser media players --- platypush/backend/http/__init__.py | 31 ++- .../backend/http/media/handlers/__init__.py | 5 +- platypush/backend/http/static/js/media.js | 255 ++++++++++++------ .../backend/http/templates/webplayer.html | 16 +- platypush/message/request/__init__.py | 8 +- platypush/plugins/media/__init__.py | 4 + platypush/plugins/media/mplayer.py | 8 + platypush/plugins/media/subtitles.py | 33 ++- requirements.txt | 1 + setup.py | 3 +- 10 files changed, 257 insertions(+), 107 deletions(-) diff --git a/platypush/backend/http/__init__.py b/platypush/backend/http/__init__.py index 722282fe29..25964511ba 100644 --- a/platypush/backend/http/__init__.py +++ b/platypush/backend/http/__init__.py @@ -11,7 +11,7 @@ import time from multiprocessing import Process from flask import Flask, Response, abort, jsonify, request as http_request, \ - render_template, send_from_directory, make_response + render_template, send_from_directory from redis import Redis @@ -379,8 +379,20 @@ class HttpBackend(Backend): if media_id in media_map: return media_map[media_id] + subfile = None + + if subtitles: + try: + subfile = get_plugin('media.subtitles').download( + link=subtitles, convert_to_vtt=True + ).output.get('filename') + except Exception as e: + self.logger.warning('Unable to load subtitle {}: {}' + .format(subtitles, str(e))) + media_hndl = MediaHandler.build(source, url=media_url, - subtitles=subtitles) + subtitles=subfile) + media_map[media_id] = media_hndl media_hndl.media_id = media_id @@ -456,8 +468,9 @@ class HttpBackend(Backend): media_url=media_hndl.url.replace( self.remote_base_url, ''), media_type=media_hndl.mime_type, - subtitles_url='/media/subtitles/{}.srt'. - format(media_id)) + subtitles_url='/media/subtitles/{}.vtt'. + format(media_id) if media_hndl.subtitles + else None) else: return Response(media_hndl.get_data( from_bytes=from_bytes, to_bytes=to_bytes, @@ -508,12 +521,13 @@ class HttpBackend(Backend): subfile = get_plugin('media.subtitles').download( link=subtitles[0].get('SubDownloadLink'), - media_resource=media_hndl.path).get('filename') + media_resource=media_hndl.path, + convert_to_vtt=True).get('filename') media_hndl.set_subtitles(subfile) return { 'filename': subfile, - 'url': self.remote_base_url + '/media/subtitles/' + media_id + '.srt', + 'url': self.remote_base_url + '/media/subtitles/' + media_id + '.vtt', } def remove_subtitles(media_id): @@ -530,7 +544,7 @@ class HttpBackend(Backend): return {} - @app.route('/media/subtitles/.srt', methods=['GET', 'PUT', 'DELETE']) + @app.route('/media/subtitles/.vtt', methods=['GET', 'POST', 'DELETE']) def handle_subtitles(media_id): """ This route can be used to download and/or expose subtitle files @@ -590,7 +604,7 @@ class HttpBackend(Backend): ret = dict(media_hndl) if media_hndl.subtitles: ret['subtitles_url'] = self.remote_base_url + \ - '/media/subtitles/' + media_hndl.media_id + '.srt' + '/media/subtitles/' + media_hndl.media_id + '.vtt' return jsonify(ret) except FileNotFoundError as e: abort(404, str(e)) @@ -754,6 +768,7 @@ class HttpBackend(Backend): self.logger.info('Initialized HTTP backend on port {}'.format(self.port)) self.app = self.webserver() + self.app.debug = False self.server_proc = Process(target=self.app.run, name='WebServer', kwargs=kwargs) diff --git a/platypush/backend/http/media/handlers/__init__.py b/platypush/backend/http/media/handlers/__init__.py index 419e50e9d9..7f264e96e4 100644 --- a/platypush/backend/http/media/handlers/__init__.py +++ b/platypush/backend/http/media/handlers/__init__.py @@ -53,8 +53,9 @@ class MediaHandler: def __iter__(self): for attr in ['name', 'source', 'mime_type', 'url', 'subtitles', - 'prefix_handlers']: - yield (attr, getattr(self, attr)) + 'prefix_handlers', 'media_id']: + if hasattr(self, attr): + yield (attr, getattr(self, attr)) diff --git a/platypush/backend/http/static/js/media.js b/platypush/backend/http/static/js/media.js index 225b7d662c..280c8a04b7 100644 --- a/platypush/backend/http/static/js/media.js +++ b/platypush/backend/http/static/js/media.js @@ -18,7 +18,9 @@ $(document).ready(function() { $mediaSubtitlesMessage = $mediaSubtitlesModal.find('.media-subtitles-message'), $mediaSearchSubtitles = $ctrlForm.find('[data-modal="#media-subtitles-modal"]'), prevVolume = undefined, - selectedResource = undefined; + selectedResource = undefined, + browserVideoWindow = undefined, + browserVideoElement = undefined; const onEvent = (event) => { switch (event.args.type) { @@ -158,7 +160,7 @@ $(document).ready(function() { resolve(uri, subtitles); } else { - reject(Error(xhr.responseText)); + reject(xhr.responseText); } }, }); @@ -192,13 +194,14 @@ $(document).ready(function() { }); }; - const downloadSubtitles = function(link, mediaResource) { + const downloadSubtitles = function(link, mediaResource, vtt=false) { return new Promise((resolve, reject) => { run({ action: 'media.subtitles.download', args: { 'link': link, 'media_resource': mediaResource, + 'convert_to_vtt': vtt, } }).then((response) => { resolve(response.response.output.filename); @@ -244,10 +247,17 @@ $(document).ready(function() { const playInBrowser = (resource, subtitles) => { return new Promise((resolve, reject) => { startStreaming(resource, subtitles).then((url, subtitles) => { - window.open(url + '?webplayer', '_blank'); + browserVideoWindow = window.open( + url + '?webplayer', '_blank'); + + browserVideoWindow.addEventListener('load', () => { + browserVideoElement = browserVideoWindow.document + .querySelector('#video-player'); + }); + resolve(url, subtitles); - }).catch((xhr, status, error) => { - reject(xhr.responseText); + }).catch((error) => { + reject(error); }); }); }; @@ -287,8 +297,8 @@ $(document).ready(function() { playHndl.then((response) => { resolve(resource); }).catch((error) => { - showError('Playback error: ' + error.message); - reject(error.message); + showError('Playback error: ' + (error ? error : 'undefined')); + reject(error); }); }); }; @@ -302,44 +312,36 @@ $(document).ready(function() { resolve(resource); }; + var defaultPlay = () => { _play(resource).finally(onVideoReady); }; $mediaSearchSubtitles.data('resource', resource); onVideoLoading(); var subtitlesConf = window.config.media.subtitles; if (subtitlesConf) { - populateSubtitlesModal(resource); - - if ('language' in subtitlesConf) { - tryFetchSubtitles(resource).then((subtitles) => { - if (!subtitles) { - showError('Cannot get subtitles'); - _play(resource).finally(onVideoReady); + populateSubtitlesModal(resource).then((subs) => { + if ('language' in subtitlesConf) { + if (subs) { + downloadSubtitles(subs[0].SubDownloadLink, resource).then((subtitles) => { + _play(resource, subtitles).finally(onVideoReady); + resolve(resource, subtitles); + }).catch((error) => { + defaultPlay(); + resolve(resource); + }); } else { - _play(resource, subtitles).finally(onVideoReady); + defaultPlay(); + resolve(resource); } - }); - } - } else { - _play(resource).finally(onVideoReady); - } - }); - }; - - const tryFetchSubtitles = (resource) => { - return new Promise((resolve, reject) => { - getSubtitles(resource).then((subs) => { - if (!subs) { - resolve(); - return; // No subtitles found - } - - downloadSubtitles(subs[0].SubDownloadLink, resource).then((filename) => { - resolve(filename); - }).catch((error) => { - resolve(); + } else { + defaultPlay(); + resolve(resource); + } }); - }); + } else { + defaultPlay(); + resolve(resource); + } }); }; @@ -361,8 +363,8 @@ $(document).ready(function() { window.open(url, '_blank'); resolve(url); }) - .catch((xhr, status, error) => { - reject(xhr.responseText); + .catch((error) => { + reject(error); }) .finally(() => { onVideoReady(); @@ -371,54 +373,114 @@ $(document).ready(function() { }; const populateSubtitlesModal = (resource) => { - $mediaSubtitlesMessage.text('Loading subtitles...'); - $mediaSubtitlesResults.text(''); - $mediaSubtitlesMessage.show(); - $mediaSubtitlesResultsContainer.hide(); + return new Promise((resolve, reject) => { + $mediaSubtitlesMessage.text('Loading subtitles...'); + $mediaSubtitlesResults.text(''); + $mediaSubtitlesMessage.show(); + $mediaSubtitlesResultsContainer.hide(); - getSubtitles(resource).then((subs) => { - if (!subs.length) { - $mediaSubtitlesMessage.text('No subtitles found'); - return; - } + getSubtitles(resource).then((subs) => { + if (!subs) { + $mediaSubtitlesMessage.text('No subtitles found'); + resolve(); + } - $mediaSearchSubtitles.show(); - for (var sub of subs) { - var flagCode; - if ('ISO639' in sub) { - switch(sub.ISO639) { - case 'en': flagCode = 'gb'; break; - default: flagCode = sub.ISO639; break; + $mediaSearchSubtitles.show(); + for (var sub of subs) { + var flagCode; + if ('ISO639' in sub) { + switch(sub.ISO639) { + case 'en': flagCode = 'gb'; break; + default: flagCode = sub.ISO639; break; + } + } + + var $subContainer = $('
').addClass('row media-subtitle-container') + .data('download-link', sub.SubDownloadLink) + .data('resource', resource); + + var $subFlagIconContainer = $('
').addClass('one column'); + var $subFlagIcon = $('') + .addClass(flagCode ? 'flag-icon flag-icon-' + flagCode : ( + sub.IsLocal ? 'fa fa-download' : '')) + .text(!(flagCode || sub.IsLocal) ? '?' : ''); + + var $subMovieName = $('
').addClass('five columns') + .text(sub.MovieName); + + var $subFileName = $('
').addClass('six columns') + .text(sub.SubFileName); + + $subFlagIcon.appendTo($subFlagIconContainer); + $subFlagIconContainer.appendTo($subContainer); + $subMovieName.appendTo($subContainer); + $subFileName.appendTo($subContainer); + $subContainer.appendTo($mediaSubtitlesResults); + } + + $mediaSubtitlesMessage.hide(); + $mediaSubtitlesResultsContainer.show(); + resolve(subs); + }).catch((error) => { + $mediaSubtitlesMessage.text('Unable to load subtitles: ' + error.message); + reject(error); + }); + }); + }; + + const setBrowserPlayerSubs = (resource, subtitles) => { + var mediaId; + if (!browserVideoElement) { + showError('No video is currently playing in the browser'); + return; + } + + return new Promise((resolve, reject) => { + $.get('/media').then((media) => { + for (var m of media) { + if (m.source === resource) { + mediaId = m.media_id; + break; } } - var $subContainer = $('
').addClass('row media-subtitle-container') - .data('download-link', sub.SubDownloadLink) - .data('resource', resource); + if (!mediaId) { + reject(resource + ' is not a registered media'); + return; + } - var $subFlagIconContainer = $('
').addClass('one column'); - var $subFlagIcon = $('') - .addClass(flagCode ? 'flag-icon flag-icon-' + flagCode : ( - sub.IsLocal ? 'fa fa-download' : '')) - .text(!(flagCode || sub.IsLocal) ? '?' : ''); + return $.ajax({ + type: 'POST', + url: '/media/subtitles/' + mediaId + '.vtt', + contentType: 'application/json', + data: JSON.stringify({ + 'filename': subtitles, + }), + }); + }).then(() => { + resolve(resource, subtitles); + }).catch((error) => { + reject('Cannot set subtitles for ' + resource + ': ' + error); + }); + }); + }; - var $subMovieName = $('
').addClass('five columns') - .text(sub.MovieName); - - var $subFileName = $('
').addClass('six columns') - .text(sub.SubFileName); - - $subFlagIcon.appendTo($subFlagIconContainer); - $subFlagIconContainer.appendTo($subContainer); - $subMovieName.appendTo($subContainer); - $subFileName.appendTo($subContainer); - $subContainer.appendTo($mediaSubtitlesResults); - } - - $mediaSubtitlesMessage.hide(); - $mediaSubtitlesResultsContainer.show(); - }).catch((error) => { - $mediaSubtitlesMessage.text('Unable to load subtitles: ' + error.message); + const setLocalPlayerSubs = (resource, subtitles) => { + return new Promise((resolve, reject) => { + run({ + action: 'media.remove_subtitles' + }).then((response) => { + return run({ + action: 'media.set_subtitles', + args: { + 'filename': subtitles, + } + }) + }).then((response) => { + resolve(response); + }).catch((error) => { + reject(error); + }); }); }; @@ -435,6 +497,7 @@ $(document).ready(function() { }; $input.prop('disabled', true); + $mediaItemPanel.find('[data-action]').removeClass('disabled'); $videoResults.text('Searching...'); if (resource.match(new RegExp('^https?://')) || @@ -594,8 +657,29 @@ $(document).ready(function() { }); }); - // $mediaSearchSubtitles.on('mouseup touchend', () => { - // }); + $mediaSubtitlesModal.on('mouseup touchend', '.media-subtitle-container', (event) => { + var resource = $(event.currentTarget).data('resource'); + var link = $(event.currentTarget).data('downloadLink'); + var selectedDevice = getSelectedDevice(); + + if (selectedDevice.isRemote) { + showError('Changing subtitles at runtime on Chromecast is not yet supported'); + return; + } + + var convertToVTT = selectedDevice.isBrowser; + + downloadSubtitles(link, resource, convertToVTT).then((subtitles) => { + if (selectedDevice.isBrowser) { + return setBrowserPlayerSubs(resource, subtitles); + } else { + return setLocalPlayerSubs(resource, subtitles); + } + }).catch((error) => { + console.warning('Could not load subtitles ' + link + + ' to the player: ' + (error || 'undefined error')) + }); + }); }; const initRemoteDevices = function() { @@ -607,8 +691,7 @@ $(document).ready(function() { action: 'media.chromecast.get_chromecasts', }, - function(results) { - $devsRefreshBtn.removeClass('disabled'); + onSuccess = function(results) { $devsList.find('.cast-device[data-remote]').remove(); if (!results || results.response.errors.length) { @@ -641,6 +724,10 @@ $(document).ready(function() { $nameContainer.appendTo($cast); $cast.appendTo($devsList); } + }, + + onComplete = function() { + $devsRefreshBtn.removeClass('disabled'); } ); }; diff --git a/platypush/backend/http/templates/webplayer.html b/platypush/backend/http/templates/webplayer.html index 62ad7ff674..af7216c40c 100644 --- a/platypush/backend/http/templates/webplayer.html +++ b/platypush/backend/http/templates/webplayer.html @@ -1,16 +1,18 @@ -