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: 0 additions & 1 deletion .github/workflows/smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ jobs:
- {os: ubuntu-24.04, python: "3.12", ffmpeg: "8.0.1", extras: true}
- {os: ubuntu-24.04, python: "3.10", ffmpeg: "8.0.1"}
- {os: ubuntu-24.04, python: "3.13", ffmpeg: "8.1"}
- {os: ubuntu-24.04, python: "pypy3.11", ffmpeg: "8.0.1"}
- {os: macos-14, python: "3.11", ffmpeg: "8.0.1"}
- {os: macos-14, python: "3.14", ffmpeg: "8.1"}

Expand Down
5 changes: 2 additions & 3 deletions av/container/core.pxd
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
cimport libav as lib
from libc.stdint cimport uint8_t

from av.codec.hwaccel cimport HWAccel
from av.container.pyio cimport PyIOFile
Expand All @@ -14,16 +15,13 @@ ctypedef struct timeout_info:


cdef class Container:
cdef readonly bint writeable
cdef lib.AVFormatContext *ptr

cdef readonly object name
cdef readonly str metadata_encoding
cdef readonly str metadata_errors

cdef readonly PyIOFile file
cdef int buffer_size
cdef bint input_was_opened
cdef readonly object io_open
cdef readonly object open_files

Expand All @@ -39,6 +37,7 @@ cdef class Container:
cdef readonly dict metadata

# Private API.
cdef uint8_t _myflag # enum: writeable, input_was_opened, started, done
cdef _assert_open(self)
cdef int err_check(self, int value) except -1

Expand Down
18 changes: 11 additions & 7 deletions av/container/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,8 +246,8 @@ def __cinit__(
if sentinel is not _cinit_sentinel:
raise RuntimeError("cannot construct base Container")

self.writeable = isinstance(self, OutputContainer)
if not self.writeable and not isinstance(self, InputContainer):
writeable: cython.bint = isinstance(self, OutputContainer)
if not writeable and not isinstance(self, InputContainer):
raise RuntimeError("Container cannot be directly extended.")

if isinstance(file_, str):
Expand Down Expand Up @@ -276,13 +276,13 @@ def __cinit__(
format_name, acodec = format_name.split(":")
self.format = ContainerFormat(format_name)

self.input_was_opened = False
res: cython.int
name_obj: bytes = os.fsencode(self.name)
name: cython.p_char = name_obj
ofmt: cython.pointer[cython.const[lib.AVOutputFormat]]

if self.writeable:
if writeable:
self._myflag |= 1 # enum.writeable = True
ofmt = (
self.format.optr
if self.format
Expand Down Expand Up @@ -320,7 +320,7 @@ def __cinit__(
# Setup Python IO.
self.open_files = {}
if not isinstance(file_, basestring):
self.file = PyIOFile(file_, buffer_size, self.writeable)
self.file = PyIOFile(file_, buffer_size, writeable)
self.ptr.pb = self.file.iocontext

if io_open is not None:
Expand All @@ -330,7 +330,7 @@ def __cinit__(

ifmt: cython.pointer[cython.const[lib.AVInputFormat]]
c_options: Dictionary
if not self.writeable:
if not writeable:
ifmt = self.format.iptr if self.format else cython.NULL
c_options = Dictionary(self.options, self.container_options)

Expand All @@ -342,7 +342,7 @@ def __cinit__(
)
self.set_timeout(None)
self.err_check(res)
self.input_was_opened = True
self._myflag |= 2 # enum.input_was_opened = True

if format_name is None:
self.format = build_container_format(self.ptr.iformat, self.ptr.oformat)
Expand Down Expand Up @@ -400,6 +400,10 @@ def flags(self, value: cython.int):
self._assert_open()
self.ptr.flags = value

@property
def input_was_opened(self):
return self._myflag & 2

def chapters(self):
self._assert_open()
result: list = []
Expand Down
4 changes: 2 additions & 2 deletions av/container/core.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,11 @@ class Chapter(TypedDict):
metadata: dict[str, str]

class Container:
writeable: bool
name: str
metadata_encoding: str
metadata_errors: str
file: Any
buffer_size: int
input_was_opened: bool
io_open: Any
open_files: Any
format: ContainerFormat
Expand All @@ -100,6 +98,8 @@ class Container:
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None: ...
@property
def input_was_opened(self) -> bool: ...
def close(self) -> None: ...
def chapters(self) -> list[Chapter]: ...
def set_chapters(self, chapters: list[Chapter]) -> None: ...
Expand Down
1 change: 0 additions & 1 deletion av/container/input.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,4 @@ from av.stream cimport Stream


cdef class InputContainer(Container):

cdef flush_buffers(self)
6 changes: 3 additions & 3 deletions av/container/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
@cython.cfunc
def close_input(self: InputContainer):
self.streams = StreamContainer()
if self.input_was_opened:
with cython.nogil:
with cython.nogil:
if self._myflag & 2:
# This causes `self.ptr` to be set to NULL.
lib.avformat_close_input(cython.address(self.ptr))
self.input_was_opened = False
self._myflag &= ~2 # enum.input_was_opened = False


@cython.cclass
Expand Down
3 changes: 0 additions & 3 deletions av/container/output.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,5 @@ from av.stream cimport Stream


cdef class OutputContainer(Container):
cdef bint _started
cdef bint _done
cdef lib.AVPacket *packet_ptr

cpdef start_encoding(self)
17 changes: 11 additions & 6 deletions av/container/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@
@cython.cfunc
def close_output(self: OutputContainer):
self.streams = StreamContainer()
if self._started and not self._done:
if self._myflag & 12 == 4: # enum.started and not enum.done
# We must only ever call av_write_trailer *once*, otherwise we get a
# segmentation fault. Therefore no matter whether it succeeds or not
# we must absolutely set self._done.
# we must absolutely set enum.done.
try:
self.err_check(lib.av_write_trailer(self.ptr))
finally:
if self.file is None and not (self.ptr.oformat.flags & lib.AVFMT_NOFILE):
lib.avio_closep(cython.address(self.ptr.pb))
self._done = True
self._myflag |= 8 # enum.done = True


@cython.cclass
Expand All @@ -35,8 +35,13 @@ def __cinit__(self, *args, **kwargs):
with cython.nogil:
self.packet_ptr = lib.av_packet_alloc()

def __del__(self):
try:
close_output(self)
except Exception:
pass

def __dealloc__(self):
close_output(self)
with cython.nogil:
lib.av_packet_free(cython.address(self.packet_ptr))

Expand Down Expand Up @@ -426,7 +431,7 @@ def add_data_stream(self, codec_name=None, options: dict | None = None):
@cython.ccall
def start_encoding(self):
"""Write the file header! Called automatically."""
if self._started:
if self._myflag & 4: # started
return

# TODO: This does NOT handle options coming from 3 sources.
Expand Down Expand Up @@ -486,7 +491,7 @@ def start_encoding(self):
log = logging.getLogger(__name__)
log.warning("Some options were not used: %s" % unused_options)

self._started = True
self._myflag |= 4

@property
def supported_codecs(self):
Expand Down
18 changes: 18 additions & 0 deletions tests/test_open.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import gc
import io
from pathlib import Path

import av
Expand Down Expand Up @@ -35,3 +37,19 @@ def test_str_output() -> None:

container = av.open(path, "w")
assert type(container) is av.container.OutputContainer


def _container_no_close() -> None:
buf = io.BytesIO()
container = av.open(buf, mode="w", format="mp4")
stream = container.add_stream("mpeg4", rate=24)
stream.width = 320
stream.height = 240
stream.pix_fmt = "yuv420p"
container.start_encoding()


def test_container_no_close() -> None:
# Do not close so that container is freed through GC.
_container_no_close()
gc.collect()
Loading