parent
f79f2f60cd
commit
6c36863185
@ -1,433 +0,0 @@ |
||||
package net.pokeranalytics.android.util.video |
||||
|
||||
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.util.Log |
||||
import net.pokeranalytics.android.util.extensions.dateTimeFileFormatted |
||||
import timber.log.Timber |
||||
import java.io.File |
||||
import java.io.IOException |
||||
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 currentIndexFrame = 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 |
||||
private var completion: ((String) -> (Unit))? = 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() |
||||
} |
||||
|
||||
fun addFrame(byteFrame: ByteArray) { |
||||
checkDataListState() |
||||
Thread(Runnable { |
||||
Logd("Android get Frame") |
||||
val bitmap = BitmapFactory.decodeByteArray(byteFrame, 0, byteFrame.size) |
||||
Logd("Android convert Bitmap") |
||||
val byteConvertFrame = |
||||
getNV21(bitmap.width, bitmap.height, bitmap) |
||||
Logd("Android convert getNV21") |
||||
bitList!!.add(byteConvertFrame) |
||||
}).start() |
||||
} |
||||
|
||||
fun addFrame(byteFrame: ByteArray, count: Int, isLast: Boolean) { |
||||
var bf = byteFrame |
||||
checkDataListState() |
||||
Logd("Android get Frames = $count") |
||||
val bitmap = BitmapFactory.decodeByteArray(bf, 0, bf.size) |
||||
Logd("Android convert Bitmap") |
||||
bf = getNV21(bitmap.width, bitmap.height, bitmap) |
||||
Logd("Android convert getNV21") |
||||
for (i in 0 until count) { |
||||
if (isLast) { |
||||
bitLast!!.add(bf) |
||||
} else { |
||||
bitFirst!!.add(bf) |
||||
} |
||||
} |
||||
} |
||||
|
||||
fun createVideo(handler: (String) -> (Unit)) { |
||||
this.completion = handler |
||||
currentIndexFrame = 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") |
||||
// } |
||||
|
||||
val codec = MediaCodec.createEncoderByType(MIME_TYPE) |
||||
|
||||
val colorFormat: Int |
||||
colorFormat = try { |
||||
selectColorFormat(codec.codecInfo, MIME_TYPE) |
||||
} catch (e: Exception) { |
||||
Timber.d(">>> color format exception: $e") |
||||
CodecCapabilities.COLOR_FormatYUV420SemiPlanar |
||||
} |
||||
|
||||
Logd("Selected codec: " + codec.name) |
||||
Logd("Selected color format: $colorFormat") |
||||
|
||||
// mediaCodec = MediaCodec.createByCodecName(codecInfo.name) |
||||
|
||||
mediaCodec = codec |
||||
|
||||
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, IFRAME_INTERVAL) |
||||
|
||||
mediaCodec?.let { |
||||
it.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE) |
||||
it.start() |
||||
Timber.d("format2: ${it.outputFormat}") |
||||
} |
||||
|
||||
try { |
||||
val formattedDate = Date().dateTimeFileFormatted |
||||
val path = File( |
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES), |
||||
"video_${formattedDate}.mp4" |
||||
).toString() |
||||
outputPath = path |
||||
mediaMuxer = MediaMuxer(path, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4) |
||||
} catch (ioe: IOException) { |
||||
Loge("MediaMuxer creation failed: ${ioe.message}") |
||||
} |
||||
} |
||||
|
||||
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!![currentIndexFrame] |
||||
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) |
||||
} |
||||
} |
||||
currentIndexFrame++ |
||||
if (currentIndexFrame > 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, path = $outputPath") |
||||
} |
||||
|
||||
this.outputPath?.let { path -> |
||||
this.completion?.invoke(path) |
||||
} ?: run { |
||||
Logd("no path") |
||||
} |
||||
|
||||
} |
||||
|
||||
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 = MediaFormat.MIMETYPE_VIDEO_AVC // H.264 Advanced Video Coding |
||||
private var _width = 512 |
||||
private var _height = 512 |
||||
private const val BIT_RATE = 2000000 // 800000 |
||||
private const val IFRAME_INTERVAL = 1 |
||||
private const val FRAME_RATE = 30 // 50 |
||||
// 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() |
||||
|
||||
val validCodecs = mutableListOf<MediaCodecInfo>() |
||||
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)) { |
||||
validCodecs.add(codecInfo) |
||||
// return codecInfo |
||||
} |
||||
} |
||||
} |
||||
/** |
||||
* OMX.qcom.video.encoder.avc |
||||
* OMX.google.h264.encoder |
||||
*/ |
||||
|
||||
validCodecs.forEach { |
||||
Timber.d("VALID CODEC name = ${it.name}") |
||||
} |
||||
|
||||
return validCodecs.firstOrNull() |
||||
} |
||||
|
||||
/** |
||||
* 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) |
||||
|
||||
capabilities.colorFormats.forEach { |
||||
Timber.d(">>> Color Format = $it") |
||||
} |
||||
|
||||
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) { |
||||
Timber.d(Mess) |
||||
// if (DEBUG) { |
||||
// Log.d(TAG, Mess) |
||||
// } |
||||
} |
||||
|
||||
private fun Loge(Mess: String?) { |
||||
Timber.e("$Mess") |
||||
// Log.e(TAG, Mess) |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue