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_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 the recorded channel in its data_types; rgb is the default, and depth, segmentation, and normals are 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 framebuffer

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

"viz" (default)

First capture-capable visualizer --viz selected, else a headless Newton GL visualizer

"viz:kit"

Kit visualizer viewport

"viz:kit:streaming_view"

Kit streaming camera panel, requires streaming_view=True

"viz:newton_gl"

Newton GL visualizer viewport

"viz:newton_rtx"

Newton OVRTX path-traced viewport

"viz:newton_gl: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_min … depth_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. 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

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="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" and source="viz:kit:streaming_view" require streaming_view=True on the corresponding visualizer cfg. A RuntimeError is 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 its data_types.

Visualizer

--video

Notes

kit

✓

Launches Isaac Sim; the launch enables camera rendering for the recording.

newton_gl

✓

Uses pyglet’s EGL backend when headless; the default for --video without a capture-capable --viz.

newton_rtx

✓

Starts the OVRTX runtime; capture performs a GPU-to-CPU readback of the path-traced framebuffer.

rerun

✗

Remote streaming tool; no local frame-capture API. viz:rerun is an error; --video --viz rerun records from a headless newton_gl.

viser

✗

Browser streaming tool; no local frame-capture API. viz:viser is an error; --video --viz viser records from a headless newton_gl.

To record video while streaming with Rerun or Viser, see Recording while streaming with Viser or Rerun.

See also#