Skip to content

Commit c77465f

Browse files
committed
Refactor mcap recording
1 parent b206e5f commit c77465f

35 files changed

+660
-1600
lines changed

examples/oxr/python/modular_example_with_mcap.py

Lines changed: 38 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,45 @@ 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+
with deviceio.DeviceIOSession.run(
55+
trackers,
56+
handles,
57+
mcap_filename=mcap_filename,
58+
mcap_channels=[(hand_tracker, "hands"), (head_tracker, "head")],
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+
# update() drives tracking AND writes to MCAP automatically
75+
if not session.update():
76+
print("Update failed")
77+
break
78+
79+
# Print every 60 frames (~1 second)
80+
if frame_count % 60 == 0:
81+
elapsed = time.time() - start_time
82+
print(f"[{elapsed:4.1f}s] Frame {frame_count} (recording...)")
83+
print()
84+
85+
frame_count += 1
86+
time.sleep(0.016) # ~60 FPS
87+
88+
print(f"\nProcessed {frame_count} frames")
89+
90+
print("✓ Recording stopped (MCAP file closed by session destructor)")
10291

10392
print()
10493
print("=" * 60)

examples/oxr/python/test_oak_camera.py

Lines changed: 51 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,57 @@ 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+
# Create DeviceIOSession with MCAP recording enabled.
69+
with deviceio.DeviceIOSession.run(
70+
[tracker],
71+
handles,
72+
mcap_filename=mcap_filename,
73+
mcap_channels=[(tracker, "oak_metadata")],
74+
) as session:
75+
print(" ✓ DeviceIO session initialized (recording active during update())")
76+
print()
77+
78+
print(f"[Step 6] Recording video and metadata ({duration} seconds)...")
79+
print("-" * 80)
80+
start_time = time.time()
81+
frame_count = 0
82+
last_print_time = 0
83+
last_seq = dict.fromkeys(stream_names, -1)
84+
metadata_samples = dict.fromkeys(stream_names, 0)
85+
86+
while time.time() - start_time < duration:
87+
plugin.check_health()
88+
if not session.update():
89+
print(" Warning: Session update failed")
90+
continue
91+
frame_count += 1
92+
93+
elapsed = time.time() - start_time
94+
for idx, name in enumerate(stream_names):
95+
tracked = tracker.get_stream_data(session, idx)
96+
if (
97+
tracked.data is not None
98+
and tracked.data.sequence_number != last_seq.get(name, -1)
99+
):
100+
metadata_samples[name] = metadata_samples.get(name, 0) + 1
101+
last_seq[name] = tracked.data.sequence_number
102+
103+
if int(elapsed) > last_print_time:
104+
last_print_time = int(elapsed)
105+
parts = []
106+
for name in stream_names:
107+
parts.append(f"{name}={metadata_samples.get(name, 0)}")
108+
print(f" [{last_print_time:3d}s] samples: {', '.join(parts)}")
109+
time.sleep(0.016)
110+
111+
print("-" * 80)
112+
print()
113+
print(f" ✓ Recording completed ({duration:.1f} seconds)")
114+
print(f" ✓ Processed {frame_count} update cycles")
115+
for name in stream_names:
116+
print(
117+
f" ✓ {name}: {metadata_samples.get(name, 0)} metadata samples"
118+
)
125119

126120

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

src/core/deviceio/cpp/CMakeLists.txt

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,12 @@
33

44
cmake_minimum_required(VERSION 3.20)
55

6-
# If this is being built standalone, declare a project
76
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
87
project(deviceio_core VERSION 1.0.0 LANGUAGES CXX)
98
set(CMAKE_CXX_STANDARD 17)
109
set(CMAKE_CXX_STANDARD_REQUIRED ON)
1110
endif()
1211

13-
# DEVICEIO only depends on oxr_utils (header-only library with OpenXR types)
14-
# It does NOT depend on or link to the OpenXR loader
1512
if(NOT TARGET oxr::oxr_utils)
1613
message(FATAL_ERROR "oxr_utils target not found. Make sure oxr_utils is built first.")
1714
endif()
@@ -43,31 +40,25 @@ add_library(deviceio_core STATIC
4340
inc/deviceio/live_frame_metadata_tracker_oak_impl.hpp
4441
inc/deviceio/deviceio_session.hpp
4542
)
43+
4644
target_include_directories(deviceio_core
4745
INTERFACE
4846
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/inc>
4947
)
5048

5149
target_link_libraries(deviceio_core
5250
PUBLIC
53-
# oxr_utils is header-only and provides OpenXRSessionHandles struct
54-
# It also brings in OpenXR headers for base types without linking to OpenXR loader
5551
oxr::oxr_utils
56-
57-
# Schema library for FlatBuffer types
5852
isaacteleop_schema
59-
60-
# OpenXR extensions for tensor data APIs (used by SchemaTracker)
6153
Teleop::openxr_extensions
54+
PRIVATE
55+
# mcap_core provides McapTrackerChannels helper + transitively mcap::mcap headers.
56+
# Linking is PRIVATE: mcap types don't appear in deviceio's public API.
57+
mcap::mcap_core
6258
)
6359

64-
# DEVICEIO does NOT link to OpenXR loader - it's completely standalone
65-
# All OpenXR functions are loaded dynamically via xrGetInstanceProcAddr at runtime
66-
67-
# Create alias for consistent naming
6860
add_library(deviceio::deviceio_core ALIAS deviceio_core)
6961

70-
# Optional: Install for distribution
7162
install(TARGETS deviceio_core
7263
ARCHIVE DESTINATION lib
7364
INCLUDES DESTINATION include

src/core/deviceio/cpp/controller_tracker.cpp

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,9 @@ XrAction create_action(const OpenXRCoreFunctions& funcs,
149149
// LiveControllerTrackerImpl
150150
// ============================================================================
151151

152-
LiveControllerTrackerImpl::LiveControllerTrackerImpl(const OpenXRSessionHandles& handles)
152+
LiveControllerTrackerImpl::LiveControllerTrackerImpl(const OpenXRSessionHandles& handles,
153+
mcap::McapWriter* writer,
154+
std::string_view base_name)
153155
: core_funcs_(OpenXRCoreFunctions::load(handles.instance, handles.xrGetInstanceProcAddr)),
154156
time_converter_(handles),
155157
session_(handles.session),
@@ -281,6 +283,12 @@ LiveControllerTrackerImpl::LiveControllerTrackerImpl(const OpenXRSessionHandles&
281283
}
282284

283285
std::cout << "ControllerTracker initialized (left + right) with action context" << std::endl;
286+
287+
mcap_channels_ =
288+
McapTrackerChannels(writer, base_name, "core.ControllerSnapshotRecord",
289+
std::string_view(reinterpret_cast<const char*>(ControllerSnapshotRecordBinarySchema::data()),
290+
ControllerSnapshotRecordBinarySchema::size()),
291+
{ "left_controller", "right_controller" });
284292
}
285293

286294
bool LiveControllerTrackerImpl::update(XrTime time)
@@ -377,6 +385,29 @@ bool LiveControllerTrackerImpl::update(XrTime time)
377385
update_controller(left_hand_path_, left_grip_space_, left_aim_space_, left_tracked_);
378386
update_controller(right_hand_path_, right_grip_space_, right_aim_space_, right_tracked_);
379387

388+
if (mcap_channels_.active())
389+
{
390+
int64_t monotonic_ns = time_converter_.convert_xrtime_to_monotonic_ns(last_update_time_);
391+
DeviceDataTimestamp timestamp(monotonic_ns, monotonic_ns, last_update_time_);
392+
393+
auto write_controller = [&](size_t channel_index, const ControllerSnapshotTrackedT& tracked)
394+
{
395+
flatbuffers::FlatBufferBuilder builder(256);
396+
ControllerSnapshotRecordBuilder record_builder(builder);
397+
if (tracked.data)
398+
{
399+
auto data_offset = ControllerSnapshot::Pack(builder, tracked.data.get());
400+
record_builder.add_data(data_offset);
401+
}
402+
record_builder.add_timestamp(&timestamp);
403+
builder.Finish(record_builder.Finish());
404+
mcap_channels_.write(channel_index, monotonic_ns, builder.GetBufferPointer(), builder.GetSize());
405+
};
406+
407+
write_controller(0, left_tracked_);
408+
write_controller(1, right_tracked_);
409+
}
410+
380411
return left_tracked_.data || right_tracked_.data;
381412
}
382413

@@ -390,31 +421,6 @@ const ControllerSnapshotTrackedT& LiveControllerTrackerImpl::get_right_controlle
390421
return right_tracked_;
391422
}
392423

393-
void LiveControllerTrackerImpl::serialize_all(size_t channel_index, const RecordCallback& callback) const
394-
{
395-
if (channel_index > 1)
396-
{
397-
throw std::runtime_error("ControllerTracker::serialize_all: invalid channel_index " +
398-
std::to_string(channel_index) + " (must be 0 or 1)");
399-
}
400-
flatbuffers::FlatBufferBuilder builder(256);
401-
402-
const auto& tracked = (channel_index == 0) ? left_tracked_ : right_tracked_;
403-
int64_t monotonic_ns = time_converter_.convert_xrtime_to_monotonic_ns(last_update_time_);
404-
DeviceDataTimestamp timestamp(monotonic_ns, monotonic_ns, last_update_time_);
405-
406-
ControllerSnapshotRecordBuilder record_builder(builder);
407-
if (tracked.data)
408-
{
409-
auto data_offset = ControllerSnapshot::Pack(builder, tracked.data.get());
410-
record_builder.add_data(data_offset);
411-
}
412-
record_builder.add_timestamp(&timestamp);
413-
builder.Finish(record_builder.Finish());
414-
415-
callback(monotonic_ns, builder.GetBufferPointer(), builder.GetSize());
416-
}
417-
418424
// ============================================================================
419425
// ControllerTracker Public Interface
420426
// ============================================================================
@@ -424,12 +430,6 @@ std::vector<std::string> ControllerTracker::get_required_extensions() const
424430
return { XR_NVX1_ACTION_CONTEXT_EXTENSION_NAME };
425431
}
426432

427-
std::string_view ControllerTracker::get_schema_text() const
428-
{
429-
return std::string_view(reinterpret_cast<const char*>(ControllerSnapshotRecordBinarySchema::data()),
430-
ControllerSnapshotRecordBinarySchema::size());
431-
}
432-
433433
const ControllerSnapshotTrackedT& ControllerTracker::get_left_controller(const DeviceIOSession& session) const
434434
{
435435
return static_cast<const ControllerTrackerImpl&>(session.get_tracker_impl(*this)).get_left_controller();
@@ -442,7 +442,7 @@ const ControllerSnapshotTrackedT& ControllerTracker::get_right_controller(const
442442

443443
std::unique_ptr<ITrackerImpl> ControllerTracker::create_tracker_impl(TrackerFactory& factory) const
444444
{
445-
return factory.create_controller_tracker_impl();
445+
return factory.create_controller_tracker_impl(this);
446446
}
447447

448448
} // namespace core

0 commit comments

Comments
 (0)