PS:やりたいことをやり、毎日少しずつでも行動する。どれだけやるかは求めず、徐々に変わっていく。
音声と映像に関する知識を理解したら、以下の 2 つの記事を先に読むことができます:
この記事の主な内容は、Android のネイティブハードウェアエンコード・デコードフレームワーク MediaCodec とマルチプレクサ MediaMuxer を使用して mp4 ビデオファイルを録画することです。ビデオデータソースは Camera2 によって提供されます。ここでは、mp4 の録画ではなく、エンコードとマルチプレクシングのプロセスに重点を置いています。単にビデオを録画するだけであれば、より便利な MediaRecorder を選択できますが、慣例として MediaCodec を事例形式で学ぶことにします。MediaCodec のさらなる使用法は、今後の記事で紹介されます。この記事の主な内容は以下の通りです:
- Camera2 の使用
- MediaCodec の入力方式
- MediaCodec による Camera2 データのエンコード
- 録画プロセス
- 録画効果
Camera2 の使用#
Camera2 は Android 5.0 から導入された新しいカメラ API で、最新の CameraX は Camera2 に基づいており、Camera2 よりも使いやすい API を提供しています。後の文で言及される関連 API は、以下の図を直接参照できます。これが Camera2 の使用の概略図です:
MediaCodec の入力方式#
MediaCodec を使用してエンコード操作を行うためには、カメラのデータをエンコーダ MediaCodec に入力する必要があります。データを MediaCodec に書き込む方法は 2 つあります。具体的には以下の通りです:
- Surface:Surface をエンコーダ MediaCodec の入力として使用します。つまり、MediaCodec が作成した Surface をその入力として使用します。この Surface は MediaCodec の createInputSurface メソッドによって作成され、カメラが有効なデータをこの Surface にレンダリングすると、MediaCodec は直接エンコードされたデータを出力できます。
- InputBuffer:入力バッファをエンコーダ MediaCodec の入力として使用します。ここで必要なデータは原始フレームデータです。Camera2 の場合、ImageReader を介してフレームデータを直接取得できます。取得した Image には、幅、高さ、フォーマット、タイムスタンプ、YUV データ成分などの情報が含まれており、制御の程度が高くなります。
MediaCodec による Camera2 データのエンコード#
MediaCodec のデータ処理方式について簡単に説明します。Android 5.0 以前は ByteBuffer [] の同期方式のみをサポートしていましたが、その後は ByteBuffer の同期および非同期方式の使用が推奨されています。ここでは ByteBuffer の同期方式を使用し、関与するプロセスは主にビデオデータのエンコードとマルチプレクシングです。前述のように、MediaCodec の入力は Surface を介して行われるため、ここではすでにエンコードされたデータを取得し、マルチプレクサ MediaMuxer を使用して Mp4 ファイルを生成するだけです。重要なコードは以下の通りです:
// 成功裏にエンコードされた出力バッファのインデックスを返す
var outputBufferId: Int = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0)
if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// ビデオトラックを追加
mTrackIndex = mMediaMuxer.addTrack(mMediaCodec.outputFormat)
mMediaMuxer.start()
mStartMuxer = true
} else {
while (outputBufferId >= 0) {
if (!mStartMuxer) {
Log.i(TAG, "MediaMuxerが開始されていません")
continue
}
// 有効なデータを取得
val outputBuffer = mMediaCodec.getOutputBuffer(outputBufferId) ?: continue
outputBuffer.position(bufferInfo.offset)
outputBuffer.limit(bufferInfo.offset + bufferInfo.size)
if (pts == 0L) {
pts = bufferInfo.presentationTimeUs
}
bufferInfo.presentationTimeUs = bufferInfo.presentationTimeUs - pts
// データをマルチプレクサに書き込んでファイルを生成
mMediaMuxer.writeSampleData(mTrackIndex, outputBuffer, bufferInfo)
Log.d(
TAG,
"pts = ${bufferInfo.presentationTimeUs / 1000000.0f} s ,${pts / 1000} ms"
)
mMediaCodec.releaseOutputBuffer(outputBufferId, false)
outputBufferId = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0)
}
}
録画プロセス#
ここでは Surface をエンコーダ MediaCodec の入力として使用します。MediaCodec が設定状態に入ると、Surface を作成できます。つまり、createInputSurface は configure と start の間にのみ呼び出すことができます。以下を参照してください:
// 設定状態
mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
// MediaCodecの入力としてSurfaceを作成。createInputSurfaceはconfigureとstartの間にのみ呼び出すことができる
mSurface = mMediaCodec.createInputSurface()
// start ...
これを SessionConfiguration の出力 Surface リストに追加します。以下を参照してください:
// CaptureSessionを作成
@RequiresApi(Build.VERSION_CODES.P)
private suspend fun createCaptureSession(): CameraCaptureSession = suspendCoroutine { cont ->
val outputs = mutableListOf<OutputConfiguration>()
// プレビューSurface
outputs.add(OutputConfiguration(mSurface))
// MediaCodec用の入力Surfaceを追加
outputs.add(OutputConfiguration(EncodeManager.getSurface()))
val sessionConfiguration = SessionConfiguration(
SessionConfiguration.SESSION_REGULAR,
outputs, mExecutor, ...)
mCameraDevice.createCaptureSession(sessionConfiguration)
}
次に CaptureRequest を発行してプレビューを開始し、Surface 出力を受信し、同時にエンコードを開始します。以下を参照してください:
// プレビューのSurfaceとImage生成のSurfaceを追加
mCaptureRequestBuild = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
val sur = EncodeManager.getSurface()
mCaptureRequestBuild.addTarget(sur)
mCaptureRequestBuild.addTarget(mSurface)
// 各種パラメータを設定
mCaptureRequestBuild.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, 1) // ビデオ安定機能が有効かどうか
// CaptureRequestを送信
mCameraCaptureSession.setRepeatingRequest(
mCaptureRequestBuild.build(),
null,
mCameraHandler
)
// エンコードを開始
EncodeManager.startEncode()