feat(mcap): add paced replay tooling
This commit is contained in:
@@ -12,9 +12,11 @@
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace cvmmap_streamer::record {
|
||||
|
||||
@@ -46,6 +48,134 @@ google::protobuf::Timestamp to_proto_timestamp(std::uint64_t timestamp_ns) {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
void append_start_code(std::vector<std::uint8_t> &output) {
|
||||
output.push_back(0x00);
|
||||
output.push_back(0x00);
|
||||
output.push_back(0x00);
|
||||
output.push_back(0x01);
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
std::expected<std::uint16_t, std::string> read_be16(std::span<const std::uint8_t> bytes, std::size_t offset) {
|
||||
if (offset + 2 > bytes.size()) {
|
||||
return std::unexpected("decoder config truncated");
|
||||
}
|
||||
return static_cast<std::uint16_t>((static_cast<std::uint16_t>(bytes[offset]) << 8) | bytes[offset + 1]);
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
bool looks_like_annexb(std::span<const std::uint8_t> bytes) {
|
||||
if (bytes.size() >= 4 &&
|
||||
bytes[0] == 0x00 &&
|
||||
bytes[1] == 0x00 &&
|
||||
bytes[2] == 0x00 &&
|
||||
bytes[3] == 0x01) {
|
||||
return true;
|
||||
}
|
||||
return bytes.size() >= 3 &&
|
||||
bytes[0] == 0x00 &&
|
||||
bytes[1] == 0x00 &&
|
||||
bytes[2] == 0x01;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
std::expected<std::vector<std::uint8_t>, std::string> avcc_to_annexb(std::span<const std::uint8_t> decoder_config) {
|
||||
if (decoder_config.size() < 7 || decoder_config[0] != 1) {
|
||||
return std::unexpected("invalid AVC decoder config");
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> annexb{};
|
||||
std::size_t offset = 5;
|
||||
const auto sps_count = static_cast<std::size_t>(decoder_config[offset++] & 0x1fu);
|
||||
for (std::size_t i = 0; i < sps_count; ++i) {
|
||||
auto size = read_be16(decoder_config, offset);
|
||||
if (!size) {
|
||||
return std::unexpected(size.error());
|
||||
}
|
||||
offset += 2;
|
||||
if (offset + *size > decoder_config.size()) {
|
||||
return std::unexpected("invalid AVC decoder config payload");
|
||||
}
|
||||
append_start_code(annexb);
|
||||
annexb.insert(annexb.end(), decoder_config.begin() + static_cast<std::ptrdiff_t>(offset), decoder_config.begin() + static_cast<std::ptrdiff_t>(offset + *size));
|
||||
offset += *size;
|
||||
}
|
||||
|
||||
if (offset >= decoder_config.size()) {
|
||||
return std::unexpected("invalid AVC decoder config: missing PPS count");
|
||||
}
|
||||
const auto pps_count = static_cast<std::size_t>(decoder_config[offset++]);
|
||||
for (std::size_t i = 0; i < pps_count; ++i) {
|
||||
auto size = read_be16(decoder_config, offset);
|
||||
if (!size) {
|
||||
return std::unexpected(size.error());
|
||||
}
|
||||
offset += 2;
|
||||
if (offset + *size > decoder_config.size()) {
|
||||
return std::unexpected("invalid AVC decoder config payload");
|
||||
}
|
||||
append_start_code(annexb);
|
||||
annexb.insert(annexb.end(), decoder_config.begin() + static_cast<std::ptrdiff_t>(offset), decoder_config.begin() + static_cast<std::ptrdiff_t>(offset + *size));
|
||||
offset += *size;
|
||||
}
|
||||
|
||||
return annexb;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
std::expected<std::vector<std::uint8_t>, std::string> hvcc_to_annexb(std::span<const std::uint8_t> decoder_config) {
|
||||
if (decoder_config.size() < 23 || decoder_config[0] != 1) {
|
||||
return std::unexpected("invalid HEVC decoder config");
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> annexb{};
|
||||
std::size_t offset = 22;
|
||||
const auto array_count = static_cast<std::size_t>(decoder_config[offset++]);
|
||||
for (std::size_t array_index = 0; array_index < array_count; ++array_index) {
|
||||
if (offset + 3 > decoder_config.size()) {
|
||||
return std::unexpected("invalid HEVC decoder config arrays");
|
||||
}
|
||||
offset += 1;
|
||||
auto nal_count = read_be16(decoder_config, offset);
|
||||
if (!nal_count) {
|
||||
return std::unexpected(nal_count.error());
|
||||
}
|
||||
offset += 2;
|
||||
|
||||
for (std::size_t nal_index = 0; nal_index < *nal_count; ++nal_index) {
|
||||
auto nal_size = read_be16(decoder_config, offset);
|
||||
if (!nal_size) {
|
||||
return std::unexpected(nal_size.error());
|
||||
}
|
||||
offset += 2;
|
||||
if (offset + *nal_size > decoder_config.size()) {
|
||||
return std::unexpected("invalid HEVC decoder config payload");
|
||||
}
|
||||
append_start_code(annexb);
|
||||
annexb.insert(annexb.end(), decoder_config.begin() + static_cast<std::ptrdiff_t>(offset), decoder_config.begin() + static_cast<std::ptrdiff_t>(offset + *nal_size));
|
||||
offset += *nal_size;
|
||||
}
|
||||
}
|
||||
|
||||
return annexb;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
std::expected<std::vector<std::uint8_t>, std::string> decoder_config_to_annexb(
|
||||
CodecType codec,
|
||||
std::span<const std::uint8_t> decoder_config) {
|
||||
if (decoder_config.empty()) {
|
||||
return std::vector<std::uint8_t>{};
|
||||
}
|
||||
if (looks_like_annexb(decoder_config)) {
|
||||
return std::vector<std::uint8_t>(decoder_config.begin(), decoder_config.end());
|
||||
}
|
||||
if (codec == CodecType::H265) {
|
||||
return hvcc_to_annexb(decoder_config);
|
||||
}
|
||||
return avcc_to_annexb(decoder_config);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct McapRecordSink::State {
|
||||
@@ -54,6 +184,8 @@ struct McapRecordSink::State {
|
||||
std::string frame_id{};
|
||||
mcap::ChannelId channel_id{0};
|
||||
std::uint32_t sequence{0};
|
||||
CodecType codec{CodecType::H264};
|
||||
std::vector<std::uint8_t> keyframe_preamble{};
|
||||
};
|
||||
|
||||
McapRecordSink::~McapRecordSink() {
|
||||
@@ -74,7 +206,9 @@ McapRecordSink &McapRecordSink::operator=(McapRecordSink &&other) noexcept {
|
||||
return *this;
|
||||
}
|
||||
|
||||
std::expected<McapRecordSink, std::string> McapRecordSink::create(const RuntimeConfig &config) {
|
||||
std::expected<McapRecordSink, std::string> McapRecordSink::create(
|
||||
const RuntimeConfig &config,
|
||||
const encode::EncodedStreamInfo &stream_info) {
|
||||
McapRecordSink sink{};
|
||||
auto state = std::make_unique<State>();
|
||||
state->path = config.record.mcap.path;
|
||||
@@ -101,9 +235,27 @@ std::expected<McapRecordSink, std::string> McapRecordSink::create(const RuntimeC
|
||||
state->channel_id = channel.id;
|
||||
|
||||
sink.state_ = state.release();
|
||||
auto update = sink.update_stream_info(stream_info);
|
||||
if (!update) {
|
||||
sink.close();
|
||||
return std::unexpected(update.error());
|
||||
}
|
||||
return sink;
|
||||
}
|
||||
|
||||
std::expected<void, std::string> McapRecordSink::update_stream_info(const encode::EncodedStreamInfo &stream_info) {
|
||||
if (state_ == nullptr) {
|
||||
return std::unexpected("MCAP sink is not open");
|
||||
}
|
||||
auto decoder_config_annexb = decoder_config_to_annexb(stream_info.codec, stream_info.decoder_config);
|
||||
if (!decoder_config_annexb) {
|
||||
return std::unexpected("failed to prepare MCAP keyframe decoder config: " + decoder_config_annexb.error());
|
||||
}
|
||||
state_->codec = stream_info.codec;
|
||||
state_->keyframe_preamble = std::move(*decoder_config_annexb);
|
||||
return {};
|
||||
}
|
||||
|
||||
std::expected<void, std::string> McapRecordSink::write_access_unit(const encode::EncodedAccessUnit &access_unit) {
|
||||
if (state_ == nullptr) {
|
||||
return std::unexpected("MCAP sink is not open");
|
||||
@@ -113,9 +265,15 @@ std::expected<void, std::string> McapRecordSink::write_access_unit(const encode:
|
||||
*message.mutable_timestamp() = to_proto_timestamp(access_unit.source_timestamp_ns);
|
||||
message.set_frame_id(state_->frame_id);
|
||||
message.set_format(codec_format(access_unit.codec));
|
||||
std::vector<std::uint8_t> payload{};
|
||||
if (access_unit.keyframe && !state_->keyframe_preamble.empty()) {
|
||||
payload.reserve(state_->keyframe_preamble.size() + access_unit.annexb_bytes.size());
|
||||
payload.insert(payload.end(), state_->keyframe_preamble.begin(), state_->keyframe_preamble.end());
|
||||
}
|
||||
payload.insert(payload.end(), access_unit.annexb_bytes.begin(), access_unit.annexb_bytes.end());
|
||||
message.set_data(
|
||||
reinterpret_cast<const char *>(access_unit.annexb_bytes.data()),
|
||||
static_cast<int>(access_unit.annexb_bytes.size()));
|
||||
reinterpret_cast<const char *>(payload.data()),
|
||||
static_cast<int>(payload.size()));
|
||||
|
||||
std::string serialized{};
|
||||
if (!message.SerializeToString(&serialized)) {
|
||||
|
||||
Reference in New Issue
Block a user