Add low-risk NVENC encoding option to ImageTransitionNode, VideoImageOverlay, and VideoSplitterNode. Each node now includes a use_gpu parameter (default: False) that enables hevc_nvenc when available, with automatic fallback to libx264 if NVIDIA GPU is unavailable or NVENC fails. Changes: - ImageTransitionNode: Add use_gpu parameter for MP4 frame encoding - VideoImageOverlay: Add use_gpu parameter for overlay video encoding - VideoSplitterNode: Add use_gpu parameter for segment encoding Expected gains: 5-10x speedup for GPU users (hevc_nvenc vs libx264) Risk level: VERY LOW (zero breaking changes, backward compatible) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
235 lines
7.6 KiB
Python
235 lines
7.6 KiB
Python
import os
|
|
import subprocess
|
|
from typing import Tuple
|
|
|
|
import comfy.utils
|
|
|
|
|
|
class VideoImageOverlay:
|
|
"""
|
|
ComfyUI custom node: Video Image Overlay (No Resize Version)
|
|
|
|
- Applies a PNG image (with optional alpha) as an overlay over a full video
|
|
using ffmpeg.
|
|
- Removes all functionality related to resizing the overlay image.
|
|
- Saves the result as an MP4 in the same directory as the input video.
|
|
- Returns the full path to the generated video as a STRING.
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"overlay_image_path": (
|
|
"STRING",
|
|
{
|
|
"default": "",
|
|
"multiline": False,
|
|
"placeholder": "Full path to overlay image (PNG with alpha)",
|
|
},
|
|
),
|
|
"video_path": (
|
|
"STRING",
|
|
{
|
|
"default": "",
|
|
"multiline": False,
|
|
"placeholder": "Full path to input video file",
|
|
},
|
|
),
|
|
"suffix": (
|
|
"STRING",
|
|
{
|
|
"default": "_overlay",
|
|
"multiline": False,
|
|
"placeholder": "Suffix for output video filename (before .mp4)",
|
|
},
|
|
),
|
|
"delete_original": (
|
|
"BOOLEAN",
|
|
{
|
|
"default": False,
|
|
},
|
|
),
|
|
},
|
|
"optional": {
|
|
"use_gpu": ("BOOLEAN", {"default": False, "label": "Use GPU encoding (NVENC) if available"})
|
|
}
|
|
}
|
|
|
|
# Returns one STRING: the full path to the output video
|
|
RETURN_TYPES = ("STRING",)
|
|
RETURN_NAMES = ("output_video_path",)
|
|
|
|
# This function gets called by ComfyUI
|
|
FUNCTION = "apply_overlay"
|
|
|
|
# Category in the node menu
|
|
CATEGORY = "Simple Video Effects/Video Processing"
|
|
|
|
# Mark as an OUTPUT node so it can run without needing any input
|
|
OUTPUT_NODE = True
|
|
|
|
def _validate_paths(self, overlay_image_path: str, video_path: str) -> None:
|
|
if not overlay_image_path:
|
|
raise ValueError("overlay_image_path is empty.")
|
|
if not video_path:
|
|
raise ValueError("video_path is empty.")
|
|
|
|
if not os.path.isfile(overlay_image_path):
|
|
raise FileNotFoundError(f"Overlay image not found: {overlay_image_path}")
|
|
if not os.path.isfile(video_path):
|
|
raise FileNotFoundError(f"Video file not found: {video_path}")
|
|
|
|
def _build_output_path(self, video_path: str, suffix: str) -> str:
|
|
"""Build the output .mp4 path in the same directory as the input video, avoiding overwrites."""
|
|
# Use the same directory as the input video
|
|
output_dir = os.path.dirname(video_path)
|
|
|
|
# Use the input path (normalized) for naming
|
|
base_name = os.path.basename(video_path)
|
|
stem, _ext = os.path.splitext(base_name)
|
|
|
|
# Base name + suffix, e.g. myvideo_overlay
|
|
suffix = suffix or ""
|
|
stem_with_suffix = f"{stem}{suffix}"
|
|
|
|
# First candidate: myvideo_overlay.mp4
|
|
output_path = os.path.join(output_dir, f"{stem_with_suffix}.mp4")
|
|
|
|
# If it doesn't exist yet, use it directly
|
|
if not os.path.exists(output_path):
|
|
return output_path
|
|
|
|
# If it exists, append _0001, _0002, ...
|
|
counter = 1
|
|
while True:
|
|
# e.g. myvideo_overlay_0001.mp4, myvideo_overlay_0002.mp4, ...
|
|
candidate = os.path.join(output_dir, f"{stem_with_suffix}_{counter:04d}.mp4")
|
|
if not os.path.exists(candidate):
|
|
return candidate
|
|
counter += 1
|
|
|
|
def _build_ffmpeg_command(
|
|
self,
|
|
overlay_image_path: str,
|
|
video_path: str,
|
|
output_path: str,
|
|
use_gpu: bool = False,
|
|
) -> list:
|
|
|
|
cmd = [
|
|
"ffmpeg",
|
|
"-y",
|
|
"-i", video_path, # use the (normalized) input video path
|
|
"-i", overlay_image_path, # overlay image
|
|
]
|
|
|
|
# No resizing: direct overlay
|
|
filter_complex = "[0:v][1:v]overlay=0:0:format=auto[vout]"
|
|
|
|
vcodec = 'hevc_nvenc' if use_gpu else 'libx264'
|
|
|
|
cmd.extend([
|
|
"-filter_complex", filter_complex,
|
|
|
|
# Output the video stream
|
|
"-map", "[vout]",
|
|
|
|
# Optional audio (won't error if missing)
|
|
"-map", "0:a?",
|
|
|
|
# Encoding settings
|
|
"-c:v", vcodec,
|
|
"-pix_fmt", "yuv420p",
|
|
"-c:a", "aac",
|
|
|
|
# Output file path
|
|
output_path,
|
|
])
|
|
|
|
return cmd
|
|
|
|
def _run_ffmpeg_with_progress(self, cmd: list) -> None:
|
|
# Use ComfyUI's ProgressBar
|
|
pbar = comfy.utils.ProgressBar(1)
|
|
try:
|
|
result = subprocess.run(
|
|
cmd,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
finally:
|
|
# Ensure the bar completes even on error
|
|
pbar.update(1)
|
|
|
|
if result.returncode != 0:
|
|
err_snippet = (result.stderr or "").splitlines()[-10:]
|
|
raise RuntimeError(
|
|
"ffmpeg failed with code {}.\nCommand: {}\nStderr:\n{}".format(
|
|
result.returncode,
|
|
" ".join(cmd),
|
|
"\n".join(err_snippet),
|
|
)
|
|
)
|
|
|
|
def apply_overlay(
|
|
self,
|
|
overlay_image_path: str,
|
|
video_path: str,
|
|
suffix: str,
|
|
delete_original: bool = False,
|
|
use_gpu: bool = False,
|
|
) -> Tuple[str]:
|
|
|
|
# Normalize paths once here so everything else uses the same real paths
|
|
overlay_image_path = os.path.abspath(overlay_image_path)
|
|
video_path = os.path.abspath(video_path)
|
|
|
|
self._validate_paths(overlay_image_path, video_path)
|
|
|
|
output_path = self._build_output_path(video_path, suffix)
|
|
|
|
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
|
|
|
cmd = self._build_ffmpeg_command(
|
|
overlay_image_path=overlay_image_path,
|
|
video_path=video_path,
|
|
output_path=output_path,
|
|
use_gpu=use_gpu,
|
|
)
|
|
|
|
try:
|
|
self._run_ffmpeg_with_progress(cmd)
|
|
except FileNotFoundError:
|
|
raise RuntimeError(
|
|
"ffmpeg executable not found. Make sure ffmpeg is installed and available in PATH."
|
|
)
|
|
except Exception as e:
|
|
# If NVENC fails, fallback to libx264
|
|
if use_gpu and "hevc_nvenc" in str(e):
|
|
print(f"NVENC encoding failed, falling back to libx264...")
|
|
cmd = self._build_ffmpeg_command(
|
|
overlay_image_path=overlay_image_path,
|
|
video_path=video_path,
|
|
output_path=output_path,
|
|
use_gpu=False,
|
|
)
|
|
self._run_ffmpeg_with_progress(cmd)
|
|
else:
|
|
raise
|
|
|
|
# If toggled on, delete the original video after processing
|
|
if delete_original:
|
|
try:
|
|
if os.path.exists(video_path):
|
|
os.remove(video_path)
|
|
else:
|
|
print(f"[VideoImageOverlay] Cannot delete original, not found: {video_path}")
|
|
except Exception as e:
|
|
# Avoid crashing the node just because deletion failed
|
|
print(f"[VideoImageOverlay] Failed to delete original: {e}")
|
|
|
|
return (output_path,)
|