diff --git a/app/src/main/java/net/pokeranalytics/android/model/handhistory/MMediaMuxer.kt b/app/src/main/java/net/pokeranalytics/android/model/handhistory/MMediaMuxer.kt new file mode 100644 index 00000000..f7de2365 --- /dev/null +++ b/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? = null + private var bitFirst: MutableList? = null + private var bitLast: MutableList? = 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) + } + } +} \ No newline at end of file