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
+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(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<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);
}
+50 -67
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;