forked from platypush/platypush
Attempt to support subtitle tracks in web player
This commit is contained in:
parent
34f0264d5e
commit
41c34b4bc5
4 changed files with 241 additions and 89 deletions
|
@ -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:
|
||||||
|
|
|
@ -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))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
19
platypush/backend/http/templates/webplayer.html
Normal file
19
platypush/backend/http/templates/webplayer.html
Normal 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>
|
||||||
|
|
Loading…
Reference in a new issue