HwCodecDetect 0.2.3__tar.gz → 0.2.4__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.
Files changed (24) hide show
  1. {hwcodecdetect-0.2.3/src/HwCodecDetect.egg-info → hwcodecdetect-0.2.4}/PKG-INFO +1 -1
  2. hwcodecdetect-0.2.4/VERSION +1 -0
  3. {hwcodecdetect-0.2.3 → hwcodecdetect-0.2.4}/src/HwCodecDetect/bitdepth_chroma_detect.py +41 -11
  4. {hwcodecdetect-0.2.3 → hwcodecdetect-0.2.4}/src/HwCodecDetect/run_tests.py +42 -12
  5. hwcodecdetect-0.2.4/src/HwCodecDetect/utils.py +166 -0
  6. hwcodecdetect-0.2.4/src/HwCodecDetect/version.py +1 -0
  7. {hwcodecdetect-0.2.3 → hwcodecdetect-0.2.4/src/HwCodecDetect.egg-info}/PKG-INFO +1 -1
  8. {hwcodecdetect-0.2.3 → hwcodecdetect-0.2.4}/src/HwCodecDetect.egg-info/SOURCES.txt +1 -0
  9. hwcodecdetect-0.2.3/VERSION +0 -1
  10. hwcodecdetect-0.2.3/src/HwCodecDetect/version.py +0 -1
  11. {hwcodecdetect-0.2.3 → hwcodecdetect-0.2.4}/LICENSE +0 -0
  12. {hwcodecdetect-0.2.3 → hwcodecdetect-0.2.4}/MANIFEST.in +0 -0
  13. {hwcodecdetect-0.2.3 → hwcodecdetect-0.2.4}/README.md +0 -0
  14. {hwcodecdetect-0.2.3 → hwcodecdetect-0.2.4}/pyproject.toml +0 -0
  15. {hwcodecdetect-0.2.3 → hwcodecdetect-0.2.4}/requirements.txt +0 -0
  16. {hwcodecdetect-0.2.3 → hwcodecdetect-0.2.4}/setup.cfg +0 -0
  17. {hwcodecdetect-0.2.3 → hwcodecdetect-0.2.4}/setup.py +0 -0
  18. {hwcodecdetect-0.2.3 → hwcodecdetect-0.2.4}/src/HwCodecDetect/__init__.py +0 -0
  19. {hwcodecdetect-0.2.3 → hwcodecdetect-0.2.4}/src/HwCodecDetect/install_ffmpeg_if_needed.py +0 -0
  20. {hwcodecdetect-0.2.3 → hwcodecdetect-0.2.4}/src/HwCodecDetect.egg-info/dependency_links.txt +0 -0
  21. {hwcodecdetect-0.2.3 → hwcodecdetect-0.2.4}/src/HwCodecDetect.egg-info/entry_points.txt +0 -0
  22. {hwcodecdetect-0.2.3 → hwcodecdetect-0.2.4}/src/HwCodecDetect.egg-info/requires.txt +0 -0
  23. {hwcodecdetect-0.2.3 → hwcodecdetect-0.2.4}/src/HwCodecDetect.egg-info/top_level.txt +0 -0
  24. {hwcodecdetect-0.2.3 → hwcodecdetect-0.2.4}/src/HwCodecDetect.egg-info/zip-safe +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: HwCodecDetect
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: A cross-platform tool to automatically detect and test hardware video decoders/encoders using FFmpeg.
5
5
  Home-page: https://github.com/whyb/HwCodecDetect
6
6
  Author: whyb
@@ -0,0 +1 @@
1
+ 0.2.4
@@ -12,6 +12,7 @@ from collections import defaultdict
12
12
  from colorama import init, Fore, Style
13
13
  from concurrent.futures import ThreadPoolExecutor, as_completed
14
14
  from tqdm import tqdm
15
+ from .utils import check_codec_support
15
16
 
16
17
  init(autoreset=True)
17
18
 
@@ -152,13 +153,19 @@ def _run_ffmpeg_command(command, verbose):
152
153
 
153
154
  def _run_encoder_bitdepth_test(test_data):
154
155
  """Tests encoder support for a specific pixel format."""
155
- codec, encoder, pix_fmt, bit_depth, chroma, test_dir, verbose = test_data
156
+ codec, encoder, pix_fmt, bit_depth, chroma, test_dir, verbose, unsupported_encoders = test_data
157
+
158
+ # Skip unsupported encoders
159
+ if encoder in unsupported_encoders:
160
+ title = ENCODER_TITLES.get((encoder, codec), f"{encoder.upper()} Encoder:")
161
+ return title, pix_fmt, bit_depth, chroma, "skipped"
162
+
156
163
  if codec == "prores":
157
164
  file_ext = ".mov"
158
165
  else:
159
166
  file_ext = ".webm" if codec in ["vp8", "vp9"] else ".mp4"
160
167
  output_file = os.path.join(test_dir, f"{encoder}_{pix_fmt}{file_ext}")
161
-
168
+
162
169
  # Determine pixel format for output based on input format
163
170
  if bit_depth == 8:
164
171
  if chroma == "4:2:0":
@@ -270,17 +277,20 @@ def _run_encoder_bitdepth_test(test_data):
270
277
  return title, pix_fmt, bit_depth, chroma, status
271
278
 
272
279
 
273
- def _run_encoder_bitdepth_tests(test_dir, max_workers, verbose):
280
+ def _run_encoder_bitdepth_tests(test_dir, max_workers, verbose, unsupported_encoders=None):
274
281
  """Tests encoder support for various pixel formats."""
275
282
  results = defaultdict(dict)
276
283
 
284
+ if unsupported_encoders is None:
285
+ unsupported_encoders = set()
286
+
277
287
  print("\n--- Running Bit-depth/Chroma Encoder Tests ---")
278
288
 
279
289
  tasks = []
280
290
  for codec, info in ENCODERS.items():
281
291
  for encoder in info['hw_encoders']:
282
292
  for pix_fmt, bit_depth, chroma, desc in PIXEL_FORMATS:
283
- tasks.append((codec, encoder, pix_fmt, bit_depth, chroma, test_dir, verbose))
293
+ tasks.append((codec, encoder, pix_fmt, bit_depth, chroma, test_dir, verbose, unsupported_encoders))
284
294
 
285
295
  with ThreadPoolExecutor(max_workers=max_workers) as executor:
286
296
  futures = [executor.submit(_run_encoder_bitdepth_test, task) for task in tasks]
@@ -297,7 +307,13 @@ def _run_encoder_bitdepth_tests(test_dir, max_workers, verbose):
297
307
 
298
308
  def _run_decoder_bitdepth_test(test_data):
299
309
  """Tests decoder support for a specific pixel format."""
300
- codec, hw_decoder, pix_fmt, bit_depth, chroma, test_dir, verbose = test_data
310
+ codec, hw_decoder, pix_fmt, bit_depth, chroma, test_dir, verbose, unsupported_decoders = test_data
311
+
312
+ # Skip unsupported decoders
313
+ if hw_decoder in unsupported_decoders:
314
+ title = DECODER_TITLES.get((hw_decoder, codec), f"{hw_decoder.upper()} Decoder:")
315
+ return title, pix_fmt, bit_depth, chroma, "skipped"
316
+
301
317
  if codec == "prores":
302
318
  file_ext = ".mov"
303
319
  else:
@@ -383,17 +399,20 @@ def _run_decoder_bitdepth_test(test_data):
383
399
  return title, pix_fmt, bit_depth, chroma, status
384
400
 
385
401
 
386
- def _run_decoder_bitdepth_tests(test_dir, max_workers, verbose):
402
+ def _run_decoder_bitdepth_tests(test_dir, max_workers, verbose, unsupported_decoders=None):
387
403
  """Tests decoder support for various pixel formats."""
388
404
  results = defaultdict(dict)
389
405
 
406
+ if unsupported_decoders is None:
407
+ unsupported_decoders = set()
408
+
390
409
  print("\n--- Running Bit-depth/Chroma Decoder Tests ---")
391
410
 
392
411
  tasks = []
393
412
  for codec, info in DECODERS.items():
394
413
  for hw_decoder in info['hw_decoders']:
395
414
  for pix_fmt, bit_depth, chroma, desc in PIXEL_FORMATS:
396
- tasks.append((codec, hw_decoder, pix_fmt, bit_depth, chroma, test_dir, verbose))
415
+ tasks.append((codec, hw_decoder, pix_fmt, bit_depth, chroma, test_dir, verbose, unsupported_decoders))
397
416
 
398
417
  with ThreadPoolExecutor(max_workers=max_workers) as executor:
399
418
  futures = [executor.submit(_run_decoder_bitdepth_test, task) for task in tasks]
@@ -463,13 +482,24 @@ def _print_bitdepth_chroma_table(results, table_type="Encoder"):
463
482
  def run_bitdepth_chroma_tests(encoder_count, decoder_count, verbose):
464
483
  """Run all bit-depth and chroma tests and return results."""
465
484
  import shutil
466
- temp_dir = os.path.join(tempfile.gettempdir(), "HwCodecDetect_BitDepth")
485
+ #temp_dir = os.path.join(tempfile.gettempdir(), "HwCodecDetect_BitDepth")
486
+ from .utils import get_temp_path
487
+ temp_dir = os.path.join(get_temp_path(), "HwCodecDetect_BitDepth")
467
488
  if os.path.exists(temp_dir):
468
489
  shutil.rmtree(temp_dir)
469
- os.makedirs(temp_dir)
490
+ os.makedirs(temp_dir, exist_ok=True)
491
+
492
+ # Check codec support before running tests
493
+ print("\nChecking FFmpeg codec support for bit-depth/chroma tests...")
494
+ unsupported_encoders, unsupported_decoders = check_codec_support(ENCODERS, DECODERS)
495
+ if unsupported_encoders or unsupported_decoders:
496
+ print(f"\nFound {len(unsupported_encoders)} unsupported encoder(s) and {len(unsupported_decoders)} unsupported decoder(s).")
497
+ print("These codecs will be marked as unavailable '-' in the results.\n")
498
+ else:
499
+ print("All defined hardware codecs are supported.\n")
470
500
 
471
- encoder_results = _run_encoder_bitdepth_tests(temp_dir, encoder_count, verbose)
472
- decoder_results = _run_decoder_bitdepth_tests(temp_dir, decoder_count, verbose)
501
+ encoder_results = _run_encoder_bitdepth_tests(temp_dir, encoder_count, verbose, unsupported_encoders)
502
+ decoder_results = _run_decoder_bitdepth_tests(temp_dir, decoder_count, verbose, unsupported_decoders)
473
503
 
474
504
  # Clean up
475
505
  shutil.rmtree(temp_dir)
@@ -10,6 +10,7 @@ import argparse
10
10
  from collections import defaultdict
11
11
  from .install_ffmpeg_if_needed import install_ffmpeg_if_needed
12
12
  from .bitdepth_chroma_detect import run_bitdepth_chroma_tests, print_bitdepth_chroma_results
13
+ from .utils import check_codec_support
13
14
  from colorama import init, Fore, Style
14
15
  from concurrent.futures import ThreadPoolExecutor, as_completed
15
16
  from tqdm import tqdm
@@ -263,7 +264,13 @@ def _run_ffmpeg_command(command, verbose):
263
264
 
264
265
  def _run_encoder_test_single(test_data):
265
266
  """Runs a single encoder test and returns the result."""
266
- codec, encoder, res_name, res_size, test_dir, verbose = test_data
267
+ codec, encoder, res_name, res_size, test_dir, verbose, unsupported_encoders = test_data
268
+
269
+ # Skip unsupported encoders
270
+ if encoder in unsupported_encoders:
271
+ title = ENCODER_TITLES.get((encoder, codec), f"{encoder.upper()} Encoder:")
272
+ return title, res_name, "skipped"
273
+
267
274
  if codec == "prores":
268
275
  file_ext = ".mov"
269
276
  else:
@@ -358,17 +365,20 @@ def _run_encoder_test_single(test_data):
358
365
  return title, res_name, status
359
366
 
360
367
 
361
- def _run_encoder_tests(test_dir, max_workers, verbose):
368
+ def _run_encoder_tests(test_dir, max_workers, verbose, unsupported_encoders=None):
362
369
  """Runs hardware encoder tests using a thread pool."""
363
370
  results = defaultdict(dict)
364
-
371
+
372
+ if unsupported_encoders is None:
373
+ unsupported_encoders = set()
374
+
365
375
  print("\n--- Running Encoder Tests ---")
366
-
376
+
367
377
  tasks = []
368
378
  for codec, info in ENCODERS.items():
369
379
  for encoder in info['hw_encoders']:
370
380
  for res_name, res_size in RESOLUTIONS.items():
371
- tasks.append((codec, encoder, res_name, res_size, test_dir, verbose))
381
+ tasks.append((codec, encoder, res_name, res_size, test_dir, verbose, unsupported_encoders))
372
382
 
373
383
  with ThreadPoolExecutor(max_workers=max_workers) as executor:
374
384
  futures = [executor.submit(_run_encoder_test_single, task) for task in tasks]
@@ -381,7 +391,13 @@ def _run_encoder_tests(test_dir, max_workers, verbose):
381
391
 
382
392
  def _run_decoder_test_single(test_data):
383
393
  """Runs a single decoder test and returns the result."""
384
- codec, hw_decoder, res_name, res_size, test_dir, verbose = test_data
394
+ codec, hw_decoder, res_name, res_size, test_dir, verbose, unsupported_decoders = test_data
395
+
396
+ # Skip unsupported decoders
397
+ if hw_decoder in unsupported_decoders:
398
+ title = DECODER_TITLES.get((hw_decoder, codec), f"{hw_decoder.upper()} Decoder:")
399
+ return title, res_name, "skipped"
400
+
385
401
  if codec == "prores":
386
402
  file_ext = ".mov"
387
403
  else:
@@ -476,17 +492,20 @@ def _run_decoder_test_single(test_data):
476
492
  return title, res_name, status
477
493
 
478
494
 
479
- def _run_decoder_tests(test_dir, max_workers, verbose):
495
+ def _run_decoder_tests(test_dir, max_workers, verbose, unsupported_decoders=None):
480
496
  """Runs hardware decoder tests using a thread pool."""
481
497
  results = defaultdict(dict)
482
498
 
499
+ if unsupported_decoders is None:
500
+ unsupported_decoders = set()
501
+
483
502
  print("\n--- Running Decoder Tests ---")
484
503
 
485
504
  tasks = []
486
505
  for codec, info in DECODERS.items():
487
506
  for hw_decoder in info['hw_decoders']:
488
507
  for res_name, res_size in RESOLUTIONS.items():
489
- tasks.append((codec, hw_decoder, res_name, res_size, test_dir, verbose))
508
+ tasks.append((codec, hw_decoder, res_name, res_size, test_dir, verbose, unsupported_decoders))
490
509
 
491
510
  with ThreadPoolExecutor(max_workers=max_workers) as executor:
492
511
  futures = [executor.submit(_run_decoder_test_single, task) for task in tasks]
@@ -575,14 +594,25 @@ def run_all_tests(args):
575
594
  print("Error: FFmpeg dependency not met. Please check installation.", file=sys.stderr)
576
595
  return -1
577
596
 
578
- temp_dir = os.path.join(tempfile.gettempdir(), "HwCodecDetect")
597
+ # Check codec support before running tests
598
+ print("\nChecking FFmpeg codec support...")
599
+ unsupported_encoders, unsupported_decoders = check_codec_support(ENCODERS, DECODERS)
600
+ if unsupported_encoders or unsupported_decoders:
601
+ print(f"\nFound {len(unsupported_encoders)} unsupported encoder(s) and {len(unsupported_decoders)} unsupported decoder(s).")
602
+ print("These codecs will be marked as unavailable '-' in the results.\n")
603
+ else:
604
+ print("All defined hardware codecs are supported.\n")
605
+
606
+ #temp_dir = os.path.join(tempfile.gettempdir(), "HwCodecDetect")
607
+ from .utils import get_temp_path
608
+ temp_dir = os.path.join(get_temp_path(), "HwCodecDetect_cli")
579
609
  if os.path.exists(temp_dir):
580
610
  # Clear previous run data to ensure a fresh test
581
611
  shutil.rmtree(temp_dir)
582
- os.makedirs(temp_dir)
612
+ os.makedirs(temp_dir, exist_ok=True)
583
613
 
584
- encoder_results = _run_encoder_tests(temp_dir, args.encoder_count, args.verbose)
585
- decoder_results = _run_decoder_tests(temp_dir, args.decoder_count, args.verbose)
614
+ encoder_results = _run_encoder_tests(temp_dir, args.encoder_count, args.verbose, unsupported_encoders)
615
+ decoder_results = _run_decoder_tests(temp_dir, args.decoder_count, args.verbose, unsupported_decoders)
586
616
 
587
617
  all_results = {}
588
618
  all_results.update(encoder_results)
@@ -0,0 +1,166 @@
1
+ import os
2
+ import os.path
3
+ import tempfile
4
+ import sys
5
+ import re
6
+ import subprocess
7
+ from pathlib import Path
8
+
9
+ def get_temp_path():
10
+ app_id = "HwCodecDetect"
11
+
12
+ candidates = []
13
+
14
+ candidates.append(os.path.join(tempfile.gettempdir(), app_id))
15
+
16
+ if sys.platform == "win32":
17
+ local_appdata = os.getenv("LOCALAPPDATA")
18
+ if local_appdata:
19
+ candidates.append(os.path.join(local_appdata, app_id))
20
+ elif sys.platform == "darwin":
21
+ candidates.append(os.path.expanduser(f"~/Library/Caches/{app_id}"))
22
+ elif sys.platform.startswith("linux"):
23
+ xdg_cache = os.getenv("XDG_CACHE_HOME", os.path.expanduser("~/.cache"))
24
+ candidates.append(os.path.join(xdg_cache, app_id))
25
+
26
+ candidates.append(os.path.expanduser(f"~/.{app_id}"))
27
+
28
+ for path in candidates:
29
+ if not path:
30
+ continue
31
+
32
+ try:
33
+ os.makedirs(path, mode=0o755, exist_ok=True)
34
+ test_file = os.path.join(path, ".perm_test")
35
+ with open(test_file, 'w', encoding='utf-8') as f:
36
+ f.write("test")
37
+
38
+ if os.path.exists(test_file):
39
+ os.remove(test_file)
40
+ return path
41
+ except Exception as e:
42
+ print(f"Warning: Attempting path {path} failed: {e}")
43
+ continue
44
+
45
+ return os.getcwd()
46
+
47
+
48
+ def get_local_version():
49
+ if getattr(sys, 'frozen', False):
50
+ base_path = sys._MEIPASS
51
+ else:
52
+ base_path = os.path.dirname(os.path.abspath(__file__))
53
+
54
+ v_path = os.path.join(base_path, "VERSION")
55
+
56
+ if not os.path.exists(v_path):
57
+ v_path = os.path.join(base_path, "..", "..", "VERSION")
58
+
59
+ try:
60
+ with open(v_path, "r", encoding="utf-8") as f:
61
+ return f.read().strip()
62
+ except FileNotFoundError:
63
+ return "Unknown Version"
64
+
65
+
66
+ def get_ffmpeg_supported_codecs():
67
+ """Get supported encoders, decoders and hwaccels from ffmpeg.
68
+
69
+ Returns:
70
+ tuple: (supported_encoders, supported_decoders) - two sets containing:
71
+ - supported_encoders: set of supported encoder names and hwaccel methods
72
+ - supported_decoders: set of supported decoder names and hwaccel methods
73
+ """
74
+ supported_encoders = set()
75
+ supported_decoders = set()
76
+
77
+ try:
78
+ # Get encoders
79
+ result = subprocess.run(
80
+ ["ffmpeg", "-hide_banner", "-encoders"],
81
+ capture_output=True,
82
+ text=True,
83
+ encoding='utf-8',
84
+ errors='ignore'
85
+ )
86
+ if result.returncode == 0:
87
+ for line in result.stdout.split('\n'):
88
+ # Parse lines like: V....D av1_nvenc NVIDIA NVENC av1 encoder (codec av1)
89
+ # Format: [VSA][6 chars of flags] [codec name] [description]
90
+ match = re.search(r'^\s*[VSA].{6}\s*(\S+)', line)
91
+ if match:
92
+ supported_encoders.add(match.group(1))
93
+
94
+ # Get decoders
95
+ result = subprocess.run(
96
+ ["ffmpeg", "-hide_banner", "-decoders"],
97
+ capture_output=True,
98
+ text=True,
99
+ encoding='utf-8',
100
+ errors='ignore'
101
+ )
102
+ if result.returncode == 0:
103
+ for line in result.stdout.split('\n'):
104
+ # Parse lines like: V..... av1_cuvid Nvidia CUVID AV1 decoder (codec av1)
105
+ match = re.search(r'^\s*[VSA].{6}\s*(\S+)', line)
106
+ if match:
107
+ supported_decoders.add(match.group(1))
108
+
109
+ # Get hardware acceleration methods
110
+ result = subprocess.run(
111
+ ["ffmpeg", "-hide_banner", "-hwaccels"],
112
+ capture_output=True,
113
+ text=True,
114
+ encoding='utf-8',
115
+ errors='ignore'
116
+ )
117
+ if result.returncode == 0:
118
+ lines = result.stdout.split('\n')
119
+ for line in lines:
120
+ line = line.strip()
121
+ # Skip empty lines and header
122
+ if not line or line.startswith('Hardware acceleration'):
123
+ continue
124
+ # Add hwaccel methods to both encoders and decoders
125
+ supported_encoders.add(line)
126
+ supported_decoders.add(line)
127
+
128
+ except Exception as e:
129
+ print(f"Warning: Failed to get ffmpeg codecs: {e}", file=sys.stderr)
130
+
131
+ return supported_encoders, supported_decoders
132
+
133
+
134
+ def check_codec_support(encoders_dict, decoders_dict):
135
+ """Check which hardware codecs are supported by current ffmpeg version.
136
+
137
+ Args:
138
+ encoders_dict: Dictionary of encoder definitions (like ENCODERS)
139
+ decoders_dict: Dictionary of decoder definitions (like DECODERS)
140
+
141
+ Returns:
142
+ tuple: (unsupported_encoders, unsupported_decoders) - two sets containing
143
+ the names of unsupported encoders and decoders
144
+ """
145
+ from colorama import Fore, Style
146
+
147
+ supported_encoders, supported_decoders = get_ffmpeg_supported_codecs()
148
+
149
+ unsupported_encoders = set()
150
+ unsupported_decoders = set()
151
+
152
+ # Check encoders
153
+ for codec, info in encoders_dict.items():
154
+ for encoder in info.get('hw_encoders', []):
155
+ if encoder not in supported_encoders:
156
+ unsupported_encoders.add(encoder)
157
+ print(f"{Fore.YELLOW}Warning: Encoder '{encoder}' is not supported by current FFmpeg version{Style.RESET_ALL}")
158
+
159
+ # Check decoders
160
+ for codec, info in decoders_dict.items():
161
+ for decoder in info.get('hw_decoders', []):
162
+ if decoder not in supported_decoders:
163
+ unsupported_decoders.add(decoder)
164
+ print(f"{Fore.YELLOW}Warning: Decoder '{decoder}' is not supported by current FFmpeg version{Style.RESET_ALL}")
165
+
166
+ return unsupported_encoders, unsupported_decoders
@@ -0,0 +1 @@
1
+ __version__ = "0.2.4"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: HwCodecDetect
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: A cross-platform tool to automatically detect and test hardware video decoders/encoders using FFmpeg.
5
5
  Home-page: https://github.com/whyb/HwCodecDetect
6
6
  Author: whyb
@@ -9,6 +9,7 @@ src/HwCodecDetect/__init__.py
9
9
  src/HwCodecDetect/bitdepth_chroma_detect.py
10
10
  src/HwCodecDetect/install_ffmpeg_if_needed.py
11
11
  src/HwCodecDetect/run_tests.py
12
+ src/HwCodecDetect/utils.py
12
13
  src/HwCodecDetect/version.py
13
14
  src/HwCodecDetect.egg-info/PKG-INFO
14
15
  src/HwCodecDetect.egg-info/SOURCES.txt
@@ -1 +0,0 @@
1
- 0.2.3
@@ -1 +0,0 @@
1
- __version__ = "0.2.3"
File without changes
File without changes
File without changes
File without changes
File without changes