VLink  2.0.0
A high-performance communication middleware
bag_reader.h
Go to the documentation of this file.
1 /*
2  * Copyright (C) 2026 by Thun Lu. All rights reserved.
3  * Author: Thun Lu <thun.lu@zohomail.cn>
4  * Repo: https://github.com/thun-res/vlink
5  * _ __ __ _ __
6  * | | / / / / (_) ____ / /__
7  * | | / / / / / / / __ \ / //_/
8  * | |/ / / /___ / / / / / / / ,<
9  * |___/ /_____/ /_/ /_/ /_/ /_/|_|
10  *
11  * Licensed under the Apache License, Version 2.0 (the "License");
12  * you may not use this file except in compliance with the License.
13  * You may obtain a copy of the License at
14  *
15  * http://www.apache.org/licenses/LICENSE-2.0
16  *
17  * Unless required by applicable law or agreed to in writing, software
18  * distributed under the License is distributed on an "AS IS" BASIS,
19  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20  * See the License for the specific language governing permissions and
21  * limitations under the License.
22  */
23 
24 /**
25  * @file bag_reader.h
26  * @brief Abstract player for VLink bag recordings with seek, loop and rate control.
27  *
28  * @details
29  * @c BagReader is the polymorphic base for VLink offline log playback. It owns a private
30  * @c MessageLoop thread, opens a bag file produced by @c BagWriter, and replays the stored
31  * messages back to user-supplied callbacks honouring their original timing. Concrete
32  * subclasses provide format-specific I/O: @c VDBReader for the SQLite-backed @c .vdb
33  * container and @c VCAPReader for the MCAP-based @c .vcap container. @c create() chooses
34  * the right one from the file suffix.
35  *
36  * Supported formats and behaviour summary:
37  *
38  * | Suffix | Concrete reader | Storage | Index source |
39  * | ------------------- | --------------- | ---------- | ------------------------------- |
40  * | @c .vdb / @c .vdbx | @c VDBReader | SQLite | SQLite tables (elapsed/URL) |
41  * | @c .vcap / @c .vcapx| @c VCAPReader | MCAP | MCAP summary + chunk index |
42  *
43  * Internal playback state machine:
44  *
45  * @verbatim
46  * play(cfg) pause()
47  * +------------+ -------> +-----------+ -------> +----------+
48  * | kStopped | | kPlaying | | kPaused |
49  * +------------+ <------- +-----------+ <------- +----------+
50  * stop() / resume() / pause_to_next()
51  * end-of-bag
52  * @endverbatim
53  *
54  * Playback features:
55  * - Rate multiplier, loop count and time-window filtering through @c Config.
56  * - @c jump() seeks to an arbitrary recording timestamp and may force-resume playback.
57  * - URL whitelist via @c Config::filter_urls applies after plugin URL remapping.
58  * - @c check() / @c reindex() / @c fix() run asynchronously and report their outcome
59  * through @c std::future<bool>.
60  * - A bound @c BagPluginInterface may rename URLs, override serialisation types
61  * and intercept individual replayed messages.
62  *
63  * @par Example
64  * @code
65  * auto reader = vlink::BagReader::create("/data/drive_log.vdb");
66  * reader->register_ready_callback([] { VLOG_I("bag ready"); });
67  * reader->register_output_callback([](const vlink::Frame& frame) {
68  * // replay each frame in real-time order
69  * VLOG_I("us=", frame.timestamp, " url=", frame.url, " bytes=", frame.data.size());
70  * });
71  * reader->async_run();
72  *
73  * vlink::BagReader::Config cfg;
74  * cfg.rate = 2.0; // play at 2x speed
75  * cfg.times = vlink::BagReader::kInfinite; // loop forever
76  * reader->play(cfg);
77  * @endcode
78  *
79  * @note Always call @c async_run() before @c play(); the loop thread must be alive to
80  * dispatch frames. Output callback timestamps are in microseconds, while
81  * @c Config::begin_time and @c Config::end_time are expressed in milliseconds.
82  */
83 
84 #pragma once
85 
86 #include <cstdint>
87 #include <future>
88 #include <memory>
89 #include <string>
90 #include <string_view>
91 #include <unordered_map>
92 #include <unordered_set>
93 #include <vector>
94 
95 #include "../base/functional.h"
96 #include "../base/macros.h"
97 #include "../base/message_loop.h"
98 #include "../impl/types.h"
99 #include "./bag_plugin_interface.h"
100 
101 namespace vlink {
102 
103 /**
104  * @class BagReader
105  * @brief Format-agnostic VLink bag player driven by an internal @c MessageLoop.
106  *
107  * @details
108  * Inherits @c MessageLoop so playback runs on a dedicated worker thread. Construction
109  * opens the target file and parses its index, but no frames are emitted until
110  * @c async_run() starts the loop and @c play() supplies a @c Config. All virtual
111  * operations are implemented by @c VDBReader and @c VCAPReader; the base class only
112  * carries shared plumbing (callback storage, plugin binding, URL filtering helpers).
113  */
115  public:
116  /**
117  * @brief Sentinel for @c Config::times that requests endless loop playback.
118  */
119  static constexpr int kInfinite{-1};
120 
121  /**
122  * @brief Coarse playback state observable through @c get_status().
123  *
124  * | Value | Meaning |
125  * | --------- | ------------------------------------------------------------ |
126  * | kStopped | No active session; position is reset to the start of the bag |
127  * | kPaused | A play session is open but the dispatcher is suspended |
128  * | kPlaying | The dispatcher is actively delivering frames |
129  */
130  enum Status : uint8_t {
131  kStopped = 0, ///< Idle; no playback in progress.
132  kPaused = 1, ///< Playback temporarily suspended; position retained.
133  kPlaying = 2, ///< Actively forwarding frames to the output callback.
134  };
135 
136  /**
137  * @struct Info
138  * @brief Aggregated metadata extracted from the bag header, summary and URL index.
139  *
140  * @details
141  * Populated when the reader opens the file and is stable thereafter unless a
142  * destructive operation such as @c reindex() or @c fix() rewrites the index.
143  */
144  struct Info final {
145  /**
146  * @struct UrlMeta
147  * @brief Per-URL accounting entry recorded inside @c Info::url_metas.
148  */
149  struct VLINK_EXPORT UrlMeta final {
150  bool valid{false}; ///< True when the entry is fully populated.
151  int index{0}; ///< Bag-local numeric URL identifier.
152  std::string url; ///< Full VLink URL string.
153  std::string url_type; ///< Communication model: Event / Method / Field.
154  ActionType action_type{ActionType::kUnknownAction}; ///< Stored action when known.
155  std::string ser_type; ///< Serialisation type name.
156  SchemaType schema_type{SchemaType::kUnknown}; ///< Coarse schema family for this URL.
157  size_t count{0}; ///< Number of recorded messages.
158  size_t size{0}; ///< Total stored bytes (compressed when applicable).
159  double freq{0}; ///< Average publication frequency in Hertz.
160  double loss{0}; ///< Declared loss ratio in the range [0, 1].
161 
162  /**
163  * @brief Defines a stable ordering between two URL metadata entries.
164  *
165  * @details
166  * Sort key is the URL transport priority first, then the URL string itself and
167  * finally the numeric index as a deterministic tie-breaker.
168  *
169  * @param target Right-hand operand.
170  * @return @c true when @c *this should appear before @p target.
171  */
172  bool operator<(const UrlMeta& target) const noexcept;
173  };
174 
175  std::string file_name; ///< Absolute path to the opened bag file.
176  std::string tag_name; ///< Free-form tag persisted in the header.
177  std::string version; ///< Bag format version string.
178  std::string storage_type; ///< Storage backend label (e.g. @c "sqlite", @c "mcap").
179  std::string compression_type; ///< Default compression codec applied to payloads.
180  std::string time_accuracy; ///< Timestamp resolution token (e.g. @c "us", @c "ns").
181  std::string process_name; ///< Name of the recording process.
182  std::string date_time; ///< Human-readable recording start date and time.
183  bool has_completed{false}; ///< True when the recording was cleanly finalised.
184  bool has_idx_elapsed{false}; ///< True when an elapsed-time index is present.
185  bool has_idx_url{false}; ///< True when a URL index is present.
186  bool has_schema{false}; ///< True when at least one embedded schema is available.
187  int32_t timezone{0}; ///< Recording timezone offset in minutes from UTC.
188  int64_t start_timestamp{0}; ///< Wall-clock recording start (milliseconds since epoch).
189  int64_t blank_duration{0}; ///< Cumulative silent-gap duration in milliseconds.
190  int64_t total_duration{0}; ///< Total recording duration in milliseconds.
191  int64_t file_size{0}; ///< On-disk file size in bytes.
192  int64_t total_raw_size{0}; ///< Sum of uncompressed payload bytes.
193  int64_t message_count{0}; ///< Total recorded message count across every URL.
194  int64_t split_count{0}; ///< Number of split files (0 for a single-file bag).
195  int64_t split_by_size{0}; ///< Split threshold in bytes when split mode is active.
196  int64_t split_by_time{0}; ///< Split threshold in milliseconds when split mode is active.
197  std::vector<UrlMeta> url_metas; ///< One entry per recorded URL.
198  };
199 
200  /**
201  * @struct Config
202  * @brief Playback parameters consumed by @c play().
203  */
204  struct Config final {
205  int64_t begin_time{0}; ///< Playback window start in milliseconds (0 means file start).
206  int64_t end_time{0}; ///< Playback window end in milliseconds (0 means file end).
207  int times{1}; ///< Loop count; values <= 0 request endless loop playback.
208  double rate{1.0}; ///< Speed multiplier relative to the recorded clock.
209  bool skip_blank{false}; ///< When true, collapses long silent gaps between frames.
210  int64_t force_delay{-1}; ///< >0 fixed delay (ms), 0 no delay, <0 use recorded timing.
211  bool auto_pause{false}; ///< When true, pauses automatically after every emitted frame.
212  bool auto_quit{false}; ///< When true, stops the loop thread at the end of playback.
213  std::unordered_set<std::string> filter_urls; ///< Whitelist of playback URLs; empty means all URLs pass.
214  };
215 
216  /**
217  * @brief Callback signature receiving one replayed @c Frame.
218  *
219  * @details
220  * Invoked on the reader's @c MessageLoop thread. @c Frame::timestamp is relative to the recording
221  * start (microseconds), @c url is the playback URL, and @c data is a shallow view valid only for the
222  * duration of the call -- copy it if it must outlive the callback. @c Frame::ser_type /
223  * @c schema_type are filled by the reader from the bag's URL metadata, so the frame is fully
224  * populated (no separate @c get_ser_type() / @c get_schema_type() lookup is required).
225  *
226  * @note Multiply @c Config::begin_time and @c Config::end_time by 1000 before comparing them
227  * against @c Frame::timestamp.
228  */
230 
231  /**
232  * @brief Callback fired on every transition of @c Status.
233  *
234  * @param status The new playback state.
235  */
236  using StatusCallback = MoveFunction<void(Status status)>;
237 
238  /**
239  * @brief Callback fired once after the bag has been opened and indexed.
240  */
241  using ReadyCallback = MoveFunction<void()>;
242 
243  /**
244  * @brief Callback fired when the current play session ends.
245  *
246  * @param is_interrupted True if termination was caused by @c stop(); false on natural end.
247  */
248  using FinishCallback = MoveFunction<void(bool is_interrupted)>;
249 
250  /**
251  * @brief Builds the concrete reader matching the extension of @p path.
252  *
253  * @details
254  * Suffix dispatch: @c .vdb / @c .vdbx select @c VDBReader, @c .vcap / @c .vcapx select
255  * @c VCAPReader; any other suffix returns @c nullptr.
256  *
257  * @param path Bag file path on disk.
258  * @param read_only When true, opens the backend read-only and rejects mutating calls.
259  * @param try_to_fix When true, allows backends to attempt a recovery pass while opening.
260  * @return Shared pointer to the freshly built reader, or @c nullptr for an unknown suffix.
261  */
262  [[nodiscard]] static std::shared_ptr<BagReader> create(const std::string& path, bool read_only = true,
263  bool try_to_fix = false);
264 
265  /**
266  * @brief Constructs the base reader and stores construction-time options.
267  *
268  * @param path Bag file path passed to the concrete subclass.
269  * @param read_only When true, prevents the backend from acquiring write access.
270  * @param try_to_fix When true, allows the subclass to run recovery while opening.
271  */
272  explicit BagReader(const std::string& path, bool read_only = true, bool try_to_fix = false);
273 
274  /**
275  * @brief Stops the loop, closes the file and releases backend resources.
276  */
277  virtual ~BagReader(); // NOLINT(modernize-use-override)
278 
279  /**
280  * @brief Attaches a custom URL/type/message rewrite plugin to this reader.
281  *
282  * @details
283  * The plugin's @c convert_url_meta() runs once per URL discovered in the bag and may
284  * rename topics, override serialisation types or filter URLs out. Its @c on_read() hook
285  * sees every replayed frame before it reaches the user @c OutputCallback.
286  *
287  * @param plugin_interface Plugin instance, or @c nullptr to detach the current binding.
288  *
289  * @see clear_plugin_interface() for the named equivalent of passing @c nullptr.
290  */
291  virtual void bind_plugin_interface(const std::shared_ptr<BagPluginInterface>& plugin_interface);
292 
293  /**
294  * @brief Detaches the currently bound plugin, if any.
295  *
296  * @details
297  * Convenience wrapper equivalent to @c bind_plugin_interface(nullptr): flushes the bound plugin,
298  * clears its callback together with the plugin-derived URL remap / exclusion state, and drops the
299  * binding. Safe to call when no plugin is bound (in which case it is a no-op).
300  */
301  virtual void clear_plugin_interface();
302 
303  /**
304  * @brief Installs a state-change observer.
305  *
306  * @param status_callback Function invoked with the new @c Status on every transition.
307  */
308  virtual void register_status_callback(StatusCallback&& status_callback);
309 
310  /**
311  * @brief Installs the "open complete" observer.
312  *
313  * @param ready_callback Function invoked once the bag is open and the index is parsed.
314  */
315  virtual void register_ready_callback(ReadyCallback&& ready_callback);
316 
317  /**
318  * @brief Installs the "play session ended" observer.
319  *
320  * @param finish_callback Function invoked at the end of a play session.
321  */
322  virtual void register_finish_callback(FinishCallback&& finish_callback);
323 
324  /**
325  * @brief Installs the per-frame data sink.
326  *
327  * @param output_callback Function called for every replayed message.
328  */
329  virtual void register_output_callback(OutputCallback&& output_callback);
330 
331  /**
332  * @brief Opens (or rewinds) a synchronous sequential read cursor over the bag.
333  *
334  * @details
335  * The cursor is an alternative to the timed @c play() / @c register_output_callback() path: it
336  * walks every stored frame in recorded order and hands them back one at a time through
337  * @c read_next() / @c operator>>, with no inter-frame delay, no loop thread and independent of any
338  * @c async_run() / @c play() session (a dedicated backend statement / iterator is used). Only
339  * @c Config::filter_urls and the @c Config::begin_time / @c Config::end_time window are honoured;
340  * @c rate, @c times and the @c auto_* / @c skip_blank flags are ignored. Any bound
341  * @c BagPluginInterface URL remap and URL exclusions still apply, but the plugin's @c on_read()
342  * interception (which is playback-only) does not run.
343  *
344  * Calling it again rewinds the cursor and re-applies @p config, clearing the @c eof() / @c fail()
345  * state. @c read_next() and @c operator>> open a default (full, unfiltered) cursor automatically
346  * on first use, so an explicit call is only needed to apply a filter or seek window.
347  *
348  * @param config Cursor filter and time window (rate / loop / auto flags are ignored).
349  * @return @c true when the cursor is positioned and ready, @c false on open failure (sets @c fail()).
350  *
351  * @note The cursor is single-threaded; do not drive it concurrently with an active @c play()
352  * session on the same reader.
353  */
354  bool open_cursor(const Config& config);
355 
356  /**
357  * @brief Opens (or rewinds) a full, unfiltered sequential read cursor over the bag.
358  *
359  * @details
360  * Convenience overload equivalent to @c open_cursor() with a default @c Config: every stored
361  * frame is visited in recorded order.
362  *
363  * @return @c true when the cursor is positioned and ready, @c false on open failure (sets @c fail()).
364  */
365  bool open_cursor();
366 
367  /**
368  * @brief Reads the next frame in recorded order into @p out.
369  *
370  * @details
371  * Lazily calls @c open_cursor() with a default @c Config on first use. On success @p out is fully
372  * populated (the same URL remap and @c ser_type / @c schema_type fill applied to playback frames);
373  * @c out.data is a shallow view that stays valid only until the next @c read_next() / @c operator>>
374  * call -- copy it if it must outlive that.
375  *
376  * @param out Receives the next frame.
377  * @return @c true when a frame was read; @c false at end of bag (sets @c eof()) or on error (sets
378  * @c fail()).
379  */
380  bool read_next(Frame& out);
381 
382  /**
383  * @brief Stream-style alias for @c read_next(), enabling @c while (reader >> frame).
384  *
385  * @param out Receives the next frame.
386  * @return Reference to @c *this, whose @c bool conversion reflects the post-read stream state.
387  */
389 
390  /**
391  * @brief Returns whether the cursor has reached the end of the bag.
392  */
393  [[nodiscard]] bool eof() const noexcept;
394 
395  /**
396  * @brief Returns whether the last cursor operation failed (open or backend read error).
397  */
398  [[nodiscard]] bool fail() const noexcept;
399 
400  /**
401  * @brief Reports whether the cursor is still in a readable state (not at end, not failed).
402  *
403  * @details
404  * Returns @c true while a frame can still be read, so @c while (reader >> frame) stops at end of
405  * bag or on error.
406  */
407  explicit operator bool() const noexcept;
408 
409  /**
410  * @brief Starts (or restarts) a play session with the supplied configuration.
411  *
412  * @details
413  * Transitions the reader into @c kPlaying. Requires that @c async_run() has already
414  * been called so that the loop thread can dispatch frames.
415  *
416  * @param config Playback window, rate, loop count and URL filter.
417  */
418  virtual void play(const Config& config) = 0;
419 
420  /**
421  * @brief Aborts the active session and rewinds to the start of the bag.
422  *
423  * @details
424  * Drives the reader into @c kStopped and invokes the @c FinishCallback with
425  * @c is_interrupted set to true.
426  */
427  virtual void stop() = 0;
428 
429  /**
430  * @brief Suspends frame dispatch while preserving the current position.
431  */
432  virtual void pause() = 0;
433 
434  /**
435  * @brief Resumes dispatch from the paused position.
436  */
437  virtual void resume() = 0;
438 
439  /**
440  * @brief Emits exactly one frame from the paused position, then pauses again.
441  */
442  virtual void pause_to_next() = 0;
443 
444  /**
445  * @brief Seeks playback to @p begin_time and applies updated rate and loop settings.
446  *
447  * @param begin_time Target recording timestamp in milliseconds.
448  * @param rate New playback speed multiplier.
449  * @param times Loop count to apply after the seek.
450  * @param force_to_play When true, transitions to @c kPlaying even if currently paused.
451  */
452  virtual void jump(int64_t begin_time, double rate, int times, bool force_to_play = false) = 0;
453 
454  /**
455  * @brief Runs an asynchronous integrity verification pass.
456  *
457  * @return Future resolving to @c true when the bag is structurally intact.
458  */
459  virtual std::future<bool> check() = 0;
460 
461  /**
462  * @brief Rebuilds backend index tables in the background where supported.
463  *
464  * @return Future resolving to @c true on success.
465  */
466  virtual std::future<bool> reindex() = 0;
467 
468  /**
469  * @brief Attempts to recover a corrupted bag in the background where supported.
470  *
471  * @param rebuild When true, also forces a full index rebuild.
472  * @return Future resolving to @c true when recovery succeeded.
473  */
474  virtual std::future<bool> fix(bool rebuild = false) = 0;
475 
476  /**
477  * @brief Overwrites the human-readable tag stored in the bag header.
478  *
479  * @param tag_name New tag value.
480  */
481  virtual void tag(const std::string& tag_name) = 0;
482 
483  /**
484  * @brief Returns the timestamp targeted by the playback cursor.
485  *
486  * @return Current playback time in milliseconds, relative to the recording start.
487  */
488  [[nodiscard]] virtual int64_t get_timestamp() const = 0;
489 
490  /**
491  * @brief Returns the timestamp of the most recently emitted frame.
492  *
493  * @return Real delivered timestamp in milliseconds, or 0 when no frame is in flight.
494  */
495  [[nodiscard]] virtual int64_t get_real_timestamp() const = 0;
496 
497  /**
498  * @brief Returns the current playback state.
499  *
500  * @return One of @c kStopped, @c kPaused or @c kPlaying.
501  */
502  [[nodiscard]] virtual Status get_status() const = 0;
503 
504  /**
505  * @brief Returns the cached header/summary metadata.
506  *
507  * @return Constant reference to the @c Info populated at open time.
508  */
509  [[nodiscard]] virtual const Info& get_info() const = 0;
510 
511  /**
512  * @brief Scans the bag and collects every embedded schema descriptor.
513  *
514  * @return Vector of @c SchemaData entries.
515  */
516  [[nodiscard]] virtual std::vector<SchemaData> detect_schema() = 0;
517 
518  /**
519  * @brief Resolves the serialisation type associated with @p url.
520  *
521  * @param url Fully-qualified URL to look up.
522  * @return Stored serialisation type, or an empty string when @p url is unknown.
523  */
524  [[nodiscard]] virtual std::string get_ser_type(const std::string& url) const;
525 
526  /**
527  * @brief Resolves the schema family associated with @p url.
528  *
529  * @param url Fully-qualified URL to look up.
530  * @return Coarse @c SchemaType, or @c SchemaType::kUnknown when unavailable.
531  */
532  [[nodiscard]] virtual SchemaType get_schema_type(const std::string& url) const;
533 
534  /**
535  * @brief Returns whether the opened bag spans multiple split files.
536  */
537  [[nodiscard]] virtual bool is_split_mode() const = 0;
538 
539  /**
540  * @brief Returns the zero-based index of the split file currently being consumed.
541  *
542  * @return Active split index, or 0 for a single-file bag.
543  */
544  [[nodiscard]] virtual int get_split_index() const = 0;
545 
546  /**
547  * @brief Returns whether a @c jump() seek is still in progress.
548  */
549  [[nodiscard]] virtual bool is_jumping() const = 0;
550 
551  protected:
552  virtual bool do_open_cursor(const Config& config);
553 
554  virtual bool do_read_next(Frame& out, bool& is_error);
555 
556  static void rebuild_url_meta_maps(const std::vector<Info::UrlMeta>& url_metas,
557  std::unordered_map<std::string, std::string>& ser_map,
558  std::unordered_map<std::string, SchemaType>& schema_type_map);
559 
560  void process_output(Frame& frame);
561 
562  void fill_frame_meta(Frame& frame) const;
563 
564  void flush_plugin();
565 
566  void process_url_metas(std::vector<Info::UrlMeta>& url_metas);
567 
568  void rebuild_url_meta_lookup(const std::vector<Info::UrlMeta>& url_metas);
569 
570  std::unordered_map<std::string, std::string>& url_ser_map();
571 
572  std::unordered_map<std::string, SchemaType>& url_schema_type_map();
573 
574  bool convert_playback_url(const std::string& input_url, std::string& output_url) const;
575 
576  bool match_playback_url_filter(std::string_view input_url, const std::unordered_set<std::string>& filter_urls) const;
577 
578  static ActionType convert_action(std::string_view str);
579 
580  void detach_plugin();
581 
582  private:
583  struct Impl;
584  std::unique_ptr<Impl> impl_;
585 
587 };
588 
589 } // namespace vlink
Unified plugin contract for rewriting bag traffic on both playback (read) and recording (write).
#define VLINK_EXPORT
Definition: macros.h:81
#define VLINK_DISALLOW_COPY_AND_ASSIGN(classname)
Deletes the copy constructor and copy-assignment operator of classname.
Definition: macros.h:174