undo the media format convertion action
This commit is contained in:
@@ -1,11 +1,10 @@
|
||||
import json
|
||||
import logging
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from .audio_metrics import (
|
||||
AudioAnalysisError,
|
||||
@@ -86,134 +85,6 @@ def _ensure_webm_cues_front(filepath: Path) -> None:
|
||||
tmp.unlink(missing_ok=True)
|
||||
|
||||
|
||||
_IOS_VIDEO_CODECS = {"h264", "hevc"}
|
||||
_IOS_AUDIO_CODECS = {"aac", "mp3", "ac3", "eac3", "alac"}
|
||||
|
||||
|
||||
def _ffprobe_bin() -> Optional[str]:
|
||||
ffprobe = shutil.which("ffprobe")
|
||||
if ffprobe:
|
||||
return ffprobe
|
||||
ffmpeg = shutil.which("ffmpeg")
|
||||
if ffmpeg:
|
||||
candidate = str(Path(ffmpeg).with_name("ffprobe"))
|
||||
if Path(candidate).is_file():
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def _probe_stream_codecs(filepath: Path) -> Tuple[Optional[str], Optional[str]]:
|
||||
ffprobe = _ffprobe_bin()
|
||||
if ffprobe is None:
|
||||
return None, None
|
||||
completed = subprocess.run(
|
||||
[
|
||||
ffprobe,
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-show_streams",
|
||||
"-of",
|
||||
"json",
|
||||
str(filepath),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
return None, None
|
||||
try:
|
||||
payload = json.loads(completed.stdout or "{}")
|
||||
except json.JSONDecodeError:
|
||||
return None, None
|
||||
video_codec = None
|
||||
audio_codec = None
|
||||
for stream in payload.get("streams") or []:
|
||||
codec_type = stream.get("codec_type")
|
||||
codec_name = stream.get("codec_name")
|
||||
if codec_type == "video" and video_codec is None and codec_name not in {"mjpeg", "png"}:
|
||||
video_codec = codec_name
|
||||
elif codec_type == "audio" and audio_codec is None:
|
||||
audio_codec = codec_name
|
||||
return video_codec, audio_codec
|
||||
|
||||
|
||||
def _needs_ios_compatible_transcode(video_codec: Optional[str], audio_codec: Optional[str]) -> bool:
|
||||
if video_codec is None and audio_codec is None:
|
||||
return False
|
||||
video_ok = video_codec is None or video_codec in _IOS_VIDEO_CODECS
|
||||
audio_ok = audio_codec is None or audio_codec in _IOS_AUDIO_CODECS
|
||||
return not (video_ok and audio_ok)
|
||||
|
||||
|
||||
def _ensure_ios_compatible_media(filepath: Path) -> None:
|
||||
"""Transcode AV1/VP9/Opus etc. to H.264/AAC so iOS AVPlayer can show video."""
|
||||
video_codec, audio_codec = _probe_stream_codecs(filepath)
|
||||
if not _needs_ios_compatible_transcode(video_codec, audio_codec):
|
||||
return
|
||||
ffmpeg = shutil.which("ffmpeg")
|
||||
if ffmpeg is None:
|
||||
raise RuntimeError(
|
||||
f"iOS cannot play {video_codec}/{audio_codec}; ffmpeg is required to transcode {filepath.name}"
|
||||
)
|
||||
tmp = filepath.with_suffix(".ios_tmp.mp4")
|
||||
command = [
|
||||
ffmpeg,
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-y",
|
||||
"-i",
|
||||
str(filepath),
|
||||
"-map",
|
||||
"0:v:0",
|
||||
"-map",
|
||||
"0:a:0?",
|
||||
"-vf",
|
||||
r"scale=w=min(1920\,iw):h=-2",
|
||||
"-c:v",
|
||||
"libx264",
|
||||
"-preset",
|
||||
"veryfast",
|
||||
"-crf",
|
||||
"23",
|
||||
"-pix_fmt",
|
||||
"yuv420p",
|
||||
"-c:a",
|
||||
"aac",
|
||||
"-b:a",
|
||||
"128k",
|
||||
"-ac",
|
||||
"2",
|
||||
"-movflags",
|
||||
"+faststart",
|
||||
str(tmp),
|
||||
]
|
||||
logger.info(
|
||||
"Transcoding %s for iOS playback (%s/%s -> h264/aac)",
|
||||
filepath.name,
|
||||
video_codec,
|
||||
audio_codec,
|
||||
)
|
||||
try:
|
||||
completed = subprocess.run(command, capture_output=True, text=True, timeout=7200)
|
||||
if completed.returncode != 0 or not tmp.is_file() or tmp.stat().st_size <= 0:
|
||||
message = (completed.stderr or "").strip() or "unknown ffmpeg error"
|
||||
raise RuntimeError(
|
||||
f"Could not transcode {filepath.name} from {video_codec}/{audio_codec} to H.264/AAC: {message[-2000:]}"
|
||||
)
|
||||
tmp.replace(filepath)
|
||||
except Exception:
|
||||
tmp.unlink(missing_ok=True)
|
||||
raise
|
||||
video_codec, audio_codec = _probe_stream_codecs(filepath)
|
||||
if _needs_ios_compatible_transcode(video_codec, audio_codec):
|
||||
raise RuntimeError(
|
||||
f"Transcoded {filepath.name} is still not iOS-compatible ({video_codec}/{audio_codec})"
|
||||
)
|
||||
logger.info("iOS-compatible transcode finished: %s", filepath.name)
|
||||
|
||||
|
||||
class VideoProcessor:
|
||||
def __init__(
|
||||
@@ -240,7 +111,6 @@ class VideoProcessor:
|
||||
work_path: Optional[Path] = None
|
||||
try:
|
||||
_ensure_webm_cues_front(media_path)
|
||||
_ensure_ios_compatible_media(media_path)
|
||||
if self.transcriber.available:
|
||||
work_path = self.settings.work_dir / f"{video_hash}-{uuid.uuid4().hex}.wav"
|
||||
extract_audio(media_path, work_path)
|
||||
|
||||
Reference in New Issue
Block a user