/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 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 .
***/
#include "viewer.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "audio/audiomanager.h"
#include "common/clamp.h"
#include "common/ratiodialog.h"
#include "common/timecodefunctions.h"
#include "config/config.h"
#include "core.h"
#include "node/block/gap/gap.h"
#include "node/generator/shape/shapenodebase.h"
#include "node/project/project.h"
#include "panel/multicam/multicampanel.h"
#include "panel/panelmanager.h"
#include "render/rendermanager.h"
#include "viewerpreventsleep.h"
#include "widget/audiomonitor/audiomonitor.h"
#include "widget/menu/menu.h"
#include "widget/multicam/multicamdisplay.h"
#include "widget/nodeparamview/nodeparamviewundo.h"
#include "widget/timelinewidget/tool/add.h"
#include "widget/timeruler/timeruler.h"
namespace olive {
#define super TimeBasedWidget
QVector ViewerWidget::instances_;
// NOTE: Hardcoded interval of size of audio chunk to render and send to the output at a time.
// We want this to be as long as possible so the code has plenty of time to send the audio
// while also being as short as possible so users get relatively immediate feedback when
// changing values. 1/4 second seems to be a good middleground.
const rational ViewerWidget::kAudioPlaybackInterval = rational(1, 4);
const rational kVideoPlaybackInterval = rational(1, 2);
ViewerWidget::ViewerWidget(ViewerDisplayWidget *display, QWidget *parent) :
super(false, true, parent),
playback_speed_(0),
color_menu_enabled_(true),
time_changed_from_timer_(false),
prequeuing_video_(false),
prequeuing_audio_(0),
record_armed_(false),
recording_(false),
first_requeue_watcher_(nullptr),
enable_audio_scrubbing_(true),
waveform_mode_(kWFAutomatic),
ignore_scrub_(0),
multicam_panel_(nullptr)
{
// Set up main layout
QVBoxLayout* layout = new QVBoxLayout(this);
layout->setMargin(0);
// Create main OpenGL-based view and sizer
sizer_ = new ViewerSizer();
layout->addWidget(sizer_);
display_widget_ = display;
display_widget_->SetShowWidgetBackground(true);
playback_devices_.append(display_widget_);
connect(display_widget_, &ViewerDisplayWidget::customContextMenuRequested, this, &ViewerWidget::ShowContextMenu);
connect(display_widget_, &ViewerDisplayWidget::CursorColor, this, &ViewerWidget::CursorColor);
connect(display_widget_, &ViewerDisplayWidget::ColorProcessorChanged, this, &ViewerWidget::ColorProcessorChanged);
connect(display_widget_, &ViewerDisplayWidget::ColorManagerChanged, this, &ViewerWidget::ColorManagerChanged);
connect(display_widget_, &ViewerDisplayWidget::DragEntered, this, &ViewerWidget::DragEntered);
connect(display_widget_, &ViewerDisplayWidget::Dropped, this, &ViewerWidget::Dropped);
connect(display_widget_, &ViewerDisplayWidget::TextureChanged, this, &ViewerWidget::TextureChanged);
connect(display_widget_, &ViewerDisplayWidget::QueueStarved, this, &ViewerWidget::QueueStarved);
connect(display_widget_, &ViewerDisplayWidget::QueueNoLongerStarved, this, &ViewerWidget::QueueNoLongerStarved);
connect(display_widget_, &ViewerDisplayWidget::CreateAddableAt, this, &ViewerWidget::CreateAddableAt);
connect(sizer_, &ViewerSizer::RequestScale, display_widget_, &ViewerDisplayWidget::SetMatrixZoom);
connect(sizer_, &ViewerSizer::RequestTranslate, display_widget_, &ViewerDisplayWidget::SetMatrixTranslate);
connect(display_widget_, &ViewerDisplayWidget::HandDragMoved, sizer_, &ViewerSizer::HandDragMove);
sizer_->SetWidget(display_widget_);
// Make the display widget the first tabbable widget. While the viewer display cannot actually
// be interacted with by tabbing, it prevents the actual first tabbable widget (the playhead
// slider in `controls_`) from getting auto-focused any time the panel is maximized (with `)
display_widget_->setFocusPolicy(Qt::TabFocus);
// Create waveform view when audio is connected and video isn't
waveform_view_ = new AudioWaveformView();
ConnectTimelineView(waveform_view_);
PassWheelEventsToScrollBar(waveform_view_);
layout->addWidget(waveform_view_);
// Create time ruler
layout->addWidget(ruler());
// Create scrollbar
layout->addWidget(scrollbar());
connect(scrollbar(), &QScrollBar::valueChanged, ruler(), &TimeRuler::SetScroll);
connect(scrollbar(), &QScrollBar::valueChanged, waveform_view_, &AudioWaveformView::SetScroll);
// Create lower controls
controls_ = new PlaybackControls();
controls_->SetTimecodeEnabled(true);
controls_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
connect(controls_, &PlaybackControls::PlayClicked, this, static_cast(&ViewerWidget::Play));
connect(controls_, &PlaybackControls::PauseClicked, this, &ViewerWidget::Pause);
connect(controls_, &PlaybackControls::PrevFrameClicked, this, &ViewerWidget::PrevFrame);
connect(controls_, &PlaybackControls::NextFrameClicked, this, &ViewerWidget::NextFrame);
connect(controls_, &PlaybackControls::BeginClicked, this, &ViewerWidget::GoToStart);
connect(controls_, &PlaybackControls::EndClicked, this, &ViewerWidget::GoToEnd);
layout->addWidget(controls_);
// FIXME: Magic number
SetScale(48.0);
// Ensures that seeking on the waveform view updates the time as expected
connect(waveform_view_, &AudioWaveformView::customContextMenuRequested, this, &ViewerWidget::ShowContextMenu);
connect(&playback_backup_timer_, &QTimer::timeout, this, &ViewerWidget::PlaybackTimerUpdate);
SetAutoMaxScrollBar(true);
instances_.append(this);
auto_cacher_ = new PreviewAutoCacher(this);
connect(display_widget_, &ViewerDisplayWidget::ColorProcessorChanged, auto_cacher_, &PreviewAutoCacher::SetDisplayColorProcessor);
UpdateWaveformViewFromMode();
connect(Core::instance(), &Core::ColorPickerEnabled, this, &ViewerWidget::SetSignalCursorColorEnabled);
connect(this, &ViewerWidget::CursorColor, Core::instance(), &Core::ColorPickerColorEmitted);
connect(AudioManager::instance(), &AudioManager::OutputParamsChanged, this, &ViewerWidget::UpdateAudioProcessor);
}
ViewerWidget::~ViewerWidget()
{
instances_.removeOne(this);
auto windows = windows_;
foreach (ViewerWindow* window, windows) {
delete window;
}
}
void ViewerWidget::TimeChangedEvent(const rational &time)
{
if (!time_changed_from_timer_) {
PauseInternal();
}
if (record_armed_) {
DisarmRecording();
}
controls_->SetTime(time);
if (GetConnectedNode() && last_time_ != time) {
if (!IsPlaying()) {
UpdateTextureFromNode();
PushScrubbedAudio();
// We don't clear the FPS timer on pause in case users want to see it immediately after, but by
// the time a new texture is drawn, assume that the FPS no longer needs to be shown.
display_widget_->ResetFPSTimer();
}
display_widget_->SetTime(time);
}
// Send time to auto-cacher
auto_cacher_->SetPlayhead(time);
last_time_ = time;
}
void ViewerWidget::ConnectNodeEvent(ViewerOutput *n)
{
connect(n, &ViewerOutput::SizeChanged, this, &ViewerWidget::SetViewerResolution);
connect(n, &ViewerOutput::PixelAspectChanged, this, &ViewerWidget::SetViewerPixelAspect);
connect(n, &ViewerOutput::LengthChanged, this, &ViewerWidget::LengthChangedSlot);
connect(n, &ViewerOutput::InterlacingChanged, this, &ViewerWidget::InterlacingChangedSlot);
connect(n, &ViewerOutput::VideoParamsChanged, this, &ViewerWidget::UpdateRendererVideoParameters);
connect(n, &ViewerOutput::VideoParamsChanged, this, &ViewerWidget::UpdateTextureFromNode, Qt::QueuedConnection);
connect(n, &ViewerOutput::AudioParamsChanged, this, &ViewerWidget::UpdateRendererAudioParameters);
connect(n->video_frame_cache(), &FrameHashCache::Invalidated, this, &ViewerWidget::ViewerInvalidatedVideoRange);
connect(n, &ViewerOutput::TextureInputChanged, this, &ViewerWidget::UpdateWaveformViewFromMode);
connect(controls_, &PlaybackControls::TimeChanged, n, &ViewerOutput::SetPlayhead);
VideoParams vp = n->GetVideoParams();
InterlacingChangedSlot(vp.interlacing());
ruler()->SetPlaybackCache(n->video_frame_cache());
SetViewerResolution(vp.width(), vp.height());
SetViewerPixelAspect(vp.pixel_aspect_ratio());
last_length_ = 0;
LengthChangedSlot(n->GetLength());
UpdateAudioProcessor();
ColorManager* color_manager = n->project()->color_manager();
foreach (ViewerDisplayWidget *dw, playback_devices_) {
dw->ConnectColorManager(color_manager);
}
UpdateWaveformViewFromMode();
waveform_view_->SetViewer(GetConnectedNode());
UpdateRendererVideoParameters();
UpdateRendererAudioParameters();
// Set texture to new texture (or null if no viewer node is available)
UpdateTextureFromNode();
}
void ViewerWidget::DisconnectNodeEvent(ViewerOutput *n)
{
PauseInternal();
disconnect(n, &ViewerOutput::SizeChanged, this, &ViewerWidget::SetViewerResolution);
disconnect(n, &ViewerOutput::PixelAspectChanged, this, &ViewerWidget::SetViewerPixelAspect);
disconnect(n, &ViewerOutput::LengthChanged, this, &ViewerWidget::LengthChangedSlot);
disconnect(n, &ViewerOutput::InterlacingChanged, this, &ViewerWidget::InterlacingChangedSlot);
disconnect(n, &ViewerOutput::VideoParamsChanged, this, &ViewerWidget::UpdateRendererVideoParameters);
disconnect(n, &ViewerOutput::VideoParamsChanged, this, &ViewerWidget::UpdateTextureFromNode);
disconnect(n, &ViewerOutput::AudioParamsChanged, this, &ViewerWidget::UpdateRendererAudioParameters);
disconnect(n->video_frame_cache(), &FrameHashCache::Invalidated, this, &ViewerWidget::ViewerInvalidatedVideoRange);
disconnect(n, &ViewerOutput::TextureInputChanged, this, &ViewerWidget::UpdateWaveformViewFromMode);
disconnect(controls_, &PlaybackControls::TimeChanged, n, &ViewerOutput::SetPlayhead);
timeline_selected_blocks_.clear();
node_view_selected_.clear();
if (multicam_panel_) {
multicam_panel_->SetMulticamNode(nullptr, nullptr, nullptr, rational::NaN);
}
CloseAudioProcessor();
audio_scrub_watchers_.clear();
SetDisplayImage(nullptr);
ruler()->SetPlaybackCache(nullptr);
// Effectively disables the viewer and clears the state
SetViewerResolution(0, 0);
foreach (ViewerDisplayWidget *dw, playback_devices_) {
dw->DisconnectColorManager();
}
waveform_view_->SetViewer(nullptr);
// Queue an UpdateStack so that when it runs, the viewer node will be fully disconnected
QMetaObject::invokeMethod(this, &ViewerWidget::UpdateWaveformViewFromMode, Qt::QueuedConnection);
SetGizmos(nullptr);
}
void ViewerWidget::ConnectedNodeChangeEvent(ViewerOutput *n)
{
auto_cacher_->SetViewerNode(n);
display_widget_->SetSubtitleTracks(dynamic_cast(n));
}
void ViewerWidget::ConnectedWorkAreaChangeEvent(TimelineWorkArea *workarea)
{
waveform_view_->SetWorkArea(workarea);
}
void ViewerWidget::ConnectedMarkersChangeEvent(TimelineMarkerList *markers)
{
waveform_view_->SetMarkers(markers);
}
void ViewerWidget::ScaleChangedEvent(const double &s)
{
super::ScaleChangedEvent(s);
waveform_view_->SetScale(s);
}
void ViewerWidget::resizeEvent(QResizeEvent *event)
{
super::resizeEvent(event);
UpdateMinimumScale();
}
void ViewerWidget::TogglePlayPause()
{
if (IsPlaying()) {
Pause();
} else {
Play();
}
}
bool ViewerWidget::IsPlaying() const
{
return playback_speed_ != 0;
}
void ViewerWidget::SetColorMenuEnabled(bool enabled)
{
color_menu_enabled_ = enabled;
}
void ViewerWidget::SetMatrix(const QMatrix4x4 &mat)
{
foreach (ViewerDisplayWidget *dw, playback_devices_) {
dw->SetMatrixCrop(mat);
}
}
void ViewerWidget::SetFullScreen(QScreen *screen)
{
if (!screen) {
// Try to find the screen that contains the mouse cursor currently
foreach (QScreen* test, QGuiApplication::screens()) {
if (test->geometry().contains(QCursor::pos())) {
screen = test;
break;
}
}
// Fallback, just use the first screen
if (!screen) {
screen = QGuiApplication::screens().first();
}
}
if (windows_.contains(screen)) {
ViewerWindow* vw = windows_.take(screen);
vw->deleteLater();
return;
}
ViewerWindow* vw = new ViewerWindow(this);
vw->setGeometry(screen->geometry());
vw->showFullScreen();
vw->display_widget()->ConnectColorManager(color_manager());
connect(vw, &ViewerWindow::destroyed, this, &ViewerWidget::WindowAboutToClose);
connect(vw->display_widget(), &ViewerDisplayWidget::customContextMenuRequested, this, &ViewerWidget::ShowContextMenu);
if (GetConnectedNode()) {
vw->SetVideoParams(GetConnectedNode()->GetVideoParams());
vw->display_widget()->SetDeinterlacing(vw->display_widget()->IsDeinterlacing());
}
vw->display_widget()->SetImage(QVariant::fromValue(display_widget()->GetCurrentTexture()));
playback_devices_.append(vw->display_widget());
(*vw->display_widget()->queue()) = *playback_devices_.first()->queue();
if (IsPlaying()) {
vw->display_widget()->Play(GetTimestamp(), playback_speed_, timebase());
}
windows_.insert(screen, vw);
}
void ViewerWidget::CacheEntireSequence()
{
auto_cacher_->ForceCacheRange(TimeRange(0, GetConnectedNode()->GetVideoLength()));
}
void ViewerWidget::CacheSequenceInOut()
{
if (GetConnectedNode() && GetConnectedNode()->GetWorkArea()->enabled()) {
auto_cacher_->ForceCacheRange(GetConnectedNode()->GetWorkArea()->range());
} else {
QMessageBox::warning(this,
tr("Error"),
tr("No in or out points are set to cache."),
QMessageBox::Ok);
}
}
void ViewerWidget::SetGizmos(Node *node)
{
display_widget_->SetTimeTarget(GetConnectedNode());
display_widget_->SetGizmos(node);
}
void ViewerWidget::StartCapture(TimelineWidget *source, const TimeRange &time, const Track::Reference &track)
{
GetConnectedNode()->SetPlayhead(time.in());
ArmForRecording();
recording_callback_ = source;
recording_range_ = time;
recording_track_ = track;
}
void ViewerWidget::ConnectMulticamWidget(MulticamWidget *p)
{
if (multicam_panel_) {
disconnect(multicam_panel_, &MulticamWidget::Switched, this, &ViewerWidget::DetectMulticamNodeNow);
}
multicam_panel_ = p;
if (multicam_panel_) {
connect(multicam_panel_, &MulticamWidget::Switched, this, &ViewerWidget::DetectMulticamNodeNow);
}
}
FramePtr ViewerWidget::DecodeCachedImage(const QString &cache_path, const QUuid &cache_id, const int64_t& time)
{
FramePtr frame = FrameHashCache::LoadCacheFrame(cache_path, cache_id, time);
if (frame) {
frame->set_timestamp(time);
} else {
qWarning() << "Tried to load cached frame from file but it was null";
}
return frame;
}
void ViewerWidget::DecodeCachedImage(RenderTicketPtr ticket, const QString &cache_path, const QUuid &cache_id, const int64_t& time)
{
ticket->Start();
FramePtr f = DecodeCachedImage(cache_path, cache_id, time);
if (f) {
ticket->Finish(QVariant::fromValue(f));
} else {
ticket->Finish();
}
}
bool ViewerWidget::ShouldForceWaveform() const
{
return GetConnectedNode()
&& !GetConnectedNode()->GetConnectedTextureOutput()
&& GetConnectedNode()->GetConnectedSampleOutput();
}
void ViewerWidget::SetEmptyImage()
{
foreach (ViewerDisplayWidget *dw, playback_devices_) {
dw->SetBlank();
}
}
void ViewerWidget::UpdateAutoCacher()
{
auto_cacher_->SetPlayhead(GetConnectedNode()->GetPlayhead());
}
void ViewerWidget::DecrementPrequeuedAudio()
{
prequeuing_audio_--;
if (!prequeuing_audio_) {
FinishPlayPreprocess();
}
}
void ViewerWidget::ArmForRecording()
{
controls_->StartPlayBlink();
record_armed_ = true;
}
void ViewerWidget::DisarmRecording()
{
controls_->StopPlayBlink();
record_armed_ = false;
}
void ViewerWidget::UpdateAudioProcessor()
{
if (GetConnectedNode()) {
CloseAudioProcessor();
AudioParams ap = GetConnectedNode()->GetAudioParams();
AudioParams packed(OLIVE_CONFIG("AudioOutputSampleRate").toInt(),
OLIVE_CONFIG("AudioOutputChannelLayout").toULongLong(),
static_cast(OLIVE_CONFIG("AudioOutputSampleFormat").toInt()));
audio_processor_.Open(ap, packed, (playback_speed_ == 0) ? 1 : std::abs(playback_speed_));
}
}
void ViewerWidget::CreateAddableAt(const QRectF &f)
{
if (Sequence *s = dynamic_cast(GetConnectedNode())) {
Track::Type type = Track::kVideo;
int track_index = -1;
TrackList *list = s->track_list(type);
const rational &in = GetConnectedNode()->GetPlayhead();
rational length = OLIVE_CONFIG("DefaultStillLength").value();
rational out = in + length;
// Find a free track where we won't overwrite anything
while (true) {
track_index++;
if (track_index >= list->GetTrackCount()) {
// Just create a new track
break;
}
Track *track = list->GetTrackAt(track_index);
if (track->IsLocked()) {
continue;
}
Block *b = track->NearestBlockBeforeOrAt(in);
if (!b || (dynamic_cast(b) && b->out() >= out)) {
break;
}
}
MultiUndoCommand *command = new MultiUndoCommand();
Node *clip = AddTool::CreateAddableClip(command, s, Track::Reference(type, track_index), in, length);
if (ShapeNodeBase *shape = dynamic_cast(clip)) {
shape->SetRect(f, s->GetVideoParams(), command);
}
Core::instance()->undo_stack()->pushIfHasChildren(command);
SetGizmos(clip);
}
}
void ViewerWidget::HandleFirstRequeueDestroy()
{
// Extra protection to ensure we don't reference a destroyed object
if (first_requeue_watcher_ == sender()) {
first_requeue_watcher_ = nullptr;
}
}
void ViewerWidget::ShowSubtitleProperties()
{
QFont f(OLIVE_CONFIG("DefaultSubtitleFamily").toString(), OLIVE_CONFIG("DefaultSubtitleSize").toInt(), OLIVE_CONFIG("DefaultSubtitleWeight").toInt());
QFontDialog fd(f, this);
if (fd.exec() == QDialog::Accepted) {
f = fd.selectedFont();
OLIVE_CONFIG("DefaultSubtitleSize") = f.pointSize();
OLIVE_CONFIG("DefaultSubtitleFamily") = f.family();
OLIVE_CONFIG("DefaultSubtitleWeight") = f.weight();
display_widget_->update();
}
}
void ViewerWidget::DryRunFinished()
{
RenderTicketWatcher *w = static_cast(sender());
if (dry_run_watchers_.contains(w)) {
RequestNextDryRun();
}
delete w;
}
void ViewerWidget::RequestNextDryRun()
{
if (IsPlaying()) {
rational next_time = Timecode::timestamp_to_time(dry_run_next_frame_, timebase());
if (FrameExistsAtTime(next_time)) {
if (next_time > GetConnectedNode()->GetPlayhead() + RenderManager::kDryRunInterval) {
QTimer::singleShot(timebase().toDouble() / playback_speed_, this, &ViewerWidget::RequestNextDryRun);
} else {
RenderTicketWatcher *watcher = new RenderTicketWatcher(this);
connect(watcher, &RenderTicketWatcher::Finished, this, &ViewerWidget::DryRunFinished);
watcher->SetTicket(GetSingleFrame(next_time, true));
dry_run_next_frame_ += playback_speed_;
dry_run_watchers_.append(watcher);
}
}
}
}
void ViewerWidget::SaveFrameAsImage()
{
Core::instance()->OpenExportDialogForViewer(GetConnectedNode(), true);
}
void ViewerWidget::DetectMulticamNodeNow()
{
DetectMulticamNode(GetConnectedNode()->GetPlayhead());
}
void ViewerWidget::CloseAudioProcessor()
{
audio_processor_.Close();
}
void ViewerWidget::SetWaveformMode(WaveformMode wf)
{
waveform_mode_ = wf;
UpdateWaveformViewFromMode();
}
void ViewerWidget::DetectMulticamNode(const rational &time)
{
// Look for multicam node
MultiCamNode *multicam = nullptr;
ClipBlock *clip = nullptr;
// Faster way to do this
if (multicam_panel_ && multicam_panel_->isVisible()) {
if (Sequence *s = dynamic_cast(GetConnectedNode())) {
// Prefer selected nodes
for (Node *n : qAsConst(node_view_selected_)) {
if ((multicam = dynamic_cast(n))) {
// Found multicam, now try to find corresponding clip from selected timeline blocks
for (Block *b : qAsConst(timeline_selected_blocks_)) {
if (ClipBlock *c = dynamic_cast(b)) {
if (c->range().Contains(time) && c->ContextContainsNode(multicam)) {
clip = c;
break;
}
}
}
break;
}
}
// Next, prefer multicam from selected block
if (!multicam) {
for (Block *b : qAsConst(timeline_selected_blocks_)) {
if (b->range().Contains(time)) {
if ((clip = dynamic_cast(b))) {
if ((multicam = clip->FindMulticam())) {
break;
}
}
}
}
}
if (!multicam) {
const QVector