Skip to content

Commit

Permalink
Implement hardware decoding
Browse files Browse the repository at this point in the history
This implements hardware decoding continuing from the work of @rvillalba-novetta and @mikeboers
with cleanup work by @WyattBlue.
  • Loading branch information
matthewlai authored Dec 17, 2024
1 parent 6eaf701 commit 2c63608
Show file tree
Hide file tree
Showing 29 changed files with 649 additions and 23 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,16 @@ We are operating with `semantic versioning <https://semver.org>`_.
are merged into the "default" branch.


v14.1.0 (Unreleased)
--------------------

Features

- Add hardware decoding by :gh-user:`matthewlai` and :gh-user:`WyattBlue` in (:pr:`1685`).


v14.0.1
-------

Fixes:

Expand Down
1 change: 1 addition & 0 deletions av/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from av.bitstream import BitStreamFilterContext, bitstream_filters_available
from av.codec.codec import Codec, codecs_available
from av.codec.context import CodecContext
from av.codec.hwaccel import HWConfig
from av.container import open
from av.format import ContainerFormat, formats_available
from av.packet import Packet
Expand Down
14 changes: 14 additions & 0 deletions av/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--codecs", action="store_true")
parser.add_argument("--hwdevices", action="store_true")
parser.add_argument("--hwconfigs", action="store_true")
parser.add_argument("--version", action="store_true")
args = parser.parse_args()

Expand All @@ -30,6 +32,18 @@ def main() -> None:
version = config["version"]
print(f"{libname:<13} {version[0]:3d}.{version[1]:3d}.{version[2]:3d}")

if args.hwdevices:
from av.codec.hwaccel import hwdevices_available

print("Hardware device types:")
for x in hwdevices_available():
print(" ", x)

if args.hwconfigs:
from av.codec.codec import dump_hwconfigs

dump_hwconfigs()

if args.codecs:
from av.codec.codec import dump_codecs

Expand Down
2 changes: 1 addition & 1 deletion av/about.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "14.0.1"
__version__ = "14.1.0"
5 changes: 3 additions & 2 deletions av/audio/codeccontext.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ cimport libav as lib
from av.audio.format cimport AudioFormat, get_audio_format
from av.audio.frame cimport AudioFrame, alloc_audio_frame
from av.audio.layout cimport AudioLayout, get_audio_layout
from av.codec.hwaccel cimport HWAccel
from av.frame cimport Frame
from av.packet cimport Packet


cdef class AudioCodecContext(CodecContext):
cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec):
CodecContext._init(self, ptr, codec)
cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec, HWAccel hwaccel):
CodecContext._init(self, ptr, codec, hwaccel)

cdef _prepare_frames_for_encode(self, Frame input_frame):

Expand Down
2 changes: 2 additions & 0 deletions av/codec/codec.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ cdef class Codec:
cdef const lib.AVCodecDescriptor *desc
cdef readonly bint is_encoder

cdef tuple _hardware_configs

cdef _init(self, name=?)


Expand Down
1 change: 1 addition & 0 deletions av/codec/codec.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,4 @@ class codec_descriptor:
codecs_available: set[str]

def dump_codecs() -> None: ...
def dump_hwconfigs() -> None: ...
38 changes: 38 additions & 0 deletions av/codec/codec.pyx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from av.audio.format cimport get_audio_format
from av.codec.hwaccel cimport wrap_hwconfig
from av.descriptor cimport wrap_avclass
from av.utils cimport avrational_to_fraction
from av.video.format cimport get_video_format
Expand Down Expand Up @@ -117,6 +118,10 @@ cdef class Codec:
if self.is_encoder and lib.av_codec_is_decoder(self.ptr):
raise RuntimeError("%s is both encoder and decoder.")

def __repr__(self):
mode = "w" if self.is_encoder else "r"
return f"<av.{self.__class__.__name__} {self.name} {mode=}>"

def create(self, kind = None):
"""Create a :class:`.CodecContext` for this codec.
Expand Down Expand Up @@ -203,6 +208,23 @@ cdef class Codec:
i += 1
return ret

@property
def hardware_configs(self):
if self._hardware_configs:
return self._hardware_configs
ret = []
cdef int i = 0
cdef lib.AVCodecHWConfig *ptr
while True:
ptr = lib.avcodec_get_hw_config(self.ptr, i)
if not ptr:
break
ret.append(wrap_hwconfig(ptr))
i += 1
ret = tuple(ret)
self._hardware_configs = ret
return ret

@property
def properties(self):
return self.desc.props
Expand Down Expand Up @@ -337,3 +359,19 @@ def dump_codecs():
)
except Exception as e:
print(f"...... {codec.name:<18} ERROR: {e}")

def dump_hwconfigs():
print("Hardware configs:")
for name in sorted(codecs_available):
try:
codec = Codec(name, "r")
except ValueError:
continue

configs = codec.hardware_configs
if not configs:
continue

print(" ", codec.name)
for config in configs:
print(" ", config)
12 changes: 9 additions & 3 deletions av/codec/context.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ from libc.stdint cimport int64_t

from av.bytesource cimport ByteSource
from av.codec.codec cimport Codec
from av.codec.hwaccel cimport HWAccel
from av.frame cimport Frame
from av.packet cimport Packet

Expand All @@ -18,11 +19,12 @@ cdef class CodecContext:
cdef int stream_index

cdef lib.AVCodecParserContext *parser
cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec)
cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec, HWAccel hwaccel)

# Public API.
cdef readonly bint is_open
cdef readonly Codec codec
cdef readonly HWAccel hwaccel
cdef public dict options
cpdef open(self, bint strict=?)

Expand All @@ -31,6 +33,9 @@ cdef class CodecContext:
cpdef decode(self, Packet packet=?)
cpdef flush_buffers(self)

# Used by hardware-accelerated decode.
cdef HWAccel hwaccel_ctx

# Used by both transcode APIs to setup user-land objects.
# 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.
Expand All @@ -49,10 +54,11 @@ cdef class CodecContext:
cdef _send_packet_and_recv(self, Packet packet)
cdef _recv_frame(self)

cdef _transfer_hwframe(self, Frame frame)

# Implemented by children for the generic send/recv API, so we have the
# correct subclass of Frame.
cdef Frame _next_frame
cdef Frame _alloc_next_frame(self)


cdef CodecContext wrap_codec_context(lib.AVCodecContext*, const lib.AVCodec*)
cdef CodecContext wrap_codec_context(lib.AVCodecContext*, const lib.AVCodec*, HWAccel hwaccel)
7 changes: 6 additions & 1 deletion av/codec/context.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ from typing import ClassVar, Literal
from av.packet import Packet

from .codec import Codec
from .hwaccel import HWAccel

class ThreadType(Flag):
NONE: ClassVar[ThreadType]
Expand Down Expand Up @@ -83,10 +84,14 @@ class CodecContext:
def delay(self) -> bool: ...
@property
def extradata_size(self) -> int: ...
@property
def is_hwaccel(self) -> bool: ...
def open(self, strict: bool = True) -> None: ...
@staticmethod
def create(
codec: str | Codec, mode: Literal["r", "w"] | None = None
codec: str | Codec,
mode: Literal["r", "w"] | None = None,
hwaccel: HWAccel | None = None,
) -> CodecContext: ...
def parse(
self, raw_input: bytes | bytearray | memoryview | None = None
Expand Down
23 changes: 18 additions & 5 deletions av/codec/context.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ from av.dictionary import Dictionary
cdef object _cinit_sentinel = object()


cdef CodecContext wrap_codec_context(lib.AVCodecContext *c_ctx, const lib.AVCodec *c_codec):
cdef CodecContext wrap_codec_context(lib.AVCodecContext *c_ctx, const lib.AVCodec *c_codec, HWAccel hwaccel):
"""Build an av.CodecContext for an existing AVCodecContext."""

cdef CodecContext py_ctx
Expand All @@ -35,7 +35,7 @@ cdef CodecContext wrap_codec_context(lib.AVCodecContext *c_ctx, const lib.AVCode
else:
py_ctx = CodecContext(_cinit_sentinel)

py_ctx._init(c_ctx, c_codec)
py_ctx._init(c_ctx, c_codec, hwaccel)

return py_ctx

Expand Down Expand Up @@ -83,10 +83,10 @@ class Flags2(IntEnum):

cdef class CodecContext:
@staticmethod
def create(codec, mode=None):
def create(codec, mode=None, hwaccel=None):
cdef Codec cy_codec = codec if isinstance(codec, Codec) else Codec(codec, mode)
cdef lib.AVCodecContext *c_ctx = lib.avcodec_alloc_context3(cy_codec.ptr)
return wrap_codec_context(c_ctx, cy_codec.ptr)
return wrap_codec_context(c_ctx, cy_codec.ptr, hwaccel)

def __cinit__(self, sentinel=None, *args, **kwargs):
if sentinel is not _cinit_sentinel:
Expand All @@ -96,11 +96,12 @@ cdef class CodecContext:
self.stream_index = -1 # This is set by the container immediately.
self.is_open = False

cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec):
cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec, HWAccel hwaccel):
self.ptr = ptr
if self.ptr.codec and codec and self.ptr.codec != codec:
raise RuntimeError("Wrapping CodecContext with mismatched codec.")
self.codec = wrap_codec(codec if codec != NULL else self.ptr.codec)
self.hwaccel = hwaccel

# Set reasonable threading defaults.
self.ptr.thread_count = 0 # use as many threads as there are CPUs.
Expand Down Expand Up @@ -310,6 +311,13 @@ cdef class CodecContext:

return packets

@property
def is_hwaccel(self):
"""
Returns ``True`` if this codec context is hardware accelerated, ``False`` otherwise.
"""
return self.hwaccel_ctx is not None

def _send_frame_and_recv(self, Frame frame):
cdef Packet packet

Expand Down Expand Up @@ -359,10 +367,15 @@ cdef class CodecContext:
return
err_check(res)

frame = self._transfer_hwframe(frame)

if not res:
self._next_frame = None
return frame

cdef _transfer_hwframe(self, Frame frame):
return frame

cdef _recv_packet(self):
cdef Packet packet = Packet()

Expand Down
20 changes: 20 additions & 0 deletions av/codec/hwaccel.pxd
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
cimport libav as lib

from av.codec.codec cimport Codec


cdef class HWConfig:
cdef object __weakref__
cdef lib.AVCodecHWConfig *ptr
cdef void _init(self, lib.AVCodecHWConfig *ptr)

cdef HWConfig wrap_hwconfig(lib.AVCodecHWConfig *ptr)

cdef class HWAccel:
cdef int _device_type
cdef str _device
cdef readonly Codec codec
cdef readonly HWConfig config
cdef lib.AVBufferRef *ptr
cdef public bint allow_software_fallback
cdef public dict options
48 changes: 48 additions & 0 deletions av/codec/hwaccel.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from enum import IntEnum

from av.codec.codec import Codec
from av.video.format import VideoFormat

class HWDeviceType(IntEnum):
none: int
vdpau: int
cuda: int
vaapi: int
dxva2: int
qsv: int
videotoolbox: int
d3d11va: int
drm: int
opencl: int
mediacodec: int
vulkan: int
d3d12va: int

class HWConfigMethod(IntEnum):
none: int
hw_device_ctx: int
hw_frame_ctx: int
internal: int
ad_hoc: int

class HWConfig:
@property
def device_type(self) -> HWDeviceType: ...
@property
def format(self) -> VideoFormat: ...
@property
def methods(self) -> HWConfigMethod: ...
@property
def is_supported(self) -> bool: ...

class HWAccel:
def __init__(
self,
device_type: str | HWDeviceType,
device: str | None = None,
allow_software_fallback: bool = False,
options: dict[str, object] | None = None,
) -> None: ...
def create(self, codec: Codec) -> HWAccel: ...

def hwdevices_available() -> list[str]: ...
Loading

0 comments on commit 2c63608

Please sign in to comment.