This commit is contained in:
2025-09-24 10:17:11 +08:00
parent dfb07d679f
commit c055a68592
31 changed files with 1357 additions and 811 deletions

11
app.py
View File

@@ -21,15 +21,18 @@ LOGGER = logging.getLogger(__name__)
init_opentelemetry(batch=False) init_opentelemetry(batch=False)
app = flask.Flask(__name__) app = flask.Flask(__name__)
@app.get('/health/check')
@app.get("/health/check")
def health_check(): def health_check():
return api.sync_center() return api.sync_center()
@app.post('/')
@app.post("/")
def do_nothing(): def do_nothing():
return "NOOP" return "NOOP"
@app.post('/<task_id>')
@app.post("/<task_id>")
def do_task(task_id): def do_task(task_id):
try: try:
task_info = api.get_task_info(task_id) task_info = api.get_task_info(task_id)
@@ -64,5 +67,5 @@ def do_task(task_id):
return "Internal server error", 500 return "Internal server error", 500
if __name__ == '__main__': if __name__ == "__main__":
app.run(host="0.0.0.0", port=9998) app.run(host="0.0.0.0", port=9998)

View File

@@ -15,9 +15,10 @@ from util import ffmpeg, oss
from util.ffmpeg import fade_out_audio from util.ffmpeg import fade_out_audio
from telemetry import get_tracer from telemetry import get_tracer
logger = logging.getLogger('biz/ffmpeg') logger = logging.getLogger("biz/ffmpeg")
_render_service = None _render_service = None
def _get_render_service(): def _get_render_service():
"""获取渲染服务实例""" """获取渲染服务实例"""
global _render_service global _render_service
@@ -31,10 +32,17 @@ def parse_ffmpeg_task(task_info, template_info):
解析FFmpeg任务 - 保留用于向后兼容 解析FFmpeg任务 - 保留用于向后兼容
实际处理逻辑已迁移到 services.TaskService.create_render_task 实际处理逻辑已迁移到 services.TaskService.create_render_task
""" """
logger.warning("parse_ffmpeg_task is deprecated, use TaskService.create_render_task instead") logger.warning(
"parse_ffmpeg_task is deprecated, use TaskService.create_render_task instead"
)
# 使用新的任务服务创建任务 # 使用新的任务服务创建任务
from services import DefaultTaskService, DefaultRenderService, DefaultTemplateService from services import (
DefaultTaskService,
DefaultRenderService,
DefaultTemplateService,
)
render_service = DefaultRenderService() render_service = DefaultRenderService()
template_service = DefaultTemplateService() template_service = DefaultTemplateService()
task_service = DefaultTaskService(render_service, template_service) task_service = DefaultTaskService(render_service, template_service)
@@ -43,7 +51,9 @@ def parse_ffmpeg_task(task_info, template_info):
render_task = task_service.create_render_task(task_info, template_info) render_task = task_service.create_render_task(task_info, template_info)
# 为了向后兼容,创建一个FfmpegTask包装器 # 为了向后兼容,创建一个FfmpegTask包装器
ffmpeg_task = FfmpegTask(render_task.input_files, output_file=render_task.output_file) ffmpeg_task = FfmpegTask(
render_task.input_files, output_file=render_task.output_file
)
ffmpeg_task.resolution = render_task.resolution ffmpeg_task.resolution = render_task.resolution
ffmpeg_task.frame_rate = render_task.frame_rate ffmpeg_task.frame_rate = render_task.frame_rate
ffmpeg_task.annexb = render_task.annexb ffmpeg_task.annexb = render_task.annexb
@@ -64,14 +74,20 @@ def parse_video(source, task_params, template_info):
logger.warning("parse_video is deprecated, functionality moved to TaskService") logger.warning("parse_video is deprecated, functionality moved to TaskService")
return source, {} return source, {}
def check_placeholder_exist(placeholder_id, task_params): def check_placeholder_exist(placeholder_id, task_params):
"""已迁移到 TaskService._check_placeholder_exist_with_count""" """已迁移到 TaskService._check_placeholder_exist_with_count"""
logger.warning("check_placeholder_exist is deprecated, functionality moved to TaskService") logger.warning(
"check_placeholder_exist is deprecated, functionality moved to TaskService"
)
return placeholder_id in task_params return placeholder_id in task_params
def check_placeholder_exist_with_count(placeholder_id, task_params, required_count=1): def check_placeholder_exist_with_count(placeholder_id, task_params, required_count=1):
"""已迁移到 TaskService._check_placeholder_exist_with_count""" """已迁移到 TaskService._check_placeholder_exist_with_count"""
logger.warning("check_placeholder_exist_with_count is deprecated, functionality moved to TaskService") logger.warning(
"check_placeholder_exist_with_count is deprecated, functionality moved to TaskService"
)
if placeholder_id in task_params: if placeholder_id in task_params:
new_sources = task_params.get(placeholder_id, []) new_sources = task_params.get(placeholder_id, [])
if isinstance(new_sources, list): if isinstance(new_sources, list):
@@ -104,7 +120,9 @@ def start_ffmpeg_task(ffmpeg_task):
def clear_task_tmp_file(ffmpeg_task): def clear_task_tmp_file(ffmpeg_task):
"""清理临时文件 - 已迁移到 TaskService._cleanup_temp_files""" """清理临时文件 - 已迁移到 TaskService._cleanup_temp_files"""
logger.warning("clear_task_tmp_file is deprecated, functionality moved to TaskService") logger.warning(
"clear_task_tmp_file is deprecated, functionality moved to TaskService"
)
try: try:
template_dir = os.getenv("TEMPLATE_DIR", "") template_dir = os.getenv("TEMPLATE_DIR", "")
output_file = ffmpeg_task.get_output_file() output_file = ffmpeg_task.get_output_file()
@@ -124,5 +142,3 @@ def probe_video_info(ffmpeg_task):
"""获取视频长度宽度和时长 - 使用新的渲染服务""" """获取视频长度宽度和时长 - 使用新的渲染服务"""
render_service = _get_render_service() render_service = _get_render_service()
return render_service.get_video_info(ffmpeg_task.get_output_file()) return render_service.get_video_info(ffmpeg_task.get_output_file())

View File

@@ -12,6 +12,7 @@ logger = logging.getLogger(__name__)
# 确保服务已注册 # 确保服务已注册
register_default_services() register_default_services()
def start_task(task_info): def start_task(task_info):
"""启动任务处理(保持向后兼容的接口)""" """启动任务处理(保持向后兼容的接口)"""
tracer = get_tracer(__name__) tracer = get_tracer(__name__)

View File

@@ -4,16 +4,28 @@ from logging.handlers import TimedRotatingFileHandler
from dotenv import load_dotenv from dotenv import load_dotenv
# 导入新的配置系统,保持向后兼容 # 导入新的配置系统,保持向后兼容
from .settings import get_config, get_ffmpeg_config, get_api_config, get_storage_config, get_server_config from .settings import (
get_config,
get_ffmpeg_config,
get_api_config,
get_storage_config,
get_server_config,
)
load_dotenv() load_dotenv()
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
root_logger = logging.getLogger() root_logger = logging.getLogger()
rf_handler = TimedRotatingFileHandler('all_log.log', when='midnight') rf_handler = TimedRotatingFileHandler("all_log.log", when="midnight")
rf_handler.setFormatter(logging.Formatter("[%(asctime)s][%(name)s]%(levelname)s - %(message)s")) rf_handler.setFormatter(
logging.Formatter("[%(asctime)s][%(name)s]%(levelname)s - %(message)s")
)
rf_handler.setLevel(logging.DEBUG) rf_handler.setLevel(logging.DEBUG)
f_handler = TimedRotatingFileHandler('error.log', when='midnight') f_handler = TimedRotatingFileHandler("error.log", when="midnight")
f_handler.setLevel(logging.ERROR) f_handler.setLevel(logging.ERROR)
f_handler.setFormatter(logging.Formatter("[%(asctime)s][%(name)s][:%(lineno)d]%(levelname)s - - %(message)s")) f_handler.setFormatter(
logging.Formatter(
"[%(asctime)s][%(name)s][:%(lineno)d]%(levelname)s - - %(message)s"
)
)
root_logger.addHandler(rf_handler) root_logger.addHandler(rf_handler)
root_logger.addHandler(f_handler) root_logger.addHandler(f_handler)

View File

@@ -6,9 +6,11 @@ from dotenv import load_dotenv
load_dotenv() load_dotenv()
@dataclass @dataclass
class FFmpegConfig: class FFmpegConfig:
"""FFmpeg相关配置""" """FFmpeg相关配置"""
encoder_args: List[str] encoder_args: List[str]
video_args: List[str] video_args: List[str]
audio_args: List[str] audio_args: List[str]
@@ -26,7 +28,7 @@ class FFmpegConfig:
amix_args: List[str] = None amix_args: List[str] = None
@classmethod @classmethod
def from_env(cls) -> 'FFmpegConfig': def from_env(cls) -> "FFmpegConfig":
encoder_args = os.getenv("ENCODER_ARGS", "-c:v h264").split(" ") encoder_args = os.getenv("ENCODER_ARGS", "-c:v h264").split(" ")
video_args = os.getenv("VIDEO_ARGS", "-profile:v high -level:v 4").split(" ") video_args = os.getenv("VIDEO_ARGS", "-profile:v high -level:v 4").split(" ")
audio_args = ["-c:a", "aac", "-b:a", "128k", "-ar", "48000", "-ac", "2"] audio_args = ["-c:a", "aac", "-b:a", "128k", "-ar", "48000", "-ac", "2"]
@@ -45,7 +47,9 @@ class FFmpegConfig:
loglevel_args = ["-loglevel", "error"] loglevel_args = ["-loglevel", "error"]
null_audio_args = ["-f", "lavfi", "-i", "anullsrc=cl=stereo:r=48000"] null_audio_args = ["-f", "lavfi", "-i", "anullsrc=cl=stereo:r=48000"]
amix_args = ["amix=duration=shortest:dropout_transition=0:normalize=0"] amix_args = ["amix=duration=shortest:dropout_transition=0:normalize=0"]
overlay_scale_mode = "scale" if bool(os.getenv("OLD_FFMPEG", False)) else "scale2ref" overlay_scale_mode = (
"scale" if bool(os.getenv("OLD_FFMPEG", False)) else "scale2ref"
)
return cls( return cls(
encoder_args=encoder_args, encoder_args=encoder_args,
@@ -60,79 +64,89 @@ class FFmpegConfig:
loglevel_args=loglevel_args, loglevel_args=loglevel_args,
null_audio_args=null_audio_args, null_audio_args=null_audio_args,
overlay_scale_mode=overlay_scale_mode, overlay_scale_mode=overlay_scale_mode,
amix_args=amix_args amix_args=amix_args,
) )
@dataclass @dataclass
class APIConfig: class APIConfig:
"""API相关配置""" """API相关配置"""
endpoint: str endpoint: str
access_key: str access_key: str
timeout: int = 10 timeout: int = 10
redirect_to_url: Optional[str] = None redirect_to_url: Optional[str] = None
@classmethod @classmethod
def from_env(cls) -> 'APIConfig': def from_env(cls) -> "APIConfig":
endpoint = os.getenv('API_ENDPOINT', '') endpoint = os.getenv("API_ENDPOINT", "")
if not endpoint: if not endpoint:
raise ValueError("API_ENDPOINT environment variable is required") raise ValueError("API_ENDPOINT environment variable is required")
access_key = os.getenv('ACCESS_KEY', '') access_key = os.getenv("ACCESS_KEY", "")
if not access_key: if not access_key:
raise ValueError("ACCESS_KEY environment variable is required") raise ValueError("ACCESS_KEY environment variable is required")
return cls( return cls(
endpoint=endpoint, endpoint=endpoint,
access_key=access_key, access_key=access_key,
timeout=int(os.getenv('API_TIMEOUT', '10')), timeout=int(os.getenv("API_TIMEOUT", "10")),
redirect_to_url=os.getenv("REDIRECT_TO_URL") or None redirect_to_url=os.getenv("REDIRECT_TO_URL") or None,
) )
@dataclass @dataclass
class StorageConfig: class StorageConfig:
"""存储相关配置""" """存储相关配置"""
template_dir: str template_dir: str
@classmethod @classmethod
def from_env(cls) -> 'StorageConfig': def from_env(cls) -> "StorageConfig":
template_dir = os.getenv('TEMPLATE_DIR', './template') template_dir = os.getenv("TEMPLATE_DIR", "./template")
return cls(template_dir=template_dir) return cls(template_dir=template_dir)
@dataclass @dataclass
class ServerConfig: class ServerConfig:
"""服务器相关配置""" """服务器相关配置"""
host: str = "0.0.0.0" host: str = "0.0.0.0"
port: int = 9998 port: int = 9998
debug: bool = False debug: bool = False
@classmethod @classmethod
def from_env(cls) -> 'ServerConfig': def from_env(cls) -> "ServerConfig":
return cls( return cls(
host=os.getenv('HOST', '0.0.0.0'), host=os.getenv("HOST", "0.0.0.0"),
port=int(os.getenv('PORT', '9998')), port=int(os.getenv("PORT", "9998")),
debug=bool(os.getenv('DEBUG', False)) debug=bool(os.getenv("DEBUG", False)),
) )
@dataclass @dataclass
class AppConfig: class AppConfig:
"""应用总配置""" """应用总配置"""
ffmpeg: FFmpegConfig ffmpeg: FFmpegConfig
api: APIConfig api: APIConfig
storage: StorageConfig storage: StorageConfig
server: ServerConfig server: ServerConfig
@classmethod @classmethod
def from_env(cls) -> 'AppConfig': def from_env(cls) -> "AppConfig":
return cls( return cls(
ffmpeg=FFmpegConfig.from_env(), ffmpeg=FFmpegConfig.from_env(),
api=APIConfig.from_env(), api=APIConfig.from_env(),
storage=StorageConfig.from_env(), storage=StorageConfig.from_env(),
server=ServerConfig.from_env() server=ServerConfig.from_env(),
) )
# 全局配置实例 # 全局配置实例
_config: Optional[AppConfig] = None _config: Optional[AppConfig] = None
def get_config() -> AppConfig: def get_config() -> AppConfig:
"""获取全局配置实例""" """获取全局配置实例"""
global _config global _config
@@ -140,21 +154,26 @@ def get_config() -> AppConfig:
_config = AppConfig.from_env() _config = AppConfig.from_env()
return _config return _config
def reload_config() -> AppConfig: def reload_config() -> AppConfig:
"""重新加载配置""" """重新加载配置"""
global _config global _config
_config = AppConfig.from_env() _config = AppConfig.from_env()
return _config return _config
# 向后兼容的配置获取函数 # 向后兼容的配置获取函数
def get_ffmpeg_config() -> FFmpegConfig: def get_ffmpeg_config() -> FFmpegConfig:
return get_config().ffmpeg return get_config().ffmpeg
def get_api_config() -> APIConfig: def get_api_config() -> APIConfig:
return get_config().api return get_config().api
def get_storage_config() -> StorageConfig: def get_storage_config() -> StorageConfig:
return get_config().storage return get_config().storage
def get_server_config() -> ServerConfig: def get_server_config() -> ServerConfig:
return get_config().server return get_config().server

View File

@@ -1,9 +1,9 @@
SUPPORT_FEATURE = ( SUPPORT_FEATURE = (
'simple_render_algo', "simple_render_algo",
'gpu_accelerate', "gpu_accelerate",
'hevc_encode', "hevc_encode",
'rapid_download', "rapid_download",
'rclone_upload', "rclone_upload",
'custom_re_encode', "custom_re_encode",
) )
SOFTWARE_VERSION = '0.0.5' SOFTWARE_VERSION = "0.0.5"

0
entity/__init__.py Normal file
View File

View File

@@ -7,19 +7,19 @@ from .tail import TailEffect
# 注册所有效果处理器 # 注册所有效果处理器
registry = EffectRegistry() registry = EffectRegistry()
registry.register('cameraShot', CameraShotEffect) registry.register("cameraShot", CameraShotEffect)
registry.register('ospeed', SpeedEffect) registry.register("ospeed", SpeedEffect)
registry.register('zoom', ZoomEffect) registry.register("zoom", ZoomEffect)
registry.register('skip', SkipEffect) registry.register("skip", SkipEffect)
registry.register('tail', TailEffect) registry.register("tail", TailEffect)
__all__ = [ __all__ = [
'EffectProcessor', "EffectProcessor",
'EffectRegistry', "EffectRegistry",
'registry', "registry",
'CameraShotEffect', "CameraShotEffect",
'SpeedEffect', "SpeedEffect",
'ZoomEffect', "ZoomEffect",
'SkipEffect', "SkipEffect",
'TailEffect' "TailEffect",
] ]

View File

@@ -5,6 +5,7 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class EffectProcessor(ABC): class EffectProcessor(ABC):
"""效果处理器抽象基类""" """效果处理器抽象基类"""
@@ -19,7 +20,9 @@ class EffectProcessor(ABC):
pass pass
@abstractmethod @abstractmethod
def generate_filter_args(self, video_input: str, effect_index: int) -> tuple[List[str], str]: def generate_filter_args(
self, video_input: str, effect_index: int
) -> tuple[List[str], str]:
""" """
生成FFmpeg滤镜参数 生成FFmpeg滤镜参数
@@ -41,17 +44,18 @@ class EffectProcessor(ABC):
"""解析参数字符串为列表""" """解析参数字符串为列表"""
if not self.params: if not self.params:
return [] return []
return self.params.split(',') return self.params.split(",")
def get_pos_json(self) -> Dict[str, Any]: def get_pos_json(self) -> Dict[str, Any]:
"""获取位置JSON数据""" """获取位置JSON数据"""
pos_json_str = self.ext_data.get('posJson', '{}') pos_json_str = self.ext_data.get("posJson", "{}")
try: try:
return json.loads(pos_json_str) if pos_json_str != '{}' else {} return json.loads(pos_json_str) if pos_json_str != "{}" else {}
except Exception as e: except Exception as e:
logger.warning(f"Failed to parse posJson: {e}") logger.warning(f"Failed to parse posJson: {e}")
return {} return {}
class EffectRegistry: class EffectRegistry:
"""效果处理器注册表""" """效果处理器注册表"""
@@ -65,7 +69,12 @@ class EffectRegistry:
self._processors[name] = processor_class self._processors[name] = processor_class
logger.debug(f"Registered effect processor: {name}") logger.debug(f"Registered effect processor: {name}")
def get_processor(self, effect_name: str, params: str = "", ext_data: Optional[Dict[str, Any]] = None) -> Optional[EffectProcessor]: def get_processor(
self,
effect_name: str,
params: str = "",
ext_data: Optional[Dict[str, Any]] = None,
) -> Optional[EffectProcessor]:
"""获取效果处理器实例""" """获取效果处理器实例"""
if effect_name not in self._processors: if effect_name not in self._processors:
logger.warning(f"Unknown effect: {effect_name}") logger.warning(f"Unknown effect: {effect_name}")
@@ -88,7 +97,7 @@ class EffectRegistry:
Returns: Returns:
tuple: (effect_name, params) tuple: (effect_name, params)
""" """
if ':' in effect_string: if ":" in effect_string:
parts = effect_string.split(':', 2) parts = effect_string.split(":", 2)
return parts[0], parts[1] if len(parts) > 1 else "" return parts[0], parts[1] if len(parts) > 1 else ""
return effect_string, "" return effect_string, ""

View File

@@ -1,6 +1,7 @@
from typing import List, Dict, Any from typing import List, Dict, Any
from .base import EffectProcessor from .base import EffectProcessor
class CameraShotEffect(EffectProcessor): class CameraShotEffect(EffectProcessor):
"""相机镜头效果处理器""" """相机镜头效果处理器"""
@@ -16,7 +17,7 @@ class CameraShotEffect(EffectProcessor):
try: try:
for i, param in enumerate(params): for i, param in enumerate(params):
if param == '': if param == "":
continue continue
if i == 2: # rotate_deg if i == 2: # rotate_deg
int(param) int(param)
@@ -26,7 +27,9 @@ class CameraShotEffect(EffectProcessor):
except ValueError: except ValueError:
return False return False
def generate_filter_args(self, video_input: str, effect_index: int) -> tuple[List[str], str]: def generate_filter_args(
self, video_input: str, effect_index: int
) -> tuple[List[str], str]:
"""生成相机镜头效果的滤镜参数""" """生成相机镜头效果的滤镜参数"""
if not self.validate_params(): if not self.validate_params():
return [], video_input return [], video_input
@@ -38,11 +41,11 @@ class CameraShotEffect(EffectProcessor):
duration = 1.0 duration = 1.0
rotate_deg = 0 rotate_deg = 0
if len(params) >= 1 and params[0] != '': if len(params) >= 1 and params[0] != "":
start = float(params[0]) start = float(params[0])
if len(params) >= 2 and params[1] != '': if len(params) >= 2 and params[1] != "":
duration = float(params[1]) duration = float(params[1])
if len(params) >= 3 and params[2] != '': if len(params) >= 3 and params[2] != "":
rotate_deg = int(params[2]) rotate_deg = int(params[2])
filter_args = [] filter_args = []
@@ -54,24 +57,36 @@ class CameraShotEffect(EffectProcessor):
final_output = f"[v_eff{effect_index}]" final_output = f"[v_eff{effect_index}]"
# 分割视频流为三部分 # 分割视频流为三部分
filter_args.append(f"{video_input}split=3{start_out_str}{mid_out_str}{end_out_str}") filter_args.append(
f"{video_input}split=3{start_out_str}{mid_out_str}{end_out_str}"
)
# 选择开始部分帧 # 选择开始部分帧
filter_args.append(f"{start_out_str}select=lt(n\\,{int(start * self.frame_rate)}){start_out_str}") filter_args.append(
f"{start_out_str}select=lt(n\\,{int(start * self.frame_rate)}){start_out_str}"
)
# 选择结束部分帧 # 选择结束部分帧
filter_args.append(f"{end_out_str}select=gt(n\\,{int(start * self.frame_rate)}){end_out_str}") filter_args.append(
f"{end_out_str}select=gt(n\\,{int(start * self.frame_rate)}){end_out_str}"
)
# 选择中间特定帧并扩展 # 选择中间特定帧并扩展
filter_args.append(f"{mid_out_str}select=eq(n\\,{int(start * self.frame_rate)}){mid_out_str}") filter_args.append(
filter_args.append(f"{mid_out_str}tpad=start_mode=clone:start_duration={duration:.4f}{mid_out_str}") f"{mid_out_str}select=eq(n\\,{int(start * self.frame_rate)}){mid_out_str}"
)
filter_args.append(
f"{mid_out_str}tpad=start_mode=clone:start_duration={duration:.4f}{mid_out_str}"
)
# 如果需要旋转 # 如果需要旋转
if rotate_deg != 0: if rotate_deg != 0:
filter_args.append(f"{mid_out_str}rotate=PI*{rotate_deg}/180{mid_out_str}") filter_args.append(f"{mid_out_str}rotate=PI*{rotate_deg}/180{mid_out_str}")
# 连接三部分 # 连接三部分
filter_args.append(f"{start_out_str}{mid_out_str}{end_out_str}concat=n=3:v=1:a=0,setpts=N/{self.frame_rate}/TB{final_output}") filter_args.append(
f"{start_out_str}{mid_out_str}{end_out_str}concat=n=3:v=1:a=0,setpts=N/{self.frame_rate}/TB{final_output}"
)
return filter_args, final_output return filter_args, final_output

View File

@@ -1,6 +1,7 @@
from typing import List from typing import List
from .base import EffectProcessor from .base import EffectProcessor
class SkipEffect(EffectProcessor): class SkipEffect(EffectProcessor):
"""跳过开头效果处理器""" """跳过开头效果处理器"""
@@ -15,7 +16,9 @@ class SkipEffect(EffectProcessor):
except ValueError: except ValueError:
return False return False
def generate_filter_args(self, video_input: str, effect_index: int) -> tuple[List[str], str]: def generate_filter_args(
self, video_input: str, effect_index: int
) -> tuple[List[str], str]:
"""生成跳过开头效果的滤镜参数""" """生成跳过开头效果的滤镜参数"""
if not self.validate_params(): if not self.validate_params():
return [], video_input return [], video_input

View File

@@ -1,6 +1,7 @@
from typing import List from typing import List
from .base import EffectProcessor from .base import EffectProcessor
class SpeedEffect(EffectProcessor): class SpeedEffect(EffectProcessor):
"""视频变速效果处理器""" """视频变速效果处理器"""
@@ -15,7 +16,9 @@ class SpeedEffect(EffectProcessor):
except ValueError: except ValueError:
return False return False
def generate_filter_args(self, video_input: str, effect_index: int) -> tuple[List[str], str]: def generate_filter_args(
self, video_input: str, effect_index: int
) -> tuple[List[str], str]:
"""生成变速效果的滤镜参数""" """生成变速效果的滤镜参数"""
if not self.validate_params(): if not self.validate_params():
return [], video_input return [], video_input

View File

@@ -1,6 +1,7 @@
from typing import List from typing import List
from .base import EffectProcessor from .base import EffectProcessor
class TailEffect(EffectProcessor): class TailEffect(EffectProcessor):
"""保留末尾效果处理器""" """保留末尾效果处理器"""
@@ -15,7 +16,9 @@ class TailEffect(EffectProcessor):
except ValueError: except ValueError:
return False return False
def generate_filter_args(self, video_input: str, effect_index: int) -> tuple[List[str], str]: def generate_filter_args(
self, video_input: str, effect_index: int
) -> tuple[List[str], str]:
"""生成保留末尾效果的滤镜参数""" """生成保留末尾效果的滤镜参数"""
if not self.validate_params(): if not self.validate_params():
return [], video_input return [], video_input
@@ -33,7 +36,7 @@ class TailEffect(EffectProcessor):
filter_args = [ filter_args = [
f"{video_input}reverse[v_rev{effect_index}]", f"{video_input}reverse[v_rev{effect_index}]",
f"[v_rev{effect_index}]trim=duration={tail_seconds}[v_trim{effect_index}]", f"[v_rev{effect_index}]trim=duration={tail_seconds}[v_trim{effect_index}]",
f"[v_trim{effect_index}]reverse{output_stream}" f"[v_trim{effect_index}]reverse{output_stream}",
] ]
return filter_args, output_stream return filter_args, output_stream

View File

@@ -2,6 +2,7 @@ from typing import List
import json import json
from .base import EffectProcessor from .base import EffectProcessor
class ZoomEffect(EffectProcessor): class ZoomEffect(EffectProcessor):
"""缩放效果处理器""" """缩放效果处理器"""
@@ -16,13 +17,13 @@ class ZoomEffect(EffectProcessor):
zoom_factor = float(params[1]) zoom_factor = float(params[1])
duration = float(params[2]) duration = float(params[2])
return (start_time >= 0 and return start_time >= 0 and zoom_factor > 0 and duration >= 0
zoom_factor > 0 and
duration >= 0)
except (ValueError, IndexError): except (ValueError, IndexError):
return False return False
def generate_filter_args(self, video_input: str, effect_index: int) -> tuple[List[str], str]: def generate_filter_args(
self, video_input: str, effect_index: int
) -> tuple[List[str], str]:
"""生成缩放效果的滤镜参数""" """生成缩放效果的滤镜参数"""
if not self.validate_params(): if not self.validate_params():
return [], video_input return [], video_input
@@ -68,12 +69,12 @@ class ZoomEffect(EffectProcessor):
pos_json = self.get_pos_json() pos_json = self.get_pos_json()
if pos_json: if pos_json:
_f_x = pos_json.get('ltX', 0) _f_x = pos_json.get("ltX", 0)
_f_x2 = pos_json.get('rbX', 0) _f_x2 = pos_json.get("rbX", 0)
_f_y = pos_json.get('ltY', 0) _f_y = pos_json.get("ltY", 0)
_f_y2 = pos_json.get('rbY', 0) _f_y2 = pos_json.get("rbY", 0)
_v_w = pos_json.get('imgWidth', 1) _v_w = pos_json.get("imgWidth", 1)
_v_h = pos_json.get('imgHeight', 1) _v_h = pos_json.get("imgHeight", 1)
if _v_w > 0 and _v_h > 0: if _v_w > 0 and _v_h > 0:
# 计算坐标系统中的中心点 # 计算坐标系统中的中心点

View File

@@ -2,10 +2,40 @@
import os import os
DEFAULT_ARGS = ("-shortest",) DEFAULT_ARGS = ("-shortest",)
ENCODER_ARGS = ("-c:v", "h264", ) if not os.getenv("ENCODER_ARGS", False) else os.getenv("ENCODER_ARGS", "").split(" ") ENCODER_ARGS = (
VIDEO_ARGS = ("-profile:v", "high", "-level:v", "4", ) if not os.getenv("VIDEO_ARGS", False) else os.getenv("VIDEO_ARGS", "").split(" ") (
AUDIO_ARGS = ("-c:a", "aac", "-b:a", "128k", "-ar", "48000", "-ac", "2", ) "-c:v",
MUTE_AUDIO_INPUT = ("-f", "lavfi", "-i", "anullsrc=cl=stereo:r=48000", ) "h264",
)
if not os.getenv("ENCODER_ARGS", False)
else os.getenv("ENCODER_ARGS", "").split(" ")
)
VIDEO_ARGS = (
(
"-profile:v",
"high",
"-level:v",
"4",
)
if not os.getenv("VIDEO_ARGS", False)
else os.getenv("VIDEO_ARGS", "").split(" ")
)
AUDIO_ARGS = (
"-c:a",
"aac",
"-b:a",
"128k",
"-ar",
"48000",
"-ac",
"2",
)
MUTE_AUDIO_INPUT = (
"-f",
"lavfi",
"-i",
"anullsrc=cl=stereo:r=48000",
)
def get_mp4toannexb_filter(): def get_mp4toannexb_filter():
@@ -25,7 +55,7 @@ class FfmpegTask(object):
实际处理逻辑已迁移到新架构,该类主要用作数据载体 实际处理逻辑已迁移到新架构,该类主要用作数据载体
""" """
def __init__(self, input_file, task_type='copy', output_file=''): def __init__(self, input_file, task_type="copy", output_file=""):
"""保持原有构造函数签名""" """保持原有构造函数签名"""
self.annexb = False self.annexb = False
if type(input_file) is str: if type(input_file) is str:
@@ -52,7 +82,7 @@ class FfmpegTask(object):
self.effects = [] self.effects = []
def __repr__(self): def __repr__(self):
return f'FfmpegTask(input_file={self.input_file}, task_type={self.task_type})' return f"FfmpegTask(input_file={self.input_file}, task_type={self.task_type})"
def analyze_input_render_tasks(self): def analyze_input_render_tasks(self):
"""分析输入中的子任务""" """分析输入中的子任务"""
@@ -73,7 +103,7 @@ class FfmpegTask(object):
def add_overlay(self, *overlays): def add_overlay(self, *overlays):
"""添加覆盖层""" """添加覆盖层"""
for overlay in overlays: for overlay in overlays:
if str(overlay).endswith('.ass'): if str(overlay).endswith(".ass"):
self.subtitles.append(overlay) self.subtitles.append(overlay)
else: else:
self.overlays.append(overlay) self.overlays.append(overlay)
@@ -96,7 +126,7 @@ class FfmpegTask(object):
def get_output_file(self): def get_output_file(self):
"""获取输出文件""" """获取输出文件"""
if self.task_type == 'copy': if self.task_type == "copy":
return self.input_file[0] if self.input_file else "" return self.input_file[0] if self.input_file else ""
if not self.output_file: if not self.output_file:
self.set_output_file() self.set_output_file()
@@ -105,29 +135,43 @@ class FfmpegTask(object):
def correct_task_type(self): def correct_task_type(self):
"""校正任务类型""" """校正任务类型"""
if self.check_can_copy(): if self.check_can_copy():
self.task_type = 'copy' self.task_type = "copy"
elif self.check_can_concat(): elif self.check_can_concat():
self.task_type = 'concat' self.task_type = "concat"
else: else:
self.task_type = 'encode' self.task_type = "encode"
def check_can_concat(self): def check_can_concat(self):
"""检查是否可以连接""" """检查是否可以连接"""
return (len(self.luts) == 0 and len(self.overlays) == 0 and return (
len(self.subtitles) == 0 and len(self.effects) == 0 and len(self.luts) == 0
self.speed == 1 and self.zoom_cut is None and self.center_cut is None) and len(self.overlays) == 0
and len(self.subtitles) == 0
and len(self.effects) == 0
and self.speed == 1
and self.zoom_cut is None
and self.center_cut is None
)
def check_can_copy(self): def check_can_copy(self):
"""检查是否可以复制""" """检查是否可以复制"""
return (len(self.luts) == 0 and len(self.overlays) == 0 and return (
len(self.subtitles) == 0 and len(self.effects) == 0 and len(self.luts) == 0
self.speed == 1 and len(self.audios) == 0 and len(self.input_file) <= 1 and and len(self.overlays) == 0
self.zoom_cut is None and self.center_cut is None) and len(self.subtitles) == 0
and len(self.effects) == 0
and self.speed == 1
and len(self.audios) == 0
and len(self.input_file) <= 1
and self.zoom_cut is None
and self.center_cut is None
)
def set_output_file(self, file=None): def set_output_file(self, file=None):
"""设置输出文件""" """设置输出文件"""
if file is None: if file is None:
import uuid import uuid
if self.annexb: if self.annexb:
self.output_file = f"rand_{uuid.uuid4()}.ts" self.output_file = f"rand_{uuid.uuid4()}.ts"
else: else:
@@ -149,12 +193,24 @@ class FfmpegTask(object):
建议使用新的 FFmpegCommandBuilder 来生成命令 建议使用新的 FFmpegCommandBuilder 来生成命令
""" """
# 简化版本,主要用于向后兼容 # 简化版本,主要用于向后兼容
if self.task_type == 'copy' and len(self.input_file) == 1: if self.task_type == "copy" and len(self.input_file) == 1:
if isinstance(self.input_file[0], str): if isinstance(self.input_file[0], str):
if self.input_file[0] == self.get_output_file(): if self.input_file[0] == self.get_output_file():
return [] return []
return ['-y', '-hide_banner', '-i', self.input_file[0], '-c', 'copy', self.get_output_file()] return [
"-y",
"-hide_banner",
"-i",
self.input_file[0],
"-c",
"copy",
self.get_output_file(),
]
# 对于复杂情况,返回基础命令结构 # 对于复杂情况,返回基础命令结构
# 实际处理会在新的服务架构中完成 # 实际处理会在新的服务架构中完成
return ['-y', '-hide_banner', '-i'] + self.input_file + ['-c', 'copy', self.get_output_file()] return (
["-y", "-hide_banner", "-i"]
+ self.input_file
+ ["-c", "copy", self.get_output_file()]
)

View File

@@ -8,14 +8,19 @@ from entity.effects import registry as effect_registry
from util.exceptions import FFmpegError from util.exceptions import FFmpegError
from util.ffmpeg import probe_video_info, probe_video_audio from util.ffmpeg import probe_video_info, probe_video_audio
from util.ffmpeg_utils import ( from util.ffmpeg_utils import (
build_base_ffmpeg_args, build_null_audio_input, build_amix_filter, build_base_ffmpeg_args,
build_overlay_scale_filter, get_annexb_filter, build_standard_output_args build_null_audio_input,
build_amix_filter,
build_overlay_scale_filter,
get_annexb_filter,
build_standard_output_args,
) )
from util.json_utils import safe_json_loads from util.json_utils import safe_json_loads
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class FFmpegCommandBuilder: class FFmpegCommandBuilder:
"""FFmpeg命令构建器""" """FFmpeg命令构建器"""
@@ -44,10 +49,14 @@ class FFmpegCommandBuilder:
return [] # 不需要处理 return [] # 不需要处理
return [ return [
"ffmpeg", "-y", "-hide_banner", "ffmpeg",
"-i", self.task.input_files[0], "-y",
"-c", "copy", "-hide_banner",
self.task.output_file "-i",
self.task.input_files[0],
"-c",
"copy",
self.task.output_file,
] ]
def _build_concat_command(self) -> List[str]: def _build_concat_command(self) -> List[str]:
@@ -88,9 +97,13 @@ class FFmpegCommandBuilder:
else: else:
output_args.extend(["-f", "mp4"]) output_args.extend(["-f", "mp4"])
filter_complex = ["-filter_complex", ";".join(filter_args)] if filter_args else [] filter_complex = (
["-filter_complex", ";".join(filter_args)] if filter_args else []
)
return args + input_args + filter_complex + output_args + [self.task.output_file] return (
args + input_args + filter_complex + output_args + [self.task.output_file]
)
def _build_encode_command(self) -> List[str]: def _build_encode_command(self) -> List[str]:
"""构建编码命令""" """构建编码命令"""
@@ -115,18 +128,26 @@ class FFmpegCommandBuilder:
# 处理中心裁剪 # 处理中心裁剪
if self.task.center_cut == 1: if self.task.center_cut == 1:
video_output_str, effect_index = self._add_center_cut(filter_args, video_output_str, effect_index) video_output_str, effect_index = self._add_center_cut(
filter_args, video_output_str, effect_index
)
# 处理缩放裁剪 # 处理缩放裁剪
if self.task.zoom_cut == 1 and self.task.resolution: if self.task.zoom_cut == 1 and self.task.resolution:
video_output_str, effect_index = self._add_zoom_cut(filter_args, video_output_str, effect_index) video_output_str, effect_index = self._add_zoom_cut(
filter_args, video_output_str, effect_index
)
# 处理效果 # 处理效果
video_output_str, effect_index = self._add_effects(filter_args, video_output_str, effect_index) video_output_str, effect_index = self._add_effects(
filter_args, video_output_str, effect_index
)
# 处理分辨率 # 处理分辨率
if self.task.resolution: if self.task.resolution:
filter_args.append(f"{video_output_str}scale={self.task.resolution.replace('x', ':')}[v]") filter_args.append(
f"{video_output_str}scale={self.task.resolution.replace('x', ':')}[v]"
)
video_output_str = "[v]" video_output_str = "[v]"
# 处理LUT # 处理LUT
@@ -151,57 +172,75 @@ class FFmpegCommandBuilder:
if audio_output_str: if audio_output_str:
output_args.extend(["-map", audio_output_str]) output_args.extend(["-map", audio_output_str])
filter_complex = ["-filter_complex", ";".join(filter_args)] if filter_args else [] filter_complex = (
["-filter_complex", ";".join(filter_args)] if filter_args else []
)
return args + input_args + filter_complex + output_args + [self.task.output_file] return (
args + input_args + filter_complex + output_args + [self.task.output_file]
)
def _add_center_cut(self, filter_args: List[str], video_input: str, effect_index: int) -> tuple[str, int]: def _add_center_cut(
self, filter_args: List[str], video_input: str, effect_index: int
) -> tuple[str, int]:
"""添加中心裁剪""" """添加中心裁剪"""
pos_json = self.task.ext_data.get('posJson', '{}') pos_json = self.task.ext_data.get("posJson", "{}")
pos_data = safe_json_loads(pos_json, {}) pos_data = safe_json_loads(pos_json, {})
_v_w = pos_data.get('imgWidth', 1) _v_w = pos_data.get("imgWidth", 1)
_f_x = pos_data.get('ltX', 0) _f_x = pos_data.get("ltX", 0)
_f_x2 = pos_data.get('rbX', 0) _f_x2 = pos_data.get("rbX", 0)
_x = f'{float((_f_x2 + _f_x)/(2 * _v_w)):.4f}*iw-ih*ih/(2*iw)' _x = f"{float((_f_x2 + _f_x)/(2 * _v_w)):.4f}*iw-ih*ih/(2*iw)"
filter_args.append(f"{video_input}crop=x={_x}:y=0:w=ih*ih/iw:h=ih[v_cut{effect_index}]") filter_args.append(
f"{video_input}crop=x={_x}:y=0:w=ih*ih/iw:h=ih[v_cut{effect_index}]"
)
return f"[v_cut{effect_index}]", effect_index + 1 return f"[v_cut{effect_index}]", effect_index + 1
def _add_zoom_cut(self, filter_args: List[str], video_input: str, effect_index: int) -> tuple[str, int]: def _add_zoom_cut(
self, filter_args: List[str], video_input: str, effect_index: int
) -> tuple[str, int]:
"""添加缩放裁剪""" """添加缩放裁剪"""
# 获取输入视频尺寸 # 获取输入视频尺寸
input_file = self.task.input_files[0] input_file = self.task.input_files[0]
_iw, _ih, _ = probe_video_info(input_file) _iw, _ih, _ = probe_video_info(input_file)
_w, _h = self.task.resolution.split('x', 1) _w, _h = self.task.resolution.split("x", 1)
pos_json = self.task.ext_data.get('posJson', '{}') pos_json = self.task.ext_data.get("posJson", "{}")
pos_data = safe_json_loads(pos_json, {}) pos_data = safe_json_loads(pos_json, {})
_v_w = pos_data.get('imgWidth', 1) _v_w = pos_data.get("imgWidth", 1)
_v_h = pos_data.get('imgHeight', 1) _v_h = pos_data.get("imgHeight", 1)
_f_x = pos_data.get('ltX', 0) _f_x = pos_data.get("ltX", 0)
_f_x2 = pos_data.get('rbX', 0) _f_x2 = pos_data.get("rbX", 0)
_f_y = pos_data.get('ltY', 0) _f_y = pos_data.get("ltY", 0)
_f_y2 = pos_data.get('rbY', 0) _f_y2 = pos_data.get("rbY", 0)
_x = min(max(0, int((_f_x + _f_x2) / 2 - int(_w) / 2)), _iw - int(_w)) _x = min(max(0, int((_f_x + _f_x2) / 2 - int(_w) / 2)), _iw - int(_w))
_y = min(max(0, int((_f_y + _f_y2) / 2 - int(_h) / 2)), _ih - int(_h)) _y = min(max(0, int((_f_y + _f_y2) / 2 - int(_h) / 2)), _ih - int(_h))
filter_args.append(f"{video_input}crop=x={_x}:y={_y}:w={_w}:h={_h}[vz_cut{effect_index}]") filter_args.append(
f"{video_input}crop=x={_x}:y={_y}:w={_w}:h={_h}[vz_cut{effect_index}]"
)
return f"[vz_cut{effect_index}]", effect_index + 1 return f"[vz_cut{effect_index}]", effect_index + 1
def _add_effects(self, filter_args: List[str], video_input: str, effect_index: int) -> tuple[str, int]: def _add_effects(
self, filter_args: List[str], video_input: str, effect_index: int
) -> tuple[str, int]:
"""添加效果处理""" """添加效果处理"""
current_input = video_input current_input = video_input
for effect_str in self.task.effects: for effect_str in self.task.effects:
effect_name, params = effect_registry.parse_effect_string(effect_str) effect_name, params = effect_registry.parse_effect_string(effect_str)
processor = effect_registry.get_processor(effect_name, params, self.task.ext_data) processor = effect_registry.get_processor(
effect_name, params, self.task.ext_data
)
if processor: if processor:
processor.frame_rate = self.task.frame_rate processor.frame_rate = self.task.frame_rate
effect_filters, output_stream = processor.generate_filter_args(current_input, effect_index) effect_filters, output_stream = processor.generate_filter_args(
current_input, effect_index
)
if effect_filters: if effect_filters:
filter_args.extend(effect_filters) filter_args.extend(effect_filters)
@@ -210,7 +249,9 @@ class FFmpegCommandBuilder:
return current_input, effect_index return current_input, effect_index
def _add_overlays(self, input_args: List[str], filter_args: List[str], video_input: str) -> str: def _add_overlays(
self, input_args: List[str], filter_args: List[str], video_input: str
) -> str:
"""添加覆盖层""" """添加覆盖层"""
current_input = video_input current_input = video_input
@@ -221,14 +262,18 @@ class FFmpegCommandBuilder:
if self.config.overlay_scale_mode == "scale": if self.config.overlay_scale_mode == "scale":
filter_args.append(f"{current_input}[{input_index}:v]scale=iw:ih[v]") filter_args.append(f"{current_input}[{input_index}:v]scale=iw:ih[v]")
else: else:
filter_args.append(f"{current_input}[{input_index}:v]{self.config.overlay_scale_mode}=iw:ih[v]") filter_args.append(
f"{current_input}[{input_index}:v]{self.config.overlay_scale_mode}=iw:ih[v]"
)
filter_args.append(f"[v][{input_index}:v]overlay=1:eof_action=endall[v]") filter_args.append(f"[v][{input_index}:v]overlay=1:eof_action=endall[v]")
current_input = "[v]" current_input = "[v]"
return current_input return current_input
def _handle_audio_concat(self, input_args: List[str], filter_args: List[str]) -> Optional[str]: def _handle_audio_concat(
self, input_args: List[str], filter_args: List[str]
) -> Optional[str]:
"""处理concat模式的音频""" """处理concat模式的音频"""
audio_output_str = "" audio_output_str = ""
@@ -242,12 +287,16 @@ class FFmpegCommandBuilder:
for audio in self.task.audios: for audio in self.task.audios:
input_index = input_args.count("-i") // 2 input_index = input_args.count("-i") // 2
input_args.extend(["-i", audio.replace("\\", "/")]) input_args.extend(["-i", audio.replace("\\", "/")])
filter_args.append(f"{audio_output_str}[{input_index}:a]{self.config.amix_args[0]}[a]") filter_args.append(
f"{audio_output_str}[{input_index}:a]{self.config.amix_args[0]}[a]"
)
audio_output_str = "[a]" audio_output_str = "[a]"
return audio_output_str.strip("[]") if audio_output_str else None return audio_output_str.strip("[]") if audio_output_str else None
def _handle_audio_encode(self, input_args: List[str], filter_args: List[str]) -> Optional[str]: def _handle_audio_encode(
self, input_args: List[str], filter_args: List[str]
) -> Optional[str]:
"""处理encode模式的音频""" """处理encode模式的音频"""
audio_output_str = "" audio_output_str = ""
@@ -262,8 +311,9 @@ class FFmpegCommandBuilder:
for audio in self.task.audios: for audio in self.task.audios:
input_index = input_args.count("-i") // 2 input_index = input_args.count("-i") // 2
input_args.extend(["-i", audio.replace("\\", "/")]) input_args.extend(["-i", audio.replace("\\", "/")])
filter_args.append(f"{audio_output_str}[{input_index}:a]{self.config.amix_args[0]}[a]") filter_args.append(
f"{audio_output_str}[{input_index}:a]{self.config.amix_args[0]}[a]"
)
audio_output_str = "[a]" audio_output_str = "[a]"
return audio_output_str if audio_output_str else None return audio_output_str if audio_output_str else None

View File

@@ -8,14 +8,17 @@ from config.settings import get_ffmpeg_config
from util.exceptions import TaskValidationError, EffectError from util.exceptions import TaskValidationError, EffectError
from entity.effects import registry as effect_registry from entity.effects import registry as effect_registry
class TaskType(Enum): class TaskType(Enum):
COPY = "copy" COPY = "copy"
CONCAT = "concat" CONCAT = "concat"
ENCODE = "encode" ENCODE = "encode"
@dataclass @dataclass
class RenderTask: class RenderTask:
"""渲染任务数据类,只包含任务数据,不包含处理逻辑""" """渲染任务数据类,只包含任务数据,不包含处理逻辑"""
input_files: List[str] = field(default_factory=list) input_files: List[str] = field(default_factory=list)
output_file: str = "" output_file: str = ""
task_type: TaskType = TaskType.COPY task_type: TaskType = TaskType.COPY
@@ -69,7 +72,7 @@ class RenderTask:
def add_overlay(self, *overlays: str): def add_overlay(self, *overlays: str):
"""添加覆盖层""" """添加覆盖层"""
for overlay in overlays: for overlay in overlays:
if overlay.endswith('.ass'): if overlay.endswith(".ass"):
self.subtitles.append(overlay) self.subtitles.append(overlay)
else: else:
self.overlays.append(overlay) self.overlays.append(overlay)
@@ -94,33 +97,43 @@ class RenderTask:
# 验证所有效果 # 验证所有效果
for effect_str in self.effects: for effect_str in self.effects:
effect_name, params = effect_registry.parse_effect_string(effect_str) effect_name, params = effect_registry.parse_effect_string(effect_str)
processor = effect_registry.get_processor(effect_name, params, self.ext_data) processor = effect_registry.get_processor(
effect_name, params, self.ext_data
)
if processor and not processor.validate_params(): if processor and not processor.validate_params():
raise EffectError(f"Invalid parameters for effect {effect_name}: {params}", effect_name, params) raise EffectError(
f"Invalid parameters for effect {effect_name}: {params}",
effect_name,
params,
)
return True return True
def can_copy(self) -> bool: def can_copy(self) -> bool:
"""检查是否可以直接复制""" """检查是否可以直接复制"""
return (len(self.luts) == 0 and return (
len(self.overlays) == 0 and len(self.luts) == 0
len(self.subtitles) == 0 and and len(self.overlays) == 0
len(self.effects) == 0 and and len(self.subtitles) == 0
self.speed == 1 and and len(self.effects) == 0
len(self.audios) == 0 and and self.speed == 1
len(self.input_files) == 1 and and len(self.audios) == 0
self.zoom_cut is None and and len(self.input_files) == 1
self.center_cut is None) and self.zoom_cut is None
and self.center_cut is None
)
def can_concat(self) -> bool: def can_concat(self) -> bool:
"""检查是否可以使用concat模式""" """检查是否可以使用concat模式"""
return (len(self.luts) == 0 and return (
len(self.overlays) == 0 and len(self.luts) == 0
len(self.subtitles) == 0 and and len(self.overlays) == 0
len(self.effects) == 0 and and len(self.subtitles) == 0
self.speed == 1 and and len(self.effects) == 0
self.zoom_cut is None and and self.speed == 1
self.center_cut is None) and self.zoom_cut is None
and self.center_cut is None
)
def determine_task_type(self) -> TaskType: def determine_task_type(self) -> TaskType:
"""自动确定任务类型""" """自动确定任务类型"""

View File

@@ -18,7 +18,7 @@ register_default_services()
template_service = get_template_service() template_service = get_template_service()
# Check for redownload parameter # Check for redownload parameter
if 'redownload' in sys.argv: if "redownload" in sys.argv:
print("Redownloading all templates...") print("Redownloading all templates...")
try: try:
for template_name in template_service.templates.keys(): for template_name in template_service.templates.keys():
@@ -35,12 +35,13 @@ import logging
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
init_opentelemetry() init_opentelemetry()
def cleanup_temp_files(): def cleanup_temp_files():
"""清理临时文件 - 异步执行避免阻塞主循环""" """清理临时文件 - 异步执行避免阻塞主循环"""
import threading import threading
def _cleanup(): def _cleanup():
for file_globs in ['*.mp4', '*.ts', 'tmp_concat*.txt']: for file_globs in ["*.mp4", "*.ts", "tmp_concat*.txt"]:
for file_path in glob.glob(file_globs): for file_path in glob.glob(file_globs):
try: try:
if os.path.exists(file_path): if os.path.exists(file_path):
@@ -52,6 +53,7 @@ def cleanup_temp_files():
# 在后台线程中执行清理 # 在后台线程中执行清理
threading.Thread(target=_cleanup, daemon=True).start() threading.Thread(target=_cleanup, daemon=True).start()
def main_loop(): def main_loop():
"""主处理循环""" """主处理循环"""
while True: while True:
@@ -83,6 +85,7 @@ def main_loop():
LOGGER.error("Unexpected error in main loop", exc_info=e) LOGGER.error("Unexpected error in main loop", exc_info=e)
sleep(5) # 避免快速循环消耗CPU sleep(5) # 避免快速循环消耗CPU
if __name__ == "__main__": if __name__ == "__main__":
try: try:
main_loop() main_loop()

View File

@@ -2,21 +2,25 @@ from .render_service import RenderService, DefaultRenderService
from .task_service import TaskService, DefaultTaskService from .task_service import TaskService, DefaultTaskService
from .template_service import TemplateService, DefaultTemplateService from .template_service import TemplateService, DefaultTemplateService
from .service_container import ( from .service_container import (
ServiceContainer, get_container, register_default_services, ServiceContainer,
get_render_service, get_template_service, get_task_service get_container,
register_default_services,
get_render_service,
get_template_service,
get_task_service,
) )
__all__ = [ __all__ = [
'RenderService', "RenderService",
'DefaultRenderService', "DefaultRenderService",
'TaskService', "TaskService",
'DefaultTaskService', "DefaultTaskService",
'TemplateService', "TemplateService",
'DefaultTemplateService', "DefaultTemplateService",
'ServiceContainer', "ServiceContainer",
'get_container', "get_container",
'register_default_services', "register_default_services",
'get_render_service', "get_render_service",
'get_template_service', "get_template_service",
'get_task_service' "get_task_service",
] ]

View File

@@ -9,18 +9,24 @@ from opentelemetry.trace import Status, StatusCode
from entity.render_task import RenderTask from entity.render_task import RenderTask
from entity.ffmpeg_command_builder import FFmpegCommandBuilder from entity.ffmpeg_command_builder import FFmpegCommandBuilder
from util.exceptions import RenderError, FFmpegError from util.exceptions import RenderError, FFmpegError
from util.ffmpeg import probe_video_info, fade_out_audio, handle_ffmpeg_output, subprocess_args from util.ffmpeg import (
probe_video_info,
fade_out_audio,
handle_ffmpeg_output,
subprocess_args,
)
from telemetry import get_tracer from telemetry import get_tracer
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# 向后兼容层 - 处理旧的FfmpegTask对象 # 向后兼容层 - 处理旧的FfmpegTask对象
class RenderService(ABC): class RenderService(ABC):
"""渲染服务抽象接口""" """渲染服务抽象接口"""
@abstractmethod @abstractmethod
def render(self, task: Union[RenderTask, 'FfmpegTask']) -> bool: def render(self, task: Union[RenderTask, "FfmpegTask"]) -> bool:
""" """
执行渲染任务 执行渲染任务
@@ -46,7 +52,9 @@ class RenderService(ABC):
pass pass
@abstractmethod @abstractmethod
def fade_out_audio(self, file_path: str, duration: float, fade_seconds: float = 2.0) -> str: def fade_out_audio(
self, file_path: str, duration: float, fade_seconds: float = 2.0
) -> str:
""" """
音频淡出处理 音频淡出处理
@@ -60,13 +68,14 @@ class RenderService(ABC):
""" """
pass pass
class DefaultRenderService(RenderService): class DefaultRenderService(RenderService):
"""默认渲染服务实现""" """默认渲染服务实现"""
def render(self, task: Union[RenderTask, 'FfmpegTask']) -> bool: def render(self, task: Union[RenderTask, "FfmpegTask"]) -> bool:
"""执行渲染任务""" """执行渲染任务"""
# 兼容旧的FfmpegTask # 兼容旧的FfmpegTask
if hasattr(task, 'get_ffmpeg_args'): # 这是FfmpegTask if hasattr(task, "get_ffmpeg_args"): # 这是FfmpegTask
# 使用旧的方式执行 # 使用旧的方式执行
return self._render_legacy_ffmpeg_task(task) return self._render_legacy_ffmpeg_task(task)
@@ -113,9 +122,7 @@ class DefaultRenderService(RenderService):
try: try:
# 执行FFmpeg进程 (使用构建器已经包含的参数) # 执行FFmpeg进程 (使用构建器已经包含的参数)
process = subprocess.run( process = subprocess.run(
args, args, stderr=subprocess.PIPE, **subprocess_args(True)
stderr=subprocess.PIPE,
**subprocess_args(True)
) )
span.set_attribute("ffmpeg.return_code", process.returncode) span.set_attribute("ffmpeg.return_code", process.returncode)
@@ -128,15 +135,21 @@ class DefaultRenderService(RenderService):
# 检查返回码 # 检查返回码
if process.returncode != 0: if process.returncode != 0:
error_msg = process.stderr.decode() if process.stderr else "Unknown error" error_msg = (
process.stderr.decode() if process.stderr else "Unknown error"
)
span.set_attribute("ffmpeg.error", error_msg) span.set_attribute("ffmpeg.error", error_msg)
span.set_status(Status(StatusCode.ERROR)) span.set_status(Status(StatusCode.ERROR))
logger.error("FFmpeg failed with return code %d: %s", process.returncode, error_msg) logger.error(
"FFmpeg failed with return code %d: %s",
process.returncode,
error_msg,
)
raise FFmpegError( raise FFmpegError(
f"FFmpeg execution failed", f"FFmpeg execution failed",
command=args, command=args,
return_code=process.returncode, return_code=process.returncode,
stderr=error_msg stderr=error_msg,
) )
# 检查输出文件 # 检查输出文件
@@ -166,7 +179,9 @@ class DefaultRenderService(RenderService):
"""获取视频信息""" """获取视频信息"""
return probe_video_info(file_path) return probe_video_info(file_path)
def fade_out_audio(self, file_path: str, duration: float, fade_seconds: float = 2.0) -> str: def fade_out_audio(
self, file_path: str, duration: float, fade_seconds: float = 2.0
) -> str:
"""音频淡出处理""" """音频淡出处理"""
return fade_out_audio(file_path, duration, fade_seconds) return fade_out_audio(file_path, duration, fade_seconds)

View File

@@ -1,13 +1,15 @@
""" """
服务容器模块 - 提供线程安全的服务实例管理 服务容器模块 - 提供线程安全的服务实例管理
""" """
import threading import threading
from typing import Dict, Type, TypeVar, Optional from typing import Dict, Type, TypeVar, Optional
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
T = TypeVar('T') T = TypeVar("T")
class ServiceContainer: class ServiceContainer:
"""线程安全的服务容器,实现依赖注入和单例管理""" """线程安全的服务容器,实现依赖注入和单例管理"""
@@ -32,7 +34,9 @@ class ServiceContainer:
# 检查是否有工厂方法 # 检查是否有工厂方法
if service_type not in self._factories: if service_type not in self._factories:
raise ValueError(f"No factory registered for service type: {service_type}") raise ValueError(
f"No factory registered for service type: {service_type}"
)
# 创建新实例 # 创建新实例
factory = self._factories[service_type] factory = self._factories[service_type]
@@ -42,7 +46,9 @@ class ServiceContainer:
logger.debug(f"Created new instance of {service_type.__name__}") logger.debug(f"Created new instance of {service_type.__name__}")
return instance return instance
except Exception as e: except Exception as e:
logger.error(f"Failed to create instance of {service_type.__name__}: {e}") logger.error(
f"Failed to create instance of {service_type.__name__}: {e}"
)
raise raise
def has_service(self, service_type: Type[T]) -> bool: def has_service(self, service_type: Type[T]) -> bool:
@@ -60,10 +66,12 @@ class ServiceContainer:
self._services.clear() self._services.clear()
logger.debug("Cleared all service cache") logger.debug("Cleared all service cache")
# 全局服务容器实例 # 全局服务容器实例
_container: Optional[ServiceContainer] = None _container: Optional[ServiceContainer] = None
_container_lock = threading.Lock() _container_lock = threading.Lock()
def get_container() -> ServiceContainer: def get_container() -> ServiceContainer:
"""获取全局服务容器实例""" """获取全局服务容器实例"""
global _container global _container
@@ -73,6 +81,7 @@ def get_container() -> ServiceContainer:
_container = ServiceContainer() _container = ServiceContainer()
return _container return _container
def register_default_services(): def register_default_services():
"""注册默认的服务实现""" """注册默认的服务实现"""
from .render_service import DefaultRenderService, RenderService from .render_service import DefaultRenderService, RenderService
@@ -89,6 +98,7 @@ def register_default_services():
service = DefaultTemplateService() service = DefaultTemplateService()
service.load_local_templates() service.load_local_templates()
return service return service
container.register_singleton(TemplateService, create_template_service) container.register_singleton(TemplateService, create_template_service)
# 注册任务服务(依赖其他服务) # 注册任务服务(依赖其他服务)
@@ -96,22 +106,29 @@ def register_default_services():
render_service = container.get_service(RenderService) render_service = container.get_service(RenderService)
template_service = container.get_service(TemplateService) template_service = container.get_service(TemplateService)
return DefaultTaskService(render_service, template_service) return DefaultTaskService(render_service, template_service)
container.register_singleton(TaskService, create_task_service) container.register_singleton(TaskService, create_task_service)
logger.info("Default services registered successfully") logger.info("Default services registered successfully")
# 便捷函数 # 便捷函数
def get_render_service() -> 'RenderService': def get_render_service() -> "RenderService":
"""获取渲染服务实例""" """获取渲染服务实例"""
from .render_service import RenderService from .render_service import RenderService
return get_container().get_service(RenderService) return get_container().get_service(RenderService)
def get_template_service() -> 'TemplateService':
def get_template_service() -> "TemplateService":
"""获取模板服务实例""" """获取模板服务实例"""
from .template_service import TemplateService from .template_service import TemplateService
return get_container().get_service(TemplateService) return get_container().get_service(TemplateService)
def get_task_service() -> 'TaskService':
def get_task_service() -> "TaskService":
"""获取任务服务实例""" """获取任务服务实例"""
from .task_service import TaskService from .task_service import TaskService
return get_container().get_service(TaskService) return get_container().get_service(TaskService)

View File

@@ -16,6 +16,7 @@ from telemetry import get_tracer
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class TaskService(ABC): class TaskService(ABC):
"""任务服务抽象接口""" """任务服务抽象接口"""
@@ -33,7 +34,9 @@ class TaskService(ABC):
pass pass
@abstractmethod @abstractmethod
def create_render_task(self, task_info: Dict[str, Any], template_info: Dict[str, Any]) -> RenderTask: def create_render_task(
self, task_info: Dict[str, Any], template_info: Dict[str, Any]
) -> RenderTask:
""" """
创建渲染任务 创建渲染任务
@@ -46,10 +49,13 @@ class TaskService(ABC):
""" """
pass pass
class DefaultTaskService(TaskService): class DefaultTaskService(TaskService):
"""默认任务服务实现""" """默认任务服务实现"""
def __init__(self, render_service: RenderService, template_service: TemplateService): def __init__(
self, render_service: RenderService, template_service: TemplateService
):
self.render_service = render_service self.render_service = render_service
self.template_service = template_service self.template_service = template_service
@@ -61,7 +67,9 @@ class DefaultTaskService(TaskService):
# 标准化任务信息 # 标准化任务信息
task_info = api.normalize_task(task_info) task_info = api.normalize_task(task_info)
span.set_attribute("task.id", task_info.get("id", "unknown")) span.set_attribute("task.id", task_info.get("id", "unknown"))
span.set_attribute("task.template_id", task_info.get("templateId", "unknown")) span.set_attribute(
"task.template_id", task_info.get("templateId", "unknown")
)
# 获取模板信息 # 获取模板信息
template_id = task_info.get("templateId") template_id = task_info.get("templateId")
@@ -83,19 +91,24 @@ class DefaultTaskService(TaskService):
return False return False
# 获取视频信息 # 获取视频信息
width, height, duration = self.render_service.get_video_info(render_task.output_file) width, height, duration = self.render_service.get_video_info(
render_task.output_file
)
span.set_attribute("video.width", width) span.set_attribute("video.width", width)
span.set_attribute("video.height", height) span.set_attribute("video.height", height)
span.set_attribute("video.duration", duration) span.set_attribute("video.duration", duration)
# 音频淡出 # 音频淡出
new_file = self.render_service.fade_out_audio(render_task.output_file, duration) new_file = self.render_service.fade_out_audio(
render_task.output_file, duration
)
render_task.output_file = new_file render_task.output_file = new_file
# 上传文件 - 创建一个兼容对象 # 上传文件 - 创建一个兼容对象
class TaskCompat: class TaskCompat:
def __init__(self, output_file): def __init__(self, output_file):
self.output_file = output_file self.output_file = output_file
def get_output_file(self): def get_output_file(self):
return self.output_file return self.output_file
@@ -110,11 +123,10 @@ class DefaultTaskService(TaskService):
self._cleanup_temp_files(render_task) self._cleanup_temp_files(render_task)
# 报告任务成功 # 报告任务成功
api.report_task_success(task_info, videoInfo={ api.report_task_success(
"width": width, task_info,
"height": height, videoInfo={"width": width, "height": height, "duration": duration},
"duration": duration )
})
span.set_status(Status(StatusCode.OK)) span.set_status(Status(StatusCode.OK))
return True return True
@@ -125,7 +137,9 @@ class DefaultTaskService(TaskService):
api.report_task_failed(task_info, str(e)) api.report_task_failed(task_info, str(e))
return False return False
def create_render_task(self, task_info: Dict[str, Any], template_info: Dict[str, Any]) -> RenderTask: def create_render_task(
self, task_info: Dict[str, Any], template_info: Dict[str, Any]
) -> RenderTask:
"""创建渲染任务""" """创建渲染任务"""
tracer = get_tracer(__name__) tracer = get_tracer(__name__)
with tracer.start_as_current_span("create_render_task") as span: with tracer.start_as_current_span("create_render_task") as span:
@@ -148,19 +162,27 @@ class DefaultTaskService(TaskService):
for part in template_info.get("video_parts", []): for part in template_info.get("video_parts", []):
source, ext_data = self._parse_video_source( source, ext_data = self._parse_video_source(
part.get('source'), task_params, template_info part.get("source"), task_params, template_info
) )
if not source: if not source:
logger.warning("No video found for part: %s", part) logger.warning("No video found for part: %s", part)
continue continue
# 检查only_if条件 # 检查only_if条件
only_if = part.get('only_if', '') only_if = part.get("only_if", "")
if only_if: if only_if:
only_if_usage_count[only_if] = only_if_usage_count.get(only_if, 0) + 1 only_if_usage_count[only_if] = (
only_if_usage_count.get(only_if, 0) + 1
)
required_count = only_if_usage_count[only_if] required_count = only_if_usage_count[only_if]
if not self._check_placeholder_exist_with_count(only_if, task_params_orig, required_count): if not self._check_placeholder_exist_with_count(
logger.info("Skipping part due to only_if condition: %s (need %d)", only_if, required_count) only_if, task_params_orig, required_count
):
logger.info(
"Skipping part due to only_if condition: %s (need %d)",
only_if,
required_count,
)
continue continue
# 创建子任务 # 创建子任务
@@ -175,7 +197,7 @@ class DefaultTaskService(TaskService):
resolution=template_info.get("video_size", ""), resolution=template_info.get("video_size", ""),
frame_rate=template_info.get("frame_rate", 25), frame_rate=template_info.get("frame_rate", 25),
center_cut=template_info.get("crop_mode"), center_cut=template_info.get("crop_mode"),
zoom_cut=template_info.get("zoom_cut") zoom_cut=template_info.get("zoom_cut"),
) )
# 应用整体模板设置 # 应用整体模板设置
@@ -193,6 +215,7 @@ class DefaultTaskService(TaskService):
def _download_resources(self, task_params: Dict[str, Any]): def _download_resources(self, task_params: Dict[str, Any]):
"""并行下载资源""" """并行下载资源"""
from config.settings import get_ffmpeg_config from config.settings import get_ffmpeg_config
config = get_ffmpeg_config() config = get_ffmpeg_config()
download_futures = [] download_futures = []
@@ -204,7 +227,9 @@ class DefaultTaskService(TaskService):
url = param.get("url", "") url = param.get("url", "")
if url.startswith("http"): if url.startswith("http"):
_, filename = os.path.split(url) _, filename = os.path.split(url)
future = executor.submit(oss.download_from_oss, url, filename, True) future = executor.submit(
oss.download_from_oss, url, filename, True
)
download_futures.append((future, url, filename)) download_futures.append((future, url, filename))
# 等待所有下载完成,并记录失败的下载 # 等待所有下载完成,并记录失败的下载
@@ -219,13 +244,16 @@ class DefaultTaskService(TaskService):
failed_downloads.append((url, filename)) failed_downloads.append((url, filename))
if failed_downloads: if failed_downloads:
logger.warning(f"Failed to download {len(failed_downloads)} resources: {[f[1] for f in failed_downloads]}") logger.warning(
f"Failed to download {len(failed_downloads)} resources: {[f[1] for f in failed_downloads]}"
)
def _parse_video_source(self, source: str, task_params: Dict[str, Any], def _parse_video_source(
template_info: Dict[str, Any]) -> tuple[Optional[str], Dict[str, Any]]: self, source: str, task_params: Dict[str, Any], template_info: Dict[str, Any]
) -> tuple[Optional[str], Dict[str, Any]]:
"""解析视频源""" """解析视频源"""
if source.startswith('PLACEHOLDER_'): if source.startswith("PLACEHOLDER_"):
placeholder_id = source.replace('PLACEHOLDER_', '') placeholder_id = source.replace("PLACEHOLDER_", "")
new_sources = task_params.get(placeholder_id, []) new_sources = task_params.get(placeholder_id, [])
pick_source = {} pick_source = {}
@@ -245,8 +273,9 @@ class DefaultTaskService(TaskService):
return os.path.join(template_info.get("local_path", ""), source), {} return os.path.join(template_info.get("local_path", ""), source), {}
def _check_placeholder_exist_with_count(self, placeholder_id: str, task_params: Dict[str, Any], def _check_placeholder_exist_with_count(
required_count: int = 1) -> bool: self, placeholder_id: str, task_params: Dict[str, Any], required_count: int = 1
) -> bool:
"""检查占位符是否存在足够数量的片段""" """检查占位符是否存在足够数量的片段"""
if placeholder_id in task_params: if placeholder_id in task_params:
new_sources = task_params.get(placeholder_id, []) new_sources = task_params.get(placeholder_id, [])
@@ -255,8 +284,13 @@ class DefaultTaskService(TaskService):
return required_count <= 1 return required_count <= 1
return False return False
def _create_sub_task(self, part: Dict[str, Any], source: str, ext_data: Dict[str, Any], def _create_sub_task(
template_info: Dict[str, Any]) -> RenderTask: self,
part: Dict[str, Any],
source: str,
ext_data: Dict[str, Any],
template_info: Dict[str, Any],
) -> RenderTask:
"""创建子任务""" """创建子任务"""
sub_task = RenderTask( sub_task = RenderTask(
input_files=[source], input_files=[source],
@@ -265,7 +299,7 @@ class DefaultTaskService(TaskService):
annexb=True, annexb=True,
center_cut=part.get("crop_mode"), center_cut=part.get("crop_mode"),
zoom_cut=part.get("zoom_cut"), zoom_cut=part.get("zoom_cut"),
ext_data=ext_data ext_data=ext_data,
) )
# 应用部分模板设置 # 应用部分模板设置
@@ -273,25 +307,29 @@ class DefaultTaskService(TaskService):
return sub_task return sub_task
def _apply_template_settings(self, task: RenderTask, template_part: Dict[str, Any], def _apply_template_settings(
template_info: Dict[str, Any]): self,
task: RenderTask,
template_part: Dict[str, Any],
template_info: Dict[str, Any],
):
"""应用模板设置到任务""" """应用模板设置到任务"""
# 添加效果 # 添加效果
for effect in template_part.get('effects', []): for effect in template_part.get("effects", []):
task.add_effect(effect) task.add_effect(effect)
# 添加LUT # 添加LUT
for lut in template_part.get('luts', []): for lut in template_part.get("luts", []):
full_path = os.path.join(template_info.get("local_path", ""), lut) full_path = os.path.join(template_info.get("local_path", ""), lut)
task.add_lut(full_path.replace("\\", "/")) task.add_lut(full_path.replace("\\", "/"))
# 添加音频 # 添加音频
for audio in template_part.get('audios', []): for audio in template_part.get("audios", []):
full_path = os.path.join(template_info.get("local_path", ""), audio) full_path = os.path.join(template_info.get("local_path", ""), audio)
task.add_audios(full_path) task.add_audios(full_path)
# 添加覆盖层 # 添加覆盖层
for overlay in template_part.get('overlays', []): for overlay in template_part.get("overlays", []):
full_path = os.path.join(template_info.get("local_path", ""), overlay) full_path = os.path.join(template_info.get("local_path", ""), overlay)
task.add_overlay(full_path) task.add_overlay(full_path)

View File

@@ -6,13 +6,18 @@ from typing import Dict, Any, Optional
from opentelemetry.trace import Status, StatusCode from opentelemetry.trace import Status, StatusCode
from util.exceptions import TemplateError, TemplateNotFoundError, TemplateValidationError from util.exceptions import (
TemplateError,
TemplateNotFoundError,
TemplateValidationError,
)
from util import api, oss from util import api, oss
from config.settings import get_storage_config from config.settings import get_storage_config
from telemetry import get_tracer from telemetry import get_tracer
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class TemplateService(ABC): class TemplateService(ABC):
"""模板服务抽象接口""" """模板服务抽象接口"""
@@ -60,6 +65,7 @@ class TemplateService(ABC):
""" """
pass pass
class DefaultTemplateService(TemplateService): class DefaultTemplateService(TemplateService):
"""默认模板服务实现""" """默认模板服务实现"""
@@ -106,26 +112,28 @@ class DefaultTemplateService(TemplateService):
logger.warning("Failed to get template info: %s", template_id) logger.warning("Failed to get template info: %s", template_id)
return False return False
local_path = template_info.get('local_path') local_path = template_info.get("local_path")
if not local_path: if not local_path:
local_path = os.path.join(self.storage_config.template_dir, str(template_id)) local_path = os.path.join(
template_info['local_path'] = local_path self.storage_config.template_dir, str(template_id)
)
template_info["local_path"] = local_path
# 创建本地目录 # 创建本地目录
if not os.path.isdir(local_path): if not os.path.isdir(local_path):
os.makedirs(local_path) os.makedirs(local_path)
# 下载模板资源 # 下载模板资源
overall_template = template_info.get('overall_template', {}) overall_template = template_info.get("overall_template", {})
video_parts = template_info.get('video_parts', []) video_parts = template_info.get("video_parts", [])
self._download_template_assets(overall_template, template_info) self._download_template_assets(overall_template, template_info)
for video_part in video_parts: for video_part in video_parts:
self._download_template_assets(video_part, template_info) self._download_template_assets(video_part, template_info)
# 保存模板定义文件 # 保存模板定义文件
template_file = os.path.join(local_path, 'template.json') template_file = os.path.join(local_path, "template.json")
with open(template_file, 'w', encoding='utf-8') as f: with open(template_file, "w", encoding="utf-8") as f:
json.dump(template_info, f, ensure_ascii=False, indent=2) json.dump(template_info, f, ensure_ascii=False, indent=2)
# 加载到内存 # 加载到内存
@@ -169,10 +177,12 @@ class DefaultTemplateService(TemplateService):
template_def_file = os.path.join(local_path, "template.json") template_def_file = os.path.join(local_path, "template.json")
if not os.path.exists(template_def_file): if not os.path.exists(template_def_file):
raise TemplateNotFoundError(f"Template definition file not found: {template_def_file}") raise TemplateNotFoundError(
f"Template definition file not found: {template_def_file}"
)
try: try:
with open(template_def_file, 'r', encoding='utf-8') as f: with open(template_def_file, "r", encoding="utf-8") as f:
template_info = json.load(f) template_info = json.load(f)
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
raise TemplateError(f"Invalid template JSON: {e}") raise TemplateError(f"Invalid template JSON: {e}")
@@ -184,7 +194,11 @@ class DefaultTemplateService(TemplateService):
self.templates[template_name] = template_info self.templates[template_name] = template_info
logger.info("Template loaded successfully: %s", template_name) logger.info("Template loaded successfully: %s", template_name)
except TemplateValidationError as e: except TemplateValidationError as e:
logger.error("Template validation failed for %s: %s. Attempting to re-download.", template_name, e) logger.error(
"Template validation failed for %s: %s. Attempting to re-download.",
template_name,
e,
)
# 模板验证失败,尝试重新下载 # 模板验证失败,尝试重新下载
if self.download_template(template_name): if self.download_template(template_name):
logger.info("Template re-downloaded successfully: %s", template_name) logger.info("Template re-downloaded successfully: %s", template_name)
@@ -192,13 +206,15 @@ class DefaultTemplateService(TemplateService):
logger.error("Failed to re-download template: %s", template_name) logger.error("Failed to re-download template: %s", template_name)
raise raise
def _download_template_assets(self, template_part: Dict[str, Any], template_info: Dict[str, Any]): def _download_template_assets(
self, template_part: Dict[str, Any], template_info: Dict[str, Any]
):
"""下载模板资源""" """下载模板资源"""
local_path = template_info['local_path'] local_path = template_info["local_path"]
# 下载源文件 # 下载源文件
if 'source' in template_part: if "source" in template_part:
source = template_part['source'] source = template_part["source"]
if isinstance(source, str) and source.startswith("http"): if isinstance(source, str) and source.startswith("http"):
_, filename = os.path.split(source) _, filename = os.path.split(source)
new_file_path = os.path.join(local_path, filename) new_file_path = os.path.join(local_path, filename)
@@ -206,39 +222,44 @@ class DefaultTemplateService(TemplateService):
if filename.endswith(".mp4"): if filename.endswith(".mp4"):
from util.ffmpeg import re_encode_and_annexb from util.ffmpeg import re_encode_and_annexb
new_file_path = re_encode_and_annexb(new_file_path) new_file_path = re_encode_and_annexb(new_file_path)
template_part['source'] = os.path.relpath(new_file_path, local_path) template_part["source"] = os.path.relpath(new_file_path, local_path)
# 下载覆盖层 # 下载覆盖层
if 'overlays' in template_part: if "overlays" in template_part:
for i, overlay in enumerate(template_part['overlays']): for i, overlay in enumerate(template_part["overlays"]):
if isinstance(overlay, str) and overlay.startswith("http"): if isinstance(overlay, str) and overlay.startswith("http"):
_, filename = os.path.split(overlay) _, filename = os.path.split(overlay)
oss.download_from_oss(overlay, os.path.join(local_path, filename)) oss.download_from_oss(overlay, os.path.join(local_path, filename))
template_part['overlays'][i] = filename template_part["overlays"][i] = filename
# 下载LUT # 下载LUT
if 'luts' in template_part: if "luts" in template_part:
for i, lut in enumerate(template_part['luts']): for i, lut in enumerate(template_part["luts"]):
if isinstance(lut, str) and lut.startswith("http"): if isinstance(lut, str) and lut.startswith("http"):
_, filename = os.path.split(lut) _, filename = os.path.split(lut)
oss.download_from_oss(lut, os.path.join(local_path, filename)) oss.download_from_oss(lut, os.path.join(local_path, filename))
template_part['luts'][i] = filename template_part["luts"][i] = filename
# 下载音频 # 下载音频
if 'audios' in template_part: if "audios" in template_part:
for i, audio in enumerate(template_part['audios']): for i, audio in enumerate(template_part["audios"]):
if isinstance(audio, str) and audio.startswith("http"): if isinstance(audio, str) and audio.startswith("http"):
_, filename = os.path.split(audio) _, filename = os.path.split(audio)
oss.download_from_oss(audio, os.path.join(local_path, filename)) oss.download_from_oss(audio, os.path.join(local_path, filename))
template_part['audios'][i] = filename template_part["audios"][i] = filename
def _validate_template_part(self, template_part: Dict[str, Any], base_dir: str): def _validate_template_part(self, template_part: Dict[str, Any], base_dir: str):
"""验证模板部分""" """验证模板部分"""
# 验证源文件 # 验证源文件
source_file = template_part.get("source", "") source_file = template_part.get("source", "")
if source_file and not source_file.startswith("http") and not source_file.startswith("PLACEHOLDER_"): if (
source_file
and not source_file.startswith("http")
and not source_file.startswith("PLACEHOLDER_")
):
if not os.path.isabs(source_file): if not os.path.isabs(source_file):
source_file = os.path.join(base_dir, source_file) source_file = os.path.join(base_dir, source_file)
if not os.path.exists(source_file): if not os.path.exists(source_file):

View File

@@ -2,36 +2,54 @@ import os
from constant import SOFTWARE_VERSION from constant import SOFTWARE_VERSION
from opentelemetry import trace from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter as OTLPSpanHttpExporter from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
from opentelemetry.sdk.resources import DEPLOYMENT_ENVIRONMENT, HOST_NAME, Resource, SERVICE_NAME, SERVICE_VERSION OTLPSpanExporter as OTLPSpanHttpExporter,
)
from opentelemetry.sdk.resources import (
DEPLOYMENT_ENVIRONMENT,
HOST_NAME,
Resource,
SERVICE_NAME,
SERVICE_VERSION,
)
from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor, SimpleSpanProcessor from opentelemetry.sdk.trace.export import BatchSpanProcessor, SimpleSpanProcessor
from opentelemetry.instrumentation.threading import ThreadingInstrumentor from opentelemetry.instrumentation.threading import ThreadingInstrumentor
ThreadingInstrumentor().instrument() ThreadingInstrumentor().instrument()
def get_tracer(name): def get_tracer(name):
return trace.get_tracer(name) return trace.get_tracer(name)
# 初始化 OpenTelemetry # 初始化 OpenTelemetry
def init_opentelemetry(batch=True): def init_opentelemetry(batch=True):
# 设置服务名、主机名 # 设置服务名、主机名
resource = Resource(attributes={ resource = Resource(
attributes={
SERVICE_NAME: "RENDER_WORKER", SERVICE_NAME: "RENDER_WORKER",
SERVICE_VERSION: SOFTWARE_VERSION, SERVICE_VERSION: SOFTWARE_VERSION,
DEPLOYMENT_ENVIRONMENT: "Python", DEPLOYMENT_ENVIRONMENT: "Python",
HOST_NAME: os.getenv("ACCESS_KEY"), HOST_NAME: os.getenv("ACCESS_KEY"),
}) }
)
# 使用HTTP协议上报 # 使用HTTP协议上报
if batch: if batch:
span_processor = BatchSpanProcessor(OTLPSpanHttpExporter( span_processor = BatchSpanProcessor(
OTLPSpanHttpExporter(
endpoint="https://oltp.jerryyan.top/v1/traces", endpoint="https://oltp.jerryyan.top/v1/traces",
)) )
)
else: else:
span_processor = SimpleSpanProcessor(OTLPSpanHttpExporter( span_processor = SimpleSpanProcessor(
OTLPSpanHttpExporter(
endpoint="https://oltp.jerryyan.top/v1/traces", endpoint="https://oltp.jerryyan.top/v1/traces",
)) )
)
trace_provider = TracerProvider(resource=resource, active_span_processor=span_processor) trace_provider = TracerProvider(
resource=resource, active_span_processor=span_processor
)
trace.set_tracer_provider(trace_provider) trace.set_tracer_provider(trace_provider)

View File

@@ -21,15 +21,11 @@ retry_strategy = Retry(
total=3, total=3,
status_forcelist=[429, 500, 502, 503, 504], status_forcelist=[429, 500, 502, 503, 504],
backoff_factor=1, backoff_factor=1,
respect_retry_after_header=True respect_retry_after_header=True,
) )
# 配置HTTP适配器(连接池) # 配置HTTP适配器(连接池)
adapter = HTTPAdapter( adapter = HTTPAdapter(pool_connections=10, pool_maxsize=20, max_retries=retry_strategy)
pool_connections=10,
pool_maxsize=20,
max_retries=retry_strategy
)
session.mount("http://", adapter) session.mount("http://", adapter)
session.mount("https://", adapter) session.mount("https://", adapter)
@@ -51,23 +47,30 @@ def sync_center():
:return: 任务列表 :return: 任务列表
""" """
from services import DefaultTemplateService from services import DefaultTemplateService
template_service = DefaultTemplateService() template_service = DefaultTemplateService()
try: try:
response = session.post(os.getenv('API_ENDPOINT') + "/sync", json={ response = session.post(
'accessKey': os.getenv('ACCESS_KEY'), os.getenv("API_ENDPOINT") + "/sync",
'clientStatus': util.system.get_sys_info(), json={
'templateList': [{'id': t.get('id', ''), 'updateTime': t.get('updateTime', '')} for t in "accessKey": os.getenv("ACCESS_KEY"),
template_service.templates.values()] "clientStatus": util.system.get_sys_info(),
}, timeout=10) "templateList": [
{"id": t.get("id", ""), "updateTime": t.get("updateTime", "")}
for t in template_service.templates.values()
],
},
timeout=10,
)
response.raise_for_status() response.raise_for_status()
except requests.RequestException as e: except requests.RequestException as e:
logger.error("请求失败!", e) logger.error("请求失败!", e)
return [] return []
data = response.json() data = response.json()
logger.debug("获取任务结果:【%s", data) logger.debug("获取任务结果:【%s", data)
if data.get('code', 0) == 200: if data.get("code", 0) == 200:
templates = data.get('data', {}).get('templates', []) templates = data.get("data", {}).get("templates", [])
tasks = data.get('data', {}).get('tasks', []) tasks = data.get("data", {}).get("tasks", [])
else: else:
tasks = [] tasks = []
templates = [] templates = []
@@ -75,12 +78,16 @@ def sync_center():
if os.getenv("REDIRECT_TO_URL", False) != False: if os.getenv("REDIRECT_TO_URL", False) != False:
for task in tasks: for task in tasks:
_sess = requests.Session() _sess = requests.Session()
logger.info("重定向任务【%s】至配置的地址:%s", task.get("id"), os.getenv("REDIRECT_TO_URL")) logger.info(
"重定向任务【%s】至配置的地址:%s",
task.get("id"),
os.getenv("REDIRECT_TO_URL"),
)
url = f"{os.getenv('REDIRECT_TO_URL')}{task.get('id')}" url = f"{os.getenv('REDIRECT_TO_URL')}{task.get('id')}"
threading.Thread(target=requests.post, args=(url,)).start() threading.Thread(target=requests.post, args=(url,)).start()
return [] return []
for template in templates: for template in templates:
template_id = template.get('id', '') template_id = template.get("id", "")
if template_id: if template_id:
logger.info("更新模板:【%s", template_id) logger.info("更新模板:【%s", template_id)
template_service.download_template(template_id) template_service.download_template(template_id)
@@ -100,10 +107,17 @@ def get_template_info(template_id):
with tracer.start_as_current_span("get_template_info.request") as req_span: with tracer.start_as_current_span("get_template_info.request") as req_span:
try: try:
req_span.set_attribute("http.method", "POST") req_span.set_attribute("http.method", "POST")
req_span.set_attribute("http.url", '{0}/template/{1}'.format(os.getenv('API_ENDPOINT'), template_id)) req_span.set_attribute(
response = session.post('{0}/template/{1}'.format(os.getenv('API_ENDPOINT'), template_id), json={ "http.url",
'accessKey': os.getenv('ACCESS_KEY'), "{0}/template/{1}".format(os.getenv("API_ENDPOINT"), template_id),
}, timeout=10) )
response = session.post(
"{0}/template/{1}".format(os.getenv("API_ENDPOINT"), template_id),
json={
"accessKey": os.getenv("ACCESS_KEY"),
},
timeout=10,
)
req_span.set_attribute("http.status_code", response.status_code) req_span.set_attribute("http.status_code", response.status_code)
req_span.set_attribute("http.response", response.text) req_span.set_attribute("http.response", response.text)
response.raise_for_status() response.raise_for_status()
@@ -113,64 +127,68 @@ def get_template_info(template_id):
return None return None
data = response.json() data = response.json()
logger.debug("获取模板信息结果:【%s", data) logger.debug("获取模板信息结果:【%s", data)
remote_template_info = data.get('data', {}) remote_template_info = data.get("data", {})
if not remote_template_info: if not remote_template_info:
logger.warning("获取模板信息结果为空", data) logger.warning("获取模板信息结果为空", data)
return None return None
template = { template = {
'id': template_id, "id": template_id,
'updateTime': remote_template_info.get('updateTime', template_id), "updateTime": remote_template_info.get("updateTime", template_id),
'scenic_name': remote_template_info.get('scenicName', '景区'), "scenic_name": remote_template_info.get("scenicName", "景区"),
'name': remote_template_info.get('name', '模版'), "name": remote_template_info.get("name", "模版"),
'video_size': remote_template_info.get('resolution', '1920x1080'), "video_size": remote_template_info.get("resolution", "1920x1080"),
'frame_rate': 25, "frame_rate": 25,
'overall_duration': 30, "overall_duration": 30,
'video_parts': [ "video_parts": [],
]
} }
def _template_normalizer(template_info): def _template_normalizer(template_info):
_template = {} _template = {}
_placeholder_type = template_info.get('isPlaceholder', -1) _placeholder_type = template_info.get("isPlaceholder", -1)
if _placeholder_type == 0: if _placeholder_type == 0:
# 固定视频 # 固定视频
_template['source'] = template_info.get('sourceUrl', '') _template["source"] = template_info.get("sourceUrl", "")
elif _placeholder_type == 1: elif _placeholder_type == 1:
# 占位符 # 占位符
_template['source'] = "PLACEHOLDER_" + template_info.get('sourceUrl', '') _template["source"] = "PLACEHOLDER_" + template_info.get(
_template['mute'] = template_info.get('mute', True) "sourceUrl", ""
_template['crop_mode'] = template_info.get('cropEnable', None) )
_template['zoom_cut'] = template_info.get('zoomCut', None) _template["mute"] = template_info.get("mute", True)
_template["crop_mode"] = template_info.get("cropEnable", None)
_template["zoom_cut"] = template_info.get("zoomCut", None)
else: else:
_template['source'] = None _template["source"] = None
_overlays = template_info.get('overlays', '') _overlays = template_info.get("overlays", "")
if _overlays: if _overlays:
_template['overlays'] = _overlays.split(",") _template["overlays"] = _overlays.split(",")
_audios = template_info.get('audios', '') _audios = template_info.get("audios", "")
if _audios: if _audios:
_template['audios'] = _audios.split(",") _template["audios"] = _audios.split(",")
_luts = template_info.get('luts', '') _luts = template_info.get("luts", "")
if _luts: if _luts:
_template['luts'] = _luts.split(",") _template["luts"] = _luts.split(",")
_only_if = template_info.get('onlyIf', '') _only_if = template_info.get("onlyIf", "")
if _only_if: if _only_if:
_template['only_if'] = _only_if _template["only_if"] = _only_if
_effects = template_info.get('effects', '') _effects = template_info.get("effects", "")
if _effects: if _effects:
_template['effects'] = _effects.split("|") _template["effects"] = _effects.split("|")
return _template return _template
# outer template definition # outer template definition
overall_template = _template_normalizer(remote_template_info) overall_template = _template_normalizer(remote_template_info)
template['overall_template'] = overall_template template["overall_template"] = overall_template
# inter template definition # inter template definition
inter_template_list = remote_template_info.get('children', []) inter_template_list = remote_template_info.get("children", [])
for children_template in inter_template_list: for children_template in inter_template_list:
parts = _template_normalizer(children_template) parts = _template_normalizer(children_template)
template['video_parts'].append(parts) template["video_parts"].append(parts)
template['local_path'] = os.path.join(os.getenv('TEMPLATE_DIR'), str(template_id)) template["local_path"] = os.path.join(
with get_tracer("api").start_as_current_span("get_template_info.template") as res_span: os.getenv("TEMPLATE_DIR"), str(template_id)
)
with get_tracer("api").start_as_current_span(
"get_template_info.template"
) as res_span:
res_span.set_attribute("normalized.response", json.dumps(template)) res_span.set_attribute("normalized.response", json.dumps(template))
return template return template
@@ -181,12 +199,19 @@ def report_task_success(task_info, **kwargs):
with tracer.start_as_current_span("report_task_success.request") as req_span: with tracer.start_as_current_span("report_task_success.request") as req_span:
try: try:
req_span.set_attribute("http.method", "POST") req_span.set_attribute("http.method", "POST")
req_span.set_attribute("http.url", req_span.set_attribute(
'{0}/{1}/success'.format(os.getenv('API_ENDPOINT'), task_info.get("id"))) "http.url",
response = session.post('{0}/{1}/success'.format(os.getenv('API_ENDPOINT'), task_info.get("id")), json={ "{0}/{1}/success".format(
'accessKey': os.getenv('ACCESS_KEY'), os.getenv("API_ENDPOINT"), task_info.get("id")
**kwargs ),
}, timeout=10) )
response = session.post(
"{0}/{1}/success".format(
os.getenv("API_ENDPOINT"), task_info.get("id")
),
json={"accessKey": os.getenv("ACCESS_KEY"), **kwargs},
timeout=10,
)
req_span.set_attribute("http.status_code", response.status_code) req_span.set_attribute("http.status_code", response.status_code)
req_span.set_attribute("http.response", response.text) req_span.set_attribute("http.response", response.text)
response.raise_for_status() response.raise_for_status()
@@ -203,11 +228,21 @@ def report_task_start(task_info):
with tracer.start_as_current_span("report_task_start.request") as req_span: with tracer.start_as_current_span("report_task_start.request") as req_span:
try: try:
req_span.set_attribute("http.method", "POST") req_span.set_attribute("http.method", "POST")
req_span.set_attribute("http.url", req_span.set_attribute(
'{0}/{1}/start'.format(os.getenv('API_ENDPOINT'), task_info.get("id"))) "http.url",
response = session.post('{0}/{1}/start'.format(os.getenv('API_ENDPOINT'), task_info.get("id")), json={ "{0}/{1}/start".format(
'accessKey': os.getenv('ACCESS_KEY'), os.getenv("API_ENDPOINT"), task_info.get("id")
}, timeout=10) ),
)
response = session.post(
"{0}/{1}/start".format(
os.getenv("API_ENDPOINT"), task_info.get("id")
),
json={
"accessKey": os.getenv("ACCESS_KEY"),
},
timeout=10,
)
req_span.set_attribute("http.status_code", response.status_code) req_span.set_attribute("http.status_code", response.status_code)
req_span.set_attribute("http.response", response.text) req_span.set_attribute("http.response", response.text)
response.raise_for_status() response.raise_for_status()
@@ -218,7 +253,7 @@ def report_task_start(task_info):
return None return None
def report_task_failed(task_info, reason=''): def report_task_failed(task_info, reason=""):
tracer = get_tracer(__name__) tracer = get_tracer(__name__)
with tracer.start_as_current_span("report_task_failed") as span: with tracer.start_as_current_span("report_task_failed") as span:
span.set_attribute("task_id", task_info.get("id")) span.set_attribute("task_id", task_info.get("id"))
@@ -226,12 +261,19 @@ def report_task_failed(task_info, reason=''):
with tracer.start_as_current_span("report_task_failed.request") as req_span: with tracer.start_as_current_span("report_task_failed.request") as req_span:
try: try:
req_span.set_attribute("http.method", "POST") req_span.set_attribute("http.method", "POST")
req_span.set_attribute("http.url", req_span.set_attribute(
'{0}/{1}/fail'.format(os.getenv('API_ENDPOINT'), task_info.get("id"))) "http.url",
response = session.post('{0}/{1}/fail'.format(os.getenv('API_ENDPOINT'), task_info.get("id")), json={ "{0}/{1}/fail".format(
'accessKey': os.getenv('ACCESS_KEY'), os.getenv("API_ENDPOINT"), task_info.get("id")
'reason': reason ),
}, timeout=10) )
response = session.post(
"{0}/{1}/fail".format(
os.getenv("API_ENDPOINT"), task_info.get("id")
),
json={"accessKey": os.getenv("ACCESS_KEY"), "reason": reason},
timeout=10,
)
req_span.set_attribute("http.status_code", response.status_code) req_span.set_attribute("http.status_code", response.status_code)
req_span.set_attribute("http.response", response.text) req_span.set_attribute("http.response", response.text)
response.raise_for_status() response.raise_for_status()
@@ -248,15 +290,26 @@ def upload_task_file(task_info, ffmpeg_task):
with get_tracer("api").start_as_current_span("upload_task_file") as span: with get_tracer("api").start_as_current_span("upload_task_file") as span:
logger.info("开始上传文件: %s", task_info.get("id")) logger.info("开始上传文件: %s", task_info.get("id"))
span.set_attribute("file.id", task_info.get("id")) span.set_attribute("file.id", task_info.get("id"))
with tracer.start_as_current_span("upload_task_file.request_upload_url") as req_span: with tracer.start_as_current_span(
"upload_task_file.request_upload_url"
) as req_span:
try: try:
req_span.set_attribute("http.method", "POST") req_span.set_attribute("http.method", "POST")
req_span.set_attribute("http.url", req_span.set_attribute(
'{0}/{1}/uploadUrl'.format(os.getenv('API_ENDPOINT'), task_info.get("id"))) "http.url",
response = session.post('{0}/{1}/uploadUrl'.format(os.getenv('API_ENDPOINT'), task_info.get("id")), "{0}/{1}/uploadUrl".format(
os.getenv("API_ENDPOINT"), task_info.get("id")
),
)
response = session.post(
"{0}/{1}/uploadUrl".format(
os.getenv("API_ENDPOINT"), task_info.get("id")
),
json={ json={
'accessKey': os.getenv('ACCESS_KEY'), "accessKey": os.getenv("ACCESS_KEY"),
}, timeout=10) },
timeout=10,
)
req_span.set_attribute("http.status_code", response.status_code) req_span.set_attribute("http.status_code", response.status_code)
req_span.set_attribute("http.response", response.text) req_span.set_attribute("http.response", response.text)
response.raise_for_status() response.raise_for_status()
@@ -267,21 +320,25 @@ def upload_task_file(task_info, ffmpeg_task):
logger.error("请求失败!", e) logger.error("请求失败!", e)
return False return False
data = response.json() data = response.json()
url = data.get('data', "") url = data.get("data", "")
logger.info("开始上传文件: %s%s", task_info.get("id"), url) logger.info("开始上传文件: %s%s", task_info.get("id"), url)
return oss.upload_to_oss(url, ffmpeg_task.get_output_file()) return oss.upload_to_oss(url, ffmpeg_task.get_output_file())
def get_task_info(id): def get_task_info(id):
try: try:
response = session.get(os.getenv('API_ENDPOINT') + "/" + id + "/info", params={ response = session.get(
'accessKey': os.getenv('ACCESS_KEY'), os.getenv("API_ENDPOINT") + "/" + id + "/info",
}, timeout=10) params={
"accessKey": os.getenv("ACCESS_KEY"),
},
timeout=10,
)
response.raise_for_status() response.raise_for_status()
except requests.RequestException as e: except requests.RequestException as e:
logger.error("请求失败!", e) logger.error("请求失败!", e)
return [] return []
data = response.json() data = response.json()
logger.debug("获取任务结果:【%s", data) logger.debug("获取任务结果:【%s", data)
if data.get('code', 0) == 200: if data.get("code", 0) == 200:
return data.get('data', {}) return data.get("data", {})

View File

@@ -1,72 +1,111 @@
class RenderWorkerError(Exception): class RenderWorkerError(Exception):
"""RenderWorker基础异常类""" """RenderWorker基础异常类"""
def __init__(self, message: str, error_code: str = None): def __init__(self, message: str, error_code: str = None):
super().__init__(message) super().__init__(message)
self.message = message self.message = message
self.error_code = error_code or self.__class__.__name__ self.error_code = error_code or self.__class__.__name__
class ConfigurationError(RenderWorkerError): class ConfigurationError(RenderWorkerError):
"""配置错误""" """配置错误"""
pass pass
class TemplateError(RenderWorkerError): class TemplateError(RenderWorkerError):
"""模板相关错误""" """模板相关错误"""
pass pass
class TemplateNotFoundError(TemplateError): class TemplateNotFoundError(TemplateError):
"""模板未找到错误""" """模板未找到错误"""
pass pass
class TemplateValidationError(TemplateError): class TemplateValidationError(TemplateError):
"""模板验证错误""" """模板验证错误"""
pass pass
class TaskError(RenderWorkerError): class TaskError(RenderWorkerError):
"""任务处理错误""" """任务处理错误"""
pass pass
class TaskValidationError(TaskError): class TaskValidationError(TaskError):
"""任务参数验证错误""" """任务参数验证错误"""
pass pass
class RenderError(RenderWorkerError): class RenderError(RenderWorkerError):
"""渲染处理错误""" """渲染处理错误"""
pass pass
class FFmpegError(RenderError): class FFmpegError(RenderError):
"""FFmpeg执行错误""" """FFmpeg执行错误"""
def __init__(self, message: str, command: list = None, return_code: int = None, stderr: str = None):
def __init__(
self,
message: str,
command: list = None,
return_code: int = None,
stderr: str = None,
):
super().__init__(message) super().__init__(message)
self.command = command self.command = command
self.return_code = return_code self.return_code = return_code
self.stderr = stderr self.stderr = stderr
class EffectError(RenderError): class EffectError(RenderError):
"""效果处理错误""" """效果处理错误"""
def __init__(self, message: str, effect_name: str = None, effect_params: str = None):
def __init__(
self, message: str, effect_name: str = None, effect_params: str = None
):
super().__init__(message) super().__init__(message)
self.effect_name = effect_name self.effect_name = effect_name
self.effect_params = effect_params self.effect_params = effect_params
class StorageError(RenderWorkerError): class StorageError(RenderWorkerError):
"""存储相关错误""" """存储相关错误"""
pass pass
class APIError(RenderWorkerError): class APIError(RenderWorkerError):
"""API调用错误""" """API调用错误"""
def __init__(self, message: str, status_code: int = None, response_body: str = None):
def __init__(
self, message: str, status_code: int = None, response_body: str = None
):
super().__init__(message) super().__init__(message)
self.status_code = status_code self.status_code = status_code
self.response_body = response_body self.response_body = response_body
class ResourceError(RenderWorkerError): class ResourceError(RenderWorkerError):
"""资源相关错误""" """资源相关错误"""
pass pass
class ResourceNotFoundError(ResourceError): class ResourceNotFoundError(ResourceError):
"""资源未找到错误""" """资源未找到错误"""
pass pass
class DownloadError(ResourceError): class DownloadError(ResourceError):
"""下载错误""" """下载错误"""
pass pass

View File

@@ -7,11 +7,19 @@ from typing import Optional, IO
from opentelemetry.trace import Status, StatusCode from opentelemetry.trace import Status, StatusCode
from entity.ffmpeg import FfmpegTask, ENCODER_ARGS, VIDEO_ARGS, AUDIO_ARGS, MUTE_AUDIO_INPUT, get_mp4toannexb_filter from entity.ffmpeg import (
FfmpegTask,
ENCODER_ARGS,
VIDEO_ARGS,
AUDIO_ARGS,
MUTE_AUDIO_INPUT,
get_mp4toannexb_filter,
)
from telemetry import get_tracer from telemetry import get_tracer
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def re_encode_and_annexb(file): def re_encode_and_annexb(file):
with get_tracer("ffmpeg").start_as_current_span("re_encode_and_annexb") as span: with get_tracer("ffmpeg").start_as_current_span("re_encode_and_annexb") as span:
span.set_attribute("file.path", file) span.set_attribute("file.path", file)
@@ -30,40 +38,69 @@ def re_encode_and_annexb(file):
_encoder_args = tuple(os.getenv("RE_ENCODE_ENCODER_ARGS", "").split(" ")) _encoder_args = tuple(os.getenv("RE_ENCODE_ENCODER_ARGS", "").split(" "))
else: else:
_encoder_args = ENCODER_ARGS _encoder_args = ENCODER_ARGS
ffmpeg_process = subprocess.run(["ffmpeg", "-y", "-hide_banner", "-i", file, ffmpeg_process = subprocess.run(
[
"ffmpeg",
"-y",
"-hide_banner",
"-i",
file,
*(set() if has_audio else MUTE_AUDIO_INPUT), *(set() if has_audio else MUTE_AUDIO_INPUT),
"-fps_mode", "cfr", "-fps_mode",
"-map", "0:v", "-map", "0:a" if has_audio else "1:a", "cfr",
*_video_args, "-bsf:v", get_mp4toannexb_filter(), "-map",
*AUDIO_ARGS, "-bsf:a", "setts=pts=DTS", "0:v",
*_encoder_args, "-shortest", "-fflags", "+genpts", "-map",
"-f", "mpegts", file + ".ts"]) "0:a" if has_audio else "1:a",
*_video_args,
"-bsf:v",
get_mp4toannexb_filter(),
*AUDIO_ARGS,
"-bsf:a",
"setts=pts=DTS",
*_encoder_args,
"-shortest",
"-fflags",
"+genpts",
"-f",
"mpegts",
file + ".ts",
]
)
logger.info(" ".join(ffmpeg_process.args)) logger.info(" ".join(ffmpeg_process.args))
span.set_attribute("ffmpeg.args", json.dumps(ffmpeg_process.args)) span.set_attribute("ffmpeg.args", json.dumps(ffmpeg_process.args))
logger.info("ReEncodeAndAnnexb: %s, returned: %s", file, ffmpeg_process.returncode) logger.info(
"ReEncodeAndAnnexb: %s, returned: %s", file, ffmpeg_process.returncode
)
span.set_attribute("ffmpeg.code", ffmpeg_process.returncode) span.set_attribute("ffmpeg.code", ffmpeg_process.returncode)
if ffmpeg_process.returncode == 0: if ffmpeg_process.returncode == 0:
span.set_status(Status(StatusCode.OK)) span.set_status(Status(StatusCode.OK))
span.set_attribute("file.size", os.path.getsize(file+".ts")) span.set_attribute("file.size", os.path.getsize(file + ".ts"))
# os.remove(file) # os.remove(file)
return file+".ts" return file + ".ts"
else: else:
span.set_status(Status(StatusCode.ERROR)) span.set_status(Status(StatusCode.ERROR))
return file return file
# start_render函数已迁移到services/render_service.py中的DefaultRenderService # start_render函数已迁移到services/render_service.py中的DefaultRenderService
# 保留原有签名用于向后兼容,但建议使用新的服务架构 # 保留原有签名用于向后兼容,但建议使用新的服务架构
def start_render(ffmpeg_task): def start_render(ffmpeg_task):
""" """
已迁移到新架构,建议使用 DefaultRenderService.render() 已迁移到新架构,建议使用 DefaultRenderService.render()
保留用于向后兼容 保留用于向后兼容
""" """
logger.warning("start_render is deprecated, use DefaultRenderService.render() instead") logger.warning(
"start_render is deprecated, use DefaultRenderService.render() instead"
)
from services import DefaultRenderService from services import DefaultRenderService
render_service = DefaultRenderService() render_service = DefaultRenderService()
return render_service.render(ffmpeg_task) return render_service.render(ffmpeg_task)
def handle_ffmpeg_output(stdout: Optional[bytes]) -> str: def handle_ffmpeg_output(stdout: Optional[bytes]) -> str:
out_time = "0:0:0.0" out_time = "0:0:0.0"
if stdout is None: if stdout is None:
@@ -81,7 +118,8 @@ def handle_ffmpeg_output(stdout: Optional[bytes]) -> str:
if line.startswith(b"speed="): if line.startswith(b"speed="):
speed = line.replace(b"speed=", b"").decode().strip() speed = line.replace(b"speed=", b"").decode().strip()
print("[ ]Speed:", out_time, "@", speed) print("[ ]Speed:", out_time, "@", speed)
return out_time+"@"+speed return out_time + "@" + speed
def duration_str_to_float(duration_str: str) -> float: def duration_str_to_float(duration_str: str) -> float:
_duration = datetime.strptime(duration_str, "%H:%M:%S.%f") - datetime(1900, 1, 1) _duration = datetime.strptime(duration_str, "%H:%M:%S.%f") - datetime(1900, 1, 1)
@@ -94,8 +132,18 @@ def probe_video_info(video_file):
span.set_attribute("video.file", video_file) span.set_attribute("video.file", video_file)
# 获取宽度和高度 # 获取宽度和高度
result = subprocess.run( result = subprocess.run(
["ffprobe", '-v', 'error', '-select_streams', 'v:0', '-show_entries', 'stream=width,height:format=duration', '-of', [
'csv=s=x:p=0', video_file], "ffprobe",
"-v",
"error",
"-select_streams",
"v:0",
"-show_entries",
"stream=width,height:format=duration",
"-of",
"csv=s=x:p=0",
video_file,
],
stderr=subprocess.STDOUT, stderr=subprocess.STDOUT,
**subprocess_args(True) **subprocess_args(True)
) )
@@ -104,14 +152,14 @@ def probe_video_info(video_file):
if result.returncode != 0: if result.returncode != 0:
span.set_status(Status(StatusCode.ERROR)) span.set_status(Status(StatusCode.ERROR))
return 0, 0, 0 return 0, 0, 0
all_result = result.stdout.decode('utf-8').strip() all_result = result.stdout.decode("utf-8").strip()
span.set_attribute("ffprobe.out", all_result) span.set_attribute("ffprobe.out", all_result)
if all_result == '': if all_result == "":
span.set_status(Status(StatusCode.ERROR)) span.set_status(Status(StatusCode.ERROR))
return 0, 0, 0 return 0, 0, 0
span.set_status(Status(StatusCode.OK)) span.set_status(Status(StatusCode.OK))
wh, duration = all_result.split('\n') wh, duration = all_result.split("\n")
width, height = wh.strip().split('x') width, height = wh.strip().split("x")
return int(width), int(height), float(duration) return int(width), int(height), float(duration)
@@ -119,8 +167,19 @@ def probe_video_audio(video_file, type=None):
tracer = get_tracer(__name__) tracer = get_tracer(__name__)
with tracer.start_as_current_span("probe_video_audio") as span: with tracer.start_as_current_span("probe_video_audio") as span:
span.set_attribute("video.file", video_file) span.set_attribute("video.file", video_file)
args = ["ffprobe", "-hide_banner", "-v", "error", "-select_streams", "a", "-show_entries", "stream=index", "-of", "csv=p=0"] args = [
if type == 'concat': "ffprobe",
"-hide_banner",
"-v",
"error",
"-select_streams",
"a",
"-show_entries",
"stream=index",
"-of",
"csv=p=0",
]
if type == "concat":
args.append("-safe") args.append("-safe")
args.append("0") args.append("0")
args.append("-f") args.append("-f")
@@ -130,16 +189,16 @@ def probe_video_audio(video_file, type=None):
result = subprocess.run(args, stderr=subprocess.STDOUT, **subprocess_args(True)) result = subprocess.run(args, stderr=subprocess.STDOUT, **subprocess_args(True))
span.set_attribute("ffprobe.args", json.dumps(result.args)) span.set_attribute("ffprobe.args", json.dumps(result.args))
span.set_attribute("ffprobe.code", result.returncode) span.set_attribute("ffprobe.code", result.returncode)
logger.info("probe_video_audio: %s", result.stdout.decode('utf-8').strip()) logger.info("probe_video_audio: %s", result.stdout.decode("utf-8").strip())
if result.returncode != 0: if result.returncode != 0:
return False return False
if result.stdout.decode('utf-8').strip() == '': if result.stdout.decode("utf-8").strip() == "":
return False return False
return True return True
# 音频淡出2秒 # 音频淡出2秒
def fade_out_audio(file, duration, fade_out_sec = 2): def fade_out_audio(file, duration, fade_out_sec=2):
if type(duration) == str: if type(duration) == str:
try: try:
duration = float(duration) duration = float(duration)
@@ -157,7 +216,25 @@ def fade_out_audio(file, duration, fade_out_sec = 2):
os.remove(new_fn) os.remove(new_fn)
logger.info("delete tmp file: " + new_fn) logger.info("delete tmp file: " + new_fn)
try: try:
process = subprocess.run(["ffmpeg", "-i", file, "-c:v", "copy", "-c:a", "aac", "-af", "afade=t=out:st=" + str(duration - fade_out_sec) + ":d=" + str(fade_out_sec), "-y", new_fn], **subprocess_args(True)) process = subprocess.run(
[
"ffmpeg",
"-i",
file,
"-c:v",
"copy",
"-c:a",
"aac",
"-af",
"afade=t=out:st="
+ str(duration - fade_out_sec)
+ ":d="
+ str(fade_out_sec),
"-y",
new_fn,
],
**subprocess_args(True)
)
span.set_attribute("ffmpeg.args", json.dumps(process.args)) span.set_attribute("ffmpeg.args", json.dumps(process.args))
logger.info(" ".join(process.args)) logger.info(" ".join(process.args))
if process.returncode != 0: if process.returncode != 0:
@@ -173,7 +250,6 @@ def fade_out_audio(file, duration, fade_out_sec = 2):
return file return file
# Create a set of arguments which make a ``subprocess.Popen`` (and # Create a set of arguments which make a ``subprocess.Popen`` (and
# variants) call work with or without Pyinstaller, ``--noconsole`` or # variants) call work with or without Pyinstaller, ``--noconsole`` or
# not, on Windows and Linux. Typical use:: # not, on Windows and Linux. Typical use::
@@ -186,7 +262,7 @@ def fade_out_audio(file, duration, fade_out_sec = 2):
# **subprocess_args(False)) # **subprocess_args(False))
def subprocess_args(include_stdout=True): def subprocess_args(include_stdout=True):
# The following is true only on Windows. # The following is true only on Windows.
if hasattr(subprocess, 'STARTUPINFO'): if hasattr(subprocess, "STARTUPINFO"):
# On Windows, subprocess calls will pop up a command window by default # On Windows, subprocess calls will pop up a command window by default
# when run from Pyinstaller with the ``--noconsole`` option. Avoid this # when run from Pyinstaller with the ``--noconsole`` option. Avoid this
# distraction. # distraction.
@@ -210,7 +286,7 @@ def subprocess_args(include_stdout=True):
# #
# So, add it only if it's needed. # So, add it only if it's needed.
if include_stdout: if include_stdout:
ret = {'stdout': subprocess.PIPE} ret = {"stdout": subprocess.PIPE}
else: else:
ret = {} ret = {}
@@ -218,8 +294,5 @@ def subprocess_args(include_stdout=True):
# with the ``--noconsole`` option requires redirecting everything # with the ``--noconsole`` option requires redirecting everything
# (stdin, stdout, stderr) to avoid an OSError exception # (stdin, stdout, stderr) to avoid an OSError exception
# "[Error 6] the handle is invalid." # "[Error 6] the handle is invalid."
ret.update({'stdin': subprocess.PIPE, ret.update({"stdin": subprocess.PIPE, "startupinfo": si, "env": env})
'startupinfo': si,
'env': env})
return ret return ret

View File

@@ -1,12 +1,14 @@
""" """
FFmpeg工具模块 - 提供FFmpeg命令构建和处理的公共函数 FFmpeg工具模块 - 提供FFmpeg命令构建和处理的公共函数
""" """
import logging import logging
from typing import List, Tuple, Optional from typing import List, Tuple, Optional
from config.settings import get_ffmpeg_config from config.settings import get_ffmpeg_config
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def build_base_ffmpeg_args() -> List[str]: def build_base_ffmpeg_args() -> List[str]:
""" """
构建基础FFmpeg参数 构建基础FFmpeg参数
@@ -20,6 +22,7 @@ def build_base_ffmpeg_args() -> List[str]:
args.extend(config.loglevel_args) args.extend(config.loglevel_args)
return args return args
def build_null_audio_input() -> List[str]: def build_null_audio_input() -> List[str]:
""" """
构建空音频输入参数 构建空音频输入参数
@@ -30,6 +33,7 @@ def build_null_audio_input() -> List[str]:
config = get_ffmpeg_config() config = get_ffmpeg_config()
return config.null_audio_args return config.null_audio_args
def build_amix_filter(input1: str, input2: str, output: str) -> str: def build_amix_filter(input1: str, input2: str, output: str) -> str:
""" """
构建音频混合滤镜 构建音频混合滤镜
@@ -45,7 +49,10 @@ def build_amix_filter(input1: str, input2: str, output: str) -> str:
config = get_ffmpeg_config() config = get_ffmpeg_config()
return f"{input1}[{input2}]{config.amix_args[0]}[{output}]" return f"{input1}[{input2}]{config.amix_args[0]}[{output}]"
def build_overlay_scale_filter(video_input: str, overlay_input: str, output: str) -> str:
def build_overlay_scale_filter(
video_input: str, overlay_input: str, output: str
) -> str:
""" """
构建覆盖层缩放滤镜 构建覆盖层缩放滤镜
@@ -61,7 +68,10 @@ def build_overlay_scale_filter(video_input: str, overlay_input: str, output: str
if config.overlay_scale_mode == "scale": if config.overlay_scale_mode == "scale":
return f"{video_input}[{overlay_input}]scale=iw:ih[{output}]" return f"{video_input}[{overlay_input}]scale=iw:ih[{output}]"
else: else:
return f"{video_input}[{overlay_input}]{config.overlay_scale_mode}=iw:ih[{output}]" return (
f"{video_input}[{overlay_input}]{config.overlay_scale_mode}=iw:ih[{output}]"
)
def get_annexb_filter() -> str: def get_annexb_filter() -> str:
""" """
@@ -76,6 +86,7 @@ def get_annexb_filter() -> str:
return "hevc_mp4toannexb" return "hevc_mp4toannexb"
return "h264_mp4toannexb" return "h264_mp4toannexb"
def build_standard_output_args() -> List[str]: def build_standard_output_args() -> List[str]:
""" """
构建标准输出参数 构建标准输出参数
@@ -88,9 +99,10 @@ def build_standard_output_args() -> List[str]:
*config.video_args, *config.video_args,
*config.audio_args, *config.audio_args,
*config.encoder_args, *config.encoder_args,
*config.default_args *config.default_args,
] ]
def validate_ffmpeg_file_extensions(file_path: str) -> bool: def validate_ffmpeg_file_extensions(file_path: str) -> bool:
""" """
验证文件扩展名是否为FFmpeg支持的格式 验证文件扩展名是否为FFmpeg支持的格式
@@ -102,16 +114,38 @@ def validate_ffmpeg_file_extensions(file_path: str) -> bool:
是否为支持的格式 是否为支持的格式
""" """
supported_extensions = { supported_extensions = {
'.mp4', '.avi', '.mov', '.mkv', '.flv', '.wmv', '.webm', ".mp4",
'.ts', '.m2ts', '.mts', '.m4v', '.3gp', '.asf', '.rm', ".avi",
'.mp3', '.wav', '.aac', '.flac', '.ogg', '.m4a', '.wma' ".mov",
".mkv",
".flv",
".wmv",
".webm",
".ts",
".m2ts",
".mts",
".m4v",
".3gp",
".asf",
".rm",
".mp3",
".wav",
".aac",
".flac",
".ogg",
".m4a",
".wma",
} }
import os import os
_, ext = os.path.splitext(file_path.lower()) _, ext = os.path.splitext(file_path.lower())
return ext in supported_extensions return ext in supported_extensions
def estimate_processing_time(input_duration: float, complexity_factor: float = 1.0) -> float:
def estimate_processing_time(
input_duration: float, complexity_factor: float = 1.0
) -> float:
""" """
估算处理时间 估算处理时间

View File

@@ -1,12 +1,14 @@
""" """
JSON处理工具模块 - 提供安全的JSON解析和处理功能 JSON处理工具模块 - 提供安全的JSON解析和处理功能
""" """
import json import json
import logging import logging
from typing import Dict, Any, Optional, Union from typing import Dict, Any, Optional, Union
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def safe_json_loads(json_str: Union[str, bytes], default: Any = None) -> Any: def safe_json_loads(json_str: Union[str, bytes], default: Any = None) -> Any:
""" """
安全解析JSON字符串 安全解析JSON字符串
@@ -18,7 +20,7 @@ def safe_json_loads(json_str: Union[str, bytes], default: Any = None) -> Any:
Returns: Returns:
解析后的对象,或默认值 解析后的对象,或默认值
""" """
if not json_str or json_str == '{}': if not json_str or json_str == "{}":
return default or {} return default or {}
try: try:
@@ -27,7 +29,10 @@ def safe_json_loads(json_str: Union[str, bytes], default: Any = None) -> Any:
logger.warning(f"Failed to parse JSON: {e}, input: {json_str}") logger.warning(f"Failed to parse JSON: {e}, input: {json_str}")
return default or {} return default or {}
def safe_json_dumps(obj: Any, indent: Optional[int] = None, ensure_ascii: bool = False) -> str:
def safe_json_dumps(
obj: Any, indent: Optional[int] = None, ensure_ascii: bool = False
) -> str:
""" """
安全序列化对象为JSON字符串 安全序列化对象为JSON字符串
@@ -45,6 +50,7 @@ def safe_json_dumps(obj: Any, indent: Optional[int] = None, ensure_ascii: bool =
logger.error(f"Failed to serialize to JSON: {e}") logger.error(f"Failed to serialize to JSON: {e}")
return "{}" return "{}"
def get_nested_value(data: Dict[str, Any], key_path: str, default: Any = None) -> Any: def get_nested_value(data: Dict[str, Any], key_path: str, default: Any = None) -> Any:
""" """
从嵌套字典中安全获取值 从嵌套字典中安全获取值
@@ -61,7 +67,7 @@ def get_nested_value(data: Dict[str, Any], key_path: str, default: Any = None) -
return default return default
try: try:
keys = key_path.split('.') keys = key_path.split(".")
current = data current = data
for key in keys: for key in keys:
@@ -75,6 +81,7 @@ def get_nested_value(data: Dict[str, Any], key_path: str, default: Any = None) -
logger.warning(f"Failed to get nested value for path '{key_path}': {e}") logger.warning(f"Failed to get nested value for path '{key_path}': {e}")
return default return default
def merge_dicts(*dicts: Dict[str, Any]) -> Dict[str, Any]: def merge_dicts(*dicts: Dict[str, Any]) -> Dict[str, Any]:
""" """
合并多个字典,后面的字典会覆盖前面的字典中相同的键 合并多个字典,后面的字典会覆盖前面的字典中相同的键

View File

@@ -31,12 +31,14 @@ def upload_to_oss(url, file_path):
if replace_map != "": if replace_map != "":
replace_list = [i.split("|", 1) for i in replace_map.split(",")] replace_list = [i.split("|", 1) for i in replace_map.split(",")]
new_url = url new_url = url
for (_src, _dst) in replace_list: for _src, _dst in replace_list:
new_url = new_url.replace(_src, _dst) new_url = new_url.replace(_src, _dst)
new_url = new_url.split("?", 1)[0] new_url = new_url.split("?", 1)[0]
r_span.set_attribute("rclone.target_dir", new_url) r_span.set_attribute("rclone.target_dir", new_url)
if new_url != url: if new_url != url:
result = os.system(f"rclone copyto --no-check-dest --ignore-existing --multi-thread-chunk-size 8M --multi-thread-streams 8 {file_path} {new_url}") result = os.system(
f"rclone copyto --no-check-dest --ignore-existing --multi-thread-chunk-size 8M --multi-thread-streams 8 {file_path} {new_url}"
)
r_span.set_attribute("rclone.result", result) r_span.set_attribute("rclone.result", result)
if result == 0: if result == 0:
span.set_status(Status(StatusCode.OK)) span.set_status(Status(StatusCode.OK))
@@ -49,8 +51,14 @@ def upload_to_oss(url, file_path):
try: try:
req_span.set_attribute("http.method", "PUT") req_span.set_attribute("http.method", "PUT")
req_span.set_attribute("http.url", url) req_span.set_attribute("http.url", url)
with open(file_path, 'rb') as f: with open(file_path, "rb") as f:
response = requests.put(url, data=f, stream=True, timeout=60, headers={"Content-Type": "video/mp4"}) response = requests.put(
url,
data=f,
stream=True,
timeout=60,
headers={"Content-Type": "video/mp4"},
)
req_span.set_attribute("http.status_code", response.status_code) req_span.set_attribute("http.status_code", response.status_code)
req_span.set_attribute("http.response", response.text) req_span.set_attribute("http.response", response.text)
response.raise_for_status() response.raise_for_status()
@@ -61,12 +69,16 @@ def upload_to_oss(url, file_path):
req_span.set_attribute("http.error", "Timeout") req_span.set_attribute("http.error", "Timeout")
req_span.set_status(Status(StatusCode.ERROR)) req_span.set_status(Status(StatusCode.ERROR))
retries += 1 retries += 1
logger.warning(f"Upload timed out. Retrying {retries}/{max_retries}...") logger.warning(
f"Upload timed out. Retrying {retries}/{max_retries}..."
)
except Exception as e: except Exception as e:
req_span.set_attribute("http.error", str(e)) req_span.set_attribute("http.error", str(e))
req_span.set_status(Status(StatusCode.ERROR)) req_span.set_status(Status(StatusCode.ERROR))
retries += 1 retries += 1
logger.warning(f"Upload failed. Retrying {retries}/{max_retries}...") logger.warning(
f"Upload failed. Retrying {retries}/{max_retries}..."
)
span.set_status(Status(StatusCode.ERROR)) span.set_status(Status(StatusCode.ERROR))
return False return False
@@ -86,7 +98,7 @@ def download_from_oss(url, file_path, skip_if_exist=None):
# 如果skip_if_exist为None,则从启动参数中读取 # 如果skip_if_exist为None,则从启动参数中读取
if skip_if_exist is None: if skip_if_exist is None:
skip_if_exist = 'skip_if_exist' in sys.argv skip_if_exist = "skip_if_exist" in sys.argv
if skip_if_exist and os.path.exists(file_path): if skip_if_exist and os.path.exists(file_path):
span.set_attribute("file.exist", True) span.set_attribute("file.exist", True)
@@ -107,7 +119,7 @@ def download_from_oss(url, file_path, skip_if_exist=None):
req_span.set_attribute("http.url", url) req_span.set_attribute("http.url", url)
response = requests.get(url, timeout=15) # 设置超时时间 response = requests.get(url, timeout=15) # 设置超时时间
req_span.set_attribute("http.status_code", response.status_code) req_span.set_attribute("http.status_code", response.status_code)
with open(file_path, 'wb') as f: with open(file_path, "wb") as f:
f.write(response.content) f.write(response.content)
req_span.set_attribute("file.size", os.path.getsize(file_path)) req_span.set_attribute("file.size", os.path.getsize(file_path))
req_span.set_status(Status(StatusCode.OK)) req_span.set_status(Status(StatusCode.OK))
@@ -117,11 +129,15 @@ def download_from_oss(url, file_path, skip_if_exist=None):
req_span.set_attribute("http.error", "Timeout") req_span.set_attribute("http.error", "Timeout")
req_span.set_status(Status(StatusCode.ERROR)) req_span.set_status(Status(StatusCode.ERROR))
retries += 1 retries += 1
logger.warning(f"Download timed out. Retrying {retries}/{max_retries}...") logger.warning(
f"Download timed out. Retrying {retries}/{max_retries}..."
)
except Exception as e: except Exception as e:
req_span.set_attribute("http.error", str(e)) req_span.set_attribute("http.error", str(e))
req_span.set_status(Status(StatusCode.ERROR)) req_span.set_status(Status(StatusCode.ERROR))
retries += 1 retries += 1
logger.warning(f"Download failed. Retrying {retries}/{max_retries}...") logger.warning(
f"Download failed. Retrying {retries}/{max_retries}..."
)
span.set_status(Status(StatusCode.ERROR)) span.set_status(Status(StatusCode.ERROR))
return False return False

View File

@@ -11,14 +11,14 @@ def get_sys_info():
Returns a dictionary with system information. Returns a dictionary with system information.
""" """
info = { info = {
'version': SOFTWARE_VERSION, "version": SOFTWARE_VERSION,
'client_datetime': datetime.now().isoformat(), "client_datetime": datetime.now().isoformat(),
'platform': platform.system(), "platform": platform.system(),
'runtime_version': 'Python ' + platform.python_version(), "runtime_version": "Python " + platform.python_version(),
'cpu_count': os.cpu_count(), "cpu_count": os.cpu_count(),
'cpu_usage': psutil.cpu_percent(), "cpu_usage": psutil.cpu_percent(),
'memory_total': psutil.virtual_memory().total, "memory_total": psutil.virtual_memory().total,
'memory_available': psutil.virtual_memory().available, "memory_available": psutil.virtual_memory().available,
'support_feature': SUPPORT_FEATURE "support_feature": SUPPORT_FEATURE,
} }
return info return info