undo the media format convertion action
This commit is contained in:
@@ -1,11 +1,10 @@
|
|||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
import uuid
|
import uuid
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from .audio_metrics import (
|
from .audio_metrics import (
|
||||||
AudioAnalysisError,
|
AudioAnalysisError,
|
||||||
@@ -86,134 +85,6 @@ def _ensure_webm_cues_front(filepath: Path) -> None:
|
|||||||
tmp.unlink(missing_ok=True)
|
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:
|
class VideoProcessor:
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -240,7 +111,6 @@ class VideoProcessor:
|
|||||||
work_path: Optional[Path] = None
|
work_path: Optional[Path] = None
|
||||||
try:
|
try:
|
||||||
_ensure_webm_cues_front(media_path)
|
_ensure_webm_cues_front(media_path)
|
||||||
_ensure_ios_compatible_media(media_path)
|
|
||||||
if self.transcriber.available:
|
if self.transcriber.available:
|
||||||
work_path = self.settings.work_dir / f"{video_hash}-{uuid.uuid4().hex}.wav"
|
work_path = self.settings.work_dir / f"{video_hash}-{uuid.uuid4().hex}.wav"
|
||||||
extract_audio(media_path, work_path)
|
extract_audio(media_path, work_path)
|
||||||
|
|||||||
Reference in New Issue
Block a user