matrice-streaming 0.1.62__tar.gz → 0.1.63__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.63}/PKG-INFO +1 -1
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/matrice_streaming.egg-info/PKG-INFO +1 -1
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/camera_streamer/nvdec.py +283 -3
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/LICENSE.txt +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/README.md +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/matrice_streaming.egg-info/SOURCES.txt +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/matrice_streaming.egg-info/dependency_links.txt +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/matrice_streaming.egg-info/not-zip-safe +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/matrice_streaming.egg-info/top_level.txt +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/pyproject.toml +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/setup.cfg +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/setup.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/__init__.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/client/__init__.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/client/client.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/client/client_utils.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/deployment/__init__.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/deployment/camera_manager.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/deployment/deployment.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/deployment/inference_pipeline.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/deployment/streaming_gateway_manager.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/deployment/todo.txt +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/py.typed +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/__init__.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/camera_streamer/ARCHITECTURE.md +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/camera_streamer/__init__.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/camera_streamer/async_camera_worker.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/camera_streamer/async_ffmpeg_worker.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/camera_streamer/camera_streamer.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/camera_streamer/device_detection.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/camera_streamer/encoder_manager.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/camera_streamer/encoding_pool_manager.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_camera_streamer.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_config.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_worker_manager.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/camera_streamer/frame_processor.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/camera_streamer/gstreamer_camera_streamer.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/camera_streamer/gstreamer_worker.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/camera_streamer/gstreamer_worker_manager.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/camera_streamer/message_builder.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/camera_streamer/nvdec_worker_manager.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/camera_streamer/platform_pipelines.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/camera_streamer/retry_manager.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/camera_streamer/stream_statistics.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/camera_streamer/video_capture_manager.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/camera_streamer/worker_manager.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/debug/README.md +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/debug/__init__.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/debug/benchmark.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/debug/debug_gstreamer_gateway.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/debug/debug_stream_backend.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/debug/debug_streaming_gateway.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/debug/debug_utils.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/debug/example_debug_streaming.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/debug/test_videoplayback.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/dynamic_camera_manager.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/event_listener.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/metrics_reporter.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/streaming_action.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/streaming_gateway.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/streaming_gateway_utils.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/streaming_gateway/streaming_status_listener.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/tests/test_async_infrastructure.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/tests/test_batch_auto_calculation.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/tests/test_batching_verification.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/tests/test_e2e_production.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/tests/test_flatten_binary.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/tests/test_gstreamer_integration.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/tests/test_msgpack_fix.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/tests/test_phase1_unit.py +0 -0
- {matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/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."""
|
|
@@ -320,13 +591,22 @@ class NVDECDecoderPool:
|
|
|
320
591
|
|
|
321
592
|
def assign_stream(self, stream_id: int, camera_id: str, video_path: str,
|
|
322
593
|
width: int = 640, height: int = 640) -> bool:
|
|
323
|
-
"""Assign a stream to a decoder (round-robin).
|
|
594
|
+
"""Assign a stream to a decoder (round-robin).
|
|
595
|
+
|
|
596
|
+
Automatically downloads HTTPS URLs to local files since PyNvVideoCodec's
|
|
597
|
+
bundled FFmpeg doesn't support HTTPS protocol.
|
|
598
|
+
"""
|
|
324
599
|
if self.actual_pool_size == 0:
|
|
325
600
|
return False
|
|
326
601
|
|
|
327
602
|
decoder_idx = stream_id % self.actual_pool_size
|
|
603
|
+
|
|
604
|
+
# Download HTTPS URLs to local files (PyNvVideoCodec lacks HTTPS support)
|
|
605
|
+
downloader = get_video_downloader()
|
|
606
|
+
local_path = downloader.prepare_source(video_path, camera_id)
|
|
607
|
+
|
|
328
608
|
try:
|
|
329
|
-
demuxer = nvc.CreateDemuxer(
|
|
609
|
+
demuxer = nvc.CreateDemuxer(local_path)
|
|
330
610
|
except Exception as e:
|
|
331
611
|
logger.error(f"Failed to create demuxer for {camera_id}: {e}")
|
|
332
612
|
return False
|
|
@@ -334,7 +614,7 @@ class NVDECDecoderPool:
|
|
|
334
614
|
stream_state = StreamState(
|
|
335
615
|
stream_id=stream_id,
|
|
336
616
|
camera_id=camera_id,
|
|
337
|
-
video_path=
|
|
617
|
+
video_path=local_path, # Store local path for video looping
|
|
338
618
|
demuxer=demuxer,
|
|
339
619
|
width=width,
|
|
340
620
|
height=height
|
|
File without changes
|
|
File without changes
|
{matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/matrice_streaming.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/matrice_streaming.egg-info/not-zip-safe
RENAMED
|
File without changes
|
{matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/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.63}/src/matrice_streaming/client/__init__.py
RENAMED
|
File without changes
|
{matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/client/client.py
RENAMED
|
File without changes
|
{matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/client/client_utils.py
RENAMED
|
File without changes
|
{matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/src/matrice_streaming/deployment/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{matrice_streaming-0.1.62 → matrice_streaming-0.1.63}/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.63}/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
|