You've already forked libopenshot
mirror of
https://github.com/OpenShot/libopenshot.git
synced 2026-03-02 08:53:52 -08:00
Merge pull request #1014 from OpenShot/ffmpeg-performance
FFmpeg Performance - Let's GO!
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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/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();
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 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<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 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<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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.)
|
||||
|
||||
@@ -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<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 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<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) {
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
#include "openshot_catch.h"
|
||||
#include <sstream>
|
||||
|
||||
|
||||
#include "Profiles.h"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user