From 95a3caa6f9a7cf58fcab5e5fcb6ea7c1dddbdc23 Mon Sep 17 00:00:00 2001 From: Tian Shilin Date: Thu, 5 Feb 2026 14:11:23 +0800 Subject: [PATCH] =?UTF-8?q?fix=EF=BC=9A=20Speaker=20Display=20Anomaly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit log: The audio module's output device list/dropdown relies on SoundWorker::cardsChanged() for refreshes. However, in certain backend implementations, plugging/unplugging wired headphones primarily triggers either the DBus port enabled state change (PortEnabledChanged) or solely CardsChanged events. Previously, only CardsWithoutUnavailableChanged was monitored, causing the UI to fail to refresh and persistently display “Speaker”. pms: bug-345709 --- src/plugin-sound/operation/soundworker.cpp | 81 ++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/src/plugin-sound/operation/soundworker.cpp b/src/plugin-sound/operation/soundworker.cpp index 4d6e1d7897..50e4963a71 100644 --- a/src/plugin-sound/operation/soundworker.cpp +++ b/src/plugin-sound/operation/soundworker.cpp @@ -52,6 +52,13 @@ SoundWorker::SoundWorker(SoundModel *model, QObject *parent) void SoundWorker::initConnect() { + auto refreshCards = [this] { + if (!m_soundDBusInter) + return; + // Trigger SoundModel::audioCardsChanged -> SoundWorker::cardsChanged to rebuild port list. + m_model->setAudioCards(m_soundDBusInter->cardsWithoutUnavailable()); + }; + connect(m_playAnimationTime, &QTimer::timeout, this, &SoundWorker::onAniTimerTimeOut); connect(m_model, &SoundModel::defaultSinkChanged, this, &SoundWorker::defaultSinkChanged); connect(m_model, &SoundModel::defaultSourceChanged, this, &SoundWorker::defaultSourceChanged); @@ -63,6 +70,33 @@ void SoundWorker::initConnect() connect(m_soundDBusInter, &SoundDBusProxy::MaxUIVolumeChanged, m_model, &SoundModel::setMaxUIVolume); connect(m_soundDBusInter, &SoundDBusProxy::IncreaseVolumeChanged, m_model, &SoundModel::setIncreaseVolume); connect(m_soundDBusInter, &SoundDBusProxy::CardsWithoutUnavailableChanged, m_model, &SoundModel::setAudioCards); + // Some backends only emit CardsChanged on port hotplug/availability updates. + // Treat it as a fallback to keep device list in sync. + connect(m_soundDBusInter, &SoundDBusProxy::CardsChanged, m_model, &SoundModel::setAudioCards); + // More fallbacks: hotplug may only change sink/source objects (PipeWire/PulseAudio). + connect(m_soundDBusInter, &SoundDBusProxy::SinksChanged, this, [refreshCards](const QList &value) { + Q_UNUSED(value); + refreshCards(); + }); + connect(m_soundDBusInter, &SoundDBusProxy::SourcesChanged, this, [refreshCards](const QList &value) { + Q_UNUSED(value); + refreshCards(); + }); + // Wired headset plug/unplug often changes port enabled state (not necessarily adding/removing cards). + connect(m_soundDBusInter, &SoundDBusProxy::PortEnabledChanged, this, + [this, refreshCards](uint cardId, const QString &portId, bool enabled) { + Port *port = m_model->findPort(portId, cardId); + if (!port) { + // If the port isn't present yet (e.g. previously unavailable), refresh card snapshot. + refreshCards(); + return; + } + + port->setEnabled(enabled); + m_model->updatePortCombo(); + m_model->updateSoundDeviceModel(port); + m_model->updateActiveComboIndex(); + }); connect(m_soundDBusInter, &SoundDBusProxy::ReduceNoiseChanged, m_model, &SoundModel::setReduceNoise); connect(m_soundDBusInter, &SoundDBusProxy::PausePlayerChanged, m_model, &SoundModel::setPausePlayer); connect(m_soundDBusInter, &SoundDBusProxy::BluetoothAudioModeOptsChanged, m_model, &SoundModel::setBluetoothAudioModeOpts); @@ -71,6 +105,10 @@ void SoundWorker::initConnect() connect(m_soundDBusInter, &SoundDBusProxy::EnabledChanged, m_model, &SoundModel::setEnableSoundEffect); connect(m_soundDBusInter, &SoundDBusProxy::pendingCallWatcherFinished, this, &SoundWorker::getSoundEnabledMapFinished); + // Fallback: Qt multimedia device hotplug notifications (covers cases where DBus signals are missing). + connect(m_mediaDevices, &QMediaDevices::audioInputsChanged, this, refreshCards); + connect(m_mediaDevices, &QMediaDevices::audioOutputsChanged, this, refreshCards); + connect(m_pingTimer, &QTimer::timeout, [this] { if (m_soundDBusInter) m_soundDBusInter->Tick(); }); connect(m_soundDBusInter, &SoundDBusProxy::HasBatteryChanged, m_model, &SoundModel::setIsLaptop); @@ -376,13 +414,56 @@ void SoundWorker::cardsChanged(const QString &cards) QStringList tmpPorts; + // Laptop internal cards (e.g. sof-hda-dsp) may report both speaker and headphones as available. + // To match UX requirement: when headphones are plugged, only show the plugged device (Headphones). + const auto isHeadphonesPort = [](const QJsonObject &jPort) -> bool { + const QString portId = jPort["Name"].toString(); + const QString desc = jPort["Description"].toString(); + return portId.contains("headphone", Qt::CaseInsensitive) + || desc.contains("headphone", Qt::CaseInsensitive) + || desc.contains(QStringLiteral("耳机")); + }; + // Prefer "active port" as the plug state signal, because on some laptops + // headphone port "Available" may stay 0 (Unknown) even when plugged. + bool shouldFilterToHeadphonesOut = false; + for (const QJsonValue &pV : jPorts) { + const QJsonObject jPort = pV.toObject(); + const double portAvai = jPort["Available"].toDouble(); + const auto dir = Port::Direction(jPort["Direction"].toDouble()); + if (dir != Port::Out || !isHeadphonesPort(jPort)) { + continue; + } + + const QString portId = jPort["Name"].toString(); + + // Case 1: backend reports explicit availability + if (portAvai == 2.0) { // 2 Available + shouldFilterToHeadphonesOut = true; + break; + } + + // Case 2: use active sink port as ground truth for "plugged" + if (cardId == m_activeOutputCard && portId == m_activeSinkPort) { + shouldFilterToHeadphonesOut = true; + break; + } + } + for (QJsonValue pV : jPorts) { QJsonObject jPort = pV.toObject(); const double portAvai = jPort["Available"].toDouble(); if (portAvai == 2.0 || portAvai == 0.0) { // 0 Unknown 1 Not available 2 Available const QString portId = jPort["Name"].toString(); const QString portName = jPort["Description"].toString(); + const auto dir = Port::Direction(jPort["Direction"].toDouble()); const bool isEnabled = jPort["Enabled"].toBool(); + + // If headphones are plugged/active on this card, hide other output ports (e.g. Speakers). + if (shouldFilterToHeadphonesOut) { + if (dir == Port::Out && !isHeadphonesPort(jPort)) { + continue; + } + } const bool isBluetooth = jPort["Bluetooth"].toBool(); Port *port = m_model->findPort(portId, cardId);