diff --git a/.github/workflows/build_tests.yml b/.github/workflows/build_tests.yml index 224267f..040ad9b 100644 --- a/.github/workflows/build_tests.yml +++ b/.github/workflows/build_tests.yml @@ -7,7 +7,7 @@ on: branches: - dev paths-ignore: - - 'ovos_audio_plugin_simple/version.py' + - 'ovos_media_plugin_simple/version.py' - 'test/**' - 'examples/**' - '.github/**' @@ -15,7 +15,7 @@ on: - 'LICENSE' - 'CHANGELOG.md' - 'MANIFEST.in' - - 'readme.md' + - 'README.md' - 'scripts/**' workflow_dispatch: diff --git a/.github/workflows/publish_alpha.yml b/.github/workflows/publish_alpha.yml index 7813a34..614c1c5 100644 --- a/.github/workflows/publish_alpha.yml +++ b/.github/workflows/publish_alpha.yml @@ -6,7 +6,7 @@ on: branches: - dev paths-ignore: - - 'ovos_audio_plugin_simple/version.py' + - 'ovos_media_plugin_simple/version.py' - 'test/**' - 'examples/**' - '.github/**' @@ -14,7 +14,7 @@ on: - 'LICENSE' - 'CHANGELOG.md' - 'MANIFEST.in' - - 'readme.md' + - 'README.md' - 'scripts/**' workflow_dispatch: diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 05bf414..f82ae8c 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -4,7 +4,7 @@ on: branches: - dev paths-ignore: - - 'ovos_audio_plugin_simple/version.py' + - 'ovos_media_plugin_simple/version.py' - 'requirements/**' - 'examples/**' - '.github/**' @@ -12,13 +12,13 @@ on: - 'LICENSE' - 'CHANGELOG.md' - 'MANIFEST.in' - - 'readme.md' + - 'README.md' - 'scripts/**' push: branches: - master paths-ignore: - - 'ovos_audio_plugin_simple/version.py' + - 'ovos_media_plugin_simple/version.py' - 'requirements/**' - 'examples/**' - '.github/**' @@ -26,7 +26,7 @@ on: - 'LICENSE' - 'CHANGELOG.md' - 'MANIFEST.in' - - 'readme.md' + - 'README.md' - 'scripts/**' workflow_dispatch: @@ -56,7 +56,7 @@ jobs: pip install pytest pytest-timeout pytest-cov - name: Run unittests run: | - pytest --cov=ovos_audio_plugin_simple --cov-report xml test/unittests + pytest --cov=ovos_media_plugin_simple --cov-report xml test/unittests # NOTE: additional pytest invocations should also add the --cov-append flag # or they will overwrite previous invocations' coverage reports # (for an example, see OVOS Skill Manager's workflow) diff --git a/MANIFEST.in b/MANIFEST.in index 8d624c6..2c7255d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -recursive-include ovos_audio_plugin_simple/ * +recursive-include ovos_media_plugin_simple/ * recursive-include requirements/ * include CHANGELOG.md include LICENSE \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c355af5 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# ovos-media-plugin-simple + +simple plugin for [ovos-media](https://github.com/OpenVoiceOS/ovos-media) + +tries to use the best command line utility available to play audio, `sox` is recommended + +## Install + +`pip install ovos-media-plugin-simple` + +## Configuration + + +```javascript +{ + "media": { + + // keys are the strings defined in "audio_players" + "preferred_audio_services": ["mplayer", "vlc", "simple"], + + // PlaybackType.AUDIO handlers + "audio_players": { + // simple player uses a slave simple instance to handle uris + "simple": { + // the plugin name + "module": "ovos-media-audio-plugin-simple", + + // users may request specific handlers in the utterance + // using these aliases + "aliases": ["Simple Player", "Command line"], + + // deactivate a plugin by setting to false + "active": true + } + } +} +``` \ No newline at end of file diff --git a/ovos_audio_plugin_simple/__init__.py b/ovos_media_plugin_simple/__init__.py similarity index 68% rename from ovos_audio_plugin_simple/__init__.py rename to ovos_media_plugin_simple/__init__.py index 4e13475..76a2bd2 100644 --- a/ovos_audio_plugin_simple/__init__.py +++ b/ovos_media_plugin_simple/__init__.py @@ -2,13 +2,11 @@ import re import signal import subprocess +import time from distutils.spawn import find_executable from time import sleep -from ovos_plugin_common_play.ocp.base import OCPAudioPlayerBackend -from ovos_plugin_common_play.ocp.status import TrackState, MediaState, PlayerState -from ovos_plugin_manager.templates.audio import AudioBackend -from ovos_bus_client import Message +from ovos_plugin_manager.templates.media import AudioPlayerBackend from ovos_utils.log import LOG from requests import Session @@ -31,7 +29,7 @@ def find_mime(path): return (None, None) -def play_audio(uri, play_cmd="play"): +def play_audio(uri, play_cmd): """ Play a audio file. Returns: subprocess.Popen object @@ -46,33 +44,43 @@ def play_audio(uri, play_cmd="play"): return None -SimpleAudioPluginConfig = { - "simple": { - "type": "ovos_simple", - "active": True - } -} - - -class OVOSSimpleService(OCPAudioPlayerBackend): +class SimpleAudioService(AudioPlayerBackend): sox_play = find_executable("play") pulse_play = find_executable("paplay") alsa_play = find_executable("aplay") mpg123_play = find_executable("mpg123") - def __init__(self, config, bus=None, name='ovos_simple'): - super(OVOSSimpleService, self).__init__(config, bus) - self.name = name - + def __init__(self, config, bus=None): + super().__init__(config, bus) self.process = None self._stop_signal = False self._is_playing = False self._paused = False + self.ts = 0 self.supports_mime_hints = True mimetypes.init() - self.bus.on('ovos.common_play.simple.play', self._play) + def on_track_start(self): + self.ts = time.time() + # Indicate to audio service which track is being played + if self._track_start_callback: + self._track_start_callback(self._now_playing) + + def on_track_end(self): + self._is_playing = False + self._paused = False + self.process = None + self.ts = 0 + if self._track_start_callback: + self._track_start_callback(None) + + def on_track_error(self): + self._is_playing = False + self._paused = False + self.process = None + self.ts = 0 + self.ocp_error() # simple player internals def _get_track(self, track_data): @@ -105,21 +113,9 @@ def _stop_running_process(self): self.process.kill() self.process = None - def _play(self, message): - """Implementation specific async method to handle playback. - - This allows mpg123 service to use the next method as well - as basic play/stop. - """ - LOG.info('SimpleAudioService._play') - - # Stop any existing audio playback - self._stop_running_process() - - repeat = message.data.get('repeat', False) - self._is_playing = True - self._paused = False - + @property + def player_cmd(self): + """determine the best command to play a stream""" # sox should handle almost every format, but fails in some urls if self.sox_play: track = self._now_playing @@ -143,23 +139,37 @@ def _play(self, message): # fallback to alsa, only wav files will play correctly player = player or self.alsa_play + return player - # Indicate to audio service which track is being played - self._track_start_callback(track) + # audio service + def supported_uris(self): + uris = ['file', 'http'] + if self.sox_play: + uris.append("https") + return uris + + def play(self, repeat=False): + """ Play playlist using simple. """ + # Stop any existing audio playback + self._stop_running_process() + + self._is_playing = True + self._paused = False # Replace file:// uri's with normal paths - uri = track.replace('file://', '') + uri = self._now_playing.replace('file://', '') + self.on_track_start() try: - self.process = play_audio(uri, player) + self.process = play_audio(uri, self.player_cmd) except FileNotFoundError as e: LOG.error(f'Couldn\'t play audio, {e}') self.process = None - self.ocp_error() + self.on_track_error() except Exception as e: LOG.exception(repr(e)) self.process = None - self.ocp_error() + self.on_track_error() # Wait for completion or stop request while (self._is_process_running() and not self._stop_signal): @@ -167,29 +177,8 @@ def _play(self, message): if self._stop_signal: self._stop_running_process() - self._is_playing = False - self._paused = False - return - else: - self.process = None - - self._track_start_callback(None) - self._is_playing = False - self._paused = False - self.ocp_stop() - - # audio service - def supported_uris(self): - uris = ['file', 'http'] - if self.sox_play: - uris.append("https") - return uris - def play(self, repeat=False): - """ Play playlist using simple. """ - self.ocp_start() - self.bus.emit(Message('ovos.common_play.simple.play', - {'repeat': repeat})) + self.on_track_end() def stop(self): """ Stop simple playback. """ @@ -199,7 +188,6 @@ def stop(self): while self._is_playing: sleep(0.1) self._stop_signal = False - self.ocp_stop() return True return False @@ -209,7 +197,6 @@ def pause(self): # Suspend the playback process self.process.send_signal(signal.SIGSTOP) self._paused = True - self.ocp_pause() def resume(self): """ Resume paused playback. """ @@ -217,25 +204,44 @@ def resume(self): # Resume the playback process self.process.send_signal(signal.SIGCONT) self._paused = False - self.ocp_resume() - def track_info(self): - """ Extract info of current track. """ - return {"track": self._now_playing} + 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. + """ + # Not available in this plugin -def load_service(base_config, bus): - backends = base_config.get('backends', []) - services = [(b, backends[b]) for b in backends - if backends[b]['type'] in ["simple", 'ovos_simple'] and - backends[b].get('active', False)] + def restore_volume(self): + """Restore normal volume. - if not any([OVOSSimpleService.sox_play, - OVOSSimpleService.pulse_play, - OVOSSimpleService.alsa_play, - OVOSSimpleService.mpg123_play]): - LOG.error("No basic playback functionality detected!!") - return [] + Called when to restore the playback volume to previous level after + OpenVoiceOS has lowered it using lower_volume(). + """ + # Not available in this plugin - instances = [OVOSSimpleService(s[1], bus, s[0]) for s in services] - return instances + def get_track_length(self) -> int: + """ + getting the duration of the audio in milliseconds + """ + # we only can estimate how much we already played as a minimum value + return self.get_track_position() + + def get_track_position(self) -> int: + """ + get current position in milliseconds + """ + # approximate given timestamp of playback start + if self.ts: + return int((time.time() - self.ts) * 1000) + return 0 + + def set_track_position(self, milliseconds): + """ + go to position in milliseconds + Args: + milliseconds (int): number of milliseconds of final position + """ + # Not available in this plugin diff --git a/ovos_audio_plugin_simple/version.py b/ovos_media_plugin_simple/version.py similarity index 100% rename from ovos_audio_plugin_simple/version.py rename to ovos_media_plugin_simple/version.py diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 15ab711..b1cff11 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,2 +1 @@ -ovos-plugin-manager -ovos_plugin_common_play \ No newline at end of file +ovos-plugin-manager>=0.0.26a5 \ No newline at end of file diff --git a/scripts/bump_alpha.py b/scripts/bump_alpha.py index 6a6d073..eb175dd 100644 --- a/scripts/bump_alpha.py +++ b/scripts/bump_alpha.py @@ -2,7 +2,7 @@ from os.path import join, dirname -version_file = join(dirname(dirname(__file__)), "ovos_audio_plugin_simple", "version.py") +version_file = join(dirname(dirname(__file__)), "ovos_media_plugin_simple", "version.py") version_var_name = "VERSION_ALPHA" with open(version_file, "r", encoding="utf-8") as v: diff --git a/scripts/bump_build.py b/scripts/bump_build.py index 7c2c177..82d42a2 100644 --- a/scripts/bump_build.py +++ b/scripts/bump_build.py @@ -2,7 +2,7 @@ from os.path import join, dirname -version_file = join(dirname(dirname(__file__)), "ovos_audio_plugin_simple", "version.py") +version_file = join(dirname(dirname(__file__)), "ovos_media_plugin_simple", "version.py") version_var_name = "VERSION_BUILD" alpha_var_name = "VERSION_ALPHA" diff --git a/scripts/bump_major.py b/scripts/bump_major.py index 2490db9..ecf8eba 100644 --- a/scripts/bump_major.py +++ b/scripts/bump_major.py @@ -2,7 +2,7 @@ from os.path import join, dirname -version_file = join(dirname(dirname(__file__)), "ovos_audio_plugin_simple", "version.py") +version_file = join(dirname(dirname(__file__)), "ovos_media_plugin_simple", "version.py") version_var_name = "VERSION_MAJOR" minor_var_name = "VERSION_MINOR" build_var_name = "VERSION_BUILD" diff --git a/scripts/bump_minor.py b/scripts/bump_minor.py index 115188e..0fe60c3 100644 --- a/scripts/bump_minor.py +++ b/scripts/bump_minor.py @@ -2,7 +2,7 @@ from os.path import join, dirname -version_file = join(dirname(dirname(__file__)), "ovos_audio_plugin_simple", "version.py") +version_file = join(dirname(dirname(__file__)), "ovos_media_plugin_simple", "version.py") version_var_name = "VERSION_MINOR" build_var_name = "VERSION_BUILD" alpha_var_name = "VERSION_ALPHA" diff --git a/scripts/remove_alpha.py b/scripts/remove_alpha.py index ba6df9b..a7f6aae 100644 --- a/scripts/remove_alpha.py +++ b/scripts/remove_alpha.py @@ -2,7 +2,7 @@ from os.path import join, dirname -version_file = join(dirname(dirname(__file__)), "ovos_audio_plugin_simple", "version.py") +version_file = join(dirname(dirname(__file__)), "ovos_media_plugin_simple", "version.py") alpha_var_name = "VERSION_ALPHA" diff --git a/setup.py b/setup.py index 642ef32..fe417fa 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ def get_version(): """ Find the version of the package""" version = None - version_file = os.path.join(BASEDIR, 'ovos_audio_plugin_simple', 'version.py') + version_file = os.path.join(BASEDIR, 'ovos_media_plugin_simple', 'version.py') major, minor, build, alpha = (None, None, None, None) with open(version_file) as f: for line in f: @@ -49,22 +49,19 @@ def required(requirements_file): if pkg.strip() and not pkg.startswith("#")] -PLUGIN_ENTRY_POINT = 'ovos_audio_simple=ovos_audio_plugin_simple' -PLUGIN_CONFIG_ENTRY_POINT = 'ovos_audio_simple.config=ovos_audio_plugin_simple:SimpleAudioPluginConfig' - +PLUGIN_ENTRY_POINT = 'ovos-media-audio-plugin-simple=ovos_media_plugin_simple:SimpleAudioService' setup( - name='ovos_audio_plugin_simple', + name='ovos-media-plugin-simple', version=get_version(), - description='simple audio plugin for ovos', - url='https://github.com/OpenVoiceOS/ovos-audio-plugin-simple', - packages=['ovos_audio_plugin_simple'], + description='simple OCP plugin for ovos', + url='https://github.com/OpenVoiceOS/ovos-media-plugin-simple', + author='JarbasAi', + author_email='jarbasai@mailfence.com', license='Apache-2.0', - author='jarbasAi', + packages=['ovos_media_plugin_simple'], install_requires=required("requirements/requirements.txt"), - package_data={'': package_files('ovos_audio_plugin_simple')}, - author_email='jarbasai@mailfence.com', - keywords='ovos audio plugin', - entry_points={'mycroft.plugin.audioservice': PLUGIN_ENTRY_POINT, - 'mycroft.plugin.audioservice.config': PLUGIN_CONFIG_ENTRY_POINT} + package_data={'': package_files('ovos_media_plugin_simple')}, + keywords='ovos audio OCP plugin', + entry_points={'opm.media.audio': PLUGIN_ENTRY_POINT} ) diff --git a/test/license_tests.py b/test/license_tests.py index a07fc38..ef25a17 100644 --- a/test/license_tests.py +++ b/test/license_tests.py @@ -28,7 +28,7 @@ allow_unlicense = True allow_ambiguous = False -pkg_name = "ovos_audio_plugin_simple" +pkg_name = "ovos_media_plugin_simple" class TestLicensing(unittest.TestCase):