Update README.md
This commit is contained in:
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal file
@@ -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
|
||||||
|
*~
|
176
README.md
176
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 <encoder>`
|
||||||
|
- 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
|
||||||
|
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# No Python package dependencies required
|
||||||
|
# This tool only requires:
|
||||||
|
# - Python 3.6+
|
||||||
|
# - FFmpeg (system package)
|
318
transcode_bench.py
Executable file
318
transcode_bench.py
Executable file
@@ -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())
|
Reference in New Issue
Block a user