Recording video#
This guide shows how to record mp4 clips from a visualizer or a scene sensor during a run.
Recorded clip of the Shadow Hand cube-reorientation task, recording the streaming view of the Rerun visualizer with 4 streaming view envs and 4 GT types
Each VideoRecorderCfg entry is independent: different sources write different files at their own
cadence, with no limit on simultaneous recorders.
Quick Start#
Add a VideoRecorderCfg to env_cfg.video_recorders:
from isaaclab.envs.utils.video_recorder_cfg import VideoRecorderCfg
env_cfg.video_recorders = [
VideoRecorderCfg(source="viz:kit", output_dir="videos/")
]
source also accepts an ImageViewCfg: use VideoRecorderCfg(source=view) to share an image
selection and frame with a window. See Streaming a camera view in a visualizer for sensor and
perspective examples.
Or pass --video [SOURCE] on the command line to record without editing the environment config. --video
adds a single recorder; to record several sources at once, list one VideoRecorderCfg per source in
env_cfg.video_recorders, which then takes precedence over the --video source (--video_length and
--video_interval still apply to every recorder):
uv run isaaclab train --rl_library rsl_rl --task Isaac-Cartpole --video
--video records from the source it names, and --viz still decides which visualizers open a
window. See the –video sources for every command-line form, and Sources and Clip control
below for the VideoRecorderCfg options. To record from a different camera angle than the
interactive view, see Recording from an independent camera angle.
Examples#
All three examples use the Shadow Hand cube-reorientation task,
Isaac-Reorient-Cube-Shadow-Camera-Direct, which ships with a built-in tiled camera
sensor. Examples 1 and 2 each demonstrate one recording source; Example 3 combines all four.
Code for run_video_recording.py
1# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
2# All rights reserved.
3#
4# SPDX-License-Identifier: BSD-3-Clause
5
6"""Tutorial: recording video from visualizers and scene sensors.
7
8This script demonstrates three progressively richer recording configurations,
9all using the Shadow Hand cube-reorientation task
10(``Isaac-Reorient-Cube-Shadow-Camera-Direct``).
11
12Example 1 — Kit viewport (simplest)
13 One clip from the Kit interactive viewport, showing 4 parallel environments.
14
15 .. code-block:: bash
16
17 uv run python scripts/tutorials/07_visualizers/run_video_recording.py \
18 --example 1 --num_envs 4 --viz kit
19
20Example 2 — scene sensor only, headless
21 One clip captured directly from the scene's tiled-camera sensor.
22 No visualizer window opens; the sensor renders offline.
23
24 .. code-block:: bash
25
26 uv run python scripts/tutorials/07_visualizers/run_video_recording.py \
27 --example 2 --num_envs 16
28
29Example 3 — Kit viewport + Kit tiled grid + Newton viewport + scene sensor
30 Four independent clip streams recorded simultaneously.
31
32 .. code-block:: bash
33
34 uv run python scripts/tutorials/07_visualizers/run_video_recording.py \
35 --example 3 --num_envs 4 --viz kit,newton_gl
36
37Clips are written to ``videos/recording_tutorial/example_<N>/`` in the working directory.
38Examples 1 and 2 each demonstrate one recording source; Example 3 combines all of them.
39``--viz`` selects the visualizers to record from; the configs below only set their camera and panels.
40"""
41
42from __future__ import annotations
43
44import argparse
45import contextlib
46import os
47import sys
48
49import gymnasium as gym
50import torch
51
52from isaaclab.visualizers import WindowCfg
53
54import isaaclab_tasks # noqa: F401
55
56with contextlib.suppress(ImportError):
57 import isaaclab_tasks_experimental # noqa: F401
58
59from isaaclab.app import add_launcher_args, launch_simulation
60from isaaclab.envs.utils.video_recorder_cfg import VideoRecorderCfg
61
62from isaaclab_tasks.utils import resolve_task_config, setup_preset_cli
63
64# ---------------------------------------------------------------------------
65# Constants
66# ---------------------------------------------------------------------------
67
68_VIDEO_LENGTH = 100 # env steps per clip
69_NUM_STEPS = 115 # slightly more than _VIDEO_LENGTH so the clip flushes cleanly
70
71_TASK_SHADOW = "Isaac-Reorient-Cube-Shadow-Camera-Direct"
72
73# Kit viewport camera: positioned to show a 2×2 grid of Shadow Hand environments.
74# env_spacing=1.0 with 4 envs → envs centered at ±0.5 in x and y.
75# Cube spawns at ~(0, -0.39, 0.6) per env; wrist cylinder is the landmark at the top.
76_SHADOW_EYE = (0.0, -2.2, 1.8)
77_SHADOW_LOOKAT = (0.0, -0.1, 0.4)
78_SHADOW_ENV_SPACING = 1.0
79
80# Skip the first few steps so the RTX renderer has warmed up before recording starts.
81_KIT_STEP_OFFSET = 5
82
83
84def _output_dir(example: int) -> str:
85 return os.path.join("videos", "recording_tutorial", f"example_{example}")
86
87
88def _shadow_env_cfg(num_envs: int, env_spacing: float = _SHADOW_ENV_SPACING):
89 """Build a base Shadow Hand camera env cfg shared by all examples."""
90 env_cfg, _ = resolve_task_config(_TASK_SHADOW, "", overrides=(*sys.argv[1:], "env.scene.tiled_camera=rgb"))
91 env_cfg.scene.tiled_camera.height = 256
92 env_cfg.scene.tiled_camera.width = 256
93 env_cfg.scene.num_envs = num_envs
94 env_cfg.scene.env_spacing = env_spacing
95 return env_cfg
96
97
98# ---------------------------------------------------------------------------
99# Per-example environment config builders
100# ---------------------------------------------------------------------------
101
102
103def _build_env_cfg_example_1(num_envs: int):
104 """Shadow Hand + Kit viewport: one clip from the interactive viewport."""
105 from isaaclab_visualizers.kit import KitVisualizerCfg
106
107 env_cfg = _shadow_env_cfg(num_envs)
108 env_cfg.sim.visualizer_cfgs = [KitVisualizerCfg(eye=_SHADOW_EYE, lookat=_SHADOW_LOOKAT)]
109
110 out = _output_dir(1)
111 env_cfg.video_recorders = [
112 VideoRecorderCfg(
113 source="viz:kit",
114 output_dir=out,
115 output_filename_prefix="kit_viewport",
116 video_length=_VIDEO_LENGTH,
117 fps=30,
118 step_offset=_KIT_STEP_OFFSET,
119 ),
120 ]
121 return env_cfg, _TASK_SHADOW
122
123
124def _build_env_cfg_example_2(num_envs: int):
125 """Shadow Hand + headless: scene tiled-camera sensor clip only."""
126 env_cfg = _shadow_env_cfg(num_envs, env_spacing=2.0)
127
128 out = _output_dir(2)
129 env_cfg.video_recorders = [
130 VideoRecorderCfg(
131 source="sensor:tiled_camera",
132 output_dir=out,
133 output_filename_prefix="sensor",
134 video_length=_VIDEO_LENGTH,
135 fps=30,
136 ),
137 ]
138 return env_cfg, _TASK_SHADOW
139
140
141def _build_env_cfg_example_3(num_envs: int):
142 """Shadow Hand + Kit viewport + Kit tiled grid + Newton viewport + sensor: four simultaneous streams.
143
144 Note: ``source='viz:newton_gl'`` captures the full Newton GL window. When
145 ``streaming_view=True`` is set on :class:`~isaaclab_visualizers.newton.NewtonGLVisualizerCfg`,
146 the GL window displays the per-environment camera panel, so this effectively records
147 a Newton streaming view without a separate ``render_tiled_rgb_array()`` call.
148 """
149 from isaaclab_visualizers.kit import KitVisualizerCfg
150 from isaaclab_visualizers.newton import NewtonGLVisualizerCfg
151
152 env_cfg = _shadow_env_cfg(num_envs)
153 kit_cfg = KitVisualizerCfg(
154 eye=_SHADOW_EYE,
155 lookat=_SHADOW_LOOKAT,
156 streaming_view=True,
157 streaming_envs=min(num_envs, 16),
158 # No streaming_sensor_prim_path: selects the existing
159 # tiled_camera sensor automatically, so the streaming panel shows the same
160 # RTX-rendered views as source="sensor:tiled_camera".
161 )
162 newton_cfg = NewtonGLVisualizerCfg(
163 eye=_SHADOW_EYE,
164 lookat=_SHADOW_LOOKAT,
165 window=WindowCfg(size=(1280, 720)),
166 focal_length=25.0,
167 )
168 env_cfg.sim.visualizer_cfgs = [kit_cfg, newton_cfg]
169
170 out = _output_dir(3)
171 env_cfg.video_recorders = [
172 VideoRecorderCfg(
173 source="viz:kit",
174 output_dir=out,
175 output_filename_prefix="kit_viewport",
176 video_length=_VIDEO_LENGTH,
177 fps=30,
178 step_offset=_KIT_STEP_OFFSET,
179 ),
180 VideoRecorderCfg(
181 source="viz:kit:streaming_view",
182 output_dir=out,
183 output_filename_prefix="tiled_kit_viewport",
184 video_length=_VIDEO_LENGTH,
185 fps=30,
186 step_offset=_KIT_STEP_OFFSET,
187 ),
188 VideoRecorderCfg(
189 source="viz:newton_gl",
190 output_dir=out,
191 output_filename_prefix="newton_viewport",
192 video_length=_VIDEO_LENGTH,
193 fps=30,
194 ),
195 VideoRecorderCfg(
196 source="sensor:tiled_camera",
197 output_dir=out,
198 output_filename_prefix="sensor",
199 video_length=_VIDEO_LENGTH,
200 fps=30,
201 ),
202 ]
203 return env_cfg, _TASK_SHADOW
204
205
206_BUILDERS = {
207 1: _build_env_cfg_example_1,
208 2: _build_env_cfg_example_2,
209 3: _build_env_cfg_example_3,
210}
211
212# ---------------------------------------------------------------------------
213# Argument parsing
214# ---------------------------------------------------------------------------
215parser = argparse.ArgumentParser(description="Video recording tutorial for Isaac Lab environments.")
216parser.add_argument(
217 "--example", type=int, default=1, choices=[1, 2, 3], help="Which recording example to run (1, 2, or 3)."
218)
219parser.add_argument("--num_envs", type=int, default=None, help="Number of environments to simulate.")
220add_launcher_args(parser)
221args_cli, hydra_args = setup_preset_cli(parser)
222sys.argv = [sys.argv[0]] + hydra_args
223
224
225def main():
226 """Run the selected video recording example."""
227 defaults = {1: 4, 2: 16, 3: 4}
228 num_envs = args_cli.num_envs if args_cli.num_envs is not None else defaults[args_cli.example]
229 env_cfg, task = _BUILDERS[args_cli.example](num_envs)
230 env_cfg.sim.device = args_cli.device if args_cli.device is not None else env_cfg.sim.device
231
232 # Examples 1 and 3 record from the Kit viewport via omni.replicator, which requires
233 # camera rendering support. Force it here for visualizer-only recording.
234 if args_cli.example in (1, 3):
235 args_cli.enable_cameras = True
236
237 with launch_simulation(env_cfg, args_cli):
238 env = gym.make(task, cfg=env_cfg)
239
240 out = _output_dir(args_cli.example)
241 print(f"[INFO]: Running Example {args_cli.example} — clips → {out}/")
242 print(f"[INFO]: Gym observation space: {env.observation_space}")
243 print(f"[INFO]: Gym action space: {env.action_space}")
244
245 print("[INFO]: Setup complete.")
246 env.reset()
247 for _ in range(_NUM_STEPS):
248 with torch.inference_mode():
249 actions = 2 * torch.rand(env.action_space.shape, device=env.unwrapped.device) - 1
250 env.step(actions)
251
252 env.close()
253 print(f"[INFO]: Done. Clips written to {out}/")
254
255
256if __name__ == "__main__":
257 main()
Example 1: Kit viewport#
uv run python scripts/tutorials/07_visualizers/run_video_recording.py \
--example 1 --num_envs 4 --viz kit
Records the Kit interactive viewport (RTX renderer)
Shows 4 parallel environments
One clip is written to
videos/recording_tutorial/example_1/kit_viewport_0000.mp4
Kit visualizer
Example 2: Scene sensor, headless#
uv run python scripts/tutorials/07_visualizers/run_video_recording.py \
--example 2 --num_envs 16
Scene sensor
No visualizer window opens; frames are read directly from the
tiled_camerasensorOne clip is written to
videos/recording_tutorial/example_2/sensor_0000.mp4source="sensor:tiled_camera"is the key under which the camera is registered inenv.scene.sensorsThe sensor must have the recorded channel in its
data_types;rgbis the default, anddepth,segmentation, andnormalsare also available (see Sources)
Example 3: All sources simultaneously#
uv run python scripts/tutorials/07_visualizers/run_video_recording.py \
--example 3 --num_envs 4 --viz kit,newton_gl
Four independent clips are written to videos/recording_tutorial/example_3/:
kit_viewport_0000.mp4: Kit interactive viewport (RTX renderer)tiled_kit_viewport_0000.mp4: Kit tiled-camera grid (per-environment views)newton_viewport_0000.mp4: Newton GL viewer framebuffersensor_0000.mp4: scene tiled-camera sensor (offline render)
Newton GL visualizer
Kit visualizer tiled streaming
Recipes#
The recipes below are command-line and configuration snippets that are not part of the tutorial script.
Recording during RL training#
Record a short clip at regular intervals while training, without opening a window:
uv run isaaclab train --rl_library rsl_rl --task Isaac-Cartpole --video \
--video_length 200 --video_interval 2000
With no --viz, --video records from a headless Newton GL visualizer. This writes a
200-step clip every 2,000 steps. The train and play entrypoints put clips in videos/
under the run’s log directory unless a recorder sets output_dir; a script that builds its own
recorders writes to output_dir (default videos). Add --viz kit to watch the run in a
window while recording the same view.
Recording depth or segmentation from a scene sensor#
Append a channel to the sensor source to record something other than RGB. Each recorder writes its own file, so several channels of one camera can be recorded in the same run:
env_cfg.video_recorders = [
VideoRecorderCfg(source="sensor:tiled_camera:rgb", output_filename_prefix="rgb"),
VideoRecorderCfg(source="sensor:tiled_camera:depth", output_filename_prefix="depth"),
]
Depth uses the turbo colormap over depth_colormap_min to depth_colormap_max. Use the
same prefix-per-recorder pattern for segmentation and normals.
Recording while streaming with Viser or Rerun#
Viser and Rerun stream to a browser or viewer and have no frame capture, so they cannot be recorded. Pair them with a capture-capable visualizer that runs headless next to them:
uv run isaaclab train --rl_library rsl_rl --task Isaac-Cartpole --viz viser --video viz:newton_gl
Alternatively, record directly from a scene camera with source="sensor:<name>", which needs no
visualizer.
Recording from an independent camera angle#
Configure the recording angle on the visualizer, not the recorder. To record from a headless Newton
visualizer at a different angle alongside an interactive Kit viewer, run with --viz kit and
configure both; the recorder adds the Newton visualizer, headless:
from isaaclab_visualizers.kit import KitVisualizerCfg
from isaaclab_visualizers.newton import NewtonGLVisualizerCfg
env_cfg.sim.visualizer_cfgs = [
KitVisualizerCfg(eye=(4.0, 4.0, 2.0)),
NewtonGLVisualizerCfg(eye=(12.0, 0.0, 6.0)),
]
env_cfg.video_recorders = [
VideoRecorderCfg(source="viz:newton_gl", output_dir="videos/"),
]
Alternatively, use a CameraCfg sensor in the scene and record
with source="sensor:<name>", which gives full control over the recording viewpoint
without requiring a second interactive visualizer.
Sources#
The source string selects what to capture:
Source string |
Captures from |
|---|---|
|
First capture-capable visualizer |
|
Kit visualizer viewport |
|
Kit streaming camera panel, requires |
|
Newton GL visualizer viewport |
|
Newton OVRTX path-traced viewport |
|
Newton GL streaming camera panel, requires |
|
|
|
RGB channel |
|
Depth, turbo colormap, range |
|
Segmentation, colorized |
|
Surface normals, colorized |
The camera angle, resolution, and other visualizer settings are configured on the corresponding
visualizer config, not on the recorder. A viz:<type> visualizer that --viz does not select runs
headless, only for the recording, with the settings of its config in sim.visualizer_cfgs.
visualizer is accepted as the long form of the viz prefix ("visualizer:kit" is "viz:kit"). The
deprecated newton type ("viz:newton") still works with a warning; use newton_gl. Bare visualizer
types are accepted here too, e.g. VideoRecorderCfg(source="newton_gl") is equivalent to
VideoRecorderCfg(source="viz:newton_gl").
Note
The Newton RTX viewer framebuffer can be recorded with "viz:newton_rtx", but
recording its streaming view is not supported.
Clip control#
Field |
Default |
Meaning |
|---|---|---|
|
|
Env steps per clip |
|
|
|
|
|
Output frame rate; |
|
|
Directory for output files (created on demand) |
|
|
File stem; output is |
|
|
Delete older clips; |
One clip at the start of a run:
VideoRecorderCfg(source="viz:kit", video_length=500, video_interval=0)
Recurring clips every 1,000 env steps:
VideoRecorderCfg(source="viz:kit", video_length=200, video_interval=1000)
Keep only the most recent clip on disk:
VideoRecorderCfg(source="viz:kit", video_length=200, video_interval=1000,
keep_last_n_clips=1)
Limitations#
source="viz:newton_gl:streaming_view"andsource="viz:kit:streaming_view"requirestreaming_view=Trueon the corresponding visualizer cfg. ARuntimeErroris raised when the recorder is constructed if it is not set.For
source="sensor:<name>", the named sensor must exist on the scene config with the recorded channel ("rgb"by default) in itsdata_types.
Visualizer |
|
Notes |
|---|---|---|
|
✓ |
Launches Isaac Sim; the launch enables camera rendering for the recording. |
|
✓ |
Uses pyglet’s EGL backend when headless; the default for |
|
✓ |
Starts the OVRTX runtime; capture performs a GPU-to-CPU readback of the path-traced framebuffer. |
|
✗ |
Remote streaming tool; no local frame-capture API. |
|
✗ |
Browser streaming tool; no local frame-capture API. |
To record video while streaming with Rerun or Viser, see Recording while streaming with Viser or Rerun.
See also#
Visualization: configuring interactive visualizers and recording from them
Streaming a camera view in a visualizer: tiled camera panel setup
Capturing sensor frames during training: saving per-frame sensor outputs as images