/* * Copyright (c) 2012-2022 Meltytech, LLC * * 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 "player.h" #include "scrubbar.h" #include "mainwindow.h" #include "widgets/timespinbox.h" #include "widgets/audioscale.h" #include "settings.h" #include "util.h" #include "widgets/newprojectfolder.h" #include "proxymanager.h" #include #include #include #define VOLUME_KNEE (88) #define SEEK_INACTIVE (-1) #define VOLUME_SLIDER_HEIGHT (300) static const int STATUS_ANIMATION_MS = 350; Player::Player(QWidget *parent) : QWidget(parent) , m_position(0) , m_playPosition(std::numeric_limits::max()) , m_previousIn(-1) , m_previousOut(-1) , m_duration(0) , m_isSeekable(false) , m_isMeltedPlaying(-1) , m_zoomToggleFactor(Settings.playerZoom() == 0.0f ? 1.0f : Settings.playerZoom()) , m_pauseAfterOpen(false) , m_monitorScreen(-1) , m_currentTransport(nullptr) { setObjectName("Player"); Mlt::Controller::singleton(); setupActions(this); m_playIcon = actionPlay->icon(); m_pauseIcon = actionPause->icon(); // Create a layout. QVBoxLayout *vlayout = new QVBoxLayout(this); vlayout->setObjectName("playerLayout"); vlayout->setContentsMargins(0, 0, 0, 0); vlayout->setSpacing(4); // Add tab bar to indicate/select what is playing: clip, playlist, timeline. m_tabs = new QTabBar; m_tabs->setShape(QTabBar::RoundedSouth); m_tabs->setUsesScrollButtons(false); m_tabs->addTab(tr("Source")); m_tabs->addTab(tr("Project")); m_tabs->setTabEnabled(SourceTabIndex, false); m_tabs->setTabEnabled(ProjectTabIndex, false); QHBoxLayout *tabLayout = new QHBoxLayout; tabLayout->setSpacing(8); tabLayout->addWidget(m_tabs); connect(m_tabs, SIGNAL(tabBarClicked(int)), SLOT(onTabBarClicked(int))); // Add status bar. m_statusLabel = new QPushButton; m_statusLabel->setFlat(true); m_statusLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); m_statusLabel->setAutoFillBackground(true); tabLayout->addWidget(m_statusLabel); tabLayout->addStretch(1); if (Settings.drawMethod() != Qt::AA_UseOpenGLES) { QGraphicsOpacityEffect *effect = new QGraphicsOpacityEffect(this); m_statusLabel->setGraphicsEffect(effect); m_statusFadeIn = new QPropertyAnimation(effect, "opacity", this); m_statusFadeIn->setDuration(STATUS_ANIMATION_MS); m_statusFadeIn->setStartValue(0); m_statusFadeIn->setEndValue(1); m_statusFadeIn->setEasingCurve(QEasingCurve::InBack); m_statusFadeOut = new QPropertyAnimation(effect, "opacity", this); m_statusFadeOut->setDuration(STATUS_ANIMATION_MS); m_statusFadeOut->setStartValue(0); m_statusFadeOut->setEndValue(0); m_statusFadeOut->setEasingCurve(QEasingCurve::OutBack); m_statusTimer.setSingleShot(true); connect(&m_statusTimer, SIGNAL(timeout()), m_statusFadeOut, SLOT(start())); connect(m_statusFadeOut, SIGNAL(finished()), SLOT(onFadeOutFinished())); m_statusFadeOut->start(); } else { connect(&m_statusTimer, SIGNAL(timeout()), SLOT(onFadeOutFinished())); } // Add the layouts for managing video view, scroll bars, and audio controls. m_videoLayout = new QHBoxLayout; m_videoLayout->setSpacing(4); m_videoLayout->setContentsMargins(0, 0, 0, 0); vlayout->addLayout(m_videoLayout, 1); m_videoScrollWidget = new QWidget; m_videoLayout->addWidget(m_videoScrollWidget, 10); m_videoLayout->addStretch(); QGridLayout *glayout = new QGridLayout(m_videoScrollWidget); glayout->setSpacing(0); glayout->setContentsMargins(0, 0, 0, 0); // Add the video widgets. m_videoWidget = qobject_cast(MLT.videoWidget()); Q_ASSERT(m_videoWidget); m_videoWidget->setMinimumSize(QSize(1, 1)); glayout->addWidget(m_videoWidget, 0, 0); m_verticalScroll = new QScrollBar(Qt::Vertical); glayout->addWidget(m_verticalScroll, 0, 1); m_verticalScroll->hide(); m_horizontalScroll = new QScrollBar(Qt::Horizontal); glayout->addWidget(m_horizontalScroll, 1, 0); m_horizontalScroll->hide(); // Add the new project widget. m_projectWidget = new NewProjectFolder(this); vlayout->addWidget(m_projectWidget, 10); vlayout->addStretch(); // Add the volume and signal level meter m_volumePopup = new QFrame(this, Qt::Popup); QVBoxLayout *volumeLayoutV = new QVBoxLayout(m_volumePopup); volumeLayoutV->setContentsMargins(0, 0, 0, 0); volumeLayoutV->addSpacerItem(new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::Expanding)); QBoxLayout *volumeLayoutH = new QHBoxLayout; volumeLayoutH->setSpacing(0); volumeLayoutH->setContentsMargins(0, 0, 0, 0); volumeLayoutH->addWidget(new AudioScale); m_volumeSlider = new QSlider(Qt::Vertical); m_volumeSlider->setFocusPolicy(Qt::NoFocus); m_volumeSlider->setMinimumHeight(VOLUME_SLIDER_HEIGHT); m_volumeSlider->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); volumeLayoutH->addWidget(m_volumeSlider); volumeLayoutV->addLayout(volumeLayoutH); m_volumeSlider->setRange(0, 99); m_volumeSlider->setValue(Settings.playerVolume()); setVolume(m_volumeSlider->value()); m_savedVolume = MLT.volume(); m_volumeSlider->setToolTip(tr("Adjust the audio volume")); connect(m_volumeSlider, SIGNAL(valueChanged(int)), this, SLOT(onVolumeChanged(int))); connect(m_volumeSlider, &QAbstractSlider::sliderReleased, m_volumePopup, &QWidget::hide); // Add mute-volume buttons layout #ifdef Q_OS_MAC if (Settings.theme() == "system") volumeLayoutH = new QVBoxLayout; else #endif volumeLayoutH = new QHBoxLayout; volumeLayoutH->setContentsMargins(0, 0, 0, 0); volumeLayoutH->setSpacing(0); volumeLayoutV->addLayout(volumeLayoutH); // Add mute button m_muteButton = new QPushButton(this); m_muteButton->setFocusPolicy(Qt::NoFocus); m_muteButton->setObjectName(QString::fromUtf8("muteButton")); m_muteButton->setIcon(QIcon::fromTheme("audio-volume-muted", QIcon(":/icons/oxygen/32x32/status/audio-volume-muted.png"))); m_muteButton->setToolTip(tr("Silence the audio")); m_muteButton->setCheckable(true); m_muteButton->setChecked(Settings.playerMuted()); onMuteButtonToggled(Settings.playerMuted()); volumeLayoutH->addWidget(m_muteButton); connect(m_muteButton, SIGNAL(clicked(bool)), this, SLOT(onMuteButtonToggled(bool))); // Add the scrub bar. m_scrubber = new ScrubBar(this); m_scrubber->setFocusPolicy(Qt::NoFocus); m_scrubber->setObjectName("scrubBar"); m_scrubber->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); vlayout->addWidget(m_scrubber); // Add toolbar for transport controls. QToolBar *toolbar = new QToolBar(tr("Transport Controls"), this); int s = style()->pixelMetric(QStyle::PM_SmallIconSize); toolbar->setIconSize(QSize(s, s)); toolbar->setContentsMargins(0, 0, 0, 0); QWidget *spacer = new QWidget(this); spacer->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); m_positionSpinner = new TimeSpinBox(this); m_positionSpinner->setToolTip(tr("Current position")); m_positionSpinner->setEnabled(false); m_positionSpinner->setKeyboardTracking(false); m_durationLabel = new QLabel(this); m_durationLabel->setToolTip(tr("Total Duration")); m_durationLabel->setText(" / 00:00:00:00"); m_durationLabel->setFixedWidth(m_positionSpinner->width() - 20); m_inPointLabel = new QLabel(this); m_inPointLabel->setText("--:--:--:--"); m_inPointLabel->setToolTip(tr("In Point")); m_inPointLabel->setFixedWidth(m_positionSpinner->width() - 20); m_selectedLabel = new QLabel(this); m_selectedLabel->setText("--:--:--:--"); m_selectedLabel->setToolTip(tr("Selected Duration")); m_selectedLabel->setFixedWidth(m_positionSpinner->width() - 30); toolbar->addWidget(m_positionSpinner); toolbar->addWidget(m_durationLabel); toolbar->addWidget(spacer); toolbar->addAction(actionSkipPrevious); toolbar->addAction(actionRewind); toolbar->addAction(actionPlay); toolbar->addAction(actionFastForward); toolbar->addAction(actionSkipNext); // Add zoom button to toolbar. m_zoomButton = new QToolButton; m_zoomMenu = new QMenu(this); m_zoomMenu->addAction( QIcon::fromTheme("zoom-fit-best", QIcon(":/icons/oxygen/32x32/actions/zoom-fit-best")), tr("Zoom Fit"), this, SLOT(onZoomTriggered()))->setData(0.0f); m_zoomMenu->addAction( QIcon::fromTheme("zoom-out", QIcon(":/icons/oxygen/32x32/actions/zoom-out")), tr("Zoom 10%"), this, SLOT(onZoomTriggered()))->setData(0.1f); m_zoomMenu->addAction( QIcon::fromTheme("zoom-out", QIcon(":/icons/oxygen/32x32/actions/zoom-out")), tr("Zoom 25%"), this, SLOT(onZoomTriggered()))->setData(0.25f); m_zoomMenu->addAction( QIcon::fromTheme("zoom-out", QIcon(":/icons/oxygen/32x32/actions/zoom-out")), tr("Zoom 50%"), this, SLOT(onZoomTriggered()))->setData(0.5f); m_zoomMenu->addAction( QIcon::fromTheme("zoom-original", QIcon(":/icons/oxygen/32x32/actions/zoom-original")), tr("Zoom 100%"), this, SLOT(onZoomTriggered()))->setData(1.0f); m_zoomMenu->addAction( QIcon::fromTheme("zoom-in", QIcon(":/icons/oxygen/32x32/actions/zoom-in")), tr("Zoom 200%"), this, SLOT(onZoomTriggered()))->setData(2.0f); m_zoomMenu->addAction( QIcon::fromTheme("zoom-in", QIcon(":/icons/oxygen/32x32/actions/zoom-in")), tr("Zoom 300%"), this, SLOT(onZoomTriggered()))->setData(3.0f); m_zoomMenu->addAction( QIcon::fromTheme("zoom-in", QIcon(":/icons/oxygen/32x32/actions/zoom-in")), tr("Zoom 400%"), this, SLOT(onZoomTriggered()))->setData(4.0f); m_zoomMenu->addAction( QIcon::fromTheme("zoom-in", QIcon(":/icons/oxygen/32x32/actions/zoom-in")), tr("Zoom 500%"), this, SLOT(onZoomTriggered()))->setData(5.0f); m_zoomMenu->addAction( QIcon::fromTheme("zoom-in", QIcon(":/icons/oxygen/32x32/actions/zoom-in")), tr("Zoom 750%"), this, SLOT(onZoomTriggered()))->setData(7.5f); m_zoomMenu->addAction( QIcon::fromTheme("zoom-in", QIcon(":/icons/oxygen/32x32/actions/zoom-in")), tr("Zoom 1000%"), this, SLOT(onZoomTriggered()))->setData(10.0f); connect(m_zoomButton, SIGNAL(toggled(bool)), SLOT(toggleZoom(bool))); m_zoomButton->setMenu(m_zoomMenu); m_zoomButton->setPopupMode(QToolButton::MenuButtonPopup); m_zoomButton->setCheckable(true); m_zoomButton->setToolTip(tr("Toggle zoom")); toolbar->addWidget(m_zoomButton); toggleZoom(false); // Add grid display button to toolbar. m_gridButton = new QToolButton; QMenu *gridMenu = new QMenu(this); m_gridActionGroup = new QActionGroup(this); QAction *action = gridMenu->addAction(tr("2x2 Grid"), this, SLOT(onGridToggled())); action->setCheckable(true); action->setData(2); m_gridDefaultAction = action; m_gridActionGroup->addAction(action); action = gridMenu->addAction(tr("3x3 Grid"), this, SLOT(onGridToggled())); action->setCheckable(true); action->setData(3); m_gridActionGroup->addAction(action); action = gridMenu->addAction(tr("4x4 Grid"), this, SLOT(onGridToggled())); action->setCheckable(true); action->setData(4); m_gridActionGroup->addAction(action); action = gridMenu->addAction(tr("16x16 Grid"), this, SLOT(onGridToggled())); action->setCheckable(true); action->setData(16); m_gridActionGroup->addAction(action); action = gridMenu->addAction(tr("20 Pixel Grid"), this, SLOT(onGridToggled())); action->setCheckable(true); action->setData(10020); m_gridActionGroup->addAction(action); action = gridMenu->addAction(tr("10 Pixel Grid"), this, SLOT(onGridToggled())); action->setCheckable(true); action->setData(10010); m_gridActionGroup->addAction(action); action = gridMenu->addAction(tr("80/90% Safe Areas"), this, SLOT(onGridToggled())); action->setCheckable(true); action->setData(8090); m_gridActionGroup->addAction(action); action = gridMenu->addAction(tr("EBU R95 Safe Areas"), this, SLOT(onGridToggled())); action->setCheckable(true); action->setData(95); m_gridActionGroup->addAction(action); gridMenu->addSeparator(); action = gridMenu->addAction(tr("Snapping")); action->setCheckable(true); action->setChecked(true); connect(action, SIGNAL(toggled(bool)), MLT.videoWidget(), SLOT(setSnapToGrid(bool))); connect(m_gridButton, SIGNAL(toggled(bool)), SLOT(toggleGrid(bool))); m_gridButton->setMenu(gridMenu); m_gridButton->setIcon(QIcon::fromTheme("view-grid", QIcon(":/icons/oxygen/32x32/actions/view-grid"))); m_gridButton->setPopupMode(QToolButton::MenuButtonPopup); m_gridButton->setCheckable(true); m_gridButton->setToolTip(tr("Toggle grid display on the player")); toolbar->addWidget(m_gridButton); // Add volume control to toolbar. toolbar->addAction(actionVolume); m_volumeWidget = toolbar->widgetForAction(actionVolume); // Add in-point and selected duration labels to toolbar. spacer = new QWidget(this); spacer->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); toolbar->addWidget(spacer); toolbar->addWidget(m_inPointLabel); toolbar->addWidget(m_selectedLabel); vlayout->addWidget(toolbar); vlayout->addLayout(tabLayout); connect(MLT.videoWidget(), SIGNAL(frameDisplayed(const SharedFrame &)), this, SLOT(onFrameDisplayed(const SharedFrame &))); connect(actionPlay, SIGNAL(triggered()), this, SLOT(togglePlayPaused())); connect(actionPause, SIGNAL(triggered()), this, SLOT(pause())); connect(actionFastForward, SIGNAL(triggered()), this, SLOT(fastForward())); connect(actionRewind, SIGNAL(triggered()), this, SLOT(rewind())); connect(m_scrubber, SIGNAL(seeked(int)), this, SLOT(seek(int))); connect(m_scrubber, SIGNAL(inChanged(int)), this, SLOT(onInChanged(int))); connect(m_scrubber, SIGNAL(outChanged(int)), this, SLOT(onOutChanged(int))); connect(m_positionSpinner, SIGNAL(valueChanged(int)), this, SLOT(seek(int))); connect(this, SIGNAL(endOfStream()), this, SLOT(pause())); connect(this, SIGNAL(gridChanged(int)), MLT.videoWidget(), SLOT(setGrid(int))); connect(this, SIGNAL(zoomChanged(float)), MLT.videoWidget(), SLOT(setZoom(float))); connect(m_horizontalScroll, SIGNAL(valueChanged(int)), MLT.videoWidget(), SLOT(setOffsetX(int))); connect(m_verticalScroll, SIGNAL(valueChanged(int)), MLT.videoWidget(), SLOT(setOffsetY(int))); setFocusPolicy(Qt::StrongFocus); } void Player::connectTransport(const TransportControllable *receiver) { if (receiver == m_currentTransport) return; if (m_currentTransport) disconnect(m_currentTransport); m_currentTransport = receiver; connect(this, SIGNAL(played(double)), receiver, SLOT(play(double))); connect(this, SIGNAL(paused()), receiver, SLOT(pause())); connect(this, SIGNAL(stopped()), receiver, SLOT(stop())); connect(this, SIGNAL(seeked(int)), receiver, SLOT(seek(int))); connect(this, SIGNAL(rewound(bool)), receiver, SLOT(rewind(bool))); connect(this, SIGNAL(fastForwarded(bool)), receiver, SLOT(fastForward(bool))); connect(this, SIGNAL(previousSought(int)), receiver, SLOT(previous(int))); connect(this, SIGNAL(nextSought(int)), receiver, SLOT(next(int))); } void Player::setupActions(QWidget *widget) { actionPlay = new QAction(widget); actionPlay->setObjectName(QString::fromUtf8("actionPlay")); actionPlay->setIcon(QIcon::fromTheme("media-playback-start", QIcon(":/icons/oxygen/32x32/actions/media-playback-start.png"))); actionPlay->setDisabled(true); actionPause = new QAction(widget); actionPause->setObjectName(QString::fromUtf8("actionPause")); actionPause->setIcon(QIcon::fromTheme("media-playback-pause", QIcon(":/icons/oxygen/32x32/actions/media-playback-pause.png"))); actionPause->setDisabled(true); actionSkipNext = new QAction(widget); actionSkipNext->setObjectName(QString::fromUtf8("actionSkipNext")); actionSkipNext->setIcon(QIcon::fromTheme("media-skip-forward", QIcon(":/icons/oxygen/32x32/actions/media-skip-forward.png"))); actionSkipNext->setDisabled(true); actionSkipPrevious = new QAction(widget); actionSkipPrevious->setObjectName(QString::fromUtf8("actionSkipPrevious")); actionSkipPrevious->setIcon(QIcon::fromTheme("media-skip-backward", QIcon(":/icons/oxygen/32x32/actions/media-skip-backward.png"))); actionSkipPrevious->setDisabled(true); actionRewind = new QAction(widget); actionRewind->setObjectName(QString::fromUtf8("actionRewind")); actionRewind->setIcon(QIcon::fromTheme("media-seek-backward", QIcon(":/icons/oxygen/32x32/actions/media-seek-backward.png"))); actionRewind->setDisabled(true); actionFastForward = new QAction(widget); actionFastForward->setObjectName(QString::fromUtf8("actionFastForward")); actionFastForward->setIcon(QIcon::fromTheme("media-seek-forward", QIcon(":/icons/oxygen/32x32/actions/media-seek-forward.png"))); actionFastForward->setDisabled(true); actionVolume = new QAction(widget); actionVolume->setObjectName(QString::fromUtf8("actionVolume")); actionVolume->setIcon(QIcon::fromTheme("player-volume", QIcon(":/icons/oxygen/32x32/actions/player-volume.png"))); retranslateUi(widget); QMetaObject::connectSlotsByName(widget); } void Player::retranslateUi(QWidget *widget) { Q_UNUSED(widget) actionPlay->setText(tr("Play")); #ifndef QT_NO_TOOLTIP actionPlay->setToolTip(tr("Start playback (L)")); #endif // QT_NO_TOOLTIP actionPlay->setShortcut(QString("Space")); actionPause->setText(tr("Pause")); #ifndef QT_NO_TOOLTIP actionPause->setToolTip(tr("Pause playback (K)")); #endif // QT_NO_TOOLTIP actionSkipNext->setText(tr("Skip Next")); #ifndef QT_NO_TOOLTIP actionSkipNext->setToolTip(tr("Skip to the next point (Alt+Right)")); #endif // QT_NO_TOOLTIP actionSkipNext->setShortcut(QString("Alt+Right")); actionSkipPrevious->setText(tr("Skip Previous")); #ifndef QT_NO_TOOLTIP actionSkipPrevious->setToolTip(tr("Skip to the previous point (Alt+Left)")); #endif // QT_NO_TOOLTIP actionSkipPrevious->setShortcut(QString("Alt+Left")); actionRewind->setText(tr("Rewind")); #ifndef QT_NO_TOOLTIP actionRewind->setToolTip(tr("Play quickly backwards (J)")); #endif // QT_NO_TOOLTIP actionFastForward->setText(tr("Fast Forward")); #ifndef QT_NO_TOOLTIP actionFastForward->setToolTip(tr("Play quickly forwards (L)")); #endif // QT_NO_TOOLTIP actionVolume->setText(tr("Volume")); #ifndef QT_NO_TOOLTIP actionVolume->setToolTip(tr("Show the volume control")); #endif } void Player::setIn(int pos) { LOG_DEBUG() << "in" << pos << "out" << m_previousOut; // Changing out must come before in because mlt_playlist will automatically swap them if out < in if (pos >= 0 && pos > m_previousOut) { onOutChanged(m_duration - 1); m_scrubber->setOutPoint(m_duration - 1); } m_scrubber->setInPoint(pos); } void Player::setOut(int pos) { LOG_DEBUG() << "in" << m_previousIn << "out" << pos; // Changing in must come before out because mlt_playlist will automatically swap them if out < in if (pos >= 0 && pos < m_previousIn) { onInChanged(0); m_scrubber->setInPoint(0); } m_scrubber->setOutPoint(pos); } void Player::setMarkers(const QList &markers) { m_scrubber->setMarkers(markers); } QSize Player::videoSize() const { return m_videoWidget->size(); } void Player::resizeEvent(QResizeEvent *) { MLT.onWindowResize(); if (Settings.playerZoom() > 0.0f) { float horizontal = float(m_horizontalScroll->value()) / m_horizontalScroll->maximum(); float vertical = float(m_verticalScroll->value()) / m_verticalScroll->maximum(); adjustScrollBars(horizontal, vertical); } else { m_horizontalScroll->hide(); m_verticalScroll->hide(); } } bool Player::event(QEvent *event) { bool result = QWidget::event(event); if (event->type() == QEvent::PaletteChange) { m_videoScrollWidget->hide(); m_videoScrollWidget->show(); } return result; } void Player::keyPressEvent(QKeyEvent *event) { QWidget::keyPressEvent(event); if (!event->isAccepted()) MAIN.keyPressEvent(event); } void Player::play(double speed) { // Start from beginning if trying to start at the end. if (m_position >= m_duration - 1 && !MLT.isMultitrack()) { emit seeked(m_previousIn); m_position = m_previousIn; } emit played(speed); if (m_isSeekable) { actionPlay->setIcon(m_pauseIcon); actionPlay->setText(tr("Pause")); actionPlay->setToolTip(tr("Pause playback (K)")); } else { actionPlay->setIcon(QIcon::fromTheme("media-playback-stop", QIcon(":/icons/oxygen/32x32/actions/media-playback-stop.png"))); actionPlay->setText(tr("Stop")); actionPlay->setToolTip(tr("Stop playback (K)")); } m_playPosition = m_position; } void Player::pause() { emit paused(); showPaused(); } void Player::stop() { emit stopped(); actionPlay->setIcon(m_playIcon); actionPlay->setText(tr("Play")); actionPlay->setToolTip(tr("Start playback (L)")); } void Player::togglePlayPaused() { if (actionPlay->icon().cacheKey() == m_playIcon.cacheKey()) play(); else if (m_isSeekable) pause(); else stop(); } void Player::seek(int position) { if (m_isSeekable) { if (position >= 0) { emit seeked(qMin(position, MLT.isMultitrack() ? m_duration : m_duration - 1)); } } // Seek implies pause. actionPlay->setIcon(m_playIcon); actionPlay->setText(tr("Play")); actionPlay->setToolTip(tr("Start playback (L)")); m_playPosition = std::numeric_limits::max(); } void Player::reset() { m_scrubber->setMarkers(QList()); m_inPointLabel->setText("--:--:--:-- / "); m_selectedLabel->setText("--:--:--:--"); m_durationLabel->setText(" / 00:00:00:00"); m_scrubber->setDisabled(true); m_scrubber->setScale(1); m_positionSpinner->setValue(0); m_positionSpinner->setDisabled(true); actionPlay->setDisabled(true); actionSkipPrevious->setDisabled(true); actionSkipNext->setDisabled(true); actionRewind->setDisabled(true); actionFastForward->setDisabled(true); m_videoWidget->hide(); m_projectWidget->show(); m_previousIn = m_previousOut = -1; } void Player::onProducerOpened(bool play) { m_projectWidget->hide(); m_videoWidget->show(); m_duration = MLT.producer()->get_length(); m_isSeekable = MLT.isSeekable(); MLT.producer()->set("ignore_points", 1); m_scrubber->setFramerate(MLT.profile().fps()); m_scrubber->setScale(m_duration); if (!MLT.isPlaylist()) m_scrubber->setMarkers(QList()); m_inPointLabel->setText("--:--:--:-- / "); m_selectedLabel->setText("--:--:--:--"); if (m_isSeekable) { m_durationLabel->setText(QString(MLT.producer()->get_length_time()).prepend(" / ")); MLT.producer()->get_length_time(mlt_time_clock); m_previousIn = MLT.isClip() ? MLT.producer()->get_in() : -1; m_scrubber->setEnabled(true); m_scrubber->setInPoint(m_previousIn); m_previousOut = MLT.isClip() ? MLT.producer()->get_out() : -1; m_scrubber->setOutPoint(m_previousOut); } else { m_durationLabel->setText(tr("Not Seekable").prepend(" / ")); m_scrubber->setDisabled(true); // cause scrubber redraw m_scrubber->setScale(m_duration); } m_positionSpinner->setEnabled(m_isSeekable); setVolume(m_volumeSlider->value()); m_savedVolume = MLT.volume(); onMuteButtonToggled(Settings.playerMuted()); toggleZoom(Settings.playerZoom() > 0.0f); actionPlay->setEnabled(true); actionSkipPrevious->setEnabled(m_isSeekable); actionSkipNext->setEnabled(m_isSeekable); actionRewind->setEnabled(m_isSeekable); actionFastForward->setEnabled(m_isSeekable); connectTransport(MLT.transportControl()); // Closing the previous producer might call pause() milliseconds before // calling play() here. Delays while purging the consumer on pause can // interfere with the play() call. So, we delay play a little to let // pause purging to complete. if (play) { if (m_pauseAfterOpen) { m_pauseAfterOpen = false; QTimer::singleShot(500, this, SLOT(postProducerOpened())); if (!MLT.isClip()) { MLT.producer()->seek(0); } } else { if (MLT.consumer()->is_stopped()) { QTimer::singleShot(500, this, SLOT(play())); } else { // This seek purges the consumer to prevent latent end-of-stream detection. seek(0); QTimer::singleShot(500, this, SLOT(play())); } } } } void Player::postProducerOpened() { if (MLT.producer()) seek(MLT.producer()->position()); } void Player::onMeltedUnitOpened() { m_isMeltedPlaying = -1; // unknown m_duration = MLT.producer()->get_length(); m_isSeekable = true; MLT.producer()->set("ignore_points", 1); m_scrubber->setFramerate(MLT.profile().fps()); m_scrubber->setScale(m_duration); m_scrubber->setMarkers(QList()); m_inPointLabel->setText("--:--:--:-- / "); m_selectedLabel->setText("--:--:--:--"); m_durationLabel->setText(QString(MLT.producer()->get_length_time()).prepend(" / ")); MLT.producer()->get_length_time(mlt_time_clock); m_previousIn = MLT.producer()->get_in(); m_scrubber->setEnabled(true); m_scrubber->setInPoint(m_previousIn); m_previousOut = MLT.producer()->get_out(); m_scrubber->setOutPoint(m_previousOut); m_positionSpinner->setEnabled(m_isSeekable); setVolume(m_volumeSlider->value()); m_savedVolume = MLT.volume(); onMuteButtonToggled(Settings.playerMuted()); actionPlay->setEnabled(true); actionSkipPrevious->setEnabled(m_isSeekable); actionSkipNext->setEnabled(m_isSeekable); actionRewind->setEnabled(m_isSeekable); actionFastForward->setEnabled(m_isSeekable); setIn(-1); setOut(-1); setFocus(); } void Player::onDurationChanged() { m_duration = MLT.producer()->get_length(); m_isSeekable = MLT.isSeekable(); m_scrubber->setScale(m_duration); m_scrubber->setMarkers(QList()); m_durationLabel->setText(QString(MLT.producer()->get_length_time()).prepend(" / ")); MLT.producer()->get_length_time(mlt_time_clock); if (MLT.producer()->get_speed() == 0) seek(m_position); else if (m_position >= m_duration) seek(m_duration - 1); } void Player::onFrameDisplayed(const SharedFrame &frame) { if (MLT.producer() && MLT.producer()->get_length() != m_duration) { // This can happen if the profile changes. Reload the properties from the producer. onProducerOpened(false); } int position = frame.get_position(); if (position <= m_duration) { m_position = position; m_positionSpinner->blockSignals(true); m_positionSpinner->setValue(position); m_positionSpinner->blockSignals(false); m_scrubber->onSeek(position); if (m_playPosition < m_previousOut && m_position >= m_previousOut) { seek(m_previousOut); } } if (position >= m_duration - 1) emit endOfStream(); } void Player::updateSelection() { if (MLT.producer() && MLT.producer()->get_in() > 0) { m_inPointLabel->setText(QString(MLT.producer()->get_time("in")).append(" / ")); m_selectedLabel->setText(MLT.producer()->frames_to_time(MLT.producer()->get_playtime())); } else { m_inPointLabel->setText("--:--:--:-- / "); if (MLT.isClip() && MLT.producer()->get_out() < m_duration - 1) { m_selectedLabel->setText(MLT.producer()->frames_to_time(MLT.producer()->get_playtime())); } else if (!MLT.producer() || MLT.producer()->get_in() == 0) { m_selectedLabel->setText("--:--:--:--"); } } } void Player::onInChanged(int in) { if (in != m_previousIn && in >= 0) { int delta = in - MLT.producer()->get_in(); MLT.setIn(in); emit inChanged(delta); } m_previousIn = in; updateSelection(); } void Player::onOutChanged(int out) { if (out != m_previousOut && out >= 0) { int delta = out - MLT.producer()->get_out(); MLT.setOut(out); emit outChanged(delta); } m_previousOut = out; m_playPosition = m_previousOut; // prevent O key from pausing updateSelection(); } void Player::on_actionSkipNext_triggered() { if (m_scrubber->markers().size() > 0) { foreach (int x, m_scrubber->markers()) { if (x > m_position) { emit seeked(x); return; } } emit seeked(m_duration - 1); } else { emit nextSought(m_position); emit nextSought(); } } void Player::on_actionSkipPrevious_triggered() { if (m_scrubber->markers().size() > 0) { QList markers = m_scrubber->markers(); int n = markers.count(); while (n--) { if (markers[n] < m_position) { emit seeked(markers[n]); return; } } emit seeked(0); } else { emit previousSought(m_position); emit previousSought(); } } void Player::rewind(bool forceChangeDirection) { if (m_isSeekable) emit rewound(forceChangeDirection); } void Player::fastForward(bool forceChangeDirection) { if (m_isSeekable) { emit fastForwarded(forceChangeDirection); m_playPosition = m_position; } else { play(); } } void Player::showPaused() { actionPlay->setIcon(m_playIcon); actionPlay->setText(tr("Play")); actionPlay->setToolTip(tr("Start playback (L)")); } void Player::showPlaying() { actionPlay->setIcon(m_pauseIcon); actionPlay->setText(tr("Pause")); actionPlay->setToolTip(tr("Pause playback (K)")); } void Player::switchToTab(TabIndex index) { m_tabs->setCurrentIndex(index); emit tabIndexChanged(index); } void Player::enableTab(TabIndex index, bool enabled) { m_tabs->setTabEnabled(index, enabled); } void Player::onTabBarClicked(int index) { // Do nothing if requested tab is already selected. if (m_tabs->currentIndex() == index) return; switch (index) { case SourceTabIndex: if (MLT.savedProducer() && MLT.savedProducer()->is_valid() && MLT.producer() && MLT.producer()->get_producer() != MLT.savedProducer()->get_producer()) { m_pauseAfterOpen = true; MAIN.open(new Mlt::Producer(MLT.savedProducer())); } break; case ProjectTabIndex: if (MAIN.isMultitrackValid()) { if (!MLT.isMultitrack()) MAIN.seekTimeline(MAIN.multitrack()->position()); } else { if (!MLT.isPlaylist() && MAIN.playlist()) MAIN.seekPlaylist(MAIN.playlist()->position()); } break; } } void Player::setStatusLabel(const QString &text, int timeoutSeconds, QAction *action, QPalette::ColorRole role) { QString s = QString(" %1 ").arg( m_statusLabel->fontMetrics().elidedText(text, Qt::ElideRight, m_scrubber->width() - m_tabs->width() - 30)); m_statusLabel->setText(s); m_statusLabel->setToolTip(text); auto palette = QApplication::palette(); if (role == QPalette::ToolTipBase) { palette.setColor(QPalette::Button, palette.color(role)); palette.setColor(QPalette::ButtonText, palette.color(QPalette::ToolTipText)); } else { palette.setColor(QPalette::Button, palette.color(role)); palette.setColor(QPalette::ButtonText, palette.color(QPalette::WindowText)); } m_statusLabel->setPalette(palette); if (action) connect(m_statusLabel, SIGNAL(clicked(bool)), action, SIGNAL(triggered(bool))); else disconnect(m_statusLabel, SIGNAL(clicked(bool))); if (Settings.drawMethod() != Qt::AA_UseOpenGLES) { // Cancel the fade out. if (m_statusFadeOut->state() == QAbstractAnimation::Running) { m_statusFadeOut->stop(); } if (text.isEmpty()) { // Make it transparent. m_statusTimer.stop(); m_statusFadeOut->setStartValue(0); m_statusFadeOut->start(); } else { // Reset the fade out animation. m_statusFadeOut->setStartValue(1); // Fade in. if (m_statusFadeIn->state() != QAbstractAnimation::Running && !m_statusTimer.isActive()) { m_statusFadeIn->start(); if (timeoutSeconds > 0) m_statusTimer.start(timeoutSeconds * 1000); } } } else { // DirectX if (text.isEmpty()) { m_statusLabel->hide(); } else { m_statusLabel->show(); if (timeoutSeconds > 0) m_statusTimer.start(timeoutSeconds * 1000); } } } void Player::onFadeOutFinished() { m_statusLabel->disconnect(SIGNAL(clicked(bool))); m_statusLabel->setToolTip(QString()); showIdleStatus(); } void Player::adjustScrollBars(float horizontal, float vertical) { if (MLT.profile().width() * m_zoomToggleFactor > m_videoWidget->width()) { m_horizontalScroll->setPageStep(m_videoWidget->width()); m_horizontalScroll->setMaximum(MLT.profile().width() * m_zoomToggleFactor - m_horizontalScroll->pageStep()); m_horizontalScroll->setValue(qRound(horizontal * m_horizontalScroll->maximum())); emit m_horizontalScroll->valueChanged(m_horizontalScroll->value()); m_horizontalScroll->show(); } else { int max = MLT.profile().width() * m_zoomToggleFactor - m_videoWidget->width(); emit m_horizontalScroll->valueChanged(qRound(0.5 * max)); m_horizontalScroll->hide(); } if (MLT.profile().height() * m_zoomToggleFactor > m_videoWidget->height()) { m_verticalScroll->setPageStep(m_videoWidget->height()); m_verticalScroll->setMaximum(MLT.profile().height() * m_zoomToggleFactor - m_verticalScroll->pageStep()); m_verticalScroll->setValue(qRound(vertical * m_verticalScroll->maximum())); emit m_verticalScroll->valueChanged(m_verticalScroll->value()); m_verticalScroll->show(); } else { int max = MLT.profile().height() * m_zoomToggleFactor - m_videoWidget->height(); emit m_verticalScroll->valueChanged(qRound(0.5 * max)); m_verticalScroll->hide(); } } double Player::setVolume(int volume) { const double gain = double(volume) / VOLUME_KNEE; MLT.setVolume(gain); return gain; } void Player::showIdleStatus() { if (Settings.proxyEnabled() && Settings.playerPreviewScale() > 0) { setStatusLabel(tr("Proxy and preview scaling are ON at %1p").arg(ProxyManager::resolution()), -1, nullptr, QPalette::AlternateBase); } else if (Settings.proxyEnabled()) { setStatusLabel(tr("Proxy is ON at %1p").arg(ProxyManager::resolution()), -1, nullptr, QPalette::AlternateBase); } else if (Settings.playerPreviewScale() > 0) { setStatusLabel(tr("Preview scaling is ON at %1p").arg(Settings.playerPreviewScale()), -1, nullptr, QPalette::AlternateBase); } else { setStatusLabel("", -1, nullptr); } } void Player::focusPositionSpinner() const { m_positionSpinner->setFocus(Qt::ShortcutFocusReason); } void Player::moveVideoToScreen(int screen) { if (screen == m_monitorScreen) return; if (screen == -2) { // -2 = embedded if (!m_videoScrollWidget->isFullScreen()) return; m_videoScrollWidget->showNormal(); m_videoLayout->insertWidget(0, m_videoScrollWidget, 10); } else if (QGuiApplication::screens().size() > 1) { // -1 = find first screen the app is not using for (int i = 0; screen == -1 && i < QGuiApplication::screens().size(); i++) { if (i != QApplication::desktop()->screenNumber(this)) screen = i; } m_videoScrollWidget->showNormal(); m_videoScrollWidget->setParent(nullptr); m_videoScrollWidget->move(QGuiApplication::screens().at(screen)->geometry().topLeft()); m_videoScrollWidget->showFullScreen(); } m_monitorScreen = screen; QCoreApplication::processEvents(); } void Player::setPauseAfterOpen(bool pause) { m_pauseAfterOpen = pause; } Player::TabIndex Player::tabIndex() const { return TabIndex(m_tabs->currentIndex()); } //---------------------------------------------------------------------------- // IEC standard dB scaling -- as borrowed from meterbridge (c) Steve Harris static inline float IEC_dB ( float fScale ) { float dB = 0.0f; if (fScale < 0.025f) // IEC_Scale(-60.0f) dB = (fScale / 0.0025f) - 70.0f; else if (fScale < 0.075f) // IEC_Scale(-50.0f) dB = (fScale - 0.025f) / 0.005f - 60.0f; else if (fScale < 0.15f) // IEC_Scale(-40.0f) dB = (fScale - 0.075f) / 0.0075f - 50.0f; else if (fScale < 0.3f) // IEC_Scale(-30.0f) dB = (fScale - 0.15f) / 0.015f - 40.0f; else if (fScale < 0.5f) // IEC_Scale(-20.0f) dB = (fScale - 0.3f) / 0.02f - 30.0f; else /* if (fScale < 1.0f) // IED_Scale(0.0f)) */ dB = (fScale - 0.5f) / 0.025f - 20.0f; return (dB > -0.001f && dB < 0.001f ? 0.0f : dB); } void Player::onVolumeChanged(int volume) { const double gain = setVolume(volume); emit showStatusMessage(QString("%L1 dB").arg(IEC_dB(gain))); Settings.setPlayerVolume(volume); Settings.setPlayerMuted(false); m_muteButton->setChecked(false); actionVolume->setIcon(QIcon::fromTheme("player-volume", QIcon(":/icons/oxygen/32x32/actions/player-volume.png"))); m_muteButton->setIcon(QIcon::fromTheme("audio-volume-muted", QIcon(":/icons/oxygen/32x32/status/audio-volume-muted.png"))); m_muteButton->setToolTip(tr("Mute")); } void Player::onCaptureStateChanged(bool active) { actionPlay->setDisabled(active); } void Player::on_actionVolume_triggered() { // We must show first to realizes the volume popup geometry. m_volumePopup->show(); int x = (m_volumePopup->width() - m_volumeWidget->width()) / 2; x = mapToParent(m_volumeWidget->geometry().bottomLeft()).x() - x; int y = m_scrubber->geometry().height() - m_volumePopup->height(); m_volumePopup->move(mapToGlobal(m_scrubber->geometry().bottomLeft()) + QPoint(x, y)); m_volumeWidget->hide(); m_volumeWidget->show(); } void Player::onMuteButtonToggled(bool checked) { if (checked) { m_savedVolume = MLT.volume(); MLT.setVolume(0); actionVolume->setIcon(QIcon::fromTheme("audio-volume-muted", QIcon(":/icons/oxygen/32x32/status/audio-volume-muted.png"))); m_muteButton->setIcon(QIcon::fromTheme("audio-volume-high", QIcon(":/icons/oxygen/32x32/status/audio-volume-high.png"))); m_muteButton->setToolTip(tr("Unmute")); } else { MLT.setVolume(m_savedVolume); actionVolume->setIcon(QIcon::fromTheme("player-volume", QIcon(":/icons/oxygen/32x32/actions/player-volume.png"))); m_muteButton->setIcon(QIcon::fromTheme("audio-volume-muted", QIcon(":/icons/oxygen/32x32/status/audio-volume-muted.png"))); m_muteButton->setToolTip(tr("Mute")); } Settings.setPlayerMuted(checked); m_volumePopup->hide(); } void Player::setZoom(float factor, const QIcon &icon) { emit zoomChanged(factor); Settings.setPlayerZoom(factor); if (factor == 0.0f) { m_zoomButton->setIcon(icon); m_zoomButton->setChecked(false); m_horizontalScroll->hide(); m_verticalScroll->hide(); } else { m_zoomToggleFactor = factor; adjustScrollBars(0.5f, 0.5f); m_zoomButton->setIcon(icon); m_zoomButton->setChecked(true); } } void Player::onZoomTriggered() { QAction *action = qobject_cast(sender()); setZoom(action->data().toFloat(), action->icon()); } void Player::toggleZoom(bool checked) { foreach (QAction *a, m_zoomMenu->actions()) { if ((!checked || m_zoomToggleFactor == 0.0f) && a->data().toFloat() == 0.0f) { setZoom(0.0f, a->icon()); break; } else if (a->data().toFloat() == m_zoomToggleFactor) { setZoom(m_zoomToggleFactor, a->icon()); break; } } } void Player::onGridToggled() { m_gridButton->setChecked(true); m_gridDefaultAction = qobject_cast(sender()); emit gridChanged(m_gridDefaultAction->data().toInt()); } void Player::toggleGrid(bool checked) { QAction *action = m_gridActionGroup->checkedAction(); if (!checked) { if (action) action->setChecked(false); emit gridChanged(0); } else { if (!action) m_gridDefaultAction->trigger(); } }