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.
Files changed (78) hide show
  1. streamvision/__init__.py +1 -0
  2. streamvision/__main__.py +108 -0
  3. streamvision/camera/__init__.py +0 -0
  4. streamvision/camera/buffer_strategies.py +28 -0
  5. streamvision/camera/camera.py +138 -0
  6. streamvision/camera/capture_properties.py +121 -0
  7. streamvision/camera/collection_policy.py +377 -0
  8. streamvision/camera/dgpu_producer.py +123 -0
  9. streamvision/camera/discoverability.py +355 -0
  10. streamvision/camera/entities.py +117 -0
  11. streamvision/camera/exceptions.py +14 -0
  12. streamvision/camera/fourcc.py +63 -0
  13. streamvision/camera/gstreamer_cuda_producer.py +557 -0
  14. streamvision/camera/gstreamer_cuda_tensor_bridge.py +308 -0
  15. streamvision/camera/gstreamer_rtsp_pipeline.py +175 -0
  16. streamvision/camera/gstreamer_rtsp_producer.py +82 -0
  17. streamvision/camera/jetson_producer.py +568 -0
  18. streamvision/camera/jetson_tensor_bridge.py +348 -0
  19. streamvision/camera/rtsp_opencv_tls.py +104 -0
  20. streamvision/camera/rtsp_tls.py +38 -0
  21. streamvision/camera/source_reference_sanitizer.py +89 -0
  22. streamvision/camera/source_reference_validation.py +62 -0
  23. streamvision/camera/stream_error_classifier.py +549 -0
  24. streamvision/camera/stream_error_codes.py +13 -0
  25. streamvision/camera/test_pattern_producer.py +208 -0
  26. streamvision/camera/utils.py +573 -0
  27. streamvision/camera/video_source.py +1454 -0
  28. streamvision/stream/__init__.py +0 -0
  29. streamvision/stream/configuration.py +189 -0
  30. streamvision/stream/entities.py +154 -0
  31. streamvision/stream/environment.py +99 -0
  32. streamvision/stream/exceptions.py +33 -0
  33. streamvision/stream/model_handlers/__init__.py +0 -0
  34. streamvision/stream/model_handlers/workflows.py +339 -0
  35. streamvision/stream/pipeline.py +952 -0
  36. streamvision/stream/session.py +30 -0
  37. streamvision/stream/sinks.py +627 -0
  38. streamvision/stream/support/__init__.py +6 -0
  39. streamvision/stream/support/async_queue.py +87 -0
  40. streamvision/stream/support/decorators.py +26 -0
  41. streamvision/stream/support/environment.py +59 -0
  42. streamvision/stream/support/images.py +245 -0
  43. streamvision/stream/utils.py +208 -0
  44. streamvision/stream/warnings.py +9 -0
  45. streamvision/stream/watchdog.py +312 -0
  46. streamvision/stream_manager/__init__.py +0 -0
  47. streamvision/stream_manager/api/__init__.py +0 -0
  48. streamvision/stream_manager/api/entities.py +47 -0
  49. streamvision/stream_manager/api/errors.py +52 -0
  50. streamvision/stream_manager/api/stream_manager_client.py +373 -0
  51. streamvision/stream_manager/manager_app/__init__.py +0 -0
  52. streamvision/stream_manager/manager_app/app.py +710 -0
  53. streamvision/stream_manager/manager_app/bootstrap.py +130 -0
  54. streamvision/stream_manager/manager_app/communication.py +83 -0
  55. streamvision/stream_manager/manager_app/entities.py +210 -0
  56. streamvision/stream_manager/manager_app/errors.py +44 -0
  57. streamvision/stream_manager/manager_app/host.py +158 -0
  58. streamvision/stream_manager/manager_app/inference_pipeline_manager.py +793 -0
  59. streamvision/stream_manager/manager_app/result_serialization.py +64 -0
  60. streamvision/stream_manager/manager_app/serialisation.py +63 -0
  61. streamvision/stream_manager/manager_app/tcp_server.py +19 -0
  62. streamvision/stream_manager/manager_app/webrtc.py +493 -0
  63. streamvision/webrtc_worker/__init__.py +1 -0
  64. streamvision/webrtc_worker/entities.py +133 -0
  65. streamvision/webrtc_worker/host.py +167 -0
  66. streamvision/webrtc_worker/modal_session.py +235 -0
  67. streamvision/webrtc_worker/serializers.py +43 -0
  68. streamvision/webrtc_worker/sources/__init__.py +6 -0
  69. streamvision/webrtc_worker/sources/file.py +175 -0
  70. streamvision/webrtc_worker/sources/rtsp.py +73 -0
  71. streamvision/webrtc_worker/utils.py +353 -0
  72. streamvision/webrtc_worker/watchdog.py +194 -0
  73. streamvision/webrtc_worker/webrtc.py +1418 -0
  74. streamvision-0.1.0.dist-info/METADATA +121 -0
  75. streamvision-0.1.0.dist-info/RECORD +78 -0
  76. streamvision-0.1.0.dist-info/WHEEL +5 -0
  77. streamvision-0.1.0.dist-info/licenses/LICENSE +7 -0
  78. streamvision-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1 @@
1
+ """Camera acquisition, InferencePipeline, and the stream-manager client and runtime."""
@@ -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
+ )