AndroidアプリにGoogle Cast機能を追加する

# 概要
cast機能とは、AndroidアプリやiOSアプリをChromecastやNexus Playerなどのcastデバイスに接続し、主に動画コンテンツをTVに表示する機能である。その実装は大きくsender(アプリ側)とreceiver(castデバイス側)に分かれる。
senderはアプリに組み込む形でcast機能を実装する。receiverはhtml、css、javascriptからなるwebアプリケーションであり、アプリとcastデバイスとが接続した際にcastデバイス自身がダウンロードし、castデバイス上で動作する。receiverには、以下の3種類が存在している。
- Default Media Receiver
- Styled Media Receiver
- Custom Receiver
Default Media Receiverはgoogle提供のreceiverであり、カスタマイズはできないがsender側からidを指定するだけで利用できる。今回はDefault Media Receiverを利用するものとして説明を進める。

# サンプルプロジェクト
googleのサンプルをベースに実装を進めていく。
githubを確認すると以下のリポジトリが存在していることが分かる。
https://github.com/googlecast/CastVideos-android
https://github.com/googlecast/CastVideos-android-v2
https://github.com/googlecast/CastCompanionLibrary-android

CastVideos-android-v2が星の数も多く新しいプロジェクトであるように見えるがこれは罠である。
このv2はcast sdk v2に対応しているということであり、実際はCastVideos-androidの方が新しいプロジェクトであり、cast sdk v3に対応しているものなので、これからcast機能を実装しようという人はこちらを参考にすべきである。また、CastCompanionLibrary-androidという便利そうなライブラリが存在しているが、こちらもcast sdk v2に対応したものなので、これからcast機能を実装しようという人は利用しない方が良い。

# ライブラリの取り込み
gradleに以下の記述を追加する。

```gradle:build.gradle
    dependencies {
        compile 'com.android.support:appcompat-v7:25.0.1'
        compile 'com.android.support:mediarouter-v7:25.0.1'
        compile 'com.google.android.gms:play-services-cast-framework:10.0.1'
    }
```
 Google Play servicesを取り込むため、この時点でAmazonアプリストアへはリリースできなくなる。

# CastOptionsProviderクラスの追加

```CastOptionsProvider.java
class CastOptionsProvider implements OptionsProvider {
    @Override
    public CastOptions getCastOptions(Context context) {
        CastOptions castOptions = new CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .build();
        return castOptions;
    }
    @Override
    public List<SessionProvider> getAdditionalSessionProviders(Context context) {
        return null;
    }
}
```
app_idはreceiverと紐づくIDである。Default Media Receiverを指定する場合は、`CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID`を指定すればよい。
Default Media Receiver以外を指定する場合はreceiverを作成し、ネットワーク上に配置した上でGoogle Cast SDK Developer Consoleでreceiverを登録しapp_idを発行してもらう必要がある。登録には$5かかるが、receiverのカスタマイズをしたい場合は必須となる。

```xml:AndroidManifest.xml
<application>
    <uses-permission android:name="android.permission.INTERNET"/>
    ...
    <meta-data
        android:name=
            "com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
        android:value="com.example.casttest.CastOptionsProvider" />
</application>
```
INTERNETのpermissionが必要である。

# CastContextの取得
cast機能のmanager的な存在である。シングルトンで提供されている。

```java
public class MainActivity extends FragmentActivity {
    private CastContext mCastContext;

    @Override
    public void onCreate() {
        ...
        mCastContext = CastContext.getSharedInstance(this);
        ...
    }
}
```

# Castボタンの追加
メニューに追加する方法と、ボタンとして任意の位置にレイアウトする方法の2種類がある。

```java:メニューに追加
// res/menu/main.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
        android:id="@+id/media_route_menu_item"
        android:title="media_route_button"
        app:actionProviderClass="android.support.v7.app.MediaRouteActionProvider"
        app:showAsAction="always" />
</menu>

// MainActivity.java
@Override
public boolean onCreateOptionsMenu(Menu menu) {
    super.onCreateOptionsMenu(menu);
    getMenuInflater().inflate(R.menu.main, menu);
    CastButtonFactory.setUpMediaRouteButton(getApplicationContext(),　menu,　R.id.media_route_menu_item);
    return true;
}
```

```java:ボタンとしてレイアウト
// res/layout/activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:gravity="center_vertical"
   android:orientation="horizontal" >

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

</LinearLayout>

// MainActivity.java
public class MainActivity extends FragmentActivity {
    ...
    private MediaRouteButton mMediaRouteButton;
    ...

    @Override
    public void onCreate() {
        ...
        mMediaRouteButton = (MediaRouteButton) findViewById(R.id.media_route_button);
        CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), mMediaRouteButton);
        ...
     }
}
```

とりあえず、ここまでの実装でアプリとcastデバイスを接続することが可能となっている。

![1.png](https://qiita-image-store.s3.amazonaws.com/0/141937/e88abcbd-8003-1e9b-76cd-bf0f21158b39.png)

![2.png](https://qiita-image-store.s3.amazonaws.com/0/141937/4032df66-762f-423b-3cca-bb939cb3912b.png)

![3.png](https://qiita-image-store.s3.amazonaws.com/0/141937/9c4f23cd-4ec9-b92b-9758-5562faf801ee.png)

TV側にはcastアイコンが表示されている。

# SessionManagerListenerの登録
castデバイスに接続した際や切断した際などにコールバックを受けることができる。

```java:MainActivity.java
public class MainActivity extends AppCompatActivity {
    private final SessionManagerListener mSessionManagerListener = new SessionManagerListenerImpl();

    @Override
    protected void onResume() {
        mCastContext.getSessionManager().addSessionManagerListener(mSessionManagerListener, CastSession.class);
        super.onResume();
    }

    @Override
    protected void onPause() {
        super.onPause();
        mCastContext.getSessionManager().removeSessionManagerListener(mSessionManagerListener, CastSession.class);
    }

    private class SessionManagerListenerImpl implements SessionManagerListener<CastSession> {
        @Override
        public void onSessionStarting(CastSession castSession) {
        }

        @Override
        public void onSessionStarted(CastSession castSession, String s) {
        }

        @Override
        public void onSessionStartFailed(CastSession castSession, int i) {
        }

        @Override
        public void onSessionEnding(CastSession castSession) {
        }

        @Override
        public void onSessionEnded(CastSession castSession, int i) {
        }

        @Override
        public void onSessionResuming(CastSession castSession, String s) {
        }

        @Override
        public void onSessionResumed(CastSession castSession, boolean b) {
        }

        @Override
        public void onSessionResumeFailed(CastSession castSession, int i) {
        }

        @Override
        public void onSessionSuspended(CastSession castSession, int i) {
        }
    }
}
```

# 動画再生
動画をcastデバイスで再生させるには、MediaInfoにパラメーターを設定し、ロードすればよい。

```java
@Override
public void onSessionStarted(CastSession session, String sessionId) {
            MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
            movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, "sub title");
            movieMetadata.putString(MediaMetadata.KEY_TITLE, "title");
            movieMetadata.addImage(new WebImage(Uri.parse("https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/images/480x270/DesigningForGoogleCast2-480x270.jpg")));
            movieMetadata.addImage(new WebImage(Uri.parse("https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/images/780x1200/DesigningForGoogleCast-887x1200.jpg")));
            MediaInfo mediaInfo = new MediaInfo.Builder("https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/hls/DesigningForGoogleCast.m3u8")
                    .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
                    .setContentType("videos/mp4")
                    .setMetadata(movieMetadata)
                    .build();
            final RemoteMediaClient remoteMediaClient = session.getRemoteMediaClient();
            remoteMediaClient.load(mediaInfo, true, 0);
}
```
とりあえず、これでTVに動画を表示させることができた。

# MiniController
MiniControllerを追加したい場合は、以下の記述をレイアウトのxmlに追加すればよい。

```xml
<fragment
    android:id="@+id/castMiniController"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:visibility="gone"
    class="com.google.android.gms.cast.framework.media.widget.
           MiniControllerFragment" />
```
特にソースコードに処理を追加しなくても、cast再生中に自動で表示される。

![4.png](https://qiita-image-store.s3.amazonaws.com/0/141937/c0df331c-5f75-0531-7477-b123cb1aa8af.png)

# Expanded Controller
フルスクリーンのExpanded Controllerも簡単に追加することができる。

```java:ExpandedControlsActivity.java
public class ExpandedControlsActivity extends ExpandedControllerActivity {

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        getMenuInflater().inflate(R.menu.main, menu);
        CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item);
        return true;
    }
}
```

```xml:AndroidManifest.xml
<application>
...
<activity
        android:name=".ExpandedControlsActivity"
        android:label="@string/app_name"
        android:launchMode="singleTask"
        android:theme="@style/Theme.AppCompat.NoActionBar"
        android:screenOrientation="portrait">
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
    </intent-filter>
    <meta-data
            android:name="android.support.PARENT_ACTIVITY"
            android:value="com.example.casttest.MainActivity"/>
</activity>
...
</application>
```

```java:CastOptionsProvider.java
public class CastOptionsProvider implements OptionsProvider {
    @Override
    public CastOptions getCastOptions(Context context) {
        NotificationOptions notificationOptions = new NotificationOptions.Builder()
                .setTargetActivityClassName(ExpandedControlsActivity.class.getName())
                .build();
        CastMediaOptions mediaOptions = new CastMediaOptions.Builder()
                .setNotificationOptions(notificationOptions)
                .setExpandedControllerActivityClassName(ExpandedControlsActivity.class.getName())
                .build();
        CastOptions castOptions = new CastOptions.Builder()
                .setReceiverApplicationId(context.getString(R.string.app_id))
                .setCastMediaOptions(mediaOptions)
                .build();
        return castOptions;
    }
    ...
}
```

cast再生を開始し、MiniControllerをタップするとExpandedControllerが表示される。

![device-2017-03-20-213624.png](https://qiita-image-store.s3.amazonaws.com/0/141937/4b68e2f5-7988-d780-1067-6d33289753b2.png)

この状態で右上のcastアイコンをタップすると、DialogのControllerも表示される。

![device-2017-03-20-213631.png](https://qiita-image-store.s3.amazonaws.com/0/141937/dc7a1a69-a530-82f8-9f56-6a1859916c85.png)

# まとめ
とりあえず、ここまでの実装でアプリをcastデバイスに接続し、動画をTVで再生させることができるようになった。cast再生中のnotification表示やlock画面上での表示にも対応できている。実際にアプリに組み込んでリリースするためには、cast再生とアプリ内再生の制御や、UIのカスタマイズなどが必要になると思われる。