Ye-Song's picture
Add files using upload-large-folder tool
2655d9f verified
Raw
History Blame Contribute Delete
11.9 kB
import errno
import os
import sys
import traceback
from threading import local
import cython
from cython.cimports import libav as lib
from cython.cimports.av.logging import get_last_error
from cython.cimports.libc.stdio import fprintf, stderr
from cython.cimports.libc.stdlib import free, malloc
# Will get extended with all the exceptions.
__all__ = [
"ErrorType",
"FFmpegError",
"LookupError",
"HTTPError",
"HTTPClientError",
"UndefinedError",
]
sentinel = cython.declare(object, object())
@cython.ccall
def code_to_tag(code: cython.int) -> bytes:
"""Convert an integer error code into 4-byte tag.
>>> code_to_tag(1953719668)
b'test'
"""
return bytes(
(
code & 0xFF,
(code >> 8) & 0xFF,
(code >> 16) & 0xFF,
(code >> 24) & 0xFF,
)
)
@cython.ccall
def tag_to_code(tag: bytes) -> cython.int:
"""Convert a 4-byte error tag into an integer code.
>>> tag_to_code(b'test')
1953719668
"""
if len(tag) != 4:
raise ValueError("Error tags are 4 bytes.")
return (tag[0]) + (tag[1] << 8) + (tag[2] << 16) + (tag[3] << 24)
class FFmpegError(Exception):
"""Exception class for errors from within FFmpeg.
.. attribute:: errno
FFmpeg's integer error code.
.. attribute:: strerror
FFmpeg's error message.
.. attribute:: filename
The filename that was being operated on (if available).
.. attribute:: log
The tuple from :func:`av.logging.get_last_log`, or ``None``.
"""
def __init__(self, code, message, filename=None, log=None):
self.errno = code
self.strerror = message
args = [code, message]
if filename or log:
args.append(filename)
if log:
args.append(log)
super().__init__(*args)
self.args = tuple(args)
@property
def filename(self):
try:
return self.args[2]
except IndexError:
pass
@property
def log(self):
try:
return self.args[3]
except IndexError:
pass
def __str__(self):
msg = ""
if self.errno is not None:
msg = f"{msg}[Errno {self.errno}] "
if self.strerror is not None:
msg = f"{msg}{self.strerror}"
if self.filename:
msg = f"{msg}: {self.filename!r}"
if self.log:
msg = (
f"{msg}; last error log: [{self.log[1].strip()}] {self.log[2].strip()}"
)
return msg
# Our custom error, used in callbacks.
c_PYAV_STASHED_ERROR = cython.declare(cython.int, tag_to_code(b"PyAV"))
PYAV_STASHED_ERROR_message = cython.declare(str, "Error in PyAV callback")
# Bases for the FFmpeg-based exceptions.
class LookupError(FFmpegError, LookupError):
pass
class HTTPError(FFmpegError):
pass
class HTTPClientError(FFmpegError):
pass
# Tuples of (enum_name, enum_value, exc_name, exc_base).
# tuple[str, int, str | None, Exception | none]
# fmt: off
_ffmpeg_specs = (
("BSF_NOT_FOUND", -lib.AVERROR_BSF_NOT_FOUND, "BSFNotFoundError", LookupError),
("BUG", -lib.AVERROR_BUG, None, RuntimeError),
("BUFFER_TOO_SMALL", -lib.AVERROR_BUFFER_TOO_SMALL, None, ValueError),
("DECODER_NOT_FOUND", -lib.AVERROR_DECODER_NOT_FOUND, None, LookupError),
("DEMUXER_NOT_FOUND", -lib.AVERROR_DEMUXER_NOT_FOUND, None, LookupError),
("ENCODER_NOT_FOUND", -lib.AVERROR_ENCODER_NOT_FOUND, None, LookupError),
("EOF", -lib.AVERROR_EOF, "EOFError", EOFError),
("EXIT", -lib.AVERROR_EXIT, None, None),
("EXTERNAL", -lib.AVERROR_EXTERNAL, None, None),
("FILTER_NOT_FOUND", -lib.AVERROR_FILTER_NOT_FOUND, None, LookupError),
("INVALIDDATA", -lib.AVERROR_INVALIDDATA, "InvalidDataError", ValueError),
("MUXER_NOT_FOUND", -lib.AVERROR_MUXER_NOT_FOUND, None, LookupError),
("OPTION_NOT_FOUND", -lib.AVERROR_OPTION_NOT_FOUND, None, LookupError),
("PATCHWELCOME", -lib.AVERROR_PATCHWELCOME, "PatchWelcomeError", None),
("PROTOCOL_NOT_FOUND", -lib.AVERROR_PROTOCOL_NOT_FOUND, None, LookupError),
("UNKNOWN", -lib.AVERROR_UNKNOWN, None, None),
("EXPERIMENTAL", -lib.AVERROR_EXPERIMENTAL, None, None),
("INPUT_CHANGED", -lib.AVERROR_INPUT_CHANGED, None, None),
("OUTPUT_CHANGED", -lib.AVERROR_OUTPUT_CHANGED, None, None),
("HTTP_BAD_REQUEST", -lib.AVERROR_HTTP_BAD_REQUEST, "HTTPBadRequestError", HTTPClientError),
("HTTP_UNAUTHORIZED", -lib.AVERROR_HTTP_UNAUTHORIZED, "HTTPUnauthorizedError", HTTPClientError),
("HTTP_FORBIDDEN", -lib.AVERROR_HTTP_FORBIDDEN, "HTTPForbiddenError", HTTPClientError),
("HTTP_NOT_FOUND", -lib.AVERROR_HTTP_NOT_FOUND, "HTTPNotFoundError", HTTPClientError),
("HTTP_OTHER_4XX", -lib.AVERROR_HTTP_OTHER_4XX, "HTTPOtherClientError", HTTPClientError),
("HTTP_SERVER_ERROR", -lib.AVERROR_HTTP_SERVER_ERROR, "HTTPServerError", HTTPError),
("PYAV_CALLBACK", c_PYAV_STASHED_ERROR, "PyAVCallbackError", RuntimeError),
)
# fmt: on
class EnumType(type):
def __new__(mcl, name, bases, attrs, *args):
# Just adapting the method signature.
return super().__new__(mcl, name, bases, attrs)
def __init__(self, name, bases, attrs, items):
self._by_name = {}
self._by_value = {}
self._all = []
for spec in items:
self._create(*spec)
def _create(self, name, value, doc=None, by_value_only=False):
# We only have one instance per value.
try:
item = self._by_value[value]
except KeyError:
item = self(sentinel, name, value, doc)
self._by_value[value] = item
return item
def __len__(self):
return len(self._all)
def __iter__(self):
return iter(self._all)
@cython.cclass
class EnumItem:
"""An enumeration of FFmpeg's error types.
.. attribute:: tag
The FFmpeg byte tag for the error.
.. attribute:: strerror
The error message that would be returned.
"""
name = cython.declare(str, visibility="readonly")
value = cython.declare(cython.int, visibility="readonly")
def __cinit__(self, sentinel_, name: str, value: cython.int, doc=None):
if sentinel_ is not sentinel:
raise RuntimeError(f"Cannot instantiate {self.__class__.__name__}.")
self.name = name
self.value = value
self.__doc__ = doc
def __repr__(self):
return f"<{self.__class__.__module__}.{self.__class__.__name__}:{self.name}(0x{self.value:x})>"
def __str__(self):
return self.name
def __int__(self):
return self.value
@property
def tag(self):
return code_to_tag(self.value)
ErrorType = EnumType(
"ErrorType", (EnumItem,), {"__module__": __name__}, [x[:2] for x in _ffmpeg_specs]
)
for enum in ErrorType:
# Mimick the errno module.
globals()[enum.name] = enum
if enum.value == c_PYAV_STASHED_ERROR:
enum.strerror = PYAV_STASHED_ERROR_message
else:
enum.strerror = lib.av_err2str(-enum.value)
classes: dict = {}
def _extend_builtin(name, codes):
base = getattr(__builtins__, name, OSError)
cls = type(name, (FFmpegError, base), {"__module__": __name__})
# Register in builder.
for code in codes:
classes[code] = cls
# Register in module.
globals()[name] = cls
__all__.append(name)
return cls
_extend_builtin("PermissionError", (errno.EACCES, errno.EPERM))
_extend_builtin(
"BlockingIOError",
(errno.EAGAIN, errno.EALREADY, errno.EINPROGRESS, errno.EWOULDBLOCK),
)
_extend_builtin("ChildProcessError", (errno.ECHILD,))
_extend_builtin("ConnectionAbortedError", (errno.ECONNABORTED,))
_extend_builtin("ConnectionRefusedError", (errno.ECONNREFUSED,))
_extend_builtin("ConnectionResetError", (errno.ECONNRESET,))
_extend_builtin("FileExistsError", (errno.EEXIST,))
_extend_builtin("InterruptedError", (errno.EINTR,))
_extend_builtin("IsADirectoryError", (errno.EISDIR,))
_extend_builtin("FileNotFoundError", (errno.ENOENT,))
_extend_builtin("NotADirectoryError", (errno.ENOTDIR,))
_extend_builtin("BrokenPipeError", (errno.EPIPE, errno.ESHUTDOWN))
_extend_builtin("ProcessLookupError", (errno.ESRCH,))
_extend_builtin("TimeoutError", (errno.ETIMEDOUT,))
_extend_builtin("MemoryError", (errno.ENOMEM,))
_extend_builtin("NotImplementedError", (errno.ENOSYS,))
_extend_builtin("OverflowError", (errno.ERANGE,))
_extend_builtin("OSError", [code for code in errno.errorcode if code not in classes])
class ArgumentError(FFmpegError, ValueError):
def __str__(self):
msg = ""
if self.strerror is not None:
msg = f"{msg}{self.strerror}"
if self.filename:
msg = f"{msg}: {self.filename!r}"
if self.errno is not None:
msg = f"{msg} returned {self.errno}"
if self.log:
msg = (
f"{msg}; last error log: [{self.log[1].strip()}] {self.log[2].strip()}"
)
return msg
class UndefinedError(FFmpegError):
"""Fallback exception type in case FFmpeg returns an error we don't know about."""
pass
classes[errno.EINVAL] = ArgumentError
globals()["ArgumentError"] = ArgumentError
__all__.append("ArgumentError")
# Classes for the FFmpeg errors.
for enum_name, code, name, base in _ffmpeg_specs:
name = name or enum_name.title().replace("_", "") + "Error"
if base is None:
bases = (FFmpegError,)
elif issubclass(base, FFmpegError):
bases = (base,)
else:
bases = (FFmpegError, base)
cls = type(name, bases, {"__module__": __name__})
# Register in builder.
classes[code] = cls
# Register in module.
globals()[name] = cls
__all__.append(name)
del _ffmpeg_specs
# Storage for stashing.
_local = cython.declare(object, local())
_err_count = cython.declare(cython.int, 0)
_last_log_count = cython.declare(cython.int, 0)
@cython.cfunc
def stash_exception(exc_info=None) -> cython.int:
global _err_count
existing = getattr(_local, "exc_info", None)
if existing is not None:
fprintf(stderr, "PyAV library exception being dropped:\n")
traceback.print_exception(*existing)
_err_count -= 1 # Balance out the +=1 that is coming.
exc_info = exc_info or sys.exc_info()
_local.exc_info = exc_info
if exc_info:
_err_count += 1
return -c_PYAV_STASHED_ERROR
@cython.ccall
@cython.exceptval(-1, check=False)
def err_check(res: cython.int, filename=None) -> cython.int:
"""Raise appropriate exceptions from library return code."""
global _err_count
global _last_log_count
# Check for stashed exceptions.
if _err_count:
exc_info = getattr(_local, "exc_info", None)
if exc_info is not None:
_err_count -= 1
_local.exc_info = None
raise exc_info[1].with_traceback(exc_info[2])
if res >= 0:
return res
# Grab details from the last log.
log_count, last_log = get_last_error()
if log_count > _last_log_count:
_last_log_count = log_count
log = last_log
else:
log = None
code: cython.int = -res
error_buffer: cython.p_char = cython.cast(
cython.p_char, malloc(lib.AV_ERROR_MAX_STRING_SIZE * cython.sizeof(char))
)
if error_buffer == cython.NULL:
raise MemoryError()
try:
if code == c_PYAV_STASHED_ERROR:
message = PYAV_STASHED_ERROR_message
else:
lib.av_strerror(res, error_buffer, lib.AV_ERROR_MAX_STRING_SIZE)
# Fallback to OS error string if no message
message = error_buffer or os.strerror(code)
cls = classes.get(code, UndefinedError)
raise cls(code, message, filename, log)
finally:
free(error_buffer)