Skip to content

Commit 18d0fec

Browse files
committed
Refactor mcap recording
1 parent 129524a commit 18d0fec

File tree

52 files changed

+1279
-1937
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1279
-1937
lines changed

examples/oxr/python/modular_example_with_mcap.py

Lines changed: 35 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,13 @@
88
- Create independent trackers
99
- Add only the trackers you need
1010
- Record all tracker data to an MCAP file for playback/analysis
11-
- McapRecorder.create() is similar to DeviceIOSession.run()
11+
- Pass mcap_filename and mcap_channels to DeviceIOSession.run() to enable recording
1212
"""
1313

1414
import sys
1515
import time
1616
from datetime import datetime
1717
import isaacteleop.deviceio as deviceio
18-
import isaacteleop.mcap as mcap
1918
import isaacteleop.oxr as oxr
2019

2120

@@ -50,55 +49,42 @@ def main():
5049
handles = oxr_session.get_handles()
5150
print("✓ OpenXR session created")
5251

53-
# Run deviceio session with trackers
54-
print("\nRunning deviceio session...")
55-
with deviceio.DeviceIOSession.run(trackers, handles) as session:
52+
# Run deviceio session with MCAP recording enabled.
53+
print("\nRunning deviceio session with MCAP recording...")
54+
recording_config = deviceio.McapRecordingConfig(
55+
mcap_filename, [(hand_tracker, "hands"), (head_tracker, "head")]
56+
)
57+
with deviceio.DeviceIOSession.run(
58+
trackers, handles, recording_config
59+
) as session:
5660
print("✓ DeviceIO session initialized with all trackers!")
61+
print(f"✓ MCAP recording active → {mcap_filename}")
62+
print()
5763

58-
with mcap.McapRecorder.create(
59-
mcap_filename,
60-
[
61-
(hand_tracker, "hands"),
62-
(head_tracker, "head"),
63-
],
64-
) as recorder:
65-
print("✓ MCAP recording started!")
66-
print()
67-
68-
# Main tracking loop
69-
print("=" * 60)
70-
print("Tracking (60 seconds)...")
71-
print("=" * 60)
72-
print()
73-
74-
frame_count = 0
75-
start_time = time.time()
76-
77-
while time.time() - start_time < 30.0:
78-
# Update session and all trackers
79-
if not session.update():
80-
print("Update failed")
81-
break
82-
83-
# Record all registered trackers
84-
recorder.record(session)
85-
86-
# Print every 60 frames (~1 second)
87-
if frame_count % 60 == 0:
88-
elapsed = time.time() - start_time
89-
print(f"[{elapsed:4.1f}s] Frame {frame_count} (recording...)")
90-
print()
91-
92-
frame_count += 1
93-
time.sleep(0.016) # ~60 FPS
94-
95-
# Cleanup
96-
print(f"\nProcessed {frame_count} frames")
97-
print("Cleaning up (RAII)...")
98-
99-
print("✓ Recording stopped")
100-
101-
print("✓ DeviceIO session cleaned up")
64+
# Main tracking loop
65+
print("=" * 60)
66+
print("Tracking (30 seconds)...")
67+
print("=" * 60)
68+
print()
69+
70+
frame_count = 0
71+
start_time = time.time()
72+
73+
while time.time() - start_time < 30.0:
74+
session.update()
75+
76+
# Print every 60 frames (~1 second)
77+
if frame_count % 60 == 0:
78+
elapsed = time.time() - start_time
79+
print(f"[{elapsed:4.1f}s] Frame {frame_count} (recording...)")
80+
print()
81+
82+
frame_count += 1
83+
time.sleep(0.016) # ~60 FPS
84+
85+
print(f"\nProcessed {frame_count} frames")
86+
87+
print("✓ Recording stopped (MCAP file closed by session destructor)")
10288

10389
print()
10490
print("=" * 60)

examples/oxr/python/test_oak_camera.py

Lines changed: 47 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424

2525
import isaacteleop.plugin_manager as pm
2626
import isaacteleop.deviceio as deviceio
27-
import isaacteleop.mcap as mcap
2827
import isaacteleop.oxr as oxr
2928

3029
PLUGIN_ROOT_DIR = Path(__file__).resolve().parent.parent.parent.parent / "plugins"
@@ -66,62 +65,53 @@ def _run_schema_pusher(
6665
handles = oxr_session.get_handles()
6766
print(" ✓ OpenXR session created")
6867

69-
# Create DeviceIOSession with all trackers
70-
with deviceio.DeviceIOSession.run([tracker], handles) as session:
71-
print(" ✓ DeviceIO session initialized")
72-
73-
# Create MCAP recorder with per-stream FrameMetadataOak channels
74-
mcap_entries = [(tracker, "oak_metadata")]
75-
with mcap.McapRecorder.create(
76-
mcap_filename,
77-
mcap_entries,
78-
) as recorder:
79-
print(" ✓ MCAP recording started")
80-
print()
81-
82-
# 6. Main tracking loop
83-
print(f"[Step 6] Recording video and metadata ({duration} seconds)...")
84-
print("-" * 80)
85-
start_time = time.time()
86-
frame_count = 0
87-
last_print_time = 0
88-
last_seq = dict.fromkeys(stream_names, -1)
89-
metadata_samples = dict.fromkeys(stream_names, 0)
90-
91-
while time.time() - start_time < duration:
92-
plugin.check_health()
93-
if not session.update():
94-
print(" Warning: Session update failed")
95-
continue
96-
recorder.record(session)
97-
frame_count += 1
98-
99-
elapsed = time.time() - start_time
100-
for idx, name in enumerate(stream_names):
101-
tracked = tracker.get_stream_data(session, idx)
102-
if (
103-
tracked.data is not None
104-
and tracked.data.sequence_number != last_seq.get(name, -1)
105-
):
106-
metadata_samples[name] = metadata_samples.get(name, 0) + 1
107-
last_seq[name] = tracked.data.sequence_number
108-
109-
if int(elapsed) > last_print_time:
110-
last_print_time = int(elapsed)
111-
parts = []
112-
for name in stream_names:
113-
parts.append(f"{name}={metadata_samples.get(name, 0)}")
114-
print(f" [{last_print_time:3d}s] samples: {', '.join(parts)}")
115-
time.sleep(0.016)
116-
117-
print("-" * 80)
118-
print()
119-
print(f" ✓ Recording completed ({duration:.1f} seconds)")
120-
print(f" ✓ Processed {frame_count} update cycles")
121-
for name in stream_names:
122-
print(
123-
f" ✓ {name}: {metadata_samples.get(name, 0)} metadata samples"
124-
)
68+
recording_config = deviceio.McapRecordingConfig(
69+
mcap_filename, [(tracker, "oak_metadata")]
70+
)
71+
with deviceio.DeviceIOSession.run(
72+
[tracker], handles, recording_config
73+
) as session:
74+
print(" ✓ DeviceIO session initialized (recording active during update())")
75+
print()
76+
77+
print(f"[Step 6] Recording video and metadata ({duration} seconds)...")
78+
print("-" * 80)
79+
start_time = time.time()
80+
frame_count = 0
81+
last_print_time = 0
82+
last_seq = dict.fromkeys(stream_names, -1)
83+
metadata_samples = dict.fromkeys(stream_names, 0)
84+
85+
while time.time() - start_time < duration:
86+
plugin.check_health()
87+
session.update()
88+
frame_count += 1
89+
90+
elapsed = time.time() - start_time
91+
for idx, name in enumerate(stream_names):
92+
tracked = tracker.get_stream_data(session, idx)
93+
if (
94+
tracked.data is not None
95+
and tracked.data.sequence_number != last_seq.get(name, -1)
96+
):
97+
metadata_samples[name] = metadata_samples.get(name, 0) + 1
98+
last_seq[name] = tracked.data.sequence_number
99+
100+
if int(elapsed) > last_print_time:
101+
last_print_time = int(elapsed)
102+
parts = [
103+
f"{name}={metadata_samples.get(name, 0)}"
104+
for name in stream_names
105+
]
106+
print(f" [{last_print_time:3d}s] samples: {', '.join(parts)}")
107+
time.sleep(0.016)
108+
109+
print("-" * 80)
110+
print()
111+
print(f" ✓ Recording completed ({duration:.1f} seconds)")
112+
print(f" ✓ Processed {frame_count} update cycles")
113+
for name in stream_names:
114+
print(f" ✓ {name}: {metadata_samples.get(name, 0)} metadata samples")
125115

126116

127117
def run_test(duration: float = 10.0, mode: str = MODE_NO_METADATA):

src/core/deviceio_base/cpp/inc/deviceio_base/tracker.hpp

Lines changed: 8 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44
#pragma once
55

66
#include <openxr/openxr.h>
7-
#include <schema/timestamp_generated.h>
87

9-
#include <functional>
108
#include <memory>
119
#include <string>
1210
#include <string_view>
@@ -19,62 +17,14 @@ namespace core
1917
struct OpenXRSessionHandles;
2018
class ITrackerFactory;
2119

22-
// Base interface for tracker implementations
23-
// These are the actual worker objects that get updated by the session
20+
// Base interface for tracker implementations.
21+
// The actual worker objects updated each frame by DeviceIOSession.
2422
class ITrackerImpl
2523
{
2624
public:
2725
virtual ~ITrackerImpl() = default;
2826

29-
// Update the tracker with the current time
3027
virtual bool update(XrTime time) = 0;
31-
32-
/**
33-
* @brief Callback type for serialize_all.
34-
*
35-
* Receives (log_time_ns, data_ptr, data_size) for each serialized record.
36-
*
37-
* @param log_time_ns Monotonic nanoseconds used as the MCAP logTime/publishTime
38-
* for this record. This is the time at which the recording
39-
* system processed the record (update-tick time), not the
40-
* sample capture time. The full per-sample DeviceDataTimestamp
41-
* (including sample_time and raw_device_time) is embedded
42-
* inside the serialized FlatBuffer payload.
43-
*
44-
* @warning The data_ptr and data_size are only valid for the duration of the
45-
* callback invocation. The buffer is owned by a FlatBufferBuilder
46-
* local to the tracker's serialize_all implementation and will be
47-
* destroyed on return. If you need the bytes after the callback
48-
* returns, copy them into your own storage before returning.
49-
*/
50-
using RecordCallback = std::function<void(int64_t log_time_ns, const uint8_t*, size_t)>;
51-
52-
/**
53-
* @brief Serialize all records accumulated since the last update() call.
54-
*
55-
* Each call to update() clears the previous batch and accumulates a fresh
56-
* set of records (one for OpenXR-direct trackers; potentially many for
57-
* SchemaTracker-based tensor-device trackers). serialize_all emits every
58-
* record in that batch via the callback.
59-
*
60-
* @note For multi-channel trackers the recorder calls serialize_all once per
61-
* channel index (channel_index = 0, 1, … N-1) after each update().
62-
* All serialize_all calls for a given update() are guaranteed to
63-
* complete before the next update() is issued. Implementations may
64-
* therefore maintain a single shared pending batch and clear it at the
65-
* start of the next update(); there is no need to track per-channel
66-
* drain state.
67-
*
68-
* For read access without MCAP recording, use the tracker's typed get_*()
69-
* accessors, which always reflect the last record in the current batch.
70-
*
71-
* @note The buffer pointer passed to the callback is only valid for the
72-
* duration of that callback call. Copy if you need it beyond return.
73-
*
74-
* @param channel_index Which record channel to serialize (0-based).
75-
* @param callback Invoked once per record with (timestamp, data_ptr, data_size).
76-
*/
77-
virtual void serialize_all(size_t channel_index, const RecordCallback& callback) const = 0;
7828
};
7929

8030
/**
@@ -100,8 +50,8 @@ class ITrackerSession
10050
virtual const ITrackerImpl& get_tracker_impl(const class ITracker& tracker) const = 0;
10151
};
10252

103-
// Base interface for all trackers
104-
// PUBLIC API: Only exposes methods that external users should call
53+
// Base interface for all trackers.
54+
// Public API: configuration, extension requirements, and typed data accessors.
10555
class ITracker
10656
{
10757
public:
@@ -110,48 +60,15 @@ class ITracker
11060
virtual std::vector<std::string> get_required_extensions() const = 0;
11161
virtual std::string_view get_name() const = 0;
11262

113-
/**
114-
* @brief Get the FlatBuffer schema name (root type) for MCAP recording.
115-
*
116-
* This should return the fully qualified FlatBuffer type name (e.g., "core.HandPose")
117-
* which matches the root_type defined in the .fbs schema file.
118-
*/
119-
virtual std::string_view get_schema_name() const = 0;
120-
121-
/**
122-
* @brief Get the binary FlatBuffer schema text for MCAP recording.
123-
*/
124-
virtual std::string_view get_schema_text() const = 0;
125-
126-
/**
127-
* @brief Get the channel names for MCAP recording.
128-
*
129-
* Every tracker must return at least one non-empty channel name. The returned
130-
* vector size determines how many times serialize_all() is called per update,
131-
* with the vector index used as the channel_index argument.
132-
*
133-
* Single-channel trackers return one name (e.g. {"head"}).
134-
* Multi-channel trackers return multiple (e.g. {"left_hand", "right_hand"}).
135-
*
136-
* The MCAP recorder combines each channel name with the base channel name
137-
* provided at registration as "base_name/channel_name". For example, a
138-
* single-channel head tracker registered with base name "tracking" produces
139-
* the MCAP channel "tracking/head". A multi-channel hand tracker registered
140-
* with base name "hands" produces "hands/left_hand" and "hands/right_hand".
141-
*
142-
* @return Non-empty vector of non-empty channel name strings.
143-
*/
144-
virtual std::vector<std::string> get_record_channels() const = 0;
145-
14663
/**
14764
* @brief Create the tracker's implementation via the provided factory.
14865
*
14966
* Uses double dispatch: the tracker calls the factory method specific to its
150-
* type (e.g., factory.create_head_tracker_impl()), so the factory controls
151-
* which concrete impl is constructed without the tracker needing to know the
152-
* session type.
67+
* concrete type. The factory (e.g. LiveDeviceIOFactory) owns the optional
68+
* MCAP writer and creates typed McapTrackerChannels for each impl that
69+
* should record.
15370
*
154-
* @param factory Session-provided factory (e.g., LiveDeviceIOFactory).
71+
* @param factory Session-provided factory.
15572
* @return Owning pointer to the newly created impl.
15673
*/
15774
virtual std::unique_ptr<ITrackerImpl> create_tracker_impl(ITrackerFactory& factory) const = 0;

src/core/deviceio_session/cpp/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ target_link_libraries(deviceio_session
2424
PRIVATE
2525
deviceio::deviceio_trackers
2626
deviceio::live_trackers
27+
mcap::mcap
2728
)
2829

2930
add_library(deviceio::deviceio_session ALIAS deviceio_session)

0 commit comments

Comments
 (0)