Skip to content

Commit 82596eb

Browse files
committed
Add recording to visualizer runtime and commands
+ multibody/Visualizer : `stopRecording()` now returns a flag + runtime : add resetCamera() command
1 parent d2b39b5 commit 82596eb

File tree

7 files changed

+67
-16
lines changed

7 files changed

+67
-16
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717

1818
- CMake : sync jrl-cmakemodules to new release 1.0.0 (https://github.com/Simple-Robotics/candlewick/pull/37)
1919
- CMake : change option `BUILD_PYTHON_BINDINGS` to `BUILD_PYTHON_INTERFACE` (https://github.com/Simple-Robotics/candlewick/pull/37)
20+
- multibody/Visualizer : `stopRecording()` now returns a flag (https://github.com/Simple-Robotics/candlewick/pull/37)
2021

2122
### Fixed
2223

bindings/python/candlewick/async_visualizer.py

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,6 @@ def send_cam_pose(sock: zmq.Socket, M: np.ndarray):
6161
return sock.recv()
6262

6363

64-
def cmd_clean(sock: zmq.Socket):
65-
"""Clean the robot from the renderer. Equivalent to `viz.clean()` on the synchronous `Visualizer` class."""
66-
assert sock.socket_type == zmq.REQ
67-
sock.send_multipart([b"cmd_clean", b""])
68-
return sock.recv()
69-
70-
7164
class AsyncVisualizer(BaseVisualizer):
7265
"""A visualizer-like client for the candlewick runtime."""
7366

@@ -111,6 +104,11 @@ def rebuildData(self):
111104
def setCameraPose(self, pose: np.ndarray):
112105
send_cam_pose(self.sync_sock, pose)
113106

107+
def resetCamera(self):
108+
self.sync_sock.send_multipart([b"reset_camera", b""])
109+
response = self.sync_sock.recv().decode()
110+
assert response == "ok"
111+
114112
def display(self, q: np.ndarray, v: Optional[np.ndarray] = None):
115113
"""Publish the robot state."""
116114
assert q.size == self.model.nq
@@ -119,7 +117,10 @@ def display(self, q: np.ndarray, v: Optional[np.ndarray] = None):
119117
send_state(self.publisher, q, v)
120118

121119
def clean(self):
122-
cmd_clean(self.sync_sock)
120+
"""Clean the robot from the renderer. Equivalent to `viz.clean()` on the synchronous `Visualizer` class."""
121+
self.sync_sock.send_multipart([b"cmd_clean", b""])
122+
response = self.sync_sock.recv().decode()
123+
assert response == "ok"
123124

124125
def close(self):
125126
self.clean()
@@ -155,3 +156,19 @@ def enableCameraControl(self):
155156

156157
def drawFrameVelocities(self, *args, **kwargs):
157158
raise NotImplementedError()
159+
160+
def startRecording(self, filename: str):
161+
"""Open a recording to a given file."""
162+
self.sync_sock.send_multipart([b"start_recording", filename.encode("utf-8")])
163+
response = self.sync_sock.recv().decode("utf-8")
164+
if response != "ok":
165+
print(f"Visualizer runtime error: {response}")
166+
167+
def stopRecording(self):
168+
"""
169+
Stop the recording if it's running.
170+
171+
:returns: Whether a recording was actually stopped.
172+
"""
173+
self.sync_sock.send_multipart([b"stop_recording", b""])
174+
return bool(self.sync_sock.recv())

bindings/python/candlewick/video_context.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
from contextlib import contextmanager
22
from . import Visualizer, hasFfmpegSupport
3+
from typing import Union, TYPE_CHECKING
34
import warnings
45

6+
if TYPE_CHECKING:
7+
from .async_visualizer import AsyncVisualizer
8+
59
_DEFAULT_VIDEO_SETTINGS = {"fps": 30, "bitRate": 3_000_000}
610

711
__all__ = ["create_recorder_context"]
812

913

1014
@contextmanager
1115
def create_recorder_context(
12-
viz: Visualizer,
16+
viz: Union[Visualizer, "AsyncVisualizer"],
1317
filename: str,
1418
/,
1519
fps: int = _DEFAULT_VIDEO_SETTINGS["fps"],
@@ -20,8 +24,9 @@ def create_recorder_context(
2024
"This context will do nothing, as Candlewick was built without video recording support."
2125
)
2226
else:
23-
viz.videoSettings().fps = fps
24-
viz.videoSettings().bitRate = bitRate
27+
if isinstance(viz, Visualizer):
28+
viz.videoSettings().fps = fps
29+
viz.videoSettings().bitRate = bitRate
2530
viz.startRecording(filename)
2631
try:
2732
yield

examples/python/ur3_async_runtime.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import numpy as np
88
import warnings
99

10+
from candlewick import create_recorder_context
1011
from candlewick.async_visualizer import AsyncVisualizer
1112

1213
try:
@@ -47,11 +48,15 @@ def f(i, vel=False):
4748
q = pin.interpolate(model, q0, q1, ph)
4849
client.display(q, _v if vel else None)
4950

50-
for i in range(400):
51-
f(i)
51+
with create_recorder_context(client, "async_record.mp4"):
52+
for i in range(400):
53+
f(i)
5254

5355
# test setting camera
5456
input("[enter] to set camera pose")
5557
Mref = np.eye(4)
5658
Mref[:3, 3] = (0.05, 0, 2.0)
5759
client.setCameraPose(pose=Mref)
60+
61+
input("[enter] to reset camera")
62+
client.resetCamera()

src/candlewick/multibody/Visualizer.cpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,13 +230,16 @@ void Visualizer::startRecording([[maybe_unused]] std::string_view filename) {
230230
#endif
231231
}
232232

233-
void Visualizer::stopRecording() {
233+
bool Visualizer::stopRecording() {
234234
#ifdef CANDLEWICK_WITH_FFMPEG_SUPPORT
235235
if (!m_videoRecorder.isRecording())
236-
return;
236+
return false;
237237
m_currentVideoFilename.clear();
238238
SDL_Log("Wrote %d frames.", m_videoRecorder.frameCounter());
239239
m_videoRecorder.close();
240+
return true;
241+
#else
242+
return false;
240243
#endif
241244
}
242245

src/candlewick/multibody/Visualizer.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,9 @@ class Visualizer final : public BaseVisualizer {
134134

135135
void startRecording(std::string_view filename);
136136

137-
void stopRecording();
137+
/// \brief Stop recording the window.
138+
/// \returns Whether a recording was actually stopped.
139+
bool stopRecording();
138140

139141
#ifdef CANDLEWICK_WITH_FFMPEG_SUPPORT
140142
auto &videoSettings() { return m_videoSettings; }

src/candlewick/runtime/main.cpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ using cdw::multibody::Visualizer;
2525
CANDLEWICK_RUNTIME_DEFINE_COMMAND(send_models);
2626
CANDLEWICK_RUNTIME_DEFINE_COMMAND(state_update);
2727
CANDLEWICK_RUNTIME_DEFINE_COMMAND(send_cam_pose);
28+
CANDLEWICK_RUNTIME_DEFINE_COMMAND(reset_camera);
29+
CANDLEWICK_RUNTIME_DEFINE_COMMAND(start_recording);
30+
CANDLEWICK_RUNTIME_DEFINE_COMMAND(stop_recording);
2831
CANDLEWICK_RUNTIME_DEFINE_COMMAND(clean);
2932

3033
using RowMat4d = Eigen::Matrix<pin::context::Scalar, 4, 4, Eigen::RowMajor>;
@@ -83,12 +86,27 @@ void pull_socket_router(Visualizer &viz, std::span<zmq::message_t, 2> msgs,
8386
auto M = get_eigen_view_from_spec<RowMat4d>(M_msg);
8487
viz.setCameraPose(M);
8588
sync_sock.send(zmq::str_buffer("ok"));
89+
} else if (header == CMD_reset_camera) {
90+
viz.resetCamera();
91+
sync_sock.send(zmq::str_buffer("ok"));
8692
} else if (header == CMD_clean) {
8793
viz.clean();
8894
sync_sock.send(zmq::str_buffer("ok"));
8995
} else if (header == CMD_send_models) {
9096
sync_sock.send(
9197
zmq::str_buffer("error: visualizer already has models open."));
98+
} else if (header == CMD_start_recording) {
99+
auto filename = msgs[1].to_string_view();
100+
try {
101+
viz.startRecording(filename);
102+
sync_sock.send(zmq::str_buffer("ok"));
103+
} catch (const std::runtime_error &err) {
104+
std::string err_msg{err.what()};
105+
sync_sock.send(zmq::message_t(err_msg));
106+
}
107+
} else if (header == CMD_stop_recording) {
108+
char data{viz.stopRecording()};
109+
sync_sock.send(zmq::buffer(&data, 1));
92110
}
93111
}
94112

0 commit comments

Comments
 (0)