TL;DR
AndroidのCamera2,AudioRecord,MediaCodec,MediaMuxerを組み合わせてMP4録画機能を実装する中で,
『こけポイント』(ハマりポイント)に何個かぶつかったので,記録として残しておきます.
ぐぐってもあまり出てこなかったものばかりなので,このTextが誰かの助けになればと.
(Code全体の説明は大規模になりすぎるので,こけポイントだけ書いておきます)
全体構成
↓ のような一番シンプルな構成を実現したときの,こけポイントを書きます.
こけポイント
GL/EGLでMediaCodec Input SurfaceにRenderした画がEncodeされない
VideoのEncodeのために,MediaCodecのInput Surfaceに画を描く方法としてGL/EGLがAndroidDeveloperに書かれていますが,実際にRenderした画をEncodeさせるために必要な設定が書かれてませんでしたので,メモ.
const EGLint eglConfigAttrs[] = {
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_RECORDABLE_ANDROID, 1,
EGL_NONE
};
eglChooseConfig(display, eglConfigAttrs, &config, 1, &numConfigs)
MediaCodecのInput Surfaceを使ってGL/EGLを初期化するときにeglChooseConfig()にわたすConfig Attributesですが,この
EGL_RECORDABLE_ANDROID, 1,
の設定が無いと,SurfaceのFrame BufferをMediaCodecから読めないらしく,画が1枚もInputされてない状態になるようです.
このEGL_RECORDABLE_ANDROID
について,↓ のKhronosのサイトに仕様?が置かれていました.
ANativeWindowの実装が複数あり,Video Recordに対応したConfigを明示的に選択するために使うもののようです.
https://www.khronos.org/registry/EGL/extensions/ANDROID/EGL_ANDROID_recordable.txt
Android supports a number of different ANativeWindow implementations that
can be used to create an EGLSurface. One implementation, which records the
rendered image as a video each time eglSwapBuffers gets called, may have
some device-specific restrictions. Because of this, some EGLConfigs may be
incompatible with these ANativeWindows. This extension introduces a new
boolean EGLConfig attribute that indicates whether the EGLConfig supports
rendering to an ANativeWindow that records images to a video.
AudioRecordの録音で先頭Bufferにノイズが乗る
AudioRecordはstartRecording()
後にread()
を使ってPCM16とかのAudio Bufferを取得します.
取得したBufferをMediaCodecに渡してEncodeすることになりますが,↓ のような処理でAudioRecordが中で持っている(と思われる)Ring Bufferの初期値を一回捨てないとノイズ音として最終のMP4に記録されてしまう問題が発生しました.
val minBufSize = AudioRecord.getMinBufferSize(
44100, // Sampling rate,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT)
val audioRecord = AudioRecord.Builder()
.setBufferSizeInBytes(minBufSize * 2)
.build()
audioRec.startRecording()
// Flush buffer.
val buf = ByteArray(minBufSize * 2)
audioRec.read(buf, 0, buf.size, AudioRecord.READ_BLOCKING)
最後の2行が(本来必要ないはずだが)追加したものです.Readしたbuf
は使わないので,すぐにGCの回収対象になります.
Bufferの初期値が不定なのが根本原因では無いかもしれませんし,機種依存かもしれませんので,この対処が正しくない可能性はあります.
MediaCodecの同期モードを使うと処理が止まる
MediaCodecのAndroidDeveloperの公式Referenceには
- 同期モード (Callbackセットしない)
- 非同期モード (Callbackセットする)
の2つのサンプルが書かれています.
が,Audio Encode用のMediaCodecを同期モードで実装したところ,
MediaCodec.dequeueInputBuffer()
から制御が戻ってこず,処理が進まない,という問題が発生しました.
(MediaCodec.queueInputBuffer()
してないとかではないです)
公式サンプルそのままのため,理由がよくわからず,深堀りする前に非同期モードに実装変更してしまいました.
MediaCodecのOutput Buffer Infoのpresentation Timestampが巻き戻る
AudioのMediaCodec処理で,MediaCodec.queueInputBuffer()
にPresentation Timestampを渡します.
このPresentation Timestampは通常,System.nanoTime() / 1000
とかの値を使って,単調増加(monotonic)になるようにします.
MediaCodec.queueInputBuffer()
に渡したPresentation TimestampがOutput Buffer InfoのPresentation Timestampとして渡ってきます.
これを元に計算した値をMediaMuxer.writeSampleData()
に渡すことになりますが,このときMediaMuxerに渡すPresentation Timestampが単調増加になってないと例外発生でCrashします.
Input BufferのPresentation Timestampが単調増加ならMediaCodecを通ったあとのOutput BufferのPresentation Timestampも単調増加になるように思えますが(実際,ほとんどのケースではそうなりますが),Output BufferのPresentation Timestampが巻き戻る問題が発生することがありました.
巻き戻った値はInput Bufferに入れたどのPresentation Timestampとも一致しなかったため,出所不明の値です.
↓ の処理でOutput BufferのPresentation Timestampを単調増加になるように調節して対応しました.
private var previousOutBufPresentationTimeUs = 0L
private fun getValidNextPresentationTimeUs(outBufPresentationTimeUs: Long): Long {
if (outBufPresentationTimeUs < previousOutBufPresentationTimeUs) {
// Output buffer presentation time must be monotonic. (increase only)
return previousOutBufPresentationTimeUs
}
previousOutBufPresentationTimeUs = outBufPresentationTimeUs
return outBufPresentationTimeUs
}
override fun onOutputBufferAvailable(codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo) {
....
info.presentationTimeUs = getValidNextPresentationTimeUs(info.presentationTimeUs)
muxer.writeSampleData(audioTrackIndex, outBuf, info)
....
}
最後にMediaMuxerにInputしたTimestampよりも前の(昔の)Timestampが渡されたときは,直前にMediaMuxerにInputしたTimestampを使う,という処理です.
これも,根本原因が不明なため,正しい対処なのかどうか不明です.
普通に考えてPresentation Timeが同じAudio Frameがあると音が重なってノイズに聞こえるような気がしますが,出来上がるMP4に不自然な音は聞こえません.
(1 Frameの異常には人は気付けない,というだけかもしれません)
GL/EGLからMediaCodecのInput SurfaceにRenderした画のPresentation Timestampの基準点が不明
AudioのほうはMediaCodec.queueInputBuffer()
のPresentation Timestampを自分で制御できるので単位だけ考えておけば問題ないですが,VideoのほうはGL/EGLからRenderするため,InputされるPresentation Timestampが不明です.
このTimestampの基準点(Timestap == 0はいつか)が不明だったので調べてみました.公式ReferenceにMONOTONIC TIME
という記載は出てきますが,具体的な関数など書かれていません.
↓ のKhronosのサイトにAndroidのPresentation Timeについての仕様書?が置かれていました.
https://www.khronos.org/registry/EGL/extensions/ANDROID/EGL_ANDROID_presentation_time.txt
Clock(Timestamp)の取得方法についての記述があり,
2. How can the current value of the clock that should be used for the
presentation time when an absolute time is needed be queried on Android?
RESOLVED: The current clock value can be queried from the Java
System.nanoTime() method, or from the native clock_gettime function by
passing CLOCK_MONOTONIC as the clock identifier.
JAVA側は
System.nanoTime()
で取れる値,
Native側は ↓ の関数で取れる値と同じ基準になっているようです.
timespec ts{};
clock_gettime(CLOCK_MONOTONIC, &ts);
long nanoTime = ts.tv_sec * 1000 * 1000 * 1000 + ts.tv_nsec;
どちらもMediaMuxerにInputするために単位をMicro Secondに変換する必要はあります.
MediaMuxer.addTrack()するタイミングがシビア
MediaMuxer.addTrack()
にVideo FormatやAudio Formatを渡しますが,MediaCodecを作るときに使ったFormatだとパラメータが足りないのか,例外発生でCrashします.
(Parameter Invalid的なエラーメッセージがlogcatに出てたと思います)
そのため,MediaMuxer.addTrack()
はMediaCodec.Callbac.onOutputFormatChanged()
で渡されるMedia Formatを使う必要がありました.
ただし,MediaMuxer.addTrack()
はMediaMuxer.start()
の前に呼ばなければいけないため,VideoとAudio両方のonOutputFormatChanged()
完了後にMediaMuxerをstartするタイミング制御が必要になります.
(VideoとAudioの遅い方のonOutputFormatChanged()
の中でMediaMuxer.start()
を呼ぶ,等)
まとめ
通常,AndroidでMP4録画しようとしたらMediaRecorderを使うと思います.
今回,VideoのAspectを1:1にしたかったため,Camera2からMediaRecorderに直接接続することはできませんでした.
(CameraのSupported SizeのAspectでないとMediaRecorderに直接接続できない)
そのため,GL/EGLを使ってCameraのFrameを1:1にCropしてRenderするような実装を作りました.
また,せっかくなのでMediaRecorderでなくMediaCodecを使ってみよう,と思ったのですが,公式サンプルもろくに無く,ぐぐっても情報がほとんど出てこない,という状態で,だいぶ七転八倒することになりました.
---///