fix(ingest): 转码滤镜改按长边 1920 封顶(修复超宽屏误降级)+ 合并 ffprobe
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2m6s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m46s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 28s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 4m28s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 5m55s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 6m18s
AI Code Review / AI Code Review (pull_request) Successful in 9m20s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 13m37s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 15m54s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 21m45s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 29m4s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 20m45s
CI/CD Pipeline / CI Gate (pull_request) Successful in 1m7s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 4m14s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 4m46s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 401h44m31s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 402h13m38s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 402h13m38s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 401h44m31s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 402h15m35s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 401h44m31s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 402h15m35s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 401h44m33s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 402h15m38s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 401h44m33s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 402h15m40s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 401h44m33s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 402h15m24s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 402h15m26s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 402h15m27s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 402h15m42s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 402h15m46s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 402h45m58s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 402h49m39s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 402h49m45s

Code Review 阻塞问题修复 + 建议采纳:

1. 缩放规则与校验规则对齐(真 bug):
   旧滤镜仅按短边 1080 触发缩放(横屏看 ih、竖屏看 iw),对超宽屏
   (如 4000x1000)短边不超 1080 完全不缩放,产物长边 4000 超过
   validate_transcode_output 的 1920 上限,转码被判定失败而降级用
   原始 HEVC——浏览器仍无法播放。改为统一滤镜:
     scale=w=if(gte(iw\,ih)\,min(1920\,iw)\,-2):
           h=if(gt(ih\,iw)\,min(1920\,ih)\,-2),format=yuv420p
   横屏限宽、竖屏限高、短边 -2 自适应,min() 保证小视频不放大;
   build_transcode_vf 不再需要 is_portrait 参数(方向无关)。
   ffmpeg 实测:1920x1080→1920x1080、4K竖屏→1080x1920、
   4000x1000→1920x480、1080x2400→864x1920、640x360 不放大,均无 side data。

2. 合并 ffprobe(性能建议):新增 probe_video_info(path) 一次
   -show_streams -of json 同时解析 width/height 与 Display Matrix
   rotation(用全量 JSON 输出,规避 show_entries 嵌套 section 名在
   ffmpeg 4.x/7.x 不一致的问题);主流程两次探测合并为一次。
   probe_rotation/probe_dimensions 保留供测试与 validate 复用。

3. 临时文件清理:转码 try 块 finally 中 _tc_tmp.unlink() 在 #1449
   即已存在(develop 基线代码),本轮 review 该条为窗口外误判,
   已在 PR 评论说明。

测试:
- TestBuildTranscodeVF 重写为统一滤镜断言(逗号转义、无 transpose、
  横竖双分支 min(1920)、yuv420p 后缀)。
- 新增 TestProbeVideoInfo(真实 ffprobe 320x240 无 rotation;
  不存在文件返回三元 None)。
- 端到端新增 test_ultra_wide_long_edge_capped(4000x1000→1920x480
  且 validate 通过);_transcode_like_production 改用 probe_video_info
  + 无参 build_transcode_vf,与生产完全一致。
- 任务级竖屏用例滤镜特征断言更新为 min(1920)。
This commit is contained in:
xiaoxia
2026-08-30 21:09:43 +08:00
parent 0eba41332c
commit e68801eb59
3 changed files with 167 additions and 39 deletions
+95 -21
View File
@@ -30,6 +30,7 @@ from worker_app.tasks.ingest import ( # noqa: E402
is_portrait_video,
probe_dimensions,
probe_rotation,
probe_video_info,
validate_transcode_output,
)
@@ -90,33 +91,65 @@ class TestIsPortraitVideo:
class TestBuildTranscodeVF:
"""统一转码滤镜:长边封顶 1920、只缩不放、方向无关。"""
def test_comma_escaped_with_backslash(self):
r"""scale 表达式内的逗号必须 \, 转义(否则报 Invalid size / No such filter)。"""
for vf in (build_transcode_vf(True), build_transcode_vf(False)):
assert "\\," in vf
# 不应存在未转义的裸逗号(filter 分隔)出现在 if 表达式内
assert "gt(ih,1080)" not in vf
assert "gt(iw,1080)" not in vf
vf = build_transcode_vf()
assert "\\," in vf
# 不应存在未转义的裸逗号(filter 分隔)出现在 if 表达式内
assert "gte(iw,ih)" not in vf
assert "gt(ih,iw)" not in vf
assert "min(1920,iw)" not in vf
def test_no_transpose_filter(self):
"""不能显式 transpose:ffmpeg autorotate 已按 side data 物理旋转,
再加 transpose 会双重旋转把竖屏转成横屏。"""
assert "transpose" not in build_transcode_vf(True)
assert "transpose" not in build_transcode_vf(False)
assert "transpose" not in build_transcode_vf()
def test_portrait_scales_by_width(self):
"""竖屏(旋转后 w<h):宽超 1080 缩到 1080,高自适应 → 4K 竖屏 1080x1920。"""
vf = build_transcode_vf(True)
assert vf.startswith("scale=if(gt(iw\\,1080)\\,1080\\,iw):-2")
def test_landscape_scales_by_height(self):
"""横屏:高超 1080 缩到 1080,宽自适应 → 4K 横屏 1920x1080。"""
vf = build_transcode_vf(False)
assert vf.startswith("scale=-2:if(gt(ih\\,1080)\\,1080\\,ih)")
def test_long_edge_capped_1920_orientation_agnostic(self):
"""横屏限宽、竖屏限高,均 min(1920,...),短边 -2 自适应。"""
vf = build_transcode_vf()
assert "gte(iw" in vf and "gt(ih" in vf, "横/竖分支都要在"
assert vf.count("min(1920") == 2, "宽高分支都按长边 1920 封顶"
def test_format_yuv420p_suffix(self):
for vf in (build_transcode_vf(True), build_transcode_vf(False)):
assert vf.endswith(",format=yuv420p")
assert build_transcode_vf().endswith(",format=yuv420p")
class TestProbeVideoInfo:
"""probe_video_info 合并探测:维度 + rotation 一次 ffprobe。"""
def test_merges_dimensions_and_rotation(self, tmp_path):
"""真实 ffprobe:r90 素材 → (1920, 1080, 90);r270 → rotation=-90;横屏 None。"""
# 端到端素材由 hevc_sources fixture 构造,这里用独立小素材验证合并函数
base = tmp_path / "v.mp4"
subprocess.run(
[
FFMPEG,
"-y",
"-hide_banner",
"-loglevel",
"error",
"-f",
"lavfi",
"-i",
"color=c=white:s=320x240:d=1:r=15",
"-c:v",
"libx264",
"-pix_fmt",
"yuv420p",
str(base),
],
check=True,
)
w, h, rot = probe_video_info(str(base))
assert (w, h) == (320, 240)
assert rot is None
def test_probe_failure_returns_triple_none(self):
w, h, rot = probe_video_info("/nonexistent/path/fake.mp4")
assert (w, h, rot) == (None, None, None)
class TestValidateTranscodeOutput:
@@ -319,10 +352,37 @@ def hevc_sources(tmp_path):
check=True,
)
# 超宽屏 HEVC(4000x1000,无 rotation):旧滤镜短边 1000<1080 不缩放,
# 长边 4000 超 validate 的 1920 上限被误降级;新滤镜长边封顶应缩到 1920x480。
ultra_wide = tmp_path / "ultra_wide_hevc.mp4"
subprocess.run(
[
FFMPEG,
"-y",
"-hide_banner",
"-loglevel",
"error",
"-f",
"lavfi",
"-i",
"color=c=red:s=4000x1000:d=1:r=30",
"-c:v",
"libx265",
"-tag:v",
"hvc1",
"-pix_fmt",
"yuv420p",
"-an",
str(ultra_wide),
],
check=True,
)
yield {
"portrait_r90": r90,
"portrait_r270": r270,
"portrait_physical": physical,
"ultra_wide": ultra_wide,
"landscape": base_hevc,
"tmp_path": tmp_path,
}
@@ -330,10 +390,10 @@ def hevc_sources(tmp_path):
def _transcode_like_production(src: Path, dst: Path) -> tuple[bool, int | None]:
"""按生产代码相同方式执行转码,返回 (is_portrait, rotation)。"""
rotation = probe_rotation(str(src))
width, height = probe_dimensions(str(src))
# 与生产一致:合并探测 + 统一滤镜
width, height, rotation = probe_video_info(str(src))
is_portrait = is_portrait_video(width, height, rotation)
vf = build_transcode_vf(is_portrait)
vf = build_transcode_vf()
subprocess.run(
[
FFMPEG,
@@ -445,6 +505,20 @@ class TestTranscodeEndToEnd:
assert probe_rotation(str(dst)) is None
assert validate_transcode_output(str(dst), False) is True
def test_ultra_wide_long_edge_capped(self, hevc_sources):
"""超宽屏 4000x1000:长边必须封顶 1920(→1920x480),validate 通过。
回归旧滤镜只按短边触发缩放、长边超 1920 被误降级的 bug。"""
src = hevc_sources["ultra_wide"]
dst = hevc_sources["tmp_path"] / "out_ultra.mp4"
is_portrait, rotation = _transcode_like_production(src, dst)
assert rotation is None
assert is_portrait is False
width, height = probe_dimensions(str(dst))
assert max(width, height) <= 1920, f"长边应封顶 1920,实际 {width}x{height}"
assert width == 1920 and height == 480
assert validate_transcode_output(str(dst), False) is True
def test_thumbnail_portrait_direction(self, hevc_sources):
"""缩略图(thumbnail_generator 同款 ffmpeg 抽帧,依赖 autorotate)竖屏源→竖版图。"""
src = hevc_sources["portrait_r90"]