LoginSignup
3
2

More than 1 year has passed since last update.

MediaCodecでbitmap列から動画作成

Last updated at Posted at 2021-10-07

動くコードと参考文献を載せておきます。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
    }
}

参考

3
2
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
3
2