"""Locating ffmpeg / ffprobe and running them without flashing console windows.""" from __future__ import annotations import os import re import shutil import subprocess import sys from dataclasses import dataclass, field from pathlib import Path IS_WINDOWS = sys.platform.startswith("win") EXE_SUFFIX = ".exe" if IS_WINDOWS else "" # Keep ffprobe/ffmpeg helper calls from popping up a console on Windows. _NO_WINDOW = {"creationflags": subprocess.CREATE_NO_WINDOW} if IS_WINDOWS else {} def app_dir() -> Path: """Directory holding the executable (frozen) or the project root (source).""" if getattr(sys, "frozen", False): return Path(sys.executable).resolve().parent return Path(__file__).resolve().parent.parent def run_quiet(args: list[str], timeout: float | None = 60) -> subprocess.CompletedProcess: return subprocess.run( args, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=timeout, **_NO_WINDOW, ) @dataclass class FFmpegTools: ffmpeg: str ffprobe: str version: str = "" encoders: set[str] = field(default_factory=set) @property def ok(self) -> bool: return bool(self.ffmpeg and self.ffprobe) def _candidate_dirs(configured_dir: str | None) -> list[Path]: dirs: list[Path] = [] if configured_dir: dirs.append(Path(configured_dir)) base = app_dir() dirs += [base, base / "ffmpeg", base / "ffmpeg" / "bin", base / "bin"] return dirs def _find_in(dirs: list[Path], name: str) -> str | None: for d in dirs: p = d / f"{name}{EXE_SUFFIX}" if p.is_file() and (IS_WINDOWS or os.access(p, os.X_OK)): return str(p) return None def locate_tools(configured_dir: str | None = None) -> FFmpegTools | None: """Find ffmpeg + ffprobe: configured dir, next to the app, then PATH.""" dirs = _candidate_dirs(configured_dir) ffmpeg = _find_in(dirs, "ffmpeg") if ffmpeg: # Prefer the ffprobe that ships alongside the chosen ffmpeg. ffprobe = _find_in([Path(ffmpeg).parent], "ffprobe") or _find_in(dirs, "ffprobe") else: ffmpeg = shutil.which("ffmpeg") ffprobe = None if ffmpeg and not ffprobe: ffprobe = _find_in([Path(ffmpeg).parent], "ffprobe") or shutil.which("ffprobe") if not ffmpeg or not ffprobe: return None tools = FFmpegTools(ffmpeg=ffmpeg, ffprobe=ffprobe) try: out = run_quiet([ffmpeg, "-hide_banner", "-version"], timeout=15).stdout.decode(errors="replace") first = out.splitlines()[0] if out else "" m = re.match(r"ffmpeg version (\S+)", first) tools.version = m.group(1) if m else first enc = run_quiet([ffmpeg, "-hide_banner", "-encoders"], timeout=15).stdout.decode(errors="replace") tools.encoders = parse_encoders(enc) except (OSError, subprocess.SubprocessError): return None return tools def parse_encoders(text: str) -> set[str]: names = set() for line in text.splitlines(): # " V....D libx264 libx264 H.264 ..." m = re.match(r"^\s*([VAS])[\w.]{5}\s+(\S+)\s", line) if m and m.group(2) != "=": names.add(m.group(2)) return names