From 94373cee720cedceaf1ceb9c2defda862ce4144a Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Wed, 5 Mar 2025 12:16:10 +0800 Subject: [PATCH 1/2] =?UTF-8?q?cameraShot=E7=89=B9=E6=95=88=E5=8F=8A?= =?UTF-8?q?=E6=97=8B=E8=BD=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- entity/ffmpeg.py | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/entity/ffmpeg.py b/entity/ffmpeg.py index 192f6dd..a3b5326 100644 --- a/entity/ffmpeg.py +++ b/entity/ffmpeg.py @@ -185,22 +185,36 @@ class FfmpegTask(object): if effect.startswith("cameraShot:"): param = effect.split(":", 2)[1] if param == '': - param = "3,1" + param = "3,1,0" _split = param.split(",") start = 3 duration = 1 + rotate_deg = 0 + if len(_split) >= 3: + if _split[2] == '': + rotate_deg = 0 + else: + rotate_deg = int(_split[2]) if len(_split) >= 2: - start = int(_split[0]) - duration = int(_split[1]) - elif len(_split) == 1: - start = int(_split[0]) + duration = float(_split[1]) + if len(_split) >= 1: + start = float(_split[0]) _start_out_str = "[eff_s]" + _mid_out_str = "[eff_m]" _end_out_str = "[eff_e]" - filter_args.append(f"{video_output_str}fps=fps={self.frame_rate},split{_start_out_str}{_end_out_str}") - filter_args.append(f"{_start_out_str}trim=start={0}:end={start+duration},freezeframes=first={start*self.frame_rate}:replace={start*self.frame_rate}{_start_out_str}") - filter_args.append(f"{_end_out_str}trim=start={start}{_end_out_str}") + filter_args.append(f"{video_output_str}split=3{_start_out_str}{_mid_out_str}{_end_out_str}") + filter_args.append(f"{video_output_str}[0:v]select=lt(n\,{int(start*self.frame_rate)}){_start_out_str}") + filter_args.append(f"{video_output_str}[0:v]select=gt(n\,{int(start*self.frame_rate)}){_end_out_str}") + filter_args.append(f"{video_output_str}[0:v]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: + filter_args.append(f"{_mid_out_str}rotate=PI*{rotate_deg}/360{_mid_out_str}") + # filter_args.append(f"{video_output_str}trim=start=0:end={start+duration},tpad=stop_mode=clone:stop_duration={duration},setpts=PTS-STARTPTS{_start_out_str}") + # filter_args.append(f"tpad=start_mode=clone:start_duration={duration},setpts=PTS-STARTPTS{_start_out_str}") + # filter_args.append(f"{_end_out_str}trim=start={start}{_end_out_str}") video_output_str = "[v_eff]" - filter_args.append(f"{_start_out_str}{_end_out_str}concat=n=2:v=1:a=0{video_output_str}") + # filter_args.append(f"{_end_out_str}{_start_out_str}overlay=eof_action=pass{video_output_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{video_output_str}") elif effect.startswith("zoom:"): ... ... From 56bdad7ad127df6fdfa619a11429e618de2b5448 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Thu, 6 Mar 2025 10:34:28 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E9=9F=B3=E8=BD=A8=E5=8F=A0=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- biz/ffmpeg.py | 1 + entity/ffmpeg.py | 163 ++++++++++++++++++++++------------------------- util/ffmpeg.py | 6 +- util/oss.py | 41 ++++++------ 4 files changed, 102 insertions(+), 109 deletions(-) diff --git a/biz/ffmpeg.py b/biz/ffmpeg.py index ee7d89f..f6da84d 100644 --- a/biz/ffmpeg.py +++ b/biz/ffmpeg.py @@ -45,6 +45,7 @@ def parse_ffmpeg_task(task_info, template_info): output_file = "out_" + str(time.time()) + ".mp4" task = FfmpegTask(tasks, output_file=output_file) overall = template_info.get("overall_template") + task.mute = False task.center_cut = template_info.get("crop_mode", None) task.frame_rate = template_info.get("frame_rate", 25) if overall.get('source', ''): diff --git a/entity/ffmpeg.py b/entity/ffmpeg.py index a3b5326..0331702 100644 --- a/entity/ffmpeg.py +++ b/entity/ffmpeg.py @@ -3,8 +3,11 @@ import time import uuid from typing import Any -ENCODER_ARGS = ("-c:v", "h264_qsv", "-global_quality", "28", "-look_ahead", "1",) -PROFILE_LEVEL_ARGS = ("-profile:v", "high", "-level:v", "4") +DEFAULT_ARGS = ("-shortest",) +ENCODER_ARGS = ("-c:v", "h264_qsv", "-global_quality", "28", "-look_ahead", "1", ) +VIDEO_ARGS = ("-profile:v", "high", "-level:v", "4", ) +AUDIO_ARGS = ("-c:a", "aac", "-b:a", "128k", "-ar", "48000", "-ac", "2", ) +MUTE_AUDIO_INPUT = ("-f", "lavfi", "-i", "anullsrc=cl=stereo:r=48000", ) class FfmpegTask(object): @@ -149,23 +152,22 @@ class FfmpegTask(object): return True def check_audio_track(self): - if len(self.audios) > 0: - self.mute = False + ... def get_ffmpeg_args(self): args = ['-y', '-hide_banner'] if self.task_type == 'encode': input_args = [] filter_args = [] - output_args = [*PROFILE_LEVEL_ARGS,"-shortest", *ENCODER_ARGS] + output_args = [*VIDEO_ARGS, *AUDIO_ARGS, *ENCODER_ARGS, *DEFAULT_ARGS] if self.annexb: output_args.append("-bsf:v") output_args.append("h264_mp4toannexb") output_args.append("-reset_timestamps") output_args.append("1") video_output_str = "[0:v]" - audio_output_str = "[0:v]" - audio_input_count = 0 + audio_output_str = "" + effect_index = 0 for input_file in self.input_file: input_args.append("-i") if type(input_file) is str: @@ -179,8 +181,9 @@ class FfmpegTask(object): _f_x = pos_json.get('ltX', 0) _f_x2 = pos_json.get('rbX', 0) _x = f'{float((_f_x2 - _f_x)/(2 * _v_w)) :.4f}*iw' - filter_args.append(f"{video_output_str}crop=x={_x}:y=0:w=ih*ih/iw:h=ih[v_cut]") - video_output_str = "[v_cut]" + filter_args.append(f"{video_output_str}crop=x={_x}:y=0:w=ih*ih/iw:h=ih[v_cut{effect_index}]") + video_output_str = f"[v_cut{effect_index}]" + effect_index += 1 for effect in self.effects: if effect.startswith("cameraShot:"): param = effect.split(":", 2)[1] @@ -203,18 +206,19 @@ class FfmpegTask(object): _mid_out_str = "[eff_m]" _end_out_str = "[eff_e]" filter_args.append(f"{video_output_str}split=3{_start_out_str}{_mid_out_str}{_end_out_str}") - filter_args.append(f"{video_output_str}[0:v]select=lt(n\,{int(start*self.frame_rate)}){_start_out_str}") - filter_args.append(f"{video_output_str}[0:v]select=gt(n\,{int(start*self.frame_rate)}){_end_out_str}") - filter_args.append(f"{video_output_str}[0:v]select=eq(n\,{int(start*self.frame_rate)}){_mid_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"{_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: filter_args.append(f"{_mid_out_str}rotate=PI*{rotate_deg}/360{_mid_out_str}") # filter_args.append(f"{video_output_str}trim=start=0:end={start+duration},tpad=stop_mode=clone:stop_duration={duration},setpts=PTS-STARTPTS{_start_out_str}") # filter_args.append(f"tpad=start_mode=clone:start_duration={duration},setpts=PTS-STARTPTS{_start_out_str}") # filter_args.append(f"{_end_out_str}trim=start={start}{_end_out_str}") - video_output_str = "[v_eff]" + video_output_str = f"[v_eff{effect_index}]" # filter_args.append(f"{_end_out_str}{_start_out_str}overlay=eof_action=pass{video_output_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{video_output_str}") + effect_index += 1 elif effect.startswith("zoom:"): ... ... @@ -235,90 +239,73 @@ class FfmpegTask(object): output_args.append("-r") output_args.append(f"{self.frame_rate}") if self.mute: - output_args.append("-an") + input_index = input_args.count("-i") + input_args += MUTE_AUDIO_INPUT + filter_args.append(f"[{input_index}:a]acopy[a]") + audio_output_str = "[a]" else: - input_index = 0 - for audio in self.audios: - input_index = input_args.count("-i") - input_args.append("-i") - input_args.append(audio.replace("\\", "/")) - if audio_input_count > 0: - filter_args.append(f"{audio_output_str}[{input_index}:a]amix[a]") - audio_output_str = "[a]" - else: - audio_output_str = f"[{input_index}:a]" - audio_input_count += 1 - if audio_input_count == 1: - audio_output_str = f"{input_index}" - output_args.append(f"-map") + audio_output_str = "[0:a]" + for audio in self.audios: + input_index = input_args.count("-i") + input_args.append("-i") + input_args.append(audio.replace("\\", "/")) + if audio_output_str == "": + filter_args.append(f"[{input_index}:a]acopy[a]") + audio_output_str = "[a]" + else: + filter_args.append(f"{audio_output_str}[{input_index}:a]amix[a]") + audio_output_str = "[a]" + if audio_output_str: + output_args.append("-map") output_args.append(audio_output_str) - return args + input_args + ["-filter_complex", ";".join(filter_args)] + output_args + [self.get_output_file()] + _filter_args = [] if len(filter_args) == 0 else ["-filter_complex", ";".join(filter_args)] + return args + input_args + _filter_args + output_args + [self.get_output_file()] elif self.task_type == 'concat': # 无法通过 annexb 合并的 input_args = [] - 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 += ("-f", "concat", "-safe", "0", "-i", _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 += ["-r", f"{self.frame_rate}", *PROFILE_LEVEL_ARGS, *ENCODER_ARGS] + output_args = [*DEFAULT_ARGS] filter_args = [] - video_output_str = "[0:v]" - audio_output_str = "[0:a]" - video_input_count = 0 - audio_input_count = 0 - for input_file in self.input_file: + audio_output_str = "" + audio_track_index = 0 + # output_args + _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 += ["-f", "concat", "-safe", "0", "-i", _tmp_file] + output_args.append("-map") + output_args.append("0:v") + output_args.append("-c:v") + output_args.append("copy") + if self.mute: + input_index = input_args.count("-i") + input_args += MUTE_AUDIO_INPUT + audio_output_str = f"[{input_index}:a]" + audio_track_index += 1 + else: + audio_output_str = "[0:a]" + audio_track_index += 1 + for audio in self.audios: input_index = input_args.count("-i") input_args.append("-i") - if type(input_file) is str: - input_args.append(input_file.replace("\\", "/")) - elif isinstance(input_file, FfmpegTask): - input_args.append(input_file.get_output_file().replace("\\", "/")) - if video_input_count > 0: - filter_args.append(f"{video_output_str}[{input_index}:v]concat=n=2:v=1:a=0[v]") - video_output_str = "[v]" + input_args.append(audio.replace("\\", "/")) + audio_track_index += 1 + filter_args.append(f"{audio_output_str}[{input_index}:a]amix[a]") + audio_output_str = "[a]" + if audio_output_str: + output_args.append("-map") + if audio_track_index <= 1: + output_args.append(audio_output_str[1:-1]) else: - video_output_str = f"[{input_index}:v]" - video_input_count += 1 - output_args.append("-map") - output_args.append(video_output_str) - if self.mute: - output_args.append("-an") - else: - input_index = 0 - for audio in self.audios: - input_index = input_args.count("-i") - input_args.append("-i") - input_args.append(audio.replace("\\", "/")) - if audio_input_count > 0: - filter_args.append(f"{audio_output_str}[{input_index}:a]amix[a]") - audio_output_str = "[a]" - else: - audio_output_str = f"[{input_index}:a]" - audio_input_count += 1 - if audio_input_count == 1: - audio_output_str = f"{input_index}" - output_args.append(f"-map") - output_args.append(audio_output_str) - return args + input_args + ["-filter_complex", ";".join(filter_args)] + output_args + [self.get_output_file()] + output_args.append(audio_output_str) + output_args += AUDIO_ARGS + output_args.append("-f") + output_args.append("mp4") + _filter_args = [] if len(filter_args) == 0 else ["-filter_complex", ";".join(filter_args)] + return args + input_args + _filter_args + output_args + [self.get_output_file()] elif self.task_type == 'copy': if len(self.input_file) == 1: if type(self.input_file[0]) is str: diff --git a/util/ffmpeg.py b/util/ffmpeg.py index da56e67..9c537d0 100644 --- a/util/ffmpeg.py +++ b/util/ffmpeg.py @@ -5,7 +5,7 @@ import subprocess from datetime import datetime from typing import Optional, IO -from entity.ffmpeg import FfmpegTask, ENCODER_ARGS, PROFILE_LEVEL_ARGS +from entity.ffmpeg import FfmpegTask, ENCODER_ARGS, VIDEO_ARGS, AUDIO_ARGS from telemetry import get_tracer logger = logging.getLogger(__name__) @@ -34,7 +34,7 @@ def re_encode_and_annexb(file): if not os.path.exists(file): return file logger.info("ReEncodeAndAnnexb: %s", file) - ffmpeg_process = subprocess.run(["ffmpeg", "-y", "-hide_banner", "-i", file, *PROFILE_LEVEL_ARGS, *ENCODER_ARGS, "-bsf:v", "h264_mp4toannexb", + ffmpeg_process = subprocess.run(["ffmpeg", "-y", "-hide_banner", "-i", file, *VIDEO_ARGS, *AUDIO_ARGS, *ENCODER_ARGS, "-bsf:v", "h264_mp4toannexb", "-f", "mpegts", file +".ts"]) span.set_attribute("ffmpeg.args", json.dumps(ffmpeg_process.args)) logger.info("ReEncodeAndAnnexb: %s, returned: %s", file, ffmpeg_process.returncode) @@ -55,12 +55,12 @@ def start_render(ffmpeg_task: FfmpegTask): ffmpeg_task.set_output_file(ffmpeg_task.input_file[0]) return True ffmpeg_args = ffmpeg_task.get_ffmpeg_args() - logger.info(ffmpeg_args) if len(ffmpeg_args) == 0: ffmpeg_task.set_output_file(ffmpeg_task.input_file[0]) return True ffmpeg_process = subprocess.run(["ffmpeg", "-progress", "-", "-loglevel", "error", *ffmpeg_args], **subprocess_args(True)) span.set_attribute("ffmpeg.args", json.dumps(ffmpeg_process.args)) + logger.info(ffmpeg_process.args) ffmpeg_final_out = handle_ffmpeg_output(ffmpeg_process.stdout) span.set_attribute("ffmpeg.out", ffmpeg_final_out) logger.info("FINISH TASK, OUTPUT IS %s", ffmpeg_final_out) diff --git a/util/oss.py b/util/oss.py index f267489..5a987b1 100644 --- a/util/oss.py +++ b/util/oss.py @@ -22,8 +22,9 @@ def upload_to_oss(url, file_path): max_retries = 5 retries = 0 while retries < max_retries: - try: - with tracer.start_as_current_span("upload_to_oss.request") as req_span: + with tracer.start_as_current_span("upload_to_oss.request") as req_span: + req_span.set_attribute("http.retry_count", retries) + try: req_span.set_attribute("http.method", "PUT") req_span.set_attribute("http.url", url) with open(file_path, 'rb') as f: @@ -31,13 +32,14 @@ def upload_to_oss(url, file_path): req_span.set_attribute("http.status_code", response.status_code) if response.status_code == 200: return True - except requests.exceptions.Timeout: - retries += 1 - logger.warning(f"Upload timed out. Retrying {retries}/{max_retries}...") - except Exception as e: - retries += 1 - span.set_attribute("oss.error", str(e)) - logger.warning(f"Upload failed. Retrying {retries}/{max_retries}...") + except requests.exceptions.Timeout: + req_span.set_attribute("http.error", "Timeout") + retries += 1 + logger.warning(f"Upload timed out. Retrying {retries}/{max_retries}...") + except Exception as e: + req_span.set_attribute("http.error", str(e)) + retries += 1 + logger.warning(f"Upload failed. Retrying {retries}/{max_retries}...") return False @@ -60,20 +62,23 @@ def download_from_oss(url, file_path): max_retries = 5 retries = 0 while retries < max_retries: - try: - with tracer.start_as_current_span("download_from_oss.request") as req_span: + with tracer.start_as_current_span("download_from_oss.request") as req_span: + req_span.set_attribute("http.retry_count", retries) + try: req_span.set_attribute("http.method", "GET") 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: f.write(response.content) + span.set_attribute("file.size", os.path.getsize(file_path)) return True - except requests.exceptions.Timeout: - retries += 1 - logger.warning(f"Download timed out. Retrying {retries}/{max_retries}...") - except Exception as e: - retries += 1 - span.set_attribute("oss.error", str(e)) - logger.warning(f"Download failed. Retrying {retries}/{max_retries}...") + except requests.exceptions.Timeout: + span.set_attribute("http.error", "Timeout") + retries += 1 + logger.warning(f"Download timed out. Retrying {retries}/{max_retries}...") + except Exception as e: + span.set_attribute("http.error", str(e)) + retries += 1 + logger.warning(f"Download failed. Retrying {retries}/{max_retries}...") return False