feat: add streamer-owned recording control service

Introduce a dedicated streamer-side recording control plane instead of sharing the producer recorder API.

- register streamer-owned recorder endpoints as a NATS micro service
- add explicit MP4 and MCAP recorder control protobufs and subject helpers
- wire recorder lifecycle handling into the pipeline runtime
- add MP4 writer and depth-alignment support files used by the new recording flow
This commit is contained in:
2026-04-12 20:21:33 +08:00
parent 4f016d9cef
commit 213adee887
11 changed files with 2474 additions and 400 deletions
@@ -0,0 +1,93 @@
#pragma once
#include "proto/cvmmap_streamer/recorder_control.pb.h"
#include <cstdint>
#include <expected>
#include <functional>
#include <memory>
#include <span>
#include <string>
namespace cvmmap_streamer::protocol {
enum class RpcErrorCode : std::uint8_t {
InvalidRequest,
Unsupported,
Busy,
Internal,
};
struct RpcError {
RpcErrorCode code{RpcErrorCode::Internal};
std::string message{};
};
struct NatsRequestReplyServerOptions {
std::string nats_url{};
std::string instance_name{};
std::string namespace_name{};
std::string ipc_prefix{};
std::string base_name{};
std::string nats_target_key{};
std::string backend{"cvmmap-streamer"};
std::string recording_formats{};
};
class NatsRequestReplyServer {
public:
explicit NatsRequestReplyServer(NatsRequestReplyServerOptions options);
~NatsRequestReplyServer();
NatsRequestReplyServer(const NatsRequestReplyServer &) = delete;
NatsRequestReplyServer &operator=(const NatsRequestReplyServer &) = delete;
template <class Request, class Response>
void register_proto_endpoint(
std::string endpoint_name,
std::string subject,
std::function<std::expected<Response, RpcError>(const Request &)> handler) {
register_raw_endpoint(
std::move(endpoint_name),
std::move(subject),
[handler = std::move(handler)](std::span<const std::uint8_t> payload) {
Request request;
Response response;
if (!request.ParseFromArray(
payload.data(),
static_cast<int>(payload.size()))) {
response.set_code(cvmmap_streamer::proto::RPC_CODE_INVALID_REQUEST);
response.set_error_message("failed to parse protobuf request");
return response.SerializeAsString();
}
auto handled = handler(request);
if (!handled) {
response.set_code(to_proto_rpc_code(handled.error().code));
response.set_error_message(handled.error().message);
return response.SerializeAsString();
}
return handled->SerializeAsString();
});
}
[[nodiscard]]
bool start();
void stop();
private:
struct Endpoint;
struct Impl;
void register_raw_endpoint(
std::string endpoint_name,
std::string subject,
std::function<std::string(std::span<const std::uint8_t>)> handler);
static cvmmap_streamer::proto::RpcCode to_proto_rpc_code(RpcErrorCode code);
std::unique_ptr<Impl> impl_;
};
} // namespace cvmmap_streamer::protocol
@@ -0,0 +1,36 @@
#pragma once
#include <string>
#include <string_view>
namespace cvmmap_streamer::protocol {
inline std::string streamer_subject_prefix(std::string_view target_key) {
return std::string("cvmmap.") + std::string(target_key) + ".streamer";
}
inline std::string subject_recorder_mp4_start(std::string_view target_key) {
return streamer_subject_prefix(target_key) + ".recorder.mp4.start";
}
inline std::string subject_recorder_mp4_stop(std::string_view target_key) {
return streamer_subject_prefix(target_key) + ".recorder.mp4.stop";
}
inline std::string subject_recorder_mp4_status(std::string_view target_key) {
return streamer_subject_prefix(target_key) + ".recorder.mp4.status";
}
inline std::string subject_recorder_mcap_start(std::string_view target_key) {
return streamer_subject_prefix(target_key) + ".recorder.mcap.start";
}
inline std::string subject_recorder_mcap_stop(std::string_view target_key) {
return streamer_subject_prefix(target_key) + ".recorder.mcap.stop";
}
inline std::string subject_recorder_mcap_status(std::string_view target_key) {
return streamer_subject_prefix(target_key) + ".recorder.mcap.status";
}
} // namespace cvmmap_streamer::protocol
@@ -0,0 +1,70 @@
#pragma once
#include "cvmmap_streamer/config/runtime_config.hpp"
#include <cstddef>
#include <cstdint>
#include <expected>
#include <filesystem>
#include <memory>
#include <string_view>
namespace cvmmap_streamer::record {
inline constexpr int kDefaultMp4Quality = 23;
enum class Mp4InputPixelFormat : std::uint8_t {
Bgr24,
Rgb24,
Bgra32,
Rgba32,
Gray8,
};
struct Mp4EncodeTuning {
int quality{kDefaultMp4Quality};
std::uint32_t gop{30};
std::uint32_t b_frames{0};
};
[[nodiscard]]
std::string_view input_pixel_format_name(Mp4InputPixelFormat pixel_format);
class Mp4RecordWriter {
public:
Mp4RecordWriter();
Mp4RecordWriter(const Mp4RecordWriter &) = delete;
Mp4RecordWriter &operator=(const Mp4RecordWriter &) = delete;
Mp4RecordWriter(Mp4RecordWriter &&) noexcept;
Mp4RecordWriter &operator=(Mp4RecordWriter &&) noexcept;
~Mp4RecordWriter();
[[nodiscard]]
std::expected<void, std::string> open(
const std::filesystem::path &output_path,
CodecType codec,
EncoderDeviceType encoder_device,
std::uint32_t width,
std::uint32_t height,
float fps,
const Mp4EncodeTuning &tuning,
Mp4InputPixelFormat input_pixel_format);
[[nodiscard]]
std::expected<void, std::string> write_frame(
const std::uint8_t *data,
std::size_t row_stride_bytes,
std::uint64_t relative_timestamp_ns);
[[nodiscard]]
std::expected<void, std::string> flush();
[[nodiscard]]
bool using_hardware() const;
private:
struct Impl;
std::unique_ptr<Impl> impl_{};
};
} // namespace cvmmap_streamer::record