ExoPlayer の実装に関するメモ
ExoPlayer を扱う時には DataSource, SampleSource, Renderer を生成し、 ExoPlayer インスタンスに渡す必要があります。 以前 ExoPlayer の紹介記事を書いた時に載せたサンプルコードは以下のような形でした。
DataSource dataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); ExtractorSampleSource sampleSource = new ExtractorSampleSource( uri, dataSource, allocator, BUFFER_SEGMENT_COUNT * BUFFER_SEGMENT_SIZE); MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer( sampleSource, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT); MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource);
それぞれが何をして ExoPlayer がどのように動作しているか、 URL を指定して File から MP4 を読み込むという流れを追ってみたいと思います。
DataSource
DataSource
は ExoPlayer で扱う動画や音声のデータファイルをやりとりするためのクラスです。
すなわち、 File から読み込んだり、 HTTP から読み込んだり、というような部分を管理しています。
このクラスでは mp4 や webm といった、実際にどのような形式のメディアデータなのかは扱いません。
- https://github.com/google/ExoPlayer/blob/r1.5.9/library/src/main/java/com/google/android/exoplayer/upstream/DataSource.java
- InputStream みたいな Interface
- open(DataSpec), close(), read(buffer, offset, length) の 3 つだけが定義されてる
public interface DataSource { long open(DataSpec dataSpec) throws IOException; void close() throws IOException; int read(byte[] buffer, int offset, int readLength) throws IOException; }
DefaultUriDataSource
- https://github.com/google/ExoPlayer/blob/r1.5.9/library/src/main/java/com/google/android/exoplayer/upstream/DefaultUriDataSource.java
- URI の scheme 部を見てどの DataSource を使うか判別していい感じの DataSource に受け渡してくれる
- FileDataSource ...
file://
- AssetDataSource ...
asset://
かfile://
の/android_asset/
- ContentDataSource ...
content://
- DefaultHttpDataSource ... それ以外
- FileDataSource ...
public long open(DataSpec dataSpec) throws IOException { Assertions.checkState(dataSource == null); // Choose the correct source for the scheme. String scheme = dataSpec.uri.getScheme(); if (Util.isLocalFileUri(dataSpec.uri)) { if (dataSpec.uri.getPath().startsWith("/android_asset/")) { dataSource = assetDataSource; } else { dataSource = fileDataSource; } } else if (SCHEME_ASSET.equals(scheme)) { dataSource = assetDataSource; } else if (SCHEME_CONTENT.equals(scheme)) { dataSource = contentDataSource; } else { dataSource = httpDataSource; } // Open the source and return. return dataSource.open(dataSpec); }
この実装を見ると分かりますが、 File, Asset, ContentResolver, Http の 4 種類の DataSource インスタンスを持っていて、 open()
メソッドが呼ばれたときにどれを使用するか振り分けています。
大したコストでは無いのですが、利用する DataSource が 1 種類の場合は DafaultUriDataSource を使わずに、個別の DataSource を呼び出したほうが良いでしょう。
FileDataSource
RandomAccessFile を使って DataSource の Interface に合うように良い感じにファイルを seek したり read します。
SampleSource
SampleSource
は、 DataSource で読み込んだファイルを解析し、プレイヤーとして扱える単位で読み込めるようにするクラスです。
一般的にコンテナフォーマットと呼ばれるものの parse をここで行なって、各トラックやメタデータにアクセスできるようになります。
- https://github.com/google/ExoPlayer/blob/r1.5.9/library/src/main/java/com/google/android/exoplayer/SampleSource.java
- SampleSource
- register() すると SampleSourceReader を返すだけ
public interface SampleSource { public SampleSourceReader register(); }
- SampleSourceReader
- SampleSource の Inner Interface
- 実際に SampleSource 的な動作を行なうのはこっちの方
- メディアファイルを読み込んだりするために必要な Interface が一通り定義されている
public interface SampleSourceReader { public void maybeThrowError() throws IOException; public boolean prepare(long positionUs); public int getTrackCount(); public MediaFormat getFormat(int track); public void enable(int track, long positionUs); public boolean continueBuffering(int track, long positionUs); public long readDiscontinuity(int track); public int readData(int track, long positionUs, MediaFormatHolder formatHolder, SampleHolder sampleHolder); public void seekToUs(long positionUs); public long getBufferedPositionUs(); public void disable(int track); public void release(); }
ExtractorSampleSource
- https://github.com/google/ExoPlayer/blob/r1.5.9/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java
- SampleSource, SampleSourceReader, ExtractorOutput を実装している
public final class ExtractorSampleSource implements SampleSource, SampleSourceReader, ExtractorOutput, Loader.Callback {
ExtractorOutput
- https://github.com/google/ExoPlayer/blob/r1.5.9/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorOutput.java
Extractor
クラスの出力クラス
public interface ExtractorOutput { TrackOutput track(int trackId); void endTracks(); void seekMap(SeekMap seekMap); void drmInitData(DrmInitData drmInitData); }
Extractor
- https://github.com/google/ExoPlayer/blob/r1.5.9/library/src/main/java/com/google/android/exoplayer/extractor/Extractor.java
- コンテナフォーマットの parse をするための Interface
- init で ExtractorOutput を指定し、 read で指定した ExtractorInput からデータを読み込んで出力する
- sniff というメソッドで自分が parse できるコンテナか判断するメソッドがある
public interface Extractor { void init(ExtractorOutput output); boolean sniff(ExtractorInput input) throws IOException, InterruptedException; int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException; void seek(); void release(); }
ExtractorInput
- https://github.com/google/ExoPlayer/blob/r1.5.9/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorInput.java
- Extractor の入力クラス
public interface ExtractorInput { int read(byte[] target, int offset, int length) throws IOException, InterruptedException; boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput) throws IOException, InterruptedException; void readFully(byte[] target, int offset, int length) throws IOException, InterruptedException; int skip(int length) throws IOException, InterruptedException; boolean skipFully(int length, boolean allowEndOfInput) throws IOException, InterruptedException; void skipFully(int length) throws IOException, InterruptedException; boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput) throws IOException, InterruptedException; void peekFully(byte[] target, int offset, int length) throws IOException, InterruptedException; boolean advancePeekPosition(int length, boolean allowEndOfInput) throws IOException, InterruptedException; void advancePeekPosition(int length) throws IOException, InterruptedException; void resetPeekPosition(); long getPeekPosition(); long getPosition(); long getLength(); }
TrackOutput
public interface TrackOutput { void format(MediaFormat format); int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) throws IOException, InterruptedException; void sampleData(ParsableByteArray data, int length); void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey); }
MediaFormat
- https://github.com/google/ExoPlayer/blob/r1.5.9/library/src/main/java/com/google/android/exoplayer/MediaFormat.java
- コンテナフォーマットの解析結果を保持するクラス
- Parcelable を実装している
Mp4Extractor
- https://github.com/google/ExoPlayer/blob/r1.5.9/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Mp4Extractor.java
- MP4 の Extractor を実装したクラス
- これは unfragmented mp4 とよばれるデータがコンテナの中で固まっている parse しやすいやつ専用
- MPEG-DASH とかで使う fragmented mp4 は FragmentedMp4Extractor クラス
- read()
- ExtractorInput から入力を取って
- ヘッダにあるメタ情報を読み込
- TrackOutput 作成
- moov を読めば後は動画と音声のフレームが続いている
- Sample 読み込み
- TrackOutput に出力する
- ExtractingLoadable#load() で Mp4Extractor#read() が呼ばれる
- ExtractorSampleSource#prepare() で load() をよんでる
- ExtractorOutput は ExtractorSampleSource 自身
- InternalTrackOutput
- DefaultTrackOutput
- RollingSampleBuffer に出力された Sample を持っている
- getData で SampleHolder に RollingSampleBuffer から得たデータを書き込む
- DefaultTrackOutput
- SampleHolder
- ByteBuffer で Sample を持っている
- ExtractorInput から入力を取って
TrackRenderer
- https://github.com/google/ExoPlayer/blob/r1.5.9/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java
- Player みたいな Interface を持った abstract class
MediaCodecVideoTrackRenderer
- https://github.com/google/ExoPlayer/blob/r1.5.9/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java
- MediaCodecTrackRenderer を継承している
- processOutputBuffer()
- MediaCodec を使ってデコードしたデータを Surface に描画する
- MediaCodec#configure で指定した Surface に描画する
- https://developer.android.com/reference/android/media/MediaCodec.html
MediaCodecTrackRenderer
- https://github.com/google/ExoPlayer/blob/r1.5.9/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java
- SampleSourceTrackRenderer を継承している
SampleSourceTrackRenderer
- https://github.com/google/ExoPlayer/blob/r1.5.9/library/src/main/java/com/google/android/exoplayer/SampleSourceTrackRenderer.java
- TrackRenderer を継承した abstract class
- SampleSource や SampleSourceReader を使ってデータを読み込んで Render する
まとめ
- DataSource ... ファイルを読み込むヤツ、中身が何かまでは関与しない
- SampleSource ... DataSource で得たバイト列をパースしてメタ情報や動画データにアクセスできるようにする
- MediaCodeVideoRenderer ... SampleSource で得たデータを MediaCodec でデコードし Surface にレンダリングする
所感
- ExoPlayer が Android 4.1 以降というのは MediaCodec に依存しているからなのだけど、 MediaCodec に依存しているのは MediaCodecTrackRenderer 以下のクラスだけ
- MediaCodec を使わない Renderer を作れば 4.1 未満でも ExoPlayer を使うことは出来る
- MediaPlayer を使う Renderer を作ろうかなと思ったが MediaPlayer は FileDescriptor か URI を指定して入力ソースを決めるので無理そう
- Animation GIF や Animation PNG を表示するものは出来そう (SampleSource とか Extractor とか作る必要はあるけど)