From a38b07d321d7eb20f1e0f449e89fb020f5b13d25 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Mon, 16 Mar 2026 12:54:13 -0500 Subject: [PATCH 1/3] Updating dependency for OpenShotAudio library to 0.6.0 --- src/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From 621393563e851c9de2cd00a6b6c4c88c9155a428 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Mon, 16 Mar 2026 12:56:47 -0500 Subject: [PATCH 2/3] Bumping version to 0.6.0, SO 29, for next major release of OpenShot 3.5 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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}") From 5ee9d1b8b2090b3fbb6d58778b0df54d771a7400 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Fri, 20 Mar 2026 14:34:28 -0500 Subject: [PATCH 3/3] Fixing a bug where a non-EOF frame is otherwise ready but still has no image data: instead of stalling indefinitely, it reuses the most recent non-future image and finalizes the frame. Added new unit tests to verify the fix. --- src/FFmpegReader.cpp | 22 +++++++++++++++++++ tests/FFmpegReader.cpp | 50 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) 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;