npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@heojeongbo/ts-ros2bag-replay

v0.1.0

Published

MCAP rosbag reader + timeline replay for ROS 2 — replays a .mcap as typed messages on real wall-clock time, on top of @heojeongbo/cdr-codec.

Readme

@heojeongbo/ts-ros2bag-replay

ROS 2 rosbag replay — reads MCAP or rosbag2 SQLite (.db3) files, delivers typed ROS 2 messages at real wall-clock pace, with play / pause / seek / setSpeed. Built on top of @mcap/core, sql.js, and @heojeongbo/cdr-codec.

Install

pnpm add @heojeongbo/ts-ros2bag-replay @heojeongbo/cdr-codec @heojeongbo/ts-ros2-msgs

@heojeongbo/ts-ros2-msgs is an optional peer — only required if you use builtInCodecs() to decode standard ROS 2 messages. If you only decode your own custom messages (via createCodec), you can skip it.

Quick start

import { BagPlayer, builtInCodecs } from "@heojeongbo/ts-ros2bag-replay";
import { Twist } from "@heojeongbo/ts-ros2-msgs/geometry_msgs";
// for `.db3` in Vite: tell sql.js where its WASM lives
import sqlWasmUrl from "sql.js/dist/sql-wasm.wasm?url";

// In a browser: wire up file input / drag-drop and pass the File.
// `.mcap` and `.db3` are auto-detected from magic bytes.
const player = await BagPlayer.open({
  source: file,                    // File | Blob | ArrayBuffer | Uint8Array
  codecs: await builtInCodecs(),   // 51 standard ROS 2 messages, indexed by name
  sqlJsLocateFile: () => sqlWasmUrl,  // only needed for .db3 in browser bundlers
});

console.log(player.topics);        // { name, schemaName, messageCount, hasCodec }[]
console.log(player.startTime);     // bigint, ns since epoch
console.log(player.endTime);

// Typed subscription — `msg` is inferred as `Twist`
const unsubCmd = player.subscribeWith(Twist, "/cmd_vel", (msg, ev) => {
  console.log(ev.logTime, msg.linear.x, msg.angular.z);
});

// Untyped subscription — handler gets `unknown`, you cast
const unsubScan = player.subscribe("/scan", (msg, ev) => {
  console.log(ev.logTime, msg);
});

player.play();                     // begins replay at 1× speed
player.setSpeed(2);                // 2× faster
player.seek(player.startTime + 5_000_000_000n);  // jump 5 seconds in
player.pause();

// When done
unsubCmd();
unsubScan();
player.dispose();

API

BagPlayer.open(options)

interface BagPlayerOptions {
  source: Blob | File | ArrayBuffer | Uint8Array;
  codecs?: ReadonlyMap<string, RosMessageCodec<unknown>>;   // schema name → codec
  speed?: number;                                            // default 1
  unknownSchema?: "skip" | "warn" | (info => void);          // default "warn"
  decompressHandlers?: DecompressHandlers;                   // extras for lz4 / bz2
  sqlJsLocateFile?: (filename: string) => string;            // sql.js wasm path (.db3)
  onError?: (err: Error) => void;                            // playback failure
}

Returns a Promise<BagPlayer>. Auto-detects MCAP vs SQLite from the file's magic bytes. For MCAP it reads the index (no full scan). For .db3 it loads the database into memory via sql.js and reads topics / messages rows in order.

player.topics, startTime, endTime, currentTime, speed, playing

Read-only metadata. startTime/endTime/currentTime are bigint nanoseconds-since-epoch (matching MCAP's clock).

Playback controls

| method | effect | | --- | --- | | play() | Start (or resume) replay from currentTime. No-op if already playing or at end. | | pause() | Stop replay; currentTime is preserved. | | seek(time) | Jump to time (clamped to [startTime, endTime]). If currently playing, replay continues from there. | | setSpeed(rate) | Change playback multiplier (must be positive). Active replay is restarted at the new pace. | | dispose() | Permanent shutdown. Cancels replay, drops subscribers, closes the reader. |

Subscriptions

// Pass a codec — the message type is inferred and decoded automatically.
player.subscribeWith<T>(codec: RosMessageCodec<T>, topic: string, handler);

// No codec — looks up `options.codecs` by the message's schema name.
// If not found, the handler is skipped (per the `unknownSchema` policy).
player.subscribe<T = unknown>(topic: string, handler);

Both return an unsubscribe function. Multiple subscribers per topic are fine; they each receive the decoded message.

builtInCodecs()

Returns a Promise<ReadonlyMap<string, RosMessageCodec<unknown>>> covering every codec exported by @heojeongbo/ts-ros2-msgs, keyed by canonical name (e.g. "std_msgs/Header"). Pass this as options.codecs to decode all standard ROS 2 messages.

normalizeSchemaName(name)

Helper that converts MCAP's canonical ROS 2 schema name ("std_msgs/msg/Header") into the lookup key used by codecs ("std_msgs/Header"). Idempotent; service / action names pass through.

How replay timing works

When play() is called, the player snapshots (wallStartMs, bagStartNs, speed) and uses these to map each message's logTime to a wall-clock dispatch time. For each message it computes delayMs = (logTime - bagStartNs) / 1e6 / speed - elapsedWall and sleeps that long before dispatching. pause cancels the in-flight sleep; setSpeed re-baselines so future messages use the new pace.

The MCAP reader is indexed, so seek is constant-time regardless of bag size, and only the chunk(s) needed for the current playback window are decompressed.

Limitations (v1)

  • .db3 is single-file. Modern rosbag2 (Humble+) embeds the YAML metadata inside the SQLite file's metadata table, so a bare .db3 plays. Split bags (bag_0.db3 + bag_1.db3) and external metadata.yaml-only setups are deferred to v2.

  • Built-in messages only. Schemas that aren't in your codecs map are skipped (with a one-time console warning by default). Future versions will parse the schema text inside the bag at runtime so any message decodes.

  • zstd is built-in. lz4 / bz2 need user handlers. zstd-compressed bags decompress out of the box via fzstd (pure-JS, no WASM). For lz4 / bz2 chunks, pass your own handlers:

    import { loadDecompressHandlers } from "@mcap/support";
    
    const player = await BagPlayer.open({
      source: file,
      codecs: await builtInCodecs(),
      decompressHandlers: await loadDecompressHandlers(),  // lz4 + zstd + bz2
    });

    @mcap/support ships WASM modules, so consumer bundlers may need vite-plugin-wasm or equivalent. User handlers override the built-in for the same key.

  • Read-only. No bag writer.

Acknowledgements

  • @mcap/core — Foxglove's TypeScript MCAP SDK handles the MCAP path.
  • sql.js — SQLite compiled to WebAssembly handles the .db3 path.
  • The wire-format and codec layer is @heojeongbo/cdr-codec.

License

MIT — see the LICENSE in the repository root.