Выходной переключатель

Output Switcher — это функция SDK Cast, которая обеспечивает плавную передачу между локальным и удалённым воспроизведением контента, начиная с Android 13. Цель — помочь приложениям-отправителям легко и быстро управлять тем, где воспроизводится контент. Output Switcher использует библиотеку 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 , приведенный ниже фрагмент кода показывает, как их настроить, а также доступны руководства по настройке обратных вызовов медиасессии для аудио- и видеоприложений :

Котлин
// 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() .

Котлин
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() .

Котлин
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 — это широковещательный приемник, обеспечивающий передачу мультимедиа между устройствами с системным пользовательским интерфейсом. Дополнительную информацию см. в справочнике MediaTransferReceiver .

Локальный — удалённый

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

Обновите SessionManagerListener для фонового преобразования типов.

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

Благодаря новой функции трансляции с использованием Output Switcher, приложения могут начинать трансляцию, даже когда они находятся в фоновом режиме. Это особенно полезно для аудиоприложений, которые отправляют уведомления во время воспроизведения в фоновом режиме. Приложения могут регистрировать слушатели SessionManager в методе onCreate() службы и отменять регистрацию в методе onDestroy() службы. Приложения всегда должны получать локальные обратные вызовы (например, onSessionStarted ), когда приложение находится в фоновом режиме.

Если приложение использует MediaBrowserService , рекомендуется зарегистрировать там SessionManagerListener .

Котлин
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 в качестве целевых устройств для передачи данных в диалоговом окне «Переключатель вывода» при наличии активной сессии Cast.

Котлин
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 и возобновлять воспроизведение локально как в фоновом, так и в активном режиме приложения.

Котлин
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 (например, метаданные медиафайлов и позицию воспроизведения). Когда приложение переводится из фонового режима в активный, локальное воспроизведение должно продолжиться с использованием сохраненной информации.

Котлин
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.

Расширение потока с помощью динамиков

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

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

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

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