LoginSignup
2
1

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-11-17

TL;DR

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

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

全体構成

↓ のような一番シンプルな構成を実現したときの,こけポイントを書きます.
blueprint.png

こけポイント

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

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

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

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

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

---///

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1