Skip to navigation

Recording video

View as Markdown

This guide shows how to record mp4 clips from a visualizer or a scene sensor during a run.

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

Or pass --video on the command line to record without editing the environment config. It records the first capture-capable visualizer selected by --viz. Without --viz, it uses the first configured visualizer (preferring one that supports capture), or creates a headless Kit visualizer when none is configured. To record several sources at once, list one VideoRecorderCfg per source in env_cfg.video_recorders; --video_length and --video_interval apply to every recorder:

uv run --extra isaacsim --extra video isaaclab train --rl_library rsl_rl \
--task Isaac-Cartpole --video

See the —video sources for the command-line selection rules, 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.

# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""Tutorial: recording video from visualizers and scene sensors.
This script demonstrates three progressively richer recording configurations,
all using the Shadow Hand cube-reorientation task
(`Isaac-Reorient-Cube-Shadow-Camera-Direct`).
Example 1 — Kit viewport (simplest)
One clip from the Kit interactive viewport, showing 4 parallel environments.
~~~bash
uv run python scripts/tutorials/07_visualizers/run_video_recording.py \
--example 1 --num_envs 4
~~~
Example 2 — scene sensor only, headless
One clip captured directly from the scene's tiled-camera sensor.
No visualizer window opens; the sensor renders offline.
~~~bash
uv run python scripts/tutorials/07_visualizers/run_video_recording.py \
--example 2 --num_envs 16
~~~
Example 3 — Kit viewport + Kit tiled grid + Newton viewport + scene sensor
Four independent clip streams recorded simultaneously.
~~~bash
uv run python scripts/tutorials/07_visualizers/run_video_recording.py \
--example 3 --num_envs 4
~~~
Clips are written to `videos/recording_tutorial/example_<N>/` in the working directory.
Examples 1 and 2 each demonstrate one recording source; Example 3 combines all of them.
"""
from __future__ import annotations
import argparse
import contextlib
import os
import sys
import gymnasium as gym
import torch
import isaaclab_tasks # noqa: F401
with contextlib.suppress(ImportError):
import isaaclab_tasks_experimental # noqa: F401
from isaaclab.app import add_launcher_args, launch_simulation
from isaaclab.envs.utils.video_recorder_cfg import VideoRecorderCfg
from isaaclab_tasks.utils import resolve_task_config, setup_preset_cli
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
_VIDEO_LENGTH = 100 # env steps per clip
_NUM_STEPS = 115 # slightly more than _VIDEO_LENGTH so the clip flushes cleanly
_TASK_SHADOW = "Isaac-Reorient-Cube-Shadow-Camera-Direct"
# Kit viewport camera: positioned to show a 2×2 grid of Shadow Hand environments.
# env_spacing=1.0 with 4 envs → envs centered at ±0.5 in x and y.
# Cube spawns at ~(0, -0.39, 0.6) per env; wrist cylinder is the landmark at the top.
_SHADOW_EYE = (0.0, -2.2, 1.8)
_SHADOW_LOOKAT = (0.0, -0.1, 0.4)
_SHADOW_ENV_SPACING = 1.0
# Skip the first few steps so the RTX renderer has warmed up before recording starts.
_KIT_STEP_OFFSET = 5
def _output_dir(example: int) -> str:
return os.path.join("videos", "recording_tutorial", f"example_{example}")
def _shadow_env_cfg(num_envs: int, env_spacing: float = _SHADOW_ENV_SPACING):
"""Build a base Shadow Hand camera env cfg shared by all examples."""
env_cfg, _ = resolve_task_config(_TASK_SHADOW, "", overrides=(*sys.argv[1:], "env.scene.tiled_camera=rgb"))
env_cfg.scene.tiled_camera.height = 256
env_cfg.scene.tiled_camera.width = 256
env_cfg.scene.num_envs = num_envs
env_cfg.scene.env_spacing = env_spacing
return env_cfg
# ---------------------------------------------------------------------------
# Per-example environment config builders
# ---------------------------------------------------------------------------
def _build_env_cfg_example_1(num_envs: int):
"""Shadow Hand + Kit viewport: one clip from the interactive viewport."""
from isaaclab_visualizers.kit import KitVisualizerCfg
env_cfg = _shadow_env_cfg(num_envs)
env_cfg.sim.visualizer_cfgs = [KitVisualizerCfg(eye=_SHADOW_EYE, lookat=_SHADOW_LOOKAT)]
out = _output_dir(1)
env_cfg.video_recorders = [
VideoRecorderCfg(
source="visualizer:kit",
output_dir=out,
output_filename_prefix="kit_viewport",
video_length=_VIDEO_LENGTH,
fps=30,
step_offset=_KIT_STEP_OFFSET,
),
]
return env_cfg, _TASK_SHADOW
def _build_env_cfg_example_2(num_envs: int):
"""Shadow Hand + headless: scene tiled-camera sensor clip only."""
env_cfg = _shadow_env_cfg(num_envs, env_spacing=2.0)
env_cfg.sim.visualizer_cfgs = [] # no interactive visualizer
out = _output_dir(2)
env_cfg.video_recorders = [
VideoRecorderCfg(
source="sensor:tiled_camera",
output_dir=out,
output_filename_prefix="sensor",
video_length=_VIDEO_LENGTH,
fps=30,
),
]
return env_cfg, _TASK_SHADOW
def _build_env_cfg_example_3(num_envs: int):
"""Shadow Hand + Kit viewport + Kit tiled grid + Newton viewport + sensor: four simultaneous streams.
Note: `source='visualizer:newton'` captures the full Newton GL window. When
`streaming_view=True` is set on `~isaaclab_visualizers.newton.NewtonGLVisualizerCfg`,
the GL window displays the per-environment camera panel, so this effectively records
a Newton streaming view without a separate `render_tiled_rgb_array()` call.
"""
from isaaclab_visualizers.kit import KitVisualizerCfg
from isaaclab_visualizers.newton import NewtonGLVisualizerCfg
env_cfg = _shadow_env_cfg(num_envs)
kit_cfg = KitVisualizerCfg(
eye=_SHADOW_EYE,
lookat=_SHADOW_LOOKAT,
streaming_view=True,
streaming_envs=min(num_envs, 16),
# No streaming_sensor_prim_path/streaming_cam_target_prim_path: adopts the existing
# tiled_camera sensor automatically, so the streaming panel shows the same
# RTX-rendered views as source="sensor:tiled_camera".
)
newton_cfg = NewtonGLVisualizerCfg(
eye=_SHADOW_EYE,
lookat=_SHADOW_LOOKAT,
window_width=1280,
window_height=720,
focal_length=25.0,
)
env_cfg.sim.visualizer_cfgs = [kit_cfg, newton_cfg]
out = _output_dir(3)
env_cfg.video_recorders = [
VideoRecorderCfg(
source="visualizer:kit",
output_dir=out,
output_filename_prefix="kit_viewport",
video_length=_VIDEO_LENGTH,
fps=30,
step_offset=_KIT_STEP_OFFSET,
),
VideoRecorderCfg(
source="visualizer:kit:streaming_view",
output_dir=out,
output_filename_prefix="tiled_kit_viewport",
video_length=_VIDEO_LENGTH,
fps=30,
step_offset=_KIT_STEP_OFFSET,
),
VideoRecorderCfg(
source="visualizer:newton",
output_dir=out,
output_filename_prefix="newton_viewport",
video_length=_VIDEO_LENGTH,
fps=30,
),
VideoRecorderCfg(
source="sensor:tiled_camera",
output_dir=out,
output_filename_prefix="sensor",
video_length=_VIDEO_LENGTH,
fps=30,
),
]
return env_cfg, _TASK_SHADOW
_BUILDERS = {
1: _build_env_cfg_example_1,
2: _build_env_cfg_example_2,
3: _build_env_cfg_example_3,
}
# ---------------------------------------------------------------------------
# Argument parsing
# ---------------------------------------------------------------------------
parser = argparse.ArgumentParser(description="Video recording tutorial for Isaac Lab environments.")
parser.add_argument(
"--example", type=int, default=1, choices=[1, 2, 3], help="Which recording example to run (1, 2, or 3)."
)
parser.add_argument("--num_envs", type=int, default=None, help="Number of environments to simulate.")
add_launcher_args(parser)
args_cli, hydra_args = setup_preset_cli(parser)
sys.argv = [sys.argv[0]] + hydra_args
def main():
"""Run the selected video recording example."""
defaults = {1: 4, 2: 16, 3: 4}
num_envs = args_cli.num_envs if args_cli.num_envs is not None else defaults[args_cli.example]
env_cfg, task = _BUILDERS[args_cli.example](num_envs)
env_cfg.sim.device = args_cli.device if args_cli.device is not None else env_cfg.sim.device
# Examples 1 and 3 record from the Kit viewport via omni.replicator, which requires
# camera rendering support. Force it here for visualizer-only recording.
if args_cli.example in (1, 3):
args_cli.enable_cameras = True
with launch_simulation(env_cfg, args_cli):
env = gym.make(task, cfg=env_cfg)
out = _output_dir(args_cli.example)
print(f"[INFO]: Running Example {args_cli.example} — clips → {out}/")
print(f"[INFO]: Gym observation space: {env.observation_space}")
print(f"[INFO]: Gym action space: {env.action_space}")
print("[INFO]: Setup complete.")
env.reset()
for _ in range(_NUM_STEPS):
with torch.inference_mode():
actions = 2 * torch.rand(env.action_space.shape, device=env.unwrapped.device) - 1
env.step(actions)
env.close()
print(f"[INFO]: Done. Clips written to {out}/")
if __name__ == "__main__":
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

Watch the embedded video

Example 2: Scene sensor, headless

uv run python scripts/tutorials/07_visualizers/run_video_recording.py \
--example 2 --num_envs 16

Watch the embedded video

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

Watch the embedded video Watch the embedded video

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 --extra isaacsim --extra video isaaclab train --rl_library rsl_rl \
--task Isaac-Cartpole --video \
--video_length 200 --video_interval 2000

With no --viz or configured visualizer, --video records from a headless Kit 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:

from isaaclab.envs.utils.video_recorder_cfg import VideoRecorderCfg
from isaaclab_visualizers.newton import NewtonGLVisualizerCfg
from isaaclab_visualizers.viser import ViserVisualizerCfg
env_cfg.sim.visualizer_cfgs = [
ViserVisualizerCfg(),
NewtonGLVisualizerCfg(headless=True),
]
env_cfg.video_recorders = [VideoRecorderCfg(source="visualizer: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, configure both and omit --viz so the configuration selects them:

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

Alternatively, use a ~isaaclab.sensors.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 stringCaptures from
"visualizer" (default)First active recording-capable visualizer
"visualizer:kit"Kit visualizer viewport
"visualizer:kit:streaming_view"Kit streaming camera panel, requires streaming_view=True
"visualizer:newton_gl"Newton GL visualizer viewport
"visualizer:newton_rtx"Newton OVRTX path-traced viewport
"visualizer: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 named visualizer source must also have a matching active visualizer in sim.visualizer_cfgs. Set its headless=True when it should capture without a window. The deprecated visualizer:newton source still selects Newton GL; use visualizer:newton_gl.

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

Clip control

FieldDefaultMeaning
video_length200Env steps per clip
video_interval00 = one clip starting at step 1; N > 0 = new clip every N steps
fpsNoneOutput 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_clipsNoneDelete 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)

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_gl" for guaranteed capture with Newton physics.

  • source="visualizer:newton_gl: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 sensor must exist on the scene config with the recorded channel ("rgb" by default) in its data_types.

Visualizer--videoNotes
kit✓Launches Isaac Sim; the launch enables camera rendering for the recording.
newton_gl✓Uses pyglet’s EGL backend when headless; select it with --viz newton_gl or configure a NewtonGLVisualizerCfg.
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. --video --viz rerun raises an error; select it together with a capture-capable visualizer to record.
viser✗Browser streaming tool; no local frame-capture API. --video --viz viser raises an error; select it together with a capture-capable visualizer to record.

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

See also