Files
cvmmap-streamer/src/config/runtime_config.cpp
T

928 lines
30 KiB
C++

#include "cvmmap_streamer/config/runtime_config.hpp"
#include <CLI/CLI.hpp>
#include <toml++/toml.hpp>
#include <array>
#include <charconv>
#include <cstdint>
#include <limits>
#include <optional>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
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<unsigned char>(value.front()))) {
value.erase(value.begin());
}
while (!value.empty() && !not_space(static_cast<unsigned char>(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<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) + "'");
}
return value;
}
std::expected<std::uint16_t, std::string> 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<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;
}
if (raw == "h265") {
return CodecType::H265;
}
return std::unexpected("invalid codec: '" + std::string(raw) + "' (expected: h264|h265)");
}
std::expected<RunMode, std::string> 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<RtmpTransportType, std::string> 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<EncoderBackendType, std::string> 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<EncoderDeviceType, std::string> 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<McapCompression, std::string> parse_mcap_compression(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::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");
}
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 '<host>:<port>' 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 <typename T>
std::optional<T> 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<T>();
if (!value) {
return std::nullopt;
}
return *value;
}
std::optional<std::vector<std::string>> 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<std::string> values{};
values.reserve(array->size());
for (const auto &item : *array) {
auto value = item.value<std::string>();
if (!value) {
return std::nullopt;
}
values.push_back(*value);
}
return values;
}
template <typename T>
std::expected<std::optional<T>, 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<T>{};
}
auto value = node.template value<std::int64_t>();
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<std::uint64_t>(*value) > static_cast<std::uint64_t>(std::numeric_limits<T>::max())) {
return std::unexpected("value out of range for " + std::string(field_name));
}
return std::optional<T>{static_cast<T>(*value)};
}
std::expected<void, std::string> 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<std::string>(table, "input.uri")) {
config.input.uri = *value;
}
if (auto value = toml_value<std::string>(table, "input.nats_url")) {
config.input.nats_url = *value;
}
if (auto value = toml_value<std::string>(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<std::string>(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<std::string>(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<std::string>(table, "encoder.codec")) {
auto parsed = parse_codec(*value);
if (!parsed) {
return std::unexpected(parsed.error());
}
config.encoder.codec = *parsed;
}
{
auto value = toml_nonnegative_integral<std::uint32_t>(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<std::uint32_t>(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<bool>(table, "outputs.rtp.enabled")) {
config.outputs.rtp.enabled = *value;
}
if (auto value = toml_value<std::string>(table, "outputs.rtp.endpoint")) {
config.outputs.rtp.enabled = true;
config.outputs.rtp.endpoint = *value;
}
{
auto value = toml_nonnegative_integral<std::uint8_t>(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<std::string>(table, "outputs.rtp.sdp_path")) {
config.outputs.rtp.enabled = true;
config.outputs.rtp.sdp_path = *value;
}
if (auto value = toml_value<bool>(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<std::string>(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<std::string>(table, "outputs.rtmp.ffmpeg_path")) {
config.outputs.rtmp.ffmpeg_path = *value;
}
if (auto value = toml_value<std::string>(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<bool>(table, "record.mcap.enabled")) {
config.record.mcap.enabled = *value;
}
if (auto value = toml_value<std::string>(table, "record.mcap.path")) {
config.record.mcap.enabled = true;
config.record.mcap.path = *value;
}
if (auto value = toml_value<std::string>(table, "record.mcap.topic")) {
config.record.mcap.enabled = true;
config.record.mcap.topic = *value;
}
if (auto value = toml_value<std::string>(table, "record.mcap.depth_topic")) {
config.record.mcap.enabled = true;
config.record.mcap.depth_topic = *value;
}
if (auto value = toml_value<std::string>(table, "record.mcap.body_topic")) {
config.record.mcap.enabled = true;
config.record.mcap.body_topic = *value;
}
if (auto value = toml_value<std::string>(table, "record.mcap.frame_id")) {
config.record.mcap.enabled = true;
config.record.mcap.frame_id = *value;
}
if (auto value = toml_value<std::string>(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<std::size_t>(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<bool>(table, "latency.realtime_sync")) {
config.latency.realtime_sync = *value;
}
if (auto value = toml_value<bool>(table, "latency.force_idr_on_reset")) {
config.latency.force_idr_on_reset = *value;
}
{
auto value = toml_nonnegative_integral<std::uint32_t>(
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<std::uint32_t>(
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<std::uint32_t>(
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<std::uint32_t>(
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<std::uint32_t>(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;
}
}
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<RuntimeConfig, std::string> 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<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_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-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<std::uint8_t>::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<std::uint8_t>(*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_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<void, std::string> 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.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 : "<unset>");
ss << ", rtp.payload_type=" << static_cast<unsigned>(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.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();
}
}