|
@@ -0,0 +1,236 @@
|
|
|
+package com.bomostory.sceneeditmodule.screen.movie
|
|
|
+
|
|
|
+import android.graphics.Bitmap
|
|
|
+import android.media.*
|
|
|
+import android.util.Log
|
|
|
+import java.io.File
|
|
|
+import java.io.IOException
|
|
|
+import java.lang.Exception
|
|
|
+
|
|
|
+class MediaCodecMovieEncoder: MovieEncoder {
|
|
|
+
|
|
|
+ companion object {
|
|
|
+ private val TAG = "MediaCodecMovieEncoder"
|
|
|
+ private val VERBOSE = false // lots of logging
|
|
|
+
|
|
|
+ private val MIME_TYPE = "video/avc" // H.264 Advanced Video Coding
|
|
|
+ private val IFRAME_INTERVAL = 1
|
|
|
+
|
|
|
+ private var TIMEOUT_USEC = 10L
|
|
|
+ private val BIT_RATE = 2 * 1024 * 1024
|
|
|
+ }
|
|
|
+
|
|
|
+ private lateinit var mBufferInfo: MediaCodec.BufferInfo
|
|
|
+ private lateinit var mediaFormat: MediaFormat
|
|
|
+ private var mediaCodec: MediaCodec? = null
|
|
|
+ private var mediaMuxer: MediaMuxer? = null
|
|
|
+
|
|
|
+ private var videoWidth = 1440
|
|
|
+ private var videoHeight = 720
|
|
|
+ private var file: File? = null
|
|
|
+ private var fps = 15
|
|
|
+
|
|
|
+ private var mTrackIndex = 0
|
|
|
+ private var mRunning = false
|
|
|
+ private var timestamp = 0L
|
|
|
+
|
|
|
+ private var colorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar
|
|
|
+
|
|
|
+ constructor() {
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun setVideoDimension(width: Int, height: Int) {
|
|
|
+ videoWidth = width
|
|
|
+ videoHeight = height
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun prepare(file: File, fps: Int) {
|
|
|
+ this.file = file
|
|
|
+ this.fps = fps
|
|
|
+ val parentFile = file.parentFile
|
|
|
+ if (!parentFile.exists() || !parentFile.isDirectory) {
|
|
|
+ parentFile.mkdirs()
|
|
|
+ }
|
|
|
+ if (file.exists())
|
|
|
+ file.delete()
|
|
|
+ try {
|
|
|
+ mBufferInfo = MediaCodec.BufferInfo()
|
|
|
+
|
|
|
+ mediaCodec = MediaCodec.createEncoderByType(MIME_TYPE)
|
|
|
+ val colorFormats = mediaCodec?.codecInfo?.getCapabilitiesForType(MIME_TYPE)!!.colorFormats
|
|
|
+ if (colorFormats.contains(MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar)) {
|
|
|
+ colorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar
|
|
|
+ }
|
|
|
+ mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, videoWidth, videoHeight)
|
|
|
+ mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE)
|
|
|
+ mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, this.fps)
|
|
|
+ mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat)
|
|
|
+ mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL)
|
|
|
+ mediaCodec?.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
|
|
|
+ } catch (e: IOException) {
|
|
|
+ e.printStackTrace()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun start() {
|
|
|
+ try {
|
|
|
+ mediaCodec?.start()
|
|
|
+
|
|
|
+ try {
|
|
|
+ mediaMuxer = MediaMuxer(file?.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
|
|
|
+ } catch (ioe: IOException) {
|
|
|
+ throw RuntimeException("MediaMuxer creation failed", ioe)
|
|
|
+ }
|
|
|
+ mRunning = true
|
|
|
+ } catch (e: IOException) {
|
|
|
+ e.printStackTrace()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun addFrame(bitmap: Bitmap, repeat: Long) {
|
|
|
+ val input = getYuvByteArray(videoWidth, videoHeight, bitmap)
|
|
|
+ addFrame(input, repeat)
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun finish() {
|
|
|
+ stop()
|
|
|
+ release()
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun stop() {
|
|
|
+ try {
|
|
|
+ mRunning = false
|
|
|
+ mediaCodec?.stop()
|
|
|
+ mediaMuxer?.stop()
|
|
|
+ } catch (e: Exception) {
|
|
|
+ e.printStackTrace()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun release() {
|
|
|
+ try {
|
|
|
+ mRunning = false
|
|
|
+ mediaCodec?.release()
|
|
|
+ mediaCodec = null
|
|
|
+ if (VERBOSE) Log.i(TAG, "RELEASE CODEC")
|
|
|
+ mediaMuxer?.release()
|
|
|
+ mediaMuxer = null
|
|
|
+ if (VERBOSE) Log.i(TAG, "RELEASE MUXER")
|
|
|
+ } catch (e: Exception) {
|
|
|
+ e.printStackTrace()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun addFrame(input: ByteArray, repeat: Long) {
|
|
|
+ val duration = repeat * 1000000L / fps
|
|
|
+ while (true) {
|
|
|
+ if (!mRunning) {
|
|
|
+ break
|
|
|
+ }
|
|
|
+ val inputBufIndex = mediaCodec?.dequeueInputBuffer(TIMEOUT_USEC)!!
|
|
|
+ if (inputBufIndex >= 0) {
|
|
|
+ val inputBuffer = mediaCodec?.getInputBuffer(inputBufIndex)!!
|
|
|
+ inputBuffer.clear()
|
|
|
+ inputBuffer.put(input)
|
|
|
+ mediaCodec?.queueInputBuffer(inputBufIndex, 0, input.size, timestamp, 0)
|
|
|
+ }
|
|
|
+ var encoderStatus = mediaCodec!!.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC)
|
|
|
+ if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
|
|
|
+ // no output available yet
|
|
|
+ if (VERBOSE) Log.d("CODEC", "no output from encoder available")
|
|
|
+ } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
|
|
+ // not expected for an encoder
|
|
|
+ var newFormat = mediaCodec?.outputFormat
|
|
|
+ mTrackIndex = mediaMuxer!!.addTrack(newFormat)
|
|
|
+ mediaMuxer?.start()
|
|
|
+ } else if (encoderStatus < 0) {
|
|
|
+ if (VERBOSE) Log.i("CODEC", "unexpected result from encoder.dequeueOutputBuffer: $encoderStatus")
|
|
|
+ } else if (mBufferInfo.size != 0) {
|
|
|
+ val encodedData = mediaCodec?.getOutputBuffer(encoderStatus)
|
|
|
+ if (encodedData == null) {
|
|
|
+ if (VERBOSE) Log.i("CODEC", "encoderOutputBuffer $encoderStatus was null")
|
|
|
+ } else {
|
|
|
+ encodedData.position(mBufferInfo.offset)
|
|
|
+ encodedData.limit(mBufferInfo.offset + mBufferInfo.size)
|
|
|
+ mediaMuxer?.writeSampleData(mTrackIndex, encodedData, mBufferInfo)
|
|
|
+ mediaCodec?.releaseOutputBuffer(encoderStatus, false)
|
|
|
+ if (VERBOSE) Log.i("CODEC", "encoderOutputBuffer success")
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ timestamp += duration
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun getYuvByteArray(width: Int, height: Int, bitmap: Bitmap): ByteArray {
|
|
|
+ val argb = IntArray(width * height)
|
|
|
+ bitmap.getPixels(argb, 0, width, 0, 0, width, height)
|
|
|
+ val yuv: ByteArray
|
|
|
+ if (colorFormat == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar)
|
|
|
+ yuv = encodeYUV420P(argb, width, height)
|
|
|
+ else
|
|
|
+ yuv = encodeYUV420SP(argb, width, height)
|
|
|
+ return yuv
|
|
|
+ }
|
|
|
+
|
|
|
+// Y = R * 0.299 + G * 0.587 + B * 0.114
|
|
|
+// U = R * -0.169 + G * -0.332 + B * 0.500 + 128
|
|
|
+// V = R * 0.500 + G * -0.419 + B * -0.0813 + 128
|
|
|
+
|
|
|
+// Y' = R * 0.299 + G * 0.587 + B * 0.114
|
|
|
+// U = R * -0.147 + G * -0.289 + B * 0.436 + 128
|
|
|
+// V = R * 0.615 + G * -0.515 + B * -0.100 + 128
|
|
|
+
|
|
|
+ private fun encodeYUV420P(argb: IntArray, width: Int, height: Int): ByteArray {
|
|
|
+ val yuv420p = ByteArray(width * height * 3 / 2)
|
|
|
+ val frameSize = width * height
|
|
|
+ var yIndex = 0
|
|
|
+ var uIndex = frameSize
|
|
|
+ var vIndex = frameSize * 5 / 4
|
|
|
+ var r: Int
|
|
|
+ var g: Int
|
|
|
+ var b: Int
|
|
|
+ var index = 0
|
|
|
+ for (j: Int in 0 until height) {
|
|
|
+ for (i: Int in 0 until width) {
|
|
|
+ r = (argb[index] and 0xff0000) shr 16
|
|
|
+ g = (argb[index] and 0xff00) shr 8
|
|
|
+ b = (argb[index] and 0xff) shr 0
|
|
|
+
|
|
|
+ yuv420p[yIndex++] = Math.max(0.0, Math.min(255.0, r * 0.299 + g * 0.587 + b * 0.114)).toByte()
|
|
|
+ if (j and 1 == 0 && index and 1 == 0) {
|
|
|
+ yuv420p[uIndex++] = Math.max(0.0, Math.min(255.0, r * -0.169 + g * -0.332 + b * 0.500 + 128)).toByte()
|
|
|
+ yuv420p[vIndex++] = Math.max(0.0, Math.min(255.0, r * 0.500 + g * -0.419 + b * -0.0813 + 128)).toByte()
|
|
|
+ }
|
|
|
+ index++
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return yuv420p;
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun encodeYUV420SP(argb: IntArray, width: Int, height: Int): ByteArray {
|
|
|
+ val yuv420sp = ByteArray(width * height * 3 / 2)
|
|
|
+ val frameSize = width * height
|
|
|
+ var yIndex = 0
|
|
|
+ var uvIndex = frameSize
|
|
|
+ var r: Int
|
|
|
+ var g: Int
|
|
|
+ var b: Int
|
|
|
+ var index = 0
|
|
|
+ for (j: Int in 0 until height) {
|
|
|
+ for (i: Int in 0 until width) {
|
|
|
+ r = (argb[index] and 0xff0000) shr 16
|
|
|
+ g = (argb[index] and 0xff00) shr 8
|
|
|
+ b = (argb[index] and 0xff) shr 0
|
|
|
+
|
|
|
+ yuv420sp[yIndex++] = Math.max(0.0, Math.min(255.0, r * 0.299 + g * 0.587 + b * 0.114)).toByte()
|
|
|
+ if (j and 1 == 0 && index and 1 == 0) {
|
|
|
+ yuv420sp[uvIndex++] = Math.max(0.0, Math.min(255.0, r * -0.169 + g * -0.332 + b * 0.500 + 128)).toByte()
|
|
|
+ yuv420sp[uvIndex++] = Math.max(0.0, Math.min(255.0, r * 0.500 + g * -0.419 + b * -0.0813 + 128)).toByte()
|
|
|
+ }
|
|
|
+ index++
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return yuv420sp
|
|
|
+ }
|
|
|
+}
|