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