#include "cvmmap_streamer/config/runtime_config.hpp" #include #include #include #include #include #include #include #include #include #include #include #include namespace cvmmap_streamer { namespace { std::string trim_copy(std::string value) { auto not_space = [](unsigned char c) { return c != ' ' && c != '\t' && c != '\n' && c != '\r'; }; while (!value.empty() && !not_space(static_cast(value.front()))) { value.erase(value.begin()); } while (!value.empty() && !not_space(static_cast(value.back()))) { value.pop_back(); } return value; } std::string normalize_cli_error(std::string raw_message) { if ( raw_message.find("The following argument was not expected:") != std::string::npos || raw_message.find("The following arguments were not expected:") != std::string::npos) { const auto pos = raw_message.find(':'); if (pos != std::string::npos && pos + 1 < raw_message.size()) { const auto argument = trim_copy(raw_message.substr(pos + 1)); if (argument.rfind("--rtmp-mode", 0) == 0) { return "unknown argument: --rtmp-mode (removed; RTMP always uses enhanced mode)"; } return "unknown argument: " + argument; } return "unknown argument"; } 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; } std::expected parse_u16(std::string_view raw, std::string_view field_name) { std::uint16_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; } 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; } if (raw == "h265") { return CodecType::H265; } return std::unexpected("invalid codec: '" + std::string(raw) + "' (expected: h264|h265)"); } std::expected parse_run_mode(std::string_view raw) { if (raw == "pipeline") { return RunMode::Pipeline; } if (raw == "ingest") { return RunMode::Ingest; } return std::unexpected("invalid run mode: '" + std::string(raw) + "' (expected: pipeline|ingest)"); } std::expected parse_rtmp_transport(std::string_view raw) { if (raw == "libavformat") { return RtmpTransportType::Libavformat; } if (raw == "ffmpeg_process" || raw == "ffmpeg-process") { return RtmpTransportType::FfmpegProcess; } if (raw == "legacy_custom" || raw == "legacy-custom") { return std::unexpected( "invalid rtmp transport: '" + std::string(raw) + "' was removed; use libavformat or ffmpeg_process"); } return std::unexpected("invalid rtmp transport: '" + std::string(raw) + "' (expected: libavformat|ffmpeg_process)"); } std::expected parse_encoder_backend(std::string_view raw) { if (raw == "auto") { return EncoderBackendType::Auto; } if (raw == "ffmpeg") { return EncoderBackendType::FFmpeg; } if (raw == "gstreamer_legacy" || raw == "gstreamer-legacy") { return std::unexpected("invalid encoder backend: '" + std::string(raw) + "' was removed; use ffmpeg"); } return std::unexpected("invalid encoder backend: '" + std::string(raw) + "' (expected: auto|ffmpeg)"); } std::expected parse_encoder_device(std::string_view raw) { if (raw == "auto") { return EncoderDeviceType::Auto; } if (raw == "nvidia") { return EncoderDeviceType::Nvidia; } if (raw == "software") { return EncoderDeviceType::Software; } return std::unexpected("invalid encoder device: '" + std::string(raw) + "' (expected: auto|nvidia|software)"); } std::expected parse_mcap_compression_impl(std::string_view raw) { if (raw == "none") { return McapCompression::None; } if (raw == "lz4") { return McapCompression::Lz4; } if (raw == "zstd") { return McapCompression::Zstd; } return std::unexpected("invalid mcap compression: '" + std::string(raw) + "' (expected: none|lz4|zstd)"); } 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"); } const auto colon = endpoint.rfind(':'); if (colon == std::string_view::npos || colon == 0 || colon + 1 >= endpoint.size()) { return std::unexpected("invalid RTP config: endpoint must be in ':' format"); } const auto host = endpoint.substr(0, colon); const auto port = endpoint.substr(colon + 1); auto parsed_port = parse_u16(port, "outputs.rtp.endpoint"); if (!parsed_port) { return std::unexpected(parsed_port.error()); } if (*parsed_port == 0) { return std::unexpected("invalid RTP config: endpoint port must be in range [1,65535]"); } return std::pair{std::string(host), *parsed_port}; } template std::optional toml_value(const toml::table &table, std::string_view path) { auto node = table.at_path(path); if (!node) { return std::nullopt; } auto value = node.template value(); if (!value) { return std::nullopt; } return *value; } std::optional> toml_string_array(const toml::table &table, std::string_view path) { auto node = table.at_path(path); if (!node) { return std::nullopt; } auto *array = node.as_array(); if (array == nullptr) { return std::nullopt; } std::vector values{}; values.reserve(array->size()); for (const auto &item : *array) { auto value = item.value(); if (!value) { return std::nullopt; } values.push_back(*value); } return values; } template std::expected, std::string> toml_nonnegative_integral( const toml::table &table, std::string_view path, std::string_view field_name) { auto node = table.at_path(path); if (!node) { return std::optional{}; } auto value = node.template value(); if (!value) { return std::unexpected("invalid value for " + std::string(field_name) + ": expected integer"); } if (*value < 0) { return std::unexpected("invalid value for " + std::string(field_name) + ": must be >= 0"); } if (static_cast(*value) > static_cast(std::numeric_limits::max())) { return std::unexpected("value out of range for " + std::string(field_name)); } return std::optional{static_cast(*value)}; } std::expected apply_toml_file(RuntimeConfig &config, const std::string &path) { toml::table table{}; try { table = toml::parse_file(path); } catch (const toml::parse_error &e) { return std::unexpected("failed to parse config file '" + path + "': " + std::string(e.description())); } if (auto value = toml_value(table, "input.uri")) { config.input.uri = *value; } if (auto value = toml_value(table, "input.nats_url")) { config.input.nats_url = *value; } if (auto value = toml_value(table, "run_mode")) { auto parsed = parse_run_mode(*value); if (!parsed) { return std::unexpected(parsed.error()); } config.run_mode = *parsed; } if (auto value = toml_value(table, "encoder.backend")) { auto parsed = parse_encoder_backend(*value); if (!parsed) { return std::unexpected(parsed.error()); } config.encoder.backend = *parsed; } if (auto value = toml_value(table, "encoder.device")) { auto parsed = parse_encoder_device(*value); if (!parsed) { return std::unexpected(parsed.error()); } config.encoder.device = *parsed; } if (auto value = toml_value(table, "encoder.codec")) { auto parsed = parse_codec(*value); if (!parsed) { return std::unexpected(parsed.error()); } config.encoder.codec = *parsed; } { auto value = toml_nonnegative_integral(table, "encoder.gop", "encoder.gop"); if (!value) { return std::unexpected(value.error()); } if (value->has_value()) { config.encoder.gop = **value; } } { auto value = toml_nonnegative_integral(table, "encoder.b_frames", "encoder.b_frames"); if (!value) { return std::unexpected(value.error()); } if (value->has_value()) { config.encoder.b_frames = **value; } } if (auto value = toml_value(table, "outputs.rtp.enabled")) { config.outputs.rtp.enabled = *value; } if (auto value = toml_value(table, "outputs.rtp.endpoint")) { config.outputs.rtp.enabled = true; config.outputs.rtp.endpoint = *value; } { auto value = toml_nonnegative_integral(table, "outputs.rtp.payload_type", "outputs.rtp.payload_type"); if (!value) { return std::unexpected(value.error()); } if (value->has_value()) { config.outputs.rtp.enabled = true; config.outputs.rtp.payload_type = **value; } } if (auto value = toml_value(table, "outputs.rtp.sdp_path")) { config.outputs.rtp.enabled = true; config.outputs.rtp.sdp_path = *value; } if (auto value = toml_value(table, "outputs.rtmp.enabled")) { config.outputs.rtmp.enabled = *value; } if (auto values = toml_string_array(table, "outputs.rtmp.urls")) { config.outputs.rtmp.enabled = true; config.outputs.rtmp.urls = std::move(*values); } if (auto value = toml_value(table, "outputs.rtmp.transport")) { auto parsed = parse_rtmp_transport(*value); if (!parsed) { return std::unexpected(parsed.error()); } config.outputs.rtmp.transport = *parsed; } if (auto value = toml_value(table, "outputs.rtmp.ffmpeg_path")) { config.outputs.rtmp.ffmpeg_path = *value; } if (auto value = toml_value(table, "outputs.rtmp.mode")) { return std::unexpected("invalid RTMP config: outputs.rtmp.mode was removed; RTMP always uses enhanced mode"); } if (auto value = toml_value(table, "record.mcap.enabled")) { config.record.mcap.enabled = *value; } if (auto value = toml_value(table, "record.mcap.path")) { config.record.mcap.enabled = true; config.record.mcap.path = *value; } if (auto value = toml_value(table, "record.mcap.topic")) { config.record.mcap.enabled = true; config.record.mcap.topic = *value; } if (auto value = toml_value(table, "record.mcap.depth_topic")) { config.record.mcap.enabled = true; config.record.mcap.depth_topic = *value; } if (auto value = toml_value(table, "record.mcap.calibration_topic")) { config.record.mcap.enabled = true; config.record.mcap.calibration_topic = *value; } if (auto value = toml_value(table, "record.mcap.pose_topic")) { config.record.mcap.enabled = true; config.record.mcap.pose_topic = *value; } if (auto value = toml_value(table, "record.mcap.body_topic")) { config.record.mcap.enabled = true; config.record.mcap.body_topic = *value; } if (auto value = toml_value(table, "record.mcap.frame_id")) { config.record.mcap.enabled = true; config.record.mcap.frame_id = *value; } if (auto value = toml_value(table, "record.mcap.compression")) { auto parsed = parse_mcap_compression(*value); if (!parsed) { return std::unexpected(parsed.error()); } config.record.mcap.enabled = true; config.record.mcap.compression = *parsed; } { auto value = toml_nonnegative_integral(table, "latency.queue_size", "latency.queue_size"); if (!value) { return std::unexpected(value.error()); } if (value->has_value()) { config.latency.queue_size = **value; } } if (auto value = toml_value(table, "latency.realtime_sync")) { config.latency.realtime_sync = *value; } if (auto value = toml_value(table, "latency.force_idr_on_reset")) { config.latency.force_idr_on_reset = *value; } { auto value = toml_nonnegative_integral( table, "latency.ingest_max_frames", "latency.ingest_max_frames"); if (!value) { return std::unexpected(value.error()); } if (value->has_value()) { config.latency.ingest_max_frames = **value; } } { auto value = toml_nonnegative_integral( table, "latency.ingest_idle_timeout_ms", "latency.ingest_idle_timeout_ms"); if (!value) { return std::unexpected(value.error()); } if (value->has_value()) { config.latency.ingest_idle_timeout_ms = **value; } } { auto value = toml_nonnegative_integral( table, "latency.ingest_consumer_delay_ms", "latency.ingest_consumer_delay_ms"); if (!value) { return std::unexpected(value.error()); } if (value->has_value()) { config.latency.ingest_consumer_delay_ms = **value; } } { auto value = toml_nonnegative_integral( table, "latency.snapshot_copy_delay_us", "latency.snapshot_copy_delay_us"); if (!value) { return std::unexpected(value.error()); } if (value->has_value()) { config.latency.snapshot_copy_delay_us = **value; } } { auto value = toml_nonnegative_integral(table, "latency.emit_stall_ms", "latency.emit_stall_ms"); if (!value) { return std::unexpected(value.error()); } if (value->has_value()) { config.latency.emit_stall_ms = **value; } } return {}; } void finalize_rtp_endpoint(RuntimeConfig &config) { config.outputs.rtp.host.reset(); config.outputs.rtp.port.reset(); if (!config.outputs.rtp.endpoint || config.outputs.rtp.endpoint->empty()) { return; } auto parsed = parse_rtp_endpoint(*config.outputs.rtp.endpoint); if (!parsed) { return; } config.outputs.rtp.host = parsed->first; config.outputs.rtp.port = parsed->second; } } std::expected parse_mcap_compression(std::string_view raw) { return parse_mcap_compression_impl(raw); } RuntimeConfig RuntimeConfig::defaults() { return RuntimeConfig{}; } std::string_view to_string(CodecType codec) { switch (codec) { case CodecType::H264: return "h264"; case CodecType::H265: return "h265"; } return "unknown"; } std::string_view to_string(RunMode mode) { switch (mode) { case RunMode::Pipeline: return "pipeline"; case RunMode::Ingest: return "ingest"; } return "unknown"; } std::string_view to_string(RtmpMode mode) { switch (mode) { case RtmpMode::Enhanced: return "enhanced"; } return "unknown"; } std::string_view to_string(RtmpTransportType transport) { switch (transport) { case RtmpTransportType::Libavformat: return "libavformat"; case RtmpTransportType::FfmpegProcess: return "ffmpeg_process"; } return "unknown"; } std::string_view to_string(EncoderBackendType backend) { switch (backend) { case EncoderBackendType::Auto: return "auto"; case EncoderBackendType::FFmpeg: return "ffmpeg"; } return "unknown"; } std::string_view to_string(EncoderDeviceType device) { switch (device) { case EncoderDeviceType::Auto: return "auto"; case EncoderDeviceType::Nvidia: return "nvidia"; case EncoderDeviceType::Software: return "software"; } return "unknown"; } std::string_view to_string(McapCompression compression) { switch (compression) { case McapCompression::None: return "none"; case McapCompression::Lz4: return "lz4"; case McapCompression::Zstd: return "zstd"; } return "unknown"; } std::expected parse_runtime_config(int argc, char **argv) { RuntimeConfig config = RuntimeConfig::defaults(); std::string config_path_raw{}; std::string input_uri_raw{}; std::string input_nats_url_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_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}; 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.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("--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-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); try { app.parse(argc, argv); } catch (const CLI::ParseError &e) { const auto exit_code = app.exit(e); if (exit_code == 0) { return std::unexpected("help"); } return std::unexpected(normalize_cli_error(e.what())); } if (!config_path_raw.empty()) { auto load_result = apply_toml_file(config, config_path_raw); if (!load_result) { return std::unexpected(load_result.error()); } } if (!input_uri_raw.empty()) { config.input.uri = input_uri_raw; } if (!input_nats_url_raw.empty()) { config.input.nats_url = input_nats_url_raw; } if (!run_mode_raw.empty()) { auto parsed = parse_run_mode(run_mode_raw); if (!parsed) { return std::unexpected(parsed.error()); } config.run_mode = *parsed; } if (!codec_raw.empty()) { auto parsed = parse_codec(codec_raw); 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 (!parsed) { return std::unexpected(parsed.error()); } config.encoder.backend = *parsed; } if (!encoder_device_raw.empty()) { auto parsed = parse_encoder_device(encoder_device_raw); 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()) { config.outputs.rtmp.enabled = true; config.outputs.rtmp.urls = std::move(rtmp_urls_raw); } if (!rtmp_transport_raw.empty()) { auto parsed = parse_rtmp_transport(rtmp_transport_raw); 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; } 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 (!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 + "'"); } config.outputs.rtp.enabled = true; config.outputs.rtp.payload_type = static_cast(*value); } if (!rtp_sdp_raw.empty()) { config.outputs.rtp.enabled = true; config.outputs.rtp.sdp_path = rtp_sdp_raw; } config.record.mcap.enabled = config.record.mcap.enabled || mcap_enabled; if (!mcap_path_raw.empty()) { config.record.mcap.enabled = true; config.record.mcap.path = mcap_path_raw; } if (!mcap_topic_raw.empty()) { config.record.mcap.enabled = true; config.record.mcap.topic = mcap_topic_raw; } if (!mcap_depth_topic_raw.empty()) { config.record.mcap.enabled = true; config.record.mcap.depth_topic = mcap_depth_topic_raw; } if (!mcap_calibration_topic_raw.empty()) { config.record.mcap.enabled = true; config.record.mcap.calibration_topic = mcap_calibration_topic_raw; } if (!mcap_pose_topic_raw.empty()) { config.record.mcap.enabled = true; config.record.mcap.pose_topic = mcap_pose_topic_raw; } if (!mcap_body_topic_raw.empty()) { config.record.mcap.enabled = true; config.record.mcap.body_topic = mcap_body_topic_raw; } if (!mcap_frame_id_raw.empty()) { config.record.mcap.enabled = true; config.record.mcap.frame_id = mcap_frame_id_raw; } if (!mcap_compression_raw.empty()) { auto parsed = parse_mcap_compression(mcap_compression_raw); if (!parsed) { return std::unexpected(parsed.error()); } config.record.mcap.enabled = true; config.record.mcap.compression = *parsed; } 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 (!gop_raw.empty()) { auto parsed = parse_u32(gop_raw, "--gop"); if (!parsed) { return std::unexpected(parsed.error()); } config.encoder.gop = *parsed; } 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 (!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 (!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 (!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; } finalize_rtp_endpoint(config); return config; } std::expected validate_runtime_config(const RuntimeConfig &config) { if (config.input.uri.empty()) { return std::unexpected("invalid input config: input.uri must not be empty"); } if (config.input.nats_url.empty()) { return std::unexpected("invalid input config: input.nats_url must not be empty"); } if (config.outputs.rtmp.enabled && config.outputs.rtmp.urls.empty()) { return std::unexpected("invalid RTMP config: enabled RTMP output requires at least one URL"); } for (const auto &url : config.outputs.rtmp.urls) { if (url.empty()) { return std::unexpected("invalid RTMP config: URL must not be empty"); } } if (config.outputs.rtmp.enabled) { if (config.encoder.backend == EncoderBackendType::Auto) { // auto resolves to FFmpeg; nothing else is supported. } else if (config.encoder.backend != EncoderBackendType::FFmpeg) { return std::unexpected("invalid backend/output matrix: RTMP requires encoder.backend=ffmpeg or auto"); } if (config.outputs.rtmp.transport == RtmpTransportType::FfmpegProcess && config.outputs.rtmp.ffmpeg_path.empty()) { return std::unexpected("invalid RTMP config: ffmpeg_process transport requires a non-empty ffmpeg path"); } } if (config.outputs.rtp.enabled) { if (!config.outputs.rtp.endpoint || config.outputs.rtp.endpoint->empty()) { return std::unexpected("invalid RTP config: enabled RTP output requires endpoint"); } auto endpoint_validation = parse_rtp_endpoint(*config.outputs.rtp.endpoint); if (!endpoint_validation) { return std::unexpected(endpoint_validation.error()); } if (config.outputs.rtp.payload_type < 96 || config.outputs.rtp.payload_type > 127) { return std::unexpected("invalid RTP config: payload type must be in dynamic range [96,127]"); } } if (config.record.mcap.enabled && config.record.mcap.path.empty()) { return std::unexpected("invalid MCAP config: enabled MCAP output requires path"); } if (config.record.mcap.topic.empty()) { return std::unexpected("invalid MCAP config: topic must not be empty"); } if (config.record.mcap.depth_topic.empty()) { return std::unexpected("invalid MCAP config: depth_topic must not be empty"); } if (config.record.mcap.calibration_topic.empty()) { return std::unexpected("invalid MCAP config: calibration_topic must not be empty"); } if (config.record.mcap.pose_topic.empty()) { return std::unexpected("invalid MCAP config: pose_topic must not be empty"); } if (config.record.mcap.body_topic.empty()) { return std::unexpected("invalid MCAP config: body_topic must not be empty"); } if (config.record.mcap.frame_id.empty()) { return std::unexpected("invalid MCAP config: frame_id must not be empty"); } if (config.latency.queue_size == 0) { return std::unexpected("invalid latency config: queue_size must be >= 1"); } if (config.encoder.gop == 0) { return std::unexpected("invalid encoder config: gop must be >= 1"); } if (config.encoder.b_frames > config.encoder.gop) { return std::unexpected("invalid encoder config: b_frames must be <= gop"); } if (config.latency.ingest_idle_timeout_ms == 0) { return std::unexpected("invalid ingest config: ingest_idle_timeout_ms must be >= 1"); } return {}; } std::string summarize_runtime_config(const RuntimeConfig &config) { std::ostringstream ss; ss << "input.uri=" << config.input.uri; ss << ", input.nats_url=" << config.input.nats_url; ss << ", run_mode=" << to_string(config.run_mode); ss << ", encoder.backend=" << to_string(config.encoder.backend); ss << ", encoder.device=" << to_string(config.encoder.device); ss << ", encoder.codec=" << to_string(config.encoder.codec); ss << ", encoder.gop=" << config.encoder.gop; ss << ", encoder.b_frames=" << config.encoder.b_frames; ss << ", rtmp.enabled=" << (config.outputs.rtmp.enabled ? "true" : "false"); ss << ", rtmp.transport=" << to_string(config.outputs.rtmp.transport); ss << ", rtmp.urls=" << config.outputs.rtmp.urls.size(); ss << ", rtp.enabled=" << (config.outputs.rtp.enabled ? "true" : "false"); ss << ", rtp.endpoint=" << (config.outputs.rtp.endpoint ? *config.outputs.rtp.endpoint : ""); ss << ", rtp.payload_type=" << static_cast(config.outputs.rtp.payload_type); ss << ", mcap.enabled=" << (config.record.mcap.enabled ? "true" : "false"); ss << ", mcap.path=" << config.record.mcap.path; ss << ", mcap.topic=" << config.record.mcap.topic; ss << ", mcap.depth_topic=" << config.record.mcap.depth_topic; ss << ", mcap.calibration_topic=" << config.record.mcap.calibration_topic; ss << ", mcap.pose_topic=" << config.record.mcap.pose_topic; ss << ", mcap.body_topic=" << config.record.mcap.body_topic; ss << ", mcap.frame_id=" << config.record.mcap.frame_id; ss << ", mcap.compression=" << to_string(config.record.mcap.compression); 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.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; ss << ", latency.snapshot_copy_delay_us=" << config.latency.snapshot_copy_delay_us; ss << ", latency.emit_stall_ms=" << config.latency.emit_stall_ms; return ss.str(); } }