直接拼接逻辑

This commit is contained in:
Jerry Yan 2025-01-11 17:25:58 +08:00
parent da6579a9ed
commit ce469dacf2
7 changed files with 191 additions and 21 deletions

View File

@ -21,6 +21,7 @@ def parse_ffmpeg_task(task_info, template_info):
logger.warning("no video found for part: " + str(part)) logger.warning("no video found for part: " + str(part))
continue continue
sub_ffmpeg_task = FfmpegTask(source) sub_ffmpeg_task = FfmpegTask(source)
sub_ffmpeg_task.annexb = True
sub_ffmpeg_task.frame_rate = template_info.get("frame_rate", 25) sub_ffmpeg_task.frame_rate = template_info.get("frame_rate", 25)
for lut in part.get('filters', []): for lut in part.get('filters', []):
sub_ffmpeg_task.add_lut(os.path.join(template_info.get("local_path"), lut)) sub_ffmpeg_task.add_lut(os.path.join(template_info.get("local_path"), lut))
@ -75,9 +76,19 @@ def clear_task_tmp_file(ffmpeg_task):
for task in ffmpeg_task.analyze_input_render_tasks(): for task in ffmpeg_task.analyze_input_render_tasks():
clear_task_tmp_file(task) clear_task_tmp_file(task)
try: try:
os.remove(ffmpeg_task.get_output_file()) if "template" not in ffmpeg_task.get_output_file():
logger.info("delete tmp file: " + ffmpeg_task.get_output_file()) os.remove(ffmpeg_task.get_output_file())
logger.info("delete tmp file: " + ffmpeg_task.get_output_file())
else:
logger.info("skip delete template file: " + ffmpeg_task.get_output_file())
except OSError: except OSError:
logger.warning("delete tmp file failed: " + ffmpeg_task.get_output_file()) logger.warning("delete tmp file failed: " + ffmpeg_task.get_output_file())
return False return False
return True return True
def probe_video_info(ffmpeg_task):
# 获取视频长度宽度和时长
return ffmpeg.probe_video_info(ffmpeg_task.get_output_file())

View File

@ -3,7 +3,7 @@ from util import api
def start_task(task_info): def start_task(task_info):
from biz.ffmpeg import parse_ffmpeg_task, start_ffmpeg_task, clear_task_tmp_file from biz.ffmpeg import parse_ffmpeg_task, start_ffmpeg_task, clear_task_tmp_file, probe_video_info
task_info = api.normalize_task(task_info) task_info = api.normalize_task(task_info)
template_info = get_template_def(task_info.get("templateId")) template_info = get_template_def(task_info.get("templateId"))
api.report_task_start(task_info) api.report_task_start(task_info)
@ -14,5 +14,11 @@ def start_task(task_info):
oss_result = api.upload_task_file(task_info, ffmpeg_task) oss_result = api.upload_task_file(task_info, ffmpeg_task)
if not oss_result: if not oss_result:
return api.report_task_failed(task_info) return api.report_task_failed(task_info)
# 获取视频长度宽度和时长
width, height, duration = probe_video_info(ffmpeg_task)
clear_task_tmp_file(ffmpeg_task) clear_task_tmp_file(ffmpeg_task)
api.report_task_success(task_info) api.report_task_success(task_info, videoInfo={
"width": width,
"height": height,
"duration": duration
})

View File

@ -1,10 +1,14 @@
import time
import uuid import uuid
class FfmpegTask(object): class FfmpegTask(object):
def __init__(self, input_file, task_type='copy', output_file=''): def __init__(self, input_file, task_type='copy', output_file=''):
self.annexb = False
if type(input_file) is str: if type(input_file) is str:
if input_file.endswith(".ts"):
self.annexb = True
self.input_file = [input_file] self.input_file = [input_file]
elif type(input_file) is list: elif type(input_file) is list:
self.input_file = input_file self.input_file = input_file
@ -19,7 +23,6 @@ class FfmpegTask(object):
self.luts = [] self.luts = []
self.audios = [] self.audios = []
self.overlays = [] self.overlays = []
self.annexb = False
def __repr__(self): def __repr__(self):
_str = f'FfmpegTask(input_file={self.input_file}, task_type={self.task_type}' _str = f'FfmpegTask(input_file={self.input_file}, task_type={self.task_type}'
@ -125,7 +128,10 @@ class FfmpegTask(object):
if self.task_type == 'encode': if self.task_type == 'encode':
input_args = [] input_args = []
filter_args = [] filter_args = []
output_args = ["-shortest", "-c:v h264_qsv"] output_args = ["-shortest", "-c:v", "h264_qsv"]
if self.annexb:
output_args.append("-bsf:v")
output_args.append("h264_mp4toannexb")
video_output_str = "[0:v]" video_output_str = "[0:v]"
audio_output_str = "[0:v]" audio_output_str = "[0:v]"
video_input_count = 0 video_input_count = 0
@ -172,11 +178,43 @@ class FfmpegTask(object):
output_args.append(audio_output_str) output_args.append(audio_output_str)
return args + input_args + ["-filter_complex", ";".join(filter_args)] + output_args + [self.get_output_file()] return args + input_args + ["-filter_complex", ";".join(filter_args)] + output_args + [self.get_output_file()]
elif self.task_type == 'concat': elif self.task_type == 'concat':
# 无法通过 annexb 合并的
input_args = [] input_args = []
output_args = ["-shortest", "-c:v", "h264_qsv", "-r", "25"] output_args = ["-shortest"]
if self.check_annexb() and len(self.audios) <= 1:
# output_args
if len(self.audios) > 0:
input_args.append("-an")
_tmp_file = "tmp_concat_"+str(time.time())+".txt"
with open(_tmp_file, "w", encoding="utf-8") as f:
for input_file in self.input_file:
if type(input_file) is str:
f.write("file '"+input_file+"'\n")
elif isinstance(input_file, FfmpegTask):
f.write("file '" + input_file.get_output_file() + "'\n")
input_args.append("-f")
input_args.append("concat")
input_args.append("-safe")
input_args.append("0")
input_args.append("-i")
input_args.append(_tmp_file)
output_args.append("-c:v")
output_args.append("copy")
if len(self.audios) > 0:
input_args.append("-i")
input_args.append(self.audios[0])
output_args.append("-c:a")
output_args.append("copy")
output_args.append("-f")
output_args.append("mp4")
return args + input_args + output_args + [self.get_output_file()]
output_args.append("-c:v")
output_args.append("h264_qsv")
output_args.append("-r")
output_args.append("25")
filter_args = [] filter_args = []
video_output_str = "[0:v]" video_output_str = "[0:v]"
audio_output_str = "[0:v]" audio_output_str = "[0:a]"
video_input_count = 0 video_input_count = 0
audio_input_count = 0 audio_input_count = 0
for input_file in self.input_file: for input_file in self.input_file:
@ -217,12 +255,31 @@ class FfmpegTask(object):
if len(self.input_file) == 1: if len(self.input_file) == 1:
if type(self.input_file[0]) is str: if type(self.input_file[0]) is str:
if self.input_file[0] == self.get_output_file(): if self.input_file[0] == self.get_output_file():
return None return []
return args + ["-i", self.input_file[0]] + ["-c", "copy", self.get_output_file()] return args + ["-i", self.input_file[0]] + ["-c", "copy", self.get_output_file()]
def set_output_file(self, file=None): def set_output_file(self, file=None):
if file is None: if file is None:
if self.output_file == '': if self.output_file == '':
self.output_file = "rand_" + str(uuid.uuid4()) + ".mp4" if self.annexb:
self.output_file = "rand_" + str(uuid.uuid4()) + ".ts"
else:
self.output_file = "rand_" + str(uuid.uuid4()) + ".mp4"
else: else:
self.output_file = file self.output_file = file
def check_annexb(self):
for input_file in self.input_file:
if type(input_file) is str:
if self.task_type == 'encode':
return self.annexb
elif self.task_type == 'concat':
return False
elif self.task_type == 'copy':
return self.annexb
else:
return False
elif isinstance(input_file, FfmpegTask):
if not input_file.check_annexb():
return False
return True

View File

@ -86,7 +86,10 @@ def download_template(template_id):
if str(_template['source']).startswith("http"): if str(_template['source']).startswith("http"):
_, _fn = os.path.split(_template['source']) _, _fn = os.path.split(_template['source'])
oss.download_from_oss(_template['source'], os.path.join(template_info['local_path'], _fn)) oss.download_from_oss(_template['source'], os.path.join(template_info['local_path'], _fn))
_template['source'] = _fn if _fn.endswith(".mp4"):
from util.ffmpeg import to_annexb
_fn = to_annexb(os.path.join(template_info['local_path'], _fn))
_template['source'] = os.path.relpath(_fn, template_info['local_path'])
if 'overlays' in _template: if 'overlays' in _template:
for i in range(len(_template['overlays'])): for i in range(len(_template['overlays'])):
overlay = _template['overlays'][i] overlay = _template['overlays'][i]

View File

@ -104,10 +104,11 @@ def get_template_info(template_id):
return template return template
def report_task_success(task_info): def report_task_success(task_info, **kwargs):
try: try:
response = session.post('{0}/{1}/success'.format(os.getenv('API_ENDPOINT'), task_info.get("id")), json={ response = session.post('{0}/{1}/success'.format(os.getenv('API_ENDPOINT'), task_info.get("id")), json={
'accessKey': os.getenv('ACCESS_KEY'), 'accessKey': os.getenv('ACCESS_KEY'),
**kwargs
}, timeout=10) }, timeout=10)
response.raise_for_status() response.raise_for_status()
except requests.RequestException as e: except requests.RequestException as e:

View File

@ -1,27 +1,48 @@
import logging
import os import os
import subprocess
from datetime import datetime from datetime import datetime
from typing import Optional, IO from typing import Optional, IO
from entity.ffmpeg import FfmpegTask from entity.ffmpeg import FfmpegTask
logger = logging.getLogger(__name__)
def to_annexb(file):
if not os.path.exists(file):
return file
logger.info("ToAnnexb: %s", file)
ffmpeg_process = subprocess.run(["ffmpeg.exe", "-y", "-hide_banner", "-i", file, "-c", "copy", "-bsf:v", "h264_mp4toannexb",
"-f", "mpegts", file+".ts"])
logger.info("ToAnnexb: %s, returned: %s", file, ffmpeg_process.returncode)
if ffmpeg_process.returncode == 0:
os.remove(file)
return file+".ts"
else:
return file
def start_render(ffmpeg_task: FfmpegTask): def start_render(ffmpeg_task: FfmpegTask):
print(ffmpeg_task) logger.info(ffmpeg_task)
print(ffmpeg_task.get_ffmpeg_args()) logger.info(ffmpeg_task.get_ffmpeg_args())
if not ffmpeg_task.need_run(): if not ffmpeg_task.need_run():
ffmpeg_task.set_output_file(ffmpeg_task.input_file[0]) ffmpeg_task.set_output_file(ffmpeg_task.input_file[0])
return True return True
code = os.system("ffmpeg.exe "+" ".join(ffmpeg_task.get_ffmpeg_args())) ffmpeg_args = ffmpeg_task.get_ffmpeg_args()
if len(ffmpeg_args) == 0:
ffmpeg_task.set_output_file(ffmpeg_task.input_file[0])
return True
ffmpeg_process = subprocess.run(["ffmpeg.exe", "-progress", "-", "-loglevel", "error", *ffmpeg_args], **subprocess_args(True))
logger.info("FINISH TASK, OUTPUT IS %s", handle_ffmpeg_output(ffmpeg_process.stdout))
code = ffmpeg_process.returncode
return code == 0 return code == 0
def handle_ffmpeg_output(stdout: Optional[IO[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:
print("[!]STDOUT is null") print("[!]STDOUT is null")
return out_time return out_time
speed = "0" speed = "0"
while True: for line in stdout.split(b"\n"):
line = stdout.readline()
if line == b"": if line == b"":
break break
if line.strip() == b"progress=end": if line.strip() == b"progress=end":
@ -32,8 +53,75 @@ def handle_ffmpeg_output(stdout: Optional[IO[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 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)
return _duration.total_seconds() return _duration.total_seconds()
def probe_video_info(video_file):
# 获取宽度和高度
result = subprocess.run(
["ffprobe.exe", '-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)
)
all_result = result.stdout.decode('utf-8').strip()
wh, duration = all_result.split('\n')
width, height = wh.strip().split('x')
return int(width), int(height), float(duration)
# 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::
#
# subprocess.call(['program_to_run', 'arg_1'], **subprocess_args())
#
# When calling ``check_output``::
#
# subprocess.check_output(['program_to_run', 'arg_1'],
# **subprocess_args(False))
def subprocess_args(include_stdout=True):
# The following is true only on Windows.
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.
si = subprocess.STARTUPINFO()
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
# Windows doesn't search the path by default. Pass it an environment so
# it will.
env = os.environ
else:
si = None
env = None
# ``subprocess.check_output`` doesn't allow specifying ``stdout``::
#
# Traceback (most recent call last):
# File "test_subprocess.py", line 58, in <module>
# **subprocess_args(stdout=None))
# File "C:\Python27\lib\subprocess.py", line 567, in check_output
# raise ValueError('stdout argument not allowed, it will be overridden.')
# ValueError: stdout argument not allowed, it will be overridden.
#
# So, add it only if it's needed.
if include_stdout:
ret = {'stdout': subprocess.PIPE}
else:
ret = {}
# On Windows, running this from the binary produced by Pyinstaller
# 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})
return ret

View File

@ -1,7 +1,10 @@
import logging
import os import os
import requests import requests
logger = logging.getLogger(__name__)
def upload_to_oss(url, file_path): def upload_to_oss(url, file_path):
""" """
@ -25,6 +28,7 @@ def download_from_oss(url, file_path):
:param Union[LiteralString, str, bytes] file_path: 文件路径 :param Union[LiteralString, str, bytes] file_path: 文件路径
:return bool: 是否成功 :return bool: 是否成功
""" """
logging.info("download_from_oss: %s", url)
file_dir, file_name = os.path.split(file_path) file_dir, file_name = os.path.split(file_path)
if file_dir: if file_dir:
if not os.path.exists(file_dir): if not os.path.exists(file_dir):