MediaCodec+MediaMuxerを使って音&動画の同時キャプチャがした〜いの第2弾♪
前回は前置きが長かったせいか、抽象クラスのMediaEncoderだけで終わってしまったので、今回は頑張らねば。と言う事でいきなりコード(^^)v 簡単な音声の方から行きます。
MediaAudioEncoder
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
package com.serenegiant.encoder; public class MediaAudioEncoder extends MediaEncoder { private static final boolean DEBUG = true; // TODO set false on release private static final String TAG = "MediaAudioEncoder"; private static final String MIME_TYPE = "audio/mp4a-latm"; private static final int SAMPLE_RATE = 44100; // 44.1[KHz] is only setting guaranteed to be available on all devices. private static final int BIT_RATE = 64000; private AudioThread mAudioThread = null; public MediaAudioEncoder(MediaMuxerWrapper muxer, MediaEncoderListener listener) { super(muxer, listener); } @Override protected void prepare() throws IOException { if (DEBUG) Log.v(TAG, "prepare:"); mTrackIndex = -1; mMuxerStarted = mIsEOS = false; // prepare MediaCodec for AAC encoding of audio data from inernal mic. final MediaCodecInfo audioCodecInfo = selectAudioCodec(MIME_TYPE); if (audioCodecInfo == null) { Log.e(TAG, "Unable to find an appropriate codec for " + MIME_TYPE); return; } if (DEBUG) Log.i(TAG, "selected codec: " + audioCodecInfo.getName()); final MediaFormat audioFormat = MediaFormat.createAudioFormat(MIME_TYPE, SAMPLE_RATE, 1); audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); audioFormat.setInteger(MediaFormat.KEY_CHANNEL_MASK, AudioFormat.CHANNEL_IN_MONO); audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE); audioFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1); // audioFormat.setLong(MediaFormat.KEY_MAX_INPUT_SIZE, inputFile.length()); // audioFormat.setLong(MediaFormat.KEY_DURATION, (long)durationInMs ); if (DEBUG) Log.i(TAG, "format: " + audioFormat); mMediaCodec = MediaCodec.createEncoderByType(MIME_TYPE); mMediaCodec.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); mMediaCodec.start(); if (DEBUG) Log.i(TAG, "prepare finishing"); if (mListener != null) { try { mListener.onPrepared(this); } catch (Exception e) { Log.e(TAG, "prepare:", e); } } } @Override protected void startRecording() { super.startRecording(); // create and execute audio capturing thread using internal mic if (mAudioThread == null) { mAudioThread = new AudioThread(); mAudioThread.start(); } } @Override protected void release() { mAudioThread = null; super.release(); } /** * Thread to capture audio data from internal mic as uncompressed 16bit PCM data * and write them to the MediaCodec encoder */ private class AudioThread extends Thread { @Override public void run() { final int buf_sz = AudioRecord.getMinBufferSize( SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT) * 4; final AudioRecord audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, buf_sz); try { if (mIsCapturing) { if (DEBUG) Log.v(TAG, "AudioThread:start audio recording"); final byte[] buf = new byte[buf_sz]; int readBytes; audioRecord.startRecording(); try { while (mIsCapturing && !mRequestStop && !mIsEOS) { // read audio data from internal mic readBytes = audioRecord.read(buf, 0, buf_sz); if (readBytes > 0) { // set audio data to encoder encode(buf, readBytes, getPTSUs()); frameAvailableSoon(); } } frameAvailableSoon(); } finally { audioRecord.stop(); } } } finally { audioRecord.release(); } if (DEBUG) Log.v(TAG, "AudioThread:finished"); } } /** * select the first codec that match a specific MIME type * @param mimeType * @return */ private static final MediaCodecInfo selectAudioCodec(String mimeType) { if (DEBUG) Log.v(TAG, "selectAudioCodec:"); MediaCodecInfo result = null; // get the list of available codecs final int numCodecs = MediaCodecList.getCodecCount(); LOOP: for (int i = 0; i < numCodecs; i++) { final MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i); if (!codecInfo.isEncoder()) { // skipp decoder continue; } final String[] types = codecInfo.getSupportedTypes(); for (int j = 0; j < types.length; j++) { if (DEBUG) Log.i(TAG, "supportedType:" + codecInfo.getName() + ",MIME=" + types[j]); if (types[j].equalsIgnoreCase(mimeType)) { if (result == null) { result = codecInfo; break LOOP; } } } } return result; } } |
抽象クラスのMediaEncoderを継承しています(2行目)。実装するのは、#prepare, #startRecording, #release, とsourceスレッドであるAudioThreadと、#prepareのヘルプ用の#selectAudioCodecです。簡単な方から説明します。
#startRecording
#startRecordingでは親の#startRecordingを呼び出した後、sourceスレッド(AudioThread)の生成と実行開始をしていますが、#prepareの最後に移動することも出来ます。ただし、その場合にはAudioThread内でstartRecording待ちの処理を追加しないといけません。
わざわざ#startRecordingというメソッドを作ったのに、AudioThread内にも待機処理を追加しないといけないのでは芸がありませんので、#startRecordingが呼ばれた時に初めてsourceスレッドを生成・実行するようにしています。
#release
これはまぁわざわざ説明するまでも無いですし、必須でもありませんがGCを確実にするためにnull代入しています。実際にはmAudioThreadフィールド自体をなくしてしまうことも可能ですが、個人的に気持ち悪いのでこんな事をしています。
#prepare
ここは、MediaCodec関係の実装で一番大事なものの1つです。これを間違えると最悪catch出来ない例外でクラッシュします。audioでは経験無いですが、videoだと端末丸ごとリセットっていうパターンもありです(´・ω・`)。と言ってもワンパターンなので大抵は丸ごとコピーですね(-_-;)
全ての機種でサポートされていることが保証されているサンプリングレートは44.1[KHz]のみらしいです。なので特に理由がない限りサンプリングレートには44100を指定しましょう。
端末内蔵マイクでステレオの物は見たことの聞いたこともないので、モノラル指定。ということはビットレートは64Kbpsもあれば十分でしょう。ステレオなら128Kbps相当ですね。ビットレートを上げれば基本的には音質が良くなっていきます。ただ端末の種類にもよりますが端末内蔵マイクからの入力自体がそんなに高音質ではないのでほどほどがいいかも。確か320Kbps程度まで設定出来たと思いますが、サポートしてない再生ソフトもありますし、高ビットレートにするとvideo側の帯域にも影響してしまいます。最新機種はともかく少し古くなるとNexus7(2012)等の様にaudio+video合計で1Mbps出ない機種も多くあります。
後は普通に、コーデックを選択・MediaFormatを設定・MediaCodec#createEncoderByTypeでコーデックを生成・configure・startの順に実行するだけです。
AudioThread
sourceスレッドの実体です。AudioRecordを使って16ビット無圧縮PCM音声データを取得してバイト配列として#encodeへ引き渡すだけです。後は上位クラスのMediaEncoderが勝手に処理してくれます。便利ですね〜わざわざ抽象クラスを作ったかいが有るというものです。
ここで気を付けるのは、AudioRecord#getMinBufferSizeが返してきた値をそのまま使うのはあまり良くないということです。2倍以上を指定するのが定番でしょう。小さすぎると音声が途切れたりします。端末の性能が低かったり他の処理の負荷が重い場合は大きめにした方が良いのかもしれません。でも大きすぎるとメモリの無駄遣いでGCが多くなってかえって思ったように動かなくなります。今回は余裕を見て4倍に設定しています。
なお、ここでの処理は単純で直線的なので余計なことはせずに全て#run内に押し込めています。
#selectAudioCodec
ここも説明は必要ないでしょうけど。
別の記事MediaCodecInfo#getCapabilitiesForTypeが激遅になる件に書いたとおり、コーデックの選択方法は2通りありますが、たぶんこっちの方がいいと思います。
今日は家の近所も時々大雨でしたが、ソースの嵐もまだまだ続きます。次はvideoです。
コメント
[…] 音&動画の同時キャプチャがした〜い(その2) […]