Merge remote-tracking branch 'refs/remotes/highfidelity/master'

# Conflicts:
#	scripts/defaultScripts.js
This commit is contained in:
Delanir 2017-06-22 11:51:05 +01:00
commit 6045169914
625 changed files with 14437 additions and 12213 deletions

View file

@ -1,4 +1,4 @@
###Dependencies
### Dependencies
* [cmake](https://cmake.org/download/) ~> 3.3.2
* [Qt](https://www.qt.io/download-open-source) ~> 5.6.2
@ -6,7 +6,7 @@
* IMPORTANT: Use the latest available version of OpenSSL to avoid security vulnerabilities.
* [VHACD](https://github.com/virneo/v-hacd)(clone this repository)(Optional)
####CMake External Project Dependencies
#### CMake External Project Dependencies
* [boostconfig](https://github.com/boostorg/config) ~> 1.58
* [Bullet Physics Engine](https://github.com/bulletphysics/bullet3/releases) ~> 2.83
@ -30,16 +30,19 @@ These are not placed in your normal build tree when doing an out of source build
If you would like to use a specific install of a dependency instead of the version that would be grabbed as a CMake ExternalProject, you can pass -DUSE_LOCAL_$NAME=0 (where $NAME is the name of the subfolder in [cmake/externals](cmake/externals)) when you run CMake to tell it not to get that dependency as an external project.
###OS Specific Build Guides
### OS Specific Build Guides
* [BUILD_OSX.md](BUILD_OSX.md) - additional instructions for OS X.
* [BUILD_LINUX.md](BUILD_LINUX.md) - additional instructions for Linux.
* [BUILD_WIN.md](BUILD_WIN.md) - additional instructions for Windows.
* [BUILD_ANDROID.md](BUILD_ANDROID.md) - additional instructions for Android
###CMake
### CMake
Hifi uses CMake to generate build files and project files for your platform.
####Qt
#### Qt
In order for CMake to find the Qt5 find modules, you will need to set a QT_CMAKE_PREFIX_PATH environment variable pointing to your Qt installation.
This can either be entered directly into your shell session before you build or in your shell profile (e.g.: ~/.bash_profile, ~/.bashrc, ~/.zshrc - this depends on your shell and environment).
@ -50,7 +53,8 @@ The path it needs to be set to will depend on where and how Qt5 was installed. e
export QT_CMAKE_PREFIX_PATH=/usr/local/Cellar/qt5/5.6.2/lib/cmake
export QT_CMAKE_PREFIX_PATH=/usr/local/opt/qt5/lib/cmake
####Generating build files
#### Generating build files
Create a build directory in the root of your checkout and then run the CMake build from there. This will keep the rest of the directory clean.
mkdir build
@ -59,14 +63,15 @@ Create a build directory in the root of your checkout and then run the CMake bui
If cmake gives you the same error message repeatedly after the build fails (e.g. you had a typo in the QT_CMAKE_PREFIX_PATH that you fixed but the `.cmake` files still cannot be found), try removing `CMakeCache.txt`.
####Variables
#### Variables
Any variables that need to be set for CMake to find dependencies can be set as ENV variables in your shell profile, or passed directly to CMake with a `-D` flag appended to the `cmake ..` command.
For example, to pass the QT_CMAKE_PREFIX_PATH variable during build file generation:
cmake .. -DQT_CMAKE_PREFIX_PATH=/usr/local/qt/5.6.2/lib/cmake
####Finding Dependencies
#### Finding Dependencies
The following applies for dependencies we do not grab via CMake ExternalProject (OpenSSL is an example), or for dependencies you have opted not to grab as a CMake ExternalProject (via -DUSE_LOCAL_$NAME=0). The list of dependencies we grab by default as external projects can be found in [the CMake External Project Dependencies section](#cmake-external-project-dependencies).
@ -78,8 +83,8 @@ In the examples below the variable $NAME would be replaced by the name of the de
* $NAME_ROOT_DIR - set this variable in your ENV
* HIFI_LIB_DIR - set this variable in your ENV to your High Fidelity lib folder, should contain a folder '$name'
###Optional Components
### Optional Components
####Devices
#### Devices
You can support external input/output devices such as Leap Motion, MIDI, and more by adding each individual SDK in the visible building path. Refer to the readme file available in each device folder in [interface/external/](interface/external) for the detailed explanation of the requirements to use the device.

View file

@ -1,6 +1,6 @@
Please read the [general build guide](BUILD.md) for information on dependencies required for all platforms. Only Android specific instructions are found in this file.
###Android Dependencies
### Android Dependencies
You will need the following tools to build our Android targets.
@ -17,23 +17,23 @@ You will need the following tools to build our Android targets.
You will also need to cross-compile the dependencies required for all platforms for Android, and help CMake find these compiled libraries on your machine.
####Scribe
#### Scribe
High Fidelity has a shader pre-processing tool called `scribe` that various libraries will call on during the build process. You must compile scribe using your native toolchain (following the build instructions for your platform) and then pass a CMake variable or set an ENV variable `SCRIBE_PATH` that is a path to the scribe executable.
CMake will fatally error if it does not find the scribe executable while using the android toolchain.
####Optional Components
#### Optional Components
* [Oculus Mobile SDK](https://developer.oculus.com/downloads/#sdk=mobile) ~> 0.4.2
####ANDROID_LIB_DIR
#### ANDROID_LIB_DIR
Since you won't be installing Android dependencies to system paths on your development machine, CMake will need a little help tracking down your Android dependencies.
This is most easily accomplished by installing all Android dependencies in the same folder. You can place this folder wherever you like on your machine. In this build guide and across our CMakeLists files this folder is referred to as `ANDROID_LIB_DIR`. You can set `ANDROID_LIB_DIR` in your environment or by passing when you run CMake.
####Qt
#### Qt
Install Qt 5.5.1 for Android for your host environment from the [Qt downloads page](http://www.qt.io/download/). Install Qt to ``$ANDROID_LIB_DIR/Qt``. This is required so that our root CMakeLists file can help CMake find your Android Qt installation.
@ -41,7 +41,7 @@ The component required for the Android build is the `Android armv7` component.
If you would like to install Qt to a different location, or attempt to build with a different Qt version, you can pass `ANDROID_QT_CMAKE_PREFIX_PATH` to CMake. Point to the `cmake` folder inside `$VERSION_NUMBER/android_armv7/lib`. Otherwise, our root CMakeLists will set it to `$ANDROID_LIB_DIR/Qt/5.5/android_armv7/lib/cmake`.
####OpenSSL
#### OpenSSL
Cross-compilation of OpenSSL has been tested from an OS X machine running 10.10 compiling OpenSSL 1.0.2. It is likely that the steps below will work for other OpenSSL versions than 1.0.2.
@ -76,7 +76,7 @@ This should generate libcrypto and libssl in the root of the OpenSSL directory.
If you have been building other components it is possible that the OpenSSL compile will fail based on the values other cross-compilations (tbb, bullet) have set. Ensure that you are in a new terminal window to avoid compilation errors from previously set environment variables.
####Oculus Mobile SDK
#### Oculus Mobile SDK
The Oculus Mobile SDK is optional, for Gear VR support. It is not required to compile gvr-interface.
@ -91,7 +91,7 @@ ndk-build
This will create the liboculus.a archive that our FindLibOVR module will look for when cmake is run.
#####Hybrid testing
##### Hybrid testing
Currently the 'vr_dual' mode that would allow us to run a hybrid app has limited support in the Oculus Mobile SDK. The best way to have an application we can launch without having to connect to the GearVR is to put the Gear VR Service into developer mode. This stops Oculus Home from taking over the device when it is plugged into the Gear VR headset, and allows the application to be launched from the Applications page.
@ -99,7 +99,7 @@ To put the Gear VR Service into developer mode you need an application with an O
Once the application is on your device, go to `Settings->Application Manager->Gear VR Service->Manage Storage`. Tap on `VR Service Version` six times. It will scan your device to verify that you have an osig file in an application on your device, and then it will let you enable Developer mode.
###CMake
### CMake
We use CMake to generate the makefiles that compile and deploy the Android APKs to your device. In order to create Makefiles for the Android targets, CMake requires that some environment variables are set, and that other variables are passed to it when it is run.

View file

@ -1,6 +1,7 @@
Please read the [general build guide](BUILD.md) for information on dependencies required for all platforms. Only Linux specific instructions are found in this file.
###Qt5 Dependencies
### Qt5 Dependencies
Should you choose not to install Qt5 via a package manager that handles dependencies for you, you may be missing some Qt5 dependencies. On Ubuntu, for example, the following additional packages are required:
libasound2 libxmu-dev libxi-dev freeglut3-dev libasound2-dev libjack0 libjack-dev libxrandr-dev libudev-dev libssl-dev

View file

@ -1,12 +1,13 @@
Please read the [general build guide](BUILD.md) for information on dependencies required for all platforms. Only OS X specific instructions are found in this file.
###Homebrew
### Homebrew
[Homebrew](https://brew.sh/) is an excellent package manager for OS X. It makes install of some High Fidelity dependencies very simple.
brew tap homebrew/versions
brew install cmake openssl
###OpenSSL
### OpenSSL
Assuming you've installed OpenSSL using the homebrew instructions above, you'll need to set OPENSSL_ROOT_DIR so CMake can find your installations.
For OpenSSL installed via homebrew, set OPENSSL_ROOT_DIR:
@ -15,7 +16,8 @@ For OpenSSL installed via homebrew, set OPENSSL_ROOT_DIR:
Note that this uses the version from the homebrew formula at the time of this writing, and the version in the path will likely change.
###Qt
### Qt
Download and install the [Qt 5.6.2 for macOS](http://download.qt.io/official_releases/qt/5.6/5.6.2/qt-opensource-mac-x64-clang-5.6.2.dmg).
Keep the default components checked when going through the installer.
@ -23,7 +25,8 @@ Keep the default components checked when going through the installer.
Once Qt is installed, you need to manually configure the following:
* Set the QT_CMAKE_PREFIX_PATH environment variable to your `Qt5.6.2/5.6/clang_64/lib/cmake/` directory.
###Xcode
### Xcode
If Xcode is your editor of choice, you can ask CMake to generate Xcode project files instead of Unix Makefiles.
cmake .. -GXcode

View file

@ -1,5 +1,7 @@
This is a stand-alone guide for creating your first High Fidelity build for Windows 64-bit.
## Building High Fidelity
### Step 1. Installing Visual Studio 2013
If you don't already have the Community or Professional edition of Visual Studio 2013, download and install [Visual Studio Community 2013](https://www.visualstudio.com/en-us/news/releasenotes/vs2013-community-vs). You do not need to install any of the optional components when going through the installer.
@ -18,8 +20,8 @@ Keep the default components checked when going through the installer.
### Step 4. Setting Qt Environment Variable
Go to "Control Panel > System > Advanced System Settings > Environment Variables > New..." (or search “Environment Variables” in Start Search).
* Set "Variable name": QT_CMAKE_PREFIX_PATH
Go to `Control Panel > System > Advanced System Settings > Environment Variables > New...` (or search “Environment Variables” in Start Search).
* Set "Variable name": `QT_CMAKE_PREFIX_PATH`
* Set "Variable value": `%QT_DIR%\5.6\msvc2013_64\lib\cmake`
### Step 5. Installing OpenSSL
@ -29,34 +31,36 @@ Download and install the [Win64 OpenSSL v1.0.2L Installer](https://slproweb.com/
### Step 6. Running CMake to Generate Build Files
Run Command Prompt from Start and run the following commands:
cd "%HIFI_DIR%"
mkdir build
cd build
cmake .. -G "Visual Studio 12 Win64"
````
cd "%HIFI_DIR%"
mkdir build
cd build
cmake .. -G "Visual Studio 12 Win64"
````
Where %HIFI_DIR% is the directory for the highfidelity repository.
Where `%HIFI_DIR%` is the directory for the highfidelity repository.
### Step 7. Making a Build
Open '%HIFI_DIR%\build\hifi.sln' using Visual Studio.
Open `%HIFI_DIR%\build\hifi.sln` using Visual Studio.
Change the Solution Configuration (next to the green play button) from "Debug" to "Release" for best performance.
Run Build > Build Solution.
Run `Build > Build Solution`.
### Step 8. Testing Interface
Create another environment variable (see Step #4)
* Set "Variable name": _NO_DEBUG_HEAP
* Set "Variable value": 1
* Set "Variable name": `_NO_DEBUG_HEAP`
* Set "Variable value": `1`
In Visual Studio, right+click "interface" under the Apps folder in Solution Explorer and select "Set as Startup Project". Run Debug > Start Debugging.
In Visual Studio, right+click "interface" under the Apps folder in Solution Explorer and select "Set as Startup Project". Run `Debug > Start Debugging`.
Now, you should have a full build of High Fidelity and be able to run the Interface using Visual Studio. Please check our [Docs](https://wiki.highfidelity.com/wiki/Main_Page) for more information regarding the programming workflow.
Note: You can also run Interface by launching it from command line or File Explorer from %HIFI_DIR%\build\interface\Release\interface.exe
Note: You can also run Interface by launching it from command line or File Explorer from `%HIFI_DIR%\build\interface\Release\interface.exe`
### Troubleshooting
## Troubleshooting
For any problems after Step #6, first try this:
* Delete your locally cloned copy of the highfidelity repository
@ -66,16 +70,16 @@ For any problems after Step #6, first try this:
#### CMake gives you the same error message repeatedly after the build fails
Remove `CMakeCache.txt` found in the '%HIFI_DIR%\build' directory
Remove `CMakeCache.txt` found in the `%HIFI_DIR%\build` directory.
#### nmake cannot be found
Make sure nmake.exe is located at the following path:
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin
If not, add the directory where nmake is located to the PATH environment variable.
#### Qt is throwing an error
Make sure you have the correct version (5.6.2) installed and 'QT_CMAKE_PREFIX_PATH' environment variable is set correctly.
Make sure you have the correct version (5.6.2) installed and `QT_CMAKE_PREFIX_PATH` environment variable is set correctly.

View file

@ -2,15 +2,15 @@ Follow the [build guide](BUILD.md) to figure out how to build High Fidelity for
During generation, CMake should produce an `install` target and a `package` target.
###Install
### Install
The `install` target will copy the High Fidelity targets and their dependencies to your `CMAKE_INSTALL_PREFIX`.
###Packaging
### Packaging
To produce an installer, run the `package` target.
####Windows
#### Windows
To produce an executable installer on Windows, the following are required:
@ -20,6 +20,6 @@ To produce an executable installer on Windows, the following are required:
Run the `package` target to create an executable installer using the Nullsoft Scriptable Install System.
####OS X
#### OS X
Run the `package` target to create an Apple Disk Image (.dmg).

View file

@ -23,6 +23,7 @@
#include <AvatarHashMap.h>
#include <AudioInjectorManager.h>
#include <AssetClient.h>
#include <LocationScriptingInterface.h>
#include <MessagesClient.h>
#include <NetworkAccessManager.h>
#include <NodeList.h>
@ -166,11 +167,7 @@ void Agent::run() {
// Setup MessagesClient
auto messagesClient = DependencyManager::set<MessagesClient>();
QThread* messagesThread = new QThread;
messagesThread->setObjectName("Messages Client Thread");
messagesClient->moveToThread(messagesThread);
connect(messagesThread, &QThread::started, messagesClient.data(), &MessagesClient::init);
messagesThread->start();
messagesClient->startThread();
// make sure we hear about connected nodes so we can grab an ATP script if a request is pending
connect(nodeList.data(), &LimitedNodeList::nodeActivated, this, &Agent::nodeActivated);
@ -410,6 +407,7 @@ void Agent::executeScript() {
bool openedInLastBlock = !_audioGateOpen && audioGateOpen; // the gate just opened
bool closedInLastBlock = _audioGateOpen && !audioGateOpen; // the gate just closed
_audioGateOpen = audioGateOpen;
Q_UNUSED(openedInLastBlock);
// the codec must be flushed to silence before sending silent packets,
// so delay the transition to silent packets by one packet after becoming silent.
@ -456,6 +454,9 @@ void Agent::executeScript() {
_scriptEngine->registerGlobalObject("EntityViewer", &_entityViewer);
_scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter,
LocationScriptingInterface::locationSetter);
auto recordingInterface = DependencyManager::get<RecordingScriptingInterface>();
_scriptEngine->registerGlobalObject("Recording", recordingInterface.data());

View file

@ -23,7 +23,6 @@
#include <EntityEditPacketSender.h>
#include <EntityTree.h>
#include <EntityTreeHeadlessViewer.h>
#include <ScriptEngine.h>
#include <ThreadedAssignment.h>
@ -31,6 +30,7 @@
#include "AudioGate.h"
#include "MixedAudioStream.h"
#include "entities/EntityTreeHeadlessViewer.h"
#include "avatars/ScriptableAvatar.h"
class Agent : public ThreadedAssignment {

View file

@ -69,17 +69,7 @@ AssignmentClient::AssignmentClient(Assignment::Type requestAssignmentType, QStri
DependencyManager::set<ResourceScriptingInterface>();
DependencyManager::set<UserActivityLoggerScriptingInterface>();
// setup a thread for the NodeList and its PacketReceiver
QThread* nodeThread = new QThread(this);
nodeThread->setObjectName("NodeList Thread");
nodeThread->start();
// make sure the node thread is given highest priority
nodeThread->setPriority(QThread::TimeCriticalPriority);
// put the NodeList on the node thread
nodeList->moveToThread(nodeThread);
nodeList->startThread();
// set the logging target to the the CHILD_TARGET_NAME
LogHandler::getInstance().setTargetName(ASSIGNMENT_CLIENT_TARGET_NAME);
@ -166,14 +156,8 @@ void AssignmentClient::stopAssignmentClient() {
}
AssignmentClient::~AssignmentClient() {
QThread* nodeThread = DependencyManager::get<NodeList>()->thread();
// remove the NodeList from the DependencyManager
DependencyManager::destroy<NodeList>();
// ask the node thread to quit and wait until it is done
nodeThread->quit();
nodeThread->wait();
}
void AssignmentClient::aboutToQuit() {

View file

@ -186,6 +186,10 @@ AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) :
listenPort = argumentVariantMap.value(ASSIGNMENT_CLIENT_LISTEN_PORT_OPTION).toUInt();
}
if (parser.isSet(portOption)) {
listenPort = parser.value(portOption).toUInt();
}
if (parser.isSet(numChildsOption)) {
if (minForks && minForks > numForks) {
qCritical() << "--min can't be more than -n";

View file

@ -55,7 +55,8 @@ QVector<AudioMixer::ZoneSettings> AudioMixer::_zoneSettings;
QVector<AudioMixer::ReverbSettings> AudioMixer::_zoneReverbSettings;
AudioMixer::AudioMixer(ReceivedMessage& message) :
ThreadedAssignment(message) {
ThreadedAssignment(message)
{
// Always clear settings first
// This prevents previous assignment settings from sticking around
@ -92,7 +93,21 @@ AudioMixer::AudioMixer(ReceivedMessage& message) :
packetReceiver.registerListener(PacketType::NodeMuteRequest, this, "handleNodeMuteRequestPacket");
packetReceiver.registerListener(PacketType::KillAvatar, this, "handleKillAvatarPacket");
packetReceiver.registerListenerForTypes({
PacketType::ReplicatedMicrophoneAudioNoEcho,
PacketType::ReplicatedMicrophoneAudioWithEcho,
PacketType::ReplicatedInjectAudio,
PacketType::ReplicatedSilentAudioFrame
},
this, "queueReplicatedAudioPacket"
);
connect(nodeList.data(), &NodeList::nodeKilled, this, &AudioMixer::handleNodeKilled);
connect(nodeList.data(), &NodeList::nodeAdded, this, [this](const SharedNodePointer& node) {
if (node->getType() == NodeType::DownstreamAudioMixer) {
node->activatePublicSocket();
}
});
}
void AudioMixer::queueAudioPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer node) {
@ -103,6 +118,33 @@ void AudioMixer::queueAudioPacket(QSharedPointer<ReceivedMessage> message, Share
getOrCreateClientData(node.data())->queuePacket(message, node);
}
void AudioMixer::queueReplicatedAudioPacket(QSharedPointer<ReceivedMessage> message) {
// make sure we have a replicated node for the original sender of the packet
auto nodeList = DependencyManager::get<NodeList>();
QUuid nodeID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
auto replicatedNode = nodeList->addOrUpdateNode(nodeID, NodeType::Agent,
message->getSenderSockAddr(), message->getSenderSockAddr(),
true, true);
replicatedNode->setLastHeardMicrostamp(usecTimestampNow());
// construct a "fake" audio received message from the byte array and packet list information
auto audioData = message->getMessage().mid(NUM_BYTES_RFC4122_UUID);
PacketType rewrittenType = REPLICATED_PACKET_MAPPING.key(message->getType());
if (rewrittenType == PacketType::Unknown) {
qDebug() << "Cannot unwrap replicated packet type not present in REPLICATED_PACKET_WRAPPING";
}
auto replicatedMessage = QSharedPointer<ReceivedMessage>::create(audioData, rewrittenType,
versionForPacketType(rewrittenType),
message->getSenderSockAddr(), nodeID);
getOrCreateClientData(replicatedNode.data())->queuePacket(replicatedMessage, replicatedNode);
}
void AudioMixer::handleMuteEnvironmentPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
auto nodeList = DependencyManager::get<NodeList>();
@ -347,7 +389,7 @@ void AudioMixer::start() {
auto nodeList = DependencyManager::get<NodeList>();
// prepare the NodeList
nodeList->addSetOfNodeTypesToNodeInterestSet({ NodeType::Agent, NodeType::EntityScriptServer });
nodeList->addSetOfNodeTypesToNodeInterestSet({ NodeType::Agent, NodeType::DownstreamAudioMixer, NodeType::EntityScriptServer });
nodeList->linkedDataCreateCallback = [&](Node* node) { getOrCreateClientData(node); };
// parse out any AudioMixer settings
@ -506,7 +548,7 @@ void AudioMixer::clearDomainSettings() {
_zoneReverbSettings.clear();
}
void AudioMixer::parseSettingsObject(const QJsonObject &settingsObject) {
void AudioMixer::parseSettingsObject(const QJsonObject& settingsObject) {
qDebug() << "AVX2 Support:" << (cpuSupportsAVX2() ? "enabled" : "disabled");
if (settingsObject.contains(AUDIO_THREADING_GROUP_KEY)) {

View file

@ -63,6 +63,7 @@ private slots:
void handleKillAvatarPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void queueAudioPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void queueReplicatedAudioPacket(QSharedPointer<ReceivedMessage> packet);
void removeHRTFsForFinishedInjector(const QUuid& streamID);
void start();

View file

@ -68,9 +68,21 @@ void AudioMixerClientData::processPackets() {
case PacketType::MicrophoneAudioWithEcho:
case PacketType::InjectAudio:
case PacketType::AudioStreamStats:
case PacketType::SilentAudioFrame: {
case PacketType::SilentAudioFrame:
case PacketType::ReplicatedMicrophoneAudioNoEcho:
case PacketType::ReplicatedMicrophoneAudioWithEcho:
case PacketType::ReplicatedInjectAudio:
case PacketType::ReplicatedSilentAudioFrame: {
if (node->isUpstream() && !_hasSetupCodecForUpstreamNode) {
setupCodecForReplicatedAgent(packet);
}
QMutexLocker lock(&getMutex());
parseData(*packet);
optionallyReplicatePacket(*packet, *node);
break;
}
case PacketType::NegotiateAudioFormat:
@ -97,6 +109,59 @@ void AudioMixerClientData::processPackets() {
assert(_packetQueue.empty());
}
bool isReplicatedPacket(PacketType packetType) {
return packetType == PacketType::ReplicatedMicrophoneAudioNoEcho
|| packetType == PacketType::ReplicatedMicrophoneAudioWithEcho
|| packetType == PacketType::ReplicatedInjectAudio
|| packetType == PacketType::ReplicatedSilentAudioFrame;
}
void AudioMixerClientData::optionallyReplicatePacket(ReceivedMessage& message, const Node& node) {
// first, make sure that this is a packet from a node we are supposed to replicate
if (node.isReplicated()) {
// now make sure it's a packet type that we want to replicate
// first check if it is an original type that we should replicate
PacketType mirroredType = REPLICATED_PACKET_MAPPING.value(message.getType());
if (mirroredType == PacketType::Unknown) {
// if it wasn't check if it is a replicated type that we should re-replicate
if (REPLICATED_PACKET_MAPPING.key(message.getType()) != PacketType::Unknown) {
mirroredType = message.getType();
} else {
qDebug() << "Packet passed to optionallyReplicatePacket was not a replicatable type - returning";
return;
}
}
std::unique_ptr<NLPacket> packet;
auto nodeList = DependencyManager::get<NodeList>();
// enumerate the downstream audio mixers and send them the replicated version of this packet
nodeList->unsafeEachNode([&](const SharedNodePointer& downstreamNode) {
if (downstreamNode->getType() == NodeType::DownstreamAudioMixer) {
// construct the packet only once, if we have any downstream audio mixers to send to
if (!packet) {
// construct an NLPacket to send to the replicant that has the contents of the received packet
packet = NLPacket::create(mirroredType);
if (!isReplicatedPacket(message.getType())) {
// since this packet will be non-sourced, we add the replicated node's ID here
packet->write(node.getUUID().toRfc4122());
}
packet->write(message.getMessage());
}
nodeList->sendUnreliablePacket(*packet, *downstreamNode);
}
});
}
}
void AudioMixerClientData::negotiateAudioFormat(ReceivedMessage& message, const SharedNodePointer& node) {
quint8 numberOfCodecs;
message.readPrimitive(&numberOfCodecs);
@ -188,8 +253,11 @@ int AudioMixerClientData::parseData(ReceivedMessage& message) {
bool isMicStream = false;
if (packetType == PacketType::MicrophoneAudioWithEcho
|| packetType == PacketType::ReplicatedMicrophoneAudioWithEcho
|| packetType == PacketType::MicrophoneAudioNoEcho
|| packetType == PacketType::SilentAudioFrame) {
|| packetType == PacketType::ReplicatedMicrophoneAudioNoEcho
|| packetType == PacketType::SilentAudioFrame
|| packetType == PacketType::ReplicatedSilentAudioFrame) {
QWriteLocker writeLocker { &_streamsLock };
@ -209,7 +277,8 @@ int AudioMixerClientData::parseData(ReceivedMessage& message) {
avatarAudioStream->setupCodec(_codec, _selectedCodecName, AudioConstants::MONO);
qDebug() << "creating new AvatarAudioStream... codec:" << _selectedCodecName;
connect(avatarAudioStream, &InboundAudioStream::mismatchedAudioCodec, this, &AudioMixerClientData::handleMismatchAudioFormat);
connect(avatarAudioStream, &InboundAudioStream::mismatchedAudioCodec,
this, &AudioMixerClientData::handleMismatchAudioFormat);
auto emplaced = _audioStreams.emplace(
QUuid(),
@ -224,10 +293,12 @@ int AudioMixerClientData::parseData(ReceivedMessage& message) {
writeLocker.unlock();
isMicStream = true;
} else if (packetType == PacketType::InjectAudio) {
} else if (packetType == PacketType::InjectAudio
|| packetType == PacketType::ReplicatedInjectAudio) {
// this is injected audio
// grab the stream identifier for this injected audio
message.seek(sizeof(quint16));
QUuid streamIdentifier = QUuid::fromRfc4122(message.readWithoutCopy(NUM_BYTES_RFC4122_UUID));
bool isStereo;
@ -608,3 +679,22 @@ bool AudioMixerClientData::shouldIgnore(const SharedNodePointer self, const Shar
return shouldIgnore;
}
void AudioMixerClientData::setupCodecForReplicatedAgent(QSharedPointer<ReceivedMessage> message) {
// hop past the sequence number that leads the packet
message->seek(sizeof(quint16));
// pull the codec string from the packet
auto codecString = message->readString();
qDebug() << "Manually setting codec for replicated agent" << uuidStringWithoutCurlyBraces(getNodeID())
<< "-" << codecString;
const std::pair<QString, CodecPluginPointer> codec = AudioMixer::negotiateCodec({ codecString });
setupCodec(codec.second, codec.first);
_hasSetupCodecForUpstreamNode = true;
// seek back to the beginning of the message so other readers are in the right place
message->seek(0);
}

View file

@ -26,7 +26,6 @@
#include "PositionalAudioStream.h"
#include "AvatarAudioStream.h"
class AudioMixerClientData : public NodeData {
Q_OBJECT
public:
@ -108,6 +107,8 @@ public:
bool getRequestsDomainListData() { return _requestsDomainListData; }
void setRequestsDomainListData(bool requesting) { _requestsDomainListData = requesting; }
void setupCodecForReplicatedAgent(QSharedPointer<ReceivedMessage> message);
signals:
void injectorStreamFinished(const QUuid& streamIdentifier);
@ -124,6 +125,8 @@ private:
QReadWriteLock _streamsLock;
AudioStreamMap _audioStreams; // microphone stream from avatar is stored under key of null UUID
void optionallyReplicatePacket(ReceivedMessage& packet, const Node& node);
using IgnoreZone = AABox;
class IgnoreZoneMemo {
public:
@ -181,6 +184,8 @@ private:
bool _shouldMuteClient { false };
bool _requestsDomainListData { false };
bool _hasSetupCodecForUpstreamNode { false };
};
#endif // hifi_AudioMixerClientData_h

View file

@ -74,6 +74,10 @@ void AudioMixerSlave::mix(const SharedNodePointer& node) {
return;
}
if (node->isUpstream()) {
return;
}
// check that the stream is valid
auto avatarStream = data->getAvatarAudioStream();
if (avatarStream == nullptr) {

View file

@ -16,10 +16,10 @@
#include <mutex>
#include <vector>
#include <tbb/concurrent_queue.h>
#include <QThread>
#include <TBBHelpers.h>
#include "AudioMixerSlave.h"
class AudioMixerSlavePool;

View file

@ -54,8 +54,108 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) :
packetReceiver.registerListener(PacketType::RadiusIgnoreRequest, this, "handleRadiusIgnoreRequestPacket");
packetReceiver.registerListener(PacketType::RequestsDomainListData, this, "handleRequestsDomainListDataPacket");
packetReceiver.registerListenerForTypes({
PacketType::ReplicatedAvatarIdentity,
PacketType::ReplicatedKillAvatar
}, this, "handleReplicatedPacket");
packetReceiver.registerListener(PacketType::ReplicatedBulkAvatarData, this, "handleReplicatedBulkAvatarPacket");
auto nodeList = DependencyManager::get<NodeList>();
connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &AvatarMixer::handlePacketVersionMismatch);
connect(nodeList.data(), &NodeList::nodeAdded, this, [this](const SharedNodePointer& node) {
if (node->getType() == NodeType::DownstreamAvatarMixer) {
getOrCreateClientData(node);
node->activatePublicSocket();
}
});
}
SharedNodePointer addOrUpdateReplicatedNode(const QUuid& nodeID, const HifiSockAddr& senderSockAddr) {
auto replicatedNode = DependencyManager::get<NodeList>()->addOrUpdateNode(nodeID, NodeType::Agent,
senderSockAddr,
senderSockAddr,
true, true);
replicatedNode->setLastHeardMicrostamp(usecTimestampNow());
return replicatedNode;
}
void AvatarMixer::handleReplicatedPacket(QSharedPointer<ReceivedMessage> message) {
auto nodeList = DependencyManager::get<NodeList>();
auto nodeID = QUuid::fromRfc4122(message->peek(NUM_BYTES_RFC4122_UUID));
auto replicatedNode = addOrUpdateReplicatedNode(nodeID, message->getSenderSockAddr());
if (message->getType() == PacketType::ReplicatedAvatarIdentity) {
handleAvatarIdentityPacket(message, replicatedNode);
} else if (message->getType() == PacketType::ReplicatedKillAvatar) {
handleKillAvatarPacket(message, replicatedNode);
}
}
void AvatarMixer::handleReplicatedBulkAvatarPacket(QSharedPointer<ReceivedMessage> message) {
while (message->getBytesLeftToRead()) {
// first, grab the node ID for this replicated avatar
auto nodeID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
// make sure we have an upstream replicated node that matches
auto replicatedNode = addOrUpdateReplicatedNode(nodeID, message->getSenderSockAddr());
// grab the size of the avatar byte array so we know how much to read
quint16 avatarByteArraySize;
message->readPrimitive(&avatarByteArraySize);
// read the avatar byte array
auto avatarByteArray = message->read(avatarByteArraySize);
// construct a "fake" avatar data received message from the byte array and packet list information
auto replicatedMessage = QSharedPointer<ReceivedMessage>::create(avatarByteArray, PacketType::AvatarData,
versionForPacketType(PacketType::AvatarData),
message->getSenderSockAddr(), nodeID);
// queue up the replicated avatar data with the client data for the replicated node
auto start = usecTimestampNow();
getOrCreateClientData(replicatedNode)->queuePacket(replicatedMessage, replicatedNode);
auto end = usecTimestampNow();
_queueIncomingPacketElapsedTime += (end - start);
}
}
void AvatarMixer::optionallyReplicatePacket(ReceivedMessage& message, const Node& node) {
// first, make sure that this is a packet from a node we are supposed to replicate
if (node.isReplicated()) {
// check if this is a packet type we replicate
// which means it must be a packet type present in REPLICATED_PACKET_MAPPING or must be the
// replicated version of one of those packet types
PacketType replicatedType = REPLICATED_PACKET_MAPPING.value(message.getType());
if (replicatedType == PacketType::Unknown) {
if (REPLICATED_PACKET_MAPPING.key(message.getType()) != PacketType::Unknown) {
replicatedType = message.getType();
} else {
qDebug() << __FUNCTION__ << "called without replicatable packet type - returning";
return;
}
}
std::unique_ptr<NLPacket> packet;
auto nodeList = DependencyManager::get<NodeList>();
nodeList->eachMatchingNode([&](const SharedNodePointer& node) {
return node->getType() == NodeType::DownstreamAvatarMixer;
}, [&](const SharedNodePointer& node) {
if (!packet) {
// construct an NLPacket to send to the replicant that has the contents of the received packet
packet = NLPacket::create(replicatedType, message.getSize());
packet->write(message.getMessage());
}
nodeList->sendUnreliablePacket(*packet, *node);
});
}
}
void AvatarMixer::queueIncomingPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer node) {
@ -70,12 +170,14 @@ AvatarMixer::~AvatarMixer() {
}
void AvatarMixer::sendIdentityPacket(AvatarMixerClientData* nodeData, const SharedNodePointer& destinationNode) {
QByteArray individualData = nodeData->getAvatar().identityByteArray();
individualData.replace(0, NUM_BYTES_RFC4122_UUID, nodeData->getNodeID().toRfc4122());
auto identityPackets = NLPacketList::create(PacketType::AvatarIdentity, QByteArray(), true, true);
identityPackets->write(individualData);
DependencyManager::get<NodeList>()->sendPacketList(std::move(identityPackets), *destinationNode);
++_sumIdentityPackets;
if (destinationNode->getType() == NodeType::Agent && !destinationNode->isUpstream()) {
QByteArray individualData = nodeData->getAvatar().identityByteArray();
individualData.replace(0, NUM_BYTES_RFC4122_UUID, nodeData->getNodeID().toRfc4122());
auto identityPackets = NLPacketList::create(PacketType::AvatarIdentity, QByteArray(), true, true);
identityPackets->write(individualData);
DependencyManager::get<NodeList>()->sendPacketList(std::move(identityPackets), *destinationNode);
++_sumIdentityPackets;
}
}
std::chrono::microseconds AvatarMixer::timeFrame(p_high_resolution_clock::time_point& timestamp) {
@ -132,7 +234,10 @@ void AvatarMixer::start() {
auto start = usecTimestampNow();
nodeList->nestedEach([&](NodeList::const_iterator cbegin, NodeList::const_iterator cend) {
std::for_each(cbegin, cend, [&](const SharedNodePointer& node) {
manageDisplayName(node);
if (node->getType() == NodeType::Agent && !node->isUpstream()) {
manageIdentityData(node);
}
++_sumListeners;
});
}, &lockWait, &nodeTransform, &functor);
@ -183,8 +288,9 @@ void AvatarMixer::start() {
// NOTE: nodeData->getAvatar() might be side effected, must be called when access to node/nodeData
// is guaranteed to not be accessed by other thread
void AvatarMixer::manageDisplayName(const SharedNodePointer& node) {
void AvatarMixer::manageIdentityData(const SharedNodePointer& node) {
AvatarMixerClientData* nodeData = reinterpret_cast<AvatarMixerClientData*>(node->getLinkedData());
bool sendIdentity = false;
if (nodeData && nodeData->getAvatarSessionDisplayNameMustChange()) {
AvatarData& avatar = nodeData->getAvatar();
const QString& existingBaseDisplayName = nodeData->getBaseDisplayName();
@ -210,9 +316,39 @@ void AvatarMixer::manageDisplayName(const SharedNodePointer& node) {
soFar.second++; // refcount
nodeData->flagIdentityChange();
nodeData->setAvatarSessionDisplayNameMustChange(false);
sendIdentityPacket(nodeData, node); // Tell node whose name changed about its new session display name.
sendIdentity = true;
qCDebug(avatars) << "Giving session display name" << sessionDisplayName << "to node with ID" << node->getUUID();
}
if (nodeData && nodeData->getAvatarSkeletonModelUrlMustChange()) { // never true for an empty _avatarWhitelist
nodeData->setAvatarSkeletonModelUrlMustChange(false);
AvatarData& avatar = nodeData->getAvatar();
static const QUrl emptyURL("");
QUrl url = avatar.cannonicalSkeletonModelURL(emptyURL);
if (!isAvatarInWhitelist(url)) {
qCDebug(avatars) << "Forbidden avatar" << nodeData->getNodeID() << avatar.getSkeletonModelURL() << "replaced with" << (_replacementAvatar.isEmpty() ? "default" : _replacementAvatar);
avatar.setSkeletonModelURL(_replacementAvatar);
sendIdentity = true;
}
}
if (sendIdentity) {
sendIdentityPacket(nodeData, node); // Tell node whose name changed about its new session display name or avatar.
}
}
bool AvatarMixer::isAvatarInWhitelist(const QUrl& url) {
// The avatar is in the whitelist if:
// 1. The avatar's URL's host matches one of the hosts of the URLs in the whitelist AND
// 2. The avatar's URL's path starts with the path of that same URL in the whitelist
for (const auto& whiteListedPrefix : _avatarWhitelist) {
auto whiteListURL = QUrl::fromUserInput(whiteListedPrefix);
// check if this script URL matches the whitelist domain and, optionally, is beneath the path
if (url.host().compare(whiteListURL.host(), Qt::CaseInsensitive) == 0 &&
url.path().startsWith(whiteListURL.path(), Qt::CaseInsensitive)) {
return true;
}
}
return false;
}
void AvatarMixer::throttle(std::chrono::microseconds duration, int frame) {
@ -279,13 +415,38 @@ void AvatarMixer::nodeKilled(SharedNodePointer killedNode) {
}
}
std::unique_ptr<NLPacket> killPacket;
std::unique_ptr<NLPacket> replicatedKillPacket;
// this was an avatar we were sending to other people
// send a kill packet for it to our other nodes
auto killPacket = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID + sizeof(KillAvatarReason));
killPacket->write(killedNode->getUUID().toRfc4122());
killPacket->writePrimitive(KillAvatarReason::AvatarDisconnected);
nodeList->eachMatchingNode([&](const SharedNodePointer& node) {
// we relay avatar kill packets to agents that are not upstream
// and downstream avatar mixers, if the node that was just killed was being replicated
return (node->getType() == NodeType::Agent && !node->isUpstream())
|| (killedNode->isReplicated() && node->getType() == NodeType::DownstreamAvatarMixer);
}, [&](const SharedNodePointer& node) {
if (node->getType() == NodeType::Agent) {
if (!killPacket) {
killPacket = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID + sizeof(KillAvatarReason));
killPacket->write(killedNode->getUUID().toRfc4122());
killPacket->writePrimitive(KillAvatarReason::AvatarDisconnected);
}
nodeList->sendUnreliablePacket(*killPacket, *node);
} else {
// send a replicated kill packet to the downstream avatar mixer
if (!replicatedKillPacket) {
replicatedKillPacket = NLPacket::create(PacketType::ReplicatedKillAvatar,
NUM_BYTES_RFC4122_UUID + sizeof(KillAvatarReason));
replicatedKillPacket->write(killedNode->getUUID().toRfc4122());
replicatedKillPacket->writePrimitive(KillAvatarReason::AvatarDisconnected);
}
nodeList->sendUnreliablePacket(*replicatedKillPacket, *node);
}
});
nodeList->broadcastToNodes(std::move(killPacket), NodeSet() << NodeType::Agent);
// we also want to remove sequence number data for this avatar on our other avatars
// so invoke the appropriate method on the AvatarMixerClientData for other avatars
@ -398,17 +559,20 @@ void AvatarMixer::handleAvatarIdentityPacket(QSharedPointer<ReceivedMessage> mes
AvatarData& avatar = nodeData->getAvatar();
// parse the identity packet and update the change timestamp if appropriate
AvatarData::Identity identity;
AvatarData::parseAvatarIdentityPacket(message->getMessage(), identity);
bool identityChanged = false;
bool displayNameChanged = false;
avatar.processAvatarIdentity(identity, identityChanged, displayNameChanged);
bool skeletonModelUrlChanged = false;
avatar.processAvatarIdentity(message->getMessage(), identityChanged, displayNameChanged, skeletonModelUrlChanged);
if (identityChanged) {
QMutexLocker nodeDataLocker(&nodeData->getMutex());
nodeData->flagIdentityChange();
if (displayNameChanged) {
nodeData->setAvatarSessionDisplayNameMustChange(true);
}
if (skeletonModelUrlChanged && !_avatarWhitelist.isEmpty()) {
nodeData->setAvatarSkeletonModelUrlMustChange(true);
}
}
}
}
@ -416,11 +580,13 @@ void AvatarMixer::handleAvatarIdentityPacket(QSharedPointer<ReceivedMessage> mes
_handleAvatarIdentityPacketElapsedTime += (end - start);
}
void AvatarMixer::handleKillAvatarPacket(QSharedPointer<ReceivedMessage> message) {
void AvatarMixer::handleKillAvatarPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer node) {
auto start = usecTimestampNow();
DependencyManager::get<NodeList>()->processKillNode(*message);
auto end = usecTimestampNow();
_handleKillAvatarPacketElapsedTime += (end - start);
optionallyReplicatePacket(*message, *node);
}
void AvatarMixer::handleNodeIgnoreRequestPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
@ -672,7 +838,6 @@ void AvatarMixer::run() {
connect(&domainHandler, &DomainHandler::settingsReceiveFail, this, &AvatarMixer::domainSettingsRequestFailed);
ThreadedAssignment::commonInit(AVATAR_MIXER_LOGGING_NAME, NodeType::AvatarMixer);
}
AvatarMixerClientData* AvatarMixer::getOrCreateClientData(SharedNodePointer node) {
@ -691,7 +856,7 @@ AvatarMixerClientData* AvatarMixer::getOrCreateClientData(SharedNodePointer node
void AvatarMixer::domainSettingsRequestComplete() {
auto nodeList = DependencyManager::get<NodeList>();
nodeList->addSetOfNodeTypesToNodeInterestSet({ NodeType::Agent, NodeType::EntityScriptServer });
nodeList->addSetOfNodeTypesToNodeInterestSet({ NodeType::Agent, NodeType::DownstreamAvatarMixer, NodeType::EntityScriptServer });
// parse the settings to pull out the values we need
parseDomainServerSettings(nodeList->getDomainHandler().getSettingsObject());
@ -764,4 +929,20 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) {
qCDebug(avatars) << "This domain requires a minimum avatar scale of" << _domainMinimumScale
<< "and a maximum avatar scale of" << _domainMaximumScale;
const QString AVATAR_WHITELIST_DEFAULT{ "" };
static const QString AVATAR_WHITELIST_OPTION = "avatar_whitelist";
_avatarWhitelist = domainSettings[AVATARS_SETTINGS_KEY].toObject()[AVATAR_WHITELIST_OPTION].toString(AVATAR_WHITELIST_DEFAULT).split(',', QString::KeepEmptyParts);
static const QString REPLACEMENT_AVATAR_OPTION = "replacement_avatar";
_replacementAvatar = domainSettings[AVATARS_SETTINGS_KEY].toObject()[REPLACEMENT_AVATAR_OPTION].toString(REPLACEMENT_AVATAR_DEFAULT);
if ((_avatarWhitelist.count() == 1) && _avatarWhitelist[0].isEmpty()) {
_avatarWhitelist.clear(); // KeepEmptyParts above will parse "," as ["", ""] (which is ok), but "" as [""] (which is not ok).
}
if (_avatarWhitelist.isEmpty()) {
qCDebug(avatars) << "All avatars are allowed.";
} else {
qCDebug(avatars) << "Avatars other than" << _avatarWhitelist << "will be replaced by" << (_replacementAvatar.isEmpty() ? "default" : _replacementAvatar);
}
}

View file

@ -42,10 +42,12 @@ private slots:
void handleAdjustAvatarSorting(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleViewFrustumPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleAvatarIdentityPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleKillAvatarPacket(QSharedPointer<ReceivedMessage> message);
void handleKillAvatarPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleNodeIgnoreRequestPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleRadiusIgnoreRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void handleRequestsDomainListDataPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleReplicatedPacket(QSharedPointer<ReceivedMessage> message);
void handleReplicatedBulkAvatarPacket(QSharedPointer<ReceivedMessage> message);
void domainSettingsRequestComplete();
void handlePacketVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID);
void start();
@ -59,7 +61,14 @@ private:
void parseDomainServerSettings(const QJsonObject& domainSettings);
void sendIdentityPacket(AvatarMixerClientData* nodeData, const SharedNodePointer& destinationNode);
void manageDisplayName(const SharedNodePointer& node);
void manageIdentityData(const SharedNodePointer& node);
bool isAvatarInWhitelist(const QUrl& url);
const QString REPLACEMENT_AVATAR_DEFAULT{ "" };
QStringList _avatarWhitelist { };
QString _replacementAvatar { REPLACEMENT_AVATAR_DEFAULT };
void optionallyReplicatePacket(ReceivedMessage& message, const Node& node);
p_high_resolution_clock::time_point _lastFrameTimestamp;

View file

@ -65,6 +65,8 @@ public:
void flagIdentityChange() { _identityChangeTimestamp = usecTimestampNow(); }
bool getAvatarSessionDisplayNameMustChange() const { return _avatarSessionDisplayNameMustChange; }
void setAvatarSessionDisplayNameMustChange(bool set = true) { _avatarSessionDisplayNameMustChange = set; }
bool getAvatarSkeletonModelUrlMustChange() const { return _avatarSkeletonModelUrlMustChange; }
void setAvatarSkeletonModelUrlMustChange(bool set = true) { _avatarSkeletonModelUrlMustChange = set; }
void resetNumAvatarsSentLastFrame() { _numAvatarsSentLastFrame = 0; }
void incrementNumAvatarsSentLastFrame() { ++_numAvatarsSentLastFrame; }
@ -146,6 +148,7 @@ private:
uint64_t _identityChangeTimestamp;
bool _avatarSessionDisplayNameMustChange{ true };
bool _avatarSkeletonModelUrlMustChange{ false };
int _numAvatarsSentLastFrame = 0;
int _numFramesSinceAdjustment = 0;

View file

@ -65,15 +65,32 @@ void AvatarMixerSlave::processIncomingPackets(const SharedNodePointer& node) {
_stats.processIncomingPacketsElapsedTime += (end - start);
}
int AvatarMixerSlave::sendIdentityPacket(const AvatarMixerClientData* nodeData, const SharedNodePointer& destinationNode) {
QByteArray individualData = nodeData->getConstAvatarData()->identityByteArray();
individualData.replace(0, NUM_BYTES_RFC4122_UUID, nodeData->getNodeID().toRfc4122()); // FIXME, this looks suspicious
auto identityPackets = NLPacketList::create(PacketType::AvatarIdentity, QByteArray(), true, true);
identityPackets->write(individualData);
DependencyManager::get<NodeList>()->sendPacketList(std::move(identityPackets), *destinationNode);
_stats.numIdentityPackets++;
return individualData.size();
if (destinationNode->getType() == NodeType::Agent && !destinationNode->isUpstream()) {
QByteArray individualData = nodeData->getConstAvatarData()->identityByteArray();
individualData.replace(0, NUM_BYTES_RFC4122_UUID, nodeData->getNodeID().toRfc4122()); // FIXME, this looks suspicious
auto identityPackets = NLPacketList::create(PacketType::AvatarIdentity, QByteArray(), true, true);
identityPackets->write(individualData);
DependencyManager::get<NodeList>()->sendPacketList(std::move(identityPackets), *destinationNode);
_stats.numIdentityPackets++;
return individualData.size();
} else {
return 0;
}
}
int AvatarMixerSlave::sendReplicatedIdentityPacket(const AvatarMixerClientData* nodeData, const SharedNodePointer& destinationNode) {
if (destinationNode->getType() == NodeType::DownstreamAvatarMixer) {
QByteArray individualData = nodeData->getConstAvatarData()->identityByteArray();
individualData.replace(0, NUM_BYTES_RFC4122_UUID, nodeData->getNodeID().toRfc4122()); // FIXME, this looks suspicious
auto identityPacket = NLPacket::create(PacketType::ReplicatedAvatarIdentity);
identityPacket->write(individualData);
DependencyManager::get<NodeList>()->sendUnreliablePacket(*identityPacket, *destinationNode);
_stats.numIdentityPackets++;
return individualData.size();
} else {
return 0;
}
}
static const int AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND = 45;
@ -81,6 +98,18 @@ static const int AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND = 45;
void AvatarMixerSlave::broadcastAvatarData(const SharedNodePointer& node) {
quint64 start = usecTimestampNow();
if (node->getType() == NodeType::Agent && node->getLinkedData() && node->getActiveSocket() && !node->isUpstream()) {
broadcastAvatarDataToAgent(node);
} else if (node->getType() == NodeType::DownstreamAvatarMixer) {
broadcastAvatarDataToDownstreamMixer(node);
}
quint64 end = usecTimestampNow();
_stats.jobElapsedTime += (end - start);
}
void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) {
auto nodeList = DependencyManager::get<NodeList>();
// setup for distributed random floating point values
@ -88,308 +117,432 @@ void AvatarMixerSlave::broadcastAvatarData(const SharedNodePointer& node) {
std::mt19937 generator(randomDevice());
std::uniform_real_distribution<float> distribution;
if (node->getLinkedData() && (node->getType() == NodeType::Agent) && node->getActiveSocket()) {
_stats.nodesBroadcastedTo++;
_stats.nodesBroadcastedTo++;
AvatarMixerClientData* nodeData = reinterpret_cast<AvatarMixerClientData*>(node->getLinkedData());
AvatarMixerClientData* nodeData = reinterpret_cast<AvatarMixerClientData*>(node->getLinkedData());
nodeData->resetInViewStats();
nodeData->resetInViewStats();
const AvatarData& avatar = nodeData->getAvatar();
glm::vec3 myPosition = avatar.getClientGlobalPosition();
const AvatarData& avatar = nodeData->getAvatar();
glm::vec3 myPosition = avatar.getClientGlobalPosition();
// reset the internal state for correct random number distribution
distribution.reset();
// reset the internal state for correct random number distribution
distribution.reset();
// reset the number of sent avatars
nodeData->resetNumAvatarsSentLastFrame();
// reset the number of sent avatars
nodeData->resetNumAvatarsSentLastFrame();
// keep a counter of the number of considered avatars
int numOtherAvatars = 0;
// keep a counter of the number of considered avatars
int numOtherAvatars = 0;
// keep track of outbound data rate specifically for avatar data
int numAvatarDataBytes = 0;
int identityBytesSent = 0;
// keep track of outbound data rate specifically for avatar data
int numAvatarDataBytes = 0;
int identityBytesSent = 0;
// max number of avatarBytes per frame
auto maxAvatarBytesPerFrame = (_maxKbpsPerNode * BYTES_PER_KILOBIT) / AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND;
// max number of avatarBytes per frame
auto maxAvatarBytesPerFrame = (_maxKbpsPerNode * BYTES_PER_KILOBIT) / AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND;
// FIXME - find a way to not send the sessionID for every avatar
int minimumBytesPerAvatar = AvatarDataPacket::AVATAR_HAS_FLAGS_SIZE + NUM_BYTES_RFC4122_UUID;
// FIXME - find a way to not send the sessionID for every avatar
int minimumBytesPerAvatar = AvatarDataPacket::AVATAR_HAS_FLAGS_SIZE + NUM_BYTES_RFC4122_UUID;
int overBudgetAvatars = 0;
int overBudgetAvatars = 0;
// keep track of the number of other avatars held back in this frame
int numAvatarsHeldBack = 0;
// keep track of the number of other avatars held back in this frame
int numAvatarsHeldBack = 0;
// keep track of the number of other avatar frames skipped
int numAvatarsWithSkippedFrames = 0;
// keep track of the number of other avatar frames skipped
int numAvatarsWithSkippedFrames = 0;
// When this is true, the AvatarMixer will send Avatar data to a client
// about avatars they've ignored or that are out of view
bool PALIsOpen = nodeData->getRequestsDomainListData();
// When this is true, the AvatarMixer will send Avatar data to a client
// about avatars they've ignored or that are out of view
bool PALIsOpen = nodeData->getRequestsDomainListData();
// When this is true, the AvatarMixer will send Avatar data to a client about avatars that have ignored them
bool getsAnyIgnored = PALIsOpen && node->getCanKick();
// When this is true, the AvatarMixer will send Avatar data to a client about avatars that have ignored them
bool getsAnyIgnored = PALIsOpen && node->getCanKick();
if (PALIsOpen) {
// Increase minimumBytesPerAvatar if the PAL is open
minimumBytesPerAvatar += sizeof(AvatarDataPacket::AvatarGlobalPosition) +
sizeof(AvatarDataPacket::AudioLoudness);
}
if (PALIsOpen) {
// Increase minimumBytesPerAvatar if the PAL is open
minimumBytesPerAvatar += sizeof(AvatarDataPacket::AvatarGlobalPosition) +
sizeof(AvatarDataPacket::AudioLoudness);
}
// setup a PacketList for the avatarPackets
auto avatarPacketList = NLPacketList::create(PacketType::BulkAvatarData);
// setup a PacketList for the avatarPackets
auto avatarPacketList = NLPacketList::create(PacketType::BulkAvatarData);
// Define the minimum bubble size
static const glm::vec3 minBubbleSize = glm::vec3(0.3f, 1.3f, 0.3f);
// Define the scale of the box for the current node
glm::vec3 nodeBoxScale = (nodeData->getPosition() - nodeData->getGlobalBoundingBoxCorner()) * 2.0f;
// Set up the bounding box for the current node
AABox nodeBox(nodeData->getGlobalBoundingBoxCorner(), nodeBoxScale);
// Clamp the size of the bounding box to a minimum scale
if (glm::any(glm::lessThan(nodeBoxScale, minBubbleSize))) {
nodeBox.setScaleStayCentered(minBubbleSize);
}
// Quadruple the scale of both bounding boxes
nodeBox.embiggen(4.0f);
// Define the minimum bubble size
static const glm::vec3 minBubbleSize = glm::vec3(0.3f, 1.3f, 0.3f);
// Define the scale of the box for the current node
glm::vec3 nodeBoxScale = (nodeData->getPosition() - nodeData->getGlobalBoundingBoxCorner()) * 2.0f;
// Set up the bounding box for the current node
AABox nodeBox(nodeData->getGlobalBoundingBoxCorner(), nodeBoxScale);
// Clamp the size of the bounding box to a minimum scale
if (glm::any(glm::lessThan(nodeBoxScale, minBubbleSize))) {
nodeBox.setScaleStayCentered(minBubbleSize);
}
// Quadruple the scale of both bounding boxes
nodeBox.embiggen(4.0f);
// setup list of AvatarData as well as maps to map betweeen the AvatarData and the original nodes
// for calling the AvatarData::sortAvatars() function and getting our sorted list of client nodes
QList<AvatarSharedPointer> avatarList;
std::unordered_map<AvatarSharedPointer, SharedNodePointer> avatarDataToNodes;
// setup list of AvatarData as well as maps to map betweeen the AvatarData and the original nodes
// for calling the AvatarData::sortAvatars() function and getting our sorted list of client nodes
QList<AvatarSharedPointer> avatarList;
std::unordered_map<AvatarSharedPointer, SharedNodePointer> avatarDataToNodes;
std::for_each(_begin, _end, [&](const SharedNodePointer& otherNode) {
std::for_each(_begin, _end, [&](const SharedNodePointer& otherNode) {
// make sure this is an agent that we have avatar data for before considering it for inclusion
if (otherNode->getType() == NodeType::Agent
&& otherNode->getLinkedData()) {
const AvatarMixerClientData* otherNodeData = reinterpret_cast<const AvatarMixerClientData*>(otherNode->getLinkedData());
// theoretically it's possible for a Node to be in the NodeList (and therefore end up here),
// but not have yet sent data that's linked to the node. Check for that case and don't
// consider those nodes.
if (otherNodeData) {
AvatarSharedPointer otherAvatar = otherNodeData->getAvatarSharedPointer();
avatarList << otherAvatar;
avatarDataToNodes[otherAvatar] = otherNode;
AvatarSharedPointer otherAvatar = otherNodeData->getAvatarSharedPointer();
avatarList << otherAvatar;
avatarDataToNodes[otherAvatar] = otherNode;
}
});
AvatarSharedPointer thisAvatar = nodeData->getAvatarSharedPointer();
ViewFrustum cameraView = nodeData->getViewFrustom();
std::priority_queue<AvatarPriority> sortedAvatars;
AvatarData::sortAvatars(avatarList, cameraView, sortedAvatars,
[&](AvatarSharedPointer avatar)->uint64_t {
auto avatarNode = avatarDataToNodes[avatar];
assert(avatarNode); // we can't have gotten here without the avatarData being a valid key in the map
return nodeData->getLastBroadcastTime(avatarNode->getUUID());
}, [&](AvatarSharedPointer avatar)->float{
glm::vec3 nodeBoxHalfScale = (avatar->getPosition() - avatar->getGlobalBoundingBoxCorner());
return glm::max(nodeBoxHalfScale.x, glm::max(nodeBoxHalfScale.y, nodeBoxHalfScale.z));
}, [&](AvatarSharedPointer avatar)->bool {
if (avatar == thisAvatar) {
return true; // ignore ourselves...
}
bool shouldIgnore = false;
// We will also ignore other nodes for a couple of different reasons:
// 1) ignore bubbles and ignore specific node
// 2) the node hasn't really updated it's frame data recently, this can
// happen if for example the avatar is connected on a desktop and sending
// updates at ~30hz. So every 3 frames we skip a frame.
auto avatarNode = avatarDataToNodes[avatar];
assert(avatarNode); // we can't have gotten here without the avatarData being a valid key in the map
const AvatarMixerClientData* avatarNodeData = reinterpret_cast<const AvatarMixerClientData*>(avatarNode->getLinkedData());
assert(avatarNodeData); // we can't have gotten here without avatarNode having valid data
quint64 startIgnoreCalculation = usecTimestampNow();
// make sure we have data for this avatar, that it isn't the same node,
// and isn't an avatar that the viewing node has ignored
// or that has ignored the viewing node
if (!avatarNode->getLinkedData()
|| avatarNode->getUUID() == node->getUUID()
|| (node->isIgnoringNodeWithID(avatarNode->getUUID()) && !PALIsOpen)
|| (avatarNode->isIgnoringNodeWithID(node->getUUID()) && !getsAnyIgnored)) {
shouldIgnore = true;
} else {
// Check to see if the space bubble is enabled
// Don't bother with these checks if the other avatar has their bubble enabled and we're gettingAnyIgnored
if (node->isIgnoreRadiusEnabled() || (avatarNode->isIgnoreRadiusEnabled() && !getsAnyIgnored)) {
// Define the scale of the box for the current other node
glm::vec3 otherNodeBoxScale = (avatarNodeData->getPosition() - avatarNodeData->getGlobalBoundingBoxCorner()) * 2.0f;
// Set up the bounding box for the current other node
AABox otherNodeBox(avatarNodeData->getGlobalBoundingBoxCorner(), otherNodeBoxScale);
// Clamp the size of the bounding box to a minimum scale
if (glm::any(glm::lessThan(otherNodeBoxScale, minBubbleSize))) {
otherNodeBox.setScaleStayCentered(minBubbleSize);
}
// Quadruple the scale of both bounding boxes
otherNodeBox.embiggen(4.0f);
// Perform the collision check between the two bounding boxes
if (nodeBox.touches(otherNodeBox)) {
nodeData->ignoreOther(node, avatarNode);
shouldIgnore = !getsAnyIgnored;
}
}
});
// Not close enough to ignore
if (!shouldIgnore) {
nodeData->removeFromRadiusIgnoringSet(node, avatarNode->getUUID());
}
}
quint64 endIgnoreCalculation = usecTimestampNow();
_stats.ignoreCalculationElapsedTime += (endIgnoreCalculation - startIgnoreCalculation);
AvatarSharedPointer thisAvatar = nodeData->getAvatarSharedPointer();
ViewFrustum cameraView = nodeData->getViewFrustom();
std::priority_queue<AvatarPriority> sortedAvatars;
AvatarData::sortAvatars(avatarList, cameraView, sortedAvatars,
if (!shouldIgnore) {
AvatarDataSequenceNumber lastSeqToReceiver = nodeData->getLastBroadcastSequenceNumber(avatarNode->getUUID());
AvatarDataSequenceNumber lastSeqFromSender = avatarNodeData->getLastReceivedSequenceNumber();
[&](AvatarSharedPointer avatar)->uint64_t{
auto avatarNode = avatarDataToNodes[avatar];
assert(avatarNode); // we can't have gotten here without the avatarData being a valid key in the map
return nodeData->getLastBroadcastTime(avatarNode->getUUID());
},
// FIXME - This code does appear to be working. But it seems brittle.
// It supports determining if the frame of data for this "other"
// avatar has already been sent to the reciever. This has been
// verified to work on a desktop display that renders at 60hz and
// therefore sends to mixer at 30hz. Each second you'd expect to
// have 15 (45hz-30hz) duplicate frames. In this case, the stat
// avg_other_av_skips_per_second does report 15.
//
// make sure we haven't already sent this data from this sender to this receiver
// or that somehow we haven't sent
if (lastSeqToReceiver == lastSeqFromSender && lastSeqToReceiver != 0) {
++numAvatarsHeldBack;
shouldIgnore = true;
} else if (lastSeqFromSender - lastSeqToReceiver > 1) {
// this is a skip - we still send the packet but capture the presence of the skip so we see it happening
++numAvatarsWithSkippedFrames;
}
}
return shouldIgnore;
});
[&](AvatarSharedPointer avatar)->float{
glm::vec3 nodeBoxHalfScale = (avatar->getPosition() - avatar->getGlobalBoundingBoxCorner());
return glm::max(nodeBoxHalfScale.x, glm::max(nodeBoxHalfScale.y, nodeBoxHalfScale.z));
},
// loop through our sorted avatars and allocate our bandwidth to them accordingly
int avatarRank = 0;
[&](AvatarSharedPointer avatar)->bool{
if (avatar == thisAvatar) {
return true; // ignore ourselves...
}
// this is overly conservative, because it includes some avatars we might not consider
int remainingAvatars = (int)sortedAvatars.size();
bool shouldIgnore = false;
while (!sortedAvatars.empty()) {
AvatarPriority sortData = sortedAvatars.top();
sortedAvatars.pop();
const auto& avatarData = sortData.avatar;
avatarRank++;
remainingAvatars--;
// We will also ignore other nodes for a couple of different reasons:
// 1) ignore bubbles and ignore specific node
// 2) the node hasn't really updated it's frame data recently, this can
// happen if for example the avatar is connected on a desktop and sending
// updates at ~30hz. So every 3 frames we skip a frame.
auto avatarNode = avatarDataToNodes[avatar];
auto otherNode = avatarDataToNodes[avatarData];
assert(otherNode); // we can't have gotten here without the avatarData being a valid key in the map
assert(avatarNode); // we can't have gotten here without the avatarData being a valid key in the map
// NOTE: Here's where we determine if we are over budget and drop to bare minimum data
int minimRemainingAvatarBytes = minimumBytesPerAvatar * remainingAvatars;
bool overBudget = (identityBytesSent + numAvatarDataBytes + minimRemainingAvatarBytes) > maxAvatarBytesPerFrame;
const AvatarMixerClientData* avatarNodeData = reinterpret_cast<const AvatarMixerClientData*>(avatarNode->getLinkedData());
assert(avatarNodeData); // we can't have gotten here without avatarNode having valid data
quint64 startIgnoreCalculation = usecTimestampNow();
quint64 startAvatarDataPacking = usecTimestampNow();
// make sure we have data for this avatar, that it isn't the same node,
// and isn't an avatar that the viewing node has ignored
// or that has ignored the viewing node
if (!avatarNode->getLinkedData()
|| avatarNode->getUUID() == node->getUUID()
|| (node->isIgnoringNodeWithID(avatarNode->getUUID()) && !PALIsOpen)
|| (avatarNode->isIgnoringNodeWithID(node->getUUID()) && !getsAnyIgnored)) {
shouldIgnore = true;
} else {
++numOtherAvatars;
// Check to see if the space bubble is enabled
// Don't bother with these checks if the other avatar has their bubble enabled and we're gettingAnyIgnored
if (node->isIgnoreRadiusEnabled() || (avatarNode->isIgnoreRadiusEnabled() && !getsAnyIgnored)) {
const AvatarMixerClientData* otherNodeData = reinterpret_cast<const AvatarMixerClientData*>(otherNode->getLinkedData());
// Define the scale of the box for the current other node
glm::vec3 otherNodeBoxScale = (avatarNodeData->getPosition() - avatarNodeData->getGlobalBoundingBoxCorner()) * 2.0f;
// Set up the bounding box for the current other node
AABox otherNodeBox(avatarNodeData->getGlobalBoundingBoxCorner(), otherNodeBoxScale);
// Clamp the size of the bounding box to a minimum scale
if (glm::any(glm::lessThan(otherNodeBoxScale, minBubbleSize))) {
otherNodeBox.setScaleStayCentered(minBubbleSize);
}
// Quadruple the scale of both bounding boxes
otherNodeBox.embiggen(4.0f);
// If the time that the mixer sent AVATAR DATA about Avatar B to Avatar A is BEFORE OR EQUAL TO
// the time that Avatar B flagged an IDENTITY DATA change, send IDENTITY DATA about Avatar B to Avatar A.
if (nodeData->getLastBroadcastTime(otherNode->getUUID()) <= otherNodeData->getIdentityChangeTimestamp()) {
identityBytesSent += sendIdentityPacket(otherNodeData, node);
}
// Perform the collision check between the two bounding boxes
if (nodeBox.touches(otherNodeBox)) {
nodeData->ignoreOther(node, avatarNode);
shouldIgnore = !getsAnyIgnored;
}
}
// Not close enough to ignore
if (!shouldIgnore) {
nodeData->removeFromRadiusIgnoringSet(node, avatarNode->getUUID());
}
}
quint64 endIgnoreCalculation = usecTimestampNow();
_stats.ignoreCalculationElapsedTime += (endIgnoreCalculation - startIgnoreCalculation);
const AvatarData* otherAvatar = otherNodeData->getConstAvatarData();
glm::vec3 otherPosition = otherAvatar->getClientGlobalPosition();
if (!shouldIgnore) {
AvatarDataSequenceNumber lastSeqToReceiver = nodeData->getLastBroadcastSequenceNumber(avatarNode->getUUID());
AvatarDataSequenceNumber lastSeqFromSender = avatarNodeData->getLastReceivedSequenceNumber();
// determine if avatar is in view, to determine how much data to include...
glm::vec3 otherNodeBoxScale = (otherPosition - otherNodeData->getGlobalBoundingBoxCorner()) * 2.0f;
AABox otherNodeBox(otherNodeData->getGlobalBoundingBoxCorner(), otherNodeBoxScale);
bool isInView = nodeData->otherAvatarInView(otherNodeBox);
// FIXME - This code does appear to be working. But it seems brittle.
// It supports determining if the frame of data for this "other"
// avatar has already been sent to the reciever. This has been
// verified to work on a desktop display that renders at 60hz and
// therefore sends to mixer at 30hz. Each second you'd expect to
// have 15 (45hz-30hz) duplicate frames. In this case, the stat
// avg_other_av_skips_per_second does report 15.
//
// make sure we haven't already sent this data from this sender to this receiver
// or that somehow we haven't sent
if (lastSeqToReceiver == lastSeqFromSender && lastSeqToReceiver != 0) {
++numAvatarsHeldBack;
shouldIgnore = true;
} else if (lastSeqFromSender - lastSeqToReceiver > 1) {
// this is a skip - we still send the packet but capture the presence of the skip so we see it happening
++numAvatarsWithSkippedFrames;
}
}
return shouldIgnore;
});
// start a new segment in the PacketList for this avatar
avatarPacketList->startSegment();
// loop through our sorted avatars and allocate our bandwidth to them accordingly
int avatarRank = 0;
AvatarData::AvatarDataDetail detail;
// this is overly conservative, because it includes some avatars we might not consider
int remainingAvatars = (int)sortedAvatars.size();
if (overBudget) {
overBudgetAvatars++;
_stats.overBudgetAvatars++;
detail = PALIsOpen ? AvatarData::PALMinimum : AvatarData::NoData;
} else if (!isInView) {
detail = PALIsOpen ? AvatarData::PALMinimum : AvatarData::MinimumData;
nodeData->incrementAvatarOutOfView();
} else {
detail = distribution(generator) < AVATAR_SEND_FULL_UPDATE_RATIO
? AvatarData::SendAllData : AvatarData::CullSmallData;
nodeData->incrementAvatarInView();
}
while (!sortedAvatars.empty()) {
AvatarPriority sortData = sortedAvatars.top();
sortedAvatars.pop();
const auto& avatarData = sortData.avatar;
avatarRank++;
remainingAvatars--;
bool includeThisAvatar = true;
auto lastEncodeForOther = nodeData->getLastOtherAvatarEncodeTime(otherNode->getUUID());
QVector<JointData>& lastSentJointsForOther = nodeData->getLastOtherAvatarSentJoints(otherNode->getUUID());
bool distanceAdjust = true;
glm::vec3 viewerPosition = myPosition;
AvatarDataPacket::HasFlags hasFlagsOut; // the result of the toByteArray
bool dropFaceTracking = false;
auto otherNode = avatarDataToNodes[avatarData];
assert(otherNode); // we can't have gotten here without the avatarData being a valid key in the map
quint64 start = usecTimestampNow();
QByteArray bytes = otherAvatar->toByteArray(detail, lastEncodeForOther, lastSentJointsForOther,
hasFlagsOut, dropFaceTracking, distanceAdjust, viewerPosition, &lastSentJointsForOther);
quint64 end = usecTimestampNow();
_stats.toByteArrayElapsedTime += (end - start);
// NOTE: Here's where we determine if we are over budget and drop to bare minimum data
int minimRemainingAvatarBytes = minimumBytesPerAvatar * remainingAvatars;
bool overBudget = (identityBytesSent + numAvatarDataBytes + minimRemainingAvatarBytes) > maxAvatarBytesPerFrame;
static const int MAX_ALLOWED_AVATAR_DATA = (1400 - NUM_BYTES_RFC4122_UUID);
if (bytes.size() > MAX_ALLOWED_AVATAR_DATA) {
qCWarning(avatars) << "otherAvatar.toByteArray() resulted in very large buffer:" << bytes.size() << "... attempt to drop facial data";
dropFaceTracking = true; // first try dropping the facial data
bytes = otherAvatar->toByteArray(detail, lastEncodeForOther, lastSentJointsForOther,
hasFlagsOut, dropFaceTracking, distanceAdjust, viewerPosition, &lastSentJointsForOther);
if (bytes.size() > MAX_ALLOWED_AVATAR_DATA) {
qCWarning(avatars) << "otherAvatar.toByteArray() without facial data resulted in very large buffer:" << bytes.size() << "... reduce to MinimumData";
bytes = otherAvatar->toByteArray(AvatarData::MinimumData, lastEncodeForOther, lastSentJointsForOther,
hasFlagsOut, dropFaceTracking, distanceAdjust, viewerPosition, &lastSentJointsForOther);
}
if (bytes.size() > MAX_ALLOWED_AVATAR_DATA) {
qCWarning(avatars) << "otherAvatar.toByteArray() MinimumData resulted in very large buffer:" << bytes.size() << "... FAIL!!";
includeThisAvatar = false;
}
}
if (includeThisAvatar) {
numAvatarDataBytes += avatarPacketList->write(otherNode->getUUID().toRfc4122());
numAvatarDataBytes += avatarPacketList->write(bytes);
if (detail != AvatarData::NoData) {
_stats.numOthersIncluded++;
// increment the number of avatars sent to this reciever
nodeData->incrementNumAvatarsSentLastFrame();
// set the last sent sequence number for this sender on the receiver
nodeData->setLastBroadcastSequenceNumber(otherNode->getUUID(),
otherNodeData->getLastReceivedSequenceNumber());
// remember the last time we sent details about this other node to the receiver
nodeData->setLastBroadcastTime(otherNode->getUUID(), start);
}
}
avatarPacketList->endSegment();
quint64 endAvatarDataPacking = usecTimestampNow();
_stats.avatarDataPackingElapsedTime += (endAvatarDataPacking - startAvatarDataPacking);
};
quint64 startPacketSending = usecTimestampNow();
// close the current packet so that we're always sending something
avatarPacketList->closeCurrentPacket(true);
_stats.numPacketsSent += (int)avatarPacketList->getNumPackets();
_stats.numBytesSent += numAvatarDataBytes;
// send the avatar data PacketList
nodeList->sendPacketList(std::move(avatarPacketList), *node);
// record the bytes sent for other avatar data in the AvatarMixerClientData
nodeData->recordSentAvatarData(numAvatarDataBytes);
// record the number of avatars held back this frame
nodeData->recordNumOtherAvatarStarves(numAvatarsHeldBack);
nodeData->recordNumOtherAvatarSkips(numAvatarsWithSkippedFrames);
quint64 endPacketSending = usecTimestampNow();
_stats.packetSendingElapsedTime += (endPacketSending - startPacketSending);
}
uint64_t REBROADCAST_IDENTITY_TO_DOWNSTREAM_EVERY_US = 5 * 1000 * 1000;
void AvatarMixerSlave::broadcastAvatarDataToDownstreamMixer(const SharedNodePointer& node) {
_stats.downstreamMixersBroadcastedTo++;
AvatarMixerClientData* nodeData = reinterpret_cast<AvatarMixerClientData*>(node->getLinkedData());
if (!nodeData) {
return;
}
// setup a PacketList for the replicated bulk avatar data
auto avatarPacketList = NLPacketList::create(PacketType::ReplicatedBulkAvatarData);
int numAvatarDataBytes = 0;
// reset the number of sent avatars
nodeData->resetNumAvatarsSentLastFrame();
std::for_each(_begin, _end, [&](const SharedNodePointer& agentNode) {
// collect agents that we have avatar data for that we are supposed to replicate
if (agentNode->getType() == NodeType::Agent && agentNode->getLinkedData() && agentNode->isReplicated()) {
const AvatarMixerClientData* agentNodeData = reinterpret_cast<const AvatarMixerClientData*>(agentNode->getLinkedData());
AvatarSharedPointer otherAvatar = agentNodeData->getAvatarSharedPointer();
quint64 startAvatarDataPacking = usecTimestampNow();
++numOtherAvatars;
const AvatarMixerClientData* otherNodeData = reinterpret_cast<const AvatarMixerClientData*>(otherNode->getLinkedData());
// If the time that the mixer sent AVATAR DATA about Avatar B to Avatar A is BEFORE OR EQUAL TO
// the time that Avatar B flagged an IDENTITY DATA change, send IDENTITY DATA about Avatar B to Avatar A.
if (nodeData->getLastBroadcastTime(otherNode->getUUID()) <= otherNodeData->getIdentityChangeTimestamp()) {
identityBytesSent += sendIdentityPacket(otherNodeData, node);
}
const AvatarData* otherAvatar = otherNodeData->getConstAvatarData();
glm::vec3 otherPosition = otherAvatar->getClientGlobalPosition();
// determine if avatar is in view, to determine how much data to include...
glm::vec3 otherNodeBoxScale = (otherPosition - otherNodeData->getGlobalBoundingBoxCorner()) * 2.0f;
AABox otherNodeBox(otherNodeData->getGlobalBoundingBoxCorner(), otherNodeBoxScale);
bool isInView = nodeData->otherAvatarInView(otherNodeBox);
// start a new segment in the PacketList for this avatar
avatarPacketList->startSegment();
AvatarData::AvatarDataDetail detail;
if (overBudget) {
overBudgetAvatars++;
_stats.overBudgetAvatars++;
detail = PALIsOpen ? AvatarData::PALMinimum : AvatarData::NoData;
} else if (!isInView) {
detail = PALIsOpen ? AvatarData::PALMinimum : AvatarData::MinimumData;
nodeData->incrementAvatarOutOfView();
} else {
detail = distribution(generator) < AVATAR_SEND_FULL_UPDATE_RATIO
? AvatarData::SendAllData : AvatarData::CullSmallData;
nodeData->incrementAvatarInView();
}
bool includeThisAvatar = true;
auto lastEncodeForOther = nodeData->getLastOtherAvatarEncodeTime(otherNode->getUUID());
QVector<JointData>& lastSentJointsForOther = nodeData->getLastOtherAvatarSentJoints(otherNode->getUUID());
bool distanceAdjust = true;
glm::vec3 viewerPosition = myPosition;
AvatarDataPacket::HasFlags hasFlagsOut; // the result of the toByteArray
bool dropFaceTracking = false;
// we cannot send a downstream avatar mixer any updates that expect them to have previous state for this avatar
// since we have no idea if they're online and receiving our packets
// so we always send a full update for this avatar
quint64 start = usecTimestampNow();
QByteArray bytes = otherAvatar->toByteArray(detail, lastEncodeForOther, lastSentJointsForOther,
hasFlagsOut, dropFaceTracking, distanceAdjust, viewerPosition, &lastSentJointsForOther);
AvatarDataPacket::HasFlags flagsOut;
QVector<JointData> emptyLastJointSendData { otherAvatar->getJointCount() };
QByteArray avatarByteArray = otherAvatar->toByteArray(AvatarData::SendAllData, 0, emptyLastJointSendData,
flagsOut, false, false, glm::vec3(0), nullptr);
quint64 end = usecTimestampNow();
_stats.toByteArrayElapsedTime += (end - start);
static const int MAX_ALLOWED_AVATAR_DATA = (1400 - NUM_BYTES_RFC4122_UUID);
if (bytes.size() > MAX_ALLOWED_AVATAR_DATA) {
qCWarning(avatars) << "otherAvatar.toByteArray() resulted in very large buffer:" << bytes.size() << "... attempt to drop facial data";
auto lastBroadcastTime = nodeData->getLastBroadcastTime(agentNode->getUUID());
if (lastBroadcastTime <= agentNodeData->getIdentityChangeTimestamp()
|| (start - lastBroadcastTime) >= REBROADCAST_IDENTITY_TO_DOWNSTREAM_EVERY_US) {
sendReplicatedIdentityPacket(agentNodeData, node);
nodeData->setLastBroadcastTime(agentNode->getUUID(), start);
}
dropFaceTracking = true; // first try dropping the facial data
bytes = otherAvatar->toByteArray(detail, lastEncodeForOther, lastSentJointsForOther,
hasFlagsOut, dropFaceTracking, distanceAdjust, viewerPosition, &lastSentJointsForOther);
// figure out how large our avatar byte array can be to fit in the packet list
// given that we need it and the avatar UUID and the size of the byte array (16 bit)
// to fit in a segment of the packet list
auto maxAvatarByteArraySize = avatarPacketList->getMaxSegmentSize();
maxAvatarByteArraySize -= NUM_BYTES_RFC4122_UUID;
maxAvatarByteArraySize -= sizeof(quint16);
if (bytes.size() > MAX_ALLOWED_AVATAR_DATA) {
qCWarning(avatars) << "otherAvatar.toByteArray() without facial data resulted in very large buffer:" << bytes.size() << "... reduce to MinimumData";
bytes = otherAvatar->toByteArray(AvatarData::MinimumData, lastEncodeForOther, lastSentJointsForOther,
hasFlagsOut, dropFaceTracking, distanceAdjust, viewerPosition, &lastSentJointsForOther);
}
auto sequenceNumberSize = sizeof(agentNodeData->getLastReceivedSequenceNumber());
maxAvatarByteArraySize -= sequenceNumberSize;
if (bytes.size() > MAX_ALLOWED_AVATAR_DATA) {
qCWarning(avatars) << "otherAvatar.toByteArray() MinimumData resulted in very large buffer:" << bytes.size() << "... FAIL!!";
includeThisAvatar = false;
if (avatarByteArray.size() > maxAvatarByteArraySize) {
qCWarning(avatars) << "Replicated avatar data too large for" << otherAvatar->getSessionUUID()
<< "-" << avatarByteArray.size() << "bytes";
avatarByteArray = otherAvatar->toByteArray(AvatarData::SendAllData, 0, emptyLastJointSendData,
flagsOut, true, false, glm::vec3(0), nullptr);
if (avatarByteArray.size() > maxAvatarByteArraySize) {
qCWarning(avatars) << "Replicated avatar data without facial data still too large for"
<< otherAvatar->getSessionUUID() << "-" << avatarByteArray.size() << "bytes";
avatarByteArray = otherAvatar->toByteArray(AvatarData::MinimumData, 0, emptyLastJointSendData,
flagsOut, true, false, glm::vec3(0), nullptr);
}
}
if (includeThisAvatar) {
numAvatarDataBytes += avatarPacketList->write(otherNode->getUUID().toRfc4122());
numAvatarDataBytes += avatarPacketList->write(bytes);
if (avatarByteArray.size() <= maxAvatarByteArraySize) {
// increment the number of avatars sent to this reciever
nodeData->incrementNumAvatarsSentLastFrame();
if (detail != AvatarData::NoData) {
_stats.numOthersIncluded++;
// set the last sent sequence number for this sender on the receiver
nodeData->setLastBroadcastSequenceNumber(agentNode->getUUID(),
agentNodeData->getLastReceivedSequenceNumber());
// increment the number of avatars sent to this reciever
nodeData->incrementNumAvatarsSentLastFrame();
// increment the number of avatars sent to this reciever
nodeData->incrementNumAvatarsSentLastFrame();
// set the last sent sequence number for this sender on the receiver
nodeData->setLastBroadcastSequenceNumber(otherNode->getUUID(),
otherNodeData->getLastReceivedSequenceNumber());
// start a new segment in the packet list for this avatar
avatarPacketList->startSegment();
// remember the last time we sent details about this other node to the receiver
nodeData->setLastBroadcastTime(otherNode->getUUID(), start);
}
// write the node's UUID, the size of the replicated avatar data,
// the sequence number of the replicated avatar data, and the replicated avatar data
numAvatarDataBytes += avatarPacketList->write(agentNode->getUUID().toRfc4122());
numAvatarDataBytes += avatarPacketList->writePrimitive((quint16) (avatarByteArray.size() + sequenceNumberSize));
numAvatarDataBytes += avatarPacketList->writePrimitive(agentNodeData->getLastReceivedSequenceNumber());
numAvatarDataBytes += avatarPacketList->write(avatarByteArray);
avatarPacketList->endSegment();
} else {
qCWarning(avatars) << "Could not fit minimum data avatar for" << otherAvatar->getSessionUUID()
<< "to packet list -" << avatarByteArray.size() << "bytes";
}
avatarPacketList->endSegment();
quint64 endAvatarDataPacking = usecTimestampNow();
_stats.avatarDataPackingElapsedTime += (endAvatarDataPacking - startAvatarDataPacking);
};
}
});
if (avatarPacketList->getNumPackets() > 0) {
quint64 startPacketSending = usecTimestampNow();
// close the current packet so that we're always sending something
@ -398,21 +551,15 @@ void AvatarMixerSlave::broadcastAvatarData(const SharedNodePointer& node) {
_stats.numPacketsSent += (int)avatarPacketList->getNumPackets();
_stats.numBytesSent += numAvatarDataBytes;
// send the avatar data PacketList
nodeList->sendPacketList(std::move(avatarPacketList), *node);
// send the replicated bulk avatar data
auto nodeList = DependencyManager::get<NodeList>();
nodeList->sendPacketList(std::move(avatarPacketList), node->getPublicSocket());
// record the bytes sent for other avatar data in the AvatarMixerClientData
nodeData->recordSentAvatarData(numAvatarDataBytes);
// record the number of avatars held back this frame
nodeData->recordNumOtherAvatarStarves(numAvatarsHeldBack);
nodeData->recordNumOtherAvatarSkips(numAvatarsWithSkippedFrames);
quint64 endPacketSending = usecTimestampNow();
_stats.packetSendingElapsedTime += (endPacketSending - startPacketSending);
}
quint64 end = usecTimestampNow();
_stats.jobElapsedTime += (end - start);
}

View file

@ -21,6 +21,7 @@ public:
quint64 processIncomingPacketsElapsedTime { 0 };
int nodesBroadcastedTo { 0 };
int downstreamMixersBroadcastedTo { 0 };
int numPacketsSent { 0 };
int numBytesSent { 0 };
int numIdentityPackets { 0 };
@ -41,6 +42,7 @@ public:
// sending job stats
nodesBroadcastedTo = 0;
downstreamMixersBroadcastedTo = 0;
numPacketsSent = 0;
numBytesSent = 0;
numIdentityPackets = 0;
@ -60,6 +62,7 @@ public:
processIncomingPacketsElapsedTime += rhs.processIncomingPacketsElapsedTime;
nodesBroadcastedTo += rhs.nodesBroadcastedTo;
downstreamMixersBroadcastedTo += rhs.downstreamMixersBroadcastedTo;
numPacketsSent += rhs.numPacketsSent;
numBytesSent += rhs.numBytesSent;
numIdentityPackets += rhs.numIdentityPackets;
@ -92,6 +95,10 @@ public:
private:
int sendIdentityPacket(const AvatarMixerClientData* nodeData, const SharedNodePointer& destinationNode);
int sendReplicatedIdentityPacket(const AvatarMixerClientData* nodeData, const SharedNodePointer& destinationNode);
void broadcastAvatarDataToAgent(const SharedNodePointer& node);
void broadcastAvatarDataToDownstreamMixer(const SharedNodePointer& node);
// frame state
ConstIter _begin;

View file

@ -76,8 +76,8 @@ void AvatarMixerSlavePool::processIncomingPackets(ConstIter begin, ConstIter end
}
void AvatarMixerSlavePool::broadcastAvatarData(ConstIter begin, ConstIter end,
p_high_resolution_clock::time_point lastFrameTimestamp,
float maxKbpsPerNode, float throttlingRatio) {
p_high_resolution_clock::time_point lastFrameTimestamp,
float maxKbpsPerNode, float throttlingRatio) {
_function = &AvatarMixerSlave::broadcastAvatarData;
_configure = [&](AvatarMixerSlave& slave) {
slave.configureBroadcast(begin, end, lastFrameTimestamp, maxKbpsPerNode, throttlingRatio);

View file

@ -16,10 +16,9 @@
#include <mutex>
#include <vector>
#include <tbb/concurrent_queue.h>
#include <QThread>
#include <TBBHelpers.h>
#include <NodeList.h>
#include "AvatarMixerSlave.h"

View file

@ -16,9 +16,9 @@
#include <SharedUtil.h>
#include <Octree.h>
#include <OctreePacketData.h>
#include <OctreeHeadlessViewer.h>
#include <ViewFrustum.h>
#include "../octree/OctreeHeadlessViewer.h"
#include "EntityTree.h"
class EntitySimulation;

View file

@ -9,17 +9,14 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <NodeList.h>
#include "OctreeLogging.h"
#include "OctreeHeadlessViewer.h"
OctreeHeadlessViewer::OctreeHeadlessViewer() : OctreeRenderer() {
_viewFrustum.setProjection(glm::perspective(glm::radians(DEFAULT_FIELD_OF_VIEW_DEGREES), DEFAULT_ASPECT_RATIO, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP));
}
#include <NodeList.h>
#include <OctreeLogging.h>
void OctreeHeadlessViewer::init() {
OctreeRenderer::init();
OctreeHeadlessViewer::OctreeHeadlessViewer() {
_viewFrustum.setProjection(glm::perspective(glm::radians(DEFAULT_FIELD_OF_VIEW_DEGREES), DEFAULT_ASPECT_RATIO, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP));
}
void OctreeHeadlessViewer::queryOctree() {

View file

@ -12,28 +12,17 @@
#ifndef hifi_OctreeHeadlessViewer_h
#define hifi_OctreeHeadlessViewer_h
#include <udt/PacketHeaders.h>
#include <SharedUtil.h>
#include <ViewFrustum.h>
#include <OctreeProcessor.h>
#include <JurisdictionListener.h>
#include <OctreeQuery.h>
#include "JurisdictionListener.h"
#include "Octree.h"
#include "OctreeConstants.h"
#include "OctreeQuery.h"
#include "OctreeRenderer.h"
#include "OctreeSceneStats.h"
#include "Octree.h"
// Generic client side Octree renderer class.
class OctreeHeadlessViewer : public OctreeRenderer {
class OctreeHeadlessViewer : public OctreeProcessor {
Q_OBJECT
public:
OctreeHeadlessViewer();
virtual ~OctreeHeadlessViewer() {};
virtual void renderElement(OctreeElementPointer element, RenderArgs* args) override { /* swallow these */ }
virtual void init() override ;
virtual void render(RenderArgs* renderArgs) override { /* swallow these */ }
void setJurisdictionListener(JurisdictionListener* jurisdictionListener) { _jurisdictionListener = jurisdictionListener; }
@ -71,6 +60,7 @@ private:
JurisdictionListener* _jurisdictionListener = nullptr;
OctreeQuery _octreeQuery;
ViewFrustum _viewFrustum;
float _voxelSizeScale { DEFAULT_OCTREE_SIZE_SCALE };
int _boundaryLevelAdjust { 0 };
int _maxPacketsPerSecond { DEFAULT_MAX_OCTREE_PPS };

View file

@ -47,7 +47,7 @@ void OctreeInboundPacketProcessor::resetStats() {
_singleSenderStats.clear();
}
unsigned long OctreeInboundPacketProcessor::getMaxWait() const {
uint32_t OctreeInboundPacketProcessor::getMaxWait() const {
// calculate time until next sendNackPackets()
quint64 nextNackTime = _lastNackTime + TOO_LONG_SINCE_LAST_NACK;
quint64 now = usecTimestampNow();

View file

@ -80,7 +80,7 @@ protected:
virtual void processPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) override;
virtual unsigned long getMaxWait() const override;
virtual uint32_t getMaxWait() const override;
virtual void preProcess() override;
virtual void midProcess() override;

View file

@ -236,11 +236,7 @@ void EntityScriptServer::run() {
// Setup MessagesClient
auto messagesClient = DependencyManager::set<MessagesClient>();
QThread* messagesThread = new QThread;
messagesThread->setObjectName("Messages Client Thread");
messagesClient->moveToThread(messagesThread);
connect(messagesThread, &QThread::started, messagesClient.data(), &MessagesClient::init);
messagesThread->start();
messagesClient->startThread();
DomainHandler& domainHandler = DependencyManager::get<NodeList>()->getDomainHandler();
connect(&domainHandler, &DomainHandler::settingsReceived, this, &EntityScriptServer::handleSettings);

View file

@ -19,10 +19,10 @@
#include <QtCore/QUuid>
#include <EntityEditPacketSender.h>
#include <EntityTreeHeadlessViewer.h>
#include <plugins/CodecPlugin.h>
#include <ScriptEngine.h>
#include <ThreadedAssignment.h>
#include "../entities/EntityTreeHeadlessViewer.h"
class EntityScriptServer : public ThreadedAssignment {
Q_OBJECT

View file

@ -8,8 +8,8 @@ string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER)
if (WIN32)
ExternalProject_Add(
${EXTERNAL_NAME}
URL http://s3.amazonaws.com/hifi-public/dependencies/nvtt-win-2.1.0.zip
URL_MD5 3ea6eeadbcc69071acf9c49ba565760e
URL http://s3.amazonaws.com/hifi-public/dependencies/nvtt-win-2.1.0.hifi.zip
URL_MD5 10da01cf601f88f6dc12a6bc13c89136
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND ""
@ -29,8 +29,8 @@ else ()
ExternalProject_Add(
${EXTERNAL_NAME}
URL http://hifi-public.s3.amazonaws.com/dependencies/nvidia-texture-tools-2.1.0.zip
URL_MD5 81b8fa6a9ee3f986088eb6e2215d6a57
URL http://hifi-public.s3.amazonaws.com/dependencies/nvidia-texture-tools-2.1.0.hifi.zip
URL_MD5 5794b950f8b265a9a41b2839b3bf7ebb
CONFIGURE_COMMAND CMAKE_ARGS ${ANDROID_CMAKE_ARGS} -DNVTT_SHARED=1 -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} -DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR>
LOG_DOWNLOAD 1
LOG_CONFIGURE 1

View file

@ -12,12 +12,19 @@ elseif ($ENV{QT_CMAKE_PREFIX_PATH})
set(QT_CMAKE_PREFIX_PATH $ENV{QT_CMAKE_PREFIX_PATH})
endif ()
set(QUAZIP_CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR> -DCMAKE_PREFIX_PATH=${QT_CMAKE_PREFIX_PATH} -DCMAKE_INSTALL_NAME_DIR:PATH=<INSTALL_DIR>/lib -DZLIB_ROOT=${ZLIB_ROOT} -DCMAKE_POSITION_INDEPENDENT_CODE=ON)
if (APPLE)
else ()
set(QUAZIP_CMAKE_ARGS ${QUAZIP_CMAKE_ARGS} -DCMAKE_CXX_STANDARD=11)
endif ()
ExternalProject_Add(
${EXTERNAL_NAME}
URL https://s3-us-west-1.amazonaws.com/hifi-production/dependencies/quazip-0.7.2.zip
URL_MD5 2955176048a31262c09259ca8d309d19
BINARY_DIR ${EXTERNAL_PROJECT_PREFIX}/build
CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR> -DCMAKE_PREFIX_PATH=${QT_CMAKE_PREFIX_PATH} -DCMAKE_INSTALL_NAME_DIR:PATH=<INSTALL_DIR>/lib -DZLIB_ROOT=${ZLIB_ROOT} -DCMAKE_POSITION_INDEPENDENT_CODE=ON
CMAKE_ARGS ${QUAZIP_CMAKE_ARGS}
LOG_DOWNLOAD 1
LOG_CONFIGURE 1
LOG_BUILD 1

View file

@ -7,16 +7,18 @@
# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
#
macro(LINK_HIFI_LIBRARIES)
function(LINK_HIFI_LIBRARIES)
file(RELATIVE_PATH RELATIVE_LIBRARY_DIR_PATH ${CMAKE_CURRENT_SOURCE_DIR} "${HIFI_LIBRARY_DIR}")
set(LIBRARIES_TO_LINK ${ARGN})
foreach(HIFI_LIBRARY ${LIBRARIES_TO_LINK})
foreach(HIFI_LIBRARY ${LIBRARIES_TO_LINK})
if (NOT TARGET ${HIFI_LIBRARY})
add_subdirectory("${RELATIVE_LIBRARY_DIR_PATH}/${HIFI_LIBRARY}" "${RELATIVE_LIBRARY_DIR_PATH}/${HIFI_LIBRARY}")
endif ()
endforeach()
foreach(HIFI_LIBRARY ${LIBRARIES_TO_LINK})
include_directories("${HIFI_LIBRARY_DIR}/${HIFI_LIBRARY}/src")
include_directories("${CMAKE_BINARY_DIR}/libraries/${HIFI_LIBRARY}/shaders")
@ -29,4 +31,4 @@ macro(LINK_HIFI_LIBRARIES)
setup_memory_debugger()
endmacro(LINK_HIFI_LIBRARIES)
endfunction()

View file

@ -147,6 +147,7 @@
{
"name": "security",
"label": "Security",
"restart": false,
"settings": [
{
"name": "http_username",
@ -866,6 +867,22 @@
"help": "Limits the scale of avatars in your domain. Cannot be greater than 1000.",
"placeholder": 3.0,
"default": 3.0
},
{
"name": "avatar_whitelist",
"label": "Avatars Allowed from:",
"help": "Comma separated list of URLs (with optional paths) that avatar .fst files are allowed from. If someone attempts to use an avatar with a different domain, it will be rejected and the replacement avatar will be used. If left blank, any domain is allowed.",
"placeholder": "",
"default": "",
"advanced": true
},
{
"name": "replacement_avatar",
"label": "Replacement Avatar for disallowed avatars",
"help": "A URL for an avatar .fst to be used when someone tries to use an avatar that is not allowed. If left blank, the generic default avatar is used.",
"placeholder": "",
"default": "",
"advanced": true
}
]
},
@ -1318,6 +1335,68 @@
"advanced": true
}
]
},
{
"name": "broadcasting",
"label": "Broadcasting",
"settings": [
{
"name": "users",
"label": "Broadcasted Users",
"type": "table",
"advanced": true,
"can_add_new_rows": true,
"help": "Users that are broadcasted to downstream servers",
"numbered": false,
"columns": [
{
"name": "user",
"label": "User",
"can_set": true
}
]
},
{
"name": "downstream_servers",
"label": "Receiving Servers",
"assignment-types": [0,1],
"type": "table",
"advanced": true,
"can_add_new_rows": true,
"help": "Servers that receive data for broadcasted users",
"numbered": false,
"columns": [
{
"name": "address",
"label": "Address",
"can_set": true
},
{
"name": "port",
"label": "Port",
"can_set": true
},
{
"name": "server_type",
"label": "Server Type",
"type": "select",
"placeholder": "Audio Mixer",
"default": "Audio Mixer",
"can_set": true,
"options": [
{
"value": "Audio Mixer",
"label": "Audio Mixer"
},
{
"value": "Avatar Mixer",
"label": "Avatar Mixer"
}
]
}
]
}
]
}
]
}

View file

@ -21,7 +21,7 @@
</p>
<p>
If your domain has any content that you would like to re-use at a later date, save a manual backup of your models.json.gz file, which is usually stored at the following paths:<br>
<pre>C:\Users\[username]\AppData\Roaming\High Fidelity\assignment-client/entities/models.json.gz</pre>
<pre>C:/Users/[username]/AppData/Roaming/High Fidelity/assignment-client/entities/models.json.gz</pre>
<pre>/Users/[username]/Library/Application Support/High Fidelity/assignment-client/entities/models.json.gz</pre>
<pre>/home/[username]/.local/share/High Fidelity/assignment-client/entities/models.json.gz</pre>
</p>

View file

@ -10,7 +10,6 @@
<link href="/css/sweetalert.css" rel="stylesheet" media="screen">
<link href="/css/bootstrap-switch.min.css" rel="stylesheet" media="screen">
</head>
<body>
<nav class="navbar navbar-default" role="navigation">
@ -38,8 +37,23 @@
</li>
<li><a href="/content/">Content</a></li>
<li><a href="/settings/">Settings</a></li>
<li><a href="#" id="restart-server"><span class="glyphicon glyphicon-refresh"></span> Restart</a></li>
</ul>
</div>
</div><!-- /.container-fluid -->
</nav>
<div class="modal fade" id="restart-modal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">domain-server is restarting</h4>
</div>
<div class="modal-body">
<h5>This page will automatically refresh in <span id="refresh-time">3 seconds</span>.</h5>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<div class="container-fluid">

View file

@ -1,3 +1,28 @@
function showRestartModal() {
$('#restart-modal').modal({
backdrop: 'static',
keyboard: false
});
var secondsElapsed = 0;
var numberOfSecondsToWait = 3;
var refreshSpan = $('span#refresh-time')
refreshSpan.html(numberOfSecondsToWait + " seconds");
// call ourselves every 1 second to countdown
var refreshCountdown = setInterval(function(){
secondsElapsed++;
secondsLeft = numberOfSecondsToWait - secondsElapsed
refreshSpan.html(secondsLeft + (secondsLeft == 1 ? " second" : " seconds"))
if (secondsElapsed == numberOfSecondsToWait) {
location.reload(true);
clearInterval(refreshCountdown);
}
}, 1000);
}
$(document).ready(function(){
var url = window.location;
// Will only work if string in href matches with location
@ -7,4 +32,10 @@ $(document).ready(function(){
$('ul.nav a').filter(function() {
return this.href == url;
}).parent().addClass('active');
$('body').on('click', '#restart-server', function(e){
$.get("/restart");
showRestartModal();
return false;
});
});

View file

@ -26,7 +26,7 @@
</ul>
<button id="advanced-toggle-button" class="btn btn-info advanced-toggle">Show advanced</button>
<button class="btn btn-success save-button">Save and restart</button>
<button class="btn btn-success save-button">Save</button>
</div>
</div>
@ -77,23 +77,10 @@
</div>
<div class="col-xs-12 hidden-sm hidden-md hidden-lg">
<button class="btn btn-success save-button" id="small-save-button">Save and restart</button>
<button class="btn btn-success save-button" id="small-save-button">Save</button>
</div>
</div>
<div class="modal fade" id="restart-modal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">domain-server is restarting</h4>
</div>
<div class="modal-body">
<h5>This page will automatically refresh in <span id="refresh-time">3 seconds</span>.</h5>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!--#include virtual="footer.html"-->
<script src='/js/underscore-min.js'></script>
<script src='/js/underscore-keypath.min.js'></script>

View file

@ -39,7 +39,8 @@ var Settings = {
ACCESS_TOKEN_SELECTOR: '[name="metaverse.access_token"]',
PLACES_TABLE_ID: 'places-table',
FORM_ID: 'settings-form',
INVALID_ROW_CLASS: 'invalid-input'
INVALID_ROW_CLASS: 'invalid-input',
DATA_ROW_INDEX: 'data-row-index'
};
var viewHelpers = {
@ -223,6 +224,14 @@ $(document).ready(function(){
// set focus to the first input in the new row
$target.closest('table').find('tr.inputs input:first').focus();
}
var tableRows = sibling.parent();
var tableBody = tableRows.parent();
// if theres no more siblings, we should jump to a new row
if (sibling.next().length == 0 && tableRows.nextAll().length == 1) {
tableBody.find("." + Settings.ADD_ROW_BUTTON_CLASS).click();
}
}
} else if ($target.is('input')) {
@ -997,7 +1006,7 @@ function saveSettings() {
var password = formJSON["security"]["http_password"];
var verify_password = formJSON["security"]["verify_http_password"];
// if they've only emptied out the default password field, we should go ahead and acknowledge
// if they've only emptied out the default password field, we should go ahead and acknowledge
// the verify password field
if (password != undefined && verify_password == undefined) {
verify_password = "";
@ -1150,8 +1159,9 @@ function makeTable(setting, keypath, setting_value) {
}
html += "<tr class='" + Settings.DATA_ROW_CLASS + "' " +
(isCategorized ? ("data-category='" + categoryValue + "'") : "") + " " +
(isArray ? "" : "name='" + keypath + "." + rowIndexOrName + "'") + ">";
(isCategorized ? ("data-category='" + categoryValue + "'") : "") + " " +
(isArray ? "" : "name='" + keypath + "." + rowIndexOrName + "'") +
(isArray ? Settings.DATA_ROW_INDEX + "='" + (row_num - 1) + "'" : "" ) + ">";
if (setting.numbered === true) {
html += "<td class='numbered'>" + row_num + "</td>"
@ -1281,6 +1291,17 @@ function makeTableHiddenInputs(setting, initialValues, categoryValue) {
"<input type='checkbox' style='display: none;' class='form-control table-checkbox' " +
"name='" + col.name + "'" + (defaultValue ? " checked" : "") + "/>" +
"</td>";
} else if (col.type === "select") {
html += "<td class='" + Settings.DATA_COL_CLASS + "'name='" + col.name + "'>"
html += "<select style='display: none;' class='form-control' data-hidden-input='" + col.name + "'>'"
for (var i in col.options) {
var option = col.options[i];
html += "<option value='" + option.value + "' " + (option.value == defaultValue ? 'selected' : '') + ">" + option.label + "</option>";
}
html += "</select>";
html += "<input type='hidden' class='table-dropdown form-control trigger-change' name='" + col.name + "' value='" + defaultValue + "'></td>";
} else {
html +=
"<td " + (col.hidden ? "style='display: none;'" : "") + " class='" + Settings.DATA_COL_CLASS + "' " +
@ -1320,6 +1341,18 @@ function makeTableCategoryInput(setting, numVisibleColumns) {
return html;
}
function getDescriptionForKey(key) {
for (var i in Settings.data.descriptions) {
if (Settings.data.descriptions[i].name === key) {
return Settings.data.descriptions[i];
}
}
}
var SAVE_BUTTON_LABEL_SAVE = "Save";
var SAVE_BUTTON_LABEL_RESTART = "Save and restart";
var reasonsForRestart = [];
function badgeSidebarForDifferences(changedElement) {
// figure out which group this input is in
var panelParentID = changedElement.closest('.panel').attr('id');
@ -1342,13 +1375,24 @@ function badgeSidebarForDifferences(changedElement) {
}
var badgeValue = 0
var description = getDescriptionForKey(panelParentID);
// badge for any settings we have that are not the same or are not present in initialValues
for (var setting in panelJSON) {
if ((!_.has(initialPanelJSON, setting) && panelJSON[setting] !== "") ||
(!_.isEqual(panelJSON[setting], initialPanelJSON[setting])
&& (panelJSON[setting] !== "" || _.has(initialPanelJSON, setting)))) {
badgeValue += 1
badgeValue += 1;
// add a reason to restart
if (description && description.restart != false) {
reasonsForRestart.push(setting);
}
} else {
// remove a reason to restart
if (description && description.restart != false) {
reasonsForRestart = $.grep(reasonsForRestart, function(v) { return v != setting; });
}
}
}
@ -1357,6 +1401,7 @@ function badgeSidebarForDifferences(changedElement) {
badgeValue = ""
}
$(".save-button").html(reasonsForRestart.length > 0 ? SAVE_BUTTON_LABEL_RESTART : SAVE_BUTTON_LABEL_SAVE);
$("a[href='#" + panelParentID + "'] .badge").html(badgeValue);
}
@ -1379,6 +1424,21 @@ function addTableRow(row) {
var setting_name = table.attr("name");
row.addClass(Settings.DATA_ROW_CLASS + " " + Settings.NEW_ROW_CLASS);
// if this is an array, add the row index (which is the index of the last row + 1)
// as a data attribute to the row
var row_index = 0;
if (isArray) {
var previous_row = row.siblings('.' + Settings.DATA_ROW_CLASS + ':last');
if (previous_row.length > 0) {
row_index = parseInt(previous_row.attr(Settings.DATA_ROW_INDEX), 10) + 1;
} else {
row_index = 0;
}
row.attr(Settings.DATA_ROW_INDEX, row_index);
}
var focusChanged = false;
_.each(row.children(), function(element) {
@ -1408,19 +1468,23 @@ function addTableRow(row) {
input.show();
var isCheckbox = input.hasClass("table-checkbox");
var isDropdown = input.hasClass("table-dropdown");
if (isArray) {
var row_index = row.siblings('.' + Settings.DATA_ROW_CLASS).length
var key = $(element).attr('name');
// are there multiple columns or just one?
// with multiple we have an array of Objects, with one we have an array of whatever the value type is
var num_columns = row.children('.' + Settings.DATA_COL_CLASS).length
var newName = setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : "");
if (isCheckbox) {
input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : ""))
} else {
input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : ""))
input.attr("name", newName);
if (isDropdown) {
// default values for hidden inputs inside child selects gets cleared so we need to remind it
var selectElement = $(element).children("select");
selectElement.attr("data-hidden-input", newName);
$(element).children("input").val(selectElement.val());
}
} else {
// because the name of the setting in question requires the key
@ -1436,6 +1500,12 @@ function addTableRow(row) {
focusChanged = true;
}
// if we are adding a dropdown, we should go ahead and make its select
// element is visible
if (isDropdown) {
$(element).children("select").attr("style", "");
}
if (isCheckbox) {
$(input).find("input").attr("data-changed", "true");
} else {
@ -1648,31 +1718,6 @@ function updateDataChangedForSiblingRows(row, forceTrue) {
})
}
function showRestartModal() {
$('#restart-modal').modal({
backdrop: 'static',
keyboard: false
});
var secondsElapsed = 0;
var numberOfSecondsToWait = 3;
var refreshSpan = $('span#refresh-time')
refreshSpan.html(numberOfSecondsToWait + " seconds");
// call ourselves every 1 second to countdown
var refreshCountdown = setInterval(function(){
secondsElapsed++;
secondsLeft = numberOfSecondsToWait - secondsElapsed
refreshSpan.html(secondsLeft + (secondsLeft == 1 ? " second" : " seconds"))
if (secondsElapsed == numberOfSecondsToWait) {
location.reload(true);
clearInterval(refreshCountdown);
}
}, 1000);
}
function cleanupFormValues(node) {
if (node.type && node.type === 'checkbox') {
return { name: node.name, value: node.checked ? true : false };

View file

@ -117,6 +117,10 @@ DomainServer::DomainServer(int argc, char* argv[]) :
// if permissions are updated, relay the changes to the Node datastructures
connect(&_settingsManager, &DomainServerSettingsManager::updateNodePermissions,
&_gatekeeper, &DomainGatekeeper::updateNodePermissions);
connect(&_settingsManager, &DomainServerSettingsManager::settingsUpdated,
this, &DomainServer::updateReplicatedNodes);
connect(&_settingsManager, &DomainServerSettingsManager::settingsUpdated,
this, &DomainServer::updateDownstreamNodes);
setupGroupCacheRefresh();
@ -129,6 +133,9 @@ DomainServer::DomainServer(int argc, char* argv[]) :
setupNodeListAndAssignments();
updateReplicatedNodes();
updateDownstreamNodes();
if (_type != NonMetaverse) {
// if we have a metaverse domain, we'll use an access token for API calls
resetAccountManagerAccessToken();
@ -958,6 +965,11 @@ void DomainServer::handleConnectedNode(SharedNodePointer newNode) {
emit userConnected();
}
if (shouldReplicateNode(*newNode)) {
qDebug() << "Setting node to replicated: " << newNode->getUUID();
newNode->setIsReplicated(true);
}
// send out this node to our other connected nodes
broadcastNewNode(newNode);
}
@ -1650,6 +1662,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
const QString URI_NODES = "/nodes";
const QString URI_SETTINGS = "/settings";
const QString URI_ENTITY_FILE_UPLOAD = "/content/upload";
const QString URI_RESTART = "/restart";
const QString UUID_REGEX_STRING = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
@ -1804,6 +1817,10 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
// send the response
connection->respond(HTTPConnection::StatusCode200, nodesDocument.toJson(), qPrintable(JSON_MIME_TYPE));
return true;
} else if (url.path() == URI_RESTART) {
connection->respond(HTTPConnection::StatusCode200);
restart();
return true;
} else {
// check if this is for json stats for a node
@ -2210,6 +2227,131 @@ void DomainServer::refreshStaticAssignmentAndAddToQueue(SharedAssignmentPointer&
_unfulfilledAssignments.enqueue(assignment);
}
static const QString BROADCASTING_SETTINGS_KEY = "broadcasting";
void DomainServer::updateDownstreamNodes() {
auto settings = _settingsManager.getSettingsMap();
if (settings.contains(BROADCASTING_SETTINGS_KEY)) {
auto nodeList = DependencyManager::get<LimitedNodeList>();
std::vector<HifiSockAddr> downstreamNodesInSettings;
auto replicationSettings = settings.value(BROADCASTING_SETTINGS_KEY).toMap();
if (replicationSettings.contains("downstream_servers")) {
auto serversSettings = replicationSettings.value("downstream_servers").toList();
std::vector<HifiSockAddr> knownDownstreamNodes;
nodeList->eachNode([&](const SharedNodePointer& otherNode) {
if (NodeType::isDownstream(otherNode->getType())) {
knownDownstreamNodes.push_back(otherNode->getPublicSocket());
}
});
for (auto& server : serversSettings) {
auto downstreamServer = server.toMap();
static const QString DOWNSTREAM_SERVER_ADDRESS = "address";
static const QString DOWNSTREAM_SERVER_PORT = "port";
static const QString DOWNSTREAM_SERVER_TYPE = "server_type";
// make sure we have the settings we need for this downstream server
if (downstreamServer.contains(DOWNSTREAM_SERVER_ADDRESS) && downstreamServer.contains(DOWNSTREAM_SERVER_PORT)) {
auto nodeType = NodeType::fromString(downstreamServer[DOWNSTREAM_SERVER_TYPE].toString());
auto downstreamNodeType = NodeType::downstreamType(nodeType);
// read the address and port and construct a HifiSockAddr from them
HifiSockAddr downstreamServerAddr {
downstreamServer[DOWNSTREAM_SERVER_ADDRESS].toString(),
(quint16) downstreamServer[DOWNSTREAM_SERVER_PORT].toString().toInt()
};
downstreamNodesInSettings.push_back(downstreamServerAddr);
bool knownNode = find(knownDownstreamNodes.cbegin(), knownDownstreamNodes.cend(),
downstreamServerAddr) != knownDownstreamNodes.cend();
if (!knownNode) {
// manually add the downstream node to our node list
auto node = nodeList->addOrUpdateNode(QUuid::createUuid(), downstreamNodeType,
downstreamServerAddr, downstreamServerAddr);
node->setIsForcedNeverSilent(true);
qDebug() << "Adding downstream node:" << node->getUUID() << downstreamServerAddr;
// manually activate the public socket for the downstream node
node->activatePublicSocket();
}
}
}
}
// enumerate the nodes to determine which are no longer downstream for this domain
// collect them in a vector to separately remove them with handleKillNode (since eachNode has a read lock and
// we cannot recursively take the write lock required by handleKillNode)
std::vector<SharedNodePointer> nodesToKill;
nodeList->eachNode([&](const SharedNodePointer& otherNode) {
if (NodeType::isDownstream(otherNode->getType())) {
bool nodeInSettings = find(downstreamNodesInSettings.cbegin(), downstreamNodesInSettings.cend(),
otherNode->getPublicSocket()) != downstreamNodesInSettings.cend();
if (!nodeInSettings) {
qDebug() << "Removing downstream node:" << otherNode->getUUID() << otherNode->getPublicSocket();
nodesToKill.push_back(otherNode);
}
}
});
for (auto& node : nodesToKill) {
handleKillNode(node);
}
}
}
void DomainServer::updateReplicatedNodes() {
// Make sure we have downstream nodes in our list
auto settings = _settingsManager.getSettingsMap();
static const QString REPLICATED_USERS_KEY = "users";
_replicatedUsernames.clear();
if (settings.contains(BROADCASTING_SETTINGS_KEY)) {
auto replicationSettings = settings.value(BROADCASTING_SETTINGS_KEY).toMap();
if (replicationSettings.contains(REPLICATED_USERS_KEY)) {
auto usersSettings = replicationSettings.value(REPLICATED_USERS_KEY).toList();
for (auto& username : usersSettings) {
_replicatedUsernames.push_back(username.toString().toLower());
}
}
}
auto nodeList = DependencyManager::get<LimitedNodeList>();
nodeList->eachMatchingNode([this](const SharedNodePointer& otherNode) -> bool {
return otherNode->getType() == NodeType::Agent;
}, [this](const SharedNodePointer& otherNode) {
auto shouldReplicate = shouldReplicateNode(*otherNode);
auto isReplicated = otherNode->isReplicated();
if (isReplicated && !shouldReplicate) {
qDebug() << "Setting node to NOT be replicated:"
<< otherNode->getPermissions().getVerifiedUserName() << otherNode->getUUID();
} else if (!isReplicated && shouldReplicate) {
qDebug() << "Setting node to replicated:"
<< otherNode->getPermissions().getVerifiedUserName() << otherNode->getUUID();
}
otherNode->setIsReplicated(shouldReplicate);
}
);
}
bool DomainServer::shouldReplicateNode(const Node& node) {
if (node.getType() == NodeType::Agent) {
QString verifiedUsername = node.getPermissions().getVerifiedUserName();
// Both the verified username and usernames in _replicatedUsernames are lowercase, so
// comparisons here are case-insensitive.
auto it = find(_replicatedUsernames.cbegin(), _replicatedUsernames.cend(), verifiedUsername);
return it != _replicatedUsernames.end();
} else {
return false;
}
};
void DomainServer::nodeAdded(SharedNodePointer node) {
// we don't use updateNodeWithData, so add the DomainServerNodeData to the node here
node->setLinkedData(std::unique_ptr<DomainServerNodeData> { new DomainServerNodeData() });

View file

@ -102,6 +102,9 @@ private slots:
void handleOctreeFileReplacement(QByteArray octreeFile);
void updateReplicatedNodes();
void updateDownstreamNodes();
signals:
void iceServerChanged();
void userConnected();
@ -161,12 +164,16 @@ private:
QJsonObject jsonForSocket(const HifiSockAddr& socket);
QJsonObject jsonObjectForNode(const SharedNodePointer& node);
bool shouldReplicateNode(const Node& node);
void setupGroupCacheRefresh();
QString pathForRedirect(QString path = QString()) const;
SubnetList _acSubnetWhitelist;
std::vector<QString> _replicatedUsernames;
DomainGatekeeper _gatekeeper;
HTTPManager _httpManager;

View file

@ -991,6 +991,7 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection
unpackPermissions();
apiRefreshGroupInformation();
emit updateNodePermissions();
emit settingsUpdated();
}
return true;
@ -1196,13 +1197,14 @@ QJsonObject DomainServerSettingsManager::settingDescriptionFromGroup(const QJson
bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject) {
static const QString SECURITY_ROOT_KEY = "security";
static const QString AC_SUBNET_WHITELIST_KEY = "ac_subnet_whitelist";
static const QString BROADCASTING_KEY = "broadcasting";
auto& settingsVariant = _configMap.getConfig();
bool needRestart = false;
// Iterate on the setting groups
foreach(const QString& rootKey, postedObject.keys()) {
QJsonValue rootValue = postedObject[rootKey];
const QJsonValue& rootValue = postedObject[rootKey];
if (!settingsVariant.contains(rootKey)) {
// we don't have a map below this key yet, so set it up now
@ -1247,7 +1249,7 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ
if (!matchingDescriptionObject.isEmpty()) {
updateSetting(rootKey, rootValue, *thisMap, matchingDescriptionObject);
if (rootKey != SECURITY_ROOT_KEY) {
if (rootKey != SECURITY_ROOT_KEY && rootKey != BROADCASTING_KEY) {
needRestart = true;
}
} else {
@ -1261,9 +1263,10 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ
// if we matched the setting then update the value
if (!matchingDescriptionObject.isEmpty()) {
QJsonValue settingValue = rootValue.toObject()[settingKey];
const QJsonValue& settingValue = rootValue.toObject()[settingKey];
updateSetting(settingKey, settingValue, *thisMap, matchingDescriptionObject);
if (rootKey != SECURITY_ROOT_KEY || settingKey == AC_SUBNET_WHITELIST_KEY) {
if ((rootKey != SECURITY_ROOT_KEY && rootKey != BROADCASTING_KEY)
|| settingKey == AC_SUBNET_WHITELIST_KEY) {
needRestart = true;
}
} else {

View file

@ -108,6 +108,7 @@ public:
signals:
void updateNodePermissions();
void settingsUpdated();
public slots:
void apiGetGroupIDJSONCallback(QNetworkReply& requestReply);

View file

@ -37,20 +37,12 @@ RenderingClient::RenderingClient(QObject *parent, const QString& launchURLString
DependencyManager::set<AvatarHashMap>();
// get our audio client setup on its own thread
QThread* audioThread = new QThread();
auto audioClient = DependencyManager::set<AudioClient>();
audioClient->setPositionGetter(getPositionForAudio);
audioClient->setOrientationGetter(getOrientationForAudio);
audioClient->startThread();
audioClient->moveToThread(audioThread);
connect(audioThread, &QThread::started, audioClient.data(), &AudioClient::start);
connect(audioClient.data(), &AudioClient::destroyed, audioThread, &QThread::quit);
connect(audioThread, &QThread::finished, audioThread, &QThread::deleteLater);
audioThread->start();
connect(&_avatarTimer, &QTimer::timeout, this, &RenderingClient::sendAvatarPacket);
_avatarTimer.setInterval(16); // 60 FPS
_avatarTimer.start();

View file

@ -103,8 +103,8 @@
"rotationVar": "spine2Rotation",
"typeVar": "spine2Type",
"weightVar": "spine2Weight",
"weight": 1.0,
"flexCoefficients": [1.0, 0.5, 0.5]
"weight": 2.0,
"flexCoefficients": [1.0, 0.5, 0.25]
},
{
"jointName": "Head",
@ -113,7 +113,7 @@
"typeVar": "headType",
"weightVar": "headWeight",
"weight": 4.0,
"flexCoefficients": [1, 0.05, 0.25, 0.25, 0.25]
"flexCoefficients": [1, 0.5, 0.25, 0.2, 0.1]
},
{
"jointName": "LeftArm",

View file

@ -1,14 +1,16 @@
{
"RenderShadowTask": {
"Enabled": {
"enabled": true
}
},
"RenderDeferredTask": {
"AmbientOcclusion": {
"RenderMainView": {
"RenderShadowTask": {
"Enabled": {
"enabled": true
}
},
"RenderDeferredTask": {
"AmbientOcclusion": {
"Enabled": {
"enabled": true
}
}
}
}
}

View file

@ -57,6 +57,8 @@
{ "from": "OculusTouch.LeftThumbUp", "to": "Standard.LeftThumbUp" },
{ "from": "OculusTouch.RightThumbUp", "to": "Standard.RightThumbUp" },
{ "from": "OculusTouch.LeftIndexPoint", "to": "Standard.LeftIndexPoint" },
{ "from": "OculusTouch.RightIndexPoint", "to": "Standard.RightIndexPoint" }
{ "from": "OculusTouch.RightIndexPoint", "to": "Standard.RightIndexPoint" },
{ "from": "OculusTouch.Head", "to" : "Standard.Head", "when" : [ "Application.InHMD"] }
]
}

View file

@ -35,35 +35,35 @@
{ "from": "Vive.RightApplicationMenu", "to": "Standard.RightSecondaryThumb" },
{ "from": "Vive.LeftHand", "to": "Standard.LeftHand", "when": [ "Application.InHMD" ] },
{ "from": "Vive.RightHand", "to": "Standard.RightHand", "when": [ "Application.InHMD" ] },
{ "from": "Vive.RightHand", "to": "Standard.RightHand", "when": [ "Application.InHMD" ] },
{
"from": "Vive.LeftFoot", "to" : "Standard.LeftFoot",
"filters" : [{"type" : "lowVelocity", "rotation" : 1.0, "translation": 1.0}],
"when": [ "Application.InHMD"]
"when": [ "Application.InHMD" ]
},
{
"from": "Vive.RightFoot", "to" : "Standard.RightFoot",
"filters" : [{"type" : "lowVelocity", "rotation" : 1.0, "translation": 1.0}],
"when": [ "Application.InHMD"]
"when": [ "Application.InHMD" ]
},
{
"from": "Vive.Hips", "to" : "Standard.Hips",
"filters" : [{"type" : "lowVelocity", "rotation" : 0.01, "translation": 0.01}],
"when": [ "Application.InHMD"]
"when": [ "Application.InHMD" ]
},
{
"from": "Vive.Spine2", "to" : "Standard.Spine2",
"filters" : [{"type" : "lowVelocity", "rotation" : 0.01, "translation": 0.01}],
"when": [ "Application.InHMD"]
"when": [ "Application.InHMD" ]
},
{ "from": "Vive.Head", "to" : "Standard.Head", "when" : [ "Application.InHMD"] },
{ "from": "Vive.Head", "to" : "Standard.Head", "when": [ "Application.InHMD" ] },
{ "from": "Vive.RightArm", "to" : "Standard.RightArm", "when" : [ "Application.InHMD"] },
{ "from": "Vive.LeftArm", "to" : "Standard.LeftArm", "when" : [ "Application.InHMD"] }
{ "from": "Vive.RightArm", "to" : "Standard.RightArm", "when": [ "Application.InHMD" ] },
{ "from": "Vive.LeftArm", "to" : "Standard.LeftArm", "when": [ "Application.InHMD" ] }
]
}

View file

@ -32,7 +32,7 @@ var EventBridge;
var webChannel = new QWebChannel(qt.webChannelTransport, function (channel) {
// replace the TempEventBridge with the real one.
var tempEventBridge = EventBridge;
EventBridge = channel.objects.eventBridgeWrapper.eventBridge;
EventBridge = channel.objects.eventBridge;
tempEventBridge._callbacks.forEach(function (callback) {
EventBridge.scriptEventReceived.connect(callback);
});

View file

@ -1,70 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 50 50"
style="enable-background:new 0 0 50 50;"
xml:space="preserve"
id="svg2"
inkscape:version="0.91 r13725"
sodipodi:docname="mic-mute-a.svg"><metadata
id="metadata22"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs20" /><sodipodi:namedview
pagecolor="#ff0000"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="852"
inkscape:window-height="480"
id="namedview18"
showgrid="false"
inkscape:zoom="4.72"
inkscape:cx="25"
inkscape:cy="25"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" /><style
type="text/css"
id="style4">
.st0{fill:#FFFFFF;}
</style><g
id="Layer_2" /><g
id="Layer_1"
style="fill:#000000;fill-opacity:1"><path
class="st0"
d="M28.9,17.1v-0.5c0-2-1.7-3.6-3.7-3.6h0c-2,0-3.7,1.6-3.7,3.6v6.9L28.9,17.1z"
id="path8"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M21.5,29.2v0.2c0,2,1.6,3.6,3.7,3.6h0c2,0,3.7-1.6,3.7-3.6v-6.6L21.5,29.2z"
id="path10"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M39.1,16.8L13.6,39.1c-0.7,0.6-1.8,0.5-2.4-0.2l-0.2-0.2c-0.6-0.7-0.5-1.8,0.2-2.4l25.4-22.4 c0.7-0.6,1.8-0.5,2.4,0.2l0.2,0.2C39.8,15.1,39.7,16.1,39.1,16.8z"
id="path12"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M23.4,40.2l0,3.4h-4.3c-1,0-1.8,0.8-1.8,1.8c0,1,0.8,1.8,1.8,1.8h12.3c1,0,1.8-0.8,1.8-1.8 c0-1-0.8-1.8-1.8-1.8h-4.4l0-3.4c5.2-0.8,9.2-5,9.2-10.1c0-0.1,0-5.1,0-5.3c0-1-0.9-1.7-1.8-1.7c-1,0-1.7,0.8-1.7,1.8 c0,0.3,0,4.8,0,5.2c0,3.7-3.4,6.7-7.5,6.7c-3.6,0-6.7-2.3-7.3-5.4L15,34C16.4,37.2,19.6,39.7,23.4,40.2z"
id="path14"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M17.7,24.9c0-1-0.7-1.8-1.6-1.8c-1-0.1-1.8,0.7-1.9,1.6c0,0.2,0,4.2,0,5.3l3.5-3.1 C17.7,25.9,17.7,25,17.7,24.9z"
id="path16"
style="fill:#000000;fill-opacity:1" /></g></svg>
<svg version="1.1"
id="svg2" inkscape:version="0.91 r13725" sodipodi:docname="mic-mute-a.svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 50 50"
style="enable-background:new 0 0 50 50;" xml:space="preserve">
<style type="text/css">
.st0{fill:#EA4C5F;}
</style>
<sodipodi:namedview bordercolor="#666666" borderopacity="1" gridtolerance="10" guidetolerance="10" id="namedview18" inkscape:current-layer="svg2" inkscape:cx="25" inkscape:cy="25" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-height="480" inkscape:window-maximized="0" inkscape:window-width="852" inkscape:window-x="0" inkscape:window-y="0" inkscape:zoom="4.72" objecttolerance="10" pagecolor="#ff0000" showgrid="false">
</sodipodi:namedview>
<g id="Layer_2">
</g>
<g id="Layer_1">
<path id="path8" class="st0" d="M28.9,17.1v-0.5c0-2-1.7-3.6-3.7-3.6l0,0c-2,0-3.7,1.6-3.7,3.6v6.9L28.9,17.1z"/>
<path id="path10" class="st0" d="M21.5,29.2v0.2c0,2,1.6,3.6,3.7,3.6l0,0c2,0,3.7-1.6,3.7-3.6v-6.6L21.5,29.2z"/>
<path id="path12" class="st0" d="M39.1,16.8L13.6,39.1c-0.7,0.6-1.8,0.5-2.4-0.2L11,38.7c-0.6-0.7-0.5-1.8,0.2-2.4l25.4-22.4
c0.7-0.6,1.8-0.5,2.4,0.2l0.2,0.2C39.8,15.1,39.7,16.1,39.1,16.8z"/>
<path id="path14" class="st0" d="M23.4,40.2v3.4h-4.3c-1,0-1.8,0.8-1.8,1.8s0.8,1.8,1.8,1.8h12.3c1,0,1.8-0.8,1.8-1.8
s-0.8-1.8-1.8-1.8H27v-3.4c5.2-0.8,9.2-5,9.2-10.1c0-0.1,0-5.1,0-5.3c0-1-0.9-1.7-1.8-1.7c-1,0-1.7,0.8-1.7,1.8c0,0.3,0,4.8,0,5.2
c0,3.7-3.4,6.7-7.5,6.7c-3.6,0-6.7-2.3-7.3-5.4L15,34C16.4,37.2,19.6,39.7,23.4,40.2z"/>
<path id="path16" class="st0" d="M17.7,24.9c0-1-0.7-1.8-1.6-1.8c-1-0.1-1.8,0.7-1.9,1.6c0,0.2,0,4.2,0,5.3l3.5-3.1
C17.7,25.9,17.7,25,17.7,24.9z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -1,21 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<svg version="1.1"
id="svg2" inkscape:version="0.91 r13725" sodipodi:docname="mic-mute-a.svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 50 50"
style="enable-background:new 0 0 50 50;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st0{fill:#EA4C5F;}
</style>
<sodipodi:namedview bordercolor="#666666" borderopacity="1" gridtolerance="10" guidetolerance="10" id="namedview18" inkscape:current-layer="svg2" inkscape:cx="25" inkscape:cy="25" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-height="480" inkscape:window-maximized="0" inkscape:window-width="852" inkscape:window-x="0" inkscape:window-y="0" inkscape:zoom="4.72" objecttolerance="10" pagecolor="#ff0000" showgrid="false">
</sodipodi:namedview>
<g id="Layer_2">
</g>
<g id="Layer_1">
<path class="st0" d="M28.9,17.1v-0.5c0-2-1.7-3.6-3.7-3.6h0c-2,0-3.7,1.6-3.7,3.6v6.9L28.9,17.1z"/>
<path class="st0" d="M21.5,29.2v0.2c0,2,1.6,3.6,3.7,3.6h0c2,0,3.7-1.6,3.7-3.6v-6.6L21.5,29.2z"/>
<path class="st0" d="M39.1,16.8L13.6,39.1c-0.7,0.6-1.8,0.5-2.4-0.2l-0.2-0.2c-0.6-0.7-0.5-1.8,0.2-2.4l25.4-22.4
<path id="path8" class="st0" d="M28.9,17.1v-0.5c0-2-1.7-3.6-3.7-3.6l0,0c-2,0-3.7,1.6-3.7,3.6v6.9L28.9,17.1z"/>
<path id="path10" class="st0" d="M21.5,29.2v0.2c0,2,1.6,3.6,3.7,3.6l0,0c2,0,3.7-1.6,3.7-3.6v-6.6L21.5,29.2z"/>
<path id="path12" class="st0" d="M39.1,16.8L13.6,39.1c-0.7,0.6-1.8,0.5-2.4-0.2L11,38.7c-0.6-0.7-0.5-1.8,0.2-2.4l25.4-22.4
c0.7-0.6,1.8-0.5,2.4,0.2l0.2,0.2C39.8,15.1,39.7,16.1,39.1,16.8z"/>
<path class="st0" d="M23.4,40.2l0,3.4h-4.3c-1,0-1.8,0.8-1.8,1.8c0,1,0.8,1.8,1.8,1.8h12.3c1,0,1.8-0.8,1.8-1.8
c0-1-0.8-1.8-1.8-1.8h-4.4l0-3.4c5.2-0.8,9.2-5,9.2-10.1c0-0.1,0-5.1,0-5.3c0-1-0.9-1.7-1.8-1.7c-1,0-1.7,0.8-1.7,1.8
c0,0.3,0,4.8,0,5.2c0,3.7-3.4,6.7-7.5,6.7c-3.6,0-6.7-2.3-7.3-5.4L15,34C16.4,37.2,19.6,39.7,23.4,40.2z"/>
<path class="st0" d="M17.7,24.9c0-1-0.7-1.8-1.6-1.8c-1-0.1-1.8,0.7-1.9,1.6c0,0.2,0,4.2,0,5.3l3.5-3.1
<path id="path14" class="st0" d="M23.4,40.2v3.4h-4.3c-1,0-1.8,0.8-1.8,1.8s0.8,1.8,1.8,1.8h12.3c1,0,1.8-0.8,1.8-1.8
s-0.8-1.8-1.8-1.8H27v-3.4c5.2-0.8,9.2-5,9.2-10.1c0-0.1,0-5.1,0-5.3c0-1-0.9-1.7-1.8-1.7c-1,0-1.7,0.8-1.7,1.8c0,0.3,0,4.8,0,5.2
c0,3.7-3.4,6.7-7.5,6.7c-3.6,0-6.7-2.3-7.3-5.4L15,34C16.4,37.2,19.6,39.7,23.4,40.2z"/>
<path id="path16" class="st0" d="M17.7,24.9c0-1-0.7-1.8-1.6-1.8c-1-0.1-1.8,0.7-1.9,1.6c0,0.2,0,4.2,0,5.3l3.5-3.1
C17.7,25.9,17.7,25,17.7,24.9z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View file

@ -1,85 +1,89 @@
name = Jointy3
name = mannequin
type = body+head
scale = 1
filename = Jointy3/Jointy3.fbx
texdir = Jointy3/textures
filename = mannequin/mannequin.baked.fbx
joint = jointEyeLeft = LeftEye
joint = jointRightHand = RightHand
joint = jointHead = Head
joint = jointEyeRight = RightEye
joint = jointLean = Spine
joint = jointNeck = Neck
joint = jointLeftHand = LeftHand
joint = jointEyeRight = RightEye
joint = jointHead = Head
joint = jointRightHand = RightHand
joint = jointRoot = Hips
joint = jointLean = Spine
joint = jointEyeLeft = LeftEye
freeJoint = LeftArm
freeJoint = LeftForeArm
freeJoint = RightArm
freeJoint = RightForeArm
jointIndex = RightHand = 17
jointIndex = LeftHandIndex3 = 56
jointIndex = Hips = 0
jointIndex = LeftHandRing2 = 47
jointIndex = LeftHandThumb3 = 60
jointIndex = RightShoulder = 14
jointIndex = RightHandRing1 = 30
jointIndex = RightHandRing3 = 32
jointIndex = LeftHandPinky4 = 45
jointIndex = LeftHandRing1 = 46
jointIndex = LeftFoot = 8
jointIndex = RightHandIndex2 = 23
jointIndex = RightToeBase = 4
jointIndex = RightHandMiddle4 = 29
jointIndex = RightHandPinky4 = 37
jointIndex = LeftToe_End = 10
jointIndex = RightEye = 66
jointIndex = RightHandPinky2 = 35
jointIndex = RightHandRing2 = 31
jointIndex = LeftHand = 41
jointIndex = RightToe_End = 5
jointIndex = LeftEye = 65
jointIndex = LeftHandThumb2 = 59
jointIndex = pCylinder73Shape1 = 67
jointIndex = LeftShoulder = 38
jointIndex = LeftHandIndex2 = 55
jointIndex = RightForeArm = 16
jointIndex = LeftHandMiddle2 = 51
jointIndex = RightHandRing4 = 33
jointIndex = LeftLeg = 7
jointIndex = LeftHandThumb4 = 61
jointIndex = LeftForeArm = 40
jointIndex = HeadTop_End = 64
jointIndex = RightHandPinky1 = 34
jointIndex = RightHandIndex1 = 22
jointIndex = LeftHandIndex1 = 54
jointIndex = RightLeg = 2
jointIndex = RightHandIndex4 = 25
jointIndex = Neck = 62
jointIndex = LeftHandMiddle1 = 50
jointIndex = RightHandPinky3 = 36
jointIndex = LeftHandPinky2 = 43
jointIndex = RightHandMiddle3 = 28
jointIndex = RightHandThumb4 = 21
jointIndex = LeftUpLeg = 6
jointIndex = RightFoot = 3
jointIndex = LeftHandThumb1 = 58
jointIndex = LeftArm = 39
jointIndex = RightHandMiddle1 = 26
jointIndex = LeftHandRing3 = 48
jointIndex = LeftHandMiddle4 = 53
jointIndex = RightUpLeg = 1
jointIndex = RightHandMiddle2 = 27
jointIndex = LeftToeBase = 9
jointIndex = RightHandThumb2 = 19
jointIndex = Spine2 = 13
jointIndex = Spine = 11
jointIndex = LeftHandRing4 = 49
jointIndex = Head = 63
jointIndex = LeftHandPinky3 = 44
bs = EyeBlink_L = blink = 1
bs = JawOpen = mouth_Open = 1
bs = LipsFunnel = Oo = 1
bs = BrowsU_L = brow_Up = 1
jointIndex = RightHandIndex2 = 27
jointIndex = LeftHandIndex2 = 51
jointIndex = RightUpLeg = 6
jointIndex = RightToe_End = 10
jointIndex = RightEye = 65
jointIndex = LeftHandPinky1 = 42
jointIndex = RightHandThumb1 = 18
jointIndex = LeftHandIndex4 = 57
jointIndex = LeftHandMiddle3 = 52
jointIndex = RightHandIndex3 = 24
jointIndex = Spine1 = 12
jointIndex = RightHandRing1 = 22
jointIndex = face = 67
jointIndex = LeftUpLeg = 1
jointIndex = LeftHand = 41
jointIndex = LeftHandMiddle1 = 58
jointIndex = LeftHandIndex1 = 50
jointIndex = LeftEye = 64
jointIndex = RightHandIndex1 = 26
jointIndex = LeftHandPinky4 = 45
jointIndex = RightArm = 15
jointIndex = RightHandThumb3 = 20
jointIndex = LeftShoulder = 38
jointIndex = RightHandPinky2 = 19
jointIndex = RightHandThumb1 = 30
jointIndex = RightForeArm = 16
jointIndex = LeftHandMiddle3 = 60
jointIndex = Neck = 62
jointIndex = LeftHandThumb1 = 54
jointIndex = RightHandMiddle2 = 35
jointIndex = LeftHandMiddle4 = 61
jointIndex = mannequin = 68
jointIndex = Spine1 = 12
jointIndex = RightFoot = 8
jointIndex = RightHand = 17
jointIndex = LeftHandIndex3 = 52
jointIndex = RightHandIndex3 = 28
jointIndex = RightHandMiddle4 = 37
jointIndex = LeftLeg = 2
jointIndex = RightHandMiddle1 = 34
jointIndex = Spine2 = 13
jointIndex = LeftHandMiddle2 = 59
jointIndex = LeftHandPinky3 = 44
jointIndex = LeftHandThumb3 = 56
jointIndex = LeftHandRing4 = 49
jointIndex = RightHandThumb2 = 31
jointIndex = LeftHandRing3 = 48
jointIndex = HeadTop_End = 66
jointIndex = LeftHandThumb4 = 57
jointIndex = RightHandThumb3 = 32
jointIndex = RightHandPinky1 = 18
jointIndex = RightLeg = 7
jointIndex = RightHandMiddle3 = 36
jointIndex = RightHandPinky3 = 20
jointIndex = LeftToeBase = 4
jointIndex = LeftForeArm = 40
jointIndex = RightShoulder = 14
jointIndex = LeftHandRing2 = 47
jointIndex = LeftHandThumb2 = 55
jointIndex = Head = 63
jointIndex = RightHandRing4 = 25
jointIndex = LeftHandRing1 = 46
jointIndex = LeftFoot = 3
jointIndex = RightHandRing3 = 24
jointIndex = RightHandThumb4 = 33
jointIndex = LeftArm = 39
jointIndex = LeftToe_End = 5
jointIndex = RightToeBase = 9
jointIndex = RightHandPinky4 = 21
jointIndex = Spine = 11
jointIndex = LeftHandIndex4 = 53
jointIndex = LeftHandPinky2 = 43
jointIndex = RightHandIndex4 = 29
jointIndex = Hips = 0
jointIndex = RightHandRing2 = 23

Binary file not shown.

View file

@ -177,7 +177,7 @@ ScrollingWindow {
SHAPE_TYPE_STATIC_MESH
],
checkStateOnDisable: false,
warningOnDisable: "Models with 'Exact' automatic collisions cannot be dynamic"
warningOnDisable: "Models with 'Exact' automatic collisions cannot be dynamic, and should not be used as floors"
}
});

View file

@ -12,84 +12,21 @@ import QtQuick.Controls 1.3
import QtGraphicalEffects 1.0
import Qt.labs.settings 1.0
import "./hifi/audio" as HifiAudio
Hifi.AvatarInputs {
id: root
id: root;
objectName: "AvatarInputs"
width: rootWidth
height: controls.height
x: 10; y: 5
width: audio.width;
height: audio.height;
x: 10; y: 5;
readonly property int rootWidth: 265
readonly property int iconSize: 24
readonly property int iconPadding: 5
readonly property bool shouldReposition: true;
readonly property bool shouldReposition: true
Settings {
category: "Overlay.AvatarInputs"
property alias x: root.x
property alias y: root.y
}
MouseArea {
id: hover
hoverEnabled: true
drag.target: parent
anchors.fill: parent
}
Item {
id: controls
width: root.rootWidth
height: 44
visible: root.showAudioTools
Rectangle {
anchors.fill: parent
color: "#00000000"
Item {
id: audioMeter
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: root.iconPadding
anchors.right: parent.right
anchors.rightMargin: root.iconPadding
height: 8
Rectangle {
id: blueRect
color: "blue"
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
width: parent.width / 4
}
Rectangle {
id: greenRect
color: "green"
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: blueRect.right
anchors.right: redRect.left
}
Rectangle {
id: redRect
color: "red"
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
width: parent.width / 5
}
Rectangle {
z: 100
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
width: (1.0 - root.audioLevel) * parent.width
color: "black"
}
}
}
HifiAudio.MicBar {
id: audio;
visible: root.showAudioTools;
standalone: true;
dragTarget: parent;
}
}

View file

@ -2,7 +2,6 @@ import QtQuick 2.5
import QtQuick.Controls 1.2
import QtWebChannel 1.0
import QtWebEngine 1.2
import FileTypeProfile 1.0
import "controls-uit"
import "styles" as HifiStyles
@ -22,8 +21,6 @@ ScrollingWindow {
property alias url: webview.url
property alias webView: webview
property alias eventBridge: eventBridgeWrapper.eventBridge
signal loadingChanged(int status)
x: 100
@ -209,21 +206,7 @@ ScrollingWindow {
WebView {
id: webview
url: "https://highfidelity.com/"
property alias eventBridgeWrapper: eventBridgeWrapper
QtObject {
id: eventBridgeWrapper
WebChannel.id: "eventBridgeWrapper"
property var eventBridge;
}
profile: FileTypeProfile {
id: webviewProfile
storageName: "qmlWebEngine"
}
webChannel.registeredObjects: [eventBridgeWrapper]
profile: FileTypeProfile;
// Create a global EventBridge object for raiseAndLowerKeyboard.
WebEngineScript {
@ -271,6 +254,8 @@ ScrollingWindow {
}
Component.onCompleted: {
webChannel.registerObject("eventBridge", eventBridge);
webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper);
desktop.initWebviewProfileHandlers(webview.profile);
}
}

View file

@ -26,15 +26,8 @@ Windows.ScrollingWindow {
// Don't destroy on close... otherwise the JS/C++ will have a dangling pointer
destroyOnCloseButton: false
property alias source: webview.url
property alias eventBridge: eventBridgeWrapper.eventBridge;
property alias scriptUrl: webview.userScriptUrl
QtObject {
id: eventBridgeWrapper
WebChannel.id: "eventBridgeWrapper"
property var eventBridge;
}
// This is for JS/QML communication, which is unused in a WebWindow,
// but not having this here results in spurious warnings about a
// missing signal
@ -70,7 +63,6 @@ Windows.ScrollingWindow {
url: "about:blank"
anchors.fill: parent
focus: true
webChannel.registeredObjects: [eventBridgeWrapper]
property string userScriptUrl: ""
@ -107,6 +99,8 @@ Windows.ScrollingWindow {
}
Component.onCompleted: {
webChannel.registerObject("eventBridge", eventBridge);
webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper);
eventBridge.webEventReceived.connect(onWebEventReceived);
}
}

View file

@ -30,15 +30,6 @@ Windows.Window {
property bool keyboardRaised: false
property bool punctuationMode: false
// JavaScript event bridge object in case QML content includes Web content.
property alias eventBridge: eventBridgeWrapper.eventBridge;
QtObject {
id: eventBridgeWrapper
WebChannel.id: "eventBridgeWrapper"
property var eventBridge;
}
onSourceChanged: {
if (dynamicContent) {
dynamicContent.destroy();
@ -74,6 +65,12 @@ Windows.Window {
root.dynamicContent.fromScript(message);
}
}
function clearDebugWindow() {
if (root.dynamicContent && root.dynamicContent.clearWindow) {
root.dynamicContent.clearWindow();
}
}
// Handle message traffic from our loaded QML to the script that launched us
signal sendToScript(var message);

View file

@ -8,7 +8,6 @@ import "controls-uit" as HifiControls
import "styles" as HifiStyles
import "styles-uit"
import "windows"
import HFTabletWebEngineProfile 1.0
Item {
id: root
@ -19,7 +18,6 @@ Item {
property variant permissionsBar: {'securityOrigin':'none','feature':'none'}
property alias url: webview.url
property WebEngineView webView: webview
property alias eventBridge: eventBridgeWrapper.eventBridge
property bool canGoBack: webview.canGoBack
property bool canGoForward: webview.canGoForward
@ -33,12 +31,6 @@ Item {
webview.profile = profile;
}
QtObject {
id: eventBridgeWrapper
WebChannel.id: "eventBridgeWrapper"
property var eventBridge;
}
WebEngineView {
id: webview
objectName: "webEngineView"
@ -47,10 +39,7 @@ Item {
width: parent.width
height: keyboardEnabled && keyboardRaised ? parent.height - keyboard.height : parent.height
profile: HFTabletWebEngineProfile {
id: webviewTabletProfile
storageName: "qmlTabletWebEngine"
}
profile: HFTabletWebEngineProfile;
property string userScriptUrl: ""
@ -82,9 +71,10 @@ Item {
property string newUrl: ""
webChannel.registeredObjects: [eventBridgeWrapper]
Component.onCompleted: {
webChannel.registerObject("eventBridge", eventBridge);
webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper);
// Ensure the JS from the web-engine makes it to our logging
webview.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) {
console.log("Web Entity JS message: " + sourceID + " " + lineNumber + " " + message);

View file

@ -79,15 +79,11 @@ ScrollingWindow {
id: webView
anchors.fill: parent
enabled: false
property alias eventBridgeWrapper: eventBridgeWrapper
QtObject {
id: eventBridgeWrapper
WebChannel.id: "eventBridgeWrapper"
property var eventBridge
Component.onCompleted: {
webChannel.registerObject("eventBridge", eventBridge);
webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper);
}
webChannel.registeredObjects: [eventBridgeWrapper]
onEnabledChanged: toolWindow.updateVisiblity()
}
}
@ -251,12 +247,9 @@ ScrollingWindow {
tab.enabled = true;
tab.originalUrl = properties.source;
var eventBridge = properties.eventBridge;
var result = tab.item;
result.enabled = true;
tabView.tabCount++;
result.eventBridgeWrapper.eventBridge = eventBridge;
result.url = properties.source;
return result;
}

View file

@ -10,13 +10,10 @@
import QtQuick 2.5
import QtWebEngine 1.2
import HFWebEngineProfile 1.0
WebEngineView {
id: root
// profile: desktop.browserProfile
Component.onCompleted: {
console.log("Connecting JS messaging to Hifi Logging")
// Ensure the JS from the web-engine makes it to our logging

View file

@ -18,10 +18,12 @@ Original.CheckBox {
id: checkBox
property int colorScheme: hifi.colorSchemes.light
property string color: hifi.colors.lightGray
readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light
property bool isRedCheck: false
property int boxSize: 14
readonly property int boxRadius: 3
property int boxRadius: 3
property bool wrap: true;
readonly property int checkSize: Math.max(boxSize - 8, 10)
readonly property int checkRadius: 2
activeFocusOnPress: true
@ -89,9 +91,10 @@ Original.CheckBox {
label: Label {
text: control.text
colorScheme: checkBox.colorScheme
color: control.color
x: 2
wrapMode: Text.Wrap
wrapMode: checkBox.wrap ? Text.Wrap : Text.NoWrap
elide: checkBox.wrap ? Text.ElideNone : Text.ElideRight
enabled: checkBox.enabled
}
}

View file

@ -20,6 +20,7 @@ FocusScope {
HifiConstants { id: hifi }
property alias model: comboBox.model;
property alias editable: comboBox.editable
property alias comboBox: comboBox
readonly property alias currentText: comboBox.currentText;
property alias currentIndex: comboBox.currentIndex;

View file

@ -2,12 +2,10 @@ import QtQuick 2.5
import QtWebEngine 1.1
import QtWebChannel 1.0
import "../controls-uit" as HiFiControls
import HFTabletWebEngineProfile 1.0
Item {
property alias url: root.url
property alias scriptURL: root.userScriptUrl
property alias eventBridge: eventBridgeWrapper.eventBridge
property alias canGoBack: root.canGoBack;
property var goBack: root.goBack;
property alias urlTag: root.urlTag
@ -23,12 +21,6 @@ Item {
}
*/
QtObject {
id: eventBridgeWrapper
WebChannel.id: "eventBridgeWrapper"
property var eventBridge;
}
property alias viewProfile: root.profile
WebEngineView {
@ -39,10 +31,7 @@ Item {
width: parent.width
height: keyboardEnabled && keyboardRaised ? parent.height - keyboard.height : parent.height
profile: HFTabletWebEngineProfile {
id: webviewProfile
storageName: "qmlTabletWebEngine"
}
profile: HFTabletWebEngineProfile;
property string userScriptUrl: ""
@ -75,10 +64,11 @@ Item {
userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard, userScript ]
property string newUrl: ""
webChannel.registeredObjects: [eventBridgeWrapper]
Component.onCompleted: {
webChannel.registerObject("eventBridge", eventBridge);
webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper);
// Ensure the JS from the web-engine makes it to our logging
root.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) {
console.log("Web Entity JS message: " + sourceID + " " + lineNumber + " " + message);

View file

@ -2,7 +2,6 @@ import QtQuick 2.5
import QtQuick.Controls 1.4
import QtWebEngine 1.2
import QtWebChannel 1.0
import HFTabletWebEngineProfile 1.0
import "../controls-uit" as HiFiControls
import "../styles" as HifiStyles
import "../styles-uit"
@ -18,7 +17,6 @@ Item {
property int headerHeight: 70
property string url
property string scriptURL
property alias eventBridge: eventBridgeWrapper.eventBridge
property bool keyboardEnabled: false
property bool keyboardRaised: false
property bool punctuationMode: false
@ -136,12 +134,6 @@ Item {
loadUrl(url);
}
QtObject {
id: eventBridgeWrapper
WebChannel.id: "eventBridgeWrapper"
property var eventBridge;
}
WebEngineView {
id: webview
objectName: "webEngineView"
@ -150,10 +142,7 @@ Item {
width: parent.width
height: keyboardEnabled && keyboardRaised ? parent.height - keyboard.height - web.headerHeight : parent.height - web.headerHeight
anchors.top: buttons.bottom
profile: HFTabletWebEngineProfile {
id: webviewTabletProfile
storageName: "qmlTabletWebEngine"
}
profile: HFTabletWebEngineProfile;
property string userScriptUrl: ""
@ -186,9 +175,9 @@ Item {
property string newUrl: ""
webChannel.registeredObjects: [eventBridgeWrapper]
Component.onCompleted: {
webChannel.registerObject("eventBridge", eventBridge);
webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper);
// Ensure the JS from the web-engine makes it to our logging
webview.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) {
console.log("Web Entity JS message: " + sourceID + " " + lineNumber + " " + message);

View file

@ -10,13 +10,9 @@
import QtQuick 2.5
import "."
import FileTypeProfile 1.0
WebView {
viewProfile: FileTypeProfile {
id: webviewProfile
storageName: "qmlWebEngine"
}
viewProfile: FileTypeProfile;
urlTag: "noDownload=true";
}

View file

@ -2,12 +2,10 @@ import QtQuick 2.5
import QtWebEngine 1.1
import QtWebChannel 1.0
import "../controls-uit" as HiFiControls
import HFWebEngineProfile 1.0
Item {
property alias url: root.url
property alias scriptURL: root.userScriptUrl
property alias eventBridge: eventBridgeWrapper.eventBridge
property alias canGoBack: root.canGoBack;
property var goBack: root.goBack;
property alias urlTag: root.urlTag
@ -23,12 +21,6 @@ Item {
}
*/
QtObject {
id: eventBridgeWrapper
WebChannel.id: "eventBridgeWrapper"
property var eventBridge;
}
property alias viewProfile: root.profile
WebEngineView {
@ -39,10 +31,7 @@ Item {
width: parent.width
height: keyboardEnabled && keyboardRaised ? parent.height - keyboard.height : parent.height
profile: HFWebEngineProfile {
id: webviewProfile
storageName: "qmlWebEngine"
}
profile: HFWebEngineProfile;
property string userScriptUrl: ""
@ -76,9 +65,9 @@ Item {
property string newUrl: ""
webChannel.registeredObjects: [eventBridgeWrapper]
Component.onCompleted: {
webChannel.registerObject("eventBridge", eventBridge);
webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper);
// Ensure the JS from the web-engine makes it to our logging
root.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) {
console.log("Web Entity JS message: " + sourceID + " " + lineNumber + " " + message);

View file

@ -353,6 +353,14 @@ FocusScope {
showDesktop();
}
function ensureTitleBarVisible(targetWindow) {
// Reposition window to ensure that title bar is vertically inside window.
if (targetWindow.frame && targetWindow.frame.decoration) {
var topMargin = -targetWindow.frame.decoration.anchors.topMargin; // Frame's topMargin is a negative value.
targetWindow.y = Math.max(targetWindow.y, topMargin);
}
}
function centerOnVisible(item) {
var targetWindow = d.getDesktopWindow(item);
if (!targetWindow) {
@ -375,11 +383,12 @@ FocusScope {
targetWindow.x = newX;
targetWindow.y = newY;
ensureTitleBarVisible(targetWindow);
// If we've noticed that our recommended desktop rect has changed, record that change here.
if (recommendedRect != newRecommendedRect) {
recommendedRect = newRecommendedRect;
}
}
function repositionOnVisible(item) {
@ -394,7 +403,6 @@ FocusScope {
return;
}
var oldRecommendedRect = recommendedRect;
var oldRecommendedDimmensions = { x: oldRecommendedRect.width, y: oldRecommendedRect.height };
var newRecommendedRect = Controller.getRecommendedOverlayRect();
@ -426,7 +434,6 @@ FocusScope {
newPosition.y = -1
}
if (newPosition.x === -1 && newPosition.y === -1) {
var originRelativeX = (targetWindow.x - oldRecommendedRect.x);
var originRelativeY = (targetWindow.y - oldRecommendedRect.y);
@ -444,6 +451,8 @@ FocusScope {
}
targetWindow.x = newPosition.x;
targetWindow.y = newPosition.y;
ensureTitleBarVisible(targetWindow);
}
Component { id: messageDialogBuilder; MessageDialog { } }

View file

@ -20,7 +20,6 @@ TabletModalWindow {
id: loginDialogRoot
objectName: "LoginDialog"
property var eventBridge;
signal sendToScript(var message);
property bool isHMD: false
property bool gotoPreviousApp: false;

View file

@ -24,8 +24,6 @@ Window {
resizable: true
modality: Qt.ApplicationModal
property alias eventBridge: eventBridgeWrapper.eventBridge
Item {
anchors.fill: parent
@ -45,16 +43,6 @@ Window {
bottom: keyboard.top
}
property alias eventBridgeWrapper: eventBridgeWrapper
QtObject {
id: eventBridgeWrapper
WebChannel.id: "eventBridgeWrapper"
property var eventBridge;
}
webChannel.registeredObjects: [eventBridgeWrapper]
// Create a global EventBridge object for raiseAndLowerKeyboard.
WebEngineScript {
id: createGlobalEventBridge
@ -73,6 +61,10 @@ Window {
userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard ]
Component.onCompleted: {
webChannel.registerObject("eventBridge", eventBridge);
webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper);
}
}
Keyboard {

View file

@ -116,9 +116,7 @@ Preference {
Component {
id: tabletAvatarBrowserBuilder;
TabletAvatarBrowser {
eventBridge: tabletRoot.eventBridge
}
TabletAvatarBrowser { }
}
}

View file

@ -1,256 +0,0 @@
//
// Audio.qml
// qml/hifi
//
// Audio setup
//
// Created by Vlad Stelmahovsky on 03/22/2017
// Copyright 2017 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
//
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtGraphicalEffects 1.0
import "../styles-uit"
import "../controls-uit" as HifiControls
import "components"
Rectangle {
id: audio;
//put info text here
property alias infoText: infoArea.text
color: "#404040";
HifiConstants { id: hifi; }
objectName: "AudioWindow"
property var eventBridge;
property string title: "Audio Options"
signal sendToScript(var message);
Component {
id: separator
LinearGradient {
start: Qt.point(0, 0)
end: Qt.point(0, 4)
gradient: Gradient {
GradientStop { position: 0.0; color: "#303030" }
GradientStop { position: 0.33; color: "#252525" } // Equivalent of darkGray0 over baseGray background.
GradientStop { position: 0.5; color: "#303030" }
GradientStop { position: 0.6; color: "#454a49" }
GradientStop { position: 1.0; color: "#454a49" }
}
cached: true
}
}
Column {
anchors { left: parent.left; right: parent.right }
spacing: 8
RalewayRegular {
anchors { left: parent.left; right: parent.right; leftMargin: 30 }
height: 45
size: 20
color: "white"
text: audio.title
}
Loader {
width: parent.width
height: 5
sourceComponent: separator
}
//connections required to syncronize with Menu
Connections {
target: AudioDevice
onMuteToggled: {
audioMute.checkbox.checked = AudioDevice.getMuted()
}
}
Connections {
target: AvatarInputs !== undefined ? AvatarInputs : null
onShowAudioToolsChanged: {
audioTools.checkbox.checked = showAudioTools
}
}
AudioCheckbox {
id: audioMute
width: parent.width
anchors { left: parent.left; right: parent.right; leftMargin: 30 }
checkbox.checked: AudioDevice.muted
text.text: qsTr("Mute microphone")
onCheckBoxClicked: {
AudioDevice.muted = checked
}
}
AudioCheckbox {
id: audioTools
width: parent.width
anchors { left: parent.left; right: parent.right; leftMargin: 30 }
checkbox.checked: AvatarInputs !== undefined ? AvatarInputs.showAudioTools : false
text.text: qsTr("Show audio level meter")
onCheckBoxClicked: {
if (AvatarInputs !== undefined) {
AvatarInputs.showAudioTools = checked
}
}
}
Loader {
width: parent.width
height: 5
sourceComponent: separator
}
Row {
anchors { left: parent.left; right: parent.right; leftMargin: 30 }
height: 40
spacing: 8
HiFiGlyphs {
text: hifi.glyphs.mic
color: hifi.colors.primaryHighlight
anchors.verticalCenter: parent.verticalCenter
size: 32
}
RalewayRegular {
anchors.verticalCenter: parent.verticalCenter
size: 16
color: "#AFAFAF"
text: qsTr("CHOOSE INPUT DEVICE")
}
}
ListView {
id: inputAudioListView
anchors { left: parent.left; right: parent.right; leftMargin: 70 }
height: 125
spacing: 0
clip: true
snapMode: ListView.SnapToItem
model: AudioDevice
delegate: Item {
width: parent.width
visible: devicemode === 0
height: visible ? 36 : 0
AudioCheckbox {
id: cbin
anchors.verticalCenter: parent.verticalCenter
Binding {
target: cbin.checkbox
property: 'checked'
value: devicechecked
}
width: parent.width
cbchecked: devicechecked
text.text: devicename
onCheckBoxClicked: {
if (checked) {
AudioDevice.setInputDeviceAsync(devicename)
}
}
}
}
}
Loader {
width: parent.width
height: 5
sourceComponent: separator
}
Row {
anchors { left: parent.left; right: parent.right; leftMargin: 30 }
height: 40
spacing: 8
HiFiGlyphs {
text: hifi.glyphs.unmuted
color: hifi.colors.primaryHighlight
anchors.verticalCenter: parent.verticalCenter
size: 32
}
RalewayRegular {
anchors.verticalCenter: parent.verticalCenter
size: 16
color: "#AFAFAF"
text: qsTr("CHOOSE OUTPUT DEVICE")
}
}
ListView {
id: outputAudioListView
anchors { left: parent.left; right: parent.right; leftMargin: 70 }
height: 250
spacing: 0
clip: true
snapMode: ListView.SnapToItem
model: AudioDevice
delegate: Item {
width: parent.width
visible: devicemode === 1
height: visible ? 36 : 0
AudioCheckbox {
id: cbout
width: parent.width
anchors.verticalCenter: parent.verticalCenter
Binding {
target: cbout.checkbox
property: 'checked'
value: devicechecked
}
text.text: devicename
onCheckBoxClicked: {
if (checked) {
AudioDevice.setOutputDeviceAsync(devicename)
}
}
}
}
}
Loader {
id: lastSeparator
width: parent.width
height: 6
sourceComponent: separator
}
Row {
anchors { left: parent.left; right: parent.right; leftMargin: 30 }
height: 40
spacing: 8
HiFiGlyphs {
id: infoSign
text: hifi.glyphs.info
color: "#AFAFAF"
anchors.verticalCenter: parent.verticalCenter
size: 60
}
RalewayRegular {
id: infoArea
width: parent.width - infoSign.implicitWidth - parent.spacing - 10
wrapMode: Text.WordWrap
anchors.verticalCenter: parent.verticalCenter
size: 12
color: hifi.colors.baseGrayHighlight
}
}
}
}

View file

@ -2,7 +2,6 @@ import QtQuick 2.5
import QtQuick.Controls 1.4
import QtWebEngine 1.1;
import Qt.labs.settings 1.0
import HFWebEngineProfile 1.0
import "../desktop" as OriginalDesktop
import ".."
@ -27,11 +26,6 @@ OriginalDesktop.Desktop {
property alias toolWindow: toolWindow
ToolWindow { id: toolWindow }
property var browserProfile: HFWebEngineProfile {
id: webviewProfile
storageName: "qmlWebEngine"
}
Action {
text: "Open Browser"
shortcut: "Ctrl+B"

View file

@ -590,14 +590,11 @@ Item {
console.log("This avatar is no longer present. goToUserInDomain() failed.");
return;
}
var vector = Vec3.subtract(avatar.position, MyAvatar.position);
var distance = Vec3.length(vector);
var target = Vec3.multiply(Vec3.normalize(vector), distance - 2.0);
// FIXME: We would like the avatar to recompute the avatar's "maybe fly" test at the new position, so that if high enough up,
// the avatar goes into fly mode rather than falling. However, that is not exposed to Javascript right now.
// FIXME: it would be nice if this used the same teleport steps and smoothing as in the teleport.js script.
// Note, however, that this script allows teleporting to a person in the air, while teleport.js is going to a grounded target.
MyAvatar.orientation = Quat.lookAtSimple(MyAvatar.position, avatar.position);
MyAvatar.position = Vec3.sum(MyAvatar.position, target);
MyAvatar.position = Vec3.sum(avatar.position, Vec3.multiplyQbyV(avatar.orientation, {x: 0, y: 0, z: -2}));
MyAvatar.orientation = Quat.multiply(avatar.orientation, {y: 1});
}
}

View file

@ -44,7 +44,6 @@ Rectangle {
property var activeTab: "nearbyTab";
property bool currentlyEditingDisplayName: false
property bool punctuationMode: false;
property var eventBridge;
HifiConstants { id: hifi; }
@ -129,7 +128,7 @@ Rectangle {
pal.sendToScript({method: 'refreshNearby', params: params});
}
Item {
Rectangle {
id: palTabContainer;
// Anchors
anchors {
@ -138,6 +137,7 @@ Rectangle {
left: parent.left;
right: parent.right;
}
color: "white";
Rectangle {
id: tabSelectorContainer;
// Anchors
@ -388,8 +388,13 @@ Rectangle {
sortIndicatorColumn: settings.nearbySortIndicatorColumn;
sortIndicatorOrder: settings.nearbySortIndicatorOrder;
onSortIndicatorColumnChanged: {
settings.nearbySortIndicatorColumn = sortIndicatorColumn;
sortModel();
if (sortIndicatorColumn > 2) {
// these are not sortable, switch back to last column
sortIndicatorColumn = settings.nearbySortIndicatorColumn;
} else {
settings.nearbySortIndicatorColumn = sortIndicatorColumn;
sortModel();
}
}
onSortIndicatorOrderChanged: {
settings.nearbySortIndicatorOrder = sortIndicatorOrder;
@ -1038,7 +1043,6 @@ Rectangle {
} // Keyboard
HifiControls.TabletWebView {
eventBridge: pal.eventBridge;
id: userInfoViewer;
anchors {
top: parent.top;

View file

@ -0,0 +1,188 @@
//
// Audio.qml
// qml/hifi/audio
//
// Audio setup
//
// Created by Vlad Stelmahovsky on 03/22/2017
// Copyright 2017 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
//
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtQuick.Layouts 1.3
import "../../styles-uit"
import "../../controls-uit" as HifiControls
import "../../windows"
import "./" as AudioControls
Rectangle {
id: root;
HifiConstants { id: hifi; }
property var eventBridge;
property string title: "Audio Settings - " + Audio.context;
signal sendToScript(var message);
color: hifi.colors.baseGray;
// only show the title if loaded through a "loader"
function showTitle() {
return root.parent.objectName == "loader";
}
Column {
y: 16; // padding does not work
spacing: 16;
width: parent.width;
RalewayRegular {
x: 16; // padding does not work
size: 16;
color: "white";
text: root.title;
visible: root.showTitle();
}
Separator { visible: root.showTitle() }
ColumnLayout {
x: 16; // padding does not work
spacing: 16;
// mute is in its own row
RowLayout {
AudioControls.CheckBox {
text: qsTr("Mute microphone");
isRedCheck: true;
checked: Audio.muted;
onClicked: {
Audio.muted = checked;
checked = Qt.binding(function() { return Audio.muted; }); // restore binding
}
}
}
RowLayout {
spacing: 16;
AudioControls.CheckBox {
text: qsTr("Enable noise reduction");
checked: Audio.noiseReduction;
onClicked: {
Audio.noiseReduction = checked;
checked = Qt.binding(function() { return Audio.noiseReduction; }); // restore binding
}
}
AudioControls.CheckBox {
text: qsTr("Show audio level meter");
checked: AvatarInputs.showAudioTools;
onClicked: {
AvatarInputs.showAudioTools = checked;
checked = Qt.binding(function() { return AvatarInputs.showAudioTools; }); // restore binding
}
}
}
}
Separator {}
RowLayout {
HiFiGlyphs {
text: hifi.glyphs.mic;
color: hifi.colors.primaryHighlight;
anchors.verticalCenter: parent.verticalCenter;
size: 28;
}
RalewayRegular {
anchors.verticalCenter: parent.verticalCenter;
size: 16;
color: hifi.colors.lightGrayText;
text: qsTr("CHOOSE INPUT DEVICE");
}
}
ListView {
anchors { left: parent.left; right: parent.right; leftMargin: 70 }
height: 125;
spacing: 0;
snapMode: ListView.SnapToItem;
clip: true;
model: Audio.devices.input;
delegate: Item {
width: parent.width;
height: 36;
RowLayout {
width: parent.width;
AudioControls.CheckBox {
Layout.maximumWidth: parent.width - level.width - 40;
text: display;
wrap: false;
checked: selected;
onClicked: {
selected = checked;
checked = Qt.binding(function() { return selected; }); // restore binding
}
}
InputLevel {
id: level;
Layout.alignment: Qt.AlignRight;
Layout.rightMargin: 30;
visible: selected;
}
}
}
}
Separator {}
RowLayout {
Column {
RowLayout {
HiFiGlyphs {
text: hifi.glyphs.unmuted;
color: hifi.colors.primaryHighlight;
anchors.verticalCenter: parent.verticalCenter;
size: 36;
}
RalewayRegular {
anchors.verticalCenter: parent.verticalCenter;
size: 16;
color: hifi.colors.lightGrayText;
text: qsTr("CHOOSE OUTPUT DEVICE");
}
}
PlaySampleSound { anchors { left: parent.left; leftMargin: 60 }}
}
}
ListView {
anchors { left: parent.left; right: parent.right; leftMargin: 70 }
height: 125;
spacing: 0;
snapMode: ListView.SnapToItem;
clip: true;
model: Audio.devices.output;
delegate: Item {
width: parent.width;
height: 36;
AudioControls.CheckBox {
text: display;
checked: selected;
onClicked: {
selected = checked;
checked = Qt.binding(function() { return selected; }); // restore binding
}
}
}
}
}
}

View file

@ -0,0 +1,18 @@
//
// CheckBox.qml
// qml/hifi/audio
//
// Created by Zach Pomerantz on 6/12/2017
// Copyright 2017 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
//
import QtQuick 2.5
import "../../controls-uit" as HifiControls
HifiControls.CheckBox {
color: "white"
}

View file

@ -0,0 +1,105 @@
//
// InputLevel.qml
// qml/hifi/audio
//
// Created by Zach Pomerantz on 6/20/2017
// Copyright 2017 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
//
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtQuick.Layouts 1.3
import QtGraphicalEffects 1.0
Rectangle {
readonly property var level: Audio.inputLevel;
width: 70;
height: 8;
color: "transparent";
Item {
id: colors;
readonly property string muted: "#E2334D";
readonly property string gutter: "#575757";
readonly property string greenStart: "#39A38F";
readonly property string greenEnd: "#1FC6A6";
readonly property string red: colors.muted;
}
Text {
id: status;
anchors {
horizontalCenter: parent.horizontalCenter;
verticalCenter: parent.verticalCenter;
}
visible: Audio.muted;
color: colors.muted;
text: "MUTED";
font.pointSize: 10;
}
Item {
id: bar;
width: parent.width;
height: parent.height;
anchors { fill: parent }
visible: !status.visible;
Rectangle { // base
radius: 4;
anchors { fill: parent }
color: colors.gutter;
}
Rectangle { // mask
id: mask;
width: parent.width * level;
radius: 5;
anchors {
bottom: parent.bottom;
bottomMargin: 0;
top: parent.top;
topMargin: 0;
left: parent.left;
leftMargin: 0;
}
}
LinearGradient {
anchors { fill: mask }
source: mask
start: Qt.point(0, 0);
end: Qt.point(70, 0);
gradient: Gradient {
GradientStop {
position: 0;
color: colors.greenStart;
}
GradientStop {
position: 0.8;
color: colors.greenEnd;
}
GradientStop {
position: 0.801;
color: colors.red;
}
GradientStop {
position: 1;
color: colors.red;
}
}
}
}
}

View file

@ -0,0 +1,223 @@
//
// MicBar.qml
// qml/hifi/audio
//
// Created by Zach Pomerantz on 6/14/2017
// Copyright 2017 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
//
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtQuick.Layouts 1.3
import QtGraphicalEffects 1.0
Rectangle {
readonly property var level: Audio.inputLevel;
property bool standalone: false;
property var dragTarget: null;
width: 240;
height: 50;
radius: 5;
color: "#00000000";
border {
width: (standalone || Audio.muted || mouseArea.containsMouse) ? 2 : 0;
color: colors.border;
}
// borders are painted over fill, so reduce the fill to fit inside the border
Rectangle {
color: standalone ? colors.fill : "#00000000";
width: 236;
height: 46;
radius: 5;
anchors {
verticalCenter: parent.verticalCenter;
horizontalCenter: parent.horizontalCenter;
}
}
MouseArea {
id: mouseArea;
anchors {
left: icon.left;
right: bar.right;
top: icon.top;
bottom: icon.bottom;
}
hoverEnabled: true;
scrollGestureEnabled: false;
onClicked: { Audio.muted = !Audio.muted; }
drag.target: dragTarget;
}
Item {
id: colors;
readonly property string unmuted: "#FFF";
readonly property string muted: "#E2334D";
readonly property string gutter: "#575757";
readonly property string greenStart: "#39A38F";
readonly property string greenEnd: "#1FC6A6";
readonly property string red: colors.muted;
readonly property string fill: "#55000000";
readonly property string border: standalone ? "#80FFFFFF" : "#55FFFFFF";
readonly property string icon: (Audio.muted && !mouseArea.containsMouse) ? muted : unmuted;
}
Item {
id: icon;
anchors {
left: parent.left;
leftMargin: 5;
verticalCenter: parent.verticalCenter;
}
width: 40;
height: 40;
Item {
Image {
readonly property string unmutedIcon: "../../../icons/tablet-icons/mic-unmute-i.svg";
readonly property string mutedIcon: "../../../icons/tablet-icons/mic-mute-i.svg";
function exclusiveOr(a, b) { return (a || b) && !(a && b); }
id: image;
source: exclusiveOr(Audio.muted, mouseArea.containsMouse) ? mutedIcon : unmutedIcon;
width: 30;
height: 30;
anchors {
left: parent.left;
leftMargin: 5;
top: parent.top;
topMargin: 5;
}
}
ColorOverlay {
anchors { fill: image }
source: image;
color: colors.icon;
}
}
}
Item {
id: status;
readonly property string color: (Audio.muted && !mouseArea.containsMouse) ? colors.muted : colors.unmuted;
visible: Audio.muted || mouseArea.containsMouse;
anchors {
left: parent.left;
leftMargin: 50;
verticalCenter: parent.verticalCenter;
}
width: 170;
height: 8
Text {
anchors {
horizontalCenter: parent.horizontalCenter;
verticalCenter: parent.verticalCenter;
}
color: parent.color;
text: Audio.muted ? (mouseArea.containsMouse ? "UNMUTE" : "MUTED") : "MUTE";
font.pointSize: 12;
}
Rectangle {
anchors {
left: parent.left;
verticalCenter: parent.verticalCenter;
}
width: 50;
height: 4;
color: parent.color;
}
Rectangle {
anchors {
right: parent.right;
verticalCenter: parent.verticalCenter;
}
width: 50;
height: 4;
color: parent.color;
}
}
Item {
id: bar;
visible: !status.visible;
anchors.fill: status;
width: status.width;
Rectangle { // base
radius: 4;
anchors { fill: parent }
color: colors.gutter;
}
Rectangle { // mask
id: mask;
width: parent.width * level;
radius: 5;
anchors {
bottom: parent.bottom;
bottomMargin: 0;
top: parent.top;
topMargin: 0;
left: parent.left;
leftMargin: 0;
}
}
LinearGradient {
anchors { fill: mask }
source: mask
start: Qt.point(0, 0);
end: Qt.point(170, 0);
gradient: Gradient {
GradientStop {
position: 0;
color: colors.greenStart;
}
GradientStop {
position: 0.8;
color: colors.greenEnd;
}
GradientStop {
position: 0.81;
color: colors.red;
}
GradientStop {
position: 1;
color: colors.red;
}
}
}
}
}

View file

@ -0,0 +1,86 @@
//
// PlaySampleSound.qml
// qml/hifi/audio
//
// Created by Zach Pomerantz on 6/13/2017
// Copyright 2017 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
//
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import QtQuick.Layouts 1.3
import "../../styles-uit"
import "../../controls-uit" as HifiControls
RowLayout {
property var sound: null;
property var sample: null;
property bool isPlaying: false;
function createSampleSound() {
var SOUND = Qt.resolvedUrl("../../../sounds/sample.wav");
sound = SoundCache.getSound(SOUND);
sample = null;
}
function playSound() {
// FIXME: MyAvatar is not properly exposed to QML; MyAvatar.qmlPosition is a stopgap
// FIXME: Audio.playSystemSound should not require position
sample = Audio.playSystemSound(sound, MyAvatar.qmlPosition);
isPlaying = true;
sample.finished.connect(function() { isPlaying = false; sample = null; });
}
function stopSound() {
sample && sample.stop();
}
Component.onCompleted: createSampleSound();
Component.onDestruction: stopSound();
onVisibleChanged: {
if (!visible) {
stopSound();
}
}
HifiConstants { id: hifi; }
Button {
style: ButtonStyle {
background: Rectangle {
implicitWidth: 20;
implicitHeight: 20;
radius: hifi.buttons.radius;
gradient: Gradient {
GradientStop {
position: 0.2;
color: isPlaying ? hifi.buttons.colorStart[hifi.buttons.blue] : hifi.buttons.colorStart[hifi.buttons.black];
}
GradientStop {
position: 1.0;
color: isPlaying ? hifi.buttons.colorFinish[hifi.buttons.blue] : hifi.buttons.colorFinish[hifi.buttons.black];
}
}
}
label: HiFiGlyphs {
// absolutely position due to asymmetry in glyph
x: isPlaying ? 0 : 1;
y: 1;
size: 14;
color: (control.pressed || control.hovered) ? (isPlaying ? "black" : hifi.colors.primaryHighlight) : "white";
text: isPlaying ? hifi.glyphs.stop_square : hifi.glyphs.playback_play;
}
}
onClicked: isPlaying ? stopSound() : playSound();
}
RalewayRegular {
Layout.leftMargin: 2;
size: 14;
color: "white";
text: isPlaying ? qsTr("Stop sample sound") : qsTr("Play sample sound");
}
}

View file

@ -1,30 +0,0 @@
import QtQuick 2.5
import QtQuick.Controls 1.4
import "../../styles-uit"
import "../../controls-uit" as HifiControls
Row {
id: row
spacing: 16
property alias checkbox: cb
property alias cbchecked: cb.checked
property alias text: txt
signal checkBoxClicked(bool checked)
HifiControls.CheckBox {
id: cb
boxSize: 20
colorScheme: hifi.colorSchemes.dark
anchors.verticalCenter: parent.verticalCenter
onClicked: checkBoxClicked(cb.checked)
}
RalewayBold {
id: txt
wrapMode: Text.WordWrap
width: parent.width - cb.boxSize - 30
anchors.verticalCenter: parent.verticalCenter
size: 16
color: "white"
}
}

View file

@ -0,0 +1,27 @@
//
// Audio.qml
//
// Created by Zach Pomerantz on 6/12/2017
// Copyright 2017 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
//
import "../../windows"
import "../audio"
ScrollingWindow {
id: root;
resizable: true;
destroyOnHidden: true;
width: 400;
height: 577;
minSize: Qt.vector2d(400, 500);
Audio { id: audio; width: root.width }
objectName: "AudioDialog";
title: audio.title;
}

View file

@ -5,9 +5,9 @@ import "../../dialogs"
PreferencesDialog {
id: root
objectName: "AudioPreferencesDialog"
objectName: "AudioBuffersDialog"
title: "Audio Settings"
showCategories: ["Audio"]
showCategories: ["Audio Buffers"]
property var settings: Settings {
category: root.objectName
property alias x: root.x
@ -16,4 +16,3 @@ PreferencesDialog {
property alias height: root.height
}
}

View file

@ -51,7 +51,20 @@ ScrollingWindow {
}
function updateRunningScripts() {
function simplify(path) {
// trim URI querystring/fragment
path = (path+'').replace(/[#?].*$/,'');
// normalize separators and grab last path segment (ie: just the filename)
path = path.replace(/\\/g, '/').split('/').pop();
// return lowercased because we want to sort mnemonically
return path.toLowerCase();
}
var runningScripts = ScriptDiscoveryService.getRunning();
runningScripts.sort(function(a,b) {
a = simplify(a.path);
b = simplify(b.path);
return a < b ? -1 : a > b ? 1 : 0;
});
runningScriptsModel.clear()
for (var i = 0; i < runningScripts.length; ++i) {
runningScriptsModel.append(runningScripts[i]);

View file

@ -24,7 +24,6 @@ Rectangle {
property string title: "Asset Browser"
property bool keyboardRaised: false
property var eventBridge;
signal sendToScript(var message);
property bool isHMD: false
@ -179,7 +178,7 @@ Rectangle {
SHAPE_TYPE_STATIC_MESH
],
checkStateOnDisable: false,
warningOnDisable: "Models with 'Exact' automatic collisions cannot be dynamic"
warningOnDisable: "Models with 'Exact' automatic collisions cannot be dynamic, and should not be used as floors"
}
});

View file

@ -20,7 +20,6 @@ Rectangle {
id: root
objectName: "DCConectionTiming"
property var eventBridge;
signal sendToScript(var message);
property bool isHMD: false

View file

@ -19,7 +19,6 @@ Rectangle {
id: root
objectName: "DebugWindow"
property var eventBridge;
property var title: "Debug Window"
property bool isHMD: false

View file

@ -20,7 +20,6 @@ Rectangle {
id: root
objectName: "EntityStatistics"
property var eventBridge;
signal sendToScript(var message);
property bool isHMD: false

View file

@ -20,7 +20,6 @@ Rectangle {
id: root
objectName: "LODTools"
property var eventBridge;
signal sendToScript(var message);
property bool isHMD: false

View file

@ -23,7 +23,6 @@ Rectangle {
property string title: "Running Scripts"
HifiConstants { id: hifi }
signal sendToScript(var message);
property var eventBridge;
property var scripts: ScriptDiscoveryService;
property var scriptsModel: scripts.scriptsModelFilter
property var runningScriptsModel: ListModel { }

View file

@ -0,0 +1,213 @@
//
// Created by Dante Ruiz on 6/1/17.
// Copyright 2017 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
//
import QtQuick 2.5
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtGraphicalEffects 1.0
import QtQuick.Controls.Styles 1.4
import "../../styles-uit"
import "../../controls"
import "../../controls-uit" as HifiControls
Rectangle {
id: info
signal canceled()
signal restart()
property int count: 3
property string calibratingText: "CALIBRATING..."
property string calibratingCountText: "CALIBRATION STARTING IN"
property string calibrationSuccess: "CALIBRATION COMPLETED"
property string calibrationFailed: "CALIBRATION FAILED"
HifiConstants { id: hifi }
visible: true
color: hifi.colors.baseGray
property string whiteIndicator: "../../../images/loader-calibrate-white.png"
property string blueIndicator: "../../../images/loader-calibrate-blue.png"
Image {
id: busyIndicator
width: 350
height: 350
property bool running: true
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.top
topMargin: 60
}
visible: busyIndicator.running
source: blueIndicator
NumberAnimation on rotation {
id: busyRotation
running: busyIndicator.running
loops: Animation.Infinite
duration: 1000
from: 0 ; to: 360
}
}
HiFiGlyphs {
id: image
text: hifi.glyphs.avatar1
size: 190
color: hifi.colors.white
anchors {
top: busyIndicator.top
topMargin: 40
horizontalCenter: busyIndicator.horizontalCenter
}
}
RalewayBold {
id: statusText
text: info.calibratingCountText
size: 16
color: hifi.colors.blueHighlight
anchors {
top: image.bottom
topMargin: 15
horizontalCenter: image.horizontalCenter
}
}
RalewayBold {
id: countDown
text: info.count
color: hifi.colors.blueHighlight
anchors {
top: statusText.bottom
topMargin: 12
horizontalCenter: statusText.horizontalCenter
}
}
RalewayBold {
id: directions
anchors {
top: busyIndicator.bottom
topMargin: 100
horizontalCenter: parent.horizontalCenter
}
color: hifi.colors.white
size: hifi.fontSizes.rootMenuDisclosure
text: "Please stand in a T-Pose during calibration"
}
NumberAnimation {
id: numberAnimation
target: info
property: "count"
to: 0
}
Timer {
id: timer
repeat: false
interval: 3000
onTriggered: {
info.calibrating();
}
}
Timer {
id: closeWindow
repeat: false
interval: 3000
onTriggered: stack.pop();
}
Row {
spacing: 20
anchors {
top: directions.bottom
topMargin: 30
horizontalCenter: parent.horizontalCenter
}
HifiControls.Button {
width: 120
height: 30
color: hifi.buttons.red
text: "RESTART"
onClicked: {
restart();
numberAnimation.stop();
info.count = (timer.interval / 1000);
numberAnimation.start();
timer.restart();
}
}
HifiControls.Button {
width: 120
height: 30
color: hifi.buttons.black
text: "CANCEL"
onClicked: {
canceled();
stack.pop()
}
}
}
function start(interval, countNumber) {
countDown.visible = true;
statusText.color = hifi.colors.blueHighlight;
numberAnimation.duration = interval
info.count = countNumber;
timer.interval = interval;
numberAnimation.start();
timer.start();
}
function calibrating() {
countDown.visible = false;
busyIndicator.source = whiteIndicator;
busyRotation.from = 360
busyRotation.to = 0
statusText.text = info.calibratingText;
statusText.color = hifi.colors.white
}
function success() {
busyIndicator.running = false;
statusText.text = info.calibrationSuccess
statusText.color = hifi.colors.greenHighlight
closeWindow.start();
}
function failure() {
busyIndicator.running = false;
statusText.text = info.calibrationFailed
statusText.color = hifi.colors.redHighlight
closeWindow.start();
}
}

Some files were not shown because too many files have changed in this diff Show more