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