Переключатель вывода

Переключатель вывода – это функция Cast SDK, которая позволяет легко переключаться между локальным и удаленным воспроизведением контента. Она доступна в Android 13 и более поздних версиях. Цель состоит в том, чтобы помочь приложениям-отправителям легко и быстро контролировать, где воспроизводится контент. Переключатель вывода использует библиотеку MediaRouter, чтобы переключать воспроизведение контента между динамиком телефона, подключенными устройствами Bluetooth и удаленными устройствами с поддержкой Cast. Примеры использования можно разделить на следующие сценарии:

Скачайте и используйте пример приложения CastVideos-android, чтобы узнать, как реализовать переключатель вывода в своем приложении.

Чтобы поддерживать переключение между локальными и удаленными устройствами, а также между удаленными устройствами, необходимо включить переключатель вывода, следуя инструкциям в этом руководстве. Никаких дополнительных действий для поддержки передачи между динамиками локального устройства и подключенными устройствами Bluetooth не требуется.

Интерфейс переключателя вывода

В переключателе вывода отображаются доступные локальные и удаленные устройства, а также их текущие состояния, включая выбранное устройство, подключение и текущий уровень громкости. Если у вас есть другие устройства, вы можете перенести воспроизведение на них.

Известные проблемы

  • Сеансы мультимедиа, созданные для локального воспроизведения, будут закрыты и созданы заново при переключении на уведомление Cast SDK.

Точки входа

Уведомление о медиаконтенте

Если приложение отправляет уведомление о медиаконтенте с MediaSession для локального воспроизведения, в правом верхнем углу уведомления показывается чип с названием устройства, на котором воспроизводится контент, например "Динамик телефона". При нажатии на чип уведомления открывается диалоговое окно переключателя вывода.

Настройки громкости

Диалоговое окно переключателя вывода также можно открыть, нажав физические кнопки регулировки громкости на устройстве, а затем – значок настроек внизу и текст "Воспроизвести <название приложения> на <название устройства Cast>".

Список шагов

Требования

  1. Перенесите существующее приложение Android в AndroidX.
  2. Обновите файл build.gradle приложения, чтобы использовать минимальную требуемую версию Android Sender SDK для переключателя вывода:
    dependencies {
      ...
      implementation 'com.google.android.gms:play-services-cast-framework:21.2.0'
      ...
    }
  3. Приложение поддерживает уведомления мультимедиа.
  4. Устройство с Android 13.

Как настроить уведомления о медиаконтенте

Чтобы использовать переключатель вывода, аудио- и видеоприложения должны создавать уведомления о воспроизведении, в которых показывается статус воспроизведения и элементы управления для локального воспроизведения. Для этого нужно создать объект MediaSession, задать MediaStyle с токеном MediaSession и настроить элементы управления мультимедиа в уведомлении.

Если вы не используете MediaStyle и MediaSession, то можете настроить их, как показано в примере ниже. Также ознакомьтесь с руководствами по настройке обратных вызовов медиасеанса для аудио- и видеоприложений.

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() и укажите все нужные константы MediaMetadata для медиаконтента в MediaMetadataCompat.Builder().

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 для медиаконтента в PlaybackStateCompat.Builder().

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 приложения. В противном случае функция не будет включена, а флаг функции remote-to-local будет недействителен.

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

MediaTransferReceiver – это широковещательный приемник, который позволяет передавать медиаконтент между устройствами с системным интерфейсом. Подробную информацию можно найти в справочнике по MediaTransferReceiver.

Локальный – удаленный

Когда пользователь переключает воспроизведение с локального на удаленное, Cast SDK автоматически запускает сеанс Cast. Однако приложения должны обрабатывать переключение с локального на удаленное воспроизведение, например останавливать локальное воспроизведение и загружать медиаконтент на устройство Cast. Приложения должны прослушивать Cast SessionManagerListener, используя обратные вызовы onSessionStarted() и onSessionEnded(), и обрабатывать действие при получении обратных вызовов Cast SessionManager. Приложения должны обеспечивать работу этих обратных вызовов, когда открыто диалоговое окно переключателя вывода, а приложение не находится на переднем плане.

Обновление SessionManagerListener для фоновой трансляции

Устаревшая версия Cast уже поддерживает локальное и удаленное воспроизведение, когда приложение находится на переднем плане. Обычно пользователи нажимают на значок Cast в приложении и выбирают устройство для трансляции. В этом случае приложение должно зарегистрироваться в SessionManagerListener в onCreate() или onStart() и отменить регистрацию прослушивателя в onStop() или onDestroy() действия приложения.

В новом интерфейсе переключателя вывода приложения могут начинать трансляцию, работая в фоновом режиме. Это особенно полезно для аудиоприложений, которые отправляют уведомления при воспроизведении в фоновом режиме. Приложения могут регистрировать прослушиватели SessionManager в методе onCreate() сервиса и отменять регистрацию в методе 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);
    }
  }
}

Благодаря этому обновлению трансляция с локального устройства на удаленное работает так же, как и обычная трансляция, когда приложение находится в фоновом режиме, и для переключения с устройств Bluetooth на устройства Cast не требуется никаких дополнительных действий.

Удаленный-локальный

Переключатель вывода позволяет перенаправить воспроизведение с удаленного устройства на динамик телефона или локальное устройство Bluetooth. Для этого нужно задать для параметра setRemoteToLocalEnabled значение true в CastOptions.

Если текущее устройство-отправитель присоединяется к существующему сеансу с несколькими отправителями и приложению нужно проверить, разрешено ли передавать текущий медиаконтент локально, используйте обратный вызов onTransferred в SessionTransferCallback, чтобы проверить SessionState.

Установите флаг setRemoteToLocalEnabled

CastOptions.Builder предоставляет setRemoteToLocalEnabled, чтобы показывать или скрывать динамик телефона и локальные устройства Bluetooth в качестве целей для передачи в диалоговом окне переключателя вывода, когда есть активный сеанс трансляции.

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 и прослушивать обратные вызовы onTransferred и onTransferFailed, когда отправитель переключается на локальное воспроизведение.

После отмены регистрации 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 для аудио в настройках приложения-приемника в Google Cast SDK Developer Console.

Расширение трансляции с помощью колонок

Аудиоприложения, в которых используется переключатель вывода, могут расширять трансляцию на несколько колонок с поддержкой Cast.

Эта функция поддерживается платформой Cast и не требует дополнительных изменений, если приложение использует пользовательский интерфейс по умолчанию. Если используется пользовательский интерфейс, приложение должно обновить его, чтобы показать, что трансляция выполняется в группу.

Чтобы получить новое название расширенной группы во время расширения трансляции, зарегистрируйте Cast.Listener с помощью CastSession#addCastListener. Затем вызовите CastSession#getCastDevice() во время обратного вызова onDeviceNameChanged.

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 ещё раз, и оно будет удалено из динамической группы.