ExoPlayerの使い方

ExoPlayerのネット上情報は古いものが多い気がしたので、最新版の使い方についてまとめてみました。


## プロジェクトにExoPlayerを追加する

`build.gradle`の`dependencies`ブロックに`implementation 'com.google.android.exoplayer:exoplayer:2.6.1'`を追加するだけで利用可能になります(2018年2月9日時点の最新バージョン)。

```gradle
dependencies {
    implementation 'com.google.android.exoplayer:exoplayer:2.6.1'
}
```

2.4.0からExoPlayerはモジュール化されました。アプリケーションで必要な機能だけを取り込むことでapkのサイズが小さくなり、ビルドも早くなります。

* exoplayer-core: 基本的な機能(必須)
* exoplayer-dash: DASHサポート
* exoplayer-hls: HLSサポート
* exoplayer-smoothstreaming: SmoothStreamingサポート
* exoplayer-ui: UIコンポーネント

また、以下の拡張機能もあります。

* extension-ima: インタラクティブメディア広告
* extension-mediasession: Media session
* extension-okhttp: OkHttpを使用
* extension-rtmp: RTMP対応
* extension-gvr: Google VR Audioを使ってAmbisonicsを再生

gradleには以下のようにして指定します。

```gradle
dependencies {

    // implementation 'com.google.android.exoplayer:exoplayer:2.6.1' は、以下5つを全部指定するのと同じ
    implementation 'com.google.android.exoplayer:exoplayer-core:2.6.1'
    implementation 'com.google.android.exoplayer:exoplayer-dash:2.6.1'
    implementation 'com.google.android.exoplayer:exoplayer-hls:2.6.1'
    implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.6.1'
    implementation 'com.google.android.exoplayer:exoplayer-ui:2.6.1'

    // 拡張機能
    implementation 'com.google.android.exoplayer:extension-ima:2.6.1'
    implementation 'com.google.android.exoplayer:extension-mediasession:2.6.1'
    implementation 'com.google.android.exoplayer:extension-okhttp:2.6.1'
    implementation 'com.google.android.exoplayer:extension-rtmp:2.6.1'
    implementation 'com.google.android.exoplayer:extension-gvr:2.6.1'
}
```

## インスタンスの作成

`ExoPlayer`のインスタンスを作る最も簡単な方法は`ExoPlayerFactory.newSimpleInstance`を呼び出す方法です。引数に渡すインターフェースがいくつかありますが、最もシンプルなのはこれです。単純な動画や音声のURIを再生するならこれで十分です。

```java
ExoPlayer exoPlayer = ExoPlayerFactory.newSimpleInstance(context, new DefaultTrackSelector());
```

ExoPlayerを使いたい大きな理由の一つにアダプティブストリーミングがあります。このような帯域の状況を見ながら読み込むストリームを変える必要がある場合は、`DefaultTrackSelector`に`AdaptiveTrackSelection.Factory`のインスタンスを渡します。

```java
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
ExoPlayer exoPlayer = ExoPlayerFactory.newSimpleInstance(context, new DefaultTrackSelector(new AdaptiveTrackSelection.Factory(bandwidthMeter)));
```

注意: `DefaultBandwidthMeter`を使う時は、この後に登場する**`DefaultDataSourceFactory`のコンストラクターにも同じインスタンスを渡す必要があります。**
`DefaultBandwidthMeter`を使わない時は、後述の例で`DefaultDataSourceFactory`のコンストラクターに渡している`bandwidthMeter`はnullにするか省略することができます。

## 読み込み

MediaPlayerと同様、ExoPlayerでも読み込むメディアはUriで指定します。しかしMediaPlayerと違い、ExoPlayerではUriを直接渡すのではなく`MediaSource`というインターフェースを使用します。

```java
MediaSource mediaSource = createMediaSource(); // TODO これから説明します
exoPlayer.prepare(mediaSource);
```

これはローカルファイルやメディアへの直URIを指定する場合とストリーミングを行う場合では必要な情報が全く異なるためです。したがって、再生するメディアがどのようなものであるかによって、上記`createMediaSource()`で行うべきことが変わってきます。


### メディアを直接読み込む

動画や音声そのものへのURLを読み込んで再生する場合は、`ExtractorMediaSource`を使います。

#### 2.6.1 以前

これを作るためにたくさんのインスタンスを指定する必要があるので、まずこれらの依存インスタンスを作ってから`ExtractorMediaSource`に渡します。
ExoPlayerではいろんなところでHandlerとイベントリスナーを渡す箇所がありますが、イベントリスナーが必要なければそれぞれ`null`を指定することもできます。

```java
Uri uri = Uri.parse(MEDIA_URL);
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context, Util.getUserAgent(context, "AppName"), bandwidthMeter);
ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory();

// 任意のイベントリスナー
Handler handler = new Handler();
ExtractorMediaSource.EventListener eventListener = new ExtractorMediaSource.EventListener() {
    @Override
    public void onLoadError(IOException error) {
        error.printStackTrace();
    }
};

player.prepare(new ExtractorMediaSource(uri, dataSourceFactory, extractorsFactory, handler, eventListener));
```

#### 2.6.1以降

`ExtractorMediaSource`のコンストラクタは非推奨になり、代わりに`ExtractorMediaSource.Factory`を使う方法が推奨されています。こちらの方がコードが少なくて良いです。

```java
Uri uri = Uri.parse(MEDIA_URL);
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context, Util.getUserAgent(context, "AppName"), bandwidthMeter);
MediaSource mediaSource = new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
player.prepare(mediaSource);
```

### MPEG DASH のmpdファイルを読み込む

MPEG DASHのmpgファイルを読み込む場合は`ExtractorMediaSource`の代わりに`DashMediaSource`を使います。これにより必要な依存関係が微妙に異なってきます。

```java
Uri uri = Uri.parse(MPD_URL);
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context, Util.getUserAgent(context, "Test", bandwidthMeter));
DashChunkSource.Factory dashChunkSourceFactory = new DefaultDashChunkSource.Factory(dataSourceFactory);

// Optional event handler
Handler handler = new Handler();
AdaptiveMediaSourceEventListener eventListener = new AdaptiveMediaSourceEventListener() {
    @Override
    public void onLoadStarted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs) {
        Log.d(TAG, "onLoadStarted: ");
    }

    @Override
    public void onLoadCompleted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded) {
        Log.d(TAG, "onLoadCompleted: ");
    }

    @Override
    public void onLoadCanceled(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded) {
        Log.d(TAG, "onLoadCanceled: ");
    }

    @Override
    public void onLoadError(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded, IOException error, boolean wasCanceled) {
        Log.d(TAG, "onLoadError: ");
    }

    @Override
    public void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs) {
        Log.d(TAG, "onUpstreamDiscarded: ");
    }

    @Override
    public void onDownstreamFormatChanged(int trackType, Format trackFormat, int trackSelectionReason, Object trackSelectionData, long mediaTimeMs) {
        Log.d(TAG, "onDownstreamFormatChanged: ");
    }
};
player.prepare(new DashMediaSource(uri, dataSourceFactory, dashChunkSourceFactory, handler, eventListener));
```

### HLSのm3u8ファイルを読み込む

HLSのm3u8ファイルを読み込む場合は`HlsMediaSource`を使います。MPEG DASHの時と似ています。

```java
Uri uri = Uri.parse(CONTENT_URL);
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context, Util.getUserAgent(context, "Test", bandwidthMeter));

// Optional event handler
Handler handler = new Handler();
AdaptiveMediaSourceEventListener eventListener = new AdaptiveMediaSourceEventListener() {
    @Override
    public void onLoadStarted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs) {
        Log.d(TAG, "onLoadStarted: ");
    }

    @Override
    public void onLoadCompleted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded) {
        Log.d(TAG, "onLoadCompleted: ");
    }

    @Override
    public void onLoadCanceled(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded) {
        Log.d(TAG, "onLoadCanceled: ");
    }

    @Override
    public void onLoadError(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded, IOException error, boolean wasCanceled) {
        Log.d(TAG, "onLoadError: ");
    }

    @Override
    public void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs) {
        Log.d(TAG, "onUpstreamDiscarded: ");
    }

    @Override
    public void onDownstreamFormatChanged(int trackType, Format trackFormat, int trackSelectionReason, Object trackSelectionData, long mediaTimeMs) {
        Log.d(TAG, "onDownstreamFormatChanged: ");
    }
};

player.prepare(new HlsMediaSource(uri, dataSourceFactory, handler, eventListener));
```

## 再生・一時停止

再生を開始したり一時停止をしたりというコントロールを制御するのに`start()`,`play()`,`pause()`などのメソッドがあるのを期待しますが、なんとExoPlayerには**そのようなものはありません。**代わりに`setPlayWhenReady()`というメソッドがあります。言われてみれば確かに、「準備ができたら再生する」の方が実際の動作に合っている気がしてきます。

```java
// 再生
player.setPlayWhenReady(true);

// 一時停止
player.setPlayWhenReady(false);
```

## 停止

これは`stop()`メソッドがあります。一時停止との使い分けですが、一時停止は後で再生に戻る予定の時に使います。停止は再生を終えたい時に使います。

```java
player.stop();
```

## シーク

`MediaPlayer`同様、`seekTo()`でシークできます。指定する単位もミリ秒で同じです。ただし引数の型が`long`になっているところだけが微妙に違います。

```java
player.seekTo(10000);
```

## SurfaceViewに映像を表示する

`ExoPlayer`でデコードされる映像を画面に表示する最も簡単な方法は`SimpleExoPlayer.setVideoSurfaceView()`メソッドを使うことです。`ExoPlayer`を`SimpleExoPlayer`にキャストして使っています。このキャストができるのは、このページの例のように`ExoPlayerFactory.newSimpleInstance()`でインスタンスを作った場合のみです。キャストを行わないで最初から`SimpleExoPlayer`型として保持しておくのも良いです。

```java
((SimpleExoPlayer) player).setVideoSurfaceView(surfaceView);
```

## 後片付け

使い終わったら`ExoPlayer.release()`を呼び出してリソースを解放します。リソースを解放した後のExoPlayerには触らないようにしましょう。

```java
player.release();
```

次のように`null`チェックを行うことで、確実に一度だけ`release()`を呼び出すようにすることもあります。

```java
if (player != null) {
  player.release();
  player = null;
}
```


## Widevine CENEのDRM付きのMPDを再生する

細かいことはまだよくわかってないですが、`ExoPlayerFactory.newSimpleInstance()`の引数に`DrmSessionManager`を渡すとDRM付きの動画等が再生できるようになるようです。

```java
player = ExoPlayerFactory.newSimpleInstance(context, trackSelector, loadControl, drmSessionManager);
```

`DrmSessionManager`を作成するまでにかなり長い道のりがありますが、とりあえずこんな感じでWidevine CENC + MPEG DASHの再生ができるようです。サンプルコードから拝借しました。

```java
String MPD_URL = ...
String LICENSE_URL = ...

DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context, userAgent);
Handler handler = new Handler();

TrackSelector trackSelector = new DefaultTrackSelector();
LoadControl loadControl = new DefaultLoadControl();

HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(LICENSE_URL, new DefaultHttpDataSourceFactory(userAgent));
DefaultDrmSessionManager.EventListener listener = new DefaultDrmSessionManager.EventListener() {
    @Override
    public void onDrmKeysLoaded() {
        Log.d(TAG, "onDrmKeysLoaded: ");
    }

    @Override
    public void onDrmSessionManagerError(Exception e) {
        Log.d(TAG, "onDrmSessionManagerError: ");
    }

    @Override
    public void onDrmKeysRestored() {
        Log.d(TAG, "onDrmKeysRestored: ");
    }

    @Override
    public void onDrmKeysRemoved() {
        Log.d(TAG, "onDrmKeysRemoved: ");
    }
};
DefaultDrmSessionManager<FrameworkMediaCrypto> drmSessionManager = new DefaultDrmSessionManager<>(C.WIDEVINE_UUID, FrameworkMediaDrm.newInstance(C.WIDEVINE_UUID), drmCallback, null, handler, listener);

player = ExoPlayerFactory.newSimpleInstance(context, trackSelector, loadControl, drmSessionManager);

DashChunkSource.Factory dashChunkSourceFactory = new DefaultDashChunkSource.Factory(dataSourceFactory);
AdaptiveMediaSourceEventListener eventListener = new AdaptiveMediaSourceEventListener() {
    @Override
    public void onLoadStarted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs) {
        Log.d(TAG, "onLoadStarted: ");
    }

    @Override
    public void onLoadCompleted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded) {
        Log.d(TAG, "onLoadCompleted: ");
    }

    @Override
    public void onLoadCanceled(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded) {
        Log.d(TAG, "onLoadCanceled: ");
    }

    @Override
    public void onLoadError(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded, IOException error, boolean wasCanceled) {
        Log.d(TAG, "onLoadError: ");
    }

    @Override
    public void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs) {
        Log.d(TAG, "onUpstreamDiscarded: ");
    }

    @Override
    public void onDownstreamFormatChanged(int trackType, Format trackFormat, int trackSelectionReason, Object trackSelectionData, long mediaTimeMs) {
        Log.d(TAG, "onDownstreamFormatChanged: ");
    }
};

player.prepare(new DashMediaSource(MPD_URL, dataSourceFactory, dashChunkSourceFactory, handler, eventListener));
```

## Ambisonics動画を再生する

ExoPlayerの拡張機能`extension-gvr`を使えば、Google Audio SDKを使って**Ambisonics音声をバイノーラル化しながら再生する**ことができるようになります。360度動画のプレイヤーをExoPlayerで作っていれば、ちょっとコードを変更するだけで簡単にAmbisonics対応をすることができます。素晴らしい！

まず、依存ライブラリを追加します。ExoPlayer本体以外に`extension-gvr`拡張機能とGoogle VR Audioのライブラリをbuild.gradleに追加します。

```groovy:build.gradle
dependencies {
    implementation 'com.google.android.exoplayer:exoplayer-core:r2.5.4' // ExoPlayer本体
    implementation 'com.google.android.exoplayer:extension-gvr:r2.5.4'  // ExoPlayer+Google VR拡張機能
    implementation 'com.google.vr:sdk-audio:1.101.0'                    // Google VR Audio
}
```

次に、ExoPlayerをインスタンス化する部分を次のようにします。`gvrAudioProcessor`は後で使うのでローカル変数ではなくフィールドで宣言しておきます。

```java
gvrAudioProcessor = new GvrAudioProcessor();
RenderersFactory renderersFactory = new DefaultRenderersFactory(context) {
    @Override
    protected AudioProcessor[] buildAudioProcessors() {
        return new AudioProcessor[]{gvrAudioProcessor};
    }
};

exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, new DefaultTrackSelector());
```

次に、毎フレーム呼ばれるコールバック内で`gvrAudioProcessor.updateOrientation()`を呼び、ヘッドトラッキング情報を伝えます。

```java
gvrAudioProcessor.updateOrientation(quaternion.w, quaternion.x, quaternion.y, quaternion.z);
```

**これだけです！**
既存の360度動画プレイヤーのコードをお持ちの方は、ぜひ試してみてくださいね！


なお、[こちら](https://github.com/ejeinc/Meganekko)の *sample-exoplayer* というのがGear VRでExoPlayerを使ってAmbisonics対応の360度動画を再生するサンプルです。参考までに。