В прошлый раз мы в целом продумали архитектуру приложения, схожего с TeamViewer. Эта часть посвящена разработке библиотеки, которая поможет нам формировать видео-поток происходящего на экране.
Изначально планировалось, что модуль будет называться InputRecorder. Но у меня произошло небольшое переосмысление задачи, поэтому было решено сосредоточиться на записи экрана, и не примешивать сюда события мыши и клавиатуры. Соответствующей библиотеке дадим имя DesktopRecorder.
Скриншоты в Qt
Видео-поток с экрана представляет собой всего лишь последовательность скриншотов, транслируемых с определенной частотой.
Сделать скриншот в Qt сверх-просто:
1 2 3 4 5 6 7 |
QImage DesktopRecorder::makeScreenShot() const { auto desktop = QApplication::desktop(); auto geom = desktop->screenGeometry(); auto pix = QPixmap::grabWindow( desktop->winId(), geom.x(), geom.y(), geom.width(), geom.height() ); return pix.toImage(); } |
Представленная функция получает скриншот с экрана по умолчанию (принципиально на многоэкранных системах), и работает одинаково хорошо в Linux и Windows. Теперь можно было бы добавить запуск этой функции по таймеру, и просто генерировать поток кадров. Но это еще не все. Нам хотелось бы иметь возможность захватывать не только содержимое экрана, но и курсор. Получившая реализация этим похвастаться не может.
Дополняем скриншот курсором мыши
Чтобы получить скриншот с курсором мыши, придется прибегнуть к специфичным API конкретных платформ. В Linux для этого подойдет расширение X11 — XFixes, а в Windows — функции Win32 API: GetCursorInfo() и GetIconInfo().
Для передачи информации о курсоре определим структуру:
1 2 3 4 5 6 7 |
struct Cursor { QImage img; QPoint pos; // Linux-reserved QVarLengthArray< quint32 > buffer; }; |
Она хранит изображение иконки, которая используется в качестве графического представления курсора, а также позицию левого верхнего угла, где курсор должен быть отрисован на скриншоте.
Тогда интерфейс функции получения курсора довольно прост:
1 |
Cursor captureCursor() const; |
Функция получения курсора в Linux
Для Linux реализация получилась несколько проще, поэтому начнем с нее:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
Cursor DesktopRecorder::captureCursor() const { Cursor cursor; if( auto curImage = XFixesGetCursorImage( QX11Info::display() ) ) { cursor.buffer.resize( curImage->width * curImage->height ); for( int i = 0; i < cursor.buffer.size(); ++i ) { cursor.buffer[ i ] = curImage->pixels[ i ] & 0xffffffff; } cursor.img = QImage( reinterpret_cast< const uchar* >( cursor.buffer.data() ), curImage->width, curImage->height, QImage::Format_ARGB32_Premultiplied ); cursor.pos = QCursor::pos() - QPoint( curImage->xhot, curImage->yhot ); XFree( curImage ); } return cursor; } |
Функция получения курсора в Windows
Теперь версия функции для Windows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
QPixmap bottomPart( const QPixmap& pixmap ) { QSize size( pixmap.width(), pixmap.height() / 2 ); return pixmap.copy( QRect( QPoint( 0, size.height() ), size ) ); } Cursor DesktopRecorder::captureCursor() const { Cursor cursor; CURSORINFO cursorInfo = { 0 }; cursorInfo.cbSize = sizeof(cursorInfo); if( GetCursorInfo( &cursorInfo ) ) { ICONINFO ii = { 0 }; if( GetIconInfo( cursorInfo.hCursor, &ii ) ) { cursor.pos = QCursor::pos() - QPoint( ii.xHotspot, ii.yHotspot ); DIBSECTION dsBitmap; DIBSECTION dsMask; if( GetObject( ii.hbmColor, sizeof( DIBSECTION ), &dsBitmap ) ) { cursor.img = QPixmap::fromWinHBITMAP( ii.hbmColor, QPixmap::PremultipliedAlpha ).toImage(); } else if( GetObject( ii.hbmMask, sizeof( DIBSECTION ), &dsMask ) ) { auto pMask = QPixmap::fromWinHBITMAP( ii.hbmMask, QPixmap::Alpha ); cursor.img = QImage( pMask.width(), pMask.height() / 2, QImage::Format_ARGB4444_Premultiplied ); cursor.img.fill( Qt::black ); QPainter p; p.begin( &cursor.img ); p.setCompositionMode( QPainter::CompositionMode_DestinationIn ); p.drawPixmap( 0, 0, bottomPart( pMask ) ); p.end(); } DeleteObject( ii.hbmColor ); DeleteObject( ii.hbmMask ); } } return cursor; } |
На самом деле, эти функции могут работать несколько нестабильно при каких-то особых ситуациях. Но в большинстве случаев их поведение нас устроит.
Все вместе: Класс DesktopRecorder
Теперь посмотрим на все это вместе. Заголовочный файл desktoprecorder.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
#ifndef DESKTOPRECORDER_H #define DESKTOPRECORDER_H #include "desktoprecorder_global.h" #include <QObject> #include <QTimer> #include <QImage> #include <QVarLengthArray> namespace ITNotes { static const uint MIN_FPS = 1; static const uint MAX_FPS = 100; static const uint DEFAULT_FPS = 30; struct Cursor { QImage img; QPoint pos; // Linux-reserved QVarLengthArray< quint32 > buffer; }; class DESKTOPRECORDERSHARED_EXPORT DesktopRecorder : public QObject { Q_OBJECT public: explicit DesktopRecorder( QObject* parent = 0 ); QImage makeScreenShot() const; Cursor captureCursor() const; signals:: void frameAvailable( const QImage& frame ); public slots: void start( uint fps = DEFAULT_FPS ); void stop(); void enableCursorCapture( bool enabled = true ); private slots: void onTimeOut(); private: QTimer m_timer; bool m_cursorCaptureEnabled; }; } #endif // DESKTOPRECORDER_H |
Действие функций-членов, сигналов и слотов класса должно быть понятно из их названий. Однако стоит отметить следующую особенность: необходимость захвата курсора мыши определяется полем m_cursorCaptureEnabled. Если эта переменная имеет значение true, то курсор будет отображен на скриншоте, полученном с помощью makeScreenShot(), иначе — не будет.
Далее переходим к файлу реализации desktoprecorder.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
#include "desktoprecorder.h" #include <QImage> #include <QDesktopWidget> #include <QApplication> #include <QPainter> #include <QDebug> #ifdef Q_OS_LINUX # include <X11/extensions/Xfixes.h> # include <QX11Info> #elif defined Q_OS_WIN32 # include <Windows.h> #endif namespace ITNotes { DesktopRecorder::DesktopRecorder( QObject* parent ) : QObject( parent ), m_cursorCaptureEnabled( false ) { connect( &m_timer, SIGNAL( timeout() ), SLOT( onTimeOut() ) ); } QImage DesktopRecorder::makeScreenShot() const { auto desktop = QApplication::desktop(); auto geom = desktop->screenGeometry(); auto pix = QPixmap::grabWindow( desktop->winId(), geom.x(), geom.y(), geom.width(), geom.height() ); if( m_cursorCaptureEnabled ) { auto cursor = captureCursor(); if( !cursor.img.isNull() ) { QPainter p; p.begin( &pix ); p.drawImage( cursor.pos, cursor.img ); p.end(); } } return pix.toImage(); } #ifdef Q_OS_LINUX Cursor DesktopRecorder::captureCursor() const { Cursor cursor; if( auto curImage = XFixesGetCursorImage( QX11Info::display() ) ) { cursor.buffer.resize( curImage->width * curImage->height ); for( int i = 0; i < cursor.buffer.size(); ++i ) { cursor.buffer[ i ] = curImage->pixels[ i ] & 0xffffffff; } cursor.img = QImage( reinterpret_cast< const uchar* >( cursor.buffer.data() ), curImage->width, curImage->height, QImage::Format_ARGB32_Premultiplied ); cursor.pos = QCursor::pos() - QPoint( curImage->xhot, curImage->yhot ); XFree( curImage ); } return cursor; } #elif defined Q_OS_WIN32 QPixmap bottomPart( const QPixmap& pixmap ) { QSize size( pixmap.width(), pixmap.height() / 2 ); return pixmap.copy( QRect( QPoint( 0, size.height() ), size ) ); } Cursor DesktopRecorder::captureCursor() const { Cursor cursor; CURSORINFO cursorInfo = { 0 }; cursorInfo.cbSize = sizeof(cursorInfo); if( GetCursorInfo( &cursorInfo ) ) { ICONINFO ii = { 0 }; if( GetIconInfo( cursorInfo.hCursor, &ii ) ) { cursor.pos = QCursor::pos() - QPoint( ii.xHotspot, ii.yHotspot ); DIBSECTION dsBitmap; DIBSECTION dsMask; if( GetObject( ii.hbmColor, sizeof( DIBSECTION ), &dsBitmap ) ) { cursor.img = QPixmap::fromWinHBITMAP( ii.hbmColor, QPixmap::PremultipliedAlpha ).toImage(); } else if( GetObject( ii.hbmMask, sizeof( DIBSECTION ), &dsMask ) ) { auto pMask = QPixmap::fromWinHBITMAP( ii.hbmMask, QPixmap::Alpha ); cursor.img = QImage( pMask.width(), pMask.height() / 2, QImage::Format_ARGB4444_Premultiplied ); cursor.img.fill( Qt::black ); QPainter p; p.begin( &cursor.img ); p.setCompositionMode( QPainter::CompositionMode_DestinationIn ); p.drawPixmap( 0, 0, bottomPart( pMask ) ); p.end(); } DeleteObject( ii.hbmColor ); DeleteObject( ii.hbmMask ); } } return cursor; } #endif void DesktopRecorder::start( uint fps ) { if( fps < MIN_FPS || MAX_FPS < fps ) { fps = DEFAULT_FPS; } m_timer.start( 1000 / fps ); } void DesktopRecorder::stop() { m_timer.stop(); } void DesktopRecorder::enableCursorCapture( bool enabled ) { m_cursorCaptureEnabled = enabled; } void DesktopRecorder::onTimeOut() { emit frameAvailable( makeScreenShot() ); } } |
То, что следует за #ifdef Q_OS_LINUX относится к Linux-версии, а то, что находится после #elif defined Q_OS_WIN32 — к Windows. Компилятор просто проигнорирует строки кода, которые не соответствуют текущей операционной системе.
Похожая ситуация наблюдается и в pro-файле библиотеки DesktopRecorder:
1 2 |
linux-g++: LIBS += -lX11 -lXfixes win32: LIBS += -lUser32 -lGdi32 |
Для разных ОС — разные библиотеки.
Сам видео-поток представляет собой всего лишь последовательность скриншотов, отправляемых по событиям таймера QTimer.
Демонстрационный пример
Проверим, что все работает. Создадим тестовое приложение DesktopRecorderDemo, которое использует возможности нашей библиотеки. Заголовочный файл desktoprecorderdemo.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#ifndef DESKTOPRECORDERDEMO_H #define DESKTOPRECORDERDEMO_H #include <QWidget> #include <desktoprecorder.h> namespace Ui { class DesktopRecorderDemo; } class DesktopRecorderDemo : public QWidget { Q_OBJECT public: explicit DesktopRecorderDemo( QWidget* parent = 0 ); ~DesktopRecorderDemo(); private slots: void onStartStop(); void onFrameAvailable( const QImage& img ); private: Ui::DesktopRecorderDemo* ui; ITNotes::DesktopRecorder m_recorder; }; #endif // DESKTOPRECORDERDEMO_H |
Реализация в desktoprecorderdemo.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
#include "desktoprecorderdemo.h" #include "ui_desktoprecorderdemo.h" DesktopRecorderDemo::DesktopRecorderDemo( QWidget* parent ) : QWidget( parent ), ui( new Ui::DesktopRecorderDemo ) { ui->setupUi( this ); ui->stopBn->hide(); connect( ui->startBn, SIGNAL( clicked( bool ) ), SLOT( onStartStop() ) ); connect( ui->stopBn, SIGNAL( clicked( bool ) ), SLOT( onStartStop() ) ); connect( ui->captureCursorChkBox, SIGNAL( clicked( bool ) ), &m_recorder, SLOT( enableCursorCapture( bool ) ) ); connect( &m_recorder, SIGNAL( frameAvailable( QImage ) ), SLOT( onFrameAvailable( QImage ) ) ); } DesktopRecorderDemo::~DesktopRecorderDemo() { delete ui; } void DesktopRecorderDemo::onStartStop() { ui->startBn->isVisible() ? m_recorder.start() : m_recorder.stop(); ui->startBn->setVisible( !ui->startBn->isVisible() ); ui->stopBn->setVisible( !ui->stopBn->isVisible() ); } void DesktopRecorderDemo::onFrameAvailable( const QImage& img ) { ui->viewLbl->setPixmap( QPixmap::fromImage( img ).scaled( ui->viewLbl->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation ) ); } |
Получилось довольно лаконично. Это объясняется использованием Qt Designer для формирования ui-формы виджета.
Выводы
Видео-трансляция у нас есть. Следующий шаг — организовать ее передачу по сети. Это и станет темой следующей части…
Исходники
Также они доступны на github: https://github.com/itnotesblog/RemoteControlDemo под тэгом v.0.1.1.
Сборка проекта проверялась под Linux и Windows с Qt 4.8.4 и компиляторами gcc и msvc.2010 соответственно.