#include "AppConfig.hpp" #include #include #include #include #include #include #include #include #include #include #include std::optional parsePresentMode( std::string_view text); namespace { struct JsonValue { using Object = std::map; using Array = std::vector; 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 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(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) { uint32_t parsed = 0; bool hasDigit = false; for (int i = 0; text[i] != '\0'; ++i) { if (text[i] < '0' || text[i] > '9') { return false; } hasDigit = true; parsed = parsed * 10u + static_cast(text[i] - '0'); } if (!hasDigit) { return false; } value = parsed; return true; } bool parseFeedIndex( const char* text, uint32_t maxFeeds, uint32_t& index) { uint32_t parsed = 0; if (!parseUint32(text, parsed) || parsed >= maxFeeds) { return false; } index = parsed; return true; } bool parseGrid( std::string_view text, uint32_t& cols, uint32_t& rows) { const size_t xPos = text.find('x'); if (xPos == std::string_view::npos) { return false; } const std::string colText(text.substr(0, xPos)); const std::string rowText(text.substr(xPos + 1)); uint32_t parsedCols = 0; uint32_t parsedRows = 0; if (!parseUint32(colText.c_str(), parsedCols) || !parseUint32(rowText.c_str(), parsedRows) || parsedCols == 0 || parsedRows == 0 || parsedCols > MaxFeedCount / parsedRows) { return false; } cols = parsedCols; rows = parsedRows; return true; } uint32_t feedCount(const AppConfig& config) { return config.gridCols * config.gridRows; } 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(&value->value); } const JsonValue::Array* jsonArray(const JsonValue* value) { if (value == nullptr) { return nullptr; } return std::get_if(&value->value); } bool jsonBool(const JsonValue* value, bool& out) { if (value == nullptr) { return false; } const bool* parsed = std::get_if(&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(&value->value); if (parsed == nullptr || *parsed < 0.0 || *parsed > 4294967295.0 || *parsed != static_cast(*parsed)) { return false; } out = static_cast(*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(file)), std::istreambuf_iterator()); std::optional rootValue = JsonParser(text).parse(); if (!rootValue.has_value()) { error = "invalid JSON"; return false; } const auto* root = std::get_if(&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 x 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 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( (feeds->size() + side - 1) / side); resizeFeedsForGrid(config); } for (size_t i = 0; i < feeds->size(); ++i) { const auto* feedObject = std::get_if( &(*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 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 parseFeedKind(std::string_view text) { if (text == "nosignal" || text == "none" || text == "fake") { return FeedKind::NoSignal; } if (text == "mxl") { return FeedKind::MxlPlaceholder; } if (text == "smpte") { return FeedKind::SmpteBars; } if (text == "mxlsdk") { return FeedKind::MxlSdk; } return std::nullopt; } std::optional parsePresentMode(std::string_view text) { if (text == "fifo") { return PresentModeConfig::Fifo; } if (text == "mailbox") { return PresentModeConfig::Mailbox; } if (text == "immediate") { return PresentModeConfig::Immediate; } return std::nullopt; } std::string_view feedKindName(FeedKind kind) { switch (kind) { case FeedKind::MxlPlaceholder: return "mxl"; case FeedKind::SmpteBars: return "smpte"; case FeedKind::MxlSdk: return "mxlsdk"; case FeedKind::NoSignal: default: return "nosignal"; } } void printUsage(const char* executableName) { std::cout << "Usage: " << executableName << " [options] [feed...]\n" << "Options:\n" << " --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 Load JSON feed configuration\n" << " --grid x Set multiview grid (default 2x2, max 16 feeds)\n" << " --fps-cap Limit render loop FPS (default 60)\n" << " --max-v210-uploads Limit v210 uploads per frame (0 = unlimited)\n" << " --present Present mode: fifo, mailbox, immediate (default mailbox)\n" << " --feed Set feed slot kind explicitly\n" << " --domain Set MXL domain for feed slot\n" << " --flow Set MXL flow UUID for feed slot\n" << " --version Print version\n" << " --help Print this help\n" << "Feed kinds: nosignal, none, fake, mxl, smpte, mxlsdk\n" << "Default: 2x2 grid, all slots nosignal" << std::endl; } ConfigParseResult parseAppConfig( int argc, char* argv[]) { ConfigParseResult result; applyDefaultFeeds(result.config); for (int i = 1; i < argc; ++i) { const std::string_view arg(argv[i]); if (arg == "--help" || arg == "-h") { printUsage(argv[0]); result.shouldExit = true; result.exitCode = 0; return result; } } for (int i = 1; i < argc; ++i) { const std::string_view arg(argv[i]); if (arg == "--version" || arg == "-v") { std::cout << AppName << " " << AppVersionMajor << "." << AppVersionMinor << "." << AppVersionPatch << std::endl; result.shouldExit = true; result.exitCode = 0; return result; } } for (int i = 1; i < argc; ++i) { if (std::string_view(argv[i]) != "--config") { continue; } if (i + 1 >= argc) { std::cerr << "--config requires " << 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 x with 1-" << MaxFeedCount << " total feeds" << std::endl; result.shouldExit = true; result.exitCode = 1; return result; } ++i; resizeFeedsForGrid(result.config); } std::vector feedArgs; for (int i = 1; i < argc; ++i) { const std::string_view arg(argv[i]); if (arg == "--no-fps") { result.config.logFps = false; continue; } if (arg == "--perf") { result.config.logPerf = true; continue; } if (arg == "--verbose") { result.config.verbose = true; 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) { std::cerr << "--fps-cap requires " << std::endl; result.shouldExit = true; result.exitCode = 1; return result; } uint32_t fpsCap = 0; if (!parseUint32(argv[i + 1], fpsCap) || fpsCap == 0 || fpsCap > 240) { std::cerr << "--fps-cap must be 1-240" << std::endl; result.shouldExit = true; result.exitCode = 1; return result; } result.config.fpsCap = fpsCap; ++i; continue; } if (arg == "--max-v210-uploads") { if (i + 1 >= argc) { std::cerr << "--max-v210-uploads requires " << 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) { std::cerr << "--present requires " << std::endl; result.shouldExit = true; result.exitCode = 1; return result; } const std::optional mode = parsePresentMode(argv[i + 1]); if (!mode.has_value()) { std::cerr << "--present must be fifo, mailbox, or immediate" << std::endl; result.shouldExit = true; result.exitCode = 1; return result; } result.config.presentMode = mode.value(); ++i; continue; } if (arg == "--feed") { if (i + 2 >= argc) { std::cerr << "--feed requires " << std::endl; result.shouldExit = true; result.exitCode = 1; return result; } uint32_t index = 0; if (!parseFeedIndex( argv[i + 1], feedCount(result.config), index)) { std::cerr << "--feed index must be 0-" << (feedCount(result.config) - 1) << std::endl; result.shouldExit = true; result.exitCode = 1; return result; } const std::optional kind = parseFeedKind(argv[i + 2]); if (!kind.has_value()) { std::cerr << "Unknown feed kind: " << argv[i + 2] << std::endl; printUsage(argv[0]); result.shouldExit = true; result.exitCode = 1; return result; } result.config.feeds[index].kind = kind.value(); i += 2; continue; } if (arg == "--domain") { if (i + 2 >= argc) { std::cerr << "--domain requires " << std::endl; result.shouldExit = true; result.exitCode = 1; return result; } uint32_t index = 0; if (!parseFeedIndex( argv[i + 1], feedCount(result.config), index)) { std::cerr << "--domain index must be 0-" << (feedCount(result.config) - 1) << std::endl; result.shouldExit = true; result.exitCode = 1; return result; } result.config.feeds[index].mxlDomain = std::string(argv[i + 2]); i += 2; continue; } if (arg == "--flow") { if (i + 2 >= argc) { std::cerr << "--flow requires " << std::endl; result.shouldExit = true; result.exitCode = 1; return result; } uint32_t index = 0; if (!parseFeedIndex( argv[i + 1], feedCount(result.config), index)) { std::cerr << "--flow index must be 0-" << (feedCount(result.config) - 1) << std::endl; result.shouldExit = true; result.exitCode = 1; return result; } result.config.feeds[index].mxlFlowId = std::string(argv[i + 2]); result.config.feeds[index].kind = FeedKind::MxlSdk; i += 2; continue; } feedArgs.push_back(arg); } if (feedArgs.size() > result.config.feeds.size()) { std::cerr << "Too many feed arguments" << std::endl; printUsage(argv[0]); result.shouldExit = true; result.exitCode = 1; return result; } size_t feedArgIndex = 0; for (uint32_t slot = 0; slot < result.config.feeds.size() && feedArgIndex < feedArgs.size(); ++slot) { if (result.config.feeds[slot].kind == FeedKind::MxlSdk) { continue; } const std::optional parsed = parseFeedKind(feedArgs[feedArgIndex]); if (!parsed.has_value()) { std::cerr << "Unknown feed kind: " << feedArgs[feedArgIndex] << std::endl; printUsage(argv[0]); result.shouldExit = true; result.exitCode = 1; return result; } result.config.feeds[slot].kind = parsed.value(); ++feedArgIndex; } if (feedArgIndex < feedArgs.size()) { std::cerr << "Too many feed arguments for available slots" << std::endl; printUsage(argv[0]); result.shouldExit = true; result.exitCode = 1; return result; } return result; }