MediaCodecで動画をExtract, Decode, Encode, Muxしてみる

## 概要
Android SDK に含まれる Media Codec を用いて動画の Extract(分離), Decode, Encode, Mux(結合) し再度動画ファイルとして出力してみる

## 処理の全体像

動画を入力したら Extract, Decode, Encode, Mux の処理を無限ループで回していく。各処理を無限ループで回しつつ一部のデータを切り出して少しずつ並列に処理することで効率を上げる作りになっている

```kotlin
    fun doExtractDecodeEncodeMux() {
        videoDecoder.start()
        videoEncoder.start()
        audioDecoder.start()
        audioEncoder.start()

        while (!isMuxEnd) {
            if (!isVideoExtractEnd) { isVideoExtractEnd = extract(videoExtractor, videoDecoder) }
            if (!isAudioExtractEnd) { isAudioExtractEnd = extract(audioExtractor, audioDecoder) }
            if (!isVideoDecodeEnd) { isVideoDecodeEnd = decode(videoDecoder, videoEncoder) }
            if (!isAudioDecodeEnd) { isAudioDecodeEnd = decode(audioDecoder, audioEncoder) }
            if (!isVideoEncodeEnd) {
                isVideoEncodeEnd = encode(videoEncoder, {
                    outputVideoFormat = it
                    outputVideoTrackIdx = muxer.addTrack(it)
                }, { outputBuffer, outputBufferInfo ->
                    muxer.writeSampleData(outputVideoTrackIdx, outputBuffer, outputBufferInfo)
                })
            }
            if (!isAudioEncodeEnd) {
                isAudioEncodeEnd = encode(audioEncoder, {
                    outputAudioFormat = it
                    outputAudioTrackIdx = muxer.addTrack(it)
                }, { outputBuffer, outputBufferInfo ->
                    muxer.writeSampleData(outputAudioTrackIdx, outputBuffer, outputBufferInfo)
                })
            }
        }
        ...
    }
```

## Extract
Extract は `MediaExtractor`クラスで行う。入力元動画のファイルパスを設定しておく。 Video用とAudio用をそれぞれ用意する必要がある

```kotlin
    private val videoExtractor = MediaExtractor()
    private val audioExtractor = MediaExtractor()

    init {
        videoExtractor.setDataSource(inputFilePath)
        videoTrackIdx = getVideoTrackIdx(videoExtractor)
        if (videoTrackIdx == -1) {
            Logger.e("video not found")
            throw RuntimeException("video not found")
        }
        videoExtractor.selectTrack(videoTrackIdx)
        ...
        Logger.e("inputVideoFormat: $inputVideoFormat")

        audioExtractor.setDataSource(inputFilePath)
        audioTrackIdx = getAudioTrackIdx(audioExtractor)
        if (audioTrackIdx == -1) {
            Logger.e("audio not found")
            throw RuntimeException("audio not found")
        }
        audioExtractor.selectTrack(audioTrackIdx)
        ...
        Logger.e("inputAudioFormat: $inputAudioFormat")

        ...
    }

    private fun getAudioTrackIdx(extractor: MediaExtractor): Int {
        for (idx in 0 until extractor.trackCount) {
            val format = extractor.getTrackFormat(idx)
            val mime = format.getString(MediaFormat.KEY_MIME)
            if (mime?.startsWith("audio") == true) {
                return idx
            }
        }
        return -1
    }

    private fun getVideoTrackIdx(extractor: MediaExtractor): Int {
        for (idx in 0 until extractor.trackCount) {
            val format = extractor.getTrackFormat(idx)
            val mime = format.getString(MediaFormat.KEY_MIME)
            if (mime?.startsWith("video") == true) {
                return idx
            }
        }
        return -1
    }
```

Extract処理では `MediaExtractor` の `readSampleData()` でデータを読み込み Decoder の inputBuffer に送る。最後まで読み込んだら送信時の flag に最後のデータであることを示す `MediaCodec.BUFFER_FLAG_END_OF_STREAM` をセットして送る。同時に isExtractEnd フラグをセットし、無限ループでこの処理が呼ばれないようにする

```kotlin
    private fun extract(extractor: MediaExtractor, decoder: MediaCodec): Boolean {
        var isExtractEnd = false
        val inputBufferIdx = decoder.dequeueInputBuffer(CODEC_TIMEOUT_IN_US)
        if (inputBufferIdx >= 0) {
            val inputBuffer = decoder.getInputBuffer(inputBufferIdx) as ByteBuffer
            val sampleSize = extractor.readSampleData(inputBuffer, 0)
            if (sampleSize > 0) {
                decoder.queueInputBuffer(inputBufferIdx, 0, sampleSize, extractor.sampleTime, extractor.sampleFlags)
            } else {
                Logger.e("isExtractEnd = true")
                isExtractEnd = true
                decoder.queueInputBuffer(inputBufferIdx, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
            }

            if (!isExtractEnd) {
                extractor.advance()
            }
        }
        return isExtractEnd
    }
```

## Decode
Decodeは `MediaCodec` クラスで行う。 `MediaCodec.createDecoderByType()` で生成し、 `configure()` で各種パラメータをセットする。今回は入力と出力で同じ動画なので入力元動画のパラメータをそのままセットしている

```kotlin
    private val videoDecoder: MediaCodec
    private val audioDecoder: MediaCodec

    init {
        ...
        inputVideoFormat = videoExtractor.getTrackFormat(videoTrackIdx)
        ...
        videoDecoder = MediaCodec.createDecoderByType(inputVideoMime)
        videoDecoder.configure(inputVideoFormat, null, null, 0)

        ...
        inputAudioFormat = audioExtractor.getTrackFormat(audioTrackIdx)
        ...
        audioDecoder = MediaCodec.createDecoderByType(inputAudioMime)
        audioDecoder.configure(inputAudioFormat, null, null, 0)
        ...
    }
```

Decode処理では、Decoder の outputBuffer からデコードされたデータを取り出し今度は Encoder の inputBuffer へ送る

今回は入力動画と出力動画が同じものなのでそのまま decode 結果を encode して送ってしまうが、このタイミングで生のデータを加工し例えば動画にフィルターをかけたり、音声データのサンプリングレートを変換したりなどの面白いことが行える

最後まで処理したら Extract 時と同様に送信時の flag に最後のデータであることを示す `MediaCodec.BUFFER_FLAG_END_OF_STREAM` をセットして送る。同時に isDecodeEnd フラグをセットし、無限ループでこの処理が呼ばれないようにする

```kotlin
    private fun decode(decoder: MediaCodec, encoder: MediaCodec): Boolean {
        var isDecodeEnd = false
        val decoderOutputBufferInfo = MediaCodec.BufferInfo()
        val decoderOutputBufferIdx = decoder.dequeueOutputBuffer(decoderOutputBufferInfo, CODEC_TIMEOUT_IN_US)

        if (decoderOutputBufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
            Logger.e("isDecodeEnd = true")
            isDecodeEnd = true
        }
        if (decoderOutputBufferIdx >= 0) {
            val encoderInputBufferIdx = encoder.dequeueInputBuffer(CODEC_TIMEOUT_IN_US)
            if (encoderInputBufferIdx >= 0) {
                val decoderOutputBuffer = (decoder.getOutputBuffer(decoderOutputBufferIdx) as ByteBuffer).duplicate()
                decoderOutputBuffer.position(decoderOutputBufferInfo.offset)
                decoderOutputBuffer.limit(decoderOutputBufferInfo.offset + decoderOutputBufferInfo.size)

                val encoderInputBuffer = encoder.getInputBuffer(encoderInputBufferIdx)
                encoderInputBuffer?.position(0)
                encoderInputBuffer?.put(decoderOutputBuffer)

                val flags = if (isDecodeEnd) MediaCodec.BUFFER_FLAG_END_OF_STREAM else decoderOutputBufferInfo.flags

                encoder.queueInputBuffer(
                    encoderInputBufferIdx, 0,
                    decoderOutputBufferInfo.size,
                    decoderOutputBufferInfo.presentationTimeUs, flags
                )
                decoder.releaseOutputBuffer(decoderOutputBufferIdx, false)
            }
        }
        return isDecodeEnd
    }
```

## Encode
Encode も `MediaCodec` クラスで行う。 `configure()` で各種パラメータをセットする。可能な限り入力元動画から取得したパラメータをセットしたが取得できないものがあるのでそこは別途指定していく。 `configure()` の第４引数は Decode の時とは違い `MediaCodec.CONFIGURE_FLAG_ENCODE` をセットする

```kotlin
    private val videoEncoder: MediaCodec
    private val audioEncoder: MediaCodec

    init {
        ...
        val inputVideoMime = inputVideoFormat.getString(MediaFormat.KEY_MIME)
        val width = inputVideoFormat.getInteger(MediaFormat.KEY_WIDTH)
        val height = inputVideoFormat.getInteger(MediaFormat.KEY_HEIGHT)
        encodeVideoFormat = MediaFormat.createVideoFormat(inputVideoMime, width, height).also {
            it.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible)
            it.setInteger(MediaFormat.KEY_BIT_RATE, 2000000)
            it.setInteger(MediaFormat.KEY_FRAME_RATE, inputVideoFormat.getInteger(MediaFormat.KEY_FRAME_RATE))
            it.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 10)
        }
        videoEncoder = MediaCodec.createEncoderByType(inputVideoMime)
        videoEncoder.configure(encodeVideoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)

        val inputAudioMime = inputAudioFormat.getString(MediaFormat.KEY_MIME)
        val sampleRate = inputAudioFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE)
        val channelCount = inputAudioFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
        encodeAudioFormat = MediaFormat.createAudioFormat(inputAudioMime, sampleRate, channelCount).also {
            it.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC)
            it.setInteger(MediaFormat.KEY_BIT_RATE, inputAudioFormat.getInteger(MediaFormat.KEY_BIT_RATE))
        }
        audioEncoder = MediaCodec.createEncoderByType(inputAudioMime)
        audioEncoder.configure(encodeAudioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
        ...
    }
```

Encode処理では、Encoder の outputBuffer からエンコードされたデータを取り出し今度は Muxer へ送る

```kotlin
    private fun encode(
        encoder: MediaCodec,
        onOutputFormatChaned: (outputFormat: MediaFormat) -> Unit,
        writeEncodedData: (outputBuffer: ByteBuffer, outputBufferInfo: MediaCodec.BufferInfo) -> Unit
    ): Boolean {
        var isEncodeEnd = false
        val encoderOutputBufferInfo = MediaCodec.BufferInfo()
        val encoderOutputBufferIdx = encoder.dequeueOutputBuffer(encoderOutputBufferInfo, CODEC_TIMEOUT_IN_US)

        if (encoderOutputBufferIdx == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
            Logger.e("output format changed: ${encoder.outputFormat}")
            onOutputFormatChaned(encoder.outputFormat)
            if (outputVideoFormat != null && outputAudioFormat != null) {
                Logger.e("muxer start")
                muxer.start()
            }
            return isEncodeEnd
        }

        if (encoderOutputBufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) {
            encoder.releaseOutputBuffer(encoderOutputBufferIdx, false)
            return isEncodeEnd
        }

        outputVideoFormat?: return isEncodeEnd
        outputAudioFormat?: return isEncodeEnd

        if (encoderOutputBufferIdx >= 0) {
            val encoderOutputBuffer = encoder.getOutputBuffer(encoderOutputBufferIdx)
            if (encoderOutputBufferInfo.size >= 0) {
                encoderOutputBuffer?.let {
                    writeEncodedData(it, encoderOutputBufferInfo)
                    encoder.releaseOutputBuffer(encoderOutputBufferIdx, false)
                }
            }
            Logger.e("presentationTimeUs: ${encoderOutputBufferInfo.presentationTimeUs}, encoder: ${encoder.codecInfo.name}")
        }

        if (encoderOutputBufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
            Logger.e("isEncodeEnd = true")
            isEncodeEnd = true
        }
        return isEncodeEnd
    }
```

ここで注意しないといけないのが `MediaCodec.INFO_OUTPUT_FORMAT_CHANGED` を受け取り動画と音声両方の outputFormat が確定してからじゃないと Muxer にデータが書き込めないということである。 そのため両方が確定するまでは return している。outputFormat を受け取ったら Muxer の `addTrack()` を実行し、動画と音声両方を `addTrack()` 完了したら `muxer.start()` でMux処理を始めている

## Mux
Muxは `MediaMuxer` クラスで行う。`setOrientationHint()` を設定しないと動画が反転することがあるようである。出力ファイルのパスを設定しておく

```kotlin
    private val muxer: MediaMuxer

    init {
        ...
        val videoMetaData = MediaMetadataRetriever()
        videoMetaData.setDataSource(inputFilePath)
        val degreeString = videoMetaData.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)
        val videoDegree = degreeString?.toInt() ?: 0
        muxer = MediaMuxer(outputFilePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
        muxer.setOrientationHint(videoDegree)
    }
```

`writeSampleData()` を実行することで最終的に動画ファイルとして出力される 

```kotlin
        while (!isMuxEnd) {
            ...
            if (!isVideoEncodeEnd) {
                isVideoEncodeEnd = encode(videoEncoder, {
                    outputVideoFormat = it
                    outputVideoTrackIdx = muxer.addTrack(it)
                }, { outputBuffer, outputBufferInfo ->
                    muxer.writeSampleData(outputVideoTrackIdx, outputBuffer, outputBufferInfo)
                })
            }
            if (!isAudioEncodeEnd) {
                isAudioEncodeEnd = encode(audioEncoder, {
                    outputAudioFormat = it
                    outputAudioTrackIdx = muxer.addTrack(it)
                }, { outputBuffer, outputBufferInfo ->
                    muxer.writeSampleData(outputAudioTrackIdx, outputBuffer, outputBufferInfo)
                })
            }
        }
```

## まとめ
MediaCodecを使って動画をExtract, Decode, Encode, Muxし元動画と同じ動画を出力することができた :ok_woman:
今回は入力動画と出力動画が同じだったが、今度はデコード結果の生の動画データや音声データを加工し動画の一部を切り出したり音のサンプリングレートを変換したりなどの面白いことに挑戦してみたい 💪

[プロジェクトファイル一式](https://github.com/tomo1139/MediaCodecExtractDecodeEncodeMux)

## 参考
[Android Developer (MediaCodec)](https://developer.android.com/reference/android/media/MediaCodec)
[上の和訳](https://qiita.com/imatomi/items/bd9d49cfb1f73383ca12)
[Googleのサンプルコード (ExtractDecodeEditEncodeMuxTest.java)](https://android.googlesource.com/platform/cts/+/jb-mr2-release/tests/tests/media/src/android/media/cts/ExtractDecodeEditEncodeMuxTest.java)