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

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

Загрузите и используйте пример приложения CastVideos-android в качестве справочного материала по внедрению переключателя выходов в ваше приложение.

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

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

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

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

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

Точки входа

Уведомление СМИ

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

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

Пользовательский интерфейс диалогового окна «Переключатель выходов» также можно вызвать, нажав физические кнопки регулировки громкости на устройстве, нажав значок настроек внизу и нажав текст «Воспроизвести <Название приложения> на <Устройстве трансляции>».

Краткое изложение шагов

Предпосылки

  1. Перенесите существующее приложение Android на AndroidX.
  2. Обновите build.gradle вашего приложения, чтобы использовать минимально необходимую версию Android Sender SDK для Output Switcher:
    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)
Ява
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()
)
Ява
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()
)
Ява
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 .

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

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

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

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

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

Котлин
class CastOptionsProvider : OptionsProvider {
    fun getCastOptions(context: Context?): CastOptions {
        ...
        return Builder()
            ...
            .setRemoteToLocalEnabled(true)
            .build()
    }
}
Ява
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.
        }
    }
}
Ява
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.
        }
    }
}
Ява
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.
    }
  }
}

Удаленный-удаленный

Output Switcher поддерживает возможность расширения на несколько акустических устройств с поддержкой Cast для аудиоприложений с помощью функции Stream Expansion.

Аудиоприложения — это приложения, которые поддерживают Google Cast для аудио в настройках приложения-приемника в консоли разработчика Google Cast SDK.

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

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

Эта функция поддерживается платформой 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)
    }
}
Ява
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 еще раз, и оно будет удалено из динамической группы.