Browse Source

PlayerComponent: cleanup playback states

This goes in hand with the C++ host branch vlang-playbackstates. Neither
the C++ nor the web change are backwards-compatible.

This also gets rid of a bunch of crud that is either unnecessary, or
was never used. For example, passing the media URL via the playback
state signals was intended for gapless audio, but will have to be
redone in a better way anyway.

Fixes #485.

We have to map multiple independent states to a single state. Moreover,
these independent state changes can be sent to us in arbitrary order. We
also want to avoid sending temporary states resulting from this (e.g.
player might send us pause=false and playbackactive=true in arbitrary
order - if the latter comes first, we'd have to go to a buffering state,
and immediately leave it again). Solve this by "collecting" all state
sent from mpv until the current batch of updates stops, and then
redeciding the canonical web state.

Certain circumstances, like the single-threaded QtQuick render loop on
Windows, make this a bit harder again, and I see myself forced to create
a bunch of fields for caching state (like m_paused).
Vincent Lang 9 years ago
parent
commit
29e85a0513

+ 88 - 38
src/player/PlayerComponent.cpp

@@ -3,6 +3,7 @@
 #include <Qt>
 #include <QDir>
 #include <QCoreApplication>
+#include <QGuiApplication>
 #include "display/DisplayComponent.h"
 #include "settings/SettingsComponent.h"
 #include "system/SystemComponent.h"
@@ -35,7 +36,10 @@ static void wakeup_cb(void *context)
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 PlayerComponent::PlayerComponent(QObject* parent)
-  : ComponentBase(parent), m_lastPositionUpdate(0.0), m_playbackAudioDelay(0), m_playbackStartSent(false), m_autoPlay(false),  m_window(nullptr), m_mediaFrameRate(0),
+  : ComponentBase(parent), m_state(State::stopped), m_paused(false), m_playbackActive(false),
+  m_inPlayback(false), m_bufferingPercentage(100), m_lastBufferingPercentage(-1),
+  m_lastPositionUpdate(0.0), m_playbackAudioDelay(0),
+  m_playbackStartSent(false), m_window(nullptr), m_mediaFrameRate(0),
   m_restoreDisplayTimer(this), m_reloadAudioTimer(this),
   m_streamSwitchImminent(false), m_doAc3Transcoding(false)
 {
@@ -274,8 +278,7 @@ void PlayerComponent::queueMedia(const QString& url, const QVariantMap& options,
   if (!audioStream.isEmpty())
     extraArgs.insert("ff-aid", audioStream);
 
-  m_autoPlay = options["autoplay"].toBool();
-  extraArgs.insert("pause", m_autoPlay ? "no" : "yes");
+  extraArgs.insert("pause", options["autoplay"].toBool() ? "no" : "yes");
 
   QString userAgent = metadata["headers"].toMap()["User-Agent"].toString();
   if (userAgent.size())
@@ -355,6 +358,66 @@ void PlayerComponent::onRefreshRateChange()
   updateVideoSettings();
 }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+void PlayerComponent::updatePlaybackState()
+{
+  State newState = m_state;
+
+  if (m_inPlayback) {
+    if (m_paused)
+    {
+      newState = State::paused;
+    }
+    else if (m_playbackActive)
+    {
+      newState = State::playing;
+    }
+    else
+    {
+      // Playback not active, but also not buffering means we're in some "other"
+      // waiting state. Pretend to web-client that we're buffering.
+      if (m_bufferingPercentage == 100)
+        m_bufferingPercentage = 0;
+      newState = State::buffering;
+    }
+  }
+  else
+  {
+    if (newState != State::error)
+      newState = State::stopped;
+  }
+
+  if (newState != m_state)
+  {
+    switch (newState) {
+    case State::paused:
+      QLOG_INFO() << "Entering state: paused";
+      emit paused();
+      break;
+    case State::playing:
+      QLOG_INFO() << "Entering state: playing";
+      emit playing();
+      break;
+    case State::stopped:
+      QLOG_INFO() << "Entering state: stopped";
+      emit stopped();
+      break;
+    case State::buffering:
+      QLOG_INFO() << "Entering state: buffering";
+      m_lastBufferingPercentage = -1; /* force update below */
+      break;
+    case State::error:
+      /* handled separately */
+      break;
+    }
+    m_state = newState;
+  }
+
+  if (m_state == State::buffering && m_lastBufferingPercentage != m_bufferingPercentage)
+    emit buffering(m_bufferingPercentage);
+  m_lastBufferingPercentage = m_bufferingPercentage;
+}
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 void PlayerComponent::handleMpvEvent(mpv_event *event)
 {
@@ -362,53 +425,39 @@ void PlayerComponent::handleMpvEvent(mpv_event *event)
   {
     case MPV_EVENT_START_FILE:
     {
-      m_currentUrl = mpv::qt::get_property_variant(m_mpv, "path").toString();
       m_playbackStartSent = false;
-      break;
-    }
-    case MPV_EVENT_FILE_LOADED:
-    {
-      emit playing(m_currentUrl);
+      m_inPlayback = true;
       break;
     }
     case MPV_EVENT_END_FILE:
     {
       mpv_event_end_file *endFile = (mpv_event_end_file *)event->data;
-      switch (endFile->reason)
+      if (endFile->reason == MPV_END_FILE_REASON_ERROR)
       {
-        case MPV_END_FILE_REASON_EOF:
-          emit finished(m_currentUrl);
-          break;
-        case MPV_END_FILE_REASON_ERROR:
-          emit error(endFile->error, mpv_error_string(endFile->error));
-          break;
-        default:
-          emit stopped(m_currentUrl);
-          break;
+        QLOG_INFO() << "Entering state: error";
+        m_state = State::error;
+        emit error(mpv_error_string(endFile->error));
       }
 
-      emit playbackEnded(m_currentUrl);
-      m_currentUrl = "";
+      m_inPlayback = false;
 
       if (!m_streamSwitchImminent)
         m_restoreDisplayTimer.start(0);
       m_streamSwitchImminent = false;
       break;
     }
-    case MPV_EVENT_IDLE:
-    {
-      emit playbackAllDone();
-      break;
-    }
     case MPV_EVENT_PLAYBACK_RESTART:
     {
-      // it's also sent after seeks are completed
-      if (!m_playbackStartSent)
-      {
-        mpv::qt::set_property_variant(m_mpv, "pause", !m_autoPlay);
-        emit paused(!m_autoPlay);
-        emit playbackStarting();
-      }
+#if defined(Q_OS_MAC)
+      // On OSX, initializing VideoTooolbox (hardware decoder API) will mysteriously
+      // show the hidden mouse pointer again. The VTDecompressionSessionCreate API
+      // function does this, and we have no influence over its behavior. To make sure
+      // the cursor is gone again when starting playback, listen to the player's
+      // playbackStarting signal, at which point decoder initialization is guaranteed
+      // to be completed. Then we just have to set the cursor again on the Cocoa level.
+      if (!m_playbackStartSent && QGuiApplication::overrideCursor())
+        QGuiApplication::setOverrideCursor(QCursor(Qt::BlankCursor));
+#endif
       m_playbackStartSent = true;
       break;
     }
@@ -417,17 +466,16 @@ void PlayerComponent::handleMpvEvent(mpv_event *event)
       mpv_event_property *prop = (mpv_event_property *)event->data;
       if (strcmp(prop->name, "pause") == 0 && prop->format == MPV_FORMAT_FLAG)
       {
-        int state = *(int *)prop->data;
-        emit paused(state);
+        m_paused = !!*(int *)prop->data;
       }
       else if (strcmp(prop->name, "core-idle") == 0 && prop->format == MPV_FORMAT_FLAG)
       {
-        emit playbackActive(!*(int *)prop->data);
+        m_playbackActive = !*(int *)prop->data;
+        emit playbackActive(m_playbackActive);
       }
       else if (strcmp(prop->name, "cache-buffering-state") == 0)
       {
-        int64_t percentage = prop->format == MPV_FORMAT_INT64 ? *(int64_t *)prop->data : 100;
-        emit buffering(percentage);
+        m_bufferingPercentage = prop->format == MPV_FORMAT_INT64 ? *(int64_t *)prop->data : 100;
       }
       else if (strcmp(prop->name, "playback-time") == 0 && prop->format == MPV_FORMAT_DOUBLE)
       {
@@ -534,6 +582,8 @@ void PlayerComponent::handleMpvEvents()
       break;
     handleMpvEvent(event);
   }
+  // Once we got all status updates, determine the new canonical state.
+  updatePlaybackState();
 }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////

+ 23 - 24
src/player/PlayerComponent.h

@@ -118,6 +118,14 @@ public:
 
   static QStringList AudioCodecsAll() { return { "ac3", "dts", "eac3", "dts-hd", "truehd" }; };
   static QStringList AudioCodecsSPDIF() { return { "ac3", "dts" }; };
+
+  enum class State {
+    stopped,
+    error,
+    paused,
+    playing,
+    buffering,
+  };
   
 public Q_SLOTS:
   void setAudioConfiguration();
@@ -133,34 +141,20 @@ private Q_SLOTS:
   void onCodecsLoadingDone(CodecsFetcher* sender);
 
 Q_SIGNALS:
-  void playing(const QString& url);
-  void buffering(float);
-  // playback has stopped due to a stop() or loadMedia() request
-  void stopped(const QString& url);
-  // playback has stopped because the current media was fully played
-  void finished(const QString& url);
-  // playback has stopped due to any reason - this always happens if the
-  // playing() signal was emitted
-  void playbackEnded(const QString& url);
-  // emitted if playback has ended, and no more items are queued for playback
-  void playbackAllDone();
-  // emitted after playing(), and as soon as the the media is fully loaded, and
-  // playback starts normally
-  void playbackStarting();
-  void paused(bool paused);
-  // true if the video (or music) is actually
+  // The following signals correspond to the State enum above.
+  void playing();                 // playback is progressing (audio playing, pictures are moving)
+  void buffering(float percent);  // temporary state during "playing", or during media loading
+  void stopped();                 // playback finished successfully, or was stopped with stop()
+  void paused();                  // paused (covers all sub-states)
+  void error(const QString& msg); // playback stopped due to external error
+
+  // true if the video (or music) is actually playing
   // false if nothing is loaded, playback is paused, during seeking, or media is being loaded
   void playbackActive(bool active);
   void windowVisible(bool visible);
   // emitted as soon as the duration of the current file is known
   void updateDuration(qint64 milliseconds);
 
-  // an error happened during playback - this implies abort of playback
-  // the id is the (negative) error number, and the message parameter is a short
-  // English description of the error (always the same for the same id, no
-  // further information)
-  void error(int id, const QString& message);
-
   // current position in ms should be triggered 2 times a second
   // when position updates
   void positionUpdate(quint64);
@@ -179,6 +173,7 @@ private:
   void loadWithOptions(const QVariantMap& options);
   void setRpiWindow(QQuickWindow* window);
   void setQtQuickWindow(QQuickWindow* window);
+  void updatePlaybackState();
   void handleMpvEvent(mpv_event *event);
   // Potentially switch the display refresh rate, and return true if the refresh rate
   // was actually changed.
@@ -196,11 +191,15 @@ private:
 
   mpv::qt::Handle m_mpv;
 
+  State m_state;
+  bool m_paused;
+  bool m_playbackActive;
+  bool m_inPlayback;
+  int m_bufferingPercentage;
+  int m_lastBufferingPercentage;
   double m_lastPositionUpdate;
   qint64 m_playbackAudioDelay;
-  QString m_currentUrl;
   bool m_playbackStartSent;
-  bool m_autoPlay;
   QQuickWindow* m_window;
   float m_mediaFrameRate;
   QTimer m_restoreDisplayTimer;

+ 3 - 11
src/power/PowerComponent.cpp

@@ -45,8 +45,7 @@ bool PowerComponent::componentInitialize()
 {
   PlayerComponent* player = &PlayerComponent::Get();
 
-  connect(player, &PlayerComponent::playing, this, &PowerComponent::playbackStarted);
-  connect(player, &PlayerComponent::playbackEnded, this, &PowerComponent::playbackEnded);
+  connect(player, &PlayerComponent::playbackActive, this, &PowerComponent::playbackActive);
 
   return true;
 }
@@ -81,16 +80,9 @@ void PowerComponent::redecideScreeensaverState()
 }
 
 /////////////////////////////////////////////////////////////////////////////////////////
-void PowerComponent::playbackStarted()
+void PowerComponent::playbackActive(bool active)
 {
-  m_videoPlaying = true;
-  redecideScreeensaverState();
-}
-
-/////////////////////////////////////////////////////////////////////////////////////////
-void PowerComponent::playbackEnded()
-{
-  m_videoPlaying = false;
+  m_videoPlaying = active;
   redecideScreeensaverState();
 }
 

+ 1 - 2
src/power/PowerComponent.h

@@ -48,8 +48,7 @@ public Q_SLOTS:
   virtual bool Suspend() { return false; }
 
 private Q_SLOTS:
-  void playbackStarted();
-  void playbackEnded();
+  void playbackActive(bool active);
 
 Q_SIGNALS:
   void screenSaverEnabled();

+ 0 - 18
src/ui/KonvergoWindow.cpp

@@ -60,9 +60,6 @@ KonvergoWindow::KonvergoWindow(QWindow* parent) : QQuickWindow(parent), m_debugL
   connect(&PlayerComponent::Get(), &PlayerComponent::windowVisible,
           this, &KonvergoWindow::playerWindowVisible);
 
-  connect(&PlayerComponent::Get(), &PlayerComponent::playbackStarting,
-          this, &KonvergoWindow::playerPlaybackStarting);
-
   // this is using old syntax because ... reasons. QQuickCloseEvent is not public class
   connect(this, SIGNAL(closing(QQuickCloseEvent*)), this, SLOT(closingWindow()));
 
@@ -285,21 +282,6 @@ void KonvergoWindow::focusOutEvent(QFocusEvent * ev)
 #endif
 }
 
-/////////////////////////////////////////////////////////////////////////////////////////
-void KonvergoWindow::playerPlaybackStarting()
-{
-#if defined(Q_OS_MAC)
-  // On OSX, initializing VideoTooolbox (hardware decoder API) will mysteriously
-  // show the hidden mouse pointer again. The VTDecompressionSessionCreate API
-  // function does this, and we have no influence over its behavior. To make sure
-  // the cursor is gone again when starting playback, listen to the player's
-  // playbackStarting signal, at which point decoder initialization is guaranteed
-  // to be completed. Then we just have to set the cursor again on the Cocoa level.
-  if (QGuiApplication::overrideCursor())
-    QGuiApplication::setOverrideCursor(QCursor(Qt::BlankCursor));
-#endif
-}
-
 /////////////////////////////////////////////////////////////////////////////////////////
 void KonvergoWindow::RegisterClass()
 {

+ 0 - 1
src/ui/KonvergoWindow.h

@@ -94,7 +94,6 @@ private slots:
   void onScreenCountChanged(int newCount);
   void updateDebugInfo();
   void playerWindowVisible(bool visible);
-  void playerPlaybackStarting();
 
 private:
   void notifyScale(const QSize& size);