2026-02-16

This commit is contained in:
2026-03-17 15:25:32 -06:00
parent d5dd373de0
commit 60100fbab2
560 changed files with 33397 additions and 20776 deletions
@@ -1,5 +1,6 @@
import bpy # type: ignore
import os
import shutil
import subprocess
import sys
import tempfile
@@ -91,6 +92,46 @@ def get_file_extension(video_format):
else:
return ".mp4" # Default to mp4 if unknown
def _resolve_ffmpeg_path():
"""Resolve ffmpeg executable. Prefer addon pref, then PATH, then Blender's bundled ffmpeg."""
try:
for addon in (__name__, "basedplayblast", "bl_ext.basedplayblast", "BasedPlayblast"):
prefs = bpy.context.preferences.addons.get(addon)
if prefs and prefs.preferences and hasattr(prefs.preferences, "ffmpeg_path"):
custom = getattr(prefs.preferences, "ffmpeg_path", "").strip()
if custom and os.path.isfile(custom):
return custom
except Exception:
pass
exe = shutil.which("ffmpeg")
if exe:
return exe
try:
blender_dir = os.path.dirname(bpy.app.binary_path)
version_str = f"{bpy.app.version[0]}.{bpy.app.version[1]}"
for search_dir in (blender_dir, os.path.join(blender_dir, version_str)):
for name in ("ffmpeg.exe", "ffmpeg"):
path = os.path.join(search_dir, name)
if os.path.isfile(path):
return path
except Exception:
pass
# Common Windows install locations (Steam/launcher often omit PATH)
for candidate in (
r"C:\ProgramData\chocolatey\bin\ffmpeg.exe", # Chocolatey
r"C:\Program Files\ffmpeg\bin\ffmpeg.exe",
r"C:\Program Files (x86)\ffmpeg\bin\ffmpeg.exe",
r"C:\ffmpeg\bin\ffmpeg.exe",
os.path.join(os.environ.get("ProgramFiles", "C:\\Program Files"), "ffmpeg", "bin", "ffmpeg.exe"),
os.path.join(os.environ.get("ProgramFiles(x86)", "C:\\Program Files (x86)"), "ffmpeg", "bin", "ffmpeg.exe"),
):
try:
if candidate and os.path.isfile(candidate):
return candidate
except Exception:
pass
return "ffmpeg" # Fallback; will fail with clear error if missing
# Helper function to convert quality enum to FFmpeg CRF value
def get_ffmpeg_quality(quality_enum):
quality_map = {
@@ -814,7 +855,8 @@ class BPL_OT_create_playblast(Operator):
'frame_end': original_frame_end, # Store original frame end
'image_settings': {
'file_format': scene.render.image_settings.file_format,
'color_mode': scene.render.image_settings.color_mode
'color_mode': scene.render.image_settings.color_mode,
'compression': self._safe_get_compression_value(scene)
},
'display_mode': context.preferences.view.render_display_type,
# Store metadata settings
@@ -1523,7 +1565,7 @@ class BPL_OT_create_playblast(Operator):
# Note: FFmpeg's %04d pattern expects frames starting at 0000, but our frames start at frame_start
# We need to add -start_number to tell FFmpeg the actual starting frame number
ffmpeg_cmd = [
"ffmpeg", "-y", # Overwrite output file
_resolve_ffmpeg_path(), "-y", # Overwrite output file
"-framerate", str(framerate),
"-start_number", str(self._frame_start), # Tell FFmpeg the starting frame number
"-i", frame_pattern,
@@ -1675,7 +1717,7 @@ class BPL_OT_create_playblast(Operator):
duration = (self._frame_end - self._frame_start + 1) / fps
ffmpeg_extract_cmd = [
"ffmpeg", "-y",
_resolve_ffmpeg_path(), "-y",
"-i", sound_path,
"-ss", str(max(0, start_time)),
"-t", str(duration),
@@ -1868,8 +1910,22 @@ class BPL_OT_create_playblast(Operator):
self.report({'ERROR'}, f"Video conversion failed: {result.stderr}")
except Exception as e:
print(f"Error converting frames to video: {str(e)}")
self.report({'ERROR'}, f"Video conversion error: {str(e)}")
err_msg = str(e)
print(f"Error converting frames to video: {err_msg}")
hint = " Set FFmpeg Path in Edit > Preferences > Add-ons > BasedPlayblast." if "WinError 2" in err_msg or "cannot find the file" in err_msg.lower() else ""
self.report({'ERROR'}, f"Video conversion error: {err_msg}{hint}")
def _safe_get_compression_value(self, scene):
"""Safely get compression value, returning None if not available or format doesn't support it."""
try:
if not hasattr(scene.render.image_settings, 'compression'):
return None
current_format = str(scene.render.image_settings.file_format).upper()
if current_format not in ('PNG', 'JPEG', 'JPEG2000'):
return None
return scene.render.image_settings.compression
except (AttributeError, TypeError, ValueError):
return None
def cleanup(self, context):
# Reset progress properties
@@ -1916,8 +1972,42 @@ class BPL_OT_create_playblast(Operator):
scene.render.use_overwrite = self._original_settings['use_overwrite']
scene.render.use_placeholder = self._original_settings['use_placeholder']
scene.camera = self._original_settings['camera']
scene.render.image_settings.file_format = self._original_settings['image_settings']['file_format']
scene.render.image_settings.color_mode = self._original_settings['image_settings']['color_mode']
# Restore image settings safely using .get() to avoid KeyError
try:
if isinstance(self._original_settings, dict):
img_settings = self._original_settings.get('image_settings')
if img_settings and isinstance(img_settings, dict):
# Restore file_format first
file_format = img_settings.get('file_format')
if file_format is not None:
scene.render.image_settings.file_format = file_format
# Restore color_mode
color_mode = img_settings.get('color_mode')
if color_mode is not None:
scene.render.image_settings.color_mode = color_mode
# Restore compression - check if format supports it
compression = img_settings.get('compression')
if compression is not None:
# Get current format (after restoration above)
try:
current_format = scene.render.image_settings.file_format
# Convert to string for comparison (handles enum types)
format_str = str(current_format).upper()
# Only restore compression if file format supports it
if format_str in ('PNG', 'JPEG', 'JPEG2000'):
try:
if hasattr(scene.render.image_settings, 'compression'):
scene.render.image_settings.compression = compression
print(f"Restored PNG compression: {compression}%")
except (AttributeError, TypeError, ValueError):
pass # Silently fail if compression can't be set
except (AttributeError, TypeError):
pass # Silently fail if format can't be read
except (KeyError, TypeError, AttributeError):
pass # Silently fail if settings can't be restored
context.preferences.view.render_display_type = self._original_settings['display_mode']
# CRITICAL: Restore frame range to original values - THIS FIXES THE MAIN BUG
@@ -3893,6 +3983,13 @@ class BPL_AddonPreferences(AddonPreferences):
default="-c:v h264_nvenc -preset fast -crf 0"
)
ffmpeg_path: StringProperty(
name="FFmpeg Path",
description="Full path to ffmpeg.exe (e.g. C:\\ffmpeg\\bin\\ffmpeg.exe). Leave blank to use PATH or Blender's bundled ffmpeg. Required when launching Blender from Steam.",
default="",
subtype='FILE_PATH'
)
repo_initialized: BoolProperty(
name="Rainy's Extensions Added",
description="Internal flag to avoid re-adding Rainy's Extensions repository multiple times.",
@@ -3907,6 +4004,7 @@ class BPL_AddonPreferences(AddonPreferences):
box.prop(self, "default_video_quality")
box.prop(self, "default_use_custom_ffmpeg_args")
box.prop(self, "default_ffmpeg_args")
box.prop(self, "ffmpeg_path", text="FFmpeg Path (Steam)")
def on_load_post(dummy):
"""Applies user defaults after a file is loaded."""