typed-ffmpeg-compatible 3.0.0a0__py3-none-any.whl → 3.0.2a0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- typed_ffmpeg/common/cache/list/options.json +1694 -0
- typed_ffmpeg/compile/compile_cli.py +397 -13
- typed_ffmpeg/compile/compile_python.py +8 -2
- typed_ffmpeg/dag/nodes.py +1 -3
- typed_ffmpeg/streams/av.py +36 -3
- {typed_ffmpeg_compatible-3.0.0a0.dist-info → typed_ffmpeg_compatible-3.0.2a0.dist-info}/METADATA +1 -1
- {typed_ffmpeg_compatible-3.0.0a0.dist-info → typed_ffmpeg_compatible-3.0.2a0.dist-info}/RECORD +10 -9
- {typed_ffmpeg_compatible-3.0.0a0.dist-info → typed_ffmpeg_compatible-3.0.2a0.dist-info}/LICENSE +0 -0
- {typed_ffmpeg_compatible-3.0.0a0.dist-info → typed_ffmpeg_compatible-3.0.2a0.dist-info}/WHEEL +0 -0
- {typed_ffmpeg_compatible-3.0.0a0.dist-info → typed_ffmpeg_compatible-3.0.2a0.dist-info}/entry_points.txt +0 -0
@@ -16,10 +16,30 @@ filter graph syntax, and escaping of special characters in FFmpeg commands.
|
|
16
16
|
|
17
17
|
from __future__ import annotations
|
18
18
|
|
19
|
-
|
19
|
+
import re
|
20
|
+
import shlex
|
21
|
+
from collections import defaultdict
|
22
|
+
from collections.abc import Mapping
|
23
|
+
|
24
|
+
from ffmpeg.dag.factory import filter_node_factory
|
25
|
+
from ffmpeg.streams.audio import AudioStream
|
26
|
+
from ffmpeg.streams.video import VideoStream
|
27
|
+
|
28
|
+
from ..base import input, merge_outputs, output
|
29
|
+
from ..common.cache import load
|
30
|
+
from ..common.schema import FFMpegFilter, FFMpegFilterDef, FFMpegOption, StreamType
|
31
|
+
from ..dag.nodes import (
|
32
|
+
FilterableStream,
|
33
|
+
FilterNode,
|
34
|
+
GlobalNode,
|
35
|
+
InputNode,
|
36
|
+
OutputNode,
|
37
|
+
OutputStream,
|
38
|
+
)
|
20
39
|
from ..dag.schema import Node, Stream
|
21
40
|
from ..exceptions import FFMpegValueError
|
22
41
|
from ..schema import Default
|
42
|
+
from ..streams.av import AVStream
|
23
43
|
from ..utils.escaping import escape
|
24
44
|
from ..utils.lazy_eval.schema import LazyValue
|
25
45
|
from ..utils.run import command_line
|
@@ -27,6 +47,341 @@ from .context import DAGContext
|
|
27
47
|
from .validate import validate
|
28
48
|
|
29
49
|
|
50
|
+
def get_options_dict() -> dict[str, FFMpegOption]:
|
51
|
+
options = load(list[FFMpegOption], "options")
|
52
|
+
return {option.name: option for option in options}
|
53
|
+
|
54
|
+
|
55
|
+
def get_filter_dict() -> dict[str, FFMpegFilter]:
|
56
|
+
filters = load(list[FFMpegFilter], "filters")
|
57
|
+
return {filter.name: filter for filter in filters}
|
58
|
+
|
59
|
+
|
60
|
+
def parse_options(tokens: list[str]) -> dict[str, list[str | None | bool]]:
|
61
|
+
"""
|
62
|
+
Parse FFmpeg command-line options into a structured dictionary.
|
63
|
+
|
64
|
+
This function processes a list of command-line tokens and converts them into
|
65
|
+
a dictionary where keys are option names (without the leading '-') and values
|
66
|
+
are lists of their corresponding values. Boolean options are handled specially:
|
67
|
+
- '-option' becomes {'option': [None]}
|
68
|
+
- '-nooption' becomes {'option': [False]}
|
69
|
+
|
70
|
+
Args:
|
71
|
+
tokens: List of command-line tokens to parse
|
72
|
+
|
73
|
+
Returns:
|
74
|
+
Dictionary mapping option names to lists of their values
|
75
|
+
"""
|
76
|
+
parsed_options: dict[str, list[str | None | bool]] = defaultdict(list)
|
77
|
+
|
78
|
+
while tokens:
|
79
|
+
assert tokens[0][0] == "-", f"Expected option, got {tokens[0]}"
|
80
|
+
if len(tokens) == 1 or tokens[1][0] == "-":
|
81
|
+
if tokens[0].startswith("-no"):
|
82
|
+
# Handle boolean options with -no prefix
|
83
|
+
option_name = tokens[0][3:]
|
84
|
+
parsed_options[option_name] = [False]
|
85
|
+
else:
|
86
|
+
# Handle boolean options without value
|
87
|
+
option_name = tokens[0][1:]
|
88
|
+
parsed_options[option_name] = [None]
|
89
|
+
tokens = tokens[1:]
|
90
|
+
else:
|
91
|
+
# Handle options with values
|
92
|
+
option_name = tokens[0][1:]
|
93
|
+
option_value = tokens[1]
|
94
|
+
parsed_options[option_name].append(option_value)
|
95
|
+
tokens = tokens[2:]
|
96
|
+
|
97
|
+
return parsed_options
|
98
|
+
|
99
|
+
|
100
|
+
def parse_stream_selector(
|
101
|
+
selector: str, mapping: Mapping[str, FilterableStream]
|
102
|
+
) -> FilterableStream:
|
103
|
+
selector = selector.strip("[]")
|
104
|
+
|
105
|
+
if ":" in selector:
|
106
|
+
stream_label, _ = selector.split(":", 1)
|
107
|
+
else:
|
108
|
+
stream_label = selector
|
109
|
+
|
110
|
+
assert stream_label in mapping, f"Unknown stream label: {stream_label}"
|
111
|
+
stream = mapping[stream_label]
|
112
|
+
|
113
|
+
if isinstance(stream, AVStream):
|
114
|
+
if selector.count(":") == 1:
|
115
|
+
stream_label, stream_type = selector.split(":", 1)
|
116
|
+
return stream.video if stream_type == "v" else stream.audio
|
117
|
+
elif selector.count(":") == 2:
|
118
|
+
stream_label, stream_type, stream_index = selector.split(":", 2)
|
119
|
+
return (
|
120
|
+
stream.video_stream(int(stream_index))
|
121
|
+
if stream_type == "v"
|
122
|
+
else stream.audio_stream(int(stream_index))
|
123
|
+
)
|
124
|
+
else:
|
125
|
+
return stream
|
126
|
+
else:
|
127
|
+
return stream
|
128
|
+
|
129
|
+
|
130
|
+
def parse_output(
|
131
|
+
source: list[str],
|
132
|
+
in_streams: Mapping[str, FilterableStream],
|
133
|
+
ffmpeg_options: dict[str, FFMpegOption],
|
134
|
+
) -> list[OutputStream]:
|
135
|
+
tokens = source.copy()
|
136
|
+
|
137
|
+
export: list[OutputStream] = []
|
138
|
+
|
139
|
+
buffer: list[str] = []
|
140
|
+
while tokens:
|
141
|
+
token = tokens.pop(0)
|
142
|
+
if token.startswith("-") or len(buffer) % 2 == 1:
|
143
|
+
buffer.append(token)
|
144
|
+
continue
|
145
|
+
|
146
|
+
filename = token
|
147
|
+
options = parse_options(buffer)
|
148
|
+
|
149
|
+
map_options = options.pop("map", [])
|
150
|
+
inputs: list[FilterableStream] = []
|
151
|
+
for map_option in map_options:
|
152
|
+
assert isinstance(map_option, str), f"Expected map option, got {map_option}"
|
153
|
+
stream = parse_stream_selector(map_option, in_streams)
|
154
|
+
inputs.append(stream)
|
155
|
+
|
156
|
+
assert inputs, f"No inputs found for output {filename}"
|
157
|
+
export.append(output(*inputs, filename=filename, extra_options=options))
|
158
|
+
buffer = []
|
159
|
+
|
160
|
+
return export
|
161
|
+
|
162
|
+
|
163
|
+
def parse_input(
|
164
|
+
tokens: list[str], ffmpeg_options: dict[str, FFMpegOption]
|
165
|
+
) -> dict[str, FilterableStream]:
|
166
|
+
output: list[AVStream] = []
|
167
|
+
|
168
|
+
while "-i" in tokens:
|
169
|
+
index = tokens.index("-i")
|
170
|
+
filename = tokens[index + 1]
|
171
|
+
assert filename[0] != "-", f"Expected filename, got {filename}"
|
172
|
+
|
173
|
+
input_options_args = tokens[:index]
|
174
|
+
|
175
|
+
options = parse_options(input_options_args)
|
176
|
+
parameters: dict[str, str | bool] = {}
|
177
|
+
|
178
|
+
for key, value in options.items():
|
179
|
+
assert key in ffmpeg_options, f"Unknown option: {key}"
|
180
|
+
option = ffmpeg_options[key]
|
181
|
+
|
182
|
+
if option.is_input_option:
|
183
|
+
# just ignore not input options
|
184
|
+
if value[-1] is None:
|
185
|
+
parameters[key] = True
|
186
|
+
else:
|
187
|
+
parameters[key] = value[-1]
|
188
|
+
|
189
|
+
output.append(input(filename=filename, extra_options=parameters))
|
190
|
+
|
191
|
+
tokens = tokens[index + 2 :]
|
192
|
+
|
193
|
+
return {str(idx): stream for idx, stream in enumerate(output)}
|
194
|
+
|
195
|
+
|
196
|
+
def parse_filter_complex(
|
197
|
+
filter_complex: str,
|
198
|
+
stream_mapping: dict[str, FilterableStream],
|
199
|
+
ffmpeg_filters: dict[str, FFMpegFilter],
|
200
|
+
) -> dict[str, FilterableStream]:
|
201
|
+
"""
|
202
|
+
Parse an FFmpeg filter_complex string into a stream mapping.
|
203
|
+
|
204
|
+
This function processes a filter_complex string (e.g. "[0:v]scale=1280:720[v0]")
|
205
|
+
and converts it into a mapping of stream labels to their corresponding
|
206
|
+
FilterableStream objects. It handles:
|
207
|
+
- Input stream references (e.g. [0:v])
|
208
|
+
- Filter definitions with parameters
|
209
|
+
- Output stream labels (e.g. [v0])
|
210
|
+
|
211
|
+
Args:
|
212
|
+
filter_complex: The filter_complex string to parse
|
213
|
+
stream_mapping: Existing mapping of stream labels to streams
|
214
|
+
ffmpeg_filters: Dictionary of available FFmpeg filters
|
215
|
+
|
216
|
+
Returns:
|
217
|
+
Updated stream mapping with new filter outputs added
|
218
|
+
"""
|
219
|
+
filter_units = filter_complex.split(";")
|
220
|
+
|
221
|
+
for filter_unit in filter_units:
|
222
|
+
pattern = re.compile(
|
223
|
+
r"""
|
224
|
+
(?P<inputs>(\[[^\[\]]+\])*) # inputs: zero or more [label]
|
225
|
+
(?P<filter>[a-zA-Z0-9_]+) # filter name
|
226
|
+
(=?(?P<params>[^[]+?))? # optional =params (until next [ or end)
|
227
|
+
(?P<outputs>(\[[^\[\]]+\])*)$ # outputs: zero or more [label] at end
|
228
|
+
""",
|
229
|
+
re.VERBOSE,
|
230
|
+
)
|
231
|
+
|
232
|
+
match = pattern.match(filter_unit)
|
233
|
+
assert match, f"Invalid filter unit: {filter_unit}"
|
234
|
+
|
235
|
+
def extract_labels(label_str: str) -> list[str]:
|
236
|
+
return re.findall(r"\[([^\[\]]+)\]", label_str)
|
237
|
+
|
238
|
+
input_labels = extract_labels(match.group("inputs") or "")
|
239
|
+
output_labels = extract_labels(match.group("outputs") or "")
|
240
|
+
filter_name = match.group("filter")
|
241
|
+
param_str = match.group("params")
|
242
|
+
|
243
|
+
# Parse filter parameters into key-value pairs
|
244
|
+
filter_params = {}
|
245
|
+
if param_str:
|
246
|
+
param_parts = param_str.strip().split(":")
|
247
|
+
for part in param_parts:
|
248
|
+
if "=" in part:
|
249
|
+
key, value = part.split("=", 1)
|
250
|
+
filter_params[key.strip()] = value.strip()
|
251
|
+
|
252
|
+
assert isinstance(filter_name, str), f"Expected filter name, got {filter_name}"
|
253
|
+
ffmpeg_filter = ffmpeg_filters[filter_name]
|
254
|
+
filter_def = FFMpegFilterDef(
|
255
|
+
name=ffmpeg_filter.name,
|
256
|
+
typings_input=ffmpeg_filter.formula_typings_input
|
257
|
+
or tuple(k.type.value for k in ffmpeg_filter.stream_typings_input),
|
258
|
+
typings_output=ffmpeg_filter.formula_typings_output
|
259
|
+
or tuple(k.type.value for k in ffmpeg_filter.stream_typings_output),
|
260
|
+
)
|
261
|
+
input_streams = [
|
262
|
+
parse_stream_selector(label, stream_mapping) for label in input_labels
|
263
|
+
]
|
264
|
+
|
265
|
+
# Create the filter node with default options and parsed parameters
|
266
|
+
filter_node = filter_node_factory(
|
267
|
+
filter_def,
|
268
|
+
*input_streams,
|
269
|
+
**(
|
270
|
+
{k.name: Default(k.default) for k in ffmpeg_filter.options}
|
271
|
+
| filter_params
|
272
|
+
),
|
273
|
+
)
|
274
|
+
|
275
|
+
# Map output streams to their labels
|
276
|
+
for idx, (output_label, output_typing) in enumerate(
|
277
|
+
zip(output_labels, filter_node.output_typings)
|
278
|
+
):
|
279
|
+
if output_typing == StreamType.video:
|
280
|
+
stream_mapping[output_label] = VideoStream(node=filter_node, index=idx)
|
281
|
+
elif output_typing == StreamType.audio:
|
282
|
+
stream_mapping[output_label] = AudioStream(node=filter_node, index=idx)
|
283
|
+
else:
|
284
|
+
raise FFMpegValueError(f"Unknown stream type: {output_typing}")
|
285
|
+
|
286
|
+
return stream_mapping
|
287
|
+
|
288
|
+
|
289
|
+
def parse_global(
|
290
|
+
tokens: list[str], ffmpeg_options: dict[str, FFMpegOption]
|
291
|
+
) -> tuple[dict[str, str | bool], list[str]]:
|
292
|
+
"""
|
293
|
+
Parse global FFmpeg options from command-line tokens.
|
294
|
+
|
295
|
+
This function processes the global options that appear before any input files
|
296
|
+
in the FFmpeg command line. These options affect the entire FFmpeg process,
|
297
|
+
such as log level, overwrite flags, etc.
|
298
|
+
|
299
|
+
Args:
|
300
|
+
tokens: List of command-line tokens to parse
|
301
|
+
ffmpeg_options: Dictionary of valid FFmpeg options
|
302
|
+
|
303
|
+
Returns:
|
304
|
+
A tuple containing:
|
305
|
+
- Dictionary of parsed global options and their values
|
306
|
+
- Remaining tokens after global options are consumed
|
307
|
+
|
308
|
+
Example:
|
309
|
+
For tokens like ['-y', '-loglevel', 'quiet', '-i', 'input.mp4']:
|
310
|
+
Returns ({'y': True, 'loglevel': 'quiet'}, ['-i', 'input.mp4'])
|
311
|
+
"""
|
312
|
+
global_params: dict[str, str | bool] = {}
|
313
|
+
remaining_tokens = tokens.copy()
|
314
|
+
|
315
|
+
# Process tokens until we hit an input file marker (-i)
|
316
|
+
while remaining_tokens and remaining_tokens[0] != "-i":
|
317
|
+
if remaining_tokens[0].startswith("-"):
|
318
|
+
option_name = remaining_tokens[0][1:]
|
319
|
+
assert option_name in ffmpeg_options, (
|
320
|
+
f"Unknown global option: {option_name}"
|
321
|
+
)
|
322
|
+
option = ffmpeg_options[option_name]
|
323
|
+
|
324
|
+
if not option.is_global_option:
|
325
|
+
continue
|
326
|
+
|
327
|
+
if len(remaining_tokens) > 1 and not remaining_tokens[1].startswith("-"):
|
328
|
+
# Option with value
|
329
|
+
global_params[option_name] = remaining_tokens[1]
|
330
|
+
remaining_tokens = remaining_tokens[2:]
|
331
|
+
else:
|
332
|
+
# Boolean option
|
333
|
+
if option_name.startswith("no"):
|
334
|
+
global_params[option_name[2:]] = False
|
335
|
+
else:
|
336
|
+
global_params[option_name] = True
|
337
|
+
remaining_tokens = remaining_tokens[1:]
|
338
|
+
else:
|
339
|
+
# Skip non-option tokens
|
340
|
+
remaining_tokens = remaining_tokens[1:]
|
341
|
+
|
342
|
+
return global_params, remaining_tokens
|
343
|
+
|
344
|
+
|
345
|
+
def parse(cli: str) -> Stream:
|
346
|
+
# ffmpeg [global_options] {[input_file_options] -i input_url} ... {[output_file_options] output_url} ...
|
347
|
+
ffmpeg_options = get_options_dict()
|
348
|
+
ffmpeg_filters = get_filter_dict()
|
349
|
+
|
350
|
+
tokens = shlex.split(cli)
|
351
|
+
assert tokens[0] == "ffmpeg"
|
352
|
+
tokens = tokens[1:]
|
353
|
+
|
354
|
+
# Parse global options first
|
355
|
+
global_params, remaining_tokens = parse_global(tokens, ffmpeg_options)
|
356
|
+
|
357
|
+
# find the index of the last -i option
|
358
|
+
index = len(remaining_tokens) - 1 - list(reversed(remaining_tokens)).index("-i")
|
359
|
+
input_streams = parse_input(remaining_tokens[: index + 2], ffmpeg_options)
|
360
|
+
remaining_tokens = remaining_tokens[index + 2 :]
|
361
|
+
|
362
|
+
if "-filter_complex" in remaining_tokens:
|
363
|
+
index = remaining_tokens.index("-filter_complex")
|
364
|
+
filter_complex = remaining_tokens[index + 1]
|
365
|
+
filterable_streams = parse_filter_complex(
|
366
|
+
filter_complex, input_streams, ffmpeg_filters
|
367
|
+
)
|
368
|
+
remaining_tokens = remaining_tokens[index + 2 :]
|
369
|
+
else:
|
370
|
+
filterable_streams = {}
|
371
|
+
|
372
|
+
output_streams = parse_output(
|
373
|
+
remaining_tokens,
|
374
|
+
input_streams | filterable_streams,
|
375
|
+
ffmpeg_options,
|
376
|
+
)
|
377
|
+
|
378
|
+
# Create a stream with global options
|
379
|
+
result = merge_outputs(*output_streams)
|
380
|
+
if global_params:
|
381
|
+
result = result.global_args(extra_options=global_params)
|
382
|
+
return result
|
383
|
+
|
384
|
+
|
30
385
|
def compile(stream: Stream, auto_fix: bool = True) -> str:
|
31
386
|
"""
|
32
387
|
Compile a stream into a command-line string.
|
@@ -41,7 +396,7 @@ def compile(stream: Stream, auto_fix: bool = True) -> str:
|
|
41
396
|
Returns:
|
42
397
|
A command-line string that can be passed to FFmpeg
|
43
398
|
"""
|
44
|
-
return command_line(compile_as_list(stream, auto_fix))
|
399
|
+
return "ffmpeg " + command_line(compile_as_list(stream, auto_fix))
|
45
400
|
|
46
401
|
|
47
402
|
def compile_as_list(stream: Stream, auto_fix: bool = True) -> list[str]:
|
@@ -50,15 +405,36 @@ def compile_as_list(stream: Stream, auto_fix: bool = True) -> list[str]:
|
|
50
405
|
|
51
406
|
This function takes a Stream object representing an FFmpeg filter graph
|
52
407
|
and converts it into a list of command-line arguments that can be passed
|
53
|
-
to FFmpeg.
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
408
|
+
to FFmpeg. The compilation process follows these steps:
|
409
|
+
|
410
|
+
1. Validation: The graph is validated to ensure it's properly formed
|
411
|
+
- Checks for cycles in the graph
|
412
|
+
- Verifies stream types match filter requirements
|
413
|
+
- Ensures all streams are properly connected
|
414
|
+
|
415
|
+
2. Global Options: Processes global FFmpeg settings
|
416
|
+
- Log level, overwrite flags, etc.
|
417
|
+
- These affect the entire FFmpeg process
|
418
|
+
|
419
|
+
3. Input Files: Handles source media files
|
420
|
+
- File paths and input-specific options
|
421
|
+
- Stream selection and format options
|
422
|
+
- Timestamp and duration settings
|
423
|
+
|
424
|
+
4. Filter Graph: Combines all filters into a -filter_complex argument
|
425
|
+
- Properly labels all streams
|
426
|
+
- Maintains correct filter chain order
|
427
|
+
- Handles stream splitting and merging
|
428
|
+
|
429
|
+
5. Output Files: Processes destination files
|
430
|
+
- File paths and output options
|
431
|
+
- Codec and format settings
|
432
|
+
- Stream mapping and selection
|
58
433
|
|
59
|
-
|
60
|
-
|
61
|
-
|
434
|
+
If auto_fix is enabled, the function will attempt to fix common issues:
|
435
|
+
- Reconnecting disconnected nodes
|
436
|
+
- Adding missing split filters
|
437
|
+
- Fixing stream type mismatches
|
62
438
|
|
63
439
|
Args:
|
64
440
|
stream: The Stream object to compile into arguments
|
@@ -160,8 +536,16 @@ def get_stream_label(stream: Stream, context: DAGContext | None = None) -> str:
|
|
160
536
|
case AVStream():
|
161
537
|
return f"{get_node_label(stream.node, context)}"
|
162
538
|
case VideoStream():
|
539
|
+
if stream.index is not None:
|
540
|
+
return (
|
541
|
+
f"{get_node_label(stream.node, context)}:v:{stream.index}"
|
542
|
+
)
|
163
543
|
return f"{get_node_label(stream.node, context)}:v"
|
164
544
|
case AudioStream():
|
545
|
+
if stream.index is not None:
|
546
|
+
return (
|
547
|
+
f"{get_node_label(stream.node, context)}:a:{stream.index}"
|
548
|
+
)
|
165
549
|
return f"{get_node_label(stream.node, context)}:a"
|
166
550
|
case _:
|
167
551
|
raise FFMpegValueError(
|
@@ -206,10 +590,10 @@ def get_args_filter_node(node: FilterNode, context: DAGContext) -> list[str]:
|
|
206
590
|
outputs = context.get_outgoing_streams(node)
|
207
591
|
|
208
592
|
outgoing_labels = ""
|
209
|
-
for
|
593
|
+
for _output in sorted(outputs, key=lambda stream: stream.index or 0):
|
210
594
|
# NOTE: all outgoing streams must be filterable
|
211
|
-
assert isinstance(
|
212
|
-
outgoing_labels += f"[{get_stream_label(
|
595
|
+
assert isinstance(_output, FilterableStream)
|
596
|
+
outgoing_labels += f"[{get_stream_label(_output, context)}]"
|
213
597
|
|
214
598
|
commands = []
|
215
599
|
for key, value in node.kwargs.items():
|
@@ -77,7 +77,10 @@ def get_input_var_name(
|
|
77
77
|
case VideoStream():
|
78
78
|
match stream.node:
|
79
79
|
case InputNode():
|
80
|
-
|
80
|
+
if stream.index is not None:
|
81
|
+
return f"{get_output_var_name(stream.node, context)}.video_stream({stream.index})"
|
82
|
+
else:
|
83
|
+
return f"{get_output_var_name(stream.node, context)}.video"
|
81
84
|
case FilterNode():
|
82
85
|
if filter_data_dict[stream.node.name].is_dynamic_output:
|
83
86
|
return f"{get_output_var_name(stream.node, context)}.video({filter_stream_typed_index(stream, context)})"
|
@@ -91,7 +94,10 @@ def get_input_var_name(
|
|
91
94
|
case AudioStream():
|
92
95
|
match stream.node:
|
93
96
|
case InputNode():
|
94
|
-
|
97
|
+
if stream.index is not None:
|
98
|
+
return f"{get_output_var_name(stream.node, context)}.audio_stream({stream.index})"
|
99
|
+
else:
|
100
|
+
return f"{get_output_var_name(stream.node, context)}.audio"
|
95
101
|
case FilterNode():
|
96
102
|
if filter_data_dict[stream.node.name].is_dynamic_output:
|
97
103
|
return f"{get_output_var_name(stream.node, context)}.audio({filter_stream_typed_index(stream, context)})"
|
typed_ffmpeg/dag/nodes.py
CHANGED
@@ -337,9 +337,7 @@ class FilterableStream(Stream, OutputArgs):
|
|
337
337
|
)
|
338
338
|
|
339
339
|
def __post_init__(self) -> None:
|
340
|
-
if isinstance(self.node, InputNode):
|
341
|
-
assert self.index is None, "Input streams cannot have an index"
|
342
|
-
else:
|
340
|
+
if not isinstance(self.node, InputNode):
|
343
341
|
assert self.index is not None, "Filter streams must have an index"
|
344
342
|
|
345
343
|
|
typed_ffmpeg/streams/av.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
from ..dag.nodes import InputNode
|
2
|
-
from ..utils.typing import override
|
3
2
|
from .audio import AudioStream
|
4
3
|
from .video import VideoStream
|
5
4
|
|
@@ -12,11 +11,45 @@ class AVStream(AudioStream, VideoStream):
|
|
12
11
|
node: InputNode
|
13
12
|
|
14
13
|
@property
|
15
|
-
@override
|
16
14
|
def video(self) -> VideoStream:
|
15
|
+
"""
|
16
|
+
Get the video stream from the input node.
|
17
|
+
|
18
|
+
Returns:
|
19
|
+
VideoStream: The video stream from the input node.
|
20
|
+
"""
|
17
21
|
return VideoStream(node=self.node, index=self.index)
|
18
22
|
|
19
23
|
@property
|
20
|
-
@override
|
21
24
|
def audio(self) -> AudioStream:
|
25
|
+
"""
|
26
|
+
Get the audio stream from the input node.
|
27
|
+
|
28
|
+
Returns:
|
29
|
+
AudioStream: The audio stream from the input node.
|
30
|
+
"""
|
22
31
|
return AudioStream(node=self.node, index=self.index)
|
32
|
+
|
33
|
+
def video_stream(self, index: int) -> VideoStream:
|
34
|
+
"""
|
35
|
+
Get the video stream from the input node with a specified index.
|
36
|
+
|
37
|
+
Args:
|
38
|
+
index: The index of the video stream.
|
39
|
+
|
40
|
+
Returns:
|
41
|
+
VideoStream: The video stream from the input node.
|
42
|
+
"""
|
43
|
+
return VideoStream(node=self.node, index=index)
|
44
|
+
|
45
|
+
def audio_stream(self, index: int) -> AudioStream:
|
46
|
+
"""
|
47
|
+
Get the audio stream from the input node with a specified index.
|
48
|
+
|
49
|
+
Args:
|
50
|
+
index: The index of the audio stream.
|
51
|
+
|
52
|
+
Returns:
|
53
|
+
AudioStream: The audio stream from the input node.
|
54
|
+
"""
|
55
|
+
return AudioStream(node=self.node, index=index)
|
{typed_ffmpeg_compatible-3.0.0a0.dist-info → typed_ffmpeg_compatible-3.0.2a0.dist-info}/METADATA
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: typed-ffmpeg-compatible
|
3
|
-
Version: 3.0.
|
3
|
+
Version: 3.0.2a0
|
4
4
|
Summary: Modern Python FFmpeg wrappers offer comprehensive support for complex filters, complete with detailed typing and documentation.
|
5
5
|
Home-page: https://livingbio.github.io/typed-ffmpeg/
|
6
6
|
License: MIT
|
{typed_ffmpeg_compatible-3.0.0a0.dist-info → typed_ffmpeg_compatible-3.0.2a0.dist-info}/RECORD
RENAMED
@@ -43,13 +43,14 @@ typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/vstack.json,sha256=MMulT4N
|
|
43
43
|
typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/xmedian.json,sha256=Er7eTcYZCop6QMZT0OXC9QV6tf76mRBYXjqPIqdKDtU,212
|
44
44
|
typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/xstack.json,sha256=6Svn1w2FopoYufK0kWCJR7xZndMyTItRs63-TafWMnk,211
|
45
45
|
typed_ffmpeg/common/cache/list/filters.json,sha256=s5-tRP3Jrun2nlj5nV_StPozuC5y3cNcb_xCc4OSulg,2390250
|
46
|
+
typed_ffmpeg/common/cache/list/options.json,sha256=bmODg-h8ZI90IWICSW44CllVkPRnUs6nJ4pOOQ__cqw,40453
|
46
47
|
typed_ffmpeg/common/cache.py,sha256=j0JvfX7jewLpdJWxgo7Pwze0BkUJdYGHX2uGR8BZ-9M,1386
|
47
48
|
typed_ffmpeg/common/schema.py,sha256=qM8yfMX9UU3EAQSNsTrr-SAmyqKx8eQCXTtu3RJWkEk,19673
|
48
49
|
typed_ffmpeg/common/serialize.py,sha256=dLim0DBP5CdJ1JiMV9xEmmh1XMSIhBOWs61EopAL15s,7719
|
49
50
|
typed_ffmpeg/compile/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
50
|
-
typed_ffmpeg/compile/compile_cli.py,sha256=
|
51
|
+
typed_ffmpeg/compile/compile_cli.py,sha256=ZRH3Kd4dJ2hcoXrTzsJqg1UJlCnUVXkQWjRARZhUJWE,27965
|
51
52
|
typed_ffmpeg/compile/compile_json.py,sha256=YCiTyfAnUVSbFr7BiQpmJYs13K5sa-xo77Iih33mb6I,992
|
52
|
-
typed_ffmpeg/compile/compile_python.py,sha256=
|
53
|
+
typed_ffmpeg/compile/compile_python.py,sha256=rsoF4spI3cq6uMHU3iN-j8QByj2yE5pEqjatWCaf0D4,11522
|
53
54
|
typed_ffmpeg/compile/context.py,sha256=macQ3HhEJ73j_WbWYtU9GCQCzcB_KQGAPimcuU-WOac,10946
|
54
55
|
typed_ffmpeg/compile/validate.py,sha256=QsWksdvlRwWw6hnatFo-ABakms1qDXRbEmvMQGRLrD8,9579
|
55
56
|
typed_ffmpeg/dag/__init__.py,sha256=qAApSNqjbZ1DtUaV5bSku9RwG7MpMPa1HJO764cSBt4,849
|
@@ -61,7 +62,7 @@ typed_ffmpeg/dag/io/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
|
|
61
62
|
typed_ffmpeg/dag/io/_input.py,sha256=KRLTSQPEfmgPcPEAJdeWRHZhNsClaJCB9Ac6czMOrmE,7214
|
62
63
|
typed_ffmpeg/dag/io/_output.py,sha256=_no6ffAOABznbLNTki8CYr7pvr4Sa0LweRfn38-cszs,12470
|
63
64
|
typed_ffmpeg/dag/io/output_args.py,sha256=SThIhZh9PXs2m6Fz5JsSy8oS-Te7GM_oz7HRuZo0-eI,13901
|
64
|
-
typed_ffmpeg/dag/nodes.py,sha256=
|
65
|
+
typed_ffmpeg/dag/nodes.py,sha256=lfHChT8JqRs3UUDWtgrWnnXn845HXSD5S1QmHpIUT4U,20526
|
65
66
|
typed_ffmpeg/dag/schema.py,sha256=dSq0o8e9qFazyd55k2yXcxbjoKZJEtqeROd91w1O3Wo,5728
|
66
67
|
typed_ffmpeg/dag/utils.py,sha256=hydh7_kjpOCw8WEGhXMxIXR4Ek-3DeoOt6esInuK2Xw,1941
|
67
68
|
typed_ffmpeg/exceptions.py,sha256=D4SID6WOwkjVV8O8mAjrEDHWn-8BRDnK_jteaDof1SY,2474
|
@@ -73,7 +74,7 @@ typed_ffmpeg/schema.py,sha256=KVtmyGeJutjFot70r6Nj8W8WBqwvfg2-rSgjdhPVh-o,1615
|
|
73
74
|
typed_ffmpeg/sources.py,sha256=ASt0l8FMW82X-70qw72167xpyBY7tQQClVuhy8r0HfU,86026
|
74
75
|
typed_ffmpeg/streams/__init__.py,sha256=Nt9uWpcVI1sQLl5Qt_kBCBcWOGZA1vczCQ0qvFbSko0,141
|
75
76
|
typed_ffmpeg/streams/audio.py,sha256=2oNRhb5UetWtlPG3NW73AjUZoFPl303yMv-6W1sGWt0,259054
|
76
|
-
typed_ffmpeg/streams/av.py,sha256=
|
77
|
+
typed_ffmpeg/streams/av.py,sha256=Nu6M7uV4aMNQf_kxADZuBdpDwFx_B7Z7x0p5j32n9iA,1500
|
77
78
|
typed_ffmpeg/streams/channel_layout.py,sha256=hGagaoc1tnWS8K1yiITp4f_92qq5e7C_zB15bHOL0DI,1162
|
78
79
|
typed_ffmpeg/streams/video.py,sha256=cQwHfew75YO_dbZjmpUYd-nXt1JHN0M7suFKKe5UH5s,453576
|
79
80
|
typed_ffmpeg/types.py,sha256=ly3zLtg7KxRa_jsDDrFPAxRZRjTQdWVpiQzOD9NdrFM,4137
|
@@ -87,8 +88,8 @@ typed_ffmpeg/utils/run.py,sha256=mSoAdcvD-InldqkRgWNc8iXKgJJoEMAOE4PL2gVmtqw,217
|
|
87
88
|
typed_ffmpeg/utils/snapshot.py,sha256=mKILRm6qiQV2egaD-70MSUEl-DFoLD5w_v9GZIequI4,2181
|
88
89
|
typed_ffmpeg/utils/typing.py,sha256=DBQn_gCF8C_DTwsfMHeCgfnNUROwAjlIcHrQ7lNDOoE,1187
|
89
90
|
typed_ffmpeg/utils/view.py,sha256=QCSlQoQkRBI-T0sWjiywGgM9DlKd8Te3CB2ZYX-pEVU,3413
|
90
|
-
typed_ffmpeg_compatible-3.0.
|
91
|
-
typed_ffmpeg_compatible-3.0.
|
92
|
-
typed_ffmpeg_compatible-3.0.
|
93
|
-
typed_ffmpeg_compatible-3.0.
|
94
|
-
typed_ffmpeg_compatible-3.0.
|
91
|
+
typed_ffmpeg_compatible-3.0.2a0.dist-info/LICENSE,sha256=8Aaya5i_09Cou2i3QMxTwz6uHGzi_fGA4uhkco07-A4,1066
|
92
|
+
typed_ffmpeg_compatible-3.0.2a0.dist-info/METADATA,sha256=eyXAjyXqnkCUsGoDc4EvOYPpx-3R8GsXKIbMuZpxnbU,7251
|
93
|
+
typed_ffmpeg_compatible-3.0.2a0.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
|
94
|
+
typed_ffmpeg_compatible-3.0.2a0.dist-info/entry_points.txt,sha256=KfZmNsM16GT_lF1otASIN6E3i6xXHXoB1gMeEdlptjA,44
|
95
|
+
typed_ffmpeg_compatible-3.0.2a0.dist-info/RECORD,,
|
{typed_ffmpeg_compatible-3.0.0a0.dist-info → typed_ffmpeg_compatible-3.0.2a0.dist-info}/LICENSE
RENAMED
File without changes
|
{typed_ffmpeg_compatible-3.0.0a0.dist-info → typed_ffmpeg_compatible-3.0.2a0.dist-info}/WHEEL
RENAMED
File without changes
|
File without changes
|