diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index d11cc72b..5f461d9a 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -37,6 +37,7 @@ linux-builder:
- cmake -DCMAKE_VERBOSE_MAKEFILE:BOOL=ON -D"CMAKE_INSTALL_PREFIX:PATH=$CI_PROJECT_DIR/build/install-x64" -D"PYTHON_MODULE_PATH=python" -D"RUBY_MODULE_PATH=ruby" -DCMAKE_BUILD_TYPE:STRING=Release -DAPPIMAGE_BUILD=1 -DUSE_SYSTEM_JSONCPP=0 ../
- make -j 4
- make install
+ - ctest --output-on-failure -VV
- make doc
- ~/auto-update-docs "$CI_PROJECT_DIR/build" "$CI_COMMIT_REF_NAME"
- PROJECT_VERSION=$(grep -E '^set\(PROJECT_VERSION_FULL "(.*)' ../CMakeLists.txt | awk '{print $2}' | tr -d '")')
@@ -66,7 +67,7 @@ mac-builder:
- cmake -DCMAKE_EXE_LINKER_FLAGS="-stdlib=libc++" -DCMAKE_SHARED_LINKER_FLAGS="-stdlib=libc++" -DCMAKE_VERBOSE_MAKEFILE:BOOL=ON -D"CMAKE_INSTALL_PREFIX:PATH=$CI_PROJECT_DIR/build/install-x64" -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER=clang -D"CMAKE_BUILD_TYPE:STRING=Release" -D"CMAKE_OSX_SYSROOT=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk" -D"CMAKE_OSX_DEPLOYMENT_TARGET=10.12" -DCMAKE_PREFIX_PATH=/usr/local/qt5.15.X/qt5.15/5.15.0/clang_64/ -D"CMAKE_INSTALL_RPATH_USE_LINK_PATH=1" -D"ENABLE_RUBY=0" ../
- make -j 9
- make install
- - make test
+ - ctest --output-on-failure -VV
- PROJECT_VERSION=$(grep -E '^set\(PROJECT_VERSION_FULL "(.*)' ../CMakeLists.txt | awk '{print $2}' | tr -d '")')
- PROJECT_SO=$(grep -E '^set\(PROJECT_SO_VERSION (.*)' ../CMakeLists.txt | awk '{print $2}' | tr -d ')')
- echo -e "CI_PROJECT_NAME:$CI_PROJECT_NAME\nCI_COMMIT_REF_NAME:$CI_COMMIT_REF_NAME\nCI_COMMIT_SHA:$CI_COMMIT_SHA\nCI_JOB_ID:$CI_JOB_ID\nCI_PIPELINE_ID:$CI_PIPELINE_ID\nVERSION:$PROJECT_VERSION\nSO:$PROJECT_SO" > "install-x64/share/$CI_PROJECT_NAME.env"
@@ -91,7 +92,7 @@ windows-builder-x64:
- $env:MSYSTEM = "MINGW64"
- cmake -B build -S . -DCMAKE_VERBOSE_MAKEFILE:BOOL=ON -D"babl_DIR=C:/msys64/mingw64" -D"CMAKE_INSTALL_PREFIX:PATH=$CI_PROJECT_DIR\build\install-x64" -D"OpenShotAudio_ROOT=$CI_PROJECT_DIR\build\install-x64" -D"PYTHON_MODULE_PATH=python" -D"RUBY_MODULE_PATH=ruby" -G "MinGW Makefiles" -D"CMAKE_BUILD_TYPE:STRING=Release"
- cmake --build build -j 4
- - cmake --build build --target coverage
+ - ctest --test-dir build --output-on-failure -VV
- cmake --install build
- $PROJECT_VERSION = (Select-String -Path "CMakeLists.txt" -Pattern '^set\(PROJECT_VERSION_FULL "(.*)\"' | %{$_.Matches.Groups[1].value})
- $PROJECT_SO = (Select-String -Path "CMakeLists.txt" -Pattern '^set\(PROJECT_SO_VERSION (.*)\)' | %{$_.Matches.Groups[1].value})
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/examples/test_aac_preroll.mp4 b/examples/test_aac_preroll.mp4
new file mode 100644
index 00000000..d46ffb88
Binary files /dev/null and b/examples/test_aac_preroll.mp4 differ
diff --git a/src/Clip.cpp b/src/Clip.cpp
index 022c0065..59a2672f 100644
--- a/src/Clip.cpp
+++ b/src/Clip.cpp
@@ -25,6 +25,7 @@
#include
#include
#include
+#include
#ifdef USE_IMAGEMAGICK
#include "MagickUtilities.h"
@@ -512,18 +513,36 @@ std::shared_ptr Clip::GetFrame(std::shared_ptr backgroun
final_cache.Add(frame);
}
- if (!background_frame) {
- // Create missing background_frame w/ transparent color (if needed)
- background_frame = std::make_shared(frame->number, frame->GetWidth(), frame->GetHeight(),
- "#00000000", frame->GetAudioSamplesCount(),
- frame->GetAudioChannelsCount());
- }
+ const bool has_external_background = (background_frame != nullptr);
- // Apply background canvas (i.e. flatten this image onto previous layer image)
- apply_background(frame, background_frame);
+ // Timeline path.
+ if (options) {
+ if (!background_frame) {
+ // Create a transparent background if missing.
+ background_frame = std::make_shared(frame->number, frame->GetWidth(), frame->GetHeight(),
+ "#00000000", frame->GetAudioSamplesCount(),
+ frame->GetAudioChannelsCount());
+ }
+ if (options->force_safe_composite) {
+ // Edit mode: composite without mutating cached frame pixels.
+ apply_background(frame, background_frame, false);
+ return frame;
+ }
- // Return processed 'frame'
- return frame;
+ // Playback mode: keep original fast path.
+ apply_background(frame, background_frame, true);
+ return frame;
+ }
+
+ // No background: return the frame directly.
+ if (!has_external_background) {
+ return frame;
+ }
+
+ // External background: composite on a copy.
+ auto output = std::make_shared(*frame.get());
+ apply_background(output, background_frame, true);
+ return output;
}
else
// Throw error if reader not initialized
@@ -1263,7 +1282,9 @@ void Clip::RemoveEffect(EffectBase* effect)
}
// Apply background image to the current clip image (i.e. flatten this image onto previous layer)
-void Clip::apply_background(std::shared_ptr frame, std::shared_ptr background_frame) {
+void Clip::apply_background(std::shared_ptr frame,
+ std::shared_ptr background_frame,
+ bool update_frame_image) {
// Add background canvas
std::shared_ptr background_canvas = background_frame->GetImage();
QPainter painter(background_canvas.get());
@@ -1273,8 +1294,10 @@ void Clip::apply_background(std::shared_ptr frame, std::shared_
painter.drawImage(0, 0, *frame->GetImage());
painter.end();
- // Add new QImage to frame
- frame->AddImage(background_canvas);
+ // Standalone clip requests update frame->image, but timeline composition
+ // draws onto the timeline-owned background frame only.
+ if (update_frame_image)
+ frame->AddImage(background_canvas);
}
// Apply effects to the source frame (if any)
@@ -1284,9 +1307,9 @@ void Clip::apply_effects(std::shared_ptr frame, int64_t timeline_frame_nu
{
// Apply the effect to this frame
if (effect->info.apply_before_clip && before_keyframes) {
- effect->GetFrame(frame, frame->number);
+ effect->ProcessFrame(frame, frame->number);
} else if (!effect->info.apply_before_clip && !before_keyframes) {
- effect->GetFrame(frame, frame->number);
+ effect->ProcessFrame(frame, frame->number);
}
}
diff --git a/src/Clip.h b/src/Clip.h
index cfb37768..d0304462 100644
--- a/src/Clip.h
+++ b/src/Clip.h
@@ -128,7 +128,9 @@ namespace openshot {
int64_t adjust_frame_number_minimum(int64_t frame_number);
/// Apply background image to the current clip image (i.e. flatten this image onto previous layer)
- void apply_background(std::shared_ptr frame, std::shared_ptr background_frame);
+ void apply_background(std::shared_ptr frame,
+ std::shared_ptr background_frame,
+ bool update_frame_image = true);
/// Apply effects to the source frame (if any)
void apply_effects(std::shared_ptr frame, int64_t timeline_frame_number, TimelineInfoStruct* options, bool before_keyframes);
diff --git a/src/EffectBase.cpp b/src/EffectBase.cpp
index d833b8cb..5bc8a0fd 100644
--- a/src/EffectBase.cpp
+++ b/src/EffectBase.cpp
@@ -12,11 +12,24 @@
#include
#include
+#include
+#include
#include "EffectBase.h"
#include "Exceptions.h"
+#include "Clip.h"
#include "Timeline.h"
+#include "ReaderBase.h"
+#include "ChunkReader.h"
+#include "FFmpegReader.h"
+#include "QtImageReader.h"
+#include "ZmqLogger.h"
+#include
+
+#ifdef USE_IMAGEMAGICK
+ #include "ImageReader.h"
+#endif
using namespace openshot;
@@ -31,6 +44,10 @@ void EffectBase::InitEffectInfo()
Order(0);
ParentClip(NULL);
parentEffect = NULL;
+ mask_invert = false;
+ mask_reader = NULL;
+ mask_time_mode = MASK_TIME_SOURCE_FPS;
+ mask_loop_mode = MASK_LOOP_PLAY_ONCE;
info.has_video = false;
info.has_audio = false;
@@ -89,6 +106,13 @@ Json::Value EffectBase::JsonValue() const {
root["has_tracked_object"] = info.has_tracked_object;
root["apply_before_clip"] = info.apply_before_clip;
root["order"] = Order();
+ root["mask_invert"] = mask_invert;
+ root["mask_time_mode"] = mask_time_mode;
+ root["mask_loop_mode"] = mask_loop_mode;
+ if (mask_reader)
+ root["mask_reader"] = mask_reader->JsonValue();
+ else
+ root["mask_reader"] = Json::objectValue;
// return JsonValue
return root;
@@ -141,6 +165,13 @@ void EffectBase::SetJsonValue(const Json::Value root) {
my_root = root;
}
+ // Legacy compatibility: older shared-mask JSON stored source trim
+ // separately from the effect trim. Canonical trim now uses ClipBase.
+ if (my_root["start"].isNull() && !my_root["mask_start"].isNull())
+ my_root["start"] = my_root["mask_start"];
+ if (my_root["end"].isNull() && !my_root["mask_end"].isNull())
+ my_root["end"] = my_root["mask_end"];
+
// Set parent data
ClipBase::SetJsonValue(my_root);
@@ -151,6 +182,32 @@ void EffectBase::SetJsonValue(const Json::Value root) {
if (!my_root["apply_before_clip"].isNull())
info.apply_before_clip = my_root["apply_before_clip"].asBool();
+ if (!my_root["mask_invert"].isNull())
+ mask_invert = my_root["mask_invert"].asBool();
+ if (!my_root["mask_time_mode"].isNull()) {
+ const int time_mode = my_root["mask_time_mode"].asInt();
+ mask_time_mode = (time_mode == MASK_TIME_TIMELINE || time_mode == MASK_TIME_SOURCE_FPS)
+ ? time_mode : MASK_TIME_SOURCE_FPS;
+ }
+ if (!my_root["mask_loop_mode"].isNull()) {
+ const int loop_mode = my_root["mask_loop_mode"].asInt();
+ if (loop_mode >= MASK_LOOP_PLAY_ONCE && loop_mode <= MASK_LOOP_PING_PONG)
+ mask_loop_mode = loop_mode;
+ else
+ mask_loop_mode = MASK_LOOP_PLAY_ONCE;
+ }
+
+ const Json::Value mask_reader_json =
+ !my_root["mask_reader"].isNull() ? my_root["mask_reader"] : my_root["reader"];
+
+ if (!mask_reader_json.isNull()) {
+ if (!mask_reader_json["type"].isNull()) {
+ MaskReader(CreateReaderFromJson(mask_reader_json));
+ } else if (mask_reader_json.isObject() && mask_reader_json.empty()) {
+ MaskReader(NULL);
+ }
+ }
+
if (!my_root["parent_effect_id"].isNull()){
info.parent_effect_id = my_root["parent_effect_id"].asString();
if (info.parent_effect_id.size() > 0 && info.parent_effect_id != "" && parentEffect == NULL)
@@ -194,9 +251,300 @@ Json::Value EffectBase::BasePropertiesJSON(int64_t requested_frame) const {
// Set the parent effect which properties this effect will inherit
root["parent_effect_id"] = add_property_json("Parent", 0.0, "string", info.parent_effect_id, NULL, -1, -1, false, requested_frame);
+ if (info.has_video) {
+ root["mask_invert"] = add_property_json("Mask: Invert", mask_invert, "int", "", NULL, 0, 1, false, requested_frame);
+ root["mask_invert"]["choices"].append(add_property_choice_json("Yes", true, mask_invert));
+ root["mask_invert"]["choices"].append(add_property_choice_json("No", false, mask_invert));
+
+ root["mask_time_mode"] = add_property_json("Mask: Time Mode", mask_time_mode, "int", "", NULL, 0, 1, false, requested_frame);
+ root["mask_time_mode"]["choices"].append(add_property_choice_json("Timeline", MASK_TIME_TIMELINE, mask_time_mode));
+ root["mask_time_mode"]["choices"].append(add_property_choice_json("Source FPS", MASK_TIME_SOURCE_FPS, mask_time_mode));
+
+ root["mask_loop_mode"] = add_property_json("Mask: Loop", mask_loop_mode, "int", "", NULL, 0, 2, false, requested_frame);
+ root["mask_loop_mode"]["choices"].append(add_property_choice_json("Play Once", MASK_LOOP_PLAY_ONCE, mask_loop_mode));
+ root["mask_loop_mode"]["choices"].append(add_property_choice_json("Repeat", MASK_LOOP_REPEAT, mask_loop_mode));
+ root["mask_loop_mode"]["choices"].append(add_property_choice_json("Ping-Pong", MASK_LOOP_PING_PONG, mask_loop_mode));
+
+ if (mask_reader)
+ root["mask_reader"] = add_property_json("Mask: Source", 0.0, "reader", mask_reader->Json(), NULL, 0, 1, false, requested_frame);
+ else
+ root["mask_reader"] = add_property_json("Mask: Source", 0.0, "reader", "{}", NULL, 0, 1, false, requested_frame);
+ }
+
return root;
}
+ReaderBase* EffectBase::CreateReaderFromJson(const Json::Value& reader_json) const {
+ if (reader_json["type"].isNull())
+ return NULL;
+
+ ReaderBase* reader = NULL;
+ const std::string type = reader_json["type"].asString();
+
+ if (type == "FFmpegReader") {
+ reader = new FFmpegReader(reader_json["path"].asString());
+ reader->SetJsonValue(reader_json);
+ // Mask readers are video-only sources. Disabling audio avoids FFmpeg
+ // A/V readiness fallbacks that can repeat stale video frames.
+ reader->info.has_audio = false;
+ reader->info.audio_stream_index = -1;
+ } else if (type == "QtImageReader") {
+ reader = new QtImageReader(reader_json["path"].asString());
+ reader->SetJsonValue(reader_json);
+ } else if (type == "ChunkReader") {
+ reader = new ChunkReader(reader_json["path"].asString(),
+ static_cast(reader_json["chunk_version"].asInt()));
+ reader->SetJsonValue(reader_json);
+#ifdef USE_IMAGEMAGICK
+ } else if (type == "ImageReader") {
+ reader = new ImageReader(reader_json["path"].asString());
+ reader->SetJsonValue(reader_json);
+#endif
+ }
+
+ return reader;
+}
+
+void EffectBase::MaskReader(ReaderBase* new_reader) {
+ if (mask_reader == new_reader)
+ return;
+
+ if (mask_reader) {
+ mask_reader->Close();
+ delete mask_reader;
+ }
+
+ mask_reader = new_reader;
+ cached_single_mask_image.reset();
+ cached_single_mask_width = 0;
+ cached_single_mask_height = 0;
+ if (mask_reader)
+ mask_reader->ParentClip(clip);
+}
+
+double EffectBase::ResolveMaskHostFps() {
+ if (clip) {
+ Clip* parent_clip = dynamic_cast(clip);
+ if (parent_clip && parent_clip->info.fps.num > 0 && parent_clip->info.fps.den > 0)
+ return parent_clip->info.fps.ToDouble();
+ }
+
+ Timeline* parent_timeline = dynamic_cast(ParentTimeline());
+ if (parent_timeline && parent_timeline->info.fps.num > 0 && parent_timeline->info.fps.den > 0)
+ return parent_timeline->info.fps.ToDouble();
+
+ if (mask_reader && mask_reader->info.fps.num > 0 && mask_reader->info.fps.den > 0)
+ return mask_reader->info.fps.ToDouble();
+
+ return 30.0;
+}
+
+double EffectBase::ResolveMaskSourceDuration() const {
+ if (!mask_reader)
+ return 0.0;
+
+ if (mask_reader->info.duration > 0.0f)
+ return static_cast(mask_reader->info.duration);
+
+ if (mask_reader->info.video_length > 0 &&
+ mask_reader->info.fps.num > 0 && mask_reader->info.fps.den > 0) {
+ return static_cast(mask_reader->info.video_length) / mask_reader->info.fps.ToDouble();
+ }
+
+ return 0.0;
+}
+
+int64_t EffectBase::MapMaskFrameNumber(int64_t frame_number) {
+ if (!mask_reader)
+ return frame_number;
+
+ int64_t requested_index = std::max(int64_t(0), frame_number - 1);
+ if (!clip && ParentTimeline()) {
+ const double host_fps = ResolveMaskHostFps();
+ if (host_fps > 0.0) {
+ const int64_t start_offset = static_cast(std::llround(std::max(0.0f, Start()) * host_fps));
+ requested_index = std::max(int64_t(0), requested_index - start_offset);
+ }
+ }
+ int64_t mapped_index = requested_index;
+
+ if (mask_time_mode == MASK_TIME_SOURCE_FPS &&
+ mask_reader->info.fps.num > 0 && mask_reader->info.fps.den > 0) {
+ const double host_fps = ResolveMaskHostFps();
+ const double source_fps = mask_reader->info.fps.ToDouble();
+ if (host_fps > 0.0 && source_fps > 0.0) {
+ const double seconds = static_cast(requested_index) / host_fps;
+ mapped_index = static_cast(std::llround(seconds * source_fps));
+ }
+ }
+
+ const int64_t source_len = mask_reader->info.video_length;
+ const double source_fps = (mask_reader->info.fps.num > 0 && mask_reader->info.fps.den > 0)
+ ? mask_reader->info.fps.ToDouble() : 30.0;
+ const double source_duration = ResolveMaskSourceDuration();
+ const double start_sec = std::min(std::max(0.0f, Start()), source_duration);
+ const double end_sec = std::min(std::max(0.0f, End()), source_duration);
+
+ const int64_t range_start = std::max(int64_t(1), static_cast(std::llround(start_sec * source_fps)) + 1);
+ int64_t range_end = (end_sec > 0.0)
+ ? static_cast(std::llround(end_sec * source_fps)) + 1
+ : source_len;
+ if (source_len > 0)
+ range_end = std::min(range_end, source_len);
+ if (range_end < range_start)
+ range_end = range_start;
+
+ const int64_t range_len = std::max(int64_t(1), range_end - range_start + 1);
+ int64_t range_index = mapped_index;
+
+ switch (mask_loop_mode) {
+ case MASK_LOOP_REPEAT:
+ range_index = mapped_index % range_len;
+ break;
+ case MASK_LOOP_PING_PONG:
+ if (range_len > 1) {
+ const int64_t cycle_len = (range_len * 2) - 2;
+ int64_t phase = mapped_index % cycle_len;
+ if (phase >= range_len)
+ phase = cycle_len - phase;
+ range_index = phase;
+ } else {
+ range_index = 0;
+ }
+ break;
+ case MASK_LOOP_PLAY_ONCE:
+ default:
+ if (mapped_index < 0)
+ range_index = 0;
+ else if (mapped_index >= range_len)
+ range_index = range_len - 1;
+ else
+ range_index = mapped_index;
+ break;
+ }
+
+ int64_t mapped_frame = range_start + range_index;
+ if (source_len > 0)
+ mapped_frame = std::min(std::max(int64_t(1), mapped_frame), source_len);
+ return std::max(int64_t(1), mapped_frame);
+}
+
+std::shared_ptr EffectBase::GetMaskImage(std::shared_ptr target_image, int64_t frame_number) {
+ if (!mask_reader || !target_image || target_image->isNull())
+ return {};
+
+ std::shared_ptr source_mask;
+ bool used_cached_scaled = false;
+ #pragma omp critical (open_effect_mask_reader)
+ {
+ try {
+ if (!mask_reader->IsOpen())
+ mask_reader->Open();
+
+ if (mask_reader->info.has_single_image &&
+ cached_single_mask_image &&
+ cached_single_mask_width == target_image->width() &&
+ cached_single_mask_height == target_image->height()) {
+ source_mask = cached_single_mask_image;
+ used_cached_scaled = true;
+ }
+ else {
+ const int64_t mapped_frame = MapMaskFrameNumber(frame_number);
+ auto source_frame = mask_reader->GetFrame(mapped_frame);
+ if (source_frame && source_frame->GetImage() && !source_frame->GetImage()->isNull())
+ source_mask = std::make_shared(*source_frame->GetImage());
+ }
+ } catch (const std::exception& e) {
+ ZmqLogger::Instance()->Log(
+ std::string("EffectBase::GetMaskImage unable to read mask frame: ") + e.what());
+ source_mask.reset();
+ }
+ }
+
+ if (!source_mask || source_mask->isNull())
+ return {};
+
+ if (used_cached_scaled)
+ return source_mask;
+
+ auto scaled_mask = std::make_shared(
+ source_mask->scaled(
+ target_image->width(), target_image->height(),
+ Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
+ if (mask_reader->info.has_single_image) {
+ cached_single_mask_image = scaled_mask;
+ cached_single_mask_width = target_image->width();
+ cached_single_mask_height = target_image->height();
+ }
+ return scaled_mask;
+}
+
+void EffectBase::BlendWithMask(std::shared_ptr original_image, std::shared_ptr effected_image,
+ std::shared_ptr mask_image) const {
+ if (!original_image || !effected_image || !mask_image)
+ return;
+ if (original_image->size() != effected_image->size() || effected_image->size() != mask_image->size())
+ return;
+
+ unsigned char* original_pixels = reinterpret_cast(original_image->bits());
+ unsigned char* effected_pixels = reinterpret_cast(effected_image->bits());
+ unsigned char* mask_pixels = reinterpret_cast(mask_image->bits());
+ const int pixel_count = effected_image->width() * effected_image->height();
+
+ #pragma omp parallel for schedule(static)
+ for (int i = 0; i < pixel_count; ++i) {
+ const int idx = i * 4;
+ int gray = qGray(mask_pixels[idx], mask_pixels[idx + 1], mask_pixels[idx + 2]);
+ if (mask_invert)
+ gray = 255 - gray;
+ const float factor = static_cast(gray) / 255.0f;
+ const float inverse = 1.0f - factor;
+
+ effected_pixels[idx] = static_cast(
+ (original_pixels[idx] * inverse) + (effected_pixels[idx] * factor));
+ effected_pixels[idx + 1] = static_cast(
+ (original_pixels[idx + 1] * inverse) + (effected_pixels[idx + 1] * factor));
+ effected_pixels[idx + 2] = static_cast(
+ (original_pixels[idx + 2] * inverse) + (effected_pixels[idx + 2] * factor));
+ effected_pixels[idx + 3] = static_cast(
+ (original_pixels[idx + 3] * inverse) + (effected_pixels[idx + 3] * factor));
+ }
+}
+
+std::shared_ptr EffectBase::ProcessFrame(std::shared_ptr frame, int64_t frame_number) {
+ // Audio-only effects skip common mask handling.
+ if (!info.has_video || !mask_reader)
+ return GetFrame(frame, frame_number);
+
+ // Effects that already apply masks inside GetFrame() should bypass common blend handling.
+ if (HandlesMaskInternally())
+ return GetFrame(frame, frame_number);
+
+ auto pre_image = frame->GetImage();
+ if (!pre_image || pre_image->isNull())
+ return GetFrame(frame, frame_number);
+
+ const auto original_image = std::make_shared(pre_image->copy());
+ auto output_frame = GetFrame(frame, frame_number);
+ if (!output_frame)
+ return output_frame;
+ auto effected_image = output_frame->GetImage();
+ if (!effected_image || effected_image->isNull() ||
+ effected_image->size() != original_image->size())
+ return output_frame;
+
+ auto mask_image = GetMaskImage(effected_image, frame_number);
+ if (!mask_image || mask_image->isNull())
+ return output_frame;
+
+ if (UseCustomMaskBlend(frame_number))
+ ApplyCustomMaskBlend(original_image, effected_image, mask_image, frame_number);
+ else
+ BlendWithMask(original_image, effected_image, mask_image);
+
+ return output_frame;
+}
+
/// Parent clip object of this reader (which can be unparented and NULL)
openshot::ClipBase* EffectBase::ParentClip() {
return clip;
@@ -205,6 +553,8 @@ openshot::ClipBase* EffectBase::ParentClip() {
/// Set parent clip object of this reader
void EffectBase::ParentClip(openshot::ClipBase* new_clip) {
clip = new_clip;
+ if (mask_reader)
+ mask_reader->ParentClip(new_clip);
}
// Set the parent effect from which this properties will be set to
@@ -239,3 +589,7 @@ std::string EffectBase::ParentClipId() const{
else
return "";
}
+
+EffectBase::~EffectBase() {
+ MaskReader(NULL);
+}
diff --git a/src/EffectBase.h b/src/EffectBase.h
index fb39d62b..8fa21888 100644
--- a/src/EffectBase.h
+++ b/src/EffectBase.h
@@ -18,12 +18,15 @@
#include "Json.h"
#include "TrackedObjectBase.h"
+#include
#include
#include