From 953f4b2fa2014a9960b6ae2d3f3423885f6828b1 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 8 May 2024 14:14:33 -0700 Subject: [PATCH 1/6] Update for ovos-utils 0.0.X compat. Related to https://github.com/OpenVoiceOS/OVOS-workshop/pull/201 --- ovos_plugin_manager/templates/media.py | 7 ++++++- requirements/requirements.txt | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/ovos_plugin_manager/templates/media.py b/ovos_plugin_manager/templates/media.py index 8d94132a..dcdeeaad 100644 --- a/ovos_plugin_manager/templates/media.py +++ b/ovos_plugin_manager/templates/media.py @@ -4,7 +4,10 @@ from ovos_utils.log import LOG from ovos_utils.messagebus import FakeBus -from ovos_utils.ocp import MediaState, PlayerState, TrackState +try: + from ovos_utils.ocp import MediaState, PlayerState, TrackState +except ImportError: + MediaState, PlayerState, TrackState = None, None, None class MediaBackend(metaclass=ABCMeta): @@ -18,6 +21,8 @@ class MediaBackend(metaclass=ABCMeta): """ def __init__(self, config=None, bus=None): + if MediaState is None: + raise RuntimeError("Please update to ovos-utils~=0.1.") self._now_playing = None # single uri self._track_start_callback = None self.supports_mime_hints = False diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 3f1f5ef2..2ffff61e 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,4 +1,4 @@ -ovos-utils < 0.2.0, >=0.1.0a8 +ovos-utils < 0.2.0, >=0.0.38 ovos-bus-client < 0.2.0, >=0.0.9a3 ovos-config < 0.2.0, >=0.0.12 combo_lock~=0.2 From d14b459aae1da6ee9079b2b0cbd28dc1ffd1f449 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 8 May 2024 14:17:16 -0700 Subject: [PATCH 2/6] Add ovos-utils alpha dependency to test.txt --- requirements/test.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements/test.txt b/requirements/test.txt index 48548151..7694cc16 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,4 +1,5 @@ pytest pytest-timeout pytest-cov -ovos-translate-server-plugin \ No newline at end of file +ovos-translate-server-plugin +ovos-utils>=0.1.0a8 \ No newline at end of file From 05ffe29b40829d461d038b2d04fe2f8dae2a5f93 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 8 May 2024 14:19:25 -0700 Subject: [PATCH 3/6] More import refactoring to allow ovos-utils<0.1 --- ovos_plugin_manager/templates/audio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_plugin_manager/templates/audio.py b/ovos_plugin_manager/templates/audio.py index fbb1e330..750e2839 100644 --- a/ovos_plugin_manager/templates/audio.py +++ b/ovos_plugin_manager/templates/audio.py @@ -8,7 +8,6 @@ from ovos_plugin_manager.templates.media import AudioPlayerBackend as _AB from ovos_utils import classproperty from ovos_utils.log import log_deprecation -from ovos_utils.ocp import PlaybackType, TrackState from ovos_utils.process_utils import RuntimeRequirements log_deprecation("ovos_plugin_manager.templates.audio has been deprecated on ovos-audio, " @@ -90,6 +89,7 @@ def _uri2meta(uri): from ovos_ocp_files_plugin.plugin import OCPFilesMetadataExtractor return OCPFilesMetadataExtractor.extract_metadata(uri) except: + from ovos_utils.ocp import PlaybackType, TrackState meta = {"uri": uri, "skill_id": "mycroft.audio_interface", "playback": PlaybackType.AUDIO, # TODO mime type check From 86ee25b4405c0d5324f7f34a61b12c3b79aa3902 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 8 May 2024 14:45:47 -0700 Subject: [PATCH 4/6] Refactor to include backwards-compat imports --- ovos_plugin_manager/templates/audio.py | 36 ++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/ovos_plugin_manager/templates/audio.py b/ovos_plugin_manager/templates/audio.py index 750e2839..d5151fd8 100644 --- a/ovos_plugin_manager/templates/audio.py +++ b/ovos_plugin_manager/templates/audio.py @@ -7,9 +7,42 @@ from ovos_plugin_manager.templates.media import AudioPlayerBackend as _AB from ovos_utils import classproperty -from ovos_utils.log import log_deprecation +from ovos_utils.log import LOG, log_deprecation from ovos_utils.process_utils import RuntimeRequirements +try: + from ovos_utils.ocp import PlaybackType, TrackState +except ImportError: + LOG.warning("Please update to ovos-utils~=0.1.") + from enum import IntEnum + + class PlaybackType(IntEnum): + SKILL = 0 # skills handle playback whatever way they see fit, + # eg spotify / mycroft common play + VIDEO = 1 # Video results + AUDIO = 2 # Results should be played audio only + AUDIO_SERVICE = 3 ## DEPRECATED - used in ovos 0.0.7 + MPRIS = 4 # External MPRIS compliant player + WEBVIEW = 5 # webview, render a url instead of media player + UNDEFINED = 100 # data not available, hopefully status will be updated soon.. + + + class TrackState(IntEnum): + DISAMBIGUATION = 1 # media result, not queued for playback + PLAYING_SKILL = 20 # Skill is handling playback internally + PLAYING_AUDIOSERVICE = 21 ## DEPRECATED - used in ovos 0.0.7 + PLAYING_VIDEO = 22 # Skill forwarded playback to video service + PLAYING_AUDIO = 23 # Skill forwarded playback to audio service + PLAYING_MPRIS = 24 # External media player is handling playback + PLAYING_WEBVIEW = 25 # Media playback handled in browser (eg. javascript) + + QUEUED_SKILL = 30 # Waiting playback to be handled inside skill + QUEUED_AUDIOSERVICE = 31 ## DEPRECATED - used in ovos 0.0.7 + QUEUED_VIDEO = 32 # Waiting playback in video service + QUEUED_AUDIO = 33 # Waiting playback in audio service + QUEUED_WEBVIEW = 34 # Waiting playback in browser service + + log_deprecation("ovos_plugin_manager.templates.audio has been deprecated on ovos-audio, " "move to ovos_plugin_manager.templates.media", "0.1.0") @@ -89,7 +122,6 @@ def _uri2meta(uri): from ovos_ocp_files_plugin.plugin import OCPFilesMetadataExtractor return OCPFilesMetadataExtractor.extract_metadata(uri) except: - from ovos_utils.ocp import PlaybackType, TrackState meta = {"uri": uri, "skill_id": "mycroft.audio_interface", "playback": PlaybackType.AUDIO, # TODO mime type check From 3e8082e0cdf26f567c6371b946757ff13dcdcbe4 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 8 May 2024 17:55:53 -0700 Subject: [PATCH 5/6] Refactor for ovos-utils 0.0.X compat --- ovos_plugin_manager/templates/audio.py | 171 ++++++++++++++++++++++++- ovos_plugin_manager/templates/media.py | 6 +- 2 files changed, 165 insertions(+), 12 deletions(-) diff --git a/ovos_plugin_manager/templates/audio.py b/ovos_plugin_manager/templates/audio.py index d5151fd8..083f049a 100644 --- a/ovos_plugin_manager/templates/audio.py +++ b/ovos_plugin_manager/templates/audio.py @@ -3,11 +3,13 @@ These classes can be used to create an Audioservice plugin extending OpenVoiceOS's media playback options. """ -from ovos_bus_client.message import Message +from abc import ABCMeta, abstractmethod -from ovos_plugin_manager.templates.media import AudioPlayerBackend as _AB +from ovos_bus_client import Message +from ovos_bus_client.message import dig_for_message from ovos_utils import classproperty -from ovos_utils.log import LOG, log_deprecation +from ovos_utils.log import log_deprecation, LOG +from ovos_utils.fakebus import FakeBus from ovos_utils.process_utils import RuntimeRequirements try: @@ -47,7 +49,7 @@ class TrackState(IntEnum): "move to ovos_plugin_manager.templates.media", "0.1.0") -class AudioBackend(_AB): +class AudioBackend(metaclass=ABCMeta): """Base class for all audio backend implementations. Arguments: @@ -55,6 +57,12 @@ class AudioBackend(_AB): bus (MessageBusClient): OpenVoiceOS messagebus emitter """ + def __init__(self, config=None, bus=None): + 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 @@ -90,14 +98,23 @@ def runtime_requirements(self): no_internet_fallback=True, no_network_fallback=True) - # methods below deprecated and handled by OCP directly - # playlists are no longer managed plugin side - # this is just a compat layer forwarding commands to OCP + @property + def playback_time(self): + return 0 + + def supported_uris(self): + """List of supported uri types. + + Returns: + list: Supported uri's + """ + def clear_list(self): """Clear playlist.""" msg = Message('ovos.common_play.playlist.clear') self.bus.emit(msg) + @abstractmethod def add_list(self, tracks): """Add tracks to backend's playlist. @@ -129,6 +146,49 @@ def _uri2meta(uri): } return meta + @abstractmethod + def play(self, repeat=False): + """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 + """ + + def stop(self): + """Stop playback. + + Stops the current playback. + + Returns: + 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 + + def pause(self): + """Pause playback. + + Stops playback but may be resumed at the exact position the pause + occured. + """ + msg = Message('ovos.common_play.pause') + self.bus.emit(msg) + + def resume(self): + """Resume paused playback. + + Resumes playback after being paused. + """ + msg = Message('ovos.common_play.resume') + def next(self): """Skip to next track in playlist.""" self.bus.emit(Message("ovos.common_play.next")) @@ -137,6 +197,103 @@ def previous(self): """Skip to previous track in playlist.""" self.bus.emit(Message("ovos.common_play.previous")) + def lower_volume(self): + """Lower volume. + + This method is used to implement audio ducking. It will be called when + OpenVoiceOS is listening or speaking to make sure the media playing isn't + interfering. + """ + + def restore_volume(self): + """Restore normal volume. + + Called when to restore the playback volume to previous level after + OpenVoiceOS has lowered it using lower_volume(). + """ + + def get_track_length(self): + """ + getting the duration of the audio in miliseconds + """ + length = 0 + msg = self._format_msg('ovos.common_play.get_track_length') + info = self.bus.wait_for_response(msg, timeout=1) + if info: + length = info.data.get("length", 0) + return length + + def get_track_position(self): + """ + get current position in miliseconds + """ + pos = 0 + msg = self._format_msg('ovos.common_play.get_track_position') + info = self.bus.wait_for_response(msg, timeout=1) + if info: + pos = info.data.get("position", 0) + return pos + + def set_track_position(self, milliseconds): + """Go to X position. + Arguments: + milliseconds (int): position to go to in milliseconds + """ + msg = self._format_msg('ovos.common_play.set_track_position', + {"position": milliseconds}) + self.bus.emit(msg) + + def seek_forward(self, seconds=1): + """Skip X seconds. + + Arguments: + seconds (int): number of seconds to seek, if negative rewind + """ + msg = self._format_msg('ovos.common_play.seek', + {"seconds": seconds}) + self.bus.emit(msg) + + def seek_backward(self, seconds=1): + """Rewind X seconds. + + Arguments: + seconds (int): number of seconds to seek, if negative jump forward. + """ + msg = self._format_msg('ovos.common_play.seek', + {"seconds": seconds * -1}) + self.bus.emit(msg) + + def track_info(self): + """Request information of current playing track. + Returns: + Dict with track info. + """ + msg = self._format_msg('ovos.common_play.track_info') + response = self.bus.wait_for_response(msg) + return response.data if response else {} + + def shutdown(self): + """Perform clean shutdown. + + Implements any audio backend specific shutdown procedures. + """ + self.stop() + + def _format_msg(self, msg_type, msg_data=None): + # this method ensures all skills are .forward from the utterance + # that triggered the skill, this ensures proper routing and metadata + msg_data = msg_data or {} + msg = dig_for_message() + if msg: + msg = msg.forward(msg_type, msg_data) + else: + msg = Message(msg_type, msg_data) + # at this stage source == skills, lets indicate audio service took over + sauce = msg.context.get("source") + if sauce == "skills": + msg.context["source"] = "audio_service" + return msg + class RemoteAudioBackend(AudioBackend): """Base class for remote audio backends. diff --git a/ovos_plugin_manager/templates/media.py b/ovos_plugin_manager/templates/media.py index dcdeeaad..85e25381 100644 --- a/ovos_plugin_manager/templates/media.py +++ b/ovos_plugin_manager/templates/media.py @@ -3,11 +3,7 @@ from ovos_bus_client.message import Message from ovos_utils.log import LOG from ovos_utils.messagebus import FakeBus - -try: - from ovos_utils.ocp import MediaState, PlayerState, TrackState -except ImportError: - MediaState, PlayerState, TrackState = None, None, None +from ovos_utils.ocp import MediaState, PlayerState, TrackState class MediaBackend(metaclass=ABCMeta): From a93b5a0549adfd529e171a8c978895586a5bff54 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 8 May 2024 18:15:32 -0700 Subject: [PATCH 6/6] Wrap unused imports for compat. --- ovos_plugin_manager/ocp.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ovos_plugin_manager/ocp.py b/ovos_plugin_manager/ocp.py index 5749cae5..9dcf9c15 100644 --- a/ovos_plugin_manager/ocp.py +++ b/ovos_plugin_manager/ocp.py @@ -1,11 +1,16 @@ from ovos_plugin_manager.utils import PluginTypes from ovos_plugin_manager.templates.ocp import OCPStreamExtractor -from ovos_plugin_manager.templates.media import AudioPlayerBackend, VideoPlayerBackend, WebPlayerBackend from ovos_utils.log import LOG from functools import lru_cache from ovos_plugin_manager.utils import find_plugins +try: + from ovos_plugin_manager.templates.media import AudioPlayerBackend, VideoPlayerBackend, WebPlayerBackend +except ImportError: + LOG.warning("Please install ovos-utils~=0.1 for `AudioPlayerBackend`, " + "`VideoPlayerBackend`, and `WebPlayerBackend` imports.") + def find_ocp_plugins() -> dict: """