Chromecast - Google Cast SDKでオレオレPlay Musicをつくる

ついに日本で発売されました、Chromecast。
AndroidでGoogle Cast SDKを利用してオレオレPlay Musicを実装してみます。

# 下準備

## ChromeCast
Chromecastを端末と同じネットワーク上に設定しておきます。
以下のアプリをインストールし、説明にしたがって設定します。

https://play.google.com/store/apps/details?id=com.google.android.apps.chromecast.app

## CastCompainionLibrary

Google Cast SDKを利用するのはちょっと面倒な感じですが、簡単に利用できるようにするためのLibrary Projectが公開されているので（素敵！）、それを利用してお手軽に実装します。

以下からLibrary Projectを取得してください。
https://github.com/googlecast/CastCompanionLibrary-android

## 再生するメディア

Chromecastからもアクセス可能なネットワーク上のどこかに再生対象のmp4ファイル等を置いておきます。
（DropBoxにおいて、公開URLを取得する等）

Chromecastがあっても再生するものがなければ何もできません...。
ぶっちゃけ、再生するメディアを用意する必要があるなら、結局のところ既存のアプリ（You TubeやGoogle Play Music等）で十分なんじゃ...という気がしますが、
レシーバー（後述）にデフォルトのレシーバーを利用することはできても、センダー(後述)にGoogle PlayやYou Tubeを利用して、独自レシーバーで再生させることはできないため、レシーバーをいろいろと弄りたい場合は、センダーも自分で頑張るしかありません。


# レシーバー (Chromecast側)
Chromecast側で動作するアプリケーションをレシーバーと呼びます。
レシーバーは、表示するデータ等に応じて以下の3つの選択肢から選びます。

1. デフォルトのレシーバーを利用する
2. デフォルトのレシーバーをカスタマイズする（Styled Media Receiver）
3. 自分でレシーバーを実装する

2のStyled Media Recieverを使用する場合は、Google Cast SDK Developer Consoleでレシーバーアプリの登録を行い、アプリケーションIDを取得する必要があります。
が、CSSと表示させるロゴ等のリソースをGoogle Drive等に配置するだけで使用できるため、とてもお手軽です。

3の場合は、自分でhtml, javascript, cssを使用してレシーバーを実装する必要があります。

今回は、何もする必要のない、１のデフォルトレシーバーを利用します。

# センダー（Androidアプリ）
Android端末側でレシーバー（Chromecast）に対して、再生対象のメディアを指定するアプリケーションをセンダーと呼びます。

センダーの実装は、Google Cast SDKを利用しますが、下準備で用意したLibrary Projectを利用してサクッと実装します。

また、下準備の通り、再生対象のメディアをアクセス可能なURLで配置してください。

## センダーの実装
以下の実装が必要です。
1. Manifestファイル
2. Applicationクラス
3. Activity
4. Activityのリソースファイル
5. メニューのリソースファイル

### Manifest

テーマには、ActionBarActivityを利用するため、AppCompatを指定する必要があります。
また、メディア再生時のActivity、サービス、レシーバーがLibrary Projectで提供されるため、それぞれManifestに利用を宣言します。

このとき、VideoCastControllerActivityのparentActivityには自分で実装したActivityを指定する必要があります。
（今回は、MainActivity）

またApplicationには、オレオレApplicationクラスを実装して指定します。

```manifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.charomecastsendersample"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="14"
        android:targetSdkVersion="19" />

    <uses-permission android:name="android.permission.INTERNET" />
    
    <uses-feature
        android:name="android.hardware.wifi"
        android:required="true" >
    </uses-feature>
    
    <application
        android:name="MyApplication"
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/Theme.AppCompat.Light.DarkActionBar" >

        <meta-data
            android:name="com.google.android.gms.version"
            android:value="@integer/google_play_services_version" />
        
        <activity
            android:name="com.example.charomecastsendersample.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        
        <activity
            android:name="com.google.sample.castcompanionlibrary.cast.player.VideoCastControllerActivity"
            android:label="@string/app_name"
            android:launchMode="singleTask"
            android:parentActivityName="com.example.charomecastsendersample.MainActivity"
            android:screenOrientation="portrait" >
            <meta-data
                android:name="android.support.PARENT_ACTIVITY"
                android:value="com.example.charomecastsendersample.MainActivity" />

            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
            </intent-filter>
        </activity>
        
        
        <receiver android:name="com.google.sample.castcompanionlibrary.remotecontrol.VideoIntentReceiver" >
            <intent-filter>
                <action android:name="android.media.AUDIO_BECOMING_NOISY" />
                <action android:name="android.intent.action.MEDIA_BUTTON" />
                <action android:name="com.google.sample.castcompanionlibrary.action.toggleplayback" />
                <action android:name="com.google.sample.castcompanionlibrary.action.stop" />
            </intent-filter>
        </receiver>

        <service
            android:name="com.google.sample.castcompanionlibrary.notification.VideoCastNotificationService"
            android:exported="false" >
            <intent-filter>
                <action android:name="com.google.sample.castcompanionlibrary.action.toggleplayback" />
                <action android:name="com.google.sample.castcompanionlibrary.action.stop" />
                <action android:name="com.google.sample.castcompanionlibrary.action.notificationvisibility" />
            </intent-filter>
        </service>
        
        
    </application>

</manifest>
```

### Application

Chromecastとの接続をお任せする便利インスタンスを生成するために、オレオレApplicationクラスを実装します。

APPLICATION_IDに指定している、CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_IDがデフォルトのレシーバーを指定するIDになります。

Styled Media Receiverや独自のレシーバーを使用する場合には、登録して取得したIDをここにセットします。

```java:MyApplication.java
package com.example.charomecastsendersample;

import com.google.android.gms.cast.CastMediaControlIntent;
import com.google.sample.castcompanionlibrary.cast.VideoCastManager;
import com.google.sample.castcompanionlibrary.utils.Utils;

import android.app.Application;
import android.content.Context;

public class MyApplication extends Application {

	/** ボリューム増減値 */
    public static final double VOLUME_INCREMENT = 0.05;
    
    /** ReceiverアプリのID */
    private static String APPLICATION_ID;

    /** CastManager */
    private static VideoCastManager mCastMgr = null;

    @Override
    public void onCreate() {
        super.onCreate();
        
        // Receiverはデフォルトアプリを利用する
        APPLICATION_ID = CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID;
        
        // ボリュームの増減値をセット
        Utils.saveFloatToPreference(getApplicationContext(),
                VideoCastManager.PREFS_KEY_VOLUME_INCREMENT, (float) VOLUME_INCREMENT);

    }

    /**
     * CastManagerを取得する。
     * 
     * @param context
     * @return
     */
    public static VideoCastManager getCastManager(Context context) {
        if (null == mCastMgr) {
            mCastMgr = VideoCastManager.initialize(context, APPLICATION_ID,
                    null, null);
            mCastMgr.enableFeatures(
                    VideoCastManager.FEATURE_NOTIFICATION |
                            VideoCastManager.FEATURE_LOCKSCREEN |
                            VideoCastManager.FEATURE_DEBUGGING);

        }
        mCastMgr.setContext(context);
        String destroyOnExitStr = Utils.getStringFromPreference(context,
                "termination_plicy");
        mCastMgr.setStopOnDisconnect(null != destroyOnExitStr
                && "1".equals(destroyOnExitStr));
        return mCastMgr;
    }
}
```


### Activity

Activityを実装します。

以下の流れです。
1. 前述のオレオレApplicationクラスからChromecastとの接続を実行してくれる便利インスタンスを取得
2. メニューの初期化時にChromecastとの接続用アイコンを追加してもらう（Library Project側がやってくれます）
3. Chromecastとの接続後、Startボタンが押下されたら、Chromecastで再生するメディアの情報を生成して(※)、１で取得したインスタンスに渡して、再生用のActivityをIntentで呼び出す（Library Projectが提供）

接続がうまくできていれば、無事にChromecastから指定した曲が流れます！

また以下のコードではオーディオファイルの再生を行うように記載していますが、
MediaInfoの情報を変えれば動画ファイルの再生も可能です。

※不具合回避のため、MetaData.KEY_STUDIOとMetaData.KEY_SUBTITLEにダミーの空文字を入れています（@hackugyoさんのコメント参照）

```java:MainActivity.xml
package com.example.charomecastsendersample;

import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaMetadata;
import com.google.sample.castcompanionlibrary.cast.BaseCastManager;
import com.google.sample.castcompanionlibrary.cast.VideoCastManager;
import com.google.sample.castcompanionlibrary.widgets.MiniController;

import android.os.Bundle;
import android.support.v7.app.ActionBar;
import android.support.v7.app.ActionBarActivity;
import android.view.Menu;
import android.view.View;
import android.widget.Button;

public class MainActivity extends ActionBarActivity {

	private VideoCastManager mCastManager;
	
	private MiniController mMini;
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		// Google Play Serviceのチェック
		BaseCastManager.checkGooglePlaySevices(this);

		// ApplicationクラスからCastManagerインスタンスを取得
		// 今回はApplicationから取得する必要は特にありませんが、
		// 画面が複数になる場合を考慮してApplicationクラスにシングルトンのインスタンスを持つようにしておきます
		mCastManager = MyApplication.getCastManager(this);
		setupMiniController();

		// ActionBarのセット
        ActionBar actionBar = getSupportActionBar();
        setupActionBar(actionBar);

        mCastManager.reconnectSessionIfPossible(this, false);

        // Startボタンにリスナー設定
        // メニューボタンからChrome Castに接続後、Startボタンを押下して、
        // 対象メディアを再生させます
		Button startBtn = (Button) findViewById(R.id.btn_start);
		startBtn.setOnClickListener(mOnClickStartBtnListener);
	}

	/**
	 * ActionBarのセットアップを行う
	 * 
	 * @param actionBar ActionBar
	 */
    private void setupActionBar(ActionBar actionBar) {
        actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
        getSupportActionBar().setDisplayShowTitleEnabled(false);
    }

    @Override
	public boolean onCreateOptionsMenu(Menu menu) {
		getMenuInflater().inflate(R.menu.main, menu);
		
		// Chrome Castとの接続ボタンメニューを追加する
		// menu/main.xmlに追記の必要がある
        mCastManager.addMediaRouterButton(menu, R.id.media_route_menu_item);
		
		return true;
	}

    @Override
    protected void onPause() {
        super.onPause();
        if (mCastManager != null) {
	        mCastManager.decrementUiCounter();
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mCastManager != null) {
            mMini.removeOnMiniControllerChangedListener(mCastManager);
            mCastManager.removeMiniController(mMini);
            mCastManager.clearContext(this);
        }
    }

    /**
     * メディア再生時に表示されるミニコントローラーをセットアップする
     */
    private void setupMiniController() {
        mMini = (MiniController) findViewById(R.id.miniController1);
        mCastManager.addMiniController(mMini);
    }

    /**
     * Startボタンのリスナー
     */
    private View.OnClickListener mOnClickStartBtnListener = new View.OnClickListener() {
		@Override
		public void onClick(View v) {
			MediaInfo media = createMediaInfo();
			loadTargetMedia(media);
		}
	};

	/**
	 * Chrome Castに再生を支持するためのActivityを起動する。
	 * 
	 * @param media 再生対象メディア
	 */
    private void loadTargetMedia(final MediaInfo media) {
    	mCastManager.startCastControllerActivity(this, media, 0, true);
    }

    /**
     * メディア情報を生成する。
     * 
     * @return メディア情報
     */
	private MediaInfo createMediaInfo() {
		// Audio用メタ情報
		MediaMetadata audioMetaData = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MUSIC_TRACK);
		
		// 再生する曲のタイトル
		audioMetaData.putString(MediaMetadata.KEY_TITLE, "music title");

		// TODO:
		//  ダミーの文字列をセットしないとExeptionが発生する
		audioMetaData.putString(MediaMetadata.KEY_SUBTITLE, "  ");
		audioMetaData.putString(MediaMetadata.KEY_STUDIO, "  ");
		

		// 再生する曲のURLを指定してMediaInfoを生成
                // "url"を再生対象メディアのURLに置き換えてください
		MediaInfo mediaInfo = new MediaInfo.Builder("url")
			.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
			.setContentType("audio/mpeg")
			.setMetadata(audioMetaData)
			.build();
		
		return mediaInfo;
	}
}
```

### Activityのリソース

Activityのリソースファイルです。
ミニコントローラの配置と、Startボタンの配置を行います。

ミニコントローラはLibrary ProjectのカスタムViewを利用します。

```main_activity.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.charomecastsendersample.MainActivity"
    android:orientation="vertical"
    tools:ignore="MergeRootFrame" >
    
    <com.google.sample.castcompanionlibrary.widgets.MiniController
        android:id="@+id/miniController1"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:visibility="gone" >
    </com.google.sample.castcompanionlibrary.widgets.MiniController>
    
    <Button 
        android:id="@+id/btn_start"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="start"
        />
    
</LinearLayout>
```

### メニューのリソース

Chromecastとの接続アイコンを表示するためのメニューを配置します。
端末と同じネットワーク上に設定されたChromecastが見つかると、接続ボタンが表示されるようになります。

```main.xml
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
	xmlns:app="http://schemas.android.com/apk/res-auto"
    tools:context="com.example.charomecastsendersample.MainActivity" >

    <item
        android:id="@+id/media_route_menu_item"
        android:title="@string/media_route_menu_title"
        app:actionProviderClass="android.support.v7.app.MediaRouteActionProvider"
        app:showAsAction="always"/>

</menu>
```

# 動作キャプチャー
### アプリ起動時
ActionBarにChromecastアイコンが表示されていれば準備OKです
![Screenshot_2014-05-28-00-36-31.png](https://qiita-image-store.s3.amazonaws.com/0/13974/a31c1365-fec4-903c-9feb-4b079abed368.png)

### Chromecastと接続
Chromecastアイコンをタップすると接続ダイアログが表示されるので、対象のChromecastを選択します
![Screenshot_2014-05-28-00-36-41.png](https://qiita-image-store.s3.amazonaws.com/0/13974/9b4e053e-1198-2fd5-64fc-3f5b3e1f79c2.png)

### メディアの再生
TV等に接続したChromecast側にChromecastアイコンが表示されたら、レシーバー側の起動完了です。

startボタンを押下すると、再生用Activityに飛び、Chromecast側でも再生が開始されます！

カバー画像の設定をちゃんとやれば、伸びたドロイド君の代わりにカバー画像が表示されます。

![Screenshot_2014-05-28-00-37-14.png](https://qiita-image-store.s3.amazonaws.com/0/13974/56b8848e-ab98-8466-9e5d-b3a05cbfb734.png)

### のびのびドロイドくんActivityから戻る

呼び出し元のActivityに戻ると、ミニコントローラが挿入されているはずです。

![Screenshot_2014-05-28-00-38-23.png](https://qiita-image-store.s3.amazonaws.com/0/13974/f5ae285c-ae01-6483-87ba-50f28913c8e1.png)

これで、お手軽オレオレ Play Musicのできあがりです。
エラー処理等まったく考慮していないので、その辺りはご了承下さい。

# 参考
Developers Guid
https://developers.google.com/cast/?hl=ja

Chromecastサンプルアプリ
https://github.com/googlecast
※CastVideos-androidがもっと参考になるサンプルです。

Chromecast 向けアプリの開発について (Google Developer Relations Japan Blog)
http://googledevjp.blogspot.jp/2014/05/chromecast.html

CastCompainionLibraryで発生するMediaInfoのNullPointerExceptionについて
http://stackoverflow.com/questions/22082265/unable-to-play-a-video-with-ccl-because-of-nullpointerexception/22481244#22481244