直接拼接逻辑
This commit is contained in:
parent
da6579a9ed
commit
ce469dacf2
@ -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())
|
||||||
|
|
||||||
|
|
||||||
|
10
biz/task.py
10
biz/task.py
@ -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
|
||||||
|
})
|
@ -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
|
@ -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]
|
||||||
|
@ -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:
|
||||||
|
102
util/ffmpeg.py
102
util/ffmpeg.py
@ -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
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user