1
0
Fork 0
mirror of https://git.jami.net/savoirfairelinux/jami-client-qt.git synced 2025-08-30 03:33:40 +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 += -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

View file

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

View file

@ -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,37 +71,62 @@ 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());
}
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)
{
QScreen* screen = qApp->screens().at(screenNumber);
QtConcurrent::run([this, screenNumber]() {
QScreen* screen = QGuiApplication::screens().at(screenNumber);
if (!screen)
return QString("");
return;
/*
* The screen window id is always 0.
*/
@ -105,32 +135,88 @@ AvAdapter::captureScreen(int screenNumber)
QBuffer buffer;
buffer.open(QIODevice::WriteOnly);
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
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);
height < 128 ? 128 : height,
getCurrentCallId());
});
#else
LRCInstance::avModel().setDisplay(0,
x,
y,
width < 128 ? 128 : width,
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.

View file

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

View file

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

View file

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

View file

@ -70,7 +70,7 @@ DistantRenderer::getScaledHeight() const
void
DistantRenderer::paint(QPainter* painter)
{
auto distantImage = LRCInstance::renderer()->getFrame(distantRenderId_);
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);
@ -88,7 +88,11 @@ DistantRenderer::paint(QPainter* painter)
or static_cast<int>(scaledHeight_ * 1000) != tempScaledHeight) {
emit offsetChanged();
}
painter->drawImage(QRect(xOffset_, yOffset_, scaledDistant.width(), scaledDistant.height()),
painter->drawImage(QRect(xOffset_,
yOffset_,
scaledDistant.width(),
scaledDistant.height()),
scaledDistant);
}
});
}

View file

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

View file

@ -51,7 +51,7 @@ Rectangle {
if (avatar === "") {
contactImage.source = ""
} 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 {
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
// 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()
}

View file

@ -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,12 +168,9 @@ Window {
fillMode: Image.PreserveAspectFit
mipmap: true
Component.onCompleted: {
screenShotOdd.source = "data:image/png;base64,"
+ AvAdapter.captureScreen(
Component.onCompleted: AvAdapter.captureScreen(
calculateScreenNumber(index, false) - 1)
}
}
Text {
id: screenNameOdd
@ -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()
}
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -31,25 +31,23 @@ 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();
LRCInstance::renderer()
->drawFrame(lrc::api::video::PREVIEW_RENDERER_ID, [this, painter](QImage* previewImage) {
if (previewImage) {
QImage scaledPreview;
auto aspectRatio = static_cast<qreal>(previewImage->width())
/ static_cast<qreal>(previewImage->height());
auto previewHeight = height();
@ -60,17 +58,21 @@ PreviewRenderer::paint(QPainter* painter)
* 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)));
* 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,7 +95,8 @@ VideoCallPreviewRenderer::~VideoCallPreviewRenderer() {}
void
VideoCallPreviewRenderer::paint(QPainter* painter)
{
auto previewImage = LRCInstance::renderer()->getPreviewFrame();
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());
@ -103,6 +106,7 @@ VideoCallPreviewRenderer::paint(QPainter* painter)
painter->drawImage(QRect(0, 0, scaledPreview.width(), scaledPreview.height()),
scaledPreview);
}
});
}
PhotoboothPreviewRender::PhotoboothPreviewRender(QQuickItem* parent)
@ -130,7 +134,8 @@ PhotoboothPreviewRender::paint(QPainter* painter)
{
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) {
QImage scaledPreview;
scaledPreview = Utils::getCirclePhoto(*previewImage,
@ -138,4 +143,5 @@ PhotoboothPreviewRender::paint(QPainter* painter)
painter->drawImage(QRect(0, 0, scaledPreview.width(), scaledPreview.height()),
scaledPreview);
}
});
}

View file

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

View file

@ -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;
{
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)
{
@ -305,3 +309,28 @@ RenderManager::removeDistantRenderer(const QString& id)
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 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:

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