feat(windows): SMTC integration
This commit is contained in:
@@ -8,7 +8,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- qt_ver: '6.10.1'
|
- qt_ver: '6.10.3'
|
||||||
vs: '2022'
|
vs: '2022'
|
||||||
aqt_arch: 'win64_msvc2022_64'
|
aqt_arch: 'win64_msvc2022_64'
|
||||||
msvc_arch: 'x64'
|
msvc_arch: 'x64'
|
||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
runs-on: windows-2022
|
runs-on: windows-2022
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- name: Install Qt
|
- name: Install Qt
|
||||||
uses: jurplel/install-qt-action@v4
|
uses: jurplel/install-qt-action@v4
|
||||||
with:
|
with:
|
||||||
@@ -68,7 +68,7 @@ jobs:
|
|||||||
if ErrorLevel 8 (exit /B 1)
|
if ErrorLevel 8 (exit /B 1)
|
||||||
copy LICENSE build/bin/
|
copy LICENSE build/bin/
|
||||||
exit /B 0
|
exit /B 0
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: "${{ matrix.aqt_arch }}-qt${{ matrix.qt_ver }}-cmake-package"
|
name: "${{ matrix.aqt_arch }}-qt${{ matrix.qt_ver }}-cmake-package"
|
||||||
path: build/bin/*
|
path: build/bin/*
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ set (PMUSIC_CPP_FILES
|
|||||||
fftspectrum.cpp
|
fftspectrum.cpp
|
||||||
playbackprogressindicator.cpp
|
playbackprogressindicator.cpp
|
||||||
lyricswidget.cpp
|
lyricswidget.cpp
|
||||||
|
winmediacontrol.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
set (PMUSIC_HEADER_FILES
|
set (PMUSIC_HEADER_FILES
|
||||||
@@ -60,6 +61,7 @@ set (PMUSIC_HEADER_FILES
|
|||||||
fftspectrum.h
|
fftspectrum.h
|
||||||
playbackprogressindicator.h
|
playbackprogressindicator.h
|
||||||
taskbarmanager.h
|
taskbarmanager.h
|
||||||
|
winmediacontrol.h
|
||||||
lyricswidget.h
|
lyricswidget.h
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -88,6 +90,7 @@ TS_FILES
|
|||||||
if (WIN32)
|
if (WIN32)
|
||||||
target_sources(${EXE_NAME} PRIVATE assets/pineapple-music.rc)
|
target_sources(${EXE_NAME} PRIVATE assets/pineapple-music.rc)
|
||||||
target_sources(${EXE_NAME} PRIVATE taskbarmanager.cpp)
|
target_sources(${EXE_NAME} PRIVATE taskbarmanager.cpp)
|
||||||
|
target_link_libraries(${EXE_NAME} PRIVATE runtimeobject ole32)
|
||||||
else ()
|
else ()
|
||||||
target_sources(${EXE_NAME} PRIVATE taskbarmanager_dummy.cpp)
|
target_sources(${EXE_NAME} PRIVATE taskbarmanager_dummy.cpp)
|
||||||
endif ()
|
endif ()
|
||||||
|
|||||||
+2
-2
@@ -4,8 +4,8 @@ environment:
|
|||||||
CMAKE_INSTALL_ROOT: C:\projects\cmake
|
CMAKE_INSTALL_ROOT: C:\projects\cmake
|
||||||
PACKAGE_INSTALL_ROOT: C:\projects\pir
|
PACKAGE_INSTALL_ROOT: C:\projects\pir
|
||||||
matrix:
|
matrix:
|
||||||
- build_name: mingw1120_64_qt6_7
|
- build_name: mingw1120_64_qt6_9
|
||||||
QTPATH: C:\Qt\6.8\mingw_64
|
QTPATH: C:\Qt\6.9\mingw_64
|
||||||
MINGW64: C:\Qt\mingw_64\Tools\mingw1310_64
|
MINGW64: C:\Qt\mingw_64\Tools\mingw1310_64
|
||||||
|
|
||||||
install:
|
install:
|
||||||
|
|||||||
@@ -13,8 +13,25 @@
|
|||||||
#include <QTranslator>
|
#include <QTranslator>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
#include <windows.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
typedef HRESULT(WINAPI *SetCurrentProcessExplicitAppUserModelIDProc)(PCWSTR);
|
||||||
|
HMODULE shell32 = LoadLibraryW(L"shell32.dll");
|
||||||
|
if (shell32) {
|
||||||
|
auto proc = reinterpret_cast<SetCurrentProcessExplicitAppUserModelIDProc>(
|
||||||
|
GetProcAddress(shell32, "SetCurrentProcessExplicitAppUserModelID"));
|
||||||
|
if (proc) {
|
||||||
|
proc(L"net.blumia.pineapple-music");
|
||||||
|
}
|
||||||
|
FreeLibrary(shell32);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
QApplication a(argc, argv);
|
QApplication a(argc, argv);
|
||||||
|
|
||||||
QTranslator translator;
|
QTranslator translator;
|
||||||
|
|||||||
+56
-6
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: 2025 Gary Wang <opensource@blumia.net>
|
// SPDX-FileCopyrightText: 2026 Gary Wang <opensource@blumia.net>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "fftspectrum.h"
|
#include "fftspectrum.h"
|
||||||
#include "lrcbar.h"
|
#include "lrcbar.h"
|
||||||
#include "taskbarmanager.h"
|
#include "taskbarmanager.h"
|
||||||
|
#include "winmediacontrol.h"
|
||||||
#include "lyricsmanager.h"
|
#include "lyricsmanager.h"
|
||||||
#include "lyricswidget.h"
|
#include "lyricswidget.h"
|
||||||
|
|
||||||
@@ -39,6 +40,7 @@
|
|||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
#include <QGraphicsDropShadowEffect>
|
#include <QGraphicsDropShadowEffect>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
#include <QFileInfo>
|
||||||
|
|
||||||
constexpr QSize miniSize(490, 160);
|
constexpr QSize miniSize(490, 160);
|
||||||
constexpr QSize fullSize(490, 420);
|
constexpr QSize fullSize(490, 420);
|
||||||
@@ -55,6 +57,7 @@ MainWindow::MainWindow(QWidget *parent)
|
|||||||
, m_lyricsWidget(new LyricsWidget(this))
|
, m_lyricsWidget(new LyricsWidget(this))
|
||||||
, m_playlistManager(new PlaylistManager(this))
|
, m_playlistManager(new PlaylistManager(this))
|
||||||
, m_taskbarManager(new TaskBarManager(this))
|
, m_taskbarManager(new TaskBarManager(this))
|
||||||
|
, m_mediaControl(new WinMediaControl(this))
|
||||||
{
|
{
|
||||||
ui->setupUi(this);
|
ui->setupUi(this);
|
||||||
m_lyricsWidget->setLyricsManager(m_lyricsManager);
|
m_lyricsWidget->setLyricsManager(m_lyricsManager);
|
||||||
@@ -98,7 +101,9 @@ MainWindow::MainWindow(QWidget *parent)
|
|||||||
centerWindow();
|
centerWindow();
|
||||||
|
|
||||||
QTimer::singleShot(1000, [this](){
|
QTimer::singleShot(1000, [this](){
|
||||||
m_taskbarManager->setWinId(window()->winId());
|
auto winId = window()->winId();
|
||||||
|
m_taskbarManager->setWinId(winId);
|
||||||
|
m_mediaControl->initialize(reinterpret_cast<void*>(winId));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,15 +168,23 @@ void MainWindow::setAudioMetadataForDisplay(QString title, QString artist, QStri
|
|||||||
{
|
{
|
||||||
Q_UNUSED(album);
|
Q_UNUSED(album);
|
||||||
|
|
||||||
|
if (title.isEmpty()) {
|
||||||
|
title = QFileInfo(m_mediaPlayer->source().fileName()).completeBaseName();
|
||||||
|
}
|
||||||
|
|
||||||
if (!title.isEmpty()) {
|
if (!title.isEmpty()) {
|
||||||
if (!artist.isEmpty()) {
|
if (!artist.isEmpty()) {
|
||||||
ui->titleLabel->setText(QString("%1 - %2").arg(artist, title));
|
title = QString("%1 - %2").arg(artist, title);
|
||||||
} else if (!album.isEmpty()) {
|
} else if (!album.isEmpty()) {
|
||||||
ui->titleLabel->setText(QString("%1 - %2").arg(album, title));
|
title = QString("%1 - %2").arg(album, title);
|
||||||
} else {
|
} else {
|
||||||
ui->titleLabel->setText(QString("%1").arg(title));
|
title = QString("%1").arg(title);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ui->titleLabel->setText(title);
|
||||||
|
|
||||||
|
m_mediaControl->setMetadata(title, artist, album);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::localSocketPlayAudioFiles(QVariant audioFilesVariant)
|
void MainWindow::localSocketPlayAudioFiles(QVariant audioFilesVariant)
|
||||||
@@ -510,10 +523,14 @@ void MainWindow::initConnections()
|
|||||||
}
|
}
|
||||||
QVariant coverArt(metadata.value(QMediaMetaData::ThumbnailImage));
|
QVariant coverArt(metadata.value(QMediaMetaData::ThumbnailImage));
|
||||||
if (!coverArt.isNull()) {
|
if (!coverArt.isNull()) {
|
||||||
ui->coverLabel->setPixmap(QPixmap::fromImage(coverArt.value<QImage>()));
|
QImage coverImage = coverArt.value<QImage>();
|
||||||
|
ui->coverLabel->setPixmap(QPixmap::fromImage(coverImage));
|
||||||
|
m_mediaControl->setCoverArt(coverImage);
|
||||||
} else {
|
} else {
|
||||||
qDebug() << "No ThumbnailImage!" << metadata.keys();
|
qDebug() << "No ThumbnailImage!" << metadata.keys();
|
||||||
ui->coverLabel->setPixmap(QPixmap(":/icons/icons/media-album-cover.svg"));
|
ui->coverLabel->setPixmap(QPixmap(":/icons/icons/media-album-cover.svg"));
|
||||||
|
QIcon appIcon(QStringLiteral(":/icons/icons/app-icon.svg"));
|
||||||
|
m_mediaControl->setCoverArt(appIcon.pixmap(QSize(256, 256)).toImage());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
connect(m_playlistManager, &PlaylistManager::currentIndexChanged, this, [=](int index){
|
connect(m_playlistManager, &PlaylistManager::currentIndexChanged, this, [=](int index){
|
||||||
@@ -525,6 +542,7 @@ void MainWindow::initConnections()
|
|||||||
if (m_mediaPlayer->duration() != 0) {
|
if (m_mediaPlayer->duration() != 0) {
|
||||||
ui->playbackProgressIndicator->setPosition(pos);
|
ui->playbackProgressIndicator->setPosition(pos);
|
||||||
m_taskbarManager->setProgressValue(pos);
|
m_taskbarManager->setProgressValue(pos);
|
||||||
|
m_mediaControl->setPosition(pos, m_mediaPlayer->duration());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_lrcbar->isVisible() || m_lyricsWidget->isVisible()) {
|
if (m_lrcbar->isVisible() || m_lyricsWidget->isVisible()) {
|
||||||
@@ -568,10 +586,15 @@ void MainWindow::initConnections()
|
|||||||
switch (newState) {
|
switch (newState) {
|
||||||
case QMediaPlayer::PlayingState:
|
case QMediaPlayer::PlayingState:
|
||||||
ui->playBtn->setIcon(QIcon(":/icons/icons/media-playback-pause.png"));
|
ui->playBtn->setIcon(QIcon(":/icons/icons/media-playback-pause.png"));
|
||||||
|
m_mediaControl->setPlaybackState(1);
|
||||||
break;
|
break;
|
||||||
case QMediaPlayer::StoppedState:
|
case QMediaPlayer::StoppedState:
|
||||||
|
ui->playBtn->setIcon(QIcon(":/icons/icons/media-playback-start.png"));
|
||||||
|
m_mediaControl->setPlaybackState(0);
|
||||||
|
break;
|
||||||
case QMediaPlayer::PausedState:
|
case QMediaPlayer::PausedState:
|
||||||
ui->playBtn->setIcon(QIcon(":/icons/icons/media-playback-start.png"));
|
ui->playBtn->setIcon(QIcon(":/icons/icons/media-playback-start.png"));
|
||||||
|
m_mediaControl->setPlaybackState(2);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
m_taskbarManager->setPlaybackState(newState);
|
m_taskbarManager->setPlaybackState(newState);
|
||||||
@@ -627,6 +650,33 @@ void MainWindow::initConnections()
|
|||||||
on_nextBtn_clicked();
|
on_nextBtn_clicked();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
connect(m_mediaControl, &WinMediaControl::playRequested, this, [this](){
|
||||||
|
if (QList<QMediaPlayer::PlaybackState> {QMediaPlayer::PausedState, QMediaPlayer::StoppedState}
|
||||||
|
.contains(m_mediaPlayer->playbackState())) {
|
||||||
|
m_mediaPlayer->play();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(m_mediaControl, &WinMediaControl::pauseRequested, this, [this](){
|
||||||
|
m_mediaPlayer->pause();
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(m_mediaControl, &WinMediaControl::stopRequested, this, [this](){
|
||||||
|
m_mediaPlayer->stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(m_mediaControl, &WinMediaControl::nextRequested, this, [this](){
|
||||||
|
on_nextBtn_clicked();
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(m_mediaControl, &WinMediaControl::previousRequested, this, [this](){
|
||||||
|
on_prevBtn_clicked();
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(m_mediaControl, &WinMediaControl::seekRequested, this, [this](qint64 positionMs){
|
||||||
|
m_mediaPlayer->setPosition(positionMs);
|
||||||
|
});
|
||||||
|
|
||||||
connect(m_mediaPlayer, &QMediaPlayer::errorOccurred, this, [=](QMediaPlayer::Error error, const QString &errorString) {
|
connect(m_mediaPlayer, &QMediaPlayer::errorOccurred, this, [=](QMediaPlayer::Error error, const QString &errorString) {
|
||||||
qDebug() << error << errorString;
|
qDebug() << error << errorString;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class LyricsManager;
|
|||||||
class LyricsWidget;
|
class LyricsWidget;
|
||||||
class PlaylistManager;
|
class PlaylistManager;
|
||||||
class TaskBarManager;
|
class TaskBarManager;
|
||||||
|
class WinMediaControl;
|
||||||
class MainWindow : public QMainWindow
|
class MainWindow : public QMainWindow
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -112,6 +113,7 @@ private:
|
|||||||
QPropertyAnimation *m_fadeOutAnimation;
|
QPropertyAnimation *m_fadeOutAnimation;
|
||||||
PlaylistManager *m_playlistManager;
|
PlaylistManager *m_playlistManager;
|
||||||
TaskBarManager *m_taskbarManager;
|
TaskBarManager *m_taskbarManager;
|
||||||
|
WinMediaControl *m_mediaControl;
|
||||||
|
|
||||||
void initUiAndAnimation();
|
void initUiAndAnimation();
|
||||||
void initConnections();
|
void initConnections();
|
||||||
|
|||||||
@@ -0,0 +1,421 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026 Gary Wang <opensource@blumia.net>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#include "winmediacontrol.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QBuffer>
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
|
||||||
|
#include <roapi.h>
|
||||||
|
#include <windows.media.h>
|
||||||
|
#include <windows.storage.streams.h>
|
||||||
|
#include <systemmediatransportcontrolsinterop.h>
|
||||||
|
|
||||||
|
#include <wrl.h>
|
||||||
|
#include <wrl/wrappers/corewrappers.h>
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
using ButtonPressedHandler = ITypedEventHandler<SystemMediaTransportControls*,
|
||||||
|
SystemMediaTransportControlsButtonPressedEventArgs*>;
|
||||||
|
using PositionChangeHandler = ITypedEventHandler<SystemMediaTransportControls*,
|
||||||
|
PlaybackPositionChangeRequestedEventArgs*>;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// COM event handler for button presses
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
class ButtonDelegate : public ButtonPressedHandler
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ButtonDelegate(WinMediaControl* 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<ButtonPressedHandler*>(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:
|
||||||
|
emit m_owner->playRequested();
|
||||||
|
break;
|
||||||
|
case SystemMediaTransportControlsButton_Pause:
|
||||||
|
emit m_owner->pauseRequested();
|
||||||
|
break;
|
||||||
|
case SystemMediaTransportControlsButton_Stop:
|
||||||
|
emit m_owner->stopRequested();
|
||||||
|
break;
|
||||||
|
case SystemMediaTransportControlsButton_Next:
|
||||||
|
emit m_owner->nextRequested();
|
||||||
|
break;
|
||||||
|
case SystemMediaTransportControlsButton_Previous:
|
||||||
|
emit m_owner->previousRequested();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
LONG m_ref;
|
||||||
|
WinMediaControl* m_owner;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// COM event handler for position change (seek) requests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
class PositionChangeDelegate : public PositionChangeHandler
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
PositionChangeDelegate(WinMediaControl* 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(PositionChangeHandler)) {
|
||||||
|
*ppv = static_cast<PositionChangeHandler*>(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*,
|
||||||
|
IPlaybackPositionChangeRequestedEventArgs* args) override
|
||||||
|
{
|
||||||
|
if (args) {
|
||||||
|
TimeSpan ts;
|
||||||
|
if (SUCCEEDED(args->get_RequestedPlaybackPosition(&ts))) {
|
||||||
|
emit m_owner->seekRequested(ts.Duration / 10000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
LONG m_ref;
|
||||||
|
WinMediaControl* m_owner;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// WinMediaControlPrivate
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
class WinMediaControlPrivate
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ComPtr<ISystemMediaTransportControls> smtc;
|
||||||
|
ComPtr<ISystemMediaTransportControls2> smtc2;
|
||||||
|
ComPtr<ISystemMediaTransportControlsDisplayUpdater> updater;
|
||||||
|
ComPtr<IMusicDisplayProperties> musicProps;
|
||||||
|
EventRegistrationToken buttonToken{};
|
||||||
|
EventRegistrationToken positionChangeToken{};
|
||||||
|
bool initialized = false;
|
||||||
|
|
||||||
|
QString title;
|
||||||
|
QString artist;
|
||||||
|
QString album;
|
||||||
|
QImage coverArt;
|
||||||
|
};
|
||||||
|
|
||||||
|
WinMediaControl::WinMediaControl(QObject* parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, d(new WinMediaControlPrivate())
|
||||||
|
{
|
||||||
|
HRESULT hr = RoInitialize(RO_INIT_SINGLETHREADED);
|
||||||
|
if (FAILED(hr) && hr != RPC_E_CHANGED_MODE) {
|
||||||
|
qWarning() << "WinMediaControl: RoInitialize failed:" << Qt::hex << hr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WinMediaControl::~WinMediaControl()
|
||||||
|
{
|
||||||
|
shutdown();
|
||||||
|
delete d;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WinMediaControl::initialize(void* hwnd)
|
||||||
|
{
|
||||||
|
if (d->initialized || !hwnd) return;
|
||||||
|
|
||||||
|
HWND window = static_cast<HWND>(hwnd);
|
||||||
|
|
||||||
|
ComPtr<ISystemMediaTransportControlsInterop> interop;
|
||||||
|
HRESULT hr = RoGetActivationFactory(
|
||||||
|
HStringReference(RuntimeClass_Windows_Media_SystemMediaTransportControls).Get(),
|
||||||
|
IID_PPV_ARGS(&interop));
|
||||||
|
if (FAILED(hr) || !interop) {
|
||||||
|
qWarning() << "WinMediaControl: Failed to get interop factory:" << Qt::hex << hr;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr = interop->GetForWindow(window, IID_PPV_ARGS(&d->smtc));
|
||||||
|
if (FAILED(hr) || !d->smtc) {
|
||||||
|
qWarning() << "WinMediaControl: GetForWindow failed:" << Qt::hex << hr;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
d->smtc->put_IsEnabled(true);
|
||||||
|
d->smtc->put_IsPlayEnabled(true);
|
||||||
|
d->smtc->put_IsPauseEnabled(true);
|
||||||
|
d->smtc->put_IsStopEnabled(true);
|
||||||
|
d->smtc->put_IsNextEnabled(true);
|
||||||
|
d->smtc->put_IsPreviousEnabled(true);
|
||||||
|
|
||||||
|
d->smtc.As(&d->smtc2);
|
||||||
|
|
||||||
|
d->smtc->get_DisplayUpdater(&d->updater);
|
||||||
|
if (d->updater) {
|
||||||
|
d->updater->put_Type(MediaPlaybackType_Music);
|
||||||
|
d->updater->get_MusicProperties(&d->musicProps);
|
||||||
|
d->updater->Update();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* btnHandler = new ButtonDelegate(this);
|
||||||
|
hr = d->smtc->add_ButtonPressed(btnHandler, &d->buttonToken);
|
||||||
|
btnHandler->Release();
|
||||||
|
|
||||||
|
if (d->smtc2) {
|
||||||
|
auto* posHandler = new PositionChangeDelegate(this);
|
||||||
|
hr = d->smtc2->add_PlaybackPositionChangeRequested(posHandler, &d->positionChangeToken);
|
||||||
|
posHandler->Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
d->initialized = true;
|
||||||
|
qDebug() << "WinMediaControl: SMTC initialized successfully";
|
||||||
|
}
|
||||||
|
|
||||||
|
void WinMediaControl::shutdown()
|
||||||
|
{
|
||||||
|
if (!d->initialized) return;
|
||||||
|
|
||||||
|
if (d->smtc) {
|
||||||
|
d->smtc->remove_ButtonPressed(d->buttonToken);
|
||||||
|
if (d->smtc2) {
|
||||||
|
d->smtc2->remove_PlaybackPositionChangeRequested(d->positionChangeToken);
|
||||||
|
}
|
||||||
|
d->smtc->put_IsEnabled(false);
|
||||||
|
}
|
||||||
|
d->smtc.Reset();
|
||||||
|
d->smtc2.Reset();
|
||||||
|
d->updater.Reset();
|
||||||
|
d->musicProps.Reset();
|
||||||
|
d->initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ComPtr<IRandomAccessStreamReference> imageToStreamReference(const QImage& image)
|
||||||
|
{
|
||||||
|
QByteArray imageData;
|
||||||
|
QBuffer buffer(&imageData);
|
||||||
|
buffer.open(QIODevice::WriteOnly);
|
||||||
|
image.save(&buffer, "PNG");
|
||||||
|
buffer.close();
|
||||||
|
|
||||||
|
ComPtr<IInspectable> insp;
|
||||||
|
HRESULT hr = RoActivateInstance(
|
||||||
|
HStringReference(RuntimeClass_Windows_Storage_Streams_InMemoryRandomAccessStream).Get(),
|
||||||
|
&insp);
|
||||||
|
if (FAILED(hr) || !insp) return nullptr;
|
||||||
|
|
||||||
|
ComPtr<IRandomAccessStream> stream;
|
||||||
|
insp.As(&stream);
|
||||||
|
if (!stream) return nullptr;
|
||||||
|
|
||||||
|
ComPtr<IOutputStream> outputStream;
|
||||||
|
stream->GetOutputStreamAt(0, &outputStream);
|
||||||
|
|
||||||
|
ComPtr<IDataWriterFactory> writerFactory;
|
||||||
|
RoGetActivationFactory(
|
||||||
|
HStringReference(RuntimeClass_Windows_Storage_Streams_DataWriter).Get(),
|
||||||
|
IID_PPV_ARGS(&writerFactory));
|
||||||
|
if (!writerFactory) return nullptr;
|
||||||
|
|
||||||
|
ComPtr<IDataWriter> writer;
|
||||||
|
writerFactory->CreateDataWriter(outputStream.Get(), &writer);
|
||||||
|
if (!writer) return nullptr;
|
||||||
|
|
||||||
|
writer->WriteBytes(
|
||||||
|
static_cast<UINT32>(imageData.size()),
|
||||||
|
reinterpret_cast<BYTE*>(imageData.data()));
|
||||||
|
|
||||||
|
ComPtr<IAsyncOperation<UINT32>> storeOp;
|
||||||
|
writer->StoreAsync(&storeOp);
|
||||||
|
if (storeOp) {
|
||||||
|
// For in-memory streams this completes synchronously; just drain it
|
||||||
|
UINT32 result = 0;
|
||||||
|
storeOp->GetResults(&result);
|
||||||
|
}
|
||||||
|
|
||||||
|
stream->Seek(0);
|
||||||
|
|
||||||
|
ComPtr<IRandomAccessStreamReferenceStatics> rars;
|
||||||
|
RoGetActivationFactory(
|
||||||
|
HStringReference(RuntimeClass_Windows_Storage_Streams_RandomAccessStreamReference).Get(),
|
||||||
|
IID_PPV_ARGS(&rars));
|
||||||
|
if (!rars) return nullptr;
|
||||||
|
|
||||||
|
ComPtr<IRandomAccessStreamReference> ref;
|
||||||
|
rars->CreateFromStream(stream.Get(), &ref);
|
||||||
|
return ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WinMediaControl::setMetadata(const QString& title, const QString& artist,
|
||||||
|
const QString& album)
|
||||||
|
{
|
||||||
|
d->title = title;
|
||||||
|
d->artist = artist;
|
||||||
|
d->album = album;
|
||||||
|
|
||||||
|
if (!d->initialized || !d->updater || !d->musicProps) return;
|
||||||
|
|
||||||
|
d->updater->put_Type(MediaPlaybackType_Music);
|
||||||
|
d->updater->get_MusicProperties(&d->musicProps);
|
||||||
|
|
||||||
|
{
|
||||||
|
std::wstring wstr = title.toStdWString();
|
||||||
|
d->musicProps->put_Title(HStringReference(wstr.c_str()).Get());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
std::wstring wstr = artist.toStdWString();
|
||||||
|
d->musicProps->put_Artist(HStringReference(wstr.c_str()).Get());
|
||||||
|
}
|
||||||
|
|
||||||
|
ComPtr<IMusicDisplayProperties2> musicProps2;
|
||||||
|
if (SUCCEEDED(d->musicProps.As(&musicProps2)) && musicProps2) {
|
||||||
|
std::wstring wstr = album.toStdWString();
|
||||||
|
musicProps2->put_AlbumTitle(HStringReference(wstr.c_str()).Get());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!d->coverArt.isNull()) {
|
||||||
|
auto thumbRef = imageToStreamReference(d->coverArt);
|
||||||
|
if (thumbRef) {
|
||||||
|
d->updater->put_Thumbnail(thumbRef.Get());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
d->updater->put_Thumbnail(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
d->updater->Update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WinMediaControl::setCoverArt(const QImage& coverArt)
|
||||||
|
{
|
||||||
|
d->coverArt = coverArt;
|
||||||
|
if (d->initialized) {
|
||||||
|
setMetadata(d->title, d->artist, d->album);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WinMediaControl::setPlaybackState(int state)
|
||||||
|
{
|
||||||
|
if (!d->initialized || !d->smtc) return;
|
||||||
|
|
||||||
|
MediaPlaybackStatus status;
|
||||||
|
switch (state) {
|
||||||
|
case 0: status = MediaPlaybackStatus_Stopped; break;
|
||||||
|
case 1: status = MediaPlaybackStatus_Playing; break;
|
||||||
|
case 2: status = MediaPlaybackStatus_Paused; break;
|
||||||
|
default: status = MediaPlaybackStatus_Stopped; break;
|
||||||
|
}
|
||||||
|
d->smtc->put_PlaybackStatus(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WinMediaControl::setPosition(qint64 positionMs, qint64 durationMs)
|
||||||
|
{
|
||||||
|
if (!d->initialized || !d->smtc2) return;
|
||||||
|
|
||||||
|
ComPtr<IInspectable> insp;
|
||||||
|
HRESULT hr = RoActivateInstance(
|
||||||
|
HStringReference(RuntimeClass_Windows_Media_SystemMediaTransportControlsTimelineProperties).Get(),
|
||||||
|
&insp);
|
||||||
|
if (FAILED(hr) || !insp) return;
|
||||||
|
|
||||||
|
ComPtr<ISystemMediaTransportControlsTimelineProperties> timeline;
|
||||||
|
insp.As(&timeline);
|
||||||
|
if (!timeline) return;
|
||||||
|
|
||||||
|
timeline->put_StartTime({ 0 });
|
||||||
|
timeline->put_EndTime({ durationMs * 10000 });
|
||||||
|
timeline->put_Position({ positionMs * 10000 });
|
||||||
|
timeline->put_MinSeekTime({ 0 });
|
||||||
|
timeline->put_MaxSeekTime({ durationMs * 10000 });
|
||||||
|
|
||||||
|
d->smtc2->UpdateTimelineProperties(timeline.Get());
|
||||||
|
}
|
||||||
|
|
||||||
|
void WinMediaControl::setControlsEnabled(bool play, bool pause, bool stop,
|
||||||
|
bool next, bool previous)
|
||||||
|
{
|
||||||
|
if (!d->initialized || !d->smtc) return;
|
||||||
|
|
||||||
|
d->smtc->put_IsPlayEnabled(play);
|
||||||
|
d->smtc->put_IsPauseEnabled(pause);
|
||||||
|
d->smtc->put_IsStopEnabled(stop);
|
||||||
|
d->smtc->put_IsNextEnabled(next);
|
||||||
|
d->smtc->put_IsPreviousEnabled(previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
#else // Q_OS_WIN
|
||||||
|
|
||||||
|
// Non-Windows stub implementations
|
||||||
|
|
||||||
|
WinMediaControl::WinMediaControl(QObject* parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, d(nullptr)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
WinMediaControl::~WinMediaControl() = default;
|
||||||
|
|
||||||
|
void WinMediaControl::initialize(void*) {}
|
||||||
|
void WinMediaControl::shutdown() {}
|
||||||
|
|
||||||
|
void WinMediaControl::setMetadata(const QString&, const QString&, const QString&) {}
|
||||||
|
void WinMediaControl::setCoverArt(const QImage&) {}
|
||||||
|
void WinMediaControl::setPlaybackState(int) {}
|
||||||
|
void WinMediaControl::setPosition(qint64, qint64) {}
|
||||||
|
void WinMediaControl::setControlsEnabled(bool, bool, bool, bool, bool) {}
|
||||||
|
|
||||||
|
#endif // Q_OS_WIN
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026 Gary Wang <opensource@blumia.net>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QImage>
|
||||||
|
|
||||||
|
class WinMediaControlPrivate;
|
||||||
|
|
||||||
|
class WinMediaControl : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit WinMediaControl(QObject* parent = nullptr);
|
||||||
|
~WinMediaControl();
|
||||||
|
|
||||||
|
void initialize(void* hwnd);
|
||||||
|
void shutdown();
|
||||||
|
|
||||||
|
void setMetadata(const QString& title, const QString& artist,
|
||||||
|
const QString& album);
|
||||||
|
void setCoverArt(const QImage& coverArt);
|
||||||
|
void setPlaybackState(int state);
|
||||||
|
void setPosition(qint64 positionMs, qint64 durationMs);
|
||||||
|
void setControlsEnabled(bool play, bool pause, bool stop,
|
||||||
|
bool next, bool previous);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void playRequested();
|
||||||
|
void pauseRequested();
|
||||||
|
void stopRequested();
|
||||||
|
void nextRequested();
|
||||||
|
void previousRequested();
|
||||||
|
void seekRequested(qint64 positionMs);
|
||||||
|
|
||||||
|
private:
|
||||||
|
WinMediaControlPrivate* d = nullptr;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user