diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..7964242 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.0.0) + +project(Hyperion_Grabber_X11_QT VERSION 0.1 LANGUAGES CXX) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_BUILD_TYPE Debug) + +find_package(Qt5Core REQUIRED) +find_package(Qt5Network REQUIRED) +find_package(Qt5Widgets REQUIRED) +find_package(X11 REQUIRED) + +set(CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_CXX_STANDARD_LIBRARIES} -lXrender -lXdamage") +include_directories(${X11_INCLUDE_DIR}) +add_executable(${PROJECT_NAME} "main.cpp" "hgx11.h" "hgx11.cpp" "hgx11net.h" "hgx11net.cpp" "hgx11damage.h" "hgx11damage.cpp" "hgx11grab.h" "hgx11grab.cpp") +target_link_libraries(${PROJECT_NAME} Qt5::Core Qt5::Network Qt5::Widgets ${X11_LIBRARIES}) diff --git a/hgx11.cpp b/hgx11.cpp new file mode 100644 index 0000000..efc360d --- /dev/null +++ b/hgx11.cpp @@ -0,0 +1,68 @@ +#include "hgx11.h" + +// public + +hgx11::hgx11(QString addr, QString port, QString scale, QString frameskip, QString inactiveTime) +{ + _addr_m = (addr.isEmpty() || addr.isNull()) ? "localhost" : addr; + _port_m = (port.toInt() > 65535 || port.toInt() < 1) ? "19444" : port; + _scale_m = (scale.toInt() < 1 || scale.toInt() > 1000) ? "4" : scale; + _frameskip_m = (frameskip.toInt() > 0 && frameskip.toInt() < 256) ? frameskip : "0"; + _inactiveTime_m = (inactiveTime.toInt() > 0 ? QString::number(inactiveTime.toInt() * 1000) : "0"); + + _grabber_p = new hgx11grab(_scale_m.toInt(), _frameskip_m.toUShort()); + _hclient_p = new hgx11net(_addr_m.toStdString().c_str(), _port_m.toUShort()); + _damage_p = new hgx11damage(); + + _hclient_p->imgWidth = QString::number(_grabber_p->getDest_width()); + _hclient_p->imgHeight = QString::number(_grabber_p->getDest_height()); + + _damage_p->start(); + + connect(_damage_p, SIGNAL(damageDetected()), _grabber_p, SLOT(grabFrame())); + connect(_grabber_p, SIGNAL(scaleChanged()), this, SLOT(_setImgSize())); + connect(_grabber_p, SIGNAL(imageCreated()), this, SLOT(_sendImage())); + + if (_inactiveTime_m.toInt()) { + _timer_p = new QTimer(this); + connect(_timer_p, SIGNAL(timeout()), this, SLOT(_inActivity())); + _timer_p->start(_inactiveTime_m.toInt()); + connect(_damage_p, SIGNAL(damageDetected()), this, SLOT(_activity())); + } +} + +hgx11::~hgx11() +{ + delete _hclient_p; + _hclient_p = nullptr; + _damage_p->terminate(); + while (!_damage_p->isFinished()) {} + delete _damage_p; + _damage_p = nullptr; + delete _grabber_p; + _grabber_p = nullptr; +} + +// private slots + +void hgx11::_sendImage() +{ + _hclient_p->sendImage(&_grabber_p->imgdata_m); +} + +void hgx11::_inActivity() +{ + _hclient_p->clearLeds(); +} + +void hgx11::_activity() +{ + _timer_p->stop(); + _timer_p->start(_inactiveTime_m.toInt()); +} + +void hgx11::_setImgSize() +{ + _hclient_p->imgWidth = QString::number(_grabber_p->getDest_width()); + _hclient_p->imgHeight = QString::number(_grabber_p->getDest_height()); +} diff --git a/hgx11.h b/hgx11.h new file mode 100644 index 0000000..86a777c --- /dev/null +++ b/hgx11.h @@ -0,0 +1,41 @@ +#ifndef HGX11_H +#define HGX11_H + +#include +#include +#include +#include "hgx11grab.h" +#include "hgx11net.h" +#include "hgx11damage.h" + + +class hgx11 : public QObject +{ + Q_OBJECT +public: + hgx11(QString, QString, QString, QString, QString); + ~hgx11(); + +private: + hgx11damage *_damage_p; + hgx11grab *_grabber_p; + hgx11net *_hclient_p; + QTimer *_timer_p; + + QString _addr_m; + QString _port_m; + QString _scale_m; + QString _frameskip_m; + QString _inactiveTime_m; + QString _destWidth_m; + QString _destHeight_m; + +private slots: + void _sendImage(); + void _inActivity(); + void _activity(); + void _setImgSize(); + +}; + +#endif // HGX11_H diff --git a/hgx11damage.cpp b/hgx11damage.cpp new file mode 100644 index 0000000..27f4771 --- /dev/null +++ b/hgx11damage.cpp @@ -0,0 +1,34 @@ +#include "hgx11damage.h" + +// public + +hgx11damage::hgx11damage() +{ + _display_p = XOpenDisplay(nullptr); + if(!_display_p){ + qCritical() << "failed to open x display"; + return; + } + Window window = DefaultRootWindow(_display_p); + _damage_m = XDamageCreate(_display_p, window, XDamageReportNonEmpty); + +} + +hgx11damage::~hgx11damage() +{ + _loop_m = false; +} + +// protected + +void hgx11damage::_monitor() +{ + if(!_display_p){ + return; + } + while (_loop_m) { + XNextEvent(_display_p, &_event_m); + emit damageDetected(); + XDamageSubtract(_display_p ,_damage_m, None, None); + } +} diff --git a/hgx11damage.h b/hgx11damage.h new file mode 100644 index 0000000..a5ba021 --- /dev/null +++ b/hgx11damage.h @@ -0,0 +1,34 @@ +#ifndef HGX11DAMAGE_H +#define HGX11DAMAGE_H + +#include +#include +#include +#include + +class hgx11damage : public QThread +{ + Q_OBJECT +public: + hgx11damage(); + ~hgx11damage(); + +private: + Display *_display_p; + XEvent _event_m; + Damage _damage_m; + bool _loop_m = true; + + void _monitor(); + +protected: + void run() { + _monitor(); + } + +signals: + void damageDetected(); + +}; + +#endif // HGX11DAMAGE_H diff --git a/hgx11grab.cpp b/hgx11grab.cpp new file mode 100644 index 0000000..14cb523 --- /dev/null +++ b/hgx11grab.cpp @@ -0,0 +1,206 @@ +#include "hgx11grab.h" + +// public + +hgx11grab::hgx11grab(int scaleDivisor, ushort frameSkip) +{ + _frameSkip_m = frameSkip; + _x11Display_p = XOpenDisplay(":0"); + if (_x11Display_p == nullptr) { + qCritical() << "Failed to open X11 display."; + return; + } + + _window_m = DefaultRootWindow(_x11Display_p); + + bool _XShmAvailable = XShmQueryExtension(_x11Display_p); + if (!_XShmAvailable) { + qCritical() << "Xshm is: not available."; + return; + } + + int dummy, pixmaps_supported; + + bool _XRenderAvailable = XRenderQueryExtension(_x11Display_p, &dummy, &dummy); + if (!_XRenderAvailable) { + qCritical() << "XRender is not available."; + return; + } + + XShmQueryVersion(_x11Display_p, &dummy, &dummy, &pixmaps_supported); + bool _XShmPixmapAvailable = pixmaps_supported && XShmPixmapFormat(_x11Display_p) == ZPixmap; + if (!_XShmPixmapAvailable) { + qCritical() << "XshmPixMap is: not available."; + return; + } + + if (!_getWinAttr()) { + return; + } + + _destWidth_m = int(_srcWidth_m / scaleDivisor); + _destHeight_m = int(_srcHeight_m / scaleDivisor); + + _imgSize_m = _destHeight_m * _destWidth_m * 4; + + memset(&_pictAttr_m, 0, sizeof(_pictAttr_m)); + _pictAttr_m.repeat = RepeatNone; + + _mTransform_m.matrix[0][0] = XDoubleToFixed(1.0); + _mTransform_m.matrix[0][1] = 0; + _mTransform_m.matrix[0][2] = 0; + _mTransform_m.matrix[1][0] = 0; + _mTransform_m.matrix[1][1] = XDoubleToFixed(1.0); + _mTransform_m.matrix[1][2] = 0; + _mTransform_m.matrix[2][0] = 0; + _mTransform_m.matrix[2][1] = 0; + _setScale(); + + connect(this, SIGNAL(scaleChanged()), this, SLOT(_changeScale())); +} + +hgx11grab::~hgx11grab() +{ + if (_x11Display_p != nullptr) { + _freeResources(); + XCloseDisplay(_x11Display_p); + } +} + +int hgx11grab::getDest_height() const +{ + return _destHeight_m; +} + +int hgx11grab::getDest_width() const +{ + return _destWidth_m; +} + +// private + +void hgx11grab::_grabFrame() +{ + if (!_getWinAttr()) { + return; + } + + _xImage_p = XShmCreateImage( + _x11Display_p, _windowAttr_m.visual, + uint(_windowAttr_m.depth), ZPixmap, nullptr, &_shminfo_m, + uint(_destWidth_m), uint(_destHeight_m) + ); + + _shminfo_m.shmid = shmget(IPC_PRIVATE, ulong(_xImage_p->bytes_per_line * _xImage_p->height), IPC_CREAT|0777); + _xImage_p->data = reinterpret_cast(shmat(_shminfo_m.shmid, nullptr, 0)); + _shminfo_m.shmaddr = _xImage_p->data; + _shminfo_m.readOnly = false; + + XShmAttach(_x11Display_p, &_shminfo_m); + _pixmap_m = XShmCreatePixmap(_x11Display_p, _window_m, _xImage_p->data, &_shminfo_m, uint(_destWidth_m), uint(_destHeight_m), uint(_windowAttr_m.depth)); + + _dstFormat_p = XRenderFindVisualFormat(_x11Display_p, _windowAttr_m.visual); + _srcFormat_p = XRenderFindVisualFormat(_x11Display_p, _windowAttr_m.visual); + + _srcPicture_m = XRenderCreatePicture(_x11Display_p, _window_m, _srcFormat_p, CPRepeat, &_pictAttr_m); + _dstPicture_m = XRenderCreatePicture(_x11Display_p, _pixmap_m, _dstFormat_p, CPRepeat, &_pictAttr_m); + + XRenderSetPictureFilter(_x11Display_p, _srcPicture_m, "bilinear", nullptr, 0); + XRenderSetPictureTransform (_x11Display_p, _srcPicture_m, &_mTransform_m); + + XRenderComposite( + _x11Display_p, // *dpy, + PictOpSrc, // op, + _srcPicture_m, // src + None, // mask + _dstPicture_m, // dst + 0, // src_x + 0, // src_y + 0, // mask_x + 0, // mask_y + 0, // dst_x + 0, // dst_y + uint(_destWidth_m), // width + uint(_destHeight_m) // height + ); + + XSync(_x11Display_p, false); + + XShmGetImage(_x11Display_p, _pixmap_m, _xImage_p, 0, 0, 0x00FFFFFF); + + if (_xImage_p == nullptr) { + qWarning() << "Failed to get image from X11 server."; + return; + } + + imgdata_m.clear(); + + if (_xImage_p->byte_order == 0) { // BGR + for (int i = 0; i < _imgSize_m; i += 4) { + imgdata_m.append(_xImage_p->data[i+2]); + imgdata_m.append(_xImage_p->data[i+1]); + imgdata_m.append(_xImage_p->data[i]); + } + } else { // RGB + for (int i = 0; i < _imgSize_m; i += 4) { + imgdata_m.append(_xImage_p->data[i]); + imgdata_m.append(_xImage_p->data[i+1]); + imgdata_m.append(_xImage_p->data[i+2]); + } + } + + imageCreated(); +} + +bool hgx11grab::_getWinAttr() +{ + if (XGetWindowAttributes(_x11Display_p, _window_m, &_windowAttr_m) == 0) { + qWarning() << "Failed to obtain X11 window attributes."; + return false; + } + if (_srcWidth_m != _windowAttr_m.width || _srcHeight_m != _windowAttr_m.height) { + if (0 != _srcWidth_m) { + _freeResources(); + } + _srcWidth_m = _windowAttr_m.width; + _srcHeight_m = _windowAttr_m.height; + emit scaleChanged(); + } + return true; +} + +void hgx11grab::_freeResources() +{ + XDestroyImage(_xImage_p); + XShmDetach(_x11Display_p, &_shminfo_m); + shmdt(_shminfo_m.shmaddr); + shmctl(_shminfo_m.shmid, IPC_RMID, nullptr); + XRenderFreePicture(_x11Display_p, _srcPicture_m); + XRenderFreePicture(_x11Display_p, _dstPicture_m); + XFreePixmap(_x11Display_p, _pixmap_m); +} + +void hgx11grab::_setScale() +{ + _scale_m = (1.0 / (double(_srcWidth_m) / double(_destWidth_m))); + _mTransform_m.matrix[2][2] = XDoubleToFixed(_scale_m); +} + +// public slots + +void hgx11grab::grabFrame() +{ + if (_frameCount_m++ < _frameSkip_m) { + return; + } + _frameCount_m = 0; + _grabFrame(); +} + +// private slots + +void hgx11grab::_changeScale() +{ + _imgSize_m = _destHeight_m * _destWidth_m * 4; + _setScale(); +} diff --git a/hgx11grab.h b/hgx11grab.h new file mode 100644 index 0000000..7891c0d --- /dev/null +++ b/hgx11grab.h @@ -0,0 +1,61 @@ +#ifndef HGX11GRAB_H +#define HGX11GRAB_H + +#include +#include +#include +#include +#include +#include + +class hgx11grab : public QObject +{ + Q_OBJECT +public: + QByteArray imgdata_m; + + hgx11grab(int, ushort); + ~hgx11grab(); + + int getDest_width() const; + int getDest_height() const; + +private: + Display *_x11Display_p; + Window _window_m; + Picture _srcPicture_m; + Picture _dstPicture_m; + Pixmap _pixmap_m; + XImage *_xImage_p; + XRenderPictFormat *_srcFormat_p; + XRenderPictFormat *_dstFormat_p; + XRenderPictureAttributes _pictAttr_m; + XShmSegmentInfo _shminfo_m; + XTransform _mTransform_m; + XWindowAttributes _windowAttr_m; + int _srcWidth_m = 0; + int _srcHeight_m = 0; + int _destWidth_m; + int _destHeight_m; + int _imgSize_m; + double _scale_m; + ushort _frameCount_m = 255; + ushort _frameSkip_m = 0; + + void _grabFrame(); + bool _getWinAttr(); + void _freeResources(); + void _setScale(); + +signals: + void imageCreated(); + void scaleChanged(); + +public slots: + void grabFrame(); + +private slots: + void _changeScale(); +}; + +#endif // HGX11GRAB_H diff --git a/hgx11net.cpp b/hgx11net.cpp new file mode 100644 index 0000000..0c2779a --- /dev/null +++ b/hgx11net.cpp @@ -0,0 +1,86 @@ +#include "hgx11net.h" + +// public + +hgx11net::hgx11net(const char *host, ushort port) +{ + _sock_p = new QTcpSocket(this); + connect(_sock_p, SIGNAL(disconnected()), this, SLOT(_disconnected())); + _host_m = host; + _port_m = port; + this->_connectHost(); +} + +hgx11net::~hgx11net() +{ + clearLeds(); + _sock_p->disconnectFromHost(); +} + +void hgx11net::clearLeds() +{ + if (!_isConnected()) { + return; + } + _sock_p->write("{\"command\": \"clearall\"}\n"); +} + +void hgx11net::setLedColor(quint8 R, quint8 G, quint8 B) +{ + if (!_isConnected()) { + return; + } + _cmd_m.clear(); + _cmd_m.append("{\"color\":["); + _cmd_m.append(QString::number(R)); + _cmd_m.append(","); + _cmd_m.append(QString::number(G)); + _cmd_m.append(","); + _cmd_m.append(QString::number(B)); + _cmd_m.append("],\"command\":\"color\",\"priority\":100}}\n"); + _sock_p->write(_cmd_m); +} + +// private + +bool hgx11net::_isConnected() +{ + return (_sock_p->state() == QAbstractSocket::ConnectedState); +} + +void hgx11net::_connectHost() +{ + _sock_p->connectToHost(_host_m, _port_m); + if(!_sock_p->waitForConnected(5000)) { + qWarning() << "hxgfnet Error: " << _sock_p->errorString(); + return; + } +} + +// public slots + +void hgx11net::sendImage(QByteArray *imgdata) +{ + if (!_isConnected()) { + return; + } + _cmd_m.clear(); + _cmd_m.append("{\"command\":\"image\",\"imagedata\":\""); + _cmd_m.append(imgdata->toBase64()); + _cmd_m.append("\",\"imageheight\":"); + _cmd_m.append(imgHeight); + _cmd_m.append(",\"imagewidth\":"); + _cmd_m.append(imgWidth); + _cmd_m.append(",\"priority\":100}\n"); + _sock_p->write(_cmd_m); +} + +// private slots + +void hgx11net::_disconnected() +{ + while (!_isConnected()) { + _connectHost(); + } +} + diff --git a/hgx11net.h b/hgx11net.h new file mode 100644 index 0000000..2cce20f --- /dev/null +++ b/hgx11net.h @@ -0,0 +1,39 @@ +#ifndef HGX11NET_H +#define HGX11NET_H + +#include +#include +#include +#include +#include + +class hgx11net : public QObject +{ + Q_OBJECT +public: + QString imgWidth; + QString imgHeight; + + hgx11net(const char *, ushort); + ~hgx11net(); + + void clearLeds(); + void setLedColor(quint8, quint8, quint8); + +private: + QTcpSocket *_sock_p; + QByteArray _cmd_m; + const char *_host_m; + quint16 _port_m; + + void _connectHost(); + bool _isConnected(); + +public slots: + void sendImage(QByteArray *); + +private slots: + void _disconnected(); +}; + +#endif // HGX11NET_H diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..d2e06de --- /dev/null +++ b/main.cpp @@ -0,0 +1,67 @@ +#include +#include +#include +#include "hgx11.h" + +static hgx11 *grab; +static QCoreApplication *qapp; + +static void quit(int sig) +{ + grab->~hgx11(); + qapp->exit(sig); +} + +int main(int argc, char *argv[]) +{ + qapp = new QCoreApplication(argc, argv); + signal(SIGINT, quit); + signal(SIGTERM, quit); + + QCoreApplication::setApplicationName("hgx11"); + QCoreApplication::setApplicationVersion("0.1"); + QCommandLineParser parser; + parser.setApplicationDescription("X11 grabber for Hyperion using QT"); + parser.addHelpOption(); + parser.addVersionOption(); + QCommandLineOption address(QStringList() << "a" << "address", + QCoreApplication::translate("main", "Address to the hyperion json server. (ex. 127.0.0.1)"), + QCoreApplication::translate("main", "hyperion address") + ); + parser.addOption(address); + QCommandLineOption port(QStringList() << "p" << "port", + QCoreApplication::translate("main", "Port for the hyperion json server. (ex. 19444)"), + QCoreApplication::translate("main", "hyperion port") + ); + parser.addOption(port); + QCommandLineOption scale(QStringList() << "s" << "scale", + QCoreApplication::translate("main", "Divisor used to scale your screen resolution (ex. 8 ; if your screen is 1920x1080, the result image sent to hyperion is 240x135"), + QCoreApplication::translate("main", "scale dividor") + ); + parser.addOption(scale); + QCommandLineOption frameskip(QStringList() << "f" << "frameskip", + QCoreApplication::translate("main", "How many X11 frames to skip over. (ex. 4 ; 1 frame will be scaled and sent to hyperion and 4 will be ignored)"), + QCoreApplication::translate("main", "X11 frames to skip") + ); + parser.addOption(frameskip); + QCommandLineOption inactiveTime(QStringList() << "i" << "inactive", + QCoreApplication::translate("main", "How many seconds after the screen has not changed to turn off the LED's. Set to 0 to disable."), + QCoreApplication::translate("main", "seconds before turning off LEDs") + ); + parser.addOption(inactiveTime); + parser.process(*qapp); + + if (!parser.isSet("address")) { + qWarning() << "Hyperion address missing."; + parser.showHelp(); + } + + if (!parser.isSet("port")) { + qWarning() << "Hyperion port missing."; + parser.showHelp(); + } + + grab = new hgx11(parser.value("address"), parser.value("port"), parser.value("scale"), parser.value("frameskip"), parser.value("inactive")); + return qapp->exec(); +} +