2026-06-16 17:09:34 +00:00
|
|
|
# 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
|
2026-02-06 22:23:20 +01:00
|
|
|
|
2026-06-16 17:09:34 +00:00
|
|
|
Buf = cython.typedef(cython.pointer[uint8_t])
|
|
|
|
|
BufC = cython.typedef(cython.pointer[cython.const[uint8_t]])
|
2026-02-06 22:23:20 +01:00
|
|
|
|
2026-06-16 17:09:34 +00:00
|
|
|
seek_func_t = cython.typedef(
|
|
|
|
|
"int64_t (*seek_func_t)(void *opaque, int64_t offset, int whence) noexcept nogil"
|
|
|
|
|
)
|
2026-02-06 22:23:20 +01:00
|
|
|
|
|
|
|
|
|
2026-06-16 17:09:34 +00:00
|
|
|
@cython.final
|
|
|
|
|
@cython.cclass
|
|
|
|
|
class PyIOFile:
|
2026-02-06 22:23:20 +01:00
|
|
|
def __cinit__(self, file, buffer_size, writeable=None):
|
|
|
|
|
self.file = file
|
|
|
|
|
|
2026-06-16 17:09:34 +00:00
|
|
|
seek_func: seek_func_t = NULL
|
2026-02-06 22:23:20 +01:00
|
|
|
|
|
|
|
|
readable = getattr(self.file, "readable", None)
|
|
|
|
|
writable = getattr(self.file, "writable", None)
|
|
|
|
|
seekable = getattr(self.file, "seekable", None)
|
|
|
|
|
self.fread = getattr(self.file, "read", None)
|
|
|
|
|
self.fwrite = getattr(self.file, "write", None)
|
|
|
|
|
self.fseek = getattr(self.file, "seek", None)
|
|
|
|
|
self.ftell = getattr(self.file, "tell", None)
|
|
|
|
|
self.fclose = getattr(self.file, "close", None)
|
|
|
|
|
|
2026-06-16 17:09:34 +00:00
|
|
|
# To be seekable, the file object must have `seek` and `tell` methods.
|
2026-02-06 22:23:20 +01:00
|
|
|
# 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())
|
|
|
|
|
):
|
2026-06-16 17:09:34 +00:00
|
|
|
seek_func: seek_func_t = pyio_seek
|
2026-02-06 22:23:20 +01:00
|
|
|
|
|
|
|
|
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()):
|
2026-06-16 17:09:34 +00:00
|
|
|
raise ValueError(
|
|
|
|
|
"File object has no write() method, or writable() returned False."
|
|
|
|
|
)
|
2026-02-06 22:23:20 +01:00
|
|
|
else:
|
|
|
|
|
if self.fread is None or (readable is not None and not readable()):
|
2026-06-16 17:09:34 +00:00
|
|
|
raise ValueError(
|
|
|
|
|
"File object has no read() method, or readable() returned False."
|
|
|
|
|
)
|
2026-02-06 22:23:20 +01:00
|
|
|
|
|
|
|
|
self.pos = 0
|
|
|
|
|
self.pos_is_valid = True
|
|
|
|
|
|
|
|
|
|
# This is effectively the maximum size of reads.
|
2026-06-16 17:09:34 +00:00
|
|
|
self.buffer = cython.cast(cython.p_uchar, lib.av_malloc(buffer_size))
|
2026-02-06 22:23:20 +01:00
|
|
|
|
|
|
|
|
self.iocontext = lib.avio_alloc_context(
|
|
|
|
|
self.buffer,
|
|
|
|
|
buffer_size,
|
|
|
|
|
writeable,
|
2026-06-16 17:09:34 +00:00
|
|
|
cython.cast(cython.p_void, self), # User data.
|
2026-02-06 22:23:20 +01:00
|
|
|
pyio_read,
|
|
|
|
|
pyio_write,
|
2026-06-16 17:09:34 +00:00
|
|
|
seek_func,
|
2026-02-06 22:23:20 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if seek_func:
|
|
|
|
|
self.iocontext.seekable = lib.AVIO_SEEKABLE_NORMAL
|
|
|
|
|
self.iocontext.max_packet_size = buffer_size
|
|
|
|
|
|
|
|
|
|
def __dealloc__(self):
|
2026-06-16 17:09:34 +00:00
|
|
|
with cython.nogil:
|
2026-02-06 22:23:20 +01:00
|
|
|
# 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:
|
2026-06-16 17:09:34 +00:00
|
|
|
lib.av_freep(cython.address(self.iocontext.buffer))
|
|
|
|
|
lib.av_freep(cython.address(self.iocontext))
|
2026-02-06 22:23:20 +01:00
|
|
|
|
2026-06-16 17:09:34 +00:00
|
|
|
# We likely errored badly if we got here, and so we are still responsible.
|
2026-02-06 22:23:20 +01:00
|
|
|
else:
|
2026-06-16 17:09:34 +00:00
|
|
|
lib.av_freep(cython.address(self.buffer))
|
2026-02-06 22:23:20 +01:00
|
|
|
|
|
|
|
|
|
2026-06-16 17:09:34 +00:00
|
|
|
@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:
|
2026-02-06 22:23:20 +01:00
|
|
|
return pyio_read_gil(opaque, buf, buf_size)
|
|
|
|
|
|
2026-06-16 17:09:34 +00:00
|
|
|
|
|
|
|
|
@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
|
2026-02-06 22:23:20 +01:00
|
|
|
try:
|
2026-06-16 17:09:34 +00:00
|
|
|
self = cython.cast(PyIOFile, opaque)
|
2026-02-06 22:23:20 +01:00
|
|
|
res = self.fread(buf_size)
|
2026-06-16 17:09:34 +00:00
|
|
|
memcpy(
|
|
|
|
|
buf, cython.cast(cython.p_void, cython.cast(cython.p_char, res)), len(res)
|
|
|
|
|
)
|
2026-02-06 22:23:20 +01:00
|
|
|
self.pos += len(res)
|
|
|
|
|
if not res:
|
|
|
|
|
return lib.AVERROR_EOF
|
|
|
|
|
return len(res)
|
|
|
|
|
except Exception:
|
|
|
|
|
return stash_exception()
|
|
|
|
|
|
|
|
|
|
|
2026-06-16 17:09:34 +00:00
|
|
|
@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:
|
2026-02-06 22:23:20 +01:00
|
|
|
return pyio_write_gil(opaque, buf, buf_size)
|
|
|
|
|
|
2026-06-16 17:09:34 +00:00
|
|
|
|
|
|
|
|
@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
|
2026-02-06 22:23:20 +01:00
|
|
|
try:
|
2026-06-16 17:09:34 +00:00
|
|
|
self = cython.cast(PyIOFile, opaque)
|
2026-02-06 22:23:20 +01:00
|
|
|
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
|
|
|
|
|
self.pos += bytes_written
|
|
|
|
|
return bytes_written
|
|
|
|
|
except Exception:
|
|
|
|
|
return stash_exception()
|
|
|
|
|
|
|
|
|
|
|
2026-06-16 17:09:34 +00:00
|
|
|
@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.
|
2026-02-06 22:23:20 +01:00
|
|
|
if whence == lib.AVSEEK_SIZE:
|
|
|
|
|
return -1
|
2026-06-16 17:09:34 +00:00
|
|
|
with cython.gil:
|
2026-02-06 22:23:20 +01:00
|
|
|
return pyio_seek_gil(opaque, offset, whence)
|
|
|
|
|
|
2026-06-16 17:09:34 +00:00
|
|
|
|
|
|
|
|
@cython.cfunc
|
|
|
|
|
def pyio_seek_gil(
|
|
|
|
|
opaque: cython.p_void, offset: int64_t, whence: cython.int
|
|
|
|
|
) -> int64_t:
|
|
|
|
|
self: PyIOFile
|
2026-02-06 22:23:20 +01:00
|
|
|
try:
|
2026-06-16 17:09:34 +00:00
|
|
|
self = cython.cast(PyIOFile, opaque)
|
2026-02-06 22:23:20 +01:00
|
|
|
res = self.fseek(offset, whence)
|
|
|
|
|
|
|
|
|
|
# Track the position for the user.
|
|
|
|
|
if whence == 0:
|
|
|
|
|
self.pos = offset
|
|
|
|
|
elif whence == 1:
|
|
|
|
|
self.pos += offset
|
|
|
|
|
else:
|
|
|
|
|
self.pos_is_valid = False
|
|
|
|
|
if res is None:
|
|
|
|
|
if self.pos_is_valid:
|
|
|
|
|
res = self.pos
|
|
|
|
|
else:
|
|
|
|
|
res = self.ftell()
|
|
|
|
|
return res
|
|
|
|
|
except Exception:
|
|
|
|
|
return stash_exception()
|
|
|
|
|
|
|
|
|
|
|
2026-06-16 17:09:34 +00:00
|
|
|
@cython.cfunc
|
|
|
|
|
def pyio_close_gil(pb: cython.pointer[lib.AVIOContext]) -> cython.int:
|
2026-02-06 22:23:20 +01:00
|
|
|
try:
|
|
|
|
|
return lib.avio_close(pb)
|
|
|
|
|
except Exception:
|
2026-06-16 17:09:34 +00:00
|
|
|
return stash_exception()
|
2026-02-06 22:23:20 +01:00
|
|
|
|
|
|
|
|
|
2026-06-16 17:09:34 +00:00
|
|
|
@cython.cfunc
|
|
|
|
|
def pyio_close_custom_gil(pb: cython.pointer[lib.AVIOContext]) -> cython.int:
|
|
|
|
|
self: PyIOFile
|
2026-02-06 22:23:20 +01:00
|
|
|
try:
|
2026-06-16 17:09:34 +00:00
|
|
|
self = cython.cast(PyIOFile, pb.opaque)
|
2026-02-06 22:23:20 +01:00
|
|
|
|
|
|
|
|
# Flush bytes in the AVIOContext buffers to the custom I/O
|
|
|
|
|
lib.avio_flush(pb)
|
|
|
|
|
|
|
|
|
|
if self.fclose is not None:
|
|
|
|
|
self.fclose()
|
|
|
|
|
|
|
|
|
|
return 0
|
|
|
|
|
except Exception:
|
|
|
|
|
stash_exception()
|