1
0
Fork 0
mirror of https://git.jami.net/savoirfairelinux/jami-client-qt.git synced 2025-08-30 19:53:33 +02:00

misc: change the way of manipulating window's display screen

- avoid using screen number as display number on Linux
- support for area selection over multiple screens on Linux
- make getFrame null safe
- make video-full-screen mode show in the correct screen
- add the option of "share all screens"
- use x11 api for unix system for sharing screen areas

Gitlab: #160

Change-Id: Ibe47a4150b6a213950a0533d85e8cd7d5d159482
This commit is contained in:
ababi 2020-11-26 15:05:01 +01:00 committed by Mingrui Zhang
parent 768ea9d601
commit 7f7e4b2202
21 changed files with 630 additions and 256 deletions

View file

@ -103,10 +103,17 @@ unix {
LIBS += -L$${LRC}/lib -lringclient LIBS += -L$${LRC}/lib -lringclient
LIBS += -lqrencode LIBS += -lqrencode
LIBS += -lX11
isEmpty(PREFIX) { PREFIX = /tmp/$${TARGET}/bin } isEmpty(PREFIX) { PREFIX = /tmp/$${TARGET}/bin }
target.path = $$PREFIX/bin target.path = $$PREFIX/bin
INSTALLS += target INSTALLS += target
# unix specific
HEADERS += \
src/xrectsel.h
SOURCES += \
src/xrectsel.c
} }
# Input # Input

View file

@ -169,9 +169,12 @@ ApplicationWindow {
onClosing: root.close() onClosing: root.close()
onScreenChanged: JamiQmlUtils.mainApplicationScreen = root.screen
Component.onCompleted: { Component.onCompleted: {
if(!startAccountMigration()){ if(!startAccountMigration()){
startClient() startClient()
} }
JamiQmlUtils.mainApplicationScreen = root.screen
} }
} }

View file

@ -23,8 +23,13 @@
#include "lrcinstance.h" #include "lrcinstance.h"
#include "qtutils.h" #include "qtutils.h"
#ifdef Q_OS_LINUX
#include "xrectsel.h"
#endif
#include <QtConcurrent/QtConcurrent> #include <QtConcurrent/QtConcurrent>
#include <QApplication> #include <QApplication>
#include <QPainter>
#include <QScreen> #include <QScreen>
AvAdapter::AvAdapter(QObject* parent) AvAdapter::AvAdapter(QObject* parent)
@ -66,37 +71,62 @@ AvAdapter::populateVideoDeviceContextMenuItem()
void void
AvAdapter::onVideoContextMenuDeviceItemClicked(const QString& deviceName) 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); auto deviceId = LRCInstance::avModel().getDeviceIdFromName(deviceName);
if (deviceId.isEmpty()) { if (deviceId.isEmpty()) {
qWarning() << "Couldn't find device: " << deviceName; qWarning() << "Couldn't find device: " << deviceName;
return; return;
} }
LRCInstance::avModel().setCurrentVideoCaptureDevice(deviceId); LRCInstance::avModel().setCurrentVideoCaptureDevice(deviceId);
LRCInstance::avModel().switchInputTo(deviceId, call->id); LRCInstance::avModel().switchInputTo(deviceId, getCurrentCallId());
} }
void void
AvAdapter::shareEntireScreen(int screenNumber) AvAdapter::shareEntireScreen(int screenNumber)
{ {
QScreen* screen = qApp->screens().at(screenNumber); QScreen* screen = QGuiApplication::screens().at(screenNumber);
if (!screen) if (!screen)
return; return;
QRect rect = screen ? screen->geometry() : qApp->primaryScreen()->geometry(); QRect rect = screen->geometry();
LRCInstance::avModel().setDisplay(screenNumber, rect.x(), rect.y(), rect.width(), rect.height());
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());
} }
const QString 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());
}
void
AvAdapter::captureScreen(int screenNumber) AvAdapter::captureScreen(int screenNumber)
{ {
QScreen* screen = qApp->screens().at(screenNumber); QtConcurrent::run([this, screenNumber]() {
QScreen* screen = QGuiApplication::screens().at(screenNumber);
if (!screen) if (!screen)
return QString(""); return;
/* /*
* The screen window id is always 0. * The screen window id is always 0.
*/ */
@ -105,32 +135,88 @@ AvAdapter::captureScreen(int screenNumber)
QBuffer buffer; QBuffer buffer;
buffer.open(QIODevice::WriteOnly); buffer.open(QIODevice::WriteOnly);
pixmap.save(&buffer, "PNG"); pixmap.save(&buffer, "PNG");
return Utils::byteArrayToBase64String(buffer.data());
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 void
AvAdapter::shareFile(const QString& filePath) AvAdapter::shareFile(const QString& filePath)
{ {
LRCInstance::avModel().setInputFile(filePath); LRCInstance::avModel().setInputFile(filePath, getCurrentCallId());
} }
void 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); #ifdef Q_OS_LINUX
if (!screen) int display;
return;
QRect rect = screen ? screen->geometry() : qApp->primaryScreen()->geometry();
/* // Get display
* Provide minimum width, height. QString display_env {getenv("DISPLAY")};
* Need to add screen x, y initial value to the setDisplay api call. if (!display_env.isEmpty()) {
*/ auto list = display_env.split(":", Qt::SkipEmptyParts);
LRCInstance::avModel().setDisplay(screenNumber, // Should only be one display, so get the first one
rect.x() + x, if (list.size() > 0) {
rect.y() + y, 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, width < 128 ? 128 : width,
height < 128 ? 128 : height); height < 128 ? 128 : height,
getCurrentCallId());
});
#else
LRCInstance::avModel().setDisplay(0,
x,
y,
width < 128 ? 128 : width,
height < 128 ? 128 : height,
getCurrentCallId());
#endif
} }
void void
@ -145,19 +231,24 @@ AvAdapter::stopAudioMeter(bool async)
LRCInstance::stopAudioMeter(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 void
AvAdapter::slotDeviceEvent() AvAdapter::slotDeviceEvent()
{ {
auto& avModel = LRCInstance::avModel(); auto& avModel = LRCInstance::avModel();
auto defaultDevice = avModel.getDefaultDevice(); auto defaultDevice = avModel.getDefaultDevice();
auto currentCaptureDevice = avModel.getCurrentVideoCaptureDevice(); auto currentCaptureDevice = avModel.getCurrentVideoCaptureDevice();
QString callId {}; QString callId = getCurrentCallId();
auto* convModel = LRCInstance::getCurrentConversationModel();
const auto conversation = convModel->getConversationForUID(LRCInstance::getCurrentConvUid());
auto call = LRCInstance::getCallInfoForConversation(conversation);
if (call)
callId = call->id;
/* /*
* Decide whether a device has plugged, unplugged, or nothing has changed. * Decide whether a device has plugged, unplugged, or nothing has changed.

View file

@ -39,6 +39,8 @@ signals:
*/ */
void videoDeviceListChanged(bool listIsEmpty); void videoDeviceListChanged(bool listIsEmpty);
void screenCaptured(int screenNumber, QString source);
protected: protected:
void safeInit() override {}; void safeInit() override {};
@ -58,9 +60,19 @@ protected:
Q_INVOKABLE void shareEntireScreen(int screenNumber); Q_INVOKABLE void shareEntireScreen(int screenNumber);
/* /*
* Take snap shot of the screen by returning base64 image string. * Share the all screens connected.
*/ */
Q_INVOKABLE const QString captureScreen(int screenNumber); 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 void captureAllScreens();
/* /*
* Share a media file. * Share a media file.
@ -68,14 +80,19 @@ protected:
Q_INVOKABLE void shareFile(const QString& filePath); 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 startAudioMeter(bool async);
Q_INVOKABLE void stopAudioMeter(bool async); Q_INVOKABLE void stopAudioMeter(bool async);
private: private:
/*
* Get current callId from current selected conv id.
*/
const QString& getCurrentCallId();
/* /*
* Used to classify capture device events. * Used to classify capture device events.
*/ */

View file

@ -107,8 +107,16 @@ function addMenuItem(itemName,
console.log("Error loading component:", console.log("Error loading component:",
menuItemComponent.errorString()) menuItemComponent.errorString())
if (menuItemObject !== null) { if (menuItemObject !== null) {
menuItemObject.clicked.connect(function () {baseContextMenuObject.close()}) menuItemObject.clicked.connect(function () {
menuItemObject.clicked.connect(onClickedCallback) var callback = function(){
onClickedCallback()
baseContextMenuObject.onVisibleChanged.disconnect(callback)
baseContextMenuObject.close()
}
baseContextMenuObject.onVisibleChanged.connect(callback)
baseContextMenuObject.visible = false
})
menuItemObject.icon.color = "green" menuItemObject.icon.color = "green"
baseContextMenuObject.addItem(menuItemObject) baseContextMenuObject.addItem(menuItemObject)

View file

@ -24,7 +24,9 @@ import QtQuick 2.14
Item { Item {
readonly property string mainViewLoadPath: "qrc:/src/mainview/MainView.qml" readonly property string mainViewLoadPath: "qrc:/src/mainview/MainView.qml"
readonly property string wizardViewLoadPath: "qrc:/src/wizardview/WizardView.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 property bool callIsFullscreen: false
TextMetrics { TextMetrics {

View file

@ -70,7 +70,7 @@ DistantRenderer::getScaledHeight() const
void void
DistantRenderer::paint(QPainter* painter) DistantRenderer::paint(QPainter* painter)
{ {
auto distantImage = LRCInstance::renderer()->getFrame(distantRenderId_); LRCInstance::renderer()->drawFrame(distantRenderId_, [this, painter](QImage* distantImage) {
if (distantImage) { if (distantImage) {
auto scaledDistant = distantImage->scaled(size().toSize(), Qt::KeepAspectRatio); auto scaledDistant = distantImage->scaled(size().toSize(), Qt::KeepAspectRatio);
auto tempScaledWidth = static_cast<int>(scaledWidth_ * 1000); auto tempScaledWidth = static_cast<int>(scaledWidth_ * 1000);
@ -88,7 +88,11 @@ DistantRenderer::paint(QPainter* painter)
or static_cast<int>(scaledHeight_ * 1000) != tempScaledHeight) { or static_cast<int>(scaledHeight_ * 1000) != tempScaledHeight) {
emit offsetChanged(); emit offsetChanged();
} }
painter->drawImage(QRect(xOffset_, yOffset_, scaledDistant.width(), scaledDistant.height()), painter->drawImage(QRect(xOffset_,
yOffset_,
scaledDistant.width(),
scaledDistant.height()),
scaledDistant); scaledDistant);
} }
});
} }

View file

@ -115,13 +115,11 @@ Item {
ContextMenuGenerator.addMenuItem(JamiStrings.shareScreenArea, ContextMenuGenerator.addMenuItem(JamiStrings.shareScreenArea,
"qrc:/images/icons/screen_share-24px.svg", "qrc:/images/icons/screen_share-24px.svg",
function (){ function (){
if (Qt.application.screens.length === 1) { if (Qt.platform.os !== "windows") {
ScreenRubberBandCreation.createScreenRubberBandWindowObject( AvAdapter.shareScreenArea(0, 0, 0, 0)
null, 0)
ScreenRubberBandCreation.showScreenRubberBandWindow()
} else { } else {
SelectScreenWindowCreation.createSelectScreenWindowObject(true) ScreenRubberBandCreation.createScreenRubberBandWindowObject()
SelectScreenWindowCreation.showSelectScreenWindow() ScreenRubberBandCreation.showScreenRubberBandWindow()
} }
}) })
ContextMenuGenerator.addMenuItem(JamiStrings.shareFile, ContextMenuGenerator.addMenuItem(JamiStrings.shareFile,

View file

@ -51,7 +51,7 @@ Rectangle {
if (avatar === "") { if (avatar === "") {
contactImage.source = "" contactImage.source = ""
} else { } else {
contactImage.source = "data:image/png;base64," + avatar contactImage.source = JamiQmlUtils.base64StringTitle + avatar
} }
} }

View file

@ -32,16 +32,32 @@ import net.jami.Constants 1.0
Window { Window {
id: screenRubberBandWindow 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
}
screenRubberBandWindow.width = width
screenRubberBandWindow.height = height
screenRubberBandWindow.x = 0
screenRubberBandWindow.y = 0
}
flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.WA_TranslucentBackground flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.WA_TranslucentBackground
// Opacity with 0.7 window that will fill the entire screen, // Opacity with 0.7 window that will fill the entire screen,
// provide the users to select the area that they // provide the users to select the area that they
// want to share. // want to share.
color: Qt.rgba(0, 0, 0, 0.7) 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. // Rect for selection.
Rectangle { Rectangle {
@ -67,7 +83,6 @@ Window {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.CrossCursor cursorShape: Qt.CrossCursor
// Geo changing for user selection. // Geo changing for user selection.
onPressed: { onPressed: {
originalX = mouseX originalX = mouseX
@ -97,7 +112,7 @@ Window {
onReleased: { onReleased: {
recSelect.visible = false recSelect.visible = false
AvAdapter.shareScreenArea(screenNumber, recSelect.x, recSelect.y, AvAdapter.shareScreenArea(recSelect.x, recSelect.y,
recSelect.width, recSelect.height) recSelect.width, recSelect.height)
screenRubberBandWindow.close() screenRubberBandWindow.close()
} }

View file

@ -37,9 +37,7 @@ Window {
property int minHeight: 500 property int minHeight: 500
property int selectedScreenNumber: -1 property int selectedScreenNumber: -1
property bool selectAllScreens: false
// Decide whether to show screen area or entire screen.
property bool selectArea: false
// How many rows the ScrollView should have. // How many rows the ScrollView should have.
function calculateRepeaterModel() { function calculateRepeaterModel() {
@ -55,10 +53,10 @@ Window {
minimumWidth: minWidth minimumWidth: minWidth
minimumHeight: minHeight minimumHeight: minHeight
title: "Screen sharing" width: minWidth
height: minHeight
// Note: Qt.application.screens[0] is the app's current existing screen. screen: JamiQmlUtils.mainApplicationScreen
screen: Qt.application.screens[0]
modality: Qt.ApplicationModal modality: Qt.ApplicationModal
@ -96,6 +94,7 @@ Window {
clip: true clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AlwaysOn
// Column of rows repeater (two screen captures in a row). // Column of rows repeater (two screen captures in a row).
Column { 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, // To make sure that two screen captures in one row,
// a repeater of two rect is needed, which one in charge // a repeater of two rect is needed, which one in charge
// of odd number screen, one in charge of even number screen. // of odd number screen, one in charge of even number screen.
@ -154,12 +168,9 @@ Window {
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
mipmap: true mipmap: true
Component.onCompleted: { Component.onCompleted: AvAdapter.captureScreen(
screenShotOdd.source = "data:image/png;base64,"
+ AvAdapter.captureScreen(
calculateScreenNumber(index, false) - 1) calculateScreenNumber(index, false) - 1)
} }
}
Text { Text {
id: screenNameOdd id: screenNameOdd
@ -224,8 +235,7 @@ Window {
Component.onCompleted: { Component.onCompleted: {
if (screenSelectionRectEven.visible) if (screenSelectionRectEven.visible)
screenShotEven.source = "data:image/png;base64," AvAdapter.captureScreen(
+ AvAdapter.captureScreen(
calculateScreenNumber(index, true) - 1) 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 width: 200
height: 36 height: 36
visible: selectedScreenNumber != -1 visible: selectedScreenNumber != -1 || selectAllScreens
color: JamiTheme.buttonTintedBlack color: JamiTheme.buttonTintedBlack
hoveredColor: JamiTheme.buttonTintedBlackHovered hoveredColor: JamiTheme.buttonTintedBlackHovered
@ -284,21 +359,11 @@ Window {
text: JamiStrings.shareScreen text: JamiStrings.shareScreen
onClicked: { onClicked: {
if (selectArea) { if (selectAllScreens)
selectScreenWindow.hide() AvAdapter.shareAllScreens()
ScreenRubberBandCreation.createScreenRubberBandWindowObject( else
selectScreenWindow, selectedScreenNumber - 1)
ScreenRubberBandCreation.showScreenRubberBandWindow()
// Destory selectScreenWindow once screenRubberBand is closed.
ScreenRubberBandCreation.connectOnClosingEvent(function () {
selectScreenWindow.close()
})
} else {
AvAdapter.shareEntireScreen(selectedScreenNumber - 1) AvAdapter.shareEntireScreen(selectedScreenNumber - 1)
selectScreenWindow.close() selectScreenWindow.close()
} }
} }
}
} }

View file

@ -18,6 +18,7 @@
import QtQuick 2.14 import QtQuick 2.14
import QtQuick.Window 2.14 import QtQuick.Window 2.14
import net.jami.Models 1.0
Window { Window {
id: videoWindow id: videoWindow
@ -28,7 +29,11 @@ Window {
flags: Qt.FramelessWindowHint 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 visible: false

View file

@ -42,7 +42,7 @@ function finishCreation() {
// Signal connection. // Signal connection.
callFullScreenWindowContainerObject.onClosing.connect( callFullScreenWindowContainerObject.onClosing.connect(
destoryVideoCallFullScreenWindowContainer) destroyVideoCallFullScreenWindowContainer)
} }
function checkIfVisible() { function checkIfVisible() {
@ -57,7 +57,7 @@ function setAsContainerChild(obj) {
} }
// Destroy and reset callFullScreenWindowContainerObject when window is closed. // Destroy and reset callFullScreenWindowContainerObject when window is closed.
function destoryVideoCallFullScreenWindowContainer() { function destroyVideoCallFullScreenWindowContainer() {
if (!callFullScreenWindowContainerObject) if (!callFullScreenWindowContainerObject)
return return
callFullScreenWindowContainerObject.destroy() callFullScreenWindowContainerObject.destroy()

View file

@ -1,4 +1,3 @@
/* /*
* Copyright (C) 2020 by Savoir-faire Linux * Copyright (C) 2020 by Savoir-faire Linux
* Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com> * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
@ -17,64 +16,45 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * 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 screenRubberBandWindowComponent
var screenRubberBandWindowObject var screenRubberBandWindowObject
function createScreenRubberBandWindowObject(parent, screenNumber) { var selectAllScreens = false
function createScreenRubberBandWindowObject() {
if (screenRubberBandWindowObject) if (screenRubberBandWindowObject)
return return
screenRubberBandWindowComponent = Qt.createComponent( screenRubberBandWindowComponent = Qt.createComponent(
"../components/ScreenRubberBand.qml") "../components/ScreenRubberBand.qml")
if (screenRubberBandWindowComponent.status === Component.Ready) if (screenRubberBandWindowComponent.status === Component.Ready)
finishCreation(parent, screenNumber) finishCreation()
else if (screenRubberBandWindowComponent.status === Component.Error) else if (screenRubberBandWindowComponent.status === Component.Error)
console.log("Error loading component:", console.log("Error loading component:",
screenRubberBandWindowComponent.errorString()) screenRubberBandWindowComponent.errorString())
} }
function finishCreation(parent, screenNumber) { function finishCreation() {
screenRubberBandWindowObject = screenRubberBandWindowComponent.createObject( screenRubberBandWindowObject = screenRubberBandWindowComponent.createObject()
parent)
if (screenRubberBandWindowObject === null) { if (screenRubberBandWindowObject === null) {
// Error Handling.
/*
* Error Handling.
*/
console.log("Error creating screen rubber band object") console.log("Error creating screen rubber band object")
} }
screenRubberBandWindowObject.screenNumber = screenNumber // Signal connection.
screenRubberBandWindowObject.screen = Qt.application.screens[screenNumber]
/*
* Signal connection.
*/
screenRubberBandWindowObject.onClosing.connect( screenRubberBandWindowObject.onClosing.connect(
destoryScreenRubberBandWindow) destroyScreenRubberBandWindow)
} }
function showScreenRubberBandWindow() { function showScreenRubberBandWindow() {
screenRubberBandWindowObject.showFullScreen() screenRubberBandWindowObject.show()
screenRubberBandWindowObject.setAllScreensGeo()
} }
// Destroy and reset screenRubberBandWindowObject when window is closed.
/* function destroyScreenRubberBandWindow() {
* Destroy and reset screenRubberBandWindowObject when window is closed.
*/
function destoryScreenRubberBandWindow() {
if (!screenRubberBandWindowObject) if (!screenRubberBandWindowObject)
return return
screenRubberBandWindowObject.destroy() screenRubberBandWindowObject.destroy()
screenRubberBandWindowObject = false screenRubberBandWindowObject = false
} }
function connectOnClosingEvent(func) {
if (screenRubberBandWindowObject)
screenRubberBandWindowObject.onClosing.connect(func)
}

View file

@ -1,4 +1,3 @@
/* /*
* Copyright (C) 2020 by Savoir-faire Linux * Copyright (C) 2020 by Savoir-faire Linux
* Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com> * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
@ -17,54 +16,45 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * 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 selectScreenWindowComponent
var selectScreenWindowObject var selectScreenWindowObject
function createSelectScreenWindowObject(selectArea = false) { function createSelectScreenWindowObject() {
if (selectScreenWindowObject) if (selectScreenWindowObject)
return return
selectScreenWindowComponent = Qt.createComponent( selectScreenWindowComponent = Qt.createComponent(
"../components/SelectScreen.qml") "../components/SelectScreen.qml")
if (selectScreenWindowComponent.status === Component.Ready) if (selectScreenWindowComponent.status === Component.Ready)
finishCreation(selectArea) finishCreation()
else if (selectScreenWindowComponent.status === Component.Error) else if (selectScreenWindowComponent.status === Component.Error)
console.log("Error loading component:", console.log("Error loading component:",
selectScreenWindowComponent.errorString()) selectScreenWindowComponent.errorString())
} }
function finishCreation(selectArea) { function finishCreation() {
selectScreenWindowObject = selectScreenWindowComponent.createObject() selectScreenWindowObject = selectScreenWindowComponent.createObject()
if (selectScreenWindowObject === null) { if (selectScreenWindowObject === null) {
// Error Handling.
/*
* Error Handling.
*/
console.log("Error creating select screen object") console.log("Error creating select screen object")
} }
selectScreenWindowObject.selectArea = selectArea // Signal connection.
selectScreenWindowObject.onClosing.connect(destroySelectScreenWindow)
/*
* Signal connection.
*/
selectScreenWindowObject.onClosing.connect(destorySelectScreenWindow)
} }
function showSelectScreenWindow() { function showSelectScreenWindow() {
selectScreenWindowObject.show() 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 destroySelectScreenWindow() {
* Destroy and reset selectScreenWindowObject when window is closed.
*/
function destorySelectScreenWindow() {
if(!selectScreenWindowObject) if(!selectScreenWindowObject)
return return
selectScreenWindowObject.destroy() selectScreenWindowObject.destroy()

View file

@ -31,25 +31,23 @@ PreviewRenderer::PreviewRenderer(QQuickItem* parent)
previewFrameUpdatedConnection_ = connect(LRCInstance::renderer(), previewFrameUpdatedConnection_ = connect(LRCInstance::renderer(),
&RenderManager::previewFrameUpdated, &RenderManager::previewFrameUpdated,
[this]() { update(QRect(0, 0, width(), height())); }); [this]() {
if (isVisible())
previewRenderingStopped_ = connect(LRCInstance::renderer(), update(QRect(0, 0, width(), height()));
&RenderManager::previewRenderingStopped, });
[this]() { update(QRect(0, 0, width(), height())); });
} }
PreviewRenderer::~PreviewRenderer() PreviewRenderer::~PreviewRenderer()
{ {
disconnect(previewFrameUpdatedConnection_); disconnect(previewFrameUpdatedConnection_);
disconnect(previewRenderingStopped_);
} }
void void
PreviewRenderer::paint(QPainter* painter) PreviewRenderer::paint(QPainter* painter)
{ {
auto previewImage = LRCInstance::renderer()->getPreviewFrame(); LRCInstance::renderer()
->drawFrame(lrc::api::video::PREVIEW_RENDERER_ID, [this, painter](QImage* previewImage) {
if (previewImage) { if (previewImage) {
QImage scaledPreview;
auto aspectRatio = static_cast<qreal>(previewImage->width()) auto aspectRatio = static_cast<qreal>(previewImage->width())
/ static_cast<qreal>(previewImage->height()); / static_cast<qreal>(previewImage->height());
auto previewHeight = height(); auto previewHeight = height();
@ -60,17 +58,21 @@ PreviewRenderer::paint(QPainter* painter)
* e.g. * e.g.
* auto parent = qobject_cast<QWidget*>(this->parent()); * auto parent = qobject_cast<QWidget*>(this->parent());
* auto xPos = (parent->width() - previewWidth) / 2; * auto xPos = (parent->width() - previewWidth) / 2;
* setGeometry(QRect(QPoint(xPos, this->pos().y()), QSize(previewWidth, previewHeight))); * setGeometry(QRect(QPoint(xPos, this->pos().y()),
* QSize(previewWidth, previewHeight)));
*/ */
setWidth(previewWidth); setWidth(previewWidth);
setHeight(previewHeight); setHeight(previewHeight);
// If the given size is empty, this function returns a null image.
QImage scaledPreview;
scaledPreview = previewImage->scaled(size().toSize(), Qt::KeepAspectRatio); scaledPreview = previewImage->scaled(size().toSize(), Qt::KeepAspectRatio);
painter->drawImage(QRect(0, 0, scaledPreview.width(), scaledPreview.height()), painter->drawImage(QRect(0, 0, scaledPreview.width(), scaledPreview.height()),
scaledPreview); scaledPreview);
} else { } else {
paintBackground(painter); paintBackground(painter);
} }
});
} }
void void
@ -93,7 +95,8 @@ VideoCallPreviewRenderer::~VideoCallPreviewRenderer() {}
void void
VideoCallPreviewRenderer::paint(QPainter* painter) VideoCallPreviewRenderer::paint(QPainter* painter)
{ {
auto previewImage = LRCInstance::renderer()->getPreviewFrame(); LRCInstance::renderer()
->drawFrame(lrc::api::video::PREVIEW_RENDERER_ID, [this, painter](QImage* previewImage) {
if (previewImage) { if (previewImage) {
auto scalingFactor = static_cast<qreal>(previewImage->height()) auto scalingFactor = static_cast<qreal>(previewImage->height())
/ static_cast<qreal>(previewImage->width()); / static_cast<qreal>(previewImage->width());
@ -103,6 +106,7 @@ VideoCallPreviewRenderer::paint(QPainter* painter)
painter->drawImage(QRect(0, 0, scaledPreview.width(), scaledPreview.height()), painter->drawImage(QRect(0, 0, scaledPreview.width(), scaledPreview.height()),
scaledPreview); scaledPreview);
} }
});
} }
PhotoboothPreviewRender::PhotoboothPreviewRender(QQuickItem* parent) PhotoboothPreviewRender::PhotoboothPreviewRender(QQuickItem* parent)
@ -130,7 +134,8 @@ PhotoboothPreviewRender::paint(QPainter* painter)
{ {
painter->setRenderHint(QPainter::Antialiasing, true); painter->setRenderHint(QPainter::Antialiasing, true);
auto previewImage = LRCInstance::renderer()->getPreviewFrame(); LRCInstance::renderer()
->drawFrame(lrc::api::video::PREVIEW_RENDERER_ID, [this, painter](QImage* previewImage) {
if (previewImage) { if (previewImage) {
QImage scaledPreview; QImage scaledPreview;
scaledPreview = Utils::getCirclePhoto(*previewImage, scaledPreview = Utils::getCirclePhoto(*previewImage,
@ -138,4 +143,5 @@ PhotoboothPreviewRender::paint(QPainter* painter)
painter->drawImage(QRect(0, 0, scaledPreview.width(), scaledPreview.height()), painter->drawImage(QRect(0, 0, scaledPreview.width(), scaledPreview.height()),
scaledPreview); scaledPreview);
} }
});
} }

View file

@ -38,7 +38,6 @@ protected:
private: private:
QMetaObject::Connection previewFrameUpdatedConnection_; QMetaObject::Connection previewFrameUpdatedConnection_;
QMetaObject::Connection previewRenderingStopped_;
}; };
class VideoCallPreviewRenderer : public PreviewRenderer class VideoCallPreviewRenderer : public PreviewRenderer

View file

@ -85,7 +85,10 @@ FrameWrapper::stopRendering()
QImage* QImage*
FrameWrapper::getFrame() FrameWrapper::getFrame()
{ {
return isRendering_ ? image_.get() : nullptr; if (image_.get()) {
return isRendering_ ? (image_.get()->isNull() ? nullptr : image_.get()) : nullptr;
}
return nullptr;
} }
bool bool
@ -94,6 +97,18 @@ FrameWrapper::isRendering()
return isRendering_; return isRendering_;
} }
bool
FrameWrapper::frameMutexTryLock()
{
return mutex_.tryLock();
}
void
FrameWrapper::frameMutexUnlock()
{
mutex_.unlock();
}
void void
FrameWrapper::slotRenderingStarted(const QString& id) FrameWrapper::slotRenderingStarted(const QString& id)
{ {
@ -127,23 +142,25 @@ FrameWrapper::slotFrameUpdated(const QString& id)
unsigned int width = renderer_->size().width(); unsigned int width = renderer_->size().width();
unsigned int height = renderer_->size().height(); unsigned int height = renderer_->size().height();
#ifndef Q_OS_LINUX #ifndef Q_OS_LINUX
unsigned int size = frame_.storage.size(); 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, * If the frame is empty or not the expected size,
* do nothing and keep the last rendered QImage. * do nothing and keep the last rendered QImage.
*/ */
if (size != 0 && size == width * height * 4) { if (size != 0 && size == width * height * 4) {
#ifndef Q_OS_LINUX
buffer_ = std::move(frame_.storage); buffer_ = std::move(frame_.storage);
image_.reset(new QImage((uchar*) buffer_.data(),
width,
height,
QImage::Format_ARGB32_Premultiplied));
#else #else
if (frame_.ptr) { buffer_.reserve(size);
image_.reset(new QImage(frame_.ptr, width, height, QImage::Format_ARGB32)); std::move(frame_.ptr, frame_.ptr + size, buffer_.begin());
#endif #endif
image_.reset(new QImage((uchar*) buffer_.data(), width, height, imageFormat));
} }
} }
emit frameUpdated(id); emit frameUpdated(id);
@ -161,7 +178,10 @@ FrameWrapper::slotRenderingStopped(const QString& id)
renderer_ = nullptr; renderer_ = nullptr;
{
QMutexLocker lock(&mutex_);
image_.reset(); image_.reset();
}
emit renderingStopped(id); emit renderingStopped(id);
} }
@ -202,12 +222,6 @@ RenderManager::isPreviewing()
return previewFrameWrapper_->isRendering(); return previewFrameWrapper_->isRendering();
} }
QImage*
RenderManager::getPreviewFrame()
{
return previewFrameWrapper_->getFrame();
}
void void
RenderManager::stopPreviewing() RenderManager::stopPreviewing()
{ {
@ -232,16 +246,6 @@ RenderManager::startPreviewing(bool force)
avModel_.startPreview(); avModel_.startPreview();
} }
QImage*
RenderManager::getFrame(const QString& id)
{
auto dfwIt = distantFrameWrapperMap_.find(id);
if (dfwIt != distantFrameWrapperMap_.end()) {
return dfwIt->second->getFrame();
}
return nullptr;
}
void void
RenderManager::addDistantRenderer(const QString& id) RenderManager::addDistantRenderer(const QString& id)
{ {
@ -305,3 +309,28 @@ RenderManager::removeDistantRenderer(const QString& id)
distantFrameWrapperMap_.erase(dfwIt); distantFrameWrapperMap_.erase(dfwIt);
} }
} }
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();
}

View file

@ -77,6 +77,10 @@ public:
*/ */
bool isRendering(); bool isRendering();
bool frameMutexTryLock();
void frameMutexUnlock();
signals: signals:
/* /*
* Emitted each time a frame is ready to be displayed. * Emitted each time a frame is ready to be displayed.
@ -168,15 +172,12 @@ public:
explicit RenderManager(AVModel& avModel); explicit RenderManager(AVModel& avModel);
~RenderManager(); ~RenderManager();
using DrawFrameCallback = std::function<void(QImage*)>;
/* /*
* Check if the preview is active. * Check if the preview is active.
*/ */
bool isPreviewing(); 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. * Start capturing and rendering preview frames.
* @param force if the capture device should be started * @param force if the capture device should be started
@ -186,13 +187,6 @@ public:
* Stop capturing. * Stop capturing.
*/ */
void stopPreviewing(); 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 * Add and connect a distant renderer for a given id
* to a FrameWrapper object * to a FrameWrapper object
@ -205,6 +199,18 @@ public:
* @param id * @param id
*/ */
void removeDistantRenderer(const QString& 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: signals:

123
src/xrectsel.c Normal file
View file

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

26
src/xrectsel.h Normal file
View file

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