Skip to content

feat: merge multiple recording files into one session (multi-mcap, mixed formats)#21

Merged
joaner merged 7 commits into
ioai-tech:mainfrom
joaner:feat/multi-source-merge
Jul 9, 2026
Merged

feat: merge multiple recording files into one session (multi-mcap, mixed formats)#21
joaner merged 7 commits into
ioai-tech:mainfrom
joaner:feat/multi-source-merge

Conversation

@joaner

@joaner joaner commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds support for loading multiple recording files (any mix of .mcap/.bag/.db3/.hdf5/.bvh) into one merged session: topics from every file are concatenated (default "sort by file order"), and the playback range becomes the union of each file's time range.
  • Each file keeps parsing in its own Worker in parallel (no change to per-format worker code); a new main-thread CombinedSourceProxy fans out initialize/message-cursor/backfill/etc. calls and merges the results, so IterablePlayer drives a merged session exactly like a single-file one. Loading a single file is byte-for-byte unchanged (zero regression path).
  • Default interactive behavior: dropping/opening more files while a session is active merges them into that session; nothing loaded yet starts one fresh merged session. Tar extraction and history replay keep today's "open an independent, switchable session" behavior.
  • Embed API (file/files/url/urls/fileManifest) is unchanged by default; a new opt-in mergeSources prop merges them for hosts that want it.
  • Topic list "more" menu shows which file(s) a topic came from (only populated for merged sessions); the sidebar's Data tab shows one row per merged session with member file names and a "N files merged" badge, without otherwise surfacing per-topic provenance in the main list.
  • New fixture generators for multi-mcap and mcap+bag testing, including a fixture derived via the real mcap filter CLI (not just @mcap/core); CI installs the mcap CLI so that fixture always runs there. A minimal ROS1 .bag fixture is committed (rosbags-generated) so bag tests don't need any extra local tooling.
  • Bumped devDependencies (vite/vitest/@playwright/test/etc.) to the latest version within existing semver ranges.

Test plan

  • npm run lint
  • npm run typecheck
  • npm test (571 unit tests, incl. new mergeInitialization/CombinedSourceProxy/CombinedMessageCursor/datasetSources grouping tests)
  • npm run build (SPA) and npm run build:lib (embeddable package)
  • npm run gen:e2e:fixtures && npm run test:e2e (35 Playwright tests, incl. new multi-mcap merge, incremental-add-merges, mixed mcap+bag, and mcap filter-derived fixture cases)
  • Manually verified against a real 21.5s / 11-channel dual-arm recording: split via mcap filter into two files and confirmed the merged session recovers all 11 topics and the original time range

joaner added 7 commits July 9, 2026 15:20
Refresh vite/vitest/@playwright/test and other devDependencies ahead of
the multi-source merge feature work.
…urces

Introduce an ISourceHandle interface that both WorkerSerializedSource
(single file) and the new CombinedSourceProxy (N files/formats fanned
out to independent Workers) implement, so IterablePlayer can drive a
merged multi-source session without any change to its own logic.

- ISourceHandle: structural surface IterablePlayer depends on.
- mergeInitialization: pure merge of N per-source Initialization
  results (topics concatenated in file order, same-name topics folded
  together with accumulated sourceLabels, start/end unioned, topicStats
  merged).
- CombinedMessageCursor: parallel k-way merge of child message cursors
  by receiveTime, tagging each message with its origin for later
  routing.
- CombinedSourceProxy: fans out initialize/getMessageCursor/
  getBackfillMessages/getAdjacentMessage/preparePlaybackBuffer/
  getLoadProgress/getDataQualityReport to only the relevant member
  sources in parallel, and routes resolveMessageBatch/
  resolveMessageForHighFrequencyLane back to the originating member
  (required for per-Worker SharedArrayBuffer payload rings).

TopicInfo gains an optional sourceLabels field (unset for single-file
sessions, so existing UI is unaffected).
Add session grouping on top of the existing dataset list:
- DatasetItem gains an optional groupId (datasetGroupKey/groupDatasets
  helpers); items without one behave exactly as a standalone group of
  one, so single-file loading is byte-for-byte unchanged.
- RosViewerImpl builds one IterablePlayer per active group instead of
  per dataset: a group of one still wires WorkerSerializedSource
  directly; 2+ members are fanned out to independent Workers (in
  parallel, any mix of mcap/bag/db3/hdf5/bvh) and combined through
  CombinedSourceProxy.
- Default interactive behavior: dropping/opening more recording files
  while a session is active merges them into that session (its topic
  list and time range grow); dropping files with nothing loaded starts
  one fresh merged session. Tar extraction and history replay keep
  today's "open an independent, switchable session" behavior.
- Embed API (`file`/`files`/`url`/`urls`/`fileManifest`) is unchanged
  by default; a new opt-in `mergeSources` prop merges them into one
  group for hosts that want it.

AppShell's active-dataset lookup now resolves by group key so
extension context and Navbar/title labeling keep working for merged
sessions.
… and Data tab

- TopicRow's overflow menu shows a "Source: a.mcap, b.mcap" line when a
  topic's sourceLabels are populated (multi-source sessions only; the
  menu is unchanged for single-file sessions).
- Sidebar's Data tab now renders one row per merged session (grouped
  by datasetGroupKey) instead of one row per file: a merged session
  shows "first.name +N", the full member list, and a "{count} files
  merged" badge, while a standalone file renders exactly as before.

Adds sidebar.topicRow.sourceFiles and sidebar.datasetGroup.fileCount
i18n strings (en/zh/ja).
- gen-test-mcap-multi-base.mjs / gen-test-mcap-multi-incremental.mjs:
  a "base recording" (camera + joint_states, 0-5s) and a separately-
  authored "incremental analysis" file (one new topic, 3-7s) that
  mirrors the described external hand-pose-overlay workflow. Loading
  both exercises topic concatenation and the union time range.
- gen-test-mcap-filtered.mjs: derives a third fixture from the base
  file using the real `mcap filter` CLI (not @mcap/core), so at least
  one multi-source test loads a CLI-derived file alongside a
  hand-authored one.
- gen-test-bag.py: writes a minimal ROS1 .bag (Python `rosbags`
  package, since there is no maintained JS/TS ROS1 bag writer) for
  mcap+bag mixed-format tests; verified against the app's own
  @foxglove/rosbag reader.

Both the CLI-derived and bag fixtures self-skip (exit 0, no output
file) when their external tool isn't installed, so `npm run
gen:e2e:fixtures` keeps working for contributors without them; CI
installs both (follow-up commit).
- .github/workflows/ci.yml (e2e job): download the pinned mcap-cli
  Linux binary from GitHub releases so gen-test-mcap-filtered.mjs's
  `mcap filter` step actually runs in CI instead of self-skipping.
- gen-test-bag.py now follows gen-test-hdf5.py's pattern: the ROS1 bag
  is generated once into the committed
  test-fixtures/media/minimal-multi.bag, and gen:e2e:fixtures just
  copies it into public/examples/test_multi.bag. This means CI and
  most contributors never need the `rosbags` Python package at all;
  it's only required to regenerate the source fixture.
- docs/DEVELOPMENT.md: document the new fixtures, the two optional
  external tools and their auto-skip behavior, and a multi-source
  merge acceptance checklist.
Extend tests/multi-sources.spec.ts with:
- Two mcap files opened together merge into one session: topics from
  both files appear, and the playback range becomes the union of each
  file's time range (0-5s + 3-7s -> 0-7s), not either file's own range.
- Dropping a second file onto an already-loaded session extends it
  in place (topics/range grow) instead of creating a separate,
  switchable dataset; the Data tab shows exactly one merged-session
  row.
- The topic "more" menu surfaces which file a topic came from once
  sessions are merged.
- mcap + bag mixed-format merge, and a file derived via the real
  `mcap filter` CLI merging with a hand-authored one; both self-skip
  when their optional fixture is unavailable.

Adds a `playback-time-line` test id to PlaybackBar so tests can assert
the merged/union time range shown next to the scrubber.
@joaner joaner merged commit 6d51145 into ioai-tech:main Jul 9, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant