Добавьте основные функции в свой Android TV-ресивер

Эта страница содержит фрагменты кода и описания функций, доступных для настройки приложения Android TV Receiver.

Настройка библиотек

Чтобы сделать API-интерфейсы Cast Connect доступными для вашего приложения Android TV:

Андроид
  1. Откройте файл build.gradle в каталоге модуля приложения.
  2. Убедитесь, что google() включен в список repositories .
      repositories {
        google()
      }
  3. В зависимости от типа целевого устройства для вашего приложения добавьте последние версии библиотек в свои зависимости:
    • Для приложения Android-приемника:
        dependencies {
          implementation 'com.google.android.gms:play-services-cast-tv:21.0.1'
          implementation 'com.google.android.gms:play-services-cast:21.4.0'
        }
    • Для приложения Android Sender:
        dependencies {
          implementation 'com.google.android.gms:play-services-cast:21.0.1'
          implementation 'com.google.android.gms:play-services-cast-framework:21.4.0'
        }
    Обязательно обновляйте этот номер версии каждый раз при обновлении служб.
  4. Сохраните изменения и нажмите Sync Project with Gradle Files на панели инструментов.
iOS
  1. Убедитесь, что ваш Podfile ориентирован на google-cast-sdk 4.8.1 или выше.
  2. Целевая iOS 14 или более поздняя версия. Дополнительные сведения см. в примечаниях к выпуску .
      platform: ios, '14'
    
      def target_pods
         pod 'google-cast-sdk', '~>4.8.1'
      end
Интернет
  1. Требуется браузер Chromium версии M87 или выше.
  2. Добавьте библиотеку Web Sender API в свой проект
      <script src="//www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1"></script>

Требование AndroidX

Новые версии Сервисов Google Play требуют, чтобы приложение было обновлено для использования пространства имен androidx . Следуйте инструкциям по переходу на AndroidX .

Приложение Android TV: необходимые условия

Чтобы поддерживать Cast Connect в приложении Android TV, вам необходимо создавать и поддерживать события из медиа-сеанса. Данные, предоставленные вашим сеансом мультимедиа, предоставляют основную информацию — например, положение, состояние воспроизведения и т. д. — для вашего статуса мультимедиа. Ваш медиа-сеанс также используется библиотекой Cast Connect для сигнализации о получении определенных сообщений от отправителя, например паузы.

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

Жизненный цикл медиа-сессии

Ваше приложение должно создать сеанс мультимедиа при запуске воспроизведения и освободить его, когда им больше нельзя будет управлять. Например, если ваше приложение является видеоприложением, вам следует завершить сеанс, когда пользователь завершает действие воспроизведения — либо выбрав «Назад» для просмотра другого контента, либо переведя приложение в фоновый режим. Если ваше приложение является музыкальным, вам следует выпустить его, когда ваше приложение больше не воспроизводит медиафайлы.

Обновление статуса сеанса

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

МедиаМетаданныеКомпат

Поле метаданных Описание
METADATA_KEY_TITLE (обязательно) Медийное название.
METADATA_KEY_DISPLAY_SUBTITLE Подзаголовок.
METADATA_KEY_DISPLAY_ICON_URI URL-адрес значка.
METADATA_KEY_DURATION (обязательно) Продолжительность медиа.
METADATA_KEY_MEDIA_URI Идентификатор контента.
METADATA_KEY_ARTIST Исполнитель.
METADATA_KEY_ALBUM Альбом.

PlaybackStateCompat

Требуемый метод Описание
УстановитьДействия() Устанавливает поддерживаемые мультимедийные команды.
setState() Установите состояние воспроизведения и текущую позицию.

Медиасессионкомпат

Требуемый метод Описание
установитьRepeatMode() Устанавливает режим повтора.
установитьShuffleMode() Устанавливает режим случайного воспроизведения.
установитьМетаданные() Устанавливает метаданные мультимедиа.
setPlaybackState() Устанавливает состояние воспроизведения.
Котлин
private fun updateMediaSession() {
    val metadata = MediaMetadataCompat.Builder()
         .putString(MediaMetadataCompat.METADATA_KEY_TITLE, "title")
         .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, "subtitle")
         .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, mMovie.getCardImageUrl())
         .build()

    val playbackState = PlaybackStateCompat.Builder()
         .setState(
             PlaybackStateCompat.STATE_PLAYING,
             player.getPosition(),
             player.getPlaybackSpeed(),
             System.currentTimeMillis()
        )
         .build()

    mediaSession.setMetadata(metadata)
    mediaSession.setPlaybackState(playbackState)
}
Джава
private void updateMediaSession() {
  MediaMetadataCompat metadata =
      new MediaMetadataCompat.Builder()
          .putString(MediaMetadataCompat.METADATA_KEY_TITLE, "title")
          .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, "subtitle")
          .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI,mMovie.getCardImageUrl())
          .build();

  PlaybackStateCompat playbackState =
      new PlaybackStateCompat.Builder()
          .setState(
               PlaybackStateCompat.STATE_PLAYING,
               player.getPosition(),
               player.getPlaybackSpeed(),
               System.currentTimeMillis())
          .build();

  mediaSession.setMetadata(metadata);
  mediaSession.setPlaybackState(playbackState);
}

Осуществление транспортного контроля

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

MediaSessionCompat.Callback

Действия Описание
onPlay() Резюме
onPause() Пауза
onSeekTo() Ищите позицию
onStop() Остановить текущие СМИ
Котлин
class MyMediaSessionCallback : MediaSessionCompat.Callback() {
  override fun onPause() {
    // Pause the player and update the play state.
    ...
  }

  override fun onPlay() {
    // Resume the player and update the play state.
    ...
  }

  override fun onSeekTo (long pos) {
    // Seek and update the play state.
    ...
  }
  ...
}

mediaSession.setCallback( MyMediaSessionCallback() );
Джава
public MyMediaSessionCallback extends MediaSessionCompat.Callback {
  public void onPause() {
    // Pause the player and update the play state.
    ...
  }

  public void onPlay() {
    // Resume the player and update the play state.
    ...
  }

  public void onSeekTo (long pos) {
    // Seek and update the play state.
    ...
  }
  ...
}

mediaSession.setCallback(new MyMediaSessionCallback());

Настройка поддержки Cast

Когда запрос на запуск отправляется приложением-отправителем, создается намерение с пространством имен приложения. Ваше приложение отвечает за его обработку и создание экземпляра объекта CastReceiverContext при запуске приложения TV. Объект CastReceiverContext необходим для взаимодействия с Cast во время работы приложения TV. Этот объект позволяет вашему ТВ-приложению принимать сообщения Cast Media, поступающие от любых подключенных отправителей.

Настройка Android-телевизора

Добавление фильтра намерений запуска

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

<activity android:name="com.example.activity">
  <intent-filter>
      <action android:name="com.google.android.gms.cast.tv.action.LAUNCH" />
      <category android:name="android.intent.category.DEFAULT" />
  </intent-filter>
</activity>

Укажите поставщика опций приемника

Вам необходимо реализовать ReceiverOptionsProvider для предоставления CastReceiverOptions :

Котлин
class MyReceiverOptionsProvider : ReceiverOptionsProvider {
  override fun getOptions(context: Context?): CastReceiverOptions {
    return CastReceiverOptions.Builder(context)
          .setStatusText("My App")
          .build()
    }
}
Джава
public class MyReceiverOptionsProvider implements ReceiverOptionsProvider {
  @Override
  public CastReceiverOptions getOptions(Context context) {
    return new CastReceiverOptions.Builder(context)
        .setStatusText("My App")
        .build();
  }
}

Затем укажите поставщика параметров в AndroidManifest :

 <meta-data
    android:name="com.google.android.gms.cast.tv.RECEIVER_OPTIONS_PROVIDER_CLASS_NAME"
    android:value="com.example.mysimpleatvapplication.MyReceiverOptionsProvider" />

ReceiverOptionsProvider используется для предоставления CastReceiverOptions при инициализации CastReceiverContext .

Контекст приемника трансляции

Инициализируйте CastReceiverContext при создании приложения:

Котлин
override fun onCreate() {
  CastReceiverContext.initInstance(this)

  ...
}
Джава
@Override
public void onCreate() {
  CastReceiverContext.initInstance(this);

  ...
}

Запустите CastReceiverContext когда ваше приложение перейдет на передний план:

Котлин
CastReceiverContext.getInstance().start()
Джава
CastReceiverContext.getInstance().start();

Вызовите stop() в CastReceiverContext после того, как приложение перейдет в фоновый режим для видеоприложений или приложений, которые не поддерживают фоновое воспроизведение:

Котлин
// Player has stopped.
CastReceiverContext.getInstance().stop()
Джава
// Player has stopped.
CastReceiverContext.getInstance().stop();

Кроме того, если ваше приложение поддерживает воспроизведение в фоновом режиме, вызовите stop() в CastReceiverContext , когда оно перестанет воспроизводиться в фоновом режиме.

Мы настоятельно рекомендуем вам использовать LifecycleObserver из библиотеки androidx.lifecycle для управления вызовами CastReceiverContext.start() и CastReceiverContext.stop() , особенно если ваше собственное приложение выполняет несколько действий. Это позволяет избежать состояний гонки при вызове start() и stop() из разных действий.

Котлин
// Create a LifecycleObserver class.
class MyLifecycleObserver : DefaultLifecycleObserver {
  override fun onStart(owner: LifecycleOwner) {
    // App prepares to enter foreground.
    CastReceiverContext.getInstance().start()
  }

  override fun onStop(owner: LifecycleOwner) {
    // App has moved to the background or has terminated.
    CastReceiverContext.getInstance().stop()
  }
}

// Add the observer when your application is being created.
class MyApplication : Application() {
  fun onCreate() {
    super.onCreate()

    // Initialize CastReceiverContext.
    CastReceiverContext.initInstance(this /* android.content.Context */)

    // Register LifecycleObserver
    ProcessLifecycleOwner.get().lifecycle.addObserver(
        MyLifecycleObserver())
  }
}
Джава
// Create a LifecycleObserver class.
public class MyLifecycleObserver implements DefaultLifecycleObserver {
  @Override
  public void onStart(LifecycleOwner owner) {
    // App prepares to enter foreground.
    CastReceiverContext.getInstance().start();
  }

  @Override
  public void onStop(LifecycleOwner owner) {
    // App has moved to the background or has terminated.
    CastReceiverContext.getInstance().stop();
  }
}

// Add the observer when your application is being created.
public class MyApplication extends Application {
  @Override
  public void onCreate() {
    super.onCreate();

    // Initialize CastReceiverContext.
    CastReceiverContext.initInstance(this /* android.content.Context */);

    // Register LifecycleObserver
    ProcessLifecycleOwner.get().getLifecycle().addObserver(
        new MyLifecycleObserver());
  }
}
// In AndroidManifest.xml set MyApplication as the application class
<application
    ...
    android:name=".MyApplication">

Подключение MediaSession к MediaManager

Когда вы создаете MediaSession , вам также необходимо предоставить текущий токен MediaSession в CastReceiverContext , чтобы он знал, куда отправлять команды и получать состояние воспроизведения мультимедиа:

Котлин
val mediaManager: MediaManager = receiverContext.getMediaManager()
mediaManager.setSessionCompatToken(currentMediaSession.getSessionToken())
Джава
MediaManager mediaManager = receiverContext.getMediaManager();
mediaManager.setSessionCompatToken(currentMediaSession.getSessionToken());

Когда вы завершаете MediaSession из-за неактивного воспроизведения, вам следует установить нулевой токен в MediaManager :

Котлин
myPlayer.stop()
mediaSession.release()
mediaManager.setSessionCompatToken(null)
Джава
myPlayer.stop();
mediaSession.release();
mediaManager.setSessionCompatToken(null);

Если ваше приложение поддерживает воспроизведение мультимедиа, пока ваше приложение находится в фоновом режиме, вместо вызова CastReceiverContext.stop() когда ваше приложение отправляется в фоновый режим, вам следует вызывать его только тогда, когда ваше приложение находится в фоновом режиме и больше не воспроизводит мультимедиа. Например:

Котлин
class MyLifecycleObserver : DefaultLifecycleObserver {
  ...
  // App has moved to the background.
  override fun onPause(owner: LifecycleOwner) {
    mIsBackground = true
    myStopCastReceiverContextIfNeeded()
  }
}

// Stop playback on the player.
private fun myStopPlayback() {
  myPlayer.stop()

  myStopCastReceiverContextIfNeeded()
}

// Stop the CastReceiverContext when both the player has
// stopped and the app has moved to the background.
private fun myStopCastReceiverContextIfNeeded() {
  if (mIsBackground && myPlayer.isStopped()) {
    CastReceiverContext.getInstance().stop()
  }
}
Джава
public class MyLifecycleObserver implements DefaultLifecycleObserver {
  ...
  // App has moved to the background.
  @Override
  public void onPause(LifecycleOwner owner) {
    mIsBackground = true;

    myStopCastReceiverContextIfNeeded();
  }
}

// Stop playback on the player.
private void myStopPlayback() {
  myPlayer.stop();

  myStopCastReceiverContextIfNeeded();
}

// Stop the CastReceiverContext when both the player has
// stopped and the app has moved to the background.
private void myStopCastReceiverContextIfNeeded() {
  if (mIsBackground && myPlayer.isStopped()) {
    CastReceiverContext.getInstance().stop();
  }
}

Использование Exoplayer с Cast Connect

Если вы используете Exoplayer , вы можете использовать MediaSessionConnector для автоматического поддержания сеанса и всей связанной информации, включая состояние воспроизведения, вместо отслеживания изменений вручную.

MediaSessionConnector.MediaButtonEventHandler можно использовать для обработки событий MediaButton путем вызова setMediaButtonEventHandler(MediaButtonEventHandler) которые в противном случае обрабатываются MediaSessionCompat.Callback по умолчанию.

Чтобы интегрировать MediaSessionConnector в ваше приложение, добавьте следующее в класс активности вашего проигрывателя или в место, где вы управляете своим медиа-сеансом:

Котлин
class PlayerActivity : Activity() {
  private var mMediaSession: MediaSessionCompat? = null
  private var mMediaSessionConnector: MediaSessionConnector? = null
  private var mMediaManager: MediaManager? = null

  override fun onCreate(savedInstanceState: Bundle?) {
    ...
    mMediaSession = MediaSessionCompat(this, LOG_TAG)
    mMediaSessionConnector = MediaSessionConnector(mMediaSession!!)
    ...
  }

  override fun onStart() {
    ...
    mMediaManager = receiverContext.getMediaManager()
    mMediaManager!!.setSessionCompatToken(currentMediaSession.getSessionToken())
    mMediaSessionConnector!!.setPlayer(mExoPlayer)
    mMediaSessionConnector!!.setMediaMetadataProvider(mMediaMetadataProvider)
    mMediaSession!!.isActive = true
    ...
  }

  override fun onStop() {
    ...
    mMediaSessionConnector!!.setPlayer(null)
    mMediaSession!!.release()
    mMediaManager!!.setSessionCompatToken(null)
    ...
  }
}
Джава
public class PlayerActivity extends Activity {
  private MediaSessionCompat mMediaSession;
  private MediaSessionConnector mMediaSessionConnector;
  private MediaManager mMediaManager;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    ...
    mMediaSession = new MediaSessionCompat(this, LOG_TAG);
    mMediaSessionConnector = new MediaSessionConnector(mMediaSession);
    ...
  }

  @Override
  protected void onStart() {
    ...
    mMediaManager = receiverContext.getMediaManager();
    mMediaManager.setSessionCompatToken(currentMediaSession.getSessionToken());

    mMediaSessionConnector.setPlayer(mExoPlayer);
    mMediaSessionConnector.setMediaMetadataProvider(mMediaMetadataProvider);
    mMediaSession.setActive(true);
    ...
  }

  @Override
  protected void onStop() {
    ...
    mMediaSessionConnector.setPlayer(null);
    mMediaSession.release();
    mMediaManager.setSessionCompatToken(null);
    ...
  }
}

Настройка приложения отправителя

Включить поддержку Cast Connect

После того как вы обновили приложение-отправитель с поддержкой Cast Connect, вы можете объявить о его готовности, установив для флага androidReceiverCompatible в LaunchOptions значение true.

Андроид

Требуется play-services-cast-framework версии 19.0.0 или выше.

Флаг androidReceiverCompatible установлен в LaunchOptions (который является частью CastOptions ):

Котлин
class CastOptionsProvider : OptionsProvider {
  override fun getCastOptions(context: Context?): CastOptions {
    val launchOptions: LaunchOptions = Builder()
          .setAndroidReceiverCompatible(true)
          .build()
    return CastOptions.Builder()
          .setLaunchOptions(launchOptions)
          ...
          .build()
    }
}
Джава
public class CastOptionsProvider implements OptionsProvider {
  @Override
  public CastOptions getCastOptions(Context context) {
    LaunchOptions launchOptions = new LaunchOptions.Builder()
              .setAndroidReceiverCompatible(true)
              .build();
    return new CastOptions.Builder()
        .setLaunchOptions(launchOptions)
        ...
        .build();
  }
}
iOS

Требуется google-cast-sdk версии v4.4.8 или выше.

Флаг androidReceiverCompatible установлен в GCKLaunchOptions (который является частью GCKCastOptions ):

let options = GCKCastOptions(discoveryCriteria: GCKDiscoveryCriteria(applicationID: kReceiverAppID))
...
let launchOptions = GCKLaunchOptions()
launchOptions.androidReceiverCompatible = true
options.launchOptions = launchOptions
GCKCastContext.setSharedInstanceWith(options)
Интернет

Требуется браузер Chromium версии M87 или выше.

const context = cast.framework.CastContext.getInstance();
const castOptions = new cast.framework.CastOptions();
castOptions.receiverApplicationId = kReceiverAppID;
castOptions.androidReceiverCompatible = true;
context.setOptions(castOptions);

Настройка консоли разработчика Cast

Настройте приложение Android TV

Добавьте имя пакета вашего приложения Android TV в консоль разработчика Cast, чтобы связать его с вашим идентификатором приложения Cast.

Регистрация устройств разработчика

Зарегистрируйте серийный номер устройства Android TV, которое вы собираетесь использовать для разработки, в консоли разработчика Cast .

Без регистрации Cast Connect будет работать только с приложениями, установленными из Google Play Store, из соображений безопасности.

Дополнительную информацию о регистрации устройства Cast или Android TV для разработки Cast см. на странице регистрации .

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

Если вы уже реализовали поддержку глубоких ссылок в своем приложении Android TV, то аналогичное определение должно быть настроено в вашем манифесте Android TV:

<activity android:name="com.example.activity">
  <intent-filter>
     <action android:name="android.intent.action.VIEW" />
     <category android:name="android.intent.category.DEFAULT" />
     <data android:scheme="https"/>
     <data android:host="www.example.com"/>
     <data android:pathPattern=".*"/>
  </intent-filter>
</activity>

Загружать по объекту отправителя

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

Котлин
val mediaToLoad = MediaInfo.Builder("some-id")
    .setEntity("https://example.com/watch/some-id")
    ...
    .build()
val loadRequest = MediaLoadRequestData.Builder()
    .setMediaInfo(mediaToLoad)
    .setCredentials("user-credentials")
    ...
    .build()
remoteMediaClient.load(loadRequest)
Андроид
Джава
MediaInfo mediaToLoad =
    new MediaInfo.Builder("some-id")
        .setEntity("https://example.com/watch/some-id")
        ...
        .build();
MediaLoadRequestData loadRequest =
    new MediaLoadRequestData.Builder()
        .setMediaInfo(mediaToLoad)
        .setCredentials("user-credentials")
        ...
        .build();
remoteMediaClient.load(loadRequest);
iOS
let mediaInfoBuilder = GCKMediaInformationBuilder(entity: "https://example.com/watch/some-id")
...
mediaInformation = mediaInfoBuilder.build()

let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder()
mediaLoadRequestDataBuilder.mediaInformation = mediaInformation
mediaLoadRequestDataBuilder.credentials = "user-credentials"
...
let mediaLoadRequestData = mediaLoadRequestDataBuilder.build()

remoteMediaClient?.loadMedia(with: mediaLoadRequestData)
Интернет

Требуется браузер Chromium версии M87 или выше.

let mediaInfo = new chrome.cast.media.MediaInfo('some-id"', 'video/mp4');
mediaInfo.entity = 'https://example.com/watch/some-id';
...

let request = new chrome.cast.media.LoadRequest(mediaInfo);
request.credentials = 'user-credentials';
...

cast.framework.CastContext.getInstance().getCurrentSession().loadMedia(request);

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

Установка учетных данных ATV отправителя

Возможно, ваше приложение веб-приемника и приложение Android TV поддерживают разные глубокие ссылки и credentials (например, если вы по-разному обрабатываете аутентификацию на двух платформах). Чтобы решить эту проблему, вы можете предоставить альтернативный entity и credentials для Android TV:

Андроид
Котлин
val mediaToLoad = MediaInfo.Builder("some-id")
        .setEntity("https://example.com/watch/some-id")
        .setAtvEntity("myscheme://example.com/atv/some-id")
        ...
        .build()
val loadRequest = MediaLoadRequestData.Builder()
        .setMediaInfo(mediaToLoad)
        .setCredentials("user-credentials")
        .setAtvCredentials("atv-user-credentials")
        ...
        .build()
remoteMediaClient.load(loadRequest)
Джава
MediaInfo mediaToLoad =
    new MediaInfo.Builder("some-id")
        .setEntity("https://example.com/watch/some-id")
        .setAtvEntity("myscheme://example.com/atv/some-id")
        ...
        .build();
MediaLoadRequestData loadRequest =
    new MediaLoadRequestData.Builder()
        .setMediaInfo(mediaToLoad)
        .setCredentials("user-credentials")
        .setAtvCredentials("atv-user-credentials")
        ...
        .build();
remoteMediaClient.load(loadRequest);
iOS
let mediaInfoBuilder = GCKMediaInformationBuilder(entity: "https://example.com/watch/some-id")
mediaInfoBuilder.atvEntity = "myscheme://example.com/atv/some-id"
...
mediaInformation = mediaInfoBuilder.build()

let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder()
mediaLoadRequestDataBuilder.mediaInformation = mediaInformation
mediaLoadRequestDataBuilder.credentials = "user-credentials"
mediaLoadRequestDataBuilder.atvCredentials = "atv-user-credentials"
...
let mediaLoadRequestData = mediaLoadRequestDataBuilder.build()

remoteMediaClient?.loadMedia(with: mediaLoadRequestData)
Интернет

Требуется браузер Chromium версии M87 или выше.

let mediaInfo = new chrome.cast.media.MediaInfo('some-id"', 'video/mp4');
mediaInfo.entity = 'https://example.com/watch/some-id';
mediaInfo.atvEntity = 'myscheme://example.com/atv/some-id';
...

let request = new chrome.cast.media.LoadRequest(mediaInfo);
request.credentials = 'user-credentials';
request.atvCredentials = 'atv-user-credentials';
...

cast.framework.CastContext.getInstance().getCurrentSession().loadMedia(request);

Если приложение веб-приемника запущено, оно использует entity и credentials в запросе на загрузку. Однако если ваше приложение Android TV запущено, SDK переопределяет entity и credentials с помощью ваших atvEntity и atvCredentials (если они указаны).

Загрузка по Content ID или MediaQueueData

Если вы не используете entity или atvEntity и используете Content ID или URL-адрес контента в своей медиа-информации или используете более подробные данные запроса на загрузку мультимедиа, вам необходимо добавить следующий предопределенный фильтр намерений в свое приложение Android TV:

<activity android:name="com.example.activity">
  <intent-filter>
     <action android:name="com.google.android.gms.cast.tv.action.LOAD"/>
     <category android:name="android.intent.category.DEFAULT" />
  </intent-filter>
</activity>

На стороне отправителя, как и при загрузке по сущности , вы можете создать запрос на загрузку с информацией о содержимом и вызвать load() .

Андроид
Котлин
val mediaToLoad = MediaInfo.Builder("some-id").build()
val loadRequest = MediaLoadRequestData.Builder()
    .setMediaInfo(mediaToLoad)
    .setCredentials("user-credentials")
    ...
    .build()
remoteMediaClient.load(loadRequest)
Джава
MediaInfo mediaToLoad =
    new MediaInfo.Builder("some-id").build();
MediaLoadRequestData loadRequest =
    new MediaLoadRequestData.Builder()
        .setMediaInfo(mediaToLoad)
        .setCredentials("user-credentials")
        ...
        .build();
remoteMediaClient.load(loadRequest);
iOS
let mediaInfoBuilder = GCKMediaInformationBuilder(contentId: "some-id")
...
mediaInformation = mediaInfoBuilder.build()

let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder()
mediaLoadRequestDataBuilder.mediaInformation = mediaInformation
mediaLoadRequestDataBuilder.credentials = "user-credentials"
...
let mediaLoadRequestData = mediaLoadRequestDataBuilder.build()

remoteMediaClient?.loadMedia(with: mediaLoadRequestData)
Интернет

Требуется браузер Chromium версии M87 или выше.

let mediaInfo = new chrome.cast.media.MediaInfo('some-id"', 'video/mp4');
...

let request = new chrome.cast.media.LoadRequest(mediaInfo);
...

cast.framework.CastContext.getInstance().getCurrentSession().loadMedia(request);

Обработка запросов на загрузку

В вашей деятельности для обработки этих запросов на загрузку вам необходимо обрабатывать намерения в обратных вызовах жизненного цикла вашей активности:

Котлин
class MyActivity : Activity() {
  override fun onStart() {
    super.onStart()
    val mediaManager = CastReceiverContext.getInstance().getMediaManager()
    // Pass the intent to the SDK. You can also do this in onCreate().
    if (mediaManager.onNewIntent(intent)) {
        // If the SDK recognizes the intent, you should early return.
        return
    }
    // If the SDK doesn't recognize the intent, you can handle the intent with
    // your own logic.
    ...
  }

  // For some cases, a new load intent triggers onNewIntent() instead of
  // onStart().
  override fun onNewIntent(intent: Intent) {
    val mediaManager = CastReceiverContext.getInstance().getMediaManager()
    // Pass the intent to the SDK. You can also do this in onCreate().
    if (mediaManager.onNewIntent(intent)) {
        // If the SDK recognizes the intent, you should early return.
        return
    }
    // If the SDK doesn't recognize the intent, you can handle the intent with
    // your own logic.
    ...
  }
}
Джава
public class MyActivity extends Activity {
  @Override
  protected void onStart() {
    super.onStart();
    MediaManager mediaManager =
        CastReceiverContext.getInstance().getMediaManager();
    // Pass the intent to the SDK. You can also do this in onCreate().
    if (mediaManager.onNewIntent(getIntent())) {
      // If the SDK recognizes the intent, you should early return.
      return;
    }
    // If the SDK doesn't recognize the intent, you can handle the intent with
    // your own logic.
    ...
  }

  // For some cases, a new load intent triggers onNewIntent() instead of
  // onStart().
  @Override
  protected void onNewIntent(Intent intent) {
    MediaManager mediaManager =
        CastReceiverContext.getInstance().getMediaManager();
    // Pass the intent to the SDK. You can also do this in onCreate().
    if (mediaManager.onNewIntent(intent)) {
      // If the SDK recognizes the intent, you should early return.
      return;
    }
    // If the SDK doesn't recognize the intent, you can handle the intent with
    // your own logic.
    ...
  }
}

Если MediaManager обнаруживает, что намерение является намерением загрузки, он извлекает объект MediaLoadRequestData из намерения и вызывает MediaLoadCommandCallback.onLoad() . Вам необходимо переопределить этот метод для обработки запроса на загрузку. Обратный вызов должен быть зарегистрирован до вызова MediaManager.onNewIntent() (рекомендуется использовать метод onCreate() действия или приложения).

Котлин
class MyActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val mediaManager = CastReceiverContext.getInstance().getMediaManager()
        mediaManager.setMediaLoadCommandCallback(MyMediaLoadCommandCallback())
    }
}

class MyMediaLoadCommandCallback : MediaLoadCommandCallback() {
  override fun onLoad(
        senderId: String?,
        loadRequestData: MediaLoadRequestData
  ): Task {
      return Tasks.call {
        // Resolve the entity into your data structure and load media.
        val mediaInfo = loadRequestData.getMediaInfo()
        if (!checkMediaInfoSupported(mediaInfo)) {
            // Throw MediaException to indicate load failure.
            throw MediaException(
                MediaError.Builder()
                    .setDetailedErrorCode(DetailedErrorCode.LOAD_FAILED)
                    .setReason(MediaError.ERROR_REASON_INVALID_REQUEST)
                    .build()
            )
        }
        myFillMediaInfo(MediaInfoWriter(mediaInfo))
        myPlayerLoad(mediaInfo.getContentUrl())

        // Update media metadata and state (this clears all previous status
        // overrides).
        castReceiverContext.getMediaManager()
            .setDataFromLoad(loadRequestData)
        ...
        castReceiverContext.getMediaManager().broadcastMediaStatus()

        // Return the resolved MediaLoadRequestData to indicate load success.
        return loadRequestData
     }
  }

  private fun myPlayerLoad(contentURL: String) {
    myPlayer.load(contentURL)

    // Update the MediaSession state.
    val playbackState: PlaybackStateCompat = Builder()
        .setState(
            player.getState(), player.getPosition(), System.currentTimeMillis()
        )
        ...
        .build()
    mediaSession.setPlaybackState(playbackState)
  }
Джава
public class MyActivity extends Activity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    MediaManager mediaManager =
        CastReceiverContext.getInstance().getMediaManager();
    mediaManager.setMediaLoadCommandCallback(new MyMediaLoadCommandCallback());
  }
}

public class MyMediaLoadCommandCallback extends MediaLoadCommandCallback {
  @Override
  public Task onLoad(String senderId, MediaLoadRequestData loadRequestData) {
    return Tasks.call(() -> {
        // Resolve the entity into your data structure and load media.
        MediaInfo mediaInfo = loadRequestData.getMediaInfo();
        if (!checkMediaInfoSupported(mediaInfo)) {
          // Throw MediaException to indicate load failure.
          throw new MediaException(
              new MediaError.Builder()
                  .setDetailedErrorCode(DetailedErrorCode.LOAD_FAILED)
                  .setReason(MediaError.ERROR_REASON_INVALID_REQUEST)
                  .build());
        }
        myFillMediaInfo(new MediaInfoWriter(mediaInfo));
        myPlayerLoad(mediaInfo.getContentUrl());

        // Update media metadata and state (this clears all previous status
        // overrides).
        castReceiverContext.getMediaManager()
            .setDataFromLoad(loadRequestData);
        ...
        castReceiverContext.getMediaManager().broadcastMediaStatus();

        // Return the resolved MediaLoadRequestData to indicate load success.
        return loadRequestData;
    });
}

private void myPlayerLoad(String contentURL) {
  myPlayer.load(contentURL);

  // Update the MediaSession state.
  PlaybackStateCompat playbackState =
      new PlaybackStateCompat.Builder()
          .setState(
              player.getState(), player.getPosition(), System.currentTimeMillis())
          ...
          .build();
  mediaSession.setPlaybackState(playbackState);
}

Чтобы обработать намерение загрузки, вы можете проанализировать его в определенные нами структуры данных ( MediaLoadRequestData для запросов загрузки).

Поддержка медиа-команд

Поддержка базового управления воспроизведением

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

Котлин
private class MyMediaSessionCallback : MediaSessionCompat.Callback() {
  override fun onPause() {
    // Pause the player and update the play state.
    myPlayer.pause()
  }

  override fun onPlay() {
    // Resume the player and update the play state.
    myPlayer.play()
  }

  override fun onSeekTo(pos: Long) {
    // Seek and update the play state.
    myPlayer.seekTo(pos)
  }
    ...
 }

mediaSession.setCallback(MyMediaSessionCallback())
Джава
private class MyMediaSessionCallback extends MediaSessionCompat.Callback {
  @Override
  public void onPause() {
    // Pause the player and update the play state.
    myPlayer.pause();
  }
  @Override
  public void onPlay() {
    // Resume the player and update the play state.
    myPlayer.play();
  }
  @Override
  public void onSeekTo(long pos) {
    // Seek and update the play state.
    myPlayer.seekTo(pos);
  }

  ...
}

mediaSession.setCallback(new MyMediaSessionCallback());

Поддержка команд управления трансляцией

Некоторые команды Cast недоступны в MediaSession , например skipAd() или setActiveMediaTracks() . Кроме того, здесь необходимо реализовать некоторые команды очереди, поскольку очередь Cast не полностью совместима с очередью MediaSession .

Котлин
class MyMediaCommandCallback : MediaCommandCallback() {
    override fun onSkipAd(requestData: RequestData?): Task {
        // Skip your ad
        ...
        return Tasks.forResult(null)
    }
}

val mediaManager = CastReceiverContext.getInstance().getMediaManager()
mediaManager.setMediaCommandCallback(MyMediaCommandCallback())
Джава
public class MyMediaCommandCallback extends MediaCommandCallback {
  @Override
  public Task onSkipAd(RequestData requestData) {
    // Skip your ad
    ...
    return Tasks.forResult(null);
  }
}

MediaManager mediaManager =
    CastReceiverContext.getInstance().getMediaManager();
mediaManager.setMediaCommandCallback(new MyMediaCommandCallback());

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

Как и в случае с приемником Cast, в вашем приложении Android TV должно быть указано, какие команды поддерживаются, чтобы отправители могли включать или отключать определенные элементы управления пользовательского интерфейса. Для команд, которые являются частью MediaSession , укажите команды в PlaybackStateCompat . Дополнительные команды должны быть указаны в MediaStatusModifier .

Котлин
// Set media session supported commands
val playbackState: PlaybackStateCompat = PlaybackStateCompat.Builder()
    .setActions(PlaybackStateCompat.ACTION_PLAY or PlaybackStateCompat.ACTION_PAUSE)
    .setState(PlaybackStateCompat.STATE_PLAYING)
    .build()

mediaSession.setPlaybackState(playbackState)

// Set additional commands in MediaStatusModifier
val mediaManager = CastReceiverContext.getInstance().getMediaManager()
mediaManager.getMediaStatusModifier()
    .setMediaCommandSupported(MediaStatus.COMMAND_QUEUE_NEXT)
Джава
// Set media session supported commands
PlaybackStateCompat playbackState =
    new PlaybackStateCompat.Builder()
        .setActions(PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE)
        .setState(PlaybackStateCompat.STATE_PLAYING)
        .build();

mediaSession.setPlaybackState(playbackState);

// Set additional commands in MediaStatusModifier
MediaManager mediaManager = CastReceiverContext.getInstance().getMediaManager();
mediaManager.getMediaStatusModifier()
            .setMediaCommandSupported(MediaStatus.COMMAND_QUEUE_NEXT);

Скрыть неподдерживаемые кнопки

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

Изменение медиастатуса

Для поддержки расширенных функций, таких как треки, реклама, прямая трансляция и постановка в очередь, вашему приложению Android TV необходимо предоставить дополнительную информацию, которую невозможно получить с помощью MediaSession .

Для этого мы предоставляем класс MediaStatusModifier . MediaStatusModifier всегда будет работать с MediaSession , который вы установили в CastReceiverContext .

Чтобы создать и транслировать MediaStatus :

Котлин
val mediaManager: MediaManager = castReceiverContext.getMediaManager()
val statusModifier: MediaStatusModifier = mediaManager.getMediaStatusModifier()

statusModifier
    .setLiveSeekableRange(seekableRange)
    .setAdBreakStatus(adBreakStatus)
    .setCustomData(customData)

mediaManager.broadcastMediaStatus()
Джава
MediaManager mediaManager = castReceiverContext.getMediaManager();
MediaStatusModifier statusModifier = mediaManager.getMediaStatusModifier();

statusModifier
    .setLiveSeekableRange(seekableRange)
    .setAdBreakStatus(adBreakStatus)
    .setCustomData(customData);

mediaManager.broadcastMediaStatus();

Наша клиентская библиотека получит базовый MediaStatus из MediaSession , ваше приложение Android TV может указать дополнительный статус и переопределить статус с помощью модификатора MediaStatus .

Некоторые состояния и метаданные можно задать как в MediaSession , так и MediaStatusModifier . Мы настоятельно рекомендуем устанавливать их только в MediaSession . Вы по-прежнему можете использовать модификатор для переопределения состояний в MediaSession — это не рекомендуется, поскольку статус в модификаторе всегда имеет более высокий приоритет, чем значения, предоставляемые MediaSession .

Перехват MediaStatus перед отправкой

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

Котлин
mediaManager.setMediaStatusInterceptor(object : MediaStatusInterceptor {
    override fun intercept(mediaStatusWriter: MediaStatusWriter) {
      // Perform customization.
        mediaStatusWriter.setCustomData(JSONObject("{data: \"my Hello\"}"))
    }
})
Джава
mediaManager.setMediaStatusInterceptor(new MediaStatusInterceptor() {
    @Override
    public void intercept(MediaStatusWriter mediaStatusWriter) {
        // Perform customization.
        mediaStatusWriter.setCustomData(new JSONObject("{data: \"my Hello\"}"));
    }
});

Обработка учетных данных пользователя

Ваше приложение Android TV может разрешать только определенным пользователям запускать сеанс приложения или присоединяться к нему. Например, разрешите отправителю запускать или присоединяться только в том случае, если:

  • Приложение отправителя зарегистрировано в той же учетной записи и профиле, что и приложение ATV.
  • Приложение отправителя зарегистрировано в той же учетной записи, но в другом профиле, что и приложение ATV.

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

Когда ваше приложение-отправитель запускает приложение Android TV или присоединяется к нему, оно должно предоставить учетные данные, указывающие, кто присоединяется к сеансу.

Прежде чем отправитель запустит ваше приложение Android TV и присоединится к нему, вы можете указать средство проверки запуска, чтобы проверить, разрешены ли учетные данные отправителя. В противном случае Cast Connect SDK возвращается к запуску веб-приемника.

Данные учетных данных для запуска приложения отправителя

На стороне отправителя вы можете указать CredentialsData , чтобы указать, кто присоединяется к сеансу.

credentials — это строка, которую можно определить пользователем, если ваше приложение ATV ее понимает. credentialsType определяет, с какой платформы поступает CredentialsData , или может быть пользовательским значением. По умолчанию указывается платформа, с которой оно отправляется.

CredentialsData передаются в ваше приложение Android TV только во время запуска или присоединения. Если вы установите его снова, когда вы подключены, он не будет передан в ваше приложение Android TV. Если ваш отправитель переключает профиль во время подключения, вы можете либо остаться в сеансе, либо вызвать SessionManager.endCurrentCastSession(boolean stopCasting) если вы считаете, что новый профиль несовместим с сеансом.

CredentialsData для каждого отправителя можно получить с помощью getSenders в CastReceiverContext , чтобы получить SenderInfo , getCastLaunchRequest() , чтобы получить CastLaunchRequest , а затем getCredentialsData() .

Андроид

Требуется play-services-cast-framework версии 19.0.0 или выше.

Котлин
CastContext.getSharedInstance().setLaunchCredentialsData(
    CredentialsData.Builder()
        .setCredentials("{\"userId\": \"abc\"}")
        .build()
)
Джава
CastContext.getSharedInstance().setLaunchCredentialsData(
    new CredentialsData.Builder()
        .setCredentials("{\"userId\": \"abc\"}")
        .build());
iOS

Требуется google-cast-sdk версии v4.8.1 или выше.

Может быть вызван в любое время после установки параметров: GCKCastContext.setSharedInstanceWith(options) .

GCKCastContext.sharedInstance().setLaunch(
    GCKCredentialsData(credentials: "{\"userId\": \"abc\"}")
Интернет

Требуется браузер Chromium версии M87 или выше.

Может быть вызван в любое время после установки параметров: cast.framework.CastContext.getInstance().setOptions(options); .

let credentialsData =
    new chrome.cast.CredentialsData("{\"userId\": \"abc\"}");
cast.framework.CastContext.getInstance().setLaunchCredentialsData(credentialsData);

Реализация проверки запросов на запуск ATV

CredentialsData передаются в ваше приложение Android TV, когда отправитель пытается запуститься или присоединиться. Вы можете реализовать LaunchRequestChecker . разрешить или отклонить этот запрос.

Если запрос отклонен, веб-приемник загружается вместо запуска в приложении ATV. Вам следует отклонить запрос, если ваш ATV не может обработать пользователя, запрашивающего запуск или присоединение. Примером может служить то, что в приложение ATV вошел другой пользователь, чем запрашивает, и ваше приложение не может обрабатывать переключение учетных данных, или в настоящее время в приложении ATV нет пользователя, вошедшего в систему.

Если запрос разрешен, запускается приложение ATV. Вы можете настроить это поведение в зависимости от того, поддерживает ли ваше приложение отправку запросов на загрузку, когда пользователь не вошел в приложение ATV, или если имеется несоответствие пользователя. Это поведение полностью настраивается в LaunchRequestChecker .

Создайте класс, реализующий интерфейс CastReceiverOptions.LaunchRequestChecker :

Котлин
class MyLaunchRequestChecker : LaunchRequestChecker {
  override fun checkLaunchRequestSupported(launchRequest: CastLaunchRequest): Task {
    return Tasks.call {
      myCheckLaunchRequest(
           launchRequest
      )
    }
  }
}

private fun myCheckLaunchRequest(launchRequest: CastLaunchRequest): Boolean {
  val credentialsData = launchRequest.getCredentialsData()
     ?: return false // or true if you allow anonymous users to join.

  // The request comes from a mobile device, e.g. checking user match.
  return if (credentialsData.credentialsType == CredentialsData.CREDENTIALS_TYPE_ANDROID) {
     myCheckMobileCredentialsAllowed(credentialsData.getCredentials())
  } else false // Unrecognized credentials type.
}
Джава
public class MyLaunchRequestChecker
    implements CastReceiverOptions.LaunchRequestChecker {
  @Override
  public Task checkLaunchRequestSupported(CastLaunchRequest launchRequest) {
    return Tasks.call(() -> myCheckLaunchRequest(launchRequest));
  }
}

private boolean myCheckLaunchRequest(CastLaunchRequest launchRequest) {
  CredentialsData credentialsData = launchRequest.getCredentialsData();
  if (credentialsData == null) {
    return false;  // or true if you allow anonymous users to join.
  }

  // The request comes from a mobile device, e.g. checking user match.
  if (credentialsData.getCredentialsType().equals(CredentialsData.CREDENTIALS_TYPE_ANDROID)) {
    return myCheckMobileCredentialsAllowed(credentialsData.getCredentials());
  }

  // Unrecognized credentials type.
  return false;
}

Затем установите его в своем ReceiverOptionsProvider :

Котлин
class MyReceiverOptionsProvider : ReceiverOptionsProvider {
  override fun getOptions(context: Context?): CastReceiverOptions {
    return CastReceiverOptions.Builder(context)
        ...
        .setLaunchRequestChecker(MyLaunchRequestChecker())
        .build()
  }
}
Джава
public class MyReceiverOptionsProvider implements ReceiverOptionsProvider {
  @Override
  public CastReceiverOptions getOptions(Context context) {
    return new CastReceiverOptions.Builder(context)
        ...
        .setLaunchRequestChecker(new MyLaunchRequestChecker())
        .build();
  }
}

При разрешении true в LaunchRequestChecker запускается приложение ATV, а при значении false — приложение веб-приемника.

Отправка и получение пользовательских сообщений

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

Android TV: укажите собственное пространство имен.

Вам необходимо указать поддерживаемые пространства имен в CastReceiverOptions во время установки:

Котлин
class MyReceiverOptionsProvider : ReceiverOptionsProvider {
  override fun getOptions(context: Context?): CastReceiverOptions {
    return CastReceiverOptions.Builder(context)
        .setCustomNamespaces(
            Arrays.asList("urn:x-cast:com.example.cast.mynamespace")
        )
        .build()
  }
}
Джава
public class MyReceiverOptionsProvider implements ReceiverOptionsProvider {
  @Override
  public CastReceiverOptions getOptions(Context context) {
    return new CastReceiverOptions.Builder(context)
        .setCustomNamespaces(
              Arrays.asList("urn:x-cast:com.example.cast.mynamespace"))
        .build();
  }
}

Android TV – отправка сообщений

Котлин
// If senderId is null, then the message is broadcasted to all senders.
CastReceiverContext.getInstance().sendMessage(
    "urn:x-cast:com.example.cast.mynamespace", senderId, customString)
Джава
// If senderId is null, then the message is broadcasted to all senders.
CastReceiverContext.getInstance().sendMessage(
    "urn:x-cast:com.example.cast.mynamespace", senderId, customString);

Android TV: получение пользовательских сообщений пространства имен

Котлин
class MyCustomMessageListener : MessageReceivedListener {
    override fun onMessageReceived(
        namespace: String, senderId: String?, message: String ) {
        ...
    }
}

CastReceiverContext.getInstance().setMessageReceivedListener(
    "urn:x-cast:com.example.cast.mynamespace", new MyCustomMessageListener());
Джава
class MyCustomMessageListener implements CastReceiverContext.MessageReceivedListener {
  @Override
  public void onMessageReceived(
      String namespace, String senderId, String message) {
    ...
  }
}

CastReceiverContext.getInstance().setMessageReceivedListener(
    "urn:x-cast:com.example.cast.mynamespace", new MyCustomMessageListener());