Attempt to support subtitle tracks in web player

This commit is contained in:
Fabio Manganiello 2019-02-11 18:46:25 +01:00
parent 34f0264d5e
commit 41c34b4bc5
4 changed files with 241 additions and 89 deletions

View file

@ -11,12 +11,12 @@ import time
from multiprocessing import Process from multiprocessing import Process
from flask import Flask, Response, abort, jsonify, request as http_request, \ from flask import Flask, Response, abort, jsonify, request as http_request, \
render_template, send_from_directory render_template, send_from_directory, make_response
from redis import Redis from redis import Redis
from platypush.config import Config from platypush.config import Config
from platypush.context import get_backend, get_or_create_event_loop from platypush.context import get_backend, get_plugin, get_or_create_event_loop
from platypush.message import Message from platypush.message import Message
from platypush.message.event import Event, StopEvent from platypush.message.event import Event, StopEvent
from platypush.message.event.web.widget import WidgetUpdateEvent from platypush.message.event.web.widget import WidgetUpdateEvent
@ -371,7 +371,7 @@ class HttpBackend(Backend):
def get_media_id(source): def get_media_id(source):
return hashlib.sha1(source.encode()).hexdigest() return hashlib.sha1(source.encode()).hexdigest()
def register_media(source): def register_media(source, subtitles=None):
media_id = get_media_id(source) media_id = get_media_id(source)
media_url = get_media_url(media_id) media_url = get_media_url(media_id)
@ -379,8 +379,10 @@ class HttpBackend(Backend):
if media_id in media_map: if media_id in media_map:
return media_map[media_id] return media_map[media_id]
media_hndl = MediaHandler.build(source, url=media_url) media_hndl = MediaHandler.build(source, url=media_url,
subtitles=subtitles)
media_map[media_id] = media_hndl media_map[media_id] = media_hndl
media_hndl.media_id = media_id
self.logger.info('Streaming "{}" on {}'.format(source, media_url)) self.logger.info('Streaming "{}" on {}'.format(source, media_url))
return media_hndl return media_hndl
@ -434,7 +436,6 @@ class HttpBackend(Backend):
if not to_bytes: if not to_bytes:
to_bytes = content_length-1 to_bytes = content_length-1
# to_bytes = from_bytes + self._DEFAULT_STREAMING_BLOCK_SIZE
content_length -= from_bytes content_length -= from_bytes
else: else:
to_bytes = int(to_bytes) to_bytes = int(to_bytes)
@ -450,12 +451,119 @@ class HttpBackend(Backend):
headers['Content-Length'] = content_length headers['Content-Length'] = content_length
if 'webplayer' in request.args:
return render_template('webplayer.html',
media_url=media_hndl.url.replace(
self.remote_base_url, ''),
media_type=media_hndl.mime_type,
subtitles_url='/media/subtitles/{}.srt'.
format(media_id))
else:
return Response(media_hndl.get_data( return Response(media_hndl.get_data(
from_bytes=from_bytes, to_bytes=to_bytes, from_bytes=from_bytes, to_bytes=to_bytes,
chunk_size=self._DEFAULT_STREAMING_CHUNK_SIZE), chunk_size=self._DEFAULT_STREAMING_CHUNK_SIZE),
status_code, headers=headers, mimetype=headers['Content-Type'], status_code, headers=headers, mimetype=headers['Content-Type'],
direct_passthrough=True) direct_passthrough=True)
def add_subtitles(media_id, req):
"""
This route can be used to download and/or expose subtitles files
associated to a media file
"""
media_hndl = media_map.get(media_id)
if not media_hndl:
raise FileNotFoundError('{} is not a registered media_id'.
format(media_id))
subfile = None
if req.data:
try:
subfile = json.loads(req.data.decode('utf-8')) \
.get('filename')
if not subfile:
raise AttributeError
except Exception as e:
raise AttributeError(400, 'No filename in the request: {}'
.format(str(e)))
if not subfile:
if not media_hndl.path:
raise NotImplementedError(
'Subtitles are currently only supported for ' +
'local media files')
try:
subtitles = get_plugin('media.subtitles').get_subtitles(
media_hndl.path)
except Exception as e:
raise RuntimeError('Could not get subtitles: {}'.
format(str(e)))
if not subtitles:
raise FileNotFoundError(
'No subtitles found for resource {}'.format(
media_hndl.path))
subfile = get_plugin('media.subtitles').download(
link=subtitles[0].get('SubDownloadLink'),
media_resource=media_hndl.path).get('filename')
media_hndl.set_subtitles(subfile)
return {
'filename': subfile,
'url': self.remote_base_url + '/media/subtitles/' + media_id + '.srt',
}
def remove_subtitles(media_id):
media_hndl = media_map.get(media_id)
if not media_hndl:
raise FileNotFoundError('{} is not a registered media_id'.
format(media_id))
if not media_hndl.subtitles:
raise FileNotFoundError('{} has no subtitles attached'.
format(media_id))
media_hndl.remove_subtitles()
return {}
@app.route('/media/subtitles/<media_id>.srt', methods=['GET', 'PUT', 'DELETE'])
def handle_subtitles(media_id):
"""
This route can be used to download and/or expose subtitle files
associated to a media file
"""
if http_request.method == 'GET':
media_hndl = media_map.get(media_id)
if not media_hndl:
abort(404, 'No such media')
if not media_hndl.subtitles:
abort(404, 'The media has no subtitles attached')
return send_from_directory(
os.path.dirname(media_hndl.subtitles),
os.path.basename(media_hndl.subtitles),
mimetype='text/vtt')
try:
if http_request.method == 'DELETE':
return jsonify(remove_subtitles(media_id))
else:
return jsonify(add_subtitles(media_id, http_request))
except FileNotFoundError as e:
abort(404, str(e))
except AttributeError as e:
abort(400, str(e))
except NotImplementedError as e:
abort(422, str(e))
except Exception as e:
abort(500, str(e))
@app.route('/media', methods=['GET', 'PUT']) @app.route('/media', methods=['GET', 'PUT'])
def add_or_get_media(): def add_or_get_media():
""" """
@ -476,9 +584,14 @@ class HttpBackend(Backend):
if not source: if not source:
abort(400, 'The request does not contain any source') abort(400, 'The request does not contain any source')
subtitles = args.get('subtitles')
try: try:
media_hndl = register_media(source) media_hndl = register_media(source, subtitles)
return jsonify(dict(media_hndl)) ret = dict(media_hndl)
if media_hndl.subtitles:
ret['subtitles_url'] = self.remote_base_url + \
'/media/subtitles/' + media_hndl.media_id + '.srt'
return jsonify(ret)
except FileNotFoundError as e: except FileNotFoundError as e:
abort(404, str(e)) abort(404, str(e))
except AttributeError as e: except AttributeError as e:

View file

@ -7,7 +7,8 @@ class MediaHandler:
prefix_handlers = [] prefix_handlers = []
def __init__(self, source, filename=None, def __init__(self, source, filename=None,
mime_type='application/octet-stream', name=None, url=None): mime_type='application/octet-stream', name=None, url=None,
subtitles=None):
matched_handlers = [hndl for hndl in self.prefix_handlers matched_handlers = [hndl for hndl in self.prefix_handlers
if source.startswith(hndl)] if source.startswith(hndl)]
@ -18,10 +19,12 @@ class MediaHandler:
self.prefix_handlers)) self.prefix_handlers))
self.name = name self.name = name
self.path = None
self.filename = name self.filename = name
self.source = source self.source = source
self.url = url self.url = url
self.mime_type = mime_type self.mime_type = mime_type
self.subtitles = subtitles
self.content_length = 0 self.content_length = 0
self._matched_handler = matched_handlers[0] self._matched_handler = matched_handlers[0]
@ -42,8 +45,15 @@ class MediaHandler:
def get_data(self, from_bytes=None, to_bytes=None, chunk_size=None): def get_data(self, from_bytes=None, to_bytes=None, chunk_size=None):
raise NotImplementedError() raise NotImplementedError()
def set_subtitles(self, subtitles_file):
self.subtitles = subtitles_file
def remove_subtitles(self):
self.subtitles = None
def __iter__(self): def __iter__(self):
for attr in ['name', 'source', 'mime_type', 'url', 'prefix_handlers']: for attr in ['name', 'source', 'mime_type', 'url', 'subtitles',
'prefix_handlers']:
yield (attr, getattr(self, attr)) yield (attr, getattr(self, attr))

View file

@ -50,6 +50,7 @@ $(document).ready(function() {
'icon': 'stop', 'icon': 'stop',
'html': 'Media playback stopped', 'html': 'Media playback stopped',
}); });
$mediaSearchSubtitles.hide();
break; break;
} }
}; };
@ -105,6 +106,7 @@ $(document).ready(function() {
}; };
const startStreamingTorrent = function(torrent) { const startStreamingTorrent = function(torrent) {
// TODO support for subtitles download on torrent metadata received
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
execute( execute(
{ {
@ -127,7 +129,7 @@ $(document).ready(function() {
}); });
}; };
const startStreaming = function(media) { const startStreaming = function(media, subtitles) {
if (media.startsWith('magnet:?')) { if (media.startsWith('magnet:?')) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
startStreamingTorrent(media) startStreamingTorrent(media)
@ -141,12 +143,20 @@ $(document).ready(function() {
type: 'PUT', type: 'PUT',
url: '/media', url: '/media',
contentType: 'application/json', contentType: 'application/json',
data: JSON.stringify({ source: media }), data: JSON.stringify({
'source': media,
'subtitles': subtitles,
}),
complete: (xhr, textStatus) => { complete: (xhr, textStatus) => {
if (xhr.status == 200) { if (xhr.status == 200) {
var uri = xhr.responseJSON.url.match(/https?:\/\/[^\/]+(\/media\/.*)/)[1] var uri = xhr.responseJSON.url.match(/https?:\/\/[^\/]+(\/media\/.*)/)[1]
resolve(uri); var subtitles;
if ('subtitles_url' in xhr.responseJSON) {
subtitles = xhr.responseJSON.subtitles_url.match(/https?:\/\/[^\/]+(\/media\/.*)/)[1]
}
resolve(uri, subtitles);
} else { } else {
reject(Error(xhr.responseText)); reject(Error(xhr.responseText));
} }
@ -168,7 +178,7 @@ $(document).ready(function() {
const getSubtitles = function(resource) { const getSubtitles = function(resource) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!window.config.media.subtitles) { if (!window.config.media.subtitles) {
return; // media.subtitles plugin not configured resolve(); // media.subtitles plugin not configured
} }
run({ run({
@ -233,11 +243,9 @@ $(document).ready(function() {
const playInBrowser = (resource, subtitles) => { const playInBrowser = (resource, subtitles) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// TODO support for subtitles in local player startStreaming(resource, subtitles).then((url, subtitles) => {
window.open(url + '?webplayer', '_blank');
startStreaming(resource).then((url) => { resolve(url, subtitles);
window.open(url, '_blank');
resolve(url);
}).catch((xhr, status, error) => { }).catch((xhr, status, error) => {
reject(xhr.responseText); reject(xhr.responseText);
}); });
@ -299,16 +307,19 @@ $(document).ready(function() {
var subtitlesConf = window.config.media.subtitles; var subtitlesConf = window.config.media.subtitles;
// TODO populate the subtitles panel and show the subtitles button if (subtitlesConf) {
if (subtitlesConf && 'language' in subtitlesConf) { populateSubtitlesModal(resource);
if ('language' in subtitlesConf) {
tryFetchSubtitles(resource).then((subtitles) => { tryFetchSubtitles(resource).then((subtitles) => {
if (!subtitles) { if (!subtitles) {
showError('Cannot get subtitles: ' + error); showError('Cannot get subtitles');
_play(resource).finally(onVideoReady); _play(resource).finally(onVideoReady);
} else { } else {
_play(resource, subtitles).finally(onVideoReady); _play(resource, subtitles).finally(onVideoReady);
} }
}); });
}
} else { } else {
_play(resource).finally(onVideoReady); _play(resource).finally(onVideoReady);
} }
@ -346,7 +357,7 @@ $(document).ready(function() {
onVideoLoading(); onVideoLoading();
startStreaming(resource) startStreaming(resource)
.then((url) => { .then((url) => {
url = url + '?download=1' url = url + '?download'
window.open(url, '_blank'); window.open(url, '_blank');
resolve(url); resolve(url);
}) })
@ -359,6 +370,58 @@ $(document).ready(function() {
}); });
}; };
const populateSubtitlesModal = (resource) => {
$mediaSubtitlesMessage.text('Loading subtitles...');
$mediaSubtitlesResults.text('');
$mediaSubtitlesMessage.show();
$mediaSubtitlesResultsContainer.hide();
getSubtitles(resource).then((subs) => {
if (!subs.length) {
$mediaSubtitlesMessage.text('No subtitles found');
return;
}
$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 = $('<div></div>').addClass('row media-subtitle-container')
.data('download-link', sub.SubDownloadLink)
.data('resource', resource);
var $subFlagIconContainer = $('<div></div>').addClass('one column');
var $subFlagIcon = $('<span></span>')
.addClass(flagCode ? 'flag-icon flag-icon-' + flagCode : (
sub.IsLocal ? 'fa fa-download' : ''))
.text(!(flagCode || sub.IsLocal) ? '?' : '');
var $subMovieName = $('<div></div>').addClass('five columns')
.text(sub.MovieName);
var $subFileName = $('<div></div>').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 initBindings = function() { const initBindings = function() {
window.registerEventListener(onEvent); window.registerEventListener(onEvent);
$searchForm.on('submit', function(event) { $searchForm.on('submit', function(event) {
@ -531,61 +594,8 @@ $(document).ready(function() {
}); });
}); });
$mediaSearchSubtitles.on('mouseup touchend', () => { // $mediaSearchSubtitles.on('mouseup touchend', () => {
var resource = $mediaSearchSubtitles.data('resource'); // });
if (!resource) {
return;
}
$mediaSubtitlesMessage.text('Loading subtitles...');
$mediaSubtitlesResults.text('');
$mediaSubtitlesMessage.show();
$mediaSubtitlesResultsContainer.hide();
getSubtitles(resource).then((subs) => {
if (!subs.length) {
$mediaSubtitlesMessage.text('No subtitles found');
return;
}
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 = $('<div></div>').addClass('row media-subtitle-container')
.data('download-link', sub.SubDownloadLink)
.data('resource', resource);
var $subFlagIconContainer = $('<div></div>').addClass('one column');
var $subFlagIcon = $('<span></span>')
.addClass(flagCode ? 'flag-icon flag-icon-' + flagCode : (
sub.IsLocal ? 'fa fa-download' : ''))
.text(!(flagCode || sub.IsLocal) ? '?' : '');
var $subMovieName = $('<div></div>').addClass('five columns')
.text(sub.MovieName);
var $subFileName = $('<div></div>').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 initRemoteDevices = function() { const initRemoteDevices = function() {

View file

@ -0,0 +1,19 @@
<script>
window.addEventListener('load', () => {
var video = document.querySelector('#video');
video.addEventListener('load', () => {
var tracks = video.textTracks;
if (tracks.length) {
tracks[0].mode = 'showing';
}
});
}, false);
</script>
<video id="video" controls autoplay preload="metadata">
<source src="{{ media_url }}" type="{{ media_type }}">
{% if subtitles_url %}
<track label="Subtitles" kind="subtitles" src="{{ subtitles_url }}" default>
{% endif %}
</video>