proxy: isolate proxies from export renders and improve proxy workflow

- FootageJob::should_use_proxy() centralizes the proxy decision; worker
  pre-decode now honors the render mode so exports always decode the
  original media (previously every frame was pre-decoded from proxies)
- global Tools > Use Proxy Media toggle with footage invalidation
- ffmpeg -progress parsing for real percentage feedback while generating
- divider mode (1/2, 1/4, 1/8 of source resolution) with UI, proxy
  filename tags and per-footage persistence (pdivider)
- Media Offline warning slat rendered for missing footage
- regression tests: export isolation, relink invalidation, offline slat,
  progress parsing, divider arguments
This commit is contained in:
2026-07-19 21:43:58 +08:00
parent bb40b4923e
commit 0c02ff0d77
21 changed files with 494 additions and 25 deletions
+7
View File
@@ -46,6 +46,7 @@ add_executable(olive-gtest
project_serializer_test.cpp
proxy_manager_test.cpp
proxy_dialog_test.cpp
footage_job_proxy_test.cpp
lut_file_field_test.cpp
node_math_test.cpp
node_undo_test.cpp
@@ -184,6 +185,12 @@ if (OAK_ENABLE_DYNAMIC_RENDER_BACKEND)
endif()
endif()
# Several render tests spawn the render worker binary from the build tree; it
# must be up to date with the code under test
if (TARGET olive-render-worker)
add_dependencies(olive-gtest olive-render-worker)
endif()
if (MSVC)
add_test("Olive.gtest" olive-gtest)
else()
+54
View File
@@ -0,0 +1,54 @@
#include <gtest/gtest.h>
#include <QTemporaryFile>
#include "render/job/footagejob.h"
TEST(FootageJobProxy, NoProxyNeverUsed)
{
olive::FootageJob job;
EXPECT_FALSE(job.has_proxy());
EXPECT_FALSE(job.should_use_proxy(olive::RenderMode::k_offline));
EXPECT_FALSE(job.should_use_proxy(olive::RenderMode::k_online));
}
TEST(FootageJobProxy, OfflineUsesProxyOnlineDoesNot)
{
QTemporaryFile proxy_file;
ASSERT_TRUE(proxy_file.open());
olive::FootageJob job;
job.set_proxy(proxy_file.fileName(), QStringLiteral("ffmpeg"), 0);
ASSERT_TRUE(job.has_proxy());
// Preview renders may decode from the proxy
EXPECT_TRUE(job.should_use_proxy(olive::RenderMode::k_offline));
// Export/master renders must always decode the original media
EXPECT_FALSE(job.should_use_proxy(olive::RenderMode::k_online));
}
TEST(FootageJobProxy, MissingProxyFileFallsBackToOriginal)
{
olive::FootageJob job;
job.set_proxy(QStringLiteral("/nonexistent/path/proxy.mp4"),
QStringLiteral("ffmpeg"), 0);
ASSERT_TRUE(job.has_proxy());
// A proxy that no longer exists on disk must not be used, even for preview
EXPECT_FALSE(job.should_use_proxy(olive::RenderMode::k_offline));
EXPECT_FALSE(job.should_use_proxy(olive::RenderMode::k_online));
}
TEST(FootageJobProxy, EmptyProxyFilenameDisablesProxy)
{
olive::FootageJob job;
job.set_proxy(QString(), QStringLiteral("ffmpeg"), 0);
EXPECT_FALSE(job.has_proxy());
EXPECT_FALSE(job.should_use_proxy(olive::RenderMode::k_offline));
EXPECT_FALSE(job.should_use_proxy(olive::RenderMode::k_online));
}
+52
View File
@@ -12,6 +12,7 @@
#include "common/filefunctions.h"
#include "core.h"
#include "codec/frame.h"
#include "node/color/colormanager/colormanager.h"
#include "node/globals.h"
#include "node/project.h"
@@ -19,6 +20,7 @@
#include "render/diskmanager.h"
#include "node/project/footage/footagedescription.h"
#include "render/job/footagejob.h"
#include "render/job/generatejob.h"
#include "render/loopmode.h"
#include "render/texture.h"
#include "ui/icons/icons.h"
@@ -589,6 +591,56 @@ TEST_F(FootageTest, ProxyChangesMarkProjectModifiedAndEmitSignal)
EXPECT_FALSE(footage->proxy_enabled());
}
TEST_F(FootageTest, OfflineMediaGeneratesWarningFrame)
{
olive::Footage *footage = add_footage();
footage->set_filename(QStringLiteral("/nonexistent/media.mp4"));
olive::FramePtr frame = olive::Frame::create();
frame->set_video_params(olive::VideoParams(320, 180, olive::PixelFormat::u8,
olive::VideoParams::k_rgba_channel_count));
frame->allocate();
footage->generate_frame(frame, olive::GenerateJob());
// The offline slat must be visible: dark red background/stripes plus
// white warning text
bool found_red_pixel = false;
bool found_bright_pixel = false;
const auto *data = reinterpret_cast<const uchar *>(frame->data());
for (int y = 0; y < frame->height(); y++) {
const uchar *row = data + y * frame->linesize_bytes();
for (int x = 0; x < frame->width(); x++) {
const uchar *px = row + x * 4;
if (px[0] > 40 && px[1] < px[0] / 2 && px[2] < px[0] / 2) {
found_red_pixel = true;
}
if (px[0] > 200 && px[1] > 200 && px[2] > 200) {
found_bright_pixel = true;
}
}
}
EXPECT_TRUE(found_red_pixel);
EXPECT_TRUE(found_bright_pixel);
}
TEST_F(FootageTest, RelinkClearsStaleProxy)
{
olive::Footage *footage = add_footage();
footage->set_filename(QStringLiteral("/media/original.mov"));
footage->set_proxy(QStringLiteral("/cache/proxy/example.mp4"),
olive::ProxyManager::k_proxy_ready, 0, 1, true);
ASSERT_FALSE(footage->proxy_path().isEmpty());
// Relinking to a different file must invalidate the proxy that was
// generated from the old source (Footage::clear() drops it when the
// filename input changes)
footage->set_filename(QStringLiteral("/media/relinked.mov"));
EXPECT_TRUE(footage->proxy_path().isEmpty());
EXPECT_FALSE(footage->proxy_enabled());
EXPECT_EQ(footage->proxy_state(), olive::ProxyManager::k_proxy_missing);
}
TEST_F(FootageTest, ReprobeRestoresStreamsFromMetadataCache)
{
QTemporaryDir dir;
+66
View File
@@ -437,12 +437,76 @@ TEST(ProxyTask, BuildArgumentsDisablesAudioWhenDisabled)
EXPECT_FALSE(args.contains(QStringLiteral("0:a?")));
}
TEST(ProxyTask, ParseProgressReadsOutTime)
{
EXPECT_DOUBLE_EQ(olive::ProxyTask::parse_progress(
QStringLiteral("out_time_us=5000000"), 10.0),
0.5);
// Despite the name, out_time_ms is also reported in microseconds
EXPECT_DOUBLE_EQ(olive::ProxyTask::parse_progress(
QStringLiteral("out_time_ms=2500000"), 10.0),
0.25);
}
TEST(ProxyTask, ParseProgressClampsToOne)
{
EXPECT_DOUBLE_EQ(olive::ProxyTask::parse_progress(
QStringLiteral("out_time_us=20000000"), 10.0),
1.0);
}
TEST(ProxyTask, ParseProgressIgnoresUnknownDuration)
{
EXPECT_LT(olive::ProxyTask::parse_progress(
QStringLiteral("out_time_us=5000000"), 0.0),
0.0);
}
TEST(ProxyTask, ParseProgressIgnoresUnrelatedLines)
{
EXPECT_LT(olive::ProxyTask::parse_progress(QStringLiteral("frame= 250"),
10.0),
0.0);
EXPECT_LT(olive::ProxyTask::parse_progress(QStringLiteral("progress=end"),
10.0),
0.0);
}
TEST(ProxyTask, BuildArgumentsDividerScalesFromSource)
{
olive::ProxyManager::ProxyParams params;
params.divider = 4;
const QStringList args = olive::ProxyTask::build_arguments(
QStringLiteral("/media/source.mov"), 0, params,
QStringLiteral("/cache/proxy/out.mp4"));
const int vf = args.indexOf(QStringLiteral("-vf"));
ASSERT_GE(vf, 0);
EXPECT_EQ(args.at(vf + 1),
QStringLiteral("scale=w=trunc(iw/4/2)*2:h=trunc(ih/4/2)*2"));
}
TEST(ProxyManager, DividerProxyFilenameUsesDividerTag)
{
olive::ProxyManager::ProxyParams params;
params.divider = 2;
const QString filename = olive::ProxyManager::get_proxy_filename(
QStringLiteral("/tmp/oak-cache"), QStringLiteral("/media/source.mov"),
0, params);
EXPECT_TRUE(QFileInfo(filename).fileName().contains(
QStringLiteral(".div2.")));
}
TEST(ProxyManager, FootagePersistsCustomProxyParams)
{
olive::Footage footage;
olive::ProxyManager::ProxyParams params;
params.width = 640;
params.height = 360;
params.divider = 4;
params.crf = 30;
params.preset = QStringLiteral("faster");
params.extension = QStringLiteral("mov");
@@ -462,6 +526,7 @@ TEST(ProxyManager, FootagePersistsCustomProxyParams)
EXPECT_TRUE(xml.contains(QStringLiteral("custom=\"1\"")));
EXPECT_TRUE(xml.contains(QStringLiteral("pwidth=\"640\"")));
EXPECT_TRUE(xml.contains(QStringLiteral("pheight=\"360\"")));
EXPECT_TRUE(xml.contains(QStringLiteral("pdivider=\"4\"")));
EXPECT_TRUE(xml.contains(QStringLiteral("pcrf=\"30\"")));
EXPECT_TRUE(xml.contains(QStringLiteral("ppreset=\"faster\"")));
EXPECT_TRUE(xml.contains(QStringLiteral("pext=\"mov\"")));
@@ -476,6 +541,7 @@ TEST(ProxyManager, FootagePersistsCustomProxyParams)
ASSERT_TRUE(loaded.has_custom_proxy_params());
EXPECT_EQ(loaded.custom_proxy_params().width, 640);
EXPECT_EQ(loaded.custom_proxy_params().height, 360);
EXPECT_EQ(loaded.custom_proxy_params().divider, 4);
EXPECT_EQ(loaded.custom_proxy_params().crf, 30);
EXPECT_EQ(loaded.custom_proxy_params().preset, QStringLiteral("faster"));
EXPECT_EQ(loaded.custom_proxy_params().extension, QStringLiteral("mov"));