Support for streaming media to browser

This commit is contained in:
Fabio Manganiello 2019-02-08 00:43:43 +01:00
parent 9ec3365413
commit b3f2974c4c
5 changed files with 155 additions and 33 deletions

View file

@ -377,7 +377,7 @@ class HttpBackend(Backend):
with self._media_map_lock: with self._media_map_lock:
if media_id in media_map: 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)) format(source, media_map[media_id].url))
media_hndl = MediaHandler.build(source, url=media_url) media_hndl = MediaHandler.build(source, url=media_url)

View file

@ -77,15 +77,11 @@ form#video-ctrl {
100% { background: #d4ffe3; } 100% { background: #d4ffe3; }
} }
button[data-toggle="#media-devices-panel"] { button.remote[data-panel-toggle="#media-devices-panel"] {
display: none;
}
button.remote[data-toggle="#media-devices-panel"] {
color: #34b868; 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), box-shadow: inset 0 2px 0 rgba(0,0,0,0.1),
inset 3px 0 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), inset 0 0 2px rgba(0,0,0,0.1),

View file

@ -5,7 +5,7 @@ $(document).ready(function() {
$volumeCtrl = $('#video-volume-ctrl'), $volumeCtrl = $('#video-volume-ctrl'),
$ctrlForm = $('#video-ctrl'), $ctrlForm = $('#video-ctrl'),
$devsPanel = $('#media-devices-panel'), $devsPanel = $('#media-devices-panel'),
$devsBtn = $('button[data-toggle="#media-devices-panel"]'), $devsBtn = $('button[data-panel-toggle="#media-devices-panel"]'),
$searchBarContainer = $('#media-search-bar-container'), $searchBarContainer = $('#media-search-bar-container'),
$mediaBtnsContainer = $('#media-btns-container'), $mediaBtnsContainer = $('#media-btns-container'),
prevVolume = undefined; prevVolume = undefined;
@ -43,18 +43,92 @@ $(document).ready(function() {
}; };
var getSelectedDevice = 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') 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) { if ($remoteDevice.length) {
device.isRemote = true; device.isRemote = true;
device.name = $remoteDevice.data('name'); device.name = $remoteDevice.data('name');
} else if ($browserDevice.length) {
device.isBrowser = true;
} }
return device; 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() { var initBindings = function() {
$searchForm.on('submit', function(event) { $searchForm.on('submit', function(event) {
var $input = $(this).find('input[name=video-search-text]'); var $input = $(this).find('input[name=video-search-text]');
@ -107,6 +181,10 @@ $(document).ready(function() {
}; };
var selectedDevice = getSelectedDevice(); var selectedDevice = getSelectedDevice();
if (selectedDevice.isBrowser) {
return; // The in-browser player can be used to control media
}
if (selectedDevice.isRemote) { if (selectedDevice.isRemote) {
requestArgs.action = 'media.chromecast.' + action; requestArgs.action = 'media.chromecast.' + action;
requestArgs.args = { 'chromecast': selectedDevice.name }; requestArgs.args = { 'chromecast': selectedDevice.name };
@ -149,19 +227,38 @@ $(document).ready(function() {
return false; return false;
} }
var onVideoLoading = function() {
$videoResults.text('Loading video...');
};
var onVideoReady = function() {
$videoResults.html(results);
};
var resource = $item.data('url')
var requestArgs = { var requestArgs = {
type: 'request', type: 'request',
action: 'media.play', action: 'media.play',
args: { resource: $item.data('url') }, args: { resource: resource },
}; };
var selectedDevice = getSelectedDevice(); var selectedDevice = getSelectedDevice();
if (selectedDevice.isBrowser) {
onVideoLoading();
startStreaming(resource)
.then((url) => { window.open(url, '_blank'); })
.finally(() => { onVideoReady(); });
return;
}
if (selectedDevice.isRemote) { if (selectedDevice.isRemote) {
requestArgs.action = 'media.chromecast.play'; requestArgs.action = 'media.chromecast.play';
requestArgs.args.chromecast = selectedDevice.name; requestArgs.args.chromecast = selectedDevice.name;
} }
$videoResults.text('Loading video...'); onVideoLoading();
execute( execute(
requestArgs, requestArgs,
function() { function() {
@ -171,7 +268,7 @@ $(document).ready(function() {
}, },
function() { function() {
$videoResults.html(results); onVideoReady();
} }
); );
}); });
@ -192,11 +289,13 @@ $(document).ready(function() {
$curSelected.removeClass('selected'); $curSelected.removeClass('selected');
$(this).addClass('selected'); $(this).addClass('selected');
if (!$(this).data('local')) { if ($(this).data('local') || $(this).data('browser')) {
$devsBtn.addClass('remote');
} else {
$devsBtn.removeClass('remote'); $devsBtn.removeClass('remote');
} else {
$devsBtn.addClass('remote');
} }
// TODO Logic for switching destination on the fly
} }
$devsPanel.hide(); $devsPanel.hide();
@ -216,10 +315,6 @@ $(document).ready(function() {
return; return;
} }
$searchBarContainer.removeClass('eleven').addClass('nine');
$mediaBtnsContainer.removeClass('one').addClass('three');
$devsBtn.show();
results = results.response.output; results = results.response.output;
for (var cast of results) { for (var cast of results) {
var $cast = $('<div></div>').addClass('row cast-device') var $cast = $('<div></div>').addClass('row cast-device')

View file

@ -10,26 +10,35 @@
</div> </div>
<div class="row"> <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"> <input type="text" name="video-search-text" placeholder="Search query or video URL">
</div> </div>
<div class="one columns" id="media-btns-container"> <div class="three columns" id="media-btns-container">
<button type="submit"> <button type="submit">
<i class="fa fa-search"></i> <i class="fa fa-search"></i>
</button> </button>
<button data-toggle="#media-devices-panel"> <button data-panel-toggle="#media-devices-panel">
<i class="fa fa-tv"></i> <i class="fa fa-tv"></i>
</button> </button>
</div> </div>
</div> </div>
<div class="row" id="media-devices-panel"> <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"> <div class="two columns">
<i class="fa fa-microchip cast-device-icon"></i> <i class="fa fa-server cast-device-icon"></i>
</div> </div>
<div class="ten columns"> <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> </div>
</div> </div>

View file

@ -34,11 +34,7 @@ class MediaWebtorrentPlugin(MediaPlugin):
Requires: Requires:
* **webtorrent** installed on your system (``npm install -g webtorrent``) * **webtorrent** installed on your system (``npm install -g webtorrent``)
* **webtorrent-cli** installed on your system * **webtorrent-cli** installed on your system (``npm install -g webtorrent-cli``)
(``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)
* A media plugin configured for streaming (e.g. media.mplayer * A media plugin configured for streaming (e.g. media.mplayer
or media.omxplayer) or media.omxplayer)
""" """
@ -48,6 +44,8 @@ class MediaWebtorrentPlugin(MediaPlugin):
# Download at least 10 MBs before starting streaming # Download at least 10 MBs before starting streaming
_download_size_before_streaming = 10 * 2**20 _download_size_before_streaming = 10 * 2**20
_web_stream_ready_timeout = 20
def __init__(self, webtorrent_bin=None, webtorrent_port=None, *args, def __init__(self, webtorrent_bin=None, webtorrent_port=None, *args,
**kwargs): **kwargs):
""" """
@ -68,6 +66,8 @@ class MediaWebtorrentPlugin(MediaPlugin):
self.webtorrent_port = webtorrent_port self.webtorrent_port = webtorrent_port
self._init_webtorrent_bin(webtorrent_bin=webtorrent_bin) self._init_webtorrent_bin(webtorrent_bin=webtorrent_bin)
self._init_media_player() self._init_media_player()
self._download_started_event = threading.Event()
self._torrent_stream_urls = {}
def _init_webtorrent_bin(self, webtorrent_bin=None): def _init_webtorrent_bin(self, webtorrent_bin=None):
@ -167,6 +167,8 @@ class MediaWebtorrentPlugin(MediaPlugin):
webtorrent_url = webtorrent_url.replace( webtorrent_url = webtorrent_url.replace(
'http://localhost', 'http://' + get_ip_or_hostname()) '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( self.logger.info('Torrent stream started on {}'.format(
webtorrent_url)) webtorrent_url))
@ -309,8 +311,9 @@ class MediaWebtorrentPlugin(MediaPlugin):
if self.webtorrent_port: if self.webtorrent_port:
webtorrent_args += ['-p', self.webtorrent_port] webtorrent_args += ['-p', self.webtorrent_port]
webtorrent_args += [resource] webtorrent_args += [resource]
self._download_started_event.clear()
self._webtorrent_process = subprocess.Popen(webtorrent_args, self._webtorrent_process = subprocess.Popen(webtorrent_args,
stdout=subprocess.PIPE) stdout=subprocess.PIPE)
@ -318,7 +321,26 @@ class MediaWebtorrentPlugin(MediaPlugin):
resource=resource, download_dir=download_dir, resource=resource, download_dir=download_dir,
player_type=player, player_args=player_args, player_type=player, player_args=player_args,
download_only=download_only)).start() 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 @action