/*** 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 . ***/ #include "viewer.h" #include #include #include #include #include #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::CachedFrameReady, this, &ViewerWidget::RendererCachedFrame); 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(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::SetTexture(OpenGLTexturePtr tex) { gl_widget_->SetTexture(tex); } void ViewerWidget::UpdateTextureFromNode(const rational& time) { if (!GetConnectedNode()) { SetTexture(nullptr); } else { SetTexture(video_renderer_->GetCachedFrameAsTexture(time)); } } 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(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(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::RendererCachedFrame(const rational &time, QVariant value, qint64 job_time) { if (GetTime() == time && job_time > frame_cache_job_time_) { frame_cache_job_time_ = job_time; SetTexture(value.value()); } } 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()); }