Skip to content

Commit 76135e5

Browse files
Add HDR color space options to h264 codec in Save Video node. (#15764)
1 parent dcbcf8c commit 76135e5

2 files changed

Lines changed: 59 additions & 10 deletions

File tree

comfy_extras/nodes_video.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,16 @@ def execute(cls, images, codec, fps, filename_prefix, crf) -> io.NodeOutput:
7272

7373
return io.NodeOutput(images, ui=ui.PreviewVideo([ui.SavedResult(file, subfolder, io.FolderType.output)]))
7474

75+
def _save_video_color_space_input():
76+
return io.Combo.Input(
77+
"color_space",
78+
options=["auto", "sRGB", "HDR", "HDR PQ"],
79+
default="auto",
80+
display_name="color space",
81+
tooltip="Auto uses sRGB for videos created from images and preserves recognized colors on loaded videos. sRGB writes SDR BT.709/sRGB. HDR writes 10-bit BT.2020/HLG; HDR PQ writes BT.2020/PQ. Other input pixels must already use the selected color space.",
82+
)
83+
84+
7585
def _save_video_codec_input(supported_codecs: list[str], *, optional=False, hidden=False):
7686
codec_options = []
7787
if "auto" in supported_codecs:
@@ -88,11 +98,14 @@ def _save_video_codec_input(supported_codecs: list[str], *, optional=False, hidd
8898
io.DynamicCombo.Option("auto", []),
8999
io.DynamicCombo.Option(
90100
"re-encode",
91-
[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.")],
101+
[
102+
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."),
103+
_save_video_color_space_input(),
104+
],
92105
),
93106
],
94107
optional=True,
95-
tooltip="Automatic preserves compatible H.264 streams. Re-encode applies a custom CRF.",
108+
tooltip="Automatic preserves compatible H.264 streams. Re-encode applies custom encoding options.",
96109
),
97110
],
98111
)
@@ -111,13 +124,7 @@ def _save_video_codec_input(supported_codecs: list[str], *, optional=False, hidd
111124
"re-encode",
112125
[
113126
io.Float.Input("crf", default=30.0, min=0.0, max=63.0, step=1.0, tooltip="Lower values produce higher quality and larger files."),
114-
io.Combo.Input(
115-
"color_space",
116-
options=["auto", "sRGB", "HDR", "HDR PQ"],
117-
default="auto",
118-
display_name="color space",
119-
tooltip="Auto uses sRGB for videos created from images and preserves recognized colors on loaded videos. sRGB writes SDR BT.709/sRGB. HDR writes 10-bit BT.2020/HLG; HDR PQ writes BT.2020/PQ. Other input pixels must already use the selected color space.",
120-
),
127+
_save_video_color_space_input(),
121128
],
122129
),
123130
],
@@ -131,7 +138,7 @@ def _save_video_codec_input(supported_codecs: list[str], *, optional=False, hidd
131138
"codec",
132139
options=codec_options,
133140
optional=optional,
134-
tooltip="The output video codec. Auto preserves a compatible source stream. H.264 re-encoding supports SDR; AV1 re-encoding supports SDR, HDR (HLG), and HDR PQ.",
141+
tooltip="The output video codec. Auto preserves a compatible source stream. H.264 and AV1 re-encoding support SDR, HDR (HLG), and HDR PQ.",
135142
extra_dict={"hidden": True} if hidden else None,
136143
)
137144

tests-unit/comfy_api_test/video_types_test.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,48 @@ def test_save_to_av1_mkv_color_space(tmp_path, color_space, transfer, pix_fmt, p
477477
assert video_packet_bytes(remuxed) == source_packets
478478

479479

480+
@pytest.mark.parametrize(
481+
"format,suffix",
482+
[
483+
(VideoContainer.MP4, "mp4"),
484+
(VideoContainer.MKV, "mkv"),
485+
],
486+
)
487+
@pytest.mark.parametrize(
488+
"color_space,transfer,pix_fmt,primaries,colorspace",
489+
[
490+
("sRGB", ColorTrc.IEC61966_2_1, "yuv420p", ColorPrimaries.BT709, 1),
491+
("HDR", ColorTrc.ARIB_STD_B67, "yuv420p10le", ColorPrimaries.BT2020, 9),
492+
("HDR PQ", ColorTrc.SMPTE2084, "yuv420p10le", ColorPrimaries.BT2020, 9),
493+
],
494+
)
495+
def test_save_to_h264_color_space(tmp_path, format, suffix, color_space, transfer, pix_fmt, primaries, colorspace):
496+
components = VideoComponents(
497+
images=torch.rand(2, 64, 64, 3),
498+
frame_rate=Fraction(30),
499+
)
500+
path = str(tmp_path / f"h264.{suffix}")
501+
502+
VideoFromComponents(components).save_to(
503+
path,
504+
format=format,
505+
codec=VideoCodec.H264,
506+
crf=23,
507+
color_space=color_space,
508+
)
509+
510+
with av.open(path) as container:
511+
stream = container.streams.video[0]
512+
assert stream.codec.canonical_name == "h264"
513+
assert stream.format.name == pix_fmt
514+
assert stream.color_primaries == primaries
515+
assert stream.color_trc == transfer
516+
assert stream.colorspace == colorspace
517+
assert stream.color_range == ColorRange.MPEG
518+
assert sum(1 for _ in container.decode(video=0)) == 2
519+
assert VideoFromFile(path).get_color_space() == color_space
520+
521+
480522
@pytest.mark.parametrize(
481523
"transfer,color_range,color_space",
482524
[

0 commit comments

Comments
 (0)