Skip to content

Commit 26acfbf

Browse files
committed
feat: encode video previews with the ultrafast x264 preset
1 parent b71ea70 commit 26acfbf

4 files changed

Lines changed: 52 additions & 9 deletions

File tree

comfy_api/latest/_input/video_types.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,15 @@ def save_to(
3131
bit_depth: int | None = None,
3232
crf: float | None = None,
3333
color_space: str | None = None,
34+
preset: str | None = None,
3435
):
3536
"""
3637
Abstract method to save the video input to a file.
3738
3839
bit_depth selects the encoded bit depth; None keeps the video's native depth.
3940
crf selects the H.264 or AV1 constant rate factor; None uses the encoder default.
41+
preset selects the H.264 encoder speed/compression trade-off (e.g. "ultrafast");
42+
None uses the encoder default. Ignored for other codecs.
4043
color_space="sRGB" writes SDR BT.709/sRGB video. "HDR" writes 10-bit BT.2020/HLG video;
4144
"HDR PQ" selects BT.2020/PQ.
4245
Tensor-created videos default to sRGB when color_space is None. Loaded videos keep matching recognized native color

comfy_api/latest/_input_impl/video_types.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -181,12 +181,18 @@ def video_stream_color_space(stream) -> str | None:
181181
return VIDEO_TRANSFER_COLOR_SPACES.get(stream.color_trc)
182182

183183

184-
def video_encoder_options(codec: VideoCodec, crf: float | None) -> dict[str, str]:
185-
if crf is None:
186-
return {}
187-
if codec == VideoCodec.AV1 and crf == 0:
188-
return {"svtav1-params": "lossless=1"}
189-
return {"crf": str(crf)}
184+
def video_encoder_options(
185+
codec: VideoCodec, crf: float | None, preset: str | None = None
186+
) -> dict[str, str]:
187+
options = {}
188+
if preset is not None and codec == VideoCodec.H264:
189+
options["preset"] = preset
190+
if crf is not None:
191+
if codec == VideoCodec.AV1 and crf == 0:
192+
options["svtav1-params"] = "lossless=1"
193+
else:
194+
options["crf"] = str(crf)
195+
return options
190196

191197

192198
def webm_streams_compatible(streams) -> bool:
@@ -594,6 +600,7 @@ def save_to(
594600
bit_depth: int | None = None,
595601
crf: float | None = None,
596602
color_space: str | None = None,
603+
preset: str | None = None,
597604
):
598605
if color_space is not None and color_space not in VIDEO_COLOR_TRANSFERS:
599606
raise ValueError(f"Unsupported video color space: {color_space}")
@@ -632,7 +639,7 @@ def save_to(
632639
if not reuse_streams:
633640
if bit_depth is None:
634641
bit_depth = source_bit_depth
635-
return self._save_transcoded(container, path, format=format, codec=codec, metadata=metadata, bit_depth=bit_depth, crf=crf, color_space=color_space)
642+
return self._save_transcoded(container, path, format=format, codec=codec, metadata=metadata, bit_depth=bit_depth, crf=crf, color_space=color_space, preset=preset)
636643

637644
streams = container.streams
638645

@@ -667,6 +674,7 @@ def _save_transcoded(
667674
bit_depth: int,
668675
crf: float | None = None,
669676
color_space: str | None = None,
677+
preset: str | None = None,
670678
):
671679
"""Re-encode one frame at a time; peak memory does not scale with video length."""
672680
open_kwargs, output_format, output_codec = video_output_config(path, format, codec)
@@ -844,7 +852,7 @@ def drain_audio(final=False):
844852
out_video.width = out_width
845853
out_video.height = out_height
846854
out_video.pix_fmt = pix_fmt
847-
out_video.options = video_encoder_options(output_codec, crf)
855+
out_video.options = video_encoder_options(output_codec, crf, preset)
848856
if preserve_source_color:
849857
copy_color_properties(video_stream, out_video.codec_context)
850858
elif color_space is not None:
@@ -1074,6 +1082,7 @@ def save_to(
10741082
bit_depth: int | None = None,
10751083
crf: float | None = None,
10761084
color_space: str | None = None,
1085+
preset: str | None = None,
10771086
):
10781087
"""Save the video to a file path or BytesIO buffer."""
10791088
if color_space is None:
@@ -1100,7 +1109,7 @@ def save_to(
11001109
video_stream.width = self.__components.images.shape[2]
11011110
video_stream.height = self.__components.images.shape[1]
11021111
video_stream.pix_fmt = pix_fmt
1103-
video_stream.options = video_encoder_options(output_codec, crf)
1112+
video_stream.options = video_encoder_options(output_codec, crf, preset)
11041113
if color_space is not None:
11051114
set_video_color_properties(video_stream.codec_context, color_space)
11061115

comfy_extras/nodes_video.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,7 @@ def save_video_preview(video: Input.Video) -> ui.PreviewVideo:
361361
full_path,
362362
format=preview_format,
363363
codec="auto",
364+
preset="ultrafast",
364365
)
365366
result = ui.SavedResult(file, subfolder, io.FolderType.temp)
366367
_preview_results[video] = (full_path, result)

tests-unit/comfy_api_test/video_types_test.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,3 +1364,33 @@ def test_as_cropped_components_releases_uncropped_storage():
13641364
cropped_images.untyped_storage().data_ptr()
13651365
!= images.untyped_storage().data_ptr()
13661366
)
1367+
1368+
1369+
def test_video_encoder_options_applies_h264_preset():
1370+
from comfy_api.latest._input_impl.video_types import video_encoder_options
1371+
1372+
assert video_encoder_options(VideoCodec.H264, None, "ultrafast") == {
1373+
"preset": "ultrafast"
1374+
}
1375+
assert video_encoder_options(VideoCodec.H264, 23.0, "ultrafast") == {
1376+
"preset": "ultrafast",
1377+
"crf": "23.0",
1378+
}
1379+
assert video_encoder_options(VideoCodec.H264, 23.0, None) == {"crf": "23.0"}
1380+
assert video_encoder_options(VideoCodec.AV1, None, "ultrafast") == {}
1381+
assert video_encoder_options(VideoCodec.AV1, 0, "ultrafast") == {
1382+
"svtav1-params": "lossless=1"
1383+
}
1384+
1385+
1386+
def test_save_to_preset_transcodes_playable_output(tmp_path):
1387+
source = create_test_video(width=32, height=32)
1388+
try:
1389+
out = str(tmp_path / "preset.mp4")
1390+
VideoFromFile(source).as_cropped(0, 0, 16, 16).save_to(
1391+
out, preset="ultrafast"
1392+
)
1393+
saved = VideoFromFile(out).get_components()
1394+
assert tuple(saved.images.shape[1:3]) == (16, 16)
1395+
finally:
1396+
os.unlink(source)

0 commit comments

Comments
 (0)