Skip to content

Commit 12a2bdc

Browse files
committed
Add add_data_stream() method
1 parent 8bf5d03 commit 12a2bdc

File tree

3 files changed

+111
-5
lines changed

3 files changed

+111
-5
lines changed

av/container/output.pyi

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
from fractions import Fraction
2-
from typing import Literal, Sequence, TypeVar, overload
2+
from typing import Literal, Sequence, TypeVar, Union, overload
33

44
from av.audio.stream import AudioStream
5+
from av.data.stream import DataStream
56
from av.packet import Packet
67
from av.stream import Stream
8+
from av.subtitles.stream import SubtitleStream
79
from av.video.stream import VideoStream
810

911
from .core import Container
1012

11-
_StreamT = TypeVar("_StreamT", bound=Stream, default=Stream)
13+
_StreamT = TypeVar("_StreamT", bound=Union[VideoStream, AudioStream, SubtitleStream])
1214

1315
class OutputContainer(Container):
1416
def __enter__(self) -> OutputContainer: ...
@@ -35,8 +37,11 @@ class OutputContainer(Container):
3537
rate: Fraction | int | None = None,
3638
options: dict[str, str] | None = None,
3739
**kwargs,
38-
) -> Stream: ...
40+
) -> VideoStream | AudioStream | SubtitleStream: ...
3941
def add_stream_from_template(self, template: _StreamT, **kwargs) -> _StreamT: ...
42+
def add_data_stream(
43+
self, codec_name: str | None = None, options: dict[str, str] | None = None
44+
) -> DataStream: ...
4045
def start_encoding(self) -> None: ...
4146
def close(self) -> None: ...
4247
def mux(self, packets: Packet | Sequence[Packet]) -> None: ...

av/container/output.pyx

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ cdef class OutputContainer(Container):
4747
"""add_stream(codec_name, rate=None)
4848
4949
Creates a new stream from a codec name and returns it.
50+
Supports video, audio, and subtitle streams.
5051
5152
:param codec_name: The name of a codec.
5253
:type codec_name: str | Codec
@@ -137,7 +138,7 @@ cdef class OutputContainer(Container):
137138

138139
def add_stream_from_template(self, Stream template not None, **kwargs):
139140
"""
140-
Creates a new stream from a template.
141+
Creates a new stream from a template. Supports video, audio, and subtitle streams.
141142
142143
:param template: Copy codec from another :class:`~av.stream.Stream` instance.
143144
:param \\**kwargs: Set attributes for the stream.
@@ -192,6 +193,65 @@ cdef class OutputContainer(Container):
192193

193194
return py_stream
194195

196+
197+
def add_data_stream(self, codec_name=None, dict options=None):
198+
"""add_data_stream(codec_name=None)
199+
200+
Creates a new data stream and returns it.
201+
202+
:param codec_name: Optional name of the data codec (e.g. 'klv')
203+
:type codec_name: str | None
204+
:param dict options: Stream options.
205+
:rtype: The new :class:`~av.data.stream.DataStream`.
206+
"""
207+
cdef const lib.AVCodec *codec = NULL
208+
209+
if codec_name is not None:
210+
codec = lib.avcodec_find_encoder_by_name(codec_name.encode())
211+
if codec == NULL:
212+
raise ValueError(f"Unknown data codec: {codec_name}")
213+
214+
# Assert that this format supports the requested codec
215+
if not lib.avformat_query_codec(self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL):
216+
raise ValueError(
217+
f"{self.format.name!r} format does not support {codec_name!r} codec"
218+
)
219+
220+
# Create new stream in the AVFormatContext
221+
cdef lib.AVStream *stream = lib.avformat_new_stream(self.ptr, codec)
222+
if stream == NULL:
223+
raise MemoryError("Could not allocate stream")
224+
225+
# Set up codec context if we have a codec
226+
cdef lib.AVCodecContext *codec_context = NULL
227+
if codec != NULL:
228+
codec_context = lib.avcodec_alloc_context3(codec)
229+
if codec_context == NULL:
230+
raise MemoryError("Could not allocate codec context")
231+
232+
# Some formats want stream headers to be separate
233+
if self.ptr.oformat.flags & lib.AVFMT_GLOBALHEADER:
234+
codec_context.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER
235+
236+
# Initialize stream codec parameters
237+
err_check(lib.avcodec_parameters_from_context(stream.codecpar, codec_context))
238+
else:
239+
# For raw data streams, just set the codec type
240+
stream.codecpar.codec_type = lib.AVMEDIA_TYPE_DATA
241+
242+
# Construct the user-land stream
243+
cdef CodecContext py_codec_context = None
244+
if codec_context != NULL:
245+
py_codec_context = wrap_codec_context(codec_context, codec)
246+
247+
cdef Stream py_stream = wrap_stream(self, stream, py_codec_context)
248+
self.streams.add_stream(py_stream)
249+
250+
if options:
251+
py_stream.options.update(options)
252+
253+
return py_stream
254+
195255
cpdef start_encoding(self):
196256
"""Write the file header! Called automatically."""
197257

@@ -206,8 +266,11 @@ cdef class OutputContainer(Container):
206266
cdef Stream stream
207267
for stream in self.streams:
208268
ctx = stream.codec_context
269+
# Skip codec context handling for data streams without codecs
209270
if ctx is None:
210-
raise ValueError(f"Stream {stream.index} has no codec context")
271+
if stream.type != "data":
272+
raise ValueError(f"Stream {stream.index} has no codec context")
273+
continue
211274

212275
if not ctx.is_open:
213276
for k, v in self.options.items():

tests/test_streams.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from fractions import Fraction
2+
13
import av
24

35
from .common import fate_suite
@@ -78,6 +80,42 @@ def test_printing_video_stream2(self) -> None:
7880
container.close()
7981
input_.close()
8082

83+
def test_data_stream(self) -> None:
84+
# First test writing and reading a simple data stream
85+
container1 = av.open("data.ts", "w")
86+
data_stream = container1.add_data_stream()
87+
88+
test_data = [b"test data 1", b"test data 2", b"test data 3"]
89+
for i, data_ in enumerate(test_data):
90+
packet = av.Packet(data_)
91+
packet.pts = i
92+
packet.stream = data_stream
93+
container1.mux(packet)
94+
container1.close()
95+
96+
# Test reading back the data stream
97+
container = av.open("data.ts")
98+
99+
# Test best stream selection
100+
data = container.streams.best("data")
101+
assert data == container.streams.data[0]
102+
103+
# Test get method
104+
assert [data] == container.streams.get(data=0)
105+
assert [data] == container.streams.get(data=(0,))
106+
107+
# Verify we can read back all the packets, ignoring empty ones
108+
packets = [p for p in container.demux(data) if bytes(p)]
109+
assert len(packets) == len(test_data)
110+
for packet, original_data in zip(packets, test_data):
111+
assert bytes(packet) == original_data
112+
113+
# Test string representation
114+
repr = f"{data_stream}"
115+
assert repr.startswith("<av.DataStream #0") and repr.endswith(">")
116+
117+
container.close()
118+
81119
# def test_side_data(self) -> None:
82120
# container = av.open(fate_suite("mov/displaymatrix.mov"))
83121
# video = container.streams.video[0]

0 commit comments

Comments
 (0)