Skip to content

Commit 2e6367f

Browse files
authored
Merge pull request #18 from swhan0329/refactor/remove-legacy-video-helper
Refactor input capture and harden output writer fallback
2 parents 6e6e90b + 71500bf commit 2e6367f

File tree

6 files changed

+154
-354
lines changed

6 files changed

+154
-354
lines changed

app/io/video_source.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,29 @@
11
from __future__ import annotations
22

3+
from pathlib import Path
4+
35
import cv2 as cv
46

5-
import video
7+
8+
def _resolve_capture_source(video_src: str) -> int | str:
9+
raw = str(video_src).strip()
10+
path_candidate = Path(raw)
11+
12+
# Keep existing files as path inputs, even if the filename is numeric.
13+
if path_candidate.exists():
14+
return raw
15+
16+
if raw.lstrip("+-").isdigit():
17+
return int(raw)
18+
19+
return raw
620

721

822
def open_video_source(video_src: str) -> cv.VideoCapture:
9-
"""Open a source path or camera index using the existing helper."""
23+
"""Open a source path or camera index with OpenCV VideoCapture."""
1024

11-
return video.create_capture(video_src)
25+
source = _resolve_capture_source(video_src)
26+
return cv.VideoCapture(source)
1227

1328

1429
def resolve_fps(capture: cv.VideoCapture, fallback_fps: float) -> float:

app/pipeline.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,31 @@ def _create_writer(
5050
if not output_path:
5151
return None
5252

53-
fourcc = cv.VideoWriter_fourcc(
54-
*("mp4v" if output_path.lower().endswith(".mp4") else "XVID")
53+
output = Path(output_path)
54+
output.parent.mkdir(parents=True, exist_ok=True)
55+
56+
if output.suffix.lower() == ".mp4":
57+
codec_candidates = ["mp4v", "avc1", "MJPG", "XVID"]
58+
else:
59+
codec_candidates = ["XVID", "MJPG", "mp4v", "avc1"]
60+
61+
attempted: list[str] = []
62+
for codec in codec_candidates:
63+
attempted.append(codec)
64+
fourcc = cv.VideoWriter_fourcc(*codec)
65+
writer = cv.VideoWriter(str(output), fourcc, fps, frame_size)
66+
if writer.isOpened():
67+
if len(attempted) > 1:
68+
print(f"Output writer fallback succeeded with codec {codec}")
69+
return writer
70+
writer.release()
71+
72+
attempted_str = ", ".join(attempted)
73+
print(
74+
"Warning: unable to open output file: "
75+
f"{output_path} (attempted codecs: {attempted_str})"
5576
)
56-
writer = cv.VideoWriter(output_path, fourcc, fps, frame_size)
57-
if not writer.isOpened():
58-
print(f"Warning: unable to open output file: {output_path}")
59-
return None
60-
return writer
77+
return None
6178

6279

6380
def run_pipeline(

tests/test_video_source.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import os
2+
import tempfile
3+
import unittest
4+
from pathlib import Path
5+
from unittest.mock import patch
6+
7+
from app.io.video_source import _resolve_capture_source, open_video_source
8+
9+
10+
class TestVideoSource(unittest.TestCase):
11+
def test_resolve_capture_source_uses_camera_index_for_numeric_string(self):
12+
self.assertEqual(_resolve_capture_source("0"), 0)
13+
self.assertEqual(_resolve_capture_source("-1"), -1)
14+
15+
def test_resolve_capture_source_keeps_existing_numeric_filename(self):
16+
with tempfile.TemporaryDirectory() as temp_dir:
17+
file_path = Path(temp_dir) / "0"
18+
file_path.write_text("dummy", encoding="utf-8")
19+
20+
previous_cwd = os.getcwd()
21+
try:
22+
os.chdir(temp_dir)
23+
self.assertEqual(_resolve_capture_source("0"), "0")
24+
finally:
25+
os.chdir(previous_cwd)
26+
27+
def test_resolve_capture_source_keeps_non_numeric_source(self):
28+
self.assertEqual(
29+
_resolve_capture_source("samples/MNn9qKG2UFI_10s.mp4"),
30+
"samples/MNn9qKG2UFI_10s.mp4",
31+
)
32+
self.assertEqual(
33+
_resolve_capture_source("rtsp://127.0.0.1:8554/stream"),
34+
"rtsp://127.0.0.1:8554/stream",
35+
)
36+
37+
def test_open_video_source_passes_resolved_source(self):
38+
with patch("app.io.video_source.cv.VideoCapture") as mock_capture:
39+
open_video_source("2")
40+
mock_capture.assert_called_once_with(2)
41+
42+
43+
if __name__ == "__main__":
44+
unittest.main()

tests/test_writer_creation.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import tempfile
2+
import unittest
3+
from pathlib import Path
4+
from unittest.mock import patch
5+
6+
from app.pipeline import _create_writer
7+
8+
9+
class _FakeWriter:
10+
def __init__(self, opened: bool) -> None:
11+
self._opened = opened
12+
self.released = False
13+
14+
def isOpened(self) -> bool:
15+
return self._opened
16+
17+
def release(self) -> None:
18+
self.released = True
19+
20+
21+
class TestWriterCreation(unittest.TestCase):
22+
def test_create_writer_falls_back_and_creates_parent_directory(self):
23+
with tempfile.TemporaryDirectory() as temp_dir:
24+
output_path = Path(temp_dir) / "nested" / "out.mp4"
25+
writers = [_FakeWriter(False), _FakeWriter(False), _FakeWriter(True)]
26+
calls: list[tuple[str, int, float, tuple[int, int]]] = []
27+
28+
def fake_video_writer(
29+
path: str,
30+
fourcc: int,
31+
fps: float,
32+
size: tuple[int, int],
33+
) -> _FakeWriter:
34+
calls.append((path, fourcc, fps, size))
35+
return writers[len(calls) - 1]
36+
37+
with (
38+
patch("app.pipeline.cv.VideoWriter_fourcc", side_effect=[1, 2, 3]),
39+
patch("app.pipeline.cv.VideoWriter", side_effect=fake_video_writer),
40+
):
41+
writer = _create_writer(str(output_path), (1280, 720), 30.0)
42+
43+
self.assertIs(writer, writers[2])
44+
self.assertEqual(len(calls), 3)
45+
self.assertTrue(writers[0].released)
46+
self.assertTrue(writers[1].released)
47+
self.assertTrue(output_path.parent.exists())
48+
49+
def test_create_writer_returns_none_when_all_codecs_fail(self):
50+
with tempfile.TemporaryDirectory() as temp_dir:
51+
output_path = Path(temp_dir) / "out.mp4"
52+
writers = [_FakeWriter(False), _FakeWriter(False), _FakeWriter(False), _FakeWriter(False)]
53+
54+
with (
55+
patch("app.pipeline.cv.VideoWriter_fourcc", side_effect=[1, 2, 3, 4]),
56+
patch("app.pipeline.cv.VideoWriter", side_effect=writers),
57+
patch("builtins.print") as mock_print,
58+
):
59+
writer = _create_writer(str(output_path), (640, 480), 30.0)
60+
61+
self.assertIsNone(writer)
62+
self.assertTrue(all(item.released for item in writers))
63+
printed = " ".join(str(arg) for arg in mock_print.call_args_list[-1][0])
64+
self.assertIn("attempted codecs", printed)
65+
66+
67+
if __name__ == "__main__":
68+
unittest.main()

tst_scene_render.py

Lines changed: 0 additions & 121 deletions
This file was deleted.

0 commit comments

Comments
 (0)