feat(downstream): add cvmmap downstream runtime implementation
This commit introduces the full downstream runtime implementation needed to ingest, transform, and publish streams. It preserves the original upstream request boundary by packaging the entire cvmmap-streamer module (build config, public API, protocol and IPC glue, and simulator/tester entrypoints) in one coherent core unit. Keeping this group isolated enables reviewers to validate runtime behavior and correctness without mixing test evidence or process documentation changes. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -0,0 +1,524 @@
|
||||
#include "cvmmap_streamer/config/runtime_config.hpp"
|
||||
|
||||
#include <charconv>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace cvmmap_streamer {
|
||||
|
||||
namespace {
|
||||
|
||||
std::expected<std::string_view, std::string>
|
||||
next_value(int argc, char **argv, int &index, std::string_view flag_name) {
|
||||
if (index + 1 >= argc) {
|
||||
return std::unexpected("missing value for " + std::string(flag_name));
|
||||
}
|
||||
++index;
|
||||
return std::string_view{argv[index]};
|
||||
}
|
||||
|
||||
std::expected<std::uint32_t, std::string> parse_u32(std::string_view raw, std::string_view flag_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(flag_name) + ": '" + std::string(raw) + "'");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
std::expected<std::uint16_t, std::string> parse_u16(std::string_view raw, std::string_view flag_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(flag_name) + ": '" + std::string(raw) + "'");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
std::expected<std::size_t, std::string> parse_size(std::string_view raw, std::string_view flag_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(flag_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(flag_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 flag_name) {
|
||||
if (raw == "true" || raw == "1") {
|
||||
return true;
|
||||
}
|
||||
if (raw == "false" || raw == "0") {
|
||||
return false;
|
||||
}
|
||||
return std::unexpected(
|
||||
"invalid value for " + std::string(flag_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<RtmpMode, std::string> parse_rtmp_mode(std::string_view raw) {
|
||||
if (raw == "enhanced") {
|
||||
return RtmpMode::Enhanced;
|
||||
}
|
||||
if (raw == "domestic") {
|
||||
return RtmpMode::Domestic;
|
||||
}
|
||||
return std::unexpected("invalid rtmp mode: '" + std::string(raw) + "' (expected: enhanced|domestic)");
|
||||
}
|
||||
|
||||
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: --rtp-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: --rtp-endpoint must be in '<host>:<port>' format");
|
||||
}
|
||||
|
||||
const auto host = endpoint.substr(0, colon);
|
||||
const auto port = endpoint.substr(colon + 1);
|
||||
if (host.empty()) {
|
||||
return std::unexpected("invalid RTP config: --rtp-endpoint host must not be empty");
|
||||
}
|
||||
|
||||
auto parsed_port = parse_u16(port, "--rtp-endpoint");
|
||||
if (!parsed_port) {
|
||||
return std::unexpected(parsed_port.error());
|
||||
}
|
||||
if (*parsed_port == 0) {
|
||||
return std::unexpected("invalid RTP config: --rtp-endpoint port must be in range [1,65535]");
|
||||
}
|
||||
|
||||
return std::pair{std::string(host), *parsed_port};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RuntimeConfig RuntimeConfig::defaults() {
|
||||
return RuntimeConfig{};
|
||||
}
|
||||
|
||||
std::string_view to_string(CodecType codec) {
|
||||
switch (codec) {
|
||||
case CodecType::H264:
|
||||
return "h264";
|
||||
case CodecType::H265:
|
||||
return "h265";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
std::string_view to_string(RunMode mode) {
|
||||
switch (mode) {
|
||||
case RunMode::Pipeline:
|
||||
return "pipeline";
|
||||
case RunMode::Ingest:
|
||||
return "ingest";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
std::string_view to_string(RtmpMode mode) {
|
||||
switch (mode) {
|
||||
case RtmpMode::Enhanced:
|
||||
return "enhanced";
|
||||
case RtmpMode::Domestic:
|
||||
return "domestic";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
std::expected<RuntimeConfig, std::string> parse_runtime_config(int argc, char **argv) {
|
||||
RuntimeConfig config = RuntimeConfig::defaults();
|
||||
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
const std::string_view arg{argv[i]};
|
||||
|
||||
if (arg == "--codec") {
|
||||
auto raw = next_value(argc, argv, i, arg);
|
||||
if (!raw) {
|
||||
return std::unexpected(raw.error());
|
||||
}
|
||||
auto codec = parse_codec(*raw);
|
||||
if (!codec) {
|
||||
return std::unexpected(codec.error());
|
||||
}
|
||||
config.codec = *codec;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--run-mode") {
|
||||
auto raw = next_value(argc, argv, i, arg);
|
||||
if (!raw) {
|
||||
return std::unexpected(raw.error());
|
||||
}
|
||||
auto mode = parse_run_mode(*raw);
|
||||
if (!mode) {
|
||||
return std::unexpected(mode.error());
|
||||
}
|
||||
config.run_mode = *mode;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--shm-name") {
|
||||
auto value = next_value(argc, argv, i, arg);
|
||||
if (!value) {
|
||||
return std::unexpected(value.error());
|
||||
}
|
||||
config.input.shm_name = std::string(*value);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--zmq-endpoint") {
|
||||
auto value = next_value(argc, argv, i, arg);
|
||||
if (!value) {
|
||||
return std::unexpected(value.error());
|
||||
}
|
||||
config.input.zmq_endpoint = std::string(*value);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--rtmp") {
|
||||
config.outputs.rtmp.enabled = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--rtmp-url") {
|
||||
auto value = next_value(argc, argv, i, arg);
|
||||
if (!value) {
|
||||
return std::unexpected(value.error());
|
||||
}
|
||||
config.outputs.rtmp.enabled = true;
|
||||
config.outputs.rtmp.urls.emplace_back(*value);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--rtmp-mode") {
|
||||
auto raw = next_value(argc, argv, i, arg);
|
||||
if (!raw) {
|
||||
return std::unexpected(raw.error());
|
||||
}
|
||||
auto mode = parse_rtmp_mode(*raw);
|
||||
if (!mode) {
|
||||
return std::unexpected(mode.error());
|
||||
}
|
||||
config.outputs.rtmp.mode = *mode;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--rtp") {
|
||||
config.outputs.rtp.enabled = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--rtp-endpoint") {
|
||||
auto value = next_value(argc, argv, i, arg);
|
||||
if (!value) {
|
||||
return std::unexpected(value.error());
|
||||
}
|
||||
auto endpoint = parse_rtp_endpoint(*value);
|
||||
if (!endpoint) {
|
||||
return std::unexpected(endpoint.error());
|
||||
}
|
||||
config.outputs.rtp.enabled = true;
|
||||
config.outputs.rtp.endpoint = std::string(*value);
|
||||
config.outputs.rtp.host = endpoint->first;
|
||||
config.outputs.rtp.port = endpoint->second;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--rtp-payload-type") {
|
||||
auto raw = next_value(argc, argv, i, arg);
|
||||
if (!raw) {
|
||||
return std::unexpected(raw.error());
|
||||
}
|
||||
auto value = parse_u32(*raw, arg);
|
||||
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: '" + std::string(*raw) + "'");
|
||||
}
|
||||
config.outputs.rtp.enabled = true;
|
||||
config.outputs.rtp.payload_type = static_cast<std::uint8_t>(*value);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--rtp-sdp" || arg == "--sdp") {
|
||||
auto value = next_value(argc, argv, i, arg);
|
||||
if (!value) {
|
||||
return std::unexpected(value.error());
|
||||
}
|
||||
if (value->empty()) {
|
||||
return std::unexpected("invalid RTP config: " + std::string(arg) + " must not be empty");
|
||||
}
|
||||
config.outputs.rtp.enabled = true;
|
||||
config.outputs.rtp.sdp_path = std::string(*value);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--queue-size") {
|
||||
auto raw = next_value(argc, argv, i, arg);
|
||||
if (!raw) {
|
||||
return std::unexpected(raw.error());
|
||||
}
|
||||
auto parsed = parse_size(*raw, arg);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
config.latency.queue_size = *parsed;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--gop") {
|
||||
auto raw = next_value(argc, argv, i, arg);
|
||||
if (!raw) {
|
||||
return std::unexpected(raw.error());
|
||||
}
|
||||
auto parsed = parse_u32(*raw, arg);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
config.latency.gop = *parsed;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--b-frames") {
|
||||
auto raw = next_value(argc, argv, i, arg);
|
||||
if (!raw) {
|
||||
return std::unexpected(raw.error());
|
||||
}
|
||||
auto parsed = parse_u32(*raw, arg);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
config.latency.b_frames = *parsed;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--realtime-sync") {
|
||||
auto raw = next_value(argc, argv, i, arg);
|
||||
if (!raw) {
|
||||
return std::unexpected(raw.error());
|
||||
}
|
||||
auto parsed = parse_bool(*raw, arg);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
config.latency.realtime_sync = *parsed;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--force-idr-on-reset") {
|
||||
auto raw = next_value(argc, argv, i, arg);
|
||||
if (!raw) {
|
||||
return std::unexpected(raw.error());
|
||||
}
|
||||
auto parsed = parse_bool(*raw, arg);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
config.latency.force_idr_on_reset = *parsed;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--ingest-max-frames") {
|
||||
auto raw = next_value(argc, argv, i, arg);
|
||||
if (!raw) {
|
||||
return std::unexpected(raw.error());
|
||||
}
|
||||
auto parsed = parse_u32(*raw, arg);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
config.latency.ingest_max_frames = *parsed;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--ingest-idle-timeout-ms") {
|
||||
auto raw = next_value(argc, argv, i, arg);
|
||||
if (!raw) {
|
||||
return std::unexpected(raw.error());
|
||||
}
|
||||
auto parsed = parse_u32(*raw, arg);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
config.latency.ingest_idle_timeout_ms = *parsed;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--ingest-consumer-delay-ms") {
|
||||
auto raw = next_value(argc, argv, i, arg);
|
||||
if (!raw) {
|
||||
return std::unexpected(raw.error());
|
||||
}
|
||||
auto parsed = parse_u32(*raw, arg);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
config.latency.ingest_consumer_delay_ms = *parsed;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--snapshot-copy-delay-us") {
|
||||
auto raw = next_value(argc, argv, i, arg);
|
||||
if (!raw) {
|
||||
return std::unexpected(raw.error());
|
||||
}
|
||||
auto parsed = parse_u32(*raw, arg);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
config.latency.snapshot_copy_delay_us = *parsed;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--emit-stall-ms") {
|
||||
auto raw = next_value(argc, argv, i, arg);
|
||||
if (!raw) {
|
||||
return std::unexpected(raw.error());
|
||||
}
|
||||
auto parsed = parse_u32(*raw, arg);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
config.latency.emit_stall_ms = *parsed;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--help" || arg == "-h" || arg == "--version") {
|
||||
continue;
|
||||
}
|
||||
|
||||
return std::unexpected("unknown argument: " + std::string(arg));
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
std::expected<void, std::string> validate_runtime_config(const RuntimeConfig &config) {
|
||||
if (config.input.shm_name.empty()) {
|
||||
return std::unexpected("invalid input config: --shm-name must not be empty");
|
||||
}
|
||||
|
||||
if (config.input.zmq_endpoint.empty()) {
|
||||
return std::unexpected("invalid input config: --zmq-endpoint must not be empty");
|
||||
}
|
||||
|
||||
if (config.outputs.rtmp.enabled && config.outputs.rtmp.urls.empty()) {
|
||||
return std::unexpected("invalid RTMP config: --rtmp requires at least one --rtmp-url");
|
||||
}
|
||||
|
||||
for (const auto &url : config.outputs.rtmp.urls) {
|
||||
if (url.empty()) {
|
||||
return std::unexpected("invalid RTMP config: --rtmp-url must not be empty");
|
||||
}
|
||||
}
|
||||
|
||||
if (config.outputs.rtmp.mode == RtmpMode::Domestic && config.codec != CodecType::H265) {
|
||||
return std::unexpected(
|
||||
"invalid mode matrix: --rtmp-mode domestic requires --codec h265 (h264+domestic is unsupported)");
|
||||
}
|
||||
|
||||
if (config.outputs.rtp.enabled) {
|
||||
if (!config.outputs.rtp.endpoint || config.outputs.rtp.endpoint->empty()) {
|
||||
return std::unexpected("invalid RTP config: --rtp requires --rtp-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: --rtp-payload-type must be in dynamic range [96,127]");
|
||||
}
|
||||
|
||||
if (config.outputs.rtp.sdp_path && config.outputs.rtp.sdp_path->empty()) {
|
||||
return std::unexpected("invalid RTP config: --rtp-sdp/--sdp must not be empty");
|
||||
}
|
||||
}
|
||||
|
||||
if (config.latency.queue_size == 0) {
|
||||
return std::unexpected("invalid latency config: --queue-size must be >= 1");
|
||||
}
|
||||
|
||||
if (config.latency.gop == 0) {
|
||||
return std::unexpected("invalid latency config: --gop must be >= 1");
|
||||
}
|
||||
|
||||
if (config.latency.b_frames > config.latency.gop) {
|
||||
return std::unexpected("invalid latency 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.shm=" << config.input.shm_name;
|
||||
ss << ", input.zmq=" << config.input.zmq_endpoint;
|
||||
ss << ", run_mode=" << to_string(config.run_mode);
|
||||
ss << ", codec=" << to_string(config.codec);
|
||||
ss << ", rtmp.enabled=" << (config.outputs.rtmp.enabled ? "true" : "false");
|
||||
ss << ", rtmp.mode=" << to_string(config.outputs.rtmp.mode);
|
||||
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 << ", rtp.sdp=" << (config.outputs.rtp.sdp_path ? *config.outputs.rtp.sdp_path : "<auto>");
|
||||
ss << ", latency.queue_size=" << config.latency.queue_size;
|
||||
ss << ", latency.gop=" << config.latency.gop;
|
||||
ss << ", latency.b_frames=" << config.latency.b_frames;
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
#include "cvmmap_streamer/config/runtime_config.hpp"
|
||||
#include "cvmmap_streamer/ipc/contracts.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <condition_variable>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <zmq.hpp>
|
||||
|
||||
namespace cvmmap_streamer::core {
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
namespace ipc = cvmmap_streamer::ipc;
|
||||
|
||||
struct SharedMemoryView {
|
||||
SharedMemoryView() = default;
|
||||
|
||||
int fd{-1};
|
||||
std::uint8_t *ptr{nullptr};
|
||||
std::size_t bytes{0};
|
||||
|
||||
~SharedMemoryView() {
|
||||
if (ptr != nullptr && ptr != MAP_FAILED && bytes > 0) {
|
||||
munmap(ptr, bytes);
|
||||
}
|
||||
if (fd >= 0) {
|
||||
close(fd);
|
||||
}
|
||||
}
|
||||
|
||||
SharedMemoryView(const SharedMemoryView &) = delete;
|
||||
SharedMemoryView &operator=(const SharedMemoryView &) = delete;
|
||||
|
||||
SharedMemoryView(SharedMemoryView &&other) noexcept {
|
||||
fd = std::exchange(other.fd, -1);
|
||||
ptr = std::exchange(other.ptr, nullptr);
|
||||
bytes = std::exchange(other.bytes, 0);
|
||||
}
|
||||
|
||||
SharedMemoryView &operator=(SharedMemoryView &&other) noexcept {
|
||||
if (this == &other) {
|
||||
return *this;
|
||||
}
|
||||
if (ptr != nullptr && ptr != MAP_FAILED && bytes > 0) {
|
||||
munmap(ptr, bytes);
|
||||
}
|
||||
if (fd >= 0) {
|
||||
close(fd);
|
||||
}
|
||||
fd = std::exchange(other.fd, -1);
|
||||
ptr = std::exchange(other.ptr, nullptr);
|
||||
bytes = std::exchange(other.bytes, 0);
|
||||
return *this;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
std::span<const std::uint8_t> region() const {
|
||||
return std::span<const std::uint8_t>(ptr, bytes);
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
static std::expected<SharedMemoryView, std::string> open_readonly(const std::string &raw_name) {
|
||||
const auto shm_name = raw_name.starts_with('/') ? raw_name : "/" + raw_name;
|
||||
const int fd = shm_open(shm_name.c_str(), O_RDONLY, 0);
|
||||
if (fd < 0) {
|
||||
return std::unexpected("shm_open failed for '" + shm_name + "'");
|
||||
}
|
||||
|
||||
struct stat statbuf {
|
||||
};
|
||||
if (fstat(fd, &statbuf) != 0) {
|
||||
close(fd);
|
||||
return std::unexpected("fstat failed for '" + shm_name + "'");
|
||||
}
|
||||
if (statbuf.st_size <= 0) {
|
||||
close(fd);
|
||||
return std::unexpected("shared memory size is zero for '" + shm_name + "'");
|
||||
}
|
||||
|
||||
const auto bytes = static_cast<std::size_t>(statbuf.st_size);
|
||||
auto *mapped = static_cast<std::uint8_t *>(mmap(nullptr, bytes, PROT_READ, MAP_SHARED, fd, 0));
|
||||
if (mapped == MAP_FAILED) {
|
||||
close(fd);
|
||||
return std::unexpected("mmap failed for '" + shm_name + "'");
|
||||
}
|
||||
|
||||
SharedMemoryView view;
|
||||
view.fd = fd;
|
||||
view.ptr = mapped;
|
||||
view.bytes = bytes;
|
||||
return view;
|
||||
}
|
||||
};
|
||||
|
||||
struct IngestFrame {
|
||||
std::uint32_t frame_count;
|
||||
std::uint64_t timestamp_ns;
|
||||
std::size_t payload_bytes;
|
||||
};
|
||||
|
||||
struct IngestStats {
|
||||
std::uint64_t dropped_frames{0};
|
||||
std::uint64_t torn_frames{0};
|
||||
std::uint64_t resets{0};
|
||||
std::uint64_t decode_reconfigs{0};
|
||||
std::uint64_t sync_messages{0};
|
||||
std::uint64_t status_messages{0};
|
||||
std::atomic<std::uint64_t> consumed_frames{0};
|
||||
};
|
||||
|
||||
[[nodiscard]]
|
||||
bool frame_info_equal(const ipc::FrameInfo &lhs, const ipc::FrameInfo &rhs) {
|
||||
return lhs.width == rhs.width &&
|
||||
lhs.height == rhs.height &&
|
||||
lhs.channels == rhs.channels &&
|
||||
lhs.depth == rhs.depth &&
|
||||
lhs.pixel_format == rhs.pixel_format &&
|
||||
lhs.buffer_size == rhs.buffer_size;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
std::string status_to_string(ipc::ModuleStatus status) {
|
||||
switch (status) {
|
||||
case ipc::ModuleStatus::Online:
|
||||
return "online";
|
||||
case ipc::ModuleStatus::Offline:
|
||||
return "offline";
|
||||
case ipc::ModuleStatus::StreamReset:
|
||||
return "stream_reset";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int run_ingest_loop(const RuntimeConfig &config) {
|
||||
auto shm = SharedMemoryView::open_readonly(config.input.shm_name);
|
||||
if (!shm) {
|
||||
spdlog::error("ingest open shared memory failed: {}", shm.error());
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (shm->bytes <= ipc::kShmPayloadOffset) {
|
||||
spdlog::error("ingest invalid shared memory size: {}", shm->bytes);
|
||||
return 3;
|
||||
}
|
||||
|
||||
const std::size_t queue_capacity = std::max<std::size_t>(1, config.latency.queue_size);
|
||||
std::vector<std::uint8_t> snapshot_buffer(shm->bytes - ipc::kShmPayloadOffset, static_cast<std::uint8_t>(0));
|
||||
|
||||
zmq::context_t zmq_ctx{1};
|
||||
zmq::socket_t subscriber(zmq_ctx, zmq::socket_type::sub);
|
||||
try {
|
||||
subscriber.set(zmq::sockopt::subscribe, "");
|
||||
subscriber.set(zmq::sockopt::rcvtimeo, 20);
|
||||
subscriber.connect(config.input.zmq_endpoint);
|
||||
} catch (const zmq::error_t &e) {
|
||||
spdlog::error("ingest subscribe failed on '{}': {}", config.input.zmq_endpoint, e.what());
|
||||
return 4;
|
||||
}
|
||||
|
||||
std::mutex queue_mutex;
|
||||
std::condition_variable queue_cv;
|
||||
std::deque<IngestFrame> queue;
|
||||
std::size_t queue_depth_peak{0};
|
||||
std::optional<ipc::FrameInfo> last_frame_info{};
|
||||
std::atomic_bool stop_requested{false};
|
||||
std::atomic_bool producer_offline{false};
|
||||
|
||||
IngestStats stats{};
|
||||
|
||||
std::thread consumer([&]() {
|
||||
while (true) {
|
||||
std::optional<IngestFrame> next{};
|
||||
{
|
||||
std::unique_lock lock(queue_mutex);
|
||||
queue_cv.wait_for(lock, 25ms, [&]() {
|
||||
return stop_requested.load(std::memory_order_relaxed) || !queue.empty();
|
||||
});
|
||||
if (stop_requested.load(std::memory_order_relaxed) && queue.empty()) {
|
||||
break;
|
||||
}
|
||||
if (!queue.empty()) {
|
||||
next.emplace(std::move(queue.front()));
|
||||
queue.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
if (!next) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (config.latency.ingest_consumer_delay_ms > 0) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(config.latency.ingest_consumer_delay_ms));
|
||||
}
|
||||
|
||||
const auto consumed = stats.consumed_frames.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
spdlog::debug(
|
||||
"consume frame_count={} timestamp_ns={} payload_bytes={} consumed_frames={}",
|
||||
next->frame_count,
|
||||
next->timestamp_ns,
|
||||
next->payload_bytes,
|
||||
consumed);
|
||||
|
||||
if (config.latency.ingest_max_frames > 0 && consumed >= config.latency.ingest_max_frames) {
|
||||
stop_requested.store(true, std::memory_order_relaxed);
|
||||
queue_cv.notify_all();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const auto idle_timeout = std::chrono::milliseconds(config.latency.ingest_idle_timeout_ms);
|
||||
auto last_event = std::chrono::steady_clock::now();
|
||||
|
||||
while (!stop_requested.load(std::memory_order_relaxed)) {
|
||||
zmq::message_t message;
|
||||
const auto recv_result = subscriber.recv(message, zmq::recv_flags::none);
|
||||
if (!recv_result) {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now - last_event >= idle_timeout) {
|
||||
spdlog::info(
|
||||
"ingest idle timeout reached ({} ms), stopping",
|
||||
config.latency.ingest_idle_timeout_ms);
|
||||
break;
|
||||
}
|
||||
|
||||
if (producer_offline.load(std::memory_order_relaxed)) {
|
||||
std::lock_guard lock(queue_mutex);
|
||||
if (queue.empty()) {
|
||||
spdlog::info("producer offline and queue drained, stopping ingest loop");
|
||||
break;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
last_event = std::chrono::steady_clock::now();
|
||||
const auto bytes = std::span<const std::uint8_t>(
|
||||
static_cast<const std::uint8_t *>(message.data()),
|
||||
message.size());
|
||||
if (bytes.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (bytes[0] == ipc::kFrameTopicMagic) {
|
||||
stats.sync_messages += 1;
|
||||
auto sync = ipc::parse_sync_message(bytes);
|
||||
if (!sync) {
|
||||
spdlog::warn("sync parse error: {}", ipc::to_string(sync.error()));
|
||||
continue;
|
||||
}
|
||||
|
||||
auto snapshot = ipc::read_coherent_snapshot(shm->region(), snapshot_buffer);
|
||||
if (!snapshot) {
|
||||
if (snapshot.error() == ipc::SnapshotError::TornRead) {
|
||||
stats.torn_frames += 1;
|
||||
}
|
||||
spdlog::warn("snapshot rejected: {}", ipc::to_string(snapshot.error()));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (last_frame_info && !frame_info_equal(*last_frame_info, snapshot->metadata.info)) {
|
||||
stats.decode_reconfigs += 1;
|
||||
spdlog::info(
|
||||
"decode reconfig detected old={}x{}x{} new={}x{}x{}",
|
||||
last_frame_info->width,
|
||||
last_frame_info->height,
|
||||
static_cast<unsigned>(last_frame_info->channels),
|
||||
snapshot->metadata.info.width,
|
||||
snapshot->metadata.info.height,
|
||||
static_cast<unsigned>(snapshot->metadata.info.channels));
|
||||
}
|
||||
last_frame_info = snapshot->metadata.info;
|
||||
|
||||
IngestFrame frame{
|
||||
.frame_count = snapshot->metadata.frame_count,
|
||||
.timestamp_ns = snapshot->metadata.timestamp_ns,
|
||||
.payload_bytes = snapshot->bytes_copied};
|
||||
|
||||
std::size_t depth_after_push{0};
|
||||
{
|
||||
std::lock_guard lock(queue_mutex);
|
||||
while (queue.size() >= queue_capacity) {
|
||||
queue.pop_front();
|
||||
stats.dropped_frames += 1;
|
||||
}
|
||||
queue.push_back(std::move(frame));
|
||||
depth_after_push = queue.size();
|
||||
queue_depth_peak = std::max(queue_depth_peak, depth_after_push);
|
||||
}
|
||||
queue_cv.notify_one();
|
||||
|
||||
spdlog::debug(
|
||||
"ingest sync={} snapshot={} queue_depth={} dropped_frames={}",
|
||||
sync->frame_count,
|
||||
snapshot->metadata.frame_count,
|
||||
depth_after_push,
|
||||
stats.dropped_frames);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (bytes[0] == ipc::kModuleStatusMagic) {
|
||||
stats.status_messages += 1;
|
||||
auto status = ipc::parse_module_status_message(bytes);
|
||||
if (!status) {
|
||||
spdlog::warn("status parse error: {}", ipc::to_string(status.error()));
|
||||
continue;
|
||||
}
|
||||
|
||||
spdlog::info(
|
||||
"status event label='{}' status={}",
|
||||
status->label(),
|
||||
status_to_string(status->module_status));
|
||||
|
||||
if (status->module_status == ipc::ModuleStatus::Online) {
|
||||
producer_offline.store(false, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
if (status->module_status == ipc::ModuleStatus::Offline) {
|
||||
producer_offline.store(true, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
if (status->module_status == ipc::ModuleStatus::StreamReset) {
|
||||
stats.resets += 1;
|
||||
std::size_t cleared{0};
|
||||
{
|
||||
std::lock_guard lock(queue_mutex);
|
||||
cleared = queue.size();
|
||||
queue.clear();
|
||||
}
|
||||
last_frame_info.reset();
|
||||
spdlog::info(
|
||||
"ingest state reset applied queue_cleared={} resets={}",
|
||||
cleared,
|
||||
stats.resets);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
spdlog::warn("unknown message type: magic=0x{:02x} size={}", bytes[0], bytes.size());
|
||||
}
|
||||
|
||||
stop_requested.store(true, std::memory_order_relaxed);
|
||||
queue_cv.notify_all();
|
||||
consumer.join();
|
||||
|
||||
std::size_t final_depth{0};
|
||||
{
|
||||
std::lock_guard lock(queue_mutex);
|
||||
final_depth = queue.size();
|
||||
}
|
||||
|
||||
spdlog::info(
|
||||
"INGEST_METRICS queue_capacity={} queue_depth={} queue_depth_peak={} dropped_frames={} torn_frames={} resets={} decode_reconfigs={} consumed_frames={} sync_messages={} status_messages={}",
|
||||
queue_capacity,
|
||||
final_depth,
|
||||
queue_depth_peak,
|
||||
stats.dropped_frames,
|
||||
stats.torn_frames,
|
||||
stats.resets,
|
||||
stats.decode_reconfigs,
|
||||
stats.consumed_frames.load(std::memory_order_relaxed),
|
||||
stats.sync_messages,
|
||||
stats.status_messages);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
#include "cvmmap_streamer/ipc/contracts.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
|
||||
namespace cvmmap_streamer::ipc {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::size_t kSyncMessageSize = 48;
|
||||
constexpr std::size_t kModuleStatusMessageSize = 32;
|
||||
constexpr std::size_t kControlRequestBaseSize = 36;
|
||||
constexpr std::size_t kControlResponseBaseSize = 40;
|
||||
constexpr std::size_t kFrameMetadataRequiredBytes = 36;
|
||||
|
||||
constexpr std::size_t kSyncFrameCountOffset = 4;
|
||||
constexpr std::size_t kSyncTimestampOffset = 16;
|
||||
constexpr std::size_t kSyncLabelOffset = 24;
|
||||
|
||||
constexpr std::size_t kModuleStatusCodeOffset = 4;
|
||||
constexpr std::size_t kModuleStatusLabelOffset = 8;
|
||||
|
||||
constexpr std::size_t kControlCommandOffset = 4;
|
||||
constexpr std::size_t kControlReqLabelOffset = 8;
|
||||
constexpr std::size_t kControlReqLengthOffset = 32;
|
||||
constexpr std::size_t kControlReqPayloadOffset = 34;
|
||||
|
||||
constexpr std::size_t kControlRespCodeOffset = 8;
|
||||
constexpr std::size_t kControlRespLabelOffset = 12;
|
||||
constexpr std::size_t kControlRespLengthOffset = 36;
|
||||
constexpr std::size_t kControlRespPayloadOffset = 38;
|
||||
|
||||
constexpr std::size_t kMetaVersionMajorOffset = 8;
|
||||
constexpr std::size_t kMetaVersionMinorOffset = 9;
|
||||
constexpr std::size_t kMetaFrameCountOffset = 12;
|
||||
constexpr std::size_t kMetaTimestampOffset = 16;
|
||||
constexpr std::size_t kMetaFrameInfoOffset = 24;
|
||||
|
||||
constexpr std::size_t kFrameInfoWidthOffset = 0;
|
||||
constexpr std::size_t kFrameInfoHeightOffset = 2;
|
||||
constexpr std::size_t kFrameInfoChannelsOffset = 4;
|
||||
constexpr std::size_t kFrameInfoDepthOffset = 5;
|
||||
constexpr std::size_t kFrameInfoPixelFmtOffset = 6;
|
||||
constexpr std::size_t kFrameInfoBufferSizeOffset = 8;
|
||||
|
||||
[[nodiscard]]
|
||||
std::uint16_t read_u16_le(std::span<const std::uint8_t> bytes, std::size_t offset) {
|
||||
return static_cast<std::uint16_t>(bytes[offset]) |
|
||||
(static_cast<std::uint16_t>(bytes[offset + 1]) << 8);
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
std::uint32_t read_u32_le(std::span<const std::uint8_t> bytes, std::size_t offset) {
|
||||
return static_cast<std::uint32_t>(bytes[offset]) |
|
||||
(static_cast<std::uint32_t>(bytes[offset + 1]) << 8) |
|
||||
(static_cast<std::uint32_t>(bytes[offset + 2]) << 16) |
|
||||
(static_cast<std::uint32_t>(bytes[offset + 3]) << 24);
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
std::int32_t read_i32_le(std::span<const std::uint8_t> bytes, std::size_t offset) {
|
||||
return static_cast<std::int32_t>(read_u32_le(bytes, offset));
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
std::uint64_t read_u64_le(std::span<const std::uint8_t> bytes, std::size_t offset) {
|
||||
return static_cast<std::uint64_t>(bytes[offset]) |
|
||||
(static_cast<std::uint64_t>(bytes[offset + 1]) << 8) |
|
||||
(static_cast<std::uint64_t>(bytes[offset + 2]) << 16) |
|
||||
(static_cast<std::uint64_t>(bytes[offset + 3]) << 24) |
|
||||
(static_cast<std::uint64_t>(bytes[offset + 4]) << 32) |
|
||||
(static_cast<std::uint64_t>(bytes[offset + 5]) << 40) |
|
||||
(static_cast<std::uint64_t>(bytes[offset + 6]) << 48) |
|
||||
(static_cast<std::uint64_t>(bytes[offset + 7]) << 56);
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
bool is_supported_version(std::uint8_t major, std::uint8_t minor) {
|
||||
return major == kVersionMajor && minor == kVersionMinor;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
std::expected<Depth, ParseError> validate_depth(std::uint8_t depth_raw) {
|
||||
if (depth_raw > static_cast<std::uint8_t>(Depth::F16)) {
|
||||
return std::unexpected(ParseError::InvalidDepth);
|
||||
}
|
||||
return static_cast<Depth>(depth_raw);
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
std::expected<PixelFormat, ParseError> validate_pixel_format(std::uint8_t pixel_format_raw) {
|
||||
if (pixel_format_raw > static_cast<std::uint8_t>(PixelFormat::YUYV)) {
|
||||
return std::unexpected(ParseError::InvalidPixelFormat);
|
||||
}
|
||||
return static_cast<PixelFormat>(pixel_format_raw);
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
std::expected<ModuleStatus, ParseError> validate_module_status(std::int32_t status_raw) {
|
||||
switch (status_raw) {
|
||||
case static_cast<std::int32_t>(ModuleStatus::Online):
|
||||
return ModuleStatus::Online;
|
||||
case static_cast<std::int32_t>(ModuleStatus::Offline):
|
||||
return ModuleStatus::Offline;
|
||||
case static_cast<std::int32_t>(ModuleStatus::StreamReset):
|
||||
return ModuleStatus::StreamReset;
|
||||
default:
|
||||
return std::unexpected(ParseError::InvalidModuleStatus);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
std::string_view to_string(ParseError error) {
|
||||
switch (error) {
|
||||
case ParseError::BufferTooSmall:
|
||||
return "buffer too small";
|
||||
case ParseError::InvalidSize:
|
||||
return "invalid message size";
|
||||
case ParseError::InvalidMagic:
|
||||
return "invalid magic";
|
||||
case ParseError::UnsupportedVersion:
|
||||
return "unsupported version";
|
||||
case ParseError::InvalidDepth:
|
||||
return "invalid depth";
|
||||
case ParseError::InvalidPixelFormat:
|
||||
return "invalid pixel format";
|
||||
case ParseError::InvalidModuleStatus:
|
||||
return "invalid module status";
|
||||
case ParseError::PayloadLengthMismatch:
|
||||
return "payload length mismatch";
|
||||
}
|
||||
return "unknown parse error";
|
||||
}
|
||||
|
||||
std::string_view to_string(SnapshotError error) {
|
||||
switch (error) {
|
||||
case SnapshotError::InvalidShmLayout:
|
||||
return "invalid shared memory layout";
|
||||
case SnapshotError::DestinationTooSmall:
|
||||
return "destination buffer too small";
|
||||
case SnapshotError::TornRead:
|
||||
return "torn read";
|
||||
}
|
||||
return "unknown snapshot error";
|
||||
}
|
||||
|
||||
std::expected<FrameMetadata, ParseError> parse_frame_metadata(std::span<const std::uint8_t> bytes) {
|
||||
if (bytes.size() < kFrameMetadataRequiredBytes) {
|
||||
return std::unexpected(ParseError::BufferTooSmall);
|
||||
}
|
||||
|
||||
FrameMetadata metadata{};
|
||||
std::copy_n(bytes.begin(), kFrameMetadataMagic.size(), metadata.magic.begin());
|
||||
if (metadata.magic != kFrameMetadataMagic) {
|
||||
return std::unexpected(ParseError::InvalidMagic);
|
||||
}
|
||||
|
||||
metadata.versions_major = bytes[kMetaVersionMajorOffset];
|
||||
metadata.versions_minor = bytes[kMetaVersionMinorOffset];
|
||||
if (!is_supported_version(metadata.versions_major, metadata.versions_minor)) {
|
||||
return std::unexpected(ParseError::UnsupportedVersion);
|
||||
}
|
||||
|
||||
metadata.frame_count = read_u32_le(bytes, kMetaFrameCountOffset);
|
||||
metadata.timestamp_ns = read_u64_le(bytes, kMetaTimestampOffset);
|
||||
|
||||
auto frame_info_bytes = bytes.subspan(kMetaFrameInfoOffset);
|
||||
auto depth = validate_depth(frame_info_bytes[kFrameInfoDepthOffset]);
|
||||
if (!depth) {
|
||||
return std::unexpected(depth.error());
|
||||
}
|
||||
auto pixel_format = validate_pixel_format(frame_info_bytes[kFrameInfoPixelFmtOffset]);
|
||||
if (!pixel_format) {
|
||||
return std::unexpected(pixel_format.error());
|
||||
}
|
||||
|
||||
metadata.info.width = read_u16_le(frame_info_bytes, kFrameInfoWidthOffset);
|
||||
metadata.info.height = read_u16_le(frame_info_bytes, kFrameInfoHeightOffset);
|
||||
metadata.info.channels = frame_info_bytes[kFrameInfoChannelsOffset];
|
||||
metadata.info.depth = *depth;
|
||||
metadata.info.pixel_format = *pixel_format;
|
||||
metadata.info.buffer_size = read_u32_le(frame_info_bytes, kFrameInfoBufferSizeOffset);
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
std::expected<SyncMessage, ParseError> parse_sync_message(std::span<const std::uint8_t> bytes) {
|
||||
if (bytes.size() < kSyncMessageSize) {
|
||||
return std::unexpected(ParseError::BufferTooSmall);
|
||||
}
|
||||
if (bytes.size() != kSyncMessageSize) {
|
||||
return std::unexpected(ParseError::InvalidSize);
|
||||
}
|
||||
if (bytes[0] != kFrameTopicMagic) {
|
||||
return std::unexpected(ParseError::InvalidMagic);
|
||||
}
|
||||
if (!is_supported_version(bytes[2], bytes[3])) {
|
||||
return std::unexpected(ParseError::UnsupportedVersion);
|
||||
}
|
||||
|
||||
SyncMessage message{};
|
||||
message.versions_major = bytes[2];
|
||||
message.versions_minor = bytes[3];
|
||||
message.frame_count = read_u32_le(bytes, kSyncFrameCountOffset);
|
||||
message.timestamp_ns = read_u64_le(bytes, kSyncTimestampOffset);
|
||||
std::copy_n(bytes.begin() + kSyncLabelOffset, kLabelLenMax, message.label_bytes.begin());
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
std::expected<ModuleStatusMessage, ParseError> parse_module_status_message(std::span<const std::uint8_t> bytes) {
|
||||
if (bytes.size() < kModuleStatusMessageSize) {
|
||||
return std::unexpected(ParseError::BufferTooSmall);
|
||||
}
|
||||
if (bytes.size() != kModuleStatusMessageSize) {
|
||||
return std::unexpected(ParseError::InvalidSize);
|
||||
}
|
||||
if (bytes[0] != kModuleStatusMagic) {
|
||||
return std::unexpected(ParseError::InvalidMagic);
|
||||
}
|
||||
if (!is_supported_version(bytes[2], bytes[3])) {
|
||||
return std::unexpected(ParseError::UnsupportedVersion);
|
||||
}
|
||||
|
||||
auto status = validate_module_status(read_i32_le(bytes, kModuleStatusCodeOffset));
|
||||
if (!status) {
|
||||
return std::unexpected(status.error());
|
||||
}
|
||||
|
||||
ModuleStatusMessage message{};
|
||||
message.versions_major = bytes[2];
|
||||
message.versions_minor = bytes[3];
|
||||
message.module_status = *status;
|
||||
std::copy_n(bytes.begin() + kModuleStatusLabelOffset, kLabelLenMax, message.label_bytes.begin());
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
std::expected<ControlRequestMessage, ParseError> parse_control_request_message(std::span<const std::uint8_t> bytes) {
|
||||
if (bytes.size() < kControlRequestBaseSize) {
|
||||
return std::unexpected(ParseError::BufferTooSmall);
|
||||
}
|
||||
if (bytes[0] != kControlRequestMagic) {
|
||||
return std::unexpected(ParseError::InvalidMagic);
|
||||
}
|
||||
if (!is_supported_version(bytes[2], bytes[3])) {
|
||||
return std::unexpected(ParseError::UnsupportedVersion);
|
||||
}
|
||||
|
||||
const auto payload_size = static_cast<std::size_t>(read_u16_le(bytes, kControlReqLengthOffset));
|
||||
if (payload_size > bytes.size() - kControlReqPayloadOffset) {
|
||||
return std::unexpected(ParseError::PayloadLengthMismatch);
|
||||
}
|
||||
if (bytes.size() != kControlRequestBaseSize + payload_size) {
|
||||
return std::unexpected(ParseError::InvalidSize);
|
||||
}
|
||||
|
||||
ControlRequestMessage message{};
|
||||
message.versions_major = bytes[2];
|
||||
message.versions_minor = bytes[3];
|
||||
message.command_id = read_i32_le(bytes, kControlCommandOffset);
|
||||
std::copy_n(bytes.begin() + kControlReqLabelOffset, kLabelLenMax, message.label_bytes.begin());
|
||||
message.request_payload = bytes.subspan(kControlReqPayloadOffset, payload_size);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
std::expected<ControlResponseMessage, ParseError> parse_control_response_message(std::span<const std::uint8_t> bytes) {
|
||||
if (bytes.size() < kControlResponseBaseSize) {
|
||||
return std::unexpected(ParseError::BufferTooSmall);
|
||||
}
|
||||
if (bytes[0] != kControlResponseMagic) {
|
||||
return std::unexpected(ParseError::InvalidMagic);
|
||||
}
|
||||
if (!is_supported_version(bytes[2], bytes[3])) {
|
||||
return std::unexpected(ParseError::UnsupportedVersion);
|
||||
}
|
||||
|
||||
const auto payload_size = static_cast<std::size_t>(read_u16_le(bytes, kControlRespLengthOffset));
|
||||
if (payload_size > bytes.size() - kControlRespPayloadOffset) {
|
||||
return std::unexpected(ParseError::PayloadLengthMismatch);
|
||||
}
|
||||
if (bytes.size() != kControlResponseBaseSize + payload_size) {
|
||||
return std::unexpected(ParseError::InvalidSize);
|
||||
}
|
||||
|
||||
ControlResponseMessage message{};
|
||||
message.versions_major = bytes[2];
|
||||
message.versions_minor = bytes[3];
|
||||
message.command_id = read_i32_le(bytes, kControlCommandOffset);
|
||||
message.response_code = read_i32_le(bytes, kControlRespCodeOffset);
|
||||
std::copy_n(bytes.begin() + kControlRespLabelOffset, kLabelLenMax, message.label_bytes.begin());
|
||||
message.response_payload = bytes.subspan(kControlRespPayloadOffset, payload_size);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
std::expected<ValidatedShmView, ParseError> validate_shm_region(std::span<const std::uint8_t> shm_region) {
|
||||
if (shm_region.size() < kShmPayloadOffset) {
|
||||
return std::unexpected(ParseError::BufferTooSmall);
|
||||
}
|
||||
|
||||
auto metadata_result = parse_frame_metadata(shm_region);
|
||||
if (!metadata_result) {
|
||||
return std::unexpected(metadata_result.error());
|
||||
}
|
||||
|
||||
const auto payload_size = static_cast<std::size_t>(metadata_result->info.buffer_size);
|
||||
if (payload_size > std::numeric_limits<std::size_t>::max() - kShmPayloadOffset) {
|
||||
return std::unexpected(ParseError::InvalidSize);
|
||||
}
|
||||
if (payload_size > shm_region.size() - kShmPayloadOffset) {
|
||||
return std::unexpected(ParseError::InvalidSize);
|
||||
}
|
||||
|
||||
return ValidatedShmView{
|
||||
.metadata = *metadata_result,
|
||||
.payload = shm_region.subspan(kShmPayloadOffset, payload_size)};
|
||||
}
|
||||
|
||||
std::expected<CoherentSnapshot, SnapshotError> read_coherent_snapshot(
|
||||
std::span<const std::uint8_t> shm_region,
|
||||
std::span<std::uint8_t> destination,
|
||||
const SnapshotReadHook &before_second_metadata_read) {
|
||||
auto first = validate_shm_region(shm_region);
|
||||
if (!first) {
|
||||
return std::unexpected(SnapshotError::InvalidShmLayout);
|
||||
}
|
||||
|
||||
if (destination.size() < first->payload.size()) {
|
||||
return std::unexpected(SnapshotError::DestinationTooSmall);
|
||||
}
|
||||
|
||||
std::copy(first->payload.begin(), first->payload.end(), destination.begin());
|
||||
|
||||
if (before_second_metadata_read) {
|
||||
before_second_metadata_read();
|
||||
}
|
||||
|
||||
auto second = validate_shm_region(shm_region);
|
||||
if (!second) {
|
||||
return std::unexpected(SnapshotError::InvalidShmLayout);
|
||||
}
|
||||
|
||||
if (first->metadata.frame_count != second->metadata.frame_count ||
|
||||
first->metadata.timestamp_ns != second->metadata.timestamp_ns) {
|
||||
return std::unexpected(SnapshotError::TornRead);
|
||||
}
|
||||
|
||||
return CoherentSnapshot{
|
||||
.metadata = first->metadata,
|
||||
.bytes_copied = first->payload.size()};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#include <array>
|
||||
#include <string_view>
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include "cvmmap_streamer/common.h"
|
||||
|
||||
namespace cvmmap_streamer {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::array<std::string_view, 10> kHelpLines{
|
||||
"Usage:",
|
||||
" --help, -h\tshow this message",
|
||||
"",
|
||||
"Options:",
|
||||
" --version\tprint version information",
|
||||
"",
|
||||
"Examples:",
|
||||
" cvmmap_streamer --help",
|
||||
" cvmmap_sim --help",
|
||||
" rtp_receiver_tester --help"};
|
||||
|
||||
}
|
||||
|
||||
void print_help(std::string_view executable) {
|
||||
spdlog::info("{}", executable);
|
||||
for (const auto &item : kHelpLines) {
|
||||
spdlog::info("{}", item);
|
||||
}
|
||||
}
|
||||
|
||||
bool has_help_flag(int argc, char **argv) {
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
std::string_view arg{argv[i]};
|
||||
if (arg == "--help" || arg == "-h") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#include <cstddef>
|
||||
|
||||
#include "cvmmap_streamer/ipc/contracts.hpp"
|
||||
|
||||
namespace cvmmap_streamer {
|
||||
|
||||
std::size_t sample_ipc_payload_size() {
|
||||
return ipc::kShmPayloadOffset;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
#include "cvmmap_streamer/common.h"
|
||||
#include "cvmmap_streamer/ipc/contracts.hpp"
|
||||
#include "cvmmap_streamer/sim/options.hpp"
|
||||
#include "cvmmap_streamer/sim/wire.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <expected>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <zmq.hpp>
|
||||
|
||||
namespace {
|
||||
|
||||
namespace ipc = cvmmap_streamer::ipc;
|
||||
|
||||
class SharedMemoryRegion {
|
||||
public:
|
||||
static std::expected<SharedMemoryRegion, std::string> create(const std::string &name, std::size_t bytes) {
|
||||
const std::string shm_name = "/" + name;
|
||||
const int fd = shm_open(shm_name.c_str(), O_CREAT | O_RDWR, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
|
||||
if (fd < 0) {
|
||||
return std::unexpected("shm_open failed");
|
||||
}
|
||||
|
||||
if (ftruncate(fd, static_cast<off_t>(bytes)) != 0) {
|
||||
close(fd);
|
||||
shm_unlink(shm_name.c_str());
|
||||
return std::unexpected("ftruncate failed");
|
||||
}
|
||||
|
||||
auto *mapped = static_cast<std::uint8_t *>(mmap(nullptr, bytes, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0));
|
||||
if (mapped == MAP_FAILED) {
|
||||
close(fd);
|
||||
shm_unlink(shm_name.c_str());
|
||||
return std::unexpected("mmap failed");
|
||||
}
|
||||
|
||||
std::memset(mapped, 0, bytes);
|
||||
return SharedMemoryRegion(shm_name, fd, mapped, bytes);
|
||||
}
|
||||
|
||||
SharedMemoryRegion(const SharedMemoryRegion &) = delete;
|
||||
SharedMemoryRegion &operator=(const SharedMemoryRegion &) = delete;
|
||||
|
||||
SharedMemoryRegion(SharedMemoryRegion &&other) noexcept
|
||||
: name_(std::move(other.name_)),
|
||||
fd_(other.fd_),
|
||||
ptr_(other.ptr_),
|
||||
bytes_(other.bytes_) {
|
||||
other.fd_ = -1;
|
||||
other.ptr_ = nullptr;
|
||||
other.bytes_ = 0;
|
||||
}
|
||||
|
||||
SharedMemoryRegion &operator=(SharedMemoryRegion &&other) noexcept {
|
||||
if (this == &other) {
|
||||
return *this;
|
||||
}
|
||||
cleanup();
|
||||
name_ = std::move(other.name_);
|
||||
fd_ = other.fd_;
|
||||
ptr_ = other.ptr_;
|
||||
bytes_ = other.bytes_;
|
||||
other.fd_ = -1;
|
||||
other.ptr_ = nullptr;
|
||||
other.bytes_ = 0;
|
||||
return *this;
|
||||
}
|
||||
|
||||
~SharedMemoryRegion() {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
std::span<std::uint8_t> metadata() {
|
||||
return std::span<std::uint8_t>(ptr_, ipc::kShmPayloadOffset);
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
std::span<std::uint8_t> payload(std::size_t payload_bytes) {
|
||||
return std::span<std::uint8_t>(ptr_ + ipc::kShmPayloadOffset, payload_bytes);
|
||||
}
|
||||
|
||||
private:
|
||||
SharedMemoryRegion(std::string name, int fd, std::uint8_t *ptr, std::size_t bytes)
|
||||
: name_(std::move(name)), fd_(fd), ptr_(ptr), bytes_(bytes) {}
|
||||
|
||||
void cleanup() {
|
||||
if (ptr_ != nullptr && bytes_ > 0) {
|
||||
munmap(ptr_, bytes_);
|
||||
ptr_ = nullptr;
|
||||
}
|
||||
if (fd_ >= 0) {
|
||||
close(fd_);
|
||||
fd_ = -1;
|
||||
}
|
||||
if (!name_.empty()) {
|
||||
shm_unlink(name_.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
std::string name_;
|
||||
int fd_{-1};
|
||||
std::uint8_t *ptr_{nullptr};
|
||||
std::size_t bytes_{0};
|
||||
};
|
||||
|
||||
void cleanup_zmq_ipc_path(std::string_view endpoint) {
|
||||
constexpr std::string_view kPrefix{"ipc://"};
|
||||
if (!endpoint.starts_with(kPrefix)) {
|
||||
return;
|
||||
}
|
||||
const auto path = endpoint.substr(kPrefix.size());
|
||||
if (!path.empty()) {
|
||||
unlink(std::string(path).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (cvmmap_streamer::has_help_flag(argc, argv)) {
|
||||
cvmmap_streamer::sim::print_help();
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto config = cvmmap_streamer::sim::parse_runtime_config(argc, argv);
|
||||
if (!config) {
|
||||
spdlog::error("{}", config.error());
|
||||
cvmmap_streamer::sim::print_help();
|
||||
return 2;
|
||||
}
|
||||
|
||||
const auto payload_bytes = static_cast<std::size_t>(config->payload_size_bytes());
|
||||
const auto switched_width = config->switch_width.value_or(config->width);
|
||||
const auto switched_height = config->switch_height.value_or(config->height);
|
||||
const auto switched_payload_bytes =
|
||||
static_cast<std::size_t>(switched_width) *
|
||||
static_cast<std::size_t>(switched_height) *
|
||||
static_cast<std::size_t>(config->channels);
|
||||
const auto payload_bytes_max = std::max(payload_bytes, switched_payload_bytes);
|
||||
const auto shm_bytes = ipc::kShmPayloadOffset + payload_bytes_max;
|
||||
|
||||
auto shm = SharedMemoryRegion::create(config->shm_name, shm_bytes);
|
||||
if (!shm) {
|
||||
spdlog::error("failed to create shared memory '{}': {}", config->shm_name, shm.error());
|
||||
return 3;
|
||||
}
|
||||
|
||||
cleanup_zmq_ipc_path(config->zmq_endpoint);
|
||||
std::optional<zmq::socket_t> publisher;
|
||||
try {
|
||||
static zmq::context_t context{1};
|
||||
publisher.emplace(context, zmq::socket_type::pub);
|
||||
publisher->bind(config->zmq_endpoint);
|
||||
} catch (const zmq::error_t &e) {
|
||||
spdlog::error("failed to bind zmq endpoint '{}': {}", config->zmq_endpoint, e.what());
|
||||
return 4;
|
||||
}
|
||||
|
||||
ipc::FrameInfo frame_info{
|
||||
.width = config->width,
|
||||
.height = config->height,
|
||||
.channels = config->channels,
|
||||
.depth = config->depth,
|
||||
.pixel_format = config->pixel_format,
|
||||
.buffer_size = config->payload_size_bytes()};
|
||||
|
||||
std::array<std::uint8_t, cvmmap_streamer::sim::kSyncMessageBytes> sync_msg{};
|
||||
std::array<std::uint8_t, cvmmap_streamer::sim::kModuleStatusMessageBytes> status_msg{};
|
||||
|
||||
const auto send_status = [&](ipc::ModuleStatus status, std::uint32_t frame_count) {
|
||||
cvmmap_streamer::sim::write_module_status_message(status_msg, config->label, status);
|
||||
publisher->send(zmq::buffer(status_msg), zmq::send_flags::none);
|
||||
spdlog::info("status={} frame_count={}", static_cast<std::int32_t>(status), frame_count);
|
||||
};
|
||||
|
||||
std::uint64_t timestamp_ns = 1'000'000'000ull;
|
||||
const std::uint64_t tick_ns =
|
||||
(config->fps == 0)
|
||||
? 0ull
|
||||
: std::max<std::uint64_t>(1ull, 1'000'000'000ull / static_cast<std::uint64_t>(config->fps));
|
||||
|
||||
spdlog::info(
|
||||
"sim start shm='{}' zmq='{}' label='{}' frames={} fps={} {}x{} payload={}",
|
||||
config->shm_name,
|
||||
config->zmq_endpoint,
|
||||
config->label,
|
||||
config->frames,
|
||||
config->fps,
|
||||
config->width,
|
||||
config->height,
|
||||
payload_bytes);
|
||||
|
||||
send_status(ipc::ModuleStatus::Online, 0);
|
||||
|
||||
bool reset_sent{false};
|
||||
std::uint32_t reset_every_count{0};
|
||||
bool format_switched{false};
|
||||
for (std::uint32_t frame_count = 1; frame_count <= config->frames; ++frame_count) {
|
||||
if (!format_switched && config->switch_format_at && *config->switch_format_at == frame_count) {
|
||||
frame_info.width = switched_width;
|
||||
frame_info.height = switched_height;
|
||||
frame_info.buffer_size = static_cast<std::uint32_t>(switched_payload_bytes);
|
||||
format_switched = true;
|
||||
spdlog::info(
|
||||
"sim format switch at frame={} new={}x{} channels={} payload={}",
|
||||
frame_count,
|
||||
frame_info.width,
|
||||
frame_info.height,
|
||||
static_cast<unsigned>(frame_info.channels),
|
||||
frame_info.buffer_size);
|
||||
}
|
||||
|
||||
const auto active_payload_bytes = static_cast<std::size_t>(frame_info.buffer_size);
|
||||
auto payload = shm->payload(active_payload_bytes);
|
||||
cvmmap_streamer::sim::write_deterministic_payload(
|
||||
payload,
|
||||
frame_count,
|
||||
frame_info.width,
|
||||
frame_info.height,
|
||||
config->channels);
|
||||
|
||||
cvmmap_streamer::sim::write_frame_metadata(
|
||||
shm->metadata(),
|
||||
frame_info,
|
||||
frame_count,
|
||||
timestamp_ns);
|
||||
|
||||
cvmmap_streamer::sim::write_sync_message(
|
||||
sync_msg,
|
||||
config->label,
|
||||
frame_count,
|
||||
timestamp_ns);
|
||||
publisher->send(zmq::buffer(sync_msg), zmq::send_flags::none);
|
||||
|
||||
spdlog::info("sync frame_count={} timestamp_ns={}", frame_count, timestamp_ns);
|
||||
|
||||
if (!reset_sent && config->emit_reset_at && *config->emit_reset_at == frame_count) {
|
||||
send_status(ipc::ModuleStatus::StreamReset, frame_count);
|
||||
reset_sent = true;
|
||||
}
|
||||
|
||||
if (config->emit_reset_every && *config->emit_reset_every > 0 && (frame_count % *config->emit_reset_every) == 0) {
|
||||
send_status(ipc::ModuleStatus::StreamReset, frame_count);
|
||||
reset_sent = true;
|
||||
reset_every_count += 1;
|
||||
}
|
||||
|
||||
timestamp_ns += tick_ns;
|
||||
if (config->fps > 0) {
|
||||
std::this_thread::sleep_for(std::chrono::nanoseconds(tick_ns));
|
||||
}
|
||||
}
|
||||
|
||||
send_status(ipc::ModuleStatus::Offline, config->frames);
|
||||
cleanup_zmq_ipc_path(config->zmq_endpoint);
|
||||
spdlog::info(
|
||||
"sim complete frames={} reset_sent={} periodic_resets={} format_switched={}",
|
||||
config->frames,
|
||||
reset_sent ? "true" : "false",
|
||||
reset_every_count,
|
||||
format_switched ? "true" : "false");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#include "cvmmap_streamer/common.h"
|
||||
#include "cvmmap_streamer/config/runtime_config.hpp"
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
namespace cvmmap_streamer::core {
|
||||
|
||||
int run_ingest_loop(const RuntimeConfig &config);
|
||||
int run_nvenc_pipeline(const RuntimeConfig &config);
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc <= 1 || cvmmap_streamer::has_help_flag(argc, argv)) {
|
||||
cvmmap_streamer::print_help("cvmmap_streamer");
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto config = cvmmap_streamer::parse_runtime_config(argc, argv);
|
||||
if (!config) {
|
||||
spdlog::error("{}", config.error());
|
||||
cvmmap_streamer::print_help("cvmmap_streamer");
|
||||
return 2;
|
||||
}
|
||||
|
||||
auto validation = cvmmap_streamer::validate_runtime_config(*config);
|
||||
if (!validation) {
|
||||
spdlog::error("{}", validation.error());
|
||||
cvmmap_streamer::print_help("cvmmap_streamer");
|
||||
return 2;
|
||||
}
|
||||
|
||||
spdlog::info("runtime config: {}", cvmmap_streamer::summarize_runtime_config(*config));
|
||||
|
||||
switch (config->run_mode) {
|
||||
case cvmmap_streamer::RunMode::Pipeline:
|
||||
return cvmmap_streamer::core::run_nvenc_pipeline(*config);
|
||||
case cvmmap_streamer::RunMode::Ingest:
|
||||
return cvmmap_streamer::core::run_ingest_loop(*config);
|
||||
}
|
||||
|
||||
spdlog::error("unknown run mode");
|
||||
return 2;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
#include "cvmmap_streamer/metrics/latency_tracker.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
|
||||
namespace cvmmap_streamer::metrics {
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]]
|
||||
std::uint64_t now_ns() {
|
||||
const auto now = std::chrono::steady_clock::now().time_since_epoch();
|
||||
return static_cast<std::uint64_t>(
|
||||
std::chrono::duration_cast<std::chrono::nanoseconds>(now).count());
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
std::uint64_t percentile_from_sorted(const std::vector<std::uint64_t> &sorted, std::uint32_t p) {
|
||||
if (sorted.empty()) {
|
||||
return 0;
|
||||
}
|
||||
const auto n = sorted.size();
|
||||
const auto idx = ((n - 1) * static_cast<std::size_t>(p)) / 100;
|
||||
return sorted[idx];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void IngestEmitLatencyTracker::note_ingest() {
|
||||
ingest_queue_ns_.push_back(now_ns());
|
||||
}
|
||||
|
||||
void IngestEmitLatencyTracker::note_emit() {
|
||||
if (ingest_queue_ns_.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto end_ns = now_ns();
|
||||
const auto begin_ns = ingest_queue_ns_.front();
|
||||
ingest_queue_ns_.pop_front();
|
||||
|
||||
const auto elapsed_ns = end_ns >= begin_ns ? end_ns - begin_ns : 0ull;
|
||||
samples_us_.push_back(elapsed_ns / 1'000ull);
|
||||
}
|
||||
|
||||
void IngestEmitLatencyTracker::note_emit_stall() {
|
||||
emit_stall_events_ += 1;
|
||||
}
|
||||
|
||||
std::uint64_t IngestEmitLatencyTracker::emit_stall_events() const {
|
||||
return emit_stall_events_;
|
||||
}
|
||||
|
||||
std::uint64_t IngestEmitLatencyTracker::pending_frames() const {
|
||||
return static_cast<std::uint64_t>(ingest_queue_ns_.size());
|
||||
}
|
||||
|
||||
LatencySummary IngestEmitLatencyTracker::summarize() const {
|
||||
LatencySummary summary{};
|
||||
summary.samples = static_cast<std::uint64_t>(samples_us_.size());
|
||||
if (samples_us_.empty()) {
|
||||
return summary;
|
||||
}
|
||||
|
||||
auto sorted = samples_us_;
|
||||
std::sort(sorted.begin(), sorted.end());
|
||||
|
||||
summary.min_us = sorted.front();
|
||||
summary.max_us = sorted.back();
|
||||
|
||||
std::uint64_t total{0};
|
||||
for (const auto item : sorted) {
|
||||
total += item;
|
||||
}
|
||||
summary.avg_us = total / static_cast<std::uint64_t>(sorted.size());
|
||||
|
||||
summary.p50_us = percentile_from_sorted(sorted, 50);
|
||||
summary.p95_us = percentile_from_sorted(sorted, 95);
|
||||
summary.p99_us = percentile_from_sorted(sorted, 99);
|
||||
return summary;
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
namespace cvmmap_streamer {
|
||||
|
||||
void protocol_step() {}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,502 @@
|
||||
#include "cvmmap_streamer/protocol/rtp_publisher.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <fcntl.h>
|
||||
#include <netdb.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
namespace cvmmap_streamer {
|
||||
}
|
||||
|
||||
namespace cvmmap_streamer::protocol {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::size_t kRtpHeaderBytes = 12;
|
||||
constexpr std::size_t kRtpPayloadBytesMax = 1200;
|
||||
constexpr std::uint32_t kRtpVideoClockRate = 90'000;
|
||||
constexpr std::uint64_t kNanosPerSecond = 1'000'000'000ull;
|
||||
constexpr std::uint64_t kErrorLogFirstPackets = 8;
|
||||
constexpr std::uint64_t kErrorLogEveryNPackets = 120;
|
||||
|
||||
[[nodiscard]]
|
||||
std::uint32_t compute_ssrc(std::string_view host, std::uint16_t port, std::uint8_t payload_type, CodecType codec) {
|
||||
std::uint32_t hash = 2166136261u;
|
||||
const auto mix_byte = [&](std::uint8_t b) {
|
||||
hash ^= static_cast<std::uint32_t>(b);
|
||||
hash *= 16777619u;
|
||||
};
|
||||
for (const auto ch : host) {
|
||||
mix_byte(static_cast<std::uint8_t>(ch));
|
||||
}
|
||||
mix_byte(static_cast<std::uint8_t>(port >> 8));
|
||||
mix_byte(static_cast<std::uint8_t>(port & 0xffu));
|
||||
mix_byte(payload_type);
|
||||
mix_byte(codec == CodecType::H265 ? 0x65u : 0x64u);
|
||||
if (hash == 0u) {
|
||||
hash = 1u;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
std::uint16_t compute_initial_sequence() {
|
||||
const auto now = std::chrono::steady_clock::now().time_since_epoch();
|
||||
return static_cast<std::uint16_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(now).count() & 0xffffu);
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
std::uint32_t to_rtp_timestamp(std::uint64_t pts_ns) {
|
||||
const auto ticks = (pts_ns * kRtpVideoClockRate) / kNanosPerSecond;
|
||||
return static_cast<std::uint32_t>(ticks & 0xffffffffu);
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
std::string rtp_encoding_name(CodecType codec) {
|
||||
return codec == CodecType::H265 ? "H265" : "H264";
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
std::string rtp_fmtp_line(CodecType codec, std::uint8_t payload_type) {
|
||||
if (codec == CodecType::H265) {
|
||||
return "a=fmtp:" + std::to_string(payload_type) + " sprop-max-don-diff=0";
|
||||
}
|
||||
return "a=fmtp:" + std::to_string(payload_type) + " packetization-mode=1;profile-level-id=42e01f";
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
std::optional<std::pair<std::size_t, std::size_t>> next_start_code(std::span<const std::uint8_t> bytes, std::size_t offset) {
|
||||
for (std::size_t i = offset; i + 3 <= bytes.size(); ++i) {
|
||||
if (bytes[i] == 0 && bytes[i + 1] == 0 && bytes[i + 2] == 1) {
|
||||
return std::pair{i, static_cast<std::size_t>(3)};
|
||||
}
|
||||
if (i + 4 <= bytes.size() && bytes[i] == 0 && bytes[i + 1] == 0 && bytes[i + 2] == 0 && bytes[i + 3] == 1) {
|
||||
return std::pair{i, static_cast<std::size_t>(4)};
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
std::vector<std::span<const std::uint8_t>> split_annexb_nalus(std::span<const std::uint8_t> access_unit) {
|
||||
std::vector<std::span<const std::uint8_t>> nalus{};
|
||||
auto first_sc = next_start_code(access_unit, 0);
|
||||
if (!first_sc) {
|
||||
if (!access_unit.empty()) {
|
||||
nalus.push_back(access_unit);
|
||||
}
|
||||
return nalus;
|
||||
}
|
||||
|
||||
std::size_t cursor = first_sc->first;
|
||||
while (true) {
|
||||
auto current_sc = next_start_code(access_unit, cursor);
|
||||
if (!current_sc) {
|
||||
break;
|
||||
}
|
||||
const std::size_t payload_begin = current_sc->first + current_sc->second;
|
||||
auto next_sc = next_start_code(access_unit, payload_begin);
|
||||
const std::size_t payload_end = next_sc ? next_sc->first : access_unit.size();
|
||||
|
||||
if (payload_begin < payload_end) {
|
||||
nalus.push_back(access_unit.subspan(payload_begin, payload_end - payload_begin));
|
||||
}
|
||||
|
||||
if (!next_sc) {
|
||||
break;
|
||||
}
|
||||
cursor = next_sc->first;
|
||||
}
|
||||
|
||||
if (nalus.empty() && !access_unit.empty()) {
|
||||
nalus.push_back(access_unit);
|
||||
}
|
||||
|
||||
return nalus;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
UdpRtpPublisher::~UdpRtpPublisher() {
|
||||
if (socket_fd_ >= 0) {
|
||||
close(socket_fd_);
|
||||
socket_fd_ = -1;
|
||||
}
|
||||
}
|
||||
|
||||
UdpRtpPublisher::UdpRtpPublisher(UdpRtpPublisher &&other) noexcept {
|
||||
socket_fd_ = std::exchange(other.socket_fd_, -1);
|
||||
destination_host_ = std::move(other.destination_host_);
|
||||
destination_ip_ = std::move(other.destination_ip_);
|
||||
destination_port_ = std::exchange(other.destination_port_, 0);
|
||||
payload_type_ = std::exchange(other.payload_type_, 96);
|
||||
codec_ = std::exchange(other.codec_, CodecType::H264);
|
||||
sequence_ = std::exchange(other.sequence_, 0);
|
||||
ssrc_ = std::exchange(other.ssrc_, 0);
|
||||
sdp_path_ = std::move(other.sdp_path_);
|
||||
stats_ = other.stats_;
|
||||
endpoint_addr_ = other.endpoint_addr_;
|
||||
endpoint_addr_len_ = std::exchange(other.endpoint_addr_len_, 0);
|
||||
}
|
||||
|
||||
UdpRtpPublisher &UdpRtpPublisher::operator=(UdpRtpPublisher &&other) noexcept {
|
||||
if (this == &other) {
|
||||
return *this;
|
||||
}
|
||||
if (socket_fd_ >= 0) {
|
||||
close(socket_fd_);
|
||||
}
|
||||
socket_fd_ = std::exchange(other.socket_fd_, -1);
|
||||
destination_host_ = std::move(other.destination_host_);
|
||||
destination_ip_ = std::move(other.destination_ip_);
|
||||
destination_port_ = std::exchange(other.destination_port_, 0);
|
||||
payload_type_ = std::exchange(other.payload_type_, 96);
|
||||
codec_ = std::exchange(other.codec_, CodecType::H264);
|
||||
sequence_ = std::exchange(other.sequence_, 0);
|
||||
ssrc_ = std::exchange(other.ssrc_, 0);
|
||||
sdp_path_ = std::move(other.sdp_path_);
|
||||
stats_ = other.stats_;
|
||||
endpoint_addr_ = other.endpoint_addr_;
|
||||
endpoint_addr_len_ = std::exchange(other.endpoint_addr_len_, 0);
|
||||
return *this;
|
||||
}
|
||||
|
||||
std::expected<UdpRtpPublisher, std::string> UdpRtpPublisher::create(const RuntimeConfig &config) {
|
||||
if (!config.outputs.rtp.enabled) {
|
||||
return std::unexpected("invalid RTP publisher init: RTP output disabled");
|
||||
}
|
||||
if (!config.outputs.rtp.host || !config.outputs.rtp.port) {
|
||||
return std::unexpected("invalid RTP publisher init: host/port not configured");
|
||||
}
|
||||
|
||||
UdpRtpPublisher publisher{};
|
||||
publisher.destination_host_ = *config.outputs.rtp.host;
|
||||
publisher.destination_port_ = *config.outputs.rtp.port;
|
||||
publisher.payload_type_ = config.outputs.rtp.payload_type;
|
||||
publisher.codec_ = config.codec;
|
||||
publisher.sequence_ = compute_initial_sequence();
|
||||
publisher.ssrc_ = compute_ssrc(
|
||||
publisher.destination_host_,
|
||||
publisher.destination_port_,
|
||||
publisher.payload_type_,
|
||||
publisher.codec_);
|
||||
|
||||
addrinfo hints{};
|
||||
hints.ai_family = AF_INET;
|
||||
hints.ai_socktype = SOCK_DGRAM;
|
||||
|
||||
addrinfo *result = nullptr;
|
||||
const auto port = std::to_string(publisher.destination_port_);
|
||||
const int gai = getaddrinfo(
|
||||
publisher.destination_host_.c_str(),
|
||||
port.c_str(),
|
||||
&hints,
|
||||
&result);
|
||||
if (gai != 0) {
|
||||
return std::unexpected("RTP getaddrinfo failed for host '" + publisher.destination_host_ + "': " + std::string(gai_strerror(gai)));
|
||||
}
|
||||
|
||||
for (auto *it = result; it != nullptr; it = it->ai_next) {
|
||||
if (it->ai_addrlen > sizeof(sockaddr_storage)) {
|
||||
continue;
|
||||
}
|
||||
std::memcpy(&publisher.endpoint_addr_, it->ai_addr, it->ai_addrlen);
|
||||
publisher.endpoint_addr_len_ = static_cast<socklen_t>(it->ai_addrlen);
|
||||
char ip_text[INET6_ADDRSTRLEN]{};
|
||||
if (getnameinfo(
|
||||
it->ai_addr,
|
||||
it->ai_addrlen,
|
||||
ip_text,
|
||||
sizeof(ip_text),
|
||||
nullptr,
|
||||
0,
|
||||
NI_NUMERICHOST) == 0) {
|
||||
publisher.destination_ip_ = ip_text;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
freeaddrinfo(result);
|
||||
|
||||
if (publisher.endpoint_addr_len_ == 0) {
|
||||
return std::unexpected("RTP endpoint resolution failed for host '" + publisher.destination_host_ + "'");
|
||||
}
|
||||
|
||||
publisher.socket_fd_ = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (publisher.socket_fd_ < 0) {
|
||||
return std::unexpected("RTP socket create failed: " + std::string(std::strerror(errno)));
|
||||
}
|
||||
|
||||
const int current_flags = fcntl(publisher.socket_fd_, F_GETFL, 0);
|
||||
if (current_flags < 0 || fcntl(publisher.socket_fd_, F_SETFL, current_flags | O_NONBLOCK) < 0) {
|
||||
return std::unexpected("RTP socket non-blocking setup failed: " + std::string(std::strerror(errno)));
|
||||
}
|
||||
|
||||
const std::string codec_name = config.codec == CodecType::H265 ? "h265" : "h264";
|
||||
if (config.outputs.rtp.sdp_path && !config.outputs.rtp.sdp_path->empty()) {
|
||||
publisher.sdp_path_ = *config.outputs.rtp.sdp_path;
|
||||
} else {
|
||||
publisher.sdp_path_ =
|
||||
"/tmp/cvmmap_streamer_" +
|
||||
codec_name +
|
||||
"_" +
|
||||
std::to_string(publisher.destination_port_) +
|
||||
".sdp";
|
||||
}
|
||||
|
||||
std::filesystem::path sdp_path{publisher.sdp_path_};
|
||||
if (sdp_path.has_parent_path() && !sdp_path.parent_path().empty()) {
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(sdp_path.parent_path(), ec);
|
||||
if (ec) {
|
||||
return std::unexpected("RTP SDP directory create failed: " + ec.message());
|
||||
}
|
||||
}
|
||||
|
||||
std::ofstream sdp(publisher.sdp_path_, std::ios::trunc);
|
||||
if (!sdp.is_open()) {
|
||||
return std::unexpected("RTP SDP open failed: " + publisher.sdp_path_);
|
||||
}
|
||||
|
||||
const auto endpoint_ip = publisher.destination_ip_.empty() ? publisher.destination_host_ : publisher.destination_ip_;
|
||||
sdp << "v=0\n";
|
||||
sdp << "o=- 0 0 IN IP4 " << endpoint_ip << "\n";
|
||||
sdp << "s=cvmmap-streamer\n";
|
||||
sdp << "c=IN IP4 " << endpoint_ip << "\n";
|
||||
sdp << "t=0 0\n";
|
||||
sdp << "m=video " << publisher.destination_port_ << " RTP/AVP " << static_cast<unsigned>(publisher.payload_type_) << "\n";
|
||||
sdp << "a=rtpmap:" << static_cast<unsigned>(publisher.payload_type_) << " " << rtp_encoding_name(publisher.codec_) << "/" << kRtpVideoClockRate << "\n";
|
||||
sdp << rtp_fmtp_line(publisher.codec_, publisher.payload_type_) << "\n";
|
||||
sdp << "a=sendonly\n";
|
||||
sdp << "a=control:streamid=0\n";
|
||||
|
||||
if (!sdp.good()) {
|
||||
return std::unexpected("RTP SDP write failed: " + publisher.sdp_path_);
|
||||
}
|
||||
|
||||
spdlog::info(
|
||||
"RTP_SDP_WRITTEN codec={} payload_type={} destination={}:{} path={}",
|
||||
to_string(publisher.codec_),
|
||||
static_cast<unsigned>(publisher.payload_type_),
|
||||
endpoint_ip,
|
||||
publisher.destination_port_,
|
||||
publisher.sdp_path_);
|
||||
|
||||
return publisher;
|
||||
}
|
||||
|
||||
void UdpRtpPublisher::publish_access_unit(std::span<const std::uint8_t> access_unit, std::uint64_t pts_ns) {
|
||||
stats_.access_units += 1;
|
||||
stats_.access_unit_bytes += access_unit.size();
|
||||
|
||||
if (socket_fd_ < 0 || endpoint_addr_len_ == 0 || access_unit.empty()) {
|
||||
if (!access_unit.empty()) {
|
||||
stats_.packets_dropped += 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const auto send_packet = [&](std::span<const std::uint8_t> payload, bool marker) {
|
||||
std::vector<std::uint8_t> packet{};
|
||||
packet.resize(kRtpHeaderBytes + payload.size());
|
||||
|
||||
packet[0] = 0x80;
|
||||
packet[1] = static_cast<std::uint8_t>((marker ? 0x80u : 0x00u) | (payload_type_ & 0x7fu));
|
||||
|
||||
const auto seq = sequence_++;
|
||||
packet[2] = static_cast<std::uint8_t>((seq >> 8) & 0xffu);
|
||||
packet[3] = static_cast<std::uint8_t>(seq & 0xffu);
|
||||
|
||||
const auto timestamp = to_rtp_timestamp(pts_ns);
|
||||
packet[4] = static_cast<std::uint8_t>((timestamp >> 24) & 0xffu);
|
||||
packet[5] = static_cast<std::uint8_t>((timestamp >> 16) & 0xffu);
|
||||
packet[6] = static_cast<std::uint8_t>((timestamp >> 8) & 0xffu);
|
||||
packet[7] = static_cast<std::uint8_t>(timestamp & 0xffu);
|
||||
|
||||
packet[8] = static_cast<std::uint8_t>((ssrc_ >> 24) & 0xffu);
|
||||
packet[9] = static_cast<std::uint8_t>((ssrc_ >> 16) & 0xffu);
|
||||
packet[10] = static_cast<std::uint8_t>((ssrc_ >> 8) & 0xffu);
|
||||
packet[11] = static_cast<std::uint8_t>(ssrc_ & 0xffu);
|
||||
|
||||
if (!payload.empty()) {
|
||||
std::memcpy(packet.data() + kRtpHeaderBytes, payload.data(), payload.size());
|
||||
}
|
||||
|
||||
const auto sent = sendto(
|
||||
socket_fd_,
|
||||
reinterpret_cast<const void *>(packet.data()),
|
||||
packet.size(),
|
||||
MSG_DONTWAIT,
|
||||
reinterpret_cast<const sockaddr *>(&endpoint_addr_),
|
||||
endpoint_addr_len_);
|
||||
|
||||
if (sent < 0) {
|
||||
stats_.send_errors += 1;
|
||||
stats_.packets_dropped += 1;
|
||||
if (stats_.send_errors <= kErrorLogFirstPackets || (stats_.send_errors % kErrorLogEveryNPackets) == 0) {
|
||||
spdlog::warn(
|
||||
"RTP_SEND_ERROR codec={} payload_type={} destination={} errno={} detail='{}' send_errors={}",
|
||||
to_string(codec_),
|
||||
static_cast<unsigned>(payload_type_),
|
||||
destination(),
|
||||
errno,
|
||||
std::strerror(errno),
|
||||
stats_.send_errors);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (static_cast<std::size_t>(sent) != packet.size()) {
|
||||
stats_.packets_dropped += 1;
|
||||
stats_.send_errors += 1;
|
||||
return false;
|
||||
}
|
||||
|
||||
stats_.packets_sent += 1;
|
||||
stats_.bytes_sent += static_cast<std::uint64_t>(sent);
|
||||
return true;
|
||||
};
|
||||
|
||||
auto nalus = split_annexb_nalus(access_unit);
|
||||
if (nalus.empty()) {
|
||||
nalus.push_back(access_unit);
|
||||
}
|
||||
|
||||
for (std::size_t nal_index = 0; nal_index < nalus.size(); ++nal_index) {
|
||||
const auto nal = nalus[nal_index];
|
||||
const bool is_last_nal = (nal_index + 1) == nalus.size();
|
||||
const bool use_single_ru = nal.size() <= kRtpPayloadBytesMax;
|
||||
|
||||
if (use_single_ru) {
|
||||
(void)send_packet(nal, is_last_nal);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (codec_ == CodecType::H264) {
|
||||
if (nal.size() < 2) {
|
||||
stats_.packets_dropped += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::uint8_t nal_hdr = nal[0];
|
||||
const std::uint8_t fu_indicator = static_cast<std::uint8_t>((nal_hdr & 0xe0u) | 28u);
|
||||
const std::uint8_t nal_type = static_cast<std::uint8_t>(nal_hdr & 0x1fu);
|
||||
|
||||
auto remaining = nal.subspan(1);
|
||||
bool first = true;
|
||||
while (!remaining.empty()) {
|
||||
const auto chunk_size = std::min<std::size_t>(remaining.size(), kRtpPayloadBytesMax - 2);
|
||||
const bool last_chunk = chunk_size == remaining.size();
|
||||
|
||||
std::vector<std::uint8_t> fu_payload{};
|
||||
fu_payload.reserve(2 + chunk_size);
|
||||
fu_payload.push_back(fu_indicator);
|
||||
std::uint8_t fu_header = nal_type;
|
||||
if (first) {
|
||||
fu_header = static_cast<std::uint8_t>(fu_header | 0x80u);
|
||||
}
|
||||
if (last_chunk) {
|
||||
fu_header = static_cast<std::uint8_t>(fu_header | 0x40u);
|
||||
}
|
||||
fu_payload.push_back(fu_header);
|
||||
fu_payload.insert(fu_payload.end(), remaining.begin(), remaining.begin() + static_cast<std::ptrdiff_t>(chunk_size));
|
||||
|
||||
const bool marker = is_last_nal && last_chunk;
|
||||
(void)send_packet(std::span<const std::uint8_t>(fu_payload.data(), fu_payload.size()), marker);
|
||||
|
||||
remaining = remaining.subspan(chunk_size);
|
||||
first = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nal.size() < 3) {
|
||||
stats_.packets_dropped += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::uint8_t hdr0 = nal[0];
|
||||
const std::uint8_t hdr1 = nal[1];
|
||||
const std::uint8_t nal_type = static_cast<std::uint8_t>((hdr0 >> 1) & 0x3fu);
|
||||
|
||||
const std::uint8_t fu_indicator0 = static_cast<std::uint8_t>((hdr0 & 0x81u) | (49u << 1));
|
||||
const std::uint8_t fu_indicator1 = hdr1;
|
||||
|
||||
auto remaining = nal.subspan(2);
|
||||
bool first = true;
|
||||
while (!remaining.empty()) {
|
||||
const auto chunk_size = std::min<std::size_t>(remaining.size(), kRtpPayloadBytesMax - 3);
|
||||
const bool last_chunk = chunk_size == remaining.size();
|
||||
|
||||
std::vector<std::uint8_t> fu_payload{};
|
||||
fu_payload.reserve(3 + chunk_size);
|
||||
fu_payload.push_back(fu_indicator0);
|
||||
fu_payload.push_back(fu_indicator1);
|
||||
std::uint8_t fu_header = nal_type;
|
||||
if (first) {
|
||||
fu_header = static_cast<std::uint8_t>(fu_header | 0x80u);
|
||||
}
|
||||
if (last_chunk) {
|
||||
fu_header = static_cast<std::uint8_t>(fu_header | 0x40u);
|
||||
}
|
||||
fu_payload.push_back(fu_header);
|
||||
fu_payload.insert(fu_payload.end(), remaining.begin(), remaining.begin() + static_cast<std::ptrdiff_t>(chunk_size));
|
||||
|
||||
const bool marker = is_last_nal && last_chunk;
|
||||
(void)send_packet(std::span<const std::uint8_t>(fu_payload.data(), fu_payload.size()), marker);
|
||||
|
||||
remaining = remaining.subspan(chunk_size);
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const RtpPublisherStats &UdpRtpPublisher::stats() const {
|
||||
return stats_;
|
||||
}
|
||||
|
||||
std::string_view UdpRtpPublisher::sdp_path() const {
|
||||
return sdp_path_;
|
||||
}
|
||||
|
||||
std::string_view UdpRtpPublisher::destination() const {
|
||||
static thread_local std::string destination_text{};
|
||||
const auto host_or_ip = destination_ip_.empty() ? destination_host_ : destination_ip_;
|
||||
destination_text = host_or_ip + ":" + std::to_string(destination_port_);
|
||||
return destination_text;
|
||||
}
|
||||
|
||||
void UdpRtpPublisher::log_metrics() const {
|
||||
spdlog::info(
|
||||
"RTP_METRICS codec={} payload_type={} destination={} sdp={} access_units={} access_unit_bytes={} packets_sent={} packets_dropped={} bytes_sent={} send_errors={}",
|
||||
to_string(codec_),
|
||||
static_cast<unsigned>(payload_type_),
|
||||
destination(),
|
||||
sdp_path_,
|
||||
stats_.access_units,
|
||||
stats_.access_unit_bytes,
|
||||
stats_.packets_sent,
|
||||
stats_.packets_dropped,
|
||||
stats_.bytes_sent,
|
||||
stats_.send_errors);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
#include "cvmmap_streamer/sim/options.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <charconv>
|
||||
#include <limits>
|
||||
#include <string_view>
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
namespace cvmmap_streamer::sim {
|
||||
|
||||
namespace {
|
||||
|
||||
std::expected<std::string_view, std::string> next_value(int argc, char **argv, int &index, std::string_view flag_name) {
|
||||
if (index + 1 >= argc) {
|
||||
return std::unexpected("missing value for " + std::string(flag_name));
|
||||
}
|
||||
++index;
|
||||
return std::string_view{argv[index]};
|
||||
}
|
||||
|
||||
std::expected<std::uint32_t, std::string> parse_u32(std::string_view raw, std::string_view flag_name) {
|
||||
std::uint32_t value{0};
|
||||
const auto result = std::from_chars(raw.data(), raw.data() + raw.size(), value, 10);
|
||||
if (result.ec != std::errc{} || result.ptr != raw.data() + raw.size()) {
|
||||
return std::unexpected("invalid value for " + std::string(flag_name) + ": '" + std::string(raw) + "'");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
std::expected<std::uint16_t, std::string> parse_u16(std::string_view raw, std::string_view flag_name) {
|
||||
std::uint16_t value{0};
|
||||
const auto result = std::from_chars(raw.data(), raw.data() + raw.size(), value, 10);
|
||||
if (result.ec != std::errc{} || result.ptr != raw.data() + raw.size()) {
|
||||
return std::unexpected("invalid value for " + std::string(flag_name) + ": '" + std::string(raw) + "'");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
std::uint32_t RuntimeConfig::payload_size_bytes() const {
|
||||
const auto width64 = static_cast<std::uint64_t>(width);
|
||||
const auto height64 = static_cast<std::uint64_t>(height);
|
||||
const auto channels64 = static_cast<std::uint64_t>(channels);
|
||||
return static_cast<std::uint32_t>(width64 * height64 * channels64);
|
||||
}
|
||||
|
||||
void print_help() {
|
||||
constexpr std::array<std::string_view, 17> kHelpLines{
|
||||
"cvmmap_sim",
|
||||
"Usage:",
|
||||
" cvmmap_sim [options]",
|
||||
"",
|
||||
"Required simulation controls:",
|
||||
" --frames <n> total frames to emit (default: 360)",
|
||||
" --fps <n> deterministic frame pacing, 0 disables sleep (default: 60)",
|
||||
" --width <px> frame width (default: 64)",
|
||||
" --height <px> frame height (default: 48)",
|
||||
" --emit-reset-at <n> emit MODULE_STATUS_STREAM_RESET at frame_count n",
|
||||
" --emit-reset-every <n> emit MODULE_STATUS_STREAM_RESET each n frames",
|
||||
" --switch-format-at <n> switch metadata frame format at frame_count n",
|
||||
" --switch-width <px> width after format switch (default: --width)",
|
||||
" --switch-height <px> height after format switch (default: --height)",
|
||||
"",
|
||||
"Optional:",
|
||||
" --label <name> --shm-name <name> --zmq-endpoint <endpoint>"};
|
||||
|
||||
for (const auto &line : kHelpLines) {
|
||||
spdlog::info("{}", line);
|
||||
}
|
||||
}
|
||||
|
||||
std::expected<RuntimeConfig, std::string> parse_runtime_config(int argc, char **argv) {
|
||||
RuntimeConfig config{};
|
||||
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
const std::string_view arg{argv[i]};
|
||||
|
||||
if (arg == "--frames") {
|
||||
auto value = next_value(argc, argv, i, arg);
|
||||
if (!value) {
|
||||
return std::unexpected(value.error());
|
||||
}
|
||||
auto parsed = parse_u32(*value, arg);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
config.frames = *parsed;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--fps") {
|
||||
auto value = next_value(argc, argv, i, arg);
|
||||
if (!value) {
|
||||
return std::unexpected(value.error());
|
||||
}
|
||||
auto parsed = parse_u32(*value, arg);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
config.fps = *parsed;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--width") {
|
||||
auto value = next_value(argc, argv, i, arg);
|
||||
if (!value) {
|
||||
return std::unexpected(value.error());
|
||||
}
|
||||
auto parsed = parse_u16(*value, arg);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
config.width = *parsed;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--height") {
|
||||
auto value = next_value(argc, argv, i, arg);
|
||||
if (!value) {
|
||||
return std::unexpected(value.error());
|
||||
}
|
||||
auto parsed = parse_u16(*value, arg);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
config.height = *parsed;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--emit-reset-at") {
|
||||
auto value = next_value(argc, argv, i, arg);
|
||||
if (!value) {
|
||||
return std::unexpected(value.error());
|
||||
}
|
||||
auto parsed = parse_u32(*value, arg);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
config.emit_reset_at = *parsed;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--emit-reset-every") {
|
||||
auto value = next_value(argc, argv, i, arg);
|
||||
if (!value) {
|
||||
return std::unexpected(value.error());
|
||||
}
|
||||
auto parsed = parse_u32(*value, arg);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
config.emit_reset_every = *parsed;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--switch-format-at") {
|
||||
auto value = next_value(argc, argv, i, arg);
|
||||
if (!value) {
|
||||
return std::unexpected(value.error());
|
||||
}
|
||||
auto parsed = parse_u32(*value, arg);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
config.switch_format_at = *parsed;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--switch-width") {
|
||||
auto value = next_value(argc, argv, i, arg);
|
||||
if (!value) {
|
||||
return std::unexpected(value.error());
|
||||
}
|
||||
auto parsed = parse_u16(*value, arg);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
config.switch_width = *parsed;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--switch-height") {
|
||||
auto value = next_value(argc, argv, i, arg);
|
||||
if (!value) {
|
||||
return std::unexpected(value.error());
|
||||
}
|
||||
auto parsed = parse_u16(*value, arg);
|
||||
if (!parsed) {
|
||||
return std::unexpected(parsed.error());
|
||||
}
|
||||
config.switch_height = *parsed;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--label") {
|
||||
auto value = next_value(argc, argv, i, arg);
|
||||
if (!value) {
|
||||
return std::unexpected(value.error());
|
||||
}
|
||||
config.label = std::string(*value);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--shm-name") {
|
||||
auto value = next_value(argc, argv, i, arg);
|
||||
if (!value) {
|
||||
return std::unexpected(value.error());
|
||||
}
|
||||
config.shm_name = std::string(*value);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--zmq-endpoint") {
|
||||
auto value = next_value(argc, argv, i, arg);
|
||||
if (!value) {
|
||||
return std::unexpected(value.error());
|
||||
}
|
||||
config.zmq_endpoint = std::string(*value);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--help" || arg == "-h") {
|
||||
continue;
|
||||
}
|
||||
|
||||
return std::unexpected("unknown argument: '" + std::string(arg) + "'");
|
||||
}
|
||||
|
||||
if (config.frames == 0) {
|
||||
return std::unexpected("--frames must be > 0");
|
||||
}
|
||||
if (config.width == 0 || config.height == 0) {
|
||||
return std::unexpected("--width and --height must be > 0");
|
||||
}
|
||||
if (config.label.empty()) {
|
||||
return std::unexpected("--label must not be empty");
|
||||
}
|
||||
if (config.label.size() > ipc::kLabelLenMax) {
|
||||
return std::unexpected("--label exceeds 24 bytes");
|
||||
}
|
||||
if (config.shm_name.empty()) {
|
||||
return std::unexpected("--shm-name must not be empty");
|
||||
}
|
||||
if (config.zmq_endpoint.empty()) {
|
||||
return std::unexpected("--zmq-endpoint must not be empty");
|
||||
}
|
||||
if (config.emit_reset_at && *config.emit_reset_at == 0) {
|
||||
return std::unexpected("--emit-reset-at must be in [1, --frames]");
|
||||
}
|
||||
if (config.emit_reset_at && *config.emit_reset_at > config.frames) {
|
||||
return std::unexpected("--emit-reset-at must be in [1, --frames]");
|
||||
}
|
||||
if (config.emit_reset_every && *config.emit_reset_every == 0) {
|
||||
return std::unexpected("--emit-reset-every must be > 0");
|
||||
}
|
||||
if (config.switch_format_at && *config.switch_format_at == 0) {
|
||||
return std::unexpected("--switch-format-at must be in [1, --frames]");
|
||||
}
|
||||
if (config.switch_format_at && *config.switch_format_at > config.frames) {
|
||||
return std::unexpected("--switch-format-at must be in [1, --frames]");
|
||||
}
|
||||
if (config.switch_width && *config.switch_width == 0) {
|
||||
return std::unexpected("--switch-width must be > 0");
|
||||
}
|
||||
if (config.switch_height && *config.switch_height == 0) {
|
||||
return std::unexpected("--switch-height must be > 0");
|
||||
}
|
||||
|
||||
const auto payload_size = static_cast<std::uint64_t>(config.width) *
|
||||
static_cast<std::uint64_t>(config.height) *
|
||||
static_cast<std::uint64_t>(config.channels);
|
||||
if (payload_size > static_cast<std::uint64_t>(std::numeric_limits<std::uint32_t>::max())) {
|
||||
return std::unexpected("computed payload size exceeds cv-mmap frame_info.buffer_size range");
|
||||
}
|
||||
|
||||
const auto switched_width = config.switch_width.value_or(config.width);
|
||||
const auto switched_height = config.switch_height.value_or(config.height);
|
||||
const auto switched_payload_size =
|
||||
static_cast<std::uint64_t>(switched_width) *
|
||||
static_cast<std::uint64_t>(switched_height) *
|
||||
static_cast<std::uint64_t>(config.channels);
|
||||
if (switched_payload_size > static_cast<std::uint64_t>(std::numeric_limits<std::uint32_t>::max())) {
|
||||
return std::unexpected("computed switched payload size exceeds cv-mmap frame_info.buffer_size range");
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
#include "cvmmap_streamer/sim/wire.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
|
||||
namespace cvmmap_streamer::sim {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::size_t kMetaVersionMajorOffset = 8;
|
||||
constexpr std::size_t kMetaVersionMinorOffset = 9;
|
||||
constexpr std::size_t kMetaFrameCountOffset = 12;
|
||||
constexpr std::size_t kMetaTimestampOffset = 16;
|
||||
constexpr std::size_t kMetaFrameInfoOffset = 24;
|
||||
|
||||
constexpr std::size_t kFrameInfoWidthOffset = 0;
|
||||
constexpr std::size_t kFrameInfoHeightOffset = 2;
|
||||
constexpr std::size_t kFrameInfoChannelsOffset = 4;
|
||||
constexpr std::size_t kFrameInfoDepthOffset = 5;
|
||||
constexpr std::size_t kFrameInfoPixelFmtOffset = 6;
|
||||
constexpr std::size_t kFrameInfoBufferSizeOffset = 8;
|
||||
|
||||
constexpr std::size_t kSyncMagicOffset = 0;
|
||||
constexpr std::size_t kSyncVersionMajor = 2;
|
||||
constexpr std::size_t kSyncVersionMinor = 3;
|
||||
constexpr std::size_t kSyncFrameCountOffset = 4;
|
||||
constexpr std::size_t kSyncTimestampOffset = 16;
|
||||
constexpr std::size_t kSyncLabelOffset = 24;
|
||||
|
||||
constexpr std::size_t kModuleStatusMagicOffset = 0;
|
||||
constexpr std::size_t kModuleStatusVersionMajor = 2;
|
||||
constexpr std::size_t kModuleStatusVersionMinor = 3;
|
||||
constexpr std::size_t kModuleStatusCodeOffset = 4;
|
||||
constexpr std::size_t kModuleStatusLabelOffset = 8;
|
||||
|
||||
void write_u16_le(std::span<std::uint8_t> bytes, std::size_t offset, std::uint16_t value) {
|
||||
bytes[offset] = static_cast<std::uint8_t>(value & 0xffu);
|
||||
bytes[offset + 1] = static_cast<std::uint8_t>((value >> 8) & 0xffu);
|
||||
}
|
||||
|
||||
void write_u32_le(std::span<std::uint8_t> bytes, std::size_t offset, std::uint32_t value) {
|
||||
bytes[offset] = static_cast<std::uint8_t>(value & 0xffu);
|
||||
bytes[offset + 1] = static_cast<std::uint8_t>((value >> 8) & 0xffu);
|
||||
bytes[offset + 2] = static_cast<std::uint8_t>((value >> 16) & 0xffu);
|
||||
bytes[offset + 3] = static_cast<std::uint8_t>((value >> 24) & 0xffu);
|
||||
}
|
||||
|
||||
void write_i32_le(std::span<std::uint8_t> bytes, std::size_t offset, std::int32_t value) {
|
||||
write_u32_le(bytes, offset, static_cast<std::uint32_t>(value));
|
||||
}
|
||||
|
||||
void write_u64_le(std::span<std::uint8_t> bytes, std::size_t offset, std::uint64_t value) {
|
||||
for (std::size_t i = 0; i < sizeof(std::uint64_t); ++i) {
|
||||
bytes[offset + i] = static_cast<std::uint8_t>((value >> (i * 8)) & 0xffu);
|
||||
}
|
||||
}
|
||||
|
||||
void write_label_bytes(std::span<std::uint8_t> out, std::size_t offset, std::string_view label) {
|
||||
const auto bounded = std::min<std::size_t>(label.size(), ipc::kLabelLenMax);
|
||||
std::fill_n(out.begin() + offset, ipc::kLabelLenMax, static_cast<std::uint8_t>(0));
|
||||
for (std::size_t i = 0; i < bounded; ++i) {
|
||||
out[offset + i] = static_cast<std::uint8_t>(label[i]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void write_frame_metadata(
|
||||
std::span<std::uint8_t> metadata,
|
||||
const ipc::FrameInfo &info,
|
||||
std::uint32_t frame_count,
|
||||
std::uint64_t timestamp_ns) {
|
||||
std::fill(metadata.begin(), metadata.end(), static_cast<std::uint8_t>(0));
|
||||
std::copy(
|
||||
ipc::kFrameMetadataMagic.begin(),
|
||||
ipc::kFrameMetadataMagic.end(),
|
||||
metadata.begin());
|
||||
|
||||
metadata[kMetaVersionMajorOffset] = ipc::kVersionMajor;
|
||||
metadata[kMetaVersionMinorOffset] = ipc::kVersionMinor;
|
||||
write_u32_le(metadata, kMetaFrameCountOffset, frame_count);
|
||||
write_u64_le(metadata, kMetaTimestampOffset, timestamp_ns);
|
||||
|
||||
auto frame_info = metadata.subspan(kMetaFrameInfoOffset);
|
||||
write_u16_le(frame_info, kFrameInfoWidthOffset, info.width);
|
||||
write_u16_le(frame_info, kFrameInfoHeightOffset, info.height);
|
||||
frame_info[kFrameInfoChannelsOffset] = info.channels;
|
||||
frame_info[kFrameInfoDepthOffset] = static_cast<std::uint8_t>(info.depth);
|
||||
frame_info[kFrameInfoPixelFmtOffset] = static_cast<std::uint8_t>(info.pixel_format);
|
||||
write_u32_le(frame_info, kFrameInfoBufferSizeOffset, info.buffer_size);
|
||||
}
|
||||
|
||||
void write_sync_message(
|
||||
std::span<std::uint8_t> out,
|
||||
std::string_view label,
|
||||
std::uint32_t frame_count,
|
||||
std::uint64_t timestamp_ns) {
|
||||
std::fill(out.begin(), out.end(), static_cast<std::uint8_t>(0));
|
||||
out[kSyncMagicOffset] = ipc::kFrameTopicMagic;
|
||||
out[kSyncVersionMajor] = ipc::kVersionMajor;
|
||||
out[kSyncVersionMinor] = ipc::kVersionMinor;
|
||||
write_u32_le(out, kSyncFrameCountOffset, frame_count);
|
||||
write_u64_le(out, kSyncTimestampOffset, timestamp_ns);
|
||||
write_label_bytes(out, kSyncLabelOffset, label);
|
||||
}
|
||||
|
||||
void write_module_status_message(
|
||||
std::span<std::uint8_t> out,
|
||||
std::string_view label,
|
||||
ipc::ModuleStatus status) {
|
||||
std::fill(out.begin(), out.end(), static_cast<std::uint8_t>(0));
|
||||
out[kModuleStatusMagicOffset] = ipc::kModuleStatusMagic;
|
||||
out[kModuleStatusVersionMajor] = ipc::kVersionMajor;
|
||||
out[kModuleStatusVersionMinor] = ipc::kVersionMinor;
|
||||
write_i32_le(out, kModuleStatusCodeOffset, static_cast<std::int32_t>(status));
|
||||
write_label_bytes(out, kModuleStatusLabelOffset, label);
|
||||
}
|
||||
|
||||
void write_deterministic_payload(
|
||||
std::span<std::uint8_t> out,
|
||||
std::uint32_t frame_count,
|
||||
std::uint16_t width,
|
||||
std::uint16_t height,
|
||||
std::uint8_t channels) {
|
||||
const auto row_stride = static_cast<std::size_t>(width) * channels;
|
||||
for (std::size_t idx = 0; idx < out.size(); ++idx) {
|
||||
const auto pixel = idx / channels;
|
||||
const auto row = pixel / width;
|
||||
const auto col = pixel % width;
|
||||
const auto ch = idx % channels;
|
||||
out[idx] = static_cast<std::uint8_t>((frame_count + (row * 7u) + (col * 13u) + (ch * 17u) + row_stride + height) & 0xffu);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include "cvmmap_streamer/common.h"
|
||||
#include "cvmmap_streamer/ipc/contracts.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::size_t kMagicOffset = 0;
|
||||
constexpr std::size_t kVersionMajorOffset = 8;
|
||||
constexpr std::size_t kVersionMinorOffset = 9;
|
||||
constexpr std::size_t kFrameCountOffset = 12;
|
||||
constexpr std::size_t kTimestampOffset = 16;
|
||||
constexpr std::size_t kInfoWidthOffset = 24;
|
||||
constexpr std::size_t kInfoHeightOffset = 26;
|
||||
constexpr std::size_t kInfoChannelsOffset = 28;
|
||||
constexpr std::size_t kInfoDepthOffset = 29;
|
||||
constexpr std::size_t kInfoPixelFormatOffset = 30;
|
||||
constexpr std::size_t kInfoBufferSizeOffset = 32;
|
||||
|
||||
void write_u16_le(std::span<std::uint8_t> buffer, std::size_t offset, std::uint16_t value) {
|
||||
buffer[offset] = static_cast<std::uint8_t>(value & 0xffu);
|
||||
buffer[offset + 1] = static_cast<std::uint8_t>((value >> 8) & 0xffu);
|
||||
}
|
||||
|
||||
void write_u32_le(std::span<std::uint8_t> buffer, std::size_t offset, std::uint32_t value) {
|
||||
buffer[offset] = static_cast<std::uint8_t>(value & 0xffu);
|
||||
buffer[offset + 1] = static_cast<std::uint8_t>((value >> 8) & 0xffu);
|
||||
buffer[offset + 2] = static_cast<std::uint8_t>((value >> 16) & 0xffu);
|
||||
buffer[offset + 3] = static_cast<std::uint8_t>((value >> 24) & 0xffu);
|
||||
}
|
||||
|
||||
void write_u64_le(std::span<std::uint8_t> buffer, std::size_t offset, std::uint64_t value) {
|
||||
for (std::size_t i = 0; i < sizeof(std::uint64_t); ++i) {
|
||||
buffer[offset + i] = static_cast<std::uint8_t>((value >> (i * 8)) & 0xffu);
|
||||
}
|
||||
}
|
||||
|
||||
void write_metadata(
|
||||
std::span<std::uint8_t> buffer,
|
||||
std::uint32_t frame_count,
|
||||
std::uint64_t timestamp_ns,
|
||||
std::uint32_t payload_size) {
|
||||
std::copy(
|
||||
cvmmap_streamer::ipc::kFrameMetadataMagic.begin(),
|
||||
cvmmap_streamer::ipc::kFrameMetadataMagic.end(),
|
||||
buffer.begin() + kMagicOffset);
|
||||
buffer[kVersionMajorOffset] = cvmmap_streamer::ipc::kVersionMajor;
|
||||
buffer[kVersionMinorOffset] = cvmmap_streamer::ipc::kVersionMinor;
|
||||
write_u32_le(buffer, kFrameCountOffset, frame_count);
|
||||
write_u64_le(buffer, kTimestampOffset, timestamp_ns);
|
||||
write_u16_le(buffer, kInfoWidthOffset, 2);
|
||||
write_u16_le(buffer, kInfoHeightOffset, 2);
|
||||
buffer[kInfoChannelsOffset] = 1;
|
||||
buffer[kInfoDepthOffset] = static_cast<std::uint8_t>(cvmmap_streamer::ipc::Depth::U8);
|
||||
buffer[kInfoPixelFormatOffset] = static_cast<std::uint8_t>(cvmmap_streamer::ipc::PixelFormat::GRAY);
|
||||
write_u32_le(buffer, kInfoBufferSizeOffset, payload_size);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc <= 1 || cvmmap_streamer::has_help_flag(argc, argv)) {
|
||||
cvmmap_streamer::print_help("ipc_snapshot_tester");
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::array<std::uint8_t, cvmmap_streamer::ipc::kShmPayloadOffset + 32> shm{};
|
||||
auto shm_view = std::span<std::uint8_t>(shm);
|
||||
|
||||
write_metadata(shm_view, 7, 2222, 32);
|
||||
for (std::size_t i = 0; i < 32; ++i) {
|
||||
shm[cvmmap_streamer::ipc::kShmPayloadOffset + i] = static_cast<std::uint8_t>(i + 1);
|
||||
}
|
||||
|
||||
std::array<std::uint8_t, 32> destination{};
|
||||
auto valid = cvmmap_streamer::ipc::read_coherent_snapshot(shm_view, destination);
|
||||
if (!valid) {
|
||||
spdlog::error("coherent snapshot should succeed: {}", cvmmap_streamer::ipc::to_string(valid.error()));
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (valid->bytes_copied != 32 || valid->metadata.frame_count != 7 || valid->metadata.timestamp_ns != 2222) {
|
||||
spdlog::error("valid snapshot verification failed");
|
||||
return 3;
|
||||
}
|
||||
|
||||
const auto torn = cvmmap_streamer::ipc::read_coherent_snapshot(
|
||||
shm_view,
|
||||
destination,
|
||||
[&shm_view]() {
|
||||
write_u32_le(shm_view, kFrameCountOffset, 8);
|
||||
});
|
||||
|
||||
if (torn) {
|
||||
spdlog::error("torn read should be rejected");
|
||||
return 4;
|
||||
}
|
||||
if (torn.error() != cvmmap_streamer::ipc::SnapshotError::TornRead) {
|
||||
spdlog::error("unexpected torn read error: {}", cvmmap_streamer::ipc::to_string(torn.error()));
|
||||
return 5;
|
||||
}
|
||||
|
||||
spdlog::info("snapshot path valid and torn-read rejection verified");
|
||||
return 0;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,505 @@
|
||||
#include <array>
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <expected>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include <netinet/in.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include "cvmmap_streamer/common.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// RFC3550 RTP header constants
|
||||
constexpr std::size_t kRtpHeaderMinSize = 12;
|
||||
constexpr std::uint8_t kRtpVersion = 2;
|
||||
constexpr std::uint8_t kRtpVersionMask = 0xC0;
|
||||
constexpr std::uint8_t kRtpVersionShift = 6;
|
||||
constexpr std::uint8_t kRtpPaddingMask = 0x20;
|
||||
constexpr std::uint8_t kRtpExtensionMask = 0x10;
|
||||
constexpr std::uint8_t kRtpCsrcCountMask = 0x0F;
|
||||
constexpr std::uint8_t kRtpMarkerMask = 0x80;
|
||||
constexpr std::uint8_t kRtpPayloadTypeMask = 0x7F;
|
||||
|
||||
// RTP header structure (RFC3550)
|
||||
struct RtpHeader {
|
||||
std::uint8_t version; // 2 bits
|
||||
bool padding; // 1 bit
|
||||
bool extension; // 1 bit
|
||||
std::uint8_t csrcCount; // 4 bits
|
||||
bool marker; // 1 bit
|
||||
std::uint8_t payloadType; // 7 bits
|
||||
std::uint16_t sequence; // 16 bits
|
||||
std::uint32_t timestamp; // 32 bits
|
||||
std::uint32_t ssrc; // 32 bits
|
||||
};
|
||||
|
||||
// Parsed SDP media info
|
||||
struct SdpMediaInfo {
|
||||
std::string encodingName;
|
||||
std::uint32_t clockRate = 0;
|
||||
std::uint8_t payloadType = 0;
|
||||
bool hasH264 = false;
|
||||
bool hasH265 = false;
|
||||
};
|
||||
|
||||
// Test configuration
|
||||
struct Config {
|
||||
std::uint16_t port = 5004;
|
||||
std::optional<std::uint8_t> expectedPt;
|
||||
std::optional<std::string> sdpFile;
|
||||
std::optional<std::string> decodeHook;
|
||||
std::uint32_t packetThreshold = 10;
|
||||
std::uint32_t timeoutMs = 5000;
|
||||
bool verbose = false;
|
||||
};
|
||||
|
||||
// Test statistics
|
||||
struct Stats {
|
||||
std::uint32_t packetsReceived = 0;
|
||||
std::uint32_t sequenceGaps = 0;
|
||||
std::uint32_t invalidPackets = 0;
|
||||
std::uint16_t lastSequence = 0;
|
||||
std::uint8_t detectedPt = 0;
|
||||
bool hasSeenPacket = false;
|
||||
std::optional<std::uint8_t> ptMismatchError;
|
||||
};
|
||||
|
||||
// Parse RTP header from buffer
|
||||
std::optional<RtpHeader> parseRtpHeader(std::span<const std::uint8_t> data) {
|
||||
if (data.size() < kRtpHeaderMinSize) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
RtpHeader header{};
|
||||
header.version = (data[0] & kRtpVersionMask) >> kRtpVersionShift;
|
||||
header.padding = (data[0] & kRtpPaddingMask) != 0;
|
||||
header.extension = (data[0] & kRtpExtensionMask) != 0;
|
||||
header.csrcCount = data[0] & kRtpCsrcCountMask;
|
||||
header.marker = (data[1] & kRtpMarkerMask) != 0;
|
||||
header.payloadType = data[1] & kRtpPayloadTypeMask;
|
||||
header.sequence = static_cast<std::uint16_t>(data[2]) << 8 | data[3];
|
||||
header.timestamp = static_cast<std::uint32_t>(data[4]) << 24 |
|
||||
static_cast<std::uint32_t>(data[5]) << 16 |
|
||||
static_cast<std::uint32_t>(data[6]) << 8 | data[7];
|
||||
header.ssrc = static_cast<std::uint32_t>(data[8]) << 24 |
|
||||
static_cast<std::uint32_t>(data[9]) << 16 |
|
||||
static_cast<std::uint32_t>(data[10]) << 8 | data[11];
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
// Parse SDP file for media information
|
||||
std::optional<SdpMediaInfo> parseSdpFile(std::string_view path) {
|
||||
std::string pathStr(path);
|
||||
std::ifstream file(pathStr);
|
||||
if (!file.is_open()) {
|
||||
spdlog::error("Failed to open SDP file: {}", path);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
SdpMediaInfo info;
|
||||
std::string line;
|
||||
bool inMediaSection = false;
|
||||
|
||||
while (std::getline(file, line)) {
|
||||
// Remove trailing \r if present
|
||||
if (!line.empty() && line.back() == '\r') {
|
||||
line.pop_back();
|
||||
}
|
||||
|
||||
if (line.starts_with("m=")) {
|
||||
inMediaSection = true;
|
||||
// Parse media line: m=<media> <port> <proto> <pt>
|
||||
// e.g., m=video 5004 RTP/AVP 96
|
||||
std::istringstream iss(line);
|
||||
std::string mediaType, port, proto;
|
||||
int pt = 0;
|
||||
// Skip "m=" prefix
|
||||
if (line.size() > 2 && iss.seekg(2) && std::getline(iss, mediaType, ' ')) {
|
||||
if (iss >> port >> proto >> pt) {
|
||||
info.payloadType = static_cast<std::uint8_t>(pt);
|
||||
}
|
||||
}
|
||||
} else if (inMediaSection && line.starts_with("a=rtpmap:")) {
|
||||
// Parse rtpmap: a=rtpmap:<pt> <encoding>/<clockrate>
|
||||
// e.g., a=rtpmap:96 H264/90000
|
||||
size_t ptEnd = line.find(' ', 9);
|
||||
if (ptEnd != std::string::npos) {
|
||||
size_t slashPos = line.find('/', ptEnd + 1);
|
||||
if (slashPos != std::string::npos) {
|
||||
info.encodingName = line.substr(ptEnd + 1, slashPos - ptEnd - 1);
|
||||
info.clockRate = std::stoul(line.substr(slashPos + 1));
|
||||
}
|
||||
}
|
||||
if (info.encodingName == "H264") {
|
||||
info.hasH264 = true;
|
||||
} else if (info.encodingName == "H265" || info.encodingName == "HEVC") {
|
||||
info.hasH265 = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
// Create UDP socket and bind to port
|
||||
std::expected<int, std::string> createUdpSocket(std::uint16_t port) {
|
||||
int sock = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (sock < 0) {
|
||||
return std::unexpected(std::format("socket failed: {}", std::strerror(errno)));
|
||||
}
|
||||
|
||||
int reuse = 1;
|
||||
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) {
|
||||
close(sock);
|
||||
return std::unexpected(std::format("setsockopt failed: {}", std::strerror(errno)));
|
||||
}
|
||||
|
||||
sockaddr_in addr{};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(port);
|
||||
addr.sin_addr.s_addr = INADDR_ANY;
|
||||
|
||||
if (bind(sock, reinterpret_cast<sockaddr *>(&addr), sizeof(addr)) < 0) {
|
||||
close(sock);
|
||||
return std::unexpected(std::format("bind failed: {}", std::strerror(errno)));
|
||||
}
|
||||
|
||||
return sock;
|
||||
}
|
||||
|
||||
// Parse command-line arguments
|
||||
std::expected<Config, std::string> parseArgs(int argc, char **argv) {
|
||||
Config config;
|
||||
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
std::string_view arg(argv[i]);
|
||||
|
||||
if (arg == "--help" || arg == "-h") {
|
||||
return std::unexpected("help");
|
||||
} else if (arg == "--port" && i + 1 < argc) {
|
||||
config.port = static_cast<std::uint16_t>(std::stoul(argv[++i]));
|
||||
} else if (arg == "--expect-pt" && i + 1 < argc) {
|
||||
config.expectedPt = static_cast<std::uint8_t>(std::stoul(argv[++i]));
|
||||
} else if (arg == "--sdp" && i + 1 < argc) {
|
||||
config.sdpFile = argv[++i];
|
||||
} else if (arg == "--decode-hook" && i + 1 < argc) {
|
||||
config.decodeHook = argv[++i];
|
||||
} else if (arg == "--packet-threshold" && i + 1 < argc) {
|
||||
config.packetThreshold = std::stoul(argv[++i]);
|
||||
} else if (arg == "--timeout-ms" && i + 1 < argc) {
|
||||
config.timeoutMs = std::stoul(argv[++i]);
|
||||
} else if (arg == "--verbose" || arg == "-v") {
|
||||
config.verbose = true;
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
// Print usage
|
||||
void printRtpReceiverUsage() {
|
||||
spdlog::info("rtp_receiver_tester - UDP RTP packet receiver and validator");
|
||||
spdlog::info("");
|
||||
spdlog::info("Usage:");
|
||||
spdlog::info(" rtp_receiver_tester [options]");
|
||||
spdlog::info("");
|
||||
spdlog::info("Options:");
|
||||
spdlog::info(" --port <num> UDP port to listen on (default: 5004)");
|
||||
spdlog::info(" --expect-pt <num> Expected payload type (0-127)");
|
||||
spdlog::info(" --sdp <path> SDP file to validate against");
|
||||
spdlog::info(" --decode-hook <cmd> Optional command to validate payload");
|
||||
spdlog::info(" --packet-threshold <n> Minimum packets to consider success (default: 10)");
|
||||
spdlog::info(" --timeout-ms <ms> Max time to wait for packets (default: 5000)");
|
||||
spdlog::info(" --verbose, -v Enable verbose logging");
|
||||
spdlog::info(" --help, -h Show this message");
|
||||
spdlog::info("");
|
||||
spdlog::info("Examples:");
|
||||
spdlog::info(" rtp_receiver_tester --port 5004 --expect-pt 96");
|
||||
spdlog::info(" rtp_receiver_tester --port 5004 --sdp /tmp/stream.sdp");
|
||||
spdlog::info("");
|
||||
spdlog::info("Exit codes:");
|
||||
spdlog::info(" 0 Success (packets received, PT matches)");
|
||||
spdlog::info(" 1 Invalid arguments");
|
||||
spdlog::info(" 2 Socket/bind error");
|
||||
spdlog::info(" 3 Payload type mismatch");
|
||||
spdlog::info(" 4 Packet threshold not met");
|
||||
spdlog::info(" 5 SDP validation failed");
|
||||
spdlog::info(" 6 Decode hook failed");
|
||||
}
|
||||
|
||||
// Run optional decode hook
|
||||
bool runDecodeHook(std::string_view hookCmd, std::span<const std::uint8_t> payload) {
|
||||
if (hookCmd.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
spdlog::error("fork failed for decode hook");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pid == 0) {
|
||||
// Child process - execute hook
|
||||
// Write payload to stdin of hook command
|
||||
// For simplicity, use a temp file approach or pipe
|
||||
// Here we use execlp with the command
|
||||
execlp("sh", "sh", "-c", std::string(hookCmd).c_str(), nullptr);
|
||||
_exit(127);
|
||||
}
|
||||
|
||||
// Parent - wait for child with timeout
|
||||
int status = 0;
|
||||
int waitResult = waitpid(pid, &status, WNOHANG);
|
||||
|
||||
// Simple non-blocking check - if not ready, we continue
|
||||
// The hook is optional/validation only
|
||||
if (waitResult == 0) {
|
||||
// Still running, don't block
|
||||
spdlog::debug("Decode hook still running (non-blocking)");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (WIFEXITED(status) && WEXITSTATUS(status) == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
spdlog::warn("Decode hook exited with error");
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc <= 1 || cvmmap_streamer::has_help_flag(argc, argv)) {
|
||||
printRtpReceiverUsage();
|
||||
return (argc <= 1) ? 1 : 0;
|
||||
}
|
||||
|
||||
auto configResult = parseArgs(argc, argv);
|
||||
if (!configResult) {
|
||||
if (configResult.error() == "help") {
|
||||
printRtpReceiverUsage();
|
||||
return 0;
|
||||
}
|
||||
spdlog::error("Argument error: {}", configResult.error());
|
||||
printRtpReceiverUsage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
const auto &config = *configResult;
|
||||
|
||||
if (config.verbose) {
|
||||
spdlog::set_level(spdlog::level::debug);
|
||||
}
|
||||
|
||||
// Parse SDP file if provided
|
||||
std::optional<SdpMediaInfo> sdpInfo;
|
||||
if (config.sdpFile) {
|
||||
sdpInfo = parseSdpFile(*config.sdpFile);
|
||||
if (!sdpInfo) {
|
||||
spdlog::error("Failed to parse SDP file: {}", *config.sdpFile);
|
||||
return 5;
|
||||
}
|
||||
spdlog::info("SDP parsed: encoding={}, clock-rate={}, PT={}",
|
||||
sdpInfo->encodingName,
|
||||
sdpInfo->clockRate,
|
||||
sdpInfo->payloadType);
|
||||
|
||||
// Cross-validate expected PT with SDP
|
||||
if (config.expectedPt && *config.expectedPt != sdpInfo->payloadType) {
|
||||
spdlog::error("Expected PT({}) does not match SDP PT({})",
|
||||
*config.expectedPt,
|
||||
sdpInfo->payloadType);
|
||||
return 5;
|
||||
}
|
||||
}
|
||||
|
||||
// Create UDP socket
|
||||
auto sockResult = createUdpSocket(config.port);
|
||||
if (!sockResult) {
|
||||
spdlog::error("Socket error: {}", sockResult.error());
|
||||
return 2;
|
||||
}
|
||||
|
||||
int sock = *sockResult;
|
||||
spdlog::info("Listening on UDP port {} for RTP packets...", config.port);
|
||||
|
||||
Stats stats;
|
||||
auto startTime = std::chrono::steady_clock::now();
|
||||
|
||||
std::vector<std::uint8_t> buffer(65535);
|
||||
sockaddr_in clientAddr{};
|
||||
socklen_t addrLen = sizeof(clientAddr);
|
||||
|
||||
while (true) {
|
||||
// Check timeout
|
||||
auto elapsed = std::chrono::steady_clock::now() - startTime;
|
||||
if (std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count() > config.timeoutMs) {
|
||||
spdlog::info("Timeout reached after {} ms", config.timeoutMs);
|
||||
break;
|
||||
}
|
||||
|
||||
// Non-blocking receive with short timeout using select
|
||||
fd_set readfds;
|
||||
FD_ZERO(&readfds);
|
||||
FD_SET(sock, &readfds);
|
||||
|
||||
timeval tv{};
|
||||
tv.tv_sec = 0;
|
||||
tv.tv_usec = 100000; // 100ms
|
||||
|
||||
int selectResult = select(sock + 1, &readfds, nullptr, nullptr, &tv);
|
||||
if (selectResult < 0) {
|
||||
spdlog::error("select error: {}", std::strerror(errno));
|
||||
break;
|
||||
}
|
||||
if (selectResult == 0) {
|
||||
continue; // Timeout, check overall timeout
|
||||
}
|
||||
|
||||
ssize_t received = recvfrom(sock,
|
||||
buffer.data(),
|
||||
buffer.size(),
|
||||
0,
|
||||
reinterpret_cast<sockaddr *>(&clientAddr),
|
||||
&addrLen);
|
||||
|
||||
if (received < 0) {
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||
continue;
|
||||
}
|
||||
spdlog::error("recvfrom error: {}", std::strerror(errno));
|
||||
break;
|
||||
}
|
||||
|
||||
// Parse RTP header
|
||||
auto headerOpt = parseRtpHeader(std::span(buffer.data(), received));
|
||||
if (!headerOpt) {
|
||||
spdlog::warn("Received invalid packet (too small)");
|
||||
stats.invalidPackets++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto &header = *headerOpt;
|
||||
|
||||
// Validate RTP version
|
||||
if (header.version != kRtpVersion) {
|
||||
spdlog::warn("Invalid RTP version: {} (expected {})", header.version, kRtpVersion);
|
||||
stats.invalidPackets++;
|
||||
continue;
|
||||
}
|
||||
|
||||
stats.packetsReceived++;
|
||||
|
||||
// Track sequence gaps
|
||||
if (stats.hasSeenPacket) {
|
||||
std::uint16_t expectedSeq = stats.lastSequence + 1;
|
||||
if (header.sequence != expectedSeq) {
|
||||
std::uint16_t gap = header.sequence - expectedSeq;
|
||||
stats.sequenceGaps += gap;
|
||||
if (config.verbose) {
|
||||
spdlog::debug("Sequence gap detected: expected {}, got {} (gap size: {})",
|
||||
expectedSeq,
|
||||
header.sequence,
|
||||
gap);
|
||||
}
|
||||
}
|
||||
}
|
||||
stats.lastSequence = header.sequence;
|
||||
stats.hasSeenPacket = true;
|
||||
stats.detectedPt = header.payloadType;
|
||||
|
||||
// Validate payload type if expected
|
||||
if (config.expectedPt && header.payloadType != *config.expectedPt) {
|
||||
spdlog::error("Payload type mismatch: expected {}, got {}",
|
||||
*config.expectedPt,
|
||||
header.payloadType);
|
||||
stats.ptMismatchError = header.payloadType;
|
||||
}
|
||||
|
||||
// Log packet info
|
||||
if (config.verbose) {
|
||||
spdlog::debug("Packet {}: PT={}, seq={}, ts={}, ssrc=0x{:08X}, marker={}",
|
||||
stats.packetsReceived,
|
||||
header.payloadType,
|
||||
header.sequence,
|
||||
header.timestamp,
|
||||
header.ssrc,
|
||||
header.marker);
|
||||
}
|
||||
|
||||
// Run optional decode hook
|
||||
if (config.decodeHook && !stats.ptMismatchError) {
|
||||
std::size_t headerSize = kRtpHeaderMinSize + (header.csrcCount * 4);
|
||||
if (header.extension) {
|
||||
// Skip extension header if present
|
||||
if (received >= headerSize + 4) {
|
||||
std::uint16_t extLength = static_cast<std::uint16_t>(buffer[headerSize + 2]) << 8 |
|
||||
buffer[headerSize + 3];
|
||||
headerSize += 4 + (extLength * 4);
|
||||
}
|
||||
}
|
||||
if (received > static_cast<ssize_t>(headerSize)) {
|
||||
auto payload = std::span(buffer.data() + headerSize, received - headerSize);
|
||||
if (!runDecodeHook(*config.decodeHook, payload)) {
|
||||
spdlog::warn("Decode hook validation failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we've received enough packets
|
||||
if (stats.packetsReceived >= config.packetThreshold) {
|
||||
spdlog::info("Packet threshold reached ({} packets)", config.packetThreshold);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
close(sock);
|
||||
|
||||
// Print summary
|
||||
spdlog::info("");
|
||||
spdlog::info("=== RTP Receiver Statistics ===");
|
||||
spdlog::info("Packets received: {}", stats.packetsReceived);
|
||||
spdlog::info("Sequence gaps: {}", stats.sequenceGaps);
|
||||
spdlog::info("Invalid packets: {}", stats.invalidPackets);
|
||||
if (stats.hasSeenPacket) {
|
||||
spdlog::info("Detected payload type: {}", stats.detectedPt);
|
||||
}
|
||||
|
||||
// Determine exit code
|
||||
if (stats.ptMismatchError) {
|
||||
spdlog::error("FAIL: Payload type mismatch detected (expected {}, got {})",
|
||||
config.expectedPt.value(),
|
||||
*stats.ptMismatchError);
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (stats.packetsReceived < config.packetThreshold) {
|
||||
spdlog::error("FAIL: Packet threshold not met (received {}, required {})",
|
||||
stats.packetsReceived,
|
||||
config.packetThreshold);
|
||||
return 4;
|
||||
}
|
||||
|
||||
spdlog::info("PASS: All validations successful");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user