Skip to main content

small-mcap

Lightweight Python library for reading and writing MCAP files.

Installation

uv add small-mcap

# With compression support
uv add small-mcap[compression]  # ZSTD + LZ4
uv add small-mcap[zstd]         # ZSTD only
uv add small-mcap[lz4]          # LZ4 only

Reader

Basic read

from small_mcap import read_message

with open("input.mcap", "rb") as f:
    for schema, channel, message in read_message(f):
        print(f"{channel.topic}: {message.data}")

Read multiple inputs

from small_mcap import read_message

with open("recording1.mcap", "rb") as f1, \
     open("recording2.mcap", "rb") as f2, \
     open("recording3.mcap", "rb") as f3:
    for schema, channel, message in read_message([f1, f2, f3]):
        print(f"{channel.topic}: {message.log_time}")

Read with topic filtering

from small_mcap import read_message, include_topics

with open("input.mcap", "rb") as f:
    topics = ["/camera/image", "/lidar/points"]
    for schema, channel, message in read_message(f, should_include=include_topics(topics)):
        print(f"{channel.topic}: {len(message.data)} bytes")

Read with time range

from small_mcap import read_message

with open("input.mcap", "rb") as f:
    start = 1000000000  # nanoseconds
    end = 2000000000
    for schema, channel, message in read_message(f, start_time_ns=start, end_time_ns=end):
        print(f"{channel.topic} at {message.log_time}")

Reuse a file across seeks

McapFile keeps the file summary and recently decompressed chunks available for repeated reads. Each iterator has independent position and ordering state.

from small_mcap import McapFile

with McapFile.open("input.mcap") as recording:
    forward = recording.read_message(start_time_ns=1_000_000_000)
    snapshot = recording.read_message(end_time_ns=2_000_000_000, reverse=True)

Recovery of an incomplete file is opt-in. It reconstructs chunk indexes in memory, preserves complete on-disk message indexes, ignores only an incomplete tail, and never modifies the source:

with McapFile.open("incomplete.mcap", recover=True) as recording:
    print(recording.is_recovered, recording.supports_reverse)
    for schema, channel, message in recording.read_message():
        ...

Follow an append-only file

McapFollower polls a local growing file without blocking. Every poll has finite message and byte budgets; partial record headers and bodies remain uncommitted until a later poll completes them.

from small_mcap import McapFollower

with McapFollower.open("recording.mcap", validate_crc=True) as follower:
    while True:
        batch = follower.poll_messages(max_messages=1000, max_bytes=16 * 1024 * 1024)
        for schema, channel, message in batch.messages:
            consume(channel.topic, message)
        if batch.is_final:
            break

The follower emits a complete chunk without waiting for its message indexes and preserves schemas and channels across polls. Footer plus trailing magic marks the file final. Truncation and inode replacement raise McapFileTruncatedError and McapFileReplacedError; the follower never silently reopens a different file.

Read decoded messages

from small_mcap import read_message_decoded
import json

class JsonDecoderFactory:
    def decoder_for(self, message_encoding, schema):
        if message_encoding == "json":
            return lambda data: json.loads(bytes(data))
        return None

with open("input.mcap", "rb") as f:
    for msg in read_message_decoded(f, decoder_factories=[JsonDecoderFactory()]):
        print(f"{msg.channel.topic}: {msg.decoded_message}")

Read summary/metadata

from small_mcap import get_summary, get_header

with open("input.mcap", "rb") as f:
    summary = get_summary(f)
    print(f"Messages: {summary.statistics.message_count}")
    print(f"Duration: {summary.statistics.message_start_time} - {summary.statistics.message_end_time}")

    for channel in summary.channels.values():
        print(f"  {channel.topic}: {channel.message_encoding}")

Writer

Basic write

from small_mcap import McapWriter

with open("output.mcap", "wb") as f:
    writer = McapWriter(f)
    writer.start(profile="", library="my-app")

    # Add schema
    schema_id = 1
    writer.add_schema(schema_id, "MySchema", "json", b'{"type": "object"}')

    # Add channel
    channel_id = 1
    writer.add_channel(channel_id, "/my/topic", "json", schema_id)

    # Add messages
    for i in range(100):
        writer.add_message(
            channel_id,
            log_time=i * 1000000,  # nanoseconds
            data=b'{"value": 42}',
            publish_time=i * 1000000
        )

    writer.finish()

Write with compression

from small_mcap import McapWriter, CompressionType

with open("output.mcap", "wb") as f:
    writer = McapWriter(
        f,
        compression=CompressionType.ZSTD,
        chunk_size=1024 * 1024  # 1MB chunks
    )
    writer.start(profile="", library="my-app")

    schema_id = 1
    writer.add_schema(schema_id, "MySchema", "json", b"{}")
    channel_id = 1
    writer.add_channel(channel_id, "/topic", "json", schema_id)

    for i in range(1000):
        writer.add_message(channel_id, log_time=i*1000, data=b"data", publish_time=i*1000)

    writer.finish()

Write with encoder factory

from small_mcap import McapWriter
import json

class JsonEncoderFactory:
    """Implements EncoderFactoryProtocol for JSON messages."""
    profile = ""
    encoding = "jsonschema"
    message_encoding = "json"

    def encoder_for(self, schema):
        return lambda msg: json.dumps(msg).encode()

with open("output.mcap", "wb") as f:
    writer = McapWriter(f, encoder_factory=JsonEncoderFactory())
    writer.start(profile="", library="my-app")

    schema_id = 1
    writer.add_schema(schema_id, "SensorData", "jsonschema", b'{"type": "object"}')

    channel_id = 1
    writer.add_channel(channel_id, "/sensor/data", "json", schema_id)

    for i in range(100):
        msg = {"timestamp": i, "value": i * 2}
        writer.add_message_encode(channel_id, i * 1000, msg, publish_time=i * 1000)

    writer.finish()

Features

  • Zero dependencies for core functionality
  • Optional compression support (ZSTD, LZ4)
  • Lazy chunk loading for efficient memory usage
  • Topic and time-range filtering
  • Automatic schema/channel registration
  • CRC validation
  • Fast summary/metadata access

Performance

small-mcap is optimized for high-performance MCAP file reading with zero-copy operations and lazy chunk loading:

Key Optimizations:

  • Zero-copy memory access: Uses memoryview to avoid unnecessary data copies
  • Lazy chunk loading: Only decompresses chunks when needed
  • Parallel chunk decompression: num_workers threads decompress chunks ahead of the reader (zstd/lz4 release the GIL)
  • Binary search: Efficient time-range filtering using chunk indexes
  • Heap-based merging: Optimal multi-file reading with automatic ID remapping

Parallel Prefetch (num_workers)

Pass num_workers to read_message to decompress chunks in parallel using a thread pool. The main thread reads raw bytes sequentially while worker threads decompress ahead.

with open("large.mcap", "rb") as f:
    for schema, channel, message in read_message(f, num_workers=4):
        ...

Benchmarked on the included nuScenes MCAP file (431 MB, 560 zstd chunks, 30,900 messages; median of 5 runs):

Workers Median time (s) Msg/s Speedup
0 0.3878 79,675 1.00x
2 0.2017 153,223 1.92x
4 0.1357 227,727 2.86x
8 0.0920 335,839 4.22x

Comparison with other libraries:

Feature small-mcap mcap (official) rosbags pybag
Performance Fastest Fast Fast Moderate
Zero dependencies Yes No No No
Non-seekable streams Yes Yes No No
Multi-file reading Yes No Yes Yes
ROS1 support No No Yes No
SQLite3 backend No No Yes No

Benchmarks

Median runtime from pytest-benchmark on the included nuScenes dataset (data/data/nuScenes-v1.0-mini-scene-0061-ros2.mcap, 30,900 messages, 19.15s duration, 560 zstd chunks):

Scenario small-mcap mcap (official) rosbags pybag
Full read (seekable) 399.4 ms 493.4 ms 429.9 ms 521.4 ms
Full read (non-seekable) 405.9 ms 495.2 ms - -
Time-range filter (seekable) 106.1 ms 127.7 ms 426.3 ms 131.1 ms
Time-range filter (non-seekable) 125.0 ms 146.6 ms - -
Topic filter (seekable) 375.9 ms 458.9 ms 397.6 ms 451.9 ms
Topic filter (non-seekable) 396.4 ms 470.0 ms - -

Note: rosbags and pybag require seekable streams and are skipped for the non-seekable cases.

Summary:

  • small-mcap was fastest in all six scenarios
  • 1.17-1.24x faster than mcap (official) across all scenarios
  • 1.06-4.02x faster than rosbags where rosbags supports the scenario
  • 1.20-1.31x faster than pybag on seekable streams

Metadata

Release files for small-mcap 0.16.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for small-mcap 0.16.0
File Size Uploaded Provenance Actions
small_mcap-0.16.0.tar.gz 47.3 kB Without provenance Details for small_mcap-0.16.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for small-mcap 0.16.0
File Interpreter ABI Platform Actions
small_mcap-0.16.0-py3-none-any.whl Python 3 none any Details for small_mcap-0.16.0-py3-none-any.whl

Total release size: 100.9 kB

Release files / small_mcap-0.16.0.tar.gz

Download URL small_mcap-0.16.0.tar.gz
Size 47.3 kB
Tags Source
SHA-256 checksum
How to use checksums
e560e7feca7d3275c55ee6a6f448dd71f8c498b53de381feca34ea8bdf190e63
BLAKE2b-256 checksum
How to use checksums
e0dc0f28aaef4c41ff4afde5c49c22148691b0cb53b618d36e67fc9bf5381077
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / small_mcap-0.16.0-py3-none-any.whl

Download URL small_mcap-0.16.0-py3-none-any.whl
Size 53.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
590e501a99d0ceda22cc5afc71a7633018389b04c5c42662d3196a9611d83281
BLAKE2b-256 checksum
How to use checksums
f6cefd7af7d9e742a095dba61347ad1f338e1988cbf93e3be0f74f81108188a5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for small-mcap 0.16.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page