diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c9934bf --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +venv/ +ENV/ +*.egg-info/ +dist/ +build/ + +# Test videos +*.mp4 +*.mkv +*.avi +*.mov + +# OS +.DS_Store +Thumbs.db + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ diff --git a/README.md b/README.md index e69de29..f79cfdc 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,176 @@ +# Video Transcoding Benchmark + +A simple CLI tool to benchmark video transcoding performance on your system. Measures how many simultaneous 1080p video streams can be transcoded in real-time or better. + +## Features + +- **Hardware Acceleration Support**: Automatically detects and uses available hardware acceleration: + - NVIDIA NVENC (CUDA) + - Intel Quick Sync Video (QSV) + - AMD AMF/VCE + - Apple VideoToolbox (macOS/iOS ARM) + - VA-API (Linux Intel/AMD) + - Falls back to software encoding (libx264) if no hardware acceleration available + +- **Cross-Platform**: Works on Linux, macOS, and Windows +- **Cross-Architecture**: Supports both Intel/AMD x86_64 and ARM architectures +- **Simple Output**: Produces a single number representing transcoding capacity + +## Requirements + +- Python 3.6+ +- FFmpeg (with hardware acceleration support compiled in if desired) + +## Installation + +### 1. Install FFmpeg + +#### Ubuntu/Debian +```bash +sudo apt update +sudo apt install ffmpeg +``` + +For hardware acceleration support: +```bash +# Intel QSV +sudo apt install intel-media-va-driver-non-free + +# NVIDIA NVENC (requires NVIDIA drivers) +# Already supported if NVIDIA drivers are installed + +# AMD VA-API +sudo apt install mesa-va-drivers +``` + +#### macOS +```bash +brew install ffmpeg +``` + +#### Windows +Download from [ffmpeg.org](https://ffmpeg.org/download.html) or use: +```bash +choco install ffmpeg +``` + +### 2. Make the script executable (Linux/macOS) +```bash +chmod +x transcode_bench.py +``` + +## Usage + +### Basic Usage +```bash +python3 transcode_bench.py +``` + +This will: +1. Detect available hardware acceleration +2. Generate a 30-second 1080p test video +3. Run benchmarks to find the maximum number of simultaneous streams +4. Output the result + +### Command-Line Options + +```bash +python3 transcode_bench.py [OPTIONS] + +Options: + --duration SECONDS Test video duration (default: 30) + --input FILE Use existing video file instead of generating one + --min-fps FPS Minimum FPS to consider real-time (default: 30.0) + --help Show help message +``` + +### Examples + +**Use a shorter test video (faster benchmark):** +```bash +python3 transcode_bench.py --duration 10 +``` + +**Use your own video file:** +```bash +python3 transcode_bench.py --input /path/to/your/video.mp4 +``` + +**Set a different real-time threshold (e.g., 60fps):** +```bash +python3 transcode_bench.py --min-fps 60 +``` + +## Output + +The benchmark will display progress as it searches for the maximum number of streams, then output a final score: + +``` +============================================================== +Video Transcoding Benchmark +============================================================== +Detected acceleration: NVIDIA NVENC +Encoder: h264_nvenc +Generating 30s 1080p test video... + +Benchmarking with NVIDIA NVENC... +Finding maximum simultaneous 1080p streams at real-time or better... + +Testing 32 simultaneous streams...  (avg 87.3 fps) +Testing 48 simultaneous streams...  (avg 58.2 fps) +Testing 56 simultaneous streams...  (avg 24.1 fps - below real-time) +Testing 52 simultaneous streams...  (avg 45.7 fps) +Testing 54 simultaneous streams...  (avg 32.8 fps) +Testing 55 simultaneous streams...  (avg 28.9 fps - below real-time) + +============================================================== +BENCHMARK RESULTS +============================================================== +Hardware Acceleration: NVIDIA NVENC +Maximum Simultaneous 1080p Streams: 54 +(at 30.0 FPS or better) +============================================================== + +Benchmark Score: 54 +``` + +## How It Works + +1. **Detection**: Scans for available hardware encoders in your FFmpeg build +2. **Test Video**: Generates a synthetic 1080p video with test patterns +3. **Binary Search**: Uses binary search to efficiently find the maximum number of streams that can be transcoded simultaneously while maintaining real-time performance (e30 FPS) +4. **Parallel Execution**: Runs multiple FFmpeg processes in parallel to simulate concurrent transcoding workloads + +## Interpreting Results + +The "Benchmark Score" represents how many 1080p video streams your system can transcode simultaneously while maintaining real-time or better performance. This is useful for: + +- Comparing hardware performance +- Capacity planning for video processing workloads +- Evaluating hardware acceleration effectiveness + +**Typical scores:** +- Software (CPU only): 1-4 streams +- Entry-level GPU: 5-15 streams +- Mid-range GPU: 15-40 streams +- High-end GPU: 40-100+ streams + +## Troubleshooting + +**FFmpeg not found:** +- Ensure FFmpeg is installed and in your system PATH +- Try running `ffmpeg -version` to verify + +**Hardware acceleration not detected:** +- Verify your FFmpeg build includes hardware encoder support: `ffmpeg -encoders | grep ` +- Ensure proper drivers are installed (NVIDIA, Intel, AMD) +- On Linux, check that you have access to `/dev/dri/renderD128` for VA-API + +**Benchmark fails or hangs:** +- Try a shorter duration: `--duration 10` +- Check system resources (CPU, memory, GPU) +- Verify FFmpeg works manually: `ffmpeg -i input.mp4 -c:v h264_nvenc output.mp4` + +## License + +MIT License diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..48ea8e3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +# No Python package dependencies required +# This tool only requires: +# - Python 3.6+ +# - FFmpeg (system package) diff --git a/transcode_bench.py b/transcode_bench.py new file mode 100755 index 0000000..654200b --- /dev/null +++ b/transcode_bench.py @@ -0,0 +1,318 @@ +#!/usr/bin/env python3 +""" +Video Transcoding Benchmark Tool +Measures how many simultaneous 1080p streams can be transcoded in real-time or better. +""" + +import subprocess +import sys +import time +import tempfile +import os +import threading +import argparse +from pathlib import Path +from typing import Optional, Tuple, List + + +class HardwareAcceleration: + """Detect and configure hardware acceleration.""" + + @staticmethod + def detect() -> Tuple[str, str, str]: + """ + Detect available hardware acceleration. + Returns: (name, encoder, hwaccel_args) + """ + # Check NVIDIA NVENC + if HardwareAcceleration._check_encoder('h264_nvenc'): + return ('NVIDIA NVENC', 'h264_nvenc', '-hwaccel cuda -hwaccel_output_format cuda') + + # Check Intel QSV + if HardwareAcceleration._check_encoder('h264_qsv'): + return ('Intel Quick Sync', 'h264_qsv', '-hwaccel qsv -hwaccel_output_format qsv') + + # Check AMD AMF (Windows/Linux) + if HardwareAcceleration._check_encoder('h264_amf'): + return ('AMD AMF', 'h264_amf', '') + + # Check VideoToolbox (macOS/iOS - ARM) + if HardwareAcceleration._check_encoder('h264_videotoolbox'): + return ('VideoToolbox', 'h264_videotoolbox', '-hwaccel videotoolbox') + + # Check VA-API (Linux Intel/AMD) + if HardwareAcceleration._check_encoder('h264_vaapi'): + return ('VA-API', 'h264_vaapi', '-hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_device /dev/dri/renderD128') + + # Fallback to software + return ('Software (libx264)', 'libx264', '') + + @staticmethod + def _check_encoder(encoder: str) -> bool: + """Check if FFmpeg supports a specific encoder.""" + try: + result = subprocess.run( + ['ffmpeg', '-hide_banner', '-encoders'], + capture_output=True, + text=True, + timeout=5 + ) + return encoder in result.stdout + except (subprocess.TimeoutExpired, FileNotFoundError): + return False + + +class TestVideo: + """Generate or manage test video file.""" + + @staticmethod + def generate(path: str, duration: int = 30) -> bool: + """Generate a 1080p test video.""" + try: + print(f"Generating {duration}s 1080p test video...") + cmd = [ + 'ffmpeg', '-y', + '-f', 'lavfi', + '-i', 'testsrc2=size=1920x1080:rate=30:duration={}'.format(duration), + '-f', 'lavfi', + '-i', 'sine=frequency=1000:duration={}'.format(duration), + '-c:v', 'libx264', + '-preset', 'medium', + '-c:a', 'aac', + '-b:a', '128k', + path + ] + result = subprocess.run(cmd, capture_output=True, timeout=120) + return result.returncode == 0 + except Exception as e: + print(f"Error generating test video: {e}") + return False + + +class TranscodeJob: + """Represents a single transcode job.""" + + def __init__(self, input_file: str, output_file: str, encoder: str, hwaccel_args: str): + self.input_file = input_file + self.output_file = output_file + self.encoder = encoder + self.hwaccel_args = hwaccel_args + self.process = None + self.start_time = None + self.end_time = None + self.success = False + self.fps = 0.0 + + def run(self): + """Execute the transcode job.""" + try: + # Build FFmpeg command + cmd = ['ffmpeg', '-y'] + + # Add hardware acceleration args + if self.hwaccel_args: + cmd.extend(self.hwaccel_args.split()) + + cmd.extend([ + '-i', self.input_file, + '-c:v', self.encoder, + '-b:v', '4M', + '-c:a', 'aac', + '-b:a', '128k', + '-f', 'null' if os.name != 'nt' else 'null', + self.output_file if os.name != 'nt' else '-' + ]) + + self.start_time = time.time() + self.process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + # Wait for completion + _, stderr = self.process.communicate() + self.end_time = time.time() + + # Parse FPS from FFmpeg output + self.fps = self._parse_fps(stderr) + self.success = self.process.returncode == 0 + + except Exception as e: + self.success = False + self.end_time = time.time() + + def _parse_fps(self, ffmpeg_output: str) -> float: + """Parse average FPS from FFmpeg output.""" + try: + # Look for the final fps value in output + for line in ffmpeg_output.split('\n'): + if 'fps=' in line: + fps_str = line.split('fps=')[1].split()[0] + return float(fps_str) + except: + pass + return 0.0 + + +class Benchmark: + """Main benchmark orchestrator.""" + + def __init__(self, test_video: str, encoder: str, hwaccel_args: str, accel_name: str): + self.test_video = test_video + self.encoder = encoder + self.hwaccel_args = hwaccel_args + self.accel_name = accel_name + + def run_parallel_transcodes(self, num_streams: int, timeout: int = 60) -> Tuple[bool, float]: + """ + Run multiple parallel transcode jobs. + Returns: (success, average_fps) + """ + jobs = [] + threads = [] + + # Create jobs + for i in range(num_streams): + output_file = f'/dev/null' if os.name != 'nt' else 'NUL' + job = TranscodeJob(self.test_video, output_file, self.encoder, self.hwaccel_args) + jobs.append(job) + + # Start all jobs in parallel + for job in jobs: + thread = threading.Thread(target=job.run) + thread.start() + threads.append(thread) + + # Wait for all to complete (with timeout) + start = time.time() + for thread in threads: + remaining = timeout - (time.time() - start) + if remaining > 0: + thread.join(timeout=remaining) + else: + return False, 0.0 + + # Check if all succeeded and calculate average FPS + all_success = all(job.success for job in jobs) + if all_success: + avg_fps = sum(job.fps for job in jobs) / len(jobs) if jobs else 0.0 + return True, avg_fps + + return False, 0.0 + + def find_max_streams(self, min_fps: float = 30.0) -> int: + """ + Use binary search to find maximum number of simultaneous streams. + A stream is considered "real-time" if it achieves >= min_fps. + """ + print(f"\nBenchmarking with {self.accel_name}...") + print("Finding maximum simultaneous 1080p streams at real-time or better...\n") + + # Binary search bounds + low, high = 1, 64 + max_streams = 0 + + while low <= high: + mid = (low + high) // 2 + print(f"Testing {mid} simultaneous streams...", end=' ', flush=True) + + success, avg_fps = self.run_parallel_transcodes(mid) + + if success and avg_fps >= min_fps: + print(f"✓ (avg {avg_fps:.1f} fps)") + max_streams = mid + low = mid + 1 + else: + if success: + print(f"✗ (avg {avg_fps:.1f} fps - below real-time)") + else: + print(f"✗ (failed)") + high = mid - 1 + + return max_streams + + +def main(): + parser = argparse.ArgumentParser( + description='Benchmark video transcoding performance', + formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument( + '--duration', + type=int, + default=30, + help='Test video duration in seconds (default: 30)' + ) + parser.add_argument( + '--input', + type=str, + help='Use existing video file instead of generating test video' + ) + parser.add_argument( + '--min-fps', + type=float, + default=30.0, + help='Minimum FPS to consider real-time (default: 30.0)' + ) + + args = parser.parse_args() + + # Check for FFmpeg + try: + subprocess.run(['ffmpeg', '-version'], capture_output=True, check=True) + except (subprocess.CalledProcessError, FileNotFoundError): + print("Error: FFmpeg not found. Please install FFmpeg and ensure it's in your PATH.") + return 1 + + print("=" * 60) + print("Video Transcoding Benchmark") + print("=" * 60) + + # Detect hardware acceleration + accel_name, encoder, hwaccel_args = HardwareAcceleration.detect() + print(f"Detected acceleration: {accel_name}") + print(f"Encoder: {encoder}") + + # Prepare test video + if args.input: + test_video = args.input + if not os.path.exists(test_video): + print(f"Error: Input file '{test_video}' not found.") + return 1 + else: + with tempfile.NamedTemporaryFile(suffix='.mp4', delete=False) as f: + test_video = f.name + + if not TestVideo.generate(test_video, args.duration): + print("Error: Failed to generate test video.") + return 1 + + try: + # Run benchmark + benchmark = Benchmark(test_video, encoder, hwaccel_args, accel_name) + max_streams = benchmark.find_max_streams(args.min_fps) + + # Display results + print("\n" + "=" * 60) + print("BENCHMARK RESULTS") + print("=" * 60) + print(f"Hardware Acceleration: {accel_name}") + print(f"Maximum Simultaneous 1080p Streams: {max_streams}") + print(f"(at {args.min_fps} FPS or better)") + print("=" * 60) + + # Also output just the number for easy parsing + print(f"\nBenchmark Score: {max_streams}") + + finally: + # Cleanup + if not args.input and os.path.exists(test_video): + os.unlink(test_video) + + return 0 + + +if __name__ == '__main__': + sys.exit(main())