Implemented support for casting local media through the localstream script

This commit is contained in:
Fabio Manganiello 2019-02-06 11:51:44 +01:00
parent d15b21ddfa
commit 6713ce0f03
3 changed files with 88 additions and 11 deletions

View file

@ -2,6 +2,7 @@ import enum
import os import os
import re import re
import subprocess import subprocess
import threading
import urllib.request import urllib.request
import urllib.parse import urllib.parse
@ -9,6 +10,7 @@ import urllib.parse
from platypush.config import Config from platypush.config import Config
from platypush.context import get_plugin from platypush.context import get_plugin
from platypush.plugins import Plugin, action from platypush.plugins import Plugin, action
from platypush.utils import get_ip_or_hostname
class PlayerState(enum.Enum): class PlayerState(enum.Enum):
STOP = 'stop' STOP = 'stop'
@ -25,12 +27,24 @@ class MediaPlugin(Plugin):
* A media player installed (supported so far: mplayer, omxplayer, chromecast) * A media player installed (supported so far: mplayer, omxplayer, chromecast)
* **python-libtorrent** (``pip install python-libtorrent``), optional for Torrent support * **python-libtorrent** (``pip install python-libtorrent``), optional for Torrent support
* **youtube-dl** installed on your system (see your distro instructions), optional for YouTube 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 # A media plugin can either be local or remote (e.g. control media on
# another device) # another device)
_is_local = True _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( _NOT_IMPLEMENTED_ERR = NotImplementedError(
'This method must be implemented in a derived class') 'This method must be implemented in a derived class')
@ -57,7 +71,7 @@ class MediaPlugin(Plugin):
'media.chromecast'} 'media.chromecast'}
def __init__(self, media_dirs=[], download_dir=None, env=None, 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 :param media_dirs: Directories that will be scanned for media files when
a search is performed (default: none) a search is performed (default: none)
@ -70,6 +84,10 @@ class MediaPlugin(Plugin):
:param env: Environment variables key-values to pass to the :param env: Environment variables key-values to pass to the
player executable (e.g. DISPLAY, XDG_VTNR, PULSE_SINK etc.) player executable (e.g. DISPLAY, XDG_VTNR, PULSE_SINK etc.)
:type env: dict :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) super().__init__(*args, **kwargs)
@ -121,6 +139,8 @@ class MediaPlugin(Plugin):
self.media_dirs.add(self.download_dir) self.media_dirs.add(self.download_dir)
self._videos_queue = [] self._videos_queue = []
self._streaming_port = streaming_port
self._streaming_proc = None
def _get_resource(self, resource): def _get_resource(self, resource):
""" """
@ -331,6 +351,50 @@ class MediaPlugin(Plugin):
return self._youtube_search_html_parse(query=query) 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): def _youtube_search_api(self, query):
return [ return [
{ {
@ -376,6 +440,7 @@ class MediaPlugin(Plugin):
return proc.stdout.read().decode("utf-8", "strict")[:-1] return proc.stdout.read().decode("utf-8", "strict")[:-1]
def is_local(self): def is_local(self):
return self._is_local return self._is_local

View file

@ -7,6 +7,7 @@ from pychromecast.controllers.youtube import YouTubeController
from platypush.context import get_plugin from platypush.context import get_plugin
from platypush.plugins import Plugin, action from platypush.plugins import Plugin, action
from platypush.plugins.media import MediaPlugin from platypush.plugins.media import MediaPlugin
from platypush.utils import get_mime_type
class MediaChromecastPlugin(MediaPlugin): class MediaChromecastPlugin(MediaPlugin):
@ -172,29 +173,26 @@ class MediaChromecastPlugin(MediaPlugin):
return hndl.play_video(yt) return hndl.play_video(yt)
resource = self._get_resource(resource) resource = self._get_resource(resource)
if resource.startswith('magnet:?'): if resource.startswith('magnet:?'):
player_args = { 'chromecast': cast } player_args = { 'chromecast': cast }
return get_plugin('media.webtorrent').play(resource, return get_plugin('media.webtorrent').play(resource,
player='chromecast', player='chromecast',
**player_args) **player_args)
# Best effort from the extension if resource.startswith('file://'):
if not content_type: resource = resource[len('file://'):]
for ext in self.video_extensions:
if ('.' + ext).lower() in resource.lower():
content_type = 'video/' + ext
break
if not content_type: if not content_type:
for ext in self.audio_extensions: content_type = get_mime_type(resource)
if ('.' + ext).lower() in resource.lower():
content_type = 'audio/' + ext
break
if not content_type: if not content_type:
raise RuntimeError('content_type required to process media {}'. raise RuntimeError('content_type required to process media {}'.
format(resource)) format(resource))
if os.path.isfile(resource):
resource = self.start_streaming(resource).output['url']
self.logger.info('Playing {} on {}'.format(resource, chromecast)) self.logger.info('Playing {} on {}'.format(resource, chromecast))
mc.play_media(resource, content_type, title=title, thumb=image_url, mc.play_media(resource, content_type, title=title, thumb=image_url,

View file

@ -4,10 +4,12 @@ import hashlib
import importlib import importlib
import inspect import inspect
import logging import logging
import magic
import os import os
import signal import signal
import socket import socket
import ssl import ssl
import urllib.request
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -216,4 +218,16 @@ def get_ip_or_hostname():
return socket.getfqdn() if ip.startswith('127.') else ip 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: # vim:sw=4:ts=4:et: