MediaSession을 사용하여 재생 제어 및 광고

미디어 세션은 오디오 또는 동영상 플레이어와 상호작용할 보편적인 방법을 제공합니다. Media3에서 기본 플레이어는 Player 인터페이스를 구현하는 ExoPlayer 클래스입니다. 미디어 세션을 플레이어에 연결하면 앱이 미디어 재생을 외부에 광고하고 외부 소스에서 재생 명령을 수신할 수 있습니다.

명령어는 헤드셋이나 TV 리모컨의 재생 버튼과 같은 실제 버튼에서 시작될 수 있습니다. Google 어시스턴트에 '일시중지'를 지시하는 것과 같이 미디어 컨트롤러가 있는 클라이언트 앱에서 발생할 수도 있습니다. 미디어 세션은 이러한 명령어를 미디어 앱의 플레이어에 위임합니다.

미디어 세션을 선택해야 하는 경우

MediaSession를 구현하면 사용자가 재생을 제어할 수 있습니다.

  • 헤드폰을 통해 사용자가 헤드폰에서 미디어를 재생하거나 일시중지하거나 다음 트랙 또는 이전 트랙으로 이동하는 데 사용할 수 있는 버튼이나 터치 상호작용이 있는 경우가 많습니다.
  • Google 어시스턴트와 대화 일반적인 패턴은 "OK Google, 일시중지"라고 말하여 현재 기기에서 재생 중인 미디어를 일시중지하는 것입니다.
  • Wear OS 시계를 통해 이렇게 하면 휴대전화에서 재생하는 동안 가장 일반적인 재생 컨트롤에 더 쉽게 액세스할 수 있습니다.
  • 미디어 컨트롤을 통해 이 캐러셀에는 실행 중인 각 미디어 세션의 컨트롤이 표시됩니다.
  • TV 물리적 재생 버튼, 플랫폼 재생 컨트롤, 전원 관리를 사용한 작업을 허용합니다. 예를 들어 TV, 사운드바 또는 A/V 수신기가 꺼지거나 입력이 전환되면 앱에서 재생이 중지되어야 합니다.
  • 재생에 영향을 주어야 하는 다른 모든 외부 프로세스.

이는 다양한 사용 사례에 적합합니다. 특히 다음과 같은 경우에 MediaSession 사용을 적극 고려해야 합니다.

  • 영화나 라이브 TV와 같은 긴 형식의 동영상 콘텐츠를 스트리밍하는 경우
  • 팟캐스트 또는 음악 재생목록과 같은 긴 형식의 오디오 콘텐츠를 스트리밍합니다.
  • TV 앱을 빌드하고 있습니다.

하지만 모든 사용 사례가 MediaSession에 적합한 것은 아닙니다. 다음과 같은 경우에는 Player만 사용하는 것이 좋습니다.

  • 사용자 참여도와 상호작용이 중요한 짧은 형식의 콘텐츠를 게재하고 있습니다.
  • 사용자가 목록을 스크롤하고 여러 동영상이 동시에 화면에 표시되는 경우와 같이 활성 동영상이 하나도 없습니다.
  • 사용자가 적극적으로 시청할 것으로 예상되는 일회성 소개 또는 설명 동영상을 재생합니다.
  • 콘텐츠가 개인 정보 보호에 민감하며 외부 프로세스가 미디어 메타데이터에 액세스하지 못하게 하려는 경우 (예: 브라우저의 시크릿 모드)

사용 사례가 위에 나열된 사례 중 어느 것에도 해당하지 않는 경우 사용자가 콘텐츠에 적극적으로 참여하지 않더라도 앱에서 계속 재생해도 괜찮은지 고려하세요. 답이 '예'인 경우 MediaSession를 선택하는 것이 좋습니다. 답변이 '아니요'인 경우 대신 Player를 사용하는 것이 좋습니다.

미디어 세션 만들기

미디어 세션은 관리하는 플레이어와 함께 움직입니다. ContextPlayer 객체로 미디어 세션을 구성할 수 있습니다. 필요한 경우 미디어 세션을 만들고 초기화해야 합니다(예: Activity 또는 FragmentonStart() 또는 onResume() 수명 주기 메서드 또는 미디어 세션 및 관련 플레이어를 소유한 ServiceonCreate() 메서드).

미디어 세션을 만들려면 Player를 초기화하고 다음과 같이 MediaSession.Builder에 제공합니다.

Kotlin

val player = ExoPlayer.Builder(context).build()
val mediaSession = MediaSession.Builder(context, player).build()

자바

ExoPlayer player = new ExoPlayer.Builder(context).build();
MediaSession mediaSession = new MediaSession.Builder(context, player).build();

자동 상태 처리

Media3 라이브러리는 플레이어의 상태를 사용하여 미디어 세션을 자동으로 업데이트합니다. 따라서 플레이어에서 세션으로의 매핑을 수동으로 처리할 필요가 없습니다.

이는 오류를 표시하는 것과 같이 플레이어 자체와 별개로 PlaybackStateCompat를 만들고 유지해야 했던 기존 접근 방식과는 다릅니다.

고유한 세션 ID

기본적으로 MediaSession.Builder는 빈 문자열을 세션 ID로 사용하여 세션을 만듭니다. 앱이 단일 세션 인스턴스만 만들려고 하는 경우 이것으로 충분하며, 이는 가장 일반적인 경우입니다.

앱이 여러 세션 인스턴스를 동시에 관리하려는 경우 앱은 각 세션의 세션 ID가 고유한지 확인해야 합니다. 세션 ID는 MediaSession.Builder.setId(String id)로 세션을 빌드할 때 설정할 수 있습니다.

IllegalStateExceptionIllegalStateException: Session ID must be unique. ID= 오류 메시지와 함께 앱을 비정상 종료하는 경우 이전에 생성된 동일한 ID의 인스턴스가 해제되기 전에 세션이 예기치 않게 생성되었을 수 있습니다. 프로그래밍 오류로 인해 세션이 유출되지 않도록 하려면 예외를 발생시켜 이러한 사례를 감지하고 알립니다.

다른 클라이언트에 제어 권한 부여

미디어 세션은 재생을 제어하는 데 중요한 역할을 합니다. 이를 통해 외부 소스의 명령어를 미디어 재생을 실행하는 플레이어로 라우팅할 수 있습니다. 이러한 소스는 헤드셋이나 TV 리모컨의 재생 버튼과 같은 물리적 버튼이거나 Google 어시스턴트에 '일시중지'를 지시하는 간접 명령어일 수 있습니다. 마찬가지로 알림 및 잠금 화면 컨트롤을 용이하게 하기 위해 Android 시스템에 액세스 권한을 부여하거나 시계 화면에서 재생을 제어할 수 있도록 Wear OS 시계에 액세스 권한을 부여할 수 있습니다. 외부 클라이언트는 미디어 컨트롤러를 사용하여 미디어 앱에 재생 명령을 실행할 수 있습니다. 이러한 명령은 미디어 세션에서 수신하여 궁극적으로 미디어 플레이어에 위임합니다.

MediaSession과 MediaController 간의 상호작용을 보여주는 다이어그램
그림 1: 미디어 컨트롤러는 외부 소스에서 미디어 세션으로 명령어를 전달합니다.

컨트롤러가 미디어 세션에 연결하려고 할 때 onConnect() 메서드가 호출됩니다. 제공된 ControllerInfo를 사용하여 요청을 수락할지 거부할지 결정할 수 있습니다. 사용 가능한 명령어 선언 섹션에서 연결 요청을 수락하는 예를 참고하세요.

연결 후 컨트롤러는 세션으로 재생 명령어를 전송할 수 있습니다. 그런 다음 세션은 이러한 명령어를 플레이어에게 위임합니다. Player 인터페이스에 정의된 재생 및 재생목록 명령은 세션에서 자동으로 처리됩니다.

다른 콜백 메서드를 사용하면 맞춤 재생 명령어 요청 및 재생목록 수정을 처리할 수 있습니다. 이러한 콜백에도 ControllerInfo 객체가 포함되므로 컨트롤러별로 각 요청에 응답하는 방법을 수정할 수 있습니다.

재생목록 수정

미디어 세션은 재생목록용 ExoPlayer 가이드에 설명된 대로 재생목록의 재생목록을 직접 수정할 수 있습니다. 컨트롤러에 COMMAND_SET_MEDIA_ITEM 또는 COMMAND_CHANGE_MEDIA_ITEMS사용 가능한 경우 컨트롤러에서 재생목록을 수정할 수도 있습니다.

재생목록에 새 항목을 추가할 때 플레이어는 일반적으로 재생할 수 있도록 정의된 URI가 있는 MediaItem 인스턴스가 필요합니다. 기본적으로 새로 추가된 항목에는 URI가 정의된 경우 player.addMediaItem와 같은 플레이어 메서드로 자동으로 전달됩니다.

플레이어에 추가된 MediaItem 인스턴스를 맞춤설정하려면 onAddMediaItems()를 재정의하면 됩니다. 이 단계는 정의된 URI 없이 미디어를 요청하는 컨트롤러를 지원하려는 경우에 필요합니다. 대신 MediaItem에는 일반적으로 요청된 미디어를 설명하기 위해 설정된 다음 필드 중 하나 이상이 있습니다.

  • MediaItem.id: 미디어를 식별하는 일반 ID입니다.
  • MediaItem.RequestMetadata.mediaUri: 맞춤 스키마를 사용할 수 있으며 플레이어에서 직접 재생할 필요가 없는 요청 URI입니다.
  • MediaItem.RequestMetadata.searchQuery: 텍스트 검색어입니다(예: Google 어시스턴트의 검색어).
  • MediaItem.MediaMetadata: '제목' 또는 '아티스트'와 같은 구조화된 메타데이터입니다.

완전히 새로운 재생목록에 대한 맞춤설정 옵션을 더 보려면 재생목록의 시작 항목과 위치를 정의할 수 있는 onSetMediaItems()를 재정의하면 됩니다. 예를 들어 요청된 단일 항목을 전체 재생목록으로 확장하고 원래 요청된 항목의 색인에서 시작하도록 플레이어에게 지시할 수 있습니다. 이 기능이 포함된 onSetMediaItems()의 샘플 구현은 세션 데모 앱에서 확인할 수 있습니다.

맞춤 레이아웃 및 맞춤 명령 관리

다음 섹션에서는 맞춤 명령어 버튼의 맞춤 레이아웃을 클라이언트 앱에 광고하고 컨트롤러가 맞춤 명령어를 전송하도록 승인하는 방법을 설명합니다.

세션의 맞춤 레이아웃 정의

사용자에게 표시할 재생 컨트롤을 클라이언트 앱에 표시하려면 서비스의 onCreate() 메서드에서 MediaSession를 빌드할 때 세션의 맞춤 레이아웃을 설정하세요.

Kotlin

override fun onCreate() {
  super.onCreate()

  val likeButton = CommandButton.Builder()
    .setDisplayName("Like")
    .setIconResId(R.drawable.like_icon)
    .setSessionCommand(SessionCommand(SessionCommand.COMMAND_CODE_SESSION_SET_RATING))
    .build()
  val favoriteButton = CommandButton.Builder()
    .setDisplayName("Save to favorites")
    .setIconResId(R.drawable.favorite_icon)
    .setSessionCommand(SessionCommand(SAVE_TO_FAVORITES, Bundle()))
    .build()

  session =
    MediaSession.Builder(this, player)
      .setCallback(CustomMediaSessionCallback())
      .setCustomLayout(ImmutableList.of(likeButton, favoriteButton))
      .build()
}

자바

@Override
public void onCreate() {
  super.onCreate();

  CommandButton likeButton = new CommandButton.Builder()
    .setDisplayName("Like")
    .setIconResId(R.drawable.like_icon)
    .setSessionCommand(new SessionCommand(SessionCommand.COMMAND_CODE_SESSION_SET_RATING))
    .build();
  CommandButton favoriteButton = new CommandButton.Builder()
    .setDisplayName("Save to favorites")
    .setIconResId(R.drawable.favorite_icon)
    .setSessionCommand(new SessionCommand(SAVE_TO_FAVORITES, new Bundle()))
    .build();

  Player player = new ExoPlayer.Builder(this).build();
  mediaSession =
      new MediaSession.Builder(this, player)
          .setCallback(new CustomMediaSessionCallback())
          .setCustomLayout(ImmutableList.of(likeButton, favoriteButton))
          .build();
}

사용 가능한 플레이어 및 맞춤 명령어 선언

미디어 애플리케이션은 맞춤 레이아웃에 사용할 수 있는 맞춤 명령어를 정의할 수 있습니다. 예를 들어 사용자가 미디어 항목을 즐겨찾는 항목 목록에 저장할 수 있는 버튼을 구현할 수 있습니다. MediaController가 맞춤 명령어를 전송하고 MediaSession.Callback가 이를 수신합니다.

MediaController가 미디어 세션에 연결될 때 사용 가능한 맞춤 세션 명령어를 정의할 수 있습니다. 이렇게 하려면 MediaSession.Callback.onConnect()를 재정의하면 됩니다. onConnect 콜백 메서드에서 MediaController의 연결 요청을 수락할 때 사용할 수 있는 명령어 집합을 구성하고 반환합니다.

Kotlin

private inner class CustomMediaSessionCallback: MediaSession.Callback {
  // Configure commands available to the controller in onConnect()
  override fun onConnect(
    session: MediaSession,
    controller: MediaSession.ControllerInfo
  ): MediaSession.ConnectionResult {
    val sessionCommands = ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
        .add(SessionCommand(SAVE_TO_FAVORITES, Bundle.EMPTY))
        .build()
    return AcceptedResultBuilder(session)
        .setAvailableSessionCommands(sessionCommands)
        .build()
  }
}

자바

class CustomMediaSessionCallback implements MediaSession.Callback {
  // Configure commands available to the controller in onConnect()
  @Override
  public ConnectionResult onConnect(
    MediaSession session,
    ControllerInfo controller) {
    SessionCommands sessionCommands =
        ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
            .add(new SessionCommand(SAVE_TO_FAVORITES, new Bundle()))
            .build();
    return new AcceptedResultBuilder(session)
        .setAvailableSessionCommands(sessionCommands)
        .build();
  }
}

MediaController에서 맞춤 명령어 요청을 수신하려면 Callback에서 onCustomCommand() 메서드를 재정의합니다.

Kotlin

private inner class CustomMediaSessionCallback: MediaSession.Callback {
  ...
  override fun onCustomCommand(
    session: MediaSession,
    controller: MediaSession.ControllerInfo,
    customCommand: SessionCommand,
    args: Bundle
  ): ListenableFuture<SessionResult> {
    if (customCommand.customAction == SAVE_TO_FAVORITES) {
      // Do custom logic here
      saveToFavorites(session.player.currentMediaItem)
      return Futures.immediateFuture(
        SessionResult(SessionResult.RESULT_SUCCESS)
      )
    }
    ...
  }
}

자바

class CustomMediaSessionCallback implements MediaSession.Callback {
  ...
  @Override
  public ListenableFuture<SessionResult> onCustomCommand(
    MediaSession session, 
    ControllerInfo controller,
    SessionCommand customCommand,
    Bundle args
  ) {
    if(customCommand.customAction.equals(SAVE_TO_FAVORITES)) {
      // Do custom logic here
      saveToFavorites(session.getPlayer().getCurrentMediaItem());
      return Futures.immediateFuture(
        new SessionResult(SessionResult.RESULT_SUCCESS)
      );
    }
    ...
  }
}

Callback 메서드에 전달되는 MediaSession.ControllerInfo 객체의 packageName 속성을 사용하여 요청하는 미디어 컨트롤러를 추적할 수 있습니다. 이를 통해 시스템, 자체 앱 또는 기타 클라이언트 앱에서 발생한 지정된 명령에 대한 응답으로 앱의 동작을 조정할 수 있습니다.

사용자 상호작용 후 맞춤 레이아웃 업데이트

맞춤 명령어 또는 플레이어와의 다른 상호작용을 처리한 후에는 컨트롤러 UI에 표시되는 레이아웃을 업데이트하는 것이 좋습니다. 일반적인 예는 이 버튼과 연결된 작업을 트리거한 후 아이콘을 변경하는 전환 버튼입니다. 레이아웃을 업데이트하려면 MediaSession.setCustomLayout를 사용합니다.

Kotlin

val removeFromFavoritesButton = CommandButton.Builder()
  .setDisplayName("Remove from favorites")
  .setIconResId(R.drawable.favorite_remove_icon)
  .setSessionCommand(SessionCommand(REMOVE_FROM_FAVORITES, Bundle()))
  .build()
mediaSession.setCustomLayout(ImmutableList.of(likeButton, removeFromFavoritesButton))

자바

CommandButton removeFromFavoritesButton = new CommandButton.Builder()
  .setDisplayName("Remove from favorites")
  .setIconResId(R.drawable.favorite_remove_icon)
  .setSessionCommand(new SessionCommand(REMOVE_FROM_FAVORITES, new Bundle()))
  .build();
mediaSession.setCustomLayout(ImmutableList.of(likeButton, removeFromFavoritesButton));

재생 명령어 동작 맞춤설정

Player 인터페이스에 정의된 명령어(예: play() 또는 seekToNext())의 동작을 맞춤설정하려면 PlayerForwardingPlayer로 래핑합니다.

Kotlin

val player = ExoPlayer.Builder(context).build()

val forwardingPlayer = object : ForwardingPlayer(player) {
  override fun play() {
    // Add custom logic
    super.play()
  }

  override fun setPlayWhenReady(playWhenReady: Boolean) {
    // Add custom logic
    super.setPlayWhenReady(playWhenReady)
  }
}

val mediaSession = MediaSession.Builder(context, forwardingPlayer).build()

자바

ExoPlayer player = new ExoPlayer.Builder(context).build();

ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player) {
  @Override
  public void play() {
    // Add custom logic
    super.play();
  }

  @Override
  public void setPlayWhenReady(boolean playWhenReady) {
    // Add custom logic
    super.setPlayWhenReady(playWhenReady);
  }
};

MediaSession mediaSession =
  new MediaSession.Builder(context, forwardingPlayer).build();

ForwardingPlayer에 관한 자세한 내용은 맞춤설정에 관한 ExoPlayer 가이드를 참고하세요.

플레이어 명령어를 요청하는 컨트롤러 식별

Player 메서드 호출이 MediaController에 의해 발생한 경우 MediaSession.controllerForCurrentRequest로 출처를 식별하고 현재 요청의 ControllerInfo를 획득할 수 있습니다.

Kotlin

class CallerAwareForwardingPlayer(player: Player) :
  ForwardingPlayer(player) {

  override fun seekToNext() {
    Log.d(
      "caller",
      "seekToNext called from package ${session.controllerForCurrentRequest?.packageName}"
    )
    super.seekToNext()
  }
}

자바

public class CallerAwareForwardingPlayer extends ForwardingPlayer {
  public CallerAwareForwardingPlayer(Player player) {
    super(player);
  }

  @Override
  public void seekToNext() {
    Log.d(
        "caller",
        "seekToNext called from package: "
            + session.getControllerForCurrentRequest().getPackageName());
    super.seekToNext();
  }
}

미디어 버튼에 응답

미디어 버튼은 블루투스 헤드셋의 재생/일시중지 버튼과 같이 Android 기기 및 기타 주변기기에서 볼 수 있는 하드웨어 버튼입니다. Media3은 미디어 버튼 이벤트가 세션에 도착할 때 자동으로 미디어 버튼 이벤트를 처리하고 세션 플레이어에서 적절한 Player 메서드를 호출합니다.

앱은 MediaSession.Callback.onMediaButtonEvent(Intent)를 재정의하여 기본 동작을 재정의할 수 있습니다. 이 경우 앱은 모든 API 사양을 자체적으로 처리할 수 있거나 처리해야 합니다.

오류 처리 및 보고

세션에서 발생되어 컨트롤러에 보고하는 두 가지 유형의 오류가 있습니다. 심각한 오류는 재생을 중단하는 세션 플레이어의 기술적 재생 실패를 보고합니다. 심각한 오류는 발생 시 컨트롤러에 자동으로 보고됩니다. 심각하지 않은 오류는 재생을 중단하지 않으며 애플리케이션에서 수동으로 컨트롤러로 전송되는 비기술적 또는 정책 오류입니다.

치명적인 재생 오류

심각한 재생 오류는 플레이어에 의해 세션에 보고된 후 Player.Listener.onPlayerError(PlaybackException)Player.Listener.onPlayerErrorChanged(@Nullable PlaybackException)를 통해 호출하도록 컨트롤러에 보고됩니다.

이 경우 재생 상태가 STATE_IDLE로 전환되고 MediaController.getPlaybackError()는 전환을 일으킨 PlaybackException를 반환합니다. 컨트롤러는 PlayerException.errorCode를 검사하여 오류의 원인에 관한 정보를 가져올 수 있습니다.

상호 운용성을 위해 심각한 오류는 상태를 STATE_ERROR로 전환하고 PlaybackException에 따라 오류 코드와 메시지를 설정하여 플랫폼 세션의 PlaybackStateCompat에 복제됩니다.

심각한 오류 맞춤설정

사용자에게 현지화되고 의미 있는 정보를 제공하기 위해 치명적인 재생 오류의 오류 코드, 오류 메시지, 오류 추가 항목은 세션을 빌드할 때 ForwardingPlayer를 사용하여 맞춤설정할 수 있습니다.

Kotlin

val forwardingPlayer = ErrorForwardingPlayer(player)
val session = MediaSession.Builder(context, forwardingPlayer).build()

자바

Player forwardingPlayer = new ErrorForwardingPlayer(player);
MediaSession session =
    new MediaSession.Builder(context, forwardingPlayer).build();

전달 플레이어는 실제 플레이어에 Player.Listener를 등록하고 오류를 보고하는 콜백을 가로챕니다. 그러면 맞춤설정된 PlaybackException가 전달 플레이어에 등록된 리스너에게 위임됩니다. 이렇게 하려면 전달 플레이어가 맞춤설정된 오류 코드, 메시지 또는 추가 항목을 전송하는 리스너에 액세스할 수 있도록 Player.addListenerPlayer.removeListener를 재정의합니다.

Kotlin

class ErrorForwardingPlayer(private val context: Context, player: Player) :
  ForwardingPlayer(player) {

  private val listeners: MutableList<Player.Listener> = mutableListOf()

  private var customizedPlaybackException: PlaybackException? = null

  init {
    player.addListener(ErrorCustomizationListener())
  }

  override fun addListener(listener: Player.Listener) {
    listeners.add(listener)
  }

  override fun removeListener(listener: Player.Listener) {
    listeners.remove(listener)
  }

  override fun getPlayerError(): PlaybackException? {
    return customizedPlaybackException
  }

  private inner class ErrorCustomizationListener : Player.Listener {

    override fun onPlayerErrorChanged(error: PlaybackException?) {
      customizedPlaybackException = error?.let { customizePlaybackException(it) }
      listeners.forEach { it.onPlayerErrorChanged(customizedPlaybackException) }
    }

    override fun onPlayerError(error: PlaybackException) {
      listeners.forEach { it.onPlayerError(customizedPlaybackException!!) }
    }

    private fun customizePlaybackException(
      error: PlaybackException,
    ): PlaybackException {
      val buttonLabel: String
      val errorMessage: String
      when (error.errorCode) {
        PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW -> {
          buttonLabel = context.getString(R.string.err_button_label_restart_stream)
          errorMessage = context.getString(R.string.err_msg_behind_live_window)
        }
        // Apps can customize further error messages by adding more branches.
        else -> {
          buttonLabel = context.getString(R.string.err_button_label_ok)
          errorMessage = context.getString(R.string.err_message_default)
        }
      }
      val extras = Bundle()
      extras.putString("button_label", buttonLabel)
      return PlaybackException(errorMessage, error.cause, error.errorCode, extras)
    }

    override fun onEvents(player: Player, events: Player.Events) {
      listeners.forEach {
        it.onEvents(player, events)
      }
    }
    // Delegate all other callbacks to all listeners without changing arguments like onEvents.
  }
}

자바

private static class ErrorForwardingPlayer extends ForwardingPlayer {

  private final Context context;
  private List<Player.Listener> listeners;
  @Nullable private PlaybackException customizedPlaybackException;

  public ErrorForwardingPlayer(Context context, Player player) {
    super(player);
    this.context = context;
    listeners = new ArrayList<>();
    player.addListener(new ErrorCustomizationListener());
  }

  @Override
  public void addListener(Player.Listener listener) {
    listeners.add(listener);
  }

  @Override
  public void removeListener(Player.Listener listener) {
    listeners.remove(listener);
  }

  @Nullable
  @Override
  public PlaybackException getPlayerError() {
    return customizedPlaybackException;
  }

  private class ErrorCustomizationListener implements Listener {

    @Override
    public void onPlayerErrorChanged(@Nullable PlaybackException error) {
      customizedPlaybackException =
          error != null ? customizePlaybackException(error, context) : null;
      for (int i = 0; i < listeners.size(); i++) {
        listeners.get(i).onPlayerErrorChanged(customizedPlaybackException);
      }
    }

    @Override
    public void onPlayerError(PlaybackException error) {
      for (int i = 0; i < listeners.size(); i++) {
        listeners.get(i).onPlayerError(checkNotNull(customizedPlaybackException));
      }
    }

    private PlaybackException customizePlaybackException(
        PlaybackException error, Context context) {
      String buttonLabel;
      String errorMessage;
      switch (error.errorCode) {
        case PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW:
          buttonLabel = context.getString(R.string.err_button_label_restart_stream);
          errorMessage = context.getString(R.string.err_msg_behind_live_window);
          break;
        // Apps can customize further error messages by adding more case statements.
        default:
          buttonLabel = context.getString(R.string.err_button_label_ok);
          errorMessage = context.getString(R.string.err_message_default);
          break;
      }
      Bundle extras = new Bundle();
      extras.putString("button_label", buttonLabel);
      return new PlaybackException(errorMessage, error.getCause(), error.errorCode, extras);
    }

    @Override
    public void onEvents(Player player, Events events) {
      for (int i = 0; i < listeners.size(); i++) {
        listeners.get(i).onEvents(player, events);
      }
    }
    // Delegate all other callbacks to all listeners without changing arguments like onEvents.
  }
}

비심각한 오류

기술 예외에서 비롯되지 않은 치명적이지 않은 오류는 앱에서 전체 컨트롤러 또는 특정 컨트롤러로 전송할 수 있습니다.

Kotlin

val sessionError = SessionError(
  SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED,
  context.getString(R.string.error_message_authentication_expired),
)

// Sending a nonfatal error to all controllers.
mediaSession.sendError(sessionError)

// Interoperability: Sending a nonfatal error to the media notification controller to set the
// error code and error message in the playback state of the platform media session.
mediaSession.mediaNotificationControllerInfo?.let {
  mediaSession.sendError(it, sessionError)
}

자바

SessionError sessionError = new SessionError(
    SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED,
    context.getString(R.string.error_message_authentication_expired));

// Sending a nonfatal error to all controllers.
mediaSession.sendError(sessionError);

// Interoperability: Sending a nonfatal error to the media notification controller to set the
// error code and error message in the playback state of the platform media session.
ControllerInfo mediaNotificationControllerInfo =
    mediaSession.getMediaNotificationControllerInfo();
if (mediaNotificationControllerInfo != null) {
  mediaSession.sendError(mediaNotificationControllerInfo, sessionError);
}

미디어 알림 컨트롤러에 전송된 치명적이지 않은 오류는 플랫폼 세션의 PlaybackStateCompat에 복제됩니다. 따라서 오류 코드와 오류 메시지만 PlaybackStateCompat로 설정되고 PlaybackStateCompat.stateSTATE_ERROR로 변경되지 않습니다.

심각하지 않은 오류 수신

MediaControllerMediaController.Listener.onError를 구현하여 심각하지 않은 오류를 수신합니다.

Kotlin

val future = MediaController.Builder(context, sessionToken)
  .setListener(object : MediaController.Listener {
    override fun onError(controller: MediaController, sessionError: SessionError) {
      // Handle nonfatal error.
    }
  })
  .buildAsync()

Java

MediaController.Builder future =
    new MediaController.Builder(context, sessionToken)
        .setListener(
            new MediaController.Listener() {
              @Override
              public void onError(MediaController controller, SessionError sessionError) {
                // Handle nonfatal error.
              }
            });