aa
This commit is contained in:
152
optimize_clips.py
Normal file
152
optimize_clips.py
Normal file
@@ -0,0 +1,152 @@
|
||||
import os
|
||||
import glob
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
CLIPS_DIR = "/data/events/clips"
|
||||
ARCHIVE_DIR = "/mnt/tour-big/clips"
|
||||
SIZE_THRESHOLD_MB = 200
|
||||
SIZE_THRESHOLD_BYTES = SIZE_THRESHOLD_MB * 1024 * 1024
|
||||
MAX_PER_RUN = 20
|
||||
|
||||
|
||||
def has_nvenc():
|
||||
r = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
|
||||
return b"hevc_nvenc" in r.stdout
|
||||
|
||||
|
||||
def _ffmpeg(cmd):
|
||||
return subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
|
||||
|
||||
|
||||
def _proxy_cmd(src, dst, hw):
|
||||
"""10fps H.265 proxy for quick local browsing."""
|
||||
if hw:
|
||||
return ["ffmpeg", "-y",
|
||||
"-hwaccel", "cuda", "-hwaccel_output_format", "cuda",
|
||||
"-i", src,
|
||||
"-vcodec", "hevc_nvenc", "-rc", "constqp", "-qp", "28", "-preset", "p4",
|
||||
"-r", "10", "-tag:v", "hvc1", "-acodec", "copy", dst]
|
||||
return ["ffmpeg", "-y", "-i", src,
|
||||
"-vcodec", "libx265", "-crf", "28", "-preset", "medium",
|
||||
"-r", "10", "-tag:v", "hvc1", "-acodec", "copy", dst]
|
||||
|
||||
|
||||
def _archive_cmd(src, dst, hw):
|
||||
"""Full-fps high-quality H.265 for archive — small enough to pull back fast over slow SMB."""
|
||||
if hw:
|
||||
return ["ffmpeg", "-y",
|
||||
"-hwaccel", "cuda", "-hwaccel_output_format", "cuda",
|
||||
"-i", src,
|
||||
"-vcodec", "hevc_nvenc", "-rc", "vbr", "-cq", "19", "-b:v", "0",
|
||||
"-preset", "p6", "-tune", "hq",
|
||||
"-tag:v", "hvc1", "-acodec", "copy", dst]
|
||||
return ["ffmpeg", "-y", "-i", src,
|
||||
"-vcodec", "libx265", "-crf", "19", "-preset", "slow",
|
||||
"-tag:v", "hvc1", "-acodec", "copy", dst]
|
||||
|
||||
|
||||
def _encode_worker(label, cmd, hw, fallback_cmd, out):
|
||||
proc = _ffmpeg(cmd)
|
||||
if proc.returncode != 0 and hw:
|
||||
print(f" [{label}] NVENC failed, retrying in software...")
|
||||
proc = _ffmpeg(fallback_cmd)
|
||||
out[label] = proc
|
||||
|
||||
|
||||
def process(src, orig_size, hw):
|
||||
name = os.path.basename(src)
|
||||
archive_dst = os.path.join(ARCHIVE_DIR, name)
|
||||
|
||||
if os.path.exists(archive_dst):
|
||||
print(f" Skip {name} — already archived")
|
||||
return 0
|
||||
|
||||
proxy_tmp = src + ".proxy.mp4"
|
||||
# Encode archive to local /tmp first — never write directly to a slow SMB share
|
||||
archive_tmp = os.path.join(tempfile.gettempdir(), name + ".archive.mp4")
|
||||
|
||||
print(f"[{name}] {orig_size/(1024*1024):.0f} MB ({'NVENC' if hw else 'software'})")
|
||||
t0 = time.time()
|
||||
|
||||
results = {}
|
||||
threads = [
|
||||
threading.Thread(target=_encode_worker, args=(
|
||||
"proxy",
|
||||
_proxy_cmd(src, proxy_tmp, hw), hw,
|
||||
_proxy_cmd(src, proxy_tmp, False),
|
||||
results,
|
||||
)),
|
||||
threading.Thread(target=_encode_worker, args=(
|
||||
"archive",
|
||||
_archive_cmd(src, archive_tmp, hw), hw,
|
||||
_archive_cmd(src, archive_tmp, False),
|
||||
results,
|
||||
)),
|
||||
]
|
||||
for t in threads: t.start()
|
||||
for t in threads: t.join()
|
||||
|
||||
failed = {k: v for k, v in results.items() if v.returncode != 0}
|
||||
if failed:
|
||||
for label, proc in failed.items():
|
||||
print(f" [{label}] error: {proc.stderr.decode()[-300:]}")
|
||||
for f in (proxy_tmp, archive_tmp):
|
||||
if os.path.exists(f): os.remove(f)
|
||||
return 0
|
||||
|
||||
proxy_mb = os.path.getsize(proxy_tmp) / (1024*1024)
|
||||
archive_mb = os.path.getsize(archive_tmp) / (1024*1024)
|
||||
orig_mb = orig_size / (1024*1024)
|
||||
print(f" proxy {orig_mb:.0f}->{proxy_mb:.0f} MB "
|
||||
f"archive {orig_mb:.0f}->{archive_mb:.0f} MB "
|
||||
f"({time.time()-t0:.1f}s) — pushing archive to share...")
|
||||
|
||||
try:
|
||||
shutil.move(archive_tmp, archive_dst)
|
||||
except Exception as e:
|
||||
print(f" Archive move failed: {e} — aborting, original intact")
|
||||
for f in (proxy_tmp, archive_tmp):
|
||||
if os.path.exists(f): os.remove(f)
|
||||
return 0
|
||||
|
||||
# Atomic swap: original -> proxy (same filesystem)
|
||||
os.replace(proxy_tmp, src)
|
||||
freed = orig_size - os.path.getsize(src)
|
||||
print(f" Done freed locally: {freed/(1024*1024):.0f} MB")
|
||||
return freed
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
hw = has_nvenc()
|
||||
print(f"Encoder: {'hevc_nvenc (A6000, 2x parallel)' if hw else 'libx265 (software)'}")
|
||||
print(f"Clips: {CLIPS_DIR}")
|
||||
print(f"Archive: {ARCHIVE_DIR}\n")
|
||||
|
||||
if not os.path.isdir(ARCHIVE_DIR):
|
||||
print(f"ERROR: archive not reachable: {ARCHIVE_DIR}")
|
||||
raise SystemExit(1)
|
||||
|
||||
files = sorted(
|
||||
[(fp, os.path.getsize(fp))
|
||||
for fp in glob.glob(os.path.join(CLIPS_DIR, "*.mp4"))
|
||||
if os.path.isfile(fp) and os.path.getsize(fp) > SIZE_THRESHOLD_BYTES],
|
||||
key=lambda x: x[1], reverse=True,
|
||||
)
|
||||
print(f"Found {len(files)} files > {SIZE_THRESHOLD_MB} MB\n")
|
||||
|
||||
total_freed = 0
|
||||
done = 0
|
||||
for path, size in files:
|
||||
freed = process(path, size, hw)
|
||||
total_freed += freed
|
||||
if freed:
|
||||
done += 1
|
||||
if done >= MAX_PER_RUN:
|
||||
print(f"Reached {MAX_PER_RUN}-file limit.")
|
||||
break
|
||||
|
||||
print(f"\nDone. Processed: {done} Freed locally: {total_freed/(1024*1024):.0f} MB")
|
||||
Reference in New Issue
Block a user