Recording Video#

Isaac Lab can record video from a Kit, Newton GL, or Newton RTX 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 and Viser) do not support local 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 resolve_task_config, 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    env_cfg, _ = resolve_task_config(_TASK_SHADOW, "", overrides=(*sys.argv[1:], "env.tiled_camera=rgb"))
 91    env_cfg.tiled_camera.height = 256
 92    env_cfg.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="visualizer: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    env_cfg.sim.visualizer_cfgs = []  # no interactive visualizer
128
129    out = _output_dir(2)
130    env_cfg.video_recorders = [
131        VideoRecorderCfg(
132            source="sensor:tiled_camera",
133            output_dir=out,
134            output_filename_prefix="sensor",
135            video_length=_VIDEO_LENGTH,
136            fps=30,
137        ),
138    ]
139    return env_cfg, _TASK_SHADOW
140
141
142def _build_env_cfg_example_3(num_envs: int):
143    """Shadow Hand + Kit viewport + Kit tiled grid + Newton viewport + sensor: four simultaneous streams.
144
145    Note: ``source='visualizer:newton'`` captures the full Newton GL window. When
146    ``streaming_view=True`` is set on :class:`~isaaclab_visualizers.newton.NewtonGLVisualizerCfg`,
147    the GL window displays the per-environment camera panel, so this effectively records
148    a Newton streaming view without a separate ``render_tiled_rgb_array()`` call.
149    """
150    from isaaclab_visualizers.kit import KitVisualizerCfg
151    from isaaclab_visualizers.newton import NewtonGLVisualizerCfg
152
153    env_cfg = _shadow_env_cfg(num_envs)
154    kit_cfg = KitVisualizerCfg(
155        eye=_SHADOW_EYE,
156        lookat=_SHADOW_LOOKAT,
157        streaming_view=True,
158        streaming_envs=min(num_envs, 16),
159        # Reuse the existing scene camera sensor so the streaming panel shows
160        # the same RTX-rendered views as source="sensor:tiled_camera".
161        streaming_sensor_prim_path="/World/envs/env_.*/Camera",
162    )
163    newton_cfg = NewtonGLVisualizerCfg(
164        eye=_SHADOW_EYE,
165        lookat=_SHADOW_LOOKAT,
166        window_width=1280,
167        window_height=720,
168        focal_length=25.0,
169    )
170    env_cfg.sim.visualizer_cfgs = [kit_cfg, newton_cfg]
171
172    out = _output_dir(3)
173    env_cfg.video_recorders = [
174        VideoRecorderCfg(
175            source="visualizer:kit",
176            output_dir=out,
177            output_filename_prefix="kit_viewport",
178            video_length=_VIDEO_LENGTH,
179            fps=30,
180            step_offset=_KIT_STEP_OFFSET,
181        ),
182        VideoRecorderCfg(
183            source="visualizer:kit:streaming_view",
184            output_dir=out,
185            output_filename_prefix="tiled_kit_viewport",
186            video_length=_VIDEO_LENGTH,
187            fps=30,
188            step_offset=_KIT_STEP_OFFSET,
189        ),
190        VideoRecorderCfg(
191            source="visualizer:newton",
192            output_dir=out,
193            output_filename_prefix="newton_viewport",
194            video_length=_VIDEO_LENGTH,
195            fps=30,
196        ),
197        VideoRecorderCfg(
198            source="sensor:tiled_camera",
199            output_dir=out,
200            output_filename_prefix="sensor",
201            video_length=_VIDEO_LENGTH,
202            fps=30,
203        ),
204    ]
205    return env_cfg, _TASK_SHADOW
206
207
208_BUILDERS = {
209    1: _build_env_cfg_example_1,
210    2: _build_env_cfg_example_2,
211    3: _build_env_cfg_example_3,
212}
213
214# ---------------------------------------------------------------------------
215# Argument parsing
216# ---------------------------------------------------------------------------
217parser = argparse.ArgumentParser(description="Video recording tutorial for Isaac Lab environments.")
218parser.add_argument(
219    "--example", type=int, default=1, choices=[1, 2, 3], help="Which recording example to run (1, 2, or 3)."
220)
221parser.add_argument("--num_envs", type=int, default=None, help="Number of environments to simulate.")
222add_launcher_args(parser)
223args_cli, hydra_args = setup_preset_cli(parser)
224sys.argv = [sys.argv[0]] + hydra_args
225
226
227def main():
228    """Run the selected video recording example."""
229    defaults = {1: 4, 2: 16, 3: 4}
230    num_envs = args_cli.num_envs if args_cli.num_envs is not None else defaults[args_cli.example]
231    env_cfg, task = _BUILDERS[args_cli.example](num_envs)
232    env_cfg.sim.device = args_cli.device if args_cli.device is not None else env_cfg.sim.device
233
234    # Examples 1 and 3 record from the Kit viewport via omni.replicator, which requires
235    # camera rendering support.  Force it here for visualizer-only recording.
236    if args_cli.example in (1, 3):
237        args_cli.enable_cameras = True
238
239    with launch_simulation(env_cfg, args_cli):
240        env = gym.make(task, cfg=env_cfg)
241
242        out = _output_dir(args_cli.example)
243        print(f"[INFO]: Running Example {args_cli.example} — clips → {out}/")
244        print(f"[INFO]: Gym observation space: {env.observation_space}")
245        print(f"[INFO]: Gym action space: {env.action_space}")
246
247        print("[INFO]: Setup complete.")
248        env.reset()
249        for _ in range(_NUM_STEPS):
250            with torch.inference_mode():
251                actions = 2 * torch.rand(env.action_space.shape, device=env.unwrapped.device) - 1
252                env.step(actions)
253
254        env.close()
255        print(f"[INFO]: Done. Clips written to {out}/")
256
257
258if __name__ == "__main__":
259    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_rtx"

Newton OVRTX path-traced 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 visualizer config, not on the recorder.

Note

The Newton RTX viewer framebuffer can be recorded with "visualizer:newton_rtx", but recording its streaming view is not supported.

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#

  • Install the video extra to provide moviepy 1.x and its ffmpeg runtime. In a uv checkout, add --extra video to the command.

  • 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="visualizer:newton_rtx": the OVRTX runtime and an active NewtonRTXVisualizerCfg are required. Capturing the path-traced LDR framebuffer performs a GPU-to-CPU readback.

  • For source="sensor:<name>": the named field must exist on the scene config and have "rgb" in its data_types.

Visualizer compatibility#

kit, newton_gl, and newton_rtx support frame capture and can run headless.

Visualizer

--video

Notes

kit

Kit/Omniverse viewport; supports headless mode

newton_gl

Newton OpenGL viewport; supports headless mode

newton_rtx

Newton OVRTX path-traced viewport; native-resolution LDR readback

rerun

Remote streaming tool; no local frame-capture API

viser

Browser streaming tool; no local frame-capture API

Passing --video alongside --viz rerun or --viz viser raises an error when no other recording-capable visualizer is configured.

To run a streaming 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#