Skip to content

Add experimental send_recording python api #9148

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Apr 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ pixi.lock linguist-generated=true
# git-lfs stuff
**/snapshots/**/*.png filter=lfs diff=lfs merge=lfs -text
tests/assets/video/*.mp4 filter=lfs diff=lfs merge=lfs -text
examples/assets/example.rrd filter=lfs diff=lfs merge=lfs -text
24 changes: 22 additions & 2 deletions crates/top/re_sdk/src/recording_stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1696,10 +1696,20 @@ impl RecordingStream {
}
}

/// Records a single [`Chunk`].
/// Logs multiple [`Chunk`]s.
///
/// This will _not_ inject `log_tick` and `log_time` timeline columns into the chunk,
/// for that use [`Self::log_chunk`].
/// for that use [`Self::log_chunks`].
pub fn log_chunks(&self, chunks: impl IntoIterator<Item = Chunk>) {
for chunk in chunks {
self.log_chunk(chunk);
}
}

/// Records a single [`Chunk`].
///
/// Will inject `log_tick` and `log_time` timeline columns into the chunk.
/// If you don't want to inject these, use [`Self::send_chunks`] instead.
#[inline]
pub fn send_chunk(&self, chunk: Chunk) {
let f = move |inner: &RecordingStreamInner| {
Expand All @@ -1711,6 +1721,16 @@ impl RecordingStream {
}
}

/// Records multiple [`Chunk`]s.
///
/// This will _not_ inject `log_tick` and `log_time` timeline columns into the chunk,
/// for that use [`Self::log_chunks`].
pub fn send_chunks(&self, chunks: impl IntoIterator<Item = Chunk>) {
for chunk in chunks {
self.send_chunk(chunk);
}
}

/// Swaps the underlying sink for a new one.
///
/// This guarantees that:
Expand Down
16 changes: 16 additions & 0 deletions docs/snippets/all/concepts/send_recording.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Send a dataframe to a new recording stream."""

import sys

import rerun as rr

path_to_rrd = sys.argv[1]

# NOTE: This is specifically demonstrating how to send `rr.dataframe.Recording` into the viewer.
# If you just want to view an RRD file, use the simpler `rr.log_file()` function instead:
# rr.log_file("path/to/file.rrd", spawn=True)

recording = rr.dataframe.load_recording(path_to_rrd)

rr.init(recording.application_id(), recording_id=recording.recording_id(), spawn=True)
rr.send_recording(recording)
35 changes: 35 additions & 0 deletions docs/snippets/all/concepts/send_recording.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//! Send a `.rrd` to a new recording stream.

use rerun::external::re_chunk_store::{ChunkStore, ChunkStoreConfig};
use rerun::external::re_log_types::{LogMsg, SetStoreInfo};
use rerun::external::re_tuid::Tuid;
use rerun::VersionPolicy;

fn main() -> Result<(), Box<dyn std::error::Error>> {
// Get the filename from the command-line args.
let filename = std::env::args().nth(2).ok_or("Missing filename argument")?;

// Load the chunk store from the file.
let (store_id, store) =
ChunkStore::from_rrd_filepath(&ChunkStoreConfig::DEFAULT, filename, VersionPolicy::Warn)?
.into_iter()
.next()
.ok_or("Expected exactly one recording in the archive")?;

// Use the same app and recording IDs as the original.
if let Some(info) = store.info().cloned() {
let new_recording = rerun::RecordingStreamBuilder::new(info.application_id.clone())
.recording_id(store_id.to_string())
.spawn()?;

new_recording.record_msg(LogMsg::SetStoreInfo(SetStoreInfo {
row_id: Tuid::new(),
info,
}));

Comment on lines +20 to +29
Copy link
Member

@Wumpf Wumpf Mar 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[not actionable] oof this is pretty wild, going way more into Rerun's guts as a would like. Ah well, why not. At least this gives us a great reference point testing the python layer! 👍

// Forward all chunks to the new recording stream.
new_recording.send_chunks(store.iter_chunks().map(|chunk| (**chunk).clone()));
}

Ok(())
}
4 changes: 4 additions & 0 deletions docs/snippets/snippets.toml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ features = [
"cpp",
"rust",
]
"concepts/send_recording" = [ # Not implemented for C++
"cpp",
]
"concepts/static/log_static" = [ # pseudo-code
"py",
"cpp",
Expand Down Expand Up @@ -361,6 +364,7 @@ quick_start = [ # These examples don't have exactly the same implementation.

# `$config_dir` will be replaced with the absolute path of `docs/snippets`.
[extra_args]
"concepts/send_recording" = ["$config_dir/../../tests/assets/rrd/dna.rrd"]
"archetypes/asset3d_simple" = ["$config_dir/../../tests/assets/cube.glb"]
"archetypes/asset3d_out_of_tree" = ["$config_dir/../../tests/assets/cube.glb"]
"archetypes/video_auto_frames" = [
Expand Down
4 changes: 2 additions & 2 deletions examples/assets/example.rrd
Git LFS file not shown
8 changes: 8 additions & 0 deletions rerun_py/rerun_bindings/rerun_bindings.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -881,6 +881,14 @@ def send_blueprint(
) -> None:
"""Send a blueprint to the given recording stream."""

def send_recording(rrd: Recording, recording: Optional[PyRecordingStream] = None) -> None:
"""
Send all chunks from a [`PyRecording`] to the given recording stream.

.. warning::
⚠️ This API is experimental and may change or be removed in future versions! ⚠️
"""

#
# misc
#
Expand Down
1 change: 1 addition & 0 deletions rerun_py/rerun_sdk/rerun/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@
disconnect as disconnect,
save as save,
send_blueprint as send_blueprint,
send_recording as send_recording,
serve_grpc as serve_grpc,
serve_web as serve_web,
spawn as spawn,
Expand Down
19 changes: 19 additions & 0 deletions rerun_py/rerun_sdk/rerun/recording_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import numpy as np
from typing_extensions import deprecated

import rerun as rr
from rerun import bindings
from rerun.memory import MemoryRecording
from rerun.time import reset_time
Expand Down Expand Up @@ -721,6 +722,24 @@ def send_blueprint(

send_blueprint(blueprint=blueprint, make_active=make_active, make_default=make_default, recording=self)

def send_recording(self, recording: rr.dataframe.Recording) -> None:
"""
Send a `Recording` loaded from a `.rrd` to the `RecordingStream`.

.. warning::
⚠️ This API is experimental and may change or be removed in future versions! ⚠️

Parameters
----------
recording:
A `Recording` loaded from a `.rrd`.

"""

from .sinks import send_recording

send_recording(rrd=recording, recording=self)

def spawn(
self,
*,
Expand Down
30 changes: 30 additions & 0 deletions rerun_py/rerun_sdk/rerun/sinks.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import rerun_bindings as bindings

from rerun.blueprint.api import BlueprintLike, create_in_memory_blueprint
from rerun.dataframe import Recording
from rerun.recording_stream import RecordingStream, get_application_id

from ._spawn import _spawn_viewer

Expand Down Expand Up @@ -398,6 +400,34 @@ def send_blueprint(
)


def send_recording(rrd: Recording, recording: RecordingStream | None = None) -> None:
"""
Send a `Recording` loaded from a `.rrd` to the `RecordingStream`.

.. warning::
⚠️ This API is experimental and may change or be removed in future versions! ⚠️

Parameters
----------
rrd:
A recording loaded from a `.rrd` file.
recording:
Specifies the [`rerun.RecordingStream`][] to use.
If left unspecified, defaults to the current active data recording, if there is one.
See also: [`rerun.init`][], [`rerun.set_global_data_recording`][].

"""
application_id = get_application_id(recording=recording) # NOLINT

if application_id is None:
raise ValueError("No application id found. You must call rerun.init before sending a recording.")

bindings.send_recording(
rrd,
recording=recording.to_native() if recording is not None else None,
)


def spawn(
*,
port: int = 9876,
Expand Down
22 changes: 21 additions & 1 deletion rerun_py/src/python_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ impl PyRuntimeErrorExt for PyRuntimeError {

use once_cell::sync::{Lazy, OnceCell};

use crate::dataframe::PyRecording;

// The bridge needs to have complete control over the lifetimes of the individual recordings,
// otherwise all the recording shutdown machinery (which includes deallocating C, Rust and Python
// data and joining a bunch of threads) can end up running at any time depending on what the
Expand Down Expand Up @@ -172,6 +174,7 @@ fn rerun_bindings(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(log_file_from_contents, m)?)?;
m.add_function(wrap_pyfunction!(send_arrow_chunk, m)?)?;
m.add_function(wrap_pyfunction!(send_blueprint, m)?)?;
m.add_function(wrap_pyfunction!(send_recording, m)?)?;

// misc
m.add_function(wrap_pyfunction!(version, m)?)?;
Expand Down Expand Up @@ -1341,6 +1344,23 @@ fn send_blueprint(
}
}

/// Send all chunks from a [`PyRecording`] to the given recording stream.
///
/// .. warning::
/// ⚠️ This API is experimental and may change or be removed in future versions! ⚠️
#[pyfunction]
#[pyo3(signature = (rrd, recording = None))]
fn send_recording(rrd: &PyRecording, recording: Option<&PyRecordingStream>) {
let Some(recording) = get_data_recording(recording) else {
return;
};

let store = rrd.store.read();
for chunk in store.iter_chunks() {
recording.send_chunk((**chunk).clone());
}
}

// --- Misc ---

/// Return a verbose version string.
Expand Down Expand Up @@ -1471,7 +1491,7 @@ fn send_recording_start_time_nanos(

// --- Helpers ---

fn python_version(py: Python<'_>) -> re_log_types::PythonVersion {
pub fn python_version(py: Python<'_>) -> re_log_types::PythonVersion {
let py_version = py.version_info();
re_log_types::PythonVersion {
major: py_version.major,
Expand Down
Loading