動くコードと参考文献を載せておきます。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
}
}
参考
https://newbedev.com/how-to-encode-bitmaps-into-a-video-using-mediacodec