Camera2+AudioRecord+MediaCodec+MediaMuxerでMP4録画するときの "こけポイント"

# TL;DR
AndroidのCamera2，AudioRecord，MediaCodec，MediaMuxerを組み合わせてMP4録画機能を実装する中で，
『こけポイント』(ハマりポイント)に何個かぶつかったので，記録として残しておきます．
ぐぐってもあまり出てこなかったものばかりなので，このTextが誰かの助けになればと．

(Code全体の説明は大規模になりすぎるので，こけポイントだけ書いておきます)



# 全体構成
↓ のような一番シンプルな構成を実現したときの，こけポイントを書きます．
![blueprint.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129035/5533b90d-9c71-8b9f-60f1-821a623df8cc.png)


# こけポイント

## GL/EGLでMediaCodec Input SurfaceにRenderした画がEncodeされない

VideoのEncodeのために，MediaCodecのInput Surfaceに画を描く方法としてGL/EGLがAndroidDeveloperに書かれていますが，実際にRenderした画をEncodeさせるために必要な設定が書かれてませんでしたので，メモ．

```cpp:.cpp
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に記録されてしまう問題が発生しました．

```kotlin:.kt
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を単調増加になるように調節して対応しました．

```kotlin:.kt
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側は ↓ の関数で取れる値と同じ基準になっているようです．

```cpp:.cpp
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を使ってみよう，と思ったのですが，公式サンプルもろくに無く，ぐぐっても情報がほとんど出てこない，という状態で，だいぶ七転八倒することになりました．

---///