From 2baf76e74b449622beed35533ea1b8c57f33cac0 Mon Sep 17 00:00:00 2001
From: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
Date: Tue, 21 Feb 2023 18:18:19 -0500
Subject: [PATCH] videoprovider: remove frame subscription on
 QVideoSink::destroyed

We can observe that frame update signals can occur after a VideoOutput component's QVideoSink object has started destruction. The deregistration of the object pointer needs to be synchronized to the object's destruction.

Additionally:
+ scope the frame obj mutex locker around checks for no subs
+ rename (un)registerSink to (un)subscribe
+ subscribe will now remove the previous subscription
+ subscribe with an empty id will unsubscribe

Gitlab: #997
Change-Id: I21460564e49189b9276f153e8c1fd3a5cc52c6b2
---
 src/app/commoncomponents/VideoView.qml |  9 +---
 src/app/videoprovider.cpp              | 61 ++++++++++++++++++++------
 src/app/videoprovider.h                |  6 +--
 3 files changed, 52 insertions(+), 24 deletions(-)

diff --git a/src/app/commoncomponents/VideoView.qml b/src/app/commoncomponents/VideoView.qml
index 897c03f4e..12b138098 100644
--- a/src/app/commoncomponents/VideoView.qml
+++ b/src/app/commoncomponents/VideoView.qml
@@ -39,12 +39,7 @@ Item {
     property real xScale: contentRect.width / videoOutput.sourceRect.width
     property real yScale: contentRect.height / videoOutput.sourceRect.height
 
-    onRendererIdChanged: {
-        videoProvider.unregisterSink(videoSink)
-        if (rendererId.length !== 0) {
-            videoProvider.registerSink(rendererId, videoSink)
-        }
-    }
+    onRendererIdChanged: videoProvider.subscribe(videoSink, rendererId)
 
     Rectangle {
         id: bgRect
@@ -71,8 +66,6 @@ Item {
 
         Behavior on opacity { NumberAnimation { duration: 150 } }
 
-        Component.onDestruction: videoProvider.unregisterSink(videoSink)
-
         layer.enabled: opacity
         layer.effect: FastBlur {
             source: videoOutput
diff --git a/src/app/videoprovider.cpp b/src/app/videoprovider.cpp
index 3cd54f398..99d5f2cfc 100644
--- a/src/app/videoprovider.cpp
+++ b/src/app/videoprovider.cpp
@@ -57,39 +57,74 @@ VideoProvider::VideoProvider(AVModel& avModel, QObject* parent)
 }
 
 void
-VideoProvider::registerSink(const QString& id, QVideoSink* obj)
+VideoProvider::subscribe(QObject* obj, const QString& id)
 {
+    // First remove any previously existing subscription.
+    unsubscribe(obj);
+
+    if (id.isEmpty()) {
+        return;
+    }
+
+    // Make sure we're dealing with a QVideoSink object.
+    auto sink = qobject_cast<QVideoSink*>(obj);
+    if (sink == nullptr) {
+        qWarning() << Q_FUNC_INFO << "Object must be a QVideoSink.";
+        return;
+    }
+
+    // We need to detect the destruction of the QVideoSink, which is destroyed before
+    // it's parent VideoOutput component emits a Component.destruction signal.
+    // e.i. If we use: Component.onDestruction: videoProvider.removeSubscription(videoSink),
+    // and a frame update occurs, it's possible the QVideoSink is in the process of being,
+    // or has already been destroyed.
+    connect(sink,
+            &QVideoSink::destroyed,
+            this,
+            &VideoProvider::unsubscribe,
+            static_cast<Qt::ConnectionType>(Qt::DirectConnection | Qt::UniqueConnection));
+
     QMutexLocker lk(&framesObjsMutex_);
+    // Check if we already have a FrameObject for this id.
     auto it = framesObjects_.find(id);
     if (it == framesObjects_.end()) {
         auto fo = std::make_unique<FrameObject>();
-        fo->subscribers.insert(obj);
         qDebug() << "Creating new FrameObject for id:" << id;
         auto emplaced = framesObjects_.emplace(id, std::move(fo));
         if (!emplaced.second) {
-            qWarning() << "Couldn't create FrameObject for id:" << id;
+            qWarning() << Q_FUNC_INFO << "Couldn't create FrameObject for id:" << id;
             return;
         }
+        // Get the iterator to the newly created FrameObject so we can add the subscriber.
         it = emplaced.first;
+    } else {
+        // Make sure it's not already subscribed to this QVideoSink.
+        QMutexLocker subsLk(&it->second->mutex);
+        if (it->second->subscribers.contains(sink)) {
+            qWarning() << Q_FUNC_INFO << "QVideoSink already subscribed to id:" << id;
+            return;
+        }
     }
-    qDebug().noquote() << QString("Adding sink: 0x%1 to subscribers for id: %2")
-                              .arg((quintptr) obj, QT_POINTER_SIZE * 2, 16, QChar('0'))
+    QMutexLocker subsLk(&it->second->mutex);
+    it->second->subscribers.insert(sink);
+    qDebug().noquote() << QString("Added sink: 0x%1 to subscribers for id: %2")
+                              .arg((quintptr) obj, QT_POINTER_SIZE, 16, QChar('0'))
                               .arg(id);
-    it->second->subscribers.insert(obj);
 }
 
 void
-VideoProvider::unregisterSink(QVideoSink* obj)
+VideoProvider::unsubscribe(QObject* obj)
 {
     QMutexLocker lk(&framesObjsMutex_);
     for (auto& frameObjIt : qAsConst(framesObjects_)) {
+        QMutexLocker subsLk(&frameObjIt.second->mutex);
         auto& subs = frameObjIt.second->subscribers;
-        auto it = subs.constFind(obj);
+        auto it = subs.constFind(static_cast<QVideoSink*>(obj));
         if (it != subs.cend()) {
-            qDebug().noquote() << QString("Removing sink: 0x%1 from subscribers for id: %2")
-                                      .arg((quintptr) obj, QT_POINTER_SIZE * 2, 16, QChar('0'))
-                                      .arg(frameObjIt.first);
             subs.erase(it);
+            qDebug().noquote() << QString("Removed sink: 0x%1 from subscribers for id: %2")
+                                      .arg((quintptr) obj, QT_POINTER_SIZE, 16, QChar('0'))
+                                      .arg(frameObjIt.first);
             return;
         }
     }
@@ -103,10 +138,10 @@ VideoProvider::frame(const QString& id)
     if (it == framesObjects_.end()) {
         return {};
     }
+    QMutexLocker lk(&it->second->mutex);
     if (it->second->subscribers.empty()) {
         return {};
     }
-    QMutexLocker lk(&it->second->mutex);
     auto videoFrame = it->second->videoFrame.get();
     if (!mapVideoFrame(videoFrame)) {
         qWarning() << "QVideoFrame can't be mapped" << id;
@@ -191,10 +226,10 @@ VideoProvider::onFrameUpdated(const QString& id)
     if (it == framesObjects_.end()) {
         return;
     }
+    QMutexLocker lk(&it->second->mutex);
     if (it->second->subscribers.empty()) {
         return;
     }
-    QMutexLocker lk(&it->second->mutex);
     auto videoFrame = it->second->videoFrame.get();
     if (videoFrame == nullptr) {
         qWarning() << "QVideoFrame has not been initialized.";
diff --git a/src/app/videoprovider.h b/src/app/videoprovider.h
index 80384502d..1edcf1782 100644
--- a/src/app/videoprovider.h
+++ b/src/app/videoprovider.h
@@ -43,8 +43,8 @@ public:
     explicit VideoProvider(AVModel& avModel, QObject* parent = nullptr);
     ~VideoProvider() = default;
 
-    Q_INVOKABLE void registerSink(const QString& id, QVideoSink* obj);
-    Q_INVOKABLE void unregisterSink(QVideoSink* obj);
+    Q_INVOKABLE void subscribe(QObject* obj, const QString& id = {});
+    Q_INVOKABLE void unsubscribe(QObject* obj);
     Q_INVOKABLE QString captureVideoFrame(const QString& id);
     Q_INVOKABLE QImage captureRawVideoFrame(const QString& id);
 
@@ -53,9 +53,9 @@ private Q_SLOTS:
     void onFrameBufferRequested(const QString& id, AVFrame* avframe);
     void onFrameUpdated(const QString& id);
     void onRendererStopped(const QString& id);
-    QVideoFrame* frame(const QString& id);
 
 private:
+    QVideoFrame* frame(const QString& id);
     void copyUnaligned(QVideoFrame* dst, const video::Frame& src);
     AVModel& avModel_;
 
-- 
GitLab