Files
cvmmap-streamer/docs/mcap_layout.md
T
crosstyan 6d50b29eff feat(mcap): add Python layout validator
Document the bundled and single-camera MCAP topic contract in docs/mcap_layout.md and link it from the README.

Add scripts/mcap_bundle_validator.py to summarize and validate both bundled /bundle-based MCAPs and single-camera /camera/* MCAPs from Python.

Validate bundled files against bundle-member presence counts and single-camera files against topic/schema expectations plus video/depth/calibration count rules.
2026-03-24 16:03:16 +08:00

5.5 KiB

MCAP Layout

cvmmap-streamer writes two related MCAP layouts:

  • single-camera MCAP export
  • bundled multi-camera MCAP export

This document covers the topic layout, schema types, and timestamp semantics for both.

Single-Camera Layout

Default topics:

Topic Schema / Encoding Notes
/camera/video foxglove.CompressedVideo H.264 or H.265 Annex B access units
/camera/depth cvmmap_streamer.DepthMap RVL-compressed depth payload
/camera/calibration foxglove.CameraCalibration Video intrinsics
/camera/depth_calibration foxglove.CameraCalibration Written only when depth resolution differs from video
/camera/pose foxglove.PoseInFrame Optional; only when pose export is enabled and tracking is valid
/camera/body raw cvmmap.body_tracking.v1 Optional raw body-tracking packets; see mcap_body_tracking.md

Single-camera export preserves the original per-frame source timestamp on video, depth, and pose messages.

Bundled Multi-Camera Layout

Bundled export namespaces each camera stream and adds one bundle manifest topic:

Topic Schema / Encoding Notes
/bundle cvmmap_streamer.BundleManifest One message per emitted bundle
/zedN/video foxglove.CompressedVideo Per-camera encoded video
/zedN/depth cvmmap_streamer.DepthMap Per-camera depth
/zedN/calibration foxglove.CameraCalibration Per-camera video intrinsics
/zedN/depth_calibration foxglove.CameraCalibration Written only when exported depth resolution differs from video
/zedN/pose foxglove.PoseInFrame Optional per-camera pose
/zedN/body raw cvmmap.body_tracking.v1 Optional raw body packets; see mcap_body_tracking.md

nearest is the default bundled policy. It emits one bundle on a common timeline, but it keeps the original per-camera timestamps on /zedN/video, /zedN/depth, and optional /zedN/pose.

strict is still available. In strict mode, bundle membership is thresholded by timestamp skew and --sync-tolerance-ms applies. In nearest mode, --sync-tolerance-ms is not used.

Bundle Manifest Contract

/bundle is the authoritative grouping contract for bundled MCAP files. Consumers should not infer grouping from identical MCAP logTime values or from matching per-camera timestamps.

cvmmap_streamer.BundleManifest contains:

  • timestamp: the nominal bundle timestamp on the common timeline
  • bundle_index: zero-based emitted bundle index
  • policy: BUNDLE_POLICY_NEAREST or BUNDLE_POLICY_STRICT
  • members: one entry per camera label in the bundle

Each bundle member contains:

  • camera_label
  • timestamp: the original camera sample timestamp when a payload is present
  • delta_ns: member.timestamp - bundle.timestamp when a payload is present
  • status
  • corrupted_frames_skipped

Current member statuses:

  • BUNDLE_MEMBER_STATUS_PRESENT
  • BUNDLE_MEMBER_STATUS_CORRUPTED_GAP

CORRUPTED_GAP means the exporter skipped one or more unreadable ZED frames and intentionally emitted no video, depth, or pose payload for that camera for this bundle. corrupted_frames_skipped reports the size of the skipped run that led to the next recovered readable frame.

Timestamp Semantics

The bundled MCAP contract intentionally separates bundle time from per-camera sample time:

  • /bundle.logTime and /bundle.publishTime use the nominal bundle timestamp
  • /zedN/video, /zedN/depth, and /zedN/pose use the original camera sample timestamp
  • calibration and depth-calibration messages use the timestamp of the first emitted sample on that camera

This means a single bundle can legitimately contain different per-camera timestamps, especially in nearest mode.

Corruption And Partial Bundles

Bundled nearest export is resilient to ZED CORRUPTED FRAME runs:

  • unreadable tail frames are treated as end-of-stream
  • mid-stream corruption is skipped until a readable frame is found
  • bundles inside the unreadable gap still exist
  • the affected camera is marked CORRUPTED_GAP in /bundle
  • no /zedN/video, /zedN/depth, or /zedN/pose message is written for that camera for those bundles

Bundled strict export stays strict:

  • corruption is skipped internally until recovery
  • only fully present, threshold-qualified groups are emitted

Validation Expectations

For single-camera MCAP files, the current validation contract is:

  • /camera/video must exist and contain at least one message
  • /camera/depth must exist and contain at least one message
  • /camera/calibration must exist exactly once
  • /camera/video and /camera/depth message counts must match
  • /camera/depth_calibration may appear zero or one time
  • /camera/pose is optional, but it may not outnumber /camera/video

For bundled MCAP files, the current validation contract is:

  • /bundle must exist
  • every bundle must contain one member per camera label
  • for each camera, the count of BUNDLE_MEMBER_STATUS_PRESENT members must match the number of /zedN/video messages
  • for each camera, the count of BUNDLE_MEMBER_STATUS_PRESENT members must match the number of /zedN/depth messages

That is why a partially written MCAP with topic presence but mismatched counts is treated as invalid.

The repository-level Python helper scripts/mcap_bundle_validator.py now understands both layouts and reports which one it found before applying the corresponding validation rules.