matrice-streaming 0.1.62__tar.gz → 0.1.64__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.
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/PKG-INFO +1 -1
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/matrice_streaming.egg-info/PKG-INFO +1 -1
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/camera_streamer/nvdec.py +333 -14
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/LICENSE.txt +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/README.md +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/matrice_streaming.egg-info/SOURCES.txt +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/matrice_streaming.egg-info/dependency_links.txt +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/matrice_streaming.egg-info/not-zip-safe +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/matrice_streaming.egg-info/top_level.txt +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/pyproject.toml +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/setup.cfg +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/setup.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/__init__.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/client/__init__.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/client/client.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/client/client_utils.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/deployment/__init__.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/deployment/camera_manager.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/deployment/deployment.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/deployment/inference_pipeline.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/deployment/streaming_gateway_manager.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/deployment/todo.txt +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/py.typed +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/__init__.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/camera_streamer/ARCHITECTURE.md +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/camera_streamer/__init__.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/camera_streamer/async_camera_worker.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/camera_streamer/async_ffmpeg_worker.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/camera_streamer/camera_streamer.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/camera_streamer/device_detection.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/camera_streamer/encoder_manager.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/camera_streamer/encoding_pool_manager.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_camera_streamer.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_config.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_worker_manager.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/camera_streamer/frame_processor.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/camera_streamer/gstreamer_camera_streamer.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/camera_streamer/gstreamer_worker.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/camera_streamer/gstreamer_worker_manager.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/camera_streamer/message_builder.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/camera_streamer/nvdec_worker_manager.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/camera_streamer/platform_pipelines.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/camera_streamer/retry_manager.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/camera_streamer/stream_statistics.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/camera_streamer/video_capture_manager.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/camera_streamer/worker_manager.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/debug/README.md +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/debug/__init__.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/debug/benchmark.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/debug/debug_gstreamer_gateway.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/debug/debug_stream_backend.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/debug/debug_streaming_gateway.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/debug/debug_utils.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/debug/example_debug_streaming.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/debug/test_videoplayback.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/dynamic_camera_manager.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/event_listener.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/metrics_reporter.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/streaming_action.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/streaming_gateway.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/streaming_gateway_utils.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/streaming_gateway/streaming_status_listener.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/tests/test_async_infrastructure.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/tests/test_batch_auto_calculation.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/tests/test_batching_verification.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/tests/test_e2e_production.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/tests/test_flatten_binary.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/tests/test_gstreamer_integration.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/tests/test_msgpack_fix.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/tests/test_phase1_unit.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/tests/test_phase2_scaling.py +0 -0
|
@@ -56,11 +56,21 @@ import os
|
|
|
56
56
|
import time
|
|
57
57
|
import threading
|
|
58
58
|
import queue as thread_queue
|
|
59
|
+
import hashlib
|
|
60
|
+
import tempfile
|
|
59
61
|
from dataclasses import dataclass
|
|
62
|
+
from pathlib import Path
|
|
60
63
|
from typing import Dict, List, Optional, Tuple, Any
|
|
64
|
+
from urllib.parse import urlparse, urlunparse
|
|
61
65
|
|
|
62
66
|
import numpy as np
|
|
63
67
|
|
|
68
|
+
try:
|
|
69
|
+
import requests
|
|
70
|
+
REQUESTS_AVAILABLE = True
|
|
71
|
+
except ImportError:
|
|
72
|
+
REQUESTS_AVAILABLE = False
|
|
73
|
+
|
|
64
74
|
try:
|
|
65
75
|
import cupy as cp
|
|
66
76
|
CUPY_AVAILABLE = True
|
|
@@ -93,6 +103,267 @@ def setup_logging(quiet: bool = True):
|
|
|
93
103
|
logging.getLogger('cuda_shm_ring_buffer').setLevel(logging.WARNING if quiet else logging.INFO)
|
|
94
104
|
|
|
95
105
|
|
|
106
|
+
# =============================================================================
|
|
107
|
+
# Video Downloader for HTTPS URLs (PyNvVideoCodec's FFmpeg lacks HTTPS support)
|
|
108
|
+
# =============================================================================
|
|
109
|
+
|
|
110
|
+
class VideoDownloader:
|
|
111
|
+
"""Downloads and caches video files from HTTPS URLs.
|
|
112
|
+
|
|
113
|
+
PyNvVideoCodec uses a bundled FFmpeg that doesn't have HTTPS support.
|
|
114
|
+
This class downloads HTTPS videos to local files before passing them
|
|
115
|
+
to the NVDEC demuxer.
|
|
116
|
+
|
|
117
|
+
Features:
|
|
118
|
+
- URL deduplication: same video URL (ignoring query params) is only downloaded once
|
|
119
|
+
- Disk caching: reuses existing files across runs
|
|
120
|
+
- Progress tracking for large files
|
|
121
|
+
- Dynamic timeout based on file size
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
# Configuration
|
|
125
|
+
DOWNLOAD_TIMEOUT = 300 # Base timeout in seconds
|
|
126
|
+
DOWNLOAD_TIMEOUT_PER_100MB = 300 # Additional seconds per 100MB
|
|
127
|
+
MAX_DOWNLOAD_TIMEOUT = 6000 # 100 minutes max
|
|
128
|
+
DOWNLOAD_CHUNK_SIZE = 8192
|
|
129
|
+
|
|
130
|
+
# Singleton instance for process-wide caching
|
|
131
|
+
_instance: Optional['VideoDownloader'] = None
|
|
132
|
+
_lock = threading.Lock()
|
|
133
|
+
|
|
134
|
+
def __new__(cls):
|
|
135
|
+
"""Singleton pattern for process-wide cache sharing."""
|
|
136
|
+
if cls._instance is None:
|
|
137
|
+
with cls._lock:
|
|
138
|
+
if cls._instance is None:
|
|
139
|
+
cls._instance = super().__new__(cls)
|
|
140
|
+
cls._instance._initialized = False
|
|
141
|
+
return cls._instance
|
|
142
|
+
|
|
143
|
+
def __init__(self):
|
|
144
|
+
"""Initialize the video downloader."""
|
|
145
|
+
if self._initialized:
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
self._initialized = True
|
|
149
|
+
self.downloaded_files: Dict[str, str] = {}
|
|
150
|
+
self._normalized_url_to_path: Dict[str, str] = {}
|
|
151
|
+
self._download_lock = threading.Lock()
|
|
152
|
+
self.temp_dir = Path(tempfile.gettempdir()) / "nvdec_video_cache"
|
|
153
|
+
self.temp_dir.mkdir(exist_ok=True)
|
|
154
|
+
logger.info(f"VideoDownloader initialized, cache dir: {self.temp_dir}")
|
|
155
|
+
|
|
156
|
+
def prepare_source(self, video_path: str, camera_id: str) -> str:
|
|
157
|
+
"""Prepare video source, downloading HTTPS URLs if needed.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
video_path: Video file path, RTSP URL, or HTTPS URL
|
|
161
|
+
camera_id: Camera identifier for logging
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Local file path (downloaded if HTTPS) or original path
|
|
165
|
+
"""
|
|
166
|
+
if not self._is_https_url(video_path):
|
|
167
|
+
return video_path
|
|
168
|
+
|
|
169
|
+
if not REQUESTS_AVAILABLE:
|
|
170
|
+
logger.warning(f"requests module not available, cannot download HTTPS URL for {camera_id}")
|
|
171
|
+
return video_path
|
|
172
|
+
|
|
173
|
+
local_path = self._download_video(video_path, camera_id)
|
|
174
|
+
if local_path:
|
|
175
|
+
return local_path
|
|
176
|
+
|
|
177
|
+
logger.warning(f"Failed to download {video_path} for {camera_id}, will try URL directly (may fail)")
|
|
178
|
+
return video_path
|
|
179
|
+
|
|
180
|
+
def _is_https_url(self, source: str) -> bool:
|
|
181
|
+
"""Check if source is an HTTPS URL."""
|
|
182
|
+
return source.startswith('https://')
|
|
183
|
+
|
|
184
|
+
def _normalize_url(self, url: str) -> str:
|
|
185
|
+
"""Normalize URL by stripping query parameters for deduplication."""
|
|
186
|
+
parsed = urlparse(url)
|
|
187
|
+
return urlunparse((
|
|
188
|
+
parsed.scheme,
|
|
189
|
+
parsed.netloc,
|
|
190
|
+
parsed.path,
|
|
191
|
+
'', '', '' # params, query, fragment
|
|
192
|
+
))
|
|
193
|
+
|
|
194
|
+
def _get_url_hash(self, normalized_url: str) -> str:
|
|
195
|
+
"""Generate a short hash for consistent file naming."""
|
|
196
|
+
return hashlib.md5(normalized_url.encode()).hexdigest()[:12]
|
|
197
|
+
|
|
198
|
+
def _download_video(self, url: str, camera_id: str) -> Optional[str]:
|
|
199
|
+
"""Download video file from HTTPS URL with caching.
|
|
200
|
+
|
|
201
|
+
Thread-safe: uses lock to prevent duplicate downloads.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
url: HTTPS video URL
|
|
205
|
+
camera_id: Camera identifier for logging
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Local file path or None if download failed
|
|
209
|
+
"""
|
|
210
|
+
normalized_url = self._normalize_url(url)
|
|
211
|
+
file_ext = Path(url.split('?')[0]).suffix or '.mp4'
|
|
212
|
+
url_hash = self._get_url_hash(normalized_url)
|
|
213
|
+
expected_path = self.temp_dir / f"video_{url_hash}{file_ext}"
|
|
214
|
+
expected_path_str = str(expected_path)
|
|
215
|
+
|
|
216
|
+
# Quick check: file already on disk
|
|
217
|
+
if expected_path.exists():
|
|
218
|
+
existing_size = expected_path.stat().st_size
|
|
219
|
+
logger.info(
|
|
220
|
+
f"[{camera_id}] Reusing cached video: {expected_path.name} "
|
|
221
|
+
f"({existing_size / (1024*1024):.1f}MB)"
|
|
222
|
+
)
|
|
223
|
+
with self._download_lock:
|
|
224
|
+
self.downloaded_files[url] = expected_path_str
|
|
225
|
+
self._normalized_url_to_path[normalized_url] = expected_path_str
|
|
226
|
+
return expected_path_str
|
|
227
|
+
|
|
228
|
+
# Check memory cache
|
|
229
|
+
with self._download_lock:
|
|
230
|
+
if url in self.downloaded_files:
|
|
231
|
+
local_path = self.downloaded_files[url]
|
|
232
|
+
if os.path.exists(local_path):
|
|
233
|
+
logger.debug(f"[{camera_id}] Using cached path (exact URL match)")
|
|
234
|
+
return local_path
|
|
235
|
+
|
|
236
|
+
if normalized_url in self._normalized_url_to_path:
|
|
237
|
+
local_path = self._normalized_url_to_path[normalized_url]
|
|
238
|
+
if os.path.exists(local_path):
|
|
239
|
+
logger.info(f"[{camera_id}] Reusing download (same base URL)")
|
|
240
|
+
self.downloaded_files[url] = local_path
|
|
241
|
+
return local_path
|
|
242
|
+
|
|
243
|
+
# Need to download - acquire lock to prevent duplicate downloads
|
|
244
|
+
with self._download_lock:
|
|
245
|
+
# Double-check after acquiring lock
|
|
246
|
+
if expected_path.exists():
|
|
247
|
+
self.downloaded_files[url] = expected_path_str
|
|
248
|
+
self._normalized_url_to_path[normalized_url] = expected_path_str
|
|
249
|
+
return expected_path_str
|
|
250
|
+
|
|
251
|
+
return self._do_download(url, expected_path, camera_id)
|
|
252
|
+
|
|
253
|
+
def _do_download(self, url: str, dest_path: Path, camera_id: str) -> Optional[str]:
|
|
254
|
+
"""Perform the actual download. Must be called with _download_lock held."""
|
|
255
|
+
content_length = 0
|
|
256
|
+
file_size_mb = 0.0
|
|
257
|
+
bytes_downloaded = 0
|
|
258
|
+
timeout = self.DOWNLOAD_TIMEOUT
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
# HEAD request to get file size
|
|
262
|
+
try:
|
|
263
|
+
head_response = requests.head(url, timeout=10, allow_redirects=True)
|
|
264
|
+
content_length = int(head_response.headers.get('Content-Length', 0))
|
|
265
|
+
file_size_mb = content_length / (1024 * 1024)
|
|
266
|
+
except Exception as e:
|
|
267
|
+
logger.debug(f"[{camera_id}] HEAD request failed: {e}")
|
|
268
|
+
|
|
269
|
+
# Calculate dynamic timeout
|
|
270
|
+
if content_length > 0:
|
|
271
|
+
timeout = min(
|
|
272
|
+
self.DOWNLOAD_TIMEOUT + int(file_size_mb // 100) * self.DOWNLOAD_TIMEOUT_PER_100MB,
|
|
273
|
+
self.MAX_DOWNLOAD_TIMEOUT
|
|
274
|
+
)
|
|
275
|
+
logger.info(f"[{camera_id}] Downloading {file_size_mb:.1f}MB (timeout: {timeout}s)")
|
|
276
|
+
else:
|
|
277
|
+
logger.info(f"[{camera_id}] Downloading video (size unknown, timeout: {timeout}s)")
|
|
278
|
+
|
|
279
|
+
# Download with progress tracking
|
|
280
|
+
response = requests.get(url, stream=True, timeout=timeout)
|
|
281
|
+
response.raise_for_status()
|
|
282
|
+
|
|
283
|
+
if content_length == 0:
|
|
284
|
+
content_length = int(response.headers.get('Content-Length', 0))
|
|
285
|
+
file_size_mb = content_length / (1024 * 1024) if content_length > 0 else 0
|
|
286
|
+
|
|
287
|
+
last_progress_log = 0
|
|
288
|
+
|
|
289
|
+
with open(dest_path, 'wb') as f:
|
|
290
|
+
for chunk in response.iter_content(chunk_size=self.DOWNLOAD_CHUNK_SIZE):
|
|
291
|
+
f.write(chunk)
|
|
292
|
+
bytes_downloaded += len(chunk)
|
|
293
|
+
|
|
294
|
+
# Log progress every 50MB for large files
|
|
295
|
+
if content_length > 50_000_000:
|
|
296
|
+
mb_downloaded = bytes_downloaded // (1024 * 1024)
|
|
297
|
+
if mb_downloaded - last_progress_log >= 50:
|
|
298
|
+
progress = (bytes_downloaded / content_length * 100) if content_length else 0
|
|
299
|
+
logger.info(
|
|
300
|
+
f"[{camera_id}] Download progress: "
|
|
301
|
+
f"{mb_downloaded}MB / {file_size_mb:.0f}MB ({progress:.1f}%)"
|
|
302
|
+
)
|
|
303
|
+
last_progress_log = mb_downloaded
|
|
304
|
+
|
|
305
|
+
# Update caches
|
|
306
|
+
normalized_url = self._normalize_url(url)
|
|
307
|
+
dest_path_str = str(dest_path)
|
|
308
|
+
self.downloaded_files[url] = dest_path_str
|
|
309
|
+
self._normalized_url_to_path[normalized_url] = dest_path_str
|
|
310
|
+
|
|
311
|
+
logger.info(
|
|
312
|
+
f"[{camera_id}] Downloaded: {dest_path.name} "
|
|
313
|
+
f"({bytes_downloaded / (1024*1024):.1f}MB)"
|
|
314
|
+
)
|
|
315
|
+
return dest_path_str
|
|
316
|
+
|
|
317
|
+
except requests.Timeout:
|
|
318
|
+
logger.error(
|
|
319
|
+
f"[{camera_id}] Download timeout: {file_size_mb:.1f}MB, "
|
|
320
|
+
f"got {bytes_downloaded/(1024*1024):.1f}MB in {timeout}s"
|
|
321
|
+
)
|
|
322
|
+
except requests.HTTPError as e:
|
|
323
|
+
logger.error(f"[{camera_id}] HTTP error: {e.response.status_code} - {e.response.reason}")
|
|
324
|
+
except IOError as e:
|
|
325
|
+
logger.error(f"[{camera_id}] Disk I/O error: {e}")
|
|
326
|
+
except Exception as e:
|
|
327
|
+
logger.error(f"[{camera_id}] Download failed: {type(e).__name__}: {e}")
|
|
328
|
+
|
|
329
|
+
# Cleanup partial download
|
|
330
|
+
try:
|
|
331
|
+
if dest_path.exists():
|
|
332
|
+
dest_path.unlink()
|
|
333
|
+
except Exception:
|
|
334
|
+
pass
|
|
335
|
+
|
|
336
|
+
return None
|
|
337
|
+
|
|
338
|
+
def cleanup(self):
|
|
339
|
+
"""Clean up downloaded temporary files."""
|
|
340
|
+
unique_files = set(self.downloaded_files.values())
|
|
341
|
+
unique_files.update(self._normalized_url_to_path.values())
|
|
342
|
+
|
|
343
|
+
for filepath in unique_files:
|
|
344
|
+
try:
|
|
345
|
+
if os.path.exists(filepath):
|
|
346
|
+
os.remove(filepath)
|
|
347
|
+
logger.debug(f"Removed temp file: {filepath}")
|
|
348
|
+
except Exception as e:
|
|
349
|
+
logger.warning(f"Failed to remove temp file {filepath}: {e}")
|
|
350
|
+
|
|
351
|
+
self.downloaded_files.clear()
|
|
352
|
+
self._normalized_url_to_path.clear()
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# Global video downloader instance
|
|
356
|
+
_video_downloader: Optional[VideoDownloader] = None
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def get_video_downloader() -> VideoDownloader:
|
|
360
|
+
"""Get or create the global VideoDownloader instance."""
|
|
361
|
+
global _video_downloader
|
|
362
|
+
if _video_downloader is None:
|
|
363
|
+
_video_downloader = VideoDownloader()
|
|
364
|
+
return _video_downloader
|
|
365
|
+
|
|
366
|
+
|
|
96
367
|
@dataclass
|
|
97
368
|
class StreamConfig:
|
|
98
369
|
"""Configuration for a single video stream."""
|
|
@@ -276,7 +547,12 @@ def surface_to_nv12(frame, target_h: int = 640, target_w: int = 640) -> Optional
|
|
|
276
547
|
return nv12_frame[:, :, cp.newaxis] if nv12_frame is not None else None
|
|
277
548
|
|
|
278
549
|
except Exception as e:
|
|
279
|
-
|
|
550
|
+
# Safely encode error message (some CUDA errors contain non-ASCII chars like '×')
|
|
551
|
+
try:
|
|
552
|
+
err_msg = str(e).encode('ascii', errors='replace').decode('ascii')
|
|
553
|
+
except Exception:
|
|
554
|
+
err_msg = "unknown error"
|
|
555
|
+
logger.warning(f"surface_to_nv12 failed: {err_msg}")
|
|
280
556
|
return None
|
|
281
557
|
|
|
282
558
|
|
|
@@ -320,13 +596,22 @@ class NVDECDecoderPool:
|
|
|
320
596
|
|
|
321
597
|
def assign_stream(self, stream_id: int, camera_id: str, video_path: str,
|
|
322
598
|
width: int = 640, height: int = 640) -> bool:
|
|
323
|
-
"""Assign a stream to a decoder (round-robin).
|
|
599
|
+
"""Assign a stream to a decoder (round-robin).
|
|
600
|
+
|
|
601
|
+
Automatically downloads HTTPS URLs to local files since PyNvVideoCodec's
|
|
602
|
+
bundled FFmpeg doesn't support HTTPS protocol.
|
|
603
|
+
"""
|
|
324
604
|
if self.actual_pool_size == 0:
|
|
325
605
|
return False
|
|
326
606
|
|
|
327
607
|
decoder_idx = stream_id % self.actual_pool_size
|
|
608
|
+
|
|
609
|
+
# Download HTTPS URLs to local files (PyNvVideoCodec lacks HTTPS support)
|
|
610
|
+
downloader = get_video_downloader()
|
|
611
|
+
local_path = downloader.prepare_source(video_path, camera_id)
|
|
612
|
+
|
|
328
613
|
try:
|
|
329
|
-
demuxer = nvc.CreateDemuxer(
|
|
614
|
+
demuxer = nvc.CreateDemuxer(local_path)
|
|
330
615
|
except Exception as e:
|
|
331
616
|
logger.error(f"Failed to create demuxer for {camera_id}: {e}")
|
|
332
617
|
return False
|
|
@@ -334,7 +619,7 @@ class NVDECDecoderPool:
|
|
|
334
619
|
stream_state = StreamState(
|
|
335
620
|
stream_id=stream_id,
|
|
336
621
|
camera_id=camera_id,
|
|
337
|
-
video_path=
|
|
622
|
+
video_path=local_path, # Store local path for video looping
|
|
338
623
|
demuxer=demuxer,
|
|
339
624
|
width=width,
|
|
340
625
|
height=height
|
|
@@ -447,7 +732,6 @@ def nvdec_pool_worker(
|
|
|
447
732
|
frames_since_counter_update = 0
|
|
448
733
|
counter_batch_size = 100
|
|
449
734
|
start_time = time.perf_counter()
|
|
450
|
-
last_log_time = start_time
|
|
451
735
|
camera_ids = pool.get_camera_ids_for_decoder(decoder_idx)
|
|
452
736
|
num_streams = len(camera_ids)
|
|
453
737
|
|
|
@@ -465,19 +749,12 @@ def nvdec_pool_worker(
|
|
|
465
749
|
next_frame_time = 0
|
|
466
750
|
fps_mode = ", unlimited FPS"
|
|
467
751
|
|
|
468
|
-
logger.
|
|
752
|
+
logger.debug(f"Worker {worker_id}: decoder={decoder_idx}, cams={num_streams}{fps_mode}")
|
|
469
753
|
|
|
470
754
|
while not stop_event.is_set():
|
|
471
755
|
if time.perf_counter() - start_time >= duration_sec:
|
|
472
756
|
break
|
|
473
757
|
|
|
474
|
-
now = time.perf_counter()
|
|
475
|
-
if now - last_log_time >= 5.0:
|
|
476
|
-
elapsed = now - start_time
|
|
477
|
-
fps = local_frames / elapsed if elapsed > 0 else 0
|
|
478
|
-
logger.info(f"Worker {worker_id}: {local_frames} frames, {fps:.0f} FPS")
|
|
479
|
-
last_log_time = now
|
|
480
|
-
|
|
481
758
|
# FPS limiting: wait until next scheduled frame time
|
|
482
759
|
if fps_limit_enabled:
|
|
483
760
|
current_time = time.perf_counter()
|
|
@@ -672,10 +949,52 @@ def nvdec_pool_process(
|
|
|
672
949
|
t.start()
|
|
673
950
|
threads.append(t)
|
|
674
951
|
|
|
952
|
+
# Progress monitoring loop with current/avg FPS tracking
|
|
675
953
|
start_time = time.perf_counter()
|
|
954
|
+
last_report_time = start_time
|
|
955
|
+
last_frame_count = 0
|
|
956
|
+
report_interval = 5.0
|
|
957
|
+
processing_start_time = None
|
|
958
|
+
frames_at_processing_start = 0
|
|
959
|
+
|
|
676
960
|
while not stop_event.is_set():
|
|
677
|
-
|
|
961
|
+
current_time = time.perf_counter()
|
|
962
|
+
if current_time - start_time >= duration_sec:
|
|
678
963
|
break
|
|
964
|
+
|
|
965
|
+
# Periodic progress report with current and average FPS
|
|
966
|
+
if current_time - last_report_time >= report_interval:
|
|
967
|
+
elapsed = current_time - start_time
|
|
968
|
+
|
|
969
|
+
if shared_frame_count is not None:
|
|
970
|
+
current_frames = shared_frame_count.value
|
|
971
|
+
interval_frames = current_frames - last_frame_count
|
|
972
|
+
interval_fps = interval_frames / report_interval
|
|
973
|
+
per_stream_fps = interval_fps / len(camera_configs) if camera_configs else 0
|
|
974
|
+
|
|
975
|
+
# Track when processing actually starts (exclude warmup)
|
|
976
|
+
if processing_start_time is None and current_frames > 0:
|
|
977
|
+
processing_start_time = last_report_time
|
|
978
|
+
frames_at_processing_start = last_frame_count
|
|
979
|
+
|
|
980
|
+
# Calculate average FPS excluding warmup
|
|
981
|
+
if processing_start_time is not None:
|
|
982
|
+
processing_elapsed = current_time - processing_start_time
|
|
983
|
+
processing_frames = current_frames - frames_at_processing_start
|
|
984
|
+
avg_fps = processing_frames / processing_elapsed if processing_elapsed > 0 else 0
|
|
985
|
+
avg_per_stream = avg_fps / len(camera_configs) if camera_configs else 0
|
|
986
|
+
remaining = max(0, duration_sec - elapsed)
|
|
987
|
+
|
|
988
|
+
logger.info(
|
|
989
|
+
f"GPU{gpu_id} [{elapsed:5.1f}s] {current_frames:,} frames | "
|
|
990
|
+
f"cur: {interval_fps:,.0f} FPS ({per_stream_fps:.1f}/stream) | "
|
|
991
|
+
f"avg: {avg_fps:,.0f} FPS ({avg_per_stream:.1f}/stream) | "
|
|
992
|
+
f"{remaining:.0f}s left"
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
last_frame_count = current_frames
|
|
996
|
+
last_report_time = current_time
|
|
997
|
+
|
|
679
998
|
time.sleep(0.1)
|
|
680
999
|
|
|
681
1000
|
thread_stop_event.set()
|
|
File without changes
|
|
File without changes
|
{matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/matrice_streaming.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/matrice_streaming.egg-info/not-zip-safe
RENAMED
|
File without changes
|
{matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/matrice_streaming.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/client/__init__.py
RENAMED
|
File without changes
|
{matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/client/client.py
RENAMED
|
File without changes
|
{matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/client/client_utils.py
RENAMED
|
File without changes
|
{matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/deployment/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/deployment/deployment.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{matrice_streaming-0.1.62 → matrice_streaming-0.1.64}/src/matrice_streaming/deployment/todo.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|