Centralize video-recording env wrapping#801
Conversation
…ecording_per_episode
Move the inline RecordVideo / CameraObsVideoRecorder wrapping out of policy_runner and eval_runner into a single video_recording module (VideoRecordingCfg + wrap_env_for_video), so the gym-wrapper plumbing lives in one place instead of being duplicated across the two runners. Signed-off-by: alex <amillane@nvidia.com>
Greptile SummaryThis PR extracts the duplicated
Confidence Score: 4/5Safe to merge for single-rank runs; distributed video output directories may diverge across ranks if processes cross the The refactor is clean and the isaaclab_arena/evaluation/policy_runner.py — the Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant CLI as CLI / args_cli
participant PR as policy_runner.py
participant ER as eval_runner.py
participant VR as video_recording.py
participant RV as RecordVideo (gym)
participant CR as CameraObsVideoRecorder
CLI->>PR: --record_viewport_video / --record_camera_video
PR->>VR: timestamped_run_dir(video_base_dir)
VR-->>PR: run_dir (per-rank timestamp ⚠️)
PR->>VR: VideoRecordingCfg(run_dir)
PR->>VR: wrap_env_for_video(env, cfg, num_steps, num_episodes)
VR->>RV: RecordVideo(env, video_folder, video_length) [if record_viewport_video]
VR->>CR: CameraObsVideoRecorder(env, video_folder) [if record_camera_video]
VR-->>PR: wrapped env
CLI->>ER: --record_viewport_video / --record_camera_video
ER->>VR: timestamped_run_dir(video_base_dir) [once, shared]
loop each job
ER->>VR: VideoRecordingCfg(run_dir/job.name)
ER->>VR: wrap_env_for_video(env, cfg, num_steps, num_episodes)
VR->>RV: RecordVideo(...) [if record_viewport_video]
VR->>CR: CameraObsVideoRecorder(...) [if record_camera_video]
VR-->>ER: wrapped env
end
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant CLI as CLI / args_cli
participant PR as policy_runner.py
participant ER as eval_runner.py
participant VR as video_recording.py
participant RV as RecordVideo (gym)
participant CR as CameraObsVideoRecorder
CLI->>PR: --record_viewport_video / --record_camera_video
PR->>VR: timestamped_run_dir(video_base_dir)
VR-->>PR: run_dir (per-rank timestamp ⚠️)
PR->>VR: VideoRecordingCfg(run_dir)
PR->>VR: wrap_env_for_video(env, cfg, num_steps, num_episodes)
VR->>RV: RecordVideo(env, video_folder, video_length) [if record_viewport_video]
VR->>CR: CameraObsVideoRecorder(env, video_folder) [if record_camera_video]
VR-->>PR: wrapped env
CLI->>ER: --record_viewport_video / --record_camera_video
ER->>VR: timestamped_run_dir(video_base_dir) [once, shared]
loop each job
ER->>VR: VideoRecordingCfg(run_dir/job.name)
ER->>VR: wrap_env_for_video(env, cfg, num_steps, num_episodes)
VR->>RV: RecordVideo(...) [if record_viewport_video]
VR->>CR: CameraObsVideoRecorder(...) [if record_camera_video]
VR-->>ER: wrapped env
end
Reviews (5): Last reviewed commit: "Merge branch 'main' into alex/refactor/c..." | Re-trigger Greptile |
| def _resolve_video_length(env, num_steps: int | None, num_episodes: int | None) -> int: | ||
| """Number of env steps to record: the step budget, or one episode's worth per episode. | ||
|
|
||
| ``max_episode_length`` is in environment steps, which matches the rollout cadence. | ||
| """ | ||
| if num_steps is not None: | ||
| return num_steps | ||
| return num_episodes * env.unwrapped.max_episode_length |
There was a problem hiding this comment.
Missing guard against both
num_steps and num_episodes being None. When _resolve_video_length is reached with both arguments None, None * env.unwrapped.max_episode_length raises an unhelpful TypeError. This can happen in eval_runner when a job has neither a step nor episode budget and the policy doesn't report a length, while --video is enabled. Adding an explicit check here makes the failure message actionable.
| def _resolve_video_length(env, num_steps: int | None, num_episodes: int | None) -> int: | |
| """Number of env steps to record: the step budget, or one episode's worth per episode. | |
| ``max_episode_length`` is in environment steps, which matches the rollout cadence. | |
| """ | |
| if num_steps is not None: | |
| return num_steps | |
| return num_episodes * env.unwrapped.max_episode_length | |
| def _resolve_video_length(env, num_steps: int | None, num_episodes: int | None) -> int: | |
| """Number of env steps to record: the step budget, or one episode's worth per episode. | |
| ``max_episode_length`` is in environment steps, which matches the rollout cadence. | |
| """ | |
| if num_steps is not None: | |
| return num_steps | |
| if num_episodes is None: | |
| raise ValueError( | |
| "wrap_env_for_video: cannot determine video length — " | |
| "both num_steps and num_episodes are None." | |
| ) | |
| return num_episodes * env.unwrapped.max_episode_length |
| print(f"Recording {video_length}-step viewport video to: {video_cfg.video_dir}") | ||
|
|
||
| # --camera_video records the embodiment-mounted cameras (from obs["camera_obs"]), | ||
| # flushed at each episode reset rather than after a fixed number of steps. | ||
| if video_cfg.camera_video: | ||
| env = CameraObsVideoRecorder( | ||
| env, | ||
| video_folder=video_cfg.video_dir, | ||
| name_prefix=video_cfg.camera_name_prefix, | ||
| ) | ||
| print(f"Recording per-episode per-camera videos to: {video_cfg.video_dir}") |
There was a problem hiding this comment.
Rank context dropped from log messages
The original policy_runner.py included [Rank {local_rank}/{world_size}] in both recording log lines, which is useful when running distributed rollouts to identify which worker is actually writing video. The new generic messages in wrap_env_for_video omit that context, making multi-rank logs harder to correlate. Consider accepting an optional log_prefix: str = "" argument so callers can inject rank context.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
alexmillane
left a comment
There was a problem hiding this comment.
Self review.
| video_folder=args_cli.video_dir, | ||
| ) | ||
| print(f"[Rank {local_rank}/{world_size}] Recording per-episode per-camera videos to: {args_cli.video_dir}") | ||
| # Optionally wrap with the viewport/camera video recorders (both independent). |
There was a problem hiding this comment.
Comment: "Optionally wrap with the viewport/camera video recorders"
| """Central helpers for wrapping a rollout env with video recorders. | ||
|
|
||
| Two independent recordings are supported and may be active at the same time: | ||
|
|
||
| * ``video`` records the kit viewport (third-person scene view) via ``env.render()`` | ||
| using gymnasium's ``RecordVideo``. | ||
| * ``camera_video`` records the embodiment-mounted cameras from ``obs['camera_obs']`` | ||
| using ``CameraObsVideoRecorder``. | ||
|
|
||
| The runners (``policy_runner``, ``eval_runner``) build a ``VideoRecordingCfg`` from their | ||
| CLI/job options and apply it through ``wrap_env_for_video``, so the gym-wrapper plumbing lives | ||
| in one place instead of being duplicated inline. | ||
| """ |
There was a problem hiding this comment.
Remove module level comment.
| class VideoRecordingCfg: | ||
| """Options describing which rollout video recorders to enable and where to write them.""" | ||
|
|
||
| video: bool = False |
There was a problem hiding this comment.
Change variable name to record_viewport_video and corresponding flags.
| video: bool = False | ||
| """Record the kit viewport (third-person scene view) via ``env.render()``.""" | ||
|
|
||
| camera_video: bool = False |
There was a problem hiding this comment.
Change variable name to record_camera_video and corresponding flags
| camera_video: bool = False | ||
| """Record the embodiment-mounted cameras from ``obs['camera_obs']``.""" | ||
|
|
||
| video_dir: str = "videos" |
There was a problem hiding this comment.
Can we make this video_base_dir (change corresponding flags).
Then we should add a subdirectory with a reverse dated folder, like Isaac Lab does for its logs.
|
|
||
| Args: | ||
| env: The env to wrap (already built with ``video_cfg.render_mode`` when ``video`` is set). | ||
| video_cfg: Which recorders to enable and where to write them. |
There was a problem hiding this comment.
comment: video_cfg: The video recording configuration struct.
| are mutually exclusive and size the viewport video. | ||
|
|
||
| Args: | ||
| env: The env to wrap (already built with ``video_cfg.render_mode`` when ``video`` is set). |
There was a problem hiding this comment.
-> env: The env to wrap
There was a problem hiding this comment.
Suggestion to move this file into isaaclab_arena/utils folder
- Move video_recording.py to isaaclab_arena/utils/ and drop its module docstring. - Rename VideoRecordingCfg fields and the corresponding CLI flags: --video -> --record_viewport_video, --camera_video -> --record_camera_video, --video_dir -> --video_base_dir. - Write videos into a reverse-dated run subdirectory (timestamped_run_dir), shared across all jobs in an eval run, mirroring Isaac Lab's log layout. - Guard _resolve_video_length when both num_steps and num_episodes are None. - Tidy wrap_env_for_video docstring args. Signed-off-by: alex <amillane@nvidia.com>
|
Addressed the review feedback in 0920eb0:
|
Use underscore-only flag names (--record_viewport_video, --record_camera_video, --video_base_dir) to match the convention used by every other CLI argument. Signed-off-by: alex <amillane@nvidia.com>
- Move camera_video.py -> video/camera_observation_video_recorder.py. - Move video_recording.py from utils/ -> video/. - Rename the test file to match (test_camera_observation_video_recorder.py). - Update all imports and mock patch targets to the new module paths. Signed-off-by: alex <amillane@nvidia.com>
Signed-off-by: alex <amillane@nvidia.com> # Conflicts: # isaaclab_arena/evaluation/camera_video.py # isaaclab_arena/evaluation/eval_runner.py # isaaclab_arena/evaluation/eval_runner_cli.py # isaaclab_arena/evaluation/policy_runner.py
Summary
Move the inline video-recorder wrapping out of the two rollout runners into a single
video_recordingmodule.Details
eval_runner.pyandpolicy_runner.py