Android TV レシーバーに主要な機能を追加する

このページには、Android TV レシーバー アプリをカスタマイズするために利用できる機能のコード スニペットと説明が掲載されています。

ライブラリの構成

Android TV アプリで Cast Connect API を利用できるようにするには:

Android
  1. アプリケーション モジュール ディレクトリ内で build.gradle ファイルを開きます。
  2. リストされた repositoriesgoogle() が含まれていることを確認します。
      repositories {
        google()
      }
  3. アプリのターゲット デバイスのタイプに応じて、最新バージョンのライブラリを依存関係に追加します。
    • Android レシーバー アプリの場合:
        dependencies {
          implementation 'com.google.android.gms:play-services-cast-tv:21.0.0'
          implementation 'com.google.android.gms:play-services-cast:21.3.0'
        }
    • Android センダーアプリの場合:
        dependencies {
          implementation 'com.google.android.gms:play-services-cast:21.0.0'
          implementation 'com.google.android.gms:play-services-cast-framework:21.3.0'
        }
    サービスが更新されるたびに、このバージョン番号を更新してください。
  4. 変更を保存し、ツールバーの Sync Project with Gradle Files をクリックします。
iOS
  1. Podfilegoogle-cast-sdk 4.8.0 以降をターゲットとしていることを確認します。
  2. iOS 13 以降をターゲットに設定していること。詳しくは、リリースノートをご覧ください。
      platform: ios, '13'
    
      def target_pods
         pod 'google-cast-sdk', '~>4.8.0'
      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 アプリ - 前提条件

Android TV アプリで Cast Connect をサポートするには、メディア セッションからイベントを作成してサポートする必要があります。メディア セッションによって提供されるデータから、メディア ステータスの基本情報(位置、再生状態など)がわかります。メディア セッションは、一時停止など、送信者から特定のメッセージを受信したときに、Cast Connect ライブラリによっても使用されます。

メディア セッションと、メディア セッションを初期化する方法の詳細については、メディア セッションの操作ガイドをご覧ください。

メディア セッションのライフサイクル

アプリは、再生の開始時にメディア セッションを作成し、制御ができなくなった時点でメディア セッションを解放する必要があります。たとえば動画アプリの場合は、ユーザーが再生アクティビティを終了したとき(他のコンテンツを閲覧するために [戻る] を選択するか、アプリをバックグラウンド化することで)セッションを解放する必要があります。音楽アプリの場合は、メディアを再生しなくなったらセッションを解放してください。

セッション ステータスを更新しています

メディア セッションのデータは、プレーヤーのステータスに合わせて最新の状態に保つ必要があります。たとえば、再生が一時停止された場合は、再生状態とサポートされているアクションを更新する必要があります。次の表に、最新の状態に維持する必要のある状態を示します。

MediaMetadataCompat

メタデータ フィールド 説明
METADATA_KEY_TITLE(必須) メディアのタイトル。
METADATA_KEY_DISPLAY_SUBTITLE サブタイトル。
METADATA_KEY_DISPLAY_ICON_URI アイコンの URL。
METADATA_KEY_DURATION(必須) メディアの再生時間。
METADATA_KEY_MEDIA_URI Content ID
METADATA_KEY_ARTIST アーティスト。
METADATA_KEY_ALBUM アルバム。

PlaybackStateCompat

必須のメソッド 説明
setActions() サポートされているメディア コマンドを設定します。
setState() 再生状態と現在の位置を設定します。

MediaSessionCompat

必須のメソッド 説明
setRepeatMode() 繰り返しモードを設定します。
setShuffleMode() シャッフル モードを設定します。
setMetadata() メディア メタデータを設定します。
setPlaybackState() 再生状態を設定します。
Kotlin
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)
}
Java
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() 現在のメディアを停止
Kotlin
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() );
Java
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());

キャスト サポートの構成

送信側アプリによって起動リクエストが送信されると、アプリの名前空間を持つインテントが作成されます。アプリは、TV アプリの起動時にこれを処理し、CastReceiverContext オブジェクトのインスタンスを作成します。CastReceiverContext オブジェクトは、TV アプリの実行中にキャストを操作するために必要です。このオブジェクトにより、TV アプリは接続済みのセンダーからのキャスト メディア メッセージを受け入れることができます。

Android TV のセットアップ

起動インテント フィルタの追加

送信側アプリからの起動インテントを処理するアクティビティに新しいインテント フィルタを追加します。

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

レシーバー オプション プロバイダを指定する

CastReceiverOptions を指定するには、ReceiverOptionsProvider を実装する必要があります。

Kotlin
class MyReceiverOptionsProvider : ReceiverOptionsProvider {
  override fun getOptions(context: Context?): CastReceiverOptions {
    return CastReceiverOptions.Builder(context)
          .setStatusText("My App")
          .build()
    }
}
Java
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 は、CastReceiverContext の初期化時に CastReceiverOptions を提供するために使用されます。

キャスト レシーバーのコンテキスト

アプリの作成時に CastReceiverContext を初期化します。

Kotlin
override fun onCreate() {
  CastReceiverContext.initInstance(this)

  ...
}
Java
@Override
public void onCreate() {
  CastReceiverContext.initInstance(this);

  ...
}

アプリがフォアグラウンドに移動したら、CastReceiverContext を開始します。

Kotlin
CastReceiverContext.getInstance().start()
Java
CastReceiverContext.getInstance().start();

動画アプリまたはバックグラウンド再生をサポートしていないアプリのアプリがバックグラウンドに移動したら、CastReceiverContextstop() を呼び出します。

Kotlin
// Player has stopped.
CastReceiverContext.getInstance().stop()
Java
// Player has stopped.
CastReceiverContext.getInstance().stop();

また、アプリがバックグラウンドでの再生をサポートしている場合は、バックグラウンドでの再生が停止したときに CastReceiverContextstop() を呼び出します。

特にネイティブ アプリに複数のアクティビティがある場合は、androidx.lifecycle ライブラリの LifecycleObserver を使用して、CastReceiverContext.start()CastReceiverContext.stop() の呼び出しを管理することを強くおすすめします。これにより、start()stop() を異なるアクティビティから呼び出す際の競合状態を回避できます。

Kotlin
// 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())
  }
}
Java
// 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 に提供する必要もあります。これにより、コマンドの送信先を把握し、メディアの再生状態を取得できます。

Kotlin
val mediaManager: MediaManager = receiverContext.getMediaManager()
mediaManager.setSessionCompatToken(currentMediaSession.getSessionToken())
Java
MediaManager mediaManager = receiverContext.getMediaManager();
mediaManager.setSessionCompatToken(currentMediaSession.getSessionToken());

再生がアクティブでないときに MediaSession を解放する場合は、MediaManager に null トークンを設定する必要があります。

Kotlin
myPlayer.stop()
mediaSession.release()
mediaManager.setSessionCompatToken(null)
Java
myPlayer.stop();
mediaSession.release();
mediaManager.setSessionCompatToken(null);

アプリがバックグラウンドで実行されている間のメディア再生をサポートしている場合は、アプリがバックグラウンドに送信されるときに CastReceiverContext.stop() を呼び出すのではなく、アプリがバックグラウンドにありメディアを再生しなくなった場合にのみ呼び出す必要があります。例:

Kotlin
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()
  }
}
Java
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();
  }
}

Cast Connect で ExoPlayer を使用する

Exoplayer を使用している場合、MediaSessionConnector を使用すると、変更を手動で追跡する代わりに、セッションと再生状態を含むすべての関連情報を自動的に維持できます。

MediaSessionConnector.MediaButtonEventHandler を使用すると、setMediaButtonEventHandler(MediaButtonEventHandler) を呼び出して MediaButton イベントを処理できます。それ以外の場合は、デフォルトで MediaSessionCompat.Callback によって処理されます。

MediaSessionConnector をアプリに統合するには、プレーヤー アクティビティ クラスまたはメディア セッションを管理する場所に、次のコードを追加します。

Kotlin
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)
    ...
  }
}
Java
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 をサポートするようにセンダーアプリを更新したら、LaunchOptionsandroidReceiverCompatible フラグを true に設定して、準備状況を宣言できます。

Android

play-services-cast-framework バージョン 19.0.0 以降が必要です。

androidReceiverCompatible フラグは、CastOptions の一部である LaunchOptions で設定されます。

Kotlin
class CastOptionsProvider : OptionsProvider {
  override fun getCastOptions(context: Context?): CastOptions {
    val launchOptions: LaunchOptions = Builder()
          .setAndroidReceiverCompatible(true)
          .build()
    return CastOptions.Builder()
          .setLaunchOptions(launchOptions)
          ...
          .build()
    }
}
Java
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 以降が必要です。

GCKCastOptions の一部である GCKLaunchOptionsandroidReceiverCompatible フラグを設定します。

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 Developer Console の設定

Android TV アプリを構成する

Cast デベロッパー コンソールに Android TV アプリのパッケージ名を追加して、キャストアプリ ID と関連付けます。

デベロッパー デバイスを登録する

Cast Developer Console で、開発に使用する Android TV デバイスのシリアル番号を登録します。

セキュリティ上の理由から、登録していない場合、Cast Connect は Google Play ストアからインストールされたアプリでしか機能しません。

キャスト開発用にキャスト デバイスまたは Android TV デバイスを登録する方法について詳しくは、登録ページをご覧ください。

メディアの読み込み

ディープリンク サポートを 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 を設定することで、ディープリンクを渡すことができます。

Kotlin
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)
Android
Java
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 認証情報の設定

Web Receiver アプリと Android TV アプリは、異なるディープリンクと credentials をサポートする可能性があります(2 つのプラットフォームで異なる認証を処理する場合など)。これに対処するには、Android TV 用に別の entitycredentials を指定します。

Android
Kotlin
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)
Java
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);

ウェブ レシーバー アプリが起動されると、読み込みリクエストで entitycredentials が使用されます。ただし、Android TV アプリが起動されると、SDK は entitycredentialsatvEntityatvCredentials(指定されている場合)でオーバーライドします。

Content ID または MediaQueueData で読み込む

entityatvEntity を使用せず、メディア情報で 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() を呼び出すことができます。

Android
Kotlin
val mediaToLoad = MediaInfo.Builder("some-id").build()
val loadRequest = MediaLoadRequestData.Builder()
    .setMediaInfo(mediaToLoad)
    .setCredentials("user-credentials")
    ...
    .build()
remoteMediaClient.load(loadRequest)
Java
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);

読み込みリクエストの処理

アクティビティでこれらの読み込みリクエストを処理するには、アクティビティのライフサイクル コールバックでインテントを処理する必要があります。

Kotlin
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.
    ...
  }
}
Java
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() メソッドで登録することをおすすめします)。

Kotlin
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)
  }
Java
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)に変換します。

メディア コマンドをサポートする

基本的な再生コントロールのサポート

基本的な統合コマンドには、メディア セッションと互換性のあるコマンドが含まれています。これらのコマンドは、メディア セッション コールバックを介して通知されます。これをサポートするには、メディア セッションにコールバックを登録する必要があります(すでに登録している可能性があります)。

Kotlin
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())
Java
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());

キャスト操作コマンドのサポート

ただし、skipAd()setActiveMediaTracks() など、MediaSession では使用できないキャスト コマンドもあります。また、キャストキューは MediaSession キューと完全には互換性がないため、一部のキューコマンドをここに実装する必要があります。

Kotlin
class MyMediaCommandCallback : MediaCommandCallback() {
    override fun onSkipAd(requestData: RequestData?): Task {
        // Skip your ad
        ...
        return Tasks.forResult(null)
    }
}

val mediaManager = CastReceiverContext.getInstance().getMediaManager()
mediaManager.setMediaCommandCallback(MyMediaCommandCallback())
Java
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());

サポートされているメディア コマンドを指定する

キャスト レシーバーと同様に、センダーが特定の UI コントロールを有効または無効にすることができるように、Android TV アプリでサポートされているコマンドを指定する必要があります。MediaSession に含まれるコマンドの場合は、PlaybackStateCompat でコマンドを指定します。追加のコマンドは MediaStatusModifier で指定する必要があります。

Kotlin
// 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)
Java
// 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 アプリが再生レートの変更をサポートしていないのに対し、ウェブレシーバー アプリはサポートしている場合は、各プラットフォームでサポートされているアクションを正しく設定し、センダーアプリで UI が正しくレンダリングされることを確認する必要があります。

MediaStatus の変更

トラック、広告、ライブ、キューなどの高度な機能をサポートするには、Android TV アプリで MediaSession では確認できない追加情報を提供する必要があります。

これを実現するために、MediaStatusModifier クラスが用意されています。MediaStatusModifier は常に CastReceiverContext で設定した MediaSession で動作します。

MediaStatus を作成してブロードキャストするには:

Kotlin
val mediaManager: MediaManager = castReceiverContext.getMediaManager()
val statusModifier: MediaStatusModifier = mediaManager.getMediaStatusModifier()

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

mediaManager.broadcastMediaStatus()
Java
MediaManager mediaManager = castReceiverContext.getMediaManager();
MediaStatusModifier statusModifier = mediaManager.getMediaStatusModifier();

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

mediaManager.broadcastMediaStatus();

Google のクライアント ライブラリは MediaSession からベース MediaStatus を取得します。Android TV アプリでは、MediaStatus 修飾子を使用して追加ステータスを指定し、ステータスをオーバーライドできます。

一部の状態とメタデータは MediaSessionMediaStatusModifier の両方で設定できます。MediaSession のみで設定することを強くおすすめします。その場合でも、修飾子を使用して MediaSession の状態をオーバーライドできます。修飾子のステータスの方が MediaSession によって提供される値よりも常に優先度が高いため、この方法は推奨されません。

送信前に MediaStatus をインターセプトする

Web Receiver SDK と同様に、送信前に最後の仕上げを行う場合は、MediaStatusInterceptor を指定して送信する MediaStatus を処理します。MediaStatusWriter を渡して、送信前に MediaStatus を操作します。

Kotlin
mediaManager.setMediaStatusInterceptor(object : MediaStatusInterceptor {
    override fun intercept(mediaStatusWriter: MediaStatusWriter) {
      // Perform customization.
        mediaStatusWriter.setCustomData(JSONObject("{data: \"my Hello\"}"))
    }
})
Java
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 アプリに参加したときに、送信側アプリはセッションに参加するユーザーを表す認証情報を提供する必要があります。

センダーが起動して Android TV アプリに参加する前に、リリース チェッカーを指定して、送信者の認証情報が許可されているかどうかを確認できます。インストールされていない場合、Cast Connect SDK は Web Receiver の起動にフォールバックします。

センダーアプリの起動認証情報データ

センダー側では、セッションに参加しているユーザーを表す CredentialsData を指定できます。

credentials は、ATV アプリが理解できる限り、ユーザー定義可能な文字列です。credentialsType は、CredentialsData の取得元のプラットフォームを定義します。カスタム値にすることもできます。デフォルトでは送信元のプラットフォームに設定されています。

CredentialsData は、起動時または参加時にのみ Android TV アプリに渡されます。接続中に設定し直しても、Android TV アプリに渡されません。センダーが接続中にプロファイルを切り替えた場合は、セッションを続けるか、新しいプロファイルがセッションに対応していないと思われる場合は SessionManager.endCurrentCastSession(boolean stopCasting) を呼び出すことができます。

各送信者の CredentialsData は、CastReceiverContextgetSenders を使用して SenderInfo を取得し、getCastLaunchRequest()CastLaunchRequest を取得し、getCredentialsData() を使用して取得できます。

Android

play-services-cast-framework バージョン 19.0.0 以降が必要です。

Kotlin
CastContext.getSharedInstance().setLaunchCredentialsData(
    CredentialsData.Builder()
        .setCredentials("{\"userId\": \"abc\"}")
        .build()
)
Java
CastContext.getSharedInstance().setLaunchCredentialsData(
    new CredentialsData.Builder()
        .setCredentials("{\"userId\": \"abc\"}")
        .build());
iOS

google-cast-sdk バージョン v4.8.0 以降が必要です。

オプションの設定後は、いつでも呼び出すことができます: 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 インターフェースを実装するクラスを作成します。

Kotlin
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.
}
Java
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 で設定します。

Kotlin
class MyReceiverOptionsProvider : ReceiverOptionsProvider {
  override fun getOptions(context: Context?): CastReceiverOptions {
    return CastReceiverOptions.Builder(context)
        ...
        .setLaunchRequestChecker(MyLaunchRequestChecker())
        .build()
  }
}
Java
public class MyReceiverOptionsProvider implements ReceiverOptionsProvider {
  @Override
  public CastReceiverOptions getOptions(Context context) {
    return new CastReceiverOptions.Builder(context)
        ...
        .setLaunchRequestChecker(new MyLaunchRequestChecker())
        .build();
  }
}

LaunchRequestCheckertrue を解決すると、ATV アプリが起動され、false によってウェブレシーバー アプリが起動します。

カスタム メッセージの送受信

キャスト プロトコルを使用すると、センダーとレシーバー アプリ間でカスタム文字列メッセージを送信できます。CastReceiverContext を初期化する前に、メッセージを送信する名前空間(チャネル)を登録する必要があります。

Android TV - カスタム名前空間を指定する

設定時に、サポートされている名前空間を CastReceiverOptions で指定する必要があります。

Kotlin
class MyReceiverOptionsProvider : ReceiverOptionsProvider {
  override fun getOptions(context: Context?): CastReceiverOptions {
    return CastReceiverOptions.Builder(context)
        .setCustomNamespaces(
            Arrays.asList("urn:x-cast:com.example.cast.mynamespace")
        )
        .build()
  }
}
Java
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 - メッセージの送信

Kotlin
// If senderId is null, then the message is broadcasted to all senders.
CastReceiverContext.getInstance().sendMessage(
    "urn:x-cast:com.example.cast.mynamespace", senderId, customString)
Java
// 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 - カスタム名前空間メッセージを受信する

Kotlin
class MyCustomMessageListener : MessageReceivedListener {
    override fun onMessageReceived(
        namespace: String, senderId: String?, message: String ) {
        ...
    }
}

CastReceiverContext.getInstance().setMessageReceivedListener(
    "urn:x-cast:com.example.cast.mynamespace", new MyCustomMessageListener());
Java
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());