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:
@@ -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,
|
||||
|
||||
@@ -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 ¶ms);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 ¶ms)
|
||||
{
|
||||
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 ¶ms)
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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 ¶ms);
|
||||
|
||||
/**
|
||||
* @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 ¶ms);
|
||||
static QString DescribeAudioStream(const AudioParams ¶ms);
|
||||
static QString DescribeSubtitleStream(const SubtitleParams ¶ms);
|
||||
@@ -283,6 +313,10 @@ private:
|
||||
|
||||
int proxy_preset_version_;
|
||||
|
||||
bool has_custom_proxy_params_;
|
||||
|
||||
ProxyManager::ProxyParams custom_proxy_params_;
|
||||
|
||||
bool valid_;
|
||||
|
||||
CancelAtom *cancelled_;
|
||||
|
||||
@@ -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
@@ -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 ¶ms,
|
||||
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);
|
||||
|
||||
@@ -32,6 +32,18 @@ public:
|
||||
const ProxyManager::ProxyParams ¶ms,
|
||||
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 ¶ms,
|
||||
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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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>());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user