Zdarzenia dotyczące odtwarzacza

Zmiany stanu odtwarzacza (np. rozpoczęcie odtwarzania, buforowanie lub błędy) wywołują zdarzenia, które są wysyłane do zarejestrowanych Player.Listener instancji. Te zdarzenia są reprezentowane przez stałe całkowite i są zdefiniowane przez Player.Event i Player.Events.

Rejestrowanie Player.Listener

Zdarzenia odtwarzacza są zgłaszane do zarejestrowanych Player.Listener instancji. Aby zarejestrować detektor, który będzie odbierać takie zdarzenia:

Kotlin

// Add a listener to receive events from the player.
player.addListener(listener)

Java

// Add a listener to receive events from the player.
player.addListener(listener);

Jeśli używasz języka Kotlin, możesz też użyć funkcji rozszerzających zawieszanie udostępnianych przez moduł media3-common-ktx, aby nasłuchiwać zdarzeń za pomocą współprogramów. W takim przypadku nie musisz rejestrować ani wyrejestrowywać Player.Listener.

Nasłuchiwanie zdarzeń odtwarzania za pomocą Player.Listener

Player.Listener ma puste metody domyślne, więc musisz zaimplementować tylko te metody, które Cię interesują. Pełny opis metod i momentów ich wywoływania znajdziesz w dokumentacji Javadoc. Niektóre z najważniejszych metod opisujemy szczegółowo poniżej.

Detektory mogą implementować poszczególne wywołania zwrotne zdarzeń lub ogólne wywołanie zwrotne onEvents, które jest wywoływane po wystąpieniu co najmniej 1 zdarzenia. Więcej informacji o tym, które Individual callbacks vs onEvents jest preferowane w różnych przypadkach użycia, znajdziesz w sekcji.

Zmiany stanu odtwarzania

Zmiany stanu odtwarzacza można odbierać, implementując onPlaybackStateChanged(@State int state) w zarejestrowanym Player.Listener. Odtwarzacz może mieć jeden z 4 stanów odtwarzania:

  • Player.STATE_IDLE: jest to stan początkowy, stan, w którym odtwarzacz jest zatrzymany, oraz stan, w którym odtwarzanie nie powiodło się. W tym stanie odtwarzacz będzie używać tylko ograniczonych zasobów.
  • Player.STATE_BUFFERING: odtwarzacz nie może odtworzyć od razu od bieżącej pozycji. Dzieje się tak głównie dlatego, że trzeba wczytać więcej danych.
  • Player.STATE_READY: odtwarzacz może odtworzyć od razu od bieżącej pozycji.
  • Player.STATE_ENDED: odtwarzacz zakończył odtwarzanie wszystkich multimediów.

Oprócz tych stanów odtwarzacz ma flagę playWhenReady, która wskazuje, że użytkownik chce odtworzyć. Zmiany tej flagi można odbierać, implementując onPlayWhenReadyChanged(playWhenReady, @PlayWhenReadyChangeReason int reason).

Odtwarzacz odtwarza (czyli jego pozycja się zmienia, a multimedia są wyświetlane użytkownikowi), gdy spełnione są wszystkie 3 z tych warunków:

  • Odtwarzacz jest w stanie Player.STATE_READY.
  • playWhenReady ma wartość true.
  • Odtwarzanie nie jest wstrzymane z powodu zwróconego przez Player.getPlaybackSuppressionReason.

Zamiast sprawdzać te właściwości osobno, możesz wywołać Player.isPlaying. Zmiany tego stanu można odbierać, implementując onIsPlayingChanged(boolean isPlaying):

Kotlin

player.addListener(
  object : Player.Listener {
    override fun onIsPlayingChanged(isPlaying: Boolean) {
      if (isPlaying) {
        // Active playback.
      } else {
        // Not playing because playback is paused, ended, suppressed, or the player
        // is buffering, stopped or failed. Check player.playWhenReady,
        // player.playbackState, player.playbackSuppressionReason and
        // player.playerError for details.
      }
    }
  }
)

Java

player.addListener(
    new Player.Listener() {
      @Override
      public void onIsPlayingChanged(boolean isPlaying) {
        if (isPlaying) {
          // Active playback.
        } else {
          // Not playing because playback is paused, ended, suppressed, or the player
          // is buffering, stopped or failed. Check player.getPlayWhenReady,
          // player.getPlaybackState, player.getPlaybackSuppressionReason and
          // player.getPlaybackError for details.
        }
      }
    });

Błędy odtwarzania

Błędy, które powodują niepowodzenie odtwarzania, można odbierać, implementując onPlayerError(PlaybackException error) w zarejestrowanym Player.Listener. Gdy wystąpi błąd, ta metoda zostanie wywołana bezpośrednio przed przejściem stanu odtwarzania do Player.STATE_IDLE. Nieudane lub zatrzymane odtwarzanie można ponowić, wywołując ExoPlayer.prepare.

Pamiętaj, że niektóre Player implementacje przekazują instancje podklas PlaybackException, aby dostarczyć dodatkowe informacje o błędzie. Na przykład, ExoPlayer przekazuje ExoPlaybackException, który ma pola type, rendererIndex, i inne pola specyficzne dla ExoPlayer.

Poniższy przykład pokazuje, jak wykryć, kiedy odtwarzanie nie powiodło się z powodu problemu z siecią HTTP:

Kotlin

player.addListener(
  object : Player.Listener {
    override fun onPlayerError(error: PlaybackException) {
      val cause = error.cause
      if (cause is HttpDataSourceException) {
        // An HTTP error occurred.
        val httpError = cause
        // It's possible to find out more about the error both by casting and by querying
        // the cause.
        if (httpError is InvalidResponseCodeException) {
          // Cast to InvalidResponseCodeException and retrieve the response code, message
          // and headers.
        } else {
          // Try calling httpError.getCause() to retrieve the underlying cause, although
          // note that it may be null.
        }
      }
    }
  }
)

Java

player.addListener(
    new Player.Listener() {
      @Override
      public void onPlayerError(PlaybackException error) {
        @Nullable Throwable cause = error.getCause();
        if (cause instanceof HttpDataSourceException) {
          // An HTTP error occurred.
          HttpDataSourceException httpError = (HttpDataSourceException) cause;
          // It's possible to find out more about the error both by casting and by querying
          // the cause.
          if (httpError instanceof HttpDataSource.InvalidResponseCodeException) {
            // Cast to InvalidResponseCodeException and retrieve the response code, message
            // and headers.
          } else {
            // Try calling httpError.getCause() to retrieve the underlying cause, although
            // note that it may be null.
          }
        }
      }
    });

Przejścia playlisty

Gdy odtwarzacz przechodzi do nowego elementu multimedialnego na playliście onMediaItemTransition(MediaItem mediaItem, @MediaItemTransitionReason int reason) wywoływana jest w zarejestrowanych obiektach Player.Listener. Powód wskazuje, czy było to przejście automatyczne, wyszukiwanie (np. po wywołaniu player.next()), powtórzenie tego samego elementu czy zmiana playlisty (np. jeśli aktualnie odtwarzany element zostanie usunięty).

Metadane

Metadane zwracane przez player.getCurrentMediaMetadata() mogą się zmieniać z wielu powodów: przejścia playlisty, aktualizacje metadanych In-Stream lub aktualizacja bieżącego MediaItem w trakcie odtwarzania.

Jeśli interesują Cię zmiany metadanych, np. aby zaktualizować interfejs, który wyświetla bieżący tytuł, możesz nasłuchiwać onMediaMetadataChanged.

Szukam

Wywołanie metod Player.seekTo powoduje serię wywołań zwrotnych do zarejestrowanych instancji Player.Listener:

  1. onPositionDiscontinuity z reason=DISCONTINUITY_REASON_SEEK. Jest to bezpośredni wynik wywołania Player.seekTo. Wywołanie zwrotne ma pola PositionInfo dla pozycji przed i po wyszukiwaniu.
  2. onPlaybackStateChanged z każdą natychmiastową zmianą stanu związaną z wyszukiwaniem. Pamiętaj, że taka zmiana może nie wystąpić.

Poszczególne wywołania zwrotne a onEvents

Detektory mogą implementować poszczególne wywołania zwrotne, takie jak onIsPlayingChanged(boolean isPlaying), oraz ogólne wywołanie zwrotne onEvents(Player player, Events events). Ogólne wywołanie zwrotne zapewnia dostęp do obiektu Player i określa zestaw events, które wystąpiły razem. To wywołanie zwrotne jest zawsze wywoływane po wywołaniach zwrotnych odpowiadających poszczególnym zdarzeniom.

Kotlin

override fun onEvents(player: Player, events: Player.Events) {
  if (
    events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED) ||
      events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)
  ) {
    uiModule.updateUi(player)
  }
}

Java

@Override
public void onEvents(Player player, Events events) {
  if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)
      || events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)) {
    uiModule.updateUi(player);
  }
}

W tych przypadkach należy preferować poszczególne zdarzenia:

  • Detektor jest zainteresowany przyczynami zmian. Na przykład przyczyny podane w przypadku onPlayWhenReadyChanged lub onMediaItemTransition.
  • Detektor działa tylko na podstawie nowych wartości podanych w parametrach wywołania zwrotnego lub wywołuje coś innego, co nie zależy od parametrów wywołania zwrotnego.
  • Implementacja detektora preferuje wyraźne wskazanie, co wywołało zdarzenie, w nazwie metody.
  • Detektor zgłasza do systemu analitycznego, który musi znać wszystkie poszczególne zdarzenia i zmiany stanu.

W tych przypadkach należy preferować ogólne wywołanie zwrotne onEvents(Player player, Events events):

  • Detektor chce wywołać tę samą logikę w przypadku wielu zdarzeń. Na przykład aktualizowanie interfejsu zarówno w przypadku onPlaybackStateChanged, jak i onPlayWhenReadyChanged.
  • Detektor musi mieć dostęp do obiektu Player, aby wywoływać kolejne zdarzenia, np. wyszukiwanie po przejściu elementu multimedialnego.
  • Detektor zamierza używać razem wielu wartości stanu, które są zgłaszane za pomocą osobnych wywołań zwrotnych, lub w połączeniu z metodami pobierania Player. Na przykład używanie Player.getCurrentWindowIndex() z Timeline podanym w onTimelineChanged jest bezpieczne tylko w wywołaniu zwrotnym onEvents.
  • Detektor jest zainteresowany tym, czy zdarzenia wystąpiły logicznie razem. Na przykład onPlaybackStateChanged do STATE_BUFFERING z powodu przejścia elementu multimedialnego.

W niektórych przypadkach detektory mogą potrzebować łączenia poszczególnych wywołań zwrotnych z ogólnym wywołaniem zwrotnym onEvents, np. aby rejestrować przyczyny zmiany elementu multimedialnego za pomocą onMediaItemTransition, ale działać tylko wtedy, gdy wszystkie zmiany stanu można używać razem w onEvents.

Nasłuchiwanie zdarzeń odtwarzania za pomocą współprogramów

Możesz też uruchomić współprogram Kotlin za pomocą Player.listenTo i określić odpowiedni Player.Event:

Pamiętaj, że Player.listen i Player.listenTo można wywoływać z dowolnego wątku, a lambda wywołania zwrotnego jest zawsze wywoływana w wątku powiązanym z Player.getApplicationLooper. Dlatego w lambdzie wywołania zwrotnego można bezpiecznie uzyskiwać dostęp do metod Player i właściwości stanu, nawet jeśli współprogram został uruchomiony w innym wątku.

Zmiany stanu odtwarzania

coroutineScope.launch {
  player.listenTo(Player.EVENT_IS_PLAYING_CHANGED) {
    // `Player` is a receiver scope for this trailing lambda
    if (isPlaying) {
      // Active playback.
    } else {
      // Not playing.
    }
  }
}

Błędy odtwarzania

coroutineScope.launch {
  player.listenTo(Player.EVENT_PLAYER_ERROR) {
    val error = playerError ?: return@listenTo
    val cause = error.cause
    if (cause is HttpDataSourceException) {
      // An HTTP error occurred.
      if (cause is InvalidResponseCodeException) {
        // Retrieve the response code, message and headers
      } else {
        // Try calling cause.cause to retrieve the underlying cause
      }
    }
  }
}

Poszczególne wywołania zwrotne a onEvents

Podczas nasłuchiwania zdarzeń odtwarzacza wewnątrz współprogramu zawsze będziesz implementować wywołanie zwrotne onEvents, a nie poszczególne wywołanie zwrotne. Możesz wybrać między Player.listen a Player.listenTo w zależności od tego, które zdarzenia mają wywoływać Twoją lambdę. Funkcje są jednak równoważne:

posłuchaj

coroutineScope.launch {
  player.listen { events ->
    // `Player` is a receiver scope for this trailing lambda
    if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)) {
      // Access the player state directly from the receiver
      updateUi(playbackState)
    }

    if (events.contains(Player.EVENT_PLAYER_ERROR)) {
      // Access the error directly from the player
      handleError(playerError)
    }
  }
}

listenTo

coroutineScope.launch {
  player.listenTo(Player.EVENT_PLAYBACK_STATE_CHANGED, Player.EVENT_PLAYER_ERROR) { events ->
    // `Player` is a receiver scope for this trailing lambda
    if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)) {
      // Access the player state directly from the receiver
      updateUi(playbackState)
    }

    if (events.contains(Player.EVENT_PLAYER_ERROR)) {
      // Access the error directly from the player
      handleError(playerError)
    }
  }
}

Jeśli interesuje Cię kilka typów zdarzeń, możesz przekazać listę zdarzeń do Player.listenTo. Twoja lambda zostanie wywołana, gdy wystąpi dowolne z tych zdarzeń, a Ty możesz sprawdzić parametr Events, aby sprawdzić, które zdarzenia zostały wywołane:

coroutineScope.launch {
  player.listenTo(Player.EVENT_PLAYBACK_STATE_CHANGED, Player.EVENT_PLAYER_ERROR) { events ->
    // Unclear which event got triggered without querying `events` parameter
    // The following function will fire whenever either one is caught
    updateUiAndHandleError(playbackState, playerError)
  }
}

Ponieważ te funkcje działają na onEvents, nie zapewniają dostępu do argumentów przejściowych przekazywanych do poszczególnych wywołań zwrotnych, takich jak przyczyna w onMediaItemTransition(..., int reason) czy oldPosition w onPositionDiscontinuity(...). Jeśli Twoja logika opiera się na tych konkretnych argumentach (i nie są one dostępne jako właściwości stanu w Player), użyj standardowego interfejsu Player.Listener.

Używanie AnalyticsListener

Gdy używasz ExoPlayer, możesz zarejestrować AnalyticsListener w odtwarzaczu wywołując addAnalyticsListener. Implementacje AnalyticsListener mogą nasłuchiwać szczegółowych zdarzeń, które mogą być przydatne do celów analitycznych i rejestrowania. Więcej informacji znajdziesz na stronie Analytics.

Używanie EventLogger

EventLogger to AnalyticsListener udostępniany bezpośrednio przez bibliotekę do celów rejestrowania. Aby włączyć przydatne dodatkowe rejestrowanie, dodaj EventLogger do ExoPlayer za pomocą jednego wiersza:

Kotlin

player.addAnalyticsListener(EventLogger())

Java

player.addAnalyticsListener(new EventLogger());

Więcej informacji znajdziesz na stronie rejestrowania debugowania.

Wywoływanie zdarzeń w określonych pozycjach odtwarzania

W niektórych przypadkach użycia trzeba wywoływać zdarzenia w określonych pozycjach odtwarzania. Jest to obsługiwane za pomocą PlayerMessage. PlayerMessage można utworzyć za pomocą ExoPlayer.createMessage. Pozycję odtwarzania, w której ma zostać wykonana, można ustawić za pomocą PlayerMessage.setPosition. Domyślnie wiadomości są wykonywane w wątku odtwarzania, ale można to dostosować za pomocą PlayerMessage.setLooper. PlayerMessage.setDeleteAfterDelivery umożliwia określenie, czy wiadomość będzie wykonywana za każdym razem, gdy zostanie napotkana określona pozycja odtwarzania (może się to zdarzyć wielokrotnie z powodu wyszukiwania i trybów powtarzania), czy tylko za pierwszym razem. Po skonfigurowaniu PlayerMessage można ją zaplanować za pomocą PlayerMessage.send.

Kotlin

player
  .createMessage { messageType: Int, payload: Any? -> }
  .setLooper(Looper.getMainLooper())
  .setPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 120000)
  .setPayload(customPayloadData)
  .setDeleteAfterDelivery(false)
  .send()

Java

player
    .createMessage(
        (messageType, payload) -> {
          // Do something at the specified playback position.
        })
    .setLooper(Looper.getMainLooper())
    .setPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 120_000)
    .setPayload(customPayloadData)
    .setDeleteAfterDelivery(false)
    .send();