Browse Source

Init: encode media module

liweihao 6 years ago
commit
9aef116483

+ 53 - 0
.gitignore

@@ -0,0 +1,53 @@
+# Built application files
+*.apk
+*.ap_
+
+# Files for the ART/Dalvik VM
+*.dex
+
+# Java class files
+*.class
+
+# Generated files
+bin/
+gen/
+out/
+
+# Gradle files
+.gradle/
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Proguard folder generated by Eclipse
+proguard/
+
+# Log Files
+*.log
+
+# Android Studio Navigation editor temp files
+.navigation/
+
+# Android Studio captures folder
+captures/
+
+# Intellij
+*.iml
+.idea/
+
+# Keystore files
+*.jks
+
+# Windows thumbnail db
+Thumbs.db
+
+# OSX files
+.DS_Store
+
+# Eclipse project files
+.classpath
+.project
+
+keystore.properties
+*.keystore

+ 41 - 0
build.gradle

@@ -0,0 +1,41 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+    compileSdkVersion 28
+
+
+
+    defaultConfig {
+        minSdkVersion 21
+        targetSdkVersion 28
+        versionCode 1
+        versionName "1.0"
+
+        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+    }
+
+}
+
+dependencies {
+    implementation fileTree(include: ['*.jar'], dir: 'libs')
+    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+    implementation 'com.android.support:appcompat-v7:28.0.0-rc01'
+    testImplementation 'junit:junit:4.12'
+    androidTestImplementation 'com.android.support.test:runner:1.0.2'
+    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
+    implementation 'com.writingminds:FFmpegAndroid:0.3.2'
+    api 'io.reactivex.rxjava2:rxandroid:2.1.0'
+    api 'io.reactivex.rxjava2:rxjava:2.2.2'
+}
+repositories {
+    mavenCentral()
+}

+ 21 - 0
proguard-rules.pro

@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile

+ 126 - 0
readme.md

@@ -0,0 +1,126 @@
+# Media Helper Module
+
+## Import
+
+```gradle
+dependencies {
+	implementation project(':exportmedia')
+}
+```
+
+
+
+## Init
+
+需要先在Application中init mediaHelper類別才能使用
+
+```kotlin
+class App : Application() {
+    private val mediaHelper = MediaHelper()
+    
+	override fun onCreate() {
+    	super.onCreate()
+        
+        mediaHelper.init(this)
+	}
+}
+```
+
+
+
+## Audio Editor
+
+剪裁與編輯單音軌時間與音量設定
+
+```kotlin
+val audioEditorBuilder = AudioEditor.Builder(mediaHelper)
+	audioEditorBuilder.editAudioFile = File()
+    audioEditorBuilder.startTime = "00:00:12"
+    audioEditorBuilder.endTime = "00:00:30"
+    audioEditorBuilder.volume = .3f
+
+val audioEditor = audioEditorBuilder.build()
+	audioEditor.output(File())
+		.subscribeOn(Schedulers.newThread())
+    	.observeOn(AndroidSchedulers.mainThread())
+        .subscribe(object : Observer<String> {
+        	override fun onComplete() {}
+            override fun onSubscribe(d: Disposable) {}
+            override fun onNext(t: String) {}
+            override fun onError(e: Throwable) {}
+        })
+```
+
+
+
+## Audio Looper
+
+音軌循環播放設定
+
+```kotlin
+val audioLoopBuilder = AudioLooper.Builder(mediaHelper)
+	audioLoopBuilder.loopAudioFile = File()
+    audioLoopBuilder.loopDuration = 20000 //mSec
+
+val audioLooper = audioLoopBuilder.build()
+	audioLooper.output(File())
+    	.subscribeOn(Schedulers.newThread())
+        .observeOn(AndroidSchedulers.mainThread())
+        .subscribe(object : Observer<String> {
+        	override fun onComplete() {}
+            override fun onSubscribe(d: Disposable) {}
+            override fun onNext(t: String) {}
+            override fun onError(e: Throwable) {}
+       	})
+```
+
+
+
+## Audio Muxer
+
+多音軌合併至單一音軌
+
+```kotlin
+val audioMuxBuilder = AudioMuxer.Builder(mediaHelper)
+	for (file in inputFileArray) {
+    	audioMuxBuilder.addInputSource(file)
+    }
+
+val audioMuxer = audioMuxBuilder.build()
+	audioMuxer.output(File())
+    	.subscribeOn(Schedulers.newThread())
+        .observeOn(AndroidSchedulers.mainThread())
+        .subscribe(object : Observer<String> {
+        	override fun onComplete() {}
+            override fun onSubscribe(d: Disposable) {}
+            override fun onNext(t: String) {}
+            override fun onError(e: Throwable) {}
+       	})
+```
+
+
+
+## Movie Maker
+
+圖片素材與音軌素材編碼成mp4影片
+
+```kotlin
+val movieMakerBuilder = MovieMaker.Builder(mediaHelper)
+	for (bitmap in bitmapArray) {
+    	movieMakerBuilder.addImage(bitmapArray.indexOf(bitmap),bitmap)
+    }
+    movieMakerBuilder.audioStartTime = "00:00:12"
+    movieMakerBuilder.audioFile = File()
+
+val movieMaker = movieMakerBuilder.build()
+	movieMaker.output(File())
+    	.subscribeOn(Schedulers.newThread())
+        .observeOn(AndroidSchedulers.mainThread())
+        .subscribe(object : Observer<String> {
+        	override fun onComplete() {}
+            override fun onSubscribe(d: Disposable) {}
+            override fun onNext(t: String) {}
+            override fun onError(e: Throwable) {}
+        })
+```
+

+ 26 - 0
src/androidTest/java/com/example/exportmedia/ExampleInstrumentedTest.java

@@ -0,0 +1,26 @@
+package com.example.exportmedia;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+    @Test
+    public void useAppContext() {
+        // Context of the app under test.
+        Context appContext = InstrumentationRegistry.getTargetContext();
+
+        assertEquals("com.example.exportmedia.test", appContext.getPackageName());
+    }
+}

+ 7 - 0
src/main/AndroidManifest.xml

@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.exportmedia">
+
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+
+</manifest>

+ 57 - 0
src/main/java/com/example/exportmedia/AudioEditor.kt

@@ -0,0 +1,57 @@
+package com.example.exportmedia
+
+import io.reactivex.Observable
+import java.io.File
+
+class AudioEditor(
+        private val mediaHelper: MediaHelper,
+        private val editAudioFile: File,
+        private val startTime: String,
+        private val endTime: String,
+        private val volume: Float) {
+
+    private constructor(builder: AudioEditor.Builder) : this(
+            builder.mediaHelper,
+            builder.editAudioFile,
+            builder.startTime,
+            builder.endTime,
+            builder.volume
+    )
+
+    fun output(file: File): Observable<String> {
+        return Observable.create {
+            mediaHelper.execute(arrayOf(
+                    "-i", editAudioFile.path,
+                    "-ss", startTime,
+                    "-t", endTime,
+                    "-filter:a", "volume=$volume",
+                    "-y", file.path
+            ), object : MediaCallBack {
+                override fun onExecuteSuccess(message: String?) {
+                    message?.let { it1 -> it.onNext(it1) }
+                }
+
+                override fun onExecuteFailed(message: String?) {
+                    it.onError(Throwable(message))
+                }
+
+                override fun onExecuteFinish() {
+                    it.onComplete()
+                }
+            })
+        }
+    }
+
+    class Builder(internal val mediaHelper: MediaHelper) {
+
+        lateinit var editAudioFile: File
+
+        lateinit var startTime: String
+
+        lateinit var endTime: String
+
+        var volume: Float = 0.0f
+
+        fun build() = AudioEditor(this)
+    }
+}

+ 74 - 0
src/main/java/com/example/exportmedia/AudioLooper.kt

@@ -0,0 +1,74 @@
+package com.example.exportmedia
+
+import android.media.MediaMetadataRetriever
+import io.reactivex.Observable
+import java.io.File
+
+class AudioLooper(
+        private val mediaHelper: MediaHelper,
+        private val loopCount: Int,
+        private val loopAudioFile: File?,
+        private val loopDuration: String
+) {
+
+    private constructor(builder: AudioLooper.Builder) : this(
+            builder.mediaHelper,
+            builder.loopCount,
+            builder.loopAudioFile,
+            builder.getTotalDuration()
+    )
+
+    fun output(file: File): Observable<String> {
+        return Observable.create {
+            mediaHelper.execute(arrayOf(
+                    "-stream_loop", "$loopCount",
+                    "-i", loopAudioFile!!.path,
+                    "-t", loopDuration,
+                    "-c", "copy",
+                    "-y", file.path
+            ), object : MediaCallBack {
+                override fun onExecuteSuccess(message: String?) {
+                    message?.let { it1 -> it.onNext(it1) }
+                }
+
+                override fun onExecuteFailed(message: String?) {
+                    it.onError(Throwable(message))
+                }
+
+                override fun onExecuteFinish() {
+                    it.onComplete()
+                }
+            })
+        }
+    }
+
+    class Builder(internal val mediaHelper: MediaHelper) {
+        var loopAudioFile: File? = null
+            set(value) {
+                field = value
+                val mediaMetadataRetriever = MediaMetadataRetriever()
+                mediaMetadataRetriever.setDataSource(value?.path)
+                sourceDuration = (mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)).toInt()
+            }
+
+        var loopDuration: Int = 0 //mSec
+            set(value) {
+                field = value
+                loopCount = value / sourceDuration + 1
+                loopRemainderDuration = value % sourceDuration
+            }
+
+        internal var loopCount: Int = 0
+
+        private var sourceDuration: Int = 0
+
+        private var loopRemainderDuration: Int = 0
+
+        internal fun getTotalDuration(): String {
+            val totalDuration = (sourceDuration * loopCount + loopRemainderDuration) / 1000
+            return String.format("%02d:%02d:%02d", totalDuration / 3600, (totalDuration % 3600) / 60, (totalDuration % 60))
+        }
+
+        fun build() = AudioLooper(this)
+    }
+}

+ 58 - 0
src/main/java/com/example/exportmedia/AudioMuxer.kt

@@ -0,0 +1,58 @@
+package com.example.exportmedia
+
+import io.reactivex.Observable
+import java.io.File
+import kotlin.collections.ArrayList
+
+class AudioMuxer(
+        private val mediaHelper2: MediaHelper,
+        private val inputFilePathArrayList: ArrayList<String>
+) {
+
+    private constructor(builder: AudioMuxer.Builder) : this(
+            builder.mediaHelper,
+            builder.inputFilePathArrayList
+    )
+
+    fun output(file: File): Observable<String> {
+        val arrayList = ArrayList<String>()
+
+        for (inputFilePath in inputFilePathArrayList) {
+            arrayList.add("-i")
+            arrayList.add(inputFilePath)
+        }
+
+        arrayList.add("-filter_complex")
+        arrayList.add("amix=inputs=${inputFilePathArrayList.size}:duration=longest:dropout_transition=0")
+        arrayList.add("-y")
+        arrayList.add(file.path)
+
+        return Observable.create {
+            mediaHelper2.execute(arrayList.toTypedArray()
+                    , object : MediaCallBack {
+                override fun onExecuteSuccess(message: String?) {
+                    message?.let { it1 -> it.onNext(it1) }
+                }
+
+                override fun onExecuteFailed(message: String?) {
+                    it.onError(Throwable(message))
+                }
+
+                override fun onExecuteFinish() {
+                    it.onComplete()
+                }
+            })
+        }
+    }
+
+    class Builder(internal val mediaHelper: MediaHelper) {
+
+        internal val inputFilePathArrayList = ArrayList<String>()
+
+        fun addInputSource(file: File) {
+            inputFilePathArrayList.add(file.path)
+        }
+
+        fun build() = AudioMuxer(this)
+    }
+}

+ 11 - 0
src/main/java/com/example/exportmedia/MediaCallBack.kt

@@ -0,0 +1,11 @@
+package com.example.exportmedia
+
+interface MediaCallBack {
+
+    fun onExecuteSuccess(message: String?)
+
+    fun onExecuteFailed(message: String?)
+
+    fun onExecuteFinish()
+
+}

+ 58 - 0
src/main/java/com/example/exportmedia/MediaHelper.kt

@@ -0,0 +1,58 @@
+package com.example.exportmedia
+
+import android.content.Context
+import com.github.hiteshsondhi88.libffmpeg.FFmpeg
+import com.github.hiteshsondhi88.libffmpeg.FFmpegExecuteResponseHandler
+import com.github.hiteshsondhi88.libffmpeg.FFmpegLoadBinaryResponseHandler
+import com.github.hiteshsondhi88.libffmpeg.exceptions.FFmpegNotSupportedException
+
+class MediaHelper {
+
+    var context: Context? = null
+
+    private var ffmpeg: FFmpeg? = null
+
+    fun init(context: Context) {
+        this.context = context
+        ffmpeg = FFmpeg.getInstance(context)
+        try {
+            ffmpeg?.loadBinary(object : FFmpegLoadBinaryResponseHandler {
+                override fun onSuccess() {
+                }
+
+                override fun onFailure() {
+                }
+
+                override fun onFinish() {
+                }
+
+                override fun onStart() {
+                }
+            })
+        } catch (e: FFmpegNotSupportedException) {
+
+        }
+    }
+
+    fun execute(cmd: Array<String>, mediaCallBack: MediaCallBack) {
+        ffmpeg?.execute(cmd, object : FFmpegExecuteResponseHandler {
+            override fun onFinish() {
+                mediaCallBack.onExecuteFinish()
+            }
+
+            override fun onSuccess(message: String?) {
+                mediaCallBack.onExecuteSuccess(message)
+            }
+
+            override fun onFailure(message: String?) {
+                mediaCallBack.onExecuteFailed(message)
+            }
+
+            override fun onProgress(message: String?) {
+            }
+
+            override fun onStart() {
+            }
+        })
+    }
+}

+ 103 - 0
src/main/java/com/example/exportmedia/MovieMaker.kt

@@ -0,0 +1,103 @@
+package com.example.exportmedia
+
+import android.graphics.Bitmap
+import io.reactivex.Observable
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.schedulers.Schedulers
+import java.io.ByteArrayOutputStream
+import java.io.File
+import java.io.FileOutputStream
+
+class MovieMaker(
+        private val mediaHelper: MediaHelper,
+        private val imageDir: String,
+        private val fps: Int,
+        private val audioStartTime: String,
+        private val audioFile: File
+) {
+
+    private constructor(builder: Builder) : this(
+            builder.mediaHelper,
+            builder.imageDir,
+            builder.fps,
+            builder.audioStartTime,
+            builder.audioFile
+    )
+
+    fun output(file: File): Observable<String> {
+        return Observable.create {
+            mediaHelper.execute(arrayOf(
+                    "-framerate", fps.toString(),
+                    "-i", imageDir + File.separator + "image%d.png",
+                    "-itsoffset", audioStartTime,
+                    "-i", audioFile.path,
+                    "-c:v", "libx264",
+                    "-pix_fmt", "yuv420p",
+                    "-c:a", "aac",
+                    "-async", "1",
+                    "-y", "-shortest", file.path
+            ), object : MediaCallBack {
+                override fun onExecuteSuccess(message: String?) {
+                    message?.let { it1 -> it.onNext(it1) }
+                }
+
+                override fun onExecuteFailed(message: String?) {
+                    it.onError(Throwable(message))
+                }
+
+                override fun onExecuteFinish() {
+                    it.onComplete()
+                    clear()
+                }
+            })
+        }
+    }
+
+    private fun clear() {
+        val folder = File(imageDir)
+        if (folder.isDirectory) {
+            for (file in folder.listFiles()) {
+                file.delete()
+            }
+        }
+    }
+
+    class Builder(internal val mediaHelper: MediaHelper) {
+
+        var fps = 1
+
+        var audioStartTime = "00:00:00"
+
+        lateinit var audioFile: File
+
+        internal val imageDir = mediaHelper.context?.filesDir?.path!! + File.separator + "EncodeImage"
+
+        fun addImage(index: Int, bitmap: Bitmap) {
+            Observable.create<String> {
+                val byteArrayOutputStream = ByteArrayOutputStream()
+                val file = File(imageDir, "image$index.png")
+                val fileOutputStream = FileOutputStream(file, false)
+
+                try {
+                    bitmap.compress(Bitmap.CompressFormat.PNG, 0, byteArrayOutputStream)
+                    val byteArray = byteArrayOutputStream.toByteArray()
+                    fileOutputStream.write(byteArray)
+                } catch (e: Exception) {
+                    it.onError(e)
+                } finally {
+                    fileOutputStream.flush()
+                    fileOutputStream.close()
+                    bitmap.recycle()
+                }
+
+                it.onNext(file.path)
+                it.onComplete()
+            }
+                    .subscribeOn(Schedulers.io())
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .subscribe()
+        }
+
+        fun build() = MovieMaker(this)
+    }
+}

BIN
src/main/res/drawable/test.png


+ 3 - 0
src/main/res/values/strings.xml

@@ -0,0 +1,3 @@
+<resources>
+    <string name="app_name">exportmedia</string>
+</resources>

+ 17 - 0
src/test/java/com/example/exportmedia/ExampleUnitTest.java

@@ -0,0 +1,17 @@
+package com.example.exportmedia;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+public class ExampleUnitTest {
+    @Test
+    public void addition_isCorrect() {
+        assertEquals(4, 2 + 2);
+    }
+}