hyperion.ng/libsrc/grabber/qt/QtGrabber.cpp
LordGrey 160c5d0b3a
UI and Web updates (#1421)
* Stop Web-Capture when priority changes

* Remote control UI: Treat duration=0 as endless

* Stop Web-Capture on non-Image events changes

* LED Matrix Layout - Support vertical cabling direction

* Additional Yeelight models

* Treat http headers case insensitive

* Update change log

* Treat http headers case insensitive (consider Qt version)

* API - Consider provided format when setImage

* UI - Support Boblight configuration per LED instance

* Support multiple Boblight clients with different priorities

* Update changelog

* Simplify isGUI rules allowing for QT only builds

* Sysinfo: Fix indents

* LED-Devices: Show warning, if get properties failed

* Qt-Grabber: Fixed position handling of multiple monitors

* LED layout: Remove indention limitations

* Yeelight: Test YLTD003

* hyperion-remote: Provide image filename to muxer/UI

* Refactor PriorityMuxer and related

* Temp: Build under Windows 2019

* Yeelight: Remove YLTD003 as it is not working without additional changes

* Test Windows-latest with out removing redistributables/new MSVC

* correct workflows

* correct CI script

* Build Windows with Qt 5.15.2

* Priority Muxer: Updates after testing

* Fix Typo

* Update BGHandler

* QTGrabber - Reactivate windows code to avoid cursor issues

* Emit prioritiesChanged when autoselect was changed by user

Co-authored-by: Paulchen Panther <Paulchen-Panter@protonmail.com>
2022-02-22 20:58:59 +01:00

479 lines
11 KiB
C++

// proj
#include <grabber/QtGrabber.h>
// qt
#include <QPixmap>
#include <QWindow>
#include <QGuiApplication>
#include <QScreen>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonDocument>
#ifdef _WIN32
#include <Windows.h>
#endif
// Constants
namespace {
const bool verbose = false;
} //End of constants
QtGrabber::QtGrabber(int display, int cropLeft, int cropRight, int cropTop, int cropBottom)
: Grabber("QTGRABBER", cropLeft, cropRight, cropTop, cropBottom)
, _display(display)
, _calculatedWidth(0)
, _calculatedHeight(0)
, _src_x(0)
, _src_y(0)
, _src_x_max(0)
, _src_y_max(0)
, _isWayland(false)
, _screen(nullptr)
, _isVirtual(false)
{
_logger = Logger::getInstance("Qt");
_useImageResampler = false;
}
QtGrabber::~QtGrabber()
{
freeResources();
}
void QtGrabber::freeResources()
{
// Qt seems to hold the ownership of the QScreen pointers
}
bool QtGrabber::open()
{
bool rc = false;
#ifndef _WIN32
if (getenv("WAYLAND_DISPLAY") != nullptr)
{
_isWayland = true;
}
else
#endif
{
rc = true;
}
return rc;
}
bool QtGrabber::setupDisplay()
{
bool result = false;
if (!open())
{
if (_isWayland)
{
Error(_log, "Grabber does not work under Wayland!");
}
}
else
{
// cleanup last screen
freeResources();
_numberOfSDisplays = 0;
QScreen* primary = QGuiApplication::primaryScreen();
QList<QScreen*> screens = QGuiApplication::screens();
// inject main screen at 0, if not nullptr
if (primary != nullptr)
{
screens.prepend(primary);
// remove last main screen if twice in list
if (screens.lastIndexOf(primary) > 0)
{
screens.removeAt(screens.lastIndexOf(primary));
}
}
if (screens.isEmpty())
{
Error(_log, "No displays found to capture from!");
result = false;
}
else
{
_numberOfSDisplays = screens.size();
Info(_log, "Available Displays:");
int index = 0;
for (auto* screen : qAsConst(screens))
{
const QRect geo = screen->geometry();
Info(_log, "Display %d: Name: %s Resolution: [%dx%d], Geometry: (L,T,R,B) %d,%d,%d,%d Depth:%dbit", index, QSTRING_CSTR(screen->name()), geo.width(), geo.height(), geo.x(), geo.y(), geo.x() + geo.width(), geo.y() + geo.height(), screen->depth());
++index;
}
if (screens.at(0)->size() != screens.at(0)->virtualSize())
{
const QRect vgeo = screens.at(0)->virtualGeometry();
Info(_log, "Display %d: Name: %s Resolution: [%dx%d], Geometry: (L,T,R,B) %d,%d,%d,%d Depth:%dbit", _numberOfSDisplays, "All Displays", vgeo.width(), vgeo.height(), vgeo.x(), vgeo.y(), vgeo.x() + vgeo.width(), vgeo.y() + vgeo.height(), screens.at(0)->depth());
}
_isVirtual = false;
// be sure the index is available
if (_display > _numberOfSDisplays - 1)
{
if ((screens.at(0)->size() != screens.at(0)->virtualSize()) && (_display == _numberOfSDisplays))
{
_isVirtual = true;
_display = 0;
}
else
{
Info(_log, "The requested display index '%d' is not available, falling back to display 0", _display);
_display = 0;
}
}
// init the requested display
_screen = screens.at(_display);
connect(_screen, &QScreen::geometryChanged, this, &QtGrabber::geometryChanged);
updateScreenDimensions(true);
if (_isVirtual)
{
Info(_log, "Using virtual display across all screens");
}
else
{
Info(_log, "Initialized display %d", _display);
}
result = true;
}
}
return result;
}
void QtGrabber::geometryChanged(const QRect& geo)
{
Info(_log, "The current display changed geometry to (L,T,R,B) %d,%d,%d,%d", geo.left(), geo.top(), geo.x() + geo.width(), geo.y() + geo.height());
updateScreenDimensions(true);
}
#ifdef _WIN32
extern QPixmap qt_pixmapFromWinHBITMAP(HBITMAP bitmap, int format = 0);
QPixmap QtGrabber::grabWindow(quintptr window, int xIn, int yIn, int width, int height) const
{
QSize windowSize;
HWND hwnd = reinterpret_cast<HWND>(window);
if (hwnd)
{
RECT r;
GetClientRect(hwnd, &r);
windowSize = QSize(r.right - r.left, r.bottom - r.top);
}
else
{
hwnd = GetDesktopWindow();
const QRect screenGeometry = _screen->geometry();
windowSize = screenGeometry.size();
}
if (width < 0)
width = windowSize.width() - xIn;
if (height < 0)
height = windowSize.height() - yIn;
// Create and setup bitmap
HDC display_dc = GetDC(nullptr);
HDC bitmap_dc = CreateCompatibleDC(display_dc);
HBITMAP bitmap = CreateCompatibleBitmap(display_dc, width, height);
HGDIOBJ null_bitmap = SelectObject(bitmap_dc, bitmap);
// copy data
HDC window_dc = GetDC(hwnd);
BitBlt(bitmap_dc, 0, 0, width, height, window_dc, xIn, yIn, SRCCOPY);
// clean up all but bitmap
ReleaseDC(hwnd, window_dc);
SelectObject(bitmap_dc, null_bitmap);
DeleteDC(bitmap_dc);
const QPixmap pixmap = qt_pixmapFromWinHBITMAP(bitmap);
DeleteObject(bitmap);
ReleaseDC(nullptr, display_dc);
return pixmap;
}
#endif
int QtGrabber::grabFrame(Image<ColorRgb>& image)
{
int rc = 0;
if (_isEnabled && !_isDeviceInError)
{
if (_screen == nullptr)
{
// reinit, this will disable capture on failure
bool result = setupDisplay();
setEnabled(result);
}
if (_isEnabled)
{
#ifdef _WIN32
QPixmap originalPixmap = grabWindow(0, _src_x, _src_y, _src_x_max, _src_y_max);
#else
QPixmap originalPixmap = _screen->grabWindow(0, _src_x, _src_y, _src_x_max, _src_y_max);
#endif
if (originalPixmap.isNull())
{
rc = -1;
}
else
{
QImage imageFrame = originalPixmap.toImage().scaled(_calculatedWidth, _calculatedHeight).convertToFormat(QImage::Format_RGB888);
image.resize(static_cast<uint>(_calculatedWidth), static_cast<uint>(_calculatedHeight));
for (int y = 0; y < imageFrame.height(); y++)
{
memcpy((unsigned char*)image.memptr() + y * image.width() * 3, static_cast<unsigned char*>(imageFrame.scanLine(y)), imageFrame.width() * 3);
}
}
}
}
return rc;
}
int QtGrabber::updateScreenDimensions(bool force)
{
if (_screen == nullptr)
{
return -1;
}
QRect geo;
if (_isVirtual)
{
geo = _screen->virtualGeometry();
}
else
{
geo = _screen->geometry();
}
if (!force && _width == geo.width() && _height == geo.height())
{
// No update required
return 0;
}
Info(_log, "Update of screen resolution: [%dx%d] to [%dx%d]", _width, _height, geo.width(), geo.height());
_width = geo.width();
_height = geo.height();
int width = 0;
int height = 0;
// Image scaling is performed by Qt
width = (_width > (_cropLeft + _cropRight))
? ((_width - _cropLeft - _cropRight) / _pixelDecimation)
: (_width / _pixelDecimation);
height = (_height > (_cropTop + _cropBottom))
? ((_height - _cropTop - _cropBottom) / _pixelDecimation)
: (_height / _pixelDecimation);
// calculate final image dimensions and adjust top/left cropping in 3D modes
if (_isVirtual)
{
_src_x = geo.x();
_src_y = geo.y();
}
else
{
_src_x = 0;
_src_y = 0;
}
switch (_videoMode)
{
case VideoMode::VIDEO_3DSBS:
_calculatedWidth = width / 2;
_calculatedHeight = height;
_src_x = _src_x + (_cropLeft / 2);
_src_y = _src_y + _cropTop;
_src_x_max = (_width / 2) - _cropRight - _cropLeft;
_src_y_max = _height - _cropBottom - _cropTop;
break;
case VideoMode::VIDEO_3DTAB:
_calculatedWidth = width;
_calculatedHeight = height / 2;
_src_x = _src_x + _cropLeft;
_src_y = _src_y + (_cropTop / 2);
_src_x_max = _width - _cropRight - _cropLeft;
_src_y_max = (_height / 2) - _cropBottom - _cropTop;
break;
case VideoMode::VIDEO_2D:
default:
_calculatedWidth = width;
_calculatedHeight = height;
_src_x = _src_x + _cropLeft;
_src_y = _src_y + _cropTop;
_src_x_max = _width - _cropRight - _cropLeft;
_src_y_max = _height - _cropBottom - _cropTop;
break;
}
Info(_log, "Update output image resolution to [%dx%d]", _calculatedWidth, _calculatedHeight);
Debug(_log, "Grab screen area: %d,%d,%d,%d", _src_x, _src_y, _src_x_max, _src_y_max);
return 1;
}
void QtGrabber::setVideoMode(VideoMode mode)
{
Grabber::setVideoMode(mode);
updateScreenDimensions(true);
}
bool QtGrabber::setPixelDecimation(int pixelDecimation)
{
bool rc(true);
if (Grabber::setPixelDecimation(pixelDecimation))
{
if (updateScreenDimensions(true) < 0)
{
rc = false;
}
}
return rc;
}
void QtGrabber::setCropping(int cropLeft, int cropRight, int cropTop, int cropBottom)
{
Grabber::setCropping(cropLeft, cropRight, cropTop, cropBottom);
updateScreenDimensions(true);
}
bool QtGrabber::setDisplayIndex(int index)
{
bool rc(true);
if (_display != index || _isVirtual)
{
_isVirtual = false;
if (index <= _numberOfSDisplays)
{
_display = index;
if (index == _numberOfSDisplays)
{
_isVirtual = true;
}
}
else
{
_display = 0;
}
rc = setupDisplay();
}
return rc;
}
QJsonObject QtGrabber::discover(const QJsonObject& params)
{
DebugIf(verbose, _log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData());
QJsonObject inputsDiscovered;
if (open())
{
QList<QScreen*> screens = QGuiApplication::screens();
if (!screens.isEmpty())
{
inputsDiscovered["device"] = "qt";
inputsDiscovered["device_name"] = "QT";
inputsDiscovered["type"] = "screen";
QJsonArray video_inputs;
QJsonArray fps = { 1, 5, 10, 15, 20, 25, 30, 40, 50, 60 };
for (int i = 0; i < screens.size(); ++i)
{
QJsonObject in;
QString name = screens.at(i)->name();
int pos = name.lastIndexOf('\\');
if (pos != -1)
{
name = name.right(name.length() - pos - 1);
}
in["name"] = name;
in["inputIdx"] = i;
QJsonArray formats;
QJsonObject format;
QJsonArray resolutionArray;
QJsonObject resolution;
resolution["width"] = screens.at(i)->size().width();
resolution["height"] = screens.at(i)->size().height();
resolution["fps"] = fps;
resolutionArray.append(resolution);
format["resolutions"] = resolutionArray;
formats.append(format);
in["formats"] = formats;
video_inputs.append(in);
}
if (screens.at(0)->size() != screens.at(0)->virtualSize())
{
QJsonObject in;
in["name"] = "All Displays";
in["inputIdx"] = screens.size();
in["virtual"] = true;
QJsonArray formats;
QJsonObject format;
QJsonArray resolutionArray;
QJsonObject resolution;
resolution["width"] = screens.at(0)->virtualSize().width();
resolution["height"] = screens.at(0)->virtualSize().height();
resolution["fps"] = fps;
resolutionArray.append(resolution);
format["resolutions"] = resolutionArray;
formats.append(format);
in["formats"] = formats;
video_inputs.append(in);
}
inputsDiscovered["video_inputs"] = video_inputs;
QJsonObject defaults, video_inputs_default, resolution_default;
resolution_default["fps"] = _fps;
video_inputs_default["resolution"] = resolution_default;
video_inputs_default["inputIdx"] = 0;
defaults["video_input"] = video_inputs_default;
inputsDiscovered["default"] = defaults;
}
if (inputsDiscovered.isEmpty())
{
DebugIf(verbose, _log, "No displays found to capture from!");
}
}
DebugIf(verbose, _log, "device: [%s]", QString(QJsonDocument(inputsDiscovered).toJson(QJsonDocument::Compact)).toUtf8().constData());
return inputsDiscovered;
}