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
+17
View File
@@ -22,7 +22,10 @@
#ifndef OAK_FOOTAGEJOB_H
#define OAK_FOOTAGEJOB_H
#include <QFileInfo>
#include "node/project/footage/footage.h"
#include "render/rendermodes.h"
namespace olive
{
@@ -85,6 +88,20 @@ public:
has_proxy_ = !filename.isEmpty();
}
/**
* @brief Whether decoding for the given render mode should use the proxy
*
* Proxies are a preview accelerator only: offline (realtime preview)
* renders may decode from them, online (export/master) renders must
* always decode the original media. The proxy file must also still
* exist on disk, otherwise decoding falls back to the original.
*/
bool should_use_proxy(RenderMode::Mode mode) const
{
return mode == RenderMode::k_offline && has_proxy() &&
QFileInfo::exists(proxy_filename_);
}
Track::Type type() const
{
return type_;
+4 -8
View File
@@ -530,10 +530,8 @@ void RenderProcessor::process_video_footage(TexturePtr destination,
return;
}
const bool use_proxy =
static_cast<RenderMode::Mode>(ticket_->property("mode").toInt()) ==
RenderMode::k_offline &&
stream->has_proxy() && QFileInfo::exists(stream->proxy_filename());
const bool use_proxy = stream->should_use_proxy(
static_cast<RenderMode::Mode>(ticket_->property("mode").toInt()));
const QString decode_filename = use_proxy ? stream->proxy_filename() :
stream->filename();
const QString decoder_id = use_proxy ? stream->proxy_decoder() :
@@ -613,10 +611,8 @@ void RenderProcessor::process_audio_footage(SampleBuffer &destination,
// 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::k_offline &&
stream->has_proxy() && QFileInfo::exists(stream->proxy_filename());
const bool use_proxy = stream->should_use_proxy(
static_cast<RenderMode::Mode>(ticket_->property("mode").toInt()));
const QString decode_filename = use_proxy ? stream->proxy_filename() :
stream->filename();
const QString decoder_id = use_proxy ? stream->proxy_decoder() :
+8 -5
View File
@@ -137,16 +137,18 @@ DecoderPtr resolve_decoder_from_cache(DecoderCache *decoder_cache,
}
FramePtr decode_input_frame(DecoderCache *decoder_cache,
const FootageInput &input, CancelAtom *cancel)
const FootageInput &input, CancelAtom *cancel,
RenderMode::Mode mode)
{
VideoParams stream_data = input.job.video_params();
QString filename = input.job.filename();
QString decoder_id = input.job.decoder();
int stream_index = stream_data.stream_index();
// Use generated proxy if one is attached to the job. The Footage node only
// attaches a proxy when it is enabled, ready, and matches this stream.
if (input.job.has_proxy()) {
// Use the generated proxy when this render is allowed to (preview only,
// never export). The Footage node only attaches a proxy when it is
// enabled, ready, and matches this stream.
if (input.job.should_use_proxy(mode)) {
filename = input.job.proxy_filename();
decoder_id = input.job.proxy_decoder();
stream_index = input.job.proxy_stream_index();
@@ -219,7 +221,8 @@ bool decode_input_frames(DecoderCache *decoder_cache,
return false;
}
FramePtr frame = decode_input_frame(decoder_cache, input, cancel);
FramePtr frame =
decode_input_frame(decoder_cache, input, cancel, params.mode);
if (!frame || !frame->is_allocated()) {
frames->clear();
return false;