diff --git a/include/cvmmap_streamer/config/runtime_config.hpp b/include/cvmmap_streamer/config/runtime_config.hpp index 7bf1370..b30e06b 100644 --- a/include/cvmmap_streamer/config/runtime_config.hpp +++ b/include/cvmmap_streamer/config/runtime_config.hpp @@ -8,6 +8,8 @@ #include #include +#include "cvmmap_streamer/ipc/contracts.hpp" + namespace cvmmap_streamer { enum class CodecType { @@ -20,6 +22,11 @@ enum class RunMode { Ingest, }; +enum class InputMode { + Real, + Dummy, +}; + enum class RtmpMode { Enhanced, Domestic, @@ -30,6 +37,21 @@ struct InputConfig { std::string zmq_endpoint{"ipc:///tmp/cvmmap_default"}; }; +struct DummyInputConfig { + std::uint32_t frames{0}; + 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::string label{"dummy"}; + std::uint8_t channels{3}; + ipc::Depth depth{ipc::Depth::U8}; + ipc::PixelFormat pixel_format{ipc::PixelFormat::BGR}; + std::uint64_t start_timestamp_ns{1'000'000'000ull}; + std::uint32_t startup_delay_ms{100}; +}; + struct RtmpOutputConfig { bool enabled{false}; std::vector urls{}; @@ -65,6 +87,8 @@ struct LatencyConfig { struct RuntimeConfig { InputConfig input{}; + DummyInputConfig dummy{}; + InputMode input_mode{InputMode::Real}; RunMode run_mode{RunMode::Pipeline}; CodecType codec{CodecType::H264}; OutputsConfig outputs{}; @@ -75,6 +99,7 @@ struct RuntimeConfig { std::string_view to_string(CodecType codec); std::string_view to_string(RunMode mode); +std::string_view to_string(InputMode mode); std::string_view to_string(RtmpMode mode); std::expected parse_runtime_config(int argc, char **argv); diff --git a/src/config/runtime_config.cpp b/src/config/runtime_config.cpp index d6d32a4..b2ef92d 100644 --- a/src/config/runtime_config.cpp +++ b/src/config/runtime_config.cpp @@ -42,10 +42,20 @@ std::string normalize_cli_error(std::string raw_message) { return "unknown argument"; } - constexpr std::array kFlags{"--codec", + constexpr std::array kFlags{"--codec", "--run-mode", + "--input-mode", "--shm-name", "--zmq-endpoint", + "--dummy-frames", + "--dummy-fps", + "--dummy-width", + "--dummy-height", + "--dummy-reset-at", + "--dummy-reset-every", + "--dummy-label", + "--dummy-start-timestamp-ns", + "--dummy-startup-delay-ms", "--rtmp-url", "--rtmp-mode", "--rtp-endpoint", @@ -85,6 +95,19 @@ parse_u32(std::string_view raw, std::string_view flag_name) { return value; } +std::expected +parse_u64(std::string_view raw, std::string_view flag_name) { + std::uint64_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}; @@ -150,6 +173,17 @@ std::expected parse_run_mode(std::string_view raw) { "' (expected: pipeline|ingest)"); } +std::expected parse_input_mode(std::string_view raw) { + if (raw == "real") { + return InputMode::Real; + } + if (raw == "dummy") { + return InputMode::Dummy; + } + return std::unexpected("invalid input mode: '" + std::string(raw) + + "' (expected: real|dummy)"); +} + std::expected parse_rtmp_mode(std::string_view raw) { if (raw == "enhanced") { return RtmpMode::Enhanced; @@ -220,6 +254,17 @@ std::string_view to_string(RunMode mode) { } } +std::string_view to_string(InputMode mode) { + switch (mode) { + case InputMode::Real: + return "real"; + case InputMode::Dummy: + return "dummy"; + default: + return "unknown"; + } +} + std::string_view to_string(RtmpMode mode) { switch (mode) { case RtmpMode::Enhanced: @@ -237,6 +282,7 @@ std::expected parse_runtime_config(int argc, std::string codec_raw; std::string run_mode_raw; + std::string input_mode_raw; std::string shm_name_raw; std::string zmq_endpoint_raw; std::vector rtmp_urls_raw; @@ -254,6 +300,15 @@ std::expected parse_runtime_config(int argc, std::string ingest_consumer_delay_raw; std::string snapshot_copy_delay_raw; std::string emit_stall_raw; + std::string dummy_frames_raw; + std::string dummy_fps_raw; + std::string dummy_width_raw; + std::string dummy_height_raw; + std::string dummy_reset_at_raw; + std::string dummy_reset_every_raw; + std::string dummy_label_raw; + std::string dummy_start_timestamp_ns_raw; + std::string dummy_startup_delay_ms_raw; bool rtmp_enabled{false}; bool rtp_enabled{false}; @@ -265,8 +320,18 @@ std::expected parse_runtime_config(int argc, app.add_option("--codec", codec_raw); app.add_option("--run-mode", run_mode_raw); + app.add_option("--input-mode", input_mode_raw); app.add_option("--shm-name", shm_name_raw); app.add_option("--zmq-endpoint", zmq_endpoint_raw); + app.add_option("--dummy-frames", dummy_frames_raw); + app.add_option("--dummy-fps", dummy_fps_raw); + app.add_option("--dummy-width", dummy_width_raw); + app.add_option("--dummy-height", dummy_height_raw); + app.add_option("--dummy-reset-at", dummy_reset_at_raw); + app.add_option("--dummy-reset-every", dummy_reset_every_raw); + app.add_option("--dummy-label", dummy_label_raw); + app.add_option("--dummy-start-timestamp-ns", dummy_start_timestamp_ns_raw); + app.add_option("--dummy-startup-delay-ms", dummy_startup_delay_ms_raw); app.add_flag("--rtmp", rtmp_enabled); app.add_option("--rtmp-url", rtmp_urls_raw); app.add_option("--rtmp-mode", rtmp_mode_raw); @@ -290,7 +355,11 @@ std::expected parse_runtime_config(int argc, try { app.parse(argc, argv); } catch (const CLI::ParseError &e) { - return std::unexpected(normalize_cli_error(e.what())); + const auto exit_code = app.exit(e); + if (exit_code == 0) { + return std::unexpected("help"); + } + return std::unexpected("parse_error"); } if (!codec_raw.empty()) { @@ -309,6 +378,14 @@ std::expected parse_runtime_config(int argc, config.run_mode = *run_mode; } + if (!input_mode_raw.empty()) { + auto input_mode = parse_input_mode(input_mode_raw); + if (!input_mode) { + return std::unexpected(input_mode.error()); + } + config.input_mode = *input_mode; + } + if (!shm_name_raw.empty()) { config.input.shm_name = shm_name_raw; } @@ -317,6 +394,84 @@ std::expected parse_runtime_config(int argc, config.input.zmq_endpoint = zmq_endpoint_raw; } + if (!dummy_frames_raw.empty()) { + auto parsed = parse_u32(dummy_frames_raw, "--dummy-frames"); + if (!parsed) { + return std::unexpected(parsed.error()); + } + config.dummy.frames = *parsed; + } + + if (!dummy_fps_raw.empty()) { + auto parsed = parse_u32(dummy_fps_raw, "--dummy-fps"); + if (!parsed) { + return std::unexpected(parsed.error()); + } + config.dummy.fps = *parsed; + } + + if (!dummy_width_raw.empty()) { + auto parsed = parse_u32(dummy_width_raw, "--dummy-width"); + if (!parsed) { + return std::unexpected(parsed.error()); + } + if (*parsed > std::numeric_limits::max()) { + return std::unexpected("value out of range for --dummy-width: '" + + dummy_width_raw + "'"); + } + config.dummy.width = static_cast(*parsed); + } + + if (!dummy_height_raw.empty()) { + auto parsed = parse_u32(dummy_height_raw, "--dummy-height"); + if (!parsed) { + return std::unexpected(parsed.error()); + } + if (*parsed > std::numeric_limits::max()) { + return std::unexpected("value out of range for --dummy-height: '" + + dummy_height_raw + "'"); + } + config.dummy.height = static_cast(*parsed); + } + + if (!dummy_reset_at_raw.empty()) { + auto parsed = parse_u32(dummy_reset_at_raw, "--dummy-reset-at"); + if (!parsed) { + return std::unexpected(parsed.error()); + } + config.dummy.emit_reset_at = *parsed; + } + + if (!dummy_reset_every_raw.empty()) { + auto parsed = parse_u32(dummy_reset_every_raw, "--dummy-reset-every"); + if (!parsed) { + return std::unexpected(parsed.error()); + } + config.dummy.emit_reset_every = *parsed; + } + + if (!dummy_label_raw.empty()) { + config.dummy.label = dummy_label_raw; + } + + if (!dummy_start_timestamp_ns_raw.empty()) { + auto parsed = + parse_u64(dummy_start_timestamp_ns_raw, "--dummy-start-timestamp-ns"); + if (!parsed) { + return std::unexpected(parsed.error()); + } + config.dummy.start_timestamp_ns = *parsed; + } + + if (!dummy_startup_delay_ms_raw.empty()) { + auto parsed = + parse_u32(dummy_startup_delay_ms_raw, "--dummy-startup-delay-ms"); + if (!parsed) { + return std::unexpected(parsed.error()); + } + config.dummy.startup_delay_ms = *parsed; + } + config.outputs.rtmp.enabled = rtmp_enabled; if (!rtmp_urls_raw.empty()) { config.outputs.rtmp.enabled = true; @@ -454,6 +609,13 @@ validate_runtime_config(const RuntimeConfig &config) { "invalid input config: --shm-name must not be empty"); } + if (config.input_mode == InputMode::Dummy && + config.input.shm_name.starts_with("cvmmap://")) { + return std::unexpected( + "invalid input config: --input-mode dummy requires POSIX --shm-name, " + "not cvmmap:// URI"); + } + if (config.input.zmq_endpoint.empty()) { return std::unexpected( "invalid input config: --zmq-endpoint must not be empty"); @@ -518,6 +680,39 @@ validate_runtime_config(const RuntimeConfig &config) { "invalid ingest config: --ingest-idle-timeout-ms must be >= 1"); } + if (config.dummy.width == 0 || config.dummy.height == 0) { + return std::unexpected( + "invalid dummy config: --dummy-width and --dummy-height must be >= 1"); + } + + if (config.dummy.channels == 0) { + return std::unexpected("invalid dummy config: channels must be >= 1"); + } + + if (config.dummy.label.empty()) { + return std::unexpected("invalid dummy config: --dummy-label must not be empty"); + } + + if (config.dummy.label.size() > ipc::kLabelLenMax) { + return std::unexpected("invalid dummy config: --dummy-label exceeds 24 bytes"); + } + + if (config.dummy.emit_reset_at && *config.dummy.emit_reset_at == 0) { + return std::unexpected( + "invalid dummy config: --dummy-reset-at must be >= 1"); + } + + if (config.dummy.frames > 0 && config.dummy.emit_reset_at && + *config.dummy.emit_reset_at > config.dummy.frames) { + return std::unexpected("invalid dummy config: --dummy-reset-at must be <= " + "--dummy-frames when --dummy-frames > 0"); + } + + if (config.dummy.emit_reset_every && *config.dummy.emit_reset_every == 0) { + return std::unexpected( + "invalid dummy config: --dummy-reset-every must be >= 1"); + } + return {}; } @@ -526,6 +721,12 @@ std::string summarize_runtime_config(const RuntimeConfig &config) { ss << "input.shm=" << config.input.shm_name; ss << ", input.zmq=" << config.input.zmq_endpoint; ss << ", run_mode=" << to_string(config.run_mode); + ss << ", input_mode=" << to_string(config.input_mode); + ss << ", dummy.frames=" << config.dummy.frames; + ss << ", dummy.fps=" << config.dummy.fps; + ss << ", dummy.size=" << config.dummy.width << "x" << config.dummy.height; + ss << ", dummy.label=" << config.dummy.label; + ss << ", dummy.start_timestamp_ns=" << config.dummy.start_timestamp_ns; ss << ", codec=" << to_string(config.codec); ss << ", rtmp.enabled=" << (config.outputs.rtmp.enabled ? "true" : "false"); ss << ", rtmp.mode=" << to_string(config.outputs.rtmp.mode); diff --git a/src/ipc/help.cpp b/src/ipc/help.cpp index 9f622c2..a8e5658 100644 --- a/src/ipc/help.cpp +++ b/src/ipc/help.cpp @@ -18,7 +18,7 @@ namespace { "", "Examples:", " cvmmap_streamer --help", - " cvmmap_sim --help", + " cvmmap_streamer --run-mode pipeline --input-mode dummy --help", " rtp_receiver_tester --help"}; } diff --git a/src/main_streamer.cpp b/src/main_streamer.cpp index d904b8f..510bbdf 100644 --- a/src/main_streamer.cpp +++ b/src/main_streamer.cpp @@ -1,4 +1,3 @@ -#include "cvmmap_streamer/common.h" #include "cvmmap_streamer/config/runtime_config.hpp" #include @@ -11,22 +10,21 @@ int run_nvenc_pipeline(const RuntimeConfig &config); } int main(int argc, char **argv) { - if (argc <= 1 || cvmmap_streamer::has_help_flag(argc, argv)) { - cvmmap_streamer::print_help("cvmmap_streamer"); - return 0; - } - auto config = cvmmap_streamer::parse_runtime_config(argc, argv); if (!config) { + if (config.error() == "help") { + return 0; + } + if (config.error() == "parse_error") { + return 2; + } spdlog::error("{}", config.error()); - cvmmap_streamer::print_help("cvmmap_streamer"); return 2; } auto validation = cvmmap_streamer::validate_runtime_config(*config); if (!validation) { spdlog::error("{}", validation.error()); - cvmmap_streamer::print_help("cvmmap_streamer"); return 2; } diff --git a/src/testers/rtmp_stub_tester.cpp b/src/testers/rtmp_stub_tester.cpp index f57946d..86f24ba 100644 --- a/src/testers/rtmp_stub_tester.cpp +++ b/src/testers/rtmp_stub_tester.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include #include @@ -22,6 +21,7 @@ #include #include +#include #include namespace { @@ -206,42 +206,15 @@ std::string_view to_string(VideoSignal signal) { } } -[[nodiscard]] -std::expected -parse_u32_arg(std::string_view raw, std::string_view name) { - std::uint32_t value{0}; - const auto *begin = raw.data(); - const auto *end = raw.data() + raw.size(); - const auto parsed = std::from_chars(begin, end, value, 10); - const bool success = parsed.ec == std::errc{} && parsed.ptr == end; - if (!success) { - return std::unexpected(std::format("invalid value for {}: '{}'", name, raw)); - } - return value; -} - -[[nodiscard]] -std::expected -parse_u16_arg(std::string_view raw, std::string_view name) { - auto parsed = parse_u32_arg(raw, name); - if (!parsed) { - return std::unexpected(parsed.error()); - } - if (*parsed == 0 || *parsed > 65535) { - return std::unexpected(std::format("{} must be in range [1, 65535]", name)); - } - return static_cast(*parsed); -} - [[nodiscard]] std::expected parse_mode(std::string_view raw) { if (raw == "h264") { return ExpectMode::H264; } - if (raw == "h265-enhanced" || raw == "enhanced") { + if (raw == "h265-enhanced") { return ExpectMode::H265Enhanced; } - if (raw == "h265-domestic" || raw == "domestic") { + if (raw == "h265-domestic") { return ExpectMode::H265Domestic; } return std::unexpected(std::format( @@ -249,157 +222,75 @@ std::expected parse_mode(std::string_view raw) { raw)); } -void print_usage() { - spdlog::info("rtmp_stub_tester - standalone RTMP ingest validator"); - spdlog::info(""); - spdlog::info("Usage:"); - spdlog::info(" rtmp_stub_tester --mode [options]"); - spdlog::info(""); - spdlog::info("Options:"); - spdlog::info(" --mode Expected publish signaling mode (required)"); - spdlog::info(" --listen-host Listen address (default: 127.0.0.1)"); - spdlog::info(" --listen-port <1-65535> Listen port (default: 19350)"); - spdlog::info(" --video-threshold Required matching video tag count (default: 1)"); - spdlog::info(" --timeout-ms Session timeout milliseconds (default: 5000)"); - spdlog::info(" --self-test Spawn built-in local RTMP publisher"); - spdlog::info(" --self-test-send-mode Self-test publish mode (default: same as --mode)"); - spdlog::info(" --verbose, -v Enable debug logging"); - spdlog::info(" --help, -h Show this message"); - spdlog::info(""); - spdlog::info("Exit codes:"); - spdlog::info(" 0 PASS"); - spdlog::info(" 1 Invalid arguments"); - spdlog::info(" 2 Socket/listen/accept failure"); - spdlog::info(" 3 RTMP handshake failure"); - spdlog::info(" 4 Missing connect/createStream/publish pipeline"); - spdlog::info(" 5 Matching video threshold not met"); - spdlog::info(" 6 Mode mismatch detected"); - spdlog::info(" 7 RTMP chunk/protocol parse failure"); - spdlog::info(" 8 Self-test client failure"); - spdlog::info(""); - spdlog::info("Examples:"); - spdlog::info(" rtmp_stub_tester --mode h264 --self-test"); - spdlog::info(" rtmp_stub_tester --mode h265-enhanced --self-test"); - spdlog::info(" rtmp_stub_tester --mode h265-domestic --self-test"); - spdlog::info(" rtmp_stub_tester --mode h265-enhanced --self-test --self-test-send-mode h265-domestic"); -} - [[nodiscard]] std::expected parse_args(int argc, char **argv) { Config config; - bool mode_set = false; + std::string mode_raw; + std::string self_test_send_mode_raw; + const std::vector accepted_modes{"h264", "h265-enhanced", "h265-domestic"}; - for (int i = 1; i < argc; ++i) { - std::string_view arg{argv[i]}; + CLI::App app{"rtmp_stub_tester - standalone RTMP ingest validator"}; + app.allow_extras(false); + app.set_help_flag("--help,-h", "show this message"); - const auto need_value = [&](std::string_view flag) -> std::expected { - if (i + 1 >= argc) { - return std::unexpected(std::format("missing value for {}", flag)); - } - ++i; - return std::string_view{argv[i]}; - }; + auto *mode_option = app.add_option("--mode", mode_raw, "expected publish signaling mode"); + mode_option->required(); + mode_option->check(CLI::IsMember(accepted_modes)); - if (arg == "--help" || arg == "-h") { + app.add_option("--listen-host", config.listen_host, "listen address"); + + app.add_option("--listen-port", config.listen_port, "listen port"); + app.add_option("--video-threshold", config.video_threshold, "required matching video tag count"); + app.add_option("--timeout-ms", config.timeout_ms, "session timeout milliseconds"); + + auto *self_test_flag = app.add_flag("--self-test", config.self_test, "spawn built-in local RTMP publisher"); + auto *self_test_send_mode_option = app.add_option( + "--self-test-send-mode", + self_test_send_mode_raw, + "self-test publish mode"); + self_test_send_mode_option->check(CLI::IsMember(accepted_modes)); + self_test_send_mode_option->needs(self_test_flag); + + app.add_flag("--verbose,-v", config.verbose, "enable debug logging"); + + try { + app.parse(argc, argv); + } catch (const CLI::ParseError &e) { + const auto exit_code = app.exit(e); + if (exit_code == 0) { return std::unexpected("help"); } - if (arg == "--mode") { - auto raw = need_value(arg); - if (!raw) { - return std::unexpected(raw.error()); - } - auto mode = parse_mode(*raw); - if (!mode) { - return std::unexpected(mode.error()); - } - config.expect_mode = *mode; - mode_set = true; - continue; - } - if (arg == "--listen-host") { - auto raw = need_value(arg); - if (!raw) { - return std::unexpected(raw.error()); - } - config.listen_host = std::string(*raw); - continue; - } - if (arg == "--listen-port") { - auto raw = need_value(arg); - if (!raw) { - return std::unexpected(raw.error()); - } - auto port = parse_u16_arg(*raw, "--listen-port"); - if (!port) { - return std::unexpected(port.error()); - } - config.listen_port = *port; - continue; - } - if (arg == "--video-threshold") { - auto raw = need_value(arg); - if (!raw) { - return std::unexpected(raw.error()); - } - auto value = parse_u32_arg(*raw, "--video-threshold"); - if (!value) { - return std::unexpected(value.error()); - } - if (*value == 0) { - return std::unexpected("--video-threshold must be >= 1"); - } - config.video_threshold = *value; - continue; - } - if (arg == "--timeout-ms") { - auto raw = need_value(arg); - if (!raw) { - return std::unexpected(raw.error()); - } - auto value = parse_u32_arg(*raw, "--timeout-ms"); - if (!value) { - return std::unexpected(value.error()); - } - if (*value < 100) { - return std::unexpected("--timeout-ms must be >= 100"); - } - config.timeout_ms = *value; - continue; - } - if (arg == "--self-test") { - config.self_test = true; - continue; - } - if (arg == "--self-test-send-mode") { - auto raw = need_value(arg); - if (!raw) { - return std::unexpected(raw.error()); - } - auto mode = parse_mode(*raw); - if (!mode) { - return std::unexpected(mode.error()); - } - config.self_test_send_mode = *mode; - continue; - } - if (arg == "--verbose" || arg == "-v") { - config.verbose = true; - continue; - } - - return std::unexpected(std::format("unknown argument: {}", arg)); + return std::unexpected("parse_error"); } - if (!mode_set) { - return std::unexpected("--mode is required"); + if (config.listen_port == 0) { + return std::unexpected("--listen-port must be in range [1, 65535]"); } if (config.listen_host.empty()) { return std::unexpected("--listen-host must not be empty"); } - if (config.self_test_send_mode && !config.self_test) { - return std::unexpected("--self-test-send-mode requires --self-test"); + if (config.video_threshold == 0) { + return std::unexpected("--video-threshold must be >= 1"); + } + + if (config.timeout_ms < 100) { + return std::unexpected("--timeout-ms must be >= 100"); + } + + auto mode = parse_mode(mode_raw); + if (!mode) { + return std::unexpected(mode.error()); + } + config.expect_mode = *mode; + + if (!self_test_send_mode_raw.empty()) { + auto self_test_send_mode = parse_mode(self_test_send_mode_raw); + if (!self_test_send_mode) { + return std::unexpected(self_test_send_mode.error()); + } + config.self_test_send_mode = *self_test_send_mode; } return config; @@ -1904,19 +1795,15 @@ void print_summary(const Config &config, const Stats &stats) { } int main(int argc, char **argv) { - if (argc <= 1) { - print_usage(); - return static_cast(ExitCode::InvalidArgs); - } - auto config_or_error = parse_args(argc, argv); if (!config_or_error) { if (config_or_error.error() == "help") { - print_usage(); return static_cast(ExitCode::Success); } + if (config_or_error.error() == "parse_error") { + return static_cast(ExitCode::InvalidArgs); + } spdlog::error("{}", config_or_error.error()); - print_usage(); return static_cast(ExitCode::InvalidArgs); } diff --git a/src/testers/rtp_receiver_tester.cpp b/src/testers/rtp_receiver_tester.cpp index 35d082e..093bce4 100644 --- a/src/testers/rtp_receiver_tester.cpp +++ b/src/testers/rtp_receiver_tester.cpp @@ -1,4 +1,5 @@ -#include +#include + #include #include #include @@ -6,12 +7,13 @@ #include #include #include +#include #include #include +#include #include #include #include -#include #include #include @@ -22,8 +24,6 @@ #include -#include "cvmmap_streamer/common.h" - namespace { // RFC3550 RTP header constants @@ -186,65 +186,59 @@ std::expected createUdpSocket(std::uint16_t port) { } // Parse command-line arguments -std::expected parseArgs(int argc, char **argv) { +std::expected parseArgs(int argc, char **argv) { Config config; - for (int i = 1; i < argc; ++i) { - std::string_view arg(argv[i]); + std::uint16_t port = config.port; + std::uint32_t expectedPtRaw = 0; + std::string sdpFileRaw; + std::string decodeHookRaw; + std::uint32_t packetThresholdRaw = config.packetThreshold; + std::uint32_t timeoutMsRaw = config.timeoutMs; - if (arg == "--help" || arg == "-h") { - return std::unexpected("help"); - } else if (arg == "--port" && i + 1 < argc) { - config.port = static_cast(std::stoul(argv[++i])); - } else if (arg == "--expect-pt" && i + 1 < argc) { - config.expectedPt = static_cast(std::stoul(argv[++i])); - } else if (arg == "--sdp" && i + 1 < argc) { - config.sdpFile = argv[++i]; - } else if (arg == "--decode-hook" && i + 1 < argc) { - config.decodeHook = argv[++i]; - } else if (arg == "--packet-threshold" && i + 1 < argc) { - config.packetThreshold = std::stoul(argv[++i]); - } else if (arg == "--timeout-ms" && i + 1 < argc) { - config.timeoutMs = std::stoul(argv[++i]); - } else if (arg == "--verbose" || arg == "-v") { - config.verbose = true; - } + CLI::App app{"rtp_receiver_tester - UDP RTP packet receiver and validator"}; + app.allow_extras(false); + app.set_help_flag("--help,-h", "Show this message"); + app.add_option("--port", port, "UDP port to listen on"); + auto *expectPtOption = + app.add_option("--expect-pt", expectedPtRaw, "Expected payload type (0-127)") + ->check(CLI::Range(0, 127)); + auto *sdpOption = app.add_option("--sdp", sdpFileRaw, "SDP file to validate against"); + auto *decodeHookOption = + app.add_option("--decode-hook", decodeHookRaw, "Optional command to validate payload"); + app.add_option("--packet-threshold", packetThresholdRaw, "Minimum packets to consider success"); + app.add_option("--timeout-ms", timeoutMsRaw, "Max time to wait for packets"); + app.add_flag("--verbose,-v", config.verbose, "Enable verbose logging"); + + try { + app.parse(argc, argv); + } catch (const CLI::ParseError &e) { + const auto exitCode = app.exit(e); + return std::unexpected(exitCode == 0 ? 0 : 1); + } + + if (argc <= 1) { + spdlog::info("{}", app.help()); + return std::unexpected(1); + } + + config.port = port; + config.packetThreshold = packetThresholdRaw; + config.timeoutMs = timeoutMsRaw; + + if (expectPtOption->count() > 0) { + config.expectedPt = static_cast(expectedPtRaw); + } + if (sdpOption->count() > 0) { + config.sdpFile = std::move(sdpFileRaw); + } + if (decodeHookOption->count() > 0) { + config.decodeHook = std::move(decodeHookRaw); } return config; } -// Print usage -void printRtpReceiverUsage() { - spdlog::info("rtp_receiver_tester - UDP RTP packet receiver and validator"); - spdlog::info(""); - spdlog::info("Usage:"); - spdlog::info(" rtp_receiver_tester [options]"); - spdlog::info(""); - spdlog::info("Options:"); - spdlog::info(" --port UDP port to listen on (default: 5004)"); - spdlog::info(" --expect-pt Expected payload type (0-127)"); - spdlog::info(" --sdp SDP file to validate against"); - spdlog::info(" --decode-hook Optional command to validate payload"); - spdlog::info(" --packet-threshold Minimum packets to consider success (default: 10)"); - spdlog::info(" --timeout-ms Max time to wait for packets (default: 5000)"); - spdlog::info(" --verbose, -v Enable verbose logging"); - spdlog::info(" --help, -h Show this message"); - spdlog::info(""); - spdlog::info("Examples:"); - spdlog::info(" rtp_receiver_tester --port 5004 --expect-pt 96"); - spdlog::info(" rtp_receiver_tester --port 5004 --sdp /tmp/stream.sdp"); - spdlog::info(""); - spdlog::info("Exit codes:"); - spdlog::info(" 0 Success (packets received, PT matches)"); - spdlog::info(" 1 Invalid arguments"); - spdlog::info(" 2 Socket/bind error"); - spdlog::info(" 3 Payload type mismatch"); - spdlog::info(" 4 Packet threshold not met"); - spdlog::info(" 5 SDP validation failed"); - spdlog::info(" 6 Decode hook failed"); -} - // Run optional decode hook bool runDecodeHook(std::string_view hookCmd, std::span payload) { if (hookCmd.empty()) { @@ -289,20 +283,9 @@ bool runDecodeHook(std::string_view hookCmd, std::span paylo } // namespace int main(int argc, char **argv) { - if (argc <= 1 || cvmmap_streamer::has_help_flag(argc, argv)) { - printRtpReceiverUsage(); - return (argc <= 1) ? 1 : 0; - } - auto configResult = parseArgs(argc, argv); if (!configResult) { - if (configResult.error() == "help") { - printRtpReceiverUsage(); - return 0; - } - spdlog::error("Argument error: {}", configResult.error()); - printRtpReceiverUsage(); - return 1; + return configResult.error(); } const auto &config = *configResult;