diff --git a/apps/api/app/api/routes/_helpers.py b/apps/api/app/api/routes/_helpers.py index 651ecb348..d6db4fe37 100755 --- a/apps/api/app/api/routes/_helpers.py +++ b/apps/api/app/api/routes/_helpers.py @@ -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: diff --git a/apps/worker/worker_app/tasks/ingest.py b/apps/worker/worker_app/tasks/ingest.py index 9a94df088..a3e001e01 100755 --- a/apps/worker/worker_app/tasks/ingest.py +++ b/apps/worker/worker_app/tasks/ingest.py @@ -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)) diff --git a/tests/unit/test_auto_select_assets.py b/tests/unit/test_auto_select_assets.py index c068be609..ed118e3eb 100755 --- a/tests/unit/test_auto_select_assets.py +++ b/tests/unit/test_auto_select_assets.py @@ -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 diff --git a/tests/unit/test_ingest_validation.py b/tests/unit/test_ingest_validation.py index 205b314e1..c16780248 100755 --- a/tests/unit/test_ingest_validation.py +++ b/tests/unit/test_ingest_validation.py @@ -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 返回值签名测试 ────────────────────────────────────