Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions av/container/output.pyi
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
from fractions import Fraction
from typing import Literal, Sequence, TypeVar, overload
from typing import Literal, Sequence, TypeVar, Union, overload

from av.audio.stream import AudioStream
from av.data.stream import DataStream
from av.packet import Packet
from av.stream import Stream
from av.subtitles.stream import SubtitleStream
from av.video.stream import VideoStream

from .core import Container

_StreamT = TypeVar("_StreamT", bound=Stream, default=Stream)
_StreamT = TypeVar("_StreamT", bound=Union[VideoStream, AudioStream, SubtitleStream])

class OutputContainer(Container):
def __enter__(self) -> OutputContainer: ...
Expand All @@ -35,8 +37,11 @@ class OutputContainer(Container):
rate: Fraction | int | None = None,
options: dict[str, str] | None = None,
**kwargs,
) -> Stream: ...
) -> VideoStream | AudioStream | SubtitleStream: ...
def add_stream_from_template(self, template: _StreamT, **kwargs) -> _StreamT: ...
def add_data_stream(
self, codec_name: str | None = None, options: dict[str, str] | None = None
) -> DataStream: ...
def start_encoding(self) -> None: ...
def close(self) -> None: ...
def mux(self, packets: Packet | Sequence[Packet]) -> None: ...
Expand Down
67 changes: 65 additions & 2 deletions av/container/output.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ cdef class OutputContainer(Container):
"""add_stream(codec_name, rate=None)

Creates a new stream from a codec name and returns it.
Supports video, audio, and subtitle streams.

:param codec_name: The name of a codec.
:type codec_name: str | Codec
Expand Down Expand Up @@ -137,7 +138,7 @@ cdef class OutputContainer(Container):

def add_stream_from_template(self, Stream template not None, **kwargs):
"""
Creates a new stream from a template.
Creates a new stream from a template. Supports video, audio, and subtitle streams.

:param template: Copy codec from another :class:`~av.stream.Stream` instance.
:param \\**kwargs: Set attributes for the stream.
Expand Down Expand Up @@ -192,6 +193,65 @@ cdef class OutputContainer(Container):

return py_stream


def add_data_stream(self, codec_name=None, dict options=None):
"""add_data_stream(codec_name=None)

Creates a new data stream and returns it.

:param codec_name: Optional name of the data codec (e.g. 'klv')
:type codec_name: str | None
:param dict options: Stream options.
:rtype: The new :class:`~av.data.stream.DataStream`.
"""
cdef const lib.AVCodec *codec = NULL

if codec_name is not None:
codec = lib.avcodec_find_encoder_by_name(codec_name.encode())
if codec == NULL:
raise ValueError(f"Unknown data codec: {codec_name}")

# Assert that this format supports the requested codec
if not lib.avformat_query_codec(self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL):
raise ValueError(
f"{self.format.name!r} format does not support {codec_name!r} codec"
)

# Create new stream in the AVFormatContext
cdef lib.AVStream *stream = lib.avformat_new_stream(self.ptr, codec)
if stream == NULL:
raise MemoryError("Could not allocate stream")

# Set up codec context if we have a codec
cdef lib.AVCodecContext *codec_context = NULL
if codec != NULL:
codec_context = lib.avcodec_alloc_context3(codec)
if codec_context == NULL:
raise MemoryError("Could not allocate codec context")

# Some formats want stream headers to be separate
if self.ptr.oformat.flags & lib.AVFMT_GLOBALHEADER:
codec_context.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER

# Initialize stream codec parameters
err_check(lib.avcodec_parameters_from_context(stream.codecpar, codec_context))
else:
# For raw data streams, just set the codec type
stream.codecpar.codec_type = lib.AVMEDIA_TYPE_DATA

# Construct the user-land stream
cdef CodecContext py_codec_context = None
if codec_context != NULL:
py_codec_context = wrap_codec_context(codec_context, codec)

cdef Stream py_stream = wrap_stream(self, stream, py_codec_context)
self.streams.add_stream(py_stream)

if options:
py_stream.options.update(options)

return py_stream

cpdef start_encoding(self):
"""Write the file header! Called automatically."""

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

if not ctx.is_open:
for k, v in self.options.items():
Expand Down
38 changes: 38 additions & 0 deletions tests/test_streams.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from fractions import Fraction

import av

from .common import fate_suite
Expand Down Expand Up @@ -78,6 +80,42 @@ def test_printing_video_stream2(self) -> None:
container.close()
input_.close()

def test_data_stream(self) -> None:
# First test writing and reading a simple data stream
container1 = av.open("data.ts", "w")
data_stream = container1.add_data_stream()

test_data = [b"test data 1", b"test data 2", b"test data 3"]
for i, data_ in enumerate(test_data):
packet = av.Packet(data_)
packet.pts = i
packet.stream = data_stream
container1.mux(packet)
container1.close()

# Test reading back the data stream
container = av.open("data.ts")

# Test best stream selection
data = container.streams.best("data")
assert data == container.streams.data[0]

# Test get method
assert [data] == container.streams.get(data=0)
assert [data] == container.streams.get(data=(0,))

# Verify we can read back all the packets, ignoring empty ones
packets = [p for p in container.demux(data) if bytes(p)]
assert len(packets) == len(test_data)
for packet, original_data in zip(packets, test_data):
assert bytes(packet) == original_data

# Test string representation
repr = f"{data_stream}"
assert repr.startswith("<av.DataStream #0") and repr.endswith(">")

container.close()

# def test_side_data(self) -> None:
# container = av.open(fate_suite("mov/displaymatrix.mov"))
# video = container.streams.video[0]
Expand Down
Loading