1
0
Fork 0
mirror of https://git.jami.net/savoirfairelinux/jami-client-qt.git synced 2025-07-23 00:45:29 +02:00

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
This commit is contained in:
Andreas Traczyk 2022-02-11 09:25:33 -05:00 committed by Adrien Béraud
parent a5adab3f58
commit e7cc0497ce
27 changed files with 552 additions and 1250 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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();
}

View file

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

View file

@ -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")));
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 {};
}

View file

@ -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_;
};

View file

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

View file

@ -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();
}

208
src/videoprovider.cpp Normal file
View file

@ -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();
}

67
src/videoprovider.h Normal file
View file

@ -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_;
};