Browse Source

Merge branch 'gifSuperMovieMaker'

cooperku_kdanmobile 6 years ago
parent
commit
6d50a48e4f

+ 2 - 0
build.gradle

@@ -65,6 +65,8 @@ dependencies {
     implementation 'com.squareup.picasso:picasso:2.71828'
     implementation 'com.github.naman14:TAndroidLame:1.1'
     implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.16'
+    implementation 'org.jcodec:jcodec:0.2.3'
+    implementation 'org.jcodec:jcodec-android:0.2.3'
 }
 repositories {
     mavenCentral()

+ 1 - 0
src/main/AndroidManifest.xml

@@ -4,6 +4,7 @@
 
     <application
         android:allowBackup="true"
+        android:largeHeap="true"
         android:icon="@mipmap/ic_launcher"
         android:label="@string/app_name"
         android:roundIcon="@mipmap/ic_launcher_round"

+ 143 - 41
src/main/java/com/bomostory/sceneeditmodule/SceneDrawer.kt

@@ -2,52 +2,110 @@ package com.bomostory.sceneeditmodule
 
 import android.content.Context
 import android.graphics.*
-import android.util.DisplayMetrics
-import android.view.WindowManager
 import com.bomostory.sceneeditmodule.basicdata.Actor
 import com.bomostory.sceneeditmodule.basicdata.Scene
+import pl.droidsonroids.gif.GifDrawable
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.math.max
 import kotlin.math.pow
 
 object SceneDrawer {
-    fun drawScene(context: Context, scene: Scene, trackX: Int, scaleWidth: Int, scaleHeight: Int): Bitmap? {
-        var sceneBitmap: Bitmap? = null
+    private val CONTROLLER_RADIUS = 25
+    private var nowScene: Scene? = null
+    private var sceneBitmap: Bitmap? = null
+    private var bitmaps = HashMap<Actor, Any>()
+    private var gifFrameInfo = HashMap<Actor, IntArray>()
+    private var widthScaleFactor = 1f
+    private var heightScaleFactor = 1f
 
+    private val lock = ReentrantLock()
+
+    fun reset() {
+        nowScene = null
+        sceneBitmap = null
+        bitmaps.clear()
+        gifFrameInfo.clear()
+
+        if (lock.isLocked)
+            lock.unlock()
+    }
+
+    fun drawScene(context: Context, scene: Scene, trackX: Int, scaleWidth: Int, scaleHeight: Int, millisecond: Long = -1): Bitmap? {
+        lock.lock()
+        if (nowScene != scene) {
+            reset()
+            nowScene = scene
+            val screenWidth = scene.sceneWidth.toFloat()
+            val screenHeight = scene.sceneWidth / 2f
+            widthScaleFactor = scaleWidth / screenWidth
+            heightScaleFactor = scaleHeight / screenHeight
+            loadBitmaps(context, scaleWidth, scaleHeight, millisecond >= 0)
+        } else {
+            lock.unlock()
+        }
+
+        val bitmap = Bitmap.createBitmap(scaleWidth, scaleHeight, Bitmap.Config.ARGB_8888)
         scene?.apply {
-            sceneBitmap = BitmapFactory.decodeFile(backgroundPath)
-            sceneBitmap = Bitmap.createScaledBitmap(sceneBitmap, scaleWidth, scaleHeight, true)
-            sceneBitmap = sceneBitmap?.copy(Bitmap.Config.ARGB_8888, true)
+            val canvas = Canvas(bitmap)
+            canvas.save()
+            canvas?.drawBitmap(sceneBitmap, trackX * widthScaleFactor / 32f - sceneBitmap!!.width / 64f, 0f, null)
+            canvas.restore()
 
-            val metrics = DisplayMetrics()
-            val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
-            windowManager.defaultDisplay.getMetrics(metrics);
-            val screenWidth = metrics.widthPixels
-            val screenHeight = metrics.widthPixels / 2
-            val widthScaleFactor: Float = scaleWidth / screenWidth.toFloat()
-            val heightScaleFactor: Float = scaleHeight / screenHeight.toFloat()
-            val canvas = Canvas(sceneBitmap)
             for (layer in layers) {
+                canvas.save()
+                canvas.translate(trackX * widthScaleFactor / 2f.pow(layers.indexOf(layer)), 0f)
                 for (actor in layer.actors) {
-                    var bitmap = if (actor.isDialogue) DialogueDrawer.drawDialogue(context, actor) else BitmapFactory.decodeFile(actor.resourcePath)
+                    var bitmap = bitmaps[actor]
                     if (bitmap != null) {
-                        canvas.save()
-                        canvas.translate(trackX / 2f.pow(layers.indexOf(layer)), 0f)
-                        drawActor(canvas, actor, bitmap, widthScaleFactor, heightScaleFactor)
-                        canvas.restore()
-                        if (!bitmap.isRecycled) {
-                            bitmap.recycle()
+                        if (bitmap is GifData) {
+                            val gifData = bitmap
+                            if (gifData.frameIndex < 0 || !(millisecond in gifData.durations[gifData.frameIndex])) {
+                                var gifTime = millisecond
+                                gifData.frameIndex = gifData.durations.size - 1
+                                if (gifData.gifDrawable.loopCount <= 0 || gifTime < gifData.gifDrawable.loopCount * gifData.gifDrawable.duration) {
+                                    gifTime %= gifData.gifDrawable.duration
+
+                                    for (index in 0 until gifData.durations.size) {
+                                        if (gifTime in gifData.durations[index]) {
+                                            gifData.frameIndex = index
+                                            break
+                                        }
+                                    }
+                                }
+
+                                val seekBitmap = gifData.gifDrawable.seekToFrameAndGet(gifData.frameIndex)
+                                var actorWidth = (actor.sideLength - CONTROLLER_RADIUS * 4).toFloat()
+                                var actorHeight = (actor.sideHeight - CONTROLLER_RADIUS * 4).toFloat()
+                                if (seekBitmap.width > seekBitmap.height) {
+                                    actorHeight = actorHeight * seekBitmap.height / seekBitmap.width
+                                } else {
+                                    actorWidth = actorWidth * seekBitmap.width / seekBitmap.height
+                                }
+                                val bitmapWidth = actorWidth * widthScaleFactor
+                                val bitmapHeight = actorHeight * heightScaleFactor
+                                gifData.bitmap = Bitmap.createScaledBitmap(seekBitmap, bitmapWidth.toInt(), bitmapHeight.toInt(), true)
+                                seekBitmap.recycle()
+                            }
+                            drawActor(canvas, actor, gifData.bitmap, widthScaleFactor, heightScaleFactor)
+                        } else {
+                            drawActor(canvas, actor, bitmap as Bitmap, widthScaleFactor, heightScaleFactor)
                         }
-                        bitmap = null
                     }
                 }
+                canvas.restore()
             }
         }
-        return sceneBitmap
+
+        if (millisecond < 0) {
+            reset()
+        }
+        if (lock.isLocked)
+            lock.unlock()
+
+        return bitmap
     }
 
     private fun drawActor(canvas: Canvas, actor: Actor, bitmap: Bitmap, widthScaleFactor: Float, heightScaleFactor: Float) {
-
-        val CONTROLLER_RADIUS = 25
-        var drawBitmap: Bitmap? = null
         if (!actor.isDialogue) {
             var actorWidth = (actor.sideLength - CONTROLLER_RADIUS * 4).toFloat()
             var actorHeight = (actor.sideHeight - CONTROLLER_RADIUS * 4).toFloat()
@@ -56,9 +114,8 @@ object SceneDrawer {
             } else {
                 actorWidth = actorWidth * bitmap.width / bitmap.height
             }
-            var bitmapWidth = actorWidth * widthScaleFactor
-            var bitmapHeight = actorHeight * heightScaleFactor
-            drawBitmap = Bitmap.createScaledBitmap(bitmap, bitmapWidth.toInt(), bitmapHeight.toInt(), true)
+            val bitmapWidth = actorWidth * widthScaleFactor
+            val bitmapHeight = actorHeight * heightScaleFactor
 
             var actorX = (actor.positionX + CONTROLLER_RADIUS * 2).toFloat()
             var actorY = (actor.positionY + CONTROLLER_RADIUS * 2).toFloat()
@@ -71,23 +128,68 @@ object SceneDrawer {
             paint.alpha = (actor.opacity * 255).toInt()
             if (actor.isMirror)
                 canvas.scale(-1f, 1f, actorX + bitmapWidth / 2, actorY + bitmapHeight / 2)
-            canvas.drawBitmap(drawBitmap, actorX, actorY, paint)
+            canvas.drawBitmap(bitmap, actorX, actorY, paint)
         } else {
-            var actorWidth = (actor.sideLength).toFloat()
-            var actorHeight = (actor.sideHeight).toFloat()
-            var bitmapWidth = actorWidth * widthScaleFactor
-            var bitmapHeight = actorHeight * heightScaleFactor
-            drawBitmap = Bitmap.createScaledBitmap(bitmap, bitmapWidth.toInt(), bitmapHeight.toInt(), true)
-
             var actorX = (actor.positionX).toFloat()
             var actorY = (actor.positionY).toFloat()
             actorX *= widthScaleFactor
             actorY *= heightScaleFactor
-            canvas?.drawBitmap(drawBitmap, actorX, actorY, null)
+            canvas?.drawBitmap(bitmap, actorX, actorY, null)
+        }
+    }
+
+    private fun GifDrawable.getBitmapAt(milliseconds: Int): Bitmap = seekToPositionAndGet(if (loopCount == 0 || milliseconds < duration * loopCount) max(0, milliseconds % duration) else (duration))
+
+    private fun getBitmap(context: Context, actor: Actor, createGifData: Boolean): Any {
+        if (!actor.isDialogue) {
+            val isGif = actor.resourcePath.toLowerCase().endsWith(".gif")
+            if (isGif && createGifData) {
+                val gifDrawable = GifDrawable(actor.resourcePath)
+                val size = gifDrawable.numberOfFrames
+                var durations = Array(size, { index -> 0L .. 1L} )
+                var count = 0L
+                for (index in 0 until size) {
+                    var duration = gifDrawable.getFrameDuration(index).toLong()
+                    durations[index] = count until (count + duration)
+                    count += duration
+                }
+                return GifData(gifDrawable, durations, -1, gifDrawable.currentFrame)
+            }
+            var bitmap = if (isGif) GifDrawable(actor.resourcePath).currentFrame else BitmapFactory.decodeFile(actor.resourcePath)
+            var actorWidth = (actor.sideLength - CONTROLLER_RADIUS * 4).toFloat()
+            var actorHeight = (actor.sideHeight - CONTROLLER_RADIUS * 4).toFloat()
+            if (bitmap.width > bitmap.height) {
+                actorHeight = actorHeight * bitmap.height / bitmap.width
+            } else {
+                actorWidth = actorWidth * bitmap.width / bitmap.height
+            }
+            val bitmapWidth = actorWidth * widthScaleFactor
+            val bitmapHeight = actorHeight * heightScaleFactor
+            bitmap = Bitmap.createScaledBitmap(bitmap, bitmapWidth.toInt(), bitmapHeight.toInt(), true)
+            return bitmap
+        } else {
+            var bitmap = DialogueDrawer.drawDialogue(context, actor)!!
+            val actorWidth = (actor.sideLength).toFloat()
+            val actorHeight = (actor.sideHeight).toFloat()
+            val bitmapWidth = actorWidth * widthScaleFactor
+            val bitmapHeight = actorHeight * heightScaleFactor
+            bitmap = Bitmap.createScaledBitmap(bitmap, bitmapWidth.toInt(), bitmapHeight.toInt(), true)
+            return bitmap
         }
-        if (!drawBitmap.isRecycled) {
-            drawBitmap.recycle()
+    }
+
+    private fun loadBitmaps(context: Context, scaleWidth: Int, scaleHeight: Int, createGifData: Boolean) {
+        nowScene?.apply {
+            sceneBitmap = BitmapFactory.decodeFile(backgroundPath)
+            sceneBitmap = Bitmap.createScaledBitmap(sceneBitmap, (scaleWidth * 33 / 32f).toInt(), scaleHeight, true)
+            sceneBitmap = sceneBitmap?.copy(Bitmap.Config.ARGB_8888, true)
+            for (layer in layers) {
+                for (actor in layer.actors) {
+                    bitmaps[actor] = getBitmap(context, actor, createGifData)
+                }
+            }
         }
-        drawBitmap = null
     }
+
+    data class GifData(val gifDrawable: GifDrawable, val durations: Array<LongRange>, var frameIndex: Int, var bitmap: Bitmap)
 }

+ 2 - 1
src/main/java/com/bomostory/sceneeditmodule/SceneEditActivity.kt

@@ -1354,7 +1354,8 @@ class SceneEditActivity : AppCompatActivity(), ActorAdapter.OnActorDragListener,
                 when (resultCode) {
                     Activity.RESULT_OK -> if (data != null) {
                         var uri = data.data
-                        if (uri.path.endsWith(".gif"))
+                        val path = FileUtils.getRealPathFromURI(this, uri)
+                        if (path != null && path.endsWith(".gif"))
                             FileUtils.saveGifForActor(this, project, data.data, System.currentTimeMillis().toString())
                         else
                             FileUtils.saveImageForActor(this, project, data.data, System.currentTimeMillis().toString())

+ 127 - 89
src/main/java/com/bomostory/sceneeditmodule/SuperMovieMaker.kt

@@ -1,25 +1,27 @@
 package com.bomostory.sceneeditmodule
 
 import android.content.Context
+import android.graphics.Bitmap
 import android.media.MediaMetadataRetriever
 import android.util.Log
 import com.bomostory.sceneeditmodule.screen.movie.MovieEditActivity.Companion.FPS
 import com.bomostory.sceneeditmodule.basicdata.Music
 import com.bomostory.sceneeditmodule.basicdata.Project
+import com.bomostory.sceneeditmodule.screen.movie.MediaCodecMovieEncoder
+import com.bomostory.sceneeditmodule.screen.movie.MovieEncoder
 import com.bomostory.sceneeditmodule.utils.FileUtils
 import com.example.exportmedia.MediaHelper
 import com.example.exportmedia.audio.AudioConcat
 import com.example.exportmedia.audio.AudioLooper
 import com.example.exportmedia.data.AudioSource
-import com.example.exportmedia.vedio.MovieMaker
-import io.reactivex.Completable
+import io.reactivex.*
 import io.reactivex.Observable
 import io.reactivex.schedulers.Schedulers
+import pl.droidsonroids.gif.GifDrawable
 import java.io.File
+import java.util.*
 import java.util.concurrent.Semaphore
 import kotlin.collections.ArrayList
-import kotlin.collections.LinkedHashMap
-import kotlin.collections.set
 
 class SuperMovieMaker {
 
@@ -30,58 +32,83 @@ class SuperMovieMaker {
             musics: List<Music>,
             scaleWidth: Int,
             scaleHeight: Int,
-            mediaHelper: MediaHelper): Observable<String> {
+            mediaHelper: MediaHelper,
+            movieEncoder: MovieEncoder? = MediaCodecMovieEncoder()): Observable<String> {
         return Observable.create<String> { emitter ->
+
             try {
+//                movieEncoder = MediaCodecMovieEncoder()
+//                movieEncoder = JcodecMovieEncoder()
+                movieEncoder?.setVideoDimension(scaleWidth, scaleHeight)
+                movieEncoder?.prepare(outputFile, FPS)
+
                 val audioSources = ArrayList<AudioSource>()
                 project.story?.let {
                     audioSources.add(generateRecordSource(project, mediaHelper))
                 }
                 audioSources.addAll(generateAudioSource(project, musics, mediaHelper))
 
-                val movieBuilder = MovieMaker.Builder(mediaHelper)
-                movieBuilder.fps = FPS
-                movieBuilder.audioSources = audioSources
-
-                val inputSceneStatus = LinkedHashMap<Int, Int>()
                 val frameDataList = generateFrameDataList(project)
-                var bitmapIndex = 0
-                val permitCount = frameDataList.size
-                val semaphore = Semaphore(permitCount)
+                Log.d(this::class.java.simpleName, "frameDataList.size = ${frameDataList.size}")
+                val semaphore = Semaphore(5)
+                var generateCount = 0
                 var completeCount = 0
-                emitter.onNext("$completeCount/${frameDataList.size}")
-
-                var beginTime = System.currentTimeMillis()
-                frameDataList.forEach { frameData ->
-                    val i = bitmapIndex++
-                    inputSceneStatus[i] = frameData.repeat
-                    semaphore.acquire()
-                    Completable.create {
-                        project?.story?.apply {
-                            val scene = scenes[frameData.sceneIndex]
-                            val bitmap = SceneDrawer.drawScene(context, scene, frameData.x, scaleWidth, scaleHeight)
-                            movieBuilder.addImage(i, bitmap!!).blockingSubscribe()
-                        }
-                        it.onComplete()
+//                emitter.onNext("$completeCount/${frameDataList.size}")
+                emitter.onNext("Ready")
+
+                val beginTime = System.nanoTime()
+                var timeDraw = 0L
+                var timeEncode = 0L
+
+                SceneDrawer.reset()
+                movieEncoder?.start()
+                val upstream = Flowable.create<Pair<Bitmap, FrameData>> ({
+                    frameDataList.forEach { frameData ->
+                        semaphore.acquire()
+                        val scene = project.story!!.scenes[frameData.sceneIndex]
+                        val time = System.nanoTime()
+                        val bitmap = SceneDrawer.drawScene(context, scene, frameData.x, scaleWidth, scaleHeight, frameData.millisecond)
+                        timeDraw += System.nanoTime() - time
+                        generateCount++
+                        it.onNext(Pair(bitmap!!, frameData))
                     }
-                            .subscribeOn(Schedulers.computation())
-                            .subscribe {
-                                completeCount++
-                                emitter.onNext("$completeCount/${frameDataList.size}")
-                                semaphore.release()
-                            }
-                }
-                semaphore.acquire(permitCount)
-                var endTime = System.currentTimeMillis()
-                Log.d(this@SuperMovieMaker::class.java.simpleName, "draw + save duration = ${endTime - beginTime}")
-                movieBuilder.inputFilePath = movieBuilder.addInputFile(inputSceneStatus).blockingFirst()
-                emitter.onNext("Encoding video")
-                movieBuilder.build().output(outputFile).blockingSubscribe()
+                    it.onComplete()
+                }, BackpressureStrategy.BUFFER)
+
+                val disposable = upstream
+                        .observeOn(Schedulers.newThread())
+                        .subscribe ({
+                            val bitmap = it.first
+                            val frameData = it.second
+                            val time = System.nanoTime()
+                            movieEncoder?.addFrame(bitmap!!, frameData.repeat)
+                            timeEncode += System.nanoTime() - time
+                            bitmap.recycle()
+
+                            completeCount++
+//                            emitter.onNext(String.format("%3.2f%% (%03d / %03d)", completeCount * 100.0 / frameDataList.size, completeCount, frameDataList.size))
+                            emitter.onNext(String.format("%3.2f%%", completeCount * 100.0 / frameDataList.size))
+//                            emitter.onNext(String.format("%3.2f%% (%03d / %03d)", completeCount * 100.0 / frameDataList.size, completeCount, generateCount))
+                            semaphore.release()
+                        }, {
+                            it.printStackTrace()
+                        })
+                SceneDrawer.reset()
+                movieEncoder?.finish()
+
+                val endTime = System.nanoTime()
+                Log.d(this::class.java.simpleName, "duration = ${(endTime - beginTime) / 1000000000.0}")
+                Log.d(this::class.java.simpleName, "timeDraw = ${timeDraw / 1000000000.0}")
+                Log.d(this::class.java.simpleName, "timeEncode = ${timeEncode / 1000000000.0}")
+                emitter.onComplete()
             } catch (e: Exception) {
                 e.printStackTrace()
             }
-            emitter.onComplete()
         }
+                .doOnDispose {
+                    SceneDrawer.reset()
+                    movieEncoder?.finish()
+                }
     }
 
     private fun generateRecordSource(project: Project, mediaHelper: MediaHelper): AudioSource {
@@ -101,7 +128,7 @@ class SuperMovieMaker {
         }
 
         val audioConcat = audioConcatBuilder.build()
-            audioConcat.output(outputFile).blockingFirst()
+        audioConcat.output(outputFile).blockingFirst()
 
         val mediaMetadataRetriever = MediaMetadataRetriever()
         mediaMetadataRetriever.setDataSource(outputFile.path)
@@ -145,72 +172,83 @@ class SuperMovieMaker {
         return audioSources
     }
 
-    fun generateFrameDataList(project: Project): List<FrameData> {
+    private fun generateFrameDataList(project: Project): List<FrameData> {
         val frameDataList = ArrayList<FrameData>()
         var index = 0
         project.story?.scenes?.forEach { scene ->
             val sceneIndex = index++
+
             scene.record?.apply {
+
+                frameDataList.add(FrameData(sceneIndex, 0, 1L, 0L))
+
                 var x = 0
                 var trackPosition = -1
 
+                val timeSet = TreeSet<Long>()
+                for (layer in scene.layers) {
+                    for (actor in layer.actors) {
+                        if (actor.resourcePath.toLowerCase().endsWith(".gif")) {
+                            val gifDrawable = GifDrawable(actor.resourcePath)
+                            val numberOfFrames = gifDrawable.numberOfFrames
+                            val loopCount = if (gifDrawable.loopCount == 0) Int.MAX_VALUE else gifDrawable.loopCount
+                            val durationCount = Math.ceil(period / gifDrawable.duration.toDouble()).toInt()
+                            var duration = 0L
+                            for (times in 1..Math.min(loopCount, durationCount)) {
+                                for (index in 0 until numberOfFrames) {
+                                    duration += gifDrawable.getFrameDuration(index)
+                                    if (duration > period)
+                                        break
+                                    timeSet.add(duration)
+                                }
+                                if (duration > period)
+                                    break
+                            }
+                        }
+                    }
+                }
+
+                val timeArray = timeSet.toArray() as Array<Any>
+                var timeArrayIndex = 0
+
                 for (t in 0..period step ((1f / FPS) * 1000).toLong()) {
-                    for (track in tracks) {
-                        if (t >= track.time && tracks.indexOf(track) > trackPosition) {
-                            x = track.positionX
-                            trackPosition = tracks.indexOf(track)
+
+                    //  gif animation
+                    var gifUpdate = false
+                    if (timeArrayIndex < timeArray.size) {
+                        var gifTime = timeArray[timeArrayIndex] as Long
+                        if (t >= gifTime) {
+                            while (timeArrayIndex < timeArray.size && t > timeArray[timeArrayIndex] as Long) {
+                                timeArrayIndex++
+                            }
+                            gifUpdate = true
                         }
                     }
 
-                    if (frameDataList.isEmpty()) {
-                        frameDataList.add(FrameData(sceneIndex, x, 1))
-                    } else {
-                        val frameData = frameDataList.last()
-                        if (frameData.x == x) {
-                            frameData.repeat++
+                    var tmpTrackPosition = trackPosition
+                    for (i in (trackPosition + 1) until tracks.size) {
+                        val track = tracks[i]
+                        if (t >= track.time) {
+                            x = track.positionX
+                            tmpTrackPosition = i
                         } else {
-                            frameDataList.add(FrameData(sceneIndex, x, 1))
+                            break
                         }
                     }
+                    trackPosition = tmpTrackPosition
+
+                    val frameData = frameDataList.last()
+                    if (!gifUpdate && frameData.x == x) {
+                        frameData.repeat++
+                    } else {
+                        frameDataList.add(FrameData(sceneIndex, x, 1L, t))
+                    }
                 }
             }
         }
+
         return frameDataList
     }
 
-//    private fun generateMovieFilms(
-//            scene: Scene,
-//            scaleWidth: Int,
-//            scaleHeight: Int): Observable<AbstractMap.SimpleEntry<Int, Bitmap>> {
-//        var preBitmap: Bitmap?
-//        return Observable.create<AbstractMap.SimpleEntry<Int, Bitmap>> {
-//            scene?.apply {
-//                record?.apply {
-//                    var bitmap = SceneDrawer.drawScene(scene, 0, scaleWidth, scaleHeight)
-//                    var bitmapIndex = 0
-//                    var trackPosition = -1
-//
-//                    for (t in 0..period step ((1f / FPS) * 1000).toLong()) {
-//                        for (track in tracks) {
-//                            if (t >= track.time && tracks.indexOf(track) > trackPosition) {
-//                                preBitmap = bitmap
-//                                preBitmap?.recycle()
-//
-//                                bitmap = SceneDrawer.drawScene(scene, track.positionX, scaleWidth, scaleHeight)
-//                                trackPosition = tracks.indexOf(track)
-//                            }
-//                        }
-//
-//                        bitmap?.apply {
-//                            it.onNext(AbstractMap.SimpleEntry(bitmapIndex++, this))
-//                        }
-//                    }
-//                    it.onComplete()
-//                }
-//            }
-//        }
-//    }
-
-
-    data class FrameData(val sceneIndex: Int, val x: Int, var repeat: Int)
+    data class FrameData(val sceneIndex: Int, val x: Int, var repeat: Long, val millisecond: Long)
 }

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

+ 2 - 5
src/main/java/com/bomostory/sceneeditmodule/screen/movie/MovieEditActivity.kt

@@ -15,10 +15,7 @@ import android.view.View
 import android.widget.CompoundButton
 import android.widget.SeekBar
 import android.widget.Toast
-import com.bomostory.sceneeditmodule.Config
-import com.bomostory.sceneeditmodule.PdfMaker
-import com.bomostory.sceneeditmodule.PrintTools
-import com.bomostory.sceneeditmodule.SuperMovieMaker
+import com.bomostory.sceneeditmodule.*
 import com.bomostory.sceneeditmodule.basicdata.Music
 import com.bomostory.sceneeditmodule.basicdata.Project
 import com.bomostory.sceneeditmodule.basicdata.Scene
@@ -117,7 +114,7 @@ class MovieEditActivity : AppCompatActivity(),
         const val PROJECT_KEY = "project_key"
         const val MUSIC_SELECT_DIALOG_TAG = "music_select_dialog_tag"
         const val MUSIC_EDIT_DIALOG_TAG = "music_edit_dialog_tag"
-        const val FPS: Int = 30
+        const val FPS: Int = 15
         private const val UI_ANIMATION_DELAY = 300
     }
 

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

+ 2 - 2
src/main/res/values/dimens.xml

@@ -1,8 +1,8 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
     <dimen name="horizontal_item_spacing">8dp</dimen>
-    <dimen name="movie_width">720dp</dimen>
-    <dimen name="movie_height">360dp</dimen>
+    <dimen name="movie_width">1920px</dimen>
+    <dimen name="movie_height">1080px</dimen>
     <dimen name="actor_height">120dp</dimen>
     <dimen name="actor_width">120dp</dimen>
     <dimen name="edit_actor_padding">24dp</dimen>