context More intuitive code flow and allows the user to undock the viewer (which forcibly destroys and recreates the context) and the viewer will handle creation of the new texture in said new context.
520 lines
15 KiB
C++
520 lines
15 KiB
C++
/***
|
|
|
|
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 "viewer.h"
|
|
|
|
#include <QDateTime>
|
|
#include <QLabel>
|
|
#include <QResizeEvent>
|
|
#include <QtMath>
|
|
#include <QVBoxLayout>
|
|
|
|
#include "audio/audiomanager.h"
|
|
#include "common/timecodefunctions.h"
|
|
#include "config/config.h"
|
|
#include "project/item/sequence/sequence.h"
|
|
#include "project/project.h"
|
|
#include "render/pixelservice.h"
|
|
#include "widget/menu/menu.h"
|
|
|
|
ViewerWidget::ViewerWidget(QWidget *parent) :
|
|
TimeBasedWidget(false, true, parent),
|
|
playback_speed_(0),
|
|
frame_cache_job_time_(0),
|
|
color_menu_enabled_(true),
|
|
divider_(Config::Current()["DefaultViewerDivider"].toInt()),
|
|
override_color_manager_(nullptr),
|
|
time_changed_from_timer_(false)
|
|
{
|
|
// Set up main layout
|
|
QVBoxLayout* layout = new QVBoxLayout(this);
|
|
layout->setMargin(0);
|
|
|
|
// Create main OpenGL-based view
|
|
sizer_ = new ViewerSizer();
|
|
layout->addWidget(sizer_);
|
|
|
|
gl_widget_ = new ViewerGLWidget();
|
|
connect(gl_widget_, &ViewerGLWidget::customContextMenuRequested, this, &ViewerWidget::ShowContextMenu);
|
|
sizer_->SetWidget(gl_widget_);
|
|
|
|
// Create time ruler
|
|
layout->addWidget(ruler());
|
|
|
|
// Create scrollbar
|
|
layout->addWidget(scrollbar());
|
|
connect(scrollbar(), &QScrollBar::valueChanged, ruler(), &TimeRuler::SetScroll);
|
|
|
|
// Create lower controls
|
|
controls_ = new PlaybackControls();
|
|
controls_->SetTimecodeEnabled(true);
|
|
controls_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
|
|
connect(controls_, &PlaybackControls::PlayClicked, this, &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);
|
|
connect(controls_, &PlaybackControls::TimeChanged, this, &ViewerWidget::SetTimeAndSignal);
|
|
layout->addWidget(controls_);
|
|
|
|
// Connect timer
|
|
connect(&playback_timer_, &QTimer::timeout, this, &ViewerWidget::PlaybackTimerUpdate);
|
|
|
|
// FIXME: Magic number
|
|
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);
|
|
audio_renderer_ = new AudioBackend(this);
|
|
|
|
connect(PixelService::instance(), &PixelService::FormatChanged, this, &ViewerWidget::UpdateRendererParameters);
|
|
|
|
SetAutoMaxScrollBar(true);
|
|
}
|
|
|
|
void ViewerWidget::TimeChangedEvent(const int64_t &i)
|
|
{
|
|
if (!time_changed_from_timer_) {
|
|
Pause();
|
|
}
|
|
|
|
controls_->SetTime(i);
|
|
|
|
if (GetConnectedNode() && last_time_ != i) {
|
|
rational time_set = Timecode::timestamp_to_time(i, timebase());
|
|
|
|
UpdateTextureFromNode(time_set);
|
|
|
|
PushScrubbedAudio();
|
|
}
|
|
|
|
last_time_ = i;
|
|
}
|
|
|
|
void ViewerWidget::ConnectNodeInternal(ViewerOutput *n)
|
|
{
|
|
SetTimebase(n->video_params().time_base());
|
|
|
|
connect(n, &ViewerOutput::TimebaseChanged, this, &ViewerWidget::SetTimebase);
|
|
connect(n, &ViewerOutput::SizeChanged, this, &ViewerWidget::SizeChangedSlot);
|
|
connect(n, &ViewerOutput::LengthChanged, this, &ViewerWidget::LengthChangedSlot);
|
|
connect(n, &ViewerOutput::VisibleInvalidated, this, &ViewerWidget::InvalidateVisible);
|
|
|
|
SizeChangedSlot(n->video_params().width(), n->video_params().height());
|
|
LengthChangedSlot(n->Length());
|
|
|
|
if (override_color_manager_) {
|
|
gl_widget_->ConnectColorManager(override_color_manager_);
|
|
} else if (n->parent()) {
|
|
gl_widget_->ConnectColorManager(static_cast<Sequence*>(n->parent())->project()->color_manager());
|
|
} else {
|
|
qWarning() << "Failed to find a suitable color manager for the connected viewer node";
|
|
}
|
|
|
|
divider_ = CalculateDivider();
|
|
|
|
UpdateRendererParameters();
|
|
}
|
|
|
|
void ViewerWidget::DisconnectNodeInternal(ViewerOutput *n)
|
|
{
|
|
SetTimebase(0);
|
|
|
|
disconnect(n, &ViewerOutput::TimebaseChanged, this, &ViewerWidget::SetTimebase);
|
|
disconnect(n, &ViewerOutput::SizeChanged, this, &ViewerWidget::SizeChangedSlot);
|
|
disconnect(n, &ViewerOutput::LengthChanged, this, &ViewerWidget::LengthChangedSlot);
|
|
disconnect(n, &ViewerOutput::VisibleInvalidated, this, &ViewerWidget::InvalidateVisible);
|
|
|
|
// Effectively disables the viewer and clears the state
|
|
SizeChangedSlot(0, 0);
|
|
|
|
gl_widget_->DisconnectColorManager();
|
|
}
|
|
|
|
void ViewerWidget::ConnectedNodeChanged(ViewerOutput *n)
|
|
{
|
|
video_renderer_->SetViewerNode(n);
|
|
audio_renderer_->SetViewerNode(n);
|
|
}
|
|
|
|
void ViewerWidget::resizeEvent(QResizeEvent *event)
|
|
{
|
|
TimeBasedWidget::resizeEvent(event);
|
|
|
|
int new_div = CalculateDivider();
|
|
if (new_div != divider_) {
|
|
divider_ = new_div;
|
|
|
|
UpdateRendererParameters();
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::TogglePlayPause()
|
|
{
|
|
if (IsPlaying()) {
|
|
Pause();
|
|
} else {
|
|
Play();
|
|
}
|
|
}
|
|
|
|
bool ViewerWidget::IsPlaying() const
|
|
{
|
|
return playback_timer_.isActive();
|
|
}
|
|
|
|
void ViewerWidget::ConnectViewerNode(ViewerOutput *node, ColorManager* color_manager)
|
|
{
|
|
override_color_manager_ = color_manager;
|
|
|
|
TimeBasedWidget::ConnectViewerNode(node);
|
|
|
|
// Set texture to new texture (or null if no viewer node is available)
|
|
UpdateTextureFromNode(GetTime());
|
|
}
|
|
|
|
void ViewerWidget::SetColorMenuEnabled(bool enabled)
|
|
{
|
|
color_menu_enabled_ = enabled;
|
|
}
|
|
|
|
void ViewerWidget::SetOverrideSize(int width, int height)
|
|
{
|
|
SizeChangedSlot(width, height);
|
|
}
|
|
|
|
void ViewerWidget::SetMatrix(const QMatrix4x4 &mat)
|
|
{
|
|
gl_widget_->SetMatrix(mat);
|
|
}
|
|
|
|
VideoRenderBackend *ViewerWidget::video_renderer() const
|
|
{
|
|
return video_renderer_;
|
|
}
|
|
|
|
void ViewerWidget::UpdateTextureFromNode(const rational& time)
|
|
{
|
|
if (!GetConnectedNode() || time >= GetConnectedNode()->Length()) {
|
|
gl_widget_->SetImage(QString());
|
|
} else {
|
|
QString frame_fn = video_renderer_->GetCachedFrame(time);
|
|
|
|
if (!frame_fn.isEmpty()) {
|
|
gl_widget_->SetImage(frame_fn);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::PlayInternal(int speed)
|
|
{
|
|
Q_ASSERT(speed != 0);
|
|
|
|
if (timebase().isNull()) {
|
|
qWarning() << "ViewerWidget can't play with an invalid timebase";
|
|
return;
|
|
}
|
|
|
|
playback_speed_ = speed;
|
|
|
|
QIODevice* audio_src = audio_renderer_->GetAudioPullDevice();
|
|
if (audio_src != nullptr && audio_src->open(QIODevice::ReadOnly)) {
|
|
audio_src->seek(audio_renderer_->params().time_to_bytes(GetTime()));
|
|
AudioManager::instance()->SetOutputParams(audio_renderer_->params());
|
|
AudioManager::instance()->StartOutput(audio_src, playback_speed_);
|
|
}
|
|
|
|
start_msec_ = QDateTime::currentMSecsSinceEpoch();
|
|
start_timestamp_ = ruler()->GetTime();
|
|
|
|
playback_timer_.start();
|
|
|
|
controls_->ShowPauseButton();
|
|
}
|
|
|
|
void ViewerWidget::PushScrubbedAudio()
|
|
{
|
|
if (!IsPlaying() && Config::Current()["AudioScrubbing"].toBool()) {
|
|
// Get audio src device from renderer
|
|
QIODevice* audio_src = audio_renderer_->GetAudioPullDevice();
|
|
|
|
if (audio_src && audio_src->open(QFile::ReadOnly)) {
|
|
// Try to get one "frame" of audio
|
|
int size_of_sample = audio_renderer_->params().time_to_bytes(timebase());
|
|
|
|
// Push audio
|
|
audio_src->seek(audio_renderer_->params().time_to_bytes(GetTime()));
|
|
QByteArray frame_audio = audio_src->read(size_of_sample);
|
|
AudioManager::instance()->SetOutputParams(audio_renderer_->params());
|
|
AudioManager::instance()->PushToOutput(frame_audio);
|
|
|
|
audio_src->close();
|
|
}
|
|
}
|
|
}
|
|
|
|
int ViewerWidget::CalculateDivider()
|
|
{
|
|
if (GetConnectedNode() && Config::Current()["AutoSelectDivider"].toBool()) {
|
|
int long_side_of_video = qMax(GetConnectedNode()->video_params().width(), GetConnectedNode()->video_params().height());
|
|
int long_side_of_widget = qMax(gl_widget_->width(), gl_widget_->height());
|
|
|
|
return qMax(1, long_side_of_video / long_side_of_widget);
|
|
}
|
|
|
|
return divider_;
|
|
}
|
|
|
|
void ViewerWidget::UpdateRendererParameters()
|
|
{
|
|
if (!GetConnectedNode()) {
|
|
return;
|
|
}
|
|
|
|
RenderMode::Mode render_mode = RenderMode::kOffline;
|
|
|
|
video_renderer_->SetParameters(VideoRenderingParams(GetConnectedNode()->video_params(),
|
|
PixelService::instance()->GetConfiguredFormatForMode(render_mode),
|
|
render_mode,
|
|
divider_));
|
|
audio_renderer_->SetParameters(AudioRenderingParams(GetConnectedNode()->audio_params(),
|
|
SampleFormat::GetConfiguredFormatForMode(render_mode)));
|
|
|
|
video_renderer_->InvalidateCache(0, GetConnectedNode()->Length());
|
|
}
|
|
|
|
void ViewerWidget::ShowContextMenu(const QPoint &pos)
|
|
{
|
|
QMenu menu(this);
|
|
|
|
// Color options
|
|
if (gl_widget_->color_manager() && color_menu_enabled_) {
|
|
QStringList displays = gl_widget_->color_manager()->ListAvailableDisplays();
|
|
QMenu* ocio_display_menu = menu.addMenu(tr("Display"));
|
|
connect(ocio_display_menu, &QMenu::triggered, this, &ViewerWidget::ColorDisplayChanged);
|
|
foreach (const QString& d, displays) {
|
|
QAction* action = ocio_display_menu->addAction(d);
|
|
action->setCheckable(true);
|
|
action->setChecked(gl_widget_->ocio_display() == d);
|
|
action->setData(d);
|
|
}
|
|
|
|
QStringList views = gl_widget_->color_manager()->ListAvailableViews(gl_widget_->ocio_display());
|
|
QMenu* ocio_view_menu = menu.addMenu(tr("View"));
|
|
connect(ocio_view_menu, &QMenu::triggered, this, &ViewerWidget::ColorViewChanged);
|
|
foreach (const QString& v, views) {
|
|
QAction* action = ocio_view_menu->addAction(v);
|
|
action->setCheckable(true);
|
|
action->setChecked(gl_widget_->ocio_view() == v);
|
|
action->setData(v);
|
|
}
|
|
|
|
QStringList looks = gl_widget_->color_manager()->ListAvailableLooks();
|
|
QMenu* ocio_look_menu = menu.addMenu(tr("Look"));
|
|
connect(ocio_look_menu, &QMenu::triggered, this, &ViewerWidget::ColorLookChanged);
|
|
QAction* no_look_action = ocio_look_menu->addAction(tr("(None)"));
|
|
no_look_action->setCheckable(true);
|
|
no_look_action->setChecked(gl_widget_->ocio_look().isEmpty());
|
|
foreach (const QString& l, looks) {
|
|
QAction* action = ocio_look_menu->addAction(l);
|
|
action->setCheckable(true);
|
|
action->setChecked(gl_widget_->ocio_look() == l);
|
|
action->setData(l);
|
|
}
|
|
|
|
menu.addSeparator();
|
|
}
|
|
|
|
// Playback resolution
|
|
QMenu* playback_resolution_menu = menu.addMenu(tr("Resolution"));
|
|
playback_resolution_menu->addAction(tr("Full"))->setData(1);
|
|
playback_resolution_menu->addAction(tr("1/2"))->setData(2);
|
|
playback_resolution_menu->addAction(tr("1/4"))->setData(4);
|
|
playback_resolution_menu->addAction(tr("1/8"))->setData(8);
|
|
playback_resolution_menu->addAction(tr("1/16"))->setData(16);
|
|
connect(playback_resolution_menu, &QMenu::triggered, this, &ViewerWidget::SetDividerFromMenu);
|
|
|
|
foreach (QAction* a, playback_resolution_menu->actions()) {
|
|
a->setCheckable(true);
|
|
if (a->data() == divider_) {
|
|
a->setChecked(true);
|
|
}
|
|
}
|
|
|
|
menu.exec(static_cast<QWidget*>(sender())->mapToGlobal(pos));
|
|
}
|
|
|
|
void ViewerWidget::Play()
|
|
{
|
|
PlayInternal(1);
|
|
}
|
|
|
|
void ViewerWidget::Pause()
|
|
{
|
|
if (IsPlaying()) {
|
|
AudioManager::instance()->StopOutput();
|
|
playback_speed_ = 0;
|
|
controls_->ShowPlayButton();
|
|
playback_timer_.stop();
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::ShuttleLeft()
|
|
{
|
|
int current_speed = playback_speed_;
|
|
|
|
if (current_speed != 0) {
|
|
Pause();
|
|
}
|
|
|
|
current_speed--;
|
|
|
|
if (current_speed != 0) {
|
|
PlayInternal(current_speed);
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::ShuttleStop()
|
|
{
|
|
Pause();
|
|
}
|
|
|
|
void ViewerWidget::ShuttleRight()
|
|
{
|
|
int current_speed = playback_speed_;
|
|
|
|
if (current_speed != 0) {
|
|
Pause();
|
|
}
|
|
|
|
current_speed++;
|
|
|
|
if (current_speed != 0) {
|
|
PlayInternal(current_speed);
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::SetOCIOParameters(const QString &display, const QString &view, const QString &look)
|
|
{
|
|
gl_widget_->SetOCIOParameters(display, view, look);
|
|
}
|
|
|
|
void ViewerWidget::SetOCIODisplay(const QString &display)
|
|
{
|
|
gl_widget_->SetOCIODisplay(display);
|
|
}
|
|
|
|
void ViewerWidget::SetOCIOView(const QString &view)
|
|
{
|
|
gl_widget_->SetOCIOView(view);
|
|
}
|
|
|
|
void ViewerWidget::SetOCIOLook(const QString &look)
|
|
{
|
|
gl_widget_->SetOCIOLook(look);
|
|
}
|
|
|
|
void ViewerWidget::TimebaseChangedEvent(const rational &timebase)
|
|
{
|
|
TimeBasedWidget::TimebaseChangedEvent(timebase);
|
|
|
|
controls_->SetTimebase(timebase);
|
|
|
|
controls_->SetTime(ruler()->GetTime());
|
|
LengthChangedSlot(GetConnectedNode() ? GetConnectedNode()->Length() : 0);
|
|
|
|
playback_timer_.setInterval(qFloor(timebase.toDouble()));
|
|
}
|
|
|
|
void ViewerWidget::PlaybackTimerUpdate()
|
|
{
|
|
int64_t real_time = QDateTime::currentMSecsSinceEpoch() - start_msec_;
|
|
|
|
int64_t frames_since_start = qRound(static_cast<double>(real_time) / (timebase_dbl() * 1000));
|
|
|
|
int64_t current_time = start_timestamp_ + frames_since_start * playback_speed_;
|
|
|
|
if (current_time < 0) {
|
|
SetTimeAndSignal(0);
|
|
} else {
|
|
time_changed_from_timer_ = true;
|
|
SetTimeAndSignal(current_time);
|
|
time_changed_from_timer_ = false;
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::RendererCachedTime(const rational &time, qint64 job_time)
|
|
{
|
|
if (GetTime() == time && job_time > frame_cache_job_time_) {
|
|
frame_cache_job_time_ = job_time;
|
|
|
|
UpdateTextureFromNode(GetTime());
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::SizeChangedSlot(int width, int height)
|
|
{
|
|
sizer_->SetChildSize(width, height);
|
|
}
|
|
|
|
void ViewerWidget::LengthChangedSlot(const rational &length)
|
|
{
|
|
controls_->SetEndTime(Timecode::time_to_timestamp(length, timebase()));
|
|
ruler()->SetCacheStatusLength(length);
|
|
}
|
|
|
|
void ViewerWidget::ColorDisplayChanged(QAction* action)
|
|
{
|
|
SetOCIODisplay(action->data().toString());
|
|
}
|
|
|
|
void ViewerWidget::ColorViewChanged(QAction *action)
|
|
{
|
|
SetOCIOView(action->data().toString());
|
|
}
|
|
|
|
void ViewerWidget::ColorLookChanged(QAction *action)
|
|
{
|
|
SetOCIOLook(action->data().toString());
|
|
}
|
|
|
|
void ViewerWidget::SetDividerFromMenu(QAction *action)
|
|
{
|
|
int divider = action->data().toInt();
|
|
|
|
if (divider <= 0) {
|
|
qWarning() << "Tried to set invalid divider:" << divider;
|
|
return;
|
|
}
|
|
|
|
divider_ = divider;
|
|
|
|
UpdateRendererParameters();
|
|
}
|
|
|
|
void ViewerWidget::InvalidateVisible()
|
|
{
|
|
video_renderer_->InvalidateCache(GetTime(), GetTime());
|
|
}
|