From 19ca836b78662d3998fa29b844460d895836f7b9 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 27 Feb 2020 22:01:20 +0100 Subject: [PATCH] Prevent using pulseaudio on event loop (#1536) * Prevent using pulseaudio on event loop * Fix name overwrite * Fix value --- supervisor/api/audio.py | 6 +- supervisor/host/sound.py | 219 +++++++++++++++++++++------------------ 2 files changed, 122 insertions(+), 103 deletions(-) diff --git a/supervisor/api/audio.py b/supervisor/api/audio.py index 36feab636..7260de76d 100644 --- a/supervisor/api/audio.py +++ b/supervisor/api/audio.py @@ -29,7 +29,7 @@ from ..const import ( ) from ..coresys import CoreSysAttributes from ..exceptions import APIError -from ..host.sound import SourceType +from ..host.sound import StreamType from .utils import api_process, api_process_raw, api_validate _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -115,7 +115,7 @@ class APIAudio(CoreSysAttributes): @api_process async def set_volume(self, request: web.Request) -> None: """Set audio volume on stream.""" - source: SourceType = SourceType(request.match_info.get("source")) + source: StreamType = StreamType(request.match_info.get("source")) body = await api_validate(SCHEMA_VOLUME, request) await asyncio.shield( @@ -125,7 +125,7 @@ class APIAudio(CoreSysAttributes): @api_process async def set_default(self, request: web.Request) -> None: """Set audio default stream.""" - source: SourceType = SourceType(request.match_info.get("source")) + source: StreamType = StreamType(request.match_info.get("source")) body = await api_validate(SCHEMA_DEFAULT, request) await asyncio.shield(self.sys_host.sound.set_default(source, body[ATTR_NAME])) diff --git a/supervisor/host/sound.py b/supervisor/host/sound.py index a77c1eea3..610dfc9ad 100644 --- a/supervisor/host/sound.py +++ b/supervisor/host/sound.py @@ -1,4 +1,5 @@ """Pulse host control.""" +from datetime import timedelta from enum import Enum import logging from typing import List @@ -8,13 +9,14 @@ from pulsectl import Pulse, PulseError, PulseIndexError, PulseOperationFailed from ..coresys import CoreSys, CoreSysAttributes from ..exceptions import PulseAudioError +from ..utils import AsyncThrottle _LOGGER: logging.Logger = logging.getLogger(__name__) PULSE_NAME = "supervisor" -class SourceType(str, Enum): +class StreamType(str, Enum): """INPUT/OUTPUT type of source.""" INPUT = "input" @@ -74,126 +76,143 @@ class SoundControl(CoreSysAttributes): """Return a list of available output streams.""" return self._outputs - async def set_default(self, source: SourceType, name: str) -> None: + async def set_default(self, stream_type: StreamType, name: str) -> None: """Set a stream to default input/output.""" - try: - with Pulse(PULSE_NAME) as pulse: - if source == SourceType.OUTPUT: - # Get source and set it as default - source = pulse.get_source_by_name(name) - pulse.source_default_set(source) - else: - # Get sink and set it as default - sink = pulse.get_sink_by_name(name) - pulse.sink_default_set(sink) - except PulseIndexError: - _LOGGER.error("Can't find %s profile %s", source, name) - raise PulseAudioError() from None - except PulseError as err: - _LOGGER.error("Can't set %s as default: %s", name, err) - raise PulseAudioError() from None - # Reload data + def _set_default(): + try: + with Pulse(PULSE_NAME) as pulse: + if stream_type == StreamType.INPUT: + # Get source and set it as default + source = pulse.get_source_by_name(name) + pulse.source_default_set(source) + else: + # Get sink and set it as default + sink = pulse.get_sink_by_name(name) + pulse.sink_default_set(sink) + except PulseIndexError: + _LOGGER.error("Can't find %s profile %s", source, name) + raise PulseAudioError() from None + except PulseError as err: + _LOGGER.error("Can't set %s as default: %s", name, err) + raise PulseAudioError() from None + + # Run and Reload data + await self.sys_run_in_executor(_set_default) await self.update() - async def set_volume(self, source: SourceType, name: str, volume: float) -> None: + async def set_volume( + self, stream_type: StreamType, name: str, volume: float + ) -> None: """Set a stream to volume input/output.""" - try: - with Pulse(PULSE_NAME) as pulse: - if source == SourceType.OUTPUT: - # Get source and set it as default - source = pulse.get_source_by_name(name) - else: - # Get sink and set it as default - source = pulse.get_sink_by_name(name) - pulse.volume_set_all_chans(source, volume) - except PulseIndexError: - _LOGGER.error("Can't find %s profile %s", source, name) - raise PulseAudioError() from None - except PulseError as err: - _LOGGER.error("Can't set %s volume: %s", name, err) - raise PulseAudioError() from None + def _set_volume(): + try: + with Pulse(PULSE_NAME) as pulse: + if stream_type == StreamType.INPUT: + # Get source and set it as default + stream = pulse.get_source_by_name(name) + else: + # Get sink and set it as default + stream = pulse.get_sink_by_name(name) - # Reload data + pulse.volume_set_all_chans(stream, volume) + except PulseIndexError: + _LOGGER.error("Can't find %s profile %s", stream_type, name) + raise PulseAudioError() from None + except PulseError as err: + _LOGGER.error("Can't set %s volume: %s", name, err) + raise PulseAudioError() from None + + # Run and Reload data + await self.sys_run_in_executor(_set_volume) await self.update() async def ativate_profile(self, card_name: str, profile_name: str) -> None: """Set a profile to volume input/output.""" - try: - with Pulse(PULSE_NAME) as pulse: - card = pulse.get_sink_by_name(card_name) - pulse.card_profile_set(card, profile_name) - except PulseIndexError: - _LOGGER.error("Can't find %s profile %s", card_name, profile_name) - raise PulseAudioError() from None - except PulseError as err: - _LOGGER.error( - "Can't activate %s profile %s: %s", card_name, profile_name, err - ) - raise PulseAudioError() from None + def _activate_profile(): + try: + with Pulse(PULSE_NAME) as pulse: + card = pulse.get_sink_by_name(card_name) + pulse.card_profile_set(card, profile_name) - # Reload data + except PulseIndexError: + _LOGGER.error("Can't find %s profile %s", card_name, profile_name) + raise PulseAudioError() from None + except PulseError as err: + _LOGGER.error( + "Can't activate %s profile %s: %s", card_name, profile_name, err + ) + raise PulseAudioError() from None + + # Run and Reload data + await self.sys_run_in_executor(_activate_profile) await self.update() + @AsyncThrottle(timedelta(seconds=10)) async def update(self): """Update properties over dbus.""" _LOGGER.info("Update PulseAudio information") - try: - with Pulse(PULSE_NAME) as pulse: - server = pulse.server_info() - # Update output - self._outputs.clear() - for sink in pulse.sink_list(): - self._outputs.append( - AudioStream( - sink.name, - sink.description, - sink.volume.value_flat, - sink.name == server.default_sink_name, - ) - ) + def _update(): + try: + with Pulse(PULSE_NAME) as pulse: + server = pulse.server_info() - # Update input - self._inputs.clear() - for source in pulse.source_list(): - # Filter monitor devices out because we did not use it now - if source.name.endswith(".monitor"): - continue - self._inputs.append( - AudioStream( - source.name, - source.description, - source.volume.value_flat, - source.name == server.default_source_name, - ) - ) - - # Update Sound Card - self._cards.clear() - for card in pulse.card_list(): - sound_profiles: List[SoundProfile] = [] - - # Generate profiles - for profile in card.profile_list: - if not profile.available: - continue - sound_profiles.append( - SoundProfile( - profile.name, - profile.description, - profile.name == card.profile_active.name, + # Update output + self._outputs.clear() + for sink in pulse.sink_list(): + self._outputs.append( + AudioStream( + sink.name, + sink.description, + sink.volume.value_flat, + sink.name == server.default_sink_name, ) ) - self._cards.append( - SoundCard(card.name, card.driver, sound_profiles) - ) + # Update input + self._inputs.clear() + for source in pulse.source_list(): + # Filter monitor devices out because we did not use it now + if source.name.endswith(".monitor"): + continue + self._inputs.append( + AudioStream( + source.name, + source.description, + source.volume.value_flat, + source.name == server.default_source_name, + ) + ) - except PulseOperationFailed as err: - _LOGGER.error("Error while processing pulse update: %s", err) - raise PulseAudioError() from None - except PulseError as err: - _LOGGER.debug("Can't update PulseAudio data: %s", err) + # Update Sound Card + self._cards.clear() + for card in pulse.card_list(): + sound_profiles: List[SoundProfile] = [] + + # Generate profiles + for profile in card.profile_list: + if not profile.available: + continue + sound_profiles.append( + SoundProfile( + profile.name, + profile.description, + profile.name == card.profile_active.name, + ) + ) + + self._cards.append( + SoundCard(card.name, card.driver, sound_profiles) + ) + + except PulseOperationFailed as err: + _LOGGER.error("Error while processing pulse update: %s", err) + raise PulseAudioError() from None + except PulseError as err: + _LOGGER.debug("Can't update PulseAudio data: %s", err) + + # Run update from pulse server + await self.sys_run_in_executor(_update)