JellyBeanの新しいAPIを試してみる(MediaCodecと外部ディスプレイ) #AndroidAdvent2012 16日目裏


このエントリは [twitter:@ytRino] による、 Android Advent Calendar (#AndroidAdvent2012) の16日目 裏 のエントリです。

MediaCodex API

API16でなんとメディアを自分でエンコードしたデコードしたりできるらしいぞーと聞いて、これは気になると思い調べてみました。
特にエンコードは個人的に夢が広がりんぐなのでやってみたくなりました。

まずはmp3をデコードしながら再生

GoogleIOのビデオSessions  |  Google I/O 2012  |  Google Developers
喋ってる人がどうみてDave Burkeさんの写真と別人ですが何があったんでしょうか
基本的にはMediaCodecに書いてある手順でやれば行けます。
InputBufferに元となる音データをqueueしていくと変換されたデータがOuputBufferからdequeueで取れるという流れです。

MediaCodec  |  Android Developers

 MediaCodec codec = MediaCodec.createDecoderByType(type);
 codec.configure(format, ...);
 codec.start();
 ByteBuffer[] inputBuffers = codec.getInputBuffers();
 ByteBuffer[] outputBuffers = codec.getOutputBuffers();
 for (;;) {
   int inputBufferIndex = codec.dequeueInputBuffer(timeoutUs);
   if (inputBufferIndex >= 0) {
     // fill inputBuffers[inputBufferIndex] with valid data
     ...
     codec.queueInputBuffer(inputBufferIndex, ...);
   }

   int outputBufferIndex = codec.dequeueOutputBuffer(timeoutUs);
   if (outputBufferIndex >= 0) {
     // outputBuffer is ready to be processed or rendered.
     ...
     codec.releaseOutputBuffer(outputBufferIndex, ...);
   } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
     outputBuffers = codec.getOutputBuffers();
   } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
     // Subsequent data will conform to new format.
     MediaFormat format = codec.getOutputFormat();
     ...
   }
 }
 codec.stop();
 codec.release();
 codec = null;

ctsのとかも参考に割りと簡単にmp3をデコードできました。
ソースをMediaExtractorに突っ込む -> MediaCodecに情報を渡す -> どんどんデータを流す -> どんどんデコードされたデータが出てくる -> 再生
こんな感じ
エンコードもこの調子で楽々! と行きたかったのですが…

flacエンコードしてみる

デコードの時はデコーダ用のMediaFormatはextractorからとれましたが、エンコードはそうはいかないので出力側のMediaFormatは入力側のMediaFormatをもとに自分で作ることにします。

        String mime = "audio/flac";
        MediaFormat outFormat = MediaFormat.createAudioFormat(mime,
                inFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE),
                inFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT));
        // flac用のkeyがあったのでつけてみた
        outFormat.setInteger(MediaFormat.KEY_FLAC_COMPRESSION_LEVEL, 0);

        final MediaCodec codec = MediaCodec.createEncoderByType(mime);
        codec.configure(outFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE /* encoder flag */);
        codec.start();

        extractor.selectTrack(0);

がこれでデコードと同じようにエンコードしようと、WAVファイルを入力としたMediaExtractor#readSampleData()したところ、
IllegalArgumentExceptionで落ちました。謎に思いつつも入力をmp3にしたところ、ファイル自体はエラーなく無事にできました。
てっきりエンコーダにはPCMを突っ込むものだと思ってたのですがそれ以前のエラーに…
ひとまずPC側にpullして(端末がflacのデコードに対応してないので)みたものの再生できず…
入力がmp3だから?
用意したwavが悪いのか?
ともかくwavのextractorがエラーになってしまうので、FileInputStreamで自前でバイナリを読み込んでqueueしてみる方法も試してみましたが、mp3のデコードはなんとかできそうな感じではあるもののやはりエンコードがびみょい。
残念。

データの中身を見てみよう

ここでmp3->wav(pcm)で、流れていくデータを覗いてみると、デコードされて出てきたデータにはWAVヘッダがなく(ってPCMそのままAudioTrackなどに流して再生できるという点からは当たり前か)音データ本体のみのようです。
とりあえずエンコードされたように見えるmp3->flacのパターンではflacのフレーム(ヘッダ+データ)らしきものが出てきてるので、(果たしてmp3を入力するのはどうなんだということを抜きにして)やはりflacヘッダはないもののエンコード自体は一応できてることになります。というわけでフレーム本体しかないのでファイルに書き出しても正しく認識されないのも当然です。
ちなみに入力のmp3もフレーム区切りでextractorから流れてくるぽいです。
FLAC - format たとえば流れてきた各outputBufferの先頭(=フレームの先頭)がff f8 39という並びで始まるとき、sample rateは44.1kHzであるみたいな。
ためしに

        MediaFormat format = MediaFormat.createAudioFormat(mime, 44100, 1);

の44100を22050にしてみると、ff f8 36で出力されるようになったので多分あってるはずです。

エンコーダから流れてきたデータにヘッダつければいける?

とりあえずわからないところを飛ばしつつ最低限のヘッダをつける

//flacのヘッダを付加したい
    public void addHeader(MediaFormat format) throws IOException {
        FileInputStream in;
        FileOutputStream out;

        ...

        // marker
        String marker = "fLaC";
        out.write(marker.getBytes());

        // meta data
        byte[] metaHeader;
        byte[] metaData;

        metaHeader = new byte[] {
                0x1 << 3 | 0, 0, 0, 34 // last metadata flag and stream-info header
        };
        // stream info のデータを作る
        metaData = metaDataStreamInfo(format);
        dump(metaData);

        out.write(metaHeader);
        out.write(metaData);

        byte[] buf = new byte[1024];
        int r;
        while ((r = in.read(buf)) > 0) {
            out.write(buf, 0, r);
        }

        in.close();
        out.close();
    }

    private byte[] metaDataStreamInfo(MediaFormat format) {
        Log.v(TAG, format.toString());
        int hz = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
        int ch = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT) - 1; /* ch -1 */
        int total = 0; /* 0->unknownのようだが… */
        int bit = 16 - 1; /* 16bits per sample -1 */
        return new byte[] {
                0x01, 0x00, /* min block size (?) */
                0x10, 0x00, /* max block size (?) */
                0x00, 0x00, 0x01, /* min frame size, unknown */
                0x10, 0x00, 0x00, /* max frame size, unknown */
                (byte) ((hz >> 12) & 0xff), /* sample rate (hz) 20bits */
                (byte) ((hz >> 4) & 0xff),
                (byte) (((hz << 4) & 0xff) /* 4bits */| ((ch << 1) & 0xff) /* 3bits */| ((bit >> 4) & 0xff) /* 1bit */),
                (byte) ((bit << 4) & 0xff | 0), /* bits(low 4bit) + total samples in stream(first 4bit...?) */
                (byte) ((total >> 24) & 0xff), (byte) ((total >> 16) & 0xff), (byte) ((total >> 8) & 0xff), (byte) (total & 0xff), /* 32bit, 0 -> unknown */
                0, 0, 0, 0, 0, 0, 0, 0, /* md5 signature of unencoded audio data */
                0, 0, 0, 0, 0, 0, 0, 0,
        };
    }

やけくそです。全然埋まってないし。 しかしこれが正しいのか確認する手段もないのでひとまず実行。

バイナリ確認してそれっぽい形になっているがやっぱり再生できずタイムアップ

total sizeやmd5をちゃんとしないとダメなのかもっと他の原因なのか…
flacの情報をさらに探していたところ、flacのgitに、wavのヘッダからトータルサイズを計算しているところがありました。wavのヘッダから読み取った値をそのまま4分の1してるだけのようだけど…
(16bit 2ch固定だから変える必要あるかも)しかしこのあとさらにflac側で処理をしていなければ自前ヘッダのサイズにも使えそう?
Xiph.org - flac.git/blob - examples/c/encode/file/main.c

  82         total_samples = (((((((unsigned)buffer[43] << 8) | buffer[42]) << 8) | buffer[41]) << 8) | buffer[40]) / 4;

しかしここでタイムアップです。あれですね、もうちょっと知識がないと辛いですね。
…ていうかflacエンコードっている? いる?
せめてmp3欲しいよね、うん。 だめなのわかってるけどさ!
ああ、jniでやれってことですよね!!


一応作ったものを公開しておきます… 整理されてないですが。 GitHub - ytRino/MediaCodecSample: MediaCodecAPIをつかってエンコードしてみたかった...
このアプリではついでにMediaCodecListを使って対応しているエンコーダ・デコーダ一覧を表示させています。

Presentation(とmiracast(Wifi Display))

android4.2から、外部デイスプレイがサポートされ、外部ディスプレイ用のコンテンツを簡単に作ることができます。やったね!
概要はhttp://greety.sakura.ne.jp/awiki/index.php?Android%204.2#k9ceb062の日本語訳がお勧めです。

とりあえず試してみる

MediaCodecに時間を取られてしまったので簡単にApiDemosで試してみます。
外部ディスプレイ機能は、実物がなくてもシミュレートすることができます。開発者向けオプションを有効にして、「2次画面シミュレート」から適当なのを選びます。とりあえず2つ出して見ます。
画面上にちっちゃいディスプレイが2つオーバーレイされ、開発者向けオプションの画面が3つになりました。外部ディスプレイが接続された体です。
ApiDemosからApp>ActivityのPresentationとPresentation with Media Routerで試すことができます。
Presentationではリストされたディスプレイの中から好きなだけ選択してそれぞれ別の内容を表示することができます。with Media RouterではMediaRouterクラスを使用して、適当なディスプレイにコンテンツを表示してくれます。

外部ディスプレイ用のコンテンツを表示するには

外部ディスプレイ用に別のコンテンツを表示するにはPresentationクラスを使います。今まで画面のサイズをとったりするのぐらいしか出番のなかった地味なDisplayクラスですが、これまた新しく加わったDisplayManagerから外部ディスプレイ一覧がDisplayとして取得できます。これをPresentationのコンストラクタに渡してshow()することで外部デイスプレイに表示させることができます。地味だったDisplayが大活躍
PresentationはonCreate()でsetContentView()するという馴染みある手順で簡単に実装できるます。やったね!

    private void showPresentation(Display display, PresentationContents contents) {
        final int displayId = display.getDisplayId();
        if (mActivePresentations.get(displayId) != null) {
            return;
        }

        Log.d(TAG, "Showing presentation photo #" + contents.photo
                + " on display #" + displayId + ".");

        // ここ
        DemoPresentation presentation = new DemoPresentation(this, display, contents);
        presentation.show();
        presentation.setOnDismissListener(mOnDismissListener);
        mActivePresentations.put(displayId, presentation);
    }
    private final class DemoPresentation extends Presentation {

        ...

        public DemoPresentation(Context context, Display display, PresentationContents contents) {
            super(context, display);
            mContents = contents;
        }

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.presentation_content);

           ...
miracast - Wifi Display

android4.2からmiracastとよばれるwifi通信による外部ディスプレイ表示ができるようになりました。GalaxyS3なんかは独自に同じようなことをやってるみたいですが。
miracastでの接続もDisplayManagerで同様に扱えるとのことで一安心ですね。
…まあそれだけなんですが。ついでにちょっと調べたところ、
Settingsも新たにSettings.Globalとして編成されていて、例によって「システム設定画面経由でかえさせてね ☆(ゝω・)vキャピ」とのことです。Specialized APIってどれのことだろう…

Global system settings, containing preferences that always apply identically to all defined users. Applications can read these but are not allowed to write; like the "Secure" settings, these are for preferences that the user must explicitly modify through the system UI or specialized APIs for those values.

というわけで手元に4.2がある方はぜひやってみてください。(ぇ
oesf.biz - このウェブサイトは販売用です! -&nbspoesf リソースおよび情報

    340         @Override
    341         public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
    342             mWifiDisplayOnSetting = isChecked;
    343             Settings.Global.putInt(getContentResolver(),
    344                     Settings.Global.WIFI_DISPLAY_ON, isChecked ? 1 : 0);
    345         }

oesf.biz - このウェブサイトは販売用です! -&nbspoesf リソースおよび情報

   4725        /**
   4726         * Whether Wifi display is enabled/disabled
   4727         * 0=disabled. 1=enabled.
   4728         * @hide
   4729         */
   4730        public static final String WIFI_DISPLAY_ON = "wifi_display_on";

最後に

「今年もAndroidアドベントカレンダー参加してみようかな 中頃がいいな」と募集開始からタイミングを伺っていたところ半日たたずに表が埋まっていたのを見て嬉しいやら悲しいやらだったytRinoです。最近はありがたいことにアルバイトでandroidのコードばっか書いているので個人で書く時間があまり無いです。なんかもう大学よりバイトの時間のほうが長いという話があるとかないとか。
そして12月といえば…シューカツですね。
「在学年数の差が、戦力の決定的差ではないことを教えてやる!」
…就職先は随時募集しております。

ということで、グダグダな上に中途半端感あふれるエントリになってしまいましたが少しでも参考になるところがあればいいなというとこで16日裏のエントリとさせて頂きます。あなたもレッツニューエーピーアイ!

Android Advent Calendar