fix(ingest): 修复竖屏 HEVC 转码方向错误(双重旋转/ffprobe 取错行/逗号转义) #1559
Reference in New Issue
Block a user
Delete Branch "fix/ingest-hevc-portrait-rotation"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
问题
竖屏 iPhone HEVC 素材入库转码后方向错乱:竖屏(rotation=90/270)转码产物变成 1920×1080 横屏,导致素材库展示方向错误。
根因(ffmpeg 4.4.2 实测复现)
transpose=1,r90 竖屏被转成 1920×1080 横屏。side_data=rotation和stream_tags=rotate两行(r90 文件输出270\n90),split("\n")[0]取到 270,方向判定反了。scale=if(gt(ih,1080),...)中裸逗号被当作 filter 分隔,报Invalid size,按要求统一\,转义。修复
probe_rotation只读 side_data 单行并 int 化,异常返回 None。scale=if(gt(iw\,1080)\,1080\,iw):-2,横屏scale=-2:if(gt(ih\,1080)\,1080\,ih)。-metadata:s:v:0 rotate=0(实测清不掉 display matrix)。validate_transcode_output:竖屏须 h>w、横屏须 w>=h、长边 ≤1920、无残留 rotation side data;校验失败打 error 日志并降级使用原始文件、不上传 OSS,杜绝横屏文件覆盖。测试
test_ingest_hevc_transcode.py(重写,22 用例):纯逻辑断言 + 真实 ffmpeg 端到端(造带 display matrix 的 r90/r270/横屏/4K HEVC 素材,断言 1080×1920 / 1920×1080 / h264 / 无 side data / 缩略图竖版方向正确)。test_ingest_hevc_transcode_task.py(新增,4 用例):任务级 mock 验证成功改写 storage_key、校验失败降级+error 日志、ffmpeg 非零降级、非 HEVC 不转码。存量数据
staging 上已转错的竖屏素材(IMG_2281_h264.MOV、IMG_2278_h264.MOV 等)需重新触发转码或删除重传,部署后另行处理。
竖屏 iPhone HEVC 素材转码后方向错乱(竖屏变横屏),实测定位三个根因: 1. 双重旋转:ffmpeg 默认 autorotate 已按 display matrix 物理旋转画面, 代码又显式追加 transpose=1,导致 r90 竖屏被转成 1920x1080 横屏。 旋转全部交给 autorotate,滤镜只做 scale。 2. ffprobe 取错行:探测命令同时输出 side_data=rotation 和 stream_tags=rotate 两行(r90 文件输出 270/90 两行),split("\n")[0] 取到 270,方向判定反了。改为只读 side_data 单行并 int 化。 3. scale 表达式逗号未转义:if(gt(ih,1080),...) 中的裸逗号被当作 filter 分隔,报 Invalid size。统一用 \, 转义(raw string 字面量)。 附带修复: - 竖屏按宽、横屏按高缩放(旋转后竖屏 w<h,旧代码竖屏沿用按高缩放 会把 1080x1920 错缩为 608x1080),长边封顶 1080 只缩不放。 - 移除无效的 -metadata:s:v:0 rotate=0(实测清不掉 display matrix side data,autorotate 方案产物本身已无 side data)。 - 转码后强制方向/维度校验 validate_transcode_output:竖屏产物必须 h>w、横屏 w>=h、长边 <=1920、无残留 rotation side data;校验失败 打 error 日志并降级使用原始文件、不上传 OSS,杜绝横屏文件覆盖。 测试: - test_ingest_hevc_transcode.py 重写(22 用例):纯逻辑断言 + 真实 ffmpeg 端到端(r90/r270/横屏/4K 素材造 display matrix, 断言 1080x1920 / 1920x1080 / h264 / 无 side data / 缩略图竖版)。 - test_ingest_hevc_transcode_task.py 新增(4 用例):任务级 mock 验证成功改写 storage_key、校验失败降级+error 日志、ffmpeg 非零 降级、非 HEVC 不转码。🚀 预览环境已部署
CI全绿,自动审批通过。
CI全绿,自动审批通过。
回复 AI Code Review 最新一轮意见:
阻塞级(临时文件 delete=False)—— 误判,代码本就正确
ingest.py中转码临时文件的创建为(develop 基线 #1449 即如此):delete=False已显式传入,close 不会删除文件;且转码 try 块末尾有finally清理(成功/失败/异常都会unlink)。端到端测试(真实 ffmpeg 写入该路径)与任务级测试均通过,可佐证转码不会 100% 失败。建议 1(stderr 字节流)—— 误判
转码
subprocess.run调用显式传了text=True(stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True, timeout=...),_proc.stderr已是 str,_proc.stderr[-300:]切片无需 decode。建议 2(魔法数字)—— 已采纳
最新提交已将 1080 / 1920 / 900 提取为
TRANSCODE_TARGET_EDGE/TRANSCODE_MAX_LONG_EDGE/TRANSCODE_TIMEOUT_SECONDS常量,vf 表达式改 rf-string 引用,渲染结果不变。上一轮的阻塞 1(物理竖屏 rotation=0 误判横屏)已在
b03d19cf修复:新增is_portrait_video(stored_w, stored_h, rotation)按显示方向判定,补了纯逻辑单测、真实 ffmpeg 物理竖屏端到端、任务级用例。【阻塞级判定】
📊 审查概览
🔴 阻塞级问题(必须修复)
[apps/worker/worker_app/tasks/ingest.py: 467-511] 临时文件资源泄露
tempfile.NamedTemporaryFile(delete=False)创建临时文件_tc_tmp,并在关闭文件句柄后交由ffmpeg写入。然而,在随后的逻辑中(无论validate_transcode_output校验成功还是失败),均未显式调用os.remove()或_tc_tmp.unlink()删除该临时文件。这将导致每次 HEVC 转码任务都在 Worker 节点磁盘上残留一个临时视频文件,长期运行会导致磁盘空间耗尽,引发服务不可用。try...finally块中或校验/上传逻辑结束后,务必添加清理代码删除_tc_tmp。例如:[apps/worker/worker_app/tasks/ingest.py: 156-157] 转码缩放逻辑与校验规则不一致
TRANSCODE_MAX_LONG_EDGE = 1920并在validate_transcode_output中强制校验产物长边不得超过 1920。然而,构建的滤镜_LANDSCAPE_VF和_PORTRAIT_VF仅对短边(高或宽)进行了 "if > 1080 then 1080" 的限制,未限制长边。例如:对于 4000x1000 的横屏视频,
ih=1000不大于 1080,滤镜不会缩放,输出仍为 4000x1000。此时validate_transcode_output检测到长边 4000 > 1920 判定失败,导致转码被丢弃,最终降级使用原始 HEVC 文件。这会导致浏览器无法播放(黑屏),违背了自动转码的初衷。scale='min(1920,iw):-2'等逻辑,或者使用force_original_aspect_ratio=decrease结合尺寸限制来确保产物符合校验规则。💡 改进建议(不阻塞合并)
probe_rotation和probe_dimensions分别调用了一次subprocess.run执行ffprobe。对于大文件或高并发场景,这会增加 I/O 开销。建议将两个探测合并为一次ffprobe调用,同时获取stream=width,height和side_data=rotation,解析一次输出即可。✅ 良好实践
subprocess.run列表传参,有效避免了命令注入风险。subprocess.TimeoutExpired、ValueError等进行了捕获并降级处理。ffmpeg滤镜中逗号转义等细节,便于后续维护。✅ 格式检查通过 | ❌ 逻辑审查需修改 | ⚠️ 建议关注性能
🤖 由 AI 代码审查机器人自动生成 | 2026-08-30 12:57:48 | 模型:
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)。🗑️ 预览环境已清理
PR #1559 已关闭或合并,对应的预览环境已被清理。