Skip to content

feat: decode video in hardware in the media player - #320

Open
devopvoid wants to merge 3 commits into
mainfrom
feat/media-hardware-decoding
Open

devopvoid wants to merge 3 commits into
mainfrom
feat/media-hardware-decoding

Conversation

@devopvoid

Copy link
Copy Markdown
Owner

Important

Only macOS was built and run. The Windows and Linux commits were written on a Mac, which has no Windows or Linux compiler. The CI jobs for those platforms compile them for the first time, and they need a run on a machine with a GPU decoder: see Needs testing.

A MediaPlayer, and a MediaFileSource, can be asked to decode H.264 and VP9 video on the media engine or GPU of the machine instead of the processor. It is off unless asked for.

MediaFileSource source = new MediaFileSource(path, true);

// or, for a player of your own
MediaPlayer player = new MediaPlayer(new MediaReader(path), videoSource, null, true);

boolean hardware = player.isHardwareDecoding();
Platform Hardware decoder Status
macOS VideoToolbox tested
Windows Direct3D 11, DXVA2 for what has no D3D11 decoder not built
Linux x86-64 and ARM64 NVDEC, on NVIDIA GPUs not built

API

  • MediaPlayer(reader, videoSource, audioSource, boolean hardwareDecoding), and MediaFileSource constructors with the flag. The existing constructors are unchanged and mean software.
  • MediaPlayer#isHardwareDecoding() tells what is in use. That can be less than was asked for, and it can turn false while playing, but never true again.

Behavior

  • The pictures are the same as software decoding gives. A decoded picture is read back from the hardware into system memory as NV12 and goes through the I420 conversion that already exists for formats WebRTC does not take.

  • What it saves is processor time, not the copy. Measured on an Apple M2 with synthetic media (noisy frames, so decoding is not trivial), process CPU time per frame:

    Stream Software, all cores Hardware
    H.264 1080p 11.6 ms 1.43 ms
    H.264 4K 35.7 ms 4.15 ms
    VP9 1080p 10.2 ms 1.48 ms

    About seven to eight times less. On the clock, multi-threaded software is two to three times faster per frame, which does not matter at playback speed. These are figures for one machine, not a promise; the guide says so.

  • Fallback to software, without the player noticing more than isHardwareDecoding() turning false:

    • a codec with no hardware decoder (VP8, MPEG-4, MJPEG, and H.265, which stays in software),
    • a stream FFmpeg does not offer the hardware for, such as VP9 profile 1 (4:4:4),
    • a decoder that fails. Until the hardware has produced its first frame, the packets sent to it are kept (up to 32) and decoded again in software if it fails, so none is lost. After the first frame, a failure goes on from the next key frame.
      The fallback lives in VideoDecoder: an error from it would end playback.

Native side

  • VideoDecoder is the only place that knows. It picks the hardware device type of the platform (VideoToolbox, D3D11VA then DXVA2, CUDA), sets a get_format callback that takes the hardware pixel format where it is offered, and brings hardware frames into system memory. MediaPlayer gets SetHardwareDecoding and IsHardwareDecoding, and the JNI create a flag and a getter.
  • FFmpeg build (dependencies/ffmpeg/CMakeLists.txt), H.264 and VP9 only:
    • macOS: --enable-videotoolbox, h264_videotoolbox, vp9_videotoolbox
    • Windows: --enable-d3d11va --enable-dxva2, h264_d3d11va2, vp9_d3d11va2, h264_dxva2, vp9_dxva2. FFmpeg loads d3d11.dll, dxgi.dll, d3d9.dll and dxva2.dll itself when a device is created.
    • Linux x86-64 and ARM64: --enable-ffnvcodec --enable-nvdec, h264_nvdec, vp9_nvdec. FFmpeg loads libcuda.so.1 and libnvcuvid.so.1 with dlopen, so the libraries load on a machine with no NVIDIA driver.
      The list of components changed, so the next build of every platform rebuilds FFmpeg once. The WebRTC caches are not affected.
  • dependencies/ffnvcodec (new): FFmpeg needs the headers of nv-codec-headers for NVDEC and finds them with pkg-config. The four headers dynlink_cuda.h, dynlink_cuviddec.h, dynlink_nvcuvid.h and dynlink_loader.h are taken unchanged from tag n12.0.16.1, the version of nvEncodeAPI.h that webrtc-jni vendors for NVENC and one FFmpeg n8.1 accepts (ffnvcodec >= 12.0.16.1 ffnvcodec < 12.1). nvEncodeAPI.h is copied from the NVENC directory, since configure checks for it too. The pkg-config file is ours; CMake fills in the path. The licenses of the files are collected in LICENSE and installed into the Linux platform jars under META-INF/licenses/ffnvcodec. The build needs pkg-config on Linux, which the runners have, and says so where it is missing.
  • Left out on purpose: 32-bit ARM, for which FFmpeg's own configure disables NVDEC (it allows it on x86 and little-endian aarch64 Linux only); VA-API, because FFmpeg links libva and it would become a requirement of the library; H.265 and AV1.

Testing

  • HardwareDecodingTest (new, 7 tests), with media made for it: the existing assets are VP8 and MS-MPEG4, for which no platform has a decoder. media-test-h264.mkv is H.264 High from VideoToolbox's encoder, media-test-vp9.webm is VP9 from libvpx, media-test-vp9-444.webm the same in profile 1. 320x240, 15 fps, two seconds, with a key frame every second.
    • h264MatchesSoftware, vp9MatchesSoftware: the file is played with and without the flag, and the CRC of the luma of every frame is the same.
    • seeksInHardware: the decoder is flushed by a seek and goes on in hardware.
    • codecWithoutHardwareFallsBack: the VP8 asset, no hardware decoder, plays in software.
    • streamTheHardwareDoesNotTakeFallsBack: VP9 4:4:4, asked for hardware, plays in software with the same pictures.
    • fileSourceTakesTheOption, softwareIsTheDefault.
      A machine without a hardware decoder skips the tests that need one; -Dwebrtc.test.hardwareDecoding=true makes them fail instead.
  • On an Apple M2 (macOS 14.5), with that property, all 53 tests of the module pass, the existing ones included.
  • The test media were wrapped in Matroska with a small script that is not part of the repo; how they were made is in the Javadoc of the test.

Not verified yet:

  • Everything on Windows and Linux: that FFmpeg configures and builds with the new options, the pkg-config hand-over in the ARM64 cross build, decoding, and the comparison with software. On Linux, configure and the header set were checked on a Mac against FFmpeg's real configure with a stand-in for pkg-config: the version constraint is accepted and the four headers compile together. Whether FFmpeg then builds the NVDEC code for Linux could not be seen there.
  • The macOS x86-64 cross build (CI cross compiles it; only arm64 was built here).
  • The case where the hardware fails after FFmpeg has offered it. There was no stream to provoke it; the related case, no hardware format offered, is tested.
  • Real content (a camera, a screen) and RTSP with packet loss; the figures are from synthetic media.
  • CI runners are virtualized or have no GPU, so the hardware tests are skipped there, or fall back, which should not turn them red.
  • Whether drivers on other hardware give exactly the same pictures. The tests compare every frame; a driver that differs would show up there.

Needs testing

On a machine with a GPU decoder (a recent AMD, Intel or NVIDIA GPU on Windows; an NVIDIA GPU on Linux):

mvn -pl webrtc-java-media -am verify -Dtest=HardwareDecodingTest -Dwebrtc.test.hardwareDecoding=true

This fails unless the player decodes in hardware, and the frames are compared with software. The module's tests only run from the reactor (-am): started alone they use the artifacts in ~/.m2, which can be from other builds.

Docs

docs/guide/media/media-files.md has a new section, "Decoding in Hardware": the flag, the platforms, the fallback, and the measurements as measurements.

A MediaPlayer, and a MediaFileSource, can be asked to decode H.264 and VP9
video on the media engine of the machine instead of the processor. It is off
unless asked for: a new constructor takes the flag, the existing ones mean
software, and MediaPlayer.isHardwareDecoding() says what is in use.

On macOS that is VideoToolbox, through FFmpeg's hwaccels, which the FFmpeg
build now has for the two codecs (h264_videotoolbox, vp9_videotoolbox). The
list of components changed, so the next build of the platform rebuilds
FFmpeg. Elsewhere the flag is accepted and the video is decoded in software
until the build has hardware decoders for the platform.

VideoDecoder does the work, and is the only place that knows. A decoded
picture is read back into system memory as NV12 and goes through the I420
conversion that already exists for other formats, so what reaches WebRTC is
what software decoding gives; the tests compare every frame. What hardware
saves is processor time: measured on an Apple M2, about seven to eight times
less for 1080p and 4K H.264 and for 1080p VP9.

A stream the hardware does not take is decoded in software, and
isHardwareDecoding turns false: a codec without a hardware decoder, a stream
whose pixel format FFmpeg does not offer the hardware for (VP9 profile 1), or
a decoder that fails. Until the hardware has produced its first frame, the
packets sent to it are kept, and decoded again in software if it fails, so
none is lost. After the first frame, a failure continues from the next key
frame. The fallback lives in VideoDecoder, because a decode error from the
decoder would end playback.

Tests: new media for H.264 and VP9 (VP9 profile 1 among them), since the
existing assets are VP8 and MS-MPEG4. A machine without a hardware decoder
skips the tests that need one; -Dwebrtc.test.hardwareDecoding=true makes them
fail instead.
The FFmpeg build for Windows now has the Direct3D 11 and DXVA2 hwaccels for
H.264 and VP9 (h264_d3d11va2, vp9_d3d11va2, h264_dxva2, vp9_dxva2), so a
player asked to decode in hardware does on Windows what it does on macOS.
VideoDecoder needed nothing: it already tries Direct3D 11 and then DXVA2 as
the device types of the platform, and reads the decoded picture back into
system memory the same way.

FFmpeg loads d3d11.dll, dxgi.dll, d3d9.dll and dxva2.dll itself when a device
is created, so the libraries load on a machine with no GPU decoder, and the
build needs the Windows SDK headers only, which it has.

This has not been built or run on Windows. The option names were checked
against what configure lists; nothing more could be on a Mac. The hardware
tests of the module run there as they do on macOS, and compare every frame
with software decoding.
The FFmpeg build for Linux on x86-64 and ARM64 now has the NVDEC hwaccels
for H.264 and VP9 (h264_nvdec, vp9_nvdec), so a player asked to decode in
hardware does on an NVIDIA GPU what it does on macOS and Windows.
VideoDecoder needed nothing: CUDA is its device type on Linux, and it reads
the decoded picture back into system memory the same way.

FFmpeg decodes with NVDEC through the headers of nv-codec-headers, which it
finds with pkg-config. dependencies/ffnvcodec carries them, at tag
n12.0.16.1: the version of nvEncodeAPI.h webrtc-jni already vendors for
NVENC, and one FFmpeg n8.1 accepts. The files are unchanged; the
pkg-config file is ours, and CMake fills in the path and hands it to
configure. The build needs pkg-config, which the CI runners have, and says
so where it is missing. The licenses of the headers are installed into the
Linux platform jars under META-INF/licenses/ffnvcodec.

FFmpeg loads libcuda.so.1 and libnvcuvid.so.1 with dlopen when a device is
created, so the libraries load on a machine with no NVIDIA driver, and a
player asked for hardware there decodes in software. 32-bit ARM stays out,
as it does in FFmpeg's own configure, which disables NVDEC for every target
but x86 and little-endian aarch64 Linux. VA-API stays out too: FFmpeg links
libva, which would make it a requirement of the library.

This has not been built or run on Linux. configure and the header set were
checked on a Mac, against FFmpeg's real configure with a stand-in for
pkg-config: the version constraint is accepted, and the four headers compile
together. Whether FFmpeg then builds the NVDEC code for Linux could not be
seen there.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant