Browse Source

Add MovieEncoder (interface and implementations)

cooperku_kdanmobile 6 years ago
parent
commit
68f91b3098

+ 111 - 0
src/main/java/com/bomostory/sceneeditmodule/screen/movie/JcodecMovieEncoder.kt

@@ -0,0 +1,111 @@
+package com.bomostory.sceneeditmodule.screen.movie
+
+import android.graphics.Bitmap
+import com.bomostory.sceneeditmodule.SceneDrawer
+import java.io.File
+import java.io.IOException
+
+import org.jcodec.api.transcode.PixelStore
+import org.jcodec.api.transcode.PixelStoreImpl
+import org.jcodec.api.transcode.Sink
+import org.jcodec.api.transcode.SinkImpl
+import org.jcodec.api.transcode.VideoFrameWithPacket
+import org.jcodec.common.AndroidUtil
+import org.jcodec.common.Codec
+import org.jcodec.common.Format
+import org.jcodec.common.io.NIOUtils
+import org.jcodec.common.io.SeekableByteChannel
+import org.jcodec.common.model.ColorSpace
+import org.jcodec.common.model.Packet
+import org.jcodec.common.model.Picture
+import org.jcodec.scale.ColorUtil
+import org.jcodec.scale.Transform
+
+class JcodecMovieEncoder: MovieEncoder {
+
+    companion object {
+        private val OUTPUT_FORMAT = Format.MOV
+        private val OUTPUT_VIDEO_CODEC = Codec.H264
+        private val OUTPUT_AUDIO_CODEC: Codec? = null
+    }
+
+    private var transform: Transform? = null
+    private var sink: Sink? = null
+    private var pixelStore: PixelStore? = null
+
+    private var videoWidth = 1440
+    private var videoHeight = 720
+    private var fps: Int = 15
+
+    private var frameNo = 1L
+    private var timestamp = 0L
+
+    constructor() {
+    }
+
+    override fun setVideoDimension(width: Int, height: Int) {
+        videoWidth = width
+        videoHeight = height
+    }
+
+    override fun prepare(file: File, fps: Int) {
+        this.fps = fps
+
+        try {
+            val out = NIOUtils.writableChannel(file)
+            sink = SinkImpl.createWithStream(out, OUTPUT_FORMAT, OUTPUT_VIDEO_CODEC, OUTPUT_AUDIO_CODEC)
+            sink?.init()
+
+            if (sink!!.inputColor != null)
+                transform = ColorUtil.getTransform(ColorSpace.RGB, sink!!.inputColor)
+
+            pixelStore = PixelStoreImpl()
+        } catch (e: IOException) {
+            e.printStackTrace()
+        }
+    }
+
+    override fun start() {
+        frameNo = 1L
+        timestamp = 0L
+    }
+
+    override fun addFrame(bitmap: Bitmap, repeat: Long) {
+        val duration = repeat
+        var input = bitmap
+        if (input.width != videoWidth || input.height != videoHeight) {
+            input = Bitmap.createScaledBitmap(bitmap, videoWidth, videoHeight, true)
+        }
+        val picture = AndroidUtil.fromBitmap(input, ColorSpace.RGB)
+        try {
+            val sinkColor = sink?.inputColor
+            val toEncode: PixelStore.LoanerPicture
+            if (sinkColor != null) {
+                toEncode = pixelStore!!.getPicture(videoWidth, videoHeight, sinkColor)
+                transform!!.transform(picture, toEncode.picture)
+            } else {
+                toEncode = PixelStore.LoanerPicture(picture, 0)
+            }
+
+            val pkt = Packet.createPacket(null, timestamp, fps, duration, frameNo++, Packet.FrameType.KEY, null)
+            sink?.outputVideoFrame(VideoFrameWithPacket(pkt, toEncode))
+
+            if (sinkColor != null)
+                pixelStore?.putBack(toEncode)
+
+            timestamp += duration
+        } catch (e: IOException) {
+            e.printStackTrace()
+        } catch (e: IllegalStateException) {
+            e.printStackTrace()
+        }
+    }
+
+    override fun finish() {
+        try {
+            sink?.finish()
+        } catch (e: IOException) {
+            e.printStackTrace()
+        }
+    }
+}

+ 236 - 0
src/main/java/com/bomostory/sceneeditmodule/screen/movie/MediaCodecMovieEncoder.kt

@@ -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
+    }
+}

+ 21 - 0
src/main/java/com/bomostory/sceneeditmodule/screen/movie/MovieEncoder.kt

@@ -0,0 +1,21 @@
+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
+
+interface MovieEncoder {
+
+    fun setVideoDimension(width: Int, height: Int)
+
+    fun prepare(file: File, fps: Int)
+
+    fun start()
+
+    fun finish()
+
+    fun addFrame(bitmap: Bitmap, repeat: Long)
+}