From c95fd837b686e0548d6f5d8f39f0bb22cef2f67d Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Wed, 4 Jun 2025 17:18:23 -0500 Subject: [PATCH 01/20] Small improvement to caching sws scale context and reusing AVFrame objects. About 1.5% less CPU calls, and more even memory allocations (less spikey). --- src/FFmpegReader.cpp | 58 ++++++++++++++++++++++++++------------------ src/FFmpegReader.h | 5 ++++ 2 files changed, 40 insertions(+), 23 deletions(-) diff --git a/src/FFmpegReader.cpp b/src/FFmpegReader.cpp index c42cc386..afdff878 100644 --- a/src/FFmpegReader.cpp +++ b/src/FFmpegReader.cpp @@ -74,8 +74,8 @@ FFmpegReader::FFmpegReader(const std::string &path, bool inspect_reader) seek_audio_frame_found(0), seek_video_frame_found(0),is_duration_known(false), largest_frame_processed(0), current_video_frame(0), packet(NULL), max_concurrent_frames(OPEN_MP_NUM_PROCESSORS), audio_pts(0), video_pts(0), pFormatCtx(NULL), videoStream(-1), audioStream(-1), pCodecCtx(NULL), aCodecCtx(NULL), - pStream(NULL), aStream(NULL), pFrame(NULL), previous_packet_location{-1,0}, - hold_packet(false) { + pStream(NULL), aStream(NULL), pFrame(NULL), previous_packet_location{-1,0}, + hold_packet(false) { // Initialize FFMpeg, and register all formats and codecs AV_REGISTER_ALL @@ -678,6 +678,13 @@ void FFmpegReader::Close() { } } #endif // USE_HW_ACCEL + if (img_convert_ctx) { + sws_freeContext(img_convert_ctx); + img_convert_ctx = nullptr; + } + if (pFrameRGB_cached) { + AV_FREE_FRAME(&pFrameRGB_cached); + } } // Close the audio codec @@ -686,6 +693,11 @@ void FFmpegReader::Close() { avcodec_flush_buffers(aCodecCtx); } AV_FREE_CONTEXT(aCodecCtx); + if (avr_ctx) { + SWR_CLOSE(avr_ctx); + SWR_FREE(&avr_ctx); + avr_ctx = nullptr; + } } // Clear final cache @@ -1469,15 +1481,17 @@ void FFmpegReader::ProcessVideoPacket(int64_t requested_frame) { int width = info.width; int64_t video_length = info.video_length; - // Create variables for a RGB Frame (since most videos are not in RGB, we must convert it) - AVFrame *pFrameRGB = nullptr; + // Create or reuse a RGB Frame (since most videos are not in RGB, we must convert it) + AVFrame *pFrameRGB = pFrameRGB_cached; + if (!pFrameRGB) { + pFrameRGB = AV_ALLOCATE_FRAME(); + if (pFrameRGB == nullptr) + throw OutOfMemory("Failed to allocate frame buffer", path); + pFrameRGB_cached = pFrameRGB; + } + AV_RESET_FRAME(pFrameRGB); uint8_t *buffer = nullptr; - // Allocate an AVFrame structure - pFrameRGB = AV_ALLOCATE_FRAME(); - if (pFrameRGB == nullptr) - throw OutOfMemory("Failed to allocate frame buffer", path); - // Determine the max size of this source image (based on the timeline's size, the scaling mode, // and the scaling keyframes). This is a performance improvement, to keep the images as small as possible, // without losing quality. NOTE: We cannot go smaller than the timeline itself, or the add_layer timeline @@ -1564,8 +1578,9 @@ void FFmpegReader::ProcessVideoPacket(int64_t requested_frame) { if (openshot::Settings::Instance()->HIGH_QUALITY_SCALING) { scale_mode = SWS_BICUBIC; } - SwsContext *img_convert_ctx = sws_getContext(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, info.width, info.height, AV_GET_CODEC_PIXEL_FORMAT(pStream, pCodecCtx), width, height, PIX_FMT_RGBA, scale_mode, NULL, NULL, NULL); + if (!img_convert_ctx) + throw OutOfMemory("Failed to initialize sws context", path); // Resize / Convert to RGB sws_scale(img_convert_ctx, pFrame->data, pFrame->linesize, 0, @@ -1590,11 +1605,10 @@ void FFmpegReader::ProcessVideoPacket(int64_t requested_frame) { last_video_frame = f; // Free the RGB image - AV_FREE_FRAME(&pFrameRGB); + AV_RESET_FRAME(pFrameRGB); - // Remove frame and packet - RemoveAVFrame(pFrame); - sws_freeContext(img_convert_ctx); + // Remove frame and packet + RemoveAVFrame(pFrame); // Get video PTS in seconds video_pts_seconds = (double(video_pts) * info.video_timebase.ToDouble()) + pts_offset_seconds; @@ -1738,10 +1752,10 @@ void FFmpegReader::ProcessAudioPacket(int64_t requested_frame) { audio_converted->nb_samples = audio_frame->nb_samples; av_samples_alloc(audio_converted->data, audio_converted->linesize, info.channels, audio_frame->nb_samples, AV_SAMPLE_FMT_FLTP, 0); - SWRCONTEXT *avr = NULL; - - // setup resample context - avr = SWR_ALLOC(); + SWRCONTEXT *avr = avr_ctx; + // setup resample context if needed + if (!avr) { + avr = SWR_ALLOC(); #if HAVE_CH_LAYOUT av_opt_set_chlayout(avr, "in_chlayout", &AV_GET_CODEC_ATTRIBUTES(aStream, aCodecCtx)->ch_layout, 0); av_opt_set_chlayout(avr, "out_chlayout", &AV_GET_CODEC_ATTRIBUTES(aStream, aCodecCtx)->ch_layout, 0); @@ -1756,6 +1770,8 @@ void FFmpegReader::ProcessAudioPacket(int64_t requested_frame) { av_opt_set_int(avr, "in_sample_rate", info.sample_rate, 0); av_opt_set_int(avr, "out_sample_rate", info.sample_rate, 0); SWR_INIT(avr); + avr_ctx = avr; + } // Convert audio samples int nb_samples = SWR_CONVERT(avr, // audio resample context @@ -1766,10 +1782,6 @@ void FFmpegReader::ProcessAudioPacket(int64_t requested_frame) { audio_frame->linesize[0], // input plane size, in bytes (0 if unknown) audio_frame->nb_samples); // number of input samples to convert - // Deallocate resample buffer - SWR_CLOSE(avr); - SWR_FREE(&avr); - avr = NULL; int64_t starting_frame_number = -1; for (int channel_filter = 0; channel_filter < info.channels; channel_filter++) { diff --git a/src/FFmpegReader.h b/src/FFmpegReader.h index 06727d62..4ccd9554 100644 --- a/src/FFmpegReader.h +++ b/src/FFmpegReader.h @@ -148,6 +148,11 @@ namespace openshot { int64_t NO_PTS_OFFSET; PacketStatus packet_status; + // Cached conversion contexts and frames for performance + SwsContext *img_convert_ctx = nullptr; ///< Cached video scaler context + SWRCONTEXT *avr_ctx = nullptr; ///< Cached audio resample context + AVFrame *pFrameRGB_cached = nullptr; ///< Temporary frame used for video conversion + int hw_de_supported = 0; // Is set by FFmpegReader #if USE_HW_ACCEL AVPixelFormat hw_de_av_pix_fmt = AV_PIX_FMT_NONE; From 6e73a23259cee10c62c8bf4fd38d878cd77ada03 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Wed, 4 Jun 2025 17:19:01 -0500 Subject: [PATCH 02/20] Changing example program to time decoding of test video, forwards and backwards, with time print-outs. --- examples/Example.cpp | 71 +++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 40 deletions(-) diff --git a/examples/Example.cpp b/examples/Example.cpp index 4c7b96c2..150ee63a 100644 --- a/examples/Example.cpp +++ b/examples/Example.cpp @@ -10,59 +10,50 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later -#include +#include #include #include -#include -#include "Clip.h" #include "Frame.h" #include "FFmpegReader.h" -#include "Timeline.h" -#include "Profiles.h" using namespace openshot; - int main(int argc, char* argv[]) { - QString filename = "/home/jonathan/test-crash.osp"; - //QString filename = "/home/jonathan/Downloads/drive-download-20221123T185423Z-001/project-3363/project-3363.osp"; - //QString filename = "/home/jonathan/Downloads/drive-download-20221123T185423Z-001/project-3372/project-3372.osp"; - //QString filename = "/home/jonathan/Downloads/drive-download-20221123T185423Z-001/project-3512/project-3512.osp"; - QString project_json = ""; - QFile file(filename); - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - std::cout << "File error!" << std::endl; - exit(1); - } else { - while (!file.atEnd()) { - QByteArray line = file.readLine(); - project_json += line; - } - } + // Path to your video + const char* path = "/home/jonathan/Downloads/openshot-testing/sintel_trailer-720p.mp4"; - // Open timeline reader - std::cout << "Project JSON length: " << project_json.length() << std::endl; - Timeline r(1280, 720, openshot::Fraction(30, 1), 44100, 2, openshot::LAYOUT_STEREO); - r.SetJson(project_json.toStdString()); - r.DisplayInfo(); + FFmpegReader r(path); r.Open(); - // Get max frame - int64_t max_frame = r.GetMaxFrame(); - std::cout << "max_frame: " << max_frame << ", r.info.video_length: " << r.info.video_length << std::endl; + const long int total_frames = r.info.video_length; - for (long int frame = 1; frame <= max_frame; frame++) - { - float percent = (float(frame) / max_frame) * 100.0; - std::cout << "Requesting Frame #: " << frame << " (" << percent << "%)" << std::endl; + // --- Measure forward pass --- + auto t0 = std::chrono::high_resolution_clock::now(); + for (long int frame = 1; frame <= total_frames; frame++) { + float percent = (float(frame) / total_frames) * 100.0f; + std::cout << "Forward: Requesting Frame #: " << frame + << " (" << percent << "%)\n"; std::shared_ptr f = r.GetFrame(frame); - - // Preview frame image - if (frame % 1 == 0) { - f->Save("preview.jpg", 1.0, "jpg", 100); - } + // (optional) preview or process f here } - r.Close(); + auto t1 = std::chrono::high_resolution_clock::now(); + auto forward_ms = std::chrono::duration_cast(t1 - t0).count(); - exit(0); + // --- Measure backward pass --- + auto t2 = std::chrono::high_resolution_clock::now(); + for (long int frame = total_frames; frame >= 1; frame--) { + float percent = (float(total_frames - frame + 1) / total_frames) * 100.0f; + std::cout << "Backward: Requesting Frame #: " << frame + << " (" << percent << "%)\n"; + std::shared_ptr f = r.GetFrame(frame); + // (optional) preview or process f here + } + auto t3 = std::chrono::high_resolution_clock::now(); + auto backward_ms = std::chrono::duration_cast(t3 - t2).count(); + + std::cout << "\nForward pass elapsed: " << forward_ms << " ms\n"; + std::cout << "Backward pass elapsed: " << backward_ms << " ms\n"; + + r.Close(); + return 0; } From c487aa43895ba646866e64c679b73b91cc7e6e49 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Wed, 4 Jun 2025 18:37:46 -0500 Subject: [PATCH 03/20] Fixing the alignment of our FFmpegReader video buffers to be 32 / AVX2. This increases performance by a factor of 3X. --- src/FFmpegReader.cpp | 8 ++++++-- src/FFmpegReader.h | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/FFmpegReader.cpp b/src/FFmpegReader.cpp index afdff878..21d847aa 100644 --- a/src/FFmpegReader.cpp +++ b/src/FFmpegReader.cpp @@ -1568,8 +1568,12 @@ void FFmpegReader::ProcessVideoPacket(int64_t requested_frame) { // Determine required buffer size and allocate buffer const int bytes_per_pixel = 4; - int buffer_size = (width * height * bytes_per_pixel) + 128; - buffer = new unsigned char[buffer_size](); + int raw_buffer_size = (width * height * bytes_per_pixel) + 128; + + // Aligned memory allocation (for speed) + constexpr size_t ALIGNMENT = 32; // AVX2 + int buffer_size = ((raw_buffer_size + ALIGNMENT - 1) / ALIGNMENT) * ALIGNMENT; + buffer = (unsigned char*) aligned_alloc(ALIGNMENT, buffer_size); // Copy picture data from one AVFrame (or AVPicture) to another one. AV_COPY_PICTURE_DATA(pFrameRGB, buffer, PIX_FMT_RGBA, width, height); diff --git a/src/FFmpegReader.h b/src/FFmpegReader.h index 4ccd9554..295cdcf3 100644 --- a/src/FFmpegReader.h +++ b/src/FFmpegReader.h @@ -31,6 +31,7 @@ #include "Clip.h" #include "OpenMPUtilities.h" #include "Settings.h" +#include namespace openshot { From b2333ab38900514ed9e7b4c61f475c278b62b9b3 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Wed, 4 Jun 2025 20:45:19 -0500 Subject: [PATCH 04/20] Refactoring aligned malloc to work on Mac, Windows, and Linux (inside FFmpegUtilities.h) --- src/FFmpegReader.cpp | 2 +- src/FFmpegUtilities.h | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/FFmpegReader.cpp b/src/FFmpegReader.cpp index 21d847aa..abce5733 100644 --- a/src/FFmpegReader.cpp +++ b/src/FFmpegReader.cpp @@ -1573,7 +1573,7 @@ void FFmpegReader::ProcessVideoPacket(int64_t requested_frame) { // Aligned memory allocation (for speed) constexpr size_t ALIGNMENT = 32; // AVX2 int buffer_size = ((raw_buffer_size + ALIGNMENT - 1) / ALIGNMENT) * ALIGNMENT; - buffer = (unsigned char*) aligned_alloc(ALIGNMENT, buffer_size); + buffer = (unsigned char*) aligned_malloc(buffer_size, ALIGNMENT); // Copy picture data from one AVFrame (or AVPicture) to another one. AV_COPY_PICTURE_DATA(pFrameRGB, buffer, PIX_FMT_RGBA, width, height); diff --git a/src/FFmpegUtilities.h b/src/FFmpegUtilities.h index f30f7fdc..6e27dc6c 100644 --- a/src/FFmpegUtilities.h +++ b/src/FFmpegUtilities.h @@ -298,5 +298,23 @@ inline static bool ffmpeg_has_alpha(PixelFormat pix_fmt) { #define AV_COPY_PARAMS_FROM_CONTEXT(av_stream, av_codec) #endif +// Aligned memory allocation helpers (cross-platform) +#if defined(_WIN32) +#include +#endif + +inline static void* aligned_malloc(size_t size, size_t alignment = 32) +{ +#if defined(_WIN32) + return _aligned_malloc(size, alignment); +#elif defined(__APPLE__) || defined(__linux__) + void* ptr = nullptr; + if (posix_memalign(&ptr, alignment, size) != 0) + return nullptr; + return ptr; +#else +#error "aligned_malloc not implemented on this platform" +#endif +} #endif // OPENSHOT_FFMPEG_UTILITIES_H From 38afcea242d4debc597769f382284d9074cd31fa Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Wed, 4 Jun 2025 21:29:31 -0500 Subject: [PATCH 05/20] Use aligned memory free for QImage / frame buffer (fix crash on Win32 running unit tests for FFmpegReader) --- src/QtUtilities.h | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/QtUtilities.h b/src/QtUtilities.h index 410ffefb..adfa6b6a 100644 --- a/src/QtUtilities.h +++ b/src/QtUtilities.h @@ -4,7 +4,7 @@ * @author FeRD (Frank Dana) */ -// Copyright (c) 2008-2020 OpenShot Studios, LLC +// Copyright (c) 2008-2025 OpenShot Studios, LLC // // SPDX-License-Identifier: LGPL-3.0-or-later @@ -14,6 +14,7 @@ #include #include #include +#include // Fix Qt::endl for older Qt versions // From: https://bugreports.qt.io/browse/QTBUG-82680 @@ -26,14 +27,25 @@ namespace Qt { namespace openshot { + + // Cross-platform aligned free function + inline void aligned_free(void* ptr) + { +#if defined(_MSC_VER) + _aligned_free(ptr); +#else + free(ptr); +#endif + } + // Clean up buffer after QImage is deleted static inline void cleanUpBuffer(void *info) { if (!info) return; - // Remove buffer since QImage tells us to - uint8_t *qbuffer = reinterpret_cast(info); - delete[] qbuffer; + + // Free the aligned memory buffer + aligned_free(info); } } // namespace From 60fbb7fe5e2fb3c4ccdb953f333a548b4cd3a2c9 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Wed, 4 Jun 2025 22:42:34 -0500 Subject: [PATCH 06/20] Fix incorrect _MSC_VER and replace with _WIN32 --- src/QtUtilities.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/QtUtilities.h b/src/QtUtilities.h index adfa6b6a..54106f71 100644 --- a/src/QtUtilities.h +++ b/src/QtUtilities.h @@ -31,7 +31,7 @@ namespace openshot { // Cross-platform aligned free function inline void aligned_free(void* ptr) { -#if defined(_MSC_VER) +#if defined(_WIN32) _aligned_free(ptr); #else free(ptr); From 81a04a12f8fba25d7d192294abf51c94be6e1b52 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Wed, 4 Jun 2025 23:03:23 -0500 Subject: [PATCH 07/20] Add libopenshot unit tests back into Mac builder GitLab CI script --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a9e5d4cf..d11cc72b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -66,6 +66,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 - 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" From 32f291c4c3cffeef77ea59bbd7aedd567e830ef9 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Wed, 4 Jun 2025 23:46:09 -0500 Subject: [PATCH 08/20] Fixing unit test building on Mac runner --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d11cc72b..d84f8c08 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -62,6 +62,7 @@ mac-builder: - fi - unzip artifacts.zip - export LIBOPENSHOT_AUDIO_DIR=$CI_PROJECT_DIR/build/install-x64 + - export DYLD_LIBRARY_PATH="$CI_PROJECT_DIR/build/install-x64/lib" - mkdir -p build; cd build; - 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 From 15936baf46174aac45e578d44a09307fed480090 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Thu, 5 Jun 2025 00:05:42 -0500 Subject: [PATCH 09/20] Experiment for Mac unit tests --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d84f8c08..2004db75 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -65,9 +65,9 @@ mac-builder: - export DYLD_LIBRARY_PATH="$CI_PROJECT_DIR/build/install-x64/lib" - mkdir -p build; cd build; - 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 + - DYLD_LIBRARY_PATH="$CI_PROJECT_DIR/build/install-x64/lib" make -j 9 - make install - - make test + - DYLD_LIBRARY_PATH="$CI_PROJECT_DIR/build/install-x64/lib" make test - 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" From 2003ae1d92df6ed77cccf9315a360bcc15d4a8c6 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Thu, 5 Jun 2025 00:10:42 -0500 Subject: [PATCH 10/20] Experiment for Mac unit tests --- .gitlab-ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2004db75..bb19fee2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -65,9 +65,9 @@ mac-builder: - export DYLD_LIBRARY_PATH="$CI_PROJECT_DIR/build/install-x64/lib" - mkdir -p build; cd build; - 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" ../ - - DYLD_LIBRARY_PATH="$CI_PROJECT_DIR/build/install-x64/lib" make -j 9 - - make install - - DYLD_LIBRARY_PATH="$CI_PROJECT_DIR/build/install-x64/lib" make test + - make -j 9 + - cd $CI_PROJECT_DIR && make -C build test + - cd build && make install - 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" From 54b33ed609a5414b018358ebb4e83240c990df96 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Thu, 5 Jun 2025 00:23:30 -0500 Subject: [PATCH 11/20] Reverting experimental libopenshot Mac build changes related to "image not found" errors when running unit tests. --- .gitlab-ci.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bb19fee2..d11cc72b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -62,12 +62,11 @@ mac-builder: - fi - unzip artifacts.zip - export LIBOPENSHOT_AUDIO_DIR=$CI_PROJECT_DIR/build/install-x64 - - export DYLD_LIBRARY_PATH="$CI_PROJECT_DIR/build/install-x64/lib" - mkdir -p build; cd build; - 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 - - cd $CI_PROJECT_DIR && make -C build test - - cd build && make install + - make install + - make test - 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" From 5e4bc364cbd77645bf47bc177e36e13cfa894720 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Thu, 5 Jun 2025 19:38:15 -0500 Subject: [PATCH 12/20] Remove per-thread scalers; use single persistent frames and SwsContext for video scaling. Total improvement of 8-9% when testing h.264 encoding @ 720p. --- src/FFmpegWriter.cpp | 254 +++++++++++++++++++++---------------------- src/FFmpegWriter.h | 17 +-- 2 files changed, 131 insertions(+), 140 deletions(-) diff --git a/src/FFmpegWriter.cpp b/src/FFmpegWriter.cpp index 340f3657..ddfa67bc 100644 --- a/src/FFmpegWriter.cpp +++ b/src/FFmpegWriter.cpp @@ -9,7 +9,7 @@ * @ref License */ -// Copyright (c) 2008-2024 OpenShot Studios, LLC, Fabrice Bellard +// Copyright (c) 2008-2025 OpenShot Studios, LLC, Fabrice Bellard // // SPDX-License-Identifier: LGPL-3.0-or-later @@ -75,8 +75,8 @@ static int set_hwframe_ctx(AVCodecContext *ctx, AVBufferRef *hw_device_ctx, int6 FFmpegWriter::FFmpegWriter(const std::string& path) : path(path), oc(NULL), audio_st(NULL), video_st(NULL), samples(NULL), audio_outbuf(NULL), audio_outbuf_size(0), audio_input_frame_size(0), audio_input_position(0), - initial_audio_input_frame_size(0), img_convert_ctx(NULL), num_of_rescalers(1), - rescaler_position(0), video_codec_ctx(NULL), audio_codec_ctx(NULL), is_writing(false), video_timestamp(0), audio_timestamp(0), + 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) { @@ -414,8 +414,6 @@ void FFmpegWriter::SetOption(StreamType stream, std::string name, std::string va else if (name == "cqp") { // encode quality and special settings like lossless - // This might be better in an extra methods as more options - // and way to set quality are possible #if USE_HW_ACCEL if (hw_en_on) { av_opt_set_int(c->priv_data, "qp", std::min(std::stoi(value),63), 0); // 0-63 @@ -464,8 +462,6 @@ void FFmpegWriter::SetOption(StreamType stream, std::string name, std::string va } } else if (name == "crf") { // encode quality and special settings like lossless - // This might be better in an extra methods as more options - // and way to set quality are possible #if USE_HW_ACCEL if (hw_en_on) { double mbs = 15000000.0; @@ -539,8 +535,6 @@ void FFmpegWriter::SetOption(StreamType stream, std::string name, std::string va } } else if (name == "qp") { // encode quality and special settings like lossless - // This might be better in an extra methods as more options - // and way to set quality are possible #if (LIBAVCODEC_VERSION_MAJOR >= 58) // FFmpeg 4.0+ switch (c->codec_id) { @@ -605,10 +599,7 @@ bool FFmpegWriter::IsValidCodec(std::string codec_name) { AV_REGISTER_ALL // Find the codec (if any) - if (avcodec_find_encoder_by_name(codec_name.c_str()) == NULL) - return false; - else - return true; + return avcodec_find_encoder_by_name(codec_name.c_str()) != NULL; } // Prepare & initialize streams and open codecs @@ -972,9 +963,9 @@ void FFmpegWriter::Close() { if (audio_st) close_audio(oc, audio_st); - // Deallocate image scalers - if (image_rescalers.size() > 0) - RemoveScalers(); + // Remove single software scaler + if (img_convert_ctx) + sws_freeContext(img_convert_ctx); if (!(oc->oformat->flags & AVFMT_NOFILE)) { /* close the output file */ @@ -1025,7 +1016,7 @@ AVStream *FFmpegWriter::add_audio_stream() { // Create a new audio stream AVStream* st = avformat_new_stream(oc, codec); if (!st) - throw OutOfMemory("Could not allocate memory for the video stream.", path); + throw OutOfMemory("Could not allocate memory for the audio stream.", path); // Allocate a new codec context for the stream ALLOC_CODEC_CTX(audio_codec_ctx, codec, st) @@ -1058,7 +1049,7 @@ AVStream *FFmpegWriter::add_audio_stream() { // Set sample rate c->sample_rate = info.sample_rate; -uint64_t channel_layout = info.channel_layout; + uint64_t channel_layout = info.channel_layout; #if HAVE_CH_LAYOUT // Set a valid number of channels (or throw error) AVChannelLayout ch_layout; @@ -1117,9 +1108,9 @@ uint64_t channel_layout = info.channel_layout; AV_COPY_PARAMS_FROM_CONTEXT(st, c); -int nb_channels; -const char* nb_channels_label; -const char* channel_layout_label; + int nb_channels; + const char* nb_channels_label; + const char* channel_layout_label; #if HAVE_CH_LAYOUT nb_channels = c->ch_layout.nb_channels; @@ -1559,7 +1550,7 @@ void FFmpegWriter::open_video(AVFormatContext *oc, AVStream *st) { av_dict_free(&opts); // Add video metadata (if any) - for (std::map::iterator iter = info.metadata.begin(); iter != info.metadata.end(); ++iter) { + for (auto iter = info.metadata.begin(); iter != info.metadata.end(); ++iter) { av_dict_set(&st->metadata, iter->first.c_str(), iter->second.c_str(), 0); } @@ -2059,69 +2050,128 @@ AVFrame *FFmpegWriter::allocate_avframe(PixelFormat pix_fmt, int width, int heig // process video frame void FFmpegWriter::process_video_packet(std::shared_ptr frame) { - // Determine the height & width of the source image - int source_image_width = frame->GetWidth(); - int source_image_height = frame->GetHeight(); + // Source dimensions (RGBA) + int src_w = frame->GetWidth(); + int src_h = frame->GetHeight(); - // Do nothing if size is 1x1 (i.e. no image in this frame) - if (source_image_height == 1 && source_image_width == 1) + // Skip empty frames (1×1) + if (src_w == 1 && src_h == 1) return; - // Init rescalers (if not initialized yet) - if (image_rescalers.size() == 0) - InitScalers(source_image_width, source_image_height); + // Point persistent_src_frame->data to RGBA pixels + const uchar* pixels = frame->GetPixels(); + if (!persistent_src_frame) { + persistent_src_frame = av_frame_alloc(); + if (!persistent_src_frame) + throw OutOfMemory("Could not allocate persistent_src_frame", path); + persistent_src_frame->format = AV_PIX_FMT_RGBA; + persistent_src_frame->width = src_w; + persistent_src_frame->height = src_h; + persistent_src_frame->linesize[0] = src_w * 4; + } + persistent_src_frame->data[0] = const_cast( + reinterpret_cast(pixels) + ); - // Get a unique rescaler (for this thread) - SwsContext *scaler = image_rescalers[rescaler_position]; - rescaler_position++; - if (rescaler_position == num_of_rescalers) - rescaler_position = 0; + // Prepare persistent_dst_frame + buffer on first use + if (!persistent_dst_frame) { + persistent_dst_frame = av_frame_alloc(); + if (!persistent_dst_frame) + throw OutOfMemory("Could not allocate persistent_dst_frame", path); - // Allocate an RGB frame & final output frame - int bytes_source = 0; - int bytes_final = 0; - AVFrame *frame_source = NULL; - const uchar *pixels = NULL; - - // Get a list of pixels from source image - pixels = frame->GetPixels(); - - // Init AVFrame for source image & final (converted image) - frame_source = allocate_avframe(PIX_FMT_RGBA, source_image_width, source_image_height, &bytes_source, (uint8_t *) pixels); -#if IS_FFMPEG_3_2 - AVFrame *frame_final; + // Decide destination pixel format: NV12 if HW accel is on, else encoder’s pix_fmt + AVPixelFormat dst_fmt = video_codec_ctx->pix_fmt; #if USE_HW_ACCEL - if (hw_en_on && hw_en_supported) { - frame_final = allocate_avframe(AV_PIX_FMT_NV12, info.width, info.height, &bytes_final, NULL); - } else -#endif // USE_HW_ACCEL - { - frame_final = allocate_avframe( - (AVPixelFormat)(video_st->codecpar->format), - info.width, info.height, &bytes_final, NULL + if (hw_en_on && hw_en_supported) { + dst_fmt = AV_PIX_FMT_NV12; + } +#endif + persistent_dst_frame->format = dst_fmt; + persistent_dst_frame->width = info.width; + persistent_dst_frame->height = info.height; + + persistent_dst_size = av_image_get_buffer_size( + dst_fmt, info.width, info.height, 1 + ); + if (persistent_dst_size < 0) + throw ErrorEncodingVideo("Invalid destination image size", -1); + + persistent_dst_buffer = static_cast( + av_malloc(persistent_dst_size) + ); + if (!persistent_dst_buffer) + throw OutOfMemory("Could not allocate persistent_dst_buffer", path); + + av_image_fill_arrays( + persistent_dst_frame->data, + persistent_dst_frame->linesize, + persistent_dst_buffer, + dst_fmt, + info.width, + info.height, + 1 ); } -#else - AVFrame *frame_final = allocate_avframe(video_codec_ctx->pix_fmt, info.width, info.height, &bytes_final, NULL); -#endif // IS_FFMPEG_3_2 - // Fill with data - AV_COPY_PICTURE_DATA(frame_source, (uint8_t *) pixels, PIX_FMT_RGBA, source_image_width, source_image_height); - ZmqLogger::Instance()->AppendDebugMethod( - "FFmpegWriter::process_video_packet", - "frame->number", frame->number, - "bytes_source", bytes_source, - "bytes_final", bytes_final); + // Initialize SwsContext (RGBA → dst_fmt) on first use + if (!img_convert_ctx) { + int flags = SWS_FAST_BILINEAR; + if (openshot::Settings::Instance()->HIGH_QUALITY_SCALING) { + flags = SWS_BICUBIC; + } + AVPixelFormat dst_fmt = video_codec_ctx->pix_fmt; +#if USE_HW_ACCEL + if (hw_en_on && hw_en_supported) { + dst_fmt = AV_PIX_FMT_NV12; + } +#endif + img_convert_ctx = sws_getContext( + src_w, src_h, AV_PIX_FMT_RGBA, + info.width, info.height, dst_fmt, + flags, NULL, NULL, NULL + ); + if (!img_convert_ctx) + throw ErrorEncodingVideo("Could not initialize sws context", -1); + } - // Resize & convert pixel format - sws_scale(scaler, frame_source->data, frame_source->linesize, 0, - source_image_height, frame_final->data, frame_final->linesize); + // Scale RGBA → dst_fmt into persistent_dst_buffer + sws_scale( + img_convert_ctx, + persistent_src_frame->data, + persistent_src_frame->linesize, + 0, src_h, + persistent_dst_frame->data, + persistent_dst_frame->linesize + ); - // Add resized AVFrame to av_frames map - add_avframe(frame, frame_final); + // Allocate a new AVFrame + buffer, then copy scaled data into it + int bytes_final = 0; + AVPixelFormat dst_fmt = video_codec_ctx->pix_fmt; +#if USE_HW_ACCEL + if (hw_en_on && hw_en_supported) { + dst_fmt = AV_PIX_FMT_NV12; + } +#endif - // Deallocate memory - AV_FREE_FRAME(&frame_source); + AVFrame* new_frame = allocate_avframe( + dst_fmt, + info.width, + info.height, + &bytes_final, + nullptr + ); + if (!new_frame) + throw OutOfMemory("Could not allocate new_frame via allocate_avframe", path); + + // Copy persistent_dst_buffer → new_frame buffer + memcpy( + new_frame->data[0], + persistent_dst_buffer, + static_cast(bytes_final) + ); + + // Queue the deep‐copied frame for encoding + add_avframe(frame, new_frame); } // write video frame @@ -2307,55 +2357,14 @@ void FFmpegWriter::OutputStreamInfo() { av_dump_format(oc, 0, path.c_str(), 1); } -// Init a collection of software rescalers (thread safe) -void FFmpegWriter::InitScalers(int source_width, int source_height) { - int scale_mode = SWS_FAST_BILINEAR; - if (openshot::Settings::Instance()->HIGH_QUALITY_SCALING) { - scale_mode = SWS_BICUBIC; - } - - // Init software rescalers vector (many of them, one for each thread) - for (int x = 0; x < num_of_rescalers; x++) { - // Init the software scaler from FFMpeg -#if USE_HW_ACCEL - if (hw_en_on && hw_en_supported) { - img_convert_ctx = sws_getContext(source_width, source_height, PIX_FMT_RGBA, - info.width, info.height, AV_PIX_FMT_NV12, scale_mode, NULL, NULL, NULL); - } else -#endif // USE_HW_ACCEL - { - img_convert_ctx = sws_getContext(source_width, source_height, PIX_FMT_RGBA, - info.width, info.height, AV_GET_CODEC_PIXEL_FORMAT(video_st, video_st->codec), - scale_mode, NULL, NULL, NULL); - } - - // Add rescaler to vector - image_rescalers.push_back(img_convert_ctx); - } -} - // Set audio resample options void FFmpegWriter::ResampleAudio(int sample_rate, int channels) { original_sample_rate = sample_rate; original_channels = channels; } -// Remove & deallocate all software scalers -void FFmpegWriter::RemoveScalers() { - // Close all rescalers - for (int x = 0; x < num_of_rescalers; x++) - sws_freeContext(image_rescalers[x]); - - // Clear vector - image_rescalers.clear(); -} - // In FFmpegWriter.cpp -void FFmpegWriter::AddSphericalMetadata(const std::string& projection, - float yaw_deg, - float pitch_deg, - float roll_deg) -{ +void FFmpegWriter::AddSphericalMetadata(const std::string& projection, float yaw_deg, float pitch_deg, float roll_deg) { if (!oc) return; if (!info.has_video || !video_st) return; @@ -2371,10 +2380,7 @@ void FFmpegWriter::AddSphericalMetadata(const std::string& projection, // Allocate the side‐data structure size_t sd_size = 0; AVSphericalMapping* map = av_spherical_alloc(&sd_size); - if (!map) { - // Allocation failed; skip metadata - return; - } + if (!map) return; // Populate it map->projection = static_cast(proj); @@ -2383,14 +2389,6 @@ void FFmpegWriter::AddSphericalMetadata(const std::string& projection, map->pitch = static_cast(pitch_deg * (1 << 16)); map->roll = static_cast(roll_deg * (1 << 16)); - // Attach to the video stream so movenc will emit an sv3d atom - av_stream_add_side_data( - video_st, - AV_PKT_DATA_SPHERICAL, - reinterpret_cast(map), - sd_size - ); -#else - // FFmpeg build too old: spherical side-data not supported + av_stream_add_side_data(video_st, AV_PKT_DATA_SPHERICAL, reinterpret_cast(map), sd_size); #endif } diff --git a/src/FFmpegWriter.h b/src/FFmpegWriter.h index 1f54eed9..4fab6a77 100644 --- a/src/FFmpegWriter.h +++ b/src/FFmpegWriter.h @@ -134,9 +134,10 @@ namespace openshot { uint8_t *audio_outbuf; uint8_t *audio_encoder_buffer; - int num_of_rescalers; - int rescaler_position; - std::vector image_rescalers; + AVFrame *persistent_src_frame = nullptr; + AVFrame *persistent_dst_frame = nullptr; + uint8_t *persistent_dst_buffer = nullptr; + int persistent_dst_size = 0; int audio_outbuf_size; int audio_input_frame_size; @@ -180,11 +181,6 @@ namespace openshot { /// initialize streams void initialize_streams(); - /// @brief Init a collection of software rescalers (thread safe) - /// @param source_width The source width of the image scalers (used to cache a bunch of scalers) - /// @param source_height The source height of the image scalers (used to cache a bunch of scalers) - void InitScalers(int source_width, int source_height); - /// open audio codec void open_audio(AVFormatContext *oc, AVStream *st); @@ -230,9 +226,6 @@ namespace openshot { /// by the Open() method if this method has not yet been called. void PrepareStreams(); - /// Remove & deallocate all software scalers - void RemoveScalers(); - /// @brief Set audio resample options /// @param sample_rate The number of samples per second of the audio /// @param channels The number of audio channels @@ -327,6 +320,6 @@ namespace openshot { }; -} +} // namespace openshot #endif From a4b2af4eb80c1d7c1a81dddf92ab97725c0720ad Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Thu, 5 Jun 2025 19:38:53 -0500 Subject: [PATCH 13/20] Adding FFmpegWriter into openshot-example executable to test with valgrind heapgrind. --- examples/Example.cpp | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/examples/Example.cpp b/examples/Example.cpp index 150ee63a..6c74c0c2 100644 --- a/examples/Example.cpp +++ b/examples/Example.cpp @@ -15,6 +15,7 @@ #include #include "Frame.h" #include "FFmpegReader.h" +#include "FFmpegWriter.h" using namespace openshot; @@ -27,6 +28,22 @@ int main(int argc, char* argv[]) { const long int total_frames = r.info.video_length; + + // Reader + std::stringstream writer_path; + writer_path << TEST_MEDIA_PATH << "sintel_trailer-720p.mp4"; + + /* WRITER ---------------- */ + FFmpegWriter w("/home/jonathan/Downloads/performance-test.mp4"); + + // Set options + w.SetAudioOptions("aac", 48000, 192000); + w.SetVideoOptions("libx264", 1280, 720, Fraction(30,1), 5000000); + + // Open writer + w.Open(); + + // --- Measure forward pass --- auto t0 = std::chrono::high_resolution_clock::now(); for (long int frame = 1; frame <= total_frames; frame++) { @@ -34,7 +51,7 @@ int main(int argc, char* argv[]) { std::cout << "Forward: Requesting Frame #: " << frame << " (" << percent << "%)\n"; std::shared_ptr f = r.GetFrame(frame); - // (optional) preview or process f here + w.WriteFrame(f); } auto t1 = std::chrono::high_resolution_clock::now(); auto forward_ms = std::chrono::duration_cast(t1 - t0).count(); @@ -46,7 +63,7 @@ int main(int argc, char* argv[]) { std::cout << "Backward: Requesting Frame #: " << frame << " (" << percent << "%)\n"; std::shared_ptr f = r.GetFrame(frame); - // (optional) preview or process f here + w.WriteFrame(f); } auto t3 = std::chrono::high_resolution_clock::now(); auto backward_ms = std::chrono::duration_cast(t3 - t2).count(); @@ -55,5 +72,6 @@ int main(int argc, char* argv[]) { std::cout << "Backward pass elapsed: " << backward_ms << " ms\n"; r.Close(); + w.Close(); return 0; } From a6ca7d9a2f3eec3562e6f738607ecce4b8e5054d Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Fri, 6 Jun 2025 15:25:46 -0500 Subject: [PATCH 14/20] Adding back Setting::VIDEO_CACHE_MAX_FRAMES, to limit the video cache thread to a hard #. Also, minor refactor to reduce duplication of capacity. --- src/Qt/VideoCacheThread.cpp | 107 +++++++++++++++++++----------------- src/Qt/VideoCacheThread.h | 7 ++- 2 files changed, 63 insertions(+), 51 deletions(-) diff --git a/src/Qt/VideoCacheThread.cpp b/src/Qt/VideoCacheThread.cpp index 4697ffe8..9c0aa9aa 100644 --- a/src/Qt/VideoCacheThread.cpp +++ b/src/Qt/VideoCacheThread.cpp @@ -34,7 +34,6 @@ namespace openshot , current_display_frame(1) , cached_frame_count(0) , min_frames_ahead(4) - , max_frames_ahead(8) , timeline_max_frame(0) , reader(nullptr) , force_directional_cache(false) @@ -89,6 +88,22 @@ namespace openshot return bytes; } + /// Start the cache thread at high priority, and return true if it’s actually running. + bool VideoCacheThread::StartThread() + { + // JUCE’s startThread() returns void, so we launch it and then check if + // the thread actually started: + startThread(Priority::high); + return isThreadRunning(); + } + + /// Stop the cache thread, waiting up to timeoutMs ms. Returns true if it actually stopped. + bool VideoCacheThread::StopThread(int timeoutMs) + { + stopThread(timeoutMs); + return !isThreadRunning(); + } + void VideoCacheThread::Seek(int64_t new_position, bool start_preroll) { if (start_preroll) { @@ -203,11 +218,14 @@ namespace openshot CacheBase* cache = reader ? reader->GetCache() : nullptr; // If caching disabled or no reader, sleep briefly - if (!settings->ENABLE_PLAYBACK_CACHING || !cache) { + if (!settings->ENABLE_PLAYBACK_CACHING || !cache || !is_playing) { std::this_thread::sleep_for(double_micro_sec(50000)); continue; } + // init local vars + min_frames_ahead = settings->VIDEO_CACHE_MIN_PREROLL_FRAMES; + Timeline* timeline = static_cast(reader); int64_t timeline_end = timeline->GetMaxFrame(); int64_t playhead = requested_display_frame; @@ -219,45 +237,7 @@ namespace openshot last_dir = dir; } - // If a seek was requested, reset last_cached_index - if (userSeeked) { - handleUserSeek(playhead, dir); - userSeeked = false; - } - else if (!paused) { - // Check if last_cached_index drifted outside the new window; if so, reset it - int64_t bytes_per_frame = getBytes( - (timeline->preview_width ? timeline->preview_width : reader->info.width), - (timeline->preview_height ? timeline->preview_height : reader->info.height), - reader->info.sample_rate, - reader->info.channels, - reader->info.fps.ToFloat() - ); - int64_t max_bytes = cache->GetMaxBytes(); - if (max_bytes > 0 && bytes_per_frame > 0) { - int64_t capacity = max_bytes / bytes_per_frame; - if (capacity >= 1) { - int64_t ahead_count = static_cast(capacity * - settings->VIDEO_CACHE_PERCENT_AHEAD); - int64_t window_begin, window_end; - computeWindowBounds(playhead, - dir, - ahead_count, - timeline_end, - window_begin, - window_end); - - bool outside_window = - (dir > 0 && last_cached_index > window_end) || - (dir < 0 && last_cached_index < window_begin); - if (outside_window) { - handleUserSeek(playhead, dir); - } - } - } - } - - // Recompute capacity & ahead_count now that we’ve possibly updated last_cached_index + // Compute bytes_per_frame, max_bytes, and capacity once int64_t bytes_per_frame = getBytes( (timeline->preview_width ? timeline->preview_width : reader->info.width), (timeline->preview_height ? timeline->preview_height : reader->info.height), @@ -266,11 +246,42 @@ namespace openshot reader->info.fps.ToFloat() ); int64_t max_bytes = cache->GetMaxBytes(); - if (max_bytes <= 0 || bytes_per_frame <= 0) { - std::this_thread::sleep_for(double_micro_sec(50000)); - continue; + int64_t capacity = 0; + if (max_bytes > 0 && bytes_per_frame > 0) { + capacity = max_bytes / bytes_per_frame; + if (capacity > settings->VIDEO_CACHE_MAX_FRAMES) { + capacity = settings->VIDEO_CACHE_MAX_FRAMES; + } } - int64_t capacity = max_bytes / bytes_per_frame; + + // Handle a user-initiated seek + if (userSeeked) { + handleUserSeek(playhead, dir); + userSeeked = false; + } + else if (!paused && capacity >= 1) { + // In playback mode, check if last_cached_index drifted outside the new window + int64_t base_ahead = static_cast(capacity * settings->VIDEO_CACHE_PERCENT_AHEAD); + + int64_t window_begin, window_end; + computeWindowBounds( + playhead, + dir, + base_ahead, + timeline_end, + window_begin, + window_end + ); + + bool outside_window = + (dir > 0 && last_cached_index > window_end) || + (dir < 0 && last_cached_index < window_begin); + if (outside_window) { + handleUserSeek(playhead, dir); + } + } + + // If capacity is insufficient, sleep and retry if (capacity < 1) { std::this_thread::sleep_for(double_micro_sec(50000)); continue; @@ -294,11 +305,7 @@ namespace openshot window_end); // Attempt to fill any missing frames in that window - bool window_full = prefetchWindow(cache, - window_begin, - window_end, - dir, - reader); + bool window_full = prefetchWindow(cache, window_begin, window_end, dir, reader); // If paused and window was already full, keep playhead fresh if (paused && window_full) { diff --git a/src/Qt/VideoCacheThread.h b/src/Qt/VideoCacheThread.h index fc2c944f..bfc0c490 100644 --- a/src/Qt/VideoCacheThread.h +++ b/src/Qt/VideoCacheThread.h @@ -68,6 +68,12 @@ namespace openshot */ void Seek(int64_t new_position, bool start_preroll); + /// Start the cache thread at high priority. Returns true if it’s actually running. + bool StartThread(); + + /// Stop the cache thread (wait up to timeoutMs ms). Returns true if it stopped. + bool StopThread(int timeoutMs = 0); + /** * @brief Attach a ReaderBase (e.g. Timeline, FFmpegReader) and begin caching. * @param new_reader @@ -165,7 +171,6 @@ namespace openshot int64_t cached_frame_count; ///< Count of frames currently added to cache. int64_t min_frames_ahead; ///< Minimum number of frames considered “ready” (pre-roll). - int64_t max_frames_ahead; ///< Maximum frames to attempt to cache (mem capped). int64_t timeline_max_frame; ///< Highest valid frame index in the timeline. ReaderBase* reader; ///< The source reader (e.g., Timeline, FFmpegReader). From 57a0bae9ac6f6ab6065b2302c963d8aeeeac5155 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Fri, 6 Jun 2025 15:26:40 -0500 Subject: [PATCH 15/20] Refactor of Settings to no longer duplicate them in the *.h and Instance() method. Also changing FF_THREADS and OMP_THREADS to use # of processors on device. --- src/Settings.cpp | 22 ++++------------------ src/Settings.h | 4 ++-- tests/Settings.cpp | 14 ++++++++++---- 3 files changed, 16 insertions(+), 24 deletions(-) diff --git a/src/Settings.cpp b/src/Settings.cpp index 7afbd1d0..b2c16045 100644 --- a/src/Settings.cpp +++ b/src/Settings.cpp @@ -10,8 +10,8 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later -#include // For std::getenv - +#include +#include #include "Settings.h" using namespace openshot; @@ -25,22 +25,8 @@ Settings *Settings::Instance() if (!m_pInstance) { // Create the actual instance of Settings only once m_pInstance = new Settings; - m_pInstance->HARDWARE_DECODER = 0; - m_pInstance->HIGH_QUALITY_SCALING = false; - m_pInstance->OMP_THREADS = 12; - m_pInstance->FF_THREADS = 8; - m_pInstance->DE_LIMIT_HEIGHT_MAX = 1100; - m_pInstance->DE_LIMIT_WIDTH_MAX = 1950; - m_pInstance->HW_DE_DEVICE_SET = 0; - m_pInstance->HW_EN_DEVICE_SET = 0; - m_pInstance->VIDEO_CACHE_PERCENT_AHEAD = 0.7; - m_pInstance->VIDEO_CACHE_MIN_PREROLL_FRAMES = 24; - m_pInstance->VIDEO_CACHE_MAX_PREROLL_FRAMES = 48; - m_pInstance->VIDEO_CACHE_MAX_FRAMES = 30 * 10; - m_pInstance->ENABLE_PLAYBACK_CACHING = true; - m_pInstance->PLAYBACK_AUDIO_DEVICE_NAME = ""; - m_pInstance->PLAYBACK_AUDIO_DEVICE_TYPE = ""; - m_pInstance->DEBUG_TO_STDERR = false; + m_pInstance->OMP_THREADS = omp_get_num_procs(); + m_pInstance->FF_THREADS = omp_get_num_procs(); auto env_debug = std::getenv("LIBOPENSHOT_DEBUG"); if (env_debug != nullptr) m_pInstance->DEBUG_TO_STDERR = true; diff --git a/src/Settings.h b/src/Settings.h index e35b2be9..0474e3d1 100644 --- a/src/Settings.h +++ b/src/Settings.h @@ -65,10 +65,10 @@ namespace openshot { bool HIGH_QUALITY_SCALING = false; /// Number of threads of OpenMP - int OMP_THREADS = 12; + int OMP_THREADS = 16; /// Number of threads that ffmpeg uses - int FF_THREADS = 8; + int FF_THREADS = 16; /// Maximum rows that hardware decode can handle int DE_LIMIT_HEIGHT_MAX = 1100; diff --git a/tests/Settings.cpp b/tests/Settings.cpp index f4717b37..0e300e34 100644 --- a/tests/Settings.cpp +++ b/tests/Settings.cpp @@ -13,15 +13,21 @@ #include "openshot_catch.h" #include "Settings.h" +#include + using namespace openshot; TEST_CASE( "Constructor", "[libopenshot][settings]" ) { + // Get system cpu count + int cpu_count = omp_get_num_procs(); + // Create an empty color Settings *s = Settings::Instance(); - CHECK(s->OMP_THREADS == 12); + CHECK(s->OMP_THREADS == cpu_count); + CHECK(s->FF_THREADS == cpu_count); CHECK_FALSE(s->HIGH_QUALITY_SCALING); } @@ -29,13 +35,13 @@ TEST_CASE( "Change settings", "[libopenshot][settings]" ) { // Create an empty color Settings *s = Settings::Instance(); - s->OMP_THREADS = 8; + s->OMP_THREADS = 13; s->HIGH_QUALITY_SCALING = true; - CHECK(s->OMP_THREADS == 8); + CHECK(s->OMP_THREADS == 13); CHECK(s->HIGH_QUALITY_SCALING == true); - CHECK(Settings::Instance()->OMP_THREADS == 8); + CHECK(Settings::Instance()->OMP_THREADS == 13); CHECK(Settings::Instance()->HIGH_QUALITY_SCALING == true); } From 7ee4643a601fc7c1eca1e49682630558f905f80e Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Fri, 6 Jun 2025 15:28:28 -0500 Subject: [PATCH 16/20] Splitting FF_NUM_PROCESSORS into a VIDEO and AUDIO constant. Also limiting VIDEO encoders to 16 threads and audio encoders to 2 threads. --- src/FFmpegReader.cpp | 6 +++--- src/FFmpegWriter.cpp | 9 +++++---- src/OpenMPUtilities.h | 5 +++-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/FFmpegReader.cpp b/src/FFmpegReader.cpp index abce5733..d66749bb 100644 --- a/src/FFmpegReader.cpp +++ b/src/FFmpegReader.cpp @@ -278,7 +278,7 @@ void FFmpegReader::Open() { retry_decode_open = 0; // Set number of threads equal to number of processors (not to exceed 16) - pCodecCtx->thread_count = std::min(FF_NUM_PROCESSORS, 16); + pCodecCtx->thread_count = std::min(FF_VIDEO_NUM_PROCESSORS, 16); if (pCodec == NULL) { throw InvalidCodec("A valid video codec could not be found for this file.", path); @@ -524,8 +524,8 @@ void FFmpegReader::Open() { const AVCodec *aCodec = avcodec_find_decoder(codecId); aCodecCtx = AV_GET_CODEC_CONTEXT(aStream, aCodec); - // Set number of threads equal to number of processors (not to exceed 16) - aCodecCtx->thread_count = std::min(FF_NUM_PROCESSORS, 16); + // Audio encoding does not typically use more than 2 threads (most codecs use 1 thread) + aCodecCtx->thread_count = std::min(FF_AUDIO_NUM_PROCESSORS, 2); if (aCodec == NULL) { throw InvalidCodec("A valid audio codec could not be found for this file.", path); diff --git a/src/FFmpegWriter.cpp b/src/FFmpegWriter.cpp index ddfa67bc..2de65257 100644 --- a/src/FFmpegWriter.cpp +++ b/src/FFmpegWriter.cpp @@ -1334,8 +1334,9 @@ void FFmpegWriter::open_audio(AVFormatContext *oc, AVStream *st) { const AVCodec *codec; AV_GET_CODEC_FROM_STREAM(st, audio_codec_ctx) - // Set number of threads equal to number of processors (not to exceed 16) - audio_codec_ctx->thread_count = std::min(FF_NUM_PROCESSORS, 16); + // Audio encoding does not typically use more than 2 threads (most codecs use 1 thread) + audio_codec_ctx->thread_count = std::min(FF_AUDIO_NUM_PROCESSORS, 2); + std::cout << "FFmpegWriter audio thread_count: " << audio_codec_ctx->thread_count << std::endl; // Find the audio encoder codec = avcodec_find_encoder_by_name(info.acodec.c_str()); @@ -1409,8 +1410,8 @@ void FFmpegWriter::open_video(AVFormatContext *oc, AVStream *st) { const AVCodec *codec; AV_GET_CODEC_FROM_STREAM(st, video_codec_ctx) - // Set number of threads equal to number of processors (not to exceed 16) - video_codec_ctx->thread_count = std::min(FF_NUM_PROCESSORS, 16); + // Set number of threads equal to number of processors (not to exceed 16, FFmpeg doesn't recommend more than 16) + video_codec_ctx->thread_count = std::min(FF_VIDEO_NUM_PROCESSORS, 16); #if USE_HW_ACCEL if (hw_en_on && hw_en_supported) { diff --git a/src/OpenMPUtilities.h b/src/OpenMPUtilities.h index 43932542..a50780b4 100644 --- a/src/OpenMPUtilities.h +++ b/src/OpenMPUtilities.h @@ -20,8 +20,9 @@ #include "Settings.h" // Calculate the # of OpenMP Threads to allow -#define OPEN_MP_NUM_PROCESSORS (std::min(omp_get_num_procs(), std::max(2, openshot::Settings::Instance()->OMP_THREADS) )) -#define FF_NUM_PROCESSORS (std::min(omp_get_num_procs(), std::max(2, openshot::Settings::Instance()->FF_THREADS) )) +#define OPEN_MP_NUM_PROCESSORS std::min(omp_get_num_procs(), std::max(2, openshot::Settings::Instance()->OMP_THREADS)) +#define FF_VIDEO_NUM_PROCESSORS std::min(omp_get_num_procs(), std::max(2, openshot::Settings::Instance()->FF_THREADS)) +#define FF_AUDIO_NUM_PROCESSORS std::min(omp_get_num_procs(), std::max(2, openshot::Settings::Instance()->FF_THREADS)) // Set max-active-levels to the max supported, if possible // (supported_active_levels is OpenMP 5.0 (November 2018) or later, only.) From 2d6db64b5215983b0c620c74f8d43740cb7d2334 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Fri, 6 Jun 2025 15:33:04 -0500 Subject: [PATCH 17/20] Incorporating VideoCacheThread into openshot-example executable, to experiment with caching during export. It works great! --- examples/Example.cpp | 94 ++++++++++++++++++++++++++------------------ 1 file changed, 56 insertions(+), 38 deletions(-) diff --git a/examples/Example.cpp b/examples/Example.cpp index 6c74c0c2..2067fb56 100644 --- a/examples/Example.cpp +++ b/examples/Example.cpp @@ -1,12 +1,12 @@ /** * @file - * @brief Source file for Example Executable (example app for libopenshot) + * @brief Example application showing how to attach VideoCacheThread to an FFmpegReader * @author Jonathan Thomas * * @ref License */ -// Copyright (c) 2008-2019 OpenShot Studios, LLC +// Copyright (c) 2008-2025 OpenShot Studios, LLC // // SPDX-License-Identifier: LGPL-3.0-or-later @@ -16,62 +16,80 @@ #include "Frame.h" #include "FFmpegReader.h" #include "FFmpegWriter.h" +#include "Timeline.h" +#include "Qt/VideoCacheThread.h" // <— your new header using namespace openshot; int main(int argc, char* argv[]) { - // Path to your video - const char* path = "/home/jonathan/Downloads/openshot-testing/sintel_trailer-720p.mp4"; - - FFmpegReader r(path); - r.Open(); - - const long int total_frames = r.info.video_length; - // Reader - std::stringstream writer_path; - writer_path << TEST_MEDIA_PATH << "sintel_trailer-720p.mp4"; + // 1) Open the FFmpegReader as usual + const char* input_path = "/home/jonathan/Downloads/openshot-testing/sintel_trailer-720p.mp4"; + FFmpegReader reader(input_path); + reader.Open(); - /* WRITER ---------------- */ - FFmpegWriter w("/home/jonathan/Downloads/performance-test.mp4"); - - // Set options - w.SetAudioOptions("aac", 48000, 192000); - w.SetVideoOptions("libx264", 1280, 720, Fraction(30,1), 5000000); - - // Open writer - w.Open(); + const int64_t total_frames = reader.info.video_length; + std::cout << "Total frames: " << total_frames << "\n"; - // --- Measure forward pass --- + + Timeline timeline(reader.info.width, reader.info.height, reader.info.fps, reader.info.sample_rate, reader.info.channels, reader.info.channel_layout); + Clip c1(&reader); + timeline.AddClip(&c1); + timeline.Open(); + timeline.DisplayInfo(); + + + // 2) Construct a VideoCacheThread around 'reader' and start its background loop + // (VideoCacheThread inherits juce::Thread) + std::shared_ptr cache = std::make_shared(); + cache->Reader(&timeline); // attaches the FFmpegReader and internally calls Play() + cache->StartThread(); // juce::Thread method, begins run() + + // 3) Set up the writer exactly as before + FFmpegWriter writer("/home/jonathan/Downloads/performance‐cachetest.mp4"); + writer.SetAudioOptions("aac", 48000, 192000); + writer.SetVideoOptions("libx264", 1280, 720, Fraction(30, 1), 5000000); + writer.Open(); + + // 4) Forward pass: for each frame 1…N, tell the cache thread to seek to that frame, + // then immediately call cache->GetFrame(frame), which will block only if that frame + // hasn’t been decoded into the cache yet. auto t0 = std::chrono::high_resolution_clock::now(); - for (long int frame = 1; frame <= total_frames; frame++) { - float percent = (float(frame) / total_frames) * 100.0f; - std::cout << "Forward: Requesting Frame #: " << frame - << " (" << percent << "%)\n"; - std::shared_ptr f = r.GetFrame(frame); - w.WriteFrame(f); + cache->setSpeed(1); + for (int64_t f = 1; f <= total_frames; ++f) { + float pct = (float(f) / total_frames) * 100.0f; + std::cout << "Forward: requesting frame " << f << " (" << pct << "%)\n"; + + cache->Seek(f); // signal “I need frame f now (and please prefetch f+1, f+2, …)” + std::shared_ptr framePtr = timeline.GetFrame(f); + writer.WriteFrame(framePtr); } auto t1 = std::chrono::high_resolution_clock::now(); auto forward_ms = std::chrono::duration_cast(t1 - t0).count(); - // --- Measure backward pass --- + // 5) Backward pass: same idea in reverse auto t2 = std::chrono::high_resolution_clock::now(); - for (long int frame = total_frames; frame >= 1; frame--) { - float percent = (float(total_frames - frame + 1) / total_frames) * 100.0f; - std::cout << "Backward: Requesting Frame #: " << frame - << " (" << percent << "%)\n"; - std::shared_ptr f = r.GetFrame(frame); - w.WriteFrame(f); + cache->setSpeed(-1); + for (int64_t f = total_frames; f >= 1; --f) { + float pct = (float(total_frames - f + 1) / total_frames) * 100.0f; + std::cout << "Backward: requesting frame " << f << " (" << pct << "%)\n"; + + cache->Seek(f); + std::shared_ptr framePtr = timeline.GetFrame(f); + writer.WriteFrame(framePtr); } auto t3 = std::chrono::high_resolution_clock::now(); auto backward_ms = std::chrono::duration_cast(t3 - t2).count(); - std::cout << "\nForward pass elapsed: " << forward_ms << " ms\n"; + std::cout << "\nForward pass elapsed: " << forward_ms << " ms\n"; std::cout << "Backward pass elapsed: " << backward_ms << " ms\n"; - r.Close(); - w.Close(); + // 6) Shut down the cache thread, close everything + cache->StopThread(10000); // politely tells run() to exit, waits up to 10s + reader.Close(); + writer.Close(); + timeline.Close(); return 0; } From 68d3850efa1eca11e221407d77f361b0acb27d27 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Fri, 6 Jun 2025 15:33:43 -0500 Subject: [PATCH 18/20] Exposing VideoCacheThread to SWIG bindings for Python, Ruby, and Java. This will be called from openshot-qt in Python, to speed up exports. --- bindings/java/openshot.i | 5 +++++ bindings/python/openshot.i | 5 +++++ bindings/ruby/openshot.i | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/bindings/java/openshot.i b/bindings/java/openshot.i index ee339e1a..7aaf7d1e 100644 --- a/bindings/java/openshot.i +++ b/bindings/java/openshot.i @@ -91,9 +91,13 @@ #include "Settings.h" #include "TimelineBase.h" #include "Timeline.h" +#include "Qt/VideoCacheThread.h" #include "ZmqLogger.h" %} +// Prevent SWIG from ever generating a wrapper for juce::Thread’s constructor (or run()) +%ignore juce::Thread::Thread; + #ifdef USE_IMAGEMAGICK %{ #include "ImageReader.h" @@ -151,6 +155,7 @@ %include "RendererBase.h" %include "Settings.h" %include "TimelineBase.h" +%include "Qt/VideoCacheThread.h" %include "Timeline.h" %include "ZmqLogger.h" diff --git a/bindings/python/openshot.i b/bindings/python/openshot.i index 29b9c8ee..612bfb11 100644 --- a/bindings/python/openshot.i +++ b/bindings/python/openshot.i @@ -96,10 +96,14 @@ #include "Settings.h" #include "TimelineBase.h" #include "Timeline.h" +#include "Qt/VideoCacheThread.h" #include "ZmqLogger.h" %} +// Prevent SWIG from ever generating a wrapper for juce::Thread’s constructor (or run()) +%ignore juce::Thread::Thread; + #ifdef USE_IMAGEMAGICK %{ #include "ImageReader.h" @@ -317,6 +321,7 @@ %include "RendererBase.h" %include "Settings.h" %include "TimelineBase.h" +%include "Qt/VideoCacheThread.h" %include "Timeline.h" %include "ZmqLogger.h" diff --git a/bindings/ruby/openshot.i b/bindings/ruby/openshot.i index be6fa19b..655241be 100644 --- a/bindings/ruby/openshot.i +++ b/bindings/ruby/openshot.i @@ -99,6 +99,7 @@ #include "Settings.h" #include "TimelineBase.h" #include "Timeline.h" +#include "Qt/VideoCacheThread.h" #include "ZmqLogger.h" /* Move FFmpeg's RSHIFT to FF_RSHIFT, if present */ @@ -112,6 +113,9 @@ #endif %} +// Prevent SWIG from ever generating a wrapper for juce::Thread’s constructor (or run()) +%ignore juce::Thread::Thread; + #ifdef USE_IMAGEMAGICK %{ #include "ImageReader.h" @@ -190,6 +194,7 @@ %include "RendererBase.h" %include "Settings.h" %include "TimelineBase.h" +%include "Qt/VideoCacheThread.h" %include "Timeline.h" %include "ZmqLogger.h" From 0fcdcdb3265b7e60ab8aa9443c50d0e367a3a2d1 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Fri, 6 Jun 2025 16:11:18 -0500 Subject: [PATCH 19/20] Removing debug code --- src/FFmpegWriter.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/FFmpegWriter.cpp b/src/FFmpegWriter.cpp index 2de65257..79cbc21c 100644 --- a/src/FFmpegWriter.cpp +++ b/src/FFmpegWriter.cpp @@ -1336,7 +1336,6 @@ void FFmpegWriter::open_audio(AVFormatContext *oc, AVStream *st) { // Audio encoding does not typically use more than 2 threads (most codecs use 1 thread) audio_codec_ctx->thread_count = std::min(FF_AUDIO_NUM_PROCESSORS, 2); - std::cout << "FFmpegWriter audio thread_count: " << audio_codec_ctx->thread_count << std::endl; // Find the audio encoder codec = avcodec_find_encoder_by_name(info.acodec.c_str()); From 099fe59dcd80a26a75de6f4ad89dfe4437a8f3a3 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Fri, 6 Jun 2025 17:46:22 -0500 Subject: [PATCH 20/20] Including missing sstream include on Profile tests --- tests/Profiles.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Profiles.cpp b/tests/Profiles.cpp index d515655c..8e319a3b 100644 --- a/tests/Profiles.cpp +++ b/tests/Profiles.cpp @@ -11,6 +11,7 @@ // SPDX-License-Identifier: LGPL-3.0-or-later #include "openshot_catch.h" +#include #include "Profiles.h"