diff --git a/CMakeLists.txt b/CMakeLists.txt
index c8a559ad3..6ba967735 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -24,8 +24,8 @@ For more information, please visit .
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/Modules")
################ PROJECT VERSION ####################
-set(PROJECT_VERSION_FULL "0.5.0")
-set(PROJECT_SO_VERSION 28)
+set(PROJECT_VERSION_FULL "0.6.0")
+set(PROJECT_SO_VERSION 29)
# Remove the dash and anything following, to get the #.#.# version for project()
STRING(REGEX REPLACE "\-.*$" "" VERSION_NUM "${PROJECT_VERSION_FULL}")
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 6e22574c5..6adbb3529 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -184,7 +184,7 @@ target_include_directories(openshot
# Find JUCE-based openshot Audio libraries
if(NOT TARGET OpenShot::Audio)
# Only load if necessary (not for integrated builds)
- find_package(OpenShotAudio 0.4.0 REQUIRED)
+ find_package(OpenShotAudio 0.6.0 REQUIRED)
endif()
target_link_libraries(openshot PUBLIC OpenShot::Audio)
diff --git a/src/FFmpegReader.cpp b/src/FFmpegReader.cpp
index 5d9e4a9f6..b63cf9db9 100644
--- a/src/FFmpegReader.cpp
+++ b/src/FFmpegReader.cpp
@@ -2827,6 +2827,28 @@ void FFmpegReader::CheckWorkingFrames(int64_t requested_frame) {
}
}
+ // If both streams have advanced past this frame but the decoder never
+ // produced image data for it, reuse the most recent non-future image.
+ // This avoids stalling indefinitely on sparse/missing decoded frames.
+ if (!f->has_image_data && is_video_ready && is_audio_ready) {
+ std::shared_ptr previous_frame_instance = final_cache.GetFrame(f->number - 1);
+ if (previous_frame_instance && previous_frame_instance->has_image_data) {
+ f->AddImage(std::make_shared(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(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(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) {
diff --git a/tests/FFmpegReader.cpp b/tests/FFmpegReader.cpp
index b31b0b128..395c5954c 100644
--- a/tests/FFmpegReader.cpp
+++ b/tests/FFmpegReader.cpp
@@ -22,7 +22,9 @@
#include "openshot_catch.h"
+#define private public
#include "FFmpegReader.h"
+#undef private
#include "Exceptions.h"
#include "Frame.h"
#include "Timeline.h"
@@ -528,6 +530,54 @@ TEST_CASE( "Attached_Picture_Audio_Does_Not_Stall_Early_Frames", "[libopenshot][
std::remove(fixture_path.str().c_str());
}
+TEST_CASE( "Missing_Image_Frame_Finalizes_Using_Previous_Image", "[libopenshot][ffmpegreader]" )
+{
+ FFmpegReader r("synthetic-missing-image", DurationStrategy::VideoPreferred, false);
+
+ r.info.has_video = true;
+ r.info.has_audio = true;
+ r.info.has_single_image = false;
+ r.info.width = 320;
+ r.info.height = 240;
+ r.info.fps = Fraction(30, 1);
+ r.info.sample_rate = 48000;
+ r.info.channels = 2;
+ r.info.channel_layout = LAYOUT_STEREO;
+ r.info.video_length = 120;
+ r.info.video_timebase = Fraction(1, 30);
+ r.info.audio_timebase = Fraction(1, 48000);
+
+ r.pts_offset_seconds = 0.0;
+ r.last_frame = 58;
+ r.video_pts_seconds = 2.233333;
+ r.audio_pts_seconds = 3.100000;
+ r.packet_status.video_eof = false;
+ r.packet_status.audio_eof = false;
+ r.packet_status.packets_eof = false;
+ r.packet_status.end_of_file = false;
+
+ const int samples_per_frame = Frame::GetSamplesPerFrame(
+ 58, r.info.fps, r.info.sample_rate, r.info.channels);
+ auto previous = std::make_shared(
+ 58, r.info.width, r.info.height, "#112233", samples_per_frame, r.info.channels);
+ previous->AddColor(r.info.width, r.info.height, "#112233");
+ r.final_cache.Add(previous);
+ r.last_final_video_frame = previous;
+
+ auto missing = r.CreateFrame(59);
+ r.working_cache.Add(missing);
+ REQUIRE(missing != nullptr);
+ REQUIRE_FALSE(missing->has_image_data);
+
+ r.CheckWorkingFrames(59);
+
+ auto finalized = r.final_cache.GetFrame(59);
+ REQUIRE(finalized != nullptr);
+ CHECK(finalized->has_image_data);
+ CHECK(finalized->CheckPixel(0, 0, 17, 34, 51, 255, 0));
+ CHECK(r.final_cache.GetFrame(58) != nullptr);
+}
+
TEST_CASE( "HardwareDecodeSuccessful_IsFalse_WhenHardwareDecodeIsDisabled", "[libopenshot][ffmpegreader][hardware]" )
{
HardwareDecoderSettingsGuard guard;