Recording Video#
Isaac Lab can record video from a Kit or Newton GL visualizer, or directly from a scene camera
sensor, by adding VideoRecorderCfg entries to
the environment config. Each recorder captures from a configurable source and writes mp4
clips to disk independently. Streaming visualizers (Rerun, Viser) and the Newton RTX backend
do not support frame capture; see Visualizer compatibility below.
from isaaclab.envs.utils.video_recorder_cfg import VideoRecorderCfg
env_cfg.video_recorders = [
VideoRecorderCfg(source="visualizer:kit", output_dir="videos/")
]
This guide is accompanied by the run_video_recording.py tutorial script in
IsaacLab/scripts/tutorials/07_visualizers. Pass --example 1, --example 2,
or --example 3 to select which recording configuration to run.
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
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
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"""
40
41from __future__ import annotations
42
43import argparse
44import contextlib
45import os
46import sys
47
48import gymnasium as gym
49import torch
50
51import isaaclab_tasks # noqa: F401
52
53with contextlib.suppress(ImportError):
54 import isaaclab_tasks_experimental # noqa: F401
55
56from isaaclab.app import add_launcher_args, launch_simulation
57from isaaclab.envs.utils.video_recorder_cfg import VideoRecorderCfg
58
59from isaaclab_tasks.utils import setup_preset_cli
60
61# ---------------------------------------------------------------------------
62# Constants
63# ---------------------------------------------------------------------------
64
65_VIDEO_LENGTH = 100 # env steps per clip
66_NUM_STEPS = 115 # slightly more than _VIDEO_LENGTH so the clip flushes cleanly
67
68_TASK_SHADOW = "Isaac-Reorient-Cube-Shadow-Camera-Direct"
69
70# Kit viewport camera: positioned to show a 2×2 grid of Shadow Hand environments.
71# env_spacing=1.0 with 4 envs → envs centered at ±0.5 in x and y.
72# Cube spawns at ~(0, -0.39, 0.6) per env; wrist cylinder is the landmark at the top.
73_SHADOW_EYE = (0.0, -2.2, 1.8)
74_SHADOW_LOOKAT = (0.0, -0.1, 0.4)
75_SHADOW_ENV_SPACING = 1.0
76
77# Tiled camera eye offset from each robot root for generated per-env cameras.
78_SHADOW_TILED_EYE = (0.0, 0.35, 0.8)
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 from isaaclab_tasks.core.reorient.config.shadow_hand.shadow_hand_direct_camera_env_cfg import ShadowHandCameraEnvCfg
91
92 env_cfg = ShadowHandCameraEnvCfg()
93 env_cfg.tiled_camera = env_cfg.tiled_camera.rgb
94 env_cfg.tiled_camera.renderer_cfg = env_cfg.tiled_camera.renderer_cfg.default
95 env_cfg.tiled_camera.height = 256
96 env_cfg.tiled_camera.width = 256
97 env_cfg.scene.num_envs = num_envs
98 env_cfg.scene.env_spacing = env_spacing
99 return env_cfg
100
101
102# ---------------------------------------------------------------------------
103# Per-example environment config builders
104# ---------------------------------------------------------------------------
105
106
107def _build_env_cfg_example_1(num_envs: int):
108 """Shadow Hand + Kit viewport: one clip from the interactive viewport."""
109 from isaaclab_visualizers.kit import KitVisualizerCfg
110
111 env_cfg = _shadow_env_cfg(num_envs)
112 env_cfg.sim.physics = env_cfg.sim.physics.default
113
114 env_cfg.sim.visualizer_cfgs = [KitVisualizerCfg(eye=_SHADOW_EYE, lookat=_SHADOW_LOOKAT)]
115
116 out = _output_dir(1)
117 env_cfg.video_recorders = [
118 VideoRecorderCfg(
119 source="visualizer:kit",
120 output_dir=out,
121 output_filename_prefix="kit_viewport",
122 video_length=_VIDEO_LENGTH,
123 fps=30,
124 step_offset=_KIT_STEP_OFFSET,
125 ),
126 ]
127 return env_cfg, _TASK_SHADOW
128
129
130def _build_env_cfg_example_2(num_envs: int):
131 """Shadow Hand + headless: scene tiled-camera sensor clip only."""
132 env_cfg = _shadow_env_cfg(num_envs, env_spacing=2.0)
133 env_cfg.sim.physics = env_cfg.sim.physics.default
134 env_cfg.sim.visualizer_cfgs = [] # no interactive visualizer
135
136 out = _output_dir(2)
137 env_cfg.video_recorders = [
138 VideoRecorderCfg(
139 source="sensor:tiled_camera",
140 output_dir=out,
141 output_filename_prefix="sensor",
142 video_length=_VIDEO_LENGTH,
143 fps=30,
144 ),
145 ]
146 return env_cfg, _TASK_SHADOW
147
148
149def _build_env_cfg_example_3(num_envs: int):
150 """Shadow Hand + Kit viewport + Kit tiled grid + Newton viewport + sensor: four simultaneous streams.
151
152 Note: ``source='visualizer:newton'`` captures the full Newton GL window. When
153 ``streaming_view=True`` is set on :class:`~isaaclab_visualizers.newton.NewtonGLVisualizerCfg`,
154 the GL window displays the per-environment camera panel, so this effectively records
155 a Newton streaming view without a separate ``render_tiled_rgb_array()`` call.
156 """
157 from isaaclab_visualizers.kit import KitVisualizerCfg
158 from isaaclab_visualizers.newton import NewtonGLVisualizerCfg
159
160 env_cfg = _shadow_env_cfg(num_envs)
161 env_cfg.sim.physics = env_cfg.sim.physics.default
162
163 kit_cfg = KitVisualizerCfg(
164 eye=_SHADOW_EYE,
165 lookat=_SHADOW_LOOKAT,
166 streaming_view=True,
167 streaming_envs=min(num_envs, 16),
168 # Reuse the existing scene camera sensor so the streaming panel shows
169 # the same RTX-rendered views as source="sensor:tiled_camera".
170 streaming_sensor_prim_path="/World/envs/env_.*/Camera",
171 )
172 newton_cfg = NewtonGLVisualizerCfg(
173 eye=_SHADOW_EYE,
174 lookat=_SHADOW_LOOKAT,
175 window_width=1280,
176 window_height=720,
177 focal_length=25.0,
178 )
179 env_cfg.sim.visualizer_cfgs = [kit_cfg, newton_cfg]
180
181 out = _output_dir(3)
182 env_cfg.video_recorders = [
183 VideoRecorderCfg(
184 source="visualizer:kit",
185 output_dir=out,
186 output_filename_prefix="kit_viewport",
187 video_length=_VIDEO_LENGTH,
188 fps=30,
189 step_offset=_KIT_STEP_OFFSET,
190 ),
191 VideoRecorderCfg(
192 source="visualizer:kit:streaming_view",
193 output_dir=out,
194 output_filename_prefix="tiled_kit_viewport",
195 video_length=_VIDEO_LENGTH,
196 fps=30,
197 step_offset=_KIT_STEP_OFFSET,
198 ),
199 VideoRecorderCfg(
200 source="visualizer:newton",
201 output_dir=out,
202 output_filename_prefix="newton_viewport",
203 video_length=_VIDEO_LENGTH,
204 fps=30,
205 ),
206 VideoRecorderCfg(
207 source="sensor:tiled_camera",
208 output_dir=out,
209 output_filename_prefix="sensor",
210 video_length=_VIDEO_LENGTH,
211 fps=30,
212 ),
213 ]
214 return env_cfg, _TASK_SHADOW
215
216
217_BUILDERS = {
218 1: _build_env_cfg_example_1,
219 2: _build_env_cfg_example_2,
220 3: _build_env_cfg_example_3,
221}
222
223# ---------------------------------------------------------------------------
224# Argument parsing
225# ---------------------------------------------------------------------------
226parser = argparse.ArgumentParser(description="Video recording tutorial for Isaac Lab environments.")
227parser.add_argument(
228 "--example", type=int, default=1, choices=[1, 2, 3], help="Which recording example to run (1, 2, or 3)."
229)
230parser.add_argument("--num_envs", type=int, default=None, help="Number of environments to simulate.")
231add_launcher_args(parser)
232args_cli, hydra_args = setup_preset_cli(parser)
233sys.argv = [sys.argv[0]] + hydra_args
234
235
236def main():
237 """Run the selected video recording example."""
238 defaults = {1: 4, 2: 16, 3: 4}
239 num_envs = args_cli.num_envs if args_cli.num_envs is not None else defaults[args_cli.example]
240 env_cfg, task = _BUILDERS[args_cli.example](num_envs)
241 env_cfg.sim.device = args_cli.device if args_cli.device is not None else env_cfg.sim.device
242
243 # Examples 1 and 3 record from the Kit viewport via omni.replicator, which requires
244 # camera rendering support. Force it here for visualizer-only recording.
245 if args_cli.example in (1, 3):
246 args_cli.enable_cameras = True
247
248 with launch_simulation(env_cfg, args_cli):
249 env = gym.make(task, cfg=env_cfg)
250
251 out = _output_dir(args_cli.example)
252 print(f"[INFO]: Running Example {args_cli.example} — clips → {out}/")
253 print(f"[INFO]: Gym observation space: {env.observation_space}")
254 print(f"[INFO]: Gym action space: {env.action_space}")
255
256 print("[INFO]: Setup complete.")
257 env.reset()
258 for _ in range(_NUM_STEPS):
259 with torch.inference_mode():
260 actions = 2 * torch.rand(env.action_space.shape, device=env.unwrapped.device) - 1
261 env.step(actions)
262
263 env.close()
264 print(f"[INFO]: Done. Clips written to {out}/")
265
266
267if __name__ == "__main__":
268 main()
Tutorial 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. Example 1 and Example 2 each demonstrate one recording source; Example 3
combines all of them simultaneously.
Example 1: Kit viewport#
uv run python scripts/tutorials/07_visualizers/run_video_recording.py \
--example 1 --num_envs 4
Records the Kit interactive viewport (RTX renderer) showing 4 parallel environments.
One clip is written to videos/recording_tutorial/example_1/kit_viewport_0000.mp4.
Example 2: Scene sensor, headless#
uv run python scripts/tutorials/07_visualizers/run_video_recording.py \
--example 2 --num_envs 16
No visualizer window opens. Frames are read directly from the tiled_camera sensor,
writing one clip to videos/recording_tutorial/example_2/sensor_0000.mp4.
source="sensor:tiled_camera" refers to the key under which the camera is registered
in env.scene.sensors. The sensor must have "rgb" in its data_types; only the
rgb channel is currently supported for sensor sources.
Example 3: All sources simultaneously#
uv run python scripts/tutorials/07_visualizers/run_video_recording.py \
--example 3 --num_envs 4
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 framebuffer.sensor_0000.mp4— scene tiled-camera sensor (offline render).
Each VideoRecorderCfg entry is fully independent — different sources write different
files at their own cadence. There is no limit on the number of simultaneous recorders.
Source types#
The source string selects what to capture:
Source string |
Captures from |
|---|---|
|
First active recording-capable visualizer (auto) |
|
Kit visualizer viewport |
|
Kit streaming camera panel (requires |
|
Newton GL visualizer 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 KitVisualizerCfg or
NewtonGLVisualizerCfg, not on the recorder.
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="visualizer:kit", video_length=500, video_interval=0)
Recurring clips every 1 000 env steps:
VideoRecorderCfg(source="visualizer:kit", video_length=200, video_interval=1000)
Keep only the most recent clip on disk:
VideoRecorderCfg(source="visualizer:kit", video_length=200, video_interval=1000,
keep_last_n_clips=1)
Recording from an independent camera angle#
Configure the recording angle on the visualizer rather than on the recorder. To open a headless Newton visualizer at a different angle alongside an interactive Kit viewer:
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), headless=True),
]
env_cfg.video_recorders = [
VideoRecorderCfg(source="visualizer:newton", 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.
Requirements#
moviepy 1.x and
ffmpegmust be installed (both are already in Isaac Lab’s dependencies).For
source="visualizer:kit"or"visualizer:kit:streaming_view": the Kit app is launched automatically byAppLauncher. In headless mode (--headless), you must also pass--enable_cameras(or setENABLE_CAMERAS=1) to activate the Replicator offscreen render pipeline; without it, captured frames are black. The--videoflag sets--enable_camerasautomatically when no explicit recorder source is configured.For
source="visualizer:newton"or"visualizer:newton_gl"/"visualizer:newton:streaming_view": an activeNewtonGLVisualizerCfgmust be inenv_cfg.sim.visualizer_cfgs. Newton GL uses pyglet’s EGL backend and works headlessly without--enable_cameras.For
source="sensor:<name>": the named field must exist on the scene config and have"rgb"in itsdata_types.
Visualizer compatibility#
Only kit and newton_gl support frame capture for video recording. Both can run
headless (headless=True on the cfg) so they add no UI window or interactive overhead
when video is the only goal.
Visualizer |
|
Notes |
|---|---|---|
|
✓ |
Kit/Omniverse viewport; supports headless mode |
|
✓ |
Newton OpenGL viewport; supports headless mode |
|
✗ |
Framebuffer readback ( |
|
✗ |
Remote streaming tool; no local frame-capture API |
|
✗ |
Browser streaming tool; no local frame-capture API |
Passing --video alongside --viz rerun, --viz viser, or --viz newton_rtx
raises an error when no other recording-capable visualizer is configured.
To run a streaming or RTX visualizer and record video simultaneously, add a headless
capture backend alongside it in sim.visualizer_cfgs:
from isaaclab_visualizers.kit import KitVisualizerCfg
from isaaclab_visualizers.rerun import RerunVisualizerCfg
env_cfg.sim.visualizer_cfgs = [
RerunVisualizerCfg(...), # streaming — for monitoring
KitVisualizerCfg(headless=True), # headless — provides frames for --video
]
Alternatively, record directly from a scene camera sensor without any visualizer:
VideoRecorderCfg(source="sensor:<name>") # add to env_cfg.video_recorders
Limitations#
source="visualizer:kit"andsource="visualizer:kit:streaming_view"require cubric to propagate Newton Fabric scene transforms to the RTX renderer. Without cubric, a warning is logged and a black-frame warning is emitted at clip write time. Usesource="visualizer:newton"for guaranteed capture with Newton physics.source="visualizer:newton:streaming_view"andsource="visualizer:kit:streaming_view"requirestreaming_view=Trueon the corresponding visualizer cfg. ARuntimeErroris raised at the first capture attempt if it is not set.
See also#
Visualization — configuring interactive visualizers
Using the Visualizer Streaming Camera View — tiled camera panel setup
Capturing sensor frames during training — saving per-frame sensor outputs as images



