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): def initial_sync(self):
# Wait for the ping to happen. # Wait for the ping to happen.
t = 0 tries = 5
while not self._server_available.value: while not self._server_available.value and tries < 5:
sleep(0.1) self._set_ping_status()
t += 0.1 tries += 1
if t >= 10: # timeout of 10 seconds on initial synchronization.
break
def shutdown(self): def shutdown(self):
self.ping_process.terminate() self.ping_process.terminate()
# Availability Properties # Availability Properties
# ================================================================================== # ==================================================================================
_first_ping_happened = multiprocessing.Value("b", False)
_server_available = multiprocessing.Value("b", False) _server_available = multiprocessing.Value("b", False)
def _check_ping_thread(self): def _check_ping_thread(self):
@@ -147,12 +144,8 @@ class SubsonicAdapter(Adapter):
# TODO: also use NM to detect when the connection changes and update # TODO: also use NM to detect when the connection changes and update
# accordingly. # accordingly.
# Try 5 times to ping the server. while True:
tries = 0
while not self._server_available.value and tries < 5:
self._set_ping_status() self._set_ping_status()
self._first_ping_happened.value = True
sleep(15) sleep(15)
# TODO maybe expose something like this on the API? # 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.window.player_controls.update_scrubber,
self.app_config.state.song_progress, self.app_config.state.song_progress,
self.app_config.state.current_song.duration, 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: 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) GLib.idle_add(self.on_next_track)
@dbus_propagate(self)
def on_player_event(event: PlayerEvent): def on_player_event(event: PlayerEvent):
if event.name == "play_state_change": if event.type == PlayerEvent.Type.PLAY_STATE_CHANGE:
self.app_config.state.playing = event.value assert event.playing
elif event.name == "volume_change": self.app_config.state.playing = event.playing
self.app_config.state.volume = event.value 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() self.update_window()
@@ -751,6 +774,7 @@ class SublimeMusicApp(Gtk.Application):
self.window.player_controls.update_scrubber( self.window.player_controls.update_scrubber(
self.app_config.state.song_progress, self.app_config.state.song_progress,
self.app_config.state.current_song.duration, self.app_config.state.current_song.duration,
self.app_config.state.song_stream_cache_progress,
) )
# If already playing, then make the player itself seek. # If already playing, then make the player itself seek.
@@ -919,7 +943,6 @@ class SublimeMusicApp(Gtk.Application):
self.app_config.state.current_song_index = ( self.app_config.state.current_song_index = (
play_queue.current_index or 0 play_queue.current_index or 0
) )
self.player.reset() self.player.reset()
self.update_window() self.update_window()

View File

@@ -7,7 +7,9 @@ import os
import socket import socket
import threading import threading
from concurrent.futures import Future, ThreadPoolExecutor from concurrent.futures import Future, ThreadPoolExecutor
from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from enum import Enum
from time import sleep from time import sleep
from typing import Any, Callable, List, Optional from typing import Any, Callable, List, Optional
from urllib.parse import urlparse from urllib.parse import urlparse
@@ -22,14 +24,17 @@ from sublime.adapters.api_objects import Song
from sublime.config import AppConfiguration from sublime.config import AppConfiguration
@dataclass
class PlayerEvent: class PlayerEvent:
# TODO standardize this class Type(Enum):
name: str PLAY_STATE_CHANGE = 0
value: Any VOLUME_CHANGE = 1
STREAM_CACHE_PROGRESS_CHANGE = 2
def __init__(self, name: str, value: Any): type: Type
self.name = name playing: Optional[bool] = False
self.value = value volume: Optional[float] = 0.0
stream_cache_duration: Optional[float] = 0.0
class Player(abc.ABC): class Player(abc.ABC):
@@ -37,6 +42,7 @@ class Player(abc.ABC):
# because it's kinda a bit strange tbh. # because it's kinda a bit strange tbh.
_can_hotswap_source: bool _can_hotswap_source: bool
# TODO unify on_timepos_change and on_player_event?
def __init__( def __init__(
self, self,
on_timepos_change: Callable[[Optional[float]], None], on_timepos_change: Callable[[Optional[float]], None],
@@ -153,7 +159,7 @@ class MPVPlayer(Player):
self._can_hotswap_source = True self._can_hotswap_source = True
@self.mpv.property_observer("time-pos") @self.mpv.property_observer("time-pos")
def time_observer(_: Any, value: Optional[float]): def time_observer(_, value: Optional[float]):
self.on_timepos_change(value) self.on_timepos_change(value)
if value is None and self.progress_value_count > 1: if value is None and self.progress_value_count > 1:
self.on_track_end() self.on_track_end()
@@ -164,6 +170,15 @@ class MPVPlayer(Player):
with self.progress_value_lock: with self.progress_value_lock:
self.progress_value_count += 1 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: def _is_playing(self) -> bool:
return not self.mpv.pause return not self.mpv.pause
@@ -182,7 +197,7 @@ class MPVPlayer(Player):
"loadfile", "loadfile",
file_or_url, file_or_url,
"replace", "replace",
f"start={progress.total_seconds()}" if progress else "", f"force-seekable=yes,start={progress.total_seconds()}" if progress else "",
) )
self._song_loaded = True self._song_loaded = True
@@ -360,15 +375,17 @@ class ChromecastPlayer(Player):
): ):
self.on_player_event( self.on_player_event(
PlayerEvent( PlayerEvent(
"volume_change", PlayerEvent.Type.VOLUME_CHANGE,
status.volume_level * 100 if not status.volume_muted else 0, volume=(status.volume_level * 100 if not status.volume_muted else 0),
) )
) )
# This normally happens when "Stop Casting" is pressed in the Google # This normally happens when "Stop Casting" is pressed in the Google
# Home app. # Home app.
if status.session_id is None: 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 self._song_loaded = False
def on_new_media_status( def on_new_media_status(
@@ -386,7 +403,8 @@ class ChromecastPlayer(Player):
self.on_player_event( self.on_player_event(
PlayerEvent( 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 if app_config.state.current_song
else None 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" icon = "pause" if app_config.state.playing else "start"
self.play_button.set_icon(f"media-playback-{icon}-symbolic") self.play_button.set_icon(f"media-playback-{icon}-symbolic")
@@ -306,7 +313,10 @@ class PlayerControls(Gtk.ActionBar):
self.album_art.set_loading(False) self.album_art.set_loading(False)
def update_scrubber( 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: if current is None or duration is None:
self.song_duration_label.set_text("-:--") self.song_duration_label.set_text("-:--")
@@ -314,10 +324,16 @@ class PlayerControls(Gtk.ActionBar):
self.song_scrubber.set_value(0) self.song_scrubber.set_value(0)
return return
percent_complete = current.total_seconds() / duration.total_seconds() * 100 percent_complete = current / duration * 100
if not self.editing: if not self.editing:
self.song_scrubber.set_value(percent_complete) 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_duration_label.set_text(util.format_song_duration(duration))
self.song_progress_label.set_text( self.song_progress_label.set_text(
util.format_song_duration(math.floor(current.total_seconds())) 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_name("song-scrubber")
self.song_scrubber.set_draw_value(False) self.song_scrubber.set_draw_value(False)
self.song_scrubber.set_restrict_to_fill_level(False)
self.song_scrubber.connect( self.song_scrubber.connect(
"change-value", lambda s, t, v: self.emit("song-scrub", v) "change-value", lambda s, t, v: self.emit("song-scrub", v)
) )

View File

@@ -1,7 +1,7 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import timedelta from datetime import timedelta
from enum import Enum 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 import AlbumSearchQuery
from sublime.adapters.api_objects import Genre, Song from sublime.adapters.api_objects import Genre, Song
@@ -45,6 +45,7 @@ class UIState:
repeat_type: RepeatType = RepeatType.NO_REPEAT repeat_type: RepeatType = RepeatType.NO_REPEAT
shuffle_on: bool = False shuffle_on: bool = False
song_progress: timedelta = timedelta() song_progress: timedelta = timedelta()
song_stream_cache_progress: Optional[timedelta] = timedelta()
current_device: str = "this device" current_device: str = "this device"
current_tab: str = "albums" current_tab: str = "albums"
selected_album_id: Optional[str] = None selected_album_id: Optional[str] = None
@@ -55,6 +56,15 @@ class UIState:
album_page_size: int = 30 album_page_size: int = 30
album_page: int = 0 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): class _DefaultGenre(Genre):
def __init__(self): def __init__(self):
self.name = "Rock" self.name = "Rock"