플레이어 이벤트

재생 시작, 버퍼링 또는 오류와 같은 플레이어 상태 변경 은 등록된 Player.Listener 인스턴스로 전송되는 이벤트를 트리거합니다. 이러한 이벤트는 정수 상수로 표시되며 Player.EventPlayer.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 상태입니다.
  • playWhenReadytrue입니다.
  • 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 하위 클래스의 인스턴스를 전달합니다. 예를 들어 ExoPlayerExoPlaybackException를 전달합니다. 여기에는 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.
          }
        }
      }
    });

재생목록 전환

플레이어가 재생목록의 새 미디어 항목으로 변경될 때마다 onMediaItemTransition(MediaItem mediaItem, @MediaItemTransitionReason int reason)이 등록된 Player.Listener 객체에서 호출됩니다. 이유는 자동 전환인지, 탐색인지 (예: player.next()를 호출한 후), 동일한 항목의 반복인지, 재생목록 변경으로 인한 것인지(예: 현재 재생 중인 항목이 삭제된 경우)를 나타냅니다.

메타데이터

player.getCurrentMediaMetadata()에서 반환된 메타데이터는 재생목록 전환, 인스트림 메타데이터 업데이트 또는 재생 중 현재 MediaItem 업데이트와 같은 여러 가지 이유로 변경될 수 있습니다.

예를 들어 현재 제목을 표시하는 UI를 업데이트하기 위해 메타데이터 변경에 관심이 있다면 onMediaMetadataChanged를 수신 대기할 수 있습니다.

탐색 중입니다.

Player.seekTo 메서드를 호출하면 등록된 Player.Listener 인스턴스에 일련의 콜백이 발생합니다.

  1. reason=DISCONTINUITY_REASON_SEEK가 있는 onPositionDiscontinuity. 이는 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에 제공된 이유입니다.
  • 리스너는 콜백 매개변수를 통해 제공된 새 값에만 작동하거나 콜백 매개변수에 의존하지 않는 다른 항목을 트리거합니다.
  • 리스너 구현은 메서드 이름에서 이벤트를 트리거한 항목을 명확하고 읽기 쉬운 방식으로 표시하는 것을 선호합니다.
  • 리스너는 모든 개별 이벤트와 상태 변경을 알아야 하는 분석 시스템에 보고합니다.

다음과 같은 경우에는 일반 onEvents(Player player, Events events)를 사용하는 것이 좋습니다.

  • 리스너가 여러 이벤트에 동일한 로직을 트리거하려고 합니다. 예를 들어 onPlaybackStateChangedonPlayWhenReadyChanged 모두에 UI를 업데이트합니다.
  • 리스너는 추가 이벤트를 트리거하기 위해 Player 객체에 액세스해야 합니다(예: 미디어 항목 전환 후 탐색).
  • 리스너는 별도의 콜백을 통해 보고되는 여러 상태 값을 함께 또는 Player getter 메서드와 함께 사용하려고 합니다. 예를 들어 onTimelineChanged에 제공된 Timeline 과 함께 Player.getCurrentWindowIndex()를 사용하는 것은 onEvents 콜백 내에서만 안전합니다.
  • 리스너는 이벤트가 논리적으로 함께 발생했는지에 관심이 있습니다. 예를 들어 미디어 항목 전환으로 인해 onPlaybackStateChangedSTATE_BUFFERING으로 변경됩니다.

경우에 따라 리스너는 개별 콜백을 일반 onEvents 콜백과 결합해야 할 수 있습니다(예: onMediaItemTransition으로 미디어 항목 변경 이유를 기록하지만 모든 상태 변경을 onEvents에서 함께 사용할 수 있는 경우에만 작동).

코루틴을 사용하여 재생 이벤트 수신 대기

또는 Player.listenTo를 사용하여 Kotlin 코루틴을 실행하고 관련 Player.Event를 지정할 수 있습니다.

Player.listenPlayer.listenTo는 모든 스레드에서 호출할 수 있지만 콜백 람다는 항상 Player.getApplicationLooper와 연결된 스레드에서 호출됩니다. 따라서 코루틴이 다른 스레드에서 실행된 경우에도 콜백 람다 내에서 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.listenPlayer.listenTo 중에서 선택할 수 있습니다. 하지만 함수는 그 외에는 동일합니다.

듣기

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에 전달할 수 있습니다. 이러한 이벤트가 발생할 때마다 람다가 호출되며 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입니다. 한 줄로 유용한 추가 로깅을 사용 설정하려면 EventLoggerExoPlayer에 추가하세요.

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

자바

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