Added mpv media plugin
This commit is contained in:
parent
a549627516
commit
de0b92f5ef
6 changed files with 303 additions and 365 deletions
|
@ -25,7 +25,7 @@ class MediaPlugin(Plugin):
|
||||||
|
|
||||||
Requires:
|
Requires:
|
||||||
|
|
||||||
* A media player installed (supported so far: mplayer, omxplayer, chromecast)
|
* A media player installed (supported so far: mplayer, mpv, omxplayer, chromecast)
|
||||||
* The :class:`platypush.plugins.media.webtorrent` plugin for optional torrent support through webtorrent (recommented)
|
* The :class:`platypush.plugins.media.webtorrent` plugin for optional torrent support through webtorrent (recommented)
|
||||||
* **python-libtorrent** (``pip install python-libtorrent``), optional, for torrent support through the native Python plugin
|
* **python-libtorrent** (``pip install python-libtorrent``), optional, for torrent support through the native Python plugin
|
||||||
* **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
|
||||||
|
@ -62,7 +62,7 @@ class MediaPlugin(Plugin):
|
||||||
}
|
}
|
||||||
|
|
||||||
_supported_media_plugins = {'media.mplayer', 'media.omxplayer',
|
_supported_media_plugins = {'media.mplayer', 'media.omxplayer',
|
||||||
'media.chromecast'}
|
'media.mpv', 'media.chromecast'}
|
||||||
|
|
||||||
_supported_media_types = ['file', 'torrent', 'youtube']
|
_supported_media_types = ['file', 'torrent', 'youtube']
|
||||||
_default_search_timeout = 60 # 60 seconds
|
_default_search_timeout = 60 # 60 seconds
|
||||||
|
@ -145,8 +145,9 @@ class MediaPlugin(Plugin):
|
||||||
|
|
||||||
if resource.startswith('youtube:') \
|
if resource.startswith('youtube:') \
|
||||||
or resource.startswith('https://www.youtube.com/watch?v='):
|
or resource.startswith('https://www.youtube.com/watch?v='):
|
||||||
if self.__class__.__name__ == 'MediaChromecastPlugin':
|
if self.__class__.__name__ == 'MediaChromecastPlugin' or \
|
||||||
# The Chromecast has already its way to handle YouTube
|
self.__class__.__name__ == 'MediaMpvPlugin':
|
||||||
|
# The Chromecast and mpv have already their way to handle YouTube
|
||||||
return resource
|
return resource
|
||||||
|
|
||||||
resource = self._get_youtube_content(resource)
|
resource = self._get_youtube_content(resource)
|
||||||
|
@ -451,4 +452,23 @@ class MediaPlugin(Plugin):
|
||||||
return self._is_local
|
return self._is_local
|
||||||
|
|
||||||
|
|
||||||
|
def get_subtitles_file(self, subtitles):
|
||||||
|
if not subtitles:
|
||||||
|
return
|
||||||
|
|
||||||
|
if subtitles.startswith('file://'):
|
||||||
|
subtitles = subtitles[len('file://'):]
|
||||||
|
if os.path.isfile(subtitles):
|
||||||
|
return os.path.abspath(subtitles)
|
||||||
|
else:
|
||||||
|
import requests
|
||||||
|
content = requests.get(subtitles).content
|
||||||
|
f = tempfile.NamedTemporaryFile(prefix='media_subs_',
|
||||||
|
suffix='.srt', delete=False)
|
||||||
|
|
||||||
|
with f:
|
||||||
|
f.write(content)
|
||||||
|
return f.name
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -241,24 +241,6 @@ class MediaMplayerPlugin(MediaPlugin):
|
||||||
|
|
||||||
return _thread
|
return _thread
|
||||||
|
|
||||||
def _get_subtitles_file(self, subtitles):
|
|
||||||
if not subtitles:
|
|
||||||
return
|
|
||||||
|
|
||||||
if subtitles.startswith('file://'):
|
|
||||||
subtitles = subtitles[len('file://'):]
|
|
||||||
if os.path.isfile(subtitles):
|
|
||||||
return os.path.abspath(subtitles)
|
|
||||||
else:
|
|
||||||
import requests
|
|
||||||
content = requests.get(subtitles).content
|
|
||||||
f = tempfile.NamedTemporaryFile(prefix='media_subs_',
|
|
||||||
suffix='.srt', delete=False)
|
|
||||||
|
|
||||||
with f:
|
|
||||||
f.write(content)
|
|
||||||
return f.name
|
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def play(self, resource, subtitles=None, mplayer_args=None):
|
def play(self, resource, subtitles=None, mplayer_args=None):
|
||||||
|
@ -279,7 +261,7 @@ class MediaMplayerPlugin(MediaPlugin):
|
||||||
get_bus().post(MediaPlayRequestEvent(resource=resource))
|
get_bus().post(MediaPlayRequestEvent(resource=resource))
|
||||||
if subtitles:
|
if subtitles:
|
||||||
mplayer_args = mplayer_args or []
|
mplayer_args = mplayer_args or []
|
||||||
mplayer_args += ['-sub', self._get_subtitles_file(subtitles)]
|
mplayer_args += ['-sub', self.get_subtitles_file(subtitles)]
|
||||||
|
|
||||||
resource = self._get_resource(resource)
|
resource = self._get_resource(resource)
|
||||||
if resource.startswith('file://'):
|
if resource.startswith('file://'):
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import re
|
||||||
import threading
|
import threading
|
||||||
import time
|
|
||||||
|
|
||||||
from platypush.context import get_bus, get_plugin
|
from platypush.context import get_bus, get_plugin
|
||||||
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, MediaPlayRequestEvent, \
|
from platypush.message.event.media import MediaPlayEvent, MediaPlayRequestEvent, \
|
||||||
MediaPauseEvent, MediaStopEvent, NewPlayingMediaEvent
|
MediaPauseEvent, MediaStopEvent, NewPlayingMediaEvent
|
||||||
|
@ -12,7 +10,6 @@ from platypush.message.event.media import MediaPlayEvent, MediaPlayRequestEvent,
|
||||||
from platypush.plugins import action
|
from platypush.plugins import action
|
||||||
|
|
||||||
|
|
||||||
# XXX WORK IN PROGRESS
|
|
||||||
class MediaMpvPlugin(MediaPlugin):
|
class MediaMpvPlugin(MediaPlugin):
|
||||||
"""
|
"""
|
||||||
Plugin to control MPV instances
|
Plugin to control MPV instances
|
||||||
|
@ -23,208 +20,98 @@ class MediaMpvPlugin(MediaPlugin):
|
||||||
* **mpv** executable on your system
|
* **mpv** executable on your system
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_mpv_default_communicate_timeout = 0.5
|
_default_mpv_args = {
|
||||||
|
'ytdl': True,
|
||||||
|
'start_event_thread': True,
|
||||||
|
}
|
||||||
|
|
||||||
_mpv_properties = [
|
def __init__(self, args=None, *argv, **kwargs):
|
||||||
'osdlevel', 'speed', 'loop', 'pause', 'filename', 'path', 'demuxer',
|
|
||||||
'stream_pos', 'stream_start', 'stream_end', 'stream_length',
|
|
||||||
'stream_time_pos', 'titles', 'chapter', 'chapters', 'angle', 'length',
|
|
||||||
'percent_pos', 'time_pos', 'metadata', 'metadata', 'volume', 'balance',
|
|
||||||
'mute', 'audio_delay', 'audio_format', 'audio_codec', 'audio_bitrate',
|
|
||||||
'samplerate', 'channels', 'switch_audio', 'switch_angle',
|
|
||||||
'switch_title', 'capturing', 'fullscreen', 'deinterlace', 'ontop',
|
|
||||||
'rootwin', 'border', 'framedropping', 'gamma', 'brightness', 'contrast',
|
|
||||||
'saturation', 'hue', 'panscan', 'vsync', 'video_format', 'video_codec',
|
|
||||||
'video_bitrate', 'width', 'height', 'fps', 'aspect', 'switch_video',
|
|
||||||
'switch_program', 'sub', 'sub_source', 'sub_file', 'sub_vob',
|
|
||||||
'sub_demux', 'sub_delay', 'sub_pos', 'sub_alignment', 'sub_visibility',
|
|
||||||
'sub_forced_only', 'sub_scale', 'tv_brightness', 'tv_contrast',
|
|
||||||
'tv_saturation', 'tv_hue', 'teletext_page', 'teletext_subpage',
|
|
||||||
'teletext_mode', 'teletext_format',
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self, mpv_bin=None,
|
|
||||||
mpv_timeout=_mpv_default_communicate_timeout,
|
|
||||||
args=None, *argv, **kwargs):
|
|
||||||
"""
|
"""
|
||||||
Create the MPV wrapper.
|
Create the MPV wrapper.
|
||||||
|
|
||||||
:param mpv_bin: Path to the mpv executable (default: search for
|
|
||||||
the first occurrence in your system PATH environment variable)
|
|
||||||
:type mpv_bin: str
|
|
||||||
|
|
||||||
:param mpv_timeout: Timeout in seconds to wait for more data
|
|
||||||
from MPV before considering a response ready (default: 0.5 seconds)
|
|
||||||
:type mpv_timeout: float
|
|
||||||
|
|
||||||
:param args: Default arguments that will be passed to the mpv executable
|
:param args: Default arguments that will be passed to the mpv executable
|
||||||
:type args: list
|
as a key-value dict (names without the `--` prefix). See `man mpv`
|
||||||
|
for available options.
|
||||||
|
:type args: dict[str, str]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
super().__init__(*argv, **kwargs)
|
super().__init__(*argv, **kwargs)
|
||||||
|
|
||||||
self.args = args or []
|
self.args = self._default_mpv_args
|
||||||
self._init_mpv_bin()
|
if args:
|
||||||
self._build_actions()
|
self.args.update(args)
|
||||||
|
|
||||||
self._player = None
|
self._player = None
|
||||||
self._mpv_timeout = mpv_timeout
|
|
||||||
self._mpv_stopped_event = threading.Event()
|
|
||||||
self._is_playing_torrent = False
|
self._is_playing_torrent = False
|
||||||
|
self._mpv_stopped_event = threading.Event()
|
||||||
|
|
||||||
|
|
||||||
def _init_mpv_bin(self, mpv_bin=None):
|
def _init_mpv(self, args=None):
|
||||||
if not mpv_bin:
|
import mpv
|
||||||
bin_name = 'mpv.exe' if os.name == 'nt' else 'mpv'
|
|
||||||
bins = find_bins_in_path(bin_name)
|
|
||||||
|
|
||||||
if not bins:
|
|
||||||
raise RuntimeError('mpv executable not specified and not ' +
|
|
||||||
'found in your PATH. Make sure that mpv' +
|
|
||||||
'is either installed or configured')
|
|
||||||
|
|
||||||
self.mpv_bin = bins[0]
|
|
||||||
else:
|
|
||||||
mpv_bin = os.path.expanduser(mpv_bin)
|
|
||||||
if not (os.path.isfile(mpv_bin)
|
|
||||||
and (os.name == 'nt' or os.access(mpv_bin, os.X_OK))):
|
|
||||||
raise RuntimeError('{} is does not exist or is not a valid ' +
|
|
||||||
'executable file'.format(mpv_bin))
|
|
||||||
|
|
||||||
self.mpv_bin = mpv_bin
|
|
||||||
|
|
||||||
def _init_mpv(self, mpv_args=None):
|
|
||||||
if self._player:
|
if self._player:
|
||||||
try:
|
try:
|
||||||
self._player.quit()
|
self.quit()
|
||||||
except:
|
except Exception as e:
|
||||||
self.logger.debug('Failed to quit mpv before _exec: {}'.
|
self.logger.debug('Failed to quit mpv before play: {}'.
|
||||||
format(str))
|
format(str(e)))
|
||||||
|
|
||||||
mpv_args = mpv_args or []
|
mpv_args = self.args.copy()
|
||||||
args = [self.mpv_bin] + self._mpv_bin_default_args
|
if args:
|
||||||
for arg in self.args + mpv_args:
|
mpv_args.update(args)
|
||||||
if arg not in args:
|
|
||||||
args.append(arg)
|
|
||||||
|
|
||||||
popen_args = {
|
self._player = mpv.MPV(**mpv_args)
|
||||||
'stdin': subprocess.PIPE,
|
self._player.register_event_callback(self._event_callback())
|
||||||
'stdout': subprocess.PIPE,
|
|
||||||
}
|
|
||||||
|
|
||||||
if self._env:
|
def _event_callback(self):
|
||||||
popen_args['env'] = self._env
|
def callback(event):
|
||||||
|
from mpv import MpvEventID as Event
|
||||||
|
self.logger.debug('Received mpv event: {}'.format(event))
|
||||||
|
|
||||||
self._player = subprocess.Popen(args, **popen_args)
|
evt = event.get('event_id')
|
||||||
threading.Thread(target=self._process_monitor()).start()
|
if not evt:
|
||||||
|
return
|
||||||
|
|
||||||
def _exec(self, cmd, *args, mpv_args=None, prefix=None,
|
bus = get_bus()
|
||||||
wait_for_response=False):
|
if evt == Event.FILE_LOADED:
|
||||||
cmd_name = cmd
|
self._mpv_stopped_event.clear()
|
||||||
response = None
|
bus.post(NewPlayingMediaEvent(resource=self._get_current_resource()))
|
||||||
|
bus.post(MediaPlayEvent(resource=self._get_current_resource()))
|
||||||
|
elif evt == Event.PAUSE:
|
||||||
|
bus.post(MediaPauseEvent(resource=self._get_current_resource()))
|
||||||
|
elif evt == Event.UNPAUSE:
|
||||||
|
bus.post(MediaPlayEvent(resource=self._get_current_resource()))
|
||||||
|
elif evt == Event.END_FILE or evt == Event.SHUTDOWN:
|
||||||
|
if evt == Event.SHUTDOWN:
|
||||||
|
self._player = None
|
||||||
|
self._mpv_stopped_event.set()
|
||||||
|
bus.post(MediaStopEvent())
|
||||||
|
return callback
|
||||||
|
|
||||||
if cmd_name == 'loadfile' or cmd_name == 'loadlist':
|
def _get_youtube_link(self, resource):
|
||||||
self._init_mpv(mpv_args)
|
base_url = 'https://youtu.be/'
|
||||||
else:
|
regexes = ['^https://(www\.)?youtube.com/watch\?v=([^?&#]+)',
|
||||||
if not self._player:
|
'^https://(www\.)?youtu.be.com/([^?&#]+)',
|
||||||
self.logger.warning('mpv is not running')
|
'^(youtube:video):([^?&#]+)']
|
||||||
|
|
||||||
cmd = '{}{}{}{}\n'.format(
|
for regex in regexes:
|
||||||
prefix + ' ' if prefix else '',
|
m = re.search(regex, resource)
|
||||||
cmd_name, ' ' if args else '',
|
if m: return base_url + m.group(2)
|
||||||
' '.join(repr(a) for a in args)).encode()
|
return None
|
||||||
|
|
||||||
self._player.stdin.write(cmd)
|
|
||||||
self._player.stdin.flush()
|
|
||||||
bus = get_bus()
|
|
||||||
|
|
||||||
if cmd_name == 'loadfile' or cmd_name == 'loadlist':
|
|
||||||
bus.post(NewPlayingMediaEvent(resource=args[0]))
|
|
||||||
elif cmd_name == 'pause':
|
|
||||||
bus.post(MediaPauseEvent())
|
|
||||||
elif cmd_name == 'quit' or cmd_name == 'stop':
|
|
||||||
if cmd_name == 'quit':
|
|
||||||
self._player.terminate()
|
|
||||||
self._player.wait()
|
|
||||||
try: self._player.kill()
|
|
||||||
except: pass
|
|
||||||
self._player = None
|
|
||||||
|
|
||||||
if not wait_for_response:
|
|
||||||
return
|
|
||||||
|
|
||||||
poll = select.poll()
|
|
||||||
poll.register(self._player.stdout, select.POLLIN)
|
|
||||||
last_read_time = time.time()
|
|
||||||
|
|
||||||
while time.time() - last_read_time < self._mpv_timeout:
|
|
||||||
result = poll.poll(0)
|
|
||||||
if result:
|
|
||||||
line = self._player.stdout.readline().decode()
|
|
||||||
last_read_time = time.time()
|
|
||||||
|
|
||||||
if line.startswith('ANS_'):
|
|
||||||
k, v = tuple(line[4:].split('='))
|
|
||||||
v = v.strip()
|
|
||||||
if v == 'yes': v = True
|
|
||||||
elif v == 'no': v = False
|
|
||||||
|
|
||||||
try: v = eval(v)
|
|
||||||
except: pass
|
|
||||||
response = { k: v }
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def execute(self, cmd, args=None):
|
def execute(self, cmd, **args):
|
||||||
"""
|
"""
|
||||||
Execute a raw mpv command.
|
Execute a raw mpv command.
|
||||||
"""
|
"""
|
||||||
|
if not self._player:
|
||||||
args = args or []
|
return (None, 'No mpv instance is running')
|
||||||
return self._exec(cmd, *args)
|
return self._player.command(cmd, *args)
|
||||||
|
|
||||||
@action
|
|
||||||
def list_actions(self):
|
|
||||||
return [ { 'action': action, 'args': self._actions[action] }
|
|
||||||
for action in sorted(self._actions.keys()) ]
|
|
||||||
|
|
||||||
def _process_monitor(self):
|
|
||||||
def _thread():
|
|
||||||
if not self._player:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._mpv_stopped_event.clear()
|
|
||||||
self._player.wait()
|
|
||||||
try: self.quit()
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
get_bus().post(MediaStopEvent())
|
|
||||||
self._mpv_stopped_event.set()
|
|
||||||
self._player = None
|
|
||||||
|
|
||||||
return _thread
|
|
||||||
|
|
||||||
def _get_subtitles_file(self, subtitles):
|
|
||||||
if not subtitles:
|
|
||||||
return
|
|
||||||
|
|
||||||
if subtitles.startswith('file://'):
|
|
||||||
subtitles = subtitles[len('file://'):]
|
|
||||||
if os.path.isfile(subtitles):
|
|
||||||
return os.path.abspath(subtitles)
|
|
||||||
else:
|
|
||||||
import requests
|
|
||||||
content = requests.get(subtitles).content
|
|
||||||
f = tempfile.NamedTemporaryFile(prefix='media_subs_',
|
|
||||||
suffix='.srt', delete=False)
|
|
||||||
|
|
||||||
with f:
|
|
||||||
f.write(content)
|
|
||||||
return f.name
|
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def play(self, resource, subtitles=None, mpv_args=None):
|
def play(self, resource, subtitles=None, **args):
|
||||||
"""
|
"""
|
||||||
Play a resource.
|
Play a resource.
|
||||||
|
|
||||||
|
@ -234,15 +121,16 @@ class MediaMpvPlugin(MediaPlugin):
|
||||||
:param subtitles: Path to optional subtitle file
|
:param subtitles: Path to optional subtitle file
|
||||||
:type subtitles: str
|
:type subtitles: str
|
||||||
|
|
||||||
:param mpv_args: Extra runtime arguments that will be passed to the
|
:param args: Extra runtime arguments that will be passed to the
|
||||||
mpv executable
|
mpv executable as a key-value dict (keys without `--` prefix)
|
||||||
:type mpv_args: list[str]
|
:type args: dict[str,str]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
get_bus().post(MediaPlayRequestEvent(resource=resource))
|
get_bus().post(MediaPlayRequestEvent(resource=resource))
|
||||||
|
self._init_mpv(args)
|
||||||
|
|
||||||
if subtitles:
|
if subtitles:
|
||||||
mpv_args = mpv_args or []
|
args['sub_file'] = self.get_subtitles_file(subtitles)
|
||||||
mpv_args += ['-sub', self._get_subtitles_file(subtitles)]
|
|
||||||
|
|
||||||
resource = self._get_resource(resource)
|
resource = self._get_resource(resource)
|
||||||
if resource.startswith('file://'):
|
if resource.startswith('file://'):
|
||||||
|
@ -250,18 +138,24 @@ class MediaMpvPlugin(MediaPlugin):
|
||||||
elif resource.startswith('magnet:?'):
|
elif resource.startswith('magnet:?'):
|
||||||
self._is_playing_torrent = True
|
self._is_playing_torrent = True
|
||||||
return get_plugin('media.webtorrent').play(resource)
|
return get_plugin('media.webtorrent').play(resource)
|
||||||
|
else:
|
||||||
|
yt_resource = self._get_youtube_link(resource)
|
||||||
|
if yt_resource: resource = yt_resource
|
||||||
|
|
||||||
self._is_playing_torrent = False
|
self._is_playing_torrent = False
|
||||||
ret = self._exec('loadfile', resource, mpv_args=mpv_args)
|
ret = self._player.play(resource)
|
||||||
get_bus().post(MediaPlayEvent(resource=resource))
|
return self.status()
|
||||||
return ret
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def pause(self):
|
def pause(self):
|
||||||
""" Toggle the paused state """
|
""" Toggle the paused state """
|
||||||
ret = self._exec('pause')
|
if not self._player:
|
||||||
get_bus().post(MediaPauseEvent())
|
return (None, 'No mpv instance is running')
|
||||||
return ret
|
|
||||||
|
self._player.pause = not self._player.pause
|
||||||
|
return self.status()
|
||||||
|
|
||||||
|
|
||||||
def _stop_torrent(self):
|
def _stop_torrent(self):
|
||||||
if self._is_playing_torrent:
|
if self._is_playing_torrent:
|
||||||
|
@ -272,96 +166,36 @@ class MediaMpvPlugin(MediaPlugin):
|
||||||
format(str(e)))
|
format(str(e)))
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def stop(self):
|
def quit(self):
|
||||||
""" Stop the playback """
|
""" Quit the player (same as `stop`) """
|
||||||
# return self._exec('stop')
|
self._stop_torrent()
|
||||||
return self.quit()
|
if not self._player:
|
||||||
|
return (None, 'No mpv instance is running')
|
||||||
|
|
||||||
|
self._player.quit()
|
||||||
|
self._player = None
|
||||||
|
# self._player.terminate()
|
||||||
|
return { 'state': PlayerState.STOP.value }
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def quit(self):
|
def stop(self):
|
||||||
""" Quit the player """
|
""" Stop the application (same as `quit`) """
|
||||||
self._stop_torrent()
|
return self.quit()
|
||||||
self._exec('quit')
|
|
||||||
get_bus().post(MediaStopEvent())
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def voldown(self, step=10.0):
|
def voldown(self, step=10.0):
|
||||||
""" Volume down by (default: 10)% """
|
""" Volume down by (default: 10)% """
|
||||||
return self._exec('volume', -step*10)
|
if not self._player:
|
||||||
|
return (None, 'No mpv instance is running')
|
||||||
|
return self.set_volume(self._player.volume-step)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def volup(self, step=10.0):
|
def volup(self, step=10.0):
|
||||||
""" Volume up by (default: 10)% """
|
""" Volume up by (default: 10)% """
|
||||||
return self._exec('volume', step*10)
|
if not self._player:
|
||||||
|
return (None, 'No mpv instance is running')
|
||||||
@action
|
return self.set_volume(self._player.volume+step)
|
||||||
def back(self, offset=60.0):
|
|
||||||
""" Back by (default: 60) seconds """
|
|
||||||
return self.step_property('time_pos', -offset)
|
|
||||||
|
|
||||||
@action
|
|
||||||
def forward(self, offset=60.0):
|
|
||||||
""" Forward by (default: 60) seconds """
|
|
||||||
return self.step_property('time_pos', offset)
|
|
||||||
|
|
||||||
@action
|
|
||||||
def toggle_subtitles(self):
|
|
||||||
""" Toggle the subtitles visibility """
|
|
||||||
subs = self.get_property('sub_visibility').output.get('sub_visibility')
|
|
||||||
return self._exec('sub_visibility', int(not subs))
|
|
||||||
|
|
||||||
@action
|
|
||||||
def set_subtitles(self, filename):
|
|
||||||
""" Sets media subtitles from filename """
|
|
||||||
self._exec('sub_visibility', 1)
|
|
||||||
return self._exec('sub_load', filename)
|
|
||||||
|
|
||||||
@action
|
|
||||||
def remove_subtitles(self, index=None):
|
|
||||||
""" Removes the subtitle specified by the index (default: all) """
|
|
||||||
if index is None:
|
|
||||||
return self._exec('sub_remove')
|
|
||||||
else:
|
|
||||||
return self._exec('sub_remove', index)
|
|
||||||
|
|
||||||
@action
|
|
||||||
def is_playing(self):
|
|
||||||
"""
|
|
||||||
:returns: True if it's playing, False otherwise
|
|
||||||
"""
|
|
||||||
return self.get_property('pause').output.get('pause') == False
|
|
||||||
|
|
||||||
@action
|
|
||||||
def load(self, resource, mpv_args={}):
|
|
||||||
"""
|
|
||||||
Load a resource/video in the player.
|
|
||||||
"""
|
|
||||||
return self.play(resource, mpv_args=mpv_args)
|
|
||||||
|
|
||||||
@action
|
|
||||||
def mute(self):
|
|
||||||
""" Toggle mute state """
|
|
||||||
return self._exec('mute')
|
|
||||||
|
|
||||||
@action
|
|
||||||
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', position)
|
|
||||||
|
|
||||||
@action
|
|
||||||
def set_position(self, position):
|
|
||||||
"""
|
|
||||||
Seek backward/forward to the specified absolute position
|
|
||||||
|
|
||||||
:param position: Number of seconds from the start
|
|
||||||
:type position: int
|
|
||||||
"""
|
|
||||||
return self.set_property('time_pos', position)
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def set_volume(self, volume):
|
def set_volume(self, volume):
|
||||||
|
@ -371,7 +205,164 @@ class MediaMpvPlugin(MediaPlugin):
|
||||||
:param volume: Volume value between 0 and 100
|
:param volume: Volume value between 0 and 100
|
||||||
:type volume: float
|
:type volume: float
|
||||||
"""
|
"""
|
||||||
return self._exec('volume', volume)
|
if not self._player:
|
||||||
|
return (None, 'No mpv instance is running')
|
||||||
|
|
||||||
|
volume = max(0, min(self._player.volume_max, volume))
|
||||||
|
self._player.volume = volume
|
||||||
|
return { 'volume': volume }
|
||||||
|
|
||||||
|
@action
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
if not self._player:
|
||||||
|
return (None, 'No mpv instance is running')
|
||||||
|
if not self._player.seekable:
|
||||||
|
return (None, 'The resource is not seekable')
|
||||||
|
pos = min(self._player.time_pos+self._player.time_remaining,
|
||||||
|
max(0, position))
|
||||||
|
self._player.time_pos = pos
|
||||||
|
return { 'position': pos }
|
||||||
|
|
||||||
|
@action
|
||||||
|
def back(self, offset=60.0):
|
||||||
|
""" Back by (default: 60) seconds """
|
||||||
|
if not self._player:
|
||||||
|
return (None, 'No mpv instance is running')
|
||||||
|
if not self._player.seekable:
|
||||||
|
return (None, 'The resource is not seekable')
|
||||||
|
pos = max(0, self._player.time_pos-offset)
|
||||||
|
return self.seek(pos)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def forward(self, offset=60.0):
|
||||||
|
""" Forward by (default: 60) seconds """
|
||||||
|
if not self._player:
|
||||||
|
return (None, 'No mpv instance is running')
|
||||||
|
if not self._player.seekable:
|
||||||
|
return (None, 'The resource is not seekable')
|
||||||
|
pos = min(self._player.time_pos+self._player.time_remaining,
|
||||||
|
self._player.time_pos+offset)
|
||||||
|
return self.seek(pos)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def next(self):
|
||||||
|
""" Play the next item in the queue """
|
||||||
|
if not self._player:
|
||||||
|
return (None, 'No mpv instance is running')
|
||||||
|
self._player.playlist_next()
|
||||||
|
|
||||||
|
@action
|
||||||
|
def prev(self):
|
||||||
|
""" Play the previous item in the queue """
|
||||||
|
if not self._player:
|
||||||
|
return (None, 'No mpv instance is running')
|
||||||
|
self._player.playlist_prev()
|
||||||
|
|
||||||
|
@action
|
||||||
|
def toggle_subtitles(self, visibile=None):
|
||||||
|
""" Toggle the subtitles visibility """
|
||||||
|
return self.toggle_property('sub_visibility')
|
||||||
|
|
||||||
|
@action
|
||||||
|
def toggle_fullscreen(self, fullscreen=None):
|
||||||
|
""" Toggle the fullscreen mode """
|
||||||
|
return self.toggle_property('fullscreen')
|
||||||
|
|
||||||
|
@action
|
||||||
|
def toggle_property(self, property):
|
||||||
|
"""
|
||||||
|
Toggle or sets the value of an mpv property (e.g. fullscreen,
|
||||||
|
sub_visibility etc.). See ``man mpv`` for a full list of properties
|
||||||
|
|
||||||
|
:param property: Property to toggle
|
||||||
|
"""
|
||||||
|
if not self._player:
|
||||||
|
return (None, 'No mpv instance is running')
|
||||||
|
|
||||||
|
if not hasattr(self._player, property):
|
||||||
|
self.logger.warning('No such mpv property: {}'.format(property))
|
||||||
|
|
||||||
|
value = not getattr(self._player, property)
|
||||||
|
setattr(self._player, property, value)
|
||||||
|
return { property: value }
|
||||||
|
|
||||||
|
@action
|
||||||
|
def get_property(self, property):
|
||||||
|
"""
|
||||||
|
Get a player property (e.g. pause, fullscreen etc.). See
|
||||||
|
``man mpv`` for a full list of the available properties
|
||||||
|
"""
|
||||||
|
if not self._player:
|
||||||
|
return (None, 'No mpv instance is running')
|
||||||
|
return getattr(self._player, property)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def set_property(self, **props):
|
||||||
|
"""
|
||||||
|
Set the value of an mpv property (e.g. fullscreen, sub_visibility
|
||||||
|
etc.). See ``man mpv`` for a full list of properties
|
||||||
|
|
||||||
|
:param props: Key-value args for the properties to set
|
||||||
|
:type props: dict
|
||||||
|
"""
|
||||||
|
if not self._player:
|
||||||
|
return (None, 'No mpv instance is running')
|
||||||
|
|
||||||
|
for k,v in props:
|
||||||
|
setattr(self._player, k, v)
|
||||||
|
return props
|
||||||
|
|
||||||
|
@action
|
||||||
|
def set_subtitles(self, filename):
|
||||||
|
""" Sets media subtitles from filename """
|
||||||
|
return self.set_property(subfile=filename, sub_visibility=True)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def remove_subtitles(self):
|
||||||
|
""" Removes (hides) the subtitles """
|
||||||
|
if not self._player:
|
||||||
|
return (None, 'No mpv instance is running')
|
||||||
|
self._player.sub_visibility = False
|
||||||
|
|
||||||
|
@action
|
||||||
|
def is_playing(self):
|
||||||
|
"""
|
||||||
|
:returns: True if it's playing, False otherwise
|
||||||
|
"""
|
||||||
|
if not self._player:
|
||||||
|
return False
|
||||||
|
return not self._player.pause
|
||||||
|
|
||||||
|
@action
|
||||||
|
def load(self, resource, **args):
|
||||||
|
"""
|
||||||
|
Load/queue a resource/video to the player
|
||||||
|
"""
|
||||||
|
if not self._player:
|
||||||
|
return self.play(resource, **args)
|
||||||
|
return self.loadfile(resource, mode='append-play', **args)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def mute(self):
|
||||||
|
""" Toggle mute state """
|
||||||
|
if not self._player:
|
||||||
|
return (None, 'No mpv instance is running')
|
||||||
|
mute = not self._player.mute
|
||||||
|
self._player.mute = mute
|
||||||
|
return { 'muted': mute }
|
||||||
|
|
||||||
|
@action
|
||||||
|
def set_position(self, position):
|
||||||
|
"""
|
||||||
|
Seek backward/forward to the specified absolute position (same as ``seek``)
|
||||||
|
"""
|
||||||
|
return self.seek(position)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def status(self):
|
def status(self):
|
||||||
|
@ -383,88 +374,26 @@ class MediaMpvPlugin(MediaPlugin):
|
||||||
Example::
|
Example::
|
||||||
|
|
||||||
output = {
|
output = {
|
||||||
|
"filename": "filename or stream URL",
|
||||||
"state": "play" # or "stop" or "pause"
|
"state": "play" # or "stop" or "pause"
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
if not self._player or not hasattr(self._player, 'pause'):
|
||||||
|
return { 'state': PlayerState.STOP.value }
|
||||||
|
|
||||||
state = { 'state': PlayerState.STOP.value }
|
return {
|
||||||
|
'filename': self._get_current_resource(),
|
||||||
|
'state': (PlayerState.PAUSE.value if self._player.pause else
|
||||||
|
PlayerState.PLAY.value),
|
||||||
|
}
|
||||||
|
|
||||||
try:
|
def _get_current_resource(self):
|
||||||
paused = self.get_property('pause').output.get('pause')
|
if not self._player or not self._player.stream_path:
|
||||||
if paused is True:
|
return
|
||||||
state['state'] = PlayerState.PAUSE.value
|
|
||||||
elif paused is False:
|
|
||||||
state['state'] = PlayerState.PLAY.value
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
return state
|
|
||||||
|
|
||||||
@action
|
return ('file://' if os.path.isfile(self._player.stream_path)
|
||||||
def get_property(self, property, args=None):
|
else '') + self._player.stream_path
|
||||||
"""
|
|
||||||
Get a player property (e.g. pause, fullscreen etc.). See
|
|
||||||
http://www.mpvhq.hu/DOCS/tech/slave.txt for a full list of the
|
|
||||||
available properties
|
|
||||||
"""
|
|
||||||
|
|
||||||
args = args or []
|
|
||||||
response = Response(output={})
|
|
||||||
|
|
||||||
result = self._exec('get_property', property, prefix='pausing_keep_force',
|
|
||||||
wait_for_response=True, *args) or {}
|
|
||||||
|
|
||||||
for k, v in result.items():
|
|
||||||
if k == 'ERROR' and v not in response.errors:
|
|
||||||
response.errors.append('{}{}: {}'.format(property, args, v))
|
|
||||||
else:
|
|
||||||
response.output[k] = v
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
@action
|
|
||||||
def set_property(self, property, value, args=None):
|
|
||||||
"""
|
|
||||||
Set a player property (e.g. pause, fullscreen etc.).
|
|
||||||
"""
|
|
||||||
|
|
||||||
args = args or []
|
|
||||||
response = Response(output={})
|
|
||||||
|
|
||||||
result = self._exec('set_property', property, value,
|
|
||||||
prefix='pausing_keep_force' if property != 'pause'
|
|
||||||
else None, wait_for_response=True, *args) or {}
|
|
||||||
|
|
||||||
for k, v in result.items():
|
|
||||||
if k == 'ERROR' and v not in response.errors:
|
|
||||||
response.errors.append('{} {}{}: {}'.format(property, value,
|
|
||||||
args, v))
|
|
||||||
else:
|
|
||||||
response.output[k] = v
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
@action
|
|
||||||
def step_property(self, property, value, args=None):
|
|
||||||
"""
|
|
||||||
Step a player property (e.g. volume, time_pos etc.).
|
|
||||||
"""
|
|
||||||
|
|
||||||
args = args or []
|
|
||||||
response = Response(output={})
|
|
||||||
|
|
||||||
result = self._exec('step_property', property, value,
|
|
||||||
prefix='pausing_keep_force',
|
|
||||||
wait_for_response=True, *args) or {}
|
|
||||||
|
|
||||||
for k, v in result.items():
|
|
||||||
if k == 'ERROR' and v not in response.errors:
|
|
||||||
response.errors.append('{} {}{}: {}'.format(property, value,
|
|
||||||
args, v))
|
|
||||||
else:
|
|
||||||
response.output[k] = v
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -39,7 +39,8 @@ class MediaWebtorrentPlugin(MediaPlugin):
|
||||||
or media.omxplayer)
|
or media.omxplayer)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_supported_media_plugins = { 'media.mplayer', 'media.omxplayer' }
|
_supported_media_plugins = {'media.mplayer', 'media.omxplayer',
|
||||||
|
'media.webtorrent'}
|
||||||
|
|
||||||
# Download at least 10 MBs before starting streaming
|
# Download at least 10 MBs before starting streaming
|
||||||
_download_size_before_streaming = 10 * 2**20
|
_download_size_before_streaming = 10 * 2**20
|
||||||
|
@ -258,6 +259,8 @@ class MediaWebtorrentPlugin(MediaPlugin):
|
||||||
|
|
||||||
if media_cls == 'MediaMplayerPlugin':
|
if media_cls == 'MediaMplayerPlugin':
|
||||||
stop_evt = player._mplayer_stopped_event
|
stop_evt = player._mplayer_stopped_event
|
||||||
|
elif media_cls == 'MediaMpvPlugin':
|
||||||
|
stop_evt = player._mpv_stopped_event
|
||||||
elif media_cls == 'MediaOmxplayerPlugin':
|
elif media_cls == 'MediaOmxplayerPlugin':
|
||||||
stop_evt = threading.Event()
|
stop_evt = threading.Event()
|
||||||
def stop_callback():
|
def stop_callback():
|
||||||
|
|
|
@ -131,3 +131,6 @@ inputs
|
||||||
# Mopidy backend
|
# Mopidy backend
|
||||||
websocket-client
|
websocket-client
|
||||||
|
|
||||||
|
# mpv player plugin
|
||||||
|
python-mpv
|
||||||
|
|
||||||
|
|
1
setup.py
1
setup.py
|
@ -97,6 +97,7 @@ setup(
|
||||||
'Support for sound devices': ['sounddevice', 'soundfile', 'numpy'],
|
'Support for sound devices': ['sounddevice', 'soundfile', 'numpy'],
|
||||||
'Support for web media subtitles': ['webvtt-py'],
|
'Support for web media subtitles': ['webvtt-py'],
|
||||||
'Support for mopidy backend': ['websocket-client'],
|
'Support for mopidy backend': ['websocket-client'],
|
||||||
|
'Support for mpv player plugin': ['python-mpv'],
|
||||||
# 'Support for Leap Motion backend': ['git+ssh://git@github.com:BlackLight/leap-sdk-python3.git'],
|
# 'Support for Leap Motion backend': ['git+ssh://git@github.com:BlackLight/leap-sdk-python3.git'],
|
||||||
# 'Support for Flic buttons': ['git+https://@github.com/50ButtonsEach/fliclib-linux-hci.git']
|
# 'Support for Flic buttons': ['git+https://@github.com/50ButtonsEach/fliclib-linux-hci.git']
|
||||||
# 'Support for media subtitles': ['git+https://github.com/agonzalezro/python-opensubtitles#egg=python-opensubtitles']
|
# 'Support for media subtitles': ['git+https://github.com/agonzalezro/python-opensubtitles#egg=python-opensubtitles']
|
||||||
|
|
Loading…
Reference in a new issue