Как интегрировать Cast в приложение для Android

В этом руководстве для разработчиков описывается, как добавить поддержку Google Cast в приложение-отправитель для Android с помощью Android Sender SDK.

Мобильное устройство или ноутбук – это отправитель, который управляет воспроизведением, а устройство Google Cast – это получатель, который показывает контент на телевизоре.

Фреймворк отправителя – это двоичный файл библиотеки классов Cast и связанные с ним ресурсы, которые используются отправителем во время выполнения. Приложение-отправитель или приложение Cast – это приложение, которое также работает на отправителе. Приложение Web Receiver – это HTML-приложение, которое работает на устройстве с поддержкой Cast.

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

Процесс в приложении

Ниже описаны основные этапы выполнения приложения Android-отправителя.

  • Фреймворк Cast автоматически начинает поиск устройств MediaRouter на основе жизненного цикла Activity.
  • Когда пользователь нажимает кнопку трансляции, фреймворк показывает диалоговое окно со списком обнаруженных устройств Cast.
  • Когда пользователь выбирает устройство Cast, фреймворк пытается запустить на нем приложение веб-приемника.
  • Фреймворк вызывает обратные вызовы в приложении-отправителе, чтобы подтвердить, что приложение Web Receiver было запущено.
  • Фреймворк создает канал связи между приложениями отправителя и веб-приемника.
  • Фреймворк использует канал связи для загрузки и управления воспроизведением медиафайлов на веб-приемнике.
  • Фреймворк синхронизирует состояние воспроизведения медиаконтента между отправителем и веб-приемником. Когда пользователь выполняет действия в интерфейсе отправителя, фреймворк передает эти запросы на управление медиаконтентом веб-приемнику. Когда веб-приемник отправляет обновления статуса медиаконтента, фреймворк обновляет состояние интерфейса отправителя.
  • Когда пользователь нажимает кнопку трансляции, чтобы отключиться от устройства Cast, фреймворк отключает приложение-отправитель от веб-приемника.

Полный список классов, методов и событий в Google Cast Android SDK можно найти в документации по Google Cast Sender API для Android. В следующих разделах описаны шаги по добавлению Cast в приложение для Android.

Настройте манифест Android

В файле AndroidManifest.xml приложения необходимо настроить следующие элементы для Cast SDK:

uses-sdk

Укажите минимальный и целевой уровни Android API, которые поддерживает 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, который содержит настройки, влияющие на поведение фреймворка. Самый важный из них – идентификатор приложения веб-приемника, который используется для фильтрации результатов поиска и запуска приложения веб-приемника при начале сеанса Cast.

Kotlin
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
    }
}
Java
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;
    }
}

Вам нужно объявить полное название реализованного класса OptionsProvider в качестве поля метаданных в файле AndroidManifest.xml приложения-отправителя:

<application>
    ...
    <meta-data
        android:name=
            "com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
        android:value="com.foo.CastOptionsProvider" />
</application>

CastContext инициализируется при вызове CastContext.getSharedInstance().

Kotlin
class MyActivity : FragmentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        val castContext = CastContext.getSharedInstance(this)
    }
}
Java
public class MyActivity extends FragmentActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        CastContext castContext = CastContext.getSharedInstance(this);
    }
}

Виджеты Cast UX

Фреймворк Cast предоставляет виджеты, соответствующие контрольному списку по дизайну Cast:

  • Вводный оверлей. Фреймворк предоставляет специальный объект View – IntroductoryOverlay, который показывается пользователю, чтобы привлечь внимание к кнопке трансляции, когда приемник становится доступен в первый раз. Приложение-отправитель может настраивать текст и положение заголовка.

  • Кнопка трансляции. Кнопка трансляции видна независимо от того, есть ли доступные устройства Cast. Когда пользователь нажимает кнопку трансляции, открывается диалоговое окно со списком обнаруженных устройств. Если нажать кнопку трансляции, когда устройство подключено, на экране появятся метаданные текущего медиаконтента (например, название, студия звукозаписи и значок) или возможность отключиться от устройства Google Cast. Кнопку трансляции иногда называют значком трансляции.

  • Мини-контроллер. Когда пользователь транслирует контент и переходит со страницы контента или развернутого контроллера на другой экран в приложении-отправителе, внизу экрана появляется мини-контроллер. С его помощью пользователь может видеть метаданные транслируемого контента и управлять воспроизведением.

  • Развернутый контроллер. Если пользователь нажмет на уведомление о медиаконтенте или мини-контроллер во время трансляции, откроется развернутый контроллер. В нем показываются метаданные воспроизводимого контента и кнопки для управления им.

  • Уведомление. Только для Android. Когда пользователь транслирует контент и переходит из приложения-отправителя в другое, появляется уведомление с метаданными транслируемого контента и элементами управления воспроизведением.

  • Заблокированный экран. Только для Android. Когда пользователь транслирует контент и переходит на заблокированный экран (или устройство переходит в режим ожидания), на заблокированном экране появляется элемент управления медиаконтентом, который показывает метаданные транслируемого контента и элементы управления воспроизведением.

Ниже описано, как добавить эти виджеты в приложение.

Добавьте кнопку трансляции

API MediaRouter для Android позволяют показывать и воспроизводить медиаконтент на дополнительных устройствах. В пользовательском интерфейсе приложений Android, использующих API MediaRouter, должна быть кнопка трансляции. С ее помощью пользователи могут выбирать маршрут передачи медиаконтента для воспроизведения на дополнительном устройстве, например устройстве Cast.

Фреймворк позволяет легко добавить 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" />
Kotlin
// 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
}
Java
// 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>
Kotlin
// 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)
}
Java
// 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 будет закрыто или приложение-отправитель перейдет в фоновый режим.

Kotlin
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
    }
}
Java
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, который включает в себя подключение к устройству, запуск веб-приложения получателя (или присоединение к нему), подключение к этому приложению и инициализацию канала управления медиаконтентом. Дополнительную информацию о сеансах Cast и жизненном цикле веб-приемника можно найти в руководстве по жизненному циклу веб-приемника.

Сеансы управляются классом SessionManager, к которому приложение может получить доступ через CastContext.getSessionManager(). Отдельные сеансы представлены подклассами класса Session. Например, CastSession – это сеансы на устройствах Cast. Приложение может получить доступ к текущему активному сеансу Cast через SessionManager.getCurrentCastSession().

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

Сеансы создаются и удаляются автоматически в ответ на жесты пользователя в диалоговых окнах MediaRouter.

Чтобы лучше понять ошибки запуска Cast, приложения могут использовать CastContext#getCastReasonCodeForCastStatusCode(int) для преобразования ошибки запуска сеанса в CastReasonCodes. Обратите внимание, что некоторые ошибки при запуске сеанса (например, CastReasonCodes#CAST_CANCELLED) являются ожидаемым поведением и не должны регистрироваться как ошибки.

Если вам нужно отслеживать изменения статуса сеанса, вы можете реализовать SessionManagerListener. В этом примере отслеживается доступность CastSession в Activity.

Kotlin
class MyActivity : Activity() {
    private var mCastSession: CastSession? = null
    private lateinit var mCastContext: CastContext
    private lateinit var mSessionManager: SessionManager
    private val mSessionManagerListener: SessionManagerListener<CastSession> =
        SessionManagerListenerImpl()

    private 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)
    }
}
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 2.x класс RemoteMediaPlayer больше не поддерживается. Вместо него используется класс RemoteMediaClient, который предоставляет те же функции в более удобных API и не требует передачи GoogleApiClient.

Когда ваше приложение устанавливает CastSession с веб-приемником, поддерживающим пространство имен мультимедиа, фреймворк автоматически создает экземпляр RemoteMediaClient. Ваше приложение может получить к нему доступ, вызвав метод getRemoteMediaClient() для экземпляра CastSession.

Все методы RemoteMediaClient, которые отправляют запросы веб-приемнику, возвращают объект PendingResult, который можно использовать для отслеживания запроса.

Экземпляр RemoteMediaClient может использоваться разными частями приложения и некоторыми внутренними компонентами фреймворка, например постоянными мини-контроллерами и сервисом уведомлений. Для этого данный экземпляр поддерживает регистрацию нескольких экземпляров RemoteMediaClient.Listener.

Как задать метаданные медиаконтента

Класс MediaMetadata содержит информацию о мультимедийном объекте, который вы хотите транслировать. В следующем примере создается новый экземпляр MediaMetadata для фильма и задаются название, подзаголовок и два изображения.

Kotlin
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))))
Java
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, чтобы управлять воспроизведением в приложении медиапроигрывателя, запущенном на веб-приемнике.

Kotlin
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())
Java
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, в приложении-отправителе должен быть постоянный элемент управления, называемый мини-контроллером. Он должен появляться, когда пользователь переходит со страницы текущего контента в другой раздел приложения-отправителя. Мини-контроллер служит видимым напоминанием о текущем сеансе 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 Design Checklist, приложение-отправитель должно предоставлять расширенный контроллер для транслируемого медиаконтента. Развернутый контроллер – это полноэкранная версия мини-контроллера.

В 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.

Kotlin
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
    }
}
Java
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, чтобы задать новое целевое действие:

Kotlin
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()
}
Java
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, чтобы при загрузке удаленного медиаконтента показывалось новое действие:

Kotlin
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()
    )
}
Java
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 физические кнопки на устройстве-отправителе можно использовать для изменения громкости сеанса Cast на веб-приемнике по умолчанию для любого устройства, использующего Jelly Bean или более новую версию.

Физическая кнопка регулировки громкости до версии Jelly Bean

Чтобы использовать физические клавиши громкости для управления громкостью веб-приемника на устройствах Android старше Jelly Bean, приложение-отправитель должно переопределить dispatchKeyEvent в своих действиях и вызвать CastContext.onDispatchVolumeKeyEventBeforeJellyBean():

Kotlin
class MyActivity : FragmentActivity() {
    override fun dispatchKeyEvent(event: KeyEvent): Boolean {
        return (CastContext.getSharedInstance(this)
            .onDispatchVolumeKeyEventBeforeJellyBean(event)
                || super.dispatchKeyEvent(event))
    }
}
Java
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.

Kotlin
// 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()
Java
// 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. Сейчас функция заблокированного экрана включена, пока включены уведомления.

Kotlin
// ... 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()
Java
// ... 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 автоматически запрашивает аудиофокус от вашего имени.

Обработка ошибок

Очень важно, чтобы приложения-отправители обрабатывали все обратные вызовы ошибок и определяли наилучший ответ для каждого этапа жизненного цикла Cast. Приложение может показывать пользователю диалоговые окна с ошибками или разрывать соединение с веб-приемником.