forked from platypush/platypush
[media.chromecast] Refactored implementation.
Explicitly use a `CastBrowser` object initialized at plugin boot instead of relying on blocking calls to `pychromecast.get_chromecasts`. 1. It enables better event handling via callbacks instead of synchronously waiting for scan batches. 2. It optimizes resources - only one Zeroconf and one CastBrowser object will be created in the plugin, and destroyed upon stop. 3. No need for separate `get_chromecast`/`_refresh_chromecasts` methods: all the scanning is run continuously, so we can just return the results from the maps.
This commit is contained in:
parent
f99f6bdab9
commit
e123463804
1 changed files with 106 additions and 110 deletions
|
@ -1,7 +1,12 @@
|
|||
import threading
|
||||
from typing import Callable, Optional
|
||||
from typing import Optional
|
||||
|
||||
import pychromecast # type: ignore
|
||||
from pychromecast import (
|
||||
CastBrowser,
|
||||
Chromecast,
|
||||
ChromecastConnectionError,
|
||||
SimpleCastListener,
|
||||
get_chromecast_from_cast_info,
|
||||
)
|
||||
|
||||
from platypush.backend.http.app.utils import get_remote_base_url
|
||||
from platypush.plugins import RunnablePlugin, action
|
||||
|
@ -35,24 +40,34 @@ class MediaChromecastPlugin(MediaPlugin, RunnablePlugin):
|
|||
|
||||
self._is_local = False
|
||||
self.chromecast = chromecast
|
||||
self.chromecasts = {}
|
||||
self._chromecasts_by_uuid = {}
|
||||
self._chromecasts_by_name = {}
|
||||
self._media_listeners = {}
|
||||
self._refresh_lock = threading.RLock()
|
||||
self._zc = None
|
||||
self._browser = None
|
||||
|
||||
def _get_chromecasts(self, *args, **kwargs):
|
||||
with self._refresh_lock:
|
||||
ret = pychromecast.get_chromecasts(*args, **kwargs)
|
||||
@property
|
||||
def zc(self):
|
||||
from zeroconf import Zeroconf
|
||||
|
||||
if isinstance(ret, tuple):
|
||||
chromecasts, browser = ret
|
||||
if browser:
|
||||
browser.stop_discovery()
|
||||
if browser.zc:
|
||||
browser.zc.close()
|
||||
if not self._zc:
|
||||
self._zc = Zeroconf()
|
||||
|
||||
return chromecasts
|
||||
return self._zc
|
||||
|
||||
return ret
|
||||
@property
|
||||
def browser(self):
|
||||
if not self._browser:
|
||||
self._browser = CastBrowser(
|
||||
SimpleCastListener(self._on_chromecast_discovered), self.zc
|
||||
)
|
||||
|
||||
self._browser.start_discovery()
|
||||
|
||||
return self._browser
|
||||
|
||||
def _on_chromecast_discovered(self, _, service: str):
|
||||
self.logger.info('Discovered Chromecast: %s', service)
|
||||
|
||||
@staticmethod
|
||||
def _get_device_property(cc, prop: str):
|
||||
|
@ -60,7 +75,7 @@ class MediaChromecastPlugin(MediaPlugin, RunnablePlugin):
|
|||
return getattr(cc.device, prop)
|
||||
return getattr(cc.cast_info, prop)
|
||||
|
||||
def _serialize_device(self, cc: pychromecast.Chromecast) -> dict:
|
||||
def _serialize_device(self, cc: Chromecast) -> dict:
|
||||
"""
|
||||
Convert a Chromecast object and its status to a dictionary.
|
||||
"""
|
||||
|
@ -102,101 +117,23 @@ class MediaChromecastPlugin(MediaPlugin, RunnablePlugin):
|
|||
),
|
||||
}
|
||||
|
||||
def _refresh_chromecasts(
|
||||
self,
|
||||
tries: int = 2,
|
||||
retry_wait: float = 10,
|
||||
timeout: float = 60,
|
||||
blocking: bool = True,
|
||||
callback: Optional[Callable] = None,
|
||||
):
|
||||
"""
|
||||
Get the list of Chromecast devices
|
||||
def _event_callback(self, _, cast: Chromecast):
|
||||
self._chromecasts_by_uuid[cast.uuid] = cast
|
||||
self._chromecasts_by_name[
|
||||
self._get_device_property(cast, 'friendly_name')
|
||||
] = cast
|
||||
|
||||
:param tries: Number of retries (default: 2)
|
||||
:param retry_wait: Number of seconds between retries (default: 10 seconds)
|
||||
:param timeout: Timeout before failing the call (default: 60 seconds)
|
||||
:param blocking: If true, then the function will block until all the
|
||||
Chromecast devices have been scanned. If false, then the provided
|
||||
callback function will be invoked when a new device is discovered
|
||||
:param callback: If blocking is false, then you can provide a callback
|
||||
function that will be invoked when a new device is discovered
|
||||
"""
|
||||
casts = {
|
||||
self._get_device_property(cast, 'friendly_name'): cast
|
||||
for cast in self._get_chromecasts(
|
||||
tries=tries,
|
||||
retry_wait=retry_wait,
|
||||
timeout=timeout,
|
||||
blocking=blocking,
|
||||
callback=callback,
|
||||
)
|
||||
}
|
||||
|
||||
for name, cast in casts.copy().items():
|
||||
if not self.chromecasts.get(name):
|
||||
self.logger.info('Discovered new Chromecast: %s', name)
|
||||
self.chromecasts[name] = cast
|
||||
self._update_listeners(name, cast)
|
||||
cast.wait()
|
||||
else:
|
||||
casts.pop(name)
|
||||
|
||||
for name, cast in casts.items():
|
||||
cast.wait()
|
||||
self.logger.info('Refreshed Chromecast state: %s', name)
|
||||
|
||||
def _event_callback(self, _, cast: pychromecast.Chromecast):
|
||||
with self._refresh_lock:
|
||||
self.chromecasts[self._get_device_property(cast, 'friendly_name')] = cast
|
||||
|
||||
def _update_listeners(self, name, cast):
|
||||
if name not in self._media_listeners:
|
||||
cast.start()
|
||||
self._media_listeners[name] = MediaListener(
|
||||
name=name, cast=cast, callback=self._event_callback
|
||||
)
|
||||
cast.media_controller.register_status_listener(self._media_listeners[name])
|
||||
self.logger.debug('Started media listener for %s', name)
|
||||
|
||||
def get_chromecast(self, chromecast=None, n_tries=2):
|
||||
if isinstance(chromecast, pychromecast.Chromecast):
|
||||
assert chromecast, 'Invalid Chromecast object'
|
||||
def get_chromecast(self, chromecast=None):
|
||||
if isinstance(chromecast, Chromecast):
|
||||
return chromecast
|
||||
|
||||
if not chromecast:
|
||||
if not self.chromecast:
|
||||
raise RuntimeError(
|
||||
'No Chromecast specified nor default Chromecast configured'
|
||||
)
|
||||
chromecast = self.chromecast
|
||||
if self._chromecasts_by_uuid.get(chromecast):
|
||||
return self._chromecasts_by_uuid[chromecast]
|
||||
|
||||
if chromecast not in self.chromecasts:
|
||||
casts = {}
|
||||
while n_tries > 0:
|
||||
n_tries -= 1
|
||||
casts.update(
|
||||
{
|
||||
self._get_device_property(cast, 'friendly_name'): cast
|
||||
for cast in self._get_chromecasts()
|
||||
}
|
||||
)
|
||||
if self._chromecasts_by_name.get(chromecast):
|
||||
return self._chromecasts_by_name[chromecast]
|
||||
|
||||
if chromecast in casts:
|
||||
self.chromecasts.update(casts)
|
||||
break
|
||||
|
||||
if chromecast not in self.chromecasts:
|
||||
raise RuntimeError(f'Device {chromecast} not found')
|
||||
|
||||
cast = self.chromecasts[chromecast]
|
||||
|
||||
try:
|
||||
cast.wait()
|
||||
except Exception as e:
|
||||
self.logger.warning('Failed to wait Chromecast sync: %s', e)
|
||||
|
||||
return cast
|
||||
raise AssertionError(f'Chromecast {chromecast} not found')
|
||||
|
||||
@action
|
||||
def play(
|
||||
|
@ -311,6 +248,17 @@ class MediaChromecastPlugin(MediaPlugin, RunnablePlugin):
|
|||
|
||||
@action
|
||||
def stop(self, *_, chromecast: Optional[str] = None, **__): # type: ignore
|
||||
if self.should_stop():
|
||||
if self._zc:
|
||||
self._zc.close()
|
||||
self._zc = None
|
||||
|
||||
if self._browser:
|
||||
self._browser.stop_discovery()
|
||||
self._browser = None
|
||||
|
||||
return
|
||||
|
||||
chromecast = chromecast or self.chromecast
|
||||
if not chromecast:
|
||||
return None
|
||||
|
@ -513,13 +461,13 @@ class MediaChromecastPlugin(MediaPlugin, RunnablePlugin):
|
|||
def _status(self, chromecast: Optional[str] = None) -> dict:
|
||||
if chromecast:
|
||||
assert (
|
||||
chromecast in self.chromecasts
|
||||
chromecast in self._chromecasts_by_name
|
||||
), f'No such Chromecast device: {chromecast}'
|
||||
return self._serialize_device(self.chromecasts[chromecast])
|
||||
return self._serialize_device(self._chromecasts_by_name[chromecast])
|
||||
|
||||
return {
|
||||
name: self._serialize_device(cast)
|
||||
for name, cast in self.chromecasts.items()
|
||||
for name, cast in self._chromecasts_by_name.items()
|
||||
}
|
||||
|
||||
@action
|
||||
|
@ -633,6 +581,54 @@ class MediaChromecastPlugin(MediaPlugin, RunnablePlugin):
|
|||
def remove_subtitles(self, *_, **__):
|
||||
raise NotImplementedError
|
||||
|
||||
def _refresh_chromecasts(self):
|
||||
cast_info = {cast.friendly_name: cast for cast in self.browser.devices.values()}
|
||||
|
||||
for info in cast_info.values():
|
||||
name = info.friendly_name
|
||||
if self._chromecasts_by_uuid.get(
|
||||
info.uuid
|
||||
) and self._chromecasts_by_name.get(name):
|
||||
self.logger.debug('Chromecast %s already connected', name)
|
||||
continue
|
||||
|
||||
self.logger.info('Started scan for Chromecast %s', name)
|
||||
|
||||
try:
|
||||
cc = get_chromecast_from_cast_info(
|
||||
info,
|
||||
self.browser.zc,
|
||||
tries=2,
|
||||
retry_wait=5,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
self._chromecasts_by_name[cc.name] = cc
|
||||
except ChromecastConnectionError:
|
||||
self.logger.warning('Failed to connect to Chromecast %s', info)
|
||||
continue
|
||||
|
||||
if cc.uuid not in self._chromecasts_by_uuid:
|
||||
self._chromecasts_by_uuid[cc.uuid] = cc
|
||||
self.logger.debug('Connecting to Chromecast %s', name)
|
||||
|
||||
if name not in self._media_listeners:
|
||||
cc.start()
|
||||
self._media_listeners[name] = MediaListener(
|
||||
name=name or str(cc.uuid),
|
||||
cast=cc,
|
||||
callback=self._event_callback,
|
||||
)
|
||||
|
||||
cc.media_controller.register_status_listener(
|
||||
self._media_listeners[name]
|
||||
)
|
||||
|
||||
self.logger.info('Connected to Chromecast %s', name)
|
||||
|
||||
self._chromecasts_by_uuid[cc.uuid] = cc
|
||||
self._chromecasts_by_name[name] = cc
|
||||
|
||||
def main(self):
|
||||
while not self.should_stop():
|
||||
try:
|
||||
|
|
Loading…
Reference in a new issue