MCAP Import Mapping
Find the typed elements that each Foxglove and ROS 2 topic of an MCAP recording imports as, with their time and frame fields.
An MCAP recording imports as one row, and each of its topics as one list-valued column of elements, one element per message. Your application reads poses, point clouds and video packets as Spiral data types instead of decoding Protobuf or ROS 2 messages itself. The mapping is fixed for each message schema, so every Table imported from MCAP names and types its elements the same way.
A /tf topic of tf2_msgs/msg/TFMessage messages, each holding two
transforms, imports as one element per transform:
log_time publish_time stamp transform
2026-10-20T09:00:00.000000000 2026-10-20T09:00:00.000001000 2026-10-20T08:59:59.999000000 {translation: {x: 1.0, y: 0.0, z: 0.0}, rotation: {x: 0.0, y: 0.0, z: 0.0, w: 1.0}, parent_frame_id: world, child_frame_id: base_link}
2026-10-20T09:00:00.000000000 2026-10-20T09:00:00.000001000 2026-10-20T08:59:59.999000000 {translation: {x: 0.1, y: 0.0, z: 0.4}, rotation: {x: 0.0, y: 0.0, z: 0.0, w: 1.0}, parent_frame_id: base_link, child_frame_id: camera}A topic reads through the recording's chunk index, so reading one topic opens only the chunks that hold it. Recordings without a summary section and chunk index cannot be read by topic.
The recordings Table
An import writes one row per MCAP file. The Table stores each recording's file and, for each topic, what a read needs to decode it. A topic's elements are decoded when you read them, not when you import.
| Column | Type | Meaning |
|---|---|---|
recording | string | The row's key: the file's path below the imported directory. |
file | spql.Blob | The MCAP file, copied into the Table's storage. |
start_time, end_time | timestamp, nanoseconds, nullable | The first and last log_time in the file. Null when the file records no statistics. |
message_count | unsigned integer, nullable | Messages in the file. |
| one column per topic | struct, nullable | The topic's reference: topic, schema_name, message_encoding, message_count and dtype, the element type its reads return. Null when the recording does not publish the topic. |
A topic column's name is the topic without its leading and trailing /,
with every character other than a letter, digit or _ replaced by _:
/camera/front is stored as camera_front. Topics that several channels of
one file publish, and topics that are neither Protobuf nor ROS 2 CDR, are left
out of that file's row, and the import reports them.
Reading a topic in a log_time range returns the elements logged in that
half-open range, and opens only the chunks whose messages overlap it. A
recording that does not publish the topic reads as null. A playback window or
a training clip reads a small part of a long camera topic; reading the whole
of such a topic at once is refused when its chunks hold more than 64 MiB once
decompressed.
Time and frame fields
Every element carries the recording's two clocks and the message's own timestamp, because they answer different questions: when the recorder saw the message, when the publisher sent it, and when the measurement was taken.
| Field | Type | Meaning |
|---|---|---|
log_time | timestamp, nanoseconds | When the message was recorded. MCAP indexes are keyed on it, so time-range reads of the source file follow this field. |
publish_time | timestamp, nanoseconds | When the message was published. A recorder that does not know it writes log_time. |
stamp | timestamp, nanoseconds, nullable | The message's own time: the ROS 2 header.stamp, or the Foxglove timestamp. Null when a Foxglove message leaves it unset. |
frame_id | string, nullable | The coordinate frame of the value: the ROS 2 header.frame_id, or the Foxglove frame_id. Null when the source string is empty. |
The timestamps carry no time zone, because MCAP does not fix the epoch: a
recording can count from the Unix epoch or from the robot's boot. Only values
that are expressed in a frame get a frame_id field. A transform names its
parent and child frames inside the value, and joint states and log records are
not positions in space, so their elements have no frame_id.
Values follow REP 103 units: metres, radians and seconds. Elements are in the order of the file.
Message mappings
A topic's schema name and message encoding select its mapping. ROS 2 schema
names are accepted with or without the msg/ segment, so
sensor_msgs/PointCloud2 and sensor_msgs/msg/PointCloud2 are the same.
| Source message | Encoding | Value field | Type | frame_id |
|---|---|---|---|---|
foxglove.CompressedVideo | protobuf | packet | spql.EncodedPacket | yes |
foxglove.CompressedImage | protobuf | image | spql.EncodedImage | yes |
sensor_msgs/msg/CompressedImage | cdr | image | spql.EncodedImage | yes |
foxglove.PointCloud | protobuf | point_cloud | spql.PointCloud | yes |
sensor_msgs/msg/PointCloud2 | cdr | point_cloud | spql.PointCloud | yes |
foxglove.FrameTransform | protobuf | transform | spql.Transform3D | no |
foxglove.FrameTransforms | protobuf | transform, one element per transform | spql.Transform3D | no |
geometry_msgs/msg/TransformStamped | cdr | transform | spql.Transform3D | no |
tf2_msgs/msg/TFMessage | cdr | transform, one element per transform | spql.Transform3D | no |
foxglove.PoseInFrame | protobuf | pose | spql.Pose3D | yes |
geometry_msgs/msg/PoseStamped | cdr | pose | spql.Pose3D | yes |
foxglove.LocationFix | protobuf | fix | spql.LocationFix | yes |
sensor_msgs/msg/NavSatFix | cdr | fix | spql.LocationFix | yes |
foxglove.JointStates | protobuf | joints | list of spql.JointState | no |
sensor_msgs/msg/JointState | cdr | joints | list of spql.JointState | no |
foxglove.Log | protobuf | log | spql.LogEntry | no |
rcl_interfaces/msg/Log | cdr | log | spql.LogEntry | no |
The two transform lists become one element per transform, each with its
message's log_time and publish_time.
Other schemas
A Protobuf or ROS 2 topic whose schema has no mapping still imports. Its
elements hold log_time, publish_time and message, the message decoded by
the schema the recording embeds, with the schema's field names. A
sensor_msgs/msg/Temperature topic imports as elements such as
{log_time, publish_time, message: {header: {stamp: {sec, nanosec}, frame_id}, temperature, variance}}.
- Proto3 scalars without presence use their zero, false or empty default. Absent message fields, proto3 optional fields and proto2 optional fields are null, including proto2 fields with declared defaults. Missing required proto2 fields fail the read. Repeated fields are empty when absent. Recursive schemas, real oneofs and protobuf Editions are unsupported.
- ROS 2 fields are never null. Arrays of
uint8orbytedecode as binary, other arrays as lists, and nested messages as structs. Topics withwstringfields cannot be read.
Trossen recordings use both mapped and generic Protobuf topics. Task descriptions
(foxglove.KeyValuePair), joint states (schemas.proto.JointState), gripper
status (schemas.proto.GripperStatus) and camera calibration
(foxglove.CameraCalibration) keep their source fields inside message.
foxglove.FrameTransforms becomes one transform element per transform, and
foxglove.CompressedVideo becomes one packet element per message. The embedded
descriptors supply nested timestamps, repeated joint values and calibration
arrays; applications need no external .proto files.
Field mappings
Video packets. A foxglove.CompressedVideo message is one access unit.
payload holds its bytes inline, codec is the message's format, and
framing is annex_b for h264 and h265 and obu for av1. pts is the
message's timestamp in nanoseconds, or log_time when it is unset, with a
time base of 1/1,000,000,000. keyframe is true when an H.264 or H.265 packet
holds an IDR, CRA or BLA picture, and null for other codecs. stream, dts,
duration_ticks and codec_config are null: the topic's packets are one
stream in decode order, and parameter sets travel in band, as Foxglove
requires.
Images. source holds the compressed bytes inline. format is jpeg,
png, webp or avif, with the matching mime_type; any other format string
is kept, lowercased, with a null mime_type. ROS 2 image_transport strings
such as bgr8; jpeg compressed bgr8 import as jpeg. width and height are
null until the image is decoded.
Point clouds. The points keep the source's packed layout: data is the
message's point bytes, point_stride the bytes from one point to the next,
and fields one entry for each point field with its name, offset, dtype
(u8, i8, u16, i16, u32, i32, f32 or f64) and count. A
foxglove.PointCloud keeps its pose. A PointCloud2 has no pose, and its
rows are joined so that points are point_stride apart; is_dense is not
kept. Big-endian clouds are refused, because spql.PointCloud is
little-endian.
Transforms. translation and rotation move the child frame onto the
parent frame. A TransformStamped takes its parent_frame_id from
header.frame_id. scale is null. A Foxglove transform without a
translation or rotation is null.
Poses. position and orientation are the pose in the element's
frame_id. A Foxglove pose without a position or orientation is null.
Location fixes. position holds latitude and longitude in degrees and
altitude in metres. covariance is the 3×3 ENU position covariance in
row-major order, and covariance_type keeps the source's code (0 unknown, 1
approximated, 2 diagonal known, 3 known). A NavSatFix altitude of NaN, which
ROS 2 uses for "no altitude", is null, and its NavSatStatus fields are kept
in metadata under the keys status and service. A foxglove.LocationFix
keeps its heading, velocity, color and metadata, which are null when
the recording's schema predates them.
Joint states. Each joint is one element with its name and its
position, velocity, acceleration and effort. A ROS 2 JointState
leaves a quantity's array empty when it does not report it, so that quantity
is null for every joint, and acceleration is always null. A non-empty array
whose length differs from the number of names fails the import.
Logs. level uses Foxglove's levels: 1 debug, 2 info, 3 warning, 4
error, 5 fatal, and 0 unknown. The ROS 2 levels 10 to 50 import as 1 to 5.
name, file and line are null when the source leaves them empty or zero.
Errors
Reading a topic fails, and returns no elements, when the recording has no
chunk index, when the topic's encoding is neither Protobuf nor ROS 2 CDR, when
a message does not decode as its schema, or when a decoded value breaks its
type, such as a point field type the mapping does not know. A read whose chunks
hold more than 64 MiB once decompressed fails; read a narrower log_time
range.
Availability
A public import command and SDK methods for topic reads are planned. The published clients do not yet expose this import workflow. Reads of a whole topic larger than 64 MiB are also planned. Until then, topic reads require time windows.