diff --git a/src/config/runtime_config.cpp b/src/config/runtime_config.cpp index 3418c6e..a3f550f 100644 --- a/src/config/runtime_config.cpp +++ b/src/config/runtime_config.cpp @@ -48,15 +48,31 @@ std::string normalize_cli_error(std::string raw_message) { return trim_copy(std::move(raw_message)); } -std::expected 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) + "'"); - } - return value; +template +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(); + } + 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 parse_u16(std::string_view raw, std::string_view field_name) { @@ -70,31 +86,6 @@ std::expected parse_u16(std::string_view raw, std::s return value; } -std::expected 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(std::numeric_limits::max())) { - return std::unexpected("value out of range for " + std::string(field_name) + ": '" + std::string(raw) + "'"); - } - return static_cast(parsed); -} - -std::expected 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 parse_codec(std::string_view raw) { if (raw == "h264") { return CodecType::H264; @@ -181,6 +172,62 @@ std::expected parse_mcap_compression_impl(std::str return std::unexpected("invalid mcap compression: '" + std::string(raw) + "' (expected: none|lz4|zstd)"); } +std::expected 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 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 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 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 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 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 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::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::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 find_disallowed_boolean_assignment(int argc, char **argv) { + struct FlagPair { + std::string_view positive; + std::string_view negative; + }; + + constexpr std::array 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 std::optional toml_value(const toml::table &table, std::string_view path) { auto node = table.at_path(path); @@ -428,6 +517,9 @@ std::expected apply_toml_file(RuntimeConfig &config, const st if (auto value = toml_value(table, "latency.force_idr_on_reset")) { config.latency.force_idr_on_reset = *value; } + if (auto value = toml_value(table, "latency.keep_stream_on_reset")) { + config.latency.keep_stream_on_reset = *value; + } { auto value = toml_nonnegative_integral( table, @@ -599,87 +691,257 @@ std::string_view to_string(McapCompression compression) { std::expected 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 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 config_path_override{}; + std::optional input_uri_override{}; + std::optional input_nats_url_override{}; + std::optional input_video_source_override{}; + std::optional run_mode_override{}; + std::optional codec_override{}; + std::optional encoder_backend_override{}; + std::optional encoder_device_override{}; + std::optional rtmp_enabled_override{}; + std::vector rtmp_urls_override{}; + std::optional rtmp_transport_override{}; + std::optional rtmp_ffmpeg_path_override{}; + std::optional rtp_enabled_override{}; + std::optional rtp_endpoint_override{}; + std::optional rtp_payload_type_override{}; + std::optional rtp_sdp_override{}; + std::optional mcap_enabled_override{}; + std::optional mcap_path_override{}; + std::optional mcap_topic_override{}; + std::optional mcap_depth_topic_override{}; + std::optional mcap_calibration_topic_override{}; + std::optional mcap_depth_calibration_topic_override{}; + std::optional mcap_pose_topic_override{}; + std::optional mcap_body_topic_override{}; + std::optional mcap_frame_id_override{}; + std::optional mcap_compression_override{}; + std::optional queue_size_override{}; + std::optional gop_override{}; + std::optional b_frames_override{}; + std::optional realtime_sync_override{}; + std::optional force_idr_on_reset_override{}; + std::optional keep_stream_on_reset_override{}; + std::optional ingest_max_frames_override{}; + std::optional ingest_idle_timeout_override{}; + std::optional ingest_consumer_delay_override{}; + std::optional snapshot_copy_delay_override{}; + std::optional 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 : 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 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()) { - config.outputs.rtp.enabled = true; - config.outputs.rtp.endpoint = rtp_endpoint_raw; + if (rtmp_enabled_override) { + config.outputs.rtmp.enabled = *rtmp_enabled_override; } - 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::max()) { - return std::unexpected("value out of range for --rtp-payload-type: '" + rtp_payload_type_raw + "'"); - } + if (rtp_endpoint_override) { config.outputs.rtp.enabled = true; - config.outputs.rtp.payload_type = static_cast(*value); + config.outputs.rtp.endpoint = *rtp_endpoint_override; } - if (!rtp_sdp_raw.empty()) { + if (rtp_payload_type_override) { config.outputs.rtp.enabled = true; - config.outputs.rtp.sdp_path = rtp_sdp_raw; + config.outputs.rtp.payload_type = static_cast(*rtp_payload_type_override); + } + if (rtp_sdp_override) { + config.outputs.rtp.enabled = true; + 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()); - } - config.latency.queue_size = *parsed; + if (queue_size_override) { + config.latency.queue_size = *queue_size_override; } - if (!gop_raw.empty()) { - auto parsed = parse_u32(gop_raw, "--gop"); - if (!parsed) { - return std::unexpected(parsed.error()); - } - config.encoder.gop = *parsed; + if (gop_override) { + config.encoder.gop = *gop_override; } - if (!b_frames_raw.empty()) { - auto parsed = parse_u32(b_frames_raw, "--b-frames"); - if (!parsed) { - return std::unexpected(parsed.error()); - } - config.encoder.b_frames = *parsed; + if (b_frames_override) { + config.encoder.b_frames = *b_frames_override; } - if (!realtime_sync_raw.empty()) { - auto parsed = parse_bool(realtime_sync_raw, "--realtime-sync"); - if (!parsed) { - return std::unexpected(parsed.error()); - } - config.latency.realtime_sync = *parsed; + if (realtime_sync_override) { + config.latency.realtime_sync = *realtime_sync_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()); - } - config.latency.force_idr_on_reset = *parsed; + if (force_idr_on_reset_override) { + config.latency.force_idr_on_reset = *force_idr_on_reset_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 (keep_stream_on_reset_override) { + config.latency.keep_stream_on_reset = *keep_stream_on_reset_override; } - 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_max_frames_override) { + config.latency.ingest_max_frames = *ingest_max_frames_override; } - 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 (ingest_idle_timeout_override) { + config.latency.ingest_idle_timeout_ms = *ingest_idle_timeout_override; } - 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 (ingest_consumer_delay_override) { + config.latency.ingest_consumer_delay_ms = *ingest_consumer_delay_override; } - 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 (snapshot_copy_delay_override) { + config.latency.snapshot_copy_delay_us = *snapshot_copy_delay_override; + } + if (emit_stall_override) { + config.latency.emit_stall_ms = *emit_stall_override; } finalize_rtp_endpoint(config); @@ -968,7 +1192,6 @@ std::expected 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; diff --git a/src/ipc/help.cpp b/src/ipc/help.cpp index bcd9753..89fb7ab 100644 --- a/src/ipc/help.cpp +++ b/src/ipc/help.cpp @@ -9,7 +9,7 @@ namespace cvmmap_streamer { namespace { -constexpr std::array kHelpLines{ +constexpr std::array kHelpLines{ "Usage:", " --help, -h\tshow this message", "", @@ -23,6 +23,7 @@ constexpr std::array kHelpLines{ " --encoder-device \tauto|nvidia|software", " --gop \tencoder GOP length", " --b-frames \tencoder B-frame count", + " --keep-stream-on-reset \tkeep RTMP/RTP sessions alive across upstream stream_reset events", " --rtp\t\tenable RTP output", " --rtp-endpoint \tRTP destination", " --rtp-payload-type \tRTP payload type (96-127)",