/***
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 .
***/
#include "viewer.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "audio/audiomanager.h"
#include "dialog/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::instances;
ViewerWidget::ViewerWidget(ViewerDisplayWidget *display, QWidget *parent)
: super(false, true, parent)
, playback_speed_(0)
, color_menu_enabled_(true)
, time_changed_from_timer_(false)
, playback_(nullptr)
, record_armed_(false)
, recording_(false)
, enable_audio_scrubbing_(true)
, waveform_mode_(k_wf_automatic)
, 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_->set_show_widget_background(true);
playback_devices_.append(display_widget_);
connect(display_widget_, &ViewerDisplayWidget::customContextMenuRequested,
this, &ViewerWidget::show_context_menu);
connect(display_widget_, &ViewerDisplayWidget::cursor_color, this,
&ViewerWidget::cursor_color);
connect(display_widget_, &ViewerDisplayWidget::color_processor_changed, this,
&ViewerWidget::color_processor_changed);
connect(
display_widget_, &ViewerDisplayWidget::color_processor_changed, this,
[](ColorProcessorPtr processor) {
RenderManager::instance()->get_cacher()->set_display_color_processor(
processor);
});
RenderManager::instance()->get_cacher()->set_display_color_processor(
display_widget_->get_current_color_processor());
connect(display_widget_, &ViewerDisplayWidget::color_manager_changed, this,
&ViewerWidget::color_manager_changed);
connect(display_widget_, &ViewerDisplayWidget::drag_entered, this,
&ViewerWidget::drag_entered);
connect(display_widget_, &ViewerDisplayWidget::dropped, this,
&ViewerWidget::dropped);
connect(display_widget_, &ViewerDisplayWidget::texture_changed, this,
&ViewerWidget::texture_changed);
connect(display_widget_, &ViewerDisplayWidget::create_addable_at, this,
&ViewerWidget::create_addable_at);
connect(sizer_, &ViewerSizer::request_scale, display_widget_,
&ViewerDisplayWidget::set_matrix_zoom);
connect(sizer_, &ViewerSizer::request_translate, display_widget_,
&ViewerDisplayWidget::set_matrix_translate);
connect(display_widget_, &ViewerDisplayWidget::hand_drag_moved, sizer_,
&ViewerSizer::hand_drag_move);
sizer_->set_widget(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();
connect_timeline_view(waveform_view_);
layout->addWidget(waveform_view_);
// Create time ruler
layout->addWidget(ruler());
// Create scrollbar
layout->addWidget(scrollbar());
// Create lower controls
controls_ = new PlaybackControls();
controls_->set_timecode_enabled(true);
controls_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
connect(controls_, &PlaybackControls::play_clicked, this,
static_cast(&ViewerWidget::play));
connect(controls_, &PlaybackControls::pause_clicked, this,
&ViewerWidget::pause);
connect(controls_, &PlaybackControls::prev_frame_clicked, this,
&ViewerWidget::prev_frame);
connect(controls_, &PlaybackControls::next_frame_clicked, this,
&ViewerWidget::next_frame);
connect(controls_, &PlaybackControls::begin_clicked, this,
&ViewerWidget::go_to_start);
connect(controls_, &PlaybackControls::end_clicked, this,
&ViewerWidget::go_to_end);
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::show_context_menu);
connect(&playback_poll_timer_, &QTimer::timeout, this,
&ViewerWidget::playback_poll_update);
set_auto_max_scroll_bar(true);
instances.append(this);
update_waveform_view_from_mode();
connect(Core::instance(), &Core::color_picker_enabled, this,
&ViewerWidget::set_signal_cursor_color_enabled);
connect(this, &ViewerWidget::cursor_color, Core::instance(),
&Core::color_picker_color_emitted);
connect(AudioManager::instance(), &AudioManager::output_params_changed, this,
&ViewerWidget::update_audio_processor);
}
ViewerWidget::~ViewerWidget()
{
instances.removeOne(this);
// Stop and release the facade playback session.
oakengine_playback_free(playback_);
playback_ = nullptr;
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_) {
pause_internal();
}
if (record_armed_) {
disarm_recording();
}
controls_->set_time(time);
if (get_connected_node() && last_time_ != time) {
if (!is_playing()) {
update_texture_from_node();
push_scrubbed_audio();
// 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_->reset_fps_timer();
}
display_widget_->set_time(time);
}
// Send time to auto-cacher
RenderManager::instance()->get_cacher()->set_playhead(time);
last_time_ = time;
}
void ViewerWidget::ConnectNodeEvent(ViewerOutput *n)
{
connect(n, &ViewerOutput::size_changed, this,
&ViewerWidget::set_viewer_resolution);
connect(n, &ViewerOutput::pixel_aspect_changed, this,
&ViewerWidget::set_viewer_pixel_aspect);
connect(n, &ViewerOutput::length_changed, this,
&ViewerWidget::length_changed_slot);
connect(n, &ViewerOutput::interlacing_changed, this,
&ViewerWidget::interlacing_changed_slot);
connect(n, &ViewerOutput::video_params_changed, this,
&ViewerWidget::update_renderer_video_parameters);
connect(n, &ViewerOutput::video_params_changed, this,
&ViewerWidget::update_texture_from_node, Qt::QueuedConnection);
connect(n, &ViewerOutput::audio_params_changed, this,
&ViewerWidget::update_renderer_audio_parameters);
if (FrameHashCache *cache = n->video_frame_cache()) {
connect(cache, &FrameHashCache::invalidated, this,
&ViewerWidget::viewer_invalidated_video_range);
}
connect(n, &ViewerOutput::texture_input_changed, this,
&ViewerWidget::update_waveform_view_from_mode);
connect(controls_, &PlaybackControls::time_changed, n,
&ViewerOutput::set_playhead);
VideoParams vp = n->get_video_params();
interlacing_changed_slot(vp.interlacing());
ruler()->set_playback_cache(n->video_frame_cache());
set_viewer_resolution(vp.width(), vp.height());
set_viewer_pixel_aspect(vp.pixel_aspect_ratio());
last_length_ = 0;
length_changed_slot(n->get_length());
update_audio_processor();
ColorManager *color_manager = n->project()->color_manager();
foreach (ViewerDisplayWidget *dw, playback_devices_) {
dw->connect_color_manager(color_manager);
}
update_waveform_view_from_mode();
waveform_view_->set_viewer(get_connected_node());
update_renderer_video_parameters();
update_renderer_audio_parameters();
// Set texture to new texture (or null if no viewer node is available)
update_texture_from_node();
}
void ViewerWidget::DisconnectNodeEvent(ViewerOutput *n)
{
pause_internal();
disconnect(n, &ViewerOutput::size_changed, this,
&ViewerWidget::set_viewer_resolution);
disconnect(n, &ViewerOutput::pixel_aspect_changed, this,
&ViewerWidget::set_viewer_pixel_aspect);
disconnect(n, &ViewerOutput::length_changed, this,
&ViewerWidget::length_changed_slot);
disconnect(n, &ViewerOutput::interlacing_changed, this,
&ViewerWidget::interlacing_changed_slot);
disconnect(n, &ViewerOutput::video_params_changed, this,
&ViewerWidget::update_renderer_video_parameters);
disconnect(n, &ViewerOutput::video_params_changed, this,
&ViewerWidget::update_texture_from_node);
disconnect(n, &ViewerOutput::audio_params_changed, this,
&ViewerWidget::update_renderer_audio_parameters);
if (FrameHashCache *cache = n->video_frame_cache()) {
disconnect(cache, &FrameHashCache::invalidated, this,
&ViewerWidget::viewer_invalidated_video_range);
}
disconnect(n, &ViewerOutput::texture_input_changed, this,
&ViewerWidget::update_waveform_view_from_mode);
disconnect(controls_, &PlaybackControls::time_changed, n,
&ViewerOutput::set_playhead);
timeline_selected_blocks_.clear();
node_view_selected_.clear();
if (multicam_panel_) {
multicam_panel_->set_multicam_node(nullptr, nullptr, nullptr,
Rational::na_n);
}
close_audio_processor();
audio_scrub_watchers_.clear();
set_display_image(nullptr);
ruler()->set_playback_cache(nullptr);
// Effectively disables the viewer and clears the state
set_viewer_resolution(0, 0);
foreach (ViewerDisplayWidget *dw, playback_devices_) {
dw->disconnect_color_manager();
}
waveform_view_->set_viewer(nullptr);
// Queue an UpdateStack so that when it runs, the viewer node will be fully disconnected
QMetaObject::invokeMethod(this, &ViewerWidget::update_waveform_view_from_mode,
Qt::QueuedConnection);
set_gizmos(nullptr);
}
void ViewerWidget::ConnectedNodeChangeEvent(ViewerOutput *n)
{
display_widget_->set_subtitle_tracks(dynamic_cast(n));
}
void ViewerWidget::ConnectedWorkAreaChangeEvent(TimelineWorkArea *workarea)
{
waveform_view_->set_work_area(workarea);
}
void ViewerWidget::ConnectedMarkersChangeEvent(TimelineMarkerList *markers)
{
waveform_view_->set_markers(markers);
}
void ViewerWidget::ScaleChangedEvent(const double &s)
{
super::ScaleChangedEvent(s);
waveform_view_->set_scale(s);
}
void ViewerWidget::resizeEvent(QResizeEvent *event)
{
super::resizeEvent(event);
update_minimum_scale();
}
RenderTicketPtr ViewerWidget::get_single_frame(const Rational &t, bool dry)
{
return RenderManager::instance()->get_cacher()->get_single_frame(
this->get_connected_node(), t, dry);
}
void ViewerWidget::toggle_play_pause()
{
if (is_playing()) {
pause();
} else {
play();
}
}
bool ViewerWidget::is_playing() const
{
return playback_speed_ != 0;
}
void ViewerWidget::set_color_menu_enabled(bool enabled)
{
color_menu_enabled_ = enabled;
}
void ViewerWidget::set_matrix(const QMatrix4x4 &mat)
{
foreach (ViewerDisplayWidget *dw, playback_devices_) {
dw->set_matrix_crop(mat);
}
}
void ViewerWidget::set_full_screen(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()->connect_color_manager(color_manager());
connect(vw, &ViewerWindow::destroyed, this,
&ViewerWidget::window_about_to_close);
connect(vw->display_widget(),
&ViewerDisplayWidget::customContextMenuRequested, this,
&ViewerWidget::show_context_menu);
if (get_connected_node()) {
vw->set_video_params(get_connected_node()->get_video_params());
vw->display_widget()->set_deinterlacing(
vw->display_widget()->is_deinterlacing());
}
vw->display_widget()->set_image(
QVariant::fromValue(display_widget()->get_current_texture()));
playback_devices_.append(vw->display_widget());
(*vw->display_widget()->queue()) = *playback_devices_.first()->queue();
if (is_playing()) {
vw->display_widget()->play(get_timestamp(), playback_speed_, timebase(),
true);
}
windows_.insert(screen, vw);
}
void ViewerWidget::cache_entire_sequence()
{
RenderManager::instance()->get_cacher()->force_cache_range(
get_connected_node(), TimeRange(0, get_connected_node()->get_video_length()));
}
void ViewerWidget::cache_sequence_in_out()
{
if (get_connected_node() && get_connected_node()->get_work_area()->enabled()) {
RenderManager::instance()->get_cacher()->force_cache_range(
get_connected_node(), get_connected_node()->get_work_area()->range());
} else {
QMessageBox::warning(this, tr("Error"),
tr("No in or out points are set to cache."),
QMessageBox::Ok);
}
}
void ViewerWidget::set_gizmos(Node *node)
{
display_widget_->set_time_target(get_connected_node());
display_widget_->set_gizmos(node);
}
void ViewerWidget::start_capture(TimelineWidget *source, const TimeRange &time,
const Track::Reference &track)
{
get_connected_node()->set_playhead(time.in());
arm_for_recording();
recording_callback_ = source;
recording_range_ = time;
recording_track_ = track;
}
void ViewerWidget::connect_multicam_widget(MulticamWidget *p)
{
if (multicam_panel_) {
disconnect(multicam_panel_, &MulticamWidget::switched, this,
&ViewerWidget::detect_multicam_node_now);
}
multicam_panel_ = p;
if (multicam_panel_) {
connect(multicam_panel_, &MulticamWidget::switched, this,
&ViewerWidget::detect_multicam_node_now);
}
}
FramePtr ViewerWidget::decode_cached_image(const QString &cache_path,
const QUuid &cache_id,
const int64_t &time)
{
FramePtr frame = FrameHashCache::load_cache_frame(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::decode_cached_image(RenderTicketPtr ticket,
const QString &cache_path,
const QUuid &cache_id, const int64_t &time)
{
ticket->start();
FramePtr f = decode_cached_image(cache_path, cache_id, time);
if (f) {
ticket->finish(QVariant::fromValue(f));
} else {
ticket->finish();
}
}
bool ViewerWidget::should_force_waveform() const
{
return get_connected_node() &&
!get_connected_node()->get_connected_texture_output() &&
get_connected_node()->get_connected_sample_output();
}
void ViewerWidget::set_empty_image()
{
foreach (ViewerDisplayWidget *dw, playback_devices_) {
dw->set_blank();
}
}
void ViewerWidget::update_auto_cacher()
{
RenderManager::instance()->get_cacher()->set_playhead(
get_connected_node()->get_playhead());
}
void ViewerWidget::arm_for_recording()
{
controls_->start_play_blink();
record_armed_ = true;
}
void ViewerWidget::disarm_recording()
{
controls_->stop_play_blink();
record_armed_ = false;
}
void ViewerWidget::update_audio_processor()
{
if (get_connected_node()) {
close_audio_processor();
AudioParams ap = get_connected_node()->get_audio_params();
if (ap.sample_rate() <= 0 || ap.channel_count() <= 0) {
ap = AudioParams(
OAK_CONFIG("DefaultSequenceAudioFrequency").toInt(),
OAK_CONFIG("DefaultSequenceAudioLayout").toULongLong(),
ViewerOutput::k_default_sample_format);
}
ap.set_format(ViewerOutput::k_default_sample_format);
AudioParams packed(
OAK_CONFIG("AudioOutputSampleRate").toInt(),
OAK_CONFIG("AudioOutputChannelLayout").toULongLong(),
SampleFormat::from_string(OAK_CONFIG("AudioOutputSampleFormat")
.toString()
.toStdString()));
qDebug() << "ViewerWidget::UpdateAudioProcessor: from sample_rate="
<< ap.sample_rate() << "channels=" << ap.channel_count()
<< "layout_mask=0x" << Qt::hex << ap.channel_layout()
<< "to sample_rate=" << packed.sample_rate()
<< "channels=" << packed.channel_count()
<< "layout_mask=0x" << packed.channel_layout()
<< Qt::dec;
audio_processor_.open(
ap, packed, (playback_speed_ == 0) ? 1 : std::abs(playback_speed_));
}
}
void ViewerWidget::create_addable_at(const QRectF &f)
{
if (Sequence *s = dynamic_cast(get_connected_node())) {
Track::Type type = Track::k_video;
int track_index = -1;
TrackList *list = s->track_list(type);
const Rational &in = get_connected_node()->get_playhead();
Rational length = OAK_CONFIG("DefaultStillLength").value();
Rational out = in + length;
// Find a free track where we won't overwrite anything
while (true) {
track_index++;
if (track_index >= list->get_track_count()) {
// Just create a new track
break;
}
Track *track = list->get_track_at(track_index);
if (track->is_locked()) {
continue;
}
Block *b = track->nearest_block_before_or_at(in);
if (!b || (dynamic_cast(b) && b->out() >= out)) {
break;
}
}
MultiUndoCommand *command = new MultiUndoCommand();
Node *clip = AddTool::create_addable_clip(
command, s, Track::Reference(type, track_index), in, length);
if (ShapeNodeBase *shape = dynamic_cast(clip)) {
shape->set_rect(f, s->get_video_params(), command);
}
Core::instance()->undo_stack()->push(command, tr("Created Shape"));
set_gizmos(clip);
}
}
void ViewerWidget::show_subtitle_properties()
{
QFont f(OAK_CONFIG("DefaultSubtitleFamily").toString(),
OAK_CONFIG("DefaultSubtitleSize").toInt(),
OAK_CONFIG("DefaultSubtitleWeight").toInt());
QFontDialog fd(f, this);
if (fd.exec() == QDialog::Accepted) {
f = fd.selectedFont();
OAK_CONFIG("DefaultSubtitleSize") = f.pointSize();
OAK_CONFIG("DefaultSubtitleFamily") = f.family();
OAK_CONFIG("DefaultSubtitleWeight") = f.weight();
display_widget_->update();
}
}
void ViewerWidget::save_frame_as_image()
{
Core::instance()->open_export_dialog_for_viewer(get_connected_node(), true);
}
void ViewerWidget::detect_multicam_node_now()
{
if (get_connected_node()) {
detect_multicam_node(get_connected_node()->get_playhead());
}
}
void ViewerWidget::close_audio_processor()
{
audio_processor_.close();
}
void ViewerWidget::set_waveform_mode(WaveformMode wf)
{
waveform_mode_ = wf;
update_waveform_view_from_mode();
}
void ViewerWidget::detect_multicam_node(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(get_connected_node())) {
// Prefer selected nodes
for (Node *n : qAsConst(node_view_selected_)) {
if ((multicam = dynamic_cast(n))) {
// Found multicam, now try to find corresponding clip from selected timeline blocks
for (Block *b : qAsConst(timeline_selected_blocks_)) {
if (ClipBlock *c = dynamic_cast(b)) {
if (c->range().contains(time) &&
c->context_contains_node(multicam)) {
clip = c;
break;
}
}
}
break;
}
}
// Next, prefer multicam from selected block
if (!multicam) {
for (Block *b : qAsConst(timeline_selected_blocks_)) {
if (b->range().contains(time)) {
if ((clip = dynamic_cast(b))) {
if ((multicam = clip->find_multicam())) {
break;
}
}
}
}
}
if (!multicam) {
const QVector