diff --git a/CMakeLists.txt b/CMakeLists.txt index 12bead7d5..0ee8a67d6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,6 +24,7 @@ # NO_UPDATE_CHECK - Disable automatic checking for new application updates. # IS_FLATPAK_BUILD - Set to "ON" when building RSS Guard with Flatpak. # FORCE_BUNDLE_ICONS - Forcibly bundles icons into executables. +# ENABLE_MEDIAPLAYER_QTMULTIMEDIA - Enable media player (QtMultimedia/ffmpeg implementation). # ENABLE_COMPRESSED_SITEMAP - Set to "ON" if you want to enable support for "sitemap.xml.gz" format. # This requires "zlib" library and if you want to use specific # zlib location, then use "ZLIB_ROOT" variable, for example @@ -122,6 +123,7 @@ option(NO_UPDATE_CHECK "Disable automatic checking for new application updates" option(IS_FLATPAK_BUILD "Set to 'ON' when building RSS Guard with Flatpak." OFF) option(FORCE_BUNDLE_ICONS "Forcibly bundle icon themes into RSS Guard." OFF) option(ENABLE_COMPRESSED_SITEMAP "Enable support for gzip-compressed sitemap feeds. Requires zlib." OFF) +option(ENABLE_MEDIAPLAYER_QTMULTIMEDIA "Enable built-in media player. Requires QtMultimedia FFMPEG plugin." ON) # Import Qt libraries. set(QT6_MIN_VERSION 6.3.0) @@ -139,14 +141,20 @@ set(QT_COMPONENTS Concurrent ) -if(NOT OS2) - list(APPEND QT_COMPONENTS Multimedia MultimediaWidgets) -endif() - if(WIN32 AND NOT BUILD_WITH_QT6) list(APPEND QT_COMPONENTS WinExtras) endif() +if(ENABLE_MEDIAPLAYER_QTMULTIMEDIA) + list(APPEND QT_COMPONENTS Multimedia MultimediaWidgets) + add_compile_definitions(ENABLE_MEDIAPLAYER_QTMULTIMEDIA) +endif() + +if(ENABLE_MEDIAPLAYER_QTMULTIMEDIA OR ENABLE_MEDIAPLAYER_LIBMVP) + set(ENABLE_MEDIAPLAYER TRUE) + add_compile_definitions(ENABLE_MEDIAPLAYER) +endif() + if(USE_WEBENGINE) list(APPEND QT_COMPONENTS WebEngineWidgets) add_compile_definitions(USE_WEBENGINE) diff --git a/src/librssguard/CMakeLists.txt b/src/librssguard/CMakeLists.txt index 652a32654..5bbbc7a88 100644 --- a/src/librssguard/CMakeLists.txt +++ b/src/librssguard/CMakeLists.txt @@ -133,8 +133,6 @@ set(SOURCES gui/reusable/locationlineedit.h gui/reusable/messagecountspinbox.cpp gui/reusable/messagecountspinbox.h - gui/reusable/mediaplayer.cpp - gui/reusable/mediaplayer.h gui/reusable/networkproxydetails.cpp gui/reusable/networkproxydetails.h gui/reusable/nonclosablemenu.cpp @@ -467,7 +465,6 @@ set(UI_FILES gui/notifications/notificationseditor.ui gui/notifications/singlenotificationeditor.ui gui/notifications/toastnotification.ui - gui/reusable/mediaplayer.ui gui/reusable/networkproxydetails.ui gui/reusable/searchtextwidget.ui gui/richtexteditor/mrichtextedit.ui @@ -509,6 +506,27 @@ set(UI_FILES services/tt-rss/gui/ttrssfeeddetails.ui ) +if(ENABLE_MEDIAPLAYER) + list(APPEND SOURCES + gui/mediaplayer/playerbackend.cpp + gui/mediaplayer/playerbackend.h + gui/mediaplayer/mediaplayer.cpp + gui/mediaplayer/mediaplayer.h + ) + + list(APPEND UI_FILES + gui/mediaplayer/mediaplayer.ui + ) +endif() + +if(ENABLE_MEDIAPLAYER_QTMULTIMEDIA) + list(APPEND SOURCES + gui/mediaplayer/qtmultimedia/qtmultimediabackend.cpp + gui/mediaplayer/qtmultimedia/qtmultimediabackend.h + ) +endif() + + if(USE_WEBENGINE) list(APPEND SOURCES # WebEngine-based web (and message) browser. @@ -770,7 +788,7 @@ if(WIN32 AND NOT BUILD_WITH_QT6) ) endif() -if(NOT OS2) +if(ENABLE_MEDIAPLAYER_QTMULTIMEDIA) target_link_libraries(rssguard PUBLIC Qt${QT_VERSION_MAJOR}::Multimedia Qt${QT_VERSION_MAJOR}::MultimediaWidgets diff --git a/src/librssguard/database/databasequeries.cpp b/src/librssguard/database/databasequeries.cpp index f65e18a46..e99de74a0 100644 --- a/src/librssguard/database/databasequeries.cpp +++ b/src/librssguard/database/databasequeries.cpp @@ -1678,7 +1678,10 @@ UpdatedArticles DatabaseQueries::updateMessages(const QSqlDatabase& db, QMutexLocker lck(db_mutex); - auto bulk_query = db.exec(final_bulk); + auto bulk_query = QSqlQuery(final_bulk, db); + + bulk_query.exec(); + auto bulk_error = bulk_query.lastError(); if (bulk_error.isValid()) { diff --git a/src/librssguard/gui/mediaplayer/mediaplayer.cpp b/src/librssguard/gui/mediaplayer/mediaplayer.cpp new file mode 100644 index 000000000..5b1b97e1f --- /dev/null +++ b/src/librssguard/gui/mediaplayer/mediaplayer.cpp @@ -0,0 +1,172 @@ +// For license of this file, see /LICENSE.md. + +#include "gui/mediaplayer/mediaplayer.h" + +#include "miscellaneous/iconfactory.h" + +#include "gui/mediaplayer/qtmultimedia/qtmultimediabackend.h" + +MediaPlayer::MediaPlayer(QWidget* parent) + : TabContent(parent), m_backend(new QtMultimediaBackend(this)), m_muted(false) { + m_ui.setupUi(this); + + m_ui.m_layoutMain->insertWidget(0, m_backend, 1); + + setupIcons(); + + createBackendConnections(); + createConnections(); +} + +MediaPlayer::~MediaPlayer() {} + +WebBrowser* MediaPlayer::webBrowser() const { + return nullptr; +} + +void MediaPlayer::playUrl(const QString& url) { + if (m_muted) { + muteUnmute(); + } + else { + setVolume(m_ui.m_slidVolume->value()); + } + + m_backend->playUrl(url); +} + +void MediaPlayer::playPause() { + m_backend->playPause(); +} + +void MediaPlayer::stop() { + m_backend->stop(); +} + +void MediaPlayer::download() { + emit urlDownloadRequested(m_backend->url()); +} + +void MediaPlayer::muteUnmute() { + m_ui.m_slidVolume->setEnabled(m_muted); + setVolume(m_muted ? m_ui.m_slidVolume->value() : 0); + + m_muted = !m_muted; +} + +void MediaPlayer::setSpeed(int speed) { + m_backend->setPlaybackSpeed(speed); +} + +void MediaPlayer::setVolume(int volume) { + m_backend->setVolume(volume); + + m_ui.m_btnVolume->setIcon(volume <= 0 ? m_iconMute : m_iconUnmute); +} + +void MediaPlayer::seek(int position) { + m_backend->setPosition(position); +} + +void MediaPlayer::onPositionChanged(int position) { + m_ui.m_slidProgress->blockSignals(true); + m_ui.m_slidProgress->setValue(position); + m_ui.m_slidProgress->blockSignals(false); + + updateTimeAndProgress(position, m_backend->duration()); +} + +void MediaPlayer::onSpeedChanged(int speed) { + m_ui.m_spinSpeed->blockSignals(true); + m_ui.m_spinSpeed->setValue(speed); + m_ui.m_spinSpeed->blockSignals(false); +} + +void MediaPlayer::onDurationChanged(int duration) { + m_ui.m_slidProgress->blockSignals(true); + m_ui.m_slidProgress->setMaximum(duration); + m_ui.m_slidProgress->blockSignals(false); + + updateTimeAndProgress(m_backend->position(), duration); +} + +void MediaPlayer::updateTimeAndProgress(int progress, int total) { + m_ui.m_lblTime->setText(QSL("%1/%2").arg(QDateTime::fromSecsSinceEpoch(progress).toUTC().toString("hh:mm:ss"), + QDateTime::fromSecsSinceEpoch(total).toUTC().toString("hh:mm:ss"))); +} + +void MediaPlayer::onErrorOccurred(const QString& error_string) { + m_ui.m_lblStatus->setStatus(WidgetWithStatus::StatusType::Error, error_string, error_string); +} + +void MediaPlayer::onAudioAvailable(bool available) { + m_ui.m_slidVolume->setEnabled(available); + m_ui.m_btnVolume->setEnabled(available); +} + +void MediaPlayer::onVideoAvailable(bool available) { + Q_UNUSED(available) +} + +void MediaPlayer::onStatusChanged(const QString& status) { + m_ui.m_lblStatus->setStatus(WidgetWithStatus::StatusType::Information, status, status); +} + +void MediaPlayer::onPlaybackStateChanged(PlayerBackend::PlaybackState state) { + switch (state) { + case PlayerBackend::PlaybackState::StoppedState: + m_ui.m_btnPlayPause->setIcon(m_iconPlay); + m_ui.m_btnStop->setEnabled(false); + break; + + case PlayerBackend::PlaybackState::PlayingState: + m_ui.m_btnPlayPause->setIcon(m_iconPause); + m_ui.m_btnStop->setEnabled(true); + break; + + case PlayerBackend::PlaybackState::PausedState: + m_ui.m_btnPlayPause->setIcon(m_iconPlay); + m_ui.m_btnStop->setEnabled(true); + break; + } +} + +void MediaPlayer::onSeekableChanged(bool seekable) { + m_ui.m_slidProgress->setEnabled(seekable); + + if (!seekable) { + onPositionChanged(0); + } +} + +void MediaPlayer::setupIcons() { + m_iconPlay = qApp->icons()->fromTheme(QSL("media-playback-start"), QSL("player_play")); + m_iconPause = qApp->icons()->fromTheme(QSL("media-playback-pause"), QSL("player_pause")); + m_iconMute = qApp->icons()->fromTheme(QSL("player-volume-muted"), QSL("audio-volume-muted")); + m_iconUnmute = qApp->icons()->fromTheme(QSL("player-volume"), QSL("stock_volume")); + + m_ui.m_btnDownload->setIcon(qApp->icons()->fromTheme(QSL("download"), QSL("browser-download"))); + m_ui.m_btnStop->setIcon(qApp->icons()->fromTheme(QSL("media-playback-stop"), QSL("player_stop"))); +} + +void MediaPlayer::createBackendConnections() { + connect(m_backend, &PlayerBackend::speedChanged, this, &MediaPlayer::onSpeedChanged); + connect(m_backend, &PlayerBackend::durationChanged, this, &MediaPlayer::onDurationChanged); + connect(m_backend, &PlayerBackend::positionChanged, this, &MediaPlayer::onPositionChanged); + connect(m_backend, &PlayerBackend::errorOccurred, this, &MediaPlayer::onErrorOccurred); + connect(m_backend, &PlayerBackend::playbackStateChanged, this, &MediaPlayer::onPlaybackStateChanged); + connect(m_backend, &PlayerBackend::statusChanged, this, &MediaPlayer::onStatusChanged); + connect(m_backend, &PlayerBackend::audioAvailable, this, &MediaPlayer::onAudioAvailable); + connect(m_backend, &PlayerBackend::videoAvailable, this, &MediaPlayer::onVideoAvailable); + connect(m_backend, &PlayerBackend::seekableChanged, this, &MediaPlayer::onSeekableChanged); +} + +void MediaPlayer::createConnections() { + connect(m_ui.m_btnPlayPause, &PlainToolButton::clicked, this, &MediaPlayer::playPause); + connect(m_ui.m_btnStop, &PlainToolButton::clicked, this, &MediaPlayer::stop); + connect(m_ui.m_btnDownload, &PlainToolButton::clicked, this, &MediaPlayer::download); + connect(m_ui.m_btnVolume, &PlainToolButton::clicked, this, &MediaPlayer::muteUnmute); + connect(m_ui.m_slidVolume, &QSlider::valueChanged, this, &MediaPlayer::setVolume); + connect(m_ui.m_slidProgress, &QSlider::valueChanged, this, &MediaPlayer::seek); + connect(m_ui.m_spinSpeed, QOverload::of(&QSpinBox::valueChanged), this, &MediaPlayer::setSpeed); +} diff --git a/src/librssguard/gui/mediaplayer/mediaplayer.h b/src/librssguard/gui/mediaplayer/mediaplayer.h new file mode 100644 index 000000000..22bd81d1e --- /dev/null +++ b/src/librssguard/gui/mediaplayer/mediaplayer.h @@ -0,0 +1,71 @@ +// For license of this file, see /LICENSE.md. + +#ifndef MEDIAPLAYER_H +#define MEDIAPLAYER_H + +#include "gui/tabcontent.h" + +#include "gui/mediaplayer/playerbackend.h" + +#include "ui_mediaplayer.h" + +class MediaPlayer : public TabContent { + Q_OBJECT + + public: + explicit MediaPlayer(QWidget* parent = nullptr); + virtual ~MediaPlayer(); + + virtual WebBrowser* webBrowser() const; + + public slots: + void playUrl(const QString& url); + + private slots: + void playPause(); + void stop(); + void download(); + void muteUnmute(); + + // NOTE: 100 means standard speed, above that value means faster, below means slower. + void setSpeed(int speed); + + // NOTE: Volume is from 0 to 100 taken directly from slider or + // elsewhere. + void setVolume(int volume); + + // NOTE: We seek by second. + void seek(int position); + + void onSpeedChanged(int speed); + void onDurationChanged(int duration); + void onPositionChanged(int position); + void onErrorOccurred(const QString& error_string); + void onStatusChanged(const QString& status); + void onPlaybackStateChanged(PlayerBackend::PlaybackState state); + void onAudioAvailable(bool available); + void onVideoAvailable(bool available); + void onSeekableChanged(bool seekable); + + signals: + void urlDownloadRequested(const QUrl& url); + + private: + void updateTimeAndProgress(int progress, int total); + void setupIcons(); + + void createBackendConnections(); + void createConnections(); + + private: + Ui::MediaPlayer m_ui; + + PlayerBackend* m_backend; + QIcon m_iconPlay; + QIcon m_iconPause; + QIcon m_iconMute; + QIcon m_iconUnmute; + bool m_muted; +}; + +#endif // MEDIAPLAYER_H diff --git a/src/librssguard/gui/reusable/mediaplayer.ui b/src/librssguard/gui/mediaplayer/mediaplayer.ui similarity index 85% rename from src/librssguard/gui/reusable/mediaplayer.ui rename to src/librssguard/gui/mediaplayer/mediaplayer.ui index 4f78498d5..bbabd034d 100644 --- a/src/librssguard/gui/reusable/mediaplayer.ui +++ b/src/librssguard/gui/mediaplayer/mediaplayer.ui @@ -13,17 +13,7 @@ Form - - - - - - 0 - 1 - - - - + @@ -132,12 +122,6 @@ - - QVideoWidget - QWidget -
qvideowidget.h
- 1 -
PlainToolButton QToolButton diff --git a/src/librssguard/gui/mediaplayer/playerbackend.cpp b/src/librssguard/gui/mediaplayer/playerbackend.cpp new file mode 100644 index 000000000..bae6d31e8 --- /dev/null +++ b/src/librssguard/gui/mediaplayer/playerbackend.cpp @@ -0,0 +1,10 @@ +// For license of this file, see /LICENSE.md. + +#include "gui/mediaplayer/playerbackend.h" + +#include + +PlayerBackend::PlayerBackend(QWidget* parent) : QWidget(parent), m_mainLayout(new QVBoxLayout(this)) { + m_mainLayout->setSpacing(0); + m_mainLayout->setContentsMargins({0, 0, 0, 0}); +} diff --git a/src/librssguard/gui/mediaplayer/playerbackend.h b/src/librssguard/gui/mediaplayer/playerbackend.h new file mode 100644 index 000000000..f67d88a71 --- /dev/null +++ b/src/librssguard/gui/mediaplayer/playerbackend.h @@ -0,0 +1,53 @@ +// For license of this file, see /LICENSE.md. + +#ifndef PLAYERBACKEND_H +#define PLAYERBACKEND_H + +#include + +class QVBoxLayout; + +class PlayerBackend : public QWidget { + Q_OBJECT + + public: + enum class PlaybackState { + StoppedState, + PlayingState, + PausedState + }; + + explicit PlayerBackend(QWidget* parent = nullptr); + + virtual QUrl url() const = 0; + virtual int position() const = 0; + virtual int duration() const = 0; + + signals: + void speedChanged(int speed); + void durationChanged(int duration); + void positionChanged(int position); + void errorOccurred(const QString& error_string); + void statusChanged(const QString& status); + void playbackStateChanged(PlaybackState state); + void audioAvailable(bool available); + void videoAvailable(bool available); + void seekableChanged(bool seekable); + + public slots: + virtual void playUrl(const QUrl& url) = 0; + virtual void playPause() = 0; + virtual void pause() = 0; + virtual void stop() = 0; + + virtual void setPlaybackSpeed(int speed) = 0; + virtual void setVolume(int volume) = 0; + virtual void setPosition(int position) = 0; + + signals: + + private: + QVBoxLayout* m_mainLayout; +}; + +#endif // PLAYERBACKEND_H diff --git a/src/librssguard/gui/mediaplayer/qtmultimedia/qtmultimediabackend.cpp b/src/librssguard/gui/mediaplayer/qtmultimedia/qtmultimediabackend.cpp new file mode 100644 index 000000000..a99bb6501 --- /dev/null +++ b/src/librssguard/gui/mediaplayer/qtmultimedia/qtmultimediabackend.cpp @@ -0,0 +1,248 @@ +// For license of this file, see /LICENSE.md. + +#include "gui/mediaplayer/qtmultimedia/qtmultimediabackend.h" + +#if QT_VERSION_MAJOR == 6 +#include +#include +#endif + +#include +#include + +QtMultimediaBackend::QtMultimediaBackend(QWidget* parent) + : PlayerBackend(parent), +#if QT_VERSION_MAJOR == 6 + m_audio(new QAudioOutput(this)), +#endif + m_player(new QMediaPlayer(this)), + + m_video(new QVideoWidget(this)) { + layout()->addWidget(m_video); + + m_player->setVideoOutput(m_video); + +#if QT_VERSION_MAJOR == 6 + m_player->setAudioOutput(m_audio); +#endif + + connect(m_player, &QMediaPlayer::durationChanged, this, &QtMultimediaBackend::onDurationChanged); + +#if QT_VERSION_MAJOR == 6 + connect(m_player, &QMediaPlayer::errorOccurred, this, &QtMultimediaBackend::onErrorOccurred); +#else + connect(m_player, QOverload::of(&QMediaPlayer::error), this, [this](QMediaPlayer::Error error) { + onErrorOccurred(error); + }); +#endif + +#if QT_VERSION_MAJOR == 6 + connect(m_player, &QMediaPlayer::hasAudioChanged, this, &QtMultimediaBackend::onAudioAvailable); + connect(m_player, &QMediaPlayer::hasVideoChanged, this, &QtMultimediaBackend::onVideoAvailable); + connect(m_player, &QMediaPlayer::playbackStateChanged, this, &QtMultimediaBackend::onPlaybackStateChanged); +#else + connect(m_player, &QMediaPlayer::audioAvailableChanged, this, &QtMultimediaBackend::onAudioAvailable); + connect(m_player, &QMediaPlayer::videoAvailableChanged, this, &QtMultimediaBackend::onVideoAvailable); + connect(m_player, &QMediaPlayer::stateChanged, this, &QtMultimediaBackend::onPlaybackStateChanged); +#endif + + connect(m_player, &QMediaPlayer::mediaStatusChanged, this, &QtMultimediaBackend::onMediaStatusChanged); + connect(m_player, &QMediaPlayer::positionChanged, this, &QtMultimediaBackend::onPositionChanged); + connect(m_player, &QMediaPlayer::seekableChanged, this, &QtMultimediaBackend::onSeekableChanged); + connect(m_player, &QMediaPlayer::playbackRateChanged, this, &QtMultimediaBackend::onPlaybackRateChanged); +} + +int QtMultimediaBackend::convertToSliderProgress(qint64 player_progress) const { + return player_progress / 1000; +} + +int QtMultimediaBackend::convertDuration(qint64 duration) const { + return duration / 1000; +} + +qreal QtMultimediaBackend::convertSpeed(int speed) const { + return speed / 100.0; +} + +int QtMultimediaBackend::convertSpinSpeed(qreal speed) const { + return speed * 100; +} + +float QtMultimediaBackend::convertSliderVolume(int slider_volume) const { + return slider_volume / 100.0f; +} + +qint64 QtMultimediaBackend::convertSliderProgress(int slider_progress) const { + return qint64(slider_progress) * qint64(1000); +} + +QString QtMultimediaBackend::mediaStatusToString(QMediaPlayer::MediaStatus status) const { + switch (status) { + case QMediaPlayer::NoMedia: + return tr("No media"); + + case QMediaPlayer::LoadingMedia: + return tr("Loading..."); + + case QMediaPlayer::LoadedMedia: + return tr("Media loaded"); + + case QMediaPlayer::StalledMedia: + return tr("Media stalled"); + + case QMediaPlayer::BufferingMedia: + return tr("Buffering..."); + + case QMediaPlayer::BufferedMedia: + return tr("Loaded"); + + case QMediaPlayer::EndOfMedia: + return tr("Ended"); + + case QMediaPlayer::InvalidMedia: + return tr("Media is invalid"); + + default: + return tr("Unknown"); + } +} + +QString QtMultimediaBackend::errorToString(QMediaPlayer::Error error) const { + switch (error) { + case QMediaPlayer::ResourceError: + return tr("Cannot load media (missing codecs)"); + + case QMediaPlayer::FormatError: + return tr("Unrecognized format"); + + case QMediaPlayer::NetworkError: + return tr("Network problem"); + + case QMediaPlayer::AccessDeniedError: + return tr("Access denied"); + +#if QT_VERSION_MAJOR == 5 + case QMediaPlayer::ServiceMissingError: + return tr("Service is missing"); + + case QMediaPlayer::MediaIsPlaylist: + return tr("This is playlist"); +#endif + + case QMediaPlayer::NoError: + return tr("No errors"); + + default: + return tr("Unknown error"); + } +} + +void QtMultimediaBackend::playUrl(const QUrl& url) { +#if QT_VERSION_MAJOR == 6 + m_player->setSource(url); +#else + m_player->setMedia(QUrl(url)); +#endif + + m_player->play(); +} + +void QtMultimediaBackend::playPause() { + if (m_player->PLAYBACK_STATE_METHOD() != QMediaPlayer::PLAYBACK_STATE::PlayingState) { + m_player->play(); + } + else { + m_player->pause(); + } +} + +void QtMultimediaBackend::pause() { + m_player->pause(); +} + +void QtMultimediaBackend::stop() { + m_player->stop(); +} + +void QtMultimediaBackend::setPlaybackSpeed(int speed) { + m_player->setPlaybackRate(convertSpeed(speed)); +} + +void QtMultimediaBackend::setVolume(int volume) { +#if QT_VERSION_MAJOR == 6 + m_player->audioOutput()->setVolume(convertSliderVolume(volume)); +#else + m_player->setVolume(volume); +#endif +} + +void QtMultimediaBackend::setPosition(int position) { + m_player->setPosition(convertSliderProgress(position)); +} + +QUrl QtMultimediaBackend::url() const { + return +#if QT_VERSION_MAJOR == 6 + m_player->source(); +#else + m_player->media().request().url(); +#endif +} + +int QtMultimediaBackend::position() const { + return convertToSliderProgress(m_player->position()); +} + +int QtMultimediaBackend::duration() const { + return convertDuration(m_player->duration()); +} + +void QtMultimediaBackend::onPositionChanged(qint64 position) { + emit positionChanged(convertToSliderProgress(position)); +} + +void QtMultimediaBackend::onPlaybackRateChanged(qreal speed) { + emit speedChanged(convertSpinSpeed(speed)); +} + +void QtMultimediaBackend::onDurationChanged(qint64 duration) { + emit durationChanged(convertDuration(duration)); +} + +void QtMultimediaBackend::onErrorOccurred(QMediaPlayer::Error error, const QString& error_string) { + QString err = error_string.isEmpty() ? errorToString(error) : error_string; + emit errorOccurred(err); +} + +void QtMultimediaBackend::onAudioAvailable(bool available) { + emit audioAvailable(available); +} + +void QtMultimediaBackend::onVideoAvailable(bool available) { + emit videoAvailable(available); +} + +void QtMultimediaBackend::onMediaStatusChanged(QMediaPlayer::MediaStatus status) { + QString st = mediaStatusToString(status); + emit statusChanged(st); +} + +void QtMultimediaBackend::onPlaybackStateChanged(QMediaPlayer::PLAYBACK_STATE state) { + switch (state) { + case QMediaPlayer::PLAYBACK_STATE::StoppedState: + emit playbackStateChanged(PlayerBackend::PlaybackState::StoppedState); + break; + + case QMediaPlayer::PLAYBACK_STATE::PlayingState: + emit playbackStateChanged(PlayerBackend::PlaybackState::PlayingState); + break; + + case QMediaPlayer::PLAYBACK_STATE::PausedState: + emit playbackStateChanged(PlayerBackend::PlaybackState::PausedState); + break; + } +} + +void QtMultimediaBackend::onSeekableChanged(bool seekable) { + emit seekableChanged(seekable); +} diff --git a/src/librssguard/gui/reusable/mediaplayer.h b/src/librssguard/gui/mediaplayer/qtmultimedia/qtmultimediabackend.h similarity index 56% rename from src/librssguard/gui/reusable/mediaplayer.h rename to src/librssguard/gui/mediaplayer/qtmultimedia/qtmultimediabackend.h index 88a75268e..a30efdabb 100644 --- a/src/librssguard/gui/reusable/mediaplayer.h +++ b/src/librssguard/gui/mediaplayer/qtmultimedia/qtmultimediabackend.h @@ -1,11 +1,11 @@ // For license of this file, see /LICENSE.md. -#ifndef MEDIAPLAYER_H -#define MEDIAPLAYER_H +#ifndef QTMULTIMEDIABACKEND_H +#define QTMULTIMEDIABACKEND_H -#include "gui/tabcontent.h" +#include "gui/mediaplayer/playerbackend.h" -#include "ui_mediaplayer.h" +#include #include @@ -17,35 +17,32 @@ #define PLAYBACK_STATE_METHOD state #endif +#if QT_VERSION_MAJOR == 6 class QAudioOutput; +#endif -class MediaPlayer : public TabContent { +class QVideoWidget; + +class QtMultimediaBackend : public PlayerBackend { Q_OBJECT public: - explicit MediaPlayer(QWidget* parent = nullptr); - virtual ~MediaPlayer(); + explicit QtMultimediaBackend(QWidget* parent = nullptr); - virtual WebBrowser* webBrowser() const; + virtual QUrl url() const; + virtual int position() const; + virtual int duration() const; public slots: - void playUrl(const QString& url); + virtual void playUrl(const QUrl& url); + virtual void playPause(); + virtual void pause(); + virtual void stop(); + virtual void setPlaybackSpeed(int speed); + virtual void setVolume(int volume); + virtual void setPosition(int position); private slots: - void playPause(); - void stop(); - void download(); - void muteUnmute(); - void setSpeed(int speed); - - // NOTE: Volume is from 0 to 100 taken directly from slider or - // elsewhere. - void setVolume(int volume); - - // NOTE: Media is seekable in miliseconds, but that is too muc - // for "int" data type, therefore we seek by second. - void seek(int position); - void onPlaybackRateChanged(qreal speed); void onDurationChanged(qint64 duration); void onErrorOccurred(QMediaPlayer::Error error, const QString& error_string = {}); @@ -56,9 +53,6 @@ class MediaPlayer : public TabContent { void onPositionChanged(qint64 position); void onSeekableChanged(bool seekable); - signals: - void urlDownloadRequested(const QUrl& url); - private: float convertSliderVolume(int slider_volume) const; qint64 convertSliderProgress(int slider_progress) const; @@ -70,23 +64,13 @@ class MediaPlayer : public TabContent { QString errorToString(QMediaPlayer::Error error) const; QString mediaStatusToString(QMediaPlayer::MediaStatus status) const; - void updateTimeAndProgress(int progress, int total); - void setupIcons(); - void createConnections(); - private: - Ui::MediaPlayer m_ui; - #if QT_VERSION_MAJOR == 6 QAudioOutput* m_audio; #endif QMediaPlayer* m_player; - QIcon m_iconPlay; - QIcon m_iconPause; - QIcon m_iconMute; - QIcon m_iconUnmute; - bool m_muted; + QVideoWidget* m_video; }; -#endif // MEDIAPLAYER_H +#endif // QTMULTIMEDIABACKEND_H diff --git a/src/librssguard/gui/notifications/basetoastnotification.cpp b/src/librssguard/gui/notifications/basetoastnotification.cpp index c15f60b35..776165b38 100644 --- a/src/librssguard/gui/notifications/basetoastnotification.cpp +++ b/src/librssguard/gui/notifications/basetoastnotification.cpp @@ -6,7 +6,6 @@ #include "miscellaneous/settings.h" #include -#include #include #include @@ -28,6 +27,11 @@ BaseToastNotification::BaseToastNotification(QWidget* parent) : QDialog(parent), setStyleSheet(QSL("BaseToastNotification { border: 1px solid %1; }").arg(palette().windowText().color().name())); installEventFilter(this); + + m_timerClosingClick.setInterval(200); + m_timerClosingClick.setSingleShot(true); + + connect(&m_timerClosingClick, &QTimer::timeout, this, &BaseToastNotification::close); } BaseToastNotification::~BaseToastNotification() {} @@ -82,7 +86,7 @@ bool BaseToastNotification::eventFilter(QObject* watched, QEvent* event) { if (dynamic_cast(event)->button() == Qt::MouseButton::RightButton) { event->accept(); QCoreApplication::processEvents(); - QTimer::singleShot(200, this, &BaseToastNotification::close); + m_timerClosingClick.start(); return true; } } diff --git a/src/librssguard/gui/notifications/basetoastnotification.h b/src/librssguard/gui/notifications/basetoastnotification.h index d29ac5ceb..141558b53 100644 --- a/src/librssguard/gui/notifications/basetoastnotification.h +++ b/src/librssguard/gui/notifications/basetoastnotification.h @@ -5,6 +5,8 @@ #include +#include + class QAbstractButton; class QLabel; @@ -32,6 +34,7 @@ class BaseToastNotification : public QDialog { void closeRequested(BaseToastNotification* notif); private: + QTimer m_timerClosingClick; int m_timerId; }; diff --git a/src/librssguard/gui/reusable/mediaplayer.cpp b/src/librssguard/gui/reusable/mediaplayer.cpp deleted file mode 100644 index ba25bb817..000000000 --- a/src/librssguard/gui/reusable/mediaplayer.cpp +++ /dev/null @@ -1,309 +0,0 @@ -// For license of this file, see /LICENSE.md. - -#include "mediaplayer.h" - -#include "miscellaneous/iconfactory.h" - -#if QT_VERSION_MAJOR == 6 -#include -#include -#endif - -MediaPlayer::MediaPlayer(QWidget* parent) - : TabContent(parent), -#if QT_VERSION_MAJOR == 6 - m_audio(new QAudioOutput(this)), -#endif - m_player(new QMediaPlayer(this)), m_muted(false) { - m_ui.setupUi(this); - - m_player->setVideoOutput(m_ui.m_video); - -#if QT_VERSION_MAJOR == 6 - m_player->setAudioOutput(m_audio); -#endif - - setupIcons(); - createConnections(); - - onPlaybackStateChanged(QMediaPlayer::PLAYBACK_STATE::StoppedState); - onMediaStatusChanged(QMediaPlayer::MediaStatus::NoMedia); -} - -MediaPlayer::~MediaPlayer() {} - -WebBrowser* MediaPlayer::webBrowser() const { - return nullptr; -} - -void MediaPlayer::playUrl(const QString& url) { - if (m_muted) { - muteUnmute(); - } - else { - setVolume(m_ui.m_slidVolume->value()); - } - -#if QT_VERSION_MAJOR == 6 - m_player->setSource(url); -#else - m_player->setMedia(QUrl(url)); -#endif - - m_player->play(); -} - -void MediaPlayer::playPause() { - if (m_player->PLAYBACK_STATE_METHOD() != QMediaPlayer::PLAYBACK_STATE::PlayingState) { - m_player->play(); - } - else { - m_player->pause(); - } -} - -void MediaPlayer::stop() { - m_player->stop(); -} - -void MediaPlayer::download() { - emit urlDownloadRequested( -#if QT_VERSION_MAJOR == 6 - m_player->source() -#else - m_player->media().request().url() -#endif - ); -} - -void MediaPlayer::muteUnmute() { - m_ui.m_slidVolume->setEnabled(m_muted); - setVolume(m_muted ? m_ui.m_slidVolume->value() : 0); - - m_muted = !m_muted; -} - -void MediaPlayer::setSpeed(int speed) { - m_player->setPlaybackRate(convertSpeed(speed)); -} - -void MediaPlayer::setVolume(int volume) { -#if QT_VERSION_MAJOR == 6 - m_player->audioOutput()->setVolume(convertSliderVolume(volume)); -#else - m_player->setVolume(volume); -#endif - - m_ui.m_btnVolume->setIcon(volume <= 0 ? m_iconMute : m_iconUnmute); -} - -void MediaPlayer::seek(int position) { - m_player->setPosition(convertSliderProgress(position)); -} - -void MediaPlayer::onPlaybackRateChanged(qreal speed) { - m_ui.m_spinSpeed->blockSignals(true); - m_ui.m_spinSpeed->setValue(convertSpinSpeed(speed)); - m_ui.m_spinSpeed->blockSignals(false); -} - -void MediaPlayer::onDurationChanged(qint64 duration) { - m_ui.m_slidProgress->blockSignals(true); - m_ui.m_slidProgress->setMaximum(convertDuration(duration)); - m_ui.m_slidProgress->blockSignals(false); - - updateTimeAndProgress(convertToSliderProgress(m_player->position()), convertDuration(duration)); -} - -void MediaPlayer::onPositionChanged(qint64 position) { - m_ui.m_slidProgress->blockSignals(true); - m_ui.m_slidProgress->setValue(convertToSliderProgress(position)); - m_ui.m_slidProgress->blockSignals(false); - - updateTimeAndProgress(convertToSliderProgress(position), convertDuration(m_player->duration())); -} - -void MediaPlayer::updateTimeAndProgress(int progress, int total) { - m_ui.m_lblTime->setText(QSL("%1/%2").arg(QDateTime::fromSecsSinceEpoch(progress).toUTC().toString("hh:mm:ss"), - QDateTime::fromSecsSinceEpoch(total).toUTC().toString("hh:mm:ss"))); -} - -void MediaPlayer::onErrorOccurred(QMediaPlayer::Error error, const QString& error_string) { - QString err = error_string.isEmpty() ? errorToString(error) : error_string; - m_ui.m_lblStatus->setStatus(WidgetWithStatus::StatusType::Error, err, err); -} - -void MediaPlayer::onAudioAvailable(bool available) { - m_ui.m_slidVolume->setEnabled(available); - m_ui.m_btnVolume->setEnabled(available); -} - -void MediaPlayer::onVideoAvailable(bool available) { - Q_UNUSED(available) -} - -void MediaPlayer::onMediaStatusChanged(QMediaPlayer::MediaStatus status) { - QString st = mediaStatusToString(status); - m_ui.m_lblStatus->setStatus(status == QMediaPlayer::MediaStatus::InvalidMedia - ? WidgetWithStatus::StatusType::Error - : WidgetWithStatus::StatusType::Information, - st, - st); -} - -void MediaPlayer::onPlaybackStateChanged(QMediaPlayer::PLAYBACK_STATE state) { - switch (state) { - case QMediaPlayer::PLAYBACK_STATE::StoppedState: - m_ui.m_btnPlayPause->setIcon(m_iconPlay); - m_ui.m_btnStop->setEnabled(false); - break; - - case QMediaPlayer::PLAYBACK_STATE::PlayingState: - m_ui.m_btnPlayPause->setIcon(m_iconPause); - m_ui.m_btnStop->setEnabled(true); - break; - - case QMediaPlayer::PLAYBACK_STATE::PausedState: - m_ui.m_btnPlayPause->setIcon(m_iconPlay); - m_ui.m_btnStop->setEnabled(true); - break; - } -} - -int MediaPlayer::convertToSliderProgress(qint64 player_progress) const { - return player_progress / 1000; -} - -int MediaPlayer::convertDuration(qint64 duration) const { - return duration / 1000; -} - -qreal MediaPlayer::convertSpeed(int speed) const { - return speed / 100.0; -} - -int MediaPlayer::convertSpinSpeed(qreal speed) const { - return speed * 100; -} - -void MediaPlayer::onSeekableChanged(bool seekable) { - m_ui.m_slidProgress->setEnabled(seekable); - - if (!seekable) { - onPositionChanged(0); - } -} - -QString MediaPlayer::errorToString(QMediaPlayer::Error error) const { - switch (error) { - case QMediaPlayer::ResourceError: - return tr("Cannot load media (missing codecs)"); - - case QMediaPlayer::FormatError: - return tr("Unrecognized format"); - - case QMediaPlayer::NetworkError: - return tr("Network problem"); - - case QMediaPlayer::AccessDeniedError: - return tr("Access denied"); - -#if QT_VERSION_MAJOR == 5 - case QMediaPlayer::ServiceMissingError: - return tr("Service is missing"); - - case QMediaPlayer::MediaIsPlaylist: - return tr("This is playlist"); -#endif - - case QMediaPlayer::NoError: - return tr("No errors"); - - default: - return tr("Unknown error"); - } -} - -float MediaPlayer::convertSliderVolume(int slider_volume) const { - return slider_volume / 100.0f; -} - -qint64 MediaPlayer::convertSliderProgress(int slider_progress) const { - return qint64(slider_progress) * qint64(1000); -} - -QString MediaPlayer::mediaStatusToString(QMediaPlayer::MediaStatus status) const { - switch (status) { - case QMediaPlayer::NoMedia: - return tr("No media"); - - case QMediaPlayer::LoadingMedia: - return tr("Loading..."); - - case QMediaPlayer::LoadedMedia: - return tr("Media loaded"); - - case QMediaPlayer::StalledMedia: - return tr("Media stalled"); - - case QMediaPlayer::BufferingMedia: - return tr("Buffering..."); - - case QMediaPlayer::BufferedMedia: - return tr("Loaded"); - - case QMediaPlayer::EndOfMedia: - return tr("Ended"); - - case QMediaPlayer::InvalidMedia: - return tr("Media is invalid"); - - default: - return tr("Unknown"); - } -} - -void MediaPlayer::setupIcons() { - m_iconPlay = qApp->icons()->fromTheme(QSL("media-playback-start"), QSL("player_play")); - m_iconPause = qApp->icons()->fromTheme(QSL("media-playback-pause"), QSL("player_pause")); - m_iconMute = qApp->icons()->fromTheme(QSL("player-volume-muted"), QSL("audio-volume-muted")); - m_iconUnmute = qApp->icons()->fromTheme(QSL("player-volume"), QSL("stock_volume")); - - m_ui.m_btnDownload->setIcon(qApp->icons()->fromTheme(QSL("download"), QSL("browser-download"))); - m_ui.m_btnStop->setIcon(qApp->icons()->fromTheme(QSL("media-playback-stop"), QSL("player_stop"))); -} - -void MediaPlayer::createConnections() { - connect(m_player, &QMediaPlayer::durationChanged, this, &MediaPlayer::onDurationChanged); - -#if QT_VERSION_MAJOR == 6 - connect(m_player, &QMediaPlayer::errorOccurred, this, &MediaPlayer::onErrorOccurred); -#else - connect(m_player, QOverload::of(&QMediaPlayer::error), this, [this](QMediaPlayer::Error error) { - onErrorOccurred(error); - }); -#endif - -#if QT_VERSION_MAJOR == 6 - connect(m_player, &QMediaPlayer::hasAudioChanged, this, &MediaPlayer::onAudioAvailable); - connect(m_player, &QMediaPlayer::hasVideoChanged, this, &MediaPlayer::onVideoAvailable); - connect(m_player, &QMediaPlayer::playbackStateChanged, this, &MediaPlayer::onPlaybackStateChanged); -#else - connect(m_player, &QMediaPlayer::audioAvailableChanged, this, &MediaPlayer::onAudioAvailable); - connect(m_player, &QMediaPlayer::videoAvailableChanged, this, &MediaPlayer::onVideoAvailable); - connect(m_player, &QMediaPlayer::stateChanged, this, &MediaPlayer::onPlaybackStateChanged); -#endif - - connect(m_player, &QMediaPlayer::mediaStatusChanged, this, &MediaPlayer::onMediaStatusChanged); - connect(m_player, &QMediaPlayer::positionChanged, this, &MediaPlayer::onPositionChanged); - connect(m_player, &QMediaPlayer::seekableChanged, this, &MediaPlayer::onSeekableChanged); - connect(m_player, &QMediaPlayer::playbackRateChanged, this, &MediaPlayer::onPlaybackRateChanged); - - connect(m_ui.m_btnPlayPause, &PlainToolButton::clicked, this, &MediaPlayer::playPause); - connect(m_ui.m_btnStop, &PlainToolButton::clicked, this, &MediaPlayer::stop); - connect(m_ui.m_btnDownload, &PlainToolButton::clicked, this, &MediaPlayer::download); - connect(m_ui.m_btnVolume, &PlainToolButton::clicked, this, &MediaPlayer::muteUnmute); - connect(m_ui.m_slidVolume, &QSlider::valueChanged, this, &MediaPlayer::setVolume); - connect(m_ui.m_slidProgress, &QSlider::valueChanged, this, &MediaPlayer::seek); - connect(m_ui.m_spinSpeed, QOverload::of(&QSpinBox::valueChanged), this, &MediaPlayer::setSpeed); -} diff --git a/src/librssguard/gui/tabwidget.cpp b/src/librssguard/gui/tabwidget.cpp index c79756c38..85fbea1eb 100644 --- a/src/librssguard/gui/tabwidget.cpp +++ b/src/librssguard/gui/tabwidget.cpp @@ -8,7 +8,6 @@ #include "gui/feedsview.h" #include "gui/messagepreviewer.h" #include "gui/messagesview.h" -#include "gui/reusable/mediaplayer.h" #include "gui/reusable/plaintoolbutton.h" #include "gui/tabbar.h" #include "gui/webbrowser.h" @@ -17,6 +16,10 @@ #include "miscellaneous/settings.h" #include "miscellaneous/textfactory.h" +#if defined(ENABLE_MEDIAPLAYER) +#include "gui/mediaplayer/mediaplayer.h" +#endif + #include #include #include @@ -224,6 +227,7 @@ int TabWidget::addEmptyBrowser() { return addBrowser(false, true); } +#if defined(ENABLE_MEDIAPLAYER) int TabWidget::addMediaPlayer(const QString& url, bool make_active) { auto* player = new MediaPlayer(this); @@ -248,6 +252,7 @@ int TabWidget::addMediaPlayer(const QString& url, bool make_active) { return index; } +#endif int TabWidget::addLinkedBrowser(const QUrl& initial_url) { return addBrowser(false, false, initial_url); diff --git a/src/librssguard/gui/tabwidget.h b/src/librssguard/gui/tabwidget.h index 9196dc5f6..e519c47a1 100644 --- a/src/librssguard/gui/tabwidget.h +++ b/src/librssguard/gui/tabwidget.h @@ -78,7 +78,9 @@ class TabWidget : public QTabWidget { // Adds new WebBrowser tab to global TabWidget. int addEmptyBrowser(); +#if defined(ENABLE_MEDIAPLAYER) int addMediaPlayer(const QString& url, bool make_active); +#endif // Adds new WebBrowser with link. This is used when user // selects to "Open link in new tab.". diff --git a/src/librssguard/gui/webviewers/webviewer.cpp b/src/librssguard/gui/webviewers/webviewer.cpp index 49993f594..6943c6a5e 100644 --- a/src/librssguard/gui/webviewers/webviewer.cpp +++ b/src/librssguard/gui/webviewers/webviewer.cpp @@ -27,7 +27,10 @@ void WebViewer::processContextMenu(QMenu* specific_menu, QContextMenuEvent* even specific_menu->addAction(m_actionPlayLink.data()); m_actionOpenExternalBrowser.data()->setEnabled(m_contextMenuData.m_linkUrl.isValid()); + +#if defined(ENABLE_MEDIAPLAYER) m_actionPlayLink.data()->setEnabled(m_contextMenuData.m_linkUrl.isValid()); +#endif if (m_contextMenuData.m_linkUrl.isValid()) { QFileIconProvider icon_provider; @@ -61,11 +64,13 @@ void WebViewer::processContextMenu(QMenu* specific_menu, QContextMenuEvent* even } void WebViewer::playClickedLinkAsMedia() { +#if defined(ENABLE_MEDIAPLAYER) auto context_url = m_contextMenuData.m_linkUrl; if (context_url.isValid()) { qApp->mainForm()->tabWidget()->addMediaPlayer(context_url.toString(), true); } +#endif } void WebViewer::openClickedLinkInExternalBrowser() { @@ -97,6 +102,11 @@ void WebViewer::initializeCommonMenuItems() { m_actionPlayLink.reset(new QAction(qApp->icons()->fromTheme(QSL("player_play"), QSL("media-playback-start")), QObject::tr("Play link as audio/video"))); +#if !defined(ENABLE_MEDIAPLAYER) + m_actionPlayLink->setText(m_actionPlayLink->text() + QSL(" ") + QObject::tr("(not supported)")); + m_actionPlayLink->setEnabled(false); +#endif + QObject::connect(m_actionOpenExternalBrowser.data(), &QAction::triggered, m_actionOpenExternalBrowser.data(),