From: Marcel van der Veldt Date: Tue, 3 Aug 2021 08:33:48 +0000 (+0200) Subject: some fixes X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=b8250cf240dfa443f78dcfc15cba50ae1bad9bba;p=music-assistant-server.git some fixes - auto select quality for queue stream - improve detection of alert finish --- diff --git a/music_assistant/constants.py b/music_assistant/constants.py index 3926751d..c213513b 100755 --- a/music_assistant/constants.py +++ b/music_assistant/constants.py @@ -1,6 +1,6 @@ """All constants for Music Assistant.""" -__version__ = "0.2.2" +__version__ = "0.2.3" REQUIRED_PYTHON_VER = "3.8" # configuration keys/attributes @@ -55,7 +55,6 @@ EVENT_TRACK_ADDED = "track added" EVENT_PLAYLIST_ADDED = "playlist added" EVENT_RADIO_ADDED = "radio added" EVENT_TASK_UPDATED = "task updated" -EVENT_ALERT_FINISHED = "alert finished" # player attributes ATTR_PLAYER_ID = "player_id" diff --git a/music_assistant/helpers/alert.mp3 b/music_assistant/helpers/alert.mp3 deleted file mode 100644 index 133b3ac7..00000000 Binary files a/music_assistant/helpers/alert.mp3 and /dev/null differ diff --git a/music_assistant/helpers/audio.py b/music_assistant/helpers/audio.py index af5c9806..789dd82c 100644 --- a/music_assistant/helpers/audio.py +++ b/music_assistant/helpers/audio.py @@ -122,16 +122,13 @@ async def get_stream_details( """ if queue_item.provider == "url": # special case: a plain url was added to the queue - if queue_item.streamdetails is not None: - streamdetails = queue_item.streamdetails - else: - streamdetails = StreamDetails( - type=StreamType.URL, - provider="url", - item_id=queue_item.item_id, - path=queue_item.uri, - content_type=ContentType(queue_item.uri.split(".")[-1]), - ) + streamdetails = StreamDetails( + type=StreamType.URL, + provider="url", + item_id=queue_item.item_id, + path=queue_item.uri if queue_item.uri else queue_item.item_id, + content_type=ContentType(queue_item.uri.split(".")[-1]), + ) else: # always request the full db track as there might be other qualities available # except for radio @@ -316,8 +313,10 @@ def get_sox_args( filter_args = [] if streamdetails.gain_correct: filter_args += ["vol", str(streamdetails.gain_correct), "dB"] - if resample: + if resample and resample > 48000: filter_args += ["rate", "-v", str(resample)] + elif resample: + filter_args += ["rate", str(resample)] # TODO: still not sure about the order of the filter arguments in the chain # assumption is they need to be at the end of the chain return input_args + output_args + filter_args diff --git a/music_assistant/managers/players.py b/music_assistant/managers/players.py index aac1829d..5fe9e4c1 100755 --- a/music_assistant/managers/players.py +++ b/music_assistant/managers/players.py @@ -9,7 +9,6 @@ from music_assistant.constants import ( CONF_CROSSFADE_DURATION, CONF_POWER_CONTROL, CONF_VOLUME_CONTROL, - EVENT_ALERT_FINISHED, EVENT_PLAYER_ADDED, EVENT_PLAYER_REMOVED, ) @@ -29,6 +28,11 @@ from music_assistant.models.provider import PlayerProvider, ProviderType POLL_INTERVAL = 30 LOGGER = logging.getLogger("player_manager") +RESOURCES_DIR = ( + pathlib.Path(__file__).parent.resolve().parent.resolve().joinpath("resources") +) +ALERT_ANNOUNCE_FILE = str(RESOURCES_DIR.joinpath("alert_announce.flac")) +ALERT_FINISH_FILE = str(RESOURCES_DIR.joinpath("alert_finish.flac")) class PlayerManager: @@ -297,6 +301,13 @@ class PlayerManager: QueueOption.NEXT -> Play item(s) after current playing item QueueOption.ADD -> Append new items at end of the queue """ + # turn on player + player = self.get_player(player_id) + if not player: + raise FileNotFoundError("Player not found %s" % player_id) + if not player.calculated_state.powered: + await self.cmd_power_on(player_id) + player_queue = self.get_active_player_queue(player_id) # a single item or list of items may be provided if not isinstance(items, list): items = [items] @@ -332,22 +343,10 @@ class PlayerManager: for track in tracks: if not track.available: continue - queue_item = QueueItem.from_track(track) - # generate url for this queue item - queue_item.stream_url = "%s/queue/%s/%s" % ( - self.mass.web.stream_url, - player_id, - queue_item.queue_item_id, - ) + queue_item = player_queue.create_queue_item(track) queue_items.append(queue_item) - # turn on player - player = self.get_player(player_id) - if not player: - raise FileNotFoundError("Player not found %s" % player_id) - if not player.calculated_state.powered: - await self.cmd_power_on(player_id) + # load items into the queue - player_queue = self.get_active_player_queue(player_id) if queue_opt == QueueOption.REPLACE: return await player_queue.load(queue_items) if queue_opt in [QueueOption.PLAY, QueueOption.NEXT] and len(queue_items) > 100: @@ -377,14 +376,6 @@ class PlayerManager: if item: return await self.play_media(player_id, item, queue_opt) raise FileNotFoundError("Invalid uri: %s" % uri) - # fallback to regular url - queue_item = QueueItem(item_id=uri, provider="url", name=uri, uri=uri) - # generate url for this queue item - queue_item.stream_url = "%s/queue/%s/%s" % ( - self.mass.web.stream_url, - player_id, - queue_item.queue_item_id, - ) # turn on player player = self.get_player(player_id) if not player: @@ -393,6 +384,9 @@ class PlayerManager: await self.cmd_power_on(player_id) # load items into the queue player_queue = self.get_active_player_queue(player_id) + queue_item = player_queue.create_queue_item( + item_id=uri, provider="url", name=uri, uri=uri + ) if queue_opt == QueueOption.REPLACE: return await player_queue.load([queue_item]) if queue_opt == QueueOption.NEXT: @@ -459,50 +453,42 @@ class PlayerManager: # load alert items in player queue queue_items = [] if announce: - alert_announce = ( - pathlib.Path(__file__) - .parent.resolve() - .parent.resolve() - .joinpath("helpers", "alert.mp3") - ) - queue_item = QueueItem( - item_id="alert_announce", - provider="url", - name="alert", - duration=3, - uri=str(alert_announce), + queue_items.append( + player_queue.create_queue_item( + item_id="alert_announce", + provider="url", + name="alert_announce", + uri=ALERT_ANNOUNCE_FILE, + ) ) - queue_item.stream_url = "%s/queue/%s/%s" % ( - self.mass.web.stream_url, - player_queue.queue_id, - queue_item.queue_item_id, + queue_items.append( + player_queue.create_queue_item( + item_id="alert", provider="url", name="alert", uri=url ) - queue_items.append(queue_item) - - queue_item = QueueItem( - item_id="alert_sound", - provider="url", - name="alert", - duration=10, - uri=url, ) - queue_item.stream_url = "%s/queue/%s/%s" % ( - self.mass.web.stream_url, - player_queue.queue_id, - queue_item.queue_item_id, + queue_items.append( + # add a special (silent) file so we can detect finishing of the alert + player_queue.create_queue_item( + item_id="alert_finish", + provider="url", + name="alert_finish", + uri=ALERT_FINISH_FILE, + ) ) - queue_items.append(queue_item) - # load queue items await player_queue.load(queue_items) # add listener when playback of alert finishes - async def restore_queue_listener(event: str, event_data: str): - """Restore queue after the alert was played.""" - if event_data != queue_item.queue_item_id: - return - # player stopped playing - remove_cb() + async def restore_queue(): + count = 0 + while count < 30: + if ( + player_queue.cur_item == queue_items[-1] + and player_queue.cur_item_time > 2 + ): + break + count += 1 + await asyncio.sleep(1) # restore queue if volume: await self.cmd_volume_set(player_id, prev_volume) @@ -520,9 +506,7 @@ class PlayerManager: await self.cmd_power_off(player_id) player_queue.signal_update() - remove_cb = self.mass.eventbus.add_listener( - restore_queue_listener, EVENT_ALERT_FINISHED - ) + create_task(restore_queue) @api_route("players/{player_id}/cmd/stop", method="PUT") async def cmd_stop(self, player_id: str) -> None: diff --git a/music_assistant/models/player_queue.py b/music_assistant/models/player_queue.py index 6682fd98..8eaebd78 100755 --- a/music_assistant/models/player_queue.py +++ b/music_assistant/models/player_queue.py @@ -10,7 +10,6 @@ from typing import Any, Dict, List, Optional, Set, Tuple, Union from music_assistant.constants import ( CONF_CROSSFADE_DURATION, - EVENT_ALERT_FINISHED, EVENT_QUEUE_ITEMS_UPDATED, EVENT_QUEUE_UPDATED, ) @@ -371,7 +370,7 @@ class PlayerQueue: """Load (overwrite) queue with new items.""" for index, item in enumerate(queue_items): item.sort_index = index - if self._shuffle_enabled and len(queue_items) > 2: + if self._shuffle_enabled and len(queue_items) > 5: queue_items = self.__shuffle_items(queue_items) self._items = queue_items if self.use_queue_stream: @@ -396,7 +395,7 @@ class PlayerQueue: insert_at_index = self.cur_index + offset for index, item in enumerate(queue_items): item.sort_index = insert_at_index + index - if self.shuffle_enabled and len(queue_items) > 10: + if self.shuffle_enabled and len(queue_items) > 5: queue_items = self.__shuffle_items(queue_items) if offset == 0: # replace current item with new @@ -527,18 +526,7 @@ class PlayerQueue: prev_item_time = int(self._cur_item_time) self._cur_item_time = int(track_time) if self._last_playback_state != self.state: - # handle special usecase where an alert is played - if ( - self._last_playback_state == PlayerState.PLAYING - and self.state == PlayerState.IDLE - and self.cur_item - and self.cur_item.name == "alert" - ): - self.mass.eventbus.signal( - EVENT_ALERT_FINISHED, - self.cur_item.queue_item_id, - ) - # fire regular event with updated state + # fire event with updated state self.signal_update() self._last_playback_state = self.state elif abs(prev_item_time - self._cur_item_time) > 3: @@ -641,6 +629,19 @@ class PlayerQueue: self._cur_index = cache_data.get("cur_index", 0) self._queue_stream_next_index = self._cur_index + def create_queue_item(self, *args, **kwargs): + """Create QueueItem including correct stream URL.""" + if args and isinstance(args[0], (Track, Radio)): + new_item = QueueItem.from_track(args[0]) + else: + new_item = QueueItem(*args, **kwargs) + new_item.stream_url = "%s/queue/%s/%s" % ( + self.mass.web.stream_url, + self.queue_id, + new_item.queue_item_id, + ) + return new_item + # pylint: enable=unused-argument async def _save_state(self) -> None: diff --git a/music_assistant/resources/alert_announce.flac b/music_assistant/resources/alert_announce.flac new file mode 100644 index 00000000..d14bfc8d Binary files /dev/null and b/music_assistant/resources/alert_announce.flac differ diff --git a/music_assistant/resources/alert_finish.flac b/music_assistant/resources/alert_finish.flac new file mode 100644 index 00000000..352dd6ff Binary files /dev/null and b/music_assistant/resources/alert_finish.flac differ diff --git a/music_assistant/web/api.py b/music_assistant/web/api.py index 51cdc7ba..504ce491 100644 --- a/music_assistant/web/api.py +++ b/music_assistant/web/api.py @@ -39,6 +39,8 @@ async def get_media_item_image_url( mass: MusicAssistant, media_type: MediaType, provider: str, item_id: str ) -> str: """Return image URL for given media item.""" + if provider == "url": + return None return await get_image_url(mass, item_id, provider, media_type) diff --git a/music_assistant/web/stream.py b/music_assistant/web/stream.py index b160b89f..7dc5020d 100644 --- a/music_assistant/web/stream.py +++ b/music_assistant/web/stream.py @@ -54,8 +54,20 @@ async def stream_queue(request: Request): await resp.prepare(request) player_conf = player_queue.player.config - pcm_format = "f64" - sample_rate = min(player_conf.get(CONF_MAX_SAMPLE_RATE, 96000), 96000) + # determine sample rate and pcm format for the queue stream, depending on player capabilities + player_max_sample_rate = player_conf.get(CONF_MAX_SAMPLE_RATE, 48000) + sample_rate = min(player_max_sample_rate, 96000) + if player_max_sample_rate > 96000: + # assume that highest possible quality is needed + # if player supports sample rates > 96000 + # we use float64 PCM format internally which is heavy on CPU + pcm_format = "f64" + elif sample_rate > 48000: + # prefer internal PCM_S32LE format + pcm_format = "s32" + else: + # fallback to 24 bits + pcm_format = "s24" args = [ "sox", @@ -80,7 +92,7 @@ async def stream_queue(request: Request): # feed stdin with pcm samples async def fill_buffer(): """Feed audio data into sox stdin for processing.""" - async for audio_chunk in get_queue_stream( + async for audio_chunk in get_pcm_queue_stream( mass, player_queue, sample_rate, pcm_format ): await sox_proc.write(audio_chunk) @@ -235,7 +247,7 @@ async def get_media_stream( ) -async def get_queue_stream( +async def get_pcm_queue_stream( mass: MusicAssistant, player_queue: PlayerQueue, sample_rate=96000, @@ -247,16 +259,14 @@ async def get_queue_stream( queue_index = None # get crossfade details fade_length = player_queue.crossfade_duration - if pcm_format in ["s64", "f64"]: + if "64" in pcm_format: bit_depth = 64 - elif pcm_format in ["s32", "f32"]: + elif "32" in pcm_format: bit_depth = 32 - elif pcm_format == "s16": - bit_depth = 16 - elif pcm_format == "s24": + elif "24" in pcm_format: bit_depth = 24 else: - raise NotImplementedError("Unsupported PCM format: %s" % pcm_format) + bit_depth = 16 pcm_args = [pcm_format, "-c", "2", "-r", str(sample_rate)] sample_size = int(sample_rate * (bit_depth / 8) * channels) # 1 second buffer_size = sample_size * fade_length if fade_length else sample_size * 10