Help us understand the problem. What is going on with this article?

Qt Quick WebGL Streaming Pluginを触ってみる

More than 1 year has passed since last update.

はじめに

ついに始まりました、Qt Advent Calendar 2018。昨日は、@kihaku_dolさんによるQtQuickでNEMに触れてみる」でした。Web経由のAPI操作をQt Quickで行っています。応答をシンプルにテキストのまま表示されているので、初めの一歩として触ってみるのに良さそうです。

さて、4日目の本記事では、まだあまり利用されたことがないであろうQt Quick WebGL Streaming Pluginについてご紹介します。

Qt Quick WebGL Streaming Pluginとは

Qt Quick WebGL Streamingは、ブラウザからリモート上のQt Quickアプリケーションを実行するQPA(Qt Platform Abstraction)プラグインです。このプラグインを利用すると、リモート上で実行したQt Quickアプリケーションがブラウザ上に描画され、ブラウザ上で利用することができるようになります。
このプラグインは、Qt 5.10から既にTechnology Preview入りしていましたが、間もなく正式リリースされるであろうQt 5.12で正式モジュールとなります。

なお、今回の記事は、CentOS7上のQt 5.12rc2 にて動作検証をしています。

まずは使ってみよう

このプラグインは、 QPA機構を使っているため、起動時のパラメータに指定するだけでQt Quick アプリケーションをブラウザ経由でアクセスできるようになります。

最初は、Qt Quick Demo - Clocksを試してみましょう。
こちらのデモは、QtのExamplesにあります。

selectclocks.png

まずは、通常通りにビルドして、実行してみて下さい。

Screenshot from 2018-12-03 11-40-35.png

ウィンドウが開き、画面上に時計が3つほど表示されます。
画面上でマウスを左クリックしたまま右にドラッグすると隠れている時計が表示されます。
一度、アプリケーションを終了させて、プラットフォームプラグイン選択の引数を追加しましょう。

Screenshot from 2018-12-03 11-41-02.png

プロジェクトを開き、Run設定のCommand line argumentsに、以下の引数を追加してください。

-platform webgl

この状態でアプリケーションの実行を選択すると、今度はウィンドウが表示されずに実行状態になります。そこで、Canvasに対応したWebブラウザからアクセスを行います。ブラウザを立ち上げ、以下のURLにアクセスしてください。

http://127.0.0.1:8080

手元の実験ではGoogle Chromeを使っています。

Screenshot from 2018-12-03 11-41-20.png

ブラウザのウィンドウサイズが大きかったため、4つほど時計が表示された状態ですが、ブラウザ上でウィンドウアプリケーションの場合と同じくマウス操作を行う事ができます。

ついでに、テキストエディタアプリの動作も見てみましょう。
Screenshot from 2018-12-03 12-44-17.png

今回はportを変えてみます。

実行時引数を以下のように設定します。

-platform webgl:port=8088

ブラウザで、以下のURLを入力します。

http://127.0.0.1:8088

Screenshot from 2018-12-03 12-44-46.png

文字の追記や、カット&ペースト、ボールド指定、イタリック指定など、いくつかの機能は動作します。ただし、ファイル操作のダイアログや、カラー選択用のダイアログを開こうとするとアプリケーションがクラッシュしました。

まぁ、ファイルダイアログはネイティブダイアログでしょうし、ダイアログを開く事自体が制限にひっかかっているのかもしれません。

メリットとデメリット

WebGLのメリットは、実装に変更を加えることなく、Qt Quickアプリケーションにリモート実行の機能を提供できることです。レンダリングするGLコマンドをシリアライズして送信するため、VNCに比べれば、ネットワークの負荷も少なくなります。

デメリットとしては、VNCに比べて負荷が少ないとはいえ、ソケット通信をし続けるため、通信量はそれなりに多く、ネットワークに負荷をかけますし、リモート用のプロセスはリモート専用になってしまい、サーバ側とブラウザ側両方で画面表示をする用途には利用できません。

また、ようやく正式モジュールになったとはいえ、まだまだ動作が不安定ですので、趣味を超えて利用を検討する段階にはないというのが、触ってみた感想です。

仕組みについて

まだ問題が見受けられて、すぐに製品に使うというわけにもいかない雰囲気でしたが、実用に耐えうるレベルにまでなれば、将来的にQt Quickでアプリケーションを作っておくと、ブラウザから操作させるWebアプリケーション化もできそうです。このモジュールをまともに利用できるようにするためには、使ってみたい人たちの貢献が欠かせない気がします。

そこで、トラブルがあったときにどこを見ていけばよさそうなのか、指針になるよう仕組みを軽く見てみたいと思います。

動作上の仕組み

WebGL Streamingプラグインを選択すると、アプリケーションはウィンドウを起動する代わりに、軽量なWeb Serverを起動し、指定されたport(デフォルトは8080)で待ち合わせます。

アクセスして取得されるのは、単純にcanvasタグをbodyにもちwebqt.jsをスクリプトとして呼び出すだけのHTMLです。

<html><head profile="http://www.w3.org/2005/10/profile">
    <link rel="icon" type="image/png" href="/favicon.png">
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>clocks</title>
    <script>
      var startTime = (new Date()).getTime();
    </script>
    <script type="text/javascript" src="webqt.js"></script>
    <script>
      var endTime = (new Date()).getTime();
      console.log("Took " + (endTime - startTime) + " milliseconds to parse and execute");
    </script>
  </head>
  <body>
  <canvas id="1" width="635" height="969" style="position: absolute; left: 0px; top: 0px; width: 635px; height: 969px; z-index: 2;"></canvas>
  </body>
</html>

実体は、JavaScriptの方ですが、1000行を超えるのでコードは割愛します。
こちらは最初にWebSocketをopenして、ブラウザ側から描画領域のサイズなどをアプリケーションに通知、canvasに書くべきOpenGLコマンドを取得し、canvasへと描画していきます。

ブラウザ側は、マウスやキーボードなどの入力イベントをWebSocketを通じてアプリケーション側に通知、それによるアプリケーションの変化を受け取り描画するといったイベントに応じた挙動となっています。

Qtでは、イベントや描画等のプラットフォーム依存の処理が QPA というプラグインを経由してQtに必要なものに変換されます。通常、OpenGLの命令が、ウィンドウへの描画ではなく、軽量 Web Serverとブラウザ間の WebSocketを通じて送受信され、ブラウザが作ったWebSocketへのイベント通知を、Qtが理解できるイベントへと変換することでWebGL Streamingが実現されています。

QPAとは

ところで、QPAとは何かを説明していませんでした。Qt Platform Abstraction(QPA)は、Qt 4.8のあたりから導入されたGUIの抽象化レイヤーです。各種プラットフォームとQtの合わせ込みを行うレイヤーで、「QPlatform*」という名称のクラス群をサブクラス化することで実装されます。

  • QPlatformIntegration
    • QAbstractEventDispatcher
    • QPlatformAccessibility
    • QPlatformBackingStore
    • QPlatformClipboard
    • QPlatformCursor
    • QPlatformDrag
    • QPlatformFontDatabase
    • QPlatformGraphicsBuffer
    • QPlatformInputContext
    • QPlatformNativeInterface
    • QPlatformOffscreenSurface
    • QPlatformOpenGLContext
    • QPlatformScreen
    • QPlatformServices
    • QPlatformSharedGraphicsCache
    • QPlatformSurface
    • QPlatformWindow
  • QPlatformTheme
    • QPlatformDialogHelper
    • QPlatformMenu
    • QPlatformMenuBar
    • QPlatformMenuItem
    • QPlatformSystemTrayIcon
    • platform palettes
    • platform fonts
    • theme hints

webglとwayland以外の一般的なQPAのソースコードは以下の場所にあります。

qtbase/src/plugins/platforms

waylandは、以下の場所にあります。

qtwayland/src/plugins/platforms/

現在用意されているwebgl以外のQPAは以下の通りです。

  • qandroid
    Android用QPA

  • qbsdfb
    BSD Frame Buffer用QPA

  • qcocoa
    Cocoa(macOSフレームワーク)用QPA

  • qdirect2d
    ラスター描画にDirect2Dを使ったWindows用QPA

  • qdirectfb
    DirectFB(組込LinuxやVxWorksで利用されるフレームバッファ向けライブラリ)用QPA

  • qeglfs
    EGL(OpenGLとWindowシステム間用のAPI)用QPA.WindowシステムなしにOpenGLを使いQtアプリケーションを描画するためのQPAとなっている。

  • qhaiku
    Haiku OS(旧OpenBeOS)用QPA

  • qios
    iOS向けQPA

  • qlinuxfb
    Linux向けのfbdevやDRMダムバッファ経由でのフレームバッファを利用するQPA

  • qmirclient
    Linux用ディスプレイサーバ Mir用QPA

  • qopenwf
    OpenWF(HW抽象インターフェース、QNX等で利用される)用QPA

  • qqnx
    QNX用QPA

  • qvnc
    VNC用QPA

  • qwayland
    Wayland用QPA

  • qwindows
    Windows用QPA

  • qwinrt
    WinRTおよびWindows Phone向けQPA

  • qxcb
    XCB(X Window System のC言語バインディング)用QPA

また、テスト・サンプル向けの最小限実装したQPAとして

  • qminimal
  • qminimalegl

があります。qminimalは、ウィンドウシステムを必要としない実装になっており、qmlplugindump等のコマンドラインツールでも利用されています。

その他に、まだ実験段階と思われますが、ソースコード中にはWebAssembly用のQPAが含まれています。

  • qwasm

QPAを実際に実装してみる方法は、 @task_jp さんが過去にブログを書かれていますので、そちらを参考にしてみると良いでしょう。

http://qt5.jp/learn-qpa-01.html

Qt Quick WebGL Streaming Pluginの実装を追う

ソースコード

WebGL Streaming Pluginは、通常のQPAとは異なり以下の場所にソースコードがあります。

qtwebglplugin/src/plugins/platforms/webgl
  • qwebglcontext.cpp
    QWebGLContext : QPlatformOpenGLContextのサブクラス

  • qwebglfunctioncall.cpp
    QWebGLContextで利用するGLFunctionCall周りのサポートクラス(と思われる)
    詳しい人の突っ込み待ちです。

  • qwebglhttpserver.cpp
     簡易Web Server。

  • qwebglintegration.cpp
    QWebGLIntegration : QPlatformIntegration, QPlatformNativeInterfaceのサブクラス

  • qwebglmain.cpp
    QWebGLIntegrationPlugin : QPlatformIntegrationPluginのサブクラス
     最初に呼び出されるクラスで、プラグインの引数チェックを行い、portをパースして、指定されたportでQWebGLIntegrationのインスタンスを生成します。

  • qwebglplatformservices.cpp
    QWebGLPlatformServices : QPlatformServicesのサブクラス

  • qwebglscreen.cpp
    QWebGLScreen : QPlatformScreenのサブクラス

  • qwebglwebsocketserver.cpp
    WebSocketを経由して実際にイベントと描画GLコマンドをやり取りするサーバ

  • qwebglwindow.cpp
    QWebGLWindow : QPlatformWindowのサブクラス

  • index.html
    簡易Webブラウザが返すHTML

  • webqt.jsx
    webqt.jsとして返すJavaScriptファイル

  • favicon.ico
    ブラウザからあったアイコン要求に対して返すアイコン

サーバーの生成

サーバーは、QWebGLIntegration::initialize()で生成され、WebSocketの方はスレッドが起こされています。
QWebSocketServer宛にinvokeMethodで"create"がキューイングされていますね。

void QWebGLIntegration::initialize()
{
    Q_D(QWebGLIntegration);

#if defined(QT_QUICK_LIB)
    qputenv("QSG_RENDER_LOOP", "threaded"); // Force threaded QSG_RENDER_LOOP
#endif

    d->inputContext = QPlatformInputContextFactory::create();
    d->screen = new QWebGLScreen;
    screenAdded(d->screen, true);

    d->webSocketServer = new QWebGLWebSocketServer;
    d->httpServer = new QWebGLHttpServer(d->webSocketServer, this);
    bool ok = d->httpServer->listen(QHostAddress::Any, d->httpPort);
    if (!ok) {
        qFatal("QWebGLIntegration::initialize: Failed to initialize: %s",
               qPrintable(d->httpServer->errorString()));
    }
    d->webSocketServerThread = new QThread(this);
    d->webSocketServerThread->setObjectName("WebSocketServer");
    d->webSocketServer->moveToThread(d->webSocketServerThread);
    connect(d->webSocketServerThread, &QThread::finished,
            d->webSocketServer, &QObject::deleteLater);
    QMetaObject::invokeMethod(d->webSocketServer, "create", Qt::QueuedConnection);
    QMutexLocker lock(d->webSocketServer->mutex());
    d->webSocketServerThread->start();
    d->webSocketServer->waitCondition()->wait(d->webSocketServer->mutex());

    qGuiApp->setQuitOnLastWindowClosed(false);
}

WebServer

Web Serverは、QTcpServerで指定されたportをlistenします。
connectされるとsocketのデータを待ち合わせ、データを受診するとHTTP RequestとしてStateを管理しつつ、順次データを読み出していきます。

void QWebGLHttpServer::readData()
{
    Q_D(QWebGLHttpServer);
    auto socket = qobject_cast<QTcpSocket *>(sender());
    if (!d->clients.contains(socket))
        d->clients[socket].port = d->server.serverPort();

    auto request = &d->clients[socket];
    bool error = false;

    request->byteSize += socket->bytesAvailable();
    if (Q_UNLIKELY(request->byteSize > 2048)) {
        socket->write(QByteArrayLiteral("HTTP 413 – Request entity too large\r\n"));
        socket->disconnectFromHost();
        d->clients.remove(socket);
        return;
    }

    if (Q_LIKELY(request->state == HttpRequest::State::ReadingMethod))
        if (Q_UNLIKELY(error = !request->readMethod(socket)))
            qCWarning(lc, "QWebGLHttpServer::readData: Invalid Method");

    if (Q_LIKELY(!error && request->state == HttpRequest::State::ReadingUrl))
        if (Q_UNLIKELY(error = !request->readUrl(socket)))
            qCWarning(lc, "QWebGLHttpServer::readData: Invalid URL");

    if (Q_LIKELY(!error && request->state == HttpRequest::State::ReadingStatus))
        if (Q_UNLIKELY(error = !request->readStatus(socket)))
            qCWarning(lc, "QWebGLHttpServer::readData: Invalid Status");

    if (Q_LIKELY(!error && request->state == HttpRequest::State::ReadingHeader))
        if (Q_UNLIKELY(error = !request->readHeader(socket)))
            qCWarning(lc, "QWebGLHttpServer::readData: Invalid Header");

    if (error) {
        socket->disconnectFromHost();
        d->clients.remove(socket);
    } else if (!request->url.isEmpty()) {
        Q_ASSERT(request->state != HttpRequest::State::ReadingUrl);
        answerClient(socket, request->url);
        d->clients.remove(socket);
    }
}

読みだしたあとは、answerClientで応答を生成しています。

void QWebGLHttpServer::answerClient(QTcpSocket *socket, const QUrl &url)
{
    Q_D(QWebGLHttpServer);
    bool disconnect = true;
    const auto path = url.path();
     :
    QByteArray answer = QByteArrayLiteral("HTTP/1.1 404 Not Found\r\n"
                                          "Content-Type: text/html\r\n"
                                          "Content-Length: 136\r\n\r\n"
                                          "<html>"
                                          "<head><title>404 Not Found</title></head>"
                                          "<body bgcolor=\"white\">"
                                          "<center><h1>404 Not Found</h1></center>"
                                          "</body>"
                                          "</html>");
    const auto addData = [&answer](const QByteArray &contentType, const QByteArray &data)
    {
        answer = QByteArrayLiteral("HTTP/1.0 200 OK \r\n");
        QByteArray ret;
        const auto dataSize = QString::number(data.size()).toUtf8();
        answer += QByteArrayLiteral("Content-Type: ") + contentType + QByteArrayLiteral("\r\n") +
                  QByteArrayLiteral("Content-Length: ") + dataSize + QByteArrayLiteral("\r\n\r\n") +
                  data;
    };
     :
    if (path == QLatin1String("/")) {
        QFile file(QStringLiteral(":/webgl/index.html"));
        Q_ASSERT(file.exists());
        file.open(QIODevice::ReadOnly | QIODevice::Text);
        Q_ASSERT(file.isOpen());
        auto data = file.readAll();
        addData(QByteArrayLiteral("text/html; charset=\"utf-8\""), data);
    } else if (path == QStringLiteral("/clipboard")) {
#ifndef QT_NO_CLIPBOARD
        auto data = qGuiApp->clipboard()->text().toUtf8();
        addData(QByteArrayLiteral("text/html; charset=\"utf-8\""), data);
#else
        qCWarning(lc, "Qt was built without clipboard support");
#endif
    } else if (path == QStringLiteral("/webqt.js")) {
        QFile file(QStringLiteral(":/webgl/webqt.jsx"));
        Q_ASSERT(file.exists());
        file.open(QIODevice::ReadOnly | QIODevice::Text);
        Q_ASSERT(file.isOpen());
        const auto host = url.host().toUtf8();
        const auto port = QString::number(d->webSocketServer->port()).toUtf8();
        QByteArray data = "var host = \"" + host + "\";\r\nvar port = " + port + ";\r\n";
        data += file.readAll();
        addData(QByteArrayLiteral("application/javascript"), data);
    } else if (path == QStringLiteral("/favicon.ico")) {
        QFile file(QStringLiteral(":/webgl/favicon.ico"));
        Q_ASSERT(file.exists());
        file.open(QIODevice::ReadOnly);
        Q_ASSERT(file.isOpen());
        auto data = file.readAll();
        addData(QByteArrayLiteral("image/x-icon"), data);
    } else if (path == QStringLiteral("/favicon.png")) {
        QBuffer buffer;
        qGuiApp->windowIcon().pixmap(16, 16).save(&buffer, "png");
        addData(QByteArrayLiteral("image/x-icon"), buffer.data());
    } else if (auto device = d->customRequestDevices.value(path)) {
        :
    }
    socket->write(answer);
     :
     :
}

ここから先は、ブラウザ側のwebqt.jsとWebSocketとQWebGLWebSocketServerが主な処理を行っていきます。

最初にWebSocket Server側のcreateがinvokeMethodされていました。ここでは、QWebSocketServerを使ってWebSocketを待ち合わせています。

void QWebGLWebSocketServer::create()
{
    Q_D(QWebGLWebSocketServer);
    const QString serverName = QLatin1String("qtwebgl");
    const QUrl url(QString::fromUtf8(qgetenv("QT_WEBGL_WEBSOCKETSERVER")));
    QHostAddress hostAddress(url.host());
    if (!url.isValid() || url.isEmpty() || !(url.scheme() == "ws" || url.scheme() == "wss")) {
        d->server = new QWebSocketServer(serverName, QWebSocketServer::NonSecureMode);
        hostAddress = QHostAddress::Any;
    } else {
        d->server = new QWebSocketServer(serverName,
#if QT_CONFIG(ssl)
                                         url.scheme() == "wss" ? QWebSocketServer::SecureMode :
#endif
                                                                 QWebSocketServer::NonSecureMode);
    }
    if (d->server->listen(hostAddress, url.port(0))) {
        connect(d->server, &QWebSocketServer::newConnection,
                this, &QWebGLWebSocketServer::onNewConnection);
    } else {
        qCCritical(lc, "The WebSocket Server cannot start: %s",
                   qPrintable(d->server->errorString()));
    }

    QMutexLocker lock(&QWebGLIntegrationPrivate::instance()->waitMutex);
    QWebGLIntegrationPrivate::instance()->waitCondition.wakeAll();
}

ブラウザ側は、WebSocketを接続する処理が実行されます。また、setupInputが実行されて、ブラウザ上のイベントをWebSocketで送信するためのハンドラが登録されます。

window.onload = function () {
      :
    var socket = new WebSocket("ws://" + host + ":" + port);
      :
    socket.onopen = function (event) {
        console.log("Socket Open");
        (function(){
            var doCheck = true;
            var check = function(){
                var size = getBrowserSize();
                var width = size.width;
                var height = size.height;
                var physicalSize = physicalSizeRatio();
                if (DEBUG)
                    console.log("Resizing canvas to " + width + " x " + height);
                sendObject({ "type": "canvas_resize",
                    "width": width, "height": height,
                    "physicalWidth": width / physicalSize.width,
                    "physicalHeight": height / physicalSize.height
                });
            };
            window.addEventListener("resize",(function(){
                if(doCheck){
                    check();
                    doCheck = false;
                    setTimeout((function(){
                        doCheck = true;
                        check();
                    }), 1000);
                }
            }));
        })();
        connect();
    };
      :
    var connect = function () {
        var size = getBrowserSize();
        var width = size.width;
        var height = size.height;
        var physicalSize = physicalSizeRatio();

        var object = { "type": "connect",
            "width": width, "height": height,
            "physicalWidth": width / physicalSize.width,
            "physicalHeight": height / physicalSize.height
        };
        sendObject(object);
        initialLoadingCanvas = createLoadingCanvas('loadingCanvas', 0, 0, width, height);
    };
     :
     :
    var setupInput = function () {
        var keyHandler = function (event) {
            var object = { "type": event.type,
                "char": event.char,
                "key": event.key,
                "which": event.which,
                "location": event.location,
                "repeat": event.repeat,
                "locale": event.locale,
                "ctrlKey": event.ctrlKey, "shiftKey": event.shiftKey, "altKey": event.altKey,
                "metaKey": event.metaKey,
                "string": String.fromCharCode(event.which ||
                                               event.keyCode),
                "keyCode": event.keyCode, "charCode": event.charCode, "code": event.code,
                "time": new Date().getTime(),
            };
            sendObject(object);
        }

        document.addEventListener('keypress', keyHandler, true);
        document.addEventListener('keydown', keyHandler, true);
        document.addEventListener('keyup', keyHandler, true);
    };
    setupInput();
};

長いので省略しますが、createLoadingCanvasでcanvasの設定をしているコードがあります。これは最初にbrowserをつなげた時に出るインジゲーターの描画となっています。

サーバ側では、socketのconnectが来るとメッセージ処理用スロットを登録後、connectのネゴシエーションを送信します。

void QWebGLWebSocketServer::onNewConnection()
{
    Q_D(QWebGLWebSocketServer);
    QWebSocket *socket = d->server->nextPendingConnection();
    if (socket) {
        connect(socket, &QWebSocket::disconnected, this, &QWebGLWebSocketServer::onDisconnect);
        connect(socket, &QWebSocket::textMessageReceived, this,
                &QWebGLWebSocketServer::onTextMessageReceived);

        const QVariantMap values{
            {
                QStringLiteral("debug"),
#ifdef QT_DEBUG
                true
#else
                false
#endif
            },
            { QStringLiteral("loadingScreen"), qgetenv("QT_WEBGL_LOADINGSCREEN") },
            { QStringLiteral("mouseTracking"), qgetenv("QT_WEBGL_MOUSETRACKING") },
            { QStringLiteral("supportedFunctions"),
              QVariant::fromValue(QWebGLContext::supportedFunctions()) },
            { "sysinfo",
                QVariantMap {
                    { QStringLiteral("buildAbi"), QSysInfo::buildAbi() },
                    { QStringLiteral("buildCpuArchitecture"), QSysInfo::buildCpuArchitecture() },
                    { QStringLiteral("currentCpuArchitecture"),
                      QSysInfo::currentCpuArchitecture() },
                    { QStringLiteral("kernelType"), QSysInfo::kernelType() },
                    { QStringLiteral("machineHostName"), QSysInfo::machineHostName() },
                    { QStringLiteral("prettyProductName"), QSysInfo::prettyProductName() },
                    { QStringLiteral("productType"), QSysInfo::productType() },
                    { QStringLiteral("productVersion"), QSysInfo::productVersion() },
                }
            }
        };

        sendMessage(socket, MessageType::Connect, values);
    }
}

さらに、connect処理が送られてきているためこのパースも実施されます。

void QWebGLIntegrationPrivate::onTextMessageReceived(QWebSocket *socket, const QString &message)
{
    QJsonParseError parseError;
    const auto document = QJsonDocument::fromJson(message.toUtf8(), &parseError);
    Q_ASSERT(parseError.error == QJsonParseError::NoError);
    Q_ASSERT(document.isObject());
    const auto object = document.object();
    Q_ASSERT(object.contains("type"));
    const auto type = object[QStringLiteral("type")].toString();

    auto integrationPrivate = QWebGLIntegrationPrivate::instance();
    const auto clientData = integrationPrivate->findClientData(socket);

    if (type == QStringLiteral("connect"))
        clientConnected(socket, object["width"].toInt(), object["height"].toInt(),
                          object["physicalWidth"].toDouble(), object["physicalHeight"].toDouble());
    else if (!clientData || clientData->platformWindows.isEmpty())
        qCWarning(lcWebGL, "Message received before connect %s", qPrintable(message));
    else if (type == QStringLiteral("default_context_parameters"))
        handleDefaultContextParameters(*clientData, object);
    else if (type == QStringLiteral("gl_response"))
        handleGlResponse(object);
    else if (type == QStringLiteral("mouse"))
        handleMouse(*clientData, object);
    else if (type == QStringLiteral("wheel"))
        handleWheel(*clientData, object);
    else if (type == QStringLiteral("touch"))
        handleTouch(*clientData, object);
    else if (type.startsWith("key"))
        handleKeyboard(*clientData, type, object);
    else if (type == QStringLiteral("canvas_resize"))
        handleCanvasResize(*clientData, object);
}

JavaScript側でも、connectのパケットを受信し処理されます。

   socket.onmessage = function (event) {
        if (event.data instanceof ArrayBuffer) {
            handleBinaryMessage(event);
            return;
        }
        var obj;
        try {
            obj = JSON.parse(event.data);
        } catch (e) {
            console.error("Failed to parse " + event.data + ": " + e.toString());
            return;
        }
        if (!("type" in obj)) {
            console.error("Message type not found");
        } else if (obj.type === "create_canvas") {
            createCanvas(obj.winId, obj.x, obj.y, obj.width, obj.height, obj.title);
            if (obj.title && obj.title.length)
                document.title = obj.title;
        } else if (obj.type === "destroy_canvas") {
            var canvas = document.getElementById(obj.winId);
            var body = document.getElementsByTagName("body")[0];
            body.removeChild(canvas);
        } else if (obj.type === "clipboard_updated") {
            // Opens a new window/tab and shows the current remote clipboard. There is no way to
            // copy some text to the local clipboard without user interaction.
            window.open("/clipboard", "clipboard");
        } else if (obj.type === "open_url") {
            window.open(obj.url);
        } else if (obj.type === "change_title") {
            document.title = obj.text;
        } else if (obj.type === "connect") {
            supportedFunctions = obj.supportedFunctions;
            var sysinfo = obj.sysinfo;
            if (obj.debug)
                DEBUG = 1;
            if (obj.mouseTracking)
                MOUSETRACKING = 1;
            if (obj.loadingScreen === "0")
                LOADINGSCREEN = 0;
            console.log(sysinfo);
        } else {
            console.error("Unknown message type");
        }
    };

ちなみに、handleBinaryMessageが、シリアライズされたGLコマンドをパースし、WebGLを使ってブラウザ上に画面を描画する処理となります。

アプリケーションには最初にbrowserのサイズがconnectで通知され描画が行われますが、この描画処理が画面への描画ではなく、GLコマンドをシリアライズしたバイナリデータとなり、WebSocketで送信されることになります。

これで大枠が整いました。あとは、ブラウザ上の操作イベントがWebSocketを経由してアプリケーションに通知されます。例えばCanvas上でマウスのクリックが発生したとします。これは、type: mouseのメッセージとしてサーバ側に送信されます。

    var sendObject = function (obj) { socket.send(JSON.stringify(obj)); };
       :
       :
    var createLoadingCanvas = function(name, x, y, width, height) {
         :
         :
        var sendMouseEvent = function (buttons, layerX, layerY, clientX, clientY, name) {
            var object = { "type": "mouse",
                "buttons": buttons,
                "layerX": layerX, "layerY": layerY, "clientX": clientX, "clientY": clientY,
                "time": new Date().getTime(),
                "name": name
            };
            sendObject(object);
        };

        canvas.onmousedown = function (event) {
            /* jslint bitwise: true */
            qtButtons |= mapButton(event.button);
            sendMouseEvent(qtButtons, event.layerX, event.layerY, event.clientX, event.clientY,
                           name);
        };

この呼び出しは、先に引用した"QWebGLIntegrationPrivate::onTextMessageReceived"で処理され、handleMouseが呼び出されます。

これは、アプリケーション側にマウスイベントとして通知されます。

void QWebGLIntegrationPrivate::handleMouse(const ClientData &clientData, const QJsonObject &object)
{
    const auto winId = object.value("name").toInt(-1);
    Q_ASSERT(winId != -1);
    QPointF localPos(object.value("layerX").toDouble(),
                     object.value("layerY").toDouble());
    QPointF globalPos(object.value("clientX").toDouble(),
                      object.value("clientY").toDouble());
    auto buttons = static_cast<Qt::MouseButtons>(object.value("buttons").toInt());
    auto time = object.value("time").toDouble();
    auto platformWindow = findWindow(clientData, winId);
    QWindowSystemInterface::handleMouseEvent(platformWindow->window(),
                                             static_cast<ulong>(time),
                                             localPos,
                                             globalPos,
                                             Qt::MouseButtons(buttons),
                                             Qt::NoButton,
                                             QEvent::None,
                                             Qt::NoModifier,
                                             Qt::MouseEventNotSynthesized);
}

その結果発生した描画イベントは、GLコマンドがシリアライズされたバイナリデータとして、ブラウザ側に送信されブラウザ上で描画されるのです。

まとめ

今回は、Qt 5.12から正式モジュールになる予定のQt Quick WebGL Streaming Pluginの紹介と、何かトラブルがあったときに向けて、解析の最初の一歩として、かなりザックリとどんな実装になっているのか、駆け足で眺めてみました。

QtはQPAという形でプラットフォーム依存部が切り出されており、このプラグインをうまく利用すると、こんな不思議なアプリケーションが作成できるようになるわけです。非常に単純なQt Quickアプリケーションであればそれらしく動かせます。面白ネタで終わるか、実用に耐えるところまで行くかはこれから次第ですが、新規モジュールはQtに貢献できるチャンスも多いかと思います。興味がある方はぜひチャレンジしてみて下さい。

さらに、現在はWebAssembry対応も進められています。WebGLはアプリケーションの1プロセスが1ブラウザと通信することを前提に作られた仕組みです。これに対し、WebAssembryは多数のユーザーがアプリケーションのバイトコードをブラウザで入手し、インストールすることなく、ブラウザ上で実行する仕組みとなっています。このあたりも今後どうなるのか楽しみなところです。

さて、明日は、 @tetsurom さんの 「Qt開発環境のコンテナ化について」の予定です。冬とは思えない程、穏やかな天気が続いていますし、秋の延長戦のつもりで、夜の長い趣味の時間楽しんでQtに触れてみましょう。

hermit4
文系出身のなんちゃってプログラマです。フリーランスで細々と生きてます。 主にC++でお仕事して生きてますが、Lispとwhitespace見たいな目で追うのがつらい言語以外はたいてい好きです。
http://hermit4.info
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away