You've already forked libopenshot
mirror of
https://github.com/OpenShot/libopenshot.git
synced 2026-06-08 22:17:28 -07:00
Merge pull request #1039 from OpenShot/comfy-ui
Masking, Playback Stability, Performance Improvements
This commit is contained in:
+3
-2
@@ -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})
|
||||
|
||||
+228
-78
@@ -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 | ✔️ <sup>1</sup> | ✅ <sup>2</sup> | - | - | - | - | *Linux Only* |
|
||||
| CUDA (NVDEC/NVENC) | ❌ <sup>3</sup> | ✔️ | - | - | - | ✔️ | *Cross Platform* |
|
||||
| VideoToolBox | - | - | ✔️ | ❌ <sup>4</sup> | - | - | *Mac Only* |
|
||||
| DXVA2 | - | - | - | - | ❌ <sup>3</sup> | - | *Windows Only* |
|
||||
| D3D11VA | - | - | - | - | ❌ <sup>3</sup> | - | *Windows Only* |
|
||||
| QSV | ❌ <sup>3</sup> | ❌ | ❌ | ❌ | ❌ | ❌ | *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 | ✔️ <sup>1</sup> | ✅ <sup>2</sup> | - | - | - | - | Linux only |
|
||||
| CUDA (NVDEC/NVENC) | ❌ <sup>3</sup> | ✔️ | - | - | - | ✔️ | Backend availability depends on the FFmpeg build |
|
||||
| VideoToolbox | - | - | ✔️ | ❌ <sup>4</sup> | - | - | macOS only |
|
||||
| DXVA2 | - | - | - | - | ❌ <sup>3</sup> | - | Windows only |
|
||||
| D3D11VA | - | - | - | - | ❌ <sup>3</sup> | - | Windows only |
|
||||
| QSV | ❌ <sup>3</sup> | ❌ | ❌ | ❌ | ❌ | ❌ | 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.
|
||||
|
||||
+38
-15
@@ -25,6 +25,7 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <sstream>
|
||||
#include <QPainter>
|
||||
|
||||
#ifdef USE_IMAGEMAGICK
|
||||
#include "MagickUtilities.h"
|
||||
@@ -512,18 +513,36 @@ std::shared_ptr<Frame> Clip::GetFrame(std::shared_ptr<openshot::Frame> backgroun
|
||||
final_cache.Add(frame);
|
||||
}
|
||||
|
||||
if (!background_frame) {
|
||||
// Create missing background_frame w/ transparent color (if needed)
|
||||
background_frame = std::make_shared<Frame>(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>(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>(*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<openshot::Frame> frame, std::shared_ptr<openshot::Frame> background_frame) {
|
||||
void Clip::apply_background(std::shared_ptr<openshot::Frame> frame,
|
||||
std::shared_ptr<openshot::Frame> background_frame,
|
||||
bool update_frame_image) {
|
||||
// Add background canvas
|
||||
std::shared_ptr<QImage> background_canvas = background_frame->GetImage();
|
||||
QPainter painter(background_canvas.get());
|
||||
@@ -1273,8 +1294,10 @@ void Clip::apply_background(std::shared_ptr<openshot::Frame> 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> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -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<openshot::Frame> frame, std::shared_ptr<openshot::Frame> background_frame);
|
||||
void apply_background(std::shared_ptr<openshot::Frame> frame,
|
||||
std::shared_ptr<openshot::Frame> background_frame,
|
||||
bool update_frame_image = true);
|
||||
|
||||
/// Apply effects to the source frame (if any)
|
||||
void apply_effects(std::shared_ptr<openshot::Frame> frame, int64_t timeline_frame_number, TimelineInfoStruct* options, bool before_keyframes);
|
||||
|
||||
@@ -12,11 +12,24 @@
|
||||
|
||||
#include <iostream>
|
||||
#include <iomanip>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#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 <omp.h>
|
||||
|
||||
#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<ChunkVersion>(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*>(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<Timeline*>(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<double>(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<double>(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<int64_t>(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<double>(requested_index) / host_fps;
|
||||
mapped_index = static_cast<int64_t>(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<double>(std::max(0.0f, Start()), source_duration);
|
||||
const double end_sec = std::min<double>(std::max(0.0f, End()), source_duration);
|
||||
|
||||
const int64_t range_start = std::max(int64_t(1), static_cast<int64_t>(std::llround(start_sec * source_fps)) + 1);
|
||||
int64_t range_end = (end_sec > 0.0)
|
||||
? static_cast<int64_t>(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<QImage> EffectBase::GetMaskImage(std::shared_ptr<QImage> target_image, int64_t frame_number) {
|
||||
if (!mask_reader || !target_image || target_image->isNull())
|
||||
return {};
|
||||
|
||||
std::shared_ptr<QImage> 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<QImage>(*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<QImage>(
|
||||
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<QImage> original_image, std::shared_ptr<QImage> effected_image,
|
||||
std::shared_ptr<QImage> 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<unsigned char*>(original_image->bits());
|
||||
unsigned char* effected_pixels = reinterpret_cast<unsigned char*>(effected_image->bits());
|
||||
unsigned char* mask_pixels = reinterpret_cast<unsigned char*>(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<float>(gray) / 255.0f;
|
||||
const float inverse = 1.0f - factor;
|
||||
|
||||
effected_pixels[idx] = static_cast<unsigned char>(
|
||||
(original_pixels[idx] * inverse) + (effected_pixels[idx] * factor));
|
||||
effected_pixels[idx + 1] = static_cast<unsigned char>(
|
||||
(original_pixels[idx + 1] * inverse) + (effected_pixels[idx + 1] * factor));
|
||||
effected_pixels[idx + 2] = static_cast<unsigned char>(
|
||||
(original_pixels[idx + 2] * inverse) + (effected_pixels[idx + 2] * factor));
|
||||
effected_pixels[idx + 3] = static_cast<unsigned char>(
|
||||
(original_pixels[idx + 3] * inverse) + (effected_pixels[idx + 3] * factor));
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<openshot::Frame> EffectBase::ProcessFrame(std::shared_ptr<openshot::Frame> 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<QImage>(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);
|
||||
}
|
||||
|
||||
+67
-1
@@ -18,12 +18,15 @@
|
||||
#include "Json.h"
|
||||
#include "TrackedObjectBase.h"
|
||||
|
||||
#include <QImage>
|
||||
#include <memory>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
namespace openshot
|
||||
{
|
||||
class ReaderBase;
|
||||
|
||||
/**
|
||||
* @brief This struct contains info about an effect, such as the name, video or audio effect, etc...
|
||||
*
|
||||
@@ -54,10 +57,48 @@ namespace openshot
|
||||
{
|
||||
private:
|
||||
int order; ///< The order to evaluate this effect. Effects are processed in this order (when more than one overlap).
|
||||
ReaderBase* mask_reader = nullptr; ///< Optional common reader-based mask source.
|
||||
std::shared_ptr<QImage> cached_single_mask_image; ///< Cached scaled mask for still-image mask sources.
|
||||
int cached_single_mask_width = 0; ///< Cached mask width.
|
||||
int cached_single_mask_height = 0; ///< Cached mask height.
|
||||
|
||||
/// Build or refresh a mask image that matches target_image dimensions.
|
||||
std::shared_ptr<QImage> GetMaskImage(std::shared_ptr<QImage> target_image, int64_t frame_number);
|
||||
|
||||
/// Blend original and effected images using mask values.
|
||||
void BlendWithMask(std::shared_ptr<QImage> original_image, std::shared_ptr<QImage> effected_image,
|
||||
std::shared_ptr<QImage> mask_image) const;
|
||||
|
||||
protected:
|
||||
openshot::ClipBase* clip; ///< Pointer to the parent clip instance (if any)
|
||||
|
||||
/// Create a reader instance from reader JSON.
|
||||
ReaderBase* CreateReaderFromJson(const Json::Value& reader_json) const;
|
||||
|
||||
/// Convert an effect frame number to a mask source frame number.
|
||||
int64_t MapMaskFrameNumber(int64_t frame_number);
|
||||
|
||||
/// Determine host FPS used to convert timeline frames to mask source FPS.
|
||||
double ResolveMaskHostFps();
|
||||
|
||||
/// Determine mask source duration in seconds.
|
||||
double ResolveMaskSourceDuration() const;
|
||||
|
||||
/// Resolve a cached/scaled mask image for the target frame dimensions.
|
||||
std::shared_ptr<QImage> ResolveMaskImage(std::shared_ptr<QImage> target_image, int64_t frame_number) {
|
||||
return GetMaskImage(target_image, frame_number);
|
||||
}
|
||||
|
||||
/// Optional override for effects that need custom mask behavior.
|
||||
virtual bool UseCustomMaskBlend(int64_t frame_number) const { return false; }
|
||||
|
||||
/// Optional override for effects with custom mask implementation.
|
||||
virtual void ApplyCustomMaskBlend(std::shared_ptr<QImage> original_image, std::shared_ptr<QImage> effected_image,
|
||||
std::shared_ptr<QImage> mask_image, int64_t frame_number) const {}
|
||||
|
||||
/// Optional override for effects that apply mask processing inside GetFrame().
|
||||
virtual bool HandlesMaskInternally() const { return false; }
|
||||
|
||||
public:
|
||||
/// Parent effect (which properties will set this effect properties)
|
||||
EffectBase* parentEffect;
|
||||
@@ -67,6 +108,21 @@ namespace openshot
|
||||
|
||||
/// Information about the current effect
|
||||
EffectInfoStruct info;
|
||||
bool mask_invert = false; ///< Invert grayscale mask values before blending.
|
||||
|
||||
enum MaskTimeMode {
|
||||
MASK_TIME_TIMELINE = 0,
|
||||
MASK_TIME_SOURCE_FPS = 1
|
||||
};
|
||||
|
||||
enum MaskLoopMode {
|
||||
MASK_LOOP_PLAY_ONCE = 0,
|
||||
MASK_LOOP_REPEAT = 1,
|
||||
MASK_LOOP_PING_PONG = 2
|
||||
};
|
||||
|
||||
int mask_time_mode = MASK_TIME_SOURCE_FPS; ///< How effect frames map to mask source frames.
|
||||
int mask_loop_mode = MASK_LOOP_PLAY_ONCE; ///< Behavior when mask range reaches the end.
|
||||
|
||||
/// Display effect information in the standard output stream (stdout)
|
||||
void DisplayInfo(std::ostream* out=&std::cout);
|
||||
@@ -112,13 +168,23 @@ namespace openshot
|
||||
/// Generate JSON object of base properties (recommended to be used by all effects)
|
||||
Json::Value BasePropertiesJSON(int64_t requested_frame) const;
|
||||
|
||||
/// Apply effect processing with common mask support (if enabled).
|
||||
std::shared_ptr<openshot::Frame> ProcessFrame(std::shared_ptr<openshot::Frame> frame, int64_t frame_number);
|
||||
|
||||
/// Get the common mask reader.
|
||||
ReaderBase* MaskReader() { return mask_reader; }
|
||||
const ReaderBase* MaskReader() const { return mask_reader; }
|
||||
|
||||
/// Set or replace the common mask reader.
|
||||
void MaskReader(ReaderBase* new_reader);
|
||||
|
||||
/// Get the order that this effect should be executed.
|
||||
int Order() const { return order; }
|
||||
|
||||
/// Set the order that this effect should be executed.
|
||||
void Order(int new_order) { order = new_order; }
|
||||
|
||||
virtual ~EffectBase() = default;
|
||||
virtual ~EffectBase();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
+2
-1
@@ -168,7 +168,8 @@ enum ChromaKeyMethod
|
||||
CHROMAKEY_CIE_LCH_H, ///< Difference between CIE LCH(ab) hues
|
||||
CHROMAKEY_CIE_DISTANCE, ///< CIEDE2000 perceptual difference
|
||||
CHROMAKEY_YCBCR, ///< YCbCr vector difference of CbCr
|
||||
CHROMAKEY_LAST_METHOD = CHROMAKEY_YCBCR
|
||||
CHROMAKEY_BASIC_SOFT, ///< BASIC metric + optional halo feathering
|
||||
CHROMAKEY_LAST_METHOD = CHROMAKEY_BASIC_SOFT
|
||||
};
|
||||
|
||||
} // namespace openshot
|
||||
|
||||
+260
-28
@@ -73,6 +73,32 @@ int hw_de_on = 0;
|
||||
AVHWDeviceType hw_de_av_device_type_global = AV_HWDEVICE_TYPE_NONE;
|
||||
#endif
|
||||
|
||||
// Normalize deprecated JPEG-range YUVJ formats before creating swscale contexts.
|
||||
// swscale expects non-YUVJ formats plus explicit color-range metadata.
|
||||
static AVPixelFormat NormalizeDeprecatedPixFmt(AVPixelFormat pix_fmt, bool& is_full_range) {
|
||||
switch (pix_fmt) {
|
||||
case AV_PIX_FMT_YUVJ420P:
|
||||
is_full_range = true;
|
||||
return AV_PIX_FMT_YUV420P;
|
||||
case AV_PIX_FMT_YUVJ422P:
|
||||
is_full_range = true;
|
||||
return AV_PIX_FMT_YUV422P;
|
||||
case AV_PIX_FMT_YUVJ444P:
|
||||
is_full_range = true;
|
||||
return AV_PIX_FMT_YUV444P;
|
||||
case AV_PIX_FMT_YUVJ440P:
|
||||
is_full_range = true;
|
||||
return AV_PIX_FMT_YUV440P;
|
||||
#ifdef AV_PIX_FMT_YUVJ411P
|
||||
case AV_PIX_FMT_YUVJ411P:
|
||||
is_full_range = true;
|
||||
return AV_PIX_FMT_YUV411P;
|
||||
#endif
|
||||
default:
|
||||
return pix_fmt;
|
||||
}
|
||||
}
|
||||
|
||||
FFmpegReader::FFmpegReader(const std::string &path, bool inspect_reader)
|
||||
: FFmpegReader(path, DurationStrategy::VideoPreferred, inspect_reader) {}
|
||||
|
||||
@@ -242,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);
|
||||
}
|
||||
|
||||
@@ -1119,7 +1148,7 @@ void FFmpegReader::UpdateVideoInfo() {
|
||||
|
||||
// Normalize FFmpeg-decoded still images (e.g. JPG/JPEG) to match image-reader behavior.
|
||||
// This keeps timing/flags consistent regardless of which reader path was used.
|
||||
if (!info.has_single_image && audioStream < 0) {
|
||||
if (!info.has_single_image) {
|
||||
const AVCodecID codec_id = AV_FIND_DECODER_CODEC_ID(pStream);
|
||||
const bool likely_still_codec =
|
||||
codec_id == AV_CODEC_ID_MJPEG ||
|
||||
@@ -1131,11 +1160,25 @@ void FFmpegReader::UpdateVideoInfo() {
|
||||
const bool likely_image_demuxer =
|
||||
pFormatCtx && pFormatCtx->iformat && pFormatCtx->iformat->name &&
|
||||
strstr(pFormatCtx->iformat->name, "image2");
|
||||
const bool has_attached_pic = HasAlbumArt();
|
||||
const bool single_frame_stream =
|
||||
(pStream && pStream->nb_frames > 0 && pStream->nb_frames <= 1);
|
||||
const bool single_frame_clip = info.video_length <= 1;
|
||||
|
||||
if (single_frame_clip && (likely_still_codec || likely_image_demuxer)) {
|
||||
const bool is_still_image_video =
|
||||
has_attached_pic ||
|
||||
((single_frame_stream || single_frame_clip) &&
|
||||
(likely_still_codec || likely_image_demuxer));
|
||||
|
||||
if (is_still_image_video) {
|
||||
info.has_single_image = true;
|
||||
record_duration(video_stream_duration_seconds, 60 * 60 * 1); // 1 hour duration
|
||||
|
||||
// Only force long duration for standalone images. For audio + attached-art
|
||||
// files, keep stream-derived duration so the cover image spans the audio.
|
||||
if (audioStream < 0) {
|
||||
record_duration(video_stream_duration_seconds, 60 * 60 * 1); // 1 hour duration
|
||||
}
|
||||
|
||||
ApplyDurationStrategy();
|
||||
}
|
||||
}
|
||||
@@ -1177,7 +1220,6 @@ std::shared_ptr<Frame> FFmpegReader::GetFrame(int64_t requested_frame) {
|
||||
if (frame) {
|
||||
// Debug output
|
||||
ZmqLogger::Instance()->AppendDebugMethod("FFmpegReader::GetFrame", "returned cached frame", requested_frame);
|
||||
|
||||
// Return the cached frame
|
||||
return frame;
|
||||
} else {
|
||||
@@ -1190,7 +1232,6 @@ std::shared_ptr<Frame> FFmpegReader::GetFrame(int64_t requested_frame) {
|
||||
if (frame) {
|
||||
// Debug output
|
||||
ZmqLogger::Instance()->AppendDebugMethod("FFmpegReader::GetFrame", "returned cached frame on 2nd look", requested_frame);
|
||||
|
||||
} else {
|
||||
// Frame is not in cache
|
||||
// Reset seek count
|
||||
@@ -1280,6 +1321,9 @@ std::shared_ptr<Frame> 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) ||
|
||||
@@ -1454,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();
|
||||
@@ -1478,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);
|
||||
@@ -1494,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) {
|
||||
@@ -1510,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(
|
||||
@@ -1540,44 +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)
|
||||
if (AV_ALLOCATE_IMAGE(pFrame, (AVPixelFormat)(pStream->codecpar->format), info.width, info.height) <= 0) {
|
||||
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,
|
||||
(AVPixelFormat)(pStream->codecpar->format), info.width, info.height);
|
||||
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(
|
||||
@@ -1587,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
|
||||
@@ -1614,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?
|
||||
@@ -1714,9 +1865,15 @@ void FFmpegReader::ProcessVideoPacket(int64_t requested_frame) {
|
||||
ZmqLogger::Instance()->AppendDebugMethod("FFmpegReader::ProcessVideoPacket (Before)", "requested_frame", requested_frame, "current_frame", current_frame);
|
||||
|
||||
// Init some things local (for OpenMP)
|
||||
PixelFormat pix_fmt = AV_GET_CODEC_PIXEL_FORMAT(pStream, pCodecCtx);
|
||||
int height = info.height;
|
||||
int width = info.width;
|
||||
AVPixelFormat decoded_pix_fmt = (pFrame && pFrame->format != AV_PIX_FMT_NONE)
|
||||
? static_cast<AVPixelFormat>(pFrame->format)
|
||||
: AV_GET_CODEC_PIXEL_FORMAT(pStream, pCodecCtx);
|
||||
bool src_full_range = (pFrame && pFrame->color_range == AVCOL_RANGE_JPEG);
|
||||
AVPixelFormat src_pix_fmt = NormalizeDeprecatedPixFmt(decoded_pix_fmt, src_full_range);
|
||||
int src_width = (pFrame && pFrame->width > 0) ? pFrame->width : info.width;
|
||||
int src_height = (pFrame && pFrame->height > 0) ? pFrame->height : info.height;
|
||||
int height = src_height;
|
||||
int width = src_width;
|
||||
int64_t video_length = info.video_length;
|
||||
|
||||
// Create or reuse a RGB Frame (since most videos are not in RGB, we must convert it)
|
||||
@@ -1789,7 +1946,7 @@ void FFmpegReader::ProcessVideoPacket(int64_t requested_frame) {
|
||||
}
|
||||
|
||||
// Determine if image needs to be scaled (for performance reasons)
|
||||
int original_height = height;
|
||||
int original_height = src_height;
|
||||
if (max_width != 0 && max_height != 0 && max_width < width && max_height < height) {
|
||||
// Override width and height (but maintain aspect ratio)
|
||||
float ratio = float(width) / float(height);
|
||||
@@ -1823,19 +1980,64 @@ void FFmpegReader::ProcessVideoPacket(int64_t requested_frame) {
|
||||
if (openshot::Settings::Instance()->HIGH_QUALITY_SCALING) {
|
||||
scale_mode = SWS_BICUBIC;
|
||||
}
|
||||
img_convert_ctx = sws_getCachedContext(img_convert_ctx, info.width, info.height, AV_GET_CODEC_PIXEL_FORMAT(pStream, pCodecCtx), width, height, PIX_FMT_RGBA, scale_mode, NULL, NULL, NULL);
|
||||
img_convert_ctx = sws_getCachedContext(img_convert_ctx, src_width, src_height, src_pix_fmt, width, height, PIX_FMT_RGBA, scale_mode, NULL, NULL, NULL);
|
||||
if (!img_convert_ctx)
|
||||
throw OutOfMemory("Failed to initialize sws context", path);
|
||||
const int *src_coeff = sws_getCoefficients(SWS_CS_DEFAULT);
|
||||
const int *dst_coeff = sws_getCoefficients(SWS_CS_DEFAULT);
|
||||
const int dst_full_range = 1; // RGB outputs are full-range
|
||||
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<Frame> f = CreateFrame(current_frame);
|
||||
|
||||
// Add Image data to frame
|
||||
if (!ffmpeg_has_alpha(AV_GET_CODEC_PIXEL_FORMAT(pStream, pCodecCtx))) {
|
||||
if (!ffmpeg_has_alpha(src_pix_fmt)) {
|
||||
// Add image with no alpha channel, Speed optimization
|
||||
f->AddImage(width, height, bytes_per_pixel, QImage::Format_RGBA8888_Premultiplied, buffer);
|
||||
} else {
|
||||
@@ -2536,7 +2738,8 @@ void FFmpegReader::CheckWorkingFrames(int64_t requested_frame) {
|
||||
"frame_pts_seconds", frame_pts_seconds,
|
||||
"video_pts_seconds", video_pts_seconds,
|
||||
"recent_pts_diff", recent_pts_diff);
|
||||
if (info.has_video && !f->has_image_data) {
|
||||
if (info.has_video && !f->has_image_data &&
|
||||
(packet_status.video_eof || packet_status.end_of_file)) {
|
||||
// Frame has no image data. Prefer timeline-previous frames to preserve
|
||||
// visual order, especially when decode/prefetch is out-of-order.
|
||||
std::shared_ptr<Frame> previous_frame_instance = final_cache.GetFrame(f->number - 1);
|
||||
@@ -2601,6 +2804,35 @@ void FFmpegReader::CheckWorkingFrames(int64_t requested_frame) {
|
||||
"end_of_file", packet_status.end_of_file);
|
||||
|
||||
// Check if working frame is final
|
||||
if (info.has_video && !f->has_image_data
|
||||
&& !packet_status.end_of_file && !is_seek_trash) {
|
||||
if (info.has_single_image) {
|
||||
// For still-image video (including attached cover art), reuse the most
|
||||
// recent image so playback does not stall waiting for video EOF.
|
||||
std::shared_ptr<Frame> previous_frame_instance = final_cache.GetFrame(f->number - 1);
|
||||
if (previous_frame_instance && previous_frame_instance->has_image_data) {
|
||||
f->AddImage(std::make_shared<QImage>(previous_frame_instance->GetImage()->copy()));
|
||||
}
|
||||
if (!f->has_image_data
|
||||
&& last_final_video_frame
|
||||
&& last_final_video_frame->has_image_data
|
||||
&& last_final_video_frame->number <= f->number) {
|
||||
f->AddImage(std::make_shared<QImage>(last_final_video_frame->GetImage()->copy()));
|
||||
}
|
||||
if (!f->has_image_data
|
||||
&& last_video_frame
|
||||
&& last_video_frame->has_image_data
|
||||
&& last_video_frame->number <= f->number) {
|
||||
f->AddImage(std::make_shared<QImage>(last_video_frame->GetImage()->copy()));
|
||||
}
|
||||
}
|
||||
|
||||
// Do not finalize non-EOF video frames without decoded image data.
|
||||
// This prevents repeated previous-frame fallbacks being cached as real frames.
|
||||
if (!f->has_image_data) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((!packet_status.end_of_file && is_video_ready && is_audio_ready) || packet_status.end_of_file || is_seek_trash) {
|
||||
// Debug output
|
||||
ZmqLogger::Instance()->AppendDebugMethod("FFmpegReader::CheckWorkingFrames (mark frame as final)",
|
||||
|
||||
@@ -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"; };
|
||||
|
||||
|
||||
+13
-4
@@ -79,7 +79,7 @@ FFmpegWriter::FFmpegWriter(const std::string& path) :
|
||||
initial_audio_input_frame_size(0), img_convert_ctx(NULL),
|
||||
video_codec_ctx(NULL), audio_codec_ctx(NULL), is_writing(false), video_timestamp(0), audio_timestamp(0),
|
||||
original_sample_rate(0), original_channels(0), avr(NULL), avr_planar(NULL), is_open(false), prepare_streams(false),
|
||||
write_header(false), write_trailer(false), audio_encoder_buffer_size(0), audio_encoder_buffer(NULL) {
|
||||
write_header(false), write_trailer(false), allow_b_frames(false), audio_encoder_buffer_size(0), audio_encoder_buffer(NULL) {
|
||||
|
||||
// Disable audio & video (so they can be independently enabled)
|
||||
info.has_audio = false;
|
||||
@@ -368,7 +368,7 @@ void FFmpegWriter::SetOption(StreamType stream, std::string name, std::string va
|
||||
// Was option found?
|
||||
if (option || (name == "g" || name == "qmin" || name == "qmax" || name == "max_b_frames" || name == "mb_decision" ||
|
||||
name == "level" || name == "profile" || name == "slices" || name == "rc_min_rate" || name == "rc_max_rate" ||
|
||||
name == "rc_buffer_size" || name == "crf" || name == "cqp" || name == "qp")) {
|
||||
name == "rc_buffer_size" || name == "crf" || name == "cqp" || name == "qp" || name == "allow_b_frames")) {
|
||||
// Check for specific named options
|
||||
if (name == "g")
|
||||
// Set gop_size
|
||||
@@ -386,6 +386,11 @@ void FFmpegWriter::SetOption(StreamType stream, std::string name, std::string va
|
||||
// Maximum number of B-frames between non-B-frames
|
||||
convert >> c->max_b_frames;
|
||||
|
||||
else if (name == "allow_b_frames")
|
||||
// Preserve configured B-frames for codecs that support them.
|
||||
// Values: 1/true/yes/on to enable, everything else disables.
|
||||
allow_b_frames = (value == "1" || value == "true" || value == "yes" || value == "on");
|
||||
|
||||
else if (name == "mb_decision")
|
||||
// Macroblock decision mode
|
||||
convert >> c->mb_decision;
|
||||
@@ -1468,8 +1473,12 @@ void FFmpegWriter::open_video(AVFormatContext *oc, AVStream *st) {
|
||||
if (!codec)
|
||||
throw InvalidCodec("Could not find codec", path);
|
||||
|
||||
/* Force max_b_frames to 0 in some cases (i.e. for mjpeg image sequences */
|
||||
if (video_codec_ctx->max_b_frames && video_codec_ctx->codec_id != AV_CODEC_ID_MPEG4 && video_codec_ctx->codec_id != AV_CODEC_ID_MPEG1VIDEO && video_codec_ctx->codec_id != AV_CODEC_ID_MPEG2VIDEO)
|
||||
/* Legacy behavior: force max_b_frames to 0 for many codecs.
|
||||
* This can be disabled via SetOption(VIDEO_STREAM, "allow_b_frames", "1"). */
|
||||
if (!allow_b_frames && video_codec_ctx->max_b_frames &&
|
||||
video_codec_ctx->codec_id != AV_CODEC_ID_MPEG4 &&
|
||||
video_codec_ctx->codec_id != AV_CODEC_ID_MPEG1VIDEO &&
|
||||
video_codec_ctx->codec_id != AV_CODEC_ID_MPEG2VIDEO)
|
||||
video_codec_ctx->max_b_frames = 0;
|
||||
|
||||
// Init options
|
||||
|
||||
@@ -124,6 +124,7 @@ namespace openshot {
|
||||
bool prepare_streams;
|
||||
bool write_header;
|
||||
bool write_trailer;
|
||||
bool allow_b_frames;
|
||||
|
||||
AVFormatContext* oc;
|
||||
AVStream *audio_st, *video_st;
|
||||
|
||||
@@ -74,9 +74,13 @@ namespace openshot
|
||||
// Pausing Code (which re-syncs audio/video times)
|
||||
// - If speed is zero or speed changes
|
||||
// - If pre-roll is not ready (This should allow scrubbing of the timeline without waiting on pre-roll)
|
||||
if ((speed == 0 && video_position == last_video_position) ||
|
||||
(speed != 0 && last_speed != speed) ||
|
||||
(speed != 0 && !is_dirty && !videoCache->isReady()))
|
||||
bool wait_paused_hold = (speed == 0 && video_position == last_video_position);
|
||||
bool wait_speed_change = (speed != 0 && last_speed != speed);
|
||||
bool cache_ready = videoCache->isReady();
|
||||
bool wait_preroll = (speed != 0 && !is_dirty && !cache_ready);
|
||||
bool should_wait = (wait_paused_hold || wait_speed_change || wait_preroll);
|
||||
|
||||
if (should_wait)
|
||||
{
|
||||
// Sleep for a fraction of frame duration
|
||||
std::this_thread::sleep_for(frame_duration / 4);
|
||||
@@ -97,7 +101,16 @@ namespace openshot
|
||||
|
||||
// Set the video frame on the video thread and render frame
|
||||
videoPlayback->frame = frame;
|
||||
videoPlayback->rendered.reset();
|
||||
videoPlayback->render.signal();
|
||||
// Keep decode/position advancement aligned with actual preview updates.
|
||||
// This avoids occasional "silent advance then jump" behavior when
|
||||
// preroll transitions and rendering are briefly out-of-sync.
|
||||
const int render_wait_ms = std::max(
|
||||
1,
|
||||
static_cast<int>(frame_duration.count() / 1000.0 * 2.0)
|
||||
);
|
||||
videoPlayback->rendered.wait(render_wait_ms);
|
||||
|
||||
// Keep track of the last displayed frame
|
||||
last_video_position = video_position;
|
||||
@@ -116,6 +129,12 @@ namespace openshot
|
||||
// Protect against invalid or too-long sleep times
|
||||
std::this_thread::sleep_for(max_sleep);
|
||||
}
|
||||
} else {
|
||||
// If we're behind schedule (e.g. preroll/render stall), do not
|
||||
// burst through delayed frames. Resync timing baseline so
|
||||
// playback continues smoothly at normal cadence.
|
||||
start_time = std::chrono::time_point_cast<micro_sec>(current_time);
|
||||
playback_frames = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,8 +169,8 @@ namespace openshot
|
||||
// Increment playback frames (always in the positive direction)
|
||||
playback_frames += std::abs(speed);
|
||||
|
||||
// Update cache on which frame was retrieved
|
||||
videoCache->Seek(video_position);
|
||||
// Update playhead hint for cache window tracking without triggering seek behavior.
|
||||
videoCache->NotifyPlaybackPosition(video_position);
|
||||
|
||||
// return frame from reader
|
||||
return reader->GetFrame(video_position);
|
||||
@@ -170,6 +189,10 @@ namespace openshot
|
||||
{
|
||||
video_position = new_position;
|
||||
last_video_position = 0;
|
||||
// Drop local frame reference so same-frame refreshes cannot reuse stale
|
||||
// content after timeline/clip property updates.
|
||||
frame.reset();
|
||||
// Always force immediate refresh after seek/update, even while playing.
|
||||
is_dirty = true;
|
||||
}
|
||||
|
||||
|
||||
+274
-27
@@ -30,6 +30,8 @@ namespace openshot
|
||||
, last_dir(1) // assume forward (+1) on first launch
|
||||
, userSeeked(false)
|
||||
, preroll_on_next_fill(false)
|
||||
, clear_cache_on_next_fill(false)
|
||||
, scrub_active(false)
|
||||
, requested_display_frame(1)
|
||||
, current_display_frame(1)
|
||||
, cached_frame_count(0)
|
||||
@@ -38,6 +40,8 @@ namespace openshot
|
||||
, reader(nullptr)
|
||||
, force_directional_cache(false)
|
||||
, last_cached_index(0)
|
||||
, seen_timeline_cache_epoch(0)
|
||||
, timeline_cache_epoch_initialized(false)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -59,7 +63,7 @@ namespace openshot
|
||||
}
|
||||
|
||||
const int64_t cached_index = last_cached_index.load();
|
||||
const int64_t playhead = requested_display_frame.load();
|
||||
int64_t playhead = requested_display_frame.load();
|
||||
int dir = computeDirection();
|
||||
|
||||
// Near timeline boundaries, don't require more pre-roll than can exist.
|
||||
@@ -73,6 +77,7 @@ namespace openshot
|
||||
if (max_frame < 1) {
|
||||
return false;
|
||||
}
|
||||
playhead = clampToTimelineRange(playhead, max_frame);
|
||||
|
||||
int64_t required_ahead = ready_min;
|
||||
int64_t available_ahead = (dir > 0)
|
||||
@@ -92,6 +97,8 @@ namespace openshot
|
||||
if (new_speed != 0) {
|
||||
last_speed.store(new_speed);
|
||||
last_dir.store(new_speed > 0 ? 1 : -1);
|
||||
// Leaving paused/scrub context: resume normal cache behavior.
|
||||
scrub_active.store(false);
|
||||
}
|
||||
speed.store(new_speed);
|
||||
}
|
||||
@@ -126,45 +133,176 @@ namespace openshot
|
||||
return !isThreadRunning();
|
||||
}
|
||||
|
||||
void VideoCacheThread::Reader(ReaderBase* new_reader)
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(seek_state_mutex);
|
||||
reader = new_reader;
|
||||
seen_timeline_cache_epoch = 0;
|
||||
timeline_cache_epoch_initialized = false;
|
||||
Play();
|
||||
}
|
||||
|
||||
void VideoCacheThread::Seek(int64_t new_position, bool start_preroll)
|
||||
{
|
||||
const int64_t timeline_end = resolveTimelineEnd();
|
||||
const int64_t clamped_new_position = clampToTimelineRange(new_position, timeline_end);
|
||||
const int64_t current_requested = requested_display_frame.load();
|
||||
|
||||
bool should_mark_seek = false;
|
||||
bool should_preroll = false;
|
||||
int64_t new_cached_count = cached_frame_count.load();
|
||||
bool entering_scrub = false;
|
||||
bool leaving_scrub = false;
|
||||
bool cache_contains = false;
|
||||
bool should_clear_cache = false;
|
||||
CacheBase* cache = reader ? reader->GetCache() : nullptr;
|
||||
const bool same_frame_refresh = (new_position == current_requested);
|
||||
if (cache) {
|
||||
cache_contains = cache->Contains(clamped_new_position);
|
||||
}
|
||||
|
||||
if (start_preroll) {
|
||||
should_mark_seek = true;
|
||||
CacheBase* cache = reader ? reader->GetCache() : nullptr;
|
||||
|
||||
if (cache && !cache->Contains(new_position))
|
||||
{
|
||||
// If user initiated seek, and current frame not found (
|
||||
if (Timeline* timeline = dynamic_cast<Timeline*>(reader)) {
|
||||
timeline->ClearAllCache();
|
||||
if (same_frame_refresh) {
|
||||
const bool is_paused = (speed.load() == 0);
|
||||
if (is_paused) {
|
||||
const bool was_scrubbing = scrub_active.load();
|
||||
if (was_scrubbing && cache && cache_contains) {
|
||||
// Preserve in-range cache for paused scrub preview -> same-frame commit.
|
||||
should_mark_seek = false;
|
||||
should_preroll = false;
|
||||
should_clear_cache = false;
|
||||
new_cached_count = cache->Count();
|
||||
} else {
|
||||
// Paused same-frame edit refresh: force full cache refresh.
|
||||
if (Timeline* timeline = dynamic_cast<Timeline*>(reader)) {
|
||||
timeline->ClearAllCache();
|
||||
}
|
||||
new_cached_count = 0;
|
||||
should_mark_seek = true;
|
||||
should_preroll = true;
|
||||
should_clear_cache = false;
|
||||
}
|
||||
} else {
|
||||
// Same-frame refresh during playback should stay lightweight.
|
||||
should_mark_seek = false;
|
||||
should_preroll = false;
|
||||
should_clear_cache = false;
|
||||
if (cache && cache_contains) {
|
||||
cache->Remove(clamped_new_position);
|
||||
}
|
||||
if (cache) {
|
||||
new_cached_count = cache->Count();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (cache && !cache_contains) {
|
||||
should_mark_seek = true;
|
||||
// Uncached commit seek: defer cache clear to cache thread loop.
|
||||
new_cached_count = 0;
|
||||
should_preroll = true;
|
||||
should_clear_cache = true;
|
||||
}
|
||||
else if (cache)
|
||||
{
|
||||
// In-range commit seek preserves cache window/baseline.
|
||||
should_mark_seek = false;
|
||||
should_preroll = false;
|
||||
should_clear_cache = false;
|
||||
new_cached_count = cache->Count();
|
||||
} else {
|
||||
// No cache object to query: use normal seek behavior.
|
||||
should_mark_seek = true;
|
||||
}
|
||||
new_cached_count = 0;
|
||||
should_preroll = true;
|
||||
}
|
||||
else if (cache)
|
||||
{
|
||||
new_cached_count = cache->Count();
|
||||
leaving_scrub = true;
|
||||
}
|
||||
else {
|
||||
// Non-preroll seeks cover paused scrubbing and live playback refresh.
|
||||
const bool is_paused = (speed.load() == 0);
|
||||
if (is_paused && same_frame_refresh) {
|
||||
// Same-frame paused refresh updates only that frame.
|
||||
should_mark_seek = false;
|
||||
should_preroll = false;
|
||||
should_clear_cache = false;
|
||||
if (cache && cache_contains) {
|
||||
cache->Remove(clamped_new_position);
|
||||
}
|
||||
if (cache) {
|
||||
new_cached_count = cache->Count();
|
||||
}
|
||||
leaving_scrub = true;
|
||||
}
|
||||
else if (is_paused) {
|
||||
if (cache && !cache_contains) {
|
||||
should_mark_seek = true;
|
||||
new_cached_count = 0;
|
||||
should_clear_cache = true;
|
||||
}
|
||||
else if (cache) {
|
||||
// In-range paused seek preserves cache continuity.
|
||||
should_mark_seek = false;
|
||||
new_cached_count = cache->Count();
|
||||
} else {
|
||||
should_mark_seek = true;
|
||||
}
|
||||
entering_scrub = true;
|
||||
} else {
|
||||
// During playback, keep seek/scrub side effects minimal.
|
||||
should_mark_seek = false;
|
||||
should_preroll = false;
|
||||
should_clear_cache = false;
|
||||
if (cache) {
|
||||
new_cached_count = cache->Count();
|
||||
}
|
||||
leaving_scrub = true;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(seek_state_mutex);
|
||||
// Reset readiness baseline only when rebuilding cache.
|
||||
const int dir = computeDirection();
|
||||
if (should_mark_seek || should_preroll || should_clear_cache) {
|
||||
last_cached_index.store(clamped_new_position - dir);
|
||||
}
|
||||
requested_display_frame.store(new_position);
|
||||
cached_frame_count.store(new_cached_count);
|
||||
if (start_preroll) {
|
||||
preroll_on_next_fill.store(should_preroll);
|
||||
userSeeked.store(should_mark_seek);
|
||||
preroll_on_next_fill.store(should_preroll);
|
||||
// Clear behavior follows the latest seek intent.
|
||||
clear_cache_on_next_fill.store(should_clear_cache);
|
||||
userSeeked.store(should_mark_seek);
|
||||
if (entering_scrub) {
|
||||
scrub_active.store(true);
|
||||
}
|
||||
if (leaving_scrub) {
|
||||
scrub_active.store(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VideoCacheThread::Seek(int64_t new_position)
|
||||
{
|
||||
Seek(new_position, false);
|
||||
NotifyPlaybackPosition(new_position);
|
||||
}
|
||||
|
||||
void VideoCacheThread::NotifyPlaybackPosition(int64_t new_position)
|
||||
{
|
||||
if (new_position <= 0) {
|
||||
return;
|
||||
}
|
||||
if (scrub_active.load()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int64_t new_cached_count = cached_frame_count.load();
|
||||
if (CacheBase* cache = reader ? reader->GetCache() : nullptr) {
|
||||
new_cached_count = cache->Count();
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(seek_state_mutex);
|
||||
requested_display_frame.store(new_position);
|
||||
cached_frame_count.store(new_cached_count);
|
||||
}
|
||||
}
|
||||
|
||||
int VideoCacheThread::computeDirection() const
|
||||
@@ -216,11 +354,39 @@ namespace openshot
|
||||
return min_frames;
|
||||
}
|
||||
|
||||
int64_t VideoCacheThread::resolveTimelineEnd() const
|
||||
{
|
||||
if (!reader) {
|
||||
return 0;
|
||||
}
|
||||
int64_t timeline_end = reader->info.video_length;
|
||||
if (auto* timeline = dynamic_cast<Timeline*>(reader)) {
|
||||
const int64_t timeline_max = timeline->GetMaxFrame();
|
||||
if (timeline_max > 0) {
|
||||
timeline_end = timeline_max;
|
||||
}
|
||||
}
|
||||
return timeline_end;
|
||||
}
|
||||
|
||||
int64_t VideoCacheThread::clampToTimelineRange(int64_t frame, int64_t timeline_end) const
|
||||
{
|
||||
if (timeline_end < 1) {
|
||||
return frame;
|
||||
}
|
||||
return std::clamp<int64_t>(frame, 1, timeline_end);
|
||||
}
|
||||
|
||||
bool VideoCacheThread::clearCacheIfPaused(int64_t playhead,
|
||||
bool paused,
|
||||
CacheBase* cache)
|
||||
{
|
||||
if (paused && !cache->Contains(playhead)) {
|
||||
const int64_t timeline_end = resolveTimelineEnd();
|
||||
int64_t cache_playhead = playhead;
|
||||
if (reader) {
|
||||
cache_playhead = clampToTimelineRange(playhead, timeline_end);
|
||||
}
|
||||
if (paused && !cache->Contains(cache_playhead)) {
|
||||
// If paused and playhead not in cache, clear everything
|
||||
if (Timeline* timeline = dynamic_cast<Timeline*>(reader)) {
|
||||
timeline->ClearAllCache();
|
||||
@@ -257,10 +423,12 @@ namespace openshot
|
||||
int64_t window_begin,
|
||||
int64_t window_end,
|
||||
int dir,
|
||||
ReaderBase* reader)
|
||||
ReaderBase* reader,
|
||||
int64_t max_frames_to_fetch)
|
||||
{
|
||||
bool window_full = true;
|
||||
int64_t next_frame = last_cached_index.load() + dir;
|
||||
int64_t fetched_this_pass = 0;
|
||||
|
||||
// Advance from last_cached_index toward window boundary
|
||||
while ((dir > 0 && next_frame <= window_end) ||
|
||||
@@ -280,6 +448,7 @@ namespace openshot
|
||||
auto framePtr = reader->GetFrame(next_frame);
|
||||
cache->Add(framePtr);
|
||||
cached_frame_count.store(cache->Count());
|
||||
++fetched_this_pass;
|
||||
}
|
||||
catch (const OutOfBoundsFrame&) {
|
||||
break;
|
||||
@@ -292,6 +461,12 @@ namespace openshot
|
||||
|
||||
last_cached_index.store(next_frame);
|
||||
next_frame += dir;
|
||||
|
||||
// In active playback, avoid long uninterrupted prefetch bursts
|
||||
// that can delay player thread frame retrieval.
|
||||
if (max_frames_to_fetch > 0 && fetched_this_pass >= max_frames_to_fetch) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return window_full;
|
||||
@@ -305,6 +480,22 @@ namespace openshot
|
||||
while (!threadShouldExit()) {
|
||||
Settings* settings = Settings::Instance();
|
||||
CacheBase* cache = reader ? reader->GetCache() : nullptr;
|
||||
Timeline* timeline = dynamic_cast<Timeline*>(reader);
|
||||
|
||||
// Process deferred clears even when caching is currently disabled
|
||||
// (e.g. active scrub mode), so stale ranges are removed promptly.
|
||||
bool should_clear_cache = clear_cache_on_next_fill.exchange(false);
|
||||
if (should_clear_cache && timeline) {
|
||||
const int dir_on_clear = computeDirection();
|
||||
const int64_t clear_playhead = clampToTimelineRange(
|
||||
requested_display_frame.load(), resolveTimelineEnd());
|
||||
timeline->ClearAllCache();
|
||||
cached_frame_count.store(0);
|
||||
// Reset ready baseline immediately after clear. Otherwise a
|
||||
// stale last_cached_index from the old cache window can make
|
||||
// isReady() report true before new preroll is actually filled.
|
||||
last_cached_index.store(clear_playhead - dir_on_clear);
|
||||
}
|
||||
|
||||
// If caching disabled or no reader, mark cache as ready and sleep briefly
|
||||
if (!settings->ENABLE_PLAYBACK_CACHING || !cache) {
|
||||
@@ -317,13 +508,13 @@ namespace openshot
|
||||
// init local vars
|
||||
min_frames_ahead.store(settings->VIDEO_CACHE_MIN_PREROLL_FRAMES);
|
||||
|
||||
Timeline* timeline = dynamic_cast<Timeline*>(reader);
|
||||
if (!timeline) {
|
||||
std::this_thread::sleep_for(double_micro_sec(50000));
|
||||
continue;
|
||||
}
|
||||
int64_t timeline_end = timeline->GetMaxFrame();
|
||||
int64_t playhead = requested_display_frame.load();
|
||||
int64_t timeline_end = resolveTimelineEnd();
|
||||
int64_t raw_playhead = requested_display_frame.load();
|
||||
int64_t playhead = clampToTimelineRange(raw_playhead, timeline_end);
|
||||
bool paused = (speed.load() == 0);
|
||||
int64_t preroll_frames = computePrerollFrames(settings);
|
||||
|
||||
@@ -335,6 +526,27 @@ namespace openshot
|
||||
last_dir.store(dir);
|
||||
}
|
||||
|
||||
// If timeline-side cache invalidation occurred (e.g. ApplyJsonDiff / SetJson),
|
||||
// restart fill from the active playhead window so invalidated gaps self-heal.
|
||||
if (timeline) {
|
||||
bool epoch_changed = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(seek_state_mutex);
|
||||
const uint64_t timeline_epoch = timeline->CacheEpoch();
|
||||
if (!timeline_cache_epoch_initialized) {
|
||||
seen_timeline_cache_epoch = timeline_epoch;
|
||||
timeline_cache_epoch_initialized = true;
|
||||
}
|
||||
else if (timeline_epoch != seen_timeline_cache_epoch) {
|
||||
seen_timeline_cache_epoch = timeline_epoch;
|
||||
epoch_changed = true;
|
||||
}
|
||||
}
|
||||
if (epoch_changed) {
|
||||
handleUserSeek(playhead, dir);
|
||||
}
|
||||
}
|
||||
|
||||
// Compute bytes_per_frame, max_bytes, and capacity once
|
||||
int64_t bytes_per_frame = getBytes(
|
||||
(timeline->preview_width ? timeline->preview_width : reader->info.width),
|
||||
@@ -357,7 +569,8 @@ namespace openshot
|
||||
bool use_preroll = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(seek_state_mutex);
|
||||
playhead = requested_display_frame.load();
|
||||
raw_playhead = requested_display_frame.load();
|
||||
playhead = clampToTimelineRange(raw_playhead, timeline_end);
|
||||
did_user_seek = userSeeked.load();
|
||||
use_preroll = preroll_on_next_fill.load();
|
||||
if (did_user_seek) {
|
||||
@@ -366,7 +579,10 @@ namespace openshot
|
||||
}
|
||||
}
|
||||
if (did_user_seek) {
|
||||
if (use_preroll) {
|
||||
// During active playback, prioritize immediate forward readiness
|
||||
// from the playhead. Use directional preroll offset only while
|
||||
// paused/scrubbing contexts.
|
||||
if (use_preroll && paused) {
|
||||
handleUserSeekWithPreroll(playhead, dir, timeline_end, preroll_frames);
|
||||
}
|
||||
else {
|
||||
@@ -395,6 +611,22 @@ namespace openshot
|
||||
}
|
||||
}
|
||||
|
||||
// If a clear was requested by a seek that arrived after the loop
|
||||
// began, apply it now before any additional prefetch work. This
|
||||
// avoids "build then suddenly clear" behavior during playback.
|
||||
bool should_clear_mid_loop = clear_cache_on_next_fill.exchange(false);
|
||||
if (should_clear_mid_loop && timeline) {
|
||||
timeline->ClearAllCache();
|
||||
cached_frame_count.store(0);
|
||||
last_cached_index.store(playhead - dir);
|
||||
}
|
||||
|
||||
// While user is dragging/scrubbing, skip cache prefetch work.
|
||||
if (scrub_active.load()) {
|
||||
std::this_thread::sleep_for(double_micro_sec(10000));
|
||||
continue;
|
||||
}
|
||||
|
||||
// If capacity is insufficient, sleep and retry
|
||||
if (capacity < 1) {
|
||||
std::this_thread::sleep_for(double_micro_sec(50000));
|
||||
@@ -411,7 +643,8 @@ namespace openshot
|
||||
ready_target = 0;
|
||||
}
|
||||
int64_t configured_min = settings->VIDEO_CACHE_MIN_PREROLL_FRAMES;
|
||||
min_frames_ahead.store(std::min<int64_t>(configured_min, ready_target));
|
||||
const int64_t required_ahead = std::min<int64_t>(configured_min, ready_target);
|
||||
min_frames_ahead.store(required_ahead);
|
||||
|
||||
// If paused and playhead is no longer in cache, clear everything
|
||||
bool did_clear = clearCacheIfPaused(playhead, paused, cache);
|
||||
@@ -429,7 +662,21 @@ namespace openshot
|
||||
window_end);
|
||||
|
||||
// Attempt to fill any missing frames in that window
|
||||
bool window_full = prefetchWindow(cache, window_begin, window_end, dir, reader);
|
||||
int64_t max_frames_to_fetch = -1;
|
||||
if (!paused) {
|
||||
// Keep cache thread responsive during playback seeks so player
|
||||
// can start as soon as pre-roll is met instead of waiting for a
|
||||
// full cache window pass.
|
||||
max_frames_to_fetch = 8;
|
||||
}
|
||||
bool window_full = prefetchWindow(
|
||||
cache,
|
||||
window_begin,
|
||||
window_end,
|
||||
dir,
|
||||
reader,
|
||||
max_frames_to_fetch
|
||||
);
|
||||
|
||||
// If paused and window was already full, keep playhead fresh
|
||||
if (paused && window_full) {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <AppConfig.h>
|
||||
#include <juce_audio_basics/juce_audio_basics.h>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <memory>
|
||||
|
||||
@@ -61,7 +62,7 @@ namespace openshot
|
||||
/// @return The current speed (1=normal, 2=fast, –1=rewind, etc.)
|
||||
int getSpeed() const { return speed.load(); }
|
||||
|
||||
/// Seek to a specific frame (no preroll).
|
||||
/// Backward-compatible alias for playback position updates (no seek side effects).
|
||||
void Seek(int64_t new_position);
|
||||
|
||||
/**
|
||||
@@ -71,6 +72,9 @@ namespace openshot
|
||||
*/
|
||||
void Seek(int64_t new_position, bool start_preroll);
|
||||
|
||||
/// Update playback position without triggering seek behavior or cache invalidation.
|
||||
void NotifyPlaybackPosition(int64_t new_position);
|
||||
|
||||
/// Start the cache thread at high priority. Returns true if it’s actually running.
|
||||
bool StartThread();
|
||||
|
||||
@@ -81,7 +85,7 @@ namespace openshot
|
||||
* @brief Attach a ReaderBase (e.g. Timeline, FFmpegReader) and begin caching.
|
||||
* @param new_reader
|
||||
*/
|
||||
void Reader(ReaderBase* new_reader) { reader = new_reader; Play(); }
|
||||
void Reader(ReaderBase* new_reader);
|
||||
|
||||
protected:
|
||||
/// Thread entry point: loops until threadShouldExit() is true.
|
||||
@@ -125,6 +129,12 @@ namespace openshot
|
||||
/// @brief Compute preroll frame count from settings.
|
||||
int64_t computePrerollFrames(const Settings* settings) const;
|
||||
|
||||
/// @brief Resolve timeline end frame from reader/timeline metadata.
|
||||
int64_t resolveTimelineEnd() const;
|
||||
|
||||
/// @brief Clamp frame index to [1, timeline_end] when timeline_end is valid.
|
||||
int64_t clampToTimelineRange(int64_t frame, int64_t timeline_end) const;
|
||||
|
||||
/**
|
||||
* @brief When paused and playhead is outside current cache, clear all frames.
|
||||
* @param playhead Current requested_display_frame
|
||||
@@ -171,7 +181,8 @@ namespace openshot
|
||||
int64_t window_begin,
|
||||
int64_t window_end,
|
||||
int dir,
|
||||
ReaderBase* reader);
|
||||
ReaderBase* reader,
|
||||
int64_t max_frames_to_fetch = -1);
|
||||
|
||||
//---------- Internal state ----------
|
||||
|
||||
@@ -182,6 +193,8 @@ namespace openshot
|
||||
std::atomic<int> last_dir; ///< Last direction sign (+1 forward, –1 backward).
|
||||
std::atomic<bool> userSeeked; ///< True if Seek(..., true) was called (forces a cache reset).
|
||||
std::atomic<bool> preroll_on_next_fill; ///< True if next cache rebuild should include preroll offset.
|
||||
std::atomic<bool> clear_cache_on_next_fill; ///< True if next cache loop should clear existing cache ranges.
|
||||
std::atomic<bool> scrub_active; ///< True while user is dragging/scrubbing the playhead.
|
||||
|
||||
std::atomic<int64_t> requested_display_frame; ///< Frame index the user requested.
|
||||
int64_t current_display_frame; ///< Currently displayed frame (unused here, reserved).
|
||||
@@ -192,6 +205,8 @@ namespace openshot
|
||||
|
||||
ReaderBase* reader; ///< The source reader (e.g., Timeline, FFmpegReader).
|
||||
bool force_directional_cache; ///< (Reserved for future use).
|
||||
uint64_t seen_timeline_cache_epoch; ///< Last observed Timeline cache invalidation epoch.
|
||||
bool timeline_cache_epoch_initialized; ///< True once an initial epoch snapshot has been taken.
|
||||
|
||||
std::atomic<int64_t> last_cached_index; ///< Index of the most recently cached frame.
|
||||
mutable std::mutex seek_state_mutex; ///< Protects coherent seek state updates/consumption.
|
||||
|
||||
+6
-1
@@ -164,11 +164,16 @@ namespace openshot
|
||||
}
|
||||
|
||||
void QtPlayer::Seek(int64_t new_frame)
|
||||
{
|
||||
Seek(new_frame, true);
|
||||
}
|
||||
|
||||
void QtPlayer::Seek(int64_t new_frame, bool start_preroll)
|
||||
{
|
||||
// Check for seek
|
||||
if (reader && threads_started && new_frame > 0) {
|
||||
// Notify cache thread that seek has occurred
|
||||
p->videoCache->Seek(new_frame, true);
|
||||
p->videoCache->Seek(new_frame, start_preroll);
|
||||
|
||||
// Notify audio thread that seek has occurred
|
||||
p->audioPlayback->Seek(new_frame);
|
||||
|
||||
@@ -75,6 +75,9 @@ namespace openshot
|
||||
/// Seek to a specific frame in the player
|
||||
void Seek(int64_t new_frame);
|
||||
|
||||
/// Seek to a specific frame, optionally triggering preroll cache rebuild.
|
||||
void Seek(int64_t new_frame, bool start_preroll);
|
||||
|
||||
/// Set the source URL/path of this player (which will create an internal Reader)
|
||||
void SetSource(const std::string &source);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
+43
-16
@@ -22,6 +22,7 @@
|
||||
#include <algorithm>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QRegularExpression>
|
||||
#include <unordered_map>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
@@ -30,7 +31,7 @@ using namespace openshot;
|
||||
|
||||
// Default Constructor for the timeline (which sets the canvas width and height)
|
||||
Timeline::Timeline(int width, int height, Fraction fps, int sample_rate, int channels, ChannelLayout channel_layout) :
|
||||
is_open(false), auto_map_clips(true), managed_cache(true), path(""), max_time(0.0)
|
||||
is_open(false), auto_map_clips(true), managed_cache(true), path(""), max_time(0.0), cache_epoch(0), safe_edit_frames_remaining(0)
|
||||
{
|
||||
// Create CrashHandler and Attach (incase of errors)
|
||||
CrashHandler::Instance();
|
||||
@@ -81,7 +82,7 @@ Timeline::Timeline(const ReaderInfo info) : Timeline::Timeline(
|
||||
|
||||
// Constructor for the timeline (which loads a JSON structure from a file path, and initializes a timeline)
|
||||
Timeline::Timeline(const std::string& projectPath, bool convert_absolute_paths) :
|
||||
is_open(false), auto_map_clips(true), managed_cache(true), path(projectPath), max_time(0.0) {
|
||||
is_open(false), auto_map_clips(true), managed_cache(true), path(projectPath), max_time(0.0), cache_epoch(0), safe_edit_frames_remaining(0) {
|
||||
|
||||
// Create CrashHandler and Attach (incase of errors)
|
||||
CrashHandler::Instance();
|
||||
@@ -585,7 +586,7 @@ std::shared_ptr<Frame> Timeline::apply_effects(std::shared_ptr<Frame> frame, int
|
||||
"does_effect_intersect", does_effect_intersect);
|
||||
|
||||
// Apply the effect to this frame
|
||||
frame = effect->GetFrame(frame, effect_frame_number);
|
||||
frame = effect->ProcessFrame(frame, effect_frame_number);
|
||||
}
|
||||
|
||||
} // end effect loop
|
||||
@@ -632,12 +633,13 @@ std::shared_ptr<Frame> Timeline::GetOrCreateFrame(std::shared_ptr<Frame> backgro
|
||||
}
|
||||
|
||||
// Process a new layer of video or audio
|
||||
void Timeline::add_layer(std::shared_ptr<Frame> new_frame, Clip* source_clip, int64_t clip_frame_number, bool is_top_clip, float max_volume)
|
||||
void Timeline::add_layer(std::shared_ptr<Frame> new_frame, Clip* source_clip, int64_t clip_frame_number, bool is_top_clip, bool force_safe_composite, float max_volume)
|
||||
{
|
||||
// Create timeline options (with details about this current frame request)
|
||||
TimelineInfoStruct options{};
|
||||
options.is_top_clip = is_top_clip;
|
||||
options.is_before_clip_keyframes = true;
|
||||
options.force_safe_composite = force_safe_composite;
|
||||
|
||||
// Get the clip's frame, composited on top of the current timeline frame
|
||||
std::shared_ptr<Frame> source_frame;
|
||||
@@ -942,10 +944,13 @@ std::shared_ptr<Frame> Timeline::GetFrame(int64_t requested_frame)
|
||||
// Adjust out of bounds frame number
|
||||
if (requested_frame < 1)
|
||||
requested_frame = 1;
|
||||
const int64_t max_frame = GetMaxFrame();
|
||||
const bool past_timeline_end = (max_frame > 0 && requested_frame > max_frame);
|
||||
|
||||
// Check cache
|
||||
std::shared_ptr<Frame> frame;
|
||||
frame = final_cache->GetFrame(requested_frame);
|
||||
if (!past_timeline_end)
|
||||
frame = final_cache->GetFrame(requested_frame);
|
||||
if (frame) {
|
||||
// Debug output
|
||||
ZmqLogger::Instance()->AppendDebugMethod(
|
||||
@@ -962,7 +967,8 @@ std::shared_ptr<Frame> Timeline::GetFrame(int64_t requested_frame)
|
||||
|
||||
// Check cache 2nd time
|
||||
std::shared_ptr<Frame> frame;
|
||||
frame = final_cache->GetFrame(requested_frame);
|
||||
if (!past_timeline_end)
|
||||
frame = final_cache->GetFrame(requested_frame);
|
||||
if (frame) {
|
||||
// Debug output
|
||||
ZmqLogger::Instance()->AppendDebugMethod(
|
||||
@@ -1058,6 +1064,11 @@ std::shared_ptr<Frame> Timeline::GetFrame(int64_t requested_frame)
|
||||
}
|
||||
|
||||
// Compose intersecting clips in a single pass
|
||||
const int safe_remaining = safe_edit_frames_remaining.load(std::memory_order_relaxed);
|
||||
const bool force_safe_composite = (safe_remaining > 0);
|
||||
if (force_safe_composite) {
|
||||
safe_edit_frames_remaining.fetch_sub(1, std::memory_order_relaxed);
|
||||
}
|
||||
for (const auto& ci : clip_infos) {
|
||||
// Debug output
|
||||
ZmqLogger::Instance()->AppendDebugMethod(
|
||||
@@ -1088,7 +1099,7 @@ std::shared_ptr<Frame> Timeline::GetFrame(int64_t requested_frame)
|
||||
"clip_frame_number", clip_frame_number);
|
||||
|
||||
// Add clip's frame as layer
|
||||
add_layer(new_frame, ci.clip, clip_frame_number, is_top_clip, max_volume_sum);
|
||||
add_layer(new_frame, ci.clip, clip_frame_number, is_top_clip, force_safe_composite, max_volume_sum);
|
||||
|
||||
} else {
|
||||
// Debug output
|
||||
@@ -1110,9 +1121,9 @@ std::shared_ptr<Frame> Timeline::GetFrame(int64_t requested_frame)
|
||||
// Set frame # on mapped frame
|
||||
new_frame->SetFrameNumber(requested_frame);
|
||||
|
||||
// Add final frame to cache
|
||||
final_cache->Add(new_frame);
|
||||
|
||||
// Add final frame to cache (only for valid timeline range)
|
||||
if (!past_timeline_end)
|
||||
final_cache->Add(new_frame);
|
||||
// Return frame (or blank frame)
|
||||
return new_frame;
|
||||
}
|
||||
@@ -1344,6 +1355,9 @@ void Timeline::SetJsonValue(const Json::Value root) {
|
||||
// Re-open if needed
|
||||
if (was_open)
|
||||
Open();
|
||||
|
||||
// Timeline content changed: notify cache clients to rescan active window.
|
||||
BumpCacheEpoch();
|
||||
}
|
||||
|
||||
// Apply a special formatted JSON object, which represents a change to the timeline (insert, update, delete)
|
||||
@@ -1374,6 +1388,13 @@ void Timeline::ApplyJsonDiff(std::string value) {
|
||||
apply_json_to_timeline(change);
|
||||
|
||||
}
|
||||
|
||||
// Timeline content changed: notify cache clients to rescan active window.
|
||||
if (!root.empty()) {
|
||||
// After edits, force safe composition for a short window.
|
||||
safe_edit_frames_remaining.store(240, std::memory_order_relaxed);
|
||||
BumpCacheEpoch();
|
||||
}
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
@@ -1382,6 +1403,10 @@ void Timeline::ApplyJsonDiff(std::string value) {
|
||||
}
|
||||
}
|
||||
|
||||
void Timeline::BumpCacheEpoch() {
|
||||
cache_epoch.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
// Apply JSON diff to clips
|
||||
void Timeline::apply_json_to_clips(Json::Value change) {
|
||||
|
||||
@@ -1434,6 +1459,11 @@ void Timeline::apply_json_to_clips(Json::Value change) {
|
||||
// Apply the change to the effect directly
|
||||
apply_json_to_effects(change, e);
|
||||
|
||||
// Effect-only diffs must clear the owning clip cache.
|
||||
if (existing_clip->GetCache()) {
|
||||
existing_clip->GetCache()->Clear();
|
||||
}
|
||||
|
||||
// Calculate start and end frames that this impacts, and remove those frames from the cache
|
||||
int64_t new_starting_frame = (existing_clip->Position() * info.fps.ToDouble()) + 1;
|
||||
int64_t new_ending_frame = ((existing_clip->Position() + existing_clip->Duration()) * info.fps.ToDouble()) + 1;
|
||||
@@ -1485,12 +1515,6 @@ void Timeline::apply_json_to_clips(Json::Value change) {
|
||||
final_cache->Remove(old_starting_frame - 8, old_ending_frame + 8);
|
||||
final_cache->Remove(new_starting_frame - 8, new_ending_frame + 8);
|
||||
|
||||
// Remove cache on clip's Reader (if found)
|
||||
if (existing_clip->Reader() && existing_clip->Reader()->GetCache()) {
|
||||
existing_clip->Reader()->GetCache()->Remove(old_starting_frame - 8, old_ending_frame + 8);
|
||||
existing_clip->Reader()->GetCache()->Remove(new_starting_frame - 8, new_ending_frame + 8);
|
||||
}
|
||||
|
||||
// Apply framemapper (or update existing framemapper)
|
||||
if (auto_map_clips) {
|
||||
apply_mapper_to_clip(existing_clip);
|
||||
@@ -1785,6 +1809,9 @@ void Timeline::ClearAllCache(bool deep) {
|
||||
} catch (const ReaderClosed & e) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// Cache content changed: notify cache clients to rebuild their window baseline.
|
||||
BumpCacheEpoch();
|
||||
}
|
||||
|
||||
// Set Max Image Size (used for performance optimization). Convenience function for setting
|
||||
|
||||
+12
-5
@@ -15,11 +15,9 @@
|
||||
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <QtGui/QImage>
|
||||
#include <QtGui/QPainter>
|
||||
#include <QtCore/QRegularExpression>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
|
||||
#include "TimelineBase.h"
|
||||
#include "ReaderBase.h"
|
||||
@@ -167,11 +165,13 @@ namespace openshot {
|
||||
std::string path; ///< Optional path of loaded UTF-8 OpenShot JSON project file
|
||||
double max_time; ///> The max duration (in seconds) of the timeline, based on the furthest clip (right edge)
|
||||
double min_time; ///> The min duration (in seconds) of the timeline, based on the position of the first clip (left edge)
|
||||
std::atomic<uint64_t> cache_epoch; ///< Cache invalidation epoch for external observers.
|
||||
std::atomic<int> safe_edit_frames_remaining; ///< Remaining composed frames forced into safe edit composition.
|
||||
|
||||
std::map<std::string, std::shared_ptr<openshot::TrackedObjectBase>> tracked_objects; ///< map of TrackedObjectBBoxes and their IDs
|
||||
|
||||
/// Process a new layer of video or audio
|
||||
void add_layer(std::shared_ptr<openshot::Frame> new_frame, openshot::Clip* source_clip, int64_t clip_frame_number, bool is_top_clip, float max_volume);
|
||||
void add_layer(std::shared_ptr<openshot::Frame> new_frame, openshot::Clip* source_clip, int64_t clip_frame_number, bool is_top_clip, bool force_safe_composite, float max_volume);
|
||||
|
||||
/// Apply a FrameMapper to a clip which matches the settings of this timeline
|
||||
void apply_mapper_to_clip(openshot::Clip* clip);
|
||||
@@ -211,6 +211,9 @@ namespace openshot {
|
||||
/// Update the list of 'opened' clips
|
||||
void update_open_clips(openshot::Clip *clip, bool does_clip_intersect);
|
||||
|
||||
/// Increment the cache invalidation epoch.
|
||||
void BumpCacheEpoch();
|
||||
|
||||
public:
|
||||
|
||||
/// @brief Constructor for the timeline (which configures the default frame properties)
|
||||
@@ -313,6 +316,10 @@ namespace openshot {
|
||||
/// of this cache object though (Timeline will not delete it for you).
|
||||
void SetCache(openshot::CacheBase* new_cache);
|
||||
|
||||
/// Return the current cache invalidation epoch.
|
||||
uint64_t CacheEpoch() const { return cache_epoch.load(std::memory_order_relaxed); };
|
||||
int SafeEditFramesRemaining() const { return safe_edit_frames_remaining.load(std::memory_order_relaxed); };
|
||||
|
||||
/// Get an openshot::Frame object for a specific frame number of this timeline.
|
||||
///
|
||||
/// @returns The requested frame (containing the image)
|
||||
|
||||
@@ -33,6 +33,7 @@ namespace openshot {
|
||||
{
|
||||
bool is_top_clip; ///< Is clip on top (if overlapping another clip)
|
||||
bool is_before_clip_keyframes; ///< Is this before clip keyframes are applied
|
||||
bool force_safe_composite; ///< If true, avoid mutating cached clip images during composition
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user