@roboto-ai/mcap-codec-browser
v0.3.0
Published
Decodes one topic of an MCAP file (ROS, DDS/IDL, JSON or MessagePack messages) into Apache Arrow in the browser.
Downloads
1,136
Readme
@roboto-ai/mcap-codec-browser
Decodes one topic of an MCAP file into Apache Arrow in the browser. A WASM module reads the file
through a byte-range reader you supply: its footer and summary, then only the chunks that hold the
topic's messages within the requested row range and, for a window on log time, overlap that
window. It decodes ROS 1 messages, ROS 2 and OMG IDL messages over CDR, and JSON or MessagePack
messages described by a JSON Schema. The rows arrive as Arrow IPC streams, one per batch, each
carrying its own schema and dictionaries, so tableFromIPC decodes any one of them alone.
Each openMcapFile call opens a cursor over one topic of one file, and one decoder runs many
cursors, over many files, concurrently. The cursors share WASM module instances, each limited to
512 MiB of memory; with the default limits, one cursor reads messages of up to 64 MiB, such as
camera images and lidar point clouds.
The package ships prebuilt, untranspiled ES modules, their TypeScript declarations and the WASM module, and it runs no install script.
Install
npm install @roboto-ai/mcap-codec-browserQuick start
import { McapDecoder } from "@roboto-ai/mcap-codec-browser";
import { tableFromIPC } from "apache-arrow";
const decoder = new McapDecoder();
await using cursor = await decoder.openMcapFile({
fileSize: BigInt(size),
// Called for each byte range the cursor reads; resolve exactly `length` bytes.
readBytes: async (offset, length, signal) => {
const response = await fetch(url, {
signal,
headers: { range: `bytes=${offset}-${offset + length - 1n}` },
});
if (response.status !== 206) {
throw new Error(`range request failed with HTTP ${response.status}`);
}
return new Uint8Array(await response.arrayBuffer());
},
request: {
channel: { topicName: "/odometry" },
projection: { include: [["speed"], ["pose", "x"]] },
timestamp: { kind: "message_log_time" },
timeWindow: {
startNs: 1_700_000_000_000_000_000n,
endNs: 1_700_000_060_000_000_000n,
},
},
});
for await (const batch of cursor) {
const table = tableFromIPC(batch.ipc);
// ... use the rows ...
batch.release();
}
await decoder.close();- File sizes, byte offsets and lengths, time bounds and row numbers are
bigint. - Column 0 of every batch is the row number: the message's zero-based position among the topic's
messages, in the order the file stores them. Column 1 is the timestamp in nanoseconds, from the
source the request's
timestampnames. The projected fields follow: theprojection'sincludepaths, or every field withoutinclude, minus itsexcludepaths. A request withoutprojectiongets every top-level field, and an emptyincludegets none. - A
McapDecoderdecodes on the thread that calls it and blocks that thread for each decoding step, so create one in each worker thread that reads data and open all of that thread's cursors on it. On the page's main thread, use aDecoderPoolinstead: it opens cursors the same way but runs each module instance in a worker of its own. - A cursor holds one batch at a time: call
batch.release()before asking for the next, ornext()rejects withinvalid_state. - Dispose of a cursor you stop reading, so its module instance can take another;
await usingdisposes it when its scope ends. AnAbortSignalpassed toopenMcapFileassignalcancels the cursor when the signal aborts. - A failure rejects with a
CodecErrorwhosecodenames it, such asunknown_channel,corrupt_inputorresource_limit. The exceptions are two values you supply, which reject unchanged: an error yourreadBytesthrows and thereasonof an abortedsignal. Recognize aCodecErrorwithisCodecError(value), which you can import alone, without the wasm module or the decoders, from@roboto-ai/mcap-codec-browser/errors.
The declarations in dist/index.d.ts document every request member, decoder option, event and
error code.
Serving the package
Both decoders load dist/bindings/mcap_codec_browser_bg.wasm, and a DecoderPool also starts
dist/worker.js, each by a URL relative to the package's own modules. Without a bundler, serve the
package's files at their installed paths. A Vite production build bundles the package with Vite's
default options, worker and WASM included. Vite's dev server needs the package excluded from
dependency pre-bundling, which would move its modules away from those files:
// vite.config.js
export default { optimizeDeps: { exclude: ["@roboto-ai/mcap-codec-browser"] } };Send the WASM file as application/wasm, so the browser can compile it while it downloads.
Where the WASM file cannot be served next to the package, or under Node, whose fetch cannot load
it, compile the module yourself and pass it as the module option; the decoders then fetch
nothing. The package exports the file as @roboto-ai/mcap-codec-browser/wasm. Under Node, which
runs a McapDecoder (tested on 22.17.0) but not a DecoderPool:
import { readFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { McapDecoder } from "@roboto-ai/mcap-codec-browser";
const path = createRequire(import.meta.url).resolve(
"@roboto-ai/mcap-codec-browser/wasm",
);
const decoder = new McapDecoder({
module: await WebAssembly.compile(await readFile(path)),
});License
The package is licensed under MPL-2.0; see LICENSE. NOTICE and THIRD_PARTY_LICENSES/ cover
the third-party components compiled into the module.
