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:
@@ -38,8 +38,8 @@ bool proxy_params_equal(const ProxyManager::ProxyParams &a,
|
|||||||
const ProxyManager::ProxyParams &b)
|
const ProxyManager::ProxyParams &b)
|
||||||
{
|
{
|
||||||
return a.width == b.width && a.height == b.height &&
|
return a.width == b.width && a.height == b.height &&
|
||||||
a.version == b.version && a.extension == b.extension &&
|
a.divider == b.divider && a.version == b.version &&
|
||||||
a.crf == b.crf && a.preset == b.preset &&
|
a.extension == b.extension && a.crf == b.crf && a.preset == b.preset &&
|
||||||
a.include_audio == b.include_audio;
|
a.include_audio == b.include_audio;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,11 +56,23 @@ QString ProxyManager::get_proxy_filename(const QString &cache_path,
|
|||||||
const QString proxy_dir = get_proxy_directory(cache_path);
|
const QString proxy_dir = get_proxy_directory(cache_path);
|
||||||
const QString extension =
|
const QString extension =
|
||||||
params.extension.isEmpty() ? QStringLiteral("mp4") : params.extension;
|
params.extension.isEmpty() ? QStringLiteral("mp4") : params.extension;
|
||||||
|
|
||||||
|
// Divider mode scales relative to the source, so the tag names the
|
||||||
|
// divider rather than an absolute target size
|
||||||
|
QString size_tag;
|
||||||
|
if (params.divider > 1) {
|
||||||
|
size_tag = QStringLiteral("div%1").arg(QString::number(params.divider));
|
||||||
|
} else {
|
||||||
|
size_tag = QStringLiteral("%1x%2")
|
||||||
|
.arg(QString::number(params.width),
|
||||||
|
QString::number(params.height));
|
||||||
|
}
|
||||||
|
|
||||||
const QString filename =
|
const QString filename =
|
||||||
QStringLiteral("%1-%2.%3x%4.v%5.a%6.%7")
|
QStringLiteral("%1-%2.%3.v%4.a%5.%6")
|
||||||
.arg(FileFunctions::get_unique_file_identifier(source_filename),
|
.arg(FileFunctions::get_unique_file_identifier(source_filename),
|
||||||
QString::number(stream_index), QString::number(params.width),
|
QString::number(stream_index), size_tag,
|
||||||
QString::number(params.height), QString::number(params.version),
|
QString::number(params.version),
|
||||||
params.include_audio ? QStringLiteral("1") : QStringLiteral("0"),
|
params.include_audio ? QStringLiteral("1") : QStringLiteral("0"),
|
||||||
extension);
|
extension);
|
||||||
|
|
||||||
@@ -133,6 +145,7 @@ ProxyManager::ProxyParams ProxyManager::proxy_params_from_config()
|
|||||||
ProxyParams params;
|
ProxyParams params;
|
||||||
params.width = OAK_CONFIG("ProxyWidth").value<int>();
|
params.width = OAK_CONFIG("ProxyWidth").value<int>();
|
||||||
params.height = OAK_CONFIG("ProxyHeight").value<int>();
|
params.height = OAK_CONFIG("ProxyHeight").value<int>();
|
||||||
|
params.divider = OAK_CONFIG("ProxyDivider").value<int>();
|
||||||
params.crf = OAK_CONFIG("ProxyCRF").value<int>();
|
params.crf = OAK_CONFIG("ProxyCRF").value<int>();
|
||||||
params.preset = OAK_CONFIG("ProxyPreset").toString();
|
params.preset = OAK_CONFIG("ProxyPreset").toString();
|
||||||
params.include_audio = OAK_CONFIG("ProxyIncludeAudio").toBool();
|
params.include_audio = OAK_CONFIG("ProxyIncludeAudio").toBool();
|
||||||
|
|||||||
@@ -62,6 +62,11 @@ public:
|
|||||||
struct ProxyParams {
|
struct ProxyParams {
|
||||||
int width = 1280;
|
int width = 1280;
|
||||||
int height = 720;
|
int height = 720;
|
||||||
|
/**
|
||||||
|
* @brief Source resolution divider (1 = use absolute width/height,
|
||||||
|
* 2/4/8 = fraction of the source resolution)
|
||||||
|
*/
|
||||||
|
int divider = 1;
|
||||||
int version = 1;
|
int version = 1;
|
||||||
QString extension = QStringLiteral("mp4");
|
QString extension = QStringLiteral("mp4");
|
||||||
int crf = 23;
|
int crf = 23;
|
||||||
|
|||||||
@@ -232,11 +232,14 @@ void Config::set_defaults()
|
|||||||
|
|
||||||
set_entry_internal(QStringLiteral("ProxyWidth"), NodeValue::k_int, 1280);
|
set_entry_internal(QStringLiteral("ProxyWidth"), NodeValue::k_int, 1280);
|
||||||
set_entry_internal(QStringLiteral("ProxyHeight"), NodeValue::k_int, 720);
|
set_entry_internal(QStringLiteral("ProxyHeight"), NodeValue::k_int, 720);
|
||||||
|
set_entry_internal(QStringLiteral("ProxyDivider"), NodeValue::k_int, 1);
|
||||||
set_entry_internal(QStringLiteral("ProxyCRF"), NodeValue::k_int, 23);
|
set_entry_internal(QStringLiteral("ProxyCRF"), NodeValue::k_int, 23);
|
||||||
set_entry_internal(QStringLiteral("ProxyPreset"), NodeValue::k_text,
|
set_entry_internal(QStringLiteral("ProxyPreset"), NodeValue::k_text,
|
||||||
QStringLiteral("veryfast"));
|
QStringLiteral("veryfast"));
|
||||||
set_entry_internal(QStringLiteral("ProxyIncludeAudio"), NodeValue::k_boolean,
|
set_entry_internal(QStringLiteral("ProxyIncludeAudio"), NodeValue::k_boolean,
|
||||||
true);
|
true);
|
||||||
|
set_entry_internal(QStringLiteral("UseProxyMedia"), NodeValue::k_boolean,
|
||||||
|
true);
|
||||||
set_entry_internal(QStringLiteral("FFmpegPath"), NodeValue::k_text,
|
set_entry_internal(QStringLiteral("FFmpegPath"), NodeValue::k_text,
|
||||||
QString());
|
QString());
|
||||||
|
|
||||||
|
|||||||
@@ -448,6 +448,20 @@ void Core::set_snapping(const bool &b)
|
|||||||
emit snapping_changed(snapping_);
|
emit snapping_changed(snapping_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Core::set_use_proxy_media(bool enabled)
|
||||||
|
{
|
||||||
|
Config::current()[QStringLiteral("UseProxyMedia")] = enabled;
|
||||||
|
|
||||||
|
// Invalidate all footage so viewers re-evaluate with the new proxy state
|
||||||
|
if (open_project_) {
|
||||||
|
for (Node *n : open_project_->nodes()) {
|
||||||
|
if (Footage *footage = dynamic_cast<Footage *>(n)) {
|
||||||
|
footage->invalidate_all(Footage::k_filename_input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Core::dialog_about_show()
|
void Core::dialog_about_show()
|
||||||
{
|
{
|
||||||
AboutDialog a(false, main_window_);
|
AboutDialog a(false, main_window_);
|
||||||
|
|||||||
@@ -337,6 +337,15 @@ public slots:
|
|||||||
*/
|
*/
|
||||||
void set_snapping(const bool &b);
|
void set_snapping(const bool &b);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Globally enable or disable decoding from proxy media
|
||||||
|
*
|
||||||
|
* When disabled, all footage decodes from its original media regardless of
|
||||||
|
* each footage's individual proxy setting. The per-footage settings are
|
||||||
|
* preserved and take effect again when this is re-enabled.
|
||||||
|
*/
|
||||||
|
void set_use_proxy_media(bool enabled);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Show an About dialog
|
* @brief Show an About dialog
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -77,6 +77,20 @@ ProxyDialog::ProxyDialog(QWidget *parent, const QVector<Footage *> &footage)
|
|||||||
|
|
||||||
int row = 0;
|
int row = 0;
|
||||||
|
|
||||||
|
settings_layout->addWidget(new QLabel(tr("Proxy Resolution:")), row, 0);
|
||||||
|
resolution_combo_ = new QComboBox();
|
||||||
|
resolution_combo_->addItem(tr("Custom size"), 1);
|
||||||
|
resolution_combo_->addItem(tr("1/2 of source"), 2);
|
||||||
|
resolution_combo_->addItem(tr("1/4 of source"), 4);
|
||||||
|
resolution_combo_->addItem(tr("1/8 of source"), 8);
|
||||||
|
const int divider_index = resolution_combo_->findData(params.divider);
|
||||||
|
if (divider_index >= 0) {
|
||||||
|
resolution_combo_->setCurrentIndex(divider_index);
|
||||||
|
}
|
||||||
|
settings_layout->addWidget(resolution_combo_, row, 1, 1, 3);
|
||||||
|
|
||||||
|
row++;
|
||||||
|
|
||||||
settings_layout->addWidget(new QLabel(tr("Proxy Width:")), row, 0);
|
settings_layout->addWidget(new QLabel(tr("Proxy Width:")), row, 0);
|
||||||
width_slider_ = new IntegerSlider();
|
width_slider_ = new IntegerSlider();
|
||||||
width_slider_->set_minimum(160);
|
width_slider_->set_minimum(160);
|
||||||
@@ -91,6 +105,16 @@ ProxyDialog::ProxyDialog(QWidget *parent, const QVector<Footage *> &footage)
|
|||||||
height_slider_->set_value(params.height);
|
height_slider_->set_value(params.height);
|
||||||
settings_layout->addWidget(height_slider_, row, 3);
|
settings_layout->addWidget(height_slider_, row, 3);
|
||||||
|
|
||||||
|
// Absolute size only applies in "Custom size" mode
|
||||||
|
const auto update_size_sliders_enabled = [this]() {
|
||||||
|
const bool custom_size = resolution_combo_->currentData().toInt() == 1;
|
||||||
|
width_slider_->setEnabled(custom_size);
|
||||||
|
height_slider_->setEnabled(custom_size);
|
||||||
|
};
|
||||||
|
connect(resolution_combo_, &QComboBox::currentIndexChanged, this,
|
||||||
|
update_size_sliders_enabled);
|
||||||
|
update_size_sliders_enabled();
|
||||||
|
|
||||||
row++;
|
row++;
|
||||||
|
|
||||||
settings_layout->addWidget(new QLabel(tr("Proxy CRF:")), row, 0);
|
settings_layout->addWidget(new QLabel(tr("Proxy CRF:")), row, 0);
|
||||||
@@ -182,6 +206,11 @@ int ProxyDialog::proxy_height() const
|
|||||||
return static_cast<int>(height_slider_->get_value());
|
return static_cast<int>(height_slider_->get_value());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int ProxyDialog::proxy_divider() const
|
||||||
|
{
|
||||||
|
return resolution_combo_->currentData().toInt();
|
||||||
|
}
|
||||||
|
|
||||||
int ProxyDialog::proxy_crf() const
|
int ProxyDialog::proxy_crf() const
|
||||||
{
|
{
|
||||||
return static_cast<int>(crf_slider_->get_value());
|
return static_cast<int>(crf_slider_->get_value());
|
||||||
@@ -212,6 +241,14 @@ void ProxyDialog::set_proxy_height(int height)
|
|||||||
height_slider_->set_value(height);
|
height_slider_->set_value(height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ProxyDialog::set_proxy_divider(int divider)
|
||||||
|
{
|
||||||
|
const int index = resolution_combo_->findData(divider);
|
||||||
|
if (index >= 0) {
|
||||||
|
resolution_combo_->setCurrentIndex(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ProxyDialog::set_proxy_crf(int crf)
|
void ProxyDialog::set_proxy_crf(int crf)
|
||||||
{
|
{
|
||||||
crf_slider_->set_value(crf);
|
crf_slider_->set_value(crf);
|
||||||
@@ -237,6 +274,7 @@ ProxyManager::ProxyParams ProxyDialog::current_params() const
|
|||||||
ProxyManager::ProxyParams params = ProxyManager::proxy_params_from_config();
|
ProxyManager::ProxyParams params = ProxyManager::proxy_params_from_config();
|
||||||
params.width = static_cast<int>(width_slider_->get_value());
|
params.width = static_cast<int>(width_slider_->get_value());
|
||||||
params.height = static_cast<int>(height_slider_->get_value());
|
params.height = static_cast<int>(height_slider_->get_value());
|
||||||
|
params.divider = resolution_combo_->currentData().toInt();
|
||||||
params.crf = static_cast<int>(crf_slider_->get_value());
|
params.crf = static_cast<int>(crf_slider_->get_value());
|
||||||
params.preset = preset_combo_->currentText();
|
params.preset = preset_combo_->currentText();
|
||||||
params.include_audio = include_audio_checkbox_->isChecked();
|
params.include_audio = include_audio_checkbox_->isChecked();
|
||||||
@@ -247,6 +285,7 @@ void ProxyDialog::save_global_settings()
|
|||||||
{
|
{
|
||||||
OAK_CONFIG("ProxyWidth") = static_cast<int>(width_slider_->get_value());
|
OAK_CONFIG("ProxyWidth") = static_cast<int>(width_slider_->get_value());
|
||||||
OAK_CONFIG("ProxyHeight") = static_cast<int>(height_slider_->get_value());
|
OAK_CONFIG("ProxyHeight") = static_cast<int>(height_slider_->get_value());
|
||||||
|
OAK_CONFIG("ProxyDivider") = resolution_combo_->currentData().toInt();
|
||||||
OAK_CONFIG("ProxyCRF") = static_cast<int>(crf_slider_->get_value());
|
OAK_CONFIG("ProxyCRF") = static_cast<int>(crf_slider_->get_value());
|
||||||
OAK_CONFIG("ProxyPreset") = preset_combo_->currentText();
|
OAK_CONFIG("ProxyPreset") = preset_combo_->currentText();
|
||||||
OAK_CONFIG("ProxyIncludeAudio") = include_audio_checkbox_->isChecked();
|
OAK_CONFIG("ProxyIncludeAudio") = include_audio_checkbox_->isChecked();
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ public:
|
|||||||
|
|
||||||
int proxy_height() const;
|
int proxy_height() const;
|
||||||
|
|
||||||
|
int proxy_divider() const;
|
||||||
|
|
||||||
int proxy_crf() const;
|
int proxy_crf() const;
|
||||||
|
|
||||||
QString proxy_preset() const;
|
QString proxy_preset() const;
|
||||||
@@ -55,6 +57,8 @@ public:
|
|||||||
|
|
||||||
void set_proxy_height(int height);
|
void set_proxy_height(int height);
|
||||||
|
|
||||||
|
void set_proxy_divider(int divider);
|
||||||
|
|
||||||
void set_proxy_crf(int crf);
|
void set_proxy_crf(int crf);
|
||||||
|
|
||||||
void set_proxy_preset(const QString &preset);
|
void set_proxy_preset(const QString &preset);
|
||||||
@@ -80,6 +84,8 @@ private:
|
|||||||
|
|
||||||
IntegerSlider *height_slider_;
|
IntegerSlider *height_slider_;
|
||||||
|
|
||||||
|
QComboBox *resolution_combo_;
|
||||||
|
|
||||||
IntegerSlider *crf_slider_;
|
IntegerSlider *crf_slider_;
|
||||||
|
|
||||||
QComboBox *preset_combo_;
|
QComboBox *preset_combo_;
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
|
#include <QImage>
|
||||||
|
#include <QPainter>
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
|
|
||||||
#include "codec/decoder.h"
|
#include "codec/decoder.h"
|
||||||
@@ -353,6 +355,11 @@ void Footage::value(const NodeValueRow &value, const NodeGlobals &globals,
|
|||||||
// Pop filename from table
|
// Pop filename from table
|
||||||
QString file = value[k_filename_input].to_string();
|
QString file = value[k_filename_input].to_string();
|
||||||
|
|
||||||
|
// Proxies can be globally disabled (Tools > Use Proxy Media) without
|
||||||
|
// losing each footage's individual proxy setting
|
||||||
|
const bool proxies_allowed =
|
||||||
|
Config::current()[QStringLiteral("UseProxyMedia")].toBool();
|
||||||
|
|
||||||
// If the file exists and the reference is valid, push a footage job to the renderer
|
// If the file exists and the reference is valid, push a footage job to the renderer
|
||||||
if (QFileInfo::exists(file)) {
|
if (QFileInfo::exists(file)) {
|
||||||
// Push length
|
// Push length
|
||||||
@@ -368,7 +375,8 @@ void Footage::value(const NodeValueRow &value, const NodeGlobals &globals,
|
|||||||
if (ref.type() == Track::k_video) {
|
if (ref.type() == Track::k_video) {
|
||||||
VideoParams vp = get_video_params(ref.index());
|
VideoParams vp = get_video_params(ref.index());
|
||||||
|
|
||||||
if (proxy_enabled_ && !proxy_path_.isEmpty() &&
|
if (proxies_allowed && proxy_enabled_ &&
|
||||||
|
!proxy_path_.isEmpty() &&
|
||||||
proxy_video_stream_index_ == vp.stream_index() &&
|
proxy_video_stream_index_ == vp.stream_index() &&
|
||||||
ProxyManager::get_proxy_state(proxy_path_) ==
|
ProxyManager::get_proxy_state(proxy_path_) ==
|
||||||
ProxyManager::k_proxy_ready) {
|
ProxyManager::k_proxy_ready) {
|
||||||
@@ -403,7 +411,8 @@ void Footage::value(const NodeValueRow &value, const NodeGlobals &globals,
|
|||||||
|
|
||||||
// Proxies generated with audio contain the video stream at
|
// Proxies generated with audio contain the video stream at
|
||||||
// index 0 followed by all source audio streams in source order
|
// index 0 followed by all source audio streams in source order
|
||||||
if (proxy_enabled_ && !proxy_path_.isEmpty() &&
|
if (proxies_allowed && proxy_enabled_ &&
|
||||||
|
!proxy_path_.isEmpty() &&
|
||||||
ProxyManager::get_proxy_state(proxy_path_) ==
|
ProxyManager::get_proxy_state(proxy_path_) ==
|
||||||
ProxyManager::k_proxy_ready &&
|
ProxyManager::k_proxy_ready &&
|
||||||
ProxyManager::proxy_filename_has_audio(proxy_path_)) {
|
ProxyManager::proxy_filename_has_audio(proxy_path_)) {
|
||||||
@@ -425,9 +434,62 @@ void Footage::value(const NodeValueRow &value, const NodeGlobals &globals,
|
|||||||
ref.to_string());
|
ref.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (!file.isEmpty()) {
|
||||||
|
// Media is offline: push a generated warning frame for each video
|
||||||
|
// stream so missing media is clearly visible in the timeline instead
|
||||||
|
// of a transparent/black hole. generate_frame() draws the slat.
|
||||||
|
for (int i = 0; i < get_total_stream_count(); i++) {
|
||||||
|
Track::Reference ref = get_reference_from_real_index(i);
|
||||||
|
if (ref.type() != Track::k_video) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoParams vp = get_video_params(ref.index());
|
||||||
|
if (!vp.is_valid()) {
|
||||||
|
vp = globals.vparams();
|
||||||
|
}
|
||||||
|
vp.set_format(PixelFormat::u8);
|
||||||
|
vp.set_colorspace(
|
||||||
|
project()->color_manager()->get_default_input_color_space());
|
||||||
|
|
||||||
|
GenerateJob job(value);
|
||||||
|
table->push(NodeValue::k_texture, Texture::job(vp, job), this,
|
||||||
|
ref.to_string());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Footage::generate_frame(FramePtr frame, const GenerateJob &job) const
|
||||||
|
{
|
||||||
|
Q_UNUSED(job)
|
||||||
|
|
||||||
|
QImage img(reinterpret_cast<uchar *>(frame->data()), frame->width(),
|
||||||
|
frame->height(), frame->linesize_bytes(),
|
||||||
|
QImage::Format_RGBA8888_Premultiplied);
|
||||||
|
|
||||||
|
// Dark red slat with diagonal stripes, matching the offline media
|
||||||
|
// warnings of other NLEs
|
||||||
|
img.fill(QColor(60, 0, 0));
|
||||||
|
|
||||||
|
QPainter p(&img);
|
||||||
|
p.setRenderHint(QPainter::Antialiasing);
|
||||||
|
|
||||||
|
p.setPen(QPen(QColor(120, 20, 20), qMax(2, frame->height() / 90)));
|
||||||
|
const int stripe_step = qMax(16, frame->height() / 6);
|
||||||
|
for (int x = -frame->height(); x < frame->width() + frame->height();
|
||||||
|
x += stripe_step) {
|
||||||
|
p.drawLine(x, 0, x + frame->height(), frame->height());
|
||||||
|
}
|
||||||
|
|
||||||
|
QFont font = p.font();
|
||||||
|
font.setPixelSize(qMax(10, frame->height() / 10));
|
||||||
|
font.setBold(true);
|
||||||
|
p.setFont(font);
|
||||||
|
p.setPen(Qt::white);
|
||||||
|
p.drawText(img.rect(), Qt::AlignCenter | Qt::TextWordWrap,
|
||||||
|
tr("Media Offline\n%1").arg(QFileInfo(filename()).fileName()));
|
||||||
|
}
|
||||||
|
|
||||||
QString Footage::get_stream_type_name(Track::Type type)
|
QString Footage::get_stream_type_name(Track::Type type)
|
||||||
{
|
{
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
|||||||
@@ -247,6 +247,9 @@ public:
|
|||||||
virtual void value(const NodeValueRow &value, const NodeGlobals &globals,
|
virtual void value(const NodeValueRow &value, const NodeGlobals &globals,
|
||||||
NodeValueTable *table) const override;
|
NodeValueTable *table) const override;
|
||||||
|
|
||||||
|
virtual void generate_frame(FramePtr frame,
|
||||||
|
const GenerateJob &job) const override;
|
||||||
|
|
||||||
static QString get_stream_type_name(Track::Type type);
|
static QString get_stream_type_name(Track::Type type);
|
||||||
|
|
||||||
virtual Node *get_connected_texture_output() override;
|
virtual Node *get_connected_texture_output() override;
|
||||||
|
|||||||
@@ -22,7 +22,10 @@
|
|||||||
#ifndef OAK_FOOTAGEJOB_H
|
#ifndef OAK_FOOTAGEJOB_H
|
||||||
#define OAK_FOOTAGEJOB_H
|
#define OAK_FOOTAGEJOB_H
|
||||||
|
|
||||||
|
#include <QFileInfo>
|
||||||
|
|
||||||
#include "node/project/footage/footage.h"
|
#include "node/project/footage/footage.h"
|
||||||
|
#include "render/rendermodes.h"
|
||||||
|
|
||||||
namespace olive
|
namespace olive
|
||||||
{
|
{
|
||||||
@@ -85,6 +88,20 @@ public:
|
|||||||
has_proxy_ = !filename.isEmpty();
|
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
|
Track::Type type() const
|
||||||
{
|
{
|
||||||
return type_;
|
return type_;
|
||||||
|
|||||||
@@ -530,10 +530,8 @@ void RenderProcessor::process_video_footage(TexturePtr destination,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool use_proxy =
|
const bool use_proxy = stream->should_use_proxy(
|
||||||
static_cast<RenderMode::Mode>(ticket_->property("mode").toInt()) ==
|
static_cast<RenderMode::Mode>(ticket_->property("mode").toInt()));
|
||||||
RenderMode::k_offline &&
|
|
||||||
stream->has_proxy() && QFileInfo::exists(stream->proxy_filename());
|
|
||||||
const QString decode_filename = use_proxy ? stream->proxy_filename() :
|
const QString decode_filename = use_proxy ? stream->proxy_filename() :
|
||||||
stream->filename();
|
stream->filename();
|
||||||
const QString decoder_id = use_proxy ? stream->proxy_decoder() :
|
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
|
// Mirror the video path: use the proxy (when enabled, ready, and containing
|
||||||
// audio) for offline renders only, never for export
|
// audio) for offline renders only, never for export
|
||||||
const bool use_proxy =
|
const bool use_proxy = stream->should_use_proxy(
|
||||||
static_cast<RenderMode::Mode>(ticket_->property("mode").toInt()) ==
|
static_cast<RenderMode::Mode>(ticket_->property("mode").toInt()));
|
||||||
RenderMode::k_offline &&
|
|
||||||
stream->has_proxy() && QFileInfo::exists(stream->proxy_filename());
|
|
||||||
const QString decode_filename = use_proxy ? stream->proxy_filename() :
|
const QString decode_filename = use_proxy ? stream->proxy_filename() :
|
||||||
stream->filename();
|
stream->filename();
|
||||||
const QString decoder_id = use_proxy ? stream->proxy_decoder() :
|
const QString decoder_id = use_proxy ? stream->proxy_decoder() :
|
||||||
|
|||||||
@@ -137,16 +137,18 @@ DecoderPtr resolve_decoder_from_cache(DecoderCache *decoder_cache,
|
|||||||
}
|
}
|
||||||
|
|
||||||
FramePtr decode_input_frame(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();
|
VideoParams stream_data = input.job.video_params();
|
||||||
QString filename = input.job.filename();
|
QString filename = input.job.filename();
|
||||||
QString decoder_id = input.job.decoder();
|
QString decoder_id = input.job.decoder();
|
||||||
int stream_index = stream_data.stream_index();
|
int stream_index = stream_data.stream_index();
|
||||||
|
|
||||||
// Use generated proxy if one is attached to the job. The Footage node only
|
// Use the generated proxy when this render is allowed to (preview only,
|
||||||
// attaches a proxy when it is enabled, ready, and matches this stream.
|
// never export). The Footage node only attaches a proxy when it is
|
||||||
if (input.job.has_proxy()) {
|
// enabled, ready, and matches this stream.
|
||||||
|
if (input.job.should_use_proxy(mode)) {
|
||||||
filename = input.job.proxy_filename();
|
filename = input.job.proxy_filename();
|
||||||
decoder_id = input.job.proxy_decoder();
|
decoder_id = input.job.proxy_decoder();
|
||||||
stream_index = input.job.proxy_stream_index();
|
stream_index = input.job.proxy_stream_index();
|
||||||
@@ -219,7 +221,8 @@ bool decode_input_frames(DecoderCache *decoder_cache,
|
|||||||
return false;
|
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()) {
|
if (!frame || !frame->is_allocated()) {
|
||||||
frames->clear();
|
frames->clear();
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
+102
-5
@@ -48,16 +48,28 @@ QStringList ProxyTask::build_arguments(const QString &source_filename,
|
|||||||
const ProxyManager::ProxyParams ¶ms,
|
const ProxyManager::ProxyParams ¶ms,
|
||||||
const QString &output_filename)
|
const QString &output_filename)
|
||||||
{
|
{
|
||||||
const QString scale_filter =
|
QString scale_filter;
|
||||||
QStringLiteral("scale=w=%1:h=%2:force_original_aspect_ratio=decrease")
|
if (params.divider > 1) {
|
||||||
.arg(QString::number(params.width),
|
// Fraction of the source resolution, rounded down to even dimensions
|
||||||
QString::number(params.height));
|
// as required by yuv420p
|
||||||
|
scale_filter =
|
||||||
|
QStringLiteral("scale=w=trunc(iw/%1/2)*2:h=trunc(ih/%1/2)*2")
|
||||||
|
.arg(QString::number(params.divider));
|
||||||
|
} else {
|
||||||
|
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 =
|
const QString container_format =
|
||||||
params.extension.isEmpty() ? QStringLiteral("mp4") : params.extension;
|
params.extension.isEmpty() ? QStringLiteral("mp4") : params.extension;
|
||||||
|
|
||||||
QStringList args;
|
QStringList args;
|
||||||
args << QStringLiteral("-y") << QStringLiteral("-i") << source_filename
|
args << QStringLiteral("-y")
|
||||||
|
// Report machine-readable progress on stdout for the task dialog
|
||||||
|
<< QStringLiteral("-nostats") << QStringLiteral("-progress")
|
||||||
|
<< QStringLiteral("pipe:1") << QStringLiteral("-i") << source_filename
|
||||||
// Map the requested video stream first so it is stream 0 in the proxy
|
// Map the requested video stream first so it is stream 0 in the proxy
|
||||||
<< QStringLiteral("-map") << QStringLiteral("0:%1").arg(stream_index);
|
<< QStringLiteral("-map") << QStringLiteral("0:%1").arg(stream_index);
|
||||||
|
|
||||||
@@ -82,6 +94,68 @@ QStringList ProxyTask::build_arguments(const QString &source_filename,
|
|||||||
return args;
|
return args;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double ProxyTask::parse_progress(const QString &line, double duration_seconds)
|
||||||
|
{
|
||||||
|
if (duration_seconds <= 0.0) {
|
||||||
|
return -1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
qint64 out_time_us = -1;
|
||||||
|
if (line.startsWith(QStringLiteral("out_time_us="))) {
|
||||||
|
out_time_us = line.mid(12).toLongLong();
|
||||||
|
} else if (line.startsWith(QStringLiteral("out_time_ms="))) {
|
||||||
|
// Despite the name, ffmpeg reports this value in microseconds
|
||||||
|
out_time_us = line.mid(12).toLongLong();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (out_time_us < 0) {
|
||||||
|
return -1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return qBound(0.0, out_time_us / 1000000.0 / duration_seconds, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Probes the source duration with the ffprobe next to ffmpeg
|
||||||
|
*
|
||||||
|
* Returns 0 when ffprobe is unavailable or the duration cannot be
|
||||||
|
* determined, in which case the task simply reports no intermediate
|
||||||
|
* progress.
|
||||||
|
*/
|
||||||
|
double probe_source_duration_seconds(const QString &ffmpeg_path,
|
||||||
|
const QString &source_filename)
|
||||||
|
{
|
||||||
|
QString ffprobe =
|
||||||
|
QFileInfo(ffmpeg_path).dir().filePath(QStringLiteral("ffprobe"));
|
||||||
|
#if defined(Q_OS_WIN)
|
||||||
|
ffprobe += QStringLiteral(".exe");
|
||||||
|
#endif
|
||||||
|
if (!QFileInfo::exists(ffprobe)) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QProcess probe;
|
||||||
|
probe.start(ffprobe,
|
||||||
|
{ QStringLiteral("-v"), QStringLiteral("error"),
|
||||||
|
QStringLiteral("-show_entries"), QStringLiteral("format=duration"),
|
||||||
|
QStringLiteral("-of"),
|
||||||
|
QStringLiteral("default=noprint_wrappers=1:nokey=1"),
|
||||||
|
source_filename });
|
||||||
|
if (!probe.waitForFinished(10000) || probe.exitCode() != 0) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ok = false;
|
||||||
|
const double duration =
|
||||||
|
QString::fromUtf8(probe.readAllStandardOutput()).trimmed().toDouble(&ok);
|
||||||
|
return ok ? duration : 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
bool ProxyTask::run()
|
bool ProxyTask::run()
|
||||||
{
|
{
|
||||||
const QString ffmpeg = ProxyManager::find_f_fmpeg_executable(
|
const QString ffmpeg = ProxyManager::find_f_fmpeg_executable(
|
||||||
@@ -115,6 +189,10 @@ bool ProxyTask::run()
|
|||||||
process.setProgram(ffmpeg);
|
process.setProgram(ffmpeg);
|
||||||
process.setArguments(args);
|
process.setArguments(args);
|
||||||
process.setProcessChannelMode(QProcess::MergedChannels);
|
process.setProcessChannelMode(QProcess::MergedChannels);
|
||||||
|
|
||||||
|
const double duration_seconds =
|
||||||
|
probe_source_duration_seconds(ffmpeg, source_filename_);
|
||||||
|
|
||||||
process.start();
|
process.start();
|
||||||
|
|
||||||
if (!process.waitForStarted()) {
|
if (!process.waitForStarted()) {
|
||||||
@@ -124,7 +202,25 @@ bool ProxyTask::run()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString progress_buffer;
|
||||||
|
double last_progress = 0.0;
|
||||||
|
|
||||||
|
const auto drain_progress = [&]() {
|
||||||
|
progress_buffer += QString::fromUtf8(process.readAll());
|
||||||
|
int newline = -1;
|
||||||
|
while ((newline = progress_buffer.indexOf(QLatin1Char('\n'))) >= 0) {
|
||||||
|
const QString line = progress_buffer.left(newline).trimmed();
|
||||||
|
progress_buffer.remove(0, newline + 1);
|
||||||
|
const double progress = parse_progress(line, duration_seconds);
|
||||||
|
if (progress >= 0.0 && progress - last_progress > 0.001) {
|
||||||
|
last_progress = progress;
|
||||||
|
emit progress_changed(progress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
while (!process.waitForFinished(100)) {
|
while (!process.waitForFinished(100)) {
|
||||||
|
drain_progress();
|
||||||
if (is_cancelled()) {
|
if (is_cancelled()) {
|
||||||
process.kill();
|
process.kill();
|
||||||
process.waitForFinished();
|
process.waitForFinished();
|
||||||
@@ -133,6 +229,7 @@ bool ProxyTask::run()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
drain_progress();
|
||||||
|
|
||||||
if (process.exitStatus() != QProcess::NormalExit ||
|
if (process.exitStatus() != QProcess::NormalExit ||
|
||||||
process.exitCode() != 0) {
|
process.exitCode() != 0) {
|
||||||
|
|||||||
@@ -44,6 +44,16 @@ public:
|
|||||||
const ProxyManager::ProxyParams ¶ms,
|
const ProxyManager::ProxyParams ¶ms,
|
||||||
const QString &output_filename);
|
const QString &output_filename);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Parses one line of ffmpeg "-progress" output
|
||||||
|
*
|
||||||
|
* Extracted for testability. If the line carries an output timestamp
|
||||||
|
* ("out_time_us=" or "out_time_ms="), returns the progress fraction in
|
||||||
|
* the range [0, 1] against duration_seconds. Returns a negative value
|
||||||
|
* when the line carries no timestamp or duration_seconds is unknown.
|
||||||
|
*/
|
||||||
|
static double parse_progress(const QString &line, double duration_seconds);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual bool run() override;
|
virtual bool run() override;
|
||||||
|
|
||||||
|
|||||||
@@ -361,6 +361,15 @@ MainMenu::MainMenu(MainWindow *parent)
|
|||||||
|
|
||||||
tools_menu_->addSeparator();
|
tools_menu_->addSeparator();
|
||||||
|
|
||||||
|
tools_use_proxy_item_ = new QAction(this);
|
||||||
|
Menu::conform_item(tools_use_proxy_item_, "useproxymedia");
|
||||||
|
tools_use_proxy_item_->setCheckable(true);
|
||||||
|
tools_use_proxy_item_->setChecked(
|
||||||
|
Config::current()[QStringLiteral("UseProxyMedia")].toBool());
|
||||||
|
connect(tools_use_proxy_item_, &QAction::triggered, Core::instance(),
|
||||||
|
&Core::set_use_proxy_media);
|
||||||
|
tools_menu_->addAction(tools_use_proxy_item_);
|
||||||
|
|
||||||
tools_proxy_settings_item_ = new QAction(this);
|
tools_proxy_settings_item_ = new QAction(this);
|
||||||
Menu::conform_item(tools_proxy_settings_item_, "proxysettings");
|
Menu::conform_item(tools_proxy_settings_item_, "proxysettings");
|
||||||
connect(tools_proxy_settings_item_, &QAction::triggered, this, [this]() {
|
connect(tools_proxy_settings_item_, &QAction::triggered, this, [this]() {
|
||||||
@@ -892,6 +901,7 @@ void MainMenu::retranslate()
|
|||||||
tools_add_item_->setText(tr("Add Tool"));
|
tools_add_item_->setText(tr("Add Tool"));
|
||||||
tools_record_item_->setText(tr("Record Tool"));
|
tools_record_item_->setText(tr("Record Tool"));
|
||||||
tools_snapping_item_->setText(tr("Enable Snapping"));
|
tools_snapping_item_->setText(tr("Enable Snapping"));
|
||||||
|
tools_use_proxy_item_->setText(tr("Use Proxy Media"));
|
||||||
tools_proxy_settings_item_->setText(tr("Proxy Settings..."));
|
tools_proxy_settings_item_->setText(tr("Proxy Settings..."));
|
||||||
tools_preferences_item_->setText(tr("Preferences"));
|
tools_preferences_item_->setText(tr("Preferences"));
|
||||||
tools_add_item_menu_->setTitle(tr("Add Tool Item"));
|
tools_add_item_menu_->setTitle(tr("Add Tool Item"));
|
||||||
|
|||||||
@@ -281,6 +281,7 @@ private:
|
|||||||
QAction *tools_add_item_;
|
QAction *tools_add_item_;
|
||||||
QAction *tools_record_item_;
|
QAction *tools_record_item_;
|
||||||
QAction *tools_snapping_item_;
|
QAction *tools_snapping_item_;
|
||||||
|
QAction *tools_use_proxy_item_;
|
||||||
QAction *tools_proxy_settings_item_;
|
QAction *tools_proxy_settings_item_;
|
||||||
QAction *tools_preferences_item_;
|
QAction *tools_preferences_item_;
|
||||||
Menu *tools_add_item_menu_;
|
Menu *tools_add_item_menu_;
|
||||||
|
|||||||
@@ -375,6 +375,8 @@ Default `Node::SaveCustom()` writes nothing. Specific node subclasses may overri
|
|||||||
- `custom`(可选):为 `1` 表示该素材使用独立的自定义代理参数,而不是全局设置。
|
- `custom`(可选):为 `1` 表示该素材使用独立的自定义代理参数,而不是全局设置。
|
||||||
- `pwidth`, `pheight` (optional, requires `custom="1"`): custom proxy dimensions.
|
- `pwidth`, `pheight` (optional, requires `custom="1"`): custom proxy dimensions.
|
||||||
- `pwidth`、`pheight`(可选,需 `custom="1"`):自定义代理分辨率。
|
- `pwidth`、`pheight`(可选,需 `custom="1"`):自定义代理分辨率。
|
||||||
|
- `pdivider` (optional): source resolution divider (`1` = use `pwidth`/`pheight`; `2`/`4`/`8` = fraction of the source resolution). Defaults to `1` when absent.
|
||||||
|
- `pdivider`(可选):源分辨率分频(`1` = 使用 `pwidth`/`pheight`;`2`/`4`/`8` = 源分辨率的几分之一)。缺省时为 `1`。
|
||||||
- `pcrf` (optional): custom x264 CRF value.
|
- `pcrf` (optional): custom x264 CRF value.
|
||||||
- `pcrf`(可选):自定义 x264 CRF 值。
|
- `pcrf`(可选):自定义 x264 CRF 值。
|
||||||
- `ppreset` (optional): custom x264 preset name.
|
- `ppreset` (optional): custom x264 preset name.
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ add_executable(olive-gtest
|
|||||||
project_serializer_test.cpp
|
project_serializer_test.cpp
|
||||||
proxy_manager_test.cpp
|
proxy_manager_test.cpp
|
||||||
proxy_dialog_test.cpp
|
proxy_dialog_test.cpp
|
||||||
|
footage_job_proxy_test.cpp
|
||||||
lut_file_field_test.cpp
|
lut_file_field_test.cpp
|
||||||
node_math_test.cpp
|
node_math_test.cpp
|
||||||
node_undo_test.cpp
|
node_undo_test.cpp
|
||||||
@@ -184,6 +185,12 @@ if (OAK_ENABLE_DYNAMIC_RENDER_BACKEND)
|
|||||||
endif()
|
endif()
|
||||||
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)
|
if (MSVC)
|
||||||
add_test("Olive.gtest" olive-gtest)
|
add_test("Olive.gtest" olive-gtest)
|
||||||
else()
|
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 "common/filefunctions.h"
|
||||||
#include "core.h"
|
#include "core.h"
|
||||||
|
#include "codec/frame.h"
|
||||||
#include "node/color/colormanager/colormanager.h"
|
#include "node/color/colormanager/colormanager.h"
|
||||||
#include "node/globals.h"
|
#include "node/globals.h"
|
||||||
#include "node/project.h"
|
#include "node/project.h"
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
#include "render/diskmanager.h"
|
#include "render/diskmanager.h"
|
||||||
#include "node/project/footage/footagedescription.h"
|
#include "node/project/footage/footagedescription.h"
|
||||||
#include "render/job/footagejob.h"
|
#include "render/job/footagejob.h"
|
||||||
|
#include "render/job/generatejob.h"
|
||||||
#include "render/loopmode.h"
|
#include "render/loopmode.h"
|
||||||
#include "render/texture.h"
|
#include "render/texture.h"
|
||||||
#include "ui/icons/icons.h"
|
#include "ui/icons/icons.h"
|
||||||
@@ -589,6 +591,56 @@ TEST_F(FootageTest, ProxyChangesMarkProjectModifiedAndEmitSignal)
|
|||||||
EXPECT_FALSE(footage->proxy_enabled());
|
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)
|
TEST_F(FootageTest, ReprobeRestoresStreamsFromMetadataCache)
|
||||||
{
|
{
|
||||||
QTemporaryDir dir;
|
QTemporaryDir dir;
|
||||||
|
|||||||
@@ -437,12 +437,76 @@ TEST(ProxyTask, BuildArgumentsDisablesAudioWhenDisabled)
|
|||||||
EXPECT_FALSE(args.contains(QStringLiteral("0:a?")));
|
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)
|
TEST(ProxyManager, FootagePersistsCustomProxyParams)
|
||||||
{
|
{
|
||||||
olive::Footage footage;
|
olive::Footage footage;
|
||||||
olive::ProxyManager::ProxyParams params;
|
olive::ProxyManager::ProxyParams params;
|
||||||
params.width = 640;
|
params.width = 640;
|
||||||
params.height = 360;
|
params.height = 360;
|
||||||
|
params.divider = 4;
|
||||||
params.crf = 30;
|
params.crf = 30;
|
||||||
params.preset = QStringLiteral("faster");
|
params.preset = QStringLiteral("faster");
|
||||||
params.extension = QStringLiteral("mov");
|
params.extension = QStringLiteral("mov");
|
||||||
@@ -462,6 +526,7 @@ TEST(ProxyManager, FootagePersistsCustomProxyParams)
|
|||||||
EXPECT_TRUE(xml.contains(QStringLiteral("custom=\"1\"")));
|
EXPECT_TRUE(xml.contains(QStringLiteral("custom=\"1\"")));
|
||||||
EXPECT_TRUE(xml.contains(QStringLiteral("pwidth=\"640\"")));
|
EXPECT_TRUE(xml.contains(QStringLiteral("pwidth=\"640\"")));
|
||||||
EXPECT_TRUE(xml.contains(QStringLiteral("pheight=\"360\"")));
|
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("pcrf=\"30\"")));
|
||||||
EXPECT_TRUE(xml.contains(QStringLiteral("ppreset=\"faster\"")));
|
EXPECT_TRUE(xml.contains(QStringLiteral("ppreset=\"faster\"")));
|
||||||
EXPECT_TRUE(xml.contains(QStringLiteral("pext=\"mov\"")));
|
EXPECT_TRUE(xml.contains(QStringLiteral("pext=\"mov\"")));
|
||||||
@@ -476,6 +541,7 @@ TEST(ProxyManager, FootagePersistsCustomProxyParams)
|
|||||||
ASSERT_TRUE(loaded.has_custom_proxy_params());
|
ASSERT_TRUE(loaded.has_custom_proxy_params());
|
||||||
EXPECT_EQ(loaded.custom_proxy_params().width, 640);
|
EXPECT_EQ(loaded.custom_proxy_params().width, 640);
|
||||||
EXPECT_EQ(loaded.custom_proxy_params().height, 360);
|
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().crf, 30);
|
||||||
EXPECT_EQ(loaded.custom_proxy_params().preset, QStringLiteral("faster"));
|
EXPECT_EQ(loaded.custom_proxy_params().preset, QStringLiteral("faster"));
|
||||||
EXPECT_EQ(loaded.custom_proxy_params().extension, QStringLiteral("mov"));
|
EXPECT_EQ(loaded.custom_proxy_params().extension, QStringLiteral("mov"));
|
||||||
|
|||||||
Reference in New Issue
Block a user