forked from platypush/platypush
Implemented support for casting local media through the localstream script
This commit is contained in:
parent
d15b21ddfa
commit
6713ce0f03
3 changed files with 88 additions and 11 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in a new issue