fix: legacy渲染引擎concat前音频不归一化exit=234 + 视频改为pad留黑边 #503

Merged
auto-approve-bot merged 1 commits from fix/legacy-audio-normalize-and-pad into develop 2026-07-18 13:52:11 +08:00
2 changed files with 54 additions and 14 deletions
+14 -8
View File
@@ -416,14 +416,14 @@ class VideoComposeService:
filters: list[str] = []
# 1. scale: 等比缩放,保证覆盖目标区域(scale to larger, then crop
filters.append(f"scale={output_width}:{output_height}" f":force_original_aspect_ratio=increase")
# 1. scale: 等比缩放(保持比例,不裁剪
filters.append(f"scale={output_width}:{output_height}" f":force_original_aspect_ratio=decrease")
# 2. crop: 居中裁剪
filters.append(f"crop={output_width}:{output_height}")
# 2. pad: 居中+留黑边到目标分辨率(保持原始比例,不裁剪内容)
filters.append(f"pad={output_width}:{output_height}:(ow-iw)/2:(oh-ih)/2:black")
# 2.5 fps: 统一帧率(concat 要求所有输入帧率一致)
# 放在 crop 之后、setpts 之前,确保分辨率和帧率都已统一
# 3. fps: 统一帧率(concat 要求所有输入帧率一致)
# 放在 pad 之后、setpts 之前,确保分辨率和帧率都已统一
if fps and fps > 0:
filters.append(f"fps={fps}")
@@ -538,11 +538,17 @@ def _build_concat_filter(
concat_filter = f"{concat_inputs}concat=n={n}:v=1:a=0[outv]"
parts.append(concat_filter)
# 音频 concat(如果有)
# 音频 concat(如果有)— 先统一音频格式再拼接,否则不同采样率/声道会导致concat失败
audio_parts: list[str] = []
for idx, chain in enumerate(clip_chains):
if chain.audio_label:
audio_parts.append(f"[{idx}:a]atrim=0:{chain.duration},asetpts=PTS-STARTPTS[{chain.audio_label}]")
# aformat: 统一采样率48000Hz + 双声道stereo + fltp采样格式(AAC标准格式)
audio_filters = [
"aformat=sample_rates=48000:channel_layouts=stereo:sample_fmts=fltp",
f"atrim=0:{chain.duration}",
"asetpts=PTS-STARTPTS",
]
audio_parts.append(f"[{idx}:a]{','.join(audio_filters)}[{chain.audio_label}]")
if audio_parts:
parts.extend(audio_parts)
+40 -6
View File
@@ -344,8 +344,8 @@ class TestBuildComposeCommand(TestCase):
crf_idx = cmd.command.index("-crf")
self.assertEqual(cmd.command[crf_idx + 1], "28")
def test_filter_chain_contains_scale_and_crop(self):
"""滤镜链包含 scale 和 crop"""
def test_filter_chain_contains_scale_and_pad(self):
"""滤镜链包含 scale 和 pad(保持比例留黑边,不裁剪)"""
plan = _StubPlan()
clips = [_make_ready_clip(plan_id=plan.id)]
svc = _make_service(plan, clips)
@@ -354,7 +354,9 @@ class TestBuildComposeCommand(TestCase):
chain = cmd.clip_chains[0]
filter_text = ",".join(chain.filters)
self.assertIn("scale=", filter_text)
self.assertIn("crop=", filter_text)
self.assertIn("force_original_aspect_ratio=decrease", filter_text)
self.assertIn("pad=", filter_text)
self.assertIn("black", filter_text)
self.assertIn("trim=", filter_text)
def test_filter_chain_contains_fps(self):
@@ -366,11 +368,11 @@ class TestBuildComposeCommand(TestCase):
chain = cmd.clip_chains[0]
filter_text = ",".join(chain.filters)
# fps 必须在 crop 之后、setpts 之前
crop_idx = filter_text.index("crop=")
# fps 必须在 pad 之后、setpts 之前
pad_idx = filter_text.index("pad=")
fps_idx = filter_text.index("fps=25")
setpts_idx = filter_text.index("setpts=")
self.assertGreater(fps_idx, crop_idx, "fps 应该在 crop 之后")
self.assertGreater(fps_idx, pad_idx, "fps 应该在 pad 之后")
self.assertLess(fps_idx, setpts_idx, "fps 应该在 setpts 之前")
def test_fps_zero_or_none_skips_fps_filter(self):
@@ -533,6 +535,38 @@ class TestBuildConcatFilter(TestCase):
self.assertIn("concat=n=2:v=1:a=0[outv]", filter_str)
self.assertEqual(duration, 13.0)
def test_multi_clip_with_audio_aformat(self):
"""多片段+音频时,每个音频流都经过 aformat 归一化再concat。"""
from app.services.video_compose_service import ClipFilterChain
chains = [
ClipFilterChain(
clip_id="c1",
input_index=0,
video_label="v0",
audio_label="a0",
filters=["scale=1280:720"],
duration=5.0,
),
ClipFilterChain(
clip_id="c2",
input_index=1,
video_label="v1",
audio_label="a1",
filters=["scale=1280:720"],
duration=8.0,
),
]
filter_str, _ = _build_concat_filter(chains)
# 两个音频流都必须有 aformat 归一化(48000Hz + stereo + fltp
self.assertIn("aformat=sample_rates=48000:channel_layouts=stereo:sample_fmts=fltp", filter_str)
# 音频 concat
self.assertIn("concat=n=2:v=0:a=1[outa]", filter_str)
# aformat 在 atrim 之前(先统一格式再裁剪)
aformat_idx = filter_str.index("aformat=")
atrim_idx = filter_str.index("atrim=")
self.assertLess(aformat_idx, atrim_idx, "aformat 应该在 atrim 之前")
class TestBuildXfadeFilter(TestCase):
"""_build_xfade_filter 测试。"""