From 3f2f8ac53629af4cb5a62c43b2a62e909bb0372b Mon Sep 17 00:00:00 2001 From: Cloud Date: Wed, 5 Aug 2026 08:46:24 +0800 Subject: [PATCH 1/2] feat: add Windows SMTC support and wrap MPRIS in Linux-only guards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Windows System Media Transport Controls (SMTC) support - Wrap MprisPlayer include and usage in Q_OS_LINUX ifdef - Use void* placeholder for non-Linux platforms to maintain ABI - Add platform guards for setMprisPlayer in presenter and main - Update tests to conditionally compile MPRIS-dependent code feat: 添加 Windows SMTC 支持并为 MPRIS 添加 Linux 条件编译 - 新增 Windows System Media Transport Controls (SMTC) 支持 - 使用 Q_OS_LINUX ifdef 将 MprisPlayer 包裹为仅 Linux 可用 - 非 Linux 平台使用 void* 占位符保持 ABI 兼容 - 为 presenter 和 main 中的 setMprisPlayer 添加平台保护 - 更新测试代码的条件编译 --- src/libdmusic/CMakeLists.txt | 5 + src/libdmusic/player/playerengine.cpp | 116 ++++++- src/libdmusic/player/playerengine.h | 8 +- src/libdmusic/player/winsmtc.cpp | 356 +++++++++++++++++++++ src/libdmusic/player/winsmtc.h | 59 ++++ src/libdmusic/presenter.cpp | 10 +- src/libdmusic/presenter.h | 8 +- src/music-player/main.cpp | 25 ++ tests/libdmusic-test/test_playerengine.cpp | 15 +- tests/libdmusic-test/test_presenter.cpp | 7 +- 10 files changed, 598 insertions(+), 11 deletions(-) create mode 100644 src/libdmusic/player/winsmtc.cpp create mode 100644 src/libdmusic/player/winsmtc.h diff --git a/src/libdmusic/CMakeLists.txt b/src/libdmusic/CMakeLists.txt index 267835180..e4f4b96db 100644 --- a/src/libdmusic/CMakeLists.txt +++ b/src/libdmusic/CMakeLists.txt @@ -79,5 +79,10 @@ target_link_libraries(${CMD_NAME} Qt6::Core Qt6::Multimedia Qt6::DBus Qt6::Sql Q # qt5_use_modules(${CMD_NAME} ) +# Windows SMTC support +if(WIN32 AND NOT MSVC) + target_link_libraries(${CMD_NAME} runtimeobject ole32 user32 propsys shell32) +endif() + include(GNUInstallDirs) install(TARGETS ${CMD_NAME} DESTINATION ${CMAKE_INSTALL_LIBDIR}) diff --git a/src/libdmusic/player/playerengine.cpp b/src/libdmusic/player/playerengine.cpp index 7d32ac689..bc58af634 100644 --- a/src/libdmusic/player/playerengine.cpp +++ b/src/libdmusic/player/playerengine.cpp @@ -1,5 +1,4 @@ -// Copyright (C) 2020 ~ 2020 Deepin Technology Co., Ltd. -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -15,7 +14,13 @@ #include #include +#ifdef Q_OS_LINUX #include +#endif + +#ifdef Q_OS_WIN +#include "winsmtc.h" +#endif #include "qtplayer.h" #include "vlcplayer.h" @@ -60,7 +65,14 @@ class PlayerEnginePrivate int m_pendingManualNavigation = 0; bool m_keepManualNavigation = false; PlayerBase *m_player = nullptr; +#ifdef Q_OS_LINUX MprisPlayer *m_mprisPlayer = nullptr; +#else + void *m_mprisPlayer = nullptr; +#endif +#ifdef Q_OS_WIN + WinSMTC *m_winSmtc = nullptr; +#endif QString m_playlistHash; DmGlobal::PlaybackMode m_playbackMode = DmGlobal::RepeatNull; int m_playingCount = 0; @@ -190,11 +202,20 @@ PlayerEngine::PlayerEngine(QObject *parent, PlayerBase *injectedPlayer) PlayerEngine::~PlayerEngine() { qCDebug(dmMusic) << "Destroying PlayerEngine"; +#ifdef Q_OS_LINUX if (m_data->m_mprisPlayer) { qCDebug(dmMusic) << "Destroying MprisPlayer"; delete m_data->m_mprisPlayer; m_data->m_mprisPlayer = nullptr; } +#endif +#ifdef Q_OS_WIN + if (m_data->m_winSmtc) { + m_data->m_winSmtc->shutdown(); + delete m_data->m_winSmtc; + m_data->m_winSmtc = nullptr; + } +#endif } double PlayerEngine::fadeInOutFactor() const @@ -216,6 +237,7 @@ void PlayerEngine::setFadeInOut(bool flag) } } +#ifdef Q_OS_LINUX void PlayerEngine::setMprisPlayer(const QString &serviceName, const QString &desktopEntry, const QString &identity) { qCDebug(dmMusic) << "Initializing MprisPlayer with service:" << serviceName; @@ -313,6 +335,75 @@ void PlayerEngine::setMprisPlayer(const QString &serviceName, const QString &des connect(m_data->m_mprisPlayer, &MprisPlayer::raiseRequested, this, &PlayerEngine::raiseRequested); qCDebug(dmMusic) << "MprisPlayer initialized"; } +#endif + +#ifdef Q_OS_WIN +void PlayerEngine::setWinSMTC(void *hwnd) +{ + if (!hwnd) return; + + m_data->m_winSmtc = new WinSMTC(this); + + if (!m_data->m_winSmtc->initialize(static_cast(hwnd))) { + qWarning() << "WinSMTC: Failed to initialize"; + delete m_data->m_winSmtc; + m_data->m_winSmtc = nullptr; + return; + } + + connect(m_data->m_winSmtc, &WinSMTC::playRequested, this, [this]() { + if (playbackStatus() == DmGlobal::Paused) { + resume(); + } else if (playbackStatus() != DmGlobal::Playing) { + if (isEmpty()) { + Q_EMIT playPlaylistRequested("all"); + } else { + if (!getMediaMeta().localPath.isEmpty() && !getMediaMeta().hash.isEmpty()) { + play(); + } else { + playNextMeta(false); + } + } + } else { + pauseNow(); + } + }); + + connect(m_data->m_winSmtc, &WinSMTC::pauseRequested, this, &PlayerEngine::pauseNow); + + connect(m_data->m_winSmtc, &WinSMTC::stopRequested, this, &PlayerEngine::stop); + + connect(m_data->m_winSmtc, &WinSMTC::nextRequested, this, [this]() { + playNextMeta(false); + }); + + connect(m_data->m_winSmtc, &WinSMTC::previousRequested, this, &PlayerEngine::playPreMeta); + + connect(this, &PlayerEngine::playbackStatusChanged, this, [this](DmGlobal::PlaybackStatus status) { + if (!m_data->m_winSmtc) return; + switch (status) { + case DmGlobal::Playing: + m_data->m_winSmtc->updatePlaybackStatus(1); + break; + case DmGlobal::Paused: + m_data->m_winSmtc->updatePlaybackStatus(2); + break; + default: + m_data->m_winSmtc->updatePlaybackStatus(0); + break; + } + }); + + m_data->m_winSmtc->setControlsEnabled(true, true, true, + hasNextPlayableMeta(getMediaMeta().hash), + hasPreviousPlayableMeta(getMediaMeta().hash)); + + MediaMeta currentMeta = getMediaMeta(); + if (!currentMeta.hash.isEmpty()) { + resetDBusMpris(currentMeta); + } +} +#endif void PlayerEngine::setMediaMeta(const QString &metaHash) { @@ -899,6 +990,7 @@ void PlayerEngine::playNextMeta(const DMusic::MediaMeta &meta, bool isAuto, bool void PlayerEngine::resetDBusMpris(const DMusic::MediaMeta &meta) { +#ifdef Q_OS_LINUX qCDebug(dmMusic) << "Resetting DBus Mpris with meta:" << meta.title; QVariantMap metadata; metadata.insert(Mpris::metadataToString(Mpris::Title), meta.title); @@ -920,6 +1012,26 @@ void PlayerEngine::resetDBusMpris(const DMusic::MediaMeta &meta) metadata.insert(Mpris::metadataToString(Mpris::ArtUrl), str); m_data->m_mprisPlayer->setMetadata(metadata); qCDebug(dmMusic) << "DBus Mpris reset with meta:" << meta.title; +#endif +#ifdef Q_OS_WIN + if (m_data->m_winSmtc) { + QString artPath = DmGlobal::cachePath() + "/images/" + meta.hash + ".jpg"; + QFileInfo artInfo(artPath); + if (!artInfo.exists()) { + artPath = DmGlobal::cachePath() + "/images/" + "default_cover_max.jpg"; + artInfo.setFile(artPath); + if (!artInfo.exists()) { + QIcon icon = QIcon::fromTheme("cover_max"); + icon.pixmap(QSize(50, 50)).save(artPath); + } + } + m_data->m_winSmtc->updateMetadata(meta.title, meta.artist, meta.album, + meta.length, artPath); + m_data->m_winSmtc->setControlsEnabled(true, true, true, + hasNextPlayableMeta(meta.hash), + hasPreviousPlayableMeta(meta.hash)); + } +#endif } void PlayerEngine::playNextMeta(bool isAuto, bool playFlag) diff --git a/src/libdmusic/player/playerengine.h b/src/libdmusic/player/playerengine.h index 7b502d3f1..132185380 100644 --- a/src/libdmusic/player/playerengine.h +++ b/src/libdmusic/player/playerengine.h @@ -1,5 +1,4 @@ -// Copyright (C) 2020 ~ 2020 Deepin Technology Co., Ltd. -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -22,7 +21,12 @@ class PlayerEngine : public QObject double fadeInOutFactor() const; void setFadeInOut(bool flag); +#ifdef Q_OS_LINUX void setMprisPlayer(const QString &serviceName, const QString &desktopEntry, const QString &identity); +#endif +#ifdef Q_OS_WIN + void setWinSMTC(void *hwnd); +#endif void setMediaMeta(const QString &metaHash); void setMediaMeta(const DMusic::MediaMeta &metaList); QStringList supportedSuffixList()const; diff --git a/src/libdmusic/player/winsmtc.cpp b/src/libdmusic/player/winsmtc.cpp new file mode 100644 index 000000000..428944fef --- /dev/null +++ b/src/libdmusic/player/winsmtc.cpp @@ -0,0 +1,356 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "winsmtc.h" + +#ifdef Q_OS_WIN + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Microsoft::WRL; +using namespace Microsoft::WRL::Wrappers; +using namespace ABI::Windows::Media; +using namespace ABI::Windows::Foundation; +using namespace ABI::Windows::Storage::Streams; + +typedef ITypedEventHandler ButtonPressedHandler; + +class ButtonDelegate : public ButtonPressedHandler +{ +public: + ButtonDelegate(WinSMTC *owner) : m_ref(1), m_owner(owner) {} + + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppv) override + { + if (!ppv) return E_POINTER; + if (riid == __uuidof(IUnknown) || riid == __uuidof(ButtonPressedHandler)) { + *ppv = static_cast(this); + AddRef(); + return S_OK; + } + *ppv = nullptr; + return E_NOINTERFACE; + } + + ULONG STDMETHODCALLTYPE AddRef() override { return InterlockedIncrement(&m_ref); } + + ULONG STDMETHODCALLTYPE Release() override + { + ULONG r = InterlockedDecrement(&m_ref); + if (r == 0) delete this; + return r; + } + + HRESULT STDMETHODCALLTYPE Invoke( + ISystemMediaTransportControls*, + ISystemMediaTransportControlsButtonPressedEventArgs* args) override + { + if (args) { + SystemMediaTransportControlsButton button; + if (SUCCEEDED(args->get_Button(&button))) { + switch (button) { + case SystemMediaTransportControlsButton_Play: + Q_EMIT m_owner->playRequested(); + break; + case SystemMediaTransportControlsButton_Pause: + Q_EMIT m_owner->pauseRequested(); + break; + case SystemMediaTransportControlsButton_Stop: + Q_EMIT m_owner->stopRequested(); + break; + case SystemMediaTransportControlsButton_Next: + Q_EMIT m_owner->nextRequested(); + break; + case SystemMediaTransportControlsButton_Previous: + Q_EMIT m_owner->previousRequested(); + break; + default: + break; + } + } + } + return S_OK; + } + +private: + LONG m_ref; + WinSMTC *m_owner; +}; + +void WinSMTC::ensureStartMenuShortcut() +{ + wchar_t exePath[MAX_PATH]; + GetModuleFileNameW(NULL, exePath, MAX_PATH); + QString appDir = QFileInfo(QString::fromWCharArray(exePath)).absolutePath(); + + PWSTR programsPath = nullptr; + if (FAILED(SHGetKnownFolderPath(FOLDERID_Programs, 0, NULL, &programsPath))) { + qDebug() << "WinSMTC: Failed to get Programs folder path"; + return; + } + QString programsDir = QString::fromWCharArray(programsPath); + CoTaskMemFree(programsPath); + + // Remove old English-name shortcuts + QDir().remove(programsDir + QStringLiteral("\\Deepin Music\\Deepin Music Player.lnk")); + QDir().remove(programsDir + QStringLiteral("\\Deepin Music\\deepin-music.lnk")); + QDir().rmdir(programsDir + QStringLiteral("\\Deepin Music")); + + // Create new Chinese-name shortcut + QString shortcutDir = programsDir + QStringLiteral("\\深度音乐"); + QDir().mkpath(shortcutDir); + QString shortcutPath = shortcutDir + QStringLiteral("\\深度音乐.lnk"); + + CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); + + ComPtr shellLink; + HRESULT hr = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(&shellLink)); + if (FAILED(hr)) { + qDebug() << "WinSMTC: Failed to create IShellLink:" << QString::number(hr, 16); + CoUninitialize(); + return; + } + + shellLink->SetPath(exePath); + shellLink->SetDescription(L"深度音乐"); + shellLink->SetWorkingDirectory(appDir.toStdWString().c_str()); + + QString iconPath = appDir + QStringLiteral("\\dsg\\img\\deepin-music.ico"); + if (QFileInfo::exists(iconPath)) { + shellLink->SetIconLocation(iconPath.toStdWString().c_str(), 0); + } + + // Set AUMID via IPropertyStore + ComPtr propStore; + hr = shellLink.As(&propStore); + if (SUCCEEDED(hr) && propStore) { + PROPVARIANT propVar; + PropVariantInit(&propVar); + propVar.vt = VT_LPWSTR; + // Use a heap-allocated copy since PropVariantClear will free it + propVar.pwszVal = static_cast(CoTaskMemAlloc(sizeof(WINSMTC_AUMID))); + if (propVar.pwszVal) { + memcpy(propVar.pwszVal, WINSMTC_AUMID, sizeof(WINSMTC_AUMID)); + propStore->SetValue(PKEY_AppUserModel_ID, propVar); + propStore->Commit(); + PropVariantClear(&propVar); + } + } + + // Save the shortcut + ComPtr persistFile; + hr = shellLink.As(&persistFile); + if (SUCCEEDED(hr) && persistFile) { + hr = persistFile->Save(shortcutPath.toStdWString().c_str(), TRUE); + if (FAILED(hr)) { + qWarning() << "WinSMTC: Failed to save shortcut:" << QString::number(hr, 16); + } + } + + CoUninitialize(); +} + +WinSMTC::WinSMTC(QObject *parent) : QObject(parent) {} + +WinSMTC::~WinSMTC() +{ + shutdown(); +} + +bool WinSMTC::initialize(HWND hwnd) +{ + if (m_initialized) return true; + if (!hwnd) { + qWarning() << "WinSMTC: Invalid HWND"; + return false; + } + + HRESULT hr = RoInitialize(RO_INIT_SINGLETHREADED); + if (hr == RPC_E_CHANGED_MODE) { + // COM already initialized, continue + } else if (FAILED(hr)) { + qWarning() << "WinSMTC: RoInitialize failed:" << QString::number(hr, 16); + return false; + } + + ComPtr interop; + hr = RoGetActivationFactory( + HStringReference(RuntimeClass_Windows_Media_SystemMediaTransportControls).Get(), + IID_PPV_ARGS(&interop)); + if (FAILED(hr) || !interop) { + qWarning() << "WinSMTC: Failed to get activation factory:" << QString::number(hr, 16); + return false; + } + + hr = interop->GetForWindow(hwnd, IID_PPV_ARGS(&m_smtc)); + if (FAILED(hr) || !m_smtc) { + qWarning() << "WinSMTC: GetForWindow failed:" << QString::number(hr, 16); + return false; + } + + m_smtc->put_IsEnabled(true); + + m_smtc->get_DisplayUpdater(&m_updater); + if (m_updater) { + m_updater->put_Type(MediaPlaybackType_Music); + m_updater->get_MusicProperties(&m_musicProps); + } else { + qWarning() << "WinSMTC: Failed to get display updater"; + } + + EventRegistrationToken token; + ButtonDelegate *handler = new ButtonDelegate(this); + hr = m_smtc->add_ButtonPressed(handler, &token); + handler->Release(); + + if (FAILED(hr)) { + qWarning() << "WinSMTC: Failed to add ButtonPressed handler:" << QString::number(hr, 16); + } + + m_initialized = true; + return true; +} + +void WinSMTC::shutdown() +{ + if (!m_initialized) return; + + if (m_smtc) { + m_smtc->put_IsEnabled(false); + m_smtc.Reset(); + } + m_updater.Reset(); + m_musicProps.Reset(); + m_initialized = false; +} + +void WinSMTC::updateMetadata(const QString &title, const QString &artist, + const QString &album, qint64 durationMs, + const QString &coverArtPath) +{ + if (!m_initialized || !m_updater || !m_musicProps) { + qWarning() << "WinSMTC: updateMetadata called but not initialized"; + return; + } + + m_updater->ClearAll(); + m_updater->put_Type(MediaPlaybackType_Music); + m_updater->get_MusicProperties(&m_musicProps); + + m_musicProps->put_Title(HStringReference(title.toStdWString().c_str()).Get()); + m_musicProps->put_Artist(HStringReference(artist.toStdWString().c_str()).Get()); + + ComPtr musicProps2; + if (SUCCEEDED(m_musicProps.As(&musicProps2)) && musicProps2) { + musicProps2->put_AlbumTitle(HStringReference(album.toStdWString().c_str()).Get()); + } + + // Set thumbnail from cover art file using in-memory stream + if (!coverArtPath.isEmpty()) { + QFileInfo fileInfo(coverArtPath); + if (fileInfo.exists()) { + QFile file(coverArtPath); + if (file.open(QIODevice::ReadOnly)) { + QByteArray imageData = file.readAll(); + file.close(); + + ComPtr memStreamIns; + HRESULT hrCreate = RoActivateInstance( + HStringReference(RuntimeClass_Windows_Storage_Streams_InMemoryRandomAccessStream).Get(), + &memStreamIns); + + if (SUCCEEDED(hrCreate) && memStreamIns) { + ComPtr memStream; + HRESULT hrQI = memStreamIns.As(&memStream); + if (SUCCEEDED(hrQI) && memStream) { + ComPtr outputStream; + HRESULT hrOut = memStream->GetOutputStreamAt(0, &outputStream); + if (SUCCEEDED(hrOut) && outputStream) { + ComPtr writerFactory; + HRESULT hrFactory = RoGetActivationFactory( + HStringReference(RuntimeClass_Windows_Storage_Streams_DataWriter).Get(), + IID_PPV_ARGS(&writerFactory)); + if (SUCCEEDED(hrFactory) && writerFactory) { + ComPtr writer; + HRESULT hrWriter = writerFactory->CreateDataWriter(outputStream.Get(), &writer); + if (SUCCEEDED(hrWriter) && writer) { + writer->WriteBytes( + static_cast(imageData.size()), + reinterpret_cast(imageData.data())); + + ComPtr> storeOp; + writer->StoreAsync(&storeOp); + if (storeOp) { + storeOp->GetResults(nullptr); + } + + memStream->Seek(0); + + ComPtr rasrStatics; + HRESULT hrStatics = RoGetActivationFactory( + HStringReference(RuntimeClass_Windows_Storage_Streams_RandomAccessStreamReference).Get(), + IID_PPV_ARGS(&rasrStatics)); + if (SUCCEEDED(hrStatics) && rasrStatics) { + ComPtr streamRef; + HRESULT hrRef = rasrStatics->CreateFromStream(memStream.Get(), &streamRef); + if (SUCCEEDED(hrRef) && streamRef) { + m_updater->put_Thumbnail(streamRef.Get()); + } + } + } + } + } + } + } + } + } + } + + HRESULT hr = m_updater->Update(); + if (FAILED(hr)) { + qWarning() << "WinSMTC: DisplayUpdater::Update failed:" << QString::number(hr, 16); + } +} + +void WinSMTC::updatePlaybackStatus(int status) +{ + if (!m_initialized || !m_smtc) return; + + MediaPlaybackStatus winrtStatus; + switch (status) { + case 0: winrtStatus = MediaPlaybackStatus_Stopped; break; + case 1: winrtStatus = MediaPlaybackStatus_Playing; break; + case 2: winrtStatus = MediaPlaybackStatus_Paused; break; + default: winrtStatus = MediaPlaybackStatus_Stopped; break; + } + + m_smtc->put_PlaybackStatus(winrtStatus); +} + +void WinSMTC::setControlsEnabled(bool play, bool pause, bool stop, + bool next, bool previous) +{ + if (!m_initialized || !m_smtc) return; + + m_smtc->put_IsPlayEnabled(play); + m_smtc->put_IsPauseEnabled(pause); + m_smtc->put_IsStopEnabled(stop); + m_smtc->put_IsNextEnabled(next); + m_smtc->put_IsPreviousEnabled(previous); +} + +#endif // Q_OS_WIN diff --git a/src/libdmusic/player/winsmtc.h b/src/libdmusic/player/winsmtc.h new file mode 100644 index 000000000..7a478d18b --- /dev/null +++ b/src/libdmusic/player/winsmtc.h @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef WINSMTC_H +#define WINSMTC_H + +#include +#include + +#ifdef Q_OS_WIN +#include +#include +#include +#include +#include + +#define WINSMTC_AUMID L"Deepin.DeepinMusicPlayer" + +class WinSMTC : public QObject +{ + Q_OBJECT +public: + explicit WinSMTC(QObject *parent = nullptr); + ~WinSMTC(); + + bool initialize(HWND hwnd); + void shutdown(); + + static void ensureStartMenuShortcut(); + + void updateMetadata(const QString &title, + const QString &artist, + const QString &album, + qint64 durationMs, + const QString &coverArtPath = QString()); + + void updatePlaybackStatus(int status); + + void setControlsEnabled(bool play, bool pause, bool stop, + bool next, bool previous); + +signals: + void playRequested(); + void pauseRequested(); + void stopRequested(); + void nextRequested(); + void previousRequested(); + +private: + Microsoft::WRL::ComPtr m_smtc; + Microsoft::WRL::ComPtr m_updater; + Microsoft::WRL::ComPtr m_musicProps; + bool m_initialized = false; +}; + +#endif // Q_OS_WIN + +#endif // WINSMTC_H diff --git a/src/libdmusic/presenter.cpp b/src/libdmusic/presenter.cpp index 8679c6dcf..51d62b0b4 100644 --- a/src/libdmusic/presenter.cpp +++ b/src/libdmusic/presenter.cpp @@ -1,5 +1,4 @@ -// Copyright (C) 2020 ~ 2026 Uniontech Software Technology Co., Ltd. -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -255,6 +254,7 @@ Presenter::~Presenter() } } +#ifdef Q_OS_LINUX void Presenter::setMprisPlayer(const QString &serviceName, const QString &desktopEntry, const QString &identity) { qCDebug(dmMusic) << "Setting MPRIS player - Service:" << serviceName @@ -262,6 +262,7 @@ void Presenter::setMprisPlayer(const QString &serviceName, const QString &deskto << "Identity:" << identity; m_data->m_playerEngine->setMprisPlayer(serviceName, desktopEntry, identity); } +#endif void Presenter::prepareStartupAssets() { @@ -280,6 +281,11 @@ QStringList Presenter::supportedSuffixList() const return suffixList; } +PlayerEngine* Presenter::playerEngine() const +{ + return m_data->m_playerEngine; +} + QColor Presenter::getMainColorByKmeans() { qCDebug(dmMusic) << "Getting main color from KMeans clustering"; diff --git a/src/libdmusic/presenter.h b/src/libdmusic/presenter.h index 8bc6e3e7f..a56876584 100644 --- a/src/libdmusic/presenter.h +++ b/src/libdmusic/presenter.h @@ -1,5 +1,4 @@ -// Copyright (C) 2020 ~ 2020 Deepin Technology Co., Ltd. -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -16,6 +15,7 @@ #include "global.h" class PresenterPrivate; +class PlayerEngine; class LIBDMUSICSHARED_EXPORT Presenter : public QObject { Q_OBJECT @@ -24,7 +24,9 @@ class LIBDMUSICSHARED_EXPORT Presenter : public QObject ~Presenter(); // dbus接口 +#ifdef Q_OS_LINUX Q_INVOKABLE void setMprisPlayer(const QString &serviceName, const QString &desktopEntry, const QString &identity); +#endif void prepareStartupAssets(); // 播放控制 @@ -109,6 +111,8 @@ class LIBDMUSICSHARED_EXPORT Presenter : public QObject // 退出 Q_INVOKABLE void forceExit(); + PlayerEngine* playerEngine() const; + public slots: // 播放器 void play(); diff --git a/src/music-player/main.cpp b/src/music-player/main.cpp index c1ed9c96d..1a9d15b6a 100644 --- a/src/music-player/main.cpp +++ b/src/music-player/main.cpp @@ -21,6 +21,10 @@ #include #endif +#ifdef Q_OS_WIN +#include +#endif + #include #include #include @@ -41,6 +45,8 @@ #include "effect/shaderimageview.h" #include "effect/shaderdataview.h" #include "presenter.h" +#include "player/playerengine.h" +#include "player/winsmtc.h" #include "util/eventsfilter.h" #include "util/shortcut.h" #include "util/dbusadpator.h" @@ -128,6 +134,16 @@ int main(int argc, char *argv[]) qputenv("D_POPUP_MODE", "embed"); #ifdef Q_OS_WIN qputenv("D_DTK_DISABLE_INWINDOWBLUR", "1"); + typedef HRESULT (WINAPI *SetCurrentProcessExplicitAppUserModelIDProc)(PCWSTR); + HMODULE shell32 = LoadLibraryW(L"shell32.dll"); + if (shell32) { + auto proc = (SetCurrentProcessExplicitAppUserModelIDProc)GetProcAddress(shell32, "SetCurrentProcessExplicitAppUserModelID"); + if (proc) { + proc(L"Deepin.DeepinMusicPlayer"); + } + FreeLibrary(shell32); + } + WinSMTC::ensureStartMenuShortcut(); #endif QGuiApplication *app = new QGuiApplication(argc, argv); @@ -226,25 +242,33 @@ int main(int argc, char *argv[]) EventsFilter eventsFilter(presenter.data()); Shortcut shortcut(presenter.data()); +#ifdef Q_OS_LINUX ApplicationAdaptor adaptor(presenter.data()); QDBusConnection::sessionBus().registerObject("/org/mpris/speech", "com.deepin.speech", &adaptor, QDBusConnection::RegisterOption::ExportAllSlots); +#endif +#ifdef Q_OS_LINUX presenter->setMprisPlayer("DeepinMusic", "deepin-music", "Deepin Music Player"); +#endif engine.rootContext()->setContextProperty("Presenter", presenter.data()); engine.rootContext()->setContextProperty("EventsFilter", &eventsFilter); engine.rootContext()->setContextProperty("ShortcutDialg", &shortcut); engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); + logStartupStage("qml-components-created"); engine.rootObjects()[0]->installEventFilter(&eventsFilter); #ifdef Q_OS_WIN if (auto *window = qobject_cast(engine.rootObjects()[0])) { window->setIcon(QIcon(":/dsg/img/deepin-music.svg")); + presenter->playerEngine()->setWinSMTC(reinterpret_cast(window->winId())); } #endif + if (engine.rootObjects().isEmpty()) { qCDebug(dmMusic) << "engine.rootObjects().isEmpty(), return -1"; return -1; } + if (auto *window = qobject_cast(engine.rootObjects().first())) { const auto deferredUiTriggered = std::make_shared(false); const auto triggerDeferredUi = [&startupTimer, window, presenter = presenter.data(), deferredUiTriggered](const char *trigger) { @@ -270,6 +294,7 @@ int main(int argc, char *argv[]) triggerDeferredUi("fallback-timeout"); }); } + // 导入自动播放 if (!OpenFilePaths.isEmpty()) { qCDebug(dmMusic) << "OpenFilePaths: " << OpenFilePaths; diff --git a/tests/libdmusic-test/test_playerengine.cpp b/tests/libdmusic-test/test_playerengine.cpp index 3c8547859..9f2c9f842 100644 --- a/tests/libdmusic-test/test_playerengine.cpp +++ b/tests/libdmusic-test/test_playerengine.cpp @@ -1,5 +1,4 @@ -// Copyright (C) 2020 ~ 2021 Uniontech Software Technology Co., Ltd. -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -252,7 +251,9 @@ TEST(PlayerEngineInjectedTest, forcePlayTriggersFakePlay) auto fake = std::make_unique(); auto engine = std::make_unique(nullptr, fake.get()); // setMediaMeta→resetDBusMpris 依赖 m_mprisPlayer 已初始化,否则 SEGV +#ifdef Q_OS_LINUX engine->setMprisPlayer("org.test.Mpris", "test", "Test"); +#endif DMusic::MediaMeta m; m.hash = "h1"; m.localPath = "/tmp/x.mp3"; engine->addMetasToPlayList(QList{m}); engine->forcePlay(); // setMediaMeta(first) + play() @@ -264,7 +265,9 @@ TEST(PlayerEngineInjectedTest, stopResetsFakeState) { auto fake = std::make_unique(); auto engine = std::make_unique(nullptr, fake.get()); +#ifdef Q_OS_LINUX engine->setMprisPlayer("org.test.Mpris", "test", "Test"); // 同上,避免 resetDBusMpris 崩溃 +#endif engine->stop(); // m_player->stop + setMediaMeta(empty) EXPECT_EQ(fake->m_fakeState, DmGlobal::Stopped); } @@ -292,7 +295,9 @@ TEST(PlayerEngineFadeInOutTest, setFadeInOutFactorChangesValue) TEST(PlayerEngineMediaMetaTest, setMediaMetaByHashWithExistingMeta) { std::unique_ptr eng(makeEngine()); +#ifdef Q_OS_LINUX eng->setMprisPlayer("org.test.Mpris", "test", "Test"); // 初始化 mprisPlayer +#endif DMusic::MediaMeta m; m.hash = "test-hash-001"; m.title = "Test Song"; @@ -306,7 +311,9 @@ TEST(PlayerEngineMediaMetaTest, setMediaMetaByHashWithExistingMeta) TEST(PlayerEngineMediaMetaTest, setMediaMetaByMetaObject) { std::unique_ptr eng(makeEngine()); +#ifdef Q_OS_LINUX eng->setMprisPlayer("org.test.Mpris", "test", "Test"); // 初始化 mprisPlayer +#endif DMusic::MediaMeta m; m.hash = "test-hash-002"; m.title = "Test Song 2"; @@ -449,7 +456,9 @@ TEST(PlayerEngineRemovePlayingTest, removePlayingMetaDoesNotCrash) { auto fake = std::make_unique(); auto engine = std::make_unique(nullptr, fake.get()); +#ifdef Q_OS_LINUX engine->setMprisPlayer("org.test.Mpris", "test", "Test"); +#endif DMusic::MediaMeta m1; m1.hash = "play1"; m1.localPath = "/tmp/p1.mp3"; DMusic::MediaMeta m2; m2.hash = "play2"; m2.localPath = "/tmp/p2.mp3"; @@ -500,7 +509,9 @@ struct InjectedEngine { fake = std::make_unique(); engine = std::make_unique(nullptr, fake.get()); if (withMpris) { +#ifdef Q_OS_LINUX engine->setMprisPlayer("org.test.Mpris", "test", "Test"); +#endif } } }; diff --git a/tests/libdmusic-test/test_presenter.cpp b/tests/libdmusic-test/test_presenter.cpp index 495f527a4..8707f9275 100644 --- a/tests/libdmusic-test/test_presenter.cpp +++ b/tests/libdmusic-test/test_presenter.cpp @@ -1,4 +1,5 @@ -// Copyright (C) 2020 ~ 2021 Uniontech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2020 - 2026 UnionTech Software Technology Co., Ltd. +// // SPDX-License-Identifier: GPL-3.0-or-later // presenter.cpp 的单元测试:覆盖巨型编排类的 getter/setter、空 DB 防御分支、 @@ -32,7 +33,9 @@ class PresenterTest : public ::testing::Test { // 每个用例新建实例,确保 DB / 设置状态隔离 m_presenter = new Presenter("unknowAlbum", "unknowArtist"); +#ifdef Q_OS_LINUX m_presenter->setMprisPlayer("DeepinMusic", "deepin-music", "Deepin Music Player"); +#endif } void TearDown() override @@ -54,7 +57,9 @@ TEST(Presenter, Construct) { Presenter *presenter = new Presenter("unknowAlbum", "unknowArtist"); ASSERT_NE(presenter, nullptr); +#ifdef Q_OS_LINUX presenter->setMprisPlayer("DeepinMusic", "deepin-music", "Deepin Music Player"); +#endif delete presenter; } From 535788fbd0ed40c6beaab7ac4c5a219791a00dda Mon Sep 17 00:00:00 2001 From: Cloud Date: Sat, 29 Aug 2026 10:05:38 +0800 Subject: [PATCH 2/2] fix: harden WinSMTC teardown and fix SMTC play fallback per review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Store the ButtonPressed registration token as a member and remove it in shutdown() before releasing SMTC, fixing use-after-free crash when a media key is dispatched after the app exits - Guard setWinSMTC() against double initialization to avoid leaking the old WinSMTC object and duplicate signal connections - Move engine.rootObjects() emptiness check before first access in main.cpp to avoid undefined behavior when QML loading fails - Make the SMTC play fallback actually start playback (play() falls back to forcePlay() when the current meta has no localPath) - Pair RoInitialize/RoUninitialize and CoInitializeEx/CoUninitialize, and validate DataWriter StoreAsync results fix: 加固 WinSMTC 释放流程并修复 SMTC 播放回退 - ButtonPressed 注册 token 保存为成员,shutdown() 中先解绑再释放 SMTC, 修复应用退出后系统派发媒体键导致的 use-after-free 崩溃 - setWinSMTC() 增加双重初始化保护,避免泄漏旧对象并重复 connect - main.cpp 将 engine.rootObjects() 判空上移到首次访问之前, 避免 QML 加载失败时的未定义行为 - SMTC 播放回退改为真正触发播放(当前曲目无 localPath 时 play() 内部走 forcePlay()),按“播放”必定有声音 - 配对 RoInitialize/RoUninitialize 与 CoInitializeEx/CoUninitialize, 并校验 DataWriter StoreAsync 返回值 --- src/libdmusic/player/playerengine.cpp | 9 +++--- src/libdmusic/player/winsmtc.cpp | 40 ++++++++++++++++++++------- src/libdmusic/player/winsmtc.h | 2 ++ src/music-player/main.cpp | 11 ++++---- 4 files changed, 42 insertions(+), 20 deletions(-) diff --git a/src/libdmusic/player/playerengine.cpp b/src/libdmusic/player/playerengine.cpp index bc58af634..bfbb499aa 100644 --- a/src/libdmusic/player/playerengine.cpp +++ b/src/libdmusic/player/playerengine.cpp @@ -341,6 +341,7 @@ void PlayerEngine::setMprisPlayer(const QString &serviceName, const QString &des void PlayerEngine::setWinSMTC(void *hwnd) { if (!hwnd) return; + if (m_data->m_winSmtc) return; m_data->m_winSmtc = new WinSMTC(this); @@ -358,11 +359,9 @@ void PlayerEngine::setWinSMTC(void *hwnd) if (isEmpty()) { Q_EMIT playPlaylistRequested("all"); } else { - if (!getMediaMeta().localPath.isEmpty() && !getMediaMeta().hash.isEmpty()) { - play(); - } else { - playNextMeta(false); - } + // play() falls back to forcePlay() when the current meta has + // no localPath, so pressing Play always produces sound + play(); } } else { pauseNow(); diff --git a/src/libdmusic/player/winsmtc.cpp b/src/libdmusic/player/winsmtc.cpp index 428944fef..2244f4311 100644 --- a/src/libdmusic/player/winsmtc.cpp +++ b/src/libdmusic/player/winsmtc.cpp @@ -114,14 +114,20 @@ void WinSMTC::ensureStartMenuShortcut() QDir().mkpath(shortcutDir); QString shortcutPath = shortcutDir + QStringLiteral("\\深度音乐.lnk"); - CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); + // Only pair CoUninitialize when we actually own the initialization + HRESULT coInit = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); + if (FAILED(coInit) && coInit != RPC_E_CHANGED_MODE) { + qWarning() << "WinSMTC: CoInitializeEx failed:" << QString::number(coInit, 16); + return; + } + bool coOwned = SUCCEEDED(coInit); ComPtr shellLink; HRESULT hr = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink)); if (FAILED(hr)) { qDebug() << "WinSMTC: Failed to create IShellLink:" << QString::number(hr, 16); - CoUninitialize(); + if (coOwned) CoUninitialize(); return; } @@ -161,7 +167,7 @@ void WinSMTC::ensureStartMenuShortcut() } } - CoUninitialize(); + if (coOwned) CoUninitialize(); } WinSMTC::WinSMTC(QObject *parent) : QObject(parent) {} @@ -181,8 +187,10 @@ bool WinSMTC::initialize(HWND hwnd) HRESULT hr = RoInitialize(RO_INIT_SINGLETHREADED); if (hr == RPC_E_CHANGED_MODE) { - // COM already initialized, continue - } else if (FAILED(hr)) { + // COM already initialized in another mode, we don't own it + } else if (SUCCEEDED(hr)) { + m_roInitialized = true; + } else { qWarning() << "WinSMTC: RoInitialize failed:" << QString::number(hr, 16); return false; } @@ -212,13 +220,13 @@ bool WinSMTC::initialize(HWND hwnd) qWarning() << "WinSMTC: Failed to get display updater"; } - EventRegistrationToken token; ButtonDelegate *handler = new ButtonDelegate(this); - hr = m_smtc->add_ButtonPressed(handler, &token); + hr = m_smtc->add_ButtonPressed(handler, &m_buttonToken); handler->Release(); if (FAILED(hr)) { qWarning() << "WinSMTC: Failed to add ButtonPressed handler:" << QString::number(hr, 16); + m_buttonToken.value = 0; } m_initialized = true; @@ -230,11 +238,21 @@ void WinSMTC::shutdown() if (!m_initialized) return; if (m_smtc) { + // Unregister the ButtonPressed handler first to avoid use-after-free + // when the system dispatches a media key after this object is gone + if (m_buttonToken.value != 0) { + m_smtc->remove_ButtonPressed(m_buttonToken); + m_buttonToken.value = 0; + } m_smtc->put_IsEnabled(false); m_smtc.Reset(); } m_updater.Reset(); m_musicProps.Reset(); + if (m_roInitialized) { + RoUninitialize(); + m_roInitialized = false; + } m_initialized = false; } @@ -293,9 +311,11 @@ void WinSMTC::updateMetadata(const QString &title, const QString &artist, reinterpret_cast(imageData.data())); ComPtr> storeOp; - writer->StoreAsync(&storeOp); - if (storeOp) { - storeOp->GetResults(nullptr); + if (SUCCEEDED(writer->StoreAsync(&storeOp)) && storeOp) { + unsigned int written = 0; + if (FAILED(storeOp->GetResults(&written))) { + qWarning() << "WinSMTC: DataWriter StoreAsync failed"; + } } memStream->Seek(0); diff --git a/src/libdmusic/player/winsmtc.h b/src/libdmusic/player/winsmtc.h index 7a478d18b..dd2a52f2e 100644 --- a/src/libdmusic/player/winsmtc.h +++ b/src/libdmusic/player/winsmtc.h @@ -51,6 +51,8 @@ class WinSMTC : public QObject Microsoft::WRL::ComPtr m_smtc; Microsoft::WRL::ComPtr m_updater; Microsoft::WRL::ComPtr m_musicProps; + ABI::Windows::Foundation::EventRegistrationToken m_buttonToken{}; + bool m_roInitialized = false; bool m_initialized = false; }; diff --git a/src/music-player/main.cpp b/src/music-player/main.cpp index 1a9d15b6a..865923a04 100644 --- a/src/music-player/main.cpp +++ b/src/music-player/main.cpp @@ -256,6 +256,12 @@ int main(int argc, char *argv[]) engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); logStartupStage("qml-components-created"); + + if (engine.rootObjects().isEmpty()) { + qCDebug(dmMusic) << "engine.rootObjects().isEmpty(), return -1"; + return -1; + } + engine.rootObjects()[0]->installEventFilter(&eventsFilter); #ifdef Q_OS_WIN if (auto *window = qobject_cast(engine.rootObjects()[0])) { @@ -264,11 +270,6 @@ int main(int argc, char *argv[]) } #endif - if (engine.rootObjects().isEmpty()) { - qCDebug(dmMusic) << "engine.rootObjects().isEmpty(), return -1"; - return -1; - } - if (auto *window = qobject_cast(engine.rootObjects().first())) { const auto deferredUiTriggered = std::make_shared(false); const auto triggerDeferredUi = [&startupTimer, window, presenter = presenter.data(), deferredUiTriggered](const char *trigger) {