From b3f2974c4c49d89b6961c6c3c3eca0bd003f50af Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Fri, 8 Feb 2019 00:43:43 +0100 Subject: [PATCH] Support for streaming media to browser --- platypush/backend/http/__init__.py | 2 +- platypush/backend/http/static/css/media.css | 8 +- platypush/backend/http/static/js/media.js | 121 ++++++++++++++++-- .../backend/http/templates/plugins/media.html | 21 ++- platypush/plugins/media/webtorrent.py | 36 +++++- 5 files changed, 155 insertions(+), 33 deletions(-) diff --git a/platypush/backend/http/__init__.py b/platypush/backend/http/__init__.py index a0ffe4ecb..9557ed3cc 100644 --- a/platypush/backend/http/__init__.py +++ b/platypush/backend/http/__init__.py @@ -377,7 +377,7 @@ class HttpBackend(Backend): with self._media_map_lock: if media_id in media_map: - raise FileExistsError('"{}" is already registered on {}'. + raise FileExistsError('"{}" is already registered on "{}"'. format(source, media_map[media_id].url)) media_hndl = MediaHandler.build(source, url=media_url) diff --git a/platypush/backend/http/static/css/media.css b/platypush/backend/http/static/css/media.css index a2260dabf..423140562 100644 --- a/platypush/backend/http/static/css/media.css +++ b/platypush/backend/http/static/css/media.css @@ -77,15 +77,11 @@ form#video-ctrl { 100% { background: #d4ffe3; } } -button[data-toggle="#media-devices-panel"] { - display: none; -} - - button.remote[data-toggle="#media-devices-panel"] { + button.remote[data-panel-toggle="#media-devices-panel"] { color: #34b868; } - button.selected[data-toggle="#media-devices-panel"] { + button.selected[data-panel-toggle="#media-devices-panel"] { box-shadow: inset 0 2px 0 rgba(0,0,0,0.1), inset 3px 0 0 rgba(0,0,0,0.1), inset 0 0 2px rgba(0,0,0,0.1), diff --git a/platypush/backend/http/static/js/media.js b/platypush/backend/http/static/js/media.js index 9effdc585..153885a41 100644 --- a/platypush/backend/http/static/js/media.js +++ b/platypush/backend/http/static/js/media.js @@ -5,7 +5,7 @@ $(document).ready(function() { $volumeCtrl = $('#video-volume-ctrl'), $ctrlForm = $('#video-ctrl'), $devsPanel = $('#media-devices-panel'), - $devsBtn = $('button[data-toggle="#media-devices-panel"]'), + $devsBtn = $('button[data-panel-toggle="#media-devices-panel"]'), $searchBarContainer = $('#media-search-bar-container'), $mediaBtnsContainer = $('#media-btns-container'), prevVolume = undefined; @@ -43,18 +43,92 @@ $(document).ready(function() { }; var getSelectedDevice = function() { - var device = { isRemote: false, name: undefined }; + var device = { isBrowser: false, isRemote: false, name: undefined }; var $remoteDevice = $devsPanel.find('.cast-device.selected') - .filter((i, dev) => !$(dev).data('local') && $(dev).data('name')); + .filter((i, dev) => !$(dev).data('local') && !$(dev).data('browser') && $(dev).data('name')); + var $browserDevice = $devsPanel.find('.cast-device.selected') + .filter((i, dev) => $(dev).data('browser')); if ($remoteDevice.length) { device.isRemote = true; device.name = $remoteDevice.data('name'); + } else if ($browserDevice.length) { + device.isBrowser = true; } return device; }; + var startStreamingTorrent = function(torrent) { + return new Promise((resolve, reject) => { + execute( + { + type: 'request', + action: 'media.webtorrent.play', + args: { + resource: torrent, + download_only: true, + } + }, + + (response) => { + resolve(response.response.output.url); + }, + + (error) => { + reject(error); + } + ); + }); + }; + + var startStreaming = function(media) { + if (media.startsWith('magnet:?')) { + return new Promise((resolve, reject) => { + startStreamingTorrent(media) + .then((url) => { resolve(url); }) + .catch((error) => { reject(error); }); + }); + } + + return new Promise((resolve, reject) => { + $.ajax({ + type: 'PUT', + url: '/media', + contentType: 'application/json', + data: JSON.stringify({ source: media }), + + complete: (xhr, textStatus) => { + var url; + if (xhr.status == 200) { + url = xhr.responseJSON.url; + } else if (xhr.status == 409) { + // Media mount point already registered + url = xhr.responseText.match( + /.*is already registered on ("|")(https?:\/\/[^\/]+\/media\/[0-9a-f]+\.[0-9a-z]+)("|").*/)[2] + } + + if (url) { + var uri = url.match(/https?:\/\/[^\/]+(\/media\/.*)/)[1] + resolve(uri); + } else { + reject(Error(xhr.responseText)); + } + }, + }); + }); + }; + + var stopStreaming = function(media_id) { + return new Promise((resolve, reject) => { + $.ajax({ + type: 'DELETE', + url: '/media/' + media_id, + contentType: 'application/json', + }); + }); + }; + var initBindings = function() { $searchForm.on('submit', function(event) { var $input = $(this).find('input[name=video-search-text]'); @@ -107,6 +181,10 @@ $(document).ready(function() { }; var selectedDevice = getSelectedDevice(); + if (selectedDevice.isBrowser) { + return; // The in-browser player can be used to control media + } + if (selectedDevice.isRemote) { requestArgs.action = 'media.chromecast.' + action; requestArgs.args = { 'chromecast': selectedDevice.name }; @@ -149,19 +227,38 @@ $(document).ready(function() { return false; } + var onVideoLoading = function() { + $videoResults.text('Loading video...'); + }; + + var onVideoReady = function() { + $videoResults.html(results); + }; + + var resource = $item.data('url') var requestArgs = { type: 'request', action: 'media.play', - args: { resource: $item.data('url') }, + args: { resource: resource }, }; var selectedDevice = getSelectedDevice(); + if (selectedDevice.isBrowser) { + onVideoLoading(); + + startStreaming(resource) + .then((url) => { window.open(url, '_blank'); }) + .finally(() => { onVideoReady(); }); + + return; + } + if (selectedDevice.isRemote) { requestArgs.action = 'media.chromecast.play'; requestArgs.args.chromecast = selectedDevice.name; } - $videoResults.text('Loading video...'); + onVideoLoading(); execute( requestArgs, function() { @@ -171,7 +268,7 @@ $(document).ready(function() { }, function() { - $videoResults.html(results); + onVideoReady(); } ); }); @@ -192,11 +289,13 @@ $(document).ready(function() { $curSelected.removeClass('selected'); $(this).addClass('selected'); - if (!$(this).data('local')) { - $devsBtn.addClass('remote'); - } else { + if ($(this).data('local') || $(this).data('browser')) { $devsBtn.removeClass('remote'); + } else { + $devsBtn.addClass('remote'); } + + // TODO Logic for switching destination on the fly } $devsPanel.hide(); @@ -216,10 +315,6 @@ $(document).ready(function() { return; } - $searchBarContainer.removeClass('eleven').addClass('nine'); - $mediaBtnsContainer.removeClass('one').addClass('three'); - $devsBtn.show(); - results = results.response.output; for (var cast of results) { var $cast = $('
').addClass('row cast-device') diff --git a/platypush/backend/http/templates/plugins/media.html b/platypush/backend/http/templates/plugins/media.html index 7286a2313..bbe817924 100644 --- a/platypush/backend/http/templates/plugins/media.html +++ b/platypush/backend/http/templates/plugins/media.html @@ -10,26 +10,35 @@
-
+
-
+
-
-
+
- +
- This device + {{ request.host_url.split('/')[2].split(':')[0] }} +
+
+ +
+
+ +
+
+ This browser
diff --git a/platypush/plugins/media/webtorrent.py b/platypush/plugins/media/webtorrent.py index c533622d7..57086dcee 100644 --- a/platypush/plugins/media/webtorrent.py +++ b/platypush/plugins/media/webtorrent.py @@ -34,11 +34,7 @@ class MediaWebtorrentPlugin(MediaPlugin): Requires: * **webtorrent** installed on your system (``npm install -g webtorrent``) - * **webtorrent-cli** installed on your system - (``npm install -g webtorrent-cli`` or better - ``npm install -g BlackLight/webtorrent-cli`` as my fork contains - the ``--[player]-args`` options to pass custom arguments to your - installed player) + * **webtorrent-cli** installed on your system (``npm install -g webtorrent-cli``) * A media plugin configured for streaming (e.g. media.mplayer or media.omxplayer) """ @@ -48,6 +44,8 @@ class MediaWebtorrentPlugin(MediaPlugin): # Download at least 10 MBs before starting streaming _download_size_before_streaming = 10 * 2**20 + _web_stream_ready_timeout = 20 + def __init__(self, webtorrent_bin=None, webtorrent_port=None, *args, **kwargs): """ @@ -68,6 +66,8 @@ class MediaWebtorrentPlugin(MediaPlugin): self.webtorrent_port = webtorrent_port self._init_webtorrent_bin(webtorrent_bin=webtorrent_bin) self._init_media_player() + self._download_started_event = threading.Event() + self._torrent_stream_urls = {} def _init_webtorrent_bin(self, webtorrent_bin=None): @@ -167,6 +167,8 @@ class MediaWebtorrentPlugin(MediaPlugin): webtorrent_url = webtorrent_url.replace( 'http://localhost', 'http://' + get_ip_or_hostname()) + self._torrent_stream_urls[resource] = webtorrent_url + self._download_started_event.set() self.logger.info('Torrent stream started on {}'.format( webtorrent_url)) @@ -309,8 +311,9 @@ class MediaWebtorrentPlugin(MediaPlugin): if self.webtorrent_port: webtorrent_args += ['-p', self.webtorrent_port] - webtorrent_args += [resource] + + self._download_started_event.clear() self._webtorrent_process = subprocess.Popen(webtorrent_args, stdout=subprocess.PIPE) @@ -318,7 +321,26 @@ class MediaWebtorrentPlugin(MediaPlugin): resource=resource, download_dir=download_dir, player_type=player, player_args=player_args, download_only=download_only)).start() - return { 'resource': resource } + + stream_url = None + player_ready_wait_start = time.time() + + while not stream_url: + triggered = self._download_started_event.wait( + self._web_stream_ready_timeout) + + if not triggered or time.time() - player_ready_wait_start >= \ + self._web_stream_ready_timeout: + break + + stream_url = self._torrent_stream_urls.get(resource) + + if not stream_url: + return (None, ('The webtorrent process hasn\'t started ' + + 'streaming after {} seconds').format( + self._web_stream_ready_timeout)) + + return { 'resource': resource, 'url': stream_url } @action