PS:やりたいことをやるために、毎日少なくとも少しでも行動し、多くを求めずにゆっくりと変化させていきましょう。
音声および動画の関連知識を理解したら、以下の 2 つの記事を読むことができます:
この記事の主な内容は、Android のネイティブなハードウェアエンコード / デコードフレームワークである MediaCodec と MediaMuxer を使用して、mp4 ビデオファイルの録画を実現することです。ビデオデータソースは Camera2 から提供され、重点はエンコードとマルチプレクサのプロセスであり、mp4 の録画ではありません。単にビデオの録画を行う場合は、より便利な MediaRecorder を選択できます。MediaCodec の使用法については、後続の記事で詳しく説明します。この記事の主な内容は次のとおりです:
- Camera2 の使用
- MediaCodec の入力方法
- Camera2 データの MediaCodec エンコード
- 録画の手順
- 録画の効果
Camera2 の使用#
Camera2 は Android 5.0 から導入された新しいカメラ API であり、最新のバージョンは CameraX です。CameraX は Camera2 に基づいており、より使いやすい API を提供しています。関連する API については、以下の図を参照してください。これは Camera2 の使用例です。
MediaCodec の入力方法#
MediaCodec を使用してエンコード操作を行うためには、カメラのデータをエンコーダー MediaCodec に入力する必要があります。データを MediaCodec に書き込む方法は 2 つあります。
- Surface:Surface をエンコーダー MediaCodec の入力として使用します。つまり、MediaCodec が作成した Surface を入力として使用します。この Surface は、カメラが有効なデータをレンダリングするために使用され、MediaCodec はエンコード後のデータを直接出力できます。
- InputBuffer:入力バッファをエンコーダー MediaCodec の入力として使用します。ここで埋めるデータは元のフレームデータです。Camera2 の場合、ImageReader を使用してフレームデータを取得できます。取得した Image には、幅、高さ、フォーマット、タイムスタンプ、YUV データの成分などの情報が含まれており、より高い制御が可能です。
Camera2 データの MediaCodec エンコード#
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 not start")
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)
// 入力として使用するSurfaceをMediaCodecの入力として作成する。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()