Интегрируйте 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 для Android . В следующих разделах описываются шаги по добавлению Cast в ваше приложение Android.

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

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

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

Установите минимальный и целевой уровни API Android, которые поддерживает Cast SDK. В настоящее время минимальный уровень API — 23, а целевой уровень API — 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 , который отображается пользователю, чтобы привлечь внимание к кнопке Cast в первый раз, когда доступен получатель. Приложение Sender может настраивать текст и положение текста заголовка .

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

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

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

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

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

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

,

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

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

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

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

Установите минимальный и целевой уровни API Android, которые поддерживает Cast SDK. В настоящее время минимальный уровень API — 23, а целевой уровень API — 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 , который отображается пользователю, чтобы привлечь внимание к кнопке Cast в первый раз, когда доступен получатель. Приложение Sender может настраивать текст и положение текста заголовка .

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

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

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

Котлин
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 автоматически отображает кнопку воспроизведения/остановки вместо кнопки Play/Pause в мини -контроллере.

Чтобы установить внешний вид текста заголовка и подзаголовок этого пользовательского представления, и для выбора кнопок, см . Mini Controller .

Добавить расширенный контроллер

Контрольный список Design Design Google требует, чтобы приложение отправителя предоставило расширенный контроллер для составления медиа. Расширенный контроллер - это полная версия мини -контроллера.

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

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

<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
            android:id="@+id/media_route_menu_item"
            android:title="@string/media_route_menu_title"
            app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
            app:showAsAction="always"/>

</menu>

Создайте новый класс, который расширяет ExpandedControllerActivity .

Котлин
class ExpandedControlsActivity : ExpandedControllerActivity() {
    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        super.onCreateOptionsMenu(menu)
        menuInflater.inflate(R.menu.expanded_controller, menu)
        CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item)
        return true
    }
}
Ява
public class ExpandedControlsActivity extends ExpandedControllerActivity {
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        getMenuInflater().inflate(R.menu.expanded_controller, menu);
        CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item);
        return true;
    }
}

Теперь объявите вашу новую деятельность в манифесте приложения в рамках тега application :

<application>
...
<activity
        android:name=".expandedcontrols.ExpandedControlsActivity"
        android:label="@string/app_name"
        android:launchMode="singleTask"
        android:theme="@style/Theme.CastVideosDark"
        android:screenOrientation="portrait"
        android:parentActivityName="com.google.sample.cast.refplayer.VideoBrowserActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
    </intent-filter>
</activity>
...
</application>

Редактировать CastOptionsProvider и изменения NotificationOptions и CastMediaOptions , чтобы установить целевую деятельность на ваше новое действие:

Котлин
override fun getCastOptions(context: Context): CastOptions? {
    val notificationOptions = NotificationOptions.Builder()
        .setTargetActivityClassName(ExpandedControlsActivity::class.java.name)
        .build()
    val mediaOptions = CastMediaOptions.Builder()
        .setNotificationOptions(notificationOptions)
        .setExpandedControllerActivityClassName(ExpandedControlsActivity::class.java.name)
        .build()

    return CastOptions.Builder()
        .setReceiverApplicationId(context.getString(R.string.app_id))
        .setCastMediaOptions(mediaOptions)
        .build()
}
Ява
public CastOptions getCastOptions(Context context) {
    NotificationOptions notificationOptions = new NotificationOptions.Builder()
            .setTargetActivityClassName(ExpandedControlsActivity.class.getName())
            .build();
    CastMediaOptions mediaOptions = new CastMediaOptions.Builder()
            .setNotificationOptions(notificationOptions)
            .setExpandedControllerActivityClassName(ExpandedControlsActivity.class.getName())
            .build();

    return new CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .setCastMediaOptions(mediaOptions)
            .build();
}

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

Котлин
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 автоматически отображает кнопку воспроизведения/остановки вместо кнопки Play/Pause в расширенном контроллере.

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

Регулировка громкости

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

Управление громкостью физической кнопки

На Android физические кнопки на устройстве отправителя могут использоваться для изменения громкости актеров на веб -приемнике по умолчанию для любого устройства с использованием желе или новым.

Управление громкостью физической кнопки перед Jelly Bean

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

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 автоматически отображает кнопку воспроизведения/остановки вместо кнопки «Воспроизведение/пауза» на управлении уведомлением, но не на экране блокировки.

ПРИМЕЧАНИЕ RemoteMediaClient

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

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