nedo-vision-worker-core 0.3.4__tar.gz → 0.3.5__tar.gz

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.

Potentially problematic release.


This version of nedo-vision-worker-core might be problematic. Click here for more details.

Files changed (112) hide show
  1. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/PKG-INFO +1 -1
  2. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/__init__.py +1 -1
  3. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/pipeline/PipelineManager.py +13 -0
  4. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/pipeline/PipelineProcessor.py +3 -2
  5. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/pipeline/PipelineSyncThread.py +17 -20
  6. nedo_vision_worker_core-0.3.5/nedo_vision_worker_core/streams/StreamSyncThread.py +107 -0
  7. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/streams/VideoStreamManager.py +76 -20
  8. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core.egg-info/PKG-INFO +1 -1
  9. nedo_vision_worker_core-0.3.4/nedo_vision_worker_core/streams/StreamSyncThread.py +0 -80
  10. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/MANIFEST.in +0 -0
  11. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/README.md +0 -0
  12. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/ai/FrameDrawer.py +0 -0
  13. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/ai/ImageDebugger.py +0 -0
  14. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/ai/VideoDebugger.py +0 -0
  15. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/ai/__init__.py +0 -0
  16. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/callbacks/DetectionCallbackManager.py +0 -0
  17. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/callbacks/DetectionCallbackTypes.py +0 -0
  18. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/callbacks/__init__.py +0 -0
  19. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/cli.py +0 -0
  20. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/config/ConfigurationManager.py +0 -0
  21. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/config/__init__.py +0 -0
  22. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/core_service.py +0 -0
  23. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/database/DatabaseManager.py +0 -0
  24. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/database/__init__.py +0 -0
  25. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/detection/BaseDetector.py +0 -0
  26. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/detection/RFDETRDetector.py +0 -0
  27. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/detection/YOLODetector.py +0 -0
  28. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/detection/__init__.py +0 -0
  29. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/detection/detection_processing/DetectionProcessor.py +0 -0
  30. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/detection/detection_processing/HumanDetectionProcessor.py +0 -0
  31. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/detection/detection_processing/PPEDetectionProcessor.py +0 -0
  32. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/detection/detection_processing/__init__.py +0 -0
  33. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/doctor.py +0 -0
  34. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/drawing_assets/blue/inner_corner.png +0 -0
  35. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/drawing_assets/blue/inner_frame.png +0 -0
  36. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/drawing_assets/blue/line.png +0 -0
  37. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/drawing_assets/blue/top_left.png +0 -0
  38. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/drawing_assets/blue/top_right.png +0 -0
  39. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/drawing_assets/red/inner_corner.png +0 -0
  40. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/drawing_assets/red/inner_frame.png +0 -0
  41. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/drawing_assets/red/line.png +0 -0
  42. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/drawing_assets/red/top_left.png +0 -0
  43. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/drawing_assets/red/top_right.png +0 -0
  44. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/icons/boots-green.png +0 -0
  45. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/icons/boots-red.png +0 -0
  46. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/icons/gloves-green.png +0 -0
  47. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/icons/gloves-red.png +0 -0
  48. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/icons/goggles-green.png +0 -0
  49. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/icons/goggles-red.png +0 -0
  50. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/icons/helmet-green.png +0 -0
  51. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/icons/helmet-red.png +0 -0
  52. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/icons/mask-red.png +0 -0
  53. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/icons/vest-green.png +0 -0
  54. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/icons/vest-red.png +0 -0
  55. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/models/__init__.py +0 -0
  56. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/models/ai_model.py +0 -0
  57. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/models/auth.py +0 -0
  58. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/models/config.py +0 -0
  59. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/models/dataset_source.py +0 -0
  60. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/models/logs.py +0 -0
  61. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/models/ppe_detection.py +0 -0
  62. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/models/ppe_detection_label.py +0 -0
  63. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/models/restricted_area_violation.py +0 -0
  64. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/models/user.py +0 -0
  65. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/models/worker_source.py +0 -0
  66. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/models/worker_source_pipeline.py +0 -0
  67. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/models/worker_source_pipeline_config.py +0 -0
  68. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/models/worker_source_pipeline_debug.py +0 -0
  69. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/models/worker_source_pipeline_detection.py +0 -0
  70. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/pipeline/ModelManager.py +0 -0
  71. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/pipeline/PipelineConfigManager.py +0 -0
  72. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/pipeline/PipelinePrepocessor.py +0 -0
  73. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/pipeline/__init__.py +0 -0
  74. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/preprocessing/ImageResizer.py +0 -0
  75. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/preprocessing/ImageRoi.py +0 -0
  76. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/preprocessing/Preprocessor.py +0 -0
  77. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/preprocessing/__init__.py +0 -0
  78. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/repositories/AIModelRepository.py +0 -0
  79. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/repositories/PPEDetectionRepository.py +0 -0
  80. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/repositories/RestrictedAreaRepository.py +0 -0
  81. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/repositories/WorkerSourcePipelineDebugRepository.py +0 -0
  82. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/repositories/WorkerSourcePipelineDetectionRepository.py +0 -0
  83. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/repositories/WorkerSourcePipelineRepository.py +0 -0
  84. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/repositories/WorkerSourceRepository.py +0 -0
  85. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/repositories/__init__.py +0 -0
  86. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/services/SharedVideoStreamServer.py +0 -0
  87. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/services/VideoSharingDaemon.py +0 -0
  88. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/services/VideoSharingDaemonManager.py +0 -0
  89. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/streams/RTMPStreamer.py +0 -0
  90. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/streams/SharedVideoDeviceManager.py +0 -0
  91. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/streams/VideoStream.py +0 -0
  92. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/streams/__init__.py +0 -0
  93. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/tracker/SFSORT.py +0 -0
  94. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/tracker/TrackerManager.py +0 -0
  95. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/tracker/__init__.py +0 -0
  96. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/util/BoundingBoxMetrics.py +0 -0
  97. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/util/DrawingUtils.py +0 -0
  98. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/util/ModelReadinessChecker.py +0 -0
  99. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/util/PersonAttributeMatcher.py +0 -0
  100. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/util/PersonRestrictedAreaMatcher.py +0 -0
  101. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/util/PlatformDetector.py +0 -0
  102. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/util/TablePrinter.py +0 -0
  103. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/util/__init__.py +0 -0
  104. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core.egg-info/SOURCES.txt +0 -0
  105. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core.egg-info/dependency_links.txt +0 -0
  106. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core.egg-info/entry_points.txt +0 -0
  107. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core.egg-info/requires.txt +0 -0
  108. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core.egg-info/top_level.txt +0 -0
  109. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/pyproject.toml +0 -0
  110. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/requirements.txt +0 -0
  111. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/setup.cfg +0 -0
  112. {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nedo-vision-worker-core
3
- Version: 0.3.4
3
+ Version: 0.3.5
4
4
  Summary: Nedo Vision Worker Core Library for AI Vision Processing
5
5
  Author-email: Willy Achmat Fauzi <willy.achmat@gmail.com>
6
6
  Maintainer-email: Willy Achmat Fauzi <willy.achmat@gmail.com>
@@ -7,7 +7,7 @@ A library for running AI vision processing and detection in the Nedo Vision plat
7
7
  from .core_service import CoreService
8
8
  from .callbacks import DetectionType, CallbackTrigger, DetectionData, IntervalMetadata
9
9
 
10
- __version__ = "0.3.4"
10
+ __version__ = "0.3.5"
11
11
  __all__ = [
12
12
  "CoreService",
13
13
  "DetectionType",
@@ -39,6 +39,11 @@ class PipelineManager:
39
39
 
40
40
  logging.info(f"🚀 Starting Pipeline processing for pipeline: {pipeline_id} | Source: {worker_source_id} ({pipeline.name})")
41
41
 
42
+ # Acquire the video stream (starts it if not already running)
43
+ if not self.video_manager.acquire_stream(worker_source_id, pipeline_id):
44
+ logging.error(f"❌ Failed to acquire stream {worker_source_id} for pipeline {pipeline_id}")
45
+ return
46
+
42
47
  processor = PipelineProcessor(pipeline, detector, False)
43
48
  processor.frame_drawer.location_name = pipeline.location_name
44
49
  self.processors[pipeline_id] = processor # Store processor instance
@@ -79,6 +84,10 @@ class PipelineManager:
79
84
  self._stopping_pipelines.add(pipeline_id)
80
85
 
81
86
  try:
87
+ # Get worker_source_id before removing metadata
88
+ pipeline = self.pipeline_metadata.get(pipeline_id)
89
+ worker_source_id = pipeline.worker_source_id if pipeline else None
90
+
82
91
  # Stop AI processing
83
92
  processor = self.processors.pop(pipeline_id, None)
84
93
  if processor:
@@ -92,6 +101,10 @@ class PipelineManager:
92
101
  # Remove metadata
93
102
  self.pipeline_metadata.pop(pipeline_id, None)
94
103
 
104
+ # Release the video stream (stops it if no more pipelines use it)
105
+ if worker_source_id:
106
+ self.video_manager.release_stream(worker_source_id, pipeline_id)
107
+
95
108
  logging.info(f"✅ Pipeline {pipeline_id} stopped successfully.")
96
109
 
97
110
  except Exception as e:
@@ -459,9 +459,10 @@ class PipelineProcessor:
459
459
  logging.error(f" Cannot get stream URL for {worker_source_id}")
460
460
  return False
461
461
 
462
- video_manager.remove_stream(worker_source_id)
462
+ # Use internal methods to restart the stream without affecting reference counting
463
+ video_manager._stop_stream(worker_source_id)
463
464
  time.sleep(1.0)
464
- video_manager.add_stream(worker_source_id, stream_url)
465
+ video_manager._start_stream(worker_source_id, stream_url)
465
466
  time.sleep(2.0)
466
467
 
467
468
  if not video_manager.has_stream(worker_source_id):
@@ -103,8 +103,22 @@ class PipelineSyncThread(threading.Thread):
103
103
 
104
104
  for pid in pipeline_ids:
105
105
  db_pipeline = db_pipelines[pid]
106
+
107
+ # Check if pipeline should be stopped (status changed to stop/restart in DB)
108
+ if db_pipeline.pipeline_status_code in ['stop', 'restart']:
109
+ if self.pipeline_manager.is_running(pid):
110
+ logging.info(f"⏹️ Stopping pipeline due to status change: {pid}")
111
+ self.pipeline_manager.stop_pipeline(pid)
112
+ continue
113
+
106
114
  processor = self.pipeline_manager.processors.get(pid)
107
115
  if not processor:
116
+ # Pipeline exists in both sets but processor doesn't exist - shouldn't happen
117
+ # but if it does, try to start it if status is 'run'
118
+ if db_pipeline.pipeline_status_code == 'run':
119
+ logging.warning(f"⚠️ Pipeline {pid} exists locally but has no processor. Restarting...")
120
+ detector = self.model_manager.get_detector(db_pipeline.ai_model_id)
121
+ self.pipeline_manager.start_pipeline(db_pipeline, detector)
108
122
  continue
109
123
 
110
124
  local_detector = processor.detector
@@ -114,29 +128,12 @@ class PipelineSyncThread(threading.Thread):
114
128
  processor.enable_debug()
115
129
 
116
130
  def update_pipeline(self, pid: str, db_pipeline: object, local_detector: object) -> None:
117
- """Updates a single pipeline if necessary."""
131
+ """Updates a single pipeline if necessary (only called for running pipelines)."""
118
132
  processor = self.pipeline_manager.processors.get(pid)
119
133
  if not processor:
120
134
  return
121
135
 
122
- # Stop/start based on status change
123
- if db_pipeline.pipeline_status_code != processor._pipeline.pipeline_status_code:
124
- if db_pipeline.pipeline_status_code == 'run':
125
- logging.info(f"▶️ Resuming pipeline: {pid}")
126
- self.pipeline_manager.start_pipeline(db_pipeline, self.model_manager.get_detector(db_pipeline.ai_model_id))
127
- elif db_pipeline.pipeline_status_code in ['stop', 'restart']:
128
- logging.info(f"⏹️ Stopping pipeline: {pid}")
129
- self.pipeline_manager.stop_pipeline(pid)
130
- if db_pipeline.pipeline_status_code == 'restart':
131
- # This will be picked up by the 'add_new_pipelines' logic in the next cycle
132
- return
133
- else:
134
- processor.update_config(db_pipeline) # Update config for non-running pipelines
135
- return
136
- elif db_pipeline.pipeline_status_code != 'run':
137
- processor.update_config(db_pipeline)
138
- return
139
-
136
+ # At this point, we know db_pipeline.pipeline_status_code == 'run' (checked in caller)
140
137
  # Check for significant changes that require a restart
141
138
  db_detector = self.model_manager.get_detector(db_pipeline.ai_model_id)
142
139
 
@@ -146,7 +143,7 @@ class PipelineSyncThread(threading.Thread):
146
143
  local_detector != db_detector
147
144
  ])
148
145
 
149
- if requires_restart and db_pipeline.pipeline_status_code == 'run':
146
+ if requires_restart:
150
147
  logging.info(f"🔄 Restarting pipeline due to significant changes: {pid}")
151
148
  self.pipeline_manager.stop_pipeline(pid)
152
149
  self.pipeline_manager.start_pipeline(db_pipeline, db_detector)
@@ -0,0 +1,107 @@
1
+ import logging
2
+ import os
3
+ import time
4
+ import threading
5
+ from ..database.DatabaseManager import DatabaseManager
6
+ from ..repositories.WorkerSourceRepository import WorkerSourceRepository
7
+ from .VideoStreamManager import VideoStreamManager
8
+
9
+ class StreamSyncThread(threading.Thread):
10
+ """Thread responsible for synchronizing video streams from the database in real-time."""
11
+
12
+ def __init__(self, manager: VideoStreamManager, polling_interval=5):
13
+ super().__init__() # Set as a daemon so it stops with the main process
14
+
15
+ self.source_file_path = DatabaseManager.STORAGE_PATHS["files"] / "source_files"
16
+
17
+ self.manager = manager
18
+ self.polling_interval = polling_interval
19
+ self.worker_source_repo = WorkerSourceRepository()
20
+ self.running = True
21
+
22
+ def _get_source_file_path(self, file):
23
+ """Returns the file path for a given source file."""
24
+ return self.source_file_path / os.path.basename(file)
25
+
26
+ def run(self):
27
+ """Continuously updates the VideoStreamManager with database changes."""
28
+ while self.running:
29
+ try:
30
+ sources = self.worker_source_repo.get_worker_sources()
31
+ db_sources = {
32
+ source.id:
33
+ source.url if source.type_code == "live"
34
+ else source.url if source.type_code == "direct"
35
+ else self._get_source_file_path(source.file_path)
36
+ for source in sources
37
+ } # Store latest sources
38
+
39
+ # Get both active streams and pending streams
40
+ active_stream_ids = set(self.manager.get_active_stream_ids())
41
+ with self.manager._lock:
42
+ pending_stream_ids = set(self.manager.pending_streams.keys())
43
+ registered_stream_ids = active_stream_ids | pending_stream_ids
44
+
45
+ # **1️⃣ Register new streams (lazy loading - don't start them yet)**
46
+ for source_id, url in db_sources.items():
47
+ if source_id not in registered_stream_ids:
48
+ logging.info(f"🟢 Registering new stream: {source_id} ({url})")
49
+ self.manager.register_stream(source_id, url)
50
+
51
+ # **2️⃣ Unregister deleted or disconnected streams**
52
+ for stream_id in registered_stream_ids:
53
+ if stream_id not in db_sources:
54
+ logging.info(f"🔴 Unregistering stream: {stream_id}")
55
+ self.manager.unregister_stream(stream_id)
56
+
57
+ # Refresh registered streams
58
+ with self.manager._lock:
59
+ pending_stream_ids = set(self.manager.pending_streams.keys())
60
+ registered_stream_ids = active_stream_ids | pending_stream_ids
61
+
62
+ # **3️⃣ Update streams if URL has changed**
63
+ for source_id, url in db_sources.items():
64
+ if source_id in registered_stream_ids:
65
+ # Check if it's an active stream or pending stream
66
+ with self.manager._lock:
67
+ is_pending = source_id in self.manager.pending_streams
68
+ if is_pending:
69
+ existing_url = self.manager.pending_streams.get(source_id)
70
+ else:
71
+ existing_url = None
72
+
73
+ if existing_url is None:
74
+ # It's an active stream, get URL from stream manager
75
+ existing_url = self.manager.get_stream_url(source_id)
76
+
77
+ # Only update if URL actually changed
78
+ if existing_url != url:
79
+ if is_pending:
80
+ # It's pending, just update the URL
81
+ with self.manager._lock:
82
+ self.manager.pending_streams[source_id] = url
83
+ logging.info(f"🟡 Updated pending stream {source_id} URL")
84
+ else:
85
+ # It's active, need to restart it
86
+ logging.info(f"🟡 Updating active stream {source_id}: New URL {url}")
87
+ # Unregister and re-register with new URL
88
+ self.manager.unregister_stream(source_id)
89
+ # Add a small delay for device cleanup
90
+ if self._is_direct_device(url) or self._is_direct_device(existing_url):
91
+ time.sleep(0.5) # Allow device to be properly released
92
+ self.manager.register_stream(source_id, url)
93
+
94
+ except Exception as e:
95
+ logging.error(f"⚠️ Error syncing streams from database: {e}")
96
+
97
+ time.sleep(self.polling_interval) # Poll every X seconds
98
+
99
+ def _is_direct_device(self, url) -> bool:
100
+ """Check if URL represents a direct video device."""
101
+ if isinstance(url, str):
102
+ return url.isdigit() or url.startswith('/dev/video')
103
+ return isinstance(url, int)
104
+
105
+ def stop(self):
106
+ """Stops the synchronization thread."""
107
+ self.running = False
@@ -1,7 +1,6 @@
1
1
  import logging
2
2
  import time
3
3
  import threading
4
- import cv2
5
4
  from typing import Any, Dict, Optional
6
5
 
7
6
  from .VideoStream import VideoStream
@@ -18,6 +17,11 @@ class VideoStreamManager:
18
17
  self.direct_device_streams: Dict[Any, Dict[str, Any]] = {}
19
18
  # Per-direct-device locks: {worker_source_id: threading.Lock}
20
19
  self.direct_device_locks: Dict[Any, threading.Lock] = {}
20
+
21
+ # Reference counting for lazy loading: {worker_source_id: set of pipeline_ids}
22
+ self.stream_references: Dict[Any, set] = {}
23
+ # Store URLs for streams that aren't started yet: {worker_source_id: url}
24
+ self.pending_streams: Dict[Any, str] = {}
21
25
 
22
26
  self.shared_device_manager = SharedVideoDeviceManager()
23
27
 
@@ -38,16 +42,71 @@ class VideoStreamManager:
38
42
  # -----------------------
39
43
  # Public API
40
44
  # -----------------------
41
- def add_stream(self, worker_source_id, url):
42
- """Adds and starts a stream (regular file/RTSP or a shared direct device) if not already present."""
45
+ def register_stream(self, worker_source_id, url):
46
+ """Register a stream URL without starting it (lazy loading)."""
43
47
  with self._lock:
48
+ if worker_source_id not in self.pending_streams:
49
+ self.pending_streams[worker_source_id] = url
50
+ logging.debug(f"📝 Registered stream {worker_source_id} for lazy loading")
51
+
52
+ def unregister_stream(self, worker_source_id):
53
+ """Unregister a stream that's no longer available in the database."""
54
+ with self._lock:
55
+ # If it's pending, remove it
56
+ if worker_source_id in self.pending_streams:
57
+ del self.pending_streams[worker_source_id]
58
+ logging.debug(f"🗑️ Unregistered pending stream {worker_source_id}")
59
+
60
+ # If it's active and has no references, remove it
61
+ if worker_source_id in self.stream_references:
62
+ if len(self.stream_references[worker_source_id]) == 0:
63
+ self._stop_stream(worker_source_id)
64
+
65
+ def acquire_stream(self, worker_source_id, pipeline_id):
66
+ """Request access to a stream for a pipeline. Starts the stream if not already running."""
67
+ with self._lock:
68
+ # Initialize reference set if needed
69
+ if worker_source_id not in self.stream_references:
70
+ self.stream_references[worker_source_id] = set()
71
+
72
+ # Add pipeline reference
73
+ self.stream_references[worker_source_id].add(pipeline_id)
74
+ logging.info(f"🔗 Pipeline {pipeline_id} acquired stream {worker_source_id} (refs: {len(self.stream_references[worker_source_id])})")
75
+
76
+ # If stream is already running, we're done
44
77
  if worker_source_id in self.streams or worker_source_id in self.direct_device_streams:
45
- logging.warning("⚠️ Stream %s is already active.", worker_source_id)
78
+ return True
79
+
80
+ # Get URL from pending streams
81
+ url = self.pending_streams.get(worker_source_id)
82
+ if not url:
83
+ logging.error(f"❌ Cannot acquire stream {worker_source_id}: URL not registered")
84
+ return False
85
+
86
+ # Start the stream (outside lock to avoid blocking)
87
+ return self._start_stream(worker_source_id, url)
88
+
89
+ def release_stream(self, worker_source_id, pipeline_id):
90
+ """Release a stream reference from a pipeline. Stops the stream if no more references."""
91
+ with self._lock:
92
+ if worker_source_id not in self.stream_references:
46
93
  return
47
-
94
+
95
+ # Remove pipeline reference
96
+ self.stream_references[worker_source_id].discard(pipeline_id)
97
+ ref_count = len(self.stream_references[worker_source_id])
98
+ logging.info(f"🔓 Pipeline {pipeline_id} released stream {worker_source_id} (refs: {ref_count})")
99
+
100
+ # If no more references, stop the stream
101
+ if ref_count == 0:
102
+ logging.info(f"💤 Stream {worker_source_id} has no more references, stopping...")
103
+ self._stop_stream(worker_source_id)
104
+
105
+ def _start_stream(self, worker_source_id, url):
106
+ """Internal method to actually start a stream."""
48
107
  if self._is_direct_device(url):
49
108
  self._add_direct_device_stream(worker_source_id, url)
50
- return
109
+ return True
51
110
 
52
111
  # Regular stream
53
112
  stream = VideoStream(url)
@@ -55,18 +114,19 @@ class VideoStreamManager:
55
114
  stream.start() # start thread
56
115
  with self._lock:
57
116
  self.streams[worker_source_id] = stream
58
- logging.info("✅ Added and started video stream: %s", worker_source_id)
117
+ logging.info("✅ Started video stream: %s", worker_source_id)
118
+ return True
59
119
  except Exception as e:
60
120
  logging.error("❌ Failed to start regular stream %s: %s", worker_source_id, e)
61
-
62
- def remove_stream(self, worker_source_id):
63
- """Stops and removes a stream (regular or direct device)."""
64
- if not worker_source_id:
65
- return
66
-
121
+ return False
122
+
123
+ def _stop_stream(self, worker_source_id):
124
+ """Internal method to stop a stream."""
67
125
  # Direct device?
68
126
  with self._lock:
69
127
  is_direct = worker_source_id in self.direct_device_streams
128
+ # Clean up references
129
+ self.stream_references.pop(worker_source_id, None)
70
130
 
71
131
  if is_direct:
72
132
  self._remove_direct_device_stream(worker_source_id)
@@ -77,20 +137,16 @@ class VideoStreamManager:
77
137
  stream = self.streams.pop(worker_source_id, None)
78
138
 
79
139
  if stream is None:
80
- logging.warning("⚠️ Stream %s not found in manager.", worker_source_id)
81
140
  return
82
141
 
83
- logging.info("🛑 Removing video stream: %s", worker_source_id)
142
+ logging.info("🛑 Stopping video stream: %s", worker_source_id)
84
143
  try:
85
- # Expectation: VideoStream.stop() should signal and join internally.
86
144
  stream.stop()
87
145
  except Exception as e:
88
146
  logging.error("❌ Error stopping stream %s: %s", worker_source_id, e)
89
147
  finally:
90
148
  stream = None
91
149
 
92
- logging.info("✅ Stream %s removed successfully.", worker_source_id)
93
-
94
150
  def start_all(self):
95
151
  """Starts all regular streams that are not alive. (Direct devices are publisher-driven.)"""
96
152
  logging.info("🔄 Starting all video streams...")
@@ -114,13 +170,13 @@ class VideoStreamManager:
114
170
 
115
171
  for wid in regular_ids:
116
172
  try:
117
- self.remove_stream(wid)
173
+ self._stop_stream(wid)
118
174
  except Exception as e:
119
175
  logging.error("Error stopping regular stream %s: %s", wid, e)
120
176
 
121
177
  for wid in direct_ids:
122
178
  try:
123
- self.remove_stream(wid)
179
+ self._stop_stream(wid)
124
180
  except Exception as e:
125
181
  logging.error("Error stopping direct device stream %s: %s", wid, e)
126
182
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nedo-vision-worker-core
3
- Version: 0.3.4
3
+ Version: 0.3.5
4
4
  Summary: Nedo Vision Worker Core Library for AI Vision Processing
5
5
  Author-email: Willy Achmat Fauzi <willy.achmat@gmail.com>
6
6
  Maintainer-email: Willy Achmat Fauzi <willy.achmat@gmail.com>
@@ -1,80 +0,0 @@
1
- import logging
2
- import os
3
- import time
4
- import threading
5
- from ..database.DatabaseManager import DatabaseManager
6
- from ..repositories.WorkerSourceRepository import WorkerSourceRepository
7
- from .VideoStreamManager import VideoStreamManager
8
-
9
- class StreamSyncThread(threading.Thread):
10
- """Thread responsible for synchronizing video streams from the database in real-time."""
11
-
12
- def __init__(self, manager: VideoStreamManager, polling_interval=5):
13
- super().__init__() # Set as a daemon so it stops with the main process
14
-
15
- self.source_file_path = DatabaseManager.STORAGE_PATHS["files"] / "source_files"
16
-
17
- self.manager = manager
18
- self.polling_interval = polling_interval
19
- self.worker_source_repo = WorkerSourceRepository()
20
- self.running = True
21
-
22
- def _get_source_file_path(self, file):
23
- """Returns the file path for a given source file."""
24
- return self.source_file_path / os.path.basename(file)
25
-
26
- def run(self):
27
- """Continuously updates the VideoStreamManager with database changes."""
28
- while self.running:
29
- try:
30
- sources = self.worker_source_repo.get_worker_sources()
31
- db_sources = {
32
- source.id: (
33
- source.url if source.type_code == "live"
34
- else source.url if source.type_code == "direct"
35
- else self._get_source_file_path(source.file_path),
36
- source.status_code
37
- ) for source in sources
38
- } # Store latest sources
39
- active_stream_ids = set(self.manager.get_active_stream_ids())
40
-
41
- # **1️⃣ Add new streams**
42
- for source_id, (url, status_code) in db_sources.items():
43
- if source_id not in active_stream_ids and status_code == "connected":
44
- logging.info(f"🟢 Adding new stream: {source_id} ({url})")
45
- self.manager.add_stream(source_id, url)
46
-
47
- # **2️⃣ Remove deleted streams**
48
- for stream_id in active_stream_ids:
49
- if stream_id not in db_sources or db_sources[stream_id][1] != "connected":
50
- logging.info(f"🔴 Removing deleted stream: {stream_id}")
51
- self.manager.remove_stream(stream_id)
52
-
53
- active_stream_ids = set(self.manager.get_active_stream_ids())
54
-
55
- # **3️⃣ Update streams if URL has changed**
56
- for source_id, (url, status_code) in db_sources.items():
57
- if source_id in active_stream_ids:
58
- existing_url = self.manager.get_stream_url(source_id)
59
- if existing_url != url:
60
- logging.info(f"🟡 Updating stream {source_id}: New URL {url}")
61
- self.manager.remove_stream(source_id)
62
- # Add a small delay for device cleanup
63
- if self._is_direct_device(url) or self._is_direct_device(existing_url):
64
- time.sleep(0.5) # Allow device to be properly released
65
- self.manager.add_stream(source_id, url)
66
-
67
- except Exception as e:
68
- logging.error(f"⚠️ Error syncing streams from database: {e}")
69
-
70
- time.sleep(self.polling_interval) # Poll every X seconds
71
-
72
- def _is_direct_device(self, url) -> bool:
73
- """Check if URL represents a direct video device."""
74
- if isinstance(url, str):
75
- return url.isdigit() or url.startswith('/dev/video')
76
- return isinstance(url, int)
77
-
78
- def stop(self):
79
- """Stops the synchronization thread."""
80
- self.running = False