OTVision 0.6.7__py3-none-any.whl → 0.6.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- OTVision/application/config.py +18 -0
- OTVision/application/config_parser.py +17 -9
- OTVision/application/detect/update_detect_config_with_cli_args.py +16 -0
- OTVision/application/event/__init__.py +0 -0
- OTVision/application/event/new_video_start.py +9 -0
- OTVision/application/video/__init__.py +0 -0
- OTVision/application/video/generate_video.py +15 -0
- OTVision/config.py +2 -0
- OTVision/detect/builder.py +27 -8
- OTVision/detect/cli.py +44 -1
- OTVision/detect/detected_frame_producer.py +14 -0
- OTVision/detect/detected_frame_producer_factory.py +43 -0
- OTVision/detect/file_based_detect_builder.py +39 -3
- OTVision/detect/rtsp_based_detect_builder.py +35 -3
- OTVision/detect/rtsp_input_source.py +119 -34
- OTVision/detect/video_input_source.py +38 -6
- OTVision/detect/yolo.py +9 -1
- OTVision/domain/cli.py +10 -0
- OTVision/domain/input_source_detect.py +1 -3
- OTVision/domain/time.py +2 -2
- OTVision/domain/video_writer.py +30 -0
- OTVision/plugin/ffmpeg_video_writer.py +300 -0
- OTVision/plugin/generate_video.py +24 -0
- OTVision/version.py +1 -1
- {otvision-0.6.7.dist-info → otvision-0.6.8.dist-info}/METADATA +6 -5
- {otvision-0.6.7.dist-info → otvision-0.6.8.dist-info}/RECORD +28 -20
- OTVision/application/detect/detected_frame_producer.py +0 -24
- {otvision-0.6.7.dist-info → otvision-0.6.8.dist-info}/WHEEL +0 -0
- {otvision-0.6.7.dist-info → otvision-0.6.8.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import socket
|
|
1
2
|
from datetime import datetime, timedelta
|
|
2
3
|
from time import sleep
|
|
3
4
|
from typing import Generator
|
|
5
|
+
from urllib.parse import urlparse
|
|
4
6
|
|
|
5
7
|
from cv2 import (
|
|
6
8
|
CAP_PROP_FRAME_HEIGHT,
|
|
@@ -19,6 +21,7 @@ from OTVision.application.config import (
|
|
|
19
21
|
StreamConfig,
|
|
20
22
|
)
|
|
21
23
|
from OTVision.application.configure_logger import logger
|
|
24
|
+
from OTVision.application.event.new_video_start import NewVideoStartEvent
|
|
22
25
|
from OTVision.application.get_current_config import GetCurrentConfig
|
|
23
26
|
from OTVision.detect.detected_frame_buffer import FlushEvent
|
|
24
27
|
from OTVision.domain.frame import Frame
|
|
@@ -26,7 +29,7 @@ from OTVision.domain.input_source_detect import InputSourceDetect
|
|
|
26
29
|
from OTVision.domain.time import DatetimeProvider
|
|
27
30
|
|
|
28
31
|
RTSP_URL = "rtsp://127.0.0.1:8554/test"
|
|
29
|
-
RETRY_SECONDS =
|
|
32
|
+
RETRY_SECONDS = 5
|
|
30
33
|
DEFAULT_READ_FAIL_THRESHOLD = 5
|
|
31
34
|
|
|
32
35
|
|
|
@@ -79,13 +82,16 @@ class RtspInputSource(InputSourceDetect):
|
|
|
79
82
|
|
|
80
83
|
def __init__(
|
|
81
84
|
self,
|
|
82
|
-
|
|
85
|
+
subject_flush: Subject[FlushEvent],
|
|
86
|
+
subject_new_video_start: Subject[NewVideoStartEvent],
|
|
83
87
|
datetime_provider: DatetimeProvider,
|
|
84
88
|
frame_counter: Counter,
|
|
85
89
|
get_current_config: GetCurrentConfig,
|
|
86
90
|
read_fail_threshold: int = DEFAULT_READ_FAIL_THRESHOLD,
|
|
87
91
|
) -> None:
|
|
88
|
-
|
|
92
|
+
|
|
93
|
+
self.subject_flush = subject_flush
|
|
94
|
+
self.subject_new_video_start = subject_new_video_start
|
|
89
95
|
self._datetime_provider = datetime_provider
|
|
90
96
|
self._stop_capture = False
|
|
91
97
|
self._frame_counter = frame_counter
|
|
@@ -123,41 +129,50 @@ class RtspInputSource(InputSourceDetect):
|
|
|
123
129
|
def produce(self) -> Generator[Frame, None, None]:
|
|
124
130
|
self._stream_start_time = self._datetime_provider.provide()
|
|
125
131
|
self._current_video_start_time = self._stream_start_time
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
self.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
self.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
self.
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
132
|
+
try:
|
|
133
|
+
while not self.should_stop():
|
|
134
|
+
if (frame := self._read_next_frame()) is not None:
|
|
135
|
+
self._frame_counter.increment()
|
|
136
|
+
occurrence = self._datetime_provider.provide()
|
|
137
|
+
|
|
138
|
+
if self._outdated:
|
|
139
|
+
self._current_video_start_time = occurrence
|
|
140
|
+
self._outdated = False
|
|
141
|
+
self._notify_new_video_start_observers()
|
|
142
|
+
|
|
143
|
+
yield Frame(
|
|
144
|
+
data=convert_frame_to_rgb(frame), # YOLO expects RGB
|
|
145
|
+
frame=self.current_frame_number,
|
|
146
|
+
source=self.rtsp_url,
|
|
147
|
+
output=self.create_output(),
|
|
148
|
+
occurrence=occurrence,
|
|
149
|
+
)
|
|
150
|
+
if self.flush_condition_met():
|
|
151
|
+
self._notify_flush_observers()
|
|
152
|
+
self._outdated = True
|
|
153
|
+
self._frame_counter.reset()
|
|
154
|
+
self._notify_flush_observers()
|
|
155
|
+
except InvalidRtspUrlError as cause:
|
|
156
|
+
logger().error(cause)
|
|
148
157
|
|
|
149
158
|
def _init_video_capture(self, source: str) -> VideoCapture:
|
|
159
|
+
self._wait_for_connection(source)
|
|
160
|
+
|
|
150
161
|
cap = VideoCapture(source)
|
|
151
162
|
while not self.should_stop() and not cap.isOpened():
|
|
152
|
-
logger().warning(
|
|
153
|
-
f"Couldn't open the RTSP stream: {source}. "
|
|
154
|
-
f"Trying again in {RETRY_SECONDS}s..."
|
|
155
|
-
)
|
|
156
|
-
sleep(RETRY_SECONDS)
|
|
157
163
|
cap.release()
|
|
164
|
+
self._wait_for_connection(source)
|
|
158
165
|
cap = VideoCapture(source)
|
|
159
166
|
return cap
|
|
160
167
|
|
|
168
|
+
def _wait_for_connection(self, connection: str) -> None:
|
|
169
|
+
while not self.should_stop() and not is_connection_available(connection):
|
|
170
|
+
logger().debug(
|
|
171
|
+
f"Couldn't open the RTSP stream: {connection}. "
|
|
172
|
+
f"Trying again in {RETRY_SECONDS}s..."
|
|
173
|
+
)
|
|
174
|
+
sleep(RETRY_SECONDS)
|
|
175
|
+
|
|
161
176
|
def _read_next_frame(self) -> ndarray | None:
|
|
162
177
|
successful, frame = self._video_capture.read()
|
|
163
178
|
if successful:
|
|
@@ -189,9 +204,9 @@ class RtspInputSource(InputSourceDetect):
|
|
|
189
204
|
def flush_condition_met(self) -> bool:
|
|
190
205
|
return self.current_frame_number % self.flush_buffer_size == 0
|
|
191
206
|
|
|
192
|
-
def
|
|
193
|
-
frame_width =
|
|
194
|
-
frame_height =
|
|
207
|
+
def _notify_flush_observers(self) -> None:
|
|
208
|
+
frame_width = self._get_width()
|
|
209
|
+
frame_height = self._get_height()
|
|
195
210
|
frames = (
|
|
196
211
|
self.flush_buffer_size
|
|
197
212
|
if self.current_frame_number % self.flush_buffer_size == 0
|
|
@@ -199,7 +214,7 @@ class RtspInputSource(InputSourceDetect):
|
|
|
199
214
|
)
|
|
200
215
|
duration = timedelta(seconds=round(frames / self.fps))
|
|
201
216
|
output = self.create_output()
|
|
202
|
-
self.
|
|
217
|
+
self.subject_flush.notify(
|
|
203
218
|
FlushEvent.create(
|
|
204
219
|
source=self.rtsp_url,
|
|
205
220
|
output=output,
|
|
@@ -211,6 +226,21 @@ class RtspInputSource(InputSourceDetect):
|
|
|
211
226
|
)
|
|
212
227
|
)
|
|
213
228
|
|
|
229
|
+
def _get_width(self) -> int:
|
|
230
|
+
return int(self._video_capture.get(CAP_PROP_FRAME_WIDTH))
|
|
231
|
+
|
|
232
|
+
def _get_height(self) -> int:
|
|
233
|
+
return int(self._video_capture.get(CAP_PROP_FRAME_HEIGHT))
|
|
234
|
+
|
|
235
|
+
def _notify_new_video_start_observers(self) -> None:
|
|
236
|
+
event = NewVideoStartEvent(
|
|
237
|
+
output=self.create_output(),
|
|
238
|
+
width=self._get_width(),
|
|
239
|
+
height=self._get_height(),
|
|
240
|
+
fps=self.fps,
|
|
241
|
+
)
|
|
242
|
+
self.subject_new_video_start.notify(event)
|
|
243
|
+
|
|
214
244
|
def create_output(self) -> str:
|
|
215
245
|
output_filename = (
|
|
216
246
|
f"{self.stream_config.name}_FR{round(self.fps)}"
|
|
@@ -221,3 +251,58 @@ class RtspInputSource(InputSourceDetect):
|
|
|
221
251
|
|
|
222
252
|
def convert_frame_to_rgb(frame: ndarray) -> ndarray:
|
|
223
253
|
return cvtColor(frame, COLOR_BGR2RGB)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class InvalidRtspUrlError(Exception):
|
|
257
|
+
"""Raised when the RTSP URL is invalid."""
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def is_connection_available(rtsp_url: str) -> bool:
|
|
261
|
+
"""
|
|
262
|
+
Check if RTSP connection is available by sending a DESCRIBE request.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
rtsp_url: The RTSP URL to check
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
bool: True if stream is available, False otherwise
|
|
269
|
+
"""
|
|
270
|
+
try:
|
|
271
|
+
parsed = urlparse(rtsp_url)
|
|
272
|
+
if parsed.hostname is None and parsed.port is None:
|
|
273
|
+
raise InvalidRtspUrlError(
|
|
274
|
+
f"Invalid RTSP URL: {rtsp_url}. Missing hostname or port."
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
host = parsed.hostname
|
|
278
|
+
port = parsed.port
|
|
279
|
+
|
|
280
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
281
|
+
sock.settimeout(5)
|
|
282
|
+
|
|
283
|
+
if sock.connect_ex((host, port)) != 0:
|
|
284
|
+
sock.close()
|
|
285
|
+
return False
|
|
286
|
+
|
|
287
|
+
# Send RTSP DESCRIBE request to get stream info
|
|
288
|
+
rtsp_request = (
|
|
289
|
+
f"DESCRIBE {rtsp_url} RTSP/1.0\r\n"
|
|
290
|
+
f"CSeq: 1\r\n"
|
|
291
|
+
f"Accept: application/sdp\r\n\r\n"
|
|
292
|
+
)
|
|
293
|
+
sock.send(rtsp_request.encode())
|
|
294
|
+
|
|
295
|
+
# Read response
|
|
296
|
+
response = sock.recv(4096).decode()
|
|
297
|
+
sock.close()
|
|
298
|
+
|
|
299
|
+
# Check if we got a valid RTSP response with SDP content
|
|
300
|
+
return (
|
|
301
|
+
response.startswith("RTSP/1.0 200 OK")
|
|
302
|
+
and "application/sdp" in response
|
|
303
|
+
and "m=video" in response
|
|
304
|
+
)
|
|
305
|
+
except InvalidRtspUrlError:
|
|
306
|
+
raise
|
|
307
|
+
except Exception:
|
|
308
|
+
return False
|
|
@@ -4,6 +4,7 @@ from pathlib import Path
|
|
|
4
4
|
from typing import Generator
|
|
5
5
|
|
|
6
6
|
import av
|
|
7
|
+
from av.container.input import InputContainer
|
|
7
8
|
from tqdm import tqdm
|
|
8
9
|
|
|
9
10
|
from OTVision.abstraction.observer import Subject
|
|
@@ -12,6 +13,7 @@ from OTVision.application.detect.detection_file_save_path_provider import (
|
|
|
12
13
|
DetectionFileSavePathProvider,
|
|
13
14
|
)
|
|
14
15
|
from OTVision.application.detect.timestamper import Timestamper
|
|
16
|
+
from OTVision.application.event.new_video_start import NewVideoStartEvent
|
|
15
17
|
from OTVision.application.get_current_config import GetCurrentConfig
|
|
16
18
|
from OTVision.detect.detected_frame_buffer import FlushEvent
|
|
17
19
|
from OTVision.detect.plugin_av.rotate_frame import AvVideoFrameRotator
|
|
@@ -38,6 +40,9 @@ class VideoSource(InputSourceDetect):
|
|
|
38
40
|
and selective frame processing based on configuration parameters.
|
|
39
41
|
|
|
40
42
|
Args:
|
|
43
|
+
subject_flush: (Subject[FlushEvent]): Subject for notifying about flush events.
|
|
44
|
+
subject_new_video_start (Subject[NewVideoStartEvent): Subject for notifying
|
|
45
|
+
about new video start events.
|
|
41
46
|
get_current_config (GetCurrentConfig): Use case to retrieve current
|
|
42
47
|
configuration.
|
|
43
48
|
frame_rotator (AvVideoFrameRotator): Use to rotate video frames.
|
|
@@ -56,13 +61,15 @@ class VideoSource(InputSourceDetect):
|
|
|
56
61
|
|
|
57
62
|
def __init__(
|
|
58
63
|
self,
|
|
59
|
-
|
|
64
|
+
subject_flush: Subject[FlushEvent],
|
|
65
|
+
subject_new_video_start: Subject[NewVideoStartEvent],
|
|
60
66
|
get_current_config: GetCurrentConfig,
|
|
61
67
|
frame_rotator: AvVideoFrameRotator,
|
|
62
68
|
timestamper_factory: TimestamperFactory,
|
|
63
69
|
save_path_provider: DetectionFileSavePathProvider,
|
|
64
70
|
) -> None:
|
|
65
|
-
|
|
71
|
+
self.subject_flush = subject_flush
|
|
72
|
+
self.subject_new_video_start = subject_new_video_start
|
|
66
73
|
self._frame_rotator = frame_rotator
|
|
67
74
|
self._get_current_config = get_current_config
|
|
68
75
|
self._timestamper_factory = timestamper_factory
|
|
@@ -97,13 +104,14 @@ class VideoSource(InputSourceDetect):
|
|
|
97
104
|
expected_duration=self._current_config.detect.expected_duration,
|
|
98
105
|
)
|
|
99
106
|
video_fps = get_fps(video_file)
|
|
107
|
+
self.notify_new_video_start_observers(video_file, video_fps)
|
|
100
108
|
detect_start = self.__get_detect_start_in_frames(video_fps)
|
|
101
109
|
detect_end = self.__get_detect_end_in_frames(video_fps)
|
|
102
110
|
counter = 0
|
|
103
111
|
try:
|
|
104
112
|
with av.open(str(video_file.absolute())) as container:
|
|
105
113
|
container.streams.video[0].thread_type = "AUTO"
|
|
106
|
-
side_data = container
|
|
114
|
+
side_data = self._extract_side_data(container)
|
|
107
115
|
for frame_number, frame in enumerate(
|
|
108
116
|
container.decode(video=0), start=1
|
|
109
117
|
):
|
|
@@ -129,10 +137,20 @@ class VideoSource(InputSourceDetect):
|
|
|
129
137
|
}
|
|
130
138
|
)
|
|
131
139
|
counter += 1
|
|
132
|
-
self.
|
|
140
|
+
self.notify_flush_event_observers(video_file, video_fps)
|
|
133
141
|
except Exception as e:
|
|
134
142
|
log.error(f"Error processing {video_file}", exc_info=e)
|
|
135
143
|
|
|
144
|
+
def _extract_side_data(self, container: InputContainer) -> dict:
|
|
145
|
+
try:
|
|
146
|
+
return container.streams.video[0].side_data
|
|
147
|
+
except AttributeError:
|
|
148
|
+
log.warning(
|
|
149
|
+
"No side_data found in video stream. "
|
|
150
|
+
"Existing rotation will not be applied."
|
|
151
|
+
)
|
|
152
|
+
return {}
|
|
153
|
+
|
|
136
154
|
def __collect_files_to_detect(self) -> list[Path]:
|
|
137
155
|
filetypes = self._current_config.filetypes.video_filetypes.to_list()
|
|
138
156
|
video_files = get_files(
|
|
@@ -169,7 +187,9 @@ class VideoSource(InputSourceDetect):
|
|
|
169
187
|
return False
|
|
170
188
|
return True
|
|
171
189
|
|
|
172
|
-
def
|
|
190
|
+
def notify_flush_event_observers(
|
|
191
|
+
self, current_video_file: Path, video_fps: float
|
|
192
|
+
) -> None:
|
|
173
193
|
if expected_duration := self._current_config.detect.expected_duration:
|
|
174
194
|
duration = expected_duration
|
|
175
195
|
else:
|
|
@@ -180,7 +200,7 @@ class VideoSource(InputSourceDetect):
|
|
|
180
200
|
current_video_file, start_time=self._start_time
|
|
181
201
|
)
|
|
182
202
|
|
|
183
|
-
self.
|
|
203
|
+
self.subject_flush.notify(
|
|
184
204
|
FlushEvent.create(
|
|
185
205
|
source=str(current_video_file),
|
|
186
206
|
output=str(current_video_file),
|
|
@@ -192,6 +212,18 @@ class VideoSource(InputSourceDetect):
|
|
|
192
212
|
)
|
|
193
213
|
)
|
|
194
214
|
|
|
215
|
+
def notify_new_video_start_observers(
|
|
216
|
+
self, current_video_file: Path, video_fps: float
|
|
217
|
+
) -> None:
|
|
218
|
+
width, height = get_video_dimensions(current_video_file)
|
|
219
|
+
event = NewVideoStartEvent(
|
|
220
|
+
output=str(current_video_file),
|
|
221
|
+
width=width,
|
|
222
|
+
height=height,
|
|
223
|
+
fps=video_fps,
|
|
224
|
+
)
|
|
225
|
+
self.subject_new_video_start.notify(event)
|
|
226
|
+
|
|
195
227
|
def __get_detect_start_in_frames(self, video_fps: float) -> int:
|
|
196
228
|
detect_start = convert_seconds_to_frames(
|
|
197
229
|
self._current_config.detect.detect_start, video_fps
|
OTVision/detect/yolo.py
CHANGED
|
@@ -142,9 +142,17 @@ class YoloDetector(ObjectDetector, Filter[Frame, DetectedFrame]):
|
|
|
142
142
|
def detect(
|
|
143
143
|
self, frames: Generator[Frame, None, None]
|
|
144
144
|
) -> Generator[DetectedFrame, None, None]:
|
|
145
|
-
for frame in tqdm(
|
|
145
|
+
for frame in tqdm(
|
|
146
|
+
frames,
|
|
147
|
+
desc="Detected frames",
|
|
148
|
+
unit=" frames",
|
|
149
|
+
disable=self.disable_tqdm_logging(),
|
|
150
|
+
):
|
|
146
151
|
yield self._predict(frame)
|
|
147
152
|
|
|
153
|
+
def disable_tqdm_logging(self) -> bool:
|
|
154
|
+
return log.level > logging.INFO
|
|
155
|
+
|
|
148
156
|
def _predict(self, frame: Frame) -> DetectedFrame:
|
|
149
157
|
if frame[FrameKeys.data] is None:
|
|
150
158
|
return self._create_empty_detection(frame)
|
OTVision/domain/cli.py
CHANGED
|
@@ -3,6 +3,12 @@ from dataclasses import dataclass
|
|
|
3
3
|
from datetime import datetime, timedelta
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
+
from OTVision.plugin.ffmpeg_video_writer import (
|
|
7
|
+
ConstantRateFactor,
|
|
8
|
+
EncodingSpeed,
|
|
9
|
+
VideoCodec,
|
|
10
|
+
)
|
|
11
|
+
|
|
6
12
|
|
|
7
13
|
class CliArgs(ABC):
|
|
8
14
|
@abstractmethod
|
|
@@ -32,6 +38,10 @@ class DetectCliArgs(CliArgs):
|
|
|
32
38
|
start_time: datetime | None = None
|
|
33
39
|
detect_start: int | None = None
|
|
34
40
|
detect_end: int | None = None
|
|
41
|
+
write_video: bool | None = None
|
|
42
|
+
video_codec: VideoCodec | None = None
|
|
43
|
+
encoding_speed: EncodingSpeed | None = None
|
|
44
|
+
crf: ConstantRateFactor | None = None
|
|
35
45
|
|
|
36
46
|
def get_config_file(self) -> Path | None:
|
|
37
47
|
return self.config_file
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
2
|
from typing import Generator
|
|
3
3
|
|
|
4
|
-
from OTVision.abstraction.observer import Observable
|
|
5
|
-
from OTVision.detect.detected_frame_buffer import FlushEvent
|
|
6
4
|
from OTVision.domain.frame import Frame
|
|
7
5
|
|
|
8
6
|
|
|
9
|
-
class InputSourceDetect(
|
|
7
|
+
class InputSourceDetect(ABC):
|
|
10
8
|
"""Interface for input sources that generate frames and notify about flush events.
|
|
11
9
|
|
|
12
10
|
This class combines the Observable pattern for flush events with frame generation
|
OTVision/domain/time.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
|
-
from datetime import datetime
|
|
2
|
+
from datetime import datetime
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class DatetimeProvider(ABC):
|
|
@@ -10,4 +10,4 @@ class DatetimeProvider(ABC):
|
|
|
10
10
|
|
|
11
11
|
class CurrentDatetimeProvider(DatetimeProvider):
|
|
12
12
|
def provide(self) -> datetime:
|
|
13
|
-
return datetime.now(
|
|
13
|
+
return datetime.now()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
from numpy import ndarray
|
|
4
|
+
|
|
5
|
+
from OTVision.abstraction.pipes_and_filter import Filter
|
|
6
|
+
from OTVision.application.event.new_video_start import NewVideoStartEvent
|
|
7
|
+
from OTVision.detect.detected_frame_buffer import FlushEvent
|
|
8
|
+
from OTVision.domain.frame import Frame
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class VideoWriter(Filter[Frame, Frame], ABC):
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def write(self, image: ndarray) -> None:
|
|
14
|
+
raise NotImplementedError
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def open(self, output: str, width: int, height: int, fps: float) -> None:
|
|
18
|
+
raise NotImplementedError
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def close(self) -> None:
|
|
22
|
+
raise NotImplementedError
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def notify_on_flush_event(self, event: FlushEvent) -> None:
|
|
26
|
+
raise NotImplementedError
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def notify_on_new_video_start(self, event: NewVideoStartEvent) -> None:
|
|
30
|
+
raise NotImplementedError
|