├── README.md ├── android ├── .gitignore ├── app │ ├── .gitignore │ ├── CMakeLists.txt │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── tuesda │ │ │ └── gouzi │ │ │ └── ExampleInstrumentedTest.kt │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── cpp │ │ │ └── native-lib.cpp │ │ ├── java │ │ │ └── com │ │ │ │ └── tuesda │ │ │ │ └── gouzi │ │ │ │ ├── GLog.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── PickVideoActivity.kt │ │ │ │ ├── Util.kt │ │ │ │ └── VideoInfo.kt │ │ └── res │ │ │ ├── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── drawable │ │ │ └── ic_launcher_background.xml │ │ │ ├── layout │ │ │ ├── activity_main.xml │ │ │ └── activity_pick_video.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ └── values │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ └── test │ │ └── java │ │ └── com │ │ └── tuesda │ │ └── gouzi │ │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle └── doc ├── h264 └── concept.md ├── issues └── 20181113_部分机型硬压视频和IOS播放器不兼容的问题 │ ├── 20181113_部分机型硬压视频和IOS播放器不兼容的问题.md │ └── imgs │ ├── issue_video.jpg │ ├── issue_video_nal_log.jpg │ ├── mp4_sps_pps_location.jpg │ ├── normal_video_nal_log_1.jpg │ └── normal_video_nal_log_2.jpg └── plan.md /README.md: -------------------------------------------------------------------------------- 1 | * 目的: 一个基于FFmpeg视频转码 App 1.0 版本,名字叫【狗子】,英文名【Gouzi】。 2 | * 时间: 2018.11.8 ~ 2019.2.8 / 3 months / 12 weeks 3 | * 节奏: 每周一个小版本 version 0.1 0.2 0.3 ... 0.11 1.0,每周四发版本。 4 | * ver 0.4: 基本款:能用 5 | * ver 0.1 选视频 6 | * ver 0.2 整个集成 FFmpeg 7 | * ver 0.3 转码 8 | * ver 0.4 视频输出/Log展示 9 | * ver 0.8: FFmpeg 瘦身 10 | * ver 0.5 本地编译 FFmpeg 去掉不必要模块 11 | * ver 0.6 尝试直接调用 FFmpeg 库代码并适配 Android 端代码 12 | * ver 0.7 继续精简 FFmpeg 代码 13 | * ver 0.8 继续精简 Ffmpeg 代码 14 | * ver 1.0: 终极款:轻量 15 | * ver 0.9 - ver 1.0 继续精简 FFmpeg 代码 16 | 17 | 18 | 这个项目的难点不在于 Android 端,而在于 FFmpeg 的适配。由于 FFmpeg 没有 Android 版本,所以必须通过 JNI 方式配合 FFmpeg 编译的 so 包去配合使用。问题在于 FFmpeg 编译的 so 包体积非常大,全量编译包有将近 20M,这对于目前安装包只有 13M 的即刻来说是不可接受的。所以这个项目的大部分精力是用来精简 FFmpeg,计划从小版本 0.5 开始就做这部分。 19 | 20 | 精简的方式主要是自定义编译 FFmpeg 和代码层面对 FFmpeg 进行剪裁。第一部分相对简单,网上也有很多教程。难的是第二种方式,因为这里需要阅读 FFmpeg 源码,而 FFmpeg 几乎都是 c/c++ 写的,我上一次看 c 代码还是在大学时候。但第一种的精简大小很有限,必须尝试第二种。写这个开发计划的目的就是为了督促我知难而上,这部分代码也会放在我 github 的公开库上面,这么做的目的也是为了防止我半途而废,希望最后不要打脸🙏🙏🙏。 -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | **/.idea 3 | .gradle 4 | /local.properties 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild -------------------------------------------------------------------------------- /android/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /android/app/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # For more information about using CMake with Android Studio, read the 2 | # documentation: https://d.android.com/studio/projects/add-native-code.html 3 | 4 | # Sets the minimum version of CMake required to build the native library. 5 | 6 | cmake_minimum_required(VERSION 3.4.1) 7 | 8 | # Creates and names a library, sets it as either STATIC 9 | # or SHARED, and provides the relative paths to its source code. 10 | # You can define multiple libraries, and CMake builds them for you. 11 | # Gradle automatically packages shared libraries with your APK. 12 | 13 | add_library( # Sets the name of the library. 14 | native-lib 15 | 16 | # Sets the library as a shared library. 17 | SHARED 18 | 19 | # Provides a relative path to your source file(s). 20 | src/main/cpp/native-lib.cpp) 21 | 22 | # Searches for a specified prebuilt library and stores the path as a 23 | # variable. Because CMake includes system libraries in the search path by 24 | # default, you only need to specify the name of the public NDK library 25 | # you want to add. CMake verifies that the library exists before 26 | # completing its build. 27 | 28 | find_library( # Sets the name of the path variable. 29 | log-lib 30 | 31 | # Specifies the name of the NDK library that 32 | # you want CMake to locate. 33 | log) 34 | 35 | # Specifies libraries CMake should link to your target library. You 36 | # can link multiple libraries, such as libraries you define in this 37 | # build script, prebuilt third-party libraries, or system libraries. 38 | 39 | target_link_libraries( # Specifies the target library. 40 | native-lib 41 | 42 | # Links the target library to the log library 43 | # included in the NDK. 44 | ${log-lib}) -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | apply plugin: 'kotlin-android' 4 | 5 | apply plugin: 'kotlin-android-extensions' 6 | 7 | android { 8 | compileSdkVersion 28 9 | defaultConfig { 10 | applicationId "com.tuesda.gouzi" 11 | minSdkVersion 21 12 | targetSdkVersion 28 13 | versionCode 1 14 | versionName "1.0" 15 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 16 | externalNativeBuild { 17 | cmake { 18 | cppFlags "" 19 | } 20 | } 21 | } 22 | buildTypes { 23 | release { 24 | minifyEnabled false 25 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 26 | } 27 | } 28 | externalNativeBuild { 29 | cmake { 30 | path "CMakeLists.txt" 31 | } 32 | } 33 | } 34 | 35 | dependencies { 36 | implementation fileTree(dir: 'libs', include: ['*.jar']) 37 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 38 | testImplementation 'junit:junit:4.12' 39 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 40 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 41 | 42 | implementation "androidx.core:core-ktx:1.0.1" 43 | implementation "com.google.android.material:material:1.0.0" 44 | implementation "androidx.constraintlayout:constraintlayout:1.1.3" 45 | } 46 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /android/app/src/androidTest/java/com/tuesda/gouzi/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.tuesda.gouzi 2 | 3 | import android.support.test.InstrumentationRegistry 4 | import android.support.test.runner.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getTargetContext() 22 | assertEquals("com.tuesda.gouzi", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /android/app/src/main/cpp/native-lib.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | extern "C" JNIEXPORT jstring JNICALL 5 | Java_com_tuesda_gouzi_MainActivity_stringFromJNI( 6 | JNIEnv *env, 7 | jobject /* this */) { 8 | std::string hello = "Hello from C++"; 9 | return env->NewStringUTF(hello.c_str()); 10 | } 11 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/tuesda/gouzi/GLog.kt: -------------------------------------------------------------------------------- 1 | package com.tuesda.gouzi 2 | 3 | import android.util.Log 4 | 5 | object GLog { 6 | 7 | private const val TAG = "GLog" 8 | 9 | fun i(tag: String, info: String) { 10 | Log.i(tag, info) 11 | } 12 | 13 | fun e(t: Throwable) { 14 | Log.e(TAG, t.toString()) 15 | } 16 | } -------------------------------------------------------------------------------- /android/app/src/main/java/com/tuesda/gouzi/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.tuesda.gouzi 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import androidx.appcompat.app.AppCompatActivity 6 | import kotlinx.android.synthetic.main.activity_main.* 7 | 8 | class MainActivity : AppCompatActivity() { 9 | 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | super.onCreate(savedInstanceState) 12 | setContentView(R.layout.activity_main) 13 | 14 | // Example of a call to a native method 15 | sample_text.text = stringFromJNI() 16 | startActivity(Intent(this, PickVideoActivity::class.java)) 17 | } 18 | 19 | /** 20 | * A native method that is implemented by the 'native-lib' native library, 21 | * which is packaged with this application. 22 | */ 23 | external fun stringFromJNI(): String 24 | 25 | companion object { 26 | 27 | // Used to load the 'native-lib' library on application startup. 28 | init { 29 | System.loadLibrary("native-lib") 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/tuesda/gouzi/PickVideoActivity.kt: -------------------------------------------------------------------------------- 1 | package com.tuesda.gouzi 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.view.View 7 | import androidx.appcompat.app.AppCompatActivity 8 | 9 | 10 | class PickVideoActivity : AppCompatActivity() { 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | setContentView(R.layout.activity_pick_video) 14 | val btnPickVideo = findViewById(R.id.btn_pick_video) 15 | btnPickVideo.setOnClickListener { 16 | val i = Intent(Intent.ACTION_GET_CONTENT) 17 | i.type = "video/*" 18 | startActivityForResult(Intent.createChooser(i, "Select Video"), PICK_VIDEO_REQUEST_CODE) 19 | } 20 | } 21 | 22 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 23 | super.onActivityResult(requestCode, resultCode, data) 24 | if (resultCode == Activity.RESULT_OK && requestCode == PICK_VIDEO_REQUEST_CODE && data != null) { 25 | data.data?.let { Util.videoInfoOfUri(this, it) }.also { 26 | GLog.i(TAG, "picked video info: $it") 27 | } 28 | } 29 | } 30 | 31 | companion object { 32 | const val TAG = "PickVideoActivity" 33 | const val PICK_VIDEO_REQUEST_CODE = 0 34 | } 35 | } -------------------------------------------------------------------------------- /android/app/src/main/java/com/tuesda/gouzi/Util.kt: -------------------------------------------------------------------------------- 1 | package com.tuesda.gouzi 2 | 3 | import android.content.Context 4 | import android.database.Cursor 5 | import android.net.Uri 6 | import android.provider.DocumentsContract 7 | import android.provider.MediaStore 8 | import androidx.core.database.getIntOrNull 9 | import androidx.core.database.getLongOrNull 10 | import androidx.core.database.getStringOrNull 11 | 12 | object Util { 13 | fun videoInfoOfUri(context: Context, uri: Uri): VideoInfo? { 14 | if (isMediaDocument(uri)) { 15 | val docId = DocumentsContract.getDocumentId(uri) 16 | val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() 17 | val selection = "_id=?" 18 | val selectionArgs = arrayOf(split[1]) 19 | 20 | val projection = arrayOf( 21 | MediaStore.Video.Media.DATA, 22 | MediaStore.Video.Media.WIDTH, 23 | MediaStore.Video.Media.HEIGHT, 24 | MediaStore.Video.Media.DURATION 25 | ) 26 | var cursor: Cursor? = null 27 | try { 28 | cursor = context.contentResolver.query( 29 | MediaStore.Video.Media.EXTERNAL_CONTENT_URI, 30 | projection, selection, selectionArgs, null 31 | ) 32 | cursor?.apply { 33 | if (moveToFirst()) { 34 | val path = getStringOrNull(getColumnIndexOrThrow(MediaStore.Video.Media.DATA)) 35 | val width = getIntOrNull(getColumnIndexOrThrow(MediaStore.Video.Media.WIDTH)) ?: 1920 36 | val height = getIntOrNull(getColumnIndexOrThrow(MediaStore.Video.Media.HEIGHT)) ?: 1080 37 | val durationUs = 38 | getLongOrNull(getColumnIndexOrThrow(MediaStore.Video.Media.DURATION))?.let { it * 1000 } 39 | if (path != null && durationUs != null) { 40 | return VideoInfo(path, width, height, durationUs) 41 | } 42 | } 43 | } 44 | } catch (e: Exception) { 45 | GLog.e(e) 46 | } finally { 47 | cursor?.close() 48 | } 49 | } 50 | return null 51 | } 52 | 53 | private fun isMediaDocument(uri: Uri): Boolean { 54 | return "com.android.providers.media.documents" == uri.authority 55 | } 56 | } -------------------------------------------------------------------------------- /android/app/src/main/java/com/tuesda/gouzi/VideoInfo.kt: -------------------------------------------------------------------------------- 1 | package com.tuesda.gouzi 2 | 3 | class VideoInfo( 4 | private val path: String, 5 | private val width: Int, 6 | private val height: Int, 7 | private val durationUs: Long 8 | ) { 9 | override fun toString(): String { 10 | return "VideoInfo(path='$path', width=$width, height=$height, durationUs=$durationUs)" 11 | } 12 | } -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /android/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/layout/activity_pick_video.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 |