- The playback backup timer was using timebase_dbl() in seconds as its QTimer interval, which truncated to 0 ms for normal frame rates. This caused the timer to fire continuously and starve the event loop when the video display was hidden, e.g. in Show Waveform Only mode, freezing the UI and stopping the playhead. Multiply by 1000 to use milliseconds. - If PlayInternal has neither video nor audio to prequeue, call FinishPlayPreprocess immediately so the backup timer starts and the playhead advances even in waveform-only mode with no audio.
2163 lines
59 KiB
C++
2163 lines
59 KiB
C++
/***
|
|
|
|
Olive - Non-Linear Video Editor
|
|
Copyright (C) 2022 Olive Team
|
|
Modifications Copyright (C) 2025 mikesolar
|
|
|
|
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 "viewer.h"
|
|
|
|
#include <QDateTime>
|
|
#include <QFontDialog>
|
|
#include <QGuiApplication>
|
|
#include <QInputDialog>
|
|
#include <QLabel>
|
|
#include <QMessageBox>
|
|
#include <QResizeEvent>
|
|
#include <QScreen>
|
|
#include <QtMath>
|
|
#include <QVBoxLayout>
|
|
|
|
#include "audio/audiomanager.h"
|
|
#include "common/ratiodialog.h"
|
|
#include "config/config.h"
|
|
#include "core.h"
|
|
#include "node/block/gap/gap.h"
|
|
#include "node/generator/shape/shapenodebase.h"
|
|
#include "node/nodeundo.h"
|
|
#include "node/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/timelinewidget/tool/add.h"
|
|
#include "widget/timeruler/timeruler.h"
|
|
|
|
namespace olive
|
|
{
|
|
|
|
#define super TimeBasedWidget
|
|
|
|
QVector<ViewerWidget *> 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, 10);
|
|
|
|
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->setContentsMargins(0, 0, 0, 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::ColorProcessorChanged, this,
|
|
[](ColorProcessorPtr processor) {
|
|
RenderManager::instance()->GetCacher()->SetDisplayColorProcessor(
|
|
processor);
|
|
});
|
|
RenderManager::instance()->GetCacher()->SetDisplayColorProcessor(
|
|
display_widget_->GetCurrentColorProcessor());
|
|
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_);
|
|
layout->addWidget(waveform_view_);
|
|
|
|
// Create time ruler
|
|
layout->addWidget(ruler());
|
|
|
|
// Create scrollbar
|
|
layout->addWidget(scrollbar());
|
|
|
|
// Create lower controls
|
|
controls_ = new PlaybackControls();
|
|
controls_->SetTimecodeEnabled(true);
|
|
controls_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
|
|
connect(controls_, &PlaybackControls::PlayClicked, this,
|
|
static_cast<void (ViewerWidget::*)()>(&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);
|
|
|
|
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;
|
|
}
|
|
|
|
delete display_widget_;
|
|
display_widget_ = nullptr;
|
|
}
|
|
|
|
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
|
|
RenderManager::instance()->GetCacher()->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);
|
|
if (FrameHashCache *cache = n->video_frame_cache()) {
|
|
connect(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);
|
|
if (FrameHashCache *cache = n->video_frame_cache()) {
|
|
disconnect(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)
|
|
{
|
|
display_widget_->SetSubtitleTracks(dynamic_cast<Sequence *>(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();
|
|
}
|
|
|
|
RenderTicketPtr ViewerWidget::GetSingleFrame(const rational &t, bool dry)
|
|
{
|
|
return RenderManager::instance()->GetCacher()->GetSingleFrame(
|
|
this->GetConnectedNode(), t, dry);
|
|
}
|
|
|
|
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(),
|
|
true);
|
|
}
|
|
|
|
windows_.insert(screen, vw);
|
|
}
|
|
|
|
void ViewerWidget::CacheEntireSequence()
|
|
{
|
|
RenderManager::instance()->GetCacher()->ForceCacheRange(
|
|
GetConnectedNode(), TimeRange(0, GetConnectedNode()->GetVideoLength()));
|
|
}
|
|
|
|
void ViewerWidget::CacheSequenceInOut()
|
|
{
|
|
if (GetConnectedNode() && GetConnectedNode()->GetWorkArea()->enabled()) {
|
|
RenderManager::instance()->GetCacher()->ForceCacheRange(
|
|
GetConnectedNode(), 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()
|
|
{
|
|
RenderManager::instance()->GetCacher()->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();
|
|
if (ap.sample_rate() <= 0 || ap.channel_count() <= 0) {
|
|
ap = AudioParams(
|
|
OLIVE_CONFIG("DefaultSequenceAudioFrequency").toInt(),
|
|
OLIVE_CONFIG("DefaultSequenceAudioLayout").toULongLong(),
|
|
ViewerOutput::kDefaultSampleFormat);
|
|
}
|
|
ap.set_format(ViewerOutput::kDefaultSampleFormat);
|
|
|
|
AudioParams packed(
|
|
OLIVE_CONFIG("AudioOutputSampleRate").toInt(),
|
|
OLIVE_CONFIG("AudioOutputChannelLayout").toULongLong(),
|
|
SampleFormat::from_string(OLIVE_CONFIG("AudioOutputSampleFormat")
|
|
.toString()
|
|
.toStdString()));
|
|
|
|
audio_processor_.Open(
|
|
ap, packed, (playback_speed_ == 0) ? 1 : std::abs(playback_speed_));
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::CreateAddableAt(const QRectF &f)
|
|
{
|
|
if (Sequence *s = dynamic_cast<Sequence *>(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>();
|
|
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<GapBlock *>(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<ShapeNodeBase *>(clip)) {
|
|
shape->SetRect(f, s->GetVideoParams(), command);
|
|
}
|
|
|
|
Core::instance()->undo_stack()->push(command, tr("Created Shape"));
|
|
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<RenderTicketWatcher *>(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()
|
|
{
|
|
if (GetConnectedNode()) {
|
|
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<Sequence *>(GetConnectedNode())) {
|
|
// Prefer selected nodes
|
|
for (Node *n : qAsConst(node_view_selected_)) {
|
|
if ((multicam = dynamic_cast<MultiCamNode *>(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<ClipBlock *>(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<ClipBlock *>(b))) {
|
|
if ((multicam = clip->FindMulticam())) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!multicam) {
|
|
const QVector<Track *> &tracks = s->GetTracks();
|
|
for (Track *t : tracks) {
|
|
if (t->IsLocked()) {
|
|
continue;
|
|
}
|
|
|
|
Block *b = t->NearestBlockBeforeOrAt(time);
|
|
if ((clip = dynamic_cast<ClipBlock *>(b))) {
|
|
if ((multicam = clip->FindMulticam())) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (multicam) {
|
|
if (multicam_panel_) {
|
|
multicam_panel_->SetMulticamNode(GetConnectedNode(), multicam, clip,
|
|
time);
|
|
}
|
|
// FIXME: Really dirty
|
|
RenderManager::instance()->GetCacher()->SetMulticamNode(multicam);
|
|
} else {
|
|
RenderManager::instance()->GetCacher()->SetMulticamNode(nullptr);
|
|
if (multicam_panel_) {
|
|
multicam_panel_->SetMulticamNode(nullptr, nullptr, nullptr, time);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool ViewerWidget::IsVideoVisible() const
|
|
{
|
|
return GetConnectedNode()->GetVideoParams().video_type() !=
|
|
VideoParams::kVideoTypeStill &&
|
|
(display_widget_->isVisible() || !windows_.isEmpty());
|
|
}
|
|
|
|
void ViewerWidget::UpdateWaveformViewFromMode()
|
|
{
|
|
bool prefer_waveform = ShouldForceWaveform();
|
|
|
|
sizer_->setVisible(waveform_mode_ == kWFViewerAndWaveform ||
|
|
waveform_mode_ == kWFViewerOnly ||
|
|
(waveform_mode_ == kWFAutomatic && !prefer_waveform));
|
|
waveform_view_->setVisible(
|
|
waveform_mode_ == kWFViewerAndWaveform ||
|
|
waveform_mode_ == kWFWaveformOnly ||
|
|
(waveform_mode_ == kWFAutomatic && prefer_waveform));
|
|
|
|
waveform_view_->setSizePolicy(QSizePolicy::Expanding,
|
|
waveform_mode_ == kWFViewerAndWaveform ?
|
|
QSizePolicy::Maximum :
|
|
QSizePolicy::Expanding);
|
|
|
|
if (GetConnectedNode()) {
|
|
GetConnectedNode()->SetWaveformEnabled(waveform_view_->isVisible());
|
|
|
|
if (waveform_view_->isVisible()) {
|
|
waveform_view_->SetViewer(GetConnectedNode());
|
|
} else {
|
|
waveform_view_->SetViewer(nullptr);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::QueueNextAudioBuffer()
|
|
{
|
|
rational queue_end =
|
|
audio_playback_queue_time_ + (kAudioPlaybackInterval * playback_speed_);
|
|
|
|
// Clamp queue end by zero and the audio length
|
|
queue_end = std::clamp(queue_end, rational(0),
|
|
GetConnectedNode()->GetAudioLength());
|
|
if ((playback_speed_ > 0 && queue_end <= audio_playback_queue_time_) ||
|
|
(playback_speed_ < 0 && queue_end >= audio_playback_queue_time_)) {
|
|
// This will queue nothing, so stop the loop here
|
|
if (prequeuing_audio_) {
|
|
DecrementPrequeuedAudio();
|
|
}
|
|
return;
|
|
}
|
|
|
|
RenderTicketWatcher *watcher = new RenderTicketWatcher(this);
|
|
connect(watcher, &RenderTicketWatcher::Finished, this,
|
|
&ViewerWidget::ReceivedAudioBufferForPlayback);
|
|
audio_playback_queue_.push_back(watcher);
|
|
watcher->SetTicket(RenderManager::instance()->GetCacher()->GetRangeOfAudio(
|
|
GetConnectedNode(), TimeRange(audio_playback_queue_time_, queue_end)));
|
|
|
|
audio_playback_queue_time_ = queue_end;
|
|
}
|
|
|
|
void ViewerWidget::ReceivedAudioBufferForPlayback()
|
|
{
|
|
while (!audio_playback_queue_.empty() &&
|
|
audio_playback_queue_.front()->HasResult()) {
|
|
RenderTicketWatcher *watcher = audio_playback_queue_.front();
|
|
audio_playback_queue_.pop_front();
|
|
|
|
if (watcher->HasResult()) {
|
|
SampleBuffer samples = watcher->Get().value<SampleBuffer>();
|
|
if (samples.is_allocated()) {
|
|
// If the samples must be reversed, reverse them now
|
|
if (playback_speed_ < 0) {
|
|
samples.reverse();
|
|
}
|
|
|
|
// Convert to packed data for audio output
|
|
AudioProcessor::Buffer buf;
|
|
int r = audio_processor_.Convert(samples.to_raw_ptrs().data(),
|
|
samples.sample_count(), &buf);
|
|
|
|
// TempoProcessor may have emptied the array
|
|
if (r >= 0) {
|
|
if (!buf.empty()) {
|
|
const QByteArray &pack = buf.at(0);
|
|
if (prequeuing_audio_) {
|
|
// Add to prequeued audio buffer
|
|
prequeued_audio_.append(pack);
|
|
} else {
|
|
// Push directly to audio manager
|
|
AudioManager::instance()->PushToOutput(
|
|
audio_processor_.to(), pack);
|
|
}
|
|
}
|
|
} else {
|
|
qCritical() << "Failed to process audio for playback:" << r;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (prequeuing_audio_) {
|
|
DecrementPrequeuedAudio();
|
|
}
|
|
|
|
delete watcher;
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::ReceivedAudioBufferForScrubbing()
|
|
{
|
|
RenderTicketWatcher *watcher = static_cast<RenderTicketWatcher *>(sender());
|
|
|
|
while (!audio_scrub_watchers_.empty() &&
|
|
audio_scrub_watchers_.front() != watcher) {
|
|
audio_scrub_watchers_.pop_front();
|
|
}
|
|
|
|
if (!audio_scrub_watchers_.empty()) {
|
|
if (watcher->HasResult()) {
|
|
SampleBuffer samples = watcher->Get().value<SampleBuffer>();
|
|
if (samples.is_allocated()) {
|
|
if (samples.audio_params().channel_count() > 0) {
|
|
AudioProcessor::Buffer buf;
|
|
int r =
|
|
audio_processor_.Convert(samples.to_raw_ptrs().data(),
|
|
samples.sample_count(), &buf);
|
|
|
|
if (r >= 0) {
|
|
if (!buf.empty()) {
|
|
QString error;
|
|
const QByteArray &packed = buf.at(0);
|
|
AudioManager::instance()->ClearBufferedOutput();
|
|
if (!AudioManager::instance()->PushToOutput(
|
|
audio_processor_.to(), packed, &error)) {
|
|
Core::instance()->ShowStatusBarMessage(
|
|
tr("Audio scrubbing failed: %1").arg(error));
|
|
}
|
|
AudioMonitor::PushSampleBufferOnAll(samples);
|
|
}
|
|
} else {
|
|
qCritical()
|
|
<< "Failed to process audio for scrubbing:" << r;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
delete watcher;
|
|
}
|
|
|
|
void ViewerWidget::QueueStarved()
|
|
{
|
|
static const int kMaximumWaitTimeMs = 250;
|
|
static const rational kMaximumWaitTime(kMaximumWaitTimeMs, 1000);
|
|
qint64 now = QDateTime::currentMSecsSinceEpoch();
|
|
|
|
if (!queue_starved_start_) {
|
|
queue_starved_start_ = now;
|
|
} else if (now > queue_starved_start_ + kMaximumWaitTimeMs) {
|
|
if (first_requeue_watcher_) {
|
|
if (GetConnectedNode()->GetPlayhead() + kMaximumWaitTime <
|
|
first_requeue_watcher_->property("time").value<rational>()) {
|
|
// We still have time
|
|
return;
|
|
}
|
|
}
|
|
|
|
ForceRequeueFromCurrentTime();
|
|
queue_starved_start_ = 0;
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::QueueNoLongerStarved()
|
|
{
|
|
queue_starved_start_ = 0;
|
|
}
|
|
|
|
void ViewerWidget::ForceRequeueFromCurrentTime()
|
|
{
|
|
// Defer the requeue to the next event-loop iteration. This function is often
|
|
// called from paintEvent paths (QueueStarved) where synchronously cancelling
|
|
// watchers can re-enter the same RenderTicket mutex and deadlock.
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
[this]() { ForceRequeueFromCurrentTimeInternal(); },
|
|
Qt::QueuedConnection);
|
|
}
|
|
|
|
void ViewerWidget::ForceRequeueFromCurrentTimeInternal()
|
|
{
|
|
// Allow half a second for requeue to complete
|
|
static const rational kRequeueWaitTime(1);
|
|
|
|
RenderManager::instance()->GetCacher()->ClearSingleFrameRenders();
|
|
queue_watchers_.clear();
|
|
int queue = DeterminePlaybackQueueSize();
|
|
playback_queue_next_frame_ =
|
|
GetTimestamp() +
|
|
playback_speed_ * Timecode::time_to_timestamp(
|
|
kRequeueWaitTime, timebase(), Timecode::kFloor);
|
|
;
|
|
first_requeue_watcher_ = nullptr;
|
|
for (int i = 0; i < queue; i++) {
|
|
RenderTicketWatcher *watcher = RequestNextFrameForQueue();
|
|
if (!first_requeue_watcher_) {
|
|
first_requeue_watcher_ = watcher;
|
|
connect(first_requeue_watcher_, &RenderTicketWatcher::destroyed,
|
|
this, &ViewerWidget::HandleFirstRequeueDestroy);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::UpdateTextureFromNode()
|
|
{
|
|
if (!GetConnectedNode()) {
|
|
return;
|
|
}
|
|
|
|
if (IsPlaying()) {
|
|
qWarning() << "UpdateTextureFromNode called while playing";
|
|
return;
|
|
}
|
|
|
|
rational time = GetConnectedNode()->GetPlayhead();
|
|
bool frame_exists_at_time = FrameExistsAtTime(time);
|
|
bool frame_might_be_still = ViewerMightBeAStill();
|
|
|
|
if (frame_exists_at_time || frame_might_be_still) {
|
|
// Frame was not in queue, will require rendering or decoding from cache
|
|
// Not playing, run a task to get the frame either from the cache or the renderer
|
|
RenderTicketWatcher *watcher = new RenderTicketWatcher();
|
|
watcher->setProperty("start", QDateTime::currentMSecsSinceEpoch());
|
|
watcher->setProperty("time", QVariant::fromValue(time));
|
|
connect(watcher, &RenderTicketWatcher::Finished, this,
|
|
&ViewerWidget::RendererGeneratedFrame);
|
|
nonqueue_watchers_.append(watcher);
|
|
|
|
// Clear queue because we want this frame more than any others
|
|
RenderManager::instance()
|
|
->GetCacher()
|
|
->ClearSingleFrameRendersThatArentRunning();
|
|
|
|
DetectMulticamNode(time);
|
|
|
|
watcher->SetTicket(GetFrame(time));
|
|
} else {
|
|
// There is definitely no frame here, we can immediately flip to showing nothing
|
|
nonqueue_watchers_.clear();
|
|
SetEmptyImage();
|
|
return;
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::PlayInternal(int speed, bool in_to_out_only)
|
|
{
|
|
Q_ASSERT(speed != 0);
|
|
|
|
if (!GetConnectedNode()) {
|
|
// Do nothing if no viewer node is attached
|
|
return;
|
|
}
|
|
|
|
if (timebase().isNull()) {
|
|
qCritical() << "ViewerWidget can't play with an invalid timebase";
|
|
return;
|
|
}
|
|
|
|
// Kindly tell all viewers to stop playing and caching so all resources can be used for playback
|
|
foreach (ViewerWidget *viewer, instances_) {
|
|
if (viewer != this) {
|
|
viewer->PauseInternal();
|
|
}
|
|
}
|
|
RenderManager::instance()->GetCacher()->SetThumbnailsPaused(true);
|
|
|
|
RenderManager::instance()->SetAggressiveGarbageCollection(true);
|
|
|
|
// Disarm recording if armed
|
|
if (record_armed_) {
|
|
DisarmRecording();
|
|
}
|
|
|
|
// If the playhead is beyond the end, restart at 0
|
|
if (!recording_) {
|
|
rational last_frame = GetConnectedNode()->GetLength() - timebase();
|
|
if (!in_to_out_only &&
|
|
GetConnectedNode()->GetPlayhead() >= last_frame) {
|
|
if (speed > 0) {
|
|
GetConnectedNode()->SetPlayhead(0);
|
|
} else {
|
|
GetConnectedNode()->SetPlayhead(last_frame);
|
|
}
|
|
}
|
|
}
|
|
|
|
playback_speed_ = speed;
|
|
play_in_to_out_only_ = in_to_out_only;
|
|
|
|
playback_queue_next_frame_ = GetTimestamp() + playback_speed_;
|
|
|
|
controls_->ShowPauseButton();
|
|
|
|
queue_starved_start_ = 0;
|
|
|
|
// Attempt to fill playback queue
|
|
if (IsVideoVisible()) {
|
|
prequeue_length_ = DeterminePlaybackQueueSize();
|
|
|
|
if (prequeue_length_ > 0) {
|
|
prequeuing_video_ = true;
|
|
prequeue_count_ = 0;
|
|
|
|
for (int i = 0; i < prequeue_length_; i++) {
|
|
RequestNextFrameForQueue();
|
|
}
|
|
|
|
dry_run_next_frame_ = playback_queue_next_frame_;
|
|
RequestNextDryRun();
|
|
}
|
|
}
|
|
|
|
AudioParams ap = GetConnectedNode()->GetAudioParams();
|
|
if (ap.is_valid() && ap.channel_count() != 0) {
|
|
UpdateAudioProcessor();
|
|
|
|
// Verify audio processor output params are valid before using them
|
|
AudioParams output_params = audio_processor_.to();
|
|
if (!output_params.is_valid()) {
|
|
qWarning() << "Audio processor output params are invalid, skipping audio playback";
|
|
} else {
|
|
AudioManager::instance()->SetOutputNotifyInterval(
|
|
output_params.time_to_bytes(kAudioPlaybackInterval));
|
|
connect(AudioManager::instance(), &AudioManager::OutputNotify, this,
|
|
&ViewerWidget::QueueNextAudioBuffer);
|
|
|
|
static const int prequeue_count = 2;
|
|
prequeuing_audio_ = prequeue_count; // Queue two buffers ahead of time
|
|
audio_playback_queue_time_ = GetConnectedNode()->GetPlayhead();
|
|
for (int i = 0; i < prequeue_count; i++) {
|
|
QueueNextAudioBuffer();
|
|
}
|
|
}
|
|
}
|
|
|
|
// If there's nothing to prequeue, start playback immediately so the
|
|
// playhead advances even when only the audio waveform is visible.
|
|
if (!prequeuing_video_ && !prequeuing_audio_) {
|
|
FinishPlayPreprocess();
|
|
}
|
|
|
|
// Force screen to stay awake
|
|
PreventSleep(true);
|
|
}
|
|
|
|
void ViewerWidget::PauseInternal()
|
|
{
|
|
if (recording_) {
|
|
AudioManager::instance()->StopRecording();
|
|
recording_ = false;
|
|
controls_->SetPauseButtonRecordingState(false);
|
|
|
|
recording_callback_->DisableRecordingOverlay();
|
|
recording_callback_->RecordingCallback(
|
|
recording_filename_, recording_range_, recording_track_);
|
|
}
|
|
|
|
if (IsPlaying()) {
|
|
playback_speed_ = 0;
|
|
controls_->ShowPlayButton();
|
|
|
|
foreach (ViewerDisplayWidget *dw, playback_devices_) {
|
|
dw->Pause();
|
|
}
|
|
|
|
// Cancel in-flight render tickets before deleting watchers,
|
|
// otherwise the render thread keeps working on stale frames
|
|
// and blocks the single-frame render requested by UpdateTextureFromNode().
|
|
foreach (RenderTicketWatcher *watcher, queue_watchers_) {
|
|
watcher->Cancel();
|
|
}
|
|
qDeleteAll(queue_watchers_);
|
|
queue_watchers_.clear();
|
|
RenderManager::instance()->GetCacher()->ClearSingleFrameRenders();
|
|
|
|
playback_backup_timer_.stop();
|
|
|
|
// Handle audio
|
|
AudioManager::instance()->StopOutput();
|
|
AudioMonitor::StopOnAll();
|
|
prequeued_audio_.clear();
|
|
disconnect(AudioManager::instance(), &AudioManager::OutputNotify, this,
|
|
&ViewerWidget::QueueNextAudioBuffer);
|
|
qDeleteAll(audio_playback_queue_);
|
|
audio_playback_queue_.clear();
|
|
UpdateAudioProcessor();
|
|
|
|
RenderManager::instance()->GetCacher()->SetThumbnailsPaused(false);
|
|
|
|
UpdateTextureFromNode();
|
|
|
|
RenderManager::instance()->SetAggressiveGarbageCollection(false);
|
|
}
|
|
|
|
prequeuing_video_ = false;
|
|
prequeuing_audio_ = 0;
|
|
dry_run_watchers_.clear();
|
|
|
|
// Reset screen timeout timer
|
|
PreventSleep(false);
|
|
}
|
|
|
|
void ViewerWidget::PushScrubbedAudio()
|
|
{
|
|
if (!IsPlaying() && GetConnectedNode() &&
|
|
OLIVE_CONFIG("AudioScrubbing").toBool() && enable_audio_scrubbing_) {
|
|
if (ignore_scrub_ > 0) {
|
|
ignore_scrub_--;
|
|
}
|
|
|
|
if (ignore_scrub_ == 0) {
|
|
// Get audio src device from renderer
|
|
const AudioParams ¶ms = GetConnectedNode()->GetAudioParams();
|
|
|
|
if (params.is_valid()) {
|
|
// NOTE: Hardcoded scrubbing interval (20ms)
|
|
rational interval = rational(20, 1000);
|
|
|
|
RenderTicketWatcher *watcher = new RenderTicketWatcher();
|
|
connect(watcher, &RenderTicketWatcher::Finished, this,
|
|
&ViewerWidget::ReceivedAudioBufferForScrubbing);
|
|
audio_scrub_watchers_.push_back(watcher);
|
|
watcher->SetTicket(
|
|
RenderManager::instance()->GetCacher()->GetRangeOfAudio(
|
|
GetConnectedNode(),
|
|
TimeRange(GetConnectedNode()->GetPlayhead(),
|
|
GetConnectedNode()->GetPlayhead() +
|
|
interval)));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::UpdateMinimumScale()
|
|
{
|
|
if (!GetConnectedNode()) {
|
|
return;
|
|
}
|
|
|
|
if (GetConnectedNode()->GetLength().isNull()) {
|
|
// Avoids divide by zero
|
|
SetMinimumScale(0);
|
|
} else {
|
|
double min_scale = static_cast<double>(ruler()->width()) /
|
|
GetConnectedNode()->GetLength().toDouble();
|
|
// Ensure min_scale doesn't exceed max_scale to prevent crash
|
|
min_scale = qMin(min_scale, GetMaximumScale());
|
|
SetMinimumScale(min_scale);
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::SetColorTransform(const ColorTransform &transform,
|
|
ViewerDisplayWidget *sender)
|
|
{
|
|
sender->SetColorTransform(transform);
|
|
}
|
|
|
|
QString ViewerWidget::GetCachedFilenameFromTime(const rational &time)
|
|
{
|
|
if (FrameExistsAtTime(time)) {
|
|
return GetConnectedNode()->video_frame_cache()->GetValidCacheFilename(
|
|
time);
|
|
}
|
|
|
|
return QString();
|
|
}
|
|
|
|
bool ViewerWidget::FrameExistsAtTime(const rational &time)
|
|
{
|
|
return GetConnectedNode() && time >= 0 &&
|
|
time < GetConnectedNode()->GetVideoLength();
|
|
}
|
|
|
|
bool ViewerWidget::ViewerMightBeAStill()
|
|
{
|
|
return GetConnectedNode() &&
|
|
GetConnectedNode()->GetConnectedTextureOutput() &&
|
|
GetConnectedNode()->GetVideoLength().isNull();
|
|
}
|
|
|
|
void ViewerWidget::SetDisplayImage(RenderTicketPtr ticket)
|
|
{
|
|
foreach (ViewerDisplayWidget *dw, playback_devices_) {
|
|
QVariant push;
|
|
if (ticket) {
|
|
if (dynamic_cast<MulticamDisplay *>(dw)) {
|
|
push = ticket->property("multicam_output");
|
|
} else {
|
|
push = ticket->Get();
|
|
}
|
|
}
|
|
dw->SetImage(push);
|
|
}
|
|
}
|
|
|
|
RenderTicketWatcher *ViewerWidget::RequestNextFrameForQueue(bool increment)
|
|
{
|
|
RenderTicketWatcher *watcher = nullptr;
|
|
|
|
rational next_time =
|
|
Timecode::timestamp_to_time(playback_queue_next_frame_, timebase());
|
|
|
|
if (FrameExistsAtTime(next_time) || ViewerMightBeAStill()) {
|
|
if (increment) {
|
|
playback_queue_next_frame_ += playback_speed_;
|
|
}
|
|
|
|
watcher = new RenderTicketWatcher();
|
|
watcher->setProperty("start", QDateTime::currentMSecsSinceEpoch());
|
|
watcher->setProperty("time", QVariant::fromValue(next_time));
|
|
DetectMulticamNode(next_time);
|
|
connect(watcher, &RenderTicketWatcher::Finished, this,
|
|
&ViewerWidget::RendererGeneratedFrameForQueue);
|
|
queue_watchers_.append(watcher);
|
|
watcher->SetTicket(GetFrame(next_time));
|
|
}
|
|
|
|
return watcher;
|
|
}
|
|
|
|
RenderTicketPtr ViewerWidget::GetFrame(const rational &t)
|
|
{
|
|
if (IsPlaying() || prequeuing_video_) {
|
|
return GetSingleFrame(t);
|
|
}
|
|
|
|
QString cache_fn =
|
|
GetConnectedNode()->video_frame_cache()->GetValidCacheFilename(t);
|
|
|
|
if (!QFileInfo::exists(cache_fn)) {
|
|
// Frame hasn't been cached, start render job
|
|
return GetSingleFrame(t);
|
|
} else {
|
|
// Frame has been cached, grab the frame
|
|
RenderTicketPtr ticket = std::make_shared<RenderTicket>();
|
|
ticket->setProperty("time", QVariant::fromValue(t));
|
|
QtConcurrent::run(
|
|
static_cast<void (*)(RenderTicketPtr, const QString &,
|
|
const QUuid &, const int64_t &)>(
|
|
ViewerWidget::DecodeCachedImage),
|
|
ticket,
|
|
GetConnectedNode()->video_frame_cache()->GetCacheDirectory(),
|
|
GetConnectedNode()->video_frame_cache()->GetUuid(),
|
|
Timecode::time_to_timestamp(t, timebase(), Timecode::kFloor));
|
|
return ticket;
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::FinishPlayPreprocess()
|
|
{
|
|
// Check if we're still waiting for video or audio respectively
|
|
if (prequeuing_video_ || prequeuing_audio_) {
|
|
return;
|
|
}
|
|
|
|
int64_t playback_start_time = GetTimestamp();
|
|
|
|
// Start audio waveform playback
|
|
if (!prequeued_audio_.isEmpty()) {
|
|
QString error;
|
|
if (!AudioManager::instance()->PushToOutput(audio_processor_.to(),
|
|
prequeued_audio_, &error)) {
|
|
QMessageBox::critical(
|
|
this, tr("Audio Error"),
|
|
tr("Failed to start audio: %1\n\n"
|
|
"Please check your audio preferences and try again.")
|
|
.arg(error));
|
|
}
|
|
prequeued_audio_.clear();
|
|
|
|
AudioMonitor::StartWaveformOnAll(
|
|
GetConnectedNode()->GetConnectedWaveform(),
|
|
GetConnectedNode()->GetPlayhead(), playback_speed_);
|
|
}
|
|
|
|
display_widget_->ResetFPSTimer();
|
|
|
|
foreach (ViewerDisplayWidget *dw, playback_devices_) {
|
|
dw->Play(playback_start_time, playback_speed_, timebase(),
|
|
IsVideoVisible());
|
|
}
|
|
|
|
// This is our timer for loading the queue and setting the time
|
|
playback_backup_timer_.setInterval(
|
|
qMax(1, qFloor(timebase_dbl() * 1000.0)));
|
|
playback_backup_timer_.start();
|
|
|
|
PlaybackTimerUpdate();
|
|
}
|
|
|
|
int ViewerWidget::DeterminePlaybackQueueSize()
|
|
{
|
|
if (playback_speed_ == 0) {
|
|
return 0;
|
|
}
|
|
|
|
int64_t end_ts;
|
|
|
|
if (playback_speed_ > 0) {
|
|
end_ts = Timecode::time_to_timestamp(
|
|
GetConnectedNode()->GetVideoLength(), timebase());
|
|
} else {
|
|
end_ts = 0;
|
|
}
|
|
|
|
int remaining_frames = (end_ts - GetTimestamp() - 1) / playback_speed_;
|
|
|
|
// Generate maximum queue
|
|
int max_frames =
|
|
qCeil(kVideoPlaybackInterval.toDouble() / timebase().toDouble());
|
|
|
|
return qMin(max_frames, remaining_frames);
|
|
}
|
|
|
|
void ViewerWidget::ContextMenuSetFullScreen(QAction *action)
|
|
{
|
|
SetFullScreen(QGuiApplication::screens().at(action->data().toInt()));
|
|
}
|
|
|
|
void ViewerWidget::ContextMenuSetPlaybackRes(QAction *action)
|
|
{
|
|
int div = action->data().toInt();
|
|
|
|
auto vp = GetConnectedNode()->GetVideoParams();
|
|
vp.set_divider(div);
|
|
|
|
auto c = new NodeParamSetStandardValueCommand(
|
|
NodeKeyframeTrackReference(
|
|
NodeInput(GetConnectedNode(), ViewerOutput::kVideoParamsInput, 0)),
|
|
QVariant::fromValue(vp));
|
|
Core::instance()->undo_stack()->push(c, tr("Changed Playback Resolution"));
|
|
}
|
|
|
|
void ViewerWidget::ContextMenuDisableSafeMargins()
|
|
{
|
|
context_menu_widget_->SetSafeMargins(ViewerSafeMarginInfo(false));
|
|
}
|
|
|
|
void ViewerWidget::ContextMenuSetSafeMargins()
|
|
{
|
|
context_menu_widget_->SetSafeMargins(ViewerSafeMarginInfo(true));
|
|
}
|
|
|
|
void ViewerWidget::ContextMenuSetCustomSafeMargins()
|
|
{
|
|
bool ok;
|
|
|
|
double new_ratio = GetFloatRatioFromUser(this, tr("Safe Margins"), &ok);
|
|
|
|
if (ok) {
|
|
context_menu_widget_->SetSafeMargins(
|
|
ViewerSafeMarginInfo(true, new_ratio));
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::WindowAboutToClose()
|
|
{
|
|
ViewerWindow *vw = static_cast<ViewerWindow *>(sender());
|
|
windows_.remove(windows_.key(vw));
|
|
playback_devices_.removeOne(vw->display_widget());
|
|
}
|
|
|
|
void ViewerWidget::RendererGeneratedFrame()
|
|
{
|
|
RenderTicketWatcher *ticket = static_cast<RenderTicketWatcher *>(sender());
|
|
|
|
if (nonqueue_watchers_.contains(ticket)) {
|
|
while (!nonqueue_watchers_.isEmpty()) {
|
|
// Pop frames that are "old"
|
|
if (nonqueue_watchers_.takeFirst() == ticket) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (ticket->HasResult()) {
|
|
SetDisplayImage(ticket->GetTicket());
|
|
}
|
|
}
|
|
|
|
delete ticket;
|
|
}
|
|
|
|
void ViewerWidget::RendererGeneratedFrameForQueue()
|
|
{
|
|
RenderTicketWatcher *watcher = static_cast<RenderTicketWatcher *>(sender());
|
|
|
|
if (queue_watchers_.contains(watcher)) {
|
|
queue_watchers_.removeOne(watcher);
|
|
|
|
if (watcher->HasResult()) {
|
|
QVariant frame = watcher->Get();
|
|
bool drop_frame = false;
|
|
|
|
// Ignore this signal if we've paused now
|
|
if (IsPlaying() || prequeuing_video_) {
|
|
const qint64 start_ms =
|
|
watcher->property("start").toLongLong();
|
|
const qint64 now_ms =
|
|
QDateTime::currentMSecsSinceEpoch();
|
|
const int playback_step = qMax(1, qAbs(playback_speed_));
|
|
const double frame_interval_ms = qMax(
|
|
1.0,
|
|
timebase().toDouble() * 1000.0 /
|
|
static_cast<double>(playback_step));
|
|
if (start_ms > 0 &&
|
|
(now_ms - start_ms) > frame_interval_ms) {
|
|
// If the queue is nearly empty, keep the frame anyway
|
|
// to prevent the viewer from freezing entirely when
|
|
// rendering can't keep up with playback speed.
|
|
if (display_widget_->queue()->size() >= 2) {
|
|
drop_frame = true;
|
|
}
|
|
}
|
|
|
|
rational ts = watcher->property("time").value<rational>();
|
|
|
|
if (!drop_frame) {
|
|
foreach (ViewerDisplayWidget *dw, playback_devices_) {
|
|
const bool is_multicam =
|
|
dynamic_cast<MulticamDisplay *>(dw);
|
|
QVariant push;
|
|
if (is_multicam) {
|
|
push = watcher->GetTicket()->property(
|
|
"multicam_output");
|
|
if (!push.isValid() || push.isNull()) {
|
|
// Fall back to the primary frame when multicam isn't available.
|
|
push = frame;
|
|
}
|
|
} else {
|
|
push = frame;
|
|
}
|
|
|
|
dw->queue()->AppendTimewise({ ts, push },
|
|
playback_speed_);
|
|
}
|
|
}
|
|
|
|
if (prequeuing_video_) {
|
|
prequeue_count_++;
|
|
|
|
if (prequeue_count_ == prequeue_length_) {
|
|
prequeuing_video_ = false;
|
|
FinishPlayPreprocess();
|
|
} else {
|
|
// This call was mostly necessary to keep the threads busy between prequeue and playback.
|
|
// If we only have a single render thread, it's no longer necessary.
|
|
//RequestNextFrameForQueue();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (first_requeue_watcher_ == watcher) {
|
|
first_requeue_watcher_ = nullptr;
|
|
}
|
|
|
|
delete watcher;
|
|
}
|
|
|
|
void ViewerWidget::ShowContextMenu(const QPoint &pos)
|
|
{
|
|
if (!GetConnectedNode()) {
|
|
return;
|
|
}
|
|
|
|
Menu menu(static_cast<QWidget *>(sender()));
|
|
|
|
context_menu_widget_ = dynamic_cast<ViewerDisplayWidget *>(sender());
|
|
|
|
// ViewerDisplayWidget options
|
|
if (context_menu_widget_) {
|
|
// Color options
|
|
if (context_menu_widget_->color_manager() && color_menu_enabled_) {
|
|
{
|
|
Menu *ocio_colorspace_menu =
|
|
context_menu_widget_->GetColorSpaceMenu(&menu);
|
|
menu.addMenu(ocio_colorspace_menu);
|
|
}
|
|
|
|
{
|
|
Menu *ocio_display_menu =
|
|
context_menu_widget_->GetDisplayMenu(&menu);
|
|
menu.addMenu(ocio_display_menu);
|
|
}
|
|
|
|
{
|
|
Menu *ocio_view_menu = context_menu_widget_->GetViewMenu(&menu);
|
|
menu.addMenu(ocio_view_menu);
|
|
}
|
|
|
|
{
|
|
Menu *ocio_look_menu = context_menu_widget_->GetLookMenu(&menu);
|
|
menu.addMenu(ocio_look_menu);
|
|
}
|
|
|
|
menu.addSeparator();
|
|
}
|
|
|
|
{
|
|
// Viewer Zoom Level
|
|
Menu *zoom_menu = new Menu(tr("Zoom"), &menu);
|
|
menu.addMenu(zoom_menu);
|
|
|
|
zoom_menu->addAction(tr("Fit"))->setData(-1);
|
|
for (int i = 0; i < ViewerSizer::kZoomLevelCount; i++) {
|
|
double z = ViewerSizer::kZoomLevels[i];
|
|
zoom_menu->addAction(tr("%1%").arg(z * 100.0))->setData(z);
|
|
}
|
|
|
|
connect(zoom_menu, &QMenu::triggered, this,
|
|
&ViewerWidget::SetZoomFromMenu);
|
|
}
|
|
|
|
{
|
|
// Full Screen Menu
|
|
Menu *full_screen_menu = new Menu(tr("Full Screen"), &menu);
|
|
menu.addMenu(full_screen_menu);
|
|
|
|
for (int i = 0; i < QGuiApplication::screens().size(); i++) {
|
|
QScreen *s = QGuiApplication::screens().at(i);
|
|
|
|
QAction *a = full_screen_menu->addAction(
|
|
tr("Screen %1: %2x%3")
|
|
.arg(QString::number(i),
|
|
QString::number(s->size().width()),
|
|
QString::number(s->size().height())));
|
|
|
|
a->setData(i);
|
|
a->setCheckable(true);
|
|
a->setChecked(
|
|
windows_.contains(QGuiApplication::screens().at(i)));
|
|
}
|
|
|
|
connect(full_screen_menu, &QMenu::triggered, this,
|
|
&ViewerWidget::ContextMenuSetFullScreen);
|
|
}
|
|
|
|
{
|
|
// Playback Resolution Menu
|
|
Menu *playback_res_menu =
|
|
new Menu(tr("Playback Resolution"), &menu);
|
|
menu.addMenu(playback_res_menu);
|
|
|
|
for (int d : VideoParams::kSupportedDividers) {
|
|
playback_res_menu->AddActionWithData(
|
|
VideoParams::GetNameForDivider(d), d,
|
|
GetConnectedNode()->GetVideoParams().divider());
|
|
}
|
|
|
|
connect(playback_res_menu, &QMenu::triggered, this,
|
|
&ViewerWidget::ContextMenuSetPlaybackRes);
|
|
}
|
|
|
|
{
|
|
// Deinterlace Option
|
|
if (GetConnectedNode()->GetVideoParams().interlacing() !=
|
|
VideoParams::kInterlaceNone) {
|
|
QAction *deinterlace_action = menu.addAction(tr("Deinterlace"));
|
|
deinterlace_action->setCheckable(true);
|
|
deinterlace_action->setChecked(
|
|
display_widget_->IsDeinterlacing());
|
|
connect(deinterlace_action, &QAction::triggered,
|
|
display_widget_,
|
|
&ViewerDisplayWidget::SetDeinterlacing);
|
|
}
|
|
}
|
|
|
|
menu.addSeparator();
|
|
|
|
/* TEMP: Hide sequence cache options. Want to see if clip caching supersedes it.
|
|
{
|
|
Menu* cache_menu = new Menu(tr("Cache"), &menu);
|
|
menu.addMenu(cache_menu);
|
|
|
|
// Cache Entire Sequence
|
|
QAction* cache_entire_sequence = cache_menu->addAction(tr("Cache Entire Sequence"));
|
|
connect(cache_entire_sequence, &QAction::triggered, this, &ViewerWidget::CacheEntireSequence);
|
|
|
|
// Cache In/Out Sequence
|
|
QAction* cache_inout_sequence = cache_menu->addAction(tr("Cache Sequence In/Out"));
|
|
connect(cache_inout_sequence, &QAction::triggered, this, &ViewerWidget::CacheSequenceInOut);
|
|
}*/
|
|
|
|
menu.addSeparator();
|
|
|
|
{
|
|
// Safe Margins
|
|
Menu *safe_margin_menu = new Menu(tr("Safe Margins"), &menu);
|
|
menu.addMenu(safe_margin_menu);
|
|
|
|
QAction *safe_margin_off = safe_margin_menu->addAction(tr("Off"));
|
|
safe_margin_off->setCheckable(true);
|
|
safe_margin_off->setChecked(
|
|
!context_menu_widget_->GetSafeMargin().is_enabled());
|
|
connect(safe_margin_off, &QAction::triggered, this,
|
|
&ViewerWidget::ContextMenuDisableSafeMargins);
|
|
|
|
QAction *safe_margin_on = safe_margin_menu->addAction(tr("On"));
|
|
safe_margin_on->setCheckable(true);
|
|
safe_margin_on->setChecked(
|
|
context_menu_widget_->GetSafeMargin().is_enabled() &&
|
|
!context_menu_widget_->GetSafeMargin().custom_ratio());
|
|
connect(safe_margin_on, &QAction::triggered, this,
|
|
&ViewerWidget::ContextMenuSetSafeMargins);
|
|
|
|
QAction *safe_margin_custom =
|
|
safe_margin_menu->addAction(tr("Custom Aspect"));
|
|
safe_margin_custom->setCheckable(true);
|
|
safe_margin_custom->setChecked(
|
|
context_menu_widget_->GetSafeMargin().is_enabled() &&
|
|
context_menu_widget_->GetSafeMargin().custom_ratio());
|
|
connect(safe_margin_custom, &QAction::triggered, this,
|
|
&ViewerWidget::ContextMenuSetCustomSafeMargins);
|
|
}
|
|
|
|
menu.addSeparator();
|
|
}
|
|
|
|
{
|
|
QAction *stop_playback_on_last_frame =
|
|
menu.addAction(tr("Stop Playback On Last Frame"));
|
|
stop_playback_on_last_frame->setCheckable(true);
|
|
stop_playback_on_last_frame->setChecked(
|
|
OLIVE_CONFIG("StopPlaybackOnLastFrame").toBool());
|
|
connect(stop_playback_on_last_frame, &QAction::triggered, this,
|
|
[](bool e) { OLIVE_CONFIG("StopPlaybackOnLastFrame") = e; });
|
|
|
|
menu.addSeparator();
|
|
}
|
|
|
|
{
|
|
auto waveform_menu = new Menu(tr("Audio Waveform"), &menu);
|
|
menu.addMenu(waveform_menu);
|
|
|
|
waveform_menu->AddActionWithData(tr("Automatically Show/Hide"),
|
|
kWFAutomatic, waveform_mode_);
|
|
waveform_menu->AddActionWithData(tr("Show Waveform Only"),
|
|
kWFWaveformOnly, waveform_mode_);
|
|
waveform_menu->AddActionWithData(tr("Show Both Viewer And Waveform"),
|
|
kWFViewerAndWaveform, waveform_mode_);
|
|
|
|
connect(waveform_menu, &Menu::triggered, this,
|
|
&ViewerWidget::UpdateWaveformModeFromMenu);
|
|
}
|
|
|
|
{
|
|
QAction *show_fps_action = menu.addAction(tr("Show FPS"));
|
|
show_fps_action->setCheckable(true);
|
|
show_fps_action->setChecked(display_widget_->GetShowFPS());
|
|
connect(show_fps_action, &QAction::triggered, display_widget_,
|
|
&ViewerDisplayWidget::SetShowFPS);
|
|
}
|
|
|
|
if (context_menu_widget_ == display_widget_) {
|
|
auto subtitle_menu = new Menu(tr("Subtitles"), &menu);
|
|
menu.addMenu(subtitle_menu);
|
|
|
|
QAction *show_subtitles_action =
|
|
subtitle_menu->addAction(tr("Show Subtitles"));
|
|
show_subtitles_action->setCheckable(true);
|
|
show_subtitles_action->setChecked(display_widget_->GetShowSubtitles());
|
|
connect(show_subtitles_action, &QAction::triggered, display_widget_,
|
|
&ViewerDisplayWidget::SetShowSubtitles);
|
|
|
|
subtitle_menu->addSeparator();
|
|
|
|
auto subtitle_font_properties =
|
|
subtitle_menu->addAction(tr("Subtitle Properties"));
|
|
connect(subtitle_font_properties, &QAction::triggered, this,
|
|
&ViewerWidget::ShowSubtitleProperties);
|
|
|
|
auto subtitle_antialias =
|
|
subtitle_menu->addAction(tr("Use Anti-aliasing"));
|
|
subtitle_antialias->setCheckable(true);
|
|
subtitle_antialias->setChecked(
|
|
OLIVE_CONFIG("AntialiasSubtitles").toBool());
|
|
connect(subtitle_antialias, &QAction::triggered, this, [this](bool e) {
|
|
OLIVE_CONFIG("AntialiasSubtitles") = e;
|
|
display_widget_->update();
|
|
});
|
|
}
|
|
|
|
menu.addSeparator();
|
|
|
|
auto save_frame_as_image = menu.addAction(tr("Save Frame As Image"));
|
|
connect(save_frame_as_image, &QAction::triggered, this,
|
|
&ViewerWidget::SaveFrameAsImage);
|
|
|
|
menu.exec(static_cast<QWidget *>(sender())->mapToGlobal(pos));
|
|
}
|
|
|
|
void ViewerWidget::Play(bool in_to_out_only)
|
|
{
|
|
if (in_to_out_only) {
|
|
if (GetConnectedNode() &&
|
|
GetConnectedNode()->GetWorkArea()->enabled()) {
|
|
// Jump to in point
|
|
GetConnectedNode()->SetPlayhead(
|
|
GetConnectedNode()->GetWorkArea()->in());
|
|
} else {
|
|
in_to_out_only = false;
|
|
}
|
|
} else if (record_armed_) {
|
|
DisarmRecording();
|
|
|
|
if (GetConnectedNode()->project()->filename().isEmpty()) {
|
|
QMessageBox::critical(
|
|
this, tr("Audio Recording"),
|
|
tr("Project must be saved before you can record audio."));
|
|
return;
|
|
}
|
|
|
|
QDir audio_path(QFileInfo(GetConnectedNode()->project()->filename())
|
|
.dir()
|
|
.filePath(tr("audio")));
|
|
if (!audio_path.exists()) {
|
|
audio_path.mkpath(QStringLiteral("."));
|
|
}
|
|
|
|
recording_filename_ = audio_path.filePath(QStringLiteral("%1.%2").arg(
|
|
QDateTime::currentDateTime().toString("yyyy-MM-dd hh-mm-ss"),
|
|
ExportFormat::GetExtension(static_cast<ExportFormat::Format>(
|
|
OLIVE_CONFIG("AudioRecordingFormat").toInt()))));
|
|
|
|
AudioParams ap(
|
|
OLIVE_CONFIG("AudioRecordingSampleRate").toInt(),
|
|
OLIVE_CONFIG("AudioRecordingChannelLayout").toULongLong(),
|
|
SampleFormat::from_string(OLIVE_CONFIG("AudioRecordingSampleFormat")
|
|
.toString()
|
|
.toStdString()));
|
|
|
|
EncodingParams encode_param;
|
|
encode_param.EnableAudio(
|
|
ap, static_cast<ExportCodec::Codec>(
|
|
OLIVE_CONFIG("AudioRecordingCodec").toInt()));
|
|
encode_param.SetFilename(recording_filename_);
|
|
encode_param.set_audio_bit_rate(
|
|
OLIVE_CONFIG("AudioRecordingBitRate").toInt() * 1000);
|
|
|
|
QString error;
|
|
if (AudioManager::instance()->StartRecording(encode_param, &error)) {
|
|
recording_ = true;
|
|
controls_->SetPauseButtonRecordingState(true);
|
|
recording_callback_->EnableRecordingOverlay(
|
|
TimelineCoordinate(recording_range_.in(), recording_track_));
|
|
} else {
|
|
QMessageBox::critical(
|
|
this, tr("Audio Recording"),
|
|
tr("Failed to start audio recording: %1").arg(error));
|
|
return;
|
|
}
|
|
}
|
|
|
|
PlayInternal(1, in_to_out_only);
|
|
}
|
|
|
|
void ViewerWidget::Play()
|
|
{
|
|
Play(false);
|
|
}
|
|
|
|
void ViewerWidget::Pause()
|
|
{
|
|
PauseInternal();
|
|
}
|
|
|
|
void ViewerWidget::ShuttleLeft()
|
|
{
|
|
int current_speed = playback_speed_;
|
|
|
|
if (current_speed != 0) {
|
|
PauseInternal();
|
|
}
|
|
|
|
current_speed--;
|
|
|
|
if (current_speed == 0) {
|
|
current_speed--;
|
|
}
|
|
|
|
PlayInternal(current_speed, false);
|
|
}
|
|
|
|
void ViewerWidget::ShuttleStop()
|
|
{
|
|
Pause();
|
|
}
|
|
|
|
void ViewerWidget::ShuttleRight()
|
|
{
|
|
int current_speed = playback_speed_;
|
|
|
|
if (current_speed != 0) {
|
|
PauseInternal();
|
|
}
|
|
|
|
current_speed++;
|
|
|
|
if (current_speed == 0) {
|
|
current_speed++;
|
|
}
|
|
|
|
PlayInternal(current_speed, false);
|
|
}
|
|
|
|
void ViewerWidget::SetColorTransform(const ColorTransform &transform)
|
|
{
|
|
SetColorTransform(transform, display_widget_);
|
|
}
|
|
|
|
void ViewerWidget::SetSignalCursorColorEnabled(bool e)
|
|
{
|
|
foreach (ViewerDisplayWidget *dw, playback_devices_) {
|
|
dw->SetSignalCursorColorEnabled(e);
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::TimebaseChangedEvent(const rational &timebase)
|
|
{
|
|
super::TimebaseChangedEvent(timebase);
|
|
|
|
controls_->SetTimebase(timebase);
|
|
|
|
controls_->SetTime(GetConnectedNode() ? GetConnectedNode()->GetPlayhead() :
|
|
0);
|
|
LengthChangedSlot(GetConnectedNode() ? GetConnectedNode()->GetLength() : 0);
|
|
}
|
|
|
|
void ViewerWidget::PlaybackTimerUpdate()
|
|
{
|
|
Q_ASSERT(playback_speed_ != 0);
|
|
|
|
rational current_time = Timecode::timestamp_to_time(
|
|
display_widget_->timer()->GetTimestampNow(), timebase());
|
|
|
|
rational min_time, max_time;
|
|
|
|
if (recording_ && recording_range_.out() != recording_range_.in()) {
|
|
// Limit recording range if applicable
|
|
min_time = recording_range_.in();
|
|
max_time = recording_range_.out();
|
|
|
|
} else if (play_in_to_out_only_ &&
|
|
GetConnectedNode()->GetWorkArea()->enabled()) {
|
|
// If "play in to out" is enabled or we're looping AND we have a workarea, only play the workarea
|
|
min_time = GetConnectedNode()->GetWorkArea()->in();
|
|
max_time = GetConnectedNode()->GetWorkArea()->out();
|
|
|
|
} else {
|
|
// Otherwise set the bounds to the range of the sequence
|
|
min_time = 0;
|
|
max_time = GetConnectedNode()->GetLength();
|
|
}
|
|
|
|
// If we're stopping playback on the last frame rather than after it, subtract our max time
|
|
// by one timebase unit
|
|
if (OLIVE_CONFIG("StopPlaybackOnLastFrame").toBool()) {
|
|
max_time = qMax(min_time, max_time - timebase());
|
|
}
|
|
|
|
rational time_to_set;
|
|
bool end_of_line = false;
|
|
bool play_after_pause = false;
|
|
|
|
if ((!recording_ || recording_range_.out() != recording_range_.in()) &&
|
|
((playback_speed_ < 0 && current_time <= min_time) ||
|
|
(playback_speed_ > 0 && current_time >= max_time))) {
|
|
// Determine which timestamp we tripped
|
|
rational tripped_time;
|
|
|
|
if (current_time <= min_time) {
|
|
tripped_time = min_time;
|
|
} else {
|
|
tripped_time = max_time;
|
|
}
|
|
|
|
// Signal that we've reached the end of whatever range we're playing and should either pause
|
|
// or restart playback
|
|
end_of_line = true;
|
|
|
|
if (OLIVE_CONFIG("Loop").toBool() && !recording_) {
|
|
// If we're looping, jump to the other side of the workarea and continue
|
|
time_to_set = (tripped_time == min_time) ? max_time : min_time;
|
|
|
|
// Signal to restart playback after the pause signalled by `end_of_line`
|
|
play_after_pause = true;
|
|
|
|
} else {
|
|
// Pause at the boundary we tripped
|
|
time_to_set = tripped_time;
|
|
}
|
|
|
|
} else {
|
|
// Sets time normally to whatever we calculated as the "current time"
|
|
time_to_set = current_time;
|
|
}
|
|
|
|
// Set the time. By wrapping in this bool, we prevent TimeChangedEvent's default behavior of
|
|
// pausing. Even if we pause it later with `end_of_line`, we prefer pausing after setting the time
|
|
// so that an audio scrub event, etc. isn't sent.
|
|
time_changed_from_timer_ = true;
|
|
GetConnectedNode()->SetPlayhead(time_to_set);
|
|
time_changed_from_timer_ = false;
|
|
if (end_of_line) {
|
|
// Cache the current speed
|
|
int current_speed = playback_speed_;
|
|
|
|
PauseInternal();
|
|
if (play_after_pause) {
|
|
PlayInternal(current_speed, play_in_to_out_only_);
|
|
}
|
|
}
|
|
|
|
if (IsPlaying() && IsVideoVisible()) {
|
|
while ((int(display_widget_->queue()->size()) +
|
|
queue_watchers_.size()) < DeterminePlaybackQueueSize()) {
|
|
if (!RequestNextFrameForQueue()) {
|
|
// Prevent infinite loop
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (ViewerDisplayWidget *dw, playback_devices_) {
|
|
dw->queue()->PurgeBefore(current_time, playback_speed_);
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::SetViewerResolution(int width, int height)
|
|
{
|
|
sizer_->SetChildSize(width, height);
|
|
|
|
foreach (ViewerWindow *vw, windows_) {
|
|
vw->SetResolution(width, height);
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::SetViewerPixelAspect(const rational &ratio)
|
|
{
|
|
sizer_->SetPixelAspectRatio(ratio);
|
|
|
|
foreach (ViewerWindow *vw, windows_) {
|
|
vw->SetPixelAspectRatio(ratio);
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::LengthChangedSlot(const rational &length)
|
|
{
|
|
if (last_length_ != length) {
|
|
controls_->SetEndTime(length);
|
|
UpdateMinimumScale();
|
|
|
|
if (GetConnectedNode() && length < last_length_ &&
|
|
GetConnectedNode()->GetPlayhead() >= length) {
|
|
UpdateTextureFromNode();
|
|
}
|
|
|
|
last_length_ = length;
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::InterlacingChangedSlot(VideoParams::Interlacing interlacing)
|
|
{
|
|
// Automatically set a "sane" deinterlacing option
|
|
bool deint = interlacing != VideoParams::kInterlaceNone;
|
|
|
|
foreach (ViewerDisplayWidget *dw, playback_devices_) {
|
|
dw->SetDeinterlacing(deint);
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::UpdateRendererVideoParameters()
|
|
{
|
|
VideoParams vp = GetConnectedNode()->GetVideoParams();
|
|
|
|
foreach (ViewerDisplayWidget *dw, playback_devices_) {
|
|
dw->SetVideoParams(vp);
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::UpdateRendererAudioParameters()
|
|
{
|
|
AudioParams ap = GetConnectedNode()->GetAudioParams();
|
|
|
|
UpdateAudioProcessor();
|
|
|
|
foreach (ViewerDisplayWidget *dw, playback_devices_) {
|
|
dw->SetAudioParams(ap);
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::SetZoomFromMenu(QAction *action)
|
|
{
|
|
auto s = sizer_->GetContainerSize();
|
|
sizer_->SetZoomAnchored(action->data().toDouble(), s.width() / 2,
|
|
s.height() / 2);
|
|
}
|
|
|
|
void ViewerWidget::ViewerInvalidatedVideoRange(const TimeRange &range)
|
|
{
|
|
// If our current frame is within this range, we need to update
|
|
if (!IsPlaying() && GetConnectedNode()->GetPlayhead() >= range.in() &&
|
|
(GetConnectedNode()->GetPlayhead() < range.out() ||
|
|
range.in() == range.out())) {
|
|
QMetaObject::invokeMethod(this, &ViewerWidget::UpdateTextureFromNode,
|
|
Qt::QueuedConnection);
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::UpdateWaveformModeFromMenu(QAction *a)
|
|
{
|
|
SetWaveformMode(static_cast<WaveformMode>(a->data().toInt()));
|
|
}
|
|
|
|
void ViewerWidget::DragEntered(QDragEnterEvent *event)
|
|
{
|
|
if (event->mimeData()->formats().contains(Project::kItemMimeType)) {
|
|
event->accept();
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::Dropped(QDropEvent *event)
|
|
{
|
|
QByteArray mimedata = event->mimeData()->data(Project::kItemMimeType);
|
|
QDataStream stream(&mimedata, QIODevice::ReadOnly);
|
|
|
|
// Variables to deserialize into
|
|
quintptr item_ptr = 0;
|
|
QVector<Track::Reference> enabled_streams;
|
|
|
|
while (!stream.atEnd()) {
|
|
stream >> enabled_streams >> item_ptr;
|
|
|
|
// We only need the one item
|
|
break;
|
|
}
|
|
|
|
if (item_ptr) {
|
|
Node *item = reinterpret_cast<Node *>(item_ptr);
|
|
ViewerOutput *viewer = dynamic_cast<ViewerOutput *>(item);
|
|
|
|
if (viewer) {
|
|
ConnectViewerNode(viewer);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|