|
|
@@ -0,0 +1,218 @@
|
|
|
+package cn.i2edu.dubbing_lib.util
|
|
|
+
|
|
|
+import android.content.Context
|
|
|
+import android.os.Message
|
|
|
+import android.text.TextUtils
|
|
|
+import cn.i2edu.dubbing_lib.audioUtils.AudioDecoder
|
|
|
+import cn.i2edu.dubbing_lib.audioUtils.AudioEncoder
|
|
|
+import cn.i2edu.dubbing_lib.audioUtils.VideoAudioMixer
|
|
|
+import cn.i2edu.dubbing_lib.audioUtils.compose.AudioComposer
|
|
|
+import cn.i2edu.dubbing_lib.callback.MixinHandlerCallback
|
|
|
+import java.io.File
|
|
|
+
|
|
|
+interface MixinVideoCallBack {
|
|
|
+ fun onResult(resultPath: String)
|
|
|
+ fun onError(message: String)
|
|
|
+}
|
|
|
+
|
|
|
+class MixinVideoUtil private constructor(private val context: Context) : MixinHandlerCallback {
|
|
|
+ private val mixinHandler: MixinHandler<MixinVideoUtil> = MixinHandler(this)
|
|
|
+ // mixin params needs
|
|
|
+ private lateinit var videoId: String
|
|
|
+ private lateinit var bgmPath: String
|
|
|
+ private lateinit var videoPath: String
|
|
|
+ private lateinit var durationList: List<Long>
|
|
|
+ private lateinit var endTimeList: List<Long>
|
|
|
+ private lateinit var audioDecodePaths: List<String>
|
|
|
+ // dir
|
|
|
+ private lateinit var pathBgmDecodeDir: String
|
|
|
+ private lateinit var pathBgmRecordSyncDir: String
|
|
|
+ private lateinit var pathBgmRecordDecodeSyncDir: String
|
|
|
+ private lateinit var pathVideoMixinDir: String
|
|
|
+ // result save
|
|
|
+ private lateinit var decodeBgmPath: String
|
|
|
+ private lateinit var decodeAsyncBgmPath: String
|
|
|
+ private lateinit var encodeAudioWithBgmPath: String
|
|
|
+ private lateinit var mixVideoPath: String
|
|
|
+
|
|
|
+ private var mixinVideoCallBack: MixinVideoCallBack? = null
|
|
|
+ private val pausableThreadPool: PausableThreadPool = PausableThreadPool(1)
|
|
|
+ private val TAG: String = "MixinVideoUtil"
|
|
|
+
|
|
|
+ companion object {
|
|
|
+ @Volatile
|
|
|
+ private var instance: MixinVideoUtil? = null
|
|
|
+
|
|
|
+ fun getInstance(context: Context) = instance
|
|
|
+ ?: synchronized(this) {
|
|
|
+ instance
|
|
|
+ ?: MixinVideoUtil(context)
|
|
|
+ .also { instance = it }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ fun initParams(
|
|
|
+ videoId: String, bgmPath: String, videoPath: String, durationList: List<Long>, endTimeList: List<Long>, audioDecodePaths: List<String>,
|
|
|
+ pathBgmDecodeDir: String, pathBgmRecordSyncDir: String, pathBgmRecordDecodeSyncDir: String, pathVideoMixinDir: String
|
|
|
+ ): MixinVideoUtil {
|
|
|
+ this.videoId = videoId
|
|
|
+ this.bgmPath = bgmPath
|
|
|
+ this.videoPath = videoPath
|
|
|
+ this.durationList = durationList
|
|
|
+ this.endTimeList = endTimeList
|
|
|
+ this.audioDecodePaths = audioDecodePaths
|
|
|
+ this.pathBgmDecodeDir = pathBgmDecodeDir
|
|
|
+ this.pathBgmRecordSyncDir = pathBgmRecordSyncDir
|
|
|
+ this.pathBgmRecordDecodeSyncDir = pathBgmRecordDecodeSyncDir
|
|
|
+ this.pathVideoMixinDir = pathVideoMixinDir
|
|
|
+ return instance!!
|
|
|
+ }
|
|
|
+
|
|
|
+ fun setComposeCallBack(callback: MixinVideoCallBack): MixinVideoUtil {
|
|
|
+ this.mixinVideoCallBack = callback
|
|
|
+ return instance!!
|
|
|
+ }
|
|
|
+
|
|
|
+ fun startMixin() {
|
|
|
+ val message = Message.obtain()
|
|
|
+ message.what = HandlerMessage.START_MIX_IN.value
|
|
|
+ mixinHandler.sendMessage(message)
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun decodeBgmAudio(fileName: String, localPath: String) {
|
|
|
+ if (TextUtils.isEmpty(localPath)) return
|
|
|
+ pausableThreadPool.execute {
|
|
|
+ val decodedPath = doDecode(fileName, localPath, pathBgmDecodeDir)
|
|
|
+ if (decodedPath == null) {
|
|
|
+ this.mixinVideoCallBack?.onError("decodeBgmAudio failed")
|
|
|
+ return@execute
|
|
|
+ }
|
|
|
+ decodeBgmPath = decodedPath
|
|
|
+ // step3 背景音乐与录音合成
|
|
|
+ val message = Message.obtain()
|
|
|
+ message.what = HandlerMessage.AUDIO_DECODE_FINISHED.value
|
|
|
+ mixinHandler.sendMessage(message)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun syncAudios() {
|
|
|
+ // 合成背景音乐和录音 (从尾部开始)
|
|
|
+ val fileDIR = File(pathBgmRecordSyncDir)
|
|
|
+ if (!fileDIR.exists()) {
|
|
|
+ fileDIR.mkdirs()
|
|
|
+ }
|
|
|
+ val mixFilePath = File(fileDIR.absolutePath + "/" + "mixin.mp3")
|
|
|
+ // 合成操作
|
|
|
+ val tempPath = arrayOf<String>(decodeBgmPath)
|
|
|
+ pausableThreadPool.execute {
|
|
|
+ AudioComposer.composeAudio(tempPath[0],
|
|
|
+ audioDecodePaths,
|
|
|
+ mixFilePath.absolutePath,
|
|
|
+ false,
|
|
|
+ endTimeList,
|
|
|
+ durationList,
|
|
|
+ object : AudioComposer.ComposeAudioInterface {
|
|
|
+ override fun composeSuccess(result: String?) {
|
|
|
+ decodeAsyncBgmPath = result!!
|
|
|
+ val message = Message.obtain()
|
|
|
+ message.what = HandlerMessage.AUDIO_SYN_FINISHED.value
|
|
|
+ mixinHandler.sendMessage(message)
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun composeFail() {
|
|
|
+ mixinVideoCallBack?.onError("composeAudio failed")
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun encodeAsynAudio() {
|
|
|
+ pausableThreadPool.execute {
|
|
|
+ val accEncoder = AudioEncoder
|
|
|
+ .createAccEncoder(decodeAsyncBgmPath)
|
|
|
+ val file = File(pathBgmRecordDecodeSyncDir)
|
|
|
+ if (!file.exists()) file.mkdirs()
|
|
|
+ val finalMixPath = pathBgmRecordDecodeSyncDir + "mixinDecode.aac"
|
|
|
+ val isEncodeFinished = !TextUtils.isEmpty(accEncoder.encodeToFile(finalMixPath))
|
|
|
+ if (!isEncodeFinished) {
|
|
|
+ mixinVideoCallBack?.onError("encodeToFile failed")
|
|
|
+ return@execute
|
|
|
+ }
|
|
|
+ encodeAudioWithBgmPath = finalMixPath
|
|
|
+ val message = Message.obtain()
|
|
|
+ message.what = HandlerMessage.AUDIO_ENCODE_FINISHED.value
|
|
|
+ mixinHandler.sendMessage(message)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun mixinAudioAndVideo() {
|
|
|
+ pausableThreadPool.execute {
|
|
|
+ val videoAudioMixer = VideoAudioMixer(context)
|
|
|
+ videoAudioMixer.setListener(object : VideoAudioMixer.VideoAudioMixListener {
|
|
|
+ override fun mixSuccess() {
|
|
|
+ mixVideoPath = pathVideoMixinDir + "${videoId}_mix.mp4"
|
|
|
+ val msg = Message()
|
|
|
+ msg.what = HandlerMessage.AUDIO_MIX_VIDIO_FINISHED.value
|
|
|
+ mixinHandler.sendMessage(msg)
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun mixFail(reason: String?) {
|
|
|
+ mixinVideoCallBack?.onError("mix video and audio failed")
|
|
|
+ }
|
|
|
+ })
|
|
|
+ videoAudioMixer.mux(encodeAudioWithBgmPath, videoPath,
|
|
|
+ "${videoId}_mix.mp4", pathVideoMixinDir)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun audioMixVideoFinish() {
|
|
|
+ mixinVideoCallBack?.onResult(mixVideoPath)
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun doDecode(fileName: String, path: String, saveDirPath: String): String? {
|
|
|
+ try {
|
|
|
+ // 解码后的路径
|
|
|
+ val decodeFile = File(saveDirPath)
|
|
|
+ if (!decodeFile.exists()) {
|
|
|
+ decodeFile.mkdirs()
|
|
|
+ }
|
|
|
+ val finalFile = File(decodeFile.absolutePath + "/" + fileName)
|
|
|
+ if (!finalFile.exists()) {
|
|
|
+ finalFile.createNewFile()
|
|
|
+ }
|
|
|
+ val audioDec = AudioDecoder
|
|
|
+ .createDefualtDecoder(path)
|
|
|
+ audioDec.decodeToFile(finalFile.absolutePath)
|
|
|
+ return finalFile.absolutePath
|
|
|
+ } catch (e: Exception) {
|
|
|
+ e.printStackTrace()
|
|
|
+ }
|
|
|
+ return null
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun onHandleMessage(msg: Message) {
|
|
|
+ when (msg.what) {
|
|
|
+ HandlerMessage.START_MIX_IN.value -> {
|
|
|
+ // step2 解码背景音乐
|
|
|
+ val i = bgmPath.lastIndexOf('/')
|
|
|
+ val name = bgmPath.substring(i)
|
|
|
+ decodeBgmAudio(name, bgmPath)
|
|
|
+ }
|
|
|
+ HandlerMessage.AUDIO_DECODE_FINISHED.value -> {
|
|
|
+ // step3 背景音乐与录音合成
|
|
|
+ syncAudios()
|
|
|
+ }
|
|
|
+ HandlerMessage.AUDIO_SYN_FINISHED.value -> {
|
|
|
+ // step4 编码音频
|
|
|
+ encodeAsynAudio()
|
|
|
+ }
|
|
|
+ HandlerMessage.AUDIO_ENCODE_FINISHED.value -> {
|
|
|
+ // step5 音视频合并
|
|
|
+ mixinAudioAndVideo()
|
|
|
+ }
|
|
|
+ HandlerMessage.AUDIO_MIX_VIDIO_FINISHED.value -> {
|
|
|
+ audioMixVideoFinish()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|