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
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ Features:
- Support reusing the thread's current CUDA context via a ``current_ctx`` flag on ``CudaContext`` and ``VideoFrame.from_dlpack``, for interop with libraries like PyTorch that initialize CUDA first by :gh-user:`Yozer` (:pr:`2339`).
- ``VideoFrame.from_dlpack`` no longer requires restating ``primary_ctx``/``current_ctx`` when passing an explicit ``cuda_context``; the flags are only validated when explicitly given by :gh-user:`WyattBlue`.

Fixes:

- Fix a crash when using a stream that has no ``CodecContext`` (a demuxed stream with no available decoder, such as one from a truncated file, or a stream created by ``add_mux_stream``); decoding now raises ``DecoderNotFoundError``, encoding now raises ``EncoderNotFoundError``, and ``BitStreamFilterContext`` accepts such a stream as ``out_stream`` by :gh-user:`WyattBlue`, reported by :gh-user:`justinrmiller` (:issue:`2344`).

v18.0.0
-------

Expand Down
5 changes: 3 additions & 2 deletions av/audio/stream.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import cython
from cython.cimports import libav as lib
from cython.cimports.av.audio.frame import AudioFrame
from cython.cimports.av.packet import Packet

Expand Down Expand Up @@ -31,7 +32,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)
packet: Packet
for packet in packets:
Expand All @@ -49,5 +50,5 @@ 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)
9 changes: 6 additions & 3 deletions av/bitstream.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,12 @@ def __cinit__(
out_stream.ptr.codecpar, self.ptr.par_out
)
err_check(res)
lib.avcodec_parameters_to_context(
out_stream.codec_context.ptr, out_stream.ptr.codecpar
)
# 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:
lib.avcodec_parameters_to_context(
out_stream.codec_context.ptr, out_stream.ptr.codecpar
)

def __dealloc__(self):
if self.ptr:
Expand Down
1 change: 1 addition & 0 deletions av/stream.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ cdef class Stream:

# Private API.
cdef _init(self, Container, lib.AVStream*, CodecContext)
cdef _assert_has_codec_context(self, int err=*)
cdef _finalize_for_output(self)
cdef _set_id(self, value)

Expand Down
8 changes: 8 additions & 0 deletions av/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,14 @@ def _init(
errors=self.container.metadata_errors,
)

@cython.cfunc
def _assert_has_codec_context(
self, err: cython.int = lib.AVERROR_DECODER_NOT_FOUND
):
# Calling into a NULL codec_context is a segfault, not an AttributeError.
if self.codec_context is None:
err_check(err)

def __repr__(self):
name = getattr(self, "name", None)
return (
Expand Down
1 change: 1 addition & 0 deletions av/subtitles/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def decode(self, packet: Packet | None = None):

.. seealso:: This is a passthrough to :meth:`.CodecContext.decode`.
"""
self._assert_has_codec_context()
if not packet:
packet = Packet()

Expand Down
3 changes: 2 additions & 1 deletion av/video/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,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)
packet: Packet
for packet in packets:
Expand All @@ -57,6 +57,7 @@ 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)

@cython.cfunc
Expand Down
29 changes: 29 additions & 0 deletions tests/test_decode.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import functools
import io
import os
import pathlib
from fractions import Fraction
Expand Down Expand Up @@ -52,6 +53,34 @@ def make_h264_test_video(path: str) -> None:


class TestDecode(TestCase):
def test_decode_stream_without_codec_context(self) -> None:
buffer = io.BytesIO()
with av.open(buffer, "w", format="mp4") as output:
stream = output.add_mux_stream("h264", width=16, height=16)
packet = av.Packet(b"invalid")
packet.stream = stream
packet.pts = packet.dts = 0
packet.time_base = Fraction(1, 1000)
output.mux(packet)

# Keep the MP4 video stream while making its codec unknown to FFmpeg.
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
with pytest.raises(av.DecoderNotFoundError):
list(container.decode(stream))

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 stream.codec_context is None
with pytest.raises(av.EncoderNotFoundError):
stream.encode(None)

# A bitstream filter only needs to update codecpar for a mux stream.
av.BitStreamFilterContext("h264_mp4toannexb", "h264", out_stream=stream)

def test_decoded_video_frame_count(self) -> None:
container = av.open(fate_suite("h264/interlaced_crop.mp4"))
video_stream = next(s for s in container.streams if s.type == "video")
Expand Down
Loading