Интегрируйте Cast в свое приложение для Android

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

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

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

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

Поток приложения

Следующие шаги описывают типичный поток выполнения высокого уровня для приложения-отправителя Android:

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

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

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

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

использует SDK

Установите минимальный и целевой уровни API Android, поддерживаемые Cast SDK. В настоящее время минимальный уровень API — 23, целевой — 34.

<uses-sdk
        android:minSdkVersion="23"
        android:targetSdkVersion="34" />

андроид:тема

Настройте тему приложения на основе минимальной версии Android SDK. Например, если вы не реализуете собственную тему, вам следует использовать вариант Theme.AppCompat , если минимальная версия Android SDK ниже Lollipop.

<application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/Theme.AppCompat" >
       ...
</application>

Инициализация контекста приведения

В фреймворке имеется глобальный одиночный объект CastContext , который координирует все взаимодействия фреймворка.

Ваше приложение должно реализовать интерфейс OptionsProvider для предоставления параметров, необходимых для инициализации синглтона CastContext . 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;
    }
}

Необходимо объявить полное имя реализованного 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() .

Котлин
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 , который отображается пользователю для привлечения внимания к кнопке трансляции при первом появлении получателя. Приложение Sender может настраивать текст и положение заголовка .

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

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

  • Расширенный контроллер : если пользователь транслирует контент, то при нажатии на уведомление мультимедиа или мини-контроллер запускается расширенный контроллер, который отображает метаданные воспроизводимого в данный момент мультимедиа и предоставляет несколько кнопок для управления воспроизведением мультимедиа.

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

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

Следующее руководство содержит описания того, как добавить эти виджеты в ваше приложение.

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

API-интерфейсы Android MediaRouter предназначены для отображения и воспроизведения медиаконтента на дополнительных устройствах. Приложения Android, использующие API MediaRouter , должны включать кнопку Cast в свой пользовательский интерфейс, чтобы пользователи могли выбирать маршрут воспроизведения медиаконтента на дополнительном устройстве, например, устройстве 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" />
Котлин
// 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);
}

Чтобы настроить внешний вид кнопки Cast с помощью темы, см. раздел Настройка кнопки Cast .

Настроить обнаружение устройств

Обнаружение устройств полностью управляется CastContext . При инициализации CastContext приложение-отправитель указывает идентификатор приложения Web Receiver и может при необходимости запросить фильтрацию пространств имён, установив параметр supportedNamespaces в CastOptions . CastContext содержит внутреннюю ссылку на MediaRouter и запускает процесс обнаружения при следующих условиях:

  • На основе алгоритма, разработанного для балансировки задержки обнаружения устройства и использования батареи, обнаружение иногда будет запускаться автоматически, когда приложение-отправитель переходит на передний план.
  • Открыто диалоговое окно трансляции.
  • 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 и жизненном цикле Web Receiver см. в руководстве по жизненному циклу приложения Web Receiver.

Сеансами управляет класс 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-файле включено автоматическое объединение.

Фреймворк запустит службу во время сеанса мультимедиа и остановит ее по завершении сеанса мультимедиа.

Как работает Media Control

Фреймворк Cast отказывается от использования класса RemoteMediaPlayer из Cast 2.x в пользу нового класса RemoteMediaClient , который предоставляет ту же функциональность в наборе более удобных API и позволяет избежать необходимости передавать GoogleApiClient.

Когда ваше приложение устанавливает CastSession с приложением Web Receiver, которое поддерживает пространство имен мультимедиа, фреймворк автоматически создает экземпляр 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 , приложение-отправитель должно предоставлять постоянный элемент управления, известный как мини-контроллер , который должен появляться при переходе пользователя с текущей страницы контента в другую часть приложения-отправителя. Мини-контроллер визуально напоминает пользователю о текущем сеансе Cast. Нажав на мини-контроллер, пользователь может вернуться к полноэкранному расширенному виду контроллера Cast.

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

<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 SDK предоставляет виджет для расширенного контроллера под названием ExpandedControllerActivity . Это абстрактный класс, который необходимо подклассифицировать для добавления кнопки Cast.

Сначала создайте новый файл ресурсов меню для расширенного контроллера, чтобы предоставить кнопку Cast:

<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 , чтобы отображать новую активность при загрузке удаленного мультимедиа:

Котлин
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 или более новой версии.

Физическая кнопка регулировки громкости до 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);
    }
}

Добавить элементы управления мультимедиа на экран уведомлений и блокировки

Только для Android, согласно контрольному списку дизайна Google Cast, приложение-отправитель должно реализовать элементы управления мультимедиа в уведомлении и на экране блокировки , когда отправитель транслирует контент, но приложение-отправитель не находится в фокусе. Фреймворк предоставляет 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 со значением null в 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 автоматически запросит аудиофокус от вашего имени.

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

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