|
|
cimport libav as lib
|
|
|
from libc.stdio cimport fprintf, stderr
|
|
|
from libc.stdlib cimport free, malloc
|
|
|
|
|
|
from av.logging cimport get_last_error
|
|
|
|
|
|
import errno
|
|
|
import os
|
|
|
import sys
|
|
|
import traceback
|
|
|
from threading import local
|
|
|
|
|
|
|
|
|
__all__ = [
|
|
|
"ErrorType", "FFmpegError", "LookupError", "HTTPError", "HTTPClientError",
|
|
|
"UndefinedError",
|
|
|
]
|
|
|
|
|
|
|
|
|
cpdef code_to_tag(int code):
|
|
|
"""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,
|
|
|
))
|
|
|
|
|
|
cpdef tag_to_code(bytes tag):
|
|
|
"""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(FFmpegError, self).__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
|
|
|
|
|
|
|
|
|
|
|
|
cdef int c_PYAV_STASHED_ERROR = tag_to_code(b"PyAV")
|
|
|
cdef str PYAV_STASHED_ERROR_message = "Error in PyAV callback"
|
|
|
|
|
|
|
|
|
|
|
|
class LookupError(FFmpegError, LookupError):
|
|
|
pass
|
|
|
|
|
|
|
|
|
class HTTPError(FFmpegError):
|
|
|
pass
|
|
|
|
|
|
|
|
|
class HTTPClientError(FFmpegError):
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
_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),
|
|
|
)
|
|
|
|
|
|
cdef sentinel = object()
|
|
|
|
|
|
|
|
|
class EnumType(type):
|
|
|
def __new__(mcl, name, bases, attrs, *args):
|
|
|
|
|
|
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):
|
|
|
|
|
|
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)
|
|
|
|
|
|
def __getitem__(self, key):
|
|
|
if isinstance(key, str):
|
|
|
return self._by_name[key]
|
|
|
|
|
|
if isinstance(key, int):
|
|
|
try:
|
|
|
return self._by_value[key]
|
|
|
except KeyError:
|
|
|
pass
|
|
|
|
|
|
raise KeyError(key)
|
|
|
|
|
|
if isinstance(key, self):
|
|
|
return key
|
|
|
|
|
|
raise TypeError(f"{self.__name__} indices must be str, int, or itself")
|
|
|
|
|
|
def _get(self, long value, bint create=False):
|
|
|
try:
|
|
|
return self._by_value[value]
|
|
|
except KeyError:
|
|
|
pass
|
|
|
|
|
|
if not create:
|
|
|
return
|
|
|
|
|
|
return self._create(f"{self.__name__.upper()}_{value}", value, by_value_only=True)
|
|
|
|
|
|
def get(self, key, default=None, create=False):
|
|
|
try:
|
|
|
return self[key]
|
|
|
except KeyError:
|
|
|
if create:
|
|
|
return self._get(key, create=True)
|
|
|
return default
|
|
|
|
|
|
|
|
|
cdef 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.
|
|
|
|
|
|
"""
|
|
|
cdef readonly str name
|
|
|
cdef readonly int value
|
|
|
|
|
|
def __cinit__(self, sentinel_, str name, int value, 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:
|
|
|
|
|
|
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 = {}
|
|
|
|
|
|
|
|
|
def _extend_builtin(name, codes):
|
|
|
base = getattr(__builtins__, name, OSError)
|
|
|
cls = type(name, (FFmpegError, base), dict(__module__=__name__))
|
|
|
|
|
|
|
|
|
for code in codes:
|
|
|
classes[code] = cls
|
|
|
|
|
|
|
|
|
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("ValueError", (errno.EINVAL, ))
|
|
|
_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])
|
|
|
|
|
|
|
|
|
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__})
|
|
|
|
|
|
|
|
|
classes[code] = cls
|
|
|
|
|
|
|
|
|
globals()[name] = cls
|
|
|
__all__.append(name)
|
|
|
|
|
|
del _ffmpeg_specs
|
|
|
|
|
|
|
|
|
|
|
|
cdef object _local = local()
|
|
|
cdef int _err_count = 0
|
|
|
|
|
|
cdef int stash_exception(exc_info=None):
|
|
|
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
|
|
|
|
|
|
exc_info = exc_info or sys.exc_info()
|
|
|
_local.exc_info = exc_info
|
|
|
if exc_info:
|
|
|
_err_count += 1
|
|
|
|
|
|
return -c_PYAV_STASHED_ERROR
|
|
|
|
|
|
|
|
|
cdef int _last_log_count = 0
|
|
|
|
|
|
cpdef int err_check(int res, filename=None) except -1:
|
|
|
"""Raise appropriate exceptions from library return code."""
|
|
|
|
|
|
global _err_count
|
|
|
global _last_log_count
|
|
|
|
|
|
|
|
|
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[0], exc_info[1], exc_info[2]
|
|
|
|
|
|
if res >= 0:
|
|
|
return res
|
|
|
|
|
|
|
|
|
log_count, last_log = get_last_error()
|
|
|
if log_count > _last_log_count:
|
|
|
_last_log_count = log_count
|
|
|
log = last_log
|
|
|
else:
|
|
|
log = None
|
|
|
|
|
|
cdef int code = -res
|
|
|
cdef char* error_buffer = <char*>malloc(lib.AV_ERROR_MAX_STRING_SIZE * sizeof(char))
|
|
|
if error_buffer == 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)
|
|
|
|
|
|
message = error_buffer or os.strerror(code)
|
|
|
|
|
|
cls = classes.get(code, UndefinedError)
|
|
|
raise cls(code, message, filename, log)
|
|
|
finally:
|
|
|
free(error_buffer)
|
|
|
|
|
|
|
|
|
class UndefinedError(FFmpegError):
|
|
|
"""Fallback exception type in case FFmpeg returns an error we don't know about."""
|
|
|
pass
|
|
|
|