354 lines · 16.3 KB · C++
RawDownload
////  ScreenshareScriptingInterface.cpp//  interface/src/scripting/////  Created by Milad Nazeri and Zach Fox on 2019-10-23.//  Copyright 2019 High Fidelity, Inc.////  Distributed under the Apache License, Version 2.0.//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html// #include <QJsonDocument>#include <QJsonObject>#include <QNetworkAccessManager>#include <QThread>#include <QUrl> #include <AccountManager.h>#include <AddressManager.h>#include <DependencyManager.h>#include <NetworkingConstants.h>#include <NodeList.h>#include <UUID.h> #include "EntityScriptingInterface.h"#include "ScreenshareScriptingInterface.h"#include "ExternalResource.h" static const int SCREENSHARE_INFO_REQUEST_RETRY_TIMEOUT_MS = 300;ScreenshareScriptingInterface::ScreenshareScriptingInterface() {    auto esi = DependencyManager::get<EntityScriptingInterface>();    if (!esi) {        return;    }     // This signal/slot connection is used when the screen share local web entity sends an event bridge message.     QObject::connect(esi.data(), &EntityScriptingInterface::webEventReceived, this, &ScreenshareScriptingInterface::onWebEventReceived);     _requestScreenshareInfoRetryTimer = new QTimer;    _requestScreenshareInfoRetryTimer->setSingleShot(true);    _requestScreenshareInfoRetryTimer->setInterval(SCREENSHARE_INFO_REQUEST_RETRY_TIMEOUT_MS);    connect(_requestScreenshareInfoRetryTimer, &QTimer::timeout, this, &ScreenshareScriptingInterface::requestScreenshareInfo);     // This packet listener handles the packet containing information about the latest zone ID in which we are allowed to share.    auto nodeList = DependencyManager::get<NodeList>();    PacketReceiver& packetReceiver = nodeList->getPacketReceiver();    packetReceiver.registerListener(PacketType::AvatarZonePresence,        PacketReceiver::makeUnsourcedListenerReference<ScreenshareScriptingInterface>(this, &ScreenshareScriptingInterface::processAvatarZonePresencePacketOnClient));}; ScreenshareScriptingInterface::~ScreenshareScriptingInterface() {    stopScreenshare();} void ScreenshareScriptingInterface::processAvatarZonePresencePacketOnClient(QSharedPointer<ReceivedMessage> message) {    QUuid zone = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID));     if (zone.isNull()) {        qWarning() << "Ignoring avatar zone presence packet that doesn't specify a zone.";        return;    }     // Set the last known authorized screenshare zone ID to the zone that the Domain Server just told us about.    _lastAuthorizedZoneID = zone;     // If we had previously started the screenshare process but knew that we weren't going to be authorized to screenshare,    // let's continue the screenshare process here.    if (_waitingForAuthorization) {        requestScreenshareInfo();    }} static const int MAX_NUM_SCREENSHARE_INFO_REQUEST_RETRIES = 5;void ScreenshareScriptingInterface::requestScreenshareInfo() {    // If the screenshare zone that we're currently in (i.e. `startScreenshare()` was called) is different from    // the zone in which we are authorized to screenshare...    // ...return early here and wait for the DS to send us a packet containing this zone's ID.    if (_screenshareZoneID != _lastAuthorizedZoneID) {        qDebug() << "Client not yet authorized to screenshare. Waiting for authorization message from domain server...";        _waitingForAuthorization = true;        return;    }     _waitingForAuthorization = false;     _requestScreenshareInfoRetries++;     if (_requestScreenshareInfoRetries >= MAX_NUM_SCREENSHARE_INFO_REQUEST_RETRIES) {        qDebug() << "Maximum number of retries for screenshare info exceeded. Screenshare will not function.";        return;    }     // Don't continue with any more of this logic if we can't get the `AccountManager` or `AddressManager`.    auto accountManager = DependencyManager::get<AccountManager>();    if (!accountManager) {        return;    }    auto addressManager = DependencyManager::get<AddressManager>();    if (!addressManager) {        return;    }     // Construct and send a request to the Metaverse to obtain the information    // necessary to start the screen sharing process.    // This request requires:    //     1. The domain ID of the domain in which the user's avatar is present    //     2. User authentication information that is automatically included when `sendRequest()` is passed    //         with the `AccountManagerAuth::Required` argument.    // Note that this request will only return successfully if the Domain Server has already registered    // the user paired with the current domain with the Metaverse.    // See `DomainServer::screensharePresence()` for more info about that.     QString currentDomainID = uuidStringWithoutCurlyBraces(addressManager->getDomainID());    QString requestURLPath = "/api/v1/domains/%1/screenshare";    JSONCallbackParameters callbackParams;    callbackParams.callbackReceiver = this;    callbackParams.jsonCallbackMethod = "handleSuccessfulScreenshareInfoGet";    callbackParams.errorCallbackMethod = "handleFailedScreenshareInfoGet";    accountManager->sendRequest(        requestURLPath.arg(currentDomainID),        AccountManagerAuth::Required,        QNetworkAccessManager::GetOperation,        callbackParams    );} static const EntityTypes::EntityType LOCAL_SCREENSHARE_WEB_ENTITY_TYPE = EntityTypes::Web;static const uint8_t LOCAL_SCREENSHARE_WEB_ENTITY_FPS = 30;// This is going to be a good amount of work to make this work dynamically for any screensize.// V1 will have only hardcoded values.// The `z` value here is dynamic.static const glm::vec3 LOCAL_SCREENSHARE_WEB_ENTITY_LOCAL_POSITION(0.0128f, -0.0918f, 0.0f);static const glm::vec3 LOCAL_SCREENSHARE_WEB_ENTITY_DIMENSIONS(3.6790f, 2.0990f, 0.0100f);static const ExternalResource::Bucket LOCAL_SCREENSHARE_WEB_ENTITY_BUCKET = ExternalResource::Bucket::HF_Content;static const QString LOCAL_SCREENSHARE_WEB_ENTITY_PATH =     "Experiences/Releases/usefulUtilities/smartBoard/screenshareViewer/screenshareClient.html";static const QString LOCAL_SCREENSHARE_WEB_ENTITY_HOST_TYPE = "local";void ScreenshareScriptingInterface::startScreenshare(const QUuid& screenshareZoneID,                                                     const QUuid& smartboardEntityID,                                                     const bool& isPresenter) {    // We must start a new QProcess from the main thread.    if (QThread::currentThread() != thread()) {        QMetaObject::invokeMethod(this, "startScreenshare", Q_ARG(const QUuid&, screenshareZoneID),                                  Q_ARG(const QUuid&, smartboardEntityID), Q_ARG(const bool&, isPresenter));        return;    }     // These three private member variables are set now so that they may be used later during asynchronous    // callbacks.    _screenshareZoneID = screenshareZoneID;    _smartboardEntityID = smartboardEntityID;    _isPresenter = isPresenter;     // If we are presenting, and the screenshare process is already running, don't do anything else here.    if (_isPresenter && _screenshareProcess && _screenshareProcess->state() != QProcess::NotRunning) {        return;    }     // If we're presenting...    if (_isPresenter) {        // ...make sure we first reset this `std::unique_ptr`.        _screenshareProcess.reset(new QProcess(this));         // Ensure that the screenshare executable exists where we expect it to.        // Error out and reset the screen share state machine if the executable doesn't exist.        QFileInfo screenshareExecutable(SCREENSHARE_EXE_PATH);        if (!screenshareExecutable.exists() || !(screenshareExecutable.isFile() || screenshareExecutable.isBundle())) {            qDebug() << "Screenshare executable doesn't exist at" << SCREENSHARE_EXE_PATH;            stopScreenshare();            emit screenshareError();            return;        }    }     if (_requestScreenshareInfoRetryTimer && _requestScreenshareInfoRetryTimer->isActive()) {        _requestScreenshareInfoRetryTimer->stop();    }     _requestScreenshareInfoRetries = 0;    requestScreenshareInfo();} void ScreenshareScriptingInterface::stopScreenshare() {    // We can only deal with our Screen Share `QProcess` on the main thread.    if (QThread::currentThread() != thread()) {        QMetaObject::invokeMethod(this, "stopScreenshare");        return;    }     // If the retry timer is active, stop it.    if (_requestScreenshareInfoRetryTimer && _requestScreenshareInfoRetryTimer->isActive()) {        _requestScreenshareInfoRetryTimer->stop();    }     // If the Screen Share process is running...    if (_screenshareProcess && _screenshareProcess->state() != QProcess::NotRunning) {        //...terminate it and make sure that scripts know we terminated it by emitting        // `screenshareProcessTerminated()`.        _screenshareProcess->terminate();    }     // Delete the local web entity if we know about it here.    if (!_screenshareViewerLocalWebEntityUUID.isNull()) {        auto esi = DependencyManager::get<EntityScriptingInterface>();        if (esi) {            esi->deleteEntity(_screenshareViewerLocalWebEntityUUID);        }    }     // Reset all private member variables related to screen share here.    _screenshareViewerLocalWebEntityUUID = "{00000000-0000-0000-0000-000000000000}";    _token = "";    _projectAPIKey = "";    _sessionID = "";    _isPresenter = false;    _waitingForAuthorization = false;} // Called when the Metaverse returns the information necessary to start/view a screen share.void ScreenshareScriptingInterface::handleSuccessfulScreenshareInfoGet(QNetworkReply* reply) {    // Read the reply and get it into a format we understand.    QString answer = reply->readAll();    QByteArray answerByteArray = answer.toUtf8();    QJsonDocument answerJSONObject = QJsonDocument::fromJson(answerByteArray);     // This Metaverse endpoint will always return a status key/value pair of "success" if things went well.    QString status = answerJSONObject["status"].toString();    if (status != "success") {        qDebug() << "Error when retrieving screenshare info via HTTP. Error:" << reply->errorString();        stopScreenshare();        emit screenshareError();        return;    }     // Store the information necessary to start/view a screen share in these private member variables.    _token = answerJSONObject["token"].toString();    _projectAPIKey = answerJSONObject["projectApiKey"].toString();    _sessionID = answerJSONObject["sessionID"].toString();     // Make sure we have all of the info that we need.    if (_token.isEmpty() || _projectAPIKey.isEmpty() || _sessionID.isEmpty()) {        qDebug() << "Not all Screen Share information was retrieved from the backend. Stopping...";        stopScreenshare();        emit screenshareError();        return;    }     // If we're presenting:    // 1. Build a list of arguments that we're going to pass to the screen share Electron app.    // 2. Make sure we connect a signal/slot to know when the user quits the Electron app.    // 3. Start the screen share Electron app with the list of args from (1).    if (_isPresenter) {        QStringList arguments;        arguments << " ";        arguments << "--token=" + _token << " ";        arguments << "--projectAPIKey=" + _projectAPIKey << " ";        arguments << "--sessionID=" + _sessionID << " ";         connect(_screenshareProcess.get(), QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),            [=](int exitCode, QProcess::ExitStatus exitStatus) {                stopScreenshare();                emit screenshareProcessTerminated();            });         _screenshareProcess->start(SCREENSHARE_EXE_PATH, arguments);    }     // Make sure we can grab the entity scripting interface. Error out if we can't.    auto esi = DependencyManager::get<EntityScriptingInterface>();    if (!esi) {        stopScreenshare();        emit screenshareError();        return;    }     // If, for some reason, we already have a record of a screen share local Web entity, delete it.    if (!_screenshareViewerLocalWebEntityUUID.isNull()) {        esi->deleteEntity(_screenshareViewerLocalWebEntityUUID);    }     // Set up the entity properties associated with the screen share local Web entity.    EntityItemProperties localScreenshareWebEntityProps;    localScreenshareWebEntityProps.setType(LOCAL_SCREENSHARE_WEB_ENTITY_TYPE);    localScreenshareWebEntityProps.setMaxFPS(LOCAL_SCREENSHARE_WEB_ENTITY_FPS);    glm::vec3 localPosition(LOCAL_SCREENSHARE_WEB_ENTITY_LOCAL_POSITION);    localPosition.z = _localWebEntityZOffset;    localScreenshareWebEntityProps.setLocalPosition(localPosition);    auto LOCAL_SCREENSHARE_WEB_ENTITY_URL = ExternalResource::getInstance()->getUrl(LOCAL_SCREENSHARE_WEB_ENTITY_BUCKET,        LOCAL_SCREENSHARE_WEB_ENTITY_PATH);    localScreenshareWebEntityProps.setSourceUrl(LOCAL_SCREENSHARE_WEB_ENTITY_URL);    localScreenshareWebEntityProps.setParentID(_smartboardEntityID);    localScreenshareWebEntityProps.setDimensions(LOCAL_SCREENSHARE_WEB_ENTITY_DIMENSIONS);     // The lines below will be used when writing the feature to support scaling the Smartboard entity to any arbitrary size.    //EntityPropertyFlags desiredSmartboardProperties;    //desiredSmartboardProperties += PROP_POSITION;    //desiredSmartboardProperties += PROP_DIMENSIONS;    //EntityItemProperties smartboardProps = esi->getEntityProperties(_smartboardEntityID, desiredSmartboardProperties);        // Add the screen share local Web entity to Interface's entity tree.    // When the Web entity loads the page specified by `LOCAL_SCREENSHARE_WEB_ENTITY_URL`, it will broadcast an Event Bridge    // message, which we will consume inside `ScreenshareScriptingInterface::onWebEventReceived()`.    _screenshareViewerLocalWebEntityUUID = esi->addEntity(localScreenshareWebEntityProps, LOCAL_SCREENSHARE_WEB_ENTITY_HOST_TYPE);} void ScreenshareScriptingInterface::handleFailedScreenshareInfoGet(QNetworkReply* reply) {    if (_requestScreenshareInfoRetries >= MAX_NUM_SCREENSHARE_INFO_REQUEST_RETRIES) {        qDebug() << "Failed to get screenshare info via HTTP after" << MAX_NUM_SCREENSHARE_INFO_REQUEST_RETRIES << "retries. Error:" << reply->errorString();        stopScreenshare();        emit screenshareError();        return;    }     _requestScreenshareInfoRetryTimer->start();} // This function will handle _all_ web events received via `EntityScriptingInterface::webEventReceived()`, including// those not related to screen sharing.void ScreenshareScriptingInterface::onWebEventReceived(const QUuid& entityID, const QVariant& message) {    // Bail early if the entity that sent the Web event isn't the one we care about.    if (entityID == _screenshareViewerLocalWebEntityUUID) {        // Bail early if we can't grab the Entity Scripting Interface.        auto esi = DependencyManager::get<EntityScriptingInterface>();        if (!esi) {            return;        }         // Web events received from the screen share Web JS will always be in stringified JSON format.        QByteArray jsonByteArray = QVariant(message).toString().toUtf8();        QJsonDocument jsonObject = QJsonDocument::fromJson(jsonByteArray);         // It should never happen where the screen share Web JS sends a message without the `app` key's value        // set to "screenshare".        if (jsonObject["app"] != "screenshare") {            return;        }         // The screen share Web JS only sends a message with one method: "eventBridgeReady". Handle it here.        if (jsonObject["method"] == "eventBridgeReady") {            // Stuff a JSON object full of information necessary for the screen share local Web entity            // to connect to the screen share session associated with the room in which the user's avatar is standing.            QJsonObject responseObject;            responseObject.insert("app", "screenshare");            responseObject.insert("method", "receiveConnectionInfo");            QJsonObject responseObjectData;            responseObjectData.insert("token", _token);            responseObjectData.insert("projectAPIKey", _projectAPIKey);            responseObjectData.insert("sessionID", _sessionID);            responseObject.insert("data", responseObjectData);             esi->emitScriptEvent(_screenshareViewerLocalWebEntityUUID, responseObject.toVariantMap());        }    }}