MediaCodecでbitmap列から動画作成

動くコードと参考文献を載せておきます。kotlin＆Coroutineに書き直しました。随所お好みにカスタマイズしてください。
jcodecの100倍くらいエンコード早かったです。さすがAndroid純正フレームワーク。  

2021/10/08: 
エラー箇所をコメントアウトして修正しました。(scaled.recycle())

2021/10/10:
読み込ませるframeのwidthとheightが、偶数値でないとエラーになるバグを修正しました。

```使い方.kt
val frames: List<Bitmap> = ....
val videoFile: File? = MediaCodecEncoder().encode(this, frames)// suspend function

```


```MediaCodecEncoder.kt

// MediaCodecフレームワークを使ったエンコード
class MediaCodecEncoder {

    private val MIME_TYPE = "video/avc"
    private val FRAME_RATE: Int = 30
    private val BIT_RATE: Int = 16000000
    private val I_FRAME_INTERVAL: Int = 1

    // フレーム列を動画にエンコードする
    // フレーム列を動画にエンコードする
    override suspend fun encode(context: Context, frames: List<Bitmap>): File? = withContext(Dispatchers.Default) {
        if (frames.isEmpty()) { return@withContext null }

        // 2021/10/10: 寸法が偶数値になるように修正
        val refEvenWidth = (frames[0].width / 2) * 2
        val refEvenHeight = (frames[0].height / 2) * 2

        if (refEvenWidth == 0 || refEvenHeight == 0) { return@withContext null }

        val outputFile = File(context.cacheDir, "${UUID.randomUUID()}.mp4")

        val mediaCodec = getInitialCodec(refEvenWidth, refEvenHeight) ?: return@withContext null
        val mediaMuxer = getInitialMuxer(outputFile) ?: return@withContext null
        mediaCodec.start()

        val TIMEOUT_USEC_IN = 500000L// 入力バッファの使用(?)制限時間
        val TIMEOUT_USEC_OUT = 500000L// 出力バッファの使用(?)制限時間
        var trackIndex = -1// muxerのトラック
        var count = 0// debug code
        for (i in frames.indices) {
            // debug code
            count += 1
            Log.d("debug_so", "MediaCodecライブラリ エンコード進捗 $count 枚 / ${frames.size}枚(${frames.size / FRAME_RATE}秒分)")

            // エンコード用のフレームバイトデータを作成
            val frame = frames[i]

　　　　　　　　　　　　　　　　　　　　　　　　// 2021/10/10: 寸法が偶数値になるように修正
            val evenFrameWidth = (frames[i].width / 2) * 2
            val evenFrameHeight = (frames[0].height / 2) * 2

            val byteFrame = getNV21(evenFrameWidth, evenFrameHeight, frame)

            // 入力バッファを取得
            val inputBufferId = mediaCodec.dequeueInputBuffer(TIMEOUT_USEC_IN)
            val ptsUsec = computePresentationTime(i.toLong(), FRAME_RATE)

            if (inputBufferId < 0) { return@withContext null }

            val inputBuffer = mediaCodec.getInputBuffer(inputBufferId) ?: return@withContext null
            inputBuffer.clear();
            inputBuffer.put(byteFrame)

            // 入力バッファにエンコードしたいバイトデータを流し込む
            mediaCodec.queueInputBuffer(inputBufferId, 0, byteFrame.size, ptsUsec, 0);

            // 出力バッファの取得
            var bufferInfo = MediaCodec.BufferInfo()
            var outputBufferId = mediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC_OUT)
            if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){// 新しい入力フォーマットが設定された時(一番最初は必ず読み出される)
                // muxerの初期化
                trackIndex = mediaMuxer.addTrack(mediaCodec.outputFormat);
                mediaMuxer.start()
                delay(500)// debug_so: なぜか時間をおくと、MediaCodec.BufferInfo().size がnon-0になってくれる。
                // 出力バッファの再設定
                bufferInfo = MediaCodec.BufferInfo()
                outputBufferId = mediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC_OUT)
            }

            if (outputBufferId < 0) {// 不正な出力バッファ
                Log.d("debug_so","invalid outputBufferId: $outputBufferId")
                return@withContext null
            }

            if (bufferInfo.size <= 0) {// バッファがない
                return@withContext null
            }

            // 1フレームのoutputBufferをエンコードして動画に加える
            val encodedData = mediaCodec.getOutputBuffer(outputBufferId) ?: return@withContext null
            encodedData.position(bufferInfo.offset)
            encodedData.limit(bufferInfo.offset + bufferInfo.size)
            mediaMuxer.writeSampleData(trackIndex, encodedData, bufferInfo)
            mediaCodec.releaseOutputBuffer(outputBufferId, false)
        }

        mediaCodec.stop()
        mediaCodec.release()
        mediaMuxer.stop()
        mediaMuxer.release()

        return@withContext outputFile
    }

    private fun getInitialCodec(width: Int, height: Int): MediaCodec? {

        // mediaCodec作成
        val codecInfo = selectCodec(MIME_TYPE) ?: return null
        val colorFormat = selectColorFormat(codecInfo, MIME_TYPE) ?: return null

        val mc: MediaCodec
        try {
            mc = MediaCodec.createByCodecName(codecInfo.name)
        } catch (e: Exception) {
            return null
        }

        // 基礎設定
        val mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, width, height)
        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE)
        mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE)
        mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat)
        mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, I_FRAME_INTERVAL)
        mc.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
        return mc
    }

    private fun getInitialMuxer(outputFile: File) : MediaMuxer? {
        return try {  MediaMuxer(outputFile.path, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4) }
        catch (e: Exception) { null }
    }

    private fun computePresentationTime(frameIndex: Long, framerate: Int): Long {
        return 132 + frameIndex * 1000000 / framerate
    }

    private fun selectCodec(mimeType: String): MediaCodecInfo? {
        val codecInfos = MediaCodecList(MediaCodecList.REGULAR_CODECS).codecInfos
        for (codecInfo in codecInfos) {
            if (!codecInfo.isEncoder) { continue }
            val types = codecInfo.supportedTypes
            for (j in types.indices) {
                if (types[j].equals(mimeType, ignoreCase = true)) {
                    return codecInfo
                }
            }
        }
        return null
    }

    private fun selectColorFormat(codecInfo: MediaCodecInfo, mimeType: String): Int? {
        val capabilities = codecInfo.getCapabilitiesForType(mimeType)
        for (colorFormat in capabilities.colorFormats) {
            if (isRecognizedFormat(colorFormat)) {
                return colorFormat
            }
        }
        return null
    }

    private fun isRecognizedFormat(colorFormat: Int): Boolean {
        return when (colorFormat) {
            CodecCapabilities.COLOR_FormatYUV420Flexible -> true
            else -> false
        }
    }

    private fun getNV21(inputWidth: Int, inputHeight: Int, scaled: Bitmap): ByteArray {
        val argb = IntArray(inputWidth * inputHeight)
        scaled.getPixels(argb, 0, inputWidth, 0, 0, inputWidth, inputHeight)
        val yuv = encodeYUV420SP(ByteArray(inputWidth * inputHeight * 3 / 2), argb, inputWidth, inputHeight)
        // scaled.recycle() => 2021/10/08: エラーが出るのでコメントアウトしました。
        return yuv
    }

    private fun encodeYUV420SP(yuv420sp: ByteArray, argb: IntArray, width: Int, height: Int): ByteArray {
        val frameSize = width * height
        var yIndex = 0
        var uvIndex = frameSize
        var a: Int
        var R: Int
        var G: Int
        var B: Int
        var Y: Int
        var U: Int
        var V: Int
        var index = 0
        for (j in 0 until height) {
            for (i in 0 until width) {
                a = argb[index] and -0x1000000 shr 24 // a is not used obviously
                R = argb[index] and 0xff0000 shr 16
                G = argb[index] and 0xff00 shr 8
                B = argb[index] and 0xff shr 0
                Y = (66 * R + 129 * G + 25 * B + 128 shr 8) + 16
                U = (-38 * R - 74 * G + 112 * B + 128 shr 8) + 128
                V = (112 * R - 94 * G - 18 * B + 128 shr 8) + 128
                yuv420sp[yIndex++] = (if (Y < 0) 0 else if (Y > 255) 255 else Y).toByte()
                if (j % 2 == 0 && index % 2 == 0) {
                    yuv420sp[uvIndex++] = (if (U < 0) 0 else if (U > 255) 255 else U).toByte()
                    yuv420sp[uvIndex++] = (if (V < 0) 0 else if (V > 255) 255 else V).toByte()
                }
                index++
            }
        }
        return yuv420sp
    }
}
```

<h1>参考</h1>
https://newbedev.com/how-to-encode-bitmaps-into-a-video-using-mediacodec

https://medium.com/nttlabs/android-mediacodec-evaluation-23dee89fe1dd
  
https://developer.android.com/reference/android/media/MediaCodec