diff --git a/comfy_api/latest/_input/video_types.py b/comfy_api/latest/_input/video_types.py index d2c92f124..fdda4a11e 100644 --- a/comfy_api/latest/_input/video_types.py +++ b/comfy_api/latest/_input/video_types.py @@ -31,12 +31,15 @@ class VideoInput(ABC): bit_depth: int | None = None, crf: float | None = None, color_space: str | None = None, + preset: str | 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 or AV1 constant rate factor; None uses the encoder default. + preset selects the H.264 encoder speed/compression trade-off (e.g. "ultrafast"); + None uses the encoder default. Ignored for other codecs. color_space="sRGB" writes SDR BT.709/sRGB video. "HDR" writes 10-bit BT.2020/HLG video; "HDR PQ" selects BT.2020/PQ. Tensor-created videos default to sRGB when color_space is None. Loaded videos keep matching recognized native color diff --git a/comfy_api/latest/_input_impl/video_types.py b/comfy_api/latest/_input_impl/video_types.py index 0b64ebefc..48a6c34a4 100644 --- a/comfy_api/latest/_input_impl/video_types.py +++ b/comfy_api/latest/_input_impl/video_types.py @@ -181,12 +181,18 @@ def video_stream_color_space(stream) -> str | None: return VIDEO_TRANSFER_COLOR_SPACES.get(stream.color_trc) -def video_encoder_options(codec: VideoCodec, crf: float | None) -> dict[str, str]: - if crf is None: - return {} - if codec == VideoCodec.AV1 and crf == 0: - return {"svtav1-params": "lossless=1"} - return {"crf": str(crf)} +def video_encoder_options( + codec: VideoCodec, crf: float | None, preset: str | None = None +) -> dict[str, str]: + options = {} + if preset is not None and codec == VideoCodec.H264: + options["preset"] = preset + if crf is not None: + if codec == VideoCodec.AV1 and crf == 0: + options["svtav1-params"] = "lossless=1" + else: + options["crf"] = str(crf) + return options def webm_streams_compatible(streams) -> bool: @@ -594,6 +600,7 @@ class VideoFromFile(VideoInput): bit_depth: int | None = None, crf: float | None = None, color_space: str | None = None, + preset: str | None = None, ): if color_space is not None and color_space not in VIDEO_COLOR_TRANSFERS: raise ValueError(f"Unsupported video color space: {color_space}") @@ -632,7 +639,7 @@ class VideoFromFile(VideoInput): 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, crf=crf, color_space=color_space) + return self._save_transcoded(container, path, format=format, codec=codec, metadata=metadata, bit_depth=bit_depth, crf=crf, color_space=color_space, preset=preset) streams = container.streams @@ -667,6 +674,7 @@ class VideoFromFile(VideoInput): bit_depth: int, crf: float | None = None, color_space: str | None = None, + preset: str | None = None, ): """Re-encode one frame at a time; peak memory does not scale with video length.""" open_kwargs, output_format, output_codec = video_output_config(path, format, codec) @@ -844,7 +852,7 @@ class VideoFromFile(VideoInput): out_video.width = out_width out_video.height = out_height out_video.pix_fmt = pix_fmt - out_video.options = video_encoder_options(output_codec, crf) + out_video.options = video_encoder_options(output_codec, crf, preset) if preserve_source_color: copy_color_properties(video_stream, out_video.codec_context) elif color_space is not None: @@ -1074,6 +1082,7 @@ class VideoFromComponents(VideoInput): bit_depth: int | None = None, crf: float | None = None, color_space: str | None = None, + preset: str | None = None, ): """Save the video to a file path or BytesIO buffer.""" if color_space is None: @@ -1100,7 +1109,7 @@ 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 - video_stream.options = video_encoder_options(output_codec, crf) + video_stream.options = video_encoder_options(output_codec, crf, preset) if color_space is not None: set_video_color_properties(video_stream.codec_context, color_space) diff --git a/comfy_extras/nodes_video.py b/comfy_extras/nodes_video.py index 978b8968f..642f9a818 100644 --- a/comfy_extras/nodes_video.py +++ b/comfy_extras/nodes_video.py @@ -361,6 +361,7 @@ def save_video_preview(video: Input.Video) -> ui.PreviewVideo: full_path, format=preview_format, codec="auto", + preset="ultrafast", ) result = ui.SavedResult(file, subfolder, io.FolderType.temp) _preview_results[video] = (full_path, result) diff --git a/tests-unit/comfy_api_test/video_types_test.py b/tests-unit/comfy_api_test/video_types_test.py index 73e58aa94..f3a9e6260 100644 --- a/tests-unit/comfy_api_test/video_types_test.py +++ b/tests-unit/comfy_api_test/video_types_test.py @@ -1364,3 +1364,33 @@ def test_as_cropped_components_releases_uncropped_storage(): cropped_images.untyped_storage().data_ptr() != images.untyped_storage().data_ptr() ) + + +def test_video_encoder_options_applies_h264_preset(): + from comfy_api.latest._input_impl.video_types import video_encoder_options + + assert video_encoder_options(VideoCodec.H264, None, "ultrafast") == { + "preset": "ultrafast" + } + assert video_encoder_options(VideoCodec.H264, 23.0, "ultrafast") == { + "preset": "ultrafast", + "crf": "23.0", + } + assert video_encoder_options(VideoCodec.H264, 23.0, None) == {"crf": "23.0"} + assert video_encoder_options(VideoCodec.AV1, None, "ultrafast") == {} + assert video_encoder_options(VideoCodec.AV1, 0, "ultrafast") == { + "svtav1-params": "lossless=1" + } + + +def test_save_to_preset_transcodes_playable_output(tmp_path): + source = create_test_video(width=32, height=32) + try: + out = str(tmp_path / "preset.mp4") + VideoFromFile(source).as_cropped(0, 0, 16, 16).save_to( + out, preset="ultrafast" + ) + saved = VideoFromFile(out).get_components() + assert tuple(saved.images.shape[1:3]) == (16, 16) + finally: + os.unlink(source)