18-05-26 result
This commit is contained in:
+670
-24
@@ -1,14 +1,313 @@
|
||||
#include "AppConfig.hpp"
|
||||
|
||||
#include <cctype>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
std::optional<PresentModeConfig> parsePresentMode(
|
||||
std::string_view text);
|
||||
|
||||
namespace
|
||||
{
|
||||
struct JsonValue
|
||||
{
|
||||
using Object = std::map<std::string, JsonValue>;
|
||||
using Array = std::vector<JsonValue>;
|
||||
|
||||
std::variant<
|
||||
std::nullptr_t,
|
||||
bool,
|
||||
double,
|
||||
std::string,
|
||||
Object,
|
||||
Array> value;
|
||||
};
|
||||
|
||||
class JsonParser
|
||||
{
|
||||
public:
|
||||
explicit JsonParser(std::string_view text)
|
||||
: mText(text)
|
||||
{
|
||||
}
|
||||
|
||||
std::optional<JsonValue> parse()
|
||||
{
|
||||
JsonValue result;
|
||||
if (!parseValue(result))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
skipWhitespace();
|
||||
if (mPos != mText.size())
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
void skipWhitespace()
|
||||
{
|
||||
while (mPos < mText.size() &&
|
||||
std::isspace(
|
||||
static_cast<unsigned char>(mText[mPos])))
|
||||
{
|
||||
++mPos;
|
||||
}
|
||||
}
|
||||
|
||||
bool consume(char expected)
|
||||
{
|
||||
skipWhitespace();
|
||||
if (mPos >= mText.size() ||
|
||||
mText[mPos] != expected)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
++mPos;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parseValue(JsonValue& out)
|
||||
{
|
||||
skipWhitespace();
|
||||
if (mPos >= mText.size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const char c = mText[mPos];
|
||||
if (c == '{')
|
||||
{
|
||||
return parseObject(out);
|
||||
}
|
||||
if (c == '[')
|
||||
{
|
||||
return parseArray(out);
|
||||
}
|
||||
if (c == '"')
|
||||
{
|
||||
std::string s;
|
||||
if (!parseString(s))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
out.value = std::move(s);
|
||||
return true;
|
||||
}
|
||||
if (c == '-' || (c >= '0' && c <= '9'))
|
||||
{
|
||||
return parseNumber(out);
|
||||
}
|
||||
if (mText.substr(mPos, 4) == "true")
|
||||
{
|
||||
mPos += 4;
|
||||
out.value = true;
|
||||
return true;
|
||||
}
|
||||
if (mText.substr(mPos, 5) == "false")
|
||||
{
|
||||
mPos += 5;
|
||||
out.value = false;
|
||||
return true;
|
||||
}
|
||||
if (mText.substr(mPos, 4) == "null")
|
||||
{
|
||||
mPos += 4;
|
||||
out.value = nullptr;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool parseObject(JsonValue& out)
|
||||
{
|
||||
if (!consume('{'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
JsonValue::Object object;
|
||||
skipWhitespace();
|
||||
if (mPos < mText.size() && mText[mPos] == '}')
|
||||
{
|
||||
++mPos;
|
||||
out.value = std::move(object);
|
||||
return true;
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
std::string key;
|
||||
if (!parseString(key) || !consume(':'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
JsonValue value;
|
||||
if (!parseValue(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
object[std::move(key)] = std::move(value);
|
||||
|
||||
skipWhitespace();
|
||||
if (mPos < mText.size() && mText[mPos] == '}')
|
||||
{
|
||||
++mPos;
|
||||
out.value = std::move(object);
|
||||
return true;
|
||||
}
|
||||
if (!consume(','))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool parseArray(JsonValue& out)
|
||||
{
|
||||
if (!consume('['))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
JsonValue::Array array;
|
||||
skipWhitespace();
|
||||
if (mPos < mText.size() && mText[mPos] == ']')
|
||||
{
|
||||
++mPos;
|
||||
out.value = std::move(array);
|
||||
return true;
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
JsonValue value;
|
||||
if (!parseValue(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
array.push_back(std::move(value));
|
||||
|
||||
skipWhitespace();
|
||||
if (mPos < mText.size() && mText[mPos] == ']')
|
||||
{
|
||||
++mPos;
|
||||
out.value = std::move(array);
|
||||
return true;
|
||||
}
|
||||
if (!consume(','))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool parseString(std::string& out)
|
||||
{
|
||||
skipWhitespace();
|
||||
if (mPos >= mText.size() || mText[mPos] != '"')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
++mPos;
|
||||
out.clear();
|
||||
|
||||
while (mPos < mText.size())
|
||||
{
|
||||
const char c = mText[mPos++];
|
||||
if (c == '"')
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (c != '\\')
|
||||
{
|
||||
out.push_back(c);
|
||||
continue;
|
||||
}
|
||||
if (mPos >= mText.size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const char esc = mText[mPos++];
|
||||
switch (esc)
|
||||
{
|
||||
case '"': out.push_back('"'); break;
|
||||
case '\\': out.push_back('\\'); break;
|
||||
case '/': out.push_back('/'); break;
|
||||
case 'b': out.push_back('\b'); break;
|
||||
case 'f': out.push_back('\f'); break;
|
||||
case 'n': out.push_back('\n'); break;
|
||||
case 'r': out.push_back('\r'); break;
|
||||
case 't': out.push_back('\t'); break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool parseNumber(JsonValue& out)
|
||||
{
|
||||
skipWhitespace();
|
||||
const size_t start = mPos;
|
||||
|
||||
if (mPos < mText.size() && mText[mPos] == '-')
|
||||
{
|
||||
++mPos;
|
||||
}
|
||||
while (mPos < mText.size() &&
|
||||
mText[mPos] >= '0' && mText[mPos] <= '9')
|
||||
{
|
||||
++mPos;
|
||||
}
|
||||
if (mPos < mText.size() && mText[mPos] == '.')
|
||||
{
|
||||
++mPos;
|
||||
while (mPos < mText.size() &&
|
||||
mText[mPos] >= '0' && mText[mPos] <= '9')
|
||||
{
|
||||
++mPos;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
out.value = std::stod(
|
||||
std::string(mText.substr(start, mPos - start))
|
||||
);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string_view mText;
|
||||
size_t mPos = 0;
|
||||
};
|
||||
|
||||
bool parseUint32(
|
||||
const char* text,
|
||||
uint32_t& value)
|
||||
@@ -94,6 +393,278 @@ void applyDefaultFeeds(AppConfig& config)
|
||||
{
|
||||
config.feeds.assign(feedCount(config), FeedConfig{});
|
||||
}
|
||||
|
||||
void resizeFeedsForGrid(AppConfig& config)
|
||||
{
|
||||
config.feeds.resize(feedCount(config));
|
||||
}
|
||||
|
||||
const JsonValue* objectField(
|
||||
const JsonValue::Object& object,
|
||||
const char* key)
|
||||
{
|
||||
const auto it = object.find(key);
|
||||
if (it == object.end())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
return &it->second;
|
||||
}
|
||||
|
||||
const std::string* jsonString(const JsonValue* value)
|
||||
{
|
||||
if (value == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
return std::get_if<std::string>(&value->value);
|
||||
}
|
||||
|
||||
const JsonValue::Array* jsonArray(const JsonValue* value)
|
||||
{
|
||||
if (value == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
return std::get_if<JsonValue::Array>(&value->value);
|
||||
}
|
||||
|
||||
bool jsonBool(const JsonValue* value, bool& out)
|
||||
{
|
||||
if (value == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool* parsed = std::get_if<bool>(&value->value);
|
||||
if (parsed == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
out = *parsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool jsonUint32(const JsonValue* value, uint32_t& out)
|
||||
{
|
||||
if (value == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const double* parsed = std::get_if<double>(&value->value);
|
||||
if (parsed == nullptr ||
|
||||
*parsed < 0.0 ||
|
||||
*parsed > 4294967295.0 ||
|
||||
*parsed != static_cast<uint32_t>(*parsed))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
out = static_cast<uint32_t>(*parsed);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool applyConfigFile(
|
||||
AppConfig& config,
|
||||
const char* path,
|
||||
std::string& error)
|
||||
{
|
||||
std::ifstream file(path);
|
||||
if (!file)
|
||||
{
|
||||
error = "failed to open config file";
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string text(
|
||||
(std::istreambuf_iterator<char>(file)),
|
||||
std::istreambuf_iterator<char>());
|
||||
|
||||
std::optional<JsonValue> rootValue =
|
||||
JsonParser(text).parse();
|
||||
if (!rootValue.has_value())
|
||||
{
|
||||
error = "invalid JSON";
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto* root =
|
||||
std::get_if<JsonValue::Object>(&rootValue->value);
|
||||
if (root == nullptr)
|
||||
{
|
||||
error = "config root must be an object";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (const std::string* grid =
|
||||
jsonString(objectField(*root, "grid")))
|
||||
{
|
||||
if (!parseGrid(
|
||||
*grid,
|
||||
config.gridCols,
|
||||
config.gridRows))
|
||||
{
|
||||
error = "grid must be <cols>x<rows> with 1-16 total feeds";
|
||||
return false;
|
||||
}
|
||||
resizeFeedsForGrid(config);
|
||||
}
|
||||
|
||||
uint32_t cols = 0;
|
||||
uint32_t rows = 0;
|
||||
const bool hasCols =
|
||||
jsonUint32(objectField(*root, "cols"), cols);
|
||||
const bool hasRows =
|
||||
jsonUint32(objectField(*root, "rows"), rows);
|
||||
if (hasCols || hasRows)
|
||||
{
|
||||
if (!hasCols || !hasRows ||
|
||||
cols == 0 || rows == 0 ||
|
||||
cols > MaxFeedCount / rows)
|
||||
{
|
||||
error = "cols and rows must describe 1-16 total feeds";
|
||||
return false;
|
||||
}
|
||||
|
||||
config.gridCols = cols;
|
||||
config.gridRows = rows;
|
||||
resizeFeedsForGrid(config);
|
||||
}
|
||||
|
||||
uint32_t fpsCap = 0;
|
||||
if (jsonUint32(objectField(*root, "fpsCap"), fpsCap))
|
||||
{
|
||||
if (fpsCap == 0 || fpsCap > 240)
|
||||
{
|
||||
error = "fpsCap must be 1-240";
|
||||
return false;
|
||||
}
|
||||
config.fpsCap = fpsCap;
|
||||
}
|
||||
|
||||
uint32_t maxV210UploadsPerFrame = 0;
|
||||
if (jsonUint32(
|
||||
objectField(*root, "maxV210UploadsPerFrame"),
|
||||
maxV210UploadsPerFrame))
|
||||
{
|
||||
if (maxV210UploadsPerFrame > MaxFeedCount)
|
||||
{
|
||||
error = "maxV210UploadsPerFrame must be 0-16";
|
||||
return false;
|
||||
}
|
||||
config.maxV210UploadsPerFrame = maxV210UploadsPerFrame;
|
||||
}
|
||||
|
||||
bool boolValue = false;
|
||||
if (jsonBool(objectField(*root, "noFps"), boolValue))
|
||||
{
|
||||
config.logFps = !boolValue;
|
||||
}
|
||||
if (jsonBool(objectField(*root, "perf"), boolValue))
|
||||
{
|
||||
config.logPerf = boolValue;
|
||||
}
|
||||
if (jsonBool(objectField(*root, "verbose"), boolValue))
|
||||
{
|
||||
config.verbose = boolValue;
|
||||
}
|
||||
if (jsonBool(objectField(*root, "paceUploads"), boolValue))
|
||||
{
|
||||
config.paceUploads = boolValue;
|
||||
}
|
||||
|
||||
if (const std::string* present =
|
||||
jsonString(objectField(*root, "present")))
|
||||
{
|
||||
const std::optional<PresentModeConfig> mode =
|
||||
parsePresentMode(*present);
|
||||
if (!mode.has_value())
|
||||
{
|
||||
error = "present must be fifo, mailbox, or immediate";
|
||||
return false;
|
||||
}
|
||||
config.presentMode = mode.value();
|
||||
}
|
||||
|
||||
const JsonValue::Array* feeds =
|
||||
jsonArray(objectField(*root, "feeds"));
|
||||
if (feeds == nullptr)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (feeds->size() > MaxFeedCount)
|
||||
{
|
||||
error = "feeds array exceeds 16 entries";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (feeds->size() > config.feeds.size())
|
||||
{
|
||||
uint32_t side = 1;
|
||||
while (side * side < feeds->size())
|
||||
{
|
||||
++side;
|
||||
}
|
||||
|
||||
config.gridCols = side;
|
||||
config.gridRows =
|
||||
static_cast<uint32_t>(
|
||||
(feeds->size() + side - 1) / side);
|
||||
resizeFeedsForGrid(config);
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < feeds->size(); ++i)
|
||||
{
|
||||
const auto* feedObject =
|
||||
std::get_if<JsonValue::Object>(
|
||||
&(*feeds)[i].value);
|
||||
if (feedObject == nullptr)
|
||||
{
|
||||
error = "each feed must be an object";
|
||||
return false;
|
||||
}
|
||||
|
||||
FeedConfig feed;
|
||||
|
||||
if (const std::string* kind =
|
||||
jsonString(objectField(*feedObject, "kind")))
|
||||
{
|
||||
const std::optional<FeedKind> parsed =
|
||||
parseFeedKind(*kind);
|
||||
if (!parsed.has_value())
|
||||
{
|
||||
error = "unknown feed kind: " + *kind;
|
||||
return false;
|
||||
}
|
||||
feed.kind = parsed.value();
|
||||
}
|
||||
|
||||
if (const std::string* domain =
|
||||
jsonString(objectField(*feedObject, "domain")))
|
||||
{
|
||||
feed.mxlDomain = *domain;
|
||||
}
|
||||
|
||||
const std::string* flow =
|
||||
jsonString(objectField(*feedObject, "flow"));
|
||||
if (flow == nullptr)
|
||||
{
|
||||
flow = jsonString(objectField(*feedObject, "flowId"));
|
||||
}
|
||||
if (flow != nullptr)
|
||||
{
|
||||
feed.mxlFlowId = *flow;
|
||||
feed.kind = FeedKind::MxlSdk;
|
||||
}
|
||||
|
||||
config.feeds[i] = std::move(feed);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<FeedKind> parseFeedKind(std::string_view text)
|
||||
@@ -172,8 +743,11 @@ void printUsage(const char* executableName)
|
||||
<< " --no-fps Disable FPS logging\n"
|
||||
<< " --perf Log per-frame CPU timing breakdown\n"
|
||||
<< " --verbose Enable detailed startup and SDK logs\n"
|
||||
<< " --pace-uploads Pace v210 uploads by feed frame rate\n"
|
||||
<< " --config <path> Load JSON feed configuration\n"
|
||||
<< " --grid <cols>x<rows> Set multiview grid (default 2x2, max 16 feeds)\n"
|
||||
<< " --fps-cap <fps> Limit render loop FPS (default 60)\n"
|
||||
<< " --max-v210-uploads <n> Limit v210 uploads per frame (0 = unlimited)\n"
|
||||
<< " --present <mode> Present mode: fifo, mailbox, immediate (default mailbox)\n"
|
||||
<< " --feed <idx> <kind> Set feed slot kind explicitly\n"
|
||||
<< " --domain <idx> <path> Set MXL domain for feed slot\n"
|
||||
@@ -191,30 +765,6 @@ ConfigParseResult parseAppConfig(
|
||||
{
|
||||
ConfigParseResult result;
|
||||
|
||||
for (int i = 1; i < argc; ++i)
|
||||
{
|
||||
if (std::string_view(argv[i]) != "--grid")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i + 1 >= argc ||
|
||||
!parseGrid(
|
||||
argv[i + 1],
|
||||
result.config.gridCols,
|
||||
result.config.gridRows))
|
||||
{
|
||||
std::cerr << "--grid must be <cols>x<rows> with 1-"
|
||||
<< MaxFeedCount << " total feeds"
|
||||
<< std::endl;
|
||||
result.shouldExit = true;
|
||||
result.exitCode = 1;
|
||||
return result;
|
||||
}
|
||||
|
||||
++i;
|
||||
}
|
||||
|
||||
applyDefaultFeeds(result.config);
|
||||
|
||||
for (int i = 1; i < argc; ++i)
|
||||
@@ -252,6 +802,63 @@ ConfigParseResult parseAppConfig(
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 1; i < argc; ++i)
|
||||
{
|
||||
if (std::string_view(argv[i]) != "--config")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i + 1 >= argc)
|
||||
{
|
||||
std::cerr << "--config requires <path>"
|
||||
<< std::endl;
|
||||
result.shouldExit = true;
|
||||
result.exitCode = 1;
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string error;
|
||||
if (!applyConfigFile(
|
||||
result.config,
|
||||
argv[i + 1],
|
||||
error))
|
||||
{
|
||||
std::cerr << "--config " << argv[i + 1]
|
||||
<< ": " << error << std::endl;
|
||||
result.shouldExit = true;
|
||||
result.exitCode = 1;
|
||||
return result;
|
||||
}
|
||||
|
||||
++i;
|
||||
}
|
||||
|
||||
for (int i = 1; i < argc; ++i)
|
||||
{
|
||||
if (std::string_view(argv[i]) != "--grid")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i + 1 >= argc ||
|
||||
!parseGrid(
|
||||
argv[i + 1],
|
||||
result.config.gridCols,
|
||||
result.config.gridRows))
|
||||
{
|
||||
std::cerr << "--grid must be <cols>x<rows> with 1-"
|
||||
<< MaxFeedCount << " total feeds"
|
||||
<< std::endl;
|
||||
result.shouldExit = true;
|
||||
result.exitCode = 1;
|
||||
return result;
|
||||
}
|
||||
|
||||
++i;
|
||||
resizeFeedsForGrid(result.config);
|
||||
}
|
||||
|
||||
std::vector<std::string_view> feedArgs;
|
||||
|
||||
for (int i = 1; i < argc; ++i)
|
||||
@@ -276,12 +883,24 @@ ConfigParseResult parseAppConfig(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--pace-uploads")
|
||||
{
|
||||
result.config.paceUploads = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--grid")
|
||||
{
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--config")
|
||||
{
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--fps-cap")
|
||||
{
|
||||
if (i + 1 >= argc)
|
||||
@@ -310,6 +929,33 @@ ConfigParseResult parseAppConfig(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--max-v210-uploads")
|
||||
{
|
||||
if (i + 1 >= argc)
|
||||
{
|
||||
std::cerr << "--max-v210-uploads requires <count>"
|
||||
<< std::endl;
|
||||
result.shouldExit = true;
|
||||
result.exitCode = 1;
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t maxUploads = 0;
|
||||
if (!parseUint32(argv[i + 1], maxUploads) ||
|
||||
maxUploads > MaxFeedCount)
|
||||
{
|
||||
std::cerr << "--max-v210-uploads must be 0-"
|
||||
<< MaxFeedCount << std::endl;
|
||||
result.shouldExit = true;
|
||||
result.exitCode = 1;
|
||||
return result;
|
||||
}
|
||||
|
||||
result.config.maxV210UploadsPerFrame = maxUploads;
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--present")
|
||||
{
|
||||
if (i + 1 >= argc)
|
||||
|
||||
Reference in New Issue
Block a user