From: Marcel van der Veldt Date: Fri, 21 Feb 2025 00:18:19 +0000 (+0100) Subject: Chore: Several tweaks to plugin source streams X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=6195c438c024e821e488aada165fb06f791a419d;p=music-assistant-server.git Chore: Several tweaks to plugin source streams --- diff --git a/music_assistant/controllers/players.py b/music_assistant/controllers/players.py index b7fc935e..b339732f 100644 --- a/music_assistant/controllers/players.py +++ b/music_assistant/controllers/players.py @@ -1635,10 +1635,6 @@ class PlayerController(CoreController): ) -> None: """Handle playback/select of given plugin source on player.""" plugin_source = plugin_prov.get_source() - if plugin_prov.in_use_by and plugin_prov.in_use_by != player.player_id: - raise PlayerCommandFailed( - f"Source {plugin_source.name} is already in use by another player" - ) player.active_source = plugin_source.id stream_url = self.mass.streams.get_plugin_source_url(plugin_source.id, player.player_id) await self.play_media( @@ -1669,9 +1665,9 @@ class PlayerController(CoreController): for plugin_prov in self.mass.get_providers(ProviderType.PLUGIN): if ProviderFeature.AUDIO_SOURCE not in plugin_prov.supported_features: continue - if plugin_prov.in_use_by and plugin_prov.in_use_by != player.player_id: - continue plugin_source = plugin_prov.get_source() + if plugin_source.in_use_by and plugin_source.in_use_by != player.player_id: + continue if plugin_source.id in player_source_ids: continue player.source_list.append(plugin_source) diff --git a/music_assistant/controllers/streams.py b/music_assistant/controllers/streams.py index 496b8b1c..1d7f13ac 100644 --- a/music_assistant/controllers/streams.py +++ b/music_assistant/controllers/streams.py @@ -8,6 +8,7 @@ the upnp callbacks and json rpc api for slimproto clients. from __future__ import annotations +import asyncio import os import urllib.parse from collections.abc import AsyncGenerator @@ -592,6 +593,7 @@ class StreamsController(CoreController): "Accept-Ranges": "none", "Content-Type": f"audio/{output_format.output_format_str}", } + resp = web.StreamResponse( status=200, reason="OK", @@ -614,12 +616,6 @@ class StreamsController(CoreController): return resp # all checks passed, start streaming! - self.logger.debug( - "Start serving audio stream for PluginSource %s (%s) to %s", - plugin_source.name, - plugin_source.id, - player.display_name, - ) async for chunk in self.get_plugin_source_stream( plugin_source_id=plugin_source_id, output_format=output_format, @@ -867,29 +863,34 @@ class StreamsController(CoreController): player = self.mass.players.get(player_id) plugin_prov: PluginProvider = self.mass.get_provider(plugin_source_id) plugin_source = plugin_prov.get_source() - if plugin_prov.in_use_by and plugin_prov.in_use_by != player_id: + if plugin_source.in_use_by and plugin_source.in_use_by != player_id: raise RuntimeError( - f"PluginSource plugin_source.name is already in use by {plugin_prov.in_use_by}" + f"PluginSource plugin_source.name is already in use by {plugin_source.in_use_by}" ) + self.logger.debug("Start streaming PluginSource %s to %s", plugin_source_id, player_id) audio_input = ( plugin_prov.get_audio_stream(player_id) if plugin_source.stream_type == StreamType.CUSTOM else plugin_source.path ) - chunk_size = int(get_chunksize(output_format, 1) / 10) player.active_source = plugin_source_id + plugin_source.in_use_by = player_id try: async for chunk in get_ffmpeg_stream( audio_input=audio_input, input_format=plugin_source.audio_format, output_format=output_format, - chunk_size=chunk_size, filter_params=player_filter_params, extra_input_args=["-re"], ): yield chunk finally: + self.logger.debug( + "Finished streaming PluginSource %s to %s", plugin_source_id, player_id + ) + await asyncio.sleep(0.5) player.active_source = player.player_id + plugin_source.in_use_by = None async def get_queue_item_stream( self, diff --git a/music_assistant/helpers/ffmpeg.py b/music_assistant/helpers/ffmpeg.py index b19ffb4e..7c1c8d83 100644 --- a/music_assistant/helpers/ffmpeg.py +++ b/music_assistant/helpers/ffmpeg.py @@ -226,9 +226,12 @@ def get_ffmpeg_args( "-ignore_unknown", "-protocol_whitelist", "file,hls,http,https,tcp,tls,crypto,pipe,data,fd,rtp,udp,concat", + "-probesize", + "8192", ] # collect input args input_args = [] + if extra_input_args: input_args += extra_input_args if input_path.startswith("http"): diff --git a/music_assistant/helpers/process.py b/music_assistant/helpers/process.py index 5becba61..2503632e 100644 --- a/music_assistant/helpers/process.py +++ b/music_assistant/helpers/process.py @@ -90,27 +90,12 @@ class AsyncProcess: async def start(self) -> None: """Perform Async init of process.""" - for attempt in range(2): - try: - self.proc = await asyncio.create_subprocess_exec( - *self._args, - stdin=asyncio.subprocess.PIPE if self._stdin is True else self._stdin, - stdout=asyncio.subprocess.PIPE if self._stdout is True else self._stdout, - stderr=asyncio.subprocess.PIPE if self._stderr is True else self._stderr, - # because we're exchanging big amounts of (audio) data with pipes - # it makes sense to extend the pipe size and (buffer) limits a bit - limit=1000000 if attempt == 0 else 65536, - pipesize=1000000 if attempt == 0 else -1, - ) - break - except PermissionError: - if attempt > 0: - raise - LOGGER.error( - "Detected that you are running the (docker) container without " - "permissive access rights. This will impact performance !" - ) - + self.proc = await asyncio.create_subprocess_exec( + *self._args, + stdin=asyncio.subprocess.PIPE if self._stdin is True else self._stdin, + stdout=asyncio.subprocess.PIPE if self._stdout is True else self._stdout, + stderr=asyncio.subprocess.PIPE if self._stderr is True else self._stderr, + ) self.logger.log( VERBOSE_LOG_LEVEL, "Process %s started with PID %s", self.name, self.proc.pid ) diff --git a/music_assistant/models/plugin.py b/music_assistant/models/plugin.py index d0023cef..e8d77461 100644 --- a/music_assistant/models/plugin.py +++ b/music_assistant/models/plugin.py @@ -56,6 +56,13 @@ class PluginSource(PlayerSource): metadata=field_options(serialize="omit", deserialize=pass_through), repr=False, ) + # in_use_by specifies the player id that is currently using this plugin (if any) + in_use_by: str | None = field( + default=None, + compare=False, + metadata=field_options(serialize="omit", deserialize=pass_through), + repr=False, + ) class PluginProvider(Provider): @@ -65,14 +72,6 @@ class PluginProvider(Provider): Plugin Provider implementations should inherit from this base model. """ - @property - def in_use_by(self) -> str | None: - """Return player id that is currently using this plugin (if any).""" - for player in self.mass.players: - if player.active_source == self.lookup_key: - return player.player_id - return None - def get_source(self) -> PluginSource: # type: ignore[return] """Get (audio)source details for this plugin.""" # Will only be called if ProviderFeature.AUDIO_SOURCE is declared diff --git a/music_assistant/providers/spotify/bin/librespot-macos-arm64 b/music_assistant/providers/spotify/bin/librespot-macos-arm64 index 730bb78b..bb8d4715 100755 Binary files a/music_assistant/providers/spotify/bin/librespot-macos-arm64 and b/music_assistant/providers/spotify/bin/librespot-macos-arm64 differ diff --git a/music_assistant/providers/spotify_connect/__init__.py b/music_assistant/providers/spotify_connect/__init__.py index 55a6233f..eadbfc38 100644 --- a/music_assistant/providers/spotify_connect/__init__.py +++ b/music_assistant/providers/spotify_connect/__init__.py @@ -272,14 +272,13 @@ class SpotifyConnectProvider(PluginProvider): # handle session connected event # this player has become the active spotify connect player # we need to start the playback - if json_data.get("event") in ("sink",) and ( - not self.in_use_by - or ((player := self.mass.players.get(self.in_use_by)) and player.state == "idle") - ): + if json_data.get("event") in ("sink", "playing") and (not self._source_details.in_use_by): # initiate playback by selecting this source on the default player + self.logger.error("Initiating playback on %s", self.mass_player_id) self.mass.create_task( self.mass.players.select_source(self.mass_player_id, self.lookup_key) ) + self._source_details.in_use_by = self.mass_player_id # parse metadata fields if "common_metadata_fields" in json_data: