forked from platypush/platypush
Support for streaming media to browser
This commit is contained in:
parent
9ec3365413
commit
b3f2974c4c
5 changed files with 155 additions and 33 deletions
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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 = $('<div></div>').addClass('row cast-device')
|
||||
|
|
|
@ -10,26 +10,35 @@
|
|||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="eleven columns" id="media-search-bar-container">
|
||||
<div class="nine columns" id="media-search-bar-container">
|
||||
<input type="text" name="video-search-text" placeholder="Search query or video URL">
|
||||
</div>
|
||||
<div class="one columns" id="media-btns-container">
|
||||
<div class="three columns" id="media-btns-container">
|
||||
<button type="submit">
|
||||
<i class="fa fa-search"></i>
|
||||
</button>
|
||||
<button data-toggle="#media-devices-panel">
|
||||
<button data-panel-toggle="#media-devices-panel">
|
||||
<i class="fa fa-tv"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" id="media-devices-panel">
|
||||
<div class="row cast-device cast-device-local selected" data-local="local">
|
||||
<div class="row cast-device cast-device-local selected" data-local="local" data-name="_server">
|
||||
<div class="two columns">
|
||||
<i class="fa fa-microchip cast-device-icon"></i>
|
||||
<i class="fa fa-server cast-device-icon"></i>
|
||||
</div>
|
||||
<div class="ten columns">
|
||||
<span class="cast-device-name">This device</span>
|
||||
<span class="cast-device-name">{{ request.host_url.split('/')[2].split(':')[0] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row cast-device cast-device-local" data-browser="browser" data-name="_local">
|
||||
<div class="two columns">
|
||||
<i class="fa fa-globe cast-device-icon"></i>
|
||||
</div>
|
||||
<div class="ten columns">
|
||||
<span class="cast-device-name">This browser</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue