> This page is for version Release 3.0 (default).
> For other versions, use one of these documentation indexes:
> - Release 3.0 (default): https://docs.nvidia.com/isaaclab/3.0/llms.txt
> - Release 2.3: https://docs.nvidia.com/isaaclab/2.3/llms.txt
> - Release 2.2: https://docs.nvidia.com/isaaclab/2.2/llms.txt
> - Release 2.1: https://docs.nvidia.com/isaaclab/2.1/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.nvidia.com/isaaclab/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.nvidia.com/isaaclab/_mcp/server.

# Recording video

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

[Watch the embedded video](https://download.isaacsim.omniverse.nvidia.com/isaaclab/images/record_video_shadow_hand_tiled_gt.mp4)
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`:

```python
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:

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

See [the --video sources](https://isaac-sim.github.io/IsaacLab/release/3.0.0/source/concepts/visualization.html) 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.

```python
# 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

```bash
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](https://download.isaacsim.omniverse.nvidia.com/isaaclab/images/record_video_kit_viewport.mp4)

### Example 2: Scene sensor, headless

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

[Watch the embedded video](https://download.isaacsim.omniverse.nvidia.com/isaaclab/images/record_video_sensor.mp4)

* 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

```bash
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](https://download.isaacsim.omniverse.nvidia.com/isaaclab/images/record_video_newton_viewport.mp4)

[Watch the embedded video](https://download.isaacsim.omniverse.nvidia.com/isaaclab/images/record_video_tiled_kit_viewport.mp4)

## 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:

```bash
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:

```python
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:

```python
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:

```python
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 string                           | Captures 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`.

> **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:**

```python
VideoRecorderCfg(source="visualizer:kit", video_length=500, video_interval=0)
```

**Recurring clips every 1,000 env steps:**

```python
VideoRecorderCfg(source="visualizer:kit", video_length=200, video_interval=1000)
```

**Keep only the most recent clip on disk:**

```python
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   | `--video` | Notes                                                                                                                                                      |
| ------------ | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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

* [https://isaac-sim.github.io/IsaacLab/release/3.0.0/source/concepts/visualization.html](https://isaac-sim.github.io/IsaacLab/release/3.0.0/source/concepts/visualization.html): configuring interactive visualizers and recording from them
* [/source/how-to/visualizer_streaming_camera_view](/source/how-to/visualizer_streaming_camera_view): tiled camera panel setup
* [/source/how-to/capture_sensor_frames](/source/how-to/capture_sensor_frames): saving per-frame sensor outputs as images