mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-24 09:36:31 +00:00
commit
14167f6e13
84
API.md
84
API.md
@ -866,6 +866,7 @@ return:
|
||||
"card": [
|
||||
{
|
||||
"name": "...",
|
||||
"index": 1,
|
||||
"driver": "...",
|
||||
"profiles": [
|
||||
{
|
||||
@ -879,17 +880,56 @@ return:
|
||||
"input": [
|
||||
{
|
||||
"name": "...",
|
||||
"index": 0,
|
||||
"description": "...",
|
||||
"volume": 0.3,
|
||||
"default": false
|
||||
"mute": false,
|
||||
"default": false,
|
||||
"card": "null|int",
|
||||
"applications": [
|
||||
{
|
||||
"name": "...",
|
||||
"index": 0,
|
||||
"stream_index": 0,
|
||||
"stream_type": "INPUT",
|
||||
"volume": 0.3,
|
||||
"mute": false,
|
||||
"addon": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"output": [
|
||||
{
|
||||
"name": "...",
|
||||
"index": 0,
|
||||
"description": "...",
|
||||
"volume": 0.3,
|
||||
"default": false
|
||||
"mute": false,
|
||||
"default": false,
|
||||
"card": "null|int",
|
||||
"applications": [
|
||||
{
|
||||
"name": "...",
|
||||
"index": 0,
|
||||
"stream_index": 0,
|
||||
"stream_type": "OUTPUT",
|
||||
"volume": 0.3,
|
||||
"mute": false,
|
||||
"addon": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"application": [
|
||||
{
|
||||
"name": "...",
|
||||
"index": 0,
|
||||
"stream_index": 0,
|
||||
"stream_type": "OUTPUT",
|
||||
"volume": 0.3,
|
||||
"mute": false,
|
||||
"addon": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -914,7 +954,7 @@ return:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "...",
|
||||
"index": "...",
|
||||
"volume": 0.5
|
||||
}
|
||||
```
|
||||
@ -923,11 +963,47 @@ return:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "...",
|
||||
"index": "...",
|
||||
"volume": 0.5
|
||||
}
|
||||
```
|
||||
|
||||
- POST `/audio/volume/{output|input}/application`
|
||||
|
||||
```json
|
||||
{
|
||||
"index": "...",
|
||||
"volume": 0.5
|
||||
}
|
||||
```
|
||||
|
||||
- POST `/audio/mute/input`
|
||||
|
||||
```json
|
||||
{
|
||||
"index": "...",
|
||||
"active": false
|
||||
}
|
||||
```
|
||||
|
||||
- POST `/audio/mute/output`
|
||||
|
||||
```json
|
||||
{
|
||||
"index": "...",
|
||||
"active": false
|
||||
}
|
||||
```
|
||||
|
||||
- POST `/audio/mute/{output|input}/application`
|
||||
|
||||
```json
|
||||
{
|
||||
"index": "...",
|
||||
"active": false
|
||||
}
|
||||
```
|
||||
|
||||
- POST `/audio/default/input`
|
||||
|
||||
```json
|
||||
|
@ -63,6 +63,8 @@ RE_WEBUI = re.compile(
|
||||
r":\/\/\[HOST\]:\[PORT:(?P<t_port>\d+)\](?P<s_suffix>.*)$"
|
||||
)
|
||||
|
||||
RE_OLD_AUDIO = re.compile(r"\d+,\d+")
|
||||
|
||||
|
||||
class Addon(AddonModel):
|
||||
"""Hold data for add-on inside Supervisor."""
|
||||
@ -282,7 +284,13 @@ class Addon(AddonModel):
|
||||
"""Return a pulse profile for output or None."""
|
||||
if not self.with_audio:
|
||||
return None
|
||||
return self.persist.get(ATTR_AUDIO_OUTPUT)
|
||||
|
||||
# Fallback with old audio settings
|
||||
# Remove after 210
|
||||
output_data = self.persist.get(ATTR_AUDIO_OUTPUT)
|
||||
if output_data and RE_OLD_AUDIO.fullmatch(output_data):
|
||||
return None
|
||||
return output_data
|
||||
|
||||
@audio_output.setter
|
||||
def audio_output(self, value: Optional[str]):
|
||||
@ -297,7 +305,13 @@ class Addon(AddonModel):
|
||||
"""Return pulse profile for input or None."""
|
||||
if not self.with_audio:
|
||||
return None
|
||||
return self.persist.get(ATTR_AUDIO_INPUT)
|
||||
|
||||
# Fallback with old audio settings
|
||||
# Remove after 210
|
||||
input_data = self.persist.get(ATTR_AUDIO_INPUT)
|
||||
if input_data and RE_OLD_AUDIO.fullmatch(input_data):
|
||||
return None
|
||||
return input_data
|
||||
|
||||
@audio_input.setter
|
||||
def audio_input(self, value: Optional[str]):
|
||||
|
@ -31,6 +31,7 @@ from ..const import (
|
||||
ATTR_HOST_PID,
|
||||
ATTR_IMAGE,
|
||||
ATTR_INGRESS,
|
||||
ATTR_INIT,
|
||||
ATTR_KERNEL_MODULES,
|
||||
ATTR_LEGACY,
|
||||
ATTR_LOCATON,
|
||||
@ -344,6 +345,11 @@ class AddonModel(CoreSysAttributes):
|
||||
"""Return Exclude list for snapshot."""
|
||||
return self.data.get(ATTR_SNAPSHOT_EXCLUDE, [])
|
||||
|
||||
@property
|
||||
def default_init(self) -> bool:
|
||||
"""Return True if the add-on have no own init."""
|
||||
return self.data[ATTR_INIT]
|
||||
|
||||
@property
|
||||
def with_stdin(self) -> bool:
|
||||
"""Return True if the add-on access use stdin input."""
|
||||
|
@ -44,6 +44,7 @@ from ..const import (
|
||||
ATTR_INGRESS_PANEL,
|
||||
ATTR_INGRESS_PORT,
|
||||
ATTR_INGRESS_TOKEN,
|
||||
ATTR_INIT,
|
||||
ATTR_KERNEL_MODULES,
|
||||
ATTR_LEGACY,
|
||||
ATTR_LOCATON,
|
||||
@ -189,6 +190,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema(
|
||||
vol.Optional(ATTR_URL): vol.Url(),
|
||||
vol.Required(ATTR_STARTUP): vol.All(_simple_startup, vol.In(STARTUP_ALL)),
|
||||
vol.Required(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]),
|
||||
vol.Optional(ATTR_INIT, default=True): vol.Boolean(),
|
||||
vol.Optional(ATTR_ADVANCED, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_STAGE, default=AddonStages.STABLE): vol.Coerce(AddonStages),
|
||||
vol.Optional(ATTR_PORTS): DOCKER_PORTS,
|
||||
|
@ -330,7 +330,10 @@ class RestAPI(CoreSysAttributes):
|
||||
web.post("/audio/restart", api_audio.restart),
|
||||
web.post("/audio/reload", api_audio.reload),
|
||||
web.post("/audio/profile", api_audio.set_profile),
|
||||
web.post("/audio/volume/{source}/application", api_audio.set_volume),
|
||||
web.post("/audio/volume/{source}", api_audio.set_volume),
|
||||
web.post("/audio/mute/{source}/application", api_audio.set_mute),
|
||||
web.post("/audio/mute/{source}", api_audio.set_mute),
|
||||
web.post("/audio/default/{source}", api_audio.set_default),
|
||||
]
|
||||
)
|
||||
|
@ -8,12 +8,15 @@ import attr
|
||||
import voluptuous as vol
|
||||
|
||||
from ..const import (
|
||||
ATTR_ACTIVE,
|
||||
ATTR_APPLICATION,
|
||||
ATTR_AUDIO,
|
||||
ATTR_BLK_READ,
|
||||
ATTR_BLK_WRITE,
|
||||
ATTR_CARD,
|
||||
ATTR_CPU_PERCENT,
|
||||
ATTR_HOST,
|
||||
ATTR_INDEX,
|
||||
ATTR_INPUT,
|
||||
ATTR_LATEST_VERSION,
|
||||
ATTR_MEMORY_LIMIT,
|
||||
@ -38,11 +41,19 @@ SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): vol.Coerce(str)})
|
||||
|
||||
SCHEMA_VOLUME = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_NAME): vol.Coerce(str),
|
||||
vol.Required(ATTR_INDEX): vol.Coerce(int),
|
||||
vol.Required(ATTR_VOLUME): vol.Coerce(float),
|
||||
}
|
||||
)
|
||||
|
||||
# pylint: disable=no-value-for-parameter
|
||||
SCHEMA_MUTE = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_INDEX): vol.Coerce(int),
|
||||
vol.Required(ATTR_ACTIVE): vol.Boolean(),
|
||||
}
|
||||
)
|
||||
|
||||
SCHEMA_DEFAULT = vol.Schema({vol.Required(ATTR_NAME): vol.Coerce(str)})
|
||||
|
||||
SCHEMA_PROFILE = vol.Schema(
|
||||
@ -68,6 +79,9 @@ class APIAudio(CoreSysAttributes):
|
||||
ATTR_OUTPUT: [
|
||||
attr.asdict(stream) for stream in self.sys_host.sound.outputs
|
||||
],
|
||||
ATTR_APPLICATION: [
|
||||
attr.asdict(stream) for stream in self.sys_host.sound.applications
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
@ -116,10 +130,26 @@ class APIAudio(CoreSysAttributes):
|
||||
async def set_volume(self, request: web.Request) -> None:
|
||||
"""Set audio volume on stream."""
|
||||
source: StreamType = StreamType(request.match_info.get("source"))
|
||||
application: bool = request.path.endswith("application")
|
||||
body = await api_validate(SCHEMA_VOLUME, request)
|
||||
|
||||
await asyncio.shield(
|
||||
self.sys_host.sound.set_volume(source, body[ATTR_NAME], body[ATTR_VOLUME])
|
||||
self.sys_host.sound.set_volume(
|
||||
source, body[ATTR_INDEX], body[ATTR_VOLUME], application
|
||||
)
|
||||
)
|
||||
|
||||
@api_process
|
||||
async def set_mute(self, request: web.Request) -> None:
|
||||
"""Mute audio volume on stream."""
|
||||
source: StreamType = StreamType(request.match_info.get("source"))
|
||||
application: bool = request.path.endswith("application")
|
||||
body = await api_validate(SCHEMA_MUTE, request)
|
||||
|
||||
await asyncio.shield(
|
||||
self.sys_host.sound.set_mute(
|
||||
source, body[ATTR_INDEX], body[ATTR_ACTIVE], application
|
||||
)
|
||||
)
|
||||
|
||||
@api_process
|
||||
|
@ -3,12 +3,12 @@ import asyncio
|
||||
from contextlib import suppress
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Awaitable, Optional
|
||||
import shutil
|
||||
from typing import Awaitable, List, Optional
|
||||
|
||||
import jinja2
|
||||
|
||||
from .const import ATTR_VERSION, FILE_HASSIO_AUDIO
|
||||
from .const import ATTR_VERSION, FILE_HASSIO_AUDIO, STATE_STARTED
|
||||
from .coresys import CoreSys, CoreSysAttributes
|
||||
from .docker.audio import DockerAudio
|
||||
from .docker.stats import DockerStats
|
||||
@ -157,6 +157,8 @@ class Audio(JsonConfig, CoreSysAttributes):
|
||||
_LOGGER.error("Can't start Audio plugin")
|
||||
raise AudioError() from None
|
||||
|
||||
await self._restart_audio_addons()
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Run CoreDNS."""
|
||||
# Start Instance
|
||||
@ -167,6 +169,8 @@ class Audio(JsonConfig, CoreSysAttributes):
|
||||
_LOGGER.error("Can't start Audio plugin")
|
||||
raise AudioError() from None
|
||||
|
||||
await self._restart_audio_addons()
|
||||
|
||||
def logs(self) -> Awaitable[bytes]:
|
||||
"""Get CoreDNS docker logs.
|
||||
|
||||
@ -217,3 +221,22 @@ class Audio(JsonConfig, CoreSysAttributes):
|
||||
default_source=input_profile,
|
||||
default_sink=output_profile,
|
||||
)
|
||||
|
||||
async def _restart_audio_addons(self):
|
||||
"""Restart all Add-ons they can be connect to unix socket."""
|
||||
tasks: List[Awaitable[None]] = []
|
||||
|
||||
# Find all Add-ons running with audio
|
||||
for addon in self.sys_addons.installed:
|
||||
if not addon.with_audio:
|
||||
continue
|
||||
if await addon.state() != STATE_STARTED:
|
||||
continue
|
||||
tasks.append(addon.restart())
|
||||
|
||||
if not tasks:
|
||||
return
|
||||
|
||||
# restart
|
||||
_LOGGER.info("Restart all Add-ons attach to pulse server: %d", len(tasks))
|
||||
await asyncio.wait(tasks)
|
||||
|
@ -3,7 +3,7 @@ from enum import Enum
|
||||
from ipaddress import ip_network
|
||||
from pathlib import Path
|
||||
|
||||
SUPERVISOR_VERSION = "203"
|
||||
SUPERVISOR_VERSION = "204"
|
||||
|
||||
|
||||
URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons"
|
||||
@ -234,6 +234,10 @@ ATTR_CLI = "cli"
|
||||
ATTR_DEFAULT = "default"
|
||||
ATTR_VOLUME = "volume"
|
||||
ATTR_CARD = "card"
|
||||
ATTR_INDEX = "index"
|
||||
ATTR_ACTIVE = "active"
|
||||
ATTR_APPLICATION = "application"
|
||||
ATTR_INIT = "init"
|
||||
|
||||
PROVIDE_SERVICE = "provide"
|
||||
NEED_SERVICE = "need"
|
||||
|
@ -344,7 +344,7 @@ class DockerAddon(DockerInterface):
|
||||
name=self.name,
|
||||
hostname=self.addon.hostname,
|
||||
detach=True,
|
||||
init=True,
|
||||
init=self.addon.default_init,
|
||||
privileged=self.full_access,
|
||||
ipc_mode=self.ipc,
|
||||
stdin_open=self.addon.with_stdin,
|
||||
|
@ -41,6 +41,7 @@ class DockerAudio(DockerInterface, CoreSysAttributes):
|
||||
docker_container = self.sys_docker.run(
|
||||
self.image,
|
||||
version=self.sys_audio.version,
|
||||
init=False,
|
||||
ipv4=self.sys_docker.network.audio,
|
||||
name=self.name,
|
||||
hostname=self.name.replace("_", "-"),
|
||||
|
@ -41,6 +41,7 @@ class DockerDNS(DockerInterface, CoreSysAttributes):
|
||||
docker_container = self.sys_docker.run(
|
||||
self.image,
|
||||
version=self.sys_dns.version,
|
||||
init=False,
|
||||
dns=False,
|
||||
ipv4=self.sys_docker.network.dns,
|
||||
name=self.name,
|
||||
|
@ -2,7 +2,7 @@
|
||||
from datetime import timedelta
|
||||
from enum import Enum
|
||||
import logging
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
import attr
|
||||
from pulsectl import Pulse, PulseError, PulseIndexError, PulseOperationFailed
|
||||
@ -24,13 +24,30 @@ class StreamType(str, Enum):
|
||||
|
||||
|
||||
@attr.s(frozen=True)
|
||||
class AudioStream:
|
||||
"""Represent a input/output profile."""
|
||||
class AudioApplication:
|
||||
"""Represent a application on the stream."""
|
||||
|
||||
name: str = attr.ib()
|
||||
index: int = attr.ib()
|
||||
stream_index: str = attr.ib()
|
||||
stream_type: StreamType = attr.ib()
|
||||
volume: float = attr.ib()
|
||||
mute: bool = attr.ib()
|
||||
addon: str = attr.ib()
|
||||
|
||||
|
||||
@attr.s(frozen=True)
|
||||
class AudioStream:
|
||||
"""Represent a input/output stream."""
|
||||
|
||||
name: str = attr.ib()
|
||||
index: int = attr.ib()
|
||||
description: str = attr.ib()
|
||||
volume: float = attr.ib()
|
||||
mute: bool = attr.ib()
|
||||
default: bool = attr.ib()
|
||||
card: Optional[int] = attr.ib()
|
||||
applications: List[AudioApplication] = attr.ib()
|
||||
|
||||
|
||||
@attr.s(frozen=True)
|
||||
@ -47,6 +64,7 @@ class SoundCard:
|
||||
"""Represent a Sound Card."""
|
||||
|
||||
name: str = attr.ib()
|
||||
index: int = attr.ib()
|
||||
driver: str = attr.ib()
|
||||
profiles: List[SoundProfile] = attr.ib()
|
||||
|
||||
@ -60,6 +78,7 @@ class SoundControl(CoreSysAttributes):
|
||||
self._cards: List[SoundCard] = []
|
||||
self._inputs: List[AudioStream] = []
|
||||
self._outputs: List[AudioStream] = []
|
||||
self._applications: List[AudioApplication] = []
|
||||
|
||||
@property
|
||||
def cards(self) -> List[SoundCard]:
|
||||
@ -76,6 +95,11 @@ class SoundControl(CoreSysAttributes):
|
||||
"""Return a list of available output streams."""
|
||||
return self._outputs
|
||||
|
||||
@property
|
||||
def applications(self) -> List[AudioApplication]:
|
||||
"""Return a list of available application streams."""
|
||||
return self._applications
|
||||
|
||||
async def set_default(self, stream_type: StreamType, name: str) -> None:
|
||||
"""Set a stream to default input/output."""
|
||||
|
||||
@ -90,11 +114,12 @@ class SoundControl(CoreSysAttributes):
|
||||
# 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)
|
||||
_LOGGER.error("Can't find %s stream %s", source, name)
|
||||
raise PulseAudioError() from None
|
||||
except PulseError as err:
|
||||
_LOGGER.error("Can't set %s as default: %s", name, err)
|
||||
_LOGGER.error("Can't set %s as stream: %s", name, err)
|
||||
raise PulseAudioError() from None
|
||||
|
||||
# Run and Reload data
|
||||
@ -102,32 +127,73 @@ class SoundControl(CoreSysAttributes):
|
||||
await self.update()
|
||||
|
||||
async def set_volume(
|
||||
self, stream_type: StreamType, name: str, volume: float
|
||||
self, stream_type: StreamType, index: int, volume: float, application: bool
|
||||
) -> None:
|
||||
"""Set a stream to volume input/output."""
|
||||
"""Set a stream to volume input/output/application."""
|
||||
|
||||
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)
|
||||
if application:
|
||||
stream = pulse.source_output_info(index)
|
||||
else:
|
||||
stream = pulse.source_info(index)
|
||||
else:
|
||||
# Get sink and set it as default
|
||||
stream = pulse.get_sink_by_name(name)
|
||||
if application:
|
||||
stream = pulse.sink_input_info(index)
|
||||
else:
|
||||
stream = pulse.sink_info(index)
|
||||
|
||||
# Set volume
|
||||
pulse.volume_set_all_chans(stream, volume)
|
||||
except PulseIndexError:
|
||||
_LOGGER.error("Can't find %s profile %s", stream_type, name)
|
||||
_LOGGER.error(
|
||||
"Can't find %s stream %d (App: %s)", stream_type, index, application
|
||||
)
|
||||
raise PulseAudioError() from None
|
||||
except PulseError as err:
|
||||
_LOGGER.error("Can't set %s volume: %s", name, err)
|
||||
_LOGGER.error("Can't set %d volume: %s", index, err)
|
||||
raise PulseAudioError() from None
|
||||
|
||||
# Run and Reload data
|
||||
await self.sys_run_in_executor(_set_volume)
|
||||
await self.update()
|
||||
|
||||
async def set_mute(
|
||||
self, stream_type: StreamType, index: int, mute: bool, application: bool
|
||||
) -> None:
|
||||
"""Set a stream to mute input/output/application."""
|
||||
|
||||
def _set_mute():
|
||||
try:
|
||||
with Pulse(PULSE_NAME) as pulse:
|
||||
if stream_type == StreamType.INPUT:
|
||||
if application:
|
||||
stream = pulse.source_output_info(index)
|
||||
else:
|
||||
stream = pulse.source_info(index)
|
||||
else:
|
||||
if application:
|
||||
stream = pulse.sink_input_info(index)
|
||||
else:
|
||||
stream = pulse.sink_info(index)
|
||||
|
||||
# Mute stream
|
||||
pulse.mute(stream, mute)
|
||||
except PulseIndexError:
|
||||
_LOGGER.error(
|
||||
"Can't find %s stream %d (App: %s)", stream_type, index, application
|
||||
)
|
||||
raise PulseAudioError() from None
|
||||
except PulseError as err:
|
||||
_LOGGER.error("Can't set %d volume: %s", index, err)
|
||||
raise PulseAudioError() from None
|
||||
|
||||
# Run and Reload data
|
||||
await self.sys_run_in_executor(_set_mute)
|
||||
await self.update()
|
||||
|
||||
async def ativate_profile(self, card_name: str, profile_name: str) -> None:
|
||||
"""Set a profile to volume input/output."""
|
||||
|
||||
@ -160,15 +226,59 @@ class SoundControl(CoreSysAttributes):
|
||||
with Pulse(PULSE_NAME) as pulse:
|
||||
server = pulse.server_info()
|
||||
|
||||
# Update applications
|
||||
self._applications.clear()
|
||||
for application in pulse.sink_input_list():
|
||||
self._applications.append(
|
||||
AudioApplication(
|
||||
application.proplist.get(
|
||||
"application.name", application.name
|
||||
),
|
||||
application.index,
|
||||
application.sink,
|
||||
StreamType.OUTPUT,
|
||||
application.volume.value_flat,
|
||||
bool(application.mute),
|
||||
application.proplist.get(
|
||||
"application.process.machine_id", ""
|
||||
).replace("-", "_"),
|
||||
)
|
||||
)
|
||||
for application in pulse.source_output_list():
|
||||
self._applications.append(
|
||||
AudioApplication(
|
||||
application.proplist.get(
|
||||
"application.name", application.name
|
||||
),
|
||||
application.index,
|
||||
application.source,
|
||||
StreamType.INPUT,
|
||||
application.volume.value_flat,
|
||||
bool(application.mute),
|
||||
application.proplist.get(
|
||||
"application.process.machine_id", ""
|
||||
).replace("-", "_"),
|
||||
)
|
||||
)
|
||||
|
||||
# Update output
|
||||
self._outputs.clear()
|
||||
for sink in pulse.sink_list():
|
||||
self._outputs.append(
|
||||
AudioStream(
|
||||
sink.name,
|
||||
sink.index,
|
||||
sink.description,
|
||||
sink.volume.value_flat,
|
||||
bool(sink.mute),
|
||||
sink.name == server.default_sink_name,
|
||||
sink.card if sink.card != 0xFFFFFFFF else None,
|
||||
[
|
||||
application
|
||||
for application in self._applications
|
||||
if application.stream_index == sink.index
|
||||
and application.stream_type == StreamType.OUTPUT
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
@ -181,9 +291,18 @@ class SoundControl(CoreSysAttributes):
|
||||
self._inputs.append(
|
||||
AudioStream(
|
||||
source.name,
|
||||
source.index,
|
||||
source.description,
|
||||
source.volume.value_flat,
|
||||
bool(source.mute),
|
||||
source.name == server.default_source_name,
|
||||
source.card if source.card != 0xFFFFFFFF else None,
|
||||
[
|
||||
application
|
||||
for application in self._applications
|
||||
if application.stream_index == source.index
|
||||
and application.stream_type == StreamType.INPUT
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
@ -205,7 +324,9 @@ class SoundControl(CoreSysAttributes):
|
||||
)
|
||||
|
||||
self._cards.append(
|
||||
SoundCard(card.name, card.driver, sound_profiles)
|
||||
SoundCard(
|
||||
card.name, card.index, card.driver, sound_profiles
|
||||
)
|
||||
)
|
||||
|
||||
except PulseOperationFailed as err:
|
||||
|
Loading…
x
Reference in New Issue
Block a user