a first attempt at a new render system
This commit is contained in:
@@ -42,7 +42,7 @@ extern "C" {
|
||||
#include "common/functiontimer.h"
|
||||
#include "common/timecodefunctions.h"
|
||||
#include "ffmpegcommon.h"
|
||||
#include "render/backend/videorenderframecache.h"
|
||||
#include "render/framehashcache.h"
|
||||
#include "render/diskmanager.h"
|
||||
#include "render/pixelformat.h"
|
||||
|
||||
@@ -662,7 +662,7 @@ void SaveCacheFrame(FFmpegDecoder* decoder,
|
||||
&converted_linesize);
|
||||
scaler_lock.unlock();
|
||||
|
||||
if (!VideoRenderFrameCache::SaveCacheFrame(dst_fn, converted_buffer.data(), params)) {
|
||||
if (!FrameHashCache::SaveCacheFrame(dst_fn, converted_buffer.data(), params)) {
|
||||
qCritical() <<" Failed to save cache frame" << dst_fn;
|
||||
}
|
||||
|
||||
@@ -1286,7 +1286,7 @@ QString FFmpegDecoder::GetProxyFrameFilename(const int64_t ×tamp, const int
|
||||
{
|
||||
QString dst_fn = GetProxyFilename(divider);
|
||||
dst_fn.append(QString::number(timestamp));
|
||||
dst_fn.append(VideoRenderFrameCache::GetFormatExtension(native_pix_fmt_));
|
||||
dst_fn.append(FrameHashCache::GetFormatExtension(native_pix_fmt_));
|
||||
return dst_fn;
|
||||
}
|
||||
|
||||
|
||||
+1
-2
@@ -379,7 +379,7 @@ void Core::DialogExportShow()
|
||||
TimeBasedPanel* latest_time_based = PanelManager::instance()->MostRecentlyFocused<TimeBasedPanel>();
|
||||
|
||||
if (latest_time_based && latest_time_based->GetConnectedViewer()) {
|
||||
if (latest_time_based->GetConnectedViewer()->Length() == 0) {
|
||||
if (latest_time_based->GetConnectedViewer()->GetLength() == 0) {
|
||||
QMessageBox::critical(main_window_,
|
||||
tr("Error"),
|
||||
tr("This Sequence is empty. There is nothing to export."),
|
||||
@@ -525,7 +525,6 @@ void Core::ProjectWasModified(bool e)
|
||||
|
||||
void Core::DeclareTypesForQt()
|
||||
{
|
||||
qRegisterMetaType<NodeDependency>();
|
||||
qRegisterMetaType<rational>();
|
||||
qRegisterMetaType<OpenGLTexturePtr>();
|
||||
qRegisterMetaType<OpenGLTextureCache::ReferencePtr>();
|
||||
|
||||
@@ -228,11 +228,6 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) :
|
||||
preview_viewer_->SetColorMenuEnabled(false);
|
||||
preview_viewer_->SetColorTransform(video_tab_->CurrentOCIOColorSpace());
|
||||
|
||||
// Update renderer
|
||||
// FIXME: This is going to be VERY slow since it will need to hash every single frame. It would be better to have a
|
||||
// the renderer save the map as some sort of file that this can load.
|
||||
preview_viewer_->video_renderer()->InvalidateCache(TimeRange(0, viewer_node_->Length()), nullptr);
|
||||
|
||||
progress_timer_.setInterval(1000);
|
||||
connect(&progress_timer_, &QTimer::timeout, this, &ExportDialog::UpdateTimeLabels);
|
||||
}
|
||||
@@ -585,7 +580,7 @@ ExportParams ExportDialog::GenerateParams() const
|
||||
|
||||
ExportParams params;
|
||||
params.SetFilename(filename_edit_->text());
|
||||
params.SetExportLength(viewer_node_->Length());
|
||||
params.SetExportLength(viewer_node_->GetLength());
|
||||
|
||||
if (video_tab_->scaling_method_combobox()->isEnabled()) {
|
||||
params.set_video_scaling_method(static_cast<ExportParams::VideoScalingMethod>(video_tab_->scaling_method_combobox()->currentData().toInt()));
|
||||
|
||||
@@ -24,8 +24,6 @@ add_subdirectory(output)
|
||||
|
||||
set(OLIVE_SOURCES
|
||||
${OLIVE_SOURCES}
|
||||
node/dependency.h
|
||||
node/dependency.cpp
|
||||
node/edge.h
|
||||
node/edge.cpp
|
||||
node/factory.h
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2019 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "dependency.h"
|
||||
|
||||
#include "node.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
NodeDependency::NodeDependency() :
|
||||
node_(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
NodeDependency::NodeDependency(const Node *node, const TimeRange &range) :
|
||||
node_(node),
|
||||
range_(range)
|
||||
{
|
||||
}
|
||||
|
||||
NodeDependency::NodeDependency(const Node *node, const rational &in, const rational &out) :
|
||||
node_(node),
|
||||
range_(in, out)
|
||||
{
|
||||
}
|
||||
|
||||
const Node *NodeDependency::node() const
|
||||
{
|
||||
return node_;
|
||||
}
|
||||
|
||||
const rational& NodeDependency::in() const
|
||||
{
|
||||
return range_.in();
|
||||
}
|
||||
|
||||
const rational &NodeDependency::out() const
|
||||
{
|
||||
return range_.out();
|
||||
}
|
||||
|
||||
const TimeRange &NodeDependency::range() const
|
||||
{
|
||||
return range_;
|
||||
}
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
@@ -55,7 +55,7 @@ void TrackOutput::set_track_type(const Timeline::TrackType &track_type)
|
||||
track_type_ = track_type;
|
||||
}
|
||||
|
||||
const Timeline::TrackType& TrackOutput::track_type()
|
||||
const Timeline::TrackType& TrackOutput::track_type() const
|
||||
{
|
||||
return track_type_;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ class TrackOutput : public Block
|
||||
public:
|
||||
TrackOutput();
|
||||
|
||||
const Timeline::TrackType& track_type();
|
||||
const Timeline::TrackType& track_type() const;
|
||||
void set_track_type(const Timeline::TrackType& track_type);
|
||||
|
||||
virtual Type type() const override;
|
||||
|
||||
@@ -55,6 +55,8 @@ ViewerOutput::ViewerOutput()
|
||||
|
||||
// Create UUID for this node
|
||||
uuid_ = QUuid::createUuid();
|
||||
|
||||
connect(this, &ViewerOutput::LengthChanged, &video_frame_cache_, &FrameHashCache::SetLength);
|
||||
}
|
||||
|
||||
Node *ViewerOutput::copy() const
|
||||
@@ -82,22 +84,16 @@ QString ViewerOutput::Description() const
|
||||
return tr("Interface between a Viewer panel and the node system.");
|
||||
}
|
||||
|
||||
NodeInput *ViewerOutput::texture_input() const
|
||||
{
|
||||
return texture_input_;
|
||||
}
|
||||
|
||||
NodeInput *ViewerOutput::samples_input() const
|
||||
{
|
||||
return samples_input_;
|
||||
}
|
||||
|
||||
void ViewerOutput::InvalidateCache(const TimeRange &range, NodeInput *from, NodeInput *source)
|
||||
{
|
||||
if (from == texture_input()) {
|
||||
emit VideoChangedBetween(range, source);
|
||||
emit GraphChangedFrom(from, source);
|
||||
|
||||
video_frame_cache_.Invalidate(range);
|
||||
} else if (from == samples_input()) {
|
||||
emit AudioChangedBetween(range, source);
|
||||
emit GraphChangedFrom(from, source);
|
||||
|
||||
audio_playback_cache_.Invalidate(range);
|
||||
}
|
||||
|
||||
Node::InvalidateCache(range, from, source);
|
||||
@@ -112,16 +108,6 @@ void ViewerOutput::InvalidateVisible(NodeInput* from, NodeInput *source)
|
||||
Node::InvalidateVisible(from, source);
|
||||
}
|
||||
|
||||
const VideoParams &ViewerOutput::video_params() const
|
||||
{
|
||||
return video_params_;
|
||||
}
|
||||
|
||||
const AudioParams &ViewerOutput::audio_params() const
|
||||
{
|
||||
return audio_params_;
|
||||
}
|
||||
|
||||
void ViewerOutput::set_video_params(const VideoParams &video)
|
||||
{
|
||||
video_params_ = video;
|
||||
@@ -134,34 +120,31 @@ void ViewerOutput::set_video_params(const VideoParams &video)
|
||||
void ViewerOutput::set_audio_params(const AudioParams &audio)
|
||||
{
|
||||
audio_params_ = audio;
|
||||
|
||||
emit AudioParamsChanged();
|
||||
}
|
||||
|
||||
rational ViewerOutput::Length()
|
||||
rational ViewerOutput::GetLength()
|
||||
{
|
||||
NodeTraverser traverser;
|
||||
|
||||
rational video_length;
|
||||
|
||||
if (texture_input_->IsConnected()) {
|
||||
NodeValueTable t = traverser.ProcessNode(NodeDependency(texture_input_->get_connected_node(), 0, 0));
|
||||
NodeValueTable t = traverser.GenerateTable(texture_input_->get_connected_node(), 0, 0);
|
||||
video_length = t.Get(NodeParam::kNumber, "length").value<rational>();
|
||||
}
|
||||
|
||||
rational audio_length;
|
||||
|
||||
if (samples_input_->IsConnected()) {
|
||||
NodeValueTable t = traverser.ProcessNode(NodeDependency(samples_input_->get_connected_node(), 0, 0));
|
||||
NodeValueTable t = traverser.GenerateTable(samples_input_->get_connected_node(), 0, 0);
|
||||
audio_length = t.Get(NodeParam::kNumber, "length").value<rational>();
|
||||
}
|
||||
|
||||
return qMax(video_length, qMax(audio_length, timeline_length_));
|
||||
}
|
||||
|
||||
const QUuid &ViewerOutput::uuid() const
|
||||
{
|
||||
return uuid_;
|
||||
}
|
||||
|
||||
void ViewerOutput::UpdateTrackCache()
|
||||
{
|
||||
track_cache_.clear();
|
||||
@@ -231,11 +214,6 @@ void ViewerOutput::Retranslate()
|
||||
}
|
||||
}
|
||||
|
||||
const QString &ViewerOutput::media_name() const
|
||||
{
|
||||
return media_name_;
|
||||
}
|
||||
|
||||
void ViewerOutput::set_media_name(const QString &name)
|
||||
{
|
||||
media_name_ = name;
|
||||
@@ -243,21 +221,6 @@ void ViewerOutput::set_media_name(const QString &name)
|
||||
emit MediaNameChanged(media_name_);
|
||||
}
|
||||
|
||||
const QVector<TrackOutput *>& ViewerOutput::Tracks() const
|
||||
{
|
||||
return track_cache_;
|
||||
}
|
||||
|
||||
NodeInput *ViewerOutput::track_input(Timeline::TrackType type) const
|
||||
{
|
||||
return track_inputs_.at(type);
|
||||
}
|
||||
|
||||
TrackList *ViewerOutput::track_list(Timeline::TrackType type) const
|
||||
{
|
||||
return track_lists_.at(type);
|
||||
}
|
||||
|
||||
void ViewerOutput::TrackListAddedBlock(Block *block, int index)
|
||||
{
|
||||
Timeline::TrackType type = static_cast<TrackList*>(sender())->type();
|
||||
|
||||
@@ -28,8 +28,10 @@
|
||||
#include "node/output/track/track.h"
|
||||
#include "node/output/track/tracklist.h"
|
||||
#include "node/node.h"
|
||||
#include "render/videoparams.h"
|
||||
#include "render/audioparams.h"
|
||||
#include "render/audioplaybackcache.h"
|
||||
#include "render/framehashcache.h"
|
||||
#include "render/videoparams.h"
|
||||
#include "timeline/trackreference.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
@@ -52,39 +54,66 @@ public:
|
||||
virtual QList<CategoryID> Category() const override;
|
||||
virtual QString Description() const override;
|
||||
|
||||
NodeInput* texture_input() const;
|
||||
NodeInput* samples_input() const;
|
||||
NodeInput* texture_input() const {
|
||||
return texture_input_;
|
||||
}
|
||||
|
||||
NodeInput* samples_input() const {
|
||||
return samples_input_;
|
||||
}
|
||||
|
||||
virtual void InvalidateCache(const TimeRange &range, NodeInput *from, NodeInput* source) override;
|
||||
virtual void InvalidateVisible(NodeInput *from, NodeInput* source) override;
|
||||
|
||||
const VideoParams& video_params() const;
|
||||
const AudioParams& audio_params() const;
|
||||
const VideoParams& video_params() const {
|
||||
return video_params_;
|
||||
}
|
||||
|
||||
const AudioParams& audio_params() const {
|
||||
return audio_params_;
|
||||
}
|
||||
|
||||
void set_video_params(const VideoParams& video);
|
||||
void set_audio_params(const AudioParams& audio);
|
||||
|
||||
rational Length();
|
||||
rational GetLength();
|
||||
|
||||
const QUuid& uuid() const;
|
||||
const QUuid& uuid() const {
|
||||
return uuid_;
|
||||
}
|
||||
|
||||
const QVector<TrackOutput *> &Tracks() const;
|
||||
const QVector<TrackOutput *> &GetTracks() const {
|
||||
return track_cache_;
|
||||
}
|
||||
|
||||
NodeInput* track_input(Timeline::TrackType type) const;
|
||||
NodeInput* track_input(Timeline::TrackType type) const {
|
||||
return track_inputs_.at(type);
|
||||
}
|
||||
|
||||
TrackList* track_list(Timeline::TrackType type) const;
|
||||
TrackList* track_list(Timeline::TrackType type) const {
|
||||
return track_lists_.at(type);
|
||||
}
|
||||
|
||||
virtual void Retranslate() override;
|
||||
|
||||
const QString& media_name() const;
|
||||
const QString& media_name() const {
|
||||
return media_name_;
|
||||
}
|
||||
|
||||
void set_media_name(const QString& name);
|
||||
|
||||
FrameHashCache* video_frame_cache() {
|
||||
return &video_frame_cache_;
|
||||
}
|
||||
|
||||
AudioPlaybackCache* audio_playback_cache() {
|
||||
return &audio_playback_cache_;
|
||||
}
|
||||
|
||||
signals:
|
||||
void TimebaseChanged(const rational&);
|
||||
|
||||
void VideoChangedBetween(const TimeRange& range, NodeInput* source);
|
||||
|
||||
void AudioChangedBetween(const TimeRange& range, NodeInput* source);
|
||||
void GraphChangedFrom(NodeInput* from, NodeInput* source);
|
||||
|
||||
void VisibleInvalidated(NodeInput* source);
|
||||
|
||||
@@ -93,6 +122,7 @@ signals:
|
||||
void SizeChanged(int width, int height);
|
||||
|
||||
void VideoParamsChanged();
|
||||
void AudioParamsChanged();
|
||||
|
||||
void BlockAdded(Block* block, TrackReference track);
|
||||
void BlockRemoved(Block* block);
|
||||
@@ -125,6 +155,10 @@ private:
|
||||
|
||||
QString media_name_;
|
||||
|
||||
FrameHashCache video_frame_cache_;
|
||||
|
||||
AudioPlaybackCache audio_playback_cache_;
|
||||
|
||||
private slots:
|
||||
void UpdateTrackCache();
|
||||
|
||||
|
||||
+25
-27
@@ -24,7 +24,7 @@
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
NodeValueDatabase NodeTraverser::GenerateDatabase(const Node* node, const TimeRange &range)
|
||||
NodeValueDatabase NodeTraverser::GenerateDatabase(const Node* node, const TimeRange &range) const
|
||||
{
|
||||
NodeValueDatabase database;
|
||||
|
||||
@@ -38,7 +38,17 @@ NodeValueDatabase NodeTraverser::GenerateDatabase(const Node* node, const TimeRa
|
||||
|
||||
TimeRange input_time = node->InputTimeAdjustment(input, range);
|
||||
|
||||
NodeValueTable table = ProcessInput(input, input_time);
|
||||
NodeValueTable table;
|
||||
|
||||
if (input->IsConnected()) {
|
||||
// Value will equal something from the connected node, follow it
|
||||
table = GenerateTable(input->get_connected_node(), range);
|
||||
} else {
|
||||
// Push onto the table the value at this time from the input
|
||||
QVariant input_value = input->get_value_at_time(range.in());
|
||||
|
||||
table.Push(input->data_type(), input_value);
|
||||
}
|
||||
|
||||
// Exception for Footage types where we actually retrieve some Footage data from a decoder
|
||||
if (input->data_type() == NodeParam::kFootage) {
|
||||
@@ -63,29 +73,32 @@ NodeValueDatabase NodeTraverser::GenerateDatabase(const Node* node, const TimeRa
|
||||
return database;
|
||||
}
|
||||
|
||||
NodeValueTable NodeTraverser::ProcessNode(const NodeDependency& dep)
|
||||
NodeValueTable NodeTraverser::GenerateTable(const Node *n, const TimeRange& range) const
|
||||
{
|
||||
const Node* node = dep.node();
|
||||
|
||||
if (node->IsTrack()) {
|
||||
if (n->IsTrack()) {
|
||||
// If the range is not wholly contained in this Block, we'll need to do some extra processing
|
||||
return RenderBlock(static_cast<const TrackOutput*>(node), dep.range());
|
||||
return GenerateBlockTable(static_cast<const TrackOutput*>(n), range);
|
||||
}
|
||||
|
||||
// FIXME: Cache certain values here if we've already processed them before
|
||||
|
||||
// Generate database of input values of node
|
||||
NodeValueDatabase database = GenerateDatabase(node, dep.range());
|
||||
NodeValueDatabase database = GenerateDatabase(n, range);
|
||||
|
||||
// By this point, the node should have all the inputs it needs to render correctly
|
||||
NodeValueTable table = node->Value(database);
|
||||
NodeValueTable table = n->Value(database);
|
||||
|
||||
ProcessNodeEvent(node, dep.range(), database, table);
|
||||
ProcessNodeEvent(n, range, database, table);
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
NodeValueTable NodeTraverser::RenderBlock(const TrackOutput *track, const TimeRange &range)
|
||||
NodeValueTable NodeTraverser::GenerateTable(const Node *n, const rational &in, const rational &out) const
|
||||
{
|
||||
return GenerateTable(n, TimeRange(in, out));
|
||||
}
|
||||
|
||||
NodeValueTable NodeTraverser::GenerateBlockTable(const TrackOutput *track, const TimeRange &range) const
|
||||
{
|
||||
// By default, just follow the in point
|
||||
Block* active_block = track->BlockAtTime(range.in());
|
||||
@@ -93,7 +106,7 @@ NodeValueTable NodeTraverser::RenderBlock(const TrackOutput *track, const TimeRa
|
||||
NodeValueTable table;
|
||||
|
||||
if (active_block) {
|
||||
table = ProcessNode(NodeDependency(active_block, range));
|
||||
table = GenerateTable(active_block, range);
|
||||
}
|
||||
|
||||
return table;
|
||||
@@ -104,19 +117,4 @@ StreamPtr NodeTraverser::ResolveStreamFromInput(NodeInput *input)
|
||||
return input->get_standard_value().value<StreamPtr>();
|
||||
}
|
||||
|
||||
NodeValueTable NodeTraverser::ProcessInput(const NodeInput *input, const TimeRange& range)
|
||||
{
|
||||
if (input->IsConnected()) {
|
||||
// Value will equal something from the connected node, follow it
|
||||
return ProcessNode(NodeDependency(input->get_connected_node(), range));
|
||||
} else {
|
||||
// Push onto the table the value at this time from the input
|
||||
QVariant input_value = input->get_value_at_time(range.in());
|
||||
|
||||
NodeValueTable table;
|
||||
table.Push(input->data_type(), input_value);
|
||||
return table;
|
||||
}
|
||||
}
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
|
||||
+10
-9
@@ -23,7 +23,6 @@
|
||||
|
||||
#include "codec/decoder.h"
|
||||
#include "common/cancelableobject.h"
|
||||
#include "dependency.h"
|
||||
#include "node/output/track/track.h"
|
||||
#include "project/item/footage/stream.h"
|
||||
#include "value.h"
|
||||
@@ -35,21 +34,23 @@ class NodeTraverser : public CancelableObject
|
||||
public:
|
||||
NodeTraverser() = default;
|
||||
|
||||
NodeValueTable ProcessNode(const NodeDependency &dep);
|
||||
NodeValueTable GenerateTable(const Node *n, const TimeRange &range) const;
|
||||
NodeValueTable GenerateTable(const Node *n, const rational &in, const rational& out) const;
|
||||
|
||||
NodeValueDatabase GenerateDatabase(const Node *node, const TimeRange &range);
|
||||
NodeValueDatabase GenerateDatabase(const Node *node, const TimeRange &range) const;
|
||||
|
||||
protected:
|
||||
virtual NodeValueTable RenderBlock(const TrackOutput *track, const TimeRange& range);
|
||||
virtual NodeValueTable GenerateBlockTable(const TrackOutput *track, const TimeRange& range) const;
|
||||
|
||||
NodeValueTable ProcessInput(const NodeInput* input, const TimeRange &range);
|
||||
virtual void FootageProcessingEvent(StreamPtr, const TimeRange&, NodeValueTable*) const {}
|
||||
|
||||
virtual void FootageProcessingEvent(StreamPtr, const TimeRange&, NodeValueTable*){}
|
||||
|
||||
virtual void ProcessNodeEvent(const Node*, const TimeRange&, NodeValueDatabase&, NodeValueTable&){}
|
||||
virtual void ProcessNodeEvent(const Node*,
|
||||
const TimeRange&,
|
||||
NodeValueDatabase&,
|
||||
NodeValueTable&) const {}
|
||||
|
||||
private:
|
||||
StreamPtr ResolveStreamFromInput(NodeInput* input);
|
||||
static StreamPtr ResolveStreamFromInput(NodeInput* input);
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -72,11 +72,6 @@ void ViewerPanelBase::DisconnectTimeBasedPanel(TimeBasedPanel *panel)
|
||||
disconnect(panel, &TimeBasedPanel::ShuttleRightRequested, this, &ViewerPanelBase::ShuttleRight);
|
||||
}
|
||||
|
||||
VideoRenderBackend *ViewerPanelBase::video_renderer() const
|
||||
{
|
||||
return static_cast<ViewerWidget*>(GetTimeBasedWidget())->video_renderer();
|
||||
}
|
||||
|
||||
void ViewerPanelBase::ConnectPixelSamplerPanel(PixelSamplerPanel *psp)
|
||||
{
|
||||
ViewerWidget* vw = static_cast<ViewerWidget*>(GetTimeBasedWidget());
|
||||
|
||||
@@ -47,8 +47,6 @@ public:
|
||||
|
||||
void DisconnectTimeBasedPanel(TimeBasedPanel* panel);
|
||||
|
||||
VideoRenderBackend* video_renderer() const;
|
||||
|
||||
void ConnectPixelSamplerPanel(PixelSamplerPanel *psp);
|
||||
|
||||
/**
|
||||
|
||||
@@ -193,7 +193,7 @@ QIcon Sequence::icon()
|
||||
|
||||
QString Sequence::duration()
|
||||
{
|
||||
rational timeline_length = viewer_output_->Length();
|
||||
rational timeline_length = viewer_output_->GetLength();
|
||||
|
||||
int64_t timestamp = Timecode::time_to_timestamp(timeline_length, video_params().time_base());
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ set(OLIVE_SOURCES
|
||||
${OLIVE_SOURCES}
|
||||
render/audioparams.h
|
||||
render/audioparams.cpp
|
||||
render/audioplaybackcache.h
|
||||
render/audioplaybackcache.cpp
|
||||
render/color.h
|
||||
render/color.cpp
|
||||
render/colormanager.h
|
||||
@@ -29,10 +31,14 @@ set(OLIVE_SOURCES
|
||||
render/colorprocessor.cpp
|
||||
render/diskmanager.h
|
||||
render/diskmanager.cpp
|
||||
render/framehashcache.h
|
||||
render/framehashcache.cpp
|
||||
render/managedcolor.h
|
||||
render/managedcolor.cpp
|
||||
render/pixelformat.h
|
||||
render/pixelformat.cpp
|
||||
render/playbackcache.h
|
||||
render/playbackcache.cpp
|
||||
render/rendermodes.h
|
||||
render/videoparams.h
|
||||
render/videoparams.cpp
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2019 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "audioplaybackcache.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QRandomGenerator>
|
||||
|
||||
#include "common/filefunctions.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
AudioPlaybackCache::AudioPlaybackCache()
|
||||
{
|
||||
// FIXME: We just use a randomly generated number, one day this should be matchable to the
|
||||
// project so it can be reused.
|
||||
quint32 r = QRandomGenerator::global()->generate();
|
||||
filename_ = QDir(FileFunctions::GetMediaCacheLocation()).filePath(QString::number(r));
|
||||
filename_.append(QStringLiteral(".pcm"));
|
||||
}
|
||||
|
||||
void AudioPlaybackCache::SetParameters(const AudioRenderingParams ¶ms)
|
||||
{
|
||||
if (params_ == params) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Our current audio cache is unusable, so we truncate it automatically
|
||||
InvalidateAll();
|
||||
|
||||
emit ParametersChanged();
|
||||
}
|
||||
|
||||
const QString &AudioPlaybackCache::GetCacheFilename() const
|
||||
{
|
||||
return filename_;
|
||||
}
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
@@ -18,35 +18,41 @@
|
||||
|
||||
***/
|
||||
|
||||
#ifndef NODEDEPENDENCY_H
|
||||
#define NODEDEPENDENCY_H
|
||||
|
||||
#include <QMetaType>
|
||||
#ifndef AUDIOPLAYBACKCACHE_H
|
||||
#define AUDIOPLAYBACKCACHE_H
|
||||
|
||||
#include "common/timerange.h"
|
||||
#include "codec/samplebuffer.h"
|
||||
#include "render/playbackcache.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
class Node;
|
||||
|
||||
class NodeDependency {
|
||||
class AudioPlaybackCache : public PlaybackCache
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
NodeDependency();
|
||||
NodeDependency(const Node* node, const TimeRange& range);
|
||||
NodeDependency(const Node* node, const rational& in, const rational &out);
|
||||
AudioPlaybackCache();
|
||||
|
||||
const Node* node() const;
|
||||
const rational& in() const;
|
||||
const rational& out() const;
|
||||
const TimeRange& range() const;
|
||||
const AudioRenderingParams& GetParameters() const {
|
||||
return params_;
|
||||
}
|
||||
|
||||
void SetParameters(const AudioRenderingParams& params);
|
||||
|
||||
void WritePCM(SampleBufferPtr samples);
|
||||
|
||||
const QString& GetCacheFilename() const;
|
||||
|
||||
signals:
|
||||
void ParametersChanged();
|
||||
|
||||
private:
|
||||
const Node* node_;
|
||||
TimeRange range_;
|
||||
QString filename_;
|
||||
|
||||
AudioRenderingParams params_;
|
||||
|
||||
};
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
|
||||
Q_DECLARE_METATYPE(OLIVE_NAMESPACE::NodeDependency)
|
||||
|
||||
#endif // NODEDEPENDENCY_H
|
||||
#endif // AUDIOPLAYBACKCACHE_H
|
||||
@@ -14,7 +14,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
add_subdirectory(audio)
|
||||
add_subdirectory(opengl)
|
||||
|
||||
set(OLIVE_SOURCES
|
||||
@@ -27,20 +26,6 @@ set(OLIVE_SOURCES
|
||||
|
||||
render/backend/renderbackend.h
|
||||
render/backend/renderbackend.cpp
|
||||
render/backend/renderworker.h
|
||||
render/backend/renderworker.cpp
|
||||
|
||||
render/backend/audiorenderbackend.h
|
||||
render/backend/audiorenderbackend.cpp
|
||||
render/backend/audiorenderworker.h
|
||||
render/backend/audiorenderworker.cpp
|
||||
|
||||
render/backend/videorenderbackend.h
|
||||
render/backend/videorenderbackend.cpp
|
||||
render/backend/videorenderframecache.h
|
||||
render/backend/videorenderframecache.cpp
|
||||
render/backend/videorenderworker.h
|
||||
render/backend/videorenderworker.cpp
|
||||
|
||||
render/backend/rendercache.h
|
||||
render/backend/colorprocessorcache.h
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# Olive - Non-Linear Video Editor
|
||||
# Copyright (C) 2019 Olive Team
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
set(OLIVE_SOURCES
|
||||
${OLIVE_SOURCES}
|
||||
render/backend/audio/audiobackend.h
|
||||
render/backend/audio/audiobackend.cpp
|
||||
render/backend/audio/audioworker.h
|
||||
render/backend/audio/audioworker.cpp
|
||||
PARENT_SCOPE
|
||||
)
|
||||
@@ -1,116 +0,0 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2019 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "audiobackend.h"
|
||||
|
||||
#include "audioworker.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
AudioBackend::AudioBackend(QObject *parent) :
|
||||
AudioRenderBackend(parent)
|
||||
{
|
||||
}
|
||||
|
||||
AudioBackend::~AudioBackend()
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
bool AudioBackend::InitInternal()
|
||||
{
|
||||
// Initiate one thread per CPU core
|
||||
for (int i=0;i<threads().size();i++) {
|
||||
// Create one processor object for each thread
|
||||
AudioWorker* processor = new AudioWorker(&node_copy_map_);
|
||||
processor->SetParameters(params());
|
||||
processors_.append(processor);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void AudioBackend::CloseInternal()
|
||||
{
|
||||
}
|
||||
|
||||
void AudioBackend::ConnectWorkerToThis(RenderWorker *worker)
|
||||
{
|
||||
AudioRenderBackend::ConnectWorkerToThis(worker);
|
||||
|
||||
connect(worker, &RenderWorker::CompletedCache, this, &AudioBackend::ThreadCompletedCache);
|
||||
}
|
||||
|
||||
void AudioBackend::ThreadCompletedCache(NodeDependency dep, NodeValueTable data, qint64 job_time)
|
||||
{
|
||||
SetWorkerBusyState(static_cast<RenderWorker*>(sender()), false);
|
||||
|
||||
if (job_time == render_job_info_.value(dep.range())) {
|
||||
render_job_info_.remove(dep.range());
|
||||
|
||||
SampleBufferPtr cached_sample_ptr = data.Get(NodeParam::kSamples).value<SampleBufferPtr>();
|
||||
|
||||
if (cached_sample_ptr) {
|
||||
QByteArray cached_samples = cached_sample_ptr->toPackedData();
|
||||
|
||||
int offset = params().time_to_bytes(dep.in());
|
||||
int length = params().time_to_bytes(dep.range().length());
|
||||
int out_point = qMin(offset + length, params().time_to_bytes(GetSequenceLength()));
|
||||
|
||||
if (offset < out_point) {
|
||||
if (offset + length > out_point) {
|
||||
length = out_point - offset;
|
||||
}
|
||||
|
||||
QFile f(CachePathName());
|
||||
if (f.open(QFile::ReadWrite)) {
|
||||
|
||||
if (f.size() < out_point && !f.resize(out_point)) {
|
||||
qCritical() << "Failed to resize file" << CachePathName();
|
||||
}
|
||||
|
||||
if (!f.seek(offset)) {
|
||||
qCritical() << "Failed to seek file" << CachePathName();
|
||||
}
|
||||
|
||||
// Replace data with this data
|
||||
int copy_length = qMin(length, cached_samples.size());
|
||||
|
||||
f.write(cached_samples.data(), copy_length);
|
||||
|
||||
if (copy_length < length) {
|
||||
|
||||
// Fill in remainder with silence
|
||||
QByteArray empty_space(length - copy_length, 0);
|
||||
f.write(empty_space);
|
||||
}
|
||||
|
||||
f.close();
|
||||
} else {
|
||||
qWarning() << "Failed to write to cached PCM file";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CacheNext();
|
||||
}
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
@@ -1,52 +0,0 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2019 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef AUDIOBACKEND_H
|
||||
#define AUDIOBACKEND_H
|
||||
|
||||
#include <QFile>
|
||||
|
||||
#include "../audiorenderbackend.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
class AudioBackend : public AudioRenderBackend
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
AudioBackend(QObject* parent = nullptr);
|
||||
|
||||
virtual ~AudioBackend() override;
|
||||
|
||||
protected:
|
||||
virtual bool InitInternal() override;
|
||||
|
||||
virtual void CloseInternal() override;
|
||||
|
||||
virtual void ConnectWorkerToThis(RenderWorker* worker) override;
|
||||
|
||||
private slots:
|
||||
void ThreadCompletedCache(NodeDependency dep, NodeValueTable data, qint64 job_time);
|
||||
|
||||
};
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
|
||||
#endif // AUDIOBACKEND_H
|
||||
@@ -1,105 +0,0 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2019 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "audioworker.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
AudioWorker::AudioWorker(QHash<Node *, Node *> *copy_map, QObject *parent) :
|
||||
AudioRenderWorker(copy_map, parent)
|
||||
{
|
||||
}
|
||||
|
||||
NodeValue AudioWorker::FrameToValue(DecoderPtr decoder, StreamPtr stream, const TimeRange &range)
|
||||
{
|
||||
if (decoder->HasConformedVersion(audio_params())) {
|
||||
SampleBufferPtr frame = decoder->RetrieveAudio(range.in(), range.out() - range.in(), audio_params());
|
||||
|
||||
if (frame) {
|
||||
return NodeValue(NodeParam::kSamples, QVariant::fromValue(frame));
|
||||
}
|
||||
} else {
|
||||
emit ConformUnavailable(decoder->stream(), CurrentPath().range(), range.out(), audio_params());
|
||||
}
|
||||
|
||||
return NodeValue();
|
||||
}
|
||||
|
||||
void AudioWorker::RunNodeAccelerated(const Node *node, const TimeRange &range, NodeValueDatabase &input_params_in, NodeValueTable &output_params)
|
||||
{
|
||||
// Check if node processes samples
|
||||
if (!(node->GetCapabilities(input_params_in) & Node::kSampleProcessor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy database so we can make some temporary modifications to it
|
||||
NodeValueDatabase input_params = input_params_in;
|
||||
NodeInput* sample_input = node->ProcessesSamplesFrom(input_params);
|
||||
|
||||
// Try to find the sample buffer in the table
|
||||
QVariant samples_var = input_params[sample_input].Get(NodeParam::kSamples);
|
||||
|
||||
// If there isn't one, there's nothing to do
|
||||
if (samples_var.isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SampleBufferPtr input_buffer = samples_var.value<SampleBufferPtr>();
|
||||
|
||||
if (!input_buffer) {
|
||||
return;
|
||||
}
|
||||
|
||||
SampleBufferPtr output_buffer = SampleBuffer::CreateAllocated(input_buffer->audio_params(), input_buffer->sample_count_per_channel());
|
||||
|
||||
int sample_count = input_buffer->sample_count_per_channel();
|
||||
|
||||
// FIXME: Hardcoded float sample format
|
||||
for (int i=0;i<sample_count;i++) {
|
||||
// Calculate the exact rational time at this sample
|
||||
int sample_out_of_channel = i / audio_params().channel_count();
|
||||
double sample_to_second = static_cast<double>(sample_out_of_channel) / static_cast<double>(audio_params().sample_rate());
|
||||
|
||||
rational this_sample_time = rational::fromDouble(range.in().toDouble() + sample_to_second);
|
||||
|
||||
// Update all non-sample and non-footage inputs
|
||||
foreach (NodeParam* param, node->parameters()) {
|
||||
if (param->type() == NodeParam::kInput
|
||||
&& param != sample_input) {
|
||||
NodeInput* input = static_cast<NodeInput*>(param);
|
||||
|
||||
// If the input isn't keyframing, we don't need to update it unless it's connected, in which case it may change
|
||||
if (input->IsConnected() || input->is_keyframing()) {
|
||||
input_params.Insert(input, ProcessInput(input, TimeRange(this_sample_time, this_sample_time)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
node->ProcessSamples(input_params,
|
||||
audio_params(),
|
||||
input_buffer,
|
||||
output_buffer,
|
||||
i);
|
||||
}
|
||||
|
||||
output_params.Push(NodeParam::kSamples, QVariant::fromValue(output_buffer));
|
||||
}
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
@@ -1,275 +0,0 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2019 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "audiorenderbackend.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QtMath>
|
||||
|
||||
#include "audiorenderworker.h"
|
||||
#include "common/filefunctions.h"
|
||||
#include "task/conform/conform.h"
|
||||
#include "task/taskmanager.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
AudioRenderBackend::AudioRenderBackend(QObject *parent) :
|
||||
RenderBackend(parent),
|
||||
ic_from_conform_(false)
|
||||
{
|
||||
connect(this, &AudioRenderBackend::QueueComplete, this, &AudioRenderBackend::FilterQueueCompleteSignal);
|
||||
}
|
||||
|
||||
void AudioRenderBackend::SetParameters(const AudioRenderingParams ¶ms)
|
||||
{
|
||||
CancelQueue();
|
||||
|
||||
// Set new parameters
|
||||
params_ = params;
|
||||
|
||||
// Set params on all processors
|
||||
foreach (RenderWorker* worker, processors_) {
|
||||
static_cast<AudioRenderWorker*>(worker)->SetParameters(params_);
|
||||
}
|
||||
|
||||
// Regenerate the cache ID
|
||||
RegenerateCacheID();
|
||||
|
||||
emit ParamsChanged();
|
||||
}
|
||||
|
||||
void AudioRenderBackend::ConnectViewer(ViewerOutput *node)
|
||||
{
|
||||
connect(node, &ViewerOutput::AudioChangedBetween, this, &AudioRenderBackend::InvalidateCache);
|
||||
connect(node, &ViewerOutput::LengthChanged, this, &AudioRenderBackend::TruncateCache);
|
||||
}
|
||||
|
||||
void AudioRenderBackend::DisconnectViewer(ViewerOutput *node)
|
||||
{
|
||||
disconnect(node, &ViewerOutput::AudioChangedBetween, this, &AudioRenderBackend::InvalidateCache);
|
||||
disconnect(node, &ViewerOutput::LengthChanged, this, &AudioRenderBackend::TruncateCache);
|
||||
|
||||
conform_wait_info_.clear();
|
||||
}
|
||||
|
||||
bool AudioRenderBackend::GenerateCacheIDInternal(QCryptographicHash &hash)
|
||||
{
|
||||
if (!params_.is_valid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Generate an ID that is more or less guaranteed to be unique to this Sequence
|
||||
hash.addData(QString::number(params_.sample_rate()).toUtf8());
|
||||
hash.addData(QString::number(params_.channel_layout()).toUtf8());
|
||||
hash.addData(QString::number(params_.format()).toUtf8());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const AudioRenderingParams &AudioRenderBackend::params() const
|
||||
{
|
||||
return params_;
|
||||
}
|
||||
|
||||
NodeInput *AudioRenderBackend::GetDependentInput()
|
||||
{
|
||||
return viewer_node()->samples_input();
|
||||
}
|
||||
|
||||
QString AudioRenderBackend::CachePathName() const
|
||||
{
|
||||
QString cache_fn = cache_id();
|
||||
cache_fn.append(".pcm");
|
||||
return QDir(FileFunctions::GetMediaCacheLocation()).filePath(cache_fn);
|
||||
}
|
||||
|
||||
bool AudioRenderBackend::CanRender()
|
||||
{
|
||||
return params_.is_valid();
|
||||
}
|
||||
|
||||
void AudioRenderBackend::ConnectWorkerToThis(RenderWorker *worker)
|
||||
{
|
||||
AudioRenderWorker* arw = static_cast<AudioRenderWorker*>(worker);
|
||||
|
||||
connect(arw, &AudioRenderWorker::ConformUnavailable, this, &AudioRenderBackend::ConformUnavailable, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
TimeRange AudioRenderBackend::PopNextFrameFromQueue()
|
||||
{
|
||||
TimeRange range = cache_queue_.first();
|
||||
|
||||
// Limit range per worker to 2 seconds (FIXME: arbitrary, should be tweaked, maybe even in config?)
|
||||
range.set_out(qMin(range.out(), range.in() + rational(2)));
|
||||
|
||||
cache_queue_.RemoveTimeRange(range);
|
||||
|
||||
return range;
|
||||
}
|
||||
|
||||
void AudioRenderBackend::InvalidateCacheInternal(const rational &start_range, const rational &end_range)
|
||||
{
|
||||
if (!ic_from_conform_) {
|
||||
// Cancel any ranges waiting on a conform here since obviously the contents have changed
|
||||
TimeRange range(start_range, end_range);
|
||||
|
||||
for (int i=0;i<conform_wait_info_.size();i++) {
|
||||
ConformWaitInfo& info = conform_wait_info_[i];
|
||||
|
||||
// FIXME: Code copied from TimeRangeList::RemoveTimeRange()
|
||||
|
||||
if (range.Contains(info.affected_range)) {
|
||||
conform_wait_info_.removeAt(i);
|
||||
i--;
|
||||
} else if (info.affected_range.Contains(range, false, false)) {
|
||||
ConformWaitInfo copy = info;
|
||||
|
||||
info.affected_range.set_out(start_range);
|
||||
copy.affected_range.set_in(end_range);
|
||||
|
||||
conform_wait_info_.append(copy);
|
||||
} else if (info.affected_range.in() < start_range && info.affected_range.out() > start_range) {
|
||||
info.affected_range.set_out(start_range);
|
||||
} else if (info.affected_range.in() < end_range && info.affected_range.out() > end_range) {
|
||||
info.affected_range.set_in(end_range);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RenderBackend::InvalidateCacheInternal(start_range, end_range);
|
||||
}
|
||||
|
||||
void AudioRenderBackend::ListenForConformSignal(AudioStreamPtr s)
|
||||
{
|
||||
foreach (const ConformWaitInfo& info, conform_wait_info_) {
|
||||
if (info.stream == s) {
|
||||
// We've probably already connected to this one
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
connect(s.get(), &AudioStream::ConformAppended, this, &AudioRenderBackend::ConformUpdated);
|
||||
}
|
||||
|
||||
void AudioRenderBackend::StopListeningForConformSignal(AudioStream* s)
|
||||
{
|
||||
foreach (const ConformWaitInfo& info, conform_wait_info_) {
|
||||
if (info.stream.get() == s) {
|
||||
// There are still conforms we're waiting for, don't disconnect
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
disconnect(s, &AudioStream::ConformAppended, this, &AudioRenderBackend::ConformUpdated);
|
||||
}
|
||||
|
||||
void AudioRenderBackend::ConformUnavailable(StreamPtr stream, TimeRange range, rational stream_time, AudioRenderingParams params)
|
||||
{
|
||||
ConformWaitInfo info = {stream, params, range, stream_time};
|
||||
|
||||
if (conform_wait_info_.contains(info)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AudioStreamPtr audio_stream = std::static_pointer_cast<AudioStream>(stream);
|
||||
|
||||
if (audio_stream->try_start_conforming(params)) {
|
||||
|
||||
// Start indexing process
|
||||
ListenForConformSignal(audio_stream);
|
||||
|
||||
conform_wait_info_.append(info);
|
||||
|
||||
ConformTask* conform_task = new ConformTask(audio_stream, params);
|
||||
|
||||
TaskManager::instance()->AddTask(conform_task);
|
||||
|
||||
} else if (audio_stream->has_conformed_version(params)) {
|
||||
|
||||
// Conform JUST finished, requeue this time
|
||||
ic_from_conform_ = true;
|
||||
InvalidateCache(range, nullptr);
|
||||
ic_from_conform_ = false;
|
||||
|
||||
} else {
|
||||
|
||||
// A conform task is already running, so we'll just wait for it
|
||||
ListenForConformSignal(audio_stream);
|
||||
|
||||
conform_wait_info_.append(info);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void AudioRenderBackend::ConformUpdated(AudioRenderingParams params)
|
||||
{
|
||||
AudioStream *stream = static_cast<AudioStream*>(sender());
|
||||
|
||||
for (int i=0;i<conform_wait_info_.size();i++) {
|
||||
const ConformWaitInfo& info = conform_wait_info_.at(i);
|
||||
|
||||
if (info.stream.get() == stream
|
||||
&& info.params == params) {
|
||||
|
||||
// Make a copy so the values we use aren't corrupt
|
||||
ConformWaitInfo copy = info;
|
||||
|
||||
// Remove this entry from the list
|
||||
conform_wait_info_.removeAt(i);
|
||||
i--;
|
||||
|
||||
// Send invalidate cache signal
|
||||
ic_from_conform_ = true;
|
||||
InvalidateCache(copy.affected_range, nullptr);
|
||||
ic_from_conform_ = false;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
StopListeningForConformSignal(stream);
|
||||
}
|
||||
|
||||
void AudioRenderBackend::TruncateCache(const rational &r)
|
||||
{
|
||||
int seq_length = params_.time_to_bytes(r);
|
||||
|
||||
QFile cache_pcm(CachePathName());
|
||||
|
||||
if (cache_pcm.size() > seq_length) {
|
||||
cache_pcm.resize(seq_length);
|
||||
}
|
||||
}
|
||||
|
||||
void AudioRenderBackend::FilterQueueCompleteSignal()
|
||||
{
|
||||
if (conform_wait_info_.isEmpty()) {
|
||||
emit AudioComplete();
|
||||
}
|
||||
}
|
||||
|
||||
bool AudioRenderBackend::ConformWaitInfo::operator==(const AudioRenderBackend::ConformWaitInfo &rhs) const
|
||||
{
|
||||
return rhs.params == params
|
||||
&& rhs.stream == stream
|
||||
&& rhs.stream_time == stream_time
|
||||
&& rhs.affected_range == affected_range;
|
||||
}
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
@@ -1,105 +0,0 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2019 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef AUDIORENDERBACKEND_H
|
||||
#define AUDIORENDERBACKEND_H
|
||||
|
||||
#include "common/timerange.h"
|
||||
#include "renderbackend.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
class AudioRenderBackend : public RenderBackend
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
AudioRenderBackend(QObject* parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Set parameters of the Renderer
|
||||
*
|
||||
* The Renderer owns the buffers that are used in the rendering process and this function sets the kind of buffers
|
||||
* to use. The Renderer must be stopped when calling this function.
|
||||
*/
|
||||
void SetParameters(const AudioRenderingParams ¶ms);
|
||||
|
||||
const AudioRenderingParams& params() const;
|
||||
|
||||
QString CachePathName() const;
|
||||
|
||||
signals:
|
||||
void ParamsChanged();
|
||||
|
||||
void AudioComplete();
|
||||
|
||||
protected:
|
||||
virtual void ConnectViewer(ViewerOutput* node) override;
|
||||
|
||||
virtual void DisconnectViewer(ViewerOutput* node) override;
|
||||
|
||||
/**
|
||||
* @brief Internal function for generating the cache ID
|
||||
*/
|
||||
virtual bool GenerateCacheIDInternal(QCryptographicHash& hash) override;
|
||||
|
||||
virtual NodeInput* GetDependentInput() override;
|
||||
|
||||
virtual bool CanRender() override;
|
||||
|
||||
virtual void ConnectWorkerToThis(RenderWorker* worker) override;
|
||||
|
||||
virtual TimeRange PopNextFrameFromQueue() override;
|
||||
|
||||
virtual void InvalidateCacheInternal(const rational &start_range, const rational &end_range) override;
|
||||
|
||||
private:
|
||||
struct ConformWaitInfo {
|
||||
StreamPtr stream;
|
||||
AudioRenderingParams params;
|
||||
TimeRange affected_range;
|
||||
rational stream_time;
|
||||
|
||||
bool operator==(const ConformWaitInfo& rhs) const;
|
||||
};
|
||||
|
||||
void ListenForConformSignal(AudioStreamPtr s);
|
||||
|
||||
void StopListeningForConformSignal(AudioStream *s);
|
||||
|
||||
QList<ConformWaitInfo> conform_wait_info_;
|
||||
|
||||
AudioRenderingParams params_;
|
||||
|
||||
bool ic_from_conform_;
|
||||
|
||||
private slots:
|
||||
void ConformUnavailable(StreamPtr stream, TimeRange range, rational stream_time, AudioRenderingParams params);
|
||||
|
||||
void ConformUpdated(OLIVE_NAMESPACE::AudioRenderingParams params);
|
||||
|
||||
void TruncateCache(const rational& r);
|
||||
|
||||
void FilterQueueCompleteSignal();
|
||||
|
||||
};
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
|
||||
#endif // AUDIORENDERBACKEND_H
|
||||
@@ -1,173 +0,0 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2019 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "audiorenderworker.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFloat16>
|
||||
|
||||
#include "audio/audiomanager.h"
|
||||
#include "audio/sumsamples.h"
|
||||
#include "config/config.h"
|
||||
#include "node/block/clip/clip.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
AudioRenderWorker::AudioRenderWorker(QHash<Node *, Node *> *copy_map, QObject *parent) :
|
||||
RenderWorker(parent),
|
||||
copy_map_(copy_map)
|
||||
{
|
||||
}
|
||||
|
||||
void AudioRenderWorker::SetParameters(const AudioRenderingParams &audio_params)
|
||||
{
|
||||
audio_params_ = audio_params;
|
||||
}
|
||||
|
||||
bool AudioRenderWorker::InitInternal()
|
||||
{
|
||||
// Nothing to init yet
|
||||
return true;
|
||||
}
|
||||
|
||||
void AudioRenderWorker::CloseInternal()
|
||||
{
|
||||
// Nothing to init yet
|
||||
}
|
||||
|
||||
NodeValueTable AudioRenderWorker::RenderBlock(const TrackOutput *track, const TimeRange &range)
|
||||
{
|
||||
QList<Block*> active_blocks = track->BlocksAtTimeRange(range);
|
||||
|
||||
// All these blocks will need to output to a buffer so we create one here
|
||||
SampleBufferPtr block_range_buffer = SampleBuffer::CreateAllocated(audio_params_, audio_params_.time_to_samples(range.length()));
|
||||
block_range_buffer->fill(0);
|
||||
|
||||
NodeValueTable merged_table;
|
||||
|
||||
// Loop through active blocks retrieving their audio
|
||||
foreach (Block* b, active_blocks) {
|
||||
TimeRange range_for_block(qMax(b->in(), range.in()),
|
||||
qMin(b->out(), range.out()));
|
||||
|
||||
int destination_offset = audio_params_.time_to_samples(range_for_block.in() - range.in());
|
||||
int max_dest_sz = audio_params_.time_to_samples(range_for_block.length());
|
||||
|
||||
// Destination buffer
|
||||
NodeValueTable table = ProcessNode(NodeDependency(b, range_for_block));
|
||||
QVariant sample_val = table.Take(NodeParam::kSamples);
|
||||
SampleBufferPtr samples_from_this_block;
|
||||
|
||||
if (sample_val.isNull()
|
||||
|| !(samples_from_this_block = sample_val.value<SampleBufferPtr>())) {
|
||||
// If we retrieved no samples from this block, do nothing
|
||||
continue;
|
||||
}
|
||||
|
||||
// Stretch samples here
|
||||
rational abs_speed = qAbs(b->speed());
|
||||
|
||||
if (abs_speed != 1) {
|
||||
samples_from_this_block->speed(abs_speed.toDouble());
|
||||
}
|
||||
|
||||
if (b->is_reversed()) {
|
||||
// Reverse the audio buffer
|
||||
samples_from_this_block->reverse();
|
||||
}
|
||||
|
||||
int copy_length = qMin(max_dest_sz, samples_from_this_block->sample_count_per_channel());
|
||||
|
||||
// Copy samples into destination buffer
|
||||
block_range_buffer->set(samples_from_this_block->const_data(), destination_offset, copy_length);
|
||||
|
||||
{
|
||||
// Save waveform to file
|
||||
Block* src_block = static_cast<Block*>(copy_map_->key(b));
|
||||
QDir local_appdata_dir(Config::Current()["DiskCachePath"].toString());
|
||||
QDir waveform_loc = local_appdata_dir.filePath(QStringLiteral("waveform"));
|
||||
waveform_loc.mkpath(".");
|
||||
QString wave_fn(waveform_loc.filePath(QString::number(reinterpret_cast<quintptr>(src_block))));
|
||||
QFile wave_file(wave_fn);
|
||||
|
||||
if (wave_file.open(QFile::ReadWrite)) {
|
||||
// We use S32 as a size-compatible substitute for SampleSummer::Sum which is 4 bytes in size
|
||||
AudioRenderingParams waveform_params(SampleSummer::kSumSampleRate, audio_params_.channel_layout(), SampleFormat::SAMPLE_FMT_S32);
|
||||
int chunk_size = (audio_params().sample_rate() / waveform_params.sample_rate());
|
||||
|
||||
{
|
||||
// Write metadata header
|
||||
SampleSummer::Info info;
|
||||
info.channels = audio_params_.channel_count();
|
||||
wave_file.write(reinterpret_cast<char*>(&info), sizeof(SampleSummer::Info));
|
||||
}
|
||||
|
||||
qint64 start_offset = sizeof(SampleSummer::Info) + waveform_params.time_to_bytes(range_for_block.in() - b->in());
|
||||
qint64 length_offset = waveform_params.time_to_bytes(range_for_block.length());
|
||||
qint64 end_offset = start_offset + length_offset;
|
||||
|
||||
if (wave_file.size() < end_offset) {
|
||||
wave_file.resize(end_offset);
|
||||
}
|
||||
|
||||
wave_file.seek(start_offset);
|
||||
|
||||
for (int i=0;i<samples_from_this_block->sample_count_per_channel();i+=chunk_size) {
|
||||
QVector<SampleSummer::Sum> summary = SampleSummer::SumSamples(samples_from_this_block,
|
||||
i,
|
||||
qMin(chunk_size, samples_from_this_block->sample_count_per_channel() - i));
|
||||
|
||||
wave_file.write(reinterpret_cast<const char*>(summary.constData()),
|
||||
summary.size() * sizeof(SampleSummer::Sum));
|
||||
}
|
||||
|
||||
wave_file.close();
|
||||
|
||||
if (src_block->type() == Block::kClip) {
|
||||
emit static_cast<ClipBlock*>(src_block)->PreviewUpdated();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NodeValueTable::Merge({merged_table, table});
|
||||
}
|
||||
|
||||
merged_table.Push(NodeParam::kSamples, QVariant::fromValue(block_range_buffer));
|
||||
|
||||
return merged_table;
|
||||
}
|
||||
|
||||
void AudioRenderWorker::FootageProcessingEvent(StreamPtr stream, const TimeRange &input_time, NodeValueTable *table)
|
||||
{
|
||||
if (stream->type() != Stream::kAudio) {
|
||||
return;
|
||||
}
|
||||
|
||||
NodeValue value = GetDataFromStream(stream, input_time);
|
||||
|
||||
table->Push(value);
|
||||
}
|
||||
|
||||
const AudioRenderingParams &AudioRenderWorker::audio_params() const
|
||||
{
|
||||
return audio_params_;
|
||||
}
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
@@ -1,59 +0,0 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2019 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef AUDIORENDERWORKER_H
|
||||
#define AUDIORENDERWORKER_H
|
||||
|
||||
#include "renderworker.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
class AudioRenderWorker : public RenderWorker
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
AudioRenderWorker(QHash<Node*, Node*>* copy_map, QObject* parent = nullptr);
|
||||
|
||||
void SetParameters(const AudioRenderingParams& audio_params);
|
||||
|
||||
signals:
|
||||
void ConformUnavailable(StreamPtr stream, TimeRange range, rational stream_time, AudioRenderingParams params);
|
||||
|
||||
protected:
|
||||
virtual bool InitInternal() override;
|
||||
|
||||
virtual void CloseInternal() override;
|
||||
|
||||
virtual NodeValueTable RenderBlock(const TrackOutput *track, const TimeRange& range) override;
|
||||
|
||||
virtual void FootageProcessingEvent(StreamPtr stream, const TimeRange &input_time, NodeValueTable* table) override;
|
||||
|
||||
const AudioRenderingParams& audio_params() const;
|
||||
|
||||
private:
|
||||
AudioRenderingParams audio_params_;
|
||||
|
||||
QHash<Node*, Node*>* copy_map_;
|
||||
|
||||
};
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
|
||||
#endif // AUDIORENDERWORKER_H
|
||||
@@ -22,7 +22,6 @@
|
||||
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
|
||||
#include "render/backend/audio/audiobackend.h"
|
||||
#include "render/backend/opengl/openglbackend.h"
|
||||
#include "render/colormanager.h"
|
||||
#include "render/pixelformat.h"
|
||||
@@ -36,8 +35,7 @@ Exporter::Exporter(ViewerOutput *viewer_node,
|
||||
QObject(parent),
|
||||
viewer_node_(viewer_node),
|
||||
params_(params),
|
||||
video_backend_(nullptr),
|
||||
audio_backend_(nullptr),
|
||||
renderer_(nullptr),
|
||||
export_status_(false),
|
||||
export_msg_(tr("Export hasn't started yet"))
|
||||
{
|
||||
@@ -54,7 +52,7 @@ Exporter::Exporter(ViewerOutput *viewer_node,
|
||||
if (params_.has_custom_range()) {
|
||||
export_range_ = params_.custom_range();
|
||||
} else {
|
||||
export_range_ = TimeRange(0, viewer_node_->Length());
|
||||
export_range_ = TimeRange(0, viewer_node_->GetLength());
|
||||
}
|
||||
|
||||
if (params_.video_enabled()) {
|
||||
@@ -87,18 +85,10 @@ const QString &Exporter::GetExportError() const
|
||||
|
||||
void Exporter::Cancel()
|
||||
{
|
||||
if (video_backend_) {
|
||||
video_backend_->CancelQueue();
|
||||
video_backend_->Close();
|
||||
video_backend_->deleteLater();
|
||||
video_backend_ = nullptr;
|
||||
}
|
||||
|
||||
if (audio_backend_) {
|
||||
audio_backend_->CancelQueue();
|
||||
audio_backend_->Close();
|
||||
audio_backend_->deleteLater();
|
||||
audio_backend_ = nullptr;
|
||||
if (renderer_) {
|
||||
renderer_->CancelQueue();
|
||||
renderer_->deleteLater();
|
||||
renderer_ = nullptr;
|
||||
}
|
||||
|
||||
SetExportMessage(tr("User cancelled export"));
|
||||
@@ -111,25 +101,18 @@ void Exporter::StartExporting()
|
||||
export_status_ = false;
|
||||
|
||||
// Create renderers
|
||||
if (!video_done_) {
|
||||
video_backend_ = new OpenGLBackend();
|
||||
renderer_ = new OpenGLBackend();
|
||||
renderer_->SetViewerNode(viewer_node_);
|
||||
|
||||
video_backend_->SetLimitCaching(false);
|
||||
video_backend_->SetViewerNode(viewer_node_);
|
||||
video_backend_->SetParameters(VideoRenderingParams(viewer_node_->video_params().width(),
|
||||
viewer_node_->video_params().height(),
|
||||
params_.video_params().time_base(),
|
||||
params_.video_params().format(),
|
||||
params_.video_params().mode()));
|
||||
if (!video_done_) {
|
||||
renderer_->SetPixelFormat(params_.video_params().format());
|
||||
renderer_->SetMode(params_.video_params().mode());
|
||||
|
||||
waiting_for_frame_ = 0;
|
||||
}
|
||||
|
||||
if (!audio_done_) {
|
||||
audio_backend_ = new AudioBackend();
|
||||
|
||||
audio_backend_->SetViewerNode(viewer_node_);
|
||||
audio_backend_->SetParameters(params_.audio_params());
|
||||
renderer_->SetSampleFormat(params_.audio_params().format());
|
||||
}
|
||||
|
||||
// Open encoder and wait for result
|
||||
@@ -153,10 +136,9 @@ void Exporter::ExportSucceeded()
|
||||
return;
|
||||
}
|
||||
|
||||
if (video_backend_) {
|
||||
video_backend_->Close();
|
||||
video_backend_->deleteLater();
|
||||
video_backend_ = nullptr;
|
||||
if (renderer_) {
|
||||
renderer_->deleteLater();
|
||||
renderer_ = nullptr;
|
||||
}
|
||||
|
||||
export_status_ = true;
|
||||
@@ -189,10 +171,10 @@ void Exporter::EncodeFrame()
|
||||
waiting_for_frame_ += params_.video_params().time_base();
|
||||
|
||||
// Calculate progress
|
||||
emit ProgressChanged(waiting_for_frame_.toDouble() / viewer_node_->Length().toDouble());
|
||||
emit ProgressChanged(waiting_for_frame_.toDouble() / viewer_node_->GetLength().toDouble());
|
||||
}
|
||||
|
||||
if (waiting_for_frame_ >= viewer_node_->Length()) {
|
||||
if (waiting_for_frame_ >= viewer_node_->GetLength()) {
|
||||
video_done_ = true;
|
||||
debug_timer_.stop();
|
||||
|
||||
@@ -261,6 +243,7 @@ void Exporter::FrameRendered(FramePtr frame)
|
||||
|
||||
void Exporter::AudioRendered()
|
||||
{
|
||||
/*
|
||||
// Retrieve the audio filename
|
||||
QString cache_fn = audio_backend_->CachePathName();
|
||||
|
||||
@@ -270,11 +253,7 @@ void Exporter::AudioRendered()
|
||||
OLIVE_NS_ARG(AudioRenderingParams, audio_backend_->params()),
|
||||
Q_ARG(const QString&, cache_fn),
|
||||
OLIVE_NS_ARG(TimeRange, export_range_));
|
||||
|
||||
// We don't need the audio backend anymorea
|
||||
audio_backend_->Close();
|
||||
audio_backend_->deleteLater();
|
||||
audio_backend_ = nullptr;
|
||||
*/
|
||||
}
|
||||
|
||||
void Exporter::AudioEncodeComplete()
|
||||
@@ -286,6 +265,7 @@ void Exporter::AudioEncodeComplete()
|
||||
|
||||
void Exporter::EncoderOpenedSuccessfully()
|
||||
{
|
||||
/*
|
||||
// Invalidate caches
|
||||
if (!video_done_) {
|
||||
// First we generate the hashes so we know exactly how many frames we need
|
||||
@@ -301,6 +281,7 @@ void Exporter::EncoderOpenedSuccessfully()
|
||||
|
||||
audio_backend_->InvalidateCache(export_range_, nullptr);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
void Exporter::EncoderOpenFailed()
|
||||
@@ -317,12 +298,13 @@ void Exporter::EncoderClosed()
|
||||
|
||||
void Exporter::VideoHashesComplete()
|
||||
{
|
||||
/*
|
||||
// We've got our hashes, time to kick off actual rendering
|
||||
disconnect(video_backend_, &VideoRenderBackend::QueueComplete, this, &Exporter::VideoHashesComplete);
|
||||
|
||||
// Determine what frames will be hashed
|
||||
TimeRangeList ranges;
|
||||
ranges.append(TimeRange(0, viewer_node_->Length()));
|
||||
ranges.append(TimeRange(0, viewer_node_->GetLength()));
|
||||
|
||||
// Set video backend to render mode but NOT hash or download
|
||||
video_backend_->SetOperatingMode(VideoRenderWorker::kRenderOnly);
|
||||
@@ -347,6 +329,7 @@ void Exporter::VideoHashesComplete()
|
||||
foreach (const TimeRange& range, ranges) {
|
||||
video_backend_->InvalidateCache(range, nullptr);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
void Exporter::DebugTimerMessage()
|
||||
@@ -356,7 +339,7 @@ void Exporter::DebugTimerMessage()
|
||||
|
||||
void Exporter::FrameColorFinished()
|
||||
{
|
||||
if (!video_backend_ && !audio_backend_) {
|
||||
if (!renderer_) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -366,7 +349,7 @@ void Exporter::FrameColorFinished()
|
||||
|
||||
debug_timer_.stop();
|
||||
|
||||
const QMap<rational, QByteArray>& time_hash_map = video_backend_->frame_cache()->time_hash_map();
|
||||
const QMap<rational, QByteArray>& time_hash_map = viewer_node_->video_frame_cache()->time_hash_map();
|
||||
|
||||
QByteArray this_hash = time_hash_map.value(frame->timestamp());
|
||||
|
||||
|
||||
@@ -28,9 +28,8 @@
|
||||
|
||||
#include "codec/encoder.h"
|
||||
#include "node/output/viewer/viewer.h"
|
||||
#include "render/backend/audiorenderbackend.h"
|
||||
#include "render/backend/exportparams.h"
|
||||
#include "render/backend/videorenderbackend.h"
|
||||
#include "render/backend/renderbackend.h"
|
||||
#include "render/colorprocessor.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
@@ -76,8 +75,7 @@ private:
|
||||
ExportParams params_;
|
||||
|
||||
// Renderers
|
||||
VideoRenderBackend* video_backend_;
|
||||
AudioRenderBackend* audio_backend_;
|
||||
RenderBackend* renderer_;
|
||||
|
||||
// Export transform
|
||||
QMatrix4x4 transform_;
|
||||
|
||||
@@ -33,7 +33,5 @@ set(OLIVE_SOURCES
|
||||
render/backend/opengl/opengltexture.cpp
|
||||
render/backend/opengl/opengltexturecache.h
|
||||
render/backend/opengl/opengltexturecache.cpp
|
||||
render/backend/opengl/openglworker.h
|
||||
render/backend/opengl/openglworker.cpp
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
@@ -20,77 +20,12 @@
|
||||
|
||||
#include "openglbackend.h"
|
||||
|
||||
#include <QEventLoop>
|
||||
#include <QThread>
|
||||
|
||||
#include "openglrenderfunctions.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
OpenGLBackend::OpenGLBackend(QObject *parent) :
|
||||
VideoRenderBackend(parent),
|
||||
proxy_(nullptr)
|
||||
OpenGLBackend::OpenGLBackend(QObject* parent) :
|
||||
RenderBackend(parent)
|
||||
{
|
||||
}
|
||||
|
||||
OpenGLBackend::~OpenGLBackend()
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
bool OpenGLBackend::InitInternal()
|
||||
{
|
||||
if (!VideoRenderBackend::InitInternal()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
proxy_ = new OpenGLProxy();
|
||||
proxy_->SetParameters(params());
|
||||
QThread* proxy_thread = new QThread();
|
||||
proxy_thread->start(QThread::IdlePriority);
|
||||
proxy_->moveToThread(proxy_thread);
|
||||
|
||||
if (!proxy_->Init()) {
|
||||
proxy_thread->quit();
|
||||
proxy_thread->wait();
|
||||
proxy_thread->deleteLater();
|
||||
|
||||
proxy_->deleteLater();
|
||||
proxy_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initiate one thread per CPU core
|
||||
for (int i=0;i<threads().size();i++) {
|
||||
// Create one processor object for each thread
|
||||
OpenGLWorker* processor = new OpenGLWorker(frame_cache(), proxy_);
|
||||
processor->SetParameters(params());
|
||||
processors_.append(processor);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void OpenGLBackend::CloseInternal()
|
||||
{
|
||||
if (proxy_) {
|
||||
proxy_->thread()->quit();
|
||||
proxy_->thread()->wait();
|
||||
proxy_->thread()->deleteLater();
|
||||
|
||||
proxy_->deleteLater();
|
||||
proxy_ = nullptr;
|
||||
}
|
||||
|
||||
VideoRenderBackend::CloseInternal();
|
||||
}
|
||||
|
||||
void OpenGLBackend::ParamsChangedEvent()
|
||||
{
|
||||
// If we're initiated, we need to recreate the texture. Otherwise this backend isn't active so it doesn't matter.
|
||||
if (IsInitiated()) {
|
||||
proxy_->SetParameters(params());
|
||||
}
|
||||
}
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
|
||||
@@ -21,32 +21,21 @@
|
||||
#ifndef OPENGLBACKEND_H
|
||||
#define OPENGLBACKEND_H
|
||||
|
||||
#include "../videorenderbackend.h"
|
||||
#include "openglframebuffer.h"
|
||||
#include "openglproxy.h"
|
||||
#include "openglshader.h"
|
||||
#include "opengltexture.h"
|
||||
#include "openglworker.h"
|
||||
#include "render/backend/renderbackend.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
class OpenGLBackend : public VideoRenderBackend
|
||||
class OpenGLBackend : public RenderBackend
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
OpenGLBackend(QObject* parent = nullptr);
|
||||
|
||||
virtual ~OpenGLBackend() override;
|
||||
|
||||
protected:
|
||||
virtual bool InitInternal() override;
|
||||
virtual void TextureToFrame(const QVariant& texture, FramePtr frame) const override;
|
||||
|
||||
virtual void CloseInternal() override;
|
||||
virtual QVariant FrameToTexture(FramePtr frame) const override;
|
||||
|
||||
virtual void ParamsChangedEvent() override;
|
||||
|
||||
private:
|
||||
OpenGLProxy* proxy_;
|
||||
virtual void ProcessNodeEvent(const Node *node, const TimeRange &range, NodeValueDatabase &input_params, NodeValueTable &output_params) const override;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#define OPENGLCOLORPROCESSOR_H
|
||||
|
||||
#include "openglshader.h"
|
||||
#include "render/backend/rendercache.h"
|
||||
#include "render/colorprocessor.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
@@ -62,6 +63,8 @@ private slots:
|
||||
|
||||
};
|
||||
|
||||
using OpenGLColorProcessorCache = RenderCache<QString, OpenGLColorProcessorPtr>;
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
|
||||
#endif // OPENGLCOLORPROCESSOR_H
|
||||
|
||||
@@ -24,7 +24,9 @@
|
||||
#include <QOffscreenSurface>
|
||||
#include <QOpenGLContext>
|
||||
|
||||
#include "../videorenderworker.h"
|
||||
#include "common/timerange.h"
|
||||
#include "node/value.h"
|
||||
#include "openglcolorprocessor.h"
|
||||
#include "openglframebuffer.h"
|
||||
#include "openglshadercache.h"
|
||||
#include "opengltexturecache.h"
|
||||
@@ -81,7 +83,7 @@ private:
|
||||
|
||||
OpenGLFramebuffer buffer_;
|
||||
|
||||
ColorProcessorCache color_cache_;
|
||||
OpenGLColorProcessorCache color_cache_;
|
||||
|
||||
VideoRenderingParams video_params_;
|
||||
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2019 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "openglworker.h"
|
||||
|
||||
#include "common/clamp.h"
|
||||
#include "core.h"
|
||||
#include "node/block/transition/transition.h"
|
||||
#include "node/node.h"
|
||||
#include "openglcolorprocessor.h"
|
||||
#include "openglrenderfunctions.h"
|
||||
#include "render/colormanager.h"
|
||||
#include "render/pixelformat.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
OpenGLWorker::OpenGLWorker(VideoRenderFrameCache *frame_cache, OpenGLProxy *proxy, QObject *parent) :
|
||||
VideoRenderWorker(frame_cache, parent),
|
||||
proxy_(proxy)
|
||||
{
|
||||
}
|
||||
|
||||
NodeValue OpenGLWorker::FrameToValue(DecoderPtr decoder, StreamPtr stream, const TimeRange &range)
|
||||
{
|
||||
FramePtr frame = decoder->RetrieveVideo(range.in(),
|
||||
video_params().divider(),
|
||||
video_params().mode() == RenderMode::kOffline);
|
||||
|
||||
NodeValue value;
|
||||
|
||||
if (frame) {
|
||||
QMetaObject::invokeMethod(proxy_,
|
||||
"FrameToValue",
|
||||
Qt::BlockingQueuedConnection,
|
||||
OLIVE_NS_RETURN_ARG(NodeValue, value),
|
||||
OLIVE_NS_ARG(FramePtr, frame),
|
||||
OLIVE_NS_ARG(StreamPtr, stream));
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
void OpenGLWorker::RunNodeAccelerated(const Node *node, const TimeRange &range, NodeValueDatabase &input_params, NodeValueTable &output_params)
|
||||
{
|
||||
QMetaObject::invokeMethod(proxy_,
|
||||
"RunNodeAccelerated",
|
||||
Qt::BlockingQueuedConnection,
|
||||
OLIVE_NS_CONST_ARG(Node*, node),
|
||||
OLIVE_NS_CONST_ARG(TimeRange&, range),
|
||||
OLIVE_NS_ARG(NodeValueDatabase&, input_params),
|
||||
OLIVE_NS_ARG(NodeValueTable&, output_params));
|
||||
}
|
||||
|
||||
void OpenGLWorker::TextureToBuffer(const QVariant &tex_in, int width, int height, const QMatrix4x4& matrix, void *buffer, int linesize)
|
||||
{
|
||||
QMetaObject::invokeMethod(proxy_,
|
||||
"TextureToBuffer",
|
||||
Qt::BlockingQueuedConnection,
|
||||
Q_ARG(const QVariant&, tex_in),
|
||||
Q_ARG(int, width),
|
||||
Q_ARG(int, height),
|
||||
Q_ARG(const QMatrix4x4&, matrix),
|
||||
Q_ARG(void*, buffer),
|
||||
Q_ARG(int, linesize));
|
||||
}
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
@@ -1,56 +0,0 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2019 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OPENGLPROCESSOR_H
|
||||
#define OPENGLPROCESSOR_H
|
||||
|
||||
#include <QOffscreenSurface>
|
||||
#include <QOpenGLContext>
|
||||
|
||||
#include "../videorenderworker.h"
|
||||
#include "openglframebuffer.h"
|
||||
#include "openglproxy.h"
|
||||
#include "openglshadercache.h"
|
||||
#include "opengltexturecache.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
class OpenGLWorker : public VideoRenderWorker {
|
||||
Q_OBJECT
|
||||
public:
|
||||
OpenGLWorker(VideoRenderFrameCache* frame_cache,
|
||||
OpenGLProxy* proxy,
|
||||
QObject* parent = nullptr);
|
||||
|
||||
protected:
|
||||
virtual NodeValue FrameToValue(DecoderPtr decoder, StreamPtr stream, const TimeRange &range) override;
|
||||
|
||||
virtual void RunNodeAccelerated(const Node *node, const TimeRange &range, NodeValueDatabase &input_params, NodeValueTable& output_params) override;
|
||||
|
||||
virtual void TextureToBuffer(const QVariant& texture, int width, int height, const QMatrix4x4& matrix, void *buffer, int linesize) override;
|
||||
|
||||
private:
|
||||
OpenGLProxy* proxy_;
|
||||
|
||||
};
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
|
||||
#endif // OPENGLPROCESSOR_H
|
||||
@@ -23,6 +23,7 @@
|
||||
#include <QDateTime>
|
||||
#include <QThread>
|
||||
|
||||
#include "config/config.h"
|
||||
#include "core.h"
|
||||
#include "window/mainwindow/mainwindow.h"
|
||||
|
||||
@@ -30,331 +31,409 @@ OLIVE_NAMESPACE_ENTER
|
||||
|
||||
RenderBackend::RenderBackend(QObject *parent) :
|
||||
QObject(parent),
|
||||
started_(false),
|
||||
viewer_node_(nullptr),
|
||||
copied_viewer_node_(nullptr)
|
||||
divider_(1),
|
||||
render_mode_(RenderMode::kOnline),
|
||||
pix_fmt_(PixelFormat::PIX_FMT_RGBA32F),
|
||||
sample_fmt_(SampleFormat::SAMPLE_FMT_FLT)
|
||||
{
|
||||
// FIXME: Don't create in CLI mode
|
||||
cancel_dialog_ = new RenderCancelDialog(Core::instance()->main_window());
|
||||
}
|
||||
|
||||
bool RenderBackend::Init()
|
||||
void RenderBackend::SetViewerNode(ViewerOutput *viewer_node)
|
||||
{
|
||||
if (started_) {
|
||||
return true;
|
||||
}
|
||||
|
||||
threads_.resize(QThread::idealThreadCount());
|
||||
|
||||
for (int i=0;i<threads_.size();i++) {
|
||||
QThread* thread = new QThread(this);
|
||||
threads_.replace(i, thread);
|
||||
|
||||
// We use low priority to keep the app responsive at all times (GUI thread should always prioritize over this one)
|
||||
thread->start(QThread::IdlePriority);
|
||||
}
|
||||
|
||||
cancel_dialog_->SetWorkerCount(threads_.size());
|
||||
|
||||
started_ = InitInternal();
|
||||
|
||||
// Connects workers and moves them to their respective threads
|
||||
InitWorkers();
|
||||
|
||||
if (!started_) {
|
||||
Close();
|
||||
}
|
||||
|
||||
return started_;
|
||||
}
|
||||
|
||||
void RenderBackend::Close()
|
||||
{
|
||||
if (!started_) {
|
||||
if (viewer_node_ == viewer_node) {
|
||||
return;
|
||||
}
|
||||
|
||||
started_ = false;
|
||||
|
||||
CancelQueue();
|
||||
|
||||
SetViewerNode(nullptr);
|
||||
|
||||
CloseInternal();
|
||||
|
||||
for (int i=0;i<processors_.size();i++) {
|
||||
// Invoke close and quit signals on processor and thread
|
||||
QMetaObject::invokeMethod(processors_.at(i),
|
||||
"Close",
|
||||
Qt::QueuedConnection);
|
||||
|
||||
threads_.at(i)->quit();
|
||||
}
|
||||
|
||||
for (int i=0;i<processors_.size();i++) {
|
||||
threads_.at(i)->wait(); // FIXME: Maximum time in case a thread is stuck?
|
||||
delete threads_.at(i);
|
||||
delete processors_.at(i);
|
||||
}
|
||||
|
||||
threads_.clear();
|
||||
processors_.clear();
|
||||
}
|
||||
|
||||
const QString &RenderBackend::GetError() const
|
||||
{
|
||||
return error_;
|
||||
}
|
||||
|
||||
void RenderBackend::SetViewerNode(ViewerOutput *viewer_node)
|
||||
{
|
||||
if (viewer_node_) {
|
||||
// Clear queue and wait for any currently running actions to complete
|
||||
CancelQueue();
|
||||
|
||||
DisconnectViewer(viewer_node_);
|
||||
// Delete all of our copied nodes
|
||||
video_copy_map_.Clear();
|
||||
audio_copy_map_.Clear();
|
||||
|
||||
copied_graph_.Clear();
|
||||
copied_viewer_node_ = nullptr;
|
||||
node_copy_map_.clear();
|
||||
disconnect(viewer_node_,
|
||||
&ViewerOutput::GraphChangedFrom,
|
||||
this,
|
||||
&RenderBackend::NodeGraphChanged);
|
||||
|
||||
disconnect(viewer_node_->audio_playback_cache(),
|
||||
&AudioPlaybackCache::Invalidated,
|
||||
this,
|
||||
&RenderBackend::AudioCallback);
|
||||
}
|
||||
|
||||
// Set viewer node
|
||||
viewer_node_ = viewer_node;
|
||||
|
||||
if (viewer_node_) {
|
||||
ConnectViewer(viewer_node_);
|
||||
// Start copying viewer
|
||||
video_copy_map_.Init(viewer_node_);
|
||||
audio_copy_map_.Init(viewer_node_);
|
||||
|
||||
RegenerateCacheID();
|
||||
video_copy_map_.Queue(viewer_node_->texture_input());
|
||||
audio_copy_map_.Queue(viewer_node_->samples_input());
|
||||
|
||||
copied_viewer_node_ = static_cast<ViewerOutput*>(viewer_node_->copy());
|
||||
copied_graph_.AddNode(copied_viewer_node_);
|
||||
node_copy_map_.insert(viewer_node_, copied_viewer_node_);
|
||||
video_copy_map_.ProcessQueue();
|
||||
audio_copy_map_.ProcessQueue();
|
||||
|
||||
InvalidateCache(TimeRange(0, RATIONAL_MAX),
|
||||
static_cast<NodeInput*>(viewer_node_->GetInputWithID(GetDependentInput()->id())));
|
||||
connect(viewer_node_,
|
||||
&ViewerOutput::GraphChangedFrom,
|
||||
this,
|
||||
&RenderBackend::NodeGraphChanged);
|
||||
|
||||
connect(viewer_node_->audio_playback_cache(),
|
||||
&AudioPlaybackCache::Invalidated,
|
||||
this,
|
||||
&RenderBackend::AudioCallback);
|
||||
}
|
||||
}
|
||||
|
||||
bool RenderBackend::IsInitiated()
|
||||
{
|
||||
return started_;
|
||||
}
|
||||
|
||||
void RenderBackend::RegenerateCacheID()
|
||||
{
|
||||
QCryptographicHash hash(QCryptographicHash::Sha1);
|
||||
|
||||
if (!viewer_node_
|
||||
|| !GenerateCacheIDInternal(hash)) {
|
||||
cache_id_.clear();
|
||||
CacheIDChangedEvent(QString());
|
||||
return;
|
||||
}
|
||||
|
||||
hash.addData(viewer_node_->uuid().toByteArray());
|
||||
|
||||
QByteArray bytes = hash.result();
|
||||
cache_id_ = bytes.toHex();
|
||||
CacheIDChangedEvent(cache_id_);
|
||||
}
|
||||
|
||||
bool RenderBackend::InitInternal()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void RenderBackend::CloseInternal()
|
||||
{
|
||||
}
|
||||
|
||||
bool RenderBackend::CanRender()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
TimeRange RenderBackend::PopNextFrameFromQueue()
|
||||
{
|
||||
return cache_queue_.takeFirst();
|
||||
}
|
||||
|
||||
rational RenderBackend::GetSequenceLength()
|
||||
{
|
||||
if (viewer_node_ == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return viewer_node_->Length();
|
||||
}
|
||||
|
||||
void RenderBackend::SetError(const QString &error)
|
||||
{
|
||||
error_ = error;
|
||||
}
|
||||
|
||||
void RenderBackend::ConnectViewer(ViewerOutput *node)
|
||||
{
|
||||
Q_UNUSED(node)
|
||||
}
|
||||
|
||||
void RenderBackend::DisconnectViewer(ViewerOutput *node)
|
||||
{
|
||||
Q_UNUSED(node)
|
||||
}
|
||||
|
||||
void RenderBackend::CacheNext()
|
||||
{
|
||||
if (cache_queue_.isEmpty()) {
|
||||
if (AllProcessorsAreAvailable()) {
|
||||
emit QueueComplete();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ViewerIsConnected()
|
||||
|| !CanRender()
|
||||
|| !Init()) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (!input_update_queued_.isEmpty()) {
|
||||
if (!AllProcessorsAreAvailable()) {
|
||||
// To update the inputs, we need all workers to stop
|
||||
return;
|
||||
}
|
||||
|
||||
CopyNodeInputValue(input_update_queued_.takeFirst());
|
||||
}
|
||||
|
||||
Node* node_connected_to_viewer = GetDependentInput()->get_connected_node();
|
||||
|
||||
if (!node_connected_to_viewer) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (RenderWorker* worker, processors_) {
|
||||
if (cache_queue_.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!WorkerIsBusy(worker)) {
|
||||
TimeRange cache_frame = PopNextFrameFromQueue();
|
||||
|
||||
NodeDependency dep = NodeDependency(node_connected_to_viewer,
|
||||
cache_frame);
|
||||
|
||||
// Timestamp this render job
|
||||
qint64 job_time = QDateTime::currentMSecsSinceEpoch();
|
||||
|
||||
// Ensure the job's time is unique (since that's the whole point)
|
||||
// NOTE: This value will be 0 if it doesn't exist, which will never be the result of currentMSecsSinceEpoch so we
|
||||
// can safely assume 0 means it doesn't exist.
|
||||
qint64 existing_job_time = render_job_info_.value(cache_frame);
|
||||
|
||||
if (existing_job_time == job_time) {
|
||||
job_time = existing_job_time + 1;
|
||||
}
|
||||
|
||||
render_job_info_.insert(cache_frame, job_time);
|
||||
|
||||
SetWorkerBusyState(worker, true);
|
||||
cancel_dialog_->WorkerStarted();
|
||||
|
||||
WorkerAboutToStartEvent(worker);
|
||||
|
||||
QMetaObject::invokeMethod(worker,
|
||||
"Render",
|
||||
Qt::QueuedConnection,
|
||||
OLIVE_NS_ARG(NodeDependency, dep),
|
||||
Q_ARG(qint64, job_time));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ViewerOutput *RenderBackend::viewer_node() const
|
||||
{
|
||||
return copied_viewer_node_;
|
||||
}
|
||||
|
||||
void RenderBackend::CancelQueue()
|
||||
{
|
||||
cache_queue_.clear();
|
||||
// FIXME: Implement something better than this...
|
||||
video_copy_map_.thread_pool()->waitForDone();
|
||||
audio_copy_map_.thread_pool()->waitForDone();
|
||||
}
|
||||
|
||||
int busy = 0;
|
||||
for (int i=0;i<processor_busy_state_.size();i++) {
|
||||
if (processor_busy_state_.at(i))
|
||||
busy++;
|
||||
QByteArray HashInternal(Node *node,
|
||||
const VideoRenderingParams ¶ms,
|
||||
const rational &time)
|
||||
{
|
||||
QCryptographicHash hasher(QCryptographicHash::Sha1);
|
||||
|
||||
// Embed video parameters into this hash
|
||||
hasher.addData(reinterpret_cast<const char*>(¶ms.effective_width()), sizeof(int));
|
||||
hasher.addData(reinterpret_cast<const char*>(¶ms.effective_height()), sizeof(int));
|
||||
hasher.addData(reinterpret_cast<const char*>(¶ms.format()), sizeof(PixelFormat::Format));
|
||||
hasher.addData(reinterpret_cast<const char*>(¶ms.mode()), sizeof(RenderMode::Mode));
|
||||
|
||||
node->Hash(hasher, time);
|
||||
|
||||
return hasher.result();
|
||||
}
|
||||
|
||||
QFuture<QByteArray> RenderBackend::Hash(const rational &time)
|
||||
{
|
||||
if (!viewer_node_) {
|
||||
return QFuture<QByteArray>();
|
||||
}
|
||||
|
||||
if (busy) {
|
||||
qDebug() << this << "is waiting for" << busy << "busy workers";
|
||||
return QtConcurrent::run(video_copy_map_.thread_pool(),
|
||||
HashInternal,
|
||||
viewer_node_,
|
||||
video_params(),
|
||||
time);
|
||||
}
|
||||
|
||||
QFuture<FramePtr> RenderBackend::RenderFrame(const rational &time)
|
||||
{
|
||||
if (!viewer_node_) {
|
||||
return QFuture<FramePtr>();
|
||||
}
|
||||
|
||||
cancel_dialog_->RunIfWorkersAreBusy();
|
||||
return QtConcurrent::run(video_copy_map_.thread_pool(),
|
||||
this,
|
||||
&RenderBackend::RenderFrameInternal,
|
||||
time);
|
||||
}
|
||||
|
||||
void RenderBackend::InvalidateCache(const TimeRange &range, NodeInput *from)
|
||||
void RenderBackend::SetDivider(const int ÷r)
|
||||
{
|
||||
// Adjust range to min/max values
|
||||
rational start_range_adj = qMax(rational(0), range.in());
|
||||
rational end_range_adj = qMin(GetSequenceLength(), range.out());
|
||||
divider_ = divider;
|
||||
}
|
||||
|
||||
qDebug() << "Cache invalidated between"
|
||||
<< start_range_adj.toDouble()
|
||||
<< "and"
|
||||
<< end_range_adj.toDouble();
|
||||
void RenderBackend::SetMode(const RenderMode::Mode &mode)
|
||||
{
|
||||
render_mode_ = mode;
|
||||
}
|
||||
|
||||
if (from) {
|
||||
// Queue value update
|
||||
qDebug() << " from" << from->parentNode()->id() << "::" << from->id();
|
||||
QueueValueUpdate(from);
|
||||
void RenderBackend::SetPixelFormat(const PixelFormat::Format &pix_fmt)
|
||||
{
|
||||
pix_fmt_ = pix_fmt;
|
||||
}
|
||||
|
||||
void RenderBackend::SetSampleFormat(const SampleFormat::Format &sample_fmt)
|
||||
{
|
||||
sample_fmt_ = sample_fmt;
|
||||
}
|
||||
|
||||
void RenderBackend::NodeGraphChanged(NodeInput *from, NodeInput *source)
|
||||
{
|
||||
if (from == viewer_node_->texture_input()) {
|
||||
video_copy_map_.Queue(source);
|
||||
} else if (from == viewer_node_->samples_input()) {
|
||||
audio_copy_map_.Queue(source);
|
||||
}
|
||||
}
|
||||
|
||||
void RenderBackend::FootageProcessingEvent(StreamPtr stream, const TimeRange &input_time, NodeValueTable *table) const
|
||||
{
|
||||
if (stream->type() == Stream::kVideo || stream->type() == Stream::kImage) {
|
||||
|
||||
ImageStreamPtr video_stream = std::static_pointer_cast<ImageStream>(stream);
|
||||
rational time_match = (stream->type() == Stream::kImage) ? rational() : input_time.in();
|
||||
QString colorspace_match = video_stream->get_colorspace_match_string();
|
||||
|
||||
NodeValue value;
|
||||
bool found_cache = false;
|
||||
|
||||
if (still_image_cache_.Has(stream.get())) {
|
||||
CachedStill cs = still_image_cache_.Get(stream.get());
|
||||
|
||||
if (cs.colorspace == colorspace_match
|
||||
&& cs.alpha_is_associated == video_stream->premultiplied_alpha()
|
||||
&& cs.divider == video_params_.divider()
|
||||
&& cs.time == time_match) {
|
||||
value = cs.texture;
|
||||
found_cache = true;
|
||||
} else {
|
||||
still_image_cache_.Remove(stream.get());
|
||||
}
|
||||
}
|
||||
|
||||
if (!found_cache) {
|
||||
|
||||
value = GetDataFromStream(stream, input_time);
|
||||
|
||||
still_image_cache_.Add(stream.get(), {value,
|
||||
colorspace_match,
|
||||
video_stream->premultiplied_alpha(),
|
||||
video_params_.divider(),
|
||||
time_match});
|
||||
|
||||
}
|
||||
|
||||
table->Push(value);
|
||||
|
||||
} else if (stream->type() != Stream::kAudio) {
|
||||
|
||||
NodeValue value = GetDataFromStream(stream, input_time);
|
||||
|
||||
table->Push(value);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
NodeValueTable RenderBackend::GenerateBlockTable(const TrackOutput *track, const TimeRange &range) const
|
||||
{
|
||||
if (track->track_type() == Timeline::kTrackTypeAudio) {
|
||||
|
||||
QList<Block*> active_blocks = track->BlocksAtTimeRange(range);
|
||||
|
||||
// All these blocks will need to output to a buffer so we create one here
|
||||
SampleBufferPtr block_range_buffer = SampleBuffer::CreateAllocated(audio_params_,
|
||||
audio_params_.time_to_samples(range.length()));
|
||||
block_range_buffer->fill(0);
|
||||
|
||||
NodeValueTable merged_table;
|
||||
|
||||
// Loop through active blocks retrieving their audio
|
||||
foreach (Block* b, active_blocks) {
|
||||
TimeRange range_for_block(qMax(b->in(), range.in()),
|
||||
qMin(b->out(), range.out()));
|
||||
|
||||
int destination_offset = audio_params_.time_to_samples(range_for_block.in() - range.in());
|
||||
int max_dest_sz = audio_params_.time_to_samples(range_for_block.length());
|
||||
|
||||
// Destination buffer
|
||||
NodeValueTable table = ProcessNode(NodeDependency(b, range_for_block));
|
||||
QVariant sample_val = table.Take(NodeParam::kSamples);
|
||||
SampleBufferPtr samples_from_this_block;
|
||||
|
||||
if (sample_val.isNull()
|
||||
|| !(samples_from_this_block = sample_val.value<SampleBufferPtr>())) {
|
||||
// If we retrieved no samples from this block, do nothing
|
||||
continue;
|
||||
}
|
||||
|
||||
// Stretch samples here
|
||||
rational abs_speed = qAbs(b->speed());
|
||||
|
||||
if (abs_speed != 1) {
|
||||
samples_from_this_block->speed(abs_speed.toDouble());
|
||||
}
|
||||
|
||||
if (b->is_reversed()) {
|
||||
// Reverse the audio buffer
|
||||
samples_from_this_block->reverse();
|
||||
}
|
||||
|
||||
int copy_length = qMin(max_dest_sz, samples_from_this_block->sample_count_per_channel());
|
||||
|
||||
// Copy samples into destination buffer
|
||||
block_range_buffer->set(samples_from_this_block->const_data(), destination_offset, copy_length);
|
||||
|
||||
{
|
||||
// Save waveform to file
|
||||
Block* src_block = static_cast<Block*>(copy_map_->key(b));
|
||||
QDir local_appdata_dir(Config::Current()["DiskCachePath"].toString());
|
||||
QDir waveform_loc = local_appdata_dir.filePath(QStringLiteral("waveform"));
|
||||
waveform_loc.mkpath(".");
|
||||
QString wave_fn(waveform_loc.filePath(QString::number(reinterpret_cast<quintptr>(src_block))));
|
||||
QFile wave_file(wave_fn);
|
||||
|
||||
if (wave_file.open(QFile::ReadWrite)) {
|
||||
// We use S32 as a size-compatible substitute for SampleSummer::Sum which is 4 bytes in size
|
||||
AudioRenderingParams waveform_params(SampleSummer::kSumSampleRate, audio_params_.channel_layout(), SampleFormat::SAMPLE_FMT_S32);
|
||||
int chunk_size = (audio_params().sample_rate() / waveform_params.sample_rate());
|
||||
|
||||
{
|
||||
// Write metadata header
|
||||
SampleSummer::Info info;
|
||||
info.channels = audio_params_.channel_count();
|
||||
wave_file.write(reinterpret_cast<char*>(&info), sizeof(SampleSummer::Info));
|
||||
}
|
||||
|
||||
qint64 start_offset = sizeof(SampleSummer::Info) + waveform_params.time_to_bytes(range_for_block.in() - b->in());
|
||||
qint64 length_offset = waveform_params.time_to_bytes(range_for_block.length());
|
||||
qint64 end_offset = start_offset + length_offset;
|
||||
|
||||
if (wave_file.size() < end_offset) {
|
||||
wave_file.resize(end_offset);
|
||||
}
|
||||
|
||||
wave_file.seek(start_offset);
|
||||
|
||||
for (int i=0;i<samples_from_this_block->sample_count_per_channel();i+=chunk_size) {
|
||||
QVector<SampleSummer::Sum> summary = SampleSummer::SumSamples(samples_from_this_block,
|
||||
i,
|
||||
qMin(chunk_size, samples_from_this_block->sample_count_per_channel() - i));
|
||||
|
||||
wave_file.write(reinterpret_cast<const char*>(summary.constData()),
|
||||
summary.size() * sizeof(SampleSummer::Sum));
|
||||
}
|
||||
|
||||
wave_file.close();
|
||||
|
||||
if (src_block->type() == Block::kClip) {
|
||||
emit static_cast<ClipBlock*>(src_block)->PreviewUpdated();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NodeValueTable::Merge({merged_table, table});
|
||||
}
|
||||
|
||||
merged_table.Push(NodeParam::kSamples, QVariant::fromValue(block_range_buffer));
|
||||
|
||||
return merged_table;
|
||||
|
||||
} else {
|
||||
return NodeTraverser::GenerateBlockTable(track, range);
|
||||
}
|
||||
}
|
||||
|
||||
FramePtr RenderBackend::RenderFrameInternal(const rational &time) const
|
||||
{
|
||||
NodeValueTable table = GenerateTable(viewer_node_,
|
||||
TimeRange(time, time + viewer_node_->video_params().time_base()));
|
||||
|
||||
QVariant texture = table.Get(NodeParam::kTexture);
|
||||
|
||||
FramePtr frame = Frame::Create();
|
||||
frame->set_video_params(video_params());
|
||||
frame->allocate();
|
||||
|
||||
if (texture.isNull()) {
|
||||
memset(frame->data(), 0, frame->allocated_size());
|
||||
} else {
|
||||
TextureToFrame(texture, frame);
|
||||
}
|
||||
|
||||
InvalidateCacheInternal(start_range_adj, end_range_adj);
|
||||
return frame;
|
||||
}
|
||||
|
||||
bool RenderBackend::ViewerIsConnected() const
|
||||
VideoRenderingParams RenderBackend::video_params() const
|
||||
{
|
||||
return viewer_node_;
|
||||
return VideoRenderingParams(viewer_node_->video_params(),
|
||||
pix_fmt_,
|
||||
render_mode_,
|
||||
divider_);
|
||||
}
|
||||
|
||||
const QString &RenderBackend::cache_id() const
|
||||
void RenderBackend::AudioCallback()
|
||||
{
|
||||
return cache_id_;
|
||||
qDebug() << "STUB";
|
||||
/*
|
||||
AudioPlaybackCache* pb_cache = viewer_node_->audio_playback_cache();
|
||||
QThreadPool* thread_pool = audio_copy_map_.thread_pool();
|
||||
|
||||
while (!pb_cache->IsFullyValidated()
|
||||
&& thread_pool->activeThreadCount() < thread_pool->maxThreadCount()) {
|
||||
// FIXME: Trigger a background audio thread
|
||||
TimeRange r = viewer_node_->audio_playback_cache()->GetInvalidatedRanges().first();
|
||||
|
||||
qDebug() << "FIXME: Start rendering audio at:" << r;
|
||||
|
||||
break;
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
void RenderBackend::QueueValueUpdate(NodeInput* from)
|
||||
bool RenderBackend::ConformWaitInfo::operator==(const RenderBackend::ConformWaitInfo &rhs) const
|
||||
{
|
||||
if (!input_update_queued_.isEmpty()) {
|
||||
return rhs.stream == stream
|
||||
&& rhs.stream_time == stream_time
|
||||
&& rhs.affected_range == affected_range;
|
||||
}
|
||||
|
||||
RenderBackend::CopyMap::CopyMap() :
|
||||
original_viewer_(nullptr),
|
||||
copied_viewer_(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
void RenderBackend::CopyMap::Init(ViewerOutput *viewer)
|
||||
{
|
||||
original_viewer_ = viewer;
|
||||
|
||||
copied_viewer_ = static_cast<ViewerOutput*>(original_viewer_->copy());
|
||||
copy_map_.insert(original_viewer_, copied_viewer_);
|
||||
}
|
||||
|
||||
void RenderBackend::CopyMap::Queue(NodeInput *input)
|
||||
{
|
||||
if (!queued_updates_.isEmpty()) {
|
||||
// Remove any inputs that are dependents of this input since they may have been removed since
|
||||
// it was queued
|
||||
QList<Node*> deps = from->GetDependencies();
|
||||
QList<Node*> deps = input->GetDependencies();
|
||||
|
||||
for (int i=0;i<input_update_queued_.size();i++) {
|
||||
if (deps.contains(input_update_queued_.at(i)->parentNode())) {
|
||||
for (int i=0;i<queued_updates_.size();i++) {
|
||||
if (deps.contains(queued_updates_.at(i)->parentNode())) {
|
||||
// We don't need to queue this value since this input supersedes it
|
||||
input_update_queued_.removeAt(i);
|
||||
queued_updates_.removeAt(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input_update_queued_.append(from);
|
||||
queued_updates_.append(input);
|
||||
}
|
||||
|
||||
bool RenderBackend::WorkerIsBusy(RenderWorker *worker) const
|
||||
void RenderBackend::CopyMap::ProcessQueue()
|
||||
{
|
||||
return processor_busy_state_.at(processors_.indexOf(worker));
|
||||
while (!queued_updates_.isEmpty()) {
|
||||
CopyNodeInputValue(queued_updates_.takeFirst());
|
||||
}
|
||||
}
|
||||
|
||||
void RenderBackend::SetWorkerBusyState(RenderWorker *worker, bool busy)
|
||||
void RenderBackend::CopyMap::Clear()
|
||||
{
|
||||
processor_busy_state_.replace(processors_.indexOf(worker), busy);
|
||||
qDeleteAll(copy_map_);
|
||||
copy_map_.clear();
|
||||
|
||||
original_viewer_ = nullptr;
|
||||
copied_viewer_ = nullptr;
|
||||
}
|
||||
|
||||
void RenderBackend::CopyNodeInputValue(NodeInput *input)
|
||||
void RenderBackend::CopyMap::CopyNodeInputValue(NodeInput *input)
|
||||
{
|
||||
// Find our copy of this parameter
|
||||
Node* our_copy_node = node_copy_map_.value(input->parentNode());
|
||||
Node* our_copy_node = copy_map_.value(input->parentNode());
|
||||
NodeInput* our_copy = our_copy_node->GetInputWithID(input->id());
|
||||
|
||||
// Copy the standard/keyframe values between these two inputs
|
||||
@@ -372,8 +451,7 @@ void RenderBackend::CopyNodeInputValue(NodeInput *input)
|
||||
QList<Node*> old_deps = our_copy->GetExclusiveDependencies();
|
||||
|
||||
foreach (Node* i, old_deps) {
|
||||
Node* n = node_copy_map_.take(node_copy_map_.key(i));
|
||||
copied_graph_.TakeNode(n);
|
||||
Node* n = copy_map_.take(copy_map_.key(i));
|
||||
delete n;
|
||||
}
|
||||
|
||||
@@ -395,16 +473,15 @@ void RenderBackend::CopyNodeInputValue(NodeInput *input)
|
||||
}
|
||||
}
|
||||
|
||||
Node* RenderBackend::CopyNodeConnections(Node* src_node)
|
||||
Node* RenderBackend::CopyMap::CopyNodeConnections(Node* src_node)
|
||||
{
|
||||
// Check if this node is already in the map
|
||||
Node* dst_node = node_copy_map_.value(src_node);
|
||||
Node* dst_node = copy_map_.value(src_node);
|
||||
|
||||
// If not, create it now
|
||||
if (!dst_node) {
|
||||
dst_node = src_node->copy();
|
||||
copied_graph_.AddNode(dst_node);
|
||||
node_copy_map_.insert(src_node, dst_node);
|
||||
copy_map_.insert(src_node, dst_node);
|
||||
}
|
||||
|
||||
// Make sure its values are copied
|
||||
@@ -423,10 +500,8 @@ Node* RenderBackend::CopyNodeConnections(Node* src_node)
|
||||
return dst_node;
|
||||
}
|
||||
|
||||
void RenderBackend::CopyNodeMakeConnection(NodeInput* src_input, NodeInput* dst_input)
|
||||
void RenderBackend::CopyMap::CopyNodeMakeConnection(NodeInput* src_input, NodeInput* dst_input)
|
||||
{
|
||||
//qDebug() << "Copying input" << src_input->id() << "from" << src_input->parentNode()->id();
|
||||
|
||||
if (src_input->IsConnected()) {
|
||||
Node* dst_node = CopyNodeConnections(src_input->get_connected_node());
|
||||
|
||||
@@ -437,68 +512,4 @@ void RenderBackend::CopyNodeMakeConnection(NodeInput* src_input, NodeInput* dst_
|
||||
}
|
||||
}
|
||||
|
||||
bool RenderBackend::AllProcessorsAreAvailable() const
|
||||
{
|
||||
foreach (bool busy, processor_busy_state_) {
|
||||
if (busy) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const QVector<QThread *> &RenderBackend::threads()
|
||||
{
|
||||
return threads_;
|
||||
}
|
||||
|
||||
void RenderBackend::InvalidateCacheInternal(const rational &start_range, const rational &end_range)
|
||||
{
|
||||
// Add the range to the list
|
||||
cache_queue_.InsertTimeRange(TimeRange(start_range, end_range));
|
||||
|
||||
CacheNext();
|
||||
}
|
||||
|
||||
void RenderBackend::CacheIDChangedEvent(const QString &id)
|
||||
{
|
||||
Q_UNUSED(id)
|
||||
}
|
||||
|
||||
void RenderBackend::WorkerAboutToStartEvent(RenderWorker *worker)
|
||||
{
|
||||
Q_UNUSED(worker)
|
||||
}
|
||||
|
||||
void RenderBackend::InitWorkers()
|
||||
{
|
||||
for (int i=0;i<processors_.size();i++) {
|
||||
RenderWorker* processor = processors_.at(i);
|
||||
QThread* thread = threads().at(i);
|
||||
|
||||
// Connect to it
|
||||
ConnectWorkerToThis(processor);
|
||||
|
||||
// Connect cancel dialog to it
|
||||
connect(processor, &RenderWorker::CompletedCache, cancel_dialog_, &RenderCancelDialog::WorkerDone, Qt::QueuedConnection);
|
||||
|
||||
// Finally, we can move it to its own thread
|
||||
processor->moveToThread(thread);
|
||||
|
||||
// This function blocks the main thread intentionally. See the documentation for this function to see why.
|
||||
processor->Init();
|
||||
}
|
||||
|
||||
processor_busy_state_.resize(processors_.size());
|
||||
processor_busy_state_.fill(false);
|
||||
}
|
||||
|
||||
bool RenderBackend::FootageWaitInfo::operator==(const RenderBackend::FootageWaitInfo &rhs) const
|
||||
{
|
||||
return rhs.stream == stream
|
||||
&& rhs.stream_time == stream_time
|
||||
&& rhs.affected_range == affected_range;
|
||||
}
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
|
||||
@@ -21,153 +21,126 @@
|
||||
#ifndef RENDERBACKEND_H
|
||||
#define RENDERBACKEND_H
|
||||
|
||||
#include <QLinkedList>
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
|
||||
#include "dialog/rendercancel/rendercancel.h"
|
||||
#include "decodercache.h"
|
||||
#include "node/graph.h"
|
||||
#include "node/output/viewer/viewer.h"
|
||||
#include "renderworker.h"
|
||||
#include "node/traverser.h"
|
||||
#include "render/backend/colorprocessorcache.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
class RenderBackend : public QObject
|
||||
class RenderBackend : public QObject, public NodeTraverser
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
RenderBackend(QObject* parent = nullptr);
|
||||
|
||||
bool Init();
|
||||
|
||||
void Close();
|
||||
|
||||
const QString& GetError() const;
|
||||
|
||||
void SetViewerNode(ViewerOutput* viewer_node);
|
||||
|
||||
bool IsInitiated();
|
||||
|
||||
ViewerOutput* viewer_node() const;
|
||||
|
||||
void CancelQueue();
|
||||
|
||||
public slots:
|
||||
void InvalidateCache(const TimeRange &range, NodeInput *from);
|
||||
QFuture<QByteArray> Hash(const rational& time);
|
||||
|
||||
signals:
|
||||
void QueueComplete();
|
||||
QFuture<FramePtr> RenderFrame(const rational& time);
|
||||
|
||||
void SetDivider(const int& divider);
|
||||
|
||||
void SetMode(const RenderMode::Mode& mode);
|
||||
|
||||
void SetPixelFormat(const PixelFormat::Format& pix_fmt);
|
||||
|
||||
void SetSampleFormat(const SampleFormat::Format& sample_fmt);
|
||||
|
||||
public slots:
|
||||
void NodeGraphChanged(NodeInput *from, NodeInput *source);
|
||||
|
||||
protected:
|
||||
void RegenerateCacheID();
|
||||
virtual void TextureToFrame(const QVariant& texture, FramePtr frame) const = 0;
|
||||
|
||||
virtual bool InitInternal();
|
||||
virtual QVariant FrameToTexture(FramePtr frame) const = 0;
|
||||
|
||||
virtual void CloseInternal();
|
||||
virtual void FootageProcessingEvent(StreamPtr stream, const TimeRange &input_time, NodeValueTable* table) const override;
|
||||
|
||||
virtual bool CanRender();
|
||||
|
||||
virtual TimeRange PopNextFrameFromQueue();
|
||||
|
||||
rational GetSequenceLength();
|
||||
|
||||
const QVector<QThread*>& threads();
|
||||
|
||||
/**
|
||||
* @brief Internal function for generating the cache ID
|
||||
*/
|
||||
virtual bool GenerateCacheIDInternal(QCryptographicHash& hash) = 0;
|
||||
|
||||
virtual void InvalidateCacheInternal(const rational &start_range, const rational &end_range);
|
||||
|
||||
virtual void CacheIDChangedEvent(const QString& id);
|
||||
|
||||
virtual void WorkerAboutToStartEvent(RenderWorker* worker);
|
||||
|
||||
void SetError(const QString& error);
|
||||
|
||||
virtual void ConnectViewer(ViewerOutput* node);
|
||||
virtual void DisconnectViewer(ViewerOutput* node);
|
||||
|
||||
/**
|
||||
* @brief Function called when there are frames in the queue to cache
|
||||
*
|
||||
* This function is NOT thread-safe and should only be called in the main thread.
|
||||
*/
|
||||
void CacheNext();
|
||||
|
||||
void InitWorkers();
|
||||
|
||||
virtual NodeInput* GetDependentInput() = 0;
|
||||
|
||||
virtual void ConnectWorkerToThis(RenderWorker* worker) = 0;
|
||||
|
||||
bool ViewerIsConnected() const;
|
||||
|
||||
const QString& cache_id() const;
|
||||
|
||||
void QueueValueUpdate(NodeInput *from);
|
||||
|
||||
bool AllProcessorsAreAvailable() const;
|
||||
bool WorkerIsBusy(RenderWorker* worker) const;
|
||||
void SetWorkerBusyState(RenderWorker* worker, bool busy);
|
||||
|
||||
TimeRangeList cache_queue_;
|
||||
|
||||
QVector<RenderWorker*> processors_;
|
||||
|
||||
QHash<TimeRange, qint64> render_job_info_;
|
||||
|
||||
QHash<Node*, Node*> node_copy_map_;
|
||||
|
||||
NodeGraph copied_graph_;
|
||||
virtual NodeValueTable GenerateBlockTable(const TrackOutput *track, const TimeRange &range) const override;
|
||||
|
||||
private:
|
||||
void CopyNodeInputValue(NodeInput* input);
|
||||
Node *CopyNodeConnections(Node *src_node);
|
||||
void CopyNodeMakeConnection(NodeInput *src_input, NodeInput *dst_input);
|
||||
|
||||
/**
|
||||
* @brief Internal list of RenderProcessThreads
|
||||
*/
|
||||
QVector<QThread*> threads_;
|
||||
|
||||
/**
|
||||
* @brief Internal variable that contains whether the Renderer has started or not
|
||||
*/
|
||||
bool started_;
|
||||
FramePtr RenderFrameInternal(const rational& time) const;
|
||||
|
||||
/**
|
||||
* @brief Internal reference to attached viewer node
|
||||
*/
|
||||
ViewerOutput* viewer_node_;
|
||||
|
||||
/**
|
||||
* @brief Internal reference to the copied viewer node we made in the compilation process
|
||||
*/
|
||||
ViewerOutput* copied_viewer_node_;
|
||||
|
||||
/**
|
||||
* @brief Error string that can be set in SetError() to handle failures
|
||||
*/
|
||||
QString error_;
|
||||
|
||||
QString cache_id_;
|
||||
|
||||
QList<NodeInput*> input_update_queued_;
|
||||
|
||||
QVector<bool> processor_busy_state_;
|
||||
|
||||
RenderCancelDialog* cancel_dialog_;
|
||||
|
||||
struct FootageWaitInfo {
|
||||
VideoRenderingParams video_params() const;
|
||||
|
||||
class CopyMap {
|
||||
public:
|
||||
CopyMap();
|
||||
|
||||
void Init(ViewerOutput* viewer);
|
||||
|
||||
void Queue(NodeInput* input);
|
||||
|
||||
void ProcessQueue();
|
||||
|
||||
void Clear();
|
||||
|
||||
QThreadPool* thread_pool() {
|
||||
return &thread_pool_;
|
||||
}
|
||||
|
||||
private:
|
||||
void CopyNodeInputValue(NodeInput* input);
|
||||
Node *CopyNodeConnections(Node *src_node);
|
||||
void CopyNodeMakeConnection(NodeInput *src_input, NodeInput *dst_input);
|
||||
|
||||
ViewerOutput* original_viewer_;
|
||||
ViewerOutput* copied_viewer_;
|
||||
QList<NodeInput*> queued_updates_;
|
||||
QHash<Node*, Node*> copy_map_;
|
||||
QThreadPool thread_pool_;
|
||||
|
||||
};
|
||||
|
||||
// VIDEO MEMBERS
|
||||
CopyMap video_copy_map_;
|
||||
int divider_;
|
||||
RenderMode::Mode render_mode_;
|
||||
PixelFormat::Format pix_fmt_;
|
||||
|
||||
ColorProcessorCache color_cache_;
|
||||
|
||||
struct CachedStill {
|
||||
NodeValue texture;
|
||||
QString colorspace;
|
||||
bool alpha_is_associated;
|
||||
int divider;
|
||||
rational time;
|
||||
};
|
||||
|
||||
RenderCache<Stream*, CachedStill> still_image_cache_;
|
||||
|
||||
// AUDIO MEMBERS
|
||||
CopyMap audio_copy_map_;
|
||||
SampleFormat::Format sample_fmt_;
|
||||
|
||||
struct ConformWaitInfo {
|
||||
StreamPtr stream;
|
||||
TimeRange affected_range;
|
||||
rational stream_time;
|
||||
|
||||
bool operator==(const FootageWaitInfo& rhs) const;
|
||||
bool operator==(const ConformWaitInfo& rhs) const;
|
||||
};
|
||||
|
||||
QList<FootageWaitInfo> footage_wait_info_;
|
||||
QList<ConformWaitInfo> footage_wait_info_;
|
||||
|
||||
private slots:
|
||||
void AudioCallback();
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2019 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "renderworker.h"
|
||||
|
||||
#include <QThread>
|
||||
|
||||
#include "node/block/block.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
RenderWorker::RenderWorker(QObject *parent) :
|
||||
QObject(parent),
|
||||
started_(false)
|
||||
{
|
||||
}
|
||||
|
||||
bool RenderWorker::Init()
|
||||
{
|
||||
if (started_) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!(started_ = InitInternal())) {
|
||||
Close();
|
||||
}
|
||||
|
||||
return started_;
|
||||
}
|
||||
|
||||
void RenderWorker::Close()
|
||||
{
|
||||
CloseInternal();
|
||||
|
||||
decoder_cache_.Clear();
|
||||
|
||||
started_ = false;
|
||||
}
|
||||
|
||||
void RenderWorker::Render(NodeDependency path, qint64 job_time)
|
||||
{
|
||||
path_ = path;
|
||||
|
||||
emit CompletedCache(path, RenderInternal(path, job_time), job_time);
|
||||
}
|
||||
|
||||
NodeValueTable RenderWorker::RenderInternal(const NodeDependency &path, const qint64 &job_time)
|
||||
{
|
||||
Q_UNUSED(job_time)
|
||||
|
||||
return ProcessNode(path);
|
||||
}
|
||||
|
||||
void RenderWorker::RunNodeAccelerated(const Node *node, const TimeRange &range, NodeValueDatabase &input_params, NodeValueTable& output_params)
|
||||
{
|
||||
Q_UNUSED(node)
|
||||
Q_UNUSED(range)
|
||||
Q_UNUSED(input_params)
|
||||
Q_UNUSED(output_params)
|
||||
}
|
||||
|
||||
DecoderPtr RenderWorker::ResolveDecoderFromInput(StreamPtr stream)
|
||||
{
|
||||
// Access a map of Node inputs and decoder instances and retrieve a frame!
|
||||
|
||||
DecoderPtr decoder = decoder_cache_.Get(stream.get());
|
||||
|
||||
if (!decoder && stream) {
|
||||
// Create a new Decoder here
|
||||
decoder = Decoder::CreateFromID(stream->footage()->decoder());
|
||||
decoder->set_stream(stream);
|
||||
|
||||
if (decoder->Open()) {
|
||||
decoder_cache_.Add(stream.get(), decoder);
|
||||
} else {
|
||||
decoder = nullptr;
|
||||
qWarning() << "Failed to open decoder for" << stream->footage()->filename() << "::" << stream->index();
|
||||
}
|
||||
}
|
||||
|
||||
return decoder;
|
||||
}
|
||||
|
||||
NodeValue RenderWorker::GetDataFromStream(StreamPtr stream, const TimeRange &input_time)
|
||||
{
|
||||
DecoderPtr decoder = ResolveDecoderFromInput(stream);
|
||||
|
||||
if (decoder) {
|
||||
return FrameToValue(decoder, stream, input_time);
|
||||
}
|
||||
|
||||
return NodeValue();
|
||||
}
|
||||
|
||||
bool RenderWorker::IsStarted()
|
||||
{
|
||||
return started_;
|
||||
}
|
||||
|
||||
void RenderWorker::ProcessNodeEvent(const Node *node, const TimeRange &range, NodeValueDatabase &input_params, NodeValueTable &output_params)
|
||||
{
|
||||
// Check if we have a shader for this output
|
||||
RunNodeAccelerated(node, range, input_params, output_params);
|
||||
}
|
||||
|
||||
const NodeDependency &RenderWorker::CurrentPath() const
|
||||
{
|
||||
return path_;
|
||||
}
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
@@ -1,81 +0,0 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2019 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef RENDERWORKER_H
|
||||
#define RENDERWORKER_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "decodercache.h"
|
||||
#include "node/node.h"
|
||||
#include "node/output/track/track.h"
|
||||
#include "node/traverser.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
class RenderWorker : public QObject, public NodeTraverser
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
RenderWorker(QObject* parent = nullptr);
|
||||
|
||||
bool Init();
|
||||
|
||||
bool IsStarted();
|
||||
|
||||
public slots:
|
||||
void Close();
|
||||
|
||||
void Render(OLIVE_NAMESPACE::NodeDependency path, qint64 job_time);
|
||||
|
||||
signals:
|
||||
void CompletedCache(OLIVE_NAMESPACE::NodeDependency dep, OLIVE_NAMESPACE::NodeValueTable data, qint64 job_time);
|
||||
|
||||
protected:
|
||||
virtual bool InitInternal() = 0;
|
||||
|
||||
virtual void CloseInternal() = 0;
|
||||
|
||||
virtual NodeValueTable RenderInternal(const NodeDependency& CurrentPath, const qint64& job_time);
|
||||
|
||||
virtual void RunNodeAccelerated(const Node *node, const TimeRange& range, NodeValueDatabase &input_params, NodeValueTable &output_params);
|
||||
|
||||
virtual NodeValue FrameToValue(DecoderPtr decoder, StreamPtr stream, const TimeRange &range) = 0;
|
||||
|
||||
virtual void ProcessNodeEvent(const Node *node, const TimeRange &range, NodeValueDatabase &input_params, NodeValueTable &output_params) override;
|
||||
|
||||
DecoderPtr ResolveDecoderFromInput(StreamPtr stream);
|
||||
|
||||
NodeValue GetDataFromStream(StreamPtr stream, const TimeRange& input_time);
|
||||
|
||||
const NodeDependency& CurrentPath() const;
|
||||
|
||||
private:
|
||||
bool started_;
|
||||
|
||||
DecoderCache decoder_cache_;
|
||||
|
||||
NodeDependency path_;
|
||||
|
||||
};
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
|
||||
#endif // RENDERWORKER_H
|
||||
@@ -1,354 +0,0 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2019 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "videorenderbackend.h"
|
||||
|
||||
#include <OpenImageIO/imageio.h>
|
||||
#include <QApplication>
|
||||
#include <QCryptographicHash>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QtMath>
|
||||
|
||||
#include "common/timecodefunctions.h"
|
||||
#include "config/config.h"
|
||||
#include "render/diskmanager.h"
|
||||
#include "render/diskmanager.h"
|
||||
#include "render/pixelformat.h"
|
||||
#include "videorenderworker.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
VideoRenderBackend::VideoRenderBackend(QObject *parent) :
|
||||
RenderBackend(parent),
|
||||
operating_mode_(VideoRenderWorker::kRenderOnly),
|
||||
only_signal_last_frame_requested_(true),
|
||||
limit_caching_(true)
|
||||
{
|
||||
connect(DiskManager::instance(), &DiskManager::DeletedFrame, this, &VideoRenderBackend::FrameRemovedFromDiskCache);
|
||||
}
|
||||
|
||||
void VideoRenderBackend::ConnectViewer(ViewerOutput *node)
|
||||
{
|
||||
connect(node, &ViewerOutput::VideoChangedBetween, this, &VideoRenderBackend::InvalidateCache);
|
||||
connect(node, &ViewerOutput::LengthChanged, this, &VideoRenderBackend::TruncateFrameCacheLength);
|
||||
}
|
||||
|
||||
void VideoRenderBackend::DisconnectViewer(ViewerOutput *node)
|
||||
{
|
||||
disconnect(node, &ViewerOutput::VideoChangedBetween, this, &VideoRenderBackend::InvalidateCache);
|
||||
disconnect(node, &ViewerOutput::LengthChanged, this, &VideoRenderBackend::TruncateFrameCacheLength);
|
||||
|
||||
frame_cache_.Clear();
|
||||
}
|
||||
|
||||
const VideoRenderingParams &VideoRenderBackend::params() const
|
||||
{
|
||||
return params_;
|
||||
}
|
||||
|
||||
void VideoRenderBackend::SetParameters(const VideoRenderingParams& params)
|
||||
{
|
||||
CancelQueue();
|
||||
|
||||
// Set new parameters
|
||||
params_ = params;
|
||||
|
||||
// Handle custom events from derivatives
|
||||
ParamsChangedEvent();
|
||||
|
||||
// Set params on all processors
|
||||
foreach (RenderWorker* worker, processors_) {
|
||||
static_cast<VideoRenderWorker*>(worker)->SetParameters(params_);
|
||||
}
|
||||
|
||||
// Regenerate the cache ID
|
||||
RegenerateCacheID();
|
||||
}
|
||||
|
||||
void VideoRenderBackend::SetOperatingMode(const VideoRenderWorker::OperatingMode &mode)
|
||||
{
|
||||
CancelQueue();
|
||||
|
||||
operating_mode_ = mode;
|
||||
|
||||
foreach (RenderWorker* worker, processors_) {
|
||||
static_cast<VideoRenderWorker*>(worker)->SetOperatingMode(operating_mode_);
|
||||
}
|
||||
}
|
||||
|
||||
void VideoRenderBackend::SetFrameGenerationParams(int width, int height, const QMatrix4x4 &matrix)
|
||||
{
|
||||
foreach (RenderWorker* worker, processors_) {
|
||||
static_cast<VideoRenderWorker*>(worker)->SetFrameGenerationParams(width, height, matrix);
|
||||
}
|
||||
}
|
||||
|
||||
void VideoRenderBackend::SetOnlySignalLastFrameRequested(bool enabled)
|
||||
{
|
||||
only_signal_last_frame_requested_ = enabled;
|
||||
}
|
||||
|
||||
bool VideoRenderBackend::IsRendered(const rational &time) const
|
||||
{
|
||||
TimeRange range(time, time);
|
||||
|
||||
return !TimeIsQueued(range) && !render_job_info_.contains(range);
|
||||
}
|
||||
|
||||
void VideoRenderBackend::SetLimitCaching(bool limit)
|
||||
{
|
||||
limit_caching_ = limit;
|
||||
}
|
||||
|
||||
bool VideoRenderBackend::GenerateCacheIDInternal(QCryptographicHash& hash)
|
||||
{
|
||||
if (!params_.is_valid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Generate an ID that is more or less guaranteed to be unique to this Sequence
|
||||
hash.addData(QString::number(params_.width()).toUtf8());
|
||||
hash.addData(QString::number(params_.height()).toUtf8());
|
||||
hash.addData(QString::number(params_.format()).toUtf8());
|
||||
hash.addData(QString::number(params_.divider()).toUtf8());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void VideoRenderBackend::CacheIDChangedEvent(const QString &id)
|
||||
{
|
||||
frame_cache_.SetCacheID(id);
|
||||
}
|
||||
|
||||
void VideoRenderBackend::ConnectWorkerToThis(RenderWorker *processor)
|
||||
{
|
||||
VideoRenderWorker* video_processor = static_cast<VideoRenderWorker*>(processor);
|
||||
|
||||
video_processor->SetOperatingMode(operating_mode_);
|
||||
|
||||
connect(video_processor, &VideoRenderWorker::HashAlreadyBeingCached, this, &VideoRenderBackend::ThreadSkippedFrame, Qt::QueuedConnection);
|
||||
connect(video_processor, &VideoRenderWorker::CompletedDownload, this, &VideoRenderBackend::ThreadCompletedDownload, Qt::QueuedConnection);
|
||||
connect(video_processor, &VideoRenderWorker::HashAlreadyExists, this, &VideoRenderBackend::ThreadHashAlreadyExists, Qt::QueuedConnection);
|
||||
connect(video_processor, &VideoRenderWorker::GeneratedFrame, this, &VideoRenderBackend::GeneratedFrame, Qt::QueuedConnection);
|
||||
connect(video_processor, &VideoRenderWorker::GeneratedFrame, this, &VideoRenderBackend::ThreadGeneratedFrame, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void VideoRenderBackend::InvalidateCacheInternal(const rational &start_range, const rational &end_range)
|
||||
{
|
||||
TimeRange invalidated(start_range, end_range);
|
||||
|
||||
if (limit_caching_) {
|
||||
invalidated_.InsertTimeRange(invalidated);
|
||||
} else {
|
||||
RenderBackend::InvalidateCacheInternal(start_range, end_range);
|
||||
}
|
||||
|
||||
emit RangeInvalidated(invalidated);
|
||||
}
|
||||
|
||||
void VideoRenderBackend::RenderFrame(const rational &time)
|
||||
{
|
||||
RenderBackend::InvalidateCacheInternal(time, time);
|
||||
}
|
||||
|
||||
VideoRenderFrameCache *VideoRenderBackend::frame_cache()
|
||||
{
|
||||
return &frame_cache_;
|
||||
}
|
||||
|
||||
QString VideoRenderBackend::GetCachedFrame(const rational &time)
|
||||
{
|
||||
UpdateLastRequestedTime(time);
|
||||
|
||||
if (viewer_node() == nullptr) {
|
||||
// Nothing is connected - nothing to show or render
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (cache_id().isEmpty()) {
|
||||
qWarning() << "No cache ID";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!params_.is_valid()) {
|
||||
qWarning() << "Invalid parameters";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Find frame in map
|
||||
QByteArray frame_hash = frame_cache_.TimeToHash(time);
|
||||
|
||||
if (!frame_hash.isEmpty()) {
|
||||
DiskManager::instance()->Accessed(frame_hash);
|
||||
|
||||
return frame_cache_.CachePathName(frame_hash, params_.format());
|
||||
}
|
||||
|
||||
return QString();
|
||||
}
|
||||
|
||||
void VideoRenderBackend::UpdateLastRequestedTime(const rational &time)
|
||||
{
|
||||
last_time_requested_ = time;
|
||||
}
|
||||
|
||||
NodeInput *VideoRenderBackend::GetDependentInput()
|
||||
{
|
||||
return viewer_node()->texture_input();
|
||||
}
|
||||
|
||||
bool VideoRenderBackend::CanRender()
|
||||
{
|
||||
return params_.is_valid();
|
||||
}
|
||||
|
||||
TimeRange VideoRenderBackend::PopNextFrameFromQueue()
|
||||
{
|
||||
rational earliest_frame = RATIONAL_MAX;
|
||||
|
||||
// Find earliest frame in the cache queue
|
||||
foreach (const TimeRange& range, cache_queue_) {
|
||||
if (range.in() < earliest_frame) {
|
||||
earliest_frame = range.in();
|
||||
}
|
||||
}
|
||||
|
||||
// Snap this frame to the timebase
|
||||
rational snapped_frame = Timecode::snap_time_to_timebase(earliest_frame, params_.time_base());
|
||||
rational next_frame;
|
||||
|
||||
if (snapped_frame > earliest_frame) {
|
||||
next_frame = snapped_frame;
|
||||
snapped_frame -= params_.time_base();
|
||||
} else {
|
||||
next_frame = snapped_frame + params_.time_base();
|
||||
}
|
||||
|
||||
TimeRange frame_range(snapped_frame, next_frame);
|
||||
|
||||
// Remove this particular frame from the queue
|
||||
cache_queue_.RemoveTimeRange(frame_range);
|
||||
|
||||
// Remove this particular frame from missing frames
|
||||
invalidated_.RemoveTimeRange(frame_range);
|
||||
|
||||
// Return the snapped frame
|
||||
return TimeRange(frame_range.in(), frame_range.in());
|
||||
}
|
||||
|
||||
void VideoRenderBackend::ThreadCompletedDownload(NodeDependency dep, qint64 job_time, QByteArray hash)
|
||||
{
|
||||
SetWorkerBusyState(static_cast<RenderWorker*>(sender()), false);
|
||||
|
||||
SetFrameHash(dep, hash, job_time);
|
||||
|
||||
QList<rational> hashes_with_time = frame_cache()->FramesWithHash(hash);
|
||||
|
||||
foreach (const rational& t, hashes_with_time) {
|
||||
emit CachedTimeReady(t, job_time);
|
||||
}
|
||||
|
||||
// Queue up a new frame for this worker
|
||||
CacheNext();
|
||||
}
|
||||
|
||||
void VideoRenderBackend::ThreadSkippedFrame(NodeDependency dep, qint64 job_time, QByteArray hash)
|
||||
{
|
||||
SetWorkerBusyState(static_cast<RenderWorker*>(sender()), false);
|
||||
|
||||
if (SetFrameHash(dep, hash, job_time)
|
||||
&& frame_cache_.HasHash(hash, params_.format())) {
|
||||
emit CachedTimeReady(dep.in(), job_time);
|
||||
}
|
||||
|
||||
// Queue up a new frame for this worker
|
||||
CacheNext();
|
||||
}
|
||||
|
||||
void VideoRenderBackend::ThreadHashAlreadyExists(NodeDependency dep, qint64 job_time, QByteArray hash)
|
||||
{
|
||||
SetWorkerBusyState(static_cast<RenderWorker*>(sender()), false);
|
||||
|
||||
if (SetFrameHash(dep, hash, job_time)) {
|
||||
emit CachedTimeReady(dep.in(), job_time);
|
||||
}
|
||||
|
||||
// Queue up a new frame for this worker
|
||||
CacheNext();
|
||||
}
|
||||
|
||||
void VideoRenderBackend::ThreadGeneratedFrame()
|
||||
{
|
||||
SetWorkerBusyState(static_cast<RenderWorker*>(sender()), false);
|
||||
|
||||
CacheNext();
|
||||
}
|
||||
|
||||
void VideoRenderBackend::TruncateFrameCacheLength(const rational &length)
|
||||
{
|
||||
// Remove frames after this time code if it's changed
|
||||
frame_cache_.Truncate(length);
|
||||
|
||||
invalidated_.RemoveTimeRange(TimeRange(length, RATIONAL_MAX));
|
||||
|
||||
// If the playhead is past the length, update the viewer to a null texture because it won't be cached through the
|
||||
// queue, but will now be a null texture
|
||||
if (last_time_requested_ >= length) {
|
||||
emit CachedTimeReady(last_time_requested_, QDateTime::currentMSecsSinceEpoch());
|
||||
}
|
||||
}
|
||||
|
||||
void VideoRenderBackend::FrameRemovedFromDiskCache(const QByteArray &hash)
|
||||
{
|
||||
QList<rational> deleted_frames = frame_cache()->TakeFramesWithHash(hash);
|
||||
|
||||
foreach (const rational& frame, deleted_frames) {
|
||||
TimeRange invalidated(frame, frame+params_.time_base());
|
||||
|
||||
invalidated_.InsertTimeRange(invalidated);
|
||||
|
||||
emit RangeInvalidated(invalidated);
|
||||
}
|
||||
}
|
||||
|
||||
bool VideoRenderBackend::TimeIsQueued(const TimeRange &time) const
|
||||
{
|
||||
return cache_queue_.ContainsTimeRange(time, true, false);
|
||||
}
|
||||
|
||||
bool VideoRenderBackend::JobIsCurrent(const NodeDependency &dep, const qint64& job_time) const
|
||||
{
|
||||
return (render_job_info_.value(dep.range()) == job_time && !TimeIsQueued(dep.range()));
|
||||
}
|
||||
|
||||
bool VideoRenderBackend::SetFrameHash(const NodeDependency &dep, const QByteArray &hash, const qint64& job_time)
|
||||
{
|
||||
if (JobIsCurrent(dep, job_time)) {
|
||||
frame_cache_.SetHash(dep.in(), hash);
|
||||
render_job_info_.remove(dep.range());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
@@ -1,150 +0,0 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2019 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef VIDEORENDERERBACKEND_H
|
||||
#define VIDEORENDERERBACKEND_H
|
||||
|
||||
#include <QLinkedList>
|
||||
|
||||
#include "colorprocessorcache.h"
|
||||
#include "node/output/viewer/viewer.h"
|
||||
#include "renderbackend.h"
|
||||
#include "render/pixelformat.h"
|
||||
#include "render/rendermodes.h"
|
||||
#include "videorenderframecache.h"
|
||||
#include "videorenderworker.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
/**
|
||||
* @brief A multithreaded OpenGL based renderer for node systems
|
||||
*/
|
||||
class VideoRenderBackend : public RenderBackend
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
/**
|
||||
* @brief Renderer Constructor
|
||||
*
|
||||
* Constructing a Renderer object will not start any threads/backend on its own. Use Start() to do this and Stop()
|
||||
* when the Renderer is about to be destroyed.
|
||||
*/
|
||||
VideoRenderBackend(QObject* parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Set parameters of the Renderer
|
||||
*
|
||||
* The Renderer owns the buffers that are used in the rendering process and this function sets the kind of buffers
|
||||
* to use. The Renderer must be stopped when calling this function.
|
||||
*/
|
||||
void SetParameters(const VideoRenderingParams ¶ms);
|
||||
|
||||
void SetOperatingMode(const VideoRenderWorker::OperatingMode& mode);
|
||||
|
||||
void SetFrameGenerationParams(int width, int height, const QMatrix4x4& matrix);
|
||||
|
||||
void SetOnlySignalLastFrameRequested(bool enabled);
|
||||
|
||||
bool IsRendered(const rational& time) const;
|
||||
|
||||
void SetLimitCaching(bool limit);
|
||||
|
||||
QString GetCachedFrame(const rational& time);
|
||||
|
||||
void UpdateLastRequestedTime(const rational& time);
|
||||
|
||||
VideoRenderFrameCache* frame_cache();
|
||||
|
||||
const VideoRenderingParams& params() const;
|
||||
|
||||
void RenderFrame(const rational& time);
|
||||
|
||||
protected:
|
||||
struct HashTimeMapping {
|
||||
rational time;
|
||||
QByteArray hash;
|
||||
};
|
||||
|
||||
virtual void ConnectViewer(ViewerOutput* node) override;
|
||||
|
||||
virtual void DisconnectViewer(ViewerOutput* node) override;
|
||||
|
||||
virtual NodeInput* GetDependentInput() override;
|
||||
|
||||
virtual bool CanRender() override;
|
||||
|
||||
virtual TimeRange PopNextFrameFromQueue() override;
|
||||
|
||||
/**
|
||||
* @brief Internal function for generating the cache ID
|
||||
*/
|
||||
virtual bool GenerateCacheIDInternal(QCryptographicHash& hash) override;
|
||||
|
||||
virtual void CacheIDChangedEvent(const QString& id) override;
|
||||
|
||||
virtual void ConnectWorkerToThis(RenderWorker* processor) override;
|
||||
|
||||
virtual void InvalidateCacheInternal(const rational &start_range, const rational &end_range) override;
|
||||
|
||||
virtual void ParamsChangedEvent(){}
|
||||
|
||||
VideoRenderWorker::OperatingMode operating_mode_;
|
||||
|
||||
signals:
|
||||
void CachedTimeReady(const rational& time, qint64 job_time);
|
||||
|
||||
void RangeInvalidated(const TimeRange& range);
|
||||
|
||||
void GeneratedFrame(FramePtr frame);
|
||||
|
||||
private:
|
||||
bool TimeIsQueued(const TimeRange &time) const;
|
||||
|
||||
bool JobIsCurrent(const NodeDependency &dep, const qint64& job_time) const;
|
||||
|
||||
bool SetFrameHash(const NodeDependency& dep, const QByteArray& hash, const qint64& job_time);
|
||||
|
||||
VideoRenderingParams params_;
|
||||
|
||||
VideoRenderFrameCache frame_cache_;
|
||||
|
||||
TimeRangeList invalidated_;
|
||||
|
||||
rational last_time_requested_;
|
||||
|
||||
bool only_signal_last_frame_requested_;
|
||||
|
||||
bool limit_caching_;
|
||||
|
||||
private slots:
|
||||
void ThreadCompletedDownload(NodeDependency dep, qint64 job_time, QByteArray hash);
|
||||
void ThreadSkippedFrame(NodeDependency dep, qint64 job_time, QByteArray hash);
|
||||
void ThreadHashAlreadyExists(NodeDependency dep, qint64 job_time, QByteArray hash);
|
||||
void ThreadGeneratedFrame();
|
||||
|
||||
void TruncateFrameCacheLength(const rational& length);
|
||||
|
||||
void FrameRemovedFromDiskCache(const QByteArray& hash);
|
||||
|
||||
};
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
|
||||
#endif // VIDEORENDERERBACKEND_H
|
||||
@@ -1,251 +0,0 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2019 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "videorenderworker.h"
|
||||
|
||||
#include "common/define.h"
|
||||
#include "common/functiontimer.h"
|
||||
#include "node/block/transition/transition.h"
|
||||
#include "node/node.h"
|
||||
#include "project/project.h"
|
||||
#include "render/pixelformat.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
VideoRenderWorker::VideoRenderWorker(VideoRenderFrameCache *frame_cache, QObject *parent) :
|
||||
RenderWorker(parent),
|
||||
frame_cache_(frame_cache),
|
||||
operating_mode_(kHashRenderCache)
|
||||
{
|
||||
}
|
||||
|
||||
const VideoRenderingParams &VideoRenderWorker::video_params()
|
||||
{
|
||||
return video_params_;
|
||||
}
|
||||
|
||||
void VideoRenderWorker::TextureToBuffer(const QVariant &texture, void *buffer, int linesize)
|
||||
{
|
||||
TextureToBuffer(texture,
|
||||
video_params_.effective_width(),
|
||||
video_params_.effective_height(),
|
||||
QMatrix4x4(),
|
||||
buffer,
|
||||
linesize);
|
||||
}
|
||||
|
||||
NodeValueTable VideoRenderWorker::RenderInternal(const NodeDependency& path, const qint64 &job_time)
|
||||
{
|
||||
// Get hash of node graph
|
||||
// We use SHA-1 for speed (benchmarks show it's the fastest hash available to us)
|
||||
QByteArray hash;
|
||||
if (operating_mode_ & kHashOnly) {
|
||||
QCryptographicHash hasher(QCryptographicHash::Sha1);
|
||||
|
||||
// Embed video parameters into this hash
|
||||
int vwidth = video_params_.effective_width();
|
||||
int vheight = video_params_.effective_height();
|
||||
PixelFormat::Format vfmt = video_params_.format();
|
||||
RenderMode::Mode vmode = video_params_.mode();
|
||||
|
||||
hasher.addData(reinterpret_cast<const char*>(&vwidth), sizeof(int));
|
||||
hasher.addData(reinterpret_cast<const char*>(&vheight), sizeof(int));
|
||||
hasher.addData(reinterpret_cast<const char*>(&vfmt), sizeof(PixelFormat::Format));
|
||||
hasher.addData(reinterpret_cast<const char*>(&vmode), sizeof(RenderMode::Mode));
|
||||
|
||||
path.node()->Hash(hasher, path.in());
|
||||
hash = hasher.result();
|
||||
}
|
||||
|
||||
NodeValueTable value;
|
||||
|
||||
if (!(operating_mode_ & kRenderOnly)) {
|
||||
|
||||
// Emit only the hash
|
||||
emit CompletedDownload(path, job_time, hash);
|
||||
|
||||
} else if ((operating_mode_ & kHashOnly) && frame_cache_->HasHash(hash, video_params_.format())) {
|
||||
|
||||
// We've already cached this hash, no need to continue
|
||||
emit HashAlreadyExists(path, job_time, hash);
|
||||
|
||||
} else if (!(operating_mode_ & kHashOnly) || frame_cache_->TryCache(hash)) {
|
||||
|
||||
// This hash is available for us to cache, start traversing graph
|
||||
value = ProcessNode(path);
|
||||
|
||||
// Find texture in hash
|
||||
QVariant texture = value.Get(NodeParam::kTexture);
|
||||
|
||||
// If we actually have a texture, download it into the disk cache
|
||||
if (!texture.isNull() || (!(operating_mode_ & kDownloadOnly))) {
|
||||
Download(hash, path.in(), texture);
|
||||
}
|
||||
|
||||
frame_cache_->RemoveHashFromCurrentlyCaching(hash);
|
||||
|
||||
// Signal that this job is complete
|
||||
if (operating_mode_ & kDownloadOnly) {
|
||||
emit CompletedDownload(path, job_time, hash);
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// Another thread must be caching this already, nothing to be done
|
||||
emit HashAlreadyBeingCached(path, job_time, hash);
|
||||
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
void VideoRenderWorker::FootageProcessingEvent(StreamPtr stream, const TimeRange &input_time, NodeValueTable *table)
|
||||
{
|
||||
if (stream->type() != Stream::kVideo && stream->type() != Stream::kImage) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImageStreamPtr video_stream = std::static_pointer_cast<ImageStream>(stream);
|
||||
rational time_match = (stream->type() == Stream::kImage) ? rational() : input_time.in();
|
||||
QString colorspace_match = video_stream->get_colorspace_match_string();
|
||||
|
||||
NodeValue value;
|
||||
bool found_cache = false;
|
||||
|
||||
if (still_image_cache_.Has(stream.get())) {
|
||||
CachedStill cs = still_image_cache_.Get(stream.get());
|
||||
|
||||
if (cs.colorspace == colorspace_match
|
||||
&& cs.alpha_is_associated == video_stream->premultiplied_alpha()
|
||||
&& cs.divider == video_params_.divider()
|
||||
&& cs.time == time_match) {
|
||||
value = cs.texture;
|
||||
found_cache = true;
|
||||
} else {
|
||||
still_image_cache_.Remove(stream.get());
|
||||
}
|
||||
}
|
||||
|
||||
if (!found_cache) {
|
||||
|
||||
value = GetDataFromStream(stream, input_time);
|
||||
|
||||
still_image_cache_.Add(stream.get(), {value,
|
||||
colorspace_match,
|
||||
video_stream->premultiplied_alpha(),
|
||||
video_params_.divider(),
|
||||
time_match});
|
||||
|
||||
}
|
||||
|
||||
table->Push(value);
|
||||
}
|
||||
|
||||
void VideoRenderWorker::SetParameters(const VideoRenderingParams &video_params)
|
||||
{
|
||||
video_params_ = video_params;
|
||||
|
||||
if (IsStarted()) {
|
||||
ResizeDownloadBuffer();
|
||||
}
|
||||
|
||||
ParametersChangedEvent();
|
||||
}
|
||||
|
||||
void VideoRenderWorker::SetOperatingMode(const VideoRenderWorker::OperatingMode &mode)
|
||||
{
|
||||
operating_mode_ = mode;
|
||||
}
|
||||
|
||||
void VideoRenderWorker::SetFrameGenerationParams(int width, int height, const QMatrix4x4 &matrix)
|
||||
{
|
||||
frame_gen_params_ = VideoRenderingParams(width,
|
||||
height,
|
||||
video_params_.time_base(),
|
||||
video_params_.format(),
|
||||
video_params_.mode(),
|
||||
video_params_.divider());
|
||||
frame_gen_mat_ = matrix;
|
||||
}
|
||||
|
||||
bool VideoRenderWorker::InitInternal()
|
||||
{
|
||||
if (video_params_.is_valid()) {
|
||||
ResizeDownloadBuffer();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void VideoRenderWorker::CloseInternal()
|
||||
{
|
||||
download_buffer_.clear();
|
||||
}
|
||||
|
||||
void VideoRenderWorker::Download(const QByteArray& hash, const rational& time, QVariant texture)
|
||||
{
|
||||
if (operating_mode_ & kDownloadOnly) {
|
||||
|
||||
TextureToBuffer(texture, download_buffer_.data(), 0);
|
||||
|
||||
frame_cache_->SaveCacheFrame(hash,
|
||||
download_buffer_.data(),
|
||||
VideoRenderingParams(video_params_.effective_width(),
|
||||
video_params_.effective_height(),
|
||||
video_params_.format()));
|
||||
|
||||
} else {
|
||||
|
||||
FramePtr frame = Frame::Create();
|
||||
|
||||
if (frame_gen_params_.is_valid()) {
|
||||
frame->set_video_params(frame_gen_params_);
|
||||
} else {
|
||||
frame->set_video_params(VideoRenderingParams(video_params_.effective_width(),
|
||||
video_params_.effective_height(),
|
||||
video_params_.format()));
|
||||
}
|
||||
|
||||
frame->allocate();
|
||||
|
||||
if (texture.isNull()) {
|
||||
memset(frame->data(), 0, frame->allocated_size());
|
||||
} else {
|
||||
TextureToBuffer(texture, frame->width(), frame->height(), frame_gen_mat_, frame->data(), frame->linesize_pixels());
|
||||
}
|
||||
|
||||
frame->set_timestamp(time);
|
||||
|
||||
emit GeneratedFrame(frame);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void VideoRenderWorker::ResizeDownloadBuffer()
|
||||
{
|
||||
download_buffer_.resize(PixelFormat::GetBufferSize(video_params_.format(), video_params_.effective_width(), video_params_.effective_height()));
|
||||
}
|
||||
|
||||
ColorProcessorCache *VideoRenderWorker::color_cache()
|
||||
{
|
||||
return &color_cache_;
|
||||
}
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
@@ -1,140 +0,0 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2019 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef VIDEORENDERWORKER_H
|
||||
#define VIDEORENDERWORKER_H
|
||||
|
||||
#include <QCryptographicHash>
|
||||
#include <QMatrix4x4>
|
||||
|
||||
#include "colorprocessorcache.h"
|
||||
#include "node/dependency.h"
|
||||
#include "render/videoparams.h"
|
||||
#include "renderworker.h"
|
||||
#include "videorenderframecache.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
class VideoRenderWorker : public RenderWorker {
|
||||
Q_OBJECT
|
||||
public:
|
||||
/**
|
||||
* @brief VideoRenderWorker uses hashes to recognize frames that are identical to others in the render queue
|
||||
*
|
||||
* This mode can modify the behavior of the worker, either to disable hash verification or only generate hashes and
|
||||
* not render. These are only used in the context of exporting.
|
||||
*
|
||||
* These are also flags that can be or'd together, mostly for the convenience of checking which functionalities are
|
||||
* disabled and enabled.
|
||||
*/
|
||||
enum OperatingMode {
|
||||
/// Generate hashes but don't render or download anything
|
||||
kHashOnly = 0x1,
|
||||
|
||||
/// Render but don't download or hash
|
||||
kRenderOnly = 0x2,
|
||||
|
||||
/// Enable download (NEVER USE THIS ON ITS OWN, this is only here for checking flags, download-only mode makes no sense)
|
||||
kDownloadOnly = 0x4,
|
||||
|
||||
/// Use hash verification and render, but don't cache any frames to disk
|
||||
kHashAndRenderOnly = 0x3,
|
||||
|
||||
/// Render and download, but don't use hash verification
|
||||
kRenderAndCacheOnly = 0x6,
|
||||
|
||||
/// Render and use hashes to identify exact matches (default)
|
||||
kHashRenderCache = 0x7
|
||||
};
|
||||
|
||||
VideoRenderWorker(VideoRenderFrameCache* frame_cache, QObject* parent = nullptr);
|
||||
|
||||
void SetParameters(const VideoRenderingParams& video_params);
|
||||
|
||||
void SetOperatingMode(const OperatingMode& mode);
|
||||
|
||||
void SetFrameGenerationParams(int width, int height, const QMatrix4x4 &matrix);
|
||||
|
||||
signals:
|
||||
void CompletedDownload(NodeDependency path, qint64 job_time, QByteArray hash);
|
||||
|
||||
void HashAlreadyBeingCached(NodeDependency path, qint64 job_time, QByteArray hash);
|
||||
|
||||
void HashAlreadyExists(NodeDependency path, qint64 job_time, QByteArray hash);
|
||||
|
||||
void GeneratedFrame(FramePtr frame);
|
||||
|
||||
void Aborted();
|
||||
|
||||
protected:
|
||||
virtual bool InitInternal() override;
|
||||
|
||||
virtual void CloseInternal() override;
|
||||
|
||||
const VideoRenderingParams& video_params();
|
||||
|
||||
virtual void ParametersChangedEvent(){}
|
||||
|
||||
void TextureToBuffer(const QVariant& texture, void *buffer, int linesize);
|
||||
|
||||
virtual void TextureToBuffer(const QVariant& texture, int width, int height, const QMatrix4x4& matrix, void *buffer, int linesize) = 0;
|
||||
|
||||
virtual NodeValueTable RenderInternal(const NodeDependency& CurrentPath, const qint64& job_time) override;
|
||||
|
||||
virtual void FootageProcessingEvent(StreamPtr stream, const TimeRange &input_time, NodeValueTable* table) override;
|
||||
|
||||
ColorProcessorCache* color_cache();
|
||||
|
||||
private:
|
||||
void Download(const QByteArray &hash, const rational &time, QVariant texture);
|
||||
|
||||
void ResizeDownloadBuffer();
|
||||
|
||||
VideoRenderingParams frame_gen_params_;
|
||||
|
||||
QMatrix4x4 frame_gen_mat_;
|
||||
|
||||
VideoRenderingParams video_params_;
|
||||
|
||||
VideoRenderFrameCache* frame_cache_;
|
||||
|
||||
ColorProcessorCache color_cache_;
|
||||
|
||||
QByteArray download_buffer_;
|
||||
|
||||
OperatingMode operating_mode_;
|
||||
|
||||
struct CachedStill {
|
||||
NodeValue texture;
|
||||
QString colorspace;
|
||||
bool alpha_is_associated;
|
||||
int divider;
|
||||
rational time;
|
||||
};
|
||||
|
||||
RenderCache<Stream*, CachedStill> still_image_cache_;
|
||||
|
||||
private slots:
|
||||
|
||||
};
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
|
||||
#endif // VIDEORENDERWORKER_H
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
***/
|
||||
|
||||
#include "videorenderframecache.h"
|
||||
#include "framehashcache.h"
|
||||
|
||||
#include <OpenEXR/ImfFloatAttribute.h>
|
||||
#include <OpenEXR/ImfInputFile.h>
|
||||
@@ -33,80 +33,24 @@
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
void VideoRenderFrameCache::Clear()
|
||||
{
|
||||
time_hash_map_.clear();
|
||||
|
||||
{
|
||||
QMutexLocker locker(¤tly_caching_lock_);
|
||||
currently_caching_list_.clear();
|
||||
}
|
||||
|
||||
cache_id_.clear();
|
||||
}
|
||||
|
||||
bool VideoRenderFrameCache::HasHash(const QByteArray &hash, const PixelFormat::Format& format)
|
||||
{
|
||||
return QFileInfo::exists(CachePathName(hash, format)) && !IsCaching(hash);
|
||||
}
|
||||
|
||||
bool VideoRenderFrameCache::IsCaching(const QByteArray &hash)
|
||||
{
|
||||
QMutexLocker locker(¤tly_caching_lock_);
|
||||
|
||||
return currently_caching_list_.contains(hash);
|
||||
}
|
||||
|
||||
bool VideoRenderFrameCache::TryCache(const QByteArray &hash)
|
||||
{
|
||||
QMutexLocker locker(¤tly_caching_lock_);
|
||||
|
||||
if (!currently_caching_list_.contains(hash)) {
|
||||
currently_caching_list_.append(hash);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void VideoRenderFrameCache::SetCacheID(const QString &id)
|
||||
{
|
||||
Clear();
|
||||
|
||||
cache_id_ = id;
|
||||
}
|
||||
|
||||
QByteArray VideoRenderFrameCache::TimeToHash(const rational &time) const
|
||||
QByteArray FrameHashCache::GetHash(const rational &time) const
|
||||
{
|
||||
return time_hash_map_.value(time);
|
||||
}
|
||||
|
||||
void VideoRenderFrameCache::SetHash(const rational &time, const QByteArray &hash)
|
||||
void FrameHashCache::SetHash(const rational &time, const QByteArray &hash)
|
||||
{
|
||||
time_hash_map_.insert(time, hash);
|
||||
|
||||
Validate(TimeRange(time, time + timebase_));
|
||||
}
|
||||
|
||||
void VideoRenderFrameCache::Truncate(const rational &time)
|
||||
void FrameHashCache::SetTimebase(const rational &tb)
|
||||
{
|
||||
QMap<rational, QByteArray>::iterator i = time_hash_map_.begin();
|
||||
|
||||
while (i != time_hash_map_.end()) {
|
||||
if (i.key() >= time) {
|
||||
i = time_hash_map_.erase(i);
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
timebase_ = tb;
|
||||
}
|
||||
|
||||
void VideoRenderFrameCache::RemoveHashFromCurrentlyCaching(const QByteArray &hash)
|
||||
{
|
||||
QMutexLocker locker(¤tly_caching_lock_);
|
||||
|
||||
currently_caching_list_.removeOne(hash);
|
||||
}
|
||||
|
||||
QList<rational> VideoRenderFrameCache::FramesWithHash(const QByteArray &hash) const
|
||||
QList<rational> FrameHashCache::GetFramesWithHash(const QByteArray &hash) const
|
||||
{
|
||||
QList<rational> times;
|
||||
|
||||
@@ -121,7 +65,7 @@ QList<rational> VideoRenderFrameCache::FramesWithHash(const QByteArray &hash) co
|
||||
return times;
|
||||
}
|
||||
|
||||
QList<rational> VideoRenderFrameCache::TakeFramesWithHash(const QByteArray &hash)
|
||||
QList<rational> FrameHashCache::TakeFramesWithHash(const QByteArray &hash)
|
||||
{
|
||||
QList<rational> times;
|
||||
|
||||
@@ -140,12 +84,12 @@ QList<rational> VideoRenderFrameCache::TakeFramesWithHash(const QByteArray &hash
|
||||
return times;
|
||||
}
|
||||
|
||||
const QMap<rational, QByteArray> &VideoRenderFrameCache::time_hash_map() const
|
||||
const QMap<rational, QByteArray> &FrameHashCache::time_hash_map() const
|
||||
{
|
||||
return time_hash_map_;
|
||||
}
|
||||
|
||||
QString VideoRenderFrameCache::GetFormatExtension(const PixelFormat::Format &f)
|
||||
QString FrameHashCache::GetFormatExtension(const PixelFormat::Format &f)
|
||||
{
|
||||
if (PixelFormat::FormatIsFloat(f)) {
|
||||
// EXR is only fast with float buffers so we only use it for those
|
||||
@@ -160,9 +104,9 @@ QString VideoRenderFrameCache::GetFormatExtension(const PixelFormat::Format &f)
|
||||
}
|
||||
}
|
||||
|
||||
void VideoRenderFrameCache::SaveCacheFrame(const QByteArray& hash,
|
||||
char* data,
|
||||
const VideoRenderingParams& vparam) const
|
||||
void FrameHashCache::SaveCacheFrame(const QByteArray& hash,
|
||||
char* data,
|
||||
const VideoRenderingParams& vparam)
|
||||
{
|
||||
QString fn = CachePathName(hash, vparam.format());
|
||||
|
||||
@@ -172,7 +116,35 @@ void VideoRenderFrameCache::SaveCacheFrame(const QByteArray& hash,
|
||||
}
|
||||
}
|
||||
|
||||
QString VideoRenderFrameCache::CachePathName(const QByteArray& hash, const PixelFormat::Format& pix_fmt) const
|
||||
void FrameHashCache::LengthChangedEvent(const rational &old, const rational &newlen)
|
||||
{
|
||||
if (newlen < old) {
|
||||
QMap<rational, QByteArray>::iterator i = time_hash_map_.begin();
|
||||
|
||||
while (i != time_hash_map_.end()) {
|
||||
if (i.key() >= newlen) {
|
||||
i = time_hash_map_.erase(i);
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FrameHashCache::InvalidateEvent(const TimeRange &r)
|
||||
{
|
||||
QMap<rational, QByteArray>::iterator i = time_hash_map_.begin();
|
||||
|
||||
while (i != time_hash_map_.end()) {
|
||||
if (i.key() >= r.in() && i.key() < r.out()) {
|
||||
i = time_hash_map_.erase(i);
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString FrameHashCache::CachePathName(const QByteArray& hash, const PixelFormat::Format& pix_fmt)
|
||||
{
|
||||
QString ext = GetFormatExtension(pix_fmt);
|
||||
|
||||
@@ -184,7 +156,7 @@ QString VideoRenderFrameCache::CachePathName(const QByteArray& hash, const Pixel
|
||||
return cache_dir.filePath(filename);
|
||||
}
|
||||
|
||||
bool VideoRenderFrameCache::SaveCacheFrame(const QString &filename, char *data, const VideoRenderingParams &vparam)
|
||||
bool FrameHashCache::SaveCacheFrame(const QString &filename, char *data, const VideoRenderingParams &vparam)
|
||||
{
|
||||
switch (vparam.format()) {
|
||||
case PixelFormat::PIX_FMT_RGB8:
|
||||
@@ -24,47 +24,29 @@
|
||||
#include <QMutex>
|
||||
|
||||
#include "common/rational.h"
|
||||
#include "common/timerange.h"
|
||||
#include "render/pixelformat.h"
|
||||
#include "render/playbackcache.h"
|
||||
#include "render/videoparams.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
class VideoRenderFrameCache
|
||||
class FrameHashCache : public PlaybackCache
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
VideoRenderFrameCache() = default;
|
||||
FrameHashCache() = default;
|
||||
|
||||
void Clear();
|
||||
|
||||
/**
|
||||
* @brief Return whether a frame with this hash already exists
|
||||
*/
|
||||
bool HasHash(const QByteArray& hash, const PixelFormat::Format &format);
|
||||
|
||||
/**
|
||||
* @brief Return whether a frame is currently being cached
|
||||
*/
|
||||
bool IsCaching(const QByteArray& hash);
|
||||
|
||||
/**
|
||||
* @brief Check if a frame is currently being cached, and if not reserve it
|
||||
*/
|
||||
bool TryCache(const QByteArray& hash);
|
||||
|
||||
void SetCacheID(const QString& id);
|
||||
|
||||
QByteArray TimeToHash(const rational& time) const;
|
||||
QByteArray GetHash(const rational& time) const;
|
||||
|
||||
void SetHash(const rational& time, const QByteArray& hash);
|
||||
|
||||
void Truncate(const rational& time);
|
||||
|
||||
void RemoveHashFromCurrentlyCaching(const QByteArray& hash);
|
||||
void SetTimebase(const rational& tb);
|
||||
|
||||
/**
|
||||
* @brief Returns a list of frames that use a particular hash
|
||||
*/
|
||||
QList<rational> FramesWithHash(const QByteArray& hash) const;
|
||||
QList<rational> GetFramesWithHash(const QByteArray& hash) const;
|
||||
|
||||
/**
|
||||
* @brief Same as FramesWithHash() but also removes these frames from the map
|
||||
@@ -73,23 +55,26 @@ public:
|
||||
|
||||
const QMap<rational, QByteArray>& time_hash_map() const;
|
||||
|
||||
static QString GetFormatExtension(const PixelFormat::Format& f);
|
||||
|
||||
/**
|
||||
* @brief Return the path of the cached image at this time
|
||||
*/
|
||||
QString CachePathName(const QByteArray &hash, const PixelFormat::Format& pix_fmt) const;
|
||||
static QString CachePathName(const QByteArray &hash, const PixelFormat::Format& pix_fmt);
|
||||
|
||||
static bool SaveCacheFrame(const QString& filename, char *data, const VideoRenderingParams &vparam);
|
||||
void SaveCacheFrame(const QByteArray& hash, char *data, const VideoRenderingParams &vparam) const;
|
||||
static void SaveCacheFrame(const QByteArray& hash, char *data, const VideoRenderingParams &vparam);
|
||||
|
||||
static QString GetFormatExtension(const PixelFormat::Format& f);
|
||||
|
||||
protected:
|
||||
virtual void LengthChangedEvent(const rational& old, const rational& newlen) override;
|
||||
|
||||
virtual void InvalidateEvent(const TimeRange& range) override;
|
||||
|
||||
private:
|
||||
QMap<rational, QByteArray> time_hash_map_;
|
||||
|
||||
QMutex currently_caching_lock_;
|
||||
QVector<QByteArray> currently_caching_list_;
|
||||
rational timebase_;
|
||||
|
||||
QString cache_id_;
|
||||
};
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
@@ -0,0 +1,84 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2019 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "playbackcache.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
PlaybackCache::PlaybackCache()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void PlaybackCache::Invalidate(const TimeRange &r)
|
||||
{
|
||||
invalidated_.InsertTimeRange(r);
|
||||
|
||||
InvalidateEvent(r);
|
||||
|
||||
emit Invalidated(r);
|
||||
}
|
||||
|
||||
void PlaybackCache::SetLength(const rational &r)
|
||||
{
|
||||
if (length_ == r) {
|
||||
// Same length - do nothing
|
||||
return;
|
||||
}
|
||||
|
||||
if (r > length_) {
|
||||
// If new length is greater, simply extend the invalidated range for now
|
||||
invalidated_.InsertTimeRange(TimeRange(r, length_));
|
||||
} else {
|
||||
// If new length is smaller, removed hashes
|
||||
invalidated_.RemoveTimeRange(TimeRange(length_, r));
|
||||
}
|
||||
|
||||
LengthChangedEvent(length_, r);
|
||||
|
||||
length_ = r;
|
||||
}
|
||||
|
||||
bool PlaybackCache::IsFullyValidated() const
|
||||
{
|
||||
return invalidated_.isEmpty();
|
||||
}
|
||||
|
||||
void PlaybackCache::Validate(const TimeRange &r)
|
||||
{
|
||||
invalidated_.RemoveTimeRange(r);
|
||||
|
||||
emit Validated(r);
|
||||
}
|
||||
|
||||
void PlaybackCache::InvalidateAll()
|
||||
{
|
||||
Invalidate(TimeRange(0, length_));
|
||||
}
|
||||
|
||||
void PlaybackCache::LengthChangedEvent(const rational &, const rational &)
|
||||
{
|
||||
}
|
||||
|
||||
void PlaybackCache::InvalidateEvent(const TimeRange &)
|
||||
{
|
||||
}
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
@@ -18,27 +18,55 @@
|
||||
|
||||
***/
|
||||
|
||||
#ifndef AUDIOWORKER_H
|
||||
#define AUDIOWORKER_H
|
||||
#ifndef PLAYBACKCACHE_H
|
||||
#define PLAYBACKCACHE_H
|
||||
|
||||
#include "../audiorenderworker.h"
|
||||
#include <QObject>
|
||||
|
||||
#include "common/timerange.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
class AudioWorker : public AudioRenderWorker
|
||||
class PlaybackCache : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
AudioWorker(QHash<Node*, Node*>* copy_map, QObject* parent = nullptr);
|
||||
PlaybackCache();
|
||||
|
||||
void Invalidate(const TimeRange& r);
|
||||
|
||||
void SetLength(const rational& r);
|
||||
|
||||
bool IsFullyValidated() const;
|
||||
|
||||
const TimeRangeList& GetInvalidatedRanges() const
|
||||
{
|
||||
return invalidated_;
|
||||
}
|
||||
|
||||
signals:
|
||||
void Invalidated(const TimeRange& r);
|
||||
|
||||
void Validated(const TimeRange& r);
|
||||
|
||||
void LengthChanged(const rational& r);
|
||||
|
||||
protected:
|
||||
virtual NodeValue FrameToValue(DecoderPtr decoder, StreamPtr stream, const TimeRange &range) override;
|
||||
void Validate(const TimeRange& r);
|
||||
|
||||
virtual void RunNodeAccelerated(const Node *node, const TimeRange& range, NodeValueDatabase& input_params, NodeValueTable& output_params) override;
|
||||
void InvalidateAll();
|
||||
|
||||
virtual void LengthChangedEvent(const rational& old, const rational& newlen);
|
||||
|
||||
virtual void InvalidateEvent(const TimeRange& range);
|
||||
|
||||
private:
|
||||
TimeRangeList invalidated_;
|
||||
|
||||
rational length_;
|
||||
|
||||
};
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
|
||||
#endif // AUDIOWORKER_H
|
||||
#endif // PLAYBACKCACHE_H
|
||||
@@ -103,7 +103,7 @@ void TimeBasedWidget::ConnectViewerNode(ViewerOutput *node)
|
||||
|
||||
void TimeBasedWidget::UpdateMaximumScroll()
|
||||
{
|
||||
rational length = (viewer_node_) ? viewer_node_->Length() : rational();
|
||||
rational length = (viewer_node_) ? viewer_node_->GetLength() : rational();
|
||||
|
||||
if (auto_max_scrollbar_) {
|
||||
scrollbar_->setMaximum(qMax(0, qCeil(TimeToScene(length)) - width()));
|
||||
@@ -244,7 +244,7 @@ void TimeBasedWidget::GoToPrevCut()
|
||||
|
||||
int64_t closest_cut = 0;
|
||||
|
||||
foreach (TrackOutput* track, viewer_node_->Tracks()) {
|
||||
foreach (TrackOutput* track, viewer_node_->GetTracks()) {
|
||||
int64_t this_track_closest_cut = 0;
|
||||
|
||||
foreach (Block* block, track->Blocks()) {
|
||||
@@ -271,7 +271,7 @@ void TimeBasedWidget::GoToNextCut()
|
||||
|
||||
int64_t closest_cut = INT64_MAX;
|
||||
|
||||
foreach (TrackOutput* track, GetConnectedNode()->Tracks()) {
|
||||
foreach (TrackOutput* track, GetConnectedNode()->GetTracks()) {
|
||||
int64_t this_track_closest_cut = Timecode::time_to_timestamp(track->track_length(), timebase());
|
||||
|
||||
if (this_track_closest_cut <= GetTimestamp()) {
|
||||
@@ -319,7 +319,7 @@ void TimeBasedWidget::NextFrame()
|
||||
void TimeBasedWidget::GoToEnd()
|
||||
{
|
||||
if (viewer_node_) {
|
||||
SetTimeAndSignal(Timecode::time_to_timestamp(viewer_node_->Length(), timebase()));
|
||||
SetTimeAndSignal(Timecode::time_to_timestamp(viewer_node_->GetLength(), timebase()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,7 +462,7 @@ void TimeBasedWidget::ToggleShowAll()
|
||||
w = w / 10 * 9;
|
||||
|
||||
toggle_show_all_old_scale_ = GetScale();
|
||||
SetScale(w / GetConnectedNode()->Length().toDouble());
|
||||
SetScale(w / GetConnectedNode()->GetLength().toDouble());
|
||||
toggle_show_all_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,7 +425,7 @@ void TimelineWidget::SplitAtPlayhead()
|
||||
bool some_blocks_are_selected = false;
|
||||
|
||||
// Get all blocks at the playhead
|
||||
foreach (TrackOutput* track, GetConnectedNode()->Tracks()) {
|
||||
foreach (TrackOutput* track, GetConnectedNode()->GetTracks()) {
|
||||
Block* b = track->BlockContainingTime(playhead_time);
|
||||
|
||||
if (b && b->type() == Block::kClip) {
|
||||
@@ -566,7 +566,7 @@ void TimelineWidget::IncreaseTrackHeight()
|
||||
return;
|
||||
}
|
||||
|
||||
QVector<TrackOutput*> all_tracks = GetConnectedNode()->Tracks();
|
||||
QVector<TrackOutput*> all_tracks = GetConnectedNode()->GetTracks();
|
||||
|
||||
// Increase the height of each track by one "unit"
|
||||
foreach (TrackOutput* t, all_tracks) {
|
||||
@@ -580,7 +580,7 @@ void TimelineWidget::DecreaseTrackHeight()
|
||||
return;
|
||||
}
|
||||
|
||||
QVector<TrackOutput*> all_tracks = GetConnectedNode()->Tracks();
|
||||
QVector<TrackOutput*> all_tracks = GetConnectedNode()->GetTracks();
|
||||
|
||||
// Decrease the height of each track by one "unit"
|
||||
foreach (TrackOutput* t, all_tracks) {
|
||||
@@ -714,7 +714,7 @@ void TimelineWidget::DeleteInToOut(bool ripple)
|
||||
|
||||
QUndoCommand* command = new QUndoCommand();
|
||||
|
||||
foreach (TrackOutput* track, GetConnectedNode()->Tracks()) {
|
||||
foreach (TrackOutput* track, GetConnectedNode()->GetTracks()) {
|
||||
if (!track->IsLocked()) {
|
||||
if (ripple) {
|
||||
new TrackRippleRemoveAreaCommand(track,
|
||||
@@ -794,7 +794,7 @@ void TimelineWidget::RippleEditTo(Timeline::MovementMode mode, bool insert_gaps)
|
||||
closest_point_to_playhead = RATIONAL_MAX;
|
||||
}
|
||||
|
||||
foreach (TrackOutput* track, GetConnectedNode()->Tracks()) {
|
||||
foreach (TrackOutput* track, GetConnectedNode()->GetTracks()) {
|
||||
Block* b = track->NearestBlockBefore(playhead_time);
|
||||
|
||||
if (b != nullptr) {
|
||||
@@ -821,7 +821,7 @@ void TimelineWidget::RippleEditTo(Timeline::MovementMode mode, bool insert_gaps)
|
||||
rational out_ripple = qMax(closest_point_to_playhead, playhead_time);
|
||||
rational ripple_length = out_ripple - in_ripple;
|
||||
|
||||
foreach (TrackOutput* track, GetConnectedNode()->Tracks()) {
|
||||
foreach (TrackOutput* track, GetConnectedNode()->GetTracks()) {
|
||||
GapBlock* gap = nullptr;
|
||||
if (insert_gaps) {
|
||||
gap = new GapBlock();
|
||||
@@ -854,7 +854,7 @@ void TimelineWidget::InsertGapsAt(const rational &earliest_point, const rational
|
||||
QList<Block*> blocks_to_append_gap_to;
|
||||
QList<Block*> gaps_to_extend;
|
||||
|
||||
foreach (TrackOutput* track, GetConnectedNode()->Tracks()) {
|
||||
foreach (TrackOutput* track, GetConnectedNode()->GetTracks()) {
|
||||
if (track->IsLocked()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ void TimelineWidget::RippleTool::InitiateGhosts(TimelineViewBlockItem *clicked_i
|
||||
}
|
||||
|
||||
// For each track that does NOT have a ghost, we need to make one for Gaps
|
||||
foreach (TrackOutput* track, parent()->GetConnectedNode()->Tracks()) {
|
||||
foreach (TrackOutput* track, parent()->GetConnectedNode()->GetTracks()) {
|
||||
// Determine if we've already created a ghost on this track
|
||||
bool ghost_on_this_track_exists = false;
|
||||
|
||||
|
||||
@@ -727,7 +727,7 @@ void TimelineRippleDeleteGapsAtRegionsCommand::redo_internal()
|
||||
|
||||
QList<Block*> blocks_around_range;
|
||||
|
||||
foreach (TrackOutput* track, timeline_->Tracks()) {
|
||||
foreach (TrackOutput* track, timeline_->GetTracks()) {
|
||||
// Get the block from every other track that is either at or just before our block's in point
|
||||
Block* block_at_time = track->NearestBlockBeforeOrAt(range.in());
|
||||
|
||||
|
||||
@@ -31,28 +31,28 @@ OLIVE_NAMESPACE_ENTER
|
||||
|
||||
AudioWaveformView::AudioWaveformView(QWidget *parent) :
|
||||
SeekableWidget(parent),
|
||||
backend_(nullptr)
|
||||
playback_(nullptr)
|
||||
{
|
||||
setAutoFillBackground(true);
|
||||
setBackgroundRole(QPalette::Base);
|
||||
}
|
||||
|
||||
void AudioWaveformView::SetBackend(AudioRenderBackend *backend)
|
||||
void AudioWaveformView::SetViewer(AudioPlaybackCache *playback)
|
||||
{
|
||||
if (backend_) {
|
||||
disconnect(backend_, &AudioRenderBackend::QueueComplete, this, &AudioWaveformView::ForceUpdate);
|
||||
disconnect(backend_, &AudioRenderBackend::ParamsChanged, this, &AudioWaveformView::BackendParamsChanged);
|
||||
if (playback_) {
|
||||
disconnect(playback_, &AudioPlaybackCache::Validated, this, &AudioWaveformView::ForceUpdate);
|
||||
disconnect(playback_, &AudioPlaybackCache::ParametersChanged, this, &AudioWaveformView::BackendParamsChanged);
|
||||
|
||||
SetTimebase(0);
|
||||
}
|
||||
|
||||
backend_ = backend;
|
||||
playback_ = playback;
|
||||
|
||||
if (backend_) {
|
||||
connect(backend_, &AudioRenderBackend::QueueComplete, this, &AudioWaveformView::ForceUpdate);
|
||||
connect(backend_, &AudioRenderBackend::ParamsChanged, this, &AudioWaveformView::BackendParamsChanged);
|
||||
if (playback_) {
|
||||
connect(playback_, &AudioPlaybackCache::Validated, this, &AudioWaveformView::ForceUpdate);
|
||||
connect(playback_, &AudioPlaybackCache::ParametersChanged, this, &AudioWaveformView::BackendParamsChanged);
|
||||
|
||||
SetTimebase(backend_->params().time_base());
|
||||
SetTimebase(playback_->GetParameters().time_base());
|
||||
}
|
||||
|
||||
ForceUpdate();
|
||||
@@ -113,12 +113,14 @@ void AudioWaveformView::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
QWidget::paintEvent(event);
|
||||
|
||||
if (!backend_ || backend_->CachePathName().isEmpty() || !backend_->params().is_valid()) {
|
||||
const AudioRenderingParams& params = playback_->GetParameters();
|
||||
|
||||
if (!playback_
|
||||
|| playback_->GetCacheFilename().isEmpty()
|
||||
|| !params.is_valid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const AudioRenderingParams& params = backend_->params();
|
||||
|
||||
if (cached_size_ != size()
|
||||
|| cached_scale_ != GetScale()
|
||||
|| cached_scroll_ != GetScroll()) {
|
||||
@@ -126,7 +128,7 @@ void AudioWaveformView::paintEvent(QPaintEvent *event)
|
||||
cached_waveform_ = QPixmap(size());
|
||||
cached_waveform_.fill(Qt::transparent);
|
||||
|
||||
QFile fs(backend_->CachePathName());
|
||||
QFile fs(playback_->GetCacheFilename());
|
||||
|
||||
if (fs.open(QFile::ReadOnly)) {
|
||||
|
||||
@@ -207,12 +209,12 @@ void AudioWaveformView::paintEvent(QPaintEvent *event)
|
||||
|
||||
void AudioWaveformView::BackendParamsChanged()
|
||||
{
|
||||
SetTimebase(backend_->params().time_base());
|
||||
SetTimebase(playback_->GetParameters().time_base());
|
||||
}
|
||||
|
||||
void AudioWaveformView::ForceUpdate()
|
||||
{
|
||||
cached_size_ = QSize();
|
||||
cached_size_ = QSize();
|
||||
update();
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
#include "audio/sumsamples.h"
|
||||
#include "render/audioparams.h"
|
||||
#include "render/backend/audiorenderbackend.h"
|
||||
#include "render/audioplaybackcache.h"
|
||||
#include "widget/timeruler/seekablewidget.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
@@ -38,7 +38,7 @@ public:
|
||||
|
||||
//void SetData(const QString& file, const AudioRenderingParams& params);
|
||||
|
||||
void SetBackend(AudioRenderBackend* backend);
|
||||
void SetViewer(AudioPlaybackCache *playback);
|
||||
|
||||
static void DrawWaveform(QPainter* painter, const QRect &rect, const double &scale, const SampleSummer::Sum *samples, int nb_samples, int channels);
|
||||
|
||||
@@ -46,7 +46,7 @@ protected:
|
||||
virtual void paintEvent(QPaintEvent* event) override;
|
||||
|
||||
private:
|
||||
AudioRenderBackend* backend_;
|
||||
AudioPlaybackCache *playback_;
|
||||
|
||||
QPixmap cached_waveform_;
|
||||
QSize cached_size_;
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
void GizmoTraverser::FootageProcessingEvent(StreamPtr stream, const TimeRange &/*input_time*/, NodeValueTable *table)
|
||||
void GizmoTraverser::FootageProcessingEvent(StreamPtr stream, const TimeRange &/*input_time*/, NodeValueTable *table) const
|
||||
{
|
||||
if (stream->type() == Stream::kVideo || stream->type() == Stream::kAudio) {
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ public:
|
||||
GizmoTraverser() = default;
|
||||
|
||||
protected:
|
||||
virtual void FootageProcessingEvent(StreamPtr stream, const TimeRange &input_time, NodeValueTable* table) override;
|
||||
virtual void FootageProcessingEvent(StreamPtr stream, const TimeRange &input_time, NodeValueTable* table) const override;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -100,14 +100,14 @@ ViewerWidget::ViewerWidget(QWidget *parent) :
|
||||
SetScale(48.0);
|
||||
|
||||
// Start background renderers
|
||||
video_renderer_ = new OpenGLBackend(this);
|
||||
connect(video_renderer_, &VideoRenderBackend::CachedTimeReady, this, &ViewerWidget::RendererCachedTime);
|
||||
connect(video_renderer_, &VideoRenderBackend::CachedTimeReady, ruler(), &TimeRuler::CacheTimeReady);
|
||||
connect(video_renderer_, &VideoRenderBackend::RangeInvalidated, ruler(), &TimeRuler::CacheInvalidatedRange);
|
||||
connect(video_renderer_, &VideoRenderBackend::GeneratedFrame, this, &ViewerWidget::RendererGeneratedFrame);
|
||||
audio_renderer_ = new AudioBackend(this);
|
||||
renderer_ = new OpenGLBackend(this);
|
||||
/*
|
||||
connect(renderer_, &RenderBackend::CachedTimeReady, this, &ViewerWidget::RendererCachedTime);
|
||||
connect(renderer_, &RenderBackend::CachedTimeReady, ruler(), &TimeRuler::CacheTimeReady);
|
||||
connect(renderer_, &RenderBackend::RangeInvalidated, ruler(), &TimeRuler::CacheInvalidatedRange);
|
||||
connect(renderer_, &RenderBackend::GeneratedFrame, this, &ViewerWidget::RendererGeneratedFrame);
|
||||
*/
|
||||
|
||||
waveform_view_->SetBackend(audio_renderer_);
|
||||
connect(waveform_view_, &AudioWaveformView::TimeChanged, this, &ViewerWidget::SetTimeAndSignal);
|
||||
|
||||
connect(PixelFormat::instance(), &PixelFormat::FormatChanged, this, &ViewerWidget::UpdateRendererParameters);
|
||||
@@ -161,11 +161,10 @@ void ViewerWidget::ConnectNodeInternal(ViewerOutput *n)
|
||||
connect(n, &ViewerOutput::LengthChanged, this, &ViewerWidget::LengthChangedSlot);
|
||||
connect(n, &ViewerOutput::VideoParamsChanged, this, &ViewerWidget::UpdateRendererParameters);
|
||||
connect(n, &ViewerOutput::VisibleInvalidated, this, &ViewerWidget::InvalidateVisible);
|
||||
connect(n, &ViewerOutput::VideoChangedBetween, this, &ViewerWidget::UpdateStack);
|
||||
connect(n, &ViewerOutput::AudioChangedBetween, this, &ViewerWidget::UpdateStack);
|
||||
connect(n, &ViewerOutput::GraphChangedFrom, this, &ViewerWidget::UpdateStack);
|
||||
|
||||
SizeChangedSlot(n->video_params().width(), n->video_params().height());
|
||||
LengthChangedSlot(n->Length());
|
||||
LengthChangedSlot(n->GetLength());
|
||||
|
||||
ColorManager* using_manager;
|
||||
if (override_color_manager_) {
|
||||
@@ -189,6 +188,7 @@ void ViewerWidget::ConnectNodeInternal(ViewerOutput *n)
|
||||
UpdateStack();
|
||||
|
||||
if (GetConnectedTimelinePoints()) {
|
||||
waveform_view_->SetViewer(GetConnectedNode()->audio_playback_cache());
|
||||
waveform_view_->ConnectTimelinePoints(GetConnectedTimelinePoints());
|
||||
}
|
||||
}
|
||||
@@ -204,8 +204,7 @@ void ViewerWidget::DisconnectNodeInternal(ViewerOutput *n)
|
||||
disconnect(n, &ViewerOutput::LengthChanged, this, &ViewerWidget::LengthChangedSlot);
|
||||
disconnect(n, &ViewerOutput::VideoParamsChanged, this, &ViewerWidget::UpdateRendererParameters);
|
||||
disconnect(n, &ViewerOutput::VisibleInvalidated, this, &ViewerWidget::InvalidateVisible);
|
||||
disconnect(n, &ViewerOutput::VideoChangedBetween, this, &ViewerWidget::UpdateStack);
|
||||
disconnect(n, &ViewerOutput::AudioChangedBetween, this, &ViewerWidget::UpdateStack);
|
||||
disconnect(n, &ViewerOutput::GraphChangedFrom, this, &ViewerWidget::UpdateStack);
|
||||
|
||||
// Effectively disables the viewer and clears the state
|
||||
SizeChangedSlot(0, 0);
|
||||
@@ -215,13 +214,13 @@ void ViewerWidget::DisconnectNodeInternal(ViewerOutput *n)
|
||||
window->display_widget()->DisconnectColorManager();
|
||||
}
|
||||
|
||||
waveform_view_->SetViewer(nullptr);
|
||||
waveform_view_->ConnectTimelinePoints(nullptr);
|
||||
}
|
||||
|
||||
void ViewerWidget::ConnectedNodeChanged(ViewerOutput *n)
|
||||
{
|
||||
video_renderer_->SetViewerNode(n);
|
||||
audio_renderer_->SetViewerNode(n);
|
||||
renderer_->SetViewerNode(n);
|
||||
}
|
||||
|
||||
void ViewerWidget::ScaleChangedEvent(const double &s)
|
||||
@@ -332,16 +331,6 @@ void ViewerWidget::ForceUpdate()
|
||||
UpdateTextureFromNode(GetTime());
|
||||
}
|
||||
|
||||
VideoRenderBackend *ViewerWidget::video_renderer() const
|
||||
{
|
||||
return video_renderer_;
|
||||
}
|
||||
|
||||
ColorManager *ViewerWidget::color_manager() const
|
||||
{
|
||||
return display_widget_->color_manager();
|
||||
}
|
||||
|
||||
void ViewerWidget::SetGizmos(Node *node)
|
||||
{
|
||||
display_widget_->SetTimeTarget(GetConnectedNode());
|
||||
@@ -376,14 +365,14 @@ void ViewerWidget::UpdateTextureFromNode(const rational& time)
|
||||
QString frame_fn = GetCachedFilenameFromTime(time);
|
||||
|
||||
if (frame_fn.isEmpty()) {
|
||||
video_renderer_->RenderFrame(time);
|
||||
// FIXME: Connect QFutureWatcher to this
|
||||
renderer_->RenderFrame(time);
|
||||
} else {
|
||||
FramePtr f = DecodeCachedImage(frame_fn);
|
||||
SetDisplayImage(f, false);
|
||||
}
|
||||
} else {
|
||||
SetDisplayImage(nullptr, false);
|
||||
video_renderer_->UpdateLastRequestedTime(time);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,11 +388,11 @@ void ViewerWidget::PlayInternal(int speed, bool in_to_out_only)
|
||||
playback_speed_ = speed;
|
||||
play_in_to_out_only_ = in_to_out_only;
|
||||
|
||||
QString audio_fn = audio_renderer_->CachePathName();
|
||||
QString audio_fn = GetConnectedNode()->audio_playback_cache()->GetCacheFilename();
|
||||
if (!audio_fn.isEmpty()) {
|
||||
AudioManager::instance()->SetOutputParams(audio_renderer_->params());
|
||||
AudioManager::instance()->SetOutputParams(GetConnectedNode()->audio_playback_cache()->GetParameters());
|
||||
AudioManager::instance()->StartOutput(audio_fn,
|
||||
audio_renderer_->params().time_to_bytes(GetTime()),
|
||||
GetConnectedNode()->audio_playback_cache()->GetParameters().time_to_bytes(GetTime()),
|
||||
playback_speed_);
|
||||
}
|
||||
|
||||
@@ -433,17 +422,19 @@ void ViewerWidget::PushScrubbedAudio()
|
||||
{
|
||||
if (!IsPlaying() && Config::Current()["AudioScrubbing"].toBool()) {
|
||||
// Get audio src device from renderer
|
||||
QString audio_fn = audio_renderer_->CachePathName();
|
||||
QString audio_fn = GetConnectedNode()->audio_playback_cache()->GetCacheFilename();
|
||||
QFile audio_src(audio_fn);
|
||||
|
||||
if (audio_src.open(QFile::ReadOnly)) {
|
||||
const AudioRenderingParams& params = GetConnectedNode()->audio_playback_cache()->GetParameters();
|
||||
|
||||
// FIXME: Hardcoded scrubbing interval (20ms)
|
||||
int size_of_sample = audio_renderer_->params().time_to_bytes(rational(20, 1000));
|
||||
int size_of_sample = params.time_to_bytes(rational(20, 1000));
|
||||
|
||||
// Push audio
|
||||
audio_src.seek(audio_renderer_->params().time_to_bytes(GetTime()));
|
||||
audio_src.seek(params.time_to_bytes(GetTime()));
|
||||
QByteArray frame_audio = audio_src.read(size_of_sample);
|
||||
AudioManager::instance()->SetOutputParams(audio_renderer_->params());
|
||||
AudioManager::instance()->SetOutputParams(params);
|
||||
AudioManager::instance()->PushToOutput(frame_audio);
|
||||
|
||||
audio_src.close();
|
||||
@@ -469,11 +460,11 @@ void ViewerWidget::UpdateMinimumScale()
|
||||
return;
|
||||
}
|
||||
|
||||
if (GetConnectedNode()->Length().isNull()) {
|
||||
if (GetConnectedNode()->GetLength().isNull()) {
|
||||
// Avoids divide by zero
|
||||
SetMinimumScale(0);
|
||||
} else {
|
||||
SetMinimumScale(static_cast<double>(ruler()->width()) / GetConnectedNode()->Length().toDouble());
|
||||
SetMinimumScale(static_cast<double>(ruler()->width()) / GetConnectedNode()->GetLength().toDouble());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,7 +517,10 @@ void ViewerWidget::FillPlaybackQueue()
|
||||
QString ViewerWidget::GetCachedFilenameFromTime(const rational &time)
|
||||
{
|
||||
if (FrameExistsAtTime(time)) {
|
||||
return video_renderer_->GetCachedFrame(time);
|
||||
QByteArray hash = GetConnectedNode()->video_frame_cache()->GetHash(time);
|
||||
return GetConnectedNode()->video_frame_cache()->CachePathName(
|
||||
hash,
|
||||
PixelFormat::instance()->GetConfiguredFormatForMode(RenderMode::kOffline));
|
||||
} else {
|
||||
return QString();
|
||||
}
|
||||
@@ -534,7 +528,7 @@ QString ViewerWidget::GetCachedFilenameFromTime(const rational &time)
|
||||
|
||||
bool ViewerWidget::FrameExistsAtTime(const rational &time)
|
||||
{
|
||||
return GetConnectedNode() && time < GetConnectedNode()->Length();
|
||||
return GetConnectedNode() && time < GetConnectedNode()->GetLength();
|
||||
}
|
||||
|
||||
FramePtr ViewerWidget::DecodeCachedImage(const QString &fn)
|
||||
@@ -683,25 +677,13 @@ void ViewerWidget::UpdateRendererParameters()
|
||||
|
||||
RenderMode::Mode render_mode = RenderMode::kOffline;
|
||||
|
||||
VideoRenderingParams vparam(GetConnectedNode()->video_params(),
|
||||
PixelFormat::instance()->GetConfiguredFormatForMode(render_mode),
|
||||
render_mode,
|
||||
divider_);
|
||||
renderer_->SetDivider(divider_);
|
||||
renderer_->SetMode(render_mode);
|
||||
renderer_->SetPixelFormat(PixelFormat::instance()->GetConfiguredFormatForMode(render_mode));
|
||||
|
||||
if (video_renderer_->params() != vparam) {
|
||||
video_renderer_->SetParameters(vparam);
|
||||
video_renderer_->InvalidateCache(TimeRange(0, GetConnectedNode()->Length()), nullptr);
|
||||
}
|
||||
display_widget_->SetVideoParams(GetConnectedNode()->video_params());
|
||||
|
||||
display_widget_->SetVideoParams(vparam);
|
||||
|
||||
AudioRenderingParams aparam(GetConnectedNode()->audio_params(),
|
||||
SampleFormat::kInternalFormat);
|
||||
|
||||
if (audio_renderer_->params() != aparam) {
|
||||
audio_renderer_->SetParameters(aparam);
|
||||
audio_renderer_->InvalidateCache(TimeRange(0, GetConnectedNode()->Length()), nullptr);
|
||||
}
|
||||
renderer_->SetSampleFormat(SampleFormat::kInternalFormat);
|
||||
}
|
||||
|
||||
void ViewerWidget::ShowContextMenu(const QPoint &pos)
|
||||
@@ -925,7 +907,7 @@ void ViewerWidget::TimebaseChangedEvent(const rational &timebase)
|
||||
controls_->SetTimebase(timebase);
|
||||
|
||||
controls_->SetTime(ruler()->GetTime());
|
||||
LengthChangedSlot(GetConnectedNode() ? GetConnectedNode()->Length() : 0);
|
||||
LengthChangedSlot(GetConnectedNode() ? GetConnectedNode()->GetLength() : 0);
|
||||
}
|
||||
|
||||
void ViewerWidget::PlaybackTimerUpdate()
|
||||
@@ -947,7 +929,7 @@ void ViewerWidget::PlaybackTimerUpdate()
|
||||
|
||||
// Otherwise set the bounds to the range of the sequence
|
||||
min_time = 0;
|
||||
max_time = Timecode::time_to_timestamp(GetConnectedNode()->Length(), timebase());
|
||||
max_time = Timecode::time_to_timestamp(GetConnectedNode()->GetLength(), timebase());
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1039,8 +1021,10 @@ void ViewerWidget::SetZoomFromMenu(QAction *action)
|
||||
|
||||
void ViewerWidget::InvalidateVisible(NodeInput* source)
|
||||
{
|
||||
video_renderer_->InvalidateCache(TimeRange(GetTime(), GetTime()), source);
|
||||
video_renderer_->RenderFrame(GetTime());
|
||||
/*
|
||||
renderer_->NodeGraphChanged(source);
|
||||
renderer_->RenderFrame(GetTime());
|
||||
*/
|
||||
}
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
|
||||
@@ -33,8 +33,6 @@
|
||||
#include "node/output/viewer/viewer.h"
|
||||
#include "panel/scope/scope.h"
|
||||
#include "render/backend/opengl/openglbackend.h"
|
||||
#include "render/backend/opengl/opengltexture.h"
|
||||
#include "render/backend/audio/audiobackend.h"
|
||||
#include "viewerdisplay.h"
|
||||
#include "viewerplaybacktimer.h"
|
||||
#include "viewerqueue.h"
|
||||
@@ -87,9 +85,13 @@ public:
|
||||
|
||||
void ForceUpdate();
|
||||
|
||||
VideoRenderBackend* video_renderer() const;
|
||||
RenderBackend* renderer() const {
|
||||
return renderer_;
|
||||
}
|
||||
|
||||
ColorManager* color_manager() const;
|
||||
ColorManager* color_manager() const {
|
||||
return display_widget_->color_manager();
|
||||
}
|
||||
|
||||
void SetGizmos(Node* node);
|
||||
|
||||
@@ -153,8 +155,7 @@ protected:
|
||||
|
||||
virtual void resizeEvent(QResizeEvent *event) override;
|
||||
|
||||
OpenGLBackend* video_renderer_;
|
||||
AudioBackend* audio_renderer_;
|
||||
RenderBackend* renderer_;
|
||||
|
||||
PlaybackControls* controls_;
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ void ViewerDisplayWidget::SetGizmos(Node *node)
|
||||
}
|
||||
}
|
||||
|
||||
void ViewerDisplayWidget::SetVideoParams(const VideoRenderingParams ¶ms)
|
||||
void ViewerDisplayWidget::SetVideoParams(const VideoParams ¶ms)
|
||||
{
|
||||
gizmo_params_ = params;
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ public:
|
||||
void SetSafeMargins(const ViewerSafeMarginInfo& safe_margin);
|
||||
|
||||
void SetGizmos(Node* node);
|
||||
void SetVideoParams(const VideoRenderingParams& params);
|
||||
void SetVideoParams(const VideoParams ¶ms);
|
||||
void SetTime(const rational& time);
|
||||
|
||||
FramePtr last_loaded_buffer() const;
|
||||
@@ -171,7 +171,7 @@ private:
|
||||
Node* gizmos_;
|
||||
NodeValueDatabase gizmo_db_;
|
||||
rational gizmo_drag_time_;
|
||||
VideoRenderingParams gizmo_params_;
|
||||
VideoParams gizmo_params_;
|
||||
bool gizmo_click_;
|
||||
|
||||
rational time_;
|
||||
|
||||
@@ -493,8 +493,6 @@ TimelinePanel* MainWindow::AppendTimelinePanel()
|
||||
connect(panel, &TimelinePanel::SelectionChanged, node_panel_, &NodePanel::SelectBlocks);
|
||||
connect(param_panel_, &ParamPanel::TimeChanged, panel, &TimelinePanel::SetTimestamp);
|
||||
connect(sequence_viewer_panel_, &SequenceViewerPanel::TimeChanged, panel, &TimelinePanel::SetTimestamp);
|
||||
connect(sequence_viewer_panel_->video_renderer(), &VideoRenderBackend::CachedTimeReady, panel->ruler(), &TimeRuler::CacheTimeReady);
|
||||
connect(sequence_viewer_panel_->video_renderer(), &VideoRenderBackend::RangeInvalidated, panel->ruler(), &TimeRuler::CacheInvalidatedRange);
|
||||
|
||||
sequence_viewer_panel_->ConnectTimeBasedPanel(panel);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user