• スポンサードリンク

Android4.1〜4.2でMediaCodecを使う時ぃ〜

Android MediaCodec

前振り

Android DevelopersによるとAPI16(Android4.1.2)以降でMediaCodecが使えることになっています。
でも実際には、API16以降と言いつつもAndroid4.1〜4.2でMediaCodecを使おうとすると色々困ることがおきます。
今回はそのうちの1つ、MediaCodec#INFO_OUTPUT_FORMAT_CHANGEDが来ない件について調べてみました。

Abdroid DevelopersのMediaCodecの項には冒頭にサンプルが載っています。

MediaCodec#dequeueInputBufferからMediaCodec#INFO_OUTPUT_FORMAT_CHANGEDが返って来た時にMediaCodec#getOutputFormatを呼び出してMediaFormat with csd(codec specific dataかな?)を取得するようになっています。
API>=18であればここで取得したMediaFormat(with csd)をMediaMuxerに渡して出力の設定をすると簡単に動画や音声のキャプチャが出来ますね。
ちなみに、MediaCodec#getOutputFormatの説明はこんな感じになっています。

Call this after dequeueOutputBuffer signals a format change by returning INFO_OUTPUT_FORMAT_CHANGED
MediaCodec#dequeueOutputBufferを呼び出した時にINFO_OUTPUT_FORMAT_CHANGEDが返って来た後なら呼び出してもいいよ。って感じ。何をするメソッドかはさっぱりわかりません。これって機能の説明ではありませんね(-_-;)閑話休題。

ところがですねぇ、Android4.1〜4.2ではINFO_OUTPUT_FORMAT_CHANGEDは返って来ないのです。どんなに応援してもどんなに待ってもどんなに媚を売ってもだめです(笑)。Android4.3以降では挙動が変わってINFO_OUTPUT_FORMAT_CHANGEDが返ってくるようになりましたし、世界中には沢山のAndroid4.1〜4.2端末が有るはずなので、もしかするとAndroid4.1〜4.2なのにINFO_OUTPUT_FORMAT_CHANGED が返ってくるレアな端末も有るかもしれませんが。MediaCodec#getOutputFormatはINFO_OUTPUT_FORMAT_CHANGEDが返ってこないことには呼び出すことは出来ません。試しに手持ちのAndroid4.1〜4.2な端末で呼んでみると、どの端末・どのタイミングにおいても例外を吐くかnativeコード内でクラッシュします。
まぁいいんですよ。使えないなら使えないで。でもAPI16以上で使えるって宣言しておきながら実際にはまったく使えないって(●`ε´●)プンプン
この分野に足を踏み入れちゃった人には常識なんだろうなぁ〜って思いつつも、今更ながらどげんかせにゃいけん。
一体どこの人やねん(笑)

解決策

1.Android4.3以降の場合はINFO_OUTPUT_FORMAT_CHANGEDが来るのでそこでMediaCodec#getOutputFormat を呼べば特に問題なし。

2.問題は、Android4.1〜4.2です。
(INFO_OUTPUT_FORMAT_CHANGEDが来ないので、)事実上MediaCodec#getOutputFormatを使えないので他の方法を考える必要があります。
実はcsdを取得できるタイミングがもう1箇所ありました。
MediaCodec#dequeueOutputBufferを呼び出す時の第1引数に渡しているMediaCodec#BufferInfoのflagsにMediaCodec#BUFFER_FLAG_CODEC_CONFIGがセットされて返って来た時です。この時にはMediaCodec#dequeueOutputBufferの返り値が示すOutBufferにcsdを保持したByteBufferがセットされているみたいなのです。でもコーデックの種類によってcsdの数は違うのに、取得できるByteByfferは1つだけです。例えばvideo/avcであればcsd-0とcsd-1の2つ必要みたいですし、video/x-vnd.on2.vp8だとcsd-0の1つだけのようです。どうすべ〜

ByteBufferをバイナリダンプしてみたりネットでウロウロしたりしてるうちに、Stack Overflowで最後の8バイトがPPSって決め打ちしていいんかなぁってのが載っているのを見つけました。こちらStackOverFlow。ただ、決め打ちではなく、スタートコードを検索して区切る方が良いみたいです。仕様はちゃんと見てませんが(-_-;)、0x00,0x00,0x00,0x01の4バイトがスタートコードとなっているようです。
ということで、MediaCodec#BufferInfoのflagsにMediaCodec#BUFFER_FLAG_CODEC_CONFIGがセットされて返って来た時に、MediaCodec#dequeueOutputBufferの返り値の示すByteBufferからbyte配列を取得して、スタートコードで区切ればcsdを取得できそうです。

抜粋ですが、例えばこんな感じにします。何からの抜粋かって?そのうち公開するかも〜(^o^)/

3〜4行目あたりがちょっと冗長ですが・・・(^_^;)17行目のMediaFormat#createVideoFormatはMediaCodec生成時に引き渡したのと同じパラメータを与えてください。
#byteCompはbyte[]から線形検索で指定したbyte[]の先頭位置を返します。csdはせいぜい数十バイトなので線形検索で十分でしょう。

Javaで書くとなんか面倒くさいなぁ(´・ω・`)
それはともかく、MediaCodec#getOutputFormatで取得できるMediaFormatと若干違いますが、これで実用上は問題なさそうなMediaFormatを生成することが出来ます。上で作ったMediaFormatをAndroid4.3以降の端末のMediaMuxerに渡しても大丈夫でした。でも公式な方法ではないtrickなので動かなくっても責任持ちませんm(__)m。
ちなみに、上のサンプルではMIME_TYPE =”video/avc”なのでcsdが2つ返ってくるものと決め打ちしていますが、色んなコーデックに対応するのであればループを作ってstart codeが見つかる間は順次csd-xxを追加していくのがいいと思います。

少しは役に立つかなぁ。実際の所これがわかっててもAndroid4.1〜4.2でMediaCodecを使うには乗り越えないといけない壁が他に沢山あるし・・・そもそも実際に困ってる人以外はこれを読んでもなんのこっちゃわからへんやろなぁ。
まぁそういうことで今回はおしまい。お疲れ様でした。

« »

  • スポンサードリンク

コメント

  • 中村 より:

    ご丁寧にご説明頂きましてありがとうございます。
    Shoutcastストリームをファイルに落としそのファイルを MediaExtrctor を使用して再生させる方法で実現できました。
    大変助かりましたありがとうございます!!

    • saki より:

      こんにちは。

      と言う事は音響データの圧縮形式がMP3(MPEG-1 Audio Layer-3)というだけでなくてファイルフォーマットとしてのMP3の状態で送られてきてるってことですね。
      ストリーミング配信ってことを考えるとID3v2のタグが埋まってて後はシーケンシャルに並んでいる感じなんでしょうかね?てことは先頭4バイト(FFF340C0だったかな?)+ID3v2タグをスキップすれば残りがデータフレームなので、各フレーム毎に切り出してMediaCodecへ流し込めば再生できるのかもしれないですね。

      フレームヘッダごと放り込めばいいのかそれともデータ部分だけにしないといけないのかは・・・AOSPのMediaExtractorのソースを眺めれば判るかも。
      でも、MediaCodecからのログ出力でフォーマット情報が出力されるってことから考えると多分フレームヘッダごと流し込めばいいんだとは思いますが。
      さらに言えばMediaExtractorから取得してMediaCodecに流し込む時にバイナリダンプすればフレームヘッダがついてるかどうか判りますね。

  • 中村 より:

    はじめまして、いつも楽しく拝見させて頂いています。

    MediaCodecでMP3のデコードをしたいと思っているのですがうまくいかず困っております。

    API>=18を使用している場合
    MediaCodec.INFO_OUTPUT_FORMAT_CHANGED
    に入ってきた後どういう処理をすれば宜しいでしょうか?

    スタックオーバーフローに書き込んでみたのですが
    もし宜しければ見て頂ければ幸いかと思います。

    http://ja.stackoverflow.com/questions/4861/android-mediaextractor-%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%AA%E3%81%84-mediacodec-%E3%83%87%E3%82%B3%E3%83%BC%E3%83%89%E6%96%B9%E6%B3%95%E3%81%8C%E3%82%8F%E3%81%8B%E3%82%8A%E3%81%BE%E3%81%9B%E3%82%93

    宜しくお願い致します。

    • saki より:

      こんにちは。

      大元はShoutcastのサーバーからくるストリームを再生したいということなんですよね?
      Shoutcastについては詳しくないんですけど、最初にicy-metaが来てその後にMP3の
      バイナリストリームが続くんですよね?

      試したことは無いのでうまくいくかどうかは判りませんけど、
      LocalSocketでproxy兼バッファリング風なクラスを作って
      1)icy-metaの部分は自前で処理してカット
      2)MP3のバイナリストリームが来たらFileDescriptorをMediaExtrctorとか
        MediaMetadataRetrieverの#setDataSource(FileDescriptor fd)へ
        渡すみたいな感じ

      とか、

      MP3のバイナリストリームの部分の長さが普通にファイルとして保存できる程度なら
      icy-metaをカットした残りをローカルファイルに出力してある程度バッファリングできたところで
      そのファイルpathまたはFileDescriptorをMediaExtrctorへ渡すなんてのも
      出来ると思いますけど。(保存した位置を超えないように読み込みの調整が必要です)

      ただ、MediaExtractorとかは基本seekableなFileDescriptorが必要なんでそこの
      辺りがどうなるんかなってのがちょっと気になります。

      ちなみに、ja.stackoverflow.comのログを見た感じだとMediaCodecの初期化がダメな気がします。
      多分MIMEを含めたMediaFormatが合ってないんじゃないかな?デコードの際にはデータを書き込む前
      (configure)の段階でちゃんと設定が完了してないとだめですが、その前の時点でエラーがかえって
      来ているように見えます。

      後は、ShoutcastからくるMP3のファイルの中身が判りませんが、MediaCodecが受け付けるのは
      MP3のタグ等のメタデータを除いた音声トラックの中身だけなので、解析せずに前から全部
      放り込んでもダメだと思います。
      例えばicy-metaを取り除いたバイナリストリーム部分をファイルに保存してそのファイルを普通に
      MediaPlayerとかMediaExtractor/MediaCodecで再生できます?
      出来るのであればそのデータ内にMP3のタグ等のメタデータが含まれているということなので、
      音声トラックのチャンクを順番通り切り出してMediaCodecに食わせないとダメなんじゃないでしょうか。
      (普通ならそれをするのがMediaExtractor)

      で、MediaCodecでの再生なんですけど、MediaCodec/AudioTrackの初期化と
      MediaCodecへの入力はなんとかなったとして、

      MediaCodec#dequeueOutputBufferの返り値によって
      1)INFO_OUTPUT_BUFFERS_CHANGEDが返って来た時
        MediaCodec#getOutputBuffersで出力バッファを取得します。
        API21でdeprecatedになっちゃいましたけど、まだ使えます。
      2)INFO_OUTPUT_FORMAT_CHANGEDが返って来た時
        特にすることはありません。
        寂しい人はMediaCodec#getOutputFormatでも呼び出してログに出力すればいいんじゃないかな
      3)0以上が返って来た時
      3−1)BufferInfo.size・offsetと取得したByteBufferをAudioTrackへ書き込みます。
        ByteBufferは直接書き込めないので一旦byte[]へ読み込まないとダメです。

      if (mAudioOutTempBuf.length < size) {
      mAudioOutTempBuf = new byte[size];
      }
      buffer.position(offset);
      buffer.get(mAudioOutTempBuf, 0, size);
      buffer.clear();
      if (mAudioTrack != null)
      mAudioTrack.write(mAudioOutTempBuf, 0, size);

        こんな感じですね。
      3−2)これで安心してはいけません。時間調整をしたいと早回し再生になってしまいます。
        簡易的にはこんな感じにします。
        大事なのはMediaCodec#releaseOutputBufferを呼び出す前に時間調整することです。
      (横に長いソースだけどちゃんと表示されるかなぁ?・・・タブとスペースが全部・・・(T_T))

      protected long adjustPresentationTime(final Object sync,
      final long startTime, final long presentationTimeUs) {

      if (startTime > 0) {
      for (long t = presentationTimeUs – (System.nanoTime() / 1000 – startTime);
      t > 0; t = presentationTimeUs – (System.nanoTime() / 1000 – startTime)) {
      synchronized (sync) {
      try {
      sync.wait(t / 1000, (int)((t % 1000) * 1000));
      } catch (InterruptedException e) {
      }
      if ((mState == REQ_STOP) || (mState == REQ_QUIT))
      break;
      }
      }
      return startTime;
      } else {
      return System.nanoTime() / 1000;
      }
      }

        まぁわざわざループにする必要はあまりないんですけどね。

      3−3)使用済みの出力バッファをMediaCodec#releaseOutputBufferで返却します。

      4)BufferInfo.flagsにBUFFER_FLAG_END_OF_STREAMフラグがたったら終了です。
        気を付けないといけないのは、MediaCodecへの入力が終わっても出力側が終了するまで
        MediaCodecを破棄しちゃダメってことです。

%d人のブロガーが「いいね」をつけました。