plex-generate-previews 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,541 @@
1
+ """
2
+ GPU detection for video processing acceleration.
3
+
4
+ Detects available GPU hardware and returns appropriate configuration
5
+ for FFmpeg hardware acceleration. Supports NVIDIA, AMD, Intel, and WSL2 GPUs.
6
+ """
7
+
8
+ import os
9
+ import subprocess
10
+ import platform
11
+ import re
12
+ from typing import Tuple, Optional, List
13
+ from loguru import logger
14
+
15
+ # Minimum required FFmpeg version
16
+ MIN_FFMPEG_VERSION = (7, 0, 0) # FFmpeg 7.0.0+ for better hardware acceleration support
17
+
18
+
19
+ def _get_ffmpeg_version() -> Optional[Tuple[int, int, int]]:
20
+ """
21
+ Get FFmpeg version as a tuple of integers.
22
+
23
+ Returns:
24
+ Optional[Tuple[int, int, int]]: Version tuple (major, minor, patch) or None if failed
25
+ """
26
+ try:
27
+ result = subprocess.run(['ffmpeg', '-version'], capture_output=True, text=True, timeout=5)
28
+ if result.returncode != 0:
29
+ logger.debug(f"Failed to get FFmpeg version: {result.stderr}")
30
+ return None
31
+
32
+ # Extract version from first line: "ffmpeg version 7.1.1-1ubuntu1.2 Copyright..."
33
+ version_line = result.stdout.split('\n')[0]
34
+ version_match = re.search(r'ffmpeg version (\d+)\.(\d+)\.(\d+)', version_line)
35
+
36
+ if version_match:
37
+ major, minor, patch = map(int, version_match.groups())
38
+ logger.debug(f"FFmpeg version: {major}.{minor}.{patch}")
39
+ return (major, minor, patch)
40
+ else:
41
+ logger.debug(f"Could not parse FFmpeg version from: {version_line}")
42
+ return None
43
+
44
+ except Exception as e:
45
+ logger.debug(f"Error getting FFmpeg version: {e}")
46
+ return None
47
+
48
+
49
+ def _check_ffmpeg_version() -> bool:
50
+ """
51
+ Check if FFmpeg version meets minimum requirements.
52
+
53
+ Returns:
54
+ bool: True if version is sufficient, False otherwise
55
+ """
56
+ version = _get_ffmpeg_version()
57
+ if version is None:
58
+ logger.warning("Could not determine FFmpeg version - proceeding with caution")
59
+ return True # Don't fail if we can't determine version
60
+
61
+ if version >= MIN_FFMPEG_VERSION:
62
+ logger.debug(f"✓ FFmpeg version {version[0]}.{version[1]}.{version[2]} meets minimum requirement {MIN_FFMPEG_VERSION[0]}.{MIN_FFMPEG_VERSION[1]}.{MIN_FFMPEG_VERSION[2]}")
63
+ return True
64
+ else:
65
+ logger.warning(f"⚠ FFmpeg version {version[0]}.{version[1]}.{version[2]} is below minimum requirement {MIN_FFMPEG_VERSION[0]}.{MIN_FFMPEG_VERSION[1]}.{MIN_FFMPEG_VERSION[2]}")
66
+ logger.warning("Hardware acceleration may not work properly. Please upgrade FFmpeg.")
67
+ return False
68
+
69
+
70
+ def _get_ffmpeg_hwaccels() -> List[str]:
71
+ """
72
+ Get list of available FFmpeg hardware accelerators.
73
+
74
+ Returns:
75
+ List[str]: Available hardware accelerators
76
+ """
77
+ try:
78
+ result = subprocess.run(['ffmpeg', '-hwaccels'], capture_output=True, text=True, timeout=5)
79
+ if result.returncode != 0:
80
+ logger.debug(f"Failed to get FFmpeg hardware accelerators: {result.stderr}")
81
+ return []
82
+
83
+ hwaccels = []
84
+ for line in result.stdout.split('\n'):
85
+ line = line.strip()
86
+ if line and not line.startswith('Hardware acceleration methods:'):
87
+ hwaccels.append(line)
88
+
89
+ return hwaccels
90
+ except Exception as e:
91
+ logger.debug(f"Error getting FFmpeg hardware accelerators: {e}")
92
+ return []
93
+
94
+
95
+ def _is_hwaccel_available(hwaccel: str) -> bool:
96
+ """
97
+ Check if a specific hardware acceleration is available.
98
+
99
+ Args:
100
+ hwaccel: Hardware acceleration type to check
101
+
102
+ Returns:
103
+ bool: True if available, False otherwise
104
+ """
105
+ available_hwaccels = _get_ffmpeg_hwaccels()
106
+ is_available = hwaccel in available_hwaccels
107
+
108
+ if is_available:
109
+ logger.debug(f"✓ {hwaccel} hardware acceleration is available")
110
+ else:
111
+ logger.debug(f"✗ {hwaccel} hardware acceleration is not available")
112
+
113
+ return is_available
114
+
115
+
116
+ def _test_hwaccel_functionality(hwaccel: str, device_path: Optional[str] = None) -> bool:
117
+ """
118
+ Test if hardware acceleration actually works by running a simple FFmpeg command.
119
+
120
+ Args:
121
+ hwaccel: Hardware acceleration type to test
122
+ device_path: Optional device path for VAAPI
123
+
124
+ Returns:
125
+ bool: True if hardware acceleration works, False otherwise
126
+ """
127
+ try:
128
+ # Build FFmpeg command based on acceleration type
129
+ if hwaccel == 'cuda':
130
+ cmd = ['ffmpeg', '-f', 'lavfi', '-i', 'testsrc=duration=0.1:size=320x240:rate=1',
131
+ '-c:v', 'h264_nvenc', '-t', '0.1', '-f', 'null', '/dev/null']
132
+ elif hwaccel == 'vaapi' and device_path:
133
+ # For VAAPI, test hardware acceleration initialization rather than encoding
134
+ # since encoding often fails due to driver issues even when hwaccel works
135
+ cmd = ['ffmpeg', '-hwaccel', 'vaapi', '-vaapi_device', device_path,
136
+ '-f', 'lavfi', '-i', 'testsrc=duration=0.1:size=320x240:rate=1',
137
+ '-t', '0.1', '-f', 'null', '/dev/null']
138
+ elif hwaccel == 'qsv':
139
+ cmd = ['ffmpeg', '-f', 'lavfi', '-i', 'testsrc=duration=0.1:size=320x240:rate=1',
140
+ '-c:v', 'h264_qsv', '-t', '0.1', '-f', 'null', '/dev/null']
141
+ elif hwaccel == 'd3d11va':
142
+ cmd = ['ffmpeg', '-f', 'lavfi', '-i', 'testsrc=duration=0.1:size=320x240:rate=1',
143
+ '-c:v', 'h264_nvenc', '-t', '0.1', '-f', 'null', '/dev/null'] # WSL2 can use NVENC
144
+ else:
145
+ # For other types, just test basic hardware acceleration
146
+ cmd = ['ffmpeg', '-hwaccel', hwaccel, '-f', 'lavfi', '-i', 'testsrc=duration=0.1:size=320x240:rate=1',
147
+ '-t', '0.1', '-f', 'null', '/dev/null']
148
+
149
+ logger.debug(f"Testing {hwaccel} functionality: {' '.join(cmd)}")
150
+ result = subprocess.run(cmd, capture_output=True, timeout=10)
151
+
152
+ # FFmpeg returns 0 for success, 141 for SIGPIPE (which is OK for our test)
153
+ if result.returncode in [0, 141]:
154
+ logger.debug(f"✓ {hwaccel} functionality test passed")
155
+ return True
156
+ else:
157
+ logger.debug(f"✗ {hwaccel} functionality test failed (exit code: {result.returncode})")
158
+ if result.stderr:
159
+ stderr_lines = result.stderr.decode('utf-8', 'ignore').split('\n')[-3:]
160
+ logger.debug(f"Error output: {' '.join(stderr_lines)}")
161
+ return False
162
+
163
+ except subprocess.TimeoutExpired:
164
+ logger.debug(f"✗ {hwaccel} functionality test timed out")
165
+ return False
166
+ except Exception as e:
167
+ logger.debug(f"✗ {hwaccel} functionality test failed with exception: {e}")
168
+ return False
169
+
170
+
171
+ def _get_gpu_devices() -> List[Tuple[str, str, str]]:
172
+ """
173
+ Get all GPU devices with their render devices and driver information.
174
+
175
+ Returns:
176
+ List[Tuple[str, str, str]]: List of (card_name, render_device, driver) tuples
177
+ """
178
+ devices = []
179
+ drm_dir = "/sys/class/drm"
180
+
181
+ if not os.path.exists(drm_dir):
182
+ logger.debug(f"DRM directory {drm_dir} does not exist")
183
+ return devices
184
+
185
+ try:
186
+ entries = os.listdir(drm_dir)
187
+ logger.debug(f"Scanning DRM devices: {entries}")
188
+
189
+ for entry in entries:
190
+ if not entry.startswith("card") or "-" in entry:
191
+ continue # Skip card1-HDMI-A-1, card0-DP-2, etc.
192
+
193
+ # Extract card number
194
+ try:
195
+ card_num = int(entry[4:]) # card0 -> 0, card1 -> 1
196
+ except ValueError:
197
+ continue
198
+
199
+ # Get render device for this card
200
+ # The mapping is: card0 -> renderD129, card1 -> renderD128
201
+ render_device = None
202
+ for render_entry in entries:
203
+ if render_entry == f"renderD{129 - card_num}": # card0 -> renderD129, card1 -> renderD128
204
+ render_device = f"/dev/dri/{render_entry}"
205
+ break
206
+
207
+ if not render_device:
208
+ logger.debug(f"No render device found for {entry}")
209
+ continue
210
+
211
+ # Get driver information
212
+ driver_path = os.path.join(drm_dir, entry, "device", "driver")
213
+ driver = "unknown"
214
+ if os.path.islink(driver_path):
215
+ driver = os.path.basename(os.readlink(driver_path))
216
+
217
+ devices.append((entry, render_device, driver))
218
+ logger.debug(f"Found GPU: {entry} -> {render_device} (driver: {driver})")
219
+
220
+ except Exception as e:
221
+ logger.debug(f"Error scanning GPU devices: {e}")
222
+
223
+ return devices
224
+
225
+
226
+ def _determine_vaapi_gpu_type(device_path: str) -> str:
227
+ """
228
+ Determine GPU type for VAAPI device by checking driver information.
229
+
230
+ Args:
231
+ device_path: Path to VAAPI device
232
+
233
+ Returns:
234
+ str: GPU type ('AMD', 'INTEL', 'NVIDIA', 'ARM', 'VIDEOCORE', or 'UNKNOWN')
235
+ """
236
+ logger.debug(f"Determining GPU type for VAAPI device: {device_path}")
237
+
238
+ try:
239
+ drm_dir = "/sys/class/drm"
240
+ if not os.path.exists(drm_dir):
241
+ logger.debug(f"DRM directory {drm_dir} does not exist")
242
+ return 'UNKNOWN'
243
+
244
+ entries = os.listdir(drm_dir)
245
+ logger.debug(f"Found DRM entries: {entries}")
246
+
247
+ for entry in entries:
248
+ if not entry.startswith("card"):
249
+ continue
250
+
251
+ driver_path = os.path.join(drm_dir, entry, "device", "driver")
252
+ if os.path.islink(driver_path):
253
+ driver_name = os.path.basename(os.readlink(driver_path))
254
+ logger.debug(f"Driver for {entry}: {driver_name}")
255
+
256
+ # Intel drivers
257
+ if driver_name == "i915":
258
+ logger.debug("Detected Intel i915 driver - GPU type: INTEL")
259
+ return 'INTEL'
260
+
261
+ # AMD drivers
262
+ elif driver_name in ("amdgpu", "radeon"):
263
+ logger.debug(f"Detected AMD driver {driver_name} - GPU type: AMD")
264
+ return 'AMD'
265
+
266
+ # ARM Mali drivers
267
+ elif driver_name == "panfrost":
268
+ logger.debug("Detected ARM Mali panfrost driver - GPU type: ARM")
269
+ return 'ARM'
270
+
271
+ # VideoCore (Raspberry Pi)
272
+ elif driver_name == "vc4":
273
+ logger.debug("Detected VideoCore vc4 driver - GPU type: VIDEOCORE")
274
+ return 'VIDEOCORE'
275
+
276
+ # Other drivers - try to detect from lspci
277
+ else:
278
+ logger.debug(f"Unknown driver {driver_name}, attempting lspci detection")
279
+ gpu_type = _detect_gpu_type_from_lspci()
280
+ if gpu_type != 'UNKNOWN':
281
+ return gpu_type
282
+
283
+ logger.debug("No suitable driver found, defaulting to UNKNOWN")
284
+ return 'UNKNOWN'
285
+ except Exception as e:
286
+ logger.debug(f"Error determining VAAPI GPU type: {e}")
287
+ return 'UNKNOWN'
288
+
289
+
290
+ def _detect_gpu_type_from_lspci() -> str:
291
+ """
292
+ Detect GPU type using lspci as fallback when driver detection fails.
293
+
294
+ Returns:
295
+ str: GPU type ('AMD', 'INTEL', 'NVIDIA', 'ARM', or 'UNKNOWN')
296
+ """
297
+ try:
298
+ result = subprocess.run(['lspci'], capture_output=True, text=True, timeout=5)
299
+ if result.returncode != 0:
300
+ logger.debug("lspci command failed")
301
+ return 'UNKNOWN'
302
+
303
+ for line in result.stdout.split('\n'):
304
+ if 'VGA' in line or 'Display' in line:
305
+ line_lower = line.lower()
306
+ if 'amd' in line_lower or 'radeon' in line_lower:
307
+ logger.debug("lspci detected AMD GPU")
308
+ return 'AMD'
309
+ elif 'intel' in line_lower:
310
+ logger.debug("lspci detected Intel GPU")
311
+ return 'INTEL'
312
+ elif 'nvidia' in line_lower or 'geforce' in line_lower:
313
+ logger.debug("lspci detected NVIDIA GPU")
314
+ return 'NVIDIA'
315
+ elif 'mali' in line_lower or 'arm' in line_lower:
316
+ logger.debug("lspci detected ARM GPU")
317
+ return 'ARM'
318
+
319
+ logger.debug("lspci did not identify GPU type")
320
+ return 'UNKNOWN'
321
+ except Exception as e:
322
+ logger.debug(f"Error running lspci: {e}")
323
+ return 'UNKNOWN'
324
+
325
+
326
+ def _log_system_info() -> None:
327
+ """Log system information for debugging GPU detection issues."""
328
+ logger.debug("=== System Information ===")
329
+ logger.debug(f"Platform: {platform.platform()}")
330
+ logger.debug(f"Python version: {platform.python_version()}")
331
+ logger.debug(f"FFmpeg path: {os.environ.get('FFMPEG_PATH', 'ffmpeg')}")
332
+
333
+ # Check FFmpeg version
334
+ _check_ffmpeg_version()
335
+
336
+ # Log available hardware accelerators
337
+ hwaccels = _get_ffmpeg_hwaccels()
338
+ if hwaccels:
339
+ logger.debug(f"Available FFmpeg hardware accelerators: {hwaccels}")
340
+
341
+ # Log GPU device mapping
342
+ gpu_devices = _get_gpu_devices()
343
+ if gpu_devices:
344
+ logger.debug("GPU device mapping:")
345
+ for card_name, render_device, driver in gpu_devices:
346
+ logger.debug(f" {card_name} -> {render_device} (driver: {driver})")
347
+ else:
348
+ logger.debug("No GPU devices found")
349
+
350
+ logger.debug("=== End System Information ===")
351
+
352
+
353
+ def _parse_lspci_gpu_name(gpu_type: str) -> str:
354
+ """
355
+ Parse GPU name from lspci output.
356
+
357
+ Args:
358
+ gpu_type: Type of GPU ('AMD', 'INTEL')
359
+
360
+ Returns:
361
+ str: GPU name or fallback description
362
+ """
363
+ try:
364
+ result = subprocess.run(['lspci'], capture_output=True, text=True, timeout=5)
365
+ if result.returncode == 0:
366
+ for line in result.stdout.split('\n'):
367
+ if 'VGA' in line and (gpu_type == 'AMD' and 'AMD' in line or gpu_type == 'INTEL' and 'Intel' in line):
368
+ parts = line.split(':')
369
+ if len(parts) > 2:
370
+ return parts[2].strip()
371
+ except Exception as e:
372
+ logger.debug(f"Error parsing lspci for {gpu_type}: {e}")
373
+
374
+ return f"{gpu_type} GPU"
375
+
376
+
377
+ def get_gpu_name(gpu_type: str, gpu_device: str) -> str:
378
+ """
379
+ Extract GPU model name from system.
380
+
381
+ Args:
382
+ gpu_type: Type of GPU ('NVIDIA', 'AMD', 'INTEL', 'WSL2')
383
+ gpu_device: GPU device path or info string
384
+
385
+ Returns:
386
+ str: GPU model name or fallback description
387
+ """
388
+ try:
389
+ if gpu_type == 'NVIDIA':
390
+ # Use nvidia-smi to get GPU name
391
+ result = subprocess.run(['nvidia-smi', '--query-gpu=name', '--format=csv,noheader,nounits'],
392
+ capture_output=True, text=True, timeout=5)
393
+ if result.returncode == 0:
394
+ gpu_names = [line.strip() for line in result.stdout.strip().split('\n') if line.strip()]
395
+ if gpu_names:
396
+ return gpu_names[0] # Return first GPU name
397
+ return "NVIDIA GPU (CUDA)"
398
+
399
+ elif gpu_type == 'WSL2':
400
+ return "WSL2 GPU (D3D11VA)"
401
+
402
+ elif gpu_type == 'INTEL' and gpu_device == 'qsv':
403
+ # Try to get Intel GPU info
404
+ gpu_name = _parse_lspci_gpu_name('INTEL')
405
+ return f"{gpu_name} (QSV)"
406
+
407
+ elif gpu_type in ('AMD', 'INTEL') and gpu_device.startswith('/dev/dri/'):
408
+ # Try to get GPU info from lspci
409
+ gpu_name = _parse_lspci_gpu_name(gpu_type)
410
+ return f"{gpu_name} (VAAPI)"
411
+
412
+ except Exception as e:
413
+ logger.debug(f"Error getting GPU name for {gpu_type}: {e}")
414
+
415
+ # Fallback
416
+ return f"{gpu_type} GPU"
417
+
418
+
419
+ def format_gpu_info(gpu_type: str, gpu_device: str, gpu_name: str) -> str:
420
+ """
421
+ Format GPU information for display.
422
+
423
+ Args:
424
+ gpu_type: Type of GPU
425
+ gpu_device: GPU device path or info
426
+ gpu_name: GPU model name
427
+
428
+ Returns:
429
+ str: Formatted GPU description
430
+ """
431
+ if gpu_type == 'NVIDIA':
432
+ return f"{gpu_name} (CUDA)"
433
+ elif gpu_type == 'WSL2':
434
+ return f"{gpu_name} (D3D11VA)"
435
+ elif gpu_type == 'INTEL' and gpu_device == 'qsv':
436
+ return f"{gpu_name} (QSV)"
437
+ elif gpu_type in ('AMD', 'INTEL', 'ARM', 'VIDEOCORE') and gpu_device.startswith('/dev/dri/'):
438
+ return f"{gpu_name} (VAAPI - {gpu_device})"
439
+ elif gpu_type == 'UNKNOWN':
440
+ return f"{gpu_name} (Unknown GPU)"
441
+ else:
442
+ return f"{gpu_name} ({gpu_type})"
443
+
444
+
445
+ def detect_all_gpus() -> List[Tuple[str, str, dict]]:
446
+ """
447
+ Detect all available GPU hardware using FFmpeg capability detection.
448
+
449
+ Checks FFmpeg's available hardware acceleration capabilities and returns
450
+ all working GPUs instead of just the first one.
451
+
452
+ Returns:
453
+ List[Tuple[str, str, dict]]: List of (gpu_type, gpu_device, gpu_info_dict)
454
+ - gpu_type: 'NVIDIA', 'AMD', 'INTEL', 'WSL2'
455
+ - gpu_device: Device path or info string
456
+ - gpu_info_dict: Dictionary with GPU details (name, vram, etc.)
457
+ """
458
+ logger.debug("=== Starting Multi-GPU Detection ===")
459
+ _log_system_info()
460
+ logger.debug("Checking FFmpeg hardware acceleration capabilities for all GPUs")
461
+
462
+ detected_gpus = []
463
+
464
+ # Check NVIDIA CUDA (can have multiple GPUs)
465
+ logger.debug("1. Checking NVIDIA CUDA acceleration...")
466
+ if _is_hwaccel_available('cuda') and _test_hwaccel_functionality('cuda'):
467
+ logger.debug("✓ NVIDIA CUDA hardware acceleration is available and working")
468
+ gpu_name = get_gpu_name('NVIDIA', 'cuda')
469
+ gpu_info = {
470
+ 'name': gpu_name,
471
+ 'acceleration': 'CUDA',
472
+ 'device_path': 'cuda'
473
+ }
474
+ detected_gpus.append(('NVIDIA', 'cuda', gpu_info))
475
+
476
+ # Check WSL2 D3D11VA (usually single GPU)
477
+ logger.debug("2. Checking WSL2 D3D11VA acceleration...")
478
+ if _is_hwaccel_available('d3d11va') and _test_hwaccel_functionality('d3d11va'):
479
+ logger.debug("✓ WSL2 D3D11VA hardware acceleration is available and working")
480
+ gpu_name = get_gpu_name('WSL2', 'd3d11va')
481
+ gpu_info = {
482
+ 'name': gpu_name,
483
+ 'acceleration': 'D3D11VA',
484
+ 'device_path': 'd3d11va'
485
+ }
486
+ detected_gpus.append(('WSL2', 'd3d11va', gpu_info))
487
+
488
+ # Check Intel QSV (usually single GPU)
489
+ logger.debug("3. Checking Intel QSV acceleration...")
490
+ if _is_hwaccel_available('qsv') and _test_hwaccel_functionality('qsv'):
491
+ logger.debug("✓ Intel QSV hardware acceleration is available and working")
492
+ gpu_name = get_gpu_name('INTEL', 'qsv')
493
+ gpu_info = {
494
+ 'name': gpu_name,
495
+ 'acceleration': 'QSV',
496
+ 'device_path': 'qsv'
497
+ }
498
+ detected_gpus.append(('INTEL', 'qsv', gpu_info))
499
+
500
+ # Check VAAPI (can have multiple devices)
501
+ logger.debug("4. Checking VAAPI acceleration...")
502
+ if _is_hwaccel_available('vaapi'):
503
+ logger.debug("VAAPI acceleration is available, searching for devices...")
504
+ vaapi_devices = _find_all_vaapi_devices()
505
+ for device_path in vaapi_devices:
506
+ if _test_hwaccel_functionality('vaapi', device_path):
507
+ gpu_type = _determine_vaapi_gpu_type(device_path)
508
+ gpu_name = get_gpu_name(gpu_type, device_path)
509
+ gpu_info = {
510
+ 'name': gpu_name,
511
+ 'acceleration': 'VAAPI',
512
+ 'device_path': device_path
513
+ }
514
+ detected_gpus.append((gpu_type, device_path, gpu_info))
515
+ logger.debug(f"✓ {gpu_type} VAAPI hardware acceleration is available and working with device {device_path}")
516
+
517
+ logger.debug(f"=== Multi-GPU Detection Complete: Found {len(detected_gpus)} GPUs ===")
518
+ return detected_gpus
519
+
520
+
521
+ def _find_all_vaapi_devices() -> List[str]:
522
+ """
523
+ Find all available VAAPI devices.
524
+
525
+ Returns:
526
+ List[str]: List of VAAPI device paths
527
+ """
528
+ devices = []
529
+ gpu_devices = _get_gpu_devices()
530
+
531
+ if not gpu_devices:
532
+ logger.debug("No GPU devices found for VAAPI")
533
+ return devices
534
+
535
+ # Add all GPU devices as potential VAAPI devices
536
+ for card_name, render_device, driver in gpu_devices:
537
+ devices.append(render_device)
538
+ logger.debug(f"Found potential VAAPI device: {render_device} (card: {card_name}, driver: {driver})")
539
+
540
+ return devices
541
+