nedo-vision-worker 1.2.6__tar.gz → 1.2.9__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.
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/PKG-INFO +4 -3
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/__init__.py +1 -1
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/database/DatabaseManager.py +17 -1
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/initializer/AppInitializer.py +60 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/models/worker_source_pipeline.py +2 -1
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/repositories/PPEDetectionRepository.py +21 -2
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/repositories/RestrictedAreaRepository.py +21 -2
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/DirectDeviceToRTMPStreamer.py +12 -6
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/FileToRTMPServer.py +7 -3
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/GrpcClientBase.py +23 -34
- nedo_vision_worker-1.2.9/nedo_vision_worker/services/GrpcConnection.py +147 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/PPEDetectionClient.py +27 -8
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/RTSPtoRTMPStreamer.py +10 -8
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/RestrictedAreaClient.py +24 -3
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/SystemUsageClient.py +2 -1
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/WorkerSourcePipelineClient.py +25 -7
- nedo_vision_worker-1.2.9/nedo_vision_worker/util/EncoderSelector.py +109 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/util/SystemMonitor.py +3 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/worker/DataSyncWorker.py +2 -2
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/worker/DatasetFrameWorker.py +1 -1
- nedo_vision_worker-1.2.9/nedo_vision_worker/worker/PipelinePreviewWorker.py +160 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/worker/SystemUsageManager.py +22 -3
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/worker/WorkerManager.py +4 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/worker_service.py +10 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker.egg-info/PKG-INFO +4 -3
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker.egg-info/SOURCES.txt +3 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker.egg-info/requires.txt +8 -6
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/pyproject.toml +6 -3
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/README.md +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/cli.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/config/ConfigurationManager.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/config/__init__.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/database/__init__.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/doctor.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/initializer/__init__.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/models/__init__.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/models/ai_model.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/models/auth.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/models/config.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/models/dataset_source.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/models/logs.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/models/ppe_detection.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/models/ppe_detection_label.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/models/restricted_area_violation.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/models/user.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/models/worker_source.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/models/worker_source_pipeline_config.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/models/worker_source_pipeline_debug.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/models/worker_source_pipeline_detection.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/protos/AIModelService_pb2.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/protos/AIModelService_pb2_grpc.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/protos/DatasetSourceService_pb2.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/protos/DatasetSourceService_pb2_grpc.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/protos/HumanDetectionService_pb2.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/protos/HumanDetectionService_pb2_grpc.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/protos/PPEDetectionService_pb2.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/protos/PPEDetectionService_pb2_grpc.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/protos/VisionWorkerService_pb2.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/protos/VisionWorkerService_pb2_grpc.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/protos/WorkerSourcePipelineService_pb2.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/protos/WorkerSourcePipelineService_pb2_grpc.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/protos/WorkerSourceService_pb2.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/protos/WorkerSourceService_pb2_grpc.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/protos/__init__.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/repositories/AIModelRepository.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/repositories/DatasetSourceRepository.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/repositories/WorkerSourcePipelineDebugRepository.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/repositories/WorkerSourcePipelineDetectionRepository.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/repositories/WorkerSourcePipelineRepository.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/repositories/WorkerSourceRepository.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/repositories/__init__.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/AIModelClient.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/ConnectionInfoClient.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/DatasetSourceClient.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/GrpcClientManager.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/ImageUploadClient.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/SharedDirectDeviceClient.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/SharedVideoStreamServer.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/SystemWideDeviceCoordinator.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/VideoSharingDaemon.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/VideoStreamClient.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/WorkerSourceClient.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/WorkerSourceUpdater.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/WorkerStatusClient.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/__init__.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/util/FFmpegUtil.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/util/HardwareID.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/util/ImageUploader.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/util/Networking.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/util/PlatformDetector.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/util/VideoProbeUtil.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/util/__init__.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/worker/CoreActionWorker.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/worker/DataSenderWorker.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/worker/DatasetFrameSender.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/worker/PPEDetectionManager.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/worker/PipelineActionWorker.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/worker/PipelineImageWorker.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/worker/RabbitMQListener.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/worker/RestrictedAreaManager.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/worker/VideoStreamWorker.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/worker/__init__.py +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker.egg-info/dependency_links.txt +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker.egg-info/entry_points.txt +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker.egg-info/top_level.txt +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/setup.cfg +0 -0
- {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nedo-vision-worker
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.9
|
|
4
4
|
Summary: Nedo Vision Worker Service 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>
|
|
@@ -40,9 +40,10 @@ Requires-Dist: protobuf>=3.20.0
|
|
|
40
40
|
Requires-Dist: psutil>=5.9.0
|
|
41
41
|
Requires-Dist: requests>=2.28.0
|
|
42
42
|
Requires-Dist: SQLAlchemy>=1.4.0
|
|
43
|
-
Requires-Dist: opencv-python>=4.6.0; platform_machine not in "aarch64 armv7l"
|
|
44
|
-
Requires-Dist: opencv-python-headless>=4.6.0; platform_machine in "aarch64 armv7l"
|
|
45
43
|
Requires-Dist: pynvml>=11.4.1; platform_system != "Darwin" or platform_machine != "arm64"
|
|
44
|
+
Provides-Extra: opencv
|
|
45
|
+
Requires-Dist: opencv-python>=4.6.0; platform_machine not in "aarch64 armv7l" and extra == "opencv"
|
|
46
|
+
Requires-Dist: opencv-python-headless>=4.6.0; platform_machine in "aarch64 armv7l" and extra == "opencv"
|
|
46
47
|
Provides-Extra: dev
|
|
47
48
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
48
49
|
Requires-Dist: black>=22.0.0; extra == "dev"
|
{nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/database/DatabaseManager.py
RENAMED
|
@@ -104,7 +104,23 @@ class DatabaseManager:
|
|
|
104
104
|
# Initialize engines and session factories for each database
|
|
105
105
|
for name, path in DB_PATHS.items():
|
|
106
106
|
path.parent.mkdir(parents=True, exist_ok=True) # Ensure directory exists
|
|
107
|
-
|
|
107
|
+
|
|
108
|
+
# Configure connection pool for multi-threaded usage
|
|
109
|
+
# pool_size: Max connections to keep open
|
|
110
|
+
# max_overflow: Additional connections that can be created temporarily
|
|
111
|
+
# pool_pre_ping: Test connections before using (prevents stale connections)
|
|
112
|
+
# pool_recycle: Recycle connections after N seconds (prevents long-lived stale connections)
|
|
113
|
+
engine = create_engine(
|
|
114
|
+
f"sqlite:///{path.as_posix()}",
|
|
115
|
+
pool_size=20, # Base pool size for persistent connections
|
|
116
|
+
max_overflow=30, # Allow up to 30 additional temporary connections
|
|
117
|
+
pool_pre_ping=True, # Verify connection health before use
|
|
118
|
+
pool_recycle=3600, # Recycle connections after 1 hour
|
|
119
|
+
connect_args={
|
|
120
|
+
"check_same_thread": False, # Required for SQLite with multiple threads
|
|
121
|
+
"timeout": 30.0 # Connection timeout
|
|
122
|
+
}
|
|
123
|
+
)
|
|
108
124
|
ENGINES[name] = engine
|
|
109
125
|
SESSION_FACTORIES[name] = scoped_session(sessionmaker(bind=engine)) # Use scoped sessions
|
|
110
126
|
DatabaseManager.synchronize(name)
|
|
@@ -76,3 +76,63 @@ class AppInitializer:
|
|
|
76
76
|
logging.error(f"Grpc Error: {ge}")
|
|
77
77
|
except Exception as e:
|
|
78
78
|
logging.error(f"Unexpected error during initialization: {e}")
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def update_connection_info(server_host: str, server_port: int, token: str):
|
|
82
|
+
"""
|
|
83
|
+
Fetch and update connection information (RabbitMQ credentials) from the server.
|
|
84
|
+
This should be called on startup to ensure credentials are up-to-date.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
server_host: The server hostname or IP address
|
|
88
|
+
server_port: The gRPC server port
|
|
89
|
+
token: Authentication token for the worker
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
bool: True if update was successful, False otherwise
|
|
93
|
+
"""
|
|
94
|
+
try:
|
|
95
|
+
# Validate server host
|
|
96
|
+
AppInitializer.validate_server_host(server_host)
|
|
97
|
+
|
|
98
|
+
# Get connection info using the ConnectionInfoClient
|
|
99
|
+
connection_client = ConnectionInfoClient(server_host, server_port, token)
|
|
100
|
+
connection_result = connection_client.get_connection_info()
|
|
101
|
+
|
|
102
|
+
if not connection_result["success"]:
|
|
103
|
+
logging.error(f"Failed to fetch connection info: {connection_result['message']}")
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
# Check if any RabbitMQ credentials have changed
|
|
107
|
+
current_config = ConfigurationManager.get_all_configs()
|
|
108
|
+
config_updated = False
|
|
109
|
+
|
|
110
|
+
rabbitmq_fields = {
|
|
111
|
+
'rabbitmq_host': connection_result['rabbitmq_host'],
|
|
112
|
+
'rabbitmq_port': str(connection_result['rabbitmq_port']),
|
|
113
|
+
'rabbitmq_username': connection_result['rabbitmq_username'],
|
|
114
|
+
'rabbitmq_password': connection_result['rabbitmq_password']
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for field, new_value in rabbitmq_fields.items():
|
|
118
|
+
if current_config.get(field) != new_value:
|
|
119
|
+
ConfigurationManager.set_config(field, new_value)
|
|
120
|
+
config_updated = True
|
|
121
|
+
logging.info(f"✅ [APP] Updated {field}")
|
|
122
|
+
|
|
123
|
+
if config_updated:
|
|
124
|
+
logging.info("✅ [APP] RabbitMQ connection info updated successfully")
|
|
125
|
+
else:
|
|
126
|
+
logging.info("✅ [APP] RabbitMQ connection info is up-to-date")
|
|
127
|
+
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
except ValueError as ve:
|
|
131
|
+
logging.error(f"Validation error: {ve}")
|
|
132
|
+
return False
|
|
133
|
+
except grpc.RpcError as ge:
|
|
134
|
+
logging.error(f"gRPC Error: {ge}")
|
|
135
|
+
return False
|
|
136
|
+
except Exception as e:
|
|
137
|
+
logging.error(f"Unexpected error updating connection info: {e}")
|
|
138
|
+
return False
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from sqlalchemy import Column, String
|
|
1
|
+
from sqlalchemy import Column, String, DateTime
|
|
2
2
|
from sqlalchemy.orm import relationship
|
|
3
3
|
from ..database.DatabaseManager import Base
|
|
4
4
|
|
|
@@ -13,6 +13,7 @@ class WorkerSourcePipelineEntity(Base):
|
|
|
13
13
|
ai_model_id = Column(String, nullable=True)
|
|
14
14
|
pipeline_status_code = Column(String, nullable=False)
|
|
15
15
|
location_name = Column(String, nullable=True)
|
|
16
|
+
last_preview_request_at = Column(DateTime, nullable=True)
|
|
16
17
|
|
|
17
18
|
worker_source_pipeline_configs = relationship(
|
|
18
19
|
"WorkerSourcePipelineConfigEntity",
|
|
@@ -77,6 +77,10 @@ class PPEDetectionRepository:
|
|
|
77
77
|
Args:
|
|
78
78
|
detection_data (list): List of dictionaries containing the detection data.
|
|
79
79
|
"""
|
|
80
|
+
if not detection_data:
|
|
81
|
+
logging.info("No detection data provided for deletion.")
|
|
82
|
+
return
|
|
83
|
+
|
|
80
84
|
try:
|
|
81
85
|
# Extract person_id from detection data to delete the corresponding records
|
|
82
86
|
person_ids_to_delete = [data['person_id'] for data in detection_data]
|
|
@@ -97,10 +101,25 @@ class PPEDetectionRepository:
|
|
|
97
101
|
if not os.path.isabs(image_path):
|
|
98
102
|
image_path = str(get_storage_path("files") / Path(image_path).relative_to("data/files"))
|
|
99
103
|
if os.path.exists(image_path):
|
|
100
|
-
|
|
101
|
-
|
|
104
|
+
try:
|
|
105
|
+
os.remove(image_path)
|
|
106
|
+
logging.info(f"Deleted image file: {image_path}")
|
|
107
|
+
except OSError as e:
|
|
108
|
+
logging.warning(f"Failed to delete image file {image_path}: {e}")
|
|
102
109
|
else:
|
|
103
110
|
logging.warning(f"Image file not found for detection {detection.id}: {image_path}")
|
|
111
|
+
|
|
112
|
+
# Delete the image tile file if it exists
|
|
113
|
+
image_tile_path = detection.image_tile_path
|
|
114
|
+
if image_tile_path:
|
|
115
|
+
if not os.path.isabs(image_tile_path):
|
|
116
|
+
image_tile_path = str(get_storage_path("files") / Path(image_tile_path).relative_to("data/files"))
|
|
117
|
+
if os.path.exists(image_tile_path):
|
|
118
|
+
try:
|
|
119
|
+
os.remove(image_tile_path)
|
|
120
|
+
logging.info(f"Deleted image tile file: {image_tile_path}")
|
|
121
|
+
except OSError as e:
|
|
122
|
+
logging.warning(f"Failed to delete image tile file {image_tile_path}: {e}")
|
|
104
123
|
|
|
105
124
|
# Delete the detection record
|
|
106
125
|
self.session.delete(detection)
|
|
@@ -61,6 +61,10 @@ class RestrictedAreaRepository:
|
|
|
61
61
|
Args:
|
|
62
62
|
violation_data (list): List of dictionaries containing the violation data.
|
|
63
63
|
"""
|
|
64
|
+
if not violation_data:
|
|
65
|
+
logging.info("No violation data provided for deletion.")
|
|
66
|
+
return
|
|
67
|
+
|
|
64
68
|
try:
|
|
65
69
|
person_ids_to_delete = [data['person_id'] for data in violation_data]
|
|
66
70
|
|
|
@@ -75,11 +79,26 @@ class RestrictedAreaRepository:
|
|
|
75
79
|
if not os.path.isabs(image_path):
|
|
76
80
|
image_path = str(get_storage_path("restricted_violations") / Path(image_path).relative_to("data/restricted_violations"))
|
|
77
81
|
if os.path.exists(image_path):
|
|
78
|
-
|
|
79
|
-
|
|
82
|
+
try:
|
|
83
|
+
os.remove(image_path)
|
|
84
|
+
logging.info(f"Deleted image file: {image_path}")
|
|
85
|
+
except OSError as e:
|
|
86
|
+
logging.warning(f"Failed to delete image file {image_path}: {e}")
|
|
80
87
|
else:
|
|
81
88
|
logging.warning(f"Image file not found for violation {violation.id}: {image_path}")
|
|
82
89
|
|
|
90
|
+
# Delete the image tile file if it exists
|
|
91
|
+
image_tile_path = violation.image_tile_path
|
|
92
|
+
if image_tile_path:
|
|
93
|
+
if not os.path.isabs(image_tile_path):
|
|
94
|
+
image_tile_path = str(get_storage_path("restricted_violations") / Path(image_tile_path).relative_to("data/restricted_violations"))
|
|
95
|
+
if os.path.exists(image_tile_path):
|
|
96
|
+
try:
|
|
97
|
+
os.remove(image_tile_path)
|
|
98
|
+
logging.info(f"Deleted image tile file: {image_tile_path}")
|
|
99
|
+
except OSError as e:
|
|
100
|
+
logging.warning(f"Failed to delete image tile file {image_tile_path}: {e}")
|
|
101
|
+
|
|
83
102
|
self.session.delete(violation)
|
|
84
103
|
|
|
85
104
|
self.session.commit()
|
|
@@ -8,6 +8,7 @@ import cv2
|
|
|
8
8
|
import os
|
|
9
9
|
from .VideoSharingDaemon import VideoSharingClient
|
|
10
10
|
from ..database.DatabaseManager import get_storage_path
|
|
11
|
+
from ..util.EncoderSelector import EncoderSelector
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
class DirectDeviceToRTMPStreamer:
|
|
@@ -68,6 +69,10 @@ class DirectDeviceToRTMPStreamer:
|
|
|
68
69
|
|
|
69
70
|
def _start_ffmpeg_stream(self):
|
|
70
71
|
"""Starts an FFmpeg process to stream frames to the RTMP server silently."""
|
|
72
|
+
# Get optimal encoder for hardware
|
|
73
|
+
encoder_args, encoder_name = EncoderSelector.get_encoder_args()
|
|
74
|
+
logging.info(f"🎬 [APP] Using encoder: {encoder_name}")
|
|
75
|
+
|
|
71
76
|
ffmpeg_command = [
|
|
72
77
|
"ffmpeg",
|
|
73
78
|
"-y",
|
|
@@ -79,16 +84,17 @@ class DirectDeviceToRTMPStreamer:
|
|
|
79
84
|
"-video_size", f"{self.width}x{self.height}",
|
|
80
85
|
"-framerate", str(self.fps),
|
|
81
86
|
"-i", "-",
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
87
|
+
|
|
88
|
+
# Video encoding with optimal encoder
|
|
89
|
+
*encoder_args,
|
|
85
90
|
"-b:v", self.bitrate,
|
|
86
|
-
# Disable Audio (Avoid unnecessary encoding overhead)
|
|
87
|
-
"-an",
|
|
88
91
|
"-maxrate", "2500k",
|
|
89
92
|
"-bufsize", "5000k",
|
|
93
|
+
|
|
94
|
+
# Disable Audio
|
|
95
|
+
"-an",
|
|
96
|
+
|
|
90
97
|
"-f", "flv",
|
|
91
|
-
# Remove duration limit - let application control duration
|
|
92
98
|
self.rtmp_url,
|
|
93
99
|
]
|
|
94
100
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import subprocess
|
|
2
2
|
import logging
|
|
3
3
|
import os
|
|
4
|
+
from ..util.EncoderSelector import EncoderSelector
|
|
4
5
|
|
|
5
6
|
class FileToRTMPStreamer:
|
|
6
7
|
def __init__(self, video_path, rtmp_url, stream_key, fps=30, resolution="1280x720", loop=False):
|
|
@@ -31,6 +32,10 @@ class FileToRTMPStreamer:
|
|
|
31
32
|
|
|
32
33
|
logging.info(f"📼 [APP] Starting file stream: {self.video_path} → {self.rtmp_url}")
|
|
33
34
|
|
|
35
|
+
# Get optimal encoder for hardware
|
|
36
|
+
encoder_args, encoder_name = EncoderSelector.get_encoder_args()
|
|
37
|
+
logging.info(f"🎬 [APP] Using encoder: {encoder_name}")
|
|
38
|
+
|
|
34
39
|
# FFmpeg command
|
|
35
40
|
ffmpeg_command = [
|
|
36
41
|
"ffmpeg",
|
|
@@ -38,9 +43,8 @@ class FileToRTMPStreamer:
|
|
|
38
43
|
"-stream_loop", "-1" if self.loop else "0", # Loop if needed
|
|
39
44
|
"-i", self.video_path,
|
|
40
45
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"-tune", "zerolatency",
|
|
46
|
+
# Video encoding with optimal encoder
|
|
47
|
+
*encoder_args,
|
|
44
48
|
"-r", str(self.fps),
|
|
45
49
|
"-b:v", "1500k",
|
|
46
50
|
"-maxrate", "2000k",
|
{nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/GrpcClientBase.py
RENAMED
|
@@ -3,6 +3,7 @@ import logging
|
|
|
3
3
|
import time
|
|
4
4
|
from grpc import StatusCode
|
|
5
5
|
from typing import Callable, Optional, Any, Dict
|
|
6
|
+
from .GrpcConnection import GrpcConnection
|
|
6
7
|
|
|
7
8
|
logger = logging.getLogger(__name__)
|
|
8
9
|
|
|
@@ -42,42 +43,21 @@ class GrpcClientBase:
|
|
|
42
43
|
def __init__(self, server_host: str, server_port: int = 50051, max_retries: int = 3):
|
|
43
44
|
self.server_address = f"{server_host}:{server_port}"
|
|
44
45
|
self.channel: Optional[grpc.Channel] = None
|
|
45
|
-
self.stub: Optional[Any] = None
|
|
46
46
|
self.connected = False
|
|
47
47
|
self.max_retries = max_retries
|
|
48
48
|
|
|
49
|
+
self.connection = GrpcConnection(server_host, server_port)
|
|
50
|
+
|
|
49
51
|
def connect(self, stub_class, retry_interval: int = 2) -> bool:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
future.result(timeout=30)
|
|
60
|
-
|
|
61
|
-
self.stub = stub_class(self.channel)
|
|
62
|
-
self.connected = True
|
|
63
|
-
logger.info(f"🚀 Connected to gRPC server at {self.server_address}")
|
|
64
|
-
return True
|
|
65
|
-
|
|
66
|
-
except (grpc.RpcError, grpc.FutureTimeoutError, Exception) as e:
|
|
67
|
-
attempts += 1
|
|
68
|
-
self.connected = False
|
|
69
|
-
error_msg = str(e)
|
|
70
|
-
|
|
71
|
-
logger.error(f"⚠️ Connection failed ({attempts}/{self.max_retries}): {error_msg}", exc_info=True)
|
|
72
|
-
|
|
73
|
-
if attempts < self.max_retries:
|
|
74
|
-
sleep_time = retry_interval * (2 ** (attempts - 1))
|
|
75
|
-
logger.info(f"⏳ Retrying in {sleep_time}s...")
|
|
76
|
-
time.sleep(sleep_time)
|
|
77
|
-
else:
|
|
78
|
-
logger.critical("❌ Max retries reached. Connection failed.")
|
|
79
|
-
|
|
80
|
-
return False
|
|
52
|
+
conn = self.connection.get_connection()
|
|
53
|
+
if conn is None:
|
|
54
|
+
return False
|
|
55
|
+
requested_stub = stub_class(conn)
|
|
56
|
+
|
|
57
|
+
self.stub = requested_stub
|
|
58
|
+
self.connected = True
|
|
59
|
+
|
|
60
|
+
return True
|
|
81
61
|
|
|
82
62
|
def _close_channel(self) -> None:
|
|
83
63
|
try:
|
|
@@ -89,6 +69,7 @@ class GrpcClientBase:
|
|
|
89
69
|
self.channel = None
|
|
90
70
|
self.stub = None
|
|
91
71
|
|
|
72
|
+
# MARK:
|
|
92
73
|
def close(self) -> None:
|
|
93
74
|
self._close_channel()
|
|
94
75
|
self.connected = False
|
|
@@ -104,6 +85,11 @@ class GrpcClientBase:
|
|
|
104
85
|
except grpc.RpcError as e:
|
|
105
86
|
return self._handle_grpc_error(e, rpc_call, *args, **kwargs)
|
|
106
87
|
except Exception as e:
|
|
88
|
+
print(e)
|
|
89
|
+
print(str(e) == "Cannot invoke RPC on closed channel!")
|
|
90
|
+
if str(e) == "Cannot invoke RPC on closed channel!":
|
|
91
|
+
self.connect(type(self.stub))
|
|
92
|
+
|
|
107
93
|
logger.error(f"💥 Unexpected RPC error: {e}")
|
|
108
94
|
return None
|
|
109
95
|
|
|
@@ -127,8 +113,11 @@ class GrpcClientBase:
|
|
|
127
113
|
|
|
128
114
|
return None
|
|
129
115
|
|
|
116
|
+
# MARK:
|
|
117
|
+
# Should request for reconnection two times. Notify grpc connection to do reconnect
|
|
130
118
|
def _handle_unavailable(self, rpc_call: Callable, *args, **kwargs) -> Optional[Any]:
|
|
131
|
-
self.connected = False
|
|
119
|
+
# self.connected = False
|
|
120
|
+
self.connection.try_reconnect()
|
|
132
121
|
|
|
133
122
|
if self.stub:
|
|
134
123
|
stub_class = type(self.stub)
|
|
@@ -154,7 +143,7 @@ class GrpcClientBase:
|
|
|
154
143
|
return error_message.split("debug_error_string")[0].strip()
|
|
155
144
|
|
|
156
145
|
def is_connected(self) -> bool:
|
|
157
|
-
return self.connected and self.
|
|
146
|
+
return self.connected and self.connection.get_connection() is not None and self.stub is not None
|
|
158
147
|
|
|
159
148
|
def get_connection_info(self) -> Dict[str, Any]:
|
|
160
149
|
return {
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import grpc
|
|
2
|
+
import logging
|
|
3
|
+
import time
|
|
4
|
+
import threading
|
|
5
|
+
from grpc import StatusCode
|
|
6
|
+
from typing import Optional, Any, Dict
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GrpcConnection:
|
|
12
|
+
"""
|
|
13
|
+
Grpc connection management. Responsible for initiating connection with gRPC server that
|
|
14
|
+
will be used across gRPC clients in the project.
|
|
15
|
+
Expected to be initiated as a singleton.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
_instance = None
|
|
19
|
+
_init_done = False
|
|
20
|
+
_lock = threading.Lock()
|
|
21
|
+
_reconnectLock = threading.Lock()
|
|
22
|
+
_reconnecting = False
|
|
23
|
+
|
|
24
|
+
def __new__(cls, *args, **kwargs):
|
|
25
|
+
if cls._instance is None:
|
|
26
|
+
with cls._lock:
|
|
27
|
+
if cls._instance is None:
|
|
28
|
+
cls._instance = super().__new__(cls)
|
|
29
|
+
return cls._instance
|
|
30
|
+
|
|
31
|
+
def __init__(self, server_host: str, server_port: int = 50051, max_retries: int = 3):
|
|
32
|
+
if self.__class__._init_done:
|
|
33
|
+
return # prevent re-initialization
|
|
34
|
+
self.__class__._init_done = True
|
|
35
|
+
|
|
36
|
+
self.server_address = f"{server_host}:{server_port}"
|
|
37
|
+
self.channel: Optional[grpc.Channel] = None
|
|
38
|
+
self.connected = False
|
|
39
|
+
self.max_retries = max_retries
|
|
40
|
+
self.connect()
|
|
41
|
+
|
|
42
|
+
def connect(self, retry_interval: int = 2) -> bool:
|
|
43
|
+
attempts = 0
|
|
44
|
+
while attempts < self.max_retries and not self.connected:
|
|
45
|
+
try:
|
|
46
|
+
if self.channel:
|
|
47
|
+
self._close_channel()
|
|
48
|
+
|
|
49
|
+
self.channel = grpc.insecure_channel(self.server_address)
|
|
50
|
+
|
|
51
|
+
future = grpc.channel_ready_future(self.channel)
|
|
52
|
+
future.result(timeout=30)
|
|
53
|
+
|
|
54
|
+
self.connected = True
|
|
55
|
+
logger.info(f"🚀 Connected to gRPC server at {self.server_address}")
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
except (grpc.RpcError, grpc.FutureTimeoutError, Exception) as e:
|
|
59
|
+
attempts += 1
|
|
60
|
+
self.connected = False
|
|
61
|
+
error_msg = str(e)
|
|
62
|
+
|
|
63
|
+
logger.error(f"⚠️ Connection failed ({attempts}/{self.max_retries}): {error_msg}")
|
|
64
|
+
|
|
65
|
+
if attempts < self.max_retries:
|
|
66
|
+
sleep_time = retry_interval * (2 ** (attempts - 1))
|
|
67
|
+
logger.info(f"⏳ Retrying in {sleep_time}s...")
|
|
68
|
+
time.sleep(sleep_time)
|
|
69
|
+
else:
|
|
70
|
+
logger.critical("❌ Max retries reached. Connection failed.")
|
|
71
|
+
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
def get_connection(self):
|
|
75
|
+
if self._reconnecting or not self.connected:
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
return self.channel
|
|
79
|
+
|
|
80
|
+
def _reconnect(self):
|
|
81
|
+
logger.info(f"⏳ Reconnecting...")
|
|
82
|
+
attempts = 0
|
|
83
|
+
|
|
84
|
+
while not self.connected:
|
|
85
|
+
try:
|
|
86
|
+
if self.channel:
|
|
87
|
+
self._close_channel()
|
|
88
|
+
|
|
89
|
+
self.channel = grpc.insecure_channel(self.server_address)
|
|
90
|
+
|
|
91
|
+
future = grpc.channel_ready_future(self.channel)
|
|
92
|
+
future.result(timeout=30)
|
|
93
|
+
|
|
94
|
+
self.connected = True
|
|
95
|
+
self._reconnecting = False
|
|
96
|
+
logger.info(f"🚀 Connected to gRPC server at {self.server_address}")
|
|
97
|
+
self._reconnectLock.release_lock()
|
|
98
|
+
except (grpc.RpcError, grpc.FutureTimeoutError, Exception) as e:
|
|
99
|
+
attempts += 1
|
|
100
|
+
self.connected = False
|
|
101
|
+
error_msg = str(e)
|
|
102
|
+
|
|
103
|
+
logger.error(f"⚠️ Connection failed ({attempts}/{self.max_retries}): {error_msg}")
|
|
104
|
+
|
|
105
|
+
sleep_time = 2 * (2 ** (attempts - 1))
|
|
106
|
+
logger.info(f"⏳ Retrying in {sleep_time}s...")
|
|
107
|
+
time.sleep(sleep_time)
|
|
108
|
+
|
|
109
|
+
def try_reconnect(self):
|
|
110
|
+
if self._reconnecting:
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
if self._reconnectLock.acquire_lock(blocking=False):
|
|
114
|
+
self._reconnecting = True
|
|
115
|
+
self.connected = False
|
|
116
|
+
self._reconnect()
|
|
117
|
+
|
|
118
|
+
def _close_channel(self) -> None:
|
|
119
|
+
try:
|
|
120
|
+
if self.channel:
|
|
121
|
+
self.channel.close()
|
|
122
|
+
logger.info("🔌 gRPC connection closed")
|
|
123
|
+
except Exception as e:
|
|
124
|
+
logger.warning(f"⚠️ Error closing channel: {e}")
|
|
125
|
+
finally:
|
|
126
|
+
self.channel = None
|
|
127
|
+
self.connected = False
|
|
128
|
+
|
|
129
|
+
def close(self) -> None:
|
|
130
|
+
if self.channel:
|
|
131
|
+
self.channel.close()
|
|
132
|
+
self.connected = False
|
|
133
|
+
logger.info("🔌 gRPC connection closed")
|
|
134
|
+
|
|
135
|
+
def is_connected(self) -> bool:
|
|
136
|
+
return self.connected and self.channel is not None
|
|
137
|
+
|
|
138
|
+
def get_connection_info(self) -> Dict[str, Any]:
|
|
139
|
+
return {
|
|
140
|
+
"server_address": self.server_address,
|
|
141
|
+
"connected": self.connected,
|
|
142
|
+
"max_retries": self.max_retries,
|
|
143
|
+
"has_channel": self.channel is not None,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
def __enter__(self):
|
|
147
|
+
return self
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import os
|
|
2
3
|
from .GrpcClientBase import GrpcClientBase
|
|
3
4
|
from ..protos.PPEDetectionService_pb2_grpc import PPEDetectionGRPCServiceStub
|
|
4
5
|
from ..protos.PPEDetectionService_pb2 import UpsertPPEDetectionBatchRequest, UpsertPPEDetectionRequest, PPEDetectionLabelRequest
|
|
@@ -56,11 +57,23 @@ class PPEDetectionClient(GrpcClientBase):
|
|
|
56
57
|
return {"success": False, "message": "gRPC connection is not established."}
|
|
57
58
|
|
|
58
59
|
try:
|
|
59
|
-
# Prepare the list of UpsertPPEDetectionRequest messages
|
|
60
60
|
ppe_detection_requests = []
|
|
61
|
+
valid_records = []
|
|
62
|
+
invalid_records = []
|
|
63
|
+
|
|
61
64
|
for data in detection_data:
|
|
62
|
-
|
|
63
|
-
|
|
65
|
+
if not os.path.exists(data['image']) or not os.path.exists(data['image_tile']):
|
|
66
|
+
logger.warning(f"⚠️ Missing image files for person_id {data.get('person_id')}")
|
|
67
|
+
invalid_records.append(data)
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
image_binary = self.read_image_as_binary(data['image'])
|
|
72
|
+
image_tile_binary = self.read_image_as_binary(data['image_tile'])
|
|
73
|
+
except Exception as e:
|
|
74
|
+
logger.error(f"❌ Error reading images for person_id {data.get('person_id')}: {e}")
|
|
75
|
+
invalid_records.append(data)
|
|
76
|
+
continue
|
|
64
77
|
|
|
65
78
|
ppe_detection_labels = [
|
|
66
79
|
PPEDetectionLabelRequest(
|
|
@@ -73,11 +86,11 @@ class PPEDetectionClient(GrpcClientBase):
|
|
|
73
86
|
)
|
|
74
87
|
for label in data['ppe_detection_labels']
|
|
75
88
|
]
|
|
76
|
-
|
|
89
|
+
|
|
77
90
|
request = UpsertPPEDetectionRequest(
|
|
78
91
|
person_id=data['person_id'],
|
|
79
92
|
worker_id=worker_id,
|
|
80
|
-
worker_source_id=
|
|
93
|
+
worker_source_id=data['worker_source_id'],
|
|
81
94
|
image=image_binary,
|
|
82
95
|
image_tile=image_tile_binary,
|
|
83
96
|
worker_timestamp=data['worker_timestamp'],
|
|
@@ -85,18 +98,24 @@ class PPEDetectionClient(GrpcClientBase):
|
|
|
85
98
|
token=token
|
|
86
99
|
)
|
|
87
100
|
ppe_detection_requests.append(request)
|
|
101
|
+
valid_records.append(data)
|
|
102
|
+
|
|
103
|
+
if invalid_records:
|
|
104
|
+
logger.info(f"🧹 Deleting {len(invalid_records)} invalid PPE detection records")
|
|
105
|
+
self.repository.delete_records_from_db(invalid_records)
|
|
106
|
+
|
|
107
|
+
if not ppe_detection_requests:
|
|
108
|
+
return {"success": True, "message": "No valid detections to send"}
|
|
88
109
|
|
|
89
|
-
# Create the UpsertPPEDetectionBatchRequest
|
|
90
110
|
batch_request = UpsertPPEDetectionBatchRequest(
|
|
91
111
|
ppe_detection_requests=ppe_detection_requests,
|
|
92
112
|
token=token
|
|
93
113
|
)
|
|
94
114
|
|
|
95
|
-
# Call the UpsertBatch RPC
|
|
96
115
|
response = self.handle_rpc(self.stub.UpsertBatch, batch_request)
|
|
97
116
|
|
|
98
117
|
if response and response.success:
|
|
99
|
-
self.repository.delete_records_from_db(
|
|
118
|
+
self.repository.delete_records_from_db(valid_records)
|
|
100
119
|
return {"success": True, "message": response.message}
|
|
101
120
|
|
|
102
121
|
return {"success": False, "message": response.message if response else "Unknown error"}
|
|
@@ -3,6 +3,7 @@ import logging
|
|
|
3
3
|
import time
|
|
4
4
|
import os
|
|
5
5
|
from urllib.parse import urlparse
|
|
6
|
+
from ..util.EncoderSelector import EncoderSelector
|
|
6
7
|
|
|
7
8
|
class RTSPtoRTMPStreamer:
|
|
8
9
|
def __init__(self, rtsp_url, rtmp_url, stream_key, fps=30, resolution="1280x720", duration=120):
|
|
@@ -38,6 +39,10 @@ class RTSPtoRTMPStreamer:
|
|
|
38
39
|
|
|
39
40
|
logging.info(f"📡 [APP] Starting RTSP to RTMP stream: {self.rtsp_url} → {self.rtmp_url} for {self.duration} seconds")
|
|
40
41
|
|
|
42
|
+
# Get optimal encoder for hardware
|
|
43
|
+
encoder_args, encoder_name = EncoderSelector.get_encoder_args()
|
|
44
|
+
logging.info(f"🎬 [APP] Using encoder: {encoder_name}")
|
|
45
|
+
|
|
41
46
|
# FFmpeg command
|
|
42
47
|
ffmpeg_command = [
|
|
43
48
|
"ffmpeg",
|
|
@@ -47,17 +52,14 @@ class RTSPtoRTMPStreamer:
|
|
|
47
52
|
"-strict", "experimental",
|
|
48
53
|
"-i", self.rtsp_url,
|
|
49
54
|
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
"-
|
|
53
|
-
"-
|
|
54
|
-
"-x264-params", "keyint=40:min-keyint=40", # 🚀 Keyframe optimization
|
|
55
|
-
"-r", "25", # ⏳ Limit FPS to 20 (prevents excessive encoding load)
|
|
56
|
-
"-b:v", "1500k", # ✅ Lower bitrate to improve performance
|
|
55
|
+
# Video encoding with optimal encoder
|
|
56
|
+
*encoder_args,
|
|
57
|
+
"-r", "25", # ⏳ Limit FPS to 25
|
|
58
|
+
"-b:v", "1500k", # ✅ Bitrate
|
|
57
59
|
"-maxrate", "2000k", # ✅ Set max bitrate
|
|
58
60
|
"-bufsize", "4000k", # ✅ Reduce buffer latency
|
|
59
61
|
"-g", "25", # ✅ Reduce GOP size for faster keyframes
|
|
60
|
-
"-vf", "scale='min(1024,iw)':-2", # ✅ Resize width to max
|
|
62
|
+
"-vf", "scale='min(1024,iw)':-2", # ✅ Resize width to max 1024px
|
|
61
63
|
|
|
62
64
|
# ❌ Disable Audio (Avoid unnecessary encoding overhead)
|
|
63
65
|
"-an",
|