From e7cc0497ce08ba71cea5f2984627b0b45d251d4b Mon Sep 17 00:00:00 2001 From: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> Date: Fri, 11 Feb 2022 09:25:33 -0500 Subject: [PATCH] video: use QVideoSink/VideoOutput and QVideoFrame instead of QImage Removes the rendermanager and framewrapper objects along with any QQuickPaintedItem-based QML render widget classes. This simplifies the video widget stack implementation. The new mechanism uses the VideoOutput component of QtMultimedia. By accessing the VideoOutput's QVideoSink object, we update the mapped buffer data of a sink's QVideoFrame when new frames are published. Updates to frames and component sink subscriptions are managed by a new class called VideoProvider. Gitlab: #500 Also https://git.jami.net/savoirfairelinux/jami-client-qt/-/issues/536 Change-Id: I2391a32294922ea435ab80ac1f876c004ff6c21e --- CMakeLists.txt | 20 +- qml.qrc | 2 + src/avadapter.cpp | 36 +- src/avadapter.h | 4 + src/calladapter.cpp | 7 +- src/commoncomponents/LocalVideo.qml | 45 +++ src/commoncomponents/PhotoboothView.qml | 20 +- src/commoncomponents/VideoView.qml | 82 +++++ src/constant/JamiStrings.qml | 1 + src/distantrenderer.cpp | 141 -------- src/distantrenderer.h | 67 ---- src/lrcinstance.cpp | 8 - src/lrcinstance.h | 3 - src/mainapplication.cpp | 4 + src/mainview/components/OngoingCallPage.qml | 35 +- src/mainview/components/ParticipantsLayer.qml | 51 ++- src/mainview/components/RecordBox.qml | 19 +- src/mainview/components/SelectScreen.qml | 55 +-- src/previewrenderer.cpp | 177 ---------- src/previewrenderer.h | 86 ----- src/qmlregister.cpp | 8 - src/rendermanager.cpp | 321 ------------------ src/rendermanager.h | 243 ------------- src/settingsview/components/VideoSettings.qml | 58 +--- src/videodevices.cpp | 34 +- src/videoprovider.cpp | 208 ++++++++++++ src/videoprovider.h | 67 ++++ 27 files changed, 552 insertions(+), 1250 deletions(-) create mode 100644 src/commoncomponents/LocalVideo.qml create mode 100644 src/commoncomponents/VideoView.qml delete mode 100644 src/distantrenderer.cpp delete mode 100644 src/distantrenderer.h delete mode 100644 src/previewrenderer.cpp delete mode 100644 src/previewrenderer.h delete mode 100644 src/rendermanager.cpp delete mode 100644 src/rendermanager.h create mode 100644 src/videoprovider.cpp create mode 100644 src/videoprovider.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 679c861f1..a7de6d5a6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,6 +46,7 @@ set(QT_MODULES WebEngineQuick WebChannel WebEngineWidgets + Multimedia ) find_package(Qt6 COMPONENTS ${QT_MODULES} REQUIRED) foreach(MODULE ${QT_MODULES}) @@ -101,14 +102,11 @@ set(COMMON_SOURCES ${SRC_DIR}/main.cpp ${SRC_DIR}/smartlistmodel.cpp ${SRC_DIR}/utils.cpp - ${SRC_DIR}/rendermanager.cpp ${SRC_DIR}/mainapplication.cpp ${SRC_DIR}/messagesadapter.cpp ${SRC_DIR}/accountadapter.cpp ${SRC_DIR}/calladapter.cpp ${SRC_DIR}/conversationsadapter.cpp - ${SRC_DIR}/distantrenderer.cpp - ${SRC_DIR}/previewrenderer.cpp ${SRC_DIR}/avadapter.cpp ${SRC_DIR}/contactadapter.cpp ${SRC_DIR}/pluginadapter.cpp @@ -140,6 +138,7 @@ set(COMMON_SOURCES ${SRC_DIR}/currentaccount.cpp ${SRC_DIR}/videodevices.cpp ${SRC_DIR}/previewengine.cpp + ${SRC_DIR}/videoprovider.cpp ) set(COMMON_HEADERS @@ -152,7 +151,6 @@ set(COMMON_HEADERS ${SRC_DIR}/version.h ${SRC_DIR}/accountlistmodel.h ${SRC_DIR}/instancemanager.h - ${SRC_DIR}/rendermanager.h ${SRC_DIR}/connectivitymonitor.h ${SRC_DIR}/jamiavatartheme.h ${SRC_DIR}/mainapplication.h @@ -161,8 +159,6 @@ set(COMMON_HEADERS ${SRC_DIR}/accountadapter.h ${SRC_DIR}/calladapter.h ${SRC_DIR}/conversationsadapter.h - ${SRC_DIR}/distantrenderer.h - ${SRC_DIR}/previewrenderer.h ${SRC_DIR}/qmladapterbase.h ${SRC_DIR}/avadapter.h ${SRC_DIR}/contactadapter.h @@ -198,8 +194,20 @@ set(COMMON_HEADERS ${SRC_DIR}/currentaccount.h ${SRC_DIR}/videodevices.h ${SRC_DIR}/previewengine.h + ${SRC_DIR}/videoprovider.h ) +# For avframe dependency on Windows/macOS. +if(NOT DEFINED LIBAV_INCLUDE_PATH) + set(LIBJAMI_CONTRIB_DIR "${PROJECT_SOURCE_DIR}/../daemon/contrib/") + if(WIN32) + set(LIBAV_INCLUDE_PATH ${LIBJAMI_CONTRIB_DIR}/build/ffmpeg/Build/win32/x64/include/) + else() + set(LIBAV_INCLUDE_PATH ${LIBJAMI_CONTRIB_DIR}/native/ffmpeg) + endif() +endif() +include_directories(${LIBAV_INCLUDE_PATH}) + if(MSVC) set(WINDOWS_SYS_LIBS Shell32.lib diff --git a/qml.qrc b/qml.qrc index ce5c7ae71..9dca240a6 100644 --- a/qml.qrc +++ b/qml.qrc @@ -5,6 +5,8 @@ <file>src/constant/JamiQmlUtils.qml</file> <file>src/constant/JamiStrings.qml</file> <file>src/constant/JamiTheme.qml</file> + <file>src/commoncomponents/VideoView.qml</file> + <file>src/commoncomponents/LocalVideo.qml</file> <file>src/commoncomponents/SettingParaCombobox.qml</file> <file>src/commoncomponents/PreferenceItemDelegate.qml</file> <file>src/commoncomponents/PasswordDialog.qml</file> diff --git a/src/avadapter.cpp b/src/avadapter.cpp index 94c73db1e..c46836e9f 100644 --- a/src/avadapter.cpp +++ b/src/avadapter.cpp @@ -40,11 +40,10 @@ AvAdapter::AvAdapter(LRCInstance* instance, QObject* parent) &lrc::api::AVModel::audioDeviceEvent, this, &AvAdapter::onAudioDeviceEvent); - connect(&lrcInstance_->avModel(), &lrc::api::AVModel::rendererStarted, [this](const QString&) { - auto callId = lrcInstance_->getCurrentCallId(); - set_currentRenderingDeviceType( - lrcInstance_->getCurrentCallModel()->getCurrentRenderedDevice(callId).type); - }); + connect(&lrcInstance_->avModel(), + &lrc::api::AVModel::rendererStarted, + this, + &AvAdapter::onRendererStarted); } // The top left corner of primary screen is (0, 0). @@ -101,8 +100,6 @@ AvAdapter::shareEntireScreen(int screenNumber) resource, lrc::api::NewCallModel::MediaRequestType::SCREENSHARING, false); - set_currentRenderingDeviceType( - lrcInstance_->getCurrentCallModel()->getCurrentRenderedDevice(callId).type); } void @@ -122,14 +119,12 @@ AvAdapter::shareAllScreens() resource, lrc::api::NewCallModel::MediaRequestType::SCREENSHARING, false); - set_currentRenderingDeviceType( - lrcInstance_->getCurrentCallModel()->getCurrentRenderedDevice(callId).type); } void AvAdapter::captureScreen(int screenNumber) { - auto futureResult = QtConcurrent::run([this, screenNumber]() { + std::ignore = QtConcurrent::run([this, screenNumber]() { QScreen* screen = QGuiApplication::screens().at(screenNumber); if (!screen) return; @@ -149,7 +144,7 @@ AvAdapter::captureScreen(int screenNumber) void AvAdapter::captureAllScreens() { - auto futureResult = QtConcurrent::run([this]() { + std::ignore = QtConcurrent::run([this]() { auto screens = QGuiApplication::screens(); QList<QPixmap> scrs; @@ -191,8 +186,6 @@ AvAdapter::shareFile(const QString& filePath) filePath, lrc::api::NewCallModel::MediaRequestType::FILESHARING, false); - set_currentRenderingDeviceType( - lrcInstance_->getCurrentCallModel()->getCurrentRenderedDevice(callId).type); } } @@ -218,8 +211,6 @@ AvAdapter::shareScreenArea(unsigned x, unsigned y, unsigned width, unsigned heig resource, lrc::api::NewCallModel::MediaRequestType::SCREENSHARING, false); - set_currentRenderingDeviceType( - lrcInstance_->getCurrentCallModel()->getCurrentRenderedDevice(callId).type); }); #else auto resource = lrcInstance_->getCurrentCallModel()->getDisplay(getScreenNumber(), @@ -234,8 +225,6 @@ AvAdapter::shareScreenArea(unsigned x, unsigned y, unsigned width, unsigned heig resource, lrc::api::NewCallModel::MediaRequestType::SCREENSHARING, false); - set_currentRenderingDeviceType( - lrcInstance_->getCurrentCallModel()->getCurrentRenderedDevice(callId).type); #endif } @@ -250,8 +239,6 @@ AvAdapter::shareWindow(const QString& windowId) resource, lrc::api::NewCallModel::MediaRequestType::SCREENSHARING, false); - set_currentRenderingDeviceType( - lrcInstance_->getCurrentCallModel()->getCurrentRenderedDevice(callId).type); } QString @@ -304,7 +291,6 @@ AvAdapter::stopSharing() lrcInstance_->avModel().getCurrentVideoCaptureDevice(), lrc::api::NewCallModel::MediaRequestType::CAMERA, muteCamera_); - set_currentRenderingDeviceType(lrc::api::video::DeviceType::CAMERA); } } @@ -329,6 +315,16 @@ AvAdapter::onAudioDeviceEvent() Q_EMIT audioDeviceListChanged(inputs, outputs); } +void +AvAdapter::onRendererStarted(const QString& id) +{ + auto callId = lrcInstance_->getCurrentCallId(); + auto callModel = lrcInstance_->getCurrentCallModel(); + auto renderDevice = callModel->getCurrentRenderedDevice(callId); + set_currentRenderingDeviceId(id); + set_currentRenderingDeviceType(renderDevice.type); +} + int AvAdapter::getScreenNumber() const { diff --git a/src/avadapter.h b/src/avadapter.h index c10e725e5..a21dfad3f 100644 --- a/src/avadapter.h +++ b/src/avadapter.h @@ -29,7 +29,10 @@ class AvAdapter final : public QmlAdapterBase { Q_OBJECT + // TODO: currentRenderingDeviceType is only used in QML to check if + // we're sharing or not, so it should maybe just be a boolean. QML_RO_PROPERTY(lrc::api::video::DeviceType, currentRenderingDeviceType) + QML_RO_PROPERTY(QString, currentRenderingDeviceId) QML_PROPERTY(bool, muteCamera) QML_RO_PROPERTY(QStringList, windowsNames) QML_RO_PROPERTY(QList<QVariant>, windowsIds) @@ -90,6 +93,7 @@ protected: private Q_SLOTS: void onAudioDeviceEvent(); + void onRendererStarted(const QString& id); private: // Get screens arrangement rect relative to primary screen. diff --git a/src/calladapter.cpp b/src/calladapter.cpp index 27c23404b..3f4c1773b 100644 --- a/src/calladapter.cpp +++ b/src/calladapter.cpp @@ -185,7 +185,6 @@ CallAdapter::onCallStatusChanged(const QString& callId, int code) case lrc::api::call::Status::PEER_BUSY: case lrc::api::call::Status::TIMEOUT: case lrc::api::call::Status::TERMINATING: { - lrcInstance_->renderer()->removeDistantRenderer(callId); const auto& convInfo = lrcInstance_->getConversationFromCallId(callId); if (convInfo.uid.isEmpty()) { return; @@ -285,7 +284,7 @@ void CallAdapter::onCallAddedToConference(const QString& callId, const QString& confId) { Q_UNUSED(callId) - lrcInstance_->renderer()->addDistantRenderer(confId); + Q_UNUSED(confId) saveConferenceSubcalls(); } @@ -450,9 +449,9 @@ CallAdapter::updateCall(const QString& convUid, const QString& accountId, bool f if (convInfo.uid == lrcInstance_->get_selectedConvUid()) { auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId_); - if (accInfo.profileInfo.type != lrc::api::profile::Type::SIP) + if (accInfo.profileInfo.type != lrc::api::profile::Type::SIP) { accInfo.callModel->setCurrentCall(call->id); - lrcInstance_->renderer()->addDistantRenderer(call->id); + } } updateCallOverlay(convInfo); diff --git a/src/commoncomponents/LocalVideo.qml b/src/commoncomponents/LocalVideo.qml new file mode 100644 index 000000000..caca6c081 --- /dev/null +++ b/src/commoncomponents/LocalVideo.qml @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2022 Savoir-faire Linux Inc. + * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> + * + * 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 3 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, see <http://www.gnu.org/licenses/>. + */ + +import QtQuick +import QtMultimedia +import Qt5Compat.GraphicalEffects + +import net.jami.Adapters 1.1 + +VideoView { + id: root + + crop: true + + function startWithId(id, force = false) { + if (id.length === 0) { + VideoDevices.stopDevice(rendererId) + rendererId = id + } else { + rendererId = VideoDevices.startDevice(id, force) + } + } + + onVisibleChanged: { + const id = visible ? VideoDevices.getDefaultDevice() : "" + startWithId(id) + } +} + + diff --git a/src/commoncomponents/PhotoboothView.qml b/src/commoncomponents/PhotoboothView.qml index c9b7137bf..828ab91fa 100644 --- a/src/commoncomponents/PhotoboothView.qml +++ b/src/commoncomponents/PhotoboothView.qml @@ -39,8 +39,7 @@ Item { height: boothLayout.height function startBooth() { - preview.deviceId = VideoDevices.getDefaultDevice() - preview.rendererId = VideoDevices.startDevice(preview.deviceId) + preview.startWithId(VideoDevices.getDefaultDevice()) isPreviewing = true } @@ -130,17 +129,19 @@ Item { showPresenceIndicator: false } - PhotoboothPreviewRender { + LocalVideo { id: preview anchors.fill: parent anchors.margins: 1 - property string deviceId: VideoDevices.getDefaultDevice() - rendererId: "" - visible: isPreviewing - lrcInstance: LRCInstance + + rendererId: VideoDevices.getDefaultDevice() + + function takePhoto() { + return videoProvider.captureVideoFrame(videoSink) + } layer.enabled: true layer.effect: OpacityMask { @@ -150,11 +151,6 @@ Item { radius: avatarSize / 2 } } - - onRenderingStopped: { - if (root.visible) - stopBooth() - } } Rectangle { diff --git a/src/commoncomponents/VideoView.qml b/src/commoncomponents/VideoView.qml new file mode 100644 index 000000000..ecfb5c3d8 --- /dev/null +++ b/src/commoncomponents/VideoView.qml @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2022 Savoir-faire Linux Inc. + * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> + * + * 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 3 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, see <http://www.gnu.org/licenses/>. + */ + +import QtQuick +import QtMultimedia +import Qt5Compat.GraphicalEffects + +Item { + id: root + + property string rendererId + property alias videoSink: videoOutput.videoSink + property alias underlayItems: rootUnderlayItem.children + property real invAspectRatio: (videoOutput.sourceRect.height + / videoOutput.sourceRect.width) || + 0.5625 // 16:9 default + property bool crop: false + + // This rect describes the actual rendered content rectangle + // as the VideoOutput component may use PreserveAspectFit + // (pillarbox/letterbox). + property rect contentRect: videoOutput.contentRect + 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) + } + } + + Rectangle { + id: bgRect + anchors.fill: parent + color: "black" + } + + Item { + id: rootUnderlayItem + anchors.fill: parent + } + + VideoOutput { + id: videoOutput + + antialiasing: true + anchors.fill: parent + opacity: videoProvider.activeRenderers[rendererId] !== undefined + visible: opacity + + fillMode: crop ? + VideoOutput.PreserveAspectCrop : + VideoOutput.PreserveAspectFit + + Behavior on opacity { NumberAnimation { duration: 150 } } + + Component.onDestruction: videoProvider.unregisterSink(videoSink) + + layer.enabled: opacity + layer.effect: FastBlur { + source: videoOutput + anchors.fill: root + radius: (1. - opacity) * 100 + } + } +} diff --git a/src/constant/JamiStrings.qml b/src/constant/JamiStrings.qml index 38b7a41df..722f829ae 100644 --- a/src/constant/JamiStrings.qml +++ b/src/constant/JamiStrings.qml @@ -184,6 +184,7 @@ Item { property string previewUnavailable: qsTr("Preview unavailable") property string screenSharing: qsTr("Screen Sharing") property string selectScreenSharingFPS: qsTr("Select screen sharing frame rate (frames per second)") + property string noVideo: qsTr("no video") // BackupKeyPage property string backupAccountInfos: qsTr("Your account only exists on this device. " + diff --git a/src/distantrenderer.cpp b/src/distantrenderer.cpp deleted file mode 100644 index f98163061..000000000 --- a/src/distantrenderer.cpp +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright (C) 2019-2022 Savoir-faire Linux Inc. - * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> - * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com> - * - * 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 3 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, see <http://www.gnu.org/licenses/>. - */ - -#include "distantrenderer.h" - -#include "lrcinstance.h" - -DistantRenderer::DistantRenderer(QQuickItem* parent) - : QQuickPaintedItem(parent) -{ - setAntialiasing(true); - setFillColor(Qt::black); - setRenderTarget(QQuickPaintedItem::FramebufferObject); - setPerformanceHint(QQuickPaintedItem::FastFBOResizing); - - connect(this, &DistantRenderer::lrcInstanceChanged, [this] { - if (lrcInstance_) { - connect(lrcInstance_->renderer(), - &RenderManager::distantFrameUpdated, - [this](const QString& id) { - if (rendererId_ == id) - update(QRect(0, 0, width(), height())); - }); - - connect(lrcInstance_->renderer(), - &RenderManager::distantRenderingStopped, - [this](const QString& id) { - if (rendererId_ == id) - update(QRect(0, 0, width(), height())); - }); - } - }); -} - -DistantRenderer::~DistantRenderer() {} - -void -DistantRenderer::setRendererId(const QString& id) -{ - rendererId_ = id; - // Note: Force a paint to update frame as we change the renderer - update(QRect(0, 0, width(), height())); -} - -QString -DistantRenderer::rendererId() -{ - return rendererId_; -} - -QString -DistantRenderer::takePhoto(int size) -{ - if (auto previewImage = lrcInstance_->renderer()->getPreviewFrame(rendererId_)) { - return Utils::byteArrayToBase64String(Utils::QImageToByteArray(previewImage->copy())); - } - return {}; -} - -int -DistantRenderer::getXOffset() const -{ - return xOffset_; -} - -int -DistantRenderer::getYOffset() const -{ - return yOffset_; -} - -double -DistantRenderer::getScaledWidth() const -{ - return scaledWidth_; -} - -double -DistantRenderer::getScaledHeight() const -{ - return scaledHeight_; -} - -void -DistantRenderer::paint(QPainter* painter) -{ - lrcInstance_->renderer()->drawFrame(rendererId_, [this, painter](QImage* distantImage) { - if (distantImage) { - painter->setRenderHint(QPainter::Antialiasing); - painter->setRenderHint(QPainter::SmoothPixmapTransform); -#if defined(Q_OS_MACOS) - - auto scaledDistant = distantImage - ->scaled(size().toSize(), - Qt::KeepAspectRatio, - Qt::SmoothTransformation) - .rgbSwapped(); -#else - auto scaledDistant = distantImage->scaled(size().toSize(), - Qt::KeepAspectRatio, - Qt::SmoothTransformation); -#endif - auto tempScaledWidth = static_cast<int>(scaledWidth_ * 1000); - auto tempScaledHeight = static_cast<int>(scaledHeight_ * 1000); - auto tempXOffset = xOffset_; - auto tempYOffset = yOffset_; - scaledWidth_ = static_cast<double>(scaledDistant.width()) - / static_cast<double>(distantImage->width()); - scaledHeight_ = static_cast<double>(scaledDistant.height()) - / static_cast<double>(distantImage->height()); - xOffset_ = (width() - scaledDistant.width()) / 2; - yOffset_ = (height() - scaledDistant.height()) / 2; - if (tempXOffset != xOffset_ or tempYOffset != yOffset_ - or static_cast<int>(scaledWidth_ * 1000) != tempScaledWidth - or static_cast<int>(scaledHeight_ * 1000) != tempScaledHeight) { - Q_EMIT offsetChanged(); - } - painter->drawImage(QRect(xOffset_, - yOffset_, - scaledDistant.width(), - scaledDistant.height()), - scaledDistant); - } - }); -} diff --git a/src/distantrenderer.h b/src/distantrenderer.h deleted file mode 100644 index 747460021..000000000 --- a/src/distantrenderer.h +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2020-2022 Savoir-faire Linux Inc. - * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> - * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com> - * - * 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 3 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, see <http://www.gnu.org/licenses/>. - */ - -#pragma once - -#include <QtQuick> - -class LRCInstance; - -/* - * Use QQuickPaintedItem so that QPainter apis can be used. - * Note: Old video pipeline. - */ -class DistantRenderer : public QQuickPaintedItem -{ - Q_OBJECT - Q_PROPERTY(LRCInstance* lrcInstance MEMBER lrcInstance_ NOTIFY lrcInstanceChanged) - Q_PROPERTY(QString rendererId READ rendererId WRITE setRendererId) - -public: - explicit DistantRenderer(QQuickItem* parent = nullptr); - ~DistantRenderer(); - - Q_INVOKABLE void setRendererId(const QString& id); - Q_INVOKABLE QString rendererId(); - Q_INVOKABLE int getXOffset() const; - Q_INVOKABLE int getYOffset() const; - Q_INVOKABLE double getScaledWidth() const; - Q_INVOKABLE double getScaledHeight() const; - Q_INVOKABLE QString takePhoto(int size); - -Q_SIGNALS: - void offsetChanged(); - void lrcInstanceChanged(); - -protected: - // LRCInstance pointer (set in qml) - LRCInstance* lrcInstance_ {nullptr}; - -private: - void paint(QPainter* painter); - - /* - * Unique DistantRenderId for each call. - */ - QString rendererId_; - int xOffset_ {0}; - int yOffset_ {0}; - double scaledWidth_ {0}; - double scaledHeight_ {0}; -}; diff --git a/src/lrcinstance.cpp b/src/lrcinstance.cpp index 2318d37cf..619f506a8 100644 --- a/src/lrcinstance.cpp +++ b/src/lrcinstance.cpp @@ -33,7 +33,6 @@ LRCInstance::LRCInstance(migrateCallback willMigrateCb, ConnectivityMonitor* connectivityMonitor, bool muteDring) : lrc_(std::make_unique<Lrc>(willMigrateCb, didMigrateCb, muteDring)) - , renderer_(std::make_unique<RenderManager>(lrc_->getAVModel())) , updateManager_(std::make_unique<UpdateManager>(updateUrl, connectivityMonitor, this)) , threadPool_(new QThreadPool(this)) { @@ -68,12 +67,6 @@ LRCInstance::LRCInstance(migrateCallback willMigrateCb, } }; -RenderManager* -LRCInstance::renderer() -{ - return renderer_.get(); -} - UpdateManager* LRCInstance::getUpdateManager() { @@ -400,7 +393,6 @@ LRCInstance::makeConversationPermanent(const QString& convId, const QString& acc void LRCInstance::finish() { - renderer_.reset(); lrc_.reset(); } diff --git a/src/lrcinstance.h b/src/lrcinstance.h index 915a3dc36..1421a2a34 100644 --- a/src/lrcinstance.h +++ b/src/lrcinstance.h @@ -25,7 +25,6 @@ #endif #include "updatemanager.h" -#include "rendermanager.h" #include "qtutils.h" #include "utils.h" @@ -71,7 +70,6 @@ public: void finish(); - RenderManager* renderer(); UpdateManager* getUpdateManager(); NewAccountModel& accountModel(); @@ -138,7 +136,6 @@ Q_SIGNALS: private: std::unique_ptr<Lrc> lrc_; - std::unique_ptr<RenderManager> renderer_; std::unique_ptr<UpdateManager> updateManager_; QString selectedConvUid_; diff --git a/src/mainapplication.cpp b/src/mainapplication.cpp index 3c526b93b..8865f834c 100644 --- a/src/mainapplication.cpp +++ b/src/mainapplication.cpp @@ -26,6 +26,7 @@ #include "connectivitymonitor.h" #include "systemtray.h" #include "previewengine.h" +#include "videoprovider.h" #include <QAction> #include <QCommandLineParser> @@ -386,6 +387,9 @@ MainApplication::initQmlLayer() &screenInfo_, this); + auto videoProvider = new VideoProvider(lrcInstance_->avModel(), this); + engine_->rootContext()->setContextProperty("videoProvider", videoProvider); + engine_->load(QUrl(QStringLiteral("qrc:/src/MainApplicationWindow.qml"))); } diff --git a/src/mainview/components/OngoingCallPage.qml b/src/mainview/components/OngoingCallPage.qml index 13f6ded2f..0a391aff2 100644 --- a/src/mainview/components/OngoingCallPage.qml +++ b/src/mainview/components/OngoingCallPage.qml @@ -44,6 +44,13 @@ Rectangle { property alias callId: distantRenderer.rendererId property var linkedWebview: null property string callPreviewId: "" + property bool sharingActive: AvAdapter.currentRenderingDeviceType === Video.DeviceType.DISPLAY + || AvAdapter.currentRenderingDeviceType === Video.DeviceType.FILE + + onSharingActiveChanged: { + const deviceId = AvAdapter.currentRenderingDeviceId + previewRenderer.startWithId(deviceId, true) + } color: "black" @@ -166,36 +173,35 @@ Rectangle { callOverlay.openCallViewContextMenuInPos(mouse.x, mouse.y) } - DistantRenderer { + VideoView { id: distantRenderer anchors.centerIn: parent anchors.fill: parent z: -1 - lrcInstance: LRCInstance visible: !root.isAudioOnly - onOffsetChanged: { + // Update overlays if the internal or visual geometry changes. + // Use Qt.callLater to combine the events in the queue since these + // signals can be emitted together. + property real area: width * height + function updateParticipantsLayer() { callOverlay.participantsLayer.update(CallAdapter.getConferencesInfos()) } + onAreaChanged: Qt.callLater(updateParticipantsLayer) + onContentRectChanged: Qt.callLater(updateParticipantsLayer) } - VideoCallPreviewRenderer { + LocalVideo { id: previewRenderer - lrcInstance: LRCInstance visible: !callOverlay.isAudioOnly && !callOverlay.isConferenceCall && !callOverlay.isVideoMuted && !callOverlay.isPaused && ((VideoDevices.listSize !== 0 && AvAdapter.currentRenderingDeviceType === Video.DeviceType.CAMERA) || AvAdapter.currentRenderingDeviceType !== Video.DeviceType.CAMERA ) rendererId: root.callPreviewId - onVisibleChanged: { - if (!visible && !AccountAdapter.hasVideoCall()) { - VideoDevices.stopDevice(rendererId, true) - } - } - + height: width * invAspectRatio width: Math.max(callPageMainRect.width / 5, JamiTheme.minimumPreviewWidth) x: callPageMainRect.width - previewRenderer.width - previewMargin y: previewMarginYTop @@ -262,13 +268,6 @@ Rectangle { radius: JamiTheme.primaryRadius } } - - onWidthChanged: { - previewRenderer.height = previewRenderer.width * previewImageScalingFactor - } - onPreviewImageScalingFactorChanged: { - previewRenderer.height = previewRenderer.width * previewImageScalingFactor - } } CallOverlay { diff --git a/src/mainview/components/ParticipantsLayer.qml b/src/mainview/components/ParticipantsLayer.qml index 10aa2b116..301652434 100644 --- a/src/mainview/components/ParticipantsLayer.qml +++ b/src/mainview/components/ParticipantsLayer.qml @@ -27,10 +27,10 @@ Item { // returns true if participant is not fully maximized function showMaximize(pX, pY, pW, pH) { // Hack: -1 offset added to avoid problems with odd sizes - return (pX - distantRenderer.getXOffset() !== 0 - || pY - distantRenderer.getYOffset() !== 0 - || pW < (distantRenderer.width - distantRenderer.getXOffset() * 2 - 1) - || pH < (distantRenderer.height - distantRenderer.getYOffset() * 2 - 1)) + return (pX - distantRenderer.contentRect.x !== 0 + || pY - distantRenderer.contentRect.y !== 0 + || pW < (distantRenderer.width - distantRenderer.contentRect.x * 2 - 1) + || pH < (distantRenderer.height - distantRenderer.contentRect.y * 2 - 1)) } function update(infos) { @@ -48,25 +48,13 @@ Item { var participant = infos.find(e => e.uri === participantOverlays[p].uri); if (participant) { // Update participant's information - var newX = Math.trunc(distantRenderer.getXOffset() - + participant.x * distantRenderer.getScaledWidth()) - var newY = Math.trunc(distantRenderer.getYOffset() - + participant.y * distantRenderer.getScaledHeight()) - - var newWidth = Math.ceil(participant.w * distantRenderer.getScaledWidth()) - var newHeight = Math.ceil(participant.h * distantRenderer.getScaledHeight()) - - var newVisible = participant.w !== 0 && participant.h !== 0 - if (participantOverlays[p].x !== newX) - participantOverlays[p].x = newX - if (participantOverlays[p].y !== newY) - participantOverlays[p].y = newY - if (participantOverlays[p].width !== newWidth) - participantOverlays[p].width = newWidth - if (participantOverlays[p].height !== newHeight) - participantOverlays[p].height = newHeight - if (participantOverlays[p].visible !== newVisible) - participantOverlays[p].visible = newVisible + participantOverlays[p].x = Math.trunc(distantRenderer.contentRect.x + + participant.x * distantRenderer.xScale) + participantOverlays[p].y = Math.trunc(distantRenderer.contentRect.y + + participant.y * distantRenderer.yScale) + participantOverlays[p].width = Math.ceil(participant.w * distantRenderer.xScale) + participantOverlays[p].height = Math.ceil(participant.h * distantRenderer.yScale) + participantOverlays[p].visible = participant.w !== 0 && participant.h !== 0 showMax = showMaximize(participantOverlays[p].x, participantOverlays[p].y, @@ -100,13 +88,16 @@ Item { for (var infoVariant in infos) { // Only create overlay for new participants if (!currentUris.includes(infos[infoVariant].uri)) { - var hover = participantComponent.createObject(root, { - x: Math.trunc(distantRenderer.getXOffset() + infos[infoVariant].x * distantRenderer.getScaledWidth()), - y: Math.trunc(distantRenderer.getYOffset() + infos[infoVariant].y * distantRenderer.getScaledHeight()), - width: Math.ceil(infos[infoVariant].w * distantRenderer.getScaledWidth()), - height: Math.ceil(infos[infoVariant].h * distantRenderer.getScaledHeight()), - visible: infos[infoVariant].w !== 0 && infos[infoVariant].h !== 0 - }) + const infoObj = { + x: Math.trunc(distantRenderer.contentRect.x + + infos[infoVariant].x * distantRenderer.xScale), + y: Math.trunc(distantRenderer.contentRect.y + + infos[infoVariant].y * distantRenderer.yScale), + width: Math.ceil(infos[infoVariant].w * distantRenderer.xScale), + height: Math.ceil(infos[infoVariant].h * distantRenderer.yScale), + visible: infos[infoVariant].w !== 0 && infos[infoVariant].h !== 0 + } + var hover = participantComponent.createObject(root, infoObj) if (!hover) { console.log("Error when creating the hover") return diff --git a/src/mainview/components/RecordBox.qml b/src/mainview/components/RecordBox.qml index 9b160957c..75679e3c6 100644 --- a/src/mainview/components/RecordBox.qml +++ b/src/mainview/components/RecordBox.qml @@ -58,8 +58,7 @@ Rectangle { updateState(RecordBox.States.INIT) if (isVideo) { - previewWidget.deviceId = VideoDevices.getDefaultDevice() - previewWidget.rendererId = VideoDevices.startDevice(previewWidget.deviceId) + previewWidget.startWithId(VideoDevices.getDefaultDevice()) } } @@ -245,29 +244,19 @@ Rectangle { color: JamiTheme.blackColor radius: 5 - PreviewRenderer { + LocalVideo { id: previewWidget anchors.fill: rectBox - anchors.centerIn: rectBox - property string deviceId: VideoDevices.getDefaultDevice() - rendererId: VideoDevices.getDefaultDevice() + anchors.margins: 1 - lrcInstance: LRCInstance + rendererId: VideoDevices.getDefaultDevice() layer.enabled: true layer.effect: OpacityMask { maskSource: rectBox } } - - onVisibleChanged: { - if (visible) { - previewWidget.deviceId = VideoDevices.getDefaultDevice() - previewWidget.rendererId = VideoDevices.startDevice(previewWidget.deviceId) - } else - VideoDevices.stopDevice(previewWidget.deviceId) - } } Label { diff --git a/src/mainview/components/SelectScreen.qml b/src/mainview/components/SelectScreen.qml index f6e362e7d..517b47eb4 100644 --- a/src/mainview/components/SelectScreen.qml +++ b/src/mainview/components/SelectScreen.qml @@ -1,7 +1,7 @@ /* * Copyright (C) 2020-2022 Savoir-faire Linux Inc. * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com> - * Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com> + * Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com> * * 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 @@ -50,11 +50,12 @@ Window { // How many rows the ScrollView should have. function calculateRepeaterModel() { screens = [] - for (var idx in Qt.application.screens) { + var idx + for (idx in Qt.application.screens) { screens.push(qsTr("Screen") + " " + idx) } AvAdapter.getListWindows() - for (var idx in AvAdapter.windowsNames) { + for (idx in AvAdapter.windowsNames) { screens.push(AvAdapter.windowsNames[idx]) } @@ -153,7 +154,7 @@ Window { color: JamiTheme.textColor } - PreviewRenderer { + VideoView { id: screenPreview anchors.top: screenName.bottom @@ -162,17 +163,17 @@ Window { height: screenItem.height - 50 width: screenItem.width - 50 - lrcInstance: LRCInstance - Component.onDestruction: { - if (screenPreview.rendererId !== "" && screenPreview.rendererId !== currentPreview) - VideoDevices.stopDevice(screenPreview.rendererId, true) + if (rendererId !== "" && rendererId !== currentPreview) { + VideoDevices.stopDevice(rendererId, true) + } } Component.onCompleted: { if (visible) { - var rendId = AvAdapter.getSharingResource(index, "") - if (rendId !== "") - screenPreview.rendererId = VideoDevices.startDevice(rendId, true) + const rId = AvAdapter.getSharingResource(index, "") + if (rId !== "") { + rendererId = VideoDevices.startDevice(rId, true) + } } } } @@ -226,7 +227,7 @@ Window { color: JamiTheme.textColor } - PreviewRenderer { + VideoView { id: screenShotAll anchors.top: screenNameAll.bottom @@ -235,17 +236,17 @@ Window { height: screenSelectionRectAll.height - 50 width: screenSelectionRectAll.width - 50 - lrcInstance: LRCInstance - Component.onDestruction: { - if (screenShotAll.rendererId !== "" && screenShotAll.rendererId !== currentPreview) - VideoDevices.stopDevice(screenShotAll.rendererId, true) + if (rendererId !== "" && rendererId !== currentPreview) { + VideoDevices.stopDevice(rendererId, true) + } } Component.onCompleted: { if (visible) { - var rendId = AvAdapter.getSharingResource(-1, "") - if (rendId !== "") - screenShotAll.rendererId = VideoDevices.startDevice(rendId, true) + const rId = AvAdapter.getSharingResource(-1, "") + if (rId !== "") { + rendererId = VideoDevices.startDevice(rId, true) + } } } } @@ -305,7 +306,7 @@ Window { color: JamiTheme.textColor } - PreviewRenderer { + VideoView { id: screenPreview2 anchors.top: screenName2.bottom @@ -316,17 +317,17 @@ Window { height: screenItem2.height - 60 width: screenItem2.width - 50 - lrcInstance: LRCInstance - Component.onDestruction: { - if (screenPreview2.rendererId !== "" && screenPreview2.rendererId !== currentPreview) - VideoDevices.stopDevice(screenPreview2.rendererId, true) + if (rendererId !== "" && rendererId !== currentPreview) { + VideoDevices.stopDevice(rendererId, true) + } } Component.onCompleted: { if (visible) { - var rendId = AvAdapter.getSharingResource(-2, AvAdapter.windowsIds[index - Qt.application.screens.length]) - if (rendId !== "") - screenPreview2.rendererId = VideoDevices.startDevice(rendId, true) + const rId = AvAdapter.getSharingResource(-2, AvAdapter.windowsIds[index - Qt.application.screens.length]) + if (rId !== "") { + rendererId = VideoDevices.startDevice(rId, true) + } } } } diff --git a/src/previewrenderer.cpp b/src/previewrenderer.cpp deleted file mode 100644 index 7c419fa1f..000000000 --- a/src/previewrenderer.cpp +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright (C) 2020-2022 Savoir-faire Linux Inc. - * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> - * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com> - * - * 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 3 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, see <http://www.gnu.org/licenses/>. - */ - -#include "previewrenderer.h" - -PreviewRenderer::PreviewRenderer(QQuickItem* parent) - : QQuickPaintedItem(parent) -{ - setAntialiasing(true); - setFillColor(Qt::black); - setRenderTarget(QQuickPaintedItem::FramebufferObject); - setPerformanceHint(QQuickPaintedItem::FastFBOResizing); - - connect(this, &PreviewRenderer::lrcInstanceChanged, [this] { - if (lrcInstance_) - previewFrameUpdatedConnection_ = connect(lrcInstance_->renderer(), - &RenderManager::distantFrameUpdated, - [this](const QString& id) { - if (rendererId_ == id && isVisible()) - update(QRect(0, 0, width(), height())); - }); - }); -} - -PreviewRenderer::~PreviewRenderer() -{ - disconnect(previewFrameUpdatedConnection_); -} - -void -PreviewRenderer::paint(QPainter* painter) -{ - lrcInstance_->renderer()->drawFrame(rendererId_, [this, painter](QImage* previewImage) { - if (previewImage) { - painter->setRenderHint(QPainter::Antialiasing); - painter->setRenderHint(QPainter::SmoothPixmapTransform); - - auto aspectRatio = static_cast<qreal>(previewImage->width()) - / static_cast<qreal>(previewImage->height()); - auto previewHeight = aspectRatio < 1 ? height() : width() / aspectRatio; - auto previewWidth = aspectRatio < 1 ? previewHeight * aspectRatio : width(); - - /* Instead of setting fixed size, we could get an x offset for the preview - * but this would render the horizontal spacers in the parent widget useless. - * e.g. - * auto parent = qobject_cast<QWidget*>(this->parent()); - * auto xPos = (parent->width() - previewWidth) / 2; - * setGeometry(QRect(QPoint(xPos, this->pos().y()), - * QSize(previewWidth, previewHeight))); - */ - setWidth(previewWidth); - setHeight(previewHeight); - - // If the given size is empty, this function returns a null image. - QImage scaledPreview; -#if defined(Q_OS_MACOS) - - scaledPreview = previewImage - ->scaled(size().toSize(), - Qt::KeepAspectRatio, - Qt::SmoothTransformation) - .rgbSwapped(); -#else - scaledPreview = previewImage->scaled(size().toSize(), - Qt::KeepAspectRatio, - Qt::SmoothTransformation); -#endif - painter->drawImage(QRect(0, 0, scaledPreview.width(), scaledPreview.height()), - scaledPreview); - } else { - paintBackground(painter); - } - }); -} - -void -PreviewRenderer::paintBackground(QPainter* painter) -{ - QBrush brush(Qt::black); - QPainterPath path; - path.addRect(QRect(0, 0, width(), height())); - painter->fillPath(path, brush); -} - -VideoCallPreviewRenderer::VideoCallPreviewRenderer(QQuickItem* parent) - : PreviewRenderer(parent) -{ - setProperty("previewImageScalingFactor", 1.0); -} - -VideoCallPreviewRenderer::~VideoCallPreviewRenderer() {} - -void -VideoCallPreviewRenderer::paint(QPainter* painter) -{ - lrcInstance_->renderer()->drawFrame(get_rendererId(), [this, painter](QImage* previewImage) { - if (previewImage) { - auto scalingFactor = static_cast<qreal>(previewImage->height()) - / static_cast<qreal>(previewImage->width()); - setProperty("previewImageScalingFactor", scalingFactor); - QImage scaledPreview; -#if defined(Q_OS_MACOS) - scaledPreview = previewImage->scaled(size().toSize(), Qt::KeepAspectRatio).rgbSwapped(); -#else - scaledPreview = previewImage->scaled(size().toSize(), Qt::KeepAspectRatio); -#endif - painter->drawImage(QRect(0, 0, scaledPreview.width(), scaledPreview.height()), - scaledPreview); - } - }); -} - -PhotoboothPreviewRender::PhotoboothPreviewRender(QQuickItem* parent) - : PreviewRenderer(parent) -{ - connect(this, &PreviewRenderer::lrcInstanceChanged, [this] { - if (lrcInstance_) - connect(lrcInstance_->renderer(), - &RenderManager::distantRenderingStopped, - this, - &PhotoboothPreviewRender::renderingStopped, - Qt::UniqueConnection); - }); -} - -QString -PhotoboothPreviewRender::takePhoto(int size) -{ - if (auto previewImage = lrcInstance_->renderer()->getPreviewFrame(get_rendererId())) { -#if defined(Q_OS_MACOS) - return Utils::byteArrayToBase64String( - Utils::QImageToByteArray(previewImage->copy().rgbSwapped())); -#else - return Utils::byteArrayToBase64String(Utils::QImageToByteArray(previewImage->copy())); -#endif - } - return {}; -} - -void -PhotoboothPreviewRender::paint(QPainter* painter) -{ - painter->setRenderHint(QPainter::Antialiasing, true); - - lrcInstance_->renderer()->drawFrame(get_rendererId(), [this, painter](QImage* previewImage) { - if (previewImage) { - QImage scaledPreview; -#if defined(Q_OS_MACOS) - - scaledPreview = Utils::getCirclePhoto(*previewImage, - height() <= width() ? height() : width()) - .rgbSwapped(); -#else - scaledPreview = Utils::getCirclePhoto(*previewImage, - height() <= width() ? height() : width()); -#endif - painter->drawImage(QRect(0, 0, scaledPreview.width(), scaledPreview.height()), - scaledPreview); - } - }); -} diff --git a/src/previewrenderer.h b/src/previewrenderer.h deleted file mode 100644 index 1e78fe087..000000000 --- a/src/previewrenderer.h +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (C) 2020-2022 Savoir-faire Linux Inc. - * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> - * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com> - * - * 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 3 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, see <https://www.gnu.org/licenses/>. - */ - -#pragma once - -#include <QtQuick> - -#include "lrcinstance.h" - -/* - * Use QQuickPaintedItem so that QPainter apis can be used. - * Note: Old video pipeline. - */ -class PreviewRenderer : public QQuickPaintedItem -{ - Q_OBJECT - Q_PROPERTY(LRCInstance* lrcInstance MEMBER lrcInstance_ NOTIFY lrcInstanceChanged) - QML_PROPERTY(QString, rendererId); - -public: - explicit PreviewRenderer(QQuickItem* parent = nullptr); - virtual ~PreviewRenderer(); - -Q_SIGNALS: - void lrcInstanceChanged(); - -protected: - void paint(QPainter* painter) override; - void paintBackground(QPainter* painter); - - // LRCInstance pointer (set in qml) - LRCInstance* lrcInstance_ {nullptr}; - -private: - QMetaObject::Connection previewFrameUpdatedConnection_; -}; - -class VideoCallPreviewRenderer : public PreviewRenderer -{ - Q_OBJECT - Q_PROPERTY(qreal previewImageScalingFactor MEMBER previewImageScalingFactor_ NOTIFY - previewImageScalingFactorChanged) -public: - explicit VideoCallPreviewRenderer(QQuickItem* parent = nullptr); - ~VideoCallPreviewRenderer(); - -Q_SIGNALS: - void previewImageScalingFactorChanged(); - -private: - void paint(QPainter* painter) override final; - - qreal previewImageScalingFactor_; -}; - -class PhotoboothPreviewRender : public PreviewRenderer -{ - Q_OBJECT -public: - explicit PhotoboothPreviewRender(QQuickItem* parent = nullptr); - ~PhotoboothPreviewRender() = default; - - Q_INVOKABLE QString takePhoto(int size); - -Q_SIGNALS: - void renderingStopped(const QString id); - -private: - void paint(QPainter* painter) override final; -}; diff --git a/src/qmlregister.cpp b/src/qmlregister.cpp index fd4dab122..527d7ccd2 100644 --- a/src/qmlregister.cpp +++ b/src/qmlregister.cpp @@ -48,11 +48,9 @@ #include "avatarregistry.h" #include "appsettingsmanager.h" #include "mainapplication.h" -#include "distantrenderer.h" #include "namedirectory.h" #include "updatemanager.h" #include "pluginlistpreferencemodel.h" -#include "previewrenderer.h" #include "version.h" #include "wizardviewstepmodel.h" @@ -167,12 +165,6 @@ registerTypes(QQmlEngine* engine, QML_REGISTERNAMESPACE(NS_MODELS, FilesToSend::staticMetaObject, "FilesToSend"); QML_REGISTERNAMESPACE(NS_MODELS, MessageList::staticMetaObject, "MessageList"); - // QQuickItems - QML_REGISTERTYPE(NS_MODELS, PreviewRenderer); - QML_REGISTERTYPE(NS_MODELS, VideoCallPreviewRenderer); - QML_REGISTERTYPE(NS_MODELS, DistantRenderer); - QML_REGISTERTYPE(NS_MODELS, PhotoboothPreviewRender) - // Qml singleton components QML_REGISTERSINGLETONTYPE_URL(NS_CONSTANTS, "qrc:/src/constant/JamiTheme.qml", JamiTheme); QML_REGISTERSINGLETONTYPE_URL(NS_MODELS, "qrc:/src/constant/JamiQmlUtils.qml", JamiQmlUtils); diff --git a/src/rendermanager.cpp b/src/rendermanager.cpp deleted file mode 100644 index cea11bc52..000000000 --- a/src/rendermanager.cpp +++ /dev/null @@ -1,321 +0,0 @@ -/* - * Copyright (C) 2019-2022 Savoir-faire Linux Inc. - * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> - * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com> - * - * 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 3 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, see <http://www.gnu.org/licenses/>. - */ - -#include "rendermanager.h" - -#include <stdexcept> - -using namespace lrc::api; - -FrameWrapper::FrameWrapper(AVModel& avModel, const QString& id) - : avModel_(avModel) - , id_(id) - , isRendering_(false) -{} - -FrameWrapper::~FrameWrapper() -{ - avModel_.stopPreview(id_); -} - -void -FrameWrapper::connectStartRendering() -{ - QObject::disconnect(renderConnections_.started); - renderConnections_.started = QObject::connect(&avModel_, - &AVModel::rendererStarted, - this, - &FrameWrapper::slotRenderingStarted); -} - -bool -FrameWrapper::startRendering() -{ - if (isRendering()) - return true; - - try { - renderer_ = const_cast<video::Renderer*>(&avModel_.getRenderer(id_)); - } catch (std::out_of_range& e) { - qWarning() << e.what(); - return false; - } - - QObject::disconnect(renderConnections_.updated); - QObject::disconnect(renderConnections_.stopped); - - renderConnections_.updated = QObject::connect(&avModel_, - &AVModel::frameUpdated, - this, - &FrameWrapper::slotFrameUpdated); - - renderConnections_.stopped = QObject::connect(&avModel_, - &AVModel::rendererStopped, - this, - &FrameWrapper::slotRenderingStopped, - Qt::DirectConnection); - - return true; -} - -void -FrameWrapper::stopRendering() -{ - isRendering_ = false; -} - -QImage* -FrameWrapper::getFrame() -{ - if (image_.get()) { - return isRendering_ ? (image_.get()->isNull() ? nullptr : image_.get()) : nullptr; - } - return nullptr; -} - -bool -FrameWrapper::isRendering() -{ - return isRendering_; -} - -bool -FrameWrapper::frameMutexTryLock() -{ - return mutex_.tryLock(); -} - -void -FrameWrapper::frameMutexUnlock() -{ - mutex_.unlock(); -} - -void -FrameWrapper::slotRenderingStarted(const QString& id) -{ - if (id != id_) { - return; - } - - if (!startRendering()) { - qWarning() << "Couldn't start rendering for id: " << id_; - return; - } - - isRendering_ = true; - - Q_EMIT renderingStarted(id); -} - -void -FrameWrapper::slotFrameUpdated(const QString& id) -{ - if (id != id_) { - return; - } - - if (!renderer_ || !renderer_->isRendering()) { - return; - } - - { - QMutexLocker lock(&mutex_); - - frame_ = renderer_->currentFrame(); - - unsigned int width = renderer_->size().width(); - unsigned int height = renderer_->size().height(); - unsigned int size; - QImage::Format imageFormat; - if (renderer_->useDirectRenderer()) { - size = frame_.storage.size(); - imageFormat = QImage::Format_ARGB32_Premultiplied; - } else { - size = frame_.size; - imageFormat = QImage::Format_ARGB32; - } - /* - * If the frame is empty or not the expected size, - * do nothing and keep the last rendered QImage. - */ - if (size != 0 && size == width * height * 4) { - if (renderer_->useDirectRenderer()) { - buffer_ = std::move(frame_.storage); - } else { - // TODO remove this path. storage should work everywhere - // https://git.jami.net/savoirfairelinux/jami-libclient/-/issues/492 - buffer_.resize(size); - std::move(frame_.ptr, frame_.ptr + size, buffer_.begin()); - } - image_.reset(new QImage((uchar*) buffer_.data(), width, height, imageFormat)); - } - } - Q_EMIT frameUpdated(id); -} - -void -FrameWrapper::slotRenderingStopped(const QString& id) -{ - if (id != id_) { - return; - } - isRendering_ = false; - - QObject::disconnect(renderConnections_.updated); - - renderer_ = nullptr; - - { - QMutexLocker lock(&mutex_); - image_.reset(); - } - - Q_EMIT renderingStopped(id); -} - -RenderManager::RenderManager(AVModel& avModel) - : avModel_(avModel) -{} - -RenderManager::~RenderManager() -{ - for (auto& dfw : distantFrameWrapperMap_) { - dfw.second.reset(); - } -} - -void -RenderManager::stopPreviewing(const QString& id) -{ - auto dfwIt = distantFrameWrapperMap_.find(id); - if (dfwIt != distantFrameWrapperMap_.end()) { - dfwIt->second->stopRendering(); - avModel_.stopPreview(id); - } -} - -const QString -RenderManager::startPreviewing(const QString& id, bool force) -{ - auto dfwIt = distantFrameWrapperMap_.find(id); - if (dfwIt != distantFrameWrapperMap_.end()) { - if (dfwIt->second->isRendering() && !force) { - return dfwIt->second->getId(); - } - - if (dfwIt->second->isRendering()) { - avModel_.stopPreview(id); - } - return avModel_.startPreview(id); - } - return ""; -} - -void -RenderManager::addDistantRenderer(const QString& id) -{ - /* - * Check if a FrameWrapper with this id exists. - */ - auto dfwIt = distantFrameWrapperMap_.find(id); - if (dfwIt != distantFrameWrapperMap_.end()) { - if (!dfwIt->second->startRendering()) { - qWarning() << "Couldn't start rendering for id: " << id; - } - } else { - auto dfw = std::make_unique<FrameWrapper>(avModel_, id); - - /* - * Connect this to the FrameWrapper. - */ - distantConnectionMap_[id].updated = QObject::connect(dfw.get(), - &FrameWrapper::frameUpdated, - [this](const QString& id) { - Q_EMIT distantFrameUpdated(id); - }); - distantConnectionMap_[id].stopped = QObject::connect(dfw.get(), - &FrameWrapper::renderingStopped, - [this](const QString& id) { - Q_EMIT distantRenderingStopped(id); - }); - - /* - * Connect FrameWrapper to avmodel. - */ - dfw->connectStartRendering(); - try { - /* - * If the renderer has already started, then start the slot. - */ - if (avModel_.getRenderer(id).isRendering()) - dfw->slotRenderingStarted(id); - } catch (...) { - } - - /* - * Add to map. - */ - distantFrameWrapperMap_.insert(std::make_pair(id, std::move(dfw))); - } -} - -void -RenderManager::removeDistantRenderer(const QString& id) -{ - auto dfwIt = distantFrameWrapperMap_.find(id); - if (dfwIt != distantFrameWrapperMap_.end()) { - /* - * Disconnect FrameWrapper from this. - */ - auto dcIt = distantConnectionMap_.find(id); - if (dcIt != distantConnectionMap_.end()) { - QObject::disconnect(dcIt->second.started); - QObject::disconnect(dcIt->second.updated); - QObject::disconnect(dcIt->second.stopped); - } - - /* - * Erase. - */ - distantFrameWrapperMap_.erase(dfwIt); - } -} - -void -RenderManager::drawFrame(const QString& id, DrawFrameCallback cb) -{ - auto dfwIt = distantFrameWrapperMap_.find(id); - if (dfwIt != distantFrameWrapperMap_.end()) { - if (dfwIt->second->frameMutexTryLock()) { - cb(dfwIt->second->getFrame()); - dfwIt->second->frameMutexUnlock(); - } - } -} - -QImage* -RenderManager::getPreviewFrame(const QString& id) -{ - auto dfwIt = distantFrameWrapperMap_.find(id); - if (dfwIt != distantFrameWrapperMap_.end()) { - return dfwIt->second->getFrame(); - } - return {}; -} diff --git a/src/rendermanager.h b/src/rendermanager.h deleted file mode 100644 index a5e577668..000000000 --- a/src/rendermanager.h +++ /dev/null @@ -1,243 +0,0 @@ -/* - * Copyright (C) 2019-2022 Savoir-faire Linux Inc. - * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> - * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com> - * - * 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 3 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, see <https://www.gnu.org/licenses/>. - */ - -#pragma once - -#include "api/avmodel.h" -#include "api/lrc.h" - -#include <QImage> -#include <QMutex> -#include <QObject> - -using namespace lrc::api; - -/* - * This class acts as a QImage rendering sink and manages - * signal/slot connections to it's underlying (AVModel) renderer - * corresponding to the object's renderer id. - * A QImage pointer is provisioned and updated once rendering - * starts. - */ - -struct RenderConnections -{ - QMetaObject::Connection started, stopped, updated; -}; - -class FrameWrapper final : public QObject -{ - Q_OBJECT; - -public: - FrameWrapper(AVModel& avModel, const QString& id); - ~FrameWrapper(); - - /* - * Reconnect the started rendering connection for this object. - */ - void connectStartRendering(); - - /* - * Get a pointer to the renderer and reconnect the update/stopped - * rendering connections for this object. - * @return whether the start succeeded or not - */ - bool startRendering(); - - /* - * Locally disable frame access to this FrameWrapper - */ - void stopRendering(); - - /* - * Get the most recently rendered frame as a QImage. - * @return the rendered image of this object's id - */ - QImage* getFrame(); - - /* - * Check if the object is updating actively. - */ - bool isRendering(); - - bool frameMutexTryLock(); - - void frameMutexUnlock(); - - const QString getId() - { - return id_; - } - -Q_SIGNALS: - /* - * Emitted once in slotRenderingStarted. - * @param id of the renderer - */ - void renderingStarted(const QString& id); - /* - * Emitted each time a frame is ready to be displayed. - * @param id of the renderer - */ - void frameUpdated(const QString& id); - /* - * Emitted once in slotRenderingStopped. - * @param id of the renderer - */ - void renderingStopped(const QString& id); - -public Q_SLOTS: - /* - * Used to listen to AVModel::rendererStarted. - * @param id of the renderer - */ - void slotRenderingStarted(const QString& id); - /* - * Used to listen to AVModel::frameUpdated. - * @param id of the renderer - */ - void slotFrameUpdated(const QString& id); - /* - * Used to listen to AVModel::renderingStopped. - * @param id of the renderer - */ - void slotRenderingStopped(const QString& id); - -private: - /* - * The id of the renderer. - */ - QString id_; - - /* - * A pointer to the lrc renderer object. - */ - video::Renderer* renderer_; - - /* - * A local copy of the renderer's current frame. - */ - video::Frame frame_; - - /* - * A the frame's storage data used to set the image. - */ - std::vector<uint8_t> buffer_; - - /* - * The frame's paint ready QImage. - */ - std::unique_ptr<QImage> image_; - - /* - * Used to protect the buffer during QImage creation routine. - */ - QMutex mutex_; - - /* - * True if the object is rendering - */ - std::atomic_bool isRendering_; - - /* - * Convenience ref to avmodel - */ - AVModel& avModel_; - - /* - * Connections to the underlying renderer signals in avmodel - */ - RenderConnections renderConnections_; -}; - -/** - * RenderManager filters signals and ecapsulates preview and distant - * frame wrappers, providing access to QImages for each and simplified - * start/stop mechanisms for renderers. It should contain as much - * renderer control logic as possible and prevent ui widgets from directly - * interfacing the rendering logic. - */ -class RenderManager final : public QObject -{ - Q_OBJECT; - -public: - explicit RenderManager(AVModel& avModel); - ~RenderManager(); - - using DrawFrameCallback = std::function<void(QImage*)>; - - /* - * Start capturing and rendering preview frames. - * @param force if the capture device should be started - */ - const QString startPreviewing(const QString& id, bool force = false); - /* - * Stop capturing. - */ - void stopPreviewing(const QString& id); - /* - * Add and connect a distant renderer for a given id - * to a FrameWrapper object - * @param id - */ - void addDistantRenderer(const QString& id); - /* - * Disconnect and remove a FrameWrapper object connected to a - * distant renderer for a given id - * @param id - */ - void removeDistantRenderer(const QString& id); - /* - * Frame will be provided in the callback thread safely - * @param id - * @param cb - */ - void drawFrame(const QString& id, DrawFrameCallback cb); - - /* - * Get the most recently rendered preview frame as a QImage (none thread safe). - * @return the rendered preview image - */ - QImage* getPreviewFrame(const QString& id = ""); - -Q_SIGNALS: - /* - * Emitted when a distant renderer has a new frame ready for a given id. - */ - void distantFrameUpdated(const QString& id); - - /* - * Emitted when a distant renderer is stopped for a given id. - */ - void distantRenderingStopped(const QString& id); - -private: - /* - * Distant for each call/conf/conversation. - */ - std::map<QString, std::unique_ptr<FrameWrapper>> distantFrameWrapperMap_; - std::map<QString, RenderConnections> distantConnectionMap_; - - /* - * Convenience ref to avmodel. - */ - AVModel& avModel_; -}; diff --git a/src/settingsview/components/VideoSettings.qml b/src/settingsview/components/VideoSettings.qml index 5545843dd..f03cfb257 100644 --- a/src/settingsview/components/VideoSettings.qml +++ b/src/settingsview/components/VideoSettings.qml @@ -35,30 +35,16 @@ ColumnLayout { property int itemWidth function startPreviewing(force = false) { - if (root.visible) { - previewWidget.deviceId = VideoDevices.getDefaultDevice() - previewWidget.rendererId = VideoDevices.startDevice(previewWidget.deviceId, force) + if (!visible) { + return } - } - - function updatePreviewRatio() { - var resolution = VideoDevices.defaultRes - if (resolution.length !== 0) { - var resVec = resolution.split("x") - var ratio = resVec[1] / resVec[0] - if (ratio) { - aspectRatio = ratio - } else { - console.error("Could not scale recording video preview") - } - } - + const deviceId = VideoDevices.getDefaultDevice() + previewWidget.startWithId(deviceId, force) } onVisibleChanged: { if (visible) { hardwareAccelControl.checked = AvAdapter.getHardwareAcceleration() - updatePreviewRatio() if (previewWidget.visible) startPreviewing(true) } else { @@ -70,14 +56,11 @@ ColumnLayout { target: VideoDevices function onDefaultResChanged() { - updatePreviewRatio() - if (previewWidget.visible) - startPreviewing(true) + startPreviewing(true) } function onDefaultFpsChanged() { - if (previewWidget.visible) - startPreviewing(true) + startPreviewing(true) } function onDeviceAvailable() { @@ -207,12 +190,10 @@ ColumnLayout { // video Preview Rectangle { - id: rectBox - visible: VideoDevices.listSize !== 0 Layout.alignment: Qt.AlignHCenter - Layout.preferredHeight: width * aspectRatio + Layout.preferredHeight: width * previewWidget.invAspectRatio Layout.minimumWidth: 200 Layout.maximumWidth: 400 @@ -221,28 +202,19 @@ ColumnLayout { color: JamiTheme.primaryForegroundColor - DistantRenderer { + LocalVideo { id: previewWidget - anchors.fill: rectBox - property string deviceId: VideoDevices.getDefaultDevice() - rendererId: VideoDevices.getDefaultDevice() + anchors.fill: parent - lrcInstance: LRCInstance - - layer.enabled: true - layer.effect: OpacityMask { - maskSource: rectBox + underlayItems: Text { + anchors.centerIn: parent + font.pointSize: 18 + font.capitalization: Font.AllUppercase + color: "white" + text: JamiStrings.noVideo } } - - onVisibleChanged: { - if (visible) { - VideoDevices.stopDevice(previewWidget.deviceId) - startPreviewing(true) - } else - VideoDevices.stopDevice(previewWidget.deviceId) - } } Label { diff --git a/src/videodevices.cpp b/src/videodevices.cpp index 5cf90e76d..ff962588e 100644 --- a/src/videodevices.cpp +++ b/src/videodevices.cpp @@ -174,10 +174,8 @@ VideoFormatFpsModel::roleNames() const int VideoFormatFpsModel::getCurrentIndex() const { - QString currentDeviceId = videoDevices_->get_defaultId(); float currentFps = videoDevices_->get_defaultFps(); auto resultList = match(index(0, 0), FPS, QVariant(currentFps)); - return resultList.size() > 0 ? resultList[0].row() : 0; } @@ -291,21 +289,26 @@ VideoDevices::getDefaultDevice() } QString -VideoDevices::startDevice(const QString& deviceId, bool force) +VideoDevices::startDevice(const QString& id, bool force) { - if (deviceId.isEmpty()) + if (id.isEmpty()) return {}; - lrcInstance_->renderer()->addDistantRenderer(deviceId); + auto& avModel = lrcInstance_->avModel(); + if (avModel.hasRenderer(id)) { + if (!force) { + return id; + } + avModel.stopPreview(id); + } deviceOpen_ = true; - return lrcInstance_->renderer()->startPreviewing(deviceId, force); + return avModel.startPreview(id); } void -VideoDevices::stopDevice(const QString& deviceId, bool force) +VideoDevices::stopDevice(const QString& id, bool force) { - if (!deviceId.isEmpty() && (!lrcInstance_->hasActiveCall(true) || force)) { - lrcInstance_->renderer()->stopPreviewing(deviceId); - lrcInstance_->renderer()->removeDistantRenderer(deviceId); + if (!id.isEmpty() && (!lrcInstance_->hasActiveCall(true) || force)) { + lrcInstance_->avModel().stopPreview(id); deviceOpen_ = false; } } @@ -481,17 +484,6 @@ VideoDevices::onVideoDeviceEvent() Q_EMIT deviceListChanged(currentDeviceListSize); } else if (deviceOpen_) { updateData(); - - // Use QueuedConnection to make sure that it happens at the event loop of current device - Utils::oneShotConnect( - lrcInstance_->renderer(), - &RenderManager::distantRenderingStopped, - this, - [this, cb](const QString& id) { - if (this->getDefaultDevice() == id) - cb(); - }, - Qt::QueuedConnection); } else { cb(); } diff --git a/src/videoprovider.cpp b/src/videoprovider.cpp new file mode 100644 index 000000000..c3d4f0e0e --- /dev/null +++ b/src/videoprovider.cpp @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2022 Savoir-faire Linux Inc. + * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> + * + * 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 3 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, see <http://www.gnu.org/licenses/>. + */ + +#include "videoprovider.h" + +using namespace lrc::api; + +static bool +mapVideoFrame(QVideoFrame* videoFrame) +{ + if (!videoFrame || !videoFrame->isValid() + || (!videoFrame->isMapped() && !videoFrame->map(QVideoFrame::WriteOnly))) { + return false; + } + return true; +} + +VideoProvider::VideoProvider(AVModel& avModel, QObject* parent) + : QObject(parent) + , avModel_(avModel) +{ + connect(&avModel_, &AVModel::rendererStarted, this, &VideoProvider::onRendererStarted); + connect(&avModel_, &AVModel::frameBufferRequested, this, &VideoProvider::onFrameBufferRequested); + connect(&avModel_, &AVModel::frameUpdated, this, &VideoProvider::onFrameUpdated); + connect(&avModel_, &AVModel::rendererStopped, this, &VideoProvider::onRendererStopped); +} + +void +VideoProvider::registerSink(const QString& id, QVideoSink* obj) +{ + QMutexLocker lk(&framesObjsMutex_); + auto it = framesObjects_.find(id); + if (it == framesObjects_.end()) { + auto fo = std::make_unique<FrameObject>(); + fo->subscribers.insert(obj); + framesObjects_.emplace(id, std::move(fo)); + return; + } + it->second->subscribers.insert(obj); +} + +void +VideoProvider::unregisterSink(QVideoSink* obj) +{ + QMutexLocker lk(&framesObjsMutex_); + for (auto& frameObjIt : qAsConst(framesObjects_)) { + auto& subs = frameObjIt.second->subscribers; + auto it = subs.constFind(obj); + if (it != subs.cend()) { + subs.erase(it); + } + } +} + +QString +VideoProvider::captureVideoFrame(QVideoSink* obj) +{ + QImage img; + QVideoFrame currentFrame = obj->videoFrame(); + FrameObject* frameObj {nullptr}; + QMutexLocker lk(&framesObjsMutex_); + for (auto& frameObjIt : qAsConst(framesObjects_)) { + auto& subs = frameObjIt.second->subscribers; + auto it = subs.constFind(obj); + if (it != subs.cend()) { + frameObj = frameObjIt.second.get(); + } + } + if (frameObj) { + QMutexLocker lk(&frameObj->mutex); + auto imageFormat = QVideoFrameFormat::imageFormatFromPixelFormat( + QVideoFrameFormat::Format_RGBA8888); + img = QImage(currentFrame.bits(0), + currentFrame.width(), + currentFrame.height(), + currentFrame.bytesPerLine(0), + imageFormat); + } + return Utils::byteArrayToBase64String(Utils::QImageToByteArray(img)); +} + +void +VideoProvider::onRendererStarted(const QString& id) +{ + auto size = avModel_.getRendererSize(id); + // This slot is queued, the renderer may have been destroyed. + if (size.width() == 0 || size.height() == 0) { + return; + } + auto pixelFormat = avModel_.useDirectRenderer() ? 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); + framesObjects_.emplace(id, std::move(fo)); + } else { + it->second->videoFrame.reset(new QVideoFrame(frameFormat)); + } + } + + activeRenderers_[id] = size; + Q_EMIT activeRenderersChanged(); +} + +void +VideoProvider::onFrameBufferRequested(const QString& id, AVFrame* avframe) +{ + QMutexLocker framesLk(&framesObjsMutex_); + auto it = framesObjects_.find(id); + if (it == framesObjects_.end()) { + return; + } + 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; + 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; + } + 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."; + return; + } + if (!avModel_.useDirectRenderer()) { + // Shared memory renderering. + if (!mapVideoFrame(videoFrame)) { + qWarning() << "QVideoFrame can't be mapped" << id; + return; + } + auto frame = avModel_.getRendererFrame(id); + std::memcpy(videoFrame->bits(0), frame.ptr, frame.size); + } + if (videoFrame->isMapped()) { + videoFrame->unmap(); + for (const auto& sink : qAsConst(it->second->subscribers)) { + sink->setVideoFrame(*videoFrame); + Q_EMIT sink->videoFrameChanged(*videoFrame); + } + } +} + +void +VideoProvider::onRendererStopped(const QString& id) +{ + QMutexLocker framesLk(&framesObjsMutex_); + auto it = framesObjects_.find(id); + if (it == framesObjects_.end()) { + return; + } + + activeRenderers_.remove(id); + Q_EMIT activeRenderersChanged(); + + QMutexLocker lk(&it->second->mutex); + if (it->second->subscribers.empty()) { + lk.unlock(); + framesObjects_.erase(it); + return; + } + it->second->videoFrame.reset(); +} diff --git a/src/videoprovider.h b/src/videoprovider.h new file mode 100644 index 000000000..02d5a9b9b --- /dev/null +++ b/src/videoprovider.h @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 Savoir-faire Linux Inc. + * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> + * + * 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 3 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, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "utils.h" +#include "qtutils.h" + +#include "api/avmodel.h" + +extern "C" { +#include "libavutil/frame.h" +} + +#include <QVideoSink> +#include <QVideoFrame> +#include <QQmlEngine> +#include <QMutex> + +using namespace lrc::api; + +class VideoProvider final : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_PROPERTY(QVariantMap, activeRenderers) +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 QString captureVideoFrame(QVideoSink* obj); + +private Q_SLOTS: + void onRendererStarted(const QString& id); + void onFrameBufferRequested(const QString& id, AVFrame* avframe); + void onFrameUpdated(const QString& id); + void onRendererStopped(const QString& id); + +private: + AVModel& avModel_; + + struct FrameObject + { + std::unique_ptr<QVideoFrame> videoFrame; + QMutex mutex; + QSet<QVideoSink*> subscribers; + }; + std::map<QString, std::unique_ptr<FrameObject>> framesObjects_; + QMutex framesObjsMutex_; +}; -- GitLab