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
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Features:
- Add ``VideoFrame.solid()`` to create a frame filled with one color, in any non-paletted pixel format.
- Add ``InputContainer.probe_score``, ``InputContainer.duration_estimation_method``, ``VideoStream.codec_rate``, ``VideoStream.video_delay``, ``Codec.wrapper_name``, and ``Codec.max_lowres``.
- Add ``VideoFrame.crop_top``, ``crop_bottom``, ``crop_left``, ``crop_right``, and ``VideoFrame.apply_cropping()``.
- Add ``InputContainer.programs``, exposing FFmpeg programs such as MPEG-TS channels, with their streams, metadata, and a settable ``discard``.
- Add ``Frame.decode_error_flags`` as a ``DecodeError`` flag and ``VideoFrame.repeat_pict``.
- Add ``Stream.cctx`` as the name for a stream's codec context; ``Stream.codec_context`` remains as an alias.
- Deprecate ``AudioStream.format``, ``AudioStream.layout``, ``VideoStream.format``, ``VideoStream.thread_count``, and ``VideoStream.thread_type``, which pass through to the codec context; use ``stream.cctx.format``, ``stream.cctx.layout``, ``stream.cctx.thread_count``, and ``stream.cctx.thread_type`` instead. Passing these to ``add_stream()`` is not deprecated.
Expand Down
12 changes: 12 additions & 0 deletions av/container/input.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import cython
from cython.cimports.av.codec.context import CodecContext, wrap_codec_context
from cython.cimports.av.container.program import wrap_program
from cython.cimports.av.container.streams import StreamContainer
from cython.cimports.av.dictionary import Dictionary
from cython.cimports.av.packet import Packet
Expand Down Expand Up @@ -145,6 +146,17 @@ def size(self):
if size >= 0:
return size

@property
def programs(self):
"""The programs in this container, such as the channels of an MPEG-TS
broadcast. Empty for formats without programs.

:type: tuple[Program, ...]
"""
self._assert_open()
i: cython.uint
return tuple(wrap_program(self, i) for i in range(self.ptr.nb_programs))

@property
def probe_score(self):
"""How confident FFmpeg was when it detected the input format, from 0 to 100.
Expand Down
3 changes: 3 additions & 0 deletions av/container/input.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ from av.video.frame import VideoFrame
from av.video.stream import VideoStream

from .core import Container
from .program import Program
from .streams import _Indices, _StreamSelector

@final
Expand All @@ -21,6 +22,8 @@ class InputContainer(Container):
bit_rate: int
size: int | None
probe_score: int
@property
def programs(self) -> tuple[Program, ...]: ...
duration_estimation_method: Literal["pts", "stream", "bitrate"] | None

@overload
Expand Down
11 changes: 11 additions & 0 deletions av/container/program.pxd
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
cimport libav as lib

from av.container.core cimport Container


cdef class Program:
cdef Container container
cdef readonly int index
cdef lib.AVProgram *_ptr(self)

cdef Program wrap_program(Container container, int index)
121 changes: 121 additions & 0 deletions av/container/program.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import cython
import cython.cimports.libav as lib
from cython.cimports.av.container.core import Container
from cython.cimports.av.utils import avdict_to_dict

from av.stream import Discard

_cinit_bypass_sentinel = cython.declare(object, object())


@cython.cfunc
def wrap_program(container: Container, index: cython.int) -> Program:
obj: Program = Program(_cinit_bypass_sentinel)
obj.container = container
obj.index = index
return obj


@cython.final
@cython.cclass
class Program:
"""A program in a container: a group of streams that play together.

MPEG-TS carries several programs (TV channels, in broadcast) in one stream.
Other formats, such as HLS, use programs for their variants.

Wraps :ffmpeg:`AVProgram`. Get one from :attr:`.InputContainer.programs`.
"""

def __cinit__(self, sentinel):
if sentinel is not _cinit_bypass_sentinel:
raise RuntimeError("cannot manually instantiate Program")

@cython.cfunc
def _ptr(self) -> cython.pointer[lib.AVProgram]:
self.container._assert_open()
return self.container.ptr.programs[self.index]

def __repr__(self):
if self.container.ptr == cython.NULL:
return f"<av.Program #{self.index} (container closed) at 0x{id(self):x}>"
return (
f"<av.Program #{self.index} id={self.id}, "
f"{len(self.streams)} streams at 0x{id(self):x}>"
)

@property
def id(self):
"""The format-specific ID of this program. In MPEG-TS, this is the
program number (service ID).

:type: int
"""
return self._ptr().id

@property
def program_num(self):
"""The MPEG-TS program number, or 0 for other formats.

:type: int
"""
return self._ptr().program_num

@property
def pmt_pid(self):
"""The PID of this program's Program Map Table in MPEG-TS.

:type: int
"""
return self._ptr().pmt_pid

@property
def pcr_pid(self):
"""The PID carrying this program's clock reference in MPEG-TS.

:type: int
"""
return self._ptr().pcr_pid

@property
def metadata(self):
"""Metadata for this program, such as ``service_name`` and
``service_provider`` in MPEG-TS.

:type: dict[str, str]
"""
return avdict_to_dict(self._ptr().metadata)

@property
def streams(self):
"""The streams that belong to this program.

A stream can belong to more than one program.

:type: tuple[Stream, ...]
"""
ptr: cython.pointer[lib.AVProgram] = self._ptr()
by_index = {stream.index: stream for stream in self.container.streams}
result = []
i: cython.uint
for i in range(ptr.nb_stream_indexes):
stream = by_index.get(ptr.stream_index[i])
if stream is not None:
result.append(stream)
return tuple(result)

@property
def discard(self):
"""Controls whether the demuxer reads this program's packets.

Set to :attr:`.Discard.all` to skip a program entirely. In MPEG-TS, a
stream's packets are only dropped once every program it belongs to is
discarded.

:type: Discard
"""
return Discard(self._ptr().discard)

@discard.setter
def discard(self, value):
self._ptr().discard = Discard(value).value
24 changes: 24 additions & 0 deletions av/container/program.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from typing import final

from av.stream import Discard, Stream

@final
class Program:
@property
def index(self) -> int: ...
@property
def id(self) -> int: ...
@property
def program_num(self) -> int: ...
@property
def pmt_pid(self) -> int: ...
@property
def pcr_pid(self) -> int: ...
@property
def metadata(self) -> dict[str, str]: ...
@property
def streams(self) -> tuple[Stream, ...]: ...
@property
def discard(self) -> Discard: ...
@discard.setter
def discard(self, value: Discard | int) -> None: ...
11 changes: 11 additions & 0 deletions docs/api/container.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ Input Containers
:members:


Programs
--------

.. currentmodule:: av.container.program

.. autoclass:: Program
:members:

.. currentmodule:: av.container


Output Containers
-----------------

Expand Down
12 changes: 12 additions & 0 deletions include/avformat.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ cdef extern from "libavformat/avformat.h" nogil:
AVRational r_frame_rate
AVRational sample_aspect_ratio

cdef struct AVProgram:
int id
AVDiscard discard
unsigned int *stream_index
unsigned int nb_stream_indexes
AVDictionary *metadata
int program_num
int pmt_pid
int pcr_pid

cdef struct AVChapter:
int64_t id
int64_t start
Expand Down Expand Up @@ -146,6 +156,8 @@ cdef extern from "libavformat/avformat.h" nogil:
const AVClass *av_class
unsigned int nb_streams
AVStream **streams
unsigned int nb_programs
AVProgram **programs
unsigned int nb_chapters
AVChapter **chapters
const AVInputFormat *iformat
Expand Down
66 changes: 66 additions & 0 deletions tests/test_programs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from collections import Counter

import pytest

import av
from av.container.program import Program
from av.stream import Discard

from .common import fate_suite


def test_programs() -> None:
with av.open(fate_suite("mpegts/loewe.ts")) as container:
programs = container.programs
assert len(programs) == 1

program = programs[0]
assert isinstance(program, Program)
assert program.index == 0
assert program.id == 769
assert program.program_num == 769
assert program.pmt_pid == 1363
assert program.pcr_pid == 1361
assert program.metadata == {
"service_name": "Das Erste HD",
"service_provider": "LOEWE. DR+",
}
assert program.streams == (container.streams[0], container.streams[1])
assert program.discard == Discard.none
assert repr(program).startswith("<av.Program #0 id=769, 2 streams at ")


def test_programs_discard() -> None:
path = fate_suite("mpegts/loewe.ts")
with av.open(path) as container:
counts = Counter(p.stream_index for p in container.demux() if p.size)
assert set(counts) == {0, 1, 2}

with av.open(path) as container:
program = container.programs[0]
program.discard = Discard.all
assert program.discard == Discard.all
counts = Counter(p.stream_index for p in container.demux() if p.size)

# Stream 2 is not part of the program, so it is still read.
assert set(counts) == {2}


def test_programs_none() -> None:
with av.open(fate_suite("h264/interlaced_crop.mp4")) as container:
assert container.programs == ()


def test_program_after_close() -> None:
container = av.open(fate_suite("mpegts/loewe.ts"))
program = container.programs[0]
container.close()

assert "container closed" in repr(program)
with pytest.raises(AssertionError):
program.id


def test_program_cannot_be_instantiated() -> None:
with pytest.raises(RuntimeError):
Program(None) # type: ignore[call-arg]
Loading