From d0ef59616f982212bd4ec23c9d47b82a3d18ee99 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Tue, 10 Mar 2026 23:34:21 -0500 Subject: [PATCH] Improving Hardware decode handling, especially on failure to get a decoded frame from the GPU, to safely fallback to software decode in those cases, so we don't end up with a black blank frame. This should be compatible with all hardware decoding, but I tested this only on VAAPI and AMD Radeon GPU on Ubuntu 24.04. Also updating the HW-ACCEL.md file with updated notes, and tried to add more detail about how decode and decode fallback works. --- doc/HW-ACCEL.md | 306 ++++++++++++++++++++++++++++++----------- src/FFmpegReader.cpp | 180 ++++++++++++++++++++++-- src/FFmpegReader.h | 10 ++ src/ReaderBase.h | 4 + tests/FFmpegReader.cpp | 185 +++++++++++++++++++++++++ 5 files changed, 592 insertions(+), 93 deletions(-) diff --git a/doc/HW-ACCEL.md b/doc/HW-ACCEL.md index e01513cc..4960777b 100644 --- a/doc/HW-ACCEL.md +++ b/doc/HW-ACCEL.md @@ -6,55 +6,187 @@ SPDX-License-Identifier: LGPL-3.0-or-later ## Hardware Acceleration -OpenShot now has experimental support for hardware acceleration, which uses 1 (or more) -graphics cards to offload some of the work for both decoding and encoding. This is -very new and experimental (as of May 2019), but we look forward to "accelerating" -our support for this in the future! +Hardware acceleration in libopenshot allows FFmpeg to use platform-specific GPU +APIs for video decode and encode when available. In practice, this means some of +the work that would otherwise be done by the CPU can be offloaded to the GPU or +to dedicated media blocks on the GPU. -The following table summarizes our current level of support: +This document focuses on what hardware acceleration in libopenshot does today, +how it fits into the current processing pipeline, and what users and developers +should expect from it. -| | Linux Decode | Linux Encode | Mac Decode | Mac Encode | Windows Decode | Windows Encode | Notes | -|--------------------|:---------------:|:--------------:|:----------:|:--------------:|:--------------:|:--------------:|------------------| -| VA-API | ✔️   | ✔️   | - | - | - | - | *Linux Only* | -| VDPAU | ✔️ 1 | ✅ 2 | - | - | - | - | *Linux Only* | -| CUDA (NVDEC/NVENC) | ❌ 3 | ✔️   | - | - | - | ✔️   | *Cross Platform* | -| VideoToolBox | - | - | ✔️   | ❌ 4 | - | - | *Mac Only* | -| DXVA2 | - | - | - | - | ❌ 3 | - | *Windows Only* | -| D3D11VA | - | - | - | - | ❌ 3 | - | *Windows Only* | -| QSV | ❌ 3 | ❌   | ❌   | ❌   | ❌   | ❌   | *Cross Platform* | +## Backend Overview + +The following table summarizes the historically supported hardware-acceleration +backends in libopenshot. Actual behavior still depends on FFmpeg build options, +driver availability, operating system support, and the runtime environment. + +| | Linux Decode | Linux Encode | macOS Decode | macOS Encode | Windows Decode | Windows Encode | Notes | +|--------------------|:------------:|:------------:|:------------:|:------------:|:--------------:|:--------------:|-------| +| VA-API | ✔️ | ✔️ | - | - | - | - | Linux only | +| VDPAU | ✔️ 1 | ✅ 2 | - | - | - | - | Linux only | +| CUDA (NVDEC/NVENC) | ❌ 3 | ✔️ | - | - | - | ✔️ | Backend availability depends on the FFmpeg build | +| VideoToolbox | - | - | ✔️ | ❌ 4 | - | - | macOS only | +| DXVA2 | - | - | - | - | ❌ 3 | - | Windows only | +| D3D11VA | - | - | - | - | ❌ 3 | - | Windows only | +| QSV | ❌ 3 | ❌ | ❌ | ❌ | ❌ | ❌ | Backend availability depends on the FFmpeg build | #### Notes -1. VDPAU for some reason needs a card number one higher than it really is -2. VDPAU is a decoder only -3. Green frames (pixel data not correctly tranferred back to system memory) -4. Crashes and burns +1. VDPAU historically needed a card number one higher than expected. +2. VDPAU is decode-only. +3. Historically associated with failed transfers, corrupt frames, or unusable output on some setups. +4. Historically unstable. + +This table should be read as a support map, not a guarantee that every backend +is fully validated on every current OS/driver combination. + +## Why Hardware Acceleration Exists + +Hardware acceleration is useful for two main reasons: + +* It can reduce CPU load during decode and encode. +* It can improve throughput for some media, especially on systems with strong + hardware video support. + +However, hardware acceleration is not automatically faster for every file or on +every system. The real result depends on codec support, driver quality, stream +format, pixel format, resolution, frame rate, and how much CPU-side work still +needs to happen after decode. + +## What libopenshot Uses Hardware Acceleration For + +Today, hardware acceleration in libopenshot is primarily used for: + +* video decode +* video encode + +It is not currently used to keep the entire edit/render pipeline on the GPU. +Decoded frames usually still need to be copied back into system memory for +colorspace conversion, scaling, caching, effect processing, compositing, and +timeline rendering. + +That detail is important because it explains why hardware decode does not always +produce a speedup. + +## Decode Flow in libopenshot + +The current decode flow looks roughly like this: + +1. A hardware decoder may be requested through `Settings::HARDWARE_DECODER`. +2. FFmpeg opens the requested hardware decode path if the backend and driver + support it. +3. The decoder produces a frame, either: + * directly as a software-readable frame, or + * as a hardware frame that must be transferred to system memory. +4. libopenshot converts that frame into the CPU-side image representation used + by the rest of the pipeline. + +If hardware decode fails during startup decode or frame transfer, libopenshot +falls back to software decode for that reader instead of returning corrupt, +green, or black frames. + +## Fallback Behavior + +Hardware decode is best-effort, not all-or-nothing. + +If a hardware decoder is requested and one of the following happens early in the +decode path: + +* repeated startup decode failures +* failed hardware-frame transfer +* invalid transferred frame data +* failed software conversion of a transferred frame + +libopenshot reopens that reader in software decode mode and continues decoding. + +This behavior is intentionally conservative. The priority is correctness and +stability: + +* valid frames are better than corrupt frames +* software fallback is better than black or green output +* a file that cannot be decoded by one hardware backend should still decode if + CPU decoding can handle it + +For diagnostics and UI checks, this means there is a difference between: + +* decode succeeded +* hardware decode actually produced frames +* hardware decode failed and software fallback was used + +`FFmpegReader::HardwareDecodeSuccessful()` exists to expose that distinction. + +## Performance Expectations + +Hardware decode is not guaranteed to be faster than software decode. + +In libopenshot's current pipeline, decoded frames are brought back to +system memory immediately after decode. That introduces costs that can erase or +outweigh the raw decode benefit: + +* hardware device setup overhead +* frame transfer overhead between GPU and CPU memory +* colorspace conversion and scaling after decode +* caching and image wrapping in CPU memory +* container/seek behavior and stream structure + +Because of that, hardware decode performance is workload-dependent. + +General guidance: + +* some files benefit from hardware decode +* some files are effectively neutral +* some files are slower with hardware decode +* files with similar codec and resolution can still behave differently + +Hardware acceleration should be treated as a capability that may help, not as a +guarantee of better performance. + +## Why Some Files Fail on Hardware Decode + +A file can fail on hardware decode for several reasons: + +* unsupported codec profile +* unsupported chroma format or pixel format +* unsupported bit depth or color range +* driver/backend limitations +* FFmpeg/backend integration quirks on a specific platform + +For example, consumer hardware decode paths often handle H.264 4:2:0 very well, +but may not support H.264 4:2:2 decode reliably. In those cases, software decode +may work perfectly while hardware decode fails. ## Supported FFmpeg Versions -* HW accel is supported from FFmpeg version 3.4 -* HW accel was removed for nVidia drivers in Ubuntu for FFmpeg 4+ +* Hardware acceleration support requires FFmpeg versions new enough to expose the + relevant hardware APIs to libopenshot. +* In practice, decode support in libopenshot relies on FFmpeg's modern send/receive + decode API and hardware-frame APIs. +* Actual backend availability depends on how FFmpeg was compiled on the target system. -**Notice:** The FFmpeg versions of Ubuntu and PPAs for Ubuntu show the -same behaviour. FFmpeg 3 has working nVidia hardware acceleration while -FFmpeg 4+ has no support for nVidia hardware acceleration -included. +Older historical note: + +* Some Ubuntu/FFmpeg/NVIDIA combinations behaved differently between FFmpeg 3.x + and FFmpeg 4.x, especially for NVIDIA decode support. + +Because backend support has changed over time, always validate against the +actual FFmpeg build and driver stack in use. ## OpenShot Settings -The following settings are use by libopenshot to enable, disable, and control -the various hardware acceleration features. +The following settings are used by libopenshot to enable, disable, and control +hardware acceleration features. -```{cpp} +```cpp /// Use video codec for faster video decoding (if supported) int HARDWARE_DECODER = 0; /* 0 - No acceleration 1 - Linux VA-API - 2 - nVidia NVDEC + 2 - NVIDIA NVDEC 3 - Windows D3D9 4 - Windows D3D11 - 5 - MacOS / VideoToolBox + 5 - macOS / VideoToolbox 6 - Linux VDPAU 7 - Intel QSV */ @@ -70,73 +202,91 @@ int DE_LIMIT_HEIGHT_MAX = 1100; /// Maximum columns that hardware decode can handle int DE_LIMIT_WIDTH_MAX = 1950; -/// Which GPU to use to decode (0 is the first, LINUX ONLY) +/// Which GPU to use to decode (0 is the first, Linux only) int HW_DE_DEVICE_SET = 0; -/// Which GPU to use to encode (0 is the first, LINUX ONLY) +/// Which GPU to use to encode (0 is the first, Linux only) int HW_EN_DEVICE_SET = 0; ``` -## Libva / VA-API (Video Acceleration API) +## Platform Notes -The correct version of libva is needed (libva in Ubuntu 16.04 or libva2 -in Ubuntu 18.04) for the AppImage to work with hardware acceleration. -An AppImage that works on both systems (supporting libva and libva2), -might be possible when no libva is included in the AppImage. +### Linux / VA-API -* vaapi is working for intel and AMD -* vaapi is working for decode only for nouveau -* nVidia driver is working for export only +VA-API is one of the primary Linux hardware-decode paths used by libopenshot. +On supported Intel and AMD systems it can work well, but not every file format, +codec profile, or pixel format is supported by every driver. -## AMD Graphics Cards (RadeonOpenCompute/ROCm) +### Linux / VDPAU -Decoding and encoding on the (AMD) GPU is possible with the default drivers. -On systems where ROCm is installed and run a future use for GPU acceleration -of effects could be implemented (contributions welcome). +VDPAU support exists historically, but behavior can vary with driver and FFmpeg +stack. Treat it as backend-dependent rather than universally reliable. + +### NVIDIA + +NVIDIA hardware encode support has historically been more reliable than decode +support in libopenshot, depending on FFmpeg build and driver stack. Validate the +actual runtime environment before assuming support. + +### macOS / VideoToolbox + +VideoToolbox support exists, but stability and feature coverage should be tested +carefully on the target FFmpeg/macOS version. + +### Windows / DXVA2 / D3D11VA + +Windows decode backends are highly dependent on FFmpeg build options and device +support. They should be treated as runtime-validated features, not assumptions. ## Multiple Graphics Cards -If the computer has multiple graphics cards installed, you can choose which -should be used by libopenshot. Also, you can optionally use one card for -decoding and the other for encoding (if both cards support acceleration). -This is currently only supported on Linux, due to the device name FFmpeg -expects (i.e. **/dev/dri/render128**). Contributions welcome if anyone can -determine what string format to pass for Windows and Mac. +If the computer has multiple graphics cards installed, libopenshot can choose +which device should be used for decode and encode. This is currently practical +mainly on Linux, where FFmpeg expects device names such as `/dev/dri/renderD128`. -## Help Us Improve Hardware Support +Contributions are welcome for improving cross-platform device enumeration and +selection. -This information might be wrong, and we would love to continue improving -our support for hardware acceleration in OpenShot. Please help us update -this document if you find an error or discover new and/or useful information. +## Testing and Validation -**FFmpeg 4 + nVidia** The manual at: -https://www.tal.org/tutorials/ffmpeg_nvidia_encode -works pretty well. We could compile and install a version of FFmpeg 4.1.3 -on Mint 19.1 that supports the GPU on nVidia cards. A version of openshot -with hardware support using these libraries could use the nVidia GPU. +When validating hardware decode, check both: -**BUG:** Hardware supported decoding still has some bugs (as you can see from -the chart above). Also, the speed gains with decoding are not as great -as with encoding. Currently, if hardware decoding fails, there is no -fallback (you either get green frames or an "invalid file" error in OpenShot). -This needs to be improved to successfully fall-back to software decoding. +* correctness of the decoded output +* whether hardware decode actually succeeded -**Needed:** - * A way to get options and limits of the GPU, such as - supported dimensions (width and height). - * A way to list the actual Graphic Cards available to FFmpeg (for the - user to choose which card for decoding and encoding, as opposed - to "Graphics Card X") +A frame that looks correct is not enough to prove that hardware acceleration +worked, because software fallback may have rescued the decode. -**Further improvement:** Right now the frame can be decoded on the GPU, but the -frame is then copied to CPU memory for modifications. It is then copied back to -GPU memory for encoding. Using the GPU for both decoding and modifications -will make it possible to do away with these two copies. A possible solution would -be to use Vulkan compute which would be available on Linux and Windows natively -and on MacOS via MoltenVK. +Recommended validation: + +* compare output against a software-decode baseline +* track whether hardware decode actually produced frames +* test both a known-good hardware sample and a known-failing fallback sample + +## Future Improvements + +The biggest architectural limitation today is that decoded frames are generally +copied back to CPU memory for the rest of the pipeline. + +Longer-term improvements could include: + +* better hardware capability probing +* better device enumeration for users +* broader backend validation across platforms +* keeping more of the pipeline on GPU memory +* GPU-native effects/compositing paths + +Avoiding repeated GPU-to-CPU and CPU-to-GPU copies would make hardware +acceleration much more effective for end-to-end editing and export workflows. + +## Help Improve This Document + +Hardware acceleration support changes with FFmpeg, drivers, operating systems, +and GPU generations. If you find incorrect information or validate a backend on +a newer stack, please update this document. ## Credit -A big thanks to Peter M (https://github.com/eisneinechse) for all his work -on integrating hardware acceleration into libopenshot! The community thanks -you for this major contribution! +A big thanks to Peter M (https://github.com/eisneinechse) for all his work on +integrating hardware acceleration into libopenshot. The community thanks you for +this major contribution. diff --git a/src/FFmpegReader.cpp b/src/FFmpegReader.cpp index 77416abd..5d9e4a9f 100644 --- a/src/FFmpegReader.cpp +++ b/src/FFmpegReader.cpp @@ -268,7 +268,10 @@ void FFmpegReader::Open() { // Initialize format context pFormatCtx = NULL; { - hw_de_on = (openshot::Settings::Instance()->HARDWARE_DECODER == 0 ? 0 : 1); + hw_de_on = (!force_sw_decode && openshot::Settings::Instance()->HARDWARE_DECODER != 0 ? 1 : 0); + hw_decode_failed = false; + hw_decode_error_count = 0; + hw_decode_succeeded = false; ZmqLogger::Instance()->AppendDebugMethod("Decode hardware acceleration settings", "hw_de_on", hw_de_on, "HARDWARE_DECODER", openshot::Settings::Instance()->HARDWARE_DECODER); } @@ -1318,6 +1321,9 @@ std::shared_ptr FFmpegReader::ReadStream(int64_t requested_frame) { (info.has_video && !packet && !packet_status.video_eof)) { // Process Video Packet ProcessVideoPacket(requested_frame); + if (ReopenWithoutHardwareDecode(requested_frame)) { + continue; + } } // Audio packet if ((info.has_audio && packet && packet->stream_index == audioStream) || @@ -1492,6 +1498,26 @@ int FFmpegReader::GetNextPacket() { // Get an AVFrame (if any) bool FFmpegReader::GetAVFrame() { int frameFinished = 0; + auto note_hw_decode_failure = [&](int err, const char* stage) { +#if USE_HW_ACCEL + if (!hw_de_on || !hw_de_supported || force_sw_decode) { + return; + } + if (err == AVERROR_INVALIDDATA && packet_status.video_decoded == 0) { + hw_decode_error_count++; + ZmqLogger::Instance()->AppendDebugMethod( + std::string("FFmpegReader::GetAVFrame (hardware decode failure candidate during ") + stage + ")", + "error_count", hw_decode_error_count, + "error", err); + if (hw_decode_error_count >= 3) { + hw_decode_failed = true; + } + } +#else + (void) err; + (void) stage; +#endif + }; // Decode video frame AVFrame *next_frame = AV_ALLOCATE_FRAME(); @@ -1516,6 +1542,7 @@ bool FFmpegReader::GetAVFrame() { #endif // USE_HW_ACCEL if (send_packet_err < 0 && send_packet_err != AVERROR_EOF) { ZmqLogger::Instance()->AppendDebugMethod("FFmpegReader::GetAVFrame (send packet: Not sent [" + av_err2string(send_packet_err) + "])", "send_packet_err", send_packet_err, "send_packet_pts", send_packet_pts); + note_hw_decode_failure(send_packet_err, "send_packet"); if (send_packet_err == AVERROR(EAGAIN)) { hold_packet = true; ZmqLogger::Instance()->AppendDebugMethod("FFmpegReader::GetAVFrame (send packet: AVERROR(EAGAIN): user must read output with avcodec_receive_frame()", "send_packet_pts", send_packet_pts); @@ -1532,6 +1559,7 @@ bool FFmpegReader::GetAVFrame() { // Even if the above avcodec_send_packet failed to send, // we might still need to receive a packet. int receive_frame_err = 0; + AVFrame *decoded_frame = next_frame; AVFrame *next_frame2; #if USE_HW_ACCEL if (hw_de_on && hw_de_supported) { @@ -1548,6 +1576,7 @@ bool FFmpegReader::GetAVFrame() { if (receive_frame_err != 0) { ZmqLogger::Instance()->AppendDebugMethod("FFmpegReader::GetAVFrame (receive frame: frame not ready yet from decoder [\" + av_err2string(receive_frame_err) + \"])", "receive_frame_err", receive_frame_err, "send_packet_pts", send_packet_pts); + note_hw_decode_failure(receive_frame_err, "receive_frame"); if (receive_frame_err == AVERROR_EOF) { ZmqLogger::Instance()->AppendDebugMethod( @@ -1578,47 +1607,93 @@ bool FFmpegReader::GetAVFrame() { if (hw_de_on && hw_de_supported) { int err; if (next_frame2->format == hw_de_av_pix_fmt) { - next_frame->format = AV_PIX_FMT_YUV420P; - if ((err = av_hwframe_transfer_data(next_frame,next_frame2,0)) < 0) { - ZmqLogger::Instance()->AppendDebugMethod("FFmpegReader::GetAVFrame (Failed to transfer data to output frame)", "hw_de_on", hw_de_on); + if ((err = av_hwframe_transfer_data(next_frame, next_frame2, 0)) < 0) { + ZmqLogger::Instance()->AppendDebugMethod( + "FFmpegReader::GetAVFrame (Failed to transfer data to output frame)", + "hw_de_on", hw_de_on, + "error", err); + note_hw_decode_failure(AVERROR_INVALIDDATA, "hwframe_transfer"); + break; } - if ((err = av_frame_copy_props(next_frame,next_frame2)) < 0) { - ZmqLogger::Instance()->AppendDebugMethod("FFmpegReader::GetAVFrame (Failed to copy props to output frame)", "hw_de_on", hw_de_on); + if ((err = av_frame_copy_props(next_frame, next_frame2)) < 0) { + ZmqLogger::Instance()->AppendDebugMethod( + "FFmpegReader::GetAVFrame (Failed to copy props to output frame)", + "hw_de_on", hw_de_on, + "error", err); + note_hw_decode_failure(AVERROR_INVALIDDATA, "hwframe_copy_props"); + break; } + if (next_frame->format == AV_PIX_FMT_NONE) { + next_frame->format = pCodecCtx->sw_pix_fmt; + } + if (next_frame->width <= 0) { + next_frame->width = next_frame2->width; + } + if (next_frame->height <= 0) { + next_frame->height = next_frame2->height; + } + decoded_frame = next_frame; + } else { + // Some hardware decoders can still return software-readable frames. + decoded_frame = next_frame2; } } else #endif // USE_HW_ACCEL { // No hardware acceleration used -> no copy from GPU memory needed - next_frame = next_frame2; + decoded_frame = next_frame2; + } + + if (!decoded_frame->data[0]) { + ZmqLogger::Instance()->AppendDebugMethod( + "FFmpegReader::GetAVFrame (Decoded frame missing image data)", + "format", decoded_frame->format, + "width", decoded_frame->width, + "height", decoded_frame->height); + note_hw_decode_failure(AVERROR_INVALIDDATA, "decoded_frame_empty"); + break; } // TODO also handle possible further frames // Use only the first frame like avcodec_decode_video2 frameFinished = 1; + hw_decode_error_count = 0; +#if USE_HW_ACCEL + if (hw_de_on && hw_de_supported && !force_sw_decode) { + hw_decode_succeeded = true; + } +#endif packet_status.video_decoded++; // Allocate image (align 32 for simd) - AVPixelFormat decoded_pix_fmt = (AVPixelFormat)(next_frame->format); + AVPixelFormat decoded_pix_fmt = (AVPixelFormat)(decoded_frame->format); if (decoded_pix_fmt == AV_PIX_FMT_NONE) decoded_pix_fmt = (AVPixelFormat)(pStream->codecpar->format); if (AV_ALLOCATE_IMAGE(pFrame, decoded_pix_fmt, info.width, info.height) <= 0) { throw OutOfMemory("Failed to allocate image buffer", path); } - av_image_copy(pFrame->data, pFrame->linesize, (const uint8_t**)next_frame->data, next_frame->linesize, + av_image_copy(pFrame->data, pFrame->linesize, (const uint8_t**)decoded_frame->data, decoded_frame->linesize, decoded_pix_fmt, info.width, info.height); + pFrame->format = decoded_pix_fmt; + pFrame->width = info.width; + pFrame->height = info.height; + pFrame->color_range = decoded_frame->color_range; + pFrame->colorspace = decoded_frame->colorspace; + pFrame->color_primaries = decoded_frame->color_primaries; + pFrame->color_trc = decoded_frame->color_trc; + pFrame->chroma_location = decoded_frame->chroma_location; // Get display PTS from video frame, often different than packet->pts. // Sending packets to the decoder (i.e. packet->pts) is async, // and retrieving packets from the decoder (frame->pts) is async. In most decoders // sending and retrieving are separated by multiple calls to this method. - if (next_frame->pts != AV_NOPTS_VALUE) { + if (decoded_frame->pts != AV_NOPTS_VALUE) { // This is the current decoded frame (and should be the pts used) for // processing this data - video_pts = next_frame->pts; - } else if (next_frame->pkt_dts != AV_NOPTS_VALUE) { + video_pts = decoded_frame->pts; + } else if (decoded_frame->pkt_dts != AV_NOPTS_VALUE) { // Some videos only set this timestamp (fallback) - video_pts = next_frame->pkt_dts; + video_pts = decoded_frame->pkt_dts; } ZmqLogger::Instance()->AppendDebugMethod( @@ -1628,7 +1703,7 @@ bool FFmpegReader::GetAVFrame() { break; } #if USE_HW_ACCEL - if (hw_de_on && hw_de_supported) { + if (hw_de_on && hw_de_supported && next_frame2 != next_frame) { AV_FREE_FRAME(&next_frame2); } #endif // USE_HW_ACCEL @@ -1655,6 +1730,41 @@ bool FFmpegReader::GetAVFrame() { return frameFinished; } +bool FFmpegReader::ReopenWithoutHardwareDecode(int64_t requested_frame) { +#if USE_HW_ACCEL + if (!hw_decode_failed || force_sw_decode) { + return false; + } + + ZmqLogger::Instance()->AppendDebugMethod( + "FFmpegReader::ReopenWithoutHardwareDecode (falling back to software decode)", + "requested_frame", requested_frame, + "video_packets_read", packet_status.video_read, + "video_packets_decoded", packet_status.video_decoded, + "hw_decode_error_count", hw_decode_error_count); + + force_sw_decode = true; + hw_decode_failed = false; + hw_decode_error_count = 0; + + Close(); + Open(); + Seek(requested_frame); + return true; +#else + (void) requested_frame; + return false; +#endif +} + +bool FFmpegReader::HardwareDecodeSuccessful() const { +#if USE_HW_ACCEL + return hw_decode_succeeded; +#else + return false; +#endif +} + // Check the current seek position and determine if we need to seek again bool FFmpegReader::CheckSeek() { // Are we seeking for a specific frame? @@ -1879,9 +1989,49 @@ void FFmpegReader::ProcessVideoPacket(int64_t requested_frame) { sws_setColorspaceDetails(img_convert_ctx, src_coeff, src_full_range ? 1 : 0, dst_coeff, dst_full_range, 0, 1 << 16, 1 << 16); + if (!pFrame || !pFrame->data[0] || pFrame->linesize[0] <= 0) { +#if USE_HW_ACCEL + if (hw_de_on && hw_de_supported && !force_sw_decode) { + hw_decode_failed = true; + ZmqLogger::Instance()->AppendDebugMethod( + "FFmpegReader::ProcessVideoPacket (Invalid source frame; forcing software fallback)", + "requested_frame", requested_frame, + "current_frame", current_frame, + "src_pix_fmt", src_pix_fmt, + "src_width", src_width, + "src_height", src_height); + } +#endif + if (pFrame) { + RemoveAVFrame(pFrame); + pFrame = NULL; + } + return; + } + // Resize / Convert to RGB - sws_scale(img_convert_ctx, pFrame->data, pFrame->linesize, 0, + const int scaled_lines = sws_scale(img_convert_ctx, pFrame->data, pFrame->linesize, 0, original_height, pFrameRGB->data, pFrameRGB->linesize); + if (scaled_lines <= 0) { +#if USE_HW_ACCEL + if (hw_de_on && hw_de_supported && !force_sw_decode) { + hw_decode_failed = true; + ZmqLogger::Instance()->AppendDebugMethod( + "FFmpegReader::ProcessVideoPacket (sws_scale failed; forcing software fallback)", + "requested_frame", requested_frame, + "current_frame", current_frame, + "scaled_lines", scaled_lines, + "src_pix_fmt", src_pix_fmt, + "src_width", src_width, + "src_height", src_height); + } +#endif + free(buffer); + AV_RESET_FRAME(pFrameRGB); + RemoveAVFrame(pFrame); + pFrame = NULL; + return; + } // Create or get the existing frame object std::shared_ptr f = CreateFrame(current_frame); diff --git a/src/FFmpegReader.h b/src/FFmpegReader.h index 7b42c9d4..6417bb1d 100644 --- a/src/FFmpegReader.h +++ b/src/FFmpegReader.h @@ -165,6 +165,10 @@ namespace openshot { AVFrame *pFrameRGB_cached = nullptr; ///< Temporary frame used for video conversion int hw_de_supported = 0; // Is set by FFmpegReader + bool force_sw_decode = false; + bool hw_decode_failed = false; + int hw_decode_error_count = 0; + bool hw_decode_succeeded = false; #if USE_HW_ACCEL AVPixelFormat hw_de_av_pix_fmt = AV_PIX_FMT_NONE; AVHWDeviceType hw_de_av_device_type = AV_HWDEVICE_TYPE_NONE; @@ -198,6 +202,9 @@ namespace openshot { /// Get an AVFrame (if any) bool GetAVFrame(); + /// Reopen the current reader with software decode after hardware decode fails + bool ReopenWithoutHardwareDecode(int64_t requested_frame); + /// Get the next packet (if any) int GetNextPacket(); @@ -285,6 +292,9 @@ namespace openshot { /// Determine if reader is open or closed bool IsOpen() override { return is_open; }; + /// Return true if hardware decode was requested and successfully produced at least one frame + bool HardwareDecodeSuccessful() const override; + /// Return the type name of the class std::string Name() override { return "FFmpegReader"; }; diff --git a/src/ReaderBase.h b/src/ReaderBase.h index aca12ff2..319eda24 100644 --- a/src/ReaderBase.h +++ b/src/ReaderBase.h @@ -113,6 +113,10 @@ namespace openshot /// Determine if reader is open or closed virtual bool IsOpen() = 0; + /// Return true if hardware decode successfully produced at least one frame. + /// Readers without hardware decode support should return false. + virtual bool HardwareDecodeSuccessful() const { return false; } + /// Return the type name of the class virtual std::string Name() = 0; diff --git a/tests/FFmpegReader.cpp b/tests/FFmpegReader.cpp index 77f459e8..b31b0b12 100644 --- a/tests/FFmpegReader.cpp +++ b/tests/FFmpegReader.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,62 @@ using namespace openshot; +namespace { + +double SampleAverageLuma(const std::shared_ptr& frame, int sample_grid = 4) { + const int width = frame->GetWidth(); + const int height = frame->GetHeight(); + if (width <= 0 || height <= 0) { + return 0.0; + } + + int64_t luma_sum = 0; + int64_t sample_count = 0; + for (int y = 0; y < sample_grid; ++y) { + const int row = std::min(height - 1, (y * height) / sample_grid); + const unsigned char* pixels = frame->GetPixels(row); + for (int x = 0; x < sample_grid; ++x) { + const int col = std::min(width - 1, (x * width) / sample_grid); + const int pixel_index = col * 4; + luma_sum += (pixels[pixel_index] + pixels[pixel_index + 1] + pixels[pixel_index + 2]) / 3; + ++sample_count; + } + } + + return sample_count > 0 + ? static_cast(luma_sum) / static_cast(sample_count) + : 0.0; +} + +struct HardwareDecoderSettingsGuard { + int decoder = 0; + int device = 0; + + HardwareDecoderSettingsGuard() + : decoder(Settings::Instance()->HARDWARE_DECODER), + device(Settings::Instance()->HW_DE_DEVICE_SET) {} + + ~HardwareDecoderSettingsGuard() { + Settings::Instance()->HARDWARE_DECODER = decoder; + Settings::Instance()->HW_DE_DEVICE_SET = device; + } +}; + +struct TemporaryFileGuard { + std::string path; + + explicit TemporaryFileGuard(std::string temp_path) + : path(std::move(temp_path)) {} + + ~TemporaryFileGuard() { + if (!path.empty()) { + std::remove(path.c_str()); + } + } +}; + +} + TEST_CASE( "Invalid_Path", "[libopenshot][ffmpegreader]" ) { // Check invalid path and error details @@ -471,6 +528,134 @@ TEST_CASE( "Attached_Picture_Audio_Does_Not_Stall_Early_Frames", "[libopenshot][ std::remove(fixture_path.str().c_str()); } +TEST_CASE( "HardwareDecodeSuccessful_IsFalse_WhenHardwareDecodeIsDisabled", "[libopenshot][ffmpegreader][hardware]" ) +{ + HardwareDecoderSettingsGuard guard; + Settings::Instance()->HARDWARE_DECODER = 0; + Settings::Instance()->HW_DE_DEVICE_SET = 0; + + std::stringstream path; + path << TEST_MEDIA_PATH << "sintel_trailer-720p.mp4"; + FFmpegReader r(path.str(), DurationStrategy::VideoPreferred); + r.Open(); + + REQUIRE(r.info.has_video); + auto frame = r.GetFrame(1); + REQUIRE(frame->has_image_data); + CHECK_FALSE(r.HardwareDecodeSuccessful()); + + r.Close(); +} + +TEST_CASE( "VAAPI_H264_420_Reports_HardwareDecodeSuccess", "[libopenshot][ffmpegreader][hardware]" ) +{ +#if !defined(__linux__) || !USE_HW_ACCEL + WARN("Skipping hardware decode success test: requires Linux build with hardware decode support"); + return; +#else + if (std::system("ffmpeg -hide_banner -version >/dev/null 2>&1") != 0) { + WARN("Skipping hardware decode success test: ffmpeg executable not available"); + return; + } + if (std::system("ffmpeg -hide_banner -hwaccels 2>/dev/null | grep -q '\\'") != 0) { + WARN("Skipping hardware decode success test: ffmpeg does not report VAAPI support"); + return; + } + if (std::system("sh -c 'test -e /dev/dri/renderD128 -o -e /dev/dri/renderD129 -o -e /dev/dri/renderD130' >/dev/null 2>&1") != 0) { + WARN("Skipping hardware decode success test: no render node available under /dev/dri"); + return; + } + + std::srand(static_cast(std::time(nullptr))); + std::stringstream fixture_path; + fixture_path << "libopenshot-vaapi-420-test-" << std::rand() << ".mp4"; + TemporaryFileGuard fixture_cleanup(fixture_path.str()); + + std::stringstream command; + command << "ffmpeg -y -hide_banner -loglevel error " + << "-f lavfi -i \"testsrc2=size=128x72:rate=30\" " + << "-t 1 " + << "-c:v libx264 " + << "-pix_fmt yuv420p " + << "-profile:v high " + << "\"" << fixture_path.str() << "\""; + const int command_result = std::system(command.str().c_str()); + REQUIRE(command_result == 0); + + HardwareDecoderSettingsGuard hw_guard; + Settings::Instance()->HARDWARE_DECODER = 1; + Settings::Instance()->HW_DE_DEVICE_SET = 0; + + FFmpegReader r(fixture_path.str(), DurationStrategy::VideoPreferred); + r.Open(); + + REQUIRE(r.info.has_video); + auto frame = r.GetFrame(1); + REQUIRE(frame->has_image_data); + CHECK(r.HardwareDecodeSuccessful()); + + r.Close(); +#endif +} + +TEST_CASE( "VAAPI_H264_422_Does_Not_Return_Black_Frames", "[libopenshot][ffmpegreader][hardware]" ) +{ +#if !defined(__linux__) || !USE_HW_ACCEL + WARN("Skipping VAAPI regression test: requires Linux build with hardware decode support"); + return; +#else + if (std::system("ffmpeg -hide_banner -version >/dev/null 2>&1") != 0) { + WARN("Skipping VAAPI regression test: ffmpeg executable not available"); + return; + } + if (std::system("ffmpeg -hide_banner -hwaccels 2>/dev/null | grep -q '\\'") != 0) { + WARN("Skipping VAAPI regression test: ffmpeg does not report VAAPI support"); + return; + } + if (std::system("sh -c 'test -e /dev/dri/renderD128 -o -e /dev/dri/renderD129 -o -e /dev/dri/renderD130' >/dev/null 2>&1") != 0) { + WARN("Skipping VAAPI regression test: no render node available under /dev/dri"); + return; + } + + std::srand(static_cast(std::time(nullptr))); + std::stringstream fixture_path; + fixture_path << "libopenshot-vaapi-422-test-" << std::rand() << ".mp4"; + TemporaryFileGuard fixture_cleanup(fixture_path.str()); + + std::stringstream command; + command << "ffmpeg -y -hide_banner -loglevel error " + << "-f lavfi -i \"testsrc2=size=128x72:rate=30\" " + << "-t 1 " + << "-c:v libx264 " + << "-pix_fmt yuvj422p " + << "-profile:v high422 " + << "-color_range pc " + << "\"" << fixture_path.str() << "\""; + const int command_result = std::system(command.str().c_str()); + REQUIRE(command_result == 0); + + HardwareDecoderSettingsGuard guard; + Settings::Instance()->HARDWARE_DECODER = 1; + Settings::Instance()->HW_DE_DEVICE_SET = 0; + + FFmpegReader r(fixture_path.str(), DurationStrategy::VideoPreferred); + r.Open(); + + REQUIRE(r.info.has_video); + REQUIRE(r.info.video_length >= 3); + + const std::array frames_to_check = {1, r.info.video_length / 2, r.info.video_length}; + for (const int64_t frame_number : frames_to_check) { + auto frame = r.GetFrame(frame_number); + REQUIRE(frame->has_image_data); + INFO("frame=" << frame_number << ", avg_luma=" << SampleAverageLuma(frame)); + CHECK(SampleAverageLuma(frame) > 8.0); + } + + r.Close(); +#endif +} + TEST_CASE( "verify parent Timeline", "[libopenshot][ffmpegreader]" ) { // Create a reader