IT Notes

Удаленное управление компьютером по сети: Формирование видео-потока

В прошлый раз мы в целом продумали архитектуру приложения, схожего с TeamViewer. Эта часть посвящена разработке библиотеки, которая поможет нам формировать видео-поток происходящего на экране.

Изначально планировалось, что модуль будет называться InputRecorder. Но у меня произошло небольшое переосмысление задачи, поэтому было решено сосредоточиться на записи экрана, и не примешивать сюда события мыши и клавиатуры. Соответствующей библиотеке дадим имя DesktopRecorder.

Скриншоты в Qt

Видео-поток с экрана представляет собой всего лишь последовательность скриншотов, транслируемых с определенной частотой.

Сделать скриншот в 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

Для 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

Теперь версия функции для 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

Теперь посмотрим на все это вместе. Заголовочный файл 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 приложение выглядит примерно так:

desktop-recorder-demo-thumbnail

Выводы

Видео-трансляция у нас есть. Следующий шаг - организовать ее передачу по сети. Это и станет темой следующей части

Исходники

 Скачать исходники примера 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).