You've already forked FrameTour-RenderWorker
mypy
This commit is contained in:
23
app.py
23
app.py
@@ -21,48 +21,51 @@ 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)
|
||||||
if not task_info:
|
if not task_info:
|
||||||
LOGGER.error("Failed to get task info for task: %s", task_id)
|
LOGGER.error("Failed to get task info for task: %s", task_id)
|
||||||
return "Failed to get task info", 400
|
return "Failed to get task info", 400
|
||||||
|
|
||||||
template_id = task_info.get("templateId")
|
template_id = task_info.get("templateId")
|
||||||
if not template_id:
|
if not template_id:
|
||||||
LOGGER.error("Task %s missing templateId", task_id)
|
LOGGER.error("Task %s missing templateId", task_id)
|
||||||
return "Missing templateId", 400
|
return "Missing templateId", 400
|
||||||
|
|
||||||
local_template_info = template_service.get_template(template_id)
|
local_template_info = template_service.get_template(template_id)
|
||||||
template_info = api.get_template_info(template_id)
|
template_info = api.get_template_info(template_id)
|
||||||
|
|
||||||
if not template_info:
|
if not template_info:
|
||||||
LOGGER.error("Failed to get template info for template: %s", template_id)
|
LOGGER.error("Failed to get template info for template: %s", template_id)
|
||||||
return "Failed to get template info", 400
|
return "Failed to get template info", 400
|
||||||
|
|
||||||
if local_template_info:
|
if local_template_info:
|
||||||
if local_template_info.get("updateTime") != template_info.get("updateTime"):
|
if local_template_info.get("updateTime") != template_info.get("updateTime"):
|
||||||
LOGGER.info("Template %s needs update, downloading...", template_id)
|
LOGGER.info("Template %s needs update, downloading...", template_id)
|
||||||
if not template_service.download_template(template_id):
|
if not template_service.download_template(template_id):
|
||||||
LOGGER.error("Failed to download template: %s", template_id)
|
LOGGER.error("Failed to download template: %s", template_id)
|
||||||
return "Failed to download template", 500
|
return "Failed to download template", 500
|
||||||
|
|
||||||
biz.task.start_task(task_info)
|
biz.task.start_task(task_info)
|
||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOGGER.error("Error processing task %s: %s", task_id, e, exc_info=True)
|
LOGGER.error("Error processing task %s: %s", task_id, e, exc_info=True)
|
||||||
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)
|
||||||
|
|||||||
@@ -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,19 +32,28 @@ 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)
|
||||||
|
|
||||||
# 创建新的渲染任务
|
# 创建新的渲染任务
|
||||||
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
|
||||||
@@ -54,7 +64,7 @@ def parse_ffmpeg_task(task_info, template_info):
|
|||||||
ffmpeg_task.luts = render_task.luts
|
ffmpeg_task.luts = render_task.luts
|
||||||
ffmpeg_task.audios = render_task.audios
|
ffmpeg_task.audios = render_task.audios
|
||||||
ffmpeg_task.overlays = render_task.overlays
|
ffmpeg_task.overlays = render_task.overlays
|
||||||
|
|
||||||
return ffmpeg_task
|
return ffmpeg_task
|
||||||
|
|
||||||
|
|
||||||
@@ -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):
|
||||||
@@ -88,14 +104,14 @@ def start_ffmpeg_task(ffmpeg_task):
|
|||||||
# 使用新的渲染服务
|
# 使用新的渲染服务
|
||||||
render_service = _get_render_service()
|
render_service = _get_render_service()
|
||||||
result = render_service.render(ffmpeg_task)
|
result = render_service.render(ffmpeg_task)
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
span.set_status(Status(StatusCode.OK))
|
span.set_status(Status(StatusCode.OK))
|
||||||
else:
|
else:
|
||||||
span.set_status(Status(StatusCode.ERROR))
|
span.set_status(Status(StatusCode.ERROR))
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
span.set_status(Status(StatusCode.ERROR))
|
span.set_status(Status(StatusCode.ERROR))
|
||||||
logger.error(f"FFmpeg task failed: {e}", exc_info=True)
|
logger.error(f"FFmpeg task failed: {e}", exc_info=True)
|
||||||
@@ -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())
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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__)
|
||||||
@@ -19,19 +20,19 @@ def start_task(task_info):
|
|||||||
try:
|
try:
|
||||||
# 使用服务容器获取任务服务
|
# 使用服务容器获取任务服务
|
||||||
task_service = get_task_service()
|
task_service = get_task_service()
|
||||||
|
|
||||||
# 使用新的任务服务处理
|
# 使用新的任务服务处理
|
||||||
result = task_service.process_task(task_info)
|
result = task_service.process_task(task_info)
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
span.set_status(Status(StatusCode.OK))
|
span.set_status(Status(StatusCode.OK))
|
||||||
logger.info("Task completed successfully: %s", task_info.get("id"))
|
logger.info("Task completed successfully: %s", task_info.get("id"))
|
||||||
else:
|
else:
|
||||||
span.set_status(Status(StatusCode.ERROR))
|
span.set_status(Status(StatusCode.ERROR))
|
||||||
logger.error("Task failed: %s", task_info.get("id"))
|
logger.error("Task failed: %s", task_info.get("id"))
|
||||||
|
|
||||||
return None # 保持原有返回值格式
|
return None # 保持原有返回值格式
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
span.set_status(Status(StatusCode.ERROR))
|
span.set_status(Status(StatusCode.ERROR))
|
||||||
logger.error("Task processing failed: %s", e, exc_info=True)
|
logger.error("Task processing failed: %s", e, exc_info=True)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -6,17 +6,19 @@ 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]
|
||||||
default_args: List[str]
|
default_args: List[str]
|
||||||
old_ffmpeg: bool = False
|
old_ffmpeg: bool = False
|
||||||
re_encode_video_args: Optional[List[str]] = None
|
re_encode_video_args: Optional[List[str]] = None
|
||||||
re_encode_encoder_args: Optional[List[str]] = None
|
re_encode_encoder_args: Optional[List[str]] = None
|
||||||
|
|
||||||
# 新增配置选项,消除硬编码
|
# 新增配置选项,消除硬编码
|
||||||
max_download_workers: int = 8
|
max_download_workers: int = 8
|
||||||
progress_args: List[str] = None
|
progress_args: List[str] = None
|
||||||
@@ -24,29 +26,31 @@ class FFmpegConfig:
|
|||||||
null_audio_args: List[str] = None
|
null_audio_args: List[str] = None
|
||||||
overlay_scale_mode: str = "scale2ref" # 新版本使用scale2ref,旧版本使用scale
|
overlay_scale_mode: str = "scale2ref" # 新版本使用scale2ref,旧版本使用scale
|
||||||
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"]
|
||||||
default_args = ["-shortest"]
|
default_args = ["-shortest"]
|
||||||
|
|
||||||
re_encode_video_args = None
|
re_encode_video_args = None
|
||||||
if os.getenv("RE_ENCODE_VIDEO_ARGS"):
|
if os.getenv("RE_ENCODE_VIDEO_ARGS"):
|
||||||
re_encode_video_args = os.getenv("RE_ENCODE_VIDEO_ARGS").split(" ")
|
re_encode_video_args = os.getenv("RE_ENCODE_VIDEO_ARGS").split(" ")
|
||||||
|
|
||||||
re_encode_encoder_args = None
|
re_encode_encoder_args = None
|
||||||
if os.getenv("RE_ENCODE_ENCODER_ARGS"):
|
if os.getenv("RE_ENCODE_ENCODER_ARGS"):
|
||||||
re_encode_encoder_args = os.getenv("RE_ENCODE_ENCODER_ARGS").split(" ")
|
re_encode_encoder_args = os.getenv("RE_ENCODE_ENCODER_ARGS").split(" ")
|
||||||
|
|
||||||
# 新增配置项的默认值
|
# 新增配置项的默认值
|
||||||
progress_args = ["-progress", "-"]
|
progress_args = ["-progress", "-"]
|
||||||
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,
|
||||||
video_args=video_args,
|
video_args=video_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
|
||||||
|
|||||||
@@ -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
0
entity/__init__.py
Normal file
@@ -1,25 +1,25 @@
|
|||||||
from .base import EffectProcessor, EffectRegistry
|
from .base import EffectProcessor, EffectRegistry
|
||||||
from .camera_shot import CameraShotEffect
|
from .camera_shot import CameraShotEffect
|
||||||
from .speed import SpeedEffect
|
from .speed import SpeedEffect
|
||||||
from .zoom import ZoomEffect
|
from .zoom import ZoomEffect
|
||||||
from .skip import SkipEffect
|
from .skip import SkipEffect
|
||||||
from .tail import TailEffect
|
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",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5,90 +5,99 @@ import logging
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class EffectProcessor(ABC):
|
class EffectProcessor(ABC):
|
||||||
"""效果处理器抽象基类"""
|
"""效果处理器抽象基类"""
|
||||||
|
|
||||||
def __init__(self, params: str = "", ext_data: Optional[Dict[str, Any]] = None):
|
def __init__(self, params: str = "", ext_data: Optional[Dict[str, Any]] = None):
|
||||||
self.params = params
|
self.params = params
|
||||||
self.ext_data = ext_data or {}
|
self.ext_data = ext_data or {}
|
||||||
self.frame_rate = 25 # 默认帧率
|
self.frame_rate = 25 # 默认帧率
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def validate_params(self) -> bool:
|
def validate_params(self) -> bool:
|
||||||
"""验证参数是否有效"""
|
"""验证参数是否有效"""
|
||||||
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滤镜参数
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
video_input: 输入视频流标识符 (例如: "[0:v]", "[v_eff1]")
|
video_input: 输入视频流标识符 (例如: "[0:v]", "[v_eff1]")
|
||||||
effect_index: 效果索引,用于生成唯一的输出标识符
|
effect_index: 效果索引,用于生成唯一的输出标识符
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
tuple: (filter_args_list, output_stream_identifier)
|
tuple: (filter_args_list, output_stream_identifier)
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_effect_name(self) -> str:
|
def get_effect_name(self) -> str:
|
||||||
"""获取效果名称"""
|
"""获取效果名称"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def parse_params(self) -> List[str]:
|
def parse_params(self) -> List[str]:
|
||||||
"""解析参数字符串为列表"""
|
"""解析参数字符串为列表"""
|
||||||
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:
|
||||||
"""效果处理器注册表"""
|
"""效果处理器注册表"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._processors: Dict[str, Type[EffectProcessor]] = {}
|
self._processors: Dict[str, Type[EffectProcessor]] = {}
|
||||||
|
|
||||||
def register(self, name: str, processor_class: Type[EffectProcessor]):
|
def register(self, name: str, processor_class: Type[EffectProcessor]):
|
||||||
"""注册效果处理器"""
|
"""注册效果处理器"""
|
||||||
if not issubclass(processor_class, EffectProcessor):
|
if not issubclass(processor_class, EffectProcessor):
|
||||||
raise ValueError(f"{processor_class} must be a subclass of EffectProcessor")
|
raise ValueError(f"{processor_class} must be a subclass of EffectProcessor")
|
||||||
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}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
processor_class = self._processors[effect_name]
|
processor_class = self._processors[effect_name]
|
||||||
return processor_class(params, ext_data)
|
return processor_class(params, ext_data)
|
||||||
|
|
||||||
def list_effects(self) -> List[str]:
|
def list_effects(self) -> List[str]:
|
||||||
"""列出所有注册的效果"""
|
"""列出所有注册的效果"""
|
||||||
return list(self._processors.keys())
|
return list(self._processors.keys())
|
||||||
|
|
||||||
def parse_effect_string(self, effect_string: str) -> tuple[str, str]:
|
def parse_effect_string(self, effect_string: str) -> tuple[str, str]:
|
||||||
"""
|
"""
|
||||||
解析效果字符串
|
解析效果字符串
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
effect_string: 效果字符串,格式为 "effect_name:params"
|
effect_string: 效果字符串,格式为 "effect_name:params"
|
||||||
|
|
||||||
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, ""
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
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):
|
||||||
"""相机镜头效果处理器"""
|
"""相机镜头效果处理器"""
|
||||||
|
|
||||||
def validate_params(self) -> bool:
|
def validate_params(self) -> bool:
|
||||||
"""验证参数:start_time,duration,rotate_deg"""
|
"""验证参数:start_time,duration,rotate_deg"""
|
||||||
params = self.parse_params()
|
params = self.parse_params()
|
||||||
if not params:
|
if not params:
|
||||||
return True # 使用默认参数
|
return True # 使用默认参数
|
||||||
|
|
||||||
# 参数格式: "start_time,duration,rotate_deg"
|
# 参数格式: "start_time,duration,rotate_deg"
|
||||||
if len(params) > 3:
|
if len(params) > 3:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
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)
|
||||||
@@ -25,55 +26,69 @@ class CameraShotEffect(EffectProcessor):
|
|||||||
return True
|
return True
|
||||||
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
|
||||||
|
|
||||||
params = self.parse_params()
|
params = self.parse_params()
|
||||||
|
|
||||||
# 设置默认值
|
# 设置默认值
|
||||||
start = 3.0
|
start = 3.0
|
||||||
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 = []
|
||||||
|
|
||||||
# 生成输出流标识符
|
# 生成输出流标识符
|
||||||
start_out_str = "[eff_s]"
|
start_out_str = "[eff_s]"
|
||||||
mid_out_str = "[eff_m]"
|
mid_out_str = "[eff_m]"
|
||||||
end_out_str = "[eff_e]"
|
end_out_str = "[eff_e]"
|
||||||
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
|
||||||
|
|
||||||
def get_effect_name(self) -> str:
|
def get_effect_name(self) -> str:
|
||||||
return "cameraShot"
|
return "cameraShot"
|
||||||
|
|||||||
@@ -1,38 +1,41 @@
|
|||||||
from typing import List
|
from typing import List
|
||||||
from .base import EffectProcessor
|
from .base import EffectProcessor
|
||||||
|
|
||||||
|
|
||||||
class SkipEffect(EffectProcessor):
|
class SkipEffect(EffectProcessor):
|
||||||
"""跳过开头效果处理器"""
|
"""跳过开头效果处理器"""
|
||||||
|
|
||||||
def validate_params(self) -> bool:
|
def validate_params(self) -> bool:
|
||||||
"""验证参数:跳过的秒数"""
|
"""验证参数:跳过的秒数"""
|
||||||
if not self.params:
|
if not self.params:
|
||||||
return True # 默认不跳过
|
return True # 默认不跳过
|
||||||
|
|
||||||
try:
|
try:
|
||||||
skip_seconds = float(self.params)
|
skip_seconds = float(self.params)
|
||||||
return skip_seconds >= 0
|
return skip_seconds >= 0
|
||||||
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
|
||||||
|
|
||||||
if not self.params:
|
if not self.params:
|
||||||
return [], video_input
|
return [], video_input
|
||||||
|
|
||||||
skip_seconds = float(self.params)
|
skip_seconds = float(self.params)
|
||||||
if skip_seconds <= 0:
|
if skip_seconds <= 0:
|
||||||
return [], video_input
|
return [], video_input
|
||||||
|
|
||||||
output_stream = f"[v_eff{effect_index}]"
|
output_stream = f"[v_eff{effect_index}]"
|
||||||
|
|
||||||
# 使用trim滤镜跳过开头
|
# 使用trim滤镜跳过开头
|
||||||
filter_args = [f"{video_input}trim=start={skip_seconds}{output_stream}"]
|
filter_args = [f"{video_input}trim=start={skip_seconds}{output_stream}"]
|
||||||
|
|
||||||
return filter_args, output_stream
|
return filter_args, output_stream
|
||||||
|
|
||||||
def get_effect_name(self) -> str:
|
def get_effect_name(self) -> str:
|
||||||
return "skip"
|
return "skip"
|
||||||
|
|||||||
@@ -1,35 +1,38 @@
|
|||||||
from typing import List
|
from typing import List
|
||||||
from .base import EffectProcessor
|
from .base import EffectProcessor
|
||||||
|
|
||||||
|
|
||||||
class SpeedEffect(EffectProcessor):
|
class SpeedEffect(EffectProcessor):
|
||||||
"""视频变速效果处理器"""
|
"""视频变速效果处理器"""
|
||||||
|
|
||||||
def validate_params(self) -> bool:
|
def validate_params(self) -> bool:
|
||||||
"""验证参数:速度倍数"""
|
"""验证参数:速度倍数"""
|
||||||
if not self.params:
|
if not self.params:
|
||||||
return True # 默认不变速
|
return True # 默认不变速
|
||||||
|
|
||||||
try:
|
try:
|
||||||
speed = float(self.params)
|
speed = float(self.params)
|
||||||
return speed > 0
|
return speed > 0
|
||||||
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
|
||||||
|
|
||||||
if not self.params or self.params == "1":
|
if not self.params or self.params == "1":
|
||||||
return [], video_input # 不需要变速
|
return [], video_input # 不需要变速
|
||||||
|
|
||||||
speed = float(self.params)
|
speed = float(self.params)
|
||||||
output_stream = f"[v_eff{effect_index}]"
|
output_stream = f"[v_eff{effect_index}]"
|
||||||
|
|
||||||
# 使用setpts进行变速
|
# 使用setpts进行变速
|
||||||
filter_args = [f"{video_input}setpts={speed}*PTS{output_stream}"]
|
filter_args = [f"{video_input}setpts={speed}*PTS{output_stream}"]
|
||||||
|
|
||||||
return filter_args, output_stream
|
return filter_args, output_stream
|
||||||
|
|
||||||
def get_effect_name(self) -> str:
|
def get_effect_name(self) -> str:
|
||||||
return "ospeed"
|
return "ospeed"
|
||||||
|
|||||||
@@ -1,42 +1,45 @@
|
|||||||
from typing import List
|
from typing import List
|
||||||
from .base import EffectProcessor
|
from .base import EffectProcessor
|
||||||
|
|
||||||
|
|
||||||
class TailEffect(EffectProcessor):
|
class TailEffect(EffectProcessor):
|
||||||
"""保留末尾效果处理器"""
|
"""保留末尾效果处理器"""
|
||||||
|
|
||||||
def validate_params(self) -> bool:
|
def validate_params(self) -> bool:
|
||||||
"""验证参数:保留的秒数"""
|
"""验证参数:保留的秒数"""
|
||||||
if not self.params:
|
if not self.params:
|
||||||
return True # 默认不截取
|
return True # 默认不截取
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tail_seconds = float(self.params)
|
tail_seconds = float(self.params)
|
||||||
return tail_seconds >= 0
|
return tail_seconds >= 0
|
||||||
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
|
||||||
|
|
||||||
if not self.params:
|
if not self.params:
|
||||||
return [], video_input
|
return [], video_input
|
||||||
|
|
||||||
tail_seconds = float(self.params)
|
tail_seconds = float(self.params)
|
||||||
if tail_seconds <= 0:
|
if tail_seconds <= 0:
|
||||||
return [], video_input
|
return [], video_input
|
||||||
|
|
||||||
output_stream = f"[v_eff{effect_index}]"
|
output_stream = f"[v_eff{effect_index}]"
|
||||||
|
|
||||||
# 使用reverse+trim+reverse的方法来精确获取最后N秒
|
# 使用reverse+trim+reverse的方法来精确获取最后N秒
|
||||||
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
|
||||||
|
|
||||||
def get_effect_name(self) -> str:
|
def get_effect_name(self) -> str:
|
||||||
return "tail"
|
return "tail"
|
||||||
|
|||||||
@@ -2,46 +2,47 @@ from typing import List
|
|||||||
import json
|
import json
|
||||||
from .base import EffectProcessor
|
from .base import EffectProcessor
|
||||||
|
|
||||||
|
|
||||||
class ZoomEffect(EffectProcessor):
|
class ZoomEffect(EffectProcessor):
|
||||||
"""缩放效果处理器"""
|
"""缩放效果处理器"""
|
||||||
|
|
||||||
def validate_params(self) -> bool:
|
def validate_params(self) -> bool:
|
||||||
"""验证参数:start_time,zoom_factor,duration"""
|
"""验证参数:start_time,zoom_factor,duration"""
|
||||||
params = self.parse_params()
|
params = self.parse_params()
|
||||||
if len(params) < 3:
|
if len(params) < 3:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
start_time = float(params[0])
|
start_time = float(params[0])
|
||||||
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
|
||||||
|
|
||||||
params = self.parse_params()
|
params = self.parse_params()
|
||||||
start_time = float(params[0])
|
start_time = float(params[0])
|
||||||
zoom_factor = float(params[1])
|
zoom_factor = float(params[1])
|
||||||
duration = float(params[2])
|
duration = float(params[2])
|
||||||
|
|
||||||
if zoom_factor == 1:
|
if zoom_factor == 1:
|
||||||
return [], video_input # 不需要缩放
|
return [], video_input # 不需要缩放
|
||||||
|
|
||||||
output_stream = f"[v_eff{effect_index}]"
|
output_stream = f"[v_eff{effect_index}]"
|
||||||
|
|
||||||
# 获取缩放中心点
|
# 获取缩放中心点
|
||||||
center_x, center_y = self._get_zoom_center()
|
center_x, center_y = self._get_zoom_center()
|
||||||
|
|
||||||
filter_args = []
|
filter_args = []
|
||||||
|
|
||||||
if duration == 0:
|
if duration == 0:
|
||||||
# 静态缩放(整个视频时长)
|
# 静态缩放(整个视频时长)
|
||||||
x_expr = f"({center_x})-(ow*zoom)/2"
|
x_expr = f"({center_x})-(ow*zoom)/2"
|
||||||
@@ -57,24 +58,24 @@ class ZoomEffect(EffectProcessor):
|
|||||||
filter_args.append(
|
filter_args.append(
|
||||||
f"{video_input}zoompan=z={zoom_expr}:x={x_expr}:y={y_expr}:d=1{output_stream}"
|
f"{video_input}zoompan=z={zoom_expr}:x={x_expr}:y={y_expr}:d=1{output_stream}"
|
||||||
)
|
)
|
||||||
|
|
||||||
return filter_args, output_stream
|
return filter_args, output_stream
|
||||||
|
|
||||||
def _get_zoom_center(self) -> tuple[str, str]:
|
def _get_zoom_center(self) -> tuple[str, str]:
|
||||||
"""获取缩放中心点坐标表达式"""
|
"""获取缩放中心点坐标表达式"""
|
||||||
# 默认中心点
|
# 默认中心点
|
||||||
center_x = "iw/2"
|
center_x = "iw/2"
|
||||||
center_y = "ih/2"
|
center_y = "ih/2"
|
||||||
|
|
||||||
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:
|
||||||
# 计算坐标系统中的中心点
|
# 计算坐标系统中的中心点
|
||||||
center_x_ratio = (_f_x + _f_x2) / (2 * _v_w)
|
center_x_ratio = (_f_x + _f_x2) / (2 * _v_w)
|
||||||
@@ -82,8 +83,8 @@ class ZoomEffect(EffectProcessor):
|
|||||||
# 转换为视频坐标系统
|
# 转换为视频坐标系统
|
||||||
center_x = f"iw*{center_x_ratio:.6f}"
|
center_x = f"iw*{center_x_ratio:.6f}"
|
||||||
center_y = f"ih*{center_y_ratio:.6f}"
|
center_y = f"ih*{center_y_ratio:.6f}"
|
||||||
|
|
||||||
return center_x, center_y
|
return center_x, center_y
|
||||||
|
|
||||||
def get_effect_name(self) -> str:
|
def get_effect_name(self) -> str:
|
||||||
return "zoom"
|
return "zoom"
|
||||||
|
|||||||
102
entity/ffmpeg.py
102
entity/ffmpeg.py
@@ -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():
|
||||||
@@ -24,8 +54,8 @@ class FfmpegTask(object):
|
|||||||
兼容类:保留原有FfmpegTask接口用于向后兼容
|
兼容类:保留原有FfmpegTask接口用于向后兼容
|
||||||
实际处理逻辑已迁移到新架构,该类主要用作数据载体
|
实际处理逻辑已迁移到新架构,该类主要用作数据载体
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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()]
|
||||||
|
)
|
||||||
|
|||||||
@@ -8,25 +8,30 @@ 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命令构建器"""
|
||||||
|
|
||||||
def __init__(self, task: RenderTask):
|
def __init__(self, task: RenderTask):
|
||||||
self.task = task
|
self.task = task
|
||||||
self.config = get_ffmpeg_config()
|
self.config = get_ffmpeg_config()
|
||||||
|
|
||||||
def build_command(self) -> List[str]:
|
def build_command(self) -> List[str]:
|
||||||
"""构建FFmpeg命令"""
|
"""构建FFmpeg命令"""
|
||||||
self.task.update_task_type()
|
self.task.update_task_type()
|
||||||
|
|
||||||
if self.task.task_type == TaskType.COPY:
|
if self.task.task_type == TaskType.COPY:
|
||||||
return self._build_copy_command()
|
return self._build_copy_command()
|
||||||
elif self.task.task_type == TaskType.CONCAT:
|
elif self.task.task_type == TaskType.CONCAT:
|
||||||
@@ -35,28 +40,32 @@ class FFmpegCommandBuilder:
|
|||||||
return self._build_encode_command()
|
return self._build_encode_command()
|
||||||
else:
|
else:
|
||||||
raise FFmpegError(f"Unsupported task type: {self.task.task_type}")
|
raise FFmpegError(f"Unsupported task type: {self.task.task_type}")
|
||||||
|
|
||||||
def _build_copy_command(self) -> List[str]:
|
def _build_copy_command(self) -> List[str]:
|
||||||
"""构建复制命令"""
|
"""构建复制命令"""
|
||||||
if len(self.task.input_files) == 1:
|
if len(self.task.input_files) == 1:
|
||||||
input_file = self.task.input_files[0]
|
input_file = self.task.input_files[0]
|
||||||
if input_file == self.task.output_file:
|
if input_file == self.task.output_file:
|
||||||
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]:
|
||||||
"""构建拼接命令"""
|
"""构建拼接命令"""
|
||||||
args = ["ffmpeg", "-y", "-hide_banner"]
|
args = ["ffmpeg", "-y", "-hide_banner"]
|
||||||
input_args = []
|
input_args = []
|
||||||
output_args = [*self.config.default_args]
|
output_args = [*self.config.default_args]
|
||||||
filter_args = []
|
filter_args = []
|
||||||
|
|
||||||
if len(self.task.input_files) == 1:
|
if len(self.task.input_files) == 1:
|
||||||
# 单个文件
|
# 单个文件
|
||||||
file = self.task.input_files[0]
|
file = self.task.input_files[0]
|
||||||
@@ -70,16 +79,16 @@ class FFmpegCommandBuilder:
|
|||||||
f.write(f"file '{input_file}'\n")
|
f.write(f"file '{input_file}'\n")
|
||||||
input_args.extend(["-f", "concat", "-safe", "0", "-i", tmp_file])
|
input_args.extend(["-f", "concat", "-safe", "0", "-i", tmp_file])
|
||||||
self.task.mute = not probe_video_audio(tmp_file, "concat")
|
self.task.mute = not probe_video_audio(tmp_file, "concat")
|
||||||
|
|
||||||
# 视频流映射
|
# 视频流映射
|
||||||
output_args.extend(["-map", "0:v", "-c:v", "copy"])
|
output_args.extend(["-map", "0:v", "-c:v", "copy"])
|
||||||
|
|
||||||
# 音频处理
|
# 音频处理
|
||||||
audio_output_str = self._handle_audio_concat(input_args, filter_args)
|
audio_output_str = self._handle_audio_concat(input_args, filter_args)
|
||||||
if audio_output_str:
|
if audio_output_str:
|
||||||
output_args.extend(["-map", audio_output_str])
|
output_args.extend(["-map", audio_output_str])
|
||||||
output_args.extend(self.config.audio_args)
|
output_args.extend(self.config.audio_args)
|
||||||
|
|
||||||
# annexb处理
|
# annexb处理
|
||||||
if self.task.annexb:
|
if self.task.annexb:
|
||||||
output_args.extend(["-bsf:v", self._get_mp4toannexb_filter()])
|
output_args.extend(["-bsf:v", self._get_mp4toannexb_filter()])
|
||||||
@@ -87,170 +96,210 @@ class FFmpegCommandBuilder:
|
|||||||
output_args.extend(["-f", "mpegts"])
|
output_args.extend(["-f", "mpegts"])
|
||||||
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]:
|
||||||
"""构建编码命令"""
|
"""构建编码命令"""
|
||||||
args = build_base_ffmpeg_args()
|
args = build_base_ffmpeg_args()
|
||||||
|
|
||||||
input_args = []
|
input_args = []
|
||||||
filter_args = []
|
filter_args = []
|
||||||
output_args = build_standard_output_args()
|
output_args = build_standard_output_args()
|
||||||
|
|
||||||
# annexb处理
|
# annexb处理
|
||||||
if self.task.annexb:
|
if self.task.annexb:
|
||||||
output_args.extend(["-bsf:v", get_annexb_filter()])
|
output_args.extend(["-bsf:v", get_annexb_filter()])
|
||||||
output_args.extend(["-reset_timestamps", "1"])
|
output_args.extend(["-reset_timestamps", "1"])
|
||||||
|
|
||||||
# 处理输入文件
|
# 处理输入文件
|
||||||
for input_file in self.task.input_files:
|
for input_file in self.task.input_files:
|
||||||
input_args.extend(["-i", input_file])
|
input_args.extend(["-i", input_file])
|
||||||
|
|
||||||
# 处理视频流
|
# 处理视频流
|
||||||
video_output_str = "[0:v]"
|
video_output_str = "[0:v]"
|
||||||
effect_index = 0
|
effect_index = 0
|
||||||
|
|
||||||
# 处理中心裁剪
|
# 处理中心裁剪
|
||||||
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
|
||||||
for lut in self.task.luts:
|
for lut in self.task.luts:
|
||||||
filter_args.append(f"{video_output_str}lut3d=file={lut}{video_output_str}")
|
filter_args.append(f"{video_output_str}lut3d=file={lut}{video_output_str}")
|
||||||
|
|
||||||
# 处理覆盖层
|
# 处理覆盖层
|
||||||
video_output_str = self._add_overlays(input_args, filter_args, video_output_str)
|
video_output_str = self._add_overlays(input_args, filter_args, video_output_str)
|
||||||
|
|
||||||
# 处理字幕
|
# 处理字幕
|
||||||
for subtitle in self.task.subtitles:
|
for subtitle in self.task.subtitles:
|
||||||
filter_args.append(f"{video_output_str}ass={subtitle}[v]")
|
filter_args.append(f"{video_output_str}ass={subtitle}[v]")
|
||||||
video_output_str = "[v]"
|
video_output_str = "[v]"
|
||||||
|
|
||||||
# 映射视频流
|
# 映射视频流
|
||||||
output_args.extend(["-map", video_output_str])
|
output_args.extend(["-map", video_output_str])
|
||||||
output_args.extend(["-r", str(self.task.frame_rate)])
|
output_args.extend(["-r", str(self.task.frame_rate)])
|
||||||
output_args.extend(["-fps_mode", "cfr"])
|
output_args.extend(["-fps_mode", "cfr"])
|
||||||
|
|
||||||
# 处理音频
|
# 处理音频
|
||||||
audio_output_str = self._handle_audio_encode(input_args, filter_args)
|
audio_output_str = self._handle_audio_encode(input_args, filter_args)
|
||||||
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]
|
)
|
||||||
|
|
||||||
def _add_center_cut(self, filter_args: List[str], video_input: str, effect_index: int) -> tuple[str, int]:
|
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]:
|
||||||
"""添加中心裁剪"""
|
"""添加中心裁剪"""
|
||||||
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)
|
||||||
current_input = output_stream
|
current_input = output_stream
|
||||||
effect_index += 1
|
effect_index += 1
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
for overlay in self.task.overlays:
|
for overlay in self.task.overlays:
|
||||||
input_index = input_args.count("-i") // 2 # 每个输入占两个参数 -i filename
|
input_index = input_args.count("-i") // 2 # 每个输入占两个参数 -i filename
|
||||||
input_args.extend(["-i", overlay])
|
input_args.extend(["-i", overlay])
|
||||||
|
|
||||||
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 = ""
|
||||||
|
|
||||||
if self.task.mute:
|
if self.task.mute:
|
||||||
input_index = input_args.count("-i") // 2
|
input_index = input_args.count("-i") // 2
|
||||||
input_args.extend(build_null_audio_input())
|
input_args.extend(build_null_audio_input())
|
||||||
audio_output_str = f"[{input_index}:a]"
|
audio_output_str = f"[{input_index}:a]"
|
||||||
else:
|
else:
|
||||||
audio_output_str = "[0:a]"
|
audio_output_str = "[0:a]"
|
||||||
|
|
||||||
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 = ""
|
||||||
|
|
||||||
if self.task.mute:
|
if self.task.mute:
|
||||||
input_index = input_args.count("-i") // 2
|
input_index = input_args.count("-i") // 2
|
||||||
input_args.extend(["-f", "lavfi", "-i", "anullsrc=cl=stereo:r=48000"])
|
input_args.extend(["-f", "lavfi", "-i", "anullsrc=cl=stereo:r=48000"])
|
||||||
@@ -258,12 +307,13 @@ class FFmpegCommandBuilder:
|
|||||||
audio_output_str = "[a]"
|
audio_output_str = "[a]"
|
||||||
else:
|
else:
|
||||||
audio_output_str = "[0:a]"
|
audio_output_str = "[0:a]"
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -8,39 +8,42 @@ 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
|
||||||
|
|
||||||
# 视频参数
|
# 视频参数
|
||||||
resolution: Optional[str] = None
|
resolution: Optional[str] = None
|
||||||
frame_rate: int = 25
|
frame_rate: int = 25
|
||||||
speed: float = 1.0
|
speed: float = 1.0
|
||||||
mute: bool = True
|
mute: bool = True
|
||||||
annexb: bool = False
|
annexb: bool = False
|
||||||
|
|
||||||
# 裁剪参数
|
# 裁剪参数
|
||||||
zoom_cut: Optional[int] = None
|
zoom_cut: Optional[int] = None
|
||||||
center_cut: Optional[int] = None
|
center_cut: Optional[int] = None
|
||||||
|
|
||||||
# 资源列表
|
# 资源列表
|
||||||
subtitles: List[str] = field(default_factory=list)
|
subtitles: List[str] = field(default_factory=list)
|
||||||
luts: List[str] = field(default_factory=list)
|
luts: List[str] = field(default_factory=list)
|
||||||
audios: List[str] = field(default_factory=list)
|
audios: List[str] = field(default_factory=list)
|
||||||
overlays: List[str] = field(default_factory=list)
|
overlays: List[str] = field(default_factory=list)
|
||||||
effects: List[str] = field(default_factory=list)
|
effects: List[str] = field(default_factory=list)
|
||||||
|
|
||||||
# 扩展数据
|
# 扩展数据
|
||||||
ext_data: Dict[str, Any] = field(default_factory=dict)
|
ext_data: Dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
"""初始化后处理"""
|
"""初始化后处理"""
|
||||||
# 检测annexb格式
|
# 检测annexb格式
|
||||||
@@ -48,80 +51,90 @@ class RenderTask:
|
|||||||
if isinstance(input_file, str) and input_file.endswith(".ts"):
|
if isinstance(input_file, str) and input_file.endswith(".ts"):
|
||||||
self.annexb = True
|
self.annexb = True
|
||||||
break
|
break
|
||||||
|
|
||||||
# 自动生成输出文件名
|
# 自动生成输出文件名
|
||||||
if not self.output_file:
|
if not self.output_file:
|
||||||
self._generate_output_filename()
|
self._generate_output_filename()
|
||||||
|
|
||||||
def _generate_output_filename(self):
|
def _generate_output_filename(self):
|
||||||
"""生成输出文件名"""
|
"""生成输出文件名"""
|
||||||
if self.annexb:
|
if self.annexb:
|
||||||
self.output_file = f"rand_{uuid.uuid4()}.ts"
|
self.output_file = f"rand_{uuid.uuid4()}.ts"
|
||||||
else:
|
else:
|
||||||
self.output_file = f"rand_{uuid.uuid4()}.mp4"
|
self.output_file = f"rand_{uuid.uuid4()}.mp4"
|
||||||
|
|
||||||
def add_input_file(self, file_path: str):
|
def add_input_file(self, file_path: str):
|
||||||
"""添加输入文件"""
|
"""添加输入文件"""
|
||||||
self.input_files.append(file_path)
|
self.input_files.append(file_path)
|
||||||
if file_path.endswith(".ts"):
|
if file_path.endswith(".ts"):
|
||||||
self.annexb = True
|
self.annexb = True
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
def add_audios(self, *audios: str):
|
def add_audios(self, *audios: str):
|
||||||
"""添加音频"""
|
"""添加音频"""
|
||||||
self.audios.extend(audios)
|
self.audios.extend(audios)
|
||||||
|
|
||||||
def add_lut(self, *luts: str):
|
def add_lut(self, *luts: str):
|
||||||
"""添加LUT"""
|
"""添加LUT"""
|
||||||
self.luts.extend(luts)
|
self.luts.extend(luts)
|
||||||
|
|
||||||
def add_effect(self, *effects: str):
|
def add_effect(self, *effects: str):
|
||||||
"""添加效果"""
|
"""添加效果"""
|
||||||
self.effects.extend(effects)
|
self.effects.extend(effects)
|
||||||
|
|
||||||
def validate(self) -> bool:
|
def validate(self) -> bool:
|
||||||
"""验证任务参数"""
|
"""验证任务参数"""
|
||||||
if not self.input_files:
|
if not self.input_files:
|
||||||
raise TaskValidationError("No input files specified")
|
raise TaskValidationError("No input files specified")
|
||||||
|
|
||||||
# 验证所有效果
|
# 验证所有效果
|
||||||
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:
|
||||||
"""自动确定任务类型"""
|
"""自动确定任务类型"""
|
||||||
if self.can_copy():
|
if self.can_copy():
|
||||||
@@ -130,17 +143,17 @@ class RenderTask:
|
|||||||
return TaskType.CONCAT
|
return TaskType.CONCAT
|
||||||
else:
|
else:
|
||||||
return TaskType.ENCODE
|
return TaskType.ENCODE
|
||||||
|
|
||||||
def update_task_type(self):
|
def update_task_type(self):
|
||||||
"""更新任务类型"""
|
"""更新任务类型"""
|
||||||
self.task_type = self.determine_task_type()
|
self.task_type = self.determine_task_type()
|
||||||
|
|
||||||
def need_processing(self) -> bool:
|
def need_processing(self) -> bool:
|
||||||
"""检查是否需要处理"""
|
"""检查是否需要处理"""
|
||||||
if self.annexb:
|
if self.annexb:
|
||||||
return True
|
return True
|
||||||
return not self.can_copy()
|
return not self.can_copy()
|
||||||
|
|
||||||
def get_output_extension(self) -> str:
|
def get_output_extension(self) -> str:
|
||||||
"""获取输出文件扩展名"""
|
"""获取输出文件扩展名"""
|
||||||
return ".ts" if self.annexb else ".mp4"
|
return ".ts" if self.annexb else ".mp4"
|
||||||
|
|||||||
19
index.py
19
index.py
@@ -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):
|
||||||
@@ -48,34 +49,35 @@ def cleanup_temp_files():
|
|||||||
LOGGER.debug(f"Deleted temp file: {file_path}")
|
LOGGER.debug(f"Deleted temp file: {file_path}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOGGER.warning(f"Error deleting file {file_path}: {e}")
|
LOGGER.warning(f"Error deleting file {file_path}: {e}")
|
||||||
|
|
||||||
# 在后台线程中执行清理
|
# 在后台线程中执行清理
|
||||||
threading.Thread(target=_cleanup, daemon=True).start()
|
threading.Thread(target=_cleanup, daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
def main_loop():
|
def main_loop():
|
||||||
"""主处理循环"""
|
"""主处理循环"""
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
print("waiting for task...")
|
print("waiting for task...")
|
||||||
task_list = api.sync_center()
|
task_list = api.sync_center()
|
||||||
|
|
||||||
if len(task_list) == 0:
|
if len(task_list) == 0:
|
||||||
# 异步清理临时文件
|
# 异步清理临时文件
|
||||||
cleanup_temp_files()
|
cleanup_temp_files()
|
||||||
sleep(5)
|
sleep(5)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for task in task_list:
|
for task in task_list:
|
||||||
task_id = task.get("id", "unknown")
|
task_id = task.get("id", "unknown")
|
||||||
print(f"Processing task: {task_id}")
|
print(f"Processing task: {task_id}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
biz.task.start_task(task)
|
biz.task.start_task(task)
|
||||||
LOGGER.info(f"Task {task_id} completed successfully")
|
LOGGER.info(f"Task {task_id} completed successfully")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOGGER.error(f"Task {task_id} failed: {e}", exc_info=True)
|
LOGGER.error(f"Task {task_id} failed: {e}", exc_info=True)
|
||||||
# 继续处理下一个任务而不是崩溃
|
# 继续处理下一个任务而不是崩溃
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
LOGGER.info("Received shutdown signal, exiting...")
|
LOGGER.info("Received shutdown signal, exiting...")
|
||||||
break
|
break
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -9,67 +9,76 @@ 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:
|
||||||
"""
|
"""
|
||||||
执行渲染任务
|
执行渲染任务
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
task: 渲染任务
|
task: 渲染任务
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: 渲染是否成功
|
bool: 渲染是否成功
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_video_info(self, file_path: str) -> tuple[int, int, float]:
|
def get_video_info(self, file_path: str) -> tuple[int, int, float]:
|
||||||
"""
|
"""
|
||||||
获取视频信息
|
获取视频信息
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
file_path: 视频文件路径
|
file_path: 视频文件路径
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
tuple: (width, height, duration)
|
tuple: (width, height, duration)
|
||||||
"""
|
"""
|
||||||
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:
|
||||||
"""
|
"""
|
||||||
音频淡出处理
|
音频淡出处理
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
file_path: 音频文件路径
|
file_path: 音频文件路径
|
||||||
duration: 音频总时长
|
duration: 音频总时长
|
||||||
fade_seconds: 淡出时长
|
fade_seconds: 淡出时长
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: 处理后的文件路径
|
str: 处理后的文件路径
|
||||||
"""
|
"""
|
||||||
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)
|
||||||
|
|
||||||
tracer = get_tracer(__name__)
|
tracer = get_tracer(__name__)
|
||||||
with tracer.start_as_current_span("render_task") as span:
|
with tracer.start_as_current_span("render_task") as span:
|
||||||
try:
|
try:
|
||||||
@@ -78,98 +87,104 @@ class DefaultRenderService(RenderService):
|
|||||||
span.set_attribute("task.type", task.task_type.value)
|
span.set_attribute("task.type", task.task_type.value)
|
||||||
span.set_attribute("task.input_files", len(task.input_files))
|
span.set_attribute("task.input_files", len(task.input_files))
|
||||||
span.set_attribute("task.output_file", task.output_file)
|
span.set_attribute("task.output_file", task.output_file)
|
||||||
|
|
||||||
# 检查是否需要处理
|
# 检查是否需要处理
|
||||||
if not task.need_processing():
|
if not task.need_processing():
|
||||||
if len(task.input_files) == 1:
|
if len(task.input_files) == 1:
|
||||||
task.output_file = task.input_files[0]
|
task.output_file = task.input_files[0]
|
||||||
span.set_status(Status(StatusCode.OK))
|
span.set_status(Status(StatusCode.OK))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 构建FFmpeg命令
|
# 构建FFmpeg命令
|
||||||
builder = FFmpegCommandBuilder(task)
|
builder = FFmpegCommandBuilder(task)
|
||||||
ffmpeg_args = builder.build_command()
|
ffmpeg_args = builder.build_command()
|
||||||
|
|
||||||
if not ffmpeg_args:
|
if not ffmpeg_args:
|
||||||
# 不需要处理,直接返回
|
# 不需要处理,直接返回
|
||||||
if len(task.input_files) == 1:
|
if len(task.input_files) == 1:
|
||||||
task.output_file = task.input_files[0]
|
task.output_file = task.input_files[0]
|
||||||
span.set_status(Status(StatusCode.OK))
|
span.set_status(Status(StatusCode.OK))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 执行FFmpeg命令
|
# 执行FFmpeg命令
|
||||||
return self._execute_ffmpeg(ffmpeg_args, span)
|
return self._execute_ffmpeg(ffmpeg_args, span)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
span.set_status(Status(StatusCode.ERROR))
|
span.set_status(Status(StatusCode.ERROR))
|
||||||
logger.error(f"Render failed: {e}", exc_info=True)
|
logger.error(f"Render failed: {e}", exc_info=True)
|
||||||
raise RenderError(f"Render failed: {e}") from e
|
raise RenderError(f"Render failed: {e}") from e
|
||||||
|
|
||||||
def _execute_ffmpeg(self, args: list[str], span) -> bool:
|
def _execute_ffmpeg(self, args: list[str], span) -> bool:
|
||||||
"""执行FFmpeg命令"""
|
"""执行FFmpeg命令"""
|
||||||
span.set_attribute("ffmpeg.args", " ".join(args))
|
span.set_attribute("ffmpeg.args", " ".join(args))
|
||||||
logger.info("Executing FFmpeg: %s", " ".join(args))
|
logger.info("Executing FFmpeg: %s", " ".join(args))
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
# 处理输出
|
# 处理输出
|
||||||
if process.stdout:
|
if process.stdout:
|
||||||
output = handle_ffmpeg_output(process.stdout)
|
output = handle_ffmpeg_output(process.stdout)
|
||||||
span.set_attribute("ffmpeg.output", output)
|
span.set_attribute("ffmpeg.output", output)
|
||||||
logger.info("FFmpeg output: %s", output)
|
logger.info("FFmpeg output: %s", output)
|
||||||
|
|
||||||
# 检查返回码
|
# 检查返回码
|
||||||
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 检查输出文件
|
# 检查输出文件
|
||||||
output_file = args[-1] # 输出文件总是最后一个参数
|
output_file = args[-1] # 输出文件总是最后一个参数
|
||||||
if not os.path.exists(output_file):
|
if not os.path.exists(output_file):
|
||||||
span.set_status(Status(StatusCode.ERROR))
|
span.set_status(Status(StatusCode.ERROR))
|
||||||
raise RenderError(f"Output file not created: {output_file}")
|
raise RenderError(f"Output file not created: {output_file}")
|
||||||
|
|
||||||
# 检查文件大小
|
# 检查文件大小
|
||||||
file_size = os.path.getsize(output_file)
|
file_size = os.path.getsize(output_file)
|
||||||
span.set_attribute("output.file_size", file_size)
|
span.set_attribute("output.file_size", file_size)
|
||||||
|
|
||||||
if file_size < 4096: # 文件过小
|
if file_size < 4096: # 文件过小
|
||||||
span.set_status(Status(StatusCode.ERROR))
|
span.set_status(Status(StatusCode.ERROR))
|
||||||
raise RenderError(f"Output file too small: {file_size} bytes")
|
raise RenderError(f"Output file too small: {file_size} bytes")
|
||||||
|
|
||||||
span.set_status(Status(StatusCode.OK))
|
span.set_status(Status(StatusCode.OK))
|
||||||
logger.info("FFmpeg execution completed successfully")
|
logger.info("FFmpeg execution completed successfully")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except subprocess.SubprocessError as e:
|
except subprocess.SubprocessError as e:
|
||||||
span.set_status(Status(StatusCode.ERROR))
|
span.set_status(Status(StatusCode.ERROR))
|
||||||
logger.error("Subprocess error: %s", e)
|
logger.error("Subprocess error: %s", e)
|
||||||
raise FFmpegError(f"Subprocess error: {e}") from e
|
raise FFmpegError(f"Subprocess error: {e}") from e
|
||||||
|
|
||||||
def get_video_info(self, file_path: str) -> tuple[int, int, float]:
|
def get_video_info(self, file_path: str) -> tuple[int, int, float]:
|
||||||
"""获取视频信息"""
|
"""获取视频信息"""
|
||||||
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)
|
||||||
|
|
||||||
def _render_legacy_ffmpeg_task(self, ffmpeg_task) -> bool:
|
def _render_legacy_ffmpeg_task(self, ffmpeg_task) -> bool:
|
||||||
"""兼容处理旧的FfmpegTask"""
|
"""兼容处理旧的FfmpegTask"""
|
||||||
tracer = get_tracer(__name__)
|
tracer = get_tracer(__name__)
|
||||||
@@ -180,19 +195,19 @@ class DefaultRenderService(RenderService):
|
|||||||
if not self.render(sub_task):
|
if not self.render(sub_task):
|
||||||
span.set_status(Status(StatusCode.ERROR))
|
span.set_status(Status(StatusCode.ERROR))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 获取FFmpeg参数
|
# 获取FFmpeg参数
|
||||||
ffmpeg_args = ffmpeg_task.get_ffmpeg_args()
|
ffmpeg_args = ffmpeg_task.get_ffmpeg_args()
|
||||||
|
|
||||||
if not ffmpeg_args:
|
if not ffmpeg_args:
|
||||||
# 不需要处理,直接返回
|
# 不需要处理,直接返回
|
||||||
span.set_status(Status(StatusCode.OK))
|
span.set_status(Status(StatusCode.OK))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 执行FFmpeg命令
|
# 执行FFmpeg命令
|
||||||
return self._execute_ffmpeg(ffmpeg_args, span)
|
return self._execute_ffmpeg(ffmpeg_args, span)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
span.set_status(Status(StatusCode.ERROR))
|
span.set_status(Status(StatusCode.ERROR))
|
||||||
logger.error(f"Legacy FFmpeg task render failed: {e}", exc_info=True)
|
logger.error(f"Legacy FFmpeg task render failed: {e}", exc_info=True)
|
||||||
raise RenderError(f"Legacy render failed: {e}") from e
|
raise RenderError(f"Legacy render failed: {e}") from e
|
||||||
|
|||||||
@@ -1,39 +1,43 @@
|
|||||||
"""
|
"""
|
||||||
服务容器模块 - 提供线程安全的服务实例管理
|
服务容器模块 - 提供线程安全的服务实例管理
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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:
|
||||||
"""线程安全的服务容器,实现依赖注入和单例管理"""
|
"""线程安全的服务容器,实现依赖注入和单例管理"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._services: Dict[Type, object] = {}
|
self._services: Dict[Type, object] = {}
|
||||||
self._factories: Dict[Type, callable] = {}
|
self._factories: Dict[Type, callable] = {}
|
||||||
self._lock = threading.RLock()
|
self._lock = threading.RLock()
|
||||||
|
|
||||||
def register_singleton(self, service_type: Type[T], factory: callable) -> None:
|
def register_singleton(self, service_type: Type[T], factory: callable) -> None:
|
||||||
"""注册单例服务工厂"""
|
"""注册单例服务工厂"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self._factories[service_type] = factory
|
self._factories[service_type] = factory
|
||||||
logger.debug(f"Registered singleton factory for {service_type.__name__}")
|
logger.debug(f"Registered singleton factory for {service_type.__name__}")
|
||||||
|
|
||||||
def get_service(self, service_type: Type[T]) -> T:
|
def get_service(self, service_type: Type[T]) -> T:
|
||||||
"""获取服务实例(懒加载单例)"""
|
"""获取服务实例(懒加载单例)"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
# 检查是否已存在实例
|
# 检查是否已存在实例
|
||||||
if service_type in self._services:
|
if service_type in self._services:
|
||||||
return self._services[service_type]
|
return self._services[service_type]
|
||||||
|
|
||||||
# 检查是否有工厂方法
|
# 检查是否有工厂方法
|
||||||
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]
|
||||||
try:
|
try:
|
||||||
@@ -42,14 +46,16 @@ 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:
|
||||||
"""检查是否有服务注册"""
|
"""检查是否有服务注册"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
return service_type in self._factories
|
return service_type in self._factories
|
||||||
|
|
||||||
def clear_cache(self, service_type: Optional[Type[T]] = None) -> None:
|
def clear_cache(self, service_type: Optional[Type[T]] = None) -> None:
|
||||||
"""清理服务缓存"""
|
"""清理服务缓存"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
@@ -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,45 +81,54 @@ 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
|
||||||
from .template_service import DefaultTemplateService, TemplateService
|
from .template_service import DefaultTemplateService, TemplateService
|
||||||
from .task_service import DefaultTaskService, TaskService
|
from .task_service import DefaultTaskService, TaskService
|
||||||
|
|
||||||
container = get_container()
|
container = get_container()
|
||||||
|
|
||||||
# 注册渲染服务
|
# 注册渲染服务
|
||||||
container.register_singleton(RenderService, lambda: DefaultRenderService())
|
container.register_singleton(RenderService, lambda: DefaultRenderService())
|
||||||
|
|
||||||
# 注册模板服务
|
# 注册模板服务
|
||||||
def create_template_service():
|
def create_template_service():
|
||||||
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)
|
||||||
|
|
||||||
# 注册任务服务(依赖其他服务)
|
# 注册任务服务(依赖其他服务)
|
||||||
def create_task_service():
|
def create_task_service():
|
||||||
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)
|
||||||
|
|||||||
@@ -16,43 +16,49 @@ from telemetry import get_tracer
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class TaskService(ABC):
|
class TaskService(ABC):
|
||||||
"""任务服务抽象接口"""
|
"""任务服务抽象接口"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def process_task(self, task_info: Dict[str, Any]) -> bool:
|
def process_task(self, task_info: Dict[str, Any]) -> bool:
|
||||||
"""
|
"""
|
||||||
处理任务
|
处理任务
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
task_info: 任务信息
|
task_info: 任务信息
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: 处理是否成功
|
bool: 处理是否成功
|
||||||
"""
|
"""
|
||||||
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:
|
||||||
"""
|
"""
|
||||||
创建渲染任务
|
创建渲染任务
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
task_info: 任务信息
|
task_info: 任务信息
|
||||||
template_info: 模板信息
|
template_info: 模板信息
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
RenderTask: 渲染任务对象
|
RenderTask: 渲染任务对象
|
||||||
"""
|
"""
|
||||||
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
|
||||||
|
|
||||||
def process_task(self, task_info: Dict[str, Any]) -> bool:
|
def process_task(self, task_info: Dict[str, Any]) -> bool:
|
||||||
"""处理任务"""
|
"""处理任务"""
|
||||||
tracer = get_tracer(__name__)
|
tracer = get_tracer(__name__)
|
||||||
@@ -61,112 +67,128 @@ 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")
|
||||||
template_info = self.template_service.get_template(template_id)
|
template_info = self.template_service.get_template(template_id)
|
||||||
if not template_info:
|
if not template_info:
|
||||||
raise TaskError(f"Template not found: {template_id}")
|
raise TaskError(f"Template not found: {template_id}")
|
||||||
|
|
||||||
# 报告任务开始
|
# 报告任务开始
|
||||||
api.report_task_start(task_info)
|
api.report_task_start(task_info)
|
||||||
|
|
||||||
# 创建渲染任务
|
# 创建渲染任务
|
||||||
render_task = self.create_render_task(task_info, template_info)
|
render_task = self.create_render_task(task_info, template_info)
|
||||||
|
|
||||||
# 执行渲染
|
# 执行渲染
|
||||||
success = self.render_service.render(render_task)
|
success = self.render_service.render(render_task)
|
||||||
if not success:
|
if not success:
|
||||||
span.set_status(Status(StatusCode.ERROR))
|
span.set_status(Status(StatusCode.ERROR))
|
||||||
api.report_task_failed(task_info, "Render failed")
|
api.report_task_failed(task_info, "Render failed")
|
||||||
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
|
||||||
|
|
||||||
task_compat = TaskCompat(render_task.output_file)
|
task_compat = TaskCompat(render_task.output_file)
|
||||||
upload_success = api.upload_task_file(task_info, task_compat)
|
upload_success = api.upload_task_file(task_info, task_compat)
|
||||||
if not upload_success:
|
if not upload_success:
|
||||||
span.set_status(Status(StatusCode.ERROR))
|
span.set_status(Status(StatusCode.ERROR))
|
||||||
api.report_task_failed(task_info, "Upload failed")
|
api.report_task_failed(task_info, "Upload failed")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 清理临时文件
|
# 清理临时文件
|
||||||
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
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
span.set_status(Status(StatusCode.ERROR))
|
span.set_status(Status(StatusCode.ERROR))
|
||||||
logger.error(f"Task processing failed: {e}", exc_info=True)
|
logger.error(f"Task processing failed: {e}", exc_info=True)
|
||||||
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:
|
||||||
# 解析任务参数
|
# 解析任务参数
|
||||||
task_params_str = task_info.get("taskParams", "{}")
|
task_params_str = task_info.get("taskParams", "{}")
|
||||||
span.set_attribute("task_params", task_params_str)
|
span.set_attribute("task_params", task_params_str)
|
||||||
|
|
||||||
task_params = safe_json_loads(task_params_str, {})
|
task_params = safe_json_loads(task_params_str, {})
|
||||||
task_params_orig = safe_json_loads(task_params_str, {})
|
task_params_orig = safe_json_loads(task_params_str, {})
|
||||||
|
|
||||||
if not task_params:
|
if not task_params:
|
||||||
raise TaskValidationError("Invalid or empty task params JSON")
|
raise TaskValidationError("Invalid or empty task params JSON")
|
||||||
|
|
||||||
# 并行下载资源
|
# 并行下载资源
|
||||||
self._download_resources(task_params)
|
self._download_resources(task_params)
|
||||||
|
|
||||||
# 创建子任务列表
|
# 创建子任务列表
|
||||||
sub_tasks = []
|
sub_tasks = []
|
||||||
only_if_usage_count = {}
|
only_if_usage_count = {}
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
# 创建子任务
|
# 创建子任务
|
||||||
sub_task = self._create_sub_task(part, source, ext_data, template_info)
|
sub_task = self._create_sub_task(part, source, ext_data, template_info)
|
||||||
sub_tasks.append(sub_task)
|
sub_tasks.append(sub_task)
|
||||||
|
|
||||||
# 创建主任务
|
# 创建主任务
|
||||||
output_file = f"out_{task_info.get('id', 'unknown')}.mp4"
|
output_file = f"out_{task_info.get('id', 'unknown')}.mp4"
|
||||||
main_task = RenderTask(
|
main_task = RenderTask(
|
||||||
@@ -175,28 +197,29 @@ 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"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# 应用整体模板设置
|
# 应用整体模板设置
|
||||||
overall_template = template_info.get("overall_template", {})
|
overall_template = template_info.get("overall_template", {})
|
||||||
self._apply_template_settings(main_task, overall_template, template_info)
|
self._apply_template_settings(main_task, overall_template, template_info)
|
||||||
|
|
||||||
# 设置扩展数据
|
# 设置扩展数据
|
||||||
main_task.ext_data = task_info
|
main_task.ext_data = task_info
|
||||||
|
|
||||||
span.set_attribute("render_task.sub_tasks", len(sub_tasks))
|
span.set_attribute("render_task.sub_tasks", len(sub_tasks))
|
||||||
span.set_attribute("render_task.effects", len(main_task.effects))
|
span.set_attribute("render_task.effects", len(main_task.effects))
|
||||||
|
|
||||||
return main_task
|
return main_task
|
||||||
|
|
||||||
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 = []
|
||||||
|
|
||||||
with ThreadPoolExecutor(max_workers=config.max_download_workers) as executor:
|
with ThreadPoolExecutor(max_workers=config.max_download_workers) as executor:
|
||||||
for param_list in task_params.values():
|
for param_list in task_params.values():
|
||||||
if isinstance(param_list, list):
|
if isinstance(param_list, list):
|
||||||
@@ -204,9 +227,11 @@ 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))
|
||||||
|
|
||||||
# 等待所有下载完成,并记录失败的下载
|
# 等待所有下载完成,并记录失败的下载
|
||||||
failed_downloads = []
|
failed_downloads = []
|
||||||
for future, url, filename in download_futures:
|
for future, url, filename in download_futures:
|
||||||
@@ -217,18 +242,21 @@ class DefaultTaskService(TaskService):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to download {url}: {e}")
|
logger.warning(f"Failed to download {url}: {e}")
|
||||||
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],
|
)
|
||||||
template_info: Dict[str, Any]) -> tuple[Optional[str], Dict[str, Any]]:
|
|
||||||
|
def _parse_video_source(
|
||||||
|
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 = {}
|
||||||
|
|
||||||
if isinstance(new_sources, list):
|
if isinstance(new_sources, list):
|
||||||
if len(new_sources) == 0:
|
if len(new_sources) == 0:
|
||||||
logger.debug("No video found for placeholder: %s", placeholder_id)
|
logger.debug("No video found for placeholder: %s", placeholder_id)
|
||||||
@@ -236,17 +264,18 @@ class DefaultTaskService(TaskService):
|
|||||||
else:
|
else:
|
||||||
pick_source = new_sources.pop(0)
|
pick_source = new_sources.pop(0)
|
||||||
new_sources = pick_source.get("url", "")
|
new_sources = pick_source.get("url", "")
|
||||||
|
|
||||||
if new_sources.startswith("http"):
|
if new_sources.startswith("http"):
|
||||||
_, source_name = os.path.split(new_sources)
|
_, source_name = os.path.split(new_sources)
|
||||||
oss.download_from_oss(new_sources, source_name, True)
|
oss.download_from_oss(new_sources, source_name, True)
|
||||||
return source_name, pick_source
|
return source_name, pick_source
|
||||||
return new_sources, pick_source
|
return new_sources, pick_source
|
||||||
|
|
||||||
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, [])
|
||||||
@@ -254,9 +283,14 @@ class DefaultTaskService(TaskService):
|
|||||||
return len(new_sources) >= required_count
|
return len(new_sources) >= required_count
|
||||||
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,36 +299,40 @@ 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,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 应用部分模板设置
|
# 应用部分模板设置
|
||||||
self._apply_template_settings(sub_task, part, template_info)
|
self._apply_template_settings(sub_task, part, template_info)
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
def _cleanup_temp_files(self, task: RenderTask):
|
def _cleanup_temp_files(self, task: RenderTask):
|
||||||
"""清理临时文件"""
|
"""清理临时文件"""
|
||||||
try:
|
try:
|
||||||
@@ -306,4 +344,4 @@ class DefaultTaskService(TaskService):
|
|||||||
else:
|
else:
|
||||||
logger.info("Skipped cleanup of template file: %s", task.output_file)
|
logger.info("Skipped cleanup of template file: %s", task.output_file)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
logger.warning("Failed to cleanup temp file %s: %s", task.output_file, e)
|
logger.warning("Failed to cleanup temp file %s: %s", task.output_file, e)
|
||||||
|
|||||||
@@ -6,67 +6,73 @@ 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):
|
||||||
"""模板服务抽象接口"""
|
"""模板服务抽象接口"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_template(self, template_id: str) -> Optional[Dict[str, Any]]:
|
def get_template(self, template_id: str) -> Optional[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
获取模板信息
|
获取模板信息
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
template_id: 模板ID
|
template_id: 模板ID
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict[str, Any]: 模板信息,如果不存在则返回None
|
Dict[str, Any]: 模板信息,如果不存在则返回None
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def load_local_templates(self):
|
def load_local_templates(self):
|
||||||
"""加载本地模板"""
|
"""加载本地模板"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def download_template(self, template_id: str) -> bool:
|
def download_template(self, template_id: str) -> bool:
|
||||||
"""
|
"""
|
||||||
下载模板
|
下载模板
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
template_id: 模板ID
|
template_id: 模板ID
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: 下载是否成功
|
bool: 下载是否成功
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def validate_template(self, template_info: Dict[str, Any]) -> bool:
|
def validate_template(self, template_info: Dict[str, Any]) -> bool:
|
||||||
"""
|
"""
|
||||||
验证模板
|
验证模板
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
template_info: 模板信息
|
template_info: 模板信息
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: 验证是否通过
|
bool: 验证是否通过
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class DefaultTemplateService(TemplateService):
|
class DefaultTemplateService(TemplateService):
|
||||||
"""默认模板服务实现"""
|
"""默认模板服务实现"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.templates: Dict[str, Dict[str, Any]] = {}
|
self.templates: Dict[str, Dict[str, Any]] = {}
|
||||||
self.storage_config = get_storage_config()
|
self.storage_config = get_storage_config()
|
||||||
|
|
||||||
def get_template(self, template_id: str) -> Optional[Dict[str, Any]]:
|
def get_template(self, template_id: str) -> Optional[Dict[str, Any]]:
|
||||||
"""获取模板信息"""
|
"""获取模板信息"""
|
||||||
if template_id not in self.templates:
|
if template_id not in self.templates:
|
||||||
@@ -74,193 +80,208 @@ class DefaultTemplateService(TemplateService):
|
|||||||
if not self.download_template(template_id):
|
if not self.download_template(template_id):
|
||||||
return None
|
return None
|
||||||
return self.templates.get(template_id)
|
return self.templates.get(template_id)
|
||||||
|
|
||||||
def load_local_templates(self):
|
def load_local_templates(self):
|
||||||
"""加载本地模板"""
|
"""加载本地模板"""
|
||||||
template_dir = self.storage_config.template_dir
|
template_dir = self.storage_config.template_dir
|
||||||
if not os.path.exists(template_dir):
|
if not os.path.exists(template_dir):
|
||||||
logger.warning("Template directory does not exist: %s", template_dir)
|
logger.warning("Template directory does not exist: %s", template_dir)
|
||||||
return
|
return
|
||||||
|
|
||||||
for template_name in os.listdir(template_dir):
|
for template_name in os.listdir(template_dir):
|
||||||
if template_name.startswith("_") or template_name.startswith("."):
|
if template_name.startswith("_") or template_name.startswith("."):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
target_path = os.path.join(template_dir, template_name)
|
target_path = os.path.join(template_dir, template_name)
|
||||||
if os.path.isdir(target_path):
|
if os.path.isdir(target_path):
|
||||||
try:
|
try:
|
||||||
self._load_template(template_name, target_path)
|
self._load_template(template_name, target_path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Failed to load template %s: %s", template_name, e)
|
logger.error("Failed to load template %s: %s", template_name, e)
|
||||||
|
|
||||||
def download_template(self, template_id: str) -> bool:
|
def download_template(self, template_id: str) -> bool:
|
||||||
"""下载模板"""
|
"""下载模板"""
|
||||||
tracer = get_tracer(__name__)
|
tracer = get_tracer(__name__)
|
||||||
with tracer.start_as_current_span("download_template") as span:
|
with tracer.start_as_current_span("download_template") as span:
|
||||||
try:
|
try:
|
||||||
span.set_attribute("template.id", template_id)
|
span.set_attribute("template.id", template_id)
|
||||||
|
|
||||||
# 获取远程模板信息
|
# 获取远程模板信息
|
||||||
template_info = api.get_template_info(template_id)
|
template_info = api.get_template_info(template_id)
|
||||||
if template_info is None:
|
if template_info is None:
|
||||||
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)
|
||||||
|
|
||||||
# 加载到内存
|
# 加载到内存
|
||||||
self._load_template(template_id, local_path)
|
self._load_template(template_id, local_path)
|
||||||
|
|
||||||
span.set_status(Status(StatusCode.OK))
|
span.set_status(Status(StatusCode.OK))
|
||||||
logger.info("Template downloaded successfully: %s", template_id)
|
logger.info("Template downloaded successfully: %s", template_id)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
span.set_status(Status(StatusCode.ERROR))
|
span.set_status(Status(StatusCode.ERROR))
|
||||||
logger.error("Failed to download template %s: %s", template_id, e)
|
logger.error("Failed to download template %s: %s", template_id, e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def validate_template(self, template_info: Dict[str, Any]) -> bool:
|
def validate_template(self, template_info: Dict[str, Any]) -> bool:
|
||||||
"""验证模板"""
|
"""验证模板"""
|
||||||
try:
|
try:
|
||||||
local_path = template_info.get("local_path")
|
local_path = template_info.get("local_path")
|
||||||
if not local_path:
|
if not local_path:
|
||||||
raise TemplateValidationError("Template missing local_path")
|
raise TemplateValidationError("Template missing local_path")
|
||||||
|
|
||||||
# 验证视频部分
|
# 验证视频部分
|
||||||
for video_part in template_info.get("video_parts", []):
|
for video_part in template_info.get("video_parts", []):
|
||||||
self._validate_template_part(video_part, local_path)
|
self._validate_template_part(video_part, local_path)
|
||||||
|
|
||||||
# 验证整体模板
|
# 验证整体模板
|
||||||
overall_template = template_info.get("overall_template", {})
|
overall_template = template_info.get("overall_template", {})
|
||||||
if overall_template:
|
if overall_template:
|
||||||
self._validate_template_part(overall_template, local_path)
|
self._validate_template_part(overall_template, local_path)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except TemplateValidationError:
|
except TemplateValidationError:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise TemplateValidationError(f"Template validation failed: {e}")
|
raise TemplateValidationError(f"Template validation failed: {e}")
|
||||||
|
|
||||||
def _load_template(self, template_name: str, local_path: str):
|
def _load_template(self, template_name: str, local_path: str):
|
||||||
"""加载单个模板"""
|
"""加载单个模板"""
|
||||||
logger.info("Loading template: %s (%s)", template_name, local_path)
|
logger.info("Loading template: %s (%s)", template_name, local_path)
|
||||||
|
|
||||||
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}")
|
||||||
|
|
||||||
template_info["local_path"] = local_path
|
template_info["local_path"] = local_path
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.validate_template(template_info)
|
self.validate_template(template_info)
|
||||||
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)
|
||||||
else:
|
else:
|
||||||
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)
|
||||||
oss.download_from_oss(source, new_file_path)
|
oss.download_from_oss(source, new_file_path)
|
||||||
|
|
||||||
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):
|
||||||
raise TemplateValidationError(f"Source file not found: {source_file}")
|
raise TemplateValidationError(f"Source file not found: {source_file}")
|
||||||
|
|
||||||
# 验证音频文件
|
# 验证音频文件
|
||||||
for audio in template_part.get("audios", []):
|
for audio in template_part.get("audios", []):
|
||||||
if not os.path.isabs(audio):
|
if not os.path.isabs(audio):
|
||||||
audio = os.path.join(base_dir, audio)
|
audio = os.path.join(base_dir, audio)
|
||||||
if not os.path.exists(audio):
|
if not os.path.exists(audio):
|
||||||
raise TemplateValidationError(f"Audio file not found: {audio}")
|
raise TemplateValidationError(f"Audio file not found: {audio}")
|
||||||
|
|
||||||
# 验证LUT文件
|
# 验证LUT文件
|
||||||
for lut in template_part.get("luts", []):
|
for lut in template_part.get("luts", []):
|
||||||
if not os.path.isabs(lut):
|
if not os.path.isabs(lut):
|
||||||
lut = os.path.join(base_dir, lut)
|
lut = os.path.join(base_dir, lut)
|
||||||
if not os.path.exists(lut):
|
if not os.path.exists(lut):
|
||||||
raise TemplateValidationError(f"LUT file not found: {lut}")
|
raise TemplateValidationError(f"LUT file not found: {lut}")
|
||||||
|
|
||||||
# 验证覆盖层文件
|
# 验证覆盖层文件
|
||||||
for overlay in template_part.get("overlays", []):
|
for overlay in template_part.get("overlays", []):
|
||||||
if not os.path.isabs(overlay):
|
if not os.path.isabs(overlay):
|
||||||
overlay = os.path.join(base_dir, overlay)
|
overlay = os.path.join(base_dir, overlay)
|
||||||
if not os.path.exists(overlay):
|
if not os.path.exists(overlay):
|
||||||
raise TemplateValidationError(f"Overlay file not found: {overlay}")
|
raise TemplateValidationError(f"Overlay file not found: {overlay}")
|
||||||
|
|||||||
@@ -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(
|
||||||
SERVICE_NAME: "RENDER_WORKER",
|
attributes={
|
||||||
SERVICE_VERSION: SOFTWARE_VERSION,
|
SERVICE_NAME: "RENDER_WORKER",
|
||||||
DEPLOYMENT_ENVIRONMENT: "Python",
|
SERVICE_VERSION: SOFTWARE_VERSION,
|
||||||
HOST_NAME: os.getenv("ACCESS_KEY"),
|
DEPLOYMENT_ENVIRONMENT: "Python",
|
||||||
})
|
HOST_NAME: os.getenv("ACCESS_KEY"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# 使用HTTP协议上报
|
# 使用HTTP协议上报
|
||||||
if batch:
|
if batch:
|
||||||
span_processor = BatchSpanProcessor(OTLPSpanHttpExporter(
|
span_processor = BatchSpanProcessor(
|
||||||
endpoint="https://oltp.jerryyan.top/v1/traces",
|
OTLPSpanHttpExporter(
|
||||||
))
|
endpoint="https://oltp.jerryyan.top/v1/traces",
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
span_processor = SimpleSpanProcessor(OTLPSpanHttpExporter(
|
span_processor = SimpleSpanProcessor(
|
||||||
endpoint="https://oltp.jerryyan.top/v1/traces",
|
OTLPSpanHttpExporter(
|
||||||
))
|
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)
|
||||||
|
|||||||
227
util/api.py
227
util/api.py
@@ -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(
|
||||||
json={
|
os.getenv("API_ENDPOINT"), task_info.get("id")
|
||||||
'accessKey': os.getenv('ACCESS_KEY'),
|
),
|
||||||
}, timeout=10)
|
)
|
||||||
|
response = session.post(
|
||||||
|
"{0}/{1}/uploadUrl".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()
|
||||||
@@ -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", {})
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
141
util/ffmpeg.py
141
util/ffmpeg.py
@@ -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(
|
||||||
*(set() if has_audio else MUTE_AUDIO_INPUT),
|
[
|
||||||
"-fps_mode", "cfr",
|
"ffmpeg",
|
||||||
"-map", "0:v", "-map", "0:a" if has_audio else "1:a",
|
"-y",
|
||||||
*_video_args, "-bsf:v", get_mp4toannexb_filter(),
|
"-hide_banner",
|
||||||
*AUDIO_ARGS, "-bsf:a", "setts=pts=DTS",
|
"-i",
|
||||||
*_encoder_args, "-shortest", "-fflags", "+genpts",
|
file,
|
||||||
"-f", "mpegts", file + ".ts"])
|
*(set() if has_audio else MUTE_AUDIO_INPUT),
|
||||||
|
"-fps_mode",
|
||||||
|
"cfr",
|
||||||
|
"-map",
|
||||||
|
"0:v",
|
||||||
|
"-map",
|
||||||
|
"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
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
"""
|
"""
|
||||||
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参数
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
基础参数列表
|
基础参数列表
|
||||||
"""
|
"""
|
||||||
@@ -20,40 +22,45 @@ 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]:
|
||||||
"""
|
"""
|
||||||
构建空音频输入参数
|
构建空音频输入参数
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
空音频输入参数列表
|
空音频输入参数列表
|
||||||
"""
|
"""
|
||||||
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:
|
||||||
"""
|
"""
|
||||||
构建音频混合滤镜
|
构建音频混合滤镜
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
input1: 第一个音频输入
|
input1: 第一个音频输入
|
||||||
input2: 第二个音频输入
|
input2: 第二个音频输入
|
||||||
output: 输出流名称
|
output: 输出流名称
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
混合滤镜字符串
|
混合滤镜字符串
|
||||||
"""
|
"""
|
||||||
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:
|
||||||
"""
|
"""
|
||||||
构建覆盖层缩放滤镜
|
构建覆盖层缩放滤镜
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
video_input: 视频输入流
|
video_input: 视频输入流
|
||||||
overlay_input: 覆盖层输入流
|
overlay_input: 覆盖层输入流
|
||||||
output: 输出流名称
|
output: 输出流名称
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
缩放滤镜字符串
|
缩放滤镜字符串
|
||||||
"""
|
"""
|
||||||
@@ -61,12 +68,15 @@ 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:
|
||||||
"""
|
"""
|
||||||
获取annexb转换滤镜
|
获取annexb转换滤镜
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
annexb滤镜名称
|
annexb滤镜名称
|
||||||
"""
|
"""
|
||||||
@@ -76,10 +86,11 @@ 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]:
|
||||||
"""
|
"""
|
||||||
构建标准输出参数
|
构建标准输出参数
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
输出参数列表
|
输出参数列表
|
||||||
"""
|
"""
|
||||||
@@ -88,40 +99,63 @@ 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支持的格式
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
file_path: 文件路径
|
file_path: 文件路径
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
是否为支持的格式
|
是否为支持的格式
|
||||||
"""
|
"""
|
||||||
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:
|
||||||
"""
|
"""
|
||||||
估算处理时间
|
估算处理时间
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
input_duration: 输入文件时长(秒)
|
input_duration: 输入文件时长(秒)
|
||||||
complexity_factor: 复杂度因子(1.0为普通处理)
|
complexity_factor: 复杂度因子(1.0为普通处理)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
预估处理时间(秒)
|
预估处理时间(秒)
|
||||||
"""
|
"""
|
||||||
# 基础处理速度假设为实时的0.5倍(即处理1秒视频需要2秒)
|
# 基础处理速度假设为实时的0.5倍(即处理1秒视频需要2秒)
|
||||||
base_processing_ratio = 2.0
|
base_processing_ratio = 2.0
|
||||||
return input_duration * base_processing_ratio * complexity_factor
|
return input_duration * base_processing_ratio * complexity_factor
|
||||||
|
|||||||
@@ -1,41 +1,46 @@
|
|||||||
"""
|
"""
|
||||||
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字符串
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
json_str: JSON字符串
|
json_str: JSON字符串
|
||||||
default: 解析失败时返回的默认值
|
default: 解析失败时返回的默认值
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
解析后的对象,或默认值
|
解析后的对象,或默认值
|
||||||
"""
|
"""
|
||||||
if not json_str or json_str == '{}':
|
if not json_str or json_str == "{}":
|
||||||
return default or {}
|
return default or {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return json.loads(json_str)
|
return json.loads(json_str)
|
||||||
except (json.JSONDecodeError, TypeError) as e:
|
except (json.JSONDecodeError, TypeError) as e:
|
||||||
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字符串
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj: 要序列化的对象
|
obj: 要序列化的对象
|
||||||
indent: 缩进空格数
|
indent: 缩进空格数
|
||||||
ensure_ascii: 是否确保ASCII编码
|
ensure_ascii: 是否确保ASCII编码
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
JSON字符串
|
JSON字符串
|
||||||
"""
|
"""
|
||||||
@@ -45,43 +50,45 @@ 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:
|
||||||
"""
|
"""
|
||||||
从嵌套字典中安全获取值
|
从嵌套字典中安全获取值
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
data: 字典数据
|
data: 字典数据
|
||||||
key_path: 键路径,用点分隔(如 "user.profile.name")
|
key_path: 键路径,用点分隔(如 "user.profile.name")
|
||||||
default: 默认值
|
default: 默认值
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
找到的值或默认值
|
找到的值或默认值
|
||||||
"""
|
"""
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
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:
|
||||||
if isinstance(current, dict) and key in current:
|
if isinstance(current, dict) and key in current:
|
||||||
current = current[key]
|
current = current[key]
|
||||||
else:
|
else:
|
||||||
return default
|
return default
|
||||||
|
|
||||||
return current
|
return current
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
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]:
|
||||||
"""
|
"""
|
||||||
合并多个字典,后面的字典会覆盖前面的字典中相同的键
|
合并多个字典,后面的字典会覆盖前面的字典中相同的键
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
*dicts: 要合并的字典
|
*dicts: 要合并的字典
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
合并后的字典
|
合并后的字典
|
||||||
"""
|
"""
|
||||||
@@ -89,4 +96,4 @@ def merge_dicts(*dicts: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
for d in dicts:
|
for d in dicts:
|
||||||
if isinstance(d, dict):
|
if isinstance(d, dict):
|
||||||
result.update(d)
|
result.update(d)
|
||||||
return result
|
return result
|
||||||
|
|||||||
40
util/oss.py
40
util/oss.py
@@ -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
|
||||||
|
|
||||||
@@ -83,11 +95,11 @@ def download_from_oss(url, file_path, skip_if_exist=None):
|
|||||||
with tracer.start_as_current_span("download_from_oss") as span:
|
with tracer.start_as_current_span("download_from_oss") as span:
|
||||||
span.set_attribute("file.url", url)
|
span.set_attribute("file.url", url)
|
||||||
span.set_attribute("file.path", file_path)
|
span.set_attribute("file.path", file_path)
|
||||||
|
|
||||||
# 如果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)
|
||||||
span.set_attribute("file.size", os.path.getsize(file_path))
|
span.set_attribute("file.size", os.path.getsize(file_path))
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user