396 lines
12 KiB
Python
396 lines
12 KiB
Python
import cython
|
|
import cython.cimports.libav as lib
|
|
from cython.cimports.av.codec.context import CodecContext
|
|
from cython.cimports.av.codec.hwaccel import HWAccel
|
|
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.
|
|
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
|
|
)
|
|
|
|
|
|
@cython.final
|
|
@cython.cclass
|
|
class VideoCodecContext(CodecContext):
|
|
@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:
|
|
self.hwaccel_ctx = hwaccel.create(self.codec)
|
|
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 = 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
|
|
# compatible with hardware decoders.
|
|
# The user may have passed in a hwaccel because they want to decode the main
|
|
# stream with it, so we shouldn't abort even if we find a stream that can't
|
|
# be HW decoded.
|
|
# If the user wants to make sure hwaccel is actually used, they can check with the
|
|
# is_hwaccel() function on each stream's codec context.
|
|
self.hwaccel_ctx = None
|
|
|
|
@cython.cfunc
|
|
def _prepare_frames_for_encode(self, input: Frame | None) -> list:
|
|
if input is None or not input:
|
|
return [None]
|
|
|
|
vframe: VideoFrame = input
|
|
if (
|
|
vframe.format.pix_fmt != self.pix_fmt
|
|
or vframe.width != self.ptr.width
|
|
or vframe.height != self.ptr.height
|
|
):
|
|
if not self.reformatter:
|
|
self.reformatter = VideoReformatter()
|
|
|
|
vframe = self.reformatter.reformat(
|
|
vframe,
|
|
self.ptr.width,
|
|
self.ptr.height,
|
|
self.format,
|
|
threads=self.ptr.thread_count,
|
|
)
|
|
|
|
if vframe.ptr.pts == lib.AV_NOPTS_VALUE:
|
|
vframe.ptr.pts = self.ptr.frame_num
|
|
|
|
return [vframe]
|
|
|
|
@cython.cfunc
|
|
def _alloc_next_frame(self) -> Frame:
|
|
return alloc_video_frame()
|
|
|
|
@cython.cfunc
|
|
def _setup_decoded_frame(self, frame: Frame, packet: Packet):
|
|
CodecContext._setup_decoded_frame(self, frame, packet)
|
|
vframe: VideoFrame = frame
|
|
vframe._init_user_attributes()
|
|
|
|
@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
|
|
|
|
if self.hwaccel_ctx.is_hw_owned:
|
|
cython.cast(VideoFrame, frame)._device_id = self.hwaccel_ctx.device_id
|
|
return 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?
|
|
frame_sw.pts = frame.pts
|
|
return frame_sw
|
|
|
|
@property
|
|
def format(self):
|
|
return get_video_format(
|
|
cython.cast(lib.AVPixelFormat, self.ptr.pix_fmt),
|
|
self.ptr.width,
|
|
self.ptr.height,
|
|
)
|
|
|
|
@format.setter
|
|
def format(self, format: VideoFormat):
|
|
self.ptr.pix_fmt = format.pix_fmt
|
|
self.ptr.width = format.width
|
|
self.ptr.height = format.height
|
|
|
|
@property
|
|
def width(self):
|
|
if self.ptr is cython.NULL:
|
|
return 0
|
|
return self.ptr.width
|
|
|
|
@width.setter
|
|
def width(self, value: cython.uint):
|
|
self.ptr.width = value
|
|
|
|
@property
|
|
def height(self):
|
|
if self.ptr is cython.NULL:
|
|
return 0
|
|
return self.ptr.height
|
|
|
|
@height.setter
|
|
def height(self, value: cython.uint):
|
|
self.ptr.height = value
|
|
|
|
@property
|
|
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, value: cython.int):
|
|
if self.is_encoder:
|
|
raise ValueError("Not supported for encoders")
|
|
|
|
self.ptr.bits_per_coded_sample = value
|
|
|
|
@property
|
|
def pix_fmt(self):
|
|
"""
|
|
The pixel format's name.
|
|
|
|
:type: str | None
|
|
"""
|
|
desc: cython.pointer[cython.const[lib.AVPixFmtDescriptor]] = (
|
|
lib.av_pix_fmt_desc_get(cython.cast(lib.AVPixelFormat, self.ptr.pix_fmt))
|
|
)
|
|
return cython.cast(str, desc.name)
|
|
|
|
@pix_fmt.setter
|
|
def pix_fmt(self, value):
|
|
self.ptr.pix_fmt = get_pix_fmt(value)
|
|
|
|
@property
|
|
def framerate(self):
|
|
"""
|
|
The frame rate, in frames per second.
|
|
|
|
:type: fractions.Fraction
|
|
"""
|
|
return avrational_to_fraction(cython.address(self.ptr.framerate))
|
|
|
|
@framerate.setter
|
|
def framerate(self, value):
|
|
to_avrational(value, cython.address(self.ptr.framerate))
|
|
|
|
@property
|
|
def rate(self):
|
|
"""Another name for :attr:`framerate`."""
|
|
return self.framerate
|
|
|
|
@rate.setter
|
|
def rate(self, value):
|
|
self.framerate = value
|
|
|
|
@property
|
|
def gop_size(self):
|
|
"""
|
|
Sets the number of frames between keyframes. Used only for encoding.
|
|
|
|
:type: int
|
|
"""
|
|
if self.is_decoder:
|
|
raise RuntimeError("Cannot access 'gop_size' as a decoder")
|
|
return self.ptr.gop_size
|
|
|
|
@gop_size.setter
|
|
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(cython.address(self.ptr.sample_aspect_ratio))
|
|
|
|
@sample_aspect_ratio.setter
|
|
def sample_aspect_ratio(self, value):
|
|
to_avrational(value, cython.address(self.ptr.sample_aspect_ratio))
|
|
|
|
@property
|
|
def display_aspect_ratio(self):
|
|
dar: lib.AVRational
|
|
lib.av_reduce(
|
|
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,
|
|
)
|
|
|
|
return avrational_to_fraction(cython.address(dar))
|
|
|
|
@property
|
|
def has_b_frames(self):
|
|
"""
|
|
:type: bool
|
|
"""
|
|
return bool(self.ptr.has_b_frames)
|
|
|
|
@property
|
|
def reorder_depth(self):
|
|
"""Raw ``has_b_frames`` value from FFmpeg (int, not bool).
|
|
|
|
After :meth:`flush_buffers`, FFmpeg may reset the internal reorder
|
|
heuristic. Set this to the known reorder depth *after* seeking to
|
|
avoid dropped hierarchical B-frames.
|
|
"""
|
|
return self.ptr.has_b_frames
|
|
|
|
@reorder_depth.setter
|
|
def reorder_depth(self, value: cython.int):
|
|
self.ptr.has_b_frames = value
|
|
|
|
@property
|
|
def coded_width(self):
|
|
"""
|
|
:type: int
|
|
"""
|
|
return self.ptr.coded_width
|
|
|
|
@property
|
|
def coded_height(self):
|
|
"""
|
|
:type: int
|
|
"""
|
|
return self.ptr.coded_height
|
|
|
|
@property
|
|
def color_range(self):
|
|
"""
|
|
Describes the signal range of the colorspace.
|
|
|
|
Wraps :ffmpeg:`AVFrame.color_range`.
|
|
|
|
:type: int
|
|
"""
|
|
return self.ptr.color_range
|
|
|
|
@color_range.setter
|
|
def color_range(self, value):
|
|
self.ptr.color_range = value
|
|
|
|
@property
|
|
def color_primaries(self):
|
|
"""
|
|
Describes the RGB/XYZ matrix of the colorspace.
|
|
|
|
Wraps :ffmpeg:`AVFrame.color_primaries`.
|
|
|
|
:type: int
|
|
"""
|
|
return self.ptr.color_primaries
|
|
|
|
@color_primaries.setter
|
|
def color_primaries(self, value):
|
|
self.ptr.color_primaries = value
|
|
|
|
@property
|
|
def color_trc(self):
|
|
"""
|
|
Describes the linearization function (a.k.a. transformation characteristics) of the colorspace.
|
|
|
|
Wraps :ffmpeg:`AVFrame.color_trc`.
|
|
|
|
:type: int
|
|
"""
|
|
return self.ptr.color_trc
|
|
|
|
@color_trc.setter
|
|
def color_trc(self, value):
|
|
self.ptr.color_trc = value
|
|
|
|
@property
|
|
def colorspace(self):
|
|
"""
|
|
Describes the YUV/RGB transformation matrix of the colorspace.
|
|
|
|
Wraps :ffmpeg:`AVFrame.colorspace`.
|
|
|
|
:type: int
|
|
"""
|
|
return self.ptr.colorspace
|
|
|
|
@colorspace.setter
|
|
def colorspace(self, value):
|
|
self.ptr.colorspace = value
|
|
|
|
@property
|
|
def max_b_frames(self):
|
|
"""
|
|
The maximum run of consecutive B frames when encoding a video.
|
|
|
|
:type: int
|
|
"""
|
|
return self.ptr.max_b_frames
|
|
|
|
@max_b_frames.setter
|
|
def max_b_frames(self, value):
|
|
self.ptr.max_b_frames = value
|
|
|
|
@property
|
|
def qmin(self):
|
|
"""
|
|
The minimum quantiser value of an encoded stream.
|
|
|
|
Wraps :ffmpeg:`AVCodecContext.qmin`.
|
|
|
|
:type: int
|
|
"""
|
|
return self.ptr.qmin
|
|
|
|
@qmin.setter
|
|
def qmin(self, value):
|
|
self.ptr.qmin = value
|
|
|
|
@property
|
|
def qmax(self):
|
|
"""
|
|
The maximum quantiser value of an encoded stream.
|
|
|
|
Wraps :ffmpeg:`AVCodecContext.qmax`.
|
|
|
|
:type: int
|
|
"""
|
|
return self.ptr.qmax
|
|
|
|
@qmax.setter
|
|
def qmax(self, value):
|
|
self.ptr.qmax = value
|