В прошлый раз мы в целом продумали архитектуру приложения, схожего с TeamViewer. Эта часть посвящена разработке библиотеки, которая поможет нам формировать видео-поток происходящего на экране.
Изначально планировалось, что модуль будет называться InputRecorder
. Но у меня произошло небольшое переосмысление задачи, поэтому было решено сосредоточиться на записи экрана, и не примешивать сюда события мыши и клавиатуры. Соответствующей библиотеке дадим имя DesktopRecorder
.
Видео-поток с экрана представляет собой всего лишь последовательность скриншотов, транслируемых с определенной частотой.
Сделать скриншот в Qt сверх-просто:
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()
.
Для передачи информации о курсоре определим структуру:
struct Cursor {
QImage img;
QPoint pos;
// Linux-reserved
QVarLengthArray< quint32 > buffer;
};
Она хранит изображение иконки, которая используется в качестве графического представления курсора, а также позицию левого верхнего угла, где курсор должен быть отрисован на скриншоте.
Тогда интерфейс функции получения курсора довольно прост:
Cursor captureCursor() const;
Для 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;
}
Теперь версия функции для Windows:
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.h
:
#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
:
#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
:
linux-g++: LIBS += -lX11 -lXfixes
win32: LIBS += -lUser32 -lGdi32
Для разных ОС - разные библиотеки.
Сам видео-поток представляет собой всего лишь последовательность скриншотов, отправляемых по событиям таймера QTimer.
Проверим, что все работает. Создадим тестовое приложение DesktopRecorderDemo
, которое использует возможности нашей библиотеки. Заголовочный файл desktoprecorderdemo.h
:
#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
:
#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
-формы виджета. Под Windows приложение выглядит примерно так:
Видео-трансляция у нас есть. Следующий шаг - организовать ее передачу по сети. Это и станет темой следующей части…
Скачать исходники примера RemoteControlDemo
Также они доступны на github: https://github.com/itnotesblog/RemoteControlDemo под тэгом v.0.1.1
.
Сборка проекта проверялась под Linux и Windows с Qt 4.8.4 и компиляторами gcc и msvc.2010 соответственно.
#include <QDesktopWidget> ошибка :LNK1181: не удается открыть входной файл "DesktopRecorder.lib"
как исправить ?
Anonymous:
#include <QDesktopWidget> ошибка :LNK1181: не удается открыть входной файл "DesktopRecorder.lib"
как исправить ?
Скорее всего, используется несовместимая версия Qt. Попробуйте пересобрать с Qt 4.8 (лучше всего 4.8.4).
Anonymous
с не терпеньем жду продолжения!!!