refactor(cli): unify streamer and testers on CLI11 parsing

This commit is contained in:
2026-03-06 08:49:58 +08:00
parent d5df65927b
commit 529de17eea
6 changed files with 344 additions and 250 deletions
@@ -8,6 +8,8 @@
#include <string_view>
#include <vector>
#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<std::uint32_t> emit_reset_at{};
std::optional<std::uint32_t> 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<std::string> 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<RuntimeConfig, std::string> parse_runtime_config(int argc, char **argv);
+203 -2
View File
@@ -42,10 +42,20 @@ std::string normalize_cli_error(std::string raw_message) {
return "unknown argument";
}
constexpr std::array<std::string_view, 17> kFlags{"--codec",
constexpr std::array<std::string_view, 27> 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<std::uint64_t, std::string>
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<std::uint16_t, std::string>
parse_u16(std::string_view raw, std::string_view flag_name) {
std::uint16_t value{0};
@@ -150,6 +173,17 @@ std::expected<RunMode, std::string> parse_run_mode(std::string_view raw) {
"' (expected: pipeline|ingest)");
}
std::expected<InputMode, std::string> 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<RtmpMode, std::string> 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<RuntimeConfig, std::string> 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<std::string> rtmp_urls_raw;
@@ -254,6 +300,15 @@ std::expected<RuntimeConfig, std::string> 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<RuntimeConfig, std::string> 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<RuntimeConfig, std::string> 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<RuntimeConfig, std::string> 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<RuntimeConfig, std::string> 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<std::uint16_t>::max()) {
return std::unexpected("value out of range for --dummy-width: '" +
dummy_width_raw + "'");
}
config.dummy.width = static_cast<std::uint16_t>(*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<std::uint16_t>::max()) {
return std::unexpected("value out of range for --dummy-height: '" +
dummy_height_raw + "'");
}
config.dummy.height = static_cast<std::uint16_t>(*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);
+1 -1
View File
@@ -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"};
}
+6 -8
View File
@@ -1,4 +1,3 @@
#include "cvmmap_streamer/common.h"
#include "cvmmap_streamer/config/runtime_config.hpp"
#include <spdlog/spdlog.h>
@@ -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;
}
+59 -172
View File
@@ -1,6 +1,5 @@
#include <algorithm>
#include <array>
#include <charconv>
#include <chrono>
#include <cstdint>
#include <cstring>
@@ -22,6 +21,7 @@
#include <sys/types.h>
#include <unistd.h>
#include <CLI/CLI.hpp>
#include <spdlog/spdlog.h>
namespace {
@@ -206,42 +206,15 @@ std::string_view to_string(VideoSignal signal) {
}
}
[[nodiscard]]
std::expected<std::uint32_t, std::string>
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<std::uint16_t, std::string>
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<std::uint16_t>(*parsed);
}
[[nodiscard]]
std::expected<ExpectMode, std::string> 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<ExpectMode, std::string> 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 <h264|h265-enhanced|h265-domestic> [options]");
spdlog::info("");
spdlog::info("Options:");
spdlog::info(" --mode <value> Expected publish signaling mode (required)");
spdlog::info(" --listen-host <ipv4> Listen address (default: 127.0.0.1)");
spdlog::info(" --listen-port <1-65535> Listen port (default: 19350)");
spdlog::info(" --video-threshold <n> Required matching video tag count (default: 1)");
spdlog::info(" --timeout-ms <ms> Session timeout milliseconds (default: 5000)");
spdlog::info(" --self-test Spawn built-in local RTMP publisher");
spdlog::info(" --self-test-send-mode <value> 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<Config, std::string> 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<std::string> 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<std::string_view, std::string> {
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("parse_error");
}
return std::unexpected(std::format("unknown argument: {}", arg));
}
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<int>(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<int>(ExitCode::Success);
}
if (config_or_error.error() == "parse_error") {
return static_cast<int>(ExitCode::InvalidArgs);
}
spdlog::error("{}", config_or_error.error());
print_usage();
return static_cast<int>(ExitCode::InvalidArgs);
}
+49 -66
View File
@@ -1,4 +1,5 @@
#include <array>
#include <CLI/CLI.hpp>
#include <cerrno>
#include <chrono>
#include <cstddef>
@@ -6,12 +7,13 @@
#include <cstdlib>
#include <cstring>
#include <expected>
#include <format>
#include <fstream>
#include <optional>
#include <span>
#include <sstream>
#include <string>
#include <string_view>
#include <thread>
#include <vector>
#include <netinet/in.h>
@@ -22,8 +24,6 @@
#include <spdlog/spdlog.h>
#include "cvmmap_streamer/common.h"
namespace {
// RFC3550 RTP header constants
@@ -186,65 +186,59 @@ std::expected<int, std::string> createUdpSocket(std::uint16_t port) {
}
// Parse command-line arguments
std::expected<Config, std::string> parseArgs(int argc, char **argv) {
std::expected<Config, int> 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::uint16_t>(std::stoul(argv[++i]));
} else if (arg == "--expect-pt" && i + 1 < argc) {
config.expectedPt = static_cast<std::uint8_t>(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<std::uint8_t>(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 <num> UDP port to listen on (default: 5004)");
spdlog::info(" --expect-pt <num> Expected payload type (0-127)");
spdlog::info(" --sdp <path> SDP file to validate against");
spdlog::info(" --decode-hook <cmd> Optional command to validate payload");
spdlog::info(" --packet-threshold <n> Minimum packets to consider success (default: 10)");
spdlog::info(" --timeout-ms <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<const std::uint8_t> payload) {
if (hookCmd.empty()) {
@@ -289,20 +283,9 @@ bool runDecodeHook(std::string_view hookCmd, std::span<const std::uint8_t> 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;