refactor media player, separate into backend logic

This commit is contained in:
Martin Rotter 2023-11-27 10:53:08 +01:00
parent d1ae926b5d
commit 2b2aaee70a
16 changed files with 642 additions and 376 deletions

View File

@ -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)

View File

@ -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

View File

@ -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()) {

View File

@ -0,0 +1,172 @@
// For license of this file, see <project-root-folder>/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<int>::of(&QSpinBox::valueChanged), this, &MediaPlayer::setSpeed);
}

View File

@ -0,0 +1,71 @@
// For license of this file, see <project-root-folder>/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

View File

@ -13,17 +13,7 @@
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QVideoWidget" name="m_video" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>1</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<layout class="QVBoxLayout" name="m_layoutMain">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
@ -132,12 +122,6 @@
</layout>
</widget>
<customwidgets>
<customwidget>
<class>QVideoWidget</class>
<extends>QWidget</extends>
<header>qvideowidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>PlainToolButton</class>
<extends>QToolButton</extends>

View File

@ -0,0 +1,10 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "gui/mediaplayer/playerbackend.h"
#include <QVBoxLayout>
PlayerBackend::PlayerBackend(QWidget* parent) : QWidget(parent), m_mainLayout(new QVBoxLayout(this)) {
m_mainLayout->setSpacing(0);
m_mainLayout->setContentsMargins({0, 0, 0, 0});
}

View File

@ -0,0 +1,53 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef PLAYERBACKEND_H
#define PLAYERBACKEND_H
#include <QWidget>
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

View File

@ -0,0 +1,248 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "gui/mediaplayer/qtmultimedia/qtmultimediabackend.h"
#if QT_VERSION_MAJOR == 6
#include <QAudioOutput>
#include <QWindow>
#endif
#include <QLayout>
#include <QVideoWidget>
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<QMediaPlayer::Error>::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);
}

View File

@ -1,11 +1,11 @@
// For license of this file, see <project-root-folder>/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 <QObject>
#include <QMediaPlayer>
@ -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

View File

@ -6,7 +6,6 @@
#include "miscellaneous/settings.h"
#include <QCloseEvent>
#include <QTimer>
#include <QTimerEvent>
#include <chrono>
@ -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<QMouseEvent*>(event)->button() == Qt::MouseButton::RightButton) {
event->accept();
QCoreApplication::processEvents();
QTimer::singleShot(200, this, &BaseToastNotification::close);
m_timerClosingClick.start();
return true;
}
}

View File

@ -5,6 +5,8 @@
#include <QDialog>
#include <QTimer>
class QAbstractButton;
class QLabel;
@ -32,6 +34,7 @@ class BaseToastNotification : public QDialog {
void closeRequested(BaseToastNotification* notif);
private:
QTimer m_timerClosingClick;
int m_timerId;
};

View File

@ -1,309 +0,0 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#include "mediaplayer.h"
#include "miscellaneous/iconfactory.h"
#if QT_VERSION_MAJOR == 6
#include <QAudioOutput>
#include <QWindow>
#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<QMediaPlayer::Error>::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<int>::of(&QSpinBox::valueChanged), this, &MediaPlayer::setSpeed);
}

View File

@ -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 <QMenu>
#include <QTimer>
#include <QToolButton>
@ -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);

View File

@ -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.".

View File

@ -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(),