MCAP
The MCAP Bridge module serves as the bidirectional gateway between MCAP files and the Mosaico Data Platform: ingesting MCAP files into Mosaico sequences (via MCAPInjector), and extracting Mosaico sequences back out as MCAP files (via MCAPSequenceExtractor). MCAP is a self-describing container: every channel points to a schema record that embeds the full definition of its messages. The bridge relies on this to ingest message types it has never seen before, without requiring any message definition or generated code up front.
API-Keys
When the connection is established via the authorization middleware (i.e. using an API-Key), the MCAP Ingestion employs the mosaico Writing Workflow, which is allowed only if the key has the write permission.
Supported schema encodings
The MCAP Bridge currently supports channels whose schema is encoded with protobuf. To ingest MCAP files recorded as ROS 2 bags, use the ROS Bridge instead. See Supported Schema Encodings for details.
The core philosophy of the module is the same as the ROS Bridge: "Adaptation, Not Just Parsing." Every message is translated into the Mosaico Ontology rather than stored as an opaque payload. Since an MCAP file can carry any schema, the bridge cannot ship adapters for them in advance: by default, every channel is translated into an Unmodeled ontology generated at runtime from the schema embedded in the file. Specific schemas can then be mapped onto built-in or custom ontologies by writing a custom adapter.
Try-It Out
You can experiment yourself the MCAP Bridge ingestion via the MCAP Ingestion Example.
Architecture¶
The module is composed of collaborating components that handle both directions of the pipeline, MCAP file → Mosaico (ingestion) and Mosaico → MCAP file (extraction), from raw file and server access down to the shared adaptation layer.
The Loaders (MCAPLoader & MosaicoToMCAPLoader)¶
Each direction has its own loader:
MCAPLoader(ingestion) acts as the abstraction layer over the physical MCAP file. It uses themcaplibrary to read the file summary (channels, schemas, message counts) and delegates the decoding of each message to the Decoder registered for the channel's message encoding. Every message is yielded as anMCAPMessage, a container holding the decoded payload together with its channel and schema information.MosaicoToMCAPLoader(extraction) is the mirror image: it streams messages back out of a Mosaico sequence viaSequenceDataStreamer, resolving each topic's adapter from the_mcap_metadata recorded at ingestion time (see Metadata: Reserved Keys & Custom Fields).
Both loaders share the topic classification logic of the ROS Bridge loaders, which is what powers topics, rejected_topics, and resolve_adapter() identically on either side.
- Responsibilities: decoding (or, for
MosaicoToMCAPLoader, remote streaming) and channel filtering (supporting glob patterns like/cam/*). - Error Handling: rejected channels are reported with a specific reason (filtered, unavailable schema, unresolved decoder, unresolved adapter, malformed metadata) rather than aborting the whole run; malformed messages on an otherwise accepted channel are skipped and counted rather than raised.
Channel names and topic names
MCAP channel names become Mosaico topic names with dots replaced by slashes and a leading / (e.g. front_car.imu becomes /front_car/imu). The ingestion options (channels, topic_metadata, topics_on_error, serialization_formats) refer to the original channel names, while the extraction topics filter refers to the Mosaico topic names. The original channel name is restored on extraction.
The Decoders¶
The payload of an MCAP message is a sequence of bytes whose layout depends on the message encoding of its channel. Decoders turn each of these payloads into a plain nested Python dict, the common input expected by every adapter, regardless of the original encoding.
MCAPMsgDecoderBase: the abstract base class of every decoder. A subclass declares the channel message encoding it handles (SUPPORTED_CHANNEL_ENCODING), provides themcapDecoderFactorythat deserializes the raw bytes into a native object, and converts that object into adict. Before iteration starts,register_schema()is called once per channel for any schema-dependent setup;decode()is then called on every message.DecoderRegistry: maps each message encoding to its decoder class, and is populated through the@register_decoderdecorator.MCAPLoaderkeeps one decoder instance per channel, since a decoder can hold schema-dependent state. Channels whose message encoding has no registered decoder are rejected asUnresolved decoder.
Supporting a new encoding only requires a new MCAPMsgDecoderBase subclass decorated with @register_decoder, with no change to the loader.
Protobuf Decoder (MCAPProtobufMsgDecoder)¶
MCAPProtobufMsgDecoder handles protobuf channels. In register_schema() it loads the FileDescriptorSet embedded in the channel's schema into a private DescriptorPool, so messages are decoded without any generated Python class. Each message is then converted through the protobuf JSON mapping, keeping google.protobuf.Any fields as their raw type_url and value bytes instead of unpacking them.
The JSON mapping does not always produce what the Arrow schema of the topic expects, so the decoder fixes the resulting dict through two classes:
Rule: pairs a predicate on a schema field (e.g. "is this field aTimestamp?") with a callback that fixes that field in the decodeddict.ProtobufRulesMatcher: walks the message descriptor once per schema, recursing into nested messages, repeated fields and maps, and associates each field path with the rules it matches. Duringdecode(), every rule is applied only to the paths it was matched against.
| Protobuf field | JSON mapping output | Fix applied |
|---|---|---|
int64 family (int64, uint64, sint64, fixed64, sfixed64) |
May be a string | Coerced to a Python int |
google.protobuf.Timestamp |
RFC 3339 string | Converted to a {"seconds", "nanos"} struct |
bytes |
Base64 string | Decoded back to raw bytes |
oneof members, singular messages |
Omitted when unset | Filled with None |
The Orchestrator (MCAPInjector)¶
The MCAPInjector is the central command center of the MCAP Bridge module. It is designed to be the primary entry point for developers who want to embed MCAP ingestion directly into their Python applications or automation scripts.
The injector orchestrates the interaction between the MCAPLoader (file access and decoding), the MCAPBridge (data adaptation), and the MosaicoClient (network transmission). It handles the complex lifecycle of a data upload, including connection management, batching, and transaction safety, while providing real-time feedback through a visual CLI interface.
Core Workflow Execution: run()¶
- Handshake: Establishes a connection to the Mosaico server and opens the MCAP file via
MCAPLoader, which resolves the decoder and the adapter of every channel. - Sequence Creation: Requests the server to initialize a new data sequence based on the provided name and metadata.
- Adaptive Streaming: Iterates through the MCAP messages. For each message, it identifies the correct adapter, translates the decoded dictionary into a Mosaico object, and pushes it into an optimized write buffer. Each topic is created on its first message, together with its
_mcap_metadata. - Transaction Finalization: Once the file is exhausted, it flushes all remaining buffers and signals the server to commit the sequence.
Configuring the Ingestion¶
The behavior of the injector is entirely driven by the MCAPInjectionConfig. This configuration object ensures that the ingestion logic is decoupled from the user interface, allowing for consistent behavior whether triggered via the CLI or a complex script.
Compared to ROSInjectionConfig, it filters the input through channels instead of topics, and it has no ros_distro, custom_msgs or registry field, since no message definition has to be provided (see No Message Definitions Required). Two more options are worth mentioning:
serialization_formats: selects, per channel, theSerializationFormatof the Unmodeled ontology created for it. Channels not listed useSerializationFormat.Default.dry_run: ifTrue, reports which channels would be ingested (and with which adapter) and which would be rejected (and why), without connecting to the Mosaico server or writing any data.
Per-Topic Metadata (topic_metadata)¶
Besides the sequence-level metadata dict, topic_metadata: Optional[Dict[str, dict]] lets you attach metadata to individual topics by exact channel name (the same exact-name convention as topics_on_error, rather than the glob patterns used by channels). Entries for channels excluded by channels filtering are simply unused. See Metadata: Reserved Keys & Custom Fields below for how it merges with metadata the bridge computes automatically.
It can also be loaded from a JSON file, handy for the CLI's --topic-metadata flag, or to keep large mappings out of your script:
{
"/imu": {"unit": "rad/s"},
"/estimation/pose": {"algorithm": "ekf_v2", "offline": true}
}
import json
config = MCAPInjectionConfig(
...,
topic_metadata=json.loads(Path("topic_metadata.json").read_text()),
)
Updating an Existing Sequence (update_if_exists)¶
By default, ingesting into a sequence_name that already exists on the server raises an error. Setting update_if_exists=True switches this to an append/merge: the injector appends this file's topics to the existing sequence instead of creating a new one. This covers two distinct real-world scenarios with the same mechanism:
- Multi-part recordings: a single logical recording split across several MCAP files, all of which should land in one sequence.
- Reprocessing / augmentation: a derived MCAP file (e.g. offline estimation results computed from an already ingested recording, with timestamps aligned to the original) whose topics should be merged into the sequence that was already ingested from the original recording.
Since sequence metadata can't be changed after creation, the per-topic source_file metadata (see Metadata: Reserved Keys & Custom Fields below) is what keeps this traceable: inspecting a topic's own metadata tells you which MCAP file introduced it, even after several update_if_exists=True runs against the same sequence.
config = MCAPInjectionConfig(
file_path=Path("estimation_results.mcap"),
sequence_name="on_track_experiment", # already ingested from the original recording
metadata={}, # ignored: the sequence already exists, its metadata is immutable
update_if_exists=True,
topic_metadata={
"/estimation/pose": {"algorithm": "ekf_v2", "offline": True},
},
)
If sequence_name doesn't exist yet, update_if_exists=True simply creates it, same as leaving it at the default False.
Resuming after a crash is not idempotent
If the process crashes midway through a file and you re-run the same command with update_if_exists=True, the injector has no memory of which topics it already fully ingested before the crash: that bookkeeping lives only in an in-memory cache scoped to the crashed process's session. Expect topic_create to be called again for those topics on resume, which the server is expected to reject as duplicates. There is currently no built-in dedup against the sequence's already existing topics before re-creating them, so a genuinely safe resume isn't supported yet. Plan for re-ingesting into a fresh sequence name if a run fails partway through, rather than relying on update_if_exists to pick up where it left off.
Practical Example: Programmatic Usage¶
from pathlib import Path
from mosaicolabs import SerializationFormat, SessionLevelErrorPolicy, TopicLevelErrorPolicy
from mosaicolabs.bridges.mcap import MCAPInjector, MCAPInjectionConfig
def run_injection():
# Define the Injection Configuration
# This data class acts as the single source for the operation.
config = MCAPInjectionConfig(
# Input Data
file_path=Path("data/session_01.mcap"),
# Target Platform Metadata
sequence_name="test_mcap_sequence",
metadata={
"driver_version": "v2.1",
"weather": "sunny",
"location": "test_track_A"
},
# Channel Filtering (supports glob patterns)
# This will only upload channels starting with '/cam' or '/lidar'
channels=["/cam*", "/lidar*"],
# Serialization Format of the Unmodeled ontologies, per channel
serialization_formats={
"/lidar/points": SerializationFormat.Ragged,
},
# Per-Topic Metadata (exact channel name to dict; the reserved "_mcap_" key,
# containing schema info and source_file, always overrides this on conflict)
topic_metadata={
"/cam/front": {"lens": "wide-angle", "calibrated": True},
},
# Update instead of Create
# If "test_mcap_sequence" already exists (e.g. a previous file of a multi-part
# recording, or a sequence to merge reprocessed results into), append to it
# instead of raising an error.
update_if_exists=False,
# Execution Settings
log_level="WARNING", # Reduce verbosity for automated scripts
# Session Level Error Handling
on_error=SessionLevelErrorPolicy.Report, # Report the error and terminate the session
# Topic Level Error Handling
topics_on_error=TopicLevelErrorPolicy.Raise # Re-raise any exception
)
# Instantiate the Controller
injector = MCAPInjector(config)
# Execute
# The run method handles connection, loading, and uploading automatically.
# It raises exceptions for fatal errors, allowing you to wrap it in try/except blocks.
try:
injector.run()
print("Injection job completed successfully.")
except Exception as e:
print(f"Injection job failed: {e}")
# Use as script or call the injection function in your code
if __name__ == "__main__":
run_injection()
The Adaptation Layer (MCAPBridge & Adapters)¶
This layer represents the semantic core of the module, translating decoded MCAP messages into the Mosaico Ontology.
MCAPAdapterBase: an abstract base class that establishes the contracts for converting MCAP messages into Mosaico Ontology types and back. An adapter is identified by theschema_nameandschema_encodingpair it handles, and implementsfrom_dict()(MCAP → Mosaico) and, optionally,to_mcap()(Mosaico → MCAP).- No built-in adapters: unlike the ROS Bridge, which ships adapters for the standard ROS messages, the structure of the messages contained in an MCAP file cannot be known in advance. The only adapter provided by the library is the Unmodeled Adapter, used whenever no custom adapter is registered for a schema.
MCAPBridge: a central registry and dispatch mechanism that maps each(schema_name, schema_encoding)pair to its adapter class. Registration fails if the adapter'sschema_encodingis not supported, or if an adapter is already registered for the same pair.
MCAP log time and publish time
Every MCAP message carries two timestamps, both in nanoseconds:
log_time: the time at which the message was recorded, assigned by the recorder that wrote the file. MCAP orders and indexes messages by this value.publish_time: the time at which the message was published, assigned by its publisher. When not available, it is set equal tolog_time.
Every Mosaico Message produced by the bridge is timestamped with the MCAP log_time. Mosaico orders and indexes records by Message.timestamp_ns, so all the topics of a sequence must share a common time origin (see Message (The Envelope)). log_time meets this requirement, since a single recorder stamps every channel of the file on the same clock. publish_time, instead, comes from the clock of each publisher and may be missing, so it is not suitable for indexing.
The publish time is not lost: it is passed to from_dict() alongside the decoded payload, so a custom adapter can store it in the ontology (the example below uses it as the Header timestamp), while the Unmodeled Adapter keeps it in the payload as publish_time_ns. On extraction, log_time is restored from Message.timestamp_ns, and publish_time from the value returned by to_mcap().
Extending the Bridge (Custom Adapters)¶
Users can map an MCAP schema onto a built-in or custom ontology by implementing a custom adapter and registering it. This is also the way to store an MCAP schema as your own ontology (see No Message Definitions Required).
- Inherit from
MCAPAdapterBase: Define theschema_nameandschema_encodinghandled by the adapter (they must match the schema recorded in the MCAP file) and the target Mosaico Ontology type. - Implement
from_dict: Define the logic to convert the decoded message dictionary (and its publish time) into an instance of the target ontology object. - Implement
to_mcap(optional): Define the reverse conversion, returning the native message (forprotobuf, an instance of the generated message class) and its publish time. It is only needed to extract the topic back to MCAP. - Register: Decorate the class with
@register_default_adapter, and import its module before running the injector or the extractor.
The adapter below maps a protobuf message named Mosaico.Imu onto the built-in IMU ontology:
from typing import Optional, Tuple, Type
from mosaicolabs import IMU, Header, Time, Vector3d
from mosaicolabs.bridges.mcap import MCAPAdapterBase, register_default_adapter
# Class generated by protoc from the .proto definition of the message
from my_project.generated.imu_pb2 import Imu as ImuProtobuf
@register_default_adapter
class MyImuProtobufAdapter(MCAPAdapterBase[IMU, ImuProtobuf]):
schema_name = "Mosaico.Imu"
schema_encoding = "protobuf"
__mosaico_ontology_type__: Type[IMU] = IMU
@classmethod
def from_dict(cls, mcap_data: dict, publish_time_ns: int) -> IMU:
mcap_acceleration = mcap_data["linear_acceleration"]
mcap_angular_velocity = mcap_data["angular_velocity"]
return IMU(
header=Header(timestamp=Time.from_nanoseconds(publish_time_ns)),
acceleration=Vector3d(
x=mcap_acceleration["x"],
y=mcap_acceleration["y"],
z=mcap_acceleration["z"],
),
angular_velocity=Vector3d(
x=mcap_angular_velocity["x"],
y=mcap_angular_velocity["y"],
z=mcap_angular_velocity["z"],
),
)
@classmethod
def to_mcap(cls, mosaico_data: IMU) -> Tuple[ImuProtobuf, Optional[int]]:
out = ImuProtobuf()
out.linear_acceleration.x = mosaico_data.acceleration.x
out.linear_acceleration.y = mosaico_data.acceleration.y
out.linear_acceleration.z = mosaico_data.acceleration.z
out.angular_velocity.x = mosaico_data.angular_velocity.x
out.angular_velocity.y = mosaico_data.angular_velocity.y
out.angular_velocity.z = mosaico_data.angular_velocity.z
publish_time_ns = None
if mosaico_data.header and mosaico_data.header.timestamp:
publish_time_ns = mosaico_data.header.timestamp.to_nanoseconds()
return out, publish_time_ns
Extending the Bridge (Unmodeled Adapters)¶
Not every MCAP schema needs a hand-written adapter before it can be ingested. When the bridge encounters a channel whose (schema_name, schema_encoding) pair has no registered adapter, it doesn't reject it. Instead, it synthesizes an UnmodeledAdapter for it at runtime, transparently and with no user intervention required, capable of translating that schema in both directions: MCAP to Mosaico, and back again from Mosaico to MCAP.
The schema definition embedded in the MCAP file is converted into an equivalent PyArrow schema, which is wrapped into a dynamically-generated Unmodeled ontology class via resolve_ontology_class. Its ontology tag is the last component of the schema name (e.g. foxglove.CompressedVideo becomes CompressedVideo), and the MCAP publish time is stored in its payload as publish_time_ns. On extraction, the adapter rebuilds the native message class from the schema definition recorded at ingestion time (for protobuf, the generated message class) and fills it with the stored data.
See Advanced: Ingesting Unmodeled Ontologies for the full mechanics of the Unmodeled ontology type, and Class-Free Queries for how to query this data on the server without ever needing to resolve a Python class for it.
Metadata: Reserved Keys & Custom Fields¶
Internal behavior
This section documents implementation detail useful for interpreting or querying ingested metadata, not something you need to configure to use the bridge.
Sequence and topic metadata are populated from a mix of auto-computed and user-supplied sources. At the topic level, exactly one key is reserved because the bridge writes it itself: _mcap_, encapsulated by MCAPSchemaMetadata so that the literal string "_mcap_" exists in a single place in the codebase. It holds everything needed to rebuild the original channel on extraction:
schema_name,schema_encodingandschema_def: the original MCAP schema, with its definition stored as a string (base64 forprotobuf).channel_nameandchannel_encoding: the original MCAP channel.source_file: the name of the MCAP file (file_path.name) that first created that topic. Written once per topic, at topic-creation time, regardless ofupdate_if_exists.
A topic whose _mcap_ block is missing one of the schema or channel fields, or holds a value of an unexpected type, is rejected on extraction as Malformed metadata.
_mcap_ is fully reserved: topic_metadata (Per-Topic Metadata above) is merged in first, then the bridge-computed _mcap_ block is applied on top, so the bridge always wins that key regardless of what topic_metadata sets for it. Every other key is fully user-owned.
metadata(sequence-level): only applied at sequence-creation time. The Mosaico server does not support mutating a sequence's metadata after ingestion, so it's simply ignored whenupdate_if_exists=Truetargets an already existing sequence.topic_metadata: merged underneath the auto-computed_mcap_block (see above).
CLI-only sequence metadata
When using the mosaicolabs.mcap_injector CLI, an additional mcap_injection key (the MCAP file name) is automatically merged into metadata for traceability. This only happens in the CLI entry point, not when constructing MCAPInjectionConfig directly in Python.
Querying these keys
_mcap_ and its nested fields (e.g. _mcap_.schema_name, _mcap_.channel_name) are ordinary metadata as far as the query engine is concerned: they're queryable through QuerySequence and QueryTopic with_user_metadata() exactly like any other metadata field, including glob patterns for nested keys (e.g. QueryTopic().with_user_metadata("_mcap_.schema_name", eq="foxglove.CompressedVideo")). See the Query Workflow guide for the full API.
CLI Usage¶
The module includes a command-line interface for quick ingestion tasks. The full list of options can be retrieved by running mosaicolabs.mcap_injector -h
# Basic Usage
mosaicolabs.mcap_injector ./data.mcap --name "Test_Run_01"
# Advanced Usage: Filtering channels and adding metadata
mosaicolabs.mcap_injector ./data.mcap \
--name "Test_Run_01" \
--channels "/camera/front/*" /gps/fix \
--metadata ./metadata.json
# Advanced Usage: Per-topic metadata and appending to an existing sequence
# (e.g. a second file of a multi-part recording, or reprocessed results
# to merge into an already ingested sequence)
mosaicolabs.mcap_injector ./estimation_results.mcap \
--name "Test_Run_01" \
--topic-metadata '{"/estimation/pose": {"algorithm": "ekf_v2"}}' \
--update-if-exists
# Dry Run: report accepted and rejected channels, without connecting to the server
mosaicolabs.mcap_injector ./data.mcap --name "Test_Run_01" --dry-run
The Extractor (MCAPSequenceExtractor)¶
The MCAPSequenceExtractor runs the ingestion pipeline in reverse: it reads a Mosaico sequence back out and writes it as an .mcap file, using the adapters' to_native() and the _mcap_ metadata recorded at ingestion time to recreate the original channels and schemas. Like the ROS Bridge's ROSSequenceExtractor, it is built on the bridge-agnostic SequenceExtractor base class.
Same schema encoding as ingestion
Extraction returns each topic to the schema encoding it was ingested with: a channel ingested as protobuf is written back as protobuf. Converting a topic to a different schema encoding is not supported.
Core Workflow Execution: run()¶
- Prepare Output Path: resolves
saving_path / sequence_nameand enforces theoverwritepolicy: raisesFileExistsErrorif the path exists andoverwrite=False, otherwise deletes it first. The file is then written assaving_path / sequence_name / sequence_name.mcap. - Handshake: connects to the Mosaico server and opens a
MosaicoToMCAPLoaderfor the requested sequence (optionally filtered bytopicsand astart_timestamp_ns/end_timestamp_nswindow). - Adaptive Streaming: for each
(topic, message)pair, resolves the topic's adapter (a custom adapter registered for the recorded schema, otherwise an Unmodeled Adapter), serializes the message viato_native(), and writes it on the topic's original channel with its original log and publish times. Each channel and schema is recreated once, from the_mcap_metadata. Topics that fail to be encoded or written are skipped (logged as an error) rather than aborting the whole extraction.
Configuring the Extraction¶
The behavior of the extractor is entirely driven by MCAPExtractorConfig, which only uses the options shared by every bridge extractor. Its main knobs:
topics: glob-based include/exclude filtering (see Configuring the Ingestion above), applied to the Mosaico topic names of the sequence.start_timestamp_ns/end_timestamp_ns: an optional time window to extract, with an inclusive start and an exclusive end (start <= t < end). Out-of-range bounds are clipped to the sequence's own bounds (with a warning) rather than raising: anend_timestamp_nsbeyond the end of the sequence extracts everything up to, and including, its last message.overwrite: if the resolved output path (saving_path / sequence_name) already exists,overwrite=False(default) raisesFileExistsError;overwrite=Truedeletes and recreates it.
Dry Run (dry_run)¶
Setting dry_run=True (or --dry-run on the CLI) resolves the sequence's topics and prints a report (per topic, the resolved adapter and schema name, or the rejection reason, plus message counts) without opening a writer or touching the output path. The dry run also reports what would happen to that path (created, deleted and recreated, or a FileExistsError) without doing it.
config = MCAPExtractorConfig(
saving_path=Path("./exports"),
sequence_name="on_track_experiment",
dry_run=True,
)
MCAPSequenceExtractor(config).run() # prints a report; nothing is written or deleted
python -m mosaicolabs.bridges.mcap.sequence_extractor on_track_experiment --mcap_path ./exports --dry-run
Practical Example: Programmatic Usage¶
from pathlib import Path
from mosaicolabs.bridges.mcap import MCAPSequenceExtractor, MCAPExtractorConfig
config = MCAPExtractorConfig(
saving_path=Path("./exports"),
sequence_name="on_track_experiment",
# Topic Filtering (supports glob patterns, applied to the Mosaico topic names)
topics=["/cam*", "!/cam/debug*"],
# Optional time-window clipping (nanoseconds); out-of-range bounds are clipped
# to the sequence's own bounds rather than raising.
start_timestamp_ns=None,
end_timestamp_ns=None,
overwrite=True,
)
extractor = MCAPSequenceExtractor(config)
extractor.run() # writes ./exports/on_track_experiment/on_track_experiment.mcap
CLI Usage¶
The extractor can be run as a module. The full list of options can be retrieved by running python -m mosaicolabs.bridges.mcap.sequence_extractor -h.
# Basic Usage
python -m mosaicolabs.bridges.mcap.sequence_extractor on_track_experiment --mcap_path ./exports
# Advanced Usage: Filtering topics and overwriting a previous export
python -m mosaicolabs.bridges.mcap.sequence_extractor on_track_experiment \
--mcap_path ./exports \
--topics "/cam/*" "!/cam/debug*" \
--overwrite
No Message Definitions Required¶
Unlike the ROS Bridge, the MCAP Bridge has no type registry and no custom_msgs option. A ROS bag may lack the definition of some of its messages, which then have to be registered through the ROSTypeRegistry before they can be decoded. An MCAP file, instead, embeds the definition of every schema it uses: any channel with a supported encoding can be decoded and ingested as is, and later rebuilt from the definition stored in the topic metadata.
Defining new ontologies is still possible, and useful to simplify the query process: a Serializable class gives an MCAP schema typed fields and the .Q query proxy, instead of the class-free queries used for Unmodeled ontologies. In this case, a custom adapter mapping the MCAP schema onto the new ontology has to be implemented.
Testing & Validation¶
The MCAP Bridge has been validated against the Labelbox robotics datasets on Hugging Face, whose recordings use protobuf schemas, both Foxglove well-known schemas (e.g. foxglove.CompressedVideo, foxglove.FrameTransforms) and vendor-specific ones. The MCAP Ingestion and Reconstruct MCAP examples ingest four of these recordings and write them back to MCAP.
Known Issues & Limitations¶
- Schema encodings: only
protobufschemas are supported (see Supported Schema Encodings). - Summary section:
MCAPLoaderreads channels, schemas and message counts from the summary section of the file, and refuses files written without one. - Message sequence counter: the per-message MCAP
sequencecounter is not restored on extraction, and messages are written withsequenceset to0. google.protobuf.Any: these fields are stored as their rawtype_urlandvaluebytes, without unpacking the packed message.- Integer map keys: protobuf
mapfields can have integer or string keys, but the protobuf JSON mapping (MessageToDict) turns every map key into a string, and the Protobuf Decoder does not post-process map keys. For a map with integer keys, the decoded keys therefore do not match the integer key type of the PyArrow schema, and the ingestion of that channel fails. - Point cloud extraction: reconstructing an MCAP file from a Mosaico sequence is slow for topics holding point clouds.
Supported Schema Encodings¶
| Schema Encoding | Support | Notes |
|---|---|---|
protobuf |
Full | |
ros2msg, ros2idl, ros1msg |
Not supported | Use the ROS Bridge instead. |
Channels whose encoding is not supported are rejected and listed in the run report, while the rest of the file is still processed. Support for further encodings will be added in future releases.