diff --git a/CMakeLists.txt b/CMakeLists.txt index 9db5a974a..b37c589a1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1293,7 +1293,7 @@ endif() optional_source(HAVE_ALSA SOURCES src/engine/alsadevicefinder.cpp src/engine/alsapcmdevicefinder.cpp) optional_source(HAVE_PULSE SOURCES src/engine/pulsedevicefinder.cpp) -optional_source(MSVC SOURCES src/engine/uwpdevicefinder.cpp src/engine/asiodevicefinder.cpp) +optional_source(MSVC SOURCES src/engine/uwpdevicefinder.cpp src/engine/asiodevicefinder.cpp src/core/windowsmediacontroller.cpp HEADERS src/core/windowsmediacontroller.h) optional_source(HAVE_CHROMAPRINT SOURCES src/engine/chromaprinter.cpp) optional_source(HAVE_MUSICBRAINZ diff --git a/src/core/windowsmediacontroller.cpp b/src/core/windowsmediacontroller.cpp new file mode 100644 index 000000000..5285c1da4 --- /dev/null +++ b/src/core/windowsmediacontroller.cpp @@ -0,0 +1,237 @@ +/* + * Strawberry Music Player + * Copyright 2025, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#include + +#include +#include +#include + +#pragma push_macro("interface") +#undef interface +#include +#include +#include +#include +#include +#pragma pop_macro("interface") + +#include "core/logging.h" +#include "windowsmediacontroller.h" + +#include "core/song.h" +#include "core/player.h" +#include "engine/enginebase.h" +#include "playlist/playlistmanager.h" +#include "covermanager/currentalbumcoverloader.h" +#include "covermanager/albumcoverloaderresult.h" + +using namespace winrt; +using namespace Windows::Media; +using namespace Windows::Storage; +using namespace Windows::Storage::Streams; + +WindowsMediaController::WindowsMediaController(HWND hwnd, + const SharedPtr player, + const SharedPtr playlist_manager, + const SharedPtr current_albumcover_loader, + QObject *parent) + : QObject(parent), + player_(player), + playlist_manager_(playlist_manager), + current_albumcover_loader_(current_albumcover_loader), + smtc_(nullptr) { + + try { + // Initialize WinRT + winrt::init_apartment(); + + // Get the SystemMediaTransportControls instance using the interop interface + auto interop = winrt::get_activation_factory(); + winrt::com_ptr smtc_inspectable; + winrt::check_hresult(interop->GetForWindow(hwnd, winrt::guid_of(), smtc_inspectable.put_void())); + + auto smtc = smtc_inspectable.as(); + smtc_ = new SystemMediaTransportControls(smtc); + + auto controls = static_cast(smtc_); + + // Enable the controls + controls->IsEnabled(true); + controls->IsPlayEnabled(true); + controls->IsPauseEnabled(true); + controls->IsStopEnabled(true); + controls->IsNextEnabled(true); + controls->IsPreviousEnabled(true); + + // Setup button handlers + SetupButtonHandlers(); + + // Connect signals from Player + QObject::connect(&*player_->engine(), &EngineBase::StateChanged, this, &WindowsMediaController::EngineStateChanged); + QObject::connect(&*playlist_manager_, &PlaylistManager::CurrentSongChanged, this, &WindowsMediaController::CurrentSongChanged); + QObject::connect(&*current_albumcover_loader_, &CurrentAlbumCoverLoader::AlbumCoverLoaded, this, &WindowsMediaController::AlbumCoverLoaded); + + qLog(Info) << "Windows Media Transport Controls initialized successfully"; + } + catch (const hresult_error &e) { + qLog(Warning) << "Failed to initialize Windows Media Transport Controls:" << QString::fromWCharArray(e.message().c_str()); + } + catch (...) { + qLog(Warning) << "Failed to initialize Windows Media Transport Controls: unknown error"; + } +} + +WindowsMediaController::~WindowsMediaController() { + if (smtc_) { + auto controls = static_cast(smtc_); + controls->IsEnabled(false); + delete controls; + smtc_ = nullptr; + } + winrt::uninit_apartment(); +} + +void WindowsMediaController::SetupButtonHandlers() { + if (!smtc_) return; + + auto controls = static_cast(smtc_); + + // Handle button pressed events + controls->ButtonPressed([this](const SystemMediaTransportControls &, const SystemMediaTransportControlsButtonPressedEventArgs &args) { + switch (args.Button()) { + case SystemMediaTransportControlsButton::Play: + player_->Play(); + break; + case SystemMediaTransportControlsButton::Pause: + player_->Pause(); + break; + case SystemMediaTransportControlsButton::Stop: + player_->Stop(); + break; + case SystemMediaTransportControlsButton::Next: + player_->Next(); + break; + case SystemMediaTransportControlsButton::Previous: + player_->Previous(); + break; + default: + break; + } + }); +} + +void WindowsMediaController::EngineStateChanged(EngineBase::State newState) { + UpdatePlaybackStatus(newState); +} + +void WindowsMediaController::UpdatePlaybackStatus(EngineBase::State state) { + if (!smtc_) return; + + auto controls = static_cast(smtc_); + + try { + switch (state) { + case EngineBase::State::Playing: + controls->PlaybackStatus(MediaPlaybackStatus::Playing); + break; + case EngineBase::State::Paused: + controls->PlaybackStatus(MediaPlaybackStatus::Paused); + break; + case EngineBase::State::Empty: + case EngineBase::State::Idle: + controls->PlaybackStatus(MediaPlaybackStatus::Stopped); + break; + } + } + catch (const hresult_error &e) { + qLog(Warning) << "Failed to update playback status:" << QString::fromWCharArray(e.message().c_str()); + } +} + +void WindowsMediaController::CurrentSongChanged(const Song &song) { + if (!song.is_valid()) { + return; + } + + // Update metadata immediately with what we have + UpdateMetadata(song, QUrl()); + + // Album cover will be updated via AlbumCoverLoaded signal +} + +void WindowsMediaController::AlbumCoverLoaded(const Song &song, const AlbumCoverLoaderResult &result) { + if (!song.is_valid()) { + return; + } + + // Update metadata with album cover + UpdateMetadata(song, result.temp_cover_url.isEmpty() ? result.cover_url : result.temp_cover_url); +} + +void WindowsMediaController::UpdateMetadata(const Song &song, const QUrl &art_url) { + if (!smtc_) return; + + auto controls = static_cast(smtc_); + + try { + // Get the updater + SystemMediaTransportControlsDisplayUpdater updater = controls->DisplayUpdater(); + updater.Type(MediaPlaybackType::Music); + + // Get the music properties + auto musicProperties = updater.MusicProperties(); + + // Set basic metadata + if (!song.title().isEmpty()) { + musicProperties.Title(winrt::hstring(song.title().toStdWString())); + } + if (!song.artist().isEmpty()) { + musicProperties.Artist(winrt::hstring(song.artist().toStdWString())); + } + if (!song.album().isEmpty()) { + musicProperties.AlbumTitle(winrt::hstring(song.album().toStdWString())); + } + + // Set album art if available + if (art_url.isValid() && art_url.isLocalFile()) { + QString artPath = art_url.toLocalFile(); + if (!artPath.isEmpty()) { + try { + auto thumbnailStream = RandomAccessStreamReference::CreateFromFile( + StorageFile::GetFileFromPathAsync(winrt::hstring(artPath.toStdWString())).get() + ); + updater.Thumbnail(thumbnailStream); + current_song_art_url_ = artPath; + } + catch (const hresult_error &e) { + qLog(Debug) << "Failed to set album art:" << QString::fromWCharArray(e.message().c_str()); + } + } + } + + // Update the display + updater.Update(); + } + catch (const hresult_error &e) { + qLog(Warning) << "Failed to update metadata:" << QString::fromWCharArray(e.message().c_str()); + } +} diff --git a/src/core/windowsmediacontroller.h b/src/core/windowsmediacontroller.h new file mode 100644 index 000000000..3eaceb6a8 --- /dev/null +++ b/src/core/windowsmediacontroller.h @@ -0,0 +1,68 @@ +/* + * Strawberry Music Player + * Copyright 2025, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef WINDOWSMEDIACONTROLLER_H +#define WINDOWSMEDIACONTROLLER_H + +#include "config.h" + +#include + +#include +#include + +#include "includes/shared_ptr.h" +#include "engine/enginebase.h" +#include "covermanager/albumcoverloaderresult.h" + +class Player; +class PlaylistManager; +class CurrentAlbumCoverLoader; +class Song; + +class WindowsMediaController : public QObject { + Q_OBJECT + + public: + explicit WindowsMediaController(HWND hwnd, + const SharedPtr player, + const SharedPtr playlist_manager, + const SharedPtr current_albumcover_loader, + QObject *parent = nullptr); + ~WindowsMediaController() override; + + private Q_SLOTS: + void AlbumCoverLoaded(const Song &song, const AlbumCoverLoaderResult &result = AlbumCoverLoaderResult()); + void EngineStateChanged(EngineBase::State newState); + void CurrentSongChanged(const Song &song); + + private: + void UpdatePlaybackStatus(EngineBase::State state); + void UpdateMetadata(const Song &song, const QUrl &art_url); + void SetupButtonHandlers(); + + private: + const SharedPtr player_; + const SharedPtr playlist_manager_; + const SharedPtr current_albumcover_loader_; + void *smtc_; // Pointer to SystemMediaTransportControls (opaque to avoid WinRT headers in public header) + QString current_song_art_url_; +}; + +#endif // WINDOWSMEDIACONTROLLER_H diff --git a/src/main.cpp b/src/main.cpp index e13db3b62..b9dafffa9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -93,6 +93,10 @@ # include "discord/richpresence.h" #endif +#ifdef _MSC_VER +# include "core/windowsmediacontroller.h" +#endif + #include "core/iconloader.h" #include "core/commandlineoptions.h" #include "core/networkproxyfactory.h" @@ -365,6 +369,11 @@ int main(int argc, char *argv[]) { #endif options); +#ifdef _MSC_VER + // Initialize Windows Media Transport Controls + WindowsMediaController windows_media_controller(reinterpret_cast(w.winId()), app.player(), app.playlist_manager(), app.current_albumcover_loader()); +#endif + #ifdef Q_OS_MACOS mac::EnableFullScreen(w); #endif // Q_OS_MACOS