fix mp4 compatibility issues
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user