diff --git a/av/audio/codeccontext.py b/av/audio/codeccontext.py index 2dc629917..8174ba26d 100644 --- a/av/audio/codeccontext.py +++ b/av/audio/codeccontext.py @@ -10,7 +10,7 @@ @cython.cclass class AudioCodecContext(CodecContext): @cython.cfunc - def _prepare_frames_for_encode(self, input_frame: Frame | None): + def _prepare_frames_for_encode(self, input_frame: Frame | None) -> list: frame: AudioFrame | None = input_frame allow_var_frame_size: cython.bint = ( self.ptr.codec.capabilities & lib.AV_CODEC_CAP_VARIABLE_FRAME_SIZE diff --git a/av/codec/context.pxd b/av/codec/context.pxd index 7ba89dab7..a9722c384 100644 --- a/av/codec/context.pxd +++ b/av/codec/context.pxd @@ -40,7 +40,7 @@ cdef class CodecContext: # TODO: Remove the `Packet` from `_setup_decoded_frame` (because flushing packets # are bogus). It should take all info it needs from the context and/or stream. cdef _prepare_and_time_rebase_frames_for_encode(self, Frame frame) - cdef _prepare_frames_for_encode(self, Frame frame) + cdef list _prepare_frames_for_encode(self, Frame frame) cdef _setup_encoded_packet(self, Packet) cdef _setup_decoded_frame(self, Frame, Packet) diff --git a/av/codec/context.py b/av/codec/context.py index 0117d8034..4751e597f 100644 --- a/av/codec/context.py +++ b/av/codec/context.py @@ -386,7 +386,7 @@ def _send_packet_and_recv(self, packet: Packet | None): return out @cython.cfunc - def _prepare_frames_for_encode(self, frame: Frame | None): + def _prepare_frames_for_encode(self, frame: Frame | None) -> list: return [frame] @cython.cfunc diff --git a/av/container/input.py b/av/container/input.py index 682158997..938ac8dab 100644 --- a/av/container/input.py +++ b/av/container/input.py @@ -236,11 +236,11 @@ def seek( self, offset, *, - backward: bint = True, - any_frame: bint = False, + backward: cython.bint = True, + any_frame: cython.bint = False, stream: Stream | None = None, - unsupported_frame_offset: bint = False, - unsupported_byte_offset: bint = False, + unsupported_frame_offset: cython.bint = False, + unsupported_byte_offset: cython.bint = False, ): """seek(offset, *, backward=True, any_frame=False, stream=None) diff --git a/av/logging.pyx b/av/logging.py similarity index 69% rename from av/logging.pyx rename to av/logging.py index 30a253fe0..ade74c954 100644 --- a/av/logging.pyx +++ b/av/logging.py @@ -1,3 +1,4 @@ +# type: ignore """ FFmpeg has a logging system that it uses extensively. It's very noisy, so PyAV turns it off by default. This unfortunately has the effect of making raised errors have less @@ -38,14 +39,15 @@ """ -cimport libav as lib -from libc.stdio cimport fprintf, stderr -from libc.stdlib cimport free, malloc - import logging import sys from threading import Lock, get_ident +import cython +import cython.cimports.libav as lib +from cython.cimports.libc.stdio import fprintf, stderr +from cython.cimports.libc.stdlib import free, malloc + # Library levels. PANIC = lib.AV_LOG_PANIC # 0 FATAL = lib.AV_LOG_FATAL # 8 @@ -60,9 +62,9 @@ CRITICAL = FATAL -cpdef adapt_level(int level): +@cython.ccall +def adapt_level(level: cython.int): """Convert a library log level to a Python log level.""" - if level <= lib.AV_LOG_FATAL: # Includes PANIC return 50 # logging.CRITICAL elif level <= lib.AV_LOG_ERROR: @@ -79,7 +81,7 @@ return 1 -cdef object level_threshold = None +level_threshold = cython.declare(object, None) # ... but lets limit ourselves to WARNING (assuming nobody already did this). if "libav" not in logging.Logger.manager.loggerDict: @@ -133,10 +135,10 @@ def restore_default_callback(): lib.av_log_set_callback(lib.av_log_default_callback) -cdef bint skip_repeated = True -cdef skip_lock = Lock() -cdef object last_log = None -cdef int skip_count = 0 +skip_repeated = cython.declare(cython.bint, True) +skip_lock = cython.declare(object, Lock()) +last_log = cython.declare(object, None) +skip_count = cython.declare(cython.int, 0) def get_skip_repeated(): @@ -151,10 +153,12 @@ def set_skip_repeated(v): # For error reporting. -cdef object last_error = None -cdef int error_count = 0 +last_error = cython.declare(object, None) +error_count = cython.declare(cython.int, 0) + -cpdef get_last_error(): +@cython.ccall +def get_last_error(): """Get the last log that was at least ``ERROR``.""" if error_count: with skip_lock: @@ -163,10 +167,12 @@ def set_skip_repeated(v): return 0, None -cdef global_captures = [] -cdef thread_captures = {} +global_captures = cython.declare(list, []) +thread_captures = cython.declare(dict, {}) -cdef class Capture: + +@cython.cclass +class Capture: """A context manager for capturing logs. :param bool local: Should logs from all threads be captured, or just one @@ -181,12 +187,11 @@ def set_skip_repeated(v): """ - cdef readonly list logs - cdef list captures + logs = cython.declare(list, visibility="readonly") + captures = cython.declare(list, visibility="private") - def __init__(self, bint local=True): + def __init__(self, local: cython.bint = True): self.logs = [] - if local: self.captures = thread_captures.setdefault(get_ident(), []) else: @@ -197,56 +202,73 @@ def __enter__(self): return self.logs def __exit__(self, type_, value, traceback): - self.captures.pop(-1) + self.captures.pop() + + +log_context = cython.struct( + class_=cython.pointer[lib.AVClass], + name=cython.p_char, +) +item_name_func = cython.typedef("const char *(*item_name_func)(void *) noexcept nogil") -cdef struct log_context: - lib.AVClass *class_ - const char *name -cdef const char *log_context_name(void *ptr) noexcept nogil: - cdef log_context *obj = ptr +@cython.cfunc +@cython.nogil +@cython.exceptval(check=False) +def log_context_name(ptr: cython.p_void) -> cython.p_char: + obj: cython.pointer[log_context] = cython.cast(cython.pointer[log_context], ptr) return obj.name -cdef lib.AVClass log_class -log_class.item_name = log_context_name -cpdef log(int level, str name, str message): +log_class = cython.declare(lib.AVClass) +log_class.item_name = cython.cast(item_name_func, log_context_name) + + +@cython.ccall +def log(level: cython.int, name: str, message: str): """Send a log through the library logging system. This is mostly for testing. - """ - - cdef log_context *obj = malloc(sizeof(log_context)) - obj.class_ = &log_class + obj: cython.pointer[log_context] = cython.cast( + cython.pointer[log_context], malloc(cython.sizeof(log_context)) + ) + obj.class_ = cython.address(log_class) obj.name = name - cdef bytes message_bytes = message.encode("utf-8") - - lib.av_log(obj, level, "%s", message_bytes) + message_bytes: bytes = message.encode("utf-8") + + lib.av_log( + cython.cast(cython.p_void, obj), + level, + "%s", + cython.cast(cython.p_char, message_bytes), + ) free(obj) -cdef log_callback_gil(int level, const char *c_name, const char *c_message): +@cython.cfunc +def log_callback_gil( + level: cython.int, c_name: cython.p_const_char, c_message: cython.p_char +): global error_count global skip_count global last_log global last_error - name = c_name if c_name is not NULL else "" - message = (c_message).decode("utf8", "backslashreplace") + name = cython.cast(str, c_name) if c_name is not cython.NULL else "" + message = cython.cast(bytes, c_message).decode("utf8", "backslashreplace") log = (level, name, message) # We have to filter it ourselves, but we will still process it in general so # it is available to our error handling. # Note that FFmpeg's levels are backwards from Python's. - cdef bint is_interesting = level <= level_threshold + is_interesting: cython.bint = level <= level_threshold # Skip messages which are identical to the previous. # TODO: Be smarter about threads. - cdef bint is_repeated = False - - cdef object repeat_log = None + is_repeated: cython.bint = False + repeat_log: object = None with skip_lock: if is_interesting: @@ -263,7 +285,7 @@ def __exit__(self, type_, value, traceback): repeat_log = ( last_log[0], last_log[1], - "%s (repeated %d more times)" % (last_log[2], skip_count) + "%s (repeated %d more times)" % (last_log[2], skip_count), ) skip_count = 0 @@ -281,7 +303,8 @@ def __exit__(self, type_, value, traceback): log_callback_emit(log) -cdef log_callback_emit(log): +@cython.cfunc +def log_callback_emit(log): lib_level, name, message = log captures = thread_captures.get(get_ident()) or global_captures @@ -296,37 +319,63 @@ def __exit__(self, type_, value, traceback): logger.log(py_level, message.strip()) -cdef void log_callback(void *ptr, int level, const char *format, lib.va_list args) noexcept nogil: - cdef bint inited = lib.Py_IsInitialized() +@cython.cfunc +@cython.nogil +@cython.exceptval(check=False) +def log_callback( + ptr: cython.p_void, + level: cython.int, + format: cython.p_const_char, + args: lib.va_list, +) -> cython.void: + inited: cython.bint = lib.Py_IsInitialized() if not inited: return - with gil: + with cython.gil: if level > level_threshold and level != lib.AV_LOG_ERROR: return # Format the message. - cdef char message[1024] + message: cython.char[1024] lib.vsnprintf(message, 1023, format, args) # Get the name. - cdef const char *name = NULL - cdef lib.AVClass *cls = (ptr)[0] if ptr else NULL + name: cython.p_const_char = cython.NULL + cls: cython.pointer[lib.AVClass] = ( + cython.cast(cython.pointer[cython.pointer[lib.AVClass]], ptr)[0] + if ptr + else cython.NULL + ) if cls and cls.item_name: name = cls.item_name(ptr) - with gil: + with cython.gil: try: log_callback_gil(level, name, message) except Exception: - fprintf(stderr, "av.logging: exception while handling %s[%d]: %s\n", - name, level, message) + fprintf( + stderr, + "av.logging: exception while handling %s[%d]: %s\n", + name, + level, + message, + ) # For some reason lib.PyErr_PrintEx(0) won't work. exc, type_, tb = sys.exc_info() lib.PyErr_Display(exc, type_, tb) -cdef void nolog_callback(void *ptr, int level, const char *format, lib.va_list args) noexcept nogil: +@cython.cfunc +@cython.nogil +@cython.exceptval(check=False) +def nolog_callback( + ptr: cython.p_void, + level: cython.int, + format: cython.p_const_char, + args: lib.va_list, +) -> cython.void: pass + lib.av_log_set_callback(nolog_callback) diff --git a/av/video/codeccontext.pxd b/av/video/codeccontext.pxd index 895ba74b1..8d128348a 100644 --- a/av/video/codeccontext.pxd +++ b/av/video/codeccontext.pxd @@ -16,18 +16,11 @@ cdef struct AVCodecPrivateData: cdef class VideoCodecContext(CodecContext): - cdef AVCodecPrivateData _private_data - cdef VideoFormat _format cdef _build_format(self) - cdef int last_w cdef int last_h cdef readonly VideoReformatter reformatter - - # For encoding. cdef readonly int encoded_frame_count - - # For decoding. cdef VideoFrame next_frame diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.py similarity index 69% rename from av/video/codeccontext.pyx rename to av/video/codeccontext.py index e82e56e91..b7842a7d5 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.py @@ -1,41 +1,56 @@ -cimport libav as lib -from libc.stdint cimport int64_t - -from av.codec.context cimport CodecContext -from av.codec.hwaccel cimport HWAccel, HWConfig -from av.error cimport err_check -from av.frame cimport Frame -from av.packet cimport Packet -from av.utils cimport avrational_to_fraction, to_avrational -from av.video.format cimport VideoFormat, get_pix_fmt, get_video_format -from av.video.frame cimport VideoFrame, alloc_video_frame -from av.video.reformatter cimport VideoReformatter - - -cdef lib.AVPixelFormat _get_hw_format(lib.AVCodecContext *ctx, const lib.AVPixelFormat *pix_fmts) noexcept: +import cython +import cython.cimports.libav as lib +from cython.cimports.av.codec.context import CodecContext +from cython.cimports.av.codec.hwaccel import HWAccel, HWConfig +from cython.cimports.av.error import err_check +from cython.cimports.av.frame import Frame +from cython.cimports.av.packet import Packet +from cython.cimports.av.utils import avrational_to_fraction, to_avrational +from cython.cimports.av.video.format import VideoFormat, get_pix_fmt, get_video_format +from cython.cimports.av.video.frame import VideoFrame, alloc_video_frame +from cython.cimports.av.video.reformatter import VideoReformatter +from cython.cimports.libc.stdint import int64_t + + +@cython.cfunc +@cython.exceptval(check=False) +def _get_hw_format( + ctx: cython.pointer[lib.AVCodecContext], + pix_fmts: cython.pointer[cython.const[lib.AVPixelFormat]], +) -> lib.AVPixelFormat: # In the case where we requested accelerated decoding, the decoder first calls this function # with a list that includes both the hardware format and software formats. # First we try to pick the hardware format if it's in the list. # However, if the decoder fails to initialize the hardware, it will call this function again, # with only software formats in pix_fmts. We return ctx->sw_pix_fmt regardless in this case, # because that should be in the candidate list. If not, we are out of ideas anyways. - cdef AVCodecPrivateData* private_data = ctx.opaque - i = 0 + private_data: cython.pointer[AVCodecPrivateData] = cython.cast( + cython.pointer[AVCodecPrivateData], ctx.opaque + ) + i: cython.int = 0 while pix_fmts[i] != -1: if pix_fmts[i] == private_data.hardware_pix_fmt: return pix_fmts[i] i += 1 - return ctx.sw_pix_fmt if private_data.allow_software_fallback else lib.AV_PIX_FMT_NONE - + return ( + ctx.sw_pix_fmt if private_data.allow_software_fallback else lib.AV_PIX_FMT_NONE + ) -cdef class VideoCodecContext(CodecContext): +@cython.cclass +class VideoCodecContext(CodecContext): def __cinit__(self, *args, **kwargs): self.last_w = 0 self.last_h = 0 - cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec, HWAccel hwaccel): - CodecContext._init(self, ptr, codec, hwaccel) # TODO: Can this be `super`? + @cython.cfunc + def _init( + self, + ptr: cython.pointer[lib.AVCodecContext], + codec: cython.pointer[cython.const[lib.AVCodec]], + hwaccel: HWAccel | None, + ): + CodecContext._init(self, ptr, codec, hwaccel) if hwaccel is not None: try: @@ -43,9 +58,13 @@ def __cinit__(self, *args, **kwargs): self.ptr.hw_device_ctx = lib.av_buffer_ref(self.hwaccel_ctx.ptr) self.ptr.pix_fmt = self.hwaccel_ctx.config.ptr.pix_fmt self.ptr.get_format = _get_hw_format - self._private_data.hardware_pix_fmt = self.hwaccel_ctx.config.ptr.pix_fmt - self._private_data.allow_software_fallback = self.hwaccel.allow_software_fallback - self.ptr.opaque = &self._private_data + self._private_data.hardware_pix_fmt = ( + self.hwaccel_ctx.config.ptr.pix_fmt + ) + self._private_data.allow_software_fallback = ( + self.hwaccel.allow_software_fallback + ) + self.ptr.opaque = cython.address(self._private_data) except NotImplementedError: # Some streams may not have a hardware decoder. For example, many action # cam videos have a low resolution mjpeg stream, which is usually not @@ -60,20 +79,20 @@ def __cinit__(self, *args, **kwargs): self._build_format() self.encoded_frame_count = 0 - cdef _prepare_frames_for_encode(self, Frame input): - if not input: + @cython.cfunc + def _prepare_frames_for_encode(self, input: Frame | None) -> list: + if input is None or not input: return [None] - cdef VideoFrame vframe = input - if self._format is None: raise ValueError("self._format is None, cannot encode") + vframe: VideoFrame = input # Reformat if it doesn't match. if ( - vframe.format.pix_fmt != self._format.pix_fmt or - vframe.width != self.ptr.width or - vframe.height != self.ptr.height + vframe.format.pix_fmt != self._format.pix_fmt + or vframe.width != self.ptr.width + or vframe.height != self.ptr.height ): if not self.reformatter: self.reformatter = VideoReformatter() @@ -84,49 +103,50 @@ def __cinit__(self, *args, **kwargs): # There is no pts, so create one. if vframe.ptr.pts == lib.AV_NOPTS_VALUE: - vframe.ptr.pts = self.encoded_frame_count + vframe.ptr.pts = cython.cast(int64_t, self.encoded_frame_count) self.encoded_frame_count += 1 - return [vframe] - cdef Frame _alloc_next_frame(self): + @cython.cfunc + def _alloc_next_frame(self) -> Frame: return alloc_video_frame() - cdef _setup_decoded_frame(self, Frame frame, Packet packet): + @cython.cfunc + def _setup_decoded_frame(self, frame: Frame, packet: Packet): CodecContext._setup_decoded_frame(self, frame, packet) - cdef VideoFrame vframe = frame + vframe: VideoFrame = frame vframe._init_user_attributes() - cdef _transfer_hwframe(self, Frame frame): + @cython.cfunc + def _transfer_hwframe(self, frame: Frame): if self.hwaccel_ctx is None: return frame - if frame.ptr.format != self.hwaccel_ctx.config.ptr.pix_fmt: # If we get a software frame, that means we are in software fallback mode, and don't actually # need to transfer. return frame - cdef Frame frame_sw - - frame_sw = self._alloc_next_frame() - + frame_sw: Frame = self._alloc_next_frame() err_check(lib.av_hwframe_transfer_data(frame_sw.ptr, frame.ptr, 0)) - - # TODO: Is there anything else to transfer?! + # TODO: Is there anything else to transfer? frame_sw.pts = frame.pts - return frame_sw - cdef _build_format(self): - self._format = get_video_format(self.ptr.pix_fmt, self.ptr.width, self.ptr.height) + @cython.cfunc + def _build_format(self): + self._format = get_video_format( + cython.cast(lib.AVPixelFormat, self.ptr.pix_fmt), + self.ptr.width, + self.ptr.height, + ) @property def format(self): return self._format @format.setter - def format(self, VideoFormat format): + def format(self, format: VideoFormat): self.ptr.pix_fmt = format.pix_fmt self.ptr.width = format.width self.ptr.height = format.height @@ -134,23 +154,23 @@ def format(self, VideoFormat format): @property def width(self): - if self.ptr is NULL: + if self.ptr is cython.NULL: return 0 return self.ptr.width @width.setter - def width(self, unsigned int value): + def width(self, value: cython.uint): self.ptr.width = value self._build_format() @property def height(self): - if self.ptr is NULL: + if self.ptr is cython.NULL: return 0 return self.ptr.height @height.setter - def height(self, unsigned int value): + def height(self, value: cython.uint): self.ptr.height = value self._build_format() @@ -160,13 +180,13 @@ def bits_per_coded_sample(self): The number of bits per sample in the codedwords. It's mandatory for this to be set for some formats to decode properly. Wraps :ffmpeg:`AVCodecContext.bits_per_coded_sample`. - + :type: int """ return self.ptr.bits_per_coded_sample - + @bits_per_coded_sample.setter - def bits_per_coded_sample(self, int value): + def bits_per_coded_sample(self, value: cython.int): if self.is_encoder: raise ValueError("Not supported for encoders") @@ -194,11 +214,11 @@ def framerate(self): :type: fractions.Fraction """ - return avrational_to_fraction(&self.ptr.framerate) + return avrational_to_fraction(cython.address(self.ptr.framerate)) @framerate.setter def framerate(self, value): - to_avrational(value, &self.ptr.framerate) + to_avrational(value, cython.address(self.ptr.framerate)) @property def rate(self): @@ -213,7 +233,7 @@ def rate(self, value): def gop_size(self): """ Sets the number of frames between keyframes. Used only for encoding. - + :type: int """ if self.is_decoder: @@ -221,29 +241,31 @@ def gop_size(self): return self.ptr.gop_size @gop_size.setter - def gop_size(self, int value): + def gop_size(self, value: cython.int): if self.is_decoder: raise RuntimeError("Cannot access 'gop_size' as a decoder") self.ptr.gop_size = value @property def sample_aspect_ratio(self): - return avrational_to_fraction(&self.ptr.sample_aspect_ratio) + return avrational_to_fraction(cython.address(self.ptr.sample_aspect_ratio)) @sample_aspect_ratio.setter def sample_aspect_ratio(self, value): - to_avrational(value, &self.ptr.sample_aspect_ratio) + to_avrational(value, cython.address(self.ptr.sample_aspect_ratio)) @property def display_aspect_ratio(self): - cdef lib.AVRational dar - + dar: lib.AVRational lib.av_reduce( - &dar.num, &dar.den, + cython.address(dar.num), + cython.address(dar.den), self.ptr.width * self.ptr.sample_aspect_ratio.num, - self.ptr.height * self.ptr.sample_aspect_ratio.den, 1024*1024) + self.ptr.height * self.ptr.sample_aspect_ratio.den, + 1024 * 1024, + ) - return avrational_to_fraction(&dar) + return avrational_to_fraction(cython.address(dar)) @property def has_b_frames(self):