Skip to main content

Overview

TopicRecorder writes subscribed streams straight into an MCAP file. MCAP is Foxglove’s open recording format, so you can step through a finished recording in Foxglove Studio, or parse it offline with the mcap command-line tool or Python’s mcap library. Recording rides on the same subscription machinery: recorder.record(stream.subscribe()) registers a channel, and everything registered starts hitting disk once you call recorder.start(path). Both device streams and global transform streams can be recorded. Matching example: examples/python/wuji_glove/2.recording.py. For the full recording API and compression options, see Data Recording and Offline Processing. This page covers only how to run it and what the measurements show.
The C SDK has no matching recording API. This page covers Python.

Prerequisites

Hardware

This example only reads. It writes no device parameters.

Software

Before You Start

Read Subscribing With Callbacks first. Recording takes the subscription handles returned by subscribe(), and the queue semantics are identical.

Data Flow

File Structure

Recording six device streams plus two global streams for 5 seconds produces these channels: Messages use json encoding and each channel carries its own schema, so Foxglove Studio shows field names directly without a separate message-definition file. The file header’s library field reads mcap-rs-0.24.0, the SDK’s built-in Rust writer.

Compression Modes

Recording the same 3 streams for 5 seconds, about 1970 frames: JSON encoding repeats field names, which compresses well. Choose zstd for long recordings, lz4 where write latency matters, and none only when another process needs to stream the file while it’s still being written. Looking at a single lz4 chunk, 3101 bytes of raw data compress to 938.

Quality Metrics

The summary returned by handle.stop() carries a quality object:
Full fields with measured values: Three things stand out: sync_rate falls to 0.857 once tf joins. The six device streams all sit near 130 Hz and line up with each other. tf measures 872 Hz and tf_static only 1 Hz, an order of magnitude apart from the rest, so the sync check naturally fails. Mixing rate bands in one recording makes this number low by design, and it doesn’t mean the data is bad. max_sync_offset_ms can show one large outlier. The 3-stream run measured 2966 ms while its avg was only 7.89 ms. That comes from the first frames of each channel not yet being aligned right after recording starts. Judge recording quality by avg and frame_drop_rate, and don’t let a startup spike in max mislead you. duration_s is wall-clock time, not effective recording time. Pausing breaks the correspondence, as the next section shows.

Pause and Resume

Besides stop(), the handle offers pause() and resume(), which the example doesn’t demonstrate. Recording 2 seconds, pausing 2 seconds, then recording 2 more with 3 streams: The frame count matches 4 seconds of effective recording, with nothing written while paused. duration_s still follows the wall clock, so deriving a rate from total_frames / duration_s comes out low. That run also raised spc_alert_count to 1. Channels go silent while paused and statistical process control flags it as an anomaly. That’s expected, not a fault. The handle also exposes subscribe_alerts(), subscribe_metrics(), and subscribe_status(), which deliver alerts, quality metrics, and status during recording rather than making you wait for stop().

Runtime Handling

This block in the example is deliberate:
asyncio.shield keeps stop() from being cancelled by an outer interrupt. Without that guard, Ctrl+C can kill the task before MCAP finishes writing its index, leaving a file whose summary can’t be read.

Integration Steps

Finish every record() call before start(). Channels registered afterward never reach the file.

Running It

Measured on a right-hand Wuji Glove resting on a desk, unworn, recording 3 streams for 10 seconds with lz4. Those 3939 frames spread across 3 channels at about 1313 each, an effective 131 Hz per channel.

Reading a Recording

With the mcap command-line tool installed:
Parsing frame by frame in Python:
Message bodies are JSON, so json.loads(message.data) gives you the same field structure as a live subscription.

Adapting It

Register Only the Channels You Need

All eight streams with zstd measured 6.01 MB over 5 seconds, roughly 72 MB per minute. tf alone accounts for 4361 messages, more than the six device streams combined. Decide which streams you actually need before a long capture.

Lower the Rate Before Recording

Subscription handles support rate control. Dropping 130 Hz to 30 Hz cuts file size to under a quarter. See Adjusting Stream Rate.

Monitor Quality While Recording

Rather than waiting for stop() to learn whether frames dropped, read metrics live with handle.subscribe_metrics() and alerts with handle.subscribe_alerts(). Any long unattended capture should wire up both.