diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c38e791
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/fonts
+/__pycache__
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..61b2a6b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,91 @@
+
ComfyUI-FFmpeg
+
+
+
中文 | English
+
+
+
+## 介绍
+
+把FFmpeg常用功能封装成ComfyUI节点,方便用户可以在ComfyUI上也可以进行各种视频处理。
+
+## 安装
+
+#### 方法1:
+
+1. 进入节点目录, `ComfyUI/custom_nodes/`
+2. `git clone https://github.com/MoonHugo/ComfyUI-FFmpeg.git`
+3. `cd ComfyUI-FFmpeg`
+4. `pip install -r requirements.txt`
+5. 重启ComfyUI
+
+#### 方法2:
+直接下载节点源码包,然后解压到custom_nodes目录下,最后重启ComfyUI
+
+#### 方法3:
+通过ComfyUI-Manager安装,搜索“ComfyUI-FFmpeg”进行安装
+
+## 节点介绍
+
+##### Video2Frames节点: 作用是将视频转为一张一张的图片,并保存到指定目录中
+
+
+
+###### 参数说明
+**video_path**: 本地视频路径,比如:`C:\Users\Desktop\222.mp4`
+**output_path**: 输出图片保存路径,比如:`C:\Users\Desktop\output`
+**frames_max_width**: 这个参数可以用来缩放视频,默认为0,表示不缩放视频,如果frames_max_width大于视频实际宽度,则视频不会被放大,保持原宽度,如果frames_max_width小于视频实际宽度,则视频会被缩小。
+
+___
+
+##### Frames2Video节点: 作用是将图片转为视频,并保存到指定目录中
+
+
+###### 参数说明
+**frame_path**: 本地图片路径,比如:`C:\Users\Desktop\output`
+**fps**: 视频帧率,默认为`30`
+**video_name**: 保存视频名称,比如:`222.mp4`
+**output_path**: 视频保存路径,比如:`C:\Users\Desktop\output`
+**audio_path**: 视频音频路径,比如:`C:\Users\Desktop\222.mp3`
+___
+
+##### AddTextWatermark节点: 作用是在视频上添加文字水印
+
+
+
+###### 参数说明
+**video_path**: 本地视频路径,比如:`C:\Users\Desktop\222.mp4`
+**output_path**: 视频保存路径,比如:`C:\Users\Desktop\output`
+**font_file**: 字体文件,需要把字体文件放到`custom_nodes\ComfyUI-FFmpeg\fonts`目录下,不仅英文字体,中文字体也可以,比如:`ComfyUI\custom_nodes\ComfyUI-FFmpeg\fonts\Alibaba-PuHuiTi-Heavy.ttf`
+**font_size**: 水印文字大小,比如:`40`
+**font_color**: 水印文字颜色,比如:`#FFFFFF`或者`white`
+**position_x**: 水印文字x坐标,比如:`100`
+**position_y**: 水印文字y坐标,比如:`100`
+
+___
+
+##### AddImgWatermark节点: 作用是在视频上添加图片水印
+
+
+
+###### 参数说明
+**video_path**: 本地视频路径,比如:`C:\Users\Desktop\222.mp4`
+**output_path**: 视频保存路径,比如:`C:\Users\Desktop\output`
+**watermark_image**: 水印图片路径,比如:`C:\Users\Desktop\watermark.png`
+**watermark_img_width**: 水印图片宽度,比如:`100`
+**position_x**: 水印图片在视频中的x坐标,比如:`100`
+**position_y**: 水印图片在视频中的y坐标,比如:`100`
+___
+
+## 社交账号
+- Bilibili:[我的B站主页](https://space.bilibili.com/1303099255)
+
+## 感谢
+
+感谢FFmpeg仓库的所有作者 [FFmpeg/FFmpeg](https://github.com/FFmpeg/FFmpeg)
+
+部分代码参考了 [Eden-yidun/ComfyUI-EdenVideoTool](https://github.com/Eden-yidun/ComfyUI-EdenVideoTool) 感谢!
+
+## 关注历史
+
+[](https://star-history.com/#MoonHugo/ComfyUI-FFmpeg&Date)
\ No newline at end of file
diff --git a/README_EN.md b/README_EN.md
new file mode 100644
index 0000000..7dabab3
--- /dev/null
+++ b/README_EN.md
@@ -0,0 +1,91 @@
+ComfyUI-FFmpeg
+
+
+
English | 中文
+
+
+
+## Introduction
+
+Encapsulate the commonly used functions of FFmpeg into ComfyUI nodes, making it convenient for users to perform various video processing tasks within ComfyUI.
+
+## Installation
+
+#### Method 1:
+
+1. Go to comfyUI custom_nodes folder, `ComfyUI/custom_nodes/`
+2. `git clone https://github.com/MoonHugo/ComfyUI-FFmpeg.git`
+3. `cd ComfyUI-FFmpeg`
+4. `pip install -r requirements.txt`
+5. restart ComfyUI
+
+#### Method 2:
+Directly download the node source package, then extract it into the custom_nodes directory, and finally restart ComfyUI.
+
+#### Method 3:
+Install through ComfyUI-Manager by searching for 'ComfyUI-BiRefNet-Hugo' and installing it.
+
+## Nodes introduction
+
+##### Video2Frames Node: The function is to convert a video into images and save them to a specified directory.
+
+
+
+###### Parameter Description
+**video_path**: The local video path, e.g:`C:\Users\Desktop\222.mp4`
+**output_path**: The path to save the output images, e.g:`C:\Users\Desktop\output`
+**frames_max_width**: This parameter can be used to resize the video. The default value is 0, which means the video will not be resized. If frames_max_width is larger than the actual width of the video, the video will not be enlarged and will retain its original width. If frames_max_width is smaller than the actual width of the video, the video will be scaled down.
+
+___
+
+##### Frames2Video Node: The function is to convert images into a video and save it to a specified directory.
+
+
+###### Parameter Description
+**frame_path**: local image path, e.g:`C:\Users\Desktop\output`
+**fps**: video frame rate, default is`30`
+**video_name**: saved video name, e.g:`222.mp4`
+**output_path**: video save path,e.g:`C:\Users\Desktop\output`
+**audio_path**: video audio path,e.g:`C:\Users\Desktop\222.mp3`
+___
+
+##### AddTextWatermark Node: The function is to add a text watermark to the video.
+
+
+
+###### Parameter Description
+**video_path**: local video path,e.g:`C:\Users\Desktop\222.mp4`
+**output_path**: video save path,e.g:`C:\Users\Desktop\output`
+**font_file**: font file: The font file needs to be placed in the`custom_nodes\ComfyUI-FFmpeg\fonts` directory. Not only English fonts, but Chinese fonts can also be used.,e.g:`ComfyUI\custom_nodes\ComfyUI-FFmpeg\fonts\Alibaba-PuHuiTi-Heavy.ttf`
+**font_size**: watermark text size,e.g:`40`
+**font_color**: watermark text color,e.g:`#FFFFFF` or `white`
+**position_x**: watermark text x-coordinate,e.g:`100`
+**position_y**: watermark text y-coordinate,e.g:`100`
+
+___
+
+##### AddImgWatermark Node: The function is to add an image watermark to the video.
+
+
+
+###### Parameter Description
+**video_path**: local video path,e.g:`C:\Users\Desktop\222.mp4`
+**output_path**: video save path,e.g:`C:\Users\Desktop\output`
+**watermark_image**: watermark image path,e.g:`C:\Users\Desktop\watermark.png`
+**watermark_img_width**: watermark image width,e.g:`100`
+**position_x**: watermark image x-coordinate in the video,e.g:`100`
+**position_y**: watermark image y-coordinate in the video,e.g:`100`
+___
+
+## Social Account Homepage
+- Bilibili:[My BILIBILI Homepage](https://space.bilibili.com/1303099255)
+
+## Acknowledgments
+
+Thanks to all the contributors of the FFmpeg repository. [FFmpeg/FFmpeg](https://github.com/FFmpeg/FFmpeg)
+
+Some of the code references [Eden-yidun/ComfyUI-EdenVideoTool](https://github.com/Eden-yidun/ComfyUI-EdenVideoTool) Thanks!
+
+## Star history
+
+[](https://star-history.com/#MoonHugo/ComfyUI-FFmpeg&Date)
\ No newline at end of file
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000..3d99661
--- /dev/null
+++ b/__init__.py
@@ -0,0 +1,3 @@
+from .nodes_map import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
+
+__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
\ No newline at end of file
diff --git a/assets/1.png b/assets/1.png
new file mode 100644
index 0000000..632c168
Binary files /dev/null and b/assets/1.png differ
diff --git a/assets/2.png b/assets/2.png
new file mode 100644
index 0000000..332aba9
Binary files /dev/null and b/assets/2.png differ
diff --git a/assets/3.png b/assets/3.png
new file mode 100644
index 0000000..80a396a
Binary files /dev/null and b/assets/3.png differ
diff --git a/assets/4.png b/assets/4.png
new file mode 100644
index 0000000..f577db3
Binary files /dev/null and b/assets/4.png differ
diff --git a/func.py b/func.py
new file mode 100644
index 0000000..149a4f5
--- /dev/null
+++ b/func.py
@@ -0,0 +1,48 @@
+import numpy as np
+from PIL import Image
+import torch
+import subprocess
+import json
+
+def tensor2pil(image):
+ return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
+
+def pil2tensor(image):
+ return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
+
+def getVideoInfo(video_path):
+ command = [
+ 'ffprobe', '-v', 'error', '-select_streams', 'v:0', '-show_entries',
+ 'stream=avg_frame_rate,duration,width,height', '-of', 'json', video_path
+ ]
+ # 运行ffprobe命令
+ result = subprocess.run(command, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
+ # 将输出转化为字符串
+ output = result.stdout.decode('utf-8').strip()
+ print(output)
+ data = json.loads(output)
+ # 查找视频流信息
+ if 'streams' in data and len(data['streams']) > 0:
+ stream = data['streams'][0] # 获取第一个视频流
+ fps = stream.get('avg_frame_rate')
+ if fps is not None:
+ # 帧率可能是一个分数形式的字符串,例如 "30/1" 或 "20.233000"
+ if '/' in fps:
+ num, denom = map(int, fps.split('/'))
+ fps = num / denom
+ else:
+ fps = float(fps) # 直接转换为浮点数
+ width = int(stream.get('width'))
+ height = int(stream.get('height'))
+ duration = float(stream.get('duration'))
+ return_data = {'fps': fps, 'width': width, 'height': height, 'duration': duration}
+ else:
+ return_data = {}
+ return return_data
+
+def get_image_size(image_path):
+ # 打开图像文件
+ with Image.open(image_path) as img:
+ # 获取图像的宽度和高度
+ width, height = img.size
+ return width, height
\ No newline at end of file
diff --git a/nodes/__init__.py b/nodes/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/nodes/__pycache__/__init__.cpython-310.pyc b/nodes/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..f76d762
Binary files /dev/null and b/nodes/__pycache__/__init__.cpython-310.pyc differ
diff --git a/nodes/__pycache__/addImgWatermark.cpython-310.pyc b/nodes/__pycache__/addImgWatermark.cpython-310.pyc
new file mode 100644
index 0000000..2b6bc5a
Binary files /dev/null and b/nodes/__pycache__/addImgWatermark.cpython-310.pyc differ
diff --git a/nodes/__pycache__/addTextWatermark.cpython-310.pyc b/nodes/__pycache__/addTextWatermark.cpython-310.pyc
new file mode 100644
index 0000000..ca3343c
Binary files /dev/null and b/nodes/__pycache__/addTextWatermark.cpython-310.pyc differ
diff --git a/nodes/__pycache__/frames2video.cpython-310.pyc b/nodes/__pycache__/frames2video.cpython-310.pyc
new file mode 100644
index 0000000..d22a2cb
Binary files /dev/null and b/nodes/__pycache__/frames2video.cpython-310.pyc differ
diff --git a/nodes/__pycache__/video2frames.cpython-310.pyc b/nodes/__pycache__/video2frames.cpython-310.pyc
new file mode 100644
index 0000000..5b1897e
Binary files /dev/null and b/nodes/__pycache__/video2frames.cpython-310.pyc differ
diff --git a/nodes/addImgWatermark.py b/nodes/addImgWatermark.py
new file mode 100644
index 0000000..d876694
--- /dev/null
+++ b/nodes/addImgWatermark.py
@@ -0,0 +1,79 @@
+import os
+from PIL import Image
+import subprocess
+import time
+from ..func import get_image_size
+
+
+class AddImgWatermark:
+
+ # 初始化方法
+ def __init__(self):
+ pass
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {
+ "required": {
+ "video_path": ("STRING", {"default":"C:/Users/Desktop/video.mp4",}),
+ "output_path": ("STRING", {"default":"C:/Users/Desktop/output/",}),
+ "watermark_image": ("STRING", {"default":"C:/Users/Desktop/logo.png",}),
+ "watermark_img_width": ("INT", {"default": 100,"min": 1, "step": 1}),
+ "position_x": ("INT", {"default": 10, "step": 1}),
+ "position_y": ("INT", {"default": 10, "step": 1}),
+ },
+ }
+
+ RETURN_TYPES = ("STRING",)
+ RETURN_NAMES = ("video_complete_path",)
+ FUNCTION = "add_img_watermark"
+ OUTPUT_NODE = True
+ CATEGORY = "FFmpeg"
+
+ def add_img_watermark(self,video_path,output_path,watermark_image,watermark_img_width,position_x,position_y):
+ try:
+ # 视频不存在
+ if not video_path.lower().endswith(('.mp4', '.avi', '.mov', '.mkv','.rmvb')):
+ raise ValueError("video_path不是视频文件(video_path is not a video file)")
+
+ if not os.path.exists(video_path):
+ raise ValueError("video_path不存在(video_path does not exist)")
+
+ #判断output_path是否是一个目录
+ if not os.path.isdir(output_path):
+ raise ValueError("output_path不是目录(output_path is not a directory)")
+
+ # 文件不是图片
+ if not watermark_image.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif')):
+ raise ValueError("水印文件不是图片文件(watermark file is not a image file)")
+
+ if not os.path.exists(watermark_image):
+ raise ValueError("水印文件不存在(watermark file does not exist)")
+
+ file_name = os.path.basename(video_path)
+ file_extension = os.path.splitext(file_name)[1]
+ #文件名根据年月日时分秒来命名
+ file_name = time.strftime("%Y%m%d%H%M%S", time.localtime()) + file_extension
+ output_path = os.path.join(output_path, file_name)
+ width,height = get_image_size(watermark_image)
+ watermark_img_height = int(height * watermark_img_width / width) # 按比例计算新高度
+ cmd = [
+ 'ffmpeg',
+ '-i', video_path,
+ '-i', watermark_image,
+ '-filter_complex',f"[1:v]scale={watermark_img_width}:{watermark_img_height}[wm];[0:v][wm]overlay=x={position_x}:y={position_y}:format=auto",
+ output_path,
+ ]
+ result = subprocess.run(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
+ #os.remove(image_save_path) # 删除临时水印图片
+ # 检查返回码
+ if result.returncode != 0:
+ # 如果有错误,输出错误信息
+ print(f"Error: {result.stderr.decode('utf-8')}")
+ raise ValueError(f"Error: {result.stderr.decode('utf-8')}")
+ else:
+ # 输出标准输出信息
+ print(result.stdout)
+ except Exception as e:
+ raise ValueError(e)
+ return (output_path,)
\ No newline at end of file
diff --git a/nodes/addTextWatermark.py b/nodes/addTextWatermark.py
new file mode 100644
index 0000000..2a7431c
--- /dev/null
+++ b/nodes/addTextWatermark.py
@@ -0,0 +1,85 @@
+import os
+import subprocess
+import folder_paths
+import time
+
+current_path = os.path.abspath(__file__)
+font_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.normpath(__file__))), 'fonts')
+folder_paths.folder_names_and_paths["fonts"] = ([font_dir], {'.ttf'})
+
+class AddTextWatermark:
+
+ # 初始化方法
+ def __init__(self):
+ pass
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {
+ "required": {
+ "video_path": ("STRING", {"default":"C:/Users/Desktop/video.mp4",}),
+ "output_path": ("STRING", {"default":"C:/Users/Desktop/output/",}),
+ 'font_file': (["default"] + folder_paths.get_filename_list("fonts"), ),
+ 'font_size': ("INT", {"default": 15, "min": 1, "max": 1000, "step": 1}),
+ 'font_color': ("STRING", {"default": "#FFFFFF"}),
+ "text": ("STRING", {"default": "Watermark"}),
+ "position_x": ("INT", {"default": 10, "step": 1}),
+ "position_y": ("INT", {"default": 10, "step": 1}),
+ },
+ }
+
+ RETURN_TYPES = ("STRING","STRING",)
+ RETURN_NAMES = ("video_path","output_path",)
+ FUNCTION = "add_text_watermark"
+ OUTPUT_NODE = True
+ CATEGORY = "FFmpeg"
+
+ def add_text_watermark(self,video_path,output_path,font_file,font_size,font_color,text,position_x,position_y):
+ try:
+ # 视频不存在
+ if not video_path.lower().endswith(('.mp4', '.avi', '.mov', '.mkv','.rmvb')):
+ raise ValueError("video_path不是视频文件(video_path is not a video file)")
+
+ if not os.path.exists(video_path):
+ raise ValueError("video_path不存在(video_path does not exist)")
+
+ #判断output_path是否是一个目录
+ if not os.path.isdir(output_path):
+ raise ValueError("output_path不是目录(output_path is not a directory)")
+
+ file_name = os.path.basename(video_path)
+ file_extension = os.path.splitext(file_name)[1]
+ #文件名根据年月日时分秒来命名
+ file_name = time.strftime("%Y%m%d%H%M%S", time.localtime()) + file_extension
+ output_path = os.path.join(output_path, file_name)
+
+ # 替换为双斜杠
+ font_path = os.path.join(font_dir, font_file).replace("\\", "/").replace(":", "\\:")
+ # 构建命令 C\\:/Windows/Fonts/simhei.ttf fontfile='J\\:/Comfyui-for-OOTDiffusion/ComfyUI/custom_nodes/ComfyUI-FFmpeg/fonts/Alibaba-PuHuiTi-Heavy.ttf
+ if font_file == "default":
+ cmd = [
+ 'ffmpeg',
+ '-i', video_path,
+ '-vf', f"drawtext=text='{text}':x={position_x}:y={position_y}:fontsize={font_size}:fontcolor={font_color}",
+ output_path,
+ ]
+ else:
+ cmd = [
+ 'ffmpeg',
+ '-i', video_path,
+ '-vf', f"drawtext=text='{text}':x={position_x}:y={position_y}:fontfile='{font_path}':fontsize={font_size}:fontcolor={font_color}",
+ output_path,
+ ]
+ result = subprocess.run(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
+ # 检查返回码
+ if result.returncode != 0:
+ # 如果有错误,输出错误信息
+ print(f"Error: {result.stderr.decode('utf-8')}")
+ raise ValueError(f"Error: {result.stderr.decode('utf-8')}")
+ else:
+ # 输出标准输出信息
+ print(result.stdout)
+ except Exception as e:
+ raise ValueError(e)
+
+ return (video_path,output_path)
\ No newline at end of file
diff --git a/nodes/frames2video.py b/nodes/frames2video.py
new file mode 100644
index 0000000..8bc33fd
--- /dev/null
+++ b/nodes/frames2video.py
@@ -0,0 +1,125 @@
+import os
+import subprocess
+import re
+from PIL import Image
+from pathlib import Path
+
+class Frames2Video:
+
+ # 初始化方法
+ def __init__(self):
+ pass
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {
+ "required": {
+ "frame_path": ("STRING", {"default": "C:/Users/Desktop",}),
+ "fps": ("FLOAT", {
+ "default": 30,
+ "min": 1,
+ "max": 120,
+ "step": 1,
+ "display": "number"
+ }),
+ "video_name": ("STRING", {"default": "new_video"}),
+ "output_path": ("STRING", {"default": "./output"}),
+ },
+ "optional":{
+ "audio_path":("STRING",{"default": "C:/Users/audio.mp3",}),
+ }
+ }
+
+ RETURN_TYPES = ("STRING","STRING",)
+ RETURN_NAMES = ("frame_path","output_path",)
+ FUNCTION = "frames2video"
+ OUTPUT_NODE = True
+ CATEGORY = "FFmpeg"
+
+ def frames2video(self,frame_path,fps,video_name,output_path,audio_path=None):
+ try:
+ if not os.path.exists(frame_path):
+ raise ValueError("frame_path不存在(frame_path does not exist)")
+
+ #判断output_path是否是一个目录
+ if not os.path.isdir(output_path):
+ raise ValueError("output_path不是目录(output_path is not a directory)")
+ output_path = f"{output_path}\{video_name}.mp4" # 将输出目录和输出文件名合并为一个输出路径
+ # 获取输入目录中的所有图像文件
+ frame_path = Path(frame_path)
+ image_files = sorted(frame_path.glob('*.png')) + sorted(frame_path.glob('*.PNG')) + sorted(frame_path.glob('*.Png'))
+
+ # 获取第一张图像的文件名,不要路径
+ image_name = os.path.basename(image_files[0])
+ print(f"第一张图像文件名: {image_name}")
+
+ # 获取图像文件名中的数字个数,包括零
+ num_digits = sum(c.isdigit() for c in image_name)
+ print(f"转换后的图像文件名: {num_digits}")
+
+ # 将图像文件名中的数字替换为%0{num_digits}d,只影响数字,不影响其他字符
+ image_name = re.sub(r'\d+', '%0{}d'.format(num_digits), image_name)
+ print(f"转换后的图像文件名: {image_name}")
+
+ # 构建输出目录
+ if not image_files:
+ print(f"Files in directory: {os.listdir(frame_path)}")
+ raise FileNotFoundError(f"No image files found in directory: {frame_path}")
+
+ # 获取第一张图像的尺寸
+ with Image.open(image_files[0]) as img:
+ width, height = img.size
+ scale = f'{width}:{height}'
+
+ if audio_path:
+ # 有音频文件,构建ffmpeg命令
+ ffmpeg_cmd = [
+ 'ffmpeg',
+ '-framerate', str(fps),
+ '-i', f'{frame_path}/{image_name}',
+ '-i', audio_path, # 添加音频文件路径
+ '-vf', f'scale={scale}',
+ '-c:v', 'libx264',
+ '-preset', 'medium',
+ '-crf', '28',
+ '-pix_fmt', 'yuv420p',
+ '-c:a', 'aac', # 指定音频编解码器
+ '-shortest',
+ '-y',
+ str(output_path)
+ ]
+ # 执行ffmpeg命令
+ process = subprocess.Popen(ffmpeg_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+
+ stdout, stderr = process.communicate()
+ if process.returncode != 0:
+ print(f"ffmpeg 执行失败,错误信息: {stderr.decode('utf-8')}")
+ else:
+ print(f"视频合成成功: {output_path}")
+
+ else:
+ # 无音频文件,构建ffmpeg命令
+ ffmpeg_cmd = [
+ 'ffmpeg',
+ '-framerate', str(fps),
+ '-i', f'{frame_path}/{image_name}',
+ '-vf', f'scale={scale}',
+ '-c:v', 'libx264',
+ '-preset', 'medium',
+ '-crf', '28',
+ '-pix_fmt', 'yuv420p',
+ '-y',
+ str(output_path)
+ ]
+ # 执行ffmpeg命令
+ process = subprocess.Popen(ffmpeg_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+
+ stdout, stderr = process.communicate()
+ if process.returncode != 0:
+ print(f"ffmpeg 执行失败,错误信息: {stderr.decode('utf-8')}")
+ else:
+ print(f"视频合成成功: {output_path}")
+ frame_path = str(frame_path) # 输出路径为字符串
+ return (frame_path,output_path)
+ except Exception as e:
+ raise ValueError(e)
\ No newline at end of file
diff --git a/nodes/video2frames.py b/nodes/video2frames.py
new file mode 100644
index 0000000..6ecf86d
--- /dev/null
+++ b/nodes/video2frames.py
@@ -0,0 +1,121 @@
+import json
+import math
+import os
+import subprocess
+
+class Video2Frames:
+ def __init__(self):
+ pass
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ "video_path": ("STRING", {"default":"C:/Users/Desktop/video.mp4",}),
+ "output_path": ("STRING", {"default":"C:/Users/Desktop/output",}),
+ "frames_max_width":("INT", {"default": 0, "min": 0, "max": 1920}),
+ },
+ }
+
+ RETURN_TYPES = ("STRING", "FLOAT", "STRING", "INT","STRING")
+ RETURN_NAMES = ("frame_path", "fps", "audio_path", "total_frames","output_path")
+ FUNCTION = "video2frames"
+ OUTPUT_NODE = True
+ CATEGORY = "FFmpeg"
+
+ def video2frames(self, video_path, output_path, frames_max_width):
+ try:
+ # 视频不存在
+ if not video_path.lower().endswith(('.mp4', '.avi', '.mov', '.mkv','.rmvb')):
+ raise ValueError("video_path不是视频文件(video_path is not a video file)")
+
+ if not os.path.exists(video_path):
+ raise ValueError("video_path不存在(video_path does not exist)")
+
+ #判断output_path是否是一个目录
+ if not os.path.isdir(output_path):
+ raise ValueError("output_path不是目录(output_path is not a directory)")
+
+ # 判断frames_max_width是否是一个整数
+ if not isinstance(frames_max_width, int):
+ raise ValueError("frames_max_width不是整数(frames_max_width is not an integer)")
+
+ # 提取音频
+ audio_path = os.path.join(output_path, 'audio.mp3')
+ video_path = os.path.abspath(video_path).replace("\ufeff", "").strip()
+ audio_cmd = [
+ 'ffmpeg', '-i', video_path,
+ '-q:a', '0', '-map', 'a', '-y', audio_path
+ ]
+ subprocess.run(audio_cmd)
+
+ # 获取视频帧率、时长、宽高信息
+ command = [
+ 'ffprobe', '-v', 'error', '-select_streams', 'v:0', '-show_entries',
+ 'stream=avg_frame_rate,duration,width,height', '-of', 'json', video_path
+ ]
+
+ # 运行ffprobe命令
+ result = subprocess.run(command, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
+ # 将输出转化为字符串
+ output = result.stdout.decode('utf-8').strip()
+ print(output)
+ data = json.loads(output)
+ # 查找视频流信息
+ if 'streams' in data and len(data['streams']) > 0:
+ stream = data['streams'][0] # 获取第一个视频流
+ fps = stream.get('avg_frame_rate')
+ if fps is not None:
+ # 帧率可能是一个分数形式的字符串,例如 "30/1" 或 "20.233000"
+ if '/' in fps:
+ num, denom = map(int, fps.split('/'))
+ fps = num / denom
+ else:
+ fps = float(fps) # 直接转换为浮点数
+ else:
+ raise ValueError("无法获取视频的帧率")
+ width = int(stream.get('width'))
+ height = int(stream.get('height'))
+ duration = float(stream.get('duration'))
+ else:
+ raise ValueError("无法获取视频信息")
+
+ # 计算总帧数
+ total_frames = math.ceil(fps * duration)
+ print(f"视频的帧率是: {fps}, 宽度是: {width}, 高度是: {height}, 时长是: {duration}, 总帧数是: {total_frames}")
+ # 提取帧
+ frame_path = os.path.join(output_path, 'frames')
+ os.makedirs(frame_path, exist_ok=True) # exist_ok=True表示如果目录已经存在,不会引发异常
+
+ # 计算输出宽度和高度以保持比例
+ if frames_max_width > 0:
+ if width > frames_max_width:
+ out_width = frames_max_width
+ out_height = int(height * frames_max_width / width) # 按比例计算新高度
+ else:
+ out_width = width
+ out_height = height
+ else:
+ out_width = width
+ out_height = height
+
+ command = [
+ 'ffmpeg', '-i', video_path, # 输入视频路径
+ '-vf', f'scale={out_width}:{out_height}', # 使用scale滤镜缩放帧
+ os.path.join(frame_path, 'frame_%06d.png') # 输出帧路径
+ ]
+
+ # 执行命令并检查错误
+ if subprocess.run(command).returncode != 0:
+ print("生成序列帧时出错,请检查输入文件和路径。")
+
+ # 打印信息
+ print(f"序列帧图片输出路径: {frame_path}")
+ print(f"音频输出路径: {audio_path}")
+ print(f"总帧数: {total_frames}")
+ print(f"帧率: {fps}")
+ print(f"输出路径: {output_path}")
+
+ return (frame_path, fps, audio_path, total_frames,output_path)
+ except Exception as e:
+ raise ValueError(e)
\ No newline at end of file
diff --git a/nodes_map.py b/nodes_map.py
new file mode 100644
index 0000000..3ae3fe1
--- /dev/null
+++ b/nodes_map.py
@@ -0,0 +1,19 @@
+from .nodes.addTextWatermark import *
+from .nodes.frames2video import *
+from .nodes.video2frames import *
+from .nodes.addImgWatermark import *
+
+NODE_CLASS_MAPPINGS = {
+ "Video2Frames": Video2Frames,
+ "Frames2Video": Frames2Video,
+ "AddTextWatermark": AddTextWatermark,
+ "AddImgWatermark": AddImgWatermark,
+}
+
+# A dictionary that contains the friendly/humanly readable titles for the nodes
+NODE_DISPLAY_NAME_MAPPINGS = {
+ "Video2Frames": "🔥Video2Frames",
+ "Frames2Video": "🔥Frames2Video",
+ "AddTextWatermark": "🔥AddTextWatermark",
+ "AddImgWatermark": "🔥AddImgWatermark",
+}
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..54f7e1b
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1 @@
+pathlib
\ No newline at end of file