typed-ffmpeg-compatible 3.0.1__tar.gz → 3.0.2a0__tar.gz
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_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/PKG-INFO +1 -1
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/pyproject.toml +1 -1
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/compile/compile_cli.py +389 -13
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/LICENSE +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/README.md +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/__init__.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/base.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/__init__.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/.gitignore +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/acrossover.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/afir.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/aiir.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/ainterleave.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/amerge.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/amix.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/amovie.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/anequalizer.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/aphasemeter.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/asegment.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/aselect.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/asplit.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/astreamselect.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/bm3d.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/channelsplit.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/concat.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/decimate.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/ebur128.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/extractplanes.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/fieldmatch.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/guided.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/headphone.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/hstack.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/interleave.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/join.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/libplacebo.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/limitdiff.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/mergeplanes.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/mix.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/movie.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/premultiply.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/segment.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/select.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/signature.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/split.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/streamselect.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/unpremultiply.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/vstack.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/xmedian.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/xstack.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/list/filters.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache/list/options.json +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/schema.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/serialize.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/compile/__init__.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/compile/compile_json.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/compile/compile_python.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/compile/context.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/compile/validate.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/dag/__init__.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/dag/factory.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/dag/global_runnable/__init__.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/dag/global_runnable/global_args.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/dag/global_runnable/runnable.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/dag/io/__init__.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/dag/io/_input.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/dag/io/_output.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/dag/io/output_args.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/dag/nodes.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/dag/schema.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/dag/utils.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/exceptions.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/filters.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/info.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/probe.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/py.typed +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/schema.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/sources.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/streams/__init__.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/streams/audio.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/streams/av.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/streams/channel_layout.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/streams/video.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/types.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/utils/__init__.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/utils/escaping.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/utils/forzendict.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/utils/lazy_eval/__init__.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/utils/lazy_eval/operator.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/utils/lazy_eval/schema.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/utils/run.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/utils/snapshot.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/utils/typing.py +0 -0
- {typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/utils/view.py +0 -0
@@ -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
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "typed-ffmpeg-compatible"
|
3
|
-
version = "3.0.
|
3
|
+
version = "3.0.2a0"
|
4
4
|
description = "Modern Python FFmpeg wrappers offer comprehensive support for complex filters, complete with detailed typing and documentation."
|
5
5
|
authors = ["lucemia <lucemia@gmail.com>"]
|
6
6
|
readme = "README.md"
|
@@ -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
|
@@ -214,10 +590,10 @@ def get_args_filter_node(node: FilterNode, context: DAGContext) -> list[str]:
|
|
214
590
|
outputs = context.get_outgoing_streams(node)
|
215
591
|
|
216
592
|
outgoing_labels = ""
|
217
|
-
for
|
593
|
+
for _output in sorted(outputs, key=lambda stream: stream.index or 0):
|
218
594
|
# NOTE: all outgoing streams must be filterable
|
219
|
-
assert isinstance(
|
220
|
-
outgoing_labels += f"[{get_stream_label(
|
595
|
+
assert isinstance(_output, FilterableStream)
|
596
|
+
outgoing_labels += f"[{get_stream_label(_output, context)}]"
|
221
597
|
|
222
598
|
commands = []
|
223
599
|
for key, value in node.kwargs.items():
|
File without changes
|
File without changes
|
{typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/__init__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/cache.py
RENAMED
File without changes
|
{typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/common/schema.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/dag/__init__.py
RENAMED
File without changes
|
{typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/dag/factory.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/dag/io/_input.py
RENAMED
File without changes
|
{typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/dag/io/_output.py
RENAMED
File without changes
|
File without changes
|
{typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/dag/nodes.py
RENAMED
File without changes
|
{typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/dag/schema.py
RENAMED
File without changes
|
{typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/dag/utils.py
RENAMED
File without changes
|
{typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/exceptions.py
RENAMED
File without changes
|
{typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/filters.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/schema.py
RENAMED
File without changes
|
{typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/sources.py
RENAMED
File without changes
|
File without changes
|
{typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/streams/audio.py
RENAMED
File without changes
|
{typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/streams/av.py
RENAMED
File without changes
|
File without changes
|
{typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/streams/video.py
RENAMED
File without changes
|
File without changes
|
{typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/utils/__init__.py
RENAMED
File without changes
|
{typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/utils/escaping.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/utils/run.py
RENAMED
File without changes
|
{typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/utils/snapshot.py
RENAMED
File without changes
|
{typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/utils/typing.py
RENAMED
File without changes
|
{typed_ffmpeg_compatible-3.0.1 → typed_ffmpeg_compatible-3.0.2a0}/src/typed_ffmpeg/utils/view.py
RENAMED
File without changes
|