/***
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 "oakengine/audio.h"
#include "oakengine/encoding.h"
#include "oakengine/project.h"
#include "olive/core/oakcore/audioparams.h"
#include "dialog/ratiodialog.h"
#include "common/configwrapper.h"
#include "core.h"
#include "engineeventbridge.h"
#include "node/block/gap/gap.h"
#include "oakengine/encoding.h"
#include "oakengine/display.h"
#include "oakengine/viewer.h"
#include "oakengine/videoparams.h"
#include "node/generator/shape/shapenodebase.h"
#include "node/project.h"
#include "panel/multicam/multicampanel.h"
#include "panel/panelmanager.h"
#include "oakengine/preview.h"
#include "oakengine/renderer.h"
#include "oakengine/timeline.h"
#include "oakengine/undo.h"
#include "olive/core/oakcore/samplebuffer.h"
#include "olive/core/render/samplebuffer.h"
#include "codec/frame.h"
#include
#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"
#include "widget/viewer/vieweroutpututils.h"
namespace olive
{
#define super TimeBasedWidget
QVector ViewerWidget::instances;
namespace {
// Convert olive::core::AudioParams to a temporary OakAudioParams*.
// The caller must free the result with oakcore_audioparams_free().
OakAudioParams *ap_to_oak(const olive::core::AudioParams &ap)
{
return oakcore_audioparams_create(
ap.sample_rate(),
ap.channel_layout(),
static_cast(ap.format()));
}
// Extract a SampleBuffer from an OakEnginePreviewRequest's audio result.
// Returns an unallocated (null) buffer on failure.
static SampleBuffer preview_req_to_sample_buffer(OakEnginePreviewRequest *req)
{
OakSampleBuffer *sb = oakcore_samplebuffer_create();
if (oakengine_preview_request_has_result(req)) {
int channels = oakengine_preview_request_get_audio_channel_count(req);
int sample_rate = oakengine_preview_request_get_audio_sample_rate(req);
if (channels > 0 && sample_rate > 0) {
OakAudioParams *ap = oakcore_audioparams_create(sample_rate, 0, 0);
oakcore_samplebuffer_set_audio_params(sb, ap);
oakcore_audioparams_free(ap);
// Determine sample count from first channel
int n = oakengine_preview_request_get_audio_samples(req, 0, nullptr, 1 << 30);
if (n > 0) {
oakcore_samplebuffer_set_sample_count(sb, size_t(n));
oakcore_samplebuffer_allocate(sb);
for (int ch = 0; ch < channels; ch++) {
float *dst = oakcore_samplebuffer_data(sb, ch);
oakengine_preview_request_get_audio_samples(req, ch, dst, n);
}
}
}
}
return SampleBuffer::from_handle(sb);
}
// Bridge from OakEnginePreviewRequest C callback to ViewerWidget main-thread
// slot. Each trampoline calls the appropriate method on the widget; the
// method finds the completed request by scanning its list.
static void nonqueue_finished_cb(void *userdata)
{
QMetaObject::invokeMethod(static_cast(userdata),
"renderer_generated_frame", Qt::QueuedConnection);
}
static void queue_finished_cb(void *userdata)
{
QMetaObject::invokeMethod(static_cast(userdata),
"renderer_generated_frame_for_queue", Qt::QueuedConnection);
}
static void audio_playback_finished_cb(void *userdata)
{
QMetaObject::invokeMethod(
static_cast(userdata),
"received_audio_buffer_for_playback", Qt::QueuedConnection);
}
static void audio_scrub_finished_cb(void *userdata)
{
QMetaObject::invokeMethod(
static_cast(userdata),
"received_audio_buffer_for_scrubbing", Qt::QueuedConnection);
}
static void dry_run_finished_cb(void *userdata)
{
QMetaObject::invokeMethod(static_cast(userdata),
"dry_run_finished", Qt::QueuedConnection);
}
} // namespace
// 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::k_audio_playback_interval = Rational(1, 4);
const Rational k_video_playback_interval = 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_(k_wf_automatic)
, ignore_scrub_(0)
, multicam_panel_(nullptr)
, bridge_(new EngineEventBridge(this))
, audio_processor_(oakengine_audio_processor_create())
{
// 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,
[](ColorProcessorHandlePtr processor) {
oakengine_render_cache_set_display_color_processor(
processor ? processor.get() : nullptr);
});
oakengine_render_cache_set_display_color_processor(
display_widget_->get_current_color_processor().get());
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::queue_starved, this,
&ViewerWidget::queue_starved);
connect(display_widget_, &ViewerDisplayWidget::queue_no_longer_starved, this,
&ViewerWidget::queue_no_longer_starved);
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_backup_timer_, &QTimer::timeout, this,
&ViewerWidget::playback_timer_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(bridge_, &EngineEventBridge::audio_output_params_changed, this,
&ViewerWidget::update_audio_processor);
}
ViewerWidget::~ViewerWidget()
{
instances.removeOne(this);
auto windows = windows_;
foreach (ViewerWindow *window, windows) {
delete window;
}
delete display_widget_;
display_widget_ = nullptr;
oakengine_audio_processor_free(audio_processor_);
audio_processor_ = 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
oakengine_preview_cacher_set_playhead(time.numerator(), time.denominator());
last_time_ = time;
}
void ViewerWidget::ConnectNodeEvent(ViewerOutput *n)
{
OakEngineNode *handle = reinterpret_cast(n);
bridge_->subscribe(handle, OAKENGINE_EVENT_VIEWER_SIZE_CHANGED);
bridge_->subscribe(handle, OAKENGINE_EVENT_VIEWER_PIXEL_ASPECT_CHANGED);
bridge_->subscribe(handle, OAKENGINE_EVENT_VIEWER_LENGTH_CHANGED);
bridge_->subscribe(handle, OAKENGINE_EVENT_VIEWER_INTERLACING_CHANGED);
bridge_->subscribe(handle, OAKENGINE_EVENT_VIEWER_VIDEO_PARAMS_CHANGED);
bridge_->subscribe(handle, OAKENGINE_EVENT_VIEWER_AUDIO_PARAMS_CHANGED);
bridge_->subscribe(handle, OAKENGINE_EVENT_VIEWER_TEXTURE_INPUT_CHANGED);
connect(bridge_, &EngineEventBridge::viewer_size_changed, this,
[this](OakEngineNode *, int w, int h) {
set_viewer_resolution(w, h);
});
connect(bridge_, &EngineEventBridge::viewer_pixel_aspect_changed, this,
[this](OakEngineNode *, qint64 num, qint64 den) {
set_viewer_pixel_aspect(Rational(num, den));
});
connect(bridge_, &EngineEventBridge::viewer_length_changed, this,
[this](OakEngineNode *, qint64 num, qint64 den) {
length_changed_slot(Rational(num, den));
});
connect(bridge_, &EngineEventBridge::viewer_interlacing_changed, this,
[this](OakEngineNode *, int mode) {
interlacing_changed_slot(mode);
});
connect(bridge_, &EngineEventBridge::viewer_video_params_changed, this,
&ViewerWidget::update_renderer_video_parameters);
connect(bridge_, &EngineEventBridge::viewer_video_params_changed, this,
&ViewerWidget::update_texture_from_node, Qt::QueuedConnection);
connect(bridge_, &EngineEventBridge::viewer_audio_params_changed, this,
&ViewerWidget::update_renderer_audio_parameters);
connect(bridge_, &EngineEventBridge::viewer_texture_input_changed, this,
&ViewerWidget::update_waveform_view_from_mode);
// FrameHashCache invalidated events
if (OakEngineFrameCache *cache = oakengine_viewer_get_frame_cache(handle)) {
bridge_->subscribe(cache, OAKENGINE_EVENT_FRAME_CACHE_INVALIDATED);
connect(bridge_, &EngineEventBridge::frame_cache_invalidated, this,
[this](void *, qint64 a, qint64 b) {
viewer_invalidated_video_range(
TimeRange(Rational(a, 1), Rational(b, 1)));
});
}
// Connect controls to set_playhead via facade
connect(controls_, &PlaybackControls::time_changed, this,
[handle](const Rational &time) {
oakengine_viewer_set_playhead(
handle, time.numerator(), time.denominator());
});
oak_video_params vp;
oakengine_viewer_get_video_params(handle, 0, &vp);
interlacing_changed_slot(vp.interlacing);
ruler()->set_playback_cache(
reinterpret_cast(
oakengine_viewer_get_playback_cache(handle)));
set_viewer_resolution(vp.width, vp.height);
set_viewer_pixel_aspect(Rational(vp.pixel_aspect_num, vp.pixel_aspect_den));
last_length_ = 0;
{
int64_t len_num, len_den;
oakengine_viewer_get_length(handle, &len_num, &len_den);
length_changed_slot(Rational(len_num, len_den));
}
update_audio_processor();
OakEngineColorManager *color_manager = oak_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 all bridge signal connections to this receiver
disconnect(bridge_, nullptr, this, nullptr);
// Unsubscribe all bridge subscriptions
// (EngineEventBridge does not expose bulk unsubscribe; the bridge
// is per-widget so stale subscriptions are harmless until the next
// ConnectNodeEvent, but we clear the ones we know about)
// For now the subscription callbacks filter by source handle internally.
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();
}
OakEnginePreviewRequest *ViewerWidget::get_single_frame(const Rational &t,
bool dry)
{
OakEngineNode *viewer =
reinterpret_cast(get_connected_node());
return oakengine_preview_request_single_frame(
viewer, t.numerator(), t.denominator(), dry ? 1 : 0);
}
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(viewer_output_video_params(get_connected_node()));
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()
{
oakengine_preview_cacher_force_cache_range(
reinterpret_cast(get_connected_node()),
0, 1,
get_connected_node()->get_video_length().numerator(),
get_connected_node()->get_video_length().denominator());
}
void ViewerWidget::cache_sequence_in_out()
{
if (get_connected_node() && get_connected_node()->get_work_area()->enabled()) {
const auto &r = get_connected_node()->get_work_area()->range();
oakengine_preview_cacher_force_cache_range(
reinterpret_cast(get_connected_node()),
r.in().numerator(), r.in().denominator(),
r.out().numerator(), r.out().denominator());
} 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)
{
oakengine_viewer_set_playhead(
reinterpret_cast(get_connected_node()),
time.in().numerator(), time.in().denominator());
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);
}
}
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()
{
Rational t = get_connected_node()->get_playhead();
oakengine_preview_cacher_set_playhead(t.numerator(), t.denominator());
}
void ViewerWidget::decrement_prequeued_audio()
{
prequeuing_audio_--;
if (!prequeuing_audio_) {
finish_play_preprocess();
}
}
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 = viewer_output_audio_params(get_connected_node());
if (ap.sample_rate() <= 0 || ap.channel_count() <= 0) {
ap = AudioParams(
OAK_CONFIG("DefaultSequenceAudioFrequency").toInt(),
OAK_CONFIG("DefaultSequenceAudioLayout").toULongLong(),
static_cast(
oakengine_viewer_default_sample_format()));
}
ap.set_format(
static_cast(
oakengine_viewer_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;
OakAudioParams *from = ap_to_oak(ap);
OakAudioParams *to = ap_to_oak(packed);
oakengine_audio_processor_open(
audio_processor_, from, to,
(playback_speed_ == 0) ? 1 : std::abs(playback_speed_));
oakcore_audioparams_free(from);
oakcore_audioparams_free(to);
}
}
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;
}
}
void *command = oakengine_undo_command_create_multi();
Node *clip = AddTool::create_addable_clip(
command, s, Track::Reference(type, track_index), in, length);
if (ShapeNodeBase *shape = dynamic_cast(clip)) {
const VideoParams vp = viewer_output_video_params(s);
oak_video_params pod = {};
pod.width = vp.width();
pod.height = vp.height();
pod.time_base_num = vp.time_base().numerator();
pod.time_base_den = vp.time_base().denominator();
pod.format = vp.format();
pod.pixel_aspect_num = vp.pixel_aspect_ratio().numerator();
pod.pixel_aspect_den = vp.pixel_aspect_ratio().denominator();
pod.interlacing = vp.interlacing();
pod.divider = vp.divider();
oakengine_shape_set_rect_undoable(
reinterpret_cast(shape), f.x(), f.y(),
f.width(), f.height(), &pod, command);
}
oakengine_undo_push(command, tr("Created Shape").toUtf8().constData());
set_gizmos(clip);
}
}
void ViewerWidget::handle_first_requeue_destroy()
{
// Extra protection to ensure we don't reference a destroyed object
if (first_requeue_watcher_ && !queue_watchers_.contains(first_requeue_watcher_)) {
first_requeue_watcher_ = nullptr;
}
}
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::dry_run_finished()
{
if (!dry_run_watchers_.isEmpty()) {
OakEnginePreviewRequest *req = dry_run_watchers_.takeFirst();
oakengine_preview_request_free(req);
request_next_dry_run();
}
}
void ViewerWidget::request_next_dry_run()
{
if (is_playing()) {
Rational next_time =
Timecode::timestamp_to_time(dry_run_next_frame_, timebase());
if (frame_exists_at_time(next_time)) {
if (next_time > get_connected_node()->get_playhead() +
Rational(10)) {
QTimer::singleShot(timebase().to_double() / playback_speed_,
this, &ViewerWidget::request_next_dry_run);
} else {
OakEnginePreviewRequest *r = get_single_frame(next_time, true);
if (r) {
oakengine_preview_request_set_finished_callback(
r, dry_run_finished_cb, this);
dry_run_next_frame_ += playback_speed_;
dry_run_watchers_.append(r);
}
}
}
}
}
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()
{
oakengine_audio_processor_close(audio_processor_);
}
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 = reinterpret_cast(
oakengine_clip_find_multicam(
reinterpret_cast(clip))))) {
break;
}
}
}
}
}
if (!multicam) {
const QVector