194 lines
6.5 KiB
Python
194 lines
6.5 KiB
Python
import io
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import tempfile
|
|
from typing import Tuple, List
|
|
from PIL import Image
|
|
import requests
|
|
|
|
FFMPEG = os.getenv("FFMPEG_BIN", "ffmpeg")
|
|
FFPROBE = os.getenv("FFPROBE_BIN", "ffprobe")
|
|
|
|
# ------------------------
|
|
# Shared helpers
|
|
# ------------------------
|
|
|
|
def _bool(name: str, default: bool) -> bool:
|
|
v = os.getenv(name)
|
|
if v is None:
|
|
return default
|
|
return v.strip().lower() in ("1", "true", "yes", "y")
|
|
|
|
def _int(name: str, default: int) -> int:
|
|
try:
|
|
return int(os.getenv(name, str(default)))
|
|
except Exception:
|
|
return default
|
|
|
|
def _float(name: str, default: float) -> float:
|
|
try:
|
|
return float(os.getenv(name, str(default)))
|
|
except Exception:
|
|
return default
|
|
|
|
def _parse_max_size(default: Tuple[int, int] = (1600, 1600)) -> Tuple[int, int]:
|
|
raw = os.getenv("IMAGE_MAX_SIZE", f"{default[0]}x{default[1]}").strip().lower()
|
|
m = re.match(r"^\s*(\d+)\s*[x,]\s*(\d+)\s*$", raw)
|
|
if not m:
|
|
return default
|
|
return (int(m.group(1)), int(m.group(2)))
|
|
|
|
def download_bytes(url: str, timeout: int = 30) -> bytes:
|
|
r = requests.get(url, stream=True, timeout=timeout)
|
|
r.raise_for_status()
|
|
return r.content
|
|
|
|
# ------------------------
|
|
# Image processing (WebP)
|
|
# ------------------------
|
|
|
|
def process_image_to_webp_single(image_bytes: bytes) -> Tuple[str, bytes, str]:
|
|
"""
|
|
Resize image to:
|
|
- scale percent from .env (default 0.85)
|
|
- and cap to max size from .env (default 1600x1600)
|
|
Save as WebP with quality from .env (default 85).
|
|
|
|
Returns (variant_name, data, content_type)
|
|
variant_name: 'webp'
|
|
"""
|
|
scale_percent = _float("IMAGE_SCALE_PERCENT", 0.85)
|
|
max_w, max_h = _parse_max_size((1600, 1600))
|
|
quality = _int("IMAGE_QUALITY", 85)
|
|
|
|
img = Image.open(io.BytesIO(image_bytes)).convert("RGB")
|
|
orig_w, orig_h = img.size
|
|
|
|
# First apply percentage scaling
|
|
target_w = max(1, int(orig_w * scale_percent))
|
|
target_h = max(1, int(orig_h * scale_percent))
|
|
|
|
# Then cap to max size if still too large
|
|
ratio_w = max_w / target_w if target_w > max_w else 1.0
|
|
ratio_h = max_h / target_h if target_h > max_h else 1.0
|
|
cap_ratio = min(ratio_w, ratio_h)
|
|
if cap_ratio < 1.0:
|
|
target_w = max(1, int(target_w * cap_ratio))
|
|
target_h = max(1, int(target_h * cap_ratio))
|
|
|
|
# Resize using LANCZOS
|
|
resized = img.resize((target_w, target_h), Image.LANCZOS)
|
|
|
|
out = io.BytesIO()
|
|
# method=6 is a good quality/speed balance for WebP
|
|
resized.save(out, format="WEBP", quality=quality, method=6)
|
|
return ("webp", out.getvalue(), "image/webp")
|
|
|
|
# ------------------------
|
|
# Video processing (H.264 @ 480p)
|
|
# ------------------------
|
|
|
|
def _probe_streams(path: str) -> dict:
|
|
"""Return ffprobe json for streams (video/audio codecs, dimensions)."""
|
|
# Minimal probing; not strictly required but helps with SIMPLE mode decisions
|
|
cmd = [
|
|
FFPROBE, "-v", "error", "-select_streams", "v:0",
|
|
"-show_entries", "stream=codec_name,width,height",
|
|
"-of", "json", path
|
|
]
|
|
try:
|
|
out = subprocess.run(cmd, check=True, capture_output=True).stdout
|
|
import json
|
|
return json.loads(out.decode("utf-8"))
|
|
except Exception:
|
|
return {}
|
|
|
|
def transcode_video_480_h264(video_bytes: bytes) -> Tuple[str, bytes, str]:
|
|
"""
|
|
Transcode to MP4 (H.264/AAC), height=VIDEO_HEIGHT (default 480), CRF=VIDEO_CRF (default 23).
|
|
Adds +faststart unless disabled. Uses PRESET (default veryfast).
|
|
If VIDEO_SIMPLE_MODE=true:
|
|
- Use 'ultrafast' preset automatically
|
|
- If input already H.264/AAC and <= target height, attempt stream-copy with -movflags +faststart
|
|
(very quick) — otherwise fallback to a minimal transcode.
|
|
Returns (variant_name, data, content_type) where variant_name='480p'
|
|
"""
|
|
height = _int("VIDEO_HEIGHT", 480)
|
|
crf = _int("VIDEO_CRF", 23)
|
|
preset = os.getenv("VIDEO_PRESET", "veryfast")
|
|
faststart = _bool("VIDEO_FASTSTART", True)
|
|
simple = _bool("VIDEO_SIMPLE_MODE", False)
|
|
|
|
with tempfile.TemporaryDirectory() as td:
|
|
in_path = os.path.join(td, "in.bin")
|
|
out_path = os.path.join(td, f"out-{height}p.mp4")
|
|
with open(in_path, "wb") as f:
|
|
f.write(video_bytes)
|
|
|
|
vf = f"scale=-2:{height}"
|
|
|
|
# SIMPLE MODE quick path
|
|
if simple:
|
|
# Try to detect if we can safely stream copy
|
|
meta = _probe_streams(in_path)
|
|
v_ok = False
|
|
try:
|
|
st = meta.get("streams", [{}])[0]
|
|
vcodec = st.get("codec_name")
|
|
v_h = int(st.get("height", 10**9))
|
|
v_ok = (vcodec == "h264" and v_h <= height)
|
|
except Exception:
|
|
v_ok = False
|
|
|
|
if v_ok:
|
|
# Quick remux with faststart; try copy video/audio
|
|
cmd = [FFMPEG, "-y", "-i", in_path, "-c", "copy"]
|
|
if faststart:
|
|
cmd += ["-movflags", "+faststart"]
|
|
cmd += [out_path]
|
|
try:
|
|
subprocess.run(cmd, check=True, capture_output=True)
|
|
with open(out_path, "rb") as f:
|
|
return (f"{height}p", f.read(), "video/mp4")
|
|
except Exception:
|
|
# fall back to quick transcode below
|
|
pass
|
|
|
|
# Quick transcode with ultrafast
|
|
cmd = [
|
|
FFMPEG, "-y",
|
|
"-i", in_path,
|
|
"-vf", vf,
|
|
"-c:v", "libx264",
|
|
"-preset", "ultrafast", # force quickest in simple mode
|
|
"-crf", str(crf),
|
|
"-c:a", "aac",
|
|
"-b:a", "128k",
|
|
]
|
|
if faststart:
|
|
cmd += ["-movflags", "+faststart"]
|
|
cmd += [out_path]
|
|
subprocess.run(cmd, check=True, capture_output=True)
|
|
with open(out_path, "rb") as f:
|
|
return (f"{height}p", f.read(), "video/mp4")
|
|
|
|
# Normal path (preset configurable)
|
|
cmd = [
|
|
FFMPEG, "-y",
|
|
"-i", in_path,
|
|
"-vf", vf,
|
|
"-c:v", "libx264",
|
|
"-preset", preset,
|
|
"-crf", str(crf),
|
|
"-c:a", "aac",
|
|
"-b:a", "128k",
|
|
]
|
|
if faststart:
|
|
cmd += ["-movflags", "+faststart"]
|
|
cmd += [out_path]
|
|
|
|
subprocess.run(cmd, check=True, capture_output=True)
|
|
with open(out_path, "rb") as f:
|
|
return (f"{height}p", f.read(), "video/mp4")
|