播放器事件

播放器狀態的變更 (例如開始播放、緩衝處理或發生錯誤) 會觸發事件,並傳送至已註冊的 Player.Listener 執行個體。這些事件以整數常數表示,並由 Player.Event 和 Player.Events 定義。

註冊 Player.Listener

系統會向已註冊的 Player.Listener 執行個體回報播放器事件。如要註冊監聽器來接收這類事件,請按照下列步驟操作:

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);

如果您使用 Kotlin,也可以使用 media3-common-ktx 模組提供的暫停擴充功能函式,透過協同程式監聽事件。在這種情況下,您不需要註冊或取消註冊 Player.Listener。

使用 Player.Listener 監聽播放事件

Player.Listener 具有空白的預設方法,因此您只需要實作感興趣的方法。如要完整瞭解這些方法及其呼叫時機,請參閱 Javadoc。以下將詳細說明幾個重要方法。

聽眾可以選擇實作個別事件回呼,或實作在發生一或多個事件後呼叫的通用 onEvents 回呼。請參閱 Individual callbacks vs onEvents,瞭解不同用途應優先使用哪種方法。

播放狀態變更

如要接收播放器狀態的變更,請在已註冊的 Player.Listener 中實作 onPlaybackStateChanged(@State int state)。播放器可處於下列四種播放狀態:

  • Player.STATE_IDLE:這是初始狀態、播放器停止時的狀態,以及播放失敗時的狀態。處於這種狀態時,播放器只會保留有限的資源。
  • Player.STATE_BUFFERING:播放器無法立即從目前位置播放。這主要是因為需要載入更多資料。
  • Player.STATE_READY:播放器可立即從目前位置開始播放。
  • Player.STATE_ENDED:播放器已播放完所有媒體。

除了這些狀態外,播放器還有 playWhenReady 標記,可指出使用者播放意圖。如要接收這項標記的變更,請實作 onPlayWhenReadyChanged(playWhenReady, @PlayWhenReadyChangeReason int reason)。

如果符合下列所有條件,播放器就會播放內容 (也就是說,播放位置會前進,且媒體會呈現給使用者):

  • 播放器處於 Player.STATE_READY 狀態
  • 「playWhenReady」股價 true
  • 因 Player.getPlaybackSuppressionReason 傳回的原因,播放作業不會遭到抑制

不必逐一檢查這些屬性,而是可以呼叫 Player.isPlaying。如要接收這項狀態的變更,請實作 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.
        }
      }
    });

播放錯誤

如要接收導致播放失敗的錯誤,請在已註冊的 Player.Listener 中實作 onPlayerError(PlaybackException error)。發生失敗時,系統會立即呼叫這個方法,然後播放狀態會轉換為 Player.STATE_IDLE。如要重試播放失敗或停止的內容,請呼叫 ExoPlayer.prepare。

請注意,部分 Player 實作會傳遞 PlaybackException 子類別的例項,以提供失敗的額外資訊。舉例來說,ExoPlayer 會傳遞 ExoPlaybackException,其中包含 type、rendererIndex 和其他 ExoPlayer 專屬欄位。

以下範例說明如何偵測因 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.
          }
        }
      }
    });

播放清單轉場效果

每當播放器在播放清單中切換至新的媒體項目時,系統就會在已註冊的 Player.Listener 物件上呼叫 onMediaItemTransition(MediaItem mediaItem, @MediaItemTransitionReason int reason)。原因會指出這是自動轉換、搜尋 (例如在呼叫 player.next() 後)、重複播放相同項目,還是因播放清單變更而導致 (例如移除目前播放的項目)。

中繼資料

由於許多原因,player.getCurrentMediaMetadata()傳回的中繼資料可能會變更:播放清單轉場效果、串流中繼資料更新,或更新目前 MediaItem 播放期間的中繼資料。

如果您對中繼資料變更感興趣 (例如更新顯示目前標題的 UI),可以監聽 onMediaMetadataChanged。

正在指定播放時間點

呼叫 Player.seekTo 方法會導致一系列回呼傳送至已註冊的 Player.Listener 執行個體:

  1. onPositionDiscontinuity搭配reason=DISCONTINUITY_REASON_SEEK。這是呼叫 Player.seekTo 的直接結果。回呼有 PositionInfo 欄位,可表示搜尋前後的位置。
  2. onPlaybackStateChanged 任何與搜尋相關的即時狀態變更。請注意,可能沒有這類變更。

個別回呼與 onEvents

事件監聽器可以選擇實作個別回呼 (例如 onIsPlayingChanged(boolean isPlaying)),以及一般 onEvents(Player player, Events events) 回呼。通用回呼可存取 Player 物件,並指定同時發生的一組 events。在對應個別事件的回呼之後,系統一律會呼叫這個回呼。

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);
  }
}

在下列情況下,建議使用個別事件:

  • 聽眾想知道變更的原因。例如 onPlayWhenReadyChanged 或 onMediaItemTransition 的原因。
  • 事件監聽器只會對透過回呼參數提供的新值採取行動,或觸發其他不依附於回呼參數的項目。
  • 事件監聽器實作會偏好在方法名稱中,清楚指出觸發事件的原因。
  • 監聽器會向需要瞭解所有個別事件和狀態變更的 Analytics 系統回報。

在下列情況中,建議使用一般 onEvents(Player player, Events events):

  • 接聽程式想為多個事件觸發相同邏輯。舉例來說,同時更新 onPlaybackStateChanged 和 onPlayWhenReadyChanged 的 UI。
  • 監聽器需要存取 Player 物件,才能觸發後續事件,例如在媒體項目轉換後搜尋。
  • 監聽器打算一起使用透過個別回呼回報的多個狀態值,或與 Player getter 方法搭配使用。舉例來說,搭配 onTimelineChanged 中提供的 Timeline 使用 Player.getCurrentWindowIndex() 時,只有在 onEvents 回呼中才是安全的。
  • 監聽器感興趣的是事件是否在邏輯上同時發生。例如,onPlaybackStateChanged 變更為 STATE_BUFFERING,這是因為媒體項目轉換。

在某些情況下,聽眾可能需要將個別回呼與一般 onEvents 回呼合併,例如使用 onMediaItemTransition 記錄媒體項目變更原因,但只有在 onEvents 中可一併使用所有狀態變更時,才會採取行動。

使用協同程式監聽播放事件

或者,您也可以使用 Player.listenTo 啟動 Kotlin 協同程式,並指定相關的 Player.Event:

請注意,Player.listen 和 Player.listenTo 可從任何執行緒呼叫,但回呼 lambda 一律會在與 Player.getApplicationLooper 相關聯的執行緒上叫用。因此,即使協同程式是在不同執行緒上啟動,您也可以在回呼 lambda 中安全地存取 Player 方法和狀態屬性。

播放狀態變更

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.
    }
  }
}

播放錯誤

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
      }
    }
  }
}

個別回呼與 onEvents

在協同程式中監聽播放器事件時,您一律會提供 onEvents 回呼的實作項目,而非個別回呼。您可以選擇 Player.listen 或 Player.listenTo,視要由哪些事件觸發 Lambda 叫用而定。但這兩個函式在其他方面是等效的:

聆聽

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)
    }
  }
}

如果您對多種事件類型感興趣,可以將事件清單傳遞至 Player.listenTo。每當發生上述任何事件時,系統就會叫用 Lambda,您可以檢查 Events 參數,確認實際觸發的事件:

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)
  }
}

由於這些函式會在 onEvents 上運作,因此無法存取傳遞至個別回呼的暫時性引數,例如 onMediaItemTransition(..., int reason) 中的原因或 onPositionDiscontinuity(...) 中的 oldPosition。如果您的邏輯依賴這些特定引數 (且這些引數無法做為 Player 上的狀態屬性),則應改用標準 Player.Listener 介面。

使用AnalyticsListener

使用 ExoPlayer 時,可以呼叫 addAnalyticsListener,向播放器註冊 AnalyticsListener。AnalyticsListener 實作項目可以監聽詳細事件,這些事件可能對分析和記錄用途很有幫助。詳情請參閱數據分析頁面。

使用EventLogger

EventLogger 是程式庫直接提供的 AnalyticsListener,用於記錄。將 EventLogger 新增至 ExoPlayer,即可透過單行程式碼啟用實用的額外記錄功能:

Kotlin

player.addAnalyticsListener(EventLogger())

Java

player.addAnalyticsListener(new EventLogger());

詳情請參閱偵錯記錄頁面。

在指定播放位置觸發事件

某些用途需要在特定播放位置觸發事件。這項功能支援使用 PlayerMessage。您可以使用 ExoPlayer.createMessage 建立 PlayerMessage。您可以使用 PlayerMessage.setPosition 設定應執行動作的播放位置。根據預設,訊息會在播放執行緒上執行,但您可以使用 PlayerMessage.setLooper 自訂此設定。PlayerMessage.setDeleteAfterDelivery 可用於控制訊息是否會在每次遇到指定的播放位置時執行 (這可能會因搜尋和重複模式而發生多次),或僅在第一次執行。設定 PlayerMessage 後,即可使用 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();