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

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

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

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

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

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

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

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

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

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

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

uses-sdk

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

<uses-sdk
        android:minSdkVersion="24"
        android:targetSdkVersion="35" />

android:theme

Настройте тему оформления вашего приложения в соответствии с минимальной версией 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
    }
}
Java
public class CastOptionsProvider implements OptionsProvider {
    @Override
    public CastOptions getCastOptions(Context context) {
        CastOptions castOptions = new CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .build();
        return castOptions;
    }
    @Override
    public List<SessionProvider> getAdditionalSessionProviders(Context context) {
        return null;
    }
}

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

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

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

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

Виджеты Cast UX

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

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

  • Кнопка 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
}
Java
// Then override the onCreateOptionMenu() for each of your activities.
// MyActivity.java
@Override public boolean onCreateOptionsMenu(Menu menu) {
    super.onCreateOptionsMenu(menu);
    getMenuInflater().inflate(R.menu.main, menu);
    CastButtonFactory.setUpMediaRouteButton(getApplicationContext(),
                                            menu,
                                            R.id.media_route_menu_item);
    return true;
}

Затем, если ваша Activity наследуется от FragmentActivity , вы можете добавить MediaRouteButton в свой макет.

// activity_layout.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:gravity="center_vertical"
   android:orientation="horizontal" >

   <androidx.mediarouter.app.MediaRouteButton
       android:id="@+id/media_route_button"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_weight="1"
       android:mediaRouteTypes="user"
       android:visibility="gone" />

</LinearLayout>
Котлин
// MyActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_layout)

    mMediaRouteButton = findViewById<View>(R.id.media_route_button) as MediaRouteButton
    CastButtonFactory.setUpMediaRouteButton(applicationContext, mMediaRouteButton)

    mCastContext = CastContext.getSharedInstance(this)
}
Java
// MyActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_layout);

   mMediaRouteButton = (MediaRouteButton) findViewById(R.id.media_route_button);
   CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), mMediaRouteButton);

   mCastContext = CastContext.getSharedInstance(this);
}

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

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

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

  • На основе алгоритма, разработанного для балансировки задержки обнаружения устройств и расхода заряда батареи, обнаружение будет периодически запускаться автоматически, когда приложение отправителя переходит в активный режим.
  • Диалоговое окно Cast открыто.
  • SDK Cast пытается восстановить сессию 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
    }
}
Java
class CastOptionsProvider implements OptionsProvider {
    public static final String CUSTOM_NAMESPACE = "urn:x-cast:custom_namespace";

    @Override
    public CastOptions getCastOptions(Context appContext) {
        List<String> supportedNamespaces = new ArrayList<>();
        supportedNamespaces.add(CUSTOM_NAMESPACE);

        CastOptions castOptions = new CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .setSupportedNamespaces(supportedNamespaces)
            .build();
        return castOptions;
    }

    @Override
    public List<SessionProvider> getAdditionalSessionProviders(Context context) {
        return null;
    }
}

Как работает управление сессиями

SDK Cast представляет концепцию сессии Cast, установление которой объединяет шаги подключения к устройству, запуска (или присоединения) приложения Web Receiver, подключения к этому приложению и инициализации канала управления мультимедиа. Дополнительную информацию о сессиях Cast и жизненном цикле Web Receiver см. в руководстве по жизненному циклу приложения Web Receiver.

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

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

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

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

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

Котлин
class MyActivity : Activity() {
    private var mCastSession: CastSession? = null
    private lateinit var mCastContext: CastContext
    private lateinit var mSessionManager: SessionManager
    private val mSessionManagerListener: SessionManagerListener<CastSession> =
        SessionManagerListenerImpl()

    private inner class SessionManagerListenerImpl : SessionManagerListener<CastSession?> {
        override fun onSessionStarting(session: CastSession?) {}

        override fun onSessionStarted(session: CastSession?, sessionId: String) {
            invalidateOptionsMenu()
        }

        override fun onSessionStartFailed(session: CastSession?, error: Int) {
            val castReasonCode = mCastContext.getCastReasonCodeForCastStatusCode(error)
            // Handle error
        }

        override fun onSessionSuspended(session: CastSession?, reason Int) {}

        override fun onSessionResuming(session: CastSession?, sessionId: String) {}

        override fun onSessionResumed(session: CastSession?, wasSuspended: Boolean) {
            invalidateOptionsMenu()
        }

        override fun onSessionResumeFailed(session: CastSession?, error: Int) {}

        override fun onSessionEnding(session: CastSession?) {}

        override fun onSessionEnded(session: CastSession?, error: Int) {
            finish()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mCastContext = CastContext.getSharedInstance(this)
        mSessionManager = mCastContext.sessionManager
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession::class.java)
    }

    override fun onResume() {
        super.onResume()
        mCastSession = mSessionManager.currentCastSession
    }

    override fun onDestroy() {
        super.onDestroy()
        mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession::class.java)
    }
}
Java
public class MyActivity extends Activity {
    private CastContext mCastContext;
    private CastSession mCastSession;
    private SessionManager mSessionManager;
    private SessionManagerListener<CastSession> mSessionManagerListener =
            new SessionManagerListenerImpl();

    private class SessionManagerListenerImpl implements SessionManagerListener<CastSession> {
        @Override
        public void onSessionStarting(CastSession session) {}
        @Override
        public void onSessionStarted(CastSession session, String sessionId) {
            invalidateOptionsMenu();
        }
        @Override
        public void onSessionStartFailed(CastSession session, int error) {
            int castReasonCode = mCastContext.getCastReasonCodeForCastStatusCode(error);
            // Handle error
        }
        @Override
        public void onSessionSuspended(CastSession session, int reason) {}
        @Override
        public void onSessionResuming(CastSession session, String sessionId) {}
        @Override
        public void onSessionResumed(CastSession session, boolean wasSuspended) {
            invalidateOptionsMenu();
        }
        @Override
        public void onSessionResumeFailed(CastSession session, int error) {}
        @Override
        public void onSessionEnding(CastSession session) {}
        @Override
        public void onSessionEnded(CastSession session, int error) {
            finish();
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mCastContext = CastContext.getSharedInstance(this);
        mSessionManager = mCastContext.getSessionManager();
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession.class);
    }

    @Override
    protected void onResume() {
        super.onResume();
        mCastSession = mSessionManager.getCurrentCastSession();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession.class);
    }
}

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

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

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

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

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

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

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

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

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

Данная платформа запустит службу при наличии медиасессии и остановит её при завершении медиасессии.

Как работает управление медиаконтентом

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

Когда ваше приложение устанавливает CastSession с приложением Web Receiver, поддерживающим пространство имен media, фреймворк автоматически создаст экземпляр 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))))
Java
MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);

movieMetadata.putString(MediaMetadata.KEY_TITLE, mSelectedMedia.getTitle());
movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, mSelectedMedia.getStudio());
movieMetadata.addImage(new WebImage(Uri.parse(mSelectedMedia.getImage(0))));
movieMetadata.addImage(new WebImage(Uri.parse(mSelectedMedia.getImage(1))));

См. раздел «Выбор изображений» о применении изображений с медиа-метаданными.

Загрузить медиафайл

Ваше приложение может загружать медиафайлы, как показано в следующем коде. Сначала используйте MediaInfo.Builder с метаданными медиафайла, чтобы создать экземпляр MediaInfo . Получите RemoteMediaClient из текущей CastSession , затем загрузите MediaInfo в этот RemoteMediaClient . Используйте RemoteMediaClient для воспроизведения, паузы и управления приложением медиаплеера, работающим на 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())
Java
MediaInfo mediaInfo = new MediaInfo.Builder(mSelectedMedia.getUrl())
        .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
        .setContentType("videos/mp4")
        .setMetadata(movieMetadata)
        .setStreamDuration(mSelectedMedia.getDuration() * 1000)
        .build();
RemoteMediaClient remoteMediaClient = mCastSession.getRemoteMediaClient();
remoteMediaClient.load(new MediaLoadRequestData.Builder().setMediaInfo(mediaInfo).build());

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

формат видео 4K

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

Дистанционное управление уведомлениями на нескольких устройствах

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

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

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

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

<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, приложение-отправитель должно предоставлять расширенный контроллер для транслируемого медиаконтента. Расширенный контроллер представляет собой полноэкранную версию мини-контроллера.

SDK Cast предоставляет виджет для расширенного контроллера, называемый 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
    }
}
Java
public class ExpandedControlsActivity extends ExpandedControllerActivity {
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        getMenuInflater().inflate(R.menu.expanded_controller, menu);
        CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item);
        return true;
    }
}

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

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

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

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

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

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

Обновите метод 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()
    )
}
Java
private void loadRemoteMedia(int position, boolean autoPlay) {
    if (mCastSession == null) {
        return;
    }
    final RemoteMediaClient remoteMediaClient = mCastSession.getRemoteMediaClient();
    if (remoteMediaClient == null) {
        return;
    }
    remoteMediaClient.registerCallback(new RemoteMediaClient.Callback() {
        @Override
        public void onStatusUpdated() {
            Intent intent = new Intent(LocalPlayerActivity.this, ExpandedControlsActivity.class);
            startActivity(intent);
            remoteMediaClient.unregisterCallback(this);
        }
    });
    remoteMediaClient.load(new MediaLoadRequestData.Builder()
            .setMediaInfo(mSelectedMedia)
            .setAutoplay(autoPlay)
            .setCurrentTime(position).build());
}

Когда ваше приложение-отправитель воспроизводит видео- или аудиопоток в режиме реального времени, SDK автоматически отображает кнопку воспроизведения/остановки вместо кнопки воспроизведения/паузы в расширенном контроллере.

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

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

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

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

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

До появления Jelly Bean регулировка громкости осуществлялась с помощью физических кнопок.

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

Котлин
class MyActivity : FragmentActivity() {
    override fun dispatchKeyEvent(event: KeyEvent): Boolean {
        return (CastContext.getSharedInstance(this)
            .onDispatchVolumeKeyEventBeforeJellyBean(event)
                || super.dispatchKeyEvent(event))
    }
}
Java
class MyActivity extends FragmentActivity {
    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        return CastContext.getSharedInstance(this)
            .onDispatchVolumeKeyEventBeforeJellyBean(event)
            || super.dispatchKeyEvent(event);
    }
}

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

Только для 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()
Java
// Example showing 4 buttons: "rewind", "play/pause", "forward" and "stop casting".
List<String> buttonActions = new ArrayList<>();
buttonActions.add(MediaIntentReceiver.ACTION_REWIND);
buttonActions.add(MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK);
buttonActions.add(MediaIntentReceiver.ACTION_FORWARD);
buttonActions.add(MediaIntentReceiver.ACTION_STOP_CASTING);

// Showing "play/pause" and "stop casting" in the compat view of the notification.
int[] compatButtonActionsIndices = new int[]{1, 3};

// Builds a notification with the above actions. Each tap on the "rewind" and "forward" buttons skips 30 seconds.
// Tapping on the notification opens an Activity with class VideoBrowserActivity.
NotificationOptions notificationOptions = new NotificationOptions.Builder()
    .setActions(buttonActions, compatButtonActionsIndices)
    .setSkipStepMs(30 * DateUtils.SECOND_IN_MILLIS)
    .setTargetActivityClassName(VideoBrowserActivity.class.getName())
    .build();

Отображение элементов управления мультимедиа на экране уведомлений и экране блокировки включено по умолчанию и может быть отключено путем вызова метода setNotificationOptions со значением 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()
Java
// ... continue with the NotificationOptions built above
CastMediaOptions mediaOptions = new CastMediaOptions.Builder()
        .setNotificationOptions(notificationOptions)
        .build();
CastOptions castOptions = new CastOptions.Builder()
        .setReceiverApplicationId(context.getString(R.string.app_id))
        .setCastMediaOptions(mediaOptions)
        .build();

Когда ваше приложение-отправитель воспроизводит видео- или аудиопоток в режиме реального времени, SDK автоматически отображает кнопку воспроизведения/остановки вместо кнопки воспроизведения/паузы в элементе управления уведомлениями, но не в элементе управления на экране блокировки.

Примечание : Для отображения элементов управления на экране блокировки на устройствах с версиями Android ниже Lollipop, RemoteMediaClient автоматически запросит фокусировку звука от вашего имени.

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

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

,

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

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

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

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

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

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

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

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

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

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

uses-sdk

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

<uses-sdk
        android:minSdkVersion="24"
        android:targetSdkVersion="35" />

android:theme

Настройте тему оформления вашего приложения в соответствии с минимальной версией 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
    }
}
Java
public class CastOptionsProvider implements OptionsProvider {
    @Override
    public CastOptions getCastOptions(Context context) {
        CastOptions castOptions = new CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .build();
        return castOptions;
    }
    @Override
    public List<SessionProvider> getAdditionalSessionProviders(Context context) {
        return null;
    }
}

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

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

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

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

Виджеты Cast UX

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

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

  • Кнопка 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
}
Java
// Then override the onCreateOptionMenu() for each of your activities.
// MyActivity.java
@Override public boolean onCreateOptionsMenu(Menu menu) {
    super.onCreateOptionsMenu(menu);
    getMenuInflater().inflate(R.menu.main, menu);
    CastButtonFactory.setUpMediaRouteButton(getApplicationContext(),
                                            menu,
                                            R.id.media_route_menu_item);
    return true;
}

Затем, если ваша Activity наследуется от FragmentActivity , вы можете добавить MediaRouteButton в свой макет.

// activity_layout.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:gravity="center_vertical"
   android:orientation="horizontal" >

   <androidx.mediarouter.app.MediaRouteButton
       android:id="@+id/media_route_button"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_weight="1"
       android:mediaRouteTypes="user"
       android:visibility="gone" />

</LinearLayout>
Котлин
// MyActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_layout)

    mMediaRouteButton = findViewById<View>(R.id.media_route_button) as MediaRouteButton
    CastButtonFactory.setUpMediaRouteButton(applicationContext, mMediaRouteButton)

    mCastContext = CastContext.getSharedInstance(this)
}
Java
// MyActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_layout);

   mMediaRouteButton = (MediaRouteButton) findViewById(R.id.media_route_button);
   CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), mMediaRouteButton);

   mCastContext = CastContext.getSharedInstance(this);
}

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

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

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

  • На основе алгоритма, разработанного для балансировки задержки обнаружения устройств и расхода заряда батареи, обнаружение будет периодически запускаться автоматически, когда приложение отправителя переходит в активный режим.
  • Диалоговое окно Cast открыто.
  • SDK Cast пытается восстановить сессию 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
    }
}
Java
class CastOptionsProvider implements OptionsProvider {
    public static final String CUSTOM_NAMESPACE = "urn:x-cast:custom_namespace";

    @Override
    public CastOptions getCastOptions(Context appContext) {
        List<String> supportedNamespaces = new ArrayList<>();
        supportedNamespaces.add(CUSTOM_NAMESPACE);

        CastOptions castOptions = new CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .setSupportedNamespaces(supportedNamespaces)
            .build();
        return castOptions;
    }

    @Override
    public List<SessionProvider> getAdditionalSessionProviders(Context context) {
        return null;
    }
}

Как работает управление сессиями

SDK Cast представляет концепцию сессии Cast, установление которой объединяет шаги подключения к устройству, запуска (или присоединения) приложения Web Receiver, подключения к этому приложению и инициализации канала управления мультимедиа. Дополнительную информацию о сессиях Cast и жизненном цикле Web Receiver см. в руководстве по жизненному циклу приложения Web Receiver.

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

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

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

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

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

Котлин
class MyActivity : Activity() {
    private var mCastSession: CastSession? = null
    private lateinit var mCastContext: CastContext
    private lateinit var mSessionManager: SessionManager
    private val mSessionManagerListener: SessionManagerListener<CastSession> =
        SessionManagerListenerImpl()

    private inner class SessionManagerListenerImpl : SessionManagerListener<CastSession?> {
        override fun onSessionStarting(session: CastSession?) {}

        override fun onSessionStarted(session: CastSession?, sessionId: String) {
            invalidateOptionsMenu()
        }

        override fun onSessionStartFailed(session: CastSession?, error: Int) {
            val castReasonCode = mCastContext.getCastReasonCodeForCastStatusCode(error)
            // Handle error
        }

        override fun onSessionSuspended(session: CastSession?, reason Int) {}

        override fun onSessionResuming(session: CastSession?, sessionId: String) {}

        override fun onSessionResumed(session: CastSession?, wasSuspended: Boolean) {
            invalidateOptionsMenu()
        }

        override fun onSessionResumeFailed(session: CastSession?, error: Int) {}

        override fun onSessionEnding(session: CastSession?) {}

        override fun onSessionEnded(session: CastSession?, error: Int) {
            finish()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mCastContext = CastContext.getSharedInstance(this)
        mSessionManager = mCastContext.sessionManager
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession::class.java)
    }

    override fun onResume() {
        super.onResume()
        mCastSession = mSessionManager.currentCastSession
    }

    override fun onDestroy() {
        super.onDestroy()
        mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession::class.java)
    }
}
Java
public class MyActivity extends Activity {
    private CastContext mCastContext;
    private CastSession mCastSession;
    private SessionManager mSessionManager;
    private SessionManagerListener<CastSession> mSessionManagerListener =
            new SessionManagerListenerImpl();

    private class SessionManagerListenerImpl implements SessionManagerListener<CastSession> {
        @Override
        public void onSessionStarting(CastSession session) {}
        @Override
        public void onSessionStarted(CastSession session, String sessionId) {
            invalidateOptionsMenu();
        }
        @Override
        public void onSessionStartFailed(CastSession session, int error) {
            int castReasonCode = mCastContext.getCastReasonCodeForCastStatusCode(error);
            // Handle error
        }
        @Override
        public void onSessionSuspended(CastSession session, int reason) {}
        @Override
        public void onSessionResuming(CastSession session, String sessionId) {}
        @Override
        public void onSessionResumed(CastSession session, boolean wasSuspended) {
            invalidateOptionsMenu();
        }
        @Override
        public void onSessionResumeFailed(CastSession session, int error) {}
        @Override
        public void onSessionEnding(CastSession session) {}
        @Override
        public void onSessionEnded(CastSession session, int error) {
            finish();
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mCastContext = CastContext.getSharedInstance(this);
        mSessionManager = mCastContext.getSessionManager();
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession.class);
    }

    @Override
    protected void onResume() {
        super.onResume();
        mCastSession = mSessionManager.getCurrentCastSession();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession.class);
    }
}

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

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

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

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

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

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

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

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

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

Данная платформа запустит службу при наличии медиасессии и остановит её при завершении медиасессии.

Как работает управление медиаконтентом

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

Когда ваше приложение устанавливает CastSession с приложением Web Receiver, поддерживающим пространство имен media, фреймворк автоматически создаст экземпляр 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))))
Java
MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);

movieMetadata.putString(MediaMetadata.KEY_TITLE, mSelectedMedia.getTitle());
movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, mSelectedMedia.getStudio());
movieMetadata.addImage(new WebImage(Uri.parse(mSelectedMedia.getImage(0))));
movieMetadata.addImage(new WebImage(Uri.parse(mSelectedMedia.getImage(1))));

См. раздел «Выбор изображений» о применении изображений с медиа-метаданными.

Загрузить медиафайл

Ваше приложение может загружать медиафайлы, как показано в следующем коде. Сначала используйте MediaInfo.Builder с метаданными медиафайла, чтобы создать экземпляр MediaInfo . Получите RemoteMediaClient из текущей CastSession , затем загрузите MediaInfo в этот RemoteMediaClient . Используйте RemoteMediaClient для воспроизведения, паузы и управления приложением медиаплеера, работающим на 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())
Java
MediaInfo mediaInfo = new MediaInfo.Builder(mSelectedMedia.getUrl())
        .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
        .setContentType("videos/mp4")
        .setMetadata(movieMetadata)
        .setStreamDuration(mSelectedMedia.getDuration() * 1000)
        .build();
RemoteMediaClient remoteMediaClient = mCastSession.getRemoteMediaClient();
remoteMediaClient.load(new MediaLoadRequestData.Builder().setMediaInfo(mediaInfo).build());

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

видеоформат 4K

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

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

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

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

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

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

<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" />

When your sender app is playing a video or audio live stream, the SDK automatically displays a play/stop button in place of the play/pause button in the mini controller.

To set the text appearance of the title and subtitle of this custom view, and to choose buttons, see Customize Mini Controller .

Add expanded controller

The Google Cast Design Checklist requires that a sender app provide an expanded controller for the media being Cast. The expanded controller is a full screen version of the mini controller.

The Cast SDK provides a widget for the expanded controller called ExpandedControllerActivity . This is an abstract class you have to subclass to add a Cast button.

First, create a new menu resource file for the expanded controller to provide the Cast button:

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

Create a new class that extends 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
    }
}
Java
public class ExpandedControlsActivity extends ExpandedControllerActivity {
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        getMenuInflater().inflate(R.menu.expanded_controller, menu);
        CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item);
        return true;
    }
}

Now declare your new activity in the app manifest within the application tag:

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

Edit the CastOptionsProvider and change NotificationOptions and CastMediaOptions to set the target activity to your new activity:

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

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

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

Update the LocalPlayerActivity loadRemoteMedia method to display your new activity when the remote media is loaded:

Котлин
private fun loadRemoteMedia(position: Int, autoPlay: Boolean) {
    val remoteMediaClient = mCastSession?.remoteMediaClient ?: return

    remoteMediaClient.registerCallback(object : RemoteMediaClient.Callback() {
        override fun onStatusUpdated() {
            val intent = Intent(this@LocalPlayerActivity, ExpandedControlsActivity::class.java)
            startActivity(intent)
            remoteMediaClient.unregisterCallback(this)
        }
    })

    remoteMediaClient.load(
        MediaLoadRequestData.Builder()
            .setMediaInfo(mSelectedMedia)
            .setAutoplay(autoPlay)
            .setCurrentTime(position.toLong()).build()
    )
}
Java
private void loadRemoteMedia(int position, boolean autoPlay) {
    if (mCastSession == null) {
        return;
    }
    final RemoteMediaClient remoteMediaClient = mCastSession.getRemoteMediaClient();
    if (remoteMediaClient == null) {
        return;
    }
    remoteMediaClient.registerCallback(new RemoteMediaClient.Callback() {
        @Override
        public void onStatusUpdated() {
            Intent intent = new Intent(LocalPlayerActivity.this, ExpandedControlsActivity.class);
            startActivity(intent);
            remoteMediaClient.unregisterCallback(this);
        }
    });
    remoteMediaClient.load(new MediaLoadRequestData.Builder()
            .setMediaInfo(mSelectedMedia)
            .setAutoplay(autoPlay)
            .setCurrentTime(position).build());
}

When your sender app is playing a video or audio live stream, the SDK automatically displays a play/stop button in place of the play/pause button in the expanded controller.

To set the appearance using themes, choose which buttons to display, and add custom buttons, see Customize Expanded Controller .

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

The framework automatically manages the volume for the sender app. The framework automatically synchronizes the sender and Web Receiver apps so that the sender UI always reports the volume specified by the Web Receiver.

Physical button volume control

On Android, the physical buttons on the sender device can be used to change the volume of the Cast session on the Web Receiver by default for any device using Jelly Bean or newer.

Physical button volume control prior to Jelly Bean

To use the physical volume keys to control the Web Receiver device volume on Android devices older than Jelly Bean, the sender app should override dispatchKeyEvent in their Activities, and call CastContext.onDispatchVolumeKeyEventBeforeJellyBean() :

Котлин
class MyActivity : FragmentActivity() {
    override fun dispatchKeyEvent(event: KeyEvent): Boolean {
        return (CastContext.getSharedInstance(this)
            .onDispatchVolumeKeyEventBeforeJellyBean(event)
                || super.dispatchKeyEvent(event))
    }
}
Java
class MyActivity extends FragmentActivity {
    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        return CastContext.getSharedInstance(this)
            .onDispatchVolumeKeyEventBeforeJellyBean(event)
            || super.dispatchKeyEvent(event);
    }
}

Add media controls to notification and lock screen

On Android only, the Google Cast Design Checklist requires a sender app to implement media controls in a notification and in the lock screen , where the sender is casting but the sender app does not have focus. The framework provides MediaNotificationService and MediaIntentReceiver to help the sender app build media controls in a notification and in the lock screen.

MediaNotificationService runs when the sender is casting, and will show a notification with image thumbnail and information about the current casting item, a play/pause button and a stop button.

MediaIntentReceiver is a BroadcastReceiver that handles user actions from the notification.

Your app can configure notification and media control from lock screen through NotificationOptions . Your app can configure what control buttons to show in the notification, and which Activity to open when the notification is tapped by the user. If actions are not explicitly provided, the default values, MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK and MediaIntentReceiver.ACTION_STOP_CASTING will be used.

Котлин
// Example showing 4 buttons: "rewind", "play/pause", "forward" and "stop casting".
val buttonActions: MutableList<String> = ArrayList()
buttonActions.add(MediaIntentReceiver.ACTION_REWIND)
buttonActions.add(MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK)
buttonActions.add(MediaIntentReceiver.ACTION_FORWARD)
buttonActions.add(MediaIntentReceiver.ACTION_STOP_CASTING)

// Showing "play/pause" and "stop casting" in the compat view of the notification.
val compatButtonActionsIndices = intArrayOf(1, 3)

// Builds a notification with the above actions. Each tap on the "rewind" and "forward" buttons skips 30 seconds.
// Tapping on the notification opens an Activity with class VideoBrowserActivity.
val notificationOptions = NotificationOptions.Builder()
    .setActions(buttonActions, compatButtonActionsIndices)
    .setSkipStepMs(30 * DateUtils.SECOND_IN_MILLIS)
    .setTargetActivityClassName(VideoBrowserActivity::class.java.name)
    .build()
Java
// Example showing 4 buttons: "rewind", "play/pause", "forward" and "stop casting".
List<String> buttonActions = new ArrayList<>();
buttonActions.add(MediaIntentReceiver.ACTION_REWIND);
buttonActions.add(MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK);
buttonActions.add(MediaIntentReceiver.ACTION_FORWARD);
buttonActions.add(MediaIntentReceiver.ACTION_STOP_CASTING);

// Showing "play/pause" and "stop casting" in the compat view of the notification.
int[] compatButtonActionsIndices = new int[]{1, 3};

// Builds a notification with the above actions. Each tap on the "rewind" and "forward" buttons skips 30 seconds.
// Tapping on the notification opens an Activity with class VideoBrowserActivity.
NotificationOptions notificationOptions = new NotificationOptions.Builder()
    .setActions(buttonActions, compatButtonActionsIndices)
    .setSkipStepMs(30 * DateUtils.SECOND_IN_MILLIS)
    .setTargetActivityClassName(VideoBrowserActivity.class.getName())
    .build();

Showing media controls from notification and lock screen are turned on by default, and can disabled by calling setNotificationOptions with null in CastMediaOptions.Builder . Currently, the lock screen feature is turned on as long as notification is turned on.

Котлин
// ... continue with the NotificationOptions built above
val mediaOptions = CastMediaOptions.Builder()
    .setNotificationOptions(notificationOptions)
    .build()
val castOptions: CastOptions = Builder()
    .setReceiverApplicationId(context.getString(R.string.app_id))
    .setCastMediaOptions(mediaOptions)
    .build()
Java
// ... continue with the NotificationOptions built above
CastMediaOptions mediaOptions = new CastMediaOptions.Builder()
        .setNotificationOptions(notificationOptions)
        .build();
CastOptions castOptions = new CastOptions.Builder()
        .setReceiverApplicationId(context.getString(R.string.app_id))
        .setCastMediaOptions(mediaOptions)
        .build();

When your sender app is playing a video or audio live stream, the SDK automatically displays a play/stop button in place of the play/pause button on the notification control but not the lock screen control.

Note : To display lock screen controls on pre-Lollipop devices, RemoteMediaClient will automatically request audio focus on your behalf.

Handle errors

It is very important for sender apps to handle all error callbacks and decide the best response for each stage of the Cast life cycle. The app can display error dialogs to the user or it can decide to tear down the connection to the Web Receiver.

,

This developer guide describes how to add Google Cast support to your Android sender app using the Android Sender SDK.

The mobile device or laptop is the sender which controls the playback, and the Google Cast device is the Receiver which displays the content on the TV.

The sender framework refers to the Cast class library binary and associated resources present at runtime on the sender. The sender app or Cast app refers to an app also running on the sender. The Web Receiver app refers to the HTML application running on the Cast-enabled device.

The sender framework uses an asynchronous callback design to inform the sender app of events and to transition between various states of the Cast app life cycle.

App flow

The following steps describe the typical high-level execution flow for a sender Android app:

  • The Cast framework automatically starts MediaRouter device discovery based on the Activity lifecycle.
  • When the user clicks on the Cast button, the framework presents the Cast dialog with the list of discovered Cast devices.
  • When the user selects a Cast device, the framework attempts to launch the Web Receiver app on the Cast device.
  • The framework invokes callbacks in the sender app to confirm that the Web Receiver app was launched.
  • The framework creates a communication channel between the sender and Web Receiver apps.
  • The framework uses the communication channel to load and control media playback on the Web Receiver.
  • The framework synchronizes the media playback state between sender and Web Receiver: when the user makes sender UI actions, the framework passes those media control requests to the Web Receiver, and when the Web Receiver sends media status updates, the framework updates the state of the sender UI.
  • When the user clicks on the Cast button to disconnect from the Cast device, the framework will disconnect the sender app from the Web Receiver.

For a comprehensive list of all classes, methods and events in the Google Cast Android SDK, see the Google Cast Sender API Reference for Android . The following sections cover the steps for you to add Cast to your Android app.

Configure the Android manifest

Your app's AndroidManifest.xml file requires you to configure the following elements for the Cast SDK:

uses-sdk

Set the minimum and target Android API levels that the Cast SDK supports. Currently the minimum is API level 24 and the target is API level 35.

<uses-sdk
        android:minSdkVersion="24"
        android:targetSdkVersion="35" />

android:theme

Set your app's theme based on the minimum Android SDK version. For example, if you are not implementing your own theme, you should use a variant of Theme.AppCompat when targeting a minimum Android SDK version that is pre-Lollipop.

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

Initialize the Cast Context

The framework has a global singleton object, the CastContext , that coordinates all the framework's interactions.

Your app must implement the OptionsProvider interface to supply options needed to initialize the CastContext singleton. OptionsProvider provides an instance of CastOptions which contains options that affect the behavior of the framework. The most important of these is the Web Receiver application ID, which is used to filter discovery results and to launch the Web Receiver app when a Cast session is started.

Котлин
class CastOptionsProvider : OptionsProvider {
    override fun getCastOptions(context: Context): CastOptions {
        return Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .build()
    }

    override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? {
        return null
    }
}
Java
public class CastOptionsProvider implements OptionsProvider {
    @Override
    public CastOptions getCastOptions(Context context) {
        CastOptions castOptions = new CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .build();
        return castOptions;
    }
    @Override
    public List<SessionProvider> getAdditionalSessionProviders(Context context) {
        return null;
    }
}

You must declare the fully qualified name of the implemented OptionsProvider as a metadata field in the AndroidManifest.xml file of the sender app:

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

CastContext is lazily initialized when the CastContext.getSharedInstance() is called.

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

The Cast UX Widgets

The Cast framework provides the widgets that comply with the Cast Design Checklist:

  • Introductory Overlay : The framework provides a custom View, IntroductoryOverlay , that is shown to the user to call attention to the Cast button the first time a receiver is available. The Sender app can customize the text and the position of the title text .

  • Cast Button : The Cast button is visible regardless of the availability of Cast devices. When the user first clicks on the Cast button, a Cast dialog is displayed which lists the discovered devices. When the user clicks on the Cast button while the device is connected, it displays the current media metadata (such as title, name of the recording studio and a thumbnail image) or allows the user to disconnect from the Cast device. The "Cast button" is sometimes referred to as the "Cast icon".

  • Mini Controller : When the user is casting content and has navigated away from the current content page or expanded controller to another screen in the sender app, the mini controller is displayed at the bottom of the screen to allow the user to see the currently casting media metadata and to control the playback.

  • Expanded Controller : When the user is casting content, if they click on the media notification or mini controller, the expanded controller launches, which displays the currently playing media metadata and provides several buttons to control the media playback.

  • Notification : Android only. When the user is casting content and navigates away from the sender app, a media notification is displayed that shows the currently casting media metadata and playback controls.

  • Lock Screen : Android only. When the user is casting content and navigates (or the device times out) to the lock screen, a media lock screen control is displayed that shows the currently casting media metadata and playback controls.

The following guide includes descriptions of how to add these widgets to your app.

Add a Cast Button

The Android MediaRouter APIs are designed to enable media display and playback on secondary devices. Android apps that use the MediaRouter API should include a Cast button as part of their user interface, to allow users to select a media route to play media on a secondary device such as a Cast device.

The framework makes adding a MediaRouteButton as a Cast button very easy. You should first add a menu item or a MediaRouteButton in the xml file that defines your menu, and use CastButtonFactory to wire it up with the framework.

// 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
}
Java
// Then override the onCreateOptionMenu() for each of your activities.
// MyActivity.java
@Override public boolean onCreateOptionsMenu(Menu menu) {
    super.onCreateOptionsMenu(menu);
    getMenuInflater().inflate(R.menu.main, menu);
    CastButtonFactory.setUpMediaRouteButton(getApplicationContext(),
                                            menu,
                                            R.id.media_route_menu_item);
    return true;
}

Then, if your Activity inherits from FragmentActivity , you can add a MediaRouteButton to your layout.

// 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)
}
Java
// MyActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_layout);

   mMediaRouteButton = (MediaRouteButton) findViewById(R.id.media_route_button);
   CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), mMediaRouteButton);

   mCastContext = CastContext.getSharedInstance(this);
}

To set the appearance of the Cast button using a theme, see Customize Cast Button .

Configure device discovery

Device discovery is completely managed by the CastContext . When initializing the CastContext, the sender app specifies the Web Receiver application ID, and can optionally request namespace filtering by setting supportedNamespaces in CastOptions . CastContext holds a reference to the MediaRouter internally, and will start the discovery process under the following conditions:

  • Based on an algorithm designed to balance device discovery latency and battery usage, discovery will occasionally be started automatically when the sender app enters the foreground.
  • The Cast dialog is open.
  • The Cast SDK is attempting to recover a Cast session.

The discovery process will be stopped when the Cast dialog is closed or the sender app enters the background.

Котлин
class CastOptionsProvider : OptionsProvider {
    companion object {
        const val CUSTOM_NAMESPACE = "urn:x-cast:custom_namespace"
    }

    override fun getCastOptions(appContext: Context): CastOptions {
        val supportedNamespaces: MutableList<String> = ArrayList()
        supportedNamespaces.add(CUSTOM_NAMESPACE)

        return CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .setSupportedNamespaces(supportedNamespaces)
            .build()
    }

    override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? {
        return null
    }
}
Java
class CastOptionsProvider implements OptionsProvider {
    public static final String CUSTOM_NAMESPACE = "urn:x-cast:custom_namespace";

    @Override
    public CastOptions getCastOptions(Context appContext) {
        List<String> supportedNamespaces = new ArrayList<>();
        supportedNamespaces.add(CUSTOM_NAMESPACE);

        CastOptions castOptions = new CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .setSupportedNamespaces(supportedNamespaces)
            .build();
        return castOptions;
    }

    @Override
    public List<SessionProvider> getAdditionalSessionProviders(Context context) {
        return null;
    }
}

How session management works

The Cast SDK introduces the concept of a Cast session, the establishment of which combines the steps of connecting to a device, launching (or joining) a Web Receiver app, connecting to that app, and initializing a media control channel. See the Web Receiver Application life cycle guide for more information about Cast sessions and the Web Receiver life cycle.

Sessions are managed by the class SessionManager , which your app can access via CastContext.getSessionManager() . Individual sessions are represented by subclasses of the class Session . For example, CastSession represents sessions with Cast devices. Your app can access the currently active Cast session via SessionManager.getCurrentCastSession() .

Your app can use the SessionManagerListener class to monitor session events, such as creation, suspension, resumption, and termination. The framework automatically attempts to resume from an abnormal/abrupt termination while a session was active.

Sessions are created and torn down automatically in response to user gestures from the MediaRouter dialogs.

To better understand Cast starting errors, apps can use CastContext#getCastReasonCodeForCastStatusCode(int) to convert the session starting error to CastReasonCodes . Please note that some session starting errors (eg CastReasonCodes#CAST_CANCELLED ) are intended behavior and should not be logged as an error.

If you need to be aware of the state changes for the session, you can implement a SessionManagerListener . This example listens to the availability of a CastSession in an 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)
    }
}
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);
    }
}

Stream transfer

Preserving session state is the basis of stream transfer, where users can move existing audio and video streams across devices using voice commands, Google Home App, or smart displays. Media stops playing on one device (the source) and continues on another (the destination). Any Cast device with the latest firmware can serve as sources or destinations in a stream transfer.

To get the new destination device during a stream transfer or expansion, register a Cast.Listener using the CastSession#addCastListener . Then call CastSession#getCastDevice() during the onDeviceNameChanged callback.

See Stream transfer on Web Receiver for more information.

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

The framework provides a ReconnectionService which can be enabled by the sender app to handle reconnection in many subtle corner cases, such as:

  • Recover from a temporary loss of WiFi
  • Recover from device sleep
  • Recover from backgrounding the app
  • Recover if the app crashed

This service is turned on by default, and can be turned off in CastOptions.Builder .

This service can be automatically merged into your app's manifest if auto-merge is enabled in your gradle file.

The framework will start the service when there is a media session, and stop it when the media session ends.

How Media Control works

The Cast framework deprecates the RemoteMediaPlayer class from Cast 2.x in favor of a new class RemoteMediaClient , which provides the same functionality in a set of more convenient APIs, and avoids having to pass in a GoogleApiClient.

When your app establishes a CastSession with a Web Receiver app that supports the media namespace, an instance of RemoteMediaClient will automatically be created by the framework; your app can access it by calling getRemoteMediaClient() method on the CastSession instance.

All methods of RemoteMediaClient that issue requests to the Web Receiver will return a PendingResult object that can be used to track that request.

It is expected that the instance of RemoteMediaClient may be shared by multiple parts of your app, and indeed some internal components of the framework, such as the persistent mini controllers and the notification service . To that end, this instance supports registration of multiple instances of RemoteMediaClient.Listener .

Set media metadata

The MediaMetadata class represents the information about a media item you want to Cast. The following example creates a new MediaMetadata instance of a movie and sets the title, subtitle and two images.

Котлин
val movieMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE)

movieMetadata.putString(MediaMetadata.KEY_TITLE, mSelectedMedia.getTitle())
movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, mSelectedMedia.getStudio())
movieMetadata.addImage(WebImage(Uri.parse(mSelectedMedia.getImage(0))))
movieMetadata.addImage(WebImage(Uri.parse(mSelectedMedia.getImage(1))))
Java
MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);

movieMetadata.putString(MediaMetadata.KEY_TITLE, mSelectedMedia.getTitle());
movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, mSelectedMedia.getStudio());
movieMetadata.addImage(new WebImage(Uri.parse(mSelectedMedia.getImage(0))));
movieMetadata.addImage(new WebImage(Uri.parse(mSelectedMedia.getImage(1))));

See Image Selection on the use of images with media metadata.

Load media

Your app can load a media item, as shown in the following code. First use MediaInfo.Builder with the media's metadata to build a MediaInfo instance. Get the RemoteMediaClient from the current CastSession , then load the MediaInfo into that RemoteMediaClient . Use RemoteMediaClient to play, pause, and otherwise control a media player app running on the 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())
Java
MediaInfo mediaInfo = new MediaInfo.Builder(mSelectedMedia.getUrl())
        .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
        .setContentType("videos/mp4")
        .setMetadata(movieMetadata)
        .setStreamDuration(mSelectedMedia.getDuration() * 1000)
        .build();
RemoteMediaClient remoteMediaClient = mCastSession.getRemoteMediaClient();
remoteMediaClient.load(new MediaLoadRequestData.Builder().setMediaInfo(mediaInfo).build());

Also see the section on using media tracks .

4K video format

To check what video format your media is, use getVideoInfo() in MediaStatus to get the current instance of VideoInfo . This instance contains the type of HDR TV format and the display height and width in pixels. Variants of 4K format are indicated by constants HDR_TYPE_* .

Remote control notifications to multiple devices

When a user is casting, other Android devices on the same network will get a notification to also let them control the playback. Anyone whose device receives such notifications can turn them off for that device in the Settings app at Google > Google Cast > Show remote control notifications . (The notifications include a shortcut to the Settings app.) For more detail, see Cast remote control notifications .

Add mini controller

According to the Cast Design Checklist , a sender app should provide a persistent control known as the mini controller that should appear when the user navigates away from the current content page to another part of the sender app. The mini controller provides a visible reminder to the user of the current Cast session. By tapping on the mini controller, the user can return to the Cast full-screen expanded controller view.

The framework provides a custom View, MiniControllerFragment, which you can add to the bottom of the layout file of each activity in which you want to show the mini controller.

<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" />

When your sender app is playing a video or audio live stream, the SDK automatically displays a play/stop button in place of the play/pause button in the mini controller.

To set the text appearance of the title and subtitle of this custom view, and to choose buttons, see Customize Mini Controller .

Add expanded controller

The Google Cast Design Checklist requires that a sender app provide an expanded controller for the media being Cast. The expanded controller is a full screen version of the mini controller.

The Cast SDK provides a widget for the expanded controller called ExpandedControllerActivity . This is an abstract class you have to subclass to add a Cast button.

First, create a new menu resource file for the expanded controller to provide the Cast button:

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

Create a new class that extends 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
    }
}
Java
public class ExpandedControlsActivity extends ExpandedControllerActivity {
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        getMenuInflater().inflate(R.menu.expanded_controller, menu);
        CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item);
        return true;
    }
}

Now declare your new activity in the app manifest within the application tag:

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

Edit the CastOptionsProvider and change NotificationOptions and CastMediaOptions to set the target activity to your new activity:

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

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

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

Update the LocalPlayerActivity loadRemoteMedia method to display your new activity when the remote media is loaded:

Котлин
private fun loadRemoteMedia(position: Int, autoPlay: Boolean) {
    val remoteMediaClient = mCastSession?.remoteMediaClient ?: return

    remoteMediaClient.registerCallback(object : RemoteMediaClient.Callback() {
        override fun onStatusUpdated() {
            val intent = Intent(this@LocalPlayerActivity, ExpandedControlsActivity::class.java)
            startActivity(intent)
            remoteMediaClient.unregisterCallback(this)
        }
    })

    remoteMediaClient.load(
        MediaLoadRequestData.Builder()
            .setMediaInfo(mSelectedMedia)
            .setAutoplay(autoPlay)
            .setCurrentTime(position.toLong()).build()
    )
}
Java
private void loadRemoteMedia(int position, boolean autoPlay) {
    if (mCastSession == null) {
        return;
    }
    final RemoteMediaClient remoteMediaClient = mCastSession.getRemoteMediaClient();
    if (remoteMediaClient == null) {
        return;
    }
    remoteMediaClient.registerCallback(new RemoteMediaClient.Callback() {
        @Override
        public void onStatusUpdated() {
            Intent intent = new Intent(LocalPlayerActivity.this, ExpandedControlsActivity.class);
            startActivity(intent);
            remoteMediaClient.unregisterCallback(this);
        }
    });
    remoteMediaClient.load(new MediaLoadRequestData.Builder()
            .setMediaInfo(mSelectedMedia)
            .setAutoplay(autoPlay)
            .setCurrentTime(position).build());
}

When your sender app is playing a video or audio live stream, the SDK automatically displays a play/stop button in place of the play/pause button in the expanded controller.

To set the appearance using themes, choose which buttons to display, and add custom buttons, see Customize Expanded Controller .

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

The framework automatically manages the volume for the sender app. The framework automatically synchronizes the sender and Web Receiver apps so that the sender UI always reports the volume specified by the Web Receiver.

Physical button volume control

On Android, the physical buttons on the sender device can be used to change the volume of the Cast session on the Web Receiver by default for any device using Jelly Bean or newer.

Physical button volume control prior to Jelly Bean

To use the physical volume keys to control the Web Receiver device volume on Android devices older than Jelly Bean, the sender app should override dispatchKeyEvent in their Activities, and call CastContext.onDispatchVolumeKeyEventBeforeJellyBean() :

Котлин
class MyActivity : FragmentActivity() {
    override fun dispatchKeyEvent(event: KeyEvent): Boolean {
        return (CastContext.getSharedInstance(this)
            .onDispatchVolumeKeyEventBeforeJellyBean(event)
                || super.dispatchKeyEvent(event))
    }
}
Java
class MyActivity extends FragmentActivity {
    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        return CastContext.getSharedInstance(this)
            .onDispatchVolumeKeyEventBeforeJellyBean(event)
            || super.dispatchKeyEvent(event);
    }
}

Add media controls to notification and lock screen

On Android only, the Google Cast Design Checklist requires a sender app to implement media controls in a notification and in the lock screen , where the sender is casting but the sender app does not have focus. The framework provides MediaNotificationService and MediaIntentReceiver to help the sender app build media controls in a notification and in the lock screen.

MediaNotificationService runs when the sender is casting, and will show a notification with image thumbnail and information about the current casting item, a play/pause button and a stop button.

MediaIntentReceiver is a BroadcastReceiver that handles user actions from the notification.

Your app can configure notification and media control from lock screen through NotificationOptions . Your app can configure what control buttons to show in the notification, and which Activity to open when the notification is tapped by the user. If actions are not explicitly provided, the default values, MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK and MediaIntentReceiver.ACTION_STOP_CASTING will be used.

Котлин
// Example showing 4 buttons: "rewind", "play/pause", "forward" and "stop casting".
val buttonActions: MutableList<String> = ArrayList()
buttonActions.add(MediaIntentReceiver.ACTION_REWIND)
buttonActions.add(MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK)
buttonActions.add(MediaIntentReceiver.ACTION_FORWARD)
buttonActions.add(MediaIntentReceiver.ACTION_STOP_CASTING)

// Showing "play/pause" and "stop casting" in the compat view of the notification.
val compatButtonActionsIndices = intArrayOf(1, 3)

// Builds a notification with the above actions. Each tap on the "rewind" and "forward" buttons skips 30 seconds.
// Tapping on the notification opens an Activity with class VideoBrowserActivity.
val notificationOptions = NotificationOptions.Builder()
    .setActions(buttonActions, compatButtonActionsIndices)
    .setSkipStepMs(30 * DateUtils.SECOND_IN_MILLIS)
    .setTargetActivityClassName(VideoBrowserActivity::class.java.name)
    .build()
Java
// Example showing 4 buttons: "rewind", "play/pause", "forward" and "stop casting".
List<String> buttonActions = new ArrayList<>();
buttonActions.add(MediaIntentReceiver.ACTION_REWIND);
buttonActions.add(MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK);
buttonActions.add(MediaIntentReceiver.ACTION_FORWARD);
buttonActions.add(MediaIntentReceiver.ACTION_STOP_CASTING);

// Showing "play/pause" and "stop casting" in the compat view of the notification.
int[] compatButtonActionsIndices = new int[]{1, 3};

// Builds a notification with the above actions. Each tap on the "rewind" and "forward" buttons skips 30 seconds.
// Tapping on the notification opens an Activity with class VideoBrowserActivity.
NotificationOptions notificationOptions = new NotificationOptions.Builder()
    .setActions(buttonActions, compatButtonActionsIndices)
    .setSkipStepMs(30 * DateUtils.SECOND_IN_MILLIS)
    .setTargetActivityClassName(VideoBrowserActivity.class.getName())
    .build();

Showing media controls from notification and lock screen are turned on by default, and can disabled by calling setNotificationOptions with null in CastMediaOptions.Builder . Currently, the lock screen feature is turned on as long as notification is turned on.

Котлин
// ... continue with the NotificationOptions built above
val mediaOptions = CastMediaOptions.Builder()
    .setNotificationOptions(notificationOptions)
    .build()
val castOptions: CastOptions = Builder()
    .setReceiverApplicationId(context.getString(R.string.app_id))
    .setCastMediaOptions(mediaOptions)
    .build()
Java
// ... continue with the NotificationOptions built above
CastMediaOptions mediaOptions = new CastMediaOptions.Builder()
        .setNotificationOptions(notificationOptions)
        .build();
CastOptions castOptions = new CastOptions.Builder()
        .setReceiverApplicationId(context.getString(R.string.app_id))
        .setCastMediaOptions(mediaOptions)
        .build();

When your sender app is playing a video or audio live stream, the SDK automatically displays a play/stop button in place of the play/pause button on the notification control but not the lock screen control.

Note : To display lock screen controls on pre-Lollipop devices, RemoteMediaClient will automatically request audio focus on your behalf.

Handle errors

It is very important for sender apps to handle all error callbacks and decide the best response for each stage of the Cast life cycle. The app can display error dialogs to the user or it can decide to tear down the connection to the Web Receiver.