Files
fuyouawa-comfyui-easytoolkit/utils/video/ffmpeg.py
T

176 lines
6.1 KiB
Python

import io
import shutil
import uuid
import os
import subprocess
from typing import List, Tuple, Optional
from PIL import Image
import json
import folder_paths
from .common import convert_image_batch_to_pil_list, process_image_format
ffmpeg_path = shutil.which("ffmpeg")
if ffmpeg_path is None:
print("ffmpeg could not be found. Using ffmpeg from imageio-ffmpeg.")
from imageio_ffmpeg import get_ffmpeg_exe
try:
ffmpeg_path = get_ffmpeg_exe()
except:
print("ffmpeg could not be found. Outputs that require it have been disabled")
# The code is based on ComfyUI-VideoHelperSuite modification.
def image_batch_to_video_bytes(
image_batch,
frame_rate: int,
video_format: str = "image/gif",
pingpong: bool = False,
loop_count: int = 0,
video_metadata: Optional[dict] = None,
ffmpeg_bin: Optional[str] = None,
) -> Tuple[bytes, str]:
"""
Convert image_batch to video and return bytes.
Returns (video_bytes, extension_without_dot)
- For image/* (gif, webp) use Pillow to save directly to BytesIO.
- For video/* use ffmpeg, output to temporary file first then read bytes.
- If using temporary file for output, third return value is temporary file path (caller can decide whether to keep or delete).
"""
# Convert image_batch to PIL Image list and normalize
frames = convert_image_batch_to_pil_list(image_batch)
if pingpong:
if len(frames) >= 2:
frames = frames + frames[-2:0:-1]
format_type, format_ext = video_format.split("/")
# image formats via Pillow
if format_type == "image":
return process_image_format(frames, format_ext, frame_rate, loop_count)
# --- video path (ffmpeg) ---
if ffmpeg_bin is None:
# fallback: use global ffmpeg_path captured outside
ffmpeg_bin = ffmpeg_path
if ffmpeg_bin is None:
raise ProcessLookupError("ffmpeg not found")
# Find corresponding video_format json (keeping consistent with original)
video_format_path = folder_paths.get_full_path("video_formats", format_ext + ".json")
with open(video_format_path, "r") as f:
video_format = json.load(f)
# Generate temporary output file
tmp_dir = folder_paths.get_temp_directory()
os.makedirs(tmp_dir, exist_ok=True)
# out_suffix = "." + video_format["extension"]
# tmp_out = os.path.join(tmp_dir, f"{uuid.uuid4().hex}{out_suffix}")
tmp_out = os.path.join(tmp_dir, f"{uuid.uuid4().hex}")
muxer = video_format.get("muxer", video_format["extension"])
dimensions = f"{frames[0].width}x{frames[0].height}"
metadata_json = json.dumps(video_metadata or {})
# base args: read rawvideo from stdin
args = [
ffmpeg_bin, "-v", "error",
"-f", "rawvideo", "-pix_fmt", "rgb24",
"-s", dimensions, "-r", str(frame_rate), "-i", "-"
] + video_format.get("main_pass", [])
# metadata handling - attempt to pass as -metadata comment=..., if too long fall back to using temporary metadata file
metadata_args = ["-metadata", "comment=" + metadata_json]
# estimate max arg length (copy from your original)
if os.name == 'posix':
max_arg_length = 4096 * 32
else:
# conservative estimate similar to your original
max_arg_length = 32767 - len(" ".join(args + [metadata_args[0]] + [tmp_out])) - 1
env = os.environ.copy()
if "environment" in video_format:
env.update(video_format["environment"])
if len(metadata_args[1]) >= max_arg_length:
# write metadata to temp file and use it as an extra input
_run_ffmpeg_with_metadata_file(args, frames, metadata_json, tmp_dir, tmp_out, env, muxer)
else:
# normal path: pass metadata arg directly
try:
_run_ffmpeg_with_metadata_arg(args, metadata_args, frames, tmp_out, env, muxer)
except (FileNotFoundError, OSError) as e:
# replicate original fallback triggers for very long metadata on Windows/Errno
# fall back to metadata temp file approach
_run_ffmpeg_with_metadata_file(args, frames, metadata_json, tmp_dir, tmp_out, env, muxer)
# Read tmp_out as bytes
with open(tmp_out, "rb") as f:
data = f.read()
# Delete temporary file
os.remove(tmp_out)
return data, video_format["extension"]
def _run_ffmpeg_with_metadata_file(
args: List[str],
frames: List[Image.Image],
metadata_json: str,
tmp_dir: str,
tmp_out: str,
env: dict,
muxer: str
):
"""
Helper function to run ffmpeg with temporary metadata file.
Handles metadata file creation, escaping, ffmpeg execution, and cleanup.
"""
# Create temporary metadata file
md_tmp = os.path.join(tmp_dir, f"{uuid.uuid4().hex}_metadata.txt")
with open(md_tmp, "w", encoding="utf-8") as mf:
mf.write(";FFMETADATA1\n")
# Escape dangerous characters
md = metadata_json.replace("\\", "\\\\").replace(";", "\\;").replace("#", "\\#").replace("\n", "\\\n")
mf.write(md)
# Build new arguments including metadata file
new_args = [args[0]] + ["-i", md_tmp] + args[1:]
try:
with subprocess.Popen(new_args + ["-f", muxer, tmp_out], stdin=subprocess.PIPE, env=env) as proc:
for fr in frames:
#TODO Error occurs when format is video/av1-webm
proc.stdin.write(fr.tobytes())
proc.stdin.close()
proc.wait()
finally:
# Clean up temporary metadata file
if md_tmp and os.path.exists(md_tmp):
os.remove(md_tmp)
def _run_ffmpeg_with_metadata_arg(
args: List[str],
meta_arg_list: List[str],
frames: List[Image.Image],
tmp_out: str,
env: dict,
muxer: str
):
"""
Helper function to run ffmpeg with metadata arguments directly.
"""
# run ffmpeg writing frames to stdin and create tmp_out
try:
with subprocess.Popen(args + meta_arg_list + ["-f", muxer, tmp_out], stdin=subprocess.PIPE, env=env) as proc:
for fr in frames:
# ensure rgb24 byte order
proc.stdin.write(fr.tobytes())
proc.stdin.close()
proc.wait()
except Exception:
# bubble up, caller can decide
raise