mirror of
https://github.com/overte-org/overte.git
synced 2025-04-22 01:44:07 +02:00
Merge branch 'master' of github.com:highfidelity/hifi into fix-linux-fbx-lib
This commit is contained in:
commit
5277a0803c
136 changed files with 5018 additions and 1518 deletions
assignment-client/src
domain-server
resources
src
interface
resources
fonts
icons/tablet-icons
images
qml
sounds
src
libraries
audio-client/src
audio/src
avatars/src
entities-renderer/src
entities/src
model-networking/src/model-networking
networking/src
AssetClient.cppAssetClient.hAssetRequest.cppLimitedNodeList.cppLimitedNodeList.hLocationScriptingInterface.cppLocationScriptingInterface.hNLPacket.hNLPacketList.hNetworkPeer.hNode.cppNode.hNodeList.cppNodeType.hReceivedMessage.cppReceivedMessage.hThreadedAssignment.cppThreadedAssignment.h
udt
physics/src
plugins/src/plugins
render-utils/src
render/src/task
script-engine/src
|
@ -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>
|
||||
|
@ -453,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());
|
||||
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
manageIdentityData(node);
|
||||
if (node->getType() == NodeType::Agent && !node->isUpstream()) {
|
||||
manageIdentityData(node);
|
||||
}
|
||||
|
||||
++_sumListeners;
|
||||
});
|
||||
}, &lockWait, &nodeTransform, &functor);
|
||||
|
@ -310,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
|
||||
|
@ -429,12 +559,11 @@ 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;
|
||||
bool skeletonModelUrlChanged = false;
|
||||
avatar.processAvatarIdentity(identity, identityChanged, displayNameChanged, skeletonModelUrlChanged);
|
||||
avatar.processAvatarIdentity(message->getMessage(), identityChanged, displayNameChanged, skeletonModelUrlChanged);
|
||||
|
||||
if (identityChanged) {
|
||||
QMutexLocker nodeDataLocker(&nodeData->getMutex());
|
||||
nodeData->flagIdentityChange();
|
||||
|
@ -451,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) {
|
||||
|
@ -707,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) {
|
||||
|
@ -726,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());
|
||||
|
@ -803,12 +933,13 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) {
|
|||
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";
|
||||
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 {
|
||||
|
|
|
@ -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();
|
||||
|
@ -66,6 +68,8 @@ private:
|
|||
QStringList _avatarWhitelist { };
|
||||
QString _replacementAvatar { REPLACEMENT_AVATAR_DEFAULT };
|
||||
|
||||
void optionallyReplicatePacket(ReceivedMessage& message, const Node& node);
|
||||
|
||||
p_high_resolution_clock::time_point _lastFrameTimestamp;
|
||||
|
||||
// FIXME - new throttling - use these values somehow
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -1335,6 +1335,66 @@
|
|||
"advanced": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "broadcasting",
|
||||
"label": "Broadcasting",
|
||||
"settings": [
|
||||
{
|
||||
"name": "users",
|
||||
"label": "Broadcasted Users",
|
||||
"type": "table",
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
});
|
|
@ -81,19 +81,6 @@
|
|||
</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>
|
||||
|
|
|
@ -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,10 +224,10 @@ $(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();
|
||||
|
@ -1005,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 = "";
|
||||
|
@ -1158,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>"
|
||||
|
@ -1289,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 + "' " +
|
||||
|
@ -1411,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) {
|
||||
|
@ -1440,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
|
||||
|
@ -1468,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 {
|
||||
|
@ -1680,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 };
|
||||
|
|
|
@ -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() });
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -108,6 +108,7 @@ public:
|
|||
|
||||
signals:
|
||||
void updateNodePermissions();
|
||||
void settingsUpdated();
|
||||
|
||||
public slots:
|
||||
void apiGetGroupIDJSONCallback(QNetworkReply& requestReply);
|
||||
|
|
Binary file not shown.
|
@ -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 (image error) Size: 2.9 KiB After (image error) Size: 2.1 KiB |
|
@ -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 (image error) Size: 1.3 KiB After (image error) Size: 2.1 KiB |
BIN
interface/resources/images/loader-calibrate-blue.png
Normal file
BIN
interface/resources/images/loader-calibrate-blue.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 7.7 KiB |
BIN
interface/resources/images/loader-calibrate-white.png
Normal file
BIN
interface/resources/images/loader-calibrate-white.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 7.2 KiB |
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -65,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);
|
||||
|
|
|
@ -22,7 +22,8 @@ Original.CheckBox {
|
|||
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
|
||||
|
@ -92,7 +93,8 @@ Original.CheckBox {
|
|||
text: control.text
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -18,7 +18,7 @@ import QtQuick.Layouts 1.3
|
|||
import "../../styles-uit"
|
||||
import "../../controls-uit" as HifiControls
|
||||
import "../../windows"
|
||||
import "./" as Audio
|
||||
import "./" as AudioControls
|
||||
|
||||
Rectangle {
|
||||
id: root;
|
||||
|
@ -52,33 +52,40 @@ Rectangle {
|
|||
|
||||
Separator { visible: root.showTitle() }
|
||||
|
||||
Grid {
|
||||
columns: 2;
|
||||
ColumnLayout {
|
||||
x: 16; // padding does not work
|
||||
spacing: 16;
|
||||
|
||||
Audio.CheckBox {
|
||||
text: qsTr("Mute microphone");
|
||||
checked: Audio.muted;
|
||||
onClicked: {
|
||||
Audio.muted = checked;
|
||||
checked = Qt.binding(function() { return Audio.muted; }); // restore binding
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
Audio.CheckBox {
|
||||
text: qsTr("Enable noise reduction");
|
||||
checked: Audio.noiseReduction;
|
||||
onClicked: {
|
||||
Audio.noiseReduction = checked;
|
||||
checked = Qt.binding(function() { return Audio.noiseReduction; }); // 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
|
||||
}
|
||||
}
|
||||
}
|
||||
Audio.CheckBox {
|
||||
text: qsTr("Show audio level meter");
|
||||
checked: AvatarInputs.showAudioTools;
|
||||
onClicked: {
|
||||
AvatarInputs.showAudioTools = checked;
|
||||
checked = Qt.binding(function() { return AvatarInputs.showAudioTools; }); // 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -110,12 +117,25 @@ Rectangle {
|
|||
delegate: Item {
|
||||
width: parent.width;
|
||||
height: 36;
|
||||
Audio.CheckBox {
|
||||
text: display;
|
||||
checked: selected;
|
||||
onClicked: {
|
||||
selected = checked;
|
||||
checked = Qt.binding(function() { return selected; }); // restore binding
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -124,17 +144,23 @@ Rectangle {
|
|||
Separator {}
|
||||
|
||||
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");
|
||||
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 }}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -148,7 +174,7 @@ Rectangle {
|
|||
delegate: Item {
|
||||
width: parent.width;
|
||||
height: 36;
|
||||
Audio.CheckBox {
|
||||
AudioControls.CheckBox {
|
||||
text: display;
|
||||
checked: selected;
|
||||
onClicked: {
|
||||
|
|
105
interface/resources/qml/hifi/audio/InputLevel.qml
Normal file
105
interface/resources/qml/hifi/audio/InputLevel.qml
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
223
interface/resources/qml/hifi/audio/MicBar.qml
Normal file
223
interface/resources/qml/hifi/audio/MicBar.qml
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
86
interface/resources/qml/hifi/audio/PlaySampleSound.qml
Normal file
86
interface/resources/qml/hifi/audio/PlaySampleSound.qml
Normal 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");
|
||||
}
|
||||
|
||||
}
|
|
@ -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]);
|
||||
|
|
213
interface/resources/qml/hifi/tablet/CalibratingScreen.qml
Normal file
213
interface/resources/qml/hifi/tablet/CalibratingScreen.qml
Normal 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();
|
||||
}
|
||||
}
|
218
interface/resources/qml/hifi/tablet/ControllerSettings.qml
Normal file
218
interface/resources/qml/hifi/tablet/ControllerSettings.qml
Normal file
|
@ -0,0 +1,218 @@
|
|||
//
|
||||
// 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.Controls 1.4
|
||||
import QtGraphicalEffects 1.0
|
||||
import "../../styles-uit"
|
||||
import "../../controls"
|
||||
import "../../controls-uit" as HifiControls
|
||||
|
||||
StackView {
|
||||
id: stack
|
||||
initialItem: inputConfiguration
|
||||
Rectangle {
|
||||
id: inputConfiguration
|
||||
anchors.fill: parent
|
||||
|
||||
HifiConstants { id: hifi }
|
||||
|
||||
color: hifi.colors.baseGray
|
||||
|
||||
property var pluginSettings: null
|
||||
|
||||
Rectangle {
|
||||
width: inputConfiguration.width
|
||||
height: 1
|
||||
color: hifi.colors.baseGrayShadow
|
||||
x: -hifi.dimensions.contentMargin.x
|
||||
}
|
||||
|
||||
RalewayRegular {
|
||||
id: header
|
||||
text: "Controller Settings"
|
||||
size: 22
|
||||
color: "white"
|
||||
|
||||
anchors.top: inputConfiguration.top
|
||||
anchors.left: inputConfiguration.left
|
||||
anchors.leftMargin: 20
|
||||
anchors.topMargin: 20
|
||||
}
|
||||
|
||||
|
||||
Separator {
|
||||
id: headerSeparator
|
||||
width: inputConfiguration.width
|
||||
anchors.top: header.bottom
|
||||
anchors.topMargin: 10
|
||||
}
|
||||
|
||||
HiFiGlyphs {
|
||||
id: sourceGlyph
|
||||
text: hifi.glyphs.source
|
||||
size: 36
|
||||
color: hifi.colors.blueHighlight
|
||||
|
||||
anchors.top: headerSeparator.bottom
|
||||
anchors.left: inputConfiguration.left
|
||||
anchors.leftMargin: 40
|
||||
anchors.topMargin: 20
|
||||
}
|
||||
|
||||
RalewayRegular {
|
||||
id: configuration
|
||||
text: "SELECT DEVICE"
|
||||
size: 15
|
||||
color: hifi.colors.lightGrayText
|
||||
|
||||
|
||||
anchors.top: headerSeparator.bottom
|
||||
anchors.left: sourceGlyph.right
|
||||
anchors.leftMargin: 10
|
||||
anchors.topMargin: 30
|
||||
}
|
||||
|
||||
Row {
|
||||
id: configRow
|
||||
z: 999
|
||||
anchors.top: sourceGlyph.bottom
|
||||
anchors.topMargin: 20
|
||||
anchors.left: sourceGlyph.left
|
||||
anchors.leftMargin: 40
|
||||
spacing: 10
|
||||
HifiControls.ComboBox {
|
||||
id: box
|
||||
width: 160
|
||||
z: 999
|
||||
editable: true
|
||||
colorScheme: hifi.colorSchemes.dark
|
||||
model: inputPlugins()
|
||||
label: ""
|
||||
|
||||
onCurrentIndexChanged: {
|
||||
changeSource();
|
||||
}
|
||||
}
|
||||
|
||||
HifiControls.CheckBox {
|
||||
id: checkBox
|
||||
colorScheme: hifi.colorSchemes.dark
|
||||
text: "show all input devices"
|
||||
|
||||
onClicked: {
|
||||
inputPlugins();
|
||||
changeSource();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Separator {
|
||||
id: configurationSeparator
|
||||
z: 0
|
||||
width: inputConfiguration.width
|
||||
anchors.top: configRow.bottom
|
||||
anchors.topMargin: 10
|
||||
}
|
||||
|
||||
|
||||
HiFiGlyphs {
|
||||
id: sliderGlyph
|
||||
text: hifi.glyphs.sliders
|
||||
size: 36
|
||||
color: hifi.colors.blueHighlight
|
||||
|
||||
anchors.top: configurationSeparator.bottom
|
||||
anchors.left: inputConfiguration.left
|
||||
anchors.leftMargin: 40
|
||||
anchors.topMargin: 20
|
||||
}
|
||||
|
||||
RalewayRegular {
|
||||
id: configurationHeader
|
||||
text: "CONFIGURATION"
|
||||
size: 15
|
||||
color: hifi.colors.lightGrayText
|
||||
|
||||
|
||||
anchors.top: configurationSeparator.bottom
|
||||
anchors.left: sliderGlyph.right
|
||||
anchors.leftMargin: 10
|
||||
anchors.topMargin: 30
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: loader
|
||||
asynchronous: false
|
||||
|
||||
width: inputConfiguration.width
|
||||
anchors.left: inputConfiguration.left
|
||||
anchors.right: inputConfiguration.right
|
||||
anchors.top: configurationHeader.bottom
|
||||
anchors.topMargin: 10
|
||||
anchors.bottom: inputConfiguration.bottom
|
||||
|
||||
source: InputConfiguration.configurationLayout(box.currentText);
|
||||
onLoaded: {
|
||||
if (loader.item.hasOwnProperty("pluginName")) {
|
||||
if (box.currentText === "Vive") {
|
||||
loader.item.pluginName = "OpenVR";
|
||||
} else {
|
||||
loader.item.pluginName = box.currentText;
|
||||
}
|
||||
}
|
||||
|
||||
if (loader.item.hasOwnProperty("displayInformation")) {
|
||||
loader.item.displayConfiguration();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function inputPlugins() {
|
||||
if (checkBox.checked) {
|
||||
return InputConfiguration.inputPlugins();
|
||||
} else {
|
||||
return InputConfiguration.activeInputPlugins();
|
||||
}
|
||||
}
|
||||
|
||||
function initialize() {
|
||||
changeSource();
|
||||
}
|
||||
|
||||
function changeSource() {
|
||||
loader.source = "";
|
||||
var source = "";
|
||||
if (box.currentText == "Vive") {
|
||||
source = InputConfiguration.configurationLayout("OpenVR");
|
||||
} else {
|
||||
source = InputConfiguration.configurationLayout(box.currentText);
|
||||
}
|
||||
|
||||
loader.source = source;
|
||||
if (source === "") {
|
||||
box.label = "(not configurable)";
|
||||
} else {
|
||||
box.label = "";
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: timer
|
||||
repeat: false
|
||||
interval: 300
|
||||
onTriggered: initialize()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
timer.start();
|
||||
}
|
||||
}
|
879
interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml
Normal file
879
interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml
Normal file
|
@ -0,0 +1,879 @@
|
|||
//
|
||||
// Created by Dante Ruiz on 6/5/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.Controls 1.4
|
||||
import QtGraphicalEffects 1.0
|
||||
import QtQuick.Controls 1.4 as Original
|
||||
import QtQuick.Controls.Styles 1.4
|
||||
import "../../styles-uit"
|
||||
import "../../controls"
|
||||
import "../../controls-uit" as HifiControls
|
||||
import "."
|
||||
|
||||
|
||||
Rectangle {
|
||||
id: openVrConfiguration
|
||||
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
anchors.fill: parent
|
||||
|
||||
property int leftMargin: 75
|
||||
property int countDown: 0
|
||||
property string pluginName: ""
|
||||
property var displayInformation: null
|
||||
|
||||
readonly property bool feetChecked: feetBox.checked
|
||||
readonly property bool hipsChecked: hipBox.checked
|
||||
readonly property bool chestChecked: chestBox.checked
|
||||
readonly property bool shouldersChecked: shoulderBox.checked
|
||||
readonly property bool hmdHead: headBox.checked
|
||||
readonly property bool headPuck: headPuckBox.checked
|
||||
readonly property bool handController: handBox.checked
|
||||
readonly property bool handPuck: handPuckBox.checked
|
||||
|
||||
property int state: buttonState.disabled
|
||||
property var lastConfiguration: null
|
||||
|
||||
HifiConstants { id: hifi }
|
||||
|
||||
Component { id: screen; CalibratingScreen {} }
|
||||
QtObject {
|
||||
id: buttonState
|
||||
readonly property int disabled: 0
|
||||
readonly property int apply: 1
|
||||
readonly property int applyAndCalibrate: 2
|
||||
readonly property int calibrate: 3
|
||||
|
||||
}
|
||||
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
propagateComposedEvents: true
|
||||
onPressed: {
|
||||
parent.forceActiveFocus()
|
||||
mouse.accepted = false;
|
||||
}
|
||||
}
|
||||
color: hifi.colors.baseGray
|
||||
|
||||
RalewayBold {
|
||||
id: head
|
||||
|
||||
text: "Head:"
|
||||
size: 12
|
||||
|
||||
color: "white"
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: leftMargin
|
||||
}
|
||||
|
||||
Row {
|
||||
id: headConfig
|
||||
anchors.top: head.bottom
|
||||
anchors.topMargin: 5
|
||||
anchors.left: openVrConfiguration.left
|
||||
anchors.leftMargin: leftMargin + 10
|
||||
spacing: 10
|
||||
|
||||
HifiControls.CheckBox {
|
||||
id: headBox
|
||||
width: 15
|
||||
height: 15
|
||||
boxRadius: 7
|
||||
|
||||
onClicked: {
|
||||
if (checked) {
|
||||
headPuckBox.checked = false;
|
||||
} else {
|
||||
checked = true;
|
||||
}
|
||||
sendConfigurationSettings();
|
||||
}
|
||||
}
|
||||
|
||||
RalewayBold {
|
||||
size: 12
|
||||
text: "Vive HMD"
|
||||
color: hifi.colors.lightGrayText
|
||||
}
|
||||
|
||||
HifiControls.CheckBox {
|
||||
id: headPuckBox
|
||||
width: 15
|
||||
height: 15
|
||||
boxRadius: 7
|
||||
|
||||
onClicked: {
|
||||
if (checked) {
|
||||
headBox.checked = false;
|
||||
} else {
|
||||
checked = true;
|
||||
}
|
||||
sendConfigurationSettings();
|
||||
}
|
||||
}
|
||||
|
||||
RalewayBold {
|
||||
size: 12
|
||||
text: "Tracker"
|
||||
color: hifi.colors.lightGrayText
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: headOffsets
|
||||
anchors.top: headConfig.bottom
|
||||
anchors.topMargin: 5
|
||||
anchors.left: openVrConfiguration.left
|
||||
anchors.leftMargin: leftMargin + 10
|
||||
spacing: 10
|
||||
visible: headPuckBox.checked
|
||||
HifiControls.SpinBox {
|
||||
id: headYOffset
|
||||
decimals: 4
|
||||
width: 112
|
||||
label: "Y: offset"
|
||||
minimumValue: -10
|
||||
stepSize: 0.0254
|
||||
colorScheme: hifi.colorSchemes.dark
|
||||
|
||||
onEditingFinished: {
|
||||
sendConfigurationSettings();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
HifiControls.SpinBox {
|
||||
id: headZOffset
|
||||
width: 112
|
||||
label: "Z: offset"
|
||||
minimumValue: -10
|
||||
stepSize: 0.0254
|
||||
decimals: 4
|
||||
colorScheme: hifi.colorSchemes.dark
|
||||
|
||||
onEditingFinished: {
|
||||
sendConfigurationSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
RalewayBold {
|
||||
id: hands
|
||||
|
||||
text: "Hands:"
|
||||
size: 12
|
||||
|
||||
color: "white"
|
||||
|
||||
anchors.top: (headOffsets.visible ? headOffsets.bottom : headConfig.bottom)
|
||||
anchors.topMargin: (headOffsets.visible ? 22 : 10)
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: leftMargin
|
||||
}
|
||||
|
||||
Row {
|
||||
id: handConfig
|
||||
anchors.top: hands.bottom
|
||||
anchors.topMargin: 5
|
||||
anchors.left: openVrConfiguration.left
|
||||
anchors.leftMargin: leftMargin + 10
|
||||
spacing: 10
|
||||
|
||||
HifiControls.CheckBox {
|
||||
id: handBox
|
||||
width: 15
|
||||
height: 15
|
||||
boxRadius: 7
|
||||
|
||||
onClicked: {
|
||||
if (checked) {
|
||||
handPuckBox.checked = false;
|
||||
} else {
|
||||
checked = true;
|
||||
}
|
||||
sendConfigurationSettings();
|
||||
}
|
||||
}
|
||||
|
||||
RalewayBold {
|
||||
size: 12
|
||||
text: "Controllers"
|
||||
color: hifi.colors.lightGrayText
|
||||
}
|
||||
|
||||
HifiControls.CheckBox {
|
||||
id: handPuckBox
|
||||
width: 12
|
||||
height: 15
|
||||
boxRadius: 7
|
||||
|
||||
onClicked: {
|
||||
if (checked) {
|
||||
handBox.checked = false;
|
||||
} else {
|
||||
checked = true;
|
||||
}
|
||||
sendConfigurationSettings();
|
||||
}
|
||||
}
|
||||
|
||||
RalewayBold {
|
||||
size: 12
|
||||
text: "Trackers"
|
||||
color: hifi.colors.lightGrayText
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: handOffset
|
||||
visible: handPuckBox.checked
|
||||
anchors.top: handConfig.bottom
|
||||
anchors.topMargin: 5
|
||||
anchors.left: openVrConfiguration.left
|
||||
anchors.leftMargin: leftMargin + 10
|
||||
spacing: 10
|
||||
|
||||
HifiControls.SpinBox {
|
||||
id: handYOffset
|
||||
decimals: 4
|
||||
width: 112
|
||||
label: "Y: offset"
|
||||
minimumValue: -10
|
||||
stepSize: 0.0254
|
||||
colorScheme: hifi.colorSchemes.dark
|
||||
|
||||
onEditingFinished: {
|
||||
sendConfigurationSettings();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
HifiControls.SpinBox {
|
||||
id: handZOffset
|
||||
width: 112
|
||||
label: "Z: offset"
|
||||
minimumValue: -10
|
||||
stepSize: 0.0254
|
||||
decimals: 4
|
||||
colorScheme: hifi.colorSchemes.dark
|
||||
|
||||
onEditingFinished: {
|
||||
sendConfigurationSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RalewayBold {
|
||||
id: additional
|
||||
|
||||
text: "Additional Trackers"
|
||||
size: 12
|
||||
|
||||
color: hifi.colors.white
|
||||
|
||||
anchors.top: (handOffset.visible ? handOffset.bottom : handConfig.bottom)
|
||||
anchors.topMargin: (handOffset.visible ? 22 : 10)
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: leftMargin
|
||||
}
|
||||
|
||||
Row {
|
||||
id: feetConfig
|
||||
anchors.top: additional.bottom
|
||||
anchors.topMargin: 15
|
||||
anchors.left: openVrConfiguration.left
|
||||
anchors.leftMargin: leftMargin + 10
|
||||
spacing: 10
|
||||
|
||||
HifiControls.CheckBox {
|
||||
id: feetBox
|
||||
width: 15
|
||||
height: 15
|
||||
boxRadius: 7
|
||||
|
||||
onClicked: {
|
||||
if (!checked) {
|
||||
shoulderBox.checked = false;
|
||||
chestBox.checked = false;
|
||||
hipBox.checked = false;
|
||||
}
|
||||
sendConfigurationSettings();
|
||||
}
|
||||
}
|
||||
|
||||
RalewayBold {
|
||||
size: 12
|
||||
text: "Feet"
|
||||
color: hifi.colors.lightGrayText
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: hipConfig
|
||||
anchors.top: feetConfig.bottom
|
||||
anchors.topMargin: 15
|
||||
anchors.left: openVrConfiguration.left
|
||||
anchors.leftMargin: leftMargin + 10
|
||||
spacing: 10
|
||||
|
||||
HifiControls.CheckBox {
|
||||
id: hipBox
|
||||
width: 15
|
||||
height: 15
|
||||
boxRadius: 7
|
||||
|
||||
onClicked: {
|
||||
if (checked) {
|
||||
feetBox.checked = true;
|
||||
}
|
||||
|
||||
if (chestChecked) {
|
||||
checked = true;
|
||||
}
|
||||
sendConfigurationSettings();
|
||||
}
|
||||
}
|
||||
|
||||
RalewayBold {
|
||||
size: 12
|
||||
text: "Hips"
|
||||
color: hifi.colors.lightGrayText
|
||||
}
|
||||
|
||||
RalewayRegular {
|
||||
size: 12
|
||||
text: "requires feet"
|
||||
color: hifi.colors.lightGray
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Row {
|
||||
id: chestConfig
|
||||
anchors.top: hipConfig.bottom
|
||||
anchors.topMargin: 15
|
||||
anchors.left: openVrConfiguration.left
|
||||
anchors.leftMargin: leftMargin + 10
|
||||
spacing: 10
|
||||
|
||||
HifiControls.CheckBox {
|
||||
id: chestBox
|
||||
width: 15
|
||||
height: 15
|
||||
boxRadius: 7
|
||||
|
||||
onClicked: {
|
||||
if (checked) {
|
||||
hipBox.checked = true;
|
||||
feetBox.checked = true;
|
||||
}
|
||||
sendConfigurationSettings();
|
||||
}
|
||||
}
|
||||
|
||||
RalewayBold {
|
||||
size: 12
|
||||
text: "Chest"
|
||||
color: hifi.colors.lightGrayText
|
||||
}
|
||||
|
||||
RalewayRegular {
|
||||
size: 12
|
||||
text: "requires hips"
|
||||
color: hifi.colors.lightGray
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Row {
|
||||
id: shoulderConfig
|
||||
anchors.top: chestConfig.bottom
|
||||
anchors.topMargin: 15
|
||||
anchors.left: openVrConfiguration.left
|
||||
anchors.leftMargin: leftMargin + 10
|
||||
spacing: 10
|
||||
|
||||
HifiControls.CheckBox {
|
||||
id: shoulderBox
|
||||
width: 15
|
||||
height: 15
|
||||
boxRadius: 7
|
||||
|
||||
onClicked: {
|
||||
if (checked) {
|
||||
hipBox.checked = true;
|
||||
feetBox.checked = true;
|
||||
}
|
||||
sendConfigurationSettings();
|
||||
}
|
||||
}
|
||||
|
||||
RalewayBold {
|
||||
size: 12
|
||||
text: "Shoulders"
|
||||
color: hifi.colors.lightGrayText
|
||||
}
|
||||
|
||||
RalewayRegular {
|
||||
size: 12
|
||||
text: "requires hips"
|
||||
color: hifi.colors.lightGray
|
||||
}
|
||||
}
|
||||
|
||||
Separator {
|
||||
id: bottomSeperator
|
||||
width: parent.width
|
||||
anchors.top: shoulderConfig.bottom
|
||||
anchors.topMargin: 10
|
||||
}
|
||||
|
||||
|
||||
|
||||
Rectangle {
|
||||
id: calibrationButton
|
||||
property int color: hifi.buttons.blue
|
||||
property int colorScheme: hifi.colorSchemes.light
|
||||
property string glyph: hifi.glyphs.avatar1
|
||||
property bool enabled: false
|
||||
property bool pressed: false
|
||||
property bool hovered: false
|
||||
property int size: 32
|
||||
property string text: "apply"
|
||||
property int padding: 12
|
||||
|
||||
width: glyphButton.width + calibrationText.width + padding
|
||||
height: hifi.dimensions.controlLineHeight
|
||||
anchors.top: bottomSeperator.bottom
|
||||
anchors.topMargin: 10
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: leftMargin
|
||||
|
||||
radius: hifi.buttons.radius
|
||||
|
||||
gradient: Gradient {
|
||||
GradientStop {
|
||||
position: 0.2
|
||||
color: {
|
||||
if (!calibrationButton.enabled) {
|
||||
hifi.buttons.disabledColorStart[calibrationButton.colorScheme]
|
||||
} else if (calibrationButton.pressed) {
|
||||
hifi.buttons.pressedColor[calibrationButton.color]
|
||||
} else if (calibrationButton.hovered) {
|
||||
hifi.buttons.hoveredColor[calibrationButton.color]
|
||||
} else {
|
||||
hifi.buttons.colorStart[calibrationButton.color]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
position: 1.0
|
||||
color: {
|
||||
if (!calibrationButton.enabled) {
|
||||
hifi.buttons.disabledColorFinish[calibrationButton.colorScheme]
|
||||
} else if (calibrationButton.pressed) {
|
||||
hifi.buttons.pressedColor[calibrationButton.color]
|
||||
} else if (calibrationButton.hovered) {
|
||||
hifi.buttons.hoveredColor[calibrationButton.color]
|
||||
} else {
|
||||
hifi.buttons.colorFinish[calibrationButton.color]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
HiFiGlyphs {
|
||||
id: glyphButton
|
||||
color: enabled ? hifi.buttons.textColor[calibrationButton.color]
|
||||
: hifi.buttons.disabledTextColor[calibrationButton.colorScheme]
|
||||
text: calibrationButton.glyph
|
||||
size: calibrationButton.size
|
||||
|
||||
anchors {
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
bottomMargin: 1
|
||||
}
|
||||
}
|
||||
|
||||
RalewayBold {
|
||||
id: calibrationText
|
||||
font.capitalization: Font.AllUppercase
|
||||
color: enabled ? hifi.buttons.textColor[calibrationButton.color]
|
||||
: hifi.buttons.disabledTextColor[calibrationButton.colorScheme]
|
||||
size: hifi.fontSizes.buttonLabel
|
||||
text: calibrationButton.text
|
||||
|
||||
anchors {
|
||||
left: glyphButton.right
|
||||
top: parent.top
|
||||
topMargin: 7
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
if (calibrationButton.enabled) {
|
||||
if (openVrConfiguration.state === buttonState.apply) {
|
||||
InputConfiguration.uncalibratePlugin(pluginName);
|
||||
updateCalibrationButton();
|
||||
} else {
|
||||
calibrationTimer.interval = timeToCalibrate.value * 1000
|
||||
openVrConfiguration.countDown = timeToCalibrate.value;
|
||||
var calibratingScreen = screen.createObject();
|
||||
stack.push(calibratingScreen);
|
||||
calibratingScreen.canceled.connect(cancelCalibration);
|
||||
calibratingScreen.restart.connect(restartCalibration);
|
||||
calibratingScreen.start(calibrationTimer.interval, timeToCalibrate.value);
|
||||
calibrationTimer.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPressed: {
|
||||
calibrationButton.pressed = true;
|
||||
}
|
||||
|
||||
onReleased: {
|
||||
calibrationButton.pressed = false;
|
||||
}
|
||||
|
||||
onEntered: {
|
||||
calibrationButton.hovered = true;
|
||||
}
|
||||
|
||||
onExited: {
|
||||
calibrationButton.hovered = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: calibrationTimer
|
||||
repeat: false
|
||||
interval: 20
|
||||
onTriggered: {
|
||||
InputConfiguration.calibratePlugin(pluginName)
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: displayTimer
|
||||
repeat: false
|
||||
interval: 3000
|
||||
onTriggered: {
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
InputConfiguration.calibrationStatus.connect(calibrationStatusInfo);
|
||||
lastConfiguration = composeConfigurationSettings();
|
||||
}
|
||||
|
||||
HifiControls.SpinBox {
|
||||
id: timeToCalibrate
|
||||
width: 70
|
||||
anchors.top: calibrationButton.bottom
|
||||
anchors.topMargin: 40
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: leftMargin
|
||||
|
||||
minimumValue: 3
|
||||
value: 3
|
||||
colorScheme: hifi.colorSchemes.dark
|
||||
|
||||
onEditingFinished: {
|
||||
calibrationTimer.interval = value * 1000;
|
||||
openVrConfiguration.countDown = value;
|
||||
numberAnimation.duration = calibrationTimer.interval;
|
||||
}
|
||||
}
|
||||
|
||||
RalewayBold {
|
||||
id: delayTextInfo
|
||||
size: 10
|
||||
text: "Delay Before Calibration Starts"
|
||||
color: hifi.colors.white
|
||||
|
||||
anchors {
|
||||
left: timeToCalibrate.right
|
||||
leftMargin: 20
|
||||
verticalCenter: timeToCalibrate.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
RalewayRegular {
|
||||
size: 12
|
||||
text: "sec"
|
||||
color: hifi.colors.lightGray
|
||||
|
||||
anchors {
|
||||
left: delayTextInfo.right
|
||||
leftMargin: 10
|
||||
verticalCenter: delayTextInfo.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
id: numberAnimation
|
||||
target: openVrConfiguration
|
||||
property: "countDown"
|
||||
to: 0
|
||||
}
|
||||
|
||||
function calibrationStatusInfo(status) {
|
||||
var calibrationScreen = stack.currentItem;
|
||||
if (status["calibrated"]) {
|
||||
calibrationScreen.success();
|
||||
} else if (!status["calibrated"]) {
|
||||
var uncalibrated = status["success"];
|
||||
if (!uncalibrated) {
|
||||
calibrationScreen.failure();
|
||||
}
|
||||
}
|
||||
|
||||
updateCalibrationButton();
|
||||
}
|
||||
|
||||
|
||||
function trackersForConfiguration() {
|
||||
var pucksNeeded = 0;
|
||||
|
||||
if (headPuckBox.checked) {
|
||||
pucksNeeded++;
|
||||
}
|
||||
|
||||
if (feetBox.checked) {
|
||||
pucksNeeded++;
|
||||
}
|
||||
|
||||
if (hipBox.checked) {
|
||||
pucksNeeded++;
|
||||
}
|
||||
|
||||
if (chestBox.checked) {
|
||||
pucksNeeded++;
|
||||
}
|
||||
|
||||
if (shoulderBox.checked) {
|
||||
pucksNeeded++;
|
||||
}
|
||||
|
||||
return pucksNeeded;
|
||||
}
|
||||
|
||||
function cancelCalibration() {
|
||||
calibrationTimer.stop();
|
||||
}
|
||||
|
||||
function restartCalibration() {
|
||||
calibrationTimer.restart();
|
||||
}
|
||||
|
||||
function displayConfiguration() {
|
||||
var settings = InputConfiguration.configurationSettings(pluginName);
|
||||
var configurationType = settings["trackerConfiguration"];
|
||||
displayTrackerConfiguration(configurationType);
|
||||
|
||||
|
||||
var HmdHead = settings["HMDHead"];
|
||||
var viveController = settings["handController"];
|
||||
|
||||
if (HmdHead) {
|
||||
headBox.checked = true;
|
||||
headPuckBox.checked = false;
|
||||
} else {
|
||||
headPuckBox.checked = true;
|
||||
headBox.checked = false;
|
||||
}
|
||||
|
||||
if (viveController) {
|
||||
handBox.checked = true;
|
||||
handPuckBox.checked = false;
|
||||
} else {
|
||||
handPuckBox.checked = true;
|
||||
handBox.checked = false;
|
||||
}
|
||||
|
||||
initializeButtonState();
|
||||
updateCalibrationText();
|
||||
}
|
||||
|
||||
function displayTrackerConfiguration(type) {
|
||||
if (type === "Feet") {
|
||||
feetBox.checked = true;
|
||||
} else if (type === "FeetAndHips") {
|
||||
feetBox.checked = true;
|
||||
hipBox.checked = true;
|
||||
} else if (type === "FeetHipsChest") {
|
||||
feetBox.checked = true;
|
||||
hipBox.checked = true;
|
||||
chestBox.checked = true;
|
||||
} else if (type === "FeetHipsAndShoulders") {
|
||||
feetBox.checked = true;
|
||||
hipBox.checked = true;
|
||||
shoulderBox.checked = true;
|
||||
} else if (type === "FeetHipsChestAndShoulders") {
|
||||
feetBox.checked = true;
|
||||
hipBox.checked = true;
|
||||
chestBox.checked = true;
|
||||
shoulderBox.checked = true;
|
||||
}
|
||||
}
|
||||
|
||||
function updateButtonState() {
|
||||
var settings = composeConfigurationSettings();
|
||||
var bodySetting = settings["bodyConfiguration"];
|
||||
var headSetting = settings["headConfiguration"];
|
||||
var headOverride = headSetting["override"];
|
||||
var handSetting = settings["handConfiguration"];
|
||||
var handOverride = handSetting["override"];
|
||||
|
||||
var settingsChanged = false;
|
||||
|
||||
if (lastConfiguration["bodyConfiguration"] !== bodySetting) {
|
||||
settingsChanged = true;
|
||||
}
|
||||
|
||||
var lastHead = lastConfiguration["headConfiguration"];
|
||||
if (lastHead["override"] !== headOverride) {
|
||||
settingsChanged = true;
|
||||
}
|
||||
|
||||
var lastHand = lastConfiguration["handConfiguration"];
|
||||
if (lastHand["override"] !== handOverride) {
|
||||
settingsChanged = true;
|
||||
}
|
||||
|
||||
if (settingsChanged) {
|
||||
if ((!handOverride) && (!headOverride) && (bodySetting === "None")) {
|
||||
state = buttonState.apply;
|
||||
} else {
|
||||
state = buttonState.applyAndCalibrate;
|
||||
}
|
||||
} else {
|
||||
if (state == buttonState.apply) {
|
||||
state = buttonState.disabled;
|
||||
} else if (state == buttonState.applyAndCalibrate) {
|
||||
state = buttonState.calibrate;
|
||||
}
|
||||
}
|
||||
|
||||
lastConfiguration = settings;
|
||||
}
|
||||
|
||||
function initializeButtonState() {
|
||||
var settings = composeConfigurationSettings();
|
||||
var bodySetting = settings["bodyConfiguration"];
|
||||
var headSetting = settings["headConfiguration"];
|
||||
var headOverride = headSetting["override"];
|
||||
var handSetting = settings["handConfiguration"];
|
||||
var handOverride = handSetting["override"];
|
||||
|
||||
|
||||
if ((!handOverride) && (!headOverride) && (bodySetting === "None")) {
|
||||
state = buttonState.disabled;
|
||||
} else {
|
||||
state = buttonState.calibrate;
|
||||
}
|
||||
}
|
||||
|
||||
function updateCalibrationButton() {
|
||||
updateButtonState();
|
||||
updateCalibrationText();
|
||||
}
|
||||
|
||||
function updateCalibrationText() {
|
||||
if (buttonState.disabled == state) {
|
||||
calibrationButton.enabled = false;
|
||||
calibrationButton.text = "Apply";
|
||||
} else if (buttonState.apply == state) {
|
||||
calibrationButton.enabled = true;
|
||||
calibrationButton.text = "Apply";
|
||||
} else if (buttonState.applyAndCalibrate == state) {
|
||||
calibrationButton.enabled = true;
|
||||
calibrationButton.text = "Apply And Calibrate";
|
||||
} else if (buttonState.calibrate == state) {
|
||||
calibrationButton.enabled = true;
|
||||
calibrationButton.text = "Calibrate";
|
||||
}
|
||||
}
|
||||
|
||||
function composeConfigurationSettings() {
|
||||
var trackerConfiguration = "";
|
||||
var overrideHead = false;
|
||||
var overrideHandController = false;
|
||||
|
||||
if (shouldersChecked && chestChecked) {
|
||||
trackerConfiguration = "FeetHipsChestAndShoulders";
|
||||
} else if (shouldersChecked) {
|
||||
trackerConfiguration = "FeetHipsAndShoulders";
|
||||
} else if (chestChecked) {
|
||||
trackerConfiguration = "FeetHipsAndChest";
|
||||
} else if (hipsChecked) {
|
||||
trackerConfiguration = "FeetAndHips";
|
||||
} else if (feetChecked) {
|
||||
trackerConfiguration = "Feet";
|
||||
} else {
|
||||
trackerConfiguration = "None";
|
||||
}
|
||||
|
||||
if (headPuck) {
|
||||
overrideHead = true;
|
||||
} else if (hmdHead) {
|
||||
overrideHead = false;
|
||||
}
|
||||
|
||||
if (handController) {
|
||||
overrideHandController = false;
|
||||
} else if (handPuck) {
|
||||
overrideHandController = true;
|
||||
}
|
||||
|
||||
var headObject = {
|
||||
"override": overrideHead,
|
||||
"Y": headYOffset.value,
|
||||
"Z": headZOffset.value
|
||||
}
|
||||
|
||||
var handObject = {
|
||||
"override": overrideHandController,
|
||||
"Y": handYOffset.value,
|
||||
"Z": handZOffset.value
|
||||
}
|
||||
|
||||
var settingsObject = {
|
||||
"bodyConfiguration": trackerConfiguration,
|
||||
"headConfiguration": headObject,
|
||||
"handConfiguration": handObject
|
||||
}
|
||||
|
||||
return settingsObject;
|
||||
}
|
||||
|
||||
function sendConfigurationSettings() {
|
||||
var settings = composeConfigurationSettings();
|
||||
InputConfiguration.setConfigurationSettings(settings, pluginName);
|
||||
updateCalibrationButton();
|
||||
}
|
||||
}
|
|
@ -1,20 +1,16 @@
|
|||
import QtQuick 2.5
|
||||
import QtGraphicalEffects 1.0
|
||||
|
||||
import "../../styles-uit"
|
||||
import "../audio" as HifiAudio
|
||||
|
||||
Item {
|
||||
id: tablet
|
||||
objectName: "tablet"
|
||||
property double micLevel: 0.8
|
||||
property int rowIndex: 0
|
||||
property int columnIndex: 0
|
||||
property int count: (flowMain.children.length - 1)
|
||||
|
||||
// called by C++ code to keep audio bar updated
|
||||
function setMicLevel(newMicLevel) {
|
||||
tablet.micLevel = newMicLevel;
|
||||
}
|
||||
|
||||
// used to look up a button by its uuid
|
||||
function findButtonIndex(uuid) {
|
||||
if (!uuid) {
|
||||
|
@ -83,6 +79,16 @@ Item {
|
|||
Rectangle {
|
||||
id: bgTopBar
|
||||
height: 90
|
||||
|
||||
anchors {
|
||||
top: parent.top
|
||||
topMargin: 0
|
||||
left: parent.left
|
||||
leftMargin: 0
|
||||
right: parent.right
|
||||
rightMargin: 0
|
||||
}
|
||||
|
||||
gradient: Gradient {
|
||||
GradientStop {
|
||||
position: 0
|
||||
|
@ -94,108 +100,13 @@ Item {
|
|||
color: "#1e1e1e"
|
||||
}
|
||||
}
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 0
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 0
|
||||
anchors.topMargin: 0
|
||||
anchors.top: parent.top
|
||||
|
||||
Item {
|
||||
id: audioIcon
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 40
|
||||
height: 40
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 5
|
||||
|
||||
Image {
|
||||
id: micIcon
|
||||
source: "../../../icons/tablet-icons/mic.svg"
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: (Audio.muted && !toggleMuteMouseArea.containsMouse)
|
||||
|| (!Audio.muted && toggleMuteMouseArea.containsMouse)
|
||||
|
||||
Image {
|
||||
id: muteIcon
|
||||
source: "../../../icons/tablet-icons/mic-mute.svg"
|
||||
}
|
||||
|
||||
ColorOverlay {
|
||||
anchors.fill: muteIcon
|
||||
source: muteIcon
|
||||
color: toggleMuteMouseArea.containsMouse ? "#a0a0a0" : "#ff0000"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: audioBar
|
||||
width: 170
|
||||
height: 10
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 50
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Rectangle {
|
||||
id: audioBarBase
|
||||
color: "#333333"
|
||||
radius: 5
|
||||
anchors.fill: parent
|
||||
}
|
||||
Rectangle {
|
||||
id: audioBarMask
|
||||
width: parent.width * tablet.micLevel
|
||||
color: "#333333"
|
||||
radius: 5
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 0
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 0
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 0
|
||||
}
|
||||
LinearGradient {
|
||||
anchors.fill: audioBarMask
|
||||
source: audioBarMask
|
||||
start: Qt.point(0, 0)
|
||||
end: Qt.point(170, 0)
|
||||
gradient: Gradient {
|
||||
GradientStop {
|
||||
position: 0
|
||||
color: "#2c8e72"
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.8
|
||||
color: "#1fc6a6"
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.81
|
||||
color: "#ea4c5f"
|
||||
}
|
||||
GradientStop {
|
||||
position: 1
|
||||
color: "#ea4c5f"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: toggleMuteMouseArea
|
||||
HifiAudio.MicBar {
|
||||
anchors {
|
||||
left: audioIcon.left
|
||||
right: audioBar.right
|
||||
top: audioIcon.top
|
||||
bottom: audioIcon.bottom
|
||||
left: parent.left
|
||||
leftMargin: 30
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
hoverEnabled: true
|
||||
preventStealing: true
|
||||
propagateComposedEvents: false
|
||||
scrollGestureEnabled: false
|
||||
onClicked: { Audio.muted = !Audio.muted }
|
||||
}
|
||||
|
||||
RalewaySemiBold {
|
||||
|
@ -254,27 +165,6 @@ Item {
|
|||
}
|
||||
}
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "muted state"
|
||||
|
||||
PropertyChanges {
|
||||
target: muteText
|
||||
text: "UNMUTE"
|
||||
}
|
||||
|
||||
PropertyChanges {
|
||||
target: muteIcon
|
||||
visible: !Audio.muted
|
||||
}
|
||||
|
||||
PropertyChanges {
|
||||
target: tablet
|
||||
micLevel: 0
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
function setCurrentItemState(state) {
|
||||
var index = rowIndex + columnIndex;
|
||||
|
||||
|
|
|
@ -333,5 +333,8 @@ Item {
|
|||
readonly property string vol_x_2: "\ue015"
|
||||
readonly property string vol_x_3: "\ue016"
|
||||
readonly property string vol_x_4: "\ue017"
|
||||
readonly property string source: "\ue01c"
|
||||
readonly property string playback_play: "\ue01d"
|
||||
readonly property string stop_square: "\ue01e"
|
||||
}
|
||||
}
|
||||
|
|
BIN
interface/resources/sounds/sample.wav
Normal file
BIN
interface/resources/sounds/sample.wav
Normal file
Binary file not shown.
|
@ -88,6 +88,7 @@
|
|||
#include <UserActivityLoggerScriptingInterface.h>
|
||||
#include <LogHandler.h>
|
||||
#include "LocationBookmarks.h"
|
||||
#include <LocationScriptingInterface.h>
|
||||
#include <MainWindow.h>
|
||||
#include <MappingRequest.h>
|
||||
#include <MessagesClient.h>
|
||||
|
@ -108,6 +109,7 @@
|
|||
#include <plugins/PluginManager.h>
|
||||
#include <plugins/PluginUtils.h>
|
||||
#include <plugins/SteamClientPlugin.h>
|
||||
#include <plugins/InputConfiguration.h>
|
||||
#include <RecordingScriptingInterface.h>
|
||||
#include <RenderableWebEntityItem.h>
|
||||
#include <RenderShadowTask.h>
|
||||
|
@ -160,7 +162,6 @@
|
|||
#include "scripting/DesktopScriptingInterface.h"
|
||||
#include "scripting/GlobalServicesScriptingInterface.h"
|
||||
#include "scripting/HMDScriptingInterface.h"
|
||||
#include "scripting/LocationScriptingInterface.h"
|
||||
#include "scripting/MenuScriptingInterface.h"
|
||||
#include "scripting/SettingsScriptingInterface.h"
|
||||
#include "scripting/WindowScriptingInterface.h"
|
||||
|
@ -442,6 +443,34 @@ static const QString STATE_NAV_FOCUSED = "NavigationFocused";
|
|||
|
||||
bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) {
|
||||
const char** constArgv = const_cast<const char**>(argv);
|
||||
|
||||
// HRS: I could not figure out how to move these any earlier in startup, so when using this option, be sure to also supply
|
||||
// --allowMultipleInstances
|
||||
auto reportAndQuit = [&](const char* commandSwitch, std::function<void(FILE* fp)> report) {
|
||||
const char* reportfile = getCmdOption(argc, constArgv, commandSwitch);
|
||||
// Reports to the specified file, because stdout is set up to be captured for logging.
|
||||
if (reportfile) {
|
||||
FILE* fp = fopen(reportfile, "w");
|
||||
if (fp) {
|
||||
report(fp);
|
||||
fclose(fp);
|
||||
if (!runningMarkerExisted) { // don't leave ours around
|
||||
RunningMarker runingMarker(RUNNING_MARKER_FILENAME);
|
||||
runingMarker.deleteRunningMarkerFile(); // happens in deleter, but making the side-effect explicit.
|
||||
}
|
||||
_exit(0);
|
||||
}
|
||||
}
|
||||
};
|
||||
reportAndQuit("--protocolVersion", [&](FILE* fp) {
|
||||
DependencyManager::set<AddressManager>();
|
||||
auto version = DependencyManager::get<AddressManager>()->protocolVersion();
|
||||
fputs(version.toLatin1().data(), fp);
|
||||
});
|
||||
reportAndQuit("--version", [&](FILE* fp) {
|
||||
fputs(BuildInfo::VERSION.toLatin1().data(), fp);
|
||||
});
|
||||
|
||||
const char* portStr = getCmdOption(argc, constArgv, "--listenPort");
|
||||
const int listenPort = portStr ? atoi(portStr) : INVALID_PORT;
|
||||
|
||||
|
@ -511,6 +540,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) {
|
|||
DependencyManager::set<HMDScriptingInterface>();
|
||||
DependencyManager::set<ResourceScriptingInterface>();
|
||||
DependencyManager::set<TabletScriptingInterface>();
|
||||
DependencyManager::set<InputConfiguration>();
|
||||
DependencyManager::set<ToolbarScriptingInterface>();
|
||||
DependencyManager::set<UserActivityLoggerScriptingInterface>();
|
||||
DependencyManager::set<AssetMappingsScriptingInterface>();
|
||||
|
@ -1997,6 +2027,7 @@ void Application::initializeUi() {
|
|||
surfaceContext->setContextProperty("TextureCache", DependencyManager::get<TextureCache>().data());
|
||||
surfaceContext->setContextProperty("ModelCache", DependencyManager::get<ModelCache>().data());
|
||||
surfaceContext->setContextProperty("SoundCache", DependencyManager::get<SoundCache>().data());
|
||||
surfaceContext->setContextProperty("InputConfiguration", DependencyManager::get<InputConfiguration>().data());
|
||||
|
||||
surfaceContext->setContextProperty("Account", AccountScriptingInterface::getInstance());
|
||||
surfaceContext->setContextProperty("Tablet", DependencyManager::get<TabletScriptingInterface>().data());
|
||||
|
@ -2479,8 +2510,11 @@ void Application::handleSandboxStatus(QNetworkReply* reply) {
|
|||
Setting::Handle<bool> tutorialComplete{ "tutorialComplete", false };
|
||||
Setting::Handle<bool> firstRun{ Settings::firstRun, true };
|
||||
|
||||
const QString HIFI_SKIP_TUTORIAL_COMMAND_LINE_KEY = "--skipTutorial";
|
||||
// Skips tutorial/help behavior, and does NOT clear firstRun setting.
|
||||
bool skipTutorial = arguments().contains(HIFI_SKIP_TUTORIAL_COMMAND_LINE_KEY);
|
||||
bool isTutorialComplete = tutorialComplete.get();
|
||||
bool shouldGoToTutorial = isUsingHMDAndHandControllers && hasTutorialContent && !isTutorialComplete;
|
||||
bool shouldGoToTutorial = isUsingHMDAndHandControllers && hasTutorialContent && !isTutorialComplete && !skipTutorial;
|
||||
|
||||
qCDebug(interfaceapp) << "HMD:" << hasHMD << ", Hand Controllers: " << hasHandControllers << ", Using HMD: " << isUsingHMDAndHandControllers;
|
||||
qCDebug(interfaceapp) << "Tutorial version:" << contentVersion << ", sufficient:" << hasTutorialContent <<
|
||||
|
@ -2523,14 +2557,9 @@ void Application::handleSandboxStatus(QNetworkReply* reply) {
|
|||
}
|
||||
} else {
|
||||
|
||||
bool isFirstRun = firstRun.get();
|
||||
|
||||
if (isFirstRun) {
|
||||
showHelp();
|
||||
}
|
||||
|
||||
// If this is a first run we short-circuit the address passed in
|
||||
if (isFirstRun) {
|
||||
if (firstRun.get() && !skipTutorial) {
|
||||
showHelp();
|
||||
if (isUsingHMDAndHandControllers) {
|
||||
if (sandboxIsRunning) {
|
||||
qCDebug(interfaceapp) << "Home sandbox appears to be running, going to Home.";
|
||||
|
@ -2568,7 +2597,9 @@ void Application::handleSandboxStatus(QNetworkReply* reply) {
|
|||
_connectionMonitor.init();
|
||||
|
||||
// After all of the constructor is completed, then set firstRun to false.
|
||||
firstRun.set(false);
|
||||
if (!skipTutorial) {
|
||||
firstRun.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
bool Application::importJSONFromURL(const QString& urlString) {
|
||||
|
@ -4120,7 +4151,7 @@ void Application::updateMyAvatarLookAtPosition() {
|
|||
lookAtSpot = transformPoint(worldHeadMat, glm::vec3(0.0f, 0.0f, TREE_SCALE));
|
||||
} else {
|
||||
lookAtSpot = myAvatar->getHead()->getEyePosition() +
|
||||
(myAvatar->getHead()->getFinalOrientationInWorldFrame() * glm::vec3(0.0f, 0.0f, -TREE_SCALE));
|
||||
(myAvatar->getHead()->getFinalOrientationInWorldFrame() * glm::vec3(0.0f, 0.0f, TREE_SCALE));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5569,6 +5600,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri
|
|||
connect(scriptEngine, &ScriptEngine::errorMessage, DependencyManager::get<ScriptEngines>().data(), &ScriptEngines::onErrorMessage);
|
||||
connect(scriptEngine, &ScriptEngine::warningMessage, DependencyManager::get<ScriptEngines>().data(), &ScriptEngines::onWarningMessage);
|
||||
connect(scriptEngine, &ScriptEngine::infoMessage, DependencyManager::get<ScriptEngines>().data(), &ScriptEngines::onInfoMessage);
|
||||
connect(scriptEngine, &ScriptEngine::clearDebugWindow, DependencyManager::get<ScriptEngines>().data(), &ScriptEngines::onClearDebugWindow);
|
||||
}
|
||||
|
||||
bool Application::canAcceptURL(const QString& urlString) const {
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
#include <AudioClient.h>
|
||||
#include <CrashHelpers.h>
|
||||
#include <DependencyManager.h>
|
||||
#include <TabletScriptingInterface.h>
|
||||
#include <display-plugins/DisplayPlugin.h>
|
||||
#include <PathUtils.h>
|
||||
#include <SettingHandle.h>
|
||||
|
@ -37,6 +38,7 @@
|
|||
#include "MainWindow.h"
|
||||
#include "render/DrawStatus.h"
|
||||
#include "scripting/MenuScriptingInterface.h"
|
||||
#include "scripting/HMDScriptingInterface.h"
|
||||
#include "ui/DialogsManager.h"
|
||||
#include "ui/StandAloneJSConsole.h"
|
||||
#include "InterfaceLogging.h"
|
||||
|
@ -309,6 +311,17 @@ Menu::Menu() {
|
|||
QString("../../hifi/tablet/TabletLodPreferences.qml"), "LodPreferencesDialog");
|
||||
});
|
||||
|
||||
action = addActionToQMenuAndActionHash(settingsMenu, "Controller Settings");
|
||||
connect(action, &QAction::triggered, [] {
|
||||
auto tablet = DependencyManager::get<TabletScriptingInterface>()->getTablet("com.highfidelity.interface.tablet.system");
|
||||
auto hmd = DependencyManager::get<HMDScriptingInterface>();
|
||||
tablet->loadQMLSource("ControllerSettings.qml");
|
||||
|
||||
if (!hmd->getShouldShowTablet()) {
|
||||
hmd->toggleShouldShowTablet();
|
||||
}
|
||||
});
|
||||
|
||||
// Settings > Control with Speech [advanced]
|
||||
#if defined(Q_OS_MAC) || defined(Q_OS_WIN)
|
||||
auto speechRecognizer = DependencyManager::get<SpeechRecognizer>();
|
||||
|
|
|
@ -87,9 +87,10 @@ const float MyAvatar::ZOOM_MAX = 25.0f;
|
|||
const float MyAvatar::ZOOM_DEFAULT = 1.5f;
|
||||
|
||||
// default values, used when avatar is missing joints... (avatar space)
|
||||
// static const glm::quat DEFAULT_AVATAR_MIDDLE_EYE_ROT { Quaternions::Y_180 };
|
||||
static const glm::quat DEFAULT_AVATAR_MIDDLE_EYE_ROT { Quaternions::Y_180 };
|
||||
static const glm::vec3 DEFAULT_AVATAR_MIDDLE_EYE_POS { 0.0f, 0.6f, 0.0f };
|
||||
static const glm::vec3 DEFAULT_AVATAR_HEAD_POS { 0.0f, 0.53f, 0.0f };
|
||||
static const glm::quat DEFAULT_AVATAR_HEAD_ROT { Quaternions::Y_180 };
|
||||
static const glm::vec3 DEFAULT_AVATAR_RIGHTARM_POS { -0.134824f, 0.396348f, -0.0515777f };
|
||||
static const glm::quat DEFAULT_AVATAR_RIGHTARM_ROT { -0.536241f, 0.536241f, -0.460918f, -0.460918f };
|
||||
static const glm::vec3 DEFAULT_AVATAR_LEFTARM_POS { 0.134795f, 0.396349f, -0.0515881f };
|
||||
|
@ -100,7 +101,9 @@ static const glm::vec3 DEFAULT_AVATAR_LEFTHAND_POS { 0.727588f, 0.39635f, -0.051
|
|||
static const glm::quat DEFAULT_AVATAR_LEFTHAND_ROT { -0.479181f, -0.52001f, 0.52254f, -0.476369f };
|
||||
static const glm::vec3 DEFAULT_AVATAR_NECK_POS { 0.0f, 0.445f, 0.025f };
|
||||
static const glm::vec3 DEFAULT_AVATAR_SPINE2_POS { 0.0f, 0.32f, 0.02f };
|
||||
static const glm::quat DEFAULT_AVATAR_SPINE2_ROT { Quaternions::Y_180 };
|
||||
static const glm::vec3 DEFAULT_AVATAR_HIPS_POS { 0.0f, 0.0f, 0.0f };
|
||||
static const glm::quat DEFAULT_AVATAR_HIPS_ROT { Quaternions::Y_180 };
|
||||
static const glm::vec3 DEFAULT_AVATAR_LEFTFOOT_POS { -0.08f, -0.96f, 0.029f};
|
||||
static const glm::quat DEFAULT_AVATAR_LEFTFOOT_ROT { -0.40167322754859924f, 0.9154590368270874f, -0.005437685176730156f, -0.023744143545627594f };
|
||||
static const glm::vec3 DEFAULT_AVATAR_RIGHTFOOT_POS { 0.08f, -0.96f, 0.029f };
|
||||
|
@ -2764,7 +2767,7 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat
|
|||
const glm::mat4& currentBodyMatrix, bool hasDriveInput) {
|
||||
|
||||
if (myAvatar.getHMDLeanRecenterEnabled()) {
|
||||
if (!isActive(Rotation) && shouldActivateRotation(myAvatar, desiredBodyMatrix, currentBodyMatrix)) {
|
||||
if (!isActive(Rotation) && (shouldActivateRotation(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) {
|
||||
activate(Rotation);
|
||||
}
|
||||
if (!isActive(Horizontal) && shouldActivateHorizontal(myAvatar, desiredBodyMatrix, currentBodyMatrix)) {
|
||||
|
@ -2966,7 +2969,7 @@ glm::mat4 MyAvatar::getCenterEyeCalibrationMat() const {
|
|||
auto centerEyeRot = Quaternions::Y_180;
|
||||
return createMatFromQuatAndPos(centerEyeRot, centerEyePos);
|
||||
} else {
|
||||
return createMatFromQuatAndPos(DEFAULT_AVATAR_MIDDLE_EYE_POS, DEFAULT_AVATAR_MIDDLE_EYE_POS);
|
||||
return createMatFromQuatAndPos(DEFAULT_AVATAR_MIDDLE_EYE_ROT, DEFAULT_AVATAR_MIDDLE_EYE_POS);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2978,7 +2981,7 @@ glm::mat4 MyAvatar::getHeadCalibrationMat() const {
|
|||
auto headRot = getAbsoluteDefaultJointRotationInObjectFrame(headIndex);
|
||||
return createMatFromQuatAndPos(headRot, headPos);
|
||||
} else {
|
||||
return createMatFromQuatAndPos(DEFAULT_AVATAR_HEAD_POS, DEFAULT_AVATAR_HEAD_POS);
|
||||
return createMatFromQuatAndPos(DEFAULT_AVATAR_HEAD_ROT, DEFAULT_AVATAR_HEAD_POS);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2990,7 +2993,7 @@ glm::mat4 MyAvatar::getSpine2CalibrationMat() const {
|
|||
auto spine2Rot = getAbsoluteDefaultJointRotationInObjectFrame(spine2Index);
|
||||
return createMatFromQuatAndPos(spine2Rot, spine2Pos);
|
||||
} else {
|
||||
return createMatFromQuatAndPos(DEFAULT_AVATAR_SPINE2_POS, DEFAULT_AVATAR_SPINE2_POS);
|
||||
return createMatFromQuatAndPos(DEFAULT_AVATAR_SPINE2_ROT, DEFAULT_AVATAR_SPINE2_POS);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3002,7 +3005,7 @@ glm::mat4 MyAvatar::getHipsCalibrationMat() const {
|
|||
auto hipsRot = getAbsoluteDefaultJointRotationInObjectFrame(hipsIndex);
|
||||
return createMatFromQuatAndPos(hipsRot, hipsPos);
|
||||
} else {
|
||||
return createMatFromQuatAndPos(DEFAULT_AVATAR_HIPS_POS, DEFAULT_AVATAR_HIPS_POS);
|
||||
return createMatFromQuatAndPos(DEFAULT_AVATAR_HIPS_ROT, DEFAULT_AVATAR_HIPS_POS);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -56,6 +56,7 @@ class MyAvatar : public Avatar {
|
|||
*
|
||||
* @namespace MyAvatar
|
||||
* @augments Avatar
|
||||
* @property qmlPosition {Vec3} Used as a stopgap for position access by QML, as glm::vec3 is unavailable outside of scripts
|
||||
* @property shouldRenderLocally {bool} Set it to true if you would like to see MyAvatar in your local interface,
|
||||
* and false if you would not like to see MyAvatar in your local interface.
|
||||
* @property motorVelocity {Vec3} Can be used to move the avatar with this velocity.
|
||||
|
@ -101,6 +102,10 @@ class MyAvatar : public Avatar {
|
|||
* "scripts/system/controllers/toggleAdvancedMovementForHandControllers.js".
|
||||
*/
|
||||
|
||||
// FIXME: `glm::vec3 position` is not accessible from QML, so this exposes position in a QML-native type
|
||||
Q_PROPERTY(QVector3D qmlPosition READ getQmlPosition)
|
||||
QVector3D getQmlPosition() { auto p = getPosition(); return QVector3D(p.x, p.y, p.z); }
|
||||
|
||||
Q_PROPERTY(bool shouldRenderLocally READ getShouldRenderLocally WRITE setShouldRenderLocally)
|
||||
Q_PROPERTY(glm::vec3 motorVelocity READ getScriptedMotorVelocity WRITE setScriptedMotorVelocity)
|
||||
Q_PROPERTY(float motorTimescale READ getScriptedMotorTimescale WRITE setScriptedMotorTimescale)
|
||||
|
|
|
@ -23,9 +23,33 @@ QString Audio::HMD { "VR" };
|
|||
|
||||
Setting::Handle<bool> enableNoiseReductionSetting { QStringList { Audio::AUDIO, "NoiseReduction" }, true };
|
||||
|
||||
float Audio::loudnessToLevel(float loudness) {
|
||||
const float LOG2 = log(2.0f);
|
||||
const float METER_LOUDNESS_SCALE = 2.8f / 5.0f;
|
||||
const float LOG2_LOUDNESS_FLOOR = 11.0f;
|
||||
|
||||
float level = 0.0f;
|
||||
|
||||
loudness += 1.0f;
|
||||
float log2loudness = logf(loudness) / LOG2;
|
||||
|
||||
if (log2loudness <= LOG2_LOUDNESS_FLOOR) {
|
||||
level = (log2loudness / LOG2_LOUDNESS_FLOOR) * METER_LOUDNESS_SCALE;
|
||||
} else {
|
||||
level = (log2loudness - (LOG2_LOUDNESS_FLOOR - 1.0f)) * METER_LOUDNESS_SCALE;
|
||||
}
|
||||
|
||||
if (level > 1.0f) {
|
||||
level = 1.0;
|
||||
}
|
||||
|
||||
return level;
|
||||
}
|
||||
|
||||
Audio::Audio() : _devices(_contextIsHMD) {
|
||||
auto client = DependencyManager::get<AudioClient>();
|
||||
connect(client.data(), &AudioClient::muteToggled, this, &Audio::onMutedChanged);
|
||||
auto client = DependencyManager::get<AudioClient>().data();
|
||||
connect(client, &AudioClient::muteToggled, this, &Audio::onMutedChanged);
|
||||
connect(client, &AudioClient::inputLoudnessChanged, this, &Audio::onInputLoudnessChanged);
|
||||
connect(this, &Audio::contextChanged, &_devices, &AudioDevices::onContextChanged);
|
||||
connect(&_devices._inputs, &AudioDeviceList::deviceChanged, this, &Audio::onInputChanged);
|
||||
enableNoiseReduction(enableNoiseReductionSetting.get());
|
||||
|
@ -88,6 +112,15 @@ void Audio::onInputChanged() {
|
|||
}
|
||||
}
|
||||
|
||||
void Audio::onInputLoudnessChanged(float loudness) {
|
||||
float level = loudnessToLevel(loudness);
|
||||
|
||||
if (_inputLevel != level) {
|
||||
_inputLevel = level;
|
||||
emit inputLevelChanged(_inputLevel);
|
||||
}
|
||||
}
|
||||
|
||||
QString Audio::getContext() const {
|
||||
return _contextIsHMD ? Audio::HMD : Audio::DESKTOP;
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ class Audio : public AudioScriptingInterface {
|
|||
Q_PROPERTY(bool muted READ isMuted WRITE setMuted NOTIFY mutedChanged)
|
||||
Q_PROPERTY(bool noiseReduction READ noiseReductionEnabled WRITE enableNoiseReduction NOTIFY noiseReductionChanged)
|
||||
Q_PROPERTY(float inputVolume READ getInputVolume WRITE setInputVolume NOTIFY inputVolumeChanged)
|
||||
Q_PROPERTY(float inputLevel READ getInputLevel NOTIFY inputLevelChanged)
|
||||
Q_PROPERTY(QString context READ getContext NOTIFY contextChanged)
|
||||
Q_PROPERTY(AudioDevices* devices READ getDevices NOTIFY nop)
|
||||
|
||||
|
@ -34,11 +35,14 @@ public:
|
|||
static QString HMD;
|
||||
static QString DESKTOP;
|
||||
|
||||
static float loudnessToLevel(float loudness);
|
||||
|
||||
virtual ~Audio() {}
|
||||
|
||||
bool isMuted() const { return _isMuted; }
|
||||
bool noiseReductionEnabled() const { return _enableNoiseReduction; }
|
||||
float getInputVolume() const { return _inputVolume; }
|
||||
float getInputLevel() const { return _inputLevel; }
|
||||
QString getContext() const;
|
||||
|
||||
void setMuted(bool muted);
|
||||
|
@ -54,12 +58,14 @@ signals:
|
|||
void mutedChanged(bool isMuted);
|
||||
void noiseReductionChanged(bool isEnabled);
|
||||
void inputVolumeChanged(float volume);
|
||||
void inputLevelChanged(float level);
|
||||
void contextChanged(const QString& context);
|
||||
|
||||
public slots:
|
||||
void onMutedChanged();
|
||||
void onContextChanged();
|
||||
void onInputChanged();
|
||||
void onInputLoudnessChanged(float loudness);
|
||||
|
||||
protected:
|
||||
// Audio must live on a separate thread from AudioClient to avoid deadlocks
|
||||
|
@ -68,6 +74,7 @@ protected:
|
|||
private:
|
||||
|
||||
float _inputVolume { 1.0f };
|
||||
float _inputLevel { 0.0f };
|
||||
bool _isMuted { false };
|
||||
bool _enableNoiseReduction;
|
||||
bool _contextIsHMD { false };
|
||||
|
|
|
@ -45,15 +45,6 @@ AvatarInputs::AvatarInputs(QQuickItem* parent) : QQuickItem(parent) {
|
|||
} \
|
||||
}
|
||||
|
||||
#define AI_UPDATE_FLOAT(name, src, epsilon) \
|
||||
{ \
|
||||
float val = src; \
|
||||
if (fabsf(_##name - val) >= epsilon) { \
|
||||
_##name = val; \
|
||||
emit name##Changed(); \
|
||||
} \
|
||||
}
|
||||
|
||||
float AvatarInputs::loudnessToAudioLevel(float loudness) {
|
||||
const float AUDIO_METER_AVERAGING = 0.5;
|
||||
const float LOG2 = log(2.0f);
|
||||
|
@ -85,27 +76,6 @@ void AvatarInputs::update() {
|
|||
AI_UPDATE(cameraEnabled, !Menu::getInstance()->isOptionChecked(MenuOption::NoFaceTracking));
|
||||
AI_UPDATE(cameraMuted, Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking));
|
||||
AI_UPDATE(isHMD, qApp->isHMDMode());
|
||||
|
||||
auto audioIO = DependencyManager::get<AudioClient>();
|
||||
|
||||
const float audioLevel = loudnessToAudioLevel(DependencyManager::get<AudioClient>()->getLastInputLoudness());
|
||||
|
||||
AI_UPDATE_FLOAT(audioLevel, audioLevel, 0.01f);
|
||||
AI_UPDATE(audioClipping, ((audioIO->getTimeSinceLastClip() > 0.0f) && (audioIO->getTimeSinceLastClip() < 1.0f)));
|
||||
AI_UPDATE(audioMuted, audioIO->isMuted());
|
||||
|
||||
//// Make muted icon pulsate
|
||||
//static const float PULSE_MIN = 0.4f;
|
||||
//static const float PULSE_MAX = 1.0f;
|
||||
//static const float PULSE_FREQUENCY = 1.0f; // in Hz
|
||||
//qint64 now = usecTimestampNow();
|
||||
//if (now - _iconPulseTimeReference > (qint64)USECS_PER_SECOND) {
|
||||
// // Prevents t from getting too big, which would diminish glm::cos precision
|
||||
// _iconPulseTimeReference = now - ((now - _iconPulseTimeReference) % USECS_PER_SECOND);
|
||||
//}
|
||||
//float t = (float)(now - _iconPulseTimeReference) / (float)USECS_PER_SECOND;
|
||||
//float pulseFactor = (glm::cos(t * PULSE_FREQUENCY * 2.0f * PI) + 1.0f) / 2.0f;
|
||||
//iconColor = PULSE_MIN + (PULSE_MAX - PULSE_MIN) * pulseFactor;
|
||||
}
|
||||
|
||||
void AvatarInputs::setShowAudioTools(bool showAudioTools) {
|
||||
|
@ -124,10 +94,6 @@ void AvatarInputs::toggleCameraMute() {
|
|||
}
|
||||
}
|
||||
|
||||
void AvatarInputs::toggleAudioMute() {
|
||||
DependencyManager::get<AudioClient>()->toggleMute();
|
||||
}
|
||||
|
||||
void AvatarInputs::resetSensors() {
|
||||
qApp->resetSensors();
|
||||
}
|
||||
|
|
|
@ -25,9 +25,6 @@ class AvatarInputs : public QQuickItem {
|
|||
|
||||
AI_PROPERTY(bool, cameraEnabled, false)
|
||||
AI_PROPERTY(bool, cameraMuted, false)
|
||||
AI_PROPERTY(bool, audioMuted, false)
|
||||
AI_PROPERTY(bool, audioClipping, false)
|
||||
AI_PROPERTY(float, audioLevel, 0)
|
||||
AI_PROPERTY(bool, isHMD, false)
|
||||
|
||||
Q_PROPERTY(bool showAudioTools READ showAudioTools WRITE setShowAudioTools NOTIFY showAudioToolsChanged)
|
||||
|
@ -45,16 +42,12 @@ public slots:
|
|||
signals:
|
||||
void cameraEnabledChanged();
|
||||
void cameraMutedChanged();
|
||||
void audioMutedChanged();
|
||||
void audioClippingChanged();
|
||||
void audioLevelChanged();
|
||||
void isHMDChanged();
|
||||
void showAudioToolsChanged(bool show);
|
||||
|
||||
protected:
|
||||
Q_INVOKABLE void resetSensors();
|
||||
Q_INVOKABLE void toggleCameraMute();
|
||||
Q_INVOKABLE void toggleAudioMute();
|
||||
|
||||
private:
|
||||
float _trailingAudioLoudness{ 0 };
|
||||
|
|
|
@ -50,7 +50,9 @@
|
|||
#include "ui/AvatarInputs.h"
|
||||
#include "avatar/AvatarManager.h"
|
||||
#include "scripting/GlobalServicesScriptingInterface.h"
|
||||
#include <plugins/InputConfiguration.h>
|
||||
#include "ui/Snapshot.h"
|
||||
#include "SoundCache.h"
|
||||
|
||||
static const float DPI = 30.47f;
|
||||
static const float INCHES_TO_METERS = 1.0f / 39.3701f;
|
||||
|
@ -181,6 +183,7 @@ void Web3DOverlay::loadSourceURL() {
|
|||
if (_webSurface->getRootItem() && _webSurface->getRootItem()->objectName() == "tabletRoot") {
|
||||
auto tabletScriptingInterface = DependencyManager::get<TabletScriptingInterface>();
|
||||
auto flags = tabletScriptingInterface->getFlags();
|
||||
|
||||
_webSurface->getSurfaceContext()->setContextProperty("offscreenFlags", flags);
|
||||
_webSurface->getSurfaceContext()->setContextProperty("AddressManager", DependencyManager::get<AddressManager>().data());
|
||||
_webSurface->getSurfaceContext()->setContextProperty("Account", AccountScriptingInterface::getInstance());
|
||||
|
@ -199,6 +202,8 @@ void Web3DOverlay::loadSourceURL() {
|
|||
_webSurface->getSurfaceContext()->setContextProperty("GlobalServices", GlobalServicesScriptingInterface::getInstance());
|
||||
_webSurface->getSurfaceContext()->setContextProperty("AvatarList", DependencyManager::get<AvatarManager>().data());
|
||||
_webSurface->getSurfaceContext()->setContextProperty("DialogsManager", DialogsManagerScriptingInterface::getInstance());
|
||||
_webSurface->getSurfaceContext()->setContextProperty("InputConfiguration", DependencyManager::get<InputConfiguration>().data());
|
||||
_webSurface->getSurfaceContext()->setContextProperty("SoundCache", DependencyManager::get<SoundCache>().data());
|
||||
|
||||
_webSurface->getSurfaceContext()->setContextProperty("pathToFonts", "../../");
|
||||
tabletScriptingInterface->setQmlTabletRoot("com.highfidelity.interface.tablet.system", _webSurface->getRootItem(), _webSurface.data());
|
||||
|
|
|
@ -1023,6 +1023,8 @@ void AudioClient::handleAudioInput(QByteArray& audioBuffer) {
|
|||
emit inputReceived(audioBuffer);
|
||||
}
|
||||
|
||||
emit inputLoudnessChanged(_lastInputLoudness);
|
||||
|
||||
// state machine to detect gate opening and closing
|
||||
bool audioGateOpen = (_lastInputLoudness != 0.0f);
|
||||
bool openedInLastBlock = !_audioGateOpen && audioGateOpen; // the gate just opened
|
||||
|
|
|
@ -210,6 +210,7 @@ signals:
|
|||
bool muteToggled();
|
||||
void mutedByMixer();
|
||||
void inputReceived(const QByteArray& inputSamples);
|
||||
void inputLoudnessChanged(float loudness);
|
||||
void outputBytesToNetwork(int numBytes);
|
||||
void inputBytesFromNetwork(int numBytes);
|
||||
void noiseGateOpened();
|
||||
|
|
|
@ -127,6 +127,7 @@ int InboundAudioStream::parseData(ReceivedMessage& message) {
|
|||
// parse the info after the seq number and before the audio data (the stream properties)
|
||||
int prePropertyPosition = message.getPosition();
|
||||
int propertyBytes = parseStreamProperties(message.getType(), message.readWithoutCopy(message.getBytesLeftToRead()), networkFrames);
|
||||
|
||||
message.seek(prePropertyPosition + propertyBytes);
|
||||
|
||||
// handle this packet based on its arrival status.
|
||||
|
@ -147,7 +148,8 @@ int InboundAudioStream::parseData(ReceivedMessage& message) {
|
|||
}
|
||||
case SequenceNumberStats::OnTime: {
|
||||
// Packet is on time; parse its data to the ringbuffer
|
||||
if (message.getType() == PacketType::SilentAudioFrame) {
|
||||
if (message.getType() == PacketType::SilentAudioFrame
|
||||
|| message.getType() == PacketType::ReplicatedSilentAudioFrame) {
|
||||
// If we recieved a SilentAudioFrame from our sender, we might want to drop
|
||||
// some of the samples in order to catch up to our desired jitter buffer size.
|
||||
writeDroppableSilentFrames(networkFrames);
|
||||
|
@ -168,7 +170,10 @@ int InboundAudioStream::parseData(ReceivedMessage& message) {
|
|||
|
||||
// inform others of the mismatch
|
||||
auto sendingNode = DependencyManager::get<NodeList>()->nodeWithUUID(message.getSourceID());
|
||||
emit mismatchedAudioCodec(sendingNode, _selectedCodecName, codecInPacket);
|
||||
if (sendingNode) {
|
||||
emit mismatchedAudioCodec(sendingNode, _selectedCodecName, codecInPacket);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
|
|
@ -1473,27 +1473,6 @@ QStringList AvatarData::getJointNames() const {
|
|||
return _jointNames;
|
||||
}
|
||||
|
||||
void AvatarData::parseAvatarIdentityPacket(const QByteArray& data, Identity& identityOut) {
|
||||
QDataStream packetStream(data);
|
||||
|
||||
packetStream >> identityOut.uuid
|
||||
>> identityOut.skeletonModelURL
|
||||
>> identityOut.attachmentData
|
||||
>> identityOut.displayName
|
||||
>> identityOut.sessionDisplayName
|
||||
>> identityOut.avatarEntityData
|
||||
>> identityOut.sequenceId;
|
||||
|
||||
#ifdef WANT_DEBUG
|
||||
qCDebug(avatars) << __FUNCTION__
|
||||
<< "identityOut.uuid:" << identityOut.uuid
|
||||
<< "identityOut.skeletonModelURL:" << identityOut.skeletonModelURL
|
||||
<< "identityOut.displayName:" << identityOut.displayName
|
||||
<< "identityOut.sessionDisplayName:" << identityOut.sessionDisplayName;
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
glm::quat AvatarData::getOrientationOutbound() const {
|
||||
return (getLocalOrientation());
|
||||
}
|
||||
|
@ -1504,47 +1483,82 @@ QUrl AvatarData::cannonicalSkeletonModelURL(const QUrl& emptyURL) const {
|
|||
return _skeletonModelURL.scheme() == "file" ? emptyURL : _skeletonModelURL;
|
||||
}
|
||||
|
||||
void AvatarData::processAvatarIdentity(const Identity& identity, bool& identityChanged, bool& displayNameChanged, bool& skeletonModelUrlChanged) {
|
||||
void AvatarData::processAvatarIdentity(const QByteArray& identityData, bool& identityChanged,
|
||||
bool& displayNameChanged, bool& skeletonModelUrlChanged) {
|
||||
|
||||
if (identity.sequenceId < _identitySequenceId) {
|
||||
qCDebug(avatars) << "Ignoring older identity packet for avatar" << getSessionUUID()
|
||||
<< "_identitySequenceId (" << _identitySequenceId << ") is greater than" << identity.sequenceId;
|
||||
return;
|
||||
QDataStream packetStream(identityData);
|
||||
|
||||
QUuid avatarSessionID;
|
||||
|
||||
// peek the sequence number, this will tell us if we should be processing this identity packet at all
|
||||
udt::SequenceNumber::Type incomingSequenceNumberType;
|
||||
packetStream >> avatarSessionID >> incomingSequenceNumberType;
|
||||
udt::SequenceNumber incomingSequenceNumber(incomingSequenceNumberType);
|
||||
|
||||
if (!_hasProcessedFirstIdentity) {
|
||||
_lastSequenceNumber = incomingSequenceNumber - 1;
|
||||
_hasProcessedFirstIdentity = true;
|
||||
qCDebug(avatars) << "Processing first identity packet for" << avatarSessionID << "-"
|
||||
<< (udt::SequenceNumber::Type) incomingSequenceNumber;
|
||||
}
|
||||
// otherwise, set the identitySequenceId to match the incoming identity
|
||||
_identitySequenceId = identity.sequenceId;
|
||||
|
||||
if (_firstSkeletonCheck || (identity.skeletonModelURL != cannonicalSkeletonModelURL(emptyURL))) {
|
||||
setSkeletonModelURL(identity.skeletonModelURL);
|
||||
identityChanged = true;
|
||||
skeletonModelUrlChanged = true;
|
||||
if (_firstSkeletonCheck) {
|
||||
if (incomingSequenceNumber > _lastSequenceNumber) {
|
||||
Identity identity;
|
||||
|
||||
packetStream >> identity.skeletonModelURL
|
||||
>> identity.attachmentData
|
||||
>> identity.displayName
|
||||
>> identity.sessionDisplayName
|
||||
>> identity.avatarEntityData;
|
||||
|
||||
// set the store identity sequence number to match the incoming identity
|
||||
_lastSequenceNumber = incomingSequenceNumber;
|
||||
|
||||
if (_firstSkeletonCheck || (identity.skeletonModelURL != cannonicalSkeletonModelURL(emptyURL))) {
|
||||
setSkeletonModelURL(identity.skeletonModelURL);
|
||||
identityChanged = true;
|
||||
skeletonModelUrlChanged = true;
|
||||
if (_firstSkeletonCheck) {
|
||||
displayNameChanged = true;
|
||||
}
|
||||
_firstSkeletonCheck = false;
|
||||
}
|
||||
|
||||
if (identity.displayName != _displayName) {
|
||||
_displayName = identity.displayName;
|
||||
identityChanged = true;
|
||||
displayNameChanged = true;
|
||||
}
|
||||
_firstSkeletonCheck = false;
|
||||
}
|
||||
maybeUpdateSessionDisplayNameFromTransport(identity.sessionDisplayName);
|
||||
|
||||
if (identity.displayName != _displayName) {
|
||||
_displayName = identity.displayName;
|
||||
identityChanged = true;
|
||||
displayNameChanged = true;
|
||||
}
|
||||
maybeUpdateSessionDisplayNameFromTransport(identity.sessionDisplayName);
|
||||
if (identity.attachmentData != _attachmentData) {
|
||||
setAttachmentData(identity.attachmentData);
|
||||
identityChanged = true;
|
||||
}
|
||||
|
||||
if (identity.attachmentData != _attachmentData) {
|
||||
setAttachmentData(identity.attachmentData);
|
||||
identityChanged = true;
|
||||
}
|
||||
bool avatarEntityDataChanged = false;
|
||||
_avatarEntitiesLock.withReadLock([&] {
|
||||
avatarEntityDataChanged = (identity.avatarEntityData != _avatarEntityData);
|
||||
});
|
||||
|
||||
if (avatarEntityDataChanged) {
|
||||
setAvatarEntityData(identity.avatarEntityData);
|
||||
identityChanged = true;
|
||||
}
|
||||
|
||||
bool avatarEntityDataChanged = false;
|
||||
_avatarEntitiesLock.withReadLock([&] {
|
||||
avatarEntityDataChanged = (identity.avatarEntityData != _avatarEntityData);
|
||||
});
|
||||
if (avatarEntityDataChanged) {
|
||||
setAvatarEntityData(identity.avatarEntityData);
|
||||
identityChanged = true;
|
||||
}
|
||||
#ifdef WANT_DEBUG
|
||||
qCDebug(avatars) << __FUNCTION__
|
||||
<< "identity.uuid:" << identity.uuid
|
||||
<< "identity.skeletonModelURL:" << identity.skeletonModelURL
|
||||
<< "identity.displayName:" << identity.displayName
|
||||
<< "identity.sessionDisplayName:" << identity.sessionDisplayName;
|
||||
} else {
|
||||
|
||||
qCDebug(avatars) << "Refusing to process identity for" << uuidStringWithoutCurlyBraces(avatarSessionID) << "since"
|
||||
<< (udt::SequenceNumber::Type) _lastSequenceNumber
|
||||
<< "is >=" << (udt::SequenceNumber::Type) incomingSequenceNumber;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray AvatarData::identityByteArray() const {
|
||||
|
@ -1552,14 +1566,17 @@ QByteArray AvatarData::identityByteArray() const {
|
|||
QDataStream identityStream(&identityData, QIODevice::Append);
|
||||
const QUrl& urlToSend = cannonicalSkeletonModelURL(emptyURL); // depends on _skeletonModelURL
|
||||
|
||||
// when mixers send identity packets to agents, they simply forward along the last incoming sequence number they received
|
||||
// whereas agents send a fresh outgoing sequence number when identity data has changed
|
||||
|
||||
_avatarEntitiesLock.withReadLock([&] {
|
||||
identityStream << getSessionUUID()
|
||||
<< urlToSend
|
||||
<< _attachmentData
|
||||
<< _displayName
|
||||
<< getSessionDisplayNameForTransport() // depends on _sessionDisplayName
|
||||
<< _avatarEntityData
|
||||
<< _identitySequenceId;
|
||||
<< (udt::SequenceNumber::Type) _lastSequenceNumber
|
||||
<< urlToSend
|
||||
<< _attachmentData
|
||||
<< _displayName
|
||||
<< getSessionDisplayNameForTransport() // depends on _sessionDisplayName
|
||||
<< _avatarEntityData;
|
||||
});
|
||||
|
||||
return identityData;
|
||||
|
@ -1735,6 +1752,12 @@ void AvatarData::sendAvatarDataPacket() {
|
|||
|
||||
void AvatarData::sendIdentityPacket() {
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
|
||||
if (_identityDataChanged) {
|
||||
// if the identity data has changed, push the sequence number forwards
|
||||
++_lastSequenceNumber;
|
||||
}
|
||||
|
||||
QByteArray identityData = identityByteArray();
|
||||
|
||||
auto packetList = NLPacketList::create(PacketType::AvatarIdentity, QByteArray(), true, true);
|
||||
|
@ -1745,7 +1768,7 @@ void AvatarData::sendIdentityPacket() {
|
|||
},
|
||||
[&](const SharedNodePointer& node) {
|
||||
nodeList->sendPacketList(std::move(packetList), *node);
|
||||
});
|
||||
});
|
||||
|
||||
_avatarEntityDataLocallyEdited = false;
|
||||
_identityDataChanged = false;
|
||||
|
|
|
@ -52,15 +52,16 @@ typedef unsigned long long quint64;
|
|||
#include <JointData.h>
|
||||
#include <NLPacket.h>
|
||||
#include <Node.h>
|
||||
#include <RegisteredMetaTypes.h>
|
||||
#include <SimpleMovingAverage.h>
|
||||
#include <SpatiallyNestable.h>
|
||||
#include <NumericalConstants.h>
|
||||
#include <Packed.h>
|
||||
#include <ThreadSafeValueCache.h>
|
||||
#include <RegisteredMetaTypes.h>
|
||||
#include <SharedUtil.h>
|
||||
#include <shared/RateCounter.h>
|
||||
#include <SimpleMovingAverage.h>
|
||||
#include <SpatiallyNestable.h>
|
||||
#include <ThreadSafeValueCache.h>
|
||||
#include <ViewFrustum.h>
|
||||
#include <shared/RateCounter.h>
|
||||
#include <udt/SequenceNumber.h>
|
||||
|
||||
#include "AABox.h"
|
||||
#include "HeadData.h"
|
||||
|
@ -526,19 +527,17 @@ public:
|
|||
const HeadData* getHeadData() const { return _headData; }
|
||||
|
||||
struct Identity {
|
||||
QUuid uuid;
|
||||
QUrl skeletonModelURL;
|
||||
QVector<AttachmentData> attachmentData;
|
||||
QString displayName;
|
||||
QString sessionDisplayName;
|
||||
AvatarEntityMap avatarEntityData;
|
||||
quint64 sequenceId;
|
||||
};
|
||||
|
||||
static void parseAvatarIdentityPacket(const QByteArray& data, Identity& identityOut);
|
||||
|
||||
// identityChanged returns true if identity has changed, false otherwise.
|
||||
// identityChanged returns true if identity has changed, false otherwise. Similarly for displayNameChanged and skeletonModelUrlChange.
|
||||
void processAvatarIdentity(const Identity& identity, bool& identityChanged, bool& displayNameChanged, bool& skeletonModelUrlChanged);
|
||||
void processAvatarIdentity(const QByteArray& identityData, bool& identityChanged,
|
||||
bool& displayNameChanged, bool& skeletonModelUrlChanged);
|
||||
|
||||
QByteArray identityByteArray() const;
|
||||
|
||||
|
@ -624,10 +623,7 @@ public:
|
|||
static float _avatarSortCoefficientAge;
|
||||
|
||||
bool getIdentityDataChanged() const { return _identityDataChanged; } // has the identity data changed since the last time sendIdentityPacket() was called
|
||||
void markIdentityDataChanged() {
|
||||
_identityDataChanged = true;
|
||||
_identitySequenceId++;
|
||||
}
|
||||
void markIdentityDataChanged() { _identityDataChanged = true; }
|
||||
|
||||
float getDensity() const { return _density; }
|
||||
|
||||
|
@ -785,7 +781,8 @@ protected:
|
|||
float _audioAverageLoudness { 0.0f };
|
||||
|
||||
bool _identityDataChanged { false };
|
||||
quint64 _identitySequenceId { 0 };
|
||||
udt::SequenceNumber _lastSequenceNumber { 0 };
|
||||
bool _hasProcessedFirstIdentity { false };
|
||||
float _density;
|
||||
|
||||
private:
|
||||
|
|
|
@ -126,8 +126,14 @@ AvatarSharedPointer AvatarHashMap::parseAvatarData(QSharedPointer<ReceivedMessag
|
|||
}
|
||||
|
||||
void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
|
||||
AvatarData::Identity identity;
|
||||
AvatarData::parseAvatarIdentityPacket(message->getMessage(), identity);
|
||||
|
||||
// peek the avatar UUID from the incoming packet
|
||||
QUuid identityUUID = QUuid::fromRfc4122(message->peek(NUM_BYTES_RFC4122_UUID));
|
||||
|
||||
if (identityUUID.isNull()) {
|
||||
qCDebug(avatars) << "Refusing to process identity packet for null avatar ID";
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure this isn't for an ignored avatar
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
|
@ -136,21 +142,22 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer<ReceivedMessage>
|
|||
{
|
||||
QReadLocker locker(&_hashLock);
|
||||
auto me = _avatarHash.find(EMPTY);
|
||||
if ((me != _avatarHash.end()) && (identity.uuid == me.value()->getSessionUUID())) {
|
||||
if ((me != _avatarHash.end()) && (identityUUID == me.value()->getSessionUUID())) {
|
||||
// We add MyAvatar to _avatarHash with an empty UUID. Code relies on this. In order to correctly handle an
|
||||
// identity packet for ourself (such as when we are assigned a sessionDisplayName by the mixer upon joining),
|
||||
// we make things match here.
|
||||
identity.uuid = EMPTY;
|
||||
identityUUID = EMPTY;
|
||||
}
|
||||
}
|
||||
if (!nodeList->isIgnoringNode(identity.uuid) || nodeList->getRequestsDomainListData()) {
|
||||
|
||||
if (!nodeList->isIgnoringNode(identityUUID) || nodeList->getRequestsDomainListData()) {
|
||||
// mesh URL for a UUID, find avatar in our list
|
||||
auto avatar = newOrExistingAvatar(identity.uuid, sendingNode);
|
||||
auto avatar = newOrExistingAvatar(identityUUID, sendingNode);
|
||||
bool identityChanged = false;
|
||||
bool displayNameChanged = false;
|
||||
bool skeletonModelUrlChanged = false;
|
||||
// In this case, the "sendingNode" is the Avatar Mixer.
|
||||
avatar->processAvatarIdentity(identity, identityChanged, displayNameChanged, skeletonModelUrlChanged);
|
||||
avatar->processAvatarIdentity(message->getMessage(), identityChanged, displayNameChanged, skeletonModelUrlChanged);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -246,8 +246,13 @@ bool EntityTreeRenderer::findBestZoneAndMaybeContainingEntities(QVector<EntityIt
|
|||
|
||||
// if this entity is a zone and visible, determine if it is the bestZone
|
||||
if (isZone && entity->getVisible()) {
|
||||
auto zone = std::dynamic_pointer_cast<ZoneEntityItem>(entity);
|
||||
_layeredZones.insert(zone);
|
||||
auto renderID = std::dynamic_pointer_cast<RenderableZoneEntityItem>(entity)->getRenderItemID();
|
||||
bool isValidRenderID = (renderID != render::Item::INVALID_ITEM_ID);
|
||||
|
||||
if (isValidRenderID) {
|
||||
auto zone = std::dynamic_pointer_cast<ZoneEntityItem>(entity);
|
||||
_layeredZones.insert(zone);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -354,6 +359,7 @@ bool EntityTreeRenderer::applyLayeredZones() {
|
|||
|
||||
for (auto& zone : _layeredZones) {
|
||||
auto id = std::dynamic_pointer_cast<RenderableZoneEntityItem>(zone.zone)->getRenderItemID();
|
||||
Q_ASSERT(id != render::Item::INVALID_ITEM_ID);
|
||||
list.push_back(id);
|
||||
}
|
||||
render::Selection selection("RankedZones", list);
|
||||
|
|
|
@ -386,7 +386,13 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef
|
|||
return 0;
|
||||
}
|
||||
|
||||
qint64 clockSkew = args.sourceNode ? args.sourceNode->getClockSkewUsec() : 0;
|
||||
int64_t clockSkew = 0;
|
||||
uint64_t maxPingRoundTrip = 33333; // two frames periods at 60 fps
|
||||
if (args.sourceNode) {
|
||||
clockSkew = args.sourceNode->getClockSkewUsec();
|
||||
const float MSECS_PER_USEC = 1000;
|
||||
maxPingRoundTrip += args.sourceNode->getPingMs() * MSECS_PER_USEC;
|
||||
}
|
||||
|
||||
BufferParser parser(data, bytesLeftToRead);
|
||||
|
||||
|
@ -653,7 +659,6 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef
|
|||
const QUuid& myNodeID = nodeList->getSessionUUID();
|
||||
bool weOwnSimulation = _simulationOwner.matchesValidID(myNodeID);
|
||||
|
||||
|
||||
// pack SimulationOwner and terse update properties near each other
|
||||
// NOTE: the server is authoritative for changes to simOwnerID so we always unpack ownership data
|
||||
// even when we would otherwise ignore the rest of the packet.
|
||||
|
@ -678,7 +683,7 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef
|
|||
if (newSimOwner.getID().isNull() && !_simulationOwner.pendingRelease(lastEditedFromBufferAdjusted)) {
|
||||
// entity-server is trying to clear our ownership (probably at our own request)
|
||||
// but we actually want to own it, therefore we ignore this clear event
|
||||
// and pretend that we own it (we assume we'll recover it soon)
|
||||
// and pretend that we own it (e.g. we assume we'll receive ownership soon)
|
||||
|
||||
// However, for now, when the server uses a newer time than what we sent, listen to what we're told.
|
||||
if (overwriteLocalData) {
|
||||
|
@ -690,16 +695,19 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef
|
|||
// recompute weOwnSimulation for later
|
||||
weOwnSimulation = _simulationOwner.matchesValidID(myNodeID);
|
||||
}
|
||||
} else if (newSimOwner.getID().isNull() && _simulationOwner.pendingTake(lastEditedFromBufferAdjusted)) {
|
||||
// entity-server is trying to clear someone else's ownership
|
||||
// but we want to own it, therefore we ignore this clear event
|
||||
// and pretend that we own it (we assume we'll get it soon)
|
||||
} else if (_simulationOwner.pendingTake(now - maxPingRoundTrip)) {
|
||||
// we sent a bid before this packet could have been sent from the server
|
||||
// so we ignore it and pretend we own the object's simulation
|
||||
weOwnSimulation = true;
|
||||
if (!_simulationOwner.isNull()) {
|
||||
// someone else really did own it
|
||||
markDirtyFlags(Simulation::DIRTY_SIMULATOR_ID);
|
||||
somethingChanged = true;
|
||||
_simulationOwner.clearCurrentOwner();
|
||||
if (newSimOwner.getID().isNull()) {
|
||||
// entity-server is trying to clear someone else's ownership
|
||||
// but we want to own it, therefore we ignore this clear event
|
||||
if (!_simulationOwner.isNull()) {
|
||||
// someone else really did own it
|
||||
markDirtyFlags(Simulation::DIRTY_SIMULATOR_ID);
|
||||
somethingChanged = true;
|
||||
_simulationOwner.clearCurrentOwner();
|
||||
}
|
||||
}
|
||||
} else if (newSimOwner.matchesValidID(myNodeID) && !_hasBidOnSimulation) {
|
||||
// entity-server tells us that we have simulation ownership while we never requested this for this EntityItem,
|
||||
|
|
|
@ -24,9 +24,10 @@ const int KTXCache::INVALID_VERSION = 0x00;
|
|||
const char* KTXCache::SETTING_VERSION_NAME = "hifi.ktx.cache_version";
|
||||
|
||||
KTXCache::KTXCache(const std::string& dir, const std::string& ext) :
|
||||
FileCache(dir, ext) {
|
||||
initialize();
|
||||
FileCache(dir, ext) { }
|
||||
|
||||
void KTXCache::initialize() {
|
||||
FileCache::initialize();
|
||||
Setting::Handle<int> cacheVersionHandle(SETTING_VERSION_NAME, INVALID_VERSION);
|
||||
auto cacheVersion = cacheVersionHandle.get();
|
||||
if (cacheVersion != CURRENT_VERSION) {
|
||||
|
@ -35,20 +36,9 @@ KTXCache::KTXCache(const std::string& dir, const std::string& ext) :
|
|||
}
|
||||
}
|
||||
|
||||
KTXFilePointer KTXCache::writeFile(const char* data, Metadata&& metadata) {
|
||||
FilePointer file = FileCache::writeFile(data, std::move(metadata), true);
|
||||
return std::static_pointer_cast<KTXFile>(file);
|
||||
}
|
||||
|
||||
KTXFilePointer KTXCache::getFile(const Key& key) {
|
||||
return std::static_pointer_cast<KTXFile>(FileCache::getFile(key));
|
||||
}
|
||||
|
||||
std::unique_ptr<File> KTXCache::createFile(Metadata&& metadata, const std::string& filepath) {
|
||||
qCInfo(file_cache) << "Wrote KTX" << metadata.key.c_str();
|
||||
return std::unique_ptr<File>(new KTXFile(std::move(metadata), filepath));
|
||||
return FileCache::createFile(std::move(metadata), filepath);
|
||||
}
|
||||
|
||||
KTXFile::KTXFile(Metadata&& metadata, const std::string& filepath) :
|
||||
cache::File(std::move(metadata), filepath) {}
|
||||
|
||||
|
|
|
@ -35,20 +35,10 @@ public:
|
|||
|
||||
KTXCache(const std::string& dir, const std::string& ext);
|
||||
|
||||
KTXFilePointer writeFile(const char* data, Metadata&& metadata);
|
||||
KTXFilePointer getFile(const Key& key);
|
||||
void initialize() override;
|
||||
|
||||
protected:
|
||||
std::unique_ptr<cache::File> createFile(Metadata&& metadata, const std::string& filepath) override final;
|
||||
};
|
||||
|
||||
class KTXFile : public cache::File {
|
||||
Q_OBJECT
|
||||
|
||||
protected:
|
||||
friend class KTXCache;
|
||||
|
||||
KTXFile(Metadata&& metadata, const std::string& filepath);
|
||||
};
|
||||
|
||||
#endif // hifi_KTXCache_h
|
||||
|
|
|
@ -58,8 +58,8 @@ static const QUrl SPECTATOR_CAMERA_FRAME_URL("resource://spectatorCameraFrame");
|
|||
static const float SKYBOX_LOAD_PRIORITY { 10.0f }; // Make sure skybox loads first
|
||||
static const float HIGH_MIPS_LOAD_PRIORITY { 9.0f }; // Make sure high mips loads after skybox but before models
|
||||
|
||||
TextureCache::TextureCache() :
|
||||
_ktxCache(KTX_DIRNAME, KTX_EXT) {
|
||||
TextureCache::TextureCache() {
|
||||
_ktxCache->initialize();
|
||||
setUnusedResourceCacheSize(0);
|
||||
setObjectName("TextureCache");
|
||||
}
|
||||
|
@ -718,7 +718,7 @@ void NetworkTexture::handleFinishedInitialLoad() {
|
|||
gpu::TexturePointer texture = textureCache->getTextureByHash(hash);
|
||||
|
||||
if (!texture) {
|
||||
KTXFilePointer ktxFile = textureCache->_ktxCache.getFile(hash);
|
||||
auto ktxFile = textureCache->_ktxCache->getFile(hash);
|
||||
if (ktxFile) {
|
||||
texture = gpu::Texture::unserialize(ktxFile);
|
||||
if (texture) {
|
||||
|
@ -742,9 +742,9 @@ void NetworkTexture::handleFinishedInitialLoad() {
|
|||
// Move ktx to file
|
||||
const char* data = reinterpret_cast<const char*>(memKtx->_storage->data());
|
||||
size_t length = memKtx->_storage->size();
|
||||
KTXFilePointer file;
|
||||
cache::FilePointer file;
|
||||
auto& ktxCache = textureCache->_ktxCache;
|
||||
if (!memKtx || !(file = ktxCache.writeFile(data, KTXCache::Metadata(filename, length)))) {
|
||||
if (!memKtx || !(file = ktxCache->writeFile(data, KTXCache::Metadata(filename, length)))) {
|
||||
qCWarning(modelnetworking) << url << " failed to write cache file";
|
||||
QMetaObject::invokeMethod(resource.data(), "setImage",
|
||||
Q_ARG(gpu::TexturePointer, nullptr),
|
||||
|
@ -900,7 +900,7 @@ void ImageReader::read() {
|
|||
|
||||
// If there is no live texture, check if there's an existing KTX file
|
||||
if (!texture) {
|
||||
KTXFilePointer ktxFile = textureCache->_ktxCache.getFile(hash);
|
||||
auto ktxFile = textureCache->_ktxCache->getFile(hash);
|
||||
if (ktxFile) {
|
||||
texture = gpu::Texture::unserialize(ktxFile);
|
||||
if (texture) {
|
||||
|
@ -950,7 +950,7 @@ void ImageReader::read() {
|
|||
const char* data = reinterpret_cast<const char*>(memKtx->_storage->data());
|
||||
size_t length = memKtx->_storage->size();
|
||||
auto& ktxCache = textureCache->_ktxCache;
|
||||
auto file = ktxCache.writeFile(data, KTXCache::Metadata(hash, length));
|
||||
auto file = ktxCache->writeFile(data, KTXCache::Metadata(hash, length));
|
||||
if (!file) {
|
||||
qCWarning(modelnetworking) << _url << "file cache failed";
|
||||
} else {
|
||||
|
|
|
@ -189,7 +189,7 @@ private:
|
|||
static const std::string KTX_DIRNAME;
|
||||
static const std::string KTX_EXT;
|
||||
|
||||
KTXCache _ktxCache;
|
||||
std::shared_ptr<cache::FileCache> _ktxCache { std::make_shared<KTXCache>(KTX_DIRNAME, KTX_EXT) };
|
||||
// Map from image hashes to texture weak pointers
|
||||
std::unordered_map<std::string, std::weak_ptr<gpu::Texture>> _texturesByHashes;
|
||||
std::mutex _texturesByHashesMutex;
|
||||
|
|
|
@ -353,13 +353,20 @@ void AssetClient::handleAssetGetReply(QSharedPointer<ReceivedMessage> message, S
|
|||
connect(message.data(), &ReceivedMessage::progress, this, [this, weakNode, messageID, length](qint64 size) {
|
||||
handleProgressCallback(weakNode, messageID, size, length);
|
||||
});
|
||||
connect(message.data(), &ReceivedMessage::completed, this, [this, weakNode, messageID]() {
|
||||
handleCompleteCallback(weakNode, messageID);
|
||||
connect(message.data(), &ReceivedMessage::completed, this, [this, weakNode, messageID, length]() {
|
||||
handleCompleteCallback(weakNode, messageID, length);
|
||||
});
|
||||
|
||||
if (message->isComplete()) {
|
||||
disconnect(message.data(), nullptr, this, nullptr);
|
||||
callbacks.completeCallback(true, error, message->readAll());
|
||||
|
||||
if (length != message->getBytesLeftToRead()) {
|
||||
callbacks.completeCallback(false, error, QByteArray());
|
||||
} else {
|
||||
callbacks.completeCallback(true, error, message->readAll());
|
||||
}
|
||||
|
||||
|
||||
messageCallbackMap.erase(requestIt);
|
||||
}
|
||||
}
|
||||
|
@ -391,7 +398,7 @@ void AssetClient::handleProgressCallback(const QWeakPointer<Node>& node, Message
|
|||
callbacks.progressCallback(size, length);
|
||||
}
|
||||
|
||||
void AssetClient::handleCompleteCallback(const QWeakPointer<Node>& node, MessageID messageID) {
|
||||
void AssetClient::handleCompleteCallback(const QWeakPointer<Node>& node, MessageID messageID, DataOffset length) {
|
||||
auto senderNode = node.toStrongRef();
|
||||
|
||||
if (!senderNode) {
|
||||
|
@ -424,8 +431,7 @@ void AssetClient::handleCompleteCallback(const QWeakPointer<Node>& node, Message
|
|||
return;
|
||||
}
|
||||
|
||||
|
||||
if (message->failed()) {
|
||||
if (message->failed() || length != message->getBytesLeftToRead()) {
|
||||
callbacks.completeCallback(false, AssetServerError::NoError, QByteArray());
|
||||
} else {
|
||||
callbacks.completeCallback(true, AssetServerError::NoError, message->readAll());
|
||||
|
|
|
@ -93,7 +93,7 @@ private:
|
|||
bool cancelUploadAssetRequest(MessageID id);
|
||||
|
||||
void handleProgressCallback(const QWeakPointer<Node>& node, MessageID messageID, qint64 size, DataOffset length);
|
||||
void handleCompleteCallback(const QWeakPointer<Node>& node, MessageID messageID);
|
||||
void handleCompleteCallback(const QWeakPointer<Node>& node, MessageID messageID, DataOffset length);
|
||||
|
||||
void forceFailureOfPendingRequests(SharedNodePointer node);
|
||||
|
||||
|
|
|
@ -104,12 +104,7 @@ void AssetRequest::start() {
|
|||
break;
|
||||
}
|
||||
} else {
|
||||
if (_byteRange.isSet()) {
|
||||
// we had a byte range, the size of the data does not match what we expect, so we return an error
|
||||
if (data.size() != _byteRange.size()) {
|
||||
_error = SizeVerificationFailed;
|
||||
}
|
||||
} else if (hashData(data).toHex() != _hash) {
|
||||
if (!_byteRange.isSet() && hashData(data).toHex() != _hash) {
|
||||
// the hash of the received data does not match what we expect, so we return an error
|
||||
_error = HashVerificationFailed;
|
||||
}
|
||||
|
|
|
@ -446,7 +446,8 @@ qint64 LimitedNodeList::sendPacketList(std::unique_ptr<NLPacketList> packetList,
|
|||
|
||||
return _nodeSocket.writePacketList(std::move(packetList), *activeSocket);
|
||||
} else {
|
||||
qCDebug(networking) << "LimitedNodeList::sendPacketList called without active socket for node. Not sending.";
|
||||
qCDebug(networking) << "LimitedNodeList::sendPacketList called without active socket for node "
|
||||
<< destinationNode.getUUID() << ". Not sending.";
|
||||
return ERROR_SENDING_PACKET_BYTES;
|
||||
}
|
||||
}
|
||||
|
@ -454,7 +455,8 @@ qint64 LimitedNodeList::sendPacketList(std::unique_ptr<NLPacketList> packetList,
|
|||
qint64 LimitedNodeList::sendPacket(std::unique_ptr<NLPacket> packet, const Node& destinationNode,
|
||||
const HifiSockAddr& overridenSockAddr) {
|
||||
if (overridenSockAddr.isNull() && !destinationNode.getActiveSocket()) {
|
||||
qCDebug(networking) << "LimitedNodeList::sendPacket called without active socket for node. Not sending.";
|
||||
qCDebug(networking) << "LimitedNodeList::sendPacket called without active socket for node"
|
||||
<< destinationNode.getUUID() << ". Not sending.";
|
||||
return ERROR_SENDING_PACKET_BYTES;
|
||||
}
|
||||
|
||||
|
@ -568,8 +570,8 @@ void LimitedNodeList::handleNodeKill(const SharedNodePointer& node) {
|
|||
|
||||
SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t nodeType,
|
||||
const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket,
|
||||
const NodePermissions& permissions,
|
||||
const QUuid& connectionSecret) {
|
||||
bool isReplicated, bool isUpstream,
|
||||
const QUuid& connectionSecret, const NodePermissions& permissions) {
|
||||
QReadLocker readLocker(&_nodeMutex);
|
||||
NodeHash::const_iterator it = _nodeHash.find(uuid);
|
||||
|
||||
|
@ -580,11 +582,20 @@ SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t
|
|||
matchingNode->setLocalSocket(localSocket);
|
||||
matchingNode->setPermissions(permissions);
|
||||
matchingNode->setConnectionSecret(connectionSecret);
|
||||
matchingNode->setIsReplicated(isReplicated);
|
||||
matchingNode->setIsUpstream(isUpstream);
|
||||
|
||||
return matchingNode;
|
||||
} else {
|
||||
// we didn't have this node, so add them
|
||||
Node* newNode = new Node(uuid, nodeType, publicSocket, localSocket, permissions, connectionSecret);
|
||||
Node* newNode = new Node(uuid, nodeType, publicSocket, localSocket);
|
||||
newNode->setIsReplicated(isReplicated);
|
||||
newNode->setIsUpstream(isUpstream);
|
||||
newNode->setConnectionSecret(connectionSecret);
|
||||
newNode->setPermissions(permissions);
|
||||
|
||||
// move the newly constructed node to the LNL thread
|
||||
newNode->moveToThread(thread());
|
||||
|
||||
if (nodeType == NodeType::AudioMixer) {
|
||||
LimitedNodeList::flagTimeForConnectionStep(LimitedNodeList::AddedAudioMixer);
|
||||
|
@ -742,7 +753,8 @@ void LimitedNodeList::removeSilentNodes() {
|
|||
SharedNodePointer node = it->second;
|
||||
node->getMutex().lock();
|
||||
|
||||
if ((usecTimestampNow() - node->getLastHeardMicrostamp()) > (NODE_SILENCE_THRESHOLD_MSECS * USECS_PER_MSEC)) {
|
||||
if (!node->isForcedNeverSilent()
|
||||
&& (usecTimestampNow() - node->getLastHeardMicrostamp()) > (NODE_SILENCE_THRESHOLD_MSECS * USECS_PER_MSEC)) {
|
||||
// call the NodeHash erase to get rid of this node
|
||||
it = _nodeHash.unsafe_erase(it);
|
||||
|
||||
|
|
|
@ -145,8 +145,9 @@ public:
|
|||
|
||||
SharedNodePointer addOrUpdateNode(const QUuid& uuid, NodeType_t nodeType,
|
||||
const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket,
|
||||
const NodePermissions& permissions = DEFAULT_AGENT_PERMISSIONS,
|
||||
const QUuid& connectionSecret = QUuid());
|
||||
bool isReplicated = false, bool isUpstream = false,
|
||||
const QUuid& connectionSecret = QUuid(),
|
||||
const NodePermissions& permissions = DEFAULT_AGENT_PERMISSIONS);
|
||||
|
||||
static bool parseSTUNResponse(udt::BasePacket* packet, QHostAddress& newPublicAddress, uint16_t& newPublicPort);
|
||||
bool hasCompletedInitialSTUN() const { return _hasCompletedInitialSTUN; }
|
||||
|
@ -257,6 +258,16 @@ public:
|
|||
return SharedNodePointer();
|
||||
}
|
||||
|
||||
// This is unsafe because it does not take a lock
|
||||
// Must only be called when you know that a read lock on the node mutex is held
|
||||
// and will be held for the duration of your iteration
|
||||
template<typename NodeLambda>
|
||||
void unsafeEachNode(NodeLambda functor) {
|
||||
for (NodeHash::const_iterator it = _nodeHash.cbegin(); it != _nodeHash.cend(); ++it) {
|
||||
functor(it->second);
|
||||
}
|
||||
}
|
||||
|
||||
void putLocalPortIntoSharedMemory(const QString key, QObject* parent, quint16 localPort);
|
||||
bool getLocalServerPortFromSharedMemory(const QString key, quint16& localPort);
|
||||
|
||||
|
@ -386,6 +397,7 @@ protected:
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private slots:
|
||||
void flagTimeForConnectionStep(ConnectionStep connectionStep, quint64 timestamp);
|
||||
void possiblyTimeoutSTUNAddressLookup();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
//
|
||||
// LocationScriptingInterface.cpp
|
||||
// interface/src/scripting
|
||||
// libraries/networking/src
|
||||
//
|
||||
// Created by Ryan Huffman on 4/29/14.
|
||||
// Copyright 2014 High Fidelity, Inc.
|
||||
|
@ -9,7 +9,7 @@
|
|||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
#include <AddressManager.h>
|
||||
#include "AddressManager.h"
|
||||
|
||||
#include "LocationScriptingInterface.h"
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
//
|
||||
// LocationScriptingInterface.h
|
||||
// interface/src/scripting
|
||||
// libraries/networking/src
|
||||
//
|
||||
// Created by Ryan Huffman on 4/29/14.
|
||||
// Copyright 2014 High Fidelity, Inc.
|
|
@ -42,6 +42,7 @@ public:
|
|||
|
||||
static std::unique_ptr<NLPacket> fromReceivedPacket(std::unique_ptr<char[]> data, qint64 size,
|
||||
const HifiSockAddr& senderSockAddr);
|
||||
|
||||
static std::unique_ptr<NLPacket> fromBase(std::unique_ptr<Packet> packet);
|
||||
|
||||
// Provided for convenience, try to limit use
|
||||
|
|
|
@ -23,6 +23,8 @@ public:
|
|||
|
||||
PacketVersion getVersion() const { return _packetVersion; }
|
||||
const QUuid& getSourceID() const { return _sourceID; }
|
||||
|
||||
qint64 getMaxSegmentSize() const override { return NLPacket::maxPayloadSize(_packetType, _isOrdered); }
|
||||
|
||||
private:
|
||||
NLPacketList(PacketType packetType, QByteArray extendedHeader = QByteArray(), bool isReliable = false,
|
||||
|
|
|
@ -76,6 +76,14 @@ public:
|
|||
float getOutboundBandwidth() const; // in kbps
|
||||
float getInboundBandwidth() const; // in kbps
|
||||
|
||||
// Typically the LimitedNodeList removes nodes after they are "silent"
|
||||
// meaning that we have not received any packets (including simple keepalive pings) from them for a set interval.
|
||||
// The _isForcedNeverSilent flag tells the LimitedNodeList that a Node should never be killed by removeSilentNodes()
|
||||
// even if its the timestamp of when it was last heard from has never been updated.
|
||||
|
||||
bool isForcedNeverSilent() const { return _isForcedNeverSilent; }
|
||||
void setIsForcedNeverSilent(bool isForcedNeverSilent) { _isForcedNeverSilent = isForcedNeverSilent; }
|
||||
|
||||
friend QDataStream& operator<<(QDataStream& out, const NetworkPeer& peer);
|
||||
friend QDataStream& operator>>(QDataStream& in, NetworkPeer& peer);
|
||||
public slots:
|
||||
|
@ -103,6 +111,8 @@ protected:
|
|||
QTimer* _pingTimer = NULL;
|
||||
|
||||
int _connectionAttempts;
|
||||
|
||||
bool _isForcedNeverSilent { false };
|
||||
};
|
||||
|
||||
QDebug operator<<(QDebug debug, const NetworkPeer &peer);
|
||||
|
|
|
@ -42,6 +42,8 @@ void NodeType::init() {
|
|||
TypeNameHash.insert(NodeType::MessagesMixer, "Messages Mixer");
|
||||
TypeNameHash.insert(NodeType::AssetServer, "Asset Server");
|
||||
TypeNameHash.insert(NodeType::EntityScriptServer, "Entity Script Server");
|
||||
TypeNameHash.insert(NodeType::DownstreamAudioMixer, "Downstream Audio Mixer");
|
||||
TypeNameHash.insert(NodeType::DownstreamAvatarMixer, "Downstream Avatar Mixer");
|
||||
TypeNameHash.insert(NodeType::Unassigned, "Unassigned");
|
||||
}
|
||||
|
||||
|
@ -50,17 +52,34 @@ const QString& NodeType::getNodeTypeName(NodeType_t nodeType) {
|
|||
return matchedTypeName != TypeNameHash.end() ? matchedTypeName.value() : UNKNOWN_NodeType_t_NAME;
|
||||
}
|
||||
|
||||
bool NodeType::isDownstream(NodeType_t nodeType) {
|
||||
return nodeType == NodeType::DownstreamAudioMixer || nodeType == NodeType::DownstreamAvatarMixer;
|
||||
}
|
||||
|
||||
NodeType_t NodeType::downstreamType(NodeType_t primaryType) {
|
||||
switch (primaryType) {
|
||||
case AudioMixer:
|
||||
return DownstreamAudioMixer;
|
||||
case AvatarMixer:
|
||||
return DownstreamAvatarMixer;
|
||||
default:
|
||||
return Unassigned;
|
||||
}
|
||||
}
|
||||
|
||||
NodeType_t NodeType::fromString(QString type) {
|
||||
return TypeNameHash.key(type, NodeType::Unassigned);
|
||||
}
|
||||
|
||||
|
||||
Node::Node(const QUuid& uuid, NodeType_t type, const HifiSockAddr& publicSocket,
|
||||
const HifiSockAddr& localSocket, const NodePermissions& permissions, const QUuid& connectionSecret,
|
||||
QObject* parent) :
|
||||
const HifiSockAddr& localSocket, QObject* parent) :
|
||||
NetworkPeer(uuid, publicSocket, localSocket, parent),
|
||||
_type(type),
|
||||
_connectionSecret(connectionSecret),
|
||||
_pingMs(-1), // "Uninitialized"
|
||||
_clockSkewUsec(0),
|
||||
_mutex(),
|
||||
_clockSkewMovingPercentile(30, 0.8f), // moving 80th percentile of 30 samples
|
||||
_permissions(permissions)
|
||||
_clockSkewMovingPercentile(30, 0.8f) // moving 80th percentile of 30 samples
|
||||
{
|
||||
// Update socket's object name
|
||||
setType(_type);
|
||||
|
@ -135,6 +154,7 @@ QDataStream& operator<<(QDataStream& out, const Node& node) {
|
|||
out << node._publicSocket;
|
||||
out << node._localSocket;
|
||||
out << node._permissions;
|
||||
out << node._isReplicated;
|
||||
return out;
|
||||
}
|
||||
|
||||
|
@ -144,6 +164,7 @@ QDataStream& operator>>(QDataStream& in, Node& node) {
|
|||
in >> node._publicSocket;
|
||||
in >> node._localSocket;
|
||||
in >> node._permissions;
|
||||
in >> node._isReplicated;
|
||||
return in;
|
||||
}
|
||||
|
||||
|
|
|
@ -37,9 +37,9 @@
|
|||
class Node : public NetworkPeer {
|
||||
Q_OBJECT
|
||||
public:
|
||||
|
||||
Node(const QUuid& uuid, NodeType_t type,
|
||||
const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket,
|
||||
const NodePermissions& permissions, const QUuid& connectionSecret = QUuid(),
|
||||
QObject* parent = nullptr);
|
||||
|
||||
bool operator==(const Node& otherNode) const { return _uuid == otherNode._uuid; }
|
||||
|
@ -48,6 +48,12 @@ public:
|
|||
char getType() const { return _type; }
|
||||
void setType(char type);
|
||||
|
||||
bool isReplicated() const { return _isReplicated; }
|
||||
void setIsReplicated(bool isReplicated) { _isReplicated = isReplicated; }
|
||||
|
||||
bool isUpstream() const { return _isUpstream; }
|
||||
void setIsUpstream(bool isUpstream) { _isUpstream = isUpstream; }
|
||||
|
||||
const QUuid& getConnectionSecret() const { return _connectionSecret; }
|
||||
void setConnectionSecret(const QUuid& connectionSecret) { _connectionSecret = connectionSecret; }
|
||||
|
||||
|
@ -89,13 +95,16 @@ private:
|
|||
|
||||
QUuid _connectionSecret;
|
||||
std::unique_ptr<NodeData> _linkedData;
|
||||
bool _isReplicated { false };
|
||||
int _pingMs;
|
||||
qint64 _clockSkewUsec;
|
||||
QMutex _mutex;
|
||||
MovingPercentile _clockSkewMovingPercentile;
|
||||
NodePermissions _permissions;
|
||||
bool _isUpstream { false };
|
||||
tbb::concurrent_unordered_set<QUuid, UUIDHasher> _ignoredNodeIDSet;
|
||||
mutable QReadWriteLock _ignoredNodeIDSetLock;
|
||||
std::vector<QString> _replicatedUsernames { };
|
||||
|
||||
std::atomic_bool _ignoreRadiusEnabled;
|
||||
};
|
||||
|
|
|
@ -654,8 +654,9 @@ void NodeList::parseNodeFromPacketStream(QDataStream& packetStream) {
|
|||
QUuid nodeUUID, connectionUUID;
|
||||
HifiSockAddr nodePublicSocket, nodeLocalSocket;
|
||||
NodePermissions permissions;
|
||||
bool isReplicated;
|
||||
|
||||
packetStream >> nodeType >> nodeUUID >> nodePublicSocket >> nodeLocalSocket >> permissions;
|
||||
packetStream >> nodeType >> nodeUUID >> nodePublicSocket >> nodeLocalSocket >> permissions >> isReplicated;
|
||||
|
||||
// if the public socket address is 0 then it's reachable at the same IP
|
||||
// as the domain server
|
||||
|
@ -666,7 +667,12 @@ void NodeList::parseNodeFromPacketStream(QDataStream& packetStream) {
|
|||
packetStream >> connectionUUID;
|
||||
|
||||
SharedNodePointer node = addOrUpdateNode(nodeUUID, nodeType, nodePublicSocket,
|
||||
nodeLocalSocket, permissions, connectionUUID);
|
||||
nodeLocalSocket, isReplicated, false, connectionUUID, permissions);
|
||||
|
||||
// nodes that are downstream of our own type are kept alive when we hear about them from the domain server
|
||||
if (node->getType() == NodeType::downstreamType(_ownerType)) {
|
||||
node->setLastHeardMicrostamp(usecTimestampNow());
|
||||
}
|
||||
}
|
||||
|
||||
void NodeList::sendAssignment(Assignment& assignment) {
|
||||
|
@ -711,14 +717,20 @@ void NodeList::pingPunchForInactiveNode(const SharedNodePointer& node) {
|
|||
}
|
||||
|
||||
void NodeList::startNodeHolePunch(const SharedNodePointer& node) {
|
||||
// connect to the correct signal on this node so we know when to ping it
|
||||
connect(node.data(), &Node::pingTimerTimeout, this, &NodeList::handleNodePingTimeout);
|
||||
|
||||
// start the ping timer for this node
|
||||
node->startPingTimer();
|
||||
// we don't hole punch to downstream servers, since it is assumed that we have a direct line to them
|
||||
// we also don't hole punch to relayed upstream nodes, since we do not communicate directly with them
|
||||
|
||||
// ping this node immediately
|
||||
pingPunchForInactiveNode(node);
|
||||
if (!NodeType::isDownstream(node->getType()) && !node->isUpstream()) {
|
||||
// connect to the correct signal on this node so we know when to ping it
|
||||
connect(node.data(), &Node::pingTimerTimeout, this, &NodeList::handleNodePingTimeout);
|
||||
|
||||
// start the ping timer for this node
|
||||
node->startPingTimer();
|
||||
|
||||
// ping this node immediately
|
||||
pingPunchForInactiveNode(node);
|
||||
}
|
||||
}
|
||||
|
||||
void NodeList::handleNodePingTimeout() {
|
||||
|
@ -761,8 +773,11 @@ void NodeList::stopKeepalivePingTimer() {
|
|||
}
|
||||
|
||||
void NodeList::sendKeepAlivePings() {
|
||||
// send keep-alive ping packets to nodes of types we care about that are not relayed to us from an upstream node
|
||||
|
||||
eachMatchingNode([this](const SharedNodePointer& node)->bool {
|
||||
return _nodeTypesOfInterest.contains(node->getType());
|
||||
auto type = node->getType();
|
||||
return !node->isUpstream() && _nodeTypesOfInterest.contains(type) && !NodeType::isDownstream(type);
|
||||
}, [&](const SharedNodePointer& node) {
|
||||
sendPacket(constructPingPacket(), *node);
|
||||
});
|
||||
|
@ -1120,4 +1135,4 @@ void NodeList::setRequestsDomainListData(bool isRequesting) {
|
|||
|
||||
void NodeList::startThread() {
|
||||
moveToNewNamedThread(this, "NodeList Thread", QThread::TimeCriticalPriority);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,10 +25,17 @@ namespace NodeType {
|
|||
const NodeType_t AssetServer = 'A';
|
||||
const NodeType_t MessagesMixer = 'm';
|
||||
const NodeType_t EntityScriptServer = 'S';
|
||||
const NodeType_t DownstreamAudioMixer = 'a';
|
||||
const NodeType_t DownstreamAvatarMixer = 'w';
|
||||
const NodeType_t Unassigned = 1;
|
||||
|
||||
void init();
|
||||
|
||||
const QString& getNodeTypeName(NodeType_t nodeType);
|
||||
bool isDownstream(NodeType_t nodeType);
|
||||
NodeType_t downstreamType(NodeType_t primaryType);
|
||||
|
||||
NodeType_t fromString(QString type);
|
||||
}
|
||||
|
||||
typedef QSet<NodeType_t> NodeSet;
|
||||
|
|
|
@ -42,6 +42,20 @@ ReceivedMessage::ReceivedMessage(NLPacket& packet)
|
|||
{
|
||||
}
|
||||
|
||||
ReceivedMessage::ReceivedMessage(QByteArray byteArray, PacketType packetType, PacketVersion packetVersion,
|
||||
const HifiSockAddr& senderSockAddr, QUuid sourceID) :
|
||||
_data(byteArray),
|
||||
_headData(_data.mid(0, HEAD_DATA_SIZE)),
|
||||
_numPackets(1),
|
||||
_sourceID(sourceID),
|
||||
_packetType(packetType),
|
||||
_packetVersion(packetVersion),
|
||||
_senderSockAddr(senderSockAddr),
|
||||
_isComplete(true)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void ReceivedMessage::setFailed() {
|
||||
_failed = true;
|
||||
_isComplete = true;
|
||||
|
|
|
@ -24,6 +24,8 @@ class ReceivedMessage : public QObject {
|
|||
public:
|
||||
ReceivedMessage(const NLPacketList& packetList);
|
||||
ReceivedMessage(NLPacket& packet);
|
||||
ReceivedMessage(QByteArray byteArray, PacketType packetType, PacketVersion packetVersion,
|
||||
const HifiSockAddr& senderSockAddr, QUuid sourceID = QUuid());
|
||||
|
||||
QByteArray getMessage() const { return _data; }
|
||||
const char* getRawMessage() const { return _data.constData(); }
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
//
|
||||
|
||||
#include <QtCore/QCoreApplication>
|
||||
#include <QtCore/QJsonArray>
|
||||
#include <QtCore/QJsonObject>
|
||||
#include <QtCore/QThread>
|
||||
#include <QtCore/QTimer>
|
||||
|
|
|
@ -18,6 +18,8 @@
|
|||
|
||||
#include "Assignment.h"
|
||||
|
||||
using DownstreamNodeFoundCallback = std::function<void(Node& downstreamNode)>;
|
||||
|
||||
class ThreadedAssignment : public Assignment {
|
||||
Q_OBJECT
|
||||
public:
|
||||
|
@ -40,6 +42,7 @@ signals:
|
|||
|
||||
protected:
|
||||
void commonInit(const QString& targetName, NodeType_t nodeType);
|
||||
|
||||
bool _isFinished;
|
||||
QTimer _domainServerTimer;
|
||||
QTimer _statsTimer;
|
||||
|
|
|
@ -39,7 +39,19 @@ const QSet<PacketType> NON_SOURCED_PACKETS = QSet<PacketType>()
|
|||
<< PacketType::ICEServerPeerInformation << PacketType::ICEServerQuery << PacketType::ICEServerHeartbeat
|
||||
<< PacketType::ICEServerHeartbeatACK << PacketType::ICEPing << PacketType::ICEPingReply
|
||||
<< PacketType::ICEServerHeartbeatDenied << PacketType::AssignmentClientStatus << PacketType::StopNode
|
||||
<< PacketType::DomainServerRemovedNode << PacketType::UsernameFromIDReply << PacketType::OctreeFileReplacement;
|
||||
<< PacketType::DomainServerRemovedNode << PacketType::UsernameFromIDReply << PacketType::OctreeFileReplacement
|
||||
<< PacketType::ReplicatedMicrophoneAudioNoEcho << PacketType::ReplicatedMicrophoneAudioWithEcho
|
||||
<< PacketType::ReplicatedInjectAudio << PacketType::ReplicatedSilentAudioFrame
|
||||
<< PacketType::ReplicatedAvatarIdentity << PacketType::ReplicatedKillAvatar << PacketType::ReplicatedBulkAvatarData;
|
||||
|
||||
const QHash<PacketType, PacketType> REPLICATED_PACKET_MAPPING {
|
||||
{ PacketType::MicrophoneAudioNoEcho, PacketType::ReplicatedMicrophoneAudioNoEcho },
|
||||
{ PacketType::MicrophoneAudioWithEcho, PacketType::ReplicatedMicrophoneAudioWithEcho },
|
||||
{ PacketType::InjectAudio, PacketType::ReplicatedInjectAudio },
|
||||
{ PacketType::SilentAudioFrame, PacketType::ReplicatedSilentAudioFrame },
|
||||
{ PacketType::AvatarIdentity, PacketType::ReplicatedAvatarIdentity },
|
||||
{ PacketType::KillAvatar, PacketType::ReplicatedKillAvatar },
|
||||
};
|
||||
|
||||
PacketVersion versionForPacketType(PacketType packetType) {
|
||||
switch (packetType) {
|
||||
|
@ -56,7 +68,7 @@ PacketVersion versionForPacketType(PacketType packetType) {
|
|||
case PacketType::AvatarData:
|
||||
case PacketType::BulkAvatarData:
|
||||
case PacketType::KillAvatar:
|
||||
return static_cast<PacketVersion>(AvatarMixerPacketVersion::MannequinDefaultAvatar);
|
||||
return static_cast<PacketVersion>(AvatarMixerPacketVersion::AvatarIdentitySequenceFront);
|
||||
case PacketType::MessagesData:
|
||||
return static_cast<PacketVersion>(MessageDataVersion::TextOrBinaryData);
|
||||
case PacketType::ICEServerHeartbeat:
|
||||
|
@ -119,7 +131,7 @@ static void ensureProtocolVersionsSignature() {
|
|||
std::call_once(once, [&] {
|
||||
QByteArray buffer;
|
||||
QDataStream stream(&buffer, QIODevice::WriteOnly);
|
||||
uint8_t numberOfProtocols = static_cast<uint8_t>(PacketType::LAST_PACKET_TYPE) + 1;
|
||||
uint8_t numberOfProtocols = static_cast<uint8_t>(PacketType::NUM_PACKET_TYPE);
|
||||
stream << numberOfProtocols;
|
||||
for (uint8_t packetType = 0; packetType < numberOfProtocols; packetType++) {
|
||||
uint8_t packetTypeVersion = static_cast<uint8_t>(versionForPacketType(static_cast<PacketType>(packetType)));
|
||||
|
|
|
@ -115,12 +115,21 @@ public:
|
|||
AdjustAvatarSorting,
|
||||
OctreeFileReplacement,
|
||||
CollisionEventChanges,
|
||||
LAST_PACKET_TYPE = CollisionEventChanges
|
||||
ReplicatedMicrophoneAudioNoEcho,
|
||||
ReplicatedMicrophoneAudioWithEcho,
|
||||
ReplicatedInjectAudio,
|
||||
ReplicatedSilentAudioFrame,
|
||||
ReplicatedAvatarIdentity,
|
||||
ReplicatedKillAvatar,
|
||||
ReplicatedBulkAvatarData,
|
||||
NUM_PACKET_TYPE
|
||||
};
|
||||
};
|
||||
|
||||
using PacketType = PacketTypeEnum::Value;
|
||||
|
||||
extern const QHash<PacketType, PacketType> REPLICATED_PACKET_MAPPING;
|
||||
|
||||
const int NUM_BYTES_MD5_HASH = 16;
|
||||
|
||||
typedef char PacketVersion;
|
||||
|
@ -237,7 +246,8 @@ enum class AvatarMixerPacketVersion : PacketVersion {
|
|||
StickAndBallDefaultAvatar,
|
||||
IdentityPacketsIncludeUpdateTime,
|
||||
AvatarIdentitySequenceId,
|
||||
MannequinDefaultAvatar
|
||||
MannequinDefaultAvatar,
|
||||
AvatarIdentitySequenceFront
|
||||
};
|
||||
|
||||
enum class DomainConnectRequestVersion : PacketVersion {
|
||||
|
|
|
@ -36,8 +36,8 @@ std::unique_ptr<PacketList> PacketList::fromReceivedPackets(std::list<std::uniqu
|
|||
|
||||
PacketList::PacketList(PacketType packetType, QByteArray extendedHeader, bool isReliable, bool isOrdered) :
|
||||
_packetType(packetType),
|
||||
_isReliable(isReliable),
|
||||
_isOrdered(isOrdered),
|
||||
_isReliable(isReliable),
|
||||
_extendedHeader(extendedHeader)
|
||||
{
|
||||
Q_ASSERT_X(!(!_isReliable && _isOrdered), "PacketList", "Unreliable ordered PacketLists are not currently supported");
|
||||
|
@ -46,8 +46,8 @@ PacketList::PacketList(PacketType packetType, QByteArray extendedHeader, bool is
|
|||
PacketList::PacketList(PacketList&& other) :
|
||||
_packetType(other._packetType),
|
||||
_packets(std::move(other._packets)),
|
||||
_isReliable(other._isReliable),
|
||||
_isOrdered(other._isOrdered),
|
||||
_isReliable(other._isReliable),
|
||||
_extendedHeader(std::move(other._extendedHeader))
|
||||
{
|
||||
}
|
||||
|
|
|
@ -49,6 +49,8 @@ public:
|
|||
void startSegment();
|
||||
void endSegment();
|
||||
|
||||
virtual qint64 getMaxSegmentSize() const { return Packet::maxPayloadSize(_isOrdered); }
|
||||
|
||||
HifiSockAddr getSenderSockAddr() const;
|
||||
|
||||
void closeCurrentPacket(bool shouldSendEmpty = false);
|
||||
|
@ -74,6 +76,8 @@ protected:
|
|||
|
||||
PacketType _packetType;
|
||||
std::list<std::unique_ptr<Packet>> _packets;
|
||||
|
||||
bool _isOrdered = false;
|
||||
|
||||
private:
|
||||
friend class ::LimitedNodeList;
|
||||
|
@ -93,7 +97,6 @@ private:
|
|||
|
||||
Packet::MessageNumber _messageNumber;
|
||||
bool _isReliable = false;
|
||||
bool _isOrdered = false;
|
||||
|
||||
std::unique_ptr<Packet> _currentPacket;
|
||||
|
||||
|
|
|
@ -35,8 +35,8 @@ public:
|
|||
explicit SequenceNumber(char* value) { _value = (*reinterpret_cast<int32_t*>(value)) & MAX; }
|
||||
explicit SequenceNumber(Type value) { _value = (value <= MAX) ? ((value >= 0) ? value : 0) : MAX; }
|
||||
explicit SequenceNumber(UType value) { _value = (value <= MAX) ? value : MAX; }
|
||||
explicit operator Type() { return _value; }
|
||||
explicit operator UType() { return static_cast<UType>(_value); }
|
||||
explicit operator Type() const { return _value; }
|
||||
explicit operator UType() const { return static_cast<UType>(_value); }
|
||||
|
||||
inline SequenceNumber& operator++() {
|
||||
_value = (_value + 1) % (MAX + 1);
|
||||
|
|
|
@ -23,14 +23,16 @@ const uint16_t ObjectActionTractor::tractorVersion = 1;
|
|||
|
||||
ObjectActionTractor::ObjectActionTractor(const QUuid& id, EntityItemPointer ownerEntity) :
|
||||
ObjectAction(DYNAMIC_TYPE_TRACTOR, id, ownerEntity),
|
||||
_positionalTarget(glm::vec3(0.0f)),
|
||||
_desiredPositionalTarget(glm::vec3(0.0f)),
|
||||
_positionalTarget(0.0f),
|
||||
_desiredPositionalTarget(0.0f),
|
||||
_linearTimeScale(FLT_MAX),
|
||||
_positionalTargetSet(true),
|
||||
_rotationalTarget(glm::quat()),
|
||||
_desiredRotationalTarget(glm::quat()),
|
||||
_positionalTargetSet(false),
|
||||
_rotationalTarget(),
|
||||
_desiredRotationalTarget(),
|
||||
_angularTimeScale(FLT_MAX),
|
||||
_rotationalTargetSet(true) {
|
||||
_rotationalTargetSet(true),
|
||||
_linearVelocityTarget(0.0f)
|
||||
{
|
||||
#if WANT_DEBUG
|
||||
qCDebug(physics) << "ObjectActionTractor::ObjectActionTractor";
|
||||
#endif
|
||||
|
@ -77,7 +79,6 @@ bool ObjectActionTractor::prepareForTractorUpdate(btScalar deltaTimeStep) {
|
|||
|
||||
glm::quat rotation;
|
||||
glm::vec3 position;
|
||||
glm::vec3 linearVelocity;
|
||||
glm::vec3 angularVelocity;
|
||||
|
||||
bool linearValid = false;
|
||||
|
@ -117,7 +118,6 @@ bool ObjectActionTractor::prepareForTractorUpdate(btScalar deltaTimeStep) {
|
|||
linearValid = true;
|
||||
linearTractorCount++;
|
||||
position += positionForAction;
|
||||
linearVelocity += linearVelocityForAction;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -126,9 +126,18 @@ bool ObjectActionTractor::prepareForTractorUpdate(btScalar deltaTimeStep) {
|
|||
withWriteLock([&]{
|
||||
if (linearValid && linearTractorCount > 0) {
|
||||
position /= linearTractorCount;
|
||||
linearVelocity /= linearTractorCount;
|
||||
if (_positionalTargetSet) {
|
||||
_lastPositionTarget = _positionalTarget;
|
||||
} else {
|
||||
_lastPositionTarget = position;
|
||||
}
|
||||
_positionalTarget = position;
|
||||
_linearVelocityTarget = linearVelocity;
|
||||
if (deltaTimeStep > EPSILON) {
|
||||
// blend the new velocity with the old (low-pass filter)
|
||||
glm::vec3 newVelocity = (1.0f / deltaTimeStep) * (position - _lastPositionTarget);
|
||||
const float blend = 0.25f;
|
||||
_linearVelocityTarget = (1.0f - blend) * _linearVelocityTarget + blend * newVelocity;
|
||||
}
|
||||
_positionalTargetSet = true;
|
||||
_active = true;
|
||||
}
|
||||
|
@ -169,19 +178,19 @@ void ObjectActionTractor::updateActionWorker(btScalar deltaTimeStep) {
|
|||
}
|
||||
|
||||
if (_linearTimeScale < MAX_TRACTOR_TIMESCALE) {
|
||||
btVector3 targetVelocity(0.0f, 0.0f, 0.0f);
|
||||
btVector3 offsetVelocity(0.0f, 0.0f, 0.0f);
|
||||
btVector3 offset = rigidBody->getCenterOfMassPosition() - glmToBullet(_positionalTarget);
|
||||
float offsetLength = offset.length();
|
||||
if (offsetLength > FLT_EPSILON) {
|
||||
float speed = glm::min(offsetLength / _linearTimeScale, TRACTOR_MAX_SPEED);
|
||||
targetVelocity = (-speed / offsetLength) * offset;
|
||||
offsetVelocity = (-speed / offsetLength) * offset;
|
||||
if (speed > rigidBody->getLinearSleepingThreshold()) {
|
||||
forceBodyNonStatic();
|
||||
rigidBody->activate();
|
||||
}
|
||||
}
|
||||
// this action is aggresively critically damped and defeats the current velocity
|
||||
rigidBody->setLinearVelocity(targetVelocity);
|
||||
rigidBody->setLinearVelocity(glmToBullet(_linearVelocityTarget) + offsetVelocity);
|
||||
}
|
||||
|
||||
if (_angularTimeScale < MAX_TRACTOR_TIMESCALE) {
|
||||
|
|
|
@ -36,6 +36,7 @@ protected:
|
|||
|
||||
glm::vec3 _positionalTarget;
|
||||
glm::vec3 _desiredPositionalTarget;
|
||||
glm::vec3 _lastPositionTarget;
|
||||
float _linearTimeScale;
|
||||
bool _positionalTargetSet;
|
||||
|
||||
|
|
143
libraries/plugins/src/plugins/InputConfiguration.cpp
Normal file
143
libraries/plugins/src/plugins/InputConfiguration.cpp
Normal file
|
@ -0,0 +1,143 @@
|
|||
//
|
||||
// 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
|
||||
//
|
||||
|
||||
|
||||
#include "InputConfiguration.h"
|
||||
|
||||
#include <QThread>
|
||||
|
||||
#include "DisplayPlugin.h"
|
||||
#include "InputPlugin.h"
|
||||
#include "PluginManager.h"
|
||||
|
||||
InputConfiguration::InputConfiguration() {
|
||||
}
|
||||
|
||||
QStringList InputConfiguration::inputPlugins() {
|
||||
if (QThread::currentThread() != thread()) {
|
||||
QStringList result;
|
||||
QMetaObject::invokeMethod(this, "inputPlugins", Qt::BlockingQueuedConnection,
|
||||
Q_RETURN_ARG(QStringList, result));
|
||||
return result;
|
||||
}
|
||||
|
||||
QStringList inputPlugins;
|
||||
for (auto plugin : PluginManager::getInstance()->getInputPlugins()) {
|
||||
QString pluginName = plugin->getName();
|
||||
if (pluginName == QString("OpenVR")) {
|
||||
inputPlugins << QString("Vive");
|
||||
} else {
|
||||
inputPlugins << pluginName;
|
||||
}
|
||||
}
|
||||
return inputPlugins;
|
||||
}
|
||||
|
||||
|
||||
QStringList InputConfiguration::activeInputPlugins() {
|
||||
if (QThread::currentThread() != thread()) {
|
||||
QStringList result;
|
||||
QMetaObject::invokeMethod(this, "activeInputPlugins", Qt::BlockingQueuedConnection,
|
||||
Q_RETURN_ARG(QStringList, result));
|
||||
return result;
|
||||
}
|
||||
|
||||
QStringList activePlugins;
|
||||
for (auto plugin : PluginManager::getInstance()->getInputPlugins()) {
|
||||
if (plugin->configurable()) {
|
||||
QString pluginName = plugin->getName();
|
||||
if (pluginName == QString("OpenVR")) {
|
||||
activePlugins << QString("Vive");
|
||||
} else {
|
||||
activePlugins << pluginName;
|
||||
}
|
||||
}
|
||||
}
|
||||
return activePlugins;
|
||||
}
|
||||
|
||||
QString InputConfiguration::configurationLayout(QString pluginName) {
|
||||
if (QThread::currentThread() != thread()) {
|
||||
QString result;
|
||||
QMetaObject::invokeMethod(this, "configurationLayout", Qt::BlockingQueuedConnection,
|
||||
Q_RETURN_ARG(QString, result),
|
||||
Q_ARG(QString, pluginName));
|
||||
return result;
|
||||
}
|
||||
|
||||
QString sourcePath;
|
||||
for (auto plugin : PluginManager::getInstance()->getInputPlugins()) {
|
||||
if (plugin->getName() == pluginName) {
|
||||
return plugin->configurationLayout();
|
||||
}
|
||||
}
|
||||
return sourcePath;
|
||||
}
|
||||
|
||||
void InputConfiguration::setConfigurationSettings(QJsonObject configurationSettings, QString pluginName) {
|
||||
if (QThread::currentThread() != thread()) {
|
||||
QMetaObject::invokeMethod(this, "setConfigurationSettings", Qt::BlockingQueuedConnection,
|
||||
Q_ARG(QJsonObject, configurationSettings),
|
||||
Q_ARG(QString, pluginName));
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto plugin : PluginManager::getInstance()->getInputPlugins()) {
|
||||
if (plugin->getName() == pluginName) {
|
||||
plugin->setConfigurationSettings(configurationSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QJsonObject InputConfiguration::configurationSettings(QString pluginName) {
|
||||
if (QThread::currentThread() != thread()) {
|
||||
QJsonObject result;
|
||||
QMetaObject::invokeMethod(this, "configurationSettings", Qt::BlockingQueuedConnection,
|
||||
Q_RETURN_ARG(QJsonObject, result),
|
||||
Q_ARG(QString, pluginName));
|
||||
return result;
|
||||
}
|
||||
|
||||
for (auto plugin : PluginManager::getInstance()->getInputPlugins()) {
|
||||
if (plugin->getName() == pluginName) {
|
||||
return plugin->configurationSettings();
|
||||
}
|
||||
}
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
void InputConfiguration::calibratePlugin(QString pluginName) {
|
||||
if (QThread::currentThread() != thread()) {
|
||||
QMetaObject::invokeMethod(this, "calibratePlugin", Qt::BlockingQueuedConnection);
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto plugin : PluginManager::getInstance()->getInputPlugins()) {
|
||||
if (plugin->getName() == pluginName) {
|
||||
plugin->calibrate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool InputConfiguration::uncalibratePlugin(QString pluginName) {
|
||||
if (QThread::currentThread() != thread()) {
|
||||
bool result;
|
||||
QMetaObject::invokeMethod(this, "uncalibratePlugin", Qt::BlockingQueuedConnection,
|
||||
Q_ARG(bool, result));
|
||||
return result;
|
||||
}
|
||||
|
||||
for (auto plugin : PluginManager::getInstance()->getInputPlugins()) {
|
||||
if (plugin->getName() == pluginName) {
|
||||
return plugin->uncalibrate();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
37
libraries/plugins/src/plugins/InputConfiguration.h
Normal file
37
libraries/plugins/src/plugins/InputConfiguration.h
Normal file
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// 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
|
||||
//
|
||||
|
||||
#ifndef hifi_InputConfiguration_h
|
||||
#define hifi_InputConfiguration_h
|
||||
|
||||
#include <mutex>
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QJsonObject>
|
||||
#include <DependencyManager.h>
|
||||
|
||||
class InputConfiguration : public QObject, public Dependency {
|
||||
Q_OBJECT
|
||||
public:
|
||||
InputConfiguration();
|
||||
|
||||
Q_INVOKABLE QStringList inputPlugins();
|
||||
Q_INVOKABLE QStringList activeInputPlugins();
|
||||
Q_INVOKABLE QString configurationLayout(QString pluginName);
|
||||
Q_INVOKABLE void setConfigurationSettings(QJsonObject configurationSettings, QString pluginName);
|
||||
Q_INVOKABLE void calibratePlugin(QString pluginName);
|
||||
Q_INVOKABLE QJsonObject configurationSettings(QString pluginName);
|
||||
Q_INVOKABLE bool uncalibratePlugin(QString pluginName);
|
||||
|
||||
signals:
|
||||
void calibrationStatus(const QJsonObject& status);
|
||||
};
|
||||
|
||||
#endif
|
|
@ -11,6 +11,7 @@
|
|||
#pragma once
|
||||
|
||||
#include "Plugin.h"
|
||||
#include <QJsonObject>
|
||||
|
||||
namespace controller {
|
||||
struct InputCalibrationData;
|
||||
|
@ -24,7 +25,12 @@ public:
|
|||
// Some input plugins are comprised of multiple subdevices (SDL2, for instance).
|
||||
// If an input plugin is only a single device, it will only return it's primary name.
|
||||
virtual QStringList getSubdeviceNames() { return { getName() }; };
|
||||
virtual void setConfigurationSettings(const QJsonObject configurationSettings) { }
|
||||
virtual QJsonObject configurationSettings() { return QJsonObject(); }
|
||||
virtual QString configurationLayout() { return QString(); }
|
||||
virtual void calibrate() {}
|
||||
virtual bool uncalibrate() { return false; }
|
||||
virtual bool configurable() { return false; }
|
||||
virtual bool isHandController() const { return false; }
|
||||
virtual bool isHeadController() const { return false; }
|
||||
};
|
||||
|
||||
|
|
|
@ -548,6 +548,7 @@ glm::ivec3 LightClusters::updateClusters() {
|
|||
|
||||
|
||||
LightClusteringPass::LightClusteringPass() {
|
||||
_lightClusters = std::make_shared<LightClusters>();
|
||||
}
|
||||
|
||||
|
||||
|
@ -566,12 +567,7 @@ void LightClusteringPass::run(const render::RenderContextPointer& renderContext,
|
|||
auto deferredTransform = inputs.get0();
|
||||
auto lightingModel = inputs.get1();
|
||||
auto surfaceGeometryFramebuffer = inputs.get2();
|
||||
|
||||
|
||||
if (!_lightClusters) {
|
||||
_lightClusters = std::make_shared<LightClusters>();
|
||||
}
|
||||
|
||||
|
||||
// first update the Grid with the new frustum
|
||||
if (!_freeze) {
|
||||
_lightClusters->updateFrustum(args->getViewFrustum());
|
||||
|
|
|
@ -124,6 +124,11 @@ signals:
|
|||
void dirtyEnabled();
|
||||
};
|
||||
|
||||
class TConfigProxy {
|
||||
public:
|
||||
using Config = JobConfig;
|
||||
};
|
||||
|
||||
class TaskConfig : public JobConfig {
|
||||
Q_OBJECT
|
||||
public:
|
||||
|
@ -134,12 +139,37 @@ public:
|
|||
TaskConfig() = default ;
|
||||
TaskConfig(bool enabled) : JobConfig(enabled) {}
|
||||
|
||||
|
||||
|
||||
// Get a sub job config through task.getConfig(path)
|
||||
// where path can be:
|
||||
// - <job_name> search for the first job named job_name traversing the the sub graph of task and jobs (from this task as root)
|
||||
// - <parent_name>.[<sub_parent_names>.]<job_name>
|
||||
// Allowing to first look for the parent_name job (from this task as root) and then search from there for the
|
||||
// optional sub_parent_names and finally from there looking for the job_name (assuming every job in the path were found)
|
||||
//
|
||||
// getter for qml integration, prefer the templated getter
|
||||
Q_INVOKABLE QObject* getConfig(const QString& name) { return QObject::findChild<JobConfig*>(name); }
|
||||
Q_INVOKABLE QObject* getConfig(const QString& name) { return getConfig<TConfigProxy>(name.toStdString()); }
|
||||
// getter for cpp (strictly typed), prefer this getter
|
||||
template <class T> typename T::Config* getConfig(std::string job = "") const {
|
||||
QString name = job.empty() ? QString() : QString(job.c_str()); // an empty string is not a null string
|
||||
return findChild<typename T::Config*>(name);
|
||||
const TaskConfig* root = this;
|
||||
QString path = (job.empty() ? QString() : QString(job.c_str())); // an empty string is not a null string
|
||||
auto tokens = path.split('.', QString::SkipEmptyParts);
|
||||
|
||||
if (tokens.empty()) {
|
||||
tokens.push_back(QString());
|
||||
} else {
|
||||
while (tokens.size() > 1) {
|
||||
auto name = tokens.front();
|
||||
tokens.pop_front();
|
||||
root = QObject::findChild<TaskConfig*>(name);
|
||||
if (!root) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return root->findChild<typename T::Config*>(tokens.front());
|
||||
}
|
||||
|
||||
void connectChildConfig(QConfigPointer childConfig, const std::string& name);
|
||||
|
|
|
@ -11,6 +11,8 @@
|
|||
|
||||
#include "AudioScriptingInterface.h"
|
||||
|
||||
#include <QVector3D>
|
||||
|
||||
#include "ScriptAudioInjector.h"
|
||||
#include "ScriptEngineLogging.h"
|
||||
|
||||
|
@ -19,6 +21,13 @@ void registerAudioMetaTypes(QScriptEngine* engine) {
|
|||
qScriptRegisterMetaType(engine, soundSharedPointerToScriptValue, soundSharedPointerFromScriptValue);
|
||||
}
|
||||
|
||||
ScriptAudioInjector* AudioScriptingInterface::playSystemSound(SharedSoundPointer sound, const QVector3D& position) {
|
||||
AudioInjectorOptions options;
|
||||
options.position = glm::vec3(position.x(), position.y(), position.z());
|
||||
options.localOnly = true;
|
||||
return playSound(sound, options);
|
||||
}
|
||||
|
||||
ScriptAudioInjector* AudioScriptingInterface::playSound(SharedSoundPointer sound, const AudioInjectorOptions& injectorOptions) {
|
||||
if (QThread::currentThread() != thread()) {
|
||||
ScriptAudioInjector* injector = NULL;
|
||||
|
|
|
@ -30,8 +30,10 @@ public:
|
|||
protected:
|
||||
AudioScriptingInterface() {}
|
||||
|
||||
// this method is protected to stop C++ callers from calling, but invokable from script
|
||||
// these methods are protected to stop C++ callers from calling, but invokable from script
|
||||
Q_INVOKABLE ScriptAudioInjector* playSound(SharedSoundPointer sound, const AudioInjectorOptions& injectorOptions = AudioInjectorOptions());
|
||||
// FIXME: there is no way to play a positionless sound
|
||||
Q_INVOKABLE ScriptAudioInjector* playSystemSound(SharedSoundPointer sound, const QVector3D& position);
|
||||
|
||||
Q_INVOKABLE void setStereoInput(bool stereo);
|
||||
|
||||
|
|
182
libraries/script-engine/src/ConsoleScriptingInterface.cpp
Normal file
182
libraries/script-engine/src/ConsoleScriptingInterface.cpp
Normal file
|
@ -0,0 +1,182 @@
|
|||
//
|
||||
// ConsoleScriptingInterface.cpp
|
||||
// libraries/script-engine/src
|
||||
//
|
||||
// Created by NeetBhagat on 6/1/17.
|
||||
// Copyright 2014 High Fidelity, Inc.
|
||||
//
|
||||
// ConsoleScriptingInterface is responsible for following functionality
|
||||
// Printing logs with various tags and grouping on debug Window and Logs/log file.
|
||||
// Debugging functionalities like Timer start-end, assertion, trace.
|
||||
// To use these functionalities, use "console" object in Javascript files.
|
||||
// For examples please refer "scripts/developer/tests/consoleObjectTest.js"
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
#include "ConsoleScriptingInterface.h"
|
||||
#include "ScriptEngine.h"
|
||||
|
||||
#define INDENTATION 4 // 1 Tab - 4 spaces
|
||||
const QString LINE_SEPARATOR = "\n ";
|
||||
const QString SPACE_SEPARATOR = " ";
|
||||
const QString STACK_TRACE_FORMAT = "\n[Stacktrace]%1%2";
|
||||
QList<QString> ConsoleScriptingInterface::_groupDetails = QList<QString>();
|
||||
|
||||
QScriptValue ConsoleScriptingInterface::info(QScriptContext* context, QScriptEngine* engine) {
|
||||
if (ScriptEngine* scriptEngine = qobject_cast<ScriptEngine*>(engine)) {
|
||||
scriptEngine->scriptInfoMessage(appendArguments(context));
|
||||
}
|
||||
return QScriptValue::NullValue;
|
||||
}
|
||||
|
||||
QScriptValue ConsoleScriptingInterface::log(QScriptContext* context, QScriptEngine* engine) {
|
||||
QString message = appendArguments(context);
|
||||
if (_groupDetails.count() == 0) {
|
||||
if (ScriptEngine* scriptEngine = qobject_cast<ScriptEngine*>(engine)) {
|
||||
scriptEngine->scriptPrintedMessage(message);
|
||||
}
|
||||
} else {
|
||||
logGroupMessage(message, engine);
|
||||
}
|
||||
return QScriptValue::NullValue;
|
||||
}
|
||||
|
||||
QScriptValue ConsoleScriptingInterface::debug(QScriptContext* context, QScriptEngine* engine) {
|
||||
if (ScriptEngine* scriptEngine = qobject_cast<ScriptEngine*>(engine)) {
|
||||
scriptEngine->scriptPrintedMessage(appendArguments(context));
|
||||
}
|
||||
return QScriptValue::NullValue;
|
||||
}
|
||||
|
||||
QScriptValue ConsoleScriptingInterface::warn(QScriptContext* context, QScriptEngine* engine) {
|
||||
if (ScriptEngine* scriptEngine = qobject_cast<ScriptEngine*>(engine)) {
|
||||
scriptEngine->scriptWarningMessage(appendArguments(context));
|
||||
}
|
||||
return QScriptValue::NullValue;
|
||||
}
|
||||
|
||||
QScriptValue ConsoleScriptingInterface::error(QScriptContext* context, QScriptEngine* engine) {
|
||||
if (ScriptEngine* scriptEngine = qobject_cast<ScriptEngine*>(engine)) {
|
||||
scriptEngine->scriptErrorMessage(appendArguments(context));
|
||||
}
|
||||
return QScriptValue::NullValue;
|
||||
}
|
||||
|
||||
QScriptValue ConsoleScriptingInterface::exception(QScriptContext* context, QScriptEngine* engine) {
|
||||
if (ScriptEngine* scriptEngine = qobject_cast<ScriptEngine*>(engine)) {
|
||||
scriptEngine->scriptErrorMessage(appendArguments(context));
|
||||
}
|
||||
return QScriptValue::NullValue;
|
||||
}
|
||||
|
||||
void ConsoleScriptingInterface::time(QString labelName) {
|
||||
_timerDetails.insert(labelName, QDateTime::currentDateTime().toUTC());
|
||||
QString message = QString("%1: Timer started").arg(labelName);
|
||||
if (ScriptEngine* scriptEngine = qobject_cast<ScriptEngine*>(engine())) {
|
||||
scriptEngine->scriptPrintedMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
void ConsoleScriptingInterface::timeEnd(QString labelName) {
|
||||
if (ScriptEngine* scriptEngine = qobject_cast<ScriptEngine*>(engine())) {
|
||||
if (!_timerDetails.contains(labelName)) {
|
||||
scriptEngine->scriptErrorMessage("No such label found " + labelName);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_timerDetails.value(labelName).isNull()) {
|
||||
_timerDetails.remove(labelName);
|
||||
scriptEngine->scriptErrorMessage("Invalid start time for " + labelName);
|
||||
return;
|
||||
}
|
||||
QDateTime _startTime = _timerDetails.value(labelName);
|
||||
QDateTime _endTime = QDateTime::currentDateTime().toUTC();
|
||||
qint64 diffInMS = _startTime.msecsTo(_endTime);
|
||||
|
||||
QString message = QString("%1: %2ms").arg(labelName).arg(QString::number(diffInMS));
|
||||
_timerDetails.remove(labelName);
|
||||
|
||||
scriptEngine->scriptPrintedMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
QScriptValue ConsoleScriptingInterface::assertion(QScriptContext* context, QScriptEngine* engine) {
|
||||
QString message;
|
||||
bool condition = false;
|
||||
for (int i = 0; i < context->argumentCount(); i++) {
|
||||
if (i == 0) {
|
||||
condition = context->argument(i).toBool(); // accept first value as condition
|
||||
} else {
|
||||
message += SPACE_SEPARATOR + context->argument(i).toString(); // accept other parameters as message
|
||||
}
|
||||
}
|
||||
|
||||
QString assertionResult;
|
||||
if (!condition) {
|
||||
if (message.isEmpty()) {
|
||||
assertionResult = "Assertion failed";
|
||||
} else {
|
||||
assertionResult = QString("Assertion failed : %1").arg(message);
|
||||
}
|
||||
if (ScriptEngine* scriptEngine = qobject_cast<ScriptEngine*>(engine)) {
|
||||
scriptEngine->scriptErrorMessage(assertionResult);
|
||||
}
|
||||
}
|
||||
return QScriptValue::NullValue;
|
||||
}
|
||||
|
||||
void ConsoleScriptingInterface::trace() {
|
||||
if (ScriptEngine* scriptEngine = qobject_cast<ScriptEngine*>(engine())) {
|
||||
scriptEngine->scriptPrintedMessage
|
||||
(QString(STACK_TRACE_FORMAT).arg(LINE_SEPARATOR,
|
||||
scriptEngine->currentContext()->backtrace().join(LINE_SEPARATOR)));
|
||||
}
|
||||
}
|
||||
|
||||
void ConsoleScriptingInterface::clear() {
|
||||
if (ScriptEngine* scriptEngine = qobject_cast<ScriptEngine*>(engine())) {
|
||||
scriptEngine->clearDebugLogWindow();
|
||||
}
|
||||
}
|
||||
|
||||
QScriptValue ConsoleScriptingInterface::group(QScriptContext* context, QScriptEngine* engine) {
|
||||
logGroupMessage(context->argument(0).toString(), engine); // accept first parameter as label
|
||||
_groupDetails.push_back(context->argument(0).toString());
|
||||
return QScriptValue::NullValue;
|
||||
}
|
||||
|
||||
QScriptValue ConsoleScriptingInterface::groupCollapsed(QScriptContext* context, QScriptEngine* engine) {
|
||||
logGroupMessage(context->argument(0).toString(), engine); // accept first parameter as label
|
||||
_groupDetails.push_back(context->argument(0).toString());
|
||||
return QScriptValue::NullValue;
|
||||
}
|
||||
|
||||
QScriptValue ConsoleScriptingInterface::groupEnd(QScriptContext* context, QScriptEngine* engine) {
|
||||
ConsoleScriptingInterface::_groupDetails.removeLast();
|
||||
return QScriptValue::NullValue;
|
||||
}
|
||||
|
||||
QString ConsoleScriptingInterface::appendArguments(QScriptContext* context) {
|
||||
QString message;
|
||||
for (int i = 0; i < context->argumentCount(); i++) {
|
||||
if (i > 0) {
|
||||
message += SPACE_SEPARATOR;
|
||||
}
|
||||
message += context->argument(i).toString();
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
void ConsoleScriptingInterface::logGroupMessage(QString message, QScriptEngine* engine) {
|
||||
int _addSpaces = _groupDetails.count() * INDENTATION;
|
||||
QString logMessage;
|
||||
for (int i = 0; i < _addSpaces; i++) {
|
||||
logMessage.append(SPACE_SEPARATOR);
|
||||
}
|
||||
logMessage.append(message);
|
||||
if (ScriptEngine* scriptEngine = qobject_cast<ScriptEngine*>(engine)) {
|
||||
scriptEngine->scriptPrintedMessage(logMessage);
|
||||
}
|
||||
}
|
56
libraries/script-engine/src/ConsoleScriptingInterface.h
Normal file
56
libraries/script-engine/src/ConsoleScriptingInterface.h
Normal file
|
@ -0,0 +1,56 @@
|
|||
//
|
||||
// ConsoleScriptingInterface.h
|
||||
// libraries/script-engine/src
|
||||
//
|
||||
// Created by NeetBhagat on 6/1/17.
|
||||
// Copyright 2014 High Fidelity, Inc.
|
||||
//
|
||||
// ConsoleScriptingInterface is responsible for following functionality
|
||||
// Printing logs with various tags and grouping on debug Window and Logs/log file.
|
||||
// Debugging functionalities like Timer start-end, assertion, trace.
|
||||
// To use these functionalities, use "console" object in Javascript files.
|
||||
// For examples please refer "scripts/developer/tests/consoleObjectTest.js"
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
#pragma once
|
||||
#ifndef hifi_ConsoleScriptingInterface_h
|
||||
#define hifi_ConsoleScriptingInterface_h
|
||||
|
||||
#include <QtCore/QObject>
|
||||
#include <QtCore/QString>
|
||||
#include <QtScript/QScriptable>
|
||||
#include <QList>
|
||||
#include <QHash>
|
||||
|
||||
// Scriptable interface of "console" object. Used exclusively in the JavaScript API
|
||||
class ConsoleScriptingInterface : public QObject, protected QScriptable {
|
||||
Q_OBJECT
|
||||
public:
|
||||
static QScriptValue info(QScriptContext* context, QScriptEngine* engine);
|
||||
static QScriptValue log(QScriptContext* context, QScriptEngine* engine);
|
||||
static QScriptValue debug(QScriptContext* context, QScriptEngine* engine);
|
||||
static QScriptValue warn(QScriptContext* context, QScriptEngine* engine);
|
||||
static QScriptValue error(QScriptContext* context, QScriptEngine* engine);
|
||||
static QScriptValue exception(QScriptContext* context, QScriptEngine* engine);
|
||||
static QScriptValue assertion(QScriptContext* context, QScriptEngine* engine);
|
||||
static QScriptValue group(QScriptContext* context, QScriptEngine* engine);
|
||||
static QScriptValue groupCollapsed(QScriptContext* context, QScriptEngine* engine);
|
||||
static QScriptValue groupEnd(QScriptContext* context, QScriptEngine* engine);
|
||||
|
||||
public slots:
|
||||
void time(QString labelName);
|
||||
void timeEnd(QString labelName);
|
||||
void trace();
|
||||
void clear();
|
||||
|
||||
private:
|
||||
QHash<QString, QDateTime> _timerDetails;
|
||||
static QList<QString> _groupDetails;
|
||||
static void logGroupMessage(QString message, QScriptEngine* engine);
|
||||
static QString appendArguments(QScriptContext* context);
|
||||
};
|
||||
|
||||
#endif // hifi_ConsoleScriptingInterface_h
|
|
@ -472,20 +472,24 @@ void ScriptEngine::scriptErrorMessage(const QString& message) {
|
|||
}
|
||||
|
||||
void ScriptEngine::scriptWarningMessage(const QString& message) {
|
||||
qCWarning(scriptengine) << message;
|
||||
qCWarning(scriptengine) << qPrintable(message);
|
||||
emit warningMessage(message, getFilename());
|
||||
}
|
||||
|
||||
void ScriptEngine::scriptInfoMessage(const QString& message) {
|
||||
qCInfo(scriptengine) << message;
|
||||
qCInfo(scriptengine) << qPrintable(message);
|
||||
emit infoMessage(message, getFilename());
|
||||
}
|
||||
|
||||
void ScriptEngine::scriptPrintedMessage(const QString& message) {
|
||||
qCDebug(scriptengine) << message;
|
||||
qCDebug(scriptengine) << qPrintable(message);
|
||||
emit printedMessage(message, getFilename());
|
||||
}
|
||||
|
||||
void ScriptEngine::clearDebugLogWindow() {
|
||||
emit clearDebugWindow();
|
||||
}
|
||||
|
||||
// Even though we never pass AnimVariantMap directly to and from javascript, the queued invokeMethod of
|
||||
// callAnimationStateHandler requires that the type be registered.
|
||||
// These two are meaningful, if we ever do want to use them...
|
||||
|
@ -668,8 +672,18 @@ void ScriptEngine::init() {
|
|||
registerGlobalObject("Mat4", &_mat4Library);
|
||||
registerGlobalObject("Uuid", &_uuidLibrary);
|
||||
registerGlobalObject("Messages", DependencyManager::get<MessagesClient>().data());
|
||||
|
||||
registerGlobalObject("File", new FileScriptingInterface(this));
|
||||
registerGlobalObject("console", &_consoleScriptingInterface);
|
||||
registerFunction("console", "info", ConsoleScriptingInterface::info, currentContext()->argumentCount());
|
||||
registerFunction("console", "log", ConsoleScriptingInterface::log, currentContext()->argumentCount());
|
||||
registerFunction("console", "debug", ConsoleScriptingInterface::debug, currentContext()->argumentCount());
|
||||
registerFunction("console", "warn", ConsoleScriptingInterface::warn, currentContext()->argumentCount());
|
||||
registerFunction("console", "error", ConsoleScriptingInterface::error, currentContext()->argumentCount());
|
||||
registerFunction("console", "exception", ConsoleScriptingInterface::exception, currentContext()->argumentCount());
|
||||
registerFunction("console", "assert", ConsoleScriptingInterface::assertion, currentContext()->argumentCount());
|
||||
registerFunction("console", "group", ConsoleScriptingInterface::group, 1);
|
||||
registerFunction("console", "groupCollapsed", ConsoleScriptingInterface::groupCollapsed, 1);
|
||||
registerFunction("console", "groupEnd", ConsoleScriptingInterface::groupEnd, 0);
|
||||
|
||||
qScriptRegisterMetaType(this, animVarMapToScriptValue, animVarMapFromScriptValue);
|
||||
qScriptRegisterMetaType(this, resultHandlerToScriptValue, resultHandlerFromScriptValue);
|
||||
|
@ -1006,6 +1020,7 @@ void ScriptEngine::run() {
|
|||
emit runningStateChanged();
|
||||
|
||||
{
|
||||
PROFILE_RANGE(script, _fileNameString);
|
||||
evaluate(_scriptContents, _fileNameString);
|
||||
maybeEmitUncaughtException(__FUNCTION__);
|
||||
}
|
||||
|
@ -1269,6 +1284,7 @@ void ScriptEngine::timerFired() {
|
|||
|
||||
// call the associated JS function, if it exists
|
||||
if (timerData.function.isValid()) {
|
||||
PROFILE_RANGE(script, __FUNCTION__);
|
||||
auto preTimer = p_high_resolution_clock::now();
|
||||
callWithEnvironment(timerData.definingEntityIdentifier, timerData.definingSandboxURL, timerData.function, timerData.function, QScriptValueList());
|
||||
auto postTimer = p_high_resolution_clock::now();
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
#include "ScriptCache.h"
|
||||
#include "ScriptUUID.h"
|
||||
#include "Vec3.h"
|
||||
#include "ConsoleScriptingInterface.h"
|
||||
#include "SettingHandle.h"
|
||||
|
||||
class QScriptEngineDebugger;
|
||||
|
@ -225,7 +226,7 @@ public:
|
|||
void scriptWarningMessage(const QString& message);
|
||||
void scriptInfoMessage(const QString& message);
|
||||
void scriptPrintedMessage(const QString& message);
|
||||
|
||||
void clearDebugLogWindow();
|
||||
int getNumRunningEntityScripts() const;
|
||||
bool getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const;
|
||||
|
||||
|
@ -245,6 +246,7 @@ signals:
|
|||
void warningMessage(const QString& message, const QString& scriptName);
|
||||
void infoMessage(const QString& message, const QString& scriptName);
|
||||
void runningStateChanged();
|
||||
void clearDebugWindow();
|
||||
void loadScript(const QString& scriptName, bool isUserLoaded);
|
||||
void reloadScript(const QString& scriptName, bool isUserLoaded);
|
||||
void doneRunning();
|
||||
|
@ -305,6 +307,7 @@ protected:
|
|||
Vec3 _vec3Library;
|
||||
Mat4 _mat4Library;
|
||||
ScriptUUID _uuidLibrary;
|
||||
ConsoleScriptingInterface _consoleScriptingInterface;
|
||||
std::atomic<bool> _isUserLoaded { false };
|
||||
bool _isReloading { false };
|
||||
|
||||
|
|
|
@ -54,6 +54,10 @@ void ScriptEngines::onInfoMessage(const QString& message, const QString& scriptN
|
|||
emit infoMessage(message, scriptName);
|
||||
}
|
||||
|
||||
void ScriptEngines::onClearDebugWindow() {
|
||||
emit clearDebugWindow();
|
||||
}
|
||||
|
||||
void ScriptEngines::onErrorLoadingScript(const QString& url) {
|
||||
emit errorLoadingScript(url);
|
||||
}
|
||||
|
|
|
@ -79,6 +79,7 @@ signals:
|
|||
void warningMessage(const QString& message, const QString& engineName);
|
||||
void infoMessage(const QString& message, const QString& engineName);
|
||||
void errorLoadingScript(const QString& url);
|
||||
void clearDebugWindow();
|
||||
|
||||
public slots:
|
||||
void onPrintedMessage(const QString& message, const QString& scriptName);
|
||||
|
@ -86,6 +87,7 @@ public slots:
|
|||
void onWarningMessage(const QString& message, const QString& scriptName);
|
||||
void onInfoMessage(const QString& message, const QString& scriptName);
|
||||
void onErrorLoadingScript(const QString& url);
|
||||
void onClearDebugWindow();
|
||||
|
||||
protected slots:
|
||||
void onScriptFinished(const QString& fileNameString, ScriptEngine* engine);
|
||||
|
|
|
@ -614,15 +614,6 @@ void TabletProxy::removeButton(QObject* tabletButtonProxy) {
|
|||
}
|
||||
}
|
||||
|
||||
void TabletProxy::updateAudioBar(const double micLevel) {
|
||||
auto tablet = getQmlTablet();
|
||||
if (!tablet) {
|
||||
//qCCritical(scriptengine) << "Could not find tablet in TabletRoot.qml";
|
||||
} else {
|
||||
QMetaObject::invokeMethod(tablet, "setMicLevel", Qt::AutoConnection, Q_ARG(QVariant, QVariant(micLevel)));
|
||||
}
|
||||
}
|
||||
|
||||
void TabletProxy::emitScriptEvent(QVariant msg) {
|
||||
if (!_toolbarMode && _qmlOffscreenSurface) {
|
||||
QMetaObject::invokeMethod(_qmlOffscreenSurface, "emitScriptEvent", Qt::AutoConnection, Q_ARG(QVariant, msg));
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue