diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4e9901275..a862fcc9a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -34,6 +34,8 @@ v19.1.0 (Unreleased) Features: - Add ``VideoFrame.solid()`` to create a frame filled with one color, in any non-paletted pixel format. +- Add ``Stream.cctx`` as the name for a stream's codec context; ``Stream.codec_context`` remains as an alias. +- Deprecate ``AudioStream.format``, ``AudioStream.layout``, and ``VideoStream.format``, which pass through to the codec context; use ``stream.cctx.format`` and ``stream.cctx.layout`` instead. Passing ``format`` or ``layout`` to ``add_stream()`` is not deprecated. Fixes: diff --git a/av/audio/stream.py b/av/audio/stream.py index 9256db4ea..46b582037 100644 --- a/av/audio/stream.py +++ b/av/audio/stream.py @@ -1,9 +1,20 @@ +import warnings + import cython from cython.cimports import libav as lib from cython.cimports.av.audio.frame import AudioFrame from cython.cimports.av.packet import Packet +def _warn_passthrough(name): + warnings.warn( + f"AudioStream.{name} is deprecated; use AudioStream.cctx.{name} instead", + DeprecationWarning, + # Cython functions have no frames, so 1 is the caller. + stacklevel=1, + ) + + @cython.final @cython.cclass class AudioStream(Stream): @@ -13,20 +24,67 @@ def __repr__(self): f"" ) - if self.codec_context is None: + if self.cctx is None: return f" at 0x{id(self):x}>" - form = self.format.name if self.format else None + cc = self.cctx + form = cc.format.name if cc.format else None return ( f"" + f" {cc.layout.name}, {form} at 0x{id(self):x}>" ) def __getattr__(self, name): - if self.codec_context is None: + if self.cctx is None: raise AttributeError( f"'{type(self).__name__}' object has no attribute '{name}'" ) - return getattr(self.codec_context, name) + return getattr(self.cctx, name) + + @property + def format(self): + """ + The audio sample format of the codec context. + + .. deprecated:: 19.1 + Use ``stream.cctx.format`` instead. + + :type: AudioFormat | None + """ + _warn_passthrough("format") + if self.cctx is None: + raise AttributeError( + f"'{type(self).__name__}' object has no attribute 'format'" + ) + return self.cctx.format + + @format.setter + def format(self, value): + _warn_passthrough("format") + if self.cctx is not None: + self.cctx.format = value + + @property + def layout(self): + """ + The audio channel layout of the codec context. + + .. deprecated:: 19.1 + Use ``stream.cctx.layout`` instead. + + :type: AudioLayout + """ + _warn_passthrough("layout") + if self.cctx is None: + raise AttributeError( + f"'{type(self).__name__}' object has no attribute 'layout'" + ) + return self.cctx.layout + + @layout.setter + def layout(self, value): + _warn_passthrough("layout") + if self.cctx is not None: + self.cctx.layout = value @cython.ccall def encode(self, frame: AudioFrame | None = None): @@ -38,7 +96,7 @@ def encode(self, frame: AudioFrame | None = None): .. seealso:: This is mostly a passthrough to :meth:`.CodecContext.encode`. """ self._assert_has_codec_context(lib.AVERROR_ENCODER_NOT_FOUND) - packets = self.codec_context.encode(frame) + packets = self.cctx.encode(frame) packet: Packet for packet in packets: packet._stream = self @@ -54,7 +112,7 @@ def encode_lazy(self, frame: AudioFrame | None = None): """ self._assert_has_codec_context(lib.AVERROR_ENCODER_NOT_FOUND) packet: Packet - for packet in self.codec_context.encode_lazy(frame): + for packet in self.cctx.encode_lazy(frame): packet._stream = self packet.ptr.stream_index = self.ptr.index yield packet @@ -69,4 +127,4 @@ def decode(self, packet: Packet | None = None): .. seealso:: This is a passthrough to :meth:`.CodecContext.decode`. """ self._assert_has_codec_context() - return self.codec_context.decode(packet) + return self.cctx.decode(packet) diff --git a/av/audio/stream.pyi b/av/audio/stream.pyi index 346c8a84a..b2ae7cfa3 100644 --- a/av/audio/stream.pyi +++ b/av/audio/stream.pyi @@ -1,6 +1,8 @@ from collections.abc import Iterator from typing import Any, Literal, final +from typing_extensions import deprecated + from av.codec.codec import Codec from av.packet import Packet from av.stream import Stream @@ -12,6 +14,7 @@ from .layout import AudioLayout @final class AudioStream(Stream): + cctx: AudioCodecContext codec_context: AudioCodecContext def encode(self, frame: AudioFrame | None = None) -> list[Packet[AudioStream]]: ... def encode_lazy( @@ -32,10 +35,14 @@ class AudioStream(Stream): channels: int type: Literal["audio"] @property + @deprecated("Use AudioStream.cctx.format instead") def format(self) -> AudioFormat | None: ... @format.setter + @deprecated("Use AudioStream.cctx.format instead") def format(self, value: AudioFormat | str) -> None: ... @property + @deprecated("Use AudioStream.cctx.layout instead") def layout(self) -> AudioLayout: ... @layout.setter + @deprecated("Use AudioStream.cctx.layout instead") def layout(self, value: AudioLayout | str) -> None: ... diff --git a/av/bitstream.py b/av/bitstream.py index c5dbc43d0..9266f348a 100644 --- a/av/bitstream.py +++ b/av/bitstream.py @@ -63,9 +63,9 @@ def __cinit__( err_check(res) # codecpar carries everything the muxer needs; a mux-only stream # (add_mux_stream) has no context to keep in sync. - if out_stream.codec_context is not None: + if out_stream.cctx is not None: lib.avcodec_parameters_to_context( - out_stream.codec_context.ptr, out_stream.ptr.codecpar + out_stream.cctx.ptr, out_stream.ptr.codecpar ) def __dealloc__(self): diff --git a/av/container/input.py b/av/container/input.py index c0d8deca3..d654095a3 100644 --- a/av/container/input.py +++ b/av/container/input.py @@ -336,6 +336,6 @@ def flush_buffers(self) -> cython.void: codec_context: CodecContext for stream in self.streams: - codec_context = stream.codec_context + codec_context = stream.cctx if codec_context: codec_context.flush_buffers() diff --git a/av/container/output.py b/av/container/output.py index befcbc987..46c1a2d5b 100644 --- a/av/container/output.py +++ b/av/container/output.py @@ -36,6 +36,17 @@ def _set_codecpar_extradata( stream.codecpar.extradata_size = size +@cython.cfunc +def _set_stream_attrs(stream: Stream, ctx: CodecContext, kwargs: dict) -> cython.void: + # format and layout go straight to the codec context, since the stream + # passthroughs for them are deprecated. + for k, v in kwargs.items(): + if k == "format" or k == "layout": + setattr(ctx, k, v) + else: + setattr(stream, k, v) + + @cython.cfunc def arm_write_timeout(self: OutputContainer) -> cython.void: """Start the write deadline, unless the output is a seekable file. @@ -257,8 +268,7 @@ def add_stream( if options: py_stream.options.update(options) - for k, v in kwargs.items(): - setattr(py_stream, k, v) + _set_stream_attrs(py_stream, py_codec_context, kwargs) return py_stream @@ -366,14 +376,14 @@ def add_stream_from_template( if opaque is None: opaque = template.type != "video" - if template.codec_context is None: + if template.cctx is None: return self._add_stream_without_codec_from_template(template, **kwargs) codec_obj: Codec if opaque: # Copy ctx from template. - codec_obj = template.codec_context.codec + codec_obj = template.cctx.codec else: # Construct new codec object. - codec_obj = Codec(template.codec_context.codec.name, "w") + codec_obj = Codec(template.cctx.codec.name, "w") codec: cython.pointer[cython.const[lib.AVCodec]] = codec_obj.ptr @@ -410,7 +420,7 @@ def add_stream_from_template( # Copy flags If we're creating a new codec object. This fixes some # muxing issues. Overwriting the flag set just above is intentional. if not opaque: - ctx.flags = template.codec_context.flags + ctx.flags = template.cctx.flags # Initialize stream codec parameters to populate the codec type. # Subsequent changes to the codec context will be applied just @@ -427,8 +437,7 @@ def add_stream_from_template( py_stream: Stream = wrap_stream(self, stream, py_codec_context) self.streams.add_stream(py_stream) - for k, v in kwargs.items(): - setattr(py_stream, k, v) + _set_stream_attrs(py_stream, py_codec_context, kwargs) return py_stream @@ -602,7 +611,7 @@ def start_encoding(self): # Finalize and open all streams. for stream in self.streams: - ctx = stream.codec_context + ctx = stream.cctx if ctx is not None and not ctx.is_open: for k, v in self.options.items(): ctx.options.setdefault(k, v) @@ -798,7 +807,7 @@ def _buffer_for_extradata(self, packet: Packet): stream: Stream for stream in self.streams: if ( - stream.codec_context is not None + stream.cctx is not None or stream.ptr.codecpar.extradata != cython.NULL ): continue diff --git a/av/filter/graph.py b/av/filter/graph.py index edeb1b4b1..76f140e53 100644 --- a/av/filter/graph.py +++ b/av/filter/graph.py @@ -13,6 +13,13 @@ from cython.cimports.av.video.frame import VideoFrame +@cython.cfunc +def _template_ctx(template): + # A stream's format and layout passthroughs are deprecated, so read them + # from its codec context. Frames and codec contexts are used directly. + return getattr(template, "cctx", template) + + @cython.final @cython.cclass class Graph: @@ -174,7 +181,7 @@ def add_buffer( if height is None: height = template.height if format is None: - format = template.format + format = _template_ctx(template).format if time_base is None: time_base = template.time_base @@ -219,9 +226,9 @@ def add_abuffer( if sample_rate is None: sample_rate = template.sample_rate if format is None: - format = template.format + format = _template_ctx(template).format if layout is None: - layout = template.layout.name + layout = _template_ctx(template).layout.name if channels is None: channels = template.channels if time_base is None: diff --git a/av/stream.pxd b/av/stream.pxd index 6d571a9f2..4f4e595bd 100644 --- a/av/stream.pxd +++ b/av/stream.pxd @@ -10,7 +10,7 @@ cdef class Stream: cdef lib.AVStream *ptr cdef readonly Container container cdef readonly dict metadata - cdef readonly CodecContext codec_context + cdef readonly CodecContext cctx cdef void _init(self, Container, lib.AVStream*, CodecContext) cdef bint _is_open(self) cdef void _assert_open(self) diff --git a/av/stream.py b/av/stream.py index 684797e83..2c324c2a5 100644 --- a/av/stream.py +++ b/av/stream.py @@ -96,7 +96,7 @@ class Stream: >>> stream - This encapsulates a :class:`.CodecContext`, located at :attr:`Stream.codec_context`. + This encapsulates a :class:`.CodecContext`, located at :attr:`Stream.cctx`. Attribute access is passed through to that context when attributes are missing on the stream itself. E.g. ``stream.options`` will be the options on the context. @@ -117,7 +117,7 @@ def _init( self.container = container self.ptr = stream - self.codec_context = codec_context + self.cctx = codec_context self.metadata = avdict_to_dict(stream.metadata) @@ -134,7 +134,7 @@ def _assert_open(self) -> cython.void: def _assert_has_codec_context( self, err: cython.int = lib.AVERROR_DECODER_NOT_FOUND ) -> cython.void: - if self.codec_context is None: + if self.cctx is None: err_check(err) def __repr__(self): @@ -164,27 +164,28 @@ def __setattr__(self, name, value): if name == "time_base": to_avrational(value, cython.address(self.ptr.time_base)) return + if name == "format" or name == "layout": + prop = getattr(type(self), name, None) + if prop is not None: + prop.__set__(self, value) + return # Convenience setter for codec context properties. - if self.codec_context is not None: - setattr(self.codec_context, name, value) + if self.cctx is not None: + setattr(self.cctx, name, value) @cython.cfunc def _finalize_for_output(self) -> cython.void: dict_to_avdict(cython.address(self.ptr.metadata), self.metadata) - if self.codec_context is None: + if self.cctx is None: return if not self.ptr.time_base.num: - self.ptr.time_base = self.codec_context.ptr.time_base + self.ptr.time_base = self.cctx.ptr.time_base # It prefers if we pass it parameters via this other object. Let's just copy what we want. - err_check( - lib.avcodec_parameters_from_context( - self.ptr.codecpar, self.codec_context.ptr - ) - ) + err_check(lib.avcodec_parameters_from_context(self.ptr.codecpar, self.cctx.ptr)) @property def index_entries(self): @@ -208,6 +209,15 @@ def _set_id(self, value) -> cython.void: else: self.ptr.id = value + @property + def codec_context(self): + """ + Alias of :attr:`cctx`. + + :type: CodecContext | None + """ + return self.cctx + @property def profiles(self): """ @@ -215,8 +225,8 @@ def profiles(self): :type: list[str] """ - if self.codec_context: - return self.codec_context.profiles + if self.cctx: + return self.cctx.profiles else: return [] @@ -227,8 +237,8 @@ def profile(self): :type: str """ - if self.codec_context: - return self.codec_context.profile + if self.cctx: + return self.cctx.profile else: return None diff --git a/av/stream.pyi b/av/stream.pyi index 91f44628a..87b6632ec 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -44,6 +44,7 @@ class Discard(IntEnum): class Stream: name: str | None container: Container + cctx: CodecContext | None codec_context: CodecContext | None metadata: dict[str, str] @property @@ -66,12 +67,14 @@ class Stream: @final class DataStream(Stream): + cctx: None codec_context: None type: Literal["data"] name: str | None @final class AttachmentStream(Stream): + cctx: None codec_context: None type: Literal["attachment"] @property diff --git a/av/subtitles/stream.py b/av/subtitles/stream.py index a39f8b0a3..3fd839c55 100644 --- a/av/subtitles/stream.py +++ b/av/subtitles/stream.py @@ -7,7 +7,7 @@ @cython.cclass class SubtitleStream(Stream): def __getattr__(self, name): - return getattr(self.codec_context, name) + return getattr(self.cctx, name) @cython.ccall def decode(self, packet: Packet | None = None): @@ -22,4 +22,4 @@ def decode(self, packet: Packet | None = None): if not packet: packet = Packet() - return self.codec_context.decode(packet) + return self.cctx.decode(packet) diff --git a/av/subtitles/stream.pyi b/av/subtitles/stream.pyi index 446f58226..b97d5dcca 100644 --- a/av/subtitles/stream.pyi +++ b/av/subtitles/stream.pyi @@ -9,6 +9,7 @@ from .codeccontext import SubtitleCodecContext @final class SubtitleStream(Stream): + cctx: SubtitleCodecContext codec_context: SubtitleCodecContext # From codec context diff --git a/av/video/stream.py b/av/video/stream.py index e2efe667b..e60ade5a3 100644 --- a/av/video/stream.py +++ b/av/video/stream.py @@ -1,3 +1,5 @@ +import warnings + import cython from cython.cimports import libav as lib from cython.cimports.av.packet import Packet @@ -8,6 +10,15 @@ from cython.cimports.libc.string import memcpy +def _warn_format_passthrough(): + warnings.warn( + "VideoStream.format is deprecated; use VideoStream.cctx.format instead", + DeprecationWarning, + # Cython functions have no frames, so 1 is the caller. + stacklevel=1, + ) + + @cython.final @cython.cclass class VideoStream(Stream): @@ -17,12 +28,13 @@ def __repr__(self): f"" ) - if self.codec_context is None: + if self.cctx is None: return f" at 0x{id(self):x}>" + fmt = self.cctx.format return ( f"" + f"{fmt.name if fmt else None} {self.cctx.width}x" + f"{self.cctx.height} at 0x{id(self):x}>" ) def __getattr__(self, name): @@ -30,11 +42,34 @@ def __getattr__(self, name): raise AttributeError( f"'{type(self).__name__}' object has no attribute '{name}'" ) - if self.codec_context is None: + if self.cctx is None: raise AttributeError( f"'{type(self).__name__}' object has no attribute '{name}'" ) - return getattr(self.codec_context, name) + return getattr(self.cctx, name) + + @property + def format(self): + """ + The video format of the codec context. + + .. deprecated:: 19.1 + Use ``stream.cctx.format`` instead. + + :type: VideoFormat | None + """ + _warn_format_passthrough() + if self.cctx is None: + raise AttributeError( + f"'{type(self).__name__}' object has no attribute 'format'" + ) + return self.cctx.format + + @format.setter + def format(self, value): + _warn_format_passthrough() + if self.cctx is not None: + self.cctx.format = value @cython.ccall def encode(self, frame: VideoFrame | None = None): @@ -46,7 +81,7 @@ def encode(self, frame: VideoFrame | None = None): .. seealso:: This is mostly a passthrough to :meth:`.CodecContext.encode`. """ self._assert_has_codec_context(lib.AVERROR_ENCODER_NOT_FOUND) - packets = self.codec_context.encode(frame) + packets = self.cctx.encode(frame) packet: Packet for packet in packets: packet._stream = self @@ -61,7 +96,7 @@ def encode_lazy(self, frame: VideoFrame | None = None): """ self._assert_has_codec_context(lib.AVERROR_ENCODER_NOT_FOUND) packet: Packet - for packet in self.codec_context.encode_lazy(frame): + for packet in self.cctx.encode_lazy(frame): packet._stream = self packet.ptr.stream_index = self.ptr.index yield packet @@ -76,16 +111,16 @@ def decode(self, packet: Packet | None = None): .. seealso:: This is a passthrough to :meth:`.CodecContext.decode`. """ self._assert_has_codec_context() - return self.codec_context.decode(packet) + return self.cctx.decode(packet) @cython.cfunc def _finalize_for_output(self) -> cython.void: Stream._finalize_for_output(self) - if self.codec_context is not None: - self.ptr.avg_frame_rate = self.codec_context.ptr.framerate + if self.cctx is not None: + self.ptr.avg_frame_rate = self.cctx.ptr.framerate # avcodec_parameters_from_context() overwrites codecpar.coded_side_data, # so inject the display matrix after it, before avformat_write_header(). - if self.codec_context is not None and self._has_display_matrix: + if self.cctx is not None and self._has_display_matrix: self._apply_display_matrix() @cython.cfunc @@ -205,8 +240,8 @@ def display_aspect_ratio(self): lib.av_reduce( cython.address(dar.num), cython.address(dar.den), - self.format.width * self.sample_aspect_ratio.num, - self.format.height * self.sample_aspect_ratio.den, + self.cctx.format.width * self.sample_aspect_ratio.num, + self.cctx.format.height * self.sample_aspect_ratio.den, 1024 * 1024, ) diff --git a/av/video/stream.pyi b/av/video/stream.pyi index 167e63b4e..43a12d20a 100644 --- a/av/video/stream.pyi +++ b/av/video/stream.pyi @@ -1,6 +1,8 @@ from collections.abc import Iterator, Sequence from typing import Any, Literal, final +from typing_extensions import deprecated + from av.codec.codec import Codec from av.codec.context import ThreadType from av.packet import Packet @@ -18,6 +20,7 @@ class VideoStream(Stream): bit_rate_tolerance: int sample_aspect_ratio: AVRational display_aspect_ratio: AVRational + cctx: VideoCodecContext codec_context: VideoCodecContext def encode(self, frame: VideoFrame | None = None) -> list[Packet[VideoStream]]: ... @@ -39,7 +42,12 @@ class VideoStream(Stream): codec: Codec codec_tag: str options: dict[str, str] - format: VideoFormat | None + @property + @deprecated("Use VideoStream.cctx.format instead") + def format(self) -> VideoFormat | None: ... + @format.setter + @deprecated("Use VideoStream.cctx.format instead") + def format(self, value: VideoFormat) -> None: ... thread_count: int @property def thread_type(self) -> ThreadType: ... diff --git a/docs/api/stream.rst b/docs/api/stream.rst index 98d8fc43d..1af5491b1 100644 --- a/docs/api/stream.rst +++ b/docs/api/stream.rst @@ -64,6 +64,8 @@ Basics .. autoattribute:: Stream.type +.. autoattribute:: Stream.cctx + .. autoattribute:: Stream.codec_context .. autoattribute:: Stream.id diff --git a/tests/test_bitstream.py b/tests/test_bitstream.py index 57e02204d..3d4fca651 100644 --- a/tests/test_bitstream.py +++ b/tests/test_bitstream.py @@ -94,13 +94,13 @@ def test_filter_output_parameters() -> None: with av.open(fate_suite("h264/interlaced_crop.mp4"), "r") as container: stream = container.streams.video[0] - assert not is_annexb(stream.codec_context.extradata) + assert not is_annexb(stream.cctx.extradata) ctx = BitStreamFilterContext("h264_mp4toannexb", stream) - assert not is_annexb(stream.codec_context.extradata) + assert not is_annexb(stream.cctx.extradata) del ctx _ = BitStreamFilterContext("h264_mp4toannexb", stream, out_stream=stream) - assert is_annexb(stream.codec_context.extradata) + assert is_annexb(stream.cctx.extradata) def test_filter_flush() -> None: diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index e6e67b605..01384d1fd 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -108,7 +108,7 @@ def test_global_quality(self) -> None: def test_level(self) -> None: with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: - assert container.streams.video[0].codec_context.level == 41 + assert container.streams.video[0].cctx.level == 41 ctx = Codec("mpeg4", "w").create() ctx.level = 5 @@ -116,7 +116,7 @@ def test_level(self) -> None: def test_field_order(self) -> None: with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: - assert container.streams.video[0].codec_context.field_order == 2 + assert container.streams.video[0].cctx.field_order == 2 ctx = Codec("mpeg4", "w").create("video") ctx.field_order = 1 @@ -128,9 +128,9 @@ def test_skip_frame_default(self) -> None: def test_codec_delay(self) -> None: with av.open(fate_suite("mkv/codec_delay_opus.mkv")) as container: - assert container.streams.audio[0].codec_context.delay == 312 + assert container.streams.audio[0].cctx.delay == 312 with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: - assert container.streams.video[0].codec_context.delay == 0 + assert container.streams.video[0].cctx.delay == 0 def test_codec_tag(self) -> None: ctx = Codec("mpeg4", "w").create() @@ -232,7 +232,7 @@ def test_bits_per_coded_sample(self) -> None: stream = output.add_stream("qtrle") with pytest.raises(ValueError): - stream.codec_context.bits_per_coded_sample = 32 + stream.cctx.bits_per_coded_sample = 32 def test_decode_keeps_frames_before_error(self) -> None: # Regression test for #2044: a single packet may contain a valid frame @@ -249,8 +249,8 @@ def test_decode_keeps_frames_before_error(self) -> None: with av.open(buf, "w", format="flac") as output: stream = output.add_stream("flac", rate=44100) assert isinstance(stream, AudioStream) - stream.format = "s16" - stream.layout = "mono" + stream.cctx.format = "s16" + stream.cctx.layout = "mono" n = 0 for _ in range(8): samples = 4096 @@ -271,7 +271,7 @@ def test_decode_keeps_frames_before_error(self) -> None: buf.seek(0) with av.open(buf, "r") as container: audio = container.streams.audio[0] - extradata = audio.codec_context.extradata + extradata = audio.cctx.extradata packets = [bytes(p) for p in container.demux(audio) if p.size] assert len(packets) >= 3 @@ -666,7 +666,7 @@ def test_stats_in_out(self) -> None: def test_audio_padding(self) -> None: with av.open(fate_suite("mkv/codec_delay_opus.mkv")) as container: - ctx = container.streams.audio[0].codec_context + ctx = container.streams.audio[0].cctx assert ctx.initial_padding == 312 assert ctx.seek_preroll == 3840 assert ctx.block_align == 0 @@ -674,7 +674,7 @@ def test_audio_padding(self) -> None: def test_coded_side_data(self) -> None: with av.open(fate_suite("mov/displaymatrix.mov")) as container: stream = container.streams.video[0] - ctx = stream.codec_context + ctx = stream.cctx assert ctx.bits_per_raw_sample == 8 assert len(ctx.coded_side_data["display_matrix"]) == 36 diff --git a/tests/test_colorspace.py b/tests/test_colorspace.py index 61a68500e..ed7e68815 100644 --- a/tests/test_colorspace.py +++ b/tests/test_colorspace.py @@ -20,14 +20,14 @@ def test_penguin_joke() -> None: ) stream = container.streams.video[0] - assert stream.codec_context.color_range == 2 - assert stream.codec_context.color_range == ColorRange.JPEG + assert stream.cctx.color_range == 2 + assert stream.cctx.color_range == ColorRange.JPEG - assert stream.codec_context.color_primaries == 2 - assert stream.codec_context.color_trc == 2 + assert stream.cctx.color_primaries == 2 + assert stream.cctx.color_trc == 2 - assert stream.codec_context.colorspace == 5 - assert stream.codec_context.colorspace == Colorspace.ITU601 + assert stream.cctx.colorspace == 5 + assert stream.cctx.colorspace == Colorspace.ITU601 for frame in container.decode(stream): assert frame.color_range == ColorRange.JPEG # a.k.a "pc" @@ -43,11 +43,11 @@ def test_sky_timelapse() -> None: assert stream.disposition == av.stream.Disposition.default - assert stream.codec_context.color_range == 1 - assert stream.codec_context.color_range == ColorRange.MPEG - assert stream.codec_context.color_primaries == 1 - assert stream.codec_context.color_trc == 1 - assert stream.codec_context.colorspace == 1 + assert stream.cctx.color_range == 1 + assert stream.cctx.color_range == ColorRange.MPEG + assert stream.cctx.color_primaries == 1 + assert stream.cctx.color_trc == 1 + assert stream.cctx.colorspace == 1 def test_frame_color_trc_property() -> None: @@ -100,7 +100,7 @@ def test_chroma_location_round_trip() -> None: out_stream.width = 64 out_stream.height = 64 out_stream.pix_fmt = "yuv420p" - out_stream.codec_context.chroma_sample_location = ChromaLocation.TOPLEFT + out_stream.cctx.chroma_sample_location = ChromaLocation.TOPLEFT for i in range(5): frame = av.VideoFrame(width=64, height=64, format="yuv420p") diff --git a/tests/test_decode.py b/tests/test_decode.py index 091825244..19581e671 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -81,7 +81,7 @@ def test_decode_stream_without_codec_context(self) -> None: data = buffer.getvalue().replace(b"avc1", b"zzzz") with av.open(io.BytesIO(data)) as container: stream = container.streams.video[0] - assert stream.codec_context is None + assert stream.cctx is None with pytest.raises(av.DecoderNotFoundError): list(container.decode(stream)) @@ -89,7 +89,7 @@ def test_mux_stream_without_codec_context(self) -> None: with av.open(io.BytesIO(), "w", format="mp4") as output: stream = output.add_mux_stream("h264", width=16, height=16) assert isinstance(stream, av.VideoStream) - assert stream.codec_context is None + assert stream.cctx is None with pytest.raises(av.EncoderNotFoundError): stream.encode(None) @@ -122,7 +122,7 @@ def test_flushed_frames_keep_time_base(self) -> None: # it must fall back to the one the container set on the context. container = av.open(fate_suite("h264/interlaced_crop.mp4")) stream = container.streams.video[0] - ctx = stream.codec_context + ctx = stream.cctx assert ctx is not None for packet in container.demux(stream): @@ -195,8 +195,8 @@ def test_decoded_time_base(self) -> None: def test_decoded_motion_vectors(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) stream = container.streams.video[0] - codec_context = stream.codec_context - codec_context.options = {"flags2": "+export_mvs"} + cctx = stream.cctx + cctx.options = {"flags2": "+export_mvs"} for frame in container.decode(stream): vectors = frame.side_data.get("MOTION_VECTORS") @@ -210,7 +210,7 @@ def test_decoded_motion_vectors(self) -> None: def test_motion_vector_index_bounds(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) stream = container.streams.video[0] - stream.codec_context.options = {"flags2": "+export_mvs"} + stream.cctx.options = {"flags2": "+export_mvs"} for frame in container.decode(stream): vectors = frame.side_data.get("MOTION_VECTORS") @@ -238,7 +238,7 @@ def test_decoded_motion_vectors_no_flag(self) -> None: def test_decoded_video_enc_params(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) stream = container.streams.video[0] - stream.codec_context.options = {"export_side_data": "venc_params"} + stream.cctx.options = {"export_side_data": "venc_params"} for frame in container.decode(stream): video_enc_params = cast( @@ -329,7 +329,7 @@ def test_flush_decoded_video_frame_count(self) -> None: def test_side_data_mapping_protocol(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) stream = container.streams.video[0] - stream.codec_context.options = {"flags2": "+export_mvs"} + stream.cctx.options = {"flags2": "+export_mvs"} for frame in container.decode(stream): side_data = frame.side_data @@ -394,7 +394,7 @@ def test_hardware_decode(self) -> None: container = av.open(test_video_path, hwaccel=hwaccel) video_stream = container.streams.video[0] - assert video_stream.codec_context.is_hwaccel + assert video_stream.cctx.is_hwaccel frame_count = 0 for frame in container.decode(video_stream): diff --git a/tests/test_encode.py b/tests/test_encode.py index 524bc1626..e8c4ec8d4 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -111,10 +111,10 @@ def assert_rgb_rotate( # codec context properties assert stream.codec.name == "mpeg4" assert stream.codec.long_name == "MPEG-4 part 2" - assert stream.format is not None - assert stream.format.name == "yuv420p" - assert stream.format.width == WIDTH - assert stream.format.height == HEIGHT + assert stream.cctx.format is not None + assert stream.cctx.format.name == "yuv420p" + assert stream.cctx.format.width == WIDTH + assert stream.cctx.format.height == HEIGHT class TestBasicVideoEncoding(TestCase): @@ -126,10 +126,10 @@ def test_default_options(self) -> None: assert not stream.time_base # codec context properties - assert stream.format is not None - assert stream.format.height == 480 - assert stream.format.name == "yuv420p" - assert stream.format.width == 640 + assert stream.cctx.format is not None + assert stream.cctx.format.height == 480 + assert stream.cctx.format.name == "yuv420p" + assert stream.cctx.format.width == 640 assert stream.height == 480 assert stream.pix_fmt == "yuv420p" assert stream.width == 640 @@ -170,7 +170,7 @@ def test_set_rate_after_add_stream(self) -> None: with av.open(path, "w") as output: stream = output.add_stream("mpeg4") - stream.codec_context.framerate = Fraction(30, 1) + stream.cctx.framerate = Fraction(30, 1) stream.width = 16 stream.height = 16 @@ -202,8 +202,8 @@ def test_default_options(self) -> None: assert not stream.time_base # codec context properties - assert stream.format is not None - assert stream.format.name == "s16" + assert stream.cctx.format is not None + assert stream.cctx.format.name == "s16" assert stream.sample_rate == 48000 def test_transcode(self) -> None: @@ -220,9 +220,9 @@ def test_transcode(self) -> None: stream = output.add_stream("mp2", sample_rate) assert stream in output.streams.audio - ctx = stream.codec_context + ctx = stream.cctx ctx.sample_rate = sample_rate - stream.format = sample_fmt + stream.cctx.format = sample_fmt ctx.layout = channel_layout with av.open( @@ -244,8 +244,8 @@ def test_transcode(self) -> None: stream = container.streams[0] # codec context properties - assert stream.format is not None - assert stream.format.name == "s16p" + assert stream.cctx.format is not None + assert stream.cctx.format.name == "s16p" assert stream.sample_rate == sample_rate @@ -279,7 +279,7 @@ def test_reconfigure_stream_after_mux(self) -> None: first = output.add_stream("ffv1", rate=30) second = output.add_stream("ffv1", rate=30) - first.format = av.VideoFormat("bgr0", width=16, height=16) + first.cctx.format = av.VideoFormat("bgr0", width=16, height=16) frame = VideoFrame(16, 16, "bgr0") frame.pts = 0 frame.time_base = Fraction(1, 30) @@ -287,9 +287,9 @@ def test_reconfigure_stream_after_mux(self) -> None: # Muxing the first packet writes the header and opens every stream. # Changing the second encoder now used to corrupt FFV1 state and crash. - assert second.codec_context.is_open + assert second.cctx.is_open with pytest.raises(RuntimeError, match="Cannot change format"): - second.format = av.VideoFormat("bgr0", width=16, height=16) + second.cctx.format = av.VideoFormat("bgr0", width=16, height=16) with pytest.raises(RuntimeError, match="Cannot change width"): second.width = 16 with pytest.raises(RuntimeError, match="Cannot change height"): @@ -307,8 +307,8 @@ def test_stream_index(self) -> None: astream = output.add_stream("mp2", 48000) assert astream in output.streams.audio - astream.layout = "stereo" - astream.format = "s16" + astream.cctx.layout = "stereo" + astream.cctx.format = "s16" assert vstream.index == 0 assert astream.index == 1 @@ -338,8 +338,8 @@ def test_stream_index(self) -> None: def test_stream_index_lazy(self) -> None: with av.open(self.sandboxed("output_lazy.mov"), "w") as output: astream = output.add_stream("mp2", 48000) - astream.layout = "stereo" - astream.format = "s16" + astream.cctx.layout = "stereo" + astream.cctx.format = "s16" vstream = output.add_stream("mpeg4", 24) vstream.pix_fmt = "yuv420p" @@ -432,8 +432,8 @@ def encode_file_with_max_b_frames(max_b_frames: int) -> io.BytesIO: stream.width = 640 stream.height = 480 stream.pix_fmt = "yuv420p" - stream.codec_context.gop_size = 15 - stream.codec_context.max_b_frames = max_b_frames + stream.cctx.gop_size = 15 + stream.cctx.max_b_frames = max_b_frames for i in range(50): array = np.empty((stream.height, stream.width, 3), dtype=np.uint8) @@ -516,8 +516,8 @@ def encode_frames_with_qminmax( stream = container.add_stream("h264", rate=30) stream.height, stream.width, _ = shape stream.pix_fmt = "yuv420p" - stream.codec_context.gop_size = 15 - stream.codec_context.qmin, stream.codec_context.qmax = qminmax + stream.cctx.gop_size = 15 + stream.cctx.qmin, stream.cctx.qmax = qminmax bytes_encoded = 0 for frame in frames: @@ -643,7 +643,7 @@ def test_hardware_encode() -> None: muxed += 1 # The hardware frames context must have been set up during open(). - assert stream.codec_context.is_hwaccel + assert stream.cctx.is_hwaccel for packet in stream.encode(): container.mux(packet) @@ -684,18 +684,18 @@ def test_hardware_encode_honors_sw_format() -> None: stream.width = 320 stream.height = 240 stream.pix_fmt = hw_format - stream.codec_context.sw_format = "yuv420p" + stream.cctx.sw_format = "yuv420p" - assert stream.codec_context.sw_format is not None - assert stream.codec_context.sw_format.name == "yuv420p" + assert stream.cctx.sw_format is not None + assert stream.cctx.sw_format.name == "yuv420p" frame = VideoFrame(320, 240, "rgb24") for packet in stream.encode(frame): container.mux(packet) - assert stream.codec_context.pix_fmt == hw_format - assert stream.codec_context.sw_format is not None - assert stream.codec_context.sw_format.name == "yuv420p" + assert stream.cctx.pix_fmt == hw_format + assert stream.cctx.sw_format is not None + assert stream.cctx.sw_format.name == "yuv420p" for packet in stream.encode(): container.mux(packet) container.close() diff --git a/tests/test_file_probing.py b/tests/test_file_probing.py index b4a157642..fa49a9420 100644 --- a/tests/test_file_probing.py +++ b/tests/test_file_probing.py @@ -47,10 +47,10 @@ def test_stream_probing(self) -> None: assert stream.channels == 2 assert stream.codec.name == "aac_latm" assert stream.codec.long_name == "AAC LATM (Advanced Audio Coding LATM syntax)" - assert stream.format is not None - assert stream.format.bits == 32 - assert stream.format.name == "fltp" - assert stream.layout.name == "stereo" + assert stream.cctx.format is not None + assert stream.cctx.format.bits == 32 + assert stream.cctx.format.name == "fltp" + assert stream.cctx.layout.name == "stereo" assert stream.max_bit_rate is None assert stream.sample_rate == 48000 @@ -100,8 +100,8 @@ def test_stream_probing(self) -> None: assert stream.codec.name == "flac" assert stream.codec.long_name == "FLAC (Free Lossless Audio Codec)" assert stream.channels == 0 - assert stream.format is None - assert stream.layout.name == "0 channels" + assert stream.cctx.format is None + assert stream.cctx.layout.name == "0 channels" assert stream.max_bit_rate is None assert stream.sample_rate == 0 @@ -250,8 +250,8 @@ def test_stream_probing(self) -> None: assert stream.codec.long_name == "MPEG-2 video" assert stream.codec.name == "mpeg2video" assert stream.display_aspect_ratio == Fraction(4, 3) - assert stream.format is not None - assert stream.format.name == "yuv420p" + assert stream.cctx.format is not None + assert stream.cctx.format.name == "yuv420p" assert not stream.has_b_frames assert stream.height == 576 assert stream.max_bit_rate is None @@ -307,7 +307,7 @@ def test_stream_probing(self) -> None: assert stream.codec.long_name == "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10" assert stream.codec.name == "h264" assert not stream.display_aspect_ratio - assert stream.format is None + assert stream.cctx.format is None assert not stream.has_b_frames assert stream.height == 0 assert stream.max_bit_rate is None diff --git a/tests/test_python_io.py b/tests/test_python_io.py index eb1419392..842b67f13 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -304,10 +304,10 @@ def test_writing_to_custom_io_image2(self) -> None: # codec context properties assert stream.codec.name == "png" - assert stream.format is not None - assert stream.format.name == "rgb24" - assert stream.format.width == width - assert stream.format.height == height + assert stream.cctx.format is not None + assert stream.cctx.format.name == "rgb24" + assert stream.cctx.format.width == width + assert stream.cctx.format.height == height def test_writing_to_file(self) -> None: path = self.sandboxed("writing.mp4") diff --git a/tests/test_remux.py b/tests/test_remux.py index bfeadc726..65c3c8a56 100644 --- a/tests/test_remux.py +++ b/tests/test_remux.py @@ -49,15 +49,15 @@ def test_add_mux_stream_video() -> None: buf = io.BytesIO() with av.open(input_path) as input_: in_stream = input_.streams.video[0] - width = in_stream.codec_context.width - height = in_stream.codec_context.height + width = in_stream.cctx.width + height = in_stream.cctx.height with av.open(buf, "w", format="mp4") as output: out_stream = output.add_mux_stream( - in_stream.codec_context.name, width=width, height=height + in_stream.cctx.name, width=width, height=height ) assert isinstance(out_stream, av.VideoStream) - assert out_stream.codec_context is None + assert out_stream.cctx is None assert out_stream.type == "video" out_stream.time_base = in_stream.time_base @@ -71,8 +71,8 @@ def test_add_mux_stream_video() -> None: buf.seek(0) with av.open(buf) as result: assert len(result.streams.video) == 1 - assert result.streams.video[0].codec_context.width == width - assert result.streams.video[0].codec_context.height == height + assert result.streams.video[0].cctx.width == width + assert result.streams.video[0].cctx.height == height def test_add_mux_stream_no_codec_context() -> None: @@ -82,8 +82,8 @@ def test_add_mux_stream_no_codec_context() -> None: video_stream = output.add_mux_stream("h264", width=1920, height=1080) audio_stream = output.add_mux_stream("aac", rate=44100) - assert video_stream.codec_context is None - assert audio_stream.codec_context is None + assert video_stream.cctx is None + assert audio_stream.cctx is None # repr should not crash assert "video/" in repr(video_stream) assert "audio/" in repr(audio_stream) diff --git a/tests/test_streams.py b/tests/test_streams.py index 3200eb032..68e49e26a 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -2,6 +2,7 @@ import os import struct from collections.abc import Iterator +from fractions import Fraction import pytest @@ -299,7 +300,7 @@ def test_data_stream_from_template(self) -> None: with av.open("data_copy.ts") as remuxed: output_stream = remuxed.streams.data[0] - assert output_stream.codec_context is None + assert output_stream.cctx is None remuxed_payloads: list[bytes] = [] for data_packet in remuxed.demux(output_stream): @@ -330,7 +331,7 @@ def test_data_stream_with_codec(self) -> None: assert data_stream.type == "data" assert klv_stream.type == "data" - # Note: codec_context may be None for descriptor-only data codecs + # Note: cctx may be None for descriptor-only data codecs test_data = [b"test1", b"test2", b"test3"] for i, data in enumerate(test_data): @@ -446,3 +447,64 @@ def test_stream_after_close(self) -> None: # Still describable, so a traceback or debugger does not blow up. assert "container closed" in repr(stream) + + +def test_audio_format_layout_passthrough_deprecated() -> None: + with av.open(io.BytesIO(), "w", format="mp4") as output: + stream = output.add_stream("aac", rate=48000) + assert isinstance(stream, av.AudioStream) + + with pytest.warns(DeprecationWarning, match="cctx\.format"): + stream.format = "fltp" + with pytest.warns(DeprecationWarning, match="cctx\.layout"): + stream.layout = "mono" + fmt = stream.cctx.format + assert fmt is not None and fmt.name == "fltp" + assert stream.cctx.layout.name == "mono" + + with pytest.warns(DeprecationWarning, match="cctx\.format"): + fmt = stream.format + assert fmt is not None and fmt.name == "fltp" + with pytest.warns(DeprecationWarning, match="cctx\.layout"): + assert stream.layout.name == "mono" + + +def test_video_format_passthrough_deprecated() -> None: + with av.open(io.BytesIO(), "w", format="mp4") as output: + stream = output.add_stream("mpeg4", rate=24) + assert isinstance(stream, av.VideoStream) + + with pytest.warns(DeprecationWarning, match="cctx\.format"): + stream.format = av.VideoFormat("yuv420p", 32, 32) + assert stream.cctx.width == 32 + + with pytest.warns(DeprecationWarning, match="cctx\.format"): + assert stream.format is not None + + +@pytest.mark.filterwarnings("error::DeprecationWarning") +def test_add_stream_format_layout_kwargs_not_deprecated() -> None: + with av.open(io.BytesIO(), "w", format="mp4") as output: + astream = output.add_stream("aac", rate=48000, format="fltp", layout="mono") + assert astream.cctx.layout.name == "mono" + + vstream = output.add_stream( + "mpeg4", rate=24, format=av.VideoFormat("yuv420p", 32, 32) + ) + assert vstream.cctx.width == 32 + + graph = av.filter.Graph() + graph.add_buffer(template=vstream, time_base=Fraction(1, 24)) + graph.add_abuffer(template=astream, time_base=Fraction(1, 48000)) + + +def test_codec_context_is_cctx_alias() -> None: + with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: + stream = container.streams.video[0] + assert stream.cctx is not None + assert stream.codec_context is stream.cctx + + with av.open(io.BytesIO(), "w", format="mp4") as output: + mux = output.add_mux_stream("h264", width=16, height=16) + assert mux.cctx is None + assert mux.codec_context is None diff --git a/tests/test_subtitles.py b/tests/test_subtitles.py index 62b807886..f6dfe5708 100644 --- a/tests/test_subtitles.py +++ b/tests/test_subtitles.py @@ -82,7 +82,7 @@ def test_subtitle_header_read(self) -> None: with av.open(path) as container: stream = container.streams.subtitles[0] - ctx = stream.codec_context + ctx = stream.cctx header = ctx.subtitle_header assert header is None or isinstance(header, bytes) @@ -154,7 +154,7 @@ def test_subtitle_encode_mp4(self) -> None: video_stream.pix_fmt = "yuv420p" sub_stream = container.add_stream("mov_text") - sub_ctx = sub_stream.codec_context + sub_ctx = sub_stream.cctx sub_ctx.subtitle_header = ass_header container.start_encoding() @@ -192,7 +192,7 @@ def test_subtitle_encode_mkv_srt(self) -> None: output = io.BytesIO() with av.open(output, "w", format="matroska") as container: sub_stream = container.add_stream("srt") - sub_ctx = sub_stream.codec_context + sub_ctx = sub_stream.cctx sub_ctx.subtitle_header = minimal_header container.start_encoding()