167 lines
5.3 KiB
Python
167 lines
5.3 KiB
Python
"""Locate an ffmpeg executable for BasedPlayblast."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import urllib.request
|
|
import zipfile
|
|
|
|
_FFMPEG_BOOTSTRAP_ATTEMPTED = False
|
|
|
|
# Official Windows essentials build recommended for Blender-adjacent tooling.
|
|
_FFMPEG_ESSENTIALS_URL = (
|
|
"https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip"
|
|
)
|
|
|
|
|
|
def addon_root() -> str:
|
|
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
|
|
def bundled_ffmpeg_path() -> str:
|
|
return os.path.join(addon_root(), "tools", "ffmpeg.exe")
|
|
|
|
|
|
def iter_blender_bundled_ffmpeg_paths():
|
|
"""Yield likely paths to ffmpeg shipped with this Blender install."""
|
|
try:
|
|
import bpy # type: ignore
|
|
|
|
binary = bpy.app.binary_path
|
|
if not binary:
|
|
return
|
|
binary_dir = os.path.dirname(binary)
|
|
version = f"{bpy.app.version[0]}.{bpy.app.version[1]}"
|
|
names = ("ffmpeg.exe", "ffmpeg") if sys.platform == "win32" else ("ffmpeg",)
|
|
|
|
roots = [binary_dir]
|
|
parent = os.path.dirname(binary_dir)
|
|
if parent:
|
|
roots.append(parent)
|
|
if sys.platform == "darwin":
|
|
resources = os.path.join(os.path.dirname(binary_dir), "Resources")
|
|
if os.path.isdir(resources):
|
|
roots.append(resources)
|
|
roots.append(os.path.join(resources, version))
|
|
|
|
subdirs = (
|
|
"",
|
|
version,
|
|
"ffmpeg",
|
|
os.path.join(version, "ffmpeg"),
|
|
os.path.join(version, "ffmpeg", "bin"),
|
|
os.path.join("ffmpeg", "bin"),
|
|
)
|
|
|
|
seen = set()
|
|
for root in roots:
|
|
for sub in subdirs:
|
|
for name in names:
|
|
path = os.path.normpath(
|
|
os.path.join(root, sub, name) if sub else os.path.join(root, name)
|
|
)
|
|
if path not in seen:
|
|
seen.add(path)
|
|
yield path
|
|
except Exception:
|
|
return
|
|
|
|
|
|
def _bootstrap_bundled_ffmpeg() -> str | None:
|
|
"""Download ffmpeg essentials into addon tools/ when no executable is found."""
|
|
global _FFMPEG_BOOTSTRAP_ATTEMPTED
|
|
if _FFMPEG_BOOTSTRAP_ATTEMPTED or sys.platform != "win32":
|
|
return None
|
|
_FFMPEG_BOOTSTRAP_ATTEMPTED = True
|
|
|
|
target = bundled_ffmpeg_path()
|
|
if os.path.isfile(target):
|
|
return target
|
|
|
|
tools_dir = os.path.dirname(target)
|
|
os.makedirs(tools_dir, exist_ok=True)
|
|
|
|
print("[BasedPlayblast] No ffmpeg found; downloading essentials build to addon tools/...")
|
|
try:
|
|
with tempfile.TemporaryDirectory(prefix="basedplayblast_ffmpeg_") as tmp_dir:
|
|
zip_path = os.path.join(tmp_dir, "ffmpeg-essentials.zip")
|
|
with urllib.request.urlopen(_FFMPEG_ESSENTIALS_URL, timeout=120) as response:
|
|
with open(zip_path, "wb") as handle:
|
|
shutil.copyfileobj(response, handle)
|
|
|
|
with zipfile.ZipFile(zip_path) as archive:
|
|
member = next(
|
|
(name for name in archive.namelist() if name.endswith("/bin/ffmpeg.exe")),
|
|
None,
|
|
)
|
|
if not member:
|
|
print("[BasedPlayblast] ffmpeg bootstrap failed: ffmpeg.exe not found in archive")
|
|
return None
|
|
archive.extract(member, tmp_dir)
|
|
extracted = os.path.join(tmp_dir, member.replace("/", os.sep))
|
|
shutil.copy2(extracted, target)
|
|
|
|
if os.path.isfile(target):
|
|
print(f"[BasedPlayblast] Installed bundled ffmpeg: {target}")
|
|
return target
|
|
except Exception as exc:
|
|
print(f"[BasedPlayblast] ffmpeg bootstrap failed: {exc}")
|
|
|
|
return None
|
|
|
|
|
|
def resolve_ffmpeg_path() -> str:
|
|
"""Resolve ffmpeg: addon override, addon bundle, Blender, bootstrap, then PATH."""
|
|
try:
|
|
import bpy # type: ignore
|
|
|
|
for addon_name in (
|
|
"BasedPlayblast",
|
|
"basedplayblast",
|
|
"bl_ext.basedplayblast",
|
|
"bl_ext.user_local.basedplayblast",
|
|
):
|
|
prefs = bpy.context.preferences.addons.get(addon_name)
|
|
if not prefs or not prefs.preferences:
|
|
continue
|
|
if hasattr(prefs.preferences, "ffmpeg_path"):
|
|
custom = getattr(prefs.preferences, "ffmpeg_path", "").strip()
|
|
if custom and os.path.isfile(custom):
|
|
return custom
|
|
except Exception:
|
|
pass
|
|
|
|
bundled = bundled_ffmpeg_path()
|
|
if os.path.isfile(bundled):
|
|
return bundled
|
|
|
|
for candidate in iter_blender_bundled_ffmpeg_paths():
|
|
if os.path.isfile(candidate):
|
|
return candidate
|
|
|
|
bootstrapped = _bootstrap_bundled_ffmpeg()
|
|
if bootstrapped and os.path.isfile(bootstrapped):
|
|
return bootstrapped
|
|
|
|
exe = shutil.which("ffmpeg")
|
|
if exe:
|
|
print(
|
|
f"[BasedPlayblast] Warning: no addon-bundled ffmpeg; falling back to PATH: {exe}"
|
|
)
|
|
return exe
|
|
|
|
for candidate in (
|
|
r"C:\ProgramData\chocolatey\bin\ffmpeg.exe",
|
|
r"C:\Program Files\ffmpeg\bin\ffmpeg.exe",
|
|
r"C:\Program Files (x86)\ffmpeg\bin\ffmpeg.exe",
|
|
r"C:\ffmpeg\bin\ffmpeg.exe",
|
|
):
|
|
if candidate and os.path.isfile(candidate):
|
|
return candidate
|
|
|
|
return "ffmpeg"
|