render: properly defined "out of bounds time" behavior with footage

If a user extends a clip beyond the footage's length, they can
choose whether they want a black frame, the last frame continued,
or for the footage to loop infinitely (same for extending earlier
than the start of footage).
This commit is contained in:
itsmattkc
2021-04-23 14:49:21 +10:00
parent 7f93ed7fc0
commit 1fca3e1767
9 changed files with 162 additions and 37 deletions
+56 -11
View File
@@ -25,6 +25,7 @@
#include <QStandardPaths>
#include "codec/decoder.h"
#include "common/clamp.h"
#include "common/filefunctions.h"
#include "common/xmlutils.h"
#include "config/config.h"
@@ -36,6 +37,7 @@
namespace olive {
const QString Footage::kFilenameInput = QStringLiteral("file_in");
const QString Footage::kLoopModeInput = QStringLiteral("loop_in");
#define super ViewerOutput
@@ -46,6 +48,9 @@ Footage::Footage(const QString &filename) :
SetCacheTextures(true);
SetViewerVideoCacheEnabled(false);
PrependInput(kLoopModeInput, NodeValue::kCombo, 0, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable));
IgnoreHashingFrom(kLoopModeInput);
PrependInput(kFilenameInput, NodeValue::kFile, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable));
Clear();
@@ -58,12 +63,14 @@ void Footage::Retranslate()
super::Retranslate();
SetInputName(kFilenameInput, tr("Filename"));
SetInputName(kLoopModeInput, tr("Loop Mode"));
SetComboBoxStrings(kLoopModeInput, {tr("None"), tr("Loop"), tr("Clamp")});
}
QVector<QString> Footage::inputs_for_output(const QString &output) const
{
Q_UNUSED(output)
return {kFilenameInput};
return {kFilenameInput, kLoopModeInput};
}
bool Footage::LoadCustom(QXmlStreamReader *reader, XMLNodeData &xml_node_data, uint version, const QAtomicInt* cancelled)
@@ -226,6 +233,11 @@ void Footage::SetValid()
valid_ = true;
}
Footage::LoopMode Footage::loop_mode() const
{
return static_cast<LoopMode>(GetStandardValue(kLoopModeInput).toInt());
}
QString Footage::filename() const
{
return GetStandardValue(kFilenameInput).toString();
@@ -333,10 +345,14 @@ void Footage::Hash(const QString& output, QCryptographicHash &hash, const ration
// Footage timestamp
if (params.video_type() != VideoParams::kVideoTypeStill) {
int64_t video_ts = Timecode::time_to_timestamp(time, params.time_base());
rational adjusted_time = AdjustTimeByLoopMode(time, loop_mode(), GetLength());
// Add timestamp in units of the video stream's timebase
hash.addData(reinterpret_cast<const char*>(&video_ts), sizeof(video_ts));
if (!adjusted_time.isNaN()) {
int64_t video_ts = Timecode::time_to_timestamp(adjusted_time, params.time_base());
// Add timestamp in units of the video stream's timebase
hash.addData(reinterpret_cast<const char*>(&video_ts), sizeof(video_ts));
}
// Add start time - used for both image sequences and video streams
auto start_time = params.start_time();
@@ -354,32 +370,29 @@ NodeValueTable Footage::Value(const QString &output, NodeValueDatabase &value) c
// Pop filename from table
QString file = value[kFilenameInput].Take(NodeValue::kFile).toString();
LoopMode loop_mode = static_cast<LoopMode>(value[kLoopModeInput].Take(NodeValue::kCombo).toInt());
// Merge table
NodeValueTable table = value.Merge();
// If the file exists and the reference is valid, push a footage job to the renderer
if (QFileInfo(file).exists()) {
FootageJob job(decoder_, filename(), ref.type());
rational length;
FootageJob job(decoder_, filename(), ref.type(), GetLength(), loop_mode);
if (ref.type() == Track::kVideo) {
VideoParams vp = GetVideoParams(ref.index());
// Ensure the colorspace is valid and not empty
vp.set_colorspace(GetColorspaceToUse(vp));
length = Timecode::timestamp_to_time(vp.duration(), vp.time_base());
job.set_video_params(vp);
} else {
AudioParams ap = GetAudioParams(ref.index());
job.set_audio_params(ap);
job.set_cache_path(project()->cache_path());
length = Timecode::timestamp_to_time(ap.duration(), ap.time_base());
}
table.Push(NodeValue::kRational, QVariant::fromValue(length), this, false, QStringLiteral("length"));
table.Push(NodeValue::kRational, QVariant::fromValue(GetLength()), this, false, QStringLiteral("length"));
table.Push(NodeValue::kFootageJob, QVariant::fromValue(job), this);
}
@@ -423,6 +436,38 @@ NodeOutput Footage::GetConnectedSampleOutput()
}
}
bool TimeIsOutOfBounds(const rational& time, const rational& length)
{
return time < 0 || time >= length;
}
rational Footage::AdjustTimeByLoopMode(rational time, Footage::LoopMode loop_mode, const rational &length)
{
if (TimeIsOutOfBounds(time, length)) {
switch (loop_mode) {
case kLoopModeOff:
// Return no time to indicate no frame should be shown here
time = rational::NaN;
break;
case kLoopModeClamp:
// Clamp footage time to length
time = clamp(time, rational(0), length);
break;
case kLoopModeLoop:
// Loop footage time around job length
do {
if (time >= length) {
time -= length;
} else {
time += length;
}
} while (TimeIsOutOfBounds(time, length));
}
}
return time;
}
void Footage::UpdateTooltip()
{
if (valid_) {
+14
View File
@@ -44,6 +44,12 @@ class Footage : public ViewerOutput
{
Q_OBJECT
public:
enum LoopMode {
kLoopModeOff,
kLoopModeLoop,
kLoopModeClamp
};
/**
* @brief Footage Constructor
*/
@@ -102,6 +108,11 @@ public:
*/
void SetValid();
/**
* @brief Get currently set loop mode
*/
LoopMode loop_mode() const;
/**
* @brief Return the current filename of this Footage object
*/
@@ -180,7 +191,10 @@ public:
virtual NodeOutput GetConnectedSampleOutput() override;
static rational AdjustTimeByLoopMode(rational time, LoopMode loop_mode, const rational& length);
static const QString kFilenameInput;
static const QString kLoopModeInput;
protected:
/**
+7 -3
View File
@@ -285,10 +285,14 @@ void NodeTraverser::PostProcessTable(const Node *node, const QString& output, co
FootageJob job = v.data().value<FootageJob>();
if (job.type() == Track::kVideo) {
QVariant value = ProcessVideoFootage(job, range.in());
rational footage_time = Footage::AdjustTimeByLoopMode(range.in(), job.loop_mode(), job.length());
if (!value.isNull()) {
output_params.Push(NodeValue::kTexture, value, node);
if (!footage_time.isNaN()) {
QVariant value = ProcessVideoFootage(job, footage_time);
if (!value.isNull()) {
output_params.Push(NodeValue::kTexture, value, node);
}
}
}
}