proxy: per-footage presets, audio in proxies, configurable ffmpeg path

- Footage can store custom proxy parameters (width/height/crf/preset/
  extension/audio) that override the global settings; they are
  serialized with the project and used by every generation entry point
  via Footage::GetEffectiveProxyParams()
- Proxies now include the source audio streams (AAC) unless disabled;
  the proxy filename records the audio flag and offline audio rendering
  decodes from the proxy when present
- ProxyTask resolves ffmpeg from the new FFmpegPath config key first,
  then PATH, then common install locations (e.g. Homebrew on macOS),
  instead of relying on PATH only; the error message points at the
  preferences when no executable is found
- ProxyTask::BuildArguments() is extracted for testability
- Add ProxyIncludeAudio and FFmpegPath config defaults
- Add regression tests for the filename audio marker, config-backed
  params, ffmpeg resolution, argument building, and custom-param
  persistence
This commit is contained in:
2026-07-16 22:31:37 +08:00
parent 15525528c9
commit 6aaf37e2e5
11 changed files with 506 additions and 38 deletions
+73 -4
View File
@@ -18,11 +18,14 @@
#include "proxymanager.h"
#include <QCoreApplication>
#include <QDir>
#include <QFileInfo>
#include <QMutexLocker>
#include <QStandardPaths>
#include "common/filefunctions.h"
#include "config/config.h"
#include "task/proxy/proxy.h"
#include "task/taskmanager.h"
@@ -36,7 +39,8 @@ bool ProxyParamsEqual(const ProxyManager::ProxyParams &a,
{
return a.width == b.width && a.height == b.height &&
a.version == b.version && a.extension == b.extension &&
a.crf == b.crf && a.preset == b.preset;
a.crf == b.crf && a.preset == b.preset &&
a.include_audio == b.include_audio;
}
QString ProxyManager::GetProxyDirectory(const QString &cache_path)
@@ -53,11 +57,12 @@ QString ProxyManager::GetProxyFilename(const QString &cache_path,
const QString extension =
params.extension.isEmpty() ? QStringLiteral("mp4") : params.extension;
const QString filename =
QStringLiteral("%1-%2.%3x%4.v%5.%6")
QStringLiteral("%1-%2.%3x%4.v%5.a%6.%7")
.arg(FileFunctions::GetUniqueFileIdentifier(source_filename),
QString::number(stream_index), QString::number(params.width),
QString::number(params.height),
QString::number(params.version), extension);
QString::number(params.height), QString::number(params.version),
params.include_audio ? QStringLiteral("1") : QStringLiteral("0"),
extension);
return QDir(proxy_dir).filePath(filename);
}
@@ -117,6 +122,70 @@ ProxyManager::ProxyStateFromString(const QString &state)
return kProxyMissing;
}
bool ProxyManager::ProxyFilenameHasAudio(const QString &proxy_filename)
{
return QFileInfo(proxy_filename).fileName().contains(
QStringLiteral(".a1."));
}
ProxyManager::ProxyParams ProxyManager::ProxyParamsFromConfig()
{
ProxyParams params;
params.width = OLIVE_CONFIG("ProxyWidth").value<int>();
params.height = OLIVE_CONFIG("ProxyHeight").value<int>();
params.crf = OLIVE_CONFIG("ProxyCRF").value<int>();
params.preset = OLIVE_CONFIG("ProxyPreset").toString();
params.include_audio = OLIVE_CONFIG("ProxyIncludeAudio").toBool();
return params;
}
QString ProxyManager::FindFFmpegExecutable(const QString &configured_path)
{
// An explicitly configured path takes precedence if it is usable
if (!configured_path.isEmpty()) {
const QFileInfo configured_info(configured_path);
if (configured_info.exists() && configured_info.isFile() &&
configured_info.isExecutable()) {
return configured_info.absoluteFilePath();
}
qWarning() << "Configured ffmpeg path is not a valid executable:"
<< configured_path;
}
// Fall back to searching the system PATH
const QString from_path =
QStandardPaths::findExecutable(QStringLiteral("ffmpeg"));
if (!from_path.isEmpty()) {
return from_path;
}
// Finally, try common install locations (PATH on GUI-launched apps,
// particularly on macOS, often lacks these)
QStringList candidates;
candidates.append(QCoreApplication::applicationDirPath() +
QStringLiteral("/ffmpeg"));
#ifdef Q_OS_MAC
candidates.append(QStringLiteral("/opt/homebrew/bin/ffmpeg"));
candidates.append(QStringLiteral("/usr/local/bin/ffmpeg"));
#endif
#ifdef Q_OS_WINDOWS
candidates.append(QCoreApplication::applicationDirPath() +
QStringLiteral("/ffmpeg.exe"));
#endif
candidates.append(QStringLiteral("/usr/bin/ffmpeg"));
candidates.append(QStringLiteral("/usr/local/bin/ffmpeg"));
for (const QString &candidate : candidates) {
const QFileInfo info(candidate);
if (info.exists() && info.isFile() && info.isExecutable()) {
return info.absoluteFilePath();
}
}
return QString();
}
ProxyManager::Proxy
ProxyManager::GetOrStartProxy(const QString &cache_path,
const QString &source_filename, int stream_index,
+22
View File
@@ -66,6 +66,7 @@ public:
QString extension = QStringLiteral("mp4");
int crf = 23;
QString preset = QStringLiteral("veryfast");
bool include_audio = true;
};
struct Proxy {
@@ -89,6 +90,27 @@ public:
static ProxyState ProxyStateFromString(const QString &state);
/**
* @brief Returns true if a proxy filename generated by GetProxyFilename()
* indicates the proxy contains audio streams
*/
static bool ProxyFilenameHasAudio(const QString &proxy_filename);
/**
* @brief Builds proxy parameters from the global application config
*/
static ProxyParams ProxyParamsFromConfig();
/**
* @brief Locates an ffmpeg executable for proxy generation
*
* Resolution order: the explicitly configured path (if non-empty and an
* existing executable file), then the system PATH, then common
* platform-specific install locations. Returns an empty string if no
* executable could be found.
*/
static QString FindFFmpegExecutable(const QString &configured_path);
Proxy GetOrStartProxy(const QString &cache_path,
const QString &source_filename, int stream_index,
const ProxyParams &params);
+4
View File
@@ -235,6 +235,10 @@ void Config::SetDefaults()
SetEntryInternal(QStringLiteral("ProxyCRF"), NodeValue::kInt, 23);
SetEntryInternal(QStringLiteral("ProxyPreset"), NodeValue::kText,
QStringLiteral("veryfast"));
SetEntryInternal(QStringLiteral("ProxyIncludeAudio"), NodeValue::kBoolean,
true);
SetEntryInternal(QStringLiteral("FFmpegPath"), NodeValue::kText,
QString());
SetEntryInternal(QStringLiteral("DefaultSequenceWidth"), NodeValue::kInt,
1920);
+97
View File
@@ -50,6 +50,7 @@ Footage::Footage(const QString &filename)
, proxy_state_(ProxyManager::kProxyMissing)
, proxy_video_stream_index_(-1)
, proxy_preset_version_(0)
, has_custom_proxy_params_(false)
, valid_(false)
, cancelled_(nullptr)
, total_stream_count_(0)
@@ -269,6 +270,37 @@ void Footage::ClearProxy()
emit ProxySettingsChanged();
}
void Footage::SetCustomProxyParams(const ProxyManager::ProxyParams &params)
{
custom_proxy_params_ = params;
has_custom_proxy_params_ = true;
if (Project *p = project()) {
p->set_modified(true);
}
emit ProxySettingsChanged();
}
void Footage::ClearCustomProxyParams()
{
if (has_custom_proxy_params_) {
has_custom_proxy_params_ = false;
custom_proxy_params_ = ProxyManager::ProxyParams();
if (Project *p = project()) {
p->set_modified(true);
}
emit ProxySettingsChanged();
}
}
ProxyManager::ProxyParams Footage::GetEffectiveProxyParams() const
{
if (has_custom_proxy_params_) {
return custom_proxy_params_;
}
return ProxyManager::ProxyParamsFromConfig();
}
QString Footage::DescribeVideoStream(const VideoParams &params)
{
if (params.video_type() == VideoParams::kVideoTypeStill) {
@@ -353,6 +385,26 @@ void Footage::Value(const NodeValueRow &value, const NodeGlobals &globals,
job.set_audio_params(ap);
job.set_cache_path(project()->cache_path());
// Proxies generated with audio contain the video stream at
// index 0 followed by all source audio streams in source order
if (proxy_enabled_ && !proxy_path_.isEmpty() &&
ProxyManager::GetProxyState(proxy_path_) ==
ProxyManager::kProxyReady &&
ProxyManager::ProxyFilenameHasAudio(proxy_path_)) {
int audio_rank = 0;
for (int i = 0; i < GetTotalStreamCount(); i++) {
const Track::Reference other =
GetReferenceFromRealIndex(i);
if (other.type() == Track::kAudio &&
GetAudioParams(other.index()).stream_index() <
ap.stream_index()) {
audio_rank++;
}
}
job.set_proxy(proxy_path_, QStringLiteral("ffmpeg"),
audio_rank + 1);
}
table->Push(NodeValue::kSamples, QVariant::fromValue(job), this,
ref.ToString());
}
@@ -530,6 +582,8 @@ bool Footage::LoadCustom(QXmlStreamReader *reader, SerializedData *data)
ProxyManager::ProxyState state = ProxyManager::kProxyMissing;
int stream = -1;
int preset_version = 0;
bool has_custom_params = false;
ProxyManager::ProxyParams custom_params;
{
XMLAttributeLoop(reader, attr)
{
@@ -543,10 +597,32 @@ bool Footage::LoadCustom(QXmlStreamReader *reader, SerializedData *data)
stream = attr.value().toInt();
} else if (attr.name() == QStringLiteral("preset")) {
preset_version = attr.value().toInt();
} else if (attr.name() == QStringLiteral("custom")) {
has_custom_params =
(attr.value() == QStringLiteral("1") ||
attr.value() == QStringLiteral("true"));
} else if (attr.name() == QStringLiteral("pwidth")) {
custom_params.width = attr.value().toInt();
} else if (attr.name() == QStringLiteral("pheight")) {
custom_params.height = attr.value().toInt();
} else if (attr.name() == QStringLiteral("pcrf")) {
custom_params.crf = attr.value().toInt();
} else if (attr.name() == QStringLiteral("ppreset")) {
custom_params.preset = attr.value().toString();
} else if (attr.name() == QStringLiteral("pext")) {
custom_params.extension = attr.value().toString();
} else if (attr.name() == QStringLiteral("paudio")) {
custom_params.include_audio =
(attr.value() == QStringLiteral("1") ||
attr.value() == QStringLiteral("true"));
}
}
}
if (has_custom_params) {
SetCustomProxyParams(custom_params);
}
const QString path = reader->readElementText();
if (!path.isEmpty()) {
SetProxy(path, state, stream, preset_version, enabled);
@@ -608,6 +684,27 @@ void Footage::SaveCustom(QXmlStreamWriter *writer) const
QString::number(proxy_video_stream_index_));
writer->writeAttribute(QStringLiteral("preset"),
QString::number(proxy_preset_version_));
if (has_custom_proxy_params_) {
writer->writeAttribute(QStringLiteral("custom"),
QStringLiteral("1"));
writer->writeAttribute(
QStringLiteral("pwidth"),
QString::number(custom_proxy_params_.width));
writer->writeAttribute(
QStringLiteral("pheight"),
QString::number(custom_proxy_params_.height));
writer->writeAttribute(
QStringLiteral("pcrf"),
QString::number(custom_proxy_params_.crf));
writer->writeAttribute(QStringLiteral("ppreset"),
custom_proxy_params_.preset);
writer->writeAttribute(QStringLiteral("pext"),
custom_proxy_params_.extension);
writer->writeAttribute(
QStringLiteral("paudio"),
custom_proxy_params_.include_audio ? QStringLiteral("1") :
QStringLiteral("0"));
}
writer->writeCharacters(proxy_path_);
writer->writeEndElement();
}
+34
View File
@@ -205,6 +205,36 @@ public:
void ClearProxy();
/**
* @brief Returns true if this footage uses its own proxy parameters
* instead of the global proxy settings
*/
bool has_custom_proxy_params() const
{
return has_custom_proxy_params_;
}
const ProxyManager::ProxyParams &custom_proxy_params() const
{
return custom_proxy_params_;
}
/**
* @brief Sets per-footage proxy parameters, overriding the global settings
*/
void SetCustomProxyParams(const ProxyManager::ProxyParams &params);
/**
* @brief Reverts this footage to using the global proxy settings
*/
void ClearCustomProxyParams();
/**
* @brief Returns the custom proxy parameters if set, otherwise the
* parameters from the global application config
*/
ProxyManager::ProxyParams GetEffectiveProxyParams() const;
static QString DescribeVideoStream(const VideoParams &params);
static QString DescribeAudioStream(const AudioParams &params);
static QString DescribeSubtitleStream(const SubtitleParams &params);
@@ -283,6 +313,10 @@ private:
int proxy_preset_version_;
bool has_custom_proxy_params_;
ProxyManager::ProxyParams custom_proxy_params_;
bool valid_;
CancelAtom *cancelled_;
+16 -3
View File
@@ -611,10 +611,23 @@ void RenderProcessor::ProcessAudioFootage(SampleBuffer &destination,
return;
}
// Mirror the video path: use the proxy (when enabled, ready, and containing
// audio) for offline renders only, never for export
const bool use_proxy =
static_cast<RenderMode::Mode>(ticket_->property("mode").toInt()) ==
RenderMode::kOffline &&
stream->has_proxy() && QFileInfo::exists(stream->proxy_filename());
const QString decode_filename = use_proxy ? stream->proxy_filename() :
stream->filename();
const QString decoder_id = use_proxy ? stream->proxy_decoder() :
stream->decoder();
const int stream_index = use_proxy ?
stream->proxy_stream_index() :
stream->audio_params().stream_index();
DecoderPtr decoder = ResolveDecoderFromInput(
stream->decoder(),
Decoder::CodecStream(stream->filename(),
stream->audio_params().stream_index(), nullptr));
decoder_id,
Decoder::CodecStream(decode_filename, stream_index, nullptr));
if (decoder) {
const AudioParams &audio_params = GetCacheAudioParams();
+47 -21
View File
@@ -26,6 +26,8 @@
#include <QProcess>
#include <QStandardPaths>
#include "config/config.h"
namespace olive
{
@@ -41,13 +43,53 @@ ProxyTask::ProxyTask(const QString &source_filename, int stream_index,
.arg(source_filename_, QString::number(stream_index_)));
}
QStringList ProxyTask::BuildArguments(const QString &source_filename,
int stream_index,
const ProxyManager::ProxyParams &params,
const QString &output_filename)
{
const QString scale_filter =
QStringLiteral("scale=w=%1:h=%2:force_original_aspect_ratio=decrease")
.arg(QString::number(params.width),
QString::number(params.height));
const QString container_format =
params.extension.isEmpty() ? QStringLiteral("mp4") : params.extension;
QStringList args;
args << QStringLiteral("-y") << QStringLiteral("-i") << source_filename
// Map the requested video stream first so it is stream 0 in the proxy
<< QStringLiteral("-map") << QStringLiteral("0:%1").arg(stream_index);
if (params.include_audio) {
// Keep the source audio (if any) so the proxy can also be used for
// audio preview. Audio streams follow the video stream in source order.
args << QStringLiteral("-map") << QStringLiteral("0:a?")
<< QStringLiteral("-c:a") << QStringLiteral("aac")
<< QStringLiteral("-b:a") << QStringLiteral("128k");
} else {
args << QStringLiteral("-an");
}
args << QStringLiteral("-vf") << scale_filter << QStringLiteral("-c:v")
<< QStringLiteral("libx264") << QStringLiteral("-preset")
<< params.preset << QStringLiteral("-crf")
<< QString::number(params.crf) << QStringLiteral("-pix_fmt")
<< QStringLiteral("yuv420p") << QStringLiteral("-movflags")
<< QStringLiteral("+faststart") << QStringLiteral("-f")
<< container_format << output_filename;
return args;
}
bool ProxyTask::Run()
{
const QString ffmpeg =
QStandardPaths::findExecutable(QStringLiteral("ffmpeg"));
const QString ffmpeg = ProxyManager::FindFFmpegExecutable(
OLIVE_CONFIG("FFmpegPath").toString());
if (ffmpeg.isEmpty()) {
SetError(
tr("Failed to generate proxy: ffmpeg executable was not found"));
tr("Failed to generate proxy: ffmpeg executable was not found. Set "
"the ffmpeg path in Preferences > Disk > Proxy Settings."));
qWarning() << "ProxyTask: ffmpeg executable not found";
return false;
}
@@ -66,24 +108,8 @@ bool ProxyTask::Run()
QFile::remove(output_filename_);
const QString scale_filter =
QStringLiteral("scale=w=%1:h=%2:force_original_aspect_ratio=decrease")
.arg(QString::number(params_.width),
QString::number(params_.height));
const QString container_format =
params_.extension.isEmpty() ? QStringLiteral("mp4") : params_.extension;
QStringList args;
args << QStringLiteral("-y") << QStringLiteral("-i") << source_filename_
<< QStringLiteral("-map") << QStringLiteral("0:%1").arg(stream_index_)
<< QStringLiteral("-an") << QStringLiteral("-vf") << scale_filter
<< QStringLiteral("-c:v") << QStringLiteral("libx264")
<< QStringLiteral("-preset") << params_.preset
<< QStringLiteral("-crf") << QString::number(params_.crf)
<< QStringLiteral("-pix_fmt") << QStringLiteral("yuv420p")
<< QStringLiteral("-movflags") << QStringLiteral("+faststart")
<< QStringLiteral("-f") << container_format << output_filename_;
const QStringList args = BuildArguments(source_filename_, stream_index_,
params_, output_filename_);
QProcess process;
process.setProgram(ffmpeg);
+12
View File
@@ -32,6 +32,18 @@ public:
const ProxyManager::ProxyParams &params,
const QString &output_filename);
/**
* @brief Builds the ffmpeg command line for a proxy generation run
*
* Extracted for testability. The video stream is always mapped first so
* that it is stream 0 in the proxy file; audio streams (when enabled)
* follow in source order.
*/
static QStringList BuildArguments(const QString &source_filename,
int stream_index,
const ProxyManager::ProxyParams &params,
const QString &output_filename);
protected:
virtual bool Run() override;
@@ -611,11 +611,7 @@ void ProjectExplorer::GenerateProxiesForSelectedFootage()
continue;
}
ProxyManager::ProxyParams params;
params.width = OLIVE_CONFIG("ProxyWidth").value<int>();
params.height = OLIVE_CONFIG("ProxyHeight").value<int>();
params.crf = OLIVE_CONFIG("ProxyCRF").value<int>();
params.preset = OLIVE_CONFIG("ProxyPreset").toString();
ProxyManager::ProxyParams params = item->GetEffectiveProxyParams();
const ProxyManager::Proxy proxy =
ProxyManager::instance()->GetOrStartProxy(
item->project()->cache_path(), item->filename(),
+1 -5
View File
@@ -1154,11 +1154,7 @@ void TimelineWidget::GenerateProxiesForSelectedClips()
continue;
}
ProxyManager::ProxyParams params;
params.width = OLIVE_CONFIG("ProxyWidth").value<int>();
params.height = OLIVE_CONFIG("ProxyHeight").value<int>();
params.crf = OLIVE_CONFIG("ProxyCRF").value<int>();
params.preset = OLIVE_CONFIG("ProxyPreset").toString();
ProxyManager::ProxyParams params = item->GetEffectiveProxyParams();
const ProxyManager::Proxy proxy =
ProxyManager::instance()->GetOrStartProxy(
item->project()->cache_path(), item->filename(),
+199
View File
@@ -1,5 +1,6 @@
#include <gtest/gtest.h>
#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QTemporaryDir>
@@ -7,8 +8,20 @@
#include <QXmlStreamWriter>
#include "codec/proxymanager.h"
#include "config/config.h"
#include "node/project/footage/footage.h"
#include "render/job/footagejob.h"
#include "task/proxy/proxy.h"
namespace
{
QVariant ProxyConfigValue(const char *key)
{
return olive::Config::Current()[QString::fromUtf8(key)];
}
} // namespace
TEST(ProxyManager, BuildsStableProxyFilename)
{
@@ -275,3 +288,189 @@ TEST(ProxyManager, FootageJobWithoutProxyHasEmptyProxyFields)
EXPECT_TRUE(job.proxy_decoder().isEmpty());
EXPECT_EQ(job.proxy_stream_index(), -1);
}
TEST(ProxyManager, ProxyFilenameIncludesAudioFlag)
{
olive::ProxyManager::ProxyParams params;
params.include_audio = true;
const QString with_audio = olive::ProxyManager::GetProxyFilename(
QStringLiteral("/tmp/oak-cache"), QStringLiteral("/media/source.mov"),
0, params);
EXPECT_TRUE(with_audio.contains(QStringLiteral(".a1.")));
EXPECT_TRUE(olive::ProxyManager::ProxyFilenameHasAudio(with_audio));
params.include_audio = false;
const QString without_audio = olive::ProxyManager::GetProxyFilename(
QStringLiteral("/tmp/oak-cache"), QStringLiteral("/media/source.mov"),
0, params);
EXPECT_TRUE(without_audio.contains(QStringLiteral(".a0.")));
EXPECT_FALSE(olive::ProxyManager::ProxyFilenameHasAudio(without_audio));
// Legacy proxy filenames (no audio marker) must be treated as video-only
EXPECT_FALSE(olive::ProxyManager::ProxyFilenameHasAudio(
QStringLiteral("/tmp/oak-cache/proxy/abc-0.1280x720.v1.mp4")));
// The audio flag distinguishes otherwise identical proxy filenames
EXPECT_NE(with_audio, without_audio);
}
TEST(ProxyManager, ProxyParamsFromConfigReadsDefaults)
{
const olive::ProxyManager::ProxyParams params =
olive::ProxyManager::ProxyParamsFromConfig();
EXPECT_EQ(params.width, ProxyConfigValue("ProxyWidth").value<int>());
EXPECT_EQ(params.height, ProxyConfigValue("ProxyHeight").value<int>());
EXPECT_EQ(params.crf, ProxyConfigValue("ProxyCRF").value<int>());
EXPECT_EQ(params.preset, ProxyConfigValue("ProxyPreset").toString());
EXPECT_EQ(params.include_audio,
ProxyConfigValue("ProxyIncludeAudio").toBool());
}
TEST(ProxyManager, FindFFmpegExecutablePrefersConfiguredPath)
{
// The test executable itself is guaranteed to be an existing executable
// file, making it a safe stand-in for an ffmpeg binary
const QString self = QCoreApplication::applicationFilePath();
ASSERT_FALSE(self.isEmpty());
EXPECT_EQ(olive::ProxyManager::FindFFmpegExecutable(self), self);
}
TEST(ProxyManager, FindFFmpegExecutableRejectsInvalidConfiguredPath)
{
const QString bogus = QStringLiteral("/nonexistent/ffmpeg-binary");
const QString result = olive::ProxyManager::FindFFmpegExecutable(bogus);
// Must not return the invalid configured path; any fallback is acceptable
EXPECT_NE(result, bogus);
}
TEST(ProxyTask, BuildArgumentsIncludesAudioWhenEnabled)
{
olive::ProxyManager::ProxyParams params;
params.include_audio = true;
const QStringList args = olive::ProxyTask::BuildArguments(
QStringLiteral("/media/source.mov"), 1, params,
QStringLiteral("/cache/proxy/out.mp4"));
EXPECT_FALSE(args.contains(QStringLiteral("-an")));
const int audio_map = args.indexOf(QStringLiteral("0:a?"));
EXPECT_GE(audio_map, 0);
EXPECT_GT(audio_map, args.indexOf(QStringLiteral("-map")));
EXPECT_TRUE(args.contains(QStringLiteral("-c:a")));
EXPECT_TRUE(args.contains(QStringLiteral("aac")));
// The requested video stream must be mapped before the audio streams
EXPECT_LT(args.indexOf(QStringLiteral("0:1")), audio_map);
}
TEST(ProxyTask, BuildArgumentsDisablesAudioWhenDisabled)
{
olive::ProxyManager::ProxyParams params;
params.include_audio = false;
const QStringList args = olive::ProxyTask::BuildArguments(
QStringLiteral("/media/source.mov"), 1, params,
QStringLiteral("/cache/proxy/out.mp4"));
EXPECT_TRUE(args.contains(QStringLiteral("-an")));
EXPECT_FALSE(args.contains(QStringLiteral("0:a?")));
}
TEST(ProxyManager, FootagePersistsCustomProxyParams)
{
olive::Footage footage;
olive::ProxyManager::ProxyParams params;
params.width = 640;
params.height = 360;
params.crf = 30;
params.preset = QStringLiteral("faster");
params.extension = QStringLiteral("mov");
params.include_audio = false;
footage.SetCustomProxyParams(params);
footage.SetProxy(QStringLiteral("/cache/proxy/example.mp4"),
olive::ProxyManager::kProxyReady, 0, 1, true);
QString xml;
QXmlStreamWriter writer(&xml);
writer.writeStartDocument();
writer.writeStartElement(QStringLiteral("custom"));
footage.SaveCustom(&writer);
writer.writeEndElement();
writer.writeEndDocument();
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("pcrf=\"30\"")));
EXPECT_TRUE(xml.contains(QStringLiteral("ppreset=\"faster\"")));
EXPECT_TRUE(xml.contains(QStringLiteral("pext=\"mov\"")));
EXPECT_TRUE(xml.contains(QStringLiteral("paudio=\"0\"")));
QXmlStreamReader reader(xml);
ASSERT_TRUE(reader.readNextStartElement());
ASSERT_EQ(reader.name(), QStringLiteral("custom"));
olive::Footage loaded;
ASSERT_TRUE(loaded.LoadCustom(&reader, nullptr));
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().crf, 30);
EXPECT_EQ(loaded.custom_proxy_params().preset, QStringLiteral("faster"));
EXPECT_EQ(loaded.custom_proxy_params().extension, QStringLiteral("mov"));
EXPECT_FALSE(loaded.custom_proxy_params().include_audio);
}
TEST(ProxyManager, FootageWithoutCustomParamsOmitsThemFromXml)
{
olive::Footage footage;
footage.SetProxy(QStringLiteral("/cache/proxy/example.mp4"),
olive::ProxyManager::kProxyReady, 0, 1, true);
QString xml;
QXmlStreamWriter writer(&xml);
writer.writeStartDocument();
writer.writeStartElement(QStringLiteral("custom"));
footage.SaveCustom(&writer);
writer.writeEndElement();
writer.writeEndDocument();
EXPECT_FALSE(xml.contains(QStringLiteral("custom=")));
// Loading old project files without custom params must not enable them
QXmlStreamReader reader(xml);
ASSERT_TRUE(reader.readNextStartElement());
olive::Footage loaded;
ASSERT_TRUE(loaded.LoadCustom(&reader, nullptr));
EXPECT_FALSE(loaded.has_custom_proxy_params());
}
TEST(ProxyManager, FootageEffectiveProxyParams)
{
olive::Footage footage;
// Without custom params, the global config values apply
const olive::ProxyManager::ProxyParams global_params =
footage.GetEffectiveProxyParams();
EXPECT_EQ(global_params.width, ProxyConfigValue("ProxyWidth").value<int>());
EXPECT_EQ(global_params.include_audio,
ProxyConfigValue("ProxyIncludeAudio").toBool());
// Custom params take precedence
olive::ProxyManager::ProxyParams custom;
custom.width = 320;
custom.height = 180;
footage.SetCustomProxyParams(custom);
EXPECT_TRUE(footage.has_custom_proxy_params());
EXPECT_EQ(footage.GetEffectiveProxyParams().width, 320);
EXPECT_EQ(footage.GetEffectiveProxyParams().height, 180);
// Clearing reverts to the global config values
footage.ClearCustomProxyParams();
EXPECT_FALSE(footage.has_custom_proxy_params());
EXPECT_EQ(footage.GetEffectiveProxyParams().width,
ProxyConfigValue("ProxyWidth").value<int>());
}