В этом руководстве для разработчиков рассказывается, как добавить поддержку Google Cast в приложение-отправитель для Android с помощью Android Sender SDK.
Мобильное устройство или ноутбук – это отправитель, который управляет воспроизведением, а устройство с поддержкой Google Cast – получатель, который показывает контент на телевизоре.
Фреймворк отправителя – это двоичный файл библиотеки классов Cast и связанные с ним ресурсы, которые присутствуют во время выполнения на устройстве отправителя. Отправляющее приложение или приложение Cast – это приложение, которое также запущено на устройстве отправителя. Приложение Web Receiver – это HTML-приложение, которое работает на устройстве с поддержкой Cast.
В фреймворке отправителя используется асинхронный обратный вызов, чтобы сообщать приложению отправителя о событиях и переходить между различными состояниями жизненного цикла приложения Cast.
Путь в приложении
Ниже описаны основные этапы выполнения для приложения Android-отправителя.
- Фреймворк Cast автоматически запускает
MediaRouterобнаружение устройств на основе жизненного циклаActivity. - Когда пользователь нажимает кнопку трансляции, фреймворк показывает диалоговое окно со списком обнаруженных устройств Cast.
- Когда пользователь выбирает устройство Cast, фреймворк пытается запустить на нем приложение веб-приемника.
- Фреймворк вызывает обратные вызовы в приложении отправителя, чтобы подтвердить запуск приложения Web Receiver.
- Фреймворк создает канал связи между отправителем и приложениями Web Receiver.
- Фреймворк использует канал связи для загрузки и управления воспроизведением медиаконтента на веб-приемнике.
- Фреймворк синхронизирует состояние воспроизведения медиаконтента между отправителем и веб-приемником. Когда пользователь выполняет действия в интерфейсе отправителя, фреймворк передает запросы на управление медиаконтентом веб-приемнику. Когда веб-приемник отправляет обновления статуса медиаконтента, фреймворк обновляет состояние интерфейса отправителя.
- Когда пользователь нажимает кнопку трансляции, чтобы отключиться от устройства для трансляции [контента], фреймворк отключает приложение отправителя от веб-приемника.
Полный список всех классов, методов и событий в Google Cast Android SDK приведен в справочнике по Google Cast Sender API для Android. Ниже описано, как добавить поддержку Cast в приложение для Android.
Как настроить манифест Android
В файле AndroidManifest.xml приложения необходимо настроить следующие элементы для Cast SDK:
uses-sdk
Укажите минимальный и целевой уровни API Android, которые поддерживает Cast SDK. Сейчас минимальный уровень API – 24, а целевой – 35.
<uses-sdk
android:minSdkVersion="24"
android:targetSdkVersion="35" />
android:theme
Задайте тему приложения на основе минимальной версии Android SDK. Например, если вы не используете собственную тему, то для версий Android SDK ниже Lollipop следует использовать вариант Theme.AppCompat.
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat" >
...
</application>
Инициализация контекста Cast
В фреймворке есть глобальный объект-одиночка CastContext, который координирует все взаимодействия фреймворка.
Чтобы предоставить параметры, необходимые для инициализации синглтона CastContext, в приложении должен быть реализован интерфейс OptionsProvider. OptionsProvider предоставляет экземпляр CastOptions, который содержит настройки, влияющие на поведение фреймворка. Самый важный из них – идентификатор приложения Web Receiver, который используется для фильтрации результатов поиска и запуска приложения Web Receiver при начале сеанса Cast.
class CastOptionsProvider : OptionsProvider { override fun getCastOptions(context: Context): CastOptions { return Builder() .setReceiverApplicationId(context.getString(R.string.app_id)) .build() } override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? { return null } }
public class CastOptionsProvider implements OptionsProvider { @Override public CastOptions getCastOptions(Context context) { CastOptions castOptions = new CastOptions.Builder() .setReceiverApplicationId(context.getString(R.string.app_id)) .build(); return castOptions; } @Override public List<SessionProvider> getAdditionalSessionProviders(Context context) { return null; } }
В файле AndroidManifest.xml приложения-отправителя необходимо объявить полное имя реализованного класса OptionsProvider в качестве поля метаданных:
<application>
...
<meta-data
android:name=
"com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="com.foo.CastOptionsProvider" />
</application>
CastContext инициализируется при первом вызове метода CastContext.getSharedInstance().
class MyActivity : FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { val castContext = CastContext.getSharedInstance(this) } }
public class MyActivity extends FragmentActivity { @Override public void onCreate(Bundle savedInstanceState) { CastContext castContext = CastContext.getSharedInstance(this); } }
Виджеты Cast UX
В фреймворке Cast есть виджеты, соответствующие контрольному списку дизайна Cast:
Вводный оверлей. Фреймворк предоставляет специальное представление
IntroductoryOverlay, которое показывается пользователю, чтобы привлечь его внимание к кнопке трансляции, когда приемник становится доступен в первый раз. Приложение отправителя может настраивать текст и положение заголовка.Кнопка трансляции видна независимо от того, доступны ли устройства для трансляции. Когда пользователь впервые нажимает кнопку трансляции, появляется диалоговое окно с обнаруженными устройствами. Когда устройство подключено, при нажатии на кнопку трансляции показываются метаданные текущего медиаконтента (например, название, имя студии звукозаписи и значок) или появляется возможность отключиться от устройства для трансляции. Кнопку трансляции иногда называют значком трансляции.
Мини-контроллер. Если пользователь транслирует контент и перешел с текущей страницы контента или развернутого контроллера на другой экран в приложении отправителя, в нижней части экрана показывается мини-контроллер. Он позволяет просматривать метаданные транслируемого контента и управлять воспроизведением.
Расширенный контроллер. Когда пользователь транслирует контент и нажимает на уведомление о медиаконтенте или мини-контроллер, запускается расширенный контроллер, на котором отображаются метаданные воспроизводимого контента и кнопки управления воспроизведением.
Уведомление: только для Android. Когда пользователь транслирует контент и переходит из приложения отправителя, появляется уведомление с метаданными транслируемого контента и элементами управления воспроизведением.
Заблокированный экран. Только для Android. Когда пользователь транслирует контент и переходит на заблокированный экран (или устройство переходит в режим ожидания), на экране блокировки отображаются метаданные транслируемого контента и элементы управления воспроизведением.
В руководстве ниже описано, как добавить эти виджеты в приложение.
Как добавить кнопку трансляции
API MediaRouter для Android позволяют показывать и воспроизводить медиаконтент на дополнительных устройствах.
В приложениях для Android, использующих API MediaRouter, должна быть кнопка трансляции как часть пользовательского интерфейса, чтобы пользователи могли выбирать маршрут передачи медиаконтента на второе устройство, например устройство для трансляции.
Благодаря фреймворку добавить MediaRouteButton в качестве Cast button очень просто. Сначала добавьте пункт меню или MediaRouteButton в XML-файл, определяющий меню, и используйте CastButtonFactory, чтобы связать его с фреймворком.
// To add a Cast button, add the following snippet.
// menu.xml
<item
android:id="@+id/media_route_menu_item"
android:title="@string/media_route_menu_title"
app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
app:showAsAction="always" />
// Then override the onCreateOptionMenu() for each of your activities. // MyActivity.kt override fun onCreateOptionsMenu(menu: Menu): Boolean { super.onCreateOptionsMenu(menu) menuInflater.inflate(R.menu.main, menu) CastButtonFactory.setUpMediaRouteButton( applicationContext, menu, R.id.media_route_menu_item ) return true }
// Then override the onCreateOptionMenu() for each of your activities. // MyActivity.java @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); getMenuInflater().inflate(R.menu.main, menu); CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), menu, R.id.media_route_menu_item); return true; }
Если ваш атрибут Activity наследуется от FragmentActivity, вы можете добавить в макет атрибут MediaRouteButton.
// activity_layout.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal" >
<androidx.mediarouter.app.MediaRouteButton
android:id="@+id/media_route_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:mediaRouteTypes="user"
android:visibility="gone" />
</LinearLayout>
// MyActivity.kt override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_layout) mMediaRouteButton = findViewById<View>(R.id.media_route_button) as MediaRouteButton CastButtonFactory.setUpMediaRouteButton(applicationContext, mMediaRouteButton) mCastContext = CastContext.getSharedInstance(this) }
// MyActivity.java @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_layout); mMediaRouteButton = (MediaRouteButton) findViewById(R.id.media_route_button); CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), mMediaRouteButton); mCastContext = CastContext.getSharedInstance(this); }
Чтобы настроить внешний вид кнопки трансляции с помощью темы, ознакомьтесь с разделом Как настроить кнопку трансляции.
Как настроить обнаружение устройств
Обнаружение устройств полностью управляется CastContext.
При инициализации CastContext приложение отправителя указывает идентификатор приложения веб-приемника и может при необходимости запросить фильтрацию пространства имен, задав supportedNamespaces в CastOptions.
CastContext содержит ссылку на MediaRouter и запускает процесс обнаружения при следующих условиях:
- В соответствии с алгоритмом, который позволяет сбалансировать задержку обнаружения устройства и расход заряда батареи, обнаружение иногда запускается автоматически, когда приложение отправителя переходит на передний план.
- Открыто диалоговое окно Cast.
- Cast SDK пытается восстановить сеанс Cast.
Процесс обнаружения будет остановлен, когда диалоговое окно Cast будет закрыто или приложение отправителя перейдет в фоновый режим.
class CastOptionsProvider : OptionsProvider { companion object { const val CUSTOM_NAMESPACE = "urn:x-cast:custom_namespace" } override fun getCastOptions(appContext: Context): CastOptions { val supportedNamespaces: MutableList<String> = ArrayList() supportedNamespaces.add(CUSTOM_NAMESPACE) return CastOptions.Builder() .setReceiverApplicationId(context.getString(R.string.app_id)) .setSupportedNamespaces(supportedNamespaces) .build() } override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? { return null } }
class CastOptionsProvider implements OptionsProvider { public static final String CUSTOM_NAMESPACE = "urn:x-cast:custom_namespace"; @Override public CastOptions getCastOptions(Context appContext) { List<String> supportedNamespaces = new ArrayList<>(); supportedNamespaces.add(CUSTOM_NAMESPACE); CastOptions castOptions = new CastOptions.Builder() .setReceiverApplicationId(context.getString(R.string.app_id)) .setSupportedNamespaces(supportedNamespaces) .build(); return castOptions; } @Override public List<SessionProvider> getAdditionalSessionProviders(Context context) { return null; } }
Как работает управление сеансами
В Cast SDK представлено понятие сеанса Cast, который устанавливается в результате подключения к устройству, запуска (или присоединения к) приложения Web Receiver, подключения к этому приложению и инициализации канала управления медиаконтентом. Дополнительную информацию о сеансах Cast и жизненном цикле веб-приемника можно найти в руководстве по жизненному циклу приложений веб-приемника.
Сеансами управляет класс SessionManager, к которому ваше приложение может получить доступ через CastContext.getSessionManager().
Отдельные сеансы представлены подклассами класса Session.
Например, CastSession – это сеансы с Cast-устройствами. Приложение может получить доступ к текущему активному сеансу Cast через SessionManager.getCurrentCastSession().
Приложение может использовать класс SessionManagerListener для отслеживания событий сеанса, таких как создание, приостановка, возобновление и завершение. Фреймворк автоматически пытается возобновить работу после аварийного или внезапного завершения активного сеанса.
Сеансы создаются и завершаются автоматически в ответ на действия пользователя в диалоговых окнах MediaRouter.
Чтобы лучше понимать ошибки запуска Cast, приложения могут использовать
CastContext#getCastReasonCodeForCastStatusCode(int)
для преобразования ошибки запуска сеанса в
CastReasonCodes.
Обратите внимание, что некоторые ошибки при запуске сеанса (например, CastReasonCodes#CAST_CANCELLED) являются ожидаемым поведением и не должны регистрироваться как ошибки.
Если вам нужно отслеживать изменения состояния сеанса, вы можете реализовать SessionManagerListener. В этом примере показано, как отслеживать доступность объекта CastSession в объекте Activity.
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 inner class SessionManagerListenerImpl : SessionManagerListener<CastSession?> { override fun onSessionStarting(session: CastSession?) {} override fun onSessionStarted(session: CastSession?, sessionId: String) { invalidateOptionsMenu() } override fun onSessionStartFailed(session: CastSession?, error: Int) { val castReasonCode = mCastContext.getCastReasonCodeForCastStatusCode(error) // Handle error } override fun onSessionSuspended(session: CastSession?, reason Int) {} override fun onSessionResuming(session: CastSession?, sessionId: String) {} override fun onSessionResumed(session: CastSession?, wasSuspended: Boolean) { invalidateOptionsMenu() } override fun onSessionResumeFailed(session: CastSession?, error: Int) {} override fun onSessionEnding(session: CastSession?) {} override fun onSessionEnded(session: CastSession?, error: Int) { finish() } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) mCastContext = CastContext.getSharedInstance(this) mSessionManager = mCastContext.sessionManager mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession::class.java) } override fun onResume() { super.onResume() mCastSession = mSessionManager.currentCastSession } 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 class SessionManagerListenerImpl implements SessionManagerListener<CastSession> { @Override public void onSessionStarting(CastSession session) {} @Override public void onSessionStarted(CastSession session, String sessionId) { invalidateOptionsMenu(); } @Override public void onSessionStartFailed(CastSession session, int error) { int castReasonCode = mCastContext.getCastReasonCodeForCastStatusCode(error); // Handle error } @Override public void onSessionSuspended(CastSession session, int reason) {} @Override public void onSessionResuming(CastSession session, String sessionId) {} @Override public void onSessionResumed(CastSession session, boolean wasSuspended) { invalidateOptionsMenu(); } @Override public void onSessionResumeFailed(CastSession session, int error) {} @Override public void onSessionEnding(CastSession session) {} @Override public void onSessionEnded(CastSession session, int error) { finish(); } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mCastContext = CastContext.getSharedInstance(this); mSessionManager = mCastContext.getSessionManager(); mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession.class); } @Override protected void onResume() { super.onResume(); mCastSession = mSessionManager.getCurrentCastSession(); } @Override protected void onDestroy() { super.onDestroy(); mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession.class); } }
Перенос трансляции
Сохранение состояния сеанса – основа передачи потока, при которой пользователи могут переключать аудио- и видеопотоки между устройствами с помощью голосовых команд, приложения Google Home или умных дисплеев. Воспроизведение контента прекращается на одном устройстве (источнике) и продолжается на другом (целевом). Любое устройство Cast с последней версией встроенного ПО может служить источником или получателем при переносе трансляции.
Чтобы получить новое целевое устройство во время переноса или расширения трансляции, зарегистрируйте Cast.Listener с помощью CastSession#addCastListener.
Затем вызовите CastSession#getCastDevice() во время обратного вызова onDeviceNameChanged.
Подробнее о передаче потока на веб-приемнике…
Автоматическое повторное подключение
Фреймворк предоставляет
ReconnectionService,
которое может быть включено в приложении отправителя для обработки повторного подключения во многих сложных случаях, например:
- Как восстановить подключение к Wi-Fi после временного сбоя
- Как выйти из спящего режима
- Как восстановить работу приложения после его сворачивания
- Восстановление после сбоя приложения
Этот сервис включен по умолчанию и может быть отключен в CastOptions.Builder.
Этот сервис можно автоматически добавить в манифест приложения, если в файле gradle включено автоматическое объединение.
Фреймворк запускает сервис, когда начинается сеанс мультимедиа, и останавливает его, когда сеанс заканчивается.
Как работают элементы управления мультимедиа
В Cast Framework класс RemoteMediaPlayer из Cast 2.x заменен на новый класс RemoteMediaClient, который предоставляет те же функции в более удобных API и позволяет не передавать GoogleApiClient.
Когда ваше приложение устанавливает CastSession с приложением веб-приемника, поддерживающим пространство имен мультимедиа, фреймворк автоматически создает экземпляр RemoteMediaClient. Ваше приложение может получить к нему доступ, вызвав метод getRemoteMediaClient() для экземпляра CastSession.
Все методы RemoteMediaClient, которые отправляют запросы на веб-приемник, возвращают объект PendingResult, который можно использовать для отслеживания запроса.
Ожидается, что экземпляр RemoteMediaClient будет использоваться несколькими частями приложения, а также некоторыми внутренними компонентами фреймворка, например постоянными мини-контроллерами и сервисом уведомлений.
Поэтому этот экземпляр поддерживает регистрацию нескольких экземпляров RemoteMediaClient.Listener.
Как задать метаданные медиаконтента
Класс MediaMetadata представляет информацию о мультимедийном объекте, который вы хотите транслировать. В следующем примере создается новый экземпляр MediaMetadata для фильма и задаются его название, субтитры и два изображения.
val movieMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE) movieMetadata.putString(MediaMetadata.KEY_TITLE, mSelectedMedia.getTitle()) movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, mSelectedMedia.getStudio()) movieMetadata.addImage(WebImage(Uri.parse(mSelectedMedia.getImage(0)))) movieMetadata.addImage(WebImage(Uri.parse(mSelectedMedia.getImage(1))))
MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); movieMetadata.putString(MediaMetadata.KEY_TITLE, mSelectedMedia.getTitle()); movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, mSelectedMedia.getStudio()); movieMetadata.addImage(new WebImage(Uri.parse(mSelectedMedia.getImage(0)))); movieMetadata.addImage(new WebImage(Uri.parse(mSelectedMedia.getImage(1))));
Подробнее о том, как использовать изображения с метаданными медиаконтента, рассказывается в разделе Выбор изображений.
Загрузить медиаконтент
Ваше приложение может загрузить мультимедийный объект, как показано в следующем коде. Сначала используйте MediaInfo.Builder с метаданными медиаконтента, чтобы создать экземпляр MediaInfo. Получите
RemoteMediaClient
из текущего CastSession, а затем загрузите MediaInfo в этот
RemoteMediaClient. Используйте RemoteMediaClient, чтобы управлять воспроизведением в приложении медиапроигрывателя, запущенном на веб-приемнике.
val mediaInfo = MediaInfo.Builder(mSelectedMedia.getUrl()) .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) .setContentType("videos/mp4") .setMetadata(movieMetadata) .setStreamDuration(mSelectedMedia.getDuration() * 1000) .build() val remoteMediaClient = mCastSession.getRemoteMediaClient() remoteMediaClient.load(MediaLoadRequestData.Builder().setMediaInfo(mediaInfo).build())
MediaInfo mediaInfo = new MediaInfo.Builder(mSelectedMedia.getUrl()) .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) .setContentType("videos/mp4") .setMetadata(movieMetadata) .setStreamDuration(mSelectedMedia.getDuration() * 1000) .build(); RemoteMediaClient remoteMediaClient = mCastSession.getRemoteMediaClient(); remoteMediaClient.load(new MediaLoadRequestData.Builder().setMediaInfo(mediaInfo).build());
Также ознакомьтесь с разделом Использование медиадорожек.
Формат видео 4K
Чтобы проверить формат видео, используйте getVideoInfo() в MediaStatus. Так вы получите текущий экземпляр VideoInfo.
Этот экземпляр содержит тип формата HDR TV, а также высоту и ширину экрана в пикселях. Варианты формата 4K обозначаются константами HDR_TYPE_*.
Уведомления с меню управления трансляцией на нескольких устройствах
Когда пользователь транслирует контент, другие устройства Android в той же сети получают уведомление, позволяющее им также управлять воспроизведением. Любой, чье устройство получает такие уведомления, может отключить их в приложении "Настройки" в разделе Google > Google Cast > Показывать уведомления о дистанционном управлении. (В уведомлениях есть ярлык для приложения "Настройки".) Подробнее об уведомлениях для дистанционного управления трансляцией…
Добавить мини-контроллер
Согласно контрольному списку по дизайну Cast, приложение-отправитель должно предоставлять постоянный элемент управления, известный как мини-контроллер, который должен появляться, когда пользователь переходит со страницы текущего контента в другой раздел приложения-отправителя. Мини-контроллер напоминает пользователю о текущем сеансе Cast. Нажав на мини-контроллер, пользователь может вернуться к полноэкранному контроллеру трансляции.
Фреймворк предоставляет специальный класс View, MiniControllerFragment, который можно добавить в нижнюю часть файла макета для каждого объекта activity, в котором вы хотите показывать мини-контроллер.
<fragment
android:id="@+id/castMiniController"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:visibility="gone"
class="com.google.android.gms.cast.framework.media.widget.MiniControllerFragment" />
Когда приложение отправителя воспроизводит видео или аудио в прямом эфире, SDK автоматически показывает кнопку "Воспроизвести/остановить" вместо кнопки "Воспроизвести/пауза" в мини-контроллере.
Чтобы настроить внешний вид текста заголовка и подзаголовка этого специального представления, а также выбрать кнопки, ознакомьтесь с разделом Настройка мини-контроллера.
Как добавить расширенный контроллер
Согласно контрольному списку требований к дизайну Google Cast, приложение-отправитель должно предоставлять расширенный контроллер для медиаконтента, транслируемого с помощью Cast. Развернутый контроллер – это полноэкранная версия мини-контроллера.
В Cast SDK есть виджет для расширенного контроллера, который называется ExpandedControllerActivity.
Это абстрактный класс, для которого нужно создать подкласс, чтобы добавить кнопку трансляции.
Сначала создайте новый файл ресурсов меню для расширенного контроллера, чтобы добавить кнопку трансляции:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/media_route_menu_item"
android:title="@string/media_route_menu_title"
app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
app:showAsAction="always"/>
</menu>
Создайте новый класс, который расширяет ExpandedControllerActivity.
class ExpandedControlsActivity : ExpandedControllerActivity() { override fun onCreateOptionsMenu(menu: Menu): Boolean { super.onCreateOptionsMenu(menu) menuInflater.inflate(R.menu.expanded_controller, menu) CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item) return true } }
public class ExpandedControlsActivity extends ExpandedControllerActivity { @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); getMenuInflater().inflate(R.menu.expanded_controller, menu); CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item); return true; } }
Теперь объявите новое действие в манифесте приложения в теге application:
<application>
...
<activity
android:name=".expandedcontrols.ExpandedControlsActivity"
android:label="@string/app_name"
android:launchMode="singleTask"
android:theme="@style/Theme.CastVideosDark"
android:screenOrientation="portrait"
android:parentActivityName="com.google.sample.cast.refplayer.VideoBrowserActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
</intent-filter>
</activity>
...
</application>
Измените CastOptionsProvider, а также NotificationOptions и CastMediaOptions, чтобы задать новое целевое действие:
override fun getCastOptions(context: Context): CastOptions? { val notificationOptions = NotificationOptions.Builder() .setTargetActivityClassName(ExpandedControlsActivity::class.java.name) .build() val mediaOptions = CastMediaOptions.Builder() .setNotificationOptions(notificationOptions) .setExpandedControllerActivityClassName(ExpandedControlsActivity::class.java.name) .build() return CastOptions.Builder() .setReceiverApplicationId(context.getString(R.string.app_id)) .setCastMediaOptions(mediaOptions) .build() }
public CastOptions getCastOptions(Context context) { NotificationOptions notificationOptions = new NotificationOptions.Builder() .setTargetActivityClassName(ExpandedControlsActivity.class.getName()) .build(); CastMediaOptions mediaOptions = new CastMediaOptions.Builder() .setNotificationOptions(notificationOptions) .setExpandedControllerActivityClassName(ExpandedControlsActivity.class.getName()) .build(); return new CastOptions.Builder() .setReceiverApplicationId(context.getString(R.string.app_id)) .setCastMediaOptions(mediaOptions) .build(); }
Обновите метод LocalPlayerActivity loadRemoteMedia, чтобы при загрузке удаленного медиаконтента отображался новый объект activity:
private fun loadRemoteMedia(position: Int, autoPlay: Boolean) { val remoteMediaClient = mCastSession?.remoteMediaClient ?: return remoteMediaClient.registerCallback(object : RemoteMediaClient.Callback() { override fun onStatusUpdated() { val intent = Intent(this@LocalPlayerActivity, ExpandedControlsActivity::class.java) startActivity(intent) remoteMediaClient.unregisterCallback(this) } }) remoteMediaClient.load( MediaLoadRequestData.Builder() .setMediaInfo(mSelectedMedia) .setAutoplay(autoPlay) .setCurrentTime(position.toLong()).build() ) }
private void loadRemoteMedia(int position, boolean autoPlay) { if (mCastSession == null) { return; } final RemoteMediaClient remoteMediaClient = mCastSession.getRemoteMediaClient(); if (remoteMediaClient == null) { return; } remoteMediaClient.registerCallback(new RemoteMediaClient.Callback() { @Override public void onStatusUpdated() { Intent intent = new Intent(LocalPlayerActivity.this, ExpandedControlsActivity.class); startActivity(intent); remoteMediaClient.unregisterCallback(this); } }); remoteMediaClient.load(new MediaLoadRequestData.Builder() .setMediaInfo(mSelectedMedia) .setAutoplay(autoPlay) .setCurrentTime(position).build()); }
Когда приложение отправителя воспроизводит видео или аудиоконтент в режиме стриминга, SDK автоматически показывает кнопку "Воспроизвести/остановить" вместо кнопки "Воспроизвести/пауза" на развернутом контроллере.
Чтобы настроить внешний вид с помощью тем, выбрать, какие кнопки показывать, и добавить собственные кнопки, ознакомьтесь с разделом Настройка расширенного контроллера.
Регулятор громкости
Фреймворк автоматически управляет громкостью для приложения отправителя. Он также автоматически синхронизирует приложения отправителя и веб-приемника, чтобы в интерфейсе отправителя всегда отображалась громкость, заданная в веб-приемнике.
Управление громкостью с помощью физических кнопок
На устройствах Android с Jelly Bean или более новой версией ОС по умолчанию можно использовать физические кнопки на устройстве отправителя, чтобы изменить громкость сеанса Cast на веб-приемнике.
Управление громкостью с помощью физических кнопок до версии Jelly Bean
Чтобы использовать физические клавиши громкости для управления громкостью устройства Web Receiver на устройствах Android старше Jelly Bean, приложение-отправитель должно переопределить dispatchKeyEvent в своих действиях и вызвать CastContext.onDispatchVolumeKeyEventBeforeJellyBean():
class MyActivity : FragmentActivity() { override fun dispatchKeyEvent(event: KeyEvent): Boolean { return (CastContext.getSharedInstance(this) .onDispatchVolumeKeyEventBeforeJellyBean(event) || super.dispatchKeyEvent(event)) } }
class MyActivity extends FragmentActivity { @Override public boolean dispatchKeyEvent(KeyEvent event) { return CastContext.getSharedInstance(this) .onDispatchVolumeKeyEventBeforeJellyBean(event) || super.dispatchKeyEvent(event); } }
Как добавить элементы управления мультимедиа в уведомления и на заблокированный экран
Согласно контрольному списку Google Cast Design Checklist, приложение-отправитель для Android должно реализовать элементы управления медиаконтентом в уведомлении и на заблокированном экране, когда отправитель выполняет трансляцию, но приложение-отправитель не находится в фокусе. Фреймворк предоставляет классы MediaNotificationService и MediaIntentReceiver, которые помогают приложению-отправителю создавать элементы управления мультимедиа в уведомлении и на заблокированном экране.
MediaNotificationService запускается, когда отправитель транслирует контент, и показывает уведомление с уменьшенным изображением и информацией о текущем транслируемом объекте, а также кнопками воспроизведения/паузы и остановки.
MediaIntentReceiver – это BroadcastReceiver, обрабатывающий действия пользователя, выполненные в уведомлении.
Приложение может настроить уведомления и элементы управления медиаконтентом на заблокированном экране с помощью NotificationOptions.
Приложение может настроить, какие кнопки управления будут показываться в уведомлении и какое Activity откроется, когда пользователь нажмет на уведомление. Если действия не указаны явно, будут использоваться значения по умолчанию: MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK и MediaIntentReceiver.ACTION_STOP_CASTING.
// Example showing 4 buttons: "rewind", "play/pause", "forward" and "stop casting". val buttonActions: MutableList<String> = ArrayList() buttonActions.add(MediaIntentReceiver.ACTION_REWIND) buttonActions.add(MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK) buttonActions.add(MediaIntentReceiver.ACTION_FORWARD) buttonActions.add(MediaIntentReceiver.ACTION_STOP_CASTING) // Showing "play/pause" and "stop casting" in the compat view of the notification. val compatButtonActionsIndices = intArrayOf(1, 3) // Builds a notification with the above actions. Each tap on the "rewind" and "forward" buttons skips 30 seconds. // Tapping on the notification opens an Activity with class VideoBrowserActivity. val notificationOptions = NotificationOptions.Builder() .setActions(buttonActions, compatButtonActionsIndices) .setSkipStepMs(30 * DateUtils.SECOND_IN_MILLIS) .setTargetActivityClassName(VideoBrowserActivity::class.java.name) .build()
// Example showing 4 buttons: "rewind", "play/pause", "forward" and "stop casting". List<String> buttonActions = new ArrayList<>(); buttonActions.add(MediaIntentReceiver.ACTION_REWIND); buttonActions.add(MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK); buttonActions.add(MediaIntentReceiver.ACTION_FORWARD); buttonActions.add(MediaIntentReceiver.ACTION_STOP_CASTING); // Showing "play/pause" and "stop casting" in the compat view of the notification. int[] compatButtonActionsIndices = new int[]{1, 3}; // Builds a notification with the above actions. Each tap on the "rewind" and "forward" buttons skips 30 seconds. // Tapping on the notification opens an Activity with class VideoBrowserActivity. NotificationOptions notificationOptions = new NotificationOptions.Builder() .setActions(buttonActions, compatButtonActionsIndices) .setSkipStepMs(30 * DateUtils.SECOND_IN_MILLIS) .setTargetActivityClassName(VideoBrowserActivity.class.getName()) .build();
Показ элементов управления мультимедиа в уведомлениях и на заблокированном экране включен по умолчанию. Его можно отключить, вызвав setNotificationOptions с нулевым значением в CastMediaOptions.Builder.
Сейчас функция блокировки экрана включена, если включены уведомления.
// ... continue with the NotificationOptions built above val mediaOptions = CastMediaOptions.Builder() .setNotificationOptions(notificationOptions) .build() val castOptions: CastOptions = Builder() .setReceiverApplicationId(context.getString(R.string.app_id)) .setCastMediaOptions(mediaOptions) .build()
// ... continue with the NotificationOptions built above CastMediaOptions mediaOptions = new CastMediaOptions.Builder() .setNotificationOptions(notificationOptions) .build(); CastOptions castOptions = new CastOptions.Builder() .setReceiverApplicationId(context.getString(R.string.app_id)) .setCastMediaOptions(mediaOptions) .build();
Когда приложение отправителя воспроизводит видео или аудио в прямом эфире, SDK автоматически показывает кнопку "Воспроизвести/Остановить" вместо кнопки "Воспроизвести/Пауза" в уведомлении, но не на заблокированном экране.
Примечание. Чтобы на устройствах с версией ОС ниже Lollipop на заблокированном экране отображались элементы управления,
RemoteMediaClientприложение "Google Play Музыка" автоматически запросит аудиофокус.
Обработка ошибок
Приложения отправителя должны обрабатывать все обратные вызовы ошибок и определять оптимальный ответ для каждого этапа жизненного цикла Cast. Приложение может показывать пользователю диалоговые окна с ошибками или разрывать соединение с веб-приемником.