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:
@@ -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()
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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"));
|
||||
|
||||
Reference in New Issue
Block a user