fix: Save Video falls back to the CPU when the GPU encoder will not open, in gpu mode too, and says why

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Q78Fcj3Wf9gZpbXmSAUuW
This commit is contained in:
Claude
2026-10-17 17:59:13 +00:00
parent e583a3c5cf
commit c3a40be163
2 changed files with 37 additions and 20 deletions
+20 -17
View File
@@ -26,6 +26,9 @@ CODECS = ("h264", "h265")
NVENC = {"h265": ("hevc_nvenc", {"preset": "p5", "rc": "vbr", "cq": "23", "b:v": "0"}),
"h264": ("h264_nvenc", {"preset": "p5", "rc": "vbr", "cq": "23", "b:v": "0"})}
X264 = ("libx264", {"crf": "23"})
# FFmpeg's "minimum required driver" line compares the driver library the PROCESS loaded, not what nvidia-smi reports
_HINT = (" (nvidia-smi can look fine while FFmpeg loads an older or missing libnvidia-encode.so.1; "
"in a container check it is there and that the NVIDIA 'video' capability is on)")
def _write(path, frames, fps, audio, metadata, codec):
@@ -57,27 +60,27 @@ def _write(path, frames, fps, audio, metadata, codec):
def encode(path, frames, fps, audio=None, metadata=None, encoder="auto", codec="h264"):
"""-> which encoder wrote the file. `frames`: uint8 [T, H, W, 3]. GPU first unless told otherwise;
a GPU attempt that fails leaves no half file and falls back to CPU H.264, unless "gpu" was demanded."""
"""-> which encoder wrote the file. `frames`: uint8 [T, H, W, 3]. GPU first unless "cpu" is chosen;
a GPU attempt that fails leaves no half file and falls back to CPU H.264 with a Log alert saying why."""
if audio is not None and not bool(audio["waveform"].isfinite().all()): # NaN/Inf sound (typically the model loaded in fp16): AAC refuses it, so the video is kept without it
audio = None
log.alert("FunPack Save Video", "The audio contains NaN/Inf values, so you are most likely using the wrong precision for this model (fp16?). The audio has been removed; the video was saved without it.")
gpu = NVENC[codec]
if encoder != "cpu" and gpu[0] in av.codecs_available:
try:
_write(path, frames, fps, audio, metadata, gpu)
return f"{codec.upper()} on NVENC (GPU)"
except Exception as exc: # noqa: BLE001 -- no encoder on this card, or a driver refusal
if os.path.exists(path):
os.remove(path)
if encoder == "gpu":
raise RuntimeError(f"the GPU encoder (NVENC) failed: {exc}") from exc
log.once("save_nvenc", log.ALERT, "FunPack Save Video", f"GPU encoder unavailable ({exc}); using H.264 on the CPU")
elif encoder == "gpu":
raise RuntimeError(f"this PyAV build has no NVENC encoder ({gpu[0]}); pick auto or cpu")
why = None
if encoder != "cpu":
if gpu[0] not in av.codecs_available:
why = f"this PyAV build has no NVENC encoder ({gpu[0]} missing)"
else:
try:
_write(path, frames, fps, audio, metadata, gpu)
return f"{codec.upper()} on NVENC (GPU)"
except Exception as exc: # noqa: BLE001 -- no encoder on this card, or a driver refusal
if os.path.exists(path):
os.remove(path)
why = f"the GPU encoder (NVENC) could not start: {exc}{_HINT}"
_write(path, frames, fps, audio, metadata, X264)
if encoder == "auto":
log.once("save_no_nvenc", log.ALERT, "FunPack Save Video", f"no NVENC in this PyAV build ({gpu[0]} missing): saved on the CPU")
if why: # the video is saved either way; never lose a generation to the encoder
log.once("save_nvenc", log.ALERT, "FunPack Save Video", f"{why}. Saved as H.264 on the CPU instead.")
return "H.264 on libx264 (CPU, all cores)"
@@ -95,7 +98,7 @@ class FunPackSaveVideo(io.ComfyNode):
io.Float.Input("fps", default=24.0, min=1.0, max=240.0, step=0.01),
io.String.Input("filename_prefix", default="FunPack"),
io.Combo.Input("encoder", options=list(ENCODERS), default="auto", optional=True,
tooltip="auto: GPU (NVENC) if available, else H.264 on the CPU. gpu: fail rather than use the CPU. cpu: H.264 (libx264) on every core."),
tooltip="auto and gpu: the GPU encoder (NVENC) when it works, otherwise H.264 on the CPU so the video is still saved. cpu: H.264 (libx264) on every core."),
io.Combo.Input("codec", options=list(CODECS), default="h264", optional=True,
tooltip="For the GPU encoder. H.264: plays in every browser. H.265: smaller files, but Firefox may not play it."),
],
+17 -3
View File
@@ -1,3 +1,5 @@
import os
import av
import numpy as np
import pytest
@@ -31,10 +33,22 @@ def test_auto_falls_back_to_the_cpu_when_there_is_no_gpu_encoder(tmp_path, monke
assert "libx264" in nodes.encode(path, _clip(10), 24.0, encoder="auto")
def test_gpu_demanded_without_one_says_so(tmp_path, monkeypatch):
def test_gpu_choice_still_saves_on_the_cpu_when_the_encoder_is_missing(tmp_path, monkeypatch):
monkeypatch.setattr(nodes, "NVENC", {"h264": ("no_such_encoder_x", {})})
with pytest.raises(RuntimeError, match="NVENC"):
nodes.encode(str(tmp_path / "c.mp4"), _clip(5), 24.0, encoder="gpu")
path = str(tmp_path / "c.mp4")
assert "libx264" in nodes.encode(path, _clip(5), 24.0, encoder="gpu")
assert os.path.getsize(path) > 0
def test_a_refused_gpu_open_leaves_no_half_file_and_the_cpu_saves_it(tmp_path, monkeypatch):
# a codec that exists but refuses to open with these options, as NVENC does on a driver mismatch
monkeypatch.setattr(nodes, "NVENC", {"h264": ("libx264", {"preset": "no-such-preset-x", "crf": "abc"})})
alerts = []
monkeypatch.setattr(nodes.log, "once", lambda key, level, who, msg: alerts.append(msg))
path = str(tmp_path / "e.mp4")
assert "libx264" in nodes.encode(path, _clip(5), 24.0, encoder="gpu")
assert os.path.getsize(path) > 0
assert alerts and "Saved as H.264 on the CPU" in alerts[0]
def test_h265_is_tagged_hvc1_so_a_mac_browser_plays_it(tmp_path, monkeypatch):