출력 전환기

출력 전환 도구는 Android 13부터 콘텐츠의 로컬 재생과 원격 재생 간에 원활하게 전환할 수 있는 Cast SDK의 기능입니다. 이를 통해 발신자 앱이 콘텐츠 재생 위치를 쉽고 빠르게 제어할 수 있습니다. Output Switcher는 MediaRouter 라이브러리를 사용하여 휴대전화 스피커, 페어링된 블루투스 기기, 원격 Cast 지원 기기 간에 콘텐츠 재생을 전환합니다. 사용 사례는 다음과 같은 시나리오로 분류할 수 있습니다.

CastVideos-android 샘플 앱을 다운로드하여 앱에서 출력 전환 도구를 구현하는 방법을 참고하세요.

이 가이드에 설명된 단계에 따라 로컬-원격, 원격-로컬, 원격-원격을 지원하도록 출력 전환 도구를 사용 설정해야 합니다. 로컬 기기 스피커와 페어링된 블루투스 기기 간의 전송을 지원하는 데 필요한 추가 단계는 없습니다.

출력 전환기 UI

출력 전환 도구에는 사용 가능한 로컬 및 원격 기기와 기기가 선택되었는지, 연결 중인지, 현재 볼륨 수준 등 현재 기기 상태가 표시됩니다. 현재 기기 외에 다른 기기가 있는 경우 다른 기기를 클릭하면 미디어 재생을 선택한 기기로 전송할 수 있습니다.

알려진 문제

  • 로컬 재생을 위해 생성된 미디어 세션은 Cast SDK 알림으로 전환할 때 닫히고 다시 생성됩니다.

진입점

미디어 알림

앱이 로컬 재생 (로컬에서 재생)을 위해 MediaSession와 함께 미디어 알림을 게시하면 미디어 알림의 오른쪽 상단에 콘텐츠가 현재 재생 중인 기기 이름 (예: 휴대전화 스피커)이 포함된 알림 칩이 표시됩니다. 알림 칩을 탭하면 출력 전환 도구 대화상자 시스템 UI가 열립니다.

볼륨 설정

출력 전환 도구 대화상자 시스템 UI는 기기의 물리적 볼륨 버튼을 클릭하고 하단의 설정 아이콘을 탭한 다음 '<전송 기기>에서 <앱 이름> 재생' 텍스트를 탭하여 트리거할 수도 있습니다.

단계 요약

기본 요건

  1. 기존 Android 앱을 AndroidX로 이전합니다.
  2. 출력 전환 도구에 필요한 최소 Android Sender SDK 버전을 사용하도록 앱의 build.gradle를 업데이트합니다.
    dependencies {
      ...
      implementation 'com.google.android.gms:play-services-cast-framework:21.2.0'
      ...
    }
  3. 앱이 미디어 알림을 지원합니다.
  4. Android 13을 실행하는 기기

미디어 알림 설정

출력 전환 도구를 사용하려면 오디오동영상 앱에서 로컬 재생을 위한 미디어의 재생 상태 및 컨트롤을 표시하는 미디어 알림을 만들어야 합니다. 이렇게 하려면 MediaSession를 만들고, MediaSession의 토큰으로 MediaStyle를 설정하고, 알림에 미디어 컨트롤을 설정해야 합니다.

현재 MediaStyleMediaSession를 사용하지 않는 경우 아래 스니펫에 설정 방법이 나와 있으며 오디오동영상 앱의 미디어 세션 콜백을 설정하는 방법에 관한 가이드를 참고하세요.

Kotlin
// Create a media session. NotificationCompat.MediaStyle
// PlayerService is your own Service or Activity responsible for media playback.
val mediaSession = MediaSessionCompat(this, "PlayerService")

// Create a MediaStyle object and supply your media session token to it.
val mediaStyle = Notification.MediaStyle().setMediaSession(mediaSession.sessionToken)

// Create a Notification which is styled by your MediaStyle object.
// This connects your media session to the media controls.
// Don't forget to include a small icon.
val notification = Notification.Builder(this@PlayerService, CHANNEL_ID)
    .setStyle(mediaStyle)
    .setSmallIcon(R.drawable.ic_app_logo)
    .build()

// Specify any actions which your users can perform, such as pausing and skipping to the next track.
val pauseAction: Notification.Action = Notification.Action.Builder(
        pauseIcon, "Pause", pauseIntent
    ).build()
notification.addAction(pauseAction)
Java
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
    // Create a media session. NotificationCompat.MediaStyle
    // PlayerService is your own Service or Activity responsible for media playback.
    MediaSession mediaSession = new MediaSession(this, "PlayerService");

    // Create a MediaStyle object and supply your media session token to it.
    Notification.MediaStyle mediaStyle = new Notification.MediaStyle().setMediaSession(mediaSession.getSessionToken());

    // Specify any actions which your users can perform, such as pausing and skipping to the next track.
    Notification.Action pauseAction = Notification.Action.Builder(pauseIcon, "Pause", pauseIntent).build();

    // Create a Notification which is styled by your MediaStyle object.
    // This connects your media session to the media controls.
    // Don't forget to include a small icon.
    String CHANNEL_ID = "CHANNEL_ID";
    Notification notification = new Notification.Builder(this, CHANNEL_ID)
        .setStyle(mediaStyle)
        .setSmallIcon(R.drawable.ic_app_logo)
        .addAction(pauseAction)
        .build();
}

또한 미디어 정보로 알림을 채우려면 미디어의 메타데이터 및 재생 상태MediaSession에 추가해야 합니다.

MediaSession에 메타데이터를 추가하려면 setMetaData()를 사용하고 MediaMetadataCompat.Builder()에서 미디어와 관련된 모든 MediaMetadata 상수를 제공합니다.

Kotlin
mediaSession.setMetadata(MediaMetadataCompat.Builder()
    // Title
    .putString(MediaMetadata.METADATA_KEY_TITLE, currentTrack.title)

    // Artist
    // Could also be the channel name or TV series.
    .putString(MediaMetadata.METADATA_KEY_ARTIST, currentTrack.artist)

    // Album art
    // Could also be a screenshot or hero image for video content
    // The URI scheme needs to be "content", "file", or "android.resource".
    .putString(
        MediaMetadata.METADATA_KEY_ALBUM_ART_URI, currentTrack.albumArtUri)
    )

    // Duration
    // If duration isn't set, such as for live broadcasts, then the progress
    // indicator won't be shown on the seekbar.
    .putLong(MediaMetadata.METADATA_KEY_DURATION, currentTrack.duration)

    .build()
)
Java
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
    mediaSession.setMetadata(
        new MediaMetadataCompat.Builder()
        // Title
        .putString(MediaMetadata.METADATA_KEY_TITLE, currentTrack.title)

        // Artist
        // Could also be the channel name or TV series.
        .putString(MediaMetadata.METADATA_KEY_ARTIST, currentTrack.artist)

        // Album art
        // Could also be a screenshot or hero image for video content
        // The URI scheme needs to be "content", "file", or "android.resource".
        .putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, currentTrack.albumArtUri)

        // Duration
        // If duration isn't set, such as for live broadcasts, then the progress
        // indicator won't be shown on the seekbar.
        .putLong(MediaMetadata.METADATA_KEY_DURATION, currentTrack.duration)

        .build()
    );
}

MediaSession에 재생 상태를 추가하려면 setPlaybackState()를 사용하고 PlaybackStateCompat.Builder()에서 미디어와 관련된 모든 PlaybackStateCompat 상수를 제공합니다.

Kotlin
mediaSession.setPlaybackState(
    PlaybackStateCompat.Builder()
        .setState(
            PlaybackStateCompat.STATE_PLAYING,

            // Playback position
            // Used to update the elapsed time and the progress bar.
            mediaPlayer.currentPosition.toLong(),

            // Playback speed
            // Determines the rate at which the elapsed time changes.
            playbackSpeed
        )

        // isSeekable
        // Adding the SEEK_TO action indicates that seeking is supported
        // and makes the seekbar position marker draggable. If this is not
        // supplied seek will be disabled but progress will still be shown.
        .setActions(PlaybackStateCompat.ACTION_SEEK_TO)
        .build()
)
Java
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
    mediaSession.setPlaybackState(
        new PlaybackStateCompat.Builder()
            .setState(
                 PlaybackStateCompat.STATE_PLAYING,

                // Playback position
                // Used to update the elapsed time and the progress bar.
                mediaPlayer.currentPosition.toLong(),

                // Playback speed
                // Determines the rate at which the elapsed time changes.
                playbackSpeed
            )

        // isSeekable
        // Adding the SEEK_TO action indicates that seeking is supported
        // and makes the seekbar position marker draggable. If this is not
        // supplied seek will be disabled but progress will still be shown.
        .setActions(PlaybackStateCompat.ACTION_SEEK_TO)
        .build()
    );
}

동영상 앱 알림 동작

백그라운드에서 로컬 재생을 지원하지 않는 동영상 앱 또는 오디오 앱은 재생이 지원되지 않는 상황에서 미디어 명령을 전송하는 문제 방지를 위해 미디어 알림에 관한 특정 동작을 갖추어야 합니다.

  • 로컬에서 미디어를 재생하고 앱이 포그라운드에 있을 때 미디어 알림을 게시합니다.
  • 앱이 백그라운드에 있을 때 로컬 재생을 일시중지하고 알림을 닫습니다.
  • 앱이 포그라운드로 다시 이동하면 로컬 재생이 재개되고 알림이 다시 게시되어야 합니다.

AndroidManifest.xml에서 출력 전환 도구 사용 설정

출력 전환기를 사용 설정하려면 MediaTransferReceiver를 앱의 AndroidManifest.xml에 추가해야 합니다. 그렇지 않으면 기능이 사용 설정되지 않으며 원격-로컬 기능 플래그도 유효하지 않습니다.

<application>
    ...
    <receiver
         android:name="androidx.mediarouter.media.MediaTransferReceiver"
         android:exported="true">
    </receiver>
    ...
</application>

MediaTransferReceiver는 시스템 UI가 있는 기기 간에 미디어 전송을 지원하는 broadcast receiver입니다. 자세한 내용은 MediaTransferReceiver 참조를 확인하세요.

로컬-원격

사용자가 재생을 로컬에서 원격으로 전환하면 Cast SDK가 전송 세션을 자동으로 시작합니다. 하지만 앱은 로컬에서 원격으로 전환하는 작업을 처리해야 합니다(예: 로컬 재생을 중지하고 Cast 기기에 미디어를 로드). 앱은 onSessionStarted()onSessionEnded() 콜백을 사용하여 Cast SessionManagerListener를 수신 대기하고 Cast SessionManager 콜백을 수신할 때 작업을 처리해야 합니다. 앱은 출력 전환 도구 대화상자가 열려 있고 앱이 포그라운드에 있지 않을 때 이러한 콜백이 여전히 활성 상태인지 확인해야 합니다.

백그라운드 전송을 위한 SessionManagerListener 업데이트

기존 Cast 환경은 앱이 포그라운드에 있을 때 이미 로컬에서 원격으로 전송하는 기능을 지원합니다. 일반적인 전송 환경은 사용자가 앱에서 전송 아이콘을 클릭하고 미디어를 스트리밍할 기기를 선택하면 시작됩니다. 이 경우 앱은 onCreate() 또는 onStart()SessionManagerListener에 등록하고 앱 활동의 onStop() 또는 onDestroy()에서 리스너를 등록 취소해야 합니다.

출력 전환 도구를 사용한 새로운 전송 환경을 통해 앱은 백그라운드에 있을 때 전송을 시작할 수 있습니다. 이는 백그라운드에서 재생할 때 알림을 게시하는 오디오 앱에 특히 유용합니다. 앱은 서비스의 onCreate()SessionManager 리스너를 등록하고 서비스의 onDestroy()에서 등록을 해제할 수 있습니다. 앱이 백그라운드에 있을 때 앱은 항상 로컬-원격 콜백 (예: onSessionStarted)을 수신해야 합니다.

앱이 MediaBrowserService를 사용하는 경우 SessionManagerListener를 거기에 등록하는 것이 좋습니다.

Kotlin
class MyService : Service() {
    private var castContext: CastContext? = null
    protected fun onCreate() {
        castContext = CastContext.getSharedInstance(this)
        castContext
            .getSessionManager()
            .addSessionManagerListener(sessionManagerListener, CastSession::class.java)
    }

    protected fun onDestroy() {
        if (castContext != null) {
            castContext
                .getSessionManager()
                .removeSessionManagerListener(sessionManagerListener, CastSession::class.java)
        }
    }
}
Java
public class MyService extends Service {
  private CastContext castContext;

  @Override
  protected void onCreate() {
     castContext = CastContext.getSharedInstance(this);
     castContext
        .getSessionManager()
        .addSessionManagerListener(sessionManagerListener, CastSession.class);
  }

  @Override
  protected void onDestroy() {
    if (castContext != null) {
       castContext
          .getSessionManager()
          .removeSessionManagerListener(sessionManagerListener, CastSession.class);
    }
  }
}

이 업데이트를 통해 로컬-원격은 앱이 백그라운드에 있을 때 기존 전송과 동일하게 작동하며 블루투스 기기에서 Cast 기기로 전환하는 데 추가 작업이 필요하지 않습니다.

원격-로컬

출력 전환 도구는 원격 재생에서 휴대전화 스피커 또는 로컬 블루투스 기기로 전송하는 기능을 제공합니다. CastOptions에서 setRemoteToLocalEnabled 플래그를 true로 설정하면 이를 사용 설정할 수 있습니다.

현재 송신기 기기가 여러 송신기가 있는 기존 세션에 참여하고 앱에서 현재 미디어를 로컬로 전송할 수 있는지 확인해야 하는 경우 앱은 SessionTransferCallbackonTransferred 콜백을 사용하여 SessionState를 확인해야 합니다.

setRemoteToLocalEnabled 플래그 설정

CastOptions.Builder는 활성 Cast 세션이 있을 때 출력 전환기 대화상자에 휴대전화 스피커와 로컬 블루투스 기기를 전송 대상으로 표시하거나 숨기는 setRemoteToLocalEnabled를 제공합니다.

Kotlin
class CastOptionsProvider : OptionsProvider {
    fun getCastOptions(context: Context?): CastOptions {
        ...
        return Builder()
            ...
            .setRemoteToLocalEnabled(true)
            .build()
    }
}
Java
public class CastOptionsProvider implements OptionsProvider {
    @Override
    public CastOptions getCastOptions(Context context) {
        ...
        return new CastOptions.Builder()
            ...
            .setRemoteToLocalEnabled(true)
            .build()
  }
}

로컬에서 계속 재생

원격-로컬을 지원하는 앱은 미디어를 전송하고 로컬에서 재생을 계속할 수 있는지 확인할 수 있도록 이벤트가 발생할 때 알림을 받기 위해 SessionTransferCallback를 등록해야 합니다.

CastContext#addSessionTransferCallback(SessionTransferCallback)를 사용하면 앱이 SessionTransferCallback를 등록하고 발신자가 로컬 재생으로 전송될 때 onTransferredonTransferFailed 콜백을 수신 대기할 수 있습니다.

앱이 SessionTransferCallback를 등록 해제하면 앱은 더 이상 SessionTransferCallback를 수신하지 않습니다.

SessionTransferCallback는 기존 SessionManagerListener 콜백의 확장이며 onSessionEnded이 트리거된 후에 트리거됩니다. 원격-로컬 콜백의 순서는 다음과 같습니다.

  1. onTransferring
  2. onSessionEnding
  3. onSessionEnded
  4. onTransferred

앱이 백그라운드에서 전송 중일 때 미디어 알림 칩에서 출력 전환 도구를 열 수 있으므로 앱은 백그라운드 재생을 지원하는지 여부에 따라 로컬로의 전송을 다르게 처리해야 합니다. 전송에 실패하면 오류가 발생할 때마다 onTransferFailed이 실행됩니다.

백그라운드 재생을 지원하는 앱

백그라운드 재생을 지원하는 앱 (일반적으로 오디오 앱)의 경우 Service (예: MediaBrowserService)를 사용하는 것이 좋습니다. 서비스는 onTransferred 콜백을 수신 대기하고 앱이 포그라운드 또는 백그라운드에 있을 때 모두 로컬에서 재생을 재개해야 합니다.

Kotlin
class MyService : Service() {
    private var castContext: CastContext? = null
    private var sessionTransferCallback: SessionTransferCallback? = null
    protected fun onCreate() {
        castContext = CastContext.getSharedInstance(this)
        castContext.getSessionManager()
                   .addSessionManagerListener(sessionManagerListener, CastSession::class.java)
        sessionTransferCallback = MySessionTransferCallback()
        castContext.addSessionTransferCallback(sessionTransferCallback)
    }

    protected fun onDestroy() {
        if (castContext != null) {
            castContext.getSessionManager()
                       .removeSessionManagerListener(sessionManagerListener, CastSession::class.java)
            if (sessionTransferCallback != null) {
                castContext.removeSessionTransferCallback(sessionTransferCallback)
            }
        }
    }

    class MySessionTransferCallback : SessionTransferCallback() {
        fun onTransferring(@SessionTransferCallback.TransferType transferType: Int) {
            // Perform necessary steps prior to onTransferred
        }

        fun onTransferred(@SessionTransferCallback.TransferType transferType: Int,
                          sessionState: SessionState?) {
            if (transferType == SessionTransferCallback.TRANSFER_TYPE_FROM_REMOTE_TO_LOCAL) {
                // Remote stream is transferred to the local device.
                // Retrieve information from the SessionState to continue playback on the local player.
            }
        }

        fun onTransferFailed(@SessionTransferCallback.TransferType transferType: Int,
                             @SessionTransferCallback.TransferFailedReason transferFailedReason: Int) {
            // Handle transfer failure.
        }
    }
}
Java
public class MyService extends Service {
    private CastContext castContext;
    private SessionTransferCallback sessionTransferCallback;

    @Override
    protected void onCreate() {
        castContext = CastContext.getSharedInstance(this);
        castContext.getSessionManager()
                   .addSessionManagerListener(sessionManagerListener, CastSession.class);
        sessionTransferCallback = new MySessionTransferCallback();
        castContext.addSessionTransferCallback(sessionTransferCallback);
    }

    @Override
    protected void onDestroy() {
        if (castContext != null) {
            castContext.getSessionManager()
                       .removeSessionManagerListener(sessionManagerListener, CastSession.class);
            if (sessionTransferCallback != null) {
                castContext.removeSessionTransferCallback(sessionTransferCallback);
            }
        }
    }

    public static class MySessionTransferCallback extends SessionTransferCallback {
        public MySessionTransferCallback() {}

        @Override
        public void onTransferring(@SessionTransferCallback.TransferType int transferType) {
            // Perform necessary steps prior to onTransferred
        }

        @Override
        public void onTransferred(@SessionTransferCallback.TransferType int transferType,
                                  SessionState sessionState) {
            if (transferType==SessionTransferCallback.TRANSFER_TYPE_FROM_REMOTE_TO_LOCAL) {
                // Remote stream is transferred to the local device.
                // Retrieve information from the SessionState to continue playback on the local player.
            }
        }

        @Override
        public void onTransferFailed(@SessionTransferCallback.TransferType int transferType,
                                     @SessionTransferCallback.TransferFailedReason int transferFailedReason) {
            // Handle transfer failure.
        }
    }
}

백그라운드 재생을 지원하지 않는 앱

백그라운드 재생을 지원하지 않는 앱 (일반적으로 동영상 앱)의 경우 onTransferred 콜백을 수신 대기하고 앱이 포그라운드에 있으면 로컬에서 재생을 재개하는 것이 좋습니다.

앱이 백그라운드에 있으면 재생을 일시중지하고 SessionState의 필요한 정보(예: 미디어 메타데이터 및 재생 위치)를 저장해야 합니다. 앱이 백그라운드에서 포그라운드로 전환되면 저장된 정보로 로컬 재생이 계속되어야 합니다.

Kotlin
class MyActivity : AppCompatActivity() {
    private var castContext: CastContext? = null
    private var sessionTransferCallback: SessionTransferCallback? = null
    protected fun onCreate() {
        castContext = CastContext.getSharedInstance(this)
        castContext.getSessionManager()
                   .addSessionManagerListener(sessionManagerListener, CastSession::class.java)
        sessionTransferCallback = MySessionTransferCallback()
        castContext.addSessionTransferCallback(sessionTransferCallback)
    }

    protected fun onDestroy() {
        if (castContext != null) {
            castContext.getSessionManager()
                       .removeSessionManagerListener(sessionManagerListener, CastSession::class.java)
            if (sessionTransferCallback != null) {
                castContext.removeSessionTransferCallback(sessionTransferCallback)
            }
        }
    }

    class MySessionTransferCallback : SessionTransferCallback() {
        fun onTransferring(@SessionTransferCallback.TransferType transferType: Int) {
            // Perform necessary steps prior to onTransferred
        }

        fun onTransferred(@SessionTransferCallback.TransferType transferType: Int,
                          sessionState: SessionState?) {
            if (transferType == SessionTransferCallback.TRANSFER_TYPE_FROM_REMOTE_TO_LOCAL) {
                // Remote stream is transferred to the local device.

                // Retrieve information from the SessionState to continue playback on the local player.
            }
        }

        fun onTransferFailed(@SessionTransferCallback.TransferType transferType: Int,
                             @SessionTransferCallback.TransferFailedReason transferFailedReason: Int) {
            // Handle transfer failure.
        }
    }
}
Java
public class MyActivity extends AppCompatActivity {
  private CastContext castContext;
  private SessionTransferCallback sessionTransferCallback;

  @Override
  protected void onCreate() {
     castContext = CastContext.getSharedInstance(this);
     castContext
        .getSessionManager()
        .addSessionManagerListener(sessionManagerListener, CastSession.class);
     sessionTransferCallback = new MySessionTransferCallback();
     castContext.addSessionTransferCallback(sessionTransferCallback);
  }

  @Override
  protected void onDestroy() {
    if (castContext != null) {
       castContext
          .getSessionManager()
          .removeSessionManagerListener(sessionManagerListener, CastSession.class);
      if (sessionTransferCallback != null) {
         castContext.removeSessionTransferCallback(sessionTransferCallback);
      }
    }
  }

  public static class MySessionTransferCallback extends SessionTransferCallback {
    public MySessionTransferCallback() {}

    @Override
    public void onTransferring(@SessionTransferCallback.TransferType int transferType) {
        // Perform necessary steps prior to onTransferred
    }

    @Override
    public void onTransferred(@SessionTransferCallback.TransferType int transferType,
                               SessionState sessionState) {
      if (transferType==SessionTransferCallback.TRANSFER_TYPE_FROM_REMOTE_TO_LOCAL) {
        // Remote stream is transferred to the local device.

        // Retrieve information from the SessionState to continue playback on the local player.
      }
    }

    @Override
    public void onTransferFailed(@SessionTransferCallback.TransferType int transferType,
                                 @SessionTransferCallback.TransferFailedReason int transferFailedReason) {
      // Handle transfer failure.
    }
  }
}

원격 간

출력 전환기는 스트림 확장을 사용하여 오디오 앱을 여러 Cast 지원 스피커 기기로 확장하는 기능을 지원합니다.

오디오 앱은 Google Cast SDK 개발자 콘솔의 수신기 앱 설정에서 오디오용 Google Cast를 지원하는 앱입니다.

스피커를 통한 스트림 확장

출력 전환 도구를 사용하는 오디오 앱은 스트림 확장을 사용하여 전송 세션 중에 오디오를 여러 Cast 지원 스피커 기기로 확장할 수 있습니다.

이 기능은 Cast 플랫폼에서 지원하며 앱이 기본 UI를 사용하는 경우 추가 변경사항이 필요하지 않습니다. 맞춤 UI를 사용하는 경우 앱이 그룹에 전송 중임을 반영하도록 UI를 업데이트해야 합니다.

스트림 확장 중에 새 확장된 그룹 이름을 가져오려면 CastSession#addCastListener를 사용하여 Cast.Listener를 등록합니다. 그런 다음 onDeviceNameChanged 콜백 중에 CastSession#getCastDevice()를 호출합니다.

Kotlin
class MyActivity : Activity() {
    private var mCastSession: CastSession? = null
    private lateinit var mCastContext: CastContext
    private lateinit var mSessionManager: SessionManager
    private val mSessionManagerListener: SessionManagerListener<CastSession> =
        SessionManagerListenerImpl()
    private val mCastListener = CastListener()

    private inner class SessionManagerListenerImpl : SessionManagerListener<CastSession?> {
        override fun onSessionStarting(session: CastSession?) {}

        override fun onSessionStarted(session: CastSession?, sessionId: String) {
            addCastListener(session)
        }

        override fun onSessionStartFailed(session: CastSession?, error: Int) {}

        override fun onSessionSuspended(session: CastSession?, reason Int) {
            removeCastListener()
        }

        override fun onSessionResuming(session: CastSession?, sessionId: String) {}

        override fun onSessionResumed(session: CastSession?, wasSuspended: Boolean) {
            addCastListener(session)
        }

        override fun onSessionResumeFailed(session: CastSession?, error: Int) {}

        override fun onSessionEnding(session: CastSession?) {}

        override fun onSessionEnded(session: CastSession?, error: Int) {
            removeCastListener()
        }
    }

    private inner class CastListener : Cast.Listener() {
        override fun onDeviceNameChanged() {
            mCastSession?.let {
                val castDevice = it.castDevice
                val deviceName = castDevice.friendlyName
                // Update UIs with the new cast device name.
            }
        }
    }

    private fun addCastListener(castSession: CastSession) {
        mCastSession = castSession
        mCastSession?.addCastListener(mCastListener)
    }

    private fun removeCastListener() {
        mCastSession?.removeCastListener(mCastListener)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mCastContext = CastContext.getSharedInstance(this)
        mSessionManager = mCastContext.sessionManager
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession::class.java)
    }

    override fun onDestroy() {
        super.onDestroy()
        mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession::class.java)
    }
}
Java
public class MyActivity extends Activity {
    private CastContext mCastContext;
    private CastSession mCastSession;
    private SessionManager mSessionManager;
    private SessionManagerListener<CastSession> mSessionManagerListener =
            new SessionManagerListenerImpl();
    private Cast.Listener mCastListener = new CastListener();

    private class SessionManagerListenerImpl implements SessionManagerListener<CastSession> {
        @Override
        public void onSessionStarting(CastSession session) {}
        @Override
        public void onSessionStarted(CastSession session, String sessionId) {
            addCastListener(session);
        }
        @Override
        public void onSessionStartFailed(CastSession session, int error) {}
        @Override
        public void onSessionSuspended(CastSession session, int reason) {
            removeCastListener();
        }
        @Override
        public void onSessionResuming(CastSession session, String sessionId) {}
        @Override
        public void onSessionResumed(CastSession session, boolean wasSuspended) {
            addCastListener(session);
        }
        @Override
        public void onSessionResumeFailed(CastSession session, int error) {}
        @Override
        public void onSessionEnding(CastSession session) {}
        @Override
        public void onSessionEnded(CastSession session, int error) {
            removeCastListener();
        }
    }

    private class CastListener extends Cast.Listener {
         @Override
         public void onDeviceNameChanged() {
             if (mCastSession == null) {
                 return;
             }
             CastDevice castDevice = mCastSession.getCastDevice();
             String deviceName = castDevice.getFriendlyName();
             // Update UIs with the new cast device name.
         }
    }

    private void addCastListener(CastSession castSession) {
        mCastSession = castSession;
        mCastSession.addCastListener(mCastListener);
    }

    private void removeCastListener() {
        if (mCastSession != null) {
            mCastSession.removeCastListener(mCastListener);
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mCastContext = CastContext.getSharedInstance(this);
        mSessionManager = mCastContext.getSessionManager();
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession.class);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession.class);
    }
}

원격 간 테스트

이 기능을 테스트하려면 다음 단계를 따르세요.

  1. 기존 전송을 사용하거나 로컬-원격을 사용하여 Cast 지원 기기로 콘텐츠를 전송합니다.
  2. 진입점 중 하나를 사용하여 출력 전환 도구를 엽니다.
  3. 다른 Cast 지원 기기를 탭하면 오디오 앱이 콘텐츠를 추가 기기로 확장하여 동적 그룹을 만듭니다.
  4. Cast 지원 기기를 다시 탭하면 동적 그룹에서 삭제됩니다.