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

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

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

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

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

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

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

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

Точки входа

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

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

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

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

Список шагов

Требования

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