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