added transcoding to mp4 in import to ensure web playback.
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user