refactor(cli): unify streamer and testers on CLI11 parsing
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user