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 といった、実際にどのような形式のメディアデータなのかは扱いません。

public interface DataSource {
  long open(DataSpec dataSpec) throws IOException;
  void close() throws IOException;
  int read(byte[] buffer, int offset, int readLength) throws IOException;
}

DefaultUriDataSource

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 をここで行なって、各トラックやメタデータにアクセスできるようになります。

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

public final class ExtractorSampleSource implements SampleSource, SampleSourceReader, ExtractorOutput, Loader.Callback {

ExtractorOutput

public interface ExtractorOutput {
  TrackOutput track(int trackId);
  void endTracks();
  void seekMap(SeekMap seekMap);
  void drmInitData(DrmInitData drmInitData);
}

Extractor

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

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

Mp4Extractor

TrackRenderer

MediaCodecVideoTrackRenderer

MediaCodecTrackRenderer

SampleSourceTrackRenderer

まとめ

  • 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 とか作る必要はあるけど)