ExoPlayerのネット上情報は古いものが多い気がしたので、最新版の使い方についてまとめてみました。
プロジェクトにExoPlayerを追加する
build.gradle
のdependencies
ブロックにimplementation 'com.google.android.exoplayer:exoplayer:2.6.1'
を追加するだけで利用可能になります(2018年2月9日時点の最新バージョン)。
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には以下のようにして指定します。
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を再生するならこれで十分です。
ExoPlayer exoPlayer = ExoPlayerFactory.newSimpleInstance(context, new DefaultTrackSelector());
ExoPlayerを使いたい大きな理由の一つにアダプティブストリーミングがあります。このような帯域の状況を見ながら読み込むストリームを変える必要がある場合は、DefaultTrackSelector
にAdaptiveTrackSelection.Factory
のインスタンスを渡します。
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
というインターフェースを使用します。
MediaSource mediaSource = createMediaSource(); // TODO これから説明します
exoPlayer.prepare(mediaSource);
これはローカルファイルやメディアへの直URIを指定する場合とストリーミングを行う場合では必要な情報が全く異なるためです。したがって、再生するメディアがどのようなものであるかによって、上記createMediaSource()
で行うべきことが変わってきます。
メディアを直接読み込む
動画や音声そのものへのURLを読み込んで再生する場合は、ExtractorMediaSource
を使います。
2.6.1 以前
これを作るためにたくさんのインスタンスを指定する必要があるので、まずこれらの依存インスタンスを作ってからExtractorMediaSource
に渡します。
ExoPlayerではいろんなところでHandlerとイベントリスナーを渡す箇所がありますが、イベントリスナーが必要なければそれぞれnull
を指定することもできます。
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
を使う方法が推奨されています。こちらの方がコードが少なくて良いです。
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
を使います。これにより必要な依存関係が微妙に異なってきます。
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の時と似ています。
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()
というメソッドがあります。言われてみれば確かに、「準備ができたら再生する」の方が実際の動作に合っている気がしてきます。
// 再生
player.setPlayWhenReady(true);
// 一時停止
player.setPlayWhenReady(false);
停止
これはstop()
メソッドがあります。一時停止との使い分けですが、一時停止は後で再生に戻る予定の時に使います。停止は再生を終えたい時に使います。
player.stop();
シーク
MediaPlayer
同様、seekTo()
でシークできます。指定する単位もミリ秒で同じです。ただし引数の型がlong
になっているところだけが微妙に違います。
player.seekTo(10000);
SurfaceViewに映像を表示する
ExoPlayer
でデコードされる映像を画面に表示する最も簡単な方法はSimpleExoPlayer.setVideoSurfaceView()
メソッドを使うことです。ExoPlayer
をSimpleExoPlayer
にキャストして使っています。このキャストができるのは、このページの例のようにExoPlayerFactory.newSimpleInstance()
でインスタンスを作った場合のみです。キャストを行わないで最初からSimpleExoPlayer
型として保持しておくのも良いです。
((SimpleExoPlayer) player).setVideoSurfaceView(surfaceView);
後片付け
使い終わったらExoPlayer.release()
を呼び出してリソースを解放します。リソースを解放した後のExoPlayerには触らないようにしましょう。
player.release();
次のようにnull
チェックを行うことで、確実に一度だけrelease()
を呼び出すようにすることもあります。
if (player != null) {
player.release();
player = null;
}
Widevine CENEのDRM付きのMPDを再生する
細かいことはまだよくわかってないですが、ExoPlayerFactory.newSimpleInstance()
の引数にDrmSessionManager
を渡すとDRM付きの動画等が再生できるようになるようです。
player = ExoPlayerFactory.newSimpleInstance(context, trackSelector, loadControl, drmSessionManager);
DrmSessionManager
を作成するまでにかなり長い道のりがありますが、とりあえずこんな感じでWidevine CENC + MPEG DASHの再生ができるようです。サンプルコードから拝借しました。
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に追加します。
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
は後で使うのでローカル変数ではなくフィールドで宣言しておきます。
gvrAudioProcessor = new GvrAudioProcessor();
RenderersFactory renderersFactory = new DefaultRenderersFactory(context) {
@Override
protected AudioProcessor[] buildAudioProcessors() {
return new AudioProcessor[]{gvrAudioProcessor};
}
};
exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, new DefaultTrackSelector());
次に、毎フレーム呼ばれるコールバック内でgvrAudioProcessor.updateOrientation()
を呼び、ヘッドトラッキング情報を伝えます。
gvrAudioProcessor.updateOrientation(quaternion.w, quaternion.x, quaternion.y, quaternion.z);
これだけです!
既存の360度動画プレイヤーのコードをお持ちの方は、ぜひ試してみてくださいね!
なお、こちらの sample-exoplayer というのがGear VRでExoPlayerを使ってAmbisonics対応の360度動画を再生するサンプルです。参考までに。