fix(P0): 放宽ingest视频编码拦截 + 自动选素材去掉编码过滤 #508
@@ -66,9 +66,6 @@ def auto_select_video_assets(
|
||||
Returns:
|
||||
选中的素材 ID 列表,无可用素材时返回空列表
|
||||
"""
|
||||
# 明确不支持的视频编码(会导致渲染失败)
|
||||
UNSUPPORTED_CODECS = {"hevc", "h265", "hev1", "hvc1", "vp9", "vp09", "av1", "av01"}
|
||||
|
||||
if not project_id:
|
||||
return []
|
||||
|
||||
@@ -96,27 +93,12 @@ def auto_select_video_assets(
|
||||
and a.mime_type.startswith("video")
|
||||
]
|
||||
|
||||
# 过滤不支持的编码格式(HEVC/VP9/AV1 等会导致渲染失败)
|
||||
# 优先读 asset.codec 字段,其次从 metadata 里取(兼容存量数据)
|
||||
codec_filtered = []
|
||||
skipped_codec = 0
|
||||
for a in ready_videos:
|
||||
codec = (a.codec or "").lower()
|
||||
if not codec and a.metadata and isinstance(a.metadata, dict):
|
||||
codec = str(a.metadata.get("codec", "")).lower()
|
||||
if codec and codec in UNSUPPORTED_CODECS:
|
||||
skipped_codec += 1
|
||||
continue
|
||||
codec_filtered.append(a)
|
||||
|
||||
if skipped_codec and logger:
|
||||
logger.warning("自动选素材: 跳过 %d 个不支持编码的素材", skipped_codec)
|
||||
|
||||
# 过滤横屏素材(只保留竖屏/正方形)
|
||||
# 移动端短视频场景默认竖屏,横屏素材裁剪后画面不可用
|
||||
# 注意:这只是选素材优化,渲染引擎本身支持任何分辨率的素材
|
||||
filtered_videos = []
|
||||
skipped_landscape = 0
|
||||
for a in codec_filtered:
|
||||
for a in ready_videos:
|
||||
width = a.width if hasattr(a, "width") and a.width else 0
|
||||
height = a.height if hasattr(a, "height") and a.height else 0
|
||||
if not width or not height:
|
||||
|
||||
@@ -23,12 +23,12 @@ MIN_VIDEO_FILE_SIZE = 1024 # 1KB
|
||||
MIN_AUDIO_FILE_SIZE = 100 # 100B
|
||||
MIN_IMAGE_FILE_SIZE = 100 # 100B
|
||||
|
||||
# 支持的视频编码格式(白名单)
|
||||
# 渲染引擎针对 H.264 优化,其他编码可能导致渲染失败或异常
|
||||
SUPPORTED_VIDEO_CODECS = {"h264", "avc1", "avc"}
|
||||
|
||||
# 明确不支持的视频编码(用于日志提示)
|
||||
UNSUPPORTED_VIDEO_CODECS = {
|
||||
# 支持的视频编码格式(白名单,尽可能放宽)
|
||||
# 渲染引擎会在 concat 前统一转码为 h264,因此只要 ffprobe 能识别的视频编码都允许 ingested
|
||||
SUPPORTED_VIDEO_CODECS = {
|
||||
"h264",
|
||||
"avc1",
|
||||
"avc", # H.264 / AVC
|
||||
"hevc",
|
||||
"h265",
|
||||
"hev1",
|
||||
@@ -41,6 +41,26 @@ UNSUPPORTED_VIDEO_CODECS = {
|
||||
"vp08", # VP8
|
||||
"mpeg4",
|
||||
"mp4v", # MPEG-4
|
||||
"mpeg2video",
|
||||
"mpg2", # MPEG-2
|
||||
"wmv2",
|
||||
"wmv1",
|
||||
"vc1", # WMV / VC-1
|
||||
"flv1",
|
||||
"flv",
|
||||
"vp6f", # Flash / FLV
|
||||
"theora",
|
||||
"ogg", # Theora
|
||||
"prores",
|
||||
"prores_ks",
|
||||
"apcn",
|
||||
"apch",
|
||||
"apco",
|
||||
"apcs",
|
||||
"ap4h",
|
||||
"ap4x", # Apple ProRes
|
||||
"dnxhd",
|
||||
"dnxhr", # DNxHD / DNxHR
|
||||
}
|
||||
|
||||
|
||||
@@ -203,10 +223,11 @@ def _is_valid_media(metadata: dict, media_type: str) -> bool:
|
||||
duration = float(metadata.get("duration", 0))
|
||||
if size < MIN_VIDEO_FILE_SIZE or duration <= 0:
|
||||
return False
|
||||
# 编码格式校验:仅支持 H.264,其他编码(HEVC/VP9/AV1等)渲染可能失败
|
||||
# 编码格式校验:只排除明确非视频的编码格式,只要 ffprobe 能识别的视频编码都允许
|
||||
# 渲染引擎会在 concat 前统一转码为 h264 yuv420p,ingest 层不再做严格的编码拦截
|
||||
codec = str(metadata.get("codec", "")).lower()
|
||||
if codec and codec not in SUPPORTED_VIDEO_CODECS:
|
||||
return False
|
||||
logger.info("检测到非白名单视频编码 %s,仍允许 ingested,渲染层会统一转码", codec)
|
||||
return True
|
||||
if media_type == "audio":
|
||||
duration = float(metadata.get("duration", 0))
|
||||
|
||||
@@ -64,7 +64,7 @@ def _make_mock_lib_repo():
|
||||
|
||||
|
||||
class TestAutoSelectVideoAssetsCodecFilter:
|
||||
"""自动选素材 — 编码格式过滤."""
|
||||
"""自动选素材 — 编码格式不再过滤,渲染引擎统一转码."""
|
||||
|
||||
def test_h264_assets_selected(self):
|
||||
"""H.264 编码素材 → 正常选中."""
|
||||
@@ -87,8 +87,8 @@ class TestAutoSelectVideoAssetsCodecFilter:
|
||||
assert "a1" in result
|
||||
assert "a2" in result
|
||||
|
||||
def test_hevc_assets_filtered_by_codec_field(self):
|
||||
"""HEVC 编码素材(codec 字段有值) → 被过滤掉."""
|
||||
def test_hevc_assets_not_filtered_by_codec_field(self):
|
||||
"""HEVC 编码素材 → 不再被过滤,渲染引擎会统一转码."""
|
||||
from app.api.routes._helpers import auto_select_video_assets
|
||||
|
||||
assets = [
|
||||
@@ -105,11 +105,14 @@ class TestAutoSelectVideoAssetsCodecFilter:
|
||||
asset_repo=asset_repo,
|
||||
)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0] == "a1"
|
||||
# 所有编码的素材都保留,不再按编码过滤
|
||||
assert len(result) == 3
|
||||
assert "a1" in result
|
||||
assert "a2" in result
|
||||
assert "a3" in result
|
||||
|
||||
def test_hevc_assets_filtered_by_metadata(self):
|
||||
"""HEVC 编码素材(codec 字段为空,但 metadata 里有) → 被过滤掉(兼容存量数据)."""
|
||||
def test_hevc_assets_not_filtered_by_metadata(self):
|
||||
"""HEVC 编码素材(metadata 里有) → 不再被过滤."""
|
||||
from app.api.routes._helpers import auto_select_video_assets
|
||||
|
||||
assets = [
|
||||
@@ -126,14 +129,14 @@ class TestAutoSelectVideoAssetsCodecFilter:
|
||||
asset_repo=asset_repo,
|
||||
)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0] == "a1"
|
||||
# 所有编码的素材都保留
|
||||
assert len(result) == 3
|
||||
|
||||
def test_codec_field_takes_precedence_over_metadata(self):
|
||||
"""codec 字段优先于 metadata 里的 codec."""
|
||||
from app.api.routes._helpers import auto_select_video_assets
|
||||
|
||||
# codec 字段是 h264,但 metadata 里是 hevc → 以 codec 字段为准(应该被选中)
|
||||
# codec 字段是 h264,但 metadata 里是 hevc → 不影响,都能选中
|
||||
assets = [
|
||||
_make_asset("a1", codec="h264", metadata={"codec": "hevc"}),
|
||||
]
|
||||
@@ -149,8 +152,8 @@ class TestAutoSelectVideoAssetsCodecFilter:
|
||||
assert len(result) == 1
|
||||
assert result[0] == "a1"
|
||||
|
||||
def test_vp9_av1_assets_filtered(self):
|
||||
"""VP9 / AV1 编码素材 → 被过滤掉."""
|
||||
def test_vp9_av1_assets_not_filtered(self):
|
||||
"""VP9 / AV1 编码素材 → 不再被过滤,渲染引擎会统一转码."""
|
||||
from app.api.routes._helpers import auto_select_video_assets
|
||||
|
||||
assets = [
|
||||
@@ -167,8 +170,11 @@ class TestAutoSelectVideoAssetsCodecFilter:
|
||||
asset_repo=asset_repo,
|
||||
)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0] == "a3"
|
||||
# 所有编码的素材都保留
|
||||
assert len(result) == 3
|
||||
assert "a1" in result
|
||||
assert "a2" in result
|
||||
assert "a3" in result
|
||||
|
||||
def test_missing_codec_kept(self):
|
||||
"""codec 字段和 metadata 里都没有 → 暂时放过(兼容存量数据)."""
|
||||
@@ -190,7 +196,7 @@ class TestAutoSelectVideoAssetsCodecFilter:
|
||||
assert len(result) == 2
|
||||
|
||||
def test_codec_case_insensitive(self):
|
||||
"""编码格式过滤不区分大小写."""
|
||||
"""不同编码格式的素材都保留,不再按编码过滤."""
|
||||
from app.api.routes._helpers import auto_select_video_assets
|
||||
|
||||
assets = [
|
||||
@@ -206,8 +212,10 @@ class TestAutoSelectVideoAssetsCodecFilter:
|
||||
asset_repo=asset_repo,
|
||||
)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0] == "a2"
|
||||
# 所有编码的素材都保留
|
||||
assert len(result) == 2
|
||||
assert "a1" in result
|
||||
assert "a2" in result
|
||||
|
||||
def test_only_ready_assets_selected(self):
|
||||
"""只有 READY 状态的素材才会被选中."""
|
||||
@@ -406,15 +414,15 @@ class TestAutoSelectVideoAssetsCodecFilter:
|
||||
assert "landscape_in_meta" not in result
|
||||
assert "portrait_in_meta" in result
|
||||
|
||||
def test_codec_and_landscape_combined_filter(self):
|
||||
"""编码过滤 + 横屏过滤同时生效."""
|
||||
def test_only_landscape_filtered_codec_not_filtered(self):
|
||||
"""只有横屏过滤生效,编码不再过滤."""
|
||||
from app.api.routes._helpers import auto_select_video_assets
|
||||
|
||||
assets = [
|
||||
_make_asset("good", codec="h264", width=1080, height=1920),
|
||||
_make_asset("bad_codec", codec="hevc", width=1080, height=1920),
|
||||
_make_asset("hevc_portrait", codec="hevc", width=1080, height=1920),
|
||||
_make_asset("bad_landscape", codec="h264", width=1920, height=1080),
|
||||
_make_asset("bad_both", codec="hevc", width=1920, height=1080),
|
||||
_make_asset("hevc_landscape", codec="hevc", width=1920, height=1080),
|
||||
]
|
||||
asset_repo = _make_mock_repo(assets)
|
||||
lib_repo = _make_mock_lib_repo()
|
||||
@@ -425,4 +433,9 @@ class TestAutoSelectVideoAssetsCodecFilter:
|
||||
asset_repo=asset_repo,
|
||||
)
|
||||
|
||||
assert result == ["good"]
|
||||
# 只有横屏被过滤,编码不再过滤
|
||||
assert len(result) == 2
|
||||
assert "good" in result
|
||||
assert "hevc_portrait" in result
|
||||
assert "bad_landscape" not in result
|
||||
assert "hevc_landscape" not in result
|
||||
|
||||
@@ -99,45 +99,55 @@ class TestIsValidMedia:
|
||||
result = _is_valid_media({"size_bytes": 1000}, "text")
|
||||
assert result is False
|
||||
|
||||
def test_video_hevc_codec_invalid(self):
|
||||
"""HEVC/H.265 编码视频 → 无效(渲染不支持)."""
|
||||
def test_video_hevc_codec_valid(self):
|
||||
"""HEVC/H.265 编码视频 → 有效(ingest层不拦截,渲染层统一转码)."""
|
||||
from worker_app.tasks.ingest import _is_valid_media
|
||||
|
||||
result = _is_valid_media(
|
||||
{"size_bytes": 10 * 1024 * 1024, "duration": 30.0, "codec": "hevc"},
|
||||
"video",
|
||||
)
|
||||
assert result is False
|
||||
assert result is True
|
||||
|
||||
def test_video_h265_codec_invalid(self):
|
||||
"""h265 编码 → 无效."""
|
||||
def test_video_h265_codec_valid(self):
|
||||
"""h265 编码 → 有效."""
|
||||
from worker_app.tasks.ingest import _is_valid_media
|
||||
|
||||
result = _is_valid_media(
|
||||
{"size_bytes": 10 * 1024 * 1024, "duration": 30.0, "codec": "h265"},
|
||||
"video",
|
||||
)
|
||||
assert result is False
|
||||
assert result is True
|
||||
|
||||
def test_video_vp9_codec_invalid(self):
|
||||
"""VP9 编码 → 无效."""
|
||||
def test_video_vp9_codec_valid(self):
|
||||
"""VP9 编码 → 有效."""
|
||||
from worker_app.tasks.ingest import _is_valid_media
|
||||
|
||||
result = _is_valid_media(
|
||||
{"size_bytes": 10 * 1024 * 1024, "duration": 30.0, "codec": "vp9"},
|
||||
"video",
|
||||
)
|
||||
assert result is False
|
||||
assert result is True
|
||||
|
||||
def test_video_av1_codec_invalid(self):
|
||||
"""AV1 编码 → 无效."""
|
||||
def test_video_av1_codec_valid(self):
|
||||
"""AV1 编码 → 有效."""
|
||||
from worker_app.tasks.ingest import _is_valid_media
|
||||
|
||||
result = _is_valid_media(
|
||||
{"size_bytes": 10 * 1024 * 1024, "duration": 30.0, "codec": "av1"},
|
||||
"video",
|
||||
)
|
||||
assert result is False
|
||||
assert result is True
|
||||
|
||||
def test_video_unknown_codec_valid(self):
|
||||
"""非白名单编码 → 仍有效(只打日志不拦截,渲染层负责统一转码)."""
|
||||
from worker_app.tasks.ingest import _is_valid_media
|
||||
|
||||
result = _is_valid_media(
|
||||
{"size_bytes": 10 * 1024 * 1024, "duration": 30.0, "codec": "mpeg4"},
|
||||
"video",
|
||||
)
|
||||
assert result is True
|
||||
|
||||
def test_video_h264_codec_valid(self):
|
||||
"""H.264 编码 → 有效."""
|
||||
@@ -170,14 +180,14 @@ class TestIsValidMedia:
|
||||
assert result is True
|
||||
|
||||
def test_video_codec_case_insensitive(self):
|
||||
"""编码格式校验不区分大小写."""
|
||||
"""编码格式校验不区分大小写,HEVC大写也识别为有效."""
|
||||
from worker_app.tasks.ingest import _is_valid_media
|
||||
|
||||
result = _is_valid_media(
|
||||
{"size_bytes": 10 * 1024 * 1024, "duration": 30.0, "codec": "HEVC"},
|
||||
"video",
|
||||
)
|
||||
assert result is False
|
||||
assert result is True
|
||||
|
||||
|
||||
# ── extract_media_metadata 返回值签名测试 ────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user