Skip to content

Commit cd18582

Browse files
Support saving Comfy VIDEO type to buffer (Comfy-Org#7939)
* get output format when saving to buffer * add unit tests for writing to file or stream with correct fmt * handle `to_format=None` * fix formatting
1 parent 80a44b9 commit cd18582

File tree

2 files changed

+135
-2
lines changed

2 files changed

+135
-2
lines changed

comfy_api/input_impl/video_types.py

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,46 @@
1212
from comfy_api.input import VideoInput
1313
from comfy_api.util import VideoContainer, VideoCodec, VideoComponents
1414

15+
16+
def container_to_output_format(container_format: str | None) -> str | None:
17+
"""
18+
A container's `format` may be a comma-separated list of formats.
19+
E.g., iso container's `format` may be `mov,mp4,m4a,3gp,3g2,mj2`.
20+
However, writing to a file/stream with `av.open` requires a single format,
21+
or `None` to auto-detect.
22+
"""
23+
if not container_format:
24+
return None # Auto-detect
25+
26+
if "," not in container_format:
27+
return container_format
28+
29+
formats = container_format.split(",")
30+
return formats[0]
31+
32+
33+
def get_open_write_kwargs(
34+
dest: str | io.BytesIO, container_format: str, to_format: str | None
35+
) -> dict:
36+
"""Get kwargs for writing a `VideoFromFile` to a file/stream with `av.open`"""
37+
open_kwargs = {
38+
"mode": "w",
39+
# If isobmff, preserve custom metadata tags (workflow, prompt, extra_pnginfo)
40+
"options": {"movflags": "use_metadata_tags"},
41+
}
42+
43+
is_write_to_buffer = isinstance(dest, io.BytesIO)
44+
if is_write_to_buffer:
45+
# Set output format explicitly, since it cannot be inferred from file extension
46+
if to_format == VideoContainer.AUTO:
47+
to_format = container_format.lower()
48+
elif isinstance(to_format, str):
49+
to_format = to_format.lower()
50+
open_kwargs["format"] = container_to_output_format(to_format)
51+
52+
return open_kwargs
53+
54+
1555
class VideoFromFile(VideoInput):
1656
"""
1757
Class representing video input from a file.
@@ -89,7 +129,7 @@ def get_components(self) -> VideoComponents:
89129

90130
def save_to(
91131
self,
92-
path: str,
132+
path: str | io.BytesIO,
93133
format: VideoContainer = VideoContainer.AUTO,
94134
codec: VideoCodec = VideoCodec.AUTO,
95135
metadata: Optional[dict] = None
@@ -116,7 +156,9 @@ def save_to(
116156
)
117157

118158
streams = container.streams
119-
with av.open(path, mode='w', options={"movflags": "use_metadata_tags"}) as output_container:
159+
160+
open_kwargs = get_open_write_kwargs(path, container_format, format)
161+
with av.open(path, **open_kwargs) as output_container:
120162
# Copy over the original metadata
121163
for key, value in container.metadata.items():
122164
if metadata is None or key not in metadata:
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import io
2+
from comfy_api.input_impl.video_types import (
3+
container_to_output_format,
4+
get_open_write_kwargs,
5+
)
6+
from comfy_api.util import VideoContainer
7+
8+
9+
def test_container_to_output_format_empty_string():
10+
"""Test that an empty string input returns None. `None` arg allows default auto-detection."""
11+
assert container_to_output_format("") is None
12+
13+
14+
def test_container_to_output_format_none():
15+
"""Test that None input returns None."""
16+
assert container_to_output_format(None) is None
17+
18+
19+
def test_container_to_output_format_comma_separated():
20+
"""Test that a comma-separated list returns a valid singular format from the list."""
21+
comma_separated_format = "mp4,mov,m4a"
22+
output_format = container_to_output_format(comma_separated_format)
23+
assert output_format in comma_separated_format
24+
25+
26+
def test_container_to_output_format_single():
27+
"""Test that a single format string (not comma-separated list) is returned as is."""
28+
assert container_to_output_format("mp4") == "mp4"
29+
30+
31+
def test_get_open_write_kwargs_filepath_no_format():
32+
"""Test that 'format' kwarg is NOT set when dest is a file path."""
33+
kwargs_auto = get_open_write_kwargs("output.mp4", "mp4", VideoContainer.AUTO)
34+
assert "format" not in kwargs_auto, "Format should not be set for file paths (AUTO)"
35+
36+
kwargs_specific = get_open_write_kwargs("output.avi", "mp4", "avi")
37+
fail_msg = "Format should not be set for file paths (Specific)"
38+
assert "format" not in kwargs_specific, fail_msg
39+
40+
41+
def test_get_open_write_kwargs_base_options_mode():
42+
"""Test basic kwargs for file path: mode and movflags."""
43+
kwargs = get_open_write_kwargs("output.mp4", "mp4", VideoContainer.AUTO)
44+
assert kwargs["mode"] == "w", "mode should be set to write"
45+
46+
fail_msg = "movflags should be set to preserve custom metadata tags"
47+
assert "movflags" in kwargs["options"], fail_msg
48+
assert kwargs["options"]["movflags"] == "use_metadata_tags", fail_msg
49+
50+
51+
def test_get_open_write_kwargs_bytesio_auto_format():
52+
"""Test kwargs for BytesIO dest with AUTO format."""
53+
dest = io.BytesIO()
54+
container_fmt = "mov,mp4,m4a"
55+
kwargs = get_open_write_kwargs(dest, container_fmt, VideoContainer.AUTO)
56+
57+
assert kwargs["mode"] == "w"
58+
assert kwargs["options"]["movflags"] == "use_metadata_tags"
59+
60+
fail_msg = (
61+
"Format should be a valid format from the container's format list when AUTO"
62+
)
63+
assert kwargs["format"] in container_fmt, fail_msg
64+
65+
66+
def test_get_open_write_kwargs_bytesio_specific_format():
67+
"""Test kwargs for BytesIO dest with a specific single format."""
68+
dest = io.BytesIO()
69+
container_fmt = "avi"
70+
to_fmt = VideoContainer.MP4
71+
kwargs = get_open_write_kwargs(dest, container_fmt, to_fmt)
72+
73+
assert kwargs["mode"] == "w"
74+
assert kwargs["options"]["movflags"] == "use_metadata_tags"
75+
76+
fail_msg = "Format should be the specified format (lowercased) when output format is not AUTO"
77+
assert kwargs["format"] == "mp4", fail_msg
78+
79+
80+
def test_get_open_write_kwargs_bytesio_specific_format_list():
81+
"""Test kwargs for BytesIO dest with a specific comma-separated format."""
82+
dest = io.BytesIO()
83+
container_fmt = "avi"
84+
to_fmt = "mov,mp4,m4a" # A format string that is a list
85+
kwargs = get_open_write_kwargs(dest, container_fmt, to_fmt)
86+
87+
assert kwargs["mode"] == "w"
88+
assert kwargs["options"]["movflags"] == "use_metadata_tags"
89+
90+
fail_msg = "Format should be a valid format from the specified format list when output format is not AUTO"
91+
assert kwargs["format"] in to_fmt, fail_msg

0 commit comments

Comments
 (0)