added transcoding to mp4 in import to ensure web playback.

This commit is contained in:
Bryan Van Deusen
2026-01-20 16:10:19 -05:00
parent cb3897c0b0
commit 87bcb633f6
4 changed files with 175 additions and 31 deletions
+87 -3
View File
@@ -25,6 +25,7 @@ from app import db
from app.models import ImageRecord, Tag, ArchiveRecord
FFMPEG_THUMB_TIMEOUT = int(os.environ.get("THUMBS_FFMPEG_TIMEOUT", "60")) # seconds
FFMPEG_TRANSCODE_TIMEOUT = int(os.environ.get("FFMPEG_TRANSCODE_TIMEOUT", "600")) # 10 min default
Image.MAX_IMAGE_PIXELS = int(os.environ.get("PIL_MAX_IMAGE_PIXELS", "178956970"))
ARCHIVE_NUM_WIDTH = int(os.environ.get("ARCHIVE_NUM_WIDTH", "4"))
@@ -36,9 +37,10 @@ ARCHIVE_NUM_WIDTH = int(os.environ.get("ARCHIVE_NUM_WIDTH", "4"))
THUMB_SIZE = (400, 400)
# Media and archive extensions we handle
ALLOWED_MEDIA_EXTS = (
".jpg", ".jpeg", ".jfif", ".png", ".gif", ".bmp", ".tiff", ".webp", ".mp4", ".mov"
)
IMAGE_EXTS = (".jpg", ".jpeg", ".jfif", ".png", ".gif", ".bmp", ".tiff", ".webp")
VIDEO_EXTS = (".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v", ".wmv", ".flv")
VIDEO_EXTS_NEED_TRANSCODE = (".mov", ".avi", ".mkv", ".webm", ".m4v", ".wmv", ".flv") # Non-MP4 formats
ALLOWED_MEDIA_EXTS = IMAGE_EXTS + VIDEO_EXTS
ALLOWED_ARCHIVE_EXTS = (
".zip", ".rar", ".cbr", ".7z",
".tar", ".tar.gz", ".tgz", ".tar.bz2", ".tbz2", ".tar.xz", ".txz"
@@ -1025,6 +1027,88 @@ def generate_video_thumbnail_mirrored(video_path: str, time_position: str = "00:
return generate_video_thumbnail(str(video_path_p), str(thumb_path), time_position=time_position)
def transcode_video_to_mp4(input_path: str, output_path: str = None) -> str | None:
"""
Transcode a video to H.264 MP4 for universal browser playback.
Args:
input_path: Path to source video file
output_path: Optional destination path. If None, replaces extension with .mp4
in a temp location.
Returns:
Path to transcoded MP4 file on success, None on failure.
"""
input_p = Path(input_path)
# Determine output path
if output_path is None:
# Create temp file with .mp4 extension
output_path = str(input_p.with_suffix(".mp4"))
if output_path == input_path:
# Already .mp4, use temp suffix
output_path = str(input_p.with_suffix(".transcoded.mp4"))
try:
os.makedirs(os.path.dirname(output_path), exist_ok=True)
# FFmpeg command for H.264/AAC transcoding with good browser compatibility
# -movflags +faststart: Enables progressive playback (moov atom at start)
# -preset medium: Balance between speed and compression
# -crf 23: Good quality (lower = better, 18-28 typical range)
# -c:v libx264: H.264 video codec
# -c:a aac: AAC audio codec
# -pix_fmt yuv420p: Pixel format for maximum compatibility
cmd = [
"ffmpeg", "-y",
"-i", str(input_path),
"-c:v", "libx264",
"-preset", "medium",
"-crf", "23",
"-pix_fmt", "yuv420p",
"-c:a", "aac",
"-b:a", "128k",
"-movflags", "+faststart",
str(output_path),
]
print(f"[INFO] Transcoding video: {input_path} -> {output_path}")
result = subprocess.run(
cmd,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=FFMPEG_TRANSCODE_TIMEOUT
)
print(f"[INFO] Transcoding complete: {output_path}")
return output_path
except subprocess.TimeoutExpired:
print(f"[WARN] Video transcode timed out after {FFMPEG_TRANSCODE_TIMEOUT}s: {input_path}")
# Clean up partial output
if os.path.exists(output_path):
os.remove(output_path)
return None
except subprocess.CalledProcessError as e:
print(f"[WARN] Failed to transcode video {input_path}: {e}")
if e.stderr:
print(f"[WARN] FFmpeg stderr: {e.stderr.decode('utf-8', errors='ignore')[:500]}")
# Clean up partial output
if os.path.exists(output_path):
os.remove(output_path)
return None
except Exception as e:
print(f"[WARN] Unexpected error transcoding video {input_path}: {e}")
if os.path.exists(output_path):
os.remove(output_path)
return None
def needs_transcode(file_path: str) -> bool:
"""Check if a video file needs transcoding to MP4."""
ext = Path(file_path).suffix.lower()
return ext in VIDEO_EXTS_NEED_TRANSCODE
def calculate_hash(file_path: str) -> str:
hash_func = hashlib.sha256()