재생 시작, 버퍼링 또는 오류와 같은 플레이어 상태 변경
은 등록된 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. } } } });
재생목록 전환
플레이어가 재생목록의 새 미디어 항목으로 변경될 때마다
onMediaItemTransition(MediaItem mediaItem, @MediaItemTransitionReason int
reason)이 등록된 Player.Listener 객체에서 호출됩니다. 이유는 자동 전환인지, 탐색인지 (예: player.next()를 호출한 후), 동일한 항목의 반복인지, 재생목록 변경으로 인한 것인지(예: 현재 재생 중인 항목이 삭제된 경우)를 나타냅니다.
메타데이터
player.getCurrentMediaMetadata()에서 반환된 메타데이터는 재생목록 전환, 인스트림 메타데이터 업데이트 또는 재생 중 현재 MediaItem 업데이트와 같은 여러 가지 이유로 변경될 수 있습니다.
예를 들어 현재 제목을 표시하는 UI를 업데이트하기 위해 메타데이터 변경에 관심이 있다면 onMediaMetadataChanged를 수신 대기할 수 있습니다.
탐색 중입니다.
Player.seekTo 메서드를 호출하면 등록된 Player.Listener 인스턴스에 일련의 콜백이 발생합니다.
reason=DISCONTINUITY_REASON_SEEK가 있는onPositionDiscontinuity. 이는Player.seekTo를 호출한 직접적인 결과입니다. 콜백에는 탐색 전후의 위치에 관한PositionInfo필드가 있습니다.- 탐색과 관련된 즉각적인 상태 변경이 있는
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)를 사용하는 것이 좋습니다.
- 리스너가 여러 이벤트에 동일한 로직을 트리거하려고 합니다. 예를 들어
onPlaybackStateChanged와onPlayWhenReadyChanged모두에 UI를 업데이트합니다. - 리스너는 추가 이벤트를 트리거하기 위해
Player객체에 액세스해야 합니다(예: 미디어 항목 전환 후 탐색). - 리스너는 별도의 콜백을 통해 보고되는 여러 상태 값을 함께 또는
Playergetter 메서드와 함께 사용하려고 합니다. 예를 들어onTimelineChanged에 제공된Timeline과 함께Player.getCurrentWindowIndex()를 사용하는 것은onEvents콜백 내에서만 안전합니다. - 리스너는 이벤트가 논리적으로 함께 발생했는지에 관심이 있습니다.
예를 들어 미디어 항목 전환으로 인해
onPlaybackStateChanged가STATE_BUFFERING으로 변경됩니다.
경우에 따라 리스너는 개별 콜백을 일반 onEvents 콜백과 결합해야 할 수 있습니다(예: onMediaItemTransition으로 미디어 항목 변경 이유를 기록하지만 모든 상태 변경을 onEvents에서 함께 사용할 수 있는 경우에만 작동).
코루틴을 사용하여 재생 이벤트 수신 대기
또는 Player.listenTo를 사용하여 Kotlin 코루틴을 실행하고
관련 Player.Event를 지정할 수 있습니다.
Player.listen 및 Player.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.listen와 Player.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입니다. 한 줄로 유용한 추가 로깅을 사용 설정하려면 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()
자바
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();