feat: encode video previews with the ultrafast x264 preset

This commit is contained in:
Terry Jia
2026-08-20 23:38:10 -04:00
parent b71ea70f33
commit 26acfbf378
4 changed files with 52 additions and 9 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)