HwCodecDetect 0.0.1__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,6 @@
1
+ # HwCodecDetect/__init__.py
2
+
3
+ from .install_ffmpeg_if_needed import install_ffmpeg_if_needed
4
+
5
+ __version__ = "0.0.1"
6
+ __author__ = "whyb"
@@ -0,0 +1,189 @@
1
+ import os
2
+ import sys
3
+ import platform
4
+ import subprocess
5
+ import shutil
6
+ import urllib.request
7
+ import zipfile
8
+ import tempfile
9
+
10
+ def install_ffmpeg_if_needed():
11
+ """
12
+ Checks for FFmpeg and installs it if not found.
13
+ """
14
+ print("Checking for FFmpeg...")
15
+
16
+ # Step 1: Check if FFmpeg is already in PATH
17
+ if shutil.which("ffmpeg"):
18
+ print("FFmpeg is already installed and in PATH.")
19
+ return 0
20
+
21
+ # Step 2: Install based on OS
22
+ os_name = platform.system()
23
+
24
+ if os_name == "Windows":
25
+ return install_on_windows()
26
+ elif os_name == "Linux":
27
+ return install_on_linux()
28
+ elif os_name == "Darwin": # 'Darwin' is the system name for macOS
29
+ return install_on_macos()
30
+ else:
31
+ print(f"Unsupported OS: {os_name}", file=sys.stderr)
32
+ return -1
33
+
34
+ def install_on_windows():
35
+ """
36
+ Tries to install FFmpeg on Windows using a cascade of package managers,
37
+ falling back to a manual download if all else fails.
38
+ """
39
+ print("FFmpeg not found. Attempting to install for Windows...")
40
+
41
+ # Method 1: Try Winget (Windows Package Manager)
42
+ if shutil.which("winget"):
43
+ print("Trying to install FFmpeg using Winget...")
44
+ try:
45
+ # Use the official ID for FFmpeg from Winget
46
+ subprocess.run(["winget", "install", "--id", "Gyan.FFmpeg", "-e", "--accept-package-agreements", "--accept-source-agreements"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
47
+ print("FFmpeg installation complete via Winget.")
48
+ return 0
49
+ except subprocess.CalledProcessError:
50
+ print("Winget installation failed. Trying next method.", file=sys.stderr)
51
+ pass
52
+
53
+ # Method 2: Try Chocolatey
54
+ if shutil.which("choco"):
55
+ print("Trying to install FFmpeg using Chocolatey...")
56
+ try:
57
+ subprocess.run(["choco", "install", "ffmpeg", "-y"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
58
+ print("FFmpeg installation complete via Chocolatey.")
59
+ return 0
60
+ except subprocess.CalledProcessError:
61
+ print("Chocolatey installation failed. Trying next method.", file=sys.stderr)
62
+ pass
63
+
64
+ # Method 3: Try Scoop
65
+ if shutil.which("scoop"):
66
+ print("Trying to install FFmpeg using Scoop...")
67
+ try:
68
+ subprocess.run(["scoop", "install", "ffmpeg"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
69
+ print("FFmpeg installation complete via Scoop.")
70
+ return 0
71
+ except subprocess.CalledProcessError:
72
+ print("Scoop installation failed. Trying next method.", file=sys.stderr)
73
+ pass
74
+
75
+ # Method 4: Manual Download (Fallback)
76
+ print("All package manager installations failed. Falling back to manual download.")
77
+ ffmpeg_url = "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl-shared.zip"
78
+
79
+ try:
80
+ with tempfile.TemporaryDirectory() as temp_dir:
81
+ zip_file_path = os.path.join(temp_dir, "ffmpeg_temp.zip")
82
+
83
+ urllib.request.urlretrieve(ffmpeg_url, zip_file_path)
84
+ print("Download complete. Extracting...")
85
+
86
+ with zipfile.ZipFile(zip_file_path, 'r') as zip_ref:
87
+ zip_ref.extractall(temp_dir)
88
+
89
+ bin_source_path = os.path.join(temp_dir, "ffmpeg-master-latest-win64-gpl-shared", "bin")
90
+
91
+ try:
92
+ for item in os.listdir(bin_source_path):
93
+ shutil.move(os.path.join(bin_source_path, item), os.getcwd())
94
+ print("FFmpeg files moved to the current directory.")
95
+ except Exception as e:
96
+ print(f"Error moving files to the current directory: {e}", file=sys.stderr)
97
+ print("Adding FFmpeg's bin directory to PATH instead.")
98
+ os.environ["PATH"] += os.pathsep + bin_source_path
99
+ print(f"Added {bin_source_path} to PATH.")
100
+
101
+ return 0
102
+
103
+ except Exception as e:
104
+ print(f"An unexpected error occurred during manual installation: {e}", file=sys.stderr)
105
+ return -1
106
+
107
+ def install_on_linux():
108
+ """
109
+ Installs FFmpeg using package managers for various Linux distributions.
110
+ """
111
+ print("FFmpeg not found. Attempting to install via package manager...")
112
+
113
+ distro = get_linux_distro()
114
+
115
+ if "arch" in distro:
116
+ command = ["sudo", "pacman", "-S", "--noconfirm", "ffmpeg"]
117
+ package_manager_name = "pacman"
118
+ elif "ubuntu" in distro or "debian" in distro:
119
+ command = ["sudo", "apt-get", "install", "-y", "ffmpeg"]
120
+ package_manager_name = "apt-get"
121
+ elif "centos" in distro or "redhat" in distro or "fedora" in distro:
122
+ command = ["sudo", "yum", "install", "-y", "ffmpeg"]
123
+ package_manager_name = "yum"
124
+ else:
125
+ print("Unsupported Linux distribution. Please install FFmpeg manually.", file=sys.stderr)
126
+ return -1
127
+
128
+ try:
129
+ print(f"Attempting to install FFmpeg using {package_manager_name}...")
130
+ subprocess.run(command, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
131
+ print("FFmpeg installation complete.")
132
+ return 0
133
+ except subprocess.CalledProcessError as e:
134
+ print(f"Error installing FFmpeg with {package_manager_name}: {e}", file=sys.stderr)
135
+ return -1
136
+ except FileNotFoundError:
137
+ print(f"Package manager '{package_manager_name}' not found. Please install FFmpeg manually.", file=sys.stderr)
138
+ return -1
139
+
140
+ def install_on_macos():
141
+ """
142
+ Installs FFmpeg using Homebrew for macOS.
143
+ """
144
+ print("FFmpeg not found. Attempting to install via Homebrew...")
145
+
146
+ if not shutil.which("brew"):
147
+ print("Homebrew not found. Please install Homebrew first by running:", file=sys.stderr)
148
+ print('/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"', file=sys.stderr)
149
+ return -1
150
+
151
+ try:
152
+ print("Attempting to install FFmpeg using brew...")
153
+ subprocess.run(["brew", "install", "ffmpeg"], check=True)
154
+ print("FFmpeg installation complete.")
155
+ return 0
156
+ except subprocess.CalledProcessError as e:
157
+ print(f"Error installing FFmpeg with Homebrew: {e}", file=sys.stderr)
158
+ return -1
159
+
160
+ def get_linux_distro():
161
+ """
162
+ A simplified way to detect Linux distributions.
163
+ """
164
+ if os.path.exists('/etc/os-release'):
165
+ with open('/etc/os-release') as f:
166
+ content = f.read()
167
+ if "ID=arch" in content:
168
+ return "arch"
169
+ elif "ID=ubuntu" in content:
170
+ return "ubuntu"
171
+ elif "ID=debian" in content:
172
+ return "debian"
173
+ elif "ID_LIKE=centos" in content or "ID=centos" in content:
174
+ return "centos"
175
+ elif "ID=fedora" in content:
176
+ return "fedora"
177
+
178
+ if os.path.exists('/etc/arch-release'):
179
+ return "arch"
180
+ if os.path.exists('/etc/debian_version'):
181
+ return "debian"
182
+ if os.path.exists('/etc/redhat-release'):
183
+ return "redhat"
184
+
185
+ return "unknown"
186
+
187
+ if __name__ == "__main__":
188
+ exit_code = install_ffmpeg_if_needed()
189
+ sys.exit(exit_code)
@@ -0,0 +1,352 @@
1
+ import os
2
+ import re
3
+ import sys
4
+ import subprocess
5
+ import shutil
6
+ import tempfile
7
+ from collections import defaultdict
8
+ from .install_ffmpeg_if_needed import install_ffmpeg_if_needed
9
+
10
+ # Step 0: Define the data for codecs and resolutions
11
+ # This approach avoids massive code duplication.
12
+ RESOLUTIONS = {
13
+ "240p": "426x240",
14
+ "360p": "640x360",
15
+ "480p": "854x480",
16
+ "720p": "1280x720",
17
+ "1080p": "1920x1080",
18
+ "2K": "2560x1440",
19
+ "4K": "3840x2160",
20
+ "8K": "7680x4320",
21
+ }
22
+
23
+ # Mapping of a decoder's ffmpeg name and codec to its descriptive title
24
+ DECODER_TITLES = {
25
+ ("h264_cuvid", "h264"): "NVIDIA CUDA H264 Decoder(NVDEC)",
26
+ ("h264_qsv", "h264"): "Intel Quick Sync Video H264 Decoder(QSV)",
27
+ ("hevc_cuvid", "h265"): "NVIDIA CUDA H265 Decoder(NVDEC)",
28
+ ("hevc_qsv", "h265"): "Intel Quick Sync Video H265 Decoder(QSV)",
29
+ ("av1_cuvid", "av1"): "NVIDIA CUDA AV1 Decoder(NVDEC)",
30
+ ("av1_qsv", "av1"): "Intel Quick Sync Video AV1 Decoder(QSV)",
31
+ ("mjpeg_cuvid", "mjpeg"): "NVIDIA CUDA MJPEG Decoder(NVDEC)",
32
+ ("mjpeg_qsv", "mjpeg"): "Intel Quick Sync Video MJPEG Decoder(QSV)",
33
+ ("mpeg1_cuvid", "mpeg1"): "NVIDIA CUDA MPEG-1 Decoder(NVDEC)",
34
+ ("mpeg2_cuvid", "mpeg2"): "NVIDIA CUDA MPEG-2 Decoder(NVDEC)",
35
+ ("mpeg2_qsv", "mpeg2"): "Intel Quick Sync Video MPEG-2 Decoder(QSV)",
36
+ ("mpeg4_cuvid", "mpeg4"): "NVIDIA CUDA MPEG-4 Decoder(NVDEC)",
37
+ ("vp8_cuvid", "vp8"): "NVIDIA CUDA VP8 Decoder(NVDEC)",
38
+ ("vp8_qsv", "vp8"): "Intel Quick Sync Video VP8 Decoder(QSV)",
39
+ ("vp9_cuvid", "vp9"): "NVIDIA CUDA VP9 Decoder(NVDEC)",
40
+ ("vp9_qsv", "vp9"): "Intel Quick Sync Video VP9 Decoder(QSV)",
41
+ ("dxva2", "h264"): "Microsoft DirectX Video Acceleration H264 Decoder(DXVA2)",
42
+ ("dxva2", "h265"): "Microsoft DirectX Video Acceleration H265 Decoder(DXVA2)",
43
+ ("dxva2", "av1"): "Microsoft DirectX Video Acceleration AV1 Decoder(DXVA2)",
44
+ ("dxva2", "mjpeg"): "Microsoft DirectX Video Acceleration MJPEG Decoder(DXVA2)",
45
+ ("dxva2", "mpeg1"): "Microsoft DirectX Video Acceleration MPEG-1 Decoder(DXVA2)",
46
+ ("dxva2", "mpeg2"): "Microsoft DirectX Video Acceleration MPEG-2 Decoder(DXVA2)",
47
+ ("dxva2", "mpeg4"): "Microsoft DirectX Video Acceleration MPEG-4 Decoder(DXVA2)",
48
+ ("dxva2", "vp8"): "Microsoft DirectX Video Acceleration VP8 Decoder(DXVA2)",
49
+ ("dxva2", "vp9"): "Microsoft DirectX Video Acceleration VP9 Decoder(DXVA2)",
50
+ ("d3d11va", "h264"): "Direct3D 11 Video Acceleration H264 Decoder(D3D11VA)",
51
+ ("d3d11va", "h265"): "Direct3D 11 Video Acceleration H265 Decoder(D3D11VA)",
52
+ ("d3d11va", "av1"): "Direct3D 11 Video Acceleration AV1 Decoder(D3D11VA)",
53
+ ("d3d11va", "mjpeg"): "Direct3D 11 Video Acceleration MJPEG Decoder(D3D11VA)",
54
+ ("d3d11va", "mpeg1"): "Direct3D 11 Video Acceleration MPEG-1 Decoder(D3D11VA)",
55
+ ("d3d11va", "mpeg2"): "Direct3D 11 Video Acceleration MPEG-2 Decoder(D3D11VA)",
56
+ ("d3d11va", "mpeg4"): "Direct3D 11 Video Acceleration MPEG-4 Decoder(D3D11VA)",
57
+ ("d3d11va", "vp8"): "Direct3D 11 Video Acceleration VP8 Decoder(D3D11VA)",
58
+ ("d3d11va", "vp9"): "Direct3D 11 Video Acceleration VP9 Decoder(D3D11VA)",
59
+ }
60
+
61
+ DECODERS = {
62
+ "h264": {"lib": "libx264", "hw_decoders": ["h264_cuvid", "h264_qsv", "dxva2", "d3d11va"]},
63
+ "h265": {"lib": "libx265", "hw_decoders": ["hevc_cuvid", "hevc_qsv", "d3d11va"]},
64
+ "av1": {"lib": "librav1e", "hw_decoders": ["av1_cuvid", "av1_qsv", "dxva2", "d3d11va"]},
65
+ "mjpeg": {"lib": "mjpeg", "hw_decoders": ["mjpeg_cuvid", "mjpeg_qsv", "dxva2", "d3d11va"]},
66
+ "mpeg1": {"lib": "mpeg1video", "hw_decoders": ["mpeg1_cuvid", "dxva2", "d3d11va"]},
67
+ "mpeg2": {"lib": "mpeg2video", "hw_decoders": ["mpeg2_cuvid", "mpeg2_qsv", "dxva2", "d3d11va"]},
68
+ "mpeg4": {"lib": "mpeg4", "hw_decoders": ["mpeg4_cuvid", "dxva2", "d3d11va"]},
69
+ "vp8": {"lib": "libvpx", "hw_decoders": ["vp8_cuvid", "vp8_qsv", "dxva2", "d3d11va"]},
70
+ "vp9": {"lib": "libvpx-vp9", "hw_decoders": ["vp9_cuvid", "vp9_qsv", "dxva2", "d3d11va"]},
71
+ }
72
+
73
+ # --- Encoder Definitions ---
74
+ ENCODER_TITLES = {
75
+ ("h264_nvenc", "h264"): "NVIDIA Hardware H264 Encoder(NVEnc)",
76
+ ("hevc_nvenc", "h265"): "NVIDIA Hardware H265 Encoder(NVEnc)",
77
+ ("av1_nvenc", "av1"): "NVIDIA Hardware AV1 Encoder(NVEnc)",
78
+ ("h264_qsv", "h264"): "Intel Hardware H264 Encoder(QSV)",
79
+ ("hevc_qsv", "h265"): "Intel Hardware H265 Encoder(QSV)",
80
+ ("av1_qsv", "av1"): "Intel Hardware AV1 Encoder(QSV)",
81
+ ("mjpeg_qsv", "mjpeg"): "Intel Hardware MJPEG Encoder(QSV)",
82
+ ("mpeg2_qsv", "mpeg2"): "Intel Hardware MPEG-2 Encoder(QSV)",
83
+ ("vp9_qsv", "vp9"): "Intel Hardware VP9 Encoder(QSV)",
84
+ ("h264_amf", "h264"): "AMD Hardware H264 Encoder(AMF)",
85
+ ("hevc_amf", "h265"): "AMD Hardware H265 Encoder(AMF)",
86
+ ("av1_amf", "av1"): "AMD Hardware AV1 Encoder(AMF)",
87
+ ("h264_mf", "h264"): "Microsoft Hardware H264 Encoder(MediaFoundation)",
88
+ ("hevc_mf", "h265"): "Microsoft Hardware H265 Encoder(MediaFoundation)",
89
+ ("h264_vaapi", "h264"): "Video Acceleration H264 Encoder(VAAPI)",
90
+ ("hevc_vaapi", "h265"): "Video Acceleration H265 Encoder(VAAPI)",
91
+ ("av1_vaapi", "av1"): "Video Acceleration AV1 Encoder(VAAPI)",
92
+ ("mjpeg_vaapi", "mjpeg"): "Video Acceleration MJPEG Encoder(VAAPI)",
93
+ ("mpeg2_vaapi", "mpeg2"): "Video Acceleration MPEG-2 Encoder(VAAPI)",
94
+ ("vp8_vaapi", "vp8"): "Video Acceleration VP8 Encoder(VAAPI)",
95
+ ("vp9_vaapi", "vp9"): "Video Acceleration VP9 Encoder(VAAPI)",
96
+ ("h264_vulkan", "h264"): "Vulkan Hardware H264 Encoder",
97
+ ("hevc_vulkan", "h265"): "Vulkan Hardware H265 Encoder",
98
+ }
99
+
100
+ ENCODERS = {
101
+ "h264": {"lib": "libx264", "hw_encoders": ["h264_nvenc", "h264_qsv", "h264_amf", "h264_mf", "h264_vaapi", "h264_vulkan"]},
102
+ "h265": {"lib": "libx265", "hw_encoders": ["hevc_nvenc", "hevc_qsv", "hevc_amf", "hevc_mf", "hevc_vaapi", "hevc_vulkan"]},
103
+ "av1": {"lib": "librav1e", "hw_encoders": ["av1_nvenc", "av1_qsv", "av1_amf", "av1_vaapi"]},
104
+ "mjpeg": {"lib": "mjpeg", "hw_encoders": ["mjpeg_qsv", "mjpeg_vaapi"]},
105
+ "mpeg2": {"lib": "mpeg2video", "hw_encoders": ["mpeg2_qsv", "mpeg2_vaapi"]},
106
+ "vp8": {"lib": "libvpx", "hw_encoders": ["vp8_vaapi"]},
107
+ "vp9": {"lib": "libvpx-vp9", "hw_encoders": ["vp9_qsv", "vp9_vaapi"]},
108
+ }
109
+
110
+ # Combine both decoder and encoder data into a single structure
111
+ # This makes it easier to work with all codecs and their associated CPU libs
112
+ ALL_CODECS = {
113
+ **DECODERS,
114
+ **{k: v for k, v in ENCODERS.items() if k not in DECODERS}
115
+ }
116
+
117
+ def _run_ffmpeg_command(command):
118
+ """Executes an FFmpeg command and returns True on success, False on failure."""
119
+ try:
120
+ result = subprocess.run(
121
+ command,
122
+ check=True,
123
+ stdout=subprocess.DEVNULL,
124
+ stderr=subprocess.DEVNULL,
125
+ )
126
+ return result.returncode == 0
127
+ except (subprocess.CalledProcessError, FileNotFoundError):
128
+ return False
129
+
130
+ def _run_encoder_tests(test_dir):
131
+ """Runs hardware encoder tests and returns a structured dictionary of results."""
132
+ results = defaultdict(dict)
133
+
134
+ GREEN = "\033[92m"
135
+ RED = "\033[91m"
136
+ RESET = "\033[0m"
137
+
138
+ print("\n--- Running Encoder Tests ---")
139
+
140
+ for codec, info in ENCODERS.items():
141
+ for encoder in info['hw_encoders']:
142
+ title = ENCODER_TITLES.get((encoder, codec), f"{encoder.upper()} Encoder:")
143
+ print(f"\n{title}")
144
+
145
+ for res_name, res_size in RESOLUTIONS.items():
146
+ # Encoder tests generate the output file
147
+ file_ext = ".webm" if codec in ["vp8", "vp9"] else ".mp4"
148
+ output_file = os.path.join(test_dir, f"{encoder}_{res_name}{file_ext}")
149
+
150
+ command = [
151
+ "ffmpeg",
152
+ "-loglevel", "quiet",
153
+ "-hide_banner",
154
+ "-y",
155
+ "-f", "lavfi",
156
+ "-i", f"color=white:s={res_size}:d=1",
157
+ "-frames:v", "1",
158
+ "-c:v", encoder,
159
+ "-pixel_format", "yuv420p",
160
+ output_file,
161
+ ]
162
+
163
+ # Specific flags for QSV encoders from the bat file
164
+ if "qsv" in encoder:
165
+ command.insert(9, "-dual_gfx")
166
+ command.insert(10, "0")
167
+
168
+ status = "succeeded" if _run_ffmpeg_command(command) else "failed"
169
+ results[title][res_name] = status
170
+
171
+ color_code = GREEN if status == "succeeded" else RED
172
+ print(f" {res_name}: {color_code}{status}{RESET}")
173
+
174
+ return results
175
+
176
+ def _run_decoder_tests(test_dir):
177
+ """
178
+ Runs hardware decoder tests and returns a structured dictionary of results.
179
+ It checks for existing files first and generates new ones if needed.
180
+ """
181
+ results = defaultdict(dict)
182
+
183
+ GREEN = "\033[92m"
184
+ RED = "\033[91m"
185
+ RESET = "\033[0m"
186
+
187
+ print("\n--- Running Decoder Tests ---")
188
+
189
+ for codec, info in DECODERS.items():
190
+ for hw_decoder in info['hw_decoders']:
191
+ title = DECODER_TITLES.get((hw_decoder, codec), f"{hw_decoder.upper()} Decoder:")
192
+ print(f"\n{title}")
193
+
194
+ for res_name, res_size in RESOLUTIONS.items():
195
+ file_ext = ".webm" if codec in ["vp8", "vp9"] else ".mp4"
196
+
197
+ # We need to find *any* existing file for this codec/resolution combo
198
+ # A simple approach is to check if a file with the correct codec and resolution exists
199
+ found_file = False
200
+ test_file_path = os.path.join(test_dir, f"{codec}_{res_name}{file_ext}")
201
+
202
+ # Try to use any file already generated from encoder tests
203
+ for filename in os.listdir(test_dir):
204
+ if filename.startswith(f"{codec}_") and f"_{res_name}" in filename:
205
+ test_file_path = os.path.join(test_dir, filename)
206
+ found_file = True
207
+ break
208
+
209
+ # If no file found, generate one using CPU encoder
210
+ if not found_file:
211
+ cpu_lib = ALL_CODECS[codec]["lib"]
212
+ command = [
213
+ "ffmpeg", "-loglevel", "quiet", "-hide_banner", "-y",
214
+ "-f", "lavfi", "-i", f"color=white:s={res_size}:d=1",
215
+ "-frames:v", "1", "-c:v", cpu_lib, "-pixel_format", "yuv420p",
216
+ test_file_path,
217
+ ]
218
+ if not _run_ffmpeg_command(command):
219
+ results[title][res_name] = "skipped"
220
+ print(f" {res_name}: {RED}skipped (failed to generate temp file){RESET}")
221
+ continue
222
+
223
+ # Run the actual decoder test
224
+ if hw_decoder in ["dxva2", "d3d11va"] and codec in ["h264", "h265", "vp8"]:
225
+ command = [
226
+ "ffmpeg", "-loglevel", "quiet", "-hide_banner", "-y",
227
+ "-hwaccel", hw_decoder, "-i", test_file_path,
228
+ "-c:v", "libx264", "-preset", "ultrafast",
229
+ "-f", "null", "null",
230
+ ]
231
+ else:
232
+ command = [
233
+ "ffmpeg", "-loglevel", "quiet", "-hide_banner", "-y",
234
+ "-c:v", hw_decoder, "-i", test_file_path,
235
+ "-c:v", "libx264", "-preset", "ultrafast",
236
+ "-f", "null", "null",
237
+ ]
238
+
239
+ status = "succeeded" if _run_ffmpeg_command(command) else "failed"
240
+ results[title][res_name] = status
241
+
242
+ color_code = GREEN if status == "succeeded" else RED
243
+ print(f" {res_name}: {color_code}{status}{RESET}")
244
+
245
+ return results
246
+
247
+ def _get_display_width(s):
248
+ """
249
+ Calculates the display width of a string, ignoring ANSI escape codes.
250
+ """
251
+ ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
252
+ return len(ansi_escape.sub('', s))
253
+
254
+ def _print_summary_table(results):
255
+ """Prints a formatted summary table of all test results."""
256
+ GREEN_CHECK = "\033[92m✓\033[0m"
257
+ RED_X = "\033[91m×\033[0m"
258
+ GRAY_DASH = "\033[90m—\033[0m"
259
+
260
+ # Get column headers (resolutions) and row headers (decoder titles)
261
+ resolutions = list(RESOLUTIONS.keys())
262
+
263
+ # Use different headers for decoders and encoders
264
+ decoder_titles = sorted([t for t in results.keys() if "Decoder" in t])
265
+ encoder_titles = sorted([t for t in results.keys() if "Encoder" in t])
266
+
267
+ # Calculate column widths based on display width
268
+ res_width = max(len(res) for res in resolutions)
269
+ row_header_width = max([_get_display_width(t) for t in results.keys()] + [20, _get_display_width("Decoder"), _get_display_width("Encoder")])
270
+
271
+ # Print Decoder Table
272
+ if decoder_titles:
273
+ print("\n" + "-" * (row_header_width + 3 + (res_width + 3) * len(resolutions)))
274
+ header_text = "Decoder"
275
+ padding_left = (row_header_width - _get_display_width(header_text)) // 2
276
+ padding_right = row_header_width - _get_display_width(header_text) - padding_left
277
+ header_row = f"| {' ' * padding_left}{header_text}{' ' * padding_right} |"
278
+ for res in resolutions:
279
+ header_row += f" {res.center(res_width)} |"
280
+ print(header_row)
281
+ print("-" * (row_header_width + 3 + (res_width + 3) * len(resolutions)))
282
+
283
+ for title in decoder_titles:
284
+ padding_needed = row_header_width - _get_display_width(title)
285
+ row_string = f"| {title}{' ' * padding_needed} |"
286
+ for res in resolutions:
287
+ status = results.get(title, {}).get(res, "skipped")
288
+ symbol = GREEN_CHECK if status == "succeeded" else RED_X if status == "failed" else GRAY_DASH
289
+ symbol_width = _get_display_width(symbol)
290
+ padding_left = (res_width - symbol_width) // 2
291
+ padding_right = res_width - symbol_width - padding_left
292
+ row_string += f" {' ' * padding_left}{symbol}{' ' * padding_right} |"
293
+ print(row_string)
294
+ print("-" * (row_header_width + 3 + (res_width + 3) * len(resolutions)))
295
+
296
+ # Print Encoder Table
297
+ if encoder_titles:
298
+ print("\n" + "-" * (row_header_width + 3 + (res_width + 3) * len(resolutions)))
299
+ header_text = "Encoder"
300
+ padding_left = (row_header_width - _get_display_width(header_text)) // 2
301
+ padding_right = row_header_width - _get_display_width(header_text) - padding_left
302
+ header_row = f"| {' ' * padding_left}{header_text}{' ' * padding_right} |"
303
+ for res in resolutions:
304
+ header_row += f" {res.center(res_width)} |"
305
+ print(header_row)
306
+ print("-" * (row_header_width + 3 + (res_width + 3) * len(resolutions)))
307
+
308
+ for title in encoder_titles:
309
+ padding_needed = row_header_width - _get_display_width(title)
310
+ row_string = f"| {title}{' ' * padding_needed} |"
311
+ for res in resolutions:
312
+ status = results.get(title, {}).get(res, "skipped")
313
+ symbol = GREEN_CHECK if status == "succeeded" else RED_X if status == "failed" else GRAY_DASH
314
+ symbol_width = _get_display_width(symbol)
315
+ padding_left = (res_width - symbol_width) // 2
316
+ padding_right = res_width - symbol_width - padding_left
317
+ row_string += f" {' ' * padding_left}{symbol}{' ' * padding_right} |"
318
+ print(row_string)
319
+ print("-" * (row_header_width + 3 + (res_width + 3) * len(resolutions)))
320
+
321
+
322
+ def run_all_tests():
323
+ """Main function to run the entire test suite."""
324
+ print("Starting hardware codec detection test suite...")
325
+
326
+ if install_ffmpeg_if_needed() != 0:
327
+ print("Error: FFmpeg dependency not met. Please check installation.", file=sys.stderr)
328
+ return -1
329
+
330
+ temp_dir = os.path.join(tempfile.gettempdir(), "HwCodecDetect")
331
+ if os.path.exists(temp_dir):
332
+ # Clear previous run data to ensure a fresh test
333
+ shutil.rmtree(temp_dir)
334
+ os.makedirs(temp_dir)
335
+
336
+ encoder_results = _run_encoder_tests(temp_dir)
337
+ decoder_results = _run_decoder_tests(temp_dir)
338
+
339
+ all_results = {}
340
+ all_results.update(encoder_results)
341
+ all_results.update(decoder_results)
342
+
343
+ _print_summary_table(all_results)
344
+
345
+ print("\nCleaning up temporary files...")
346
+ shutil.rmtree(temp_dir)
347
+ print("Cleanup complete.")
348
+
349
+ return all_results
350
+
351
+ if __name__ == "__main__":
352
+ run_all_tests()
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.4
2
+ Name: HwCodecDetect
3
+ Version: 0.0.1
4
+ Summary: A cross-platform tool to automatically detect and test hardware video decoders/encoders using FFmpeg.
5
+ Author-email: whyb <whyber@outlook.com>
6
+ License-Expression: BSD-3-Clause
7
+ Project-URL: Homepage, https://github.com/whyb/HwCodecDetect
8
+ Project-URL: Issues, https://github.com/whyb/HwCodecDetect/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Topic :: Multimedia :: Video
12
+ Classifier: Topic :: System :: Installation/Setup
13
+ Classifier: Topic :: Utilities
14
+ Requires-Python: >=3.8
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Dynamic: license-file
18
+
19
+ # FFmpeg 硬件编解码器测试脚本
20
+ 在今天的编解码硬件加速的生态中,视频编解码技术面临着一个“百家争鸣”的局面。为了利用 GPU 的强大算力,各家硬件厂商都推出了自己的加速框架或编码器标准,例如 NVIDIA 的 NVEnc/NVDec、Intel 的 QSV 和 AMD 的 AMF。此外,操作系统层也提供了通用的接口,如微软的 Media Foundation 和 DXAV 、 D3D12VA ,而开源社区则发展了跨平台的 VAAPI 和 Vulkan 等标准。
21
+
22
+ 这种多样性虽然带来了技术上的进步,但也给普通用户和开发者带来了挑战。由于历史遗留和兼容性问题,一个硬件可能支持多种编码器,但它们在性能、支持的格式和分辨率方面都有所不同。因此,在使用 FFmpeg 进行硬件加速时,很难直观地知道哪种编码器最适合自己的设备。
23
+
24
+ 本项目正是为了解决这一痛点而生。它是一个用于自动化检测系统硬件视频编码器功能的便捷工具。它利用 FFmpeg,通过生成不同分辨率(从 240p 到 8K)的单帧视频文件,并尝试使用各种硬件编码器进行处理,以此来快速判断哪些硬件编码器在您的系统上可用以及它们所支持的分辨率。
25
+
26
+ ## 主要功能
27
+ ### 编码器
28
+ 脚本会自动测试并报告以下主流硬件编码器及其支持的格式:
29
+ | 编码器名称 | 支持的视频编码格式 |
30
+ |-----------------------------------|------------------------------------------- |
31
+ | NVEnc | H.264、H.265、AV1 |
32
+ | QSV (Quick Sync Video) | H.264、H.265、AV1、MJPEG、MPEG-2、VP9 |
33
+ | AMF (Advanced Media Framework) | H.264、H.265、AV1 |
34
+ | Media Foundation | H.264、H.265 |
35
+ | VAAPI (Video Acceleration API) | H.264、H.265、AV1、MJPEG、MPEG-2、VP8、VP9 |
36
+ | Vulkan | H.264、H.265 |
37
+
38
+ ### 解码器
39
+ 脚本会自动测试并报告以下主流硬件解码器及其支持的格式:
40
+ | 编码器名称 | 支持的视频编码格式 |
41
+ |------------------------------------------ |------------------------------------------------------------- |
42
+ | NVDec (CUVID) | H.264、H.265、AV1、MJPEG、MPEG-1、MPEG-2、MPEG-4、VP8、VP9 |
43
+ | QSV (Quick Sync Video) | H.264、H.265、AV1、MJPEG、MPEG-2、VP8、VP9 |
44
+ | AMF (Advanced Media Framework) | H.264、H.265、AV1 |
45
+ | DXVA2 (DirectX Video Acceleration) | H.264、H.265、MJPEG、MPEG-1、MPEG-2、MPEG-4、VP8 |
46
+ | D3D11VA (Direct3D 11 Video Acceleration) | H.264、H.265、AV1、MJPEG、MPEG-1、MPEG-2、MPEG-4、VP8、VP9 |
47
+
48
+
49
+ ## 如何使用
50
+
51
+ ### 1. 克隆仓库
52
+ 首先,克隆此仓库到你的本地。
53
+ ```bash
54
+ git clone https://github.com/whyb/HwCodecDetect.git
55
+ cd HwCodecDetect
56
+ ```
57
+
58
+ ### 2. 运行测试
59
+ ```bash
60
+ python -m HwCodecDetect.run_tests
61
+ ```
62
+
63
+
64
+ ## 效果展示
65
+ 下面是本地运行测试的可能的结果:
66
+
67
+ ![decoder test result](imgs/decoder.png)
68
+
69
+
70
+ ![encoder test result](imgs/encoder.png)
@@ -0,0 +1,9 @@
1
+ HwCodecDetect/__init__.py,sha256=2ymTSqSTlUPK_QexrL4dxxL8N7tkqPdaWfFA56R83LY,134
2
+ HwCodecDetect/install_ffmpeg_if_needed.py,sha256=y1T8hZ-_sPZdxEDIj3lYPsfk3mhlIc2pG58k3xiqw0Q,7312
3
+ HwCodecDetect/run_tests.py,sha256=I3M9PNbkbo6cAiaPAp-tzG83X78fqJ1-IhX2ER-b4Kc,16619
4
+ hwcodecdetect-0.0.1.dist-info/licenses/LICENSE,sha256=MbDIouN72hBze3wjpi9OjjvDtTpnF7HmRo3rJ3CQX0U,1496
5
+ hwcodecdetect-0.0.1.dist-info/METADATA,sha256=1ETxxM8dkW_xfHGuE0oWwk5hg-sJlVV62P6UYah6xa4,4034
6
+ hwcodecdetect-0.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
+ hwcodecdetect-0.0.1.dist-info/entry_points.txt,sha256=KdclE06P1pTzQLgtBWwsEkqNKf3dzvzPxUZDs19g0KQ,72
8
+ hwcodecdetect-0.0.1.dist-info/top_level.txt,sha256=Vm6bhU7wKyxnNL_3ndZ7bHlt-shBFhfEPIeeBDKz_Ag,14
9
+ hwcodecdetect-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ hwcodecdetect = HwCodecDetect.run_tests:run_all_tests
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2025, 張小凡
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1 @@
1
+ HwCodecDetect