Skip to content
Merged

Patch #2495

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
4 changes: 3 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ jobs:
CIBW_BEFORE_BUILD: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-latest.json /tmp/vendor
CIBW_BEFORE_BUILD_MACOS: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-latest.json /tmp/vendor
CIBW_BEFORE_BUILD_WINDOWS: python scripts\fetch-vendor.py --config-file scripts\ffmpeg-latest.json C:\cibw\vendor
CIBW_ENVIRONMENT_LINUX: LD_LIBRARY_PATH=/tmp/vendor/lib:$LD_LIBRARY_PATH PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig
# Linux is the only platform that embeds debug info in the extensions;
# macOS and Windows keep it out of the shipped binaries.
CIBW_ENVIRONMENT_LINUX: LD_LIBRARY_PATH=/tmp/vendor/lib:$LD_LIBRARY_PATH PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig LDFLAGS=-Wl,--strip-debug
CIBW_ENVIRONMENT_MACOS: PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig LDFLAGS=-headerpad_max_install_names
CIBW_ENVIRONMENT_WINDOWS: INCLUDE=C:\\cibw\\vendor\\include LIB=C:\\cibw\\vendor\\lib PYAV_SKIP_TESTS=unicode_filename
CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: delvewheel repair --add-path C:\cibw\vendor\bin -w {dest_dir} {wheel}
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Fixes:
- Type stubs now pass ``mypy --strict`` and have been corrected throughout to match runtime behavior, including attributes on the wrong class, values that may be ``None``, missing return types, and mismatched parameter names.
- ``FilterContext.link_to`` raises ``ValueError`` when linking filters from different graphs instead of aborting the process.
- Filters added or linked after ``Graph.configure()`` are now configured on the next push or pull instead of segfaulting.
- ``SubtitleCodecContext.decode(None)`` flushes the decoder instead of raising ``RuntimeError``, so delayed decoders such as EIA-608 closed captions return their final subtitle.

v19.0.2
-------
Expand Down
5 changes: 4 additions & 1 deletion av/audio/codeccontext.pyi
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from collections.abc import Iterator
from typing import Any, Literal, final
from typing import Any, Literal, final, override

from av.codec.context import CodecContext
from av.packet import Packet
Expand Down Expand Up @@ -27,8 +27,11 @@ class AudioCodecContext(CodecContext):
def channels(self) -> int: ...
@property
def block_align(self) -> int: ...
@override
def encode(self, frame: AudioFrame | None = None) -> list[Packet[Stream]]: ... # type: ignore[override]
@override
def encode_lazy( # type: ignore[override]
self, frame: AudioFrame | None = None
) -> Iterator[Packet[Stream]]: ...
@override
def decode(self, packet: Packet[Any] | None = None) -> list[AudioFrame]: ...
9 changes: 8 additions & 1 deletion av/codec/context.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ from av.packet import Packet, PktSideDataT
from av.rational import AVRational
from av.sidedata.sidedata import Type as FrameSideDataType
from av.stream import Stream
from av.subtitles import _SubtitleCodecName
from av.subtitles import _SubtitleCodecName, _SubtitleDecoderName
from av.subtitles.codeccontext import SubtitleCodecContext
from av.subtitles.subtitle import AssSubtitle, BitmapSubtitle
from av.video import _VideoCodecName
Expand Down Expand Up @@ -222,6 +222,13 @@ class CodecContext:
) -> SubtitleCodecContext: ...
@overload
@staticmethod
def create(
codec: _SubtitleDecoderName,
mode: Literal["r"] | None = None,
hwaccel: HWAccel | None = None,
) -> SubtitleCodecContext: ...
@overload
@staticmethod
def create(
codec: str | Codec,
mode: Literal["r", "w"] | None = None,
Expand Down
35 changes: 35 additions & 0 deletions av/subtitles/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,38 @@ type _SubtitleCodecName = Literal[
"webvtt",
"xsub",
]

# FFmpeg 9.0 decoders and the codec descriptor aliases that resolve to them.
type _SubtitleDecoderName = Literal[
"arib_caption",
"ass",
"cc_dec",
"dvb_subtitle",
"dvb_teletext",
"dvbsub",
"dvd_subtitle",
"dvdsub",
"eia_608",
"hdmv_pgs_subtitle",
"jacosub",
"libaribb24",
"libaribcaption",
"libzvbi_teletextdec",
"microdvd",
"mov_text",
"mpl2",
"pgssub",
"pjs",
"realtext",
"sami",
"srt",
"ssa",
"stl",
"subrip",
"subviewer",
"subviewer1",
"text",
"vplayer",
"webvtt",
"xsub",
]
1 change: 1 addition & 0 deletions av/subtitles/codeccontext.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ from av.packet cimport Packet
cdef class SubtitleCodecContext(CodecContext):
cdef bint subtitle_header_set
cdef _decode(self, Packet packet)
cdef _decode_one(self, Packet packet)
cpdef decode2(self, Packet packet)
22 changes: 17 additions & 5 deletions av/subtitles/codeccontext.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,22 @@ def _decode(self, packet: Packet | None):
if not self.ptr.codec:
raise ValueError("cannot decode unknown codec")

if packet is None:
raise RuntimeError("packet cannot be None")

self.open(strict=False)

if packet is not None:
subtitle: SubtitleSet | None = self._decode_one(packet)
return list(subtitle) if subtitle is not None else []

# Flush: send empty packets until the decoder stops returning subtitles,
# as avcodec_decode_subtitle2() documents.
flush_packet: Packet = Packet()
subtitles: list = []
while (subtitle := self._decode_one(flush_packet)) is not None:
subtitles.extend(subtitle)
return subtitles

@cython.cfunc
def _decode_one(self, packet: Packet):
proxy: SubtitleProxy = SubtitleProxy()
got_frame: cython.int = 0

Expand All @@ -116,8 +128,8 @@ def _decode(self, packet: Packet | None):
)

if got_frame:
return list(SubtitleSet(proxy))
return []
return SubtitleSet(proxy)
return None

@cython.ccall
def decode2(self, packet: Packet):
Expand Down
7 changes: 4 additions & 3 deletions av/subtitles/codeccontext.pyi
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Literal, final
from typing import Any, Literal, final, override

from av.codec.context import CodecContext
from av.packet import Packet
Expand All @@ -9,8 +9,9 @@ from av.subtitles.subtitle import AssSubtitle, BitmapSubtitle, SubtitleSet
class SubtitleCodecContext(CodecContext):
type: Literal["subtitle"]
subtitle_header: bytes | None
def decode( # type: ignore[override]
self, packet: Packet[Any]
@override
def decode(
self, packet: Packet[Any] | None = None
) -> list[AssSubtitle] | list[BitmapSubtitle]: ...
def decode2(self, packet: Packet[Any]) -> SubtitleSet | None: ...
def encode_subtitle(self, subtitle: SubtitleSet) -> Packet[Stream]: ...
5 changes: 4 additions & 1 deletion av/video/codeccontext.pyi
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from collections.abc import Iterator
from fractions import Fraction
from typing import Any, Literal, final
from typing import Any, Literal, final, override

from av.codec.context import CodecContext
from av.packet import Packet
Expand Down Expand Up @@ -54,8 +54,11 @@ class VideoCodecContext(CodecContext):
qmax: int
type: Literal["video"]

@override
def encode(self, frame: VideoFrame | None = None) -> list[Packet[Stream]]: ... # type: ignore[override]
@override
def encode_lazy( # type: ignore[override]
self, frame: VideoFrame | None = None
) -> Iterator[Packet[Stream]]: ...
@override
def decode(self, packet: Packet[Any] | None = None) -> list[VideoFrame]: ...
4 changes: 3 additions & 1 deletion scripts/build-armv7l-cross.sh
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,14 @@ build_one() {
# The _PYTHON_* vars make sysconfig report armv7l; zig does the compiling.
# max-page-size=4096 aligns our extensions to the armv7l (4K) page size;
# lld's default 64K alignment uses 4K file offsets, which the loader rejects.
# zig cc emits debug info by default and links its own debug-built runtime;
# --strip-debug drops it, which otherwise makes this the largest wheel.
# Setting CFLAGS replaces the target's default OPT flags, so -O2 -DNDEBUG
# must be passed explicitly (without NDEBUG the free-threaded build trips a
# Py_SET_REFCNT assertion that release wheels compile out).
env \
CC="$BIN_DIR/zig-cc" CXX="$BIN_DIR/zig-cxx" AR="$BIN_DIR/zig-ar" \
LDSHARED="$BIN_DIR/zig-cc -shared -Wl,-z,max-page-size=4096" \
LDSHARED="$BIN_DIR/zig-cc -shared -Wl,-z,max-page-size=4096 -Wl,--strip-debug" \
_PYTHON_HOST_PLATFORM=linux-armv7l \
_PYTHON_SYSCONFIGDATA_NAME="$(basename "${scd[0]}" .py)" \
PYTHONPATH="$(dirname "${scd[0]}")" \
Expand Down
26 changes: 26 additions & 0 deletions tests/test_subtitles.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import io
from fractions import Fraction
from typing import cast

import av
Expand Down Expand Up @@ -76,6 +77,31 @@ def test_subtitle_flush(self) -> None:

assert len(subs) == 3

def test_codec_context_flush_returns_delayed_subtitles(self) -> None:
def cc_pair(b1: int, b2: int) -> bytes:
# EIA-608 bytes carry odd parity in the high bit.
def odd(b: int) -> int:
return b if b.bit_count() % 2 else b | 0x80

return bytes([0xFC, odd(b1), odd(b2)])

ctx = CodecContext.create("eia_608", "r")
ctx.pkt_timebase = Fraction(1, 1000)

# A pop-on caption: resume caption loading, "HI", end of caption.
subs: list[AssSubtitle | BitmapSubtitle] = []
for i, pair in enumerate([(0x14, 0x20), (ord("H"), ord("I")), (0x14, 0x2F)]):
packet = av.Packet(cc_pair(*pair))
packet.pts = i * 33
subs.extend(ctx.decode(packet))
assert subs == []

# cc_dec holds the displayed caption until flushed.
flushed = ctx.decode(None)
assert len(flushed) == 1
assert isinstance(flushed[0], AssSubtitle)
assert flushed[0].dialogue.endswith(b"HI")

def test_subtitle_header_read(self) -> None:
"""Test reading subtitle_header from a decoded subtitle stream."""
path = fate_suite("sub/MovText_capability_tester.mp4")
Expand Down
Loading