Skip to content

Commit c0c29c7

Browse files
Continue video clip indices after existing files
Previously, VideoRecorder started every process at clip index zero. Recording into an existing output directory could overwrite clip_0000.mp4 and any later clips from a previous play or replay run. Initialize the recorder index from the highest existing file matching the configured prefix. Fresh or empty directories still start at zero.
1 parent c647d39 commit c0c29c7

4 files changed

Lines changed: 78 additions & 9 deletions

File tree

CONTRIBUTORS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ Guidelines for modifications:
8383
* Daniela Hasenbring
8484
* Dhananjay Shendre
8585
* Dhyan Thakkar
86+
* Diego Ferigo
8687
* Dongxuan Fan
8788
* Dorsa Rohani
8889
* Ege Sekkin
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fixed
2+
^^^^^
3+
4+
* Fixed video recording overwriting existing clips when a new process writes to a non-empty output directory.

source/isaaclab/isaaclab/envs/utils/video_recorder.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import logging
1616
import os
17+
import re
1718
from typing import TYPE_CHECKING
1819

1920
import numpy as np
@@ -78,7 +79,7 @@ def __init__(self, cfg: VideoRecorderCfg, env: object):
7879
self._frames: list[np.ndarray] = []
7980
self._step_count = 0
8081
self._frames_step_count = 0
81-
self._clip_index = 0
82+
self._clip_index = self._next_clip_index()
8283
self._recording = False
8384
# Set to True after the first unrecoverable frame-capture error so that
8485
# subsequent steps do not propagate the exception or repeat the log message.
@@ -300,6 +301,21 @@ def _effective_output_dir(self) -> str:
300301
def _clip_path(self, index: int) -> str:
301302
return os.path.join(self._effective_output_dir(), f"{self.cfg.output_filename_prefix}_{index:04d}.mp4")
302303

304+
def _next_clip_index(self) -> int:
305+
return max(self._existing_clip_indices(), default=-1) + 1
306+
307+
def _existing_clip_indices(self) -> list[int]:
308+
output_dir = self._effective_output_dir()
309+
if not os.path.isdir(output_dir):
310+
return []
311+
312+
pattern = re.compile(rf"^{re.escape(str(self.cfg.output_filename_prefix))}_(?P<index>\d+)\.mp4$")
313+
return [
314+
int(match.group("index"))
315+
for filename in os.listdir(output_dir)
316+
if (match := pattern.match(filename)) is not None
317+
]
318+
303319
def _close_clip(self) -> None:
304320
if not self._frames:
305321
self._recording = False
@@ -343,7 +359,9 @@ def _maybe_delete_old_clips(self) -> None:
343359
if self.cfg.keep_last_n_clips is None:
344360
return
345361
cutoff = self._clip_index - self.cfg.keep_last_n_clips
346-
for index in range(max(0, cutoff)):
362+
for index in self._existing_clip_indices():
363+
if index >= cutoff:
364+
continue
347365
path = self._clip_path(index)
348366
try:
349367
os.remove(path)

source/isaaclab/test/envs/test_video_recorder.py

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,28 @@ def test_init_raises_import_error_when_moviepy_missing():
106106
VideoRecorder(_cfg(), _make_env())
107107

108108

109+
def test_init_continues_clip_index_after_existing_files(tmp_path):
110+
output_dir = tmp_path / "videos"
111+
output_dir.mkdir()
112+
(output_dir / "clip_0000.mp4").touch()
113+
(output_dir / "clip_0007.mp4").touch()
114+
(output_dir / "clip_final.mp4").touch()
115+
(output_dir / "other_0008.mp4").touch()
116+
117+
recorder = VideoRecorder(_cfg(output_dir=str(output_dir), output_filename_prefix="clip"), _make_env())
118+
119+
assert recorder._clip_index == 8
120+
121+
122+
def test_init_starts_clip_index_at_zero_for_empty_output_dir(tmp_path):
123+
output_dir = tmp_path / "videos"
124+
output_dir.mkdir()
125+
126+
recorder = VideoRecorder(_cfg(output_dir=str(output_dir)), _make_env())
127+
128+
assert recorder._clip_index == 0
129+
130+
109131
# ---------------------------------------------------------------------------
110132
# Trigger logic
111133
# ---------------------------------------------------------------------------
@@ -419,24 +441,48 @@ def test_keep_last_n_clips_prunes_old_clips():
419441
_cfg(output_dir="/tmp/test_prune", keep_last_n_clips=2),
420442
_make_env(),
421443
)
444+
recorder._clip_index = 3
422445
removed = []
423446

424447
def fake_remove(path):
425448
removed.append(path)
426449

427-
mock_clip = MagicMock()
428-
with patch("isaaclab.envs.utils.video_recorder.ImageSequenceClip", return_value=mock_clip):
429-
with patch("isaaclab.envs.utils.video_recorder.os.makedirs"):
450+
with patch("isaaclab.envs.utils.video_recorder.os.path.isdir", return_value=True):
451+
with patch(
452+
"isaaclab.envs.utils.video_recorder.os.listdir",
453+
return_value=["clip_0000.mp4", "clip_0001.mp4", "clip_0002.mp4"],
454+
):
430455
with patch("isaaclab.envs.utils.video_recorder.os.remove", side_effect=fake_remove):
431-
for i in range(3):
432-
recorder._frames = [_FRAME.copy()]
433-
recorder._recording = True
434-
recorder._close_clip()
456+
recorder._maybe_delete_old_clips()
435457

436458
# After 3 clips with keep_last_n_clips=2, clip index 0 should be removed.
437459
assert any("_0000.mp4" in p for p in removed), f"Expected clip 0 to be removed, got: {removed}"
438460

439461

462+
def test_keep_last_n_clips_prunes_only_existing_sparse_clips():
463+
"""Sparse clip indices must not trigger one deletion attempt per missing index."""
464+
465+
recorder = VideoRecorder(
466+
_cfg(output_dir="/tmp/test_sparse_prune", keep_last_n_clips=2),
467+
_make_env(),
468+
)
469+
recorder._clip_index = 10_000
470+
removed = []
471+
472+
def fake_remove(path):
473+
removed.append(path)
474+
475+
with patch("isaaclab.envs.utils.video_recorder.os.path.isdir", return_value=True):
476+
with patch(
477+
"isaaclab.envs.utils.video_recorder.os.listdir",
478+
return_value=["clip_0001.mp4", "clip_9997.mp4", "clip_9998.mp4", "other_0000.mp4"],
479+
):
480+
with patch("isaaclab.envs.utils.video_recorder.os.remove", side_effect=fake_remove):
481+
recorder._maybe_delete_old_clips()
482+
483+
assert removed == ["/tmp/test_sparse_prune/clip_0001.mp4", "/tmp/test_sparse_prune/clip_9997.mp4"]
484+
485+
440486
# ---------------------------------------------------------------------------
441487
# Minor 14: partial-clip close() flush
442488
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)