From 6713ce0f03b0b5a0487c2303d6347d0098123154 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Wed, 6 Feb 2019 11:51:44 +0100 Subject: [PATCH] Implemented support for casting local media through the localstream script --- platypush/plugins/media/__init__.py | 67 ++++++++++++++++++++++++++- platypush/plugins/media/chromecast.py | 18 ++++--- platypush/utils/__init__.py | 14 ++++++ 3 files changed, 88 insertions(+), 11 deletions(-) diff --git a/platypush/plugins/media/__init__.py b/platypush/plugins/media/__init__.py index 617baca5..fd7895ec 100644 --- a/platypush/plugins/media/__init__.py +++ b/platypush/plugins/media/__init__.py @@ -2,6 +2,7 @@ import enum import os import re import subprocess +import threading import urllib.request import urllib.parse @@ -9,6 +10,7 @@ import urllib.parse from platypush.config import Config from platypush.context import get_plugin from platypush.plugins import Plugin, action +from platypush.utils import get_ip_or_hostname class PlayerState(enum.Enum): STOP = 'stop' @@ -25,12 +27,24 @@ class MediaPlugin(Plugin): * 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 + + To start the local media stream service over HTTP: + + * **nodejs** installed on your system + * **express** module (``npm install express``) + * **mime-types** module (``npm install mime-types``) """ # A media plugin can either be local or remote (e.g. control media on # another device) _is_local = True + # Default port for the local resources HTTP streaming service + _default_streaming_port = 8989 + + _local_stream_bin = os.path.join(os.path.dirname(__file__), 'bin', + 'localstream') + _NOT_IMPLEMENTED_ERR = NotImplementedError( 'This method must be implemented in a derived class') @@ -57,7 +71,7 @@ class MediaPlugin(Plugin): 'media.chromecast'} def __init__(self, media_dirs=[], download_dir=None, env=None, - *args, **kwargs): + streaming_port=_default_streaming_port, *args, **kwargs): """ :param media_dirs: Directories that will be scanned for media files when a search is performed (default: none) @@ -70,6 +84,10 @@ class MediaPlugin(Plugin): :param env: Environment variables key-values to pass to the player executable (e.g. DISPLAY, XDG_VTNR, PULSE_SINK etc.) :type env: dict + + :param streaming_port: Port to be used for streaming local resources + over HTTP (default: 8989) + :type streaming_port: int """ super().__init__(*args, **kwargs) @@ -121,6 +139,8 @@ class MediaPlugin(Plugin): self.media_dirs.add(self.download_dir) self._videos_queue = [] + self._streaming_port = streaming_port + self._streaming_proc = None def _get_resource(self, resource): """ @@ -331,6 +351,50 @@ class MediaPlugin(Plugin): return self._youtube_search_html_parse(query=query) + @action + def start_streaming(self, media): + if self._streaming_proc: + self.logger.info('A streaming process is already running, ' + + 'terminating it first') + self.stop_streaming() + + self._streaming_proc = subprocess.Popen( + [self._local_stream_bin, media, str(self._streaming_port)], + stdin=subprocess.PIPE, stdout=subprocess.PIPE + ) + + threading.Thread(target=self._streaming_process_monitor(media)).start() + url = 'http://{}:{}/video'.format(get_ip_or_hostname(), + self._streaming_port) + + self.logger.info('Starting streaming {} on {}'.format(media, url)) + return { 'url': url } + + @action + def stop_streaming(self): + if not self._streaming_proc: + self.logger.info('No streaming process found') + return + + self._streaming_proc.terminate() + self._streaming_proc.wait() + try: self._streaming_proc.kill() + except: pass + self._streaming_proc = None + + + def _streaming_process_monitor(self, media): + def _thread(): + if not self._streaming_proc: + return + + self._streaming_proc.wait() + try: self.stop_streaming() + except: pass + + return _thread + + def _youtube_search_api(self, query): return [ { @@ -376,6 +440,7 @@ class MediaPlugin(Plugin): return proc.stdout.read().decode("utf-8", "strict")[:-1] + def is_local(self): return self._is_local diff --git a/platypush/plugins/media/chromecast.py b/platypush/plugins/media/chromecast.py index e40fce32..2a9a7db8 100644 --- a/platypush/plugins/media/chromecast.py +++ b/platypush/plugins/media/chromecast.py @@ -7,6 +7,7 @@ from pychromecast.controllers.youtube import YouTubeController from platypush.context import get_plugin from platypush.plugins import Plugin, action from platypush.plugins.media import MediaPlugin +from platypush.utils import get_mime_type class MediaChromecastPlugin(MediaPlugin): @@ -172,29 +173,26 @@ class MediaChromecastPlugin(MediaPlugin): 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 resource.startswith('file://'): + resource = resource[len('file://'):] if not content_type: - for ext in self.audio_extensions: - if ('.' + ext).lower() in resource.lower(): - content_type = 'audio/' + ext - break + content_type = get_mime_type(resource) if not content_type: raise RuntimeError('content_type required to process media {}'. format(resource)) + if os.path.isfile(resource): + resource = self.start_streaming(resource).output['url'] + self.logger.info('Playing {} on {}'.format(resource, chromecast)) mc.play_media(resource, content_type, title=title, thumb=image_url, diff --git a/platypush/utils/__init__.py b/platypush/utils/__init__.py index a5df7bfa..84a23a57 100644 --- a/platypush/utils/__init__.py +++ b/platypush/utils/__init__.py @@ -4,10 +4,12 @@ import hashlib import importlib import inspect import logging +import magic import os import signal import socket import ssl +import urllib.request logger = logging.getLogger(__name__) @@ -216,4 +218,16 @@ def get_ip_or_hostname(): return socket.getfqdn() if ip.startswith('127.') else ip +def get_mime_type(resource): + if resource.startswith('file://'): + resource = resource[len('file://'):] + + if resource.startswith('http://') or resource.startswith('https://'): + with urllib.request.urlopen(resource) as response: + return response.info().get_content_type() + else: + mime = magic.Magic(mime=True) + return mime.from_file(resource) + + # vim:sw=4:ts=4:et: