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 381ff3bdc..75f72db4d 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 @@ -252,6 +251,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 @@ -259,6 +259,7 @@ void Presenter::setMprisPlayer(const QString &serviceName, const QString &deskto << "Identity:" << identity; m_data->m_playerEngine->setMprisPlayer(serviceName, desktopEntry, identity); } +#endif void Presenter::prepareStartupAssets() { @@ -277,6 +278,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; }