diff --git a/nodes/pipVideo.py b/nodes/pipVideo.py new file mode 100644 index 0000000..7820176 --- /dev/null +++ b/nodes/pipVideo.py @@ -0,0 +1,166 @@ +import os +import subprocess +from ..func import has_audio,getVideoInfo,set_file_name,video_type +import torch +import math + +device = "cuda" if torch.cuda.is_available() else "cpu" + +class PipVideo: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "video1_path": ("STRING", {"default":"C:/Users/Desktop/video1.mp4", "tooltip": "说明:画中画背景画面!"}), + "video2_path": ("STRING", {"default":"C:/Users/Desktop/video2.mp4", "tooltip": "说明:画中画前景画面!"}), + "device": (["cpu","cuda"], {"default":device,}), + "use_audio": (["video1","video2"], {"default":"video1", "tooltip": "说明:最终视频使用哪个视频的音轨!"}), + "use_duration": (["video1","video2"], {"default":"video2", "tooltip": "说明:使用哪个视频作为最终参考时长!"}), + "align_type":(["top-left","top-right", "bottom-left", "bottom-right", "center"], {"default":"center",}), + "pip_fg_zoom": ("FLOAT", { "default": 2.5, "min": 1, "max": 100, "step": 0.5, "tooltip": "说明:画中画背景缩放系数,越大前景画面越小,值为背景宽高的缩小倍数!"}), + "output_path": ("STRING", {"default": "C:/Users/Desktop/output"}), + "scale_and_crop": (["none","540*960", "960*540"], {"default": "none", "tooltip": "说明:缩放和裁剪比例!"}), # 新增参数控制缩放裁剪 + "fps": ("FLOAT", {"min": 0, "max": 60, "step": 0.1, "default": 30.0, "tooltip": "说明:画中画合并后的强制帧率,设置为0将使用video2的帧率,设置为1为将使用video2的帧率!"}), + "is_chromakey": ("BOOLEAN", { "default": False ,"label_on": "绿幕去背景", "label_off": "关闭绿幕透明", "tooltip": "说明:是否进行绿幕去背景!"}), #是否画中画 绿幕透明 + }, + } + + RETURN_TYPES = ("STRING","INT","INT","FLOAT","FLOAT",) + RETURN_NAMES = ("video_complete_paths","width","height","duration","fps",) + FUNCTION = "pip_video" + OUTPUT_NODE = True + CATEGORY = "🔥FFmpeg" + DESCRIPTION = """两个视频叠加成一个画中画效果,可以控制前景video2出现在前景video1画面上的位置, + 可以设置前景画面的缩放系数和是否去掉绿幕背景等设置.""" + + def pip_video(self, video1_path, video2_path,device,use_audio,use_duration, align_type,pip_fg_zoom, output_path,scale_and_crop,fps,is_chromakey): + try: + video1_path = os.path.abspath(video1_path).strip() + video2_path = os.path.abspath(video2_path).strip() + output_path = os.path.abspath(output_path).strip() + # 视频不存在 + if not video1_path.lower().endswith(video_type()): + raise ValueError("video1_path:"+video1_path+"不是视频文件(video1_path:"+video1_path+" is not a video file)") + if not os.path.isfile(video1_path): + raise ValueError("video1_path:"+video1_path+"不存在(video1_path:"+video1_path+" does not exist)") + + if not video2_path.lower().endswith(video_type()): + raise ValueError("video2_path:"+video2_path+"不是视频文件(video2_path:"+video2_path+" is not a video file)") + if not os.path.isfile(video2_path): + raise ValueError("video2_path:"+video2_path+"不存在(video2_path:"+video2_path+" does not exist)") + + #判断output_path是否是一个目录 + if not os.path.isdir(output_path): + raise ValueError("output_path:"+output_path+"不是目录(output_path:"+output_path+" is not a directory)") + + video1_audio = has_audio(video1_path) + video2_audio = has_audio(video2_path) + + #final_output = set_file_name(video1_path) + #增加以视频1+视频2的文件名合并重命名 + final_output = f"{os.path.splitext(os.path.basename(video1_path))[0]}--{os.path.splitext(os.path.basename(video2_path))[0]}.mp4" + + #文件名根据年月日时分秒来命名 + output_path = os.path.join(output_path, final_output) + + use_cuvid = "" + use_encoder = "-c:v libx264" #默认用CPU编码 + + if device == "cuda": + use_cuvid = "-hwaccel cuda" + use_encoder = "-c:v h264_nvenc" + + video_info = getVideoInfo(video1_path) + video_info1 = getVideoInfo(video2_path) + if use_duration == "video1": + duration_1 = video_info['duration'] + else: + duration_1 = video_info1['duration'] + if fps==0: + fps=video_info['fps'] + elif fps==1: + fps=video_info1['fps'] + + # libx264、libx265等编码器要求宽高必须是2的倍数,如果报错,可以把device换为GPU + width = math.ceil(video_info['width']/2)*2 + height = math.ceil(video_info['height']/2)*2 + + use_audio = { + 'video1': '0', + 'video2': '1', + }.get(use_audio, '0') + + align_position = { + "top-left": f"0:0", + "top-right": f"(W-w):0", + "bottom-left": f"0:(H-h)", + "bottom-right": f"(W-w):(H-h)", + "center": f"(W-w)/2:(H-h)/2", + }.get(align_type, f"(W-w)/2:(H-h)/2") + + if height*540/width>=960: #如果高同比缩放高度超出960,需要裁剪 + pad_or_crop1='crop=540:960:(iw-iw)/2:(ih-ih/2)/2' + else: #比固定高小,需要填充黑边 + pad_or_crop1='pad=540:960:(ow-iw)/2:(oh-ih)/2:color=black' + if height*960/width>=540: #如果缩放到宽960时,高超出540,需要裁剪 + pad_or_crop2='crop=960:540:(iw-iw)/2:(ih-ih/2)/2' + else: + pad_or_crop2='pad=960:540:(ow-iw)/2:(oh-ih)/2:color=black' + scale_and_crop_data = { + 'none': 'null', + '540*960': f'scale=540:-1,setsar=1,{pad_or_crop1}', + '960*540': f'scale=960:-1,setsar=1,{pad_or_crop2}', + }.get(scale_and_crop, 'null') + + video2_width = { + 'none': f'{width}', + '540*960': '540', + '960*540': '960', + }.get(scale_and_crop, f'{width}') + + final_out = { + 'none': f'scale={width}:{height}:force_original_aspect_ratio=disable,setsar=1', + '540*960': 'scale=540:960:force_original_aspect_ratio=disable,setsar=1', + '960*540': 'scale=960:540:force_original_aspect_ratio=disable,setsar=1', + }.get(scale_and_crop, f'scale={width}:{height}:force_original_aspect_ratio=disable,setsar=1') + + #测试去绿幕效果 + if is_chromakey: + chromakey="chromakey=0x00FF00:0.3:0.1,format=yuva420p" + else: + chromakey="null" + + + if video1_audio or video2_audio: + #-map 1:a 指定使用第二个视频的音频流 + command = fr'ffmpeg "-y" {use_cuvid} -stream_loop -1 -i "{video1_path}" -stream_loop -1 -i "{video2_path}" -filter_complex "[0:v]fps={fps},setpts=PTS-STARTPTS[bg];[1:v]fps={fps},setpts=PTS-STARTPTS[fg];[bg]{scale_and_crop_data}[bg_out];[fg]{chromakey}[fgd];[fgd]scale={video2_width}/{pip_fg_zoom}:-1,setsar=1[fg_out];[bg_out][fg_out]overlay={align_position}[out];[out]{final_out}[final_out]" -map "[final_out]" -map {use_audio}:a? {use_encoder} -c:a aac -t {duration_1} "{output_path}"' + else: + command = fr'ffmpeg "-y" {use_cuvid} -stream_loop -1 -i "{video1_path}" -stream_loop -1 -i "{video2_path}" -filter_complex "[0:v]fps={fps},setpts=PTS-STARTPTS[bg];[1:v]fps={fps},setpts=PTS-STARTPTS[fg];[bg]{scale_and_crop_data}[bg_out];[fg]{chromakey}[fgd];[fgd]scale={video2_width}/{pip_fg_zoom}:-1,setsar=1[fg_out];[bg_out][fg_out]overlay={align_position}[out];[out]{final_out}[final_out]" -map "[final_out]" -t {duration_1} "{output_path}"' + + print(f">>>{command}") + + # 执行命令并检查错误 + result = subprocess.run(command, stderr=subprocess.PIPE, stdout=subprocess.PIPE) + #print("command result",result.returncode) + + # 检查返回码 + if result.returncode != 0: + # 如果有错误,输出错误信息 + print(f"Error: {result.stderr.decode('utf-8')}") + if device == "cuda": + print(f"***当前运算模式*[{device}]*************看下换成CPU重新执行,是否解决因编码问题的报错!********") + self.pip_video(video1_path, video2_path,"cpu",use_audio,use_duration, align_type,pip_fg_zoom, os.path.dirname(output_path),scale_and_crop,fps,is_chromakey) + + else: + # 输出标准输出信息 + print(f">>FFmpeg 执行完毕!Completed!\t stdout: {result.stdout}") + + return (output_path,width,height,duration_1,fps,) + except Exception as e: + raise ValueError(e) + +#a=StitchingVideo() +#a.stitching_video("C:/Users/wtc/Desktop/tt/l.mp4", "C:/Users/wtc/Desktop/tt/r.mp4", "cpu", "video2", "horizontal","picture-picture", "bottom-left", "C:/Users/wtc/Desktop/tt", "yes") \ No newline at end of file diff --git a/nodes/stitchingVideo.py b/nodes/stitchingVideo.py index 1a51a57..46db742 100644 --- a/nodes/stitchingVideo.py +++ b/nodes/stitchingVideo.py @@ -3,6 +3,7 @@ import subprocess from ..func import has_audio,getVideoInfo,set_file_name,video_type import torch import math +import time device = "cuda" if torch.cuda.is_available() else "cpu" @@ -20,6 +21,7 @@ class StitchingVideo: "use_audio": (["video1","video2"], {"default":"video1",}), "stitching_type":(["horizontal","vertical"], {"default":"horizontal",}), "output_path": ("STRING", {"default": "C:/Users/Desktop/output"}), + "scale_and_crop": (["yes", "no"], {"default": "no"}), # 新增参数控制缩放裁剪 }, } @@ -29,7 +31,7 @@ class StitchingVideo: OUTPUT_NODE = True CATEGORY = "🔥FFmpeg" - def stitching_video(self, video1_path, video2_path,device,use_audio,stitching_type,output_path): + def stitching_video(self, video1_path, video2_path,device,use_audio,stitching_type,output_path,scale_and_crop): try: video1_path = os.path.abspath(video1_path).strip() video2_path = os.path.abspath(video2_path).strip() @@ -52,10 +54,14 @@ class StitchingVideo: video1_audio = has_audio(video1_path) video2_audio = has_audio(video2_path) - final_output = set_file_name(video1_path) + #final_output = set_file_name(video1_path) + #增加以视频1+视频2的文件名合并重命名 + final_output = f"{os.path.splitext(os.path.basename(video1_path))[0]}--{os.path.splitext(os.path.basename(video2_path))[0]}.mp4" + #文件名根据年月日时分秒来命名 output_path = os.path.join(output_path, final_output) + #CPU默认参数: use_cuvid = "" use_encoder = "-c:v libx264" #默认用CPU编码 @@ -64,7 +70,11 @@ class StitchingVideo: use_encoder = "-c:v h264_nvenc" video_info = getVideoInfo(video1_path) + video_info1 = getVideoInfo(video2_path) + duration = video_info['duration'] + fps = video_info['fps'] + loop_count = max(1, int(duration / video_info1['duration'] + 0.9999)) # libx264、libx265等编码器要求宽高必须是2的倍数,如果报错,可以把device换为GPU width = math.ceil(video_info['width']/2)*2 height = math.ceil(video_info['height']/2)*2 @@ -84,24 +94,45 @@ class StitchingVideo: 'vertical': f'{width}:-1', }.get(stitching_type, f'{width}:-1') - + print(f">>loop_count:{loop_count}") if video1_audio or video2_audio: #-map 1:a 指定使用第二个视频的音频流 - command = f'ffmpeg {use_cuvid} -i {video1_path} -i {video2_path} -filter_complex "[1:v]scale={scale}[vid2];[0:v][vid2]{tack_type}=inputs=2[v]" -map "[v]" -map {use_audio}:a? {use_encoder} -c:a aac -strict experimental {output_path}' + command = f'ffmpeg "-y" {use_cuvid} -i "{video1_path}" -stream_loop {loop_count-1} -i "{video2_path}" -filter_complex "[0:v]setpts=PTS-STARTPTS[ref];[1:v]scale={scale},fps={fps},setpts=PTS-STARTPTS[vid2];[ref][vid2]{tack_type}=inputs=2[v]" -map "[v]" -map {use_audio}:a? {use_encoder} -c:a aac -strict experimental -t {duration} "{output_path}"' else: - command = f'ffmpeg {use_cuvid} -i {video1_path} -i {video2_path} -filter_complex "[1:v]scale={scale}[vid2];[0:v][vid2]{tack_type}=inputs=2[v]" -map "[v]" {use_encoder} {output_path}' + command = f'ffmpeg "-y" {use_cuvid} -i "{video1_path}" -i "{video2_path}" -filter_complex "[0:v]setpts=PTS-STARTPTS[ref];[1:v]scale={scale},fps={fps}[vid2];[vid2]loop=loop={loop_count}:size=1:start=0,setpts=N/({fps})/TB[looped];[ref][looped]{tack_type}=inputs=2[v]" -map "[v]" {use_encoder} -t {duration} "{output_path}"' + print(command) + # 执行命令并检查错误 result = subprocess.run(command, stderr=subprocess.PIPE, stdout=subprocess.PIPE) + + # 构建滤镜链 + if scale_and_crop == "yes": + time.sleep(1) + if not os.path.isfile(output_path) and os.path.getsize(output_path)<=0: + print(f"output video:{output_path}不存在或文件内容为空!") + return (output_path,) + crop_video_path=os.path.join(os.path.dirname(output_path),"crop--"+final_output) + #ffmpeg "-y" -i Z:\sucai\video_right_test\20250520040751.mp4 -filter_complex "[0:v]split=2[bg][fg];[bg]scale=540:-1,setsar=1[scaled_bg];[scaled_bg]gblur=sigma=10[blurred];[blurred]scale=540:960:force_original_aspect_ratio=disable[bg_out];[fg]scale=540:-1,setsar=1[fg_out];[bg_out][fg_out]overlay=(W-w)/2:(H-h)/2[out];[out]scale=540:960:force_original_aspect_ratio=disable,setsar=1[final_out]" -map "[final_out]" -map 0:a -c:v h264_nvenc -c:a aac Z:\sucai\video_right_test\crop20250520040751.mp4 + if stitching_type == "vertical": + command = f'ffmpeg "-y" -i "{output_path}" -filter_complex "[0:v]scale=-1:960[scaled];[scaled]crop=540:ih:(iw-540)/2:0[out]" -map "[out]" -map 0:a {use_encoder} -c:a aac "{crop_video_path}"' + else: + command = f'ffmpeg "-y" -i "{output_path}" -filter_complex "[0:v]split=2[bg][fg];[bg]scale=540:-1,setsar=1[scaled_bg];[scaled_bg]gblur=sigma=10[blurred];[blurred]scale=540:960:force_original_aspect_ratio=disable[bg_out];[fg]scale=540:-1,setsar=1[fg_out];[bg_out][fg_out]overlay=(W-w)/2:(H-h)/2[out];[out]scale=540:960:force_original_aspect_ratio=disable,setsar=1[final_out]" -map "[final_out]" -map 0:a {use_encoder} -c:a aac "{crop_video_path}"' + print(f">>FFmpeg Scale and crop::{command}") + result = subprocess.run(command, stderr=subprocess.PIPE, stdout=subprocess.PIPE) + output_path=crop_video_path + # 检查返回码 if result.returncode != 0: # 如果有错误,输出错误信息 - print(f"Error: {result.stderr.decode('utf-8')}") - raise ValueError(f"Error: {result.stderr.decode('utf-8')}") + print(f"Error: {result.stderr.decode('utf-8')}") + if device == "cuda": + print(f"***当前运算模式*[{device}]*************看下换成CPU重新执行,是否解决因编码问题的报错!********") + self.stitching_video(video1_path, video2_path,"cpu",use_audio,stitching_type,os.path.dirname(output_path),scale_and_crop) else: # 输出标准输出信息 - print(result.stdout) - + print(f">>FFmpeg 执行完毕!Completed!\t stdout: {result.stdout}") return (output_path,) except Exception as e: + #print("******[2]***********看下是否执行到这里!") raise ValueError(e) \ No newline at end of file