[media.chromecast] Resource clean up + new API adaptations.

- `pychromecast.get_chromecasts` returns both a list of devices and a
  browser object. Since the Chromecast plugin is the most likely culprit
  of the excessive number of open MDNS sockets, it seems that we may
  need to explicitly stop discovery on the browser and close the
  ZeroConf object after the discovery is done.

- I was still using an ancient version of pychromecast on my RPi4, and I
  didn't notice that more recent versions implemented several breaking
  changes. Adapted the code to cope with those changes.
This commit is contained in:
Fabio Manganiello 2024-04-17 02:49:31 +02:00
parent 4972c8bdcf
commit f99f6bdab9
Signed by: blacklight
GPG key ID: D90FBA7F76362774
2 changed files with 55 additions and 41 deletions

View file

@ -31,7 +31,6 @@ class MediaChromecastPlugin(MediaPlugin, RunnablePlugin):
:param poll_interval: How often the plugin should poll for new/removed :param poll_interval: How often the plugin should poll for new/removed
Chromecast devices (default: 30 seconds). Chromecast devices (default: 30 seconds).
""" """
super().__init__(poll_interval=poll_interval, **kwargs) super().__init__(poll_interval=poll_interval, **kwargs)
self._is_local = False self._is_local = False
@ -42,11 +41,18 @@ class MediaChromecastPlugin(MediaPlugin, RunnablePlugin):
def _get_chromecasts(self, *args, **kwargs): def _get_chromecasts(self, *args, **kwargs):
with self._refresh_lock: with self._refresh_lock:
chromecasts = pychromecast.get_chromecasts(*args, **kwargs) ret = pychromecast.get_chromecasts(*args, **kwargs)
if isinstance(chromecasts, tuple): if isinstance(ret, tuple):
return chromecasts[0] chromecasts, browser = ret
return chromecasts if browser:
browser.stop_discovery()
if browser.zc:
browser.zc.close()
return chromecasts
return ret
@staticmethod @staticmethod
def _get_device_property(cc, prop: str): def _get_device_property(cc, prop: str):
@ -58,14 +64,25 @@ class MediaChromecastPlugin(MediaPlugin, RunnablePlugin):
""" """
Convert a Chromecast object and its status to a dictionary. Convert a Chromecast object and its status to a dictionary.
""" """
if hasattr(cc, 'cast_info'): # Newer PyChromecast API
host = cc.cast_info.host
port = cc.cast_info.port
elif hasattr(cc, 'host'):
host = getattr(cc, 'host', None)
port = getattr(cc, 'port', None)
elif hasattr(cc, 'uri'):
host, port = cc.uri.split(':')
else:
raise RuntimeError('Invalid Chromecast object')
return { return {
'type': cc.cast_type, 'type': cc.cast_type,
'name': cc.name, 'name': cc.name,
'manufacturer': self._get_device_property(cc, 'manufacturer'), 'manufacturer': self._get_device_property(cc, 'manufacturer'),
'model_name': cc.model_name, 'model_name': cc.model_name,
'uuid': str(cc.uuid), 'uuid': str(cc.uuid),
'address': cc.host if hasattr(cc, 'host') else cc.uri.split(':')[0], 'address': host,
'port': cc.port if hasattr(cc, 'port') else int(cc.uri.split(':')[1]), 'port': port,
'status': ( 'status': (
{ {
'app': { 'app': {
@ -284,24 +301,23 @@ class MediaChromecastPlugin(MediaPlugin, RunnablePlugin):
chromecast = chromecast or self.chromecast chromecast = chromecast or self.chromecast
cast = self.get_chromecast(chromecast) cast = self.get_chromecast(chromecast)
if cast.media_controller.is_paused: if cast.media_controller.status.player_is_paused:
cast.media_controller.play() cast.media_controller.play()
elif cast.media_controller.is_playing: elif cast.media_controller.status.player_is_playing:
cast.media_controller.pause() cast.media_controller.pause()
cast.wait() cast.wait()
return self.status(chromecast=chromecast) return self.status(chromecast=chromecast)
@action @action
def stop(self, *_, chromecast: Optional[str] = None, **__): def stop(self, *_, chromecast: Optional[str] = None, **__): # type: ignore
chromecast = chromecast or self.chromecast chromecast = chromecast or self.chromecast
if not chromecast: if not chromecast:
return return None
cast = self.get_chromecast(chromecast) cast = self.get_chromecast(chromecast)
cast.media_controller.stop() cast.media_controller.stop()
cast.wait() cast.wait()
return self.status(chromecast=chromecast) return self.status(chromecast=chromecast)
@action @action
@ -347,51 +363,51 @@ class MediaChromecastPlugin(MediaPlugin, RunnablePlugin):
def is_playing(self, chromecast: Optional[str] = None, **_): def is_playing(self, chromecast: Optional[str] = None, **_):
return self.get_chromecast( return self.get_chromecast(
chromecast or self.chromecast chromecast or self.chromecast
).media_controller.is_playing ).media_controller.status.player_is_playing
@action @action
def is_paused(self, chromecast: Optional[str] = None, **_): def is_paused(self, chromecast: Optional[str] = None, **_):
return self.get_chromecast( return self.get_chromecast(
chromecast or self.chromecast chromecast or self.chromecast
).media_controller.is_paused ).media_controller.status.player_is_paused
@action @action
def is_idle(self, chromecast: Optional[str] = None): def is_idle(self, chromecast: Optional[str] = None):
return self.get_chromecast( return self.get_chromecast(
chromecast or self.chromecast chromecast or self.chromecast
).media_controller.is_idle ).media_controller.status.player_is_idle
@action @action
def list_subtitles(self, chromecast: Optional[str] = None): def list_subtitles(self, chromecast: Optional[str] = None):
return self.get_chromecast( return self.get_chromecast(
chromecast or self.chromecast chromecast or self.chromecast
).media_controller.subtitle_tracks ).media_controller.status.subtitle_tracks
@action @action
def enable_subtitles( def enable_subtitles(
self, chromecast: Optional[str] = None, track_id: Optional[str] = None, **_ self, chromecast: Optional[str] = None, track_id: Optional[int] = None, **_
): ):
mc = self.get_chromecast(chromecast or self.chromecast).media_controller mc = self.get_chromecast(chromecast or self.chromecast).media_controller
if track_id is not None: if track_id is not None:
return mc.enable_subtitle(track_id) return mc.enable_subtitle(track_id)
if mc.subtitle_tracks: if mc.status.subtitle_tracks:
return mc.enable_subtitle(mc.subtitle_tracks[0].get('trackId')) return mc.enable_subtitle(mc.status.subtitle_tracks[0].get('trackId'))
@action @action
def disable_subtitles( def disable_subtitles(
self, chromecast: Optional[str] = None, track_id: Optional[str] = None, **_ self, chromecast: Optional[str] = None, track_id: Optional[int] = None, **_
): ):
mc = self.get_chromecast(chromecast or self.chromecast).media_controller mc = self.get_chromecast(chromecast or self.chromecast).media_controller
if track_id: if track_id:
return mc.disable_subtitle(track_id) return mc.disable_subtitle(track_id)
if mc.current_subtitle_tracks: if mc.status.current_subtitle_tracks:
return mc.disable_subtitle(mc.current_subtitle_tracks[0]) return mc.disable_subtitle(mc.status.current_subtitle_tracks[0])
@action @action
def toggle_subtitles(self, chromecast: Optional[str] = None, **_): def toggle_subtitles(self, chromecast: Optional[str] = None, **_):
mc = self.get_chromecast(chromecast or self.chromecast).media_controller mc = self.get_chromecast(chromecast or self.chromecast).media_controller
all_subs = mc.status.subtitle_tracks all_subs = mc.status.subtitle_tracks
cur_subs = mc.status.status.current_subtitle_tracks cur_subs = mc.status.current_subtitle_tracks
if cur_subs: if cur_subs:
return self.disable_subtitles(chromecast, cur_subs[0]) return self.disable_subtitles(chromecast, cur_subs[0])
@ -511,7 +527,6 @@ class MediaChromecastPlugin(MediaPlugin, RunnablePlugin):
self, self,
chromecast: Optional[str] = None, chromecast: Optional[str] = None,
timeout: Optional[float] = None, timeout: Optional[float] = None,
blocking: bool = True,
): ):
""" """
Disconnect a Chromecast and wait for it to terminate Disconnect a Chromecast and wait for it to terminate
@ -520,11 +535,9 @@ class MediaChromecastPlugin(MediaPlugin, RunnablePlugin):
the default configured Chromecast will be used. the default configured Chromecast will be used.
:param timeout: Number of seconds to wait for disconnection (default: :param timeout: Number of seconds to wait for disconnection (default:
None: block until termination). None: block until termination).
:param blocking: If set (default), then the code will wait until
disconnection, otherwise it will return immediately.
""" """
cast = self.get_chromecast(chromecast) cast = self.get_chromecast(chromecast)
cast.disconnect(timeout=timeout, blocking=blocking) cast.disconnect(timeout=timeout)
@action @action
def join(self, chromecast: Optional[str] = None, timeout: Optional[float] = None): def join(self, chromecast: Optional[str] = None, timeout: Optional[float] = None):
@ -550,17 +563,6 @@ class MediaChromecastPlugin(MediaPlugin, RunnablePlugin):
cast = self.get_chromecast(chromecast) cast = self.get_chromecast(chromecast)
cast.quit_app() cast.quit_app()
@action
def reboot(self, chromecast: Optional[str] = None):
"""
Reboots the Chromecast
:param chromecast: Chromecast to cast to. If none is specified, then
the default configured Chromecast will be used.
"""
cast = self.get_chromecast(chromecast)
cast.reboot()
@action @action
def set_volume(self, volume: float, chromecast: Optional[str] = None): def set_volume(self, volume: float, chromecast: Optional[str] = None):
""" """
@ -621,7 +623,7 @@ class MediaChromecastPlugin(MediaPlugin, RunnablePlugin):
""" """
chromecast = chromecast or self.chromecast chromecast = chromecast or self.chromecast
cast = self.get_chromecast(chromecast) cast = self.get_chromecast(chromecast)
cast.set_volume_muted(not cast.status.volume_muted) cast.set_volume_muted(not cast.media_controller.status.volume_muted)
cast.wait() cast.wait()
return self.status(chromecast=chromecast) return self.status(chromecast=chromecast)

View file

@ -1,5 +1,9 @@
# pylint: disable=too-few-public-methods import logging
class SubtitlesAsyncHandler:
from pychromecast.controllers.media import MediaStatusListener
class SubtitlesAsyncHandler(MediaStatusListener):
""" """
This class is used to enable subtitles when the media is loaded. This class is used to enable subtitles when the media is loaded.
""" """
@ -8,9 +12,17 @@ class SubtitlesAsyncHandler:
self.mc = mc self.mc = mc
self.subtitle_id = subtitle_id self.subtitle_id = subtitle_id
self.initialized = False self.initialized = False
self.logger = logging.getLogger(__name__)
def new_media_status(self, *_): def new_media_status(self, *_):
if self.subtitle_id and not self.initialized: if self.subtitle_id and not self.initialized:
self.mc.update_status() self.mc.update_status()
self.mc.enable_subtitle(self.subtitle_id) self.mc.enable_subtitle(self.subtitle_id)
self.initialized = True self.initialized = True
def load_media_failed(self, queue_item_id: int, error_code: int) -> None:
self.logger.warning(
"Failed to load media with queue_item_id %d, error code: %d",
queue_item_id,
error_code,
)