diff --git a/platypush/message/event/media.py b/platypush/message/event/media.py new file mode 100644 index 000000000..85af3e2b7 --- /dev/null +++ b/platypush/message/event/media.py @@ -0,0 +1,57 @@ +from platypush.message.event import Event + + +class MediaEvent(Event): + """ Base class for media events """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + +class MediaPlayEvent(MediaEvent): + """ + Event triggered when a new media content is played + """ + + def __init__(self, resource=None, *args, **kwargs): + """ + :param resource: File name or URI of the played video + :type resource: str + """ + + super().__init__(*args, resource=resource, **kwargs) + + +class MediaStopEvent(MediaEvent): + """ + Event triggered when a media is stopped + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + +class MediaPauseEvent(MediaEvent): + """ + Event triggered when a media playback is paused + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + +class NewPlayingMediaEvent(MediaEvent): + """ + Event triggered when a new media source is being played + """ + + def __init__(self, resource=None, *args, **kwargs): + """ + :param video: File name or URI of the played resource + :type video: str + """ + + super().__init__(*args, resource=resource, **kwargs) + + +# vim:sw=4:ts=4:et: diff --git a/platypush/message/event/video/__init__.py b/platypush/message/event/video/__init__.py index e954dc63e..6a0140d4e 100644 --- a/platypush/message/event/video/__init__.py +++ b/platypush/message/event/video/__init__.py @@ -1,7 +1,8 @@ from platypush.message.event import Event +from platypush.message.event.media import MediaEvent -class VideoEvent(Event): +class VideoEvent(MediaEvent): """ Base class for video events """ def __init__(self, *args, **kwargs): @@ -55,4 +56,3 @@ class NewPlayingVideoEvent(VideoEvent): # vim:sw=4:ts=4:et: - diff --git a/platypush/plugins/media/mplayer.py b/platypush/plugins/media/mplayer.py new file mode 100644 index 000000000..0e15b36ca --- /dev/null +++ b/platypush/plugins/media/mplayer.py @@ -0,0 +1,420 @@ +import os +import select +import subprocess +import time + +from platypush.context import get_bus +from platypush.message.response import Response +from platypush.plugins.media import PlayerState +from platypush.message.event.media import MediaPlayEvent, MediaPauseEvent, \ + MediaStopEvent, NewPlayingMediaEvent + +from platypush.plugins import Plugin, action + + +class MediaMplayerPlugin(Plugin): + """ + 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 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._mplayer = None + self._mplayer_timeout = mplayer_timeout + self._videos_queue = [] + + + def _init_mplayer_bin(self, mplayer_bin=None): + if not mplayer_bin: + bin_name = 'mplayer.exe' if os.name == 'nt' else 'mplayer' + bins = [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))] + + 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._mplayer: + try: + self._mplayer.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) + + self._mplayer = subprocess.Popen(args, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE) + + 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._mplayer: + raise RuntimeError('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._mplayer.stdin.write(cmd) + self._mplayer.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': + bus.post(MediaStopEvent()) + if cmd_name == 'quit': + self._mplayer.wait() + self._mplayer = None + + if not wait_for_response: + return + + poll = select.poll() + poll.register(self._mplayer.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._mplayer.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()) ] + + @action + def play(self, resource, mplayer_args=None): + """ + Play a resource. + + :param resource: Resource to play - can be a local file or a remote URL + :type resource: str + + :param mplayer_args: Extra runtime arguments that will be passed to the + MPlayer executable + :type mplayer_args: list[str] + """ + return self._exec('loadfile', resource, mplayer_args=mplayer_args) + + @action + def pause(self): + """ Toggle the paused state """ + return self._exec('pause') + + @action + def stop(self): + """ Stop the playback """ + return self._exec('stop') + + @action + def quit(self): + """ Quit the player """ + self._exec('quit') + + @action + def voldown(self, step=10.0): + """ Volume down by (default: 10)% """ + volume = self.get_property('volume').output.get('volume') + if volume is None: + self.logger.warning('Unable to read volume property') + return + + new_volume = max(0, volume-step) + return self.set_property('volume', new_volume) + + @action + def volup(self, step=10.0): + """ Volume up by (default: 10)% """ + volume = self.get_property('volume').output.get('volume') + if volume is None: + self.logger.warning('Unable to read volume property') + return + + new_volume = min(100, volume+step) + return self.set_property('volume', new_volume) + + + @action + def back(self, offset=60.0): + """ Back by (default: 60) seconds """ + pos = self.get_property('time_pos').output.get('time_pos') + if pos is None: + self.logger.warning('Unable to read time_pos property') + return + + new_pos = max(0, pos-offset) + return self.set_property('time_pos', new_pos) + + @action + def forward(self, offset=60.0): + """ Forward by (default: 60) seconds """ + pos = self.get_property('time_pos').output.get('time_pos') + if pos is None: + self.logger.warning('Unable to read time_pos property') + return + + length = self.get_property('length').output.get('length') + if length is None: + self.logger.warning('Unable to read length property') + return + + new_pos = min(length, pos+offset) + return self.set_property('time_pos', new_pos) + + + @action + def next(self): + """ Play the next item in the queue """ + if self._mplayer: + self.quit() + + if self._videos_queue: + video = self._videos_queue.pop(0) + return self.play(video) + + @action + def hide_subtitles(self): + """ Hide the subtitles """ + return self._exec('sub_visibility', 1) + + @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): + """ + Load a resource/video in the player. + """ + return self._exec('loadfile', resource) + + @action + def mute(self): + """ Toggle mute state """ + mute = self.get_property('mute').output.get('mute') + if mute is None: + self.logger.warning('Unable to read mute property') + return + + return self._exec('mute', int(not mute)) + + @action + def seek(self, relative_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 + """ + + pos = self.get_property('time_pos').output.get('time_pos') + if pos is None: + self.logger.warning('Unable to read time_pos property') + return + + length = self.get_property('length').output.get('length') + if length is None: + self.logger.warning('Unable to read length property') + return + + new_pos = max(0, min(length, pos+offset)) + return self.set_property('time_pos', new_pos) + + @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 + """ + + length = self.get_property('length').output.get('length') + if length is None: + self.logger.warning('Unable to read length property') + return + + return self.set_property('time_pos', max(0, min(length, position))) + + @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', + 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: