[vlc-commits] [Git][videolan/vlc][master] 13 commits: qt: expose the inner ActionGroup of ListMenuHelper

Steve Lhomme (@robUx4) gitlab at videolan.org
Fri Oct 11 13:08:20 UTC 2024



Steve Lhomme pushed to branch master at VideoLAN / VLC


Commits:
5b5cace7 by Pierre Lamot at 2024-10-11T12:38:52+00:00
qt: expose the inner ActionGroup of ListMenuHelper

- - - - -
1aa580f4 by Pierre Lamot at 2024-10-11T12:38:52+00:00
qt: forward the checked state on select events in ListMenuHelper

- - - - -
a06cd634 by Pierre Lamot at 2024-10-11T12:38:52+00:00
qt: ensure CheckStateRole is defined in ListMenuHelper models

- - - - -
1c5d7b47 by Pierre Lamot at 2024-10-11T12:38:52+00:00
qt: allow to create an VLCAccessImageProvider image response directly from URL

this can be used by Qt GUI classes that don't need to pass through the QML image
provider scheme

- - - - -
411fb08c by Pierre Lamot at 2024-10-11T12:38:52+00:00
qt: support icons in ListMenuHelper

- - - - -
c8807eab by Pierre Lamot at 2024-10-11T12:38:52+00:00
qt: selectively update Actions in ListMenuHelper

- - - - -
83b7c8d9 by Pierre Lamot at 2024-10-11T12:38:52+00:00
qt: make CheckableListMenu use Qt enum instead of a custom one

- - - - -
5b749e59 by Pierre Lamot at 2024-10-11T12:38:52+00:00
qt: make CheckableListMenu use ListMenuHelper

- - - - -
28df34f9 by Pierre Lamot at 2024-10-11T12:38:52+00:00
qt: make RecentMenu use ListMenuHelper

- - - - -
4327da49 by Pierre Lamot at 2024-10-11T12:38:52+00:00
qt: update RendererManager model

* make it implement QAbstractListModel
* RendererManager being tied to the player instance make it a submodel of the
  player controller
* react on player renderer changes
* factorize RendererMenu to use ListMenuHelper as RendererManager is now a QAbstractListModel

- - - - -
26ea26a1 by Pierre Lamot at 2024-10-11T12:38:52+00:00
qt: add configure check to detect QTest

- - - - -
b3dfa3f7 by Pierre Lamot at 2024-10-11T12:38:52+00:00
meson: allow qt tests to moc .cpp files

- - - - -
e32174b3 by Pierre Lamot at 2024-10-11T12:38:52+00:00
qt: add unit test for RendererManager model

- - - - -


18 changed files:

- configure.ac
- modules/gui/qt/Makefile.am
- modules/gui/qt/maininterface/mainctx.cpp
- modules/gui/qt/medialibrary/mlrecentsmodel.cpp
- modules/gui/qt/menus/custom_menus.cpp
- modules/gui/qt/menus/custom_menus.hpp
- modules/gui/qt/menus/menus.cpp
- modules/gui/qt/menus/qml_menu_wrapper.cpp
- modules/gui/qt/meson.build
- modules/gui/qt/player/player_controller.cpp
- modules/gui/qt/player/player_controller.hpp
- modules/gui/qt/player/player_controller_p.hpp
- + modules/gui/qt/tests/test_renderer_manager_model.cpp
- modules/gui/qt/util/renderer_manager.cpp
- modules/gui/qt/util/renderer_manager.hpp
- modules/gui/qt/util/vlcaccess_image_provider.cpp
- modules/gui/qt/util/vlcaccess_image_provider.hpp
- test/meson.build


Changes:

=====================================
configure.ac
=====================================
@@ -3986,6 +3986,7 @@ AC_ARG_ENABLE([qt],
   ])
 ])
 have_qt_gtk="no"
+have_qt_qtest="no"
 have_qt_quick_test="no"
 have_qt_declarative_private="no"
 have_qt_gui_private="no"
@@ -4092,6 +4093,24 @@ AS_IF([test "${enable_qt}" != "no"], [
         have_qt="no"
       ])
 
+      AC_MSG_CHECKING([for Qt QTest])
+      (${QMAKE} ${srcdir}/modules/gui/qt/qtest.pro -o ${ac_pwd}/modules/gui/qt/qmake-qtest) 2>/dev/null
+      ac_status=$?
+      AS_IF([test $ac_status = 0 && test -f ${ac_pwd}/modules/gui/qt/qmake-qtest],[
+        echo "include ${ac_pwd}/modules/gui/qt/qmake-common.mk"        >> ${ac_pwd}/modules/gui/qt/qmake-qtest
+        AC_MSG_RESULT([yes])
+        QT_QTEST_LIBS=$(cd ${ac_pwd}/modules/gui/qt && make -s -f qmake-qtest get_libs)
+        QT_QTEST_CFLAGS=$(cd ${ac_pwd}/modules/gui/qt && make -s -f qmake-qtest get_cflags)
+        QT_QTEST_LDFLAGS=$(cd ${ac_pwd}/modules/gui/qt && make -s -f qmake-qtest get_cflags)
+        AC_SUBST([QT_QTEST_LIBS])
+        AC_SUBST([QT_QTEST_CFLAGS])
+        AC_SUBST([QT_QTEST_LDFLAGS])
+        have_qt_qtest="yes"
+      ],[
+        AC_MSG_RESULT([no])
+      ])
+      rm -f ${ac_pwd}/modules/gui/qt/qmake-qtest
+
       AC_MSG_CHECKING([for QuickTest])
       (${QMAKE} ${srcdir}/modules/gui/qt/quicktest.pro -o ${ac_pwd}/modules/gui/qt/qmake-quicktest) 2>/dev/null
       ac_status=$?
@@ -4217,6 +4236,7 @@ AS_IF([test "${enable_qt}" != "no"], [
 AC_SUBST([QT_INDIRECT_LIBS])
 AM_CONDITIONAL([ENABLE_QT], [test "$enable_qt" != "no"])
 AM_CONDITIONAL([HAVE_QT_GTK], [test "${have_qt_gtk}" = "yes"])
+AM_CONDITIONAL([HAVE_QT_QTEST], [test "${have_qt_qtest}" = "yes"])
 AM_CONDITIONAL([HAVE_QT_QUICK_TEST], [test "${have_qt_quick_test}" = "yes"])
 AM_CONDITIONAL([HAVE_QT_DECLARATIVE_PRIVATE], [test "${have_qt_declarative_private}" = "yes"])
 AM_CONDITIONAL([HAVE_QT_GUI_PRIVATE], [test "${have_qt_gui_private}" = "yes"])


=====================================
modules/gui/qt/Makefile.am
=====================================
@@ -1538,6 +1538,28 @@ base_model_test_LDFLAGS = $(AM_LDFLAGS) $(QT_LDFLAGS)
 check_PROGRAMS = base_model_test
 TESTS = base_model_test
 
+if HAVE_QT_QTEST
+
+LIBVLC = -L../../../lib -lvlc
+
+test_renderer_manager_model_SOURCES = \
+	tests/test_renderer_manager_model.cpp \
+    util/renderer_manager.hpp util/renderer_manager.cpp
+
+nodist_test_renderer_manager_model_SOURCES = \
+	tests/test_renderer_manager_model.moc \
+    util/renderer_manager.moc.cpp
+
+BUILT_SOURCES += tests/test_renderer_manager_model.moc
+CLEANFILES += tests/test_renderer_manager_model.moc
+test_renderer_manager_model_CPPFLAGS = $(libqt_plugin_la_CPPFLAGS) -I$(builddir)/tests -DTOP_BUILDDIR=\"$(abs_top_builddir)\"
+test_renderer_manager_model_CXXFLAGS = $(AM_CXXFLAGS) $(QT_CFLAGS) $(QT_QTEST_CFLAGS) -fPIC $(CXXFLAGS_qt)
+test_renderer_manager_model_LDADD = $(QT_LIBS) $(LIBS_qt) $(QT_QTEST_LIBS) $(LIBVLCCORE) $(LIBVLC)
+test_renderer_manager_model_LDFLAGS = $(AM_LDFLAGS) $(QT_LDFLAGS) $(QT_QTEST_LDFLAGS)
+check_PROGRAMS += test_renderer_manager_model
+TESTS += test_renderer_manager_model
+
+endif
 
 QML_LOG_COMPILER = $(builddir)/qml_test -input
 


=====================================
modules/gui/qt/maininterface/mainctx.cpp
=====================================
@@ -260,8 +260,6 @@ MainCtx::MainCtx(qt_intf_t *_p_intf)
 
 MainCtx::~MainCtx()
 {
-    RendererManager::killInstance();
-
     /* Save states */
 
     settings->beginGroup("MainWindow");


=====================================
modules/gui/qt/medialibrary/mlrecentsmodel.cpp
=====================================
@@ -53,6 +53,7 @@ QVariant MLRecentsModel::itemRoleData(MLItem *item , int role ) const
     {
     case RECENT_MEDIA_ID:
         return QVariant::fromValue( media->getId() );
+    case Qt::DisplayRole:
     case RECENT_MEDIA_URL:
         return QVariant::fromValue( media->getUrl().toString(QUrl::PreferLocalFile | QUrl::RemovePassword));
     case RECENT_MEDIA_LAST_PLAYED_DATE:


=====================================
modules/gui/qt/menus/custom_menus.cpp
=====================================
@@ -36,6 +36,7 @@
 
 // Menus includes
 #include "menus/menus.hpp"
+#include "player/player_controller.hpp"
 
 // Qt includes
 #include <QMenu>
@@ -49,260 +50,117 @@
 #include <QMetaProperty>
 #include <QMetaMethod>
 
-RendererAction::RendererAction( vlc_renderer_item_t *p_item_ )
-    : QAction()
-{
-    p_item = p_item_;
-    vlc_renderer_item_hold( p_item );
-    if( vlc_renderer_item_flags( p_item ) & VLC_RENDERER_CAN_VIDEO )
-        setIcon( QIcon( ":/menu/movie.svg" ) );
-    else
-        setIcon( QIcon( ":/menu/music.svg" ) );
-    setText( vlc_renderer_item_name( p_item ) );
-    setCheckable(true);
-}
-
-RendererAction::~RendererAction()
-{
-    vlc_renderer_item_release( p_item );
-}
-
-vlc_renderer_item_t * RendererAction::getItem()
-{
-    return p_item;
-}
+#include "util/vlcaccess_image_provider.hpp"
 
-RendererMenu::RendererMenu( QMenu *parent, qt_intf_t *p_intf_ )
-    : QMenu( parent ), p_intf( p_intf_ )
+RendererMenu::RendererMenu( QMenu* parent, qt_intf_t* intf, PlayerController* player )
+    : QMenu( parent)
+    , p_intf( intf )
+    , m_renderManager(player->getRendererManager())
 {
     setTitle( qtr("&Renderer") );
 
-    group = new QActionGroup( this );
-
     QAction *action = new QAction( qtr("<Local>"), this );
     action->setCheckable(true);
+    action->setChecked(!m_renderManager->useRenderer());
+    connect(action, &QAction::triggered, this, [this](bool checked){
+        if (checked) {
+            m_renderManager->disableRenderer();
+        }
+    });
     addAction( action );
-    group->addAction(action);
+    connect(m_renderManager,  &RendererManager::useRendererChanged, action,
+            [action, this](){
+                action->setChecked(!m_renderManager->useRenderer());
+    });
 
-    vlc_player_Lock( p_intf_->p_player );
-    if ( vlc_player_GetRenderer( p_intf->p_player ) == nullptr )
-        action->setChecked( true );
-    vlc_player_Unlock( p_intf_->p_player );
+    QAction* separator = addSeparator();
 
-    addSeparator();
+    ListMenuHelper* helper = new ListMenuHelper(this, m_renderManager, separator, this);
+    connect(helper, &ListMenuHelper::select, this, [this](int row, bool checked){
+        m_renderManager->setData(m_renderManager->index(row), checked, Qt::CheckStateRole);
+    });
+
+    QActionGroup* actionGroup = helper->getActionGroup();
+    actionGroup->setExclusionPolicy(QActionGroup::ExclusionPolicy::Exclusive);
+    //the <Local> node is part of the group
+    actionGroup->addAction(action);
 
     QWidget *statusWidget = new QWidget();
     statusWidget->setLayout( new QVBoxLayout );
-    QLabel *label = new QLabel();
-    label->setObjectName( "statuslabel" );
-    statusWidget->layout()->addWidget( label );
-    QProgressBar *pb = new QProgressBar();
-    pb->setObjectName( "statusprogressbar" );
-    pb->setMaximumHeight( 10 );
-    pb->setStyleSheet( QString("\
-        QProgressBar:horizontal {\
-            border: none;\
-            background: transparent;\
-            padding: 1px;\
-        }\
-        QProgressBar::chunk:horizontal {\
-            background: qlineargradient(x1: 0, y1: 0.5, x2: 1, y2: 0.5, \
-                        stop: 0 white, stop: 0.4 orange, stop: 0.6 orange, stop: 1 white);\
-        }") );
-    pb->setRange( 0, 0 );
-    pb->setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Maximum );
-    statusWidget->layout()->addWidget( pb );
+    m_statusLabel = new QLabel();
+    statusWidget->layout()->addWidget( m_statusLabel );
+    m_statusProgressBar = new QProgressBar();
+    m_statusProgressBar->setMaximumHeight( 10 );
+    m_statusProgressBar->setStyleSheet( QString(R"RAW(
+        QProgressBar:horizontal {
+            border: none;
+            background: transparent;
+            padding: 1px;
+        }
+        QProgressBar::chunk:horizontal {
+            background: qlineargradient(x1: 0, y1: 0.5, x2: 1, y2: 0.5,
+                        stop: 0 white, stop: 0.4 orange, stop: 0.6 orange, stop: 1 white);
+        })RAW") );
+    m_statusProgressBar->setRange( 0, 0 );
+    m_statusProgressBar->setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Maximum );
+    statusWidget->layout()->addWidget( m_statusProgressBar );
+
     QWidgetAction *qwa = new QWidgetAction( this );
     qwa->setDefaultWidget( statusWidget );
     qwa->setDisabled( true );
     addAction( qwa );
-    status = qwa;
-
-    RendererManager *manager = RendererManager::getInstance( p_intf );
-    connect( this, &RendererMenu::aboutToShow, manager, &RendererManager::StartScan );
-    connect( group, &QActionGroup::triggered, this, &RendererMenu::RendererSelected );
-    connect( manager, SIGNAL(rendererItemAdded( vlc_renderer_item_t * )),
-             this, SLOT(addRendererItem( vlc_renderer_item_t * )), Qt::DirectConnection );
-    connect( manager, SIGNAL(rendererItemRemoved( vlc_renderer_item_t * )),
-             this, SLOT(removeRendererItem( vlc_renderer_item_t * )), Qt::DirectConnection );
-    connect( manager, &RendererManager::statusUpdated, this, &RendererMenu::updateStatus );
+    m_statusAction = qwa;
+
+    connect( this, &RendererMenu::aboutToShow, m_renderManager, &RendererManager::StartScan );
+    connect( m_renderManager, &RendererManager::statusChanged, this, &RendererMenu::updateStatus );
+    connect( m_renderManager, &RendererManager::scanRemainChanged, this, &RendererMenu::updateStatus );
+    updateStatus();
 }
 
 RendererMenu::~RendererMenu()
-{
-    reset();
-}
+{}
 
-void RendererMenu::updateStatus( int val )
+void RendererMenu::updateStatus()
 {
-    QProgressBar *pb = findChild<QProgressBar *>("statusprogressbar");
-    QLabel *label = findChild<QLabel *>("statuslabel");
-    if( val >= RendererManager::RendererStatus::RUNNING )
+
+    switch (m_renderManager->getStatus())
     {
-        label->setText( qtr("Scanning...").
-               append( QString(" (%1s)").arg( val ) ) );
-        pb->setVisible( true );
-        status->setVisible( true );
-    }
-    else if( val == RendererManager::RendererStatus::FAILED )
+    case RendererManager::RendererStatus::RUNNING:
     {
-        label->setText( "Failed (no discovery module available)" );
-        pb->setVisible( false );
-        status->setVisible( true );
+        int scanRemain = m_renderManager->getScanRemain();
+        m_statusLabel->setText( qtr("Scanning...").
+               append( QString(" (%1s)").arg( scanRemain ) ) );
+        m_statusProgressBar->setVisible( true );
+        m_statusAction->setVisible( true );
+        break;
     }
-    else status->setVisible( false );
-}
-
-void RendererMenu::addRendererItem( vlc_renderer_item_t *p_item )
-{
-    QAction *action = new RendererAction( p_item );
-    insertAction( status, action );
-    group->addAction( action );
-}
-
-void RendererMenu::removeRendererItem( vlc_renderer_item_t *p_item )
-{
-    foreach (QAction* action, group->actions())
+    case RendererManager::RendererStatus::FAILED:
     {
-        RendererAction *ra = qobject_cast<RendererAction *>( action );
-        if( !ra || ra->getItem() != p_item )
-            continue;
-        removeRendererAction( ra );
-        delete ra;
+        m_statusLabel->setText( "Failed (no discovery module available)" );
+        m_statusProgressBar->setVisible( false );
+        m_statusAction->setVisible( true );
         break;
     }
-}
-
-void RendererMenu::addRendererAction(QAction *action)
-{
-    insertAction( status, action );
-    group->addAction( action );
-}
-
-void RendererMenu::removeRendererAction(QAction *action)
-{
-    removeAction( action );
-    group->removeAction( action );
-}
-
-void RendererMenu::reset()
-{
-    /* reset the list of renderers */
-    foreach (QAction* action, group->actions())
-    {
-        RendererAction *ra = qobject_cast<RendererAction *>( action );
-        if( ra )
-        {
-            removeRendererAction( ra );
-            delete ra;
-        }
+    case RendererManager::RendererStatus::IDLE:
+        m_statusAction->setVisible( false );
+        break;
     }
 }
 
-void RendererMenu::RendererSelected(QAction *action)
-{
-    RendererAction *ra = qobject_cast<RendererAction *>( action );
-    if( ra )
-        RendererManager::getInstance( p_intf )->SelectRenderer( ra->getItem() );
-    else
-        RendererManager::getInstance( p_intf )->SelectRenderer( NULL );
-}
 
 /*   CheckableListMenu   */
 
-CheckableListMenu::CheckableListMenu(QString title, QAbstractListModel* model , GroupingMode grouping,  QWidget *parent)
+CheckableListMenu::CheckableListMenu(QString title, QAbstractListModel* model , QActionGroup::ExclusionPolicy grouping,  QWidget *parent)
     : QMenu(parent)
     , m_model(model)
-    , m_grouping(grouping)
 {
     this->setTitle(title);
-    if (m_grouping != UNGROUPED)
-    {
-        m_actionGroup = new QActionGroup(this);
-        if (m_grouping == GROUPED_OPTIONAL)
-        {
-            m_actionGroup->setExclusionPolicy(QActionGroup::ExclusionPolicy::ExclusiveOptional);
-        }
-    }
-
-    connect(m_model, &QAbstractListModel::rowsAboutToBeRemoved, this, &CheckableListMenu::onRowsAboutToBeRemoved);
-    connect(m_model, &QAbstractListModel::rowsInserted, this, &CheckableListMenu::onRowInserted);
-    connect(m_model, &QAbstractListModel::dataChanged, this, &CheckableListMenu::onDataChanged);
-    connect(m_model, &QAbstractListModel::modelAboutToBeReset, this, &CheckableListMenu::onModelAboutToBeReset);
-    connect(m_model, &QAbstractListModel::modelReset, this, &CheckableListMenu::onModelReset);
-    onModelReset();
-}
-
-void CheckableListMenu::onRowsAboutToBeRemoved(const QModelIndex &, int first, int last)
-{
-    for (int i = last; i >= first; i--)
-    {
-        QAction* action = actions()[i];
-        if (m_actionGroup)
-            m_actionGroup->removeAction(action);
-        delete action;
-    }
-    if (actions().count() == 0)
-        setEnabled(false);
-}
-
-void CheckableListMenu::onRowInserted(const QModelIndex &, int first, int last)
-{
-    for (int i = first; i <= last; i++)
-    {
-        QModelIndex index = m_model->index(i);
-        QString title = m_model->data(index, Qt::DisplayRole).toString();
-        bool checked = m_model->data(index, Qt::CheckStateRole).toBool();
-
-        QAction *choiceAction = new QAction(title, this);
-        addAction(choiceAction);
-        if (m_actionGroup)
-            m_actionGroup->addAction(choiceAction);
-        connect(choiceAction, &QAction::triggered, [this, i](bool checked){
-            QModelIndex dataIndex = m_model->index(i);
-            m_model->setData(dataIndex, QVariant::fromValue<bool>(checked), Qt::CheckStateRole);
-        });
-        choiceAction->setCheckable(true);
-        choiceAction->setChecked(checked);
-        setEnabled(true);
-    }
-}
-
-void CheckableListMenu::onDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> & )
-{
-    for (int i = topLeft.row(); i <= bottomRight.row(); i++)
-    {
-        if (i >= actions().size())
-            break;
-        QAction *choiceAction = actions()[i];
-
-        QModelIndex index = m_model->index(i);
-        QString title = m_model->data(index, Qt::DisplayRole).toString();
-        bool checked = m_model->data(index, Qt::CheckStateRole).toBool();
-
-        choiceAction->setText(title);
-        choiceAction->setChecked(checked);
-    }
-}
-
-void CheckableListMenu::onModelAboutToBeReset()
-{
-    for (QAction* action  :actions())
-    {
-        if (m_actionGroup)
-            m_actionGroup->removeAction(action);
-        delete action;
-    }
-    setEnabled(false);
-}
 
-void CheckableListMenu::onModelReset()
-{
-    int nb_rows = m_model->rowCount();
-    if (nb_rows == 0)
-        setEnabled(false);
-    else
-        onRowInserted({}, 0, nb_rows - 1);
+    ListMenuHelper* helper = new ListMenuHelper(this, model, nullptr, this);
+    helper->getActionGroup()->setExclusionPolicy(grouping);
+    connect(helper, &ListMenuHelper::select, this, [this](int row, bool checked){
+        m_model->setData(m_model->index(row), checked, Qt::CheckStateRole);
+    });
 }
 
 // ListMenuHelper
@@ -323,6 +181,9 @@ ListMenuHelper::ListMenuHelper(QMenu * menu, QAbstractListModel * model, QAction
     connect(m_model, &QAbstractListModel::modelReset, this, &ListMenuHelper::onModelReset);
 }
 
+
+ListMenuHelper::~ListMenuHelper()
+{}
 // Interface
 
 int ListMenuHelper::count() const
@@ -330,6 +191,40 @@ int ListMenuHelper::count() const
     return m_actions.count();
 }
 
+QActionGroup* ListMenuHelper::getActionGroup() const
+{
+    return m_group;
+}
+
+
+void ListMenuHelper::setIcon(QAction* action,  const QUrl& iconUrl)
+{
+
+    if (!iconUrl.isValid())
+    {
+        action->setIcon({});
+        return;
+    }
+
+    if (m_iconLoader)
+    {
+        disconnect(m_iconLoader.get(), nullptr, this, nullptr);
+        m_iconLoader->cancel();
+    }
+    m_iconLoader.reset(VLCAccessImageProvider::requestImageResponseUnWrapped(iconUrl, {64,64}));
+    connect(m_iconLoader.get(), &QQuickImageResponse::finished, this, [this, action] {
+        std::unique_ptr<QQuickTextureFactory> factory(m_iconLoader->textureFactory());
+        if (!factory)
+        {
+            action->setIcon({});
+            return;
+        }
+        QImage img = factory->image();
+        action->setIcon(QIcon(QPixmap::fromImage(img)));
+    });
+}
+
+
 // Private slots
 
 void ListMenuHelper::onRowsInserted(const QModelIndex &, int first, int last)
@@ -349,11 +244,24 @@ void ListMenuHelper::onRowsInserted(const QModelIndex &, int first, int last)
 
         QAction * action = new QAction(name, this);
 
-        action->setCheckable(true);
+        QVariant checked = m_model->data(index, Qt::CheckStateRole);
+        if (checked.isValid() && checked.canConvert<bool>())
+        {
+            action->setCheckable(true);
+            action->setChecked(checked.toBool());
+        }
 
-        bool checked = m_model->data(index, Qt::CheckStateRole).toBool();
+        QVariant iconPath = m_model->data(index, Qt::DecorationRole);
+        if (iconPath.isValid())
+        {
+            QUrl iconUrl;
+            if (iconPath.canConvert<QUrl>())
+                iconUrl = iconPath.toUrl();
+            else if (iconPath.canConvert<QString>())
+                iconUrl = QUrl::fromEncoded(iconPath.toString().toUtf8());
 
-        action->setChecked(checked);
+            setIcon(action, iconUrl);
+        }
 
         // NOTE: We are adding sequentially *before* the next action in the list.
         m_menu->insertAction(before, action);
@@ -387,21 +295,44 @@ void ListMenuHelper::onRowsRemoved(const QModelIndex &, int first, int last)
 }
 
 void ListMenuHelper::onDataChanged(const QModelIndex & topLeft,
-                                   const QModelIndex & bottomRight, const QVector<int> &)
+                                   const QModelIndex & bottomRight, const QVector<int> & roles)
 {
+    const bool updateDisplay = roles.contains(Qt::DisplayRole);
+    const bool updateChecked = roles.contains(Qt::CheckStateRole);
+    const bool udpateIcon = roles.contains(Qt::DecorationRole);
+
     for (int i = topLeft.row(); i <= bottomRight.row(); i++)
     {
         QAction * action = m_actions.at(i);
-
         QModelIndex index = m_model->index(i, 0);
 
-        QString name = m_model->data(index, Qt::DisplayRole).toString();
-
-        action->setText(name);
+        if (updateDisplay)
+        {
+            QString name = m_model->data(index, Qt::DisplayRole).toString();
+            action->setText(name);
+        }
 
-        bool checked = m_model->data(index, Qt::CheckStateRole).toBool();
+        if (updateChecked)
+        {
+            QVariant checked = m_model->data(index, Qt::CheckStateRole);
+            if (checked.isValid() && checked.canConvert<bool>())
+                action->setChecked(checked.toBool());
+        }
 
-        action->setChecked(checked);
+        if (udpateIcon)
+        {
+            QVariant iconPath = m_model->data(index, Qt::DecorationRole);
+            if (iconPath.isValid())
+            {
+                QUrl iconUrl;
+                if (iconPath.canConvert<QUrl>())
+                    iconUrl = iconPath.toUrl();
+                else if (iconPath.canConvert<QString>())
+                    iconUrl = QUrl::fromEncoded(iconPath.toString().toUtf8());
+
+                setIcon(action, iconUrl);
+            }
+        }
     }
 }
 
@@ -422,11 +353,11 @@ void ListMenuHelper::onModelReset()
         onRowsInserted(QModelIndex(), 0, count - 1);
 }
 
-void ListMenuHelper::onTriggered(bool)
+void ListMenuHelper::onTriggered(bool checked)
 {
     QAction * action = static_cast<QAction *> (sender());
 
-    emit select(m_actions.indexOf(action));
+    emit select(m_actions.indexOf(action), checked);
 }
 
 /*     BooleanPropertyAction    */
@@ -465,83 +396,17 @@ RecentMenu::RecentMenu(MLRecentsModel* model, MediaLib* ml,  QWidget* parent)
     , m_model(model)
     , m_ml(ml)
 {
-    connect(m_model, &MLRecentsModel::rowsRemoved, this, &RecentMenu::onRowsRemoved);
-    connect(m_model, &MLRecentsModel::rowsInserted, this, &RecentMenu::onRowInserted);
-    connect(m_model, &MLRecentsModel::dataChanged, this, &RecentMenu::onDataChanged);
-    connect(m_model, &MLRecentsModel::modelReset, this, &RecentMenu::onModelReset);
-    m_separator = addSeparator();
-    addAction( qtr("&Clear"), m_model, &MLRecentsModel::clearHistory );
-    onModelReset();
-}
-
-void RecentMenu::onRowsRemoved(const QModelIndex&, int first, int last)
-{
-    for (int i = first; i <= last; i++)
-    {
-        delete m_actions.at(i);
-    }
-
-    QList<QAction *>::iterator begin = m_actions.begin();
-
-    m_actions.erase(begin + first, begin + last + 1);
-
-    if (m_actions.isEmpty())
-        setEnabled(false);
-}
-
-void RecentMenu::onRowInserted(const QModelIndex&, int first, int last)
-{
-    QAction * before;
-
-    if (first < m_actions.count())
-        before = m_actions.at(first);
-    else
-        // NOTE: In that case we insert *before* the 'Clear' separator.
-        before = m_separator;
+    QAction* separator = addSeparator();
 
-    for (int i = first; i <= last; i++)
-    {
-        QModelIndex index = m_model->index(i);
-        QString url = m_model->data(index, MLRecentsModel::RECENT_MEDIA_URL).toString();
+    ListMenuHelper* helper = new ListMenuHelper(this, model, separator, this);
+    connect(helper, &ListMenuHelper::select, this, [this](int row, bool){
+        QModelIndex index = m_model->index(row);
 
-        QAction *choiceAction = new QAction(url, this);
-
-        // NOTE: We are adding sequentially *before* the next action in the list.
-        insertAction(before, choiceAction);
-
-        m_actions.insert(i, choiceAction);
-
-        connect(choiceAction, &QAction::triggered, [this, choiceAction](){
-            QModelIndex index = m_model->index(m_actions.indexOf(choiceAction));
-
-            MLItemId id = m_model->data(index, MLRecentsModel::RECENT_MEDIA_ID).value<MLItemId>();
-            m_ml->addAndPlay(id);
-        });
-        setEnabled(true);
-    }
-}
-
-void RecentMenu::onDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector<int>& )
-{
-    for (int i = topLeft.row(); i <= bottomRight.row(); i++)
-    {
-        QModelIndex index = m_model->index(i);
-        QString title = m_model->data(index, MLRecentsModel::RECENT_MEDIA_URL).toString();
-
-        m_actions.at(i)->setText(title);
-    }
-}
-
-void RecentMenu::onModelReset()
-{
-    qDeleteAll(m_actions);
-    m_actions.clear();
+        MLItemId id = m_model->data(index, MLRecentsModel::RECENT_MEDIA_ID).value<MLItemId>();
+        m_ml->addAndPlay(id);
+    });
 
-    int nb_rows = m_model->rowCount();
-    if (nb_rows == 0 || nb_rows == -1)
-        setEnabled(false);
-    else
-        onRowInserted({}, 0, nb_rows - 1);
+    addAction( qtr("&Clear"), model, &MLRecentsModel::clearHistory );
 }
 
 // BookmarkMenu
@@ -560,7 +425,7 @@ BookmarkMenu::BookmarkMenu(MediaLib * mediaLib, vlc_player_t * player, QWidget *
 
     ListMenuHelper * helper = new ListMenuHelper(this, model, nullptr, this);
 
-    connect(helper, &ListMenuHelper::select, [model](int index)
+    connect(helper, &ListMenuHelper::select, [model](int index, bool )
     {
         model->select(model->index(index, 0));
     });


=====================================
modules/gui/qt/menus/custom_menus.hpp
=====================================
@@ -24,49 +24,11 @@
 #include "qt.hpp"
 
 #include <QMenu>
+#include <QActionGroup>
 #include "medialibrary/mlrecentsmodel.hpp"
 
 class QAbstractListModel;
 
-class RendererAction : public QAction
-{
-    Q_OBJECT
-
-    public:
-        RendererAction( vlc_renderer_item_t * );
-        ~RendererAction();
-        vlc_renderer_item_t *getItem();
-
-    private:
-        vlc_renderer_item_t *p_item;
-};
-
-class RendererMenu : public QMenu
-{
-    Q_OBJECT
-
-public:
-    RendererMenu( QMenu *, qt_intf_t * );
-    virtual ~RendererMenu();
-    void reset();
-
-private slots:
-    void addRendererItem( vlc_renderer_item_t * );
-    void removeRendererItem( vlc_renderer_item_t * );
-    void updateStatus( int );
-    void RendererSelected( QAction* );
-
-private:
-    void addRendererAction( QAction * );
-    void removeRendererAction( QAction * );
-    static vlc_renderer_item_t* getMatchingRenderer( const QVariant &,
-                                                     vlc_renderer_item_t* );
-    QAction *status;
-    QActionGroup *group;
-    qt_intf_t *p_intf;
-};
-
-
 /*
  * Construct a menu from a QAbstractListModel with Qt::DisplayRole and Qt::CheckStateRole
  */
@@ -74,12 +36,6 @@ class CheckableListMenu : public QMenu
 {
     Q_OBJECT
 public:
-    enum GroupingMode {
-        GROUPED_EXLUSIVE,
-        GROUPED_OPTIONAL,
-        UNGROUPED
-    };
-
     /**
      * @brief CheckableListMenu
      * @param title the title of the menu
@@ -87,21 +43,14 @@ public:
      * @param grouping whether the menu should use an ActionGroup or not
      * @param parent QObject parent
      */
-    CheckableListMenu(QString title, QAbstractListModel* model ,  GroupingMode grouping = UNGROUPED, QWidget *parent = nullptr);
-
-private slots:
-    void onRowsAboutToBeRemoved(const QModelIndex &parent, int first, int last);
-    void onRowInserted(const QModelIndex &parent, int first, int last);
-    void onDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles = QVector<int>());
-    void onModelAboutToBeReset();
-    void onModelReset();
+    CheckableListMenu(QString title, QAbstractListModel* model, QActionGroup::ExclusionPolicy grouping = QActionGroup::ExclusionPolicy::None, QWidget *parent = nullptr);
 
-private:
-    QAbstractListModel* m_model;
-    GroupingMode m_grouping;
-    QActionGroup* m_actionGroup = nullptr;
+protected:
+    QAbstractListModel* m_model = nullptr;
 };
 
+class QQuickImageResponse;
+
 // NOTE: This class is a helper to populate and maintain a QMenu from an QAbstractListModel.
 class ListMenuHelper : public QObject
 {
@@ -111,10 +60,13 @@ public:
     // NOTE: The model actions will be inserted before 'before' or at the end if it's NULL.
     ListMenuHelper(QMenu * menu, QAbstractListModel * model, QAction * before = nullptr,
                    QObject * parent = nullptr);
+    virtual ~ListMenuHelper();
 
 public: // Interface
     int count() const;
 
+    QActionGroup* getActionGroup() const;
+
 private slots:
     void onRowsInserted(const QModelIndex & parent, int first, int last);
     void onRowsRemoved (const QModelIndex & parent, int first, int last);
@@ -127,11 +79,13 @@ private slots:
     void onTriggered(bool checked);
 
 signals:
-    void select(int index);
+    void select(int index, bool checked);
 
     void countChanged(int count);
 
 private:
+    void setIcon(QAction* action,  const QUrl& iconUrl);
+
     QMenu * m_menu = nullptr;
 
     QActionGroup * m_group = nullptr;
@@ -141,6 +95,8 @@ private:
     QList<QAction *> m_actions;
 
     QAction * m_before = nullptr;
+
+    std::unique_ptr<QQuickImageResponse> m_iconLoader;
 };
 
 /**
@@ -166,24 +122,41 @@ private:
     QString m_propertyName;
 };
 
-class RecentMenu : public QMenu
+class RendererManager;
+class QProgressBar;
+class QLabel;
+class PlayerController;
+class RendererMenu : public QMenu
 {
     Q_OBJECT
+
 public:
-    RecentMenu(MLRecentsModel* model, MediaLib* ml, QWidget *parent = nullptr);
+    RendererMenu( QMenu* parent, qt_intf_t* intf, PlayerController* playerController );
+    virtual ~RendererMenu();
 
 private slots:
-    void onRowsRemoved(const QModelIndex &parent, int first, int last);
-    void onRowInserted(const QModelIndex &parent, int first, int last);
-    void onDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles = QVector<int>());
-    void onModelReset();
+    void updateStatus();
+
+protected:
+    QProgressBar* m_statusProgressBar;
+    QLabel* m_statusLabel;
+
+    QAction *m_statusAction;
+    QActionGroup *group;
+    qt_intf_t *p_intf;
+    RendererManager* m_renderManager = nullptr;
+};
+
+
+class RecentMenu : public QMenu
+{
+    Q_OBJECT
+public:
+    RecentMenu(MLRecentsModel* model, MediaLib* ml, QWidget *parent = nullptr);
 
 private:
     MLRecentsModel* m_model = nullptr;
-    QAction* m_separator = nullptr;
     MediaLib* m_ml = nullptr;
-
-    QList<QAction *> m_actions;
 };
 
 class BookmarkMenu : public QMenu


=====================================
modules/gui/qt/menus/menus.cpp
=====================================
@@ -359,7 +359,7 @@ void VLCMenuBar::ViewMenu(qt_intf_t *p_intf, QMenu *menu, std::optional<bool> pl
     action->setCheckable( true );
     action->setChecked( mi->hasGridView() );
 
-    menu->addMenu( new CheckableListMenu(qtr( "&Color Scheme" ), mi->getColorScheme(), CheckableListMenu::GROUPED_EXLUSIVE, menu) );
+    menu->addMenu( new CheckableListMenu(qtr( "&Color Scheme" ), mi->getColorScheme(), QActionGroup::ExclusionPolicy::Exclusive, menu) );
 
     menu->addSeparator();
 
@@ -377,7 +377,7 @@ void VLCMenuBar::InterfacesMenu( qt_intf_t *p_intf, QMenu *current )
 {
     assert(current);
     VLCVarChoiceModel* model = new VLCVarChoiceModel(VLC_OBJECT(p_intf->intf), "intf-add", current);
-    CheckableListMenu* submenu = new CheckableListMenu(qtr("Interfaces"), model, CheckableListMenu::UNGROUPED, current);
+    CheckableListMenu* submenu = new CheckableListMenu(qtr("Interfaces"), model, QActionGroup::ExclusionPolicy::None, current);
     current->addMenu(submenu);
 }
 
@@ -421,7 +421,7 @@ void VLCMenuBar::AudioMenu( qt_intf_t *p_intf, QMenu * current )
 {
     if( current->isEmpty() )
     {
-        current->addMenu(new CheckableListMenu(qtr( "Audio &Track" ), THEMIM->getAudioTracks(), CheckableListMenu::GROUPED_OPTIONAL, current));
+        current->addMenu(new CheckableListMenu(qtr( "Audio &Track" ), THEMIM->getAudioTracks(), QActionGroup::ExclusionPolicy::ExclusiveOptional, current));
 
         QAction *audioDeviceAction = new QAction( qtr( "&Audio Device" ), current );
         QMenu *audioDeviceSubmenu = new QMenu( current );
@@ -433,12 +433,12 @@ void VLCMenuBar::AudioMenu( qt_intf_t *p_intf, QMenu * current )
 
         VLCVarChoiceModel *mix_mode = THEMIM->getAudioMixMode();
         if (mix_mode->rowCount() == 0)
-            current->addMenu( new CheckableListMenu(qtr( "&Stereo Mode" ), THEMIM->getAudioStereoMode(), CheckableListMenu::GROUPED_EXLUSIVE, current) );
+            current->addMenu( new CheckableListMenu(qtr( "&Stereo Mode" ), THEMIM->getAudioStereoMode(), QActionGroup::ExclusionPolicy::Exclusive, current) );
         else
-            current->addMenu( new CheckableListMenu(qtr( "&Mix Mode" ), mix_mode, CheckableListMenu::GROUPED_EXLUSIVE, current) );
+            current->addMenu( new CheckableListMenu(qtr( "&Mix Mode" ), mix_mode, QActionGroup::ExclusionPolicy::Exclusive, current) );
         current->addSeparator();
 
-        current->addMenu( new CheckableListMenu(qtr( "&Visualizations" ), THEMIM->getAudioVisualizations(), CheckableListMenu::GROUPED_EXLUSIVE, current) );
+        current->addMenu( new CheckableListMenu(qtr( "&Visualizations" ), THEMIM->getAudioVisualizations(), QActionGroup::ExclusionPolicy::Exclusive, current) );
         VolumeEntries( p_intf, current );
     }
 }
@@ -450,7 +450,7 @@ void VLCMenuBar::SubtitleMenu( qt_intf_t *p_intf, QMenu *current, bool b_popup )
     {
         addDPStaticEntry( current, qtr( "Add &Subtitle File..." ), "",
                 &DialogsProvider::loadSubtitlesFile);
-        current->addMenu(new CheckableListMenu(qtr( "Sub &Track" ), THEMIM->getSubtitleTracks(), CheckableListMenu::GROUPED_OPTIONAL, current));
+        current->addMenu(new CheckableListMenu(qtr( "Sub &Track" ), THEMIM->getSubtitleTracks(), QActionGroup::ExclusionPolicy::ExclusiveOptional, current));
         current->addSeparator();
     }
 }
@@ -463,7 +463,7 @@ void VLCMenuBar::VideoMenu( qt_intf_t *p_intf, QMenu *current )
 {
     if( current->isEmpty() )
     {
-        current->addMenu(new CheckableListMenu(qtr( "Video &Track" ), THEMIM->getVideoTracks(), CheckableListMenu::GROUPED_OPTIONAL, current));
+        current->addMenu(new CheckableListMenu(qtr( "Video &Track" ), THEMIM->getVideoTracks(), QActionGroup::ExclusionPolicy::ExclusiveOptional, current));
 
         current->addSeparator();
         /* Surface modifiers */
@@ -476,15 +476,15 @@ void VLCMenuBar::VideoMenu( qt_intf_t *p_intf, QMenu *current )
 
         current->addSeparator();
         /* Size modifiers */
-        current->addMenu( new CheckableListMenu(qtr( "&Zoom" ), THEMIM->getZoom(), CheckableListMenu::GROUPED_EXLUSIVE, current) );
-        current->addMenu( new CheckableListMenu(qtr( "&Aspect Ratio" ), THEMIM->getAspectRatio(), CheckableListMenu::GROUPED_EXLUSIVE, current) );
-        current->addMenu( new CheckableListMenu(qtr( "&Crop" ), THEMIM->getCrop(), CheckableListMenu::GROUPED_EXLUSIVE, current) );
-        current->addMenu( new CheckableListMenu(qtr( "&Fit" ), THEMIM->getFit(), CheckableListMenu::GROUPED_EXLUSIVE, current) );
+        current->addMenu( new CheckableListMenu(qtr( "&Zoom" ), THEMIM->getZoom(), QActionGroup::ExclusionPolicy::Exclusive, current) );
+        current->addMenu( new CheckableListMenu(qtr( "&Aspect Ratio" ), THEMIM->getAspectRatio(), QActionGroup::ExclusionPolicy::Exclusive, current) );
+        current->addMenu( new CheckableListMenu(qtr( "&Crop" ), THEMIM->getCrop(), QActionGroup::ExclusionPolicy::Exclusive, current) );
+        current->addMenu( new CheckableListMenu(qtr( "&Fit" ), THEMIM->getFit(), QActionGroup::ExclusionPolicy::Exclusive, current) );
 
         current->addSeparator();
         /* Rendering modifiers */
-        current->addMenu( new CheckableListMenu(qtr( "&Deinterlace" ), THEMIM->getDeinterlace(), CheckableListMenu::GROUPED_EXLUSIVE, current) );
-        current->addMenu( new CheckableListMenu(qtr( "&Deinterlace mode" ), THEMIM->getDeinterlaceMode(), CheckableListMenu::GROUPED_EXLUSIVE, current) );
+        current->addMenu( new CheckableListMenu(qtr( "&Deinterlace" ), THEMIM->getDeinterlace(), QActionGroup::ExclusionPolicy::Exclusive, current) );
+        current->addMenu( new CheckableListMenu(qtr( "&Deinterlace mode" ), THEMIM->getDeinterlaceMode(), QActionGroup::ExclusionPolicy::Exclusive, current) );
 
         current->addSeparator();
         /* Other actions */
@@ -503,11 +503,11 @@ void VLCMenuBar::NavigMenu( qt_intf_t *p_intf, QMenu *menu )
     QAction *action;
     QMenu *submenu;
 
-    menu->addMenu( new CheckableListMenu( qtr( "T&itle" ), THEMIM->getTitles(), CheckableListMenu::GROUPED_EXLUSIVE, menu) );
-    submenu = new CheckableListMenu( qtr( "&Chapter" ), THEMIM->getChapters(), CheckableListMenu::GROUPED_EXLUSIVE, menu );
+    menu->addMenu( new CheckableListMenu( qtr( "T&itle" ), THEMIM->getTitles(), QActionGroup::ExclusionPolicy::Exclusive, menu) );
+    submenu = new CheckableListMenu( qtr( "&Chapter" ), THEMIM->getChapters(), QActionGroup::ExclusionPolicy::Exclusive, menu );
     submenu->setTearOffEnabled( true );
     menu->addMenu( submenu );
-    menu->addMenu( new CheckableListMenu( qtr("&Program") , THEMIM->getPrograms(), CheckableListMenu::GROUPED_EXLUSIVE, menu) );
+    menu->addMenu( new CheckableListMenu( qtr("&Program") , THEMIM->getPrograms(), QActionGroup::ExclusionPolicy::Exclusive, menu) );
 
     MainCtx * mi = p_intf->p_mi;
 
@@ -527,7 +527,7 @@ void VLCMenuBar::NavigMenu( qt_intf_t *p_intf, QMenu *menu )
     menu->addSeparator();
 
     if ( !VLCMenuBar::rendererMenu )
-        VLCMenuBar::rendererMenu = new RendererMenu( NULL, p_intf );
+        VLCMenuBar::rendererMenu = new RendererMenu( NULL, p_intf, p_intf->p_mainPlayerController );
 
     menu->addMenu( VLCMenuBar::rendererMenu );
     menu->addSeparator();
@@ -833,7 +833,7 @@ QMenu* VLCMenuBar::PopupMenu( qt_intf_t *p_intf, bool show )
             }
 
             VLCVarChoiceModel* skinmodel = new VLCVarChoiceModel(p_object, "intf-skins", submenu);
-            CheckableListMenu* skinsubmenu = new CheckableListMenu(qtr("Interface"), skinmodel, CheckableListMenu::GROUPED_OPTIONAL, submenu);
+            CheckableListMenu* skinsubmenu = new CheckableListMenu(qtr("Interface"), skinmodel, QActionGroup::ExclusionPolicy::ExclusiveOptional, submenu);
             submenu->addMenu(skinsubmenu);
 
             submenu->addSeparator();


=====================================
modules/gui/qt/menus/qml_menu_wrapper.cpp
=====================================
@@ -467,7 +467,7 @@ bool QmlMenuPositioner::eventFilter(QObject * object, QEvent * event)
 
     ListMenuHelper * helper = new ListMenuHelper(m_menu.get(), titles, sectionChapters, m_menu.get());
 
-    connect(helper, &ListMenuHelper::select, [titles](int index)
+    connect(helper, &ListMenuHelper::select, [titles](int index, bool)
     {
         titles->setData(titles->index(index), true, Qt::CheckStateRole);
     });
@@ -486,7 +486,7 @@ bool QmlMenuPositioner::eventFilter(QObject * object, QEvent * event)
 
     helper = new ListMenuHelper(m_menu.get(), chapters, sectionBookmarks, m_menu.get());
 
-    connect(helper, &ListMenuHelper::select, [chapters](int index)
+    connect(helper, &ListMenuHelper::select, [chapters](int index, bool)
     {
         chapters->setData(chapters->index(index), true, Qt::CheckStateRole);
     });
@@ -511,7 +511,7 @@ bool QmlMenuPositioner::eventFilter(QObject * object, QEvent * event)
 
         helper = new ListMenuHelper(m_menu.get(), bookmarks, nullptr, m_menu.get());
 
-        connect(helper, &ListMenuHelper::select, [bookmarks](int index)
+        connect(helper, &ListMenuHelper::select, [bookmarks](int index, bool)
         {
             bookmarks->select(bookmarks->index(index, 0));
         });
@@ -551,7 +551,7 @@ void QmlBookmarkMenu::close()
 
     ListMenuHelper * helper = new ListMenuHelper(m_menu.get(), programs, nullptr, m_menu.get());
 
-    connect(helper, &ListMenuHelper::select, [programs](int index)
+    connect(helper, &ListMenuHelper::select, [programs](int index, bool)
     {
         programs->setData(programs->index(index), true, Qt::CheckStateRole);
     });
@@ -578,7 +578,7 @@ void QmlProgramMenu::close()
     if (m_ctx == nullptr)
         return;
 
-    m_menu = std::make_unique<RendererMenu>(nullptr, m_ctx->getIntf());
+    m_menu = std::make_unique<RendererMenu>(nullptr, m_ctx->getIntf(), m_ctx->getIntf()->p_mainPlayerController);
 
     connect(m_menu.get(), &QMenu::aboutToHide, this, &QmlRendererMenu::aboutToHide);
     connect(m_menu.get(), &QMenu::aboutToShow, this, &QmlRendererMenu::aboutToShow);


=====================================
modules/gui/qt/meson.build
=====================================
@@ -1066,18 +1066,18 @@ if qt6_dep.found()
         'link_with' : qmllibs,
     }
 
-    test_qt6_dep = dependency('qt6', version: '=' + qt6_dep.version(), modules: ['QuickTest'], required: false)
-    if test_qt6_dep.found()
+    quicktest_qt6_dep = dependency('qt6', version: '=' + qt6_dep.version(), modules: ['QuickTest'], required: false)
+    if quicktest_qt6_dep.found()
         qml_test_moc = qt6.compile_moc(
             headers : files('tests/qml_test.hpp'),
-            dependencies : [test_qt6_dep, qt6_dep]
+            dependencies : [quicktest_qt6_dep, qt6_dep]
         )
         qml_test = executable(
           'qml_test',
           [files('tests/qml_test.cpp', 'tests/qml_test.hpp'), qml_test_moc],
           qt6pre_qrc,
           build_by_default: false,
-          dependencies: [test_qt6_dep, qt6_dep],
+          dependencies: [quicktest_qt6_dep, qt6_dep],
           link_with: qmllibs,
           cpp_args: ['-DQUICK_TEST_SOURCE_DIR="' + meson.current_source_dir() + '/tests"']
         )
@@ -1101,4 +1101,26 @@ if qt6_dep.found()
         'link_with': [libvlccore],
         'dependencies': [qt6_dep, qt_extra_deps],
     }
+
+    qtest_qt6_dep = dependency('qt6', version: '=' + qt6_dep.version(), modules: ['Test'], required: false)
+    if qtest_qt6_dep.found()
+        vlc_tests += {
+            'name': 'test_qt_renderer_manager',
+            'sources': files(
+                'tests/test_renderer_manager_model.cpp',
+                'util/renderer_manager.hpp',
+                'util/renderer_manager.cpp'
+            ),
+            'moc_sources': files(
+                'tests/test_renderer_manager_model.cpp'
+            ),
+            'moc_headers': files(
+                'util/renderer_manager.hpp'
+            ),
+            'suite': ['qt'],
+            'include_directories' : qt_include_dir,
+            'link_with': [libvlccore, libvlc],
+            'dependencies': [qt6_dep, qt_extra_deps, qtest_qt6_dep],
+        }
+    endif
 endif


=====================================
modules/gui/qt/player/player_controller.cpp
=====================================
@@ -1125,6 +1125,7 @@ PlayerControllerPrivate::PlayerControllerPrivate(PlayerController *playercontrol
     , m_audioMixMode((audio_output_t*)nullptr, "mix-mode")
     , m_audioDeviceList(m_player)
     , m_audioVisualization((audio_output_t*)nullptr, "visual")
+    , m_rendererManager(p_intf, m_player)
 {
     {
         vlc_player_locker locker{m_player};
@@ -2067,7 +2068,7 @@ QABSTRACTLIST_GETTER( VLCVarChoiceModel, getDeinterlaceMode, m_deinterlaceMode)
 QABSTRACTLIST_GETTER( VLCVarChoiceModel, getAudioStereoMode, m_audioStereoMode)
 QABSTRACTLIST_GETTER( VLCVarChoiceModel, getAudioMixMode, m_audioMixMode)
 QABSTRACTLIST_GETTER( VLCVarChoiceModel, getAudioVisualizations, m_audioVisualization)
-
+QABSTRACTLIST_GETTER( RendererManager, getRendererManager, m_rendererManager)
 
 #undef QABSTRACTLIST_GETTER
 


=====================================
modules/gui/qt/player/player_controller.hpp
=====================================
@@ -34,12 +34,14 @@
 #include "util/varchoicemodel.hpp"
 #include "util/vlctick.hpp"
 
+Q_MOC_INCLUDE("util/renderer_manager.hpp")
 
 using vlc_player_locker = vlc_locker<vlc_player_t, vlc_player_Lock, vlc_player_Unlock>;
 
 using SharedVOutThread = vlc_shared_data_ptr_type(vout_thread_t, vout_Hold, vout_Release);
 using SharedAOut = vlc_shared_data_ptr_type(audio_output_t, aout_Hold, aout_Release);
 
+class RendererManager;
 class QSignalMapper;
 
 class IMEvent : public QEvent
@@ -191,6 +193,7 @@ public:
     Q_PROPERTY(VLCTick ABLoopA READ getABLoopA NOTIFY ABLoopAChanged FINAL)
     Q_PROPERTY(VLCTick ABLoopB READ getABLoopB NOTIFY ABLoopBChanged FINAL)
     Q_PROPERTY(bool recording READ isRecording WRITE setRecording NOTIFY recordingChanged FINAL)
+    Q_PROPERTY(RendererManager* rendererManager READ getRendererManager CONSTANT FINAL)
 
     // High resolution time fed by SMPTE Timer
     Q_PROPERTY(QString highResolutionTime READ highResolutionTime NOTIFY highResolutionTimeChanged FINAL)
@@ -394,6 +397,9 @@ public slots:
     QString getAlbum() const;
     QUrl getArtwork() const;
 
+    //Renderer
+    RendererManager* getRendererManager();
+
 signals:
     //playback
     void playingStateChanged( PlayingState state );


=====================================
modules/gui/qt/player/player_controller_p.hpp
=====================================
@@ -24,6 +24,7 @@
 #include "input_models.hpp"
 #include "util/varchoicemodel.hpp"
 #include "util/shared_input_item.hpp"
+#include "util/renderer_manager.hpp"
 
 #include <QTimer>
 #include <QUrl>
@@ -173,6 +174,8 @@ public:
     QString         m_artUrl;
     struct input_stats_t m_stats;
 
+    RendererManager m_rendererManager;
+
     // meta
     QString m_title;
     QString m_artist;


=====================================
modules/gui/qt/tests/test_renderer_manager_model.cpp
=====================================
@@ -0,0 +1,492 @@
+/*****************************************************************************
+ * Copyright (C) 2024 VLC authors and VideoLAN
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+#ifdef HAVE_CONFIG_H
+# include <config.h>
+#endif
+
+/* Define a builtin module for mocked parts */
+#define MODULE_NAME renderer_manager_test
+#undef VLC_DYNAMIC_PLUGIN
+
+#include "../../../../test/libvlc/test.h"
+
+#include <vlc/vlc.h>
+
+#include <vlc_common.h>
+#include <vlc_plugin.h>
+#include <vlc_playlist.h>
+#include <vlc_services_discovery.h>
+#include <vlc_renderer_discovery.h>
+#include <vlc_probe.h>
+#include <vlc_interface.h>
+#include <vlc_player.h>
+
+#include "../../../../lib/libvlc_internal.h"
+
+#include "qt.hpp"
+
+namespace vlc {
+class Compositor {};
+}
+
+static vlc_renderer_discovery_t* g_rd = nullptr;
+static bool g_rd_probe_enabled = false;
+
+static int OpenRD( vlc_object_t* p_this )
+{
+    g_rd = (vlc_renderer_discovery_t *)p_this;
+    return VLC_SUCCESS;
+}
+
+static void CloseRD( vlc_object_t* )
+{
+    g_rd = nullptr;
+
+}
+
+static int vlc_rd_probe_open(vlc_object_t *obj) {
+    auto probe = (struct vlc_probe_t *)obj;
+
+    if (g_rd_probe_enabled)
+        vlc_rd_probe_add(probe, "rd", "a fake renderer for testing purpose");
+    //only probe ourself
+    return VLC_PROBE_STOP;
+}
+
+static qt_intf_t* g_intf = nullptr;
+
+static int OpenIntf(vlc_object_t* p_this)
+{
+    auto intfThread = reinterpret_cast<intf_thread_t*>(p_this);
+    libvlc_int_t* libvlc = vlc_object_instance( p_this );
+
+    /* Ensure initialization of objects in qt_intf_t. */
+    g_intf = vlc_object_create<qt_intf_t>( libvlc );
+    if (!g_intf)
+        return VLC_ENOMEM;
+
+    g_intf->obj.logger = vlc_LogHeaderCreate(libvlc->obj.logger, "qt");
+    if (!g_intf->obj.logger)
+    {
+        vlc_object_delete(g_intf);
+        return VLC_EGENERIC;
+    }
+    g_intf->intf = intfThread;
+    intfThread->p_sys = reinterpret_cast<intf_sys_t*>(g_intf);
+    return VLC_SUCCESS;
+}
+
+static void CloseIntf( vlc_object_t *p_this )
+{
+    intf_thread_t* intfThread = (intf_thread_t*)(p_this);
+    auto p_intf = reinterpret_cast<qt_intf_t*>(intfThread->p_sys);
+    if (!p_intf)
+        return;
+    vlc_LogDestroy(p_intf->obj.logger);
+    vlc_object_delete(p_intf);
+}
+
+vlc_module_begin()
+    set_callbacks(OpenIntf, CloseIntf)
+    set_capability("interface", 0)
+add_submodule()
+    set_capability("renderer_discovery", 0)
+    add_shortcut("rd")
+    set_callbacks(OpenRD, CloseRD)
+add_submodule()
+    set_capability("renderer probe", 10000)
+    set_callback(vlc_rd_probe_open)
+vlc_module_end()
+
+extern "C" {
+
+const char vlc_module_name[] = MODULE_STRING;
+VLC_EXPORT vlc_plugin_cb vlc_static_modules[] = {
+    VLC_SYMBOL(vlc_entry),
+    NULL
+};
+}
+
+#include <QTest>
+#include <QAbstractItemModelTester>
+#include "../util/renderer_manager.hpp"
+#include <vlc_cxx_helpers.hpp>
+
+using RendererItemPtr = vlc_shared_data_ptr_type(vlc_renderer_item_t,
+    vlc_renderer_item_hold,
+    vlc_renderer_item_release);
+
+
+class TestClass : public QObject
+{
+    Q_OBJECT
+
+private:
+    RendererItemPtr pushDummyRDItem(int i) {
+        QString name = QString("name%1").arg(i);
+        QString sout = QString("dummy://%1.%1.%1.%1:%1").arg(i);
+        RendererItemPtr item(vlc_renderer_item_new(
+                "type", qtu(name), qtu(sout), "extra sout",
+                nullptr, "icon://", i ));
+        vlc_rd_add_item( g_rd, item.get() );
+        return item;
+    }
+
+    void checkDummyRDItem(int row, int id = -1) {
+        if (id == -1)
+            id = row;
+        QModelIndex idx = m_model->index(row, 0);
+        QCOMPARE(m_model->data(idx, RendererManager::TYPE), "type");
+        QCOMPARE(m_model->data(idx, RendererManager::NAME), QString("name%1").arg(id));
+        QCOMPARE(m_model->data(idx, RendererManager::SOUT), QString("dummy{ip=%1.%1.%1.%1,port=%1,extra sout}").arg(id));
+        QCOMPARE(m_model->data(idx, RendererManager::ICON_URI), QString("icon://"));
+        QCOMPARE(m_model->data(idx, RendererManager::FLAGS), id);
+    }
+
+private slots:
+    void initTestCase() {
+        test_init();
+
+        m_vlc = libvlc_new(test_defaults_nargs, test_defaults_args);
+        libvlc_InternalAddIntf(m_vlc->p_libvlc_int, MODULE_STRING);
+        libvlc_InternalPlay(m_vlc->p_libvlc_int);
+
+        m_playlist = vlc_intf_GetMainPlaylist(g_intf->intf);
+        m_player = vlc_playlist_GetPlayer( m_playlist );
+    }
+
+    void cleanupTestCase() {
+        libvlc_release(m_vlc);
+    }
+
+    void init() {
+        g_rd_probe_enabled = true;
+        m_model = new RendererManager(g_intf, m_player);
+        //QAbstractItemModelTester checks that QAbstractItemModel events are coherents
+        m_modelTester = new QAbstractItemModelTester(m_model);
+        QVERIFY(g_rd == nullptr);
+    }
+
+    void cleanup() {
+        delete m_modelTester;
+        delete m_model;
+        QVERIFY(g_rd == nullptr);
+    }
+
+    void testEmpty() {
+        QVERIFY(g_rd == nullptr);
+        //model is empty before scan
+        QCOMPARE(m_model->rowCount(), 0);
+        m_model->StartScan();
+        QVERIFY(g_rd != nullptr);
+        QCOMPARE(m_model->rowCount(), 0);
+        QCOMPARE(m_model->getStatus(), RendererManager::RUNNING);
+        //scan didn't find anything
+        m_model->StopScan();
+        QVERIFY(g_rd == nullptr);
+        QCOMPARE(m_model->rowCount(), 0);
+        QCOMPARE(m_model->getStatus(), RendererManager::IDLE);
+    }
+
+    void testStatic() {
+        QCOMPARE(m_model->rowCount(), 0);
+        m_model->StartScan();
+        QCOMPARE(m_model->rowCount(), 0);
+        QCOMPARE(m_model->getStatus(), RendererManager::RUNNING);
+
+        QVERIFY(g_rd != nullptr);
+
+        for (int i = 0; i < 5; ++i) {
+            pushDummyRDItem(i);
+            QCOMPARE(m_model->rowCount(), i+1);
+            checkDummyRDItem(i);
+        }
+        m_model->StopScan();
+
+        //items are still present
+        QCOMPARE(m_model->rowCount(), 5);
+        QCOMPARE(m_model->getStatus(), RendererManager::IDLE);
+
+        for (int i = 0; i < 5; ++i) {
+            checkDummyRDItem(i);
+        }
+
+        //module is closed
+        QVERIFY(g_rd == nullptr);
+    }
+
+    void testTwoPassesIdentical() {
+        m_model->StartScan();
+        QVERIFY(g_rd != nullptr);
+        QCOMPARE(m_model->rowCount(), 0);
+        for (int i = 0; i < 5; ++i) {
+            pushDummyRDItem(i);
+        }
+        QCOMPARE(m_model->rowCount(), 5);
+        m_model->StopScan();
+        QCOMPARE(m_model->rowCount(), 5);
+
+        //second pass will yield the same items
+        m_model->StartScan();
+        QCOMPARE(m_model->rowCount(), 5);
+        for (int i = 0; i < 5; ++i) {
+            pushDummyRDItem(i);
+        }
+        QCOMPARE(m_model->rowCount(), 5);
+        m_model->StopScan();
+        QCOMPARE(m_model->rowCount(), 5);
+        QCOMPARE(m_model->getStatus(), RendererManager::IDLE);
+
+        for (int i = 0; i < 5; ++i) {
+            checkDummyRDItem(i);
+        }
+    }
+
+    void testTwoPassesDifferent() {
+        m_model->StartScan();
+        for (int i = 0; i < 5; ++i) {
+            pushDummyRDItem(i);
+        }
+        m_model->StopScan();
+
+        //second pass will yield the same items
+        m_model->StartScan();
+        QCOMPARE(m_model->rowCount(), 5);
+        for (int i = 0; i < 3; ++i) {
+            pushDummyRDItem(i);
+        }
+        QCOMPARE(m_model->rowCount(), 5);
+        m_model->StopScan();
+        //items from former passes have been removed
+        QCOMPARE(m_model->rowCount(), 3);
+        QCOMPARE(m_model->getStatus(), RendererManager::IDLE);
+        for (int i = 0; i < 3; ++i) {
+            checkDummyRDItem(i);
+        }
+    }
+
+    void testRemovedItem() {
+        m_model->StartScan();
+        for (int i = 0; i < 3; ++i) {
+            pushDummyRDItem(i);
+        }
+        auto item = pushDummyRDItem(3);
+        for (int i = 4; i <= 6; ++i) {
+            pushDummyRDItem(i);
+        }
+        QCOMPARE(m_model->rowCount(), 7);
+        vlc_rd_remove_item(g_rd, item.get());
+        QCOMPARE(m_model->rowCount(), 6);
+        m_model->StopScan();
+        QCOMPARE(m_model->rowCount(), 6);
+
+        for (int i = 0; i < 3; ++i) {
+            checkDummyRDItem(i, i);
+        }
+        for (int i = 3; i < 6; ++i) {
+            checkDummyRDItem(i, i+1);
+        }
+    }
+
+    void testRendererSelection() {
+        m_model->StartScan();
+
+        auto r0 = pushDummyRDItem(0);
+        auto r1 = pushDummyRDItem(1);
+        auto r2 = pushDummyRDItem(2);
+        auto r3 = pushDummyRDItem(3);
+        auto r4 = pushDummyRDItem(4);
+
+        //item are not selected by default
+        QCOMPARE(m_model->useRenderer(), false);
+        for (int i = 0; i < 5; ++i) {
+            QModelIndex idx = m_model->index(i, 0);
+            QCOMPARE(m_model->data(idx, RendererManager::SELECTED), false);
+        }
+
+        //select a first renderer
+        QModelIndex selidx = m_model->index(3);
+        m_model->setData(selidx, true, RendererManager::SELECTED);
+        QCOMPARE(m_model->useRenderer(), true);
+        for (int i = 0; i < 5; ++i) {
+            QModelIndex idx = m_model->index(i, 0);
+            if (i == 3)
+                QCOMPARE(m_model->data(idx, RendererManager::SELECTED), true);
+            else
+                QCOMPARE(m_model->data(idx, RendererManager::SELECTED), false);
+        }
+
+        //player renderer is actually set
+        {
+            vlc_player_Lock(m_player);
+            QCOMPARE(vlc_player_GetRenderer(m_player), r3.get());
+            vlc_player_Unlock(m_player);
+        }
+
+        //select another renderer
+        selidx = m_model->index(2);
+        m_model->setData(selidx, true, RendererManager::SELECTED);
+        QCOMPARE(m_model->useRenderer(), true);
+        for (int i = 0; i < 5; ++i) {
+            QModelIndex idx = m_model->index(i, 0);
+            if (i == 2)
+                QCOMPARE(m_model->data(idx, RendererManager::SELECTED), true);
+            else
+                QCOMPARE(m_model->data(idx, RendererManager::SELECTED), false);
+        }
+
+        //player renderer has actually changed
+        {
+            vlc_player_Lock(m_player);
+            QCOMPARE(vlc_player_GetRenderer(m_player), r2.get());
+            vlc_player_Unlock(m_player);
+        }
+
+        //deselect renderer from index
+        m_model->setData(selidx, false, RendererManager::SELECTED);
+        QCOMPARE(m_model->useRenderer(), false);
+        for (int i = 0; i < 5; ++i) {
+            QModelIndex idx = m_model->index(i, 0);
+            QCOMPARE(m_model->data(idx, RendererManager::SELECTED), false);
+        }
+
+        //player renderer is actually unset
+        {
+            vlc_player_Lock(m_player);
+            QCOMPARE(vlc_player_GetRenderer(m_player), nullptr);
+            vlc_player_Unlock(m_player);
+        }
+
+        //re-select renderer then deselect it using disableRenderer
+        m_model->setData(selidx, true, RendererManager::SELECTED);
+        m_model->disableRenderer();
+        QCOMPARE(m_model->useRenderer(), false);
+        for (int i = 0; i < 5; ++i) {
+            QModelIndex idx = m_model->index(i, 0);
+            QCOMPARE(m_model->data(idx, RendererManager::SELECTED), false);
+        }
+
+
+        //make the player select the renderer itself
+        {
+            vlc_player_Lock(m_player);
+            vlc_player_SetRenderer(m_player, r4.get());
+            vlc_player_Unlock(m_player);
+        }
+        QCOMPARE(m_model->useRenderer(), true);
+        for (int i = 0; i < 5; ++i) {
+            QModelIndex idx = m_model->index(i, 0);
+            if (i == 4)
+                QCOMPARE(m_model->data(idx, RendererManager::SELECTED), true);
+            else
+                QCOMPARE(m_model->data(idx, RendererManager::SELECTED), false);
+        }
+        //the  make the player unselect the renderer
+        {
+            vlc_player_Lock(m_player);
+            vlc_player_SetRenderer(m_player, nullptr);
+            vlc_player_Unlock(m_player);
+        }
+        QCOMPARE(m_model->useRenderer(), false);
+        for (int i = 0; i < 5; ++i) {
+            QModelIndex idx = m_model->index(i, 0);
+            QCOMPARE(m_model->data(idx, RendererManager::SELECTED), false);
+        }
+    }
+
+    void testSelectionLost() {
+        m_model->StartScan();
+
+        auto r0 = pushDummyRDItem(0);
+        auto r1 = pushDummyRDItem(1);
+        auto r2 = pushDummyRDItem(2);
+        auto r3 = pushDummyRDItem(3);
+        auto r4 = pushDummyRDItem(4);
+
+        QModelIndex selidx = m_model->index(3);
+        m_model->setData(selidx, true, RendererManager::SELECTED);
+
+        //selected item is removed
+        QCOMPARE(m_model->rowCount(), 5);
+        vlc_rd_remove_item( g_rd, r3.get() );
+
+        //item is held by model
+        QCOMPARE(m_model->useRenderer(), true);
+        QCOMPARE(m_model->rowCount(), 5);
+
+        //disable the renderer
+        m_model->setData(selidx, false, RendererManager::SELECTED);
+        QCOMPARE(m_model->useRenderer(), false);
+        //item is kept by model until next scan
+        QCOMPARE(m_model->rowCount(), 5);
+        m_model->StopScan();
+        QCOMPARE(m_model->rowCount(), 5);
+
+        //item should be gone after next scan
+        m_model->StartScan();
+        vlc_rd_add_item( g_rd, r0.get() );
+        vlc_rd_add_item( g_rd, r1.get() );
+        vlc_rd_add_item( g_rd, r2.get() );
+        //no r3
+        vlc_rd_add_item( g_rd, r4.get() );
+        m_model->StopScan();
+        QCOMPARE(m_model->rowCount(), 4);
+
+        //not finding the selected item again should keep it in the model
+        selidx = m_model->index(2);
+        m_model->setData(selidx, true, RendererManager::SELECTED);
+        m_model->StartScan();
+        vlc_rd_add_item( g_rd, r0.get() );
+        vlc_rd_add_item( g_rd, r1.get() );
+        m_model->StopScan();
+        QCOMPARE(m_model->rowCount(), 3);
+        QCOMPARE(m_model->data(selidx, RendererManager::SELECTED), true);
+        QCOMPARE(m_model->useRenderer(), true);
+        m_model->StartScan();
+        m_model->StopScan();
+        selidx = m_model->index(0);
+        QCOMPARE(m_model->rowCount(), 1);
+        QCOMPARE(m_model->data(selidx, RendererManager::SELECTED), true);
+        QCOMPARE(m_model->useRenderer(), true);
+    }
+
+    //deleting the model without stopping the scan
+    void testNoStopScanDeletion() {
+        m_model->StartScan();
+    }
+
+    //failed state when no renderer manager is found
+    void testNoProbes() {
+        g_rd_probe_enabled = false;
+        QCOMPARE(m_model->getStatus(), RendererManager::IDLE);
+        m_model->StartScan();
+        QCOMPARE(m_model->getStatus(), RendererManager::FAILED);
+        QCOMPARE(m_model->rowCount(), 0);
+        QVERIFY(g_rd == nullptr);
+        g_rd_probe_enabled = true;
+    }
+private:
+    libvlc_instance_t* m_vlc = nullptr;
+    vlc_playlist_t* m_playlist = nullptr;
+    vlc_player_t* m_player = nullptr;
+    QAbstractItemModelTester* m_modelTester = nullptr;
+    RendererManager* m_model;
+};
+
+QTEST_GUILESS_MAIN(TestClass)
+#include "test_renderer_manager_model.moc"


=====================================
modules/gui/qt/util/renderer_manager.cpp
=====================================
@@ -1,108 +1,412 @@
+/*****************************************************************************
+ * Copyright (C) 2024 VLC authors and VideoLAN
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
 #ifdef HAVE_CONFIG_H
 # include "config.h"
 #endif
 
 #include "renderer_manager.hpp"
-#include "player/player_controller.hpp"
 
 #include <QApplication>
+#include <QTimer>
+#include <QVector>
+#include <QHash>
 
 #include <vlc_common.h>
 #include <vlc_renderer_discovery.h>
 #include <vlc_player.h>
+#include <vlc_cxx_helpers.hpp>
 
-const QEvent::Type RendererManagerEvent::AddedEvent =
-        (QEvent::Type)QEvent::registerEventType();
-const QEvent::Type RendererManagerEvent::RemovedEvent =
-        (QEvent::Type)QEvent::registerEventType();
+static void player_renderer_changed(vlc_player_t *, vlc_renderer_item_t *new_item, void *data);
 
-RendererManagerEvent::RendererManagerEvent(Type type, vlc_renderer_item_t* p_item_)
-    : QEvent( type ), p_item( p_item_ )
-{
-    vlc_renderer_item_hold( p_item );
-}
+using RendererItemPtr = vlc_shared_data_ptr_type(vlc_renderer_item_t, vlc_renderer_item_hold, vlc_renderer_item_release);
+using vlc_player_locker = vlc_locker<vlc_player_t, vlc_player_Lock, vlc_player_Unlock>;
 
-RendererManagerEvent::~RendererManagerEvent()
+struct ItemEntry
 {
-    vlc_renderer_item_release( p_item );
-}
+    ItemEntry(const RendererItemPtr& renderItem, bool current = true)
+        : renderItem(renderItem)
+        , currentScan(current)
+    {}
 
+    ItemEntry(){}
 
-RendererManager::RendererManager( qt_intf_t *p_intf_ ) :
-    p_intf( p_intf_ ), p_selected_item( NULL )
-{
-    connect( &m_stop_scan_timer, &QTimer::timeout, this, &RendererManager::RendererMenuCountdown );
-}
+    RendererItemPtr renderItem;
+    /**
+     * we scan for 20 seconds, when the scan ends render discoverer are destroyed but
+     * items are kept in the model, at this moment the items are marked as "old", if
+     * a new scan starts and finish without finding them again, then they will be removed
+     * from the model
+     */
+    bool currentScan = true;
+};
 
-RendererManager::~RendererManager()
+struct RendererManagerPrivate
 {
-    StopScan();
-    foreach( ItemEntry entry, m_items )
+    RendererManagerPrivate(RendererManager* pub, qt_intf_t* const intf, vlc_player_t* const player)
+        : p_intf(intf)
+        , m_player(player)
+        , q_ptr(pub)
+    {
+        assert(m_player);
+        QObject::connect( &m_stop_scan_timer, &QTimer::timeout, q_ptr, [this](){ timerCountdown(); });
+
+        static struct vlc_player_cbs cbs = {};
+        cbs.on_renderer_changed = &player_renderer_changed;
+        {
+            vlc_player_locker lock{ m_player };
+            m_playerListener = vlc_player_AddListener(m_player, &cbs, this);
+        }
+    }
+
+    ~RendererManagerPrivate()
     {
-        emit rendererItemRemoved( entry.second );
-        vlc_renderer_item_release( entry.second );
+        if (m_playerListener)
+        {
+            vlc_player_locker lock{ m_player };
+            vlc_player_RemoveListener(m_player, m_playerListener);
+        }
     }
-}
 
-void RendererManager::customEvent( QEvent *event )
-{
-    if( event->type() == RendererManagerEvent::AddedEvent ||
-        event->type() == RendererManagerEvent::RemovedEvent )
+    void timerCountdown()
+    {
+        Q_Q(RendererManager);
+        if( m_stop_scan_timer.isActive() && m_scan_remain > 0 )
+        {
+            m_scan_remain--;
+            emit q->scanRemainChanged();
+        }
+        else
+        {
+            q->StopScan();
+        }
+    }
+
+    void setStatus(RendererManager::RendererStatus status)
     {
-        RendererManagerEvent *ev = static_cast<RendererManagerEvent *>(event);
-        QString souturi( vlc_renderer_item_sout( ev->getItem() ) );
-        vlc_renderer_item_t *p_item = ev->getItem();
+        Q_Q(RendererManager);
+        if (status == m_status)
+            return;
+        m_status = status;
+        emit q->statusChanged();
+    }
+
 
-        if( event->type() == RendererManagerEvent::AddedEvent )
+    void onRendererAdded(const RendererItemPtr& renderItem) {
+        Q_Q(RendererManager);
+        QString key = getItemKey(renderItem);
+        if( !m_items.contains( key ) )
         {
-            if( !m_items.contains( souturi ) )
+            emit q->beginInsertRows({}, m_itemsIndex.size(), m_itemsIndex.size());
+            m_items.emplace( key, renderItem );
+            m_itemsIndex.push_back(key);
+            emit q->endInsertRows();
+        }
+        else
+        {
+            /* mark the item as seen during this scan */
+            ItemEntry& entry = m_items[key];
+            entry.currentScan = true;
+        }
+    }
+
+    void onRendererRemoved(const RendererItemPtr& renderItem) {
+        Q_Q(RendererManager);
+
+        QString key = getItemKey(renderItem);
+        if( m_items.contains( key ) )
+        {
+            //only remove the item if we are using it
+            //and if we are actively scanning
+            if( m_selectedItem != renderItem && m_status == RendererManager::RUNNING )
             {
-                vlc_renderer_item_hold( p_item );
-                ItemEntry entry( true, p_item );
-                m_items.insert( souturi, entry );
-                emit rendererItemAdded( p_item );
+                qsizetype index = m_itemsIndex.indexOf(key);
+                emit q->beginRemoveRows({}, index, index);
+                m_items.remove(key);
+                m_itemsIndex.remove(index);
+                emit q->endRemoveRows();
             }
-            else /* incref for now */
+        }
+    }
+
+    void onPlayerRendererChanged(const RendererItemPtr& renderItem)
+    {
+        Q_Q(RendererManager);
+
+        if (m_selectedItem == renderItem)
+            return;
+
+        if (m_selectedItem)
+        {
+            int oldIndexRow = getItemRow(m_selectedItem);
+            m_selectedItem.reset();
+            if (oldIndexRow >= 0)
             {
-                ItemEntry &entry = m_items[ souturi ];
-                entry.first = true; /* to be kept */
+                auto oldIndex = q->index(oldIndexRow);
+                emit q->dataChanged(oldIndex, oldIndex, { Qt::CheckStateRole, RendererManager::SELECTED });
             }
         }
-        else /* remove event */
+
+        if (renderItem)
         {
-            if( m_items.contains( souturi ) )
+            m_selectedItem = renderItem;
+
+            int newIndexRow= getItemRow(m_selectedItem);
+            if (newIndexRow >= 0)
+            {
+                auto newIndex = q->index(newIndexRow);
+                emit q->dataChanged(newIndex, newIndex, { Qt::CheckStateRole, RendererManager::SELECTED });
+            }
+            else
             {
-                p_item = m_items[ souturi ].second;
-                if( p_selected_item != p_item )
-                {
-                    m_items.remove( souturi );
-                    emit rendererItemRemoved( p_item );
-                    vlc_renderer_item_release( p_item );
-                }
-                else m_items[ souturi ].first = true; /* keep */
+                QString key = getItemKey(m_selectedItem);
+                emit q->beginInsertRows({}, m_itemsIndex.size(), m_itemsIndex.size());
+                //renderer is not part of the current scan, mark it like it
+                m_items.emplace( key, renderItem, false );
+                m_itemsIndex.push_back(key);
+                emit q->endInsertRows();
             }
-            /* else ignore */
         }
+        emit q->useRendererChanged();
+    }
+
+    QString getItemKey(const RendererItemPtr& item) const
+    {
+        const char* type = vlc_renderer_item_type(item.get());
+        const char* sout = vlc_renderer_item_sout(item.get());
+        return QString("%1-%2").arg(type).arg(sout);
+    }
+
+    int getItemRow(const RendererItemPtr& item) const
+    {
+        QString newKey = getItemKey(item);
+        return m_itemsIndex.indexOf(newKey);
+    }
+
+    qt_intf_t* const p_intf;
+    vlc_player_t* const m_player;
+
+    vlc_player_listener_id* m_playerListener = nullptr;
+
+    RendererManager::RendererStatus m_status = RendererManager::IDLE;
+
+    std::vector<vlc_renderer_discovery_t*> m_rds;
+
+
+    //the elements of the model (in model order), the string is the key of m_items
+    QVector<QString> m_itemsIndex;
+
+    QHash<QString, ItemEntry> m_items;
+
+    //renderer currently used by the player
+    RendererItemPtr m_selectedItem;
+
+    //model automatically stops scanning after a while
+    QTimer m_stop_scan_timer;
+    //remaining time in seconds
+    unsigned m_scan_remain = 0;
+
+    RendererManager* q_ptr;
+    Q_DECLARE_PUBLIC(RendererManager);
+};
+
+void RendererManager::disableRenderer()
+{
+    Q_D(RendererManager);
+    if (!d->m_selectedItem)
+        return;
+
+    {
+        vlc_player_locker lock{ d->m_player };
+        //vlc_player synchronously report the renderer change
+        vlc_player_SetRenderer( d->m_player, nullptr );
     }
 }
 
+QVariant RendererManager::data(const QModelIndex &index, int role) const
+{
+    Q_D(const RendererManager);
+    if (index.row() >= d->m_itemsIndex.size())
+        return {};
+
+    const ItemEntry& entry = d->m_items.value(d->m_itemsIndex[index.row()]);
+    vlc_renderer_item_t* item = entry.renderItem.get();
+    switch (role)
+    {
+    case Qt::DisplayRole:
+    case NAME:
+        return QString(vlc_renderer_item_name(item));
+    case TYPE:
+        return QString(vlc_renderer_item_type(item));
+    case DEMUX_FILTER:
+        return QString(vlc_renderer_item_demux_filter(item));
+    case SOUT:
+        return QString(vlc_renderer_item_sout(item));
+    case ICON_URI:
+    case Qt::DecorationRole:
+    {
+        const char* iconUri = vlc_renderer_item_icon_uri(item);
+        if (!iconUri)
+        {
+            if (vlc_renderer_item_flags( item ) & VLC_RENDERER_CAN_VIDEO)
+                return QString("qrc://menu/movie.svg");
+            else
+                return QString("qrc://menu/music.svg");
+        }
+        return QString(iconUri);
+    }
+    case FLAGS:
+        return vlc_renderer_item_flags(entry.renderItem.get());
+    case SELECTED:
+        return entry.renderItem == d->m_selectedItem;
+    case Qt::CheckStateRole:
+        return entry.renderItem == d->m_selectedItem ? Qt::Checked : Qt::Unchecked;
+    }
+    return {};
+}
+
+bool RendererManager::setData(const QModelIndex &index, const QVariant &value, int role)
+{
+    Q_D(RendererManager);
+    if (index.row() >= d->m_itemsIndex.size())
+        return false;
+
+    QString sout = d->m_itemsIndex[index.row()];
+    ItemEntry& entry = d->m_items[sout];
+    switch (role)
+    {
+    case Qt::CheckStateRole:
+    case SELECTED:
+    {
+        bool enableRenderer;
+
+        if ( value.canConvert<bool>() )
+            enableRenderer = value.toBool();
+        else
+            return false;
+
+        {
+            vlc_player_locker lock{ d->m_player };
+            //vlc_player synchronously report the renderer change
+            vlc_player_SetRenderer(
+                d->m_player,
+                enableRenderer ? entry.renderItem.get() : nullptr
+            );
+        }
+        return true;
+    }
+    default:
+        return false;
+    }
+}
+
+QHash<int,QByteArray> RendererManager::roleNames() const
+{
+    return {
+        { NAME, "name" },
+        { TYPE, "type" },
+        { DEMUX_FILTER, "demuxFilter" },
+        { SOUT, "sour" },
+        { ICON_URI, "iconUri" },
+        { FLAGS, "flags" },
+    };
+}
+
+int RendererManager::rowCount(const QModelIndex&) const
+{
+    Q_D(const RendererManager);
+    return d->m_itemsIndex.size();
+}
+
+
+static void renderer_event_item_added( vlc_renderer_discovery_t* p_rd, vlc_renderer_item_t *p_item )
+{
+    RendererManagerPrivate *self = reinterpret_cast<RendererManagerPrivate*>( p_rd->owner.sys );
+    RendererItemPtr renderItem(p_item);
+    QMetaObject::invokeMethod(self->q_func(), [self, renderItem = std::move(renderItem)](){
+        self->onRendererAdded(renderItem);
+    });
+}
+
+static void renderer_event_item_removed( vlc_renderer_discovery_t *p_rd,
+                                 vlc_renderer_item_t *p_item )
+{
+    RendererManagerPrivate *self = reinterpret_cast<RendererManagerPrivate*>( p_rd->owner.sys );
+    RendererItemPtr renderItem(p_item);
+    QMetaObject::invokeMethod(self->q_func(), [self, renderItem = std::move(renderItem)](){
+        self->onRendererRemoved(renderItem);
+    });
+}
+
+static void player_renderer_changed(vlc_player_t *, vlc_renderer_item_t *new_item, void *data)
+{
+    RendererManagerPrivate *self = reinterpret_cast<RendererManagerPrivate*>( data);
+    RendererItemPtr renderItem(new_item);
+    QMetaObject::invokeMethod(self->q_func(), [self, renderItem = std::move(renderItem)](){
+        self->onPlayerRendererChanged(renderItem);
+    });
+}
+
+RendererManager::RendererManager( qt_intf_t *p_intf_, vlc_player_t* player )
+    : d_ptr(new RendererManagerPrivate(this, p_intf_, player))
+{
+}
+
+RendererManager::~RendererManager()
+{
+    StopScan();
+}
+
+int RendererManager::getScanRemain() const
+{
+    Q_D(const RendererManager);
+    return d->m_scan_remain;
+}
+
+RendererManager::RendererStatus RendererManager::getStatus() const
+{
+    Q_D(const RendererManager);
+    return d->m_status;
+}
+
+bool RendererManager::useRenderer() const
+{
+    Q_D(const RendererManager);
+    return d->m_selectedItem.get() != nullptr;
+}
+
 void RendererManager::StartScan()
 {
-    if( m_stop_scan_timer.isActive() )
+    Q_D(RendererManager);
+    if( d->m_stop_scan_timer.isActive() )
         return;
 
     /* SD subnodes */
     char **ppsz_longnames;
     char **ppsz_names;
-    if( vlc_rd_get_names( p_intf, &ppsz_names, &ppsz_longnames ) != VLC_SUCCESS )
+    if( vlc_rd_get_names( d->p_intf, &ppsz_names, &ppsz_longnames ) != VLC_SUCCESS )
     {
-        emit statusUpdated( RendererManager::RendererStatus::FAILED );
+        d->setStatus( RendererManager::RendererStatus::FAILED );
         return;
     }
 
     struct vlc_renderer_discovery_owner owner =
     {
-        this,
+        d,
         renderer_event_item_added,
         renderer_event_item_removed,
     };
@@ -110,83 +414,50 @@ void RendererManager::StartScan()
     char **ppsz_name = ppsz_names, **ppsz_longname = ppsz_longnames;
     for( ; *ppsz_name; ppsz_name++, ppsz_longname++ )
     {
-        msg_Dbg( p_intf, "starting renderer discovery service %s", *ppsz_longname );
-        vlc_renderer_discovery_t* p_rd = vlc_rd_new( VLC_OBJECT(p_intf), *ppsz_name, &owner );
+        msg_Dbg( d->p_intf, "starting renderer discovery service %s", *ppsz_longname );
+        vlc_renderer_discovery_t* p_rd = vlc_rd_new( VLC_OBJECT(d->p_intf), *ppsz_name, &owner );
         if( p_rd != NULL )
-            m_rds.push_back( p_rd );
+            d->m_rds.push_back( p_rd );
         free( *ppsz_name );
         free( *ppsz_longname );
     }
     free( ppsz_names );
     free( ppsz_longnames );
 
-    emit statusUpdated( RendererManager::RendererStatus::RUNNING );
-    m_scan_remain = 20000;
-    m_stop_scan_timer.setInterval( 1000 );
-    m_stop_scan_timer.start();
+    d->m_scan_remain = 20;
+    d->m_stop_scan_timer.setInterval( 1000 );
+    d->m_stop_scan_timer.start();
+    d->setStatus( RendererManager::RendererStatus::RUNNING );
+    emit scanRemainChanged();
 }
 
 void RendererManager::StopScan()
 {
-    m_stop_scan_timer.stop();
-    foreach ( vlc_renderer_discovery_t* p_rd, m_rds )
+    Q_D(RendererManager);
+    d->m_stop_scan_timer.stop();
+
+    for ( vlc_renderer_discovery_t* p_rd : d->m_rds )
         vlc_rd_release( p_rd );
+    d->m_rds.clear();
+
     /* Cleanup of outdated items, and notify removal */
-    QHash<QString, ItemEntry>::iterator it = m_items.begin();
-    while ( it != m_items.end() )
+    for (int i = d->m_itemsIndex.size() - 1; i >= 0; --i)
     {
-        ItemEntry &entry = it.value();
-        if( !entry.first /* don't keep */ && entry.second != p_selected_item )
+        QString key = d->m_itemsIndex[i];
+        ItemEntry& entry = d->m_items[key];
+        if( !entry.currentScan && entry.renderItem != d->m_selectedItem )
         {
-            emit rendererItemRemoved( entry.second );
-            vlc_renderer_item_release( entry.second );
-            it = m_items.erase( it );
+            //remove items from previous scans
+            emit beginRemoveRows({}, i, i);
+            d->m_items.remove(key);
+            d->m_itemsIndex.remove(i);
+            emit endRemoveRows();
         }
         else
         {
-            entry.first = false; /* don't keep if not updated by new detect */
-            assert( it.value().first == false );
-            ++it;
+            /* don't keep if not updated by next detect */
+            entry.currentScan = false;
         }
     }
-    m_rds.clear();
-    emit statusUpdated( RendererManager::RendererStatus::IDLE );
-}
-
-void RendererManager::RendererMenuCountdown()
-{
-    if( m_stop_scan_timer.isActive() && m_scan_remain > 0 )
-    {
-        m_scan_remain -= 1000;
-        emit statusUpdated( RendererManager::RendererStatus::RUNNING +  m_scan_remain / 1000 );
-    }
-    else
-    {
-        StopScan();
-    }
-}
-
-void RendererManager::SelectRenderer( vlc_renderer_item_t *p_item )
-{
-    p_selected_item = p_item;
-    vlc_player_locker lock{ p_intf->p_player };
-    vlc_player_SetRenderer( p_intf->p_player, p_item );
-}
-
-void RendererManager::renderer_event_item_added( vlc_renderer_discovery_t* p_rd,
-                                                 vlc_renderer_item_t *p_item )
-{
-    RendererManager *self = reinterpret_cast<RendererManager*>( p_rd->owner.sys );
-    QEvent *ev = new RendererManagerEvent( RendererManagerEvent::AddedEvent,
-                                           p_item );
-    QApplication::postEvent( self, ev );
-}
-
-void RendererManager::renderer_event_item_removed( vlc_renderer_discovery_t *p_rd,
-                                                   vlc_renderer_item_t *p_item )
-{
-    RendererManager *self = reinterpret_cast<RendererManager*>( p_rd->owner.sys );
-    QEvent *ev = new RendererManagerEvent( RendererManagerEvent::RemovedEvent,
-                                           p_item );
-    QApplication::postEvent( self, ev );
+    d->setStatus( RendererManager::RendererStatus::IDLE );
 }


=====================================
modules/gui/qt/util/renderer_manager.hpp
=====================================
@@ -1,38 +1,30 @@
+/*****************************************************************************
+ * Copyright (C) 2024 VLC authors and VideoLAN
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
 #ifndef RENDERER_MANAGER_HPP
 #define RENDERER_MANAGER_HPP
 
 #include "qt.hpp"
-#include "util/singleton.hpp"
+#include <QAbstractListModel>
 
-#include <QObject>
-#include <QEvent>
-#include <QTimer>
-#include <QVector>
-#include <QHash>
-
-extern "C" {
-    typedef struct vlc_renderer_item_t vlc_renderer_item_t;
-}
-
-class RendererManagerEvent : public QEvent
-{
-public:
-    static const QEvent::Type AddedEvent;
-    static const QEvent::Type RemovedEvent;
-
-    RendererManagerEvent( QEvent::Type type, vlc_renderer_item_t *p_item_ );
-    virtual ~RendererManagerEvent();
-
-    vlc_renderer_item_t * getItem() const { return p_item; }
-
-private:
-    vlc_renderer_item_t *p_item;
-};
-
-class RendererManager : public QObject, public Singleton<RendererManager>
+class RendererManagerPrivate;
+class RendererManager : public QAbstractListModel
 {
     Q_OBJECT
-    friend class Singleton<RendererManager>;
 
 public:
     enum RendererStatus
@@ -41,36 +33,53 @@ public:
         IDLE = -1,
         RUNNING,
     };
-    RendererManager( qt_intf_t * );
+    Q_ENUM(RendererStatus)
+
+    enum Roles {
+        NAME = Qt::UserRole,
+        TYPE,
+        DEMUX_FILTER,
+        SOUT,
+        ICON_URI,
+        FLAGS,
+        SELECTED
+    };
+
+    ///remaining time before scan timeout
+    Q_PROPERTY(int scanRemain READ getScanRemain NOTIFY scanRemainChanged FINAL)
+    Q_PROPERTY(RendererStatus status READ getStatus NOTIFY statusChanged FINAL)
+    Q_PROPERTY(bool useRenderer READ useRenderer NOTIFY useRendererChanged FINAL)
+
+public:
+    RendererManager( qt_intf_t* intf, vlc_player_t* player );
     virtual ~RendererManager();
-    void customEvent( QEvent * );
+
+    void disableRenderer();
 
 public slots:
-    void SelectRenderer( vlc_renderer_item_t * );
     void StartScan();
     void StopScan();
 
+    //QAbstractListModel override
+public:
+    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+    bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
+    QHash<int,QByteArray> roleNames() const override;
+    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+
+    //properties accessor
+public:
+    int getScanRemain() const;
+    RendererStatus getStatus() const;
+    bool useRenderer() const;
 signals:
-    void rendererItemAdded( vlc_renderer_item_t * );   /* For non queued only */
-    void rendererItemRemoved( vlc_renderer_item_t * ); /* For non queued only */
-    void statusUpdated( int );
+    void statusChanged();
+    void scanRemainChanged();
+    void useRendererChanged();
 
 private:
-    static void renderer_event_item_added( vlc_renderer_discovery_t *,
-                                           vlc_renderer_item_t * );
-    static void renderer_event_item_removed( vlc_renderer_discovery_t *,
-                                             vlc_renderer_item_t * );
-
-    typedef std::pair<bool, vlc_renderer_item_t *> ItemEntry;
-    qt_intf_t* const p_intf;
-    const vlc_renderer_item_t *p_selected_item;
-    QVector<vlc_renderer_discovery_t*> m_rds;
-    QHash<QString, ItemEntry> m_items;
-    QTimer m_stop_scan_timer;
-    unsigned m_scan_remain;
-
-private slots:
-    void RendererMenuCountdown();
+    Q_DECLARE_PRIVATE(RendererManager);
+    QScopedPointer<RendererManagerPrivate> d_ptr;
 };
 
 #endif // RENDERER_MANAGER_HPP


=====================================
modules/gui/qt/util/vlcaccess_image_provider.cpp
=====================================
@@ -127,6 +127,12 @@ public:
         return errorStr;
     }
 
+    void cancel() override
+    {
+        reader.reset();
+        emit finished();
+    }
+
 private:
     void handleImageRead()
     {
@@ -171,6 +177,11 @@ QString VLCAccessImageProvider::wrapUri(QString path)
     return QStringLiteral("image://vlcaccess/?") + query.toString(QUrl::FullyEncoded);
 }
 
+QQuickImageResponse* VLCAccessImageProvider::requestImageResponseUnWrapped(const QUrl url, const QSize &requestedSize, VLCAccessImageProvider::ImagePostProcessCb cb)
+{
+    return new VLCAccessImageResponse(url, requestedSize, cb);
+}
+
 //// VLCImageAccess
 
 VLCAccessImage::VLCAccessImage(QObject* parent)


=====================================
modules/gui/qt/util/vlcaccess_image_provider.hpp
=====================================
@@ -73,6 +73,8 @@ public:
 
     static QString wrapUri(QString path);
 
+    static QQuickImageResponse* requestImageResponseUnWrapped(const QUrl, const QSize &requestedSize, ImagePostProcessCb cb = nullptr);
+
 private:
     ImagePostProcessCb postProcessCb;
 };


=====================================
test/meson.build
=====================================
@@ -15,6 +15,7 @@ foreach vlc_test: vlc_tests
         'name',
         'sources',
         'moc_headers',
+        'moc_sources',
         'suite',
         'link_with',
         'module_depends',
@@ -57,10 +58,12 @@ foreach vlc_test: vlc_tests
     endif
 
     moc_sources = []
-    if vlc_test.has_key('moc_headers') and qt6_dep.found()
-        moc_sources += qt6.preprocess(moc_headers: vlc_test['moc_headers'],
-        include_directories: qt_include_dir,
-        dependencies: qt6_dep)
+    if (vlc_test.has_key('moc_headers') or vlc_test.has_key('moc_sources')) and qt6_dep.found()
+        moc_sources += qt6.preprocess(
+            moc_headers: vlc_test.get('moc_headers', []),
+            moc_sources: vlc_test.get('moc_sources', []),
+            include_directories: qt_include_dir,
+            dependencies: qt6_dep)
     endif
 
     test(vlc_test['name'],



View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/7b1acdc1bfb741b3bb7c2fef2b485af61e737953...e32174b38709288e07b6e1e6e71994a96e186731

-- 
View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/7b1acdc1bfb741b3bb7c2fef2b485af61e737953...e32174b38709288e07b6e1e6e71994a96e186731
You're receiving this email because of your account on code.videolan.org.


VideoLAN code repository instance


More information about the vlc-commits mailing list