From: Marcel van der Veldt Date: Fri, 7 Jun 2019 23:43:06 +0000 (+0200) Subject: queue stream fixes X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=084661a54a0eb86867b879458b41c6d6fcbdb4c2;p=music-assistant-server.git queue stream fixes --- diff --git a/music_assistant/models.py b/music_assistant/models.py index ef2a0a06..982b0a48 100755 --- a/music_assistant/models.py +++ b/music_assistant/models.py @@ -107,6 +107,7 @@ class Track(object): self.media_type = MediaType.Track self.in_library = [] self.is_lazy = False + self.uri = "" def __eq__(self, other): if not isinstance(other, self.__class__): return NotImplemented @@ -505,12 +506,13 @@ class MusicPlayer(): self.cur_item = None self.cur_item_time = 0 self.cur_queue_index = 0 + self.next_queue_index = 0 self.volume_level = 0 self.shuffle_enabled = True self.repeat_enabled = False self.muted = False - self.group_parent = None # set to id of REAL group/parent player - self.is_group = False # is this player a group player ? + self.group_parent = None + self.is_group = False self.settings = {} self.enabled = True diff --git a/music_assistant/modules/http_streamer.py b/music_assistant/modules/http_streamer.py index 2df65cd7..0d3a6fd5 100755 --- a/music_assistant/modules/http_streamer.py +++ b/music_assistant/modules/http_streamer.py @@ -4,7 +4,7 @@ import asyncio import os from utils import LOGGER, try_parse_int, get_ip, run_async_background_task, run_periodic, get_folder_size -from models import TrackQuality, MediaType +from models import TrackQuality, MediaType, PlayerState import shutil import xml.etree.ElementTree as ET import random @@ -134,6 +134,7 @@ class HTTPStreamer(): use case is enable crossfade support for chromecast devices ''' player_id = http_request.query.get('player_id') + startindex = int(http_request.query.get('startindex',0)) cancelled = threading.Event() resp = web.StreamResponse(status=200, reason='OK', @@ -145,7 +146,7 @@ class HTTPStreamer(): cancelled = threading.Event() run_async_background_task( self.mass.bg_executor, - self.__stream_queue, player_id, queue, cancelled) + self.__stream_queue, player_id, startindex, queue, cancelled) try: while True: chunk = await queue.get() @@ -160,7 +161,7 @@ class HTTPStreamer(): raise asyncio.CancelledError() return resp - async def __stream_queue(self, player_id, buffer, cancelled): + async def __stream_queue_org(self, player_id, startindex, buffer, cancelled): ''' start streaming all queue tracks ''' # TODO: get correct queue index and implement reporting of position sample_rate = self.mass.config['player_settings'][player_id]['max_sample_rate'] @@ -179,14 +180,13 @@ class HTTPStreamer(): await buffer.put(b'') # indicate EOF asyncio.create_task(fill_buffer()) + queue_index = startindex last_fadeout_data = None while True: - # get current track in queue - queue_tracks = await self.mass.player.player_queue(player_id, 0, 10000) - player = self.mass.player._players[player_id] - queue_index = player.cur_queue_index + # get the (next) track in queue try: - queue_track = queue_tracks[queue_index] + queue_tracks = await self.mass.player.player_queue(player_id, queue_index, queue_index+1) + queue_track = queue_tracks[0] except IndexError: LOGGER.info("queue index out of range or end reached") break @@ -194,18 +194,22 @@ class HTTPStreamer(): params = urllib.parse.parse_qs(queue_track.uri.split('?')[1]) track_id = params['track_id'][0] provider = params['provider'][0] - LOGGER.info("Stream queue track: %s - %s" % (track_id, queue_track.name)) + LOGGER.info("Start Streaming queue track: %s - %s" % (track_id, queue_track.name)) audiodata = await self.__get_raw_audio(track_id, provider, sample_rate) fade_bytes = int(sample_rate * 4 * 2 * fade_length) LOGGER.debug("total bytes in audio_data: %s - fade_bytes: %s" % (len(audiodata),fade_bytes)) - - # get fade in part - args = 'sox --ignore-length -t %s - -t %s - fade t %s' % (pcm_args, pcm_args, fade_length) - process = await asyncio.create_subprocess_shell(args, - stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE) - fade_in_part, stderr = await process.communicate(audiodata[:fade_bytes]) - LOGGER.debug("Got %s bytes in memory for fadein_part after sox" % len(fade_in_part)) + + # report start stream of current queue index + self.mass.event_loop.create_task(self.mass.player.player_queue_stream_move(player_id, queue_index)) + queue_index += 1 + if last_fadeout_data: + # get fade in part + args = 'sox --ignore-length -t %s - -t %s - fade t %s' % (pcm_args, pcm_args, fade_length) + process = await asyncio.create_subprocess_shell(args, + stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE) + fade_in_part, stderr = await process.communicate(audiodata[:fade_bytes]) + LOGGER.debug("Got %s bytes in memory for fadein_part after sox" % len(fade_in_part)) # perform crossfade with previous fadeout samples fadeinfile = MemoryTempfile(fallback=True).NamedTemporaryFile(buffering=0) fadeinfile.write(fade_in_part) @@ -225,9 +229,8 @@ class HTTPStreamer(): last_fadeout_data = None else: # simply put the fadein part in the final file - sox_proc.stdin.write(fade_in_part) + sox_proc.stdin.write(audiodata[:fade_bytes]) await sox_proc.stdin.drain() - del fade_in_part # feed the middle part into the main sox sox_proc.stdin.write(audiodata[fade_bytes:-fade_bytes]) @@ -242,14 +245,14 @@ class HTTPStreamer(): # cleanup audio data del audiodata + LOGGER.info("Queued Streaming queue track: %s - %s" % (track_id, queue_track.name)) + # wait for the queue to consume the data - while buffer.qsize() > 5 and not cancelled.is_set(): + while buffer.qsize() > 1 and not cancelled.is_set(): await asyncio.sleep(1) if cancelled.is_set(): break - # assume end of track and increase queue_index - player.cur_queue_index += 1 - await self.mass.player.trigger_update(player_id) + LOGGER.info("Finished Streaming queue track: %s - %s" % (track_id, queue_track.name)) # end of queue reached, pass last fadeout bits to final output if last_fadeout_data: @@ -259,7 +262,7 @@ class HTTPStreamer(): await sox_proc.wait() LOGGER.info("streaming of queue for player %s completed" % player_id) - async def __get_raw_audio(self, track_id, provider, sample_rate=96000): + async def __get_raw_audio_org(self, track_id, provider, sample_rate=96000): ''' get raw pcm data for a track upsampled to given sample_rate packed as wav ''' audiodata = b'' cachefile = self.__get_track_cache_filename(track_id, provider) @@ -281,11 +284,163 @@ class HTTPStreamer(): stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE) asyncio.get_event_loop().create_task( self.__fill_audio_buffer(process.stdin, track_id, provider, input_content_type)) - #await process.wait() audiodata, stderr = await process.communicate() LOGGER.debug("__get_raw_audio for track_id %s completed" % (track_id)) return audiodata + async def __stream_queue(self, player_id, startindex, buffer, cancelled): + ''' start streaming all queue tracks ''' + # TODO: get correct queue index and implement reporting of position + sample_rate = self.mass.config['player_settings'][player_id]['max_sample_rate'] + fade_length = self.mass.config['player_settings'][player_id]["crossfade_duration"] + pcm_args = 'raw -b 32 -c 2 -e signed-integer -r %s' % sample_rate + args = 'sox -t %s - -t flac -C 2 -' % pcm_args + sox_proc = await asyncio.create_subprocess_shell(args, + stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE) + + async def fill_buffer(): + while not sox_proc.stdout.at_eof(): + chunk = await sox_proc.stdout.read(256000) + if not chunk: + break + await buffer.put(chunk) + await buffer.put(b'') # indicate EOF + asyncio.create_task(fill_buffer()) + + queue_index = startindex + last_fadeout_data = b'' + while True: + # get the (next) track in queue + try: + queue_tracks = await self.mass.player.player_queue(player_id, queue_index, queue_index+1) + queue_track = queue_tracks[0] + except IndexError: + LOGGER.info("queue index out of range or end reached") + break + + params = urllib.parse.parse_qs(queue_track.uri.split('?')[1]) + track_id = params['track_id'][0] + provider = params['provider'][0] + LOGGER.info("Start Streaming queue track: %s - %s" % (track_id, queue_track.name)) + fade_bytes = int(sample_rate * 4 * 2 * fade_length) + cachefile = self.__get_track_cache_filename(track_id, provider) + if os.path.isfile(cachefile): + # get track length from cachefile + args = 'soxi -d "%s"' % cachefile + process = await asyncio.create_subprocess_shell(args, + stderr=asyncio.subprocess.PIPE) + stdout, stderr = await process.communicate() + hours = stderr.split(":")[0] + minutes = stderr.split(":")[1] + seconds = stderr.split(":")[2] + total_chunks = hours*60*60 + minutes*60 + seconds + else: + total_chunks = int(queue_track.duration) + cur_chunk = 0 + + # report start stream of current queue index + self.mass.event_loop.create_task(self.mass.player.player_queue_stream_move(player_id, queue_index)) + queue_index += 1 + fade_in_part = b'' + + async for chunk in self.__get_raw_audio(track_id, provider, sample_rate): + cur_chunk += 1 + + if cur_chunk <= fade_length and not last_fadeout_data: + # fade-in part but this is the first track so just pass it to the final file + sox_proc.stdin.write(chunk) + await sox_proc.stdin.drain() + elif (cur_chunk < fade_length) and last_fadeout_data: + # need to have fade_length of chunks for the fade-in data + fade_in_part += chunk + elif fade_in_part and last_fadeout_data: + fade_in_part += chunk + # perform crossfade with previous fadeout samples + args = 'sox --ignore-length -t %s - -t %s - reverse fade t %s reverse' % (pcm_args, pcm_args, fade_length) + process = await asyncio.create_subprocess_shell(args, + stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE) + last_fadeout_data, stderr = await process.communicate(last_fadeout_data) + LOGGER.info("Got %s bytes in memory for fade_out_part after sox" % len(last_fadeout_data)) + args = 'sox --ignore-length -t %s - -t %s - fade t %s' % (pcm_args, pcm_args, fade_length) + process = await asyncio.create_subprocess_shell(args, + stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE) + fade_in_part, stderr = await process.communicate(fade_in_part) + LOGGER.info("Got %s bytes in memory for fadein_part after sox" % len(fade_in_part)) + fadeinfile = MemoryTempfile(fallback=True).NamedTemporaryFile(buffering=0) + fadeinfile.write(fade_in_part) + fadeoutfile = MemoryTempfile(fallback=True).NamedTemporaryFile(buffering=0) + fadeoutfile.write(last_fadeout_data) + args = 'sox -m -v 1.0 -t %s %s -v 1.0 -t %s %s -t %s -' % (pcm_args, fadeoutfile.name, pcm_args, fadeinfile.name, pcm_args) + process = await asyncio.create_subprocess_shell(args, + stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE) + crossfade_part, stderr = await process.communicate(fade_in_part) + LOGGER.info("Got %s bytes in memory for crossfade_part after sox" % len(crossfade_part)) + sox_proc.stdin.write(crossfade_part) + await sox_proc.stdin.drain() + fadeinfile.close() + fadeoutfile.close() + del crossfade_part + fade_in_part = None + last_fadeout_data = b'' + elif (cur_chunk > fade_length) and (cur_chunk < (total_chunks-fade_length)): + # middle part of the track + sox_proc.stdin.write(chunk) + await sox_proc.stdin.drain() + else: + # fade out part + last_fadeout_data += chunk + + LOGGER.info("Queued Streaming queue track: %s - %s" % (track_id, queue_track.name)) + + #wait for the queue to consume the data + while buffer.qsize() > 1 and not cancelled.is_set(): + await asyncio.sleep(1) + if cancelled.is_set(): + break + LOGGER.info("Finished Streaming queue track: %s - %s" % (track_id, queue_track.name)) + + # end of queue reached, pass last fadeout bits to final output + if last_fadeout_data: + sox_proc.stdin.write(last_fadeout_data) + await sox_proc.stdin.drain() + sox_proc.stdin.close() + await sox_proc.wait() + LOGGER.info("streaming of queue for player %s completed" % player_id) + + async def __get_raw_audio(self, track_id, provider, sample_rate=96000): + ''' get raw pcm data for a track upsampled to given sample_rate packed as wav ''' + cachefile = self.__get_track_cache_filename(track_id, provider) + pcm_args = 'raw -b 32 -c 2 -e signed-integer' + if self.mass.config['base']['http_streamer']['volume_normalisation']: + gain_correct = await self.__get_track_gain_correct(track_id, provider) + else: + gain_correct = -6 # always need some headroom for upsampling and crossfades + if os.path.isfile(cachefile): + # we have a cache file for this track which we can use + args = 'sox -t flac "%s" -t %s - vol %s dB rate -v %s' % (cachefile, pcm_args, gain_correct, sample_rate) + process = await asyncio.create_subprocess_shell(args, stdout=asyncio.subprocess.PIPE) + else: + # stream from provider + input_content_type = await self.mass.music.providers[provider].get_stream_content_type(track_id) + assert(input_content_type) + args = 'sox -t %s - -t %s - vol %s dB rate -v %s' % (input_content_type, pcm_args, gain_correct, sample_rate) + process = await asyncio.create_subprocess_shell(args, + stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE) + asyncio.get_event_loop().create_task( + self.__fill_audio_buffer(process.stdin, track_id, provider, input_content_type)) + # put chunks from stdout into queue + chunksize = int(sample_rate * (32/8) * 2) # 1 second + while not process.stdout.at_eof(): + try: + chunk = await process.stdout.readexactly(chunksize) + except asyncio.streams.IncompleteReadError: + chunk = await process.stdout.read(chunksize) + if not chunk: + break + yield chunk + await process.wait() + LOGGER.info("__get_raw_audio for track_id %s completed" % (track_id)) + async def __get_audio_stream(self, audioqueue, track_id, provider, player_id=None, cancelled=None): ''' get audio stream from provider and apply additional effects/processing where/if needed''' cachefile = self.__get_track_cache_filename(track_id, provider) diff --git a/music_assistant/modules/player.py b/music_assistant/modules/player.py index 33250db9..b2ac73ad 100755 --- a/music_assistant/modules/player.py +++ b/music_assistant/modules/player.py @@ -193,6 +193,7 @@ class Player(): player_details.cur_item_time = parent_player.cur_item_time player_details.cur_item = parent_player.cur_item player_details.state = parent_player.state + # handle group volume/power setting if player_details.is_group: player_childs = [item for item in self._players.values() if item.group_parent == player_id] @@ -389,6 +390,16 @@ class Player(): player_prov = self.providers[player.player_provider] return await player_prov.player_queue(player_id, offset=offset, limit=limit) + async def player_queue_index(self, player_id): + ''' get current index of the player's queue ''' + return self._players[player_id].cur_queue_index + + async def player_queue_stream_move(self, player_id, new_index): + ''' called by our queue streamer when it's loading a new track ''' + new_index = int(new_index) + player = self._players[player_id] + return await self.providers[player.player_provider].player_queue_stream_move(player_id, new_index) + def load_providers(self): ''' dynamically load providers ''' for item in os.listdir(MODULES_PATH): diff --git a/music_assistant/modules/playerproviders/chromecast.py b/music_assistant/modules/playerproviders/chromecast.py index 0132a6d7..da6e7986 100644 --- a/music_assistant/modules/playerproviders/chromecast.py +++ b/music_assistant/modules/playerproviders/chromecast.py @@ -55,7 +55,6 @@ class ChromecastProvider(PlayerProvider): self.supported_musicproviders = ['http'] asyncio.ensure_future(self.__discover_chromecasts()) - ### Provider specific implementation ##### async def player_config_entries(self): @@ -85,11 +84,16 @@ class ChromecastProvider(PlayerProvider): elif cmd == 'stop': self._chromecasts[player_id].media_controller.stop() elif cmd == 'next': - self.mass.player._players[player_id].cur_queue_index +=1 - self._chromecasts[player_id].media_controller.queue_next() + enable_crossfade = self.mass.config['player_settings'][player_id]["crossfade_duration"] > 0 + if enable_crossfade: + await self.__play_stream_queue(player_id, self._players[player_id].cur_queue_index+1) + else: + self._chromecasts[player_id].media_controller.queue_next() elif cmd == 'previous': - self.mass.player._players[player_id].cur_queue_index -=1 - self._chromecasts[player_id].media_controller.queue_prev() + if enable_crossfade: + await self.__play_stream_queue(player_id, self._players[player_id].cur_queue_index-1) + else: + self._chromecasts[player_id].media_controller.queue_prev() elif cmd == 'power' and cmd_args == 'off': self._players[player_id].powered = False self._chromecasts[player_id].media_controller.stop() # power is not supported so send stop instead @@ -116,15 +120,22 @@ class ChromecastProvider(PlayerProvider): ''' castplayer = self._chromecasts[player_id] cur_queue_index = await self.__get_cur_queue_index(player_id) + enable_crossfade = self.mass.config['player_settings'][player_id]["crossfade_duration"] > 0 if queue_opt == 'replace' or not self._player_queue[player_id]: # overwrite queue with new items self._player_queue[player_id] = media_items - await self.__queue_load(player_id, self._player_queue[player_id], 0) + if enable_crossfade: + await self.__play_stream_queue(player_id, cur_queue_index) + else: + await self.__queue_load(player_id, self._player_queue[player_id], 0) elif queue_opt == 'play': # replace current item with new item(s) self._player_queue[player_id] = self._player_queue[player_id][:cur_queue_index] + media_items + self._player_queue[player_id][cur_queue_index+1:] - await self.__queue_load(player_id, self._player_queue[player_id], cur_queue_index) + if enable_crossfade: + await self.__play_stream_queue(player_id, cur_queue_index) + else: + await self.__queue_load(player_id, self._player_queue[player_id], cur_queue_index) elif queue_opt == 'next': # insert new items at current index +1 if len(self._player_queue[player_id]) > cur_queue_index+1: @@ -132,25 +143,36 @@ class ChromecastProvider(PlayerProvider): else: old_next_uri = None self._player_queue[player_id] = self._player_queue[player_id][:cur_queue_index+1] + media_items + self._player_queue[player_id][cur_queue_index+1:] - # find out the itemID of the next item in CC queue - insert_at_item_id = None - if old_next_uri: - for item in castplayer.media_controller.queue_items: - if item['media']['contentId'] == old_next_uri: - insert_at_item_id = item['itemId'] - await self.__queue_insert(player_id, media_items, insert_at_item_id) + if not enable_crossfade: + # find out the itemID of the next item in CC queue + insert_at_item_id = None + if old_next_uri: + for item in castplayer.media_controller.queue_items: + if item['media']['contentId'] == old_next_uri: + insert_at_item_id = item['itemId'] + await self.__queue_insert(player_id, media_items, insert_at_item_id) elif queue_opt == 'add': # add new items at end of queue self._player_queue[player_id] = self._player_queue[player_id] + media_items - await self.__queue_insert(player_id, media_items) + if not enable_crossfade: + await self.__queue_insert(player_id, media_items) + + async def player_queue_stream_move(self, player_id, new_index): + ''' called by the queue streamer when it's loading a new track ''' + self._players[player_id].cur_queue_index = new_index + # trigger update + chromecast = self._chromecasts[player_id] + mediastatus = chromecast.media_controller.status + await self.__handle_player_state(chromecast, mediastatus=mediastatus) + LOGGER.info("player_queue_stream_move") ### Provider specific (helper) methods ##### async def __get_cur_queue_index(self, player_id): ''' retrieve index of current item in the player queue ''' enable_crossfade = self.mass.config['player_settings'][player_id]["crossfade_duration"] > 0 - if enable_crossfade and player_id in self.mass.player._players: - return self.mass.player._players[player_id].cur_queue_index + if enable_crossfade: + return self._players[player_id].cur_queue_index cur_index = 0 for index, track in enumerate(self._player_queue[player_id]): if track.uri == self._chromecasts[player_id].media_controller.status.content_id: @@ -162,7 +184,7 @@ class ChromecastProvider(PlayerProvider): ''' load queue on player with given queue items ''' castplayer = self._chromecasts[player_id] player = self._players[player_id] - queue_items = await self.__create_queue_items(new_tracks[:50], player_id=player_id) + queue_items = await self.__create_queue_items(new_tracks[:50]) self.mass.player._players[player_id].cur_queue_index = 0 queuedata = { "type": 'QUEUE_LOAD', @@ -178,10 +200,17 @@ class ChromecastProvider(PlayerProvider): await self.__queue_insert(player_id, new_tracks[51:]) await asyncio.sleep(0.2) + async def __play_stream_queue(self, player_id, startindex=0): + ''' tell the cast player to stream our special queue (crossfaded) stream ''' + castplayer = self._chromecasts[player_id] + uri = 'http://%s:%s/stream_queue?player_id=%s&startindex=%s'% ( + self.mass.player.local_ip, self.mass.config['base']['web']['http_port'], player_id, startindex) + castplayer.play_media(uri, 'audio/flac') + async def __queue_insert(self, player_id, new_tracks, insert_before=None): ''' insert item into the player queue ''' castplayer = self._chromecasts[player_id] - queue_items = await self.__create_queue_items(new_tracks, player_id=player_id) + queue_items = await self.__create_queue_items(new_tracks) for chunk in chunks(queue_items, 50): queuedata = { "type": 'QUEUE_INSERT', @@ -210,26 +239,20 @@ class ChromecastProvider(PlayerProvider): async def __resume_queue(self, player_id): ''' resume queue play after power off ''' - queue_index = await self.__get_cur_queue_index(player_id) - LOGGER.info('resume queue at index %s' % queue_index) + LOGGER.info('resuming queue....') tracks = self._player_queue[player_id] - await self.__queue_load(player_id, tracks, queue_index) + await self.play_media(player_id, tracks) - async def __create_queue_items(self, tracks, player_id): + async def __create_queue_items(self, tracks): ''' create list of CC queue items from tracks ''' queue_items = [] for track in tracks: - queue_item = await self.__create_queue_item(track, player_id) + queue_item = await self.__create_queue_item(track) queue_items.append(queue_item) return queue_items - async def __create_queue_item(self, track, player_id): + async def __create_queue_item(self, track): '''create queue item from track info ''' - enable_crossfade = self.mass.config['player_settings'][player_id]["crossfade_duration"] > 0 - if enable_crossfade: - uri = 'http://%s:%s/stream_queue?player_id=%s'% (self.mass.player.local_ip, self.mass.config['base']['web']['http_port'], player_id) - else: - uri = track.uri return { 'autoplay' : True, 'preloadTime' : 10, @@ -237,7 +260,7 @@ class ChromecastProvider(PlayerProvider): 'startTime' : 0, 'activeTrackIds' : [], 'media': { - 'contentId': uri, + 'contentId': track.uri, 'customData': { 'provider': track.provider, 'uri': track.uri, @@ -285,9 +308,21 @@ class ChromecastProvider(PlayerProvider): player.state = PlayerState.Paused else: player.state = PlayerState.Stopped - player.cur_item = await self.__parse_track(mediastatus) - player.cur_item_time = chromecast.media_controller.status.adjusted_current_time - player.cur_queue_index = await self.__get_cur_queue_index(player_id) + if not 'stream_queue' in mediastatus.content_id: + player.cur_item = await self.__parse_track(mediastatus) + player.cur_item_time = mediastatus.adjusted_current_time + player.cur_queue_index = await self.__get_cur_queue_index(player_id) + else: + # try to work out the current time + # player is playing a constant stream of the queue so we need to do this the hard way + cur_queue_index = player.cur_queue_index + player.cur_item = self._player_queue[player_id][cur_queue_index] + cur_time = mediastatus.adjusted_current_time + while cur_time > player.cur_item.duration: + cur_queue_index -=1 + prev_track = self._player_queue[player_id][cur_queue_index] + cur_time -= prev_track.duration + player.cur_item_time = cur_time await self.mass.player.update_player(player) async def __parse_track(self, mediastatus): @@ -319,11 +354,6 @@ class ChromecastProvider(PlayerProvider): track_id = params['track_id'][0] provider = params['provider'][0] track = await self.mass.music.providers[provider].track(track_id) - elif uri.startswith('http') and '/stream_queue' in uri: - params = urllib.parse.parse_qs(uri.split('?')[1]) - player_id = params['player_id'][0] - queue_index = await self.__get_cur_queue_index(player_id) - track = self._player_queue[player_id][queue_index] return track async def __handle_group_members_update(self, mz, added_player=None, removed_player=None):