Merge pull request #16429 from MiladNazeri/screenshareElectronApp

Screenshare in Interface!
This commit is contained in:
Zach Fox 2019-11-15 15:32:51 -08:00 committed by GitHub
commit b64ce343bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 3950 additions and 9 deletions

View file

@ -256,6 +256,7 @@ endif()
if (BUILD_CLIENT)
add_subdirectory(interface)
add_subdirectory(screenshare)
set_target_properties(interface PROPERTIES FOLDER "Apps")
option(USE_SIXENSE "Build Interface with sixense library/plugin" OFF)

View file

@ -1081,6 +1081,12 @@ void AvatarMixer::setupEntityQuery() {
priorityZoneQuery["avatarPriority"] = true;
priorityZoneQuery["type"] = "Zone";
QJsonObject queryFlags;
queryFlags["includeAncestors"] = true;
queryFlags["includeDescendants"] = true;
priorityZoneQuery["flags"] = queryFlags;
priorityZoneQuery["name"] = true; // Handy for debugging.
_entityViewer.getOctreeQuery().setJSONParameters(priorityZoneQuery);
_slaveSharedData.entityTree = entityTree;
}

View file

@ -99,6 +99,7 @@ namespace {
glm::vec3 position;
bool isInPriorityZone { false };
float zoneVolume { std::numeric_limits<float>::max() };
EntityItemID id {};
static bool operation(const OctreeElementPointer& element, void* extraData) {
auto findPriorityZone = static_cast<FindPriorityZone*>(extraData);
@ -113,6 +114,7 @@ namespace {
if (volume < findPriorityZone->zoneVolume) { // Smaller volume wins
findPriorityZone->isInPriorityZone = zoneItem->getAvatarPriority() == COMPONENT_MODE_ENABLED;
findPriorityZone->zoneVolume = volume;
findPriorityZone->id = zoneItem->getEntityItemID();
}
}
}
@ -152,7 +154,15 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message, const SlaveShared
EntityTree& entityTree = *slaveSharedData.entityTree;
FindPriorityZone findPriorityZone { newPosition } ;
entityTree.recurseTreeWithOperation(&FindPriorityZone::operation, &findPriorityZone);
_avatar->setHasPriority(findPriorityZone.isInPriorityZone);
bool currentlyHasPriority = findPriorityZone.isInPriorityZone;
if (currentlyHasPriority != _avatar->getHasPriority()) {
_avatar->setHasPriority(currentlyHasPriority);
auto nodeList = DependencyManager::get<NodeList>();
auto packet = NLPacket::create(PacketType::AvatarZonePresence, 2 * NUM_BYTES_RFC4122_UUID, true);
packet->write(_avatar->getSessionUUID().toRfc4122());
packet->write(findPriorityZone.id.toRfc4122());
nodeList->sendPacket(std::move(packet), nodeList->getDomainSockAddr());
}
_avatar->setNeedsHeroCheck(false);
}

View file

@ -146,23 +146,27 @@ macro(SET_PACKAGING_PARAMETERS)
set(DMG_SUBFOLDER_ICON "${HF_CMAKE_DIR}/installer/install-folder.rsrc")
set(CONSOLE_INSTALL_DIR ${DMG_SUBFOLDER_NAME})
set(INTERFACE_INSTALL_DIR ${DMG_SUBFOLDER_NAME})
set(NITPICK_INSTALL_DIR ${DMG_SUBFOLDER_NAME})
set(CONSOLE_INSTALL_DIR ${DMG_SUBFOLDER_NAME})
set(INTERFACE_INSTALL_DIR ${DMG_SUBFOLDER_NAME})
set(SCREENSHARE_INSTALL_DIR ${DMG_SUBFOLDER_NAME})
set(NITPICK_INSTALL_DIR ${DMG_SUBFOLDER_NAME})
if (CLIENT_ONLY)
set(CONSOLE_EXEC_NAME "Console.app")
else ()
set(CONSOLE_EXEC_NAME "Sandbox.app")
endif()
set(CONSOLE_INSTALL_APP_PATH "${CONSOLE_INSTALL_DIR}/${CONSOLE_EXEC_NAME}")
set(SCREENSHARE_EXEC_NAME "hifi-screenshare.app")
set(SCREENSHARE_INSTALL_APP_PATH "${SCREENSHARE_INSTALL_DIR}/${SCREENSHARE_EXEC_NAME}")
set(CONSOLE_APP_CONTENTS "${CONSOLE_INSTALL_APP_PATH}/Contents")
set(COMPONENT_APP_PATH "${CONSOLE_APP_CONTENTS}/MacOS/Components.app")
set(COMPONENT_INSTALL_DIR "${COMPONENT_APP_PATH}/Contents/MacOS")
set(CONSOLE_PLUGIN_INSTALL_DIR "${COMPONENT_APP_PATH}/Contents/PlugIns")
set(SCREENSHARE_APP_CONTENTS "${SCREENSHARE_INSTALL_APP_PATH}/Contents")
set(INTERFACE_INSTALL_APP_PATH "${CONSOLE_INSTALL_DIR}/${INTERFACE_BUNDLE_NAME}.app")
set(INTERFACE_ICON_FILENAME "${INTERFACE_ICON_PREFIX}.icns")
@ -170,9 +174,11 @@ macro(SET_PACKAGING_PARAMETERS)
else ()
if (WIN32)
set(CONSOLE_INSTALL_DIR "server-console")
set(SCREENSHARE_INSTALL_DIR "hifi-screenshare")
set(NITPICK_INSTALL_DIR "nitpick")
else ()
set(CONSOLE_INSTALL_DIR ".")
set(SCREENSHARE_INSTALL_DIR ".")
set(NITPICK_INSTALL_DIR ".")
endif ()
@ -186,6 +192,7 @@ macro(SET_PACKAGING_PARAMETERS)
set(NITPICK_ICON_FILENAME "${NITPICK_ICON_PREFIX}.ico")
set(CONSOLE_EXEC_NAME "server-console.exe")
set(SCREENSHARE_EXEC_NAME "hifi-screenshare.exe")
set(DS_EXEC_NAME "domain-server.exe")
set(AC_EXEC_NAME "assignment-client.exe")

View file

@ -766,6 +766,7 @@ void DomainServer::setupNodeListAndAssignments() {
packetReceiver.registerListener(PacketType::DomainServerPathQuery, this, "processPathQueryPacket");
packetReceiver.registerListener(PacketType::NodeJsonStats, this, "processNodeJSONStatsPacket");
packetReceiver.registerListener(PacketType::DomainDisconnectRequest, this, "processNodeDisconnectRequestPacket");
packetReceiver.registerListener(PacketType::AvatarZonePresence, this, "processAvatarZonePresencePacket");
// NodeList won't be available to the settings manager when it is created, so call registerListener here
packetReceiver.registerListener(PacketType::DomainSettingsRequest, &_settingsManager, "processSettingsRequestPacket");
@ -3613,3 +3614,62 @@ void DomainServer::handleOctreeFileReplacementRequest(QSharedPointer<ReceivedMes
handleOctreeFileReplacement(message->readAll(), QString(), QString(), username);
}
}
void DomainServer::processAvatarZonePresencePacket(QSharedPointer<ReceivedMessage> message) {
QUuid avatar = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
QUuid zone = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
if (avatar.isNull()) {
qCWarning(domain_server) << "Ignoring null avatar presence";
return;
}
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
auto matchingNode = limitedNodeList->nodeWithUUID(avatar);
if (!matchingNode) {
qCWarning(domain_server) << "Ignoring avatar presence for unknown avatar" << avatar;
return;
}
QString verifiedUsername = matchingNode->getPermissions().getVerifiedUserName();
static const int SCREENSHARE_EXPIRATION_SECONDS = 24 * 60 * 60;
screensharePresence(zone.isNull() ? "" : zone.toString(), verifiedUsername, SCREENSHARE_EXPIRATION_SECONDS);
}
void DomainServer::screensharePresence(QString roomname, QString username, int expirationSeconds) {
if (!DependencyManager::get<AccountManager>()->hasValidAccessToken()) {
static std::once_flag presenceAuthorityWarning;
std::call_once(presenceAuthorityWarning, [] {
qCDebug(domain_server) << "No authority to send screensharePresence.";
});
return;
}
JSONCallbackParameters callbackParams;
callbackParams.callbackReceiver = this;
callbackParams.jsonCallbackMethod = "handleSuccessfulScreensharePresence";
callbackParams.errorCallbackMethod = "handleFailedScreensharePresence";
const QString PATH = "api/v1/domains/%1/screenshare";
QString domain_id = uuidStringWithoutCurlyBraces(getID());
QJsonObject json, screenshare;
screenshare["username"] = username;
screenshare["roomname"] = roomname;
if (expirationSeconds > 0) {
screenshare["expiration"] = expirationSeconds;
}
json["screenshare"] = screenshare;
DependencyManager::get<AccountManager>()->sendRequest(
PATH.arg(domain_id),
AccountManagerAuth::Required,
QNetworkAccessManager::PostOperation,
callbackParams, QJsonDocument(json).toJson()
);
}
void DomainServer::handleSuccessfulScreensharePresence(QNetworkReply* requestReply) {
QJsonObject jsonObject = QJsonDocument::fromJson(requestReply->readAll()).object();
if (jsonObject["status"].toString() != "success") {
qCWarning(domain_server) << "screensharePresence api call failed:" << QJsonDocument(jsonObject).toJson(QJsonDocument::Compact);
}
}
void DomainServer::handleFailedScreensharePresence(QNetworkReply* requestReply) {
qCWarning(domain_server) << "screensharePresence api call failed:" << requestReply->error();
}

View file

@ -78,6 +78,8 @@ public:
bool isAssetServerEnabled();
void screensharePresence(QString roomname, QString username, int expiration_seconds = 0);
public slots:
/// Called by NodeList to inform us a node has been added
void nodeAdded(SharedNodePointer node);
@ -96,6 +98,7 @@ private slots:
void processNodeDisconnectRequestPacket(QSharedPointer<ReceivedMessage> message);
void processICEServerHeartbeatDenialPacket(QSharedPointer<ReceivedMessage> message);
void processICEServerHeartbeatACK(QSharedPointer<ReceivedMessage> message);
void processAvatarZonePresencePacket(QSharedPointer<ReceivedMessage> packet);
void handleDomainContentReplacementFromURLRequest(QSharedPointer<ReceivedMessage> message);
void handleOctreeFileReplacementRequest(QSharedPointer<ReceivedMessage> message);
@ -129,6 +132,9 @@ private slots:
void handleSuccessfulICEServerAddressUpdate(QNetworkReply* requestReply);
void handleFailedICEServerAddressUpdate(QNetworkReply* requestReply);
void handleSuccessfulScreensharePresence(QNetworkReply* requestReply);
void handleFailedScreensharePresence(QNetworkReply* requestReply);
void updateReplicatedNodes();
void updateDownstreamNodes();
void updateUpstreamNodes();

View file

@ -49,4 +49,6 @@ Item {
Component.onCompleted: {
load(root.url, root.scriptUrl);
}
signal sendToScript(var message);
}

View file

@ -184,6 +184,7 @@
#include "scripting/AssetMappingsScriptingInterface.h"
#include "scripting/ClipboardScriptingInterface.h"
#include "scripting/DesktopScriptingInterface.h"
#include "scripting/ScreenshareScriptingInterface.h"
#include "scripting/AccountServicesScriptingInterface.h"
#include "scripting/HMDScriptingInterface.h"
#include "scripting/MenuScriptingInterface.h"
@ -967,6 +968,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) {
DependencyManager::set<KeyboardScriptingInterface>();
DependencyManager::set<GrabManager>();
DependencyManager::set<AvatarPackager>();
DependencyManager::set<ScreenshareScriptingInterface>();
PlatformHelper::setup();
QObject::connect(PlatformHelper::instance(), &PlatformHelper::systemWillWake, [] {
@ -2919,6 +2921,7 @@ Application::~Application() {
DependencyManager::destroy<SoundCache>();
DependencyManager::destroy<OctreeStatsProvider>();
DependencyManager::destroy<GeometryCache>();
DependencyManager::destroy<ScreenshareScriptingInterface>();
DependencyManager::get<ResourceManager>()->cleanup();
@ -3430,7 +3433,7 @@ void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) {
surfaceContext->setContextProperty("Users", DependencyManager::get<UsersScriptingInterface>().data());
surfaceContext->setContextProperty("UserActivityLogger", DependencyManager::get<UserActivityLoggerScriptingInterface>().data());
surfaceContext->setContextProperty("Screenshare", DependencyManager::get<ScreenshareScriptingInterface>().data());
surfaceContext->setContextProperty("Camera", &_myCamera);
#if defined(Q_OS_MAC) || defined(Q_OS_WIN)
@ -3536,6 +3539,7 @@ void Application::userKickConfirmation(const QUuid& nodeID) {
}
void Application::setupQmlSurface(QQmlContext* surfaceContext, bool setAdditionalContextProperties) {
surfaceContext->setContextProperty("Screenshare", DependencyManager::get<ScreenshareScriptingInterface>().data());
surfaceContext->setContextProperty("Users", DependencyManager::get<UsersScriptingInterface>().data());
surfaceContext->setContextProperty("HMD", DependencyManager::get<HMDScriptingInterface>().data());
surfaceContext->setContextProperty("UserActivityLogger", DependencyManager::get<UserActivityLoggerScriptingInterface>().data());
@ -7314,6 +7318,7 @@ void Application::registerScriptEngineWithApplicationServices(const ScriptEngine
scriptEngine->registerGlobalObject("AvatarList", DependencyManager::get<AvatarManager>().data());
scriptEngine->registerGlobalObject("Camera", &_myCamera);
scriptEngine->registerGlobalObject("Screenshare", DependencyManager::get<ScreenshareScriptingInterface>().data());
#if defined(Q_OS_MAC) || defined(Q_OS_WIN)
scriptEngine->registerGlobalObject("SpeechRecognizer", DependencyManager::get<SpeechRecognizer>().data());

View file

@ -0,0 +1,276 @@
//
// 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 <UUID.h>
#include "EntityScriptingInterface.h"
#include "ScreenshareScriptingInterface.h"
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);
};
ScreenshareScriptingInterface::~ScreenshareScriptingInterface() {
stopScreenshare();
}
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.
static const glm::vec3 LOCAL_SCREENSHARE_WEB_ENTITY_LOCAL_POSITION(0.0f, -0.0862f, 0.0711f);
static const glm::vec3 LOCAL_SCREENSHARE_WEB_ENTITY_DIMENSIONS(4.0419f, 2.2735f, 0.0100f);
static const QString LOCAL_SCREENSHARE_WEB_ENTITY_URL =
"https://content.highfidelity.com/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()) {
qDebug() << "Screenshare executable doesn't exist at" << SCREENSHARE_EXE_PATH;
stopScreenshare();
emit screenshareError();
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
);
}
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 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();
emit screenshareProcessTerminated();
}
// 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;
}
// 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);
localScreenshareWebEntityProps.setLocalPosition(LOCAL_SCREENSHARE_WEB_ENTITY_LOCAL_POSITION);
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) {
qDebug() << "Failed to get screenshare info via HTTP. Error:" << reply->errorString();
stopScreenshare();
emit screenshareError();
}
// 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());
}
}
}

View file

@ -0,0 +1,72 @@
//
// ScreenshareScriptingInterface.h
// 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
//
#ifndef hifi_ScreenshareScriptingInterface_h
#define hifi_ScreenshareScriptingInterface_h
#include <QObject>
#include <QProcess>
#include <QtCore/QCoreApplication>
#include <QNetworkReply>
#include <PathUtils.h>
class ScreenshareScriptingInterface : public QObject, public Dependency {
Q_OBJECT
public:
ScreenshareScriptingInterface();
~ScreenshareScriptingInterface();
Q_INVOKABLE void startScreenshare(const QUuid& screenshareZoneID, const QUuid& smartboardEntityID, const bool& isPresenter = false);
Q_INVOKABLE void stopScreenshare();
signals:
void screenshareError();
void screenshareProcessTerminated();
void startScreenshareViewer();
private slots:
void onWebEventReceived(const QUuid& entityID, const QVariant& message);
void handleSuccessfulScreenshareInfoGet(QNetworkReply* reply);
void handleFailedScreenshareInfoGet(QNetworkReply* reply);
private:
#if DEV_BUILD
#ifdef Q_OS_WIN
const QString SCREENSHARE_EXE_PATH{ PathUtils::projectRootPath() + "/screenshare/hifi-screenshare-win32-x64/hifi-screenshare.exe" };
#elif defined(Q_OS_MAC)
const QString SCREENSHARE_EXE_PATH{ PathUtils::projectRootPath() + "/screenshare/screenshare-darwin-x64/hifi-screenshare.app" };
#else
// This path won't exist on other platforms, so the Screenshare Scripting Interface will exit early when invoked.
const QString SCREENSHARE_EXE_PATH{ PathUtils::projectRootPath() + "/screenshare/screenshare-other-os/hifi-screenshare" };
#endif
#else
#ifdef Q_OS_WIN
const QString SCREENSHARE_EXE_PATH{ QCoreApplication::applicationDirPath() + "/hifi-screenshare/hifi-screenshare.exe" };
#elif defined(Q_OS_MAC)
const QString SCREENSHARE_EXE_PATH{ QCoreApplication::applicationDirPath() + "/hifi-screenshare/hifi-screenshare.app" };
#else
// This path won't exist on other platforms, so the Screenshare Scripting Interface will exit early when invoked.
const QString SCREENSHARE_EXE_PATH{ QCoreApplication::applicationDirPath() + "/hifi-screenshare/hifi-screenshare" };
#endif
#endif
std::unique_ptr<QProcess> _screenshareProcess{ nullptr };
QUuid _screenshareViewerLocalWebEntityUUID;
QString _token{ "" };
QString _projectAPIKey{ "" };
QString _sessionID{ "" };
QUuid _screenshareZoneID;
QUuid _smartboardEntityID;
bool _isPresenter{ false };
};
#endif // hifi_ScreenshareScriptingInterface_h

View file

@ -135,6 +135,7 @@ public:
AudioSoloRequest,
BulkAvatarTraitsAck,
StopInjector,
AvatarZonePresence,
NUM_PACKET_TYPE
};
@ -185,7 +186,8 @@ public:
<< PacketTypeEnum::Value::OctreeFileReplacement << PacketTypeEnum::Value::ReplicatedMicrophoneAudioNoEcho
<< PacketTypeEnum::Value::ReplicatedMicrophoneAudioWithEcho << PacketTypeEnum::Value::ReplicatedInjectAudio
<< PacketTypeEnum::Value::ReplicatedSilentAudioFrame << PacketTypeEnum::Value::ReplicatedAvatarIdentity
<< PacketTypeEnum::Value::ReplicatedKillAvatar << PacketTypeEnum::Value::ReplicatedBulkAvatarData;
<< PacketTypeEnum::Value::ReplicatedKillAvatar << PacketTypeEnum::Value::ReplicatedBulkAvatarData
<< PacketTypeEnum::Value::AvatarZonePresence;
return NON_SOURCED_PACKETS;
}

4
screenshare/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
hifi-screenshare-*/
hifi-screenshare*.zip
screenshare*.zip
screenshare-*/

View file

@ -0,0 +1,38 @@
set(TARGET_NAME screenshare)
add_custom_target(${TARGET_NAME}-npm-install
COMMAND npm install
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
add_custom_target(${TARGET_NAME}
COMMAND npm run packager -- --out ${CMAKE_CURRENT_BINARY_DIR}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
DEPENDS ${TARGET_NAME}-npm-install
)
set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "screenshare")
set_target_properties(${TARGET_NAME}-npm-install PROPERTIES FOLDER "hidden/screenshare")
if (WIN32)
set(PACKAGED_SCREENSHARE_FOLDER "hifi-screenshare-win32-x64")
set(SCREENSHARE_DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/${PACKAGED_SCREENSHARE_FOLDER}")
install(
DIRECTORY "${SCREENSHARE_DESTINATION}/"
DESTINATION ${SCREENSHARE_INSTALL_DIR}
)
set(EXECUTABLE_PATH "${SCREENSHARE_DESTINATION}/${SCREENSHARE_EXEC_NAME}")
optional_win_executable_signing()
elseif (APPLE)
set(PACKAGED_SCREENSHARE_FOLDER "hifi-screenshare-darwin-x64/${SCREENSHARE_EXEC_NAME}")
install(
DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/${PACKAGED_SCREENSHARE_FOLDER}"
USE_SOURCE_PERMISSIONS
DESTINATION ${SCREENSHARE_INSTALL_DIR}
)
endif()
# DO build the Screenshare Electron app when building the `ALL_BUILD` target.
# DO build the Screenshare Electron app when a user selects "Build Solution" from within Visual Studio.
set_target_properties(${TARGET_NAME} PROPERTIES EXCLUDE_FROM_ALL FALSE EXCLUDE_FROM_DEFAULT_BUILD FALSE)
set_target_properties(${TARGET_NAME}-npm-install PROPERTIES EXCLUDE_FROM_ALL FALSE EXCLUDE_FROM_DEFAULT_BUILD FALSE)

16
screenshare/README.md Normal file
View file

@ -0,0 +1,16 @@
# Screen Sharing within High Fidelity
This Screen Share app, built using Electron, allows for easy desktop screen sharing when used in conjuction with various scripts in the `hifi-content` repository.
# Screen Sharing Source Files
## `packager.js`
Calling npm run packager will use this file to create the actual Electron `hifi-screenshare` executable.
It will kick out a folder `hifi-screenshare-<platform>` which contains an executable.
## `src/screenshareApp.js`
The main process file to configure the electron app.
## `src/screenshareMainProcess.js`
The render file to display the app's UI.
## `screenshareApp.html`
The HTML that displays the screen selection UI and the confirmation screen UI.

2289
screenshare/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

27
screenshare/package.json Normal file
View file

@ -0,0 +1,27 @@
{
"name": "highfidelity_screenshare",
"version": "1.0.0",
"description": "High Fidelity Screenshare",
"main": "src/screenshareMainProcess.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"packager": "node packager.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/highfidelity/hifi.git"
},
"author": "High Fidelity",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/highfidelity/hifi/issues"
},
"homepage": "https://github.com/highfidelity/hifi#readme",
"devDependencies": {
"electron": "^6.0.12",
"electron-packager": "^14.0.6"
},
"dependencies": {
"yargs": "^14.2.0"
}
}

50
screenshare/packager.js Normal file
View file

@ -0,0 +1,50 @@
var packager = require('electron-packager');
var osType = require('os').type();
var argv = require('yargs').argv;
var platform = null;
if (osType == "Darwin" || osType == "Linux") {
platform = osType.toLowerCase();
} else if (osType == "Windows_NT") {
platform = "win32"
}
var NAME = "hifi-screenshare";
var options = {
dir: __dirname,
name: NAME,
version: "0.1.0",
overwrite: true,
prune: true,
arch: "x64",
platform: platform,
ignore: "electron-packager|README.md|CMakeLists.txt|packager.js|.gitignore"
};
// setup per OS options
if (osType == "Darwin") {
options["app-bundle-id"] = "com.highfidelity.hifi-screenshare";
} else if (osType == "Windows_NT") {
options["version-string"] = {
CompanyName: "High Fidelity, Inc.",
FileDescription: "High Fidelity Screenshare",
ProductName: NAME,
OriginalFilename: NAME + ".exe"
}
}
// check if we were passed a custom out directory, pass it along if so
if (argv.out) {
options.out = argv.out
}
// call the packager to produce the executable
packager(options)
.then(appPath => {
console.log("Wrote new app to " + appPath);
})
.catch(error => {
console.error("There was an error writing the packaged console: " + error.message);
process.exit(1);
});

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 B

View file

@ -0,0 +1,52 @@
<!--
screenshareApp.html
Created by Milad Nazeri, Rebecca Stankus, and Zach Fox 2019/11/13
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
-->
<html>
<head>
<link href="styles.css" rel="stylesheet">
</head>
<body id="main">
<div id="title" class="text_title">
<h1>Share your screen</h1>
<h3 id="subtitle">Please select the content you'd like to share.</h3>
</div>
<button id="screenshare" onclick="stopSharing()" style="display: none;">Stop Screenshare</button>
<div id="select_screen">
<div class="scrollbar" id="style-1">
<div class="force-overflow">
<div id="selects">
</div>
</div>
</div>
</div>
<div id="confirmation_screen" style="display: none;">
<div id="share_pick">
</div> <!-- share_pick -->
<div id="confirmation_text" style="color: white;">
<p>
Are you sure you'd like to share <span id="content_name">Content Name</span>?
</p>
<p>
Others will be able to see everything contained within this view.
</p>
</div>
<div id="button_selection">
<div id="yes" class="button_confirmation grey_background" onClick="screenConfirmed(true)">
YES, SHARE THIS CONTENT
</div>
<div id="no" class="button_confirmation" onClick="screenConfirmed(false)">
No, don't share this content
</div>
</div>
</div> <!-- confirmation screen -->
<script src="screenshareApp.js"></script>
<script src="https://static.opentok.com/v2/js/opentok.min.js"></script>
</body>
</html>

View file

@ -0,0 +1,298 @@
'use strict';
// screenshareApp.js
//
// Created by Milad Nazeri, Rebecca Stankus, and Zach Fox 2019/11/13
// 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
// Helpers
function handleError(error) {
if (error) {
console.error(error);
}
}
// When an application is picked, make sure we clear out the previous pick, toggle the page,
// and add the correct source
let currentScreensharePickID = "";
function screensharePicked(id) {
currentScreensharePickID = id;
document.getElementById("share_pick").innerHTML = "";
togglePage();
addSource(sourceMap[id], "share_pick");
}
// Once we have confirmed that we want to share, prepare the tokbox publishing initiating
// and toggle back to the selects page
function screenConfirmed(isConfirmed) {
document.getElementById("selects").innerHTML = "";
if (isConfirmed === true){
onAccessApproved(currentScreensharePickID);
}
togglePage();
}
// Hide/show the select page or the confirmation page
let currentPage = "mainPage";
function togglePage(){
if (currentPage === "mainPage") {
currentPage = "confirmationPage";
document.getElementById("select_screen").style.display = "none";
document.getElementById("subtitle").innerHTML = "Confirm that you'd like to share this content.";
document.getElementById("confirmation_screen").style.display = "block";
} else {
showSources();
currentPage = "mainPage";
document.getElementById("select_screen").style.display = "block";
document.getElementById("subtitle").innerHTML = "Please select the content you'd like to share.";
document.getElementById("confirmation_screen").style.display = "none";
}
}
// UI
// Render the html properly and append that to the correct parent
function addSource(source, type) {
let renderedHTML = renderSourceHTML(source);
if (type === "selects") {
document.getElementById("selects").appendChild(renderedHTML);
} else {
document.getElementById("share_pick").appendChild(renderedHTML);
document.getElementById("content_name").innerHTML = source.name;
}
}
// Get the html created from the source. Alter slightly depending on whether this source
// is on the selects screen, or the confirmation screen. Mainly removing highlighting.
// If there is an app Icon, then add it.
function renderSourceHTML(source) {
let type = currentPage === "confirmationPage" ? "share_pick" : "selects";
let sourceBody = document.createElement('div')
let thumbnail = source.thumbnail.toDataURL();
sourceBody.classList.add("box")
if (type === "share_pick") {
sourceBody.style.marginLeft = "0px";
}
let image = "";
if (source.appIcon) {
image = `<img class="icon" src="${source.appIcon.toDataURL()}" />`;
}
sourceBody.innerHTML = `
<div class="heading">
${image}
<span class="screen_label">${source.name}</span>
</div>
<div class="${type === "share_pick" ? "image_no_hover" : "image"}" onclick="screensharePicked('${source.id}')">
<img src="${thumbnail}" />
</div>
`
return sourceBody;
}
// Separate out the screenshares and applications
// Make sure the screens are labeled in order
// Concact the two arrays back together and return
function sortSources() {
let screenSources = [];
let applicationSources = [];
// Difference with Mac selects:
// 1 screen = "Enitre Screen", more than one like PC "Screen 1, Screen 2..."
screenshareSourceArray.forEach((source) => {
if (source.name.match(/(entire )?screen( )?([0-9]?)/i)) {
screenSources.push(source);
} else {
applicationSources.push(source)
}
});
screenSources.sort((a, b) => {
let aNumber = a.name.replace(/[^\d]/, "");
let bNumber = b.name.replace(/[^\d]/, "");
return aNumber - bNumber;
});
let finalSources = [...screenSources, ...applicationSources];
return finalSources;
}
// Setup sorting the selection array, add individual sources, and update the sourceMap
function addSources() {
screenshareSourceArray = sortSources();
for (let i = 0; i < screenshareSourceArray.length; i++) {
addSource(screenshareSourceArray[i], "selects");
sourceMap[screenshareSourceArray[i].id] = screenshareSourceArray[i];
}
}
// 1. Get the screens and window that are available from electron
// 2. Remove the screenshare app itself
// 3. Create a source map to help grab the correct source when picked
// 4. push all the sources for sorting to the source array
// 5. Add thse sources
const electron = require('electron');
const SCREENSHARE_TITLE = "Screen share";
const SCREENSHARE_TITLE_REGEX = new RegExp("^" + SCREENSHARE_TITLE + "$");
const IMAGE_WIDTH = 265;
const IMAGE_HEIGHT = 165;
let screenshareSourceArray = [];
let sourceMap = {};
function showSources() {
screenshareSourceArray = [];
electron.desktopCapturer.getSources({
types:['window', 'screen'],
thumbnailSize: {
width: IMAGE_WIDTH,
height: IMAGE_HEIGHT
},
fetchWindowIcons: true
}, (error, sources) => {
if (error) {
console.log("Error getting sources", error);
}
for (let source of sources) {
if (source.name.match(SCREENSHARE_TITLE_REGEX)){
continue;
}
sourceMap[source.id] = source;
screenshareSourceArray.push(source);
}
addSources();
});
}
// Stop the localstream and end the tokrok publishing
let localStream;
function stopSharing() {
desktopSharing = false;
if (localStream) {
localStream.getTracks()[0].stop();
localStream = null;
}
document.getElementById('screenshare').style.display = "none";
stopTokBoxPublisher();
}
// Callback to start publishing after we have setup the chromium stream
function gotStream(stream) {
localStream = stream;
startTokboxPublisher(localStream);
stream.onended = () => {
if (desktopSharing) {
togglePage();
}
};
}
// After we grant access to electron, create a stream and using the callback
// start the tokbox publisher
function onAccessApproved(desktop_id) {
if (!desktop_id) {
console.log('Desktop Capture access rejected.');
return;
}
document.getElementById('screenshare').style.visibility = "block";
desktopSharing = true;
navigator.webkitGetUserMedia({
audio: false,
video: {
mandatory: {
chromeMediaSource: 'desktop',
chromeMediaSourceId: desktop_id,
minWidth: 1280,
maxWidth: 1280,
minHeight: 720,
maxHeight: 720
}
}
}, gotStream, handleError);
}
// Tokbox
// Once we have the connection info, this will create the session which will allow
// us to publish a stream when we are ready
function initializeTokboxSession() {
session = OT.initSession(projectAPIKey, sessionID);
session.on('sessionDisconnected', (event) => {
console.log('You were disconnected from the session.', event.reason);
});
// Connect to the session
session.connect(token, (error) => {
if (error) {
handleError(error);
}
});
}
// Init the tokbox publisher with our newly created stream
var publisher;
function startTokboxPublisher(stream) {
publisher = document.createElement("div");
var publisherOptions = {
videoSource: stream.getVideoTracks()[0],
audioSource: null,
insertMode: 'append',
width: 1280,
height: 720
};
publisher = OT.initPublisher(publisher, publisherOptions, function(error){
if (error) {
console.log("ERROR: " + error);
} else {
session.publish(publisher, function(error) {
if (error) {
console.log("ERROR FROM Session.publish: " + error);
return;
}
})
}
});
}
// Kills the streaming being sent to tokbox
function stopTokBoxPublisher() {
publisher.destroy();
}
// When the app is ready, we get this info from the command line arguments.
const ipcRenderer = electron.ipcRenderer;
let projectAPIKey;
let sessionID;
let token;
let session;
ipcRenderer.on('connectionInfo', function(event, message) {
const connectionInfo = JSON.parse(message);
projectAPIKey = connectionInfo.projectAPIKey;
sessionID = connectionInfo.sessionID;
token = connectionInfo.token;
initializeTokboxSession();
});
// Show the initial sources after the dom has loaded
// Sources come from electron.desktopCapturer
document.addEventListener("DOMContentLoaded", () => {
showSources();
});

View file

@ -0,0 +1,74 @@
'use strict';
// screenshareMainProcess.js
//
// Milad Nazeri and Zach Fox 2019/11/13
// 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
const {app, BrowserWindow, ipcMain} = require('electron');
const gotTheLock = app.requestSingleInstanceLock()
const argv = require('yargs').argv;
const connectionInfo = {
token: argv.token || "token",
projectAPIKey: argv.projectAPIKey || "projectAPIKey",
sessionID: argv.sessionID || "sessionID"
}
// Mac and PC need slightly different width and height sizes.
const osType = require('os').type();
let width;
let height;
if (osType == "Darwin" || osType == "Linux") {
width = 960;
height = 660;
} else if (osType == "Windows_NT") {
width = 973;
height = 740;
}
if (!gotTheLock) {
console.log("Another instance of the screenshare is already running - this instance will quit.");
app.exit(0);
return;
}
let window;
const zoomFactor = 1.0;
function createWindow(){
window = new BrowserWindow({
backgroundColor: "#000000",
width: width,
height: height,
center: true,
frame: true,
useContentSize: true,
zoomFactor: zoomFactor,
resizable: false,
webPreferences: {
nodeIntegration: true
},
icon: __dirname + `/resources/interface.png`,
skipTaskbar: false,
title: "Screen share"
});
window.loadURL('file://' + __dirname + '/screenshareApp.html');
window.setMenu(null);
window.webContents.on("did-finish-load", () => {
window.webContents.send('connectionInfo', JSON.stringify(connectionInfo));
});
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
app.on('ready', function() {
createWindow();
window.webContents.send('connectionInfo', JSON.stringify(connectionInfo))
});

217
screenshare/src/styles.css Normal file
View file

@ -0,0 +1,217 @@
body {
background-color: black;
box-sizing: border-box;
font-family: "Graphik";
margin: 0px 22px 10px 22px;
}
#confirmation_screen {
width: 100%;
display: flex;
text-align: center;
justify-content: center;
align-items: center;
}
#confirmation_text {
margin-top: 65px;
font-size: 25px;
line-height: 25px;
}
#confirmation_text p {
margin: 0px;
}
#button_selection {
margin-top: 25px;
width: 100%;
display: flex;
text-align: center;
justify-content: center;
align-items: center;
flex-direction: column;
}
.button_confirmation {
margin: 4px;
cursor: pointer;
width: 300px;
height: 32px;
line-height: 32px;
text-align: center;
vertical-align: middle;
color: white
}
.grey_background {
background-color: #191919;
}
#no {
color: rgba(1, 152, 203,0.7);
}
#no:hover {
color: rgba(1, 152, 203,1);
}
#yes {
outline: solid white 2px;
}
#yes:hover {
background: #0198CB;
}
yes:hover + #yes_background {
display: block;
}
#share_pick {
margin-top: 60px;
}
.text_title {
color: white;
}
@font-face {
font-family: "Graphik";
src: url("./resources/Graphik-Regular.ttf");
}
#title {
margin-top: 21px;
}
h1 {
line-height: 48px;
font-size: 48px;
margin: 0px;
}
h3 {
line-height: 24px;
font-size: 24px;
margin: 9px 0px 0px 0px;
}
#publisher {
visibility: hidden;
width: 0px;
height: 0px;
bottom: 10px;
left: 10px;
z-index: 100;
border: 3px solid white;
border-radius: 3px;
}
#selects {
margin-right: 19px;
padding-left: 3px;
}
.screen_label {
max-width: 220px;
font-size: 25px;
line-height: 25px;
color: white;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.icon {
display: inline-block;
background: #000000;
width: 20px;
height: 20px;
margin-right: 15px;
}
.box {
height: 165px;
width: 265px;
display: inline-block;
margin-left: 35px;
margin-top: 60px;
cursor: pointer;
}
.box:nth-child(1) {
margin-top: 0 !important;
}
.box:nth-child(2) {
margin-top: 0 !important;
}
.box:nth-child(3) {
margin-top: 0 !important;
}
.box:nth-child(3n) {
margin-right: 0 !important;
}
.box:nth-child(3n+1) {
margin-left: 0 !important;
}
.heading {
height: 35px;
display: flex;
align-items: center;
}
.image {
width: 265px;
height: 165px;
max-height: 165px;
max-width: 265px;
margin: 0px;
}
.image:hover {
outline: solid white 3px;
}
.image_no_hover {
width: 265px;
height: 165px;
max-height: 165px;
max-width: 265px;
}
img {
width: 265px;
height: 165px;
margin: 0px;
}
.scrollbar {
float: right;
height: 500px;
width: 100%;
overflow-y: scroll;
margin-top: 40px;
}
#style-1::-webkit-scrollbar {
width: 9px;
overflow: scroll;
overflow-x: hidden;
}
#style-1::-webkit-scrollbar-thumb {
background-color: #0198CB;
width: 9px;
}
#style-1::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
background-color: #848484;
width: 9px;
}

View file

@ -0,0 +1,428 @@
//
// Created by Luis Cuenca on 11/14/19
// 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
//
(function() {
var ZoomStatus = {
"zoomingIn" : 0,
"zoomingOut" : 1,
"zoomedIn" : 2,
"zoomedOut" : 4,
"consumed" : 5
}
var FocusType = {
"avatar" : 0,
"entity" : 1
}
var EasingFunctions = {
easeInOutQuad: function (t) { return t<.5 ? 2*t*t : -1+(4-2*t)*t },
// accelerating from zero velocity
easeInCubic: function (t) { return t*t*t },
// decelerating to zero velocity
easeOutCubic: function (t) { return (--t)*t*t+1 },
// acceleration until halfway, then deceleration
easeInOutCubic: function (t) { return t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1 },
// accelerating from zero velocity
easeInQuart: function (t) { return t*t*t*t },
// decelerating to zero velocity
easeOutQuart: function (t) { return 1-(--t)*t*t*t },
// acceleration until halfway, then deceleration
easeInOutQuart: function (t) { return t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t },
// accelerating from zero velocity
easeInQuint: function (t) { return t*t*t*t*t },
// decelerating to zero velocity
easeOutQuint: function (t) { return 1+(--t)*t*t*t*t },
// acceleration until halfway, then deceleration
easeInOutQuint: function (t) { return t<.5 ? 16*t*t*t*t*t : 1+16*(--t)*t*t*t*t }
};
var ZoomData = function(type, lookAt, focusNormal, focusDimensions, velocity, maxDuration) {
var self = this;
this.focusType = type;
this.lookAt = lookAt;
this.focusDimensions = focusDimensions;
this.focusNormal = focusNormal;
this.velocity = velocity;
this.maxDuration = maxDuration;
this.initialPos = Camera.getPosition();
this.initialRot = Camera.getOrientation();
this.interpolatedPos = this.initialPos;
this.interpolatedRot = this.initialRot;
this.initialMode = Camera.mode;
this.initialOffset = Vec3.distance(self.initialPos, MyAvatar.getDefaultEyePosition());
this.finalPos = Vec3.ZERO;
this.finalRot = Quat.IDENTITY;
this.direction = Vec3.ZERO;
this.distance = Vec3.ZERO;
this.totalTime = 0.0;
this.elapsedTime = 0.0;
this.maxZoomInAmount = 0.6;
this.maxZoomOutAmount = 0.2;
this.currentZoomAmount = 0.0;
this.zoomPanOffset = {x: 0.5 * Window.innerWidth, y: 0.5 * Window.innerHeight};
this.zoomPanDelta = {x: 0.0, y: 0.0};
this.status = ZoomStatus.zoomedOut;
var OWN_CAMERA_CHANGE_MAX_FRAMES = 30;
this.ownCameraChangeElapseTime = 0.0;
this.ownCameraChange = false;
this.applyZoomPan = function() {
self.zoomPanOffset.x += self.zoomPanDelta.x;
self.zoomPanOffset.y += self.zoomPanDelta.y;
}
this.setZoomPanDelta = function(x, y) {
self.zoomPanDelta.x = x;
self.zoomPanDelta.y = y;
var totalX = self.zoomPanOffset.x + self.zoomPanDelta.x;
var totalY = self.zoomPanOffset.y + self.zoomPanDelta.y;
totalX = Math.min(Math.max(totalX, 0.0), Window.innerWidth);
totalY = Math.min(Math.max(totalY, 0.0), Window.innerHeight);
self.zoomPanDelta.x = totalX - self.zoomPanOffset.x;
self.zoomPanDelta.y = totalY - self.zoomPanOffset.y;
self.updateSuperPan(totalX, totalY);
}
this.getFocusDistance = function(zoomDims) {
var objAspect = zoomDims.x / zoomDims.y;
var camAspect = Camera.frustum.aspectRatio;
var m = 0.0;
if (objAspect < camAspect) {
m = zoomDims.y;
} else {
m = zoomDims.x / camAspect;
}
var DEGREE_TO_RADIAN = 0.0174533;
var fov = DEGREE_TO_RADIAN * Camera.frustum.fieldOfView;
return (0.5 * m) / Math.tan(0.5 * fov);
}
this.finalPos = Vec3.sum(this.lookAt, Vec3.multiply(this.getFocusDistance(this.focusDimensions), this.focusNormal));
this.finalRot = Quat.lookAtSimple(this.finalPos, this.lookAt);
this.computeRouteIn = function() {
var railVector = Vec3.subtract(self.finalPos, self.initialPos);
self.direction = Vec3.normalize(railVector);
self.distance = Vec3.length(railVector);
self.totalTime = self.distance / self.velocity;
self.totalTime = self.totalTime > self.maxDuration ? self.maxDuration : self.totalTime;
}
this.computeRouteOut = function() {
self.finalPos = Camera.getPosition();
var camOffset = Vec3.ZERO;
var myAvatarRotation = MyAvatar.orientation;
if (self.initialMode.indexOf("first") != -1) {
self.initialRot = myAvatarRotation;
} else {
var lookAtPoint = MyAvatar.getDefaultEyePosition();
var lookFromSign = self.initialMode.indexOf("selfie") != -1 ? 1 : -1;
var lookFromPoint = Vec3.sum(lookAtPoint, Vec3.multiply(self.initialOffset * lookFromSign, Quat.getFront(myAvatarRotation)));
self.initialRot = Quat.lookAtSimple(lookFromPoint, lookAtPoint);
self.initialPos = lookFromPoint;
}
self.computeRouteIn();
}
this.initZoomIn = function() {
if (self.status === ZoomStatus.zoomedOut) {
self.computeRouteIn();
self.status = ZoomStatus.zoomingIn;
self.changeCameraMode("independent");
self.elapsedTime = 0.0;
}
}
this.initZoomOut = function() {
if (self.status === ZoomStatus.zoomedIn) {
self.computeRouteOut();
self.status = ZoomStatus.zoomingOut;
self.changeCameraMode("independent");
self.elapsedTime = 0.0;
}
}
this.needsUpdate = function() {
return self.status === ZoomStatus.zoomingIn || self.status === ZoomStatus.zoomingOut;
}
this.updateZoom = function(deltaTime) {
if (self.ownCameraChange) {
self.ownCameraChange = self.ownCameraChangeElapseTime < OWN_CAMERA_CHANGE_MAX_FRAMES * deltaTime;
self.ownCameraChangeElapseTime += deltaTime;
}
if (!self.needsUpdate) {
return;
}
if (self.elapsedTime < self.totalTime) {
var ratio = EasingFunctions.easeInOutQuart(self.elapsedTime / self.totalTime);
if (self.status === ZoomStatus.zoomingIn) {
var curDist = self.distance * ratio;
var addition = Vec3.multiply(curDist, self.direction);
self.interpolatedPos = Vec3.sum(self.initialPos, addition);
self.interpolatedRot = Quat.mix(self.initialRot, self.finalRot, ratio);
} else if (self.status === ZoomStatus.zoomingOut) {
self.interpolatedPos = Vec3.sum(self.finalPos, Vec3.multiply(self.distance * ratio, Vec3.multiply(-1, self.direction)));
self.interpolatedRot = Quat.mix(self.finalRot, self.initialRot, ratio);
}
self.elapsedTime += deltaTime;
Camera.setPosition(self.interpolatedPos);
Camera.setOrientation(self.interpolatedRot);
} else {
Camera.setPosition(self.finalPos);
Camera.setOrientation(self.finalRot);
if (self.status === ZoomStatus.zoomingIn) {
self.status = ZoomStatus.zoomedIn;
} else if (self.status === ZoomStatus.zoomingOut) {
self.status = ZoomStatus.consumed;
self.changeCameraMode(self.initialMode);
}
}
}
this.resetZoomAspect = function() {
self.computeRouteIn();
Camera.setPosition(self.finalPos);
}
this.updateSuperZoom = function(delta) {
var ZOOM_STEP = 0.1;
self.currentZoomAmount = self.currentZoomAmount + (delta < 0.0 ? -1 : 1) * ZOOM_STEP;
self.currentZoomAmount = Math.min(Math.max(self.currentZoomAmount, -self.maxZoomOutAmount), self.maxZoomInAmount);
self.updateSuperPan(self.zoomPanOffset.x, self.zoomPanOffset.y);
}
this.updateSuperPan = function(x, y) {
var zoomOffset = Vec3.multiply(self.currentZoomAmount, Vec3.subtract(self.lookAt, self.finalPos));
var xRatio = 0.5 - x / Window.innerWidth;
var yRatio = 0.5 - y / Window.innerHeight;
var cameraOrientation = Camera.getOrientation();
var cameraY = Quat.getUp(cameraOrientation);
var cameraX = Vec3.multiply(-1, Quat.getRight(cameraOrientation));
var xOffset = Vec3.multiply(xRatio * self.focusDimensions.x, cameraX);
var yOffset = Vec3.multiply(yRatio * self.focusDimensions.y, cameraY);
zoomOffset = Vec3.sum(zoomOffset, xOffset);
zoomOffset = Vec3.sum(zoomOffset, yOffset);
Camera.setPosition(Vec3.sum(self.finalPos, zoomOffset));
}
this.abort = function() {
self.changeCameraMode(self.initialMode);
}
this.changeCameraMode = function(mode) {
self.ownCameraChange = true;
self.ownCameraChangeElapseTime = 0.0;
Camera.mode = mode;
}
}
var ZoomOnAnything = function() {
var self = this;
this.zoomEntityID;
this.zoomEntityData;
this.zoomCameraPos;
var ZOOM_MAX_VELOCITY = 15.0; // meters per second
var ZOOM_MAX_DURATION = 1.0;
this.zoomDelta = {x: 0.0, y: 0.0};
this.isPanning = false;
this.screenPointRef = {x: 0.0, y: 0.0};
this.getEntityDimsFromNormal = function(dims, rot, normal) {
var zoomXNormal = Vec3.multiplyQbyV(rot, Vec3.UNIT_X);
var zoomYNormal = Vec3.multiplyQbyV(rot, Vec3.UNIT_Y);
var zoomZNormal = Vec3.multiplyQbyV(rot, Vec3.UNIT_Z);
var affinities = [
{axis: "x", normal: zoomXNormal, affin: Math.abs(Vec3.dot(zoomXNormal, normal)), dims: {x: dims.z, y: dims.y}},
{axis: "y", normal: zoomYNormal, affin: Math.abs(Vec3.dot(zoomYNormal, normal)), dims: {x: dims.x, y: dims.z}},
{axis: "z", normal: zoomZNormal, affin: Math.abs(Vec3.dot(zoomZNormal, normal)), dims: {x: dims.x, y: dims.y}}
];
affinities.sort(function(a, b) {
return b.affin - a.affin;
});
return affinities[0];
}
this.getAvatarFocusPoint = function(avatar) {
var rEyeIndex = avatar.getJointIndex("RightEye");
var lEyeIndex = avatar.getJointIndex("LeftEye");
var headIndex = avatar.getJointIndex("Head");
var focusPoint = Vec3.ZERO;
var validPoint = false;
var count = 0;
if (rEyeIndex != -1) {
focusPoint = Vec3.sum(focusPoint, avatar.getJointPosition(rEyeIndex));
validPoint = true;
count++;
}
if (lEyeIndex != -1) {
var leftEyePos = avatar.getJointPosition(lEyeIndex);
var NORMAL_EYE_DISTANCE = 0.1;
focusPoint = Vec3.sum(focusPoint, leftEyePos);
validPoint = true;
count++;
}
if (headIndex != -1) {
focusPoint = Vec3.sum(focusPoint, avatar.getJointPosition(headIndex));
validPoint = true;
count++;
}
if (!validPoint) {
focusPoint = avatar.getJointPosition("Hips");
count++;
}
return Vec3.multiply(1.0/count, focusPoint);
}
this.getZoomDataFromAvatar = function(avatarID, skinToBoneDist, zoomVelocity, maxDuration) {
var headDiam = 2.0 * skinToBoneDist;
headDiam = headDiam < 0.5 ? 0.5 : headDiam;
var avatar = AvatarList.getAvatar(avatarID);
var focusPoint = self.getAvatarFocusPoint(avatar);
var focusDims = {x: headDiam, y: headDiam};
var focusNormal = Quat.getFront(avatar.orientation);
var zoomData = new ZoomData(FocusType.avatar, focusPoint, focusNormal, focusDims, zoomVelocity, maxDuration);
return zoomData;
}
this.getZoomDataFromEntity = function(intersection, objectProps, zoomVelocity, maxDuration) {
var position = objectProps.position;
var dimensions = objectProps.dimensions;
var rotation = objectProps.rotation;
var focusNormal = intersection.surfaceNormal;
var dimsResult = self.getEntityDimsFromNormal(dimensions, rotation, focusNormal);
var focusDims = dimsResult.dims;
var focusDepth = Vec3.dot(Vec3.subtract(intersection.intersection, position), dimsResult.normal);
var newPosition = Vec3.sum(position, Vec3.multiply(focusDepth, dimsResult.normal));
var zoomData = new ZoomData(FocusType.entity, newPosition, focusNormal, focusDims, zoomVelocity, maxDuration);
return zoomData;
}
this.zoomOnEntity = function(intersection, objectProps) {
self.zoomEntityData = self.getZoomDataFromEntity(intersection, objectProps, ZOOM_MAX_VELOCITY, ZOOM_MAX_DURATION);
self.zoomEntityData.initZoomIn();
}
this.zoomOnAvatar = function(avatarID, skinToBoneDist) {
self.zoomEntityData = self.getZoomDataFromAvatar(avatarID, skinToBoneDist, ZOOM_MAX_VELOCITY, ZOOM_MAX_DURATION);
self.zoomEntityData.initZoomIn();
}
this.updateZoom = function(deltaTime) {
if (self.zoomEntityData && self.zoomEntityData.needsUpdate()) {
self.zoomEntityData.updateZoom(deltaTime);
if (self.zoomEntityData.status === ZoomStatus.consumed) {
self.zoomEntityData = undefined;
}
}
}
this.mouseDoublePressEvent = function(event) {
if (event.isLeftButton) {
if (!self.zoomEntityData) {
var pickRay = Camera.computePickRay(event.x, event.y);
var intersection = AvatarManager.findRayIntersection({origin: pickRay.origin, direction: pickRay.direction}, [], [MyAvatar.sessionUUID], false);
zoomingAtAvatarID = intersection.intersects ? intersection.avatarID : undefined;
if (!zoomingAtAvatarID) {
intersection = Entities.findRayIntersection({origin: pickRay.origin, direction: pickRay.direction}, true);
self.zoomEntityID = intersection.entityID;
var entityProps = Entities.getEntityProperties(intersection.entityID);
if (entityProps.type === "Shape") {
var FIND_SHAPES_DISTANCE = 10.0;
var shapes = Entities.findEntitiesByType("Shape", intersection.intersection, FIND_SHAPES_DISTANCE);
intersection = Entities.findRayIntersection({origin: pickRay.origin, direction: pickRay.direction}, true, [], shapes);
self.zoomEntityID = intersection.entityID;
entityProps = Entities.getEntityProperties(intersection.entityID);
}
if (!entityProps.dimensions) {
return;
}
self.zoomOnEntity(intersection, entityProps);
} else {
var avatar = AvatarList.getAvatar(zoomingAtAvatarID);
var skinToBoneDist = Vec3.distance(intersection.intersection, avatar.getJointPosition(intersection.jointIndex));
self.zoomOnAvatar(zoomingAtAvatarID, skinToBoneDist);
}
} else if (!self.zoomEntityData.needsUpdate()){
self.zoomEntityData.initZoomOut();
}
}
}
this.mousePressEvent = function(event) {
if (event.isRightButton) {
self.isPanning = true;
self.screenPointRef = {x: event.x, y: event.y};
}
}
this.mouseReleaseEvent = function(event) {
if (event.isRightButton) {
if (self.zoomEntityData) {
self.zoomEntityData.applyZoomPan();
self.isPanning = false;
self.screenPointRef = {x: 0, y: 0};
}
}
}
this.mouseMoveEvent = function(event) {
if (event.isRightButton) {
if (self.isPanning && self.zoomEntityData) {
self.zoomEntityData.setZoomPanDelta(event.x - self.screenPointRef.x, event.y - self.screenPointRef.y);
}
}
}
this.mouseWheel = function(event) {
if (self.zoomEntityData) {
self.zoomEntityData.updateSuperZoom(event.delta);
}
}
this.abort = function() {
self.zoomEntityData.abort();
self.zoomEntityData = undefined;
}
}
var zoomOE = new ZoomOnAnything();
Window.geometryChanged.connect(function() {
if (zoomOE.zoomEntityData){
zoomOE.zoomEntityData.resetZoomAspect();
}
});
Camera.modeUpdated.connect(function(mode) {
if (zoomOE.zoomEntityData && !zoomOE.zoomEntityData.ownCameraChange) {
zoomOE.abort();
}
});
Controller.mousePressEvent.connect(zoomOE.mousePressEvent);
Controller.mouseDoublePressEvent.connect(zoomOE.mouseDoublePressEvent);
Controller.mouseMoveEvent.connect(zoomOE.mouseMoveEvent);
Controller.mouseReleaseEvent.connect(zoomOE.mouseReleaseEvent);
Controller.wheelEvent.connect(zoomOE.mouseWheel);
Script.update.connect(zoomOE.updateZoom);
Script.scriptEnding.connect(function() {
if (zoomOE.zoomEntityData) {
zoomOE.abort();
}
});
})();

View file

@ -17,7 +17,8 @@ var currentlyRunningScripts = ScriptDiscoveryService.getRunning();
var DEFAULT_SCRIPTS_SEPARATE = [
"system/controllers/controllerScripts.js",
"simplifiedUI/ui/simplifiedUI.js"
"simplifiedUI/ui/simplifiedUI.js",
"simplifiedUI/clickToZoom/clickToZoom.js"
];
function loadSeparateDefaults() {
for (var i = 0; i < DEFAULT_SCRIPTS_SEPARATE.length; i++) {