Recording Video#

Isaac Lab supports video recording from visualizers and sensor data streams from renderers. Recordings output as mp4 clips.

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

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="visualizer:kit", output_dir="videos/")
]

Or pass --video on the command line to record from the default visualizer without editing the environment config:

uv run isaaclab train --rl_library rsl_rl --task Isaac-Cartpole --viz kit --video
./isaaclab.sh train --rl_library rsl_rl --task Isaac-Cartpole --viz kit --video

See Source types for the full list of recordable sources and Clip control for length and interval options.

Overview#

Each VideoRecorderCfg entry is independent: different sources write different files at their own cadence, with no limit on simultaneous recorders. The examples below record all four sources from the same run.

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
 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# Skip the first few steps so the RTX renderer has warmed up before recording starts.
 78_KIT_STEP_OFFSET = 5
 79
 80
 81def _output_dir(example: int) -> str:
 82    return os.path.join("videos", "recording_tutorial", f"example_{example}")
 83
 84
 85def _shadow_env_cfg(num_envs: int, env_spacing: float = _SHADOW_ENV_SPACING):
 86    """Build a base Shadow Hand camera env cfg shared by all examples."""
 87    env_cfg, _ = resolve_task_config(_TASK_SHADOW, "", overrides=(*sys.argv[1:], "env.tiled_camera=rgb"))
 88    env_cfg.tiled_camera.height = 256
 89    env_cfg.tiled_camera.width = 256
 90    env_cfg.scene.num_envs = num_envs
 91    env_cfg.scene.env_spacing = env_spacing
 92    return env_cfg
 93
 94
 95# ---------------------------------------------------------------------------
 96# Per-example environment config builders
 97# ---------------------------------------------------------------------------
 98
 99
100def _build_env_cfg_example_1(num_envs: int):
101    """Shadow Hand + Kit viewport: one clip from the interactive viewport."""
102    from isaaclab_visualizers.kit import KitVisualizerCfg
103
104    env_cfg = _shadow_env_cfg(num_envs)
105    env_cfg.sim.visualizer_cfgs = [KitVisualizerCfg(eye=_SHADOW_EYE, lookat=_SHADOW_LOOKAT)]
106
107    out = _output_dir(1)
108    env_cfg.video_recorders = [
109        VideoRecorderCfg(
110            source="visualizer:kit",
111            output_dir=out,
112            output_filename_prefix="kit_viewport",
113            video_length=_VIDEO_LENGTH,
114            fps=30,
115            step_offset=_KIT_STEP_OFFSET,
116        ),
117    ]
118    return env_cfg, _TASK_SHADOW
119
120
121def _build_env_cfg_example_2(num_envs: int):
122    """Shadow Hand + headless: scene tiled-camera sensor clip only."""
123    env_cfg = _shadow_env_cfg(num_envs, env_spacing=2.0)
124    env_cfg.sim.visualizer_cfgs = []  # no interactive visualizer
125
126    out = _output_dir(2)
127    env_cfg.video_recorders = [
128        VideoRecorderCfg(
129            source="sensor:tiled_camera",
130            output_dir=out,
131            output_filename_prefix="sensor",
132            video_length=_VIDEO_LENGTH,
133            fps=30,
134        ),
135    ]
136    return env_cfg, _TASK_SHADOW
137
138
139def _build_env_cfg_example_3(num_envs: int):
140    """Shadow Hand + Kit viewport + Kit tiled grid + Newton viewport + sensor: four simultaneous streams.
141
142    Note: ``source='visualizer:newton'`` captures the full Newton GL window. When
143    ``streaming_view=True`` is set on :class:`~isaaclab_visualizers.newton.NewtonGLVisualizerCfg`,
144    the GL window displays the per-environment camera panel, so this effectively records
145    a Newton streaming view without a separate ``render_tiled_rgb_array()`` call.
146    """
147    from isaaclab_visualizers.kit import KitVisualizerCfg
148    from isaaclab_visualizers.newton import NewtonGLVisualizerCfg
149
150    env_cfg = _shadow_env_cfg(num_envs)
151    kit_cfg = KitVisualizerCfg(
152        eye=_SHADOW_EYE,
153        lookat=_SHADOW_LOOKAT,
154        streaming_view=True,
155        streaming_envs=min(num_envs, 16),
156        # No streaming_sensor_prim_path/streaming_cam_target_prim_path: adopts the existing
157        # tiled_camera sensor automatically, so the streaming panel shows the same
158        # RTX-rendered views as source="sensor:tiled_camera".
159    )
160    newton_cfg = NewtonGLVisualizerCfg(
161        eye=_SHADOW_EYE,
162        lookat=_SHADOW_LOOKAT,
163        window_width=1280,
164        window_height=720,
165        focal_length=25.0,
166    )
167    env_cfg.sim.visualizer_cfgs = [kit_cfg, newton_cfg]
168
169    out = _output_dir(3)
170    env_cfg.video_recorders = [
171        VideoRecorderCfg(
172            source="visualizer:kit",
173            output_dir=out,
174            output_filename_prefix="kit_viewport",
175            video_length=_VIDEO_LENGTH,
176            fps=30,
177            step_offset=_KIT_STEP_OFFSET,
178        ),
179        VideoRecorderCfg(
180            source="visualizer:kit:streaming_view",
181            output_dir=out,
182            output_filename_prefix="tiled_kit_viewport",
183            video_length=_VIDEO_LENGTH,
184            fps=30,
185            step_offset=_KIT_STEP_OFFSET,
186        ),
187        VideoRecorderCfg(
188            source="visualizer:newton",
189            output_dir=out,
190            output_filename_prefix="newton_viewport",
191            video_length=_VIDEO_LENGTH,
192            fps=30,
193        ),
194        VideoRecorderCfg(
195            source="sensor:tiled_camera",
196            output_dir=out,
197            output_filename_prefix="sensor",
198            video_length=_VIDEO_LENGTH,
199            fps=30,
200        ),
201    ]
202    return env_cfg, _TASK_SHADOW
203
204
205_BUILDERS = {
206    1: _build_env_cfg_example_1,
207    2: _build_env_cfg_example_2,
208    3: _build_env_cfg_example_3,
209}
210
211# ---------------------------------------------------------------------------
212# Argument parsing
213# ---------------------------------------------------------------------------
214parser = argparse.ArgumentParser(description="Video recording tutorial for Isaac Lab environments.")
215parser.add_argument(
216    "--example", type=int, default=1, choices=[1, 2, 3], help="Which recording example to run (1, 2, or 3)."
217)
218parser.add_argument("--num_envs", type=int, default=None, help="Number of environments to simulate.")
219add_launcher_args(parser)
220args_cli, hydra_args = setup_preset_cli(parser)
221sys.argv = [sys.argv[0]] + hydra_args
222
223
224def main():
225    """Run the selected video recording example."""
226    defaults = {1: 4, 2: 16, 3: 4}
227    num_envs = args_cli.num_envs if args_cli.num_envs is not None else defaults[args_cli.example]
228    env_cfg, task = _BUILDERS[args_cli.example](num_envs)
229    env_cfg.sim.device = args_cli.device if args_cli.device is not None else env_cfg.sim.device
230
231    # Examples 1 and 3 record from the Kit viewport via omni.replicator, which requires
232    # camera rendering support.  Force it here for visualizer-only recording.
233    if args_cli.example in (1, 3):
234        args_cli.enable_cameras = True
235
236    with launch_simulation(env_cfg, args_cli):
237        env = gym.make(task, cfg=env_cfg)
238
239        out = _output_dir(args_cli.example)
240        print(f"[INFO]: Running Example {args_cli.example} — clips → {out}/")
241        print(f"[INFO]: Gym observation space: {env.observation_space}")
242        print(f"[INFO]: Gym action space: {env.action_space}")
243
244        print("[INFO]: Setup complete.")
245        env.reset()
246        for _ in range(_NUM_STEPS):
247            with torch.inference_mode():
248                actions = 2 * torch.rand(env.action_space.shape, device=env.unwrapped.device) - 1
249                env.step(actions)
250
251        env.close()
252        print(f"[INFO]: Done. Clips written to {out}/")
253
254
255if __name__ == "__main__":
256    main()

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)

  • 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_camera sensor

  • One clip is written to videos/recording_tutorial/example_2/sensor_0000.mp4

  • source="sensor:tiled_camera" is 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)

Newton GL visualizer

Kit visualizer tiled streaming

Usage#

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, not 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.

Limitations and compatibility#

  • 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.

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

Visualizer

--video

Notes

kit

Headless mode requires --enable_cameras (or ENABLE_CAMERAS=1) to activate offscreen rendering, or frames are black; --video sets this automatically when no explicit source is configured.

newton_gl

Requires an active NewtonGLVisualizerCfg; uses pyglet’s EGL backend and works headlessly without --enable_cameras.

newton_rtx

Requires an active NewtonRTXVisualizerCfg and the OVRTX runtime; capture performs a GPU-to-CPU readback of the path-traced framebuffer.

rerun

Remote streaming tool; no local frame-capture API. Passing --video alongside --viz rerun raises an error unless another recording-capable visualizer is set.

viser

Browser streaming tool; no local frame-capture API. Passing --video alongside --viz viser raises an error unless another recording-capable visualizer is set.

To record video while streaming with Rerun or Viser, add a headless capture-capable visualizer 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

See also#