Skip to content
Merged
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
128 changes: 81 additions & 47 deletions av/container/pyio.pyx → av/container/pyio.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
cimport libav as lib
from libc.string cimport memcpy
# type: ignore
import cython
from cython import NULL
from cython.cimports import libav as lib
from cython.cimports.av.error import stash_exception
from cython.cimports.libc.stdint import int64_t, uint8_t
from cython.cimports.libc.string import memcpy

from av.error cimport stash_exception
Buf = cython.typedef(cython.pointer[uint8_t])
BufC = cython.typedef(cython.pointer[cython.const[uint8_t]])

ctypedef int64_t (*seek_func_t)(void *opaque, int64_t offset, int whence) noexcept nogil
seek_func_t = cython.typedef(
"int64_t (*seek_func_t)(void *opaque, int64_t offset, int whence) noexcept nogil"
)


cdef class PyIOFile:
@cython.cclass
class PyIOFile:
def __cinit__(self, file, buffer_size, writeable=None):
self.file = file

cdef seek_func_t seek_func = NULL
seek_func: seek_func_t = NULL

readable = getattr(self.file, "readable", None)
writable = getattr(self.file, "writable", None)
Expand All @@ -21,70 +30,81 @@ def __cinit__(self, file, buffer_size, writeable=None):
self.ftell = getattr(self.file, "tell", None)
self.fclose = getattr(self.file, "close", None)

# To be seekable the file object must have `seek` and `tell` methods.
# To be seekable, the file object must have `seek` and `tell` methods.
# If it also has a `seekable` method, it must return True.
if (
self.fseek is not None
and self.ftell is not None
and (seekable is None or seekable())
):
seek_func = pyio_seek
seek_func: seek_func_t = pyio_seek

if writeable is None:
writeable = self.fwrite is not None

if writeable:
if self.fwrite is None or (writable is not None and not writable()):
raise ValueError("File object has no write() method, or writable() returned False.")
raise ValueError(
"File object has no write() method, or writable() returned False."
)
else:
if self.fread is None or (readable is not None and not readable()):
raise ValueError("File object has no read() method, or readable() returned False.")
raise ValueError(
"File object has no read() method, or readable() returned False."
)

self.pos = 0
self.pos_is_valid = True

# This is effectively the maximum size of reads.
self.buffer = <unsigned char*>lib.av_malloc(buffer_size)
self.buffer = cython.cast(cython.p_uchar, lib.av_malloc(buffer_size))

self.iocontext = lib.avio_alloc_context(
self.buffer,
buffer_size,
writeable,
<void*>self, # User data.
cython.cast(cython.p_void, self), # User data.
pyio_read,
pyio_write,
seek_func
seek_func,
)

if seek_func:
self.iocontext.seekable = lib.AVIO_SEEKABLE_NORMAL
self.iocontext.max_packet_size = buffer_size

def __dealloc__(self):
with nogil:
with cython.nogil:
# FFmpeg will not release custom input, so it's up to us to free it.
# Do not touch our original buffer as it may have been freed and replaced.
if self.iocontext:
lib.av_freep(&self.iocontext.buffer)
lib.av_freep(&self.iocontext)
lib.av_freep(cython.address(self.iocontext.buffer))
lib.av_freep(cython.address(self.iocontext))

# We likely errored badly if we got here, and so are still
# responsible for our buffer.
# We likely errored badly if we got here, and so we are still responsible.
else:
lib.av_freep(&self.buffer)
lib.av_freep(cython.address(self.buffer))


cdef int pyio_read(void *opaque, uint8_t *buf, int buf_size) noexcept nogil:
with gil:
@cython.cfunc
@cython.nogil
@cython.exceptval(check=False)
def pyio_read(opaque: cython.p_void, buf: Buf, buf_size: cython.int) -> cython.int:
with cython.gil:
return pyio_read_gil(opaque, buf, buf_size)

cdef int pyio_read_gil(void *opaque, uint8_t *buf, int buf_size) noexcept:
cdef PyIOFile self
cdef bytes res

@cython.cfunc
@cython.exceptval(check=False)
def pyio_read_gil(opaque: cython.p_void, buf: Buf, buf_size: cython.int) -> cython.int:
self: PyIOFile
res: bytes
try:
self = <PyIOFile>opaque
self = cython.cast(PyIOFile, opaque)
res = self.fread(buf_size)
memcpy(buf, <void*><char*>res, len(res))
memcpy(
buf, cython.cast(cython.p_void, cython.cast(cython.p_char, res)), len(res)
)
self.pos += len(res)
if not res:
return lib.AVERROR_EOF
Expand All @@ -93,16 +113,24 @@ def __dealloc__(self):
return stash_exception()


cdef int pyio_write(void *opaque, const uint8_t *buf, int buf_size) noexcept nogil:
with gil:
@cython.cfunc
@cython.nogil
@cython.exceptval(check=False)
def pyio_write(opaque: cython.p_void, buf: BufC, buf_size: cython.int) -> cython.int:
with cython.gil:
return pyio_write_gil(opaque, buf, buf_size)

cdef int pyio_write_gil(void *opaque, const uint8_t *buf, int buf_size) noexcept:
cdef PyIOFile self
cdef bytes bytes_to_write
cdef int bytes_written

@cython.cfunc
@cython.exceptval(check=False)
def pyio_write_gil(
opaque: cython.p_void, buf: BufC, buf_size: cython.int
) -> cython.int:
self: PyIOFile
bytes_to_write: bytes
bytes_written: cython.int
try:
self = <PyIOFile>opaque
self = cython.cast(PyIOFile, opaque)
bytes_to_write = buf[:buf_size]
ret_value = self.fwrite(bytes_to_write)
bytes_written = ret_value if isinstance(ret_value, int) else buf_size
Expand All @@ -112,19 +140,25 @@ def __dealloc__(self):
return stash_exception()


cdef int64_t pyio_seek(void *opaque, int64_t offset, int whence) noexcept nogil:
# Seek takes the standard flags, but also a ad-hoc one which means that
# the library wants to know how large the file is. We are generally
# allowed to ignore this.
@cython.cfunc
@cython.nogil
@cython.exceptval(check=False)
def pyio_seek(opaque: cython.p_void, offset: int64_t, whence: cython.int) -> int64_t:
# Seek takes the standard flags, but also a ad-hoc one which means that the library
# wants to know how large the file is. We are generally allowed to ignore this.
if whence == lib.AVSEEK_SIZE:
return -1
with gil:
with cython.gil:
return pyio_seek_gil(opaque, offset, whence)

cdef int64_t pyio_seek_gil(void *opaque, int64_t offset, int whence):
cdef PyIOFile self

@cython.cfunc
def pyio_seek_gil(
opaque: cython.p_void, offset: int64_t, whence: cython.int
) -> int64_t:
self: PyIOFile
try:
self = <PyIOFile>opaque
self = cython.cast(PyIOFile, opaque)
res = self.fseek(offset, whence)

# Track the position for the user.
Expand All @@ -144,18 +178,19 @@ def __dealloc__(self):
return stash_exception()


cdef int pyio_close_gil(lib.AVIOContext *pb):
@cython.cfunc
def pyio_close_gil(pb: cython.pointer[lib.AVIOContext]) -> cython.int:
try:
return lib.avio_close(pb)

except Exception:
stash_exception()


cdef int pyio_close_custom_gil(lib.AVIOContext *pb):
cdef PyIOFile self
@cython.cfunc
def pyio_close_custom_gil(pb: cython.pointer[lib.AVIOContext]) -> cython.int:
self: PyIOFile
try:
self = <PyIOFile>pb.opaque
self = cython.cast(PyIOFile, pb.opaque)

# Flush bytes in the AVIOContext buffers to the custom I/O
lib.avio_flush(pb)
Expand All @@ -164,6 +199,5 @@ def __dealloc__(self):
self.fclose()

return 0

except Exception:
stash_exception()
Loading