Files
sublime-music/sublime_music/players/base.py
t11230 8ff857e0a8 Added basic gapless playback for mpv player
- Added property to Player to check whether gapless playback is supported and enabled
- Added method to Player to inform when the next song in a playlist is cached on the local computer
- Added gapless playback setting to the mpv player
- Updated mpv commands for gapless playback support
- Modified PlayerManager to track whether the current song is ending and what the expected next song is
- Modified PlayerManager to not invoke play_media if gapless playback is in use and the next song is expected
- Modified SublimeMusicApp to notify the player manager when the next song in a playlist is cached
2022-01-08 20:55:06 -05:00

230 lines
7.0 KiB
Python

import abc
from dataclasses import dataclass
from datetime import timedelta
from enum import Enum
from typing import Callable, Dict, Optional, Set, Tuple, Type, Union
from ..adapters.api_objects import Song
@dataclass
class PlayerEvent:
"""
Represents an event triggered by the player. This is a way to signal state changes
to Sublime Music if the player can be controlled outside of Sublime Music (for
example, Chromecast player).
Each player event has a :class:`PlayerEvent.EventType`. Additionally, each event
type has additional information in the form of additional properties on the
:class:`PlayerEvent` object.
* :class:`PlayerEvent.EventType.PLAY_STATE_CHANGE` -- indicates that the play state
of the player has changed. The :class:`PlayerEvent.playing` property is required
for this event type.
* :class:`PlayerEvent.EventType.VOLUME_CHANGE` -- indicates that the player's volume
has changed. The :classs`PlayerEvent.volume` property is required for this event
type and should be in the range [0, 100].
* :class:`PlayerEvent.EventType.STREAM_CACHE_PROGRESS_CHANGE` -- indicates that the
stream cache progress has changed. When streaming a song, this will be used to
show how much of the song has been loaded into the player. The
:class:`PlayerEvent.stream_cache_duration` property is required for this event
type and should be a float represent the number of seconds of the song that have
been cached.
* :class:`PlayerEvent.EventType.CONNECTING` -- indicates that a device is being
connected to. The :class:`PlayerEvent.device_id` property is required for this
event type and indicates the device ID that is being connected to.
* :class:`PlayerEvent.EventType.CONNECTED` -- indicates that a device has been
connected to. The :class:`PlayerEvent.device_id` property is required for this
event type and indicates the device ID that has been connected to.
"""
class EventType(Enum):
PLAY_STATE_CHANGE = 0
VOLUME_CHANGE = 1
STREAM_CACHE_PROGRESS_CHANGE = 2
CONNECTING = 3
CONNECTED = 4
DISCONNECT = 5
type: EventType
device_id: str
playing: Optional[bool] = None
volume: Optional[float] = None
stream_cache_duration: Optional[float] = None
@dataclass
class PlayerDeviceEvent:
class Delta(Enum):
ADD = 0
REMOVE = 1
delta: Delta
player_type: Type
id: str
name: str
class Player(abc.ABC):
song_loaded = False
@property
@abc.abstractmethod
def enabled(self) -> bool:
return True
@property
@abc.abstractmethod
def name(self) -> str:
"""
:returns: returns the friendly name of the player for display in the UI.
"""
@property
@abc.abstractmethod
def supported_schemes(self) -> Set[str]:
"""
:returns: a set of all the schemes that the player can play.
"""
@property
def can_start_playing_with_no_latency(self) -> bool:
"""
:returns: whether the player can start playing a song with no latency.
"""
return False
@property
def gapless_playback(self) -> bool:
"""
:returns: whether the player supports and is using gapless playback
"""
return False
@staticmethod
@abc.abstractmethod
def get_configuration_options() -> Dict[str, Union[Type, Tuple[str, ...]]]:
"""
:returns: a dictionary of configuration key -> type of the option or tuple of
options (for a dropdown menu).
"""
@abc.abstractmethod
def __init__(
self,
on_timepos_change: Callable[[Optional[float]], None],
on_track_end: Callable[[], None],
on_player_event: Callable[[PlayerEvent], None],
player_device_change_callback: Callable[[PlayerDeviceEvent], None],
config: Dict[str, Union[str, int, bool]],
):
"""
Initialize the player.
:param config: A dictionary of configuration key -> configuration value.
"""
@abc.abstractmethod
def change_settings(self, config: Dict[str, Union[str, int, bool]]):
"""
This function is called when the player settings are changed (normally this
happens when the user changes the settings in the UI).
:param config: A dictionary of configuration key -> configuration value.
"""
@abc.abstractmethod
def refresh_players(self):
"""
This function is called when the user requests the player list to be refreshed
in the UI.
This function should call the ``player_device_change_callback`` with the delta
events to indicate changes to the UI. If there is no reason to refresh (for
example, the MPV player), then this function can do nothing.
"""
@abc.abstractmethod
def set_current_device_id(self, device_id: str):
"""
Switch to the given device ID.
"""
def reset(self):
"""
Reset the player.
"""
@abc.abstractmethod
def shutdown(self):
"""
Do any cleanup of the player.
"""
@property
@abc.abstractmethod
def playing(self) -> bool:
"""
:returns: whether or not the player is currently playing a song.
"""
@abc.abstractmethod
def get_volume(self) -> float:
"""
:returns: the current volume on a scale of [0, 100]
"""
@abc.abstractmethod
def set_volume(self, volume: float):
"""
Set the volume of the player to the given value.
:param volume: the value to set the volume to. Will be in the range [0, 100]
"""
@abc.abstractmethod
def get_is_muted(self) -> bool:
"""
:returns: whether or not the player is muted.
"""
@abc.abstractmethod
def set_muted(self, muted: bool):
"""
:param muted: set the player's "muted" property to the given value.
"""
@abc.abstractmethod
def play_media(self, uri: str, progress: timedelta, song: Song):
"""
:param uri: the URI to play. The URI is guaranteed to be one of the schemes in
the :class:`supported_schemes` set for this adapter.
:param progress: the time at which to start playing the song.
:param song: the actual song. This could be used to set metadata and such on the
player.
"""
@abc.abstractmethod
def pause(self):
"""
Pause the player.
"""
@abc.abstractmethod
def play(self):
"""
Play the current media.
"""
def seek(self, position: timedelta):
"""
:param position: seek to the given position in the song.
"""
def next_media_cached(self, uri: str, song: Song):
"""
:param uri: the URI to prepare to play. The URI is guaranteed to be one of
the schemes in the :class:`supported_schemes` set for this adapter.
:param song: the actual song.
"""