Reduce ffmpeg console output

This commit is contained in:
hbrain 2026-05-25 09:47:19 +00:00
parent 53b2e5ecca
commit 55ee91cae2
3 changed files with 81 additions and 6 deletions

1
.gitignore vendored
View file

@ -5,6 +5,7 @@ __pycache__/
venv/
# Generated videos / outputs
movmaker.log
out/
*.mp4
*.mov

View file

@ -195,6 +195,7 @@ python3 movmaker.py input --preview
- embeds MP4 metadata: title, date, location/place, QuickTime location name, artist, comment, and creation_time
- writes an MP4 file named like `YYYYMMDD_title.mp4` in the current directory by default; use `--out-dir DIR` to choose another output directory
- takes `YYYYMMDD` from the first picture/video metadata, falling back to file modification date
- keeps console output minimal during rendering and writes detailed ffmpeg output to `movmaker.log` in the input directory
- uses local `ffmpeg/ffmpeg` and `ffmpeg/ffprobe` when present, otherwise system tools
## Notes

View file

@ -35,6 +35,7 @@ BASE_DIR = Path(__file__).resolve().parent
FFMPEG = BASE_DIR / "ffmpeg" / "ffmpeg"
FFPROBE = BASE_DIR / "ffmpeg" / "ffprobe"
PIXABAY_ENV = Path.home() / ".config" / "movmaker" / "pixabay.env"
LOG_PATH: Path | None = None
def tool_path(local_path: Path, fallback: str) -> str:
@ -56,9 +57,75 @@ class MediaItem:
duration: float
def run(cmd: list[str]) -> None:
print("+", shlex.join(cmd))
subprocess.run(cmd, check=True)
def log_message(message: str) -> None:
if LOG_PATH:
with LOG_PATH.open("a", encoding="utf-8") as log:
log.write(message.rstrip() + "\n")
def ffmpeg_progress_cmd(cmd: list[str], progress: bool) -> list[str]:
if not cmd or Path(cmd[0]).name != "ffmpeg":
return cmd
out = [cmd[0], "-hide_banner", "-nostats"]
rest = cmd[1:]
if rest and rest[0] == "-y":
out.append("-y")
rest = rest[1:]
if progress:
out += ["-progress", "pipe:1"]
return out + rest
def format_bytes(value: int) -> str:
size = float(max(0, value))
for unit in ["B", "KB", "MB", "GB"]:
if size < 1024 or unit == "GB":
return f"{size:.1f}{unit}"
size /= 1024
return f"{size:.1f}GB"
def run(cmd: list[str], progress_duration: float | None = None) -> None:
progress = progress_duration is not None and progress_duration > 0 and Path(cmd[0]).name == "ffmpeg"
run_cmd = ffmpeg_progress_cmd(cmd, progress)
log_message("+ " + shlex.join(run_cmd))
if not LOG_PATH:
subprocess.run(run_cmd, check=True)
return
with LOG_PATH.open("a", encoding="utf-8") as log:
if not progress:
subprocess.run(run_cmd, stdout=log, stderr=log, check=True)
return
process = subprocess.Popen(run_cmd, stdout=subprocess.PIPE, stderr=log, text=True, bufsize=1)
fields: dict[str, str] = {}
assert process.stdout is not None
for raw_line in process.stdout:
line = raw_line.strip()
log.write(line + "\n")
if "=" not in line:
continue
key, value = line.split("=", 1)
fields[key] = value
if key in {"out_time_ms", "total_size", "speed", "fps", "progress"}:
try:
seconds = int(fields.get("out_time_ms", "0")) / 1_000_000
except ValueError:
seconds = 0.0
try:
written = int(fields.get("total_size", "0"))
except ValueError:
written = 0
percent = min(100.0, max(0.0, seconds / progress_duration * 100.0))
fps = fields.get("fps", "?")
speed = fields.get("speed", "?")
sys.stdout.write(f"\rRendering {percent:5.1f}% fps={fps} speed={speed} written={format_bytes(written)}")
sys.stdout.flush()
rc = process.wait()
sys.stdout.write("\n")
if rc != 0:
raise subprocess.CalledProcessError(rc, run_cmd)
def capture(cmd: list[str]) -> str:
@ -785,7 +852,7 @@ def build_video(
cmd += ["-an"]
cmd += ["-c:v", "libx264", "-preset", preset, "-crf", str(crf), "-r", str(fps), "-pix_fmt", "yuv420p", "-movflags", "+faststart+use_metadata_tags", str(output)]
run(cmd)
run(cmd, total_duration)
def main(argv: list[str] | None = None) -> int:
@ -806,6 +873,10 @@ def main(argv: list[str] | None = None) -> int:
parser.add_argument("--music-genre", default="cinematic punk rock", help="Pixabay music search query used only when input has no audio files")
args = parser.parse_args(argv)
global LOG_PATH
LOG_PATH = args.input / "movmaker.log"
LOG_PATH.write_text("movmaker log\n", encoding="utf-8")
require_tool("ffmpeg")
require_tool("ffprobe")
@ -852,6 +923,7 @@ def main(argv: list[str] | None = None) -> int:
print(f"Found {len(media)} media file(s), {len(audio_files)} audio file(s).")
print(f"Output: {output}")
print(f"Log: {LOG_PATH}")
if data.title or data.date or data.place:
print("Title data:", " | ".join(x for x in [data.title, data.date, data.place] if x))
@ -860,15 +932,16 @@ def main(argv: list[str] | None = None) -> int:
jobs = max(1, args.jobs)
clips = [tmp / f"clip_{idx:04d}.mp4" for idx in range(len(media))]
if jobs == 1:
print("Preparing clips...")
for item, out in zip(media, clips):
print(f"Preparing {item.kind}: {item.path.name}")
log_message(f"Preparing {item.kind}: {item.path.name}")
normalize_clip(item, out, width, height, fps, preset, crf)
else:
print(f"Preparing clips with {jobs} parallel jobs")
with concurrent.futures.ThreadPoolExecutor(max_workers=jobs) as executor:
futures = []
for item, out in zip(media, clips):
print(f"Preparing {item.kind}: {item.path.name}")
log_message(f"Preparing {item.kind}: {item.path.name}")
futures.append(executor.submit(normalize_clip, item, out, width, height, fps, preset, crf))
for future in concurrent.futures.as_completed(futures):
future.result()