parent
ef362bfa28
commit
cf3a8b2f16
@ -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…
Reference in new issue