mirror of
https://github.com/hyperion-project/hyperion.ng.git
synced 2023-10-10 13:36:59 +02:00
160c5d0b3a
* 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>
479 lines
11 KiB
C++
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;
|
|
}
|