/*** Olive - Non-Linear Video Editor Copyright (C) 2022 Olive Team Modifications Copyright (C) 2025 mikesolar This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ***/ #include "viewer.h" #include #include #include #include #include #include #include #include #include #include #include "audio/audiomanager.h" #include "dialog/ratiodialog.h" #include "config/config.h" #include "core.h" #include "node/block/gap/gap.h" #include "node/generator/shape/shapenodebase.h" #include "node/nodeundo.h" #include "node/project.h" #include "panel/multicam/multicampanel.h" #include "panel/panelmanager.h" #include "render/rendermanager.h" #include "viewerpreventsleep.h" #include "widget/audiomonitor/audiomonitor.h" #include "widget/menu/menu.h" #include "widget/multicam/multicamdisplay.h" #include "widget/timelinewidget/tool/add.h" #include "widget/timeruler/timeruler.h" namespace olive { #define super TimeBasedWidget QVector ViewerWidget::instances; ViewerWidget::ViewerWidget(ViewerDisplayWidget *display, QWidget *parent) : super(false, true, parent) , playback_speed_(0) , color_menu_enabled_(true) , time_changed_from_timer_(false) , playback_(nullptr) , record_armed_(false) , recording_(false) , enable_audio_scrubbing_(true) , waveform_mode_(k_wf_automatic) , ignore_scrub_(0) , multicam_panel_(nullptr) { // Set up main layout QVBoxLayout *layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); // Create main OpenGL-based view and sizer sizer_ = new ViewerSizer(); layout->addWidget(sizer_); display_widget_ = display; display_widget_->set_show_widget_background(true); playback_devices_.append(display_widget_); connect(display_widget_, &ViewerDisplayWidget::customContextMenuRequested, this, &ViewerWidget::show_context_menu); connect(display_widget_, &ViewerDisplayWidget::cursor_color, this, &ViewerWidget::cursor_color); connect(display_widget_, &ViewerDisplayWidget::color_processor_changed, this, &ViewerWidget::color_processor_changed); connect( display_widget_, &ViewerDisplayWidget::color_processor_changed, this, [](ColorProcessorPtr processor) { RenderManager::instance()->get_cacher()->set_display_color_processor( processor); }); RenderManager::instance()->get_cacher()->set_display_color_processor( display_widget_->get_current_color_processor()); connect(display_widget_, &ViewerDisplayWidget::color_manager_changed, this, &ViewerWidget::color_manager_changed); connect(display_widget_, &ViewerDisplayWidget::drag_entered, this, &ViewerWidget::drag_entered); connect(display_widget_, &ViewerDisplayWidget::dropped, this, &ViewerWidget::dropped); connect(display_widget_, &ViewerDisplayWidget::texture_changed, this, &ViewerWidget::texture_changed); connect(display_widget_, &ViewerDisplayWidget::create_addable_at, this, &ViewerWidget::create_addable_at); connect(sizer_, &ViewerSizer::request_scale, display_widget_, &ViewerDisplayWidget::set_matrix_zoom); connect(sizer_, &ViewerSizer::request_translate, display_widget_, &ViewerDisplayWidget::set_matrix_translate); connect(display_widget_, &ViewerDisplayWidget::hand_drag_moved, sizer_, &ViewerSizer::hand_drag_move); sizer_->set_widget(display_widget_); // Make the display widget the first tabbable widget. While the viewer display cannot actually // be interacted with by tabbing, it prevents the actual first tabbable widget (the playhead // slider in `controls_`) from getting auto-focused any time the panel is maximized (with `) display_widget_->setFocusPolicy(Qt::TabFocus); // Create waveform view when audio is connected and video isn't waveform_view_ = new AudioWaveformView(); connect_timeline_view(waveform_view_); layout->addWidget(waveform_view_); // Create time ruler layout->addWidget(ruler()); // Create scrollbar layout->addWidget(scrollbar()); // Create lower controls controls_ = new PlaybackControls(); controls_->set_timecode_enabled(true); controls_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum); connect(controls_, &PlaybackControls::play_clicked, this, static_cast(&ViewerWidget::play)); connect(controls_, &PlaybackControls::pause_clicked, this, &ViewerWidget::pause); connect(controls_, &PlaybackControls::prev_frame_clicked, this, &ViewerWidget::prev_frame); connect(controls_, &PlaybackControls::next_frame_clicked, this, &ViewerWidget::next_frame); connect(controls_, &PlaybackControls::begin_clicked, this, &ViewerWidget::go_to_start); connect(controls_, &PlaybackControls::end_clicked, this, &ViewerWidget::go_to_end); layout->addWidget(controls_); // FIXME: Magic number SetScale(48.0); // Ensures that seeking on the waveform view updates the time as expected connect(waveform_view_, &AudioWaveformView::customContextMenuRequested, this, &ViewerWidget::show_context_menu); connect(&playback_poll_timer_, &QTimer::timeout, this, &ViewerWidget::playback_poll_update); set_auto_max_scroll_bar(true); instances.append(this); update_waveform_view_from_mode(); connect(Core::instance(), &Core::color_picker_enabled, this, &ViewerWidget::set_signal_cursor_color_enabled); connect(this, &ViewerWidget::cursor_color, Core::instance(), &Core::color_picker_color_emitted); connect(AudioManager::instance(), &AudioManager::output_params_changed, this, &ViewerWidget::update_audio_processor); } ViewerWidget::~ViewerWidget() { instances.removeOne(this); // Stop and release the facade playback session. oakengine_playback_free(playback_); playback_ = nullptr; auto windows = windows_; foreach (ViewerWindow *window, windows) { delete window; } delete display_widget_; display_widget_ = nullptr; } void ViewerWidget::TimeChangedEvent(const Rational &time) { if (!time_changed_from_timer_) { pause_internal(); } if (record_armed_) { disarm_recording(); } controls_->set_time(time); if (get_connected_node() && last_time_ != time) { if (!is_playing()) { update_texture_from_node(); push_scrubbed_audio(); // We don't clear the FPS timer on pause in case users want to see it immediately after, but by // the time a new texture is drawn, assume that the FPS no longer needs to be shown. display_widget_->reset_fps_timer(); } display_widget_->set_time(time); } // Send time to auto-cacher RenderManager::instance()->get_cacher()->set_playhead(time); last_time_ = time; } void ViewerWidget::ConnectNodeEvent(ViewerOutput *n) { connect(n, &ViewerOutput::size_changed, this, &ViewerWidget::set_viewer_resolution); connect(n, &ViewerOutput::pixel_aspect_changed, this, &ViewerWidget::set_viewer_pixel_aspect); connect(n, &ViewerOutput::length_changed, this, &ViewerWidget::length_changed_slot); connect(n, &ViewerOutput::interlacing_changed, this, &ViewerWidget::interlacing_changed_slot); connect(n, &ViewerOutput::video_params_changed, this, &ViewerWidget::update_renderer_video_parameters); connect(n, &ViewerOutput::video_params_changed, this, &ViewerWidget::update_texture_from_node, Qt::QueuedConnection); connect(n, &ViewerOutput::audio_params_changed, this, &ViewerWidget::update_renderer_audio_parameters); if (FrameHashCache *cache = n->video_frame_cache()) { connect(cache, &FrameHashCache::invalidated, this, &ViewerWidget::viewer_invalidated_video_range); } connect(n, &ViewerOutput::texture_input_changed, this, &ViewerWidget::update_waveform_view_from_mode); connect(controls_, &PlaybackControls::time_changed, n, &ViewerOutput::set_playhead); VideoParams vp = n->get_video_params(); interlacing_changed_slot(vp.interlacing()); ruler()->set_playback_cache(n->video_frame_cache()); set_viewer_resolution(vp.width(), vp.height()); set_viewer_pixel_aspect(vp.pixel_aspect_ratio()); last_length_ = 0; length_changed_slot(n->get_length()); update_audio_processor(); ColorManager *color_manager = n->project()->color_manager(); foreach (ViewerDisplayWidget *dw, playback_devices_) { dw->connect_color_manager(color_manager); } update_waveform_view_from_mode(); waveform_view_->set_viewer(get_connected_node()); update_renderer_video_parameters(); update_renderer_audio_parameters(); // Set texture to new texture (or null if no viewer node is available) update_texture_from_node(); } void ViewerWidget::DisconnectNodeEvent(ViewerOutput *n) { pause_internal(); disconnect(n, &ViewerOutput::size_changed, this, &ViewerWidget::set_viewer_resolution); disconnect(n, &ViewerOutput::pixel_aspect_changed, this, &ViewerWidget::set_viewer_pixel_aspect); disconnect(n, &ViewerOutput::length_changed, this, &ViewerWidget::length_changed_slot); disconnect(n, &ViewerOutput::interlacing_changed, this, &ViewerWidget::interlacing_changed_slot); disconnect(n, &ViewerOutput::video_params_changed, this, &ViewerWidget::update_renderer_video_parameters); disconnect(n, &ViewerOutput::video_params_changed, this, &ViewerWidget::update_texture_from_node); disconnect(n, &ViewerOutput::audio_params_changed, this, &ViewerWidget::update_renderer_audio_parameters); if (FrameHashCache *cache = n->video_frame_cache()) { disconnect(cache, &FrameHashCache::invalidated, this, &ViewerWidget::viewer_invalidated_video_range); } disconnect(n, &ViewerOutput::texture_input_changed, this, &ViewerWidget::update_waveform_view_from_mode); disconnect(controls_, &PlaybackControls::time_changed, n, &ViewerOutput::set_playhead); timeline_selected_blocks_.clear(); node_view_selected_.clear(); if (multicam_panel_) { multicam_panel_->set_multicam_node(nullptr, nullptr, nullptr, Rational::na_n); } close_audio_processor(); audio_scrub_watchers_.clear(); set_display_image(nullptr); ruler()->set_playback_cache(nullptr); // Effectively disables the viewer and clears the state set_viewer_resolution(0, 0); foreach (ViewerDisplayWidget *dw, playback_devices_) { dw->disconnect_color_manager(); } waveform_view_->set_viewer(nullptr); // Queue an UpdateStack so that when it runs, the viewer node will be fully disconnected QMetaObject::invokeMethod(this, &ViewerWidget::update_waveform_view_from_mode, Qt::QueuedConnection); set_gizmos(nullptr); } void ViewerWidget::ConnectedNodeChangeEvent(ViewerOutput *n) { display_widget_->set_subtitle_tracks(dynamic_cast(n)); } void ViewerWidget::ConnectedWorkAreaChangeEvent(TimelineWorkArea *workarea) { waveform_view_->set_work_area(workarea); } void ViewerWidget::ConnectedMarkersChangeEvent(TimelineMarkerList *markers) { waveform_view_->set_markers(markers); } void ViewerWidget::ScaleChangedEvent(const double &s) { super::ScaleChangedEvent(s); waveform_view_->set_scale(s); } void ViewerWidget::resizeEvent(QResizeEvent *event) { super::resizeEvent(event); update_minimum_scale(); } RenderTicketPtr ViewerWidget::get_single_frame(const Rational &t, bool dry) { return RenderManager::instance()->get_cacher()->get_single_frame( this->get_connected_node(), t, dry); } void ViewerWidget::toggle_play_pause() { if (is_playing()) { pause(); } else { play(); } } bool ViewerWidget::is_playing() const { return playback_speed_ != 0; } void ViewerWidget::set_color_menu_enabled(bool enabled) { color_menu_enabled_ = enabled; } void ViewerWidget::set_matrix(const QMatrix4x4 &mat) { foreach (ViewerDisplayWidget *dw, playback_devices_) { dw->set_matrix_crop(mat); } } void ViewerWidget::set_full_screen(QScreen *screen) { if (!screen) { // Try to find the screen that contains the mouse cursor currently foreach (QScreen *test, QGuiApplication::screens()) { if (test->geometry().contains(QCursor::pos())) { screen = test; break; } } // Fallback, just use the first screen if (!screen) { screen = QGuiApplication::screens().first(); } } if (windows_.contains(screen)) { ViewerWindow *vw = windows_.take(screen); vw->deleteLater(); return; } ViewerWindow *vw = new ViewerWindow(this); vw->setGeometry(screen->geometry()); vw->showFullScreen(); vw->display_widget()->connect_color_manager(color_manager()); connect(vw, &ViewerWindow::destroyed, this, &ViewerWidget::window_about_to_close); connect(vw->display_widget(), &ViewerDisplayWidget::customContextMenuRequested, this, &ViewerWidget::show_context_menu); if (get_connected_node()) { vw->set_video_params(get_connected_node()->get_video_params()); vw->display_widget()->set_deinterlacing( vw->display_widget()->is_deinterlacing()); } vw->display_widget()->set_image( QVariant::fromValue(display_widget()->get_current_texture())); playback_devices_.append(vw->display_widget()); (*vw->display_widget()->queue()) = *playback_devices_.first()->queue(); if (is_playing()) { vw->display_widget()->play(get_timestamp(), playback_speed_, timebase(), true); } windows_.insert(screen, vw); } void ViewerWidget::cache_entire_sequence() { RenderManager::instance()->get_cacher()->force_cache_range( get_connected_node(), TimeRange(0, get_connected_node()->get_video_length())); } void ViewerWidget::cache_sequence_in_out() { if (get_connected_node() && get_connected_node()->get_work_area()->enabled()) { RenderManager::instance()->get_cacher()->force_cache_range( get_connected_node(), get_connected_node()->get_work_area()->range()); } else { QMessageBox::warning(this, tr("Error"), tr("No in or out points are set to cache."), QMessageBox::Ok); } } void ViewerWidget::set_gizmos(Node *node) { display_widget_->set_time_target(get_connected_node()); display_widget_->set_gizmos(node); } void ViewerWidget::start_capture(TimelineWidget *source, const TimeRange &time, const Track::Reference &track) { get_connected_node()->set_playhead(time.in()); arm_for_recording(); recording_callback_ = source; recording_range_ = time; recording_track_ = track; } void ViewerWidget::connect_multicam_widget(MulticamWidget *p) { if (multicam_panel_) { disconnect(multicam_panel_, &MulticamWidget::switched, this, &ViewerWidget::detect_multicam_node_now); } multicam_panel_ = p; if (multicam_panel_) { connect(multicam_panel_, &MulticamWidget::switched, this, &ViewerWidget::detect_multicam_node_now); } } FramePtr ViewerWidget::decode_cached_image(const QString &cache_path, const QUuid &cache_id, const int64_t &time) { FramePtr frame = FrameHashCache::load_cache_frame(cache_path, cache_id, time); if (frame) { frame->set_timestamp(time); } else { qWarning() << "Tried to load cached frame from file but it was null"; } return frame; } void ViewerWidget::decode_cached_image(RenderTicketPtr ticket, const QString &cache_path, const QUuid &cache_id, const int64_t &time) { ticket->start(); FramePtr f = decode_cached_image(cache_path, cache_id, time); if (f) { ticket->finish(QVariant::fromValue(f)); } else { ticket->finish(); } } bool ViewerWidget::should_force_waveform() const { return get_connected_node() && !get_connected_node()->get_connected_texture_output() && get_connected_node()->get_connected_sample_output(); } void ViewerWidget::set_empty_image() { foreach (ViewerDisplayWidget *dw, playback_devices_) { dw->set_blank(); } } void ViewerWidget::update_auto_cacher() { RenderManager::instance()->get_cacher()->set_playhead( get_connected_node()->get_playhead()); } void ViewerWidget::arm_for_recording() { controls_->start_play_blink(); record_armed_ = true; } void ViewerWidget::disarm_recording() { controls_->stop_play_blink(); record_armed_ = false; } void ViewerWidget::update_audio_processor() { if (get_connected_node()) { close_audio_processor(); AudioParams ap = get_connected_node()->get_audio_params(); if (ap.sample_rate() <= 0 || ap.channel_count() <= 0) { ap = AudioParams( OAK_CONFIG("DefaultSequenceAudioFrequency").toInt(), OAK_CONFIG("DefaultSequenceAudioLayout").toULongLong(), ViewerOutput::k_default_sample_format); } ap.set_format(ViewerOutput::k_default_sample_format); AudioParams packed( OAK_CONFIG("AudioOutputSampleRate").toInt(), OAK_CONFIG("AudioOutputChannelLayout").toULongLong(), SampleFormat::from_string(OAK_CONFIG("AudioOutputSampleFormat") .toString() .toStdString())); qDebug() << "ViewerWidget::UpdateAudioProcessor: from sample_rate=" << ap.sample_rate() << "channels=" << ap.channel_count() << "layout_mask=0x" << Qt::hex << ap.channel_layout() << "to sample_rate=" << packed.sample_rate() << "channels=" << packed.channel_count() << "layout_mask=0x" << packed.channel_layout() << Qt::dec; audio_processor_.open( ap, packed, (playback_speed_ == 0) ? 1 : std::abs(playback_speed_)); } } void ViewerWidget::create_addable_at(const QRectF &f) { if (Sequence *s = dynamic_cast(get_connected_node())) { Track::Type type = Track::k_video; int track_index = -1; TrackList *list = s->track_list(type); const Rational &in = get_connected_node()->get_playhead(); Rational length = OAK_CONFIG("DefaultStillLength").value(); Rational out = in + length; // Find a free track where we won't overwrite anything while (true) { track_index++; if (track_index >= list->get_track_count()) { // Just create a new track break; } Track *track = list->get_track_at(track_index); if (track->is_locked()) { continue; } Block *b = track->nearest_block_before_or_at(in); if (!b || (dynamic_cast(b) && b->out() >= out)) { break; } } MultiUndoCommand *command = new MultiUndoCommand(); Node *clip = AddTool::create_addable_clip( command, s, Track::Reference(type, track_index), in, length); if (ShapeNodeBase *shape = dynamic_cast(clip)) { shape->set_rect(f, s->get_video_params(), command); } Core::instance()->undo_stack()->push(command, tr("Created Shape")); set_gizmos(clip); } } void ViewerWidget::show_subtitle_properties() { QFont f(OAK_CONFIG("DefaultSubtitleFamily").toString(), OAK_CONFIG("DefaultSubtitleSize").toInt(), OAK_CONFIG("DefaultSubtitleWeight").toInt()); QFontDialog fd(f, this); if (fd.exec() == QDialog::Accepted) { f = fd.selectedFont(); OAK_CONFIG("DefaultSubtitleSize") = f.pointSize(); OAK_CONFIG("DefaultSubtitleFamily") = f.family(); OAK_CONFIG("DefaultSubtitleWeight") = f.weight(); display_widget_->update(); } } void ViewerWidget::save_frame_as_image() { Core::instance()->open_export_dialog_for_viewer(get_connected_node(), true); } void ViewerWidget::detect_multicam_node_now() { if (get_connected_node()) { detect_multicam_node(get_connected_node()->get_playhead()); } } void ViewerWidget::close_audio_processor() { audio_processor_.close(); } void ViewerWidget::set_waveform_mode(WaveformMode wf) { waveform_mode_ = wf; update_waveform_view_from_mode(); } void ViewerWidget::detect_multicam_node(const Rational &time) { // Look for multicam node MultiCamNode *multicam = nullptr; ClipBlock *clip = nullptr; // Faster way to do this if (multicam_panel_ && multicam_panel_->isVisible()) { if (Sequence *s = dynamic_cast(get_connected_node())) { // Prefer selected nodes for (Node *n : qAsConst(node_view_selected_)) { if ((multicam = dynamic_cast(n))) { // Found multicam, now try to find corresponding clip from selected timeline blocks for (Block *b : qAsConst(timeline_selected_blocks_)) { if (ClipBlock *c = dynamic_cast(b)) { if (c->range().contains(time) && c->context_contains_node(multicam)) { clip = c; break; } } } break; } } // Next, prefer multicam from selected block if (!multicam) { for (Block *b : qAsConst(timeline_selected_blocks_)) { if (b->range().contains(time)) { if ((clip = dynamic_cast(b))) { if ((multicam = clip->find_multicam())) { break; } } } } } if (!multicam) { const QVector &tracks = s->get_tracks(); for (Track *t : tracks) { if (t->is_locked()) { continue; } Block *b = t->nearest_block_before_or_at(time); if ((clip = dynamic_cast(b))) { if ((multicam = clip->find_multicam())) { break; } } } } } } if (multicam) { if (multicam_panel_) { multicam_panel_->set_multicam_node(get_connected_node(), multicam, clip, time); } // FIXME: Really dirty RenderManager::instance()->get_cacher()->set_multicam_node(multicam); } else { RenderManager::instance()->get_cacher()->set_multicam_node(nullptr); if (multicam_panel_) { multicam_panel_->set_multicam_node(nullptr, nullptr, nullptr, time); } } } bool ViewerWidget::is_video_visible() const { return get_connected_node()->get_video_params().video_type() != VideoParams::k_video_type_still && (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()) { get_connected_node()->set_waveform_enabled(waveform_view_->isVisible()); if (waveform_view_->isVisible()) { waveform_view_->set_viewer(get_connected_node()); } else { waveform_view_->set_viewer(nullptr); } } } void ViewerWidget::received_audio_buffer_for_scrubbing() { RenderTicketWatcher *watcher = static_cast(sender()); while (!audio_scrub_watchers_.empty() && audio_scrub_watchers_.front() != watcher) { audio_scrub_watchers_.pop_front(); } if (!audio_scrub_watchers_.empty()) { if (watcher->has_result()) { SampleBuffer samples = watcher->get().value(); if (samples.is_allocated()) { if (samples.audio_params().channel_count() > 0) { AudioProcessor::Buffer buf; int r = audio_processor_.convert(samples.to_raw_ptrs().data(), samples.sample_count(), &buf); if (r >= 0) { if (!buf.empty()) { QString error; const QByteArray &packed = buf.at(0); AudioManager::instance()->clear_buffered_output(); if (!AudioManager::instance()->push_to_output( audio_processor_.to(), packed, &error)) { Core::instance()->show_status_bar_message( tr("Audio scrubbing failed: %1").arg(error)); } AudioMonitor::push_sample_buffer_on_all(samples); } } else { qCritical() << "Failed to process audio for scrubbing:" << r; } } } } } delete watcher; } void ViewerWidget::update_texture_from_node() { if (!get_connected_node()) { return; } if (is_playing()) { qWarning() << "UpdateTextureFromNode called while playing"; return; } Rational time = get_connected_node()->get_playhead(); 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 RenderTicketWatcher *watcher = new RenderTicketWatcher(); watcher->setProperty("start", QDateTime::currentMSecsSinceEpoch()); watcher->setProperty("time", QVariant::fromValue(time)); connect(watcher, &RenderTicketWatcher::finished, this, &ViewerWidget::renderer_generated_frame); nonqueue_watchers_.append(watcher); // Clear queue because we want this frame more than any others RenderManager::instance() ->get_cacher() ->clear_single_frame_renders_that_arent_running(); detect_multicam_node(time); watcher->set_ticket(get_frame(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; } if (speed < 0) { // The facade playback engine covers forward playback only this // round; backward/shuttle playback is deferred (roadmap 附 A). qWarning() << "ViewerWidget: backward playback is not supported yet"; 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(); } } RenderManager::instance()->get_cacher()->set_thumbnails_paused(true); RenderManager::instance()->set_aggressive_garbage_collection(true); // Disarm recording if armed if (record_armed_) { disarm_recording(); } // If the playhead is beyond the end, restart at 0 if (!recording_) { Rational last_frame = get_connected_node()->get_length() - timebase(); if (!in_to_out_only && get_connected_node()->get_playhead() >= last_frame) { get_connected_node()->set_playhead(0); } } playback_speed_ = speed; play_in_to_out_only_ = in_to_out_only; controls_->show_pause_button(); // Start the facade playback session: its pull thread renders frames // and 1/4s audio blocks ahead, pushes audio to the AudioManager // itself and feeds our frame/audio callbacks. if (!playback_) { const VideoParams vp = get_connected_node()->get_video_params(); playback_ = oakengine_playback_create( reinterpret_cast(get_connected_node()), vp.effective_width(), vp.effective_height(), timebase().denominator(), timebase().numerator()); if (!playback_) { qWarning() << "ViewerWidget: failed to create playback session"; playback_speed_ = 0; controls_->show_play_button(); return; } oakengine_playback_set_frame_callback( playback_, &ViewerWidget::facade_frame_callback, this); oakengine_playback_set_audio_callback( playback_, &ViewerWidget::facade_audio_callback, this); } const int64_t start_ts = get_timestamp(); if (oakengine_playback_start(playback_, start_ts, playback_speed_) != OAKENGINE_OK) { char err[512]; err[0] = '\0'; oakengine_playback_last_error(playback_, err, sizeof(err)); qWarning() << "ViewerWidget: failed to start playback:" << (err[0] ? err : "(no error)"); playback_speed_ = 0; controls_->show_play_button(); return; } // Waveform monitor stays UI-side (it needs the connected waveform // metadata); the facade pushes audio to the output by itself. if (get_connected_node()->get_audio_params().channel_count() > 0) { AudioMonitor::start_waveform_on_all( get_connected_node()->get_connected_waveform(), get_connected_node()->get_playhead(), playback_speed_); } display_widget_->reset_fps_timer(); foreach (ViewerDisplayWidget *dw, playback_devices_) { dw->play(start_ts, playback_speed_, timebase(), is_video_visible()); } // The UI poll timer drives the playhead, boundary and loop policy // from the facade's playback position. playback_poll_timer_.setInterval( qMax(1, qFloor(timebase().to_double() * 1000.0))); playback_poll_timer_.start(); playback_poll_update(); // Force screen to stay awake prevent_sleep(true); } void ViewerWidget::pause_internal() { if (recording_) { AudioManager::instance()->stop_recording(); recording_ = false; controls_->set_pause_button_recording_state(false); recording_callback_->disable_recording_overlay(); 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(); } // The facade pause also stops the audio output (engine side). if (playback_) { oakengine_playback_pause(playback_); } playback_poll_timer_.stop(); AudioMonitor::stop_on_all(); RenderManager::instance()->get_cacher()->clear_single_frame_renders(); RenderManager::instance()->get_cacher()->set_thumbnails_paused(false); update_texture_from_node(); RenderManager::instance()->set_aggressive_garbage_collection(false); } // Reset screen timeout timer prevent_sleep(false); } void ViewerWidget::facade_frame_callback(const oak_playback_frame *frame, void *userdata) { ViewerWidget *viewer = static_cast(userdata); // Wrap the CPU pixels now (the payload dies when we return), then // append to the display queue on the main thread -- the same // destination the old renderer_generated_frame_for_queue used. FramePtr copy = Frame::create(); copy->set_video_params( VideoParams(frame->width, frame->height, viewer->timebase(), static_cast(frame->format), VideoParams::k_internal_channel_count)); copy->allocate(); const int row_bytes = qMin(frame->linesize, copy->linesize_bytes()); for (int y = 0; y < frame->height; y++) { memcpy(copy->data() + y * copy->linesize_bytes(), static_cast(frame->data) + y * frame->linesize, size_t(row_bytes)); } const Rational ts = Timecode::timestamp_to_time(frame->timestamp, viewer->timebase()); QMetaObject::invokeMethod(viewer, [viewer, copy, ts]() { viewer->deliver_facade_frame(copy, ts); }, Qt::QueuedConnection); } void ViewerWidget::facade_audio_callback(const oak_playback_audio *audio, void *userdata) { ViewerWidget *viewer = static_cast(userdata); // Copy the block for the level monitor (the payload dies when we // return; the facade already pushed it to the AudioManager). const AudioParams params( audio->sample_rate, viewer->get_connected_node() ? viewer->get_connected_node()->get_audio_params().channel_layout() : core::k_channel_layout_stereo, core::SampleFormat::f32_p); SampleBuffer buffer(params, size_t(audio->sample_count)); for (int ch = 0; ch < audio->channels; ch++) { memcpy(buffer.to_raw_ptrs()[ch], audio->channel_data[ch], size_t(audio->sample_count) * sizeof(float)); } QMetaObject::invokeMethod(viewer, [buffer]() { AudioMonitor::push_sample_buffer_on_all(buffer); }, Qt::QueuedConnection); } void ViewerWidget::deliver_facade_frame(const FramePtr &frame, const Rational &ts) { if (!is_playing()) { return; } foreach (ViewerDisplayWidget *dw, playback_devices_) { dw->queue()->append_timewise({ ts, QVariant::fromValue(frame) }, playback_speed_); } } 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 = get_connected_node()->get_audio_params(); if (params.is_valid()) { // NOTE: Hardcoded scrubbing interval (20ms) Rational interval = Rational(20, 1000); RenderTicketWatcher *watcher = new RenderTicketWatcher(); connect(watcher, &RenderTicketWatcher::finished, this, &ViewerWidget::received_audio_buffer_for_scrubbing); audio_scrub_watchers_.push_back(watcher); watcher->set_ticket( RenderManager::instance()->get_cacher()->get_range_of_audio( get_connected_node(), TimeRange(get_connected_node()->get_playhead(), get_connected_node()->get_playhead() + interval))); } } } } void ViewerWidget::update_minimum_scale() { if (!get_connected_node()) { return; } if (get_connected_node()->get_length().isNull()) { // Avoids divide by zero set_minimum_scale(0); } else { double min_scale = static_cast(ruler()->width()) / get_connected_node()->get_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 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 get_connected_node()->video_frame_cache()->get_valid_cache_filename( time); } return QString(); } bool ViewerWidget::frame_exists_at_time(const Rational &time) { return get_connected_node() && time >= 0 && time < get_connected_node()->get_video_length(); } bool ViewerWidget::viewer_might_be_a_still() { return get_connected_node() && get_connected_node()->get_connected_texture_output() && get_connected_node()->get_video_length().isNull(); } void ViewerWidget::set_display_image(RenderTicketPtr ticket) { foreach (ViewerDisplayWidget *dw, playback_devices_) { QVariant push; if (ticket) { if (dynamic_cast(dw)) { push = ticket->property("multicam_output"); } else { push = ticket->get(); } } dw->set_image(push); } } RenderTicketPtr ViewerWidget::get_frame(const Rational &t) { if (is_playing()) { return get_single_frame(t); } QString cache_fn = get_connected_node()->video_frame_cache()->get_valid_cache_filename(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 RenderTicketPtr ticket = std::make_shared(); ticket->setProperty("time", QVariant::fromValue(t)); QtConcurrent::run( static_cast( ViewerWidget::decode_cached_image), ticket, get_connected_node()->video_frame_cache()->get_cache_directory(), get_connected_node()->video_frame_cache()->get_uuid(), Timecode::time_to_timestamp(t, timebase(), Timecode::k_floor)); return ticket; } } 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(); auto vp = get_connected_node()->get_video_params(); vp.set_divider(div); auto c = new NodeParamSetStandardValueCommand( NodeKeyframeTrackReference( NodeInput(get_connected_node(), ViewerOutput::k_video_params_input, 0)), QVariant::fromValue(vp)); Core::instance()->undo_stack()->push(c, tr("Changed Playback Resolution")); } 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() { RenderTicketWatcher *ticket = static_cast(sender()); if (nonqueue_watchers_.contains(ticket)) { while (!nonqueue_watchers_.isEmpty()) { // Pop frames that are "old" if (nonqueue_watchers_.takeFirst() == ticket) { break; } } if (ticket->has_result()) { set_display_image(ticket->get_ticket()); } } delete ticket; } 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); for (int d : VideoParams::k_supported_dividers) { playback_res_menu->add_action_with_data( VideoParams::get_name_for_divider(d), d, get_connected_node()->get_video_params().divider()); } connect(playback_res_menu, &QMenu::triggered, this, &ViewerWidget::context_menu_set_playback_res); } { // Deinterlace Option if (get_connected_node()->get_video_params().interlacing() != VideoParams::k_interlace_none) { 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) { if (get_connected_node() && get_connected_node()->get_work_area()->enabled()) { // Jump to in point get_connected_node()->set_playhead( get_connected_node()->get_work_area()->in()); } else { in_to_out_only = false; } } else if (record_armed_) { disarm_recording(); if (get_connected_node()->project()->filename().isEmpty()) { QMessageBox::critical( this, tr("Audio Recording"), tr("Project must be saved before you can record audio.")); return; } QDir audio_path(QFileInfo(get_connected_node()->project()->filename()) .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"), ExportFormat::get_extension(static_cast( OAK_CONFIG("AudioRecordingFormat").toInt())))); AudioParams ap( OAK_CONFIG("AudioRecordingSampleRate").toInt(), OAK_CONFIG("AudioRecordingChannelLayout").toULongLong(), SampleFormat::from_string(OAK_CONFIG("AudioRecordingSampleFormat") .toString() .toStdString())); EncodingParams encode_param; encode_param.enable_audio( ap, static_cast( OAK_CONFIG("AudioRecordingCodec").toInt())); encode_param.set_filename(recording_filename_); encode_param.set_audio_bit_rate( OAK_CONFIG("AudioRecordingBitRate").toInt() * 1000); QString error; if (AudioManager::instance()->start_recording(encode_param, &error)) { 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(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 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() ? get_connected_node()->get_playhead() : 0); length_changed_slot(get_connected_node() ? get_connected_node()->get_length() : 0); } void ViewerWidget::playback_poll_update() { if (!playback_ || !is_playing() || !get_connected_node()) { return; } // The facade playback engine owns the master clock; poll its // position for the playhead, boundary and loop policy (the min/max // part of the old playback_timer_update). int64_t pos_ts = 0; oakengine_playback_get_position(playback_, &pos_ts); Rational current_time = Timecode::timestamp_to_time(pos_ts, 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()->get_work_area()->enabled()) { // If "play in to out" is enabled or we're looping AND we have a workarea, only play the workarea min_time = get_connected_node()->get_work_area()->in(); max_time = get_connected_node()->get_work_area()->out(); } else { // Otherwise set the bounds to the range of the sequence min_time = 0; max_time = get_connected_node()->get_length(); } // 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()) && current_time >= max_time) { // We've reached the end of whatever range we're playing and should either pause // or restart playback (negative speeds are out of scope this round). end_of_line = true; if (OAK_CONFIG("Loop").toBool() && !recording_) { // If we're looping, jump back to the start of the range and continue time_to_set = 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 = max_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; get_connected_node()->set_playhead(time_to_set); time_changed_from_timer_ = false; // Feed the display clocks and purge consumed queue entries. foreach (ViewerDisplayWidget *dw, playback_devices_) { dw->set_playback_timestamp(pos_ts); dw->queue()->purge_before(current_time, playback_speed_); } 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_); } } } void ViewerWidget::set_viewer_resolution(int width, int height) { sizer_->set_child_size(width, height); foreach (ViewerWindow *vw, windows_) { vw->set_resolution(width, height); } } 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_ && get_connected_node()->get_playhead() >= length) { update_texture_from_node(); } last_length_ = length; } } void ViewerWidget::interlacing_changed_slot(VideoParams::Interlacing interlacing) { // Automatically set a "sane" deinterlacing option bool deint = interlacing != VideoParams::k_interlace_none; foreach (ViewerDisplayWidget *dw, playback_devices_) { dw->set_deinterlacing(deint); } } void ViewerWidget::update_renderer_video_parameters() { VideoParams vp = get_connected_node()->get_video_params(); foreach (ViewerDisplayWidget *dw, playback_devices_) { dw->set_video_params(vp); } } void ViewerWidget::update_renderer_audio_parameters() { AudioParams ap = get_connected_node()->get_audio_params(); 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::viewer_invalidated_video_range(const TimeRange &range) { // If our current frame is within this range, we need to update if (!is_playing() && get_connected_node()->get_playhead() >= range.in() && (get_connected_node()->get_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(Project::k_item_mime_type)) { event->accept(); } } void ViewerWidget::dropped(QDropEvent *event) { QByteArray mimedata = event->mimeData()->data(Project::k_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) { Node *item = reinterpret_cast(item_ptr); ViewerOutput *viewer = dynamic_cast(item); if (viewer) { connect_viewer_node(viewer); } } } }