Reduce ffmpeg console output
This commit is contained in:
parent
53b2e5ecca
commit
55ee91cae2
3 changed files with 81 additions and 6 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -5,6 +5,7 @@ __pycache__/
|
||||||
venv/
|
venv/
|
||||||
|
|
||||||
# Generated videos / outputs
|
# Generated videos / outputs
|
||||||
|
movmaker.log
|
||||||
out/
|
out/
|
||||||
*.mp4
|
*.mp4
|
||||||
*.mov
|
*.mov
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,7 @@ python3 movmaker.py input --preview
|
||||||
- embeds MP4 metadata: title, date, location/place, QuickTime location name, artist, comment, and creation_time
|
- 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
|
- 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
|
- 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
|
- uses local `ffmpeg/ffmpeg` and `ffmpeg/ffprobe` when present, otherwise system tools
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
|
||||||
85
movmaker.py
85
movmaker.py
|
|
@ -35,6 +35,7 @@ BASE_DIR = Path(__file__).resolve().parent
|
||||||
FFMPEG = BASE_DIR / "ffmpeg" / "ffmpeg"
|
FFMPEG = BASE_DIR / "ffmpeg" / "ffmpeg"
|
||||||
FFPROBE = BASE_DIR / "ffmpeg" / "ffprobe"
|
FFPROBE = BASE_DIR / "ffmpeg" / "ffprobe"
|
||||||
PIXABAY_ENV = Path.home() / ".config" / "movmaker" / "pixabay.env"
|
PIXABAY_ENV = Path.home() / ".config" / "movmaker" / "pixabay.env"
|
||||||
|
LOG_PATH: Path | None = None
|
||||||
|
|
||||||
|
|
||||||
def tool_path(local_path: Path, fallback: str) -> str:
|
def tool_path(local_path: Path, fallback: str) -> str:
|
||||||
|
|
@ -56,9 +57,75 @@ class MediaItem:
|
||||||
duration: float
|
duration: float
|
||||||
|
|
||||||
|
|
||||||
def run(cmd: list[str]) -> None:
|
def log_message(message: str) -> None:
|
||||||
print("+", shlex.join(cmd))
|
if LOG_PATH:
|
||||||
subprocess.run(cmd, check=True)
|
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:
|
def capture(cmd: list[str]) -> str:
|
||||||
|
|
@ -785,7 +852,7 @@ def build_video(
|
||||||
cmd += ["-an"]
|
cmd += ["-an"]
|
||||||
|
|
||||||
cmd += ["-c:v", "libx264", "-preset", preset, "-crf", str(crf), "-r", str(fps), "-pix_fmt", "yuv420p", "-movflags", "+faststart+use_metadata_tags", str(output)]
|
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:
|
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")
|
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)
|
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("ffmpeg")
|
||||||
require_tool("ffprobe")
|
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"Found {len(media)} media file(s), {len(audio_files)} audio file(s).")
|
||||||
print(f"Output: {output}")
|
print(f"Output: {output}")
|
||||||
|
print(f"Log: {LOG_PATH}")
|
||||||
if data.title or data.date or data.place:
|
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))
|
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)
|
jobs = max(1, args.jobs)
|
||||||
clips = [tmp / f"clip_{idx:04d}.mp4" for idx in range(len(media))]
|
clips = [tmp / f"clip_{idx:04d}.mp4" for idx in range(len(media))]
|
||||||
if jobs == 1:
|
if jobs == 1:
|
||||||
|
print("Preparing clips...")
|
||||||
for item, out in zip(media, 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)
|
normalize_clip(item, out, width, height, fps, preset, crf)
|
||||||
else:
|
else:
|
||||||
print(f"Preparing clips with {jobs} parallel jobs")
|
print(f"Preparing clips with {jobs} parallel jobs")
|
||||||
with concurrent.futures.ThreadPoolExecutor(max_workers=jobs) as executor:
|
with concurrent.futures.ThreadPoolExecutor(max_workers=jobs) as executor:
|
||||||
futures = []
|
futures = []
|
||||||
for item, out in zip(media, 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}")
|
||||||
futures.append(executor.submit(normalize_clip, item, out, width, height, fps, preset, crf))
|
futures.append(executor.submit(normalize_clip, item, out, width, height, fps, preset, crf))
|
||||||
for future in concurrent.futures.as_completed(futures):
|
for future in concurrent.futures.as_completed(futures):
|
||||||
future.result()
|
future.result()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue