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()

Kit viewport — 4 Shadow Hand environments (RTX)

Newton GL viewport — 4 Shadow Hand environments

Kit visualizer

Newton GL visualizer

Kit tiled-camera grid — per-environment views

Scene tiled-camera sensor recording

Kit visualizer tiled streaming

Scene sensor

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

"visualizer"

First active recording-capable visualizer (auto)

"visualizer:kit"

Kit visualizer viewport

"visualizer:kit:streaming_view"

Kit streaming camera panel (requires streaming_view=True)

"visualizer:newton"

Newton GL visualizer viewport

"visualizer:newton:streaming_view"

Newton GL streaming camera panel (requires streaming_view=True)

"sensor:<name>"

env.scene.sensors[name], RGB (default)

"sensor:<name>:rgb"

RGB channel

"sensor:<name>:depth"

Depth, turbo colormap (range: depth_colormap_mindepth_colormap_max)

"sensor:<name>:segmentation"

Segmentation, colorized

"sensor:<name>:normals"

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

video_length

200

Env steps per clip

video_interval

0

0 = one clip starting at step 1; N > 0 = new clip every N steps

fps

None

Output frame rate; None resolves from env.metadata["render_fps"] or 1 / step_dt

output_dir

"videos"

Directory for output files (created on demand)

output_filename_prefix

"clip"

File stem; output is <prefix>_NNNN.mp4

keep_last_n_clips

None

Delete older clips; None keeps all

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 ffmpeg must 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 by AppLauncher. In headless mode (--headless), you must also pass --enable_cameras (or set ENABLE_CAMERAS=1) to activate the Replicator offscreen render pipeline; without it, captured frames are black. The --video flag sets --enable_cameras automatically when no explicit recorder source is configured.

  • For source="visualizer:newton" or "visualizer:newton_gl" / "visualizer:newton:streaming_view": an active NewtonGLVisualizerCfg must be in env_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 its data_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

--video

Notes

kit

Kit/Omniverse viewport; supports headless mode

newton_gl

Newton OpenGL viewport; supports headless mode

newton_rtx

Framebuffer readback (ViewerRTX.get_frame()) not yet available from the Newton SDK

rerun

Remote streaming tool; no local frame-capture API

viser

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" and source="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. Use source="visualizer:newton" for guaranteed capture with Newton physics.

  • source="visualizer:newton:streaming_view" and source="visualizer:kit:streaming_view" require streaming_view=True on the corresponding visualizer cfg. A RuntimeError is raised at the first capture attempt if it is not set.

See also#