From dc2a686d238efd0da2a0a93501a19b22999ae673 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Tue, 5 Feb 2019 02:30:20 +0100 Subject: [PATCH] Support for casting torrents to Chromecast --- platypush/plugins/media/__init__.py | 24 +++-- platypush/plugins/media/chromecast.py | 127 ++++++++++++++++---------- platypush/plugins/media/mplayer.py | 4 +- platypush/plugins/media/webtorrent.py | 44 ++++++--- platypush/utils/__init__.py | 6 ++ 5 files changed, 134 insertions(+), 71 deletions(-) diff --git a/platypush/plugins/media/__init__.py b/platypush/plugins/media/__init__.py index 6d8ec6b5a..77ce7fede 100644 --- a/platypush/plugins/media/__init__.py +++ b/platypush/plugins/media/__init__.py @@ -22,7 +22,7 @@ class MediaPlugin(Plugin): Requires: - * A media player installed (supported so far: mplayer, omxplayer) + * A media player installed (supported so far: mplayer, omxplayer, chromecast) * **python-libtorrent** (``pip install python-libtorrent``), optional for Torrent support * **youtube-dl** installed on your system (see your distro instructions), optional for YouTube support """ @@ -53,7 +53,8 @@ class MediaPlugin(Plugin): 'f4b', } - _supported_media_plugins = { 'media.mplayer', 'media.omxplayer' } + _supported_media_plugins = {'media.mplayer', 'media.omxplayer', + 'media.chromecast'} def __init__(self, media_dirs=[], download_dir=None, env=None, *args, **kwargs): @@ -75,10 +76,17 @@ class MediaPlugin(Plugin): player = None player_config = {} - for plugin in Config.get_plugins().keys(): - if plugin in self._supported_media_plugins: - player = plugin - break + + if self.__class__.__name__ == 'MediaPlugin': + # Abstract class, initialize with the default configured player + for plugin in Config.get_plugins().keys(): + if plugin in self._supported_media_plugins: + player = plugin + if get_plugin(player).is_local(): + # Local players have priority as default if configured + break + else: + player = self # Derived concrete class if not player: raise AttributeError('No media plugin configured') @@ -126,6 +134,10 @@ class MediaPlugin(Plugin): if resource.startswith('youtube:') \ or resource.startswith('https://www.youtube.com/watch?v='): + if self.__class.__.__name__ != 'MediaChromecastPlugin': + # The Chromecast has already its way to handle YouTube + return resource + resource = self._get_youtube_content(resource) elif resource.startswith('magnet:?'): try: diff --git a/platypush/plugins/media/chromecast.py b/platypush/plugins/media/chromecast.py index 04dbe90c7..f30be42f0 100644 --- a/platypush/plugins/media/chromecast.py +++ b/platypush/plugins/media/chromecast.py @@ -3,15 +3,18 @@ import pychromecast from pychromecast.controllers.youtube import YouTubeController +from platypush.context import get_plugin from platypush.plugins import Plugin, action +from platypush.plugins.media import MediaPlugin -class MediaChromecastPlugin(Plugin): +class MediaChromecastPlugin(MediaPlugin): """ Plugin to interact with Chromecast devices Supported formats: + * HTTP media URLs * YouTube URLs * Plex (through ``media.plex`` plugin, experimental) @@ -32,6 +35,7 @@ class MediaChromecastPlugin(Plugin): super().__init__(*args, **kwargs) + self._is_local = False self.chromecast = chromecast self.chromecasts = {} @@ -67,7 +71,10 @@ class MediaChromecastPlugin(Plugin): } for cc in pychromecast.get_chromecasts() ] - def get_chromecast(self, chromecast=None): + def get_chromecast(self, chromecast=None, n_tries=3): + if isinstance(chromecast, pychromecast.Chromecast): + return chromecast + if not chromecast: if not self.chromecast: raise RuntimeError('No Chromecast specified nor default Chromecast configured') @@ -75,10 +82,17 @@ class MediaChromecastPlugin(Plugin): if chromecast not in self.chromecasts: - self.chromecasts = { - cast.device.friendly_name: cast - for cast in pychromecast.get_chromecasts() - } + casts = {} + while n_tries > 0: + n_tries -= 1 + casts.update({ + cast.device.friendly_name: cast + for cast in pychromecast.get_chromecasts() + }) + + if chromecast in casts: + self.chromecasts.update(casts) + break if chromecast not in self.chromecasts: raise RuntimeError('Device {} not found'.format(chromecast)) @@ -87,7 +101,7 @@ class MediaChromecastPlugin(Plugin): @action - def cast_media(self, media, content_type=None, chromecast=None, title=None, + def play(self, resource, content_type=None, chromecast=None, title=None, image_url=None, autoplay=True, current_time=0, stream_type=STREAM_TYPE_BUFFERED, subtitles=None, subtitles_lang='en-US', subtitles_mime='text/vtt', @@ -95,10 +109,10 @@ class MediaChromecastPlugin(Plugin): """ Cast media to a visible Chromecast - :param media: Media to cast - :type media: str + :param resource: Media to cast + :type resource: str - :param content_type: Content type + :param content_type: Content type as a MIME type string :type content_type: str :param chromecast: Chromecast to cast to. If none is specified, then the default configured Chromecast will be used. @@ -139,7 +153,7 @@ class MediaChromecastPlugin(Plugin): cast.wait() mc = cast.media_controller - yt = self._get_youtube_url(media) + yt = self._get_youtube_url(resource) if yt: self.logger.info('Playing YouTube video {} on {}'.format( @@ -150,13 +164,33 @@ class MediaChromecastPlugin(Plugin): hndl.update_screen_id() return hndl.play_video(yt) + resource = self._get_resource(resource) + if resource.startswith('magnet:?'): + player_args = { 'chromecast': cast } + return get_plugin('media.webtorrent').play(resource, + player='chromecast', + **player_args) + + # Best effort from the extension + if not content_type: + for ext in self.video_extensions: + if ('.' + ext).lower() in resource.lower(): + content_type = 'video/' + ext + break + + if not content_type: + for ext in self.audio_extensions: + if ('.' + ext).lower() in resource.lower(): + content_type = 'audio/' + ext + break + if not content_type: raise RuntimeError('content_type required to process media {}'. - format(media)) + format(resource)) - self.logger.info('Playing {} on {}'.format(media, chromecast)) + self.logger.info('Playing {} on {}'.format(resource, chromecast)) - mc.play_media(media, content_type, title=title, thumb=image_url, + mc.play_media(resource, content_type, title=title, thumb=image_url, current_time=current_time, autoplay=autoplay, stream_type=stream_type, subtitles=subtitles, subtitles_lang=subtitles_lang, subtitles_mime=subtitles_mime, @@ -177,19 +211,12 @@ class MediaChromecastPlugin(Plugin): return None - @action - def play(self, chromecast=None): - return self.get_chromecast(chromecast or self.chromecast).media_controller.play() - + def load(self, *args, **kwargs): + return self.play(*args, **kwargs) @action def pause(self, chromecast=None): - return self.get_chromecast(chromecast or self.chromecast).media_controller.pause() - - - @action - def toggle_pause(self, chromecast=None): cast = self.get_chromecast(chromecast or self.chromecast) if cast.media_controller.is_paused: return cast.media_controller.play() @@ -208,22 +235,25 @@ class MediaChromecastPlugin(Plugin): @action - def seek(self, location, chromecast=None): - return self.get_chromecast(chromecast or self.chromecast).media_controller.seek(location) + def set_position(self, position, chromecast=None): + return self.get_chromecast(chromecast or self.chromecast).media_controller.seek(position) + + @action + def seek(self, position, chromecast=None): + return self.forward(chromecast=chromecast, offset=position) + + @action + def back(self, chromecast=None, offset=60): + mc = self.get_chromecast(chromecast or self.chromecast).media_controller + if mc.status.current_time: + return mc.seek(mc.status.current_time-offset) @action - def back(self, chromecast=None, delta=30): + def forward(self, chromecast=None, offset=60): mc = self.get_chromecast(chromecast or self.chromecast).media_controller if mc.status.current_time: - return mc.seek(mc.status.current_time-delta) - - - @action - def forward(self, chromecast=None, delta=30): - mc = self.get_chromecast(chromecast or self.chromecast).media_controller - if mc.status.current_time: - return mc.seek(mc.status.current_time+delta) + return mc.seek(mc.status.current_time+offset) @action @@ -288,7 +318,7 @@ class MediaChromecastPlugin(Plugin): cast.join(timeout=timeout, blocking=blocking) @action - def quit_app(self, chromecast=None): + def quit(self, chromecast=None): """ Exits the current app on the Chromecast @@ -327,41 +357,41 @@ class MediaChromecastPlugin(Plugin): cast.set_volume(volume/100) @action - def volume_up(self, chromecast=None, delta=10): + def volup(self, chromecast=None, step=10): """ - Turn up the Chromecast volume by 10% or delta. + Turn up the Chromecast volume by 10% or step. :param chromecast: Chromecast to cast to. If none is specified, then the default configured Chromecast will be used. :type chromecast: str - :param delta: Volume increment between 0 and 100 (default: 100%) - :type delta: float + :param step: Volume increment between 0 and 100 (default: 100%) + :type step: float """ cast = self.get_chromecast(chromecast) - delta /= 100 - cast.volume_up(min(delta, 1)) + step /= 100 + cast.volume_up(min(step, 1)) @action - def volume_down(self, chromecast=None, delta=10): + def voldown(self, chromecast=None, step=10): """ - Turn down the Chromecast volume by 10% or delta. + Turn down the Chromecast volume by 10% or step. :param chromecast: Chromecast to cast to. If none is specified, then the default configured Chromecast will be used. :type chromecast: str - :param delta: Volume decrement between 0 and 100 (default: 100%) - :type delta: float + :param step: Volume decrement between 0 and 100 (default: 100%) + :type step: float """ cast = self.get_chromecast(chromecast) - delta /= 100 - cast.volume_down(max(delta, 0)) + step /= 100 + cast.volume_down(max(step, 0)) @action - def toggle_mute(self, chromecast=None): + def mute(self, chromecast=None): """ Toggle the mute status on the Chromecast @@ -374,4 +404,3 @@ class MediaChromecastPlugin(Plugin): # vim:sw=4:ts=4:et: - diff --git a/platypush/plugins/media/mplayer.py b/platypush/plugins/media/mplayer.py index 4f480b296..ac81c2c6e 100644 --- a/platypush/plugins/media/mplayer.py +++ b/platypush/plugins/media/mplayer.py @@ -330,14 +330,14 @@ class MediaMplayerPlugin(MediaPlugin): return self._exec('mute') @action - def seek(self, relative_position): + def seek(self, position): """ Seek backward/forward by the specified number of seconds :param relative_position: Number of seconds relative to the current cursor :type relative_position: int """ - return self.step_property('time_pos', offset) + return self.step_property('time_pos', position) @action def set_position(self, position): diff --git a/platypush/plugins/media/webtorrent.py b/platypush/plugins/media/webtorrent.py index 71818b725..ae1335968 100644 --- a/platypush/plugins/media/webtorrent.py +++ b/platypush/plugins/media/webtorrent.py @@ -17,7 +17,7 @@ from platypush.message.event.torrent import TorrentDownloadStartEvent, \ from platypush.plugins import action from platypush.utils import find_bins_in_path, find_files_by_ext, \ - is_process_alive + is_process_alive, get_ip_or_hostname class TorrentState(enum.Enum): @@ -26,6 +26,7 @@ class TorrentState(enum.Enum): DOWNLOADING = 3 DOWNLOADED = 4 + class MediaWebtorrentPlugin(MediaPlugin): """ Plugin to download and stream videos using webtorrent @@ -118,7 +119,7 @@ class MediaWebtorrentPlugin(MediaPlugin): return re.sub('\x1b\[((\d+m)|(.{1,2}))', '', line).strip() - def _process_monitor(self, resource, download_dir): + def _process_monitor(self, resource, download_dir, player_type, player_args): def _thread(): if not self._webtorrent_process: return @@ -162,6 +163,9 @@ class MediaWebtorrentPlugin(MediaPlugin): # Streaming started webtorrent_url = re.search('server running at: (.+?)$', line, flags=re.IGNORECASE).group(1) + webtorrent_url = webtorrent_url.replace( + 'http://localhost', 'http://' + get_ip_or_hostname()) + self.logger.info('Torrent stream started on {}'.format( webtorrent_url)) @@ -215,17 +219,19 @@ class MediaWebtorrentPlugin(MediaPlugin): except FileNotFoundError: continue + player = get_plugin('media.' + player_type) if player_type \ + else self._media_plugin + + media = media_file if player.is_local() else webtorrent_url + self.logger.info( 'Starting playback of {} to {} through {}'.format( - media_file, self._media_plugin.__class__.__name__, + media_file, player.__class__.__name__, webtorrent_url)) - media = media_file if self._media_plugin.is_local() \ - else webtorrent_url - - self._media_plugin.play(media) + player.play(media, **player_args) self.logger.info('Waiting for player to terminate') - self._wait_for_player() + self._wait_for_player(player) self.logger.info('Torrent player terminated') bus.post(TorrentDownloadCompletedEvent(resource=resource)) @@ -235,17 +241,17 @@ class MediaWebtorrentPlugin(MediaPlugin): return _thread - def _wait_for_player(self): - media_cls = self._media_plugin.__class__.__name__ + def _wait_for_player(self, player): + media_cls = player.__class__.__name__ stop_evt = None if media_cls == 'MediaMplayerPlugin': - stop_evt = self._media_plugin._mplayer_stopped_event + stop_evt = player._mplayer_stopped_event elif media_cls == 'MediaOmxplayerPlugin': stop_evt = threading.Event() def stop_callback(): stop_evt.set() - self._media_plugin.add_handler('stop', stop_callback) + player.add_handler('stop', stop_callback) if stop_evt: stop_evt.wait() @@ -264,13 +270,22 @@ class MediaWebtorrentPlugin(MediaPlugin): @action - def play(self, resource): + def play(self, resource, player=None, **player_args): """ Download and stream a torrent :param resource: Play a resource, as a magnet link, torrent URL or torrent file path :type resource: str + + :param player: If set, use this plugin type as a player for the + torrent. Supported types: 'mplayer', 'omxplayer', 'chromecast'. + If not set, then the default configured media plugin will be used. + :type player: str + + :param player_args: Any arguments to pass to the player plugin's + play() method + :type player_args: dict """ if self._webtorrent_process: @@ -291,7 +306,8 @@ class MediaWebtorrentPlugin(MediaPlugin): stdout=subprocess.PIPE) threading.Thread(target=self._process_monitor( - resource=resource, download_dir=download_dir)).start() + resource=resource, download_dir=download_dir, + player_type=player, player_args=player_args)).start() return { 'resource': resource } diff --git a/platypush/utils/__init__.py b/platypush/utils/__init__.py index ad9a43af9..a5df7bfa5 100644 --- a/platypush/utils/__init__.py +++ b/platypush/utils/__init__.py @@ -6,6 +6,7 @@ import inspect import logging import os import signal +import socket import ssl logger = logging.getLogger(__name__) @@ -210,4 +211,9 @@ def is_process_alive(pid): return False +def get_ip_or_hostname(): + ip = socket.gethostbyname(socket.gethostname()) + return socket.getfqdn() if ip.startswith('127.') else ip + + # vim:sw=4:ts=4:et: