From a4f80d4622c014f977b41267e548e162e7a76d1f Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Sun, 3 Feb 2019 17:43:30 +0100 Subject: [PATCH] Added media.webtorrent plugin --- platypush/plugins/media/__init__.py | 10 +- platypush/plugins/media/mplayer.py | 33 +++- platypush/plugins/media/webtorrent.py | 249 ++++++++++++++++++++++++++ platypush/utils/__init__.py | 8 +- 4 files changed, 288 insertions(+), 12 deletions(-) create mode 100644 platypush/plugins/media/webtorrent.py diff --git a/platypush/plugins/media/__init__.py b/platypush/plugins/media/__init__.py index 5598c3b053..681aea406d 100644 --- a/platypush/plugins/media/__init__.py +++ b/platypush/plugins/media/__init__.py @@ -124,9 +124,15 @@ class MediaPlugin(Plugin): or resource.startswith('https://www.youtube.com/watch?v='): resource = self._get_youtube_content(resource) elif resource.startswith('magnet:?'): + try: + get_plugin('media.webtorrent') + return resource # media.webtorrent will handle this + except: + pass + torrents = get_plugin('torrent') - self.logger.info('Downloading torrent {} to {}'.format(resource, - download_dir)) + self.logger.info('Downloading torrent {} to {}'.format( + resource, 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)] diff --git a/platypush/plugins/media/mplayer.py b/platypush/plugins/media/mplayer.py index f774dbf026..d5c84a0d25 100644 --- a/platypush/plugins/media/mplayer.py +++ b/platypush/plugins/media/mplayer.py @@ -1,15 +1,17 @@ import os import select import subprocess +import threading import time -from platypush.context import get_bus +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 MediaMplayerPlugin(MediaPlugin): @@ -77,11 +79,7 @@ class MediaMplayerPlugin(MediaPlugin): def _init_mplayer_bin(self, mplayer_bin=None): if not mplayer_bin: bin_name = 'mplayer.exe' if os.name == 'nt' else 'mplayer' - bins = [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))] + bins = find_bins_in_path(bin_name) if not bins: raise RuntimeError('MPlayer executable not specified and not ' + @@ -121,13 +119,14 @@ class MediaMplayerPlugin(MediaPlugin): popen_args['env'] = self._env self._mplayer = subprocess.Popen(args, **popen_args) + threading.Thread(target=self._process_monitor()).start() def _build_actions(self): """ Populates the actions list by introspecting the mplayer executable """ self._actions = {} mplayer = subprocess.Popen([self.mplayer_bin, '-input', 'cmdlist'], - stdin=subprocess.PIPE, stdout=subprocess.PIPE) + stdin=subprocess.PIPE, stdout=subprocess.PIPE) def args_pprint(txt): lc = txt.lower() @@ -222,6 +221,17 @@ class MediaMplayerPlugin(MediaPlugin): return [ { 'action': action, 'args': self._actions[action] } 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 def play(self, resource, mplayer_args=None): """ @@ -234,9 +244,14 @@ class MediaMplayerPlugin(MediaPlugin): MPlayer executable :type mplayer_args: list[str] """ + resource = self._get_resource(resource) if resource.startswith('file://'): 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) @action @@ -288,11 +303,11 @@ class MediaMplayerPlugin(MediaPlugin): return self.get_property('pause').output.get('pause') == False @action - def load(self, resource): + def load(self, resource, mplayer_args={}): """ Load a resource/video in the player. """ - return self.play('loadfile', resource) + return self.play(resource, mplayer_args=mplayer_args) @action def mute(self): diff --git a/platypush/plugins/media/webtorrent.py b/platypush/plugins/media/webtorrent.py new file mode 100644 index 0000000000..db3c1b996f --- /dev/null +++ b/platypush/plugins/media/webtorrent.py @@ -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: diff --git a/platypush/utils/__init__.py b/platypush/utils/__init__.py index 8b29533d68..123de1da4a 100644 --- a/platypush/utils/__init__.py +++ b/platypush/utils/__init__.py @@ -172,6 +172,12 @@ def set_thread_name(name): except ImportError: 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: -