diff --git a/src/qEmbyApp/components/mpvcontroller.cpp b/src/qEmbyApp/components/mpvcontroller.cpp index cf06c16..d44d26b 100644 --- a/src/qEmbyApp/components/mpvcontroller.cpp +++ b/src/qEmbyApp/components/mpvcontroller.cpp @@ -217,6 +217,7 @@ bool MpvController::init() { observeProperty("eof-reached", MPV_FORMAT_FLAG); observeProperty("cache-speed", MPV_FORMAT_INT64); + observeProperty("demuxer-cache-time", MPV_FORMAT_DOUBLE); observeProperty("volume", MPV_FORMAT_DOUBLE); observeProperty("mute", MPV_FORMAT_FLAG); observeProperty("speed", MPV_FORMAT_DOUBLE); diff --git a/src/qEmbyApp/components/mpvwidget.cpp b/src/qEmbyApp/components/mpvwidget.cpp index d68f704..e425d53 100644 --- a/src/qEmbyApp/components/mpvwidget.cpp +++ b/src/qEmbyApp/components/mpvwidget.cpp @@ -120,6 +120,7 @@ void MpvWidget::onMpvRenderUpdate(void *ctx) { } void MpvWidget::maybeUpdate() { + Q_EMIT renderUpdateRequested(); update(); } diff --git a/src/qEmbyApp/components/mpvwidget.h b/src/qEmbyApp/components/mpvwidget.h index 4a179ba..5b368c1 100644 --- a/src/qEmbyApp/components/mpvwidget.h +++ b/src/qEmbyApp/components/mpvwidget.h @@ -33,6 +33,7 @@ signals: void playbackStateChanged(bool isPaused); void networkSpeedChanged(qint64 bytesPerSecond); void relayActiveChanged(bool active); + void renderUpdateRequested(); void errorOccurred(const QString &errorMsg); protected: diff --git a/src/qEmbyApp/components/nativedanmakuoverlay.cpp b/src/qEmbyApp/components/nativedanmakuoverlay.cpp index b9bc97e..618276f 100644 --- a/src/qEmbyApp/components/nativedanmakuoverlay.cpp +++ b/src/qEmbyApp/components/nativedanmakuoverlay.cpp @@ -155,16 +155,8 @@ NativeDanmakuOverlay::NativeDanmakuOverlay(MpvController *controller, m_frameTimer = new QTimer(this); m_frameTimer->setTimerType(Qt::PreciseTimer); m_frameTimer->setInterval(kFallbackFrameIntervalMs); - connect(m_frameTimer, &QTimer::timeout, this, [this]() { - const qint64 positionMs = currentPositionMs(); - if (m_lastResolvedPositionMs < 0 || positionMs < m_lastResolvedPositionMs || - positionMs - m_lastResolvedPositionMs > 1500) { - rebuildActiveItems(positionMs); - } else { - updateActiveItems(positionMs); - } - update(); - }); + connect(m_frameTimer, &QTimer::timeout, this, + &NativeDanmakuOverlay::advanceFrame); m_scheduleRebuildTimer = new QTimer(this); m_scheduleRebuildTimer->setSingleShot(true); @@ -279,6 +271,32 @@ void NativeDanmakuOverlay::setBottomSubtitleProtected(bool enabled) requestScheduleRebuild(false, false); } +void NativeDanmakuOverlay::setExternalFrameClockEnabled(bool enabled) +{ + if (m_externalFrameClockEnabled == enabled) { + return; + } + + m_externalFrameClockEnabled = enabled; + syncFrameTimer(); +} + +void NativeDanmakuOverlay::advanceFrame() +{ + if (!shouldAdvanceFrames()) { + return; + } + + const qint64 positionMs = currentPositionMs(); + if (m_lastResolvedPositionMs < 0 || positionMs < m_lastResolvedPositionMs || + positionMs - m_lastResolvedPositionMs > 1500) { + rebuildActiveItems(positionMs); + } else { + updateActiveItems(positionMs); + } + update(); +} + void NativeDanmakuOverlay::paintEvent(QPaintEvent *event) { QPainter painter(this); @@ -649,15 +667,17 @@ void NativeDanmakuOverlay::clearScheduledState(bool clearSpriteCache) void NativeDanmakuOverlay::syncFrameTimer() { + if (m_externalFrameClockEnabled) { + m_frameTimer->stop(); + return; + } + const int targetInterval = targetFrameIntervalMs(); if (m_frameTimer->interval() != targetInterval) { m_frameTimer->setInterval(targetInterval); } - const bool shouldRun = - m_visibleRequested && isVisible() && !m_schedule.isEmpty() && - !m_playbackPaused; - if (shouldRun) { + if (shouldAdvanceFrames()) { if (!m_frameTimer->isActive()) { m_frameTimer->start(); } @@ -667,6 +687,12 @@ void NativeDanmakuOverlay::syncFrameTimer() m_frameTimer->stop(); } +bool NativeDanmakuOverlay::shouldAdvanceFrames() const +{ + return m_visibleRequested && isVisible() && !m_schedule.isEmpty() && + !m_playbackPaused; +} + int NativeDanmakuOverlay::targetFrameIntervalMs() const { const QScreen *screen = targetScreen(); diff --git a/src/qEmbyApp/components/nativedanmakuoverlay.h b/src/qEmbyApp/components/nativedanmakuoverlay.h index 690b4ba..cc21163 100644 --- a/src/qEmbyApp/components/nativedanmakuoverlay.h +++ b/src/qEmbyApp/components/nativedanmakuoverlay.h @@ -35,6 +35,8 @@ public: void setDanmakuVisible(bool visible); void setBottomSubtitleProtected(bool enabled); + void setExternalFrameClockEnabled(bool enabled); + void advanceFrame(); protected: void paintEvent(QPaintEvent *event) override; @@ -70,6 +72,7 @@ private: void updateActiveItems(qint64 positionMs); void clearScheduledState(bool clearSpriteCache = false); void syncFrameTimer(); + bool shouldAdvanceFrames() const; int targetFrameIntervalMs() const; QScreen *targetScreen() const; void updateScreenRefreshBinding(); @@ -125,6 +128,7 @@ private: bool m_bottomSubtitleProtected = false; bool m_scheduleDirty = false; bool m_clearSpriteCacheOnNextRebuild = false; + bool m_externalFrameClockEnabled = false; mutable bool m_surfaceRectDirty = true; }; diff --git a/src/qEmbyApp/views/media/playerview.cpp b/src/qEmbyApp/views/media/playerview.cpp index beebeb9..a2ef5e6 100644 --- a/src/qEmbyApp/views/media/playerview.cpp +++ b/src/qEmbyApp/views/media/playerview.cpp @@ -384,93 +384,19 @@ PlayerView::PlayerView(QEmbyCore *core, QWidget *parent) m_bufferTimer = new QTimer(this); - m_bufferTimer->setInterval(500); + m_bufferTimer->setInterval(250); connect(m_bufferTimer, &QTimer::timeout, this, [this]() { - if (m_totalDuration > 0 && m_mpvWidget && m_mpvWidget->controller()) - { - double bufferedPos = m_currentPosition; - - - QVariant stateVar = m_mpvWidget->controller()->getProperty("demuxer-cache-state"); - if (stateVar.isValid() && stateVar.metaType().id() == QMetaType::QVariantMap) - { - auto ranges = stateVar.toMap()["seekable-ranges"].toList(); - if (!ranges.isEmpty()) - { - - bufferedPos = ranges.last().toMap()["end"].toDouble(); - } - } - else - { - - double cacheDuration = - m_mpvWidget->controller()->getProperty("demuxer-cache-duration").toDouble(); - if (cacheDuration > 0) - { - bufferedPos = m_currentPosition + cacheDuration; - } - } - - if (bufferedPos > m_totalDuration) - { - bufferedPos = m_totalDuration; - } - - - m_progressSlider->setBufferValue(static_cast(bufferedPos)); - } + updateBufferProgress(); if (m_showStatisticsOverlay && m_statisticsOverlay) { - updateStatisticsDisplay(); - } - }); - - - m_mousePollTimer = new QTimer(this); - m_mousePollTimer->setInterval(100); - connect(m_mousePollTimer, &QTimer::timeout, this, - [this]() - { - if (!m_isPlaying) - { - if (m_topOpacity->opacity() < 1.0) + m_bufferTimerTick = (m_bufferTimerTick + 1) % 2; + if (m_bufferTimerTick == 0) { - showControls(); + updateStatisticsDisplay(); } - return; - } - - if (areControlsFullyVisible() && this->cursor().shape() != Qt::BlankCursor && - (!m_mpvWidget || m_mpvWidget->cursor().shape() != Qt::BlankCursor)) - { - return; - } - - QPoint currentPos = QCursor::pos(); - if (currentPos == m_lastMousePos) - { - return; - } - - m_lastMousePos = currentPos; - - - if (this->window()->isMinimized() || !this->isVisible()) - { - return; - } - - QPoint localPos = this->mapFromGlobal(currentPos); - bool isMouseInside = this->rect().contains(localPos); - - - if (isMouseInside) - { - handlePointerActivity(currentPos); } }); @@ -1118,6 +1044,9 @@ void PlayerView::setupUi() m_mpvWidget->installEventFilter(this); m_mpvWidget->setMouseTracking(true); m_nativeDanmakuOverlay = new NativeDanmakuOverlay(m_mpvWidget->controller(), this); + m_nativeDanmakuOverlay->setExternalFrameClockEnabled(true); + connect(m_mpvWidget, &MpvWidget::renderUpdateRequested, m_nativeDanmakuOverlay, + &NativeDanmakuOverlay::advanceFrame); m_danmakuController = new PlayerDanmakuController(m_core, m_mpvWidget, m_nativeDanmakuOverlay, this); connect(m_danmakuController, &PlayerDanmakuController::toastRequested, this, [this](const QString &message) { showToast(message); }); @@ -2419,8 +2348,6 @@ void PlayerView::stopAndReport() if (m_reportTimer) m_reportTimer->stop(); - if (m_mousePollTimer) - m_mousePollTimer->stop(); m_longPressHandler->stopKeyLongPress(false); m_longPressHandler->stopMouseEdgeLongPress(); if (m_bufferTimer) @@ -2512,8 +2439,6 @@ void PlayerView::beginViewTeardown() if (m_reportTimer) m_reportTimer->stop(); - if (m_mousePollTimer) - m_mousePollTimer->stop(); if (m_bufferTimer) m_bufferTimer->stop(); if (m_hideTimer) @@ -2974,8 +2899,7 @@ bool PlayerView::eventFilter(QObject *watched, QEvent *event) else if (event->type() == QEvent::MouseMove) { auto *me = static_cast(event); - m_lastMousePos = me->globalPosition().toPoint(); - handlePointerActivity(m_lastMousePos); + handlePointerActivity(me->globalPosition().toPoint()); const QPoint localPos = this->mapFromGlobal(me->globalPosition().toPoint()); const bool isOnHud = m_topHUD->geometry().contains(localPos) || m_bottomHUD->geometry().contains(localPos); const bool movedEnough = @@ -3044,6 +2968,10 @@ bool PlayerView::eventFilter(QObject *watched, QEvent *event) return true; } } + else if (event->type() == QEvent::Enter) + { + handlePointerActivity(QCursor::pos()); + } } @@ -3121,6 +3049,30 @@ void PlayerView::handlePointerActivity(const QPoint &globalPos) showControls(); } +void PlayerView::updateBufferProgress() +{ + if (!m_progressSlider || m_totalDuration <= 0) + { + return; + } + + double bufferedPos = m_bufferedPosition; + if (std::isnan(bufferedPos) || std::isinf(bufferedPos) || bufferedPos <= 0.0) + { + bufferedPos = m_currentPosition; + } + if (bufferedPos < m_currentPosition) + { + bufferedPos = m_currentPosition; + } + if (bufferedPos > m_totalDuration) + { + bufferedPos = m_totalDuration; + } + + m_progressSlider->setBufferValue(static_cast(bufferedPos)); +} + void PlayerView::setCursorHidden(bool hidden) { const auto targetShape = hidden ? Qt::BlankCursor : Qt::ArrowCursor; @@ -3363,8 +3315,22 @@ void PlayerView::onMpvPropertyChanged(const QString &property, const QVariant &v else if (property == "seeking") { + const bool wasSeeking = m_isSeeking; m_isSeeking = value.toBool(); updateLoadingState(); + if (wasSeeking && !m_isSeeking) + { + QTimer::singleShot(0, this, [this]() { reportProgressToServer(); }); + } + } + else if (property == "demuxer-cache-time") + { + const double cacheTime = value.toDouble(); + if (!std::isnan(cacheTime) && !std::isinf(cacheTime) && cacheTime >= 0.0) + { + m_bufferedPosition = cacheTime; + updateBufferProgress(); + } } else if (property == "eof-reached") { @@ -4686,6 +4652,8 @@ void PlayerView::playMedia(const QString &mediaId, const QString &title, const Q m_hasReportedStop = false; m_isPlaybackFinished = false; m_autoPlayAdvanceInProgress = false; + m_bufferedPosition = 0.0; + m_bufferTimerTick = 0; m_currentMediaId = mediaId; m_currentMediaSourceId = mediaId; m_currentPlaySessionId.clear(); @@ -4776,6 +4744,7 @@ void PlayerView::playMedia(const QString &mediaId, const QString &title, const Q m_currentPosition = m_pendingSeekSeconds; m_totalDuration = 0.0; + updateBufferProgress(); if (m_statisticsOverlay) { @@ -5109,7 +5078,6 @@ void PlayerView::playMedia(const QString &mediaId, const QString &title, const Q startPositionTicks); m_reportTimer->start(); - m_mousePollTimer->start(); m_bufferTimer->start(); showControls(); } @@ -5141,7 +5109,15 @@ void PlayerView::onBackClicked() prepareForStackLeave(); - Q_EMIT navigateBack(); + QTimer::singleShot(0, this, + [this]() + { + if (!m_hasReportedStop) + { + return; + } + Q_EMIT navigateBack(); + }); diff --git a/src/qEmbyApp/views/media/playerview.h b/src/qEmbyApp/views/media/playerview.h index 537547d..08427ed 100644 --- a/src/qEmbyApp/views/media/playerview.h +++ b/src/qEmbyApp/views/media/playerview.h @@ -145,6 +145,7 @@ private: void setCursorHidden(bool hidden); bool areControlsFullyVisible() const; void setPlayerChromeVisible(bool visible); + void updateBufferProgress(); void stopTransientUiAnimations(bool immediate = false); void beginViewTeardown(); @@ -261,10 +262,10 @@ private: QParallelAnimationGroup *m_fadeGroup; QTimer *m_hideTimer; QTimer *m_reportTimer; - QTimer *m_mousePollTimer; QTimer *m_bufferTimer; + int m_bufferTimerTick = 0; QString m_currentMediaId; QString m_currentMediaSourceId; @@ -283,6 +284,7 @@ private: double m_currentPosition; double m_totalDuration = 0.0; + double m_bufferedPosition = 0.0; double m_pendingSeekSeconds = 0.0; double m_osdSeekPreviewPosition = -1.0; @@ -333,7 +335,6 @@ private: QRect m_originalGeometry; bool m_wasMaximized = false; QPoint m_dragPos; - QPoint m_lastMousePos; bool m_didDrag = false; QTimer *m_singleClickTimer = nullptr;