From 9b876c198e024153c1019f28fb792e853a515c69 Mon Sep 17 00:00:00 2001
From: Vladimir Stoiakin <VStoiakin@lavabit.com>
Date: Thu, 11 May 2023 16:30:56 +0300
Subject: [PATCH] videoprovider: cleanup synchronization with the scene graph
 renderer

Applies some suggestions based on reviews by vstoiakin regarding concurrent resource access and overall structural simplification.
- Create and handoff ownership of QVideoFrames each frame
- use QReadWriteLocks when multiple reads are possible instead of QMutex
- remove the superfluous layer of unique_ptrs on QVideoFrames which are explicitly shared using QExplicitlySharedDataPointer for their underlying data.

This patch is required to prevent crashes when using Qt 6.4 and up.

Gitlab: #938
Change-Id: Ia9a9f0310344bcee47dbd88ec622bc3daa2ce6d7
---
 src/app/commoncomponents/VideoView.qml |   2 +-
 src/app/videoprovider.cpp              | 319 ++++++++++++++-----------
 src/app/videoprovider.h                |  28 ++-
 src/libclient/directrenderer.cpp       |   2 +-
 4 files changed, 202 insertions(+), 149 deletions(-)

diff --git a/src/app/commoncomponents/VideoView.qml b/src/app/commoncomponents/VideoView.qml
index dc611153c..2b2e28632 100644
--- a/src/app/commoncomponents/VideoView.qml
+++ b/src/app/commoncomponents/VideoView.qml
@@ -55,7 +55,7 @@ Item {
 
         antialiasing: true
         anchors.fill: parent
-        opacity: videoProvider.activeRenderers[rendererId] !== undefined
+        opacity: videoProvider.renderers[rendererId] !== undefined
         visible: opacity
 
         fillMode: crop ? VideoOutput.PreserveAspectCrop : VideoOutput.PreserveAspectFit
diff --git a/src/app/videoprovider.cpp b/src/app/videoprovider.cpp
index 99d5f2cfc..e37d4bb45 100644
--- a/src/app/videoprovider.cpp
+++ b/src/app/videoprovider.cpp
@@ -17,18 +17,12 @@
  */
 
 #include "videoprovider.h"
+#include "utils.h"
 
-using namespace lrc::api;
+#include <QReadLocker>
+#include <QWriteLocker>
 
-static bool
-mapVideoFrame(QVideoFrame* videoFrame)
-{
-    if (!videoFrame || !videoFrame->isValid()
-        || (!videoFrame->isMapped() && !videoFrame->map(QVideoFrame::WriteOnly))) {
-        return false;
-    }
-    return true;
-}
+using namespace lrc::api;
 
 VideoProvider::VideoProvider(AVModel& avModel, QObject* parent)
     : QObject(parent)
@@ -84,29 +78,23 @@ VideoProvider::subscribe(QObject* obj, const QString& id)
             &VideoProvider::unsubscribe,
             static_cast<Qt::ConnectionType>(Qt::DirectConnection | Qt::UniqueConnection));
 
-    QMutexLocker lk(&framesObjsMutex_);
+    QWriteLocker lock(&renderersMutex_);
     // 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>();
+    auto it = renderers_.find(id);
+    if (it == renderers_.end()) {
         qDebug() << "Creating new FrameObject for id:" << id;
-        auto emplaced = framesObjects_.emplace(id, std::move(fo));
-        if (!emplaced.second) {
-            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;
+        FrameObject& object = renderers_[id];
+        object.active = false;
+        it = renderers_.find(id);
     } else {
         // Make sure it's not already subscribed to this QVideoSink.
-        QMutexLocker subsLk(&it->second->mutex);
-        if (it->second->subscribers.contains(sink)) {
+        if (it->second.subscribers.contains(sink)) {
             qWarning() << Q_FUNC_INFO << "QVideoSink already subscribed to id:" << id;
             return;
         }
     }
-    QMutexLocker subsLk(&it->second->mutex);
-    it->second->subscribers.insert(sink);
+
+    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);
@@ -115,39 +103,27 @@ VideoProvider::subscribe(QObject* obj, const QString& id)
 void
 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(static_cast<QVideoSink*>(obj));
-        if (it != subs.cend()) {
-            subs.erase(it);
+    QString id;
+
+    renderersMutex_.lockForRead();
+    for (auto& pair : renderers_) {
+        QWriteLocker lock(&pair.second.subscribersMutex);
+        if (pair.second.subscribers.remove(static_cast<QVideoSink*>(obj))) {
             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;
+                                      .arg(pair.first);
+            if (!pair.second.active && pair.second.subscribers.isEmpty())
+                id = pair.first;
+            break;
         }
     }
-}
+    renderersMutex_.unlock();
 
-QVideoFrame*
-VideoProvider::frame(const QString& id)
-{
-    // framesObjsMutex_ MUST be locked
-    auto it = framesObjects_.find(id);
-    if (it == framesObjects_.end()) {
-        return {};
+    if (!id.isEmpty()) {
+        renderersMutex_.lockForWrite();
+        renderers_.erase(id);
+        renderersMutex_.unlock();
     }
-    QMutexLocker lk(&it->second->mutex);
-    if (it->second->subscribers.empty()) {
-        return {};
-    }
-    auto videoFrame = it->second->videoFrame.get();
-    if (!mapVideoFrame(videoFrame)) {
-        qWarning() << "QVideoFrame can't be mapped" << id;
-        return {};
-    }
-    return videoFrame;
 }
 
 QString
@@ -160,18 +136,24 @@ VideoProvider::captureVideoFrame(const QString& id)
 QImage
 VideoProvider::captureRawVideoFrame(const QString& id)
 {
-    QMutexLocker framesLk(&framesObjsMutex_);
-    if (auto* videoFrame = frame(id)) {
-        auto imageFormat = QVideoFrameFormat::imageFormatFromPixelFormat(
-            QVideoFrameFormat::Format_RGBA8888);
-        auto img = QImage(videoFrame->bits(0),
-                          videoFrame->width(),
-                          videoFrame->height(),
-                          videoFrame->bytesPerLine(0),
-                          imageFormat);
-        return img;
+    QReadLocker lock(&renderersMutex_);
+    QImage img;
+
+    auto it = renderers_.find(id);
+    if (it != renderers_.end()) {
+        QReadLocker lock(&it->second.frameMutex);
+        QVideoFrame& videoFrame = it->second.videoFrame;
+        if (videoFrame.map(QVideoFrame::ReadOnly)) {
+            auto imageFormat = QVideoFrameFormat::imageFormatFromPixelFormat(
+                QVideoFrameFormat::Format_RGBA8888);
+            img = QImage(videoFrame.bits(0),
+                         videoFrame.width(),
+                         videoFrame.height(),
+                         videoFrame.bytesPerLine(0),
+                         imageFormat);
+        }
     }
-    return {};
+    return img;
 }
 
 void
@@ -181,103 +163,152 @@ VideoProvider::onRendererStarted(const QString& id, const QSize& size)
                                         ? QVideoFrameFormat::Format_RGBA8888
                                         : QVideoFrameFormat::Format_BGRA8888;
     auto frameFormat = QVideoFrameFormat(size, pixelFormat);
-    {
-        QMutexLocker lk(&framesObjsMutex_);
-        auto it = framesObjects_.find(id);
-        if (it == framesObjects_.end()) {
-            auto fo = std::make_unique<FrameObject>();
-            fo->videoFrame = std::make_unique<QVideoFrame>(frameFormat);
-            qDebug() << "Create new QVideoFrame" << frameFormat.frameSize();
-            framesObjects_.emplace(id, std::move(fo));
-        } else {
-            it->second->videoFrame.reset(new QVideoFrame(frameFormat));
-            qDebug() << "QVideoFrame reset to" << frameFormat.frameSize();
-        }
+
+    renderersMutex_.lockForWrite();
+    auto it = renderers_.find(id);
+    if (it == renderers_.end()) {
+        qDebug() << "Create new QVideoFrame" << frameFormat.frameSize();
+        FrameObject& fo = renderers_[id];
+        fo.videoFrame = QVideoFrame(frameFormat);
+        fo.active = true;
+    } else {
+        it->second.videoFrame = QVideoFrame(frameFormat);
+        it->second.active = true;
+        qDebug() << "QVideoFrame reset to" << frameFormat.frameSize();
     }
+    renderersMutex_.unlock();
 
-    activeRenderers_[id] = size;
-    Q_EMIT activeRenderersChanged();
+    Q_EMIT renderersChanged();
 }
 
 void
 VideoProvider::onFrameBufferRequested(const QString& id, AVFrame* avframe)
 {
-    QMutexLocker framesLk(&framesObjsMutex_);
-    if (auto* videoFrame = frame(id)) {
-        // The ownership of avframe structure remains the subscriber(jamid), and
-        // the videoFrame instance is owned by the VideoProvider(client). The
-        // avframe structure contains only a description of the QVideoFrame
-        // underlying buffer.
-        // TODO: ideally, the colorspace format should likely come from jamid and
-        // be the decoded format.
-        avframe->format = AV_PIX_FMT_RGBA;
-        avframe->width = videoFrame->width();
-        avframe->height = videoFrame->height();
-        avframe->data[0] = (uint8_t*) videoFrame->bits(0);
-        avframe->linesize[0] = videoFrame->bytesPerLine(0);
+    renderersMutex_.lockForRead();
+
+    auto it = renderers_.find(id);
+    if (it == renderers_.end()) {
+        renderersMutex_.unlock();
+        qWarning() << Q_FUNC_INFO << "Can't find renderer for id:" << id;
+        return;
+    }
+    QVideoFrame& videoFrame = it->second.videoFrame;
+
+    it->second.frameMutex.lockForWrite(); // because captureRawVideoFrame() can be called
+
+    // Create a new QVideoFrame with the same format. We don't know when or if the
+    // render thread will upload it, so ownership must be transferred to the
+    // render thread. This is done in onFrameUpdated for shared memory frames.
+    videoFrame = QVideoFrame(videoFrame.surfaceFormat());
+    if (!videoFrame.map(QVideoFrame::WriteOnly)) {
+        it->second.frameMutex.unlock();
+        renderersMutex_.unlock();
+        qWarning() << Q_FUNC_INFO << "Can't map QVideoFrame for id:" << id;
+        avframe->format = AV_PIX_FMT_NONE;
+        return;
     }
+
+    // The ownership of avframe structure remains the subscriber(jamid), and
+    // the videoFrame instance is owned by the VideoProvider(client). The
+    // avframe structure contains only a description of the QVideoFrame
+    // underlying buffer.
+    // TODO: ideally, the colorspace format should likely come from jamid and
+    // be the decoded format.
+    avframe->format = AV_PIX_FMT_RGBA;
+    avframe->width = videoFrame.width();
+    avframe->height = videoFrame.height();
+    avframe->data[0] = (uint8_t*) videoFrame.bits(0);
+    avframe->linesize[0] = videoFrame.bytesPerLine(0);
 }
 
 void
 VideoProvider::onFrameUpdated(const QString& id)
 {
-    QMutexLocker framesLk(&framesObjsMutex_);
-    auto it = framesObjects_.find(id);
-    if (it == framesObjects_.end()) {
-        return;
-    }
-    QMutexLocker lk(&it->second->mutex);
-    if (it->second->subscribers.empty()) {
-        return;
-    }
-    auto videoFrame = it->second->videoFrame.get();
-    if (videoFrame == nullptr) {
-        qWarning() << "QVideoFrame has not been initialized.";
-        return;
-    }
-    if (!avModel_.useDirectRenderer()) {
-        // Shared memory renderering.
-        if (!mapVideoFrame(videoFrame)) {
-            qWarning() << "QVideoFrame can't be mapped" << id;
+    if (avModel_.useDirectRenderer()) {
+        auto it = renderers_.find(id);
+        if (it == renderers_.end()) {
+            qWarning() << Q_FUNC_INFO << "Can't find renderer for id:" << id;
+            return;
+        }
+
+        QVideoFrame& videoFrame = it->second.videoFrame;
+        videoFrame.unmap();
+        it->second.frameMutex.unlock(); // locked by onFrameBufferRequested()
+
+        it->second.frameMutex.lockForRead();
+        it->second.subscribersMutex.lockForRead();
+        for (const auto& sink : qAsConst(it->second.subscribers)) {
+            sink->setVideoFrame(videoFrame);
+        }
+        it->second.subscribersMutex.unlock();
+        it->second.frameMutex.unlock();
+
+        renderersMutex_.unlock(); // locked by onFrameBufferRequested()
+    } else {
+        QReadLocker lock(&renderersMutex_);
+        auto it = renderers_.find(id);
+        if (it == renderers_.end()) {
+            qWarning() << Q_FUNC_INFO << "Can't find renderer for id:" << id;
             return;
         }
+
+        QVideoFrame& videoFrame = it->second.videoFrame;
+        it->second.frameMutex.lockForWrite();
+        videoFrame = QVideoFrame(videoFrame.surfaceFormat());
+        if (!videoFrame.map(QVideoFrame::WriteOnly)) {
+            it->second.frameMutex.unlock();
+            qWarning() << Q_FUNC_INFO << "Can't map video frame for id:" << id;
+            return;
+        }
+
         auto srcFrame = avModel_.getRendererFrame(id);
         if (srcFrame.ptr != nullptr and srcFrame.size > 0) {
             copyUnaligned(videoFrame, srcFrame);
         }
-    }
-    if (videoFrame->isMapped()) {
-        videoFrame->unmap();
-        for (const auto& sink : qAsConst(it->second->subscribers)) {
-            sink->setVideoFrame(*videoFrame);
-            Q_EMIT sink->videoFrameChanged(*videoFrame);
+
+        videoFrame.unmap();
+        it->second.frameMutex.unlock();
+
+        it->second.frameMutex.lockForRead();
+        it->second.subscribersMutex.lockForRead();
+        for (const auto& sink : qAsConst(it->second.subscribers)) {
+            sink->setVideoFrame(videoFrame);
         }
+        it->second.subscribersMutex.unlock();
+        it->second.frameMutex.unlock();
     }
 }
 
 void
 VideoProvider::onRendererStopped(const QString& id)
 {
-    QMutexLocker framesLk(&framesObjsMutex_);
-    auto it = framesObjects_.find(id);
-    if (it == framesObjects_.end()) {
+    renderersMutex_.lockForWrite();
+
+    auto it = renderers_.find(id);
+    if (it == renderers_.end()) {
+        renderersMutex_.unlock();
+        qWarning() << Q_FUNC_INFO << "Can't find renderer for id:" << id;
         return;
     }
-
-    activeRenderers_.remove(id);
-    Q_EMIT activeRenderersChanged();
-
-    QMutexLocker lk(&it->second->mutex);
-    if (it->second->subscribers.empty()) {
-        lk.unlock();
-        framesObjects_.erase(it);
+    if (it->second.subscribers.isEmpty()) {
+        renderers_.erase(id);
+        renderersMutex_.unlock();
+        Q_EMIT renderersChanged();
         return;
     }
-    it->second->videoFrame.reset();
+
+    it->second.frameMutex.lockForWrite();
+    it->second.videoFrame = QVideoFrame();
+    it->second.active = false;
+    it->second.frameMutex.unlock();
+
+    renderersMutex_.unlock();
+
+    Q_EMIT renderersChanged();
 }
 
 void
-VideoProvider::copyUnaligned(QVideoFrame* dst, const video::Frame& src)
+VideoProvider::copyUnaligned(QVideoFrame& dst, const video::Frame& src)
 {
     // Copy from a frame residing in the shared memory.
     // Frames in shared memory have no specific line alignment
@@ -286,9 +317,9 @@ VideoProvider::copyUnaligned(QVideoFrame* dst, const video::Frame& src)
 
     // This helper only handles RGBA and BGRA pixel formats, so the
     // following constraints must apply.
-    assert(dst->pixelFormat() == QVideoFrameFormat::Format_RGBA8888
-           or dst->pixelFormat() == QVideoFrameFormat::Format_BGRA8888);
-    assert(dst->planeCount() == 1);
+    assert(dst.pixelFormat() == QVideoFrameFormat::Format_RGBA8888
+           or dst.pixelFormat() == QVideoFrameFormat::Format_BGRA8888);
+    assert(dst.planeCount() == 1);
 
     const int BYTES_PER_PIXEL = 4;
 
@@ -297,15 +328,31 @@ VideoProvider::copyUnaligned(QVideoFrame* dst, const video::Frame& src)
     // The source buffer must be greater or equal to the min required
     // buffer size. The SHM buffer might be slightly larger than the
     // required size due to the 16-byte alignment.
-    if (dst->width() * dst->height() * BYTES_PER_PIXEL > src.size) {
+    if (static_cast<size_t>(dst.width() * dst.height() * BYTES_PER_PIXEL) > src.size) {
         qCritical() << "The size of frame buffer " << src.size << " is smaller than expected "
-                    << dst->width() * dst->height() * BYTES_PER_PIXEL;
+                    << dst.width() * dst.height() * BYTES_PER_PIXEL;
         return;
     }
 
-    for (int row = 0; row < dst->height(); row++) {
-        auto dstPtr = dst->bits(0) + row * dst->bytesPerLine(0);
-        auto srcPtr = src.ptr + row * dst->width() * BYTES_PER_PIXEL;
-        std::memcpy(dstPtr, srcPtr, dst->width() * BYTES_PER_PIXEL);
+    for (int row = 0; row < dst.height(); row++) {
+        auto dstPtr = dst.bits(0) + row * dst.bytesPerLine(0);
+        auto srcPtr = src.ptr + row * dst.width() * BYTES_PER_PIXEL;
+        std::memcpy(dstPtr, srcPtr, dst.width() * BYTES_PER_PIXEL);
+    }
+}
+
+QVariantMap
+VideoProvider::getRenderers()
+{
+    QVariantMap map;
+    renderersMutex_.lockForRead();
+    for (auto& r : renderers_) {
+        if (r.second.active) {
+            r.second.frameMutex.lockForRead();
+            map[r.first] = r.second.videoFrame.size();
+            r.second.frameMutex.unlock();
+        }
     }
+    renderersMutex_.unlock();
+    return map;
 }
diff --git a/src/app/videoprovider.h b/src/app/videoprovider.h
index 1edcf1782..b44acf329 100644
--- a/src/app/videoprovider.h
+++ b/src/app/videoprovider.h
@@ -18,19 +18,21 @@
 
 #pragma once
 
-#include "utils.h"
-#include "qtutils.h"
-
 #include "api/avmodel.h"
 
 extern "C" {
 #include "libavutil/frame.h"
 }
 
+#include <QSet>
+#include <QVariantMap>
 #include <QVideoSink>
 #include <QVideoFrame>
 #include <QQmlEngine>
-#include <QMutex>
+#include <QReadWriteLock>
+
+#include <map>
+#include <atomic>
 
 using namespace lrc::api;
 
@@ -38,7 +40,6 @@ class VideoProvider final : public QObject
 {
     Q_OBJECT
     QML_ELEMENT
-    QML_PROPERTY(QVariantMap, activeRenderers)
 public:
     explicit VideoProvider(AVModel& avModel, QObject* parent = nullptr);
     ~VideoProvider() = default;
@@ -48,6 +49,10 @@ public:
     Q_INVOKABLE QString captureVideoFrame(const QString& id);
     Q_INVOKABLE QImage captureRawVideoFrame(const QString& id);
 
+    Q_PROPERTY(QVariantMap renderers READ getRenderers NOTIFY renderersChanged)
+    QVariantMap getRenderers();
+    Q_SIGNAL void renderersChanged();
+
 private Q_SLOTS:
     void onRendererStarted(const QString& id, const QSize& size);
     void onFrameBufferRequested(const QString& id, AVFrame* avframe);
@@ -55,16 +60,17 @@ private Q_SLOTS:
     void onRendererStopped(const QString& id);
 
 private:
-    QVideoFrame* frame(const QString& id);
-    void copyUnaligned(QVideoFrame* dst, const video::Frame& src);
     AVModel& avModel_;
+    void copyUnaligned(QVideoFrame& dst, const video::Frame& src);
 
     struct FrameObject
     {
-        std::unique_ptr<QVideoFrame> videoFrame;
-        QMutex mutex;
+        QVideoFrame videoFrame;
+        QReadWriteLock frameMutex;
         QSet<QVideoSink*> subscribers;
+        QReadWriteLock subscribersMutex;
+        std::atomic_bool active;
     };
-    std::map<QString, std::unique_ptr<FrameObject>> framesObjects_;
-    QMutex framesObjsMutex_;
+    std::map<QString, FrameObject> renderers_;
+    QReadWriteLock renderersMutex_;
 };
diff --git a/src/libclient/directrenderer.cpp b/src/libclient/directrenderer.cpp
index 008a29127..f4ecf09b5 100644
--- a/src/libclient/directrenderer.cpp
+++ b/src/libclient/directrenderer.cpp
@@ -68,7 +68,7 @@ public:
         // Important: Subscription to this signal MUST be synchronous(Qt::DirectConnection).
         Q_EMIT parent_->frameBufferRequested(frameBufferPtr.get());
 
-        if (frameBufferPtr->data[0] == nullptr) {
+        if (frameBufferPtr->format == AV_PIX_FMT_NONE) {
             return nullptr;
         }
 
-- 
GitLab