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

このページでは、Android TV レシーバー アプリのカスタマイズに使用できる機能のコード スニペットと説明について説明します。

ライブラリの構成

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

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

Cast サポートの構成

送信元のアプリから起動リクエストが送信されると、アプリケーション名前空間を持つインテントの作成がトリガーされます。アプリは、これを処理し、テレビアプリの起動時に CastReceiverContext オブジェクトのインスタンスを作成します。CastReceiverContext オブジェクトは、TV アプリの実行中に Cast を操作するために必要です。このオブジェクトを使用すると、接続された送信者からの Cast メディア メッセージを 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 フラグは LaunchOptionsCastOptions の一部)で設定されます。

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 以降が必要です。

androidReceiverCompatible フラグは GCKLaunchOptionsGCKCastOptions の一部)で設定されます。

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 Developer Console で Android TV アプリのパッケージ名を追加して、Cast アプリ ID に関連付けます。

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

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

登録しないと、セキュリティ上の理由により、Cast Connect は Google Play ストアからインストールしたアプリでしか動作しません。

Cast デベロッパー向けに Cast デバイスまたは 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 認証情報を設定する

ウェブ レシーバー アプリと 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 による読み込み

entity または atvEntity を使用しておらず、メディア情報でコンテンツ 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() が呼び出される前に登録する必要があります(Activity または Application の 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());

Cast コントロール コマンドのサポート

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

Kotlin
class MyMediaCommandCallback : MediaCommandCallback() {
    override fun onSkipAd(requestData: RequestData?): Task<Void?> {
        // 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());

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

Cast レシーバーと同様に、Android TV アプリでは、送信側が特定の UI コントロールを有効または無効にできるように、サポートされているコマンドを指定する必要があります。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 アプリが基本的なメディア操作のみをサポートし、Web レシーバー アプリがより高度な操作をサポートしている場合は、Android TV アプリにキャストするときに送信元アプリが正しく動作するようにする必要があります。たとえば、Android TV アプリが再生レートの変更をサポートしていないが、Web レシーバー アプリがサポートしている場合は、各プラットフォームでサポートされているアクションを正しく設定し、送信元アプリが UI を正しくレンダリングするようにする必要があります。

MediaStatus の変更

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

そのために、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();

クライアント ライブラリは 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 アプリを起動して参加する前に、起動チェックを指定して、送信者の認証情報が許可されているかどうかを確認できます。そうでない場合、Cast Connect SDK はウェブ レシーバーの起動にフォールバックします。

送信者のアプリ起動認証情報データ

送信側では、セッションに参加するユーザーを表す 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.3 以降が必要です。

オプションが設定された後でいつでも呼び出せます。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 でウェブ レシーバー アプリが起動します。

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

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

Android TV - カスタム Namespace を指定する

セットアップ時に、サポートされている Namespace を 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());