プレーヤー イベント

再生イベントをリッスンする

状態の変化や再生エラーなどのイベントは、登録済みの 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);

Player.Listener には空のデフォルト メソッドがあるため、必要なメソッドだけを実装すれば済みます。メソッドの詳細と呼び出しのタイミングについては、Javadoc をご覧ください。最も重要なメソッドの一部について、以下で詳しく説明します。

リスナーは、個々のイベント コールバックを実装するか、1 つ以上のイベントが同時に発生した後に呼び出される汎用 onEvents コールバックを実装するかを選択できます。さまざまなユースケースでどちらを優先すべきかについては、Individual callbacks vs onEvents をご覧ください。

再生状態の変更

プレーヤーの状態の変更は、登録された Player.ListeneronPlaybackStateChanged(@State int state) を実装することで受信できます。プレーヤーは次の 4 つの再生状態のいずれかになります。

  • Player.STATE_IDLE: これは初期状態、プレーヤーが停止した状態、再生が失敗した状態です。この状態では、プレーヤーは限られたリソースのみを保持します。
  • Player.STATE_BUFFERING: プレイヤーは現在の位置からすぐに再生できません。これは主に、読み込む必要のあるデータが増えることが原因です。
  • Player.STATE_READY: プレーヤーは現在の位置からすぐに再生できます。
  • Player.STATE_ENDED: プレーヤーがすべてのメディアの再生を終了しました。

これらの状態に加えて、プレーヤーにはユーザーの再生意図を示す playWhenReady フラグがあります。このフラグの変更は、onPlayWhenReadyChanged(playWhenReady, @PlayWhenReadyChangeReason int reason) を実装することで受け取ることができます。

次の 3 つの条件をすべて満たす場合、プレーヤーは再生中(位置が進行し、メディアがユーザーに提示されている)です。

  • プレーヤーが 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.ListeneronPlayerError(PlaybackException error) を実装することで受信できます。エラーが発生すると、このメソッドは再生状態が Player.STATE_IDLE に移行する直前に呼び出されます。再生に失敗した場合や再生が停止した場合は、ExoPlayer.prepare を呼び出して再試行できます。

一部の Player 実装では、失敗に関する追加情報を提供するために PlaybackException のサブクラスのインスタンスを渡します。たとえば、ExoPlayerExoPlaybackException を渡します。これには typerendererIndex、その他の 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);
  }
}

次のような場合は、個々のイベントを使用することをおすすめします。

  • リスナーは変更の理由に関心があります。たとえば、onPlayWhenReadyChangedonMediaItemTransition に指定された理由などです。
  • リスナーは、コールバック パラメータで提供された新しい値に対してのみ動作するか、コールバック パラメータに依存しない別のものをトリガーします。
  • リスナーの実装では、メソッド名でイベントのトリガーを明確に示していることが望ましいです。
  • リスナーは、個々のイベントと状態の変化をすべて把握する必要がある分析システムにレポートします。

次の場合は、汎用 onEvents(Player player, Events events) を優先する必要があります。

  • リスナーが複数のイベントに対して同じロジックをトリガーしたい。たとえば、onPlaybackStateChangedonPlayWhenReadyChanged の両方の UI を更新します。
  • リスナーは、メディア アイテムの切り替え後のシークなど、さらなるイベントをトリガーするために Player オブジェクトにアクセスする必要があります。
  • リスナーは、個別のコールバックを通じてレポートされる複数の状態値を一緒に使用するか、Player ゲッター メソッドと組み合わせて使用することを想定しています。たとえば、onTimelineChanged で提供される Timeline とともに Player.getCurrentWindowIndex() を使用することは、onEvents コールバック内でのみ安全です。
  • リスナーは、イベントが論理的に同時に発生したかどうかに関心があります。たとえば、メディア アイテムの切り替えにより onPlaybackStateChanged から STATE_BUFFERING になります。

場合によっては、リスナーが個々のコールバックを汎用の onEvents コールバックと組み合わせる必要があります。たとえば、onMediaItemTransition でメディア アイテムの変更理由を記録し、すべての状態変更を onEvents でまとめて使用できる場合にのみ処理を行う場合などです。

AnalyticsListener の使用

ExoPlayer を使用する場合、addAnalyticsListener を呼び出すことで、AnalyticsListener をプレーヤーに登録できます。AnalyticsListener の実装では、分析やロギングに役立つ詳細なイベントをリッスンできます。詳しくは、アナリティクスのページをご覧ください。

EventLogger の使用

EventLogger は、ロギング目的でライブラリによって直接提供される AnalyticsListener です。ExoPlayerEventLogger を追加して、1 行で有用な追加ロギングを有効にします。

Kotlin

player.addAnalyticsListener(EventLogger())

Java

player.addAnalyticsListener(new EventLogger());

詳細については、デバッグ ロギングのページをご覧ください。

指定した再生位置でイベントを発生させる

ユースケースによっては、指定した再生位置でイベントを発生させる必要があります。これは PlayerMessage を使用してサポートされています。PlayerMessage は、ExoPlayer.createMessage を使用して作成できます。実行する再生位置は 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();