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:
|
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)
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue