feat: add mcap recorder control and cnats providers
Register an MCAP recorder service on the streamer control subjects, reuse the shared recording request and status model, and expose the zed recording preview/conversion helper. This also replaces the temporary cnats boolean with the explicit CVMMAP_CNATS_PROVIDER modes and documents the supported system and workspace build paths.
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
|
||||
#include <cvmmap/client.hpp>
|
||||
#include <cvmmap/nats_client.hpp>
|
||||
#include <cvmmap/nats_service.hpp>
|
||||
#include <cvmmap/parser.hpp>
|
||||
|
||||
#include <chrono>
|
||||
@@ -250,6 +251,219 @@ std::uint64_t body_tracking_timestamp_ns(const cvmmap::body_tracking_frame_t &fr
|
||||
return frame.header.sdk_timestamp_ns;
|
||||
}
|
||||
|
||||
struct McapRecorderState {
|
||||
mutable std::mutex mutex{};
|
||||
RuntimeConfig base_config{};
|
||||
std::optional<RuntimeConfig> active_record_config{};
|
||||
std::optional<encode::EncodedStreamInfo> current_stream_info{};
|
||||
std::optional<record::McapRecordSink> sink{};
|
||||
cvmmap::RecordingStatus status{
|
||||
.format = cvmmap::RecordingFormat::Mcap,
|
||||
.can_record = true,
|
||||
};
|
||||
};
|
||||
|
||||
[[nodiscard]]
|
||||
cvmmap::ControlError make_recording_control_error(
|
||||
const int32_t code,
|
||||
std::string message) {
|
||||
return cvmmap::ControlError{
|
||||
.code = code,
|
||||
.message = std::move(message),
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
RuntimeConfig make_mcap_record_config(
|
||||
const RuntimeConfig &base_config,
|
||||
const cvmmap::RecordingRequest &request) {
|
||||
auto record_config = base_config;
|
||||
record_config.record.mcap.enabled = true;
|
||||
record_config.record.mcap.path = request.output_path;
|
||||
if (request.mcap_options) {
|
||||
if (request.mcap_options->topic) {
|
||||
record_config.record.mcap.topic = *request.mcap_options->topic;
|
||||
}
|
||||
if (request.mcap_options->depth_topic) {
|
||||
record_config.record.mcap.depth_topic = *request.mcap_options->depth_topic;
|
||||
}
|
||||
if (request.mcap_options->body_topic) {
|
||||
record_config.record.mcap.body_topic = *request.mcap_options->body_topic;
|
||||
}
|
||||
if (request.mcap_options->frame_id) {
|
||||
record_config.record.mcap.frame_id = *request.mcap_options->frame_id;
|
||||
}
|
||||
}
|
||||
return record_config;
|
||||
}
|
||||
|
||||
void reset_mcap_status_after_stop(cvmmap::RecordingStatus &status) {
|
||||
status.is_recording = false;
|
||||
status.is_paused = false;
|
||||
status.active_path.clear();
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
std::expected<cvmmap::RecordingStatus, cvmmap::ControlError> start_mcap_recording(
|
||||
McapRecorderState &recorder_state,
|
||||
const cvmmap::RecordingRequest &request) {
|
||||
std::lock_guard lock(recorder_state.mutex);
|
||||
if (request.format != cvmmap::RecordingFormat::Mcap) {
|
||||
return std::unexpected(make_recording_control_error(
|
||||
cvmmap::CONTROL_RESPONSE_UNSUPPORTED,
|
||||
"recording format is not supported by the streamer"));
|
||||
}
|
||||
if (!recorder_state.current_stream_info) {
|
||||
return std::unexpected(make_recording_control_error(
|
||||
cvmmap::CONTROL_RESPONSE_ERROR,
|
||||
"MCAP recorder is not ready; stream info unavailable"));
|
||||
}
|
||||
if (recorder_state.sink) {
|
||||
return std::unexpected(make_recording_control_error(
|
||||
cvmmap::CONTROL_RESPONSE_ERROR,
|
||||
"MCAP recording is already active"));
|
||||
}
|
||||
|
||||
auto record_config = make_mcap_record_config(recorder_state.base_config, request);
|
||||
if (request.mcap_options && request.mcap_options->compression) {
|
||||
auto parsed = parse_mcap_compression(*request.mcap_options->compression);
|
||||
if (!parsed) {
|
||||
return std::unexpected(make_recording_control_error(
|
||||
cvmmap::CONTROL_RESPONSE_INVALID_PAYLOAD,
|
||||
parsed.error()));
|
||||
}
|
||||
record_config.record.mcap.compression = *parsed;
|
||||
}
|
||||
|
||||
auto created = record::McapRecordSink::create(record_config, *recorder_state.current_stream_info);
|
||||
if (!created) {
|
||||
return std::unexpected(make_recording_control_error(
|
||||
cvmmap::CONTROL_RESPONSE_ERROR,
|
||||
"pipeline MCAP sink init failed: " + created.error()));
|
||||
}
|
||||
|
||||
recorder_state.active_record_config = record_config;
|
||||
recorder_state.sink.emplace(std::move(*created));
|
||||
recorder_state.status.can_record = true;
|
||||
recorder_state.status.is_recording = true;
|
||||
recorder_state.status.is_paused = false;
|
||||
recorder_state.status.last_frame_ok = true;
|
||||
recorder_state.status.frames_ingested = 0;
|
||||
recorder_state.status.frames_encoded = 0;
|
||||
recorder_state.status.active_path = request.output_path;
|
||||
return recorder_state.status;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
std::expected<cvmmap::RecordingStatus, cvmmap::ControlError> stop_mcap_recording(
|
||||
McapRecorderState &recorder_state) {
|
||||
std::lock_guard lock(recorder_state.mutex);
|
||||
if (recorder_state.sink) {
|
||||
recorder_state.sink->close();
|
||||
recorder_state.sink.reset();
|
||||
}
|
||||
recorder_state.active_record_config.reset();
|
||||
reset_mcap_status_after_stop(recorder_state.status);
|
||||
return recorder_state.status;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
std::expected<cvmmap::RecordingStatus, cvmmap::ControlError> get_mcap_recording_status(
|
||||
McapRecorderState &recorder_state) {
|
||||
std::lock_guard lock(recorder_state.mutex);
|
||||
recorder_state.status.can_record = true;
|
||||
return recorder_state.status;
|
||||
}
|
||||
|
||||
void update_mcap_stream_info(
|
||||
McapRecorderState &recorder_state,
|
||||
const encode::EncodedStreamInfo &stream_info) {
|
||||
std::lock_guard lock(recorder_state.mutex);
|
||||
recorder_state.current_stream_info = stream_info;
|
||||
if (recorder_state.sink) {
|
||||
auto updated = recorder_state.sink->update_stream_info(stream_info);
|
||||
if (!updated) {
|
||||
recorder_state.status.last_frame_ok = false;
|
||||
recorder_state.status.is_recording = false;
|
||||
recorder_state.status.active_path.clear();
|
||||
recorder_state.sink->close();
|
||||
recorder_state.sink.reset();
|
||||
recorder_state.active_record_config.reset();
|
||||
spdlog::error("pipeline MCAP stream update failed: {}", updated.error());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
record::McapRecordSink *lock_mcap_sink(
|
||||
McapRecorderState *recorder_state,
|
||||
std::unique_lock<std::mutex> &lock) {
|
||||
if (recorder_state == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
lock = std::unique_lock<std::mutex>(recorder_state->mutex);
|
||||
if (!recorder_state->sink) {
|
||||
return nullptr;
|
||||
}
|
||||
return &*recorder_state->sink;
|
||||
}
|
||||
|
||||
Status write_mcap_access_unit(
|
||||
McapRecorderState *recorder_state,
|
||||
const encode::EncodedAccessUnit &access_unit) {
|
||||
std::unique_lock<std::mutex> lock{};
|
||||
auto *sink = lock_mcap_sink(recorder_state, lock);
|
||||
if (sink == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto write = sink->write_access_unit(access_unit);
|
||||
if (!write) {
|
||||
recorder_state->status.last_frame_ok = false;
|
||||
return unexpected_error(ERR_SERIALIZATION, write.error());
|
||||
}
|
||||
recorder_state->status.last_frame_ok = true;
|
||||
recorder_state->status.is_recording = true;
|
||||
recorder_state->status.frames_ingested += 1;
|
||||
recorder_state->status.frames_encoded += 1;
|
||||
return {};
|
||||
}
|
||||
|
||||
Status write_mcap_body_message(
|
||||
McapRecorderState *recorder_state,
|
||||
const record::RawBodyTrackingMessageView &body_message) {
|
||||
std::unique_lock<std::mutex> lock{};
|
||||
auto *sink = lock_mcap_sink(recorder_state, lock);
|
||||
if (sink == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto write = sink->write_body_tracking_message(body_message);
|
||||
if (!write) {
|
||||
recorder_state->status.last_frame_ok = false;
|
||||
return unexpected_error(ERR_SERIALIZATION, write.error());
|
||||
}
|
||||
recorder_state->status.last_frame_ok = true;
|
||||
return {};
|
||||
}
|
||||
|
||||
Status write_mcap_depth_map(
|
||||
McapRecorderState *recorder_state,
|
||||
const record::RawDepthMapView &depth_map) {
|
||||
std::unique_lock<std::mutex> lock{};
|
||||
auto *sink = lock_mcap_sink(recorder_state, lock);
|
||||
if (sink == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto write = sink->write_depth_map(depth_map);
|
||||
if (!write) {
|
||||
recorder_state->status.last_frame_ok = false;
|
||||
return unexpected_error(ERR_SERIALIZATION, write.error());
|
||||
}
|
||||
recorder_state->status.last_frame_ok = true;
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
Status publish_access_units(
|
||||
const RuntimeConfig &config,
|
||||
@@ -257,7 +471,7 @@ Status publish_access_units(
|
||||
PipelineStats &stats,
|
||||
protocol::UdpRtpPublisher *rtp_publisher,
|
||||
protocol::RtmpOutput *rtmp_output,
|
||||
record::McapRecordSink *mcap_sink,
|
||||
McapRecorderState *mcap_recorder,
|
||||
metrics::IngestEmitLatencyTracker &latency_tracker) {
|
||||
for (auto &access_unit : access_units) {
|
||||
if (access_unit.annexb_bytes.empty()) {
|
||||
@@ -278,10 +492,10 @@ Status publish_access_units(
|
||||
return std::unexpected(publish.error());
|
||||
}
|
||||
}
|
||||
if (mcap_sink != nullptr) {
|
||||
auto write = mcap_sink->write_access_unit(access_unit);
|
||||
if (mcap_recorder != nullptr) {
|
||||
auto write = write_mcap_access_unit(mcap_recorder, access_unit);
|
||||
if (!write) {
|
||||
return unexpected_error(ERR_SERIALIZATION, write.error());
|
||||
return std::unexpected(write.error());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,7 +522,7 @@ Status drain_encoder(
|
||||
PipelineStats &stats,
|
||||
protocol::UdpRtpPublisher *rtp_publisher,
|
||||
protocol::RtmpOutput *rtmp_output,
|
||||
record::McapRecordSink *mcap_sink,
|
||||
McapRecorderState *mcap_recorder,
|
||||
metrics::IngestEmitLatencyTracker &latency_tracker) {
|
||||
auto drained = flushing ? backend->flush() : backend->drain();
|
||||
if (!drained) {
|
||||
@@ -320,7 +534,7 @@ Status drain_encoder(
|
||||
stats,
|
||||
rtp_publisher,
|
||||
rtmp_output,
|
||||
mcap_sink,
|
||||
mcap_recorder,
|
||||
latency_tracker);
|
||||
}
|
||||
|
||||
@@ -377,10 +591,16 @@ int run_pipeline(const RuntimeConfig &config) {
|
||||
|
||||
std::optional<protocol::UdpRtpPublisher> rtp_publisher{};
|
||||
std::optional<protocol::RtmpOutput> rtmp_output{};
|
||||
std::optional<record::McapRecordSink> mcap_sink{};
|
||||
McapRecorderState mcap_recorder{};
|
||||
mcap_recorder.base_config = config;
|
||||
mcap_recorder.status.can_record = true;
|
||||
cvmmap::NatsControlClient nats_client(
|
||||
input_endpoints->nats_target_key,
|
||||
config.input.nats_url);
|
||||
cvmmap::NatsControlService recorder_service(
|
||||
config.input.uri,
|
||||
input_endpoints->nats_target_key,
|
||||
config.input.nats_url);
|
||||
std::mutex nats_event_mutex{};
|
||||
std::deque<std::vector<std::uint8_t>> pending_body_packets{};
|
||||
std::deque<int32_t> pending_status_codes{};
|
||||
@@ -389,18 +609,52 @@ int run_pipeline(const RuntimeConfig &config) {
|
||||
std::lock_guard lock(nats_event_mutex);
|
||||
pending_status_codes.push_back(status_code);
|
||||
});
|
||||
if (config.record.mcap.enabled) {
|
||||
nats_client.SetBodyTrackingRawCallback(
|
||||
[&nats_event_mutex, &pending_body_packets](std::span<const std::uint8_t> bytes) {
|
||||
std::lock_guard lock(nats_event_mutex);
|
||||
pending_body_packets.emplace_back(bytes.begin(), bytes.end());
|
||||
});
|
||||
}
|
||||
nats_client.SetBodyTrackingRawCallback(
|
||||
[&nats_event_mutex, &pending_body_packets](std::span<const std::uint8_t> bytes) {
|
||||
std::lock_guard lock(nats_event_mutex);
|
||||
pending_body_packets.emplace_back(bytes.begin(), bytes.end());
|
||||
});
|
||||
if (!nats_client.Start()) {
|
||||
spdlog::error("pipeline NATS subscribe failed on '{}'", config.input.nats_url);
|
||||
return exit_code(PipelineExitCode::SubscriberError);
|
||||
}
|
||||
|
||||
cvmmap::NatsControlHandlers recorder_handlers{};
|
||||
recorder_handlers.on_recording_available =
|
||||
[](const cvmmap::RecordingFormat format) {
|
||||
return format == cvmmap::RecordingFormat::Mcap;
|
||||
};
|
||||
recorder_handlers.on_start_recording =
|
||||
[&mcap_recorder](const cvmmap::RecordingRequest &request) {
|
||||
return start_mcap_recording(mcap_recorder, request);
|
||||
};
|
||||
recorder_handlers.on_stop_recording =
|
||||
[&mcap_recorder](const cvmmap::RecordingFormat format)
|
||||
-> std::expected<cvmmap::RecordingStatus, cvmmap::ControlError> {
|
||||
if (format != cvmmap::RecordingFormat::Mcap) {
|
||||
return std::unexpected(make_recording_control_error(
|
||||
cvmmap::CONTROL_RESPONSE_UNSUPPORTED,
|
||||
"recording format is not supported by the streamer"));
|
||||
}
|
||||
return stop_mcap_recording(mcap_recorder);
|
||||
};
|
||||
recorder_handlers.on_get_recording_status =
|
||||
[&mcap_recorder](const cvmmap::RecordingFormat format)
|
||||
-> std::expected<cvmmap::RecordingStatus, cvmmap::ControlError> {
|
||||
if (format != cvmmap::RecordingFormat::Mcap) {
|
||||
return std::unexpected(make_recording_control_error(
|
||||
cvmmap::CONTROL_RESPONSE_UNSUPPORTED,
|
||||
"recording format is not supported by the streamer"));
|
||||
}
|
||||
return get_mcap_recording_status(mcap_recorder);
|
||||
};
|
||||
recorder_service.SetHandlers(std::move(recorder_handlers));
|
||||
if (!recorder_service.Start()) {
|
||||
spdlog::error("pipeline recorder control service failed on '{}'", config.input.nats_url);
|
||||
nats_client.Stop();
|
||||
return exit_code(PipelineExitCode::SubscriberError);
|
||||
}
|
||||
|
||||
if (config.outputs.rtp.enabled) {
|
||||
auto created = protocol::UdpRtpPublisher::create(config);
|
||||
if (!created) {
|
||||
@@ -458,28 +712,29 @@ int run_pipeline(const RuntimeConfig &config) {
|
||||
}
|
||||
rtmp_output.emplace(std::move(*created));
|
||||
}
|
||||
auto stream_info = (*backend)->stream_info();
|
||||
if (!stream_info) {
|
||||
return unexpected_error(
|
||||
stream_info.error().code,
|
||||
"pipeline encoder stream info unavailable: " + format_error(stream_info.error()));
|
||||
}
|
||||
update_mcap_stream_info(mcap_recorder, *stream_info);
|
||||
if (config.record.mcap.enabled) {
|
||||
auto stream_info = (*backend)->stream_info();
|
||||
if (!stream_info) {
|
||||
return unexpected_error(
|
||||
stream_info.error().code,
|
||||
"pipeline MCAP stream info unavailable: " + format_error(stream_info.error()));
|
||||
}
|
||||
if (!mcap_sink) {
|
||||
std::lock_guard lock(mcap_recorder.mutex);
|
||||
if (!mcap_recorder.sink) {
|
||||
auto created = record::McapRecordSink::create(config, *stream_info);
|
||||
if (!created) {
|
||||
return unexpected_error(
|
||||
ERR_INTERNAL,
|
||||
"pipeline MCAP sink init failed: " + created.error());
|
||||
}
|
||||
mcap_sink.emplace(std::move(*created));
|
||||
} else {
|
||||
auto updated = mcap_sink->update_stream_info(*stream_info);
|
||||
if (!updated) {
|
||||
return unexpected_error(
|
||||
ERR_INTERNAL,
|
||||
"pipeline MCAP stream update failed: " + updated.error());
|
||||
}
|
||||
mcap_recorder.active_record_config = config;
|
||||
mcap_recorder.sink.emplace(std::move(*created));
|
||||
mcap_recorder.status.format = cvmmap::RecordingFormat::Mcap;
|
||||
mcap_recorder.status.can_record = true;
|
||||
mcap_recorder.status.is_recording = true;
|
||||
mcap_recorder.status.last_frame_ok = true;
|
||||
mcap_recorder.status.active_path = config.record.mcap.path;
|
||||
}
|
||||
}
|
||||
started = true;
|
||||
@@ -529,10 +784,6 @@ int run_pipeline(const RuntimeConfig &config) {
|
||||
}
|
||||
}
|
||||
for (const auto &body_bytes_vec : body_packets) {
|
||||
if (!mcap_sink) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto body_bytes = std::span<const std::uint8_t>(
|
||||
body_bytes_vec.data(),
|
||||
body_bytes_vec.size());
|
||||
@@ -546,12 +797,12 @@ int run_pipeline(const RuntimeConfig &config) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto write_body = mcap_sink->write_body_tracking_message(record::RawBodyTrackingMessageView{
|
||||
auto write_body = write_mcap_body_message(&mcap_recorder, record::RawBodyTrackingMessageView{
|
||||
.timestamp_ns = body_tracking_timestamp_ns(*parsed_body),
|
||||
.bytes = body_bytes,
|
||||
});
|
||||
if (!write_body) {
|
||||
const auto reason = "pipeline body MCAP write failed: " + write_body.error();
|
||||
const auto reason = "pipeline body MCAP write failed: " + format_error(write_body.error());
|
||||
restart_backend(reason, active_info);
|
||||
break;
|
||||
}
|
||||
@@ -595,7 +846,7 @@ int run_pipeline(const RuntimeConfig &config) {
|
||||
stats,
|
||||
rtp_publisher ? &*rtp_publisher : nullptr,
|
||||
rtmp_output ? &*rtmp_output : nullptr,
|
||||
mcap_sink ? &*mcap_sink : nullptr,
|
||||
&mcap_recorder,
|
||||
latency_tracker);
|
||||
if (!drain) {
|
||||
const auto reason = format_error(drain.error());
|
||||
@@ -670,7 +921,7 @@ int run_pipeline(const RuntimeConfig &config) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mcap_sink.has_value() && !snapshot->depth.empty()) {
|
||||
if (!snapshot->depth.empty()) {
|
||||
if (snapshot->depth_unit == ipc::DepthUnit::Unknown) {
|
||||
if (!warned_unknown_depth_unit) {
|
||||
spdlog::warn(
|
||||
@@ -686,9 +937,9 @@ int run_pipeline(const RuntimeConfig &config) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto write_depth = mcap_sink->write_depth_map(*depth_map);
|
||||
auto write_depth = write_mcap_depth_map(&mcap_recorder, *depth_map);
|
||||
if (!write_depth) {
|
||||
const auto reason = "pipeline depth MCAP write failed: " + write_depth.error();
|
||||
const auto reason = "pipeline depth MCAP write failed: " + format_error(write_depth.error());
|
||||
restart_backend(reason, active_info);
|
||||
continue;
|
||||
}
|
||||
@@ -702,7 +953,7 @@ int run_pipeline(const RuntimeConfig &config) {
|
||||
stats,
|
||||
rtp_publisher ? &*rtp_publisher : nullptr,
|
||||
rtmp_output ? &*rtmp_output : nullptr,
|
||||
mcap_sink ? &*mcap_sink : nullptr,
|
||||
&mcap_recorder,
|
||||
latency_tracker);
|
||||
if (!drain) {
|
||||
const auto reason = format_error(drain.error());
|
||||
@@ -730,7 +981,7 @@ int run_pipeline(const RuntimeConfig &config) {
|
||||
stats,
|
||||
rtp_publisher ? &*rtp_publisher : nullptr,
|
||||
rtmp_output ? &*rtmp_output : nullptr,
|
||||
mcap_sink ? &*mcap_sink : nullptr,
|
||||
&mcap_recorder,
|
||||
latency_tracker);
|
||||
if (!drain) {
|
||||
spdlog::error("pipeline publish failed during flush: {}", format_error(drain.error()));
|
||||
@@ -739,9 +990,8 @@ int run_pipeline(const RuntimeConfig &config) {
|
||||
}
|
||||
|
||||
(*backend)->shutdown();
|
||||
if (mcap_sink) {
|
||||
mcap_sink->close();
|
||||
}
|
||||
std::ignore = stop_mcap_recording(mcap_recorder);
|
||||
recorder_service.Stop();
|
||||
|
||||
spdlog::info(
|
||||
"PIPELINE_METRICS codec={} backend={} sync_messages={} status_messages={} torn_frames={} pushed_frames={} encoded_access_units={} resets={} format_rebuilds={} supervised_restarts={}",
|
||||
|
||||
Reference in New Issue
Block a user