修改缝合视频ffmpeg功能,支持裁剪和背景填充;添加画中画ffmpeg节点。
This commit is contained in:
@@ -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")
|
||||
+40
-9
@@ -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)
|
||||
Reference in New Issue
Block a user