diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 3dade95..5b08569 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -46,7 +46,7 @@ jobs: - name: Install System Dependencies run: | sudo apt-get update - sudo apt install python3-dev swig + sudo apt install python3-dev swig sox mpg123 python -m pip install build wheel - name: Install repo run: | diff --git a/ovos_audio/audio.py b/ovos_audio/audio.py index d945272..f1efab9 100644 --- a/ovos_audio/audio.py +++ b/ovos_audio/audio.py @@ -26,6 +26,39 @@ except ImportError: OCPAudioBackend = None +try: + from ovos_utils.ocp import MediaState +except ImportError: + LOG.warning("Please update to ovos-utils~=0.1.") + from enum import IntEnum + + + class MediaState(IntEnum): + # https://doc.qt.io/qt-5/qmediaplayer.html#MediaStatus-enum + # The status of the media cannot be determined. + UNKNOWN = 0 + # There is no current media. PlayerState == STOPPED + NO_MEDIA = 1 + # The current media is being loaded. The player may be in any state. + LOADING_MEDIA = 2 + # The current media has been loaded. PlayerState== STOPPED + LOADED_MEDIA = 3 + # Playback of the current media has stalled due to + # insufficient buffering or some other temporary interruption. + # PlayerState != STOPPED + STALLED_MEDIA = 4 + # The player is buffering data but has enough data buffered + # for playback to continue for the immediate future. + # PlayerState != STOPPED + BUFFERING_MEDIA = 5 + # The player has fully buffered the current media. PlayerState != STOPPED + BUFFERED_MEDIA = 6 + # Playback has reached the end of the current media. PlayerState == STOPPED + END_OF_MEDIA = 7 + # The current media cannot be played. PlayerState == STOPPED + INVALID_MEDIA = 8 + + MINUTES = 60 # Seconds in a minute @@ -92,16 +125,16 @@ def load_services(self): local = [] remote = [] for plugin_name, plugin_module in found_plugins.items(): - LOG.info(f'Loading audio service plugin: {plugin_name}') + LOG.info(f'Found audio service plugin: {plugin_name}') s = setup_audio_service(plugin_module, config=self.config, bus=self.bus) if not s: + LOG.debug(f"{plugin_name} not loaded! config: {self.config}") continue if isinstance(s, RemoteAudioBackend): remote += s else: local += s - # Sort services so local services are checked first self.service = local + remote @@ -163,11 +196,13 @@ def track_start(self, track): LOG.debug('New track coming up!') self.bus.emit(Message('mycroft.audio.playing_track', data={'track': track})) + self.current.ocp_start() else: # If no track is about to start last track of the queue has been # played. LOG.debug('End of playlist!') self.bus.emit(Message('mycroft.audio.queue_end')) + self.current.ocp_stop() def _pause(self, message=None): """ @@ -181,6 +216,7 @@ def _pause(self, message=None): return if self.current: self.current.pause() + self.current.ocp_pause() def _resume(self, message=None): """ @@ -193,6 +229,7 @@ def _resume(self, message=None): return if self.current: self.current.resume() + self.current.ocp_resume() def _next(self, message=None): """ @@ -227,6 +264,7 @@ def _perform_stop(self, message=None): if self.current: name = self.current.name if self.current.stop(): + self.current.ocp_stop() if message: msg = message.reply("mycroft.stop.handled", {"by": "audio:" + name}) @@ -342,13 +380,22 @@ def play(self, tracks, prefered_service, repeat=False): break else: LOG.info('No service found for uri_type: ' + uri_type) + self.bus.emit(Message("ovos.common_play.media.state", + {"state": MediaState.INVALID_MEDIA})) return if not selected_service.supports_mime_hints: tracks = [t[0] if isinstance(t, list) else t for t in tracks] - selected_service.clear_list() - selected_service.add_list(tracks) - selected_service.play(repeat) + self.current = selected_service + self.current.clear_list() + self.current.add_list(tracks) + + try: + self.current.play(repeat) + self.current.ocp_start() + except Exception as e: + LOG.exception(f"failed to play with {self.current}") + self.current.ocp_error() self.play_start_time = time.monotonic() def _is_message_for_service(self, message): @@ -390,7 +437,7 @@ def _play(self, message): if ('utterance' in message.data and s.name in message.data['utterance']): prefered_service = s - LOG.debug(s.name + ' would be prefered') + LOG.debug(s.name + ' would be preferred') break except Exception as e: LOG.error(f"failed to parse audio service name: {s}") diff --git a/requirements/extras.txt b/requirements/extras.txt index a822e9c..2758063 100644 --- a/requirements/extras.txt +++ b/requirements/extras.txt @@ -1 +1,2 @@ -ovos-tts-plugin-server \ No newline at end of file +ovos-tts-plugin-server +ovos_audio_plugin_simple>=0.0.2a7 \ No newline at end of file diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 094ef9f..3e4268e 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,4 +1,4 @@ ovos-utils~=0.0, >=0.0.38 ovos-bus-client~=0.0, >=0.0.8 ovos-config~=0.0,>=0.0.13a7 -ovos-plugin-manager~=0.0, >=0.0.26a16 \ No newline at end of file +ovos-plugin-manager~=0.0, >=0.0.26a22 \ No newline at end of file diff --git a/test/requirements.txt b/test/requirements.txt index e6a8bed..cdc1a26 100644 --- a/test/requirements.txt +++ b/test/requirements.txt @@ -3,7 +3,5 @@ flake8==3.7.9 pytest==5.2.4 pytest-cov==2.8.1 cov-core==1.15.0 -sphinx==2.2.1 -sphinx-rtd-theme==0.4.3 -ovos-audio-plugin-simple~=0.0.1 -ovos-plugin-vlc~=0.0.1 \ No newline at end of file +ovos_audio_plugin_simple>=0.0.2a7 +ovos-utils>=0.1.0a16 \ No newline at end of file diff --git a/test/unittests/test_end2end.py b/test/unittests/test_end2end.py new file mode 100644 index 0000000..d58092c --- /dev/null +++ b/test/unittests/test_end2end.py @@ -0,0 +1,400 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import time +import unittest +from time import sleep + +from ovos_bus_client.message import Message +from ovos_utils.fakebus import FakeBus +from ovos_utils.ocp import MediaState, TrackState, PlayerState + +from ovos_audio.service import AudioService + + +class TestLegacy(unittest.TestCase): + def setUp(self): + self.core = AudioService(FakeBus(), disable_ocp=True, autoload=False) + self.core.config['default-backend'] = "simple" + self.core.config['backends'] = {"simple": { + "type": "ovos_simple", + "active": True + }} + self.core.load_services() + # simple plugin + self.core.bus.remove_all_listeners('ovos.common_play.simple.play') + + def test_http(self): + + messages = [] + + def new_msg(msg): + nonlocal messages + m = Message.deserialize(msg) + messages.append(m) + print(len(messages), msg) + + def wait_for_n_messages(n): + nonlocal messages + t = time.time() + while len(messages) < n: + sleep(0.1) + if time.time() - t > 10: + raise RuntimeError("did not get the number of expected messages under 10 seconds") + + self.core.bus.on("message", new_msg) + + utt = Message('mycroft.audio.service.play', + {"tracks": ["http://fake.mp3"]}, + {}) + self.core.bus.emit(utt) + + # confirm all expected messages are sent + expected_messages = [ + 'mycroft.audio.service.play', + "ovos.common_play.media.state", # LOADING_MEDIA + "ovos.common_play.track.state", # QUEUED_AUDIOSERVICE + "ovos.common_play.simple.play", # call simple plugin + "ovos.common_play.player.state", # PLAYING + "ovos.common_play.media.state", # LOADED_MEDIA + "ovos.common_play.track.state", # PLAYING_AUDIOSERVICE + ] + wait_for_n_messages(len(expected_messages)) + + self.assertEqual(len(expected_messages), len(messages)) + + for idx, m in enumerate(messages): + self.assertEqual(m.msg_type, expected_messages[idx]) + + state = messages[1] + self.assertEqual(state.data["state"], MediaState.LOADING_MEDIA) + state = messages[2] + self.assertEqual(state.data["state"], TrackState.QUEUED_AUDIOSERVICE) + state = messages[4] + self.assertEqual(state.data["state"], PlayerState.PLAYING) + state = messages[5] + self.assertEqual(state.data["state"], MediaState.LOADED_MEDIA) + state = messages[6] + self.assertEqual(state.data["state"], TrackState.PLAYING_AUDIOSERVICE) + + def test_uri_error(self): + + messages = [] + + def new_msg(msg): + nonlocal messages + m = Message.deserialize(msg) + messages.append(m) + print(len(messages), msg) + + def wait_for_n_messages(n): + nonlocal messages + t = time.time() + while len(messages) < n: + sleep(0.1) + if time.time() - t > 10: + raise RuntimeError("did not get the number of expected messages under 10 seconds") + + self.core.bus.on("message", new_msg) + + utt = Message('mycroft.audio.service.play', + {"tracks": ["bad_uri://fake.mp3"]}, + {}) + self.core.bus.emit(utt) + + # confirm all expected messages are sent + expected_messages = [ + 'mycroft.audio.service.play', + "ovos.common_play.media.state" # invalid media + ] + wait_for_n_messages(len(expected_messages)) + + self.assertEqual(len(expected_messages), len(messages)) + + for idx, m in enumerate(messages): + self.assertEqual(m.msg_type, expected_messages[idx]) + + state = messages[-1] + self.assertEqual(state.data["state"], MediaState.INVALID_MEDIA) + + def test_PLAYLIST(self): + + messages = [] + + def new_msg(msg): + nonlocal messages + m = Message.deserialize(msg) + messages.append(m) + print(len(messages), msg) + + def wait_for_n_messages(n): + nonlocal messages + t = time.time() + while len(messages) < n: + sleep(0.1) + if time.time() - t > 10: + raise RuntimeError("did not get the number of expected messages under 10 seconds") + + self.core.bus.on("message", new_msg) + + utt = Message('mycroft.audio.service.play', + {"tracks": ["http://fake.mp3", + "http://fake2.mp3", + "http://fake3.mp3"]}, + {}) + self.core.bus.emit(utt) + + # confirm all expected messages are sent + expected_messages = [ + 'mycroft.audio.service.play', + "ovos.common_play.media.state", # LOADED_MEDIA + "ovos.common_play.track.state", # QUEUED_AUDIOSERVICE + "ovos.common_play.simple.play", # call simple plugin + "ovos.common_play.player.state", # PLAYING + "ovos.common_play.media.state", # LOADED_MEDIA + "ovos.common_play.track.state", # PLAYING_AUDIOSERVICE + ] + wait_for_n_messages(len(expected_messages)) + + self.assertEqual(len(expected_messages), len(messages)) + + for idx, m in enumerate(messages): + self.assertEqual(m.msg_type, expected_messages[idx]) + + self.assertEqual(self.core.default._tracks, + ['http://fake.mp3', + 'http://fake2.mp3', + 'http://fake3.mp3']) + self.assertEqual(self.core.default._idx, 0) + + messages = [] + + utt = Message('mycroft.audio.service.next', + {}, + {}) + self.core.bus.emit(utt) + + # confirm all expected messages are sent + expected_messages = [ + 'mycroft.audio.service.next', + "ovos.common_play.media.state", # LOADED_MEDIA + "ovos.common_play.track.state", # QUEUED_AUDIOSERVICE + "ovos.common_play.simple.play", # call simple plugin + ] + wait_for_n_messages(len(expected_messages)) + + self.assertEqual(len(expected_messages), len(messages)) + + for idx, m in enumerate(messages): + self.assertEqual(m.msg_type, expected_messages[idx]) + + self.assertEqual(self.core.default._tracks, + ['http://fake.mp3', + 'http://fake2.mp3', + 'http://fake3.mp3']) + self.assertEqual(self.core.default._idx, 1) + messages = [] + + utt = Message('mycroft.audio.service.next', + {}, + {}) + self.core.bus.emit(utt) + + # confirm all expected messages are sent + expected_messages = [ + 'mycroft.audio.service.next', + "ovos.common_play.media.state", # LOADED_MEDIA + "ovos.common_play.track.state", # QUEUED_AUDIOSERVICE + "ovos.common_play.simple.play", # call simple plugin + ] + wait_for_n_messages(len(expected_messages)) + + self.assertEqual(len(expected_messages), len(messages)) + + for idx, m in enumerate(messages): + self.assertEqual(m.msg_type, expected_messages[idx]) + + self.assertEqual(self.core.default._tracks, + ['http://fake.mp3', + 'http://fake2.mp3', + 'http://fake3.mp3']) + self.assertEqual(self.core.default._idx, 2) + messages = [] + + utt = Message('mycroft.audio.service.prev', + {}, + {}) + self.core.bus.emit(utt) + + # confirm all expected messages are sent + expected_messages = [ + 'mycroft.audio.service.prev', + "ovos.common_play.media.state", # LOADED_MEDIA + "ovos.common_play.track.state", # QUEUED_AUDIOSERVICE + "ovos.common_play.simple.play", # call simple plugin + ] + wait_for_n_messages(len(expected_messages)) + + self.assertEqual(len(expected_messages), len(messages)) + + for idx, m in enumerate(messages): + self.assertEqual(m.msg_type, expected_messages[idx]) + + self.assertEqual(self.core.default._tracks, + ['http://fake.mp3', + 'http://fake2.mp3', + 'http://fake3.mp3']) + self.assertEqual(self.core.default._idx, 1) + + messages = [] + + utt = Message('mycroft.audio.service.queue', + {"tracks": ['http://fake4.mp3', 'http://fake5.mp3']}, + {}) + self.core.bus.emit(utt) + + # confirm all expected messages are sent + expected_messages = [ + 'mycroft.audio.service.queue' + ] + wait_for_n_messages(len(expected_messages)) + + self.assertEqual(len(expected_messages), len(messages)) + + for idx, m in enumerate(messages): + self.assertEqual(m.msg_type, expected_messages[idx]) + + self.assertEqual(self.core.default._tracks, + ['http://fake.mp3', + 'http://fake2.mp3', + 'http://fake3.mp3', + 'http://fake4.mp3', + 'http://fake5.mp3']) + self.assertEqual(self.core.default._idx, 1) + + def test_play_pause_resume(self): + + messages = [] + + def new_msg(msg): + nonlocal messages + m = Message.deserialize(msg) + messages.append(m) + print(len(messages), msg) + + def wait_for_n_messages(n): + nonlocal messages + t = time.time() + while len(messages) < n: + sleep(0.1) + if time.time() - t > 10: + raise RuntimeError("did not get the number of expected messages under 10 seconds") + + self.core.bus.on("message", new_msg) + + utt = Message('mycroft.audio.service.play', + {"tracks": ["http://fake.mp3"]}, + {}) + self.core.bus.emit(utt) + + # confirm all expected messages are sent + expected_messages = [ + 'mycroft.audio.service.play', + "ovos.common_play.media.state", # LOADING_MEDIA + "ovos.common_play.track.state", # QUEUED_AUDIOSERVICE + "ovos.common_play.simple.play", # call simple plugin + "ovos.common_play.player.state", # PLAYING + "ovos.common_play.media.state", # LOADED_MEDIA + "ovos.common_play.track.state", # PLAYING_AUDIOSERVICE + ] + wait_for_n_messages(len(expected_messages)) + + self.assertEqual(len(expected_messages), len(messages)) + + for idx, m in enumerate(messages): + self.assertEqual(m.msg_type, expected_messages[idx]) + + state = messages[4] + self.assertEqual(state.data["state"], PlayerState.PLAYING) + state = messages[5] + self.assertEqual(state.data["state"], MediaState.LOADED_MEDIA) + state = messages[6] + self.assertEqual(state.data["state"], TrackState.PLAYING_AUDIOSERVICE) + + messages = [] + utt = Message('mycroft.audio.service.pause', + {}, + {}) + self.core.bus.emit(utt) + + # confirm all expected messages are sent + expected_messages = [ + 'mycroft.audio.service.pause', + "ovos.common_play.player.state", # PAUSED + ] + wait_for_n_messages(len(expected_messages)) + + self.assertEqual(len(expected_messages), len(messages)) + + for idx, m in enumerate(messages): + self.assertEqual(m.msg_type, expected_messages[idx]) + + state = messages[-1] + self.assertEqual(state.data["state"], PlayerState.PAUSED) + + messages = [] + utt = Message('mycroft.audio.service.resume', + {}, + {}) + self.core.bus.emit(utt) + + # confirm all expected messages are sent + expected_messages = [ + 'mycroft.audio.service.resume', + "ovos.common_play.player.state", # PAUSED + "ovos.common_play.track.state", # PLAYING_AUDIOSERVICE + ] + wait_for_n_messages(len(expected_messages)) + + self.assertEqual(len(expected_messages), len(messages)) + + for idx, m in enumerate(messages): + self.assertEqual(m.msg_type, expected_messages[idx]) + + state = messages[-2] + self.assertEqual(state.data["state"], PlayerState.PLAYING) + + messages = [] + + self.core.track_start(None) # end of track + + # confirm all expected messages are sent + expected_messages = [ + "mycroft.audio.queue_end", + "ovos.common_play.player.state", # PAUSED + "ovos.common_play.media.state", # PLAYING_AUDIOSERVICE + ] + wait_for_n_messages(len(expected_messages)) + + self.assertEqual(len(expected_messages), len(messages)) + + for idx, m in enumerate(messages): + self.assertEqual(m.msg_type, expected_messages[idx]) + + state = messages[-2] + self.assertEqual(state.data["state"], PlayerState.STOPPED) + state = messages[-1] + self.assertEqual(state.data["state"], MediaState.END_OF_MEDIA) + + +if __name__ == "__main__": + unittest.main()