streamvision 0.1.0__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.
- streamvision/__init__.py +1 -0
- streamvision/__main__.py +108 -0
- streamvision/camera/__init__.py +0 -0
- streamvision/camera/buffer_strategies.py +28 -0
- streamvision/camera/camera.py +138 -0
- streamvision/camera/capture_properties.py +121 -0
- streamvision/camera/collection_policy.py +377 -0
- streamvision/camera/dgpu_producer.py +123 -0
- streamvision/camera/discoverability.py +355 -0
- streamvision/camera/entities.py +117 -0
- streamvision/camera/exceptions.py +14 -0
- streamvision/camera/fourcc.py +63 -0
- streamvision/camera/gstreamer_cuda_producer.py +557 -0
- streamvision/camera/gstreamer_cuda_tensor_bridge.py +308 -0
- streamvision/camera/gstreamer_rtsp_pipeline.py +175 -0
- streamvision/camera/gstreamer_rtsp_producer.py +82 -0
- streamvision/camera/jetson_producer.py +568 -0
- streamvision/camera/jetson_tensor_bridge.py +348 -0
- streamvision/camera/rtsp_opencv_tls.py +104 -0
- streamvision/camera/rtsp_tls.py +38 -0
- streamvision/camera/source_reference_sanitizer.py +89 -0
- streamvision/camera/source_reference_validation.py +62 -0
- streamvision/camera/stream_error_classifier.py +549 -0
- streamvision/camera/stream_error_codes.py +13 -0
- streamvision/camera/test_pattern_producer.py +208 -0
- streamvision/camera/utils.py +573 -0
- streamvision/camera/video_source.py +1454 -0
- streamvision/stream/__init__.py +0 -0
- streamvision/stream/configuration.py +189 -0
- streamvision/stream/entities.py +154 -0
- streamvision/stream/environment.py +99 -0
- streamvision/stream/exceptions.py +33 -0
- streamvision/stream/model_handlers/__init__.py +0 -0
- streamvision/stream/model_handlers/workflows.py +339 -0
- streamvision/stream/pipeline.py +952 -0
- streamvision/stream/session.py +30 -0
- streamvision/stream/sinks.py +627 -0
- streamvision/stream/support/__init__.py +6 -0
- streamvision/stream/support/async_queue.py +87 -0
- streamvision/stream/support/decorators.py +26 -0
- streamvision/stream/support/environment.py +59 -0
- streamvision/stream/support/images.py +245 -0
- streamvision/stream/utils.py +208 -0
- streamvision/stream/warnings.py +9 -0
- streamvision/stream/watchdog.py +312 -0
- streamvision/stream_manager/__init__.py +0 -0
- streamvision/stream_manager/api/__init__.py +0 -0
- streamvision/stream_manager/api/entities.py +47 -0
- streamvision/stream_manager/api/errors.py +52 -0
- streamvision/stream_manager/api/stream_manager_client.py +373 -0
- streamvision/stream_manager/manager_app/__init__.py +0 -0
- streamvision/stream_manager/manager_app/app.py +710 -0
- streamvision/stream_manager/manager_app/bootstrap.py +130 -0
- streamvision/stream_manager/manager_app/communication.py +83 -0
- streamvision/stream_manager/manager_app/entities.py +210 -0
- streamvision/stream_manager/manager_app/errors.py +44 -0
- streamvision/stream_manager/manager_app/host.py +158 -0
- streamvision/stream_manager/manager_app/inference_pipeline_manager.py +793 -0
- streamvision/stream_manager/manager_app/result_serialization.py +64 -0
- streamvision/stream_manager/manager_app/serialisation.py +63 -0
- streamvision/stream_manager/manager_app/tcp_server.py +19 -0
- streamvision/stream_manager/manager_app/webrtc.py +493 -0
- streamvision/webrtc_worker/__init__.py +1 -0
- streamvision/webrtc_worker/entities.py +133 -0
- streamvision/webrtc_worker/host.py +167 -0
- streamvision/webrtc_worker/modal_session.py +235 -0
- streamvision/webrtc_worker/serializers.py +43 -0
- streamvision/webrtc_worker/sources/__init__.py +6 -0
- streamvision/webrtc_worker/sources/file.py +175 -0
- streamvision/webrtc_worker/sources/rtsp.py +73 -0
- streamvision/webrtc_worker/utils.py +353 -0
- streamvision/webrtc_worker/watchdog.py +194 -0
- streamvision/webrtc_worker/webrtc.py +1418 -0
- streamvision-0.1.0.dist-info/METADATA +121 -0
- streamvision-0.1.0.dist-info/RECORD +78 -0
- streamvision-0.1.0.dist-info/WHEEL +5 -0
- streamvision-0.1.0.dist-info/licenses/LICENSE +7 -0
- streamvision-0.1.0.dist-info/top_level.txt +1 -0
streamvision/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Camera acquisition, InferencePipeline, and the stream-manager client and runtime."""
|
streamvision/__main__.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Run the stream manager standalone: `python -m streamvision --host-factory ...`."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import importlib.util
|
|
5
|
+
from typing import List, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
from streamvision.stream.configuration import get_configuration
|
|
8
|
+
from streamvision.stream_manager.manager_app.bootstrap import install_process_settings
|
|
9
|
+
from streamvision.stream_manager.manager_app.host import (
|
|
10
|
+
PipelineHostDescriptor,
|
|
11
|
+
import_attribute,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _parse_setting(value: str) -> Tuple[str, str]:
|
|
16
|
+
key, separator, setting = value.partition("=")
|
|
17
|
+
if not (key and separator):
|
|
18
|
+
raise argparse.ArgumentTypeError(f"expected KEY=VALUE, got {value!r}")
|
|
19
|
+
|
|
20
|
+
return key, setting
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _non_negative_int(value: str) -> int:
|
|
24
|
+
try:
|
|
25
|
+
parsed = int(value)
|
|
26
|
+
except ValueError:
|
|
27
|
+
raise argparse.ArgumentTypeError(
|
|
28
|
+
f"expected a non-negative integer, got {value!r}"
|
|
29
|
+
)
|
|
30
|
+
if parsed < 0:
|
|
31
|
+
raise argparse.ArgumentTypeError(
|
|
32
|
+
f"expected a non-negative integer, got {value!r}"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
return parsed
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
39
|
+
"""Build the command-line parser of the standalone stream manager.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
The parser.
|
|
43
|
+
"""
|
|
44
|
+
parser = argparse.ArgumentParser(prog="python -m streamvision")
|
|
45
|
+
parser.add_argument(
|
|
46
|
+
"--host-factory",
|
|
47
|
+
required=True,
|
|
48
|
+
help="Pipeline host factory as 'package.module:attribute'.",
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--host-setting",
|
|
52
|
+
type=_parse_setting,
|
|
53
|
+
action="append",
|
|
54
|
+
default=[],
|
|
55
|
+
metavar="KEY=VALUE",
|
|
56
|
+
help="String keyword argument for the host factory (repeatable).",
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"--warm-pipelines",
|
|
60
|
+
type=_non_negative_int,
|
|
61
|
+
default=0,
|
|
62
|
+
help="Number of idle pipeline processes kept ready.",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return parser
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def main(argv: Optional[List[str]] = None) -> None:
|
|
69
|
+
"""Launch the stream manager; its address comes from `STREAM_MANAGER_*` env.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
argv: Command-line arguments; `sys.argv[1:]` when `None`.
|
|
73
|
+
"""
|
|
74
|
+
parser = build_parser()
|
|
75
|
+
args = parser.parse_args(argv)
|
|
76
|
+
if importlib.util.find_spec("aiortc") is None:
|
|
77
|
+
parser.error(
|
|
78
|
+
"the stream manager needs the webrtc extra: pip install 'streamvision[webrtc]'"
|
|
79
|
+
)
|
|
80
|
+
if importlib.util.find_spec("roboflow_workflows") is None:
|
|
81
|
+
parser.error(
|
|
82
|
+
"the stream manager needs the workflows extra: "
|
|
83
|
+
"pip install 'streamvision[webrtc,workflows]'"
|
|
84
|
+
)
|
|
85
|
+
host_descriptor = PipelineHostDescriptor(
|
|
86
|
+
factory=args.host_factory,
|
|
87
|
+
settings=dict(args.host_setting),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# The host module may install its own configuration on import, so import it first.
|
|
91
|
+
import_attribute(host_descriptor.factory)
|
|
92
|
+
# Returns what the host installed, else installs and returns the defaults.
|
|
93
|
+
configuration = get_configuration()
|
|
94
|
+
install_process_settings(
|
|
95
|
+
configuration=configuration,
|
|
96
|
+
host_descriptor=host_descriptor,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
from streamvision.stream_manager.manager_app.app import start
|
|
100
|
+
|
|
101
|
+
start(
|
|
102
|
+
expected_warmed_up_pipelines=args.warm_pipelines,
|
|
103
|
+
host_descriptor=host_descriptor,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
if __name__ == "__main__":
|
|
108
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Buffer strategies of a video source, importable without the decoder.
|
|
2
|
+
|
|
3
|
+
Request entities validate these values without needing cv2 or the video
|
|
4
|
+
source itself. `video_source` re-exports both enums, and each class keeps the
|
|
5
|
+
`__module__` it was historically defined in, so signatures, annotations and
|
|
6
|
+
pickles still name `video_source` exactly as before the extraction.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from enum import Enum
|
|
10
|
+
|
|
11
|
+
_HISTORICAL_MODULE = f"{__name__.rsplit('.', 1)[0]}.video_source"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BufferFillingStrategy(str, Enum):
|
|
15
|
+
WAIT = "WAIT"
|
|
16
|
+
DROP_OLDEST = "DROP_OLDEST"
|
|
17
|
+
ADAPTIVE_DROP_OLDEST = "ADAPTIVE_DROP_OLDEST"
|
|
18
|
+
DROP_LATEST = "DROP_LATEST"
|
|
19
|
+
ADAPTIVE_DROP_LATEST = "ADAPTIVE_DROP_LATEST"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BufferConsumptionStrategy(str, Enum):
|
|
23
|
+
LAZY = "LAZY"
|
|
24
|
+
EAGER = "EAGER"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
BufferFillingStrategy.__module__ = _HISTORICAL_MODULE
|
|
28
|
+
BufferConsumptionStrategy.__module__ = _HISTORICAL_MODULE
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
from threading import Thread
|
|
5
|
+
|
|
6
|
+
import cv2
|
|
7
|
+
from PIL import Image
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class WebcamStream:
|
|
13
|
+
"""Class to handle webcam streaming using a separate thread.
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
stream_id (int): The ID of the webcam stream.
|
|
17
|
+
frame_id (int): A counter for the current frame.
|
|
18
|
+
vcap (VideoCapture): OpenCV video capture object.
|
|
19
|
+
width (int): The width of the video frame.
|
|
20
|
+
height (int): The height of the video frame.
|
|
21
|
+
fps_input_stream (int): Frames per second of the input stream.
|
|
22
|
+
grabbed (bool): A flag indicating if a frame was successfully grabbed.
|
|
23
|
+
frame (array): The current frame as a NumPy array.
|
|
24
|
+
pil_image (Image): The current frame as a PIL image.
|
|
25
|
+
stopped (bool): A flag indicating if the stream is stopped.
|
|
26
|
+
t (Thread): The thread used to update the stream.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, stream_id=0, enforce_fps=False):
|
|
30
|
+
"""Initialize the webcam stream.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
stream_id (int, optional): The ID of the webcam stream. Defaults to 0.
|
|
34
|
+
"""
|
|
35
|
+
self.stream_id = stream_id
|
|
36
|
+
self.enforce_fps = enforce_fps
|
|
37
|
+
self.frame_id = 0
|
|
38
|
+
self.vcap = cv2.VideoCapture(self.stream_id)
|
|
39
|
+
|
|
40
|
+
for key in os.environ:
|
|
41
|
+
if key.startswith("CV2_CAP_PROP"):
|
|
42
|
+
opencv_prop = key[4:]
|
|
43
|
+
opencv_constant = getattr(cv2, opencv_prop, None)
|
|
44
|
+
if opencv_constant is not None:
|
|
45
|
+
value = int(os.getenv(key))
|
|
46
|
+
self.vcap.set(opencv_constant, value)
|
|
47
|
+
logger.info(f"set {opencv_prop} to {value}")
|
|
48
|
+
else:
|
|
49
|
+
logger.warning(f"Property {opencv_prop} not found in cv2")
|
|
50
|
+
|
|
51
|
+
self.width = int(self.vcap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
52
|
+
self.height = int(self.vcap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
53
|
+
self.file_mode = self.vcap.get(cv2.CAP_PROP_FRAME_COUNT) > 0
|
|
54
|
+
if self.enforce_fps and not self.file_mode:
|
|
55
|
+
logger.warning(
|
|
56
|
+
"Ignoring enforce_fps flag for this stream. It is not compatible with streams and will cause the process to crash"
|
|
57
|
+
)
|
|
58
|
+
self.enforce_fps = False
|
|
59
|
+
self.max_fps = None
|
|
60
|
+
if self.vcap.isOpened() is False:
|
|
61
|
+
logger.debug("[Exiting]: Error accessing webcam stream.")
|
|
62
|
+
exit(0)
|
|
63
|
+
self.fps_input_stream = int(self.vcap.get(cv2.CAP_PROP_FPS))
|
|
64
|
+
logger.debug(
|
|
65
|
+
"FPS of webcam hardware/input stream: {}".format(self.fps_input_stream)
|
|
66
|
+
)
|
|
67
|
+
self.grabbed, self.frame = self.vcap.read()
|
|
68
|
+
self.pil_image = Image.fromarray(cv2.cvtColor(self.frame, cv2.COLOR_BGR2RGB))
|
|
69
|
+
if self.grabbed is False:
|
|
70
|
+
logger.debug("[Exiting] No more frames to read")
|
|
71
|
+
exit(0)
|
|
72
|
+
self.stopped = True
|
|
73
|
+
self.t = Thread(target=self.update, args=())
|
|
74
|
+
self.t.daemon = True
|
|
75
|
+
|
|
76
|
+
def start(self):
|
|
77
|
+
"""Start the thread for reading frames."""
|
|
78
|
+
self.stopped = False
|
|
79
|
+
self.t.start()
|
|
80
|
+
|
|
81
|
+
def update(self):
|
|
82
|
+
"""Update the frame by reading from the webcam."""
|
|
83
|
+
frame_id = 0
|
|
84
|
+
next_frame_time = 0
|
|
85
|
+
t0 = time.perf_counter()
|
|
86
|
+
while True:
|
|
87
|
+
t1 = time.perf_counter()
|
|
88
|
+
if self.stopped is True:
|
|
89
|
+
break
|
|
90
|
+
|
|
91
|
+
self.grabbed = self.vcap.grab()
|
|
92
|
+
if self.grabbed is False:
|
|
93
|
+
logger.debug("[Exiting] No more frames to read")
|
|
94
|
+
self.stopped = True
|
|
95
|
+
break
|
|
96
|
+
frame_id += 1
|
|
97
|
+
# We can't retrieve each frame on nano and other lower powered devices quickly enough to keep up with the stream.
|
|
98
|
+
# By default, we will only retrieve frames when we'll be ready process them (determined by self.max_fps).
|
|
99
|
+
if t1 > next_frame_time:
|
|
100
|
+
ret, frame = self.vcap.retrieve()
|
|
101
|
+
if frame is None:
|
|
102
|
+
logger.debug("[Exiting] Frame not available for read")
|
|
103
|
+
self.stopped = True
|
|
104
|
+
break
|
|
105
|
+
logger.debug(
|
|
106
|
+
f"retrieved frame {frame_id}, effective FPS: {frame_id / (t1 - t0):.2f}"
|
|
107
|
+
)
|
|
108
|
+
self.frame_id = frame_id
|
|
109
|
+
self.frame = frame
|
|
110
|
+
while self.file_mode and self.enforce_fps and self.max_fps is None:
|
|
111
|
+
# sleep until we have processed the first frame and we know what our FPS should be
|
|
112
|
+
time.sleep(0.01)
|
|
113
|
+
if self.max_fps is None:
|
|
114
|
+
self.max_fps = 30
|
|
115
|
+
next_frame_time = t1 + (1 / self.max_fps) + 0.02
|
|
116
|
+
if self.file_mode:
|
|
117
|
+
t2 = time.perf_counter()
|
|
118
|
+
if self.enforce_fps:
|
|
119
|
+
# when enforce_fps is true, grab video frames 1:1 with inference speed
|
|
120
|
+
time_to_sleep = next_frame_time - t2
|
|
121
|
+
else:
|
|
122
|
+
# otherwise, grab at native FPS of the video file
|
|
123
|
+
time_to_sleep = (1 / self.fps_input_stream) - (t2 - t1)
|
|
124
|
+
if time_to_sleep > 0:
|
|
125
|
+
time.sleep(time_to_sleep)
|
|
126
|
+
self.vcap.release()
|
|
127
|
+
|
|
128
|
+
def read_opencv(self):
|
|
129
|
+
"""Read the current frame using OpenCV.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
array, int: The current frame as a NumPy array, and the frame ID.
|
|
133
|
+
"""
|
|
134
|
+
return self.frame, self.frame_id
|
|
135
|
+
|
|
136
|
+
def stop(self):
|
|
137
|
+
"""Stop the webcam stream."""
|
|
138
|
+
self.stopped = True
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Apply ``cv2.VideoCapture`` properties in a camera-safe order.
|
|
2
|
+
|
|
3
|
+
The order in which capture properties are set matters for V4L2 / USB cameras.
|
|
4
|
+
Many UVC cameras (e.g. Logitech C920) open in an uncompressed pixel format
|
|
5
|
+
(YUYV) whose frame-rate ceiling at high resolutions is very low. Setting
|
|
6
|
+
``fps`` while the device is still in that format clamps the frame interval,
|
|
7
|
+
and a later ``fourcc`` switch (e.g. to MJPG) keeps the clamped interval.
|
|
8
|
+
|
|
9
|
+
Properties often arrive as dictionaries built from JSON payloads whose key
|
|
10
|
+
order is not guaranteed, so they are applied in a fixed order:
|
|
11
|
+
|
|
12
|
+
1. ``fourcc`` first - selects the pixel format,
|
|
13
|
+
2. every other property in the order given (e.g. frame size),
|
|
14
|
+
3. ``fps`` last - negotiated against the final format and frame size.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
19
|
+
|
|
20
|
+
import cv2
|
|
21
|
+
from streamvision.camera.fourcc import FOURCC_PROPERTY, parse_fourcc
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
FPS_PROPERTY = "fps"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def apply_capture_properties(
|
|
29
|
+
stream: cv2.VideoCapture,
|
|
30
|
+
*,
|
|
31
|
+
properties: Optional[Dict[str, Union[float, int, str]]],
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Set capture properties on ``stream`` with ``fourcc`` first and ``fps`` last.
|
|
34
|
+
|
|
35
|
+
Properties other than ``fourcc`` and ``fps`` keep their given order.
|
|
36
|
+
``fourcc`` may be a four-character code such as ``"MJPG"`` or its numeric
|
|
37
|
+
value. Codes are case-sensitive and used exactly as given, so ``"mjpg"`` is
|
|
38
|
+
a different format from ``"MJPG"``. An invalid ``fourcc`` is logged and
|
|
39
|
+
skipped, and a ``fourcc`` the device rejects is logged as a warning. When
|
|
40
|
+
``fourcc`` or ``fps`` is requested, the effective format is read back and
|
|
41
|
+
logged.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
stream (cv2.VideoCapture): Opened capture to configure.
|
|
45
|
+
properties (Optional[Dict[str, Union[float, int, str]]]): Mapping of
|
|
46
|
+
``cv2.CAP_PROP_*`` suffixes (case-insensitive) to values.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
AttributeError: If a property name has no ``cv2.CAP_PROP_*`` constant.
|
|
50
|
+
"""
|
|
51
|
+
if not properties:
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
for property_id, value in _order_capture_properties(properties=properties):
|
|
55
|
+
is_fourcc = property_id.lower() == FOURCC_PROPERTY
|
|
56
|
+
if is_fourcc:
|
|
57
|
+
parsed_fourcc = parse_fourcc(value)
|
|
58
|
+
if parsed_fourcc is None:
|
|
59
|
+
logger.warning(
|
|
60
|
+
f"Ignoring invalid fourcc video source property: {value!r}. "
|
|
61
|
+
"Expected a case-sensitive four-character code (e.g. 'MJPG') "
|
|
62
|
+
"or its numeric value."
|
|
63
|
+
)
|
|
64
|
+
continue
|
|
65
|
+
value = parsed_fourcc
|
|
66
|
+
cv2_id = getattr(cv2, "CAP_PROP_" + property_id.upper())
|
|
67
|
+
if not stream.set(cv2_id, value):
|
|
68
|
+
# A rejected fourcc leaves the device in its default pixel format,
|
|
69
|
+
# which usually caps the frame rate, so it is surfaced by default.
|
|
70
|
+
log = logger.warning if is_fourcc else logger.debug
|
|
71
|
+
log(f"Video source did not accept property {property_id}={value!r}")
|
|
72
|
+
|
|
73
|
+
requested_ids = {property_id.lower() for property_id in properties}
|
|
74
|
+
if requested_ids & {FOURCC_PROPERTY, FPS_PROPERTY}:
|
|
75
|
+
_log_effective_format(stream)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _order_capture_properties(
|
|
79
|
+
properties: Dict[str, Any],
|
|
80
|
+
) -> List[Tuple[str, Any]]:
|
|
81
|
+
fourcc, others, fps = [], [], []
|
|
82
|
+
for property_id, value in properties.items():
|
|
83
|
+
normalised_id = property_id.lower()
|
|
84
|
+
if normalised_id == FOURCC_PROPERTY:
|
|
85
|
+
fourcc.append((property_id, value))
|
|
86
|
+
elif normalised_id == FPS_PROPERTY:
|
|
87
|
+
fps.append((property_id, value))
|
|
88
|
+
else:
|
|
89
|
+
others.append((property_id, value))
|
|
90
|
+
|
|
91
|
+
ordered_properties = fourcc + others + fps
|
|
92
|
+
|
|
93
|
+
return ordered_properties
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _decode_fourcc(value: float) -> str:
|
|
97
|
+
code = int(value)
|
|
98
|
+
if code <= 0:
|
|
99
|
+
return ""
|
|
100
|
+
|
|
101
|
+
decoded = "".join(chr((code >> (8 * i)) & 0xFF) for i in range(4))
|
|
102
|
+
|
|
103
|
+
return decoded
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _log_effective_format(stream: cv2.VideoCapture) -> None:
|
|
107
|
+
# Diagnostics only: surfaces drivers that silently ignore or clamp a
|
|
108
|
+
# requested format / frame rate (e.g. MJPG applied but fps left at 5).
|
|
109
|
+
try:
|
|
110
|
+
fourcc = _decode_fourcc(stream.get(cv2.CAP_PROP_FOURCC))
|
|
111
|
+
fps = stream.get(cv2.CAP_PROP_FPS)
|
|
112
|
+
width = int(stream.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
113
|
+
height = int(stream.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
114
|
+
except Exception as error: # noqa: BLE001 - diagnostics must never fail startup
|
|
115
|
+
logger.debug(f"Could not read back video source format: {error!r}")
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
logger.info(
|
|
119
|
+
"Video source format after applying properties: "
|
|
120
|
+
f"fourcc={fourcc!r}, fps={fps}, size={width}x{height}"
|
|
121
|
+
)
|