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

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

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

Платформа отправителя относится к двоичному файлу библиотеки классов Cast и связанным с ним ресурсам, присутствующим во время выполнения на отправителе. Приложение-отправитель или приложение Cast относится к приложению, которое также работает на отправителе. Приложение «Веб-приемник» — это HTML-приложение, работающее на устройстве с поддержкой Cast.

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

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

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

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

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

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

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

использует-SDK

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

<uses-sdk
        android:minSdkVersion="21"
        android:targetSdkVersion="28" />

Android: тема

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

Котлин
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);
    }
}

UX-виджеты Cast

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

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

  • Кнопка трансляции : кнопка трансляции видна независимо от доступности устройств трансляции. Когда пользователь впервые нажимает кнопку 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);
}

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

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

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

    override fun onResume() {
        super.onResume()
        mCastSession = mSessionManager.currentCastSession
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession::class.java)
    }

    override fun onPause() {
        super.onPause()
        mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession::class.java)
        mCastSession = null
    }
}
Джава
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();
    }
    @Override
    protected void onResume() {
        super.onResume();
        mCastSession = mSessionManager.getCurrentCastSession();
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession.class);
    }
    @Override
    protected void onPause() {
        super.onPause();
        mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession.class);
        mCastSession = null;
    }
}

Потоковая передача

Сохранение состояния сеанса является основой передачи потока, при которой пользователи могут перемещать существующие аудио- и видеопотоки между устройствами с помощью голосовых команд, приложения Google Home или интеллектуальных дисплеев. Воспроизведение мультимедиа прекращается на одном устройстве (источнике) и продолжается на другом (назначении). Любое устройство Cast с последней версией прошивки может служить источником или пунктом назначения при потоковой передаче.

Чтобы получить новое целевое устройство во время передачи или расширения потока, зарегистрируйте Cast.Listener с помощью CastSession#addCastListener . Затем вызовите CastSession#getCastDevice() во время обратного вызова onDeviceNameChanged .

Дополнительную информацию см. в разделе Передача потока в веб-приемнике .

Автоматическое переподключение

Платформа предоставляет ReconnectionService , который может быть включен приложением-отправителем для обработки повторного подключения во многих сложных случаях, таких как:

  • Восстановление после временной потери Wi-Fi
  • Выход из спящего режима устройства
  • Восстановление из фонового режима приложения
  • Восстановление в случае сбоя приложения

Эта служба включена по умолчанию, и ее можно отключить в CastOptions.Builder .

Эту службу можно автоматически объединить с манифестом вашего приложения, если в вашем файле Gradle включено автоматическое объединение.

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

Как работает медиа-контроль

Платформа Cast отказывается от класса 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());

Также см. раздел об использовании медиадорожек .

Формат видео 4К

Чтобы проверить формат видео вашего медиа, используйте getVideoInfo() в MediaStatus, чтобы получить текущий экземпляр VideoInfo . Этот экземпляр содержит тип формата HDR TV, а также высоту и ширину дисплея в пикселях. Варианты формата 4К обозначаются константами HDR_TYPE_* .

Уведомления удаленного управления на несколько устройств

Когда пользователь выполняет трансляцию, другие устройства Android в той же сети получат уведомление, которое также позволит им управлять воспроизведением. Любой, чье устройство получает такие уведомления, может отключить их для этого устройства в приложении «Настройки», выбрав Google > Google Cast > «Показать уведомления удаленного управления» . (Уведомления включают ярлык для приложения «Настройки».) Более подробную информацию см. в разделе Трансляция уведомлений с пульта дистанционного управления .

Добавить мини-контроллер

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

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

Сначала создайте новый файл ресурсов меню для расширенного контроллера, чтобы предоставить кнопку 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();
}

Обновите метод loadRemoteMedia LocalPlayerActivity , чтобы отображать новую активность при загрузке удаленного носителя:

Котлин
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

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

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

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