refactor(cli): restructure runtime parsing around CLI11 validators
Refactor runtime option parsing to use structured CLI11 option bindings, validators, and grouped help output instead of the previous raw-string collection and manual field-by-field parsing. This introduces reusable canonicalization helpers, endpoint validation, non-empty validators, and boolean-assignment checks so invalid CLI input is rejected earlier and help/default rendering comes directly from the option definitions. The static help text is updated to advertise the new keep-stream-on-reset flag added by the streaming work.
This commit is contained in:
+448
-224
@@ -48,15 +48,31 @@ std::string normalize_cli_error(std::string raw_message) {
|
||||
return trim_copy(std::move(raw_message));
|
||||
}
|
||||
|
||||
std::expected<std::uint32_t, std::string> parse_u32(std::string_view raw, std::string_view field_name) {
|
||||
std::uint32_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(field_name) + ": '" + std::string(raw) + "'");
|
||||
template <typename Parser>
|
||||
CLI::Validator canonicalize_option(Parser parser) {
|
||||
return CLI::Validator(
|
||||
[parser = std::move(parser)](std::string &value) {
|
||||
auto canonical = parser(value);
|
||||
if (!canonical) {
|
||||
return canonical.error();
|
||||
}
|
||||
return value;
|
||||
value = std::move(*canonical);
|
||||
return std::string{};
|
||||
},
|
||||
std::string{},
|
||||
std::string{});
|
||||
}
|
||||
|
||||
CLI::Validator require_non_empty(std::string_view option_name) {
|
||||
return CLI::Validator(
|
||||
[label = std::string(option_name)](std::string &value) {
|
||||
if (!value.empty()) {
|
||||
return std::string{};
|
||||
}
|
||||
return "invalid value for " + label + ": must not be empty";
|
||||
},
|
||||
std::string{},
|
||||
std::string{});
|
||||
}
|
||||
|
||||
std::expected<std::uint16_t, std::string> parse_u16(std::string_view raw, std::string_view field_name) {
|
||||
@@ -70,31 +86,6 @@ std::expected<std::uint16_t, std::string> parse_u16(std::string_view raw, std::s
|
||||
return value;
|
||||
}
|
||||
|
||||
std::expected<std::size_t, std::string> parse_size(std::string_view raw, std::string_view field_name) {
|
||||
unsigned long long parsed{0};
|
||||
const auto *begin = raw.data();
|
||||
const auto *end = raw.data() + raw.size();
|
||||
const auto result = std::from_chars(begin, end, parsed, 10);
|
||||
if (result.ec != std::errc{} || result.ptr != end) {
|
||||
return std::unexpected("invalid value for " + std::string(field_name) + ": '" + std::string(raw) + "'");
|
||||
}
|
||||
if (parsed > static_cast<unsigned long long>(std::numeric_limits<std::size_t>::max())) {
|
||||
return std::unexpected("value out of range for " + std::string(field_name) + ": '" + std::string(raw) + "'");
|
||||
}
|
||||
return static_cast<std::size_t>(parsed);
|
||||
}
|
||||
|
||||
std::expected<bool, std::string> parse_bool(std::string_view raw, std::string_view field_name) {
|
||||
if (raw == "true" || raw == "1") {
|
||||
return true;
|
||||
}
|
||||
if (raw == "false" || raw == "0") {
|
||||
return false;
|
||||
}
|
||||
return std::unexpected(
|
||||
"invalid value for " + std::string(field_name) + ": '" + std::string(raw) + "' (expected: true|false|1|0)");
|
||||
}
|
||||
|
||||
std::expected<CodecType, std::string> parse_codec(std::string_view raw) {
|
||||
if (raw == "h264") {
|
||||
return CodecType::H264;
|
||||
@@ -181,6 +172,62 @@ std::expected<McapCompression, std::string> parse_mcap_compression_impl(std::str
|
||||
return std::unexpected("invalid mcap compression: '" + std::string(raw) + "' (expected: none|lz4|zstd)");
|
||||
}
|
||||
|
||||
std::expected<std::string, std::string> canonicalize_codec(std::string_view raw) {
|
||||
auto parsed = parse_codec(raw);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
return std::string(to_string(*parsed));
|
||||
}
|
||||
|
||||
std::expected<std::string, std::string> canonicalize_run_mode(std::string_view raw) {
|
||||
auto parsed = parse_run_mode(raw);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
return std::string(to_string(*parsed));
|
||||
}
|
||||
|
||||
std::expected<std::string, std::string> canonicalize_rtmp_transport(std::string_view raw) {
|
||||
auto parsed = parse_rtmp_transport(raw);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
return std::string(to_string(*parsed));
|
||||
}
|
||||
|
||||
std::expected<std::string, std::string> canonicalize_encoder_backend(std::string_view raw) {
|
||||
auto parsed = parse_encoder_backend(raw);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
return std::string(to_string(*parsed));
|
||||
}
|
||||
|
||||
std::expected<std::string, std::string> canonicalize_encoder_device(std::string_view raw) {
|
||||
auto parsed = parse_encoder_device(raw);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
return std::string(to_string(*parsed));
|
||||
}
|
||||
|
||||
std::expected<std::string, std::string> canonicalize_input_video_source(std::string_view raw) {
|
||||
auto parsed = parse_input_video_source(raw);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
return std::string(to_string(*parsed));
|
||||
}
|
||||
|
||||
std::expected<std::string, std::string> canonicalize_mcap_compression(std::string_view raw) {
|
||||
auto parsed = parse_mcap_compression_impl(raw);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
return std::string(to_string(*parsed));
|
||||
}
|
||||
|
||||
std::expected<std::pair<std::string, std::uint16_t>, std::string> parse_rtp_endpoint(std::string_view endpoint) {
|
||||
if (endpoint.empty()) {
|
||||
return std::unexpected("invalid RTP config: endpoint must not be empty");
|
||||
@@ -203,6 +250,48 @@ std::expected<std::pair<std::string, std::uint16_t>, std::string> parse_rtp_endp
|
||||
return std::pair{std::string(host), *parsed_port};
|
||||
}
|
||||
|
||||
CLI::Validator validate_rtp_endpoint() {
|
||||
return CLI::Validator(
|
||||
[](std::string &value) {
|
||||
auto parsed = parse_rtp_endpoint(value);
|
||||
if (!parsed) {
|
||||
return parsed.error();
|
||||
}
|
||||
return std::string{};
|
||||
},
|
||||
std::string{},
|
||||
std::string{});
|
||||
}
|
||||
|
||||
std::optional<std::string> find_disallowed_boolean_assignment(int argc, char **argv) {
|
||||
struct FlagPair {
|
||||
std::string_view positive;
|
||||
std::string_view negative;
|
||||
};
|
||||
|
||||
constexpr std::array<FlagPair, 6> kFlagPairs{{
|
||||
{"--rtmp", "--no-rtmp"},
|
||||
{"--rtp", "--no-rtp"},
|
||||
{"--mcap", "--no-mcap"},
|
||||
{"--realtime-sync", "--no-realtime-sync"},
|
||||
{"--force-idr-on-reset", "--no-force-idr-on-reset"},
|
||||
{"--keep-stream-on-reset", "--no-keep-stream-on-reset"},
|
||||
}};
|
||||
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
const std::string_view arg{argv[i]};
|
||||
for (const auto &pair : kFlagPairs) {
|
||||
if (arg.rfind(std::string(pair.positive) + "=", 0) == 0 ||
|
||||
arg.rfind(std::string(pair.negative) + "=", 0) == 0) {
|
||||
return "invalid boolean flag syntax: " + std::string(arg) + " (use " + std::string(pair.positive) +
|
||||
" or " + std::string(pair.negative) + ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
std::optional<T> toml_value(const toml::table &table, std::string_view path) {
|
||||
auto node = table.at_path(path);
|
||||
@@ -428,6 +517,9 @@ std::expected<void, std::string> apply_toml_file(RuntimeConfig &config, const st
|
||||
if (auto value = toml_value<bool>(table, "latency.force_idr_on_reset")) {
|
||||
config.latency.force_idr_on_reset = *value;
|
||||
}
|
||||
if (auto value = toml_value<bool>(table, "latency.keep_stream_on_reset")) {
|
||||
config.latency.keep_stream_on_reset = *value;
|
||||
}
|
||||
{
|
||||
auto value = toml_nonnegative_integral<std::uint32_t>(
|
||||
table,
|
||||
@@ -599,87 +691,257 @@ std::string_view to_string(McapCompression compression) {
|
||||
|
||||
std::expected<RuntimeConfig, std::string> parse_runtime_config(int argc, char **argv) {
|
||||
RuntimeConfig config = RuntimeConfig::defaults();
|
||||
const RuntimeConfig defaults = config;
|
||||
|
||||
std::string config_path_raw{};
|
||||
std::string input_uri_raw{};
|
||||
std::string input_nats_url_raw{};
|
||||
std::string input_video_source_raw{};
|
||||
std::string run_mode_raw{};
|
||||
std::string codec_raw{};
|
||||
std::string encoder_backend_raw{};
|
||||
std::string encoder_device_raw{};
|
||||
std::string rtmp_transport_raw{};
|
||||
std::string rtmp_ffmpeg_path_raw{};
|
||||
std::vector<std::string> rtmp_urls_raw{};
|
||||
std::string rtp_endpoint_raw{};
|
||||
std::string rtp_payload_type_raw{};
|
||||
std::string rtp_sdp_raw{};
|
||||
std::string mcap_path_raw{};
|
||||
std::string mcap_topic_raw{};
|
||||
std::string mcap_depth_topic_raw{};
|
||||
std::string mcap_calibration_topic_raw{};
|
||||
std::string mcap_depth_calibration_topic_raw{};
|
||||
std::string mcap_pose_topic_raw{};
|
||||
std::string mcap_body_topic_raw{};
|
||||
std::string mcap_frame_id_raw{};
|
||||
std::string mcap_compression_raw{};
|
||||
std::string queue_size_raw{};
|
||||
std::string gop_raw{};
|
||||
std::string b_frames_raw{};
|
||||
std::string realtime_sync_raw{};
|
||||
std::string force_idr_on_reset_raw{};
|
||||
std::string ingest_max_frames_raw{};
|
||||
std::string ingest_idle_timeout_raw{};
|
||||
std::string ingest_consumer_delay_raw{};
|
||||
std::string snapshot_copy_delay_raw{};
|
||||
std::string emit_stall_raw{};
|
||||
bool rtmp_enabled{false};
|
||||
bool rtp_enabled{false};
|
||||
bool mcap_enabled{false};
|
||||
std::optional<std::string> config_path_override{};
|
||||
std::optional<std::string> input_uri_override{};
|
||||
std::optional<std::string> input_nats_url_override{};
|
||||
std::optional<std::string> input_video_source_override{};
|
||||
std::optional<std::string> run_mode_override{};
|
||||
std::optional<std::string> codec_override{};
|
||||
std::optional<std::string> encoder_backend_override{};
|
||||
std::optional<std::string> encoder_device_override{};
|
||||
std::optional<bool> rtmp_enabled_override{};
|
||||
std::vector<std::string> rtmp_urls_override{};
|
||||
std::optional<std::string> rtmp_transport_override{};
|
||||
std::optional<std::string> rtmp_ffmpeg_path_override{};
|
||||
std::optional<bool> rtp_enabled_override{};
|
||||
std::optional<std::string> rtp_endpoint_override{};
|
||||
std::optional<std::uint16_t> rtp_payload_type_override{};
|
||||
std::optional<std::string> rtp_sdp_override{};
|
||||
std::optional<bool> mcap_enabled_override{};
|
||||
std::optional<std::string> mcap_path_override{};
|
||||
std::optional<std::string> mcap_topic_override{};
|
||||
std::optional<std::string> mcap_depth_topic_override{};
|
||||
std::optional<std::string> mcap_calibration_topic_override{};
|
||||
std::optional<std::string> mcap_depth_calibration_topic_override{};
|
||||
std::optional<std::string> mcap_pose_topic_override{};
|
||||
std::optional<std::string> mcap_body_topic_override{};
|
||||
std::optional<std::string> mcap_frame_id_override{};
|
||||
std::optional<std::string> mcap_compression_override{};
|
||||
std::optional<std::size_t> queue_size_override{};
|
||||
std::optional<std::uint32_t> gop_override{};
|
||||
std::optional<std::uint32_t> b_frames_override{};
|
||||
std::optional<bool> realtime_sync_override{};
|
||||
std::optional<bool> force_idr_on_reset_override{};
|
||||
std::optional<bool> keep_stream_on_reset_override{};
|
||||
std::optional<std::uint32_t> ingest_max_frames_override{};
|
||||
std::optional<std::uint32_t> ingest_idle_timeout_override{};
|
||||
std::optional<std::uint32_t> ingest_consumer_delay_override{};
|
||||
std::optional<std::uint32_t> snapshot_copy_delay_override{};
|
||||
std::optional<std::uint32_t> emit_stall_override{};
|
||||
bool version_requested{false};
|
||||
|
||||
CLI::App app{"cvmmap-streamer runtime options"};
|
||||
app.allow_extras(false);
|
||||
app.set_help_flag("--help,-h", "show this message");
|
||||
app.set_help_flag("--help,-h", "Show this message");
|
||||
app.get_formatter()->column_width(36);
|
||||
|
||||
app.add_option("--config", config_path_raw);
|
||||
app.add_option("--input-uri", input_uri_raw);
|
||||
app.add_option("--nats-url", input_nats_url_raw);
|
||||
app.add_option("--input-video-source", input_video_source_raw);
|
||||
app.add_option("--run-mode", run_mode_raw);
|
||||
app.add_option("--codec", codec_raw);
|
||||
app.add_option("--encoder-backend", encoder_backend_raw);
|
||||
app.add_option("--encoder-device", encoder_device_raw);
|
||||
app.add_flag("--rtmp", rtmp_enabled);
|
||||
app.add_option("--rtmp-url", rtmp_urls_raw);
|
||||
app.add_option("--rtmp-transport", rtmp_transport_raw);
|
||||
app.add_option("--rtmp-ffmpeg", rtmp_ffmpeg_path_raw);
|
||||
app.add_flag("--rtp", rtp_enabled);
|
||||
app.add_option("--rtp-endpoint", rtp_endpoint_raw);
|
||||
app.add_option("--rtp-payload-type", rtp_payload_type_raw);
|
||||
auto *rtp_sdp = app.add_option("--rtp-sdp", rtp_sdp_raw);
|
||||
app.add_option("--sdp", rtp_sdp_raw)->excludes(rtp_sdp);
|
||||
app.add_flag("--mcap", mcap_enabled);
|
||||
app.add_option("--mcap-path", mcap_path_raw);
|
||||
app.add_option("--mcap-topic", mcap_topic_raw);
|
||||
app.add_option("--mcap-depth-topic", mcap_depth_topic_raw);
|
||||
app.add_option("--mcap-calibration-topic", mcap_calibration_topic_raw);
|
||||
app.add_option("--mcap-depth-calibration-topic", mcap_depth_calibration_topic_raw);
|
||||
app.add_option("--mcap-pose-topic", mcap_pose_topic_raw);
|
||||
app.add_option("--mcap-body-topic", mcap_body_topic_raw);
|
||||
app.add_option("--mcap-frame-id", mcap_frame_id_raw);
|
||||
app.add_option("--mcap-compression", mcap_compression_raw);
|
||||
app.add_option("--queue-size", queue_size_raw);
|
||||
app.add_option("--gop", gop_raw);
|
||||
app.add_option("--b-frames", b_frames_raw);
|
||||
app.add_option("--realtime-sync", realtime_sync_raw);
|
||||
app.add_option("--force-idr-on-reset", force_idr_on_reset_raw);
|
||||
app.add_option("--ingest-max-frames", ingest_max_frames_raw);
|
||||
app.add_option("--ingest-idle-timeout-ms", ingest_idle_timeout_raw);
|
||||
app.add_option("--ingest-consumer-delay-ms", ingest_consumer_delay_raw);
|
||||
app.add_option("--snapshot-copy-delay-us", snapshot_copy_delay_raw);
|
||||
app.add_option("--emit-stall-ms", emit_stall_raw);
|
||||
app.add_flag("--version", version_requested);
|
||||
app.add_option("--config", config_path_override, "Load runtime config from TOML")
|
||||
->group("General")
|
||||
->type_name("PATH")
|
||||
->check(CLI::ExistingFile);
|
||||
app.add_flag("--version", version_requested, "Show version information")
|
||||
->group("General")
|
||||
->disable_flag_override();
|
||||
|
||||
app.add_option("--input-uri", input_uri_override, "cv-mmap source URI")
|
||||
->group("Input")
|
||||
->type_name("URI")
|
||||
->check(require_non_empty("--input-uri"))
|
||||
->default_str(defaults.input.uri);
|
||||
app.add_option("--nats-url", input_nats_url_override, "NATS server URL for control traffic")
|
||||
->group("Input")
|
||||
->type_name("URL")
|
||||
->check(require_non_empty("--nats-url"))
|
||||
->default_str(defaults.input.nats_url);
|
||||
app.add_option("--input-video-source", input_video_source_override, "Preferred upstream video source")
|
||||
->group("Input")
|
||||
->type_name("SOURCE")
|
||||
->transform(canonicalize_option(canonicalize_input_video_source))
|
||||
->default_str(std::string(to_string(defaults.input.video_source)));
|
||||
app.add_option("--run-mode", run_mode_override, "Execution mode")
|
||||
->group("General")
|
||||
->type_name("MODE")
|
||||
->transform(canonicalize_option(canonicalize_run_mode))
|
||||
->default_str(std::string(to_string(defaults.run_mode)));
|
||||
|
||||
app.add_option("--codec", codec_override, "Output video codec")
|
||||
->group("Encoder")
|
||||
->type_name("CODEC")
|
||||
->transform(canonicalize_option(canonicalize_codec))
|
||||
->default_str(std::string(to_string(defaults.encoder.codec)));
|
||||
app.add_option("--encoder-backend", encoder_backend_override, "Encoder backend implementation")
|
||||
->group("Encoder")
|
||||
->type_name("BACKEND")
|
||||
->transform(canonicalize_option(canonicalize_encoder_backend))
|
||||
->default_str(std::string(to_string(defaults.encoder.backend)));
|
||||
app.add_option("--encoder-device", encoder_device_override, "Preferred encoder device")
|
||||
->group("Encoder")
|
||||
->type_name("DEVICE")
|
||||
->transform(canonicalize_option(canonicalize_encoder_device))
|
||||
->default_str(std::string(to_string(defaults.encoder.device)));
|
||||
app.add_option("--gop", gop_override, "Encoder GOP length in frames")
|
||||
->group("Encoder")
|
||||
->type_name("FRAMES")
|
||||
->check(CLI::PositiveNumber)
|
||||
->default_str(std::to_string(defaults.encoder.gop));
|
||||
app.add_option("--b-frames", b_frames_override, "Encoder B-frame count")
|
||||
->group("Encoder")
|
||||
->type_name("COUNT")
|
||||
->check(CLI::NonNegativeNumber)
|
||||
->default_str(std::to_string(defaults.encoder.b_frames));
|
||||
|
||||
app.add_flag("--rtmp,!--no-rtmp", rtmp_enabled_override, "Enable or disable RTMP output")
|
||||
->group("RTMP Output")
|
||||
->default_str(defaults.outputs.rtmp.enabled ? "true" : "false")
|
||||
->disable_flag_override();
|
||||
app.add_option("--rtmp-url", rtmp_urls_override, "RTMP destination URL; repeat to publish to multiple sinks")
|
||||
->group("RTMP Output")
|
||||
->type_name("URL")
|
||||
->check(require_non_empty("--rtmp-url"));
|
||||
app.add_option("--rtmp-transport", rtmp_transport_override, "RTMP transport backend")
|
||||
->group("RTMP Output")
|
||||
->type_name("MODE")
|
||||
->transform(canonicalize_option(canonicalize_rtmp_transport))
|
||||
->default_str(std::string(to_string(defaults.outputs.rtmp.transport)));
|
||||
app.add_option("--rtmp-ffmpeg", rtmp_ffmpeg_path_override, "ffmpeg binary path for ffmpeg_process transport")
|
||||
->group("RTMP Output")
|
||||
->type_name("PATH")
|
||||
->check(require_non_empty("--rtmp-ffmpeg"))
|
||||
->default_str(defaults.outputs.rtmp.ffmpeg_path);
|
||||
|
||||
app.add_flag("--rtp,!--no-rtp", rtp_enabled_override, "Enable or disable RTP output")
|
||||
->group("RTP Output")
|
||||
->default_str(defaults.outputs.rtp.enabled ? "true" : "false")
|
||||
->disable_flag_override();
|
||||
app.add_option("--rtp-endpoint", rtp_endpoint_override, "RTP destination in <host>:<port> format")
|
||||
->group("RTP Output")
|
||||
->type_name("HOST:PORT")
|
||||
->check(validate_rtp_endpoint());
|
||||
app.add_option("--rtp-payload-type", rtp_payload_type_override, "Dynamic RTP payload type")
|
||||
->group("RTP Output")
|
||||
->type_name("PT")
|
||||
->check(CLI::Range(96, 127))
|
||||
->default_str(std::to_string(defaults.outputs.rtp.payload_type));
|
||||
auto *rtp_sdp = app.add_option("--rtp-sdp", rtp_sdp_override, "Write an SDP sidecar file for RTP output")
|
||||
->group("RTP Output")
|
||||
->type_name("PATH")
|
||||
->check(require_non_empty("--rtp-sdp"));
|
||||
app.add_option("--sdp", rtp_sdp_override, "Alias for --rtp-sdp")
|
||||
->group("RTP Output")
|
||||
->type_name("PATH")
|
||||
->check(require_non_empty("--sdp"))
|
||||
->excludes(rtp_sdp);
|
||||
|
||||
app.add_flag("--mcap,!--no-mcap", mcap_enabled_override, "Enable or disable MCAP recording")
|
||||
->group("MCAP Record")
|
||||
->default_str(defaults.record.mcap.enabled ? "true" : "false")
|
||||
->disable_flag_override();
|
||||
app.add_option("--mcap-path", mcap_path_override, "MCAP output file path")
|
||||
->group("MCAP Record")
|
||||
->type_name("PATH")
|
||||
->check(require_non_empty("--mcap-path"))
|
||||
->default_str(defaults.record.mcap.path);
|
||||
app.add_option("--mcap-topic", mcap_topic_override, "Foxglove compressed video topic name")
|
||||
->group("MCAP Record")
|
||||
->type_name("TOPIC")
|
||||
->check(require_non_empty("--mcap-topic"))
|
||||
->default_str(defaults.record.mcap.topic);
|
||||
app.add_option("--mcap-depth-topic", mcap_depth_topic_override, "Depth image topic name")
|
||||
->group("MCAP Record")
|
||||
->type_name("TOPIC")
|
||||
->check(require_non_empty("--mcap-depth-topic"))
|
||||
->default_str(defaults.record.mcap.depth_topic);
|
||||
app.add_option("--mcap-calibration-topic", mcap_calibration_topic_override, "RGB camera calibration topic name")
|
||||
->group("MCAP Record")
|
||||
->type_name("TOPIC")
|
||||
->check(require_non_empty("--mcap-calibration-topic"))
|
||||
->default_str(defaults.record.mcap.calibration_topic);
|
||||
app.add_option(
|
||||
"--mcap-depth-calibration-topic",
|
||||
mcap_depth_calibration_topic_override,
|
||||
"Depth camera calibration topic name")
|
||||
->group("MCAP Record")
|
||||
->type_name("TOPIC")
|
||||
->check(require_non_empty("--mcap-depth-calibration-topic"))
|
||||
->default_str(defaults.record.mcap.depth_calibration_topic);
|
||||
app.add_option("--mcap-pose-topic", mcap_pose_topic_override, "Pose topic name")
|
||||
->group("MCAP Record")
|
||||
->type_name("TOPIC")
|
||||
->check(require_non_empty("--mcap-pose-topic"))
|
||||
->default_str(defaults.record.mcap.pose_topic);
|
||||
app.add_option("--mcap-body-topic", mcap_body_topic_override, "Body tracking topic name")
|
||||
->group("MCAP Record")
|
||||
->type_name("TOPIC")
|
||||
->check(require_non_empty("--mcap-body-topic"))
|
||||
->default_str(defaults.record.mcap.body_topic);
|
||||
app.add_option("--mcap-frame-id", mcap_frame_id_override, "Frame ID written into MCAP messages")
|
||||
->group("MCAP Record")
|
||||
->type_name("ID")
|
||||
->check(require_non_empty("--mcap-frame-id"))
|
||||
->default_str(defaults.record.mcap.frame_id);
|
||||
app.add_option("--mcap-compression", mcap_compression_override, "MCAP chunk compression mode")
|
||||
->group("MCAP Record")
|
||||
->type_name("MODE")
|
||||
->transform(canonicalize_option(canonicalize_mcap_compression))
|
||||
->default_str(std::string(to_string(defaults.record.mcap.compression)));
|
||||
|
||||
app.add_option("--queue-size", queue_size_override, "Pipeline queue depth")
|
||||
->group("Latency")
|
||||
->type_name("FRAMES")
|
||||
->check(CLI::PositiveNumber)
|
||||
->default_str(std::to_string(defaults.latency.queue_size));
|
||||
app.add_flag("--realtime-sync,!--no-realtime-sync", realtime_sync_override, "Enable or disable realtime pacing")
|
||||
->group("Latency")
|
||||
->default_str(defaults.latency.realtime_sync ? "true" : "false")
|
||||
->disable_flag_override();
|
||||
app.add_flag(
|
||||
"--force-idr-on-reset,!--no-force-idr-on-reset",
|
||||
force_idr_on_reset_override,
|
||||
"Force a keyframe after upstream stream_reset")
|
||||
->group("Latency")
|
||||
->default_str(defaults.latency.force_idr_on_reset ? "true" : "false")
|
||||
->disable_flag_override();
|
||||
app.add_flag(
|
||||
"--keep-stream-on-reset,!--no-keep-stream-on-reset",
|
||||
keep_stream_on_reset_override,
|
||||
"Keep RTMP/RTP live outputs open across upstream stream_reset")
|
||||
->group("Latency")
|
||||
->default_str(defaults.latency.keep_stream_on_reset ? "true" : "false")
|
||||
->disable_flag_override();
|
||||
app.add_option("--ingest-max-frames", ingest_max_frames_override, "Maximum frames to ingest before exit; 0 disables the limit")
|
||||
->group("Latency")
|
||||
->type_name("FRAMES")
|
||||
->check(CLI::NonNegativeNumber)
|
||||
->default_str(std::to_string(defaults.latency.ingest_max_frames));
|
||||
app.add_option("--ingest-idle-timeout-ms", ingest_idle_timeout_override, "Stop ingest if no consumer activity occurs for this long; 0 disables the timeout")
|
||||
->group("Latency")
|
||||
->type_name("MS")
|
||||
->check(CLI::NonNegativeNumber)
|
||||
->default_str(std::to_string(defaults.latency.ingest_idle_timeout_ms));
|
||||
app.add_option(
|
||||
"--ingest-consumer-delay-ms",
|
||||
ingest_consumer_delay_override,
|
||||
"Artificial ingest-side consumer delay")
|
||||
->group("Latency")
|
||||
->type_name("MS")
|
||||
->check(CLI::NonNegativeNumber)
|
||||
->default_str(std::to_string(defaults.latency.ingest_consumer_delay_ms));
|
||||
app.add_option("--snapshot-copy-delay-us", snapshot_copy_delay_override, "Artificial delay before snapshot copy completion")
|
||||
->group("Latency")
|
||||
->type_name("US")
|
||||
->check(CLI::NonNegativeNumber)
|
||||
->default_str(std::to_string(defaults.latency.snapshot_copy_delay_us));
|
||||
app.add_option("--emit-stall-ms", emit_stall_override, "Artificial stall before emitting downstream frames")
|
||||
->group("Latency")
|
||||
->type_name("MS")
|
||||
->check(CLI::NonNegativeNumber)
|
||||
->default_str(std::to_string(defaults.latency.emit_stall_ms));
|
||||
|
||||
if (auto invalid_boolean_assignment = find_disallowed_boolean_assignment(argc, argv)) {
|
||||
return std::unexpected(*invalid_boolean_assignment);
|
||||
}
|
||||
|
||||
try {
|
||||
app.parse(argc, argv);
|
||||
@@ -691,202 +953,164 @@ std::expected<RuntimeConfig, std::string> parse_runtime_config(int argc, char **
|
||||
return std::unexpected(normalize_cli_error(e.what()));
|
||||
}
|
||||
|
||||
if (!config_path_raw.empty()) {
|
||||
auto load_result = apply_toml_file(config, config_path_raw);
|
||||
if (config_path_override) {
|
||||
auto load_result = apply_toml_file(config, *config_path_override);
|
||||
if (!load_result) {
|
||||
return std::unexpected(load_result.error());
|
||||
}
|
||||
}
|
||||
|
||||
if (!input_uri_raw.empty()) {
|
||||
config.input.uri = input_uri_raw;
|
||||
if (input_uri_override) {
|
||||
config.input.uri = *input_uri_override;
|
||||
}
|
||||
if (!input_nats_url_raw.empty()) {
|
||||
config.input.nats_url = input_nats_url_raw;
|
||||
if (input_nats_url_override) {
|
||||
config.input.nats_url = *input_nats_url_override;
|
||||
}
|
||||
if (!input_video_source_raw.empty()) {
|
||||
auto parsed = parse_input_video_source(input_video_source_raw);
|
||||
if (input_video_source_override) {
|
||||
auto parsed = parse_input_video_source(*input_video_source_override);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
config.input.video_source = *parsed;
|
||||
}
|
||||
if (!run_mode_raw.empty()) {
|
||||
auto parsed = parse_run_mode(run_mode_raw);
|
||||
if (run_mode_override) {
|
||||
auto parsed = parse_run_mode(*run_mode_override);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
config.run_mode = *parsed;
|
||||
}
|
||||
if (!codec_raw.empty()) {
|
||||
auto parsed = parse_codec(codec_raw);
|
||||
if (codec_override) {
|
||||
auto parsed = parse_codec(*codec_override);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
config.encoder.codec = *parsed;
|
||||
}
|
||||
if (!encoder_backend_raw.empty()) {
|
||||
auto parsed = parse_encoder_backend(encoder_backend_raw);
|
||||
if (encoder_backend_override) {
|
||||
auto parsed = parse_encoder_backend(*encoder_backend_override);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
config.encoder.backend = *parsed;
|
||||
}
|
||||
if (!encoder_device_raw.empty()) {
|
||||
auto parsed = parse_encoder_device(encoder_device_raw);
|
||||
if (encoder_device_override) {
|
||||
auto parsed = parse_encoder_device(*encoder_device_override);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
config.encoder.device = *parsed;
|
||||
}
|
||||
|
||||
config.outputs.rtmp.enabled = config.outputs.rtmp.enabled || rtmp_enabled;
|
||||
if (!rtmp_urls_raw.empty()) {
|
||||
if (!rtmp_urls_override.empty()) {
|
||||
config.outputs.rtmp.enabled = true;
|
||||
config.outputs.rtmp.urls = std::move(rtmp_urls_raw);
|
||||
config.outputs.rtmp.urls = std::move(rtmp_urls_override);
|
||||
}
|
||||
if (!rtmp_transport_raw.empty()) {
|
||||
auto parsed = parse_rtmp_transport(rtmp_transport_raw);
|
||||
if (rtmp_transport_override) {
|
||||
auto parsed = parse_rtmp_transport(*rtmp_transport_override);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
config.outputs.rtmp.transport = *parsed;
|
||||
}
|
||||
if (!rtmp_ffmpeg_path_raw.empty()) {
|
||||
config.outputs.rtmp.ffmpeg_path = rtmp_ffmpeg_path_raw;
|
||||
if (rtmp_ffmpeg_path_override) {
|
||||
config.outputs.rtmp.ffmpeg_path = *rtmp_ffmpeg_path_override;
|
||||
}
|
||||
config.outputs.rtp.enabled = config.outputs.rtp.enabled || rtp_enabled;
|
||||
if (!rtp_endpoint_raw.empty()) {
|
||||
if (rtmp_enabled_override) {
|
||||
config.outputs.rtmp.enabled = *rtmp_enabled_override;
|
||||
}
|
||||
if (rtp_endpoint_override) {
|
||||
config.outputs.rtp.enabled = true;
|
||||
config.outputs.rtp.endpoint = rtp_endpoint_raw;
|
||||
}
|
||||
if (!rtp_payload_type_raw.empty()) {
|
||||
auto value = parse_u32(rtp_payload_type_raw, "--rtp-payload-type");
|
||||
if (!value) {
|
||||
return std::unexpected(value.error());
|
||||
}
|
||||
if (*value > std::numeric_limits<std::uint8_t>::max()) {
|
||||
return std::unexpected("value out of range for --rtp-payload-type: '" + rtp_payload_type_raw + "'");
|
||||
config.outputs.rtp.endpoint = *rtp_endpoint_override;
|
||||
}
|
||||
if (rtp_payload_type_override) {
|
||||
config.outputs.rtp.enabled = true;
|
||||
config.outputs.rtp.payload_type = static_cast<std::uint8_t>(*value);
|
||||
config.outputs.rtp.payload_type = static_cast<std::uint8_t>(*rtp_payload_type_override);
|
||||
}
|
||||
if (!rtp_sdp_raw.empty()) {
|
||||
if (rtp_sdp_override) {
|
||||
config.outputs.rtp.enabled = true;
|
||||
config.outputs.rtp.sdp_path = rtp_sdp_raw;
|
||||
config.outputs.rtp.sdp_path = *rtp_sdp_override;
|
||||
}
|
||||
if (rtp_enabled_override) {
|
||||
config.outputs.rtp.enabled = *rtp_enabled_override;
|
||||
}
|
||||
|
||||
config.record.mcap.enabled = config.record.mcap.enabled || mcap_enabled;
|
||||
if (!mcap_path_raw.empty()) {
|
||||
if (mcap_path_override) {
|
||||
config.record.mcap.enabled = true;
|
||||
config.record.mcap.path = mcap_path_raw;
|
||||
config.record.mcap.path = *mcap_path_override;
|
||||
}
|
||||
if (!mcap_topic_raw.empty()) {
|
||||
if (mcap_topic_override) {
|
||||
config.record.mcap.enabled = true;
|
||||
config.record.mcap.topic = mcap_topic_raw;
|
||||
config.record.mcap.topic = *mcap_topic_override;
|
||||
}
|
||||
if (!mcap_depth_topic_raw.empty()) {
|
||||
if (mcap_depth_topic_override) {
|
||||
config.record.mcap.enabled = true;
|
||||
config.record.mcap.depth_topic = mcap_depth_topic_raw;
|
||||
config.record.mcap.depth_topic = *mcap_depth_topic_override;
|
||||
}
|
||||
if (!mcap_calibration_topic_raw.empty()) {
|
||||
if (mcap_calibration_topic_override) {
|
||||
config.record.mcap.enabled = true;
|
||||
config.record.mcap.calibration_topic = mcap_calibration_topic_raw;
|
||||
config.record.mcap.calibration_topic = *mcap_calibration_topic_override;
|
||||
}
|
||||
if (!mcap_depth_calibration_topic_raw.empty()) {
|
||||
if (mcap_depth_calibration_topic_override) {
|
||||
config.record.mcap.enabled = true;
|
||||
config.record.mcap.depth_calibration_topic = mcap_depth_calibration_topic_raw;
|
||||
config.record.mcap.depth_calibration_topic = *mcap_depth_calibration_topic_override;
|
||||
}
|
||||
if (!mcap_pose_topic_raw.empty()) {
|
||||
if (mcap_pose_topic_override) {
|
||||
config.record.mcap.enabled = true;
|
||||
config.record.mcap.pose_topic = mcap_pose_topic_raw;
|
||||
config.record.mcap.pose_topic = *mcap_pose_topic_override;
|
||||
}
|
||||
if (!mcap_body_topic_raw.empty()) {
|
||||
if (mcap_body_topic_override) {
|
||||
config.record.mcap.enabled = true;
|
||||
config.record.mcap.body_topic = mcap_body_topic_raw;
|
||||
config.record.mcap.body_topic = *mcap_body_topic_override;
|
||||
}
|
||||
if (!mcap_frame_id_raw.empty()) {
|
||||
if (mcap_frame_id_override) {
|
||||
config.record.mcap.enabled = true;
|
||||
config.record.mcap.frame_id = mcap_frame_id_raw;
|
||||
config.record.mcap.frame_id = *mcap_frame_id_override;
|
||||
}
|
||||
if (!mcap_compression_raw.empty()) {
|
||||
auto parsed = parse_mcap_compression(mcap_compression_raw);
|
||||
if (mcap_compression_override) {
|
||||
auto parsed = parse_mcap_compression(*mcap_compression_override);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
config.record.mcap.enabled = true;
|
||||
config.record.mcap.compression = *parsed;
|
||||
}
|
||||
if (mcap_enabled_override) {
|
||||
config.record.mcap.enabled = *mcap_enabled_override;
|
||||
}
|
||||
|
||||
if (!queue_size_raw.empty()) {
|
||||
auto parsed = parse_size(queue_size_raw, "--queue-size");
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
if (queue_size_override) {
|
||||
config.latency.queue_size = *queue_size_override;
|
||||
}
|
||||
config.latency.queue_size = *parsed;
|
||||
if (gop_override) {
|
||||
config.encoder.gop = *gop_override;
|
||||
}
|
||||
if (!gop_raw.empty()) {
|
||||
auto parsed = parse_u32(gop_raw, "--gop");
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
if (b_frames_override) {
|
||||
config.encoder.b_frames = *b_frames_override;
|
||||
}
|
||||
config.encoder.gop = *parsed;
|
||||
if (realtime_sync_override) {
|
||||
config.latency.realtime_sync = *realtime_sync_override;
|
||||
}
|
||||
if (!b_frames_raw.empty()) {
|
||||
auto parsed = parse_u32(b_frames_raw, "--b-frames");
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
if (force_idr_on_reset_override) {
|
||||
config.latency.force_idr_on_reset = *force_idr_on_reset_override;
|
||||
}
|
||||
config.encoder.b_frames = *parsed;
|
||||
if (keep_stream_on_reset_override) {
|
||||
config.latency.keep_stream_on_reset = *keep_stream_on_reset_override;
|
||||
}
|
||||
if (!realtime_sync_raw.empty()) {
|
||||
auto parsed = parse_bool(realtime_sync_raw, "--realtime-sync");
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
if (ingest_max_frames_override) {
|
||||
config.latency.ingest_max_frames = *ingest_max_frames_override;
|
||||
}
|
||||
config.latency.realtime_sync = *parsed;
|
||||
if (ingest_idle_timeout_override) {
|
||||
config.latency.ingest_idle_timeout_ms = *ingest_idle_timeout_override;
|
||||
}
|
||||
if (!force_idr_on_reset_raw.empty()) {
|
||||
auto parsed = parse_bool(force_idr_on_reset_raw, "--force-idr-on-reset");
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
if (ingest_consumer_delay_override) {
|
||||
config.latency.ingest_consumer_delay_ms = *ingest_consumer_delay_override;
|
||||
}
|
||||
config.latency.force_idr_on_reset = *parsed;
|
||||
if (snapshot_copy_delay_override) {
|
||||
config.latency.snapshot_copy_delay_us = *snapshot_copy_delay_override;
|
||||
}
|
||||
if (!ingest_max_frames_raw.empty()) {
|
||||
auto parsed = parse_u32(ingest_max_frames_raw, "--ingest-max-frames");
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
config.latency.ingest_max_frames = *parsed;
|
||||
}
|
||||
if (!ingest_idle_timeout_raw.empty()) {
|
||||
auto parsed = parse_u32(ingest_idle_timeout_raw, "--ingest-idle-timeout-ms");
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
config.latency.ingest_idle_timeout_ms = *parsed;
|
||||
}
|
||||
if (!ingest_consumer_delay_raw.empty()) {
|
||||
auto parsed = parse_u32(ingest_consumer_delay_raw, "--ingest-consumer-delay-ms");
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
config.latency.ingest_consumer_delay_ms = *parsed;
|
||||
}
|
||||
if (!snapshot_copy_delay_raw.empty()) {
|
||||
auto parsed = parse_u32(snapshot_copy_delay_raw, "--snapshot-copy-delay-us");
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
config.latency.snapshot_copy_delay_us = *parsed;
|
||||
}
|
||||
if (!emit_stall_raw.empty()) {
|
||||
auto parsed = parse_u32(emit_stall_raw, "--emit-stall-ms");
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
config.latency.emit_stall_ms = *parsed;
|
||||
if (emit_stall_override) {
|
||||
config.latency.emit_stall_ms = *emit_stall_override;
|
||||
}
|
||||
|
||||
finalize_rtp_endpoint(config);
|
||||
@@ -968,7 +1192,6 @@ std::expected<void, std::string> validate_runtime_config(const RuntimeConfig &co
|
||||
if (config.encoder.b_frames > config.encoder.gop) {
|
||||
return std::unexpected("invalid encoder config: b_frames must be <= gop");
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -1002,6 +1225,7 @@ std::string summarize_runtime_config(const RuntimeConfig &config) {
|
||||
ss << ", latency.queue_size=" << config.latency.queue_size;
|
||||
ss << ", latency.realtime_sync=" << (config.latency.realtime_sync ? "true" : "false");
|
||||
ss << ", latency.force_idr_on_reset=" << (config.latency.force_idr_on_reset ? "true" : "false");
|
||||
ss << ", latency.keep_stream_on_reset=" << (config.latency.keep_stream_on_reset ? "true" : "false");
|
||||
ss << ", latency.ingest_max_frames=" << config.latency.ingest_max_frames;
|
||||
ss << ", latency.ingest_idle_timeout_ms=" << config.latency.ingest_idle_timeout_ms;
|
||||
ss << ", latency.ingest_consumer_delay_ms=" << config.latency.ingest_consumer_delay_ms;
|
||||
|
||||
+2
-1
@@ -9,7 +9,7 @@ namespace cvmmap_streamer {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::array<std::string_view, 33> kHelpLines{
|
||||
constexpr std::array<std::string_view, 34> kHelpLines{
|
||||
"Usage:",
|
||||
" --help, -h\tshow this message",
|
||||
"",
|
||||
@@ -23,6 +23,7 @@ constexpr std::array<std::string_view, 33> kHelpLines{
|
||||
" --encoder-device <device>\tauto|nvidia|software",
|
||||
" --gop <frames>\tencoder GOP length",
|
||||
" --b-frames <count>\tencoder B-frame count",
|
||||
" --keep-stream-on-reset <bool>\tkeep RTMP/RTP sessions alive across upstream stream_reset events",
|
||||
" --rtp\t\tenable RTP output",
|
||||
" --rtp-endpoint <host:port>\tRTP destination",
|
||||
" --rtp-payload-type <pt>\tRTP payload type (96-127)",
|
||||
|
||||
Reference in New Issue
Block a user