1- """
2- VBAN protocol receiver plugin for Music Assistant.
3-
4- We tie a single player to a single VBAN session name.
5- The provider has multi instance support,
6- so multiple players can be linked to multiple VBAN streams.
7- """
1+ """VBAN protocol receiver plugin for Music Assistant."""
82
93from __future__ import annotations
104
148from contextlib import suppress
159from typing import TYPE_CHECKING , cast
1610
17- from aiovban .asyncio import AsyncVBANClient
1811from aiovban .asyncio .util import BackPressureStrategy
1912from aiovban .enums import VBANSampleRate
2013from music_assistant_models .config_entries import ConfigEntry , ConfigValueOption
2114from music_assistant_models .enums import (
2215 ConfigEntryType ,
2316 ContentType ,
24- MediaType ,
2517 ProviderFeature ,
2618 StreamType ,
2719)
2820from music_assistant_models .errors import SetupFailedError
2921from music_assistant_models .media_items import AudioFormat
22+ from music_assistant_models .streamdetails import StreamMetadata
3023
3124from music_assistant .constants import (
3225 CONF_BIND_IP ,
3629from music_assistant .helpers .util import (
3730 get_ip_addresses ,
3831)
39- from music_assistant .models .player import PlayerMedia
4032from music_assistant .models .plugin import PluginProvider , PluginSource
4133
34+ from .vban import AsyncVBANClientMod
35+
4236if TYPE_CHECKING :
4337 from aiovban .asyncio .device import VBANDevice
4438 from aiovban .asyncio .streams import VBANIncomingStream
@@ -189,8 +183,8 @@ def __init__(
189183 self ._pcm_audio_format : str = cast ("str" , self .config .get_value (CONF_PCM_AUDIO_FORMAT ))
190184 self ._pcm_sample_rate : int = cast ("int" , self .config .get_value (CONF_PCM_SAMPLE_RATE ))
191185
192- self ._vban_receiver : AsyncVBANClient | None = None
193- self ._vban_device : VBANDevice | None = None
186+ self ._vban_receiver : AsyncVBANClientMod | None = None
187+ self ._vban_sender : VBANDevice | None = None
194188 self ._vban_stream : VBANIncomingStream | None = None
195189
196190 self ._source_details = PluginSource (
@@ -207,11 +201,9 @@ def __init__(
207201 bit_depth = _get_supported_pcm_formats ()[self ._pcm_audio_format ],
208202 channels = 2 ,
209203 ),
210- metadata = PlayerMedia (
211- "VBAN Receiver" ,
212- artist = self ._sender_host ,
204+ metadata = StreamMetadata (
213205 title = self ._vban_stream_name ,
214- media_type = MediaType . PLUGIN_SOURCE ,
206+ artist = self . _sender_host ,
215207 ),
216208 stream_type = StreamType .CUSTOM ,
217209 )
@@ -228,36 +220,31 @@ def instance_name_postfix(self) -> str | None:
228220
229221 async def handle_async_init (self ) -> None :
230222 """Handle async initialization of the provider."""
231- self ._vban_receiver = AsyncVBANClient (ignore_audio_streams = False )
223+ self ._vban_receiver = AsyncVBANClientMod (ignore_audio_streams = False )
232224 try :
233- result = await self ._vban_receiver .listen (self ._bind_ip , self ._bind_port )
225+ self ._udp_socket_task = asyncio .create_task (
226+ self ._vban_receiver .listen (self ._bind_ip , self ._bind_port )
227+ )
234228 except OSError as err :
235229 raise SetupFailedError (f"Failed to start VBAN receiver plugin: { err } " ) from err
236- else :
237- self ._udp_socket_fut = result
238230
239- self ._vban_device = self ._vban_receiver .register_device (self ._sender_host , self . _bind_port )
240- if self ._vban_device :
241- self ._vban_stream = self ._vban_device .receive_stream (
231+ self ._vban_sender = self ._vban_receiver .register_device (self ._sender_host )
232+ if self ._vban_sender :
233+ self ._vban_stream = self ._vban_sender .receive_stream (
242234 self ._vban_stream_name , back_pressure_strategy = BackPressureStrategy .DRAIN_OLDEST
243235 )
244236
245237 async def unload (self , is_removed : bool = False ) -> None :
246238 """Handle close/cleanup of the provider."""
247239 self .logger .debug ("Unloading plugin" )
248240 if self ._vban_receiver :
249- self .logger .info ("Closing UDP transport" )
250- # Can raise an uncatchable exception due to bug in aiovban library.
241+ self .logger .debug ("Closing UDP transport" )
251242 self ._vban_receiver .close ()
252-
253- if self ._udp_socket_fut and not self ._udp_socket_fut .done ():
254- self ._udp_socket_fut .cancel ()
255243 with suppress (asyncio .CancelledError ):
256- await self ._udp_socket_fut
244+ await self ._udp_socket_task
257245
258- self ._udp_socket_fut = None
259246 self ._vban_receiver = None
260- self ._vban_device = None
247+ self ._vban_sender = None
261248 self ._vban_stream = None
262249 await asyncio .sleep (0.1 )
263250
@@ -268,21 +255,23 @@ def get_source(self) -> PluginSource:
268255 async def get_audio_stream (self , player_id : str ) -> AsyncGenerator [bytes , None ]:
269256 """Yield raw PCM chunks from the VBANIncomingStream queue."""
270257 self .logger .debug (
271- "Sending VBAN PCM audio stream for Player: %s//Stream: %s//Config: %s" ,
258+ "Getting VBAN PCM audio stream for Player: %s//Stream: %s//Config: %s" ,
272259 player_id ,
273260 self ._vban_stream_name ,
274- self ._source_details .audio_format .output_format_str , # type: ignore[union-attr]
261+ self ._source_details .audio_format .output_format_str ,
275262 )
276263 while (
277- self ._source_details .in_use_by and self ._vban_stream and not self ._udp_socket_fut .done ()
264+ self ._source_details .in_use_by
265+ and self ._vban_stream
266+ and not self ._udp_socket_task .done ()
278267 ):
279268 try :
280269 packet = await self ._vban_stream .get_packet ()
281270 except asyncio .QueueShutDown : # type: ignore[attr-defined]
282271 self .logger .error (
283272 "Found VBANIncomingStream queue shut down when attempting to get VBAN packet"
284273 )
285- raise
274+ break
286275
287276 # Skip processing full null packets.
288277 # pipewire vban-send module constantly sends full null VBAN packets when a "Stream"
0 commit comments