Merge branch 'master' into DOC-124

# Conflicts:
#	interface/src/scripting/Audio.h
This commit is contained in:
David Rowe 2019-08-02 20:34:09 +12:00
commit 789f9c7d67
144 changed files with 6506 additions and 1494 deletions

View file

@ -66,12 +66,6 @@ Open `%HIFI_DIR%\build\hifi.sln` using Visual Studio.
Change the Solution Configuration (menu ribbon under the menu bar, next to the green play button) from "Debug" to "Release" for best performance.
Create another environment variable (see Step #3)
* Set "Variable name": `PreferredToolArchitecture`
* Set "Variable value": `x64`
Restart Visual Studio for the new variable to take effect.
Run from the menu bar `Build > Build Solution`.
### Step 6. Testing Interface

View file

@ -10,6 +10,11 @@ endif()
include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/macros/TargetPython.cmake")
target_python()
if (WIN32 AND NOT HIFI_ANDROID)
# Force x64 toolset
set(CMAKE_GENERATOR_TOOLSET "host=x64" CACHE STRING "64-bit toolset" FORCE)
endif()
# set our OS X deployment target
# (needs to be set before first project() call and before prebuild.py)
# Will affect VCPKG dependencies

View file

@ -17,7 +17,7 @@ Documentation
=========
Documentation is available at [docs.highfidelity.com](https://docs.highfidelity.com), if something is missing, please suggest it via a new job on Worklist (add to the hifi-docs project).
There is also detailed [documentation on our coding standards](https://wiki.highfidelity.com/wiki/Coding_Standards).
There is also detailed [documentation on our coding standards](CODING_STANDARD.md).
Contributor License Agreement (CLA)
=========

View file

@ -71,7 +71,7 @@ void MixerAvatar::fetchAvatarFST() {
connect(fstRequest, &ResourceRequest::finished, this, &MixerAvatar::fstRequestComplete);
fstRequest->send();
} else {
qCDebug(avatars) << "Couldn't create FST request for" << avatarURL;
qCDebug(avatars) << "Couldn't create FST request for" << avatarURL << getSessionUUID();
_verifyState = error;
}
_needsIdentityUpdate = true;
@ -84,7 +84,7 @@ void MixerAvatar::fstRequestComplete() {
auto result = fstRequest->getResult();
if (result != ResourceRequest::Success) {
_verifyState = error;
qCDebug(avatars) << "FST request for" << fstRequest->getUrl() << "failed:" << result;
qCDebug(avatars) << "FST request for" << fstRequest->getUrl() << "(user " << getSessionUUID() << ") failed:" << result;
} else {
_avatarFSTContents = fstRequest->getData();
_verifyState = receivedFST;
@ -178,7 +178,8 @@ void MixerAvatar::ownerRequestComplete() {
} else {
auto jsonData = QJsonDocument::fromJson(networkReply->readAll())["data"];
if (!jsonData.isUndefined() && !jsonData.toObject()["message"].isUndefined()) {
qCDebug(avatars) << "Owner lookup failed for" << getDisplayName() << ":"
qCDebug(avatars) << "Owner lookup failed for" << getDisplayName() << "("
<< getSessionUUID() << ") :"
<< jsonData.toObject()["message"].toString();
_verifyState = error;
_pendingEvent = false;
@ -221,7 +222,7 @@ void MixerAvatar::processCertifyEvents() {
} else {
_needsIdentityUpdate = true;
_pendingEvent = false;
qCDebug(avatars) << "Avatar" << getDisplayName() << "FAILED static certification";
qCDebug(avatars) << "Avatar" << getDisplayName() << "(" << getSessionUUID() << ") FAILED static certification";
}
} else { // FST doesn't have a certificate, so noncertified rather than failed:
_pendingEvent = false;
@ -261,6 +262,8 @@ void MixerAvatar::processCertifyEvents() {
_pendingEvent = true;
} else {
_verifyState = error;
qCDebug(avatars) << "Get owner status - couldn't parse response for" << getSessionUUID()
<< ":" << _dynamicMarketResponse;
}
} else {
qCDebug(avatars) << "Get owner status failed for " << getDisplayName() << _marketplaceIdFromURL <<
@ -332,7 +335,7 @@ void MixerAvatar::sendOwnerChallenge() {
nonceHash.addData(nonce);
_challengeNonceHash = nonceHash.result();
static constexpr int CHALLENGE_TIMEOUT_MS = 5 * 1000; // 5 s
static constexpr int CHALLENGE_TIMEOUT_MS = 10 * 1000; // 10 s
if (_challengeTimeout) {
_challengeTimeout->deleteLater();
}
@ -344,6 +347,7 @@ void MixerAvatar::sendOwnerChallenge() {
_pendingEvent = false;
_verifyState = verificationFailed;
_needsIdentityUpdate = true;
qCDebug(avatars) << "Dynamic verification TIMED-OUT for " << getDisplayName() << getSessionUUID();
}
});
_challengeTimeout->start();

View file

@ -1292,6 +1292,7 @@ void OctreeServer::aboutToFinish() {
for (auto& it : _sendThreads) {
auto& sendThread = *it.second;
sendThread.setIsShuttingDown();
sendThread.terminate();
}
// Clear will destruct all the unique_ptr to OctreeSendThreads which will call the GenericThread's dtor

View file

@ -131,8 +131,11 @@ macro(SET_PACKAGING_PARAMETERS)
endif ()
if (DEPLOY_PACKAGE)
# for deployed packages always grab the serverless content
set(DOWNLOAD_SERVERLESS_CONTENT ON)
# For deployed packages we do not grab the serverless content any longer.
# Instead, we deploy just the serverless content that is in the interface/resources/serverless
# directory. If we ever move back to delivering serverless via a hosted .zip file,
# we can re-enable this.
set(DOWNLOAD_SERVERLESS_CONTENT OFF)
endif ()
if (APPLE)

View file

@ -0,0 +1,24 @@
#
# Copyright 2019 High Fidelity, Inc.
#
# Distributed under the Apache License, Version 2.0.
# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
#
macro(TARGET_WEBRTC)
if (ANDROID)
# I don't yet have working libwebrtc for android
# include(SelectLibraryConfigurations)
# set(INSTALL_DIR ${HIFI_ANDROID_PRECOMPILED}/webrtc/webrtc)
# set(WEBRTC_INCLUDE_DIRS "${INSTALL_DIR}/include/webrtc")
# set(WEBRTC_LIBRARY_DEBUG ${INSTALL_DIR}/debug/lib/libwebrtc.a)
# set(WEBRTC_LIBRARY_RELEASE ${INSTALL_DIR}/lib/libwebrtc.a)
# select_library_configurations(WEBRTC)
else()
set(WEBRTC_INCLUDE_DIRS "${VCPKG_INSTALL_ROOT}/include/webrtc")
find_library(WEBRTC_LIBRARY NAMES webrtc PATHS ${VCPKG_INSTALL_ROOT}/lib/ NO_DEFAULT_PATH)
target_include_directories(${TARGET_NAME} SYSTEM PUBLIC ${WEBRTC_INCLUDE_DIRS})
target_link_libraries(${TARGET_NAME} ${WEBRTC_LIBRARY})
endif()
endmacro()

View file

@ -1,4 +1,4 @@
Source: hifi-deps
Version: 0.1
Description: Collected dependencies for High Fidelity applications
Build-Depends: bullet3, draco, etc2comp, glm, nvtt, openexr (!android), openssl (windows), tbb (!android&!osx), zlib
Build-Depends: bullet3, draco, etc2comp, glm, nvtt, openexr (!android), openssl (windows), tbb (!android&!osx), zlib, webrtc (!android)

View file

@ -0,0 +1,3 @@
Source: webrtc
Version: 20190626
Description: WebRTC

View file

@ -0,0 +1,36 @@
include(vcpkg_common_functions)
set(WEBRTC_VERSION 20190626)
set(MASTER_COPY_SOURCE_PATH ${CURRENT_BUILDTREES_DIR}/src)
if (ANDROID)
# this is handled by hifi_android.py
elseif (WIN32)
vcpkg_download_distfile(
WEBRTC_SOURCE_ARCHIVE
URLS https://hifi-public.s3.amazonaws.com/seth/webrtc-20190626-windows.zip
SHA512 c0848eddb1579b3bb0496b8785e24f30470f3c477145035fd729264a326a467b9467ae9f426aa5d72d168ad9e9bf2c279150744832736bdf39064d24b04de1a3
FILENAME webrtc-20190626-windows.zip
)
elseif (APPLE)
vcpkg_download_distfile(
WEBRTC_SOURCE_ARCHIVE
URLS https://hifi-public.s3.amazonaws.com/seth/webrtc-20190626-osx.tar.gz
SHA512 fc70cec1b5ee87395137b7090f424e2fc2300fc17d744d5ffa1cf7aa0e0f1a069a9d72ba1ad2fb4a640ebeb6c218bda24351ba0083e1ff96c4a4b5032648a9d2
FILENAME webrtc-20190626-osx.tar.gz
)
else ()
# else Linux desktop
vcpkg_download_distfile(
WEBRTC_SOURCE_ARCHIVE
URLS https://hifi-public.s3.amazonaws.com/seth/webrtc-20190626-linux.tar.gz
SHA512 07d7776551aa78cb09a3ef088a8dee7762735c168c243053b262083d90a1d258cec66dc386f6903da5c4461921a3c2db157a1ee106a2b47e7756cb424b66cc43
FILENAME webrtc-20190626-linux.tar.gz
)
endif ()
vcpkg_extract_source_archive(${WEBRTC_SOURCE_ARCHIVE})
file(COPY ${MASTER_COPY_SOURCE_PATH}/webrtc/include DESTINATION ${CURRENT_PACKAGES_DIR})
file(COPY ${MASTER_COPY_SOURCE_PATH}/webrtc/lib DESTINATION ${CURRENT_PACKAGES_DIR})
file(COPY ${MASTER_COPY_SOURCE_PATH}/webrtc/share DESTINATION ${CURRENT_PACKAGES_DIR})
file(COPY ${MASTER_COPY_SOURCE_PATH}/webrtc/debug DESTINATION ${CURRENT_PACKAGES_DIR})

View file

@ -1788,6 +1788,39 @@
"default": false
}
]
},
{
"name": "installed_content",
"label": "Installed Content",
"hidden": true,
"settings": [
{
"name": "filename",
"content_setting": true,
"default": ""
},
{
"name": "name",
"content_setting": true,
"default": ""
},
{
"name": "creation_time",
"content_setting": true,
"default": 0
},
{
"name": "install_time",
"type": "int",
"content_setting": true,
"default": 0
},
{
"name": "installed_by",
"content_setting": true,
"default": ""
}
]
}
]
}

View file

@ -4,8 +4,14 @@ $(document).ready(function(){
var RESTORE_SETTINGS_FILE_ID = 'restore-settings-file';
var UPLOAD_CONTENT_ALLOWED_DIV_ID = 'upload-content-allowed';
var UPLOAD_CONTENT_RECOVERING_DIV_ID = 'upload-content-recovering';
var INSTALLED_CONTENT_FILENAME_ID = 'installed-content-filename';
var INSTALLED_CONTENT_NAME_ID = 'installed-content-name';
var INSTALLED_CONTENT_CREATED_ID = 'installed-content-created';
var INSTALLED_CONTENT_INSTALLED_ID = 'installed-content-installed';
var INSTALLED_CONTENT_INSTALLED_BY_ID = 'installed-content-installed-by';
var isRestoring = false;
var restoreErrorShown = false;
function progressBarHTML(extraClass, label) {
var html = "<div class='progress'>";
@ -64,11 +70,23 @@ $(document).ready(function(){
var ajaxObject = $.ajax(ajaxParams);
ajaxObject.fail(function (jqXHR, textStatus, errorThrown) {
showErrorMessage(
"Error",
"There was a problem restoring domain content.\n"
+ "Please ensure that the content archive or entity file is valid and try again."
);
// status of 0 means the connection was reset, which
// happens after the content is parsed and the server restarts
// in the case of json and json.gz files
if (jqXHR.status != 0) {
showErrorMessage(
"Error",
"There was a problem restoring domain content.\n"
+ "Please ensure that the content archive or entity file is valid and try again."
);
} else {
isRestoring = true;
// immediately reload backup information since one should be restoring now
reloadBackupInformation();
swal.close();
}
});
updateProgressBars($('.upload-content-progress'), (offset + nextChunkSize) * 100 / fileSize);
@ -103,10 +121,25 @@ $(document).ready(function(){
html += "<span class='help-block'>Restore in progress</span>";
html += progressBarHTML('recovery', 'Restoring');
html += "</div></div>";
$('#' + Settings.UPLOAD_CONTENT_BACKUP_PANEL_ID + ' .panel-body').html(html);
}
function setupInstalledContentInfo() {
var html = "<table class='table table-bordered'><tbody>";
html += "<tr class='headers'><td class='data'><strong>Name</strong></td>";
html += "<td class='data'><strong>File Name</strong></td>";
html += "<td class='data'><strong>Created</strong></td>";
html += "<td class='data'><strong>Installed</strong></td>";
html += "<td class='data'><strong>Installed By</strong></td></tr>";
html += "<tr><td class='data' id='" + INSTALLED_CONTENT_NAME_ID + "'/>";
html += "<td class='data' id='" + INSTALLED_CONTENT_FILENAME_ID + "'/>";
html += "<td class='data' id='" + INSTALLED_CONTENT_CREATED_ID + "'/>";
html += "<td class='data' id='" + INSTALLED_CONTENT_INSTALLED_ID + "'/>";
html += "<td class='data' id='" + INSTALLED_CONTENT_INSTALLED_BY_ID + "'/></tr>";
html += "</tbody></table>";
$('#' + Settings.INSTALLED_CONTENT + ' .panel-body').html(html);
}
// handle content archive or entity file upload
// when the selected file is changed, enable the button if there's a selected file
@ -135,6 +168,7 @@ $(document).ready(function(){
var GENERATE_ARCHIVE_BUTTON_ID = 'generate-archive-button';
var CONTENT_ARCHIVES_NORMAL_ID = 'content-archives-success';
var CONTENT_ARCHIVES_CONTENT_INFO_ID = 'content-archives-content-info';
var CONTENT_ARCHIVES_ERROR_ID = 'content-archives-error';
var AUTOMATIC_ARCHIVES_TABLE_ID = 'automatic-archives-table';
var AUTOMATIC_ARCHIVES_TBODY_ID = 'automatic-archives-tbody';
@ -230,13 +264,27 @@ $(document).ready(function(){
url: '/api/backups',
cache: false
}).done(function(data) {
// split the returned data into manual and automatic manual backups
var splitBackups = _.partition(data.backups, function(value, index) {
return value.isManualBackup;
});
if (isRestoring && !data.status.isRecovering) {
if (data.status.recoveryError && !restoreErrorShown) {
restoreErrorShown = true;
swal({
title: "Error",
text: "There was a problem restoring domain content.\n"
+ data.status.recoveryError,
type: "error",
showCancelButton: false,
confirmButtonText: "Restart",
closeOnConfirm: true,
},
function () {
$.get("/restart");
showRestartModal();
});
}
if (isRestoring && !data.status.isRecovering && !data.status.recoveryError) {
// we were recovering and we finished - the DS is going to restart so show the restart modal
showRestartModal();
return;
@ -327,6 +375,12 @@ $(document).ready(function(){
$('#' + UPLOAD_CONTENT_ALLOWED_DIV_ID).toggle(!data.status.isRecovering);
$('#' + UPLOAD_CONTENT_RECOVERING_DIV_ID).toggle(data.status.isRecovering);
$('#' + INSTALLED_CONTENT_NAME_ID).text(data.installed_content.name);
$('#' + INSTALLED_CONTENT_FILENAME_ID).text(data.installed_content.filename);
$('#' + INSTALLED_CONTENT_CREATED_ID).text(data.installed_content.creation_time ? moment(data.installed_content.creation_time).format('lll') : "");
$('#' + INSTALLED_CONTENT_INSTALLED_ID).text(data.installed_content.install_time ? moment(data.installed_content.install_time).format('lll') : "");
$('#' + INSTALLED_CONTENT_INSTALLED_BY_ID).text(data.installed_content.installed_by);
// update the progress bars for current restore status
if (data.status.isRecovering) {
updateProgressBars($('.recovery.progress-bar'), data.status.recoveryProgress * 100);
@ -514,6 +568,7 @@ $(document).ready(function(){
Settings.afterReloadActions = function() {
setupBackupUpload();
setupContentArchives();
setupInstalledContentInfo();
// load the latest backups immediately
reloadBackupInformation();

View file

@ -57,10 +57,14 @@ $(document).ready(function(){
// define extra groups to add to setting panels, with their splice index
Settings.extraContentGroupsAtIndex = {
0: {
html_id: Settings.INSTALLED_CONTENT,
label: 'Installed Content'
},
1: {
html_id: Settings.CONTENT_ARCHIVES_PANEL_ID,
label: 'Content Archives'
},
1: {
2: {
html_id: Settings.UPLOAD_CONTENT_BACKUP_PANEL_ID,
label: 'Upload Content'
}

View file

@ -44,7 +44,8 @@ $.extend(Settings, {
INVALID_ROW_CLASS: 'invalid-input',
DATA_ROW_INDEX: 'data-row-index',
CONTENT_ARCHIVES_PANEL_ID: 'content_archives',
UPLOAD_CONTENT_BACKUP_PANEL_ID: 'upload_content'
UPLOAD_CONTENT_BACKUP_PANEL_ID: 'upload_content',
INSTALLED_CONTENT: 'installed_content'
});
var URLs = {

View file

@ -278,17 +278,19 @@ void AssetsBackupHandler::createBackup(const QString& backupName, QuaZip& zip) {
_backups.emplace_back(backupName, mappings, false);
}
void AssetsBackupHandler::recoverBackup(const QString& backupName, QuaZip& zip) {
std::pair<bool, QString> AssetsBackupHandler::recoverBackup(const QString& backupName, QuaZip& zip, const QString& username, const QString& sourceFilename) {
Q_ASSERT(QThread::currentThread() == thread());
if (operationInProgress()) {
qCWarning(asset_backup) << "There is already a backup/restore in progress.";
return;
QString errorStr ("There is already a backup/restore in progress. Please wait.");
qWarning() << errorStr;
return { false, errorStr };
}
if (_lastMappingsRefresh.time_since_epoch().count() == 0) {
qCWarning(asset_backup) << "Current mappings not yet loaded.";
return;
QString errorStr ("Current mappings not yet loaded. Please wait.");
qWarning() << errorStr;
return { false, errorStr };
}
if ((p_high_resolution_clock::now() - _lastMappingsRefresh) > MAX_REFRESH_TIME) {
@ -301,6 +303,16 @@ void AssetsBackupHandler::recoverBackup(const QString& backupName, QuaZip& zip)
if (it == end(_backups)) {
loadBackup(backupName, zip);
auto emplaced_backup = find_if(begin(_backups), end(_backups), [&](const AssetServerBackup& backup) {
return backup.name == backupName;
});
if(emplaced_backup->corruptedBackup) {
QString errorStr ("Current mappings file is corrupted.");
qWarning() << errorStr;
return { false, errorStr };
}
QuaZipDir zipDir { &zip, ZIP_ASSETS_FOLDER };
auto assetNames = zipDir.entryList(QDir::Files);
@ -330,8 +342,9 @@ void AssetsBackupHandler::recoverBackup(const QString& backupName, QuaZip& zip)
});
if (it == end(_backups)) {
qCCritical(asset_backup) << "Failed to recover backup:" << backupName;
return;
QString errorStr ("Failed to recover backup: " + backupName);
qWarning() << errorStr;
return { false, errorStr };
}
}
@ -339,6 +352,7 @@ void AssetsBackupHandler::recoverBackup(const QString& backupName, QuaZip& zip)
computeServerStateDifference(_currentMappings, newMappings);
restoreAllAssets();
return { true, QString() };
}
void AssetsBackupHandler::deleteBackup(const QString& backupName) {

View file

@ -38,7 +38,7 @@ public:
void loadBackup(const QString& backupName, QuaZip& zip) override;
void loadingComplete() override;
void createBackup(const QString& backupName, QuaZip& zip) override;
void recoverBackup(const QString& backupName, QuaZip& zip) override;
std::pair<bool, QString> recoverBackup(const QString& backupName, QuaZip& zip, const QString& username, const QString& sourceFilename) override;
void deleteBackup(const QString& backupName) override;
void consolidateBackup(const QString& backupName, QuaZip& zip) override;
bool isCorruptedBackup(const QString& backupName) override;

View file

@ -30,7 +30,7 @@ public:
virtual void loadBackup(const QString& backupName, QuaZip& zip) = 0;
virtual void loadingComplete() = 0;
virtual void createBackup(const QString& backupName, QuaZip& zip) = 0;
virtual void recoverBackup(const QString& backupName, QuaZip& zip) = 0;
virtual std::pair<bool, QString> recoverBackup(const QString& backupName, QuaZip& zip, const QString& username, const QString& sourceFilename) = 0;
virtual void deleteBackup(const QString& backupName) = 0;
virtual void consolidateBackup(const QString& backupName, QuaZip& zip) = 0;
virtual bool isCorruptedBackup(const QString& backupName) = 0;

View file

@ -10,6 +10,7 @@
//
#include "ContentSettingsBackupHandler.h"
#include "DomainContentBackupManager.h"
#if !defined(__clang__) && defined(__GNUC__)
#pragma GCC diagnostic push
@ -24,6 +25,7 @@
#pragma GCC diagnostic pop
#endif
static const QString DATETIME_FORMAT { "yyyy-MM-dd_HH-mm-ss" };
ContentSettingsBackupHandler::ContentSettingsBackupHandler(DomainServerSettingsManager& domainServerSettingsManager) :
_settingsManager(domainServerSettingsManager)
@ -41,6 +43,26 @@ void ContentSettingsBackupHandler::createBackup(const QString& backupName, QuaZi
DomainServerSettingsManager::IncludeContentSettings, DomainServerSettingsManager::NoDefaultSettings,
DomainServerSettingsManager::ForBackup
);
QString prefixFormat = "(" + QRegExp::escape(AUTOMATIC_BACKUP_PREFIX) + "|" + QRegExp::escape(MANUAL_BACKUP_PREFIX) + ")";
QString nameFormat = "(.+)";
QString dateTimeFormat = "(" + DATETIME_FORMAT_RE + ")";
QRegExp backupNameFormat { prefixFormat + nameFormat + "-" + dateTimeFormat + "\\.zip" };
QString name{ "" };
QDateTime createdAt;
if (backupNameFormat.exactMatch(backupName)) {
name = backupNameFormat.cap(2);
auto dateTime = backupNameFormat.cap(3);
createdAt = QDateTime::fromString(dateTime, DATETIME_FORMAT);
}
QJsonObject installed_content {
{ INSTALLED_CONTENT_NAME, name},
{ INSTALLED_CONTENT_CREATION_TIME, createdAt.currentMSecsSinceEpoch()}
};
contentSettingsJSON.insert(INSTALLED_CONTENT, installed_content);
// make a QJsonDocument using the object
QJsonDocument contentSettingsDocument { contentSettingsJSON };
@ -62,24 +84,48 @@ void ContentSettingsBackupHandler::createBackup(const QString& backupName, QuaZi
}
}
void ContentSettingsBackupHandler::recoverBackup(const QString& backupName, QuaZip& zip) {
std::pair<bool, QString> ContentSettingsBackupHandler::recoverBackup(const QString& backupName, QuaZip& zip, const QString& username, const QString& sourceFilename) {
if (!zip.setCurrentFile(CONTENT_SETTINGS_BACKUP_FILENAME)) {
qWarning() << "Failed to find" << CONTENT_SETTINGS_BACKUP_FILENAME << "while recovering backup";
return;
QString errorStr("Failed to find " + CONTENT_SETTINGS_BACKUP_FILENAME + " while recovering backup");
qWarning() << errorStr;
return { false, errorStr };
}
QuaZipFile zipFile { &zip };
if (!zipFile.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open" << CONTENT_SETTINGS_BACKUP_FILENAME << "in backup";
return;
QString errorStr("Failed to open " + CONTENT_SETTINGS_BACKUP_FILENAME + " in backup");
qCritical() << errorStr;
return { false, errorStr };
}
auto rawData = zipFile.readAll();
zipFile.close();
QJsonDocument jsonDocument = QJsonDocument::fromJson(rawData);
if (!_settingsManager.restoreSettingsFromObject(jsonDocument.object(), ContentSettings)) {
qCritical() << "Failed to restore settings from" << CONTENT_SETTINGS_BACKUP_FILENAME << "in content archive";
if (zipFile.getZipError() != UNZ_OK) {
QString errorStr("Failed to unzip " + CONTENT_SETTINGS_BACKUP_FILENAME + ": " + zipFile.getZipError());
qCritical() << errorStr;
return { false, errorStr };
}
QJsonDocument jsonDocument = QJsonDocument::fromJson(rawData);
QJsonObject jsonObject = jsonDocument.object();
auto archiveJson = jsonObject.find(INSTALLED_CONTENT)->toObject();
QJsonObject installed_content {
{ INSTALLED_CONTENT_FILENAME, sourceFilename },
{ INSTALLED_CONTENT_NAME, archiveJson[INSTALLED_CONTENT_NAME].toString()},
{ INSTALLED_CONTENT_CREATION_TIME, archiveJson[INSTALLED_CONTENT_CREATION_TIME].toVariant().toLongLong() },
{ INSTALLED_CONTENT_INSTALL_TIME, QDateTime::currentDateTime().currentMSecsSinceEpoch() },
{ INSTALLED_CONTENT_INSTALLED_BY, username }
};
jsonObject.insert(INSTALLED_CONTENT, installed_content);
if (!_settingsManager.restoreSettingsFromObject(jsonObject, ContentSettings)) {
QString errorStr("Failed to restore settings from " + CONTENT_SETTINGS_BACKUP_FILENAME + " in content archive");
qCritical() << errorStr;
return { false, errorStr };
}
return { true, QString() };
}

View file

@ -28,7 +28,7 @@ public:
void createBackup(const QString& backupName, QuaZip& zip) override;
void recoverBackup(const QString& backupName, QuaZip& zip) override;
std::pair<bool, QString> recoverBackup(const QString& backupName, QuaZip& zip, const QString& username, const QString& sourceFilename) override;
void deleteBackup(const QString& backupName) override {}

View file

@ -42,9 +42,7 @@ const std::chrono::seconds DomainContentBackupManager::DEFAULT_PERSIST_INTERVAL
// Backup format looks like: daily_backup-TIMESTAMP.zip
static const QString DATETIME_FORMAT { "yyyy-MM-dd_HH-mm-ss" };
static const QString DATETIME_FORMAT_RE { "\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}-\\d{2}" };
static const QString AUTOMATIC_BACKUP_PREFIX { "autobackup-" };
static const QString MANUAL_BACKUP_PREFIX { "backup-" };
static const QString PRE_UPLOAD_SUFFIX{ "pre_upload" };
static const QString MANUAL_BACKUP_NAME_RE { "[a-zA-Z0-9\\-_ ]+" };
void DomainContentBackupManager::addBackupHandler(BackupHandlerPointer handler) {
@ -52,9 +50,10 @@ void DomainContentBackupManager::addBackupHandler(BackupHandlerPointer handler)
}
DomainContentBackupManager::DomainContentBackupManager(const QString& backupDirectory,
const QVariantList& backupRules,
DomainServerSettingsManager& domainServerSettingsManager,
std::chrono::milliseconds persistInterval,
bool debugTimestampNow) :
_settingsManager(domainServerSettingsManager),
_consolidatedBackupDirectory(PathUtils::generateTemporaryDir()),
_backupDirectory(backupDirectory), _persistInterval(persistInterval), _lastCheck(p_high_resolution_clock::now())
{
@ -63,7 +62,8 @@ DomainContentBackupManager::DomainContentBackupManager(const QString& backupDire
// Make sure the backup directory exists.
QDir(_backupDirectory).mkpath(".");
parseBackupRules(backupRules);
static const QString BACKUP_RULES_KEYPATH = AUTOMATIC_CONTENT_ARCHIVES_GROUP + ".backup_rules";
parseBackupRules(_settingsManager.valueOrDefaultValueForKeyPath(BACKUP_RULES_KEYPATH).toList());
constexpr int CONSOLIDATED_BACKUP_CLEANER_INTERVAL_MSECS = 30 * 1000;
_consolidatedBackupCleanupTimer.setInterval(CONSOLIDATED_BACKUP_CLEANER_INTERVAL_MSECS);
@ -170,7 +170,9 @@ bool DomainContentBackupManager::process() {
return handler->getRecoveryStatus().first;
});
if (!isStillRecovering) {
// if an error occurred, don't restart the server so that the user
// can be notified of the error and take action.
if (!isStillRecovering && _recoveryError.isEmpty()) {
_isRecovering = false;
_recoveryFilename = "";
emit recoveryCompleted();
@ -277,7 +279,7 @@ void DomainContentBackupManager::deleteBackup(MiniPromise::Promise promise, cons
});
}
bool DomainContentBackupManager::recoverFromBackupZip(const QString& backupName, QuaZip& zip) {
bool DomainContentBackupManager::recoverFromBackupZip(const QString& backupName, QuaZip& zip, const QString& username, const QString& sourceFilename, bool rollingBack) {
if (!zip.open(QuaZip::Mode::mdUnzip)) {
qWarning() << "Failed to unzip file: " << backupName;
return false;
@ -286,7 +288,15 @@ bool DomainContentBackupManager::recoverFromBackupZip(const QString& backupName,
_recoveryFilename = backupName;
for (auto& handler : _backupHandlers) {
handler->recoverBackup(backupName, zip);
bool success;
QString errorStr;
std::tie(success, errorStr) = handler->recoverBackup(backupName, zip, username, sourceFilename);
if (!success) {
if (!rollingBack) {
_recoveryError = errorStr;
}
return false;
}
}
qDebug() << "Successfully started recovering from " << backupName;
@ -294,7 +304,7 @@ bool DomainContentBackupManager::recoverFromBackupZip(const QString& backupName,
}
}
void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise, const QString& backupName) {
void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise, const QString& backupName, const QString& username) {
if (_isRecovering) {
promise->resolve({
{ "success", false }
@ -304,12 +314,12 @@ void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise,
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "recoverFromBackup", Q_ARG(MiniPromise::Promise, promise),
Q_ARG(const QString&, backupName));
Q_ARG(const QString&, backupName), Q_ARG(const QString&, username));
return;
}
qDebug() << "Recovering from" << backupName;
_recoveryError = "";
bool success { false };
QDir backupDir { _backupDirectory };
auto backupFilePath { backupDir.filePath(backupName) };
@ -317,7 +327,7 @@ void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise,
if (backupFile.open(QIODevice::ReadOnly)) {
QuaZip zip { &backupFile };
success = recoverFromBackupZip(backupName, zip);
success = recoverFromBackupZip(backupName, zip, username, backupName);
backupFile.close();
} else {
@ -330,11 +340,11 @@ void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise,
});
}
void DomainContentBackupManager::recoverFromUploadedBackup(MiniPromise::Promise promise, QByteArray uploadedBackup) {
void DomainContentBackupManager::recoverFromUploadedBackup(MiniPromise::Promise promise, QByteArray uploadedBackup, QString username) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "recoverFromUploadedBackup", Q_ARG(MiniPromise::Promise, promise),
Q_ARG(QByteArray, uploadedBackup));
Q_ARG(QByteArray, uploadedBackup), Q_ARG(QString, username));
return;
}
@ -345,29 +355,51 @@ void DomainContentBackupManager::recoverFromUploadedBackup(MiniPromise::Promise
QuaZip uploadedZip { &uploadedBackupBuffer };
QString backupName = MANUAL_BACKUP_PREFIX + "uploaded.zip";
bool success = recoverFromBackupZip(backupName, uploadedZip);
bool success = recoverFromBackupZip(backupName, uploadedZip, username, QString());
promise->resolve({
{ "success", success }
});
}
void DomainContentBackupManager::recoverFromUploadedFile(MiniPromise::Promise promise, QString uploadedFilename) {
void DomainContentBackupManager::recoverFromUploadedFile(MiniPromise::Promise promise, QString uploadedFilename, const QString username, QString sourceFilename) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "recoverFromUploadedFile", Q_ARG(MiniPromise::Promise, promise),
Q_ARG(QString, uploadedFilename));
Q_ARG(QString, uploadedFilename), Q_ARG(QString, username), Q_ARG(QString, sourceFilename));
return;
}
qDebug() << "Recovering from uploaded file -" << uploadedFilename;
qDebug() << "Recovering from uploaded file -" << uploadedFilename << "source" << sourceFilename;
bool success;
QString path;
std::tie(success, path) = createBackup(AUTOMATIC_BACKUP_PREFIX, PRE_UPLOAD_SUFFIX);
if(!success) {
_recoveryError = "Failed to create backup for " + PRE_UPLOAD_SUFFIX + " at " + path;
qCWarning(domain_server) << _recoveryError;
} else {
QFile uploadedFile(uploadedFilename);
QuaZip uploadedZip { &uploadedFile };
QFile uploadedFile(uploadedFilename);
QuaZip uploadedZip { &uploadedFile };
QString backupName = MANUAL_BACKUP_PREFIX + "uploaded.zip";
QString backupName = MANUAL_BACKUP_PREFIX + "uploaded.zip";
bool success = recoverFromBackupZip(backupName, uploadedZip, username, sourceFilename);
bool success = recoverFromBackupZip(backupName, uploadedZip);
if (!success) {
// attempt to rollback to
QString filename;
QDateTime filetime;
if (getMostRecentBackup(PRE_UPLOAD_SUFFIX, filename, filetime)) {
QFile uploadedFile(uploadedFilename);
QuaZip uploadedZip { &uploadedFile };
QString backupName = MANUAL_BACKUP_PREFIX + "uploaded.zip";
recoverFromBackupZip(backupName, uploadedZip, username, sourceFilename, true);
}
}
}
promise->resolve({
{ "success", success }
});
@ -455,9 +487,44 @@ void DomainContentBackupManager::getAllBackupsAndStatus(MiniPromise::Promise pro
{ "recoveryProgress", recoveryProgress }
};
if(!_recoveryError.isEmpty()) {
status["recoveryError"] = _recoveryError;
}
QString filename = _settingsManager.valueForKeyPath(CONTENT_SETTINGS_INSTALLED_CONTENT_FILENAME).toString();
QString name = _settingsManager.valueForKeyPath(CONTENT_SETTINGS_INSTALLED_CONTENT_NAME).toString();
auto creationTime = _settingsManager.valueForKeyPath(CONTENT_SETTINGS_INSTALLED_CONTENT_CREATION_TIME).toULongLong();
if (name.isEmpty() || creationTime == 0) {
QString prefixFormat = "(" + QRegExp::escape(AUTOMATIC_BACKUP_PREFIX) + "|" + QRegExp::escape(MANUAL_BACKUP_PREFIX) + ")";
QString nameFormat = "(.+)";
QString dateTimeFormat = "(" + DATETIME_FORMAT_RE + ")";
QRegExp backupNameFormat { prefixFormat + nameFormat + "-" + dateTimeFormat + "\\.zip" };
if (backupNameFormat.exactMatch(filename)) {
if (name.isEmpty()) {
name = backupNameFormat.cap(2);
}
if (creationTime == 0) {
auto dateTime = backupNameFormat.cap(3);
creationTime = QDateTime::fromString(dateTime, DATETIME_FORMAT).toMSecsSinceEpoch();
}
}
}
QVariantMap currentArchive;
currentArchive["filename"] = filename;
currentArchive["name"] = name;
currentArchive["creation_time"] = creationTime;
currentArchive["install_time"] = _settingsManager.valueForKeyPath(CONTENT_SETTINGS_INSTALLED_CONTENT_INSTALL_TIME).toULongLong();
currentArchive["installed_by"] = _settingsManager.valueForKeyPath(CONTENT_SETTINGS_INSTALLED_CONTENT_INSTALLED_BY).toString();
QVariantMap info {
{ "backups", variantBackups },
{ "status", status }
{ "status", status },
{ "installed_content", currentArchive }
};
promise->resolve(info);

View file

@ -28,11 +28,22 @@
#include <GenericThread.h>
#include "BackupHandler.h"
#include "DomainServerSettingsManager.h"
#include <shared/MiniPromises.h>
#include <PortableHighResolutionClock.h>
const QString DATETIME_FORMAT_RE { "\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}-\\d{2}" };
const QString AUTOMATIC_BACKUP_PREFIX { "autobackup-" };
const QString MANUAL_BACKUP_PREFIX { "backup-" };
const QString INSTALLED_CONTENT = "installed_content";
const QString INSTALLED_CONTENT_FILENAME = "filename";
const QString INSTALLED_CONTENT_NAME = "name";
const QString INSTALLED_CONTENT_CREATION_TIME = "creation_time";
const QString INSTALLED_CONTENT_INSTALL_TIME = "install_time";
const QString INSTALLED_CONTENT_INSTALLED_BY = "installed_by";
struct BackupItemInfo {
BackupItemInfo(QString pId, QString pName, QString pAbsolutePath, QDateTime pCreatedAt, bool pIsManualBackup) :
id(pId), name(pName), absolutePath(pAbsolutePath), createdAt(pCreatedAt), isManualBackup(pIsManualBackup) { };
@ -71,7 +82,7 @@ public:
static const std::chrono::seconds DEFAULT_PERSIST_INTERVAL;
DomainContentBackupManager(const QString& rootBackupDirectory,
const QVariantList& settings,
DomainServerSettingsManager& domainServerSettingsManager,
std::chrono::milliseconds persistInterval = DEFAULT_PERSIST_INTERVAL,
bool debugTimestampNow = false);
@ -84,9 +95,9 @@ public:
public slots:
void getAllBackupsAndStatus(MiniPromise::Promise promise);
void createManualBackup(MiniPromise::Promise promise, const QString& name);
void recoverFromBackup(MiniPromise::Promise promise, const QString& backupName);
void recoverFromUploadedBackup(MiniPromise::Promise promise, QByteArray uploadedBackup);
void recoverFromUploadedFile(MiniPromise::Promise promise, QString uploadedFilename);
void recoverFromBackup(MiniPromise::Promise promise, const QString& backupName, const QString& username);
void recoverFromUploadedBackup(MiniPromise::Promise promise, QByteArray uploadedBackup, QString username);
void recoverFromUploadedFile(MiniPromise::Promise promise, QString uploadedFilename, QString username, QString sourceFilename);
void deleteBackup(MiniPromise::Promise promise, const QString& backupName);
signals:
@ -108,13 +119,15 @@ protected:
std::pair<bool, QString> createBackup(const QString& prefix, const QString& name);
bool recoverFromBackupZip(const QString& backupName, QuaZip& backupZip);
bool recoverFromBackupZip(const QString& backupName, QuaZip& backupZip, const QString& username, const QString& sourceFilename, bool rollingBack = false);
private slots:
void removeOldConsolidatedBackups();
void consolidateBackupInternal(QString fileName);
private:
DomainServerSettingsManager& _settingsManager;
QTimer _consolidatedBackupCleanupTimer;
const QString _consolidatedBackupDirectory;
@ -126,6 +139,7 @@ private:
std::unordered_map<QString, ConsolidatedBackupInfo> _consolidatedBackups;
std::atomic<bool> _isRecovering { false };
QString _recoveryError;
QString _recoveryFilename { };
p_high_resolution_clock::time_point _lastCheck;

View file

@ -307,11 +307,7 @@ DomainServer::DomainServer(int argc, char* argv[]) :
}
maybeHandleReplacementEntityFile();
static const QString BACKUP_RULES_KEYPATH = AUTOMATIC_CONTENT_ARCHIVES_GROUP + ".backup_rules";
auto backupRulesVariant = _settingsManager.valueOrDefaultValueForKeyPath(BACKUP_RULES_KEYPATH);
_contentManager.reset(new DomainContentBackupManager(getContentBackupDir(), backupRulesVariant.toList()));
_contentManager.reset(new DomainContentBackupManager(getContentBackupDir(), _settingsManager));
connect(_contentManager.get(), &DomainContentBackupManager::started, _contentManager.get(), [this](){
_contentManager->addBackupHandler(BackupHandlerPointer(new EntitiesBackupHandler(getEntitiesFilePath(), getEntitiesReplacementFilePath())));
@ -1961,6 +1957,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
QPointer<HTTPConnection> connectionPtr { connection };
auto nodeList = DependencyManager::get<LimitedNodeList>();
QString username;
auto getSetting = [this](QString keyPath, QVariant& value) -> bool {
@ -2028,7 +2025,9 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
}
// all requests below require a cookie to prove authentication so check that first
if (!isAuthenticatedRequest(connection, url)) {
bool isAuthenticated { false };
std::tie(isAuthenticated, username) = isAuthenticatedRequest(connection);
if (!isAuthenticated) {
// this is not an authenticated request
// return true from the handler since it was handled with a 401 or re-direct to auth
return true;
@ -2194,7 +2193,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
return true;
} else if (url.path() == URI_RESTART) {
connection->respond(HTTPConnection::StatusCode200);
connection->respond(HTTPConnection::StatusCode204);
restart();
return true;
} else if (url.path() == URI_API_METAVERSE_INFO) {
@ -2333,8 +2332,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
QJsonObject rootJSON;
auto success = result["success"].toBool();
rootJSON["success"] = success;
QJsonDocument docJSON(rootJSON);
QJsonDocument docJSON(QJsonObject::fromVariantMap(result));
connectionPtr->respond(success ? HTTPConnection::StatusCode200 : HTTPConnection::StatusCode400, docJSON.toJson(),
JSON_MIME_TYPE.toUtf8());
});
@ -2362,12 +2360,11 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
QJsonObject rootJSON;
auto success = result["success"].toBool();
rootJSON["success"] = success;
QJsonDocument docJSON(rootJSON);
QJsonDocument docJSON(QJsonObject::fromVariantMap(result));
connectionPtr->respond(success ? HTTPConnection::StatusCode200 : HTTPConnection::StatusCode400, docJSON.toJson(),
JSON_MIME_TYPE.toUtf8());
});
_contentManager->recoverFromBackup(deferred, id);
_contentManager->recoverFromBackup(deferred, id, username);
return true;
}
} else if (connection->requestOperation() == QNetworkAccessManager::PutOperation) {
@ -2467,8 +2464,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
QJsonObject rootJSON;
auto success = result["success"].toBool();
rootJSON["success"] = success;
QJsonDocument docJSON(rootJSON);
QJsonDocument docJSON(QJsonObject::fromVariantMap(result));
connectionPtr->respond(success ? HTTPConnection::StatusCode200 : HTTPConnection::StatusCode400, docJSON.toJson(),
JSON_MIME_TYPE.toUtf8());
});
@ -2564,6 +2560,9 @@ bool DomainServer::processPendingContent(HTTPConnection* connection, QString ite
int sessionId = sessionIdBytes.toInt();
bool newUpload = itemName == "restore-file" || itemName == "restore-file-chunk-initial" || itemName == "restore-file-chunk-only";
bool isAuthenticated;
QString username;
std::tie(isAuthenticated, username) = isAuthenticatedRequest(connection);
if (filename.endsWith(".zip", Qt::CaseInsensitive)) {
static const QString TEMPORARY_CONTENT_FILEPATH { QDir::tempPath() + "/hifiUploadContent_XXXXXX.zip" };
@ -2590,7 +2589,7 @@ bool DomainServer::processPendingContent(HTTPConnection* connection, QString ite
_pendingFileContent.close();
// Respond immediately - will timeout if we wait for restore.
connection->respond(HTTPConnection::StatusCode200);
connection->respond(HTTPConnection::StatusCode204);
if (itemName == "restore-file" || itemName == "restore-file-chunk-final" || itemName == "restore-file-chunk-only") {
auto deferred = makePromise("recoverFromUploadedBackup");
@ -2598,7 +2597,7 @@ bool DomainServer::processPendingContent(HTTPConnection* connection, QString ite
_pendingContentFiles.erase(sessionId);
});
_contentManager->recoverFromUploadedFile(deferred, _pendingFileContent.fileName());
_contentManager->recoverFromUploadedFile(deferred, _pendingFileContent.fileName(), username, filename);
}
} else if (filename.endsWith(".json", Qt::CaseInsensitive)
|| filename.endsWith(".json.gz", Qt::CaseInsensitive)) {
@ -2608,14 +2607,16 @@ bool DomainServer::processPendingContent(HTTPConnection* connection, QString ite
}
QByteArray& _pendingUploadedContent = _pendingUploadedContents[sessionId];
_pendingUploadedContent += dataChunk;
connection->respond(HTTPConnection::StatusCode200);
if (itemName == "restore-file" || itemName == "restore-file-chunk-final" || itemName == "restore-file-chunk-only") {
// invoke our method to hand the new octree file off to the octree server
QMetaObject::invokeMethod(this, "handleOctreeFileReplacement",
Qt::QueuedConnection, Q_ARG(QByteArray, _pendingUploadedContent));
if (!handleOctreeFileReplacement(_pendingUploadedContent, filename, QString(), username)) {
connection->respond(HTTPConnection::StatusCode400);
return false;
}
_pendingUploadedContents.erase(sessionId);
}
connection->respond(HTTPConnection::StatusCode204);
} else {
connection->respond(HTTPConnection::StatusCode400);
return false;
@ -2685,13 +2686,14 @@ void DomainServer::profileRequestFinished() {
}
}
bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl& url) {
std::pair<bool, QString> DomainServer::isAuthenticatedRequest(HTTPConnection* connection) {
const QByteArray HTTP_COOKIE_HEADER_KEY = "Cookie";
const QString ADMIN_USERS_CONFIG_KEY = "admin-users";
const QString ADMIN_ROLES_CONFIG_KEY = "admin-roles";
const QString BASIC_AUTH_USERNAME_KEY_PATH = "security.http_username";
const QString BASIC_AUTH_PASSWORD_KEY_PATH = "security.http_password";
static const QByteArray HTTP_COOKIE_HEADER_KEY = "Cookie";
static const QString ADMIN_USERS_CONFIG_KEY = "admin-users";
static const QString ADMIN_ROLES_CONFIG_KEY = "admin-roles";
static const QString BASIC_AUTH_USERNAME_KEY_PATH = "security.http_username";
static const QString BASIC_AUTH_PASSWORD_KEY_PATH = "security.http_password";
const QString COOKIE_UUID_REGEX_STRING = HIFI_SESSION_COOKIE_KEY + "=([\\d\\w-]+)($|;)";
const QByteArray UNAUTHENTICATED_BODY = "You do not have permission to access this domain-server.";
@ -2702,7 +2704,6 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
&& (adminUsersVariant.isValid() || adminRolesVariant.isValid())) {
QString cookieString = connection->requestHeader(HTTP_COOKIE_HEADER_KEY);
const QString COOKIE_UUID_REGEX_STRING = HIFI_SESSION_COOKIE_KEY + "=([\\d\\w-]+)($|;)";
QRegExp cookieUUIDRegex(COOKIE_UUID_REGEX_STRING);
QUuid cookieUUID;
@ -2722,7 +2723,7 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
if (_settingsManager.valueForKeyPath(ADMIN_USERS_CONFIG_KEY).toStringList().contains(profileUsername)) {
// this is an authenticated user
return true;
return { true, profileUsername };
}
// loop the roles of this user and see if they are in the admin-roles array
@ -2732,7 +2733,7 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
foreach(const QString& userRole, sessionData.getRoles()) {
if (adminRolesArray.contains(userRole)) {
// this user has a role that allows them to administer the domain-server
return true;
return { true, profileUsername };
}
}
}
@ -2740,7 +2741,7 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
connection->respond(HTTPConnection::StatusCode401, UNAUTHENTICATED_BODY);
// the user does not have allowed username or role, return 401
return false;
return { false, QString() };
} else {
static const QByteArray REQUESTED_WITH_HEADER = "X-Requested-With";
static const QString XML_REQUESTED_WITH = "XMLHttpRequest";
@ -2769,7 +2770,7 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
}
// we don't know about this user yet, so they are not yet authenticated
return false;
return { false, QString() };
}
} else if (_settingsManager.valueForKeyPath(BASIC_AUTH_USERNAME_KEY_PATH).isValid()) {
// config file contains username and password combinations for basic auth
@ -2798,7 +2799,7 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
"" : QCryptographicHash::hash(headerPassword.toUtf8(), QCryptographicHash::Sha256).toHex();
if (settingsUsername == headerUsername && hexHeaderPassword == settingsPassword) {
return true;
return { true, headerUsername };
}
}
}
@ -2820,11 +2821,11 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
HTTPConnection::DefaultContentType, basicAuthHeader);
// not authenticated, bubble up false
return false;
return { false, QString() };
} else {
// we don't have an OAuth URL + admin roles/usernames, so all users are authenticated
return true;
return { true, QString() };
}
}
@ -3498,7 +3499,7 @@ void DomainServer::maybeHandleReplacementEntityFile() {
}
}
void DomainServer::handleOctreeFileReplacement(QByteArray octreeFile) {
bool DomainServer::handleOctreeFileReplacement(QByteArray octreeFile, QString sourceFilename, QString name, QString username) {
OctreeUtils::RawEntityData data;
if (data.readOctreeDataInfoFromData(octreeFile)) {
data.resetIdAndVersion();
@ -3514,19 +3515,41 @@ void DomainServer::handleOctreeFileReplacement(QByteArray octreeFile) {
// process it when it comes back up
qInfo() << "Wrote octree replacement file to" << replacementFilePath << "- stopping server";
QJsonObject installed_content {
{ INSTALLED_CONTENT_FILENAME, sourceFilename },
{ INSTALLED_CONTENT_NAME, name },
{ INSTALLED_CONTENT_CREATION_TIME, 0 },
{ INSTALLED_CONTENT_INSTALL_TIME, QDateTime::currentDateTime().currentMSecsSinceEpoch() },
{ INSTALLED_CONTENT_INSTALLED_BY, username }
};
QJsonObject jsonObject { { INSTALLED_CONTENT, installed_content } };
_settingsManager.recurseJSONObjectAndOverwriteSettings(jsonObject, ContentSettings);
QMetaObject::invokeMethod(this, "restart", Qt::QueuedConnection);
return true;
} else {
qWarning() << "Could not write replacement octree data to file - refusing to process";
return false;
}
} else {
qDebug() << "Received replacement octree file that is invalid - refusing to process";
return false;
}
}
static const QString CONTENT_SET_NAME_QUERY_PARAM = "name";
void DomainServer::handleDomainContentReplacementFromURLRequest(QSharedPointer<ReceivedMessage> message) {
qInfo() << "Received request to replace content from a url";
auto node = DependencyManager::get<LimitedNodeList>()->findNodeWithAddr(message->getSenderSockAddr());
if (node && node->getCanReplaceContent()) {
DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(node->getLinkedData());
QString username;
if (nodeData) {
username = nodeData->getUsername();
}
// Convert message data into our URL
QString url(message->getMessage());
QUrl modelsURL = QUrl(url, QUrl::StrictMode);
@ -3534,16 +3557,19 @@ void DomainServer::handleDomainContentReplacementFromURLRequest(QSharedPointer<R
QNetworkRequest request(modelsURL);
QNetworkReply* reply = networkAccessManager.get(request);
qDebug() << "Downloading JSON from: " << modelsURL;
qDebug() << "Downloading JSON from: " << modelsURL.toString(QUrl::FullyEncoded);
connect(reply, &QNetworkReply::finished, [this, reply, modelsURL]() {
connect(reply, &QNetworkReply::finished, [this, reply, modelsURL, username]() {
QNetworkReply::NetworkError networkError = reply->error();
if (networkError == QNetworkReply::NoError) {
if (modelsURL.fileName().endsWith(".json.gz")) {
handleOctreeFileReplacement(reply->readAll());
QUrlQuery urlQuery(modelsURL.query(QUrl::FullyEncoded));
QString itemName = urlQuery.queryItemValue(CONTENT_SET_NAME_QUERY_PARAM);
handleOctreeFileReplacement(reply->readAll(), modelsURL.fileName(), itemName, username);
} else if (modelsURL.fileName().endsWith(".zip")) {
auto deferred = makePromise("recoverFromUploadedBackup");
_contentManager->recoverFromUploadedBackup(deferred, reply->readAll());
_contentManager->recoverFromUploadedBackup(deferred, reply->readAll(), username);
}
} else {
qDebug() << "Error downloading JSON from specified file: " << modelsURL;
@ -3554,7 +3580,12 @@ void DomainServer::handleDomainContentReplacementFromURLRequest(QSharedPointer<R
void DomainServer::handleOctreeFileReplacementRequest(QSharedPointer<ReceivedMessage> message) {
auto node = DependencyManager::get<NodeList>()->nodeWithLocalID(message->getSourceID());
if (node->getCanReplaceContent()) {
handleOctreeFileReplacement(message->readAll());
if (node && node->getCanReplaceContent()) {
QString username;
DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(node->getLinkedData());
if (nodeData) {
username = nodeData->getUsername();
}
handleOctreeFileReplacement(message->readAll(), QString(), QString(), username);
}
}

View file

@ -99,7 +99,7 @@ private slots:
void handleDomainContentReplacementFromURLRequest(QSharedPointer<ReceivedMessage> message);
void handleOctreeFileReplacementRequest(QSharedPointer<ReceivedMessage> message);
void handleOctreeFileReplacement(QByteArray octreeFile);
bool handleOctreeFileReplacement(QByteArray octreeFile, QString sourceFilename, QString name, QString username);
void processOctreeDataRequestMessage(QSharedPointer<ReceivedMessage> message);
void processOctreeDataPersistMessage(QSharedPointer<ReceivedMessage> message);
@ -194,7 +194,7 @@ private:
QUrl oauthRedirectURL();
QUrl oauthAuthorizationURL(const QUuid& stateUUID = QUuid::createUuid());
bool isAuthenticatedRequest(HTTPConnection* connection, const QUrl& url);
std::pair<bool, QString> isAuthenticatedRequest(HTTPConnection* connection);
QNetworkReply* profileRequestGivenTokenReply(QNetworkReply* tokenReply);
Headers setupCookieHeadersFromProfileReply(QNetworkReply* profileReply);

View file

@ -35,6 +35,11 @@ const QString MACHINE_FINGERPRINT_PERMISSIONS_KEYPATH = "security.machine_finger
const QString GROUP_PERMISSIONS_KEYPATH = "security.group_permissions";
const QString GROUP_FORBIDDENS_KEYPATH = "security.group_forbiddens";
const QString AUTOMATIC_CONTENT_ARCHIVES_GROUP = "automatic_content_archives";
const QString CONTENT_SETTINGS_INSTALLED_CONTENT_FILENAME = "installed_content.filename";
const QString CONTENT_SETTINGS_INSTALLED_CONTENT_NAME = "installed_content.name";
const QString CONTENT_SETTINGS_INSTALLED_CONTENT_CREATION_TIME = "installed_content.creation_time";
const QString CONTENT_SETTINGS_INSTALLED_CONTENT_INSTALL_TIME = "installed_content.install_time";
const QString CONTENT_SETTINGS_INSTALLED_CONTENT_INSTALLED_BY = "installed_content.installed_by";
using GroupByUUIDKey = QPair<QUuid, QUuid>; // groupID, rankID

View file

@ -57,36 +57,41 @@ void EntitiesBackupHandler::createBackup(const QString& backupName, QuaZip& zip)
}
}
void EntitiesBackupHandler::recoverBackup(const QString& backupName, QuaZip& zip) {
std::pair<bool, QString> EntitiesBackupHandler::recoverBackup(const QString& backupName, QuaZip& zip, const QString& username, const QString& sourceFilename) {
if (!zip.setCurrentFile(ENTITIES_BACKUP_FILENAME)) {
qWarning() << "Failed to find" << ENTITIES_BACKUP_FILENAME << "while recovering backup";
return;
QString errorStr("Failed to find " + ENTITIES_BACKUP_FILENAME + " while recovering backup");
qWarning() << errorStr;
return { false, errorStr };
}
QuaZipFile zipFile { &zip };
if (!zipFile.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open" << ENTITIES_BACKUP_FILENAME << "in backup";
return;
QString errorStr("Failed to open " + ENTITIES_BACKUP_FILENAME + " in backup");
qCritical() << errorStr;
return { false, errorStr };
}
auto rawData = zipFile.readAll();
zipFile.close();
if (zipFile.getZipError() != UNZ_OK) {
QString errorStr("Failed to unzip " + ENTITIES_BACKUP_FILENAME + ": " + zipFile.getZipError());
qCritical() << errorStr;
return { false, errorStr };
}
OctreeUtils::RawEntityData data;
if (!data.readOctreeDataInfoFromData(rawData)) {
qCritical() << "Unable to parse octree data during backup recovery";
return;
QString errorStr("Unable to parse octree data during backup recovery");
qCritical() << errorStr;
return { false, errorStr };
}
data.resetIdAndVersion();
if (zipFile.getZipError() != UNZ_OK) {
qCritical().nospace() << "Failed to unzip " << ENTITIES_BACKUP_FILENAME << ": " << zipFile.getZipError();
return;
}
QFile entitiesFile { _entitiesReplacementFilePath };
if (entitiesFile.open(QIODevice::WriteOnly)) {
entitiesFile.write(data.toGzippedByteArray());
}
return { true, QString() };
}

View file

@ -29,7 +29,7 @@ public:
void createBackup(const QString& backupName, QuaZip& zip) override;
// Recover from a full backup
void recoverBackup(const QString& backupName, QuaZip& zip) override;
std::pair<bool, QString> recoverBackup(const QString& backupName, QuaZip& zip, const QString& username, const QString& sourceFilename) override;
// Delete a skeleton backup
void deleteBackup(const QString& backupName) override {}

View file

@ -94,6 +94,10 @@ ANDROID_PACKAGES = {
'checksum': 'ddcb23df336b08017042ba4786db1d9e',
'sharedLibFolder': 'lib',
'includeLibs': {'libbreakpad_client.a'}
},
'webrtc': {
'file': 'webrtc-20190626-android.tar.gz',
'checksum': 'e2dccd3d8efdcba6d428c87ba7fb2a53'
}
}

View file

@ -142,22 +142,6 @@ endif()
if (APPLE)
add_executable(${TARGET_NAME} MACOSX_BUNDLE ${INTERFACE_SRCS} ${QM})
set_from_env(XCODE_DEVELOPMENT_TEAM XCODE_DEVELOPMENT_TEAM "")
if ("${XCODE_DEVELOPMENT_TEAM}" STREQUAL "")
message(WARNING "XCODE_DEVELOPMENT_TEAM environment variable is not set. Not signing build.")
else()
set_target_properties(${TARGET_NAME} PROPERTIES
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "${CMAKE_CURRENT_SOURCE_DIR}/interface.entitlements"
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Developer ID Application"
XCODE_ATTRIBUTE_CODE_SIGN_INJECT_BASE_ENTITLEMENTS NO
XCODE_ATTRIBUTE_CODE_SIGN_STYLE "Manual"
XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "${XCODE_DEVELOPMENT_TEAM}"
XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME YES
XCODE_ATTRIBUTE_OTHER_CODE_SIGN_FLAGS "--timestamp --deep"
XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER ""
)
endif()
# make sure the output name for the .app bundle is correct
# Fix up the rpath so macdeployqt works
set_target_properties(${TARGET_NAME} PROPERTIES INSTALL_RPATH "@executable_path/../Frameworks")

View file

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>high-fidelity.hifi</string>
</array>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>

Binary file not shown.

File diff suppressed because it is too large Load diff

View file

@ -33,33 +33,6 @@ var EventBridge;
// replace the TempEventBridge with the real one.
var tempEventBridge = EventBridge;
EventBridge = channel.objects.eventBridge;
EventBridge.audioOutputDeviceChanged.connect(function(deviceName) {
navigator.mediaDevices.getUserMedia({ audio: true, video: false }).then(function(mediaStream) {
navigator.mediaDevices.enumerateDevices().then(function(devices) {
devices.forEach(function(device) {
if (device.kind == "audiooutput") {
if (device.label == deviceName){
console.log("Changing HTML audio output to device " + device.label);
var deviceId = device.deviceId;
var videos = document.getElementsByTagName("video");
for (var i = 0; i < videos.length; i++){
videos[i].setSinkId(deviceId);
}
var audios = document.getElementsByTagName("audio");
for (var i = 0; i < audios.length; i++){
audios[i].setSinkId(deviceId);
}
}
}
});
}).catch(function(err) {
console.log("Error getting media devices"+ err.name + ": " + err.message);
});
}).catch(function(err) {
console.log("Error getting user media"+ err.name + ": " + err.message);
});
});
// To be able to update the state of the output device selection for every element added to the DOM
// we need to listen to events that might precede the addition of this elements.

View file

@ -14,7 +14,6 @@ Item {
HifiStyles.HifiConstants { id: hifistyles }
height: 600
property variant permissionsBar: {'securityOrigin':'none','feature':'none'}
property alias url: webview.url
property bool canGoBack: webview.canGoBack
@ -30,6 +29,10 @@ Item {
webview.profile = profile;
}
onUrlChanged: {
permissionPopupBackground.visible = false;
}
WebEngineView {
id: webview
objectName: "webEngineView"
@ -84,7 +87,14 @@ Item {
}
onFeaturePermissionRequested: {
grantFeaturePermission(securityOrigin, feature, false);
if (permissionPopupBackground.visible === true) {
console.log("Browser engine requested a new permission, but user is already being presented with a different permission request. Aborting request for new permission...");
return;
}
permissionPopupBackground.securityOrigin = securityOrigin;
permissionPopupBackground.feature = feature;
permissionPopupBackground.visible = true;
}
onLoadingChanged: {
@ -122,4 +132,11 @@ Item {
break;
}
}
HifiControls.PermissionPopupBackground {
id: permissionPopupBackground
onSendPermission: {
webview.grantFeaturePermission(securityOrigin, feature, shouldGivePermission);
}
}
}

View file

@ -5,4 +5,6 @@ Text {
style: Text.Outline;
styleColor: "black";
font.pixelSize: 12;
font.bold: true;
font.family: "monospace";
}

View file

@ -5,6 +5,7 @@ import QtWebChannel 1.0
import QtQuick.Controls 2.2
import stylesUit 1.0 as StylesUIt
import controlsUit 1.0 as ControlsUit
Item {
id: flick
@ -28,6 +29,10 @@ Item {
property bool blurOnCtrlShift: true
onUrlChanged: {
permissionPopupBackground.visible = false;
}
StylesUIt.HifiConstants {
id: hifi
}
@ -141,7 +146,15 @@ Item {
}
onFeaturePermissionRequested: {
grantFeaturePermission(securityOrigin, feature, false);
if (permissionPopupBackground.visible === true) {
console.log("Browser engine requested a new permission, but user is already being presented with a different permission request. Aborting request for new permission...");
return;
}
permissionPopupBackground.securityOrigin = securityOrigin;
permissionPopupBackground.feature = feature;
permissionPopupBackground.visible = true;
}
//disable popup
@ -186,4 +199,12 @@ Item {
webViewCore.focus = false;
}
}
ControlsUit.PermissionPopupBackground {
id: permissionPopupBackground
onSendPermission: {
webViewCore.grantFeaturePermission(securityOrigin, feature, shouldGivePermission);
}
}
}

View file

@ -10,10 +10,10 @@
import QtQuick 2.7
import QtWebEngine 1.5
import controlsUit 1.0 as ControlsUit
WebEngineView {
id: root
Component.onCompleted: {
console.log("Connecting JS messaging to Hifi Logging")
// Ensure the JS from the web-engine makes it to our logging
@ -22,6 +22,10 @@ WebEngineView {
});
}
onUrlChanged: {
permissionPopupBackground.visible = false;
}
onLoadingChanged: {
// Required to support clicking on "hifi://" links
if (WebEngineView.LoadStartedStatus == loadRequest.status) {
@ -37,6 +41,21 @@ WebEngineView {
WebSpinner { }
onFeaturePermissionRequested: {
grantFeaturePermission(securityOrigin, feature, false);
if (permissionPopupBackground.visible === true) {
console.log("Browser engine requested a new permission, but user is already being presented with a different permission request. Aborting request for new permission...");
return;
}
permissionPopupBackground.securityOrigin = securityOrigin;
permissionPopupBackground.feature = feature;
permissionPopupBackground.visible = true;
}
ControlsUit.PermissionPopupBackground {
id: permissionPopupBackground
z: 100
onSendPermission: {
root.grantFeaturePermission(securityOrigin, feature, shouldGivePermission);
}
}
}

View file

@ -0,0 +1,90 @@
import QtQuick 2.5
import QtWebEngine 1.5
import QtQuick.Layouts 1.3
import controlsUit 1.0 as HifiControls
import stylesUit 1.0 as HifiStyles
Rectangle {
id: root
width: 750
height: 210
color: hifi.colors.white
anchors.centerIn: parent
readonly property var permissionLanguage: ({
[WebEngineView.MediaAudioCapture]: "access an audio input device",
[WebEngineView.MediaVideoCapture]: "access a video device, like your webcam",
[WebEngineView.MediaAudioVideoCapture]: "access an audio input device and video device",
[WebEngineView.Geolocation]: "access your location",
[WebEngineView.DesktopVideoCapture]: "capture video from your desktop",
[WebEngineView.DesktopAudioVideoCapture]: "capture audio and video from your desktop",
"none": "Undefined permission being requested"
})
property string currentRequestedPermission
signal permissionButtonPressed(int buttonNumber)
ColumnLayout {
anchors.fill: parent
Rectangle {
Layout.preferredHeight: 75
Layout.preferredWidth: parent.width
HifiStyles.RalewayBold {
text: "REQUEST FOR DEVICE ACCESS"
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
wrapMode: Text.WordWrap
color: hifi.colors.black
size: 30
}
}
Rectangle {
Layout.preferredHeight: 35
Layout.preferredWidth: parent.width
HifiStyles.RalewayLight {
text: "This website is attempting to " + root.permissionLanguage[root.currentRequestedPermission] + "."
anchors.centerIn: parent
wrapMode: Text.Wrap
size: 20
color: hifi.colors.black
}
}
Rectangle {
Layout.preferredHeight: 100
Layout.preferredWidth: parent.width
Layout.topMargin: 35
property int space: 8
HifiControls.Button {
text: "Don't Allow"
anchors.right: parent.horizontalCenter
anchors.rightMargin: parent.space
width: 125
color: hifi.buttons.red
height: 38
onClicked: {
root.permissionButtonPressed(0);
}
}
HifiControls.Button {
text: "Yes allow access"
anchors.left: parent.horizontalCenter
anchors.leftMargin: parent.space
color: hifi.buttons.blue
width: 155
height: 38
onClicked: {
root.permissionButtonPressed(1);
}
}
}
}
}

View file

@ -0,0 +1,35 @@
import QtQuick 2.5
Rectangle {
id: root
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.5);
visible: false
property string securityOrigin: 'none'
property string feature: 'none'
signal sendPermission(string securityOrigin, string feature, bool shouldGivePermission)
onFeatureChanged: {
permissionPopupItem.currentRequestedPermission = feature;
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: false
}
PermissionPopup {
id: permissionPopupItem
onPermissionButtonPressed: {
if (buttonNumber === 0) {
root.sendPermission(securityOrigin, feature, false);
} else {
root.sendPermission(securityOrigin, feature, true);
}
root.visible = false;
securityOrigin = 'none';
feature = 'none';
}
}
}

View file

@ -15,6 +15,8 @@ Key 1.0 Key.qml
Keyboard 1.0 Keyboard.qml
Label 1.0 Label.qml
QueuedButton 1.0 QueuedButton.qml
PermissionPopup 1.0 PermissionPopup.qml
PermissionPopupBackground 1.0 PermissionPopupBackground.qml
RadioButton 1.0 RadioButton.qml
ScrollBar 1.0 ScrollBar.qml
Separator 1.0 Separator.qml

View file

@ -166,16 +166,16 @@ Rectangle {
x: 2 * margins.paddings;
width: parent.width;
// switch heights + 2 * top margins
height: (root.switchHeight) * 3 + 48;
height: (root.switchHeight) * 6 + 48;
anchors.top: firstSeparator.bottom;
anchors.topMargin: 10;
// mute is in its own row
Item {
id: switchContainer;
x: margins.paddings;
width: parent.width / 2;
height: parent.height;
anchors.top: parent.top
anchors.left: parent.left;
HifiControlsUit.Switch {
id: muteMic;
@ -222,12 +222,29 @@ Rectangle {
}
HifiControlsUit.Switch {
id: pttSwitch
id: acousticEchoCancellationSwitch;
height: root.switchHeight;
switchWidth: root.switchWidth;
anchors.top: noiseReductionSwitch.bottom
anchors.topMargin: 24
anchors.left: parent.left
labelTextOn: "Echo Cancellation";
labelTextSize: 16;
backgroundOnColor: "#E3E3E3";
checked: AudioScriptingInterface.acousticEchoCancellation;
onCheckedChanged: {
AudioScriptingInterface.acousticEchoCancellation = checked;
checked = Qt.binding(function() { return AudioScriptingInterface.acousticEchoCancellation; });
}
}
HifiControlsUit.Switch {
id: pttSwitch
height: root.switchHeight;
switchWidth: root.switchWidth;
anchors.top: acousticEchoCancellationSwitch.bottom;
anchors.topMargin: 24
anchors.left: parent.left
labelTextOn: (bar.currentIndex === 0) ? qsTr("Push To Talk (T)") : qsTr("Push To Talk");
labelTextSize: 16;
backgroundOnColor: "#E3E3E3";
@ -298,7 +315,6 @@ Rectangle {
checked = Qt.binding(function() { return AudioScriptingInterface.isStereoInput; }); // restore binding
}
}
}
}

View file

@ -39,8 +39,8 @@ Rectangle {
}
Component.onCompleted: {
AudioScriptingInterface.noiseGateOpened.connect(function() { gated = false; });
AudioScriptingInterface.noiseGateClosed.connect(function() { gated = true; });
AudioScriptingInterface.noiseGateOpened.connect(function() { micBar.gated = false; });
AudioScriptingInterface.noiseGateClosed.connect(function() { micBar.gated = true; });
HMD.displayModeChanged.connect(function() {
muted = AudioScriptingInterface.muted;
pushToTalk = AudioScriptingInterface.pushToTalk;
@ -151,7 +151,7 @@ Rectangle {
readonly property string yellow: "#C0C000";
readonly property string fill: "#55000000";
readonly property string border: standalone ? "#80FFFFFF" : "#55FFFFFF";
readonly property string icon: (muted || clipping) ? mutedColor : gated ? gatedColor : unmutedColor;
readonly property string icon: (muted || clipping) ? mutedColor : micBar.gated ? gatedColor : unmutedColor;
}
Item {
@ -169,7 +169,7 @@ Rectangle {
Image {
id: image;
source: (pushToTalk) ? pushToTalkIcon : muted ? mutedIcon :
clipping ? clippingIcon : gated ? gatedIcon : unmutedIcon;
clipping ? clippingIcon : micBar.gated ? gatedIcon : unmutedIcon;
width: 29;
height: 32;
anchors {

View file

@ -786,7 +786,7 @@ Rectangle {
}
lightboxPopup.button2text = "CONFIRM";
lightboxPopup.button2method = function() {
Commerce.replaceContentSet(root.itemHref, root.certificateId);
Commerce.replaceContentSet(root.itemHref, root.certificateId, root.itemName);
lightboxPopup.visible = false;
rezzedNotifContainer.visible = true;
rezzedNotifContainerTimer.start();

View file

@ -36,7 +36,7 @@ Rectangle {
break;
case "content set":
urlHandler.handleUrl("hifi://localhost/0,0,0");
Commerce.replaceContentSet(toUrl(resource), "");
Commerce.replaceContentSet(toUrl(resource), "", "");
break;
case "entity":
case "wearable":

View file

@ -729,7 +729,7 @@ Item {
onClicked: {
Tablet.playSound(TabletEnums.ButtonClick);
if (root.itemType === "contentSet") {
sendToPurchases({method: 'showReplaceContentLightbox', itemHref: root.itemHref, certID: root.certificateId});
sendToPurchases({method: 'showReplaceContentLightbox', itemHref: root.itemHref, certID: root.certificateId, itemName: root.itemName});
} else if (root.itemType === "avatar") {
sendToPurchases({method: 'showChangeAvatarLightbox', itemName: root.itemName, itemHref: root.itemHref});
} else if (root.itemType === "app") {

View file

@ -609,7 +609,7 @@ Rectangle {
}
lightboxPopup.button2text = "CONFIRM";
lightboxPopup.button2method = function() {
Commerce.replaceContentSet(msg.itemHref, msg.certID);
Commerce.replaceContentSet(msg.itemHref, msg.certID, msg.itemName);
lightboxPopup.visible = false;
};
lightboxPopup.visible = true;

View file

@ -222,6 +222,17 @@ Flickable {
}
}
}
SimplifiedControls.Switch {
id: acousticEchoCancellationSwitch
Layout.preferredHeight: 18
Layout.preferredWidth: parent.width
labelTextOn: "Acoustic Echo Cancellation"
checked: AudioScriptingInterface.acousticEchoCancellation
onClicked: {
AudioScriptingInterface.acousticEchoCancellation = !AudioScriptingInterface.acousticEchoCancellation;
}
}
}
}

View file

@ -362,8 +362,8 @@ Rectangle {
id: displayModeImage
source: HMD.active ? "./images/desktopMode.svg" : "./images/vrMode.svg"
anchors.centerIn: parent
width: HMD.active ? 25 : 43
height: 22
width: HMD.active ? 25 : 26
height: HMD.active ? 22 : 14
visible: false
}

File diff suppressed because it is too large Load diff

View file

@ -1225,8 +1225,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
#endif
bool isStore = property(hifi::properties::OCULUS_STORE).toBool();
DependencyManager::get<WalletScriptingInterface>()->setLimitedCommerce(isStore); // Or we could make it a separate arg, or if either arg is set, etc. And should this instead by a hifi::properties?
// Or we could make it a separate arg, or if either arg is set, etc. And should this instead by a hifi::properties?
DependencyManager::get<WalletScriptingInterface>()->setLimitedCommerce(isStore || property(hifi::properties::STEAM).toBool());
updateHeartbeat();
@ -2824,6 +2824,7 @@ void Application::cleanupBeforeQuit() {
// destroy Audio so it and its threads have a chance to go down safely
// this must happen after QML, as there are unexplained audio crashes originating in qtwebengine
AudioInjector::setLocalAudioInterface(nullptr);
DependencyManager::destroy<AudioClient>();
DependencyManager::destroy<AudioScriptingInterface>();
@ -7788,9 +7789,15 @@ bool Application::askToWearAvatarAttachmentUrl(const QString& url) {
return true;
}
void Application::replaceDomainContent(const QString& url) {
static const QString CONTENT_SET_NAME_QUERY_PARAM = "name";
void Application::replaceDomainContent(const QString& url, const QString& itemName) {
qCDebug(interfaceapp) << "Attempting to replace domain content";
QByteArray urlData(url.toUtf8());
QUrl msgUrl(url);
QUrlQuery urlQuery(msgUrl.query());
urlQuery.addQueryItem(CONTENT_SET_NAME_QUERY_PARAM, itemName);
msgUrl.setQuery(urlQuery.query(QUrl::QUrl::FullyEncoded));
QByteArray urlData(msgUrl.toString(QUrl::QUrl::FullyEncoded).toUtf8());
auto limitedNodeList = DependencyManager::get<NodeList>();
const auto& domainHandler = limitedNodeList->getDomainHandler();
@ -7824,7 +7831,7 @@ bool Application::askToReplaceDomainContent(const QString& url) {
QString details;
if (static_cast<QMessageBox::StandardButton>(answer.toInt()) == QMessageBox::Yes) {
// Given confirmation, send request to domain server to replace content
replaceDomainContent(url);
replaceDomainContent(url, QString());
details = "SuccessfulRequestToReplaceContent";
} else {
details = "UserDeclinedToReplaceContent";

View file

@ -326,7 +326,7 @@ public:
bool isInterstitialMode() const { return _interstitialMode; }
bool failedToConnectToEntityServer() const { return _failedToConnectToEntityServer; }
void replaceDomainContent(const QString& url);
void replaceDomainContent(const QString& url, const QString& itemName);
void loadAvatarScripts(const QVector<QString>& urls);
void unloadAvatarScripts();

View file

@ -52,6 +52,7 @@ protected:
protected slots:
/**jsdoc
* Prompts the user to delete a bookmark. The user can select the bookmark to delete in the dialog that is opened.
* @function LocationBookmarks.deleteBookmark
*/
virtual void deleteBookmark();

View file

@ -54,6 +54,7 @@ void LODManager::setRenderTimes(float presentTime, float engineRunTime, float ba
}
void LODManager::autoAdjustLOD(float realTimeDelta) {
std::lock_guard<std::mutex> { _automaticLODLock };
// The "render time" is the worse of:
// - engineRunTime: Time spent in the render thread in the engine producing the gpu::Frame N
@ -235,6 +236,7 @@ void LODManager::resetLODAdjust() {
}
void LODManager::setAutomaticLODAdjust(bool value) {
std::lock_guard<std::mutex> { _automaticLODLock };
_automaticLODAdjust = value;
emit autoLODChanged();
}
@ -426,7 +428,6 @@ float LODManager::getWorldDetailQuality() const {
return HIGH;
}
void LODManager::setLODQualityLevel(float quality) {
_lodQualityLevel = quality;
}

View file

@ -12,6 +12,8 @@
#ifndef hifi_LODManager_h
#define hifi_LODManager_h
#include <mutex>
#include <DependencyManager.h>
#include <NumericalConstants.h>
#include <OctreeConstants.h>
@ -47,11 +49,6 @@ class AABox;
* @property {number} presentTime <em>Read-only.</em>
* @property {number} engineRunTime <em>Read-only.</em>
* @property {number} gpuTime <em>Read-only.</em>
* @property {number} avgRenderTime <em>Read-only.</em>
* @property {number} fps <em>Read-only.</em>
* @property {number} lodLevel <em>Read-only.</em>
* @property {number} lodDecreaseFPS <em>Read-only.</em>
* @property {number} lodIncreaseFPS <em>Read-only.</em>
*/
class LODManager : public QObject, public Dependency {
@ -240,6 +237,7 @@ signals:
private:
LODManager();
std::mutex _automaticLODLock;
bool _automaticLODAdjust = true;
float _presentTime{ 0.0f }; // msec

View file

@ -17,6 +17,9 @@
#include "Bookmarks.h"
/**jsdoc
* The <code>LocationBookmarks</code> API provides facilities for working with location bookmarks. A location bookmark
* associates a name with a metaverse address.
*
* @namespace LocationBookmarks
*
* @hifi-client-entity
@ -35,28 +38,35 @@ public:
static const QString HOME_BOOKMARK;
/**jsdoc
* Gets the metaverse address associated with a bookmark.
* @function LocationBookmarks.getAddress
* @param {string} bookmarkName Name of the bookmark to get the address for.
* @returns {string} The url for the specified bookmark. If the bookmark does not exist, the empty string will be returned.
* @param {string} bookmarkName - Name of the bookmark to get the metaverse address for (case sensitive).
* @returns {string} The metaverse address for the bookmark. If the bookmark does not exist, <code>""</code> is returned.
* @example <caption>Report the "Home" bookmark's metaverse address.</caption>
* print("Home bookmark's address: " + LocationBookmarks.getAddress("Home"));
*/
Q_INVOKABLE QString getAddress(const QString& bookmarkName);
public slots:
/**jsdoc
* Prompts the user to bookmark their current location. The user can specify the name of the bookmark in the dialog that is
* opened.
* @function LocationBookmarks.addBookmark
*/
void addBookmark();
/**jsdoc
* Sets the metaverse address associated with the "Home" bookmark.
* @function LocationBookmarks.setHomeLocationToAddress
* @param {string} address
* @param {string} address - The metaverse address to set the "Home" bookmark to.
*/
void setHomeLocationToAddress(const QVariant& address);
/**jsdoc
* Gets the metaverse address associated with the "Home" bookmark.
* @function LocationBookmarks.getHomeLocationAddress
* @returns {string} The url for the home location bookmark
* @returns {string} The metaverse address for the "Home" bookmark.
*/
QString getHomeLocationAddress();

View file

@ -125,6 +125,18 @@ QString userRecenterModelToString(MyAvatar::SitStandModelType model) {
}
}
static const QStringList REACTION_NAMES = {
QString("positive"),
QString("negative"),
QString("raiseHand"),
QString("applaud"),
QString("point")
};
static int reactionNameToIndex(const QString& reactionName) {
return REACTION_NAMES.indexOf(reactionName);
}
MyAvatar::MyAvatar(QThread* thread) :
Avatar(thread),
_yawSpeed(YAW_SPEED_DEFAULT),
@ -5812,6 +5824,53 @@ void MyAvatar::setModelScale(float scale) {
}
}
QStringList MyAvatar::getReactions() const {
return REACTION_NAMES;
}
bool MyAvatar::triggerReaction(QString reactionName) {
int reactionIndex = reactionNameToIndex(reactionName);
if (reactionIndex >= 0 && reactionIndex < (int)NUM_AVATAR_REACTIONS) {
std::lock_guard<std::mutex> guard(_reactionLock);
_reactionTriggers[reactionIndex] = true;
return true;
}
return false;
}
bool MyAvatar::beginReaction(QString reactionName) {
int reactionIndex = reactionNameToIndex(reactionName);
if (reactionIndex >= 0 && reactionIndex < (int)NUM_AVATAR_REACTIONS) {
std::lock_guard<std::mutex> guard(_reactionLock);
_reactionEnabledRefCounts[reactionIndex]++;
return true;
}
return false;
}
bool MyAvatar::endReaction(QString reactionName) {
int reactionIndex = reactionNameToIndex(reactionName);
if (reactionIndex >= 0 && reactionIndex < (int)NUM_AVATAR_REACTIONS) {
std::lock_guard<std::mutex> guard(_reactionLock);
_reactionEnabledRefCounts[reactionIndex]--;
return true;
}
return false;
}
void MyAvatar::updateRigControllerParameters(Rig::ControllerParameters& params) {
std::lock_guard<std::mutex> guard(_reactionLock);
for (int i = 0; i < NUM_AVATAR_REACTIONS; i++) {
// copy current state into params.
params.reactionEnabledFlags[i] = _reactionEnabledRefCounts[i] > 0;
params.reactionTriggers[i] = _reactionTriggers[i];
// clear reaction triggers here as well
_reactionTriggers[i] = false;
}
}
SpatialParentTree* MyAvatar::getParentTree() const {
auto entityTreeRenderer = qApp->getEntities();
EntityTreePointer entityTree = entityTreeRenderer ? entityTreeRenderer->getTree() : nullptr;
@ -6148,3 +6207,51 @@ void MyAvatar::sendPacket(const QUuid& entityID) const {
});
}
}
void MyAvatar::setSitDriveKeysStatus(bool enabled) {
const std::vector<DriveKeys> DISABLED_DRIVE_KEYS_DURING_SIT = {
DriveKeys::TRANSLATE_X,
DriveKeys::TRANSLATE_Y,
DriveKeys::TRANSLATE_Z,
DriveKeys::STEP_TRANSLATE_X,
DriveKeys::STEP_TRANSLATE_Y,
DriveKeys::STEP_TRANSLATE_Z
};
for (auto key : DISABLED_DRIVE_KEYS_DURING_SIT) {
if (enabled) {
enableDriveKey(key);
} else {
disableDriveKey(key);
}
}
}
void MyAvatar::beginSit(const glm::vec3& position, const glm::quat& rotation) {
_characterController.setSeated(true);
setCollisionsEnabled(false);
setHMDLeanRecenterEnabled(false);
// Disable movement
setSitDriveKeysStatus(false);
centerBody();
int hipIndex = getJointIndex("Hips");
clearPinOnJoint(hipIndex);
goToLocation(position, true, rotation, false, false);
pinJoint(hipIndex, position, rotation);
}
void MyAvatar::endSit(const glm::vec3& position, const glm::quat& rotation) {
if (_characterController.getSeated()) {
clearPinOnJoint(getJointIndex("Hips"));
_characterController.setSeated(false);
setCollisionsEnabled(true);
setHMDLeanRecenterEnabled(true);
centerBody();
goToLocation(position, true, rotation, false, false);
float TIME_BEFORE_DRIVE_ENABLED_MS = 150.0f;
QTimer::singleShot(TIME_BEFORE_DRIVE_ENABLED_MS, [this]() {
// Enable movement again
setSitDriveKeysStatus(true);
});
}
}

View file

@ -1835,10 +1835,30 @@ public:
*/
Q_INVOKABLE QVariantList getCollidingFlowJoints();
/**jsdoc
* Starts a sitting action for the avatar
* @function MyAvatar.beginSit
* @param {Vec3} position - The point in space where the avatar will sit.
* @param {Quat} rotation - Initial absolute orientation of the avatar once is seated.
*/
Q_INVOKABLE void beginSit(const glm::vec3& position, const glm::quat& rotation);
/**jsdoc
* Ends a sitting action for the avatar
* @function MyAvatar.endSit
* @param {Vec3} position - The position of the avatar when standing up.
* @param {Quat} rotation - The absolute rotation of the avatar once the sitting action ends.
*/
Q_INVOKABLE void endSit(const glm::vec3& position, const glm::quat& rotation);
int getOverrideJointCount() const;
bool getFlowActive() const;
bool getNetworkGraphActive() const;
// sets the reaction enabled and triggered parameters of the passed in params
// also clears internal reaction triggers
void updateRigControllerParameters(Rig::ControllerParameters& params);
public slots:
/**jsdoc
@ -2194,6 +2214,33 @@ public slots:
*/
virtual void setModelScale(float scale) override;
/**jsdoc
* MyAvatar.getReactions
* @returns {string[]} Array of reaction names.
*/
QStringList getReactions() const;
/**jsdoc
* MyAvatar.triggerReaction
* @param {string} reactionName - reaction name
* @returns {bool} false if the given reaction is not supported.
*/
bool triggerReaction(QString reactionName);
/**jsdoc
* MyAvatar.beginReaction
* @param {string} reactionName - reaction name
* @returns {bool} false if the given reaction is not supported.
*/
bool beginReaction(QString reactionName);
/**jsdoc
* MyAvatar.endReaction
* @param {string} reactionName - reaction name
* @returns {bool} false if the given reaction is not supported.
*/
bool endReaction(QString reactionName);
signals:
/**jsdoc
@ -2492,6 +2539,7 @@ private:
virtual void updatePalms() override {}
void lateUpdatePalms();
void setSitDriveKeysStatus(bool enabled);
void clampTargetScaleToDomainLimits();
void clampScaleChangeToDomainLimits(float desiredScale);
@ -2825,6 +2873,10 @@ private:
mutable std::mutex _scriptEngineLock;
QScriptEngine* _scriptEngine { nullptr };
bool _needToSaveAvatarEntitySettings { false };
int _reactionEnabledRefCounts[NUM_AVATAR_REACTIONS] { 0, 0, 0, 0, 0 };
bool _reactionTriggers[NUM_AVATAR_REACTIONS] { false, false, false, false, false };
mutable std::mutex _reactionLock;
};
QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioListenerMode& audioListenerMode);

View file

@ -32,6 +32,8 @@ Rig::CharacterControllerState convertCharacterControllerState(CharacterControlle
return Rig::CharacterControllerState::InAir;
case CharacterController::State::Hover:
return Rig::CharacterControllerState::Hover;
case CharacterController::State::Seated:
return Rig::CharacterControllerState::Seated;
};
}
@ -294,8 +296,6 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
_prevIsEstimatingHips = false;
}
params.isTalking = head->getTimeWithoutTalking() <= 1.5f;
// pass detailed torso k-dops to rig.
int hipsJoint = _rig.indexOfJoint("Hips");
if (hipsJoint >= 0) {
@ -314,6 +314,10 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
params.spine2ShapeInfo = hfmModel.joints[spine2Joint].shapeInfo;
}
params.isTalking = head->getTimeWithoutTalking() <= 1.5f;
myAvatar->updateRigControllerParameters(params);
_rig.updateFromControllerParameters(params, deltaTime);
Rig::CharacterControllerState ccState = convertCharacterControllerState(myAvatar->getCharacterController()->getState());

View file

@ -259,7 +259,7 @@ void QmlCommerce::authorizeAssetTransfer(const QString& couponID,
ledger->authorizeAssetTransfer(key, couponID, certificateID, amount, optionalMessage);
}
void QmlCommerce::replaceContentSet(const QString& itemHref, const QString& certificateID) {
void QmlCommerce::replaceContentSet(const QString& itemHref, const QString& certificateID, const QString& itemName) {
if (!certificateID.isEmpty()) {
auto ledger = DependencyManager::get<Ledger>();
ledger->updateLocation(
@ -267,7 +267,7 @@ void QmlCommerce::replaceContentSet(const QString& itemHref, const QString& cert
DependencyManager::get<AddressManager>()->getPlaceName(),
true);
}
qApp->replaceDomainContent(itemHref);
qApp->replaceDomainContent(itemHref, itemName);
QJsonObject messageProperties = {
{ "status", "SuccessfulRequestToReplaceContent" },
{ "content_set_url", itemHref } };

View file

@ -90,7 +90,7 @@ protected:
Q_INVOKABLE void transferAssetToUsername(const QString& username, const QString& certificateID, const int& amount, const QString& optionalMessage);
Q_INVOKABLE void authorizeAssetTransfer(const QString& couponID, const QString& certificateID, const int& amount, const QString& optionalMessage);
Q_INVOKABLE void replaceContentSet(const QString& itemHref, const QString& certificateID);
Q_INVOKABLE void replaceContentSet(const QString& itemHref, const QString& certificateID, const QString& itemName);
Q_INVOKABLE QString getInstalledApps(const QString& justInstalledAppID = "");
Q_INVOKABLE bool installApp(const QString& appHref, const bool& alsoOpenImmediately = false);

View file

@ -26,6 +26,7 @@ QString Audio::HMD { "VR" };
Setting::Handle<bool> enableNoiseReductionSetting { QStringList { Audio::AUDIO, "NoiseReduction" }, true };
Setting::Handle<bool> enableWarnWhenMutedSetting { QStringList { Audio::AUDIO, "WarnWhenMuted" }, true };
Setting::Handle<bool> enableAcousticEchoCancellationSetting { QStringList { Audio::AUDIO, "AcousticEchoCancellation" }, true };
float Audio::loudnessToLevel(float loudness) {
@ -40,12 +41,14 @@ Audio::Audio() : _devices(_contextIsHMD) {
connect(client, &AudioClient::muteToggled, this, &Audio::setMuted);
connect(client, &AudioClient::noiseReductionChanged, this, &Audio::enableNoiseReduction);
connect(client, &AudioClient::warnWhenMutedChanged, this, &Audio::enableWarnWhenMuted);
connect(client, &AudioClient::acousticEchoCancellationChanged, this, &Audio::enableAcousticEchoCancellation);
connect(client, &AudioClient::inputLoudnessChanged, this, &Audio::onInputLoudnessChanged);
connect(client, &AudioClient::inputVolumeChanged, this, &Audio::setInputVolume);
connect(this, &Audio::contextChanged, &_devices, &AudioDevices::onContextChanged);
connect(this, &Audio::pushingToTalkChanged, this, &Audio::handlePushedToTalk);
enableNoiseReduction(enableNoiseReductionSetting.get());
enableWarnWhenMuted(enableWarnWhenMutedSetting.get());
enableAcousticEchoCancellation(enableAcousticEchoCancellationSetting.get());
onContextChanged();
}
@ -277,6 +280,28 @@ void Audio::enableWarnWhenMuted(bool enable) {
}
}
bool Audio::acousticEchoCancellationEnabled() const {
return resultWithReadLock<bool>([&] {
return _enableAcousticEchoCancellation;
});
}
void Audio::enableAcousticEchoCancellation(bool enable) {
bool changed = false;
withWriteLock([&] {
if (_enableAcousticEchoCancellation != enable) {
_enableAcousticEchoCancellation = enable;
auto client = DependencyManager::get<AudioClient>().data();
QMetaObject::invokeMethod(client, "setAcousticEchoCancellation", Q_ARG(bool, enable), Q_ARG(bool, false));
enableAcousticEchoCancellationSetting.set(enable);
changed = true;
}
});
if (changed) {
emit acousticEchoCancellationChanged(enable);
}
}
float Audio::getInputVolume() const {
return resultWithReadLock<bool>([&] {
return _inputVolume;

View file

@ -76,6 +76,9 @@ class Audio : public AudioScriptingInterface, protected ReadWriteLockable {
* @property {number} systemInjectorGain - The gain (relative volume in dB) that system sounds are played at.
* @property {number} pushingToTalkOutputGainDesktop - The gain (relative volume in dB) that all sounds are played at when
* the user is holding the push-to-talk key in desktop mode.
* @property {boolean} acousticEchoCancellation - <code>true</code> if acoustic echo cancellation is enabled, otherwise
* <code>false</code>. When enabled, sound from the audio output is suppressed when it echos back to the input audio
* signal.
*
* @comment The following properties are from AudioScriptingInterface.h.
* @property {boolean} isStereoInput - <code>true</code> if the input audio is being used in stereo, otherwise
@ -89,6 +92,8 @@ class Audio : public AudioScriptingInterface, protected ReadWriteLockable {
Q_PROPERTY(bool muted READ isMuted WRITE setMuted NOTIFY mutedChanged)
Q_PROPERTY(bool noiseReduction READ noiseReductionEnabled WRITE enableNoiseReduction NOTIFY noiseReductionChanged)
Q_PROPERTY(bool warnWhenMuted READ warnWhenMutedEnabled WRITE enableWarnWhenMuted NOTIFY warnWhenMutedChanged)
Q_PROPERTY(bool acousticEchoCancellation
READ acousticEchoCancellationEnabled WRITE enableAcousticEchoCancellation NOTIFY acousticEchoCancellationChanged)
Q_PROPERTY(float inputVolume READ getInputVolume WRITE setInputVolume NOTIFY inputVolumeChanged)
Q_PROPERTY(float inputLevel READ getInputLevel NOTIFY inputLevelChanged)
Q_PROPERTY(bool clipping READ isClipping NOTIFY clippingChanged)
@ -119,6 +124,7 @@ public:
bool isMuted() const;
bool noiseReductionEnabled() const;
bool warnWhenMutedEnabled() const;
bool acousticEchoCancellationEnabled() const;
float getInputVolume() const;
float getInputLevel() const;
bool isClipping() const;
@ -400,6 +406,14 @@ signals:
*/
void warnWhenMutedChanged(bool isEnabled);
/**jsdoc
* Triggered when acoustic echo cancellation is enabled or disabled.
* @function Audio.acousticEchoCancellationChanged
* @param {boolean} isEnabled - <code>true</code> if acoustic echo cancellation is enabled, otherwise <code>false</code>.
* @returns {Signal}
*/
void acousticEchoCancellationChanged(bool isEnabled);
/**jsdoc
* Triggered when the input audio volume changes.
* @function Audio.inputVolumeChanged
@ -498,6 +512,7 @@ private slots:
void setMuted(bool muted);
void enableNoiseReduction(bool enable);
void enableWarnWhenMuted(bool enable);
void enableAcousticEchoCancellation(bool enable);
void setInputVolume(float volume);
void onInputLoudnessChanged(float loudness, bool isClipping);
@ -516,6 +531,7 @@ private:
bool _isClipping { false };
bool _enableNoiseReduction { true }; // Match default value of AudioClient::_isNoiseGateEnabled.
bool _enableWarnWhenMuted { true };
bool _enableAcousticEchoCancellation { true }; // AudioClient::_isAECEnabled
bool _contextIsHMD { false };
AudioDevices* getDevices() { return &_devices; }
AudioDevices _devices;

View file

@ -69,6 +69,14 @@ void QmlWindowProxy::parentNativeWindowToMainWindow() {
#endif
}
void InteractiveWindowProxy::emitScriptEvent(const QVariant& scriptMessage){
emit scriptEventReceived(scriptMessage);
}
void InteractiveWindowProxy::emitWebEvent(const QVariant& webMessage) {
emit webEventReceived(webMessage);
}
static void qmlWindowProxyDeleter(QmlWindowProxy* qmlWindowProxy) {
qmlWindowProxy->deleteLater();
}
@ -128,6 +136,16 @@ InteractiveWindow::InteractiveWindow(const QString& sourceUrl, const QVariantMap
if (properties.contains(PRESENTATION_MODE_PROPERTY)) {
presentationMode = (InteractiveWindowPresentationMode) properties[PRESENTATION_MODE_PROPERTY].toInt();
}
_interactiveWindowProxy = std::unique_ptr<InteractiveWindowProxy,
std::function<void(InteractiveWindowProxy*)>>(new InteractiveWindowProxy, [](InteractiveWindowProxy *p) {
p->deleteLater();
});
connect(_interactiveWindowProxy.get(), &InteractiveWindowProxy::webEventReceived,
this, &InteractiveWindow::emitWebEvent, Qt::QueuedConnection);
connect(this, &InteractiveWindow::scriptEventReceived, _interactiveWindowProxy.get(),
&InteractiveWindowProxy::emitScriptEvent, Qt::QueuedConnection);
if (properties.contains(DOCKED_PROPERTY) && presentationMode == InteractiveWindowPresentationMode::Native) {
QVariantMap nativeWindowInfo = properties[DOCKED_PROPERTY].toMap();
@ -147,7 +165,7 @@ InteractiveWindow::InteractiveWindow(const QString& sourceUrl, const QVariantMap
_dockWidget = std::shared_ptr<DockWidget>(new DockWidget(title, mainWindow), dockWidgetDeleter);
auto quickView = _dockWidget->getQuickView();
Application::setupQmlSurface(quickView->rootContext() , true);
Application::setupQmlSurface(quickView->rootContext(), true);
//add any whitelisted callbacks
OffscreenUi::applyWhiteList(sourceUrl, quickView->rootContext());
@ -182,13 +200,17 @@ InteractiveWindow::InteractiveWindow(const QString& sourceUrl, const QVariantMap
break;
}
}
QObject::connect(quickView.get(), &QQuickView::statusChanged, [&, this] (QQuickView::Status status) {
if (status == QQuickView::Ready) {
QQuickItem* rootItem = _dockWidget->getRootItem();
_dockWidget->getQuickView()->rootContext()->setContextProperty(EVENT_BRIDGE_PROPERTY, this);
_dockWidget->getQuickView()->rootContext()->setContextProperty(EVENT_BRIDGE_PROPERTY, _interactiveWindowProxy.get());
// The qmlToScript method handles the thread-safety of this call. Because the QVariant argument
// passed to the sendToScript signal may wrap an externally managed and thread-unsafe QJSValue,
// qmlToScript needs to be called directly, so the QJSValue can be immediately converted to a plain QVariant.
QObject::connect(rootItem, SIGNAL(sendToScript(QVariant)), this, SLOT(qmlToScript(const QVariant&)),
Qt::QueuedConnection);
Qt::DirectConnection);
QObject::connect(rootItem, SIGNAL(keyPressEvent(int, int)), this, SLOT(forwardKeyPressEvent(int, int)),
Qt::QueuedConnection);
QObject::connect(rootItem, SIGNAL(keyReleaseEvent(int, int)), this, SLOT(forwardKeyReleaseEvent(int, int)),
@ -204,7 +226,7 @@ InteractiveWindow::InteractiveWindow(const QString& sourceUrl, const QVariantMap
// Build the event bridge and wrapper on the main thread
offscreenUi->loadInNewContext(CONTENT_WINDOW_QML, [&](QQmlContext* context, QObject* object) {
_qmlWindowProxy = std::shared_ptr<QmlWindowProxy>(new QmlWindowProxy(object, nullptr), qmlWindowProxyDeleter);
context->setContextProperty(EVENT_BRIDGE_PROPERTY, this);
context->setContextProperty(EVENT_BRIDGE_PROPERTY, _interactiveWindowProxy.get());
if (properties.contains(ADDITIONAL_FLAGS_PROPERTY)) {
object->setProperty(ADDITIONAL_FLAGS_PROPERTY, properties[ADDITIONAL_FLAGS_PROPERTY].toUInt());
}
@ -229,7 +251,10 @@ InteractiveWindow::InteractiveWindow(const QString& sourceUrl, const QVariantMap
object->setProperty(VISIBLE_PROPERTY, properties[INTERACTIVE_WINDOW_VISIBLE_PROPERTY].toBool());
}
connect(object, SIGNAL(sendToScript(QVariant)), this, SLOT(qmlToScript(const QVariant&)), Qt::QueuedConnection);
// The qmlToScript method handles the thread-safety of this call. Because the QVariant argument
// passed to the sendToScript signal may wrap an externally managed and thread-unsafe QJSValue,
// qmlToScript needs to be called directly, so the QJSValue can be immediately converted to a plain QVariant.
connect(object, SIGNAL(sendToScript(QVariant)), this, SLOT(qmlToScript(const QVariant&)), Qt::DirectConnection);
QObject::connect(object, SIGNAL(keyPressEvent(int, int)), this, SLOT(forwardKeyPressEvent(int, int)),
Qt::QueuedConnection);
QObject::connect(object, SIGNAL(keyReleaseEvent(int, int)), this, SLOT(forwardKeyReleaseEvent(int, int)),
@ -263,6 +288,7 @@ InteractiveWindow::~InteractiveWindow() {
}
void InteractiveWindow::sendToQml(const QVariant& message) {
// Forward messages received from the script on to QML
if (_dockWidget) {
QQuickItem* rootItem = _dockWidget->getRootItem();
@ -301,6 +327,7 @@ void InteractiveWindow::close() {
}
_dockWidget = nullptr;
_qmlWindowProxy = nullptr;
_interactiveWindowProxy = nullptr;
}
void InteractiveWindow::show() {
@ -315,13 +342,21 @@ void InteractiveWindow::raise() {
}
}
void InteractiveWindow::qmlToScript(const QVariant& message) {
void InteractiveWindow::qmlToScript(const QVariant& originalMessage) {
QVariant message = originalMessage;
if (message.canConvert<QJSValue>()) {
emit fromQml(qvariant_cast<QJSValue>(message).toVariant());
message = qvariant_cast<QJSValue>(message).toVariant();
} else if (message.canConvert<QString>()) {
emit fromQml(message.toString());
message = message.toString();
} else {
qWarning() << "Unsupported message type " << message;
return;
}
if (thread() != QThread::currentThread()) {
QMetaObject::invokeMethod(this, "fromQml", Q_ARG(const QVariant&, message));
} else {
emit fromQml(message);
}
}

View file

@ -34,10 +34,24 @@ public:
QObject* getQmlWindow() const { return _qmlWindow; }
private:
QObject* _qmlWindow;
};
class InteractiveWindowProxy : public QObject {
Q_OBJECT
public:
InteractiveWindowProxy(){}
public slots:
void emitScriptEvent(const QVariant& scriptMessage);
void emitWebEvent(const QVariant& webMessage);
signals:
void scriptEventReceived(const QVariant& message);
void webEventReceived(const QVariant& message);
};
namespace InteractiveWindowEnums {
Q_NAMESPACE
@ -309,6 +323,7 @@ protected slots:
private:
std::shared_ptr<QmlWindowProxy> _qmlWindowProxy;
std::shared_ptr<DockWidget> _dockWidget { nullptr };
std::unique_ptr<InteractiveWindowProxy, std::function<void(InteractiveWindowProxy*)>> _interactiveWindowProxy;
};
typedef InteractiveWindow* InteractiveWindowPointer;

View file

@ -11,11 +11,52 @@
#include "ResourceImageItem.h"
#include <gl/Config.h>
#include <gl/GLHelpers.h>
#include <QOpenGLFramebufferObjectFormat>
#include <QOpenGLShaderProgram>
#include <plugins/DisplayPlugin.h>
static const char* VERTEX_SHADER = R"SHADER(
#version 450 core
out vec2 vTexCoord;
void main(void) {
const float depth = 0.0;
const vec4 UNIT_QUAD[4] = vec4[4](
vec4(-1.0, -1.0, depth, 1.0),
vec4(1.0, -1.0, depth, 1.0),
vec4(-1.0, 1.0, depth, 1.0),
vec4(1.0, 1.0, depth, 1.0)
);
vec4 pos = UNIT_QUAD[gl_VertexID];
gl_Position = pos;
vTexCoord = (pos.xy + 1.0) * 0.5;
}
)SHADER";
static const char* FRAGMENT_SHADER = R"SHADER(
#version 450 core
uniform sampler2D sampler;
in vec2 vTexCoord;
out vec4 FragColor;
vec3 color_LinearTosRGB(vec3 lrgb) {
return mix(vec3(1.055) * pow(vec3(lrgb), vec3(0.41666)) - vec3(0.055), vec3(lrgb) * vec3(12.92), vec3(lessThan(lrgb, vec3(0.0031308))));
}
void main() {
FragColor = vec4(color_LinearTosRGB(texture(sampler, vTexCoord).rgb), 1.0);
}
)SHADER";
ResourceImageItem::ResourceImageItem() : QQuickFramebufferObject() {
auto textureCache = DependencyManager::get<TextureCache>();
connect(textureCache.data(), SIGNAL(spectatorCameraFramebufferReset()), this, SLOT(update()));
@ -95,16 +136,29 @@ void ResourceImageItemRenderer::render() {
}
if (_ready) {
_fboMutex.lock();
_copyFbo->bind();
QOpenGLFramebufferObject::blitFramebuffer(framebufferObject(), _copyFbo, GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT, GL_NEAREST);
// this clears the copyFbo texture
// so next frame starts fresh - helps
// when aspect ratio changes
_copyFbo->takeTexture();
if (!_shader) {
_shader = new QOpenGLShaderProgram();
_shader->addCacheableShaderFromSourceCode(QOpenGLShader::Vertex, VERTEX_SHADER);
_shader->addCacheableShaderFromSourceCode(QOpenGLShader::Fragment, FRAGMENT_SHADER);
_shader->link();
glGenVertexArrays(1, &_vao);
}
framebufferObject()->bind();
_shader->bind();
auto sourceTextureId = _copyFbo->takeTexture();
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, sourceTextureId);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glBindVertexArray(_vao);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glDeleteTextures(1, &sourceTextureId);
_copyFbo->bind();
_copyFbo->release();
_fboMutex.unlock();
}
glFlush();

View file

@ -22,6 +22,9 @@
#include <TextureCache.h>
class QOpenGLFramebufferObject;
class QOpenGLShaderProgram;
class ResourceImageItemRenderer : public QObject, public QQuickFramebufferObject::Renderer {
Q_OBJECT
public:
@ -30,14 +33,16 @@ public:
void synchronize(QQuickFramebufferObject* item) override;
void render() override;
private:
bool _ready;
bool _ready{ false };
QString _url;
bool _visible;
bool _visible{ false };
NetworkTexturePointer _networkTexture;
QQuickWindow* _window;
QQuickWindow* _window{ nullptr };
QMutex _fboMutex;
uint32_t _vao{ 0 };
QOpenGLFramebufferObject* _copyFbo { nullptr };
QOpenGLShaderProgram* _shader{ nullptr };
GLsync _fenceSync { 0 };
QTimer _updateTimer;
public slots:

View file

@ -41,7 +41,6 @@
#include "MainWindow.h"
#include "Snapshot.h"
#include "SnapshotUploader.h"
#include "ToneMappingEffect.h"
// filename format: hifi-snap-by-%username%-on-%date%_%time%_@-%location%.jpg
// %1 <= username, %2 <= date and time, %3 <= current location

View file

@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.0)
set(ENV{MACOSX_DEPLOYMENT_TARGET} 10.9)
project(HQLauncher)
set (CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules/")
include("cmake/macros/SetPackagingParameters.cmake")
set(src_files
src/Launcher.h
src/Launcher.m
@ -59,43 +60,27 @@ set_target_properties(${this_target} PROPERTIES
set(MACOSX_BUNDLE_ICON_FILE "interface.icns")
function(set_from_env _RESULT_NAME _ENV_VAR_NAME _DEFAULT_VALUE)
if (NOT DEFINED ${_RESULT_NAME})
if ("$ENV{${_ENV_VAR_NAME}}" STREQUAL "")
set (${_RESULT_NAME} ${_DEFAULT_VALUE} PARENT_SCOPE)
else()
set (${_RESULT_NAME} $ENV{${_ENV_VAR_NAME}} PARENT_SCOPE)
endif()
if (NOT DEFINED ${_RESULT_NAME})
if ("$ENV{${_ENV_VAR_NAME}}" STREQUAL "")
set (${_RESULT_NAME} ${_DEFAULT_VALUE} PARENT_SCOPE)
else()
set (${_RESULT_NAME} $ENV{${_ENV_VAR_NAME}} PARENT_SCOPE)
endif()
endif()
endfunction()
set_packaging_parameters()
add_executable(${PROJECT_NAME} MACOSX_BUNDLE ${src_files})
set_from_env(XCODE_DEVELOPMENT_TEAM XCODE_DEVELOPMENT_TEAM "")
if ("${XCODE_DEVELOPMENT_TEAM}" STREQUAL "")
message(WARNING "XCODE_DEVELOPMENT_TEAM environmental variable is not set. Not signing build.")
set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME ${APP_NAME}
MACOSX_BUNDLE_BUNDLE_NAME ${APP_NAME}
)
else()
set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME ${APP_NAME}
MACOSX_BUNDLE_BUNDLE_NAME ${APP_NAME}
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "HQ Launcher.entitlements"
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Developer ID Application"
XCODE_ATTRIBUTE_CODE_SIGN_INJECT_BASE_ENTITLEMENTS NO
XCODE_ATTRIBUTE_CODE_SIGN_STYLE "Manual"
XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "${XCODE_DEVELOPMENT_TEAM}"
XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME YES
XCODE_ATTRIBUTE_OTHER_CODE_SIGN_FLAGS "--timestamp"
XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER ""
)
endif()
set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME ${APP_NAME}
MACOSX_BUNDLE_BUNDLE_NAME ${APP_NAME})
set_from_env(LAUNCHER_HMAC_SECRET LAUNCHER_HMAC_SECRET "")
if ("${LAUNCHER_HMAC_SECRET}" STREQUAL "")
message(FATAL_ERROR "LAUNCHER_HMAC_SECRET is not set")
endif()
target_compile_definitions(${PROJECT_NAME} PRIVATE LAUNCHER_HMAC_SECRET="${LAUNCHER_HMAC_SECRET}")
target_compile_definitions(${PROJECT_NAME} PRIVATE LAUNCHER_BUILD_VERSION="${BUILD_VERSION}")
file(GLOB NIB_FILES "nib/*.xib")

View file

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>high-fidelity.hifi</string>
</array>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

View file

@ -0,0 +1,45 @@
#
# SetPackagingParameters.cmake
# cmake/macros
#
# Created by Leonardo Murillo on 07/14/2015.
# Copyright 2015 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
# This macro checks some Jenkins defined environment variables to determine the origin of this build
# and decides how targets should be packaged.
macro(SET_PACKAGING_PARAMETERS)
set(PR_BUILD 0)
set(PRODUCTION_BUILD 0)
set(DEV_BUILD 0)
set(BUILD_NUMBER 0)
set_from_env(RELEASE_TYPE RELEASE_TYPE "DEV")
set_from_env(RELEASE_NUMBER RELEASE_NUMBER "")
set_from_env(STABLE_BUILD STABLE_BUILD 0)
message(STATUS "The RELEASE_TYPE variable is: ${RELEASE_TYPE}")
set(BUILD_NUMBER ${RELEASE_NUMBER})
if (RELEASE_TYPE STREQUAL "PRODUCTION")
set(PRODUCTION_BUILD 1)
set(BUILD_VERSION ${RELEASE_NUMBER})
# add definition for this release type
add_definitions(-DPRODUCTION_BUILD)
elseif (RELEASE_TYPE STREQUAL "PR")
set(PR_BUILD 1)
set(BUILD_VERSION "PR${RELEASE_NUMBER}")
# add definition for this release type
add_definitions(-DPR_BUILD)
else ()
set(DEV_BUILD 1)
set(BUILD_VERSION "dev")
endif ()
endmacro(SET_PACKAGING_PARAMETERS)

View file

@ -102,12 +102,22 @@
<action selector="termsOfService:" target="YVh-OH-vU8" id="bgc-08-8Lj"/>
</connections>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="oJs-u5-OOJ">
<rect key="frame" x="380" y="0.0" width="130" height="14"/>
<autoresizingMask key="autoresizingMask"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="right" title="Label" id="H6o-Xs-wK1">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="systemGrayColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<point key="canvasLocation" x="138.5" y="154"/>
</customView>
<customObject id="YVh-OH-vU8" customClass="DisplayNameScreen">
<connections>
<outlet property="backgroundImage" destination="aus-lo-eVi" id="SRc-pV-lXG"/>
<outlet property="buildVersion" destination="oJs-u5-OOJ" id="avj-j2-5P6"/>
<outlet property="displayName" destination="Vhg-rq-xUH" id="Fb5-im-2hx"/>
<outlet property="smallLogo" destination="j8K-TD-h7e" id="OVd-p3-nu6"/>
</connections>

View file

@ -69,12 +69,22 @@
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="CBD-Vk-Xd4"/>
</imageView>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Omk-5j-C6r">
<rect key="frame" x="381" y="0.0" width="130" height="14"/>
<autoresizingMask key="autoresizingMask"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="right" title="Label" id="xpG-oP-agI">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="systemGrayColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<point key="canvasLocation" x="138.5" y="152"/>
</customView>
<customObject id="nWn-x7-LxT" customClass="ErrorViewController">
<connections>
<outlet property="backgroundImage" destination="eih-a8-Pqa" id="2xh-8r-1Qu"/>
<outlet property="buildVersion" destination="Omk-5j-C6r" id="aTp-c3-RVy"/>
<outlet property="smallLogo" destination="ugn-Hk-gWL" id="EVI-d3-mf5"/>
<outlet property="voxelImage" destination="jmW-5q-Mf3" id="NiI-cY-tAf"/>
</connections>

View file

@ -119,12 +119,22 @@
<action selector="havingTrouble:" target="NkF-nk-81S" id="tsf-tC-aqq"/>
</connections>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="EsT-hn-six">
<rect key="frame" x="380" y="0.0" width="130" height="14"/>
<autoresizingMask key="autoresizingMask"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="right" title="Label" id="AGt-jH-mX2">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="systemGrayColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<point key="canvasLocation" x="138.5" y="154"/>
</customView>
<customObject id="NkF-nk-81S" customClass="LoginScreen">
<connections>
<outlet property="backgroundImage" destination="L56-Jv-0N8" id="INT-rB-YtG"/>
<outlet property="buildVersion" destination="EsT-hn-six" id="Z4k-Gv-U5g"/>
<outlet property="button" destination="jKE-fV-Tjv" id="or6-tG-r6R"/>
<outlet property="header" destination="hIC-qf-Abj" id="sVQ-rl-cvR"/>
<outlet property="orginization" destination="L7W-3n-OKy" id="TiL-wn-Z2b"/>

View file

@ -53,6 +53,15 @@
<rect key="frame" x="68" y="78" width="394" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
</progressIndicator>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="U3I-TA-xRz">
<rect key="frame" x="380" y="0.0" width="130" height="14"/>
<autoresizingMask key="autoresizingMask"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="right" title="Label" id="8YH-Td-daK">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="systemGrayColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<point key="canvasLocation" x="138.5" y="154"/>
</customView>
@ -60,6 +69,7 @@
<connections>
<outlet property="background" destination="kuY-e2-Hqb" id="CBc-bD-ux7"/>
<outlet property="boldStatus" destination="EMF-E4-qLL" id="udm-8B-7lt"/>
<outlet property="buildVersion" destination="U3I-TA-xRz" id="HDI-tW-1cC"/>
<outlet property="progressView" destination="aEr-fi-fkV" id="OUy-Qp-tiP"/>
<outlet property="smallLogo" destination="uh2-4K-n56" id="pYg-hP-nr5"/>
<outlet property="smallStatus" destination="BSg-lp-njL" id="ziz-ek-Lq4"/>

View file

@ -26,11 +26,21 @@
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="qC6-tI-Uwf"/>
</imageView>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="EgF-VK-Hfo">
<rect key="frame" x="380" y="0.0" width="130" height="14"/>
<autoresizingMask key="autoresizingMask"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="right" title="Label" id="gWS-UL-cjB">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="systemGrayColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="systemGrayColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<point key="canvasLocation" x="119.5" y="134"/>
</customView>
<customObject id="iJ0-FI-XIf" customClass="SplashScreen">
<connections>
<outlet property="buildVersion" destination="EgF-VK-Hfo" id="lVp-Ua-9Mt"/>
<outlet property="imageView" destination="qtD-mb-qqq" id="rCt-Gd-Uux"/>
<outlet property="logoImage" destination="2i5-Zw-nH7" id="7tM-sX-cvR"/>
</connections>

View file

@ -6,6 +6,7 @@
@property (nonatomic, assign) IBOutlet NSImageView* backgroundImage;
@property (nonatomic, assign) IBOutlet NSImageView* smallLogo;
@property (nonatomic, assign) IBOutlet NSTextField* displayName;
@property (nonatomic, assign) IBOutlet NSTextField* buildVersion;
@end
@implementation DisplayNameScreen
@ -13,10 +14,9 @@
[self.backgroundImage setImage: [NSImage imageNamed:hifiBackgroundFilename]];
[self.smallLogo setImage: [NSImage imageNamed:hifiSmallLogoFilename]];
NSMutableAttributedString* displayNameString = [[NSMutableAttributedString alloc] initWithString:@"Display Name"];
[self.buildVersion setStringValue: [@"V." stringByAppendingString:@LAUNCHER_BUILD_VERSION]];
[displayNameString addAttribute:NSForegroundColorAttributeName value:[NSColor grayColor] range:NSMakeRange(0, displayNameString.length)];
[displayNameString addAttribute:NSFontAttributeName value:[NSFont systemFontOfSize:18] range:NSMakeRange(0,displayNameString.length)];
[self.displayName setPlaceholderAttributedString:displayNameString];
[self.displayName setTarget:self];
[self.displayName setAction:@selector(login:)];

View file

@ -6,6 +6,7 @@
@property (nonatomic, assign) IBOutlet NSImageView* backgroundImage;
@property (nonatomic, assign) IBOutlet NSImageView* smallLogo;
@property (nonatomic, assign) IBOutlet NSImageView* voxelImage;
@property (nonatomic, assign) IBOutlet NSTextField* buildVersion;
@end
@ -16,6 +17,7 @@
[self.backgroundImage setImage:[NSImage imageNamed:hifiBackgroundFilename]];
[self.smallLogo setImage:[NSImage imageNamed:hifiSmallLogoFilename]];
[self.voxelImage setImage:[NSImage imageNamed:hifiVoxelFilename]];
[self.buildVersion setStringValue: [@"V." stringByAppendingString:@LAUNCHER_BUILD_VERSION]];
}
-(IBAction)resartLauncher:(id)sender

View file

@ -12,6 +12,7 @@
@property (nonatomic, assign) IBOutlet NSTextField* smallHeader;
@property (nonatomic, assign) IBOutlet NSTextField* trouble;
@property (nonatomic, assign) IBOutlet NSButton* button;
@property (nonatomic, assign) IBOutlet NSTextField* buildVersion;
@end
@implementation LoginScreen
@ -36,6 +37,7 @@
[self.button setTitle:@"TRY AGAIN"];
}
[self.buildVersion setStringValue: [@"V." stringByAppendingString:@LAUNCHER_BUILD_VERSION]];
[self.backgroundImage setImage:[NSImage imageNamed:hifiBackgroundFilename]];
[self.smallLogo setImage:[NSImage imageNamed:hifiSmallLogoFilename]];

View file

@ -9,6 +9,7 @@
@property (nonatomic, assign) IBOutlet NSTextField* boldStatus;
@property (nonatomic, assign) IBOutlet NSTextField* smallStatus;
@property (nonatomic, assign) IBOutlet NSProgressIndicator* progressView;
@property (nonatomic, assign) IBOutlet NSTextField* buildVersion;
@end
@implementation ProcessScreen
@ -37,6 +38,7 @@
default:
break;
}
[self.buildVersion setStringValue: [@"V." stringByAppendingString:@LAUNCHER_BUILD_VERSION]];
[self.background setImage: [NSImage imageNamed:hifiBackgroundFilename]];
[self.smallLogo setImage: [NSImage imageNamed:hifiSmallLogoFilename]];
[self.voxelImage setImage: [NSImage imageNamed:hifiVoxelFilename]];

View file

@ -6,6 +6,7 @@
@property (nonatomic, assign) IBOutlet NSImageView* imageView;
@property (nonatomic, assign) IBOutlet NSImageView* logoImage;
@property (nonatomic, assign) IBOutlet NSButton* button;
@property (nonatomic, assign) IBOutlet NSTextField* buildVersion;
@end
@implementation SplashScreen
@ -15,5 +16,6 @@
-(void)awakeFromNib {
[self.imageView setImage:[NSImage imageNamed:hifiBackgroundFilename]];
[self.logoImage setImage:[NSImage imageNamed:hifiLargeLogoFilename]];
[self.buildVersion setStringValue: [@"V." stringByAppendingString:@LAUNCHER_BUILD_VERSION]];
}
@end

View file

@ -10,6 +10,7 @@ set(CMAKE_MFC_FLAG 1)
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MT")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MTd")
include("cmake/macros/SetPackagingParameters.cmake")
add_executable(HQLauncher
WIN32
@ -49,6 +50,8 @@ function(set_from_env _RESULT_NAME _ENV_VAR_NAME _DEFAULT_VALUE)
endif()
endfunction()
set_packaging_parameters()
set_from_env(LAUNCHER_HMAC_SECRET LAUNCHER_HMAC_SECRET "")
if (LAUNCHER_HMAC_SECRET STREQUAL "")
@ -56,7 +59,7 @@ if (LAUNCHER_HMAC_SECRET STREQUAL "")
endif()
target_compile_definitions(${PROJECT_NAME} PRIVATE LAUNCHER_HMAC_SECRET="${LAUNCHER_HMAC_SECRET}")
target_compile_definitions(${PROJECT_NAME} PRIVATE LAUNCHER_BUILD_VERSION="${BUILD_VERSION}")
# Preprocessor definitions
target_compile_definitions(HQLauncher PRIVATE

View file

@ -92,8 +92,8 @@ STYLE DS_SETFONT | DS_FIXEDSYS | WS_POPUP | WS_VISIBLE | WS_CAPTION
EXSTYLE WS_EX_APPWINDOW
FONT 10, "MS Shell Dlg", 400, 0, 0x0
BEGIN
CONTROL "",IDC_VOXEL,"Static",SS_BLACKRECT,65,3,174,123, NOT WS_VISIBLE
CONTROL "", IDC_PROGRESS, "Static", SS_BLACKRECT, 35, 170, 239, 4, NOT WS_VISIBLE
CONTROL "",IDC_VOXEL,"Static",SS_BLACKRECT,65,3,174,123
CONTROL "",IDC_PROGRESS,"Static",SS_BLACKRECT,35,170,239,4
EDITTEXT IDC_ORGNAME,44,68,219,12,ES_AUTOHSCROLL | NOT WS_VISIBLE | NOT WS_BORDER
EDITTEXT IDC_USERNAME,44,95,219,12,ES_AUTOHSCROLL | NOT WS_VISIBLE | NOT WS_BORDER
EDITTEXT IDC_PASSWORD,44,122,219,12,ES_PASSWORD | ES_AUTOHSCROLL | NOT WS_VISIBLE | NOT WS_BORDER
@ -107,6 +107,7 @@ BEGIN
RTEXT "",IDC_TERMS,15,172,180,15,NOT WS_VISIBLE
CONTROL "",IDC_TERMS_LINK,"Button",BS_OWNERDRAW | BS_FLAT | NOT WS_VISIBLE | WS_TABSTOP,197,172,80,15
CTEXT "",IDC_TROUBLE,65,203,174,15,NOT WS_VISIBLE
RTEXT "",IDC_VERSION,100,205,205,10
CONTROL "NEXT",IDC_BUTTON_NEXT,"Button",BS_OWNERDRAW | BS_FLAT | NOT WS_VISIBLE | WS_TABSTOP,107,158,94,16
CONTROL "Having Trouble?",IDC_TROUBLE_LINK,"Button",BS_OWNERDRAW | BS_FLAT | NOT WS_VISIBLE | WS_TABSTOP,126,203,56,11
END

View file

@ -39,16 +39,27 @@ BOOL CLauncherApp::InitInstance() {
}
int iNumOfArgs;
LPWSTR* pArgs = CommandLineToArgvW(GetCommandLine(), &iNumOfArgs);
bool isUninstalling = false;
bool isRestarting = false;
bool uninstalling = false;
bool restarting = false;
bool noUpdate = false;
LauncherManager::ContinueActionOnStart continueAction = LauncherManager::ContinueActionOnStart::ContinueNone;
if (iNumOfArgs > 1) {
if (CString(pArgs[1]).Compare(_T("--uninstall")) == 0) {
isUninstalling = true;
} else if (CString(pArgs[1]).Compare(_T("--restart")) == 0) {
isRestarting = true;
for (int i = 1; i < iNumOfArgs; i++) {
CString curArg = CString(pArgs[i]);
if (curArg.Compare(_T("--uninstall")) == 0) {
uninstalling = true;
} else if (curArg.Compare(_T("--restart")) == 0) {
restarting = true;
} else if (curArg.Compare(_T("--noUpdate")) == 0) {
noUpdate = true;
} else if (curArg.Compare(_T("--continueAction")) == 0) {
if (i + 1 < iNumOfArgs) {
continueAction = LauncherManager::getContinueActionFromParam(pArgs[i + 1]);
}
}
}
}
if (!isRestarting) {
if (!restarting) {
// don't launch if already running
CreateMutex(NULL, TRUE, _T("HQ_Launcher_Mutex"));
if (GetLastError() == ERROR_ALREADY_EXISTS) {
@ -56,10 +67,10 @@ BOOL CLauncherApp::InitInstance() {
}
}
if (isUninstalling) {
if (uninstalling) {
_manager.uninstall();
} else {
_manager.init();
_manager.init(!noUpdate, continueAction);
}
if (!_manager.hasFailed() && !_manager.installLauncher()) {
return FALSE;

View file

@ -40,6 +40,8 @@ static CString GRAPHIK_SEMIBOLD = _T("Graphik-Semibold");
static CString TROUBLE_URL = _T("https://www.highfidelity.com/hq-support");
static CString TERMS_URL = _T("https://www.highfidelity.com/termsofservice");
static int SPLASH_DURATION = 100;
CLauncherDlg::CLauncherDlg(CWnd* pParent)
: CDialog(IDD_LAUNCHER_DIALOG, pParent)
@ -112,6 +114,11 @@ BOOL CLauncherDlg::OnInitDialog() {
m_voxel = (CStatic *)GetDlgItem(IDC_VOXEL);
m_progress = (CStatic *)GetDlgItem(IDC_PROGRESS);
m_version = (CStatic *)GetDlgItem(IDC_VERSION);
CString version;
version.Format(_T("V.%s"), theApp._manager.getLauncherVersion());
m_version->SetWindowTextW(version);
m_voxel->EnableD2DSupport();
m_progress->EnableD2DSupport();
@ -230,7 +237,6 @@ void CLauncherDlg::startProcess() {
theApp._manager.setFailed(true);
}
});
}
BOOL CLauncherDlg::getHQInfo(const CString& orgname) {
@ -322,11 +328,12 @@ void CLauncherDlg::drawLogo(CHwndRenderTarget* pRenderTarget) {
void CLauncherDlg::drawSmallLogo(CHwndRenderTarget* pRenderTarget) {
CD2DBitmap m_pBitmamLogo(pRenderTarget, IDB_PNG5, _T("PNG"));
auto size = pRenderTarget->GetSize();
int padding = 6;
int xPadding = 6;
int yPadding = 22;
int logoWidth = 100;
int logoHeight = 18;
float logoPosX = size.width - logoWidth - padding;
float logoPosY = size.height - logoHeight - padding;
float logoPosX = size.width - logoWidth - xPadding;
float logoPosY = size.height - logoHeight - yPadding;
CD2DRectF logoRec(logoPosX, logoPosY, logoPosX + logoWidth, logoPosY + logoHeight);
pRenderTarget->DrawBitmap(&m_pBitmamLogo, logoRec);
}
@ -350,7 +357,7 @@ void CLauncherDlg::drawVoxel(CHwndRenderTarget* pRenderTarget) {
}
void CLauncherDlg::drawProgress(CHwndRenderTarget* pRenderTarget, float progress, const D2D1::ColorF& color) {
auto size = pRenderTarget->GetPixelSize();
auto size = pRenderTarget->GetSize();
if (progress == 0.0f) {
return;
} else {
@ -521,6 +528,7 @@ BOOL CLauncherDlg::getTextFormat(int resID, TextFormat& formatOut) {
formatOut.size = FIELDS_FONT_SIZE;
formatOut.color = COLOR_GREY;
break;
case IDC_VERSION:
case IDC_TERMS:
formatOut.size = TERMS_FONT_SIZE;
break;
@ -663,16 +671,50 @@ void CLauncherDlg::OnTimer(UINT_PTR nIDEvent) {
// Refresh
setDrawDialog(_drawStep, true);
}
if (theApp._manager.needsSelfUpdate()) {
if (theApp._manager.needsSelfDownload()) {
theApp._manager.downloadNewLauncher();
} else {
if (_splashStep > SPLASH_DURATION && _splashStep < 2 * SPLASH_DURATION) {
float progress = (float)(_splashStep - SPLASH_DURATION) / SPLASH_DURATION;
if (theApp._manager.willContinueUpdating()) {
progress = CONTINUE_UPDATING_GLOBAL_OFFSET * progress;
progress = min(progress, CONTINUE_UPDATING_GLOBAL_OFFSET);
}
theApp._manager.updateProgress(LauncherManager::ProcessType::Uninstall, progress);
_splashStep++;
}
if (theApp._manager.needsRestartNewLauncher()) {
if (_splashStep >= 2 * SPLASH_DURATION) {
theApp._manager.restartNewLauncher();
exit(0);
}
}
}
}
LauncherManager::ContinueActionOnStart continueAction = theApp._manager.getContinueAction();
if (_showSplash) {
if (_splashStep == 0) {
if (theApp._manager.needsUninstall()) {
theApp._manager.addToLog(_T("Waiting to uninstall"));
setDrawDialog(DrawStep::DrawProcessUninstall);
} else if (continueAction == LauncherManager::ContinueActionOnStart::ContinueUpdate) {
setDrawDialog(DrawStep::DrawProcessUpdate);
theApp._manager.updateProgress(LauncherManager::ProcessType::Uninstall, 0.0f);
} else if (continueAction == LauncherManager::ContinueActionOnStart::ContinueLogIn) {
_splashStep = SPLASH_DURATION;
} else if (continueAction == LauncherManager::ContinueActionOnStart::ContinueFinish) {
theApp._manager.updateProgress(LauncherManager::ProcessType::Uninstall, 1.0f);
setDrawDialog(DrawStep::DrawProcessFinishUpdate);
_splashStep = SPLASH_DURATION;
_showSplash = false;
} else {
theApp._manager.addToLog(_T("Start splash screen"));
setDrawDialog(DrawStep::DrawLogo);
}
} else if (_splashStep > 100 && !theApp._manager.needsToWait()) {
} else if (_splashStep > SPLASH_DURATION && !theApp._manager.needsToWait()) {
_showSplash = false;
if (theApp._manager.shouldShutDown()) {
if (_applicationWND != NULL) {
@ -692,12 +734,14 @@ void CLauncherDlg::OnTimer(UINT_PTR nIDEvent) {
theApp._manager.addToLog(_T("HQ failed to uninstall."));
theApp._manager.setFailed(true);
}
} else if (theApp._manager.needsSelfUpdate()) {
setDrawDialog(DrawStep::DrawProcessUpdate);
} else {
theApp._manager.addToLog(_T("Starting login"));
setDrawDialog(DrawStep::DrawLoginLogin);
}
} else if (theApp._manager.needsUninstall()) {
theApp._manager.updateProgress(LauncherManager::ProcessType::Uninstall, (float)_splashStep/100);
theApp._manager.updateProgress(LauncherManager::ProcessType::Uninstall, (float)_splashStep / SPLASH_DURATION);
}
_splashStep++;
} else if (theApp._manager.shouldShutDown()) {
@ -741,12 +785,17 @@ void CLauncherDlg::setDrawDialog(DrawStep step, BOOL isUpdate) {
auto m_voxelRenderTarget = m_voxel->GetRenderTarget();
auto m_progressRenderTarget = m_progress->GetRenderTarget();
switch (_drawStep) {
case DrawStep::DrawLogo:
case DrawStep::DrawLogo: {
m_pRenderTarget->BeginDraw();
drawBackground(m_pRenderTarget);
drawLogo(m_pRenderTarget);
m_pRenderTarget->EndDraw();
CRect redrawRec;
GetClientRect(redrawRec);
redrawRec.top = redrawRec.bottom - 30;
RedrawWindow(redrawRec);
break;
}
case DrawStep::DrawLoginLogin:
case DrawStep::DrawLoginErrorOrg:
case DrawStep::DrawLoginErrorCred:

View file

@ -94,6 +94,8 @@ protected:
CStatic* m_username_banner;
CStatic* m_password_banner;
CStatic* m_version;
HWND _applicationWND { 0 };
void drawBackground(CHwndRenderTarget* pRenderTarget);

View file

@ -16,17 +16,55 @@
LauncherManager::LauncherManager() {
int tokenPos = 0;
_launcherVersion = CString(LAUNCHER_BUILD_VERSION).Tokenize(_T("-"), tokenPos);
}
LauncherManager::~LauncherManager() {
}
void LauncherManager::init() {
void LauncherManager::init(BOOL allowUpdate, ContinueActionOnStart continueAction) {
initLog();
addToLog(_T("Getting most recent build"));
getMostRecentBuild(_latestApplicationURL, _latestVersion);
_updateLauncherAllowed = allowUpdate;
_continueAction = continueAction;
CString msg;
msg.Format(_T("Start Screen: %s"), getContinueActionParam(continueAction));
addToLog(msg);
_shouldWait = _continueAction == ContinueActionOnStart::ContinueNone;
if (_continueAction == ContinueActionOnStart::ContinueUpdate) {
_progressOffset = CONTINUE_UPDATING_GLOBAL_OFFSET;
}
addToLog(_T("Launcher is running version: " + _launcherVersion));
addToLog(_T("Getting most recent builds"));
getMostRecentBuilds(_latestLauncherURL, _latestLauncherVersion, _latestApplicationURL, _latestVersion);
}
CString LauncherManager::getContinueActionParam(LauncherManager::ContinueActionOnStart continueAction) {
switch (continueAction) {
case LauncherManager::ContinueActionOnStart::ContinueNone:
return _T("");
case LauncherManager::ContinueActionOnStart::ContinueLogIn:
return _T("LogIn");
case LauncherManager::ContinueActionOnStart::ContinueUpdate:
return _T("Update");
case LauncherManager::ContinueActionOnStart::ContinueFinish:
return _T("Finish");
default:
return _T("");
}
}
LauncherManager::ContinueActionOnStart LauncherManager::getContinueActionFromParam(const CString& param) {
if (param.Compare(_T("LogIn")) == 0) {
return ContinueActionOnStart::ContinueLogIn;
} else if (param.Compare(_T("Update")) == 0) {
return ContinueActionOnStart::ContinueUpdate;
} else if (param.Compare(_T("Finish")) == 0) {
return ContinueActionOnStart::ContinueFinish;
} else {
return ContinueActionOnStart::ContinueNone;
}
}
BOOL LauncherManager::initLog() {
CString logPath;
auto result = getAndCreatePaths(PathType::Launcher_Directory, logPath);
@ -124,6 +162,8 @@ BOOL LauncherManager::restartLauncher() {
void LauncherManager::updateProgress(ProcessType processType, float progress) {
switch (processType) {
case ProcessType::DownloadLauncher:
break;
case ProcessType::Uninstall:
_progress = progress;
break;
@ -153,6 +193,7 @@ void LauncherManager::updateProgress(ProcessType processType, float progress) {
default:
break;
}
_progress = _progressOffset + (1.0f - _progressOffset) * _progress;
TRACE("progress = %f\n", _progress);
}
@ -200,11 +241,11 @@ BOOL LauncherManager::isApplicationInstalled(CString& version, CString& domain,
CString applicationDir;
getAndCreatePaths(PathType::Launcher_Directory, applicationDir);
CString applicationPath = applicationDir + "interface\\interface.exe";
BOOL isApplicationInstalled = PathFileExistsW(applicationPath);
BOOL isInstalled = PathFileExistsW(applicationPath);
BOOL configFileExist = PathFileExistsW(applicationDir + _T("interface\\config.json"));
if (configFileExist) {
LauncherUtils::ResponseError status = readConfigJSON(version, domain, content, loggedIn);
return isApplicationInstalled && status == LauncherUtils::ResponseError::NoError;
return isInstalled && status == LauncherUtils::ResponseError::NoError;
}
return FALSE;
}
@ -359,14 +400,23 @@ LauncherUtils::ResponseError LauncherManager::readOrganizationJSON(const CString
return LauncherUtils::ResponseError::ParsingJSON;
}
void LauncherManager::getMostRecentBuild(CString& urlOut, CString& versionOut) {
void LauncherManager::getMostRecentBuilds(CString& launcherUrlOut, CString& launcherVersionOut,
CString& interfaceUrlOut, CString& interfaceVersionOut) {
CString contentTypeJson = L"content-type:application/json";
std::function<void(CString, int)> httpCallback = [&](CString response, int err) {
LauncherUtils::ResponseError error = LauncherUtils::ResponseError(err);
if (error == LauncherUtils::ResponseError::NoError) {
Json::Value json;
error = LauncherUtils::ResponseError::ParsingJSON;
if (LauncherUtils::parseJSON(response, json)) {
if (json["launcher"].isObject()) {
if (json["launcher"]["windows"].isObject() && json["launcher"]["windows"]["url"].isString()) {
launcherUrlOut = json["launcher"]["windows"]["url"].asCString();
}
if (json["launcher"]["version"].isInt()) {
std::string version = std::to_string(json["launcher"]["version"].asInt());
launcherVersionOut = CString(version.c_str());
}
}
int count = json["count"].isInt() ? json["count"].asInt() : 0;
if (count > 0 && json["results"].isArray()) {
for (int i = 0; i < count; i++) {
@ -374,20 +424,22 @@ void LauncherManager::getMostRecentBuild(CString& urlOut, CString& versionOut) {
Json::Value result = json["results"][i];
if (result["latest_version"].isInt()) {
std::string version = std::to_string(result["latest_version"].asInt());
versionOut = CString(version.c_str());
interfaceVersionOut = CString(version.c_str());
}
if (result["installers"].isObject() &&
result["installers"]["windows"].isObject() &&
result["installers"]["windows"]["zip_url"].isString()) {
urlOut = result["installers"]["windows"]["zip_url"].asCString();
error = LauncherUtils::ResponseError::NoError;
interfaceUrlOut = result["installers"]["windows"]["zip_url"].asCString();
}
}
}
}
if (launcherUrlOut.IsEmpty() || launcherVersionOut.IsEmpty() || interfaceUrlOut.IsEmpty() || interfaceVersionOut.IsEmpty()) {
error = LauncherUtils::ResponseError::ParsingJSON;
}
}
onMostRecentBuildReceived(response, error);
}
onMostRecentBuildsReceived(response, error);
};
LauncherUtils::httpCallOnThread(L"HQ Launcher",
L"thunder.highfidelity.com",
@ -395,31 +447,50 @@ void LauncherManager::getMostRecentBuild(CString& urlOut, CString& versionOut) {
contentTypeJson, CStringA(), false, httpCallback);
}
void LauncherManager::onMostRecentBuildReceived(const CString& response, LauncherUtils::ResponseError error) {
void LauncherManager::onMostRecentBuildsReceived(const CString& response, LauncherUtils::ResponseError error) {
if (error == LauncherUtils::ResponseError::NoError) {
addToLog(_T("Latest version: ") + _latestVersion);
addToLog(_T("Latest launcher version: ") + _latestLauncherVersion);
CString currentVersion;
if (isApplicationInstalled(currentVersion, _domainURL, _contentURL, _loggedIn) && _loggedIn) {
addToLog(_T("Installed version: ") + currentVersion);
if (_latestVersion.Compare(currentVersion) == 0) {
addToLog(_T("Already running most recent build. Launching interface.exe"));
_shouldLaunch = TRUE;
_shouldShutdown = TRUE;
} else {
addToLog(_T("New build found. Updating"));
_shouldUpdate = TRUE;
}
} else if (_loggedIn) {
addToLog(_T("Interface not found but logged in. Reinstalling"));
_shouldUpdate = TRUE;
BOOL isInstalled = (isApplicationInstalled(currentVersion, _domainURL, _contentURL, _loggedIn) && _loggedIn);
bool newInterfaceVersion = _latestVersion.Compare(currentVersion) != 0;
bool newLauncherVersion = _latestLauncherVersion.Compare(_launcherVersion) != 0 && _updateLauncherAllowed;
if (newLauncherVersion) {
CString updatingMsg;
updatingMsg.Format(_T("Updating Launcher from version: %s to version: %s"), _launcherVersion, _latestLauncherVersion);
addToLog(updatingMsg);
_shouldUpdateLauncher = TRUE;
_shouldDownloadLauncher = TRUE;
_keepLoggingIn = !isInstalled;
_keepUpdating = isInstalled && newInterfaceVersion;
} else {
_shouldInstall = TRUE;
if (_updateLauncherAllowed) {
addToLog(_T("Already running most recent build. Launching interface.exe"));
} else {
addToLog(_T("Updating the launcher was not allowed --noUpdate"));
}
if (isInstalled) {
addToLog(_T("Installed version: ") + currentVersion);
if (!newInterfaceVersion) {
addToLog(_T("Already running most recent build. Launching interface.exe"));
_shouldLaunch = TRUE;
_shouldShutdown = TRUE;
} else {
addToLog(_T("New build found. Updating"));
_shouldUpdate = TRUE;
}
} else if (_loggedIn) {
addToLog(_T("Interface not found but logged in. Reinstalling"));
_shouldUpdate = TRUE;
} else {
_shouldInstall = TRUE;
}
}
_shouldWait = FALSE;
} else {
_hasFailed = true;
CString msg;
msg.Format(_T("Getting most recent build has failed with error: %d"), error);
msg.Format(_T("Getting most recent builds has failed with error: %d"), error);
addToLog(msg);
msg.Format(_T("Response: %s"), response);
addToLog(msg);
@ -521,7 +592,7 @@ BOOL LauncherManager::extractApplication() {
}
};
std::function<void(float)> onProgress = [&](float progress) {
updateProgress(ProcessType::UnzipApplication, progress);
updateProgress(ProcessType::UnzipApplication, max(progress, 0.0f));
};
_currentProcess = ProcessType::UnzipApplication;
BOOL success = LauncherUtils::unzipFileOnThread(ProcessType::UnzipApplication,
@ -563,9 +634,25 @@ void LauncherManager::onFileDownloaded(ProcessType type) {
setFailed(true);
}
});
} else if (type == ProcessType::DownloadLauncher) {
_shouldRestartNewLauncher = true;
}
}
void LauncherManager::restartNewLauncher() {
closeLog();
ContinueActionOnStart continueAction = ContinueActionOnStart::ContinueFinish;
if (_keepUpdating) {
continueAction = ContinueActionOnStart::ContinueUpdate;
} else if (_keepLoggingIn) {
continueAction = ContinueActionOnStart::ContinueLogIn;
}
CStringW params;
params.Format(_T(" --restart --noUpdate --continueAction %s"), getContinueActionParam(continueAction));
LauncherUtils::launchApplication(_tempLauncherPath, params.GetBuffer());
Sleep(500);
}
BOOL LauncherManager::installContent() {
std::string contentZipFile = LauncherUtils::cStringToStd(_contentZipPath);
@ -582,7 +669,7 @@ BOOL LauncherManager::installContent() {
}
};
std::function<void(float)> onProgress = [&](float progress) {
updateProgress(ProcessType::UnzipContent, progress);
updateProgress(ProcessType::UnzipContent, max(progress, 0.0f));
};
_currentProcess = ProcessType::UnzipContent;
BOOL success = LauncherUtils::unzipFileOnThread(ProcessType::UnzipContent, contentZipFile,
@ -597,10 +684,13 @@ BOOL LauncherManager::installContent() {
BOOL LauncherManager::downloadFile(ProcessType type, const CString& url, CString& outPath) {
CString fileName = url.Mid(url.ReverseFind('/') + 1);
CString downloadDirectory;
BOOL success = getAndCreatePaths(LauncherManager::PathType::Download_Directory, downloadDirectory);
outPath = downloadDirectory + fileName;
BOOL success = TRUE;
if (outPath.IsEmpty()) {
CString fileName = url.Mid(url.ReverseFind('/') + 1);
CString downloadDirectory;
BOOL success = getAndCreatePaths(LauncherManager::PathType::Download_Directory, downloadDirectory);
outPath = downloadDirectory + fileName;
}
_currentProcess = type;
if (success) {
addToLog(_T("Downloading: ") + url);
@ -610,18 +700,18 @@ BOOL LauncherManager::downloadFile(ProcessType type, const CString& url, CString
} else {
if (type == ProcessType::DownloadApplication) {
addToLog(_T("Error downloading content."));
} else if (type == ProcessType::DownloadLauncher) {
addToLog(_T("Error downloading launcher."));
} else {
addToLog(_T("Error downloading application."));
}
_hasFailed = true;
}
};
std::function<void(float)> onProgress = [&](float progress) {
updateProgress(_currentProcess, progress);
std::function<void(float)> onProgress = [&, type](float progress) {
updateProgress(_currentProcess, max(progress, 0.0f));
};
if (!LauncherUtils::downloadFileOnThread(type, url, outPath, onDownloadFinished, onProgress)) {
success = FALSE;
}
success = LauncherUtils::downloadFileOnThread(type, url, outPath, onDownloadFinished, onProgress);
}
return success;
}
@ -637,6 +727,14 @@ BOOL LauncherManager::downloadApplication() {
return downloadFile(ProcessType::DownloadApplication, applicationURL, _applicationZipPath);
}
BOOL LauncherManager::downloadNewLauncher() {
_shouldDownloadLauncher = FALSE;
getAndCreatePaths(PathType::Temp_Directory, _tempLauncherPath);
CString tempName = _T("HQLauncher") + _launcherVersion + _T(".exe");
_tempLauncherPath += tempName;
return downloadFile(ProcessType::DownloadLauncher, _latestLauncherURL, _tempLauncherPath);
}
void LauncherManager::onCancel() {
if (_currentProcess == ProcessType::UnzipApplication) {
_latestVersion = _T("");

View file

@ -11,6 +11,7 @@
#pragma once
#include "LauncherUtils.h"
#include "LauncherDlg.h"
const CString DIRECTORY_NAME_APP = _T("HQ");
const CString DIRECTORY_NAME_DOWNLOADS = _T("downloads");
@ -25,9 +26,9 @@ const float DOWNLOAD_APPLICATION_INSTALL_WEIGHT = 0.5f;
const float EXTRACT_APPLICATION_INSTALL_WEIGHT = 0.2f;
const float DOWNLOAD_APPLICATION_UPDATE_WEIGHT = 0.75f;
const float EXTRACT_APPLICATION_UPDATE_WEIGHT = 0.25f;
const float CONTINUE_UPDATING_GLOBAL_OFFSET = 0.2f;
class LauncherManager
{
class LauncherManager {
public:
enum PathType {
Running_Path = 0,
@ -49,27 +50,40 @@ public:
ErrorIOFiles
};
enum ProcessType {
DownloadLauncher = 0,
DownloadContent,
DownloadApplication,
UnzipContent,
UnzipApplication,
Uninstall
};
enum ContinueActionOnStart {
ContinueNone = 0,
ContinueLogIn,
ContinueUpdate,
ContinueFinish
};
LauncherManager();
~LauncherManager();
void init();
void init(BOOL allowUpdate, ContinueActionOnStart continueAction);
static CString getContinueActionParam(ContinueActionOnStart continueAction);
static ContinueActionOnStart getContinueActionFromParam(const CString& param);
BOOL initLog();
BOOL addToLog(const CString& line);
void closeLog();
void saveErrorLog();
BOOL getAndCreatePaths(PathType type, CString& outPath);
BOOL getInstalledVersion(const CString& path, CString& version);
BOOL isApplicationInstalled(CString& version, CString& domain,
BOOL isApplicationInstalled(CString& version, CString& domain,
CString& content, bool& loggedIn);
LauncherUtils::ResponseError getAccessTokenForCredentials(const CString& username, const CString& password);
void getMostRecentBuild(CString& urlOut, CString& versionOut);
void getMostRecentBuilds(CString& launcherUrlOut,
CString& launcherVersionOut,
CString& interfaceUrlOut,
CString& interfaceVersionOut);
LauncherUtils::ResponseError readOrganizationJSON(const CString& hash);
LauncherUtils::ResponseError readConfigJSON(CString& version, CString& domain,
LauncherUtils::ResponseError readConfigJSON(CString& version, CString& domain,
CString& content, bool& loggedIn);
BOOL createConfigJSON();
BOOL createApplicationRegistryKeys(int size);
@ -87,33 +101,46 @@ public:
const CString& getVersion() const { return _version; }
BOOL shouldShutDown() const { return _shouldShutdown; }
BOOL shouldLaunch() const { return _shouldLaunch; }
BOOL needsUpdate() { return _shouldUpdate; }
BOOL needsUninstall() { return _shouldUninstall; }
BOOL needsInstall() { return _shouldInstall; }
BOOL needsToWait() { return _shouldWait; }
BOOL needsUpdate() const { return _shouldUpdate; }
BOOL needsSelfUpdate() const { return _shouldUpdateLauncher; }
BOOL needsSelfDownload() const { return _shouldDownloadLauncher; }
BOOL needsUninstall() const { return _shouldUninstall; }
BOOL needsInstall() const { return _shouldInstall; }
BOOL needsToWait() const { return _shouldWait; }
BOOL needsRestartNewLauncher() const { return _shouldRestartNewLauncher; }
BOOL willContinueUpdating() const { return _keepUpdating; }
ContinueActionOnStart getContinueAction() { return _continueAction; }
void setDisplayName(const CString& displayName) { _displayName = displayName; }
bool isLoggedIn() { return _loggedIn; }
bool hasFailed() { return _hasFailed; }
bool isLoggedIn() const { return _loggedIn; }
bool hasFailed() const { return _hasFailed; }
void setFailed(bool hasFailed) { _hasFailed = hasFailed; }
const CString& getLatestInterfaceURL() const { return _latestApplicationURL; }
void uninstall() { _shouldUninstall = true; _shouldWait = false; };
void uninstall() {
_shouldUninstall = true;
_shouldWait = false;
};
BOOL downloadFile(ProcessType type, const CString& url, CString& localPath);
BOOL downloadContent();
BOOL downloadApplication();
BOOL downloadNewLauncher();
BOOL installContent();
BOOL extractApplication();
void restartNewLauncher();
void onZipExtracted(ProcessType type, int size);
void onFileDownloaded(ProcessType type);
float getProgress() { return _progress; }
float getProgress() const { return _progress; }
void updateProgress(ProcessType processType, float progress);
void onCancel();
const CString& getLauncherVersion() const { return _launcherVersion; }
private:
ProcessType _currentProcess { ProcessType::DownloadApplication };
void onMostRecentBuildReceived(const CString& response, LauncherUtils::ResponseError error);
void onMostRecentBuildsReceived(const CString& response, LauncherUtils::ResponseError error);
CString _latestApplicationURL;
CString _latestVersion;
CString _latestLauncherURL;
CString _latestLauncherVersion;
CString _contentURL;
CString _domainURL;
CString _version;
@ -121,6 +148,8 @@ private:
CString _tokensJSON;
CString _applicationZipPath;
CString _contentZipPath;
CString _launcherVersion;
CString _tempLauncherPath;
bool _loggedIn { false };
bool _hasFailed { false };
BOOL _shouldUpdate { FALSE };
@ -129,7 +158,14 @@ private:
BOOL _shouldShutdown { FALSE };
BOOL _shouldLaunch { FALSE };
BOOL _shouldWait { TRUE };
BOOL _shouldUpdateLauncher { FALSE };
BOOL _shouldDownloadLauncher { FALSE };
BOOL _updateLauncherAllowed { TRUE };
BOOL _shouldRestartNewLauncher { FALSE };
BOOL _keepLoggingIn { FALSE };
BOOL _keepUpdating { FALSE };
ContinueActionOnStart _continueAction;
float _progressOffset { 0.0f };
float _progress { 0.0f };
CStdioFile _logFile;
};

View file

@ -0,0 +1,45 @@
#
# SetPackagingParameters.cmake
# cmake/macros
#
# Created by Leonardo Murillo on 07/14/2015.
# Copyright 2015 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
# This macro checks some Jenkins defined environment variables to determine the origin of this build
# and decides how targets should be packaged.
macro(SET_PACKAGING_PARAMETERS)
set(PR_BUILD 0)
set(PRODUCTION_BUILD 0)
set(DEV_BUILD 0)
set(BUILD_NUMBER 0)
set_from_env(RELEASE_TYPE RELEASE_TYPE "DEV")
set_from_env(RELEASE_NUMBER RELEASE_NUMBER "")
set_from_env(STABLE_BUILD STABLE_BUILD 0)
message(STATUS "The RELEASE_TYPE variable is: ${RELEASE_TYPE}")
set(BUILD_NUMBER ${RELEASE_NUMBER})
if (RELEASE_TYPE STREQUAL "PRODUCTION")
set(PRODUCTION_BUILD 1)
set(BUILD_VERSION ${RELEASE_NUMBER})
# add definition for this release type
add_definitions(-DPRODUCTION_BUILD)
elseif (RELEASE_TYPE STREQUAL "PR")
set(PR_BUILD 1)
set(BUILD_VERSION "PR${RELEASE_NUMBER}")
# add definition for this release type
add_definitions(-DPR_BUILD)
else ()
set(DEV_BUILD 1)
set(BUILD_VERSION "dev")
endif ()
endmacro(SET_PACKAGING_PARAMETERS)

View file

@ -27,6 +27,7 @@
#define IDC_TROUBLE 1023
#define IDC_VOXEL 1024
#define IDC_PROGRESS 1025
#define IDC_VERSION 1026
#define IDC_TROUBLE_LINK 1027
// Next default values for new objects

View file

@ -1092,6 +1092,11 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos
_desiredStateAge = 0.0f;
}
_desiredState = RigRole::Takeoff;
} else if (ccState == CharacterControllerState::Seated) {
if (_desiredState != RigRole::Seated) {
_desiredStateAge = 0.0f;
}
_desiredState = RigRole::Seated;
} else {
float moveThresh;
if (_state != RigRole::Move) {
@ -1216,6 +1221,7 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos
_animVars.set("isInAirStand", false);
_animVars.set("isInAirRun", false);
_animVars.set("isNotInAir", true);
_animVars.set("isSeated", false);
} else if (_state == RigRole::Turn) {
if (turningSpeed > 0.0f) {
@ -1244,6 +1250,7 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos
_animVars.set("isInAirStand", false);
_animVars.set("isInAirRun", false);
_animVars.set("isNotInAir", true);
_animVars.set("isSeated", false);
} else if (_state == RigRole::Idle) {
// default anim vars to notMoving and notTurning
@ -1265,6 +1272,7 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos
_animVars.set("isInAirStand", false);
_animVars.set("isInAirRun", false);
_animVars.set("isNotInAir", true);
_animVars.set("isSeated", false);
} else if (_state == RigRole::Hover) {
// flying.
@ -1286,6 +1294,7 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos
_animVars.set("isInAirStand", false);
_animVars.set("isInAirRun", false);
_animVars.set("isNotInAir", true);
_animVars.set("isSeated", false);
} else if (_state == RigRole::Takeoff) {
// jumping in-air
@ -1315,6 +1324,7 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos
_animVars.set("isInAirStand", false);
_animVars.set("isInAirRun", false);
_animVars.set("isNotInAir", false);
_animVars.set("isSeated", false);
} else if (_state == RigRole::InAir) {
// jumping in-air
@ -1333,6 +1343,7 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos
_animVars.set("isTakeoffStand", false);
_animVars.set("isTakeoffRun", false);
_animVars.set("isNotTakeoff", true);
_animVars.set("isSeated", false);
bool inAirRun = forwardSpeed > 0.1f;
if (inAirRun) {
@ -1354,6 +1365,26 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos
float alpha = glm::clamp((-workingVelocity.y * sensorToWorldScale) / jumpSpeed, -1.0f, 1.0f) + 1.0f;
_animVars.set("inAirAlpha", alpha);
} else if (_state == RigRole::Seated) {
_animVars.set("isMovingForward", false);
_animVars.set("isMovingBackward", false);
_animVars.set("isMovingRight", false);
_animVars.set("isMovingLeft", false);
_animVars.set("isMovingRightHmd", false);
_animVars.set("isMovingLeftHmd", false);
_animVars.set("isNotMoving", false);
_animVars.set("isTurningRight", false);
_animVars.set("isTurningLeft", false);
_animVars.set("isNotTurning", true);
_animVars.set("isFlying", false);
_animVars.set("isNotFlying", true);
_animVars.set("isTakeoffStand", false);
_animVars.set("isTakeoffRun", false);
_animVars.set("isNotTakeoff", true);
_animVars.set("isInAirStand", false);
_animVars.set("isInAirRun", false);
_animVars.set("isNotInAir", true);
_animVars.set("isSeated", true);
}
t += deltaTime;
@ -1873,6 +1904,61 @@ void Rig::updateFeet(bool leftFootEnabled, bool rightFootEnabled, bool headEnabl
}
}
void Rig::updateReactions(const ControllerParameters& params) {
// enable/disable animVars
bool enabled = params.reactionEnabledFlags[AVATAR_REACTION_POSITIVE];
_animVars.set("reactionPositiveEnabled", enabled);
_animVars.set("reactionPositiveDisabled", !enabled);
enabled = params.reactionEnabledFlags[AVATAR_REACTION_NEGATIVE];
_animVars.set("reactionNegativeEnabled", enabled);
_animVars.set("reactionNegativeDisabled", !enabled);
enabled = params.reactionEnabledFlags[AVATAR_REACTION_RAISE_HAND];
_animVars.set("reactionRaiseHandEnabled", enabled);
_animVars.set("reactionRaiseHandDisabled", !enabled);
enabled = params.reactionEnabledFlags[AVATAR_REACTION_APPLAUD];
_animVars.set("reactionApplaudEnabled", enabled);
_animVars.set("reactionApplaudDisabled", !enabled);
enabled = params.reactionEnabledFlags[AVATAR_REACTION_POINT];
_animVars.set("reactionPointEnabled", enabled);
_animVars.set("reactionPointDisabled", !enabled);
// trigger animVars
if (params.reactionTriggers[AVATAR_REACTION_POSITIVE]) {
_animVars.set("reactionPositiveTrigger", true);
} else {
_animVars.set("reactionPositiveTrigger", false);
}
if (params.reactionTriggers[AVATAR_REACTION_NEGATIVE]) {
_animVars.set("reactionNegativeTrigger", true);
} else {
_animVars.set("reactionNegativeTrigger", false);
}
if (params.reactionTriggers[AVATAR_REACTION_RAISE_HAND]) {
_animVars.set("reactionRaiseHandTrigger", true);
} else {
_animVars.set("reactionRaiseHandTrigger", false);
}
if (params.reactionTriggers[AVATAR_REACTION_APPLAUD]) {
_animVars.set("reactionApplaudTrigger", true);
} else {
_animVars.set("reactionApplaudTrigger", false);
}
if (params.reactionTriggers[AVATAR_REACTION_POINT]) {
_animVars.set("reactionPointTrigger", true);
} else {
_animVars.set("reactionPointTrigger", false);
}
}
void Rig::updateEyeJoint(int index, const glm::vec3& modelTranslation, const glm::quat& modelRotation, const glm::vec3& lookAtSpot, const glm::vec3& saccade) {
// TODO: does not properly handle avatar scale.
@ -2152,6 +2238,8 @@ void Rig::updateFromControllerParameters(const ControllerParameters& params, flo
}
}
updateReactions(params);
_previousControllerParameters = params;
}

View file

@ -88,6 +88,8 @@ public:
AnimPose secondaryControllerPoses[NumSecondaryControllerTypes]; // rig space
uint8_t secondaryControllerFlags[NumSecondaryControllerTypes];
bool isTalking;
bool reactionEnabledFlags[NUM_AVATAR_REACTIONS];
bool reactionTriggers[NUM_AVATAR_REACTIONS];
HFMJointShapeInfo hipsShapeInfo;
HFMJointShapeInfo spineShapeInfo;
HFMJointShapeInfo spine1ShapeInfo;
@ -107,7 +109,8 @@ public:
Ground = 0,
Takeoff,
InAir,
Hover
Hover,
Seated
};
Rig();
@ -268,6 +271,7 @@ protected:
void updateFeet(bool leftFootEnabled, bool rightFootEnabled, bool headEnabled,
const AnimPose& leftFootPose, const AnimPose& rightFootPose,
const glm::mat4& rigToSensorMatrix, const glm::mat4& sensorToRigMatrix);
void updateReactions(const ControllerParameters& params);
void updateEyeJoint(int index, const glm::vec3& modelTranslation, const glm::quat& modelRotation, const glm::vec3& lookAt, const glm::vec3& saccade);
void calcAnimAlpha(float speed, const std::vector<float>& referenceSpeeds, float* alphaOut) const;
@ -336,7 +340,8 @@ protected:
Move,
Hover,
Takeoff,
InAir
InAir,
Seated
};
RigRole _state { RigRole::Idle };
RigRole _desiredState { RigRole::Idle };

View file

@ -7,6 +7,11 @@ link_hifi_libraries(audio plugins)
include_hifi_library_headers(shared)
include_hifi_library_headers(networking)
if (ANDROID)
else ()
target_webrtc()
endif ()
# append audio includes to our list of includes to bubble
target_include_directories(${TARGET_NAME} PUBLIC "${HIFI_LIBRARY_DIR}/audio/src")

View file

@ -24,7 +24,7 @@
#endif
#ifdef WIN32
#define WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN 1
#include <windows.h>
#include <Mmsystem.h>
#include <mmdeviceapi.h>
@ -286,6 +286,7 @@ AudioClient::AudioClient() :
_shouldEchoLocally(false),
_shouldEchoToServer(false),
_isNoiseGateEnabled(true),
_isAECEnabled(true),
_reverb(false),
_reverbOptions(&_scriptReverbOptions),
_inputToNetworkResampler(NULL),
@ -302,6 +303,7 @@ AudioClient::AudioClient() :
_isHeadsetPluggedIn(false),
#endif
_orientationGetter(DEFAULT_ORIENTATION_GETTER) {
// avoid putting a lock in the device callback
assert(_localSamplesAvailable.is_lock_free());
@ -353,6 +355,10 @@ AudioClient::AudioClient() :
configureReverb();
#if defined(WEBRTC_ENABLED)
configureWebrtc();
#endif
auto nodeList = DependencyManager::get<NodeList>();
auto& packetReceiver = nodeList->getPacketReceiver();
packetReceiver.registerListener(PacketType::AudioStreamStats, &_stats, "processStreamStatsPacket");
@ -1084,6 +1090,131 @@ void AudioClient::setReverbOptions(const AudioEffectOptions* options) {
}
}
#if defined(WEBRTC_ENABLED)
static void deinterleaveToFloat(const int16_t* src, float* const* dst, int numFrames, int numChannels) {
for (int i = 0; i < numFrames; i++) {
for (int ch = 0; ch < numChannels; ch++) {
float f = *src++;
f *= (1/32768.0f); // scale
dst[ch][i] = f; // deinterleave
}
}
}
static void interleaveToInt16(const float* const* src, int16_t* dst, int numFrames, int numChannels) {
for (int i = 0; i < numFrames; i++) {
for (int ch = 0; ch < numChannels; ch++) {
float f = src[ch][i];
f *= 32768.0f; // scale
f += (f < 0.0f) ? -0.5f : 0.5f; // round
f = std::max(std::min(f, 32767.0f), -32768.0f); // saturate
*dst++ = (int16_t)f; // interleave
}
}
}
void AudioClient::configureWebrtc() {
_apm = webrtc::AudioProcessingBuilder().Create();
webrtc::AudioProcessing::Config config;
config.pre_amplifier.enabled = false;
config.high_pass_filter.enabled = false;
config.echo_canceller.enabled = true;
config.echo_canceller.mobile_mode = false;
config.echo_canceller.use_legacy_aec = false;
config.noise_suppression.enabled = false;
config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kModerate;
config.voice_detection.enabled = false;
config.gain_controller1.enabled = false;
config.gain_controller2.enabled = false;
config.gain_controller2.fixed_digital.gain_db = 0.0f;
config.gain_controller2.adaptive_digital.enabled = false;
config.residual_echo_detector.enabled = true;
config.level_estimation.enabled = false;
_apm->ApplyConfig(config);
}
// rebuffer into 10ms chunks
void AudioClient::processWebrtcFarEnd(const int16_t* samples, int numFrames, int numChannels, int sampleRate) {
const webrtc::StreamConfig streamConfig = webrtc::StreamConfig(sampleRate, numChannels);
const int numChunk = (int)streamConfig.num_frames();
if (sampleRate > WEBRTC_SAMPLE_RATE_MAX) {
qCWarning(audioclient) << "WebRTC does not support" << sampleRate << "output sample rate.";
return;
}
if (numChannels > WEBRTC_CHANNELS_MAX) {
qCWarning(audioclient) << "WebRTC does not support" << numChannels << "output channels.";
return;
}
while (numFrames > 0) {
// number of frames to fill
int numFill = std::min(numFrames, numChunk - _numFifoFarEnd);
// refill fifo
memcpy(&_fifoFarEnd[_numFifoFarEnd], samples, numFill * numChannels * sizeof(int16_t));
samples += numFill * numChannels;
numFrames -= numFill;
_numFifoFarEnd += numFill;
if (_numFifoFarEnd == numChunk) {
// convert audio format
float buffer[WEBRTC_CHANNELS_MAX][WEBRTC_FRAMES_MAX];
float* const buffers[WEBRTC_CHANNELS_MAX] = { buffer[0], buffer[1] };
deinterleaveToFloat(_fifoFarEnd, buffers, numChunk, numChannels);
// process one chunk
int error = _apm->ProcessReverseStream(buffers, streamConfig, streamConfig, buffers);
if (error != _apm->kNoError) {
qCWarning(audioclient) << "WebRTC ProcessReverseStream() returned ERROR:" << error;
}
_numFifoFarEnd = 0;
}
}
}
void AudioClient::processWebrtcNearEnd(int16_t* samples, int numFrames, int numChannels, int sampleRate) {
const webrtc::StreamConfig streamConfig = webrtc::StreamConfig(sampleRate, numChannels);
const int numChunk = (int)streamConfig.num_frames();
if (sampleRate > WEBRTC_SAMPLE_RATE_MAX) {
qCWarning(audioclient) << "WebRTC does not support" << sampleRate << "input sample rate.";
return;
}
if (numChannels > WEBRTC_CHANNELS_MAX) {
qCWarning(audioclient) << "WebRTC does not support" << numChannels << "input channels.";
return;
}
if (numFrames != numChunk) {
qCWarning(audioclient) << "WebRTC requires exactly 10ms of input.";
return;
}
// convert audio format
float buffer[WEBRTC_CHANNELS_MAX][WEBRTC_FRAMES_MAX];
float* const buffers[WEBRTC_CHANNELS_MAX] = { buffer[0], buffer[1] };
deinterleaveToFloat(samples, buffers, numFrames, numChannels);
// process one chunk
int error = _apm->ProcessStream(buffers, streamConfig, streamConfig, buffers);
if (error != _apm->kNoError) {
qCWarning(audioclient) << "WebRTC ProcessStream() returned ERROR:" << error;
} else {
// modify samples in-place
interleaveToInt16(buffers, samples, numFrames, numChannels);
}
}
#endif // WEBRTC_ENABLED
void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) {
// If there is server echo, reverb will be applied to the recieved audio stream so no need to have it here.
bool hasReverb = _reverb || _receivedAudioStream.hasReverb();
@ -1262,6 +1393,13 @@ void AudioClient::handleMicAudioInput() {
_inputRingBuffer.readSamples(inputAudioSamples.get(), inputSamplesRequired);
#if defined(WEBRTC_ENABLED)
if (_isAECEnabled) {
processWebrtcNearEnd(inputAudioSamples.get(), inputSamplesRequired / _inputFormat.channelCount(),
_inputFormat.channelCount(), _inputFormat.sampleRate());
}
#endif
// detect loudness and clipping on the raw input
bool isClipping = false;
float loudness = computeLoudness(inputAudioSamples.get(), inputSamplesRequired, _inputFormat.channelCount(), isClipping);
@ -1574,6 +1712,15 @@ void AudioClient::setWarnWhenMuted(bool enable, bool emitSignal) {
}
}
void AudioClient::setAcousticEchoCancellation(bool enable, bool emitSignal) {
if (_isAECEnabled != enable) {
_isAECEnabled = enable;
if (emitSignal) {
emit acousticEchoCancellationChanged(_isAECEnabled);
}
}
}
bool AudioClient::setIsStereoInput(bool isStereoInput) {
bool stereoInputChanged = false;
if (isStereoInput != _isStereoInput && _inputDeviceInfo.supportedChannelCounts().contains(2)) {
@ -2107,15 +2254,16 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) {
return maxSize;
}
// samples requested from OUTPUT_CHANNEL_COUNT
// max samples requested from OUTPUT_CHANNEL_COUNT
int deviceChannelCount = _audio->_outputFormat.channelCount();
int samplesRequested = (int)(maxSize / AudioConstants::SAMPLE_SIZE) * OUTPUT_CHANNEL_COUNT / deviceChannelCount;
int maxSamplesRequested = (int)(maxSize / AudioConstants::SAMPLE_SIZE) * OUTPUT_CHANNEL_COUNT / deviceChannelCount;
// restrict samplesRequested to the size of our mix/scratch buffers
samplesRequested = std::min(samplesRequested, _audio->_outputPeriod);
maxSamplesRequested = std::min(maxSamplesRequested, _audio->_outputPeriod);
int16_t* scratchBuffer = _audio->_outputScratchBuffer;
float* mixBuffer = _audio->_outputMixBuffer;
int samplesRequested = maxSamplesRequested;
int networkSamplesPopped;
if ((networkSamplesPopped = _receivedAudioStream.popSamples(samplesRequested, false)) > 0) {
qCDebug(audiostream, "Read %d samples from buffer (%d available, %d requested)", networkSamplesPopped, _receivedAudioStream.getSamplesAvailable(), samplesRequested);
@ -2160,45 +2308,45 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) {
});
int samplesPopped = std::max(networkSamplesPopped, injectorSamplesPopped);
int framesPopped = samplesPopped / AudioConstants::STEREO;
int bytesWritten;
if (samplesPopped > 0) {
// apply output gain
float newGain = _audio->_outputGain.load(std::memory_order_acquire);
float oldGain = _audio->_lastOutputGain;
_audio->_lastOutputGain = newGain;
applyGainSmoothing<OUTPUT_CHANNEL_COUNT>(mixBuffer, framesPopped, oldGain, newGain);
if (deviceChannelCount == OUTPUT_CHANNEL_COUNT) {
// limit the audio
_audio->_audioLimiter.render(mixBuffer, (int16_t*)data, framesPopped);
} else {
_audio->_audioLimiter.render(mixBuffer, scratchBuffer, framesPopped);
// upmix or downmix to deviceChannelCount
if (deviceChannelCount > OUTPUT_CHANNEL_COUNT) {
int extraChannels = deviceChannelCount - OUTPUT_CHANNEL_COUNT;
channelUpmix(scratchBuffer, (int16_t*)data, samplesPopped, extraChannels);
} else {
channelDownmix(scratchBuffer, (int16_t*)data, samplesPopped);
}
}
bytesWritten = framesPopped * AudioConstants::SAMPLE_SIZE * deviceChannelCount;
assert(bytesWritten <= maxSize);
} else {
// nothing on network, don't grab anything from injectors, and just return 0s
memset(data, 0, maxSize);
bytesWritten = maxSize;
if (samplesPopped == 0) {
// nothing on network, don't grab anything from injectors, and fill with silence
samplesPopped = maxSamplesRequested;
memset(mixBuffer, 0, samplesPopped * sizeof(float));
}
int framesPopped = samplesPopped / OUTPUT_CHANNEL_COUNT;
// apply output gain
float newGain = _audio->_outputGain.load(std::memory_order_acquire);
float oldGain = _audio->_lastOutputGain;
_audio->_lastOutputGain = newGain;
applyGainSmoothing<OUTPUT_CHANNEL_COUNT>(mixBuffer, framesPopped, oldGain, newGain);
// limit the audio
_audio->_audioLimiter.render(mixBuffer, scratchBuffer, framesPopped);
#if defined(WEBRTC_ENABLED)
if (_audio->_isAECEnabled) {
_audio->processWebrtcFarEnd(scratchBuffer, framesPopped, OUTPUT_CHANNEL_COUNT, _audio->_outputFormat.sampleRate());
}
#endif
// if required, upmix or downmix to deviceChannelCount
if (deviceChannelCount == OUTPUT_CHANNEL_COUNT) {
memcpy(data, scratchBuffer, samplesPopped * AudioConstants::SAMPLE_SIZE);
} else if (deviceChannelCount > OUTPUT_CHANNEL_COUNT) {
int extraChannels = deviceChannelCount - OUTPUT_CHANNEL_COUNT;
channelUpmix(scratchBuffer, (int16_t*)data, samplesPopped, extraChannels);
} else {
channelDownmix(scratchBuffer, (int16_t*)data, samplesPopped);
}
int bytesWritten = framesPopped * AudioConstants::SAMPLE_SIZE * deviceChannelCount;
assert(bytesWritten <= maxSize);
// send output buffer for recording
if (_audio->_isRecording) {
Lock lock(_recordMutex);
_audio->_audioFileWav.addRawAudioChunk(reinterpret_cast<char*>(scratchBuffer), bytesWritten);
_audio->_audioFileWav.addRawAudioChunk(data, bytesWritten);
}
int bytesAudioOutputUnplayed = _audio->_audioOutput->bufferSize() - _audio->_audioOutput->bytesFree();

View file

@ -29,6 +29,7 @@
#include <AbstractAudioInterface.h>
#include <AudioEffectOptions.h>
#include <AudioStreamStats.h>
#include <shared/WebRTC.h>
#include <DependencyManager.h>
#include <HifiSockAddr.h>
@ -215,6 +216,9 @@ public slots:
void setWarnWhenMuted(bool isNoiseGateEnabled, bool emitSignal = true);
bool isWarnWhenMutedEnabled() const { return _warnWhenMuted; }
void setAcousticEchoCancellation(bool isAECEnabled, bool emitSignal = true);
bool isAcousticEchoCancellationEnabled() const { return _isAECEnabled; }
virtual bool getLocalEcho() override { return _shouldEchoLocally; }
virtual void setLocalEcho(bool localEcho) override { _shouldEchoLocally = localEcho; }
virtual void toggleLocalEcho() override { _shouldEchoLocally = !_shouldEchoLocally; }
@ -256,6 +260,7 @@ signals:
void muteToggled(bool muted);
void noiseReductionChanged(bool noiseReductionEnabled);
void warnWhenMutedChanged(bool warnWhenMutedEnabled);
void acousticEchoCancellationChanged(bool acousticEchoCancellationEnabled);
void mutedByMixer();
void inputReceived(const QByteArray& inputSamples);
void inputLoudnessChanged(float loudness, bool isClipping);
@ -377,6 +382,7 @@ private:
bool _shouldEchoToServer;
bool _isNoiseGateEnabled;
bool _warnWhenMuted;
bool _isAECEnabled;
bool _reverb;
AudioEffectOptions _scriptReverbOptions;
@ -414,9 +420,23 @@ private:
// Adds Reverb
void configureReverb();
void updateReverbOptions();
void handleLocalEchoAndReverb(QByteArray& inputByteArray);
#if defined(WEBRTC_ENABLED)
static const int WEBRTC_SAMPLE_RATE_MAX = 96000;
static const int WEBRTC_CHANNELS_MAX = 2;
static const int WEBRTC_FRAMES_MAX = webrtc::AudioProcessing::kChunkSizeMs * WEBRTC_SAMPLE_RATE_MAX / 1000;
webrtc::AudioProcessing* _apm { nullptr };
int16_t _fifoFarEnd[WEBRTC_CHANNELS_MAX * WEBRTC_FRAMES_MAX] {};
int _numFifoFarEnd = 0; // numFrames saved in fifo
void configureWebrtc();
void processWebrtcFarEnd(const int16_t* samples, int numFrames, int numChannels, int sampleRate);
void processWebrtcNearEnd(int16_t* samples, int numFrames, int numChannels, int sampleRate);
#endif
bool switchInputToAudioDevice(const QAudioDeviceInfo inputDeviceInfo, bool isShutdownRequest = false);
bool switchOutputToAudioDevice(const QAudioDeviceInfo outputDeviceInfo, bool isShutdownRequest = false);

View file

@ -22,6 +22,7 @@
#include "HTTPManager.h"
const char* HTTPConnection::StatusCode200 = "200 OK";
const char* HTTPConnection::StatusCode204 = "204 No Content";
const char* HTTPConnection::StatusCode301 = "301 Moved Permanently";
const char* HTTPConnection::StatusCode302 = "302 Found";
const char* HTTPConnection::StatusCode400 = "400 Bad Request";

View file

@ -45,6 +45,7 @@ class HTTPConnection : public QObject {
public:
static const char* StatusCode200;
static const char* StatusCode204;
static const char* StatusCode301;
static const char* StatusCode302;
static const char* StatusCode400;

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