diff --git a/jami-qt.pro b/jami-qt.pro index 473442d67f789756fb464b37d7d6fee0a4f2933d..842d012980abdd3184c2da2137039f9b44603279 100644 --- a/jami-qt.pro +++ b/jami-qt.pro @@ -103,10 +103,17 @@ unix { LIBS += -L$${LRC}/lib -lringclient LIBS += -lqrencode + LIBS += -lX11 isEmpty(PREFIX) { PREFIX = /tmp/$${TARGET}/bin } target.path = $$PREFIX/bin INSTALLS += target + + # unix specific + HEADERS += \ + src/xrectsel.h + SOURCES += \ + src/xrectsel.c } # Input diff --git a/src/MainApplicationWindow.qml b/src/MainApplicationWindow.qml index d61215fdc87754941ffbd1769bacdb84c83f6ed5..32319ce29b5914eafb9aaf8f63f9075247e988df 100644 --- a/src/MainApplicationWindow.qml +++ b/src/MainApplicationWindow.qml @@ -169,9 +169,12 @@ ApplicationWindow { onClosing: root.close() + onScreenChanged: JamiQmlUtils.mainApplicationScreen = root.screen + Component.onCompleted: { if(!startAccountMigration()){ startClient() } + JamiQmlUtils.mainApplicationScreen = root.screen } } diff --git a/src/avadapter.cpp b/src/avadapter.cpp index e1e750df384b527785354ee4a2205351d3577a62..cd5bcfb8e5eb50515348f8b856e6be2b2a25f417 100644 --- a/src/avadapter.cpp +++ b/src/avadapter.cpp @@ -23,8 +23,13 @@ #include "lrcinstance.h" #include "qtutils.h" +#ifdef Q_OS_LINUX +#include "xrectsel.h" +#endif + #include <QtConcurrent/QtConcurrent> #include <QApplication> +#include <QPainter> #include <QScreen> AvAdapter::AvAdapter(QObject* parent) @@ -66,71 +71,152 @@ AvAdapter::populateVideoDeviceContextMenuItem() void AvAdapter::onVideoContextMenuDeviceItemClicked(const QString& deviceName) { - auto* convModel = LRCInstance::getCurrentConversationModel(); - const auto conversation = convModel->getConversationForUID(LRCInstance::getCurrentConvUid()); - auto call = LRCInstance::getCallInfoForConversation(conversation); - if (!call) - return; - auto deviceId = LRCInstance::avModel().getDeviceIdFromName(deviceName); if (deviceId.isEmpty()) { qWarning() << "Couldn't find device: " << deviceName; return; } LRCInstance::avModel().setCurrentVideoCaptureDevice(deviceId); - LRCInstance::avModel().switchInputTo(deviceId, call->id); + LRCInstance::avModel().switchInputTo(deviceId, getCurrentCallId()); } void AvAdapter::shareEntireScreen(int screenNumber) { - QScreen* screen = qApp->screens().at(screenNumber); + QScreen* screen = QGuiApplication::screens().at(screenNumber); if (!screen) return; - QRect rect = screen ? screen->geometry() : qApp->primaryScreen()->geometry(); - LRCInstance::avModel().setDisplay(screenNumber, rect.x(), rect.y(), rect.width(), rect.height()); + QRect rect = screen->geometry(); + + int display = 0; +#ifdef Q_OS_WIN + display = screenNumber; +#else + QString display_env {getenv("DISPLAY")}; + if (!display_env.isEmpty()) { + auto list = display_env.split(":", Qt::SkipEmptyParts); + // Should only be one display, so get the first one + if (list.size() > 0) { + display = list.at(0).toInt(); + } + } +#endif + LRCInstance::avModel() + .setDisplay(display, rect.x(), rect.y(), rect.width(), rect.height(), getCurrentCallId()); +} + +void +AvAdapter::shareAllScreens() +{ + auto screens = QGuiApplication::screens(); + + int width = 0, height = 0; + for (auto scr : screens) { + width += scr->geometry().width(); + if (height < scr->geometry().height()) + height = scr->geometry().height(); + } + + LRCInstance::avModel().setDisplay(0, 0, 0, width, height, getCurrentCallId()); } -const QString +void AvAdapter::captureScreen(int screenNumber) { - QScreen* screen = qApp->screens().at(screenNumber); - if (!screen) - return QString(""); - /* - * The screen window id is always 0. - */ - auto pixmap = screen->grabWindow(0); + QtConcurrent::run([this, screenNumber]() { + QScreen* screen = QGuiApplication::screens().at(screenNumber); + if (!screen) + return; + /* + * The screen window id is always 0. + */ + auto pixmap = screen->grabWindow(0); - QBuffer buffer; - buffer.open(QIODevice::WriteOnly); - pixmap.save(&buffer, "PNG"); - return Utils::byteArrayToBase64String(buffer.data()); + QBuffer buffer; + buffer.open(QIODevice::WriteOnly); + pixmap.save(&buffer, "PNG"); + + emit screenCaptured(screenNumber, Utils::byteArrayToBase64String(buffer.data())); + }); +} + +void +AvAdapter::captureAllScreens() +{ + QtConcurrent::run([this]() { + auto screens = QGuiApplication::screens(); + + QList<QPixmap> scrs; + int width = 0, height = 0, currentPoint = 0; + + foreach (auto scr, screens) { + QPixmap pix = scr->grabWindow(0); + width += pix.width(); + if (height < pix.height()) + height = pix.height(); + scrs << pix; + } + + QPixmap final(width, height); + QPainter painter(&final); + final.fill(Qt::black); + + foreach (auto scr, scrs) { + painter.drawPixmap(QPoint(currentPoint, 0), scr); + currentPoint += scr.width(); + } + + QBuffer buffer; + buffer.open(QIODevice::WriteOnly); + final.save(&buffer, "PNG"); + emit screenCaptured(-1, Utils::byteArrayToBase64String(buffer.data())); + }); } void AvAdapter::shareFile(const QString& filePath) { - LRCInstance::avModel().setInputFile(filePath); + LRCInstance::avModel().setInputFile(filePath, getCurrentCallId()); } void -AvAdapter::shareScreenArea(int screenNumber, int x, int y, int width, int height) +AvAdapter::shareScreenArea(unsigned x, unsigned y, unsigned width, unsigned height) { - QScreen* screen = qApp->screens().at(screenNumber); - if (!screen) - return; - QRect rect = screen ? screen->geometry() : qApp->primaryScreen()->geometry(); +#ifdef Q_OS_LINUX + int display; - /* - * Provide minimum width, height. - * Need to add screen x, y initial value to the setDisplay api call. - */ - LRCInstance::avModel().setDisplay(screenNumber, - rect.x() + x, - rect.y() + y, + // Get display + QString display_env {getenv("DISPLAY")}; + if (!display_env.isEmpty()) { + auto list = display_env.split(":", Qt::SkipEmptyParts); + // Should only be one display, so get the first one + if (list.size() > 0) { + display = list.at(0).toInt(); + } + } + + // xrectsel will freeze all displays too fast so that the call + // context menu will not be closed even closed signal is emitted + // use timer to wait until popup is closed + QTimer::singleShot(100, [=]() mutable { + x = y = width = height = 0; + xrectsel(&x, &y, &width, &height); + + LRCInstance::avModel().setDisplay(0, + x, + y, + width < 128 ? 128 : width, + height < 128 ? 128 : height, + getCurrentCallId()); + }); +#else + LRCInstance::avModel().setDisplay(0, + x, + y, width < 128 ? 128 : width, - height < 128 ? 128 : height); + height < 128 ? 128 : height, + getCurrentCallId()); +#endif } void @@ -145,19 +231,24 @@ AvAdapter::stopAudioMeter(bool async) LRCInstance::stopAudioMeter(async); } +const QString& +AvAdapter::getCurrentCallId() +{ + auto* convModel = LRCInstance::getCurrentConversationModel(); + const auto conversation = convModel->getConversationForUID(LRCInstance::getCurrentConvUid()); + auto call = LRCInstance::getCallInfoForConversation(conversation); + if (!call) + return QString(); + return call->id; +} + void AvAdapter::slotDeviceEvent() { auto& avModel = LRCInstance::avModel(); auto defaultDevice = avModel.getDefaultDevice(); auto currentCaptureDevice = avModel.getCurrentVideoCaptureDevice(); - QString callId {}; - - auto* convModel = LRCInstance::getCurrentConversationModel(); - const auto conversation = convModel->getConversationForUID(LRCInstance::getCurrentConvUid()); - auto call = LRCInstance::getCallInfoForConversation(conversation); - if (call) - callId = call->id; + QString callId = getCurrentCallId(); /* * Decide whether a device has plugged, unplugged, or nothing has changed. @@ -211,4 +302,4 @@ AvAdapter::slotDeviceEvent() emit videoDeviceListChanged(currentDeviceListSize == 0); deviceListSize_ = currentDeviceListSize; -} \ No newline at end of file +} diff --git a/src/avadapter.h b/src/avadapter.h index 3c7d619bb6f84e3cbfd51fce16de1e9b7b8b47b1..d3e961d60015881897b97135a1ae5d141fdf8012 100644 --- a/src/avadapter.h +++ b/src/avadapter.h @@ -39,6 +39,8 @@ signals: */ void videoDeviceListChanged(bool listIsEmpty); + void screenCaptured(int screenNumber, QString source); + protected: void safeInit() override {}; @@ -58,9 +60,19 @@ protected: Q_INVOKABLE void shareEntireScreen(int screenNumber); /* - * Take snap shot of the screen by returning base64 image string. + * Share the all screens connected. + */ + Q_INVOKABLE void shareAllScreens(); + + /* + * Take snap shot of the screen and return emitting signal. + */ + Q_INVOKABLE void captureScreen(int screenNumber); + + /* + * Take snap shot of the all screens and return by emitting signal. */ - Q_INVOKABLE const QString captureScreen(int screenNumber); + Q_INVOKABLE void captureAllScreens(); /* * Share a media file. @@ -68,14 +80,19 @@ protected: Q_INVOKABLE void shareFile(const QString& filePath); /* - * Select screen area to display. + * Select screen area to display (from all screens). */ - Q_INVOKABLE void shareScreenArea(int screenNumber, int x, int y, int width, int height); + Q_INVOKABLE void shareScreenArea(unsigned x, unsigned y, unsigned width, unsigned height); Q_INVOKABLE void startAudioMeter(bool async); Q_INVOKABLE void stopAudioMeter(bool async); private: + /* + * Get current callId from current selected conv id. + */ + const QString& getCurrentCallId(); + /* * Used to classify capture device events. */ diff --git a/src/commoncomponents/js/contextmenugenerator.js b/src/commoncomponents/js/contextmenugenerator.js index c68f6efbb7ae7ece88a7a75ffa6b7c1f5eec98e5..ef93c309152566e0239fac9b8b162cf2a5b166cf 100644 --- a/src/commoncomponents/js/contextmenugenerator.js +++ b/src/commoncomponents/js/contextmenugenerator.js @@ -107,8 +107,16 @@ function addMenuItem(itemName, console.log("Error loading component:", menuItemComponent.errorString()) if (menuItemObject !== null) { - menuItemObject.clicked.connect(function () {baseContextMenuObject.close()}) - menuItemObject.clicked.connect(onClickedCallback) + menuItemObject.clicked.connect(function () { + var callback = function(){ + onClickedCallback() + baseContextMenuObject.onVisibleChanged.disconnect(callback) + baseContextMenuObject.close() + } + + baseContextMenuObject.onVisibleChanged.connect(callback) + baseContextMenuObject.visible = false + }) menuItemObject.icon.color = "green" baseContextMenuObject.addItem(menuItemObject) diff --git a/src/constant/JamiQmlUtils.qml b/src/constant/JamiQmlUtils.qml index 6a75fce9e78e392de145ffc90f6370ede25b878c..4cb1670c4c378f7afeb3f2d71f3e623f051d914d 100644 --- a/src/constant/JamiQmlUtils.qml +++ b/src/constant/JamiQmlUtils.qml @@ -24,7 +24,9 @@ import QtQuick 2.14 Item { readonly property string mainViewLoadPath: "qrc:/src/mainview/MainView.qml" readonly property string wizardViewLoadPath: "qrc:/src/wizardview/WizardView.qml" + readonly property string base64StringTitle: "data:image/png;base64," + property var mainApplicationScreen: "" property bool callIsFullscreen: false TextMetrics { diff --git a/src/distantrenderer.cpp b/src/distantrenderer.cpp index 14b7f6db4a9669292454b44f82f552a2f9488785..55b15fa15f02ea59fcc8250de38cf3d5b05749cd 100644 --- a/src/distantrenderer.cpp +++ b/src/distantrenderer.cpp @@ -70,25 +70,29 @@ DistantRenderer::getScaledHeight() const void DistantRenderer::paint(QPainter* painter) { - auto distantImage = LRCInstance::renderer()->getFrame(distantRenderId_); - if (distantImage) { - auto scaledDistant = distantImage->scaled(size().toSize(), Qt::KeepAspectRatio); - 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) { - emit offsetChanged(); + LRCInstance::renderer()->drawFrame(distantRenderId_, [this, painter](QImage* distantImage) { + if (distantImage) { + auto scaledDistant = distantImage->scaled(size().toSize(), Qt::KeepAspectRatio); + 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) { + emit offsetChanged(); + } + painter->drawImage(QRect(xOffset_, + yOffset_, + scaledDistant.width(), + scaledDistant.height()), + scaledDistant); } - painter->drawImage(QRect(xOffset_, yOffset_, scaledDistant.width(), scaledDistant.height()), - scaledDistant); - } + }); } diff --git a/src/mainview/components/CallViewContextMenu.qml b/src/mainview/components/CallViewContextMenu.qml index a0dbdfed0b1052fb65cd5dad9b2fd29b38b51c1e..33378136d5e27338294fa6c5c255bd4679dbc751 100644 --- a/src/mainview/components/CallViewContextMenu.qml +++ b/src/mainview/components/CallViewContextMenu.qml @@ -115,13 +115,11 @@ Item { ContextMenuGenerator.addMenuItem(JamiStrings.shareScreenArea, "qrc:/images/icons/screen_share-24px.svg", function (){ - if (Qt.application.screens.length === 1) { - ScreenRubberBandCreation.createScreenRubberBandWindowObject( - null, 0) - ScreenRubberBandCreation.showScreenRubberBandWindow() + if (Qt.platform.os !== "windows") { + AvAdapter.shareScreenArea(0, 0, 0, 0) } else { - SelectScreenWindowCreation.createSelectScreenWindowObject(true) - SelectScreenWindowCreation.showSelectScreenWindow() + ScreenRubberBandCreation.createScreenRubberBandWindowObject() + ScreenRubberBandCreation.showScreenRubberBandWindow() } }) ContextMenuGenerator.addMenuItem(JamiStrings.shareFile, diff --git a/src/mainview/components/ParticipantOverlay.qml b/src/mainview/components/ParticipantOverlay.qml index 7a090acac976a50ba33cb7cdca6cf32473cd09fa..f187bef9c2593d0f9c5d19642284f2dd6effc524 100644 --- a/src/mainview/components/ParticipantOverlay.qml +++ b/src/mainview/components/ParticipantOverlay.qml @@ -51,7 +51,7 @@ Rectangle { if (avatar === "") { contactImage.source = "" } else { - contactImage.source = "data:image/png;base64," + avatar + contactImage.source = JamiQmlUtils.base64StringTitle + avatar } } diff --git a/src/mainview/components/ScreenRubberBand.qml b/src/mainview/components/ScreenRubberBand.qml index 0115351a536e73cbd47c4c718741707ff05a4135..79e85328398ca2421fe4e616bc316ae05090061d 100644 --- a/src/mainview/components/ScreenRubberBand.qml +++ b/src/mainview/components/ScreenRubberBand.qml @@ -32,16 +32,32 @@ import net.jami.Constants 1.0 Window { id: screenRubberBandWindow - property int screenNumber: 0 + function setAllScreensGeo() { + var width = 0, height = 0 + var screens = Qt.application.screens + for (var i = 0; i < screens.length; ++i) { + width += screens[i].width + if (height < screens[i].height) + height = screens[i].height + } - flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.WA_TranslucentBackground + screenRubberBandWindow.width = width + screenRubberBandWindow.height = height + screenRubberBandWindow.x = 0 + screenRubberBandWindow.y = 0 + } + flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.WA_TranslucentBackground // Opacity with 0.7 window that will fill the entire screen, // provide the users to select the area that they // want to share. color: Qt.rgba(0, 0, 0, 0.7) + // +1 so that it does not fallback to the previous screen + x: screen.virtualX + 1 + y: screen.virtualY + 1 + screen: Qt.application.screens[0] // Rect for selection. Rectangle { @@ -67,7 +83,6 @@ Window { hoverEnabled: true cursorShape: Qt.CrossCursor - // Geo changing for user selection. onPressed: { originalX = mouseX @@ -97,7 +112,7 @@ Window { onReleased: { recSelect.visible = false - AvAdapter.shareScreenArea(screenNumber, recSelect.x, recSelect.y, + AvAdapter.shareScreenArea(recSelect.x, recSelect.y, recSelect.width, recSelect.height) screenRubberBandWindow.close() } diff --git a/src/mainview/components/SelectScreen.qml b/src/mainview/components/SelectScreen.qml index 04f3cfcd2c08e744f59c70a8e98ffc3ef45825d3..60ab8bd10f245e672f427217e11e1aa86c3c1acb 100644 --- a/src/mainview/components/SelectScreen.qml +++ b/src/mainview/components/SelectScreen.qml @@ -37,9 +37,7 @@ Window { property int minHeight: 500 property int selectedScreenNumber: -1 - - // Decide whether to show screen area or entire screen. - property bool selectArea: false + property bool selectAllScreens: false // How many rows the ScrollView should have. function calculateRepeaterModel() { @@ -55,10 +53,10 @@ Window { minimumWidth: minWidth minimumHeight: minHeight - title: "Screen sharing" + width: minWidth + height: minHeight - // Note: Qt.application.screens[0] is the app's current existing screen. - screen: Qt.application.screens[0] + screen: JamiQmlUtils.mainApplicationScreen modality: Qt.ApplicationModal @@ -96,6 +94,7 @@ Window { clip: true ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AlwaysOn // Column of rows repeater (two screen captures in a row). Column { @@ -125,6 +124,21 @@ Window { } } + Connections { + target: AvAdapter + + function onScreenCaptured(screenNumber, source) { + if (screenNumber === -1) + screenShotAll.source = JamiQmlUtils.base64StringTitle + source + if (screenNumber !== index && screenNumber !== index + 1) + return + if (screenNumber % 2 !== 1) + screenShotOdd.source = JamiQmlUtils.base64StringTitle + source + else + screenShotEven.source = JamiQmlUtils.base64StringTitle + source + } + } + // To make sure that two screen captures in one row, // a repeater of two rect is needed, which one in charge // of odd number screen, one in charge of even number screen. @@ -154,11 +168,8 @@ Window { fillMode: Image.PreserveAspectFit mipmap: true - Component.onCompleted: { - screenShotOdd.source = "data:image/png;base64," - + AvAdapter.captureScreen( - calculateScreenNumber(index, false) - 1) - } + Component.onCompleted: AvAdapter.captureScreen( + calculateScreenNumber(index, false) - 1) } Text { @@ -224,8 +235,7 @@ Window { Component.onCompleted: { if (screenSelectionRectEven.visible) - screenShotEven.source = "data:image/png;base64," - + AvAdapter.captureScreen( + AvAdapter.captureScreen( calculateScreenNumber(index, true) - 1) } } @@ -259,6 +269,71 @@ Window { } } } + + Rectangle { + id: screenSelectionRectAll + + property string borderColor: JamiTheme.tabbarBorderColor + + anchors.horizontalCenter: screenSelectionScrollViewColumn.horizontalCenter + + color: JamiTheme.secondaryBackgroundColor + + height: screenSelectionScrollView.height + width: screenSelectionScrollView.width - 2 * JamiTheme.preferredMarginSize + + border.color: borderColor + + Connections { + target: selectScreenWindow + + function onSelectedScreenNumberChanged() { + // Recover from green state. + selectAllScreens = false + screenSelectionRectAll.borderColor = JamiTheme.tabbarBorderColor + } + } + + Image { + id: screenShotAll + + anchors.top: screenSelectionRectAll.top + anchors.topMargin: 10 + anchors.horizontalCenter: screenSelectionRectAll.horizontalCenter + + height: screenSelectionRectAll.height - 50 + width: screenSelectionRectAll.width - 50 + + fillMode: Image.PreserveAspectFit + mipmap: true + + Component.onCompleted: AvAdapter.captureAllScreens() + } + + Text { + id: screenNameAll + + anchors.top: screenShotAll.bottom + anchors.topMargin: 10 + anchors.horizontalCenter: screenSelectionRectAll.horizontalCenter + + font.pointSize: JamiTheme.textFontSize - 2 + text: qsTr("All Screens") + color: JamiTheme.textColor + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton + + onClicked: { + selectedScreenNumber = -1 + selectAllScreens = true + screenSelectionRectAll.borderColor + = JamiTheme.screenSelectionBorderGreen + } + } + } } } } @@ -273,7 +348,7 @@ Window { width: 200 height: 36 - visible: selectedScreenNumber != -1 + visible: selectedScreenNumber != -1 || selectAllScreens color: JamiTheme.buttonTintedBlack hoveredColor: JamiTheme.buttonTintedBlackHovered @@ -284,21 +359,11 @@ Window { text: JamiStrings.shareScreen onClicked: { - if (selectArea) { - selectScreenWindow.hide() - ScreenRubberBandCreation.createScreenRubberBandWindowObject( - selectScreenWindow, selectedScreenNumber - 1) - ScreenRubberBandCreation.showScreenRubberBandWindow() - - - // Destory selectScreenWindow once screenRubberBand is closed. - ScreenRubberBandCreation.connectOnClosingEvent(function () { - selectScreenWindow.close() - }) - } else { + if (selectAllScreens) + AvAdapter.shareAllScreens() + else AvAdapter.shareEntireScreen(selectedScreenNumber - 1) - selectScreenWindow.close() - } + selectScreenWindow.close() } } } diff --git a/src/mainview/components/VideoCallFullScreenWindowContainer.qml b/src/mainview/components/VideoCallFullScreenWindowContainer.qml index f585b8ea9d81fe7e00c76c70cc2059b65332f9b8..2c76179fefb9ba458c8208677f7d80467ae392b3 100644 --- a/src/mainview/components/VideoCallFullScreenWindowContainer.qml +++ b/src/mainview/components/VideoCallFullScreenWindowContainer.qml @@ -18,6 +18,7 @@ import QtQuick 2.14 import QtQuick.Window 2.14 +import net.jami.Models 1.0 Window { id: videoWindow @@ -28,7 +29,11 @@ Window { flags: Qt.FramelessWindowHint - screen: Qt.application.screens[0] + screen: JamiQmlUtils.mainApplicationScreen + + // +1 so that it does not fallback to the previous screen + x: screen.virtualX + 1 + y: screen.virtualY + 1 visible: false diff --git a/src/mainview/js/callfullscreenwindowcontainercreation.js b/src/mainview/js/callfullscreenwindowcontainercreation.js index 9eba21925935fde1b376c7610cee19bf1e8ba7bc..2ce42c14e02bd87cd6aa8cc0db53c7debf7038c9 100644 --- a/src/mainview/js/callfullscreenwindowcontainercreation.js +++ b/src/mainview/js/callfullscreenwindowcontainercreation.js @@ -42,7 +42,7 @@ function finishCreation() { // Signal connection. callFullScreenWindowContainerObject.onClosing.connect( - destoryVideoCallFullScreenWindowContainer) + destroyVideoCallFullScreenWindowContainer) } function checkIfVisible() { @@ -57,7 +57,7 @@ function setAsContainerChild(obj) { } // Destroy and reset callFullScreenWindowContainerObject when window is closed. -function destoryVideoCallFullScreenWindowContainer() { +function destroyVideoCallFullScreenWindowContainer() { if (!callFullScreenWindowContainerObject) return callFullScreenWindowContainerObject.destroy() diff --git a/src/mainview/js/screenrubberbandcreation.js b/src/mainview/js/screenrubberbandcreation.js index 38a22fdbd9f49ee3e6961c44868358f8409b1ae0..4be315f2bcdc7da25d87d7a92b56735e44d6d02b 100644 --- a/src/mainview/js/screenrubberbandcreation.js +++ b/src/mainview/js/screenrubberbandcreation.js @@ -1,4 +1,3 @@ - /* * Copyright (C) 2020 by Savoir-faire Linux * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com> @@ -17,64 +16,45 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ - -/* - * Global screen rubber band window component, object variable for creation. - */ +// Global screen rubber band window component, object variable for creation. var screenRubberBandWindowComponent var screenRubberBandWindowObject -function createScreenRubberBandWindowObject(parent, screenNumber) { +var selectAllScreens = false + +function createScreenRubberBandWindowObject() { if (screenRubberBandWindowObject) return screenRubberBandWindowComponent = Qt.createComponent( "../components/ScreenRubberBand.qml") if (screenRubberBandWindowComponent.status === Component.Ready) - finishCreation(parent, screenNumber) + finishCreation() else if (screenRubberBandWindowComponent.status === Component.Error) console.log("Error loading component:", screenRubberBandWindowComponent.errorString()) } -function finishCreation(parent, screenNumber) { - screenRubberBandWindowObject = screenRubberBandWindowComponent.createObject( - parent) +function finishCreation() { + screenRubberBandWindowObject = screenRubberBandWindowComponent.createObject() if (screenRubberBandWindowObject === null) { - - - /* - * Error Handling. - */ + // Error Handling. console.log("Error creating screen rubber band object") } - screenRubberBandWindowObject.screenNumber = screenNumber - screenRubberBandWindowObject.screen = Qt.application.screens[screenNumber] - - - /* - * Signal connection. - */ + // Signal connection. screenRubberBandWindowObject.onClosing.connect( - destoryScreenRubberBandWindow) + destroyScreenRubberBandWindow) } function showScreenRubberBandWindow() { - screenRubberBandWindowObject.showFullScreen() + screenRubberBandWindowObject.show() + screenRubberBandWindowObject.setAllScreensGeo() } - -/* - * Destroy and reset screenRubberBandWindowObject when window is closed. - */ -function destoryScreenRubberBandWindow() { +// Destroy and reset screenRubberBandWindowObject when window is closed. +function destroyScreenRubberBandWindow() { if (!screenRubberBandWindowObject) return screenRubberBandWindowObject.destroy() screenRubberBandWindowObject = false } - -function connectOnClosingEvent(func) { - if (screenRubberBandWindowObject) - screenRubberBandWindowObject.onClosing.connect(func) -} diff --git a/src/mainview/js/selectscreenwindowcreation.js b/src/mainview/js/selectscreenwindowcreation.js index 15bfbd157beae1f737c6d0f594e5d1945bb9aa4c..d4bbed83e48e15b73b38f529165fdaabf7138e62 100644 --- a/src/mainview/js/selectscreenwindowcreation.js +++ b/src/mainview/js/selectscreenwindowcreation.js @@ -1,4 +1,3 @@ - /* * Copyright (C) 2020 by Savoir-faire Linux * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com> @@ -17,54 +16,45 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ - -/* - * Global select screen window component, object variable for creation. - */ +// Global select screen window component, object variable for creation. var selectScreenWindowComponent var selectScreenWindowObject -function createSelectScreenWindowObject(selectArea = false) { +function createSelectScreenWindowObject() { if (selectScreenWindowObject) return selectScreenWindowComponent = Qt.createComponent( "../components/SelectScreen.qml") if (selectScreenWindowComponent.status === Component.Ready) - finishCreation(selectArea) + finishCreation() else if (selectScreenWindowComponent.status === Component.Error) console.log("Error loading component:", selectScreenWindowComponent.errorString()) } -function finishCreation(selectArea) { +function finishCreation() { selectScreenWindowObject = selectScreenWindowComponent.createObject() if (selectScreenWindowObject === null) { - - - /* - * Error Handling. - */ + // Error Handling. console.log("Error creating select screen object") } - selectScreenWindowObject.selectArea = selectArea - - - /* - * Signal connection. - */ - selectScreenWindowObject.onClosing.connect(destorySelectScreenWindow) + // Signal connection. + selectScreenWindowObject.onClosing.connect(destroySelectScreenWindow) } function showSelectScreenWindow() { selectScreenWindowObject.show() -} + var screen = selectScreenWindowObject.screen + selectScreenWindowObject.x = screen.virtualX + + (screen.width - selectScreenWindowObject.width) / 2 + selectScreenWindowObject.y = screen.virtualY + + (screen.height - selectScreenWindowObject.height) / 2 +} -/* - * Destroy and reset selectScreenWindowObject when window is closed. - */ -function destorySelectScreenWindow() { +// Destroy and reset selectScreenWindowObject when window is closed. +function destroySelectScreenWindow() { if(!selectScreenWindowObject) return selectScreenWindowObject.destroy() diff --git a/src/previewrenderer.cpp b/src/previewrenderer.cpp index 22e1796537e6f7a8e27176bcb24a6622d3066a89..2861c2bb43bd87cea556f03532065b27513b8755 100644 --- a/src/previewrenderer.cpp +++ b/src/previewrenderer.cpp @@ -31,46 +31,48 @@ PreviewRenderer::PreviewRenderer(QQuickItem* parent) previewFrameUpdatedConnection_ = connect(LRCInstance::renderer(), &RenderManager::previewFrameUpdated, - [this]() { update(QRect(0, 0, width(), height())); }); - - previewRenderingStopped_ = connect(LRCInstance::renderer(), - &RenderManager::previewRenderingStopped, - [this]() { update(QRect(0, 0, width(), height())); }); + [this]() { + if (isVisible()) + update(QRect(0, 0, width(), height())); + }); } PreviewRenderer::~PreviewRenderer() { disconnect(previewFrameUpdatedConnection_); - disconnect(previewRenderingStopped_); } void PreviewRenderer::paint(QPainter* painter) { - auto previewImage = LRCInstance::renderer()->getPreviewFrame(); - if (previewImage) { - QImage scaledPreview; - auto aspectRatio = static_cast<qreal>(previewImage->width()) - / static_cast<qreal>(previewImage->height()); - auto previewHeight = height(); - auto previewWidth = previewHeight * aspectRatio; - - /* 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); - - scaledPreview = previewImage->scaled(size().toSize(), Qt::KeepAspectRatio); - painter->drawImage(QRect(0, 0, scaledPreview.width(), scaledPreview.height()), - scaledPreview); - } else { - paintBackground(painter); - } + LRCInstance::renderer() + ->drawFrame(lrc::api::video::PREVIEW_RENDERER_ID, [this, painter](QImage* previewImage) { + if (previewImage) { + auto aspectRatio = static_cast<qreal>(previewImage->width()) + / static_cast<qreal>(previewImage->height()); + auto previewHeight = height(); + auto previewWidth = previewHeight * aspectRatio; + + /* 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; + scaledPreview = previewImage->scaled(size().toSize(), Qt::KeepAspectRatio); + painter->drawImage(QRect(0, 0, scaledPreview.width(), scaledPreview.height()), + scaledPreview); + } else { + paintBackground(painter); + } + }); } void @@ -93,16 +95,18 @@ VideoCallPreviewRenderer::~VideoCallPreviewRenderer() {} void VideoCallPreviewRenderer::paint(QPainter* painter) { - auto previewImage = LRCInstance::renderer()->getPreviewFrame(); - if (previewImage) { - auto scalingFactor = static_cast<qreal>(previewImage->height()) - / static_cast<qreal>(previewImage->width()); - setProperty("previewImageScalingFactor", scalingFactor); - QImage scaledPreview; - scaledPreview = previewImage->scaled(size().toSize(), Qt::KeepAspectRatio); - painter->drawImage(QRect(0, 0, scaledPreview.width(), scaledPreview.height()), - scaledPreview); - } + LRCInstance::renderer() + ->drawFrame(lrc::api::video::PREVIEW_RENDERER_ID, [this, painter](QImage* previewImage) { + if (previewImage) { + auto scalingFactor = static_cast<qreal>(previewImage->height()) + / static_cast<qreal>(previewImage->width()); + setProperty("previewImageScalingFactor", scalingFactor); + QImage scaledPreview; + scaledPreview = previewImage->scaled(size().toSize(), Qt::KeepAspectRatio); + painter->drawImage(QRect(0, 0, scaledPreview.width(), scaledPreview.height()), + scaledPreview); + } + }); } PhotoboothPreviewRender::PhotoboothPreviewRender(QQuickItem* parent) @@ -130,12 +134,14 @@ PhotoboothPreviewRender::paint(QPainter* painter) { painter->setRenderHint(QPainter::Antialiasing, true); - auto previewImage = LRCInstance::renderer()->getPreviewFrame(); - if (previewImage) { - QImage scaledPreview; - scaledPreview = Utils::getCirclePhoto(*previewImage, - height() <= width() ? height() : width()); - painter->drawImage(QRect(0, 0, scaledPreview.width(), scaledPreview.height()), - scaledPreview); - } + LRCInstance::renderer() + ->drawFrame(lrc::api::video::PREVIEW_RENDERER_ID, [this, painter](QImage* previewImage) { + if (previewImage) { + QImage scaledPreview; + scaledPreview = Utils::getCirclePhoto(*previewImage, + height() <= width() ? height() : width()); + painter->drawImage(QRect(0, 0, scaledPreview.width(), scaledPreview.height()), + scaledPreview); + } + }); } diff --git a/src/previewrenderer.h b/src/previewrenderer.h index b607f2901506a962737b1edb028ba90aa50c8057..646365e3f22c00aaf747a69ce9a19000fd213cb9 100644 --- a/src/previewrenderer.h +++ b/src/previewrenderer.h @@ -38,7 +38,6 @@ protected: private: QMetaObject::Connection previewFrameUpdatedConnection_; - QMetaObject::Connection previewRenderingStopped_; }; class VideoCallPreviewRenderer : public PreviewRenderer diff --git a/src/rendermanager.cpp b/src/rendermanager.cpp index f6d59158b53725d67a320883a848b768627ac312..2565e317738151126bc9c0a04e9ca81e7fa2492c 100644 --- a/src/rendermanager.cpp +++ b/src/rendermanager.cpp @@ -85,7 +85,10 @@ FrameWrapper::stopRendering() QImage* FrameWrapper::getFrame() { - return isRendering_ ? image_.get() : nullptr; + if (image_.get()) { + return isRendering_ ? (image_.get()->isNull() ? nullptr : image_.get()) : nullptr; + } + return nullptr; } bool @@ -94,6 +97,18 @@ FrameWrapper::isRendering() return isRendering_; } +bool +FrameWrapper::frameMutexTryLock() +{ + return mutex_.tryLock(); +} + +void +FrameWrapper::frameMutexUnlock() +{ + mutex_.unlock(); +} + void FrameWrapper::slotRenderingStarted(const QString& id) { @@ -127,23 +142,25 @@ FrameWrapper::slotFrameUpdated(const QString& id) unsigned int width = renderer_->size().width(); unsigned int height = renderer_->size().height(); - #ifndef Q_OS_LINUX unsigned int size = frame_.storage.size(); + auto imageFormat = QImage::Format_ARGB32_Premultiplied; +#else + unsigned int size = frame_.size; + auto imageFormat = QImage::Format_ARGB32; +#endif /* * 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) { +#ifndef Q_OS_LINUX buffer_ = std::move(frame_.storage); - image_.reset(new QImage((uchar*) buffer_.data(), - width, - height, - QImage::Format_ARGB32_Premultiplied)); #else - if (frame_.ptr) { - image_.reset(new QImage(frame_.ptr, width, height, QImage::Format_ARGB32)); + buffer_.reserve(size); + std::move(frame_.ptr, frame_.ptr + size, buffer_.begin()); #endif + image_.reset(new QImage((uchar*) buffer_.data(), width, height, imageFormat)); } } emit frameUpdated(id); @@ -161,7 +178,10 @@ FrameWrapper::slotRenderingStopped(const QString& id) renderer_ = nullptr; - image_.reset(); + { + QMutexLocker lock(&mutex_); + image_.reset(); + } emit renderingStopped(id); } @@ -202,12 +222,6 @@ RenderManager::isPreviewing() return previewFrameWrapper_->isRendering(); } -QImage* -RenderManager::getPreviewFrame() -{ - return previewFrameWrapper_->getFrame(); -} - void RenderManager::stopPreviewing() { @@ -232,16 +246,6 @@ RenderManager::startPreviewing(bool force) avModel_.startPreview(); } -QImage* -RenderManager::getFrame(const QString& id) -{ - auto dfwIt = distantFrameWrapperMap_.find(id); - if (dfwIt != distantFrameWrapperMap_.end()) { - return dfwIt->second->getFrame(); - } - return nullptr; -} - void RenderManager::addDistantRenderer(const QString& id) { @@ -304,4 +308,29 @@ RenderManager::removeDistantRenderer(const QString& id) */ distantFrameWrapperMap_.erase(dfwIt); } -} \ No newline at end of file +} + +void +RenderManager::drawFrame(const QString& id, DrawFrameCallback cb) +{ + if (id == lrc::api::video::PREVIEW_RENDERER_ID) { + if (previewFrameWrapper_->frameMutexTryLock()) { + cb(previewFrameWrapper_->getFrame()); + previewFrameWrapper_->frameMutexUnlock(); + } + } else { + auto dfwIt = distantFrameWrapperMap_.find(id); + if (dfwIt != distantFrameWrapperMap_.end()) { + if (dfwIt->second->frameMutexTryLock()) { + cb(dfwIt->second->getFrame()); + dfwIt->second->frameMutexUnlock(); + } + } + } +} + +QImage* +RenderManager::getPreviewFrame() +{ + return previewFrameWrapper_->getFrame(); +} diff --git a/src/rendermanager.h b/src/rendermanager.h index 298f9d76a8c56bbb918cca728df842f801d43596..98380b38c32ad815bca0fd308efdac14694ddbb0 100644 --- a/src/rendermanager.h +++ b/src/rendermanager.h @@ -77,6 +77,10 @@ public: */ bool isRendering(); + bool frameMutexTryLock(); + + void frameMutexUnlock(); + signals: /* * Emitted each time a frame is ready to be displayed. @@ -168,15 +172,12 @@ public: explicit RenderManager(AVModel& avModel); ~RenderManager(); + using DrawFrameCallback = std::function<void(QImage*)>; + /* * Check if the preview is active. */ bool isPreviewing(); - /* - * Get the most recently rendered preview frame as a QImage. - * @return the rendered preview image - */ - QImage* getPreviewFrame(); /* * Start capturing and rendering preview frames. * @param force if the capture device should be started @@ -186,13 +187,6 @@ public: * Stop capturing. */ void stopPreviewing(); - - /* - * Get the most recently rendered distant frame for a given id - * as a QImage. - * @return the rendered preview image - */ - QImage* getFrame(const QString& id); /* * Add and connect a distant renderer for a given id * to a FrameWrapper object @@ -205,6 +199,18 @@ public: * @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(); signals: diff --git a/src/xrectsel.c b/src/xrectsel.c new file mode 100644 index 0000000000000000000000000000000000000000..df682a2d12642f51107014de56974b16de43be49 --- /dev/null +++ b/src/xrectsel.c @@ -0,0 +1,123 @@ +/* + * This code is based and adapted from: + * https://github.com/lolilolicon/FFcast2/blob/master/xrectsel.c + * + * now located at: + * https://github.com/lolilolicon/xrectsel/blob/master/xrectsel.c + * + * xrectsel.c -- print the geometry of a rectangular screen region. + * Copyright (C) 2011-2014 lolilolicon <lolilolicon@gmail.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 <X11/Xlib.h> +#include <X11/cursorfont.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +void +xrectsel(unsigned* x_sel, unsigned* y_sel, unsigned* w_sel, unsigned* h_sel) +{ + Display* dpy = XOpenDisplay(NULL); + if (!dpy) + return; + + Window root = DefaultRootWindow(dpy); + + XEvent ev; + + GC sel_gc; + XGCValues sel_gv; + + int btn_pressed = 0; + int x = 0, y = 0; + unsigned int width = 0, height = 0; + int start_x = 0, start_y = 0; + + Cursor cursor; + cursor = XCreateFontCursor(dpy, XC_crosshair); + + /* Grab pointer for these events */ + XGrabPointer(dpy, + root, + True, + PointerMotionMask | ButtonPressMask | ButtonReleaseMask, + GrabModeAsync, + GrabModeAsync, + None, + cursor, + CurrentTime); + + sel_gv.function = GXinvert; + sel_gv.subwindow_mode = IncludeInferiors; + sel_gv.line_width = 1; + sel_gc = XCreateGC(dpy, root, GCFunction | GCSubwindowMode | GCLineWidth, &sel_gv); + + for (;;) { + XNextEvent(dpy, &ev); + + if (ev.type == ButtonPress) { + btn_pressed = 1; + x = start_x = ev.xbutton.x_root; + y = start_y = ev.xbutton.y_root; + width = height = 0; + + } else if (ev.type == MotionNotify) { + if (!btn_pressed) + continue; /* Draw only if button is pressed */ + + /* Re-draw last Rectangle to clear it */ + XDrawRectangle(dpy, root, sel_gc, x, y, width, height); + + x = ev.xbutton.x_root; + y = ev.xbutton.y_root; + + if (x > start_x) { + width = x - start_x; + x = start_x; + } else { + width = start_x - x; + } + + if (y > start_y) { + height = y - start_y; + y = start_y; + } else { + height = start_y - y; + } + + /* Draw Rectangle */ + XDrawRectangle(dpy, root, sel_gc, x, y, width, height); + XFlush(dpy); + + } else if (ev.type == ButtonRelease) + break; + } + + /* Re-draw last Rectangle to clear it */ + XDrawRectangle(dpy, root, sel_gc, x, y, width, height); + XFlush(dpy); + + XUngrabPointer(dpy, CurrentTime); + XFreeCursor(dpy, cursor); + XFreeGC(dpy, sel_gc); + XSync(dpy, 1); + + *x_sel = x; + *y_sel = y; + *w_sel = width; + *h_sel = height; + + XCloseDisplay(dpy); +} diff --git a/src/xrectsel.h b/src/xrectsel.h new file mode 100644 index 0000000000000000000000000000000000000000..5d2fca5e7995f21a58bad293e3226cd046f4e5c0 --- /dev/null +++ b/src/xrectsel.h @@ -0,0 +1,26 @@ +/* + * This code is based and adapted from: + * https://github.com/lolilolicon/FFcast2/blob/master/xrectsel.c + * + * now located at: + * https://github.com/lolilolicon/xrectsel/blob/master/xrectsel.c + * + * xrectsel.c -- print the geometry of a rectangular screen region. + * Copyright (C) 2011-2014 lolilolicon <lolilolicon@gmail.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 + +extern "C" { +void xrectsel(unsigned* x_sel, unsigned* y_sel, unsigned* w_sel, unsigned* h_sel); +}