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
+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_;