fix mp4 compatibility issues

This commit is contained in:
zaiun xu
2026-04-10 18:16:15 +08:00
parent 09736f9e15
commit 62bff77fa0
15 changed files with 748 additions and 98 deletions

View File

@@ -241,9 +241,11 @@ def _split_sbs_video(src: Path, left_dst: Path, right_dst: Path) -> bool:
Returns True if split succeeded, False otherwise (caller should fall back to copy).
"""
ffmpeg_path = _get_ffmpeg_path()
ffprobe_path = str(Path(ffmpeg_path).parent / "ffprobe")
probe = subprocess.run(
[
"ffprobe", "-v", "quiet", "-print_format", "json",
ffprobe_path, "-v", "quiet", "-print_format", "json",
"-show_streams", str(src),
],
capture_output=True, text=True,
@@ -263,19 +265,282 @@ def _split_sbs_video(src: Path, left_dst: Path, right_dst: Path) -> bool:
if half_w < 1 or w < h:
return False
encoder, encoder_options, _ = _get_h264_encoder()
for crop, dst in [
(f"crop={half_w}:{h}:{half_w}:0", left_dst),
(f"crop={half_w}:{h}:0:0", right_dst),
]:
r = subprocess.run(
["ffmpeg", "-y", "-i", str(src), "-vf", crop, "-an", "-q:v", "5", str(dst)],
capture_output=True, text=True,
)
cmd = [ffmpeg_path, "-y", "-i", str(src), "-vf", crop, "-an"]
if encoder:
cmd.extend(["-c:v", encoder, "-pix_fmt", "yuv420p", "-movflags", "+faststart"])
cmd.extend(encoder_options)
else:
cmd.extend(["-q:v", "5"])
cmd.append(str(dst))
r = subprocess.run(cmd, capture_output=True, text=True)
if r.returncode != 0:
return False
return True
def _get_ffmpeg_path() -> str:
"""获取可用的 ffmpeg 路径。优先使用项目配置的 ffmpeg。"""
# 优先使用项目目录下的 ffmpeg
project_ffmpeg = Path("/home/ubuntu/projects/FishServer/tools/ffmpeg/bin/ffmpeg")
if project_ffmpeg.is_file():
return str(project_ffmpeg)
# 尝试系统路径
system_paths = ["/usr/bin/ffmpeg", "/usr/local/bin/ffmpeg"]
for path in system_paths:
if Path(path).is_file():
return path
# 回退到 PATH 中的 ffmpeg
return "ffmpeg"
def _get_h264_encoder() -> tuple[str, list[str], str]:
"""检测可用的H.264编码器,返回 (encoder_name, options, ffmpeg_path)。
优先使用 libx264纯软件最可靠硬件编码器需要实际测试才能确认可用。
"""
encoders_to_try = [
("libx264", ["-preset", "fast", "-crf", "23"]),
("h264_nvenc", ["-preset", "fast"]),
("libopenh264", []),
]
ffmpeg_path = _get_ffmpeg_path()
try:
result = subprocess.run(
[ffmpeg_path, "-encoders"],
capture_output=True, text=True, timeout=10
)
encoders_output = result.stdout
for encoder, options in encoders_to_try:
if encoder in encoders_output:
return encoder, options, ffmpeg_path
except Exception:
pass
return "", [], ffmpeg_path
def _get_x264_path() -> Optional[str]:
"""检测系统上是否有可用的 x264 命令行工具。"""
for path in ["/usr/bin/x264", "/usr/local/bin/x264", "x264"]:
if path == "x264":
try:
result = subprocess.run(["which", "x264"], capture_output=True, text=True, timeout=5)
if result.returncode == 0 and result.stdout.strip():
return result.stdout.strip()
except Exception:
pass
elif Path(path).is_file():
return path
return None
def _transcode_with_x264(src: Path, dst: Path) -> bool:
"""使用 x264 命令行工具将视频转码为 H.264。
这是当 ffmpeg 的 H.264 编码器都不可用时(如 libopenh264 版本不匹配)的最后备选方案。
通过 ffmpeg 提取原始 YUV 帧,然后用 x264 编码。
"""
import tempfile
import shutil
x264_path = _get_x264_path()
ffmpeg_path = _get_ffmpeg_path()
if not x264_path:
logger.debug("[FishMeasure] x264 not available")
return False
# 首先用 ffprobe 获取视频信息
try:
probe = subprocess.run(
["ffprobe", "-v", "quiet", "-print_format", "json", "-show_streams", str(src)],
capture_output=True, text=True, timeout=10
)
import json as _json
streams = _json.loads(probe.stdout).get("streams", [])
vstream = next((s for s in streams if s.get("codec_type") == "video"), None)
if not vstream:
return False
width = int(vstream["width"])
height = int(vstream["height"])
fps_str = vstream.get("r_frame_rate", "25/1")
# 解析 fps (可能是 "30/1" 或 "30000/1001" 格式)
if "/" in fps_str:
num, den = map(int, fps_str.split("/"))
fps = num / den if den != 0 else 25.0
else:
fps = float(fps_str)
except Exception as e:
logger.debug("[FishMeasure] x264 probe failed: {}", str(e))
return False
tmp_yuv = None
try:
# 创建临时 YUV 文件
with tempfile.NamedTemporaryFile(suffix=".yuv", delete=False) as f:
tmp_yuv = Path(f.name)
# 步骤1: 用 ffmpeg 提取 YUV 原始帧
extract_cmd = [
ffmpeg_path, "-y", "-i", str(src),
"-f", "rawvideo",
"-pix_fmt", "yuv420p",
str(tmp_yuv)
]
result = subprocess.run(extract_cmd, capture_output=True, text=True, timeout=300)
if result.returncode != 0:
logger.debug("[FishMeasure] x264: YUV extraction failed: {}", result.stderr[-200:] if result.stderr else "unknown")
return False
# 步骤2: 用 x264 编码
# x264 需要特定格式的输入参数
encode_cmd = [
x264_path,
"--input-res", f"{width}x{height}",
"--fps", str(fps),
"--preset", "fast",
"--crf", "23",
"--output-csp", "i420",
"-o", str(dst),
str(tmp_yuv)
]
result = subprocess.run(encode_cmd, capture_output=True, text=True, timeout=600)
if result.returncode == 0 and dst.is_file():
logger.info("[FishMeasure] x264 transcoding SUCCESS: {} ({} bytes)", dst.name, dst.stat().st_size)
return True
else:
stderr = result.stderr[-300:] if result.stderr else "Unknown error"
logger.warning("[FishMeasure] x264 transcoding FAILED: {}", stderr)
if dst.exists():
dst.unlink()
return False
except Exception as e:
logger.warning("[FishMeasure] x264 transcoding exception: {}", str(e))
if dst.exists():
dst.unlink()
return False
finally:
if tmp_yuv and tmp_yuv.exists():
tmp_yuv.unlink()
def _transcode_fallback(src: Path, dst: Path) -> bool:
"""备选转码方案提取帧为图像序列然后用ffmpeg编码为H.264。
这种方法避免编码器直接读取 mp4v 文件的兼容性问题。
"""
import tempfile
import shutil
encoder, encoder_options, ffmpeg_path = _get_h264_encoder()
if not encoder:
return False
tmp_dir = tempfile.mkdtemp()
try:
# 步骤1: 提取帧为 jpg 序列
frames_pattern = f"{tmp_dir}/frame_%04d.jpg"
extract_cmd = [
ffmpeg_path, "-y", "-i", str(src),
"-q:v", "2", # 高质量
frames_pattern
]
result = subprocess.run(extract_cmd, capture_output=True, text=True, timeout=60)
if result.returncode != 0:
logger.debug("[FishMeasure] Fallback: frame extraction failed: {}", result.stderr[-200:] if result.stderr else "unknown")
return False
# 步骤2: 从帧编码为 H.264 MP4
encode_cmd = [
ffmpeg_path, "-y",
"-i", frames_pattern,
"-c:v", encoder,
"-pix_fmt", "yuv420p",
"-movflags", "+faststart",
"-an",
]
encode_cmd.extend(encoder_options)
encode_cmd.append(str(dst))
result = subprocess.run(encode_cmd, capture_output=True, text=True, timeout=120)
if result.returncode == 0 and dst.is_file():
logger.info("[FishMeasure] Fallback transcoding SUCCESS: {} ({} bytes)", dst.name, dst.stat().st_size)
return True
else:
stderr = result.stderr[-300:] if result.stderr else "Unknown error"
logger.warning("[FishMeasure] Fallback transcoding FAILED: {}", stderr)
if dst.exists():
dst.unlink()
return False
except Exception as e:
logger.warning("[FishMeasure] Fallback transcoding exception: {}", str(e))
if dst.exists():
dst.unlink()
return False
finally:
# 清理临时目录
shutil.rmtree(tmp_dir, ignore_errors=True)
def _transcode_to_h264(src: Path, dst: Path) -> bool:
"""使用 ffmpeg 将视频转码为 H.264 (浏览器兼容格式)。
尝试多种H.264编码器,包括软件编码和硬件加速编码。
如果直接转码失败,依次尝试备选方案:
1. 提取帧重新编码
2. 使用 x264 命令行工具(当 ffmpeg 的 H.264 编码器都不可用时)
"""
encoder, encoder_options, ffmpeg_path = _get_h264_encoder()
# 如果有可用的 ffmpeg H.264 编码器,先尝试直接转码
if encoder:
try:
# 基础参数
cmd = [
ffmpeg_path, "-y", "-i", str(src),
"-c:v", encoder,
"-pix_fmt", "yuv420p", # 确保兼容性
"-movflags", "+faststart", # 优化网络播放moov前置
"-an", # 去除音频
]
cmd.extend(encoder_options)
cmd.append(str(dst))
logger.info("[FishMeasure] Transcoding with {} using {}: {} -> {}", encoder, ffmpeg_path, src.name, dst.name)
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=300
)
if result.returncode == 0 and dst.is_file():
logger.info("[FishMeasure] Transcoding SUCCESS: {} ({} bytes)", dst.name, dst.stat().st_size)
return True
else:
stderr = result.stderr[-500:] if result.stderr else "Unknown error"
logger.warning("[FishMeasure] Direct transcoding FAILED, trying fallback: {}", stderr)
# 尝试备选方案1: 提取帧重新编码
if _transcode_fallback(src, dst):
return True
# 备选方案1失败尝试 x264
logger.info("[FishMeasure] Fallback failed, trying x264...")
return _transcode_with_x264(src, dst)
except Exception as e:
logger.warning("[FishMeasure] Transcoding exception: {}", str(e))
if _transcode_fallback(src, dst):
return True
return _transcode_with_x264(src, dst)
else:
# 没有可用的 ffmpeg H.264 编码器,直接尝试 x264
logger.warning("[FishMeasure] No H.264 encoder available in ffmpeg, trying x264...")
return _transcode_with_x264(src, dst)
def _publish_media(
left: Optional[Path],
right: Optional[Path],
@@ -298,7 +563,13 @@ def _publish_media(
def publish(src: Optional[Path], dst: Path) -> str:
if src is None or not src.is_file():
return ""
shutil.copy2(src, dst)
# 尝试转码为 H.264,如果失败则直接复制原文件
if _transcode_to_h264(src, dst):
logger.info("[FishMeasure] transcoded to H.264: {} -> {}", src.name, dst.name)
else:
# 转码失败,直接复制原文件
shutil.copy2(src, dst)
logger.warning("[FishMeasure] copied without transcoding: {} -> {}", src.name, dst.name)
return f"{base}/media/{dst.name}"
vl = publish(left, left_dst)