mirror of
https://github.com/pacnpal/Pac-cogs.git
synced 2025-12-20 10:51:05 -05:00
Fix ffmpeg
This commit is contained in:
@@ -83,7 +83,17 @@ class FFmpegManager:
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.base_path = Path(__file__).parent / "bin"
|
self.base_path = Path(__file__).parent / "bin"
|
||||||
self.base_path.mkdir(exist_ok=True)
|
|
||||||
|
# Create bin directory with proper permissions if it doesn't exist
|
||||||
|
try:
|
||||||
|
self.base_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
if platform.system() != "Windows":
|
||||||
|
# Set directory permissions to rwxr-xr-x (755)
|
||||||
|
self.base_path.chmod(0o755)
|
||||||
|
logger.info(f"Created bin directory with permissions: {oct(self.base_path.stat().st_mode)[-3:]}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to create/set permissions on bin directory: {e}")
|
||||||
|
raise FFmpegError(f"Failed to initialize FFmpeg directory: {e}")
|
||||||
|
|
||||||
# Get system architecture
|
# Get system architecture
|
||||||
self.system = platform.system()
|
self.system = platform.system()
|
||||||
@@ -96,27 +106,20 @@ class FFmpegManager:
|
|||||||
if system_ffmpeg:
|
if system_ffmpeg:
|
||||||
self.ffmpeg_path = Path(system_ffmpeg)
|
self.ffmpeg_path = Path(system_ffmpeg)
|
||||||
logger.info(f"Using system FFmpeg: {self.ffmpeg_path}")
|
logger.info(f"Using system FFmpeg: {self.ffmpeg_path}")
|
||||||
else:
|
return
|
||||||
# Check for existing FFmpeg in our bin directory
|
|
||||||
try:
|
# Check for existing FFmpeg in our bin directory
|
||||||
arch_config = self.FFMPEG_URLS[self.system][self.machine]
|
try:
|
||||||
self.ffmpeg_path = self.base_path / arch_config["bin_name"]
|
arch_config = self.FFMPEG_URLS[self.system][self.machine]
|
||||||
if not self.ffmpeg_path.exists():
|
self.ffmpeg_path = self.base_path / arch_config["bin_name"]
|
||||||
# Only download if FFmpeg doesn't exist
|
|
||||||
self._download_ffmpeg()
|
if not self.ffmpeg_path.exists() or not self._verify_ffmpeg():
|
||||||
if not self._verify_ffmpeg():
|
logger.info("Downloading FFmpeg...")
|
||||||
raise FFmpegError("Downloaded FFmpeg binary is not functional")
|
self._download_ffmpeg()
|
||||||
elif not self._verify_ffmpeg():
|
if not self._verify_ffmpeg():
|
||||||
logger.warning(
|
raise FFmpegError("Downloaded FFmpeg binary is not functional")
|
||||||
"Existing FFmpeg binary not functional, downloading new copy"
|
except KeyError:
|
||||||
)
|
raise FFmpegError(f"Unsupported system/architecture: {self.system}/{self.machine}")
|
||||||
self._download_ffmpeg()
|
|
||||||
if not self._verify_ffmpeg():
|
|
||||||
raise FFmpegError("Downloaded FFmpeg binary is not functional")
|
|
||||||
except KeyError:
|
|
||||||
raise FFmpegError(
|
|
||||||
f"Unsupported system/architecture: {self.system}/{self.machine}"
|
|
||||||
)
|
|
||||||
|
|
||||||
self._gpu_info = self._detect_gpu()
|
self._gpu_info = self._detect_gpu()
|
||||||
self._cpu_cores = multiprocessing.cpu_count()
|
self._cpu_cores = multiprocessing.cpu_count()
|
||||||
@@ -131,14 +134,15 @@ class FFmpegManager:
|
|||||||
# Make binary executable on Unix systems
|
# Make binary executable on Unix systems
|
||||||
if self.system != "Windows":
|
if self.system != "Windows":
|
||||||
try:
|
try:
|
||||||
self.ffmpeg_path.chmod(
|
current_mode = self.ffmpeg_path.stat().st_mode
|
||||||
self.ffmpeg_path.stat().st_mode | stat.S_IEXEC
|
# Add execute permission for user (100), maintaining existing permissions
|
||||||
)
|
new_mode = current_mode | stat.S_IXUSR | stat.S_IRUSR | stat.S_IWUSR
|
||||||
|
self.ffmpeg_path.chmod(new_mode)
|
||||||
|
logger.info(f"Set FFmpeg permissions to: {oct(new_mode)[-3:]}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.error(f"Failed to set FFmpeg executable permissions: {e}")
|
||||||
f"Failed to set FFmpeg executable permissions: {str(e)}"
|
# Continue anyway as the file might already have correct permissions
|
||||||
)
|
pass
|
||||||
return False
|
|
||||||
|
|
||||||
# Test FFmpeg and check for required encoders
|
# Test FFmpeg and check for required encoders
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
@@ -168,12 +172,16 @@ class FFmpegManager:
|
|||||||
if encoder not in encoders:
|
if encoder not in encoders:
|
||||||
logger.warning(f"Required encoder {encoder} not available")
|
logger.warning(f"Required encoder {encoder} not available")
|
||||||
if encoder != "libx264": # Only warn for GPU encoders
|
if encoder != "libx264": # Only warn for GPU encoders
|
||||||
self._gpu_info[encoder.split('_')[1].replace('h264', '')] = False
|
self._gpu_info[
|
||||||
|
encoder.split("_")[1].replace("h264", "")
|
||||||
|
] = False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"FFmpeg verification attempt {attempt + 1} failed: {str(e)}")
|
logger.error(
|
||||||
|
f"FFmpeg verification attempt {attempt + 1} failed: {e}"
|
||||||
|
)
|
||||||
if attempt < self.MAX_RETRIES - 1:
|
if attempt < self.MAX_RETRIES - 1:
|
||||||
time.sleep(self.RETRY_DELAY)
|
time.sleep(self.RETRY_DELAY)
|
||||||
else:
|
else:
|
||||||
@@ -200,13 +208,19 @@ class FFmpegManager:
|
|||||||
# Verify NVENC functionality
|
# Verify NVENC functionality
|
||||||
test_cmd = [
|
test_cmd = [
|
||||||
str(self.ffmpeg_path),
|
str(self.ffmpeg_path),
|
||||||
"-f", "lavfi",
|
"-f",
|
||||||
"-i", "testsrc=duration=1:size=1280x720:rate=30",
|
"lavfi",
|
||||||
"-c:v", "h264_nvenc",
|
"-i",
|
||||||
"-f", "null",
|
"testsrc=duration=1:size=1280x720:rate=30",
|
||||||
"-"
|
"-c:v",
|
||||||
|
"h264_nvenc",
|
||||||
|
"-f",
|
||||||
|
"null",
|
||||||
|
"-",
|
||||||
]
|
]
|
||||||
result = subprocess.run(test_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
result = subprocess.run(
|
||||||
|
test_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||||
|
)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
gpu_info["nvidia"] = True
|
gpu_info["nvidia"] = True
|
||||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||||
@@ -221,13 +235,21 @@ class FFmpegManager:
|
|||||||
# Verify AMF functionality
|
# Verify AMF functionality
|
||||||
test_cmd = [
|
test_cmd = [
|
||||||
str(self.ffmpeg_path),
|
str(self.ffmpeg_path),
|
||||||
"-f", "lavfi",
|
"-f",
|
||||||
"-i", "testsrc=duration=1:size=1280x720:rate=30",
|
"lavfi",
|
||||||
"-c:v", "h264_amf",
|
"-i",
|
||||||
"-f", "null",
|
"testsrc=duration=1:size=1280x720:rate=30",
|
||||||
"-"
|
"-c:v",
|
||||||
|
"h264_amf",
|
||||||
|
"-f",
|
||||||
|
"null",
|
||||||
|
"-",
|
||||||
]
|
]
|
||||||
result = subprocess.run(test_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
result = subprocess.run(
|
||||||
|
test_cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
gpu_info["amd"] = True
|
gpu_info["amd"] = True
|
||||||
except (IOError, OSError):
|
except (IOError, OSError):
|
||||||
@@ -246,13 +268,19 @@ class FFmpegManager:
|
|||||||
# Verify QSV functionality
|
# Verify QSV functionality
|
||||||
test_cmd = [
|
test_cmd = [
|
||||||
str(self.ffmpeg_path),
|
str(self.ffmpeg_path),
|
||||||
"-f", "lavfi",
|
"-f",
|
||||||
"-i", "testsrc=duration=1:size=1280x720:rate=30",
|
"lavfi",
|
||||||
"-c:v", "h264_qsv",
|
"-i",
|
||||||
"-f", "null",
|
"testsrc=duration=1:size=1280x720:rate=30",
|
||||||
"-"
|
"-c:v",
|
||||||
|
"h264_qsv",
|
||||||
|
"-f",
|
||||||
|
"null",
|
||||||
|
"-",
|
||||||
]
|
]
|
||||||
result = subprocess.run(test_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
result = subprocess.run(
|
||||||
|
test_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||||
|
)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
gpu_info["intel"] = True
|
gpu_info["intel"] = True
|
||||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||||
@@ -270,7 +298,7 @@ class FFmpegManager:
|
|||||||
["powershell", "-Command", ps_command],
|
["powershell", "-Command", ps_command],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=10
|
timeout=10,
|
||||||
)
|
)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
gpu_data = json.loads(result.stdout)
|
gpu_data = json.loads(result.stdout)
|
||||||
@@ -283,39 +311,63 @@ class FFmpegManager:
|
|||||||
# Verify NVENC
|
# Verify NVENC
|
||||||
test_cmd = [
|
test_cmd = [
|
||||||
str(self.ffmpeg_path),
|
str(self.ffmpeg_path),
|
||||||
"-f", "lavfi",
|
"-f",
|
||||||
"-i", "testsrc=duration=1:size=1280x720:rate=30",
|
"lavfi",
|
||||||
"-c:v", "h264_nvenc",
|
"-i",
|
||||||
"-f", "null",
|
"testsrc=duration=1:size=1280x720:rate=30",
|
||||||
"-"
|
"-c:v",
|
||||||
|
"h264_nvenc",
|
||||||
|
"-f",
|
||||||
|
"null",
|
||||||
|
"-",
|
||||||
]
|
]
|
||||||
result = subprocess.run(test_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
result = subprocess.run(
|
||||||
|
test_cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
gpu_info["nvidia"] = True
|
gpu_info["nvidia"] = True
|
||||||
if "amd" in name or "radeon" in name:
|
if "amd" in name or "radeon" in name:
|
||||||
# Verify AMF
|
# Verify AMF
|
||||||
test_cmd = [
|
test_cmd = [
|
||||||
str(self.ffmpeg_path),
|
str(self.ffmpeg_path),
|
||||||
"-f", "lavfi",
|
"-f",
|
||||||
"-i", "testsrc=duration=1:size=1280x720:rate=30",
|
"lavfi",
|
||||||
"-c:v", "h264_amf",
|
"-i",
|
||||||
"-f", "null",
|
"testsrc=duration=1:size=1280x720:rate=30",
|
||||||
"-"
|
"-c:v",
|
||||||
|
"h264_amf",
|
||||||
|
"-f",
|
||||||
|
"null",
|
||||||
|
"-",
|
||||||
]
|
]
|
||||||
result = subprocess.run(test_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
result = subprocess.run(
|
||||||
|
test_cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
gpu_info["amd"] = True
|
gpu_info["amd"] = True
|
||||||
if "intel" in name:
|
if "intel" in name:
|
||||||
# Verify QSV
|
# Verify QSV
|
||||||
test_cmd = [
|
test_cmd = [
|
||||||
str(self.ffmpeg_path),
|
str(self.ffmpeg_path),
|
||||||
"-f", "lavfi",
|
"-f",
|
||||||
"-i", "testsrc=duration=1:size=1280x720:rate=30",
|
"lavfi",
|
||||||
"-c:v", "h264_qsv",
|
"-i",
|
||||||
"-f", "null",
|
"testsrc=duration=1:size=1280x720:rate=30",
|
||||||
"-"
|
"-c:v",
|
||||||
|
"h264_qsv",
|
||||||
|
"-f",
|
||||||
|
"null",
|
||||||
|
"-",
|
||||||
]
|
]
|
||||||
result = subprocess.run(test_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
result = subprocess.run(
|
||||||
|
test_cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
gpu_info["intel"] = True
|
gpu_info["intel"] = True
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -336,37 +388,61 @@ class FFmpegManager:
|
|||||||
if "nvidia" in content:
|
if "nvidia" in content:
|
||||||
test_cmd = [
|
test_cmd = [
|
||||||
str(self.ffmpeg_path),
|
str(self.ffmpeg_path),
|
||||||
"-f", "lavfi",
|
"-f",
|
||||||
"-i", "testsrc=duration=1:size=1280x720:rate=30",
|
"lavfi",
|
||||||
"-c:v", "h264_nvenc",
|
"-i",
|
||||||
"-f", "null",
|
"testsrc=duration=1:size=1280x720:rate=30",
|
||||||
"-"
|
"-c:v",
|
||||||
|
"h264_nvenc",
|
||||||
|
"-f",
|
||||||
|
"null",
|
||||||
|
"-",
|
||||||
]
|
]
|
||||||
result = subprocess.run(test_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
result = subprocess.run(
|
||||||
|
test_cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
gpu_info["nvidia"] = True
|
gpu_info["nvidia"] = True
|
||||||
if "amd" in content or "radeon" in content:
|
if "amd" in content or "radeon" in content:
|
||||||
test_cmd = [
|
test_cmd = [
|
||||||
str(self.ffmpeg_path),
|
str(self.ffmpeg_path),
|
||||||
"-f", "lavfi",
|
"-f",
|
||||||
"-i", "testsrc=duration=1:size=1280x720:rate=30",
|
"lavfi",
|
||||||
"-c:v", "h264_amf",
|
"-i",
|
||||||
"-f", "null",
|
"testsrc=duration=1:size=1280x720:rate=30",
|
||||||
"-"
|
"-c:v",
|
||||||
|
"h264_amf",
|
||||||
|
"-f",
|
||||||
|
"null",
|
||||||
|
"-",
|
||||||
]
|
]
|
||||||
result = subprocess.run(test_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
result = subprocess.run(
|
||||||
|
test_cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
gpu_info["amd"] = True
|
gpu_info["amd"] = True
|
||||||
if "intel" in content:
|
if "intel" in content:
|
||||||
test_cmd = [
|
test_cmd = [
|
||||||
str(self.ffmpeg_path),
|
str(self.ffmpeg_path),
|
||||||
"-f", "lavfi",
|
"-f",
|
||||||
"-i", "testsrc=duration=1:size=1280x720:rate=30",
|
"lavfi",
|
||||||
"-c:v", "h264_qsv",
|
"-i",
|
||||||
"-f", "null",
|
"testsrc=duration=1:size=1280x720:rate=30",
|
||||||
"-"
|
"-c:v",
|
||||||
|
"h264_qsv",
|
||||||
|
"-f",
|
||||||
|
"null",
|
||||||
|
"-",
|
||||||
]
|
]
|
||||||
result = subprocess.run(test_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
result = subprocess.run(
|
||||||
|
test_cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
gpu_info["intel"] = True
|
gpu_info["intel"] = True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -381,14 +457,14 @@ class FFmpegManager:
|
|||||||
"""Analyze video content for optimal encoding settings"""
|
"""Analyze video content for optimal encoding settings"""
|
||||||
try:
|
try:
|
||||||
probe = ffmpeg.probe(input_path)
|
probe = ffmpeg.probe(input_path)
|
||||||
video_info = next(s for s in probe['streams'] if s['codec_type'] == 'video')
|
video_info = next(s for s in probe["streams"] if s["codec_type"] == "video")
|
||||||
|
|
||||||
# Get video properties
|
# Get video properties
|
||||||
width = int(video_info.get('width', 0))
|
width = int(video_info.get("width", 0))
|
||||||
height = int(video_info.get('height', 0))
|
height = int(video_info.get("height", 0))
|
||||||
fps = eval(video_info.get('r_frame_rate', '30/1'))
|
fps = eval(video_info.get("r_frame_rate", "30/1"))
|
||||||
duration = float(probe['format'].get('duration', 0))
|
duration = float(probe["format"].get("duration", 0))
|
||||||
bitrate = float(probe['format'].get('bit_rate', 0))
|
bitrate = float(probe["format"].get("bit_rate", 0))
|
||||||
|
|
||||||
# Advanced analysis
|
# Advanced analysis
|
||||||
has_high_motion = False
|
has_high_motion = False
|
||||||
@@ -396,8 +472,8 @@ class FFmpegManager:
|
|||||||
has_complex_scenes = False
|
has_complex_scenes = False
|
||||||
|
|
||||||
# Analyze frame statistics if available
|
# Analyze frame statistics if available
|
||||||
if video_info.get('avg_frame_rate'):
|
if video_info.get("avg_frame_rate"):
|
||||||
avg_fps = eval(video_info['avg_frame_rate'])
|
avg_fps = eval(video_info["avg_frame_rate"])
|
||||||
if abs(avg_fps - fps) > 5: # Significant frame rate variation
|
if abs(avg_fps - fps) > 5: # Significant frame rate variation
|
||||||
has_high_motion = True
|
has_high_motion = True
|
||||||
|
|
||||||
@@ -408,19 +484,24 @@ class FFmpegManager:
|
|||||||
frames_file = os.path.join(temp_dir, "frames.txt")
|
frames_file = os.path.join(temp_dir, "frames.txt")
|
||||||
sample_cmd = [
|
sample_cmd = [
|
||||||
str(self.ffmpeg_path),
|
str(self.ffmpeg_path),
|
||||||
"-i", input_path,
|
"-i",
|
||||||
"-vf", "select='eq(pict_type,I)',signalstats",
|
input_path,
|
||||||
"-show_entries", "frame_tags=lavfi.signalstats.YAVG",
|
"-vf",
|
||||||
"-f", "null", "-"
|
"select='eq(pict_type,I)',signalstats",
|
||||||
|
"-show_entries",
|
||||||
|
"frame_tags=lavfi.signalstats.YAVG",
|
||||||
|
"-f",
|
||||||
|
"null",
|
||||||
|
"-",
|
||||||
]
|
]
|
||||||
result = subprocess.run(sample_cmd, capture_output=True, text=True)
|
result = subprocess.run(sample_cmd, capture_output=True, text=True)
|
||||||
|
|
||||||
# Analyze brightness levels
|
# Analyze brightness levels
|
||||||
dark_frames = 0
|
dark_frames = 0
|
||||||
total_frames = 0
|
total_frames = 0
|
||||||
for line in result.stderr.split('\n'):
|
for line in result.stderr.split("\n"):
|
||||||
if 'YAVG' in line:
|
if "YAVG" in line:
|
||||||
avg_brightness = float(line.split('=')[1])
|
avg_brightness = float(line.split("=")[1])
|
||||||
if avg_brightness < 40: # Dark scene threshold
|
if avg_brightness < 40: # Dark scene threshold
|
||||||
dark_frames += 1
|
dark_frames += 1
|
||||||
total_frames += 1
|
total_frames += 1
|
||||||
@@ -431,27 +512,29 @@ class FFmpegManager:
|
|||||||
logger.warning(f"Advanced scene analysis failed: {str(e)}")
|
logger.warning(f"Advanced scene analysis failed: {str(e)}")
|
||||||
|
|
||||||
# Get audio properties
|
# Get audio properties
|
||||||
audio_info = next((s for s in probe['streams'] if s['codec_type'] == 'audio'), None)
|
audio_info = next(
|
||||||
|
(s for s in probe["streams"] if s["codec_type"] == "audio"), None
|
||||||
|
)
|
||||||
audio_bitrate = 0
|
audio_bitrate = 0
|
||||||
audio_channels = 2
|
audio_channels = 2
|
||||||
audio_sample_rate = 48000
|
audio_sample_rate = 48000
|
||||||
if audio_info:
|
if audio_info:
|
||||||
audio_bitrate = int(audio_info.get('bit_rate', 0))
|
audio_bitrate = int(audio_info.get("bit_rate", 0))
|
||||||
audio_channels = int(audio_info.get('channels', 2))
|
audio_channels = int(audio_info.get("channels", 2))
|
||||||
audio_sample_rate = int(audio_info.get('sample_rate', 48000))
|
audio_sample_rate = int(audio_info.get("sample_rate", 48000))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'width': width,
|
"width": width,
|
||||||
'height': height,
|
"height": height,
|
||||||
'fps': fps,
|
"fps": fps,
|
||||||
'duration': duration,
|
"duration": duration,
|
||||||
'bitrate': bitrate,
|
"bitrate": bitrate,
|
||||||
'has_high_motion': has_high_motion,
|
"has_high_motion": has_high_motion,
|
||||||
'has_dark_scenes': has_dark_scenes,
|
"has_dark_scenes": has_dark_scenes,
|
||||||
'has_complex_scenes': has_complex_scenes,
|
"has_complex_scenes": has_complex_scenes,
|
||||||
'audio_bitrate': audio_bitrate,
|
"audio_bitrate": audio_bitrate,
|
||||||
'audio_channels': audio_channels,
|
"audio_channels": audio_channels,
|
||||||
'audio_sample_rate': audio_sample_rate
|
"audio_sample_rate": audio_sample_rate,
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error analyzing video: {str(e)}")
|
logger.error(f"Error analyzing video: {str(e)}")
|
||||||
@@ -479,126 +562,184 @@ class FFmpegManager:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Add advanced encoding parameters
|
# Add advanced encoding parameters
|
||||||
params.update({
|
params.update(
|
||||||
"x264opts": "rc-lookahead=60:me=umh:subme=7:ref=4:b-adapt=2:direct=auto",
|
{
|
||||||
"tune": "film", # General-purpose tuning
|
"x264opts": "rc-lookahead=60:me=umh:subme=7:ref=4:b-adapt=2:direct=auto",
|
||||||
"fastfirstpass": "1", # Fast first pass for two-pass encoding
|
"tune": "film", # General-purpose tuning
|
||||||
})
|
"fastfirstpass": "1", # Fast first pass for two-pass encoding
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Adjust for content type
|
# Adjust for content type
|
||||||
if video_info.get('has_high_motion'):
|
if video_info.get("has_high_motion"):
|
||||||
params.update({
|
params.update(
|
||||||
"tune": "grain", # Better for high motion
|
{
|
||||||
"x264opts": params["x264opts"] + ":deblock=-1,-1:psy-rd=1.0:aq-strength=0.8"
|
"tune": "grain", # Better for high motion
|
||||||
})
|
"x264opts": params["x264opts"]
|
||||||
|
+ ":deblock=-1,-1:psy-rd=1.0:aq-strength=0.8",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if video_info.get('has_dark_scenes'):
|
if video_info.get("has_dark_scenes"):
|
||||||
# Optimize for dark scenes
|
# Optimize for dark scenes
|
||||||
params.update({
|
params.update(
|
||||||
"x264opts": params["x264opts"] + ":aq-mode=3:aq-strength=1.0:deblock=1:1",
|
{
|
||||||
"tune": "film" if not video_info.get('has_high_motion') else "grain"
|
"x264opts": params["x264opts"]
|
||||||
})
|
+ ":aq-mode=3:aq-strength=1.0:deblock=1:1",
|
||||||
|
"tune": (
|
||||||
|
"film" if not video_info.get("has_high_motion") else "grain"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# GPU-specific optimizations with fallback
|
# GPU-specific optimizations with fallback
|
||||||
if self._gpu_info["nvidia"]:
|
if self._gpu_info["nvidia"]:
|
||||||
try:
|
try:
|
||||||
params.update({
|
params.update(
|
||||||
"c:v": "h264_nvenc",
|
{
|
||||||
"preset": "p7", # Highest quality NVENC preset
|
"c:v": "h264_nvenc",
|
||||||
"rc:v": "vbr", # Variable bitrate
|
"preset": "p7", # Highest quality NVENC preset
|
||||||
"cq:v": "19", # Quality level
|
"rc:v": "vbr", # Variable bitrate
|
||||||
"b_ref_mode": "middle",
|
"cq:v": "19", # Quality level
|
||||||
"spatial-aq": "1",
|
"b_ref_mode": "middle",
|
||||||
"temporal-aq": "1",
|
"spatial-aq": "1",
|
||||||
"rc-lookahead": "32",
|
"temporal-aq": "1",
|
||||||
"surfaces": "64",
|
"rc-lookahead": "32",
|
||||||
"max_muxing_queue_size": "1024",
|
"surfaces": "64",
|
||||||
"gpu": "any", # Allow any available GPU
|
"max_muxing_queue_size": "1024",
|
||||||
})
|
"gpu": "any", # Allow any available GPU
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Test NVENC configuration
|
# Test NVENC configuration
|
||||||
test_cmd = [
|
test_cmd = (
|
||||||
str(self.ffmpeg_path),
|
[
|
||||||
"-f", "lavfi",
|
str(self.ffmpeg_path),
|
||||||
"-i", "testsrc=duration=1:size=1280x720:rate=30",
|
"-f",
|
||||||
"-c:v", "h264_nvenc"
|
"lavfi",
|
||||||
] + [f"-{k}" if len(k) == 1 else f"-{k}" if not v else f"-{k}" f" {v}" for k, v in params.items() if k != "c:v"] + [
|
"-i",
|
||||||
"-f", "null",
|
"testsrc=duration=1:size=1280x720:rate=30",
|
||||||
"-"
|
"-c:v",
|
||||||
]
|
"h264_nvenc",
|
||||||
|
]
|
||||||
|
+ [
|
||||||
|
(
|
||||||
|
f"-{k}"
|
||||||
|
if len(k) == 1
|
||||||
|
else f"-{k}" if not v else f"-{k}" f" {v}"
|
||||||
|
)
|
||||||
|
for k, v in params.items()
|
||||||
|
if k != "c:v"
|
||||||
|
]
|
||||||
|
+ ["-f", "null", "-"]
|
||||||
|
)
|
||||||
result = subprocess.run(test_cmd, capture_output=True)
|
result = subprocess.run(test_cmd, capture_output=True)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
raise GPUError("NVENC test failed")
|
raise GPUError("NVENC test failed")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"NVENC initialization failed, falling back to CPU: {str(e)}")
|
logger.error(
|
||||||
|
f"NVENC initialization failed, falling back to CPU: {str(e)}"
|
||||||
|
)
|
||||||
self._gpu_info["nvidia"] = False
|
self._gpu_info["nvidia"] = False
|
||||||
params["c:v"] = "libx264" # Fallback to CPU
|
params["c:v"] = "libx264" # Fallback to CPU
|
||||||
|
|
||||||
elif self._gpu_info["amd"]:
|
elif self._gpu_info["amd"]:
|
||||||
try:
|
try:
|
||||||
params.update({
|
params.update(
|
||||||
"c:v": "h264_amf",
|
{
|
||||||
"quality": "quality",
|
"c:v": "h264_amf",
|
||||||
"rc": "vbr_peak",
|
"quality": "quality",
|
||||||
"enforce_hrd": "1",
|
"rc": "vbr_peak",
|
||||||
"vbaq": "1",
|
"enforce_hrd": "1",
|
||||||
"preanalysis": "1",
|
"vbaq": "1",
|
||||||
"max_muxing_queue_size": "1024",
|
"preanalysis": "1",
|
||||||
})
|
"max_muxing_queue_size": "1024",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Test AMF configuration
|
# Test AMF configuration
|
||||||
test_cmd = [
|
test_cmd = (
|
||||||
str(self.ffmpeg_path),
|
[
|
||||||
"-f", "lavfi",
|
str(self.ffmpeg_path),
|
||||||
"-i", "testsrc=duration=1:size=1280x720:rate=30",
|
"-f",
|
||||||
"-c:v", "h264_amf"
|
"lavfi",
|
||||||
] + [f"-{k}" if len(k) == 1 else f"-{k}" if not v else f"-{k}" f" {v}" for k, v in params.items() if k != "c:v"] + [
|
"-i",
|
||||||
"-f", "null",
|
"testsrc=duration=1:size=1280x720:rate=30",
|
||||||
"-"
|
"-c:v",
|
||||||
]
|
"h264_amf",
|
||||||
|
]
|
||||||
|
+ [
|
||||||
|
(
|
||||||
|
f"-{k}"
|
||||||
|
if len(k) == 1
|
||||||
|
else f"-{k}" if not v else f"-{k}" f" {v}"
|
||||||
|
)
|
||||||
|
for k, v in params.items()
|
||||||
|
if k != "c:v"
|
||||||
|
]
|
||||||
|
+ ["-f", "null", "-"]
|
||||||
|
)
|
||||||
result = subprocess.run(test_cmd, capture_output=True)
|
result = subprocess.run(test_cmd, capture_output=True)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
raise GPUError("AMF test failed")
|
raise GPUError("AMF test failed")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"AMF initialization failed, falling back to CPU: {str(e)}")
|
logger.error(
|
||||||
|
f"AMF initialization failed, falling back to CPU: {str(e)}"
|
||||||
|
)
|
||||||
self._gpu_info["amd"] = False
|
self._gpu_info["amd"] = False
|
||||||
params["c:v"] = "libx264" # Fallback to CPU
|
params["c:v"] = "libx264" # Fallback to CPU
|
||||||
|
|
||||||
elif self._gpu_info["intel"]:
|
elif self._gpu_info["intel"]:
|
||||||
try:
|
try:
|
||||||
params.update({
|
params.update(
|
||||||
"c:v": "h264_qsv",
|
{
|
||||||
"preset": "veryslow",
|
"c:v": "h264_qsv",
|
||||||
"look_ahead": "1",
|
"preset": "veryslow",
|
||||||
"global_quality": "23",
|
"look_ahead": "1",
|
||||||
"max_muxing_queue_size": "1024",
|
"global_quality": "23",
|
||||||
})
|
"max_muxing_queue_size": "1024",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Test QSV configuration
|
# Test QSV configuration
|
||||||
test_cmd = [
|
test_cmd = (
|
||||||
str(self.ffmpeg_path),
|
[
|
||||||
"-f", "lavfi",
|
str(self.ffmpeg_path),
|
||||||
"-i", "testsrc=duration=1:size=1280x720:rate=30",
|
"-f",
|
||||||
"-c:v", "h264_qsv"
|
"lavfi",
|
||||||
] + [f"-{k}" if len(k) == 1 else f"-{k}" if not v else f"-{k}" f" {v}" for k, v in params.items() if k != "c:v"] + [
|
"-i",
|
||||||
"-f", "null",
|
"testsrc=duration=1:size=1280x720:rate=30",
|
||||||
"-"
|
"-c:v",
|
||||||
]
|
"h264_qsv",
|
||||||
|
]
|
||||||
|
+ [
|
||||||
|
(
|
||||||
|
f"-{k}"
|
||||||
|
if len(k) == 1
|
||||||
|
else f"-{k}" if not v else f"-{k}" f" {v}"
|
||||||
|
)
|
||||||
|
for k, v in params.items()
|
||||||
|
if k != "c:v"
|
||||||
|
]
|
||||||
|
+ ["-f", "null", "-"]
|
||||||
|
)
|
||||||
result = subprocess.run(test_cmd, capture_output=True)
|
result = subprocess.run(test_cmd, capture_output=True)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
raise GPUError("QSV test failed")
|
raise GPUError("QSV test failed")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"QSV initialization failed, falling back to CPU: {str(e)}")
|
logger.error(
|
||||||
|
f"QSV initialization failed, falling back to CPU: {str(e)}"
|
||||||
|
)
|
||||||
self._gpu_info["intel"] = False
|
self._gpu_info["intel"] = False
|
||||||
params["c:v"] = "libx264" # Fallback to CPU
|
params["c:v"] = "libx264" # Fallback to CPU
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Calculate target bitrate
|
# Calculate target bitrate
|
||||||
input_size = os.path.getsize(input_path)
|
input_size = os.path.getsize(input_path)
|
||||||
duration = video_info.get('duration', 0)
|
duration = video_info.get("duration", 0)
|
||||||
|
|
||||||
if duration > 0 and input_size > target_size_bytes:
|
if duration > 0 and input_size > target_size_bytes:
|
||||||
# Reserve 5% for container overhead
|
# Reserve 5% for container overhead
|
||||||
@@ -608,17 +749,14 @@ class FFmpegManager:
|
|||||||
total_bitrate = (video_size_target * 8) / duration
|
total_bitrate = (video_size_target * 8) / duration
|
||||||
|
|
||||||
# Determine audio quality based on content
|
# Determine audio quality based on content
|
||||||
audio_channels = video_info.get('audio_channels', 2)
|
audio_channels = video_info.get("audio_channels", 2)
|
||||||
min_audio_bitrate = 64000 * audio_channels # Minimum per channel
|
min_audio_bitrate = 64000 * audio_channels # Minimum per channel
|
||||||
max_audio_bitrate = 192000 * audio_channels # Maximum per channel
|
max_audio_bitrate = 192000 * audio_channels # Maximum per channel
|
||||||
|
|
||||||
# Allocate 10-20% for audio depending on content
|
# Allocate 10-20% for audio depending on content
|
||||||
audio_bitrate = min(
|
audio_bitrate = min(
|
||||||
max_audio_bitrate,
|
max_audio_bitrate,
|
||||||
max(
|
max(min_audio_bitrate, int(total_bitrate * 0.15)), # 15% baseline
|
||||||
min_audio_bitrate,
|
|
||||||
int(total_bitrate * 0.15) # 15% baseline
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Remaining bitrate for video
|
# Remaining bitrate for video
|
||||||
@@ -626,7 +764,7 @@ class FFmpegManager:
|
|||||||
|
|
||||||
# Set bitrate constraints
|
# Set bitrate constraints
|
||||||
params["maxrate"] = str(int(video_bitrate * 1.5)) # Allow 50% overflow
|
params["maxrate"] = str(int(video_bitrate * 1.5)) # Allow 50% overflow
|
||||||
params["bufsize"] = str(int(video_bitrate * 2)) # Double buffer size
|
params["bufsize"] = str(int(video_bitrate * 2)) # Double buffer size
|
||||||
|
|
||||||
# Adjust quality based on compression ratio and content
|
# Adjust quality based on compression ratio and content
|
||||||
ratio = input_size / target_size_bytes
|
ratio = input_size / target_size_bytes
|
||||||
@@ -641,33 +779,39 @@ class FFmpegManager:
|
|||||||
params["preset"] = "slow"
|
params["preset"] = "slow"
|
||||||
|
|
||||||
# Adjust for dark scenes
|
# Adjust for dark scenes
|
||||||
if video_info.get('has_dark_scenes'):
|
if video_info.get("has_dark_scenes"):
|
||||||
if params["c:v"] == "libx264":
|
if params["c:v"] == "libx264":
|
||||||
params["crf"] = str(max(18, int(params["crf"]) - 2)) # Better quality for dark scenes
|
params["crf"] = str(
|
||||||
|
max(18, int(params["crf"]) - 2)
|
||||||
|
) # Better quality for dark scenes
|
||||||
elif params["c:v"] == "h264_nvenc":
|
elif params["c:v"] == "h264_nvenc":
|
||||||
params["cq:v"] = str(max(15, int(params["cq:v"]) - 2))
|
params["cq:v"] = str(max(15, int(params["cq:v"]) - 2))
|
||||||
|
|
||||||
# Audio settings
|
# Audio settings
|
||||||
params.update({
|
params.update(
|
||||||
"c:a": "aac",
|
{
|
||||||
"b:a": f"{int(audio_bitrate/1000)}k",
|
"c:a": "aac",
|
||||||
"ar": str(video_info.get('audio_sample_rate', 48000)),
|
"b:a": f"{int(audio_bitrate/1000)}k",
|
||||||
"ac": str(video_info.get('audio_channels', 2)),
|
"ar": str(video_info.get("audio_sample_rate", 48000)),
|
||||||
})
|
"ac": str(video_info.get("audio_channels", 2)),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error calculating bitrates: {str(e)}")
|
logger.error(f"Error calculating bitrates: {str(e)}")
|
||||||
# Use safe default parameters
|
# Use safe default parameters
|
||||||
params.update({
|
params.update(
|
||||||
"crf": "23",
|
{
|
||||||
"preset": "medium",
|
"crf": "23",
|
||||||
"maxrate": f"{2 * 1024 * 1024}", # 2 Mbps
|
"preset": "medium",
|
||||||
"bufsize": f"{4 * 1024 * 1024}", # 4 Mbps buffer
|
"maxrate": f"{2 * 1024 * 1024}", # 2 Mbps
|
||||||
"c:a": "aac",
|
"bufsize": f"{4 * 1024 * 1024}", # 4 Mbps buffer
|
||||||
"b:a": "128k",
|
"c:a": "aac",
|
||||||
"ar": "48000",
|
"b:a": "128k",
|
||||||
"ac": "2",
|
"ar": "48000",
|
||||||
})
|
"ac": "2",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return params
|
return params
|
||||||
|
|
||||||
@@ -683,7 +827,10 @@ class FFmpegManager:
|
|||||||
url = arch_config["url"]
|
url = arch_config["url"]
|
||||||
|
|
||||||
with temp_path_context() as temp_dir:
|
with temp_path_context() as temp_dir:
|
||||||
archive_path = Path(temp_dir) / f"ffmpeg_archive{'.zip' if self.system == 'Windows' else '.tar.xz'}"
|
archive_path = (
|
||||||
|
Path(temp_dir)
|
||||||
|
/ f"ffmpeg_archive{'.zip' if self.system == 'Windows' else '.tar.xz'}"
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Download archive with retries
|
# Download archive with retries
|
||||||
@@ -711,7 +858,9 @@ class FFmpegManager:
|
|||||||
if self.system == "Windows":
|
if self.system == "Windows":
|
||||||
with zipfile.ZipFile(archive_path, "r") as zip_ref:
|
with zipfile.ZipFile(archive_path, "r") as zip_ref:
|
||||||
ffmpeg_files = [
|
ffmpeg_files = [
|
||||||
f for f in zip_ref.namelist() if arch_config["bin_name"] in f
|
f
|
||||||
|
for f in zip_ref.namelist()
|
||||||
|
if arch_config["bin_name"] in f
|
||||||
]
|
]
|
||||||
if not ffmpeg_files:
|
if not ffmpeg_files:
|
||||||
raise DownloadError("FFmpeg binary not found in archive")
|
raise DownloadError("FFmpeg binary not found in archive")
|
||||||
@@ -720,7 +869,9 @@ class FFmpegManager:
|
|||||||
else:
|
else:
|
||||||
with tarfile.open(archive_path, "r:xz") as tar_ref:
|
with tarfile.open(archive_path, "r:xz") as tar_ref:
|
||||||
ffmpeg_files = [
|
ffmpeg_files = [
|
||||||
f for f in tar_ref.getnames() if arch_config["bin_name"] in f
|
f
|
||||||
|
for f in tar_ref.getnames()
|
||||||
|
if arch_config["bin_name"] in f
|
||||||
]
|
]
|
||||||
if not ffmpeg_files:
|
if not ffmpeg_files:
|
||||||
raise DownloadError("FFmpeg binary not found in archive")
|
raise DownloadError("FFmpeg binary not found in archive")
|
||||||
|
|||||||
Reference in New Issue
Block a user