IT Notes

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

В прошлый раз мы решили задачу формирования видео-потока. Теперь займемся сетевой частью. Реализуем передачу видео-потока по сети.

Исправление ошибки

Начнем с исправления одной неприятной ошибки, которая осталась незамеченной в предыдущей части. Она касалась некорректного управления памятью при захвате изображения курсора мыши под Linux.

Проблема заключалась в том, что конструктор QImage не делает глубокую копию переданного буфера с данными изображения. Поэтому память освобождалась и поведение приложения становилось неопределенным (хоть это и было незаметно).

Немного изменим структуру Cursor:

struct Cursor {
    QImage img;
    QPoint pos;

    // Linux-reserved
    QVarLengthArray< quint32 > buffer;
};

А теперь саму функцию получения курсора для 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;
}

Проблема решена.

Серверная часть

Для сетевого взаимодействия воспользуемся библиотекой LibQxt. О ней мы уже говорили, когда создавали простой чат, поэтому обойдемся без лишних пояснений.

Модуль назовем RemoteControlServer. Начинаем с заголовочного файла remotecontrolserver.h:

#ifndef REMOTECONTROLSERVER_H
#define REMOTECONTROLSERVER_H

#include <QObject>
#include <QxtRPCPeer>
#include <desktoprecorder.h>

class RemoteControlServer : public QObject {
    Q_OBJECT
public:
    explicit RemoteControlServer( QObject* parent = 0 );

    bool start();

private slots:
    void onClientConnected( quint64 clientID );
    void onClientDisconnected( quint64 clientID );

    void onEnableCursorCapture( quint64, bool enabled );

    void onFrameAvailable( const QImage& frame );

private:
    QxtRPCPeer m_peer;
    ITNotes::DesktopRecorder m_recorder;

};

#endif // REMOTECONTROLSERVER_H

Далее реализация в remotecontrolserver.cpp:

#include "remotecontrolserver.h"

#include <shareddefs.h>

#include <QBuffer>

static const QSize SCALED_FRAME_SIZE = QSize( 900, 550 );

RemoteControlServer::RemoteControlServer( QObject* parent ) : QObject( parent ) {
    connect( &m_peer, SIGNAL( clientConnected( quint64 ) ), SLOT( onClientConnected( quint64 ) ) );
    connect( &m_peer, SIGNAL( clientDisconnected( quint64 ) ), SLOT( onClientDisconnected( quint64 ) ) );

    connect( &m_recorder, SIGNAL( frameAvailable( QImage ) ), SLOT( onFrameAvailable( QImage ) ) );
    m_peer.attachSlot( ENABLE_CURSOR_CAPTURE_SIG, this, SLOT( onEnableCursorCapture( quint64, bool ) ) );
}

bool RemoteControlServer::start() {
    return m_peer.listen( QHostAddress::Any, PORT );
}

void RemoteControlServer::onClientConnected( quint64 clientID ) {
    qDebug() << "Client connected:" << clientID;
    m_recorder.start( 10 );
}

void RemoteControlServer::onClientDisconnected( quint64 clientID ) {
    qDebug() << "Client disconnected:" << clientID;
    m_recorder.stop();
}

void RemoteControlServer::onEnableCursorCapture( quint64, bool enabled ) {
    m_recorder.enableCursorCapture( enabled );
}

void RemoteControlServer::onFrameAvailable( const QImage& frame ) {
    QByteArray ba;
    QBuffer buffer( &ba );
    frame.scaled( SCALED_FRAME_SIZE, Qt::KeepAspectRatio, Qt::SmoothTransformation ).save( &buffer, "JPG" );

    m_peer.call( FRAME_AVAILABLE_SIG, qCompress( ba, 9 ), frame.size() );
}

Сетевое взаимодействие двунаправленное. Сервер перенаправляем кадры видео-потока, полученные от DesktopRecorder. А Клиент может включать/отключать захват курсора мыши. Связь осуществляется через именованные функции FRAME_AVAILABLE_SIG и ENABLE_CURSOR_CAPTURE_SIG, объявленные в shareddefs.h:

#ifndef SHAREDDEFS_H
#define SHAREDDEFS_H

#include <QString>

static const char* const FRAME_AVAILABLE_SIG = "FRAME_AVAILABLE_SIG";
static const char* const ENABLE_CURSOR_CAPTURE_SIG = "ENABLE_CURSOR_CAPTURE_SIG";

static const int PORT = 9900;

#endif // SHAREDDEFS_H

В этом же файле задана константа PORT, которая определяет то, на каком порту работает сервер.

Обратим внимание на то, что Сервер не просто отправляет кадры в том виде, в котором они приходят от DesktopRecorder, а уменьшает их масштаб, кодирует их в JPG и дополнительно сжимает с помощью qCompress(). В зависимости от исходного разрешения кадра размер (в байтах) получившегося изображения оказывается существенно меньше. Однако это приводит к заметной потере качества.

Также мы передаем исходный размер изображения, чтобы потом иметь возможность согласовывать координаты мыши.

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

Алгоритм получился не слишком адаптивным. Я настроил его на оптимальное соотношение частоты кадров/качества картинки для своей локальной сети. На своей системе вы можете покрутить константой SCALED_FRAME_SIZE, которая определяет масштаб пересылаемого кадра.

Клиентская часть

Позаимствуем внешний вид клиентского сетевого приложения от виджета DesktopRecorderDemo из прошлой части. При запуске Сервера на Linux и Клиента на Windows (допустимы и любые другие комбинации) имеем следующее:

video-translation-linux-windows-thumbnail

Обращаю внимание, что на форме виджета можно указать ip-адрес или имя хоста для сервера, к которому нужно подключиться.

Заголовочный файл remotecontrolclientwidget.h:

#ifndef REMOTECONTROLCLIENTWIDGET_H
#define REMOTECONTROLCLIENTWIDGET_H

#include <QWidget>
#include <QxtRPCPeer>

namespace Ui {
class RemoteControlClientWidget;
}

class RemoteControlClientWidget : public QWidget {
    Q_OBJECT

public:
    explicit RemoteControlClientWidget( QWidget* parent = 0 );
    ~RemoteControlClientWidget();

private slots:
    void onStartStop();
    void onFrameAvailable( const QByteArray& imgData, const QSize& realSize );

    void onConnectedToServer();
    void onDisconnectedFromServer();
    void onServerError( const QAbstractSocket::SocketError& error );
    void refreshConnection();

private:
    Ui::RemoteControlClientWidget* ui;

    QxtRPCPeer m_peer;

    bool m_connected;
};

#endif // REMOTECONTROLCLIENTWIDGET_H

И реализация remotecontrolclientwidget.cpp:

#include "remotecontrolclientwidget.h"
#include "ui_remotecontrolclientwidget.h"

#include <shareddefs.h>

#include <QTimer>

static const int CONNECTION_RETRY_TIME_OUT_MSEC = 2 * 1000; // 2 секунды

RemoteControlClientWidget::RemoteControlClientWidget( QWidget* parent ) :
    QWidget( parent ),
    ui( new Ui::RemoteControlClientWidget ),
    m_connected( false ) {
    ui->setupUi( this );

    ui->stopBn->hide();

    connect( ui->startBn, SIGNAL( clicked( bool ) ), SLOT( onStartStop() ) );
    connect( ui->stopBn, SIGNAL( clicked( bool ) ), SLOT( onStartStop() ) );

    connect( &m_peer, SIGNAL( connectedToServer() ), SLOT( onConnectedToServer() ) );
    connect( &m_peer, SIGNAL( disconnectedFromServer() ), SLOT( onDisconnectedFromServer() ) );
    connect(
        &m_peer,
        SIGNAL( serverError( const QAbstractSocket::SocketError& ) ),
        SLOT( onServerError( const QAbstractSocket::SocketError& ) )
    );

    m_peer.attachSlot( FRAME_AVAILABLE_SIG, this, SLOT( onFrameAvailable( QByteArray, QSize ) ) );
    m_peer.attachSignal( ui->captureCursorChkBox, SIGNAL( clicked( bool ) ), ENABLE_CURSOR_CAPTURE_SIG );
}

RemoteControlClientWidget::~RemoteControlClientWidget() {
    delete ui;
}

void RemoteControlClientWidget::onStartStop() {
    if( ui->startBn->isVisible() ) {
        refreshConnection();
    } else {
        m_peer.disconnectServer();
        m_connected = false;
    }

    ui->startBn->setVisible( !ui->startBn->isVisible() );
    ui->stopBn->setVisible( !ui->stopBn->isVisible() );
    ui->ipEd->setEnabled( !ui->stopBn->isVisible() );
}

void RemoteControlClientWidget::onFrameAvailable( const QByteArray& imgData, const QSize& realSize ) {
    Q_UNUSED( realSize )

    auto img = QImage::fromData( qUncompress( imgData ), "JPG" );
    ui->viewLbl->setPixmap( QPixmap::fromImage( img ).scaled(
        ui->viewLbl->size(),
        Qt::KeepAspectRatio,
        Qt::SmoothTransformation
    ));
}

void RemoteControlClientWidget::onConnectedToServer() {
    qDebug() << "Connected to server";
    m_connected = true;

    ui->startBn->setVisible( false );
    ui->stopBn->setVisible( true );
    ui->ipEd->setEnabled( false );

    m_peer.call( ENABLE_CURSOR_CAPTURE_SIG, ui->captureCursorChkBox->isChecked() );
}

void RemoteControlClientWidget::onDisconnectedFromServer() {
    qDebug() << "Disconnected from server";
    m_connected = false;
}

void RemoteControlClientWidget::onServerError( const QAbstractSocket::SocketError& error ) {
    qDebug() << "Server error:" << QString::number( error, 16 );
    m_connected = false;
    if( ui->stopBn->isVisible() ) {
        QTimer::singleShot( CONNECTION_RETRY_TIME_OUT_MSEC, this, SLOT( refreshConnection() ) );
    }
}

void RemoteControlClientWidget::refreshConnection() {
    if( !m_connected ) {
        m_peer.connect( ui->ipEd->text(), PORT );
    }
}

Выводы

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

Исходники

 Скачать исходники примера RemoteControlDemo

Также они доступны на github: https://github.com/itnotesblog/RemoteControlDemo под тэгом v.0.2.1.

Сборка проекта проверялась под Linux и Windows с Qt 4.8.4 и компиляторами gcc (x64) и msvc.2010 (32-bit) соответственно.

Обратите внимание, что у проекта есть зависимость от LibQxt. Вам понадобится либо использовать совместимый компилятор, либо пересобрать LibQxt самостоятельно из исходников.

Для Windows-версии dll-файлы LibQxt расположены в lib.win32/LibQxt/. Не забудьте скопировать их в bin/(debug|release)/.

Похожие публикации

Комментарии

bin/(debug|release)/ это надо скопировать в директорию tq или проекта ? тогда подключится библиотека ?

Anonymous:

bin/(debug|release)/ это надо скопировать в директорию tq или проекта ? тогда подключится библиотека ?

В директорию проекта. Так, чтобы dll-библиотеки LibQxt лежали рядом с получившимися исполняемыми файлами RemoteControlServer.exe и RemoteControlClient.exe.

Программа завершился с кодом -1073741515 как это исправить ?

Anonymous:

Программа завершился с кодом -1073741515 как это исправить ?

Здравствуйте. Какая именно программа (Клиент/Сервер или оба)? Насколько я понимаю, запуск осуществляется под Windows? С какой версией компилятора и Qt работаете? Ошибка выдается в сборке Release/Debug или в обоих случаях?

Не удалось запустить программу. Путь или права недопустимы?

C:\Qt\qtcreator-2.5.2\RemoteControlDemo-v.0.2.1\bin\debug\RemoteControlClient.exe завершился с кодом -1 Клиент не запускается

C:\Qt\qtcreator-2.5.2\RemoteControlDemo-v.0.2.1\bin\debug\RemoteControlServer.exe завершился с кодом -1 сервер тоже в режиме отладки не запускается . Запуск под Windows ,Qt 4.8.4 ,Qt Creator v2.5.2 ,компилятор msvc.2010 (32-bit).

Anonymous:

сервер тоже в режиме отладки не запускается

А как дела с Релизом? Стартует ли DesktopRecorderDemo.exe? Пробовали запускаться и из Qt Creator, и из файловой системы?

Могу посоветовать приложение Dependency Walker. Оно позволяет исследовать зависимости исполняемых файлов от библиотек. Возможно, какой-то из dll-файлов не виден. В Дебаге Клиент с Сервером дополнительно должны зависеть от QxtCored.dll и QxtNetworkd.dll (кроме основных зависимостей), а в Релизе d на конце имен библиотек исчезает - QxtCore.dll и QxtNetwork.dll.

Дополнительно можете попробовать провести сборку на чистом проекте из консоли Visual Studio Command Prompt (2010), которая находится среди инструментов Microsoft Visual Studio 2010 --> Visual Studio Tools. Для этого достаточно выполнить команды "qmake && nmake" (без кавычек).

Если в системе установлено несколько сборок Qt, то очень похоже на путаницу в переменной среды окружения Path. Попробуйте ее перепроверить.

клиент работает а сервер собрался с ошибкой запускал через Qt 4.8.4 Command Promt Ошибка link Fatal error Lin1104 не удается открыть DesktopRecorder.lib . Что делать ?

Anonymous:

клиент работает а сервер собрался с ошибкой запускал через Qt 4.8.4 Command Promt Ошибка link Fatal error Lin1104 не удается открыть DesktopRecorder.lib . Что делать ?

А сборка осуществляется из корневого каталога проекта? Убедитесь, что библиотека DesktopRecorder собралась, а в lib.win32/ появились файлы DesktopRecorder.lib и DesktopRecorder.dll.

она наверно не собралась вот путь как я собирал C:\Qt\qtcreator-2.5.2\RemoteControlDemo-v.0.2.1\src\RemoteControlServer

Anonymous:

она наверно не собралась вот путь как я собирал C:\Qt\qtcreator-2.5.2\RemoteControlDemo-v.0.2.1\src\RemoteControlServer

Попробуйте выполнить "qmake && nmake" из чистого каталога проекта в C:\Qt\qtcreator-2.5.2\RemoteControlDemo-v.0.2.1\

да работает спасибо =)

Пожалуйста :)

А как установить Qt 4.8.4 Linux Ubuntu 14.04 ?

Anonymous:

А как установить Qt 4.8.4 Linux Ubuntu 14.04 ?

Для Linux не так принципиальна точная версия Qt. Можете попробовать с любой сборкой Qt4.

Единственная важная деталь - я собирал LibQxt в 64-бит, поэтому и Qt нужен соответствующий.

:-1: ошибка: skipping incompatible /home/project/RemoteControlDemo-v.0.2.1//lib.linux//LibQxt//libQxtCore.so when searching for -lQxtCore

:-1: ошибка: cannot find -lQxtCore

:-1: ошибка: skipping incompatible /home/project/RemoteControlDemo-v.0.2.1//lib.linux//LibQxt//libQxtNetwork.so when searching for -lQxtNetwork

:-1: ошибка: cannot find -lQxtNetwork

:-1: ошибка: collect2: error: ld returned 1 exit status

как исправить ошибки

Здравствуйте. Сообщите версию gcc (команда "gcc -v") и Qt (команда "qmake -v"), используемые для сборки.

Qt 4.8.6 GCC 4.8.3 20140627 gcc-4_8-branch revision 212064

Anonymous:

Qt 4.8.6 GCC 4.8.3 20140627 gcc-4_8-branch revision 212064

Ошибка про "skipping incompatible … when searching for" явно указывает на несовместимость архитектур (32/64-bit) сборки и линкуемой библиотеки.

Вы собираете проект прямо из консоли? Прошу все же прислать сюда полный вывод команды "gcc -v" (без кавычек).

А почему в папке релиз нету исполняемого файла программы ?

Anonymous:

А почему в папке релиз нету исполняемого файла программы ?

Под какой системой осуществляется сборка? Выводятся ли какие-нибудь ошибки?

линукс ошибок нет программа работает а выходных файлов нету

есть только Make file ?

Anonymous:

линукс ошибок нет программа работает а выходных файлов нету

Похоже, что либо собирается debug, либо используется режим Qt Creator "Shadow build".

debug тоже нету а как по русски этот режим называется ?

Shadow build = Теневая сборка

сборка под виндовс получил EXE файлы но при запуске пишет нет соответствующих библиотек как запустить программу под виндовс ?

Anonymous:

сборка под виндовс получил EXE файлы но при запуске пишет нет соответствующих библиотек как запустить программу под виндовс ?

Говорится ли о том, каких именно библиотек не хватает? Скопированы ли dll-файлы из lib.win32/LibQxt/ в каталог к исполняемым?

не хватает библиотек QtGui4.dll , QxtCore.dll

Anonymous:

не хватает библиотек QtGui4.dll , QxtCore.dll

QtGui4.dll - библиотека, которая находится в bin-каталоге Qt, а QxtCore.dll прилагается к проекту, и расположена в lib.win32/LibQxt/.

Вероятно, понадобятся и другие библиотеки. Все они явно присутствуют на компьютере, если сборка проекта прошла успешно. Можете найти их через функцию поиска Windows.

да я нашел библиотеки приложение запустилось но нет изображения рабочего стола какой то библиотеки не хватает ?

Anonymous:

да я нашел библиотеки приложение запустилось но нет изображения рабочего стола какой то библиотеки не хватает ?

Если бы не хватало библиотек, то приложение не запустилось бы.

Требуется уточнить, о каком именно приложении идет речь. Работает ли сервер? Реагирует ли он на подключение от клиента?

Для начала можно попробовать DesktopRecorderDemo.exe. С ним проблем быть не должно.

DesktopRecorderDemo.exe он работает сервер подключается и выключается а какртинки на клиенте нету ?

Клиент и сервер запускаются на одном и том же компьютере? Какая версия Windows используется?

И я так и не понял, выводятся ли кадры через DesktopRecorderDemo.exe?

Возможно, приложения выдают какие-то предупреждения? Проблема может быть в отсутствии плагина для кодирования/декодирования Jpeg, но это маловероятно.

Хотя не так уж и маловероятно, что не находит плагины, если не находило QtGui4.dll. Попробуйте скопировать к исполняемым файлам проекта весь каталог imageformats/ (не содержимое, а именно каталог) из директории plugins/, который находится в корневом каталоге установленной версии Qt. Должно помочь.

Но лучше все же настроить пути в переменных окружения.