/***
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
#include "viewer.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "oakengine/audio.h"
#include "oakengine/color.h"
#include "oakengine/encoding.h"
#include "oakengine/node.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 "oakengine/encoding.h"
#include "oakengine/display.h"
#include "widget/viewer/displaybuffer.h"
#include "oakengine/viewer.h"
#include "playback/playbackcontroller.h"
#include "oakengine/videoparams.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
#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()));
}
// FrameHashCache::get_valid_cache_filename() has no C ABI equivalent; the
// engine type is complete in this TU through the retained engine viewer.h
// include in timebasedwidget.h (same pattern as
// timelinewidget/view/timelineview.cpp). FrameHashCache is not one of the
// banned engine type names.
QString viewer_valid_cache_filename(OakEngineNode *viewer,
const Rational &time)
{
auto *cache = reinterpret_cast(
oakengine_viewer_get_frame_cache(viewer));
return cache ? cache->get_valid_cache_filename(time) : QString();
}
// 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_sample_count(req);
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)
, overlay_(nullptr)
, info_chip_(nullptr)
, safe_frame_btn_(nullptr)
, overlay_zoom_index_(5)
, playback_speed_(0)
, color_menu_enabled_(true)
, time_changed_from_timer_(false)
, prequeuing_video_(false)
, prequeuing_audio_(0)
, audio_processor_(oakengine_audio_processor_create())
, 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))
{
// 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_);
// Create the design-reference overlay (info chip + zoom/safe-frame buttons)
create_overlay();
// 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_);
// Per the Oak UI design reference the viewer no longer shows its own time
// ruler: it duplicates the timeline's ruler and clutters the viewer. The
// ruler object still exists (created by TimeBasedWidget) and continues to
// back marker/work-area/snapping logic and the scrollbar; we simply do not
// add it to the visible layout. Scrubbing is provided by the transport
// controls and the scrollbar below.
// 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(OakEngineNode *handle)
{
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) {
PlaybackController::instance()->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(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 =
oakengine_color_manager_from_project(
oakengine_node_get_project(handle));
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(OakEngineNode *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(OakEngineNode *n)
{
// Sequence check goes through the facade type-id predicate (replaces
// dynamic_cast); the handle is shared, so the cast is a
// reinterpret.
display_widget_->set_subtitle_tracks(
viewer_output_is_sequence(n) ? reinterpret_cast(n)
: nullptr);
}
void ViewerWidget::ConnectedWorkAreaChangeEvent(OakEngineWorkarea *workarea)
{
waveform_view_->set_work_area(workarea);
}
void ViewerWidget::ConnectedMarkersChangeEvent(OakEngineMarkerList *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();
position_overlay();
}
OakEnginePreviewRequest *ViewerWidget::get_single_frame(const Rational &t,
bool dry)
{
OakEngineNode *viewer = 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(oak_make_shared_texture(
oakengine_display_texture_retain(
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()
{
const Rational video_length =
viewer_output_video_length(get_connected_node());
oakengine_preview_cacher_force_cache_range(get_connected_node(),
0, 1,
video_length.numerator(),
video_length.denominator());
}
void ViewerWidget::cache_sequence_in_out()
{
oakengine_viewer_workarea wa;
if (get_connected_node() &&
oakengine_viewer_get_workarea(get_connected_node(), &wa) ==
OAKENGINE_OK &&
wa.enabled) {
oakengine_preview_cacher_force_cache_range(get_connected_node(),
wa.in_num, wa.in_den,
wa.out_num, wa.out_den);
} else {
QMessageBox::warning(this, tr("Error"),
tr("No in or out points are set to cache."),
QMessageBox::Ok);
}
}
void ViewerWidget::set_gizmos(OakEngineNode *node)
{
display_widget_->set_time_target(get_connected_node());
display_widget_->set_gizmos(node);
}
void ViewerWidget::start_capture(TimelineWidget *source, const TimeRange &time,
const TrackReference &track)
{
PlaybackController::instance()->set_playhead(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() &&
!oakengine_viewer_output_get_connected_texture(
get_connected_node()) &&
oakengine_node_input_get_connected_node(
get_connected_node(), oakengine_viewer_samples_input_id(), -1);
}
void ViewerWidget::set_empty_image()
{
foreach (ViewerDisplayWidget *dw, playback_devices_) {
dw->set_blank();
}
}
void ViewerWidget::update_auto_cacher()
{
Rational t = viewer_output_playhead(get_connected_node());
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)
{
auto *connected = get_connected_node();
// Sequence check via facade type-id predicate (replaces
// dynamic_cast); the handle is shared, so the cast is a
// reinterpret.
if (connected && viewer_output_is_sequence(connected)) {
OakEngineSequence *seq = reinterpret_cast(connected);
int track_index = -1;
int video_track_count = 0;
oakengine_sequence_track_count(seq, &video_track_count, nullptr,
nullptr);
const Rational &in = viewer_output_playhead(get_connected_node());
Rational length = OAK_CONFIG("DefaultStillLength").value();
Rational out = in + length;
// Track/block times are frame timestamps in the sequence's
// frame-rate timebase (see oakengine/timeline.h)
const Rational seq_tb = sequence_timebase(seq);
const int64_t in_ts = Timecode::time_to_timestamp(in, seq_tb);
const int64_t out_ts = Timecode::time_to_timestamp(out, seq_tb);
// Find a free track where we won't overwrite anything
while (true) {
track_index++;
if (track_index >= video_track_count) {
// Just create a new track
break;
}
if (oakengine_track_is_locked(seq, OAKENGINE_TRACK_TYPE_VIDEO,
track_index)) {
continue;
}
OakEngineTrack *track = oakengine_sequence_track_at(
seq, OAKENGINE_TRACK_TYPE_VIDEO, track_index);
OakEngineBlock *b =
oakengine_track_nearest_block_before_or_at(track, in_ts);
if (!b) {
break;
}
// Gap check via facade predicate (replaces
// dynamic_cast + Block::out())
int64_t b_out = 0;
oakengine_block_get_range(b, nullptr, &b_out);
if (oakengine_block_is_gap(b) && b_out >= out_ts) {
break;
}
}
void *command = oakengine_undo_command_create_multi();
OakEngineNode *clip = AddTool::create_addable_clip(
command, seq,
TrackReference(TrackReference::k_video, track_index),
in, length);
// Shape check via engine type-id string compare (replaces
// dynamic_cast); the id must stay in sync with
// ShapeNode::id() (engine/node/generator/shape/shapenode.cpp).
if (clip && viewer_output_node_type_is(
clip, "org.olivevideoeditor.Olive.shape")) {
const oak::VideoParams vp = viewer_output_video_params(seq);
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(
clip, 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 > viewer_output_playhead(get_connected_node()) +
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(viewer_output_playhead(get_connected_node()));
}
}
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();
}
namespace {
/**
* @brief Block::range().contains(time) through the C ABI: the block range
* is returned as frame timestamps in the owning sequence's frame-rate
* timebase, so the probe time is converted with the same timebase.
*/
bool block_range_contains(OakEngineBlock *block, const Rational &seq_tb,
const Rational &time)
{
int64_t in = 0, out = 0;
if (oakengine_block_get_range(block, &in, &out) != OAKENGINE_OK) {
return false;
}
const int64_t ts = Timecode::time_to_timestamp(time, seq_tb);
return ts >= in && ts < out;
}
} // namespace
void ViewerWidget::detect_multicam_node(const Rational &time)
{
// Look for multicam node
OakEngineNode *multicam = nullptr;
OakEngineBlock *clip = nullptr;
// Faster way to do this
if (multicam_panel_ && multicam_panel_->isVisible()) {
auto *connected = get_connected_node();
// Sequence check via facade type-id predicate (replaces
// dynamic_cast)
if (connected && viewer_output_is_sequence(connected)) {
OakEngineSequence *seq =
reinterpret_cast(connected);
const Rational seq_tb = sequence_timebase(seq);
// Prefer selected nodes
for (OakEngineNode *n : std::as_const(node_view_selected_)) {
// MultiCam check via facade predicate (replaces
// dynamic_cast)
if (oakengine_node_is_multicam(n)) {
multicam = n;
// Found multicam, now try to find corresponding clip from selected timeline blocks
for (OakEngineBlock *b : std::as_const(timeline_selected_blocks_)) {
// Clip check via facade predicate (replaces
// dynamic_cast)
if (oakengine_node_is_clip(
reinterpret_cast(b)) &&
block_range_contains(b, seq_tb, time) &&
oakengine_node_context_contains_node(
reinterpret_cast(b),
multicam)) {
clip = b;
break;
}
}
break;
}
}
// Next, prefer multicam from selected block
if (!multicam) {
for (OakEngineBlock *b : std::as_const(timeline_selected_blocks_)) {
if (block_range_contains(b, seq_tb, time) &&
oakengine_node_is_clip(
reinterpret_cast(b))) {
clip = b;
if ((multicam = oakengine_clip_find_multicam(
reinterpret_cast(clip)))) {
break;
}
}
}
}
if (!multicam) {
const int64_t time_ts =
Timecode::time_to_timestamp(time, seq_tb);
int track_counts[3] = { 0, 0, 0 };
oakengine_sequence_track_count(seq, &track_counts[0],
&track_counts[1],
&track_counts[2]);
// Iterate all track types (Sequence::get_tracks())
for (int type = OAKENGINE_TRACK_TYPE_VIDEO;
type <= OAKENGINE_TRACK_TYPE_SUBTITLE && !multicam;
type++) {
for (int i = 0; i < track_counts[type] && !multicam; i++) {
if (oakengine_track_is_locked(seq, type, i)) {
continue;
}
OakEngineTrack *track =
oakengine_sequence_track_at(seq, type, i);
OakEngineBlock *b =
oakengine_track_nearest_block_before_or_at(track,
time_ts);
if (b && oakengine_node_is_clip(
reinterpret_cast(b))) {
clip = b;
if ((multicam = oakengine_clip_find_multicam(
reinterpret_cast(
clip)))) {
break;
}
}
}
}
}
}
}
if (multicam) {
if (multicam_panel_) {
multicam_panel_->set_multicam_node(get_connected_node(),
multicam, clip, time);
}
// FIXME: Really dirty
oakengine_render_cache_set_multicam_node(multicam);
} else {
oakengine_render_cache_set_multicam_node(nullptr);
if (multicam_panel_) {
multicam_panel_->set_multicam_node(nullptr, nullptr, nullptr, time);
}
}
}
bool ViewerWidget::is_video_visible() const
{
return viewer_output_video_params(get_connected_node()).video_type() !=
1 &&
(display_widget_->isVisible() || !windows_.isEmpty());
}
void ViewerWidget::update_waveform_view_from_mode()
{
bool prefer_waveform = should_force_waveform();
sizer_->setVisible(waveform_mode_ == k_wf_viewer_and_waveform ||
waveform_mode_ == k_wf_viewer_only ||
(waveform_mode_ == k_wf_automatic && !prefer_waveform));
waveform_view_->setVisible(
waveform_mode_ == k_wf_viewer_and_waveform ||
waveform_mode_ == k_wf_waveform_only ||
(waveform_mode_ == k_wf_automatic && prefer_waveform));
waveform_view_->setSizePolicy(QSizePolicy::Expanding,
waveform_mode_ == k_wf_viewer_and_waveform ?
QSizePolicy::Maximum :
QSizePolicy::Expanding);
if (get_connected_node()) {
oakengine_viewer_set_waveform_enabled(get_connected_node(),
waveform_view_->isVisible() ? 1 : 0);
if (waveform_view_->isVisible()) {
waveform_view_->set_viewer(get_connected_node());
} else {
waveform_view_->set_viewer(nullptr);
}
}
}
void ViewerWidget::queue_next_audio_buffer()
{
Rational queue_end =
audio_playback_queue_time_ + (k_audio_playback_interval * playback_speed_);
// Clamp queue end by zero and the audio length
queue_end = std::clamp(queue_end, Rational(0),
viewer_output_audio_length(get_connected_node()));
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_) {
decrement_prequeued_audio();
}
return;
}
OakEngineNode *viewer = get_connected_node();
OakEnginePreviewRequest *req = oakengine_preview_request_audio_range(
viewer,
audio_playback_queue_time_.numerator(),
audio_playback_queue_time_.denominator(),
queue_end.numerator(), queue_end.denominator());
if (req) {
oakengine_preview_request_set_finished_callback(
req, audio_playback_finished_cb, this);
audio_playback_queue_.push_back(req);
}
audio_playback_queue_time_ = queue_end;
}
void ViewerWidget::received_audio_buffer_for_playback()
{
while (!audio_playback_queue_.empty() &&
oakengine_preview_request_is_done(audio_playback_queue_.front())) {
OakEnginePreviewRequest *req = audio_playback_queue_.front();
audio_playback_queue_.pop_front();
if (oakengine_preview_request_has_result(req)) {
SampleBuffer samples = preview_req_to_sample_buffer(req);
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
const void *pack_data = nullptr;
int pack_size = 0;
int r = oakengine_audio_processor_convert(
audio_processor_, samples.to_raw_ptrs().data(),
samples.sample_count(), &pack_data, &pack_size);
// TempoProcessor may have emptied the array
if (r >= 0) {
if (pack_size > 0) {
if (prequeuing_audio_) {
// Add to prequeued audio buffer
prequeued_audio_.append(
static_cast(pack_data), pack_size);
} else {
// Push directly to audio manager
{
OakAudioParams *oap =
oakengine_audio_processor_output_params(
audio_processor_);
oakengine_audio_push_to_output(
oap, static_cast(pack_data),
pack_size, nullptr, 0);
oakcore_audioparams_free(oap);
}
}
}
} else {
qCritical() << "Failed to process audio for playback:" << r;
}
}
}
if (prequeuing_audio_) {
decrement_prequeued_audio();
}
oakengine_preview_request_free(req);
}
}
void ViewerWidget::received_audio_buffer_for_scrubbing()
{
if (audio_scrub_watchers_.empty()) {
return;
}
OakEnginePreviewRequest *req = audio_scrub_watchers_.front();
audio_scrub_watchers_.pop_front();
if (oakengine_preview_request_has_result(req)) {
SampleBuffer samples = preview_req_to_sample_buffer(req);
if (samples.is_allocated()) {
if (samples.audio_params().channel_count() > 0) {
const void *pack_data = nullptr;
int pack_size = 0;
int r = oakengine_audio_processor_convert(
audio_processor_, samples.to_raw_ptrs().data(),
samples.sample_count(), &pack_data, &pack_size);
if (r >= 0) {
if (pack_size > 0) {
QString error;
oakengine_audio_clear_buffered_output();
char errbuf[256];
OakAudioParams *oap =
oakengine_audio_processor_output_params(
audio_processor_);
if (!oakengine_audio_push_to_output(
oap, static_cast(pack_data),
pack_size, errbuf, sizeof(errbuf))) {
oakcore_audioparams_free(oap);
Core::instance()->show_status_bar_message(
tr("Audio scrubbing failed: %1").arg(QString::fromUtf8(errbuf)));
} else {
oakcore_audioparams_free(oap);
}
AudioMonitor::push_sample_buffer_on_all(samples);
}
} else {
qCritical()
<< "Failed to process audio for scrubbing:" << r;
}
}
}
}
oakengine_preview_request_free(req);
}
void ViewerWidget::queue_starved()
{
static const int k_maximum_wait_time_ms = 250;
static const Rational k_maximum_wait_time(k_maximum_wait_time_ms, 1000);
qint64 now = QDateTime::currentMSecsSinceEpoch();
if (!queue_starved_start_) {
queue_starved_start_ = now;
} else if (now > queue_starved_start_ + k_maximum_wait_time_ms) {
if (first_requeue_watcher_ && !queue_watchers_.isEmpty()) {
// Stale check: request still pending below timeout
return;
}
force_requeue_from_current_time();
queue_starved_start_ = 0;
}
}
void ViewerWidget::queue_no_longer_starved()
{
queue_starved_start_ = 0;
}
void ViewerWidget::force_requeue_from_current_time()
{
// 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 request and deadlock.
QMetaObject::invokeMethod(
this, [this]() { force_requeue_from_current_time_internal(); },
Qt::QueuedConnection);
}
void ViewerWidget::force_requeue_from_current_time_internal()
{
// Allow half a second for requeue to complete
static const Rational k_requeue_wait_time(1);
oakengine_preview_cacher_clear_single_frame_renders(0);
queue_watchers_.clear();
int queue = determine_playback_queue_size();
playback_queue_next_frame_ =
get_timestamp() +
playback_speed_ * Timecode::time_to_timestamp(
k_requeue_wait_time, timebase(), Timecode::k_floor);
;
first_requeue_watcher_ = nullptr;
for (int i = 0; i < queue; i++) {
OakEnginePreviewRequest *req = request_next_frame_for_queue();
if (!first_requeue_watcher_) {
first_requeue_watcher_ = req;
}
}
}
void ViewerWidget::update_texture_from_node()
{
if (!get_connected_node()) {
return;
}
if (is_playing()) {
qWarning() << "UpdateTextureFromNode called while playing";
return;
}
Rational time = viewer_output_playhead(get_connected_node());
bool frame_exists = frame_exists_at_time(time);
bool frame_might_be_still = viewer_might_be_a_still();
if (frame_exists || 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
OakEnginePreviewRequest *req = get_frame(time);
if (req) {
oakengine_preview_request_set_finished_callback(
req, nonqueue_finished_cb, this);
nonqueue_watchers_.append(req);
}
// Clear queue because we want this frame more than any others
oakengine_preview_cacher_clear_single_frame_renders(1);
detect_multicam_node(time);
} else {
// There is definitely no frame here, we can immediately flip to showing nothing
nonqueue_watchers_.clear();
set_empty_image();
return;
}
}
void ViewerWidget::play_internal(int speed, bool in_to_out_only)
{
Q_ASSERT(speed != 0);
if (!get_connected_node()) {
// 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->pause_internal();
}
}
oakengine_preview_cacher_set_thumbnails_paused(1);
oakengine_render_manager_set_aggressive_garbage_collection(1);
// Disarm recording if armed
if (record_armed_) {
disarm_recording();
}
// If the playhead is beyond the end, restart at 0
if (!recording_) {
Rational last_frame =
viewer_output_length(get_connected_node()) - timebase();
if (!in_to_out_only &&
viewer_output_playhead(get_connected_node()) >= last_frame) {
if (speed > 0) {
PlaybackController::instance()->set_playhead(get_connected_node(),
0, 1);
} else {
PlaybackController::instance()->set_playhead(get_connected_node(),
last_frame.numerator(), last_frame.denominator());
}
}
}
playback_speed_ = speed;
play_in_to_out_only_ = in_to_out_only;
playback_queue_next_frame_ = get_timestamp() + playback_speed_;
controls_->show_pause_button();
queue_starved_start_ = 0;
// Attempt to fill playback queue
if (is_video_visible()) {
prequeue_length_ = determine_playback_queue_size();
if (prequeue_length_ > 0) {
prequeuing_video_ = true;
prequeue_count_ = 0;
for (int i = 0; i < prequeue_length_; i++) {
request_next_frame_for_queue();
}
dry_run_next_frame_ = playback_queue_next_frame_;
request_next_dry_run();
}
}
AudioParams ap = viewer_output_audio_params(get_connected_node());
qDebug() << "ViewerWidget::PlayInternal: audio params valid=" << ap.is_valid()
<< "channel_count=" << ap.channel_count();
if (ap.is_valid() && ap.channel_count() != 0) {
update_audio_processor();
// Verify audio processor output params are valid before using them
OakAudioParams *output_params =
oakengine_audio_processor_output_params(audio_processor_);
const bool output_valid =
output_params && oakcore_audioparams_is_valid(output_params);
qDebug() << "ViewerWidget::PlayInternal: audio processor output params valid="
<< output_valid;
if (!output_valid) {
qWarning()
<< "Audio processor output params are invalid, skipping audio playback";
} else {
oakengine_audio_set_output_notify_interval(
oakcore_audioparams_time_to_bytes(
output_params, k_audio_playback_interval.to_double()));
audio_notify_sub_ = oakengine_event_subscribe(
oakengine_audio_manager_handle(),
OAKENGINE_EVENT_AUDIO_MANAGER_OUTPUT_NOTIFY,
[](const oakengine_event *, void *userdata) {
static_cast(userdata)->queue_next_audio_buffer();
},
this);
static const int prequeue_count = 2;
prequeuing_audio_ =
prequeue_count; // Queue two buffers ahead of time
audio_playback_queue_time_ =
viewer_output_playhead(get_connected_node());
qDebug() << "ViewerWidget::PlayInternal: prequeuing audio start time="
<< audio_playback_queue_time_.to_double();
for (int i = 0; i < prequeue_count; i++) {
queue_next_audio_buffer();
}
}
oakcore_audioparams_free(output_params);
}
// 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_) {
finish_play_preprocess();
}
// Force screen to stay awake
prevent_sleep(true);
}
void ViewerWidget::pause_internal()
{
if (recording_) {
oakengine_audio_stop_recording();
recording_ = false;
controls_->set_pause_button_recording_state(false);
recording_callback_->disable_recording_overlay();
// recording_track_ and TimelineWidget::recording_callback() both use
// the app TrackReference mirror now.
recording_callback_->recording_callback(
recording_filename_, recording_range_, recording_track_);
}
if (is_playing()) {
playback_speed_ = 0;
controls_->show_play_button();
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 (OakEnginePreviewRequest *req, queue_watchers_) {
oakengine_preview_request_free(req);
}
queue_watchers_.clear();
oakengine_preview_cacher_clear_single_frame_renders(0);
playback_backup_timer_.stop();
// Handle audio
oakengine_audio_stop_output();
AudioMonitor::stop_on_all();
prequeued_audio_.clear();
if (audio_notify_sub_ > 0) {
oakengine_event_unsubscribe(audio_notify_sub_);
audio_notify_sub_ = 0;
}
for (auto *req : audio_playback_queue_) { oakengine_preview_request_free(req); }
audio_playback_queue_.clear();
update_audio_processor();
oakengine_preview_cacher_set_thumbnails_paused(0);
update_texture_from_node();
oakengine_render_manager_set_aggressive_garbage_collection(false);
}
prequeuing_video_ = false;
prequeuing_audio_ = 0;
dry_run_watchers_.clear();
// Reset screen timeout timer
prevent_sleep(false);
}
void ViewerWidget::push_scrubbed_audio()
{
if (!is_playing() && get_connected_node() &&
OAK_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 = viewer_output_audio_params(get_connected_node());
if (params.is_valid()) {
// NOTE: Hardcoded scrubbing interval (20ms)
Rational interval = Rational(20, 1000);
OakEngineNode *viewer = get_connected_node();
const Rational playhead = viewer_output_playhead(viewer);
OakEnginePreviewRequest *req =
oakengine_preview_request_audio_range(
viewer,
playhead.numerator(),
playhead.denominator(),
(playhead + interval).numerator(),
(playhead + interval).denominator());
if (req) {
oakengine_preview_request_set_finished_callback(
req, audio_scrub_finished_cb, this);
audio_scrub_watchers_.push_back(req);
}
}
}
}
}
void ViewerWidget::update_minimum_scale()
{
if (!get_connected_node()) {
return;
}
const Rational length = viewer_output_length(get_connected_node());
if (length.isNull()) {
// Avoids divide by zero
set_minimum_scale(0);
} else {
double min_scale = static_cast(ruler()->width()) /
length.to_double();
// Ensure min_scale doesn't exceed max_scale to prevent crash
min_scale = qMin(min_scale, get_maximum_scale());
set_minimum_scale(min_scale);
}
}
void ViewerWidget::set_color_transform(const oak::ColorTransform &transform,
ViewerDisplayWidget *sender)
{
sender->set_color_transform(transform);
}
QString ViewerWidget::get_cached_filename_from_time(const Rational &time)
{
if (frame_exists_at_time(time)) {
return viewer_valid_cache_filename(get_connected_node(), time);
}
return QString();
}
bool ViewerWidget::frame_exists_at_time(const Rational &time)
{
return get_connected_node() && time >= 0 &&
time < viewer_output_video_length(get_connected_node());
}
bool ViewerWidget::viewer_might_be_a_still()
{
return get_connected_node() &&
oakengine_viewer_output_get_connected_texture(
get_connected_node()) &&
viewer_output_video_length(get_connected_node()).isNull();
}
void ViewerWidget::set_display_image(OakEnginePreviewRequest *req)
{
foreach (ViewerDisplayWidget *dw, playback_devices_) {
QVariant push;
if (req && oakengine_preview_request_has_result(req)) {
oak_playback_frame frame;
if (dynamic_cast(dw)) {
// Multicam: use the default frame for now
if (oakengine_preview_request_get_frame(req, &frame) == 0) {
void *f = oakengine_codec_frame_create();
{
oak_video_params pod = {};
pod.width = frame.width;
pod.height = frame.height;
pod.format = frame.format;
oakengine_codec_frame_set_video_params(f, &pod);
}
oakengine_codec_frame_allocate(f);
int fls = oakengine_codec_frame_linesize_bytes(f);
if (frame.data && frame.linesize > 0 && fls > 0) {
memcpy(oakengine_codec_frame_data(f), frame.data,
qMin(fls, frame.linesize) * frame.height);
}
push = QVariant::fromValue(oak_make_shared_frame(f));
}
} else {
if (oakengine_preview_request_get_frame(req, &frame) == 0) {
void *f = oakengine_codec_frame_create();
{
oak_video_params pod = {};
pod.width = frame.width;
pod.height = frame.height;
pod.format = frame.format;
oakengine_codec_frame_set_video_params(f, &pod);
}
oakengine_codec_frame_allocate(f);
int fls = oakengine_codec_frame_linesize_bytes(f);
if (frame.data && frame.linesize > 0 && fls > 0) {
memcpy(oakengine_codec_frame_data(f), frame.data,
qMin(fls, frame.linesize) * frame.height);
}
push = QVariant::fromValue(oak_make_shared_frame(f));
}
}
}
dw->set_image(push);
}
}
OakEnginePreviewRequest *ViewerWidget::request_next_frame_for_queue(bool increment)
{
OakEnginePreviewRequest *req = nullptr;
Rational next_time =
Timecode::timestamp_to_time(playback_queue_next_frame_, timebase());
if (frame_exists_at_time(next_time) || viewer_might_be_a_still()) {
if (increment) {
playback_queue_next_frame_ += playback_speed_;
}
req = get_frame(next_time);
if (req) {
oakengine_preview_request_set_finished_callback(
req, queue_finished_cb, this);
queue_watchers_.append(req);
}
}
return req;
}
OakEnginePreviewRequest *ViewerWidget::get_frame(const Rational &t)
{
if (is_playing() || prequeuing_video_) {
return get_single_frame(t);
}
QString cache_fn = viewer_valid_cache_filename(get_connected_node(), t);
if (!QFileInfo::exists(cache_fn)) {
// Frame hasn't been cached, start render job
return get_single_frame(t);
} else {
// Frame has been cached, grab the frame via preview request
OakEngineNode *viewer = get_connected_node();
return oakengine_preview_request_single_frame(
viewer, t.numerator(), t.denominator(), 0);
}
}
void ViewerWidget::finish_play_preprocess()
{
// Check if we're still waiting for video or audio respectively
if (prequeuing_video_ || prequeuing_audio_) {
return;
}
int64_t playback_start_time = get_timestamp();
// Restart the audio output clock for this playback run; the playback
// timer uses it as its master clock
oakengine_audio_reset_output_clock();
// Start audio waveform playback
if (!prequeued_audio_.isEmpty()) {
char errbuf[256];
OakAudioParams *oap =
oakengine_audio_processor_output_params(audio_processor_);
if (!oakengine_audio_push_to_output(oap,
prequeued_audio_.constData(),
prequeued_audio_.size(),
errbuf, sizeof(errbuf))) {
oakcore_audioparams_free(oap);
QMessageBox::critical(
this, tr("Audio Error"),
tr("Failed to start audio: %1\n\n"
"Please check your audio preferences and try again.")
.arg(QString::fromUtf8(errbuf)));
} else {
oakcore_audioparams_free(oap);
}
prequeued_audio_.clear();
AudioMonitor::start_waveform_on_all(
oakengine_viewer_get_connected_waveform(get_connected_node()),
viewer_output_playhead(get_connected_node()), playback_speed_);
}
display_widget_->reset_fps_timer();
foreach (ViewerDisplayWidget *dw, playback_devices_) {
dw->play(playback_start_time, playback_speed_, timebase(),
is_video_visible());
}
// 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();
playback_timer_update();
}
int ViewerWidget::determine_playback_queue_size()
{
if (playback_speed_ == 0) {
return 0;
}
int64_t end_ts;
if (playback_speed_ > 0) {
end_ts = Timecode::time_to_timestamp(
viewer_output_video_length(get_connected_node()), timebase());
} else {
end_ts = 0;
}
int remaining_frames = (end_ts - get_timestamp() - 1) / playback_speed_;
// Generate maximum queue
int max_frames =
qCeil(k_video_playback_interval.to_double() / timebase().to_double());
return qMin(max_frames, remaining_frames);
}
void ViewerWidget::context_menu_set_full_screen(QAction *action)
{
set_full_screen(QGuiApplication::screens().at(action->data().toInt()));
}
void ViewerWidget::context_menu_set_playback_res(QAction *action)
{
int div = action->data().toInt();
void *c = oakengine_viewer_set_preview_divider_command(
get_connected_node(), div);
oakengine_undo_push(c, tr("Changed Playback Resolution").toUtf8().constData());
}
void ViewerWidget::context_menu_disable_safe_margins()
{
context_menu_widget_->set_safe_margins(ViewerSafeMarginInfo(false));
}
void ViewerWidget::context_menu_set_safe_margins()
{
context_menu_widget_->set_safe_margins(ViewerSafeMarginInfo(true));
}
void ViewerWidget::context_menu_set_custom_safe_margins()
{
bool ok;
double new_ratio = get_float_ratio_from_user(this, tr("Safe Margins"), &ok);
if (ok) {
context_menu_widget_->set_safe_margins(
ViewerSafeMarginInfo(true, new_ratio));
}
}
void ViewerWidget::window_about_to_close()
{
ViewerWindow *vw = static_cast(sender());
windows_.remove(windows_.key(vw));
playback_devices_.removeOne(vw->display_widget());
}
void ViewerWidget::renderer_generated_frame()
{
if (nonqueue_watchers_.isEmpty()) {
return;
}
OakEnginePreviewRequest *req = nonqueue_watchers_.takeFirst();
if (oakengine_preview_request_has_result(req)) {
set_display_image(req);
}
oakengine_preview_request_free(req);
}
void ViewerWidget::renderer_generated_frame_for_queue()
{
if (queue_watchers_.isEmpty()) {
return;
}
OakEnginePreviewRequest *req = queue_watchers_.takeFirst();
if (oakengine_preview_request_has_result(req)) {
oak_playback_frame pf;
bool has_frame = (oakengine_preview_request_get_frame(req, &pf) == 0);
bool drop_frame = false;
// Ignore this signal if we've paused now
if (is_playing() || prequeuing_video_) {
if (!drop_frame && has_frame) {
void *f = oakengine_codec_frame_create();
{
oak_video_params pod = {};
pod.width = pf.width;
pod.height = pf.height;
pod.format = pf.format;
oakengine_codec_frame_set_video_params(f, &pod);
}
oakengine_codec_frame_allocate(f);
int fls = oakengine_codec_frame_linesize_bytes(f);
if (pf.data && pf.linesize > 0 && fls > 0) {
memcpy(oakengine_codec_frame_data(f), pf.data,
qMin(fls, pf.linesize) * pf.height);
}
// The display expects an OakSharedBuffer (see set_display_image);
// a raw void* QVariant fails to unwrap and paints blank.
QVariant frame = QVariant::fromValue(oak_make_shared_frame(f));
if (qEnvironmentVariableIsSet("OAK_DEBUG_PLAYBACK")) {
qWarning("PLAYBACK-DEBUG: queue append ts=%lld/%lld %dx%d data=%p ls=%d",
(long long)pf.timestamp_num,
(long long)pf.timestamp_den, pf.width, pf.height,
pf.data, pf.linesize);
}
foreach (ViewerDisplayWidget *dw, playback_devices_) {
dw->queue()->append_timewise({ Rational(pf.timestamp_num,
pf.timestamp_den),
frame },
playback_speed_);
}
}
if (prequeuing_video_) {
prequeue_count_++;
if (prequeue_count_ == prequeue_length_) {
prequeuing_video_ = false;
finish_play_preprocess();
}
}
}
}
if (first_requeue_watcher_ == req) {
first_requeue_watcher_ = nullptr;
}
oakengine_preview_request_free(req);
}
void ViewerWidget::show_context_menu(const QPoint &pos)
{
if (!get_connected_node()) {
return;
}
Menu menu(static_cast(sender()));
context_menu_widget_ = dynamic_cast(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_->get_color_space_menu(&menu);
menu.addMenu(ocio_colorspace_menu);
}
{
Menu *ocio_display_menu =
context_menu_widget_->get_display_menu(&menu);
menu.addMenu(ocio_display_menu);
}
{
Menu *ocio_view_menu = context_menu_widget_->get_view_menu(&menu);
menu.addMenu(ocio_view_menu);
}
{
Menu *ocio_look_menu = context_menu_widget_->get_look_menu(&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::k_zoom_level_count; i++) {
double z = ViewerSizer::k_zoom_levels[i];
zoom_menu->addAction(tr("%1%").arg(z * 100.0))->setData(z);
}
connect(zoom_menu, &QMenu::triggered, this,
&ViewerWidget::set_zoom_from_menu);
}
{
// 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::context_menu_set_full_screen);
}
{
// Playback Resolution Menu
Menu *playback_res_menu =
new Menu(tr("Playback Resolution"), &menu);
menu.addMenu(playback_res_menu);
{
const int n = oakengine_video_params_supported_divider_count();
for (int i = 0; i < n; i++) {
int d = oakengine_video_params_supported_divider_at(i);
char name_buf[64];
oakengine_video_params_divider_name(d, name_buf, sizeof(name_buf));
playback_res_menu->add_action_with_data(
QString::fromUtf8(name_buf), d,
viewer_output_video_params(get_connected_node()).divider());
}
connect(playback_res_menu, &QMenu::triggered, this,
&ViewerWidget::context_menu_set_playback_res);
}
}
{
// Deinterlace Option
if (viewer_output_video_params(get_connected_node()).interlacing() !=
0) {
QAction *deinterlace_action = menu.addAction(tr("Deinterlace"));
deinterlace_action->setCheckable(true);
deinterlace_action->setChecked(
display_widget_->is_deinterlacing());
connect(deinterlace_action, &QAction::triggered,
display_widget_,
&ViewerDisplayWidget::set_deinterlacing);
}
}
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_->get_safe_margin().is_enabled());
connect(safe_margin_off, &QAction::triggered, this,
&ViewerWidget::context_menu_disable_safe_margins);
QAction *safe_margin_on = safe_margin_menu->addAction(tr("On"));
safe_margin_on->setCheckable(true);
safe_margin_on->setChecked(
context_menu_widget_->get_safe_margin().is_enabled() &&
!context_menu_widget_->get_safe_margin().custom_ratio());
connect(safe_margin_on, &QAction::triggered, this,
&ViewerWidget::context_menu_set_safe_margins);
QAction *safe_margin_custom =
safe_margin_menu->addAction(tr("Custom Aspect"));
safe_margin_custom->setCheckable(true);
safe_margin_custom->setChecked(
context_menu_widget_->get_safe_margin().is_enabled() &&
context_menu_widget_->get_safe_margin().custom_ratio());
connect(safe_margin_custom, &QAction::triggered, this,
&ViewerWidget::context_menu_set_custom_safe_margins);
}
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(
OAK_CONFIG("StopPlaybackOnLastFrame").toBool());
connect(stop_playback_on_last_frame, &QAction::triggered, this,
[](bool e) { OAK_CONFIG("StopPlaybackOnLastFrame") = e; });
menu.addSeparator();
}
{
auto waveform_menu = new Menu(tr("Audio Waveform"), &menu);
menu.addMenu(waveform_menu);
waveform_menu->add_action_with_data(tr("Automatically Show/Hide"),
k_wf_automatic, waveform_mode_);
waveform_menu->add_action_with_data(tr("Show Waveform Only"),
k_wf_waveform_only, waveform_mode_);
waveform_menu->add_action_with_data(tr("Show Both Viewer And Waveform"),
k_wf_viewer_and_waveform, waveform_mode_);
connect(waveform_menu, &Menu::triggered, this,
&ViewerWidget::update_waveform_mode_from_menu);
}
{
QAction *show_fps_action = menu.addAction(tr("Show FPS"));
show_fps_action->setCheckable(true);
show_fps_action->setChecked(display_widget_->get_show_fps());
connect(show_fps_action, &QAction::triggered, display_widget_,
&ViewerDisplayWidget::set_show_fps);
}
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_->get_show_subtitles());
connect(show_subtitles_action, &QAction::triggered, display_widget_,
&ViewerDisplayWidget::set_show_subtitles);
subtitle_menu->addSeparator();
auto subtitle_font_properties =
subtitle_menu->addAction(tr("Subtitle Properties"));
connect(subtitle_font_properties, &QAction::triggered, this,
&ViewerWidget::show_subtitle_properties);
auto subtitle_antialias =
subtitle_menu->addAction(tr("Use Anti-aliasing"));
subtitle_antialias->setCheckable(true);
subtitle_antialias->setChecked(
OAK_CONFIG("AntialiasSubtitles").toBool());
connect(subtitle_antialias, &QAction::triggered, this, [this](bool e) {
OAK_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::save_frame_as_image);
menu.exec(static_cast(sender())->mapToGlobal(pos));
}
void ViewerWidget::play(bool in_to_out_only)
{
if (in_to_out_only) {
oakengine_viewer_workarea wa;
if (get_connected_node() &&
oakengine_viewer_get_workarea(get_connected_node(), &wa) ==
OAKENGINE_OK &&
wa.enabled) {
// Jump to in point
PlaybackController::instance()->set_playhead(get_connected_node(),
wa.in_num, wa.in_den);
} else {
in_to_out_only = false;
}
} else if (record_armed_) {
disarm_recording();
char fn_buf[512];
oakengine_project_filename(
oakengine_node_get_project(get_connected_node()),
fn_buf, sizeof(fn_buf));
if (fn_buf[0] == '\0') {
QMessageBox::critical(
this, tr("Audio Recording"),
tr("Project must be saved before you can record audio."));
return;
}
QDir audio_path(QFileInfo(fn_buf)
.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"),
[]() -> QString {
char ext_buf[64];
int fmt = OAK_CONFIG("AudioRecordingFormat").toInt();
oakengine_encoding_format_extension(fmt, ext_buf, sizeof(ext_buf));
return QString::fromUtf8(ext_buf);
}()));
OakEngineEncodingParams *encode_param =
oakengine_encoding_params_create();
oakengine_encoding_params_enable_audio(
encode_param,
OAK_CONFIG("AudioRecordingSampleRate").toInt(),
OAK_CONFIG("AudioRecordingChannelLayout").toULongLong(),
SampleFormat::from_string(OAK_CONFIG("AudioRecordingSampleFormat")
.toString()
.toStdString()),
OAK_CONFIG("AudioRecordingCodec").toInt());
oakengine_encoding_params_set_filename(
encode_param, recording_filename_.toUtf8().constData());
oakengine_encoding_params_set_audio_bit_rate(
encode_param,
OAK_CONFIG("AudioRecordingBitRate").toInt() * 1000);
char errbuf[256];
const int rec_ret = oakengine_encoding_start_audio_recording(
encode_param, errbuf, static_cast(sizeof(errbuf)));
oakengine_encoding_params_destroy(encode_param);
if (rec_ret == OAKENGINE_OK) {
recording_ = true;
controls_->set_pause_button_recording_state(true);
recording_callback_->enable_recording_overlay(
TimelineCoordinate(recording_range_.in(), recording_track_));
} else {
QMessageBox::critical(
this, tr("Audio Recording"),
tr("Failed to start audio recording: %1").arg(
errbuf[0] ? QString::fromUtf8(errbuf)
: tr("Unknown error")));
return;
}
}
play_internal(1, in_to_out_only);
}
void ViewerWidget::play()
{
play(false);
}
void ViewerWidget::pause()
{
pause_internal();
}
void ViewerWidget::shuttle_left()
{
int current_speed = playback_speed_;
if (current_speed != 0) {
pause_internal();
}
current_speed--;
if (current_speed == 0) {
current_speed--;
}
play_internal(current_speed, false);
}
void ViewerWidget::shuttle_stop()
{
pause();
}
void ViewerWidget::shuttle_right()
{
int current_speed = playback_speed_;
if (current_speed != 0) {
pause_internal();
}
current_speed++;
if (current_speed == 0) {
current_speed++;
}
play_internal(current_speed, false);
}
void ViewerWidget::set_color_transform(const oak::ColorTransform &transform)
{
set_color_transform(transform, display_widget_);
}
void ViewerWidget::set_signal_cursor_color_enabled(bool e)
{
foreach (ViewerDisplayWidget *dw, playback_devices_) {
dw->set_signal_cursor_color_enabled(e);
}
}
void ViewerWidget::TimebaseChangedEvent(const Rational &timebase)
{
super::TimebaseChangedEvent(timebase);
controls_->set_timebase(timebase);
controls_->set_time(get_connected_node() ?
viewer_output_playhead(get_connected_node()) :
0);
length_changed_slot(get_connected_node() ?
viewer_output_length(get_connected_node()) :
0);
}
void ViewerWidget::playback_timer_update()
{
Q_ASSERT(playback_speed_ != 0);
Rational current_time = Timecode::timestamp_to_time(
display_widget_->timer()->get_timestamp_now(), 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_ && get_connected_node()) {
// If "play in to out" is enabled or we're looping AND we have a workarea, only play the workarea
oakengine_viewer_workarea wa;
if (oakengine_viewer_get_workarea(get_connected_node(), &wa) ==
OAKENGINE_OK &&
wa.enabled) {
min_time = Rational(int(wa.in_num), int(wa.in_den));
max_time = Rational(int(wa.out_num), int(wa.out_den));
} else {
min_time = 0;
max_time = viewer_output_length(get_connected_node());
}
} else {
// Otherwise set the bounds to the range of the sequence
min_time = 0;
max_time = viewer_output_length(get_connected_node());
}
// If we're stopping playback on the last frame rather than after it, subtract our max time
// by one timebase unit
if (OAK_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 (OAK_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;
PlaybackController::instance()->set_playhead(get_connected_node(),
time_to_set.numerator(), time_to_set.denominator());
time_changed_from_timer_ = false;
if (end_of_line) {
// Cache the current speed
int current_speed = playback_speed_;
pause_internal();
if (play_after_pause) {
play_internal(current_speed, play_in_to_out_only_);
}
}
if (is_playing() && is_video_visible()) {
while ((int(display_widget_->queue()->size()) +
queue_watchers_.size()) < determine_playback_queue_size()) {
if (!request_next_frame_for_queue()) {
// Prevent infinite loop
break;
}
}
}
foreach (ViewerDisplayWidget *dw, playback_devices_) {
dw->queue()->purge_before(current_time, playback_speed_);
}
}
void ViewerWidget::set_viewer_resolution(int width, int height)
{
sizer_->set_child_size(width, height);
foreach (ViewerWindow *vw, windows_) {
vw->set_resolution(width, height);
}
update_info_chip();
}
void ViewerWidget::set_viewer_pixel_aspect(const Rational &ratio)
{
sizer_->set_pixel_aspect_ratio(ratio);
foreach (ViewerWindow *vw, windows_) {
vw->set_pixel_aspect_ratio(ratio);
}
}
void ViewerWidget::length_changed_slot(const Rational &length)
{
if (last_length_ != length) {
controls_->set_end_time(length);
update_minimum_scale();
if (get_connected_node() && length < last_length_ &&
viewer_output_playhead(get_connected_node()) >= length) {
update_texture_from_node();
}
last_length_ = length;
}
}
void ViewerWidget::interlacing_changed_slot(int interlacing)
{
// Automatically set a "sane" deinterlacing option
bool deint = interlacing != 0; // k_interlace_none
foreach (ViewerDisplayWidget *dw, playback_devices_) {
dw->set_deinterlacing(deint);
}
}
void ViewerWidget::update_renderer_video_parameters()
{
oak::VideoParams vp = viewer_output_video_params(get_connected_node());
foreach (ViewerDisplayWidget *dw, playback_devices_) {
dw->set_video_params(vp);
}
}
void ViewerWidget::update_renderer_audio_parameters()
{
AudioParams ap = viewer_output_audio_params(get_connected_node());
update_audio_processor();
foreach (ViewerDisplayWidget *dw, playback_devices_) {
dw->set_audio_params(ap);
}
}
void ViewerWidget::set_zoom_from_menu(QAction *action)
{
auto s = sizer_->get_container_size();
sizer_->set_zoom_anchored(action->data().toDouble(), s.width() / 2,
s.height() / 2);
}
void ViewerWidget::create_overlay()
{
// A thin transparent strip laid over the top of the viewer display. Left
// side carries an info chip (resolution + frame rate); right side carries
// zoom in/out/fit and a safe-frame toggle, per the UI design reference.
overlay_ = new QWidget(sizer_);
overlay_->setAttribute(Qt::WA_TransparentForMouseEvents, false);
overlay_->setAutoFillBackground(false);
auto *overlay_layout = new QHBoxLayout(overlay_);
overlay_layout->setContentsMargins(6, 4, 6, 4);
overlay_layout->setSpacing(4);
info_chip_ = new QLabel(overlay_);
info_chip_->setStyleSheet(QStringLiteral(
"QLabel { background-color: rgba(0,0,0,160); color: white; "
"border-radius: 3px; padding: 2px 6px; }"));
info_chip_->setVisible(false);
overlay_layout->addWidget(info_chip_);
overlay_layout->addStretch();
const QString btn_style = QStringLiteral(
"QToolButton { background-color: rgba(0,0,0,160); color: white; "
"border: none; border-radius: 3px; padding: 2px 6px; } "
"QToolButton:hover { background-color: rgba(0,0,0,200); } "
"QToolButton:checked { background-color: rgba(0,120,215,200); }");
auto make_btn = [this, &btn_style](const QString &text,
const QString &tooltip) {
auto *b = new QToolButton(overlay_);
b->setText(text);
b->setToolTip(tooltip);
b->setStyleSheet(btn_style);
b->setAutoRaise(false);
return b;
};
QToolButton *zoom_out_btn = make_btn(QStringLiteral("\u2212"), tr("Zoom Out"));
QToolButton *zoom_in_btn = make_btn(QStringLiteral("+"), tr("Zoom In"));
QToolButton *zoom_fit_btn = make_btn(QStringLiteral("\u2922"), tr("Fit to Window"));
safe_frame_btn_ = make_btn(QStringLiteral("\u25A2"), tr("Safe Margins"));
safe_frame_btn_->setCheckable(true);
overlay_layout->addWidget(zoom_out_btn);
overlay_layout->addWidget(zoom_in_btn);
overlay_layout->addWidget(zoom_fit_btn);
overlay_layout->addWidget(safe_frame_btn_);
connect(zoom_out_btn, &QToolButton::clicked, this,
&ViewerWidget::overlay_zoom_out);
connect(zoom_in_btn, &QToolButton::clicked, this,
&ViewerWidget::overlay_zoom_in);
connect(zoom_fit_btn, &QToolButton::clicked, this,
&ViewerWidget::overlay_zoom_fit);
connect(safe_frame_btn_, &QToolButton::toggled, this,
[this](bool) { overlay_toggle_safe_frame(); });
overlay_->raise();
}
void ViewerWidget::position_overlay()
{
if (!overlay_ || !sizer_) {
return;
}
// Span the full width of the sizer, hugging the top edge.
overlay_->setGeometry(0, 0, sizer_->width(), overlay_->sizeHint().height());
overlay_->raise();
}
void ViewerWidget::update_info_chip()
{
if (!info_chip_) {
return;
}
if (!get_connected_node()) {
info_chip_->setVisible(false);
return;
}
oak::VideoParams vp = viewer_output_video_params(get_connected_node());
if (vp.width() <= 0 || vp.height() <= 0) {
info_chip_->setVisible(false);
return;
}
const double fps = vp.frame_rate().to_double();
const double fps_rounded = qRound(fps);
QString fps_str = (qFuzzyCompare(fps, fps_rounded))
? QString::number(static_cast(fps_rounded))
: QString::number(fps, 'f', 2);
info_chip_->setText(QStringLiteral("%1\u00d7%2 \u00b7 %3 FPS")
.arg(vp.width())
.arg(vp.height())
.arg(fps_str));
info_chip_->setVisible(true);
}
void ViewerWidget::overlay_zoom_in()
{
if (overlay_zoom_index_ < ViewerSizer::k_zoom_level_count - 1) {
overlay_zoom_index_++;
}
auto s = sizer_->get_container_size();
sizer_->set_zoom_anchored(ViewerSizer::k_zoom_levels[overlay_zoom_index_],
s.width() / 2, s.height() / 2);
}
void ViewerWidget::overlay_zoom_out()
{
if (overlay_zoom_index_ > 0) {
overlay_zoom_index_--;
}
auto s = sizer_->get_container_size();
sizer_->set_zoom_anchored(ViewerSizer::k_zoom_levels[overlay_zoom_index_],
s.width() / 2, s.height() / 2);
}
void ViewerWidget::overlay_zoom_fit()
{
// The zoom menu uses -1 to mean "fit to window".
auto s = sizer_->get_container_size();
sizer_->set_zoom_anchored(-1, s.width() / 2, s.height() / 2);
}
void ViewerWidget::overlay_toggle_safe_frame()
{
if (!safe_frame_btn_) {
return;
}
display_widget_->set_safe_margins(
ViewerSafeMarginInfo(safe_frame_btn_->isChecked()));
}
void ViewerWidget::viewer_invalidated_video_range(const TimeRange &range)
{
// If our current frame is within this range, we need to update
const Rational playhead = viewer_output_playhead(get_connected_node());
if (!is_playing() && playhead >= range.in() &&
(playhead < range.out() || range.in() == range.out())) {
QMetaObject::invokeMethod(this, &ViewerWidget::update_texture_from_node,
Qt::QueuedConnection);
}
}
void ViewerWidget::update_waveform_mode_from_menu(QAction *a)
{
set_waveform_mode(static_cast(a->data().toInt()));
}
void ViewerWidget::drag_entered(QDragEnterEvent *event)
{
if (event->mimeData()->formats().contains(QString::fromUtf8(oakengine_project_item_mime_type()))) {
event->accept();
}
}
void ViewerWidget::dropped(QDropEvent *event)
{
QByteArray mimedata = event->mimeData()->data(QString::fromUtf8(oakengine_project_item_mime_type()));
QDataStream stream(&mimedata, QIODevice::ReadOnly);
// Variables to deserialize into
quintptr item_ptr = 0;
QVector enabled_streams;
while (!stream.atEnd()) {
stream >> enabled_streams >> item_ptr;
// We only need the one item
break;
}
if (item_ptr) {
if (OakEngineNode *viewer = oakengine_viewer_from_node(
reinterpret_cast(item_ptr))) {
connect_viewer_node(viewer);
}
}
}
}