You've already forked FrameTour-RenderWorker
mypy
This commit is contained in:
227
util/api.py
227
util/api.py
@@ -21,15 +21,11 @@ retry_strategy = Retry(
|
||||
total=3,
|
||||
status_forcelist=[429, 500, 502, 503, 504],
|
||||
backoff_factor=1,
|
||||
respect_retry_after_header=True
|
||||
respect_retry_after_header=True,
|
||||
)
|
||||
|
||||
# 配置HTTP适配器(连接池)
|
||||
adapter = HTTPAdapter(
|
||||
pool_connections=10,
|
||||
pool_maxsize=20,
|
||||
max_retries=retry_strategy
|
||||
)
|
||||
adapter = HTTPAdapter(pool_connections=10, pool_maxsize=20, max_retries=retry_strategy)
|
||||
|
||||
session.mount("http://", adapter)
|
||||
session.mount("https://", adapter)
|
||||
@@ -51,23 +47,30 @@ def sync_center():
|
||||
:return: 任务列表
|
||||
"""
|
||||
from services import DefaultTemplateService
|
||||
|
||||
template_service = DefaultTemplateService()
|
||||
try:
|
||||
response = session.post(os.getenv('API_ENDPOINT') + "/sync", json={
|
||||
'accessKey': os.getenv('ACCESS_KEY'),
|
||||
'clientStatus': util.system.get_sys_info(),
|
||||
'templateList': [{'id': t.get('id', ''), 'updateTime': t.get('updateTime', '')} for t in
|
||||
template_service.templates.values()]
|
||||
}, timeout=10)
|
||||
response = session.post(
|
||||
os.getenv("API_ENDPOINT") + "/sync",
|
||||
json={
|
||||
"accessKey": os.getenv("ACCESS_KEY"),
|
||||
"clientStatus": util.system.get_sys_info(),
|
||||
"templateList": [
|
||||
{"id": t.get("id", ""), "updateTime": t.get("updateTime", "")}
|
||||
for t in template_service.templates.values()
|
||||
],
|
||||
},
|
||||
timeout=10,
|
||||
)
|
||||
response.raise_for_status()
|
||||
except requests.RequestException as e:
|
||||
logger.error("请求失败!", e)
|
||||
return []
|
||||
data = response.json()
|
||||
logger.debug("获取任务结果:【%s】", data)
|
||||
if data.get('code', 0) == 200:
|
||||
templates = data.get('data', {}).get('templates', [])
|
||||
tasks = data.get('data', {}).get('tasks', [])
|
||||
if data.get("code", 0) == 200:
|
||||
templates = data.get("data", {}).get("templates", [])
|
||||
tasks = data.get("data", {}).get("tasks", [])
|
||||
else:
|
||||
tasks = []
|
||||
templates = []
|
||||
@@ -75,12 +78,16 @@ def sync_center():
|
||||
if os.getenv("REDIRECT_TO_URL", False) != False:
|
||||
for task in tasks:
|
||||
_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')}"
|
||||
threading.Thread(target=requests.post, args=(url,)).start()
|
||||
return []
|
||||
for template in templates:
|
||||
template_id = template.get('id', '')
|
||||
template_id = template.get("id", "")
|
||||
if template_id:
|
||||
logger.info("更新模板:【%s】", 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:
|
||||
try:
|
||||
req_span.set_attribute("http.method", "POST")
|
||||
req_span.set_attribute("http.url", '{0}/template/{1}'.format(os.getenv('API_ENDPOINT'), template_id))
|
||||
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.url",
|
||||
"{0}/template/{1}".format(os.getenv("API_ENDPOINT"), template_id),
|
||||
)
|
||||
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.response", response.text)
|
||||
response.raise_for_status()
|
||||
@@ -113,64 +127,68 @@ def get_template_info(template_id):
|
||||
return None
|
||||
data = response.json()
|
||||
logger.debug("获取模板信息结果:【%s】", data)
|
||||
remote_template_info = data.get('data', {})
|
||||
remote_template_info = data.get("data", {})
|
||||
if not remote_template_info:
|
||||
logger.warning("获取模板信息结果为空", data)
|
||||
return None
|
||||
template = {
|
||||
'id': template_id,
|
||||
'updateTime': remote_template_info.get('updateTime', template_id),
|
||||
'scenic_name': remote_template_info.get('scenicName', '景区'),
|
||||
'name': remote_template_info.get('name', '模版'),
|
||||
'video_size': remote_template_info.get('resolution', '1920x1080'),
|
||||
'frame_rate': 25,
|
||||
'overall_duration': 30,
|
||||
'video_parts': [
|
||||
|
||||
]
|
||||
"id": template_id,
|
||||
"updateTime": remote_template_info.get("updateTime", template_id),
|
||||
"scenic_name": remote_template_info.get("scenicName", "景区"),
|
||||
"name": remote_template_info.get("name", "模版"),
|
||||
"video_size": remote_template_info.get("resolution", "1920x1080"),
|
||||
"frame_rate": 25,
|
||||
"overall_duration": 30,
|
||||
"video_parts": [],
|
||||
}
|
||||
|
||||
def _template_normalizer(template_info):
|
||||
_template = {}
|
||||
_placeholder_type = template_info.get('isPlaceholder', -1)
|
||||
_placeholder_type = template_info.get("isPlaceholder", -1)
|
||||
if _placeholder_type == 0:
|
||||
# 固定视频
|
||||
_template['source'] = template_info.get('sourceUrl', '')
|
||||
_template["source"] = template_info.get("sourceUrl", "")
|
||||
elif _placeholder_type == 1:
|
||||
# 占位符
|
||||
_template['source'] = "PLACEHOLDER_" + template_info.get('sourceUrl', '')
|
||||
_template['mute'] = template_info.get('mute', True)
|
||||
_template['crop_mode'] = template_info.get('cropEnable', None)
|
||||
_template['zoom_cut'] = template_info.get('zoomCut', None)
|
||||
_template["source"] = "PLACEHOLDER_" + template_info.get(
|
||||
"sourceUrl", ""
|
||||
)
|
||||
_template["mute"] = template_info.get("mute", True)
|
||||
_template["crop_mode"] = template_info.get("cropEnable", None)
|
||||
_template["zoom_cut"] = template_info.get("zoomCut", None)
|
||||
else:
|
||||
_template['source'] = None
|
||||
_overlays = template_info.get('overlays', '')
|
||||
_template["source"] = None
|
||||
_overlays = template_info.get("overlays", "")
|
||||
if _overlays:
|
||||
_template['overlays'] = _overlays.split(",")
|
||||
_audios = template_info.get('audios', '')
|
||||
_template["overlays"] = _overlays.split(",")
|
||||
_audios = template_info.get("audios", "")
|
||||
if _audios:
|
||||
_template['audios'] = _audios.split(",")
|
||||
_luts = template_info.get('luts', '')
|
||||
_template["audios"] = _audios.split(",")
|
||||
_luts = template_info.get("luts", "")
|
||||
if _luts:
|
||||
_template['luts'] = _luts.split(",")
|
||||
_only_if = template_info.get('onlyIf', '')
|
||||
_template["luts"] = _luts.split(",")
|
||||
_only_if = template_info.get("onlyIf", "")
|
||||
if _only_if:
|
||||
_template['only_if'] = _only_if
|
||||
_effects = template_info.get('effects', '')
|
||||
_template["only_if"] = _only_if
|
||||
_effects = template_info.get("effects", "")
|
||||
if _effects:
|
||||
_template['effects'] = _effects.split("|")
|
||||
_template["effects"] = _effects.split("|")
|
||||
return _template
|
||||
|
||||
# outer template definition
|
||||
overall_template = _template_normalizer(remote_template_info)
|
||||
template['overall_template'] = overall_template
|
||||
template["overall_template"] = overall_template
|
||||
# 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:
|
||||
parts = _template_normalizer(children_template)
|
||||
template['video_parts'].append(parts)
|
||||
template['local_path'] = os.path.join(os.getenv('TEMPLATE_DIR'), str(template_id))
|
||||
with get_tracer("api").start_as_current_span("get_template_info.template") as res_span:
|
||||
template["video_parts"].append(parts)
|
||||
template["local_path"] = os.path.join(
|
||||
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))
|
||||
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:
|
||||
try:
|
||||
req_span.set_attribute("http.method", "POST")
|
||||
req_span.set_attribute("http.url",
|
||||
'{0}/{1}/success'.format(os.getenv('API_ENDPOINT'), task_info.get("id")))
|
||||
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.url",
|
||||
"{0}/{1}/success".format(
|
||||
os.getenv("API_ENDPOINT"), task_info.get("id")
|
||||
),
|
||||
)
|
||||
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.response", response.text)
|
||||
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:
|
||||
try:
|
||||
req_span.set_attribute("http.method", "POST")
|
||||
req_span.set_attribute("http.url",
|
||||
'{0}/{1}/start'.format(os.getenv('API_ENDPOINT'), task_info.get("id")))
|
||||
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.url",
|
||||
"{0}/{1}/start".format(
|
||||
os.getenv("API_ENDPOINT"), task_info.get("id")
|
||||
),
|
||||
)
|
||||
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.response", response.text)
|
||||
response.raise_for_status()
|
||||
@@ -218,7 +253,7 @@ def report_task_start(task_info):
|
||||
return None
|
||||
|
||||
|
||||
def report_task_failed(task_info, reason=''):
|
||||
def report_task_failed(task_info, reason=""):
|
||||
tracer = get_tracer(__name__)
|
||||
with tracer.start_as_current_span("report_task_failed") as span:
|
||||
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:
|
||||
try:
|
||||
req_span.set_attribute("http.method", "POST")
|
||||
req_span.set_attribute("http.url",
|
||||
'{0}/{1}/fail'.format(os.getenv('API_ENDPOINT'), task_info.get("id")))
|
||||
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.url",
|
||||
"{0}/{1}/fail".format(
|
||||
os.getenv("API_ENDPOINT"), task_info.get("id")
|
||||
),
|
||||
)
|
||||
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.response", response.text)
|
||||
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:
|
||||
logger.info("开始上传文件: %s", 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:
|
||||
req_span.set_attribute("http.method", "POST")
|
||||
req_span.set_attribute("http.url",
|
||||
'{0}/{1}/uploadUrl'.format(os.getenv('API_ENDPOINT'), task_info.get("id")))
|
||||
response = session.post('{0}/{1}/uploadUrl'.format(os.getenv('API_ENDPOINT'), task_info.get("id")),
|
||||
json={
|
||||
'accessKey': os.getenv('ACCESS_KEY'),
|
||||
}, timeout=10)
|
||||
req_span.set_attribute(
|
||||
"http.url",
|
||||
"{0}/{1}/uploadUrl".format(
|
||||
os.getenv("API_ENDPOINT"), task_info.get("id")
|
||||
),
|
||||
)
|
||||
response = session.post(
|
||||
"{0}/{1}/uploadUrl".format(
|
||||
os.getenv("API_ENDPOINT"), task_info.get("id")
|
||||
),
|
||||
json={
|
||||
"accessKey": os.getenv("ACCESS_KEY"),
|
||||
},
|
||||
timeout=10,
|
||||
)
|
||||
req_span.set_attribute("http.status_code", response.status_code)
|
||||
req_span.set_attribute("http.response", response.text)
|
||||
response.raise_for_status()
|
||||
@@ -267,21 +320,25 @@ def upload_task_file(task_info, ffmpeg_task):
|
||||
logger.error("请求失败!", e)
|
||||
return False
|
||||
data = response.json()
|
||||
url = data.get('data', "")
|
||||
url = data.get("data", "")
|
||||
logger.info("开始上传文件: %s 至 %s", task_info.get("id"), url)
|
||||
return oss.upload_to_oss(url, ffmpeg_task.get_output_file())
|
||||
|
||||
|
||||
def get_task_info(id):
|
||||
try:
|
||||
response = session.get(os.getenv('API_ENDPOINT') + "/" + id + "/info", params={
|
||||
'accessKey': os.getenv('ACCESS_KEY'),
|
||||
}, timeout=10)
|
||||
response = session.get(
|
||||
os.getenv("API_ENDPOINT") + "/" + id + "/info",
|
||||
params={
|
||||
"accessKey": os.getenv("ACCESS_KEY"),
|
||||
},
|
||||
timeout=10,
|
||||
)
|
||||
response.raise_for_status()
|
||||
except requests.RequestException as e:
|
||||
logger.error("请求失败!", e)
|
||||
return []
|
||||
data = response.json()
|
||||
logger.debug("获取任务结果:【%s】", data)
|
||||
if data.get('code', 0) == 200:
|
||||
return data.get('data', {})
|
||||
if data.get("code", 0) == 200:
|
||||
return data.get("data", {})
|
||||
|
||||
@@ -1,72 +1,111 @@
|
||||
class RenderWorkerError(Exception):
|
||||
"""RenderWorker基础异常类"""
|
||||
|
||||
def __init__(self, message: str, error_code: str = None):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
self.error_code = error_code or self.__class__.__name__
|
||||
|
||||
|
||||
class ConfigurationError(RenderWorkerError):
|
||||
"""配置错误"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TemplateError(RenderWorkerError):
|
||||
"""模板相关错误"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TemplateNotFoundError(TemplateError):
|
||||
"""模板未找到错误"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TemplateValidationError(TemplateError):
|
||||
"""模板验证错误"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TaskError(RenderWorkerError):
|
||||
"""任务处理错误"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TaskValidationError(TaskError):
|
||||
"""任务参数验证错误"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class RenderError(RenderWorkerError):
|
||||
"""渲染处理错误"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class FFmpegError(RenderError):
|
||||
"""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)
|
||||
self.command = command
|
||||
self.return_code = return_code
|
||||
self.stderr = stderr
|
||||
|
||||
|
||||
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)
|
||||
self.effect_name = effect_name
|
||||
self.effect_params = effect_params
|
||||
|
||||
|
||||
class StorageError(RenderWorkerError):
|
||||
"""存储相关错误"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class APIError(RenderWorkerError):
|
||||
"""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)
|
||||
self.status_code = status_code
|
||||
self.response_body = response_body
|
||||
|
||||
|
||||
class ResourceError(RenderWorkerError):
|
||||
"""资源相关错误"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ResourceNotFoundError(ResourceError):
|
||||
"""资源未找到错误"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
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 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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def re_encode_and_annexb(file):
|
||||
with get_tracer("ffmpeg").start_as_current_span("re_encode_and_annexb") as span:
|
||||
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(" "))
|
||||
else:
|
||||
_encoder_args = ENCODER_ARGS
|
||||
ffmpeg_process = subprocess.run(["ffmpeg", "-y", "-hide_banner", "-i", file,
|
||||
*(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"])
|
||||
ffmpeg_process = subprocess.run(
|
||||
[
|
||||
"ffmpeg",
|
||||
"-y",
|
||||
"-hide_banner",
|
||||
"-i",
|
||||
file,
|
||||
*(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))
|
||||
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)
|
||||
if ffmpeg_process.returncode == 0:
|
||||
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)
|
||||
return file+".ts"
|
||||
return file + ".ts"
|
||||
else:
|
||||
span.set_status(Status(StatusCode.ERROR))
|
||||
return file
|
||||
|
||||
|
||||
# start_render函数已迁移到services/render_service.py中的DefaultRenderService
|
||||
# 保留原有签名用于向后兼容,但建议使用新的服务架构
|
||||
|
||||
|
||||
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
|
||||
|
||||
render_service = DefaultRenderService()
|
||||
return render_service.render(ffmpeg_task)
|
||||
|
||||
|
||||
def handle_ffmpeg_output(stdout: Optional[bytes]) -> str:
|
||||
out_time = "0:0:0.0"
|
||||
if stdout is None:
|
||||
@@ -81,7 +118,8 @@ def handle_ffmpeg_output(stdout: Optional[bytes]) -> str:
|
||||
if line.startswith(b"speed="):
|
||||
speed = line.replace(b"speed=", b"").decode().strip()
|
||||
print("[ ]Speed:", out_time, "@", speed)
|
||||
return out_time+"@"+speed
|
||||
return out_time + "@" + speed
|
||||
|
||||
|
||||
def duration_str_to_float(duration_str: str) -> float:
|
||||
_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)
|
||||
# 获取宽度和高度
|
||||
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,
|
||||
**subprocess_args(True)
|
||||
)
|
||||
@@ -104,14 +152,14 @@ def probe_video_info(video_file):
|
||||
if result.returncode != 0:
|
||||
span.set_status(Status(StatusCode.ERROR))
|
||||
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)
|
||||
if all_result == '':
|
||||
if all_result == "":
|
||||
span.set_status(Status(StatusCode.ERROR))
|
||||
return 0, 0, 0
|
||||
span.set_status(Status(StatusCode.OK))
|
||||
wh, duration = all_result.split('\n')
|
||||
width, height = wh.strip().split('x')
|
||||
wh, duration = all_result.split("\n")
|
||||
width, height = wh.strip().split("x")
|
||||
return int(width), int(height), float(duration)
|
||||
|
||||
|
||||
@@ -119,8 +167,19 @@ def probe_video_audio(video_file, type=None):
|
||||
tracer = get_tracer(__name__)
|
||||
with tracer.start_as_current_span("probe_video_audio") as span:
|
||||
span.set_attribute("video.file", video_file)
|
||||
args = ["ffprobe", "-hide_banner", "-v", "error", "-select_streams", "a", "-show_entries", "stream=index", "-of", "csv=p=0"]
|
||||
if type == 'concat':
|
||||
args = [
|
||||
"ffprobe",
|
||||
"-hide_banner",
|
||||
"-v",
|
||||
"error",
|
||||
"-select_streams",
|
||||
"a",
|
||||
"-show_entries",
|
||||
"stream=index",
|
||||
"-of",
|
||||
"csv=p=0",
|
||||
]
|
||||
if type == "concat":
|
||||
args.append("-safe")
|
||||
args.append("0")
|
||||
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))
|
||||
span.set_attribute("ffprobe.args", json.dumps(result.args))
|
||||
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:
|
||||
return False
|
||||
if result.stdout.decode('utf-8').strip() == '':
|
||||
if result.stdout.decode("utf-8").strip() == "":
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
# 音频淡出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:
|
||||
try:
|
||||
duration = float(duration)
|
||||
@@ -157,7 +216,25 @@ def fade_out_audio(file, duration, fade_out_sec = 2):
|
||||
os.remove(new_fn)
|
||||
logger.info("delete tmp file: " + new_fn)
|
||||
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))
|
||||
logger.info(" ".join(process.args))
|
||||
if process.returncode != 0:
|
||||
@@ -173,7 +250,6 @@ def fade_out_audio(file, duration, fade_out_sec = 2):
|
||||
return file
|
||||
|
||||
|
||||
|
||||
# Create a set of arguments which make a ``subprocess.Popen`` (and
|
||||
# variants) call work with or without Pyinstaller, ``--noconsole`` or
|
||||
# not, on Windows and Linux. Typical use::
|
||||
@@ -186,7 +262,7 @@ def fade_out_audio(file, duration, fade_out_sec = 2):
|
||||
# **subprocess_args(False))
|
||||
def subprocess_args(include_stdout=True):
|
||||
# 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
|
||||
# when run from Pyinstaller with the ``--noconsole`` option. Avoid this
|
||||
# distraction.
|
||||
@@ -210,7 +286,7 @@ def subprocess_args(include_stdout=True):
|
||||
#
|
||||
# So, add it only if it's needed.
|
||||
if include_stdout:
|
||||
ret = {'stdout': subprocess.PIPE}
|
||||
ret = {"stdout": subprocess.PIPE}
|
||||
else:
|
||||
ret = {}
|
||||
|
||||
@@ -218,8 +294,5 @@ def subprocess_args(include_stdout=True):
|
||||
# with the ``--noconsole`` option requires redirecting everything
|
||||
# (stdin, stdout, stderr) to avoid an OSError exception
|
||||
# "[Error 6] the handle is invalid."
|
||||
ret.update({'stdin': subprocess.PIPE,
|
||||
'startupinfo': si,
|
||||
'env': env})
|
||||
ret.update({"stdin": subprocess.PIPE, "startupinfo": si, "env": env})
|
||||
return ret
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
"""
|
||||
FFmpeg工具模块 - 提供FFmpeg命令构建和处理的公共函数
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List, Tuple, Optional
|
||||
from config.settings import get_ffmpeg_config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def build_base_ffmpeg_args() -> List[str]:
|
||||
"""
|
||||
构建基础FFmpeg参数
|
||||
|
||||
|
||||
Returns:
|
||||
基础参数列表
|
||||
"""
|
||||
@@ -20,40 +22,45 @@ def build_base_ffmpeg_args() -> List[str]:
|
||||
args.extend(config.loglevel_args)
|
||||
return args
|
||||
|
||||
|
||||
def build_null_audio_input() -> List[str]:
|
||||
"""
|
||||
构建空音频输入参数
|
||||
|
||||
|
||||
Returns:
|
||||
空音频输入参数列表
|
||||
"""
|
||||
config = get_ffmpeg_config()
|
||||
return config.null_audio_args
|
||||
|
||||
|
||||
def build_amix_filter(input1: str, input2: str, output: str) -> str:
|
||||
"""
|
||||
构建音频混合滤镜
|
||||
|
||||
|
||||
Args:
|
||||
input1: 第一个音频输入
|
||||
input2: 第二个音频输入
|
||||
input2: 第二个音频输入
|
||||
output: 输出流名称
|
||||
|
||||
|
||||
Returns:
|
||||
混合滤镜字符串
|
||||
"""
|
||||
config = get_ffmpeg_config()
|
||||
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:
|
||||
video_input: 视频输入流
|
||||
overlay_input: 覆盖层输入流
|
||||
output: 输出流名称
|
||||
|
||||
|
||||
Returns:
|
||||
缩放滤镜字符串
|
||||
"""
|
||||
@@ -61,12 +68,15 @@ def build_overlay_scale_filter(video_input: str, overlay_input: str, output: str
|
||||
if config.overlay_scale_mode == "scale":
|
||||
return f"{video_input}[{overlay_input}]scale=iw:ih[{output}]"
|
||||
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:
|
||||
"""
|
||||
获取annexb转换滤镜
|
||||
|
||||
|
||||
Returns:
|
||||
annexb滤镜名称
|
||||
"""
|
||||
@@ -76,10 +86,11 @@ def get_annexb_filter() -> str:
|
||||
return "hevc_mp4toannexb"
|
||||
return "h264_mp4toannexb"
|
||||
|
||||
|
||||
def build_standard_output_args() -> List[str]:
|
||||
"""
|
||||
构建标准输出参数
|
||||
|
||||
|
||||
Returns:
|
||||
输出参数列表
|
||||
"""
|
||||
@@ -88,40 +99,63 @@ def build_standard_output_args() -> List[str]:
|
||||
*config.video_args,
|
||||
*config.audio_args,
|
||||
*config.encoder_args,
|
||||
*config.default_args
|
||||
*config.default_args,
|
||||
]
|
||||
|
||||
|
||||
def validate_ffmpeg_file_extensions(file_path: str) -> bool:
|
||||
"""
|
||||
验证文件扩展名是否为FFmpeg支持的格式
|
||||
|
||||
|
||||
Args:
|
||||
file_path: 文件路径
|
||||
|
||||
|
||||
Returns:
|
||||
是否为支持的格式
|
||||
"""
|
||||
supported_extensions = {
|
||||
'.mp4', '.avi', '.mov', '.mkv', '.flv', '.wmv', '.webm',
|
||||
'.ts', '.m2ts', '.mts', '.m4v', '.3gp', '.asf', '.rm',
|
||||
'.mp3', '.wav', '.aac', '.flac', '.ogg', '.m4a', '.wma'
|
||||
".mp4",
|
||||
".avi",
|
||||
".mov",
|
||||
".mkv",
|
||||
".flv",
|
||||
".wmv",
|
||||
".webm",
|
||||
".ts",
|
||||
".m2ts",
|
||||
".mts",
|
||||
".m4v",
|
||||
".3gp",
|
||||
".asf",
|
||||
".rm",
|
||||
".mp3",
|
||||
".wav",
|
||||
".aac",
|
||||
".flac",
|
||||
".ogg",
|
||||
".m4a",
|
||||
".wma",
|
||||
}
|
||||
|
||||
|
||||
import os
|
||||
|
||||
_, ext = os.path.splitext(file_path.lower())
|
||||
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:
|
||||
input_duration: 输入文件时长(秒)
|
||||
complexity_factor: 复杂度因子(1.0为普通处理)
|
||||
|
||||
|
||||
Returns:
|
||||
预估处理时间(秒)
|
||||
"""
|
||||
# 基础处理速度假设为实时的0.5倍(即处理1秒视频需要2秒)
|
||||
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解析和处理功能
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Dict, Any, Optional, Union
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def safe_json_loads(json_str: Union[str, bytes], default: Any = None) -> Any:
|
||||
"""
|
||||
安全解析JSON字符串
|
||||
|
||||
|
||||
Args:
|
||||
json_str: JSON字符串
|
||||
default: 解析失败时返回的默认值
|
||||
|
||||
|
||||
Returns:
|
||||
解析后的对象,或默认值
|
||||
"""
|
||||
if not json_str or json_str == '{}':
|
||||
if not json_str or json_str == "{}":
|
||||
return default or {}
|
||||
|
||||
|
||||
try:
|
||||
return json.loads(json_str)
|
||||
except (json.JSONDecodeError, TypeError) as e:
|
||||
logger.warning(f"Failed to parse JSON: {e}, input: {json_str}")
|
||||
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字符串
|
||||
|
||||
|
||||
Args:
|
||||
obj: 要序列化的对象
|
||||
indent: 缩进空格数
|
||||
ensure_ascii: 是否确保ASCII编码
|
||||
|
||||
|
||||
Returns:
|
||||
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}")
|
||||
return "{}"
|
||||
|
||||
|
||||
def get_nested_value(data: Dict[str, Any], key_path: str, default: Any = None) -> Any:
|
||||
"""
|
||||
从嵌套字典中安全获取值
|
||||
|
||||
|
||||
Args:
|
||||
data: 字典数据
|
||||
key_path: 键路径,用点分隔(如 "user.profile.name")
|
||||
default: 默认值
|
||||
|
||||
|
||||
Returns:
|
||||
找到的值或默认值
|
||||
"""
|
||||
if not isinstance(data, dict):
|
||||
return default
|
||||
|
||||
|
||||
try:
|
||||
keys = key_path.split('.')
|
||||
keys = key_path.split(".")
|
||||
current = data
|
||||
|
||||
|
||||
for key in keys:
|
||||
if isinstance(current, dict) and key in current:
|
||||
current = current[key]
|
||||
else:
|
||||
return default
|
||||
|
||||
|
||||
return current
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get nested value for path '{key_path}': {e}")
|
||||
return default
|
||||
|
||||
|
||||
def merge_dicts(*dicts: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
合并多个字典,后面的字典会覆盖前面的字典中相同的键
|
||||
|
||||
|
||||
Args:
|
||||
*dicts: 要合并的字典
|
||||
|
||||
|
||||
Returns:
|
||||
合并后的字典
|
||||
"""
|
||||
@@ -89,4 +96,4 @@ def merge_dicts(*dicts: Dict[str, Any]) -> Dict[str, Any]:
|
||||
for d in dicts:
|
||||
if isinstance(d, dict):
|
||||
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 != "":
|
||||
replace_list = [i.split("|", 1) for i in replace_map.split(",")]
|
||||
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.split("?", 1)[0]
|
||||
r_span.set_attribute("rclone.target_dir", new_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)
|
||||
if result == 0:
|
||||
span.set_status(Status(StatusCode.OK))
|
||||
@@ -49,8 +51,14 @@ def upload_to_oss(url, file_path):
|
||||
try:
|
||||
req_span.set_attribute("http.method", "PUT")
|
||||
req_span.set_attribute("http.url", url)
|
||||
with open(file_path, 'rb') as f:
|
||||
response = requests.put(url, data=f, stream=True, timeout=60, headers={"Content-Type": "video/mp4"})
|
||||
with open(file_path, "rb") as f:
|
||||
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.response", response.text)
|
||||
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_status(Status(StatusCode.ERROR))
|
||||
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:
|
||||
req_span.set_attribute("http.error", str(e))
|
||||
req_span.set_status(Status(StatusCode.ERROR))
|
||||
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))
|
||||
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:
|
||||
span.set_attribute("file.url", url)
|
||||
span.set_attribute("file.path", file_path)
|
||||
|
||||
|
||||
# 如果skip_if_exist为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):
|
||||
span.set_attribute("file.exist", True)
|
||||
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)
|
||||
response = requests.get(url, timeout=15) # 设置超时时间
|
||||
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)
|
||||
req_span.set_attribute("file.size", os.path.getsize(file_path))
|
||||
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_status(Status(StatusCode.ERROR))
|
||||
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:
|
||||
req_span.set_attribute("http.error", str(e))
|
||||
req_span.set_status(Status(StatusCode.ERROR))
|
||||
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))
|
||||
return False
|
||||
|
||||
@@ -11,14 +11,14 @@ def get_sys_info():
|
||||
Returns a dictionary with system information.
|
||||
"""
|
||||
info = {
|
||||
'version': SOFTWARE_VERSION,
|
||||
'client_datetime': datetime.now().isoformat(),
|
||||
'platform': platform.system(),
|
||||
'runtime_version': 'Python ' + platform.python_version(),
|
||||
'cpu_count': os.cpu_count(),
|
||||
'cpu_usage': psutil.cpu_percent(),
|
||||
'memory_total': psutil.virtual_memory().total,
|
||||
'memory_available': psutil.virtual_memory().available,
|
||||
'support_feature': SUPPORT_FEATURE
|
||||
"version": SOFTWARE_VERSION,
|
||||
"client_datetime": datetime.now().isoformat(),
|
||||
"platform": platform.system(),
|
||||
"runtime_version": "Python " + platform.python_version(),
|
||||
"cpu_count": os.cpu_count(),
|
||||
"cpu_usage": psutil.cpu_percent(),
|
||||
"memory_total": psutil.virtual_memory().total,
|
||||
"memory_available": psutil.virtual_memory().available,
|
||||
"support_feature": SUPPORT_FEATURE,
|
||||
}
|
||||
return info
|
||||
|
||||
Reference in New Issue
Block a user