/** * @file * @brief Benchmark executable for core libopenshot operations * @author Jonathan Thomas * @ref License */ // Copyright (c) 2025 OpenShot Studios, LLC // // SPDX-License-Identifier: LGPL-3.0-or-later #include #include #include #include #include #include #include #include #include #include extern "C" { #include } #include "BenchmarkOptions.h" #include "Clip.h" #include "FFmpegReader.h" #include "FFmpegWriter.h" #include "Frame.h" #include "Fraction.h" #include "FrameMapper.h" #ifdef USE_IMAGEMAGICK #include "ImageReader.h" #else #include "QtImageReader.h" #endif #include "ReaderBase.h" #include "Settings.h" #include "Timeline.h" // Each effect is guarded so the benchmark compiles against older libopenshot // builds that predate a given effect — missing effects are silently skipped. #if __has_include("effects/AudioVisualization.h") # include "effects/AudioVisualization.h" # define OPENSHOT_HAS_AUDIOVISUALIZATION #endif #if __has_include("effects/BeatSync.h") # include "effects/BeatSync.h" # define OPENSHOT_HAS_BEATSYNC #endif #if __has_include("effects/Brightness.h") # include "effects/Brightness.h" # define OPENSHOT_HAS_BRIGHTNESS #endif #if __has_include("effects/ChromaKey.h") # include "effects/ChromaKey.h" # define OPENSHOT_HAS_CHROMAKEY #endif #if __has_include("effects/ColorGrade.h") # include "effects/ColorGrade.h" # define OPENSHOT_HAS_COLORGRADE #endif #if __has_include("effects/Crop.h") # include "effects/Crop.h" # define OPENSHOT_HAS_CROP #endif #if __has_include("effects/FilmGrain.h") # include "effects/FilmGrain.h" # define OPENSHOT_HAS_FILMGRAIN #endif #if __has_include("effects/Glow.h") # include "effects/Glow.h" # define OPENSHOT_HAS_GLOW #endif #if __has_include("effects/Mask.h") # include "effects/Mask.h" # define OPENSHOT_HAS_MASK #endif #if __has_include("effects/Saturation.h") # include "effects/Saturation.h" # define OPENSHOT_HAS_SATURATION #endif #if __has_include("effects/Shadow.h") # include "effects/Shadow.h" # define OPENSHOT_HAS_SHADOW #endif #if __has_include("effects/Blur.h") # include "effects/Blur.h" # define OPENSHOT_HAS_BLUR #endif #if __has_include("effects/Sharpen.h") # include "effects/Sharpen.h" # define OPENSHOT_HAS_SHARPEN #endif #if __has_include("effects/DenoiseImage.h") # include "effects/DenoiseImage.h" # define OPENSHOT_HAS_DENOISEIMAGE #endif #if __has_include("effects/Hue.h") # include "effects/Hue.h" # define OPENSHOT_HAS_HUE #endif #if __has_include("effects/Negate.h") # include "effects/Negate.h" # define OPENSHOT_HAS_NEGATE #endif #if __has_include("effects/Pixelate.h") # include "effects/Pixelate.h" # define OPENSHOT_HAS_PIXELATE #endif #if __has_include("effects/Wave.h") # include "effects/Wave.h" # define OPENSHOT_HAS_WAVE #endif #if __has_include("effects/Caption.h") # include "effects/Caption.h" # define OPENSHOT_HAS_CAPTION #endif #if __has_include("effects/Displace.h") # include "effects/Displace.h" # define OPENSHOT_HAS_DISPLACE #endif #if __has_include("FrameScope.h") # include "FrameScope.h" # define OPENSHOT_HAS_FRAMESCOPE #endif #include #include using namespace openshot; using namespace std; using Clock = chrono::steady_clock; using TrialResult = pair; // (frames, elapsed_seconds) using TrialFunc = function; using Trial = pair; // Frame 241 = 10 seconds into a 24 fps source (frames are 1-based: 24*10 + 1). // The seek to this frame is included in the timed window — it's real cost OpenShot pays. constexpr int64_t START_FRAME = 241; constexpr int64_t BENCH_FRAMES = 120; struct BenchmarkRecord { string name; double fps; }; // Each trial is run this many times; the median is reported. // The first (cold-cache) run is naturally the low outlier and is discarded by the median, // giving stable warm-state numbers without an explicit warmup pass. constexpr int TRIAL_RUNS = 3; static const char* spinner_frames[] = {"⠋","⠙","⠸","⠴","⠦","⠇"}; static int spinner_index = 0; static BenchmarkRecord run_trial(const string& name, TrialFunc func) { vector samples; samples.reserve(TRIAL_RUNS); for (int run = 1; run <= TRIAL_RUNS; ++run) { if (isatty(STDOUT_FILENO)) { cout << "\r" << spinner_frames[spinner_index % 6] << " " << left << setw(44) << (name + "...") << " [" << run << "/" << TRIAL_RUNS << "]" << flush; spinner_index++; } TrialResult r = func(); samples.push_back(r.first / r.second); } sort(samples.begin(), samples.end()); return {name, samples[TRIAL_RUNS / 2]}; // median } static void print_results(const vector& records) { if (records.empty()) return; double max_fps = 0.0; size_t max_name = 5; // at least "Trial" for (const auto& rec : records) { max_fps = std::max(max_fps, rec.fps); max_name = std::max(max_name, rec.name.size()); } const int bar_width = 24; const double sqrt_max = std::sqrt(max_fps); auto make_bar = [&](double fps) { int n = static_cast(std::sqrt(fps) / sqrt_max * bar_width + 0.5); string bar; bar.reserve(static_cast(n) * 3); for (int i = 0; i < n; ++i) bar += "\xe2\x96\x88"; // UTF-8 █ return bar; }; cout << "| " << left << setw(static_cast(max_name)) << "Trial" << " | " << right << setw(8) << "FPS" << " | Chart |\n"; cout << "|:" << string(max_name, '-') << "-|" << string(8, '-') << ":|:" << string(bar_width, '-') << "-|\n"; for (const auto& rec : records) { cout << "| " << left << setw(static_cast(max_name)) << rec.name << " | " << fixed << setprecision(1) << right << setw(8) << rec.fps << " | " << make_bar(rec.fps) << " |\n"; } } // Time a forward sequential read of BENCH_FRAMES frames starting at START_FRAME. // The seek to START_FRAME is included — it reflects real scrubbing cost. static TrialResult timed_read(ReaderBase& r) { auto t0 = Clock::now(); for (int64_t i = 0; i < BENCH_FRAMES; ++i) r.GetFrame(START_FRAME + i); return {BENCH_FRAMES, chrono::duration(Clock::now() - t0).count()}; } #if defined(OPENSHOT_HAS_AUDIOVISUALIZATION) || defined(OPENSHOT_HAS_BEATSYNC) static std::shared_ptr make_audio_visualization_frame(int64_t frame_number) { const int width = 1280; const int height = 720; const int sample_rate = 48000; const int samples = 1600; constexpr double pi = 3.14159265358979323846; auto frame = std::make_shared(frame_number, width, height, "#00000000", samples, 2); auto image = std::make_shared(width, height, QImage::Format_RGBA8888_Premultiplied); image->fill(Qt::transparent); frame->AddImage(image); frame->ResizeAudio(2, samples, sample_rate, LAYOUT_STEREO); std::vector left(samples); std::vector right(samples); for (int i = 0; i < samples; ++i) { const double t = static_cast(i + frame_number * samples) / sample_rate; left[i] = static_cast((std::sin(2.0 * pi * 110.0 * t) * 0.22) + (std::sin(2.0 * pi * 440.0 * t) * 0.48) + (std::sin(2.0 * pi * 1760.0 * t) * 0.18)); right[i] = static_cast((std::sin(2.0 * pi * 165.0 * t) * 0.18) + (std::sin(2.0 * pi * 660.0 * t) * 0.42) + (std::sin(2.0 * pi * 2640.0 * t) * 0.16)); } frame->AddAudio(true, 0, 0, left.data(), samples, 1.0f); frame->AddAudio(true, 1, 0, right.data(), samples, 1.0f); return frame; } #endif #ifdef OPENSHOT_HAS_AUDIOVISUALIZATION static void run_audio_visualization_mode(int mode, int64_t start_frame, int64_t frames) { AudioVisualization effect; effect.visualization_type = mode; effect.background = AUDIO_VISUALIZATION_BACKGROUND_TRANSPARENT; effect.detail = Keyframe(0.75); effect.glow = Keyframe(0.25); effect.intensity = Keyframe(1.25); for (int64_t i = 0; i < frames; ++i) { int64_t fn = start_frame + i; effect.GetFrame(make_audio_visualization_frame(fn), fn); } } static TrialResult timed_audio_viz(int mode) { auto t0 = Clock::now(); run_audio_visualization_mode(mode, START_FRAME, BENCH_FRAMES); return {BENCH_FRAMES, chrono::duration(Clock::now() - t0).count()}; } #endif // OPENSHOT_HAS_AUDIOVISUALIZATION int main(int argc, char* argv[]) { // QApplication is required by any effect that renders text (e.g. Caption). // It must be alive for the duration of main. #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); #endif QApplication app(argc, argv); const string base = TEST_MEDIA_PATH; const string video = base + "sintel_trailer-720p.mp4"; const string overlay = base + "front3.png"; benchmark::BenchmarkOptions options; try { vector args; args.reserve(std::max(0, argc - 1)); for (int i = 1; i < argc; ++i) args.emplace_back(argv[i]); options = benchmark::ParseBenchmarkOptions(args); } catch (const std::exception& e) { cerr << e.what() << "\n"; cerr << benchmark::BenchmarkUsage() << "\n"; return 1; } if (options.show_help) { cout << benchmark::BenchmarkUsage() << "\n"; return 0; } // Suppress FFmpeg/codec log output so it doesn't interleave with results. av_log_set_level(AV_LOG_QUIET); Settings *settings = Settings::Instance(); if (options.omp_threads > 0) { settings->OMP_THREADS = options.omp_threads; settings->ApplyOpenMPSettings(); } if (options.ff_threads > 0) { settings->FF_THREADS = options.ff_threads; } vector trials; trials.reserve(40); trials.emplace_back("FFmpegReader", [&]() -> TrialResult { FFmpegReader r(video); r.Open(); TrialResult result = timed_read(r); r.Close(); return result; }); trials.emplace_back("FFmpegWriter", [&]() -> TrialResult { FFmpegReader r(video); r.Open(); FFmpegWriter w("benchmark_output.mp4"); w.SetAudioOptions("aac", r.info.sample_rate, 192000); w.SetVideoOptions("libx264", r.info.width, r.info.height, r.info.fps, 5000000); w.Open(); auto t0 = Clock::now(); for (int64_t i = 0; i < BENCH_FRAMES; ++i) w.WriteFrame(r.GetFrame(START_FRAME + i)); double elapsed = chrono::duration(Clock::now() - t0).count(); w.Close(); r.Close(); return {BENCH_FRAMES, elapsed}; }); trials.emplace_back("FrameMapper", [&]() -> TrialResult { FFmpegReader r(video); r.Open(); FrameMapper map(&r, Fraction(30000, 1001), PULLDOWN_NONE, r.info.sample_rate, r.info.channels, r.info.channel_layout); map.Open(); TrialResult result = timed_read(map); map.Close(); r.Close(); return result; }); trials.emplace_back("Clip", [&]() -> TrialResult { Clip c(video); c.Open(); TrialResult result = timed_read(c); c.Close(); return result; }); trials.emplace_back("Timeline", [&]() -> TrialResult { Timeline t(1280, 720, Fraction(30000, 1001), 44100, 2, LAYOUT_STEREO); Clip video_clip(video); video_clip.Layer(0); video_clip.Start(0.0); video_clip.End(video_clip.Reader()->info.duration); video_clip.Open(); Clip overlay1(overlay); overlay1.Layer(1); overlay1.Start(0.0); overlay1.End(video_clip.Reader()->info.duration); overlay1.Open(); Clip overlay2(overlay); overlay2.Layer(2); overlay2.Start(0.0); overlay2.End(video_clip.Reader()->info.duration); overlay2.Open(); t.AddClip(&video_clip); t.AddClip(&overlay1); t.AddClip(&overlay2); t.Open(); t.info.video_length = t.GetMaxFrame(); TrialResult result = timed_read(t); t.Close(); return result; }); trials.emplace_back("Timeline (with transforms)", [&]() -> TrialResult { Timeline t(1280, 720, Fraction(30000, 1001), 44100, 2, LAYOUT_STEREO); Clip video_clip(video); int64_t last = video_clip.Reader()->info.video_length; video_clip.Layer(0); video_clip.Start(0.0); video_clip.End(video_clip.Reader()->info.duration); video_clip.alpha.AddPoint(1, 1.0); video_clip.alpha.AddPoint(last, 0.0); video_clip.Open(); Clip overlay1(overlay); overlay1.Layer(1); overlay1.Start(0.0); overlay1.End(video_clip.Reader()->info.duration); overlay1.Open(); overlay1.scale_x.AddPoint(1, 1.0); overlay1.scale_x.AddPoint(last, 0.25); overlay1.scale_y.AddPoint(1, 1.0); overlay1.scale_y.AddPoint(last, 0.25); Clip overlay2(overlay); overlay2.Layer(2); overlay2.Start(0.0); overlay2.End(video_clip.Reader()->info.duration); overlay2.Open(); overlay2.rotation.AddPoint(1, 90.0); t.AddClip(&video_clip); t.AddClip(&overlay1); t.AddClip(&overlay2); t.Open(); t.info.video_length = t.GetMaxFrame(); TrialResult result = timed_read(t); t.Close(); return result; }); #ifdef OPENSHOT_HAS_MASK const string mask_img = base + "mask.png"; trials.emplace_back("Effect_Mask", [&]() -> TrialResult { FFmpegReader r(video); r.Open(); # ifdef USE_IMAGEMAGICK ImageReader mask_reader(mask_img); # else QtImageReader mask_reader(mask_img); # endif mask_reader.Open(); Clip clip(&r); clip.Open(); Mask m(&mask_reader, Keyframe(0.0), Keyframe(0.5)); clip.AddEffect(&m); TrialResult result = timed_read(clip); mask_reader.Close(); clip.Close(); r.Close(); return result; }); #endif #ifdef OPENSHOT_HAS_BRIGHTNESS trials.emplace_back("Effect_Brightness", [&]() -> TrialResult { FFmpegReader r(video); r.Open(); Clip clip(&r); clip.Open(); Brightness b(Keyframe(0.5), Keyframe(1.0)); clip.AddEffect(&b); TrialResult result = timed_read(clip); clip.Close(); r.Close(); return result; }); #endif #ifdef OPENSHOT_HAS_CROP trials.emplace_back("Effect_Crop", [&]() -> TrialResult { FFmpegReader r(video); r.Open(); Clip clip(&r); clip.Open(); Crop c(Keyframe(0.25), Keyframe(0.25), Keyframe(0.25), Keyframe(0.25)); clip.AddEffect(&c); TrialResult result = timed_read(clip); clip.Close(); r.Close(); return result; }); #endif #ifdef OPENSHOT_HAS_SATURATION trials.emplace_back("Effect_Saturation", [&]() -> TrialResult { FFmpegReader r(video); r.Open(); Clip clip(&r); clip.Open(); Saturation s(Keyframe(0.25), Keyframe(0.25), Keyframe(0.25), Keyframe(0.25)); clip.AddEffect(&s); TrialResult result = timed_read(clip); clip.Close(); r.Close(); return result; }); #endif #ifdef OPENSHOT_HAS_CHROMAKEY trials.emplace_back("Effect_ChromaKey_BASIC", [&]() -> TrialResult { FFmpegReader r(video); r.Open(); Clip clip(&r); clip.Open(); ChromaKey key(Color(0, 255, 0, 255), Keyframe(80.0), Keyframe(20.0), CHROMAKEY_BASIC); clip.AddEffect(&key); TrialResult result = timed_read(clip); clip.Close(); r.Close(); return result; }); trials.emplace_back("Effect_ChromaKey_BASIC_SOFT", [&]() -> TrialResult { FFmpegReader r(video); r.Open(); Clip clip(&r); clip.Open(); ChromaKey key(Color(0, 255, 0, 255), Keyframe(80.0), Keyframe(20.0), CHROMAKEY_BASIC_SOFT); clip.AddEffect(&key); TrialResult result = timed_read(clip); clip.Close(); r.Close(); return result; }); #endif #ifdef OPENSHOT_HAS_COLORGRADE trials.emplace_back("Effect_ColorGrade", [&]() -> TrialResult { FFmpegReader r(video); r.Open(); Clip clip(&r); clip.Open(); ColorGrade cg; clip.AddEffect(&cg); TrialResult result = timed_read(clip); clip.Close(); r.Close(); return result; }); #endif #ifdef OPENSHOT_HAS_FILMGRAIN trials.emplace_back("Effect_FilmGrain", [&]() -> TrialResult { FFmpegReader r(video); r.Open(); Clip clip(&r); clip.Open(); FilmGrain fg; clip.AddEffect(&fg); TrialResult result = timed_read(clip); clip.Close(); r.Close(); return result; }); #endif #ifdef OPENSHOT_HAS_SHADOW trials.emplace_back("Effect_Shadow", [&]() -> TrialResult { FFmpegReader r(video); r.Open(); Clip clip(&r); clip.Open(); Shadow s; clip.AddEffect(&s); TrialResult result = timed_read(clip); clip.Close(); r.Close(); return result; }); #endif #ifdef OPENSHOT_HAS_GLOW trials.emplace_back("Effect_Glow", [&]() -> TrialResult { FFmpegReader r(video); r.Open(); Clip clip(&r); clip.Open(); Glow g; clip.AddEffect(&g); TrialResult result = timed_read(clip); clip.Close(); r.Close(); return result; }); #endif #ifdef OPENSHOT_HAS_BLUR trials.emplace_back("Effect_Blur", [&]() -> TrialResult { FFmpegReader r(video); r.Open(); Clip clip(&r); clip.Open(); // Gaussian-style: radius=4 px, sigma=3.0, 3 iterations Blur b(Keyframe(4), Keyframe(4), Keyframe(3.0), Keyframe(3)); clip.AddEffect(&b); TrialResult result = timed_read(clip); clip.Close(); r.Close(); return result; }); #endif #ifdef OPENSHOT_HAS_SHARPEN trials.emplace_back("Effect_Sharpen", [&]() -> TrialResult { FFmpegReader r(video); r.Open(); Clip clip(&r); clip.Open(); Sharpen s(Keyframe(1.0), Keyframe(3.0), Keyframe(0.1)); clip.AddEffect(&s); TrialResult result = timed_read(clip); clip.Close(); r.Close(); return result; }); #endif #ifdef OPENSHOT_HAS_DENOISEIMAGE trials.emplace_back("Effect_DenoiseImage", [&]() -> TrialResult { FFmpegReader r(video); r.Open(); Clip clip(&r); clip.Open(); DenoiseImage d; clip.AddEffect(&d); TrialResult result = timed_read(clip); clip.Close(); r.Close(); return result; }); #endif #ifdef OPENSHOT_HAS_HUE trials.emplace_back("Effect_Hue", [&]() -> TrialResult { FFmpegReader r(video); r.Open(); Clip clip(&r); clip.Open(); Hue h(Keyframe(0.25)); clip.AddEffect(&h); TrialResult result = timed_read(clip); clip.Close(); r.Close(); return result; }); #endif #ifdef OPENSHOT_HAS_NEGATE trials.emplace_back("Effect_Negate", [&]() -> TrialResult { FFmpegReader r(video); r.Open(); Clip clip(&r); clip.Open(); Negate n; clip.AddEffect(&n); TrialResult result = timed_read(clip); clip.Close(); r.Close(); return result; }); #endif #ifdef OPENSHOT_HAS_PIXELATE trials.emplace_back("Effect_Pixelate", [&]() -> TrialResult { FFmpegReader r(video); r.Open(); Clip clip(&r); clip.Open(); // 5 % pixelization across the full frame Pixelate p(Keyframe(0.05), Keyframe(0.0), Keyframe(0.0), Keyframe(0.0), Keyframe(0.0)); clip.AddEffect(&p); TrialResult result = timed_read(clip); clip.Close(); r.Close(); return result; }); #endif #ifdef OPENSHOT_HAS_WAVE trials.emplace_back("Effect_Wave", [&]() -> TrialResult { FFmpegReader r(video); r.Open(); Clip clip(&r); clip.Open(); Wave w(Keyframe(0.5), Keyframe(3.0), Keyframe(0.5), Keyframe(0.0), Keyframe(2.0)); clip.AddEffect(&w); TrialResult result = timed_read(clip); clip.Close(); r.Close(); return result; }); #endif #ifdef OPENSHOT_HAS_CAPTION { const char* qt_platform = std::getenv("QT_QPA_PLATFORM"); const bool headless = qt_platform && std::string(qt_platform) == "offscreen"; if (!headless) trials.emplace_back("Effect_Caption", [&]() -> TrialResult { FFmpegReader r(video); r.Open(); Clip clip(&r); clip.Open(); Caption c("WEBVTT\n\n" "00:00:10.000 --> 00:00:15.000\nThis is a test caption.\n\n" "00:00:15.000 --> 00:00:20.000\nSecond caption line."); clip.AddEffect(&c); TrialResult result = timed_read(clip); clip.Close(); r.Close(); return result; }); } // headless guard #endif #ifdef OPENSHOT_HAS_DISPLACE trials.emplace_back("Effect_Displace", [&]() -> TrialResult { FFmpegReader r(video); r.Open(); # ifdef USE_IMAGEMAGICK ImageReader map_reader(overlay); # else QtImageReader map_reader(overlay); # endif map_reader.Open(); Clip clip(&r); clip.Open(); Displace d(&map_reader, Keyframe(0.5), Keyframe(0.1), Keyframe(0.1), Keyframe(0.5), Keyframe(1.5)); clip.AddEffect(&d); TrialResult result = timed_read(clip); map_reader.Close(); clip.Close(); r.Close(); return result; }); #endif #ifdef OPENSHOT_HAS_AUDIOVISUALIZATION const std::vector> audio_visualization_modes = { {"Effect_AudioVisualization_Waveform", AUDIO_VISUALIZATION_WAVEFORM}, {"Effect_AudioVisualization_FilledWaveform", AUDIO_VISUALIZATION_FILLED_WAVEFORM}, {"Effect_AudioVisualization_Bars", AUDIO_VISUALIZATION_BARS}, {"Effect_AudioVisualization_Radial", AUDIO_VISUALIZATION_RADIAL}, {"Effect_AudioVisualization_Spectrum", AUDIO_VISUALIZATION_SPECTRUM}, {"Effect_AudioVisualization_PhaseScope", AUDIO_VISUALIZATION_PHASE_SCOPE}, {"Effect_AudioVisualization_Particles", AUDIO_VISUALIZATION_PARTICLES}, {"Effect_AudioVisualization_VUMeter", AUDIO_VISUALIZATION_VU_METER}, {"Effect_AudioVisualization_RadialBars", AUDIO_VISUALIZATION_RADIAL_BARS} }; for (const auto& mode : audio_visualization_modes) { trials.emplace_back(mode.first, [mode]() -> TrialResult { return timed_audio_viz(mode.second); }); } trials.emplace_back("Effect_AudioVisualization_SpectrumModes", [&]() -> TrialResult { const std::vector modes = { AUDIO_VISUALIZATION_BARS, AUDIO_VISUALIZATION_RADIAL, AUDIO_VISUALIZATION_SPECTRUM, AUDIO_VISUALIZATION_PARTICLES, AUDIO_VISUALIZATION_RADIAL_BARS }; auto t0 = Clock::now(); for (int mode : modes) run_audio_visualization_mode(mode, START_FRAME, BENCH_FRAMES); double elapsed = chrono::duration(Clock::now() - t0).count(); return {static_cast(modes.size()) * BENCH_FRAMES, elapsed}; }); #endif #ifdef OPENSHOT_HAS_BEATSYNC trials.emplace_back("Effect_BeatSync", [&]() -> TrialResult { BeatSync effect; effect.frequency_low = Keyframe(0.0); // full band effect.frequency_high = Keyframe(1.0); effect.threshold = Keyframe(0.05); effect.attack_ms = Keyframe(10.0); effect.decay_ms = Keyframe(200.0); auto t0 = Clock::now(); for (int64_t i = 0; i < BENCH_FRAMES; ++i) { int64_t fn = START_FRAME + i; effect.GetFrame(make_audio_visualization_frame(fn), fn); } return {BENCH_FRAMES, chrono::duration(Clock::now() - t0).count()}; }); #endif #ifdef OPENSHOT_HAS_FRAMESCOPE trials.emplace_back("FrameScope", [&]() -> TrialResult { // Mirror the openshot-qt playback path from preview_thread.py: // FrameScope() → SetWaveformColumns(widget_width) → SetVectorscopeSize(128) // → SetFrame(frame) → individual typed getters (no JsonValue). // Vectorscope size 128 is the value used during live playback when // both waveform and vectorscope panels are visible. FFmpegReader r(video); r.Open(); auto t0 = Clock::now(); for (int64_t i = 0; i < BENCH_FRAMES; ++i) { auto frame = r.GetFrame(START_FRAME + i); FrameScope scope; scope.SetWaveformColumns(256); scope.SetVectorscopeSize(128); scope.SetFrame(frame); // triggers analyze() // Read the same outputs openshot-qt reads scope.GetVideoWaveformLuma(); scope.GetVideoHistogramLuma(); scope.GetVideoVectorscope(); scope.GetVideoAverageLuma(); scope.GetVideoClippedShadows(); scope.GetVideoClippedHighlights(); scope.GetAudioChannels(); } double elapsed = chrono::duration(Clock::now() - t0).count(); r.Close(); return {BENCH_FRAMES, elapsed}; }); #endif if (options.list_only) { for (const auto& trial : trials) cout << trial.first << "\n"; return 0; } vector records; records.reserve(trials.size()); for (const auto& trial : trials) { if (!options.filter_test.empty() && trial.first != options.filter_test) continue; records.push_back(run_trial(trial.first, trial.second)); } if (!options.filter_test.empty() && records.empty()) { cerr << "Unknown test: " << options.filter_test << "\nAvailable tests:\n"; for (const auto& trial : trials) cerr << " " << trial.first << "\n"; return 2; } if (isatty(STDOUT_FILENO)) cout << "\r" << string(64, ' ') << "\r" << flush; print_results(records); return 0; }