diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4e5215c --- /dev/null +++ b/.gitignore @@ -0,0 +1,71 @@ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Linker files +*.ilk + +# Debugger Files +*.pdb + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll +*.so.* + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# Build directories +build/ +Build/ +build-*/ + +# CMake generated files +CMakeFiles/ +CMakeCache.txt +cmake_install.cmake +Makefile +install_manifest.txt +compile_commands.json + +# Temporary files +*.tmp +*.log +*.bak +*.swp + +# vcpkg +vcpkg_installed/ + +# debug information files +*.dwo + +# test output & cache +Testing/ +.cache/ + +# local evidence artifacts generated by standalone scripts +.sisyphus/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 2ff79a8..defa604 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,6 +8,10 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) find_package(Threads REQUIRED) +if (EXISTS "${CMAKE_CURRENT_LIST_DIR}/lib/CLI11/CMakeLists.txt") + add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/lib/CLI11" "${CMAKE_CURRENT_BINARY_DIR}/vendor/cli11") +endif() + find_package(cppzmq QUIET) find_package(ZeroMQ QUIET) find_package(spdlog REQUIRED) @@ -76,6 +80,10 @@ elseif (TARGET spdlog) list(APPEND CVMAP_STREAMER_LINK_DEPS spdlog) endif() +if (TARGET CLI11::CLI11) + list(APPEND CVMAP_STREAMER_LINK_DEPS CLI11::CLI11) +endif() + target_link_libraries(cvmmap_streamer_common PUBLIC ${CVMAP_STREAMER_LINK_DEPS}) function(add_cvmmap_binary target source) diff --git a/include/cvmmap_streamer/sim/options.hpp b/include/cvmmap_streamer/sim/options.hpp index 6e7f409..56f99cd 100644 --- a/include/cvmmap_streamer/sim/options.hpp +++ b/include/cvmmap_streamer/sim/options.hpp @@ -4,33 +4,50 @@ #include #include #include +#include #include "cvmmap_streamer/ipc/contracts.hpp" namespace cvmmap_streamer::sim { struct RuntimeConfig { - std::uint32_t frames{360}; - std::uint32_t fps{60}; - std::uint16_t width{64}; - std::uint16_t height{48}; - std::optional emit_reset_at{}; - std::optional emit_reset_every{}; - std::optional switch_format_at{}; - std::optional switch_width{}; - std::optional switch_height{}; - std::string label{"sim"}; - std::string shm_name{"cvmmap_sim"}; - std::string zmq_endpoint{"ipc:///tmp/cvmmap_sim"}; - std::uint8_t channels{3}; - ipc::Depth depth{ipc::Depth::U8}; - ipc::PixelFormat pixel_format{ipc::PixelFormat::BGR}; + std::uint32_t frames{360}; + std::uint32_t fps{60}; + std::uint16_t width{64}; + std::uint16_t height{48}; + std::optional emit_reset_at{}; + std::optional emit_reset_every{}; + std::optional switch_format_at{}; + std::optional switch_width{}; + std::optional switch_height{}; + std::string label{"sim"}; + std::string shm_name{"cvmmap_sim"}; + std::string zmq_endpoint{"ipc:///tmp/cvmmap_sim"}; + std::uint8_t channels{3}; + ipc::Depth depth{ipc::Depth::U8}; + ipc::PixelFormat pixel_format{ipc::PixelFormat::BGR}; - [[nodiscard]] - std::uint32_t payload_size_bytes() const; + [[nodiscard]] + std::uint32_t payload_size_bytes() const; }; -std::expected parse_runtime_config(int argc, char **argv); -void print_help(); +enum class ParseStatus { + Ok, + Help, + Error, +}; -} +struct ParseResult { + ParseStatus status{ParseStatus::Ok}; + RuntimeConfig config{}; + std::string message{}; + int exit_code{0}; +}; + +std::expected parse_runtime_config(int argc, + char **argv); +void print_help(); +ParseResult parse_runtime_config_with_cli11( + int argc, char **argv, std::string_view executable_name = "cvmmap_sim"); + +} // namespace cvmmap_streamer::sim diff --git a/src/config/runtime_config.cpp b/src/config/runtime_config.cpp index 04ff30f..d6d32a4 100644 --- a/src/config/runtime_config.cpp +++ b/src/config/runtime_config.cpp @@ -1,524 +1,559 @@ #include "cvmmap_streamer/config/runtime_config.hpp" +#include + +#include #include #include #include +#include #include +#include #include #include +#include namespace cvmmap_streamer { namespace { - std::expected - next_value(int argc, char **argv, int &index, std::string_view flag_name) { - if (index + 1 >= argc) { - return std::unexpected("missing value for " + std::string(flag_name)); - } - ++index; - return std::string_view{argv[index]}; - } - - std::expected parse_u32(std::string_view raw, std::string_view flag_name) { - std::uint32_t value{0}; - const auto *begin = raw.data(); - const auto *end = raw.data() + raw.size(); - const auto result = std::from_chars(begin, end, value, 10); - if (result.ec != std::errc{} || result.ptr != end) { - return std::unexpected("invalid value for " + std::string(flag_name) + ": '" + std::string(raw) + "'"); - } - return value; - } - - std::expected parse_u16(std::string_view raw, std::string_view flag_name) { - std::uint16_t value{0}; - const auto *begin = raw.data(); - const auto *end = raw.data() + raw.size(); - const auto result = std::from_chars(begin, end, value, 10); - if (result.ec != std::errc{} || result.ptr != end) { - return std::unexpected("invalid value for " + std::string(flag_name) + ": '" + std::string(raw) + "'"); - } - return value; - } - - std::expected parse_size(std::string_view raw, std::string_view flag_name) { - unsigned long long parsed{0}; - const auto *begin = raw.data(); - const auto *end = raw.data() + raw.size(); - const auto result = std::from_chars(begin, end, parsed, 10); - if (result.ec != std::errc{} || result.ptr != end) { - return std::unexpected("invalid value for " + std::string(flag_name) + ": '" + std::string(raw) + "'"); - } - if (parsed > static_cast(std::numeric_limits::max())) { - return std::unexpected("value out of range for " + std::string(flag_name) + ": '" + std::string(raw) + "'"); - } - return static_cast(parsed); - } - - std::expected parse_bool(std::string_view raw, std::string_view flag_name) { - if (raw == "true" || raw == "1") { - return true; - } - if (raw == "false" || raw == "0") { - return false; - } - return std::unexpected( - "invalid value for " + std::string(flag_name) + ": '" + std::string(raw) + "' (expected: true|false|1|0)"); - } - - std::expected parse_codec(std::string_view raw) { - if (raw == "h264") { - return CodecType::H264; - } - if (raw == "h265") { - return CodecType::H265; - } - return std::unexpected("invalid codec: '" + std::string(raw) + "' (expected: h264|h265)"); - } - - std::expected parse_run_mode(std::string_view raw) { - if (raw == "pipeline") { - return RunMode::Pipeline; - } - if (raw == "ingest") { - return RunMode::Ingest; - } - return std::unexpected("invalid run mode: '" + std::string(raw) + "' (expected: pipeline|ingest)"); - } - - std::expected parse_rtmp_mode(std::string_view raw) { - if (raw == "enhanced") { - return RtmpMode::Enhanced; - } - if (raw == "domestic") { - return RtmpMode::Domestic; - } - return std::unexpected("invalid rtmp mode: '" + std::string(raw) + "' (expected: enhanced|domestic)"); - } - - std::expected, std::string> parse_rtp_endpoint(std::string_view endpoint) { - if (endpoint.empty()) { - return std::unexpected("invalid RTP config: --rtp-endpoint must not be empty"); - } - - const auto colon = endpoint.rfind(':'); - if (colon == std::string_view::npos || colon == 0 || colon + 1 >= endpoint.size()) { - return std::unexpected("invalid RTP config: --rtp-endpoint must be in ':' format"); - } - - const auto host = endpoint.substr(0, colon); - const auto port = endpoint.substr(colon + 1); - if (host.empty()) { - return std::unexpected("invalid RTP config: --rtp-endpoint host must not be empty"); - } - - auto parsed_port = parse_u16(port, "--rtp-endpoint"); - if (!parsed_port) { - return std::unexpected(parsed_port.error()); - } - if (*parsed_port == 0) { - return std::unexpected("invalid RTP config: --rtp-endpoint port must be in range [1,65535]"); - } - - return std::pair{std::string(host), *parsed_port}; - } - +std::string trim_copy(std::string value) { + auto not_space = [](unsigned char c) { + return c != ' ' && c != '\t' && c != '\n' && c != '\r'; + }; + while (!value.empty() && + !not_space(static_cast(value.front()))) { + value.erase(value.begin()); + } + while (!value.empty() && + !not_space(static_cast(value.back()))) { + value.pop_back(); + } + return value; } -RuntimeConfig RuntimeConfig::defaults() { - return RuntimeConfig{}; +std::string normalize_cli_error(std::string raw_message) { + if (raw_message.find("The following argument was not expected:") != + std::string::npos) { + const auto pos = raw_message.find(':'); + if (pos != std::string::npos && pos + 1 < raw_message.size()) { + return "unknown argument: " + trim_copy(raw_message.substr(pos + 1)); + } + return "unknown argument"; + } + + constexpr std::array kFlags{"--codec", + "--run-mode", + "--shm-name", + "--zmq-endpoint", + "--rtmp-url", + "--rtmp-mode", + "--rtp-endpoint", + "--rtp-payload-type", + "--rtp-sdp", + "--sdp", + "--queue-size", + "--gop", + "--b-frames", + "--realtime-sync", + "--force-idr-on-reset", + "--ingest-max-frames", + "--ingest-idle-timeout-ms"}; + + if (raw_message.find("requires at least") != std::string::npos || + raw_message.find("requires ") != std::string::npos) { + for (const auto flag : kFlags) { + if (raw_message.find(flag) != std::string::npos) { + return "missing value for " + std::string(flag); + } + } + } + + return trim_copy(std::move(raw_message)); } +std::expected +parse_u32(std::string_view raw, std::string_view flag_name) { + std::uint32_t value{0}; + const auto *begin = raw.data(); + const auto *end = raw.data() + raw.size(); + const auto result = std::from_chars(begin, end, value, 10); + if (result.ec != std::errc{} || result.ptr != end) { + return std::unexpected("invalid value for " + std::string(flag_name) + + ": '" + std::string(raw) + "'"); + } + return value; +} + +std::expected +parse_u16(std::string_view raw, std::string_view flag_name) { + std::uint16_t value{0}; + const auto *begin = raw.data(); + const auto *end = raw.data() + raw.size(); + const auto result = std::from_chars(begin, end, value, 10); + if (result.ec != std::errc{} || result.ptr != end) { + return std::unexpected("invalid value for " + std::string(flag_name) + + ": '" + std::string(raw) + "'"); + } + return value; +} + +std::expected parse_size(std::string_view raw, + std::string_view flag_name) { + unsigned long long parsed{0}; + const auto *begin = raw.data(); + const auto *end = raw.data() + raw.size(); + const auto result = std::from_chars(begin, end, parsed, 10); + if (result.ec != std::errc{} || result.ptr != end) { + return std::unexpected("invalid value for " + std::string(flag_name) + + ": '" + std::string(raw) + "'"); + } + if (parsed > static_cast( + std::numeric_limits::max())) { + return std::unexpected("value out of range for " + std::string(flag_name) + + ": '" + std::string(raw) + "'"); + } + return static_cast(parsed); +} + +std::expected parse_bool(std::string_view raw, + std::string_view flag_name) { + if (raw == "true" || raw == "1") { + return true; + } + if (raw == "false" || raw == "0") { + return false; + } + return std::unexpected("invalid value for " + std::string(flag_name) + ": '" + + std::string(raw) + "' (expected: true|false|1|0)"); +} + +std::expected parse_codec(std::string_view raw) { + if (raw == "h264") { + return CodecType::H264; + } + if (raw == "h265") { + return CodecType::H265; + } + return std::unexpected("invalid codec: '" + std::string(raw) + + "' (expected: h264|h265)"); +} + +std::expected parse_run_mode(std::string_view raw) { + if (raw == "pipeline") { + return RunMode::Pipeline; + } + if (raw == "ingest") { + return RunMode::Ingest; + } + return std::unexpected("invalid run mode: '" + std::string(raw) + + "' (expected: pipeline|ingest)"); +} + +std::expected parse_rtmp_mode(std::string_view raw) { + if (raw == "enhanced") { + return RtmpMode::Enhanced; + } + if (raw == "domestic") { + return RtmpMode::Domestic; + } + return std::unexpected("invalid rtmp mode: '" + std::string(raw) + + "' (expected: enhanced|domestic)"); +} + +std::expected, std::string> +parse_rtp_endpoint(std::string_view endpoint) { + if (endpoint.empty()) { + return std::unexpected( + "invalid RTP config: --rtp-endpoint must not be empty"); + } + + const auto colon = endpoint.rfind(':'); + if (colon == std::string_view::npos || colon == 0 || + colon + 1 >= endpoint.size()) { + return std::unexpected( + "invalid RTP config: --rtp-endpoint must be in ':' format"); + } + + const auto host = endpoint.substr(0, colon); + const auto port = endpoint.substr(colon + 1); + if (host.empty()) { + return std::unexpected( + "invalid RTP config: --rtp-endpoint host must not be empty"); + } + + auto parsed_port = parse_u16(port, "--rtp-endpoint"); + if (!parsed_port) { + return std::unexpected(parsed_port.error()); + } + if (*parsed_port == 0) { + return std::unexpected( + "invalid RTP config: --rtp-endpoint port must be in range [1,65535]"); + } + + return std::pair{std::string(host), *parsed_port}; +} + +} // namespace + +RuntimeConfig RuntimeConfig::defaults() { return RuntimeConfig{}; } + std::string_view to_string(CodecType codec) { - switch (codec) { - case CodecType::H264: - return "h264"; - case CodecType::H265: - return "h265"; - default: - return "unknown"; - } + switch (codec) { + case CodecType::H264: + return "h264"; + case CodecType::H265: + return "h265"; + default: + return "unknown"; + } } std::string_view to_string(RunMode mode) { - switch (mode) { - case RunMode::Pipeline: - return "pipeline"; - case RunMode::Ingest: - return "ingest"; - default: - return "unknown"; - } + switch (mode) { + case RunMode::Pipeline: + return "pipeline"; + case RunMode::Ingest: + return "ingest"; + default: + return "unknown"; + } } std::string_view to_string(RtmpMode mode) { - switch (mode) { - case RtmpMode::Enhanced: - return "enhanced"; - case RtmpMode::Domestic: - return "domestic"; - default: - return "unknown"; - } + switch (mode) { + case RtmpMode::Enhanced: + return "enhanced"; + case RtmpMode::Domestic: + return "domestic"; + default: + return "unknown"; + } } -std::expected parse_runtime_config(int argc, char **argv) { - RuntimeConfig config = RuntimeConfig::defaults(); +std::expected parse_runtime_config(int argc, + char **argv) { + RuntimeConfig config = RuntimeConfig::defaults(); - for (int i = 1; i < argc; ++i) { - const std::string_view arg{argv[i]}; + std::string codec_raw; + std::string run_mode_raw; + std::string shm_name_raw; + std::string zmq_endpoint_raw; + std::vector rtmp_urls_raw; + std::string rtmp_mode_raw; + std::string rtp_endpoint_raw; + std::string rtp_payload_type_raw; + std::string rtp_sdp_raw; + std::string queue_size_raw; + std::string gop_raw; + std::string b_frames_raw; + std::string realtime_sync_raw; + std::string force_idr_on_reset_raw; + std::string ingest_max_frames_raw; + std::string ingest_idle_timeout_raw; + std::string ingest_consumer_delay_raw; + std::string snapshot_copy_delay_raw; + std::string emit_stall_raw; - if (arg == "--codec") { - auto raw = next_value(argc, argv, i, arg); - if (!raw) { - return std::unexpected(raw.error()); - } - auto codec = parse_codec(*raw); - if (!codec) { - return std::unexpected(codec.error()); - } - config.codec = *codec; - continue; - } + bool rtmp_enabled{false}; + bool rtp_enabled{false}; + bool version_requested{false}; - if (arg == "--run-mode") { - auto raw = next_value(argc, argv, i, arg); - if (!raw) { - return std::unexpected(raw.error()); - } - auto mode = parse_run_mode(*raw); - if (!mode) { - return std::unexpected(mode.error()); - } - config.run_mode = *mode; - continue; - } + CLI::App app{"cvmmap-streamer runtime options"}; + app.allow_extras(false); + app.set_help_flag("--help,-h", "show this message"); - if (arg == "--shm-name") { - auto value = next_value(argc, argv, i, arg); - if (!value) { - return std::unexpected(value.error()); - } - config.input.shm_name = std::string(*value); - continue; - } + app.add_option("--codec", codec_raw); + app.add_option("--run-mode", run_mode_raw); + app.add_option("--shm-name", shm_name_raw); + app.add_option("--zmq-endpoint", zmq_endpoint_raw); + app.add_flag("--rtmp", rtmp_enabled); + app.add_option("--rtmp-url", rtmp_urls_raw); + app.add_option("--rtmp-mode", rtmp_mode_raw); + app.add_flag("--rtp", rtp_enabled); + app.add_option("--rtp-endpoint", rtp_endpoint_raw); + app.add_option("--rtp-payload-type", rtp_payload_type_raw); + auto *rtp_sdp = app.add_option("--rtp-sdp", rtp_sdp_raw); + app.add_option("--sdp", rtp_sdp_raw)->excludes(rtp_sdp); + app.add_option("--queue-size", queue_size_raw); + app.add_option("--gop", gop_raw); + app.add_option("--b-frames", b_frames_raw); + app.add_option("--realtime-sync", realtime_sync_raw); + app.add_option("--force-idr-on-reset", force_idr_on_reset_raw); + app.add_option("--ingest-max-frames", ingest_max_frames_raw); + app.add_option("--ingest-idle-timeout-ms", ingest_idle_timeout_raw); + app.add_option("--ingest-consumer-delay-ms", ingest_consumer_delay_raw); + app.add_option("--snapshot-copy-delay-us", snapshot_copy_delay_raw); + app.add_option("--emit-stall-ms", emit_stall_raw); + app.add_flag("--version", version_requested); - if (arg == "--zmq-endpoint") { - auto value = next_value(argc, argv, i, arg); - if (!value) { - return std::unexpected(value.error()); - } - config.input.zmq_endpoint = std::string(*value); - continue; - } + try { + app.parse(argc, argv); + } catch (const CLI::ParseError &e) { + return std::unexpected(normalize_cli_error(e.what())); + } - if (arg == "--rtmp") { - config.outputs.rtmp.enabled = true; - continue; - } + if (!codec_raw.empty()) { + auto codec = parse_codec(codec_raw); + if (!codec) { + return std::unexpected(codec.error()); + } + config.codec = *codec; + } - if (arg == "--rtmp-url") { - auto value = next_value(argc, argv, i, arg); - if (!value) { - return std::unexpected(value.error()); - } - config.outputs.rtmp.enabled = true; - config.outputs.rtmp.urls.emplace_back(*value); - continue; - } + if (!run_mode_raw.empty()) { + auto run_mode = parse_run_mode(run_mode_raw); + if (!run_mode) { + return std::unexpected(run_mode.error()); + } + config.run_mode = *run_mode; + } - if (arg == "--rtmp-mode") { - auto raw = next_value(argc, argv, i, arg); - if (!raw) { - return std::unexpected(raw.error()); - } - auto mode = parse_rtmp_mode(*raw); - if (!mode) { - return std::unexpected(mode.error()); - } - config.outputs.rtmp.mode = *mode; - continue; - } + if (!shm_name_raw.empty()) { + config.input.shm_name = shm_name_raw; + } - if (arg == "--rtp") { - config.outputs.rtp.enabled = true; - continue; - } + if (!zmq_endpoint_raw.empty()) { + config.input.zmq_endpoint = zmq_endpoint_raw; + } - if (arg == "--rtp-endpoint") { - auto value = next_value(argc, argv, i, arg); - if (!value) { - return std::unexpected(value.error()); - } - auto endpoint = parse_rtp_endpoint(*value); - if (!endpoint) { - return std::unexpected(endpoint.error()); - } - config.outputs.rtp.enabled = true; - config.outputs.rtp.endpoint = std::string(*value); - config.outputs.rtp.host = endpoint->first; - config.outputs.rtp.port = endpoint->second; - continue; - } + config.outputs.rtmp.enabled = rtmp_enabled; + if (!rtmp_urls_raw.empty()) { + config.outputs.rtmp.enabled = true; + config.outputs.rtmp.urls = std::move(rtmp_urls_raw); + } - if (arg == "--rtp-payload-type") { - auto raw = next_value(argc, argv, i, arg); - if (!raw) { - return std::unexpected(raw.error()); - } - auto value = parse_u32(*raw, arg); - if (!value) { - return std::unexpected(value.error()); - } - if (*value > std::numeric_limits::max()) { - return std::unexpected("value out of range for --rtp-payload-type: '" + std::string(*raw) + "'"); - } - config.outputs.rtp.enabled = true; - config.outputs.rtp.payload_type = static_cast(*value); - continue; - } + if (!rtmp_mode_raw.empty()) { + auto mode = parse_rtmp_mode(rtmp_mode_raw); + if (!mode) { + return std::unexpected(mode.error()); + } + config.outputs.rtmp.mode = *mode; + } - if (arg == "--rtp-sdp" || arg == "--sdp") { - auto value = next_value(argc, argv, i, arg); - if (!value) { - return std::unexpected(value.error()); - } - if (value->empty()) { - return std::unexpected("invalid RTP config: " + std::string(arg) + " must not be empty"); - } - config.outputs.rtp.enabled = true; - config.outputs.rtp.sdp_path = std::string(*value); - continue; - } + config.outputs.rtp.enabled = rtp_enabled; + if (!rtp_endpoint_raw.empty()) { + auto endpoint = parse_rtp_endpoint(rtp_endpoint_raw); + if (!endpoint) { + return std::unexpected(endpoint.error()); + } + config.outputs.rtp.enabled = true; + config.outputs.rtp.endpoint = rtp_endpoint_raw; + config.outputs.rtp.host = endpoint->first; + config.outputs.rtp.port = endpoint->second; + } - if (arg == "--queue-size") { - auto raw = next_value(argc, argv, i, arg); - if (!raw) { - return std::unexpected(raw.error()); - } - auto parsed = parse_size(*raw, arg); - if (!parsed) { - return std::unexpected(parsed.error()); - } - config.latency.queue_size = *parsed; - continue; - } + if (!rtp_payload_type_raw.empty()) { + auto value = parse_u32(rtp_payload_type_raw, "--rtp-payload-type"); + if (!value) { + return std::unexpected(value.error()); + } + if (*value > std::numeric_limits::max()) { + return std::unexpected("value out of range for --rtp-payload-type: '" + + rtp_payload_type_raw + "'"); + } + config.outputs.rtp.enabled = true; + config.outputs.rtp.payload_type = static_cast(*value); + } - if (arg == "--gop") { - auto raw = next_value(argc, argv, i, arg); - if (!raw) { - return std::unexpected(raw.error()); - } - auto parsed = parse_u32(*raw, arg); - if (!parsed) { - return std::unexpected(parsed.error()); - } - config.latency.gop = *parsed; - continue; - } + if (!rtp_sdp_raw.empty()) { + config.outputs.rtp.enabled = true; + config.outputs.rtp.sdp_path = rtp_sdp_raw; + } - if (arg == "--b-frames") { - auto raw = next_value(argc, argv, i, arg); - if (!raw) { - return std::unexpected(raw.error()); - } - auto parsed = parse_u32(*raw, arg); - if (!parsed) { - return std::unexpected(parsed.error()); - } - config.latency.b_frames = *parsed; - continue; - } + if (!queue_size_raw.empty()) { + auto parsed = parse_size(queue_size_raw, "--queue-size"); + if (!parsed) { + return std::unexpected(parsed.error()); + } + config.latency.queue_size = *parsed; + } - if (arg == "--realtime-sync") { - auto raw = next_value(argc, argv, i, arg); - if (!raw) { - return std::unexpected(raw.error()); - } - auto parsed = parse_bool(*raw, arg); - if (!parsed) { - return std::unexpected(parsed.error()); - } - config.latency.realtime_sync = *parsed; - continue; - } + if (!gop_raw.empty()) { + auto parsed = parse_u32(gop_raw, "--gop"); + if (!parsed) { + return std::unexpected(parsed.error()); + } + config.latency.gop = *parsed; + } - if (arg == "--force-idr-on-reset") { - auto raw = next_value(argc, argv, i, arg); - if (!raw) { - return std::unexpected(raw.error()); - } - auto parsed = parse_bool(*raw, arg); - if (!parsed) { - return std::unexpected(parsed.error()); - } - config.latency.force_idr_on_reset = *parsed; - continue; - } + if (!b_frames_raw.empty()) { + auto parsed = parse_u32(b_frames_raw, "--b-frames"); + if (!parsed) { + return std::unexpected(parsed.error()); + } + config.latency.b_frames = *parsed; + } - if (arg == "--ingest-max-frames") { - auto raw = next_value(argc, argv, i, arg); - if (!raw) { - return std::unexpected(raw.error()); - } - auto parsed = parse_u32(*raw, arg); - if (!parsed) { - return std::unexpected(parsed.error()); - } - config.latency.ingest_max_frames = *parsed; - continue; - } + if (!realtime_sync_raw.empty()) { + auto parsed = parse_bool(realtime_sync_raw, "--realtime-sync"); + if (!parsed) { + return std::unexpected(parsed.error()); + } + config.latency.realtime_sync = *parsed; + } - if (arg == "--ingest-idle-timeout-ms") { - auto raw = next_value(argc, argv, i, arg); - if (!raw) { - return std::unexpected(raw.error()); - } - auto parsed = parse_u32(*raw, arg); - if (!parsed) { - return std::unexpected(parsed.error()); - } - config.latency.ingest_idle_timeout_ms = *parsed; - continue; - } + if (!force_idr_on_reset_raw.empty()) { + auto parsed = parse_bool(force_idr_on_reset_raw, "--force-idr-on-reset"); + if (!parsed) { + return std::unexpected(parsed.error()); + } + config.latency.force_idr_on_reset = *parsed; + } - if (arg == "--ingest-consumer-delay-ms") { - auto raw = next_value(argc, argv, i, arg); - if (!raw) { - return std::unexpected(raw.error()); - } - auto parsed = parse_u32(*raw, arg); - if (!parsed) { - return std::unexpected(parsed.error()); - } - config.latency.ingest_consumer_delay_ms = *parsed; - continue; - } + if (!ingest_max_frames_raw.empty()) { + auto parsed = parse_u32(ingest_max_frames_raw, "--ingest-max-frames"); + if (!parsed) { + return std::unexpected(parsed.error()); + } + config.latency.ingest_max_frames = *parsed; + } - if (arg == "--snapshot-copy-delay-us") { - auto raw = next_value(argc, argv, i, arg); - if (!raw) { - return std::unexpected(raw.error()); - } - auto parsed = parse_u32(*raw, arg); - if (!parsed) { - return std::unexpected(parsed.error()); - } - config.latency.snapshot_copy_delay_us = *parsed; - continue; - } + if (!ingest_idle_timeout_raw.empty()) { + auto parsed = + parse_u32(ingest_idle_timeout_raw, "--ingest-idle-timeout-ms"); + if (!parsed) { + return std::unexpected(parsed.error()); + } + config.latency.ingest_idle_timeout_ms = *parsed; + } - if (arg == "--emit-stall-ms") { - auto raw = next_value(argc, argv, i, arg); - if (!raw) { - return std::unexpected(raw.error()); - } - auto parsed = parse_u32(*raw, arg); - if (!parsed) { - return std::unexpected(parsed.error()); - } - config.latency.emit_stall_ms = *parsed; - continue; - } + if (!ingest_consumer_delay_raw.empty()) { + auto parsed = + parse_u32(ingest_consumer_delay_raw, "--ingest-consumer-delay-ms"); + if (!parsed) { + return std::unexpected(parsed.error()); + } + config.latency.ingest_consumer_delay_ms = *parsed; + } - if (arg == "--help" || arg == "-h" || arg == "--version") { - continue; - } + if (!snapshot_copy_delay_raw.empty()) { + auto parsed = + parse_u32(snapshot_copy_delay_raw, "--snapshot-copy-delay-us"); + if (!parsed) { + return std::unexpected(parsed.error()); + } + config.latency.snapshot_copy_delay_us = *parsed; + } - return std::unexpected("unknown argument: " + std::string(arg)); - } + if (!emit_stall_raw.empty()) { + auto parsed = parse_u32(emit_stall_raw, "--emit-stall-ms"); + if (!parsed) { + return std::unexpected(parsed.error()); + } + config.latency.emit_stall_ms = *parsed; + } - return config; + return config; } -std::expected validate_runtime_config(const RuntimeConfig &config) { - if (config.input.shm_name.empty()) { - return std::unexpected("invalid input config: --shm-name must not be empty"); - } +std::expected +validate_runtime_config(const RuntimeConfig &config) { + if (config.input.shm_name.empty()) { + return std::unexpected( + "invalid input config: --shm-name must not be empty"); + } - if (config.input.zmq_endpoint.empty()) { - return std::unexpected("invalid input config: --zmq-endpoint must not be empty"); - } + if (config.input.zmq_endpoint.empty()) { + return std::unexpected( + "invalid input config: --zmq-endpoint must not be empty"); + } - if (config.outputs.rtmp.enabled && config.outputs.rtmp.urls.empty()) { - return std::unexpected("invalid RTMP config: --rtmp requires at least one --rtmp-url"); - } + if (config.outputs.rtmp.enabled && config.outputs.rtmp.urls.empty()) { + return std::unexpected( + "invalid RTMP config: --rtmp requires at least one --rtmp-url"); + } - for (const auto &url : config.outputs.rtmp.urls) { - if (url.empty()) { - return std::unexpected("invalid RTMP config: --rtmp-url must not be empty"); - } - } + for (const auto &url : config.outputs.rtmp.urls) { + if (url.empty()) { + return std::unexpected( + "invalid RTMP config: --rtmp-url must not be empty"); + } + } - if (config.outputs.rtmp.mode == RtmpMode::Domestic && config.codec != CodecType::H265) { - return std::unexpected( - "invalid mode matrix: --rtmp-mode domestic requires --codec h265 (h264+domestic is unsupported)"); - } + if (config.outputs.rtmp.mode == RtmpMode::Domestic && + config.codec != CodecType::H265) { + return std::unexpected("invalid mode matrix: --rtmp-mode domestic requires " + "--codec h265 (h264+domestic is unsupported)"); + } - if (config.outputs.rtp.enabled) { - if (!config.outputs.rtp.endpoint || config.outputs.rtp.endpoint->empty()) { - return std::unexpected("invalid RTP config: --rtp requires --rtp-endpoint"); - } + if (config.outputs.rtp.enabled) { + if (!config.outputs.rtp.endpoint || config.outputs.rtp.endpoint->empty()) { + return std::unexpected( + "invalid RTP config: --rtp requires --rtp-endpoint"); + } - auto endpoint_validation = parse_rtp_endpoint(*config.outputs.rtp.endpoint); - if (!endpoint_validation) { - return std::unexpected(endpoint_validation.error()); - } + auto endpoint_validation = parse_rtp_endpoint(*config.outputs.rtp.endpoint); + if (!endpoint_validation) { + return std::unexpected(endpoint_validation.error()); + } - if (config.outputs.rtp.payload_type < 96 || config.outputs.rtp.payload_type > 127) { - return std::unexpected( - "invalid RTP config: --rtp-payload-type must be in dynamic range [96,127]"); - } + if (config.outputs.rtp.payload_type < 96 || + config.outputs.rtp.payload_type > 127) { + return std::unexpected("invalid RTP config: --rtp-payload-type must be " + "in dynamic range [96,127]"); + } - if (config.outputs.rtp.sdp_path && config.outputs.rtp.sdp_path->empty()) { - return std::unexpected("invalid RTP config: --rtp-sdp/--sdp must not be empty"); - } - } + if (config.outputs.rtp.sdp_path && config.outputs.rtp.sdp_path->empty()) { + return std::unexpected( + "invalid RTP config: --rtp-sdp/--sdp must not be empty"); + } + } - if (config.latency.queue_size == 0) { - return std::unexpected("invalid latency config: --queue-size must be >= 1"); - } + if (config.latency.queue_size == 0) { + return std::unexpected("invalid latency config: --queue-size must be >= 1"); + } - if (config.latency.gop == 0) { - return std::unexpected("invalid latency config: --gop must be >= 1"); - } + if (config.latency.gop == 0) { + return std::unexpected("invalid latency config: --gop must be >= 1"); + } - if (config.latency.b_frames > config.latency.gop) { - return std::unexpected("invalid latency config: --b-frames must be <= --gop"); - } + if (config.latency.b_frames > config.latency.gop) { + return std::unexpected( + "invalid latency config: --b-frames must be <= --gop"); + } - if (config.latency.ingest_idle_timeout_ms == 0) { - return std::unexpected("invalid ingest config: --ingest-idle-timeout-ms must be >= 1"); - } + if (config.latency.ingest_idle_timeout_ms == 0) { + return std::unexpected( + "invalid ingest config: --ingest-idle-timeout-ms must be >= 1"); + } - return {}; + return {}; } std::string summarize_runtime_config(const RuntimeConfig &config) { - std::ostringstream ss; - ss << "input.shm=" << config.input.shm_name; - ss << ", input.zmq=" << config.input.zmq_endpoint; - ss << ", run_mode=" << to_string(config.run_mode); - ss << ", codec=" << to_string(config.codec); - ss << ", rtmp.enabled=" << (config.outputs.rtmp.enabled ? "true" : "false"); - ss << ", rtmp.mode=" << to_string(config.outputs.rtmp.mode); - ss << ", rtmp.urls=" << config.outputs.rtmp.urls.size(); - ss << ", rtp.enabled=" << (config.outputs.rtp.enabled ? "true" : "false"); - ss << ", rtp.endpoint=" << (config.outputs.rtp.endpoint ? *config.outputs.rtp.endpoint : ""); - ss << ", rtp.payload_type=" << static_cast(config.outputs.rtp.payload_type); - ss << ", rtp.sdp=" << (config.outputs.rtp.sdp_path ? *config.outputs.rtp.sdp_path : ""); - ss << ", latency.queue_size=" << config.latency.queue_size; - ss << ", latency.gop=" << config.latency.gop; - ss << ", latency.b_frames=" << config.latency.b_frames; - ss << ", latency.realtime_sync=" << (config.latency.realtime_sync ? "true" : "false"); - ss << ", latency.force_idr_on_reset=" << (config.latency.force_idr_on_reset ? "true" : "false"); - ss << ", latency.ingest_max_frames=" << config.latency.ingest_max_frames; - ss << ", latency.ingest_idle_timeout_ms=" << config.latency.ingest_idle_timeout_ms; - ss << ", latency.ingest_consumer_delay_ms=" << config.latency.ingest_consumer_delay_ms; - ss << ", latency.snapshot_copy_delay_us=" << config.latency.snapshot_copy_delay_us; - ss << ", latency.emit_stall_ms=" << config.latency.emit_stall_ms; - return ss.str(); + std::ostringstream ss; + ss << "input.shm=" << config.input.shm_name; + ss << ", input.zmq=" << config.input.zmq_endpoint; + ss << ", run_mode=" << to_string(config.run_mode); + ss << ", codec=" << to_string(config.codec); + ss << ", rtmp.enabled=" << (config.outputs.rtmp.enabled ? "true" : "false"); + ss << ", rtmp.mode=" << to_string(config.outputs.rtmp.mode); + ss << ", rtmp.urls=" << config.outputs.rtmp.urls.size(); + ss << ", rtp.enabled=" << (config.outputs.rtp.enabled ? "true" : "false"); + ss << ", rtp.endpoint=" + << (config.outputs.rtp.endpoint ? *config.outputs.rtp.endpoint + : ""); + ss << ", rtp.payload_type=" + << static_cast(config.outputs.rtp.payload_type); + ss << ", rtp.sdp=" + << (config.outputs.rtp.sdp_path ? *config.outputs.rtp.sdp_path : ""); + ss << ", latency.queue_size=" << config.latency.queue_size; + ss << ", latency.gop=" << config.latency.gop; + ss << ", latency.b_frames=" << config.latency.b_frames; + ss << ", latency.realtime_sync=" + << (config.latency.realtime_sync ? "true" : "false"); + ss << ", latency.force_idr_on_reset=" + << (config.latency.force_idr_on_reset ? "true" : "false"); + ss << ", latency.ingest_max_frames=" << config.latency.ingest_max_frames; + ss << ", latency.ingest_idle_timeout_ms=" + << config.latency.ingest_idle_timeout_ms; + ss << ", latency.ingest_consumer_delay_ms=" + << config.latency.ingest_consumer_delay_ms; + ss << ", latency.snapshot_copy_delay_us=" + << config.latency.snapshot_copy_delay_us; + ss << ", latency.emit_stall_ms=" << config.latency.emit_stall_ms; + return ss.str(); } -} +} // namespace cvmmap_streamer diff --git a/src/sim/options.cpp b/src/sim/options.cpp index 4c504ff..15bc9a4 100644 --- a/src/sim/options.cpp +++ b/src/sim/options.cpp @@ -1,8 +1,13 @@ #include "cvmmap_streamer/sim/options.hpp" +#include + #include #include +#include #include +#include +#include #include #include @@ -11,281 +16,396 @@ namespace cvmmap_streamer::sim { namespace { - std::expected next_value(int argc, char **argv, int &index, std::string_view flag_name) { - if (index + 1 >= argc) { - return std::unexpected("missing value for " + std::string(flag_name)); - } - ++index; - return std::string_view{argv[index]}; - } - - std::expected parse_u32(std::string_view raw, std::string_view flag_name) { - std::uint32_t value{0}; - const auto result = std::from_chars(raw.data(), raw.data() + raw.size(), value, 10); - if (result.ec != std::errc{} || result.ptr != raw.data() + raw.size()) { - return std::unexpected("invalid value for " + std::string(flag_name) + ": '" + std::string(raw) + "'"); - } - return value; - } - - std::expected parse_u16(std::string_view raw, std::string_view flag_name) { - std::uint16_t value{0}; - const auto result = std::from_chars(raw.data(), raw.data() + raw.size(), value, 10); - if (result.ec != std::errc{} || result.ptr != raw.data() + raw.size()) { - return std::unexpected("invalid value for " + std::string(flag_name) + ": '" + std::string(raw) + "'"); - } - return value; - } - +std::string trim_copy(std::string value) { + auto not_space = [](unsigned char c) { + return c != ' ' && c != '\t' && c != '\n' && c != '\r'; + }; + while (!value.empty() && + !not_space(static_cast(value.front()))) { + value.erase(value.begin()); + } + while (!value.empty() && + !not_space(static_cast(value.back()))) { + value.pop_back(); + } + return value; } +std::string normalize_cli_error(std::string raw_message) { + if (raw_message.find("The following argument was not expected:") != + std::string::npos) { + const auto pos = raw_message.find(':'); + if (pos != std::string::npos && pos + 1 < raw_message.size()) { + return "unknown argument: '" + trim_copy(raw_message.substr(pos + 1)) + + "'"; + } + return "unknown argument"; + } + + constexpr std::array kFlags{"--frames", + "--fps", + "--width", + "--height", + "--emit-reset-at", + "--emit-reset-every", + "--switch-format-at", + "--switch-width", + "--switch-height", + "--label", + "--shm-name", + "--zmq-endpoint"}; + + if (raw_message.find("requires at least") != std::string::npos || + raw_message.find("requires ") != std::string::npos) { + for (const auto flag : kFlags) { + if (raw_message.find(flag) != std::string::npos) { + return "missing value for " + std::string(flag); + } + } + } + + return trim_copy(std::move(raw_message)); +} + +std::expected +parse_u32(std::string_view raw, std::string_view flag_name) { + unsigned long parsed{0}; + const auto *begin = raw.data(); + const auto *end = raw.data() + raw.size(); + const auto result = std::from_chars(begin, end, parsed, 10); + if (result.ec != std::errc{} || result.ptr != end) { + return std::unexpected("invalid value for " + std::string(flag_name) + + ": '" + std::string(raw) + "'"); + } + if (parsed > std::numeric_limits::max()) { + return std::unexpected("value out of range for " + std::string(flag_name) + + ": '" + std::string(raw) + "'"); + } + return static_cast(parsed); +} + +std::expected +parse_u16(std::string_view raw, std::string_view flag_name) { + unsigned int parsed{0}; + const auto *begin = raw.data(); + const auto *end = raw.data() + raw.size(); + const auto result = std::from_chars(begin, end, parsed, 10); + if (result.ec != std::errc{} || result.ptr != end) { + return std::unexpected("invalid value for " + std::string(flag_name) + + ": '" + std::string(raw) + "'"); + } + if (parsed > std::numeric_limits::max()) { + return std::unexpected("value out of range for " + std::string(flag_name) + + ": '" + std::string(raw) + "'"); + } + return static_cast(parsed); +} + +std::expected +validate_runtime_config(const RuntimeConfig &config) { + if (config.frames == 0) { + return std::unexpected("--frames must be > 0"); + } + if (config.width == 0 || config.height == 0) { + return std::unexpected("--width and --height must be > 0"); + } + if (config.label.empty()) { + return std::unexpected("--label must not be empty"); + } + if (config.label.size() > ipc::kLabelLenMax) { + return std::unexpected("--label exceeds 24 bytes"); + } + if (config.shm_name.empty()) { + return std::unexpected("--shm-name must not be empty"); + } + if (config.zmq_endpoint.empty()) { + return std::unexpected("--zmq-endpoint must not be empty"); + } + if (config.emit_reset_at && *config.emit_reset_at == 0) { + return std::unexpected("--emit-reset-at must be in [1, --frames]"); + } + if (config.emit_reset_at && *config.emit_reset_at > config.frames) { + return std::unexpected("--emit-reset-at must be in [1, --frames]"); + } + if (config.emit_reset_every && *config.emit_reset_every == 0) { + return std::unexpected("--emit-reset-every must be > 0"); + } + if (config.switch_format_at && *config.switch_format_at == 0) { + return std::unexpected("--switch-format-at must be in [1, --frames]"); + } + if (config.switch_format_at && *config.switch_format_at > config.frames) { + return std::unexpected("--switch-format-at must be in [1, --frames]"); + } + if (config.switch_width && *config.switch_width == 0) { + return std::unexpected("--switch-width must be > 0"); + } + if (config.switch_height && *config.switch_height == 0) { + return std::unexpected("--switch-height must be > 0"); + } + + const auto payload_size = static_cast(config.width) * + static_cast(config.height) * + static_cast(config.channels); + if (payload_size > + static_cast(std::numeric_limits::max())) { + return std::unexpected( + "computed payload size exceeds cv-mmap frame_info.buffer_size range"); + } + + const auto switched_width = config.switch_width.value_or(config.width); + const auto switched_height = config.switch_height.value_or(config.height); + const auto switched_payload_size = + static_cast(switched_width) * + static_cast(switched_height) * + static_cast(config.channels); + if (switched_payload_size > + static_cast(std::numeric_limits::max())) { + return std::unexpected("computed switched payload size exceeds cv-mmap " + "frame_info.buffer_size range"); + } + + return {}; +} + +std::string build_help_text(std::string_view executable_name = "cvmmap_sim") { + RuntimeConfig defaults{}; + std::string frames_raw; + std::string fps_raw; + std::string width_raw; + std::string height_raw; + std::string emit_reset_at_raw; + std::string emit_reset_every_raw; + std::string switch_format_at_raw; + std::string switch_width_raw; + std::string switch_height_raw; + std::string label_raw; + std::string shm_name_raw; + std::string zmq_endpoint_raw; + + CLI::App app{"cv-mmap deterministic frame simulator"}; + app.name(std::string(executable_name)); + app.allow_extras(false); + app.set_help_flag("--help,-h", "show this message"); + + app.add_option("--frames", frames_raw, "total frames to emit (default: 360)"); + app.add_option("--fps", fps_raw, + "deterministic frame pacing, 0 disables sleep (default: 60)"); + app.add_option("--width", width_raw, "frame width (default: 64)"); + app.add_option("--height", height_raw, "frame height (default: 48)"); + app.add_option("--emit-reset-at", emit_reset_at_raw, + "emit MODULE_STATUS_STREAM_RESET at frame_count n"); + app.add_option("--emit-reset-every", emit_reset_every_raw, + "emit MODULE_STATUS_STREAM_RESET each n frames"); + app.add_option("--switch-format-at", switch_format_at_raw, + "switch metadata frame format at frame_count n"); + app.add_option("--switch-width", switch_width_raw, + "width after format switch (default: --width)"); + app.add_option("--switch-height", switch_height_raw, + "height after format switch (default: --height)"); + app.add_option("--label", label_raw, "stream label (<=24 bytes)") + ->default_val(defaults.label); + app.add_option("--shm-name", shm_name_raw, "POSIX shm object name") + ->default_val(defaults.shm_name); + app.add_option("--zmq-endpoint", zmq_endpoint_raw, "ZMQ pub endpoint") + ->default_val(defaults.zmq_endpoint); + + return app.help(); +} + +} // namespace + std::uint32_t RuntimeConfig::payload_size_bytes() const { - const auto width64 = static_cast(width); - const auto height64 = static_cast(height); - const auto channels64 = static_cast(channels); - return static_cast(width64 * height64 * channels64); + const auto width64 = static_cast(width); + const auto height64 = static_cast(height); + const auto channels64 = static_cast(channels); + return static_cast(width64 * height64 * channels64); } void print_help() { - constexpr std::array kHelpLines{ - "cvmmap_sim", - "Usage:", - " cvmmap_sim [options]", - "", - "Required simulation controls:", - " --frames total frames to emit (default: 360)", - " --fps deterministic frame pacing, 0 disables sleep (default: 60)", - " --width frame width (default: 64)", - " --height frame height (default: 48)", - " --emit-reset-at emit MODULE_STATUS_STREAM_RESET at frame_count n", - " --emit-reset-every emit MODULE_STATUS_STREAM_RESET each n frames", - " --switch-format-at switch metadata frame format at frame_count n", - " --switch-width width after format switch (default: --width)", - " --switch-height height after format switch (default: --height)", - "", - "Optional:", - " --label --shm-name --zmq-endpoint "}; - - for (const auto &line : kHelpLines) { - spdlog::info("{}", line); - } + std::istringstream in(build_help_text()); + for (std::string line; std::getline(in, line);) { + spdlog::info("{}", line); + } } -std::expected parse_runtime_config(int argc, char **argv) { - RuntimeConfig config{}; +ParseResult parse_runtime_config_with_cli11(int argc, char **argv, + std::string_view executable_name) { + RuntimeConfig config{}; - for (int i = 1; i < argc; ++i) { - const std::string_view arg{argv[i]}; + std::string frames_raw; + std::string fps_raw; + std::string width_raw; + std::string height_raw; + std::string emit_reset_at_raw; + std::string emit_reset_every_raw; + std::string switch_format_at_raw; + std::string switch_width_raw; + std::string switch_height_raw; + std::string label_raw; + std::string shm_name_raw; + std::string zmq_endpoint_raw; - if (arg == "--frames") { - auto value = next_value(argc, argv, i, arg); - if (!value) { - return std::unexpected(value.error()); - } - auto parsed = parse_u32(*value, arg); - if (!parsed) { - return std::unexpected(parsed.error()); - } - config.frames = *parsed; - continue; - } + CLI::App app{"cv-mmap deterministic frame simulator"}; + app.name(std::string(executable_name)); + app.allow_extras(false); + app.set_help_flag("--help,-h", "show this message"); - if (arg == "--fps") { - auto value = next_value(argc, argv, i, arg); - if (!value) { - return std::unexpected(value.error()); - } - auto parsed = parse_u32(*value, arg); - if (!parsed) { - return std::unexpected(parsed.error()); - } - config.fps = *parsed; - continue; - } + app.add_option("--frames", frames_raw, "total frames to emit (default: 360)"); + app.add_option("--fps", fps_raw, + "deterministic frame pacing, 0 disables sleep (default: 60)"); + app.add_option("--width", width_raw, "frame width (default: 64)"); + app.add_option("--height", height_raw, "frame height (default: 48)"); + app.add_option("--emit-reset-at", emit_reset_at_raw, + "emit MODULE_STATUS_STREAM_RESET at frame_count n"); + app.add_option("--emit-reset-every", emit_reset_every_raw, + "emit MODULE_STATUS_STREAM_RESET each n frames"); + app.add_option("--switch-format-at", switch_format_at_raw, + "switch metadata frame format at frame_count n"); + app.add_option("--switch-width", switch_width_raw, + "width after format switch (default: --width)"); + app.add_option("--switch-height", switch_height_raw, + "height after format switch (default: --height)"); + app.add_option("--label", label_raw, "stream label (<=24 bytes)"); + app.add_option("--shm-name", shm_name_raw, "POSIX shm object name"); + app.add_option("--zmq-endpoint", zmq_endpoint_raw, "ZMQ pub endpoint"); - if (arg == "--width") { - auto value = next_value(argc, argv, i, arg); - if (!value) { - return std::unexpected(value.error()); - } - auto parsed = parse_u16(*value, arg); - if (!parsed) { - return std::unexpected(parsed.error()); - } - config.width = *parsed; - continue; - } + if (argc <= 1) { + return ParseResult{ + .status = ParseStatus::Help, .message = app.help(), .exit_code = 0}; + } - if (arg == "--height") { - auto value = next_value(argc, argv, i, arg); - if (!value) { - return std::unexpected(value.error()); - } - auto parsed = parse_u16(*value, arg); - if (!parsed) { - return std::unexpected(parsed.error()); - } - config.height = *parsed; - continue; - } + try { + app.parse(argc, argv); + } catch (const CLI::CallForHelp &) { + return ParseResult{ + .status = ParseStatus::Help, .message = app.help(), .exit_code = 0}; + } catch (const CLI::ParseError &e) { + return ParseResult{.status = ParseStatus::Error, + .message = normalize_cli_error(e.what()), + .exit_code = 2}; + } - if (arg == "--emit-reset-at") { - auto value = next_value(argc, argv, i, arg); - if (!value) { - return std::unexpected(value.error()); - } - auto parsed = parse_u32(*value, arg); - if (!parsed) { - return std::unexpected(parsed.error()); - } - config.emit_reset_at = *parsed; - continue; - } + if (!frames_raw.empty()) { + auto parsed = parse_u32(frames_raw, "--frames"); + if (!parsed) { + return ParseResult{.status = ParseStatus::Error, + .message = parsed.error(), + .exit_code = 2}; + } + config.frames = *parsed; + } - if (arg == "--emit-reset-every") { - auto value = next_value(argc, argv, i, arg); - if (!value) { - return std::unexpected(value.error()); - } - auto parsed = parse_u32(*value, arg); - if (!parsed) { - return std::unexpected(parsed.error()); - } - config.emit_reset_every = *parsed; - continue; - } + if (!fps_raw.empty()) { + auto parsed = parse_u32(fps_raw, "--fps"); + if (!parsed) { + return ParseResult{.status = ParseStatus::Error, + .message = parsed.error(), + .exit_code = 2}; + } + config.fps = *parsed; + } - if (arg == "--switch-format-at") { - auto value = next_value(argc, argv, i, arg); - if (!value) { - return std::unexpected(value.error()); - } - auto parsed = parse_u32(*value, arg); - if (!parsed) { - return std::unexpected(parsed.error()); - } - config.switch_format_at = *parsed; - continue; - } + if (!width_raw.empty()) { + auto parsed = parse_u16(width_raw, "--width"); + if (!parsed) { + return ParseResult{.status = ParseStatus::Error, + .message = parsed.error(), + .exit_code = 2}; + } + config.width = *parsed; + } - if (arg == "--switch-width") { - auto value = next_value(argc, argv, i, arg); - if (!value) { - return std::unexpected(value.error()); - } - auto parsed = parse_u16(*value, arg); - if (!parsed) { - return std::unexpected(parsed.error()); - } - config.switch_width = *parsed; - continue; - } + if (!height_raw.empty()) { + auto parsed = parse_u16(height_raw, "--height"); + if (!parsed) { + return ParseResult{.status = ParseStatus::Error, + .message = parsed.error(), + .exit_code = 2}; + } + config.height = *parsed; + } - if (arg == "--switch-height") { - auto value = next_value(argc, argv, i, arg); - if (!value) { - return std::unexpected(value.error()); - } - auto parsed = parse_u16(*value, arg); - if (!parsed) { - return std::unexpected(parsed.error()); - } - config.switch_height = *parsed; - continue; - } + if (!emit_reset_at_raw.empty()) { + auto parsed = parse_u32(emit_reset_at_raw, "--emit-reset-at"); + if (!parsed) { + return ParseResult{.status = ParseStatus::Error, + .message = parsed.error(), + .exit_code = 2}; + } + config.emit_reset_at = *parsed; + } - if (arg == "--label") { - auto value = next_value(argc, argv, i, arg); - if (!value) { - return std::unexpected(value.error()); - } - config.label = std::string(*value); - continue; - } + if (!emit_reset_every_raw.empty()) { + auto parsed = parse_u32(emit_reset_every_raw, "--emit-reset-every"); + if (!parsed) { + return ParseResult{.status = ParseStatus::Error, + .message = parsed.error(), + .exit_code = 2}; + } + config.emit_reset_every = *parsed; + } - if (arg == "--shm-name") { - auto value = next_value(argc, argv, i, arg); - if (!value) { - return std::unexpected(value.error()); - } - config.shm_name = std::string(*value); - continue; - } + if (!switch_format_at_raw.empty()) { + auto parsed = parse_u32(switch_format_at_raw, "--switch-format-at"); + if (!parsed) { + return ParseResult{.status = ParseStatus::Error, + .message = parsed.error(), + .exit_code = 2}; + } + config.switch_format_at = *parsed; + } - if (arg == "--zmq-endpoint") { - auto value = next_value(argc, argv, i, arg); - if (!value) { - return std::unexpected(value.error()); - } - config.zmq_endpoint = std::string(*value); - continue; - } + if (!switch_width_raw.empty()) { + auto parsed = parse_u16(switch_width_raw, "--switch-width"); + if (!parsed) { + return ParseResult{.status = ParseStatus::Error, + .message = parsed.error(), + .exit_code = 2}; + } + config.switch_width = *parsed; + } - if (arg == "--help" || arg == "-h") { - continue; - } + if (!switch_height_raw.empty()) { + auto parsed = parse_u16(switch_height_raw, "--switch-height"); + if (!parsed) { + return ParseResult{.status = ParseStatus::Error, + .message = parsed.error(), + .exit_code = 2}; + } + config.switch_height = *parsed; + } - return std::unexpected("unknown argument: '" + std::string(arg) + "'"); - } + if (!label_raw.empty()) { + config.label = label_raw; + } + if (!shm_name_raw.empty()) { + config.shm_name = shm_name_raw; + } + if (!zmq_endpoint_raw.empty()) { + config.zmq_endpoint = zmq_endpoint_raw; + } - if (config.frames == 0) { - return std::unexpected("--frames must be > 0"); - } - if (config.width == 0 || config.height == 0) { - return std::unexpected("--width and --height must be > 0"); - } - if (config.label.empty()) { - return std::unexpected("--label must not be empty"); - } - if (config.label.size() > ipc::kLabelLenMax) { - return std::unexpected("--label exceeds 24 bytes"); - } - if (config.shm_name.empty()) { - return std::unexpected("--shm-name must not be empty"); - } - if (config.zmq_endpoint.empty()) { - return std::unexpected("--zmq-endpoint must not be empty"); - } - if (config.emit_reset_at && *config.emit_reset_at == 0) { - return std::unexpected("--emit-reset-at must be in [1, --frames]"); - } - if (config.emit_reset_at && *config.emit_reset_at > config.frames) { - return std::unexpected("--emit-reset-at must be in [1, --frames]"); - } - if (config.emit_reset_every && *config.emit_reset_every == 0) { - return std::unexpected("--emit-reset-every must be > 0"); - } - if (config.switch_format_at && *config.switch_format_at == 0) { - return std::unexpected("--switch-format-at must be in [1, --frames]"); - } - if (config.switch_format_at && *config.switch_format_at > config.frames) { - return std::unexpected("--switch-format-at must be in [1, --frames]"); - } - if (config.switch_width && *config.switch_width == 0) { - return std::unexpected("--switch-width must be > 0"); - } - if (config.switch_height && *config.switch_height == 0) { - return std::unexpected("--switch-height must be > 0"); - } + auto validation = validate_runtime_config(config); + if (!validation) { + return ParseResult{.status = ParseStatus::Error, + .message = validation.error(), + .exit_code = 2}; + } - const auto payload_size = static_cast(config.width) * - static_cast(config.height) * - static_cast(config.channels); - if (payload_size > static_cast(std::numeric_limits::max())) { - return std::unexpected("computed payload size exceeds cv-mmap frame_info.buffer_size range"); - } - - const auto switched_width = config.switch_width.value_or(config.width); - const auto switched_height = config.switch_height.value_or(config.height); - const auto switched_payload_size = - static_cast(switched_width) * - static_cast(switched_height) * - static_cast(config.channels); - if (switched_payload_size > static_cast(std::numeric_limits::max())) { - return std::unexpected("computed switched payload size exceeds cv-mmap frame_info.buffer_size range"); - } - - return config; + return ParseResult{ + .status = ParseStatus::Ok, .config = config, .exit_code = 0}; } +std::expected parse_runtime_config(int argc, + char **argv) { + auto parsed = parse_runtime_config_with_cli11(argc, argv); + if (parsed.status == ParseStatus::Ok) { + return parsed.config; + } + if (!parsed.message.empty()) { + return std::unexpected(parsed.message); + } + return std::unexpected("failed to parse runtime options"); } + +} // namespace cvmmap_streamer::sim