Skip to content

Commit 8974992

Browse files
committed
fix #614 improve output option handling
1 parent d14374e commit 8974992

File tree

3 files changed

+107
-2
lines changed

3 files changed

+107
-2
lines changed

src/ffmpeg/compile/compile_cli.py

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,23 @@
4747

4848

4949
def get_options_dict() -> dict[str, FFMpegOption]:
50+
"""
51+
Load and index FFmpeg options from the cache.
52+
53+
Returns:
54+
Dictionary mapping option names to their FFMpegOption definitions
55+
"""
5056
options = load(list[FFMpegOption], "options")
5157
return {option.name: option for option in options}
5258

5359

5460
def get_filter_dict() -> dict[str, FFMpegFilter]:
61+
"""
62+
Load and index FFmpeg filters from the cache.
63+
64+
Returns:
65+
Dictionary mapping filter names to their FFMpegFilter definitions
66+
"""
5567
filters = load(list[FFMpegFilter], "filters")
5668
return {filter.name: filter for filter in filters}
5769

@@ -99,6 +111,24 @@ def parse_options(tokens: list[str]) -> dict[str, list[str | None | bool]]:
99111
def parse_stream_selector(
100112
selector: str, mapping: Mapping[str, FilterableStream]
101113
) -> FilterableStream:
114+
"""
115+
Parse a stream selector string and return the corresponding stream.
116+
117+
This function handles FFmpeg's stream selector syntax:
118+
- Simple selectors: "[0]" (first input)
119+
- Type selectors: "[0:v]" (first input, video stream)
120+
- Index selectors: "[0:v:0]" (first input, first video stream)
121+
122+
Args:
123+
selector: Stream selector string to parse
124+
mapping: Dictionary of available streams
125+
126+
Returns:
127+
The selected FilterableStream
128+
129+
Raises:
130+
AssertionError: If the stream label is not found in the mapping
131+
"""
102132
selector = selector.strip("[]")
103133

104134
if ":" in selector:
@@ -126,19 +156,48 @@ def parse_stream_selector(
126156
return stream
127157

128158

159+
def _is_filename(token: str) -> bool:
160+
"""
161+
Check if a token is a filename.
162+
163+
Args:
164+
token: The token to check
165+
166+
Returns:
167+
True if the token is a filename, False otherwise
168+
"""
169+
# not start with - and has ext
170+
return not token.startswith("-") and len(token.split(".")) > 1
171+
172+
129173
def parse_output(
130174
source: list[str],
131175
in_streams: Mapping[str, FilterableStream],
132176
ffmpeg_options: dict[str, FFMpegOption],
133177
) -> list[OutputStream]:
178+
"""
179+
Parse output file specifications and their options.
180+
181+
This function processes the output portion of an FFmpeg command line,
182+
handling output file paths, stream mapping, and output-specific options.
183+
184+
Args:
185+
source: List of command-line tokens for output specifications
186+
in_streams: Dictionary of available input streams
187+
ffmpeg_options: Dictionary of valid FFmpeg options
188+
189+
Returns:
190+
List of OutputStream objects representing the output specifications
191+
"""
134192
tokens = source.copy()
135193

136194
export: list[OutputStream] = []
137-
138195
buffer: list[str] = []
196+
139197
while tokens:
140198
token = tokens.pop(0)
141-
if token.startswith("-") or len(buffer) % 2 == 1:
199+
200+
if not _is_filename(token):
142201
buffer.append(token)
143202
continue
144203

@@ -183,6 +242,19 @@ def parse_output(
183242
def parse_input(
184243
tokens: list[str], ffmpeg_options: dict[str, FFMpegOption]
185244
) -> dict[str, FilterableStream]:
245+
"""
246+
Parse input file specifications and their options.
247+
248+
This function processes the input portion of an FFmpeg command line,
249+
handling input file paths and input-specific options.
250+
251+
Args:
252+
tokens: List of command-line tokens for input specifications
253+
ffmpeg_options: Dictionary of valid FFmpeg options
254+
255+
Returns:
256+
Dictionary mapping input indices to their FilterableStream objects
257+
"""
186258
output: list[AVStream] = []
187259

188260
while "-i" in tokens:
@@ -347,6 +419,29 @@ def parse_global(
347419

348420

349421
def parse(cli: str) -> Stream:
422+
"""
423+
Parse a complete FFmpeg command line into a Stream object.
424+
425+
This function takes a full FFmpeg command line string and converts it into
426+
a Stream object representing the filter graph. It handles all components:
427+
- Global options
428+
- Input files and their options
429+
- Filter complex
430+
- Output files and their options
431+
432+
Args:
433+
cli: Complete FFmpeg command line string
434+
435+
Returns:
436+
Stream object representing the parsed command line
437+
438+
Example:
439+
```python
440+
stream = parse(
441+
"ffmpeg -i input.mp4 -filter_complex '[0:v]scale=1280:720[v]' -map '[v]' output.mp4"
442+
)
443+
```
444+
"""
350445
# ffmpeg [global_options] {[input_file_options] -i input_url} ... {[output_file_options] output_url} ...
351446
ffmpeg_options = get_options_dict()
352447
ffmpeg_filters = get_filter_dict()

src/ffmpeg/compile/tests/__snapshots__/test_compile_cli.ambr

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@
1111
# name: test_parse_ffmpeg_commands[global_binary_option][parse-ffmpeg-commands]
1212
GlobalStream(node=GlobalNode(kwargs=FrozenDict({'y': True, 'stdin': False}), inputs=(OutputStream(node=OutputNode(kwargs=FrozenDict({}), inputs=(AVStream(node=InputNode(kwargs=FrozenDict({}), inputs=(), filename='input_video.mkv'), index=None),), filename='output_video.mp4'), index=None),)), index=None)
1313
# ---
14+
# name: test_parse_ffmpeg_commands[output_option_with_boolean_option][build-ffmpeg-commands]
15+
'ffmpeg -y -nostdin -i input_video.mkv -shortest -b:v 1000k -b:a 128k output_video.mp4'
16+
# ---
17+
# name: test_parse_ffmpeg_commands[output_option_with_boolean_option][parse-ffmpeg-commands]
18+
GlobalStream(node=GlobalNode(kwargs=FrozenDict({'y': True, 'stdin': False}), inputs=(OutputStream(node=OutputNode(kwargs=FrozenDict({'shortest': True, 'b:v': '1000k', 'b:a': '128k'}), inputs=(AVStream(node=InputNode(kwargs=FrozenDict({}), inputs=(), filename='input_video.mkv'), index=None),), filename='output_video.mp4'), index=None),)), index=None)
19+
# ---
1420
# name: test_parse_ffmpeg_commands[output_option_with_stream_selector][build-ffmpeg-commands]
1521
'ffmpeg -y -nostdin -i input_video.mkv -b:v 1000k output_video.mp4'
1622
# ---

src/ffmpeg/compile/tests/test_compile_cli.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ def test_parse_compile(snapshot: SnapshotAssertion, graph: Stream) -> None:
4040
"ffmpeg -y -nostdin -i input_video.mkv -b:v 1000k output_video.mp4",
4141
id="output_option_with_stream_selector",
4242
),
43+
pytest.param(
44+
"ffmpeg -y -nostdin -i input_video.mkv -shortest -b:v 1000k -b:a 128k output_video.mp4",
45+
id="output_option_with_boolean_option",
46+
),
4347
],
4448
)
4549
def test_parse_ffmpeg_commands(snapshot: SnapshotAssertion, command: str) -> None:

0 commit comments

Comments
 (0)