diff --git a/ovos_plugin_manager/templates/audio.py b/ovos_plugin_manager/templates/audio.py index 3bd0d2b1..1f9022ca 100644 --- a/ovos_plugin_manager/templates/audio.py +++ b/ovos_plugin_manager/templates/audio.py @@ -1,63 +1,84 @@ -"""Definition of the audio service backends base classes. - -These classes can be used to create an Audioservice plugin extending -OpenVoiceOS's media playback options. -""" from abc import ABCMeta, abstractmethod -from ovos_utils import classproperty +from ovos_bus_client.message import Message +from ovos_utils.log import LOG from ovos_utils.messagebus import FakeBus -from ovos_utils.process_utils import RuntimeRequirements +from ocp_nlp.constants import MediaState, PlayerState, TrackState -class AudioBackend(metaclass=ABCMeta): - """Base class for all audio backend implementations. - Arguments: - config (dict): configuration dict for the instance - bus (MessageBusClient): OpenVoiceOS messagebus emitter - """ +class MediaBackend(metaclass=ABCMeta): + """Base class for all OCP media backend implementations. + + Media backends are single-track, playlists are handled by OCP + + Arguments: + config (dict): configuration dict for the instance + bus (MessageBusClient): Mycroft messagebus emitter + """ def __init__(self, config=None, bus=None): + self._now_playing = None # single uri self._track_start_callback = None self.supports_mime_hints = False self.config = config or {} self.bus = bus or FakeBus() - @classproperty - def runtime_requirements(self): - """ skill developers should override this if they do not require connectivity - some examples: - IOT plugin that controls devices via LAN could return: - scans_on_init = True - RuntimeRequirements(internet_before_load=False, - network_before_load=scans_on_init, - requires_internet=False, - requires_network=True, - no_internet_fallback=True, - no_network_fallback=False) - online search plugin with a local cache: - has_cache = False - RuntimeRequirements(internet_before_load=not has_cache, - network_before_load=not has_cache, - requires_internet=True, - requires_network=True, - no_internet_fallback=True, - no_network_fallback=True) - a fully offline plugin: - RuntimeRequirements(internet_before_load=False, - network_before_load=False, - requires_internet=False, - requires_network=False, - no_internet_fallback=True, - no_network_fallback=True) - """ - return RuntimeRequirements(internet_before_load=False, - network_before_load=False, - requires_internet=False, - requires_network=False, - no_internet_fallback=True, - no_network_fallback=True) + def set_track_start_callback(self, callback_func): + """Register callback on track start. + + This method should be called as each track in a playlist is started. + """ + self._track_start_callback = callback_func + + def load_track(self, uri): + self._now_playing = uri + LOG.debug(f"queuing for {self.__class__.__name__} playback: {uri}") + self.bus.emit(Message("ovos.common_play.media.state", + {"state": MediaState.LOADED_MEDIA})) + + def ocp_start(self): + """Emit OCP status events for play""" + self.bus.emit(Message("ovos.common_play.player.state", + {"state": PlayerState.PLAYING})) + self.bus.emit(Message("ovos.common_play.media.state", + {"state": MediaState.LOADED_MEDIA})) + self.play() + + def ocp_error(self): + """Emit OCP status events for playback error""" + if self._now_playing: + self._now_playing = None + self.bus.emit(Message("ovos.common_play.media.state", + {"state": MediaState.INVALID_MEDIA})) + self.bus.emit(Message("ovos.common_play.player.state", + {"state": PlayerState.STOPPED})) + + def ocp_stop(self): + """Emit OCP status events for stop""" + if self._now_playing: + self._now_playing = None + self.bus.emit(Message("ovos.common_play.player.state", + {"state": PlayerState.STOPPED})) + self.bus.emit(Message("ovos.common_play.media.state", + {"state": MediaState.END_OF_MEDIA})) + self.stop() + + def ocp_pause(self): + """Emit OCP status events for pause""" + if self._now_playing: + self.bus.emit(Message("ovos.common_play.player.state", + {"state": PlayerState.PAUSED})) + self.pause() + + def ocp_resume(self): + """Emit OCP status events for resume""" + if self._now_playing: + self.bus.emit(Message("ovos.common_play.player.state", + {"state": PlayerState.PLAYING})) + self.bus.emit(Message("ovos.common_play.track.state", + {"state": TrackState.PLAYING_AUDIO})) + self.resume() @property def playback_time(self): @@ -72,26 +93,11 @@ def supported_uris(self): """ @abstractmethod - def clear_list(self): - """Clear playlist.""" - - @abstractmethod - def add_list(self, tracks): - """Add tracks to backend's playlist. - - Arguments: - tracks (list): list of tracks. - """ - - @abstractmethod - def play(self, repeat=False): + def play(self): """Start playback. Starts playing the first track in the playlist and will contiune until all tracks have been played. - - Arguments: - repeat (bool): Repeat playlist, defaults to False """ @abstractmethod @@ -104,13 +110,7 @@ def stop(self): bool: True if playback was stopped, otherwise False """ - def set_track_start_callback(self, callback_func): - """Register callback on track start. - - This method should be called as each track in a playlist is started. - """ - self._track_start_callback = callback_func - + @abstractmethod def pause(self): """Pause playback. @@ -118,18 +118,14 @@ def pause(self): occured. """ + @abstractmethod def resume(self): """Resume paused playback. Resumes playback after being paused. """ - def next(self): - """Skip to next track in playlist.""" - - def previous(self): - """Skip to previous track in playlist.""" - + @abstractmethod def lower_volume(self): """Lower volume. @@ -138,6 +134,7 @@ def lower_volume(self): interfering. """ + @abstractmethod def restore_volume(self): """Restore normal volume. @@ -145,22 +142,22 @@ def restore_volume(self): OpenVoiceOS has lowered it using lower_volume(). """ - def get_track_length(self): + @abstractmethod + def get_track_length(self) -> int: """ getting the duration of the audio in milliseconds - NOTE: not yet supported by mycroft-core """ - def get_track_position(self): + @abstractmethod + def get_track_position(self) -> int: """ get current position in milliseconds - NOTE: not yet supported by mycroft-core """ + @abstractmethod def set_track_position(self, milliseconds): """ go to position in milliseconds - NOTE: not yet supported by mycroft-core Args: milliseconds (int): number of milliseconds of final position """ @@ -171,6 +168,9 @@ def seek_forward(self, seconds=1): Arguments: seconds (int): number of seconds to seek, if negative rewind """ + miliseconds = seconds * 1000 + new_pos = self.get_track_position() + miliseconds + self.set_track_position(new_pos) def seek_backward(self, seconds=1): """Rewind X seconds. @@ -178,6 +178,9 @@ def seek_backward(self, seconds=1): Arguments: seconds (int): number of seconds to seek, if negative jump forward. """ + miliseconds = seconds * 1000 + new_pos = self.get_track_position() - miliseconds + self.set_track_position(new_pos) def track_info(self): """Get info about current playing track. @@ -188,6 +191,7 @@ def track_info(self): ret = {} ret['artist'] = '' ret['album'] = '' + ret['title'] = self._now_playing return ret def shutdown(self): @@ -198,12 +202,76 @@ def shutdown(self): self.stop() +class AudioBackend(MediaBackend): + """ for audio""" + + def load_track(self, uri): + super().load_track(uri) + self.bus.emit(Message("ovos.common_play.track.state", + {"state": TrackState.QUEUED_AUDIO})) + + def ocp_start(self): + """Emit OCP status events for play""" + super().ocp_start() + self.bus.emit(Message("ovos.common_play.track.state", + {"state": TrackState.PLAYING_AUDIO})) + + class RemoteAudioBackend(AudioBackend): """Base class for remote audio backends. RemoteAudioBackends will always be checked after the normal AudioBackends to make playback start locally by default. - An example of a RemoteAudioBackend would be things like Chromecasts, - mopidy servers, etc. + An example of a RemoteAudioBackend would be things like mopidy servers, etc. + """ + + +class VideoBackend(MediaBackend): + """ for audio""" + def load_track(self, uri): + super().load_track(uri) + self.bus.emit(Message("ovos.common_play.track.state", + {"state": TrackState.QUEUED_VIDEO})) + + def ocp_start(self): + """Emit OCP status events for play""" + super().ocp_start() + self.bus.emit(Message("ovos.common_play.track.state", + {"state": TrackState.PLAYING_VIDEO})) + + +class RemoteVideoBackend(VideoBackend): + """Base class for remote audio backends. + + RemoteAudioBackends will always be checked after the normal + VideoBackends to make playback start locally by default. + + An example of a RemoteVideoBackend would be things like Chromecasts, etc. + """ + + +class VideoWebBackend(MediaBackend): + """ for web pages""" + + def load_track(self, uri): + super().load_track(uri) + self.bus.emit(Message("ovos.common_play.track.state", + {"state": TrackState.QUEUED_WEBVIEW})) + + def ocp_start(self): + """Emit OCP status events for play""" + super().ocp_start() + self.bus.emit(Message("ovos.common_play.track.state", + {"state": TrackState.PLAYING_WEBVIEW})) + + +class RemoteWebBackend(VideoBackend): + """Base class for remote web backends. + + RemoteAudioBackends will always be checked after the normal + VideoBackends to make playback start locally by default. + + An example of a RemoteVideoBackend would be + things that can render a webpage in a different machine """