refactor(streamer): adopt proxy backends and typed statuses

This commit is contained in:
2026-03-10 23:29:59 +08:00
parent 6af97ee5d3
commit 0ad6887095
22 changed files with 1686 additions and 275 deletions
+19 -6
View File
@@ -11,6 +11,19 @@
namespace {
enum class TesterExitCode : int {
Success = 0,
ReadError = 2,
VerificationError = 3,
TornReadAccepted = 4,
TornReadWrongError = 5,
};
[[nodiscard]]
constexpr int exit_code(TesterExitCode code) {
return static_cast<int>(code);
}
constexpr std::size_t kMagicOffset = 0;
constexpr std::size_t kVersionMajorOffset = 8;
constexpr std::size_t kVersionMinorOffset = 9;
@@ -67,7 +80,7 @@ void write_metadata(
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;
return exit_code(TesterExitCode::Success);
}
std::array<std::uint8_t, cvmmap_streamer::ipc::kShmPayloadOffset + 32> shm{};
@@ -82,12 +95,12 @@ int main(int argc, char **argv) {
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;
return exit_code(TesterExitCode::ReadError);
}
if (valid->bytes_copied != 32 || valid->metadata.frame_count != 7 || valid->metadata.timestamp_ns != 2222) {
spdlog::error("valid snapshot verification failed");
return 3;
return exit_code(TesterExitCode::VerificationError);
}
const auto torn = cvmmap_streamer::ipc::read_coherent_snapshot(
@@ -99,13 +112,13 @@ int main(int argc, char **argv) {
if (torn) {
spdlog::error("torn read should be rejected");
return 4;
return exit_code(TesterExitCode::TornReadAccepted);
}
if (torn.error() != cvmmap_streamer::ipc::SnapshotError::TornRead) {
spdlog::error("unexpected torn read error: {}", cvmmap_streamer::ipc::to_string(torn.error()));
return 5;
return exit_code(TesterExitCode::TornReadWrongError);
}
spdlog::info("snapshot path valid and torn-read rejection verified");
return 0;
return exit_code(TesterExitCode::Success);
}
+27 -10
View File
@@ -15,6 +15,23 @@
namespace {
enum class TesterExitCode : int {
Success = 0,
OpenError = 2,
SchemaError = 3,
TopicMismatch = 4,
TimestampError = 5,
ParseError = 6,
FormatMismatch = 7,
EmptyPayload = 8,
ThresholdError = 9,
};
[[nodiscard]]
constexpr int exit_code(TesterExitCode code) {
return static_cast<int>(code);
}
struct Config {
std::string input_path{};
std::optional<std::string> expected_topic{};
@@ -51,7 +68,7 @@ int main(int argc, char **argv) {
const auto open_status = reader.open(config->input_path);
if (!open_status.ok()) {
spdlog::error("failed to open MCAP file '{}': {}", config->input_path, open_status.message);
return 2;
return exit_code(TesterExitCode::OpenError);
}
std::uint64_t message_count{0};
@@ -63,7 +80,7 @@ int main(int argc, char **argv) {
if (it->schema == nullptr || it->channel == nullptr) {
spdlog::error("MCAP message missing schema or channel metadata");
reader.close();
return 3;
return exit_code(TesterExitCode::SchemaError);
}
if (it->schema->encoding != "protobuf" || it->schema->name != "foxglove.CompressedVideo") {
continue;
@@ -71,34 +88,34 @@ int main(int argc, char **argv) {
if (it->channel->messageEncoding != "protobuf") {
spdlog::error("unexpected MCAP message encoding: {}", it->channel->messageEncoding);
reader.close();
return 3;
return exit_code(TesterExitCode::SchemaError);
}
if (config->expected_topic && it->channel->topic != *config->expected_topic) {
spdlog::error("unexpected topic: expected '{}' got '{}'", *config->expected_topic, it->channel->topic);
reader.close();
return 4;
return exit_code(TesterExitCode::TopicMismatch);
}
if (saw_log_time && it->message.logTime < previous_log_time) {
spdlog::error("non-monotonic logTime detected: {} < {}", it->message.logTime, previous_log_time);
reader.close();
return 5;
return exit_code(TesterExitCode::TimestampError);
}
foxglove::CompressedVideo message{};
if (!message.ParseFromArray(it->message.data, static_cast<int>(it->message.dataSize))) {
spdlog::error("failed to parse foxglove.CompressedVideo payload");
reader.close();
return 6;
return exit_code(TesterExitCode::ParseError);
}
if (config->expected_format && message.format() != *config->expected_format) {
spdlog::error("unexpected format: expected '{}' got '{}'", *config->expected_format, message.format());
reader.close();
return 7;
return exit_code(TesterExitCode::FormatMismatch);
}
if (message.data().empty()) {
spdlog::error("compressed video payload is empty");
reader.close();
return 8;
return exit_code(TesterExitCode::EmptyPayload);
}
previous_log_time = it->message.logTime;
@@ -110,9 +127,9 @@ int main(int argc, char **argv) {
if (message_count < config->min_messages) {
spdlog::error("message threshold not met: {} < {}", message_count, config->min_messages);
return 9;
return exit_code(TesterExitCode::ThresholdError);
}
spdlog::info("validated {} foxglove.CompressedVideo MCAP messages", message_count);
return 0;
return exit_code(TesterExitCode::Success);
}
+238
View File
@@ -0,0 +1,238 @@
#include "cvmmap_streamer/config/runtime_config.hpp"
#include "cvmmap_streamer/encode/encoder_backend.hpp"
#include "cvmmap_streamer/ipc/contracts.hpp"
#include "cvmmap_streamer/protocol/rtmp_output.hpp"
#include <CLI/CLI.hpp>
#include <chrono>
#include <cstdint>
#include <expected>
#include <span>
#include <string>
#include <thread>
#include <vector>
#include <spdlog/spdlog.h>
namespace {
enum class TesterExitCode : int {
Success = 0,
InvalidArgument = 2,
BackendSelectionError = 3,
BackendInitError = 4,
StreamInfoError = 5,
OutputInitError = 6,
PushError = 7,
DrainError = 8,
PublishError = 9,
FlushError = 10,
FlushPublishError = 11,
};
[[nodiscard]]
constexpr int exit_code(TesterExitCode code) {
return static_cast<int>(code);
}
struct Config {
std::string rtmp_url{"rtmp://127.0.0.1/live/cvmmap_streamer_test"};
std::string transport{"libavformat"};
std::string codec{"h264"};
std::string ffmpeg_path{"ffmpeg"};
std::uint32_t frames{48};
std::uint32_t width{320};
std::uint32_t height{240};
std::uint32_t frame_interval_ms{33};
std::uint32_t linger_ms{3000};
};
[[nodiscard]]
std::expected<Config, int> parse_args(int argc, char **argv) {
Config config{};
CLI::App app{"rtmp_output_tester - publish synthetic encoded video to RTMP using the configured sink"};
app.add_option("--rtmp-url", config.rtmp_url, "RTMP destination URL")->required();
app.add_option("--transport", config.transport, "RTMP transport backend (libavformat|ffmpeg_process)")
->check(CLI::IsMember({"libavformat", "ffmpeg_process", "legacy_custom"}));
app.add_option("--codec", config.codec, "Video codec (h264|h265)")
->check(CLI::IsMember({"h264", "h265"}));
app.add_option("--ffmpeg-path", config.ffmpeg_path, "ffmpeg binary path for ffmpeg_process transport");
app.add_option("--frames", config.frames, "Number of frames to publish")->check(CLI::PositiveNumber);
app.add_option("--width", config.width, "Frame width")->check(CLI::PositiveNumber);
app.add_option("--height", config.height, "Frame height")->check(CLI::PositiveNumber);
app.add_option("--frame-interval-ms", config.frame_interval_ms, "Frame interval in milliseconds")->check(CLI::PositiveNumber);
app.add_option("--linger-ms", config.linger_ms, "How long to keep the RTMP output open after flush")->check(CLI::NonNegativeNumber);
try {
app.parse(argc, argv);
} catch (const CLI::ParseError &e) {
return std::unexpected(app.exit(e));
}
return config;
}
[[nodiscard]]
std::expected<cvmmap_streamer::CodecType, std::string> parse_codec(std::string_view raw) {
if (raw == "h264") {
return cvmmap_streamer::CodecType::H264;
}
if (raw == "h265") {
return cvmmap_streamer::CodecType::H265;
}
return std::unexpected("unsupported codec");
}
[[nodiscard]]
std::expected<cvmmap_streamer::RtmpTransportType, std::string> parse_transport(std::string_view raw) {
if (raw == "libavformat") {
return cvmmap_streamer::RtmpTransportType::Libavformat;
}
if (raw == "ffmpeg_process") {
return cvmmap_streamer::RtmpTransportType::FfmpegProcess;
}
if (raw == "legacy_custom") {
return cvmmap_streamer::RtmpTransportType::LegacyCustom;
}
return std::unexpected("unsupported transport");
}
void fill_pattern(std::vector<std::uint8_t> &buffer, std::uint32_t width, std::uint32_t height, std::uint32_t frame_index) {
for (std::uint32_t y = 0; y < height; ++y) {
for (std::uint32_t x = 0; x < width; ++x) {
const std::size_t pixel = static_cast<std::size_t>(y) * width * 3 + static_cast<std::size_t>(x) * 3;
buffer[pixel + 0] = static_cast<std::uint8_t>((x + frame_index * 3) & 0xffu);
buffer[pixel + 1] = static_cast<std::uint8_t>((y * 2 + frame_index * 5) & 0xffu);
buffer[pixel + 2] = static_cast<std::uint8_t>(((x + y) / 2 + frame_index * 7) & 0xffu);
}
}
}
}
int main(int argc, char **argv) {
auto args = parse_args(argc, argv);
if (!args) {
return args.error();
}
auto codec = parse_codec(args->codec);
if (!codec) {
spdlog::error("{}", codec.error());
return exit_code(TesterExitCode::InvalidArgument);
}
auto transport = parse_transport(args->transport);
if (!transport) {
spdlog::error("{}", transport.error());
return exit_code(TesterExitCode::InvalidArgument);
}
cvmmap_streamer::RuntimeConfig config = cvmmap_streamer::RuntimeConfig::defaults();
config.encoder.backend = cvmmap_streamer::EncoderBackendType::FFmpeg;
config.encoder.device = cvmmap_streamer::EncoderDeviceType::Software;
config.encoder.codec = *codec;
config.encoder.gop = 15;
config.encoder.b_frames = 0;
config.outputs.rtmp.enabled = true;
config.outputs.rtmp.urls = {args->rtmp_url};
config.outputs.rtmp.transport = *transport;
config.outputs.rtmp.ffmpeg_path = args->ffmpeg_path;
if (config.outputs.rtmp.transport == cvmmap_streamer::RtmpTransportType::LegacyCustom) {
config.encoder.backend = cvmmap_streamer::EncoderBackendType::GStreamerLegacy;
}
cvmmap_streamer::ipc::FrameInfo frame_info{
.width = static_cast<std::uint16_t>(args->width),
.height = static_cast<std::uint16_t>(args->height),
.channels = 3,
.depth = cvmmap_streamer::ipc::Depth::U8,
.pixel_format = cvmmap_streamer::ipc::PixelFormat::BGR,
.buffer_size = args->width * args->height * 3,
};
auto backend = cvmmap_streamer::encode::make_encoder_backend(config);
if (!backend) {
spdlog::error("failed to select encoder backend: {}", cvmmap_streamer::format_error(backend.error()));
return exit_code(TesterExitCode::BackendSelectionError);
}
auto init = (*backend)->init(config, frame_info);
if (!init) {
spdlog::error("failed to initialize encoder backend: {}", cvmmap_streamer::format_error(init.error()));
return exit_code(TesterExitCode::BackendInitError);
}
auto stream_info = (*backend)->stream_info();
if (!stream_info) {
spdlog::error("failed to get encoder stream info: {}", cvmmap_streamer::format_error(stream_info.error()));
return exit_code(TesterExitCode::StreamInfoError);
}
auto output = cvmmap_streamer::protocol::make_rtmp_output(config, *stream_info);
if (!output) {
spdlog::error("failed to initialize RTMP output: {}", cvmmap_streamer::format_error(output.error()));
return exit_code(TesterExitCode::OutputInitError);
}
std::vector<std::uint8_t> frame_bytes(frame_info.buffer_size, 0);
const auto frame_interval = std::chrono::milliseconds(args->frame_interval_ms);
std::uint64_t timestamp_ns{0};
for (std::uint32_t frame_index = 0; frame_index < args->frames; ++frame_index) {
fill_pattern(frame_bytes, args->width, args->height, frame_index);
auto push = (*backend)->push_frame(cvmmap_streamer::encode::RawVideoFrame{
.info = frame_info,
.source_timestamp_ns = timestamp_ns,
.bytes = std::span<const std::uint8_t>(frame_bytes.data(), frame_bytes.size()),
});
if (!push) {
spdlog::error("encoder push failed at frame {}: {}", frame_index, cvmmap_streamer::format_error(push.error()));
return exit_code(TesterExitCode::PushError);
}
auto drained = (*backend)->drain();
if (!drained) {
spdlog::error("encoder drain failed at frame {}: {}", frame_index, cvmmap_streamer::format_error(drained.error()));
return exit_code(TesterExitCode::DrainError);
}
for (const auto &access_unit : *drained) {
auto publish = (*output)->publish_access_unit(access_unit);
if (!publish) {
spdlog::error("RTMP publish failed at frame {}: {}", frame_index, cvmmap_streamer::format_error(publish.error()));
return exit_code(TesterExitCode::PublishError);
}
}
std::this_thread::sleep_for(frame_interval);
timestamp_ns += static_cast<std::uint64_t>(args->frame_interval_ms) * 1'000'000ull;
}
auto flushed = (*backend)->flush();
if (!flushed) {
spdlog::error("encoder flush failed: {}", cvmmap_streamer::format_error(flushed.error()));
return exit_code(TesterExitCode::FlushError);
}
for (const auto &access_unit : *flushed) {
auto publish = (*output)->publish_access_unit(access_unit);
if (!publish) {
spdlog::error("RTMP publish failed during flush: {}", cvmmap_streamer::format_error(publish.error()));
return exit_code(TesterExitCode::FlushPublishError);
}
}
spdlog::info(
"rtmp_output_tester completed publish: transport={} codec={} frames={} linger_ms={}",
args->transport,
args->codec,
args->frames,
args->linger_ms);
std::this_thread::sleep_for(std::chrono::milliseconds(args->linger_ms));
(*output)->log_metrics();
(*backend)->shutdown();
return exit_code(TesterExitCode::Success);
}
+19 -6
View File
@@ -26,6 +26,19 @@
namespace {
enum class TesterExitCode : int {
Success = 0,
SocketError = 2,
PayloadTypeMismatch = 3,
PacketThresholdError = 4,
SdpValidationError = 5,
};
[[nodiscard]]
constexpr int exit_code(TesterExitCode code) {
return static_cast<int>(code);
}
// RFC3550 RTP header constants
constexpr std::size_t kRtpHeaderMinSize = 12;
constexpr std::uint8_t kRtpVersion = 2;
@@ -300,7 +313,7 @@ int main(int argc, char **argv) {
sdpInfo = parseSdpFile(*config.sdpFile);
if (!sdpInfo) {
spdlog::error("Failed to parse SDP file: {}", *config.sdpFile);
return 5;
return exit_code(TesterExitCode::SdpValidationError);
}
spdlog::info("SDP parsed: encoding={}, clock-rate={}, PT={}",
sdpInfo->encodingName,
@@ -312,7 +325,7 @@ int main(int argc, char **argv) {
spdlog::error("Expected PT({}) does not match SDP PT({})",
*config.expectedPt,
sdpInfo->payloadType);
return 5;
return exit_code(TesterExitCode::SdpValidationError);
}
}
@@ -320,7 +333,7 @@ int main(int argc, char **argv) {
auto sockResult = createUdpSocket(config.port);
if (!sockResult) {
spdlog::error("Socket error: {}", sockResult.error());
return 2;
return exit_code(TesterExitCode::SocketError);
}
int sock = *sockResult;
@@ -473,16 +486,16 @@ int main(int argc, char **argv) {
spdlog::error("FAIL: Payload type mismatch detected (expected {}, got {})",
config.expectedPt.value(),
*stats.ptMismatchError);
return 3;
return exit_code(TesterExitCode::PayloadTypeMismatch);
}
if (stats.packetsReceived < config.packetThreshold) {
spdlog::error("FAIL: Packet threshold not met (received {}, required {})",
stats.packetsReceived,
config.packetThreshold);
return 4;
return exit_code(TesterExitCode::PacketThresholdError);
}
spdlog::info("PASS: All validations successful");
return 0;
return exit_code(TesterExitCode::Success);
}