#define MCAP_IMPLEMENTATION #include #include #include #include #include #include #include #include #include #include namespace { enum class TesterExitCode : int { Success = 0, OpenError = 2, SchemaError = 3, TopicMismatch = 4, TimestampError = 5, ParseError = 6, FormatMismatch = 7, EmptyPayload = 8, ThresholdError = 9, DumpError = 10, }; [[nodiscard]] constexpr int exit_code(TesterExitCode code) { return static_cast(code); } struct Config { std::string input_path{}; std::optional expected_topic{}; std::optional expected_format{}; std::optional dump_annexb_output{}; std::uint32_t min_messages{1}; }; [[nodiscard]] std::expected parse_args(int argc, char **argv) { Config config{}; CLI::App app{"mcap_reader_tester - validate foxglove.CompressedVideo MCAP output"}; app.add_option("input", config.input_path, "Input MCAP path")->required(); app.add_option("--expect-topic", config.expected_topic, "Expected MCAP topic"); app.add_option("--expect-format", config.expected_format, "Expected CompressedVideo format"); app.add_option("--dump-annexb-output", config.dump_annexb_output, "Write concatenated CompressedVideo.data payloads to a file"); app.add_option("--min-messages", config.min_messages, "Minimum expected message count")->check(CLI::PositiveNumber); try { app.parse(argc, argv); } catch (const CLI::ParseError &e) { return std::unexpected(app.exit(e)); } return config; } } int main(int argc, char **argv) { auto config = parse_args(argc, argv); if (!config) { return config.error(); } mcap::McapReader reader{}; 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 exit_code(TesterExitCode::OpenError); } std::uint64_t message_count{0}; std::uint64_t previous_log_time{0}; bool saw_log_time{false}; std::optional dump_stream{}; if (config->dump_annexb_output) { dump_stream.emplace(*config->dump_annexb_output, std::ios::binary | std::ios::trunc); if (!dump_stream->is_open()) { spdlog::error("failed to open dump output '{}'", *config->dump_annexb_output); reader.close(); return exit_code(TesterExitCode::DumpError); } } auto message_view = reader.readMessages(); for (auto it = message_view.begin(); it != message_view.end(); ++it) { if (it->schema == nullptr || it->channel == nullptr) { spdlog::error("MCAP message missing schema or channel metadata"); reader.close(); return exit_code(TesterExitCode::SchemaError); } if (it->schema->encoding != "protobuf" || it->schema->name != "foxglove.CompressedVideo") { continue; } if (it->channel->messageEncoding != "protobuf") { spdlog::error("unexpected MCAP message encoding: {}", it->channel->messageEncoding); reader.close(); 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 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 exit_code(TesterExitCode::TimestampError); } foxglove::CompressedVideo message{}; if (!message.ParseFromArray(it->message.data, static_cast(it->message.dataSize))) { spdlog::error("failed to parse foxglove.CompressedVideo payload"); reader.close(); 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 exit_code(TesterExitCode::FormatMismatch); } if (message.data().empty()) { spdlog::error("compressed video payload is empty"); reader.close(); return exit_code(TesterExitCode::EmptyPayload); } if (dump_stream) { dump_stream->write(message.data().data(), static_cast(message.data().size())); if (!dump_stream->good()) { spdlog::error("failed to write Annex B dump to '{}'", *config->dump_annexb_output); reader.close(); return exit_code(TesterExitCode::DumpError); } } previous_log_time = it->message.logTime; saw_log_time = true; message_count += 1; } reader.close(); if (message_count < config->min_messages) { spdlog::error("message threshold not met: {} < {}", message_count, config->min_messages); return exit_code(TesterExitCode::ThresholdError); } spdlog::info("validated {} foxglove.CompressedVideo MCAP messages", message_count); return exit_code(TesterExitCode::Success); }