typed-ffmpeg-compatible 3.2.5__py3-none-any.whl → 3.3.1__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/__init__.py CHANGED
@@ -28,7 +28,7 @@ from .dag import Stream
28
28
  from .exceptions import FFMpegExecuteError, FFMpegTypeError, FFMpegValueError
29
29
  from .ffprobe.probe import probe, probe_obj
30
30
  from .info import get_codecs, get_decoders, get_encoders
31
- from .streams import AudioStream, AVStream, VideoStream
31
+ from .streams import AudioStream, AVStream, SubtitleStream, VideoStream
32
32
 
33
33
  __all__ = [
34
34
  "sources",
@@ -46,6 +46,7 @@ __all__ = [
46
46
  "AudioStream",
47
47
  "VideoStream",
48
48
  "AVStream",
49
+ "SubtitleStream",
49
50
  "vfilter",
50
51
  "afilter",
51
52
  "filter_multi_output",
typed_ffmpeg/_version.py CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '3.2.5'
21
- __version_tuple__ = version_tuple = (3, 2, 5)
20
+ __version__ = version = '3.3.1'
21
+ __version_tuple__ = version_tuple = (3, 3, 1)
typed_ffmpeg/base.py CHANGED
@@ -137,7 +137,7 @@ def filter_multi_output(
137
137
  *streams: FilterableStream,
138
138
  name: str,
139
139
  input_typings: tuple[StreamType, ...] = (),
140
- output_tyings: tuple[StreamType, ...] = (),
140
+ output_typings: tuple[StreamType, ...] = (),
141
141
  **kwargs: Any,
142
142
  ) -> FilterNode:
143
143
  """
@@ -151,7 +151,7 @@ def filter_multi_output(
151
151
  *streams: One or more input streams to apply the filter to
152
152
  name: The FFmpeg filter name (e.g., 'split', 'channelsplit', etc.)
153
153
  input_typings: The expected types of the input streams
154
- output_tyings: The expected types of each output stream
154
+ output_typings: The expected types of each output stream
155
155
  **kwargs: Filter-specific parameters as keyword arguments
156
156
 
157
157
  Returns:
@@ -162,7 +162,7 @@ def filter_multi_output(
162
162
  ```python
163
163
  # Split a video into two identical streams
164
164
  split_node = ffmpeg.filter_multi_output(
165
- stream, name="split", output_tyings=(StreamType.video, StreamType.video)
165
+ stream, name="split", output_typings=(StreamType.video, StreamType.video)
166
166
  )
167
167
  stream1 = split_node.video(0)
168
168
  stream2 = split_node.video(1)
@@ -172,12 +172,13 @@ def filter_multi_output(
172
172
  This function is for custom filters not implemented in typed-ffmpeg.
173
173
  Use the built-in filters from the filters module when available.
174
174
  """
175
+
175
176
  return FilterNode(
176
177
  name=name,
177
178
  kwargs=FrozenDict(kwargs),
178
179
  inputs=streams,
179
180
  input_typings=input_typings,
180
- output_typings=output_tyings,
181
+ output_typings=output_typings,
181
182
  )
182
183
 
183
184
 
@@ -20,6 +20,7 @@ import re
20
20
  import shlex
21
21
  from collections import defaultdict
22
22
  from collections.abc import Mapping
23
+ from dataclasses import replace
23
24
 
24
25
  from ..base import input, merge_outputs, output
25
26
  from ..common.cache import load
@@ -141,6 +142,12 @@ def parse_stream_selector(
141
142
  stream = mapping[stream_label]
142
143
 
143
144
  if isinstance(stream, AVStream):
145
+ if "?" in selector:
146
+ optional = True
147
+ selector = selector.strip("?")
148
+ else:
149
+ optional = False
150
+
144
151
  if ":" in selector:
145
152
  if selector.count(":") == 1:
146
153
  stream_label, stream_type = selector.split(":", 1)
@@ -151,13 +158,14 @@ def parse_stream_selector(
151
158
 
152
159
  match stream_type:
153
160
  case "v":
154
- return stream.video_stream(stream_index)
161
+ return stream.video_stream(stream_index, optional)
155
162
  case "a":
156
- return stream.audio_stream(stream_index)
163
+ return stream.audio_stream(stream_index, optional)
157
164
  case "s":
158
- return stream.subtitle_stream(stream_index)
165
+ return stream.subtitle_stream(stream_index, optional)
159
166
  case _:
160
167
  raise FFMpegValueError(f"Unknown stream type: {stream_type}")
168
+ return replace(stream, optional=optional)
161
169
  return stream
162
170
 
163
171
 
@@ -674,6 +682,8 @@ def get_stream_label(stream: Stream, context: DAGContext | None = None) -> str:
674
682
  if len(stream.node.output_typings) > 1:
675
683
  return f"{get_node_label(stream.node, context)}#{stream.index}"
676
684
  return f"{get_node_label(stream.node, context)}"
685
+ case OutputNode():
686
+ return f"{get_node_label(stream.node, context)}"
677
687
  case _:
678
688
  raise FFMpegValueError(
679
689
  f"Unknown node type: {stream.node.__class__.__name__}"
@@ -812,7 +822,10 @@ def get_args_output_node(node: OutputNode, context: DAGContext) -> list[str]:
812
822
  and len(node.inputs) == 1
813
823
  ):
814
824
  continue
815
- commands += ["-map", get_stream_label(input, context)]
825
+ if not input.optional:
826
+ commands += ["-map", get_stream_label(input, context)]
827
+ else:
828
+ commands += ["-map", f"{get_stream_label(input, context)}?"]
816
829
  else:
817
830
  commands += ["-map", f"[{get_stream_label(input, context)}]"]
818
831
 
@@ -77,10 +77,7 @@ def get_input_var_name(
77
77
  case VideoStream():
78
78
  match stream.node:
79
79
  case InputNode():
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"
80
+ return f"{get_output_var_name(stream.node, context)}.video_stream({stream.index}, optional={stream.optional})"
84
81
  case FilterNode():
85
82
  if filter_data_dict[stream.node.name].is_dynamic_output:
86
83
  return f"{get_output_var_name(stream.node, context)}.video({filter_stream_typed_index(stream, context)})"
@@ -94,10 +91,7 @@ def get_input_var_name(
94
91
  case AudioStream():
95
92
  match stream.node:
96
93
  case InputNode():
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"
94
+ return f"{get_output_var_name(stream.node, context)}.audio_stream({stream.index}, optional={stream.optional})"
101
95
  case FilterNode():
102
96
  if filter_data_dict[stream.node.name].is_dynamic_output:
103
97
  return f"{get_output_var_name(stream.node, context)}.audio({filter_stream_typed_index(stream, context)})"
@@ -111,10 +105,7 @@ def get_input_var_name(
111
105
  case SubtitleStream():
112
106
  match stream.node:
113
107
  case InputNode():
114
- if stream.index is not None:
115
- return f"{get_output_var_name(stream.node, context)}.subtitle_stream({stream.index})"
116
- else:
117
- return f"{get_output_var_name(stream.node, context)}.subtitle"
108
+ return f"{get_output_var_name(stream.node, context)}.subtitle_stream({stream.index}, optional={stream.optional})"
118
109
  case OutputStream():
119
110
  return f"{get_output_var_name(stream.node, context)}"
120
111
  case GlobalStream():
@@ -49,6 +49,14 @@ class Stream(HashableBaseModel):
49
49
  See Also: [Stream specifiers](https://ffmpeg.org/ffmpeg.html#Stream-specifiers-1) `stream_index`
50
50
  """
51
51
 
52
+ optional: bool = False
53
+ """
54
+ Represents whether the stream is optional.
55
+
56
+ Note:
57
+ See Also: [Advanced options](https://ffmpeg.org/ffmpeg.html#Advanced-options)
58
+ """
59
+
52
60
  def view(self, format: Literal["png", "svg", "dot"] = "png") -> str:
53
61
  """
54
62
  Visualize the stream.
@@ -1,5 +1,6 @@
1
1
  from .audio import AudioStream
2
2
  from .av import AVStream
3
+ from .subtitle import SubtitleStream
3
4
  from .video import VideoStream
4
5
 
5
- __all__ = ["AudioStream", "VideoStream", "AVStream"]
6
+ __all__ = ["AudioStream", "VideoStream", "AVStream", "SubtitleStream"]
@@ -41,38 +41,47 @@ class AVStream(AudioStream, VideoStream):
41
41
  """
42
42
  return SubtitleStream(node=self.node, index=self.index)
43
43
 
44
- def video_stream(self, index: int | None = None) -> VideoStream:
44
+ def video_stream(
45
+ self, index: int | None = None, optional: bool = False
46
+ ) -> VideoStream:
45
47
  """
46
48
  Get the video stream from the input node with a specified index.
47
49
 
48
50
  Args:
49
51
  index: The index of the video stream.
52
+ optional: Whether the video stream is optional.
50
53
 
51
54
  Returns:
52
55
  VideoStream: The video stream from the input node.
53
56
  """
54
- return VideoStream(node=self.node, index=index)
57
+ return VideoStream(node=self.node, index=index, optional=optional)
55
58
 
56
- def audio_stream(self, index: int | None = None) -> AudioStream:
59
+ def audio_stream(
60
+ self, index: int | None = None, optional: bool = False
61
+ ) -> AudioStream:
57
62
  """
58
63
  Get the audio stream from the input node with a specified index.
59
64
 
60
65
  Args:
61
66
  index: The index of the audio stream.
67
+ optional: Whether the audio stream is optional.
62
68
 
63
69
  Returns:
64
70
  AudioStream: The audio stream from the input node.
65
71
  """
66
- return AudioStream(node=self.node, index=index)
72
+ return AudioStream(node=self.node, index=index, optional=optional)
67
73
 
68
- def subtitle_stream(self, index: int | None = None) -> SubtitleStream:
74
+ def subtitle_stream(
75
+ self, index: int | None = None, optional: bool = False
76
+ ) -> SubtitleStream:
69
77
  """
70
78
  Get the subtitle stream from the input node with a specified index.
71
79
 
72
80
  Args:
73
81
  index: The index of the subtitle stream.
82
+ optional: Whether the subtitle stream is optional.
74
83
 
75
84
  Returns:
76
85
  SubtitleStream: The subtitle stream from the input node.
77
86
  """
78
- return SubtitleStream(node=self.node, index=index)
87
+ return SubtitleStream(node=self.node, index=index, optional=optional)
@@ -21,7 +21,7 @@ from syrupy.types import (
21
21
  from ..dag.schema import Stream
22
22
 
23
23
 
24
- class DAGSnapshotExtenstion(JSONSnapshotExtension):
24
+ class DAGSnapshotExtension(JSONSnapshotExtension):
25
25
  """
26
26
  A snapshot extension for serializing and testing FFmpeg DAG structures.
27
27
 
@@ -10,6 +10,7 @@ from __future__ import annotations
10
10
 
11
11
  from typing import Literal
12
12
 
13
+ from ..compile.compile_cli import get_args, get_stream_label
13
14
  from ..compile.context import DAGContext
14
15
  from ..dag.nodes import FilterNode, InputNode, OutputNode
15
16
  from ..dag.schema import Node
@@ -92,7 +93,7 @@ def view(node: Node, format: Literal["png", "svg", "dot"]) -> str:
92
93
  color = _get_node_color(node)
93
94
  graph.node(
94
95
  name=node.hex,
95
- label=node.repr(),
96
+ label=" ".join(get_args(node, context)),
96
97
  shape="box",
97
98
  style="filled",
98
99
  fillcolor=color,
@@ -105,7 +106,7 @@ def view(node: Node, format: Literal["png", "svg", "dot"]) -> str:
105
106
  graph.edge(
106
107
  stream.node.hex,
107
108
  node.hex,
108
- label=f"{'*' if stream.index is None else stream.index} => {idx}",
109
+ label=f"{get_stream_label(stream, context)} -> {idx}",
109
110
  )
110
111
 
111
112
  return graph.render(engine="dot")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: typed-ffmpeg-compatible
3
- Version: 3.2.5
3
+ Version: 3.3.1
4
4
  Summary: Modern Python FFmpeg wrappers offer comprehensive support for complex filters, complete with detailed typing and documentation.
5
5
  Author-email: lucemia <lucemia@gmail.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
- typed_ffmpeg/__init__.py,sha256=n1SZfENc9xhZ0eA3ZzkBNPuIY-Pt3-rOwB8-uUj5olU,1592
2
- typed_ffmpeg/_version.py,sha256=VJzOHz1iq5b2MvgVxeYYraqogSncMB5aG1qIW2TvKks,511
3
- typed_ffmpeg/base.py,sha256=C5Tqbx2I0c-09D7aXKZoGkspu-lAAeAhuOns5zr3PXQ,6304
1
+ typed_ffmpeg/__init__.py,sha256=yixdEj2DRDUwCVipkVMaPGNo-DjIA_6ZSZrYQTxkUQY,1630
2
+ typed_ffmpeg/_version.py,sha256=DGQPU6_vrM6PfHseD1DbTrLzSemTnUX5siYWgZbIrdI,511
3
+ typed_ffmpeg/base.py,sha256=oNL_ZCYiweYoKOm2Kh-OePvIfVOlpQ4cDF8_aLNTz98,6309
4
4
  typed_ffmpeg/exceptions.py,sha256=D4SID6WOwkjVV8O8mAjrEDHWn-8BRDnK_jteaDof1SY,2474
5
5
  typed_ffmpeg/filters.py,sha256=_lBpGZgPHK3KgGVrw-TCdQEsBRuEXVIgwggYNGd80MU,110076
6
6
  typed_ffmpeg/info.py,sha256=0KCzf8hJaI6ObPRT0uftLa96RlYvaQmoEq1sqFJSc24,9521
@@ -13,15 +13,15 @@ typed_ffmpeg/common/cache.py,sha256=j0JvfX7jewLpdJWxgo7Pwze0BkUJdYGHX2uGR8BZ-9M,
13
13
  typed_ffmpeg/common/schema.py,sha256=qM8yfMX9UU3EAQSNsTrr-SAmyqKx8eQCXTtu3RJWkEk,19673
14
14
  typed_ffmpeg/common/serialize.py,sha256=dLim0DBP5CdJ1JiMV9xEmmh1XMSIhBOWs61EopAL15s,7719
15
15
  typed_ffmpeg/compile/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- typed_ffmpeg/compile/compile_cli.py,sha256=4XCkBC2WGP-P03XWVI3tgoBs--oQLvhuqyocSk8HhMw,32491
16
+ typed_ffmpeg/compile/compile_cli.py,sha256=FyFcFN2qgpi57M3fERzXlSdEs4Z7XwD0zKicXbl7_4M,32980
17
17
  typed_ffmpeg/compile/compile_json.py,sha256=YCiTyfAnUVSbFr7BiQpmJYs13K5sa-xo77Iih33mb6I,992
18
- typed_ffmpeg/compile/compile_python.py,sha256=zxaG9DC0r8lO_cOV9HBN6cnxy3-wCZyvo3-4M47F84E,11914
18
+ typed_ffmpeg/compile/compile_python.py,sha256=YnnRRHE8TEUiqFF9DsqkYOwIcA2ejCYw12k-O5n825A,11506
19
19
  typed_ffmpeg/compile/context.py,sha256=macQ3HhEJ73j_WbWYtU9GCQCzcB_KQGAPimcuU-WOac,10946
20
20
  typed_ffmpeg/compile/validate.py,sha256=QsWksdvlRwWw6hnatFo-ABakms1qDXRbEmvMQGRLrD8,9579
21
21
  typed_ffmpeg/dag/__init__.py,sha256=qAApSNqjbZ1DtUaV5bSku9RwG7MpMPa1HJO764cSBt4,849
22
22
  typed_ffmpeg/dag/factory.py,sha256=2IMVKP_2UaTrlGXBg8YDx5KXBqhpScJiJQ87PRrppzY,3147
23
23
  typed_ffmpeg/dag/nodes.py,sha256=lfHChT8JqRs3UUDWtgrWnnXn845HXSD5S1QmHpIUT4U,20526
24
- typed_ffmpeg/dag/schema.py,sha256=dSq0o8e9qFazyd55k2yXcxbjoKZJEtqeROd91w1O3Wo,5728
24
+ typed_ffmpeg/dag/schema.py,sha256=xsVT46XgT3Usgb4N9OWdjP74MG9dBrXgZGbjsYhsUEM,5916
25
25
  typed_ffmpeg/dag/utils.py,sha256=hydh7_kjpOCw8WEGhXMxIXR4Ek-3DeoOt6esInuK2Xw,1941
26
26
  typed_ffmpeg/dag/global_runnable/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
27
  typed_ffmpeg/dag/global_runnable/global_args.py,sha256=ehLtx4v-XxqkmODhpE_gik0r79hs4Sa8TJnRsH9Fj1o,8043
@@ -35,9 +35,9 @@ typed_ffmpeg/ffprobe/parse.py,sha256=fq1cGFkUpUdvyVi3S-TAQ7qn-tXUdae6ADhymZt7-t4
35
35
  typed_ffmpeg/ffprobe/probe.py,sha256=D_opsKp3OgT2HSBzhxgY1b9SIqXpDhz3xh_k9HUGux0,10367
36
36
  typed_ffmpeg/ffprobe/schema.py,sha256=PBgBWYGO8wKnI0Lae9oCJ1Nprhv2ciPkdLrumzPVllY,13631
37
37
  typed_ffmpeg/ffprobe/xml2json.py,sha256=1TSuxR7SYc-M_-JmE-1khHGbXCapgW0Oap9kzL0nwNg,2455
38
- typed_ffmpeg/streams/__init__.py,sha256=Nt9uWpcVI1sQLl5Qt_kBCBcWOGZA1vczCQ0qvFbSko0,141
38
+ typed_ffmpeg/streams/__init__.py,sha256=32kiqBVigKH2EqS3cA6EfXCW8KpVdQNWSStAmQ0MktM,196
39
39
  typed_ffmpeg/streams/audio.py,sha256=2oNRhb5UetWtlPG3NW73AjUZoFPl303yMv-6W1sGWt0,259054
40
- typed_ffmpeg/streams/av.py,sha256=q7zqNyr44vBT7tiR2nKUGTf3g93HtNVA_WSwCiysmgI,2240
40
+ typed_ffmpeg/streams/av.py,sha256=qrsO692BG6OVMWVwIeuCKPntBGTR685lc2ULbGPTSkQ,2594
41
41
  typed_ffmpeg/streams/channel_layout.py,sha256=hGagaoc1tnWS8K1yiITp4f_92qq5e7C_zB15bHOL0DI,1162
42
42
  typed_ffmpeg/streams/subtitle.py,sha256=zhbRKUYH0C972Hurm4hK_dqLvT4Y6v_VNZFzQrkCm9A,141
43
43
  typed_ffmpeg/streams/video.py,sha256=cQwHfew75YO_dbZjmpUYd-nXt1JHN0M7suFKKe5UH5s,453576
@@ -45,15 +45,15 @@ typed_ffmpeg/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuF
45
45
  typed_ffmpeg/utils/escaping.py,sha256=m6CTEBwWZTFdtZHTHW-3pQCgkpdZb9f9ynoO-gsD7uM,2937
46
46
  typed_ffmpeg/utils/forzendict.py,sha256=9QWdPQA2AbSfiC9-mrq1YeZFl1qN--zw8WlsqQ2ulCk,3114
47
47
  typed_ffmpeg/utils/run.py,sha256=mSoAdcvD-InldqkRgWNc8iXKgJJoEMAOE4PL2gVmtqw,2178
48
- typed_ffmpeg/utils/snapshot.py,sha256=mKILRm6qiQV2egaD-70MSUEl-DFoLD5w_v9GZIequI4,2181
48
+ typed_ffmpeg/utils/snapshot.py,sha256=SHdfM1OXxR0cReNs4snDdAASjW45unz2KjF_6jMwq7c,2180
49
49
  typed_ffmpeg/utils/typing.py,sha256=DBQn_gCF8C_DTwsfMHeCgfnNUROwAjlIcHrQ7lNDOoE,1187
50
- typed_ffmpeg/utils/view.py,sha256=QCSlQoQkRBI-T0sWjiywGgM9DlKd8Te3CB2ZYX-pEVU,3413
50
+ typed_ffmpeg/utils/view.py,sha256=jiCKJnx-KVJh-uvhkADBQNlWH1uHPqyYgmSZ_iSEj0c,3484
51
51
  typed_ffmpeg/utils/lazy_eval/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
52
  typed_ffmpeg/utils/lazy_eval/operator.py,sha256=QWybd-UH3VdDa8kgWkqAMi3WV0b0WF1d1JixQr6is2E,4136
53
53
  typed_ffmpeg/utils/lazy_eval/schema.py,sha256=WSg-E3MS3itN1AT6Dq4Z9btnRHEReuN3o6zruXou7h4,9623
54
- typed_ffmpeg_compatible-3.2.5.dist-info/licenses/LICENSE,sha256=8Aaya5i_09Cou2i3QMxTwz6uHGzi_fGA4uhkco07-A4,1066
55
- typed_ffmpeg_compatible-3.2.5.dist-info/METADATA,sha256=GSSr2eBSGWwgMrZZBx4wgAcRx6bas71nQjLxF8GVJPA,8385
56
- typed_ffmpeg_compatible-3.2.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
57
- typed_ffmpeg_compatible-3.2.5.dist-info/entry_points.txt,sha256=kUQvZ27paV-07qtkIFV-emKsYtjFOTw9kknBRSXPs04,45
58
- typed_ffmpeg_compatible-3.2.5.dist-info/top_level.txt,sha256=vuASJGVRQiNmhWY1pt0RXESWSNkknWXqWLIRAU7H_L4,13
59
- typed_ffmpeg_compatible-3.2.5.dist-info/RECORD,,
54
+ typed_ffmpeg_compatible-3.3.1.dist-info/licenses/LICENSE,sha256=8Aaya5i_09Cou2i3QMxTwz6uHGzi_fGA4uhkco07-A4,1066
55
+ typed_ffmpeg_compatible-3.3.1.dist-info/METADATA,sha256=5ZtyLl8XCPa1W-B6XCCjyosTQMJotk5LWqy1WiixwYg,8385
56
+ typed_ffmpeg_compatible-3.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
57
+ typed_ffmpeg_compatible-3.3.1.dist-info/entry_points.txt,sha256=kUQvZ27paV-07qtkIFV-emKsYtjFOTw9kknBRSXPs04,45
58
+ typed_ffmpeg_compatible-3.3.1.dist-info/top_level.txt,sha256=vuASJGVRQiNmhWY1pt0RXESWSNkknWXqWLIRAU7H_L4,13
59
+ typed_ffmpeg_compatible-3.3.1.dist-info/RECORD,,