typed-ffmpeg-compatible 3.2.4__py3-none-any.whl → 3.3__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/_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.4'
21
- __version_tuple__ = version_tuple = (3, 2, 4)
20
+ __version__ = version = '3.3'
21
+ __version_tuple__ = version_tuple = (3, 3)
@@ -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
@@ -38,6 +39,7 @@ from ..exceptions import FFMpegValueError
38
39
  from ..schema import Default
39
40
  from ..streams.audio import AudioStream
40
41
  from ..streams.av import AVStream
42
+ from ..streams.subtitle import SubtitleStream
41
43
  from ..streams.video import VideoStream
42
44
  from ..utils.escaping import escape
43
45
  from ..utils.lazy_eval.schema import LazyValue
@@ -140,20 +142,31 @@ def parse_stream_selector(
140
142
  stream = mapping[stream_label]
141
143
 
142
144
  if isinstance(stream, AVStream):
143
- if selector.count(":") == 1:
144
- stream_label, stream_type = selector.split(":", 1)
145
- return stream.video if stream_type == "v" else stream.audio
146
- elif selector.count(":") == 2:
147
- stream_label, stream_type, stream_index = selector.split(":", 2)
148
- return (
149
- stream.video_stream(int(stream_index))
150
- if stream_type == "v"
151
- else stream.audio_stream(int(stream_index))
152
- )
145
+ if "?" in selector:
146
+ optional = True
147
+ selector = selector.strip("?")
153
148
  else:
154
- return stream
155
- else:
156
- return stream
149
+ optional = False
150
+
151
+ if ":" in selector:
152
+ if selector.count(":") == 1:
153
+ stream_label, stream_type = selector.split(":", 1)
154
+ stream_index = None
155
+ elif selector.count(":") == 2:
156
+ stream_label, stream_type, _stream_index = selector.split(":", 2)
157
+ stream_index = int(_stream_index)
158
+
159
+ match stream_type:
160
+ case "v":
161
+ return stream.video_stream(stream_index, optional)
162
+ case "a":
163
+ return stream.audio_stream(stream_index, optional)
164
+ case "s":
165
+ return stream.subtitle_stream(stream_index, optional)
166
+ case _:
167
+ raise FFMpegValueError(f"Unknown stream type: {stream_type}")
168
+ return replace(stream, optional=optional)
169
+ return stream
157
170
 
158
171
 
159
172
  def _is_filename(token: str) -> bool:
@@ -371,12 +384,17 @@ def parse_filter_complex(
371
384
  for idx, (output_label, output_typing) in enumerate(
372
385
  zip(output_labels, filter_node.output_typings)
373
386
  ):
374
- if output_typing == StreamType.video:
375
- stream_mapping[output_label] = VideoStream(node=filter_node, index=idx)
376
- elif output_typing == StreamType.audio:
377
- stream_mapping[output_label] = AudioStream(node=filter_node, index=idx)
378
- else:
379
- raise FFMpegValueError(f"Unknown stream type: {output_typing}")
387
+ match output_typing:
388
+ case StreamType.video:
389
+ stream_mapping[output_label] = VideoStream(
390
+ node=filter_node, index=idx
391
+ )
392
+ case StreamType.audio:
393
+ stream_mapping[output_label] = AudioStream(
394
+ node=filter_node, index=idx
395
+ )
396
+ case _:
397
+ raise FFMpegValueError(f"Unknown stream type: {output_typing}")
380
398
 
381
399
  return stream_mapping
382
400
 
@@ -608,6 +626,7 @@ def get_stream_label(stream: Stream, context: DAGContext | None = None) -> str:
608
626
  For input streams, labels follow FFmpeg's stream specifier syntax:
609
627
  - Video streams: "0:v" (first input, video stream)
610
628
  - Audio streams: "0:a" (first input, audio stream)
629
+ - Subtitle streams: "0:s" (first input, subtitle stream)
611
630
  - AV streams: "0" (first input, all streams)
612
631
 
613
632
  For filter outputs, labels use the filter's label:
@@ -649,6 +668,12 @@ def get_stream_label(stream: Stream, context: DAGContext | None = None) -> str:
649
668
  f"{get_node_label(stream.node, context)}:a:{stream.index}"
650
669
  )
651
670
  return f"{get_node_label(stream.node, context)}:a"
671
+ case SubtitleStream():
672
+ if stream.index is not None:
673
+ return (
674
+ f"{get_node_label(stream.node, context)}:s:{stream.index}"
675
+ )
676
+ return f"{get_node_label(stream.node, context)}:s"
652
677
  case _:
653
678
  raise FFMpegValueError(
654
679
  f"Unknown stream type: {stream.__class__.__name__}"
@@ -795,7 +820,10 @@ def get_args_output_node(node: OutputNode, context: DAGContext) -> list[str]:
795
820
  and len(node.inputs) == 1
796
821
  ):
797
822
  continue
798
- commands += ["-map", get_stream_label(input, context)]
823
+ if not input.optional:
824
+ commands += ["-map", get_stream_label(input, context)]
825
+ else:
826
+ commands += ["-map", f"{get_stream_label(input, context)}?"]
799
827
  else:
800
828
  commands += ["-map", f"[{get_stream_label(input, context)}]"]
801
829
 
@@ -17,6 +17,7 @@ from ..dag.nodes import (
17
17
  from ..dag.schema import Node, Stream
18
18
  from ..streams.audio import AudioStream
19
19
  from ..streams.av import AVStream
20
+ from ..streams.subtitle import SubtitleStream
20
21
  from ..streams.video import VideoStream
21
22
  from .context import DAGContext
22
23
  from .validate import validate
@@ -76,10 +77,7 @@ def get_input_var_name(
76
77
  case VideoStream():
77
78
  match stream.node:
78
79
  case InputNode():
79
- if stream.index is not None:
80
- return f"{get_output_var_name(stream.node, context)}.video_stream({stream.index})"
81
- else:
82
- 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})"
83
81
  case FilterNode():
84
82
  if filter_data_dict[stream.node.name].is_dynamic_output:
85
83
  return f"{get_output_var_name(stream.node, context)}.video({filter_stream_typed_index(stream, context)})"
@@ -93,10 +91,7 @@ def get_input_var_name(
93
91
  case AudioStream():
94
92
  match stream.node:
95
93
  case InputNode():
96
- if stream.index is not None:
97
- return f"{get_output_var_name(stream.node, context)}.audio_stream({stream.index})"
98
- else:
99
- 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})"
100
95
  case FilterNode():
101
96
  if filter_data_dict[stream.node.name].is_dynamic_output:
102
97
  return f"{get_output_var_name(stream.node, context)}.audio({filter_stream_typed_index(stream, context)})"
@@ -107,12 +102,16 @@ def get_input_var_name(
107
102
  return f"{get_output_var_name(stream.node, context)}[{stream.index}]"
108
103
  else:
109
104
  return f"{get_output_var_name(stream.node, context)}"
105
+ case SubtitleStream():
106
+ match stream.node:
107
+ case InputNode():
108
+ return f"{get_output_var_name(stream.node, context)}.subtitle_stream({stream.index}, optional={stream.optional})"
110
109
  case OutputStream():
111
110
  return f"{get_output_var_name(stream.node, context)}"
112
111
  case GlobalStream():
113
112
  return f"{get_output_var_name(stream.node, context)}"
114
- case _:
115
- raise ValueError(f"Unknown node type: {type(stream.node)}")
113
+
114
+ raise ValueError(f"Unknown stream type: {type(stream)}") # pragma: no cover
116
115
 
117
116
 
118
117
  def get_output_var_name(node: Node, context: DAGContext) -> str:
@@ -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 ..dag.nodes import InputNode
2
2
  from .audio import AudioStream
3
+ from .subtitle import SubtitleStream
3
4
  from .video import VideoStream
4
5
 
5
6
 
@@ -30,26 +31,57 @@ class AVStream(AudioStream, VideoStream):
30
31
  """
31
32
  return AudioStream(node=self.node, index=self.index)
32
33
 
33
- def video_stream(self, index: int) -> VideoStream:
34
+ @property
35
+ def subtitle(self) -> SubtitleStream:
36
+ """
37
+ Get the subtitle stream from the input node.
38
+
39
+ Returns:
40
+ SubtitleStream: The subtitle stream from the input node.
41
+ """
42
+ return SubtitleStream(node=self.node, index=self.index)
43
+
44
+ def video_stream(
45
+ self, index: int | None = None, optional: bool = False
46
+ ) -> VideoStream:
34
47
  """
35
48
  Get the video stream from the input node with a specified index.
36
49
 
37
50
  Args:
38
51
  index: The index of the video stream.
52
+ optional: Whether the video stream is optional.
39
53
 
40
54
  Returns:
41
55
  VideoStream: The video stream from the input node.
42
56
  """
43
- return VideoStream(node=self.node, index=index)
57
+ return VideoStream(node=self.node, index=index, optional=optional)
44
58
 
45
- def audio_stream(self, index: int) -> AudioStream:
59
+ def audio_stream(
60
+ self, index: int | None = None, optional: bool = False
61
+ ) -> AudioStream:
46
62
  """
47
63
  Get the audio stream from the input node with a specified index.
48
64
 
49
65
  Args:
50
66
  index: The index of the audio stream.
67
+ optional: Whether the audio stream is optional.
51
68
 
52
69
  Returns:
53
70
  AudioStream: The audio stream from the input node.
54
71
  """
55
- return AudioStream(node=self.node, index=index)
72
+ return AudioStream(node=self.node, index=index, optional=optional)
73
+
74
+ def subtitle_stream(
75
+ self, index: int | None = None, optional: bool = False
76
+ ) -> SubtitleStream:
77
+ """
78
+ Get the subtitle stream from the input node with a specified index.
79
+
80
+ Args:
81
+ index: The index of the subtitle stream.
82
+ optional: Whether the subtitle stream is optional.
83
+
84
+ Returns:
85
+ SubtitleStream: The subtitle stream from the input node.
86
+ """
87
+ return SubtitleStream(node=self.node, index=index, optional=optional)
@@ -0,0 +1,7 @@
1
+ from ..dag.nodes import FilterableStream
2
+
3
+
4
+ class SubtitleStream(FilterableStream):
5
+ """
6
+ A stream that contains subtitle data.
7
+ """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: typed-ffmpeg-compatible
3
- Version: 3.2.4
3
+ Version: 3.3
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,5 +1,5 @@
1
1
  typed_ffmpeg/__init__.py,sha256=n1SZfENc9xhZ0eA3ZzkBNPuIY-Pt3-rOwB8-uUj5olU,1592
2
- typed_ffmpeg/_version.py,sha256=RFt1Qs0Gxv7mhjBYwRXtP2Na7ShtFF9luYHvh8kkWxM,511
2
+ typed_ffmpeg/_version.py,sha256=lgTPmPhRGLqWrHHDFV9z5QiSuA5Dn5Dch5YbYnpAWj4,506
3
3
  typed_ffmpeg/base.py,sha256=C5Tqbx2I0c-09D7aXKZoGkspu-lAAeAhuOns5zr3PXQ,6304
4
4
  typed_ffmpeg/exceptions.py,sha256=D4SID6WOwkjVV8O8mAjrEDHWn-8BRDnK_jteaDof1SY,2474
5
5
  typed_ffmpeg/filters.py,sha256=_lBpGZgPHK3KgGVrw-TCdQEsBRuEXVIgwggYNGd80MU,110076
@@ -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=SwXVtEK1-Bl-KWpFESMpX2On7nOGAAmsDQWGmnv3Dnw,31741
16
+ typed_ffmpeg/compile/compile_cli.py,sha256=Xuz4gAp4kqeywOxBNgPXH_Ie8lrmwfEdAl-HxMGo9kI,32892
17
17
  typed_ffmpeg/compile/compile_json.py,sha256=YCiTyfAnUVSbFr7BiQpmJYs13K5sa-xo77Iih33mb6I,992
18
- typed_ffmpeg/compile/compile_python.py,sha256=oo4e8Ldwk0OkrZtHucfuGR5JDFF8xY8omNKPMDyUpQ8,11506
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
@@ -37,8 +37,9 @@ typed_ffmpeg/ffprobe/schema.py,sha256=PBgBWYGO8wKnI0Lae9oCJ1Nprhv2ciPkdLrumzPVll
37
37
  typed_ffmpeg/ffprobe/xml2json.py,sha256=1TSuxR7SYc-M_-JmE-1khHGbXCapgW0Oap9kzL0nwNg,2455
38
38
  typed_ffmpeg/streams/__init__.py,sha256=Nt9uWpcVI1sQLl5Qt_kBCBcWOGZA1vczCQ0qvFbSko0,141
39
39
  typed_ffmpeg/streams/audio.py,sha256=2oNRhb5UetWtlPG3NW73AjUZoFPl303yMv-6W1sGWt0,259054
40
- typed_ffmpeg/streams/av.py,sha256=Nu6M7uV4aMNQf_kxADZuBdpDwFx_B7Z7x0p5j32n9iA,1500
40
+ typed_ffmpeg/streams/av.py,sha256=qrsO692BG6OVMWVwIeuCKPntBGTR685lc2ULbGPTSkQ,2594
41
41
  typed_ffmpeg/streams/channel_layout.py,sha256=hGagaoc1tnWS8K1yiITp4f_92qq5e7C_zB15bHOL0DI,1162
42
+ typed_ffmpeg/streams/subtitle.py,sha256=zhbRKUYH0C972Hurm4hK_dqLvT4Y6v_VNZFzQrkCm9A,141
42
43
  typed_ffmpeg/streams/video.py,sha256=cQwHfew75YO_dbZjmpUYd-nXt1JHN0M7suFKKe5UH5s,453576
43
44
  typed_ffmpeg/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
45
  typed_ffmpeg/utils/escaping.py,sha256=m6CTEBwWZTFdtZHTHW-3pQCgkpdZb9f9ynoO-gsD7uM,2937
@@ -50,9 +51,9 @@ typed_ffmpeg/utils/view.py,sha256=QCSlQoQkRBI-T0sWjiywGgM9DlKd8Te3CB2ZYX-pEVU,34
50
51
  typed_ffmpeg/utils/lazy_eval/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
52
  typed_ffmpeg/utils/lazy_eval/operator.py,sha256=QWybd-UH3VdDa8kgWkqAMi3WV0b0WF1d1JixQr6is2E,4136
52
53
  typed_ffmpeg/utils/lazy_eval/schema.py,sha256=WSg-E3MS3itN1AT6Dq4Z9btnRHEReuN3o6zruXou7h4,9623
53
- typed_ffmpeg_compatible-3.2.4.dist-info/licenses/LICENSE,sha256=8Aaya5i_09Cou2i3QMxTwz6uHGzi_fGA4uhkco07-A4,1066
54
- typed_ffmpeg_compatible-3.2.4.dist-info/METADATA,sha256=q3R0Y2gpwPQ1cLL04rvq8RySvoJvsb8xE1XANzruez0,8385
55
- typed_ffmpeg_compatible-3.2.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
56
- typed_ffmpeg_compatible-3.2.4.dist-info/entry_points.txt,sha256=kUQvZ27paV-07qtkIFV-emKsYtjFOTw9kknBRSXPs04,45
57
- typed_ffmpeg_compatible-3.2.4.dist-info/top_level.txt,sha256=vuASJGVRQiNmhWY1pt0RXESWSNkknWXqWLIRAU7H_L4,13
58
- typed_ffmpeg_compatible-3.2.4.dist-info/RECORD,,
54
+ typed_ffmpeg_compatible-3.3.dist-info/licenses/LICENSE,sha256=8Aaya5i_09Cou2i3QMxTwz6uHGzi_fGA4uhkco07-A4,1066
55
+ typed_ffmpeg_compatible-3.3.dist-info/METADATA,sha256=FtrDI-yit60_yKnw5CfgYotZX9MLBIMiNtqCXBnDPmA,8383
56
+ typed_ffmpeg_compatible-3.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
57
+ typed_ffmpeg_compatible-3.3.dist-info/entry_points.txt,sha256=kUQvZ27paV-07qtkIFV-emKsYtjFOTw9kknBRSXPs04,45
58
+ typed_ffmpeg_compatible-3.3.dist-info/top_level.txt,sha256=vuASJGVRQiNmhWY1pt0RXESWSNkknWXqWLIRAU7H_L4,13
59
+ typed_ffmpeg_compatible-3.3.dist-info/RECORD,,