Переключатель вывода — это функция Cast SDK, которая обеспечивает беспрепятственный переход между локальным и удаленным воспроизведением контента, начиная с Android 13. Цель состоит в том, чтобы помочь приложениям-отправителям легко и быстро контролировать, где воспроизводится контент. Output Switcher использует библиотеку MediaRouter
для переключения воспроизведения контента между динамиком телефона, сопряженными устройствами Bluetooth и удаленными устройствами с поддержкой Cast. Варианты использования можно разбить на следующие сценарии:
Загрузите и используйте приведенный ниже пример для справки о том, как реализовать Output Switcher в вашем аудиоприложении. Инструкции по запуску примера см. в прилагаемом файле README.md .
Выходной коммутатор должен быть включен для поддержки локального и удаленного и удаленного локального, используя шаги, описанные в этом руководстве. Для поддержки передачи между динамиками локального устройства и сопряженными устройствами Bluetooth не требуется никаких дополнительных действий.
Аудиоприложения — это приложения, которые поддерживают Google Cast для аудио в настройках приложения-приемника в консоли разработчика Google Cast SDK.
Пользовательский интерфейс переключателя выходов
Переключатель вывода отображает доступные локальные и удаленные устройства, а также текущее состояние устройства, в том числе, если устройство выбрано, подключается, текущий уровень громкости. Если в дополнение к текущему устройству есть другие устройства, щелчок по другому устройству позволяет перенести воспроизведение мультимедиа на выбранное устройство.
Известные вопросы
- Сеансы мультимедиа, созданные для локального воспроизведения, будут закрыты и созданы заново при переключении на уведомление Cast SDK.
Точки входа
Уведомление СМИ
Если приложение публикует уведомление о мультимедиа с помощью MediaSession
для локального воспроизведения (воспроизведения локально), в правом верхнем углу уведомления о мультимедиа отображается чип уведомления с именем устройства (например, динамика телефона), на котором в данный момент воспроизводится контент. При нажатии на чип уведомлений открывается пользовательский интерфейс диалоговой системы Output Switcher.
Настройки громкости
Пользовательский интерфейс диалоговой системы Output Switcher также можно вызвать, нажав кнопки физической громкости на устройстве, коснувшись значка настроек внизу и коснувшись текста «Play <App Name> on <Cast Device>».
Краткое изложение шагов
- Убедитесь, что выполнены предварительные условия
- Включить переключатель вывода в AndroidManifest.xml
- Обновите SessionManagerListener для фонового литья
- Установите флаг setRemoteToLocalEnabled
- Продолжить воспроизведение локально
Предпосылки
- Перенесите существующее приложение Android на AndroidX.
- Обновите
build.gradle
вашего приложения, чтобы использовать минимальную требуемую версию Android Sender SDK для переключателя вывода:dependencies { ... implementation 'com.google.android.gms:play-services-cast-framework:21.2.0' ... }
- Приложение поддерживает медиа-уведомления.
- Устройство под управлением 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
. Приложения должны гарантировать, что эти обратные вызовы все еще активны, когда диалоговое окно Output Switcher открыто, а приложение не находится на переднем плане.
Обновите SessionManagerListener для фонового литья
Устаревший интерфейс 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
предоставляет setRemoteToLocalEnabled
для отображения или скрытия динамика телефона и локальных устройств Bluetooth в качестве целей передачи в диалоговом окне Output Switcher при наличии активного сеанса Cast.
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
. Следовательно, порядок обратных вызовов удаленно-локальным:
-
onTransferring
-
onSessionEnding
-
onSessionEnded
-
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. } } }