Added media.webtorrent plugin

This commit is contained in:
Fabio Manganiello 2019-02-03 17:43:30 +01:00
parent 4b7730d4cf
commit a4f80d4622
4 changed files with 288 additions and 12 deletions

View file

@ -124,9 +124,15 @@ class MediaPlugin(Plugin):
or resource.startswith('https://www.youtube.com/watch?v='): or resource.startswith('https://www.youtube.com/watch?v='):
resource = self._get_youtube_content(resource) resource = self._get_youtube_content(resource)
elif resource.startswith('magnet:?'): elif resource.startswith('magnet:?'):
try:
get_plugin('media.webtorrent')
return resource # media.webtorrent will handle this
except:
pass
torrents = get_plugin('torrent') torrents = get_plugin('torrent')
self.logger.info('Downloading torrent {} to {}'.format(resource, self.logger.info('Downloading torrent {} to {}'.format(
download_dir)) resource, self.download_dir))
response = torrents.download(resource, download_dir=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)] resources = [f for f in response.output if self._is_video_file(f)]

View file

@ -1,15 +1,17 @@
import os import os
import select import select
import subprocess import subprocess
import threading
import time import time
from platypush.context import get_bus from platypush.context import get_bus, get_plugin
from platypush.message.response import Response from platypush.message.response import Response
from platypush.plugins.media import PlayerState, MediaPlugin from platypush.plugins.media import PlayerState, MediaPlugin
from platypush.message.event.media import MediaPlayEvent, MediaPauseEvent, \ from platypush.message.event.media import MediaPlayEvent, MediaPauseEvent, \
MediaStopEvent, NewPlayingMediaEvent MediaStopEvent, NewPlayingMediaEvent
from platypush.plugins import action from platypush.plugins import action
from platypush.utils import find_bins_in_path
class MediaMplayerPlugin(MediaPlugin): class MediaMplayerPlugin(MediaPlugin):
@ -77,11 +79,7 @@ class MediaMplayerPlugin(MediaPlugin):
def _init_mplayer_bin(self, mplayer_bin=None): def _init_mplayer_bin(self, mplayer_bin=None):
if not mplayer_bin: if not mplayer_bin:
bin_name = 'mplayer.exe' if os.name == 'nt' else 'mplayer' bin_name = 'mplayer.exe' if os.name == 'nt' else 'mplayer'
bins = [os.path.join(p, bin_name) bins = find_bins_in_path(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))]
if not bins: if not bins:
raise RuntimeError('MPlayer executable not specified and not ' + raise RuntimeError('MPlayer executable not specified and not ' +
@ -121,13 +119,14 @@ class MediaMplayerPlugin(MediaPlugin):
popen_args['env'] = self._env popen_args['env'] = self._env
self._mplayer = subprocess.Popen(args, **popen_args) self._mplayer = subprocess.Popen(args, **popen_args)
threading.Thread(target=self._process_monitor()).start()
def _build_actions(self): def _build_actions(self):
""" Populates the actions list by introspecting the mplayer executable """ """ Populates the actions list by introspecting the mplayer executable """
self._actions = {} self._actions = {}
mplayer = subprocess.Popen([self.mplayer_bin, '-input', 'cmdlist'], mplayer = subprocess.Popen([self.mplayer_bin, '-input', 'cmdlist'],
stdin=subprocess.PIPE, stdout=subprocess.PIPE) stdin=subprocess.PIPE, stdout=subprocess.PIPE)
def args_pprint(txt): def args_pprint(txt):
lc = txt.lower() lc = txt.lower()
@ -222,6 +221,17 @@ class MediaMplayerPlugin(MediaPlugin):
return [ { 'action': action, 'args': self._actions[action] } return [ { 'action': action, 'args': self._actions[action] }
for action in sorted(self._actions.keys()) ] 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 @action
def play(self, resource, mplayer_args=None): def play(self, resource, mplayer_args=None):
""" """
@ -234,9 +244,14 @@ class MediaMplayerPlugin(MediaPlugin):
MPlayer executable MPlayer executable
:type mplayer_args: list[str] :type mplayer_args: list[str]
""" """
resource = self._get_resource(resource) resource = self._get_resource(resource)
if resource.startswith('file://'): if resource.startswith('file://'):
resource = resource[7:] 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) return self._exec('loadfile', resource, mplayer_args=mplayer_args)
@action @action
@ -288,11 +303,11 @@ class MediaMplayerPlugin(MediaPlugin):
return self.get_property('pause').output.get('pause') == False return self.get_property('pause').output.get('pause') == False
@action @action
def load(self, resource): def load(self, resource, mplayer_args={}):
""" """
Load a resource/video in the player. Load a resource/video in the player.
""" """
return self.play('loadfile', resource) return self.play(resource, mplayer_args=mplayer_args)
@action @action
def mute(self): def mute(self):

View file

@ -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:

View file

@ -172,6 +172,12 @@ def set_thread_name(name):
except ImportError: except ImportError:
logger.debug('Unable to set thread name: prctl module is missing') 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: # vim:sw=4:ts=4:et: