from __future__ import annotations import os import shutil import subprocess import sys from pathlib import Path from typing import MutableMapping, Optional ROOT_DIR = Path(__file__).resolve().parent.parent def get_runtime_root(root_dir: Path | str | None = None) -> Path: if root_dir is not None: return Path(root_dir).expanduser() if getattr(sys, "frozen", False): return Path(sys.executable).resolve().parent return ROOT_DIR def _normalize_path_key(path: Path | str) -> str: return os.path.normcase(os.path.normpath(str(path))) def _iter_ffmpeg_bin_candidates(root_dir: Path) -> list[Path]: candidates = [ root_dir / "tools" / "ffmpeg" / "bin", root_dir / "tools" / "ffmpeg", ] meipass = getattr(sys, "_MEIPASS", None) if meipass: meipass_root = Path(meipass) candidates.extend( [ meipass_root / "tools" / "ffmpeg" / "bin", meipass_root / "tools" / "ffmpeg", ] ) try: import imageio_ffmpeg candidates.append(Path(imageio_ffmpeg.get_ffmpeg_exe()).resolve().parent) except Exception: pass unique: list[Path] = [] seen: set[str] = set() for candidate in candidates: key = _normalize_path_key(candidate) if key not in seen: unique.append(candidate) seen.add(key) return unique def get_ffmpeg_bin_dir(root_dir: Path | str | None = None) -> Optional[Path]: runtime_root = get_runtime_root(root_dir) ffmpeg_name = "ffmpeg.exe" if os.name == "nt" else "ffmpeg" for candidate in _iter_ffmpeg_bin_candidates(runtime_root): if (candidate / ffmpeg_name).exists(): return candidate found = shutil.which("ffmpeg") if found: return Path(found).resolve().parent return None def _check_executable_runs(executable: Path, label: str) -> None: try: result = subprocess.run( [str(executable), "-version"], capture_output=True, text=True, timeout=10, ) except Exception as exc: raise RuntimeError(f"{label} 无法启动: {executable} ({exc})") from exc if result.returncode != 0: details = "\n".join( part.strip() for part in (result.stdout, result.stderr) if part and part.strip() ) if not details: details = f"退出码: {result.returncode}" raise RuntimeError(f"{label} 无法正常运行: {executable}\n{details}") def configure_ffmpeg_runtime( root_dir: Path | str | None = None, env: MutableMapping[str, str] | None = None, ) -> Optional[Path]: env = os.environ if env is None else env bin_dir = get_ffmpeg_bin_dir(root_dir=root_dir) if bin_dir is None: return None bin_dir_str = str(bin_dir) current_path = env.get("PATH", "") path_entries = [entry for entry in current_path.split(os.pathsep) if entry] normalized_entries = {_normalize_path_key(entry) for entry in path_entries} if _normalize_path_key(bin_dir_str) not in normalized_entries: env["PATH"] = bin_dir_str if not current_path else bin_dir_str + os.pathsep + current_path ffmpeg_name = "ffmpeg.exe" if os.name == "nt" else "ffmpeg" ffmpeg_path = bin_dir / ffmpeg_name if ffmpeg_path.exists(): _check_executable_runs(ffmpeg_path, "ffmpeg") env.setdefault("FFMPEG_BINARY", str(ffmpeg_path)) env.setdefault("IMAGEIO_FFMPEG_EXE", str(ffmpeg_path)) ffprobe_name = "ffprobe.exe" if os.name == "nt" else "ffprobe" ffprobe_path = bin_dir / ffprobe_name if ffprobe_path.exists(): _check_executable_runs(ffprobe_path, "ffprobe") env.setdefault("FFPROBE_BINARY", str(ffprobe_path)) else: resolved_ffprobe = shutil.which("ffprobe", path=env.get("PATH", "")) if resolved_ffprobe is None: raise RuntimeError("未找到 ffprobe。请安装 ffmpeg/ffprobe,或将 ffprobe 放入 tools/ffmpeg/bin。") _check_executable_runs(Path(resolved_ffprobe), "ffprobe") env.setdefault("FFPROBE_BINARY", str(Path(resolved_ffprobe))) return bin_dir