Added media.webtorrent plugin
This commit is contained in:
parent
4b7730d4cf
commit
a4f80d4622
4 changed files with 288 additions and 12 deletions
|
@ -124,9 +124,15 @@ class MediaPlugin(Plugin):
|
||||||
or resource.startswith('https://www.youtube.com/watch?v='):
|
or resource.startswith('https://www.youtube.com/watch?v='):
|
||||||
resource = self._get_youtube_content(resource)
|
resource = self._get_youtube_content(resource)
|
||||||
elif resource.startswith('magnet:?'):
|
elif resource.startswith('magnet:?'):
|
||||||
|
try:
|
||||||
|
get_plugin('media.webtorrent')
|
||||||
|
return resource # media.webtorrent will handle this
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
torrents = get_plugin('torrent')
|
torrents = get_plugin('torrent')
|
||||||
self.logger.info('Downloading torrent {} to {}'.format(resource,
|
self.logger.info('Downloading torrent {} to {}'.format(
|
||||||
download_dir))
|
resource, self.download_dir))
|
||||||
|
|
||||||
response = torrents.download(resource, download_dir=self.download_dir)
|
response = torrents.download(resource, download_dir=self.download_dir)
|
||||||
resources = [f for f in response.output if self._is_video_file(f)]
|
resources = [f for f in response.output if self._is_video_file(f)]
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
import os
|
import os
|
||||||
import select
|
import select
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from platypush.context import get_bus
|
from platypush.context import get_bus, get_plugin
|
||||||
from platypush.message.response import Response
|
from platypush.message.response import Response
|
||||||
from platypush.plugins.media import PlayerState, MediaPlugin
|
from platypush.plugins.media import PlayerState, MediaPlugin
|
||||||
from platypush.message.event.media import MediaPlayEvent, MediaPauseEvent, \
|
from platypush.message.event.media import MediaPlayEvent, MediaPauseEvent, \
|
||||||
MediaStopEvent, NewPlayingMediaEvent
|
MediaStopEvent, NewPlayingMediaEvent
|
||||||
|
|
||||||
from platypush.plugins import action
|
from platypush.plugins import action
|
||||||
|
from platypush.utils import find_bins_in_path
|
||||||
|
|
||||||
|
|
||||||
class MediaMplayerPlugin(MediaPlugin):
|
class MediaMplayerPlugin(MediaPlugin):
|
||||||
|
@ -77,11 +79,7 @@ class MediaMplayerPlugin(MediaPlugin):
|
||||||
def _init_mplayer_bin(self, mplayer_bin=None):
|
def _init_mplayer_bin(self, mplayer_bin=None):
|
||||||
if not mplayer_bin:
|
if not mplayer_bin:
|
||||||
bin_name = 'mplayer.exe' if os.name == 'nt' else 'mplayer'
|
bin_name = 'mplayer.exe' if os.name == 'nt' else 'mplayer'
|
||||||
bins = [os.path.join(p, bin_name)
|
bins = find_bins_in_path(bin_name)
|
||||||
for p in os.environ.get('PATH', '').split(':')
|
|
||||||
if os.path.isfile(os.path.join(p, bin_name))
|
|
||||||
and (os.name == 'nt' or
|
|
||||||
os.access(os.path.join(p, bin_name), os.X_OK))]
|
|
||||||
|
|
||||||
if not bins:
|
if not bins:
|
||||||
raise RuntimeError('MPlayer executable not specified and not ' +
|
raise RuntimeError('MPlayer executable not specified and not ' +
|
||||||
|
@ -121,6 +119,7 @@ class MediaMplayerPlugin(MediaPlugin):
|
||||||
popen_args['env'] = self._env
|
popen_args['env'] = self._env
|
||||||
|
|
||||||
self._mplayer = subprocess.Popen(args, **popen_args)
|
self._mplayer = subprocess.Popen(args, **popen_args)
|
||||||
|
threading.Thread(target=self._process_monitor()).start()
|
||||||
|
|
||||||
def _build_actions(self):
|
def _build_actions(self):
|
||||||
""" Populates the actions list by introspecting the mplayer executable """
|
""" Populates the actions list by introspecting the mplayer executable """
|
||||||
|
@ -222,6 +221,17 @@ class MediaMplayerPlugin(MediaPlugin):
|
||||||
return [ { 'action': action, 'args': self._actions[action] }
|
return [ { 'action': action, 'args': self._actions[action] }
|
||||||
for action in sorted(self._actions.keys()) ]
|
for action in sorted(self._actions.keys()) ]
|
||||||
|
|
||||||
|
def _process_monitor(self):
|
||||||
|
def _thread():
|
||||||
|
if not self._mplayer:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._mplayer.wait()
|
||||||
|
get_bus().post(MediaStopEvent())
|
||||||
|
self._mplayer = None
|
||||||
|
|
||||||
|
return _thread
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def play(self, resource, mplayer_args=None):
|
def play(self, resource, mplayer_args=None):
|
||||||
"""
|
"""
|
||||||
|
@ -234,9 +244,14 @@ class MediaMplayerPlugin(MediaPlugin):
|
||||||
MPlayer executable
|
MPlayer executable
|
||||||
:type mplayer_args: list[str]
|
:type mplayer_args: list[str]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
resource = self._get_resource(resource)
|
resource = self._get_resource(resource)
|
||||||
if resource.startswith('file://'):
|
if resource.startswith('file://'):
|
||||||
resource = resource[7:]
|
resource = resource[7:]
|
||||||
|
elif resource.startswith('magnet:?'):
|
||||||
|
torrent_plugin = get_plugin('media.webtorrent')
|
||||||
|
return torrent_plugin.play(resource)
|
||||||
|
|
||||||
return self._exec('loadfile', resource, mplayer_args=mplayer_args)
|
return self._exec('loadfile', resource, mplayer_args=mplayer_args)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -288,11 +303,11 @@ class MediaMplayerPlugin(MediaPlugin):
|
||||||
return self.get_property('pause').output.get('pause') == False
|
return self.get_property('pause').output.get('pause') == False
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def load(self, resource):
|
def load(self, resource, mplayer_args={}):
|
||||||
"""
|
"""
|
||||||
Load a resource/video in the player.
|
Load a resource/video in the player.
|
||||||
"""
|
"""
|
||||||
return self.play('loadfile', resource)
|
return self.play(resource, mplayer_args=mplayer_args)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def mute(self):
|
def mute(self):
|
||||||
|
|
249
platypush/plugins/media/webtorrent.py
Normal file
249
platypush/plugins/media/webtorrent.py
Normal file
|
@ -0,0 +1,249 @@
|
||||||
|
import os
|
||||||
|
import select
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
from platypush.context import get_bus, get_plugin
|
||||||
|
from platypush.message.response import Response
|
||||||
|
from platypush.plugins.media import PlayerState, MediaPlugin
|
||||||
|
from platypush.message.event.media import MediaPlayEvent, MediaPauseEvent, \
|
||||||
|
MediaStopEvent, NewPlayingMediaEvent
|
||||||
|
|
||||||
|
from platypush.plugins import action
|
||||||
|
from platypush.utils import find_bins_in_path
|
||||||
|
|
||||||
|
|
||||||
|
class MediaWebtorrentPlugin(MediaPlugin):
|
||||||
|
"""
|
||||||
|
Plugin to download and stream videos using webtorrent
|
||||||
|
|
||||||
|
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)
|
||||||
|
"""
|
||||||
|
|
||||||
|
_supported_media_plugins = { 'media.mplayer', 'media.omxplayer' }
|
||||||
|
_supported_webtorrent_players = {'airplay', 'chromecast', 'mplayer',
|
||||||
|
'mpv', 'omxplayer', 'vlc', 'xbmc'}
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, player_type=None, player_args=None, webtorrent_bin=None,
|
||||||
|
*args, **kwargs):
|
||||||
|
"""
|
||||||
|
Create the webtorrent wrapper
|
||||||
|
|
||||||
|
:param player_type: Player type to be used for streaming. Check
|
||||||
|
https://www.npmjs.com/package/webtorrent for a full list of the
|
||||||
|
supported players. If no player is specified then it will search in
|
||||||
|
your Platypush configuration for any enabled and supported media
|
||||||
|
plugin (either ``media.mplayer`` or ``media.omxplayer``) to
|
||||||
|
configure the streaming player. Currently supported options:
|
||||||
|
- airplay
|
||||||
|
- chromecast
|
||||||
|
- mplayer
|
||||||
|
- mpv
|
||||||
|
- omxlayer
|
||||||
|
- vlc
|
||||||
|
- xbmc
|
||||||
|
|
||||||
|
:type player_type: str
|
||||||
|
|
||||||
|
:param player_args: Extra arguments to pass to the player as a list.
|
||||||
|
For mplayer, vlc, omxplayer and mpv this will be a list of extra
|
||||||
|
arguments to be passed to the executable (e.g. fullscreen, volume,
|
||||||
|
audio sink etc.).
|
||||||
|
:type player_args: list[str]
|
||||||
|
|
||||||
|
:param webtorrent_bin: Path to your webtorrent executable. If not set,
|
||||||
|
then Platypush will search for the right executable in your PATH
|
||||||
|
:type webtorrent_bin: str
|
||||||
|
"""
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self._init_webtorrent_bin(webtorrent_bin=webtorrent_bin)
|
||||||
|
self._init_media_player(player_type=player_type,
|
||||||
|
player_args=player_args or [])
|
||||||
|
|
||||||
|
|
||||||
|
def _init_webtorrent_bin(self, webtorrent_bin=None):
|
||||||
|
if not webtorrent_bin:
|
||||||
|
bin_name = 'webtorrent.exe' if os.name == 'nt' else 'webtorrent'
|
||||||
|
bins = find_bins_in_path(bin_name)
|
||||||
|
|
||||||
|
if not bins:
|
||||||
|
raise RuntimeError('Webtorrent executable not specified and ' +
|
||||||
|
'not found in your PATH. Make sure that ' +
|
||||||
|
'webtorrent is either installed or ' +
|
||||||
|
'configured and that both webtorrent and ' +
|
||||||
|
'webtorrent-cli are installed')
|
||||||
|
|
||||||
|
self.webtorrent_bin = bins[0]
|
||||||
|
else:
|
||||||
|
webtorrent_bin = os.path.expanduser(webtorrent_bin)
|
||||||
|
if not (os.path.isfile(webtorrent_bin)
|
||||||
|
and (os.name == 'nt' or os.access(webtorrent_bin, os.X_OK))):
|
||||||
|
raise RuntimeError('{} is does not exist or is not a valid ' +
|
||||||
|
'executable file'.format(webtorrent_bin))
|
||||||
|
|
||||||
|
self.webtorrent_bin = webtorrent_bin
|
||||||
|
|
||||||
|
def _init_media_player(self, player_type, player_args):
|
||||||
|
if player_type is None:
|
||||||
|
media_plugin = None
|
||||||
|
plugin_name = None
|
||||||
|
|
||||||
|
for plugin_name in self._supported_media_plugins:
|
||||||
|
try:
|
||||||
|
media_plugin = get_plugin(plugin_name)
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not media_plugin:
|
||||||
|
raise RuntimeError(('No media player specified and no ' +
|
||||||
|
'compatible media plugin configured - ' +
|
||||||
|
'supported media plugins: {}').format(
|
||||||
|
self._supported_media_plugins))
|
||||||
|
|
||||||
|
self.player_type = plugin_name[len('media.'):]
|
||||||
|
self.player_args = media_plugin.args
|
||||||
|
else:
|
||||||
|
if player_type not in self._supported_webtorrent_players:
|
||||||
|
raise RuntimeError(('{} is not a supported player (supported ' +
|
||||||
|
'players: {})').format(
|
||||||
|
player_type,
|
||||||
|
self._supported_webtorrent_players))
|
||||||
|
|
||||||
|
self.player_type = player_type
|
||||||
|
self.player_args = player_args or []
|
||||||
|
|
||||||
|
if not self.player_args:
|
||||||
|
try:
|
||||||
|
plugin = get_plugin('media.' + player_type)
|
||||||
|
self.player_args = plugin.args
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self._player = None
|
||||||
|
|
||||||
|
|
||||||
|
def _start_webtorrent(self, resource):
|
||||||
|
if self._player:
|
||||||
|
try:
|
||||||
|
self.quit()
|
||||||
|
except:
|
||||||
|
self.logger.debug('Failed to quit the previous instance: {}'.
|
||||||
|
format(str))
|
||||||
|
|
||||||
|
webtorrent_args = [self.webtorrent_bin, '--' + self.player_type,
|
||||||
|
resource]
|
||||||
|
|
||||||
|
if self.player_args:
|
||||||
|
webtorrent_args += ['--' + self.player_type + "-args='" + \
|
||||||
|
repr(' '.join(self.player_args)) + "'"]
|
||||||
|
|
||||||
|
popen_args = {
|
||||||
|
'stdin': subprocess.PIPE,
|
||||||
|
'stdout': subprocess.PIPE,
|
||||||
|
}
|
||||||
|
|
||||||
|
if self._env:
|
||||||
|
popen_args['env'] = self._env
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
plugin = get_plugin('media.' + player_type)
|
||||||
|
if plugin._env:
|
||||||
|
popen_args['env'] = plugin._env
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self._player = subprocess.Popen(webtorrent_args, **popen_args)
|
||||||
|
|
||||||
|
def _process_monitor(self):
|
||||||
|
def _thread():
|
||||||
|
if not self._player:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._player.wait()
|
||||||
|
get_bus().post(MediaStopEvent())
|
||||||
|
self._player = None
|
||||||
|
|
||||||
|
return _thread
|
||||||
|
|
||||||
|
@action
|
||||||
|
def play(self, resource):
|
||||||
|
"""
|
||||||
|
Download and stream a torrent
|
||||||
|
|
||||||
|
:param resource: Play a resource, as a magnet link, torrent URL or
|
||||||
|
torrent file path
|
||||||
|
:type resource: str
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._start_webtorrent(resource)
|
||||||
|
bus = get_bus()
|
||||||
|
bus.post(NewPlayingMediaEvent(resource=resource))
|
||||||
|
threading.Thread(target=self._process_monitor()).start()
|
||||||
|
|
||||||
|
@action
|
||||||
|
def stop(self):
|
||||||
|
""" Stop the playback """
|
||||||
|
return self.quit()
|
||||||
|
|
||||||
|
@action
|
||||||
|
def quit(self):
|
||||||
|
""" Quit the player """
|
||||||
|
if self._player and self._is_process_alive(self._player.pid):
|
||||||
|
self._player.terminate()
|
||||||
|
self._player.wait()
|
||||||
|
try: self._player.kill()
|
||||||
|
except: pass
|
||||||
|
bus.post(MediaStopEvent())
|
||||||
|
|
||||||
|
self._player = None
|
||||||
|
|
||||||
|
@action
|
||||||
|
def load(self, resource):
|
||||||
|
"""
|
||||||
|
Load a torrent resource in the player.
|
||||||
|
"""
|
||||||
|
return self.play(resource)
|
||||||
|
|
||||||
|
def _is_process_alive(self):
|
||||||
|
if not self._player:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.kill(self._player.pid, 0)
|
||||||
|
return True
|
||||||
|
except OSError:
|
||||||
|
self._player = None
|
||||||
|
return False
|
||||||
|
|
||||||
|
@action
|
||||||
|
def status(self):
|
||||||
|
"""
|
||||||
|
Get the current player state.
|
||||||
|
|
||||||
|
:returns: A dictionary containing the current state.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
output = {
|
||||||
|
"state": "play" # or "stop" or "pause"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
return {'state': PlayerState.PLAY.value
|
||||||
|
if self._player and self._is_process_alive()
|
||||||
|
else PlayerState.STOP.value}
|
||||||
|
|
||||||
|
|
||||||
|
# vim:sw=4:ts=4:et:
|
|
@ -172,6 +172,12 @@ def set_thread_name(name):
|
||||||
except ImportError:
|
except ImportError:
|
||||||
logger.debug('Unable to set thread name: prctl module is missing')
|
logger.debug('Unable to set thread name: prctl module is missing')
|
||||||
|
|
||||||
|
def find_bins_in_path(bin_name):
|
||||||
|
return [os.path.join(p, bin_name)
|
||||||
|
for p in os.environ.get('PATH', '').split(':')
|
||||||
|
if os.path.isfile(os.path.join(p, bin_name))
|
||||||
|
and (os.name == 'nt' or
|
||||||
|
os.access(os.path.join(p, bin_name), os.X_OK))]
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue