import os import select import subprocess import tempfile 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, MediaPlayRequestEvent, \ MediaPauseEvent, MediaStopEvent, NewPlayingMediaEvent from platypush.plugins import action from platypush.utils import find_bins_in_path class MediaMplayerPlugin(MediaPlugin): """ Plugin to control MPlayer instances Requires: * **mplayer** executable on your system """ _mplayer_default_communicate_timeout = 0.5 _mplayer_bin_default_args = ['-slave', '-quiet', '-idle', '-input', 'nodefault-bindings', '-noconfig', 'all'] _mplayer_properties = [ '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, mplayer_bin=None, mplayer_timeout=_mplayer_default_communicate_timeout, args=None, *argv, **kwargs): """ Create the MPlayer wrapper. Note that the plugin methods are populated dynamically by introspecting the mplayer executable. You can verify the supported methods at runtime by using the `list_actions` method. :param mplayer_bin: Path to the MPlayer executable (default: search for the first occurrence in your system PATH environment variable) :type mplayer_bin: str :param mplayer_timeout: Timeout in seconds to wait for more data from MPlayer before considering a response ready (default: 0.5 seconds) :type mplayer_timeout: float :param subtitles: Path to the subtitles file :type subtitles: str :param args: Default arguments that will be passed to the MPlayer executable :type args: list """ super().__init__(*argv, **kwargs) self.args = args or [] self._init_mplayer_bin() self._build_actions() self._player = None self._mplayer_timeout = mplayer_timeout self._mplayer_stopped_event = threading.Event() self._is_playing_torrent = False def _init_mplayer_bin(self, mplayer_bin=None): if not mplayer_bin: bin_name = 'mplayer.exe' if os.name == 'nt' else 'mplayer' bins = find_bins_in_path(bin_name) if not bins: raise RuntimeError('MPlayer executable not specified and not ' + 'found in your PATH. Make sure that mplayer' + 'is either installed or configured') self.mplayer_bin = bins[0] else: mplayer_bin = os.path.expanduser(mplayer_bin) if not (os.path.isfile(mplayer_bin) and (os.name == 'nt' or os.access(mplayer_bin, os.X_OK))): raise RuntimeError('{} is does not exist or is not a valid ' + 'executable file'.format(mplayer_bin)) self.mplayer_bin = mplayer_bin def _init_mplayer(self, mplayer_args=None): if self._player: try: self._player.quit() except: self.logger.debug('Failed to quit mplayer before _exec: {}'. format(str)) mplayer_args = mplayer_args or [] args = [self.mplayer_bin] + self._mplayer_bin_default_args for arg in self.args + mplayer_args: if arg not in args: args.append(arg) popen_args = { 'stdin': subprocess.PIPE, 'stdout': subprocess.PIPE, } if self._env: popen_args['env'] = self._env self._player = subprocess.Popen(args, **popen_args) threading.Thread(target=self._process_monitor()).start() def _build_actions(self): """ Populates the actions list by introspecting the mplayer executable """ self._actions = {} mplayer = subprocess.Popen([self.mplayer_bin, '-input', 'cmdlist'], stdin=subprocess.PIPE, stdout=subprocess.PIPE) def args_pprint(txt): lc = txt.lower() if lc[0] == '[': return '%s=None'%lc[1:-1] return lc while True: line = mplayer.stdout.readline() if not line: break line = line.decode() if line[0].isupper(): continue args = line.split() cmd_name = args.pop(0) arguments = ', '.join([args_pprint(a) for a in args]) self._actions[cmd_name] = '{}({})'.format(cmd_name, arguments) def _exec(self, cmd, *args, mplayer_args=None, prefix=None, wait_for_response=False): cmd_name = cmd response = None if cmd_name == 'loadfile' or cmd_name == 'loadlist': self._init_mplayer(mplayer_args) else: if not self._player: self.logger.warning('MPlayer is not running') cmd = '{}{}{}{}\n'.format( prefix + ' ' if prefix else '', cmd_name, ' ' if args else '', ' '.join(repr(a) for a in args)).encode() 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._mplayer_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 def execute(self, cmd, args=None): """ Execute a raw MPlayer command. See http://www.mplayerhq.hu/DOCS/tech/slave.txt for a reference or call :method:`platypush.plugins.media.mplayer.list_actions` to get a list """ args = args or [] return self._exec(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._mplayer_stopped_event.clear() self._player.wait() try: self.quit() except: pass get_bus().post(MediaStopEvent()) self._mplayer_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 def play(self, resource, subtitles=None, mplayer_args=None): """ Play a resource. :param resource: Resource to play - can be a local file or a remote URL :type resource: str :param subtitles: Path to optional subtitle file :type subtitles: str :param mplayer_args: Extra runtime arguments that will be passed to the MPlayer executable :type mplayer_args: list[str] """ get_bus().post(MediaPlayRequestEvent(resource=resource)) if subtitles: mplayer_args = mplayer_args or [] mplayer_args += ['-sub', self._get_subtitles_file(subtitles)] resource = self._get_resource(resource) if resource.startswith('file://'): resource = resource[7:] elif resource.startswith('magnet:?'): self._is_playing_torrent = True return get_plugin('media.webtorrent').play(resource) self._is_playing_torrent = False ret = self._exec('loadfile', resource, mplayer_args=mplayer_args) get_bus().post(MediaPlayEvent(resource=resource)) return ret @action def pause(self): """ Toggle the paused state """ ret = self._exec('pause') get_bus().post(MediaPauseEvent()) return ret def _stop_torrent(self): if self._is_playing_torrent: try: get_plugin('media.webtorrent').quit() except: self.logger.warning('Cannot quit the webtorrent instance: {}'. format(str(e))) @action def stop(self): """ Stop the playback """ # return self._exec('stop') return self.quit() @action def quit(self): """ Quit the player """ self._stop_torrent() self._exec('quit') get_bus().post(MediaStopEvent()) @action def voldown(self, step=10.0): """ Volume down by (default: 10)% """ return self._exec('volume', -step*10) @action def volup(self, step=10.0): """ Volume up by (default: 10)% """ return self._exec('volume', step*10) @action 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 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, mplayer_args={}): """ Load a resource/video in the player. """ return self.play(resource, mplayer_args=mplayer_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 def set_volume(self, volume): """ Set the volume :param volume: Volume value between 0 and 100 :type volume: float """ return self._exec('volume', volume) @action def status(self): """ Get the current player state. :returns: A dictionary containing the current state. Example:: output = { "state": "play" # or "stop" or "pause" } """ state = { 'state': PlayerState.STOP.value } try: paused = self.get_property('pause').output.get('pause') if paused is True: state['state'] = PlayerState.PAUSE.value elif paused is False: state['state'] = PlayerState.PLAY.value except: pass finally: return state @action def get_property(self, property, args=None): """ Get a player property (e.g. pause, fullscreen etc.). See http://www.mplayerhq.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.). See http://www.mplayerhq.hu/DOCS/tech/slave.txt for a full list of the available properties """ 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.). See http://www.mplayerhq.hu/DOCS/tech/slave.txt for a full list of the available steppable properties """ 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: