Merge pull request #1014 from OpenShot/ffmpeg-performance

FFmpeg Performance - Let's GO!
This commit is contained in:
Jonathan Thomas
2025-06-06 23:38:41 -05:00
committed by GitHub
18 changed files with 388 additions and 296 deletions

View File

@@ -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"

View File

@@ -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::Threads 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"

View File

@@ -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::Threads 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"

View File

@@ -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::Threads 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"

View File

@@ -1,68 +1,95 @@
/**
* @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 <jonathan@openshot.org>
*
* @ref License
*/
// Copyright (c) 2008-2019 OpenShot Studios, LLC
// Copyright (c) 2008-2025 OpenShot Studios, LLC
//
// SPDX-License-Identifier: LGPL-3.0-or-later
#include <fstream>
#include <chrono>
#include <iostream>
#include <memory>
#include <QFileDialog>
#include "Clip.h"
#include "Frame.h"
#include "FFmpegReader.h"
#include "FFmpegWriter.h"
#include "Timeline.h"
#include "Profiles.h"
#include "Qt/VideoCacheThread.h" // <— your new header
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;
}
// 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();
const int64_t total_frames = reader.info.video_length;
std::cout << "Total frames: " << total_frames << "\n";
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<VideoCacheThread> cache = std::make_shared<VideoCacheThread>();
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/performancecachetest.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
// hasnt been decoded into the cache yet.
auto t0 = std::chrono::high_resolution_clock::now();
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<Frame> framePtr = timeline.GetFrame(f);
writer.WriteFrame(framePtr);
}
auto t1 = std::chrono::high_resolution_clock::now();
auto forward_ms = std::chrono::duration_cast<std::chrono::milliseconds>(t1 - t0).count();
// 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();
r.Open();
// 5) Backward pass: same idea in reverse
auto t2 = std::chrono::high_resolution_clock::now();
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";
// 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;
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;
std::shared_ptr<Frame> f = r.GetFrame(frame);
// Preview frame image
if (frame % 1 == 0) {
f->Save("preview.jpg", 1.0, "jpg", 100);
}
cache->Seek(f);
std::shared_ptr<Frame> framePtr = timeline.GetFrame(f);
writer.WriteFrame(framePtr);
}
r.Close();
auto t3 = std::chrono::high_resolution_clock::now();
auto backward_ms = std::chrono::duration_cast<std::chrono::milliseconds>(t3 - t2).count();
exit(0);
std::cout << "\nForward pass elapsed: " << forward_ms << " ms\n";
std::cout << "Backward pass elapsed: " << backward_ms << " ms\n";
// 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;
}

View File

@@ -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
@@ -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);
@@ -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
@@ -1554,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_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);
@@ -1564,8 +1582,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 +1609,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 +1756,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 +1774,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 +1786,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++) {

View File

@@ -31,6 +31,7 @@
#include "Clip.h"
#include "OpenMPUtilities.h"
#include "Settings.h"
#include <cstdlib>
namespace openshot {
@@ -148,6 +149,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;

View File

@@ -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 <malloc.h>
#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

View File

@@ -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;
@@ -1343,8 +1334,8 @@ 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);
// Find the audio encoder
codec = avcodec_find_encoder_by_name(info.acodec.c_str());
@@ -1418,8 +1409,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) {
@@ -1559,7 +1550,7 @@ void FFmpegWriter::open_video(AVFormatContext *oc, AVStream *st) {
av_dict_free(&opts);
// Add video metadata (if any)
for (std::map<std::string, std::string>::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> 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<uint8_t*>(
reinterpret_cast<const uint8_t*>(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 encoders 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<uint8_t*>(
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<size_t>(bytes_final)
);
// Queue the deepcopied 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 sidedata 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<AVSphericalProjection>(proj);
@@ -2383,14 +2389,6 @@ void FFmpegWriter::AddSphericalMetadata(const std::string& projection,
map->pitch = static_cast<int32_t>(pitch_deg * (1 << 16));
map->roll = static_cast<int32_t>(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<uint8_t*>(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<uint8_t*>(map), sd_size);
#endif
}

View File

@@ -134,9 +134,10 @@ namespace openshot {
uint8_t *audio_outbuf;
uint8_t *audio_encoder_buffer;
int num_of_rescalers;
int rescaler_position;
std::vector<SwsContext *> 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

View File

@@ -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.)

View File

@@ -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 its actually running.
bool VideoCacheThread::StartThread()
{
// JUCEs 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<Timeline*>(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<int64_t>(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 weve 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<int64_t>(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) {

View File

@@ -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 its 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).

View File

@@ -4,7 +4,7 @@
* @author FeRD (Frank Dana) <ferdnyc@gmail.com>
*/
// 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 <iostream>
#include <Qt>
#include <QTextStream>
#include <cstdlib>
// 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(_WIN32)
_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<uint8_t *>(info);
delete[] qbuffer;
// Free the aligned memory buffer
aligned_free(info);
}
} // namespace

View File

@@ -10,8 +10,8 @@
//
// SPDX-License-Identifier: LGPL-3.0-or-later
#include <cstdlib> // For std::getenv
#include <cstdlib>
#include <omp.h>
#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;

View File

@@ -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;

View File

@@ -11,6 +11,7 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
#include "openshot_catch.h"
#include <sstream>
#include "Profiles.h"

View File

@@ -13,15 +13,21 @@
#include "openshot_catch.h"
#include "Settings.h"
#include <omp.h>
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);
}