@@ -134,7 +134,7 @@ def add_stream_from_template(
134134 self , template : Stream , opaque : bool | None = None , ** kwargs
135135 ):
136136 """
137- Creates a new stream from a template. Supports video, audio, and subtitle streams.
137+ Creates a new stream from a template. Supports video, audio, subtitle, data and attachment streams.
138138
139139 :param template: Copy codec from another :class:`~av.stream.Stream` instance.
140140 :param opaque: If True, copy opaque data from the template's codec context.
@@ -145,9 +145,7 @@ def add_stream_from_template(
145145 opaque = template .type != "video"
146146
147147 if template .codec_context is None :
148- raise ValueError (
149- f"template stream of type { template .type } has no codec context"
150- )
148+ return self ._add_stream_without_codec_from_template (template , ** kwargs )
151149
152150 codec_obj : Codec
153151 if opaque : # Copy ctx from template.
@@ -196,6 +194,79 @@ def add_stream_from_template(
196194
197195 return py_stream
198196
197+ def _add_stream_without_codec_from_template (
198+ self , template : Stream , ** kwargs
199+ ) -> Stream :
200+ codec_type : cython .int = template .ptr .codecpar .codec_type
201+ if codec_type not in {lib .AVMEDIA_TYPE_ATTACHMENT , lib .AVMEDIA_TYPE_DATA }:
202+ raise ValueError (
203+ f"template stream of type { template .type } has no codec context"
204+ )
205+
206+ stream : cython .pointer [lib .AVStream ] = lib .avformat_new_stream (
207+ self .ptr , cython .NULL
208+ )
209+ if stream == cython .NULL :
210+ raise MemoryError ("Could not allocate stream" )
211+
212+ err_check (lib .avcodec_parameters_copy (stream .codecpar , template .ptr .codecpar ))
213+
214+ # Mirror basic properties that are not derived from a codec context.
215+ stream .time_base = template .ptr .time_base
216+ stream .start_time = template .ptr .start_time
217+ stream .duration = template .ptr .duration
218+ stream .disposition = template .ptr .disposition
219+
220+ py_stream : Stream = wrap_stream (self , stream , None )
221+ self .streams .add_stream (py_stream )
222+
223+ py_stream .metadata = dict (template .metadata )
224+
225+ for k , v in kwargs .items ():
226+ setattr (py_stream , k , v )
227+
228+ return py_stream
229+
230+ def add_attachment (self , name : str , mimetype : str , data : bytes ):
231+ """
232+ Create an attachment stream and embed its payload into the container header.
233+
234+ - Only supported by formats that support attachments (e.g. Matroska).
235+ - No per-packet muxing is required; attachments are written at header time.
236+ """
237+ # Create stream with no codec (attachments are codec-less).
238+ stream : cython .pointer [lib .AVStream ] = lib .avformat_new_stream (
239+ self .ptr , cython .NULL
240+ )
241+ if stream == cython .NULL :
242+ raise MemoryError ("Could not allocate stream" )
243+
244+ stream .codecpar .codec_type = lib .AVMEDIA_TYPE_ATTACHMENT
245+ stream .codecpar .codec_id = lib .AV_CODEC_ID_NONE
246+
247+ # Allocate and copy payload into codecpar.extradata.
248+ payload_size : cython .size_t = len (data )
249+ if payload_size :
250+ buf = cython .cast (cython .p_uchar , lib .av_malloc (payload_size + 1 ))
251+ if buf == cython .NULL :
252+ raise MemoryError ("Could not allocate attachment data" )
253+ # Copy bytes.
254+ for i in range (payload_size ):
255+ buf [i ] = data [i ]
256+ buf [payload_size ] = 0
257+ stream .codecpar .extradata = cython .cast (cython .p_uchar , buf )
258+ stream .codecpar .extradata_size = payload_size
259+
260+ # Wrap as user-land stream.
261+ meta_ptr = cython .address (stream .metadata )
262+ err_check (lib .av_dict_set (meta_ptr , b"filename" , name .encode (), 0 ))
263+ mime_bytes = mimetype .encode ()
264+ err_check (lib .av_dict_set (meta_ptr , b"mimetype" , mime_bytes , 0 ))
265+
266+ py_stream : Stream = wrap_stream (self , stream , None )
267+ self .streams .add_stream (py_stream )
268+ return py_stream
269+
199270 def add_data_stream (self , codec_name = None , options : dict | None = None ):
200271 """add_data_stream(codec_name=None)
201272
@@ -270,21 +341,20 @@ def start_encoding(self):
270341 # Finalize and open all streams.
271342 for stream in self .streams :
272343 ctx = stream .codec_context
273- # Skip codec context handling for data streams without codecs
344+ # Skip codec context handling for streams without codecs (e.g. data/attachments).
274345 if ctx is None :
275- if stream .type != "data" :
346+ if stream .type not in { "data" , "attachment" } :
276347 raise ValueError (f"Stream { stream .index } has no codec context" )
277- continue
278-
279- if not ctx .is_open :
280- for k , v in self .options .items ():
281- ctx .options .setdefault (k , v )
282- ctx .open ()
283-
284- # Track option consumption.
285- for k in self .options :
286- if k not in ctx .options :
287- used_options .add (k )
348+ else :
349+ if not ctx .is_open :
350+ for k , v in self .options .items ():
351+ ctx .options .setdefault (k , v )
352+ ctx .open ()
353+
354+ # Track option consumption.
355+ for k in self .options :
356+ if k not in ctx .options :
357+ used_options .add (k )
288358
289359 stream ._finalize_for_output ()
290360
0 commit comments