feat(video): 添加视频缩放特效功能支持

- 在 EffectConfig 中新增 zoom 特效类型及参数解析
- 实现 get_zoom_params 方法用于获取缩放效果参数
- 更新文档注释说明 zoom 特效使用格式示例
- 修改渲染逻辑支持 zoom 特效的 filter_complex 处理
- 添加缩放特效的视频滤镜构建实现
- 统一处理 cameraShot 和 zoom 特效的效果叠加逻辑
This commit is contained in:
2026-02-07 01:25:20 +08:00
parent 88aa3adca1
commit ad4a9cc869
2 changed files with 76 additions and 12 deletions

View File

@@ -5,12 +5,13 @@
定义任务类型、任务实体、渲染规格、输出规格等数据结构。
"""
from enum import Enum
from dataclasses import dataclass, field
from typing import Dict, Any, Optional, List
from datetime import datetime
from urllib.parse import urlparse, unquote
import os
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from math import isfinite
from typing import Dict, Any, Optional, List
from urllib.parse import urlparse, unquote
# 支持的图片扩展名
@@ -95,7 +96,9 @@ class Effect:
特效配置
格式:type:params
例如:cameraShot:3,1 表示在第3秒定格1秒
例如:
- cameraShot:3,1 表示在第3秒定格1秒
- zoom:1.5,1.2,2 表示从第1.5秒开始放大 1.2 倍并持续 2 秒
"""
effect_type: str # 效果类型
params: str = "" # 参数字符串
@@ -122,7 +125,7 @@ class Effect:
解析效果字符串
格式:effect1|effect2|effect3
例如:cameraShot:3,1|blur:5
例如:cameraShot:3,1|zoom:1.5,1.2,2
"""
if not effects_str:
return []
@@ -152,6 +155,40 @@ class Effect:
except ValueError:
return (3, 1)
def get_zoom_params(self) -> tuple:
"""
获取 zoom 效果参数
Returns:
(start_sec, scale_factor, duration_sec): 起始时间、放大倍数、持续时长(秒)
"""
if self.effect_type != 'zoom':
return (0.0, 1.2, 1.0)
default_start_sec = 0.0
default_scale_factor = 1.2
default_duration_sec = 1.0
if not self.params:
return (default_start_sec, default_scale_factor, default_duration_sec)
parts = [part.strip() for part in self.params.split(',')]
try:
start_sec = float(parts[0]) if len(parts) >= 1 and parts[0] else default_start_sec
scale_factor = float(parts[1]) if len(parts) >= 2 and parts[1] else default_scale_factor
duration_sec = float(parts[2]) if len(parts) >= 3 and parts[2] else default_duration_sec
except ValueError:
return (default_start_sec, default_scale_factor, default_duration_sec)
if not isfinite(start_sec) or start_sec < 0:
start_sec = default_start_sec
if not isfinite(scale_factor) or scale_factor <= 1.0:
scale_factor = default_scale_factor
if not isfinite(duration_sec) or duration_sec <= 0:
duration_sec = default_duration_sec
return (start_sec, scale_factor, duration_sec)
@dataclass
class RenderSpec:

View File

@@ -491,7 +491,10 @@ class RenderSegmentVideoHandler(BaseHandler):
# 解析 effects
effects = render_spec.get_effects()
has_camera_shot = any(e.effect_type == 'cameraShot' for e in effects)
has_complex_effect = any(
effect.effect_type in {'cameraShot', 'zoom'}
for effect in effects
)
# 硬件加速时需要先 hwdownload(将 GPU 表面下载到系统内存)
hwaccel_prefix = self.get_hwaccel_filter_prefix()
@@ -541,9 +544,8 @@ class RenderSegmentVideoHandler(BaseHandler):
)
filters.append(scale_filter)
# 5. 特效处理(cameraShot 需要特殊处理
if has_camera_shot:
# cameraShot 需要使用 filter_complex 格式
# 5. 特效处理(cameraShot / zoom 需要使用 filter_complex
if has_complex_effect:
return self._build_filter_complex_with_effects(
base_filters=filters,
effects=effects,
@@ -624,7 +626,7 @@ class RenderSegmentVideoHandler(BaseHandler):
"""
构建包含特效的 filter_complex 滤镜图
cameraShot 效果需要使用 split/freezeframes/concat 滤镜组合
cameraShot / zoom 效果都在此处统一处理并按 effects 顺序叠加
Args:
base_filters: 基础滤镜列表
@@ -695,6 +697,31 @@ class RenderSegmentVideoHandler(BaseHandler):
f"{frozen_out}{rest_out}concat=n=2:v=1:a=0{effect_output}"
)
current_output = effect_output
effect_idx += 1
elif effect.effect_type == 'zoom':
start_sec, scale_factor, duration_sec = effect.get_zoom_params()
if start_sec < 0 or scale_factor <= 1.0 or duration_sec <= 0:
continue
zoom_end_sec = start_sec + duration_sec
base_out = f'[eff{effect_idx}_base]'
zoom_source_out = f'[eff{effect_idx}_zoom_src]'
zoom_scaled_out = f'[eff{effect_idx}_zoom_scaled]'
effect_output = f'[v_eff{effect_idx}]'
zoom_enable = f"'between(t,{start_sec},{zoom_end_sec})'"
filter_parts.append(
f"{current_output}split=2{base_out}{zoom_source_out}"
)
filter_parts.append(
f"{zoom_source_out}scale=iw*{scale_factor}:ih*{scale_factor},"
f"crop={width}:{height}:(in_w-{width})/2:(in_h-{height})/2{zoom_scaled_out}"
)
filter_parts.append(
f"{base_out}{zoom_scaled_out}overlay=0:0:enable={zoom_enable}{effect_output}"
)
current_output = effect_output
effect_idx += 1