Adds MMediaMuxer to assemble images into video

hh
Laurent 6 years ago
parent ef362bfa28
commit cf3a8b2f16
  1. 395
      app/src/main/java/net/pokeranalytics/android/model/handhistory/MMediaMuxer.kt

@ -0,0 +1,395 @@
package net.pokeranalytics.android.model.handhistory
import android.app.Activity
import android.app.ProgressDialog
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.media.*
import android.media.MediaCodecInfo.CodecCapabilities
import android.os.Environment
import android.os.Handler
import android.util.Log
import java.io.File
import java.io.IOException
import java.text.DateFormat
import java.util.*
class MMediaMuxer {
private var mediaCodec: MediaCodec? = null
private var mediaMuxer: MediaMuxer? = null
private var mRunning = false
private var generateIndex = 0
private var mTrackIndex = 0
private var MAX_FRAME_VIDEO = 0
private var bitList: MutableList<ByteArray>? = null
private var bitFirst: MutableList<ByteArray>? = null
private var bitLast: MutableList<ByteArray>? = null
private var current_index_frame = 0
private var outputPath: String? = null
private var _activity: Activity? = null
private var pd: ProgressDialog? = null
private var _title: String? = null
private var _mess: String? = null
fun Init(
activity: Activity?,
width: Int,
height: Int,
title: String?,
mess: String?
) {
_title = title
_mess = mess
_activity = activity
_width = width
_height = height
Logd("MMediaMuxer Init")
ShowProgressBar()
}
private val aHandler = Handler()
fun AddFrame(byteFrame: ByteArray) {
CheckDataListState()
Thread(Runnable {
Logd("Android get Frame")
val bit = BitmapFactory.decodeByteArray(byteFrame, 0, byteFrame.size)
Logd("Android convert Bitmap")
val byteConvertFrame =
getNV21(bit.width, bit.height, bit)
Logd("Android convert getNV21")
bitList!!.add(byteConvertFrame)
}).start()
}
fun AddFrame(byteFrame: ByteArray, count: Int, isLast: Boolean) {
var byteFrame = byteFrame
CheckDataListState()
Logd("Android get Frames = $count")
val bit = BitmapFactory.decodeByteArray(byteFrame, 0, byteFrame.size)
Logd("Android convert Bitmap")
byteFrame = getNV21(bit.width, bit.height, bit)
Logd("Android convert getNV21")
for (i in 0 until count) {
if (isLast) {
bitLast!!.add(byteFrame)
} else {
bitFirst!!.add(byteFrame)
}
}
}
fun CreateVideo() {
current_index_frame = 0
Logd("Prepare Frames Data")
bitFirst!!.addAll(bitList!!)
bitFirst!!.addAll(bitLast!!)
MAX_FRAME_VIDEO = bitFirst!!.size
Logd("CreateVideo")
mRunning = true
bufferEncoder()
}
fun GetStateEncoder(): Boolean {
return mRunning
}
fun GetPath(): String? {
return outputPath
}
fun onBackPressed() {
mRunning = false
}
fun ShowProgressBar() {
_activity!!.runOnUiThread {
pd = ProgressDialog(_activity)
pd!!.setTitle(_title)
pd!!.setCancelable(false)
pd!!.setMessage(_mess)
pd!!.setCanceledOnTouchOutside(false)
pd!!.show()
}
}
fun HideProgressBar() {
Thread(Runnable { _activity!!.runOnUiThread { pd!!.dismiss() } }).start()
}
private fun bufferEncoder() {
val runnable = Runnable {
try {
Logd("PrepareEncoder start")
PrepareEncoder()
Logd("PrepareEncoder end")
} catch (e: IOException) {
Loge(e.message)
}
try {
while (mRunning) {
Encode()
}
} finally {
Logd("release")
Release()
HideProgressBar()
bitFirst = null
bitLast = null
}
}
val thread = Thread(runnable)
thread.start()
}
fun ClearTask() {
bitList = null
bitFirst = null
bitLast = null
}
@Throws(IOException::class)
private fun PrepareEncoder() {
val codecInfo =
selectCodec(MIME_TYPE)
if (codecInfo == null) {
Loge("Unable to find an appropriate codec for $MIME_TYPE")
}
Logd("found codec: " + codecInfo!!.name)
val colorFormat: Int
colorFormat = try {
selectColorFormat(codecInfo, MIME_TYPE)
} catch (e: Exception) {
CodecCapabilities.COLOR_FormatYUV420SemiPlanar
}
mediaCodec = MediaCodec.createByCodecName(codecInfo.name)
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,
INFLAME_INTERVAL
)
mediaCodec?.let {
it.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
it.start()
}
try {
val currentDateTimeString =
DateFormat.getDateTimeInstance().format(Date())
outputPath = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES),
"pixel$currentDateTimeString.mp4"
).toString()
mediaMuxer = MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
} catch (ioe: IOException) {
Loge("MediaMuxer creation failed")
}
}
private fun Encode() {
while (true) {
if (!mRunning) {
break
}
Logd("Encode start")
val TIMEOUT_USEC: Long = 5000
val inputBufIndex = mediaCodec!!.dequeueInputBuffer(TIMEOUT_USEC)
val ptsUsec =
computePresentationTime(generateIndex.toLong(), FRAME_RATE)
if (inputBufIndex >= 0) {
val input = bitFirst!![current_index_frame]
val inputBuffer = mediaCodec!!.getInputBuffer(inputBufIndex)
inputBuffer.clear()
inputBuffer.put(input)
mediaCodec!!.queueInputBuffer(inputBufIndex, 0, input.size, ptsUsec, 0)
generateIndex++
}
val mBufferInfo = MediaCodec.BufferInfo()
val encoderStatus = mediaCodec!!.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC)
if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) { // no output available yet
Loge("No output from encoder available")
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { // not expected for an encoder
val newFormat = mediaCodec!!.outputFormat
mTrackIndex = mediaMuxer!!.addTrack(newFormat)
mediaMuxer!!.start()
} else if (encoderStatus < 0) {
Loge("unexpected result from encoder.dequeueOutputBuffer: $encoderStatus")
} else if (mBufferInfo.size != 0) {
val encodedData = mediaCodec!!.getOutputBuffer(encoderStatus)
if (encodedData == null) {
Loge("encoderOutputBuffer $encoderStatus was null")
} else {
encodedData.position(mBufferInfo.offset)
encodedData.limit(mBufferInfo.offset + mBufferInfo.size)
mediaMuxer!!.writeSampleData(mTrackIndex, encodedData, mBufferInfo)
mediaCodec!!.releaseOutputBuffer(encoderStatus, false)
}
}
current_index_frame++
if (current_index_frame > MAX_FRAME_VIDEO - 1) {
Log.d(TAG, "mRunning = false;")
mRunning = false
}
Logd("Encode end")
}
}
private fun Release() {
if (mediaCodec != null) {
mediaCodec!!.stop()
mediaCodec!!.release()
mediaCodec = null
Logd("RELEASE CODEC")
}
if (mediaMuxer != null) {
mediaMuxer!!.stop()
mediaMuxer!!.release()
mediaMuxer = null
Logd("RELEASE MUXER")
}
}
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 = ByteArray(inputWidth * inputHeight * 3 / 2)
encodeYUV420SP(yuv, argb, inputWidth, inputHeight)
scaled.recycle()
return yuv
}
private fun encodeYUV420SP(
yuv420sp: ByteArray,
argb: IntArray,
width: Int,
height: Int
) {
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++
}
}
}
private fun CheckDataListState() {
if (bitList == null) {
bitList = ArrayList()
}
if (bitFirst == null) {
bitFirst = ArrayList()
}
if (bitLast == null) {
bitLast = ArrayList()
}
}
private fun computePresentationTime(frameIndex: Long, framerate: Int): Long {
return 132 + frameIndex * 1000000 / framerate
}
companion object {
private const val MIME_TYPE = "video/avc" // H.264 Advanced Video Coding
private var _width = 512
private var _height = 512
private const val BIT_RATE = 800000
private const val INFLAME_INTERVAL = 1
private const val FRAME_RATE = 10
private const val DEBUG = false
private const val TAG = "CODEC"
/**
* Returns the first codec capable of encoding the specified MIME type, or
* null if no match was found.
*/
private fun selectCodec(mimeType: String): MediaCodecInfo? {
val numCodecs = MediaCodecList.getCodecCount()
for (i in 0 until numCodecs) {
val codecInfo = MediaCodecList.getCodecInfoAt(i)
if (!codecInfo.isEncoder) {
continue
}
val types = codecInfo.supportedTypes
for (j in types.indices) {
if (types[j].equals(mimeType, ignoreCase = true)) {
return codecInfo
}
}
}
return null
}
/**
* Returns a color format that is supported by the codec and by this test
* code. If no match is found, this throws a test failure -- the set of
* formats known to the test should be expanded for new platforms.
*/
private fun selectColorFormat(
codecInfo: MediaCodecInfo,
mimeType: String
): Int {
val capabilities = codecInfo
.getCapabilitiesForType(mimeType)
for (i in capabilities.colorFormats.indices) {
val colorFormat = capabilities.colorFormats[i]
if (isRecognizedFormat(colorFormat)) {
return colorFormat
}
}
return 0 // not reached
}
/**
* Returns true if this is a color format that this test code understands
* (i.e. we know how to read and generate frames in this format).
*/
private fun isRecognizedFormat(colorFormat: Int): Boolean {
return when (colorFormat) {
CodecCapabilities.COLOR_FormatYUV420Planar, CodecCapabilities.COLOR_FormatYUV420PackedPlanar, CodecCapabilities.COLOR_FormatYUV420SemiPlanar, CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar, CodecCapabilities.COLOR_TI_FormatYUV420PackedSemiPlanar -> true
else -> false
}
}
private fun Logd(Mess: String) {
if (DEBUG) {
Log.d(TAG, Mess)
}
}
private fun Loge(Mess: String?) {
Log.e(TAG, Mess)
}
}
}
Loading…
Cancel
Save