Skip to content

Commit

Permalink
feat/OCP_backends
Browse files Browse the repository at this point in the history
  • Loading branch information
JarbasAl committed Jan 8, 2024
1 parent 5f079a1 commit 8523c28
Showing 1 changed file with 151 additions and 83 deletions.
234 changes: 151 additions & 83 deletions ovos_plugin_manager/templates/audio.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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
Expand All @@ -104,32 +110,22 @@ 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.
Stops playback but may be resumed at the exact position the pause
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.
Expand All @@ -138,29 +134,30 @@ def lower_volume(self):
interfering.
"""

@abstractmethod
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):
@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
"""
Expand All @@ -171,13 +168,19 @@ 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.
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.
Expand All @@ -188,6 +191,7 @@ def track_info(self):
ret = {}
ret['artist'] = ''
ret['album'] = ''
ret['title'] = self._now_playing
return ret

def shutdown(self):
Expand All @@ -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
"""

0 comments on commit 8523c28

Please sign in to comment.