feat: add shared TTY progress bars for ZED offline tools

Extract the tqdm-like stderr progress bar into a shared helper and reuse it across zed_svo_to_mp4, zed_svo_grid_to_mp4, and zed_svo_to_mcap.

For zed_svo_to_mcap, single-source exports now report exact frame totals and bundled multi-camera exports report exact synced-group totals on TTY. When bundled mode runs without --end-frame, the tool performs a counting pass first so the progress total remains exact instead of estimated.

Also document the bundled MCAP progress behavior in the README and record the current third_party dependency state in third_party/README. That note now makes it explicit that CLI11 and proxy are the active submodules, while tomlplusplus and mcap are vendored source drops, and adds a TODO to revisit converting mcap into a submodule later.

Verified with the Debug build during implementation, including single-camera and bundled zed_svo_to_mcap runs that rendered clean progress output to a TTY.
This commit is contained in:
2026-03-20 10:19:19 +00:00
parent 039379f5fe
commit 1369f5235d
10 changed files with 307 additions and 135 deletions
+121
View File
@@ -0,0 +1,121 @@
#include "cvmmap_streamer/tools/zed_progress_bar.hpp"
#include <chrono>
#include <cmath>
#include <cstdio>
#include <string>
#include <unistd.h>
namespace cvmmap_streamer::zed_tools {
namespace {
[[nodiscard]]
std::string format_duration(const double seconds_raw) {
const auto seconds = seconds_raw > 0.0 ? static_cast<long long>(std::llround(seconds_raw)) : 0ll;
const auto hours = seconds / 3600;
const auto minutes = (seconds % 3600) / 60;
const auto secs = seconds % 60;
char buffer[32]{};
if (hours > 0) {
std::snprintf(buffer, sizeof(buffer), "%02lld:%02lld:%02lld", hours, minutes, secs);
} else {
std::snprintf(buffer, sizeof(buffer), "%02lld:%02lld", minutes, secs);
}
return std::string(buffer);
}
} // namespace
bool stderr_supports_progress_bar() {
return ::isatty(STDERR_FILENO) == 1;
}
struct ProgressBar::Impl {
using Clock = std::chrono::steady_clock;
explicit Impl(const std::uint64_t total_frames_arg)
: total_frames(total_frames_arg),
enabled(stderr_supports_progress_bar()),
started_at(Clock::now()),
last_render_at(started_at) {}
void render(const std::uint64_t completed_frames, const bool force) {
if (!enabled || total_frames == 0) {
return;
}
const auto now = Clock::now();
if (!force && rendered && now - last_render_at < std::chrono::milliseconds(125)) {
return;
}
last_render_at = now;
rendered = true;
const auto bounded_completed = completed_frames > total_frames ? total_frames : completed_frames;
const double ratio = static_cast<double>(bounded_completed) / static_cast<double>(total_frames);
const auto filled = static_cast<std::size_t>(std::llround(ratio * 24.0));
std::string bar{};
bar.reserve(24);
for (std::size_t index = 0; index < 24; ++index) {
bar.push_back(index < filled ? '#' : '-');
}
const auto elapsed_seconds = std::chrono::duration<double>(now - started_at).count();
const auto fps = elapsed_seconds > 0.0 ? static_cast<double>(bounded_completed) / elapsed_seconds : 0.0;
const auto eta_seconds = fps > 0.0 ? static_cast<double>(total_frames - bounded_completed) / fps : 0.0;
char line[256]{};
std::snprintf(
line,
sizeof(line),
"\r[%s] %6.2f%% %llu/%llu | %5.1f fps | %s elapsed | %s ETA\x1b[K",
bar.c_str(),
ratio * 100.0,
static_cast<unsigned long long>(bounded_completed),
static_cast<unsigned long long>(total_frames),
fps,
format_duration(elapsed_seconds).c_str(),
format_duration(eta_seconds).c_str());
std::fprintf(stderr, "%s", line);
std::fflush(stderr);
}
std::uint64_t total_frames{0};
bool enabled{false};
bool rendered{false};
Clock::time_point started_at{};
Clock::time_point last_render_at{};
};
ProgressBar::ProgressBar(const std::uint64_t total_frames)
: impl_(std::make_unique<Impl>(total_frames)) {}
ProgressBar::~ProgressBar() = default;
bool ProgressBar::enabled() const {
return impl_ != nullptr && impl_->enabled;
}
void ProgressBar::update(const std::uint64_t completed_frames) {
impl_->render(completed_frames, false);
}
void ProgressBar::finish(const std::uint64_t completed_frames, const bool success) {
if (impl_ == nullptr || !impl_->enabled) {
return;
}
if (!(success && impl_->rendered && completed_frames >= impl_->total_frames)) {
impl_->render(completed_frames, true);
if (!impl_->rendered) {
return;
}
}
std::fprintf(stderr, "%s", success ? "\n" : " [failed]\n");
std::fflush(stderr);
}
} // namespace cvmmap_streamer::zed_tools
+1
View File
@@ -6,6 +6,7 @@
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include "cvmmap_streamer/tools/zed_progress_bar.hpp"
#include "cvmmap_streamer/tools/zed_svo_mp4_support.hpp"
#include <algorithm>
-100
View File
@@ -11,17 +11,13 @@ extern "C" {
#include <libswscale/swscale.h>
}
#include <chrono>
#include <cmath>
#include <cstdio>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include <unistd.h>
namespace cvmmap_streamer::zed_tools {
namespace {
@@ -68,22 +64,6 @@ AVRational frame_rate_rational(const float fps) {
return AVRational{scaled, 1000};
}
[[nodiscard]]
std::string format_duration(const double seconds_raw) {
const auto seconds = seconds_raw > 0.0 ? static_cast<long long>(std::llround(seconds_raw)) : 0ll;
const auto hours = seconds / 3600;
const auto minutes = (seconds % 3600) / 60;
const auto secs = seconds % 60;
char buffer[32]{};
if (hours > 0) {
std::snprintf(buffer, sizeof(buffer), "%02lld:%02lld:%02lld", hours, minutes, secs);
} else {
std::snprintf(buffer, sizeof(buffer), "%02lld:%02lld", minutes, secs);
}
return std::string(buffer);
}
[[nodiscard]]
std::vector<EncoderCandidate> encoder_candidates(const CodecType codec, const EncoderDeviceType device) {
const std::string hardware_name = codec == CodecType::H265 ? "hevc_nvenc" : "h264_nvenc";
@@ -320,63 +300,6 @@ std::expected<OpenedEncoder, std::string> open_encoder(
} // namespace
struct ProgressBar::Impl {
using Clock = std::chrono::steady_clock;
explicit Impl(const std::uint64_t total_frames_arg)
: total_frames(total_frames_arg),
enabled(::isatty(STDERR_FILENO) == 1),
started_at(Clock::now()),
last_render_at(started_at) {}
void render(const std::uint64_t completed_frames, const bool force) {
if (!enabled || total_frames == 0) {
return;
}
const auto now = Clock::now();
if (!force && rendered && now - last_render_at < std::chrono::milliseconds(125)) {
return;
}
last_render_at = now;
rendered = true;
const auto bounded_completed = completed_frames > total_frames ? total_frames : completed_frames;
const double ratio = static_cast<double>(bounded_completed) / static_cast<double>(total_frames);
const auto filled = static_cast<std::size_t>(std::llround(ratio * 24.0));
std::string bar{};
bar.reserve(24);
for (std::size_t i = 0; i < 24; ++i) {
bar.push_back(i < filled ? '#' : '-');
}
const auto elapsed_seconds = std::chrono::duration<double>(now - started_at).count();
const auto fps = elapsed_seconds > 0.0 ? static_cast<double>(bounded_completed) / elapsed_seconds : 0.0;
const auto eta_seconds = fps > 0.0 ? static_cast<double>(total_frames - bounded_completed) / fps : 0.0;
char line[256]{};
std::snprintf(
line,
sizeof(line),
"\r[%s] %6.2f%% %llu/%llu | %5.1f fps | %s elapsed | %s ETA\x1b[K",
bar.c_str(),
ratio * 100.0,
static_cast<unsigned long long>(bounded_completed),
static_cast<unsigned long long>(total_frames),
fps,
format_duration(elapsed_seconds).c_str(),
format_duration(eta_seconds).c_str());
std::fprintf(stderr, "%s", line);
std::fflush(stderr);
}
std::uint64_t total_frames{0};
bool enabled{false};
bool rendered{false};
Clock::time_point started_at{};
Clock::time_point last_render_at{};
};
struct Mp4Writer::Impl {
[[nodiscard]]
std::expected<void, std::string> open(
@@ -726,29 +649,6 @@ std::filesystem::path derive_output_path(const std::filesystem::path &input_path
return output_path;
}
ProgressBar::ProgressBar(const std::uint64_t total_frames)
: impl_(std::make_unique<Impl>(total_frames)) {}
ProgressBar::~ProgressBar() = default;
void ProgressBar::update(const std::uint64_t completed_frames) {
impl_->render(completed_frames, false);
}
void ProgressBar::finish(const std::uint64_t completed_frames, const bool success) {
if (impl_ == nullptr || !impl_->enabled) {
return;
}
impl_->render(completed_frames, true);
if (!impl_->rendered) {
return;
}
std::fprintf(stderr, "%s", success ? "\n" : " [failed]\n");
std::fflush(stderr);
}
Mp4Writer::Mp4Writer()
: impl_(std::make_unique<Impl>()) {}
+138 -22
View File
@@ -9,6 +9,7 @@
#include "cvmmap_streamer/encode/encoder_backend.hpp"
#include "cvmmap_streamer/ipc/contracts.hpp"
#include "cvmmap_streamer/record/mcap_record_sink.hpp"
#include "cvmmap_streamer/tools/zed_progress_bar.hpp"
#include <algorithm>
#include <array>
@@ -17,6 +18,7 @@
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <expected>
#include <filesystem>
@@ -32,6 +34,9 @@
namespace {
using cvmmap_streamer::zed_tools::ProgressBar;
using cvmmap_streamer::zed_tools::stderr_supports_progress_bar;
volatile std::sig_atomic_t g_signal_count = 0;
volatile std::sig_atomic_t g_last_signal = 0;
@@ -922,7 +927,8 @@ std::expected<void, std::string> register_mcap_streams(
[[nodiscard]]
std::expected<void, std::string> sync_streams_to_timestamp(
std::vector<CameraStream> &streams,
const std::uint64_t effective_start_ts) {
const std::uint64_t effective_start_ts,
const bool log_sync_info = true) {
bool shutdown_logged{false};
for (auto &stream : streams) {
if (log_shutdown_request(shutdown_logged, "multi-camera sync")) {
@@ -962,14 +968,16 @@ std::expected<void, std::string> sync_streams_to_timestamp(
}
}
spdlog::info(
"ZED_SVO_MCAP_SYNC input={} label={} sync_position={} first_timestamp_ns={} current_timestamp_ns={} next_timestamp_ns={}",
stream.source.path.string(),
stream.source.label,
stream.sync_position,
stream.first_timestamp_ns,
stream.current_timestamp_ns,
stream.has_next ? stream.next_timestamp_ns : 0);
if (log_sync_info) {
spdlog::info(
"ZED_SVO_MCAP_SYNC input={} label={} sync_position={} first_timestamp_ns={} current_timestamp_ns={} next_timestamp_ns={}",
stream.source.path.string(),
stream.source.label,
stream.sync_position,
stream.first_timestamp_ns,
stream.current_timestamp_ns,
stream.has_next ? stream.next_timestamp_ns : 0);
}
}
return {};
}
@@ -1146,6 +1154,58 @@ std::expected<void, std::string> advance_after_emit(std::vector<CameraStream> &s
return {};
}
[[nodiscard]]
std::expected<std::uint64_t, std::string> count_synced_groups(
std::vector<CameraStream> &streams,
const std::uint64_t tolerance_ns,
const std::uint64_t common_end_ts,
const std::optional<std::uint64_t> max_groups) {
bool shutdown_logged{false};
std::uint64_t counted_groups{0};
while (true) {
if (log_shutdown_request(shutdown_logged, "multi-camera progress scan")) {
return std::unexpected("interrupted");
}
auto group_timestamp = next_synced_group_timestamp(streams, tolerance_ns, common_end_ts);
if (!group_timestamp) {
return std::unexpected(group_timestamp.error());
}
if (!*group_timestamp) {
break;
}
counted_groups += 1;
if (max_groups && counted_groups >= *max_groups) {
break;
}
auto advance = advance_after_emit(streams);
if (!advance) {
if (advance.error() == "end-of-svo") {
break;
}
return std::unexpected(advance.error());
}
}
return counted_groups;
}
void reset_streams_after_count(std::vector<CameraStream> &streams) {
for (auto &stream : streams) {
stream.current_tracking = {};
stream.next_tracking = {};
stream.current_timestamp_ns = 0;
stream.next_timestamp_ns = 0;
stream.dropped_frames = 0;
stream.sync_position = -1;
stream.has_next = false;
stream.calibration_written = false;
stream.last_tracking_state.reset();
}
}
[[nodiscard]]
int run_single_source(
const CliOptions &options,
@@ -1339,6 +1399,8 @@ int run_single_source(
? options.end_frame
: static_cast<std::uint32_t>(total_frames - 1);
const auto nominal_frame_period_ns = frame_period_ns(camera_config.fps);
const auto total_frames_to_emit = static_cast<std::uint64_t>(last_frame - options.start_frame + 1);
ProgressBar progress{total_frames_to_emit};
while (options.start_frame + emitted_frames <= last_frame) {
if (log_shutdown_request(shutdown_logged, "single-camera export")) {
@@ -1350,6 +1412,7 @@ int run_single_source(
break;
}
if (grab_status != sl::ERROR_CODE::SUCCESS) {
progress.finish(emitted_frames, false);
sink->close();
backend->shutdown();
close_camera();
@@ -1359,6 +1422,7 @@ int run_single_source(
const auto image_status = camera.retrieveImage(left_frame, sl::VIEW::LEFT_BGR, sl::MEM::CPU);
if (image_status != sl::ERROR_CODE::SUCCESS) {
progress.finish(emitted_frames, false);
sink->close();
backend->shutdown();
close_camera();
@@ -1366,6 +1430,7 @@ int run_single_source(
return exit_code(ToolExitCode::RuntimeError);
}
if (auto valid = validate_u8c3_mat(left_frame, "left image"); !valid) {
progress.finish(emitted_frames, false);
sink->close();
backend->shutdown();
close_camera();
@@ -1375,6 +1440,7 @@ int run_single_source(
const auto depth_status = camera.retrieveMeasure(depth_frame, sl::MEASURE::DEPTH_U16_MM, sl::MEM::CPU);
if (depth_status != sl::ERROR_CODE::SUCCESS) {
progress.finish(emitted_frames, false);
sink->close();
backend->shutdown();
close_camera();
@@ -1382,6 +1448,7 @@ int run_single_source(
return exit_code(ToolExitCode::RuntimeError);
}
if (auto valid = validate_u16c1_mat(depth_frame, "depth map"); !valid) {
progress.finish(emitted_frames, false);
sink->close();
backend->shutdown();
close_camera();
@@ -1410,6 +1477,7 @@ int run_single_source(
};
if (auto push = backend->push_frame(raw_video); !push) {
progress.finish(emitted_frames, false);
sink->close();
backend->shutdown();
close_camera();
@@ -1419,6 +1487,7 @@ int run_single_source(
auto drained = backend->drain();
if (!drained) {
progress.finish(emitted_frames, false);
sink->close();
backend->shutdown();
close_camera();
@@ -1426,6 +1495,7 @@ int run_single_source(
return exit_code(ToolExitCode::RuntimeError);
}
if (auto write = write_access_units(*sink, *drained); !write) {
progress.finish(emitted_frames, false);
sink->close();
backend->shutdown();
close_camera();
@@ -1445,6 +1515,7 @@ int run_single_source(
.projection_matrix = projection_matrix,
};
if (auto write = sink->write_camera_calibration(calibration); !write) {
progress.finish(emitted_frames, false);
sink->close();
backend->shutdown();
close_camera();
@@ -1457,6 +1528,7 @@ int run_single_source(
const auto depth_step_bytes = depth_frame.getStepBytes(sl::MEM::CPU);
const auto packed_depth_bytes = static_cast<std::size_t>(width) * sizeof(std::uint16_t);
if (depth_step_bytes < packed_depth_bytes) {
progress.finish(emitted_frames, false);
sink->close();
backend->shutdown();
close_camera();
@@ -1482,6 +1554,7 @@ int run_single_source(
.pixels = depth_pixels,
};
if (auto write = sink->write_depth_map_u16(depth_map); !write) {
progress.finish(emitted_frames, false);
sink->close();
backend->shutdown();
close_camera();
@@ -1519,6 +1592,7 @@ int run_single_source(
},
};
if (auto write = sink->write_pose(pose_view); !write) {
progress.finish(emitted_frames, false);
sink->close();
backend->shutdown();
close_camera();
@@ -1529,9 +1603,11 @@ int run_single_source(
}
emitted_frames += 1;
progress.update(emitted_frames);
}
if (auto flushed = flush_and_write(*sink, backend); !flushed) {
progress.finish(emitted_frames, false);
sink->close();
backend->shutdown();
close_camera();
@@ -1544,6 +1620,7 @@ int run_single_source(
close_camera();
if (interrupted) {
progress.finish(emitted_frames, false);
spdlog::warn(
"gracefully stopped after writing {} frame(s) from '{}' to '{}'",
emitted_frames,
@@ -1552,6 +1629,7 @@ int run_single_source(
return interrupted_exit_code();
}
progress.finish(emitted_frames, true);
spdlog::info(
"wrote {} frame(s) from '{}' to '{}'",
emitted_frames,
@@ -1628,21 +1706,37 @@ int run_multi_source(
common_start_ts,
common_end_ts,
tolerance_ns);
auto sink = cvmmap_streamer::record::MultiMcapRecordSink::create(output_path.string(), compression);
if (!sink) {
close_camera_streams(streams);
spdlog::error("failed to create MCAP sink: {}", sink.error());
return exit_code(ToolExitCode::RuntimeError);
}
if (auto registered = register_mcap_streams(*sink, streams, options); !registered) {
sink->close();
close_camera_streams(streams);
spdlog::error("failed to register MCAP streams: {}", registered.error());
return exit_code(ToolExitCode::RuntimeError);
const auto render_progress = stderr_supports_progress_bar();
std::uint64_t total_groups_to_emit{0};
if (render_progress) {
if (auto synced = sync_streams_to_timestamp(streams, common_start_ts, false); !synced) {
close_camera_streams(streams);
if (synced.error() == "interrupted") {
return interrupted_exit_code();
}
spdlog::error("{}", synced.error());
return exit_code(ToolExitCode::RuntimeError);
}
if (!options.has_end_frame) {
std::fprintf(stderr, "counting synced groups for exact progress...\n");
std::fflush(stderr);
}
const auto max_groups = options.has_end_frame
? std::optional<std::uint64_t>{static_cast<std::uint64_t>(options.end_frame) + 1}
: std::nullopt;
auto counted_groups = count_synced_groups(streams, tolerance_ns, common_end_ts, max_groups);
if (!counted_groups) {
close_camera_streams(streams);
if (counted_groups.error() == "interrupted") {
return interrupted_exit_code();
}
spdlog::error("failed to count synced groups: {}", counted_groups.error());
return exit_code(ToolExitCode::RuntimeError);
}
total_groups_to_emit = *counted_groups;
reset_streams_after_count(streams);
}
if (auto synced = sync_streams_to_timestamp(streams, common_start_ts); !synced) {
sink->close();
close_camera_streams(streams);
if (synced.error() == "interrupted") {
return interrupted_exit_code();
@@ -1650,6 +1744,22 @@ int run_multi_source(
spdlog::error("{}", synced.error());
return exit_code(ToolExitCode::RuntimeError);
}
ProgressBar progress{total_groups_to_emit};
auto sink = cvmmap_streamer::record::MultiMcapRecordSink::create(output_path.string(), compression);
if (!sink) {
progress.finish(0, false);
close_camera_streams(streams);
spdlog::error("failed to create MCAP sink: {}", sink.error());
return exit_code(ToolExitCode::RuntimeError);
}
if (auto registered = register_mcap_streams(*sink, streams, options); !registered) {
progress.finish(0, false);
sink->close();
close_camera_streams(streams);
spdlog::error("failed to register MCAP streams: {}", registered.error());
return exit_code(ToolExitCode::RuntimeError);
}
std::uint64_t emitted_groups{0};
while (true) {
@@ -1659,6 +1769,7 @@ int run_multi_source(
}
auto group_timestamp = next_synced_group_timestamp(streams, tolerance_ns, common_end_ts);
if (!group_timestamp) {
progress.finish(emitted_groups, false);
sink->close();
close_camera_streams(streams);
if (group_timestamp.error() == "interrupted") {
@@ -1672,12 +1783,14 @@ int run_multi_source(
}
if (auto write = encode_and_write_group(*sink, streams, options, **group_timestamp); !write) {
progress.finish(emitted_groups, false);
sink->close();
close_camera_streams(streams);
spdlog::error("{}", write.error());
return exit_code(ToolExitCode::RuntimeError);
}
emitted_groups += 1;
progress.update(emitted_groups);
if (options.has_end_frame && emitted_groups > options.end_frame) {
break;
}
@@ -1687,6 +1800,7 @@ int run_multi_source(
if (advance.error() == "end-of-svo") {
break;
}
progress.finish(emitted_groups, false);
sink->close();
close_camera_streams(streams);
spdlog::error("{}", advance.error());
@@ -1696,6 +1810,7 @@ int run_multi_source(
for (auto &stream : streams) {
if (auto flushed = flush_and_write(*sink, stream.mcap_stream_id, *stream.backend); !flushed) {
progress.finish(emitted_groups, false);
sink->close();
close_camera_streams(streams);
spdlog::error("failed to finalize encoded video for {}: {}", stream.source.label, flushed.error());
@@ -1704,6 +1819,7 @@ int run_multi_source(
}
sink->close();
progress.finish(emitted_groups, !interrupted);
for (const auto &stream : streams) {
spdlog::info(
"bundled {} dropped_frame(s) for {}",
+1
View File
@@ -3,6 +3,7 @@
#include <sl/Camera.hpp>
#include "cvmmap_streamer/tools/zed_progress_bar.hpp"
#include "cvmmap_streamer/tools/zed_svo_mp4_support.hpp"
#include <cstdint>