Closes #174: adds fill level for streaming

This commit is contained in:
Sumner Evans
2020-05-16 20:48:08 -06:00
parent 914136b474
commit 9c4a9f09d2
5 changed files with 99 additions and 38 deletions

View File

@@ -126,19 +126,16 @@ class SubsonicAdapter(Adapter):
def initial_sync(self):
# Wait for the ping to happen.
t = 0
while not self._server_available.value:
sleep(0.1)
t += 0.1
if t >= 10: # timeout of 10 seconds on initial synchronization.
break
tries = 5
while not self._server_available.value and tries < 5:
self._set_ping_status()
tries += 1
def shutdown(self):
self.ping_process.terminate()
# Availability Properties
# ==================================================================================
_first_ping_happened = multiprocessing.Value("b", False)
_server_available = multiprocessing.Value("b", False)
def _check_ping_thread(self):
@@ -147,12 +144,8 @@ class SubsonicAdapter(Adapter):
# TODO: also use NM to detect when the connection changes and update
# accordingly.
# Try 5 times to ping the server.
tries = 0
while not self._server_available.value and tries < 5:
while True:
self._set_ping_status()
self._first_ping_happened.value = True
sleep(15)
# TODO maybe expose something like this on the API?

View File

@@ -174,6 +174,7 @@ class SublimeMusicApp(Gtk.Application):
self.window.player_controls.update_scrubber,
self.app_config.state.song_progress,
self.app_config.state.current_song.duration,
self.app_config.state.song_stream_cache_progress,
)
if (self.last_play_queue_update + timedelta(15)).total_seconds() <= value:
@@ -201,12 +202,34 @@ class SublimeMusicApp(Gtk.Application):
GLib.idle_add(self.on_next_track)
@dbus_propagate(self)
def on_player_event(event: PlayerEvent):
if event.name == "play_state_change":
self.app_config.state.playing = event.value
elif event.name == "volume_change":
self.app_config.state.volume = event.value
if event.type == PlayerEvent.Type.PLAY_STATE_CHANGE:
assert event.playing
self.app_config.state.playing = event.playing
if self.dbus_manager:
self.dbus_manager.property_diff()
elif event.type == PlayerEvent.Type.VOLUME_CHANGE:
assert event.volume
self.app_config.state.volume = event.volume
if self.dbus_manager:
self.dbus_manager.property_diff()
elif event.type == PlayerEvent.Type.STREAM_CACHE_PROGRESS_CHANGE:
if (
self.loading_state
or not self.window
or not self.app_config.state.current_song
or not event.stream_cache_duration
):
return
self.app_config.state.song_stream_cache_progress = timedelta(
seconds=event.stream_cache_duration
)
GLib.idle_add(
self.window.player_controls.update_scrubber,
self.app_config.state.song_progress,
self.app_config.state.current_song.duration,
self.app_config.state.song_stream_cache_progress,
)
self.update_window()
@@ -751,6 +774,7 @@ class SublimeMusicApp(Gtk.Application):
self.window.player_controls.update_scrubber(
self.app_config.state.song_progress,
self.app_config.state.current_song.duration,
self.app_config.state.song_stream_cache_progress,
)
# If already playing, then make the player itself seek.
@@ -919,12 +943,11 @@ class SublimeMusicApp(Gtk.Application):
self.app_config.state.current_song_index = (
play_queue.current_index or 0
)
self.player.reset()
self.update_window()
self.player.reset()
self.update_window()
if was_playing:
self.on_play_pause()
if was_playing:
self.on_play_pause()
play_queue_future = AdapterManager.get_play_queue()
play_queue_future.add_done_callback(lambda f: GLib.idle_add(do_update, f))

View File

@@ -7,7 +7,9 @@ import os
import socket
import threading
from concurrent.futures import Future, ThreadPoolExecutor
from dataclasses import dataclass
from datetime import timedelta
from enum import Enum
from time import sleep
from typing import Any, Callable, List, Optional
from urllib.parse import urlparse
@@ -22,14 +24,17 @@ from sublime.adapters.api_objects import Song
from sublime.config import AppConfiguration
@dataclass
class PlayerEvent:
# TODO standardize this
name: str
value: Any
class Type(Enum):
PLAY_STATE_CHANGE = 0
VOLUME_CHANGE = 1
STREAM_CACHE_PROGRESS_CHANGE = 2
def __init__(self, name: str, value: Any):
self.name = name
self.value = value
type: Type
playing: Optional[bool] = False
volume: Optional[float] = 0.0
stream_cache_duration: Optional[float] = 0.0
class Player(abc.ABC):
@@ -37,6 +42,7 @@ class Player(abc.ABC):
# because it's kinda a bit strange tbh.
_can_hotswap_source: bool
# TODO unify on_timepos_change and on_player_event?
def __init__(
self,
on_timepos_change: Callable[[Optional[float]], None],
@@ -153,7 +159,7 @@ class MPVPlayer(Player):
self._can_hotswap_source = True
@self.mpv.property_observer("time-pos")
def time_observer(_: Any, value: Optional[float]):
def time_observer(_, value: Optional[float]):
self.on_timepos_change(value)
if value is None and self.progress_value_count > 1:
self.on_track_end()
@@ -164,6 +170,15 @@ class MPVPlayer(Player):
with self.progress_value_lock:
self.progress_value_count += 1
@self.mpv.property_observer("demuxer-cache-time")
def cache_size_observer(_, value: Optional[float]):
on_player_event(
PlayerEvent(
PlayerEvent.Type.STREAM_CACHE_PROGRESS_CHANGE,
stream_cache_duration=value,
)
)
def _is_playing(self) -> bool:
return not self.mpv.pause
@@ -182,7 +197,7 @@ class MPVPlayer(Player):
"loadfile",
file_or_url,
"replace",
f"start={progress.total_seconds()}" if progress else "",
f"force-seekable=yes,start={progress.total_seconds()}" if progress else "",
)
self._song_loaded = True
@@ -360,15 +375,17 @@ class ChromecastPlayer(Player):
):
self.on_player_event(
PlayerEvent(
"volume_change",
status.volume_level * 100 if not status.volume_muted else 0,
PlayerEvent.Type.VOLUME_CHANGE,
volume=(status.volume_level * 100 if not status.volume_muted else 0),
)
)
# This normally happens when "Stop Casting" is pressed in the Google
# Home app.
if status.session_id is None:
self.on_player_event(PlayerEvent("play_state_change", False))
self.on_player_event(
PlayerEvent(PlayerEvent.Type.PLAY_STATE_CHANGE, playing=False)
)
self._song_loaded = False
def on_new_media_status(
@@ -386,7 +403,8 @@ class ChromecastPlayer(Player):
self.on_player_event(
PlayerEvent(
"play_state_change", status.player_state in ("PLAYING", "BUFFERING"),
PlayerEvent.Type.PLAY_STATE_CHANGE,
playing=(status.player_state in ("PLAYING", "BUFFERING")),
)
)

View File

@@ -71,7 +71,14 @@ class PlayerControls(Gtk.ActionBar):
if app_config.state.current_song
else None
)
self.update_scrubber(app_config.state.song_progress, duration)
song_stream_cache_progress = (
app_config.state.song_stream_cache_progress
if app_config.state.current_song
else None
)
self.update_scrubber(
app_config.state.song_progress, duration, song_stream_cache_progress
)
icon = "pause" if app_config.state.playing else "start"
self.play_button.set_icon(f"media-playback-{icon}-symbolic")
@@ -306,7 +313,10 @@ class PlayerControls(Gtk.ActionBar):
self.album_art.set_loading(False)
def update_scrubber(
self, current: Optional[timedelta], duration: Optional[timedelta],
self,
current: Optional[timedelta],
duration: Optional[timedelta],
song_stream_cache_progress: Optional[timedelta],
):
if current is None or duration is None:
self.song_duration_label.set_text("-:--")
@@ -314,10 +324,16 @@ class PlayerControls(Gtk.ActionBar):
self.song_scrubber.set_value(0)
return
percent_complete = current.total_seconds() / duration.total_seconds() * 100
percent_complete = current / duration * 100
if not self.editing:
self.song_scrubber.set_value(percent_complete)
self.song_scrubber.set_show_fill_level(song_stream_cache_progress is not None)
if song_stream_cache_progress:
percent_cached = song_stream_cache_progress / duration * 100
self.song_scrubber.set_fill_level(percent_cached)
self.song_duration_label.set_text(util.format_song_duration(duration))
self.song_progress_label.set_text(
util.format_song_duration(math.floor(current.total_seconds()))
@@ -517,6 +533,7 @@ class PlayerControls(Gtk.ActionBar):
)
self.song_scrubber.set_name("song-scrubber")
self.song_scrubber.set_draw_value(False)
self.song_scrubber.set_restrict_to_fill_level(False)
self.song_scrubber.connect(
"change-value", lambda s, t, v: self.emit("song-scrub", v)
)

View File

@@ -1,7 +1,7 @@
from dataclasses import dataclass, field
from datetime import timedelta
from enum import Enum
from typing import Dict, Optional, Tuple
from typing import Any, Dict, Optional, Tuple
from sublime.adapters import AlbumSearchQuery
from sublime.adapters.api_objects import Genre, Song
@@ -45,6 +45,7 @@ class UIState:
repeat_type: RepeatType = RepeatType.NO_REPEAT
shuffle_on: bool = False
song_progress: timedelta = timedelta()
song_stream_cache_progress: Optional[timedelta] = timedelta()
current_device: str = "this device"
current_tab: str = "albums"
selected_album_id: Optional[str] = None
@@ -55,6 +56,15 @@ class UIState:
album_page_size: int = 30
album_page: int = 0
def __getstate__(self):
state = self.__dict__.copy()
del state["song_stream_cache_progress"]
return state
def __setstate__(self, state: Dict[str, Any]):
self.__dict__.update(state)
self.song_stream_cache_progress = None
class _DefaultGenre(Genre):
def __init__(self):
self.name = "Rock"