feat(qconnect): gate volume on renderer volume_remote_control capability

Add volume_remote_control to QconnectRendererInfo plus the
renderer_allows_remote_volume predicate (absent => allowed, only an explicit
non-1 value disables). Parse it from device_info.capabilities in the ADD and
UPDATE renderer paths, and short-circuit set_volume_if_remote when the active
renderer disallows remote volume (returns handled no-op so the frontend does
not fall back to local). Frontend surfaces volume_remote_control in the session
snapshot, derives a unified volume lock + reason, guards handleVolumeChange,
and shows a distinct player.volumeLockedRemote tooltip (added to all 5 locales).

cc
This commit is contained in:
vicrodh committed 2026-06-01 04:44:58 -06:00
1 parent d18bec1905
commit c4016e539f
12 files changed
+111 -6

No files matched your search

+7 -1
View File
@@ -106,6 +106,9 @@
qconnectBusy?: boolean;
showQconnectDevButton?: boolean;
volumeLocked?: boolean;
// P1-5: distinguishes the hardware (ALSA Direct) lock from the remote
// renderer (cast device) lock so the tooltip can be specific.
volumeLockedReason?: 'hardware' | 'remote';
bufferProgress?: number | null;
}
@@ -162,6 +165,7 @@
qconnectBusy = false,
showQconnectDevButton = false,
volumeLocked = false,
volumeLockedReason = 'hardware',
bufferProgress = null,
}: Props = $props();
@@ -823,7 +827,9 @@
{#if volumeLocked}
<button
class="control-btn volume-btn"
title={$translateStore('player.volumeLockedHw')}
title={volumeLockedReason === 'remote'
? $translateStore('player.volumeLockedRemote')
: $translateStore('player.volumeLockedHw')}
disabled
>
<Volume2 size={16} />
+1
View File
@@ -1540,6 +1540,7 @@
"volumeUp": "Lauter",
"volumeDown": "Leiser",
"volumeLockedHw": "Lautstärke auf 100% fixiert — ALSA Direct hw: (am DAC/Hardware einstellen)",
"volumeLockedRemote": "Lautstärkeregelung durch das Cast-Gerät deaktiviert",
"clearQueue": "Warteschlange leeren",
"queueEmpty": "Warteschlange ist leer",
"queueEmptyDescription": "Suche nach Musik oder durchsuchen Sie Ihre Bibliothek, um Titel hinzuzufügen.",
+1
View File
@@ -1542,6 +1542,7 @@
"volumeUp": "Increase volume",
"volumeDown": "Decrease volume",
"volumeLockedHw": "Volume locked at 100% — ALSA Direct hw: (adjust at DAC/hardware level)",
"volumeLockedRemote": "Volume control disabled by the cast device",
"clearQueue": "Clear Queue",
"queueEmpty": "Queue is empty",
"queueEmptyDescription": "Search for music or browse your library to add tracks.",
+1
View File
@@ -1540,6 +1540,7 @@
"volumeUp": "Subir volumen",
"volumeDown": "Bajar volumen",
"volumeLockedHw": "Volumen fijo al 100% — ALSA Direct hw: (ajusta desde el DAC/hardware)",
"volumeLockedRemote": "El control de volumen está deshabilitado por el dispositivo",
"clearQueue": "Limpiar Cola",
"queueEmpty": "La cola está vacía",
"queueEmptyDescription": "Busca música o explora tu biblioteca para agregar pistas.",
+1
View File
@@ -1540,6 +1540,7 @@
"volumeUp": "Augmenter le volume",
"volumeDown": "Diminuer le volume",
"volumeLockedHw": "Volume verrouillé à 100% — ALSA Direct hw: (réglez au niveau DAC/matériel)",
"volumeLockedRemote": "Contrôle du volume désactivé par l'appareil de diffusion",
"clearQueue": "Vider la file d'attente",
"queueEmpty": "La file d'attente est vide",
"queueEmptyDescription": "Recherchez de la musique ou parcourez votre bibliothèque pour ajouter des titres.",
+1
View File
@@ -1540,6 +1540,7 @@
"volumeUp": "Aumentar volume",
"volumeDown": "Diminuir volume",
"volumeLockedHw": "Volume fixo em 100% — ALSA Direct hw: (ajuste no DAC/hardware)",
"volumeLockedRemote": "Controle de volume desativado pelo dispositivo de transmissão",
"clearQueue": "Limpar Fila",
"queueEmpty": "A fila está vazia",
"queueEmptyDescription": "Pesquise músicas ou navegue pela sua biblioteca para adicionar faixas.",
+3
View File
@@ -32,6 +32,9 @@ export type QconnectRendererInfo = {
brand?: string | null;
model?: string | null;
device_type?: number | null;
// P1-5: capabilities.volume_remote_control. 1 == ALLOWED. null/undefined ==
// not advertised (treated as allowed). Any other value disables remote volume.
volume_remote_control?: number | null;
};
export type QconnectSessionSnapshot = {
+27 -3
View File
@@ -1125,6 +1125,26 @@
const qconnectPeerRendererActive = $derived(
isQconnectPeerRendererActive(qobuzConnectSessionSnapshot)
);
// P1-5: respect the active renderer's volume_remote_control capability.
// Absent (null/undefined) => allowed; only an explicit non-1 value disables.
const qconnectRemoteVolumeAllowed = $derived.by(() => {
if (!qconnectPeerRendererActive) return true;
const snap = qobuzConnectSessionSnapshot;
const activeId = snap?.active_renderer_id ?? null;
const info = snap?.renderers?.find((r) => r.renderer_id === activeId) ?? null;
const vrc = info?.volume_remote_control;
return vrc == null || vrc === 1;
});
// P1-5: unified volume-lock state shared by the now-playing bar + immersive
// player. Hardware lock applies only locally; remote lock applies when a peer
// renderer is active and disallows remote volume.
const qconnectVolumeLocked = $derived(
(isAlsaDirectHw && !qconnectPeerRendererActive) ||
(qconnectPeerRendererActive && !qconnectRemoteVolumeAllowed)
);
const qconnectVolumeLockedReason: 'hardware' | 'remote' = $derived(
qconnectPeerRendererActive && !qconnectRemoteVolumeAllowed ? 'remote' : 'hardware'
);
const qconnectSuppressLocalPlaybackAutomation = $derived(
shouldQconnectSuppressLocalPlaybackAutomation(
isQobuzConnectConnected,
@@ -2887,6 +2907,8 @@
async function handleVolumeChange(newVolume: number) {
// ALSA Direct hw: locks volume at 100% — unless controlling a remote renderer
if (isAlsaDirectHw && !qconnectPeerRendererActive) return;
// P1-5: the active remote renderer disallows remote volume control.
if (qconnectPeerRendererActive && !qconnectRemoteVolumeAllowed) return;
try {
const handledRemotely = await invoke<boolean>('v2_qconnect_set_volume_if_remote', { volume: newVolume });
@@ -7141,7 +7163,8 @@
onToggleQconnectConnection={handleQobuzConnectButton}
qconnectBusy={qobuzConnectBusy}
{showQconnectDevButton}
volumeLocked={isAlsaDirectHw && !qconnectPeerRendererActive}
volumeLocked={qconnectVolumeLocked}
volumeLockedReason={qconnectVolumeLockedReason}
{bufferProgress}
/>
{:else}
@@ -7164,7 +7187,8 @@
onToggleQconnectConnection={handleQobuzConnectButton}
qconnectBusy={qobuzConnectBusy}
{showQconnectDevButton}
volumeLocked={isAlsaDirectHw && !qconnectPeerRendererActive}
volumeLocked={qconnectVolumeLocked}
volumeLockedReason={qconnectVolumeLockedReason}
/>
{/if}
@@ -7198,7 +7222,7 @@
{volume}
onVolumeChange={handleVolumeChange}
onToggleMute={handleToggleMute}
volumeLocked={isAlsaDirectHw && !qconnectPeerRendererActive}
volumeLocked={qconnectVolumeLocked}
{isShuffle}
onToggleShuffle={toggleShuffle}
{repeatMode}