combine settings and setup into single tab

This commit is contained in:
Stephen Birarda 2014-09-25 16:32:09 -07:00
parent 2e176589b7
commit 33a411b895
9 changed files with 252 additions and 261 deletions

View file

@ -6,11 +6,11 @@ setup_hifi_project(Network)
# remove and then copy the files for the webserver
add_custom_command(TARGET ${TARGET_NAME} POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E remove_directory
$<TARGET_FILE_DIR:${TARGET_NAME}>/resources/web)
$<TARGET_FILE_DIR:${TARGET_NAME}>/resources)
add_custom_command(TARGET ${TARGET_NAME} POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E copy_directory
"${PROJECT_SOURCE_DIR}/resources/web"
$<TARGET_FILE_DIR:${TARGET_NAME}>/resources/web)
"${PROJECT_SOURCE_DIR}/resources"
$<TARGET_FILE_DIR:${TARGET_NAME}>/resources)
# link the shared hifi libraries
link_hifi_libraries(embedded-webserver networking shared)

View file

@ -1,74 +1,119 @@
{
"audio": {
[
{
"name": "metaverse",
"label": "Metaverse Registration",
"settings": [
{
"name": "access-token",
"label": "High Fidelity Access Token",
"help": "This is an access token generated on the <a href='https://data.highfidelity.io/tokens'>My Tokens</a> page of your High Fidelity account.<br/>Generate a token with the 'domains' scope and paste it here.<br/>This is required to associate this domain-server with a domain in your account."
},
{
"name": "id",
"label": "Domain ID",
"help": "This is your High Fidelity domain ID. If you do not want your domain to be registered in the High Fidelity metaverse you can leave this blank."
}
]
},
{
"name": "basic",
"label": "Basic",
"settings": [
{
"name": "http-username",
"label": "HTTP Username",
"help": "Username used for basic HTTP authentication"
},
{
"name": "http-password",
"label": "HTTP Password",
"type": "password",
"help": "Password used for basic HTTP authentication"
}
]
},
{
"name": "audio",
"label": "Audio",
"assignment-types": [0],
"settings": {
"A-dynamic-jitter-buffer": {
"settings": [
{
"name": "enable-filter",
"type": "checkbox",
"label": "Dynamic Jitter Buffers",
"help": "Dynamically buffer client audio based on perceived jitter in packet receipt timing",
"label": "Enable Positional Filter",
"help": "If enabled, positional audio stream uses lowpass filter",
"default": false
},
"B-static-desired-jitter-buffer-frames": {
"label": "Static Desired Jitter Buffer Frames",
"help": "If dynamic jitter buffers is disabled, this determines the target number of frames maintained by the AudioMixer's jitter buffers",
"placeholder": "1",
"default": "1"
},
"C-max-frames-over-desired": {
"label": "Max Frames Over Desired",
"help": "The highest number of frames an AudioMixer's ringbuffer can exceed the desired jitter buffer frames by",
"placeholder": "10",
"default": "10"
},
"D-use-stdev-for-desired-calc": {
"type": "checkbox",
"label": "Use Stdev for Desired Jitter Frames Calc:",
"help": "If checked, Philip's method (stdev of timegaps) is used to calculate desired jitter frames. Otherwise, Fred's method (max timegap) is used",
"default": false
},
"E-window-starve-threshold": {
"label": "Window Starve Threshold",
"help": "If this many starves occur in an N-second window (N is the number in the next field), then the desired jitter frames will be re-evaluated using Window A.",
"placeholder": "3",
"default": "3"
},
"F-window-seconds-for-desired-calc-on-too-many-starves": {
"label": "Timegaps Window (A) Seconds:",
"help": "Window A contains a history of timegaps. Its max timegap is used to re-evaluate the desired jitter frames when too many starves occur within it.",
"placeholder": "50",
"default": "50"
},
"G-window-seconds-for-desired-reduction": {
"label": "Timegaps Window (B) Seconds:",
"help": "Window B contains a history of timegaps. Its max timegap is used as a ceiling for the desired jitter frames value.",
"placeholder": "10",
"default": "10"
},
"H-repetition-with-fade": {
"type": "checkbox",
"label": "Repetition with Fade:",
"help": "If enabled, dropped frames and mixing during starves will repeat the last frame, eventually fading to silence",
"default": false
},
"I-print-stream-stats": {
"type": "checkbox",
"label": "Print Stream Stats:",
"help": "If enabled, audio upstream and downstream stats of each agent will be printed each second to stdout",
"default": false
},
"Z-unattenuated-zone": {
{
"name": "unattenuated-zone",
"label": "Unattenuated Zone",
"help": "Boxes for source and listener (corner x, corner y, corner z, size x, size y, size z, corner x, corner y, corner z, size x, size y, size z)",
"placeholder": "no zone",
"default": ""
},
"J-enable-filter": {
"type": "checkbox",
"label": "Enable Positional Filter",
"help": "If enabled, positional audio stream uses lowpass filter",
{
"name": "dynamic-jitter-buffer",
"type": "checkbox",
"label": "Dynamic Jitter Buffers",
"help": "Dynamically buffer client audio based on perceived jitter in packet receipt timing",
"default": false
}
}
},
{
"name": "static-desired-jitter-buffer-frames",
"label": "Static Desired Jitter Buffer Frames",
"help": "If dynamic jitter buffers is disabled, this determines the target number of frames maintained by the AudioMixer's jitter buffers",
"placeholder": "1",
"default": "1"
},
{
"name": "max-frames-over-desired",
"label": "Max Frames Over Desired",
"help": "The highest number of frames an AudioMixer's ringbuffer can exceed the desired jitter buffer frames by",
"placeholder": "10",
"default": "10"
},
{
"name": "use-stdev-for-desired-calc",
"type": "checkbox",
"label": "Use Stdev for Desired Jitter Frames Calc:",
"help": "If checked, Philip's method (stdev of timegaps) is used to calculate desired jitter frames. Otherwise, Fred's method (max timegap) is used",
"default": false
},
{
"name": "window-starve-threshold",
"label": "Window Starve Threshold",
"help": "If this many starves occur in an N-second window (N is the number in the next field), then the desired jitter frames will be re-evaluated using Window A.",
"placeholder": "3",
"default": "3"
},
{
"name": "window-seconds-for-desired-calc-on-too-many-starves",
"label": "Timegaps Window (A) Seconds:",
"help": "Window A contains a history of timegaps. Its max timegap is used to re-evaluate the desired jitter frames when too many starves occur within it.",
"placeholder": "50",
"default": "50"
},
{
"name": "window-seconds-for-desired-reduction",
"label": "Timegaps Window (B) Seconds:",
"help": "Window B contains a history of timegaps. Its max timegap is used as a ceiling for the desired jitter frames value.",
"placeholder": "10",
"default": "10"
},
{
"name": "repetition-with-fade",
"type": "checkbox",
"label": "Repetition with Fade:",
"help": "If enabled, dropped frames and mixing during starves will repeat the last frame, eventually fading to silence",
"default": false
},
{
"name": "I-print-stream-stats",
"type": "checkbox",
"label": "Print Stream Stats:",
"help": "If enabled, audio upstream and downstream stats of each agent will be printed each second to stdout",
"default": false
}
]
}
}
]

View file

@ -32,8 +32,6 @@
</ul>
</li>
<li><a href="/settings/">Settings</a></li>
<li><a href="/setup/">Setup</a></li>
</ul>
</div>
</div><!-- /.container-fluid -->

View file

@ -1,15 +1,40 @@
var Settings = {};
$(document).ready(function(){
var source = $('#settings-template').html();
Settings.template = _.template(source);
/*
* Clamped-width.
* Usage:
* <div data-clampedwidth=".myParent">This long content will force clamped width</div>
*
* Author: LV
*/
$('[data-clampedwidth]').each(function () {
var elem = $(this);
var parentPanel = elem.data('clampedwidth');
var resizeFn = function () {
var sideBarNavWidth = $(parentPanel).width() - parseInt(elem.css('paddingLeft')) - parseInt(elem.css('paddingRight')) - parseInt(elem.css('marginLeft')) - parseInt(elem.css('marginRight')) - parseInt(elem.css('borderLeftWidth')) - parseInt(elem.css('borderRightWidth'));
elem.css('width', sideBarNavWidth);
};
resizeFn();
$(window).resize(resizeFn);
});
var panelsSource = $('#panels-template').html();
Settings.panelsTemplate = _.template(panelsSource);
var sidebarTemplate = $('#list-group-template').html();
Settings.sidebarTemplate = _.template(sidebarTemplate)
reloadSettings();
});
function reloadSettings() {
$.getJSON('/settings.json', function(data){
$('#settings').html(Settings.template(data));
$('.list-group').html(Settings.sidebarTemplate(data))
$('#panels').html(Settings.panelsTemplate(data));
});
}

View file

@ -1,50 +1,73 @@
<!--#include virtual="header.html"-->
<div id="settings-lead" class="table-lead"><h3>Settings</h3><div class="lead-line"></div></div>
<div style="clear: both;"></div>
<div class="alert" style="display:none;"></div>
<form class="form-horizontal" id="settings-form" role="form">
<div class="col-md-10 col-md-offset-1">
<div class="col-md-12">
<div class="alert" style="display:none;"></div>
</div>
<script id="settings-template" type="text/template">
<% _.each(descriptions, function(group, group_key){ %>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><%- group.label %></h3>
<div class="col-md-3 col-sm-3" id="setup-sidebar-col">
<div id="setup-sidebar" data-clampedwidth="#setup-sidebar-col" class="hidden-xs" data-spy="affix" data-offset-top="50">
<script id="list-group-template" type="text/template">
<% _.each(descriptions, function(group){ %>
<a href="" class="list-group-item">
<%- group.label %>
</a>
<% }); %>
</script>
<div class="list-group">
</div>
<div class="panel-body">
<% _.each(group.settings, function(setting, setting_key){ %>
<div class="form-group">
<% var setting_id = group_key + "." + setting_key %>
<label for="<%- setting_id %>" class="col-sm-2 control-label"><%- setting.label %></label>
<div class="col-sm-10">
<% if (setting.type === "checkbox") { %>
<% var checked_box = _.has(values, group_key) ? values[group_key][setting_key] : setting.default %>
<input type="checkbox" id="<%- setting_id %>" <%- checked_box ? "checked" : "" %>>
<% } else { %>
<% if (setting.input_addon) { %>
<div class="input-group">
<div class="input-group-addon"><%- setting.input_addon %></div>
<% } %>
<input type="text" class="form-control" id="<%- setting_id %>"
placeholder="<%- setting.placeholder %>"
value="<%- (values[group_key] || {})[setting_key] %>">
<% if (setting.input_addon) { %>
</div>
<% } %>
<% } %>
<button id="save-button" class="btn btn-success">Save and restart</button>
</div>
</div>
<div class="col-md-9 col-sm-9">
<form class="form-horizontal" id="settings-form" role="form">
<script id="panels-template" type="text/template">
<% _.each(descriptions, function(group){ %>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><%- group.label %></h3>
</div>
<div class="panel-body">
<% _.each(group.settings, function(setting){ %>
<div class="form-group">
<% var setting_id = group.name + "." + setting.name %>
<label for="<%- setting_id %>" class="col-sm-2 control-label"><%- setting.label %></label>
<div class="col-sm-10">
<% if (setting.type === "checkbox") { %>
<% var checked_box = _.has(values, group.name) ? values[group.name][setting.name] : setting.default %>
<input type="checkbox" id="<%- setting_id %>" <%- checked_box ? "checked" : "" %>>
<% } else { %>
<% if (setting.input_addon) { %>
<div class="input-group">
<div class="input-group-addon"><%- setting.input_addon %></div>
<% } %>
<input type="text" class="form-control" id="<%- setting_id %>"
placeholder="<%- setting.placeholder %>"
value="<%- (values[group.name] || {})[setting.name] %>">
<% if (setting.input_addon) { %>
</div>
<% } %>
<% } %>
</div>
<p class="help-block col-sm-offset-2 col-sm-10"><%- setting.help %></p>
</div>
<% }); %>
</div>
<p class="help-block col-sm-offset-2 col-sm-10"><%- setting.help %></p>
</div>
<% }); %>
</div>
</div>
<% }); %>
<button type="submit" class="btn btn-default">Save</button>
</script>
</script>
<div id="panels"></div>
</form>
</div>
</div>
<div id="settings"></div>
</form>
<!--#include virtual="footer.html"-->
<script src='/js/settings.js'></script>
<script src='/js/form2js.min.js'></script>

View file

@ -1,31 +0,0 @@
{
"descriptions": {
"basic": {
"label": "Basic",
"settings": {
"http-username": {
"label": "HTTP Username",
"help": "Username used for basic HTTP authentication"
},
"http-password": {
"label": "HTTP Password",
"type": "password",
"help": "Password used for basic HTTP authentication"
}
}
},
"metaverse": {
"label": "Metaverse Registration",
"settings": {
"access-token": {
"label": "High Fidelity Access Token",
"help": "This is an access token generated on the <a href='https://data.highfidelity.io/tokens'>My Tokens</a> page of your High Fidelity account.<br/>Generate a token with the 'domains' scope and paste it here.<br/>This is required to associate this domain-server with a domain in your account."
},
"id": {
"label": "Domain ID",
"help": "This is your High Fidelity domain ID. If you do not want your domain to be registered in the High Fidelity metaverse you can leave this blank."
}
}
}
}
}

View file

@ -1,75 +0,0 @@
<!--#include virtual="header.html"-->
<div class="col-md-10 col-md-offset-1">
<div class="col-md-3 col-sm-3" id="setup-sidebar-col">
<div id="setup-sidebar" data-clampedwidth="#setup-sidebar-col" class="hidden-xs" data-spy="affix" data-offset-top="50">
<div class="list-group">
<script id="list-group-template" type="text/template">
<% _.each(descriptions, function(group, group_key){ %>
<a href="" class="list-group-item">
<%- group.label %>
</a>
<% }); %>
</script>
</div>
<button id="save-button" class="btn btn-success">Save and restart</button>
</div>
</div>
<div class="col-md-9 col-sm-9">
<form id="setup-form" role="form">
<div class="panel panel-default" id="metaverse">
<div class="panel-heading">
<h3 class="panel-title">Metaverse Registration</h3>
</div>
<div class="panel-body">
<div class="form-group">
<label for="token" class="control-label">High Fidelity Access Token</label>
<input type="text" class="form-control" id="token" name="token">
<span class="help-block">
This is an access token generated on the <a href="https://data.highfidelity.io/tokens">My Tokens</a> page of your High Fidelity account.<br/>
Generate a token with the 'domains' scope and paste it here.<br/>
This is required to associate this domain-server with a domain in your.
</span>
</div>
<div class="form-group">
<label for="name" class="control-label">Domain ID</label>
<input type="text" class="form-control" id="domain_id" name="domain_id">
<span class="help-block">This is your High Fidelity domain ID. If you do not want your domain to be registered in the High Fidelity metaverse you can leave this blank.</span>
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#choose-domain-modal">Choose from my domains</button>
</div>
</div>
<div>
</div>
</form>
</div>
</div>
<div class="modal fade" id="choose-domain-modal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
<h4 class="modal-title">Choose from my domains</h4>
</div>
<div class="modal-body">
<div class="form-group">
<select id="domain_id" name="domain_id" class="form-control">
</select>
</div>
<a class="btn btn-primary" href="https://data.highfidelity.io/domains/new" target="_blank">Create new domain</a>
<button type="button" class="btn btn-default">Refresh domain list</button>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success">Choose domain</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!--#include virtual="footer.html"-->
<script src='/js/setup.js'></script>
<script src='/js/underscore-1.5.0.min.js'></script>
<!--#include virtual="page-end.html"-->

View file

@ -21,18 +21,18 @@
#include "DomainServerSettingsManager.h"
const QString SETTINGS_DESCRIPTION_RELATIVE_PATH = "/resources/web/settings/describe-settings.json";
const QString SETTINGS_DESCRIPTION_RELATIVE_PATH = "/resources/describe-settings.json";
const QString SETTINGS_JSON_FILE_RELATIVE_PATH = "/resources/settings.json";
DomainServerSettingsManager::DomainServerSettingsManager() :
_descriptionObject(),
_descriptionArray(),
_settingsMap()
{
// load the description object from the settings description
QFile descriptionFile(QCoreApplication::applicationDirPath() + SETTINGS_DESCRIPTION_RELATIVE_PATH);
descriptionFile.open(QIODevice::ReadOnly);
_descriptionObject = QJsonDocument::fromJson(descriptionFile.readAll()).object();
_descriptionArray = QJsonDocument::fromJson(descriptionFile.readAll()).array();
// load the existing config file to get the current values
QFile configFile(QCoreApplication::applicationDirPath() + SETTINGS_JSON_FILE_RELATIVE_PATH);
@ -46,6 +46,7 @@ DomainServerSettingsManager::DomainServerSettingsManager() :
const QString DESCRIPTION_SETTINGS_KEY = "settings";
const QString SETTING_DEFAULT_KEY = "default";
const QString SETTINGS_GROUP_KEY_NAME = "key";
bool DomainServerSettingsManager::handlePublicHTTPRequest(HTTPConnection* connection, const QUrl &url) {
if (connection->requestOperation() == QNetworkAccessManager::GetOperation && url.path() == "/settings.json") {
@ -60,7 +61,7 @@ bool DomainServerSettingsManager::handlePublicHTTPRequest(HTTPConnection* connec
if (typeValue.isEmpty()) {
// combine the description object and our current settings map
responseObject["descriptions"] = _descriptionObject;
responseObject["descriptions"] = _descriptionArray;
responseObject["values"] = QJsonDocument::fromVariant(_settingsMap).object();
} else {
// convert the string type value to a QJsonValue
@ -69,8 +70,9 @@ bool DomainServerSettingsManager::handlePublicHTTPRequest(HTTPConnection* connec
const QString AFFECTED_TYPES_JSON_KEY = "assignment-types";
// enumerate the groups in the description object to find which settings to pass
foreach(const QString& group, _descriptionObject.keys()) {
QJsonObject groupObject = _descriptionObject[group].toObject();
foreach(const QJsonValue& groupValue, _descriptionArray) {
QJsonObject groupObject = groupValue.toObject();
QString groupKey = groupObject[SETTINGS_GROUP_KEY_NAME].toString();
QJsonObject groupSettingsObject = groupObject[DESCRIPTION_SETTINGS_KEY].toObject();
QJsonObject groupResponseObject;
@ -89,7 +91,7 @@ bool DomainServerSettingsManager::handlePublicHTTPRequest(HTTPConnection* connec
// we need to check if the settings map has a value for this setting
QVariant variantValue;
QVariant settingsMapGroupValue = _settingsMap.value(group);
QVariant settingsMapGroupValue = _settingsMap.value(groupObject[SETTINGS_GROUP_KEY_NAME].toString());
if (!settingsMapGroupValue.isNull()) {
variantValue = settingsMapGroupValue.toMap().value(settingKey);
@ -106,7 +108,7 @@ bool DomainServerSettingsManager::handlePublicHTTPRequest(HTTPConnection* connec
if (!groupResponseObject.isEmpty()) {
// set this group's object to the constructed object
responseObject[group] = groupResponseObject;
responseObject[groupKey] = groupResponseObject;
}
}
@ -126,7 +128,7 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection
QJsonObject postedObject = postedDocument.object();
// we recurse one level deep below each group for the appropriate setting
recurseJSONObjectAndOverwriteSettings(postedObject, _settingsMap, _descriptionObject);
recurseJSONObjectAndOverwriteSettings(postedObject, _settingsMap, _descriptionArray);
// store whatever the current _settingsMap is to file
persistToFile();
@ -145,46 +147,49 @@ const QString SETTING_DESCRIPTION_TYPE_KEY = "type";
void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject,
QVariantMap& settingsVariant,
QJsonObject descriptionObject) {
QJsonArray descriptionArray) {
foreach(const QString& key, postedObject.keys()) {
QJsonValue rootValue = postedObject[key];
// we don't continue if this key is not present in our descriptionObject
if (descriptionObject.contains(key)) {
if (rootValue.isString()) {
if (rootValue.toString().isEmpty()) {
// this is an empty value, clear it in settings variant so the default is sent
settingsVariant.remove(key);
} else {
if (descriptionObject[key].toObject().contains(SETTING_DESCRIPTION_TYPE_KEY)) {
// for now this means that this is a double, so set it as a double
settingsVariant[key] = rootValue.toString().toDouble();
} else {
settingsVariant[key] = rootValue.toString();
}
}
} else if (rootValue.isBool()) {
settingsVariant[key] = rootValue.toBool();
} else if (rootValue.isObject()) {
// there's a JSON Object to explore, so attempt to recurse into it
QJsonObject nextDescriptionObject = descriptionObject[key].toObject();
if (nextDescriptionObject.contains(DESCRIPTION_SETTINGS_KEY)) {
if (!settingsVariant.contains(key)) {
// we don't have a map below this key yet, so set it up now
settingsVariant[key] = QVariantMap();
}
QVariantMap& thisMap = *reinterpret_cast<QVariantMap*>(settingsVariant[key].data());
recurseJSONObjectAndOverwriteSettings(rootValue.toObject(),
thisMap,
nextDescriptionObject[DESCRIPTION_SETTINGS_KEY].toObject());
if (thisMap.isEmpty()) {
// we've cleared all of the settings below this value, so remove this one too
foreach(const QJsonValue& groupValue, descriptionArray) {
if (groupValue.toObject()[SETTINGS_GROUP_KEY_NAME].toString() == key) {
QJsonObject groupObject = groupValue.toObject();
if (rootValue.isString()) {
if (rootValue.toString().isEmpty()) {
// this is an empty value, clear it in settings variant so the default is sent
settingsVariant.remove(key);
} else {
if (groupObject.contains(SETTING_DESCRIPTION_TYPE_KEY)) {
// for now this means that this is a double, so set it as a double
settingsVariant[key] = rootValue.toString().toDouble();
} else {
settingsVariant[key] = rootValue.toString();
}
}
} else if (rootValue.isBool()) {
settingsVariant[key] = rootValue.toBool();
} else if (rootValue.isObject()) {
// there's a JSON Object to explore, so attempt to recurse into it
QJsonObject nextDescriptionObject = groupObject;
if (nextDescriptionObject.contains(DESCRIPTION_SETTINGS_KEY)) {
if (!settingsVariant.contains(key)) {
// we don't have a map below this key yet, so set it up now
settingsVariant[key] = QVariantMap();
}
QVariantMap& thisMap = *reinterpret_cast<QVariantMap*>(settingsVariant[key].data());
recurseJSONObjectAndOverwriteSettings(rootValue.toObject(),
thisMap,
nextDescriptionObject[DESCRIPTION_SETTINGS_KEY].toArray());
if (thisMap.isEmpty()) {
// we've cleared all of the settings below this value, so remove this one too
settingsVariant.remove(key);
}
}
}
}

View file

@ -12,6 +12,7 @@
#ifndef hifi_DomainServerSettingsManager_h
#define hifi_DomainServerSettingsManager_h
#include <QtCore/QJsonArray>
#include <QtCore/QJsonDocument>
#include <HTTPManager.h>
@ -26,10 +27,10 @@ public:
QByteArray getJSONSettingsMap() const;
private:
void recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject, QVariantMap& settingsVariant,
QJsonObject descriptionObject);
QJsonArray descriptionArray);
void persistToFile();
QJsonObject _descriptionObject;
QJsonArray _descriptionArray;
QVariantMap _settingsMap;
};