import os import urllib.parse from platypush.context import get_bus, get_plugin from platypush.plugins.media import PlayerState, MediaPlugin from platypush.message.event.media import MediaPlayEvent, MediaPlayRequestEvent, \ MediaPauseEvent, MediaStopEvent, MediaSeekEvent, MediaVolumeChangedEvent, \ MediaMuteChangedEvent, NewPlayingMediaEvent from platypush.plugins import action class MediaVlcPlugin(MediaPlugin): """ Plugin to control vlc instances Requires: * **python-vlc** (``pip install python-vlc``) * **vlc** executable on your system """ def __init__(self, args=None, fullscreen=False, volume=100, *argv, **kwargs): """ Create the vlc wrapper. :param args: List of extra arguments to pass to the VLC executable (e.g. ``['--sub-language=en', '--snapshot-path=/mnt/snapshots']``) :type args: list[str] :param fullscreen: Set to True if you want media files to be opened in fullscreen by default (can be overridden by `.play()`) (default: False) :type fullscreen: bool :param volume: Default media volume (default: 100) :type volume: int """ super().__init__(*argv, **kwargs) self._args = args or [] self._instance = None self._player = None self._latest_seek = None self._default_fullscreen = fullscreen self._default_volume = volume self._on_stop_callbacks = [] self._title = None self._filename = None @classmethod def _watched_event_types(cls): import vlc return [getattr(vlc.EventType, evt) for evt in [ 'MediaPlayerLengthChanged', 'MediaPlayerMediaChanged', 'MediaDurationChanged', 'MediaPlayerMuted', 'MediaPlayerUnmuted', 'MediaPlayerOpening', 'MediaPlayerPaused', 'MediaPlayerPlaying', 'MediaPlayerPositionChanged', 'MediaPlayerStopped', 'MediaPlayerTimeChanged', 'MediaStateChanged', 'MediaPlayerForward', 'MediaPlayerBackward', 'MediaPlayerEndReached', 'MediaPlayerTitleChanged', 'MediaPlayerAudioVolume', ] if hasattr(vlc.EventType, evt)] def _init_vlc(self, resource): import vlc self._reset_state() for k, v in self._env.items(): os.environ[k] = v self._instance = vlc.Instance(*self._args) self._player = self._instance.media_player_new(resource) for evt in self._watched_event_types(): self._player.event_manager().event_attach( eventtype=evt, callback=self._event_callback()) def _reset_state(self): self._latest_seek = None self._title = None self._filename = None if self._player: self._player.release() self._player = None if self._instance: self._instance.release() self._instance = None @staticmethod def _post_event(evt_type, **evt): bus = get_bus() bus.post(evt_type(player='local', plugin='media.vlc', **evt)) def _event_callback(self): def callback(event): from vlc import EventType self.logger.debug('Received vlc event: {}'.format(event)) if event.type == EventType.MediaPlayerPlaying: self._post_event(MediaPlayEvent, resource=self._get_current_resource()) elif event.type == EventType.MediaPlayerPaused: self._post_event(MediaPauseEvent) elif event.type == EventType.MediaPlayerStopped or \ event.type == EventType.MediaPlayerEndReached: self._reset_state() self._post_event(MediaStopEvent) for cbk in self._on_stop_callbacks: cbk() elif event.type == EventType.MediaPlayerTitleChanged: self._filename = event.u.filename self._title = event.u.new_title self._post_event(NewPlayingMediaEvent, resource=event.u.new_title) elif event.type == EventType.MediaPlayerMediaChanged: self._filename = event.u.filename self._title = event.u.new_title self._post_event(NewPlayingMediaEvent, resource=event.u.filename) elif event.type == EventType.MediaPlayerLengthChanged: self._post_event(NewPlayingMediaEvent, resource=self._get_current_resource()) elif event.type == EventType.MediaPlayerTimeChanged: pos = float(event.u.new_time/1000) if self._latest_seek is None or \ abs(pos-self._latest_seek) > 5: self._post_event(MediaSeekEvent, position=pos) self._latest_seek = pos elif event.type == EventType.MediaPlayerAudioVolume: self._post_event(MediaVolumeChangedEvent, volume=self._player.audio_get_volume()) elif event.type == EventType.MediaPlayerMuted: self._post_event(MediaMuteChangedEvent, mute=True) elif event.type == EventType.MediaPlayerUnmuted: self._post_event(MediaMuteChangedEvent, mute=False) return callback @action def play(self, resource, subtitles=None, fullscreen=None, volume=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 fullscreen: Set to explicitly enable/disable fullscreen (default: `fullscreen` configured value or False) :type fullscreen: bool :param volume: Set to explicitly set the playback volume (default: `volume` configured value or 100) :type fullscreen: bool """ get_bus().post(MediaPlayRequestEvent(resource=resource)) resource = self._get_resource(resource) if resource.startswith('file://'): resource = resource[len('file://'):] self._init_vlc(resource) if subtitles: if subtitles.startswith('file://'): subtitles = subtitles[len('file://'):] self._player.video_set_subtitle_file(subtitles) self._player.play() if self.volume: self.set_volume(volume=self.volume) if fullscreen or self._default_fullscreen: self.set_fullscreen(True) if volume is not None or self._default_volume is not None: self.set_volume(volume if volume is not None else self._default_volume) return self.status() @action def pause(self): """ Toggle the paused state """ if not self._player: return None, 'No vlc instance is running' if not self._player.can_pause(): return None, 'The specified media type cannot be paused' self._player.pause() return self.status() @action def quit(self): """ Quit the player (same as `stop`) """ self._stop_torrent() if not self._player: return None, 'No vlc instance is running' self._player.stop() self._player = None return self.status() @action def stop(self): """ Stop the application (same as `quit`) """ return self.quit() @action def voldown(self, step=10.0): """ Volume down by (default: 10)% """ if not self._player: return None, 'No vlc instance is running' return self.set_volume(self._player.audio_get_volume()-step) @action def volup(self, step=10.0): """ Volume up by (default: 10)% """ if not self._player: return None, 'No vlc instance is running' return self.set_volume(self._player.audio_get_volume()+step) @action def set_volume(self, volume): """ Set the volume :param volume: Volume value between 0 and 100 :type volume: float """ if not self._player: return None, 'No vlc instance is running' volume = max(0, min([100, volume])) self._player.audio_set_volume(volume) status = self.status().output status['volume'] = volume return status @action def seek(self, position): """ Seek backward/forward by the specified number of seconds :param position: Number of seconds relative to the current cursor :type position: int """ if not self._player: return None, 'No vlc instance is running' if not self._player.is_seekable(): return None, 'The resource is not seekable' media = self._player.get_media() if not media: return None, 'No media loaded' pos = min(media.get_duration()/1000, max(0, position)) self._player.set_time(int(pos*1000)) return self.status() @action def back(self, offset=60.0): """ Back by (default: 60) seconds """ if not self._player: return None, 'No vlc instance is running' media = self._player.get_media() if not media: return None, 'No media loaded' pos = max(0, (self._player.get_time()/1000)-offset) return self.seek(pos) @action def forward(self, offset=60.0): """ Forward by (default: 60) seconds """ if not self._player: return None, 'No vlc instance is running' media = self._player.get_media() if not media: return None, 'No media loaded' pos = min(media.get_duration()/1000, (self._player.get_time()/1000)+offset) return self.seek(pos) @action def toggle_subtitles(self, visibile=None): """ Toggle the subtitles visibility """ if not self._player: return None, 'No vlc instance is running' if self._player.video_get_spu_count() == 0: return None, 'The media file has no subtitles set' if self._player.video_get_spu() is None or \ self._player.video_get_spu() == -1: self._player.video_set_spu(0) else: self._player.video_set_spu(-1) @action def toggle_fullscreen(self): """ Toggle the fullscreen mode """ if not self._player: return None, 'No vlc instance is running' self._player.toggle_fullscreen() @action def set_fullscreen(self, fullscreen=True): """ Set fullscreen mode """ if not self._player: return None, 'No vlc instance is running' self._player.set_fullscreen(fullscreen) @action def set_subtitles(self, filename, **args): """ Sets media subtitles from filename """ if not self._player: return None, 'No vlc instance is running' if filename.startswith('file://'): filename = filename[len('file://'):] self._player.video_set_subtitle_file(filename) @action def remove_subtitles(self): """ Removes (hides) the subtitles """ if not self._player: return None, 'No vlc instance is running' self._player.video_set_spu(-1) @action def is_playing(self): """ :returns: True if it's playing, False otherwise """ if not self._player: return False return self._player.is_playing() @action def load(self, resource, **args): """ Load/queue a resource/video to the player """ if not self._player: return self.play(resource, **args) self._player.set_media(resource) return self.status() @action def mute(self): """ Toggle mute state """ if not self._player: return None, 'No vlc instance is running' self._player.audio_toggle_mute() @action def set_position(self, position): """ Seek backward/forward to the specified absolute position (same as ``seek``) """ return self.seek(position) @action def status(self): """ Get the current player state. :returns: A dictionary containing the current state. Example:: output = { "filename": "filename or stream URL", "state": "play" # or "stop" or "pause" } """ import vlc if not self._player: return {'state': PlayerState.STOP.value} status = {} vlc_state = self._player.get_state() if vlc_state == vlc.State.Playing: status['state'] = PlayerState.PLAY.value elif vlc_state == vlc.State.Paused: status['state'] = PlayerState.PAUSE.value else: status['state'] = PlayerState.STOP.value status['url'] = urllib.parse.unquote(self._player.get_media().get_mrl()) if self._player.get_media() else None status['position'] = float(self._player.get_time()/1000) if self._player.get_time() is not None else None media = self._player.get_media() status['duration'] = media.get_duration()/1000 if media and media.get_duration() is not None else None status['seekable'] = status['duration'] is not None status['fullscreen'] = self._player.get_fullscreen() status['mute'] = self._player.audio_get_mute() status['path'] = status['url'] status['pause'] = status['state'] == PlayerState.PAUSE.value status['percent_pos'] = self._player.get_position()*100 status['filename'] = self._filename status['title'] = self._title status['volume'] = self._player.audio_get_volume() status['volume_max'] = 100 return status def on_stop(self, callback): self._on_stop_callbacks.append(callback) def _get_current_resource(self): if not self._player or not self._player.get_media(): return return self._player.get_media().get_mrl() # vim:sw=4:ts=4:et: