mirror of
https://github.com/Comfy-Org/ComfyUI.git
synced 2026-08-05 18:05:08 +08:00
Add crf option to save video node. (#15191)
This commit is contained in:
@@ -29,11 +29,13 @@ class VideoInput(ABC):
|
||||
codec: VideoCodec = VideoCodec.AUTO,
|
||||
metadata: Optional[dict] = None,
|
||||
bit_depth: int | None = None,
|
||||
crf: float | None = None,
|
||||
):
|
||||
"""
|
||||
Abstract method to save the video input to a file.
|
||||
|
||||
bit_depth selects the encoded bit depth; None keeps the video's native depth.
|
||||
crf selects the H.264 constant rate factor; None uses the encoder default.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@@ -460,6 +460,7 @@ class VideoFromFile(VideoInput):
|
||||
codec: VideoCodec = VideoCodec.AUTO,
|
||||
metadata: Optional[dict] = None,
|
||||
bit_depth: int | None = None,
|
||||
crf: float | None = None,
|
||||
):
|
||||
if isinstance(self.__file, io.BytesIO):
|
||||
self.__file.seek(0) # Reset the BytesIO object to the beginning
|
||||
@@ -475,13 +476,15 @@ class VideoFromFile(VideoInput):
|
||||
reuse_streams = False
|
||||
if bit_depth is not None and video_encoding is not None and bit_depth != source_bit_depth:
|
||||
reuse_streams = False
|
||||
if crf is not None:
|
||||
reuse_streams = False
|
||||
if self.__start_time or self.__duration:
|
||||
reuse_streams = False
|
||||
|
||||
if not reuse_streams:
|
||||
if bit_depth is None:
|
||||
bit_depth = source_bit_depth
|
||||
return self._save_transcoded(container, path, format=format, codec=codec, metadata=metadata, bit_depth=bit_depth)
|
||||
return self._save_transcoded(container, path, format=format, codec=codec, metadata=metadata, bit_depth=bit_depth, crf=crf)
|
||||
|
||||
streams = container.streams
|
||||
|
||||
@@ -514,6 +517,7 @@ class VideoFromFile(VideoInput):
|
||||
codec: VideoCodec,
|
||||
metadata: dict | None,
|
||||
bit_depth: int,
|
||||
crf: float | None = None,
|
||||
):
|
||||
"""Re-encode to H.264/AAC one frame at a time; peak memory does not scale with video length."""
|
||||
open_kwargs = mp4_output_open_kwargs(path, format, codec)
|
||||
@@ -659,6 +663,8 @@ class VideoFromFile(VideoInput):
|
||||
out_video.width = out_width
|
||||
out_video.height = out_height
|
||||
out_video.pix_fmt = pix_fmt
|
||||
if crf is not None:
|
||||
out_video.options = {"crf": str(crf)}
|
||||
# source pts pass through (rebased to 0), so variable frame rate survives
|
||||
out_video.codec_context.time_base = video_stream.time_base
|
||||
if audio_stream is not None:
|
||||
@@ -827,6 +833,7 @@ class VideoFromComponents(VideoInput):
|
||||
codec: VideoCodec = VideoCodec.AUTO,
|
||||
metadata: Optional[dict] = None,
|
||||
bit_depth: int | None = None,
|
||||
crf: float | None = None,
|
||||
):
|
||||
"""Save the video to a file path or BytesIO buffer."""
|
||||
open_kwargs = mp4_output_open_kwargs(path, format, codec)
|
||||
@@ -847,6 +854,8 @@ class VideoFromComponents(VideoInput):
|
||||
video_stream.width = self.__components.images.shape[2]
|
||||
video_stream.height = self.__components.images.shape[1]
|
||||
video_stream.pix_fmt = pix_fmt
|
||||
if crf is not None:
|
||||
video_stream.options = {"crf": str(crf)}
|
||||
|
||||
# Create an audio stream
|
||||
audio_sample_rate = 1
|
||||
|
||||
@@ -86,7 +86,31 @@ class SaveVideo(io.ComfyNode):
|
||||
io.Video.Input("video", tooltip="The video to save."),
|
||||
io.String.Input("filename_prefix", default="video/ComfyUI", tooltip="The prefix for the file to save. This may include formatting information such as %date:yyyy-MM-dd% or %Empty Latent Image.width% to include values from nodes."),
|
||||
io.Combo.Input("format", options=Types.VideoContainer.as_input(), default="auto", tooltip="The format to save the video as."),
|
||||
io.Combo.Input("codec", options=Types.VideoCodec.as_input(), default="auto", tooltip="The codec to use for the video."),
|
||||
io.DynamicCombo.Input(
|
||||
"codec",
|
||||
options=[
|
||||
io.DynamicCombo.Option("auto", []),
|
||||
io.DynamicCombo.Option(
|
||||
"h264",
|
||||
[
|
||||
io.DynamicCombo.Input(
|
||||
"encoding",
|
||||
display_name="encoding mode",
|
||||
options=[
|
||||
io.DynamicCombo.Option("auto", []),
|
||||
io.DynamicCombo.Option(
|
||||
"re-encode",
|
||||
[io.Float.Input("crf", default=23.0, min=0.0, max=51.0, step=1.0, tooltip="Lower values produce higher quality and larger files.")],
|
||||
),
|
||||
],
|
||||
optional=True,
|
||||
tooltip="Automatic preserves compatible H.264 streams. Re-encode applies a custom CRF.",
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
tooltip="The codec to use for the video.",
|
||||
),
|
||||
],
|
||||
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
|
||||
is_output_node=True,
|
||||
@@ -94,7 +118,9 @@ class SaveVideo(io.ComfyNode):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, video: Input.Video, filename_prefix, format: str, codec) -> io.NodeOutput:
|
||||
def execute(cls, video: Input.Video, filename_prefix, format: str, codec: io.DynamicCombo.Type) -> io.NodeOutput:
|
||||
codec_name = codec["codec"]
|
||||
encoding = codec.get("encoding") or {}
|
||||
width, height = video.get_dimensions()
|
||||
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(
|
||||
filename_prefix,
|
||||
@@ -115,8 +141,9 @@ class SaveVideo(io.ComfyNode):
|
||||
video.save_to(
|
||||
os.path.join(full_output_folder, file),
|
||||
format=Types.VideoContainer(format),
|
||||
codec=codec,
|
||||
metadata=saved_metadata
|
||||
codec=codec_name,
|
||||
metadata=saved_metadata,
|
||||
crf=encoding.get("crf"),
|
||||
)
|
||||
|
||||
return io.NodeOutput(video, ui=ui.PreviewVideo([ui.SavedResult(file, subfolder, io.FolderType.output)]))
|
||||
|
||||
@@ -240,6 +240,24 @@ def test_duration_consistency(video_components):
|
||||
assert duration == pytest.approx(manual_duration)
|
||||
|
||||
|
||||
def test_save_to_h264_crf_controls_quality(tmp_path):
|
||||
generator = torch.Generator().manual_seed(7)
|
||||
components = VideoComponents(
|
||||
images=torch.rand(12, 64, 64, 3, generator=generator),
|
||||
frame_rate=Fraction(30),
|
||||
)
|
||||
high_quality = str(tmp_path / "high_quality.mp4")
|
||||
low_quality = str(tmp_path / "low_quality.mp4")
|
||||
transcoded = str(tmp_path / "transcoded.mp4")
|
||||
|
||||
VideoFromComponents(components).save_to(high_quality, codec=VideoCodec.H264, crf=0)
|
||||
VideoFromComponents(components).save_to(low_quality, codec=VideoCodec.H264, crf=51)
|
||||
assert os.path.getsize(high_quality) > os.path.getsize(low_quality)
|
||||
|
||||
VideoFromFile(high_quality).save_to(transcoded, codec=VideoCodec.H264, crf=51)
|
||||
assert os.path.getsize(transcoded) < os.path.getsize(high_quality)
|
||||
|
||||
|
||||
def create_transcode_source(
|
||||
width=64, height=64, frames=30, fps=30, audio_streams=1, undecodable_audio=0, rotation=False,
|
||||
container_format="mov", audio_codec="pcm_s16le",
|
||||
|
||||
Reference in New Issue
Block a user