Skip to content

Commit f085181

Browse files
committed
Return AVRational instead of fractions.Fraction
Delete `avrational_to_fraction()`, the last thing in PyAV producing a Fraction, and route its call sites through from_avrational. Rational attributes now hand back an AVRational, never None. An unset value is the falsy `AVRational(0, 1)` Settable attributes are stubbed as property/setter pairs so a `fractions.Fraction` is still accepted on assignment, as documented, while reads are typed AVRational. The reflected arithmetic dunders gain overloads, without which `ctx.framerate = 1 / ctx.time_base` fails to type check on the float arm of their return union.
1 parent ae4836a commit f085181

28 files changed

Lines changed: 143 additions & 102 deletions

CHANGELOG.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,18 @@ Major:
3535

3636
- Drop support for Python 3.11. Binary wheels are now built for Python 3.12 and later.
3737
- Remove the undocumented ``CodecContext.hwaccel`` attribute. It held the ``HWAccel`` settings object passed in, not the live device context; use ``CodecContext.is_hwaccel`` to check whether hardware acceleration is in use.
38+
- Rational attributes (``time_base``, ``average_rate``, ``base_rate``, ``guessed_rate``, ``framerate``, ``rate``, ``sample_aspect_ratio``, and ``display_aspect_ratio``) now return :class:`av.AVRational` rather than ``fractions.Fraction``, and are never ``None``: an unset value is the falsy ``AVRational(0, 1)``. Test them with ``if not stream.time_base:`` instead of ``is None``. Setters still accept a ``fractions.Fraction``.
39+
- Remove ``Capabilities.hwaccel``, ``Capabilities.hwaccel_vdpau``, and ``Capabilities.neg_linesizes``, none of which FFmpeg defines any more.
40+
41+
Features:
42+
43+
- ``av.dump_codecs()`` now lists every codec FFmpeg knows of rather than only those with an encoder or a decoder, so data and attachment codecs appear, matching ``ffmpeg -codecs``. Its legend gains the ``..D...`` and ``..T...`` media types.
44+
- ``ContainerFormat.fixed_framesize`` reports whether a format wants fixed size audio frames.
45+
- Enums gained the members FFmpeg has since added: ``Properties.FIELDS``, ``Properties.ENHANCEMENT``, ``PixFmtLoss.EXCESS_RESOLUTION``, ``PixFmtLoss.EXCESS_DEPTH``, ``Flags2.icc_profiles``, ``format.Flags.experimental``, ``Interpolation.STRICT``, ``Interpolation.UNSTABLE``, ``ColorTrc.V_LOG``, ``ColorPrimaries.V_GAMUT``, and the ``LCEVC``, ``VIEW_ID``, ``THREE_D_REFERENCE_DISPLAYS``, and ``EXIF`` members of ``sidedata.Type``.
3846

3947
Fixes:
4048

49+
- ``av.dump_codecs()`` no longer drops the canonical names ``h264``, ``hevc``, ``av1``, ``dirac``, and ``ilbc``, each of which was overwritten by the row of whichever encoder it resolved to.
4150
- Frames returned by flushing a codec context directly (``CodecContext.decode()`` with no packet) now carry the stream's ``time_base`` instead of ``None``.
4251
- ``VideoFrame.reformat()`` (and so ``to_ndarray(format=...)``, ``to_rgb()``, ``to_image()``) now shares one ``SwsContext`` per thread instead of allocating one per frame. FFmpeg 8's swscale retains megabytes of graph state per context, which showed up as large RSS growth when many frames were alive at once.
4352

av/codec/codec.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from cython.cimports.av.audio.format import get_audio_format
66
from cython.cimports.av.codec.hwaccel import HWConfig, wrap_hwconfig
77
from cython.cimports.av.rational import from_avrational
8-
from cython.cimports.av.utils import avrational_to_fraction
98
from cython.cimports.av.video.format import VideoFormat, get_pix_fmt, get_video_format
109
from cython.cimports.libc.stdlib import free, malloc
1110

av/codec/context.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
from cython.cimports.av.dictionary import Dictionary
99
from cython.cimports.av.error import err_check
1010
from cython.cimports.av.packet import Packet
11-
from cython.cimports.av.utils import avrational_to_fraction, to_avrational
11+
from cython.cimports.av.rational import from_avrational
12+
from cython.cimports.av.utils import to_avrational
1213
from cython.cimports.libc.errno import EAGAIN
1314
from cython.cimports.libc.stdint import uint8_t
1415
from cython.cimports.libc.string import memcpy, strcmp
@@ -857,7 +858,7 @@ def level(self, value: cython.int):
857858
def time_base(self):
858859
if self.is_decoder:
859860
raise RuntimeError("Cannot access 'time_base' as a decoder")
860-
return avrational_to_fraction(cython.address(self.ptr.time_base))
861+
return from_avrational(self.ptr.time_base)
861862

862863
@time_base.setter
863864
def time_base(self, value):

av/codec/context.pyi

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ from typing import ClassVar, Literal, cast, overload
66
from av.audio import _AudioCodecName
77
from av.audio.codeccontext import AudioCodecContext
88
from av.packet import Packet
9+
from av.rational import AVRational
910
from av.subtitles import _SubtitleCodecName
1011
from av.subtitles.codeccontext import SubtitleCodecContext
1112
from av.video import _VideoCodecName
@@ -125,7 +126,10 @@ class CodecContext:
125126
@property
126127
def profiles(self) -> list[str]: ...
127128
extradata: bytes | None
128-
time_base: Fraction
129+
@property
130+
def time_base(self) -> AVRational: ...
131+
@time_base.setter
132+
def time_base(self, value: AVRational | Fraction | int) -> None: ...
129133
codec_tag: str
130134
global_quality: int
131135
bit_rate: int | None

av/container/core.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
from cython.cimports.av.container.pyio import pyio_close_custom_gil, pyio_close_gil
1212
from cython.cimports.av.error import err_check, stash_exception
1313
from cython.cimports.av.format import build_container_format
14+
from cython.cimports.av.rational import from_avrational
1415
from cython.cimports.av.utils import (
1516
avdict_to_dict,
16-
avrational_to_fraction,
1717
dict_to_avdict,
1818
to_avrational,
1919
)
@@ -433,7 +433,7 @@ def chapters(self):
433433
"id": ch.id,
434434
"start": ch.start,
435435
"end": ch.end,
436-
"time_base": avrational_to_fraction(cython.address(ch.time_base)),
436+
"time_base": from_avrational(ch.time_base),
437437
"metadata": avdict_to_dict(
438438
ch.metadata, self.metadata_encoding, self.metadata_errors
439439
),

av/container/core.pyi

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ from typing import Any, ClassVar, Literal, Self, TypedDict, cast, overload
77

88
from av.codec.hwaccel import HWAccel
99
from av.format import ContainerFormat
10+
from av.rational import AVRational
1011

1112
from .input import InputContainer
1213
from .output import OutputContainer
1314
from .streams import StreamContainer
1415

15-
Real = int | float | Fraction
16+
Real = int | float | Fraction | AVRational
1617

1718
class Flags(Flag):
1819
gen_pts = cast(ClassVar[Flags], ...)
@@ -72,7 +73,7 @@ class Chapter(TypedDict):
7273
id: int
7374
start: int
7475
end: int
75-
time_base: Fraction | None
76+
time_base: AVRational
7677
metadata: dict[str, str]
7778

7879
class Container:

av/container/output.pyi

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ from av.audio import _AudioCodecName
66
from av.audio.stream import AudioStream
77
from av.codec.hwaccel import HWAccel
88
from av.packet import Packet
9+
from av.rational import AVRational
910
from av.stream import AttachmentStream, DataStream, Stream
1011
from av.subtitles import _SubtitleCodecName
1112
from av.subtitles.stream import SubtitleStream
@@ -29,7 +30,7 @@ class OutputContainer(Container):
2930
def add_stream(
3031
self,
3132
codec_name: _VideoCodecName,
32-
rate: Fraction | int | None = None,
33+
rate: AVRational | Fraction | int | None = None,
3334
options: dict[str, str] | None = None,
3435
hwaccel: HWAccel | None = None,
3536
**kwargs,
@@ -38,23 +39,23 @@ class OutputContainer(Container):
3839
def add_stream(
3940
self,
4041
codec_name: _SubtitleCodecName,
41-
rate: Fraction | int | None = None,
42+
rate: AVRational | Fraction | int | None = None,
4243
options: dict[str, str] | None = None,
4344
**kwargs,
4445
) -> SubtitleStream: ...
4546
@overload
4647
def add_stream(
4748
self,
4849
codec_name: str,
49-
rate: Fraction | int | None = None,
50+
rate: AVRational | Fraction | int | None = None,
5051
options: dict[str, str] | None = None,
5152
hwaccel: HWAccel | None = None,
5253
**kwargs,
5354
) -> VideoStream | AudioStream | SubtitleStream: ...
5455
def add_mux_stream(
5556
self,
5657
codec_name: str,
57-
rate: Fraction | int | None = None,
58+
rate: AVRational | Fraction | int | None = None,
5859
**kwargs,
5960
) -> Stream: ...
6061
def add_stream_from_template(

av/filter/context.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from cython.cimports.av.error import err_check
66
from cython.cimports.av.filter.link import alloc_filter_pads
77
from cython.cimports.av.frame import Frame
8-
from cython.cimports.av.utils import avrational_to_fraction
8+
from cython.cimports.av.rational import from_avrational
99
from cython.cimports.av.video.frame import alloc_video_frame
1010

1111
_cinit_sentinel = cython.declare(object, object())
@@ -162,9 +162,7 @@ def pull(self):
162162
err_check(res)
163163

164164
frame._init_user_attributes()
165-
frame.time_base = avrational_to_fraction(
166-
cython.address(self.ptr.inputs[0].time_base)
167-
)
165+
frame.time_base = from_avrational(self.ptr.inputs[0].time_base)
168166
return frame
169167

170168
def process_command(

av/filter/graph.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import warnings
2-
from fractions import Fraction
32

43
import cython
54
from cython.cimports.av.audio.format import AudioFormat
@@ -8,6 +7,7 @@
87
from cython.cimports.av.error import err_check
98
from cython.cimports.av.filter.context import FilterContext, wrap_filter_context
109
from cython.cimports.av.filter.filter import Filter, wrap_filter
10+
from cython.cimports.av.rational import AVRational
1111
from cython.cimports.av.video.format import VideoFormat
1212
from cython.cimports.av.video.frame import VideoFrame
1313

@@ -160,7 +160,7 @@ def add_buffer(
160160
"This is deprecated and may be removed in future releases.",
161161
DeprecationWarning,
162162
)
163-
time_base = Fraction(1, 1000)
163+
time_base = AVRational(1, 1000)
164164

165165
return self.add(
166166
"buffer",
@@ -204,7 +204,7 @@ def add_abuffer(
204204
if layout is None and channels is None:
205205
raise ValueError("missing layout or channels")
206206
if time_base is None:
207-
time_base = Fraction(1, sample_rate)
207+
time_base = AVRational(1, sample_rate)
208208

209209
kwargs = {
210210
"sample_rate": f"{sample_rate}",

av/filter/graph.pyi

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ from av.audio.format import AudioFormat
55
from av.audio.frame import AudioFrame
66
from av.audio.layout import AudioLayout
77
from av.audio.stream import AudioStream
8+
from av.rational import AVRational
89
from av.video.format import VideoFormat
910
from av.video.frame import VideoFrame
1011
from av.video.stream import VideoStream
@@ -29,7 +30,7 @@ class Graph:
2930
height: int | None = None,
3031
format: VideoFormat | None = None,
3132
name: str | None = None,
32-
time_base: Fraction | None = None,
33+
time_base: AVRational | Fraction | None = None,
3334
) -> FilterContext: ...
3435
def add_abuffer(
3536
self,
@@ -39,7 +40,7 @@ class Graph:
3940
layout: AudioLayout | str | None = None,
4041
channels: int | None = None,
4142
name: str | None = None,
42-
time_base: Fraction | None = None,
43+
time_base: AVRational | Fraction | None = None,
4344
) -> FilterContext: ...
4445
def set_audio_frame_size(self, frame_size: int) -> None: ...
4546
def push(self, frame: None | AudioFrame | VideoFrame, at: int = -1) -> None: ...

0 commit comments

Comments
 (0)