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