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:
2026-04-09 12:18:12 +08:00
parent 965b03c053
commit 4f016d9cef
2 changed files with 453 additions and 228 deletions
+448 -224
View File
@@ -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
View File
@@ -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)",