fix some issues with chromecasts
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Wed, 4 Aug 2021 12:47:25 +0000 (14:47 +0200)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Wed, 4 Aug 2021 12:47:25 +0000 (14:47 +0200)
music_assistant/managers/players.py
music_assistant/providers/chromecast/__init__.py
music_assistant/providers/chromecast/helpers.py [new file with mode: 0644]
music_assistant/providers/chromecast/models.py [deleted file]
music_assistant/providers/chromecast/player.py

index b9f99e3e684cadd6e58dbe84cc747f415cca0292..1b2cccac17201d16a52eeeda0604387d4895ff23 100755 (executable)
@@ -306,9 +306,10 @@ class PlayerManager:
         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)
+        if player_queue.queue_id != player_id and not player.calculated_state.powered:
+            # only force player on if its not the actual queue player
+            await self.cmd_power_on(player_id)
         # a single item or list of items may be provided
         if not isinstance(items, list):
             items = [items]
@@ -381,10 +382,12 @@ class PlayerManager:
         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 player_queue.queue_id != player_id and not player.calculated_state.powered:
+            # only force player on if its not the actual queue player
+            await self.cmd_power_on(player_id)
+        # load item into the queue
         queue_item = player_queue.create_queue_item(
             item_id=uri, provider="url", name=uri, uri=uri
         )
@@ -437,6 +440,8 @@ class PlayerManager:
                     player.calculated_state.name,
                 )
                 return
+        if player_queue.queue_id != player_id and not player.calculated_state.powered:
+            # only force player on if its not the actual queue player
             await self.cmd_power_on(player_id)
         # snapshot the (active) queue
         prev_queue_items = player_queue.items
index e6b0dc2694e1d207e0537d6d2cf4ca8a1907d06d..dcabd589cbdabc5b3793e868394147653238ede0 100644 (file)
@@ -1,7 +1,7 @@
 """ChromeCast playerprovider."""
 
 import logging
-from typing import List
+from typing import List, Optional
 
 import pychromecast
 from music_assistant.helpers.util import create_task
@@ -10,7 +10,7 @@ from music_assistant.models.provider import PlayerProvider
 from pychromecast.controllers.multizone import MultizoneManager
 
 from .const import PROV_ID, PROV_NAME, PROVIDER_CONFIG_ENTRIES
-from .models import ChromecastInfo
+from .helpers import DEFAULT_PORT, ChromecastInfo
 from .player import ChromecastPlayer
 
 LOGGER = logging.getLogger(PROV_ID)
@@ -29,8 +29,7 @@ class ChromecastProvider(PlayerProvider):
     def __init__(self, *args, **kwargs):
         """Initialize."""
         self.mz_mgr = MultizoneManager()
-        self._listener = None
-        self._browser = None
+        self._browser: Optional[pychromecast.discovery.CastBrowser] = None
         super().__init__(*args, **kwargs)
 
     @property
@@ -50,18 +49,16 @@ class ChromecastProvider(PlayerProvider):
 
     async def on_start(self) -> bool:
         """Handle initialization of the provider based on config."""
-        self._listener = pychromecast.CastListener(
-            self.__chromecast_add_update_callback,
-            self.__chromecast_remove_callback,
-            self.__chromecast_add_update_callback,
+        self._browser = pychromecast.discovery.CastBrowser(
+            pychromecast.discovery.SimpleCastListener(
+                add_callback=self._discover_chromecast,
+                remove_callback=self._remove_chromecast,
+                update_callback=self._discover_chromecast,
+            ),
+            self.mass.zeroconf,
         )
-
-        def start_discovery():
-            self._browser = pychromecast.discovery.start_discovery(
-                self._listener, self.mass.zeroconf
-            )
-
-        create_task(start_discovery)
+        # start discovery in executor
+        create_task(self._browser.start_discovery)
         return True
 
     async def on_stop(self):
@@ -69,44 +66,42 @@ class ChromecastProvider(PlayerProvider):
         if not self._browser:
             return
         # stop discovery
-        create_task(pychromecast.stop_discovery, self._browser)
-
-    def __chromecast_add_update_callback(self, cast_uuid, cast_service_name):
-        """Handle zeroconf discovery of a new or updated chromecast."""
-        # pylint: disable=unused-argument
-        service = self._listener.services[cast_uuid]
-        cast_info = ChromecastInfo(
-            services=service[0],
-            uuid=service[1],
-            model_name=service[2],
-            friendly_name=service[3],
-            host=service[4],
-            port=service[5],
-        )
-        cast_info.fill_out_missing_chromecast_info(self.mass.zeroconf)
-        if cast_info.uuid is None:
-            return  # Discovered chromecast without uuid?
-        player_id = cast_info.uuid
-        LOGGER.debug(
-            "Chromecast discovered: %s (%s)", cast_info.friendly_name, player_id
+        create_task(self._browser.stop_discovery)
+
+    def _discover_chromecast(self, uuid, _):
+        """Discover a Chromecast."""
+        device_info = self._browser.devices[uuid]
+
+        info = ChromecastInfo(
+            services=device_info.services,
+            uuid=device_info.uuid,
+            model_name=device_info.model_name,
+            friendly_name=device_info.friendly_name,
+            is_audio_group=device_info.port != DEFAULT_PORT,
         )
+
+        if info.uuid is None:
+            LOGGER.error("Discovered chromecast without uuid %s", info)
+            return
+
+        info = info.fill_out_missing_chromecast_info(self.mass.zeroconf)
+        if info.is_dynamic_group:
+            LOGGER.warning("Discovered dynamic cast group which will be ignored.")
+            return
+
+        LOGGER.debug("Discovered new or updated chromecast %s", info)
+        player_id = str(info.uuid)
         player = self.mass.players.get_player(player_id)
         if not player:
-            # cast players may reappear with new uuid, try to handle that
-            player = self.mass.players.get_player_by_name(
-                cast_info.friendly_name, PROV_ID
-            )
-        if not player:
-            player = ChromecastPlayer(self.mass, cast_info)
+            player = ChromecastPlayer(self.mass, info)
         # if player was already added, the player will take care of reconnects itself.
-        player.set_cast_info(cast_info)
+        player.set_cast_info(info)
         create_task(self.mass.players.add_player(player))
 
-    @staticmethod
-    def __chromecast_remove_callback(cast_uuid, cast_service_name, cast_service):
-        """Handle a Chromecast removed event."""
+    def _remove_chromecast(self, uuid, service, cast_info):
+        """Handle zeroconf discovery of a removed chromecast."""
         # pylint: disable=unused-argument
-        player_id = str(cast_service[1])
-        friendly_name = cast_service[3]
+        player_id = str(service[1])
+        friendly_name = service[3]
         LOGGER.debug("Chromecast removed: %s - %s", friendly_name, player_id)
         # we ignore this event completely as the Chromecast socket client handles this itself
diff --git a/music_assistant/providers/chromecast/helpers.py b/music_assistant/providers/chromecast/helpers.py
new file mode 100644 (file)
index 0000000..5b8606f
--- /dev/null
@@ -0,0 +1,179 @@
+"""Helpers to deal with Cast devices."""
+from __future__ import annotations
+
+from typing import Optional
+
+import attr
+from pychromecast import dial
+from pychromecast.const import CAST_MANUFACTURERS
+
+DEFAULT_PORT = 8009
+
+
+@attr.s(slots=True, frozen=True)
+class ChromecastInfo:
+    """Class to hold all data about a chromecast for creating connections.
+
+    This also has the same attributes as the mDNS fields by zeroconf.
+    """
+
+    services: set | None = attr.ib()
+    uuid: str | None = attr.ib(
+        converter=attr.converters.optional(str), default=None
+    )  # always convert UUID to string if not None
+    _manufacturer = attr.ib(type=Optional[str], default=None)
+    model_name: str = attr.ib(default="")
+    friendly_name: str | None = attr.ib(default=None)
+    is_audio_group = attr.ib(type=Optional[bool], default=False)
+    is_dynamic_group = attr.ib(type=Optional[bool], default=None)
+
+    @property
+    def is_information_complete(self) -> bool:
+        """Return if all information is filled out."""
+        want_dynamic_group = self.is_audio_group
+        have_dynamic_group = self.is_dynamic_group is not None
+        have_all_except_dynamic_group = all(
+            attr.astuple(
+                self,
+                filter=attr.filters.exclude(
+                    attr.fields(ChromecastInfo).is_dynamic_group
+                ),
+            )
+        )
+        return have_all_except_dynamic_group and (
+            not want_dynamic_group or have_dynamic_group
+        )
+
+    @property
+    def manufacturer(self) -> str | None:
+        """Return the manufacturer."""
+        if self._manufacturer:
+            return self._manufacturer
+        if not self.model_name:
+            return None
+        return CAST_MANUFACTURERS.get(self.model_name.lower(), "Google Inc.")
+
+    def fill_out_missing_chromecast_info(self, zconf) -> ChromecastInfo:
+        """Return a new ChromecastInfo object with missing attributes filled in.
+
+        Uses blocking HTTP / HTTPS.
+        """
+        if self.is_information_complete:
+            # We have all information, no need to check HTTP API.
+            return self
+
+        # Fill out missing group information via HTTP API.
+        if self.is_audio_group:
+            is_dynamic_group = False
+            http_group_status = None
+            if self.uuid:
+                http_group_status = dial.get_multizone_status(
+                    None,
+                    services=self.services,
+                    zconf=zconf,
+                )
+                if http_group_status is not None:
+                    is_dynamic_group = any(
+                        str(g.uuid) == self.uuid
+                        for g in http_group_status.dynamic_groups
+                    )
+
+            return ChromecastInfo(
+                services=self.services,
+                uuid=self.uuid,
+                friendly_name=self.friendly_name,
+                model_name=self.model_name,
+                is_audio_group=True,
+                is_dynamic_group=is_dynamic_group,
+            )
+
+        # Fill out some missing information (friendly_name, uuid) via HTTP dial.
+        http_device_status = dial.get_device_status(
+            None, services=self.services, zconf=zconf
+        )
+        if http_device_status is None:
+            # HTTP dial didn't give us any new information.
+            return self
+
+        return ChromecastInfo(
+            services=self.services,
+            uuid=(self.uuid or http_device_status.uuid),
+            friendly_name=(self.friendly_name or http_device_status.friendly_name),
+            manufacturer=(self.manufacturer or http_device_status.manufacturer),
+            model_name=(self.model_name or http_device_status.model_name),
+        )
+
+    def __str__(self):
+        """Return pretty printable string for logging."""
+        return f"{self.friendly_name} ({self.uuid})"
+
+
+class CastStatusListener:
+    """Helper class to handle pychromecast status callbacks.
+
+    Necessary because a CastDevice entity can create a new socket client
+    and therefore callbacks from multiple chromecast connections can
+    potentially arrive. This class allows invalidating past chromecast objects.
+    """
+
+    def __init__(self, cast_device, chromecast, mz_mgr, mz_only=False):
+        """Initialize the status listener."""
+        self._cast_device = cast_device
+        self._uuid = chromecast.uuid
+        self._valid = True
+        self._mz_mgr = mz_mgr
+
+        if cast_device._cast_info.is_audio_group:
+            self._mz_mgr.add_multizone(chromecast)
+        if mz_only:
+            return
+
+        chromecast.register_status_listener(self)
+        chromecast.socket_client.media_controller.register_status_listener(self)
+        chromecast.register_connection_listener(self)
+        if not cast_device._cast_info.is_audio_group:
+            self._mz_mgr.register_listener(chromecast.uuid, self)
+
+    def new_cast_status(self, cast_status):
+        """Handle reception of a new CastStatus."""
+        if self._valid:
+            self._cast_device.new_cast_status(cast_status)
+
+    def new_media_status(self, media_status):
+        """Handle reception of a new MediaStatus."""
+        if self._valid:
+            self._cast_device.new_media_status(media_status)
+
+    def new_connection_status(self, connection_status):
+        """Handle reception of a new ConnectionStatus."""
+        if self._valid:
+            self._cast_device.new_connection_status(connection_status)
+
+    @staticmethod
+    def added_to_multizone(group_uuid):
+        """Handle the cast added to a group."""
+
+    def removed_from_multizone(self, group_uuid):
+        """Handle the cast removed from a group."""
+        if self._valid:
+            self._cast_device.multizone_new_media_status(group_uuid, None)
+
+    def multizone_new_cast_status(self, group_uuid, cast_status):
+        """Handle reception of a new CastStatus for a group."""
+
+    def multizone_new_media_status(self, group_uuid, media_status):
+        """Handle reception of a new MediaStatus for a group."""
+        if self._valid:
+            self._cast_device.multizone_new_media_status(group_uuid, media_status)
+
+    def invalidate(self):
+        """Invalidate this status listener.
+
+        All following callbacks won't be forwarded.
+        """
+        # pylint: disable=protected-access
+        if self._cast_device._cast_info.is_audio_group:
+            self._mz_mgr.remove_multizone(self._uuid)
+        else:
+            self._mz_mgr.deregister_listener(self._uuid, self)
+        self._valid = False
diff --git a/music_assistant/providers/chromecast/models.py b/music_assistant/providers/chromecast/models.py
deleted file mode 100644 (file)
index a96ae10..0000000
+++ /dev/null
@@ -1,159 +0,0 @@
-"""
-Class to hold all data about a chromecast for creating connections.
-
-This also has the same attributes as the mDNS fields by zeroconf.
-"""
-import logging
-from dataclasses import dataclass, field
-from typing import Optional, Tuple
-
-from pychromecast import dial
-from pychromecast.const import CAST_MANUFACTURERS
-
-from .const import PROV_ID
-
-LOGGER = logging.getLogger(PROV_ID)
-DEFAULT_PORT = 8009
-
-
-@dataclass()
-class ChromecastInfo:
-    """Class to hold all data about a chromecast for creating connections.
-
-    This also has the same attributes as the mDNS fields by zeroconf.
-    """
-
-    services: Optional[set] = field(default_factory=set)
-    uuid: Optional[str] = None
-    model_name: str = ""
-    friendly_name: Optional[str] = None
-    is_dynamic_group: bool = False
-    manufacturer: Optional[str] = None
-    host: Optional[str] = ""
-    port: Optional[int] = 0
-    _info_requested: bool = field(init=False, default=False)
-
-    def __post_init__(self):
-        """Convert UUID to string."""
-        self.uuid = str(self.uuid)
-
-    @property
-    def is_audio_group(self) -> bool:
-        """Return if this is an audio group."""
-        return self.port != DEFAULT_PORT
-
-    @property
-    def host_port(self) -> Tuple[str, int]:
-        """Return the host+port tuple."""
-        return self.host, self.port
-
-    def fill_out_missing_chromecast_info(self, zconf) -> None:
-        """Lookup missing info for the Chromecast player."""
-        http_device_status = None
-
-        if self._info_requested:
-            return
-
-        # Fill out missing group information via HTTP API.
-        if self.is_audio_group:
-            http_group_status = None
-            if self.uuid:
-                http_group_status = dial.get_multizone_status(
-                    None,
-                    services=self.services,
-                    zconf=zconf,
-                )
-                if http_group_status is not None:
-                    self.is_dynamic_group = any(
-                        str(g.uuid) == self.uuid
-                        for g in http_group_status.dynamic_groups
-                    )
-        else:
-            # Fill out some missing information (friendly_name, uuid) via HTTP dial.
-            http_device_status = dial.get_device_status(
-                None, services=self.services, zconf=zconf
-            )
-        if http_device_status:
-            self.uuid = str(http_device_status.uuid)
-        if not self.friendly_name and http_device_status:
-            self.friendly_name = http_device_status.friendly_name
-        if not self.model_name and http_device_status:
-            self.model_name = http_device_status.model_name
-        if not self.manufacturer and http_device_status:
-            self.manufacturer = http_device_status.manufacturer
-        if not self.manufacturer and self.model_name:
-            self.manufacturer = CAST_MANUFACTURERS.get(
-                self.model_name.lower(), "Google Inc."
-            )
-
-        self._info_requested = True
-
-
-class CastStatusListener:
-    """Helper class to handle pychromecast status callbacks.
-
-    Necessary because a CastDevice entity can create a new socket client
-    and therefore callbacks from multiple chromecast connections can
-    potentially arrive. This class allows invalidating past chromecast objects.
-    """
-
-    def __init__(self, cast_device, chromecast, mz_mgr):
-        """Initialize the status listener."""
-        self._cast_device = cast_device
-        self._uuid = chromecast.uuid
-        self._valid = True
-        self._mz_mgr = mz_mgr
-
-        chromecast.register_status_listener(self)
-        chromecast.socket_client.media_controller.register_status_listener(self)
-        chromecast.register_connection_listener(self)
-        if cast_device._cast_info.is_audio_group:
-            self._mz_mgr.add_multizone(chromecast)
-        else:
-            self._mz_mgr.register_listener(chromecast.uuid, self)
-
-    def new_cast_status(self, cast_status):
-        """Handle reception of a new CastStatus."""
-        if self._valid:
-            self._cast_device.new_cast_status(cast_status)
-
-    def new_media_status(self, media_status):
-        """Handle reception of a new MediaStatus."""
-        if self._valid:
-            self._cast_device.new_media_status(media_status)
-
-    def new_connection_status(self, connection_status):
-        """Handle reception of a new ConnectionStatus."""
-        if self._valid:
-            self._cast_device.new_connection_status(connection_status)
-
-    def added_to_multizone(self, group_uuid):
-        """Handle the cast added to a group."""
-        LOGGER.debug(
-            "Player %s is added to group %s", self._cast_device.name, group_uuid
-        )
-
-    def removed_from_multizone(self, group_uuid):
-        """Handle the cast removed from a group."""
-        if self._valid:
-            self._cast_device.multizone_new_media_status(group_uuid, None)
-
-    def multizone_new_cast_status(self, group_uuid, cast_status):
-        """Handle reception of a new CastStatus for a group."""
-
-    def multizone_new_media_status(self, group_uuid, media_status):
-        """Handle reception of a new MediaStatus for a group."""
-        if self._valid:
-            self._cast_device.multizone_new_media_status(group_uuid, media_status)
-
-    def invalidate(self):
-        """Invalidate this status listener.
-
-        All following callbacks won't be forwarded.
-        """
-        # pylint: disable=protected-access
-        if self._cast_device._cast_info.is_audio_group:
-            self._mz_mgr.remove_multizone(self._uuid)
-        else:
-            self._mz_mgr.deregister_listener(self._uuid, self)
-        self._valid = False
index 78614d98afbe0002537a38efd7621c3aa53974ea..1a8cf650c3f2c8011fc012dca24abb3f8a4cf0ce 100644 (file)
@@ -1,24 +1,24 @@
 """Representation of a Cast device on the network."""
+import asyncio
 import logging
 import uuid
 from typing import List, Optional
 
 import pychromecast
-from asyncio_throttle import Throttler
 from music_assistant.helpers.compare import compare_strings
 from music_assistant.helpers.typing import MusicAssistant
 from music_assistant.helpers.util import create_task, yield_chunks
 from music_assistant.models.config_entry import ConfigEntry
 from music_assistant.models.player import DeviceInfo, Player, PlayerFeature, PlayerState
 from music_assistant.models.player_queue import QueueItem
-from pychromecast.controllers.multizone import MultizoneController
+from pychromecast.controllers.multizone import MultizoneController, MultizoneManager
 from pychromecast.socket_client import (
     CONNECTION_STATUS_CONNECTED,
     CONNECTION_STATUS_DISCONNECTED,
 )
 
 from .const import PLAYER_CONFIG_ENTRIES, PROV_ID
-from .models import CastStatusListener, ChromecastInfo
+from .helpers import CastStatusListener, ChromecastInfo
 
 LOGGER = logging.getLogger(PROV_ID)
 PLAYER_FEATURES = [PlayerFeature.QUEUE]
@@ -44,12 +44,10 @@ class ChromecastPlayer(Player):
         self.cast_status = None
         self.media_status = None
         self.media_status_received = None
-        self.mz_mgr = None
-        self.mz_manager = None
+        self.mz_mgr: Optional[MultizoneManager] = None
         self._available = False
         self._status_listener: Optional[CastStatusListener] = None
         self._is_speaker_group = False
-        self._throttler = Throttler(rate_limit=1, period=0.1)
 
     @property
     def player_id(self) -> str:
@@ -73,15 +71,23 @@ class ChromecastPlayer(Player):
             return False
         if self.is_group_player:
             return (
-                self.media_status.player_is_playing
-                or self.media_status.player_is_paused
-                or self.media_status.player_is_idle
+                self._chromecast.media_controller.is_active
+                and self.cast_status.app_id == pychromecast.APP_MEDIA_RECEIVER
             )
+            # return self._chromecast is not None and self._chromecast.is_idle
+            # return (
+            #     self._chromecast.media_controller.app_id
+            #     == pychromecast.config.APP_MEDIA_RECEIVER
+            # )
+
         # Chromecast does not support power so we (ab)use mute instead
-        return (
-            not self.cast_status.display_name
-            or self.cast_status.display_name == "Default Media Receiver"
-        ) and not self.cast_status.volume_muted
+        if self._chromecast.media_controller.is_active:
+            return (
+                self.cast_status.app_id
+                in ["705D30C6", self._chromecast.media_controller.app_id]
+                and not self.cast_status.volume_muted
+            )
+        return not self.cast_status.volume_muted
 
     @property
     def should_poll(self) -> bool:
@@ -159,7 +165,7 @@ class ChromecastPlayer(Player):
         """Return deviceinfo."""
         return DeviceInfo(
             model=self._cast_info.model_name,
-            address=f"{self._cast_info.host}:{self._cast_info.port}",
+            address=f"{self._chromecast.uri}" if self._chromecast else "",
             manufacturer=self._cast_info.manufacturer,
         )
 
@@ -189,8 +195,7 @@ class ChromecastPlayer(Player):
             self.mass.zeroconf,
         )
         self._chromecast = chromecast
-        self.mz_mgr = self.mass.get_provider(PROV_ID).mz_mgr
-
+        self.mz_mgr: MultizoneManager = self.mass.get_provider(PROV_ID).mz_mgr
         self._status_listener = CastStatusListener(self, chromecast, self.mz_mgr)
         self._available = False
         self.cast_status = chromecast.status
@@ -285,8 +290,8 @@ class ChromecastPlayer(Player):
 
     async def cmd_stop(self) -> None:
         """Send stop command to player."""
-        if self._chromecast and self._chromecast.media_controller:
-            await self.chromecast_command(self._chromecast.quit_app)
+        if self._chromecast.media_controller:
+            await self.chromecast_command(self._chromecast.media_controller.stop)
 
     async def cmd_play(self) -> None:
         """Send play command to player."""
@@ -310,12 +315,22 @@ class ChromecastPlayer(Player):
 
     async def cmd_power_on(self) -> None:
         """Send power ON command to player."""
-        await self.chromecast_command(self._chromecast.set_volume_muted, False)
+        if self.is_group_player:
+            await self.launch_app()
+        else:
+            # chromecast has no real poweroff so we (ab)use mute instead
+            await self.chromecast_command(self._chromecast.set_volume_muted, False)
 
     async def cmd_power_off(self) -> None:
         """Send power OFF command to player."""
-        # chromecast has no real poweroff so we send mute instead
-        await self.chromecast_command(self._chromecast.set_volume_muted, True)
+        if self.is_group_player or (
+            self._chromecast.media_controller.is_active
+            and self.cast_status.app_id == self._chromecast.media_controller.app_id
+        ):
+            await self.chromecast_command(self._chromecast.quit_app)
+        if not self.is_group_player:
+            # chromecast has no real poweroff so we (ab)use mute instead
+            await self.chromecast_command(self._chromecast.set_volume_muted, True)
 
     async def cmd_volume_set(self, volume_level: int) -> None:
         """Send new volume level command to player."""
@@ -339,7 +354,7 @@ class ChromecastPlayer(Player):
     async def cmd_queue_load(self, queue_items: List[QueueItem]) -> None:
         """Load (overwrite) queue with new items."""
         player_queue = self.mass.players.get_player_queue(self.player_id)
-        cc_queue_items = self.__create_queue_items(queue_items[:50])
+        cc_queue_items = self.__create_queue_items(queue_items[:25])
         repeat_enabled = player_queue.use_queue_stream or player_queue.repeat_enabled
         queuedata = {
             "type": "QUEUE_LOAD",
@@ -347,30 +362,32 @@ class ChromecastPlayer(Player):
             "shuffle": False,  # handled by our queue controller
             "queueType": "PLAYLIST",
             "startIndex": 0,  # Item index to play after this request or keep same item if undefined
-            "items": cc_queue_items,  # only load 50 tracks at once or the socket will crash
+            "items": cc_queue_items,  # only load 25 tracks at once or the socket will crash
         }
+        await self.launch_app()
         await self.chromecast_command(self.__send_player_queue, queuedata)
-        if len(queue_items) > 50:
-            await self.cmd_queue_append(queue_items[51:])
+        if len(queue_items) > 25:
+            await self.cmd_queue_append(queue_items[26:])
 
     async def cmd_queue_append(self, queue_items: List[QueueItem]) -> None:
         """Append new items at the end of the queue."""
         cc_queue_items = self.__create_queue_items(queue_items)
-        async for chunk in yield_chunks(cc_queue_items, 50):
+        async for chunk in yield_chunks(cc_queue_items, 25):
             queuedata = {
                 "type": "QUEUE_INSERT",
                 "insertBefore": None,
                 "items": chunk,
             }
+            await self.launch_app()
             await self.chromecast_command(self.__send_player_queue, queuedata)
 
     def __create_queue_items(self, tracks) -> None:
         """Create list of CC queue items from tracks."""
         return [self.__create_queue_item(track) for track in tracks]
 
-    def __create_queue_item(self, queue_item: QueueItem):
+    @staticmethod
+    def __create_queue_item(queue_item: QueueItem):
         """Create CC queue item from track info."""
-        player_queue = self.mass.players.get_player_queue(self.player_id)
         return {
             "opt_itemId": queue_item.queue_item_id,
             "autoplay": True,
@@ -386,7 +403,7 @@ class ChromecastPlayer(Player):
                     "item_id": queue_item.queue_item_id,
                 },
                 "contentType": "audio/flac",
-                "streamType": "LIVE" if player_queue.use_queue_stream else "BUFFERED",
+                "streamType": pychromecast.STREAM_TYPE_BUFFERED,
                 "metadata": {
                     "title": queue_item.name,
                     "artist": "/".join(x.name for x in queue_item.artists),
@@ -398,30 +415,38 @@ class ChromecastPlayer(Player):
     def __send_player_queue(self, queuedata: dict) -> None:
         """Send new data to the CC queue."""
         media_controller = self._chromecast.media_controller
-        # pylint: disable=protected-access
-        receiver_ctrl = media_controller._socket_client.receiver_controller
+        queuedata["mediaSessionId"] = media_controller.status.media_session_id
+        media_controller.send_message(queuedata, False)
 
-        def send_queue():
-            """Plays media after chromecast has switched to requested app."""
-            queuedata["mediaSessionId"] = media_controller.status.media_session_id
-            media_controller.send_message(queuedata, False)
+    async def launch_app(self):
+        """Launch the default media receiver app and wait until its launched."""
+        media_controller = self._chromecast.media_controller
+        if (
+            media_controller.is_active
+            and self.cast_status.app_id == pychromecast.APP_MEDIA_RECEIVER
+            and media_controller.status.media_session_id is not None
+        ):
+            # already active
+            return
 
-        if not media_controller.status.media_session_id:
-            receiver_ctrl.launch_app(
-                media_controller.app_id,
-                callback_function=send_queue,
-            )
-        else:
-            send_queue()
+        event = asyncio.Event()
+
+        def launched():
+            self.mass.loop.call_soon_threadsafe(event.set)
+
+        # pylint: disable=protected-access
+        receiver_ctrl = media_controller._socket_client.receiver_controller
+        receiver_ctrl.launch_app(
+            media_controller.app_id,
+            callback_function=launched,
+        )
+        await event.wait()
 
     async def chromecast_command(self, func, *args, **kwargs):
         """Execute command on Chromecast."""
-        # Chromecast socket really doesn't like multiple commands arriving at the same time
-        # so we apply some throtling.
         if not self.available:
             LOGGER.warning(
                 "Player %s is not available, command can't be executed", self.name
             )
             return
-        async with self._throttler:
-            create_task(func, *args, **kwargs)
+        await create_task(func, *args, **kwargs)